Skip to content

fix: resolve scoped npm packages and warn on missing deps#88

Open
uriva wants to merge 1 commit intodenoland:mainfrom
uriva:fix/npm-specifier-resolution
Open

fix: resolve scoped npm packages and warn on missing deps#88
uriva wants to merge 1 commit intodenoland:mainfrom
uriva:fix/npm-specifier-resolution

Conversation

@uriva
Copy link
Copy Markdown

@uriva uriva commented Mar 4, 2026

Summary

Fixes two bugs in prefixPlugin.ts when resolving npm: specifiers. Closes #40.

Bug 1: Scoped package names parsed incorrectly

deno info --json returns npmPackage identifiers like:

  • Unscoped: preact@10.25.4
  • Scoped: @preact/signals@2.8.1_preact@10.25.4

The existing code uses indexOf("@") to find the version separator. For scoped packages starting with @, this returns 0, so slice(0, 0) produces an empty string "". The fix finds the @ after the / in scoped package names.

Bug 2: Silent externalization on resolution failure

When this.resolve(packageName) fails (package not in node_modules), the old code returned the bare package name as a string. Vite interpreted this as an external module and silently externalized it — the error only appeared at runtime in the browser as:

Module "xyz" has been externalized for browser compatibility

Now the plugin emits a warning with actionable guidance and returns undefined, allowing Vite to report the unresolved import at build time instead.

Why packages might not be in node_modules

This commonly happens when an npm: specifier comes from a JSR package. Deno resolves JSR transitive npm dependencies through its own module graph, but they don't get installed into the project's node_modules/. The warning tells users to add the package to their import map or package.json.

Tests

  • Added extractPackageName unit tests covering unscoped, scoped, scoped with peer dep suffixes, and versionless package identifiers
  • Added integration test for inline npm:@preact/signals (scoped package)
  • All 17 tests pass (11 existing + 6 new)

Two bugs in prefixPlugin's npm: specifier handling:

1. Scoped package names (e.g. @preact/signals@2.8.1_preact@10.25.4)
   were parsed incorrectly: indexOf('@') returns 0 for '@preact/...',
   resulting in an empty package name. Fixed by finding the version
   separator '@' after the scope's '/'.

2. When this.resolve() fails (package not in node_modules), the bare
   package name was returned as-is, causing Vite to silently
   externalize it. The error only surfaced at runtime in the browser.
   Now emits a warning and returns undefined so Vite can report the
   unresolved import at build time.

Closes denoland#40
@uriva
Copy link
Copy Markdown
Author

uriva commented Mar 4, 2026

Note: the lint-and-format check is failing, but this is a pre-existing failure on main. The no-import-prefix and no-unversioned-import rules flag all the inline test fixtures (inlineNpm.ts, inlineJsr.ts, inlineHttp.ts, etc.) which intentionally use inline specifiers since that's what the plugin resolves. My new inlineScopedNpm.ts follows the same pattern.

This PR fixes #40.

@uriva
Copy link
Copy Markdown
Author

uriva commented Mar 9, 2026

@marvinhagemeister could you ptal?

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.

npm prefix imports externalized in vite.

1 participant