feat: add output.environment.let option and emit let/const in generated code#21010
Conversation
🦋 Changeset detectedLatest commit: 4d1c287 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 (e42372c). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@e42372c
yarn add -D webpack@https://pkg.pr.new/webpack@e42372c
pnpm add -D webpack@https://pkg.pr.new/webpack@e42372c |
| * @returns {"let" | "var"} return `let` when it is supported, otherwise `var` | ||
| */ | ||
| renderLet() { | ||
| return this.supportsLet() ? "let" : "var"; |
| const err = `Cannot find module '${request}'`; | ||
| return `var e = new Error(${JSON.stringify( | ||
| return `${this.renderConst()} e = new Error(${JSON.stringify( | ||
| err | ||
| )}); e.code = 'MODULE_NOT_FOUND'; throw e;`; |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new output.environment.let capability (separate from output.environment.const) and updates webpack’s generated runtime/bootstrap code to prefer const or let over var when safe, improving output modernity while preserving var where function-scoping is required.
Changes:
- Add
output.environment.letto options schema/types/defaults/targets (including browserslist handling). - Extend
RuntimeTemplatewithsupportsLet()/renderLet()and makerenderConst()fall back toletbeforevar. - Update many runtime modules and snapshots to emit
const/letdeclarations where appropriate, plus add a new config case forenvironment-let.
Reviewed changes
Copilot reviewed 66 out of 69 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| types.d.ts | Updates public typings for Environment.let and new RuntimeTemplate APIs/return types. |
| declarations/WebpackOptions.d.ts | Adds Environment.let to generated option declarations. |
| schemas/WebpackOptions.json | Adds schema support for output.environment.let. |
| lib/config/defaults.js | Computes default environment.let (tied to targets/environment.const). |
| lib/config/target.js | Adds let to target properties (but JSDoc text for const is now misleading). |
| lib/config/browserslistTargetHandler.js | Adds browserslist feature detection for let. |
| lib/RuntimeTemplate.js | Adds supportsLet() / renderLet(); renderConst() now falls back to let; uses renderLet() in destructuring helpers. |
| lib/javascript/JavascriptModulesPlugin.js | Switches bootstrap/runtime variable declarations to const/let where safe; keeps let for __webpack_exports__ due to reassignment. |
| lib/web/JsonpChunkLoadingRuntimeModule.js | Uses renderConst()/renderLet() for installed-chunks and loader internals. |
| lib/esm/ModuleChunkLoadingRuntimeModule.js | Uses renderConst()/renderLet() for ESM chunk loading runtime declarations. |
| lib/node/RequireChunkLoadingRuntimeModule.js | Uses renderConst() in node chunk loading runtime. |
| lib/runtime/LoadScriptRuntimeModule.js | Uses renderConst()/renderLet() for runtime script-loader internals. |
| lib/runtime/OnChunksLoadedRuntimeModule.js | Uses renderConst()/renderLet() for deferred queue bookkeeping. |
| lib/runtime/StartupEntrypointRuntimeModule.js | Uses renderConst() for local temporaries. |
| lib/runtime/StartupChunkDependenciesRuntimeModule.js | Uses renderConst() for next binding. |
| lib/runtime/RelativeUrlRuntimeModule.js | Uses renderConst() for locals in helper runtime. |
| lib/runtime/GetTrustedTypesPolicyRuntimeModule.js | Uses renderLet() for mutable policy binding. |
| lib/runtime/CreateFakeNamespaceObjectRuntimeModule.js | Uses renderConst()/renderLet() for helper locals. |
| lib/runtime/CompatGetDefaultExportRuntimeModule.js | Uses renderConst() for getter binding. |
| lib/runtime/AutoPublicPathRuntimeModule.js | Uses renderConst()/renderLet(); security rationale comment appears truncated. |
| lib/runtime/AsyncModuleRuntimeModule.js | Uses renderConst()/renderLet() broadly for async module runtime locals. |
| lib/runtime/MakeDeferredNamespaceObjectRuntime.js | Uses renderConst()/renderLet() for deferred-namespace runtime locals. |
| lib/sharing/ShareRuntimeModule.js | Uses renderConst()/renderLet() for sharing runtime locals. |
| lib/sharing/ConsumeSharedRuntimeModule.js | Uses renderConst() for many helper bindings and tables. |
| lib/container/RemoteRuntimeModule.js | Uses renderConst()/renderLet() for remote loading runtime locals. |
| lib/container/FallbackModule.js | Uses renderConst()/renderLet() for fallback loop locals. |
| lib/container/ContainerEntryModule.js | Uses renderConst() for container runtime bindings (moduleMap, get, init). |
| lib/library/AssignLibraryPlugin.js | Uses renderConst() for export-target locals in generated wrappers. |
| lib/hmr/LazyCompilationPlugin.js | Uses renderConst()/renderLet() for proxy module locals. |
| lib/ExternalModule.js | Uses renderConst() for small helper bindings in external module wrappers and script externals. |
| lib/css/CssLoadingRuntimeModule.js | Uses renderConst()/renderLet() for CSS chunk runtime locals. |
| lib/css/CssInjectStyleRuntimeModule.js | Uses renderConst() for style injection runtime locals. |
| lib/ContextModule.js | Threads runtimeTemplate into more codegen paths to emit const/var appropriately. |
| lib/optimize/ConcatenatedModule.js | Documents why certain external bindings must remain var. |
| test/Defaults.unittest.js | Updates defaults snapshots to include environment.let. |
| test/configCases/output/environment-let/webpack.config.js | New test matrix covering const+let / let-only / neither. |
| test/configCases/output/environment-let/index.js | New assertions that generated bootstrap uses const/let/var as expected. |
| test/configCases/sharing/consume-shared-stable-runtime/test.config.js | Loosens regex to accept `var |
| test/configCases/library/module-useless-export-requirement/entry1.js | Accepts `var |
| test/configCases/library/module-useless-export-requirement/entry2.js | Accepts `var |
| test/configCases/library/module-useless-export-requirement/entry3.js | Accepts `var |
| test/configCases/library/render-order-issue/snapshots/ConfigTest.snap | Snapshot updated for const module cache and let exports binding. |
| test/configCases/library/modern-module-named-import-externals/snapshots/ConfigTest.snap | Snapshot updated to use const in remapping helpers and module cache. |
| test/configCases/library/1-use-library/snapshots/errors.snap | New snapshot now expecting a redeclaration error (likely regression). |
| test/configCases/html/script-src/index.js | Updates assertion to accept `var |
| test/configCases/html/script-src/snapshots/ConfigTest.snap | Snapshot updated to use const require scope/cache and let exports binding. |
| test/configCases/html/script-src-mixed/snapshots/ConfigTest.snap | Snapshot updated similarly for mixed script cases. |
| test/configCases/html/script-src-classic/snapshots/ConfigTest.snap | Snapshot updated similarly for classic script cases. |
| test/configCases/html/modulepreload-esm/snapshots/ConfigTest.snap | Snapshot updated similarly. |
| test/configCases/html/inline-script-classic/snapshots/ConfigTest.snap | Snapshot updated similarly. |
| test/configCases/ecmaVersion/browserslist/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/ecmaVersion/browserslist-query/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/ecmaVersion/browserslist-query-with-config-file/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/ecmaVersion/browserslist-extends/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/ecmaVersion/browserslist-env/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/ecmaVersion/browserslist-config/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/ecmaVersion/browserslist-config-extends/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/ecmaVersion/browserslist-config-env/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/ecmaVersion/browserslist-config-env-extends/webpack.config.js | Adds environment.let to expected resolved environment. |
| test/configCases/chunks-order/cjs/snapshots/ConfigTest.snap | Snapshot updates one binding to const. |
| test/configCases/css/public-path/snapshots/ConfigTest.snap | Snapshot updates asset hashes (likely due to output changes). |
| test/configCases/css/css-auto/snapshots/errors.snap | New snapshot now accepting less-loader compilation errors (likely unintended). |
| test/configCases/css/css-loader/snapshots/errors.snap | New snapshot now accepting compilation errors (likely unintended). |
| test/configCases/css/css-loader/snapshots/warnings.snap | New warning snapshot for css-loader case. |
| test/configCases/css/local-ident-name/snapshots/errors.snap | New snapshot now accepting less-loader compilation errors (likely unintended). |
| test/configCases/css/prefer-relative-css-import/snapshots/errors.snap | New snapshot now accepting less-loader compilation errors (likely unintended). |
| test/configCases/css/source-map-export-types/snapshots/errors.snap | New snapshot now accepting less-loader compilation errors (likely unintended). |
| .changeset/output-environment-let.md | Adds a changeset entry documenting the new option and codegen behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -60,6 +60,7 @@ const getDefaultTarget = (context) => { | |||
| * @property {boolean | null} globalThis has globalThis variable available | |||
| * @property {boolean | null} bigIntLiteral big int literal syntax is available | |||
| * @property {boolean | null} const const and let variable declarations are available | |||
| Array [ | ||
| Object { | ||
| "message": "Identifier 'fs__WEBPACK_IMPORTED_MODULE_0__' has already been declared (151:27)", | ||
| }, | ||
| ] |
| Object { | ||
| "message": "Module build failed (from ../../../../node_modules/less-loader/dist/cjs.js): | ||
| NonErrorEmittedError: (Emitted value instead of an instance of Error) TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG]: A dynamic import callback was invoked without --experimental-vm-modules", | ||
| "moduleName": "css ./global.less", |
| Object { | ||
| "compilerPath": "link", | ||
| "message": "Module build failed (from ../../../../node_modules/less-loader/dist/cjs.js): | ||
| NonErrorEmittedError: (Emitted value instead of an instance of Error) TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG]: A dynamic import callback was invoked without --experimental-vm-modules", | ||
| "moduleName": "css ./foo.less (exportType: link)", |
| Object { | ||
| "message": "Module build failed (from ../../../../node_modules/less-loader/dist/cjs.js): | ||
| NonErrorEmittedError: (Emitted value instead of an instance of Error) TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG]: A dynamic import callback was invoked without --experimental-vm-modules", | ||
| "moduleName": "css ./style.module.less", |
| Object { | ||
| "message": "Module build failed (from ../../../../node_modules/less-loader/dist/cjs.js): | ||
| NonErrorEmittedError: (Emitted value instead of an instance of Error) TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG]: A dynamic import callback was invoked without --experimental-vm-modules", | ||
| "moduleName": "css ./style.less", |
| Object { | ||
| "compilerPath": "link-less", | ||
| "message": "Module build failed (from ../../../../node_modules/less-loader/dist/cjs.js): | ||
| NonErrorEmittedError: (Emitted value instead of an instance of Error) TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG]: A dynamic import callback was invoked without --experimental-vm-modules", | ||
| "moduleName": "css ./style.less", |
| "if (!scriptUrl && document) {", | ||
| Template.indent([ | ||
| // Technically we could use `document.currentScript instanceof window.HTMLScriptElement`, | ||
| // but an attacker could try to inject `<script>HTMLScriptElement = HTMLImageElement</script>` |
- target.js: drop ", and let" from the `const` capability JSDoc now that `let` is a separate property. - AutoPublicPathRuntimeModule: restore the third line of the security rationale comment (`<img name="currentScript">` attack vector) that was truncated during the earlier const/let edit. - Remove seven spurious snapshot files that were accidentally committed. They were generated by an earlier test run without `--experimental-vm-modules`, so they captured less-loader infrastructure errors and a downstream redeclaration error rather than real failures. All affected tests pass cleanly when run with the proper flag.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #21010 +/- ##
==========================================
+ Coverage 91.59% 91.63% +0.03%
==========================================
Files 573 573
Lines 59582 59766 +184
Branches 16094 16144 +50
==========================================
+ Hits 54576 54764 +188
+ Misses 5006 5002 -4
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:
|
- target.js: drop ", and let" from the `const` capability JSDoc now that `let` is a separate property. - AutoPublicPathRuntimeModule: restore the third line of the security rationale comment (`<img name="currentScript">` attack vector) that was truncated during the earlier const/let edit. - Remove seven spurious snapshot files that were accidentally committed. They were generated by an earlier test run without `--experimental-vm-modules`, so they captured less-loader infrastructure errors and a downstream redeclaration error rather than real failures. All affected tests pass cleanly when run with the proper flag.
4a6bb10 to
c5edb6f
Compare
- target.js: drop ", and let" from the `const` capability JSDoc now that `let` is a separate property. - AutoPublicPathRuntimeModule: restore the third line of the security rationale comment (`<img name="currentScript">` attack vector) that was truncated during the earlier const/let edit. - Remove seven spurious snapshot files that were accidentally committed. They were generated by an earlier test run without `--experimental-vm-modules`, so they captured less-loader infrastructure errors and a downstream redeclaration error rather than real failures. All affected tests pass cleanly when run with the proper flag.
c5edb6f to
fe562d1
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 72 out of 75 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
lib/web/JsonpChunkLoadingRuntimeModule.js:1
- The abbreviations
cstandltare non-obvious in a codebase whereconst/letalready have specific meaning. Consider renaming to something self-describing likeconstDecl/letDecl(orconstKeyword/letKeyword) to make the generated-code intent easier to follow during maintenance.
schemas/WebpackOptions.json:1 - Schema descriptions in this file commonly format language tokens with backticks (e.g.
import.meta.dirname). To stay consistent and avoid ambiguity, consider changing the description to use backticks aroundlet(e.g. "The environment supportsletfor variable declarations.").
| ); | ||
| F(environment, "let", () => | ||
| conditionallyOptimistic( | ||
| /** @type {boolean | undefined} */ (tp && tp.let), |
- target.js: drop ", and let" from the `const` capability JSDoc now that `let` is a separate property. - AutoPublicPathRuntimeModule: restore the third line of the security rationale comment (`<img name="currentScript">` attack vector) that was truncated during the earlier const/let edit. - Remove seven spurious snapshot files that were accidentally committed. They were generated by an earlier test run without `--experimental-vm-modules`, so they captured less-loader infrastructure errors and a downstream redeclaration error rather than real failures. All affected tests pass cleanly when run with the proper flag.
fe562d1 to
ae9dbc4
Compare
| let: rawChecker({ | ||
| chrome: 49, | ||
| and_chr: 49, | ||
| edge: 12, | ||
| firefox: 44, | ||
| and_ff: 44, | ||
| // ie: Not supported | ||
| opera: 36, | ||
| op_mob: 36, | ||
| safari: [10, 0], | ||
| ios_saf: [10, 0], | ||
| samsung: [5, 0], | ||
| android: 49, | ||
| and_qq: [10, 4], | ||
| baidu: [13, 18], | ||
| and_uc: [12, 12], | ||
| kaios: [2, 5], | ||
| node: [6, 0] | ||
| }), |
There was a problem hiding this comment.
The stricter let thresholds are intentional and reflect real engine support, so I've kept them and instead corrected the misleading comment in applyOutputDefaults that implied const always implies let.
Standard block-scoped let shipped later than const in some engines — e.g. Firefox got reliable block-scoped let in 44 (and Android WebView in 49), whereas webpack has tracked const from Firefox 36 / Android 37 (those Firefox numbers and notes predate this PR). So for a Firefox 36–43 browserslist target, const: true / let: false is the correct result, and we genuinely want to emit const but not let there.
The conditionallyOptimistic(tp.let, environment.const) fallback is purely for backward compatibility: it only kicks in when a target doesn't report let at all (undefined) — i.e. older/custom target objects that predate this capability and only declare const. Targets that do report let (browserslist and the built-in presets in target.js) keep their own accurate value, so there's no contradiction. Aligning the let map to the const versions would incorrectly claim let support for Firefox 36–43.
Generated by Claude Code
…ed code Add a new `output.environment.let` option (mirroring `output.environment.const`) so targets can advertise `let` support independently of `const`. `RuntimeTemplate` gains `supportsLet()` / `renderLet()`, and `renderConst()` now falls back to `let` before `var`. Generated runtime/bootstrap/library/sharing/css/wasm/container/HMR code now uses `const` or `let` instead of `var` wherever it is safe. Bindings that may be wrapped in runtime-condition `if` blocks (harmony imports, ConcatenatedModule external imports) keep `var` to preserve function scoping.
- target.js: drop ", and let" from the `const` capability JSDoc now that `let` is a separate property. - AutoPublicPathRuntimeModule: restore the third line of the security rationale comment (`<img name="currentScript">` attack vector) that was truncated during the earlier const/let edit. - Remove seven spurious snapshot files that were accidentally committed. They were generated by an earlier test run without `--experimental-vm-modules`, so they captured less-loader infrastructure errors and a downstream redeclaration error rather than real failures. All affected tests pass cleanly when run with the proper flag.
The `target-browserslist` and `Cli` snapshot suites enumerate every `environment.*` capability / CLI flag, so the new `let` capability needs to be reflected in their snapshots too. Pure additive snapshot updates — no logic changes.
ConfigCacheTestCases reuses the same fixtures as ConfigTestCases but keeps a parallel ConfigCacheTest.snap per case. Update those parallel snapshots to match the new const/let bootstrap declarations — same diffs that were already applied to ConfigTest.snap. Pure snapshot updates, no logic changes.
The asset-size snapshots in Stats.test.js encode the exact byte length of generated bundles. The new `const __webpack_module_cache__` / `let __webpack_exports__` declarations are a couple bytes longer than the old `var` form, so the recorded asset sizes shift by ~10 bytes per entry chunk. Snapshot-only update; no logic change.
ae9dbc4 to
4d1c287
Compare
Types CoverageCoverage after merging claude/add-supports-let-option-GLCe1 into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Add a new
output.environment.letoption (mirroringoutput.environment.const)so targets can advertise
letsupport independently ofconst.RuntimeTemplategains
supportsLet()/renderLet(), andrenderConst()now falls back toletbeforevar.Generated runtime/bootstrap/library/sharing/css/wasm/container/HMR code now uses
constorletinstead ofvarwherever it is safe. Bindings that may bewrapped in runtime-condition
ifblocks (harmony imports, ConcatenatedModuleexternal imports) keep
varto preserve function scoping.