diff --git a/.changeset/discourage-section-blocks.md b/.changeset/discourage-section-blocks.md new file mode 100644 index 000000000..966c5e4a3 --- /dev/null +++ b/.changeset/discourage-section-blocks.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-check-common': minor +--- + +Add `DiscourageSectionBlocks` check that warns when section-defined blocks (blocks with `settings`) are used inside a section schema, suggesting the use of theme blocks instead. diff --git a/packages/theme-check-common/src/checks/discourage-section-blocks/index.spec.ts b/packages/theme-check-common/src/checks/discourage-section-blocks/index.spec.ts new file mode 100644 index 000000000..d6b5cbb3b --- /dev/null +++ b/packages/theme-check-common/src/checks/discourage-section-blocks/index.spec.ts @@ -0,0 +1,143 @@ +import { expect, describe, it } from 'vitest'; +import { runLiquidCheck } from '../../test'; +import { DiscourageSectionBlocks } from './index'; + +const sectionFile = 'sections/my-section.liquid'; + +describe('Module: DiscourageSectionBlocks', () => { + it('reports a section block', async () => { + const source = ` + {% schema %} + { + "name": "My Section", + "blocks": [ + { + "type": "text", + "name": "Text", + "settings": [ + { "type": "text", "id": "content", "label": "Content" } + ] + } + ] + } + {% endschema %} + `; + + const offenses = await runLiquidCheck(DiscourageSectionBlocks, source, sectionFile); + + expect(offenses).toHaveLength(1); + }); + + it('reports each section block independently', async () => { + const source = ` + {% schema %} + { + "name": "My Section", + "blocks": [ + { + "type": "text", + "name": "Text", + "settings": [] + }, + { + "type": "image", + "name": "Image", + "settings": [] + } + ] + } + {% endschema %} + `; + + const offenses = await runLiquidCheck(DiscourageSectionBlocks, source, sectionFile); + + expect(offenses).toHaveLength(2); + }); + + it('does not report theme blocks', async () => { + const source = ` + {% schema %} + { + "name": "My Section", + "blocks": [ + { "type": "@app" } + ] + } + {% endschema %} + `; + + const offenses = await runLiquidCheck(DiscourageSectionBlocks, source, sectionFile); + + expect(offenses).toHaveLength(0); + }); + + it('does not report when there are no blocks', async () => { + const source = ` + {% schema %} + { + "name": "My Section", + "settings": [] + } + {% endschema %} + `; + + const offenses = await runLiquidCheck(DiscourageSectionBlocks, source, sectionFile); + + expect(offenses).toHaveLength(0); + }); + + it('does not report when the schema tag is missing', async () => { + const source = `
No schema here
`; + + const offenses = await runLiquidCheck(DiscourageSectionBlocks, source, sectionFile); + + expect(offenses).toHaveLength(0); + }); + + it('reports section blocks but not theme blocks when mixed', async () => { + const source = ` + {% schema %} + { + "name": "My Section", + "blocks": [ + { "type": "@app" }, + { + "type": "text", + "name": "Text", + "settings": [] + } + ] + } + {% endschema %} + `; + + const offenses = await runLiquidCheck(DiscourageSectionBlocks, source, sectionFile); + + expect(offenses).toHaveLength(1); + }); + + it('does not report for non-section files', async () => { + const source = ` + {% schema %} + { + "name": "My Block", + "settings": [ + { + "type": "text", + "id": "content", + "label": "Content" + } + ] + } + {% endschema %} + `; + + const offenses = await runLiquidCheck( + DiscourageSectionBlocks, + source, + 'blocks/my-block.liquid', + ); + + expect(offenses).toHaveLength(0); + }); +}); diff --git a/packages/theme-check-common/src/checks/discourage-section-blocks/index.ts b/packages/theme-check-common/src/checks/discourage-section-blocks/index.ts new file mode 100644 index 000000000..17315b7bf --- /dev/null +++ b/packages/theme-check-common/src/checks/discourage-section-blocks/index.ts @@ -0,0 +1,57 @@ +import { getLocEnd, getLocStart, nodeAtPath } from '../../json'; +import { getSchema, isSection } from '../../to-schema'; +import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types'; + +export const DiscourageSectionBlocks: LiquidCheckDefinition = { + meta: { + code: 'DiscourageSectionBlocks', + name: 'Discourage Section Blocks', + docs: { + description: 'Discourages the use of section blocks in favor of theme blocks.', + recommended: false, + }, + type: SourceCodeType.LiquidHtml, + severity: Severity.WARNING, + schema: {}, + targets: [], + }, + + create(context) { + if (!isSection(context.file.uri)) { + return {}; + } + + return { + async LiquidRawTag(node) { + if (node.name !== 'schema' || node.body.kind !== 'json') { + return; + } + + const schema = await getSchema(context); + const { ast, validSchema } = schema ?? {}; + + if (!ast || ast instanceof Error || !validSchema || validSchema instanceof Error) { + return; + } + + const offset = node.blockStartPosition.end; + validSchema.blocks?.forEach((block, index) => { + if (!('settings' in block)) { + return; + } + + const astNode = nodeAtPath(ast, ['blocks', String(index), 'type']); + if (!astNode) { + return; + } + + context.report({ + message: 'Consider using a ThemeBlock instead of a SectionBlock.', + startIndex: offset + getLocStart(astNode), + endIndex: offset + getLocEnd(astNode), + }); + }); + }, + }; + }, +}; diff --git a/packages/theme-check-common/src/checks/index.ts b/packages/theme-check-common/src/checks/index.ts index be0370c41..8e84d00be 100644 --- a/packages/theme-check-common/src/checks/index.ts +++ b/packages/theme-check-common/src/checks/index.ts @@ -10,6 +10,7 @@ import { BlockIdUsage } from './block-id-usage'; import { CdnPreconnect } from './cdn-preconnect'; import { ContentForHeaderModification } from './content-for-header-modification'; import { DeprecateBgsizes } from './deprecate-bgsizes'; +import { DiscourageSectionBlocks } from './discourage-section-blocks'; import { DeprecateLazysizes } from './deprecate-lazysizes'; import { DeprecatedFilter } from './deprecated-filter'; import { DeprecatedFontsOnSectionsAndBlocks } from './deprecated-fonts-on-sections-and-blocks'; @@ -81,6 +82,7 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [ ContentForHeaderModification, DeprecateBgsizes, DeprecateLazysizes, + DiscourageSectionBlocks, DeprecatedFilter, DeprecatedFontsOnSectionsAndBlocks, DeprecatedFontsOnSettingsData,