Skip to content

fix(angular-compiler): add type-only import elision to angular compiler#2249

Merged
brandonroberts merged 6 commits into
betafrom
feat/type-import-elision
Apr 6, 2026
Merged

fix(angular-compiler): add type-only import elision to angular compiler#2249
brandonroberts merged 6 commits into
betafrom
feat/type-import-elision

Conversation

@brandonroberts

Copy link
Copy Markdown
Member

PR Checklist

Add type-only import elision to the angular compiler so that imports used only in type positions are automatically removed before reaching the browser.

Affected scope

  • Primary scope: angular-compiler
  • Secondary scopes: vite-plugin-angular

Recommended merge strategy for maintainer [optional]

  • Squash merge
  • Rebase merge
  • Other

What is the new behavior?

  • Imports that are only referenced in type positions (type annotations, implements clauses, generic type arguments, return types) are now automatically elided from the compiler output.
  • Uses oxc-parser (already a dependency) for fast, Rust-based AST usage analysis — no TypeScript type-checker needed (~0.12ms per file).
  • Handles named imports (import { Foo }), default imports (import Foo), and mixed declarations (import Foo, { Bar }).
  • Fixes runtime errors like Uncaught SyntaxError: The requested module does not provide an export named 'PotatoType' when types are imported without the explicit type keyword.
  • New type-elision.ts module with detectTypeOnlyImportNames() and elideTypeOnlyImports() functions.
  • Integrated as step 4 of the compilation pipeline (after Ivy codegen, before OXC type stripping).

Test plan

  • npx vitest run packages/angular-compiler/src/lib/type-elision.spec.ts — 27 new tests covering detection and elision for all import forms
  • npx vitest run packages/angular-compiler/src/lib/ — all 432 tests pass (17/17 test files), no regressions
  • pnpm build
  • pnpm test

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

This addresses the issue where import { PotatoType } from "potato" (without the type keyword) leaks to the browser because single-file transpilers like OXC/esbuild cannot distinguish type-only imports without type information. The import type and import { type X } forms already worked; this fix covers the bare import { X } case.

Namespace imports (import * as ns) are not handled as they require qualified-name tracking and are rare in Angular code. Re-exports (export { X } from) cannot be fixed with single-file analysis.

@netlify

netlify Bot commented Apr 6, 2026

Copy link
Copy Markdown

Deploy Preview for analog-blog ready!

Name Link
🔨 Latest commit b5f4dff
🔍 Latest deploy log https://app.netlify.com/projects/analog-blog/deploys/69d3eff8c313c50008b8accd
😎 Deploy Preview https://deploy-preview-2249--analog-blog.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Apr 6, 2026

Copy link
Copy Markdown

Deploy Preview for analog-app ready!

Name Link
🔨 Latest commit b5f4dff
🔍 Latest deploy log https://app.netlify.com/projects/analog-app/deploys/69d3eff8f968e30007830dd9
😎 Deploy Preview https://deploy-preview-2249--analog-app.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Apr 6, 2026

Copy link
Copy Markdown

Deploy Preview for analog-docs ready!

Name Link
🔨 Latest commit b5f4dff
🔍 Latest deploy log https://app.netlify.com/projects/analog-docs/deploys/69d3eff8ea88f70008ec0fc4
😎 Deploy Preview https://deploy-preview-2249--analog-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the scope:angular-compiler Changes in @analogjs/angular-compiler label Apr 6, 2026
@coderabbitai

coderabbitai Bot commented Apr 6, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A new OXC-based type-elision stage was added that detects and removes imports used only in TypeScript type positions. New exports: detectTypeOnlyImportNames(code), elideTypeOnlyImports(code), and elideTypeOnlyImportsMagicString(ms) live in packages/angular-compiler/src/lib/type-elision.ts. The compiler’s compile(...) now calls the MagicString-aware elision step before producing final code and sourcemap. Documentation (COMPILER.md) and tests (type-elision.spec.ts, updated integration tests) were added/updated to cover detection, elision, and sourcemap correctness.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

BREAKING CHANGE

Before:

  • compile(...) returned emitted JS that could still include imports referenced only in type positions (e.g., import { SomeType } from './models').

After:

  • compile(...) post-processes emitted JS to remove type-only imports; such import declarations/specifiers are elided from output and sourcemaps adjusted accordingly.

Reviewer checklist:

  • Verify no runtime imports required by Angular DI or other value positions are removed.
  • Confirm sourcemap accuracy after MagicString edits.
  • Confirm public API surface unchanged (only new exported helper functions added).
  • Run full test suite to catch integration regressions.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title follows Conventional Commit format with 'fix(angular-compiler)' and clearly summarizes the main change: adding type-only import elision.
Description check ✅ Passed Description comprehensively covers the changeset: new behavior, technical approach, test results, scope, and known limitations, all directly related to the type-only import elision implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/angular-compiler/src/lib/compile.ts (1)

845-862: ⚠️ Potential issue | 🟠 Major

Source map misalignment when type-only imports are elided.

The map is generated from ms before elision, but code is returned post-elision. When elideTypeOnlyImports removes or rewrites import statements, source positions shift, causing the source map to reference positions that no longer exist in the output.

While component.spec.ts verifies the map is generated, the test input contains only value imports (Component, signal), so elideTypeOnlyImports returns the code unchanged. There is no integration test coverage for cases where type-only imports are actually elided.

Fix: Apply elision via MagicString before generateMap():

const compiledCode = elideTypeOnlyImports(ms.toString());
ms = new MagicString(compiledCode);
const map = ms.generateMap({...});

Or rebuild the MagicString from the elided output to keep positions synchronized. Without this, source maps become invalid whenever type-only imports are present in the compiled output.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-compiler/src/lib/compile.ts` around lines 845 - 862, The
source map is generated from the original MagicString `ms` before
`elideTypeOnlyImports` runs, so when `elideTypeOnlyImports(ms.toString())`
changes the code the map becomes misaligned; fix this by performing the elision
first and rebuilding the MagicString used for `generateMap()` — i.e., call
`elideTypeOnlyImports(ms.toString())`, assign the result to `compiledCode`, then
create a new `MagicString(compiledCode)` (or otherwise replace `ms`) before
calling `ms.generateMap({...})` so the `map` matches the returned `code` and
preserves options like `includeContent` and `hires`.
🧹 Nitpick comments (1)
packages/angular-compiler/src/lib/type-elision.ts (1)

167-168: Minor: Only \n consumed when trimming declaration end.

If source has Windows line endings (\r\n), the \r would remain after removal. This is unlikely to cause issues since most build pipelines normalize line endings, but worth noting.

     let declEnd = node.end;
-    while (declEnd < code.length && code[declEnd] === '\n') declEnd++;
+    while (declEnd < code.length && (code[declEnd] === '\n' || code[declEnd] === '\r')) declEnd++;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-compiler/src/lib/type-elision.ts` around lines 167 - 168,
The trimming loop for declaration end only checks for '\n' so a preceding '\r'
in Windows CRLF endings remains; update the loop that advances declEnd (the
variable declared as let declEnd = node.end) to consume both '\r' and '\n'
characters (e.g., check code[declEnd] === '\n' || code[declEnd] === '\r' or use
a small helper isLineBreak) so CRLF sequences are fully skipped when moving
declEnd past trailing line breaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/angular-compiler/COMPILER.md`:
- Around line 444-445: The COMPILER.md entry for conformance.spec.ts has an
incorrect test count: update the table row referencing `conformance.spec.ts` to
show the actual number of tests (replace 167 with 2) so the documentation
matches the file; edit the Markdown table line containing `conformance.spec.ts`
in COMPILER.md to change the numeric count to 2.

---

Outside diff comments:
In `@packages/angular-compiler/src/lib/compile.ts`:
- Around line 845-862: The source map is generated from the original MagicString
`ms` before `elideTypeOnlyImports` runs, so when
`elideTypeOnlyImports(ms.toString())` changes the code the map becomes
misaligned; fix this by performing the elision first and rebuilding the
MagicString used for `generateMap()` — i.e., call
`elideTypeOnlyImports(ms.toString())`, assign the result to `compiledCode`, then
create a new `MagicString(compiledCode)` (or otherwise replace `ms`) before
calling `ms.generateMap({...})` so the `map` matches the returned `code` and
preserves options like `includeContent` and `hires`.

---

Nitpick comments:
In `@packages/angular-compiler/src/lib/type-elision.ts`:
- Around line 167-168: The trimming loop for declaration end only checks for
'\n' so a preceding '\r' in Windows CRLF endings remains; update the loop that
advances declEnd (the variable declared as let declEnd = node.end) to consume
both '\r' and '\n' characters (e.g., check code[declEnd] === '\n' ||
code[declEnd] === '\r' or use a small helper isLineBreak) so CRLF sequences are
fully skipped when moving declEnd past trailing line breaks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3ee34a35-60e9-4a72-9116-0ec7cec127c9

📥 Commits

Reviewing files that changed from the base of the PR and between bc92d70 and aa07edb.

📒 Files selected for processing (4)
  • packages/angular-compiler/COMPILER.md
  • packages/angular-compiler/src/lib/compile.ts
  • packages/angular-compiler/src/lib/type-elision.spec.ts
  • packages/angular-compiler/src/lib/type-elision.ts

Comment thread packages/angular-compiler/COMPILER.md Outdated
Comment on lines +444 to +445
| `type-elision.spec.ts` | 27 | Type-only import detection (annotations, implements, generics, mixed, unused, default imports) and elision (whole-declaration removal, partial specifier removal, default import handling, mixed default+named, compiler output integration) |
| `conformance.spec.ts` | 167 | Angular compliance test suite (v17-v21, 87%+ Ivy instruction match) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Count actual tests in the spec files
echo "=== type-elision.spec.ts test count ==="
rg -c "^\s*it\(" packages/angular-compiler/src/lib/type-elision.spec.ts || echo "File not found"

echo ""
echo "=== conformance.spec.ts test count ==="
rg -c "^\s*it\(" packages/angular-compiler/src/lib/conformance.spec.ts || echo "File not found"

Repository: analogjs/analog

Length of output: 143


Update conformance.spec.ts test count in documentation.

The type-elision.spec.ts count of 27 is correct, but conformance.spec.ts documentation claims 167 tests while the file contains only 2. Update COMPILER.md line 445 to reflect the actual test count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-compiler/COMPILER.md` around lines 444 - 445, The
COMPILER.md entry for conformance.spec.ts has an incorrect test count: update
the table row referencing `conformance.spec.ts` to show the actual number of
tests (replace 167 with 2) so the documentation matches the file; edit the
Markdown table line containing `conformance.spec.ts` in COMPILER.md to change
the numeric count to 2.

Imports used only in type positions (annotations, implements, generics)
are now automatically elided from compiler output using OXC AST usage
analysis. This prevents runtime errors when single-file transpilers
(OXC/esbuild) can't distinguish type-only imports without the explicit
`type` keyword. Handles named imports, default imports, and mixed
default+named declarations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@brandonroberts brandonroberts force-pushed the feat/type-import-elision branch from aa07edb to 788b514 Compare April 6, 2026 13:49

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/angular-compiler/src/lib/type-elision.ts (2)

53-66: Namespace imports (import * as ns) silently pass through.

ImportNamespaceSpecifier is not collected at line 63 since only spec.local.name is grabbed without checking for this specifier type. If a namespace import is used only in type positions, it won't be detected or elided.

The PR summary notes this is intentional, but consider adding a code comment here to document the limitation for future maintainers.

📝 Suggested documentation comment
   // Step 1 – collect all value-imported names (skip `import type` / `{ type X }`)
+  // NOTE: Namespace imports (`import * as ns`) are intentionally skipped; we do
+  // not analyse namespace member access, so they are left untouched.
   const importedNames = new Set<string>();
   for (const node of (ast as any).body) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-compiler/src/lib/type-elision.ts` around lines 53 - 66, The
loop in detectTypeOnlyImportNames currently skips namespace imports because it
assumes specifiers have a .local.name and doesn't handle
ImportNamespaceSpecifier, so namespace imports used only as types won't be
detected; update the import-collection loop (in detectTypeOnlyImportNames,
referencing the importedNames set and the node.specifiers iteration) to either
explicitly handle ImportNamespaceSpecifier by checking spec.type ===
'ImportNamespaceSpecifier' and adding the namespace identifier, or—if you
intentionally don't support namespace elision—add a clear inline comment above
this loop stating that ImportNamespaceSpecifier (import * as ns) is
intentionally ignored and why, so future maintainers understand the limitation.

131-136: Double-parsing the same source is inefficient.

elideTypeOnlyImports calls detectTypeOnlyImportNames (which parses once), then immediately parses again at line 135. For large files this doubles AST construction cost.

A refactor to return the AST from detection—or combine both passes into one function with an internal helper—would cut parse overhead in half. Not blocking, but worth considering if profiling shows this is a hot path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-compiler/src/lib/type-elision.ts` around lines 131 - 136,
elideTypeOnlyImports currently reparses the source after
detectTypeOnlyImportNames already parsed it, causing duplicate AST work; change
detectTypeOnlyImportNames to either return the parsed AST (e.g., { names, ast })
or accept a pre-parsed AST parameter so elideTypeOnlyImports can call parseSync
once (use parseSync in elideTypeOnlyImports and pass its AST into
detectTypeOnlyImportNames), then reuse that AST for the rest of the logic
instead of calling parseSync a second time; update call sites of
detectTypeOnlyImportNames accordingly (or consolidate both passes into a single
function that detects type-only names and performs elision) so parseSync is
invoked only once per source.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/angular-compiler/src/lib/type-elision.ts`:
- Around line 166-168: The loop that advances declEnd from node.end only checks
for '\n' and thus fails on Windows CRLF; update the logic around declEnd
(starting at node.end) to consume both '\r' and '\n' sequences (e.g., skip a
'\r' if present and then a '\n', or treat any '\r' or '\n' as newline
characters) so CRLF ("\r\n") is fully consumed from the code buffer variable;
ensure the check references code[declEnd] and advances declEnd appropriately
until no more CR or LF characters remain.
- Around line 131-135: elideTypeOnlyImports currently removes entire import
declarations by string-manipulation causing the final compiledCode to be shorter
than the MagicString (ms) used later for sourcemap generation; update
elideTypeOnlyImports (and its use of detectTypeOnlyImportNames) to perform
removals using MagicString APIs (e.g., ms.remove(rangeStart, rangeEnd) for each
import declaration) so ms reflects the same edits, or alternatively after
producing compiledCode regenerate the sourcemap from compiledCode (using
magic-string/source-map utilities) before returning from compile.ts; ensure you
reference and update the same ranges that elideTypeOnlyImports targets so no
line/column drift occurs between ms and compiledCode.

---

Nitpick comments:
In `@packages/angular-compiler/src/lib/type-elision.ts`:
- Around line 53-66: The loop in detectTypeOnlyImportNames currently skips
namespace imports because it assumes specifiers have a .local.name and doesn't
handle ImportNamespaceSpecifier, so namespace imports used only as types won't
be detected; update the import-collection loop (in detectTypeOnlyImportNames,
referencing the importedNames set and the node.specifiers iteration) to either
explicitly handle ImportNamespaceSpecifier by checking spec.type ===
'ImportNamespaceSpecifier' and adding the namespace identifier, or—if you
intentionally don't support namespace elision—add a clear inline comment above
this loop stating that ImportNamespaceSpecifier (import * as ns) is
intentionally ignored and why, so future maintainers understand the limitation.
- Around line 131-136: elideTypeOnlyImports currently reparses the source after
detectTypeOnlyImportNames already parsed it, causing duplicate AST work; change
detectTypeOnlyImportNames to either return the parsed AST (e.g., { names, ast })
or accept a pre-parsed AST parameter so elideTypeOnlyImports can call parseSync
once (use parseSync in elideTypeOnlyImports and pass its AST into
detectTypeOnlyImportNames), then reuse that AST for the rest of the logic
instead of calling parseSync a second time; update call sites of
detectTypeOnlyImportNames accordingly (or consolidate both passes into a single
function that detects type-only names and performs elision) so parseSync is
invoked only once per source.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4fc3682-d8a6-4689-af2b-38efad63875f

📥 Commits

Reviewing files that changed from the base of the PR and between aa07edb and 788b514.

📒 Files selected for processing (4)
  • packages/angular-compiler/COMPILER.md
  • packages/angular-compiler/src/lib/compile.ts
  • packages/angular-compiler/src/lib/type-elision.spec.ts
  • packages/angular-compiler/src/lib/type-elision.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/angular-compiler/src/lib/compile.ts
  • packages/angular-compiler/COMPILER.md
  • packages/angular-compiler/src/lib/type-elision.spec.ts

Comment thread packages/angular-compiler/src/lib/type-elision.ts Outdated
Comment thread packages/angular-compiler/src/lib/type-elision.ts Outdated
brandonroberts and others added 2 commits April 6, 2026 12:11
…icstring

Refactor type-only import elision to operate directly on MagicString
using ms.remove()/ms.overwrite() instead of string splicing. This
ensures ms.generateMap() produces a sourcemap that matches the final
output after imports are elided.

Also eliminates double-parse (shared analyzeTypeOnlyImports helper),
fixes CRLF handling in trailing newline consumption, and adds a comment
documenting intentional namespace import skip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… counts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/angular-compiler/src/lib/type-elision.ts`:
- Around line 57-68: The collector currently adds all specifiers into
importedNames including ImportNamespaceSpecifier, which later causes the rewrite
path (rebuilding ImportDeclaration) to drop live namespace imports like `* as
ns`; update the analysis loop that populates importedNames (the for loop over
(ast as any).body and node.specifiers) to detect and skip any ImportDeclaration
that contains an ImportNamespaceSpecifier (i.e., if any spec.type ===
'ImportNamespaceSpecifier' then continue and do not add its specifiers to
importedNames), so namespace imports are excluded from type-elision rewriting
and preserved by the rewrite logic that only handles ImportDefaultSpecifier and
ImportSpecifier.
- Around line 30-31: Remove 'TSAsExpression' and 'TSSatisfiesExpression' from
the TYPE_NODE_TYPES array in packages/angular-compiler/src/lib/type-elision.ts
so the walk logic treats their expression subtrees as value-position (allowing
identifiers like imported names to be preserved); update any related comments
and ensure the walker implementation (the walk function referenced in this
module) remains unchanged so it will traverse the expression part normally. Also
add a unit test that asserts an imported identifier used only in a cast (e.g.,
const x = ImportedName as Type) is not elided to prevent regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff7fcac0-0150-4fd9-bb3b-0962d03f9e67

📥 Commits

Reviewing files that changed from the base of the PR and between 788b514 and 189aac5.

📒 Files selected for processing (5)
  • packages/angular-compiler/COMPILER.md
  • packages/angular-compiler/src/lib/compile.ts
  • packages/angular-compiler/src/lib/integration.spec.ts
  • packages/angular-compiler/src/lib/type-elision.spec.ts
  • packages/angular-compiler/src/lib/type-elision.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/angular-compiler/src/lib/integration.spec.ts
  • packages/angular-compiler/src/lib/type-elision.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/angular-compiler/src/lib/compile.ts
  • packages/angular-compiler/COMPILER.md

Comment on lines +30 to +31
'TSAsExpression',
'TSSatisfiesExpression',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the current classification and whether regression coverage exists for `as` / `satisfies`.
sed -n '23,35p' packages/angular-compiler/src/lib/type-elision.ts
sed -n '85,95p' packages/angular-compiler/src/lib/type-elision.ts
rg -n 'TSAsExpression|TSSatisfiesExpression|\bsatisfies\b|\bas\b' \
  packages/angular-compiler/src/lib/type-elision.spec.ts \
  packages/angular-compiler/src/lib/integration.spec.ts

Repository: analogjs/analog

Length of output: 4322


🏁 Script executed:

#!/bin/bash
# Check for test cases involving imported names used in `as` or `satisfies` expressions
rg -A 3 -B 3 'import.*from|as Type|satisfies' packages/angular-compiler/src/lib/type-elision.spec.ts | head -100

Repository: analogjs/analog

Length of output: 2801


🏁 Script executed:

#!/bin/bash
# Get the full walk() function to understand traversal order
sed -n '75,130p' packages/angular-compiler/src/lib/type-elision.ts

Repository: analogjs/analog

Length of output: 1713


🏁 Script executed:

#!/bin/bash
# Check if there are any tests specifically for TSAsExpression or TSSatisfiesExpression classification
rg -n 'as.*import|import.*as' packages/angular-compiler/src/lib/type-elision.spec.ts -A 5

Repository: analogjs/analog

Length of output: 2910


🏁 Script executed:

#!/bin/bash
# Search for any test with imported names in `as` or `satisfies` contexts
rg -B 5 -A 5 'import.*\bas\b|import.*satisfies' packages/angular-compiler/src/lib/type-elision.spec.ts

Repository: analogjs/analog

Length of output: 2644


🏁 Script executed:

#!/bin/bash
# Check the full test case around line 201
sed -n '195,210p' packages/angular-compiler/src/lib/type-elision.spec.ts

Repository: analogjs/analog

Length of output: 717


🏁 Script executed:

#!/bin/bash
# Look for any test involving a variable assignment with `as` that uses an imported name
rg -B 8 -A 2 'const.*=.*as\b' packages/angular-compiler/src/lib/type-elision.spec.ts

Repository: analogjs/analog

Length of output: 641


🏁 Script executed:

#!/bin/bash
# Check the AST structure: does TSAsExpression have a separate "expression" subtree?
# Look at how walk() processes node keys
sed -n '110,130p' packages/angular-compiler/src/lib/type-elision.ts

Repository: analogjs/analog

Length of output: 680


🏁 Script executed:

#!/bin/bash
# Find TYPE_POSITION_KEYS definition
rg -n 'TYPE_POSITION_KEYS' packages/angular-compiler/src/lib/type-elision.ts -A 15

Repository: analogjs/analog

Length of output: 1068


🏁 Script executed:

#!/bin/bash
# Check if there's special handling for 'expression' key in walk logic
sed -n '50,130p' packages/angular-compiler/src/lib/type-elision.ts

Repository: analogjs/analog

Length of output: 2686


Remove TSAsExpression and TSSatisfiesExpression from TYPE_NODE_TYPES.

Both expr as Type and expr satisfies Type evaluate expr at runtime. The current logic marks the entire subtree as type-position, incorrectly classifying imported names used in the expression part as type-only and eliding them. For example, import Foo from 'lib'; const x = Foo as any; would incorrectly elide Foo.

The walk function has no mechanism to distinguish between the value-expression and type-annotation subtrees within these node types. Removing them allows the expression to be walked normally in value-position, preserving the correct classification.

Missing test coverage: no test verifies an imported name used only within a cast expression (e.g., const x = ImportedName as Type;).

Suggested fix
-  'TSAsExpression',
-  'TSSatisfiesExpression',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'TSAsExpression',
'TSSatisfiesExpression',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-compiler/src/lib/type-elision.ts` around lines 30 - 31,
Remove 'TSAsExpression' and 'TSSatisfiesExpression' from the TYPE_NODE_TYPES
array in packages/angular-compiler/src/lib/type-elision.ts so the walk logic
treats their expression subtrees as value-position (allowing identifiers like
imported names to be preserved); update any related comments and ensure the
walker implementation (the walk function referenced in this module) remains
unchanged so it will traverse the expression part normally. Also add a unit test
that asserts an imported identifier used only in a cast (e.g., const x =
ImportedName as Type) is not elided to prevent regressions.

Comment on lines +57 to +68
// Step 1 – collect all value-imported names (skip `import type` / `{ type X }`)
// Namespace imports (`import * as ns`) are intentionally skipped — they are
// always value imports in TypeScript and cannot be type-only at the
// declaration level.
const importedNames = new Set<string>();
for (const node of (ast as any).body) {
if (node.type !== 'ImportDeclaration') continue;
if (node.importKind === 'type') continue; // already type-only
for (const spec of node.specifiers ?? []) {
if (spec.importKind === 'type') continue; // already type-only
importedNames.add(spec.local.name);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify namespace specifiers are still included in analysis and not preserved by the rewrite path.
sed -n '57,68p' packages/angular-compiler/src/lib/type-elision.ts
sed -n '155,208p' packages/angular-compiler/src/lib/type-elision.ts
rg -n '\* as ' \
  packages/angular-compiler/src/lib/type-elision.spec.ts \
  packages/angular-compiler/src/lib/integration.spec.ts

Repository: analogjs/analog

Length of output: 3173


🏁 Script executed:

#!/bin/bash
# Check complete type-elision.ts to understand the flow
wc -l packages/angular-compiler/src/lib/type-elision.ts
cat -n packages/angular-compiler/src/lib/type-elision.ts | head -250

Repository: analogjs/analog

Length of output: 9685


🏁 Script executed:

#!/bin/bash
# Search for any tests involving mixed imports with namespace specifiers
rg -A5 -B5 'import.*\*\s+as.*from' packages/angular-compiler/src/lib/type-elision.spec.ts | head -100

Repository: analogjs/analog

Length of output: 41


Exclude declarations with namespace imports from rewriting.

The analysis phase adds all specifiers (including ImportNamespaceSpecifier) to importedNames, but the rewrite logic only reconstructs ImportDefaultSpecifier and ImportSpecifier types. On import Foo, * as ns from './x' where Foo is type-only, the entire declaration is removed, silently dropping the live ns binding. Namespace imports cannot be type-only and should not reach the rewrite path.

Add a guard to skip any declaration containing a namespace specifier before attempting reconstruction:

Suggested fix
   const allSpecs = node.specifiers ?? [];

+  if (allSpecs.some((s: any) => s.type === 'ImportNamespaceSpecifier')) {
+    continue;
+  }
+
   // Separate default and named specifiers
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-compiler/src/lib/type-elision.ts` around lines 57 - 68, The
collector currently adds all specifiers into importedNames including
ImportNamespaceSpecifier, which later causes the rewrite path (rebuilding
ImportDeclaration) to drop live namespace imports like `* as ns`; update the
analysis loop that populates importedNames (the for loop over (ast as any).body
and node.specifiers) to detect and skip any ImportDeclaration that contains an
ImportNamespaceSpecifier (i.e., if any spec.type === 'ImportNamespaceSpecifier'
then continue and do not add its specifiers to importedNames), so namespace
imports are excluded from type-elision rewriting and preserved by the rewrite
logic that only handles ImportDefaultSpecifier and ImportSpecifier.

brandonroberts and others added 3 commits April 6, 2026 12:30
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@brandonroberts brandonroberts merged commit f66f042 into beta Apr 6, 2026
27 checks passed
@brandonroberts brandonroberts deleted the feat/type-import-elision branch April 6, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope:angular-compiler Changes in @analogjs/angular-compiler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant