Skip to content

Commit fe78bfe

Browse files
committed
fix(vite-plugin-angular): keep nested class metadata in scope
setClassMetadata calls were appended at module scope, so a decorated class nested in a function threw a ReferenceError in dev mode. Emit them right after the class when it is not top-level. parseOxc now falls back to the default transfer if raw transfer throws, e.g. when its buffer cannot be allocated.
1 parent b61f376 commit fe78bfe

4 files changed

Lines changed: 45 additions & 10 deletions

File tree

‎packages/vite-plugin-angular/src/lib/compiler/COMPILER.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ The existing vite-plugin-angular plugins (build optimizer, router, vitest, etc.)
7979
| `styles.ts` | Style preprocessing via Vite's `preprocessCSS` (OXC-based extraction) |
8080
| `type-elision.ts` | OXC-based type-only import detection and elision (usage analysis, no type-checker needed) |
8181
| `constants.ts` | Shared Angular name sets (`ANGULAR_DECORATORS`, `COMPILABLE_DECORATORS`, `FIELD_DECORATORS`, `SIGNAL_APIS`) — single source of truth to prevent drift |
82-
| `utils.ts` | Type-only import detection (syntactic), class finder, `forwardRef` unwrapper (TS + OXC versions) |
82+
| `utils.ts` | `parseOxc` (raw-transfer parse with fallback), OXC class finder and walker, `forwardRef` unwrapper |
8383

8484
## What's Supported
8585

‎packages/vite-plugin-angular/src/lib/compiler/compile.spec.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,21 @@ describe('NgLite Compiler', () => {
4040
`export function setup() {
4141
@NgModule({ providers: [] })
4242
class TestModule {}
43-
return TestModule;
43+
@Component({ selector: 'x-nested', template: '<p>{{ v }}</p>' })
44+
class Nested { v = input(0); }
45+
return [TestModule, Nested];
4446
}`,
4547
'setup.ts',
4648
);
4749
expect(result).toContain('static ɵmod');
50+
expect(result).toContain('static ɵcmp');
4851
expect(result).not.toContain('@NgModule');
52+
// setClassMetadata references the class, so it must stay inside setup().
53+
const scopeEnd = result.indexOf('return [TestModule, Nested]');
54+
for (const cls of ['TestModule', 'Nested']) {
55+
const idx = result.indexOf(`ɵsetClassMetadata(${cls},`);
56+
expect(idx).toBeGreaterThan(-1);
57+
expect(idx).toBeLessThan(scopeEnd);
58+
}
4959
});
5060
});

‎packages/vite-plugin-angular/src/lib/compiler/compile.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,14 @@ export function compile(
345345
ivyCode: string[]; // "static ɵfac = ...", "static ɵcmp = ...", etc.
346346
decorators: any[]; // Angular decorators to remove (OXC nodes)
347347
classEnd: number; // Position of closing } in original source
348+
// setClassMetadata calls for classes nested in a function scope, which
349+
// must be emitted next to the class rather than at module scope.
350+
scopedMetadata: o.Statement[];
348351
}
349352
const classResults: ClassCompileResult[] = [];
353+
const topLevelClasses = new Set(
354+
(oxcProgram.body as any[]).map((stmt) => toTopLevelStatement(stmt).decl),
355+
);
350356
// Track synthetic imports needed for NgModule export expansion.
351357
// Maps exported class name → import specifier.
352358
const syntheticImports = new Map<string, string>();
@@ -1187,6 +1193,7 @@ export function compile(
11871193
typeOnlyImports,
11881194
);
11891195
const propDecorators = buildPropDecorators(node, sourceCode, 'i0.');
1196+
const scopedMetadata: o.Statement[] = [];
11901197
angularDecorators.forEach((dec) => {
11911198
const decName = decoratorCallName(dec, sourceCode)!;
11921199
const decArgsNode = dec.expression.arguments[0];
@@ -1283,9 +1290,12 @@ export function compile(
12831290
const classMetadataExpr = isPartial
12841291
? compileDeclareClassMetadata(classMetaInput)
12851292
: compileClassMetadata(classMetaInput);
1286-
constantPool.statements.push(
1287-
new o.ExpressionStatement(classMetadataExpr),
1288-
);
1293+
const metadataStmt = new o.ExpressionStatement(classMetadataExpr);
1294+
if (topLevelClasses.has(node)) {
1295+
constantPool.statements.push(metadataStmt);
1296+
} else {
1297+
scopedMetadata.push(metadataStmt);
1298+
}
12891299
} catch (e) {
12901300
// Skip if compileClassMetadata fails — surfaced via DEBUG=analog-fast-compile
12911301
if (debugCompile.enabled) {
@@ -1315,6 +1325,7 @@ export function compile(
13151325
ivyCode,
13161326
decorators: [...angularDecorators, ...memberDecorators],
13171327
classEnd: node.end,
1328+
scopedMetadata,
13181329
});
13191330
}
13201331

@@ -1439,6 +1450,10 @@ export function compile(
14391450
const memberCode = cr.ivyCode.map((c) => ' ' + c + ';').join('\n');
14401451
ms.appendLeft(cr.classEnd - 1, '\n' + memberCode + '\n');
14411452
}
1453+
1454+
for (const stmt of cr.scopedMetadata) {
1455+
ms.appendLeft(cr.classEnd, `\n/*@__PURE__*/ ${emitAngularStmt(stmt)}`);
1456+
}
14421457
}
14431458

14441459
// 3. Emit constant pool statements.

‎packages/vite-plugin-angular/src/lib/compiler/utils.ts‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,23 @@ import {
88

99
// Raw transfer reads the AST straight from Rust memory instead of going
1010
// through JSON (~3x faster). Not yet in oxc-parser's published types.
11-
const parserOptions = {
12-
experimentalRawTransfer: rawTransferSupported(),
13-
} as ParserOptions;
11+
const rawTransferOptions = { experimentalRawTransfer: true } as ParserOptions;
12+
let useRawTransfer = rawTransferSupported();
1413

15-
/** Parse with oxc-parser, using raw transfer when the platform supports it. */
14+
/**
15+
* Parse with oxc-parser, using raw transfer when the platform supports it.
16+
* Falls back to the default transfer for this and later calls if raw
17+
* transfer throws (e.g. its large buffer cannot be allocated).
18+
*/
1619
export function parseOxc(fileName: string, code: string): ParseResult {
17-
return parseSync(fileName, code, parserOptions);
20+
if (useRawTransfer) {
21+
try {
22+
return parseSync(fileName, code, rawTransferOptions);
23+
} catch {
24+
useRawTransfer = false;
25+
}
26+
}
27+
return parseSync(fileName, code);
1828
}
1929

2030
/** Recursively find all class declarations in an OXC AST, including nested scopes. */

0 commit comments

Comments
 (0)