2.1.4 • Published 4 months ago

@alleyford/schematic v2.1.4

Weekly downloads
-
License
ISC
Repository
github
Last release
4 months ago

Schematic

A more sane approach for writing custom schema definitions within Shopify themes.

Working with Shopify schema sucks

Working with syntatically strict JSON in Shopify themes sucks. You can't put schema into partials to be included, breaking all hopes of modularity or code reuse, which means intensely duplicated schemas and inconsistency in naming and labeling. Worse, if you have big schemas (like icon lists) that get updated regularly, you have to update definitions everywhere they exist, which is a giant mess.

This helps a little bit

Schematic helps you write Shopify theme schema in JS, not JSON. You can build arrays or objects however you want with normal import/require. Use functions. Do whatever. This is a standalone node executable that will compile & swap schema definitions for sections whenever it's run. That means it edits the actual .liquid file for simplicity and compatibility with task runners, build managers, Shopify CLI theme serving, and whatever else.

To use

Locally: npm i -D @alleyford/schematic

Globally: npm i -g @alleyford/schematic

Running: npx schematic

By default, Schematic wants to be executed in the theme root and looks for schema definitions in src/schema. You can change this by passing arguments to the Schematic constructor, if invoking directly and not via npx:

const app = new Schematic({
  paths: {
    config: './config', // directory for shopify configuration files
    sections: './sections', // directory for section files
    snippets: './snippets', // directory for snippet files
    locales: './locales', // directory for locale files
    schema: './src/schema', // directory with all of the schema definitions for schematic to consume
  },
  localization: {
    file: './snippets/p-app-localization.liquid', // file to scan to replace magic comment with localization strings
    expression: 'window.app.copy = %%json%%;', // the expression to write for localization strings
  },
  verbose: true, // show details in console if true, otherwise silent except on error
});

Then you're free to create schema definitions, either in full, partials, or whatever else. Here's some example Schematic schema JS for a Shopify section which renders a single icon and a heading.

First, some JS which exports objects we can reuse:

// ./src/schema/global.js

module.exports = {
  iconWidth: {
    type: 'select',
    id: 'icon_width',
    label: 'Icon width',
    options: [
      {
        value: '96px',
        label: '96px',
      },
      {
        value: '64px',
        label: '64px',
      },
      {
        value: '48px',
        label: '48px',
      },
      {
        value: '32px',
        label: '32px',
      },
      {
        value: '24px',
        label: '24px',
      },
      {
        value: '16px',
        label: '16px',
      },
    ],
    default: '32px',
  },
};

Then, the JS which produces the full Shopify section JSON schema:

// ./src/schema/iconAndHeading.js

const global = require('./global');

module.exports = {
  name: 'Icon and heading',
  tag: 'section',
  enabled_on: {
    templates: [
      'collection',
      'product',
    ],
  },
  presets: [{
    name: 'Icon and heading',
  }],
  settings: [
    {
      type: 'text',
      id: 'heading',
      label: 'Heading',
    },
    {
      type: 'select',
      id: 'icon',
      label: 'Icon',
      options: [
        {
          label: '',
          value: 'None',
        },
        {
          label: 'Heart',
          value: 'heart',
        },
        {
          label: 'Fire',
          value: 'fire',
        },
      ],
    },
    global.iconWidth,
  ],
};

To tie this to Shopify and tell Schematic what to build, you edit your section liquid files with a magic comment with the entry point for the schema definition.

// ./sections/iconAndHeading.liquid, bottom of file

{%- comment -%} schematic iconAndHeading {%- endcomment -%}

This will find ./src/schema/iconAndHeading.js, build the JSON, and either draw in the schema tag with full definition, or replace the existing schema definition with the new one.

If you've named your schema and section files the same (./src/schema/iconAndHeading.js, ./sections/iconAndHeading.liquid), then you can simply use:

{%- comment -%} schematic {%- endcomment -%}

And Schematic will intuit the path for the schema definition from the filename.

Set this up as an executable

The most straight-forward way to use this by installing globally (or as a dev dependency) and running npx schematic within the Shopify theme directory. That assumes you have ./src/schema/ set up with your schema definitions.

If you need more customization, or your directory structure for schema definitions is different, you can create a node executable:

touch schematic && chmod +x schematic

// ./schematic

#!/usr/bin/env node --no-warnings
const { Schematic } = require('@alleyford/schematic');

const app = new Schematic({
  paths: {
    sections: './sections', // directory to look for sections
    schema: './src/schema', // directory with schema definitions
  },
  verbose: true, // show details in console if true, otherwise silent except on error
});

app.run();

Then when you want to build, run the command ./schematic.

Using the built-in executable

The best way to run this is npx schematic. If you need to apply options, like verbosity or paths, you can set them in your environment:

VariableExample value
SCHEMATIC_VERBOSE0 or 1
SCHEMATIC_PATH_CONFIGPath to to the Shopify config/ directory
SCHEMATIC_PATH_SECTIONSPath to to the Shopify sections/ directory
SCHEMATIC_PATH_SNIPPETSPath to to the Shopify snippets/ directory
SCHEMATIC_PATH_SCHEMAPath to to the directory with our schema definitions

An example command to run Schematic without verbose output and a schema directory out of the Shopify theme root: SCHEMATIC_VERBOSE=0 SCHEMATIC_PATH_SCHEMA=../src/schema npx schematic

Built-in components and functions

Since it also sucks creating a bunch of schema from scratch for every project, Schematic comes with some nice generic definitions and helper methods to use out of the box. The app variable derived from the package will contain everything you can use. We'll tie this together at the end to show it in use.

Sidebar methods

MethodArgumentsDescriptionExample
headercontent, infoReturns sidebar header objectapp.header('Icon', "Icons help convey meaning in a visual and quick way.")
paragraphcontentReturns sidebar paragraph objectapp.paragraph("This adds some helpful copy to the sidebar.")

Quality of life methods & objects

Method or propertyArgumentsDescriptionExample
sectionname, tagReturns starter object for section schema (name, preset name, and optionally tag)...app.section('Icon and heading', 'div')
optionid, labelReturns option objectapp.option('text-left', 'Left align')
typesReturns object of camelCase input typesapp.types.bgColor; // returns "color_background"
templatesReturns object of camelCase template namesapp.templates.activateAccount; // returns "customers/activate_account"
commonReturns object of common, pre-build generic inputsapp.common.colorBackgroundSelector
normalTemplatesReturns an array of normal templates (article, index, page, product, blog, collection, collection list, gift card)app.normalTemplates
allTemplatesReturns an array of all Shopify templatesapp.allTemplates
fontSidebar input object for selecting font style (sans, serif, script)app.font
textAlignSidebar input object for selecting text alignment (left, center, right, justify, start, end)app.textAlign
orientationSidebar input object for selecting orientation (left, right)app.orientation
imageStyleSidebar input object for selecting image style (cover, full)app.imageStyle
defaultsContains objects for often repeated blank valuesoptions: [app.defaults.blank, ...]

Input methods

MethodArgumentsDescriptionExample
maketype, propsFactory to return input settings objectsapp.make('richtext', {id: 'copy', label: "Copy"})
inputtype, propsAlias of makeapp.input('blog', {label: "Select the blog for related reading"})
prefixOptionsprefix, optionsReturns array with option values prefixedapp.prefixOptions('fill-', ['red', 'green', 'blue'])
suffixOptionssuffix, optionsReturns array with option values suffixedapp.suffixOptions('-500', ['red', 'green', 'blue'])
enumerateId(obj|obj, ..), indexReturns object(s) with the id suffixed with enumerationapp.enumerateId(app.imageSelector, 1) app.enumerateId(app.imageSelector, 2)
changeIdobj, idReturns object with the id property changedapp.changeId(app.imageSelector, 'backgroundImage')
changeLabelobj, labelReturns object with the label property changedapp.changeLabel(app.imageSelector, 'Background image')
changeDefaultobj, defaultReturns object with the default property changedapp.changeDefault(app.number, 42)
changeLimitobj, limitReturns object with the limit property changedapp.changeLimit(app.collectionsSelector, 3)
changePropertyobj, key, valueReturns object with the property changedapp.changeProperty(app.number, 'id', 'articleLimit')
changeProperties(obj|obj, ..), propsReturns object(s) with the properties changedapp.changeProperties(app.number, {id: 'articleLimit', default: 3})
removePropertyobj, keyReturns object with the property deletedapp.removeProperty(app.productSelector, 'limit')
removePropertiesobj, keysReturns object with the properties deletedapp.removeProperties(app.productSelector, ['limit', 'default'])

Default input objects

PropertyAliases
articleSelectorarticlePicker, app.make('article')
blogSelectorblogPicker, app.make('blog')
pageSelectorpagePicker, app.make('page')
menuSelectormenuPicker, app.make('menu'), app.make('linkList')
collectionSelectorcollectionPicker, app.make('collection')
collectionsSelectorcollectionsPicker, collectionList, app.make('collections')
productSelectorproductPicker, app.make('product')
productsSelectorproductsPicker, productList, app.make('products')
colorSelectorcolorPicker, app.make('color')
colorBackgroundSelectorcolorBackgroundPicker, backgroundColorSelector, backgroundColorPicker, bgColorSelector, bgColorPicker, app.make('bgColor')
urlSelectorurlPicker, app.make('url')
backgroundImageSelectorbackgroundImagePicker, backgroundImage, bgImage, app.make('bgImage')
fontSelectorfontPicker, app.make('font')
imageSelectorimagePicker, app.make('image')
subheadingapp.make('subheading')
headingapp.make('heading')
textapp.make('text')
copyapp.make('copy')
htmlapp.make('html')
liquidapp.make('liquid')
videoLinkapp.make('videoLink')
checkboxapp.make('checkbox')
numberapp.make('number')
rangeapp.make('range', {...})
selectdropdown, app.make('select')
textinput, app.make('text')
textareaapp.make('textarea')

Using these helpers, we can significantly reduce the amount of JS we have to write:

// ./src/schema/iconAndHeading.js

const { app } = require('@alleyford/schematic');
const global = require('./global');

module.exports = {
  ...app.section('Icon and heading'),
  settings: [
    app.heading,
    app.make('select', {
      id: 'icon',
      label: 'Icon',
      options: [
        app.defaults.none,
        app.option('heart', 'Heart icon'),
        app.option('fire', 'Fire icon'),
      ],
    }),
    global.iconWidth,
  ],
};

Sometimes you have a specific code reason to change the ID or another property for a definition, but otherwise keep the definition the same. Schematic supports some helper methods to adjust definitions on the fly:

  //...

  settings: [
    app.changeId(app.heading, 'heading_left'),
    app.changeId(app.heading, 'heading_right'),
    app.changeProperties(app.heading, {
      id: 'heading_center',
      label: 'Center heading',
      default: 'Welcome to Zombocom',
    }),
  ],

  //...

Or to make drawing panel schema a little easier:

  //...

  settings: [
    app.paragraph("Icons and headings really make the world go 'round."),

    app.header('Left icon'),
    app.changeId(app.heading, 'heading_left'),
    app.changeId(common.iconWidth, 'icon_left'),

    app.header('Right icon'),
    app.changeId(app.heading, 'heading_right'),
    app.changeId(common.iconWidth, 'icon_right'),

    app.header('Center heading', "A center heading helps focus attention. Loudly."),
    app.changeProperties(app.heading, {
      id: 'heading_center',
      label: 'Center heading',
      default: 'Welcome to Zombocom',
    }),
  ],

  //...
};

Bundling common patterns

Sections sometimes have fields that always go together, like a heading, subheading, and copy. Or a reusable CTA. Instead of defining every one repeatedly, you can use the spread (...) operator when pulling them from a definition.

// ./src/schema/components/cta.js

module.exports = [ // default export of an array of objects
  {
    type: 'text',
    id: 'cta_copy',
    label: 'Button copy',
  },
  {
    type: 'url',
    id: 'cta_link',
    label: 'Button destination',
  },
  {
    type: 'select',
    id: 'cta_style',
    label: 'Button style',
    options: [
      {
        value: 'button',
        label: 'Button',
      },
      {
        value: 'link',
        label: 'Link',
      },
      {
        value: 'hidden',
        label: 'None',
      },
    ],
    default: 'link',
  },
];

Include the file in your schema, and (...cta) will draw in all three definitions:

// ./src/schema/iconAndHeading.js

const { app } = require('@alleyford/schematic');
const global = require('./global');
const cta = require('./components/cta');

module.exports = {
  ...app.section('Icon and heading'),
  settings: [
    app.header('Left icon'),
    app.changeId(app.heading, 'heading_left'),
    app.changeId(common.iconWidth, 'icon_left'),

    app.header('Right icon'),
    app.changeId(app.heading, 'heading_right'),
    app.changeId(common.iconWidth, 'icon_right'),

    ...cta,
  ],
};

If you have other section schema definitions which could use the same CTA pattern, you can just include the same file and its definition using the spread operator. If you ever update the original defintion, running Schematic will update all instances where it's being used.

Auto-write boilerplate switchboard code

I think it's a helpful design pattern to keep most logic out of sections and offload it to snippets. Since you're auto-generating schema, it may make sense in some cases to also auto-generate the switchboard code to render section schema to its identically-named snippet.

You do this by passing an argument to the magic comment, like so:

// ./sections/iconAndHeading.liquid, bottom of file

{%- comment -%} schematic iconAndHeading writeCode {%- endcomment -%}

Or if your files are named the same across schema, sections, and snippets:

{%- comment -%} schematic writeCode {%- endcomment -%}

Running Schematic then produces the compiled schema, plus a line to render the snippet with all that schema automatically mapped:

{%-

    render 'iconAndHeading'
        heading_left: section.settings.heading_left
        icon_left: section.settings.icon_left
        heading_right: section.settings.heading_right
        icon_right: section.settings.icon_right
        cta_copy: section.settings.cta_copy
        cta_link: section.settings.cta_link
        cta_style: section.settings.cta_style

-%}

Note: When building custom Shopify themes, it's strongly recommended to use the section/snippet separation pattern, and to use writeCode where possible to handle connecting variables to snippets as schema changes over time. Using writeCode will wipe out any other code in the file.

Scaffolding pattern

Reiterating the above, you can use Schematic to create placeholder files for this pattern to scaffold things out when building custom sections: npx schematic scaffold iconAndHeading

This will create three files:

./sections/iconAndHeader.liquid
./snippets/iconAndHeader.liquid
./src/schema/iconAndHeader.js

The section file will contain the magic comment to make Schematic work, the snippet will be blank, and the schema file will contain code to include the Schematic helper methods and types.

Other ways to use

The approach is simple and can be worked into whatever setup you have for dev. Because it writes back to the existing .liquid files, be wary of infinite loops when including this in an automatic build step.

Running on a single section file

Schematic supports running on a single section file instead of scanning the entire contents of the project. To invoke, run npx schematic section (path/to/file). This can also be invoked in code through Schematic.runSection(filePath).

Schematic has planned support for running on individual configuration and localization files.

Using Schematic for settings_schema.json

Schematic will automatically write config/settings_schema.json for you if it detects the presence of a settings_schema.js file in your schema directory.

The file should export an array of objects that match what would be manually defined in settings_schema.json:

// ./src/schema/settings_schema.js

const { app } = require('@alleyford/schematic');

module.exports = [
  {
    name: 'theme_info',
    theme_name: 'Magic fungus',
    //...
  },
  {
    name: 'Globals',
    settings: {
      app.header('Free shipping threshold'),
      app.make(app.types.range, {
        id: 'free_shipping_threshold',
        label: 'Amount to trigger free shipping',
        min: 0,
        max: 500,
        step: 10,
        unit: '$',
        default: 150,
      }),
      //...
    },
  },
  //...
];

Using Schematic for localization features

Schematic will automatically write locale JSON for you if it detects the presence of locales/[lang].js in your schema directory.

The files should export an object that matches what would be manually defined in a locale file:

// ./src/schema/locales/en.js

module.exports = {
  cart: {
    error: "Something went wrong -- please try again.",
    quantityError: "You may only add {{ quantity }} of this item to your cart.",
    //...
  },
  //...
};

Schematic will also try to find a magic comment and replace it with localized strings. This works as follows: 1. Schematic looks at the localization.file and scans for a liquid comment with schematicLocalization 2. It then writes JSON into the localization.expression string so client side code can consume it

Before running Schematic:

// ./snippets/p-app-localization.liquid

{%- comment -%} schematicLocalization {%- endcomment -%}

After:

// ./snippets/p-app-localization.liquid

{%- comment -%} schematicLocalization {%- endcomment -%}
window.app.copy = {
  "cart.error": {{ 'cart.error' | t | json }},
  "cart.quantityError": {{ 'cart.quantityError' | t | json }},
  // ...
};

In the above example, window.app.copy is coming from the Schematic configuration option for localization.expression. The %%json%% value in that expression is needed and will be replaced with the localization strings.

Thanks

2.1.4

4 months ago

2.1.3

4 months ago

2.1.2

5 months ago

2.1.1

5 months ago

2.1.0

5 months ago

2.0.7

7 months ago

2.0.9

6 months ago

2.0.8

7 months ago

2.0.3

7 months ago

2.0.2

9 months ago

2.0.5

7 months ago

2.0.4

7 months ago

2.0.6

7 months ago

2.0.1

9 months ago

1.1.8

1 year ago

1.1.7

1 year ago

1.1.6

1 year ago

1.1.5

1 year ago

1.1.3

1 year ago

1.1.2

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago