From 7e9e04c250d69d72816992b7b2b9b94983ab17f8 Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Tue, 26 Aug 2025 21:10:49 -0700 Subject: [PATCH 1/2] feat(compiler-sfc): support Node subpath imports for type resolution --- .../compileScript/resolveType.spec.ts | 36 +++++++++++++ packages/compiler-sfc/package.json | 1 + .../compiler-sfc/src/script/resolveType.ts | 53 ++++++++++++++++++- pnpm-lock.yaml | 9 ++++ 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index c0f4db82080..d6de8e27edb 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -1198,6 +1198,42 @@ describe('resolveType', () => { expect(deps && [...deps]).toStrictEqual(['/user.ts']) }) + test('node subpath imports', () => { + const files = { + '/package.json': JSON.stringify({ + imports: { + '#t1': './t1.ts', + '#t2': '/t2.ts', + '#o/*.ts': './other/*.ts', + }, + }), + '/t1.ts': 'export type T1 = { foo: string }', + '/t2.ts': 'export type T2 = { bar: number }', + '/other/t3.ts': 'export type T3 = { baz: string }', + } + + const { props, deps } = resolve( + ` + import type { T1 } from '#t1' + import type { T2 } from '#t2' + import type { T3 } from '#o/t3.ts' + defineProps() + `, + files, + ) + + expect(props).toStrictEqual({ + foo: ['String'], + bar: ['Number'], + baz: ['String'], + }) + expect(deps && [...deps]).toStrictEqual([ + '/t1.ts', + '/t2.ts', + '/other/t3.ts', + ]) + }) + test('ts module resolve w/ project reference folder', () => { const files = { '/tsconfig.json': JSON.stringify({ diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 67b4520b36e..494fbd5090c 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -62,6 +62,7 @@ "postcss-modules": "^6.0.1", "postcss-selector-parser": "^7.1.0", "pug": "^3.0.3", + "resolve.exports": "^2.0.3", "sass": "^1.90.0" } } diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index d8f43070050..9dbd4e9f415 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -39,9 +39,10 @@ import { parse as babelParse } from '@babel/parser' import { parse } from '../parse' import { createCache } from '../cache' import type TS from 'typescript' -import { dirname, extname, join } from 'path' +import { dirname, extname, join, resolve } from 'path' import { minimatch as isMatch } from 'minimatch' import * as process from 'process' +import { imports as resolveImports } from 'resolve.exports' export type SimpleTypeResolveOptions = Partial< Pick< @@ -958,7 +959,9 @@ function importSourceToScope( ) } } - resolved = resolveWithTS(scope.filename, source, ts, fs) + resolved = + resolveWithTS(scope.filename, source, ts, fs) || + resolveWithNodeSubpathImports(source, fs) } if (resolved) { resolved = scope.resolvedImportSources[source] = normalizePath(resolved) @@ -1123,6 +1126,52 @@ function loadTSConfig( return res } +function resolveWithNodeSubpathImports( + source: string, + fs: FS, +): string | undefined { + if (!__CJS__) return + + try { + const pkgPath = findPackageJsonFile(fs) + if (!pkgPath) { + return + } + + const pkgStr = fs.readFile(pkgPath) + if (!pkgStr) { + return + } + + const pkg = JSON.parse(pkgStr) + const resolvedImports = resolveImports(pkg, source) + if (!resolvedImports || !resolvedImports.length) { + return + } + + const resolved = resolve(dirname(pkgPath), resolvedImports[0]) + + return fs.realpath ? fs.realpath(resolved) : resolved + } catch (e) {} +} + +function findPackageJsonFile(fs: FS): string | undefined { + let currDir = process.cwd() + while (true) { + const filePath = joinPaths(currDir, 'package.json') + if (fs.fileExists(filePath)) { + return filePath + } + + const parentDir = dirname(currDir) + if (parentDir === currDir) { + return + } + + currDir = parentDir + } +} + const fileToScopeCache = createCache() /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea57b085232..36e8c5277f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,6 +332,9 @@ importers: pug: specifier: ^3.0.3 version: 3.0.3 + resolve.exports: + specifier: ^2.0.3 + version: 2.0.3 sass: specifier: ^1.90.0 version: 1.90.0 @@ -3025,6 +3028,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -6084,6 +6091,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} + resolve@1.22.8: dependencies: is-core-module: 2.15.0 From 7f28cc356aa40118e78ec1639a8734502b2b10df Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Sun, 31 Aug 2025 15:45:46 -0700 Subject: [PATCH 2/2] fix: fix on windows, add windows test --- .../compileScript/resolveType.spec.ts | 46 +++++++++++++++++++ packages/compiler-sfc/package.json | 2 +- .../compiler-sfc/src/script/resolveType.ts | 18 +++++--- pnpm-lock.yaml | 6 +-- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index d6de8e27edb..29ae6414b42 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -1234,6 +1234,52 @@ describe('resolveType', () => { ]) }) + test.runIf(process.platform === 'win32')( + 'node subpath imports on Windows', + () => { + const files = { + 'C:\\Test\\package.json': JSON.stringify({ + imports: { + '#t1': '.\\t1.ts', + '#t2': '..\\t2.ts', + '#t3': 'C:\\Test/t3.ts', + '#o/*.ts': '.\\Other\\*.ts', + }, + }), + 'C:\\Test\\t1.ts': 'export type T1 = { foo: string }', + 'C:\\t2.ts': 'export type T2 = { bar: number }', + 'C:\\Test\\t3.ts': 'export type T3 = { baz: number }', + 'C:\\Test\\Other\\t4.ts': 'export type T4 = { qux: string }', + } + + const { props, deps } = resolve( + ` + import type { T1 } from '#t1' + import type { T2 } from '#t2' + import type { T3 } from '#t3' + import type { T4 } from '#o/t4.ts' + defineProps() + `, + files, + {}, + 'C:\\Test\\Test.vue', + ) + + expect(props).toStrictEqual({ + foo: ['String'], + bar: ['Number'], + baz: ['Number'], + qux: ['String'], + }) + expect(deps && [...deps]).toStrictEqual([ + 'C:/Test/t1.ts', + 'C:/t2.ts', + 'C:/Test/t3.ts', + 'C:/Test/Other/t4.ts', + ]) + }, + ) + test('ts module resolve w/ project reference folder', () => { const files = { '/tsconfig.json': JSON.stringify({ diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 494fbd5090c..6e4188c65a1 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -50,6 +50,7 @@ "estree-walker": "catalog:", "magic-string": "catalog:", "postcss": "^8.5.6", + "resolve.exports": "^2.0.3", "source-map-js": "catalog:" }, "devDependencies": { @@ -62,7 +63,6 @@ "postcss-modules": "^6.0.1", "postcss-selector-parser": "^7.1.0", "pug": "^3.0.3", - "resolve.exports": "^2.0.3", "sass": "^1.90.0" } } diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 9dbd4e9f415..7c3228dabec 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -39,7 +39,7 @@ import { parse as babelParse } from '@babel/parser' import { parse } from '../parse' import { createCache } from '../cache' import type TS from 'typescript' -import { dirname, extname, join, resolve } from 'path' +import { dirname, extname, isAbsolute, join } from 'path' import { minimatch as isMatch } from 'minimatch' import * as process from 'process' import { imports as resolveImports } from 'resolve.exports' @@ -961,7 +961,7 @@ function importSourceToScope( } resolved = resolveWithTS(scope.filename, source, ts, fs) || - resolveWithNodeSubpathImports(source, fs) + resolveWithNodeSubpathImports(scope.filename, source, fs) } if (resolved) { resolved = scope.resolvedImportSources[source] = normalizePath(resolved) @@ -1127,13 +1127,14 @@ function loadTSConfig( } function resolveWithNodeSubpathImports( + containingFile: string, source: string, fs: FS, ): string | undefined { if (!__CJS__) return try { - const pkgPath = findPackageJsonFile(fs) + const pkgPath = findPackageJsonFile(containingFile, fs) if (!pkgPath) { return } @@ -1149,14 +1150,19 @@ function resolveWithNodeSubpathImports( return } - const resolved = resolve(dirname(pkgPath), resolvedImports[0]) + const resolved = isAbsolute(resolvedImports[0]) + ? resolvedImports[0] + : joinPaths(dirname(pkgPath), resolvedImports[0]) return fs.realpath ? fs.realpath(resolved) : resolved } catch (e) {} } -function findPackageJsonFile(fs: FS): string | undefined { - let currDir = process.cwd() +function findPackageJsonFile( + searchStartPath: string, + fs: FS, +): string | undefined { + let currDir = searchStartPath while (true) { const filePath = joinPaths(currDir, 'package.json') if (fs.fileExists(filePath)) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36e8c5277f7..03d206f6904 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -301,6 +301,9 @@ importers: postcss: specifier: ^8.5.6 version: 8.5.6 + resolve.exports: + specifier: ^2.0.3 + version: 2.0.3 source-map-js: specifier: 'catalog:' version: 1.2.1 @@ -332,9 +335,6 @@ importers: pug: specifier: ^3.0.3 version: 3.0.3 - resolve.exports: - specifier: ^2.0.3 - version: 2.0.3 sass: specifier: ^1.90.0 version: 1.90.0