From cbdffd4abd27089c656c632f966913394ae46698 Mon Sep 17 00:00:00 2001 From: Alok Swamy Date: Thu, 2 Jan 2025 17:09:49 -0500 Subject: [PATCH] Fetch metafield definitions on start-up using CLI --- .changeset/plenty-flowers-eat.md | 6 +++ .../src/server/startServer.ts | 28 +++++++++++++ .../theme-language-server-common/src/types.ts | 11 ++++- .../theme-language-server-node/src/index.ts | 2 + .../src/metafieldDefinitions.ts | 41 +++++++++++++++++++ 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 .changeset/plenty-flowers-eat.md create mode 100644 packages/theme-language-server-node/src/metafieldDefinitions.ts diff --git a/.changeset/plenty-flowers-eat.md b/.changeset/plenty-flowers-eat.md new file mode 100644 index 00000000..ee6d7611 --- /dev/null +++ b/.changeset/plenty-flowers-eat.md @@ -0,0 +1,6 @@ +--- +'@shopify/theme-language-server-common': minor +'@shopify/theme-language-server-node': minor +--- + +Fetch metafield definitions on start-up using CLI diff --git a/packages/theme-language-server-common/src/server/startServer.ts b/packages/theme-language-server-common/src/server/startServer.ts index 06aa0708..702e6c06 100644 --- a/packages/theme-language-server-common/src/server/startServer.ts +++ b/packages/theme-language-server-common/src/server/startServer.ts @@ -18,6 +18,7 @@ import { InitializeResult, ShowDocumentRequest, TextDocumentSyncKind, + WorkspaceFolder, } from 'vscode-languageserver'; import { ClientCapabilities } from '../ClientCapabilities'; import { CodeActionKinds, CodeActionsProvider } from '../codeActions'; @@ -79,6 +80,7 @@ export function startServer( log = defaultLogger, jsonValidationSet, themeDocset: remoteThemeDocset, + fetchMetafieldDefinitionsForURI, }: Dependencies, ) { const fs = new CachedFileSystem(injectedFs); @@ -241,6 +243,18 @@ export function startServer( connection, ); + const fetchMetafieldDefinitionsForWorkspaceFolders = async (folders: WorkspaceFolder[]) => { + if (!fetchMetafieldDefinitionsForURI) return; + + for (let folder of folders) { + const mode = await getModeForURI(folder.uri); + + if (mode === 'theme') { + fetchMetafieldDefinitionsForURI(folder.uri); + } + } + }; + connection.onInitialize((params) => { clientCapabilities.setup(params.capabilities, params.initializationOptions); jsonLanguageService.setup(params.capabilities); @@ -294,6 +308,10 @@ export function startServer( workDoneProgress: false, }, workspace: { + workspaceFolders: { + supported: true, + changeNotifications: true, + }, fileOperations: { didRename: fileOperationRegistrationOptions, didCreate: fileOperationRegistrationOptions, @@ -321,6 +339,16 @@ export function startServer( }, ], }); + + connection.workspace.getWorkspaceFolders().then(async (folders) => { + if (!folders) return; + + fetchMetafieldDefinitionsForWorkspaceFolders(folders); + }); + + connection.workspace.onDidChangeWorkspaceFolders(async (params) => { + fetchMetafieldDefinitionsForWorkspaceFolders(params.added); + }); }); connection.onDidChangeConfiguration((_params) => { diff --git a/packages/theme-language-server-common/src/types.ts b/packages/theme-language-server-common/src/types.ts index f3b6d7ca..0a19cb7f 100644 --- a/packages/theme-language-server-common/src/types.ts +++ b/packages/theme-language-server-common/src/types.ts @@ -7,7 +7,10 @@ import { URI } from 'vscode-languageserver'; import { WithOptional } from './utils'; -export type Dependencies = WithOptional; +export type Dependencies = WithOptional< + RequiredDependencies, + 'log' | 'getMetafieldDefinitions' | 'fetchMetafieldDefinitionsForURI' +>; export interface RequiredDependencies { /** @@ -86,4 +89,10 @@ export interface RequiredDependencies { * fetching the set of metafield definitions every time. */ getMetafieldDefinitions: ThemeCheckDependencies['getMetafieldDefinitions']; + + /** + * Fetch Metafield definitions using the CLI provided the URI of the project root. + * This should only be used in node environments; not on the browser. + */ + fetchMetafieldDefinitionsForURI: (uri: URI) => Promise; } diff --git a/packages/theme-language-server-node/src/index.ts b/packages/theme-language-server-node/src/index.ts index 3e34a347..c74e1c65 100644 --- a/packages/theme-language-server-node/src/index.ts +++ b/packages/theme-language-server-node/src/index.ts @@ -4,6 +4,7 @@ import { startServer as startCoreServer } from '@shopify/theme-language-server-c import { stdin, stdout } from 'node:process'; import { createConnection } from 'vscode-languageserver/node'; import { loadConfig } from './dependencies'; +import { fetchMetafieldDefinitionsForURI } from './metafieldDefinitions'; export { NodeFileSystem } from '@shopify/theme-check-node'; export * from '@shopify/theme-language-server-common'; @@ -21,5 +22,6 @@ export function startServer(connection = getConnection(), fs: AbstractFileSystem loadConfig, themeDocset: themeLiquidDocsManager, jsonValidationSet: themeLiquidDocsManager, + fetchMetafieldDefinitionsForURI, }); } diff --git a/packages/theme-language-server-node/src/metafieldDefinitions.ts b/packages/theme-language-server-node/src/metafieldDefinitions.ts new file mode 100644 index 00000000..f474ac3b --- /dev/null +++ b/packages/theme-language-server-node/src/metafieldDefinitions.ts @@ -0,0 +1,41 @@ +import * as child_process from 'child_process'; +import { promisify } from 'node:util'; + +const exec = promisify(child_process.exec); + +const isWin = process.platform === 'win32'; + +const shopifyCliPathPromise = getShopifyCliPath(); + +export async function fetchMetafieldDefinitionsForURI(uri: string) { + const path = await shopifyCliPathPromise; + + if (!path) { + return; + } + + try { + await exec(`${path} theme metafields pull`, { + cwd: new URL(uri), + timeout: 10_000, + }); + } catch (_) { + // CLI command can break because of incorrect version or not being logged in + // If this fails, the user must fetch their own metafield definitions + } +} + +// eslint-disable-next-line no-unused-vars +async function getShopifyCliPath() { + if (isWin) { + const { stdout } = await exec(`where.exe shopify`); + const executables = stdout + .replace(/\r/g, '') + .split('\n') + .filter((exe) => exe.endsWith('bat')); + return executables.length > 0 ? executables[0] : ''; + } else { + const { stdout } = await exec(`which shopify`); + return stdout.split('\n')[0].replace('\r', ''); + } +}