From 73439d35004c905adb26ea21d1c1424269c60ee0 Mon Sep 17 00:00:00 2001 From: Puneet Arora Date: Tue, 11 Aug 2026 16:29:38 -0700 Subject: [PATCH] fix: warn when an asset is skipped at the package boundary The package-boundary guard added in #568 stops a package inside node_modules from emitting assets outside node_modules. That is deliberate, but the skip is currently only visible under `job.log`, so in a normal build the file is simply absent from the output and the first symptom is a runtime failure in the deployed application. Surface it in `warnings` as well, so consumers can see and act on it at build time. Behaviour is unchanged: the asset is still not emitted. This cost us a multi-hour production outage (see #606). next-i18next resolves its user config from the current working directory: const configPath = path.resolve('./next-i18next.config.js'); if (!userConfig && fs.existsSync(configPath)) { userConfig = await import(configPath); } Babel compiles that dynamic import to a Promise wrapper around `require`, which is analysed as an asset rather than a dependency, so the path goes through emitAssetPath and is skipped. The config never reached the serverless bundle, `existsSync` returned false, and next-i18next threw before any page rendered -- with nothing in the build output to indicate why. The new fixture covers the case the existing *-outside-base fixtures do not: their `/../../secret.txt` normalises to a path that does not exist, so it returns at the stat check before reaching this guard. Tests: 1375 passed (1373 before, plus the two variants of the new fixture). --- src/analyze.ts | 21 +++++++++++-------- test/unit.test.js | 15 ++++++++++++- .../pkg-cwd-asset-outside-pkg-base/.gitignore | 1 + .../pkg-cwd-asset-outside-pkg-base/input.js | 1 + .../node_modules/some-pkg/index.js | 13 ++++++++++++ .../node_modules/some-pkg/package.json | 5 +++++ .../pkg-cwd-asset-outside-pkg-base/output.js | 6 ++++++ .../some.config.js | 1 + 8 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 test/unit/pkg-cwd-asset-outside-pkg-base/.gitignore create mode 100644 test/unit/pkg-cwd-asset-outside-pkg-base/input.js create mode 100644 test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/index.js create mode 100644 test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/package.json create mode 100644 test/unit/pkg-cwd-asset-outside-pkg-base/output.js create mode 100644 test/unit/pkg-cwd-asset-outside-pkg-base/some.config.js diff --git a/src/analyze.ts b/src/analyze.ts index a4ba9862..27dd6042 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -1299,15 +1299,18 @@ export default async function analyze( 'node_modules' + path.sep; if (!assetPath.startsWith(nodeModulesBase)) { - if (job.log) - console.log( - 'Skipping asset emission of ' + - assetPath + - ' for ' + - id + - ' as it is outside the package base ' + - pkgBase, - ); + const message = + 'Skipping asset emission of ' + + assetPath + + ' for ' + + id + + ' as it is outside the package base ' + + pkgBase; + // Also surfaced as a warning, not only under `job.log`: the file is otherwise + // simply absent from the output, so the first symptom is a runtime failure in + // the deployed application rather than anything visible at build time. + job.warnings.add(new Error(message)); + if (job.log) console.log(message); return; } } diff --git a/test/unit.test.js b/test/unit.test.js index 662ae82b..0fb1f1b1 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -176,7 +176,7 @@ for (const { testName, isRoot } of unitTests) { // Ignore. } - const { fileList, reasons } = await nodeFileTrace( + const { fileList, reasons, warnings } = await nodeFileTrace( inputFileNames.map((file) => join(unitPath, file)), { conditions: testOpts.conditions, @@ -216,6 +216,19 @@ for (const { testName, isRoot } of unitTests) { const getReasonType = (f) => reasons.get(normalizeInputRoot(f)).type; + // Only on the uncached pass: the second run replays cached analysis and does not + // re-emit warnings. + if (!cached && testName === 'pkg-cwd-asset-outside-pkg-base') { + // The asset stays out of the output, but the skip has to be reported: silently + // omitting a file the package reads at runtime only surfaces once the deployed + // application fails. + expect( + [...warnings].some((warning) => + warning.message.startsWith('Skipping asset emission of'), + ), + ).toBe(true); + } + if (testName === 'multi-input') { const collectFiles = (parent, files = new Set()) => { fileList.forEach((file) => { diff --git a/test/unit/pkg-cwd-asset-outside-pkg-base/.gitignore b/test/unit/pkg-cwd-asset-outside-pkg-base/.gitignore new file mode 100644 index 00000000..cf4bab9d --- /dev/null +++ b/test/unit/pkg-cwd-asset-outside-pkg-base/.gitignore @@ -0,0 +1 @@ +!node_modules diff --git a/test/unit/pkg-cwd-asset-outside-pkg-base/input.js b/test/unit/pkg-cwd-asset-outside-pkg-base/input.js new file mode 100644 index 00000000..878bb5e0 --- /dev/null +++ b/test/unit/pkg-cwd-asset-outside-pkg-base/input.js @@ -0,0 +1 @@ +require('some-pkg'); diff --git a/test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/index.js b/test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/index.js new file mode 100644 index 00000000..0172cc3d --- /dev/null +++ b/test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/index.js @@ -0,0 +1,13 @@ +const path = require('path'); + +// Babel's CommonJS output for `await import(configPath)`, as shipped by packages that +// look up a user config relative to the current working directory. +const configPath = path.resolve('./some.config.js'); + +module.exports = (function (specifier) { + return new Promise(function (r) { + return r(''.concat(specifier)); + }).then(function (s) { + return require(s); + }); +})(configPath); diff --git a/test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/package.json b/test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/package.json new file mode 100644 index 00000000..d234a264 --- /dev/null +++ b/test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/package.json @@ -0,0 +1,5 @@ +{ + "name": "some-pkg", + "version": "1.0.0", + "main": "index.js" +} diff --git a/test/unit/pkg-cwd-asset-outside-pkg-base/output.js b/test/unit/pkg-cwd-asset-outside-pkg-base/output.js new file mode 100644 index 00000000..73355bb5 --- /dev/null +++ b/test/unit/pkg-cwd-asset-outside-pkg-base/output.js @@ -0,0 +1,6 @@ +[ + "package.json", + "test/unit/pkg-cwd-asset-outside-pkg-base/input.js", + "test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/index.js", + "test/unit/pkg-cwd-asset-outside-pkg-base/node_modules/some-pkg/package.json" +] diff --git a/test/unit/pkg-cwd-asset-outside-pkg-base/some.config.js b/test/unit/pkg-cwd-asset-outside-pkg-base/some.config.js new file mode 100644 index 00000000..3731a5bb --- /dev/null +++ b/test/unit/pkg-cwd-asset-outside-pkg-base/some.config.js @@ -0,0 +1 @@ +module.exports = { config: true };