@platformos/platformos-check-docs-updater 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/data/app_block_entry.json +13 -0
- package/data/default_setting_values.json +24 -0
- package/data/filters.json +7 -0
- package/data/graphql.graphql +12964 -0
- package/data/latest.json +2 -0
- package/data/local_block_entry.json +25 -0
- package/data/manifest_theme.json +19 -0
- package/data/manifest_theme_app_extension.json +10 -0
- package/data/objects.json +6 -0
- package/data/preset.json +72 -0
- package/data/preset_blocks.json +91 -0
- package/data/section.json +208 -0
- package/data/setting.json +1353 -0
- package/data/settings.json +10 -0
- package/data/shopify_system_translations.json +2432 -0
- package/data/tags.json +7 -0
- package/data/targetted_block_entry.json +15 -0
- package/data/theme_block.json +91 -0
- package/data/theme_block_entry.json +14 -0
- package/data/theme_settings.json +83 -0
- package/data/translations.json +63 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/themeLiquidDocsDownloader.d.ts +23 -0
- package/dist/themeLiquidDocsDownloader.js +148 -0
- package/dist/themeLiquidDocsDownloader.js.map +1 -0
- package/dist/themeLiquidDocsManager.d.ts +34 -0
- package/dist/themeLiquidDocsManager.js +175 -0
- package/dist/themeLiquidDocsManager.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +18 -0
- package/dist/utils.js.map +1 -0
- package/package.json +42 -0
- package/scripts/cli.js +48 -0
- package/src/index.ts +8 -0
- package/src/themeLiquidDocsDownloader.ts +192 -0
- package/src/themeLiquidDocsManager.spec.ts +103 -0
- package/src/themeLiquidDocsManager.ts +255 -0
- package/src/utils.ts +12 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +18 -0
package/dist/utils.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.memoize = exports.memo = exports.tap = exports.identity = exports.noop = void 0;
|
|
4
|
+
const noop = () => { };
|
|
5
|
+
exports.noop = noop;
|
|
6
|
+
const identity = (x) => x;
|
|
7
|
+
exports.identity = identity;
|
|
8
|
+
const tap = (tappingFunction) => {
|
|
9
|
+
return (x) => {
|
|
10
|
+
tappingFunction(x);
|
|
11
|
+
return x;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
exports.tap = tap;
|
|
15
|
+
var platformos_check_common_1 = require("@platformos/platformos-check-common");
|
|
16
|
+
Object.defineProperty(exports, "memo", { enumerable: true, get: function () { return platformos_check_common_1.memo; } });
|
|
17
|
+
Object.defineProperty(exports, "memoize", { enumerable: true, get: function () { return platformos_check_common_1.memoize; } });
|
|
18
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;AACO,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAAhB,QAAA,IAAI,QAAY;AAEtB,MAAM,QAAQ,GAAG,CAAI,CAAI,EAAK,EAAE,CAAC,CAAC,CAAC;AAA7B,QAAA,QAAQ,YAAqB;AACnC,MAAM,GAAG,GAAG,CAAI,eAA+B,EAAE,EAAE;IACxD,OAAO,CAAC,CAAI,EAAE,EAAE;QACd,eAAe,CAAC,CAAC,CAAC,CAAC;QACnB,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;AACJ,CAAC,CAAC;AALW,QAAA,GAAG,OAKd;AAEF,+EAAoE;AAA3D,+GAAA,IAAI,OAAA;AAAE,kHAAA,OAAO,OAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@platformos/platformos-check-docs-updater",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Scripts to initialize theme-check data with assets from the theme-liquid-docs repo.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"author": "platformOS",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/Platform-OS/platformos-tools/tree/main/packages/platformos-check-docs-updater#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/Platform-OS/platformos-tools.git",
|
|
12
|
+
"directory": "packages/platformos-check-docs-updater"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Platform-OS/platformos-tools/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"@platformos:registry": "https://registry.npmjs.org"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"theme-docs": "./scripts/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "yarn build:ts",
|
|
26
|
+
"build:ci": "yarn build",
|
|
27
|
+
"build:ts": "tsc -b tsconfig.build.json",
|
|
28
|
+
"postbuild": "node scripts/cli.js download data",
|
|
29
|
+
"test": "vitest",
|
|
30
|
+
"type-check": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@platformos/platformos-check-common": "0.0.2",
|
|
34
|
+
"env-paths": "^2.2.1",
|
|
35
|
+
"he": "^1.2.0",
|
|
36
|
+
"node-fetch": "^2.6.11"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/he": "^1.2.3",
|
|
40
|
+
"@types/node-fetch": "^2.6.4"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { downloadThemeLiquidDocs, root } = require(path.resolve(__dirname, '../dist'));
|
|
6
|
+
|
|
7
|
+
// Get the command line arguments
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
// Check if a command was provided
|
|
11
|
+
if (args.length === 0) {
|
|
12
|
+
console.log(`
|
|
13
|
+
Please provide a command.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
download <dir> \t\tDownloads all docsets and JSON Schemas to the specified directory.
|
|
17
|
+
root \tPrints the default docsets root directory.
|
|
18
|
+
clear-cache \tClears the default docsets root directory.
|
|
19
|
+
`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
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...');
|
|
31
|
+
|
|
32
|
+
downloadThemeLiquidDocs(args[1], console.error.bind(console));
|
|
33
|
+
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
case 'root':
|
|
37
|
+
console.log(root);
|
|
38
|
+
break;
|
|
39
|
+
|
|
40
|
+
case 'clear-cache':
|
|
41
|
+
console.log(`Removing '${root}'`);
|
|
42
|
+
fs.rmSync(root, { recursive: true });
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
default:
|
|
46
|
+
console.log(`Unknown command: ${args[0]}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Mode } from '@platformos/platformos-check-common';
|
|
2
|
+
import envPaths from 'env-paths';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { Logger, noop, tap } from './utils';
|
|
7
|
+
import he from 'he';
|
|
8
|
+
const paths = envPaths('theme-liquid-docs');
|
|
9
|
+
export const root = paths.cache;
|
|
10
|
+
|
|
11
|
+
export const ThemeLiquidDocsRootFallback =
|
|
12
|
+
'https://raw.githubusercontent.com/Shopify/theme-liquid-docs/main';
|
|
13
|
+
export const ThemeLiquidDocsRoot = 'https://documentation.platformos.com/api/liquid';
|
|
14
|
+
export const ThemeCustomSchemas = ['tags', 'latest', 'objects', 'filters'];
|
|
15
|
+
export const ThemeGraphQLSchema = 'https://documentation.platformos.com/api/graphql/schema';
|
|
16
|
+
|
|
17
|
+
export type Resource = (typeof Resources)[number];
|
|
18
|
+
export const Resources = [
|
|
19
|
+
'filters',
|
|
20
|
+
'objects',
|
|
21
|
+
'tags',
|
|
22
|
+
'shopify_system_translations',
|
|
23
|
+
'manifest_theme',
|
|
24
|
+
'manifest_theme_app_extension',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export const Manifests = {
|
|
28
|
+
app: 'manifest_theme_app_extension',
|
|
29
|
+
theme: 'manifest_theme',
|
|
30
|
+
} as const satisfies Record<Mode, Resource>;
|
|
31
|
+
|
|
32
|
+
const THEME_LIQUID_DOCS: Record<Resource | 'latest', string> = {
|
|
33
|
+
filters: 'filters.json',
|
|
34
|
+
objects: 'objects.json',
|
|
35
|
+
tags: 'tags.json',
|
|
36
|
+
latest: 'latest.json',
|
|
37
|
+
shopify_system_translations: 'data/shopify_system_translations.json',
|
|
38
|
+
manifest_theme: 'schemas/manifest_theme.json',
|
|
39
|
+
manifest_theme_app_extension: 'schemas/manifest_theme_app_extension.json',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export async function downloadSchema(
|
|
43
|
+
relativeUri: string,
|
|
44
|
+
destination: string = root,
|
|
45
|
+
log: Logger = noop,
|
|
46
|
+
) {
|
|
47
|
+
const remotePath = schemaUrl(relativeUri);
|
|
48
|
+
const localPath = schemaPath(relativeUri, destination);
|
|
49
|
+
const text = await download(remotePath, log);
|
|
50
|
+
await fs.writeFile(localPath, text, 'utf8');
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function downloadResource(
|
|
55
|
+
resource: Resource | 'latest',
|
|
56
|
+
destination: string = root,
|
|
57
|
+
log: Logger = noop,
|
|
58
|
+
) {
|
|
59
|
+
const remotePath = resourceUrl(resource);
|
|
60
|
+
const localPath = resourcePath(resource, destination);
|
|
61
|
+
const text = await download(remotePath, log);
|
|
62
|
+
await fs.writeFile(localPath, text, 'utf8');
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function downloadGraphQLSchema(destination: string = root, log: Logger = noop) {
|
|
67
|
+
const localPath = graphQLPath(destination);
|
|
68
|
+
const text = await download(ThemeGraphQLSchema, log);
|
|
69
|
+
await fs.writeFile(localPath, he.decode(text), 'utf8');
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function graphQLPath(destination: string = root) {
|
|
74
|
+
return path.join(destination, `graphql.graphql`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function download(path: string, log: Logger) {
|
|
78
|
+
if (path.startsWith('file:')) {
|
|
79
|
+
return await fs
|
|
80
|
+
.readFile(path.replace(/^file:/, ''), 'utf8')
|
|
81
|
+
.then(tap(() => log(`Using local file: ${path}`)))
|
|
82
|
+
.catch((error) => {
|
|
83
|
+
log(`Failed to read local file: ${path}`);
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
log(path);
|
|
88
|
+
const res = await fetch(path);
|
|
89
|
+
return res.text();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resourcePath(resource: Resource | 'latest', destination: string = root) {
|
|
94
|
+
return path.join(destination, `${resource}.json`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resourceUrl(resource: Resource | 'latest') {
|
|
98
|
+
let resourceRoot = `${ThemeLiquidDocsRootFallback}`;
|
|
99
|
+
|
|
100
|
+
if (ThemeCustomSchemas.includes(resource)) {
|
|
101
|
+
resourceRoot = ThemeLiquidDocsRoot;
|
|
102
|
+
}
|
|
103
|
+
const relativePath = THEME_LIQUID_DOCS[resource];
|
|
104
|
+
return `${resourceRoot}/${relativePath}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function schemaPath(relativeUri: string, destination: string = root) {
|
|
108
|
+
return path.resolve(destination, path.basename(relativeUri));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function schemaUrl(relativeUri: string) {
|
|
112
|
+
const schemaRoot = process.env.SHOPIFY_TLD_ROOT
|
|
113
|
+
? `file:${process.env.SHOPIFY_TLD_ROOT}`
|
|
114
|
+
: ThemeLiquidDocsRootFallback;
|
|
115
|
+
return `${schemaRoot}/schemas/${relativeUri}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function exists(path: string) {
|
|
119
|
+
try {
|
|
120
|
+
await fs.stat(path);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function downloadThemeLiquidDocs(destination: string, log: Logger) {
|
|
128
|
+
if (!(await exists(destination))) {
|
|
129
|
+
await fs.mkdir(destination);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const resources = ['latest'].concat(Resources) as (Resource | 'latest')[];
|
|
133
|
+
const resourceContents = await Promise.all(
|
|
134
|
+
resources.map((file) => {
|
|
135
|
+
return downloadResource(file, destination, log)
|
|
136
|
+
.then(
|
|
137
|
+
tap(() =>
|
|
138
|
+
log(
|
|
139
|
+
`Successfully downloaded latest resource:\n\t${resourceUrl(file)}\n\t> ${resourcePath(
|
|
140
|
+
file,
|
|
141
|
+
destination,
|
|
142
|
+
)}`,
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
.catch((error) => {
|
|
147
|
+
log(
|
|
148
|
+
`Failed to download latest resource:\n\t${resourceUrl(file)} to\n\t${resourcePath(
|
|
149
|
+
file,
|
|
150
|
+
destination,
|
|
151
|
+
)}\n${error}`,
|
|
152
|
+
);
|
|
153
|
+
throw error;
|
|
154
|
+
});
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const manifests = Object.values(Manifests)
|
|
159
|
+
.map((resource) => resources.indexOf(resource))
|
|
160
|
+
.map((index) => resourceContents[index])
|
|
161
|
+
.map((manifest) => JSON.parse(manifest));
|
|
162
|
+
|
|
163
|
+
const relativeUris = manifests.flatMap((manifest) =>
|
|
164
|
+
manifest.schemas.map((schema: { uri: string }) => schema.uri),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
await Promise.all(
|
|
168
|
+
unique(relativeUris).map((uri) =>
|
|
169
|
+
downloadSchema(uri, destination, log)
|
|
170
|
+
.then(
|
|
171
|
+
tap(() =>
|
|
172
|
+
log(
|
|
173
|
+
`Successfully downloaded schema:\n\t${schemaUrl(uri)}\n\t> ${schemaPath(
|
|
174
|
+
uri,
|
|
175
|
+
destination,
|
|
176
|
+
)}`,
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
.catch((error) => {
|
|
181
|
+
log(`Failed to download schema: ${uri}, ${error}`);
|
|
182
|
+
throw error;
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
await downloadGraphQLSchema(destination, log);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function unique<T>(array: T[]): T[] {
|
|
191
|
+
return [...new Set(array).values()];
|
|
192
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { expect, describe, it, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { ThemeLiquidDocsManager } from './themeLiquidDocsManager';
|
|
3
|
+
import { downloadResource, Resources } from './themeLiquidDocsDownloader';
|
|
4
|
+
import { noop } from './utils';
|
|
5
|
+
|
|
6
|
+
vi.mock('./themeLiquidDocsDownloader', async (importOriginal) => {
|
|
7
|
+
const actual: any = await importOriginal();
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
downloadResource: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
vi.mock('node:path', async () => {
|
|
15
|
+
return {
|
|
16
|
+
default: {
|
|
17
|
+
join: (...paths: string[]) => paths.join('/'),
|
|
18
|
+
resolve: () => '.',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock('env-paths', async () => {
|
|
24
|
+
return {
|
|
25
|
+
default: (appPath: string) => ({ cache: `MOCKED_CACHE/${appPath}` }),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
vi.mock('node:fs/promises', async () => {
|
|
30
|
+
const fileSystem: Record<string, string> = {
|
|
31
|
+
'MOCKED_CACHE/theme-liquid-docs/filters.json': '[{"name": "upcase"}]',
|
|
32
|
+
'MOCKED_CACHE/theme-liquid-docs/objects.json': '[{"name": "product"}]',
|
|
33
|
+
'MOCKED_CACHE/theme-liquid-docs/tags.json': '[{"name": "if"}]',
|
|
34
|
+
'MOCKED_CACHE/theme-liquid-docs/latest.json': '{"revision": "1"}',
|
|
35
|
+
'MOCKED_CACHE/theme-liquid-docs/section_schema.json':
|
|
36
|
+
'{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"}},"required":["name","age"]}',
|
|
37
|
+
'MOCKED_CACHE/theme-liquid-docs/shopify_system_translations.json':
|
|
38
|
+
'{"shopify.checkout.general.cart": "Cart"}',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
default: {
|
|
43
|
+
readFile: vi.fn().mockImplementation((path) => fileSystem[path]),
|
|
44
|
+
mkdir: vi.fn(),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Module: ThemeLiquidDocsManager', async () => {
|
|
50
|
+
let manager: ThemeLiquidDocsManager;
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
manager = new ThemeLiquidDocsManager();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(async () => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should not download remote files if the revision is stable', async () => {
|
|
61
|
+
await Promise.all([manager.filters(), manager.objects(), manager.tags()]);
|
|
62
|
+
expect(vi.mocked(downloadResource)).toHaveBeenNthCalledWith(
|
|
63
|
+
1,
|
|
64
|
+
'latest',
|
|
65
|
+
'MOCKED_CACHE/theme-liquid-docs',
|
|
66
|
+
noop,
|
|
67
|
+
);
|
|
68
|
+
expect(vi.mocked(downloadResource)).toHaveBeenCalledTimes(1);
|
|
69
|
+
for (const resource of Resources) {
|
|
70
|
+
expect(vi.mocked(downloadResource)).not.toHaveBeenCalledWith(resource, expect.any(String));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('Unit: filters', () => {
|
|
75
|
+
it('should return an array', async () => {
|
|
76
|
+
const filters = await manager.filters();
|
|
77
|
+
expect(filters).to.eql([{ name: 'upcase' }]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('Unit: objects', () => {
|
|
82
|
+
it('should return an array', async () => {
|
|
83
|
+
const objects = await manager.objects();
|
|
84
|
+
expect(objects).to.eql([{ name: 'product' }]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('Unit: tags', () => {
|
|
89
|
+
it('should return an array', async () => {
|
|
90
|
+
const tags = await manager.tags();
|
|
91
|
+
expect(tags).to.eql([{ name: 'if' }]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('Unit: systemTranslations', () => {
|
|
96
|
+
it('should return the parsed JSON content of the system translations', async () => {
|
|
97
|
+
const systemTranslations = await manager.systemTranslations();
|
|
98
|
+
expect(systemTranslations).to.eql({
|
|
99
|
+
'shopify.checkout.general.cart': 'Cart',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FilterEntry,
|
|
3
|
+
JsonValidationSet,
|
|
4
|
+
Mode,
|
|
5
|
+
ObjectEntry,
|
|
6
|
+
SchemaDefinition,
|
|
7
|
+
TagEntry,
|
|
8
|
+
ThemeDocset,
|
|
9
|
+
Translations,
|
|
10
|
+
} from '@platformos/platformos-check-common';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import {
|
|
14
|
+
Manifests,
|
|
15
|
+
Resource,
|
|
16
|
+
ThemeCustomSchemas,
|
|
17
|
+
ThemeLiquidDocsRoot,
|
|
18
|
+
ThemeLiquidDocsRootFallback,
|
|
19
|
+
downloadResource,
|
|
20
|
+
downloadThemeLiquidDocs,
|
|
21
|
+
exists,
|
|
22
|
+
graphQLPath,
|
|
23
|
+
resourcePath,
|
|
24
|
+
root,
|
|
25
|
+
schemaPath,
|
|
26
|
+
} from './themeLiquidDocsDownloader';
|
|
27
|
+
import { Logger, identity, memo, memoize, noop, tap } from './utils';
|
|
28
|
+
|
|
29
|
+
type JSONSchemaManifest = { schemas: { uri: string; fileMatch?: string[] }[] };
|
|
30
|
+
|
|
31
|
+
export class ThemeLiquidDocsManager implements ThemeDocset, JsonValidationSet {
|
|
32
|
+
constructor(private log: Logger = noop) {}
|
|
33
|
+
|
|
34
|
+
filters = memo(async (): Promise<FilterEntry[]> => {
|
|
35
|
+
return findSuitableResource(this.loaders('filters'), JSON.parse, [], this.log);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
objects = memo(async (): Promise<ObjectEntry[]> => {
|
|
39
|
+
return findSuitableResource(this.loaders('objects'), JSON.parse, [], this.log);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
liquidDrops = memo(async (): Promise<ObjectEntry[]> => {
|
|
43
|
+
return findSuitableResource(this.loaders('objects'), JSON.parse, [], this.log);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
tags = memo(async (): Promise<TagEntry[]> => {
|
|
47
|
+
return findSuitableResource(this.loaders('tags'), JSON.parse, [], this.log);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
graphQL = memo(async (): Promise<string | null> => {
|
|
51
|
+
return findSuitableResource(this.graphQLLoaders(), (x: string) => x, null, this.log);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
systemTranslations = memo(async (): Promise<Translations> => {
|
|
55
|
+
return findSuitableResource(
|
|
56
|
+
this.loaders('shopify_system_translations'),
|
|
57
|
+
JSON.parse,
|
|
58
|
+
{},
|
|
59
|
+
this.log,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
schemas = memoize(
|
|
64
|
+
(mode: Mode) =>
|
|
65
|
+
findSuitableResource<JSONSchemaManifest>(
|
|
66
|
+
this.loaders(Manifests[mode]),
|
|
67
|
+
JSON.parse,
|
|
68
|
+
{
|
|
69
|
+
schemas: [],
|
|
70
|
+
},
|
|
71
|
+
this.log,
|
|
72
|
+
).then((manifest) => {
|
|
73
|
+
return Promise.all(
|
|
74
|
+
manifest.schemas.map(async (schemaDefinition): Promise<SchemaDefinition> => {
|
|
75
|
+
let schemaRoot = `${ThemeLiquidDocsRootFallback}/schemas`;
|
|
76
|
+
|
|
77
|
+
if (ThemeCustomSchemas.includes(schemaDefinition.uri)) {
|
|
78
|
+
schemaRoot = ThemeLiquidDocsRoot;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
uri: `${schemaRoot}/${schemaDefinition.uri}`,
|
|
82
|
+
fileMatch: schemaDefinition.fileMatch,
|
|
83
|
+
schema: await findSuitableResource(
|
|
84
|
+
this.schemaLoaders(schemaDefinition.uri),
|
|
85
|
+
identity,
|
|
86
|
+
'',
|
|
87
|
+
this.log,
|
|
88
|
+
),
|
|
89
|
+
};
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}),
|
|
93
|
+
identity<Mode>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The setup method checks that the latest revision matches the one from
|
|
98
|
+
* Shopify/theme-liquid-docs. If there's a diff in revision, it means
|
|
99
|
+
* that the documentations that you have locally are out of date.
|
|
100
|
+
*
|
|
101
|
+
* The setup method then downloads the other files.
|
|
102
|
+
*/
|
|
103
|
+
setup = memo(async (): Promise<void> => {
|
|
104
|
+
try {
|
|
105
|
+
if (!(await exists(root))) {
|
|
106
|
+
await fs.mkdir(root, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const local = await this.latestRevision();
|
|
110
|
+
await downloadResource('latest', root, this.log);
|
|
111
|
+
const remote = await this.latestRevision();
|
|
112
|
+
if (local !== remote) {
|
|
113
|
+
await downloadThemeLiquidDocs(root, this.log);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error instanceof Error) {
|
|
117
|
+
this.log(`Failed to setup with the following error: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
private async latestRevision(): Promise<string> {
|
|
124
|
+
const latest = await findSuitableResource(
|
|
125
|
+
[loader(() => this.load('latest'), 'loadLatestRevision')],
|
|
126
|
+
JSON.parse,
|
|
127
|
+
{},
|
|
128
|
+
this.log,
|
|
129
|
+
);
|
|
130
|
+
return latest['revision'] ?? '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async loadResource(name: Resource): Promise<string> {
|
|
134
|
+
// Always wait for setup first. Since it's memoized, this will always
|
|
135
|
+
// be the same promise.
|
|
136
|
+
await this.setup();
|
|
137
|
+
return this.load(name).then(tap(() => this.log(`Loaded resource from ${resourcePath(name)}`)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async load(name: Resource | 'latest') {
|
|
141
|
+
return fs.readFile(resourcePath(name), 'utf8');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async loadSchema(relativeUri: string) {
|
|
145
|
+
return fs
|
|
146
|
+
.readFile(schemaPath(relativeUri), 'utf8')
|
|
147
|
+
.then(tap(() => this.log(`Loaded schema from ${schemaPath(relativeUri)}`)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async loadGraphQL() {
|
|
151
|
+
return fs.readFile(graphQLPath(), 'utf8').then(tap(() => this.log(`Loaded graphQL`)));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private loaders(name: Resource): Loader<string>[] {
|
|
155
|
+
return [
|
|
156
|
+
loader(() => this.loadResource(name), `loadResource(${name})`),
|
|
157
|
+
loader(() => fallbackResource(name, this.log), `fallbackResource(${name})`),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private schemaLoaders(relativeUri: string): Loader<string>[] {
|
|
162
|
+
return [
|
|
163
|
+
loader(() => this.loadSchema(relativeUri), `loadSchema(${relativeUri})`),
|
|
164
|
+
loader(() => fallbackSchema(relativeUri, this.log), `fallbackSchema(${relativeUri})`),
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private graphQLLoaders(): Loader<string>[] {
|
|
169
|
+
return [
|
|
170
|
+
loader(() => this.loadGraphQL(), `loadGraphQL()`),
|
|
171
|
+
loader(() => fallbackGraphQL(this.log), `fallbackSchema()`),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface Loader<A> {
|
|
177
|
+
(): Promise<A>;
|
|
178
|
+
loaderName: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function loader<A>(fn: () => Promise<A>, loaderName: string): Loader<A> {
|
|
182
|
+
return Object.assign(fn, { loaderName });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Find the first resource that can be loaded and transformed.
|
|
187
|
+
*
|
|
188
|
+
* Will try to load the resource from the loaders in order. If the loader
|
|
189
|
+
* throws an error, it will try the next loader. If all loaders throw an
|
|
190
|
+
* error, it will return the default value.
|
|
191
|
+
*
|
|
192
|
+
* This should allow us to load the latest version of the resource if it's
|
|
193
|
+
* available, and fall back to the local version if it's not. If neither
|
|
194
|
+
* work, we'll just return the default value.
|
|
195
|
+
*/
|
|
196
|
+
async function findSuitableResource<B, A = string>(
|
|
197
|
+
dataLoaders: Loader<A>[],
|
|
198
|
+
transform: (x: A) => B,
|
|
199
|
+
defaultValue: B,
|
|
200
|
+
log: Logger,
|
|
201
|
+
): Promise<B> {
|
|
202
|
+
for (const loader of dataLoaders) {
|
|
203
|
+
try {
|
|
204
|
+
return transform(await loader());
|
|
205
|
+
} catch (e) {
|
|
206
|
+
log(
|
|
207
|
+
`Failed to load or transform ${loader.loaderName} with the following error:\n${
|
|
208
|
+
e instanceof Error ? e.message : e
|
|
209
|
+
}`,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return defaultValue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The root directory for the data files. This is different in the VS Code build
|
|
220
|
+
* (since those files are copied to the dist folder at a different relative path)
|
|
221
|
+
*/
|
|
222
|
+
function dataRoot() {
|
|
223
|
+
if (process.env.WEBPACK_MODE) {
|
|
224
|
+
return path.resolve(__dirname, './data');
|
|
225
|
+
} else {
|
|
226
|
+
return path.resolve(__dirname, '../data');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Returns the at-build-time path to the fallback data file. */
|
|
231
|
+
async function fallbackResource(name: Resource, log: Logger): Promise<string> {
|
|
232
|
+
const sourcePath = path.resolve(dataRoot(), `${name}.json`);
|
|
233
|
+
return fs
|
|
234
|
+
.readFile(sourcePath, 'utf8')
|
|
235
|
+
.then(tap(() => log(`Loaded fallback resource\n\t${name} from\n\t${sourcePath}`)));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Returns the at-build-time path to the fallback schema file. */
|
|
239
|
+
async function fallbackSchema(
|
|
240
|
+
/** e.g. themes/section.json */
|
|
241
|
+
relativeUri: string,
|
|
242
|
+
log: Logger,
|
|
243
|
+
): Promise<string> {
|
|
244
|
+
const sourcePath = path.resolve(dataRoot(), path.basename(relativeUri));
|
|
245
|
+
return fs
|
|
246
|
+
.readFile(sourcePath, 'utf8')
|
|
247
|
+
.then(tap(() => log(`Loaded fallback schema\n\t${relativeUri} from\n\t${sourcePath}`)));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function fallbackGraphQL(log: Logger): Promise<string> {
|
|
251
|
+
const sourcePath = path.resolve(dataRoot(), `graphql.graphql`);
|
|
252
|
+
return fs
|
|
253
|
+
.readFile(sourcePath, 'utf8')
|
|
254
|
+
.then(tap(() => log(`Loaded fallback graphQL from\n\t${sourcePath}`)));
|
|
255
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type Logger = (message: string) => void;
|
|
2
|
+
export const noop = () => {};
|
|
3
|
+
|
|
4
|
+
export const identity = <T>(x: T): T => x;
|
|
5
|
+
export const tap = <T>(tappingFunction: (x: T) => void) => {
|
|
6
|
+
return (x: T) => {
|
|
7
|
+
tappingFunction(x);
|
|
8
|
+
return x;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export { memo, memoize } from '@platformos/platformos-check-common';
|