voidzero-dev / voidzero-dev/oxc-angular-compiler

fix(defer): emit lazy dependencies for components used only inside @defer blocks

Open
#313 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug rust
Dominant language
Rust
Stars
228
Forks
20
Avg merge
1d 15h
Merged PRs (30d)
36

Description

Summary

Angular's @defer block is supposed to lazy-load any standalone component that appears only inside it — the user gets import().then(m => m.X) instead of an eager import at the top of the file. OXC currently emits these references through the regular dependencies factory, so @defer parses correctly but does not actually defer: the deferred component lands in the initial bundle and the lazy boundary is lost.

This is the default ngc behavior and is not opt-in via @Component.deferredImports — the recent bb2735b commit added the explicit-deferredImports path but the implicit detection is still missing.

Reference behavior

ngc at packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:2344-2475 (collectDeferredSymbols / surrounding logic):

  1. Walks the component's template AST and collects every standalone import (scope.directives, scope.pipes).
  2. For each import, checks whether all references appear inside DeferredBlock nodes — if so, the import is added to allDeferredDecls.
  3. Routes those imports through DeferBlockDepsEmitMode.PerComponent (or PerBlock for fine-grained mode), producing lazy import().then(...) resolvers instead of regular dependency entries.
  4. The normal dependencies factory is built from the non-deferred imports only.

Test that demonstrates the gap (in napi/angular-compiler/test/analog-compat/component.spec.ts:3262):

import { Component } from '@angular/core';
import { LazyCmp } from './lazy';
@Component({
  selector: 'app-parent',
  imports: [LazyCmp],
  template: '@defer { <app-lazy/> }',
})
export class Parent {}

Expected output (matching ngc):

ɵcmp = ɵɵdefineComponent({
  ...,
  dependencies: ɵɵgetComponentDepsFactory(Parent, []),
  deferBlockDependencies: [() => import('./lazy').then(m => m.LazyCmp)],
});

Current OXC output:

ɵcmp = ɵɵdefineComponent({
  ...,
  dependencies: ɵɵgetComponentDepsFactory(Parent, [LazyCmp]),  // eager
});

Required work

  1. Defer-only detection — extend the existing template walk in crates/oxc_angular_compiler/src/component/defer_resolver.rs to flag each imports: [...] entry that is referenced only inside DeferredBlock AST nodes.
  2. Route to lazy emit — currently defer_resolver.rs only handles @Component.deferredImports. Generalize it to also accept implicitly-deferred imports from step 1.
  3. Emit deferBlockDependencies — wire the lazy import().then(m => m.X) resolver into the defineComponent emit. Use original export name (not local alias) — see napi/angular-compiler/test/analog-compat/component.spec.ts:3284 for the aliased-import case.
  4. Default-import shapem.default rather than m.X for default-imported components — covered by napi/angular-compiler/test/analog-compat/component.spec.ts:3312.

Tests

The four currently-failing tests under @defer dependency import shape in component.spec.ts (3262, 3284, 3312, plus the @defer inside @switch/@case cases at 1764-area) cover the matrix.

Reference

  • ngc impl: packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts:1312 (DeferBlockDepsEmitMode.PerComponent), :2344-2475 (allDeferredDecls collection)
  • DefersBlock AST: packages/compiler/src/render3/r3_ast.ts
  • OXC defer entry: crates/oxc_angular_compiler/src/component/defer_resolver.rs (added in #307)
  • Related upstream commit: bb2735b feat(defer): emit lazy resolver and async metadata for @Component.deferredImports (#307) — explicit-deferredImports path; this issue covers the implicit case

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in crates/oxc_angular_compiler/src/component/defer_resolver.rs and compare its existing explicit-deferredImports handling with ngc's collectDeferredSymbols logic in handler.ts:2344-2475. Run the @defer dependency import shape cases in napi/angular-compiler/test/analog-compat/component.spec.ts, including the aliased, default-import, and nested @switch cases. Done means deferred-only imports produce deferBlockDependencies with the correct lazy export shape while regular dependencies exclude them.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, rust, typescript
Domain
compilers, frontend, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.