Skip to content

Commit 16df60f

Browse files
committed
Add Liquid docs bundle command
1 parent c0a6504 commit 16df60f

7 files changed

Lines changed: 178 additions & 26 deletions

File tree

.changeset/bright-lions-build.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme-check-docs-updater': minor
3+
---
4+
5+
Add a `theme-docs bundle` command/API for generating self-contained Liquid docs bundles.

packages/theme-check-docs-updater/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"build:ci": "pnpm build",
2727
"build:ts": "tsc -b tsconfig.build.json",
2828
"postbuild": "node scripts/cli.js download data",
29+
"theme-docs": "node scripts/cli.js",
2930
"test": "vitest",
3031
"type-check": "tsc --noEmit"
3132
},

packages/theme-check-docs-updater/scripts/cli.js

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,77 @@
22

33
const path = require('path');
44
const fs = require('fs');
5-
const { downloadThemeLiquidDocs, root } = require(path.resolve(__dirname, '../dist'));
5+
const {
6+
ThemeLiquidDocsManager,
7+
buildThemeLiquidDocsBundle,
8+
downloadThemeLiquidDocs,
9+
root,
10+
} = require(path.resolve(__dirname, '../dist'));
611

712
// Get the command line arguments
813
const args = process.argv.slice(2);
914

10-
// Check if a command was provided
11-
if (args.length === 0) {
15+
function printUsage() {
1216
console.log(`
1317
Please provide a command.
1418
1519
Usage:
1620
download <dir> \t\tDownloads all docsets and JSON Schemas to the specified directory.
21+
bundle \t\t\tPrints a self-contained Liquid docs JSON bundle.
1722
root \tPrints the default docsets root directory.
1823
clear-cache \tClears the default docsets root directory.
1924
`);
20-
process.exit(1);
2125
}
2226

23-
// Handle the command
24-
switch (args[0]) {
25-
case 'download':
26-
if (args.length > 2) {
27-
console.log('Please provide a directory to download docs into.');
28-
process.exit(1);
29-
}
30-
console.log('Downloading docs...');
27+
async function main() {
28+
// Check if a command was provided
29+
if (args.length === 0) {
30+
printUsage();
31+
process.exit(1);
32+
}
3133

32-
downloadThemeLiquidDocs(args[1], console.error.bind(console));
34+
// Handle the command
35+
switch (args[0]) {
36+
case 'download':
37+
if (args.length > 2) {
38+
console.error('Please provide a directory to download docs into.');
39+
process.exit(1);
40+
}
41+
console.log('Downloading docs...');
3342

34-
break;
43+
await downloadThemeLiquidDocs(args[1], console.error.bind(console));
3544

36-
case 'root':
37-
console.log(root);
38-
break;
45+
break;
3946

40-
case 'clear-cache':
41-
console.log(`Removing '${root}'`);
42-
fs.rmSync(root, { recursive: true });
43-
break;
47+
case 'bundle': {
48+
if (args.length > 1) {
49+
console.error('The bundle command does not accept arguments.');
50+
process.exit(1);
51+
}
4452

45-
default:
46-
console.log(`Unknown command: ${args[0]}`);
47-
process.exit(1);
53+
const docsManager = new ThemeLiquidDocsManager(console.error.bind(console));
54+
const bundle = await buildThemeLiquidDocsBundle(docsManager);
55+
process.stdout.write(`${JSON.stringify(bundle)}\n`);
56+
57+
break;
58+
}
59+
60+
case 'root':
61+
console.log(root);
62+
break;
63+
64+
case 'clear-cache':
65+
console.log(`Removing '${root}'`);
66+
fs.rmSync(root, { recursive: true, force: true });
67+
break;
68+
69+
default:
70+
console.error(`Unknown command: ${args[0]}`);
71+
process.exit(1);
72+
}
4873
}
74+
75+
main().catch((error) => {
76+
console.error(error instanceof Error ? error.message : error);
77+
process.exit(1);
78+
});

packages/theme-check-docs-updater/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
export {
2+
ThemeLiquidDocsBundle,
3+
ThemeLiquidDocsBundleManager,
4+
ThemeLiquidDocsBundleSchemaVersion,
5+
buildThemeLiquidDocsBundle,
6+
} from './themeLiquidDocsBundle';
17
export { ThemeLiquidDocsManager } from './themeLiquidDocsManager';
28
export {
39
Resource,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { buildThemeLiquidDocsBundle } from './themeLiquidDocsBundle';
3+
4+
const manager = {
5+
revision: async () => '40f966fc7793462726d9f8573c8d522f8ab44c54',
6+
tags: async () => [{ name: 'if' }],
7+
filters: async () => [{ name: 'upcase' }],
8+
objects: async () => [{ name: 'product' }],
9+
systemTranslations: async () => ({ 'shopify.checkout.general.cart': 'Cart' }),
10+
schemas: async (mode: 'theme') => [
11+
{
12+
uri: `https://example.com/schemas/${mode}.json`,
13+
fileMatch: ['templates/*.json'],
14+
schema: '{"type":"object"}',
15+
},
16+
],
17+
};
18+
19+
describe('Module: themeLiquidDocsBundle', () => {
20+
it('builds a self-contained Liquid docs bundle', async () => {
21+
await expect(buildThemeLiquidDocsBundle(manager)).resolves.toEqual({
22+
schemaVersion: 1,
23+
revision: '40f966fc7793462726d9f8573c8d522f8ab44c54',
24+
tags: [{ name: 'if' }],
25+
filters: [{ name: 'upcase' }],
26+
objects: [{ name: 'product' }],
27+
systemTranslations: { 'shopify.checkout.general.cart': 'Cart' },
28+
schemas: [
29+
{
30+
uri: 'https://example.com/schemas/theme.json',
31+
fileMatch: ['templates/*.json'],
32+
schema: '{"type":"object"}',
33+
},
34+
],
35+
});
36+
});
37+
38+
it('throws when the revision cannot be determined', async () => {
39+
await expect(
40+
buildThemeLiquidDocsBundle({
41+
...manager,
42+
revision: async () => '',
43+
}),
44+
).rejects.toThrow('Unable to determine the theme-liquid-docs revision.');
45+
});
46+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
FilterEntry,
3+
ObjectEntry,
4+
SchemaDefinition,
5+
TagEntry,
6+
Translations,
7+
} from '@shopify/theme-check-common';
8+
import { ThemeLiquidDocsManager } from './themeLiquidDocsManager';
9+
10+
export const ThemeLiquidDocsBundleSchemaVersion = 1;
11+
12+
export interface ThemeLiquidDocsBundle {
13+
schemaVersion: typeof ThemeLiquidDocsBundleSchemaVersion;
14+
revision: string;
15+
tags: TagEntry[];
16+
filters: FilterEntry[];
17+
objects: ObjectEntry[];
18+
systemTranslations: Translations;
19+
schemas: SchemaDefinition[];
20+
}
21+
22+
export interface ThemeLiquidDocsBundleManager {
23+
revision(): Promise<string>;
24+
tags(): Promise<TagEntry[]>;
25+
filters(): Promise<FilterEntry[]>;
26+
objects(): Promise<ObjectEntry[]>;
27+
systemTranslations(): Promise<Translations>;
28+
schemas(mode: 'theme'): Promise<SchemaDefinition[]>;
29+
}
30+
31+
export async function buildThemeLiquidDocsBundle(
32+
docsManager: ThemeLiquidDocsBundleManager = new ThemeLiquidDocsManager(),
33+
): Promise<ThemeLiquidDocsBundle> {
34+
const [revision, tags, filters, objects, systemTranslations, schemas] = await Promise.all([
35+
docsManager.revision(),
36+
docsManager.tags(),
37+
docsManager.filters(),
38+
docsManager.objects(),
39+
docsManager.systemTranslations(),
40+
docsManager.schemas('theme'),
41+
]);
42+
43+
if (!revision) {
44+
throw new Error('Unable to determine the theme-liquid-docs revision.');
45+
}
46+
47+
return {
48+
schemaVersion: ThemeLiquidDocsBundleSchemaVersion,
49+
revision,
50+
tags,
51+
filters,
52+
objects,
53+
systemTranslations,
54+
schemas,
55+
};
56+
}

packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ type JSONSchemaManifest = { schemas: { uri: string; fileMatch?: string[] }[] };
2828
export class ThemeLiquidDocsManager implements ThemeDocset, JsonValidationSet {
2929
constructor(private log: Logger = noop) {}
3030

31+
revision = memo(async (): Promise<string> => {
32+
await this.setup();
33+
return this.latestRevision();
34+
});
35+
3136
filters = memo(async (): Promise<FilterEntry[]> => {
3237
return findSuitableResource(this.loaders('filters'), JSON.parse, [], this.log);
3338
});
@@ -110,7 +115,10 @@ export class ThemeLiquidDocsManager implements ThemeDocset, JsonValidationSet {
110115

111116
private async latestRevision(): Promise<string> {
112117
const latest = await findSuitableResource(
113-
[loader(() => this.load('latest'), 'loadLatestRevision')],
118+
[
119+
loader(() => this.load('latest'), 'loadLatestRevision'),
120+
loader(() => fallbackResource('latest', this.log), 'fallbackLatestRevision'),
121+
],
114122
JSON.parse,
115123
{},
116124
this.log,
@@ -205,7 +213,7 @@ function dataRoot() {
205213
}
206214

207215
/** Returns the at-build-time path to the fallback data file. */
208-
async function fallbackResource(name: Resource, log: Logger): Promise<string> {
216+
async function fallbackResource(name: Resource | 'latest', log: Logger): Promise<string> {
209217
const sourcePath = path.resolve(dataRoot(), `${name}.json`);
210218
return fs
211219
.readFile(sourcePath, 'utf8')

0 commit comments

Comments
 (0)