From d63332d00925a38055cfe28cebe7251e2f9d8372 Mon Sep 17 00:00:00 2001 From: Ashley Hunter Date: Fri, 2 Oct 2026 11:43:34 +0100 Subject: [PATCH] =?UTF-8?q?fix(linker):=20link=20files=20whose=20=C9=B5=20?= =?UTF-8?q?identifiers=20are=20written=20as=20\u0275=20escapes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit esbuild's default minify (`--charset=ascii`) writes `ɵ` in identifiers as `\u0275`, e.g. `e.\u0275\u0275ngDeclareComponent`. The raw-text `ɵɵngDeclare` prefilter in `link()` and in the Vite linker plugin never matched such files, so they were skipped and the library fell back to JIT at runtime. Match `ngDeclare` without the prefix instead. The AST walk already compares decoded identifier names, so escaped input now links exactly like the unescaped input. --- crates/oxc_angular_compiler/src/linker/mod.rs | 23 +++++++++++++++++-- napi/angular-compiler/test/linker.test.ts | 15 ++++++++++++ .../vite-plugin/angular-linker-plugin.ts | 3 ++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/oxc_angular_compiler/src/linker/mod.rs b/crates/oxc_angular_compiler/src/linker/mod.rs index 3c63ff805..3f98d7241 100644 --- a/crates/oxc_angular_compiler/src/linker/mod.rs +++ b/crates/oxc_angular_compiler/src/linker/mod.rs @@ -82,8 +82,9 @@ pub struct LinkResult { /// Scans the code for `ɵɵngDeclare*` calls and replaces them with their /// fully compiled equivalents. pub fn link(allocator: &Allocator, code: &str, filename: &str) -> LinkResult { - // Quick check: if no declarations, return early - if !code.contains("\u{0275}\u{0275}ngDeclare") { + // Quick check: if no declarations, return early. Match without the `ɵɵ` prefix because + // minifiers may write it as `\u0275` escapes; the AST below has them decoded. + if !code.contains("ngDeclare") { return LinkResult { code: code.to_string(), map: None, linked: false }; } @@ -4053,4 +4054,22 @@ MyDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.0 result.code ); } + + #[test] + fn test_link_unicode_escaped_identifiers() { + // esbuild's default `--charset=ascii` writes `ɵ` in identifiers as `\u0275`. + let utf8 = r#" +import * as e from "@angular/core"; +class Hi { static ɵfac = e.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: e, type: Hi, deps: [], target: e.ɵɵFactoryTarget.Directive }); static ɵdir = e.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0", type: Hi, isStandalone: true, selector: "[libHi]", inputs: { on: "on" }, host: { properties: { "class.on": "on" } }, ngImport: e }); } +e.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: e, type: Hi, decorators: [{ type: Directive, args: [{ selector: "[libHi]" }] }] }); +class Greet { static ɵcmp = e.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.0", type: Greet, isStandalone: true, selector: "lib-greet", ngImport: e, template: '

Hello {{ name }}

' }); } +"#; + let escaped = utf8.replace('\u{0275}', "\\u0275"); + let allocator = Allocator::default(); + let expected = link(&allocator, utf8, "test.mjs"); + let result = link(&allocator, &escaped, "test.mjs"); + assert!(expected.linked); + assert!(result.linked, "Escaped input should be linked, got:\n{}", result.code); + assert_eq!(result.code.replace("\\u0275", "\u{0275}"), expected.code); + } } diff --git a/napi/angular-compiler/test/linker.test.ts b/napi/angular-compiler/test/linker.test.ts index 4a0356177..d467cdabd 100644 --- a/napi/angular-compiler/test/linker.test.ts +++ b/napi/angular-compiler/test/linker.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest' import { linkAngularPackageSync } from '../index.js' +import { angularLinkerPlugin } from '../vite-plugin/angular-linker-plugin.js' /** * Minimal Angular partial declaration fixtures that simulate the structure @@ -126,6 +127,20 @@ describe('Angular linker - chunk file linking', () => { expect(result.code).not.toContain('\u0275\u0275ngDeclare') }) + it('should link through the Vite plugin when \u0275 is written as an escape', async () => { + // esbuild's default `--charset=ascii` minify writes `ɵ` in identifiers as `\u0275`. + const code = PIPE_CHUNK.replaceAll('\u0275', '\\u0275') + const plugin = angularLinkerPlugin() as any + const result = await plugin.transform.handler.call( + {}, + code, + 'node_modules/@angular/common/fesm2022/_pipes-chunk.mjs', + ) + + expect(result?.code).toContain('definePipe') + expect(result.code).not.toContain('ngDeclare') + }) + it('should return linked: false for files without declarations', () => { const code = ` export function helper() { return 42; } diff --git a/napi/angular-compiler/vite-plugin/angular-linker-plugin.ts b/napi/angular-compiler/vite-plugin/angular-linker-plugin.ts index 954b727bb..e4c1eb3ff 100644 --- a/napi/angular-compiler/vite-plugin/angular-linker-plugin.ts +++ b/napi/angular-compiler/vite-plugin/angular-linker-plugin.ts @@ -19,7 +19,8 @@ import type { Plugin } from 'vite' * 2. During Vite's transform pipeline for non-optimized node_modules files */ -const LINKER_DECLARATION_PREFIX = '\u0275\u0275ngDeclare' +// No `ɵɵ` prefix: minifiers may write it as `\u0275` escapes. +const LINKER_DECLARATION_PREFIX = 'ngDeclare' // Skip these packages - they don't need linking const SKIP_REGEX = /[\\/]@angular[\\/](?:compiler|core)[\\/]/