npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@magnolia/cli-prototype-template

v1.0.0-preview.1

Published

PrototypeTemplate simplifies creation process of components and pages

Downloads

60

Readme

PrototypeTemplate class

The @magnolia/cli-prototype-template package simplifies creation process of components and pages for @magnolia/cli-create-component and @magnolia/cli-create-page.

Overview

Every prototype should be provided within a separate package which extends the PrototypeTemplate class to use predefined class parameters and methods.

getProgram()

Returns program with predefined options.

PrototypeTemplate

PrototypeTemplate arguments

  • pathToPrototype (abstract property): This property must be explicitly defined by subclasses, pointing to the location of their prototypes, e.g.: this.pathToPrototype = path.join(__dirname, 'resources', options.prototype);
  • prototypesRequirements (abstract property): This property must be explicitly defined by subclasses, e.g.: this.prototypesRequirements = {"headless": true, "availableTypes": ["js", "jsx", "tsx"], "defaultType": "tsx"}
  • templateData: A set of key-value pairs used by Handlebars. The default properties included in this object are:
    • name: This value is derived from the <name> parameter provided by the user.
    • exportName: This is also derived from the <name> parameter, mirroring the name property.
    • lightModuleName: This value is determined based on the directory of the light module, which is specified by the -lp or --lmPath option.
    • dialog: This property combines the lightModuleName and name values in the format ${lightModuleName}:${options.context}/${name}.
    • magnoliaHost: Points to http://localhost:8080
  • preparedComponent: Processed prototype files
  • options: Options passed from user
  • args: Arguments modifying behaviour

PrototypeTemplate methods

  • async start(): Promise

    starts the create component process

  • stop(): void

    Throws error to stop the process.

  • async loadExternalMethods(): Promise

    Checks for config.js file in prototype folder. If found:

    • merges this.args.prototypeTemplateOptions with object from getPrototypeTemplateOption function from config.js
    • merges this.templateData with object from getTemplateData function from config.js
    • passes super().function to all exposed functions in config.js
  • async prototypeTemplateArgs(): Promise

    Prepares arguments and:

    • merges this.args.prototypeTemplateOptions with object passed by -ta, --templateArgs
    • merges this.templateData with object passed by -td, --templateData
  • async prepareComponentFromPrototype(prototypeTemplatePath: string, componentDestinationPath: string, type: string): Promise

    Prepares files from prototype and passed them with handlebars to this.preparedComponent, each file has the following structure:

    // 'spa' for file from spa folder, or 'lm' for file from spa folder
    type: string; 
    // unprocessed source file path, e.g.: /PATH/TO/RESORCES/SPA/_default/spa/js/{{name}}.js.hbs
    srcPath: string;
    // unprocessed destination file path, e.g.: /PATH/TO/PROJECT/COMPONENTS/{{name}}.js.hbs
    destPath: string;
    // processed destination file path, e.g.: /PATH/TO/PROJECT/COMPONENTS/newComponent.js
    hbDestPath: string;
    // unprocessed file content, e.g.:
    // import React from 'react';
    // const {{name}} = props => <h2>{props.text}</h2>;
    // export default {{exportName}};
    content: string;
    // processed file content, e.g.:
    // import React from 'react';
    // const newComponent = props => <h2>{props.text}</h2>;
    // export default newComponent;
    hbContent: string;
  • async preventDuplicateComponentCreation(): Promise

    Cycles throw this.preparedComponent and if any hbDestPath exists it stops the component creation

  • async createComponent(): Promise

    Cycles throw this.preparedComponent, and creates every hbDestPath with content from hbContent.

  • async handleComponentMapping(): Promise

    In case of headless frameworks, calls following three functions.

  • async buildComponentMappingString(): Promise

    Sets this.args.componentMapping to "${this.templateData.dialog}": ${this.templateData.exportName}

  • async buildImportString(): Promise

    Sets this.args.import to import ${exportComponentName} from '${importSource}'\n

  • async writeComponentMapping(): Promise

    Adds this.args.import and this.args.componentMapping to the file containing componentsMapping object.

config.js file in prototype

It is possible to create config.js file in each prototype. This file allows to pass custom this.args.prototypeTemplateOptions object and this.templateData by creating those two functions:

  • prototypeTemplateOptions, e.g.
    export function getPrototypeTemplateOption() {
      // Sets `importSource` to `{{name}}.${this.options.type}.hbs` to determine which path should be used in components mapping file
      // Overrides default value of `removeExtension` to true, to remove externsion from importString in components mapping file
      return {
        "headlessConfig": {
          "importSource": `{{name}}.${this.options.type}.hbs`,
          "removeExtension": true
        }
      }
    }
  • getTemplateData, e.g.:
    function kebabize(str) {
      return str.split('').map((letter, idx) => {
        return letter.toUpperCase() === letter
          ? `${idx !== 0 ? '-' : ''}${letter.toLowerCase()}`
          : letter;
      }).join('');
    }
      
    export function getTemplateData() {
      // Add "selector" with coresponding value to be used with Handlebars
      return {
        "selector": kebabize(this.templateData.name)
      }
    }

It is also possible to override all PrototypeTemplate methods to modify the behaviour of component creation, except start, and stop method, e.g.:

  • Override createComponent to add other file after a component is created:
    // WARNING: requires node v18 and higher to use native fetch function
    import path from "path";
    import * as fs from "fs";
    import { pipeline } from 'stream/promises';
      
    export async function createComponent(superCreateComponent) {
        // Call original createComponent from PrototypeTemplate class
        superCreateComponent();
        // Download random image from unsplash.com and add it images folder
        try {
            const url = `https://source.unsplash.com/random/200x200`;
            const response = await fetch(url);
            if (!response.ok) {
                console.error(`Failed to fetch image: ${response.statusText}`);
                return
            }
            const folderPath = path.join(this.options.spaPath, 'images')
            if (!fs.existsSync(folderPath)) {
                fs.mkdirSync(folderPath, {recursive: true});
            }
            const filePath = path.join(folderPath, `${this.templateData.name}.png`);
            const fileStream = fs.createWriteStream(filePath);
            await pipeline(response.body, fileStream);
            console.log(`Image successfully downloaded to: ${filePath}`);
        } catch (error) {
            console.error('Error downloading random image:', error);
        }
    }

Example

example-components project

Project structure

/package.json
/tsconfig.json
/index.ts
/example-prototypes.ts
/resources
|--/components
|  |--/_default
|  |  |-- ...
|  |--/complex
|  |  |-- ...
|--/pages
|  |--/_default
|  |  |-- ...
|  |--/complex
|  |  |-- ...

This example is in git:

  • package.json:

    {
      "name": "example-prototypes",
      "version": "1.0.0",
      "type": "module",
      "bin": {
        "create": "./dist/index.js"
      },
      "main": "./dist/index.js",
      "types": "./dist/index.d.ts",
      "scripts": {
        "build": "tsc && cpy resources dist"
      },
      "devDependencies": {
        "@types/node": "^20.11.5",
        "cpy-cli": "^5.0.0"
      },
      "dependencies": {
        "@magnolia/cli-prototype-template": "^1.0.0"
      }
    }
  • tsconfig.json:

    {
      "compilerOptions": {
        "target": "es6",
        "module": "esnext",
        "outDir": "dist",
        "declaration": true,
        "moduleResolution": "node",
        "strict": true,
        "esModuleInterop": true,
        "resolveJsonModule": true
      },
      "include": ["**/*"],
      "exclude": [
        "node_modules",
        "dist",
        "resources"
      ]
    }
  • index.ts:

    #!/usr/bin/env node
    
    import { ExamplePrototypes } from "./example-prototypes.js"
    import {getProgram} from "@magnolia/cli-prototype-template";
    
    const program = getProgram()
    
    const exampleComponent = new ExamplePrototypes(program.args[0], program.opts());
    exampleComponent.start();
  • example-prototypes.ts

    import {PrototypeOptions, PrototypesRequirements, PrototypeTemplate} from "@magnolia/cli-prototype-template";
    import path from "path";
    import {fileURLToPath} from "url";
    
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    
    export class ExamplePrototypes extends PrototypeTemplate {
        pathToPrototype: string;
        prototypesRequirements: PrototypesRequirements;
        constructor(name: string, options: PrototypeOptions) {
          super(name, options);
          // options.context determines 'pages' or 'components' prototypes
          // options.prototype is the name of the prototype ('_default' or 'complex' in our case)
          this.pathToPrototype = path.join(__dirname, 'resources', options.context, options.prototype);
          this.prototypesRequirements = {"headless": true, "availableTypes": ["js", "jsx", "tsx"], "defaultType": "tsx"}
        }
    }
  • resources folder, with 'components' and 'pages' prototypes:

    /...
    /resources
    |--/components
    |  |--/_default
    |  |  |-- ...
    |  |--/complex
    |  |  |-- ...
    |--/pages
    |  |--/_default
    |  |  |-- ...
    |  |--/complex
    |  |  |-- ...

components prototypes

_default component prototype

/...
/resources
|--/components
|  |--/_default
|  |  |--/spa
|  |  |  |--/js
|  |  |  |  |--/{{name}}.js.hbs
|  |  |  |--/jsx
|  |  |  |  |--/{{name}}.jsx.hbs
|  |  |  |--/tsx
|  |  |  |  |--/{{name}}.tsx.hbs
  • _default/spa/js/{{name}}.js.hbs and _default/spa/jsx/{{name}}.jsx.hbs files:

    import React from 'react';
      
    const {{name}} = props => <h2>{props.text}</h2>;
      
    export default {{exportName}};
  • _default/spa/tsx/{{name}}.tsx.hbs file:

    import React from 'react';
      
    interface I{{name}}Props {
      text: string;
    }
      
    const {{name}} = (props: I{{name}}Props) => <h2>{props.text}</h2>;
      
    export default {{exportName}};

complex component prototype with config.js

/...
/resources
|--/components
|  |--/complex
|  |  |--/light-module
|  |  |  |--/dialogs
|  |  |  |  |--/components
|  |  |  |  |  |--/{{name}}.yaml.hbs
|  |  |  |--/templates
|  |  |  |  |--/components
|  |  |  |  |  |--/{{name}}.yaml.hbs
|  |  |--/spa
|  |  |  |--/js
|  |  |  |  |--/{{name}}
|  |  |  |  |  |--/{{name}}.js.hbs
|  |  |  |  |  |--/{{name}}.stories.js.hbs
|  |  |  |  |  |--/{{name}}.model.js.hbs
|  |  |  |--/jsx
|  |  |  |  |--/{{name}}
|  |  |  |  |  |--/{{name}}.jsx.hbs
|  |  |  |  |  |--/{{name}}.stories.jsx.hbs
|  |  |  |  |  |--/{{name}}.model.js.hbs
|  |  |  |--/tsx
|  |  |  |  |--/{{name}}
|  |  |  |  |  |--/{{name}}.tsx.hbs
|  |  |  |  |  |--/{{name}}.stories.tsx.hbs
|  |  |  |  |  |--/{{name}}.model.ts.hbs
|  |  |--/config.js
  • complex/light-module/dialogs/components/{{name}}.yaml.hbs file:

    label: {{name}}
    form:
      properties:
        title:
          label: title
          $type: textField
          i18n: true
        description:
          label: description
          $type: textField
          i18n: true
        image:
          label: image
          $type: damLinkField
  • complex/light-module/templates/components/{{name}}.yaml file:

    title: {{name}}
    dialog: {{dialog}}
  • complex/spa/js/{{name}}/{{name}}.stories.js and complex/spa/jsx/{{name}}/{{name}}.stories.jsx files:

    import React from 'react';
    import {{name}} from './{{name}}';
    import { {{name}}Model } from './{{name}}.model';
      
    export default {
      title: '{{name}}',
      component: {{name}},
    };
      
    const Template = (args) => <{{name}} {...args} />;
      
    export const Default = Template.bind({});
    Default.args = new {{name}}Model('Title text', 'Description text', {"@link": "Add link to image here, e.g.: \"/magnoliaAuthor/dam/jcr:7279fb99-094f-452b-ac4c-3b4defb56203\""});
  • complex/spa/js/{{name}}/{{name}}.js and complex/spa/jsx/{{name}}/{{name}}.jsx files:

    import React from 'react';
    import PropTypes from 'prop-types';
    import { {{name}}Model } from './{{name}}.model';
    import randomImage from '../images/{{name}}.png';
      
    const {{name}} = ( props ) => {
      return (
        <div>
          <img src={ '{{magnoliaHost}}' + props.image['@link']} alt="image" />
          <h2>{props.name}</h2>
          <p>{props.description}</p>
          <img src={randomImage} alt="randomImage" />
        </div>
      );
    };
      
    {{name}}.propTypes = PropTypes.instanceOf({{name}}Model).isRequired;
      
    export default {{name}};
  • complex/spa/js/{{name}}/{{name}}.model.js and complex/spa/jsx/{{name}}/{{name}}.model.js files:

    export class {{name}}Model {
      constructor(name, description, image) {
        this.name = name;
        this.description = description;
        this.image = image;
      }
    }
  • complex/spa/tsx/{{name}}/{{name}}.stories.tsx file:

    import React from 'react';
    import {{name}}, { {{name}}Props } from './{{name}}';
    import { {{name}}Model } from './{{name}}.model';
    import { Story, Meta } from '@storybook/react';
      
    export default {
        title: '{{name}}',
        component: {{name}},
    } as Meta;
      
    const Template: Story<{{name}}Props> = (args) => <{{name}} {...args} />;
      
    export const Default = Template.bind({});
    Default.args = new {{name}}Model('Title text', 'Description text', {"@link": "Add link to image here, e.g.: \"/magnoliaAuthor/dam/jcr:7279fb99-094f-452b-ac4c-3b4defb56203\""});
  • complex/spa/tsx/{{name}}/{{name}}.tsx file:

    import React from 'react';
    import { {{name}}Model } from './{{name}}.model';
    import randomImage from '../images/{{name}}.png';
      
    const {{name}}: React.FC<{{name}}Model> = ( props ) => {
        return (
            <div>
                <img src={ '{{magnoliaHost}}' + props.image['@link']} alt="image" />
                <h2>{props.name}</h2>
                <p>{props.description}</p>
                <img src={randomImage} alt="randomImage" />
            </div>
        );
    };
      
    export default {{name}};
  • complex/spa/tsx/{{name}}/{{name}}.model.ts file:

    export class {{name}}Model {
        name: string;
        description: string;
        image: any;
      
        constructor(name: string, description: string, image: any) {
            this.name = name;
            this.description = description;
            this.image = image;
        }
    }
  • complex/config.js file:

    // WARNING: requires node v18 and higher to use native fetch function
    import path from "path";
    import * as fs from "fs";
    import { pipeline } from 'stream/promises';
      
    export function getPrototypeTemplateOption() {
        return {
            headlessConfig: {
                importSource: `{{name}}.${this.options.type}.hbs`
            }
        }
    }
      
    export async function createComponent(superCreateComponent) {
        // Call original createComponent from PrototypeTemplate class
        superCreateComponent();
        // Download random image from https://source.unsplash.com and add it to images folder
        try {
            const url = `https://source.unsplash.com/random/200x200`;
            const response = await fetch(url);
            if (!response.ok) {
                console.error(`Failed to fetch image: ${response.statusText}`);
                return
            }
            const folderPath = path.join(this.options.spaPath, 'images')
            if (!fs.existsSync(folderPath)) {
                fs.mkdirSync(folderPath, {recursive: true});
            }
            const filePath = path.join(folderPath, `${this.templateData.name}.png`);
            const fileStream = fs.createWriteStream(filePath);
            await pipeline(response.body, fileStream);
            console.log(`Image successfully downloaded to: ${filePath}`);
        } catch (error) {
            console.error('Error downloading random image:', error);
        }
    }

    NOTE: The "importSource" parameter in the getPrototypeTemplateOption function is essential for specifying which file should be used as the import source in the components mapping file. This specification is crucial because the complex prototype includes multiple files within the spa folder.

pages prototypes

_default pages prototype

/...
/resources
|--/page
|  |--/_default
|  |  |--/light-module
|  |  |  |--/dialogs
|  |  |  |  |--/pages
|  |  |  |  |  |--/{{name}}.yaml.hbs
|  |  |  |--/templates
|  |  |  |  |--/pages
|  |  |  |  |  |--/{{name}}.yaml.hbs
|  |  |--/spa
|  |  |  |--/js
|  |  |  |  |--/{{name}}.js.hbs
|  |  |  |--/jsx
|  |  |  |  |--/{{name}}.jsx.hbs
|  |  |  |--/tsx
|  |  |  |  |--/{{name}}.tsx.hbs
  • _default/light-module/dialogs/pages/{{name}}.yaml.hbs file:

    label: Page Properties
    form:
      properties:
        title:
          label: Title
          $type: textField
          i18n: true
  • _default/light-module/templates/pages/{{name}}.yaml.hbs file:

    renderType: spa
    class: info.magnolia.rendering.spa.renderer.SpaRenderableDefinition
      
    title: {{name}}
    dialog: {{dialog}}
    baseUrl: http://localhost:3000
    routeTemplate: '/{language}\{{@path}}'
    # templateScript: /{{lightModuleName}}/webresources/build/index.html
      
    areas:
      main:
        title: Main Area
      
      extras:
        title: Extras Area
  • _default/spa/js/{{name}}.js.hbs and _default/spa/jsx/{{name}}.jsx.hbs files:

    import React from 'react';
    import { EditableArea } from '@magnolia/react-editor';
      
    const {{name}} = props => {
      const { main, extras, title } = props;
      
      return (
        <div className="{{name}}">
          <div>[Basic Page]</div>
          <h1>{title || props.metadata['@name']}</h1>
      
          <main>
            <div>[Main Area]</div>
            {main && <EditableArea className="Area" content={main} />}
          </main>
      
          <div>
            <div>[Secondary Area]</div>
            {extras && <EditableArea className="Area" content={extras} />}
          </div>
        </div>
      )
    };
      
    export default {{name}};
  • _default/spa/tsx/{{name}}.tsx.hbs file:

    import React from 'react';
    // @ts-ignore
    import { EditableArea } from '@magnolia/react-editor';
      
    interface I{{name}} {
      metadata?: any;
      main?: any;
      extras?: any;
      title?: string;
    }
      
    const {{name}} = (props: I{{name}}) => {
      const { main, extras, title } = props;
      
      return (
        <div className="{{name}}">
          <div>[Basic Page]</div>
          <h1>{title || props.metadata['@name']}</h1>
      
          <main>
            <div>[Main Area]</div>
            {main && <EditableArea className="Area" content={main} />}
          </main>
      
          <div>
            <div>[Secondary Area]</div>
            {extras && <EditableArea className="Area" content={extras} />}
          </div>
        </div>
      )
    };
      
    export default {{name}};

complex pages prototype with config.js

/...
/resources
|--/components
|  |--/complex
|  |  |--/light-module
|  |  |  |--/dialogs
|  |  |  |  |--/pages
|  |  |  |  |  |--/{{name}}.yaml.hbs
|  |  |  |--/templates
|  |  |  |  |--/pages
|  |  |  |  |  |--/{{name}}.yaml.hbs
|  |  |--/spa
|  |  |  |--/js
|  |  |  |  |--/{{name}}
|  |  |  |  |  |--/{{name}}.js.hbs
|  |  |  |  |  |--/{{name}}.model.js.hbs
|  |  |  |--/jsx
|  |  |  |  |--/{{name}}
|  |  |  |  |  |--/{{name}}.jsx.hbs
|  |  |  |  |  |--/{{name}}.model.js.hbs
|  |  |  |--/tsx
|  |  |  |  |--/{{name}}
|  |  |  |  |  |--/{{name}}.tsx.hbs
|  |  |  |  |  |--/{{name}}.model.tsx.hbs
|  |  |--/config.js
  • complex/light-module/dialogs/pages/{{name}}.yaml.hbs file:

    label: Page Properties
    form:
      properties:
        title:
          $type: textField
          i18n: true
        navigationTitle:
          $type: textField
          i18n: true
        windowTitle:
          $type: textField
          i18n: true
        abstract:
          $type: textField
          rows: 5
          i18n: true
        keywords:
          $type: textField
          rows: 3
          i18n: true
        description:
          $type: textField
          rows: 5
          i18n: true
      layout:
        $type: tabbedLayout
        tabs:
          - name: tabMain
            fields:
              - name: title
              - name: navigationTitle
              - name: windowTitle
              - name: abstract
          - name: tabMeta
            fields:
              - name: keywords
              - name: description
  • complex/light-module/templates/pages/{{name}}.yaml.hbs file:

    renderType: spa
    class: info.magnolia.rendering.spa.renderer.SpaRenderableDefinition
      
    title: {{name}}
    dialog: {{dialog}}
    baseUrl: http://localhost:3000
    routeTemplate: '/{language}\{{@path}}'
    # templateScript: /{{lightModuleName}}/webresources/build/index.html
      
    areas:
      main:
        title: Main Area
      
      extras:
        title: Extras Area
  • complex/spa/js/{{name}}/{{name}}.js.hbs and complex/spa/jsx/{{name}}/{{name}}.jsx.hbs files:

    import React from 'react';
    import PropTypes from 'prop-types';
    import { EditableArea } from '@magnolia/react-editor';
    import { Helmet } from 'react-helmet';
    import { {{name}}Model } from './{{name}}.model'
      
    const {{name}} = props => {
      const { main, extras, title, navigationTitle, description, keywords, abstract } = props;
      
      return (
        <div className="{{name}}">
          <Helmet>
            <title>{title}</title>
            <meta name="description" content={description} />
            <meta name="keywords" content={keywords} />
            <meta name="abstract" content={abstract} />
          </Helmet>
          <h1>{navigationTitle}</h1>
          <p>{description}</p>
      
          <div>[Basic Page]</div>
          <h1>{title || props.metadata['@name']}</h1>
      
          <main>
            <div>[Main Area]</div>
            {main && <EditableArea className="Area" content={main} />}
          </main>
      
          <div>
            <div>[Secondary Area]</div>
            {extras && <EditableArea className="Area" content={extras} />}
          </div>
        </div>
      )
    };
      
    {{name}}.propTypes = PropTypes.instanceOf({{name}}Model).isRequired;
      
    export default {{name}};
  • complex/spa/js/{{name}}/{{name}}.model.js.hbs and complex/spa/jsx/{{name}}/{{name}}.model.js.hbs files:

    export class {{name}}Model {
      constructor(metadata, main, extras, title, navigationTitle, windowTitle, abstract, keywords, description) {
        this.metadata = metadata;
        this.main = main;
        this.extras = extras;
        this.title = title;
        this.navigationTitle = navigationTitle;
        this.windowTitle = windowTitle;
        this.abstract = abstract;
        this.keywords = keywords;
        this.description = description;
      }
    }
  • complex/spa/js/{{name}}/{{name}}.js.hbs and complex/spa/jsx/{{name}}/{{name}}.jsx.hbs files:

    import React from 'react';
    import PropTypes from 'prop-types';
    //@ts-ignore
    import { EditableArea } from '@magnolia/react-editor';
    import { Helmet } from 'react-helmet';
    import { {{name}}Model } from './{{name}}.model'
      
    const {{name}} = props => {
      const { main, extras, title, navigationTitle, description, keywords, abstract } = props;
      
      return (
        <div className="{{name}}">
          <Helmet>
            <title>{title}</title>
            <meta name="description" content={description} />
            <meta name="keywords" content={keywords} />
            <meta name="abstract" content={abstract} />
          </Helmet>
          <h1>{navigationTitle}</h1>
          <p>{description}</p>
      
          <div>[Basic Page]</div>
          <h1>{title || props.metadata['@name']}</h1>
      
          <main>
            <div>[Main Area]</div>
            {main && <EditableArea className="Area" content={main} />}
          </main>
      
          <div>
            <div>[Secondary Area]</div>
            {extras && <EditableArea className="Area" content={extras} />}
          </div>
        </div>
      )
    };
      
    {{name}}.propTypes = PropTypes.instanceOf({{name}}Model).isRequired;
      
    export default {{name}};
  • complex/spa/tsx/{{name}}/{{name}}.model.ts.hbs file:

    export class testPageModel {
      constructor(metadata: any, main: any, extras: any, title: string, navigationTitle: string, windowTitle: string, abstract: string, keywords: string, description: string) {
        this.metadata = metadata;
        this.main = main;
        this.extras = extras;
        this.title = title;
        this.navigationTitle = navigationTitle;
        this.windowTitle = windowTitle;
        this.abstract = abstract;
        this.keywords = keywords;
        this.description = description;
      }
    }
  • complex/spa/config.js.hbs file:

export function getPrototypeTemplateOption() {
    return {
        headlessConfig: {
            importSource: `{{name}}.${this.options.type}.hbs`
        }
    }
}