Skip to content

fix(partial): give NgModule components their scope (#655) - #665

Open
Brooooooklyn wants to merge 6 commits into
mainfrom
fix/partial-ngmodule-scope
Open

Brooooooklyn wants to merge 6 commits into
mainfrom
fix/partial-ngmodule-scope

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Closes #655.

Problem

In partial mode, a component declared by an NgModule had no dependencies. ngtsc fills them from the module's scope, which needs the whole program. The Angular CLI's linker also emits library NgModules without their scope. So at runtime the component could not find any directive or pipe (<div d> not matched, | p → NG0302).

Fix

app.module.ts (partial output)
  ...ɵɵngDeclareNgModule(AppModule)...
  // at the end of the file:
  i0.ɵɵsetComponentScope(i1.App, () => [i2.D, i1.App], () => [i2.P]);
  • NgModule file (only with compilationMode: 'partial' + resolveImportedValues): cross-file analysis reads the scope statically, following

    • imports, exports, export * barrels, const lists, forwardRef,
    • X.forRoot() (ModuleWithProviders<T> or the returned { ngModule }),
    • .d.ts packages (ɵmod / ɵdir / ɵcmp / ɵpipe type args).

    The module then emits ɵɵsetComponentScope for each component it declares.

  • Standalone component: an NgModule in imports adds the directives and pipes it exports to dependencies. Each class is listed once.

  • Safe fallbacks: if anything can't be read (unknown value, call, cycle), the output stays the same as before.

    • A component whose file imports the module file back gets no scope call, because it may not exist yet when the module file runs.
  • Names: classes are imported the way ngtsc picks them:

    • through the package or alias the module came from,
    • else by relative path.

Also fixed here: the version field

Declarations wrote version: "0.0.0-PLACEHOLDER", which Angular's linker reads as 0.0.0:

  • @if / @for failed to link,
  • the standalone and OnPush defaults were wrong.

Now version is the Angular version compiled for (angularVersion), or 22.2.3. For a version before 19, isStandalone: true is written out, because older linkers default it to false.

Verification

  • New Rust tests in tests/partial_ng_module_scope_test.rs (21).
  • New napi test partial-ng-module-scope-runtime.test.ts: it links with Angular's babel linker and with ours, then checks directiveDefs / pipeDefs on the real @angular/core (12 tests).
  • SSR renders, with both linkers, of CommonModule, FormsModule, RouterModule.forRoot(), export * barrels, standalone imports, and the review repros (import cycles, export * cycles).
  • cargo tests (with and without cross_file_elision), napi tests, conformance 1264/1264.

Not in this PR (already on main)

  • Our linker always links optional chaining the old way (a?.b → null), even for version ≥ 22. Angular's v22 linker gives undefined.
  • Our linker puts declarations straight into ɵɵdefineNgModule. In a module ↔ component import cycle this throws Cannot access 'App' before initialization. Angular's linker leaves them out.

🤖 Generated with Claude Code

Brooooooklyn and others added 3 commits October 30, 2026 20:39
A partial declaration of a component an NgModule declares had no
dependencies, so after linking it had no directives or pipes. The
NgModule's file now calls ɵɵsetComponentScope for each component it
declares, with the module's compilation scope read across files (source
@NgModule decorators and .d.ts ɵmod types). A standalone component that
imports an NgModule now lists the module's exported directives and pipes.

Partial declarations also carry a real version (the target Angular
version, else 22.2.3) instead of 0.0.0-PLACEHOLDER, which Angular's
published linker reads as 0.0.0 and so turns off block syntax.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Set component scopes at the end of the module's file with `() => [...]`
  lists, and skip a component whose file imports the module's file back.
- A standalone component lists a class an imported NgModule exports once,
  by declaration, not by name.
- Look up each `(file, name)` once per export search, so `export *`
  cycles end in linear time.
- Import a workspace library class by path when its entry point doesn't
  export it.
- Emit `isStandalone: true` for a linker before v19, which reads a missing
  field as `false`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orts

- Skip a component when the file its scope call imports it from loads
  the module file back, not only the file that declares it.
- Follow `export * as ns from`, and not type-only imports and re-exports,
  when looking for a cycle.
- List a module's classes as seen only once its list is kept: a module
  with a pipe of no static name no longer hides them from later ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T13:59:07.244376Z ae05c6f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbda06f3ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

A class an imported NgModule exports that the file declares after the
standalone component is listed as `forwardRef(() => X)`, as ngtsc does,
so creating the component's definition doesn't read it too early.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: fae6557970

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…dows

`canonicalize` gives the compiled file a `\\?\` path, which the resolver's
paths don't have, so an import cycle back to it was never found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d47680ba9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- A class an imported NgModule exports, whose file loads the standalone
  component's file back, is listed as `forwardRef(() => i1.X)`: the
  component's definition can be created before that file has run.
- Name a class imported by path from an `.mts`/`.cts` file by the `.mjs`
  /`.cjs` file TypeScript writes for it.
- Compare canonical paths when looking for a cycle: a path through a
  tsconfig alias can be spelled differently, as Windows short names are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: ae05c6f9ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Partial mode: NgModule-declared components get no dependencies, so their directives never match

1 participant