diff --git a/src/rollup/config.ts b/src/rollup/config.ts index 3782f94d4b..61170d715a 100644 --- a/src/rollup/config.ts +++ b/src/rollup/config.ts @@ -25,6 +25,7 @@ import { replace } from "./plugins/replace"; import { virtual } from "./plugins/virtual"; import { dynamicRequire } from "./plugins/dynamic-require"; import { externals } from "./plugins/externals"; +import { externals as legacyExternals } from "./plugins/externals-legacy"; import { timing } from "./plugins/timing"; import { publicAssets } from "./plugins/public-assets"; import { serverAssets } from "./plugins/server-assets"; @@ -305,8 +306,11 @@ export const plugins = [ // Externals Plugin if (!nitro.options.noExternals) { + const externalsPlugin = nitro.options.experimental.legacyExternals + ? legacyExternals + : externals; rollupConfig.plugins.push( - externals( + externalsPlugin( defu(nitro.options.externals, { outDir: nitro.options.output.serverDir, moduleDirectories: nitro.options.nodeModulesDirs, diff --git a/src/rollup/plugins/externals-legacy.ts b/src/rollup/plugins/externals-legacy.ts new file mode 100644 index 0000000000..6b1c57e8dd --- /dev/null +++ b/src/rollup/plugins/externals-legacy.ts @@ -0,0 +1,338 @@ +import { existsSync, promises as fsp } from "node:fs"; +import { resolve, dirname, normalize, join, isAbsolute } from "pathe"; +import consola from "consola"; +import { nodeFileTrace, NodeFileTraceOptions } from "@vercel/nft"; +import type { Plugin } from "rollup"; +import { resolvePath, isValidNodeImport, normalizeid } from "mlly"; +import semver from "semver"; +import { isDirectory, retry } from "../../utils"; + +export interface NodeExternalsOptions { + inline?: string[]; + external?: string[]; + outDir?: string; + trace?: boolean; + traceOptions?: NodeFileTraceOptions; + moduleDirectories?: string[]; + exportConditions?: string[]; + traceInclude?: string[]; +} + +export function externals(opts: NodeExternalsOptions): Plugin { + const trackedExternals = new Set(); + + const _resolveCache = new Map(); + const _resolve = async (id: string) => { + let resolved = _resolveCache.get(id); + if (resolved) { + return resolved; + } + resolved = await resolvePath(id, { + conditions: opts.exportConditions, + url: opts.moduleDirectories, + }); + _resolveCache.set(id, resolved); + return resolved; + }; + + // Normalize options + opts.inline = (opts.inline || []).map((p) => normalize(p)); + opts.external = (opts.external || []).map((p) => normalize(p)); + + return { + name: "node-externals", + async resolveId(originalId, importer, options) { + // Skip internals + if ( + !originalId || + originalId.startsWith("\u0000") || + originalId.includes("?") || + originalId.startsWith("#") + ) { + return null; + } + + // Skip relative paths + if (originalId.startsWith(".")) { + return null; + } + + // Normalize path (windows) + const id = normalize(originalId); + + // Id without .../node_modules/ + const idWithoutNodeModules = id.split("node_modules/").pop(); + + // Check for explicit inlines + if ( + opts.inline.some( + (i) => id.startsWith(i) || idWithoutNodeModules.startsWith(i) + ) + ) { + return null; + } + + // Check for explicit externals + if ( + opts.external.some( + (i) => id.startsWith(i) || idWithoutNodeModules.startsWith(i) + ) + ) { + return { id, external: true }; + } + + // Resolve id using rollup resolver + const resolved = (await this.resolve(originalId, importer, { + ...options, + skipSelf: true, + })) || { id }; + + // Try resolving with mlly as fallback + if ( + !isAbsolute(resolved.id) || + !existsSync(resolved.id) || + (await isDirectory(resolved.id)) + ) { + resolved.id = await _resolve(resolved.id).catch(() => resolved.id); + } + + // Inline invalid node imports + if (!(await isValidNodeImport(resolved.id).catch(() => false))) { + return null; + } + + // Externalize with full path if trace is disabled + if (opts.trace === false) { + return { + ...resolved, + id: isAbsolute(resolved.id) ? normalizeid(resolved.id) : resolved.id, + external: true, + }; + } + + // -- Trace externals -- + + // Try to extract package name from path + const { pkgName, subpath } = parseNodeModulePath(resolved.id); + + // Inline if cannot detect package name + if (!pkgName) { + return null; + } + + // Normally package name should be same as originalId + // Edge cases: Subpath export and full paths + if (pkgName !== originalId) { + // Subpath export + if (!isAbsolute(originalId)) { + const fullPath = await _resolve(originalId); + trackedExternals.add(fullPath); + return { + id: originalId, + external: true, + }; + } + + // Absolute path, we are not sure about subpath to generate import statement + // Guess as main subpath export + const packageEntry = await _resolve(pkgName).catch(() => null); + if (packageEntry !== originalId) { + // Guess subpathexport + const guessedSubpath = pkgName + subpath.replace(/\.[a-z]+$/, ""); + const resolvedGuess = await _resolve(guessedSubpath).catch( + () => null + ); + if (resolvedGuess === originalId) { + trackedExternals.add(resolvedGuess); + return { + id: guessedSubpath, + external: true, + }; + } + // Inline since we cannot guess subpath + return null; + } + } + + trackedExternals.add(resolved.id); + return { + id: pkgName, + external: true, + }; + }, + async buildEnd() { + if (opts.trace === false) { + return; + } + + // Force trace paths + for (const pkgName of opts.traceInclude || []) { + const path = await this.resolve(pkgName); + if (path?.id) { + trackedExternals.add(path.id.replace(/\?.+/, "")); + } + } + + // Trace files + let tracedFiles = await nodeFileTrace( + [...trackedExternals], + opts.traceOptions + ) + .then((r) => + [...r.fileList].map((f) => resolve(opts.traceOptions.base, f)) + ) + .then((r) => r.filter((file) => file.includes("node_modules"))); + + // Resolve symlinks + tracedFiles = await Promise.all( + tracedFiles.map((file) => fsp.realpath(file)) + ); + + // Read package.json with cache + const packageJSONCache = new Map(); // pkgDir => contents + const getPackageJson = async (pkgDir: string) => { + if (packageJSONCache.has(pkgDir)) { + return packageJSONCache.get(pkgDir); + } + const pkgJSON = JSON.parse( + await fsp.readFile(resolve(pkgDir, "package.json"), "utf8") + ); + packageJSONCache.set(pkgDir, pkgJSON); + return pkgJSON; + }; + + // Keep track of npm packages + const tracedPackages = new Map(); // name => pkgDir + const ignoreDirs = []; + const ignoreWarns = new Set(); + for (const file of tracedFiles) { + const { baseDir, pkgName } = parseNodeModulePath(file); + if (!pkgName) { + continue; + } + let pkgDir = resolve(baseDir, pkgName); + + // Check for duplicate versions + const existingPkgDir = tracedPackages.get(pkgName); + if (existingPkgDir && existingPkgDir !== pkgDir) { + const v1 = await getPackageJson(existingPkgDir).then( + (r) => r.version + ); + const v2 = await getPackageJson(pkgDir).then((r) => r.version); + const isNewer = semver.gt(v2, v1); + + // Warn about major version differences + const getMajor = (v: string) => v.split(".").find((s) => s !== "0"); + if (getMajor(v1) !== getMajor(v2)) { + const warn = + `Multiple major versions of package \`${pkgName}\` are being externalized. Picking latest version:\n\n` + + [ + ` ${isNewer ? "-" : "+"} ` + existingPkgDir + "@" + v1, + ` ${isNewer ? "+" : "-"} ` + pkgDir + "@" + v2, + ].join("\n"); + if (!ignoreWarns.has(warn)) { + consola.warn(warn); + ignoreWarns.add(warn); + } + } + + const [newerDir, olderDir] = isNewer + ? [pkgDir, existingPkgDir] + : [existingPkgDir, pkgDir]; + // Try to map traced files from one package to another for minor/patch versions + if (getMajor(v1) === getMajor(v2)) { + tracedFiles = tracedFiles.map((f) => + f.startsWith(olderDir + "/") ? f.replace(olderDir, newerDir) : f + ); + } + // Exclude older version files + ignoreDirs.push(olderDir + "/"); + pkgDir = newerDir; // Update for tracedPackages + } + + // Add to traced packages + tracedPackages.set(pkgName, pkgDir); + } + + // Filter out files from ignored packages and dedup + tracedFiles = tracedFiles.filter( + (f) => !ignoreDirs.some((d) => f.startsWith(d)) + ); + tracedFiles = [...new Set(tracedFiles)]; + + // Ensure all package.json files are traced + for (const pkgDir of tracedPackages.values()) { + const pkgJSON = join(pkgDir, "package.json"); + if (!tracedFiles.includes(pkgJSON)) { + tracedFiles.push(pkgJSON); + } + } + + const writeFile = async (file: string) => { + if (!(await isFile(file))) { + return; + } + const src = resolve(opts.traceOptions.base, file); + const { pkgName, subpath } = parseNodeModulePath(file); + const dst = resolve(opts.outDir, `node_modules/${pkgName + subpath}`); + await fsp.mkdir(dirname(dst), { recursive: true }); + try { + await fsp.copyFile(src, dst); + } catch { + consola.warn(`Could not resolve \`${src}\`. Skipping.`); + } + }; + + // Write traced files + await Promise.all( + tracedFiles.map((file) => retry(() => writeFile(file), 3)) + ); + + // Write an informative package.json + await fsp.writeFile( + resolve(opts.outDir, "package.json"), + JSON.stringify( + { + name: "nitro-output", + version: "0.0.0", + private: true, + bundledDependencies: [...tracedPackages.keys()], + }, + null, + 2 + ), + "utf8" + ); + }, + }; +} + +function parseNodeModulePath(path: string) { + if (!path) { + return {}; + } + const match = /^(.+\/node_modules\/)([^/@]+|@[^/]+\/[^/]+)(\/?.*?)?$/.exec( + normalize(path) + ); + if (!match) { + return {}; + } + const [, baseDir, pkgName, subpath] = match; + return { + baseDir, + pkgName, + subpath, + }; +} + +async function isFile(file: string) { + try { + const stat = await fsp.stat(file); + return stat.isFile(); + } catch (err) { + if (err.code === "ENOENT") { + return false; + } + throw err; + } +} diff --git a/src/rollup/plugins/externals.ts b/src/rollup/plugins/externals.ts index 6b1c57e8dd..38f60de5a3 100644 --- a/src/rollup/plugins/externals.ts +++ b/src/rollup/plugins/externals.ts @@ -1,10 +1,8 @@ import { existsSync, promises as fsp } from "node:fs"; import { resolve, dirname, normalize, join, isAbsolute } from "pathe"; -import consola from "consola"; import { nodeFileTrace, NodeFileTraceOptions } from "@vercel/nft"; import type { Plugin } from "rollup"; import { resolvePath, isValidNodeImport, normalizeid } from "mlly"; -import semver from "semver"; import { isDirectory, retry } from "../../utils"; export interface NodeExternalsOptions { @@ -165,7 +163,7 @@ export function externals(opts: NodeExternalsOptions): Plugin { return; } - // Force trace paths + // Manually traced paths for (const pkgName of opts.traceInclude || []) { const path = await this.resolve(pkgName); if (path?.id) { @@ -173,19 +171,10 @@ export function externals(opts: NodeExternalsOptions): Plugin { } } - // Trace files - let tracedFiles = await nodeFileTrace( + // Trace used files using nft + const _fileTrace = await nodeFileTrace( [...trackedExternals], opts.traceOptions - ) - .then((r) => - [...r.fileList].map((f) => resolve(opts.traceOptions.base, f)) - ) - .then((r) => r.filter((file) => file.includes("node_modules"))); - - // Resolve symlinks - tracedFiles = await Promise.all( - tracedFiles.map((file) => fsp.realpath(file)) ); // Read package.json with cache @@ -201,94 +190,199 @@ export function externals(opts: NodeExternalsOptions): Plugin { return pkgJSON; }; - // Keep track of npm packages - const tracedPackages = new Map(); // name => pkgDir - const ignoreDirs = []; - const ignoreWarns = new Set(); - for (const file of tracedFiles) { - const { baseDir, pkgName } = parseNodeModulePath(file); - if (!pkgName) { - continue; - } - let pkgDir = resolve(baseDir, pkgName); + // Resolve traced files + type TracedFile = { + path: string; + subpath: string; + parents: string[]; - // Check for duplicate versions - const existingPkgDir = tracedPackages.get(pkgName); - if (existingPkgDir && existingPkgDir !== pkgDir) { - const v1 = await getPackageJson(existingPkgDir).then( - (r) => r.version - ); - const v2 = await getPackageJson(pkgDir).then((r) => r.version); - const isNewer = semver.gt(v2, v1); - - // Warn about major version differences - const getMajor = (v: string) => v.split(".").find((s) => s !== "0"); - if (getMajor(v1) !== getMajor(v2)) { - const warn = - `Multiple major versions of package \`${pkgName}\` are being externalized. Picking latest version:\n\n` + - [ - ` ${isNewer ? "-" : "+"} ` + existingPkgDir + "@" + v1, - ` ${isNewer ? "+" : "-"} ` + pkgDir + "@" + v2, - ].join("\n"); - if (!ignoreWarns.has(warn)) { - consola.warn(warn); - ignoreWarns.add(warn); + pkgPath: string; + pkgName: string; + pkgVersion: string; + }; + const _resolveTracedPath = (p) => + fsp.realpath(resolve(opts.traceOptions.base, p)); + const tracedFiles: Record = Object.fromEntries( + await Promise.all( + [..._fileTrace.reasons.entries()].map(async ([_path, reasons]) => { + if (reasons.ignored) { + return; } - } - - const [newerDir, olderDir] = isNewer - ? [pkgDir, existingPkgDir] - : [existingPkgDir, pkgDir]; - // Try to map traced files from one package to another for minor/patch versions - if (getMajor(v1) === getMajor(v2)) { - tracedFiles = tracedFiles.map((f) => - f.startsWith(olderDir + "/") ? f.replace(olderDir, newerDir) : f + const path = await _resolveTracedPath(_path); + if (!path.includes("node_modules")) { + return; + } + if (!(await isFile(path))) { + return; + } + const { baseDir, pkgName, subpath } = parseNodeModulePath(path); + const pkgPath = join(baseDir, pkgName); + const parents = await Promise.all( + [...reasons.parents].map((p) => _resolveTracedPath(p)) ); - } - // Exclude older version files - ignoreDirs.push(olderDir + "/"); - pkgDir = newerDir; // Update for tracedPackages - } + const tracedFile = { + path, + parents, - // Add to traced packages - tracedPackages.set(pkgName, pkgDir); - } + subpath, + pkgName, + pkgPath, + }; + return [path, tracedFile]; + }) + ).then((r) => r.filter(Boolean)) + ); - // Filter out files from ignored packages and dedup - tracedFiles = tracedFiles.filter( - (f) => !ignoreDirs.some((d) => f.startsWith(d)) + // Resolve traced packages + type TracedPackage = { + name: string; + versions: Record< + string, + { + path: string; + files: string[]; + } + >; + }; + const tracedPackages: Record = {}; + await Promise.all( + Object.values(tracedFiles).map(async (tracedFile) => { + const pkgJSON = await getPackageJson(tracedFile.pkgPath); + const pkgName = tracedFile.pkgName; // Use file path as name to support aliases + let tracedPackage = tracedPackages[pkgName]; + if (!tracedPackage) { + tracedPackage = { + name: pkgName, + versions: {}, + }; + tracedPackages[pkgName] = tracedPackage; + } + let tracedPackageVersion = tracedPackage.versions[pkgJSON.version]; + if (!tracedPackageVersion) { + tracedPackageVersion = { path: tracedFile.pkgPath, files: [] }; + tracedPackage.versions[pkgJSON.version] = tracedPackageVersion; + } + tracedPackageVersion.files.push(tracedFile.path); + tracedFile.pkgName = pkgName; + tracedFile.pkgVersion = pkgJSON.version; + }) ); - tracedFiles = [...new Set(tracedFiles)]; - // Ensure all package.json files are traced - for (const pkgDir of tracedPackages.values()) { - const pkgJSON = join(pkgDir, "package.json"); - if (!tracedFiles.includes(pkgJSON)) { - tracedFiles.push(pkgJSON); + const writePackage = async ( + name: string, + version: string, + outputName?: string + ) => { + // Find pkg + const pkg = tracedPackages[name]; + + // Copy files + for (const src of pkg.versions[version].files) { + const { subpath } = parseNodeModulePath(src); + const dst = join( + opts.outDir, + "node_modules", + outputName || pkg.name, + subpath + ); + await fsp.mkdir(dirname(dst), { recursive: true }); + await fsp.copyFile(src, dst); } - } - const writeFile = async (file: string) => { - if (!(await isFile(file))) { - return; + // Copy package.json + const pkgJSONPath = join( + opts.outDir, + "node_modules", + outputName || pkg.name, + "package.json" + ); + await fsp.mkdir(dirname(pkgJSONPath), { recursive: true }); + await fsp.copyFile( + join(pkg.versions[version].path, "package.json"), + pkgJSONPath + ); + }; + + 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)) { + return; // TODO: Warn? } - const src = resolve(opts.traceOptions.base, file); - const { pkgName, subpath } = parseNodeModulePath(file); - const dst = resolve(opts.outDir, `node_modules/${pkgName + subpath}`); await fsp.mkdir(dirname(dst), { recursive: true }); - try { - await fsp.copyFile(src, dst); - } catch { - consola.warn(`Could not resolve \`${src}\`. Skipping.`); - } + // TODO: Use copy for windows for portable output? + await fsp.symlink(src, dst, "junction").catch((err) => { + console.error("Cannot link", src, "to", dst, ":", err.message); + }); }; - // Write traced files + // Utility to find package parents + const findPackageParents = (pkg: TracedPackage, version: string) => { + // Try to find parent packages + const versionFiles: TracedFile[] = pkg.versions[version].files.map( + (path) => tracedFiles[path] + ); + const parentPkgs = [ + ...new Set( + versionFiles.flatMap((file) => + file.parents.flatMap( + (parentPath) => + tracedFiles[parentPath].pkgName + + "@" + + tracedFiles[parentPath].pkgVersion + ) + ) + ), + ]; + return parentPkgs; + }; + + // Write traced packages await Promise.all( - tracedFiles.map((file) => retry(() => writeFile(file), 3)) + Object.values(tracedPackages).map(async (tracedPackage) => { + const versions = Object.keys(tracedPackage.versions); // TODO: sort by semver + 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, + `${tracedPackage.name}@${version}` + ); + // For each parent, link into node_modules/{parent}/node_modules/{name} + for (const parentPath of parentPkgs) { + await linkPackage( + `${tracedPackage.name}@${version}`, + `${parentPath}/node_modules/${tracedPackage.name}` + ); + await linkPackage( + `${tracedPackage.name}@${version}`, + `${parentPath.split("@")[0]}/node_modules/${ + tracedPackage.name + }` + ); + } + } + } + } + }) ); // 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( @@ -296,7 +390,7 @@ export function externals(opts: NodeExternalsOptions): Plugin { name: "nitro-output", version: "0.0.0", private: true, - bundledDependencies: [...tracedPackages.keys()], + bundledDependencies, }, null, 2 diff --git a/src/types/nitro.ts b/src/types/nitro.ts index 1fdfc1b80b..70f27e1861 100644 --- a/src/types/nitro.ts +++ b/src/types/nitro.ts @@ -174,6 +174,7 @@ export interface NitroOptions extends PresetOptions { noPublicDir: boolean; experimental?: { wasm?: boolean | RollupWasmOptions; + legacyExternals?: boolean; }; serverAssets: ServerAssetDir[]; publicAssets: PublicAssetDir[]; diff --git a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/index.mjs b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/index.mjs index 3d64381bce..0029f235ab 100644 --- a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/index.mjs +++ b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/index.mjs @@ -1 +1,3 @@ -export default '1.0.0'; +import nestedLib from 'nested-lib' + +export default 'nitro-lib@1.0.0+' + nestedLib diff --git a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/index.mjs b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/index.mjs new file mode 100644 index 0000000000..da8927e1e8 --- /dev/null +++ b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/index.mjs @@ -0,0 +1 @@ +export default 'nested-lib@1.0.0' diff --git a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/package.json b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/package.json new file mode 100644 index 0000000000..5993dae833 --- /dev/null +++ b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/node_modules/nested-lib/package.json @@ -0,0 +1,5 @@ +{ + "name": "nested-lib", + "version": "1.0.0", + "exports": "./index.mjs" +} diff --git a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/package.json b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/package.json index dd674789ba..b8bd61c01d 100644 --- a/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/package.json +++ b/test/fixture/_/node_modules/nitro-dep-a/node_modules/nitro-lib/package.json @@ -1,5 +1,8 @@ { "name": "nitro-lib", "version": "1.0.0", - "exports": "./index.mjs" + "exports": "./index.mjs", + "dependencies": { + "nested-lib": "1.0.0" + } } diff --git a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/index.mjs b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/index.mjs index c9e08004a5..53aaa0be76 100644 --- a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/index.mjs +++ b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/index.mjs @@ -1 +1,3 @@ -export default '2.0.1'; +import nestedLib from 'nested-lib' + +export default 'nitro-lib@2.0.1+' + nestedLib diff --git a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/index.mjs b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/index.mjs new file mode 100644 index 0000000000..caace8b77b --- /dev/null +++ b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/index.mjs @@ -0,0 +1 @@ +export default 'nested-lib@2.0.1' diff --git a/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/package.json b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/package.json new file mode 100644 index 0000000000..fbc00e77b8 --- /dev/null +++ b/test/fixture/_/node_modules/nitro-dep-b/node_modules/nitro-lib/node_modules/nested-lib/package.json @@ -0,0 +1,5 @@ +{ + "name": "nested-lib", + "version": "2.0.1", + "exports": "./index.mjs" +} 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 bfb99596eb..3396d6ee1d 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 @@ -4,5 +4,8 @@ "exports": { ".": "./index.mjs", "./subpath": "./subpath.mjs" + }, + "dependencies": { + "nested-lib": "2.0.0" } } diff --git a/test/fixture/_/node_modules/nitro-dep-b/package.json b/test/fixture/_/node_modules/nitro-dep-b/package.json index 7e30c8dd8f..d7235353b7 100644 --- a/test/fixture/_/node_modules/nitro-dep-b/package.json +++ b/test/fixture/_/node_modules/nitro-dep-b/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "exports": "./index.mjs", "dependencies": { - "nitro-lib": "2.0.0" + "nitro-lib": "2.0.1" } } diff --git a/test/fixture/_/node_modules/nitro-lib/index.mjs b/test/fixture/_/node_modules/nitro-lib/index.mjs index ff603d7009..5e07a96ab0 100644 --- a/test/fixture/_/node_modules/nitro-lib/index.mjs +++ b/test/fixture/_/node_modules/nitro-lib/index.mjs @@ -1 +1,3 @@ -export default '2.0.0'; +import nestedLib from 'nested-lib' + +export default 'nitro-lib@2.0.0+' + nestedLib diff --git a/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/index.mjs b/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/index.mjs new file mode 100644 index 0000000000..69cb6433e5 --- /dev/null +++ b/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/index.mjs @@ -0,0 +1 @@ +export default 'nested-lib@2.0.0' diff --git a/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/package.json b/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/package.json new file mode 100644 index 0000000000..e8d4a98a0a --- /dev/null +++ b/test/fixture/_/node_modules/nitro-lib/node_modules/nested-lib/package.json @@ -0,0 +1,5 @@ +{ + "name": "nested-lib", + "version": "2.0.0", + "exports": "./index.mjs" +} diff --git a/test/fixture/_/node_modules/nitro-lib/package.json b/test/fixture/_/node_modules/nitro-lib/package.json index 81618a9dc9..3709569af3 100644 --- a/test/fixture/_/node_modules/nitro-lib/package.json +++ b/test/fixture/_/node_modules/nitro-lib/package.json @@ -1,5 +1,5 @@ { - "name": "nitro-lib", + "name": "nitro-lib-aliased-from-another-name", "version": "2.0.0", "exports": { ".": "./index.mjs", diff --git a/test/fixture/_/node_modules/nitro-lib/subpath.mjs b/test/fixture/_/node_modules/nitro-lib/subpath.mjs index ff603d7009..c4ac86cb68 100644 --- a/test/fixture/_/node_modules/nitro-lib/subpath.mjs +++ b/test/fixture/_/node_modules/nitro-lib/subpath.mjs @@ -1 +1 @@ -export default '2.0.0'; +export default 'nitro-lib@2.0.0'; diff --git a/test/tests.ts b/test/tests.ts index 960f41e18d..aa3ffcf361 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -228,18 +228,15 @@ export function testNitro( expect(status).toBe(404); }); - // TODO: Enable test after https://github.com/unjs/nitro/pull/782 - if (!isWindows) { - it("resolve module version conflicts", async () => { - const { data } = await callHandler({ url: "/modules" }); - expect(data).toMatchObject({ - depA: "2.0.1", - depB: "2.0.1", - depLib: "2.0.1", - subpathLib: "2.0.1", - }); + it("resolve module version conflicts", async () => { + const { data } = await callHandler({ url: "/modules" }); + expect(data).toMatchObject({ + depA: "nitro-lib@1.0.0+nested-lib@1.0.0", + depB: "nitro-lib@2.0.1+nested-lib@2.0.1", + depLib: "nitro-lib@2.0.0+nested-lib@2.0.0", + subpathLib: "nitro-lib@2.0.0", }); - } + }); if (additionalTests) { additionalTests(ctx, callHandler);