diff --git a/src/rollup/plugins/externals.ts b/src/rollup/plugins/externals.ts index b119932591..b7a42d8d90 100644 --- a/src/rollup/plugins/externals.ts +++ b/src/rollup/plugins/externals.ts @@ -5,7 +5,7 @@ import type { PackageJson } from "pkg-types"; import { nodeFileTrace, NodeFileTraceOptions } from "@vercel/nft"; import type { Plugin } from "rollup"; import { resolvePath, isValidNodeImport, normalizeid } from "mlly"; -// import semver from "semver"; +import semver from "semver"; import { isDirectory } from "../../utils"; export interface NodeExternalsOptions { @@ -324,7 +324,10 @@ export function externals(opts: NodeExternalsOptions): Plugin { const linkPackage = async (from: string, to: string) => { const src = join(opts.outDir, "node_modules", from); const dst = join(opts.outDir, "node_modules", to); - if (existsSync(dst)) { + const dstStat = await fsp.lstat(dst).catch(() => null); + const exists = dstStat && dstStat.isSymbolicLink(); + // console.log("Linking", from, "to", to, exists ? "!!!!" : ""); + if (exists) { return; } await fsp.mkdir(dirname(dst), { recursive: true }); @@ -335,7 +338,7 @@ export function externals(opts: NodeExternalsOptions): Plugin { isWindows ? "junction" : "dir" ) .catch((err) => { - console.error("Cannot link", src, "to", dst, ":", err.message); + console.error("Cannot link", from, "to", to, err); }); }; @@ -363,52 +366,68 @@ export function externals(opts: NodeExternalsOptions): Plugin { return parentPkgs; }; - const writeTracedPackage = async (tracedPackage: TracedPackage) => { + // Analyze dependency tree + const multiVersionPkgs: Record = + {}; + const singleVersionPackages: string[] = []; + for (const tracedPackage of Object.values(tracedPackages)) { const versions = Object.keys(tracedPackage.versions); if (versions.length === 1) { - // Write the only version into node_modules/{name} - await writePackage(tracedPackage.name, versions[0]); - return; + singleVersionPackages.push(tracedPackage.name); + continue; } + multiVersionPkgs[tracedPackage.name] = {}; for (const version of versions) { - const parentPkgs = findPackageParents(tracedPackage, version); - if (parentPkgs.length === 0) { - // No parent packages, assume as the hoisted version - await writePackage(tracedPackage.name, version); - continue; - } - // Write alternative version into node_modules/{name}@{version} - await writePackage( - tracedPackage.name, - version, - `.nitro/${tracedPackage.name}@${version}` + multiVersionPkgs[tracedPackage.name][version] = findPackageParents( + tracedPackage, + version ); + } + } + + // Directly write single version packages + await Promise.all( + singleVersionPackages.map((pkgName) => { + const pkg = tracedPackages[pkgName]; + const version = Object.keys(pkg.versions)[0]; + return writePackage(pkgName, version); + }) + ); + + // Write packages with multiple versions + for (const [pkgName, pkgVersions] of Object.entries(multiVersionPkgs)) { + const versionEntires = Object.entries(pkgVersions).sort( + ([v1, p1], [v2, p2]) => { + // 1. Packege with no parent packages to be hoisted + if (p1.length === 0) { + return -1; + } + if (p2.length === 0) { + return 1; + } + // 2. Newest version to be hoisted + return compareVersions(v1, v2); + } + ); + for (const [version, parentPkgs] of versionEntires) { + // Write each version into node_modules/.nitro/{name}@{version} + await writePackage(pkgName, version, `.nitro/${pkgName}@${version}`); // Link one version to the top level (for indirect bundle deps) - await linkPackage( - `.nitro/${tracedPackage.name}@${version}`, - `${tracedPackage.name}` - ); - // For each parent, link into node_modules/{parent}/node_modules/{name} - for (const parentPath of parentPkgs) { - await linkPackage( - `.nitro/${tracedPackage.name}@${version}`, - `.nitro/${parentPath}/node_modules/${tracedPackage.name}` - ); - await linkPackage( - `.nitro/${tracedPackage.name}@${version}`, - `${parentPath.split("@")[0]}/node_modules/${tracedPackage.name}` - ); + await linkPackage(`.nitro/${pkgName}@${version}`, `${pkgName}`); + // Link to parent packages + for (const parentPkg of parentPkgs) { + const parentPkgName = parentPkg.replace(/@[^@]+$/, ""); + await (multiVersionPkgs[parentPkgName] + ? linkPackage( + `.nitro/${pkgName}@${version}`, + `.nitro/${parentPkg}/node_modules/${pkgName}` + ) + : linkPackage( + `.nitro/${pkgName}@${version}`, + `${parentPkgName}/node_modules/${pkgName}` + )); } } - }; - // Write traced packages - // await Promise.all( - // Object.values(tracedPackages).map(async (tracedPackage) => { - // await writeTracedPackage(tracedPackage); - // }) - // ); - for (const tracedPackage of Object.values(tracedPackages)) { - await writeTracedPackage(tracedPackage); } // Write an informative package.json @@ -418,6 +437,7 @@ export function externals(opts: NodeExternalsOptions): Plugin { Object.keys(pkg.versions).join(" | "), ]) ); + await fsp.writeFile( resolve(opts.outDir, "package.json"), JSON.stringify( @@ -436,15 +456,13 @@ export function externals(opts: NodeExternalsOptions): Plugin { }; } -// function sortVersions(versions: string[]) { -// return versions.sort((v1 = "0.0.0", v2 = "0.0.0") => { -// try { -// return semver.lt(v1, v2, { loose: true }) ? 1 : -1; -// } catch { -// return v1.localeCompare(v2); -// } -// }); -// } +function compareVersions(v1 = "0.0.0", v2 = "0.0.0") { + try { + return semver.lt(v1, v2, { loose: true }) ? 1 : -1; + } catch { + return v1.localeCompare(v2); + } +} function parseNodeModulePath(path: string) { if (!path) { diff --git a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/package.json b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/package.json index 3396d6ee1d..7eb0cbba5d 100644 --- a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/package.json +++ b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/package.json @@ -6,6 +6,6 @@ "./subpath": "./subpath.mjs" }, "dependencies": { - "nested-lib": "2.0.0" + "nested-lib": "2.0.1" } } diff --git a/test/fixture/_/node_modules/nitro-dep-b/package.json b/test/fixture/_/node_modules/nitro-dep-b/package.json index d7235353b7..4154a93930 100644 --- a/test/fixture/_/node_modules/nitro-dep-b/package.json +++ b/test/fixture/_/node_modules/nitro-dep-b/package.json @@ -1,6 +1,6 @@ { "name": "nitro-dep-b", - "version": "1.0.0", + "version": "2.0.1", "exports": "./index.mjs", "dependencies": { "nitro-lib": "2.0.1" diff --git a/test/fixture/_/node_modules/nitro-lib/package.json b/test/fixture/_/node_modules/nitro-lib/package.json index ac01a7ab9d..9eeab28277 100644 --- a/test/fixture/_/node_modules/nitro-lib/package.json +++ b/test/fixture/_/node_modules/nitro-lib/package.json @@ -6,6 +6,6 @@ "./subpath": "./subpath.mjs" }, "dependencies": { - "nested-dep": "2.0.0" + "nested-lib": "2.0.0" } } diff --git a/test/fixture/routes/modules.ts b/test/fixture/routes/modules.ts index 6fda0684de..37aae1bd6a 100644 --- a/test/fixture/routes/modules.ts +++ b/test/fixture/routes/modules.ts @@ -7,11 +7,23 @@ import depLib from "nitro-lib"; // @ts-ignore import subpathLib from "nitro-lib/subpath"; +/* +Structure in fixture/_/node_modules: +| nitrodep-a (1.0.0) +| nitro-lib (1.0.0) +| nested-lib (1.0.0) +| nitrodep-b (2.0.1) +| nitro-lib (2.0.1) +| nested-lib (2.0.1) +| nitro-lib (2.0.0) +| nested-lib (2.0.0) +*/ + export default defineEventHandler(() => { return { - depA, - depB, - depLib, - subpathLib, + depA, // expected to all be 1.0.0 + depB, // expected to all be 2.0.1 + depLib, // expected to all be 2.0.0 + subpathLib, // expected to 2.0.0 }; });