Skip to content

fix(@angular/build): improve library bundler resolution and watch shared chunk consistency - #34440

Open
alan-agius4 wants to merge 1 commit into
angular:mainfrom
alan-agius4:fix/library-bundler-memory-loader
Open

alan-agius4 wants to merge 1 commit into
angular:mainfrom
alan-agius4:fix/library-bundler-memory-loader

Conversation

@alan-agius4

@alan-agius4 alan-agius4 commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes module resolution issues in createMemoryFileLoaderPlugin and ensures shared chunk consistency during partial watch-mode rebuilds in the @angular/build:library bundler pipeline:

  • Trailing slash normalization: Strip trailing slashes from relative directory specifiers (import './dir/') before probing candidate extensions so .../dir/index.js is resolved instead of .../dir//index.js.
  • .mjs -> .d.mts declaration preference: In .d.ts bundling mode, prefer .d.mts when an explicit .mjs specifier is used and both .d.ts and .d.mts exist.
  • Windows drive-letter paths: Recognize normalized POSIX paths with Windows drive letters (C:/...) as internal modules in resolveId rather than marking them as external.
  • Watch-mode shared chunk consistency: When a partial watch rebuild includes entry points that previously emitted shared chunks (fesm2022/shared-*.mjs or types/shared-*.d.ts), expand the Rolldown invocation to include all entry points present in the in-memory esmFiles / dtsFiles maps and update bundleResults for all bundled entry points so shared chunk hashes and stale file cleanup remain consistent.

@alan-agius4
alan-agius4 marked this pull request as ready for review October 21, 2026 06:50
@alan-agius4
alan-agius4 requested a review from dgp1130 October 21, 2026 06:50
@alan-agius4 alan-agius4 added action: review The PR is still awaiting reviews from at least one requested reviewer target: minor This PR is targeted for the next minor release labels Oct 21, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request enhances the library bundler pipeline to maintain shared chunk consistency during watch mode rebuilds and improves path resolution in the memory file loader plugin, including handling trailing slashes, Windows drive-letter paths, and preferring .d.mts for .mjs specifiers. A high-severity issue was identified where partial rebuilds that only trigger ESM or DTS changes will fail to update bundleResults due to a strict check requiring both bundles to be present, which can lead to stale file cleanup issues or unnecessary full rebuilds.

Comment thread packages/angular/build/src/builders/library/pipeline/bundler.ts Outdated
Comment thread packages/angular/build/src/builders/library/pipeline/bundler.ts Outdated
…red chunk consistency

- Strip trailing slashes when resolving relative directory imports in `createMemoryFileLoaderPlugin` so `./dir/` resolves `.../dir/index.js`.
- Support `.cjs` and `.d.cts` extensions, and prioritize `.d.mts`/`.d.cts` before `.d.ts` when resolving explicit `.mjs`/`.cjs` specifiers in `.d.ts` mode.
- Treat Windows drive-letter absolute paths (`/^[a-zA-Z]:\//`) as internal modules.
- Fall back to on-disk declaration files in `.d.ts` mode for hand-authored ambient/declaration files not emitted into the in-memory `dtsFiles` map.
- Re-bundle all entry points from in-memory outputs during partial watch rebuilds when previous bundle results contained shared chunks to prevent inlining shared code into a subset of entry points.
@alan-agius4
alan-agius4 force-pushed the fix/library-bundler-memory-loader branch from 82efdc3 to 4e8c365 Compare October 22, 2026 08:44
/**
* Checks whether two sets share at least one common element.
*/
function hasSetIntersection(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

continue;
}
} else {
selectedNames ??= new Set(selectedEntryPoints.map((item) => item.name));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider: Could we just initialize on line 137 instead of using ??=?

const selectedNames = new Set<string>();

Comment on lines +138 to +139
const singleSelectedName =
selectedEntryPoints.length === 1 ? selectedEntryPoints[0].name : undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Question: Why are we special casing singleSelectedName?

If I'm following this correctly, I think the below while loop does nothing in that case? Could we just return early or let this be naturally handled as an array that just happens to have one item in it?

Comment on lines +111 to +113
if (selectedEntryPoints.length >= allEntryPoints.length) {
return selectedEntryPoints;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Question: In what practical situation do we have more selected entry points than all entry points? Should that be an assertion error?

* Expands the selected entry points to include any available entry points whose module IDs
* transitively overlap with the selected set.
*/
function expandOverlappingEntryPoints(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider: TBH, this function implementation has kinda lost me. IIUC, the intent is to figure out all entry points which share a module ID with the given selected entry points?

Is that fundamentally just an Object.groupBy call? Is there a way we can simplify to something closer to:

return Object.groupBy(selectedEntryPoints, getModuleIds).values();

I suspect that wouldn't exactly work because each entry point has multiple module IDs, but I'm wondering if conceptually it's a cleaner direction?

getPreviousModuleIds: (ep: NormalizedEntryPoint) => ReadonlySet<string> | undefined,
): boolean {
for (const ep of bundledEntryPoints) {
const newIds = output.bundles.get(ep.bundleName)?.moduleIds;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: We should always expect to get a bundle right? Would it be more clear to do output.bundles.get(ep.bundleName)!.moduleIds and then drop the subsequent if (!newIds) style checks? Similarly, do we really care about skipping .size === 0 cases or will that no-op on its own?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: review The PR is still awaiting reviews from at least one requested reviewer area: @angular/build target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants