Skip to content

[2.x] fix: anchor webpack-config extension source detection to the real src directory - #4770

Open
PrimateCoder wants to merge 2 commits into
flarum:2.xfrom
PrimateCoder:fix/webpack-anchor-src-detection
Open

[2.x] fix: anchor webpack-config extension source detection to the real src directory#4770
PrimateCoder wants to merge 2 commits into
flarum:2.xfrom
PrimateCoder:fix/webpack-anchor-src-detection

Conversation

@PrimateCoder

@PrimateCoder PrimateCoder commented Jun 19, 2026

Copy link
Copy Markdown

Fixes a build failure in js-packages/webpack-config that occurs when an extension (or core) is checked out under a filesystem path that itself contains a src segment (e.g. ~/src/my-ext).

Problem

The webpack config identifies an extension's own source files (under its src/ dir) using loose "src" matching in several places:

  • index.cjs: loader rules use include: /src/ — a regex matching the substring "src" anywhere in a module's absolute path.
  • RegisterAsyncChunksPlugin.cjs: module.resource.split(path.sep).includes('src') and module.resource.includes('/src/').
  • autoChunkNameLoader.cjs: absolutePathToImport.includes('src') then .split('src/')[1].

When the project lives under a path containing a src segment, the absolute paths of node_modules files also contain "src", so:

  1. The auto-export loader runs on third-party modules (e.g. @babel/runtime/helpers/esm/defineProperty.js, which contains export { _defineProperty as default }) and emits invalid flarum.reg.add('…', { _defineProperty as default: … }), failing the build with Module parse failed: Unexpected token.
  2. RegisterAsyncChunksPlugin treats node_modules modules as extension source and throws TypeError: Cannot read properties of undefined (reading 'includes') when such a module's _source._value is undefined.
  3. autoChunkNameLoader splits on the first src/ in the path, so an extension with dynamic imports gets chunk names that leak the checkout path (e.g. acme/js/… instead of forum/Lazy).

This doesn't reproduce in normal CI checkouts because their paths don't contain a src segment — which is why it's easy to miss.

Fix

Anchor every "is this file part of the extension's src?" check to the extension's actual src directory (path.resolve(process.cwd(), 'src'), or the loader's this.rootContext) and match by directory prefix instead of loose substring/segment matching. Also hardens a _source._value?. access with optional chaining.

This is a no-op for checkouts not under a src path (where /src/ only ever matched the project's own src). process.cwd() is already the basis for entry points, the output dir, and composer.json / package.json resolution throughout this config, so this introduces no new assumption.

Tests

Adds tests/srcPathAnchoring.test.js (self-contained; does not depend on the monorepo composer.json):

  • the source-loader rules use an absolute <cwd>/src include rather than a /src/ regex, and do not match node_modules under an unrelated …/src/… path;
  • autoChunkNameLoader produces chunk names relative to the extension's src even when the checkout path contains a src segment;
  • external (ext: / flarum/) imports still convert to flarum.reg.asyncModuleImport(...).

These fail against the pre-fix code and pass after it.

Verification

Built a real Flarum 2.x extension whose checkout path contains a src segment: the build previously failed with both errors above and now succeeds. A dynamic import yields addChunkModule('…','…','<ns>','forum/Lazy') (correctly src-relative) with no checkout-path leakage, and the produced bundle is otherwise byte-identical.

Windows: core's JS was also built natively on Windows (PowerShell) from a checkout under a src segment (C:\Users\…\GitHub\src\framework), comparing pre-fix and post-fix clones side by side. Pre-fix, the build fails with Module parse failed on the @babel/runtime helpers under js-packages/webpack-config/node_modules (the auto-export loader ran on them). Post-fix, it builds cleanly with 455 flarum.reg.add calls in dist/forum.js — the non-zero count rules out a silent prefix mismatch between webpack's context and module.resource on Windows.

Addendum (from review)

Two corrections/expansions based on review feedback:

  • The chunk-name symptom is worse than described above: split('src/')[1] returns a fragment of the checkout path that is identical for every module in the checkout, so with webpackMode: 'lazy-once' all dynamic imports receive the same webpackChunkName and collapse into a single lazy chunk — not just an ugly name.
  • Also fixed here (previously unclaimed): the urlPath rewrite in RegisterAsyncChunksPlugin replaced a greedy .*/src/ match (last occurrence) with a slice from the project's own src (first occurrence). For a nested src (e.g. <proj>/src/forum/src/Foo.ts) the old code produced Foo while autoExportLoader registered forum/src/Foo; both now agree on forum/src/Foo.

The source root is now unified on webpack's context (set explicitly in index.cjs, derived in RegisterAsyncChunksPlugin via compiler.options.context, and this.rootContext in the loaders) rather than four separate process.cwd()-based bases.

@PrimateCoder
PrimateCoder requested a review from a team as a code owner June 19, 2026 03:15
@PrimateCoder PrimateCoder changed the title fix: anchor webpack-config extension source detection to the real src directory [2.x] fix: anchor webpack-config extension source detection to the real src directory Jun 19, 2026

@imorland imorland left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — the diagnosis is right. Some of what I checked, for the record:

  • The chunk-name symptom is actually worse than you describe. absolutePathToImport.split('src/')[1] returns the checkout path fragment, which doesn't vary by module — so every dynamic import in such a checkout gets the same webpackChunkName (acme/js/ in your fixture). With webpackMode: 'lazy-once' that collapses all lazy chunks into one, rather than just producing an ugly name. Worth saying in the description.
  • Your tests do pin the fix: reverting only src/ fails three of the four, and they pass after.
  • The premise holds — webpack compiles a string condition to str.startsWith(condition) in RuleSetCompiler, so the anchored include has the semantics you're relying on.
  • I built core's JS before and after: byte-identical across all 44 output files, so the no-op claim checks out.
  • Nothing else in the package does loose matching; autoExportLoader was already anchored via this.rootContext.

One thing you've fixed without claiming it: the urlPath rewrite in RegisterAsyncChunksPlugin replaces a greedy .*/src/ (last match) with a slice from the project's own src (first). For a nested src — say <proj>/src/forum/src/Foo.ts — the old code produced Foo and the new produces forum/src/Foo, which is what autoExportLoader registers. So it also removes a real disagreement between the two. Worth calling out.

Three things before I approve:

1. Windows confirmation. include: /src/ was a separator-agnostic substring regex; the replacement is an exact, case-sensitive prefix string. On Windows, drive-letter and path casing can differ between process.cwd() and webpack's module.resource, and a mismatch here fails silently — the loaders just don't run, so you get a successful build with no flarum.reg.add calls rather than an error. This code supports Windows deliberately (path.sep == '\\' branches in two places), so I'd like a build confirmed on Windows before this goes in.

2. Unify on webpack's context. There are now four bases for the same idea: index.cjs and RegisterAsyncChunksPlugin use process.cwd(), autoChunkNameLoader uses this.rootContext || process.cwd(), and autoExportLoader uses this.rootContext. They're identical today only because nothing sets context and webpack defaults it to cwd. Since the whole point of this PR is to stop inferring the source root, I'd rather it came from one place:

  • RegisterAsyncChunksPlugin gets compiler in apply(), so use compiler.options.context.
  • Set context: process.cwd() explicitly in index.cjs and derive the include from it, so the contract is stated rather than implied.
  • Drop the || process.cwd() fallback in autoChunkNameLoader to match autoExportLoader — real webpack always sets rootContext, and your test already passes it.

3. Rebase on current 2.x. The branch is based on rc.4-era 2.x and the checks last ran in June. Nothing has touched these files since, so it should rebase clean — I just want CI to have run against current 2.x before it goes in.

One optional note while you're in there: the include test asserts rule.include equals a value recomputed the same way as the implementation, and the second test hand-rolls its own matches() helper. I checked webpack's condition semantics myself so the fix is right, but neither test would catch it being wrong. tests/compiler.js already runs real webpack compiles — building the config through index.cjs there would exercise the rule properly. Not blocking.

I'll add the changelog entry ((webpack) scope) when this lands, so don't worry about that.

@PrimateCoder

Copy link
Copy Markdown
Author

I didn't think about Windows, thanks for the call out. Fortunately the testing looks good.

Here are the results from orig vs patched.

Orig:

C:\Users\navindra\GitHub\src\framework.orig\framework\core\js>npx webpack --mode production
Browserslist: browsers data (caniuse-lite) is 7 months old. Please run:
  npx update-browserslist-db@latest
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
assets by status 1.31 MiB [cached] 3 assets
orphan modules 232 KiB [orphan] 92 modules
runtime modules 14.6 KiB 24 modules
modules by path ./ 1010 KiB
  modules by path ./src/ 891 KiB
    modules by path ./src/common/ 371 KiB 106 modules
    modules by path ./src/forum/ 324 KiB 70 modules
    modules by path ./src/admin/ 195 KiB 42 modules
  modules by path ./*.ts 119 KiB
    ./forum.ts + 31 modules 69.6 KiB [built] [code generated]
    ./admin.ts + 25 modules 49.8 KiB [built] [code generated]
modules by path ../../../ 651 KiB
  modules by path ../../../node_modules/ 650 KiB 52 modules
  modules with errors 1.13 KiB [true errors]
    ../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/def...(truncated) 468 bytes [built] [code generated] [1 error]
    ../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/obje...(truncated) 686 bytes [built] [code generated] [1 error]

WARNING in ./admin.ts 2:0-28
The requested module './src/admin' contains conflicting star exports for the name 'app' with the previous requested module './src/common'

WARNING in ./forum.ts 2:0-28
The requested module './src/forum' contains conflicting star exports for the name 'app' with the previous requested module './src/common'

ERROR in ../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js 11:138
Module parse failed: Unexpected token (11:138)
File was processed with these loaders:
 * ../../../js-packages/webpack-config/src/autoExportLoader.cjs
 * ../../../js-packages/webpack-config/src/autoChunkNameLoader.cjs
 * ../../../node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
| }
| export { _defineProperty as default };
> flarum.reg.add('core', '../../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty', { _defineProperty as default: _defineProperty as default, });
 @ ./src/forum/ForumApplication.tsx 1:0-72 30:4-19 36:4-19 43:4-19 48:4-19 52:4-19 56:4-19 60:4-19 65:4-19 66:4-19 67:4-19 72:4-19
 @ ./src/forum/app.ts
 @ ./src/forum/index.ts 4:0-24 5:0-15
 @ ./forum.ts 2:0-28 2:0-28

ERROR in ../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties.js 14:156
Module parse failed: Unexpected token (14:156)
File was processed with these loaders:
 * ../../../js-packages/webpack-config/src/autoExportLoader.cjs
 * ../../../js-packages/webpack-config/src/autoChunkNameLoader.cjs
 * ../../../node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
| }
| export { _objectWithoutProperties as default };
> flarum.reg.add('core', '../../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties', { _objectWithoutProperties as default: _objectWithoutProperties as default, });
 @ ./src/forum/states/GlobalSearchState.ts 1:0-90 67:15-39
 @ ./src/forum/forum.ts 11:0-36
 @ ./src/forum/index.ts 6:0-17
 @ ./forum.ts 2:0-28 2:0-28

webpack 5.104.1 compiled with 2 errors and 2 warnings in 14463 ms

Patched:

C:\Users\navindra\GitHub\src\framework.patch\framework\core\js>npx webpack --mode production
Browserslist: browsers data (caniuse-lite) is 7 months old. Please run:
  npx update-browserslist-db@latest
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
assets by path forum/components/*.js 63.3 KiB
  asset forum/components/PostStream.js 10.9 KiB [compared for emit] [minimized] (name: forum/components/PostStream) 1 related asset
  asset forum/components/UserSecurityPage.js 10.2 KiB [compared for emit] [minimized] (name: forum/components/UserSecurityPage) 1 related asset
  asset forum/components/SettingsPage.js 8.64 KiB [compared for emit] [minimized] (name: forum/components/SettingsPage) 1 related asset
  + 9 assets
assets by path admin/ 47.5 KiB
  asset admin/utils/loadSortable.js 44.2 KiB [compared for emit] [minimized] (name: admin/utils/loadSortable) 2 related assets
  asset admin/components/FontAwesomePreviewModal.js 1.75 KiB [compared for emit] [minimized] (name: admin/components/FontAwesomePreviewModal) 1 related asset
  asset admin/components/ResetExtensionSettingsModal.js 1.6 KiB [compared for emit] [minimized] (name: admin/components/ResetExtensionSettingsModal) 1 related asset
assets by status 917 KiB [big]
  asset forum.js 464 KiB [emitted] [compared for emit] [minimized] [big] (name: forum) 2 related assets
  asset admin.js 453 KiB [compared for emit] [minimized] [big] (name: admin) 2 related assets
assets by path common/components/*.js 12.4 KiB
  asset common/components/SearchModal.js 7.81 KiB [compared for emit] [minimized] (name: common/components/SearchModal) 1 related asset
  asset common/components/EditUserModal.js 4.62 KiB [compared for emit] [minimized] (name: common/components/EditUserModal) 1 related asset
orphan modules 224 KiB [orphan] 94 modules
runtime modules 15.3 KiB 22 modules
modules by path ./ 1.05 MiB
  modules by path ./src/ 883 KiB
    modules by path ./src/common/ 358 KiB 106 modules
    modules by path ./src/forum/ 327 KiB 70 modules
    modules by path ./src/admin/ 198 KiB 42 modules
  modules by path ./*.ts 189 KiB
    ./forum.ts + 42 modules 99.2 KiB [built] [code generated]
    ./admin.ts + 43 modules 89.8 KiB [built] [code generated]
modules by path ../../../ 660 KiB
  modules by path ../../../node_modules/ 658 KiB 54 modules
  modules by path ../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/*.js 1.99 KiB
    ../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defi...(truncated) 1.26 KiB [built] [code generated]
    ../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/obje...(truncated) 747 bytes [built] [code generated]

WARNING in ./admin.ts 2:0-28
The requested module './src/admin' contains conflicting star exports for the name 'app' with the previous requested module './src/common'

WARNING in ./forum.ts 2:0-28
The requested module './src/forum' contains conflicting star exports for the name 'app' with the previous requested module './src/common'

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  forum.js (464 KiB)
  admin.js (453 KiB)

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  forum (464 KiB)
      forum.js
  admin (453 KiB)
      admin.js


webpack 5.104.1 compiled with 4 warnings in 23868 ms

C:\Users\navindra\GitHub\src\framework.patch\framework\core\js>
C:\Users\navindra\GitHub\src\framework.patch\framework\core\js>exit
PS C:\Users\navindra\GitHub\src\framework.patch\framework\core\js> (Select-String -Path dist\forum.js -Pattern 'flarum\.reg\.add' -AllMatches |
>>   ForEach-Object { $_.Matches.Count } | Measure-Object -Sum).Sum
455

@PrimateCoder
PrimateCoder force-pushed the fix/webpack-anchor-src-detection branch from efc469b to e6b1423 Compare August 23, 2026 19:45
@PrimateCoder

PrimateCoder commented Aug 23, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review! All three blocking items are done:

1. Windows confirmation

Built core's JS natively on Windows (PowerShell) from a checkout under a src segment (C:\Users\navindra\GitHub\src\framework), comparing pre-fix and post-fix clones side by side:

  • Pre-fix: build fails with Module parse failed on the @babel/runtime helpers under js-packages/webpack-config/node_modules — the auto-export loader ran on them and emitted invalid syntax:
    flarum.reg.add('core', '.../defineProperty', { _defineProperty as default: _defineProperty as default, });
    
  • Post-fix: builds cleanly (only the pre-existing star-export/size warnings), with 455 flarum.reg.add calls in dist/forum.js — ruling out the silent prefix-mismatch failure mode.
  • The jest suite also passes on Windows.

2. Unified on webpack's context ✅ (857ef13)

  • index.cjs now sets context: process.cwd() explicitly and derives both loader includes from it.
  • RegisterAsyncChunksPlugin uses compiler.options.context (computed once in apply()).
  • Dropped the || process.cwd() fallback in autoChunkNameLoader to match autoExportLoader.

No behaviour change — the values are identical today; the contract is just stated in one place now.

3. Rebased on current 2.x ✅ — rebased onto 664494da6 (current tip, clean, no conflicts); CI runs against the updated branch.

I've also updated the PR description per your notes: the chunk-name symptom is worse than originally stated (the constant name collapses all lazy chunks into one under lazy-once), and I've called out the urlPath fix in RegisterAsyncChunksPlugin (greedy last-match .*/src/ replaced by a slice from the project's own src, which also resolves the disagreement with autoExportLoader for nested src directories).

@PrimateCoder
PrimateCoder requested a review from imorland August 23, 2026 22:36
… dir

The config identifies an extension's own source files (under its `src/`
dir) using loose "src" matching: `include: /src/` in index.cjs,
`resource.split(path.sep).includes('src')` / `resource.includes('/src/')`
in RegisterAsyncChunksPlugin, and `absolutePathToImport.includes('src')`
in autoChunkNameLoader.

When a project is checked out under a path that itself contains a "src"
segment (e.g. ~/src/my-ext), node_modules paths also contain "src", so:
the auto-export loader runs on third-party modules (e.g. @babel/runtime
helpers) and emits invalid flarum.reg.add(...) (build fails with "Module
parse failed"); RegisterAsyncChunksPlugin treats node_modules as
extension source and throws "Cannot read properties of undefined
(reading 'includes')"; and autoChunkNameLoader produces chunk names that
leak the checkout path.

Anchor all of these to the extension's actual src directory
(path.resolve(process.cwd(), 'src') / this.rootContext) using directory
prefix matching, and harden the _source._value access. No-op for
checkouts not under a "src" path. Adds regression tests.
Derive the extension's src directory from a single source of truth
instead of four separate bases: set `context` explicitly in index.cjs
and derive the loader includes from it, use `compiler.options.context`
in RegisterAsyncChunksPlugin, and drop the `process.cwd()` fallback in
autoChunkNameLoader so it matches autoExportLoader. No behaviour change;
the values are identical today since nothing overrides context.
@PrimateCoder
PrimateCoder force-pushed the fix/webpack-anchor-src-detection branch from e6b1423 to 857ef13 Compare August 23, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants