Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@shopify/theme-language-server-common

Package Overview
Dependencies
Maintainers
24
Versions
49
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@shopify/theme-language-server-common - npm Package Compare versions

Comparing version 1.10.0 to 1.11.0

38

CHANGELOG.md
# @shopify/theme-language-server-common
## 1.11.0
### Minor Changes
- 03b41e1: Breaking: `jsonValidationSet`'s schemas public API change
Now takes a function of the following signature:
```ts
interface JsonValidationSet = {
schemas: (context: 'theme' | 'app') => Promise<SchemaDefinition[]>
}
```
Reason being we want to support `fileMatch` overloading of `blocks/*.liquid` files and we needed a way to identify which context you're in.
Unfortunately, the JSON schema for `blocks/*.liquid` files in theme app extensions isn't the same one we have in themes. There doesn't seem to be a way to unify them either.
- 03b41e1: Add support for the schemas manifest on Shopify/theme-liquid-docs
Shopify/theme-liquid-docs now supports composable JSON schemas (with relative paths). To solve the `blocks/*.liquid` file match JSON schema overload depending on the context (`app` or `theme`), we defined two manifests that describe the schemas required by your solution and define the fileMatch rules:
- [manifest_theme.json](https://github.com/Shopify/theme-liquid-docs/blob/main/schemas/manifest_theme.json)
- [manifest_theme_app_extension.json](https://github.com/Shopify/theme-liquid-docs/blob/main/schemas/manifest_theme.json)
`@shopify/theme-check-docs-updater` now reads those manifests and downloads the tree of dependency that they require. We will no longer need to make new theme-tools releases whenever we add new schemas. We'll be able to dev them and their file associations directly from Shopify/theme-liquid-docs and have downstream consumers updated automatically (the same way docs are automatically updated).
- 03b41e1: Breaking: the `Config` object requires a `context` property.
This is mostly for internal use, but documented here anyway.
### Patch Changes
- Updated dependencies [03b41e1]
- Updated dependencies [03b41e1]
- Updated dependencies [03b41e1]
- @shopify/theme-check-common@2.5.0
## 1.10.0

@@ -4,0 +42,0 @@

9

dist/json/JSONLanguageService.d.ts

@@ -1,2 +0,2 @@

import { JsonValidationSet } from '@shopify/theme-check-common';
import { JsonValidationSet, Mode } from '@shopify/theme-check-common';
import { CompletionItem, CompletionList, CompletionParams, Hover, HoverParams, ClientCapabilities as LSPClientCapabilities } from 'vscode-languageserver';

@@ -9,6 +9,7 @@ import { DocumentManager } from '../documents';

private getDefaultSchemaTranslations;
private service;
private getModeForURI;
private services;
private schemas;
constructor(documentManager: DocumentManager, jsonValidationSet: JsonValidationSet, getDefaultSchemaTranslations: GetTranslationsForURI);
setup(clientCapabilities: LSPClientCapabilities): void;
constructor(documentManager: DocumentManager, jsonValidationSet: JsonValidationSet, getDefaultSchemaTranslations: GetTranslationsForURI, getModeForURI: (uri: string) => Promise<Mode>);
setup(clientCapabilities: LSPClientCapabilities): Promise<void>;
completions(params: CompletionParams): Promise<null | CompletionList | CompletionItem[]>;

@@ -15,0 +16,0 @@ hover(params: HoverParams): Promise<Hover | null>;

@@ -12,46 +12,72 @@ "use strict";

class JSONLanguageService {
constructor(documentManager, jsonValidationSet, getDefaultSchemaTranslations) {
constructor(documentManager, jsonValidationSet, getDefaultSchemaTranslations, getModeForURI) {
this.documentManager = documentManager;
this.jsonValidationSet = jsonValidationSet;
this.getDefaultSchemaTranslations = getDefaultSchemaTranslations;
this.service = null;
this.schemas = (0, theme_check_common_1.indexBy)((x) => x.uri, this.jsonValidationSet.schemas);
this.getModeForURI = getModeForURI;
this.services = Object.fromEntries(theme_check_common_1.Modes.map((mode) => [mode, null]));
this.schemas = {};
}
setup(clientCapabilities) {
this.service = (0, vscode_json_languageservice_1.getLanguageService)({
schemaRequestService: this.getSchemaForURI.bind(this),
contributions: [
new TranslationFileContributions_1.TranslationFileContributions(this.documentManager),
new SchemaTranslationContributions_1.SchemaTranslationContributions(this.documentManager, this.getDefaultSchemaTranslations),
],
clientCapabilities,
});
this.service.configure({
schemas: this.jsonValidationSet.schemas.map((schemaDefinition) => ({
uri: schemaDefinition.uri,
fileMatch: schemaDefinition.fileMatch,
})),
});
async setup(clientCapabilities) {
await Promise.all(theme_check_common_1.Modes.map(async (mode) => {
const schemas = await this.jsonValidationSet.schemas(mode);
for (const schema of schemas) {
this.schemas[schema.uri] = schema;
}
if (!schemas.length)
return;
const service = (0, vscode_json_languageservice_1.getLanguageService)({
clientCapabilities,
// Map URIs to schemas without making network requests. Removes the
// network dependency.
schemaRequestService: this.getSchemaForURI.bind(this),
// This is how we make sure that our "$ref": "./inputSettings.json" in
// our JSON schemas resolve correctly.
workspaceContext: {
resolveRelativePath: (relativePath, resource) => {
const url = new URL(relativePath, resource);
return url.toString();
},
},
// Custom non-JSON schema completion & hover contributions
contributions: [
new TranslationFileContributions_1.TranslationFileContributions(this.documentManager),
new SchemaTranslationContributions_1.SchemaTranslationContributions(this.documentManager, this.getDefaultSchemaTranslations),
],
});
service.configure({
// This is what we use to map file names to JSON schemas. Without
// this, we'd need folks to use the `$schema` field in their JSON
// blobs. That ain't fun nor is going to happen.
schemas: schemas.map((schemaDefinition) => ({
uri: schemaDefinition.uri,
fileMatch: schemaDefinition.fileMatch,
})),
});
this.services[mode] = service;
}));
}
async completions(params) {
if (!this.service)
const mode = await this.getModeForURI(params.textDocument.uri);
const service = this.services[mode];
if (!service)
return null;
const documents = this.getDocuments(params);
const documents = this.getDocuments(params, service);
if (!documents)
return null;
const [jsonTextDocument, jsonDocument] = documents;
return this.service.doComplete(jsonTextDocument, params.position, jsonDocument);
return service.doComplete(jsonTextDocument, params.position, jsonDocument);
}
async hover(params) {
if (!this.service)
const mode = await this.getModeForURI(params.textDocument.uri);
const service = this.services[mode];
if (!service)
return null;
const documents = this.getDocuments(params);
const documents = this.getDocuments(params, service);
if (!documents)
return null;
const [jsonTextDocument, jsonDocument] = documents;
return this.service.doHover(jsonTextDocument, params.position, jsonDocument);
return service.doHover(jsonTextDocument, params.position, jsonDocument);
}
getDocuments(params) {
if (!this.service)
return null;
getDocuments(params, service) {
const document = this.documentManager.get(params.textDocument.uri);

@@ -63,3 +89,3 @@ if (!document)

const jsonTextDocument = document.textDocument;
const jsonDocument = this.service.parseJSONDocument(jsonTextDocument);
const jsonDocument = service.parseJSONDocument(jsonTextDocument);
return [jsonTextDocument, jsonDocument];

@@ -83,3 +109,3 @@ }

const jsonTextDocument = vscode_languageserver_textdocument_1.TextDocument.create(textDocument.uri, 'json', textDocument.version, jsonString);
const jsonDocument = this.service.parseJSONDocument(jsonTextDocument);
const jsonDocument = service.parseJSONDocument(jsonTextDocument);
return [jsonTextDocument, jsonDocument];

@@ -91,6 +117,6 @@ }

var _a;
const promise = (_a = this.schemas[uri]) === null || _a === void 0 ? void 0 : _a.schema;
if (!promise)
const schema = (_a = this.schemas[uri]) === null || _a === void 0 ? void 0 : _a.schema;
if (!schema)
return `Could not get schema for '${uri}'`;
return promise;
return schema;
}

@@ -97,0 +123,0 @@ }

@@ -91,3 +91,8 @@ "use strict";

};
const jsonLanguageService = new JSONLanguageService_1.JSONLanguageService(documentManager, jsonValidationSet, getSchemaTranslationsForURI);
const getModeForURI = async (uri) => {
const rootUri = await findConfigurationRootURI(uri);
const config = await loadConfig(rootUri);
return config.context;
};
const jsonLanguageService = new JSONLanguageService_1.JSONLanguageService(documentManager, jsonValidationSet, getSchemaTranslationsForURI, getModeForURI);
const completionsProvider = new completions_1.CompletionsProvider({

@@ -94,0 +99,0 @@ documentManager,

{
"name": "@shopify/theme-language-server-common",
"version": "1.10.0",
"version": "1.11.0",
"main": "dist/index.js",

@@ -27,7 +27,7 @@ "types": "dist/index.d.ts",

"test": "vitest",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit -p src/tsconfig.json"
},
"dependencies": {
"@shopify/liquid-html-parser": "^2.0.3",
"@shopify/theme-check-common": "2.4.0",
"@shopify/theme-check-common": "2.5.0",
"@vscode/web-custom-data": "^0.4.6",

@@ -34,0 +34,0 @@ "vscode-json-languageservice": "^5.3.10",

@@ -62,2 +62,3 @@ import { vi, expect, describe, it, beforeEach } from 'vitest';

loadConfig: async () => ({
context: 'theme',
settings: {},

@@ -74,3 +75,3 @@ checks: [LiquidFilter],

jsonValidationSet: {
schemas: [],
schemas: async () => [],
},

@@ -220,2 +221,3 @@ });

loadConfig: async () => ({
context: 'theme',
settings: {},

@@ -232,3 +234,3 @@ checks: matchingTranslation,

jsonValidationSet: {
schemas: [],
schemas: async () => [],
},

@@ -235,0 +237,0 @@ });

@@ -1,2 +0,2 @@

import { Translations } from '@shopify/theme-check-common';
import { Mode, Translations } from '@shopify/theme-check-common';
import { assert, beforeEach, describe, expect, it } from 'vitest';

@@ -20,5 +20,28 @@ import { CompletionParams, HoverParams } from 'vscode-languageserver';

},
disabled_on: {
type: 'string',
},
},
});
const simplifiedTaeBlockSchema = 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',
},
javascript: {
type: 'string',
description: 'Haha this one is different',
},
},
});
const simplifiedTranslationSchema = JSON.stringify({

@@ -70,6 +93,6 @@ $schema: 'https://json-schema.org/draft/2020-12/schema',

{
schemas: [
schemas: async (mode: Mode) => [
{
uri: 'https://shopify.dev/section-schema.json',
schema: Promise.resolve(simplifiedSectionSchema),
schema: simplifiedSectionSchema,
fileMatch: ['**/sections/*.liquid'],

@@ -79,8 +102,17 @@ },

uri: 'https://shopify.dev/translation-schema.json',
schema: Promise.resolve(simplifiedTranslationSchema),
schema: simplifiedTranslationSchema,
fileMatch: ['**/locales/*.json'],
},
{
uri:
mode === 'theme'
? 'https://shopify.dev/block-schema.json'
: 'https://shopify.dev/block-tae-schema.json',
schema: mode === 'theme' ? simplifiedSectionSchema : simplifiedTaeBlockSchema,
fileMatch: ['**/blocks/*.liquid'],
},
],
},
() => Promise.resolve(schemaTranslations),
(uri: string) => Promise.resolve(uri.includes('tae') ? 'app' : 'theme'),
);

@@ -124,3 +156,3 @@

);
expect(completions.items).to.have.lengthOf(1);
expect(completions.items).to.have.lengthOf(2);
expect(completions.items[0].label).to.equal('class');

@@ -162,2 +194,35 @@ expect(completions.items[0].documentation).to.equal('Additional CSS class for the section');

it('should be possible to offer theme app extension completions in app contexts, and theme completions in theme contexts for blocks', async () => {
const testContexts: [Mode, string, string[]][] = [
['theme', 'theme/blocks/block.liquid', ['class', 'disabled_on']],
['app', 'tae/blocks/block.liquid', ['class', 'javascript']],
];
for (const [mode, path, expectedRecommendations] of testContexts) {
const params = getParams(
documentManager,
path,
`
<div>hello world</div>
{% 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(expectedRecommendations.length);
expect(completions.items).toEqual(
expectedRecommendations.map((recommendation) =>
expect.objectContaining({ label: recommendation }),
),
);
}
});
it('should complete t:translations', async () => {

@@ -164,0 +229,0 @@ schemaTranslations = {

import { LiquidRawTag, NodeTypes } from '@shopify/liquid-html-parser';
import {
JsonValidationSet,
Mode,
Modes,
SchemaDefinition,

@@ -25,3 +27,10 @@ SourceCodeType,

export class JSONLanguageService {
private service: LanguageService | null = null;
// We index by Mode here because I don't want to reconfigure the service depending on the URI.
// This is because you may be in a "app" context in one folder, and in a "theme" context in another one.
// Because theme app extensions and themes do not share a common JSON schema for blocks/*.liquid files,
// we need to do this switch on mode here to figure out which language service we will use to power
// completions/hover. The mode comes from the theme check config.
private services: Record<Mode, LanguageService | null>;
// One record for all modes since collisions on URIs should point to the same schema
private schemas: Record<string, SchemaDefinition>;

@@ -33,37 +42,77 @@

private getDefaultSchemaTranslations: GetTranslationsForURI,
private getModeForURI: (uri: string) => Promise<Mode>,
) {
this.schemas = indexBy((x) => x.uri, this.jsonValidationSet.schemas);
this.services = Object.fromEntries(Modes.map((mode) => [mode, null])) as typeof this.services;
this.schemas = {};
}
setup(clientCapabilities: LSPClientCapabilities) {
this.service = getLanguageService({
schemaRequestService: this.getSchemaForURI.bind(this),
contributions: [
new TranslationFileContributions(this.documentManager),
new SchemaTranslationContributions(this.documentManager, this.getDefaultSchemaTranslations),
],
clientCapabilities,
});
this.service.configure({
schemas: this.jsonValidationSet.schemas.map((schemaDefinition) => ({
uri: schemaDefinition.uri,
fileMatch: schemaDefinition.fileMatch,
})),
});
async setup(clientCapabilities: LSPClientCapabilities) {
await Promise.all(
Modes.map(async (mode) => {
const schemas = await this.jsonValidationSet.schemas(mode);
for (const schema of schemas) {
this.schemas[schema.uri] = schema;
}
if (!schemas.length) return;
const service = getLanguageService({
clientCapabilities,
// Map URIs to schemas without making network requests. Removes the
// network dependency.
schemaRequestService: this.getSchemaForURI.bind(this),
// This is how we make sure that our "$ref": "./inputSettings.json" in
// our JSON schemas resolve correctly.
workspaceContext: {
resolveRelativePath: (relativePath, resource) => {
const url = new URL(relativePath, resource);
return url.toString();
},
},
// Custom non-JSON schema completion & hover contributions
contributions: [
new TranslationFileContributions(this.documentManager),
new SchemaTranslationContributions(
this.documentManager,
this.getDefaultSchemaTranslations,
),
],
});
service.configure({
// This is what we use to map file names to JSON schemas. Without
// this, we'd need folks to use the `$schema` field in their JSON
// blobs. That ain't fun nor is going to happen.
schemas: schemas.map((schemaDefinition) => ({
uri: schemaDefinition.uri,
fileMatch: schemaDefinition.fileMatch,
})),
});
this.services[mode] = service;
}),
);
}
async completions(params: CompletionParams): Promise<null | CompletionList | CompletionItem[]> {
if (!this.service) return null;
const documents = this.getDocuments(params);
const mode = await this.getModeForURI(params.textDocument.uri);
const service = this.services[mode];
if (!service) return null;
const documents = this.getDocuments(params, service);
if (!documents) return null;
const [jsonTextDocument, jsonDocument] = documents;
return this.service.doComplete(jsonTextDocument, params.position, jsonDocument);
return service.doComplete(jsonTextDocument, params.position, jsonDocument);
}
async hover(params: HoverParams): Promise<Hover | null> {
if (!this.service) return null;
const documents = this.getDocuments(params);
const mode = await this.getModeForURI(params.textDocument.uri);
const service = this.services[mode];
if (!service) return null;
const documents = this.getDocuments(params, service);
if (!documents) return null;
const [jsonTextDocument, jsonDocument] = documents;
return this.service.doHover(jsonTextDocument, params.position, jsonDocument);
return service.doHover(jsonTextDocument, params.position, jsonDocument);
}

@@ -73,5 +122,4 @@

params: HoverParams | CompletionParams,
service: LanguageService,
): [TextDocument, JSONDocument] | null {
if (!this.service) return null;
const document = this.documentManager.get(params.textDocument.uri);

@@ -83,3 +131,3 @@ if (!document) return null;

const jsonTextDocument = document.textDocument;
const jsonDocument = this.service.parseJSONDocument(jsonTextDocument);
const jsonDocument = service.parseJSONDocument(jsonTextDocument);
return [jsonTextDocument, jsonDocument];

@@ -112,3 +160,3 @@ }

);
const jsonDocument = this.service.parseJSONDocument(jsonTextDocument);
const jsonDocument = service.parseJSONDocument(jsonTextDocument);
return [jsonTextDocument, jsonDocument];

@@ -120,6 +168,6 @@ }

private async getSchemaForURI(uri: string): Promise<string> {
const promise = this.schemas[uri]?.schema;
if (!promise) return `Could not get schema for '${uri}'`;
return promise;
const schema = this.schemas[uri]?.schema;
if (!schema) return `Could not get schema for '${uri}'`;
return schema;
}
}

@@ -364,2 +364,3 @@ import { vi, expect, describe, it, beforeEach, afterEach, assert } from 'vitest';

loadConfig: async () => ({
context: 'theme',
settings: {},

@@ -377,3 +378,3 @@ checks: MissingTemplate,

jsonValidationSet: {
schemas: [],
schemas: async () => [],
},

@@ -380,0 +381,0 @@ } as Dependencies;

@@ -135,2 +135,8 @@ import { AugmentedThemeDocset } from '@shopify/theme-check-common';

const getModeForURI = async (uri: string) => {
const rootUri = await findConfigurationRootURI(uri);
const config = await loadConfig(rootUri);
return config.context;
};
const jsonLanguageService = new JSONLanguageService(

@@ -140,2 +146,3 @@ documentManager,

getSchemaTranslationsForURI,
getModeForURI,
);

@@ -142,0 +149,0 @@ const completionsProvider = new CompletionsProvider({

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

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc