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')