@magnolia/cli-prototype-template
TypeScript icon, indicating that this package has built-in type declarations

1.0.0-preview.1 • Public • Published

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`
        }
    }
}

Readme

Keywords

none

Package Sidebar

Install

npm i @magnolia/cli-prototype-template

Weekly Downloads

15

Version

1.0.0-preview.1

License

SEE LICENSE IN LICENSE.txt

Unpacked Size

106 kB

Total Files

17

Last publish

Collaborators

  • magnolia