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/quiet-themes-inherit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-graph': patch
---

Skip inherited contextual JSON entries without an explicit type when building the theme graph.
114 changes: 114 additions & 0 deletions packages/theme-graph/src/graph/contextual-json.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { path as pathUtils } from '@shopify/theme-check-common';
import { MockFileSystem } from '@shopify/theme-check-common/src/test';
import { describe, expect, it } from 'vitest';
import { buildThemeGraph } from '../index';
import { ThemeGraph } from '../types';
import { getDependencies } from './test-helpers';

describe('contextual JSON modules', () => {
const rootUri = 'file:///theme';
const p = (part: string) => pathUtils.join(rootUri, ...part.split('/'));

async function buildJsonModule(
relativePath: string,
source: object,
options: { additionalFiles?: Record<string, string>; explicitEntry?: boolean } = {},
): Promise<ThemeGraph> {
const fs = new MockFileSystem(
{
'assets/theme.js': '',
'layout/theme.liquid': '',
'sections/hero.liquid': `{% schema %}
{ "name": "Hero", "blocks": [{ "type": "@theme" }] }
{% endschema %}`,
'blocks/group.liquid': `{% schema %}
{ "name": "Group", "blocks": [{ "type": "@theme" }] }
{% endschema %}`,
...options.additionalFiles,
[relativePath]: JSON.stringify(source),
},
rootUri,
);
const dependencies = await getDependencies(rootUri, fs);
const entryPoints = options.explicitEntry === false ? undefined : [p(relativePath)];

return buildThemeGraph(rootUri, dependencies, entryPoints);
}

function expectNoUndefinedModules(graph: ThemeGraph) {
expect(Object.keys(graph.modules)).not.toContain(p('sections/undefined.liquid'));
expect(Object.keys(graph.modules)).not.toContain(p('blocks/undefined.liquid'));
}

it('skips a contextual template section that inherits its type', async () => {
const baseTemplateUri = p('templates/index.json');
const graph = await buildJsonModule(
'templates/index.context.ca.json',
{
context: { market: 'ca' },
parent: 'index.json',
sections: { hero: { settings: { heading: 'Canada' } } },
},
{
additionalFiles: {
'templates/index.json': JSON.stringify({ sections: { hero: { type: 'hero' } } }),
},
explicitEntry: false,
},
);

expect(graph.modules[baseTemplateUri].dependencies.map(({ target }) => target.uri)).toContain(
p('sections/hero.liquid'),
);
expectNoUndefinedModules(graph);
});

it('skips a contextual section-group section that inherits its type', async () => {
const graph = await buildJsonModule('sections/header-group.context.ca.json', {
context: { market: 'ca' },
parent: 'header-group.json',
sections: { header: { settings: { sticky: true } } },
});

expectNoUndefinedModules(graph);
});

it('keeps the section edge while skipping a block that inherits its type', async () => {
const moduleUri = p('sections/header-group.context.ca.json');
const graph = await buildJsonModule('sections/header-group.context.ca.json', {
sections: {
header: {
type: 'hero',
blocks: { inherited: { settings: { text: 'Canada' } } },
},
},
});

expect(graph.modules[moduleUri].dependencies.map(({ target }) => target.uri)).toContain(
p('sections/hero.liquid'),
);
expectNoUndefinedModules(graph);
});

it('keeps valid edges while skipping a nested block that inherits its type', async () => {
const moduleUri = p('sections/header-group.context.ca.json');
const graph = await buildJsonModule('sections/header-group.context.ca.json', {
sections: {
header: {
type: 'hero',
blocks: {
group: {
type: 'group',
blocks: { inherited: { settings: { text: 'Canada' } } },
},
},
},
},
});

expect(graph.modules[moduleUri].dependencies.map(({ target }) => target.uri)).toEqual(
expect.arrayContaining([p('sections/hero.liquid'), p('blocks/group.liquid')]),
);
expectNoUndefinedModules(graph);
});
});
27 changes: 15 additions & 12 deletions packages/theme-graph/src/graph/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,12 @@ async function traverseJsonModule(
const template = parseJSON(sourceCode.source) as Template.Template;
const promises: Promise<Void>[] = [];
for (const [key, section] of Object.entries(template.sections)) {
const sectionType = section.type;
const path = ['sections', key];
const node = nodeAtPath(ast, path)! as ObjectNode;
const sectionModule = getSectionModule(themeGraph, sectionType);
const typeProperty = node.children.find((child) => child.key.value === 'type')!;
const typeProperty = node.children.find((child) => child.key.value === 'type');
if (!typeProperty) continue;

const sectionModule = getSectionModule(themeGraph, section.type);
const start = typeProperty.loc.start.offset;
const end = typeProperty.loc.end.offset;
const sourceRange: Range = [start, end];
Expand Down Expand Up @@ -505,12 +506,12 @@ async function traverseJsonModule(
const sectionGroup = parseJSON(sourceCode.source) as Template.SectionGroup;
const promises: Promise<Void>[] = [];
for (const [key, section] of Object.entries(sectionGroup.sections)) {
const sectionType = section.type;
const path = ['sections', key];
const node = nodeAtPath(ast, path)! as ObjectNode;
const sectionModule = getSectionModule(themeGraph, sectionType);
const typeProperty = node.children.find((child) => child.key.value === 'type');
if (!typeProperty) continue;

const typeProperty = node.children.find((child) => child.key.value === 'type')!;
const sectionModule = getSectionModule(themeGraph, section.type);
const start = typeProperty.loc.start.offset;
const end = typeProperty.loc.end.offset;
const sourceRange: Range = [start, end];
Expand Down Expand Up @@ -554,11 +555,12 @@ async function traverseSectionReferences(
const promises: Promise<Void>[] = [];

for (const [key, block] of Object.entries(section.blocks)) {
const blockType = block.type;
const blockModule = getThemeBlockModule(themeGraph, blockType);
const path = [...nodePath, 'blocks', key];
const node = nodeAtPath(sourceAst, path)! as ObjectNode;
const typeProperty = node.children.find((child) => child.key.value === 'type')!;
const typeProperty = node.children.find((child) => child.key.value === 'type');
if (!typeProperty) continue;

const blockModule = getThemeBlockModule(themeGraph, block.type);
const start = typeProperty.loc.start.offset;
const end = typeProperty.loc.end.offset;
const sourceRange: Range = [start, end];
Expand Down Expand Up @@ -587,11 +589,12 @@ async function traverseBlockReferences(

const promises: Promise<Void>[] = [];
for (const [key, childBlock] of Object.entries(block.blocks)) {
const childBlockType = childBlock.type;
const childBlockModule = getThemeBlockModule(themeGraph, childBlockType);
const path = [...nodePath, 'blocks', key];
const node = nodeAtPath(sourceAst, path)! as ObjectNode;
const typeProperty = node.children.find((child) => child.key.value === 'type')!;
const typeProperty = node.children.find((child) => child.key.value === 'type');
if (!typeProperty) continue;

const childBlockModule = getThemeBlockModule(themeGraph, childBlock.type);
const start = typeProperty.loc.start.offset;
const end = typeProperty.loc.end.offset;
const sourceRange: Range = [start, end];
Expand Down
Loading