Skip to content

Conversation

alex-snezhko
Copy link
Contributor

@alex-snezhko alex-snezhko commented Aug 27, 2025

close #13586

Support reading user-defined Node.js subpath imports for type resolution.

Example:

Comp1.vue

<script setup lang="ts">
import type { MyProps } from '#comp2'
const props = defineProps<MyProps>()
</script>

<template>
  {{ props.abc }}
</template>

Comp2.vue

<script setup lang="ts">
export interface MyProps {
  abc: number;
}
</script>

<template>
  Hello
</template>

package.json

{
  ...
  "imports": {
    "#comp2": "./Comp2.vue"
  }
}

Summary by CodeRabbit

  • New Features
    • Support Node.js-style subpath imports in SFC type resolution, improving compatibility with package.json imports/exports and accurate dependency tracking.
  • Bug Fixes
    • Fix resolution of non-relative type imports by adding a reliable fallback when TypeScript-based resolution fails (CommonJS environments).
  • Tests
    • Added coverage for subpath imports and dependency collection to ensure correct behavior.
  • Chores
    • Added a development dependency to enable subpath import resolution during development.

Copy link

coderabbitai bot commented Aug 27, 2025

Walkthrough

Adds Node subpath import resolution fallback in type resolver using resolve.exports, integrates it into importSourceToScope after TS resolution, introduces helpers to locate/read nearest package.json, and expands tests to cover node-style subpath imports. Also adds a devDependency on resolve.exports in package.json.

Changes

Cohort / File(s) Summary of Changes
Type resolution logic
packages/compiler-sfc/src/script/resolveType.ts
Adds Node subpath import resolution via resolve.exports as a fallback to TS resolution; introduces findPackageJsonFile and resolveWithNodeSubpathImports; integrates fallback in importSourceToScope; guarded by CJS; uses path.resolve and fs realpath when available.
Tests: subpath imports
packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
Adds "node subpath imports" test validating type resolution and dependency collection for package.json imports map with specifiers (#t1, #t2, #o/*.ts).
Tooling/devDeps
packages/compiler-sfc/package.json
Adds devDependency "resolve.exports@^2.0.3".

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant SFC as SFC Compiler
  participant RTS as resolveType.importSourceToScope
  participant TS as resolveWithTS
  participant NSI as resolveWithNodeSubpathImports
  participant FS as FS
  participant RE as resolve.exports

  SFC->>RTS: resolve import source for types
  RTS->>TS: attempt TS-based resolution
  alt TS resolves
    TS-->>RTS: resolved path
    RTS-->>SFC: resolved and scoped types
  else TS fails
    note over RTS,NSI: Fallback (CommonJS only)
    RTS->>NSI: resolve via Node subpath imports
    NSI->>FS: findPackageJsonFile (walk up)
    FS-->>NSI: package.json path (or none)
    alt package.json found
      NSI->>FS: read package.json
      NSI->>RE: resolveExports(imports map, specifier)
      RE-->>NSI: mapped subpath
      NSI->>FS: realpath (optional)
      NSI-->>RTS: resolved path (if successful)
      RTS-->>SFC: resolved and scoped types
    else not found or mapping fails
      NSI-->>RTS: no resolution
      RTS-->>SFC: unresolved (existing handling)
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Resolve external types via package.json import map in @vue/compiler-sfc (#13586)

Assessment against linked issues: Out-of-scope changes

(none)

Suggested labels

ready to merge, scope: sfc

Poem

I hop through paths with nimble feet,
From #t1 lanes to /t2 street;
A map in paws, exports in sight,
I find the types by moonlit night.
Baz and foo now neatly linked—
My whiskers twitch: the build’s distinct! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 101 kB 38.4 kB 34.6 kB
vue.global.prod.js 159 kB 58.5 kB 52.2 kB

Usages

Name Size Gzip Brotli
createApp (CAPI only) 46.5 kB 18.2 kB 16.7 kB
createApp 54.5 kB 21.2 kB 19.4 kB
createSSRApp 58.7 kB 22.9 kB 20.9 kB
defineCustomElement 59.5 kB 22.8 kB 20.8 kB
overall 68.5 kB 26.4 kB 24 kB

Copy link

pkg-pr-new bot commented Aug 27, 2025

Open in StackBlitz

@vue/compiler-core

npm i https://pkg.pr.new/@vue/compiler-core@13813

@vue/compiler-dom

npm i https://pkg.pr.new/@vue/compiler-dom@13813

@vue/compiler-sfc

npm i https://pkg.pr.new/@vue/compiler-sfc@13813

@vue/compiler-ssr

npm i https://pkg.pr.new/@vue/compiler-ssr@13813

@vue/reactivity

npm i https://pkg.pr.new/@vue/reactivity@13813

@vue/runtime-core

npm i https://pkg.pr.new/@vue/runtime-core@13813

@vue/runtime-dom

npm i https://pkg.pr.new/@vue/runtime-dom@13813

@vue/server-renderer

npm i https://pkg.pr.new/@vue/server-renderer@13813

@vue/shared

npm i https://pkg.pr.new/@vue/shared@13813

vue

npm i https://pkg.pr.new/vue@13813

@vue/compat

npm i https://pkg.pr.new/@vue/compat@13813

commit: 7e9e04c

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
packages/compiler-sfc/src/script/resolveType.ts (2)

1159-1173: Consider caching package.json for performance.

The findPackageJsonFile function searches for package.json by traversing the directory tree on every call. Since package.json files rarely change during runtime, consider implementing a simple cache to avoid repeated filesystem operations.

Here's a suggested implementation with caching:

+const packageJsonCache = new Map<string, string | undefined>()
+
 function findPackageJsonFile(fs: FS): string | undefined {
+  const cacheKey = process.cwd()
+  if (packageJsonCache.has(cacheKey)) {
+    return packageJsonCache.get(cacheKey)
+  }
+  
   let currDir = process.cwd()
   while (true) {
     const filePath = joinPaths(currDir, 'package.json')
     if (fs.fileExists(filePath)) {
+      packageJsonCache.set(cacheKey, filePath)
       return filePath
     }

     const parentDir = dirname(currDir)
     if (parentDir === currDir) {
+      packageJsonCache.set(cacheKey, undefined)
       return
     }

     currDir = parentDir
   }
 }

1152-1152: Consider using joinPaths for consistency.

The code uses resolve from the path module here, but uses joinPaths elsewhere in the codebase for path operations. Consider using joinPaths for consistency with the rest of the codebase.

-    const resolved = resolve(dirname(pkgPath), resolvedImports[0])
+    const resolved = joinPaths(dirname(pkgPath), resolvedImports[0])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 24fccb4 and 7e9e04c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts (1 hunks)
  • packages/compiler-sfc/package.json (1 hunks)
  • packages/compiler-sfc/src/script/resolveType.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/compiler-sfc/src/script/resolveType.ts (1)
packages/compiler-sfc/src/script/utils.ts (1)
  • joinPaths (111-112)
🪛 GitHub Actions: ci
packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

[error] 1-1: Test 'external type imports > node subpath imports' failed while running 'pnpm run test-unit': [@vue/compiler-sfc] Failed to resolve import source '#t1'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (2)
packages/compiler-sfc/src/script/resolveType.ts (1)

42-45: Incorrect import usage of resolve.exports.

The destructured import { imports as resolveImports } is incorrect. According to the resolve.exports documentation, the correct import should be { imports } and it should be used as imports() not resolveImports().

-import { imports as resolveImports } from 'resolve.exports'
+import { imports } from 'resolve.exports'

Likely an incorrect or invalid review comment.

packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts (1)

1201-1235: Test correctly validates the new Node subpath imports feature.

The test effectively covers the main functionality:

  • Maps #t1 to ./t1.ts (relative path)
  • Maps #t2 to /t2.ts (absolute path)
  • Maps wildcard pattern #o/*.ts to ./other/*.ts
  • Verifies type resolution and dependency tracking

@@ -1198,6 +1198,42 @@ describe('resolveType', () => {
expect(deps && [...deps]).toStrictEqual(['/user.ts'])
})

test('node subpath imports', () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Test failure indicates the feature isn't working as expected.

Based on the pipeline failure message "Failed to resolve import source '#t1'", the Node subpath imports resolution is not working correctly. This could be due to:

  1. The incorrect import statement for resolve.exports (as noted in the other file)
  2. The package being in devDependencies instead of dependencies
  3. Missing __CJS__ flag during test execution

Let me verify the test environment setup:


🏁 Script executed:

#!/bin/bash
# Check if __CJS__ is defined in the test environment
rg -n "__CJS__" packages/compiler-sfc/__tests__/ -A 2 -B 2

# Check how the tests are built and run
fd "jest.config" --type f | xargs cat 2>/dev/null

# Check the build configuration for tests
fd "rollup.config" --type f | head -5 | xargs grep -l "compiler-sfc" 2>/dev/null | xargs cat 2>/dev/null | grep -A 10 -B 10 "__CJS__"

Length of output: 993


🏁 Script executed:

#!/bin/bash
# Search for usages of resolve.exports in the compiler-sfc package
rg -n "resolve\\.exports" -C3 packages/compiler-sfc

# Inspect dependencies and devDependencies in the compiler-sfc package.json
grep -n "\"dependencies\"" -A20 packages/compiler-sfc/package.json
grep -n "\"devDependencies\"" -A20 packages/compiler-sfc/package.json

Length of output: 2326


🏁 Script executed:

#!/bin/bash
# Show code around __CJS__ usage in resolveType.ts
rg -n "__CJS__" -C5 packages/compiler-sfc/src/script/resolveType.ts

# Show how resolveImports is invoked
rg -n "resolveImports" -C5 packages/compiler-sfc/src/script/resolveType.ts

# Inspect the failing test for '#t1' import source
rg -n "'#t1'" -C5 packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

Length of output: 2807


Ensure Node subpath imports are resolved at runtime and in tests

The failure “Failed to resolve import source ‘#t1’” is expected:

  • At runtime, resolve.exports is only listed under devDependencies, so it isn’t available when this code path executes.
  • In the unbundled test harness, the __CJS__ guard is falsey by default (no global definition), so resolveWithNodeSubpathImports immediately returns without attempting subpath resolution.

To fix this:

  • Move resolve.exports into dependencies so it’s always installed at runtime.
  • Define __CJS__ = true in the test environment (e.g. via a Jest setup file or globals in your test config) so the Node‐only resolution path is exercised.
  • No change is needed to the import statement (import { imports as resolveImports } from 'resolve.exports' is correct).

Locations to update:

  • packages/compiler-sfc/package.json
     "dependencies": {
       "@babel/parser": "catalog:",
       // …
  • },
  • "devDependencies": {
  • "resolve.exports": "^2.0.3",
  • }
    +},
  • "dependencies": {
  • "resolve.exports": "^2.0.3",
  • // …
    +},
  • "devDependencies": {
  • // other dev deps…
    // …
    }

- **Test setup** (e.g. add a `setupTests.ts` or update Jest/Vitest config)  
```ts
// packages/compiler-sfc/__tests__/setupTests.ts
;(global as any).__CJS__ = true

and ensure it’s loaded before tests (via your test runner’s setupFiles/setup option).

These changes will install the resolver at runtime and unblock the resolveWithNodeSubpathImports logic in tests.

🤖 Prompt for AI Agents
In packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts around line
1201, the test fails because resolve.exports is only in devDependencies and the
test runtime doesn't set the __CJS__ global, so Node subpath resolution path
isn't executed; fix by moving "resolve.exports" from devDependencies to
dependencies in packages/compiler-sfc/package.json so it is installed at
runtime, and add a test setup file (or update your Jest/Vitest config's
setupFiles) that sets global.__CJS__ = true before tests run so
resolveWithNodeSubpathImports is exercised (no change to the import statement is
required).

@@ -62,6 +62,7 @@
"postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.1.0",
"pug": "^3.0.3",
"resolve.exports": "^2.0.3",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Should be a regular dependency, not a devDependency.

The resolve.exports package is used in production code (src/script/resolveType.ts) to resolve Node.js subpath imports. Since it's required at runtime when the package is used by consumers, it should be listed under dependencies rather than devDependencies.

Apply this fix to move it to the correct section:

   "dependencies": {
     "@babel/parser": "catalog:",
     "@vue/compiler-core": "workspace:*",
     "@vue/compiler-dom": "workspace:*",
     "@vue/compiler-ssr": "workspace:*",
     "@vue/shared": "workspace:*",
     "estree-walker": "catalog:",
     "magic-string": "catalog:",
     "postcss": "^8.5.6",
+    "resolve.exports": "^2.0.3",
     "source-map-js": "catalog:"
   },
   "devDependencies": {
     "@babel/types": "catalog:",
     "@vue/consolidate": "^1.0.0",
     "hash-sum": "^2.0.0",
     "lru-cache": "10.1.0",
     "merge-source-map": "^1.1.0",
     "minimatch": "~10.0.3",
     "postcss-modules": "^6.0.1",
     "postcss-selector-parser": "^7.1.0",
     "pug": "^3.0.3",
-    "resolve.exports": "^2.0.3",
     "sass": "^1.90.0"
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"resolve.exports": "^2.0.3",
"dependencies": {
"@babel/parser": "catalog:",
"@vue/compiler-core": "workspace:*",
"@vue/compiler-dom": "workspace:*",
"@vue/compiler-ssr": "workspace:*",
"@vue/shared": "workspace:*",
"estree-walker": "catalog:",
"magic-string": "catalog:",
"postcss": "^8.5.6",
"resolve.exports": "^2.0.3",
"source-map-js": "catalog:"
},
"devDependencies": {
"@babel/types": "catalog:",
"@vue/consolidate": "^1.0.0",
"hash-sum": "^2.0.0",
"lru-cache": "10.1.0",
"merge-source-map": "^1.1.0",
"minimatch": "~10.0.3",
"postcss-modules": "^6.0.1",
"postcss-selector-parser": "^7.1.0",
"pug": "^3.0.3",
"sass": "^1.90.0"
}
🤖 Prompt for AI Agents
In packages/compiler-sfc/package.json around line 65, the dependency
"resolve.exports": "^2.0.3" is currently listed as a devDependency but is
required at runtime (used in src/script/resolveType.ts); move the
"resolve.exports": "^2.0.3" entry from devDependencies to dependencies in
package.json and remove it from devDependencies, then run your package manager
(npm/yarn/pnpm) to update the lockfile so consumers get the runtime dependency.

Comment on lines +962 to +964
resolved =
resolveWithTS(scope.filename, source, ts, fs) ||
resolveWithNodeSubpathImports(source, fs)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix reference to renamed import.

Update the call site to use the correct import name after fixing the import statement.

       resolved =
         resolveWithTS(scope.filename, source, ts, fs) ||
-        resolveWithNodeSubpathImports(source, fs)
+        resolveWithNodeSubpathImports(source, fs)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/compiler-sfc/src/script/resolveType.ts around lines 962 to 964, the
call site still uses the old name resolveWithNodeSubpathImports; update this
invocation to match the renamed import (use the exact new exported identifier
from the top-of-file import), e.g. replace resolveWithNodeSubpathImports with
the corrected import name and ensure the function is called with the same
parameters (source, fs) as before so the fallback resolution chain remains:
resolveWithTS(...) || <correctRename>(source, fs).

Comment on lines +1129 to +1156
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) {}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for package.json imports field.

The function assumes that pkg.imports exists and that resolvedImports returns an array. Add validation to handle edge cases where the imports field might be missing or malformed.

 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)
+    if (!pkg.imports || typeof pkg.imports !== 'object') {
+      return
+    }
+    
-    const resolvedImports = imports(pkg, source)
+    const resolvedImports = imports(pkg, source)
     if (!resolvedImports || !resolvedImports.length) {
       return
     }

     const resolved = resolve(dirname(pkgPath), resolvedImports[0])

     return fs.realpath ? fs.realpath(resolved) : resolved
   } catch (e) {}
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/compiler-sfc/src/script/resolveType.ts around lines 1129 to 1156,
the code assumes pkg.imports exists and that resolveImports returns an array;
add defensive validation: after parsing package.json validate that pkg && typeof
pkg === 'object' and that pkg.imports exists and is an object (or of the
expected shape) before calling resolveImports; call resolveImports only if
imports look valid, then ensure its return is an array and has at least one
element before using resolvedImports[0]; if validations fail, return undefined
early; also avoid swallowing errors silently — keep the try/catch but return
undefined on error (or rethrow/log if preferred).

}

const pkg = JSON.parse(pkgStr)
const resolvedImports = resolveImports(pkg, source)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update to use correct import name from resolve.exports.

After fixing the import statement, update this line to use the correct function name.

-    const resolvedImports = resolveImports(pkg, source)
+    const resolvedImports = imports(pkg, source)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const resolvedImports = resolveImports(pkg, source)
const resolvedImports = imports(pkg, source)
🤖 Prompt for AI Agents
In packages/compiler-sfc/src/script/resolveType.ts around line 1147, the call
currently uses resolveImports(pkg, source) but the correct function name from
resolve.exports is resolveExports; change this invocation to resolveExports(pkg,
source) and ensure the file's import statement at the top imports resolveExports
(not resolveImports) from resolve.exports so the symbol matches.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

@vue/compiler-sfc unable to resolve external types in other vue components if using import map in package.json
1 participant