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/bright-lions-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-check-docs-updater': minor
---

Add a `theme-docs bundle` command/API for generating self-contained Liquid docs bundles.
1 change: 1 addition & 0 deletions packages/theme-check-docs-updater/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
78 changes: 54 additions & 24 deletions packages/theme-check-docs-updater/scripts/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dir> \t\tDownloads all docsets and JSON Schemas to the specified directory.
bundle \t\t\tPrints a self-contained Liquid docs JSON bundle.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the first time I've seen the \t\t\t and asked Claude about it. Apparently it's a way to align the descriptions into columns. C mentioned that there are some libraries that exist that that handle alignment automatically. Just curious what the tradeoffs are between this syntax and a library-- maybe a whole library is just overkill and the current syntax is more lightweight for the purposes you need?

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);
});
6 changes: 6 additions & 0 deletions packages/theme-check-docs-updater/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export {
ThemeLiquidDocsBundle,
ThemeLiquidDocsBundleManager,
ThemeLiquidDocsBundleSchemaVersion,
buildThemeLiquidDocsBundle,
} from './themeLiquidDocsBundle';
export { ThemeLiquidDocsManager } from './themeLiquidDocsManager';
export {
Resource,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.');
});
});
56 changes: 56 additions & 0 deletions packages/theme-check-docs-updater/src/themeLiquidDocsBundle.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
tags(): Promise<TagEntry[]>;
filters(): Promise<FilterEntry[]>;
objects(): Promise<ObjectEntry[]>;
systemTranslations(): Promise<Translations>;
schemas(mode: 'theme'): Promise<SchemaDefinition[]>;
}

export async function buildThemeLiquidDocsBundle(
docsManager: ThemeLiquidDocsBundleManager = new ThemeLiquidDocsManager(),
): Promise<ThemeLiquidDocsBundle> {
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,
};
}
12 changes: 10 additions & 2 deletions packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
await this.setup();
return this.latestRevision();
});

filters = memo(async (): Promise<FilterEntry[]> => {
return findSuitableResource(this.loaders('filters'), JSON.parse, [], this.log);
});
Expand Down Expand Up @@ -110,7 +115,10 @@ export class ThemeLiquidDocsManager implements ThemeDocset, JsonValidationSet {

private async latestRevision(): Promise<string> {
const latest = await findSuitableResource(
[loader(() => this.load('latest'), 'loadLatestRevision')],
[
loader(() => this.load('latest'), 'loadLatestRevision'),
loader(() => fallbackResource('latest', this.log), 'fallbackLatestRevision'),
],
JSON.parse,
{},
this.log,
Expand Down Expand Up @@ -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<string> {
async function fallbackResource(name: Resource | 'latest', log: Logger): Promise<string> {
const sourcePath = path.resolve(dataRoot(), `${name}.json`);
return fs
.readFile(sourcePath, 'utf8')
Expand Down
Loading