From 8b824fba49f6168d7aae681c2c6ec0d3e0714eb3 Mon Sep 17 00:00:00 2001 From: "Charles-P. Clermont" Date: Mon, 19 Feb 2024 16:28:10 -0500 Subject: [PATCH 1/2] Add section Schema & locales/*.json ICC + hover --- .changeset/afraid-apes-impress.md | 17 + .changeset/small-ants-complain.md | 16 + .../playground/src/language-server-worker.ts | 4 +- .../liquid-html-parser/src/stage-2-ast.ts | 2 - packages/release-orchestrator/package.json | 3 +- .../src/generate-markdown.spec.ts | 15 +- .../src/AugmentedJsonValidationSet.ts | 11 + .../src/AugmentedSchemaValidators.ts | 9 - .../src/checks/valid-schema/index.spec.ts | 10 +- .../src/checks/valid-schema/index.ts | 2 +- packages/theme-check-common/src/index.ts | 8 +- packages/theme-check-common/src/types.ts | 4 +- .../src/types/theme-liquid-docs.ts | 8 +- .../src/themeLiquidDocsDownloader.ts | 2 + .../src/themeLiquidDocsManager.spec.ts | 6 +- .../src/themeLiquidDocsManager.ts | 126 +++++--- packages/theme-check-node/src/file-utils.ts | 2 +- packages/theme-check-node/src/index.ts | 2 +- .../theme-language-server-common/package.json | 1 + .../src/diagnostics/runChecks.spec.ts | 8 +- .../src/diagnostics/runChecks.ts | 6 +- .../src/json/JSONLanguageService.spec.ts | 296 ++++++++++++++++++ .../src/json/JSONLanguageService.ts | 127 ++++++++ .../src/json/TranslationFileContributions.ts | 136 ++++++++ .../src/server/startServer.spec.ts | 4 +- .../src/server/startServer.ts | 23 +- .../theme-language-server-common/src/types.ts | 2 +- .../theme-language-server-node/src/index.ts | 2 +- yarn.lock | 131 +++----- 29 files changed, 806 insertions(+), 177 deletions(-) create mode 100644 .changeset/afraid-apes-impress.md create mode 100644 .changeset/small-ants-complain.md create mode 100644 packages/theme-check-common/src/AugmentedJsonValidationSet.ts delete mode 100644 packages/theme-check-common/src/AugmentedSchemaValidators.ts create mode 100644 packages/theme-language-server-common/src/json/JSONLanguageService.spec.ts create mode 100644 packages/theme-language-server-common/src/json/JSONLanguageService.ts create mode 100644 packages/theme-language-server-common/src/json/TranslationFileContributions.ts diff --git a/.changeset/afraid-apes-impress.md b/.changeset/afraid-apes-impress.md new file mode 100644 index 00000000..562a82b9 --- /dev/null +++ b/.changeset/afraid-apes-impress.md @@ -0,0 +1,17 @@ +--- +'@shopify/theme-language-server-browser': minor +'@shopify/theme-language-server-common': minor +'@shopify/theme-language-server-node': minor +'@shopify/theme-check-docs-updater': minor +'@shopify/theme-check-browser': minor +'@shopify/theme-check-common': minor +'@shopify/theme-check-node': minor +'@shopify/codemirror-language-client': patch +'release-orchestrator': patch +--- + +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. diff --git a/.changeset/small-ants-complain.md b/.changeset/small-ants-complain.md new file mode 100644 index 00000000..4cb4ef6d --- /dev/null +++ b/.changeset/small-ants-complain.md @@ -0,0 +1,16 @@ +--- +'@shopify/theme-language-server-common': minor +'@shopify/theme-language-server-node': minor +'@shopify/theme-language-server-browser': minor +'theme-check-vscode': minor +--- + +Add section schema and translation file JSON completion and hover support + +JSON object authoring and editing should be better in the following contexts: +- `sections/*.liquid` `{% schema %}` bodies +- `locales/*.json` files + +Hovering over any key in any translation file will show the path of the translation key (for easy copy and paste). + +Pluralized strings and `_html` support is baked into the feature. diff --git a/packages/codemirror-language-client/playground/src/language-server-worker.ts b/packages/codemirror-language-client/playground/src/language-server-worker.ts index d3ee4d34..b48bc230 100644 --- a/packages/codemirror-language-client/playground/src/language-server-worker.ts +++ b/packages/codemirror-language-client/playground/src/language-server-worker.ts @@ -75,8 +75,10 @@ startServer(worker, { objects: async () => objects, systemTranslations: async () => systemTranslations, }, - schemaValidators: { + jsonValidationSet: { validateSectionSchema: async () => () => true, + sectionSchema: async () => '{}', + translationSchema: async () => '{}', }, loadConfig, log(message) { diff --git a/packages/liquid-html-parser/src/stage-2-ast.ts b/packages/liquid-html-parser/src/stage-2-ast.ts index 3e26d8c5..fc68f2f3 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.ts @@ -1916,8 +1916,6 @@ function toTextNode(node: ConcreteTextNode): TextNode { }; } -const MAX_NUMBER_OF_SIBLING_DANGLING_NODES = 2; - function isAcceptableDanglingMarkerClose( builder: ASTBuilder, cst: LiquidHtmlCST, diff --git a/packages/release-orchestrator/package.json b/packages/release-orchestrator/package.json index 5465b08b..4ced234e 100644 --- a/packages/release-orchestrator/package.json +++ b/packages/release-orchestrator/package.json @@ -10,6 +10,7 @@ "build": "yarn build:ts", "build:ci": "yarn build", "build:ts": "tsc -b tsconfig.build.json", - "start": "yarn build && node dist/index.js" + "start": "yarn build && node dist/index.js", + "type-check": "tsc --noEmit" } } diff --git a/packages/release-orchestrator/src/generate-markdown.spec.ts b/packages/release-orchestrator/src/generate-markdown.spec.ts index 0cafcde0..603c1a89 100644 --- a/packages/release-orchestrator/src/generate-markdown.spec.ts +++ b/packages/release-orchestrator/src/generate-markdown.spec.ts @@ -1,16 +1,25 @@ import { expect, it, describe } from 'vitest'; import { generateMarkdown } from './generate-markdown'; +import { ChangesetStatus } from './types'; describe('generateMarkdown', () => { it('should generate a markdown string detailing the changes made in each release', () => { - const status = { + const status: ChangesetStatus = { releases: [ { name: 'package1', type: 'patch', oldVersion: '1.0.0', newVersion: '1.0.1' }, { name: 'package2', type: 'minor', oldVersion: '2.0.0', newVersion: '2.1.0' }, ], changesets: [ - { summary: 'Fixed a bug in package1', releases: [{ name: 'package1', type: 'patch' }] }, - { summary: 'Added a feature to package2', releases: [{ name: 'package2', type: 'minor' }] }, + { + id: '0', + summary: 'Fixed a bug in package1', + releases: [{ name: 'package1', type: 'patch' }], + }, + { + id: '1', + summary: 'Added a feature to package2', + releases: [{ name: 'package2', type: 'minor' }], + }, ], }; diff --git a/packages/theme-check-common/src/AugmentedJsonValidationSet.ts b/packages/theme-check-common/src/AugmentedJsonValidationSet.ts new file mode 100644 index 00000000..031b9a36 --- /dev/null +++ b/packages/theme-check-common/src/AugmentedJsonValidationSet.ts @@ -0,0 +1,11 @@ +import { JsonValidationSet } from './types'; +import { memo } from './utils'; + +/** This simply memoizes the result to prevent over fetching */ +export class AugmentedJsonValidationSet implements JsonValidationSet { + constructor(private schemaValidators: JsonValidationSet) {} + public isAugmented = true; + sectionSchema = memo(async () => await this.schemaValidators.sectionSchema()); + translationSchema = memo(async () => await this.schemaValidators.translationSchema()); + validateSectionSchema = memo(async () => await this.schemaValidators.validateSectionSchema()); +} diff --git a/packages/theme-check-common/src/AugmentedSchemaValidators.ts b/packages/theme-check-common/src/AugmentedSchemaValidators.ts deleted file mode 100644 index e79d753c..00000000 --- a/packages/theme-check-common/src/AugmentedSchemaValidators.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { JsonSchemaValidators } from './types'; -import { memo } from './utils'; - -/** This simply memoizes the result to prevent over fetching */ -export class AugmentedSchemaValidators implements JsonSchemaValidators { - constructor(private schemaValidators: JsonSchemaValidators) {} - public isAugmented = true; - validateSectionSchema = memo(async () => await this.schemaValidators.validateSectionSchema()); -} diff --git a/packages/theme-check-common/src/checks/valid-schema/index.spec.ts b/packages/theme-check-common/src/checks/valid-schema/index.spec.ts index 0a0e6722..5283131c 100644 --- a/packages/theme-check-common/src/checks/valid-schema/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-schema/index.spec.ts @@ -44,7 +44,7 @@ lodashSet(INVALID_SECTION_SCHEMA, 'disabled_on', true); lodashSet(INVALID_SECTION_SCHEMA, 'max_blocks', 51); const buildMockDeps = (errors?: any[]): Partial => ({ - schemaValidators: { + jsonValidationSet: { async validateSectionSchema() { const mockValidator: ValidateFunction = () => { mockValidator.errors = errors ?? [ @@ -57,6 +57,14 @@ const buildMockDeps = (errors?: any[]): Partial => ({ return mockValidator; }, + + async sectionSchema() { + return JSON.stringify(VALID_SECTION_SCHEMA); + }, + + async translationSchema() { + return '{}'; + }, }, }); diff --git a/packages/theme-check-common/src/checks/valid-schema/index.ts b/packages/theme-check-common/src/checks/valid-schema/index.ts index 88c04a7c..64956dbc 100644 --- a/packages/theme-check-common/src/checks/valid-schema/index.ts +++ b/packages/theme-check-common/src/checks/valid-schema/index.ts @@ -53,7 +53,7 @@ export const ValidSchema: LiquidCheckDefinition = { return; } - const validateSectionSchema = await context.schemaValidators?.validateSectionSchema(); + const validateSectionSchema = await context.jsonValidationSet?.validateSectionSchema(); if (!validateSectionSchema) { return; } diff --git a/packages/theme-check-common/src/index.ts b/packages/theme-check-common/src/index.ts index 3ab95b2c..a7f9c716 100644 --- a/packages/theme-check-common/src/index.ts +++ b/packages/theme-check-common/src/index.ts @@ -25,10 +25,10 @@ import * as path from './path'; import { getPosition } from './utils'; import { isIgnored } from './ignore'; import { AugmentedThemeDocset } from './AugmentedThemeDocset'; -import { AugmentedSchemaValidators } from './AugmentedSchemaValidators'; +import { AugmentedJsonValidationSet } from './AugmentedJsonValidationSet'; export * from './AugmentedThemeDocset'; -export * from './AugmentedSchemaValidators'; +export * from './AugmentedJsonValidationSet'; export * from './fixes'; export * from './types'; export * from './checks'; @@ -55,8 +55,8 @@ export async function check( dependencies.themeDocset = new AugmentedThemeDocset(dependencies.themeDocset); } - if (dependencies.schemaValidators && !dependencies.schemaValidators.isAugmented) { - dependencies.schemaValidators = new AugmentedSchemaValidators(dependencies.schemaValidators); + if (dependencies.jsonValidationSet && !dependencies.jsonValidationSet.isAugmented) { + dependencies.jsonValidationSet = new AugmentedJsonValidationSet(dependencies.jsonValidationSet); } for (const type of Object.values(SourceCodeType)) { diff --git a/packages/theme-check-common/src/types.ts b/packages/theme-check-common/src/types.ts index 143c9912..a73679c7 100644 --- a/packages/theme-check-common/src/types.ts +++ b/packages/theme-check-common/src/types.ts @@ -12,7 +12,7 @@ import { Schema, Settings } from './types/schema-prop-factory'; import { StringCorrector, JSONCorrector } from './fixes'; -import { ThemeDocset, JsonSchemaValidators } from './types/theme-liquid-docs'; +import { ThemeDocset, JsonValidationSet } from './types/theme-liquid-docs'; export * from './types/theme-liquid-docs'; export * from './types/schema-prop-factory'; @@ -261,7 +261,7 @@ export interface Dependencies { fileExists(absolutePath: string): Promise; fileSize?(absolutePath: string): Promise; themeDocset?: ThemeDocset; - schemaValidators?: JsonSchemaValidators; + jsonValidationSet?: JsonValidationSet; } type StaticContextProperties = T extends SourceCodeType diff --git a/packages/theme-check-common/src/types/theme-liquid-docs.ts b/packages/theme-check-common/src/types/theme-liquid-docs.ts index da711ebe..441f5ce3 100644 --- a/packages/theme-check-common/src/types/theme-liquid-docs.ts +++ b/packages/theme-check-common/src/types/theme-liquid-docs.ts @@ -23,12 +23,18 @@ export interface ThemeDocset { /** * JSON schemas resources for themes. */ -export interface JsonSchemaValidators { +export interface JsonValidationSet { /** Whether it was augmented prior to being passed. */ isAugmented?: boolean; /** Retrieves the JSON schema validator for theme sections. */ validateSectionSchema(): Promise; + + /** Retrieves the JSON schema of the {% schema %} JSON blobs in sections/*.liquid files */ + sectionSchema(): Promise; + + /** Retrieves the JSON schema of the locales/*.json files */ + translationSchema(): Promise; } /** diff --git a/packages/theme-check-docs-updater/src/themeLiquidDocsDownloader.ts b/packages/theme-check-docs-updater/src/themeLiquidDocsDownloader.ts index cdc25bc9..34106907 100644 --- a/packages/theme-check-docs-updater/src/themeLiquidDocsDownloader.ts +++ b/packages/theme-check-docs-updater/src/themeLiquidDocsDownloader.ts @@ -8,6 +8,7 @@ export const Resources = [ 'objects', 'tags', 'section_schema', + 'translations_schema', 'shopify_system_translations', ] as const; @@ -17,6 +18,7 @@ const THEME_LIQUID_DOCS: Record = { tags: 'data/tags.json', latest: 'data/latest.json', section_schema: 'schemas/theme/section_schema.json', + translations_schema: 'schemas/theme/translations_schema.json', shopify_system_translations: 'data/shopify_system_translations.json', }; diff --git a/packages/theme-check-docs-updater/src/themeLiquidDocsManager.spec.ts b/packages/theme-check-docs-updater/src/themeLiquidDocsManager.spec.ts index 7546ee86..7990b3f4 100644 --- a/packages/theme-check-docs-updater/src/themeLiquidDocsManager.spec.ts +++ b/packages/theme-check-docs-updater/src/themeLiquidDocsManager.spec.ts @@ -1,6 +1,5 @@ -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'; vi.mock('./themeLiquidDocsDownloader', async (importOriginal) => { @@ -132,8 +131,7 @@ describe('Module: ThemeLiquidDocsManager', async () => { // Conditional to satisfy typescript. This is already checked by the previous expect. if (validate.errors) { const error = validate.errors[0]; - expect(error.keyword).toBe('required'); - expect(error.params.missingProperty).toBe('age'); + expect(error.message).to.match(/'age'/); } }); }); diff --git a/packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts b/packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts index aa859f92..66c9a07f 100644 --- a/packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts +++ b/packages/theme-check-docs-updater/src/themeLiquidDocsManager.ts @@ -3,65 +3,52 @@ import { ObjectEntry, TagEntry, ThemeDocset, - JsonSchemaValidators, + JsonValidationSet, Translations, + ValidateFunction, } from '@shopify/theme-check-common'; -import { ValidateFunction } from 'ajv'; import path from 'node:path'; import fs from 'node:fs/promises'; import { compileJsonSchema } from './jsonSchemaCompiler'; import { Resource, Resources, exists } from './themeLiquidDocsDownloader'; 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(name: string, defaultValue: T): Promise { - 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 => { - return this.loadResource('filters', await fallback('filters.json', [])); + return findSuitableResource(this.loaders('filters'), JSON.parse, []); }); objects = memo(async (): Promise => { - return this.loadResource('objects', await fallback('objects.json', [])); + return findSuitableResource(this.loaders('objects'), JSON.parse, []); }); tags = memo(async (): Promise => { - return this.loadResource('tags', await fallback('tags.json', [])); + return findSuitableResource(this.loaders('tags'), JSON.parse, []); }); systemTranslations = memo(async (): Promise => { - 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 => { + return findSuitableResource(this.loaders('section_schema'), identity, '{}'); }); validateSectionSchema = memo(async (): Promise => { - 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 => { + return findSuitableResource(this.loaders('translations_schema'), identity, '{}'); }); /** @@ -89,27 +76,74 @@ export class ThemeLiquidDocsManager implements ThemeDocset, JsonSchemaValidators } }); - private async loadResource(name: Resource, defaultValue: T) { + private async latestRevision(): Promise { + const latest = await findSuitableResource([() => this.load('latest')], JSON.parse, {}); + return latest['revision'] ?? ''; + } + + private async loadResource(name: Resource): Promise { // 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(name: Resource | 'latest', defaultValue: T) { - try { - const content = await fs.readFile(filePath(name), 'utf8'); - const json = JSON.parse(content); + private async load(name: Resource | 'latest') { + return fs.readFile(filePath(name), 'utf8'); + } + + private loaders(name: Resource) { + return [() => this.loadResource(name), () => fallback(name)]; + } +} - return json; - } catch (err: any) { - this.log(`[SERVER] Error loading theme resource (${name}), ${err.message}`); - return defaultValue; +const identity = (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( + dataLoaders: (() => Promise)[], + transform: (x: A) => B, + defaultValue: B, +): Promise { + for (const loader of dataLoaders) { + try { + return transform(await loader()); + } catch (_) { + continue; } } + return defaultValue; +} - private async latestRevision(): Promise { - 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 { + return fs.readFile(path.resolve(dataRoot(), `${name}.json`), 'utf8'); +} + +function alwaysValid(): ValidateFunction { + const validate: ValidateFunction = () => true; + validate.errors = null; + return validate; +} diff --git a/packages/theme-check-node/src/file-utils.ts b/packages/theme-check-node/src/file-utils.ts index 37b62b7a..7899b19e 100644 --- a/packages/theme-check-node/src/file-utils.ts +++ b/packages/theme-check-node/src/file-utils.ts @@ -10,7 +10,7 @@ export async function fileExists(path: string) { } } -export const fileSize: Dependencies['fileSize'] = async (path: string) => { +export const fileSize: NonNullable = async (path: string) => { try { const stats = await fs.stat(path); return stats.size; diff --git a/packages/theme-check-node/src/index.ts b/packages/theme-check-node/src/index.ts index 07486da5..c0ccb619 100644 --- a/packages/theme-check-node/src/index.ts +++ b/packages/theme-check-node/src/index.ts @@ -66,7 +66,7 @@ export async function themeCheckRun(root: string, configPath?: string): Promise< fileExists, fileSize, themeDocset: themeLiquidDocsManager, - schemaValidators: themeLiquidDocsManager, + jsonValidationSet: themeLiquidDocsManager, async getDefaultTranslations() { return defaultTranslations; }, diff --git a/packages/theme-language-server-common/package.json b/packages/theme-language-server-common/package.json index fe8fe8f7..ac4a1db4 100644 --- a/packages/theme-language-server-common/package.json +++ b/packages/theme-language-server-common/package.json @@ -30,6 +30,7 @@ "@shopify/liquid-html-parser": "^2.0.3", "@shopify/theme-check-common": "2.0.4", "@vscode/web-custom-data": "^0.4.6", + "vscode-json-languageservice": "^5.3.9", "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.8", "vscode-uri": "^3.0.7" diff --git a/packages/theme-language-server-common/src/diagnostics/runChecks.spec.ts b/packages/theme-language-server-common/src/diagnostics/runChecks.spec.ts index 28df24a2..a8afe926 100644 --- a/packages/theme-language-server-common/src/diagnostics/runChecks.spec.ts +++ b/packages/theme-language-server-common/src/diagnostics/runChecks.spec.ts @@ -68,8 +68,10 @@ describe('Module: runChecks', () => { tags: async () => [], systemTranslations: async () => ({}), }, - schemaValidators: { + jsonValidationSet: { validateSectionSchema: async () => ({} as ValidateFunction), + sectionSchema: async () => '{}', + translationSchema: async () => '{}', }, }); }); @@ -222,8 +224,10 @@ describe('Module: runChecks', () => { tags: async () => [], systemTranslations: async () => ({}), }, - schemaValidators: { + jsonValidationSet: { validateSectionSchema: async () => ({} as ValidateFunction), + sectionSchema: async () => '{}', + translationSchema: async () => '{}', }, }); diff --git a/packages/theme-language-server-common/src/diagnostics/runChecks.ts b/packages/theme-language-server-common/src/diagnostics/runChecks.ts index 0f344968..93996178 100644 --- a/packages/theme-language-server-common/src/diagnostics/runChecks.ts +++ b/packages/theme-language-server-common/src/diagnostics/runChecks.ts @@ -17,7 +17,7 @@ export function makeRunChecks( getDefaultTranslationsFactory, getDefaultLocaleFactory, themeDocset, - schemaValidators, + jsonValidationSet, }: Pick< Dependencies, | 'loadConfig' @@ -27,7 +27,7 @@ export function makeRunChecks( | 'getDefaultTranslationsFactory' | 'getDefaultLocaleFactory' | 'themeDocset' - | 'schemaValidators' + | 'jsonValidationSet' >, ) { return async function runChecks(triggerURIs: string[]): Promise { @@ -64,8 +64,8 @@ export function makeRunChecks( fileSize, getDefaultLocale: getDefaultLocaleFactory(rootURI.toString()), getDefaultTranslations: async () => defaultTranslations, + jsonValidationSet, themeDocset, - schemaValidators, }); // We iterate over the theme files (as opposed to offenses) because if diff --git a/packages/theme-language-server-common/src/json/JSONLanguageService.spec.ts b/packages/theme-language-server-common/src/json/JSONLanguageService.spec.ts new file mode 100644 index 00000000..dda93b5a --- /dev/null +++ b/packages/theme-language-server-common/src/json/JSONLanguageService.spec.ts @@ -0,0 +1,296 @@ +import { JsonValidationSet, ValidateFunction } from '@shopify/theme-check-common'; +import { assert, beforeEach, describe, expect, it } from 'vitest'; +import { CompletionParams, HoverParams, ClientCapabilities } from 'vscode-languageserver'; +import { DocumentManager } from '../documents'; +import { JSONLanguageService } from './JSONLanguageService'; + +const simplifiedSectionSchema = JSON.stringify({ + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string', + description: 'The section title shown in the theme editor', + }, + class: { + type: 'string', + description: 'Additional CSS class for the section', + }, + }, +}); + +const simplifiedTranslationSchema = JSON.stringify({ + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + additionalProperties: { + oneOf: [ + { + type: 'string', + }, + { + $ref: '#/$defs/pluralizedString', + }, + { + $ref: '#', + }, + ], + }, + $defs: { + pluralizedString: { + type: 'object', + properties: { + one: { + type: 'string', + description: 'Translation for the singular form', + }, + other: { + type: 'string', + description: 'Translation for the plural form', + }, + }, + additionalProperties: false, + description: 'An object representing a pluralized translation string', + }, + }, +}); + +describe('Module: JSONLanguageService', () => { + let jsonLanguageService: JSONLanguageService; + let documentManager: DocumentManager; + + beforeEach(() => { + documentManager = new DocumentManager(); + jsonLanguageService = new JSONLanguageService(documentManager, { + async validateSectionSchema() { + const mockValidator: ValidateFunction = () => { + mockValidator.errors = []; + return false; + }; + return mockValidator; + }, + + async sectionSchema() { + return simplifiedSectionSchema; + }, + + async translationSchema() { + return simplifiedTranslationSchema; + }, + }); + + jsonLanguageService.setup({ + textDocument: { + completion: { + contextSupport: true, + completionItem: { + snippetSupport: true, + commitCharactersSupport: true, + documentationFormat: ['markdown'], + deprecatedSupport: true, + preselectSupport: true, + }, + }, + }, + }); + }); + + describe('completions', () => { + it('should return section JSON schema completions in a section file {% schema %}', async () => { + const params = getParams( + documentManager, + 'sections/section.liquid', + ` +
hello world
+ {% schema %} + { + "name": "My section", + "█" + } + {% endschema %} + `, + ); + + const completions = await jsonLanguageService.completions(params); + assert( + typeof completions === 'object' && completions !== null && !Array.isArray(completions), + ); + expect(completions.items).to.have.lengthOf(1); + expect(completions.items[0].label).to.equal('class'); + expect(completions.items[0].documentation).to.equal('Additional CSS class for the section'); + }); + + it('should return translation key completions in a theme translation file', async () => { + const paths = [ + 'locales/en.default.json', + 'locales/en.default.schema.json', + 'locales/fr.json', + 'locales/fr.schema.json', + ]; + for (const path of paths) { + const params = getParams( + documentManager, + path, + ` + { + "general": { + "hello": { + "other": "hey folks", + "█" + } + } + } + `, + ); + const completions = await jsonLanguageService.completions(params); + assert( + typeof completions === 'object' && completions !== null && !Array.isArray(completions), + ); + expect(completions.items).to.have.lengthOf(1); + expect(completions.items[0].label).to.equal('one'); + expect(completions.items[0].documentation).to.eql('Translation for the singular form'); + } + }); + }); + + describe('hover', () => { + it('should return hover information for the given property in a section {% schema %}', async () => { + const params = getParams( + documentManager, + 'sections/section.liquid', + ` +
hello world
+ {% schema %} + { + "name█": "My section" + } + {% endschema %} + `, + ); + const hover = await jsonLanguageService.hover(params); + assert(hover !== null); + expect(hover.contents).to.eql(['The section title shown in the theme editor']); + }); + + it('should return the path of a translation key in a theme translation file', async () => { + const paths = ['locales/en.default.json', 'locales/fr.json']; + for (const path of paths) { + const params = getParams( + documentManager, + path, + ` + { + "general": { + "hello█": "world" + } + } + `, + ); + const hover = await jsonLanguageService.hover(params); + assert(hover !== null); + expect(hover.contents).to.eql([{ language: 'liquid', value: `{{ 'general.hello' | t }}` }]); + } + }); + + it('should return add interpolated variables to the hover', async () => { + const paths = ['locales/en.default.json', 'locales/fr.json']; + for (const path of paths) { + const params = getParams( + documentManager, + path, + ` + { + "general": { + "hello█": " {{ var1 }} world {{ var2 }}" + } + } + `, + ); + const hover = await jsonLanguageService.hover(params); + assert(hover !== null); + expect(hover.contents).to.eql([ + { language: 'liquid', value: `{{ 'general.hello' | t: var1: var1, var2: var2 }}` }, + ]); + } + }); + + it('should return translation group hover for non-leaf nodes', async () => { + const paths = ['locales/en.default.json', 'locales/fr.json']; + for (const path of paths) { + const params = getParams( + documentManager, + path, + ` + { + "general█": { + "hello": " {{ var1 }} world {{ var2 }}" + } + } + `, + ); + const hover = await jsonLanguageService.hover(params); + assert(hover !== null); + expect(hover.contents).to.eql([`Translation group: general`]); + } + }); + + it('should return the parent translation key for pluralized translations', async () => { + const paths = ['locales/en.default.json', 'locales/fr.json']; + for (const path of paths) { + const params = getParams( + documentManager, + path, + ` + { + "general": { + "one█": " {{ var1 }} world" + } + } + `, + ); + const hover = await jsonLanguageService.hover(params); + assert(hover !== null); + expect(hover.contents).to.eql([ + { language: 'liquid', value: `{{ 'general' | t: var1: var1 }}` }, + ]); + } + }); + + it('should return the t:path of a translation key in a schema translation file', async () => { + const paths = ['locales/en.default.schema.json', 'locales/fr.schema.json']; + for (const path of paths) { + const params = getParams( + documentManager, + path, + ` + { + "general": { + "hello█": "world" + } + } + `, + ); + const hover = await jsonLanguageService.hover(params); + assert(hover !== null); + expect(hover.contents).to.eql([{ language: 'json', value: `"t:general.hello"` }]); + } + }); + }); + + function getParams( + documentManager: DocumentManager, + relativePath: string, + source: string, + ): HoverParams & CompletionParams { + const uri = `file:///root/${relativePath}`; + const sourceWithoutCursor = source.replace('█', ''); + documentManager.open(uri, sourceWithoutCursor, 1); + const doc = documentManager.get(uri)!.textDocument; + const position = doc.positionAt(source.indexOf('█')); + + return { + textDocument: { uri: uri }, + position: position, + }; + } +}); diff --git a/packages/theme-language-server-common/src/json/JSONLanguageService.ts b/packages/theme-language-server-common/src/json/JSONLanguageService.ts new file mode 100644 index 00000000..5d28d6fd --- /dev/null +++ b/packages/theme-language-server-common/src/json/JSONLanguageService.ts @@ -0,0 +1,127 @@ +import { LiquidRawTag, NodeTypes } from '@shopify/liquid-html-parser'; +import { JsonValidationSet, SourceCodeType } from '@shopify/theme-check-common'; +import { JSONDocument, LanguageService, getLanguageService } from 'vscode-json-languageservice'; +import { + CompletionItem, + CompletionList, + CompletionParams, + Hover, + HoverParams, + ClientCapabilities as LSPClientCapabilities, +} from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { DocumentManager } from '../documents'; +import { findCurrentNode } from '../visitor'; +import { TranslationFileContributions } from './TranslationFileContributions'; + +const SectionSchemaURI = + 'https://raw.githubusercontent.com/Shopify/theme-liquid-docs/main/schemas/theme/section_schema.json'; + +const TranslationFileURI = + 'https://raw.githubusercontent.com/Shopify/theme-liquid-docs/main/schemas/theme/translations_schema.json'; + +export class JSONLanguageService { + private service: LanguageService | null = null; + + constructor( + private documentManager: DocumentManager, + private jsonValidationSet: JsonValidationSet, + ) {} + + setup(clientCapabilities: LSPClientCapabilities) { + this.service = getLanguageService({ + schemaRequestService: this.getSchemaForURI.bind(this), + contributions: [new TranslationFileContributions(this.documentManager)], + clientCapabilities, + }); + this.service.configure({ + schemas: [ + { + uri: SectionSchemaURI, + fileMatch: ['**/sections/*.liquid'], + }, + { + uri: TranslationFileURI, + fileMatch: [ + '**/locales/*.json', + '**/locales/*.default.json', + '**/locales/*.schema.json', + '**/locales/*.default.schema.json', + ], + }, + ], + }); + } + + async completions(params: CompletionParams): Promise { + if (!this.service) return null; + const documents = this.getDocuments(params); + if (!documents) return null; + const [jsonTextDocument, jsonDocument] = documents; + return this.service.doComplete(jsonTextDocument, params.position, jsonDocument); + } + + async hover(params: HoverParams): Promise { + if (!this.service) return null; + const documents = this.getDocuments(params); + if (!documents) return null; + const [jsonTextDocument, jsonDocument] = documents; + return this.service.doHover(jsonTextDocument, params.position, jsonDocument); + } + + private getDocuments( + params: HoverParams | CompletionParams, + ): [TextDocument, JSONDocument] | null { + if (!this.service) return null; + + const document = this.documentManager.get(params.textDocument.uri); + if (!document) return null; + + switch (document.type) { + case SourceCodeType.JSON: { + const jsonTextDocument = document.textDocument; + const jsonDocument = this.service.parseJSONDocument(jsonTextDocument); + return [jsonTextDocument, jsonDocument]; + } + + case SourceCodeType.LiquidHtml: { + if (document.ast instanceof Error) return null; + const textDocument = document.textDocument; + const offset = textDocument.offsetAt(params.position); + const [_, ancestors] = findCurrentNode(document.ast, offset); + const schema = ancestors.find( + (node): node is LiquidRawTag => + node.type === NodeTypes.LiquidRawTag && node.name === 'schema', + ); + if (!schema) return null; + + const schemaLineNumber = textDocument.positionAt(schema.blockStartPosition.end).line; + // Hacking away "same line numbers" here by prefixing the file with newlines + // This way params.position will be at the same line number in this fake jsonTextDocument + // Which means that the completions will be at the same line number in the Liquid document + const jsonString = + Array(schemaLineNumber).fill('\n').join('') + + schema.source.slice(schema.blockStartPosition.end, schema.blockEndPosition.start); + const jsonTextDocument = TextDocument.create( + textDocument.uri, + 'json', + textDocument.version, + jsonString, + ); + const jsonDocument = this.service.parseJSONDocument(jsonTextDocument); + return [jsonTextDocument, jsonDocument]; + } + } + } + + private async getSchemaForURI(uri: string): Promise { + switch (uri) { + case SectionSchemaURI: + return this.jsonValidationSet.sectionSchema(); + case TranslationFileURI: + return this.jsonValidationSet.translationSchema(); + default: + throw new Error(`No schema for ${uri}`); + } + } +} diff --git a/packages/theme-language-server-common/src/json/TranslationFileContributions.ts b/packages/theme-language-server-common/src/json/TranslationFileContributions.ts new file mode 100644 index 00000000..e03431e4 --- /dev/null +++ b/packages/theme-language-server-common/src/json/TranslationFileContributions.ts @@ -0,0 +1,136 @@ +import { + CompletionsCollector, + JSONPath, + JSONWorkerContribution, + MarkedString, +} from 'vscode-json-languageservice'; +import { DocumentManager } from '../documents'; +import { JSONNode, SourceCodeType } from '@shopify/theme-check-common'; + +function nodeAtLocation(ast: JSONNode, location: JSONPath): JSONNode | undefined { + return location.reduce((value: JSONNode | undefined, segment: string | number) => { + if (value && typeof value !== 'string') { + switch (value.type) { + case 'Object': { + return value.children.find((child) => child.key.value === segment)?.value; + } + case 'Array': { + if (typeof segment !== 'number') return undefined; + return value.children[segment]; + } + case 'Identifier': { + return undefined; // trying to [segment] onto a string or number + } + case 'Literal': { + return undefined; // trying to [segment] onto a string or number + } + case 'Property': { + return undefined; // this shouldn't be happening + } + } + } + }, ast); +} + +const nothing = undefined as any; + +export class TranslationFileContributions implements JSONWorkerContribution { + private filePatterns = [/^.*\/locales\/[^\/]*\.json$/]; + + constructor(private documentManager: DocumentManager) {} + + getInfoContribution(uri: string, location: JSONPath): Promise { + // TODO: This is a hack to get around the fact that the JSON language service + // actually is not typed properly and performs "if-undefined-skip" logic. + // https://github.com/microsoft/vscode-json-languageservice/pull/222 + // would fix this, but it's not merged yet. + if (!fileMatch(uri, this.filePatterns)) return nothing; + const doc = this.documentManager.get(uri); + if (!doc || location.length === 0 || doc.type !== SourceCodeType.JSON) return nothing; + const ast = doc.ast; + if (ast instanceof Error) return nothing; + const node = nodeAtLocation(ast, location); + + switch (true) { + // Because the JSON language service doesn't support composition of hover info, + // We have to hardcode the docs for the translation file schema here. + case ['zero', 'one', 'two', 'few', 'many', 'other'].includes(location.at(-1) as string): { + if (!node || node.type !== 'Literal' || typeof node.value !== 'string') { + return Promise.resolve([`Pluralized translations should have a string value`]); + } + return Promise.resolve([contextualizedLabel(uri, location.slice(0, -1), node.value)]); + } + + case location.at(-1)!.toString().endsWith('_html'): { + if (!node || node.type !== 'Literal' || typeof node.value !== 'string') { + return Promise.resolve([`Translations ending in '_html' should have a string value`]); + } + return Promise.resolve([ + contextualizedLabel(uri, location, node.value), + `The '_html' suffix prevents the HTML content from being escaped.`, + ]); + } + + default: { + if (!node || node.type !== 'Literal' || typeof node.value !== 'string') { + return Promise.resolve([`Translation group: ${location.join('.')}`]); + } + return Promise.resolve([contextualizedLabel(uri, location, node.value)]); + } + } + } + + async collectDefaultCompletions(uri: string, result: CompletionsCollector) {} + + async collectPropertyCompletions( + uri: string, + location: JSONPath, + currentWord: string, + addValue: boolean, + isLast: boolean, + result: CompletionsCollector, + ) {} + + async collectValueCompletions( + uri: string, + location: JSONPath, + propertyKey: string, + result: CompletionsCollector, + ) {} +} + +export function fileMatch(uri: string, patterns: RegExp[]): boolean { + return patterns.some((pattern) => pattern.test(uri)); +} + +export function contextualizedLabel( + uri: string, + str: (string | number)[], + value: string, +): MarkedString { + if (uri.includes('.schema')) { + return marked(`"t:${str.join('.')}"`, 'json'); + } else { + const params = extractParams(value); + return marked(`{{ '${str.join('.')}' | t${paramsString(params)} }}`, 'liquid'); + } +} + +function extractParams(value: string) { + const regex = /\{\{([^}]+?)\}\}/g; + const results = []; + let current; + while ((current = regex.exec(value)) !== null) { + results.push(current[1].trim()); + } + return results; +} + +function paramsString(params: string[]) { + if (params.length === 0) return ''; + return `: ` + params.map((param) => `${param}: ${param}`).join(', '); +} + +function marked(value: string, language = 'liquid'): { language: string; value: string } { + return { language, value }; +} diff --git a/packages/theme-language-server-common/src/server/startServer.spec.ts b/packages/theme-language-server-common/src/server/startServer.spec.ts index 3a447b87..82697fd6 100644 --- a/packages/theme-language-server-common/src/server/startServer.spec.ts +++ b/packages/theme-language-server-common/src/server/startServer.spec.ts @@ -370,8 +370,10 @@ describe('Module: server', () => { tags: async () => [], systemTranslations: async () => ({}), }, - schemaValidators: { + jsonValidationSet: { validateSectionSchema: async () => ({} as ValidateFunction), + sectionSchema: async () => '{}', + translationSchema: async () => '{}', }, } as Dependencies; } diff --git a/packages/theme-language-server-common/src/server/startServer.ts b/packages/theme-language-server-common/src/server/startServer.ts index eb459fdf..81509ebf 100644 --- a/packages/theme-language-server-common/src/server/startServer.ts +++ b/packages/theme-language-server-common/src/server/startServer.ts @@ -1,13 +1,8 @@ -import { AugmentedSchemaValidators, AugmentedThemeDocset } from '@shopify/theme-check-common'; +import { AugmentedJsonValidationSet, AugmentedThemeDocset } from '@shopify/theme-check-common'; import { - ConfigurationRequest, Connection, - DidCreateFilesNotification, - DidDeleteFilesNotification, - DidRenameFilesNotification, FileOperationRegistrationOptions, InitializeResult, - RegistrationRequest, TextDocumentSyncKind, } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; @@ -21,6 +16,7 @@ import { DocumentLinksProvider } from '../documentLinks'; import { DocumentManager } from '../documents'; import { OnTypeFormattingProvider } from '../formatting'; import { HoverProvider } from '../hover'; +import { JSONLanguageService } from '../json/JSONLanguageService'; import { GetTranslationsForURI, useBufferOrInjectedTranslations } from '../translations'; import { Dependencies } from '../types'; import { debounce } from '../utils'; @@ -51,7 +47,7 @@ export function startServer( getThemeSettingsSchemaForRootURI, loadConfig, log = defaultLogger, - schemaValidators: remoteSchemaValidators, + jsonValidationSet: remoteSchemaValidators, themeDocset: remoteThemeDocset, }: Dependencies, ) { @@ -74,7 +70,7 @@ export function startServer( // These are augmented here so that the caching is maintained over different runs. const themeDocset = new AugmentedThemeDocset(remoteThemeDocset); - const schemaValidators = new AugmentedSchemaValidators(remoteSchemaValidators); + const jsonValidationSet = new AugmentedJsonValidationSet(remoteSchemaValidators); const runChecks = debounce( makeRunChecks(documentManager, diagnosticsManager, { fileExists, @@ -84,7 +80,7 @@ export function startServer( getDefaultTranslationsFactory, loadConfig, themeDocset, - schemaValidators, + jsonValidationSet, }), 100, ); @@ -125,6 +121,7 @@ export function startServer( return getThemeSettingsSchemaForRootURI(rootUri); }; + const jsonLanguageService = new JSONLanguageService(documentManager, jsonValidationSet); const completionsProvider = new CompletionsProvider({ documentManager, themeDocset, @@ -150,6 +147,7 @@ export function startServer( connection.onInitialize((params) => { clientCapabilities.setup(params.capabilities, params.initializationOptions); + jsonLanguageService.setup(params.capabilities); configuration.setup(); const fileOperationRegistrationOptions: FileOperationRegistrationOptions = { @@ -264,11 +262,14 @@ export function startServer( }); connection.onCompletion(async (params) => { - return completionsProvider.completions(params); + return ( + (await jsonLanguageService.completions(params)) ?? + (await completionsProvider.completions(params)) + ); }); connection.onHover(async (params) => { - return hoverProvider.hover(params); + return (await jsonLanguageService.hover(params)) ?? (await hoverProvider.hover(params)); }); connection.onDocumentOnTypeFormatting(async (params) => { diff --git a/packages/theme-language-server-common/src/types.ts b/packages/theme-language-server-common/src/types.ts index 2adb9753..22acbb0d 100644 --- a/packages/theme-language-server-common/src/types.ts +++ b/packages/theme-language-server-common/src/types.ts @@ -64,7 +64,7 @@ export interface RequiredDependencies { * browser environments, json schemas must be precompiled into validators prior * to theme-check instantiation. */ - schemaValidators: NonNullable; + jsonValidationSet: NonNullable; /** * findRootURI(uri: URI) diff --git a/packages/theme-language-server-node/src/index.ts b/packages/theme-language-server-node/src/index.ts index e9242c49..9a7ac94b 100644 --- a/packages/theme-language-server-node/src/index.ts +++ b/packages/theme-language-server-node/src/index.ts @@ -30,6 +30,6 @@ export function startServer() { filesForURI, loadConfig, themeDocset: themeLiquidDocsManager, - schemaValidators: themeLiquidDocsManager, + jsonValidationSet: themeLiquidDocsManager, }); } diff --git a/yarn.lock b/yarn.lock index 882f92b9..99770896 100644 --- a/yarn.lock +++ b/yarn.lock @@ -504,16 +504,11 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0": +"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== - "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" @@ -873,12 +868,7 @@ resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.4.tgz#574bfbec51eb41ab5f444116c8555bc4347feba5" integrity sha512-ARVxjAEX5TARFRzpDRVC6cEk0hUIXCCwaMhz8y7S1/PxU6zZS1UMjyobz7q4w/D/R552r4++EhwmXK1N2rAy0A== -"@types/mime@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.2.tgz#c1ae807f13d308ee7511a5b81c74f327028e66e8" - integrity sha512-Wj+fqpTLtTbG7c0tH47dkahefpLKEbB+xAZuLq7b4/IDHPl/n6VoXcyUQ2bypFlbSwvCr0y+bD4euTTqTJsPxQ== - -"@types/mime@^1": +"@types/mime@*", "@types/mime@^1": version "1.3.3" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.3.tgz#bbe64987e0eb05de150c305005055c7ad784a9ce" integrity sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg== @@ -901,12 +891,7 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*": - version "20.4.5" - resolved "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz" - integrity sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg== - -"@types/node@16.x": +"@types/node@*", "@types/node@16.x": version "16.18.53" resolved "https://registry.npmjs.org/@types/node/-/node-16.18.53.tgz" integrity sha512-vVmHeo4tpF8zsknALU90Hh24VueYdu45ZlXzYWFbom61YR4avJqTFDC3QlWzjuTdAv6/3xHaxiO9NrtVZXrkmw== @@ -1117,6 +1102,11 @@ loupe "^2.3.7" pretty-format "^29.7.0" +"@vscode/l10n@^0.0.18": + version "0.0.18" + resolved "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz#916d3a5e960dbab47c1c56f58a7cb5087b135c95" + integrity sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ== + "@vscode/test-electron@^2.2.0": version "2.3.4" resolved "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.4.tgz" @@ -1334,7 +1324,7 @@ acorn-walk@^8.1.1, acorn-walk@^8.3.2: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== -acorn@^8.10.0, acorn@^8.11.3, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.11.3, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: version "8.11.3" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -1769,7 +1759,7 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -check-error@^1.0.2, check-error@^1.0.3: +check-error@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== @@ -2181,7 +2171,7 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" -deep-eql@^4.1.2, deep-eql@^4.1.3: +deep-eql@^4.1.3: version "4.1.3" resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz" integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== @@ -2433,21 +2423,16 @@ enquirer@^2.3.0: ansi-colors "^4.1.1" strip-ansi "^6.0.1" -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +entities@^2.0.0, entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== -entities@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" - integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== - entities@~3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" @@ -3043,7 +3028,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.0, get-func-name@^2.0.1, get-func-name@^2.0.2: +get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== @@ -3573,7 +3558,7 @@ is-ci@^3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.11.0, is-core-module@^2.13.0: +is-core-module@^2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== @@ -3855,10 +3840,10 @@ json5@^2.2.2: resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonc-parser@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" - integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== +jsonc-parser@^3.2.0, jsonc-parser@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== jsonfile@^4.0.0: version "4.0.0" @@ -4012,7 +3997,7 @@ lodash@^4.17.20, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loupe@^2.3.1, loupe@^2.3.6, loupe@^2.3.7: +loupe@^2.3.6, loupe@^2.3.7: version "2.3.7" resolved "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== @@ -4289,7 +4274,7 @@ mute-stream@~0.0.4: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nanoid@^3.3.6, nanoid@^3.3.7: +nanoid@^3.3.7: version "3.3.7" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== @@ -4848,20 +4833,13 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.0: resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -qs@6.11.0: +qs@6.11.0, qs@^6.9.1: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" -qs@^6.9.1: - version "6.11.2" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz" - integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== - dependencies: - side-channel "^1.0.4" - querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -5138,12 +5116,12 @@ safe-array-concat@^1.0.1: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5548,14 +5526,7 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: +string_decoder@^1.1.1, string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== @@ -5886,7 +5857,7 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: +type-detect@^4.0.0, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -5977,7 +5948,7 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== -ufo@^1.3.0, ufo@^1.3.2: +ufo@^1.3.2: version "1.4.0" resolved "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz#39845b31be81b4f319ab1d99fd20c56cac528d32" integrity sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ== @@ -6142,16 +6113,22 @@ vitest@^1.3.0: vite-node "1.3.0" why-is-node-running "^2.2.2" +vscode-json-languageservice@^5.3.9: + version "5.3.9" + resolved "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.3.9.tgz#512463ed580237d958df9280b43da9e3b5b621ce" + integrity sha512-0IcymTw0ZYX5Zcx+7KLLwTRvg0FzXUVnM1hrUH+sPhqEX0fHGg2h5UUOSp1f8ydGS7/xxzlFI3TR01yaHs6Y0Q== + dependencies: + "@vscode/l10n" "^0.0.18" + jsonc-parser "^3.2.1" + vscode-languageserver-textdocument "^1.0.11" + vscode-languageserver-types "^3.17.5" + vscode-uri "^3.0.8" + vscode-jsonrpc@8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz" integrity sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw== -vscode-jsonrpc@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" - integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== - vscode-languageclient@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz" @@ -6161,7 +6138,7 @@ vscode-languageclient@^8.1.0: semver "^7.3.7" vscode-languageserver-protocol "3.17.3" -vscode-languageserver-protocol@3.17.3: +vscode-languageserver-protocol@3.17.3, vscode-languageserver-protocol@^3.17.3: version "3.17.3" resolved "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz" integrity sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA== @@ -6169,25 +6146,17 @@ vscode-languageserver-protocol@3.17.3: vscode-jsonrpc "8.1.0" vscode-languageserver-types "3.17.3" -vscode-languageserver-protocol@^3.17.3: - version "3.17.5" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" - integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== - dependencies: - vscode-jsonrpc "8.2.0" - vscode-languageserver-types "3.17.5" - -vscode-languageserver-textdocument@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz" - integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q== +vscode-languageserver-textdocument@^1.0.11, vscode-languageserver-textdocument@^1.0.8: + version "1.0.11" + resolved "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" + integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== vscode-languageserver-types@3.17.3: version "3.17.3" resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz" integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA== -vscode-languageserver-types@3.17.5: +vscode-languageserver-types@^3.17.5: version "3.17.5" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== @@ -6209,10 +6178,10 @@ vscode-test@^1.3.0: rimraf "^3.0.2" unzipper "^0.10.11" -vscode-uri@^3.0.7: - version "3.0.7" - resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz" - integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== +vscode-uri@^3.0.7, vscode-uri@^3.0.8: + version "3.0.8" + resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== w3c-keyname@^2.2.4: version "2.2.8" From 976802c9535bb62a7fd3de6392809f62d6819a8c Mon Sep 17 00:00:00 2001 From: "Charles-P. Clermont" Date: Wed, 21 Feb 2024 10:16:48 -0500 Subject: [PATCH 2/2] Add type-check to CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2a81171..e56dcdf4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: env: NODE_ENV: production + - name: Type check (for tests) + run: yarn type-check + - name: Check formatting run: yarn format:check