diff --git a/src/build/plugins/externals.ts b/src/build/plugins/externals.ts index ec56829cc7..9d4903cc23 100644 --- a/src/build/plugins/externals.ts +++ b/src/build/plugins/externals.ts @@ -157,6 +157,7 @@ export function externals(opts: ExternalsOptions): Plugin { await traceNodeModules([...tracedPaths], { ...traceOpts, fullTraceInclude: resolved?.fullTraceInclude, + traceInclude: resolved?.traceInclude, conditions: opts.conditions, rootDir: opts.rootDir, writePackageJson: true, // deno compat @@ -227,11 +228,18 @@ export function resolveTraceDeps( const fullTraceInclude = [...new Set([...builtinFullTrace, ...userFullTrace])].filter( (d) => !negated.has(d) ); + // Named (non-RegExp) deps to force-trace by name. nft cannot statically detect + // packages that are loaded dynamically (e.g. native bindings), so they are + // resolved and traced explicitly by `traceNodeModules`. This also makes them + // work under pnpm, where a nested dependency is not hoisted and only resolves + // from the dependent package's real `.pnpm` location. + const traceInclude = resolved.filter((d): d is string => typeof d === "string"); return { includePattern: tracePattern ? new RegExp(`(?:^|[/\\\\]node_modules[/\\\\])(?:${tracePattern})(?:[/\\\\]|$)`) : undefined, fullTraceInclude: fullTraceInclude.length > 0 ? fullTraceInclude : undefined, + traceInclude: traceInclude.length > 0 ? traceInclude : undefined, }; } diff --git a/test/unit/trace-deps.test.ts b/test/unit/trace-deps.test.ts index dd79b5937f..71b551208d 100644 --- a/test/unit/trace-deps.test.ts +++ b/test/unit/trace-deps.test.ts @@ -40,6 +40,26 @@ describe("resolveTraceDeps", () => { expect(result.fullTraceInclude).toContain("prisma"); }); + it("returns named deps as traceInclude (builtins + user, RegExp excluded)", () => { + const result = resolveTraceDeps(["my-pkg", /my-.*-pkg/], defaults); + expect(result.traceInclude).toContain("sharp"); + expect(result.traceInclude).toContain("canvas"); + expect(result.traceInclude).toContain("my-pkg"); + // RegExp entries cannot be resolved by name and must be excluded + expect(result.traceInclude!.every((d) => typeof d === "string")).toBe(true); + }); + + it("excludes negated packages from traceInclude", () => { + const result = resolveTraceDeps(["!sharp"], defaults); + expect(result.traceInclude).not.toContain("sharp"); + expect(result.traceInclude).toContain("canvas"); + }); + + it("returns undefined traceInclude when all deps are negated", () => { + const result = resolveTraceDeps(["!sharp", "!canvas"], defaults); + expect(result.traceInclude).toBeUndefined(); + }); + it("throws on bare ! selector", () => { expect(() => resolveTraceDeps(["!"], defaults)).toThrow('Invalid traceDeps selector: "!"'); });