diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index c0f4db82080..29ae6414b42 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -1198,6 +1198,88 @@ 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.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 67b4520b36e..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": { diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index d8f43070050..7c3228dabec 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, isAbsolute, join } 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(scope.filename, source, fs) } if (resolved) { resolved = scope.resolvedImportSources[source] = normalizePath(resolved) @@ -1123,6 +1126,58 @@ function loadTSConfig( return res } +function resolveWithNodeSubpathImports( + containingFile: string, + source: string, + fs: FS, +): string | undefined { + if (!__CJS__) return + + try { + const pkgPath = findPackageJsonFile(containingFile, 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 = isAbsolute(resolvedImports[0]) + ? resolvedImports[0] + : joinPaths(dirname(pkgPath), resolvedImports[0]) + + return fs.realpath ? fs.realpath(resolved) : resolved + } catch (e) {} +} + +function findPackageJsonFile( + searchStartPath: string, + fs: FS, +): string | undefined { + let currDir = searchStartPath + 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..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 @@ -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