Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-penguins-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-check-common': patch
---

Avoid reporting missing block offenses for app block references when the containing schema allows app blocks.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,119 @@ describe('Module: JsonMissingBlock', () => {
expect(offenses).to.be.empty;
});

it('should not report an offense for an app block when the section allows @app and @theme', async () => {
const theme: MockTheme = {
'templates/product.app-block.json': `{
"sections": {
"main": {
"type": "main-product",
"blocks": {
"some_app_block": {
"type": "shopify://apps/some-app/blocks/example/1234"
}
},
"block_order": ["some_app_block"]
}
},
"order": ["main"]
}`,
'sections/main-product.liquid': `
{% schema %}
{
"name": "Main product",
"blocks": [
{
"type": "@app"
},
{
"type": "@theme"
}
]
}
{% endschema %}
`,
};

const offenses = await check(theme, [JSONMissingBlock]);
expect(offenses).to.be.empty;
});

it('should not report an offense for an app block when the section allows @app', async () => {
const theme: MockTheme = {
'templates/product.app-block.json': `{
"sections": {
"main": {
"type": "main-product",
"blocks": {
"some_app_block": {
"type": "shopify://apps/some-app/blocks/example/1234"
}
},
"block_order": ["some_app_block"]
}
},
"order": ["main"]
}`,
'sections/main-product.liquid': `
{% schema %}
{
"name": "Main product",
"blocks": [
{
"type": "@app"
}
]
}
{% endschema %}
`,
};

const offenses = await check(theme, [JSONMissingBlock]);
expect(offenses).to.be.empty;
});

it('should report an offense for a local block when the section allows only @app', async () => {
const theme: MockTheme = {
'templates/product.local-block.json': `{
"sections": {
"main": {
"type": "main-product",
"blocks": {
"some_local_block": {
"type": "text"
}
},
"block_order": ["some_local_block"]
}
},
"order": ["main"]
}`,
'sections/main-product.liquid': `
{% schema %}
{
"name": "Main product",
"blocks": [
{
"type": "@app"
}
]
}
{% endschema %}
`,
'blocks/text.liquid': '',
};

const offenses = await check(theme, [JSONMissingBlock]);
expect(offenses).to.have.length(1);
expect(offenses[0].message).to.equal(
"Block type 'text' is not allowed in 'sections/main-product.liquid'.",
);

const content = theme['templates/product.local-block.json'];
const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
expect(erroredContent).to.equal('"text"');
});

it('should report an offense when a nested block does not exist', async () => {
const theme: MockTheme = {
'templates/product.nested.json': `{
Expand Down Expand Up @@ -140,6 +253,47 @@ describe('Module: JsonMissingBlock', () => {
});

describe('Allowed block type validation', () => {
it('should report an offense for an app block when the section does not allow @app', async () => {
const theme: MockTheme = {
'templates/product.app-block.json': `{
"sections": {
"main": {
"type": "main-product",
"blocks": {
"some_app_block": {
"type": "shopify://apps/some-app/blocks/example/1234"
}
},
"block_order": ["some_app_block"]
}
},
"order": ["main"]
}`,
'sections/main-product.liquid': `
{% schema %}
{
"name": "Main product",
"blocks": [
{
"type": "@theme"
}
]
}
{% endschema %}
`,
};

const offenses = await check(theme, [JSONMissingBlock]);
expect(offenses).to.have.length(1);
expect(offenses[0].message).to.equal(
"Block type 'shopify://apps/some-app/blocks/example/1234' is not allowed in 'sections/main-product.liquid'.",
);

const content = theme['templates/product.app-block.json'];
const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index);
expect(erroredContent).to.equal('"shopify://apps/some-app/blocks/example/1234"');
});

it('should report an offense when block exists but is not in the section liquid schema', async () => {
const theme: MockTheme = {
'templates/product.valid.json': `{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ function isNestedBlock(currentPath: string[]): boolean {
return currentPath.filter((segment) => segment === 'blocks').length > 1;
}

function isShopifyAppBlockType(blockType: string): boolean {
return blockType.startsWith('shopify://apps/');
}

function reportWarning(
message: string,
offset: number,
Expand All @@ -28,7 +32,7 @@ async function validateBlockFileExistence(
blockType: string,
context: Context<SourceCodeType.JSON>,
): Promise<boolean> {
if (blockType === '@theme' || blockType === '@app') {
if (blockType === '@theme' || blockType === '@app' || isShopifyAppBlockType(blockType)) {
return true;
}
const blockPath = `blocks/${blockType}.liquid`;
Expand All @@ -53,7 +57,7 @@ async function getThemeBlocks(

if (Array.isArray(validSchema.blocks)) {
validSchema.blocks.forEach((block) => {
if (!('name' in block) && block.type !== '@app') {
if (!('name' in block)) {
themeBlocks.push(block.type);
}
});
Expand Down Expand Up @@ -85,13 +89,18 @@ async function validateBlock(
// Static blocks are not required to be in the schema blocks array
return;
} else {
const isAppBlock = isShopifyAppBlockType(blockType);
const isPrivateBlock = blockType.startsWith('_');
const schemaIncludesAtApp = themeBlocks.includes('@app');
const schemaIncludesAtTheme = themeBlocks.includes('@theme');
const schemaIncludesBlockType = themeBlocks.includes(blockType);
const schemaAllowsBlockType = isAppBlock
? schemaIncludesAtApp
: !isPrivateBlock
? schemaIncludesBlockType || schemaIncludesAtTheme
: schemaIncludesBlockType;

if (
!isPrivateBlock ? schemaIncludesBlockType || schemaIncludesAtTheme : schemaIncludesBlockType
) {
if (schemaAllowsBlockType) {
return;
} else {
const location = isNestedBlock(currentPath) ? 'blocks' : 'sections';
Expand Down
Loading