-
Notifications
You must be signed in to change notification settings - Fork 292
fix: standalone dependency resolution #765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,14 @@ | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
| import { builtinModules } from "node:module"; | ||
| import type { Plugin } from "vite"; | ||
|
|
||
| const BUILTIN_MODULES = new Set( | ||
| builtinModules.flatMap((name) => | ||
| name.startsWith("node:") ? [name, name.slice(5)] : [name, `node:${name}`], | ||
| ), | ||
| ); | ||
|
|
||
| /** | ||
| * Extract the npm package name from a bare module specifier. | ||
| * | ||
|
|
@@ -11,16 +18,23 @@ import type { Plugin } from "vite"; | |
| * - Node built-ins ("node:fs") | ||
| * - Package self-references ("#imports") | ||
| */ | ||
| function packageNameFromSpecifier(specifier: string): string | null { | ||
| export function packageNameFromSpecifier(specifier: string): string | null { | ||
| if ( | ||
| !specifier || | ||
| specifier.startsWith(".") || | ||
| specifier.startsWith("/") || | ||
| specifier.startsWith("node:") || | ||
| specifier.startsWith("\\") || | ||
| specifier.startsWith("#") | ||
| ) { | ||
| return null; | ||
| } | ||
|
|
||
| // External specifiers can include non-package schemes such as | ||
| // "virtual:vite-rsc" or "file:...". Those are never npm packages. | ||
| if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: this regex matches RFC 3986 scheme syntax, which is the right call. Worth noting it also matches Windows drive letters like One thing I noticed: the |
||
| return null; | ||
| } | ||
|
|
||
| if (specifier.startsWith("@")) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: scoped packages ( |
||
| const parts = specifier.split("/"); | ||
| if (parts.length >= 2) { | ||
|
|
@@ -29,7 +43,11 @@ function packageNameFromSpecifier(specifier: string): string | null { | |
| return null; | ||
| } | ||
|
|
||
| return specifier.split("/")[0] || null; | ||
| const packageName = specifier.split("/")[0] || null; | ||
| if (!packageName || BUILTIN_MODULES.has(specifier) || BUILTIN_MODULES.has(packageName)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking both |
||
| return null; | ||
| } | ||
| return packageName; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -95,6 +113,7 @@ export function createServerExternalsManifestPlugin(): Plugin { | |
| outDir = path.basename(dir) === "server" ? dir : path.dirname(dir); | ||
| } | ||
|
|
||
| const bundleFiles = new Set(Object.keys(bundle)); | ||
| for (const item of Object.values(bundle)) { | ||
| if (item.type !== "chunk") continue; | ||
| // In Rollup output, item.imports normally contains filenames of other | ||
|
|
@@ -104,6 +123,9 @@ export function createServerExternalsManifestPlugin(): Plugin { | |
| // filenames (relative/absolute paths) and extracts the package name from | ||
| // bare specifiers — which is exactly what the standalone BFS needs. | ||
| for (const specifier of [...item.imports, ...item.dynamicImports]) { | ||
| if (bundleFiles.has(specifier)) { | ||
| continue; | ||
| } | ||
| const pkg = packageNameFromSpecifier(specifier); | ||
| if (pkg) externals.add(pkg); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { describe, expect, it } from "vite-plus/test"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a few direct unit tests for |
||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { createServerExternalsManifestPlugin } from "../packages/vinext/src/plugins/server-externals-manifest.js"; | ||
|
|
||
| describe("createServerExternalsManifestPlugin", () => { | ||
| it("ignores bundle files, virtual modules, and node builtins when writing the manifest", () => { | ||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-externals-manifest-")); | ||
| const outDir = path.join(tmpDir, "dist", "server"); | ||
| fs.mkdirSync(outDir, { recursive: true }); | ||
|
|
||
| try { | ||
| const plugin = createServerExternalsManifestPlugin(); | ||
| const writeBundle = (plugin.writeBundle as { handler: Function }).handler; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the |
||
|
|
||
| writeBundle.call( | ||
| { environment: { name: "ssr" } }, | ||
| { dir: outDir }, | ||
| { | ||
| "index.js": { | ||
| type: "chunk", | ||
| imports: [ | ||
| "react", | ||
| "@scope/pkg/subpath", | ||
| "ipaddr.js", | ||
| "index.js", | ||
| "assets/chunk-abc.js", | ||
| "virtual:vite-rsc", | ||
| "crypto", | ||
| "fs/promises", | ||
| "node:path", | ||
| ], | ||
| dynamicImports: ["react-dom/server.edge"], | ||
| }, | ||
| "assets/chunk-abc.js": { | ||
| type: "chunk", | ||
| imports: [], | ||
| dynamicImports: [], | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| const manifest = JSON.parse( | ||
| fs.readFileSync(path.join(outDir, "vinext-externals.json"), "utf-8"), | ||
| ) as string[]; | ||
|
|
||
| expect(manifest.sort()).toEqual(["@scope/pkg", "ipaddr.js", "react", "react-dom"].sort()); | ||
| } finally { | ||
| fs.rmSync(tmpDir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good fallback strategy.
resolve.paths()is stable Node API and gives the same lookup directories thatrequire.resolve()would search. Thepkg.name === packageNameguard on line 87 prevents false positives from similarly-named directories that aren't the right package (e.g., a stale directory left over from a different version).