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)[\\/]/