Skip to content
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

fix(externals): use stable dependency tree #909

Merged
merged 2 commits into from
Feb 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 74 additions & 54 deletions src/rollup/plugins/externals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 });
Expand All @@ -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);
});
};

Expand Down Expand Up @@ -363,59 +366,78 @@ export function externals(opts: NodeExternalsOptions): Plugin {
return parentPkgs;
};

// Write traced packages
// Analyze dependency tree
const multiVersionPkgs: Record<string, { [version: string]: string[] }> =
{};
const singleVersionPackages: string[] = [];
for (const tracedPackage of Object.values(tracedPackages)) {
const versions = Object.keys(tracedPackage.versions);
if (versions.length === 1) {
singleVersionPackages.push(tracedPackage.name);
continue;
}
multiVersionPkgs[tracedPackage.name] = {};
for (const version of versions) {
multiVersionPkgs[tracedPackage.name][version] = findPackageParents(
tracedPackage,
version
);
}
}

// Directly write single version packages
await Promise.all(
Object.values(tracedPackages).map(async (tracedPackage) => {
// TODO: Sort versions
// const versions = sortVersions(Object.keys(tracedPackage.versions));
const versions = Object.keys(tracedPackage.versions);
if (versions.length === 1) {
// Write the only version into node_modules/{name}
await writePackage(tracedPackage.name, versions[0]);
} else {
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);
} else {
// Write alternative version into node_modules/{name}@{version}
await writePackage(
tracedPackage.name,
version,
`.nitro/${tracedPackage.name}@${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
}`
);
}
}
}
}
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/${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 an informative package.json
const bundledDependencies = Object.fromEntries(
Object.values(tracedPackages).map((pkg) => [
pkg.name,
Object.keys(pkg.versions).join(" | "),
])
);

await fsp.writeFile(
resolve(opts.outDir, "package.json"),
JSON.stringify(
Expand All @@ -434,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) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/fixture/_/node_modules/nitro-dep-b/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/fixture/_/node_modules/nitro-lib/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 16 additions & 4 deletions test/fixture/routes/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
});