diff --git a/.changeset/tidy-snakes-listen.md b/.changeset/tidy-snakes-listen.md new file mode 100644 index 000000000..23415219e --- /dev/null +++ b/.changeset/tidy-snakes-listen.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-check-node': patch +--- + +Fix cross-file LiquidDoc checks on Windows by normalising snippet paths before lookup. diff --git a/packages/theme-check-node/src/index.spec.ts b/packages/theme-check-node/src/index.spec.ts index 04883b333..c776faf2c 100644 --- a/packages/theme-check-node/src/index.spec.ts +++ b/packages/theme-check-node/src/index.spec.ts @@ -1,7 +1,9 @@ -import { afterEach, assert, beforeEach, describe, expect, it } from 'vitest'; -import { Config, SourceCodeType, getTheme, getThemeFilesPathPattern } from './index'; +import { afterEach, assert, beforeEach, describe, expect, it, vi } from 'vitest'; +import os from 'node:os'; +import path from 'node:path'; +import { Config, SourceCodeType, getTheme, getThemeFilesPathPattern, themeCheckRun } from './index'; import { Workspace, makeTempWorkspace } from './test/test-helpers'; -import { pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; describe('Unit: getTheme', () => { let workspace: Workspace; @@ -49,3 +51,45 @@ describe('Unit: getThemeFilesPathPattern', () => { expect(normalizedGlob).to.equal(__dirname.replace(/\\/g, '/') + '/**/*.{liquid,json}'); }); }); + +describe('Integration: themeCheckRun', () => { + it('reports cross-file LiquidDoc errors with Windows separators and spaces in the root path', async () => { + const workspace = await makeTempWorkspace( + { + locales: { + 'en.default.json': '{}', + }, + sections: { + 'main.liquid': "{% render 'card' %}", + }, + snippets: { + 'card.liquid': ` + {% doc %} + @param {string} title - The card title + {% enddoc %} + `, + }, + }, + path.join(os.tmpdir(), 'theme check.XXXXX'), + ); + const originalRelative = path.relative; + + const relativeSpy = vi.spyOn(path, 'relative').mockImplementation((from, to) => { + const relative = originalRelative(from, to); + if (typeof from === 'string' && from.startsWith('file:')) { + return relative.replace(/\//g, '\\'); + } + return relative; + }); + + try { + const { offenses } = await themeCheckRun(fileURLToPath(workspace.rootUri)); + expect( + offenses.filter(({ check }) => check === 'MissingRenderSnippetArguments'), + ).toHaveLength(1); + } finally { + relativeSpy.mockRestore(); + await workspace.clean(); + } + }); +}); diff --git a/packages/theme-check-node/src/index.ts b/packages/theme-check-node/src/index.ts index 22cb13827..5dc08c1fb 100644 --- a/packages/theme-check-node/src/index.ts +++ b/packages/theme-check-node/src/index.ts @@ -125,7 +125,7 @@ export async function themeCheckRun( ])); const docDefinitions = new Map( theme.map((file) => [ - path.relative(URI.file(root).toString(), file.uri), + pathUtils.relative(file.uri, pathUtils.normalize(URI.file(root))), memo(async (): Promise => { const ast = file.ast; if (!isLiquidHtmlNode(ast)) { diff --git a/packages/theme-check-node/src/test/test-helpers.ts b/packages/theme-check-node/src/test/test-helpers.ts index c55af1ec3..7efe8a7e3 100644 --- a/packages/theme-check-node/src/test/test-helpers.ts +++ b/packages/theme-check-node/src/test/test-helpers.ts @@ -74,8 +74,11 @@ export interface Workspace { clean(): Promise; } -export async function makeTempWorkspace(structure: Tree): Promise { - const root = await mktemp.createDir(path.join(__dirname, '..', '.XXXXX')); +export async function makeTempWorkspace( + structure: Tree, + rootTemplate = path.join(__dirname, '..', '.XXXXX'), +): Promise { + const root = await mktemp.createDir(rootTemplate); if (!root) throw new Error('Could not create temp dir for temp workspace'); await createFiles(structure, [root]);