When a variable holding a statically-known asset path (from require.resolve) is read as a shorthand object property, the relocation overwrites the identifier — which is simultaneously the property's key and value — with the asset expression, producing a bare expression inside an object literal. The result is syntactically invalid and webpack fails with Module parse failed: Unexpected token.
Claude's understanding of the problem & a suggested fix
Mechanism
In src/asset-relocator.js (as of fa39a77):
- The
require.resolve('./tool.js') assignment registers toolPath as a known absolute-path binding.
- At the shorthand property read,
isIdentifierRead returns true via its Property case (line ~44: return node === parent.value) — in a shorthand property, acorn makes the key and value the same node, so the check can't distinguish this from a plain value read.
emitStaticChildAsset then runs magicString.overwrite(staticChildNode.start, staticChildNode.end, inlineString) (line ~1318), replacing the identifier — key included — with the bare asset expression.
Possible fixes
Two contained options:
-
In isIdentifierRead, exclude shorthand properties:
case 'Property':
return node === parent.value && !parent.shorthand;
I tested this variant against the loader source: the minimal repro above compiles and runs correctly in both branches (the default branch resolves to the relocated asset because the require.resolve call is still rewritten at the assignment site, and the asset is still emitted), and require("@npmcli/run-script") (v10.0.4) goes from the parse error to compiling and loading successfully, with node-gyp.js emitted. I have not run the repo's unit-test fixtures against it.
-
Alternatively, in emitStaticChildAsset, when staticChildNode is the value of a shorthand Property, emit name: <inlineString> instead of the bare expression (preserves the read-site inlining).
Repro files
other.js:
#!/usr/bin/env node
console.log("gyp stand-in");
index.js:
let toolPath
if (process.env.TOOL_PATH) {
toolPath = process.env.TOOL_PATH
} else {
toolPath = require.resolve('./tool.js')
}
console.log({
name: 'demo',
toolPath,
})
webpack.config.js:
module.exports = {
mode: 'production',
target: 'node',
entry: './index.js',
output: { libraryTarget: 'commonjs2' },
module: {
rules: [
{
test: /\.(m?js|node)$/,
parser: { amd: false },
use: { loader: '@vercel/webpack-asset-relocator-loader' },
},
],
},
};
package.json
{
"name": "relocator-shorthand-repro",
"private": true,
"devDependencies": {
"@vercel/webpack-asset-relocator-loader": "1.10.0",
"webpack": "5.108.1",
"webpack-cli": "^7.1.0"
},
"dependencies": {
"@npmcli/run-script": "10.0.4"
}
}
Build with @vercel/webpack-asset-relocator-loader@1.10.0 and webpack 5.108.1:
$ npm install && npx webpack
ERROR in ./index.js 10:21
Module parse failed: Unexpected token (10:21)
File was processed with these loaders:
* ./node_modules/@vercel/webpack-asset-relocator-loader/src/asset-relocator.js
You may need an additional loader to handle the result of these loaders.
| console.log({
| name: 'demo',
> __webpack_require__.ab + "tool.js",
| })
Also reproduced through ncc 0.44.1 (which pins this loader at 1.7.3 as its relocate-loader), so the bug spans at least 1.7.3 → 1.10.0 → current main.
Control: changing the shorthand toolPath, to toolPath: toolPath, builds successfully and emits the expected
console.log({
name: 'demo',
- toolPath,
+ toolPath: toolPath,
})
toolPath: __webpack_require__.ab + "tool.js",
with tool.js relocated next to the bundle and the program running correctly.
Impact
This was run down by Claude when trying to figure out why pulumi/actions#1460 couldn't merge as is.
This is the root cause of the long-standing inability to bundle @npmcli/run-script v9+/pacote v11+/@npmcli/arborist v8+ with ncc (vercel/ncc#986, vercel/ncc#1162 — the latter was closed as unfixable in ncc, but the defect is in this loader and looks contained).
When a variable holding a statically-known asset path (from
require.resolve) is read as a shorthand object property, the relocation overwrites the identifier — which is simultaneously the property's key and value — with the asset expression, producing a bare expression inside an object literal. The result is syntactically invalid and webpack fails withModule parse failed: Unexpected token.Claude's understanding of the problem & a suggested fix
Mechanism
In
src/asset-relocator.js(as of fa39a77):require.resolve('./tool.js')assignment registerstoolPathas a known absolute-path binding.isIdentifierReadreturnstruevia itsPropertycase (line ~44:return node === parent.value) — in a shorthand property, acorn makes the key and value the same node, so the check can't distinguish this from a plain value read.emitStaticChildAssetthen runsmagicString.overwrite(staticChildNode.start, staticChildNode.end, inlineString)(line ~1318), replacing the identifier — key included — with the bare asset expression.Possible fixes
Two contained options:
In
isIdentifierRead, exclude shorthand properties:I tested this variant against the loader source: the minimal repro above compiles and runs correctly in both branches (the default branch resolves to the relocated asset because the
require.resolvecall is still rewritten at the assignment site, and the asset is still emitted), andrequire("@npmcli/run-script")(v10.0.4) goes from the parse error to compiling and loading successfully, withnode-gyp.jsemitted. I have not run the repo's unit-test fixtures against it.Alternatively, in
emitStaticChildAsset, whenstaticChildNodeis the value of a shorthandProperty, emitname: <inlineString>instead of the bare expression (preserves the read-site inlining).Repro files
other.js:index.js:webpack.config.js:package.json{ "name": "relocator-shorthand-repro", "private": true, "devDependencies": { "@vercel/webpack-asset-relocator-loader": "1.10.0", "webpack": "5.108.1", "webpack-cli": "^7.1.0" }, "dependencies": { "@npmcli/run-script": "10.0.4" } }Build with
@vercel/webpack-asset-relocator-loader@1.10.0and webpack 5.108.1:Also reproduced through ncc 0.44.1 (which pins this loader at 1.7.3 as its relocate-loader), so the bug spans at least 1.7.3 → 1.10.0 → current
main.Control: changing the shorthand
toolPath,totoolPath: toolPath,builds successfully and emits the expectedconsole.log({ name: 'demo', - toolPath, + toolPath: toolPath, })with
tool.jsrelocated next to the bundle and the program running correctly.Impact
This was run down by Claude when trying to figure out why pulumi/actions#1460 couldn't merge as is.
This is the root cause of the long-standing inability to bundle
@npmcli/run-scriptv9+/pacotev11+/@npmcli/arboristv8+ with ncc (vercel/ncc#986, vercel/ncc#1162 — the latter was closed as unfixable in ncc, but the defect is in this loader and looks contained).