diff --git a/.changeset/bright-lions-build.md b/.changeset/bright-lions-build.md new file mode 100644 index 000000000..85e33db21 --- /dev/null +++ b/.changeset/bright-lions-build.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-check-docs-updater': minor +--- + +Add a `theme-docs bundle` command/API for generating self-contained Liquid docs bundles. diff --git a/packages/theme-check-docs-updater/package.json b/packages/theme-check-docs-updater/package.json index 0327af898..cab800c85 100644 --- a/packages/theme-check-docs-updater/package.json +++ b/packages/theme-check-docs-updater/package.json @@ -26,6 +26,7 @@ "build:ci": "pnpm build", "build:ts": "tsc -b tsconfig.build.json", "postbuild": "node scripts/cli.js download data", + "theme-docs": "node scripts/cli.js", "test": "vitest", "type-check": "tsc --noEmit" }, diff --git a/packages/theme-check-docs-updater/scripts/cli.js b/packages/theme-check-docs-updater/scripts/cli.js index fa6597e0e..d46272421 100755 --- a/packages/theme-check-docs-updater/scripts/cli.js +++ b/packages/theme-check-docs-updater/scripts/cli.js @@ -2,47 +2,77 @@ const path = require('path'); const fs = require('fs'); -const { downloadThemeLiquidDocs, root } = require(path.resolve(__dirname, '../dist')); +const { + ThemeLiquidDocsManager, + buildThemeLiquidDocsBundle, + downloadThemeLiquidDocs, + root, +} = require(path.resolve(__dirname, '../dist')); // Get the command line arguments const args = process.argv.slice(2); -// Check if a command was provided -if (args.length === 0) { +function printUsage() { console.log(` Please provide a command. Usage: download \t\tDownloads all docsets and JSON Schemas to the specified directory. + bundle \t\t\tPrints a self-contained Liquid docs JSON bundle. root \tPrints the default docsets root directory. clear-cache \tClears the default docsets root directory. `); - process.exit(1); } -// Handle the command -switch (args[0]) { - case 'download': - if (args.length > 2) { - console.log('Please provide a directory to download docs into.'); - process.exit(1); - } - console.log('Downloading docs...'); +async function main() { + // Check if a command was provided + if (args.length === 0) { + printUsage(); + process.exit(1); + } - downloadThemeLiquidDocs(args[1], console.error.bind(console)); + // Handle the command + switch (args[0]) { + case 'download': + if (args.length > 2) { + console.error('Please provide a directory to download docs into.'); + process.exit(1); + } + console.log('Downloading docs...'); - break; + await downloadThemeLiquidDocs(args[1], console.error.bind(console)); - case 'root': - console.log(root); - break; + break; - case 'clear-cache': - console.log(`Removing '${root}'`); - fs.rmSync(root, { recursive: true }); - break; + case 'bundle': { + if (args.length > 1) { + console.error('The bundle command does not accept arguments.'); + process.exit(1); + } - default: - console.log(`Unknown command: ${args[0]}`); - process.exit(1); + const docsManager = new ThemeLiquidDocsManager(console.error.bind(console)); + const bundle = await buildThemeLiquidDocsBundle(docsManager); + process.stdout.write(`${JSON.stringify(bundle)}\n`); + + break; + } + + case 'root': + console.log(root); + break; + + case 'clear-cache': + console.log(`Removing '${root}'`); + fs.rmSync(root, { recursive: true, force: true }); + break; + + default: + console.error(`Unknown command: ${args[0]}`); + process.exit(1); + } } + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +}); diff --git a/packages/theme-check-docs-updater/src/index.ts b/packages/theme-check-docs-updater/src/index.ts index e4266c7e9..e3b2c8d9e 100644 --- a/packages/theme-check-docs-updater/src/index.ts +++ b/packages/theme-check-docs-updater/src/index.ts @@ -1,3 +1,9 @@ +export { + ThemeLiquidDocsBundle, + ThemeLiquidDocsBundleManager, + ThemeLiquidDocsBundleSchemaVersion, + buildThemeLiquidDocsBundle, +} from './themeLiquidDocsBundle'; export { ThemeLiquidDocsManager } from './themeLiquidDocsManager'; export { Resource, diff --git a/packages/theme-check-docs-updater/src/themeLiquidDocsBundle.spec.ts b/packages/theme-check-docs-updater/src/themeLiquidDocsBundle.spec.ts new file mode 100644 index 000000000..8986127f4 --- /dev/null +++ b/packages/theme-check-docs-updater/src/themeLiquidDocsBundle.spec.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { buildThemeLiquidDocsBundle } from './themeLiquidDocsBundle'; + +const manager = { + revision: async () => '40f966fc7793462726d9f8573c8d522f8ab44c54', + tags: async () => [{ name: 'if' }], + filters: async () => [{ name: 'upcase' }], + objects: async () => [{ name: 'product' }], + systemTranslations: async () => ({ 'shopify.checkout.general.cart': 'Cart' }), + schemas: async (mode: 'theme') => [ + { + uri: `https://example.com/schemas/${mode}.json`, + fileMatch: ['templates/*.json'], + schema: '{"type":"object"}', + }, + ], +}; + +describe('Module: themeLiquidDocsBundle', () => { + it('builds a self-contained Liquid docs bundle', async () => { + await expect(buildThemeLiquidDocsBundle(manager)).resolves.toEqual({ + schemaVersion: 1, + revision: '40f966fc7793462726d9f8573c8d522f8ab44c54', + tags: [{ name: 'if' }], + filters: [{ name: 'upcase' }], + objects: [{ name: 'product' }], + systemTranslations: { 'shopify.checkout.general.cart': 'Cart' }, + schemas: [ + { + uri: 'https://example.com/schemas/theme.json', + fileMatch: ['templates/*.json'], + schema: '{"type":"object"}', + }, + ], + }); + }); + + it('throws when the revision cannot be determined', async () => { + await expect( + buildThemeLiquidDocsBundle({ + ...manager, + revision: async () => '', + }), + ).rejects.toThrow('Unable to determine the theme-liquid-docs revision.'); + }); +}); diff --git a/packages/theme-check-docs-updater/src/themeLiquidDocsBundle.ts b/packages/theme-check-docs-updater/src/themeLiquidDocsBundle.ts new file mode 100644 index 000000000..e7a0c1f97 --- /dev/null +++ b/packages/theme-check-docs-updater/src/themeLiquidDocsBundle.ts @@ -0,0 +1,56 @@ +import { + FilterEntry, + ObjectEntry, + SchemaDefinition, + TagEntry, + Translations, +} from '@shopify/theme-check-common'; +import { ThemeLiquidDocsManager } from './themeLiquidDocsManager'; + +export const ThemeLiquidDocsBundleSchemaVersion = 1; + +export interface ThemeLiquidDocsBundle { + schemaVersion: typeof ThemeLiquidDocsBundleSchemaVersion; + revision: string; + tags: TagEntry[]; + filters: FilterEntry[]; + objects: ObjectEntry[]; + systemTranslations: Translations; + schemas: SchemaDefinition[]; +} + +export interface ThemeLiquidDocsBundleManager { + revision(): Promise; + tags(): Promise; + filters(): Promise; + objects(): Promise; + systemTranslations(): Promise; + schemas(mode: 'theme'): Promise; +} + +export async function buildThemeLiquidDocsBundle( + docsManager: ThemeLiquidDocsBundleManager = new ThemeLiquidDocsManager(), +): Promise { + const [revision, tags, filters, objects, systemTranslations, schemas] = await Promise.all([ + docsManager.revision(), + docsManager.tags(), + docsManager.filters(), + docsManager.objects(), + docsManager.systemTranslations(), + docsManager.schemas('theme'), + ]); + + if (!revision) { + throw new Error('Unable to determine the theme-liquid-docs revision.'); + } + + return { + schemaVersion: ThemeLiquidDocsBundleSchemaVersion, + revision, + tags, + filters, + objects, + systemTranslations, + schemas, + }; +} diff --git a/packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts b/packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts index b565941ec..3c30f1cd4 100644 --- a/packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts +++ b/packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts @@ -28,6 +28,11 @@ type JSONSchemaManifest = { schemas: { uri: string; fileMatch?: string[] }[] }; export class ThemeLiquidDocsManager implements ThemeDocset, JsonValidationSet { constructor(private log: Logger = noop) {} + revision = memo(async (): Promise => { + await this.setup(); + return this.latestRevision(); + }); + filters = memo(async (): Promise => { return findSuitableResource(this.loaders('filters'), JSON.parse, [], this.log); }); @@ -110,7 +115,10 @@ export class ThemeLiquidDocsManager implements ThemeDocset, JsonValidationSet { private async latestRevision(): Promise { const latest = await findSuitableResource( - [loader(() => this.load('latest'), 'loadLatestRevision')], + [ + loader(() => this.load('latest'), 'loadLatestRevision'), + loader(() => fallbackResource('latest', this.log), 'fallbackLatestRevision'), + ], JSON.parse, {}, this.log, @@ -205,7 +213,7 @@ function dataRoot() { } /** Returns the at-build-time path to the fallback data file. */ -async function fallbackResource(name: Resource, log: Logger): Promise { +async function fallbackResource(name: Resource | 'latest', log: Logger): Promise { const sourcePath = path.resolve(dataRoot(), `${name}.json`); return fs .readFile(sourcePath, 'utf8')