Skip to content

Commit

Permalink
Merge pull request #659 from Shopify/miaz/check-preset-static-blocks
Browse files Browse the repository at this point in the history
static blocks content_for check
  • Loading branch information
miazbikowski authored Dec 10, 2024
2 parents b35a6c2 + 6f1862c commit 89e2dbe
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 16 deletions.
7 changes: 7 additions & 0 deletions .changeset/silly-parrots-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@shopify/theme-check-common': minor
'@shopify/theme-check-node': minor
'theme-check-vscode': minor
---

Add `SchemaPresetsStaticBlocks` check
3 changes: 2 additions & 1 deletion packages/theme-check-common/src/checks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { AssetSizeAppBlockJavaScript } from './asset-size-app-block-javascript';
import { AssetSizeCSS } from './asset-size-css';
import { AssetSizeJavaScript } from './asset-size-javascript';
import { BlockIdUsage } from './block-id-usage';
``;
import { CaptureOnContentForBlock } from './capture-on-content-for-block';
import { CdnPreconnect } from './cdn-preconnect';
import { ContentForHeaderModification } from './content-for-header-modification';
Expand All @@ -27,6 +26,7 @@ import { MissingTemplate } from './missing-template';
import { PaginationSize } from './pagination-size';
import { ParserBlockingScript } from './parser-blocking-script';
import { SchemaPresetsBlockOrder } from './schema-presets-block-order';
import { SchemaPresetsStaticBlocks } from './schema-presets-static-blocks';
import { RemoteAsset } from './remote-asset';
import { RequiredLayoutThemeObject } from './required-layout-theme-object';
import { TranslationKeyExists } from './translation-key-exists';
Expand Down Expand Up @@ -74,6 +74,7 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [
PaginationSize,
ParserBlockingScript,
SchemaPresetsBlockOrder,
SchemaPresetsStaticBlocks,
RemoteAsset,
RequiredLayoutThemeObject,
TranslationKeyExists,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getLocEnd, getLocStart, nodeAtPath } from '../../json';
import { basename } from '../../path';
import { isBlock, isSection } from '../../to-schema';
import { getSchema } from '../../to-schema';
import {
ArrayNode,
Context,
Expand Down Expand Up @@ -28,26 +27,14 @@ export const SchemaPresetsBlockOrder: LiquidCheckDefinition = {
},

create(context) {
function getSchema() {
const name = basename(context.file.uri, '.liquid');
switch (true) {
case isBlock(context.file.uri):
return context.getBlockSchema?.(name);
case isSection(context.file.uri):
return context.getSectionSchema?.(name);
default:
return undefined;
}
}

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

const offset = node.blockStartPosition.end;
const schema = await getSchema();
const schema = await getSchema(context);
const { validSchema, ast } = schema ?? {};
if (!validSchema || validSchema instanceof Error) return;
if (!ast || ast instanceof Error) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { expect, describe, it } from 'vitest';
import { highlightedOffenses, runLiquidCheck, check } from '../../test';
import { SchemaPresetsStaticBlocks } from './index';

const DEFAULT_FILE_NAME = 'sections/file.liquid';

describe('Module: SchemaPresetsStaticBlocks', () => {
it('reports no errors when there are {% content_for "block" ... %} for each static block in the preset blocks array', async () => {
const sourceCode = `
{% content_for "block" type:"text" id: "block-1" %}
{% content_for "block" type:"icon" id: "block-2" %}
{% schema %}
{
"name": "Test section",
"blocks": [{"type": "@theme"}],
"presets": [
{
"name": "Preset with two static blocks",
"blocks": [
{
"type": "text",
"static": true,
"id": "block-1"
},
{
"type": "icon",
"static": true,
"id": "block-2"
}
]
}
]
}
{% endschema %}`;

const offenses = await runLiquidCheck(SchemaPresetsStaticBlocks, sourceCode, DEFAULT_FILE_NAME);
expect(offenses).toHaveLength(0);
});

it('reports an error when there are {% content_for "block" ... %} missing for static blocks in the preset blocks array', async () => {
const sourceCode = `
{% content_for "block" type:"text" id:"block-1" %}
{% comment %} here we are missing the other content_for block for block-2 {% endcomment %}
{% schema %}
{
"name": "Test section",
"blocks": [{"type": "@theme"}],
"presets": [
{
"name": "Preset with two static blocks",
"blocks": [
{
"type": "text",
"static": true,
"id": "block-1"
},
{
"type": "icon",
"static": true,
"id": "block-2"
}
]
}
]
}
{% endschema %}`;

const offenses = await runLiquidCheck(SchemaPresetsStaticBlocks, sourceCode, DEFAULT_FILE_NAME);
expect(offenses).toHaveLength(1);
console.log(offenses);
expect(offenses[0].message).toEqual(
'Static block block-2 is missing a corresponding content_for "block" tag.',
);

const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses);
expect(highlights).toHaveLength(1);
});

it('reports no errors when there are {% content_for "block" ... %} for each static block in the preset blocks hash', async () => {
const sourceCode = `
{% content_for "block" type:"text" id: "block-1" %}
{% content_for "block" type:"icon" id: "block-2" %}
{% schema %}
{
"name": "Test section",
"blocks": [{"type": "@theme"}],
"presets": [
{
"name": "Preset with two static blocks",
"blocks": {
"block-1": {
"type": "text",
"static": true
},
"block-2": {
"type": "icon",
"static": true
}
}
}
]
}
{% endschema %}`;

const offenses = await runLiquidCheck(SchemaPresetsStaticBlocks, sourceCode, DEFAULT_FILE_NAME);
expect(offenses).toHaveLength(0);
});

it('reports an error when there are {% content_for "block" ... %} missing for static blocks in the preset blocks hash', async () => {
const sourceCode = `
{% content_for "block" type:"text" id:"block-1" %}
{% comment %} here we are missing the other content_for block for block-2 {% endcomment %}
{% schema %}
{
"name": "Test section",
"blocks": [{"type": "@theme"}],
"presets": [
{
"name": "Preset with two static blocks",
"blocks": {
"block-1": {
"type": "text",
"static": true
},
"block-2": {
"type": "icon",
"static": true
}
}
}
]
}
{% endschema %}`;

const offenses = await runLiquidCheck(SchemaPresetsStaticBlocks, sourceCode, DEFAULT_FILE_NAME);
expect(offenses).toHaveLength(1);
console.log(offenses);
expect(offenses[0].message).toEqual(
'Static block block-2 is missing a corresponding content_for "block" tag.',
);

const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses);
expect(highlights).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { NamedTags, NodeTypes } from '@shopify/liquid-html-parser';
import { getLocEnd, getLocStart, nodeAtPath } from '../../json';
import { getSchema } from '../../to-schema';
import { ArrayNode, LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
import { isContentForBlock } from '../../utils/markup';

export const SchemaPresetsStaticBlocks: LiquidCheckDefinition = {
meta: {
code: 'SchemaPresetsStaticBlocks',
name: 'Ensure the preset static blocks are used in the liquid',
docs: {
description:
'Warns if a preset static block does not have a corresponding content_for "block" tag.',
recommended: true,
url: 'https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/schema-presets-static-blocks',
},
type: SourceCodeType.LiquidHtml,
severity: Severity.ERROR,
schema: {},
targets: [],
},

create(context) {
type contentForBlock = {
id: string;
type: string;
};
type StaticBlock = {
id: string;
type: string;
startIndex: number;
endIndex: number;
};
let contentForBlockList: contentForBlock[] = [];
let staticBlockList: StaticBlock[] = [];
let offset: number = 0;

function checkStaticBlocks() {
staticBlockList.forEach((block) => {
if (
!contentForBlockList.some(
(contentBlock) => contentBlock.id === block.id && contentBlock.type === block.type,
)
) {
context.report({
message: `Static block ${block.id} is missing a corresponding content_for "block" tag.`,
startIndex: block.startIndex,
endIndex: block.endIndex,
});
}
});
}

return {
async LiquidTag(node) {
// Early return if not a content_for block tag
if (node.name !== NamedTags.content_for || !isContentForBlock(node.markup)) return;

// Extract id and type from markup args
const idValue = node.markup.args.find((arg) => arg.name === 'id')?.value;
const typeArg = node.markup.args.find((arg) => arg.name === 'type')?.value;
if (!typeArg || typeArg.type !== NodeTypes.String) {
return; // covered by VariableContentForArguments
}
const typeValue = typeArg.value;

// Add to list if valid string id
if (idValue?.type === NodeTypes.String) {
contentForBlockList.push({ id: idValue.value, type: typeValue });
}
},

async LiquidRawTag(node) {
// when we get the schema tag, get the list of static blocks from each preset
if (node.name === 'schema' && node.body.kind === 'json') {
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;

const presets = validSchema.presets;
if (!presets) return;

presets.forEach((preset, index) => {
if ('blocks' in preset && preset.blocks) {
let ast_path: any[] = ['presets', index, 'blocks'];
// blocks as an array
if (Array.isArray(preset.blocks)) {
preset.blocks.forEach((block, block_index) => {
if (block.static === true && block.id) {
let node = nodeAtPath(ast, ast_path.concat([block_index]))! as ArrayNode;
staticBlockList.push({
id: block.id,
type: block.type,
startIndex: offset + getLocStart(node),
endIndex: offset + getLocEnd(node),
});
}
});
}
// blocks as an object
else if (typeof preset.blocks === 'object') {
Object.entries(preset.blocks).forEach(([block_id, block]) => {
if (block.static === true) {
let node = nodeAtPath(ast, ast_path.concat(block_id))! as ArrayNode;
staticBlockList.push({
id: block_id,
type: block.type,
startIndex: offset + getLocStart(node),
endIndex: offset + getLocEnd(node),
});
}
});
}
}
});
}
},

async onCodePathEnd() {
checkStaticBlocks();
},
};
},
};
3 changes: 3 additions & 0 deletions packages/theme-check-node/configs/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ RequiredLayoutThemeObject:
SchemaPresetsBlockOrder:
enabled: true
severity: 1
SchemaPresetsStaticBlocks:
enabled: true
severity: 0
TranslationKeyExists:
enabled: true
severity: 0
Expand Down
3 changes: 3 additions & 0 deletions packages/theme-check-node/configs/recommended.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ RequiredLayoutThemeObject:
SchemaPresetsBlockOrder:
enabled: true
severity: 1
SchemaPresetsStaticBlocks:
enabled: true
severity: 0
TranslationKeyExists:
enabled: true
severity: 0
Expand Down

0 comments on commit 89e2dbe

Please sign in to comment.