From e83e5af20be9f594b466da4132c85533a16e5e86 Mon Sep 17 00:00:00 2001 From: Gasser Aly Date: Thu, 5 Mar 2026 01:08:05 -0500 Subject: [PATCH 1/4] feat: add support for flow config fields in liquid lsp --- .../src/AugmentedThemeDocset.ts | 19 +++++++++++++ .../src/checks/undefined-object/index.ts | 7 ++--- .../src/types/theme-liquid-docs.ts | 3 +++ .../src/TypeSystem.ts | 27 ++++++++++--------- .../src/server/startServer.ts | 5 ++++ .../theme-language-server-common/src/types.ts | 10 +++++++ 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/packages/theme-check-common/src/AugmentedThemeDocset.ts b/packages/theme-check-common/src/AugmentedThemeDocset.ts index b168b8335..91126d401 100644 --- a/packages/theme-check-common/src/AugmentedThemeDocset.ts +++ b/packages/theme-check-common/src/AugmentedThemeDocset.ts @@ -103,6 +103,25 @@ export class AugmentedThemeDocset implements ThemeDocset { public isAugmented = true; + private objectsByPrefix = new Map(); + + setObjectsForURI(uri: string, objects: ObjectEntry[]) { + this.objectsByPrefix.set(uri, objects); + } + + getObjectsForURI(uri: string): ObjectEntry[] | undefined { + // Exact match first + const exact = this.objectsByPrefix.get(uri); + if (exact) return exact; + + // Prefix match: allows registering by step path and matching file URIs within it + for (const [prefix, objects] of this.objectsByPrefix) { + if (uri.startsWith(prefix)) return objects; + } + + return undefined; + } + filters = memo(async (): Promise => { const officialFilters = await this.themeDocset.filters(); return [ diff --git a/packages/theme-check-common/src/checks/undefined-object/index.ts b/packages/theme-check-common/src/checks/undefined-object/index.ts index 9057bbb94..8ea558974 100644 --- a/packages/theme-check-common/src/checks/undefined-object/index.ts +++ b/packages/theme-check-common/src/checks/undefined-object/index.ts @@ -146,7 +146,7 @@ export const UndefinedObject: LiquidCheckDefinition = { }, async onCodePathEnd() { - const objects = await globalObjects(themeDocset, relativePath, context.mode); + const objects = await globalObjects(themeDocset, relativePath, context.file.uri, context.mode); objects.forEach((obj) => fileScopedVariables.add(obj.name)); @@ -172,8 +172,9 @@ export const UndefinedObject: LiquidCheckDefinition = { }, }; -async function globalObjects(themeDocset: ThemeDocset, relativePath: string, mode: Mode = 'theme') { - const objects = await themeDocset.objects(); +async function globalObjects(themeDocset: ThemeDocset, relativePath: string, uri?: string, mode: Mode = 'theme') { + const perURI = uri ? themeDocset.getObjectsForURI?.(uri) : undefined; + const objects = perURI ?? (await themeDocset.objects()); const contextualObjects = getContextualObjects(relativePath, mode); const globalObjects = objects.filter(({ access, name }) => { 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 38d404518..94e03ba5c 100644 --- a/packages/theme-check-common/src/types/theme-liquid-docs.ts +++ b/packages/theme-check-common/src/types/theme-liquid-docs.ts @@ -21,6 +21,9 @@ export interface ThemeDocset { /** Returns system translations available on themes. */ systemTranslations(): Promise; + + /** Returns objects scoped to a specific URI, if available. */ + getObjectsForURI?(uri: string): ObjectEntry[] | undefined; } /** A URI that will uniquely describe the schema */ diff --git a/packages/theme-language-server-common/src/TypeSystem.ts b/packages/theme-language-server-common/src/TypeSystem.ts index 9b8859d3c..77bf22a04 100644 --- a/packages/theme-language-server-common/src/TypeSystem.ts +++ b/packages/theme-language-server-common/src/TypeSystem.ts @@ -123,7 +123,7 @@ export class TypeSystem { */ public objectMap = async (uri: string, ast: LiquidHtmlNode): Promise => { const [objectMap, themeSettingProperties, metafieldDefinitionsObjectMap] = await Promise.all([ - this._objectMap(), + this._objectMap(uri), this.themeSettingProperties(uri), this.metafieldDefinitionsObjectMap(uri), ]); @@ -261,14 +261,13 @@ export class TypeSystem { return result; } - // This is the big one we reuse (memoized) - private _objectMap = memo(async (): Promise => { - const entries = await this.objectEntries(); + private async _objectMap(uri?: string): Promise { + const entries = await this.objectEntries(uri); return entries.reduce((map, entry) => { map[entry.name] = entry; return map; }, {} as ObjectMap); - }); + } /** An indexed representation of filters.json by name */ public filtersMap = memo(async (): Promise => { @@ -283,9 +282,13 @@ export class TypeSystem { return this.themeDocset.filters(); }); - public objectEntries = memo(async () => { + public async objectEntries(uri?: string): Promise { + if (uri && this.themeDocset.getObjectsForURI) { + const perURI = this.themeDocset.getObjectsForURI(uri); + if (perURI) return perURI; + } return this.themeDocset.objects(); - }); + } private async symbolsTable(partialAst: LiquidHtmlNode, uri: string): Promise { const seedSymbolsTable = await this.seedSymbolsTable(uri); @@ -303,7 +306,7 @@ export class TypeSystem { */ private seedSymbolsTable = async (uri: string) => { const [globalVariables, contextualVariables] = await Promise.all([ - this.globalVariables(), + this.globalVariables(uri), this.contextualVariables(uri), ]); return globalVariables.concat(contextualVariables).reduce((table, objectEntry) => { @@ -317,15 +320,15 @@ export class TypeSystem { }, {} as SymbolsTable); }; - private globalVariables = memo(async () => { - const entries = await this.objectEntries(); + private globalVariables = async (uri?: string) => { + const entries = await this.objectEntries(uri); return entries.filter( (entry) => !entry.access || entry.access.global === true || entry.access.template.length > 0, ); - }); + }; private contextualVariables = async (uri: string) => { - const [entries, mode] = await Promise.all([this.objectEntries(), this.getModeForURI(uri)]); + const [entries, mode] = await Promise.all([this.objectEntries(uri), this.getModeForURI(uri)]); const contextualEntries = getContextualEntries(uri, mode); return entries.filter((entry) => contextualEntries.includes(entry.name)); }; diff --git a/packages/theme-language-server-common/src/server/startServer.ts b/packages/theme-language-server-common/src/server/startServer.ts index a13d4164b..891c6565f 100644 --- a/packages/theme-language-server-common/src/server/startServer.ts +++ b/packages/theme-language-server-common/src/server/startServer.ts @@ -47,6 +47,7 @@ import { RenameHandler } from '../renamed/RenameHandler'; import { GetTranslationsForURI } from '../translations'; import { Dependencies, + SetObjectsNotification, ThemeGraphDeadCodeRequest, ThemeGraphDependenciesRequest, ThemeGraphReferenceRequest, @@ -762,5 +763,9 @@ export function startServer( return deadFiles; }); + connection.onNotification(SetObjectsNotification.type, ({ uri, objects }) => { + themeDocset.setObjectsForURI(uri, objects); + }); + connection.listen(); } diff --git a/packages/theme-language-server-common/src/types.ts b/packages/theme-language-server-common/src/types.ts index 57acb1420..b986525c4 100644 --- a/packages/theme-language-server-common/src/types.ts +++ b/packages/theme-language-server-common/src/types.ts @@ -2,6 +2,7 @@ import { AbstractFileSystem, Config, Dependencies as ThemeCheckDependencies, + ObjectEntry, } from '@shopify/theme-check-common'; import { URI } from 'vscode-languageserver'; import * as rpc from 'vscode-jsonrpc'; @@ -148,6 +149,15 @@ export namespace ThemeGraphDidUpdateNotification { } } +export namespace SetObjectsNotification { + export const method = 'shopify/setObjects'; + export interface Params { + uri: string; + objects: ObjectEntry[]; + } + export const type = new rpc.NotificationType(method); +} + export type AugmentedLocationWithExistence = { uri: string; range: undefined; From 8cbc887e979b881a0499a30b296e51ee30467001 Mon Sep 17 00:00:00 2001 From: Gasser Aly Date: Mon, 9 Mar 2026 16:36:27 -0400 Subject: [PATCH 2/4] v3.23.2-flow.0 --- packages/theme-check-common/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/theme-check-common/package.json b/packages/theme-check-common/package.json index eaa58726f..4ff125d48 100644 --- a/packages/theme-check-common/package.json +++ b/packages/theme-check-common/package.json @@ -1,6 +1,6 @@ { "name": "@shopify/theme-check-common", - "version": "3.26.1", + "version": "3.26.1-flow.0", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", From f6b455591964d748b1d69c1476706bf2ab351313 Mon Sep 17 00:00:00 2001 From: Gasser Aly Date: Wed, 11 Mar 2026 02:29:49 -0400 Subject: [PATCH 3/4] feat: skip array/string core properties for custom object URIs --- packages/theme-language-server-common/src/TypeSystem.ts | 4 ++++ .../providers/ObjectAttributeCompletionProvider.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/packages/theme-language-server-common/src/TypeSystem.ts b/packages/theme-language-server-common/src/TypeSystem.ts index 77bf22a04..8431ce317 100644 --- a/packages/theme-language-server-common/src/TypeSystem.ts +++ b/packages/theme-language-server-common/src/TypeSystem.ts @@ -282,6 +282,10 @@ export class TypeSystem { return this.themeDocset.filters(); }); + public hasObjectsForURI(uri: string): boolean { + return !!this.themeDocset.getObjectsForURI?.(uri); + } + public async objectEntries(uri?: string): Promise { if (uri && this.themeDocset.getObjectsForURI) { const perURI = this.themeDocset.getObjectsForURI(uri); diff --git a/packages/theme-language-server-common/src/completions/providers/ObjectAttributeCompletionProvider.ts b/packages/theme-language-server-common/src/completions/providers/ObjectAttributeCompletionProvider.ts index 3f8c90cd3..fec08da43 100644 --- a/packages/theme-language-server-common/src/completions/providers/ObjectAttributeCompletionProvider.ts +++ b/packages/theme-language-server-common/src/completions/providers/ObjectAttributeCompletionProvider.ts @@ -45,12 +45,15 @@ export class ObjectAttributeCompletionProvider implements Provider { partialAst, params.textDocument.uri, ); + const hasCustomObjects = this.typeSystem.hasObjectsForURI(params.textDocument.uri); if (isArrayType(parentType)) { + if (hasCustomObjects) return []; return completionItems( ArrayCoreProperties.map((name) => ({ name })), partial, ); } else if (parentType === 'string') { + if (hasCustomObjects) return []; return completionItems( StringCoreProperties.map((name) => ({ name })), partial, From 624b3207898310191237672ea33def87d29a899b Mon Sep 17 00:00:00 2001 From: Gasser Aly Date: Wed, 17 Jun 2026 15:33:36 -0400 Subject: [PATCH 4/4] feat: gate URI-scoped object memo bypass on shopify.supportsSetObjects init flag Co-Authored-By: Claude Sonnet 4.6 --- .../src/ClientCapabilities.ts | 4 ++++ .../src/TypeSystem.ts | 24 +++++++++++++++++-- .../src/completions/CompletionsProvider.ts | 3 +++ .../src/hover/HoverProvider.ts | 2 ++ .../src/server/startServer.ts | 2 ++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/theme-language-server-common/src/ClientCapabilities.ts b/packages/theme-language-server-common/src/ClientCapabilities.ts index 8a45be78b..ec519c630 100644 --- a/packages/theme-language-server-common/src/ClientCapabilities.ts +++ b/packages/theme-language-server-common/src/ClientCapabilities.ts @@ -43,6 +43,10 @@ export class ClientCapabilities { return !!this.capabilities?.window?.workDoneProgress; } + get supportsSetObjects(): boolean { + return this.initializationOption('shopify.supportsSetObjects', false); + } + initializationOption(key: string, defaultValue: T): T { // { 'themeCheck.checkOnSave': true } const direct = this.initializationOptions?.[key]; diff --git a/packages/theme-language-server-common/src/TypeSystem.ts b/packages/theme-language-server-common/src/TypeSystem.ts index 8431ce317..2425b90bb 100644 --- a/packages/theme-language-server-common/src/TypeSystem.ts +++ b/packages/theme-language-server-common/src/TypeSystem.ts @@ -48,6 +48,7 @@ export class TypeSystem { private readonly getThemeSettingsSchemaForURI: GetThemeSettingsSchemaForURI, private readonly getMetafieldDefinitions: (rootUri: string) => Promise, private readonly getModeForURI: GetModeForURI = async () => 'theme', + private readonly isUriScopedMode: () => boolean = () => false, ) {} async inferType( @@ -262,6 +263,7 @@ export class TypeSystem { } private async _objectMap(uri?: string): Promise { + if (!this.isUriScopedMode()) return this._memoObjectMap(); const entries = await this.objectEntries(uri); return entries.reduce((map, entry) => { map[entry.name] = entry; @@ -269,6 +271,16 @@ export class TypeSystem { }, {} as ObjectMap); } + private _memoObjectEntries = memo(() => this.themeDocset.objects()); + + private _memoObjectMap = memo(async (): Promise => { + const entries = await this._memoObjectEntries(); + return entries.reduce((map, entry) => { + map[entry.name] = entry; + return map; + }, {} as ObjectMap); + }); + /** An indexed representation of filters.json by name */ public filtersMap = memo(async (): Promise => { const entries = await this.filterEntries(); @@ -287,11 +299,11 @@ export class TypeSystem { } public async objectEntries(uri?: string): Promise { - if (uri && this.themeDocset.getObjectsForURI) { + if (this.isUriScopedMode() && uri && this.themeDocset.getObjectsForURI) { const perURI = this.themeDocset.getObjectsForURI(uri); if (perURI) return perURI; } - return this.themeDocset.objects(); + return this._memoObjectEntries(); } private async symbolsTable(partialAst: LiquidHtmlNode, uri: string): Promise { @@ -324,7 +336,15 @@ export class TypeSystem { }, {} as SymbolsTable); }; + private _memoGlobalVariables = memo(async () => { + const entries = await this._memoObjectEntries(); + return entries.filter( + (entry) => !entry.access || entry.access.global === true || entry.access.template.length > 0, + ); + }); + private globalVariables = async (uri?: string) => { + if (!this.isUriScopedMode()) return this._memoGlobalVariables(); const entries = await this.objectEntries(uri); return entries.filter( (entry) => !entry.access || entry.access.global === true || entry.access.template.length > 0, diff --git a/packages/theme-language-server-common/src/completions/CompletionsProvider.ts b/packages/theme-language-server-common/src/completions/CompletionsProvider.ts index 64704916d..eff2013e1 100644 --- a/packages/theme-language-server-common/src/completions/CompletionsProvider.ts +++ b/packages/theme-language-server-common/src/completions/CompletionsProvider.ts @@ -42,6 +42,7 @@ export interface CompletionProviderDependencies { getDocDefinitionForURI?: GetDocDefinitionForURI; getThemeBlockNames?: (rootUri: string, includePrivate: boolean) => Promise; getModeForURI?: (uri: string) => Promise; + isUriScopedMode?: () => boolean; log?: (message: string) => void; } @@ -61,6 +62,7 @@ export class CompletionsProvider { getDocDefinitionForURI = async (uri, _relativePath) => ({ uri }), getThemeBlockNames = async (_rootUri: string, _includePrivate: boolean) => [], getModeForURI, + isUriScopedMode = () => false, log = () => {}, }: CompletionProviderDependencies) { this.documentManager = documentManager; @@ -71,6 +73,7 @@ export class CompletionsProvider { getThemeSettingsSchemaForURI, getMetafieldDefinitions, getModeForURI, + isUriScopedMode, ); this.providers = [ diff --git a/packages/theme-language-server-common/src/hover/HoverProvider.ts b/packages/theme-language-server-common/src/hover/HoverProvider.ts index 22210611c..edb17db7c 100644 --- a/packages/theme-language-server-common/src/hover/HoverProvider.ts +++ b/packages/theme-language-server-common/src/hover/HoverProvider.ts @@ -39,12 +39,14 @@ export class HoverProvider { readonly getSettingsSchemaForURI: GetThemeSettingsSchemaForURI = async () => [], readonly getDocDefinitionForURI: GetDocDefinitionForURI = async () => undefined, readonly getModeForURI: (uri: string) => Promise = async () => 'theme', + readonly isUriScopedMode: () => boolean = () => false, ) { const typeSystem = new TypeSystem( themeDocset, getSettingsSchemaForURI, getMetafieldDefinitions, getModeForURI, + isUriScopedMode, ); this.providers = [ new ContentForArgumentHoverProvider(getDocDefinitionForURI), diff --git a/packages/theme-language-server-common/src/server/startServer.ts b/packages/theme-language-server-common/src/server/startServer.ts index 891c6565f..3c13a6e8f 100644 --- a/packages/theme-language-server-common/src/server/startServer.ts +++ b/packages/theme-language-server-common/src/server/startServer.ts @@ -342,6 +342,7 @@ export function startServer( getMetafieldDefinitions, getDocDefinitionForURI, getModeForURI, + isUriScopedMode: () => clientCapabilities.supportsSetObjects, }); const hoverProvider = new HoverProvider( documentManager, @@ -351,6 +352,7 @@ export function startServer( getThemeSettingsSchemaForURI, getDocDefinitionForURI, getModeForURI, + () => clientCapabilities.supportsSetObjects, ); const executeCommandProvider = new ExecuteCommandProvider(