diff --git a/packages/core/src/treeSitter/runtime/analyzeJavaScript/callImports.ts b/packages/core/src/treeSitter/runtime/analyzeJavaScript/callImports.ts index b5eac12b5..f8ab47757 100644 --- a/packages/core/src/treeSitter/runtime/analyzeJavaScript/callImports.ts +++ b/packages/core/src/treeSitter/runtime/analyzeJavaScript/callImports.ts @@ -7,6 +7,7 @@ import { getIdentifierText, getStringSpecifier } from '../analyze/nodes'; export function getImportRelationForJavaScriptCallExpression( callExpression: Parser.SyntaxNode, filePath: string, + workspaceRoot?: string, ): IAnalysisRelation | null { const calleeNode = callExpression.childForFieldName('function') ?? callExpression.namedChildren[0]; const argumentsNode = callExpression.childForFieldName('arguments') @@ -19,7 +20,9 @@ export function getImportRelationForJavaScriptCallExpression( return null; } - const resolvedPath = resolveTreeSitterImportPath(filePath, specifier); + const resolvedPath = workspaceRoot + ? resolveTreeSitterImportPath(filePath, specifier, workspaceRoot) + : resolveTreeSitterImportPath(filePath, specifier); if (calleeNode?.type === 'import') { return { kind: 'import', diff --git a/packages/core/src/treeSitter/runtime/analyzeJavaScript/calls.ts b/packages/core/src/treeSitter/runtime/analyzeJavaScript/calls.ts index 95f06b614..2f97c75d8 100644 --- a/packages/core/src/treeSitter/runtime/analyzeJavaScript/calls.ts +++ b/packages/core/src/treeSitter/runtime/analyzeJavaScript/calls.ts @@ -22,8 +22,9 @@ export function handleJavaScriptCallExpression( relations: IAnalysisRelation[], importedBindings: ReadonlyMap, currentSymbolId?: string, + workspaceRoot?: string, ): void { - const importRelation = getImportRelationForJavaScriptCallExpression(node, filePath); + const importRelation = getImportRelationForJavaScriptCallExpression(node, filePath, workspaceRoot); if (importRelation) { relations.push(importRelation); return; diff --git a/packages/core/src/treeSitter/runtime/analyzeJavaScript/file.ts b/packages/core/src/treeSitter/runtime/analyzeJavaScript/file.ts index 9d75874aa..44f439086 100644 --- a/packages/core/src/treeSitter/runtime/analyzeJavaScript/file.ts +++ b/packages/core/src/treeSitter/runtime/analyzeJavaScript/file.ts @@ -31,6 +31,7 @@ type JavaScriptVisitContext = { state: SymbolWalkState; symbols: IAnalysisSymbol[]; symbolsEnabled: boolean; + workspaceRoot: string; walk: (node: Parser.SyntaxNode, context: SymbolWalkState) => void; }; @@ -58,6 +59,7 @@ const JAVASCRIPT_NODE_VISITORS: Record = { context.relations, context.importedBindings, context.state.currentSymbolId, + context.workspaceRoot, ); }, class_declaration: (node, context) => { @@ -78,6 +80,7 @@ const JAVASCRIPT_NODE_VISITORS: Record = { context.relations, context.symbols, context.symbolsEnabled, + context.workspaceRoot, ); }, function_declaration: (node, context) => @@ -85,7 +88,13 @@ const JAVASCRIPT_NODE_VISITORS: Record = { ? handleJavaScriptFunctionDeclaration(node, context.filePath, context.symbols, context.walk) : undefined, import_statement: (node, context) => - handleJavaScriptImportStatement(node, context.filePath, context.relations, context.importedBindings), + handleJavaScriptImportStatement( + node, + context.filePath, + context.relations, + context.importedBindings, + context.workspaceRoot, + ), interface_declaration: handleTypeDeclarationNode, method_definition: (node, context) => context.symbolsEnabled @@ -107,6 +116,7 @@ function visitJavaScriptNode( symbols: IAnalysisSymbol[], importedBindings: Map, symbolsEnabled: boolean, + workspaceRoot: string, ): TreeWalkAction | void { const visitor = JAVASCRIPT_NODE_VISITORS[node.type]; return visitor?.(node, { @@ -116,6 +126,7 @@ function visitJavaScriptNode( state, symbols, symbolsEnabled, + workspaceRoot, walk, }); } @@ -123,7 +134,7 @@ function visitJavaScriptNode( export function analyzeJavaScriptFamilyFile( filePath: string, tree: Parser.Tree, - _workspaceRoot: string, + workspaceRoot: string, options: TreeSitterAnalysisOptions = {}, ): IFileAnalysisResult { const importedBindings = new Map(); @@ -140,6 +151,7 @@ export function analyzeJavaScriptFamilyFile( symbols, importedBindings, symbolsEnabled, + workspaceRoot, ), ); return normalizeAnalysisResult(filePath, symbols, relations); diff --git a/packages/core/src/treeSitter/runtime/analyzeJavaScript/imports.ts b/packages/core/src/treeSitter/runtime/analyzeJavaScript/imports.ts index 40bd607dd..55347ffd0 100644 --- a/packages/core/src/treeSitter/runtime/analyzeJavaScript/imports.ts +++ b/packages/core/src/treeSitter/runtime/analyzeJavaScript/imports.ts @@ -23,6 +23,7 @@ export function handleJavaScriptImportStatement( filePath: string, relations: IAnalysisRelation[], importedBindings: Map, + workspaceRoot?: string, ): TreeWalkAction { const specifier = getStringSpecifier(node.namedChildren.find((child) => child.type === 'string')); if (!specifier) { @@ -34,7 +35,9 @@ export function handleJavaScriptImportStatement( importedBindings, node, relations, - resolvedPath: resolveTreeSitterImportPath(filePath, specifier), + resolvedPath: workspaceRoot + ? resolveTreeSitterImportPath(filePath, specifier, workspaceRoot) + : resolveTreeSitterImportPath(filePath, specifier), specifier, }; @@ -82,10 +85,13 @@ export function handleJavaScriptExportStatement( relations: IAnalysisRelation[], symbols: IAnalysisSymbol[] = [], symbolsEnabled = true, + workspaceRoot?: string, ): void { const specifier = getStringSpecifier(node.namedChildren.find((child) => child.type === 'string')); if (!specifier) return; - const resolvedPath = resolveTreeSitterImportPath(filePath, specifier); + const resolvedPath = workspaceRoot + ? resolveTreeSitterImportPath(filePath, specifier, workspaceRoot) + : resolveTreeSitterImportPath(filePath, specifier); const exportClause = node.namedChildren.find((child) => child.type === 'export_clause'); if (!exportClause) { addReexportRelation(relations, filePath, specifier, resolvedPath, { reexportAll: true }); diff --git a/packages/core/src/treeSitter/runtime/resolve.ts b/packages/core/src/treeSitter/runtime/resolve.ts index 64ad7081c..ced75ac0c 100644 --- a/packages/core/src/treeSitter/runtime/resolve.ts +++ b/packages/core/src/treeSitter/runtime/resolve.ts @@ -1,5 +1,6 @@ import * as path from 'node:path'; import { findExistingFile } from './analyze/existingFile'; +import { resolvePathMappingBases } from './resolve/pathMappings'; const IMPORT_RESOLUTION_EXTENSIONS = [ '', @@ -23,8 +24,14 @@ function findExistingPath(basePath: string): string | null { export function resolveTreeSitterImportPath( filePath: string, specifier: string, + workspaceRoot?: string, ): string | null { - if (!specifier.startsWith('.') && !specifier.startsWith('/')) { + if (!specifier.startsWith('.') && !path.isAbsolute(specifier)) { + if (!workspaceRoot) return null; + for (const candidateBasePath of resolvePathMappingBases(filePath, workspaceRoot, specifier)) { + const resolvedPath = findExistingPath(candidateBasePath); + if (resolvedPath) return resolvedPath; + } return null; } diff --git a/packages/core/src/treeSitter/runtime/resolve/pathMappings.ts b/packages/core/src/treeSitter/runtime/resolve/pathMappings.ts new file mode 100644 index 000000000..272af9f5b --- /dev/null +++ b/packages/core/src/treeSitter/runtime/resolve/pathMappings.ts @@ -0,0 +1,168 @@ +import * as path from 'node:path'; +import { + treeSitterPathIsFile, + treeSitterReadTextFile, +} from '../pathHost'; + +type PathMapping = { + pattern: string; + targets: readonly string[]; +}; + +type PathMappingConfig = { + basePath: string; + mappings: readonly PathMapping[]; +}; + +function parseJsonWithComments(source: string): unknown { + let output = ''; + let inString = false; + let escaped = false; + let lineComment = false; + let blockComment = false; + + for (let index = 0; index < source.length; index += 1) { + const character = source[index]; + const nextCharacter = source[index + 1]; + + if (lineComment) { + if (character === '\n' || character === '\r') { + lineComment = false; + output += character; + } else { + output += ' '; + } + continue; + } + + if (blockComment) { + if (character === '*' && nextCharacter === '/') { + output += ' '; + blockComment = false; + index += 1; + } else { + output += character === '\n' || character === '\r' ? character : ' '; + } + continue; + } + + if (inString) { + output += character; + if (escaped) { + escaped = false; + } else if (character === '\\') { + escaped = true; + } else if (character === '"') { + inString = false; + } + continue; + } + + if (character === '"') { + inString = true; + output += character; + } else if (character === '/' && nextCharacter === '/') { + lineComment = true; + output += ' '; + index += 1; + } else if (character === '/' && nextCharacter === '*') { + blockComment = true; + output += ' '; + index += 1; + } else { + output += character; + } + } + + return JSON.parse(output.replace(/,\s*([}\]])/g, '$1')) as unknown; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function readPathMappingConfig(configPath: string): PathMappingConfig | null { + const source = treeSitterReadTextFile(configPath); + if (!source) return null; + + try { + const config = parseJsonWithComments(source); + if (!isRecord(config) || !isRecord(config.compilerOptions)) return null; + + const compilerOptions = config.compilerOptions; + if (!isRecord(compilerOptions.paths)) return null; + + const configDirectory = path.dirname(configPath); + const basePath = typeof compilerOptions.baseUrl === 'string' + ? path.resolve(configDirectory, compilerOptions.baseUrl) + : configDirectory; + const mappings = Object.entries(compilerOptions.paths).flatMap(([pattern, targets]) => { + if (!Array.isArray(targets)) return []; + const stringTargets = targets.filter((target): target is string => typeof target === 'string'); + return stringTargets.length > 0 ? [{ pattern, targets: stringTargets }] : []; + }); + + return mappings.length > 0 ? { basePath, mappings } : null; + } catch { + return null; + } +} + +function isWithinWorkspace(candidatePath: string, workspaceRoot: string): boolean { + const relativePath = path.relative(workspaceRoot, candidatePath); + return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); +} + +function findNearestPathMappingConfig( + filePath: string, + workspaceRoot: string, +): PathMappingConfig | null { + let directory = path.dirname(filePath); + const root = path.resolve(workspaceRoot); + + if (!isWithinWorkspace(directory, root)) return null; + + while (true) { + for (const configName of ['tsconfig.json', 'jsconfig.json']) { + const configPath = path.join(directory, configName); + if (treeSitterPathIsFile(configPath)) { + const config = readPathMappingConfig(configPath); + if (config) return config; + } + } + + if (directory === root) return null; + const parentDirectory = path.dirname(directory); + if (parentDirectory === directory || !isWithinWorkspace(parentDirectory, root)) return null; + directory = parentDirectory; + } +} + +function matchPathPattern(pattern: string, specifier: string): string | null { + const wildcardIndex = pattern.indexOf('*'); + if (wildcardIndex < 0) return pattern === specifier ? '' : null; + + const prefix = pattern.slice(0, wildcardIndex); + const suffix = pattern.slice(wildcardIndex + 1); + if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) return null; + return specifier.slice(prefix.length, specifier.length - suffix.length); +} + +export function resolvePathMappingBases( + filePath: string, + workspaceRoot: string, + specifier: string, +): string[] { + const config = findNearestPathMappingConfig(filePath, workspaceRoot); + if (!config) return []; + + for (const mapping of config.mappings) { + const wildcard = matchPathPattern(mapping.pattern, specifier); + if (wildcard === null) continue; + return mapping.targets.map(target => + path.resolve(config.basePath, target.replace('*', wildcard)), + ); + } + + return []; +} diff --git a/packages/core/tests/treeSitter/resolve.test.ts b/packages/core/tests/treeSitter/resolve.test.ts index 75a40d507..79113626c 100644 --- a/packages/core/tests/treeSitter/resolve.test.ts +++ b/packages/core/tests/treeSitter/resolve.test.ts @@ -53,6 +53,41 @@ describe('pipeline/plugins/treesitter/runtime/resolve', () => { expect(resolveTreeSitterImportPath(filePath, './missing')).toBeNull(); }); + it('resolves compiler path mappings from the nearest tsconfig', () => { + const workspaceRoot = createWorkspaceRoot(); + const filePath = writeWorkspaceFile(workspaceRoot, 'src/components/MeasureTool.vue', ''); + const storePath = writeWorkspaceFile(workspaceRoot, 'src/stores/useMeasureStore.ts', ''); + writeWorkspaceFile(workspaceRoot, 'tsconfig.json', `{ + // Vite source alias + "compilerOptions": { + "paths": { + "@/*": ["./src/*"], + }, + }, + }`); + + expect(resolveTreeSitterImportPath( + filePath, + '@/stores/useMeasureStore', + workspaceRoot, + )).toBe(storePath); + }); + + it('uses baseUrl and ordered path mapping targets without resolving external packages', () => { + const workspaceRoot = createWorkspaceRoot(); + const filePath = writeWorkspaceFile(workspaceRoot, 'packages/app/src/app.ts', ''); + const fallbackPath = writeWorkspaceFile(workspaceRoot, 'packages/app/source/lib/helper.ts', ''); + writeWorkspaceFile(workspaceRoot, 'packages/app/tsconfig.json', JSON.stringify({ + compilerOptions: { + baseUrl: '.', + paths: { '@app/*': ['missing/*', 'source/*'] }, + }, + })); + + expect(resolveTreeSitterImportPath(filePath, '@app/lib/helper', workspaceRoot)).toBe(fallbackPath); + expect(resolveTreeSitterImportPath(filePath, 'react', workspaceRoot)).toBeNull(); + }); + it('resolves exact files before trying extension variants and index fallbacks', () => { const workspaceRoot = createWorkspaceRoot(); const filePath = writeWorkspaceFile(workspaceRoot, 'src/app.ts', 'export {};\n'); diff --git a/packages/plugin-vue/src/analysis.ts b/packages/plugin-vue/src/analysis.ts index 12aa6d2f3..320d6a512 100644 --- a/packages/plugin-vue/src/analysis.ts +++ b/packages/plugin-vue/src/analysis.ts @@ -8,7 +8,11 @@ const SCRIPT_IMPORT_SOURCE_ID = 'sfc-script-import'; const SCRIPT_TYPE_IMPORT_SOURCE_ID = 'sfc-script-type-import'; const SCRIPT_DYNAMIC_IMPORT_SOURCE_ID = 'sfc-script-dynamic-import'; -export function analyzeVueSfc(filePath: string, content: string): IFileAnalysisResult { +export function analyzeVueSfc( + filePath: string, + content: string, + workspaceRoot?: string, +): IFileAnalysisResult { if (!filePath.endsWith('.vue')) { return { filePath, relations: [] }; } @@ -26,8 +30,8 @@ export function analyzeVueSfc(filePath: string, content: string): IFileAnalysisR return { filePath, relations: [ - ...extractVueScriptImportRelations(filePath, scriptContents), - ...extractScriptCalls(filePath, scriptContents.join('\n')), + ...extractVueScriptImportRelations(filePath, scriptContents, workspaceRoot), + ...extractScriptCalls(filePath, scriptContents.join('\n'), workspaceRoot), ], }; } @@ -35,12 +39,13 @@ export function analyzeVueSfc(filePath: string, content: string): IFileAnalysisR function extractVueScriptImportRelations( filePath: string, scriptContents: string[], + workspaceRoot?: string, ): IAnalysisRelation[] { return scriptContents .flatMap(scriptContent => extractScriptImports(filePath, scriptContent)) .map(scriptImport => ({ scriptImport, - resolvedPath: resolveVueScriptImport(filePath, scriptImport.specifier), + resolvedPath: resolveVueScriptImport(filePath, scriptImport.specifier, workspaceRoot), })) .filter((relation): relation is { scriptImport: ReturnType[number]; diff --git a/packages/plugin-vue/src/calls.ts b/packages/plugin-vue/src/calls.ts index 133c1d40e..b4240d75a 100644 --- a/packages/plugin-vue/src/calls.ts +++ b/packages/plugin-vue/src/calls.ts @@ -11,7 +11,11 @@ interface ImportedCallable { specifier: string; } -export function extractScriptCalls(filePath: string, content: string): IAnalysisRelation[] { +export function extractScriptCalls( + filePath: string, + content: string, + workspaceRoot?: string, +): IAnalysisRelation[] { const sourceFile = ts.createSourceFile( filePath, content, @@ -19,7 +23,7 @@ export function extractScriptCalls(filePath: string, content: string): IAnalysis true, ts.ScriptKind.TS, ); - const callables = collectImportedCallables(filePath, sourceFile); + const callables = collectImportedCallables(filePath, sourceFile, workspaceRoot); const relations: IAnalysisRelation[] = []; const seen = new Set(); @@ -28,7 +32,11 @@ export function extractScriptCalls(filePath: string, content: string): IAnalysis return relations; } -function collectImportedCallables(filePath: string, sourceFile: ts.SourceFile): Map { +function collectImportedCallables( + filePath: string, + sourceFile: ts.SourceFile, + workspaceRoot?: string, +): Map { const callables = new Map(); for (const statement of sourceFile.statements) { @@ -41,7 +49,7 @@ function collectImportedCallables(filePath: string, sourceFile: ts.SourceFile): continue; } - const resolvedPath = resolveVueScriptImport(filePath, specifier); + const resolvedPath = resolveVueScriptImport(filePath, specifier, workspaceRoot); if (!resolvedPath) { continue; } diff --git a/packages/plugin-vue/src/plugin.ts b/packages/plugin-vue/src/plugin.ts index a3c65c324..6e8893ffd 100644 --- a/packages/plugin-vue/src/plugin.ts +++ b/packages/plugin-vue/src/plugin.ts @@ -14,8 +14,8 @@ export function createVuePlugin(): IPlugin { contributeGraphScopeCapabilities: () => ({ edgeTypes: ['import', 'type-import', 'call'], }), - analyzeFile(filePath: string, content: string): Promise { - return Promise.resolve(analyzeVueSfc(filePath, content)); + analyzeFile(filePath: string, content: string, workspaceRoot: string): Promise { + return Promise.resolve(analyzeVueSfc(filePath, content, workspaceRoot)); }, }; } diff --git a/packages/plugin-vue/src/resolver.ts b/packages/plugin-vue/src/resolver.ts index f8b09b901..1a729def6 100644 --- a/packages/plugin-vue/src/resolver.ts +++ b/packages/plugin-vue/src/resolver.ts @@ -1,5 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; +import ts from 'typescript'; const RELATIVE_SPECIFIER_PREFIXES = ['./', '../'] as const; const EXTENSIONLESS_IMPORT_CANDIDATE_EXTENSIONS = [ @@ -27,18 +28,92 @@ const IMPORT_EXTENSION_SUBSTITUTIONS: Record = { '.cts': ['.cts'], }; -export function resolveVueScriptImport(filePath: string, specifier: string): string | null { - if (!isRelativeSpecifier(specifier)) { - return null; +export function resolveVueScriptImport( + filePath: string, + specifier: string, + workspaceRoot?: string, +): string | null { + if (isRelativeSpecifier(specifier)) { + return resolveExistingFile(path.resolve(path.dirname(filePath), specifier)); } - return resolveExistingFile(path.resolve(path.dirname(filePath), specifier)); + return workspaceRoot + ? resolveCompilerPathMapping(filePath, specifier, workspaceRoot) + : null; } function isRelativeSpecifier(specifier: string): boolean { return RELATIVE_SPECIFIER_PREFIXES.some(prefix => specifier.startsWith(prefix)); } +function resolveCompilerPathMapping( + filePath: string, + specifier: string, + workspaceRoot: string, +): string | null { + const configPath = findNearestCompilerConfig(filePath, workspaceRoot); + if (!configPath) return null; + + const readResult = ts.readConfigFile(configPath, ts.sys.readFile); + if (readResult.error || !readResult.config) return null; + + const configDirectory = path.dirname(configPath); + const compilerOptions = ts.parseJsonConfigFileContent( + readResult.config, + ts.sys, + configDirectory, + ).options; + if (!compilerOptions.paths || !matchesPathMapping(specifier, compilerOptions.paths)) return null; + + const resolvedModule = ts.resolveModuleName( + specifier, + filePath, + compilerOptions, + ts.sys, + ).resolvedModule; + const resolvedPath = resolvedModule?.resolvedFileName; + return resolvedPath && isWithinWorkspace(resolvedPath, workspaceRoot) + ? path.normalize(resolvedPath) + : null; +} + +function findNearestCompilerConfig(filePath: string, workspaceRoot: string): string | null { + let directory = path.dirname(filePath); + const root = path.resolve(workspaceRoot); + if (!isWithinWorkspace(directory, root)) return null; + + while (true) { + for (const configName of ['tsconfig.json', 'jsconfig.json']) { + const configPath = path.join(directory, configName); + if (fs.existsSync(configPath) && fs.statSync(configPath).isFile()) return configPath; + } + + if (directory === root) return null; + const parentDirectory = path.dirname(directory); + if (parentDirectory === directory || !isWithinWorkspace(parentDirectory, root)) return null; + directory = parentDirectory; + } +} + +function matchesPathMapping( + specifier: string, + paths: NonNullable, +): boolean { + return Object.keys(paths).some(pattern => { + const wildcardIndex = pattern.indexOf('*'); + if (wildcardIndex < 0) return pattern === specifier; + + const prefix = pattern.slice(0, wildcardIndex); + const suffix = pattern.slice(wildcardIndex + 1); + return specifier.startsWith(prefix) && specifier.endsWith(suffix); + }); +} + +function isWithinWorkspace(candidatePath: string, workspaceRoot: string): boolean { + const relativePath = path.relative(workspaceRoot, candidatePath); + return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); +} + function resolveExistingFile(basePath: string): string | null { const candidates = createExistingFileCandidates(basePath); return candidates.find(candidate => fs.existsSync(candidate) && fs.statSync(candidate).isFile()) ?? null; diff --git a/packages/plugin-vue/tests/analysis.test.ts b/packages/plugin-vue/tests/analysis.test.ts index 7eb73524a..9b4ab7e1e 100644 --- a/packages/plugin-vue/tests/analysis.test.ts +++ b/packages/plugin-vue/tests/analysis.test.ts @@ -3,6 +3,61 @@ import { createVuePlugin } from '../src/plugin'; import { createWorkspaceRoot, removeWorkspaceRoot, writeWorkspaceFile } from './workspace'; describe('Vue SFC analysis', () => { + it('resolves tsconfig path aliases in Vue script imports and calls', async () => { + const workspaceRoot = createWorkspaceRoot(); + try { + const source = [ + '', + ].join('\n'); + const sourcePath = writeWorkspaceFile( + workspaceRoot, + 'src/components/panel/tools/MeasureTool.vue', + source, + ); + const storePath = writeWorkspaceFile( + workspaceRoot, + 'src/stores/useMeasureStore.ts', + 'export function useMeasureStore(): void {}\n', + ); + writeWorkspaceFile(workspaceRoot, 'tsconfig.json', `{ + "compilerOptions": { + "moduleResolution": "bundler", + "paths": { "@/*": ["./src/*"] }, + }, + }`); + + const result = await createVuePlugin().analyzeFile?.(sourcePath, source, workspaceRoot); + + expect(result?.relations).toEqual([ + { + kind: 'import', + sourceId: 'sfc-script-import', + fromFilePath: sourcePath, + toFilePath: storePath, + resolvedPath: storePath, + specifier: '@/stores/useMeasureStore', + }, + { + kind: 'call', + sourceId: 'sfc-script-call', + fromFilePath: sourcePath, + toFilePath: storePath, + resolvedPath: storePath, + specifier: '@/stores/useMeasureStore', + metadata: { + importedName: 'useMeasureStore', + localName: 'useMeasureStore', + }, + }, + ]); + } finally { + removeWorkspaceRoot(workspaceRoot); + } + }); + it('emits runtime import relationships from script setup and normal script blocks', async () => { const workspaceRoot = createWorkspaceRoot(); try {