@shopify/theme-check-docs-updater
Advanced tools
Comparing version 2.0.4 to 2.1.0
# @shopify/theme-check-docs-updater | ||
## 2.1.0 | ||
### Minor Changes | ||
- 042f1e0: Breaking: internal rename of `schemaValidators` to `jsonValidationSet` | ||
This breaks the browser dependencies public API (for `startServer` and `runChecks`) and will thus require some code changes in those contexts. | ||
The node packages absorb the dependency injection and are not breaking. | ||
### Patch Changes | ||
- Updated dependencies [042f1e0] | ||
- Updated dependencies [a9ae65f] | ||
- @shopify/theme-check-common@2.1.0 | ||
## 2.0.4 | ||
@@ -4,0 +20,0 @@ |
@@ -1,2 +0,2 @@ | ||
{ "revision": "f7040f8b8c747ab62910cd5bb581a706ef6bfb76" } | ||
{ "revision": "8f45acec4e008812e35c4b75def37b9da377557d" } | ||
export type Resource = (typeof Resources)[number]; | ||
export declare const Resources: readonly ["filters", "objects", "tags", "section_schema", "shopify_system_translations"]; | ||
export declare const Resources: readonly ["filters", "objects", "tags", "section_schema", "translations_schema", "shopify_system_translations"]; | ||
export declare function downloadFile(file: Resource | 'latest', destination: string): Promise<void>; | ||
export declare function exists(path: string): Promise<boolean>; | ||
export declare function downloadThemeLiquidDocs(outputDir: string): Promise<void>; |
@@ -15,2 +15,3 @@ "use strict"; | ||
'section_schema', | ||
'translations_schema', | ||
'shopify_system_translations', | ||
@@ -24,2 +25,3 @@ ]; | ||
section_schema: 'schemas/theme/section_schema.json', | ||
translations_schema: 'schemas/theme/translations_schema.json', | ||
shopify_system_translations: 'data/shopify_system_translations.json', | ||
@@ -26,0 +28,0 @@ }; |
@@ -1,5 +0,4 @@ | ||
import { FilterEntry, ObjectEntry, TagEntry, ThemeDocset, JsonSchemaValidators, Translations } from '@shopify/theme-check-common'; | ||
import { ValidateFunction } from 'ajv'; | ||
import { FilterEntry, ObjectEntry, TagEntry, ThemeDocset, JsonValidationSet, Translations, ValidateFunction } from '@shopify/theme-check-common'; | ||
type Logger = (message: string) => void; | ||
export declare class ThemeLiquidDocsManager implements ThemeDocset, JsonSchemaValidators { | ||
export declare class ThemeLiquidDocsManager implements ThemeDocset, JsonValidationSet { | ||
private log; | ||
@@ -11,3 +10,5 @@ constructor(log?: Logger); | ||
systemTranslations: () => Promise<Translations>; | ||
sectionSchema: () => Promise<string>; | ||
validateSectionSchema: () => Promise<ValidateFunction<unknown>>; | ||
translationSchema: () => Promise<string>; | ||
/** | ||
@@ -21,6 +22,7 @@ * The setup method checks that the latest revision matches the one from | ||
setup: () => Promise<void>; | ||
private latestRevision; | ||
private loadResource; | ||
private load; | ||
private latestRevision; | ||
private loaders; | ||
} | ||
export {}; |
@@ -12,40 +12,27 @@ "use strict"; | ||
const utils_1 = require("./utils"); | ||
function dataRoot() { | ||
if (process.env.WEBPACK_MODE) { | ||
return node_path_1.default.resolve(__dirname, './data'); | ||
} | ||
else { | ||
return node_path_1.default.resolve(__dirname, '../data'); | ||
} | ||
} | ||
async function fallback(name, defaultValue) { | ||
try { | ||
const content = await promises_1.default.readFile(node_path_1.default.resolve(dataRoot(), name), 'utf8'); | ||
return JSON.parse(content); | ||
} | ||
catch (_) { | ||
return defaultValue; | ||
} | ||
} | ||
const fp_1 = require("lodash/fp"); | ||
class ThemeLiquidDocsManager { | ||
constructor(log = utils_1.noop) { | ||
this.log = log; | ||
// These methods are memoized so that they both are lazy and cached with | ||
// minimal amount of state lying around. | ||
this.filters = (0, utils_1.memo)(async () => { | ||
return this.loadResource('filters', await fallback('filters.json', [])); | ||
return findSuitableResource(this.loaders('filters'), JSON.parse, []); | ||
}); | ||
this.objects = (0, utils_1.memo)(async () => { | ||
return this.loadResource('objects', await fallback('objects.json', [])); | ||
return findSuitableResource(this.loaders('objects'), JSON.parse, []); | ||
}); | ||
this.tags = (0, utils_1.memo)(async () => { | ||
return this.loadResource('tags', await fallback('tags.json', [])); | ||
return findSuitableResource(this.loaders('tags'), JSON.parse, []); | ||
}); | ||
this.systemTranslations = (0, utils_1.memo)(async () => { | ||
return this.loadResource('shopify_system_translations', await fallback('shopify_system_translations.json', {})); | ||
return findSuitableResource(this.loaders('shopify_system_translations'), JSON.parse, {}); | ||
}); | ||
this.sectionSchema = (0, utils_1.memo)(async () => { | ||
return findSuitableResource(this.loaders('section_schema'), identity, '{}'); | ||
}); | ||
this.validateSectionSchema = (0, utils_1.memo)(async () => { | ||
const sectionSchema = await this.loadResource('section_schema', await fallback('section_schema.json', {})); | ||
return (0, jsonSchemaCompiler_1.compileJsonSchema)(sectionSchema); | ||
return findSuitableResource([this.sectionSchema], (0, fp_1.pipe)(JSON.parse, jsonSchemaCompiler_1.compileJsonSchema), alwaysValid()); | ||
}); | ||
this.translationSchema = (0, utils_1.memo)(async () => { | ||
return findSuitableResource(this.loaders('translations_schema'), identity, '{}'); | ||
}); | ||
/** | ||
@@ -77,26 +64,65 @@ * The setup method checks that the latest revision matches the one from | ||
} | ||
async loadResource(name, defaultValue) { | ||
async latestRevision() { | ||
var _a; | ||
const latest = await findSuitableResource([() => this.load('latest')], JSON.parse, {}); | ||
return (_a = latest['revision']) !== null && _a !== void 0 ? _a : ''; | ||
} | ||
async loadResource(name) { | ||
// Always wait for setup first. Since it's memoized, this will always | ||
// be the same promise. | ||
await this.setup(); | ||
return this.load(name, defaultValue); | ||
return this.load(name); | ||
} | ||
async load(name, defaultValue) { | ||
async load(name) { | ||
return promises_1.default.readFile((0, utils_1.filePath)(name), 'utf8'); | ||
} | ||
loaders(name) { | ||
return [() => this.loadResource(name), () => fallback(name)]; | ||
} | ||
} | ||
exports.ThemeLiquidDocsManager = ThemeLiquidDocsManager; | ||
const identity = (x) => x; | ||
/** | ||
* Find the first resource that can be loaded and transformed. | ||
* | ||
* Will try to load the resource from the loaders in order. If the loader | ||
* throws an error, it will try the next loader. If all loaders throw an | ||
* error, it will return the default value. | ||
* | ||
* This should allow us to load the latest version of the resource if it's | ||
* available, and fall back to the local version if it's not. If neither | ||
* work, we'll just return the default value. | ||
*/ | ||
async function findSuitableResource(dataLoaders, transform, defaultValue) { | ||
for (const loader of dataLoaders) { | ||
try { | ||
const content = await promises_1.default.readFile((0, utils_1.filePath)(name), 'utf8'); | ||
const json = JSON.parse(content); | ||
return json; | ||
return transform(await loader()); | ||
} | ||
catch (err) { | ||
this.log(`[SERVER] Error loading theme resource (${name}), ${err.message}`); | ||
return defaultValue; | ||
catch (_) { | ||
continue; | ||
} | ||
} | ||
async latestRevision() { | ||
var _a; | ||
const latest = await this.load('latest', {}); | ||
return (_a = latest['revision']) !== null && _a !== void 0 ? _a : ''; | ||
return defaultValue; | ||
} | ||
/** | ||
* The root directory for the data files. This is different in the VS Code build | ||
* (since those files are copied to the dist folder at a different relative path) | ||
*/ | ||
function dataRoot() { | ||
if (process.env.WEBPACK_MODE) { | ||
return node_path_1.default.resolve(__dirname, './data'); | ||
} | ||
else { | ||
return node_path_1.default.resolve(__dirname, '../data'); | ||
} | ||
} | ||
exports.ThemeLiquidDocsManager = ThemeLiquidDocsManager; | ||
/** Returns the at-build-time path to the fallback data file. */ | ||
async function fallback(name) { | ||
return promises_1.default.readFile(node_path_1.default.resolve(dataRoot(), `${name}.json`), 'utf8'); | ||
} | ||
function alwaysValid() { | ||
const validate = () => true; | ||
validate.errors = null; | ||
return validate; | ||
} | ||
//# sourceMappingURL=themeLiquidDocsManager.js.map |
{ | ||
"name": "@shopify/theme-check-docs-updater", | ||
"version": "2.0.4", | ||
"version": "2.1.0", | ||
"description": "Scripts to initialize theme-check data with assets from the theme-liquid-docs repo.", | ||
@@ -33,3 +33,3 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"@shopify/theme-check-common": "^2.0.4", | ||
"@shopify/theme-check-common": "^2.1.0", | ||
"ajv": "^8.12.0", | ||
@@ -36,0 +36,0 @@ "env-paths": "^2.2.1", |
@@ -11,2 +11,3 @@ import fs from 'node:fs/promises'; | ||
'section_schema', | ||
'translations_schema', | ||
'shopify_system_translations', | ||
@@ -21,2 +22,3 @@ ] as const; | ||
section_schema: 'schemas/theme/section_schema.json', | ||
translations_schema: 'schemas/theme/translations_schema.json', | ||
shopify_system_translations: 'data/shopify_system_translations.json', | ||
@@ -23,0 +25,0 @@ }; |
@@ -1,4 +0,3 @@ | ||
import { expect, describe, it, beforeEach, afterEach, vi } from 'vitest'; | ||
import { expect, describe, it, beforeEach, afterEach, vi, assert } from 'vitest'; | ||
import { ThemeLiquidDocsManager } from './themeLiquidDocsManager'; | ||
import fs from 'node:fs/promises'; | ||
import { downloadFile, Resources } from './themeLiquidDocsDownloader'; | ||
@@ -135,4 +134,3 @@ | ||
const error = validate.errors[0]; | ||
expect(error.keyword).toBe('required'); | ||
expect(error.params.missingProperty).toBe('age'); | ||
expect(error.message).to.match(/'age'/); | ||
} | ||
@@ -139,0 +137,0 @@ }); |
@@ -6,6 +6,6 @@ import { | ||
ThemeDocset, | ||
JsonSchemaValidators, | ||
JsonValidationSet, | ||
Translations, | ||
ValidateFunction, | ||
} from '@shopify/theme-check-common'; | ||
import { ValidateFunction } from 'ajv'; | ||
import path from 'node:path'; | ||
@@ -16,54 +16,41 @@ import fs from 'node:fs/promises'; | ||
import { download, filePath, memo, noop, root } from './utils'; | ||
import { pipe } from 'lodash/fp'; | ||
type Logger = (message: string) => void; | ||
function dataRoot() { | ||
if (process.env.WEBPACK_MODE) { | ||
return path.resolve(__dirname, './data'); | ||
} else { | ||
return path.resolve(__dirname, '../data'); | ||
} | ||
} | ||
async function fallback<T>(name: string, defaultValue: T): Promise<T> { | ||
try { | ||
const content = await fs.readFile(path.resolve(dataRoot(), name), 'utf8'); | ||
return JSON.parse(content); | ||
} catch (_) { | ||
return defaultValue; | ||
} | ||
} | ||
export class ThemeLiquidDocsManager implements ThemeDocset, JsonSchemaValidators { | ||
export class ThemeLiquidDocsManager implements ThemeDocset, JsonValidationSet { | ||
constructor(private log: Logger = noop) {} | ||
// These methods are memoized so that they both are lazy and cached with | ||
// minimal amount of state lying around. | ||
filters = memo(async (): Promise<FilterEntry[]> => { | ||
return this.loadResource('filters', await fallback('filters.json', [])); | ||
return findSuitableResource(this.loaders('filters'), JSON.parse, []); | ||
}); | ||
objects = memo(async (): Promise<ObjectEntry[]> => { | ||
return this.loadResource('objects', await fallback('objects.json', [])); | ||
return findSuitableResource(this.loaders('objects'), JSON.parse, []); | ||
}); | ||
tags = memo(async (): Promise<TagEntry[]> => { | ||
return this.loadResource('tags', await fallback('tags.json', [])); | ||
return findSuitableResource(this.loaders('tags'), JSON.parse, []); | ||
}); | ||
systemTranslations = memo(async (): Promise<Translations> => { | ||
return this.loadResource( | ||
'shopify_system_translations', | ||
await fallback('shopify_system_translations.json', {}), | ||
); | ||
return findSuitableResource(this.loaders('shopify_system_translations'), JSON.parse, {}); | ||
}); | ||
sectionSchema = memo(async (): Promise<string> => { | ||
return findSuitableResource(this.loaders('section_schema'), identity, '{}'); | ||
}); | ||
validateSectionSchema = memo(async (): Promise<ValidateFunction> => { | ||
const sectionSchema = await this.loadResource( | ||
'section_schema', | ||
await fallback('section_schema.json', {}), | ||
return findSuitableResource( | ||
[this.sectionSchema], | ||
pipe(JSON.parse, compileJsonSchema), | ||
alwaysValid(), | ||
); | ||
return compileJsonSchema(sectionSchema); | ||
}); | ||
translationSchema = memo(async (): Promise<string> => { | ||
return findSuitableResource(this.loaders('translations_schema'), identity, '{}'); | ||
}); | ||
/** | ||
@@ -94,25 +81,72 @@ * The setup method checks that the latest revision matches the one from | ||
private async loadResource<T>(name: Resource, defaultValue: T) { | ||
private async latestRevision(): Promise<string> { | ||
const latest = await findSuitableResource([() => this.load('latest')], JSON.parse, {}); | ||
return latest['revision'] ?? ''; | ||
} | ||
private async loadResource(name: Resource): Promise<string> { | ||
// Always wait for setup first. Since it's memoized, this will always | ||
// be the same promise. | ||
await this.setup(); | ||
return this.load(name, defaultValue); | ||
return this.load(name); | ||
} | ||
private async load<T>(name: Resource | 'latest', defaultValue: T) { | ||
private async load<T>(name: Resource | 'latest') { | ||
return fs.readFile(filePath(name), 'utf8'); | ||
} | ||
private loaders(name: Resource) { | ||
return [() => this.loadResource(name), () => fallback(name)]; | ||
} | ||
} | ||
const identity = <T>(x: T): T => x; | ||
/** | ||
* Find the first resource that can be loaded and transformed. | ||
* | ||
* Will try to load the resource from the loaders in order. If the loader | ||
* throws an error, it will try the next loader. If all loaders throw an | ||
* error, it will return the default value. | ||
* | ||
* This should allow us to load the latest version of the resource if it's | ||
* available, and fall back to the local version if it's not. If neither | ||
* work, we'll just return the default value. | ||
*/ | ||
async function findSuitableResource<B, A = string>( | ||
dataLoaders: (() => Promise<A>)[], | ||
transform: (x: A) => B, | ||
defaultValue: B, | ||
): Promise<B> { | ||
for (const loader of dataLoaders) { | ||
try { | ||
const content = await fs.readFile(filePath(name), 'utf8'); | ||
const json = JSON.parse(content); | ||
return json; | ||
} catch (err: any) { | ||
this.log(`[SERVER] Error loading theme resource (${name}), ${err.message}`); | ||
return defaultValue; | ||
return transform(await loader()); | ||
} catch (_) { | ||
continue; | ||
} | ||
} | ||
return defaultValue; | ||
} | ||
private async latestRevision(): Promise<string> { | ||
const latest = await this.load('latest', {}); | ||
return latest['revision'] ?? ''; | ||
/** | ||
* The root directory for the data files. This is different in the VS Code build | ||
* (since those files are copied to the dist folder at a different relative path) | ||
*/ | ||
function dataRoot() { | ||
if (process.env.WEBPACK_MODE) { | ||
return path.resolve(__dirname, './data'); | ||
} else { | ||
return path.resolve(__dirname, '../data'); | ||
} | ||
} | ||
/** Returns the at-build-time path to the fallback data file. */ | ||
async function fallback(name: Resource): Promise<string> { | ||
return fs.readFile(path.resolve(dataRoot(), `${name}.json`), 'utf8'); | ||
} | ||
function alwaysValid(): ValidateFunction { | ||
const validate: ValidateFunction = () => true; | ||
validate.errors = null; | ||
return validate; | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
1225624
34
30307
0