Skip to content

feat(theme-check-common): add LiquidSchema check visitor method#1194

Open
SinhSinhAn wants to merge 1 commit intoShopify:mainfrom
SinhSinhAn:feat/liquid-schema-visitor
Open

feat(theme-check-common): add LiquidSchema check visitor method#1194
SinhSinhAn wants to merge 1 commit intoShopify:mainfrom
SinhSinhAn:feat/liquid-schema-visitor

Conversation

@SinhSinhAn
Copy link
Copy Markdown
Contributor

@SinhSinhAn SinhSinhAn commented Apr 23, 2026

What is this PR?

Implements the LiquidSchema check node type proposed in #817. It abstracts the repeated preamble found in every schema-validating Liquid check.

Before

Every schema check currently opens with the same ten-line preamble:

create(context) {
  return {
    async LiquidRawTag(node) {
      if (node.name !== 'schema' || node.body.kind !== 'json') return;

      const offset = node.blockStartPosition.end;
      const schema = await getSchema(context);

      const { validSchema, ast } = schema ?? {};
      if (!validSchema || validSchema instanceof Error) return;
      if (!ast || ast instanceof Error) return;

      // actual check logic...
    },
  };
}

At least nine checks in the codebase repeat this boilerplate verbatim.

After

create(context) {
  return {
    async LiquidSchema({ schema, validSchema, ast, offset }) {
      // actual check logic...
    },
  };
}

The LiquidSchema callback fires once per {% schema %} tag in a section or theme-block file, after the schema has been parsed and validated. The payload exposes:

  • node = the original LiquidRawTag node
  • schema = full SectionSchema | ThemeBlockSchema, for isSectionSchema / isBlockSchema narrowing
  • validSchema= narrowed to Section.Schema | ThemeBlock.Schema (never Error)
  • ast = JSON AST (never Error)
  • offset= character offset of the JSON inside the Liquid source

Implementation

  • Added LiquidSchemaNode type and extended Check<LiquidHtml> with an optional LiquidSchema method via a CheckExtraMethods parameter on the Check<T> type.
  • Added wrapLiquidSchema helper in packages/theme-check-common/src/wrap-liquid-schema.ts. It transforms a check's LiquidSchema method into a standard LiquidRawTag visitor by baking in the preamble. When a check declares both LiquidRawTag and LiquidSchema, the existing LiquidRawTag runs first for every raw tag, then the schema preamble runs and LiquidSchema fires only for valid schemas.
  • createCheck in index.ts runs wrapLiquidSchema once per check instance, so every LiquidHtml check gets the new method for free. No changes to visitLiquid, no new node type in the parser, no context threading through the visitor.

Why a wrapper rather than a visitor change

visitLiquid only has access to (node, check). Resolving a schema requires context.getBlockSchema / context.getSectionSchema, which are per-check dependencies captured in the create(context) closure. Threading context through the visitor would be a larger API change and would couple the visitor to a check-level concept. Wrapping at createCheck time keeps the visitor clean and keeps the new method a first-class part of the check contract.

LiquidSchema does not fire for

  • {% schema %} tags outside sections/ and blocks/ (matches existing getSchema behavior)
  • Tags whose body is not valid JSON (body.kind !== 'json')
  • Tags whose parsed schema is an Error (invalid JSON or validation failure)
  • Any non-schema LiquidRawTag (stylesheet, javascript, comment, raw, etc.)

Tests

Added 5 dedicated unit tests in wrap-liquid-schema.spec.ts:

  • Fires once per valid {% schema %} in a section
  • Fires for theme blocks
  • Does not fire in snippet files
  • Does not fire for non-schema raw tags
  • Composes correctly with an existing LiquidRawTag method

Migrated ValidSettingsKey as a demonstration. Its full 17-test suite passes unchanged, proving the API is a drop-in replacement.

Full regression run: 861 theme-check tests pass (856 existing + 5 new), 162 parser tests pass, 141 prettier-plugin tests pass. Zero regressions across 1,164 tests.

Follow-up opportunities

Eight additional checks currently follow the verbatim preamble and could be migrated in follow-up PRs once this API is accepted:

  • valid-visible-if
  • valid-block-target
  • valid-local-blocks
  • valid-schema-name
  • valid-schema-translations
  • schema-presets-block-order
  • schema-presets-static-blocks (partial migration — it uses both LiquidRawTag for schema and LiquidTag for content_for)
  • deprecated-fonts-on-sections-and-blocks

I kept migrations out of this PR to minimize scope. Happy to migrate any/all in this PR or separate follow-ups based on reviewer preference.

Closes #817

Introduces a `LiquidSchema(node)` method on `Check<LiquidHtml>` that
replaces the repeated preamble found in every schema-validating check:

  async LiquidRawTag(node) {
    if (node.name !== 'schema' || node.body.kind !== 'json') return;
    const offset = node.blockStartPosition.end;
    const schema = await getSchema(context);
    const { validSchema, ast } = schema ?? {};
    if (!validSchema || validSchema instanceof Error) return;
    if (!ast || ast instanceof Error) return;
    // ... check logic ...
  }

becomes:

  async LiquidSchema({ validSchema, ast, offset, schema }) {
    // ... check logic ...
  }

Implementation:

- New `LiquidSchemaNode` type exposing `{ node, schema, validSchema,
  ast, offset }`, with `validSchema` and `ast` narrowed to their
  non-Error variants.
- New `wrapLiquidSchema` helper that transforms a check's
  `LiquidSchema` method into a composed `LiquidRawTag` visitor. This
  keeps `visitLiquid` oblivious to schemas and avoids threading
  `context` through the visitor.
- `createCheck` runs `wrapLiquidSchema` once per check instance, so
  every LiquidHtml check gets the new method for free.
- When a check declares both `LiquidRawTag` and `LiquidSchema`, the
  existing `LiquidRawTag` runs first for every raw tag, then the
  schema preamble runs and `LiquidSchema` fires only for valid
  schemas.
- Checks not in section or theme-block files never fire
  `LiquidSchema` (matches the existing `getSchema` behaviour).

Migrated `ValidSettingsKey` as a demonstration. Its 17 tests pass
unchanged, proving the API is a drop-in replacement.

Closes Shopify#817
@SinhSinhAn SinhSinhAn requested a review from a team as a code owner April 23, 2026 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: LiquidSchema(node) check type for SourceType.LiquidHtml

1 participant