Repository navigation
fix: support no-expression template literals in computed member access - #20889
Conversation
`import.meta[`url`]` was falling through to runtime member access while `import.meta["url"]` was statically resolved. Both are statically analyzable strings — the only difference is the AST node type (TemplateLiteral vs Literal). In `extractMemberExpressionChain`, accept a TemplateLiteral with no expressions in addition to a Literal, pushing `quasis[0].value.cooked`. BinaryExpression and other arbitrary expressions are intentionally NOT evaluated here — concatenations like `obj["c" + "lass"]` are sometimes written specifically to opt out of webpack's static analysis (see `test/configCases/css/css-modules/use-style.js`), so evaluating them would change semantics. In `walkMemberExpressionWithExpressionName`, read the property string from the already-extracted `members` array. The previous code looked at `expression.property.name || expression.property.value`, which doesn't work for a TemplateLiteral node (it has neither field).
🦋 Changeset detectedLatest commit: 2995319 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is packaged and the instant preview is available (5bcf0e2). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@5bcf0e2
yarn add -D webpack@https://pkg.pr.new/webpack@5bcf0e2
pnpm add -D webpack@https://pkg.pr.new/webpack@5bcf0e2 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #20889 +/- ##
==========================================
+ Coverage 82.98% 91.33% +8.35%
==========================================
Files 530 562 +32
Lines 53890 55609 +1719
Branches 14236 14700 +464
==========================================
+ Hits 44721 50791 +6070
+ Misses 9169 4818 -4351
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Fixes webpack’s static member-chain analysis to treat no-expression template literals (e.g. obj[`prop`]) as statically known strings in computed member access, aligning behavior with quoted string literals (e.g. obj["prop"]).
Changes:
- Extend
extractMemberExpressionChainto acceptTemplateLiteralproperties with zero expressions and extract their cooked string value. - Update
walkMemberExpressionWithExpressionNameto derive the current property segment from the already-extractedmembersarray (supportsTemplateLiteralnodes). - Add a parsing config-case that asserts
import.meta["url"]andimport.meta[`url`]both resolve consistently.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
lib/javascript/JavascriptParser.js |
Enables static member-chain extraction for computed properties using no-expression template literals and fixes property derivation during member-walk optimization. |
test/configCases/parsing/import-meta-computed/index.js |
Adds regression tests covering computed access via both string literal and template literal on import.meta. |
test/configCases/parsing/import-meta-computed/webpack.config.js |
Adds the config-case harness for the new parsing test. |
.changeset/fuzzy-pigs-hide.md |
Adds a patch changeset describing the behavior change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Merging this PR will degrade performance by 53.95%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Memory | benchmark "react", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
232 KB | 163.8 KB | +41.67% |
| ⚡ | Memory | benchmark "many-chunks-commonjs", scenario '{"name":"mode-development","mode":"development"}' |
1,297.3 KB | 809.8 KB | +60.2% |
| ❌ | Memory | benchmark "devtool-eval-source-map", scenario '{"name":"mode-development","mode":"development"}' |
1.1 MB | 1.4 MB | -21.26% |
| ❌ | Memory | benchmark "devtool-eval", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
261.5 KB | 372.9 KB | -29.87% |
| ❌ | Memory | benchmark "json-modules", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
211.4 KB | 459.1 KB | -53.95% |
| ❌ | Memory | benchmark "asset-modules-inline", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
153.2 KB | 219.8 KB | -30.31% |
Comparing claude/fix-pr-20607-6FBRi (2995319) with main (45b2358)
The entry for #20889 padded a double-backtick code span with spaces, which `fmt:check` rejects, so `lint` is red on main and on every branch cut from it.
… speed up pack writes (#21843) * perf(serialization): dispatch cache items on their type `ObjectMiddleware`'s write loop is the hottest function webpack runs while storing the persistent cache. It opened with `Buffer.isBuffer(item)` and only reached numbers, booleans and `undefined` after four more failed comparisons — and counted over a 1,500-module build, those three are 1,066,959 of the 1,649,896 items it sees (64.6%), against 46,549 buffers (2.8%). Dispatch on `typeof` instead, so each item reaches its branch directly. The object branch also keys its type lookup on the serializer registration rather than on a `${request}/${name}` string, dropping another 254,319 string allocations per build. The wire format is unchanged: one registration is one `(request, name)` pair, so the same entries are emitted in the same order. Self time of that function drops from 296-321 ms to 201-230 ms over a 1,897-module fixture (`--cpu-prof`, two runs each, interleaved), and the cache store measured through `compiler.close()` from 1,179 ms to 1,056 ms (median of 6 interleaved runs). Emitted output is byte-identical. * fix: accept a snapshot.managedPaths RegExp without a capture group The schema documents a `snapshot.managedPaths` entry as "a RegExp matching a managed directory", but `FileSystemInfo` read the managed directory out of `match[1]`. A pattern written exactly as documented — with no capture group — therefore reached `getManagedItem(undefined, path)` and failed the build with `TypeError: Cannot read properties of undefined (reading 'length')` from inside the snapshot layer, naming nothing the user wrote. Fall back to the whole match, which is the directory the schema describes. * test: type the ObjectMiddleware round-trip context `ObjectMiddleware`'s serialize/deserialize take a context carrying a logger, and deserialize returns a value that may be a promise, so `{}` and a bare `[0]` failed `lint:types-test` on CI. * chore: shorten the comments the review flagged Both new comments ran past the two-line limit AGENTS.md sets for `lib/`, and one narrated the code it replaced. The changeset entry now fits the 80-character limit while still naming the option. * style: run prettier over CHANGELOG.md The entry for #20889 padded a double-backtick code span with spaces, which `fmt:check` rejects, so `lint` is red on main and on every branch cut from it. * test: pin the round-trip with strict equality `toEqual` ignores an undefined property and a swapped prototype, so the two cases the dispatch table exists for — `d: undefined` and the null-prototype object — would have passed even if the format dropped them. * test: use strict equality for the rest of the round-trip assertions The buffer case is the one that mattered: `toEqual` would have accepted a plain Uint8Array where the format restores a Buffer.
The released entry for #20889 fails `fmt:check` on prettier 3.9.6, which stops the lint chain before spellcheck on every pull request.
…d namespace import assignment (#21867) * test(test262): correct four recorded causes The two jest cases are not a drain-timing problem: the abandoned rejection reaches neither unhandledRejection nor uncaughtException while the test body runs, so the runner cannot capture it at all. The two deferred-namespace cases are not blocked on unknowable exports — the call site has the module graph. Sealing the proxy target needs those names emitted at each deferred import, which costs bytes. * test(test262): assert the rejections two cases abandon on purpose Both bundles pass every assertion; each drops a rejected promise by design, which jest records against the running test. The rejection is not visible through process events on the sandboxed process, but it is in jest's own state, which the environment already exposes. Assert the expected count and clear it the way rejectionHandled would, so a change in what they abandon fails instead of passing quietly. * test(test262): record what the two deferred-namespace cases really need Extensibility is only the first assertion of exotic-object-behavior; it also wants ownKeys to be the sorted exports plus @@toStringTag, so __esModule would have to go. That is the namespace-object trade-off, not defer-local. * test: unskip dynamic imports whose specifier names no module `import()` stringifies its argument, so `import(import(...))` asks for "[object Promise]" and `import(typeof x)` for "object". webpack builds and runs both correctly and the promise rejects, which is what the spec requires and what Node.js does unbundled — the only fallout is a rejection the test abandons and jest attributes to it. Assert those alongside the two already tracked, now by reason as well as count, and drop the twelve files from knownBugs. * fix: keep the options argument of a computed-request import() `import(`./${name}.js`, options)` goes through the context-dependency path, which replaced everything after the request with `)` and so dropped the options expression. The spec evaluates it right after the request, and it can have side effects, so keep its source in place as a second argument the context module ignores. Also run the test262 suite with `output.strictModuleErrorHandling`, which is what a Cyclic Module Record's [[EvaluationError]] asks for, and drop the three files the two changes fix. * fix: parenthesize the preserved import() options argument A comma expression spliced in bare splits into extra arguments the context module ignores; wrapping it keeps the call shape the source had. * fix: check the options of a computed-request import(), and keep a comma expression whole The context-dependency path passed the options as a second argument the context module ignores, which reads as if it took one and skipped the checks the spec asks for. Wrap the call the way the static-request path already does instead, with the request and the options as its two arguments so they are evaluated in that order, and share the check between the two paths. An options range excludes any parentheses the source had, so `import(req, (a, b))` split into arguments of its own and bound the check's parameter to `a`. That rejected valid code with a TypeError on the static path already. * fix: let exportsPresence decide how a circular reexport is reported The circular-reexport diagnostic was built off the export-presence path entirely, so it could be neither escalated nor suppressed: setting `reexportExportsPresence: "error"` still only warned, and setting it to `false" still warned. The spec makes a cycle in ResolveExport a link error, the same as the ambiguous star export next to it, which does honour the option. Keep the default as it is — a cycle warns, skipping the promotion to an error that AUTO gives a strict harmony module — but let an explicit setting say whether and how it is reported. * fix: await an async entry in an ESM bundle An entry using top-level await produces a promise the startup code kept for itself: the bundle settled before the entry did, and a rejecting entry left its rejection with no one to hand it to, so it surfaced as a process-level unhandled rejection instead of failing the bundle. Only `output.library.type: "module"` awaited it. Move that await into the generic startup, where it covers plain `output.module` too, and drop the library plugin's copy so it is still emitted once. * test(test262): record why the two production-only skips are not bugs Both fire only where the class or the export is unused, and development observes the same as the spec either way. * test(test262): run unscopables-ignored with the parsers off It only needs a bare global reference left alone, which is what the eval cases next to it already ask for. * fix: report an assignment to a namespace import `import * as ns from "./x"; ns = null;` silently succeeded and left every later read of `ns` looking at the assigned value. A real ES module throws a TypeError there, and webpack already keeps that throw for a named import by bailing out of concatenation so the write still lands on the namespace's getter-only property. A namespace import is the binding itself, and nothing webpack can emit for it throws: the declaration has to stay a `var` because a runtime-condition import must be function-scoped and HMR accept reassigns it. Report it at build time instead, which is what Rollup and esbuild do. * chore: fold the branch's changesets into one * chore: format a CHANGELOG entry prettier rejects The released entry for #20889 fails `fmt:check` on prettier 3.9.6, which stops the lint chain before spellcheck on every pull request. * fix: await every async entry, and keep options webpack cannot read Two defects a review found. The entry await was gated on the last entry and spliced into the startup buffer, so an earlier async entry still settled after the bundle did, and the `await` would have been a syntax error inside the synchronous startup function that `RuntimeGlobals.startup` renders. Capture each async entry instead and await them after the loop, only where the startup lands at the top level of an ESM chunk. `isStaticStringAttributes` returned true for a key that is not `with` or `assert`, so `import(request, { unused: effect() })` dropped `effect()`. Only those two keys can be read at build time; anything else keeps the argument. * test: cover the import() options checks The null, non-object `with` and non-string attribute branches had no test, on either the static or the computed-request path. * fix: observe every async entry together, and gate the cases on Node.js 14.8 Awaiting the entries one by one leaves the rest unobserved once one rejects, so a second failing entry became an unhandled rejection. Await them in a single `Promise.all`. The two cases need top-level await, which the runner's `vm.SourceTextModule` cannot parse before Node.js 14.8, and both fixtures now settle on a task so they fail when startup returns too early. * test: assert the TypeError, and check 'with' on a computed request The option assertions matched only the message, so a change to a plain Error would have passed, and only the static request exercised the 'with' check. * perf: await a lone async entry without Promise.all The code size report grew from 3 changed assets to 9 when the await became a `Promise.all`, which every ESM bundle with one async entry pays for. A single promise has nothing to orphan, so keep the short form for it. * feat(config): add output.environment.topLevelAwait Awaiting an async ESM entry emits top-level await, which a target can read ESM without supporting: `target: "node14"` (added in v14.8) then produced a bundle it cannot parse. Gate the await on a new environment flag, derived from the node/deno/bun/electron/nwjs/esX presets and the browserslist data, so such targets fall back to not awaiting. Also classify the flag in the ecma-conformance helper, which needed the walk to track function nesting to tell top-level await from nested await, and drop `globalThis` from the namespace-assignment fixture so it runs on Node 10. * test: read electron 10 back as ES2021 Chromium 85 has no top-level await, so the new environment flag makes the version ladder answer 2021. The case still shows `module: false` is ignored: without that, only the ES5 rung would match. * fix: keep awaiting an async entry a module library exports The topLevelAwait gate also suppressed the await `ModuleLibraryPlugin` has always emitted, so an async ESM library built for `target: "node14"` handed its consumers unsettled exports instead of values. Restore that plugin to what it was and let it own the await, gating only the startup await this branch adds. * test: drive the multi-entry await through Promise.all The case had one async entry among two, so it took the single-await path and nothing exercised the `Promise.all` the multiple-entry fix added. Make both entries async and assert both settled.
fixes #20607