From d4dcdf9356a411e6f14939f9d16cf5429a97e8d9 Mon Sep 17 00:00:00 2001 From: web-orbit12 <115070295+web-orbit12@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:25:30 +0400 Subject: [PATCH] Add SchemaPresetsAndDefault check Reports section schemas that define both presets and default, which `shopify theme dev` rejects with "Invalid schema: cannot define both 'default' and 'presets'". Fixes #750 --- .changeset/schema-presets-and-default.md | 8 ++ .../theme-check-common/src/checks/index.ts | 2 + .../schema-presets-and-default/index.spec.ts | 84 +++++++++++++++++++ .../schema-presets-and-default/index.ts | 46 ++++++++++ packages/theme-check-node/configs/all.yml | 3 + .../theme-check-node/configs/recommended.yml | 3 + 6 files changed, 146 insertions(+) create mode 100644 .changeset/schema-presets-and-default.md create mode 100644 packages/theme-check-common/src/checks/schema-presets-and-default/index.spec.ts create mode 100644 packages/theme-check-common/src/checks/schema-presets-and-default/index.ts diff --git a/.changeset/schema-presets-and-default.md b/.changeset/schema-presets-and-default.md new file mode 100644 index 000000000..5e3030673 --- /dev/null +++ b/.changeset/schema-presets-and-default.md @@ -0,0 +1,8 @@ +--- +'@shopify/theme-check-common': minor +'@shopify/theme-check-node': minor +--- + +Add `SchemaPresetsAndDefault` check + +Reports section schemas that define both `presets` and `default`, matching the platform validation that rejects them with `Invalid schema: cannot define both 'default' and 'presets'`. diff --git a/packages/theme-check-common/src/checks/index.ts b/packages/theme-check-common/src/checks/index.ts index b1f9c6632..2abc25fda 100644 --- a/packages/theme-check-common/src/checks/index.ts +++ b/packages/theme-check-common/src/checks/index.ts @@ -34,6 +34,7 @@ import { MissingTemplate } from './missing-template'; import { OrphanedSnippet } from './orphaned-snippet'; import { PaginationSize } from './pagination-size'; import { ParserBlockingScript } from './parser-blocking-script'; +import { SchemaPresetsAndDefault } from './schema-presets-and-default'; import { SchemaPresetsBlockOrder } from './schema-presets-block-order'; import { SchemaPresetsStaticBlocks } from './schema-presets-static-blocks'; import { RemoteAsset } from './remote-asset'; @@ -128,6 +129,7 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [ OrphanedSnippet, PaginationSize, ParserBlockingScript, + SchemaPresetsAndDefault, SchemaPresetsBlockOrder, SchemaPresetsStaticBlocks, RemoteAsset, diff --git a/packages/theme-check-common/src/checks/schema-presets-and-default/index.spec.ts b/packages/theme-check-common/src/checks/schema-presets-and-default/index.spec.ts new file mode 100644 index 000000000..dd8d3da3f --- /dev/null +++ b/packages/theme-check-common/src/checks/schema-presets-and-default/index.spec.ts @@ -0,0 +1,84 @@ +import { expect, describe, it } from 'vitest'; +import { highlightedOffenses, runLiquidCheck } from '../../test'; +import { SchemaPresetsAndDefault } from './index'; + +const DEFAULT_FILE_NAME = 'sections/file.liquid'; + +describe('Module: SchemaPresetsAndDefault', () => { + it('reports an error when a section defines both presets and default', async () => { + const sourceCode = ` + {% schema %} + { + "name": "Test section", + "presets": [{ "name": "Preset 1" }], + "default": { "settings": {} } + } + {% endschema %}`; + + const offenses = await runLiquidCheck(SchemaPresetsAndDefault, sourceCode, DEFAULT_FILE_NAME); + expect(offenses).toHaveLength(1); + expect(offenses[0].message).toEqual( + "Invalid schema: cannot define both 'default' and 'presets'", + ); + + const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); + expect(highlights).toEqual(['{ "settings": {} }']); + }); + + it('reports an error when presets is an empty array', async () => { + const sourceCode = ` + {% schema %} + { + "name": "Test section", + "presets": [], + "default": {} + } + {% endschema %}`; + + const offenses = await runLiquidCheck(SchemaPresetsAndDefault, sourceCode, DEFAULT_FILE_NAME); + expect(offenses).toHaveLength(1); + }); + + it('reports no error when a section only defines presets', async () => { + const sourceCode = ` + {% schema %} + { + "name": "Test section", + "presets": [{ "name": "Preset 1" }] + } + {% endschema %}`; + + const offenses = await runLiquidCheck(SchemaPresetsAndDefault, sourceCode, DEFAULT_FILE_NAME); + expect(offenses).toHaveLength(0); + }); + + it('reports no error when a section only defines default', async () => { + const sourceCode = ` + {% schema %} + { + "name": "Test section", + "default": { "settings": {} } + } + {% endschema %}`; + + const offenses = await runLiquidCheck(SchemaPresetsAndDefault, sourceCode, DEFAULT_FILE_NAME); + expect(offenses).toHaveLength(0); + }); + + it('does not run on theme blocks', async () => { + const sourceCode = ` + {% schema %} + { + "name": "Test block", + "presets": [{ "name": "Preset 1" }] + } + {% endschema %}`; + + const offenses = await runLiquidCheck( + SchemaPresetsAndDefault, + sourceCode, + 'blocks/file.liquid', + ); + expect(offenses).toHaveLength(0); + }); +}); diff --git a/packages/theme-check-common/src/checks/schema-presets-and-default/index.ts b/packages/theme-check-common/src/checks/schema-presets-and-default/index.ts new file mode 100644 index 000000000..3a0639c44 --- /dev/null +++ b/packages/theme-check-common/src/checks/schema-presets-and-default/index.ts @@ -0,0 +1,46 @@ +import { getLocEnd, getLocStart, nodeAtPath } from '../../json'; +import { getSchema, isSectionSchema } from '../../to-schema'; +import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types'; + +export const SchemaPresetsAndDefault: LiquidCheckDefinition = { + meta: { + code: 'SchemaPresetsAndDefault', + name: 'Prevent sections from defining both presets and default', + docs: { + description: + 'Reports section schemas that define both `presets` and `default`, which the platform rejects.', + recommended: true, + url: 'https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/schema-presets-and-default', + }, + type: SourceCodeType.LiquidHtml, + severity: Severity.ERROR, + schema: {}, + targets: [], + }, + + create(context) { + return { + async LiquidRawTag(node) { + if (node.name !== 'schema' || node.body.kind !== 'json') return; + + const schema = await getSchema(context); + if (!isSectionSchema(schema)) return; + + const { validSchema, ast } = schema; + if (!validSchema || validSchema instanceof Error) return; + if (!ast || ast instanceof Error) return; + if (validSchema.presets === undefined || validSchema.default === undefined) return; + + const defaultNode = nodeAtPath(ast, ['default']); + if (!defaultNode) return; + + const offset = node.blockStartPosition.end; + context.report({ + message: "Invalid schema: cannot define both 'default' and 'presets'", + startIndex: offset + getLocStart(defaultNode), + endIndex: offset + getLocEnd(defaultNode), + }); + }, + }; + }, +}; diff --git a/packages/theme-check-node/configs/all.yml b/packages/theme-check-node/configs/all.yml index e275c92de..b62a0f464 100644 --- a/packages/theme-check-node/configs/all.yml +++ b/packages/theme-check-node/configs/all.yml @@ -160,6 +160,9 @@ ReservedDocParamNames: SchemaOncePerFile: enabled: true severity: 0 +SchemaPresetsAndDefault: + enabled: true + severity: 0 SchemaPresetsBlockOrder: enabled: true severity: 1 diff --git a/packages/theme-check-node/configs/recommended.yml b/packages/theme-check-node/configs/recommended.yml index 15ef418a3..9f284370b 100644 --- a/packages/theme-check-node/configs/recommended.yml +++ b/packages/theme-check-node/configs/recommended.yml @@ -138,6 +138,9 @@ ReservedDocParamNames: SchemaOncePerFile: enabled: true severity: 0 +SchemaPresetsAndDefault: + enabled: true + severity: 0 SchemaPresetsBlockOrder: enabled: true severity: 1