From 9a2bc0cbfc39efb14f0ce8bf58d7144245e24101 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 3 Mar 2026 13:29:51 +0100 Subject: [PATCH] Cache files while scanning imports on app dev --- .../extensions/extension-instance.test.ts | 28 +++++++++++++ .../models/extensions/extension-instance.ts | 13 ++++++- .../cli-kit/src/private/node/constants.ts | 1 + .../src/public/node/import-extractor.test.ts | 39 ++++++++++++++++++- .../src/public/node/import-extractor.ts | 28 +++++++++++-- 5 files changed, 103 insertions(+), 6 deletions(-) diff --git a/packages/app/src/cli/models/extensions/extension-instance.test.ts b/packages/app/src/cli/models/extensions/extension-instance.test.ts index 5a808958df9..72886e65431 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.test.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.test.ts @@ -694,3 +694,31 @@ describe('rescanImports', async () => { }) }) }) + +describe('SHOPIFY_CLI_DISABLE_IMPORT_SCANNING', () => { + test('skips import scanning when env var is set', async () => { + await inTemporaryDirectory(async (tmpDir) => { + const extensionInstance = await testUIExtension({ + directory: tmpDir, + entrySourceFilePath: joinPath(tmpDir, 'src', 'index.ts'), + }) + + const srcDir = joinPath(tmpDir, 'src') + await mkdir(srcDir) + await writeFile(joinPath(srcDir, 'index.ts'), 'import "../shared"') + + vi.mocked(extractImportPathsRecursively).mockReset() + vi.mocked(extractImportPathsRecursively).mockReturnValue(['/some/external/file.ts']) + + process.env.SHOPIFY_CLI_DISABLE_IMPORT_SCANNING = '1' + try { + const watched = extensionInstance.watchedFiles() + expect(extractImportPathsRecursively).not.toHaveBeenCalled() + expect(watched.some((file) => file.includes('external'))).toBe(false) + } finally { + delete process.env.SHOPIFY_CLI_DISABLE_IMPORT_SCANNING + vi.mocked(extractImportPathsRecursively).mockReset() + } + }) + }) +}) diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index 1732badeae3..6a70a548328 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -31,7 +31,12 @@ import {joinPath, basename, normalizePath, resolvePath} from '@shopify/cli-kit/n import {fileExists, touchFile, moveFile, writeFile, glob, copyFile, globSync} from '@shopify/cli-kit/node/fs' import {getPathValue} from '@shopify/cli-kit/common/object' import {outputDebug} from '@shopify/cli-kit/node/output' -import {extractJSImports, extractImportPathsRecursively} from '@shopify/cli-kit/node/import-extractor' +import { + extractJSImports, + extractImportPathsRecursively, + clearImportPathsCache, +} from '@shopify/cli-kit/node/import-extractor' +import {isTruthy} from '@shopify/cli-kit/node/context/utilities' import {uniq} from '@shopify/cli-kit/common/array' export const CONFIG_EXTENSION_IDS: string[] = [ @@ -516,6 +521,7 @@ export class ExtensionInstance { const oldImportPaths = this.cachedImportPaths this.cachedImportPaths = undefined + clearImportPathsCache() this.scanImports() return oldImportPaths !== this.cachedImportPaths } @@ -530,6 +536,11 @@ export class ExtensionInstance { return extractImportPathsRecursively(entryFile).map((importPath) => normalizePath(resolvePath(importPath))) diff --git a/packages/cli-kit/src/private/node/constants.ts b/packages/cli-kit/src/private/node/constants.ts index 3672b8b4c29..652d60cd962 100644 --- a/packages/cli-kit/src/private/node/constants.ts +++ b/packages/cli-kit/src/private/node/constants.ts @@ -46,6 +46,7 @@ export const environmentVariables = { neverUsePartnersApi: 'SHOPIFY_CLI_NEVER_USE_PARTNERS_API', skipNetworkLevelRetry: 'SHOPIFY_CLI_SKIP_NETWORK_LEVEL_RETRY', maxRequestTimeForNetworkCalls: 'SHOPIFY_CLI_MAX_REQUEST_TIME_FOR_NETWORK_CALLS', + disableImportScanning: 'SHOPIFY_CLI_DISABLE_IMPORT_SCANNING', } export const defaultThemeKitAccessDomain = 'theme-kit-access.shopifyapps.com' diff --git a/packages/cli-kit/src/public/node/import-extractor.test.ts b/packages/cli-kit/src/public/node/import-extractor.test.ts index e707ad56936..07a24810c89 100644 --- a/packages/cli-kit/src/public/node/import-extractor.test.ts +++ b/packages/cli-kit/src/public/node/import-extractor.test.ts @@ -1,7 +1,11 @@ import {inTemporaryDirectory, mkdir, writeFile} from './fs.js' import {joinPath} from './path.js' -import {extractImportPaths, extractImportPathsRecursively} from './import-extractor.js' -import {describe, test, expect} from 'vitest' +import {extractImportPaths, extractImportPathsRecursively, clearImportPathsCache} from './import-extractor.js' +import {describe, test, expect, beforeEach} from 'vitest' + +beforeEach(() => { + clearImportPathsCache() +}) describe('extractImportPaths', () => { describe('JavaScript imports', () => { @@ -610,3 +614,34 @@ describe('extractImportPathsRecursively', () => { }) }) }) + +describe('clearImportPathsCache', () => { + test('picks up file changes after cache is cleared', async () => { + await inTemporaryDirectory(async (tmpDir) => { + const mainFile = joinPath(tmpDir, 'main.js') + const utilsFile = joinPath(tmpDir, 'utils.js') + + await writeFile(utilsFile, 'export const foo = "bar"') + await writeFile(mainFile, `import { foo } from './utils.js'`) + + const firstResult = extractImportPaths(mainFile) + expect(firstResult).toContain(utilsFile) + + // Modify the file to add a new import + const helpersFile = joinPath(tmpDir, 'helpers.js') + await writeFile(helpersFile, 'export const helper = () => {}') + await writeFile(mainFile, `import { foo } from './utils.js'\nimport { helper } from './helpers.js'`) + + // Without clearing, we still get cached results + const cachedResult = extractImportPaths(mainFile) + expect(cachedResult).toHaveLength(1) + + // After clearing, new imports are picked up + clearImportPathsCache() + const freshResult = extractImportPaths(mainFile) + expect(freshResult).toContain(utilsFile) + expect(freshResult).toContain(helpersFile) + expect(freshResult).toHaveLength(2) + }) + }) +}) diff --git a/packages/cli-kit/src/public/node/import-extractor.ts b/packages/cli-kit/src/public/node/import-extractor.ts index a08c3501d70..b81ef2d6eaf 100644 --- a/packages/cli-kit/src/public/node/import-extractor.ts +++ b/packages/cli-kit/src/public/node/import-extractor.ts @@ -1,17 +1,34 @@ import {readFileSync, fileExistsSync, isDirectorySync} from './fs.js' import {dirname, joinPath} from './path.js' +// Caches direct import results per file path to avoid redundant file reads and parsing +// when multiple extensions import the same shared code. +const directImportsCache = new Map() + +/** + * Clears the import paths cache. Should be called when watched files change + * so that rescanning picks up updated imports. + */ +export function clearImportPathsCache(): void { + directImportsCache.clear() +} + /** * Extracts import paths from a source file. * Supports JavaScript, TypeScript, and Rust files. + * Results are cached per file path to avoid redundant I/O. * * @param filePath - Path to the file to analyze. * @returns Array of absolute paths to imported files. */ export function extractImportPaths(filePath: string): string[] { + const cached = directImportsCache.get(filePath) + if (cached) return cached + const content = readFileSync(filePath).toString() const ext = filePath.substring(filePath.lastIndexOf('.')) + let result: string[] switch (ext) { case '.js': case '.mjs': @@ -19,12 +36,17 @@ export function extractImportPaths(filePath: string): string[] { case '.ts': case '.tsx': case '.jsx': - return extractJSLikeImports(content, filePath) + result = extractJSLikeImports(content, filePath) + break case '.rs': - return extractRustImports(content, filePath) + result = extractRustImports(content, filePath) + break default: - return [] + result = [] } + + directImportsCache.set(filePath, result) + return result } /**