Skip to content

Angular 21.2.0 + @oxc-angular/vite 0.0.7 SSR: AngularNodeAppEngine is unusable (missing app-engine manifest) #60

Description

@nkrass

Summary

With Angular 21.2.0 and @oxc-angular/vite 0.0.7, OXC SSR build artifacts can be generated, but SSR cannot use AngularNodeAppEngine from @angular/ssr/node because Angular app-engine manifest data is missing in the OXC output.

This makes OXC SSR currently dependent on CommonEngine fallback in our app.

Environment

  • OS: macOS 15.7.4 (Darwin 24.6.0)
  • Arch: x86_64 (darwin-x64)
  • Node: v22.20.0
  • npm: 10.9.3

Versions

  • @angular/core: ^21.2.0
  • @angular/ssr: ^21.2.0
  • @angular/cli: ^21.2.0
  • @angular-devkit/build-angular: ^21.2.0
  • @oxc-angular/vite: ^0.0.7
  • vite: ^8.0.0-beta.16
  • typescript: ~5.9

Reproduction (project-level)

  1. Build OXC browser + server bundles:
    npm run build:oxc:ssr

Start SSR server with AngularNodeAppEngine path enabled.
Error observed from Angular SSR internals:
txt

Error: Angular app engine manifest is not set. Please ensure you are using the '@angular/build:application' builder to build your server application.
at getAngularAppEngineManifest (...)
at new AngularAppEngine (...)
at new AngularNodeAppEngine (...)
Actual behavior
OXC SSR build completes successfully.
Runtime AngularNodeAppEngine path fails due to missing Angular app-engine manifest.
We had to switch to CommonEngine for OXC SSR runtime.
Expected behavior
Either:
OXC SSR output should include/manually register the manifest required by AngularNodeAppEngine, or
official guidance should clearly state that AngularNodeAppEngine is not currently supported for OXC SSR and CommonEngine is the intended runtime path.
Dist output observation
From OXC SSR artifacts, we get entries like:

dist/.../browser/src/index.oxc.html
dist/.../server/server.mjs
But no Angular app-engine manifest setup compatible with AngularNodeAppEngine.

What we already tried
Upgraded to @oxc-angular/vite@0.0.7 (darwin-x64 binding issue appears resolved).
Kept separate client/server OXC Vite configs.
Forced AngularNodeAppEngine path for verification.
Added fallback to CommonEngine with explicit document resolution.
Added host allowlist handling for SSR to rule out host validation noise.
Verified runtime routes work under CommonEngine (including SSR login callback path).
Additional SSR parity notes (secondary)
Before our app-side shims, we also saw OXC SSR runtime DI/parity issues around:

RouterLinkActive
Forms (NgControlStatus / FormControlName) on SSR route rendering
We can open a separate issue for these if you prefer one issue per problem.

Request
Please confirm:

Is AngularNodeAppEngine support expected with @oxc-angular/vite@0.0.7 + Angular 21.2.0?
If yes, can you share the expected manifest contract/output and recommended config?
If not, please document current SSR support scope and preferred runtime path (CommonEngine only) with known limitations.

Activity

  1. self-assigned this
    on Mar 4, 2026
  2. brandonroberts commented on Mar 4, 2026

    @brandonroberts
    Collaborator

    I don't think the base plugin should be responsible for wiring up Angular SSR manifests

  3. nkrass commented on Mar 6, 2026

    @nkrass
    Author

    I don't think the base plugin should be responsible for wiring up Angular SSR manifests

    If the builder is deemed to be the drop-in support for angular builder, and angular by default provides ssr support out of the box, then I guess integrating ssr support in this package is essential.

  4. Brooooooklyn commented on Mar 6, 2026

    @Brooooooklyn
    Member

    I've added ssr plugin in #74, you can try it

  5. nkrass commented on Mar 7, 2026

    @nkrass
    Author

    Update after upgrading and re-testing with newer OXC Angular SSR support.

    Current status

    This partially fixes the earlier blocker.

    With the updated packages below, the OXC SSR server bundle now does emit Angular SSR manifest hooks, so the previous runtime failure:

    Angular app engine manifest is not set

    is no longer the main blocker.

    However, in our app, AngularNodeAppEngine still does not work fully out of the box. We can now get it working only with several app-side patches/workarounds.

    Real environment

    • OS: macOS 15.7.4
    • Darwin: 24.6.0
    • Arch: x86_64
    • Node: v22.22.0
    • npm: 10.9.4

    Versions used for this retest

    • @angular/core: 21.2.1
    • @angular/ssr: 21.2.1
    • @angular/cli: 21.2.1
    • @angular-devkit/build-angular: 21.2.1
    • @oxc-angular/vite: 0.0.11
    • vite: 8.0.0-beta.16
    • typescript: 5.9.3

    TypeScript native preview / tsgo was not used for this SSR reproduction. This was tested with stable typescript@5.9.3.

    What is fixed compared to the previous report

    The generated OXC SSR bundle now contains Angular manifest registration calls:

    • setAngularAppManifest(...)
    • setAngularAppEngineManifest(...)

    So this part is improved versus the earlier build where AngularNodeAppEngine could not start because the app-engine manifest was missing.

    Remaining bugs / parity issues still requiring fixes upstream

    1. Wrong root selector in generated Angular SSR app manifest

    The OXC-generated Angular app manifest still hardcodes:

    <!DOCTYPE html><html><head></head><body><app-root></app-root></body></html>

    But our actual root component selector is:

    selector: 'mp-root'

    This caused runtime SSR failure with AngularNodeAppEngine until we patched the manifest ourselves.

    Observed runtime failure before the patch:

    • NG05104
    • stack included DefaultDomRenderer2.selectRootElement, locateHostElement, ComponentFactory.create, renderAngular, AngularServerApp.handleRendering

    This looks like a builder/manifest generation bug. The generated index.server.html asset inside the Angular app manifest must use the real app root selector, not a hardcoded <app-root>.

    1. RouterLinkActive SSR parity issue

    We still need an app-side shim for RouterLinkActive.

    Observed behavior:

    • links can be undefined during ngAfterContentInit in the OXC SSR lane
    • this appears to be a content-query metadata/runtime parity problem in the SSR bundle path

    Our workaround was to patch RouterLinkActive.prototype.ngAfterContentInit and initialize:

    this.links = new QueryList<RouterLink>()

    when links is missing.

    1. Angular forms SSR parity issues

    We still need two forms-related shims.

    Observed issues:

    • NgControlStatus could be instantiated before a matching NgControl provider is available
    • this produced NG0201 during SSR route rendering
    • FormControlName could run _setUpControl before the parent form directive was ready

    Our workarounds:

    • patch NgControlStatus.ɵfac to inject NgControl with optional/self-style flags
    • patch FormControlName.prototype._setUpControl to return early if formDirective or addControl is not ready

    `import { ɵɵdirectiveInject } from '@angular/core';
    import { FormControlName, NgControl, NgControlStatus } from '@angular/forms';

    let ngControlStatusShimApplied = false;
    let formControlNameShimApplied = false;

    /**

    • OXC build lane workaround:
    • Some SSR bundles can instantiate NgControlStatus before a matching NgControl
    • provider is available, which throws NG0201 during route rendering.
      */
      export function applyNgControlStatusOxcShim(): void {
      if (ngControlStatusShimApplied) return;
      ngControlStatusShimApplied = true;

    const target = NgControlStatus as unknown as {
    ɵfac?: (type?: unknown) => unknown;
    };

    if (typeof target.ɵfac !== 'function') return;

    target.ɵfac = function NgControlStatus_Factory(ngFactoryType?: unknown) {
    const Type =
    (ngFactoryType as new (cd?: unknown) => unknown) ??
    (NgControlStatus as unknown as new (cd?: unknown) => unknown);
    return new Type(ɵɵdirectiveInject(NgControl as any, 10 as any));
    };
    }

    /**

    • OXC build lane workaround:
    • Some SSR bundles evaluate FormControlName before its parent form directive is
    • fully wired, causing _setUpControl to throw on a null parent.
      */
      export function applyFormControlNameOxcShim(): void {
      if (formControlNameShimApplied) return;
      formControlNameShimApplied = true;

    const prototype = FormControlName.prototype as any;

    if (typeof prototype._setUpControl !== 'function') return;

    const originalSetUpControl = prototype._setUpControl as () => void;
    prototype._setUpControl = function patchedSetUpControl(this: any) {
    if (
    !this.formDirective ||
    typeof this.formDirective.addControl !== 'function'
    ) {
    return;
    }

    return originalSetUpControl.call(this);
    

    };
    }
    `

    1. Apollo Angular generated service DI metadata issue in OXC SSR lane

    We also still need a generated-code patch for Apollo Angular services.

    Observed behavior before the patch:

    • generated GraphQL services could fail DI in the OXC SSR build
    • we saw the generated factory path degrade to ɵɵinvalidFactoryDep(...)
    • runtime symptom was Angular DI failure (NG0202 class of error)

    Our workaround:

    • post-process generated apollo-angular services and change constructors from:
    constructor(apollo: Apollo.Apollo)

    to:

    constructor(@Inject(Apollo.Apollo) apollo: Apollo.Apollo)

    and add:

    import { Inject, Injectable } from '@angular/core';

    So this looks like another OXC SSR metadata / DI parity problem, not an Apollo transport/link issue.

    Exact local workaround needed to make AngularNodeAppEngine work

    We had to override Angular’s app manifest before creating the request engine, replacing the generated index.server.html asset with one that uses the real selector:

    <!DOCTYPE html><html><head></head><body><mp-root></mp-root></body></html>

    After that manifest override, AngularNodeAppEngine started working for our app.

    Validation after applying the workarounds

    Build:

    npm run build:oxc:ssr

    Run:

    SSR_ENGINE=node NG_ALLOWED_HOSTS=127.0.0.1,3bs.store DISABLE_HTTPS_REDIRECT=true PORT=4215 HOST=127.0.0.1 FEED_REFRESH_INTERVAL_MS=0 node dist/portal-oxc/server/server.mjs

    Requests tested:

    • GET /en -> 200
    • GET /en/account/login?cburl=%2Fen%2Fmerchant%2Fdashboard -> 200
    • GET /en/women/all -> 200
    • GET /en/cart -> 302 to /en/account/login?cburl=%2Fen%2Fcart which is expected app behavior

    The returned HTML is real SSR output with <mp-root ...> and rendered page content, not just a server startup success.

    Summary for maintainers

    @oxc-angular/vite@0.0.11 is a real improvement because Angular app-engine manifests are now emitted.

    But for this Angular 21.2.1 app, SSR with AngularNodeAppEngine is still not production-ready out of the box because we still need all of the following local patches:

    • manifest root-selector override (app-root -> actual selector mp-root)
    • RouterLinkActive content-query shim
    • NgControlStatus DI shim
    • FormControlName setup-order shim
    • Apollo generated-service DI patch with explicit @Inject(Apollo.Apollo)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

Priority

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions