@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/data/app_block_entry.json +13 -0
  3. package/data/default_setting_values.json +24 -0
  4. package/data/filters.json +7 -0
  5. package/data/graphql.graphql +12964 -0
  6. package/data/latest.json +2 -0
  7. package/data/local_block_entry.json +25 -0
  8. package/data/manifest_theme.json +19 -0
  9. package/data/manifest_theme_app_extension.json +10 -0
  10. package/data/objects.json +6 -0
  11. package/data/preset.json +72 -0
  12. package/data/preset_blocks.json +91 -0
  13. package/data/section.json +208 -0
  14. package/data/setting.json +1353 -0
  15. package/data/settings.json +10 -0
  16. package/data/shopify_system_translations.json +2432 -0
  17. package/data/tags.json +7 -0
  18. package/data/targetted_block_entry.json +15 -0
  19. package/data/theme_block.json +91 -0
  20. package/data/theme_block_entry.json +14 -0
  21. package/data/theme_settings.json +83 -0
  22. package/data/translations.json +63 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +11 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/themeLiquidDocsDownloader.d.ts +23 -0
  27. package/dist/themeLiquidDocsDownloader.js +148 -0
  28. package/dist/themeLiquidDocsDownloader.js.map +1 -0
  29. package/dist/themeLiquidDocsManager.d.ts +34 -0
  30. package/dist/themeLiquidDocsManager.js +175 -0
  31. package/dist/themeLiquidDocsManager.js.map +1 -0
  32. package/dist/tsconfig.tsbuildinfo +1 -0
  33. package/dist/utils.d.ts +5 -0
  34. package/dist/utils.js +18 -0
  35. package/dist/utils.js.map +1 -0
  36. package/package.json +42 -0
  37. package/scripts/cli.js +48 -0
  38. package/src/index.ts +8 -0
  39. package/src/themeLiquidDocsDownloader.ts +192 -0
  40. package/src/themeLiquidDocsManager.spec.ts +103 -0
  41. package/src/themeLiquidDocsManager.ts +255 -0
  42. package/src/utils.ts +12 -0
  43. package/tsconfig.build.json +7 -0
  44. 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,8 @@
1
+ export { ThemeLiquidDocsManager } from './themeLiquidDocsManager';
2
+ export {
3
+ Resource,
4
+ Resources,
5
+ downloadResource,
6
+ downloadThemeLiquidDocs,
7
+ root,
8
+ } from './themeLiquidDocsDownloader';
@@ -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';
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["**/*.spec.ts"],
4
+ "references": [
5
+ { "path": "../platformos-check-common/tsconfig.build.json" }
6
+ ]
7
+ }