1.3.2 package-boundary guard silently drops cwd-anchored config files referenced by node_modules packages
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 1.7k
- Forks
- 185
- PR merge metrics
- No merged PRs in 30d
Description
[!NOTE]
Used AI to replicate the issue, and generate this ticket.
Summary
Since 1.3.2 (#568), a package inside node_modules can no longer emit an asset that lives outside node_modules, even when the path is inside job.base and was resolved from process.cwd().
This silently drops user-project config files that packages look up by convention, e.g. path.resolve('./some-tool.config.js'). It took our production app down for several hours with no build-time signal.
I want to check whether the in-base case is intended before proposing anything — the tests added in #568 suggest the package boundary is deliberate, but the PR title and description both describe the goal as "outside job base", which is a wider allowance than the code implements.
Reproduction
#!/usr/bin/env bash
set -e
DIR=$(mktemp -d); cd "$DIR"
echo '{"name":"host","version":"1.0.0"}' > package.json
echo 'module.exports = { hello: true }' > target.js
cat > check.js <<'EOF2'
const { nodeFileTrace } = require('@vercel/nft');
nodeFileTrace([process.cwd() + '/node_modules/fakepkg/index.js'], { base: process.cwd() })
.then((r) => {
const traced = [...r.fileList].some((f) => f.includes('target.js'));
console.log(` target.js: ${traced ? 'TRACED' : 'NOT TRACED'}`);
});
EOF2
for v in 1.3.1 1.3.2; do
npm i --silent --no-audit --no-fund "@vercel/nft@$v" > /dev/null 2>&1
# recreate after install, since npm prunes extraneous packages
mkdir -p node_modules/fakepkg
echo '{"name":"fakepkg","version":"1.0.0","main":"index.js"}' > node_modules/fakepkg/package.json
cat > node_modules/fakepkg/index.js <<'EOF2'
const path = require('path');
const p = path.resolve('./target.js');
// Babel's CJS output for `await import(p)`
module.exports = (function (specifier) {
return new Promise(function (r) { return r("".concat(specifier)); })
.then(function (s) { return require(s); });
})(p);
EOF2
echo "@vercel/nft@$v"
node check.js
done
@vercel/nft@1.3.1
target.js: TRACED
@vercel/nft@1.3.2
target.js: NOT TRACED
target.js is inside the job base, and cwd equals the base.
Mechanism
The guard added in src/analyze.ts:
if (pkgBase) {
const nodeModulesBase =
id.substring(0, id.indexOf(path.sep + 'node_modules')) + path.sep + 'node_modules' + path.sep;
if (!assetPath.startsWith(nodeModulesBase)) {
// skipped
return;
}
}
When the analysed file is inside node_modules, any asset outside <project>/node_modules/ is skipped — including files inside job.base.
This only affects paths that reach emitAssetPath. It's why the shape matters: require(p) is emitted as a dependency and is unaffected, while Babel's transpiled await import(p) is not recognised as a require, so the computed path is treated as an asset and hits the guard. Measured on the same file with the same base:
| Shape | 1.3.1 | 1.3.2 |
|---|---|---|
import(p) — native |
TRACED | TRACED |
Babel new Promise(r => r("".concat(p))).then(s => require(s)) |
TRACED | NOT TRACED |
new Promise(r => r(require(p))) |
TRACED | TRACED |
require("".concat(p)) |
TRACED | TRACED |
require(p) |
TRACED | TRACED |
Real-world impact
next-i18next's CommonJS build loads its user config this way:
// next-i18next/dist/commonjs/serverSideTranslations.js
const configPath = path.resolve('./next-i18next.config.js');
if (!userConfig && fs.existsSync(configPath)) {
userConfig = await import(configPath); // → Babel Promise/require shape
}
if (userConfig === null) {
throw new Error(`next-i18next was unable to find a user config at ${configPath}`);
}
Next.js 16.3.0 bundles a post-1.3.2 nft (16.2.10 bundled an earlier one). After that upgrade next-i18next.config.js stopped being traced, so on Vercel fs.existsSync('/var/task/next-i18next.config.js') was false and the throw fired before any page rendered — every server-rendered response 500'd, including the on-demand 404, while prerendered pages were unaffected.
Measured against the real next-i18next entry point (base = project root, cwd = project root):
| nft | files traced | next-i18next.config.js |
|---|---|---|
| 1.3.1 | 308 | traced |
| 1.3.2 | 297 | not traced |
The 11 dropped files were exactly next-i18next.config.js, the root package.json, and the nine locale JSONs reachable through that import.
Looking up a config file from process.cwd() is a widespread convention (i18n, CSS, lint and test tooling all do it), so this likely reaches beyond next-i18next.
Questions
- Is skipping in-base assets intended, or is the guard broader than #568 needed? The SvelteKit case that motivated it (stringified env vars pointing at Node/Yarn system paths) is outside
job.base, which the second half of that change already handles viajob.ignoreFn(path.relative(job.base, assetPath)). - Regardless of (1) — could the skip be surfaced in
warningsrather than only logged underjob.log? Right now the file is dropped silently, so the first symptom is a 500 in production. A warning would have made this a build-time signal. Happy to open a PR for that.
Workaround
Declare the files explicitly. For Next.js:
outputFileTracingIncludes: {
'/**': ['./next-i18next.config.js', './public/locales/**'],
},
Environment
@vercel/nft1.3.1 vs 1.3.2 (standalone), and as bundled innext16.2.10 vs 16.3.0next-i18next15.4.3- Node.js v22.21.1, macOS
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the guard in src/analyze.ts and the tests added for #568, then run the supplied reproduction against nft 1.3.1 and 1.3.2. Determine the intended treatment of in-base assets and whether skipped assets should appear in warnings; done means the decision is covered by regression tests for the reported import shape.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100