OXC compiles one file at a time. ngtsc has the whole program, so its static evaluator follows an import into the other file and reads the value. OXC can't do that. Since #493 (fix/decorator-metadata-inputs-outputs) and #495 (feat/decorator-metadata-queries), OXC reports a clear error in these cases. Main silently emits wrong metadata.
Error: @Directive.inputs depends on 'INPUTS', which is imported from another module. OXC compiles one file at a time and cannot evaluate values from other files.
This issue tracks the gap and possible ways to close it.
Affected forms (other.ts exports a plain const)
| Source |
ngtsc 22.1.7 |
OXC #504 tip |
OXC main |
@Directive({inputs: INPUTS}), INPUTS = ['a'] |
inputs: { a: "a" } |
error |
input dropped (no inputs) |
@Directive({inputs: [NAME]}), NAME = 'a' |
inputs: { a: "a" } |
error |
input dropped |
@Directive({outputs: OUTPUTS}), OUTPUTS = ['o: oo'] |
outputs: { o: "oo" } |
error |
output dropped |
@Input(OPTS) a, OPTS = {alias: 'b', required: true} |
inputs: { a: [0, "b", "a"] } ("required": true in .d.ts) |
error |
inputs:{a:"a"} (alias and required ignored) |
@Input({alias: ALIAS}) a, ALIAS = 'b' |
inputs: { a: [0, "b", "a"] } |
error |
inputs:{a:"a"} |
@Output(ONAME) o, ONAME = 'oo' |
outputs: { o: "oo" } |
error |
outputs:{o:"o"} |
@ViewChild('a', {static: STATIC}), STATIC = true |
ɵɵviewQuery(_c0, 7) |
error |
ɵɵviewQuery(_c0,5) (not static) |
@Input({transform: T}), T a function in other.ts |
.d.ts: ngAcceptInputType_a: string |
ngAcceptInputType_a: unknown |
unknown |
@ViewChild(SEL), SEL = 'a' |
ɵɵviewQuery(_c0, 5), _c0 = ["a"] |
ɵɵviewQuery(SEL,5) |
same as tip |
The last row works at runtime (ɵɵviewQuery accepts a string predicate), so only the shape differs.
These are not gaps, because ngtsc rejects them too and the tip matches it:
@ViewChild('a', QOPTS) → NG1001: @ViewChild options must be an object literal
queries: Q → NG1010: Decorator queries metadata must be an object literal
input(0, {alias: ALIAS}) → NG1010: Alias needs to be a string that is statically analyzable. (OXC misses this one; it's in the diagnostics issue)
Example:
// test.ts
import {Directive} from '@angular/core';
import {INPUTS} from './other';
@Directive({selector: '[d]', inputs: INPUTS})
export class D { a: any; }
// other.ts
export const INPUTS = ['a'];
Where
- The error comes from
crates/oxc_angular_compiler/src/directive/decorator.rs (~L706). The static evaluator is directive/evaluator.rs. Both are in the stack.
Possible approaches (not decided)
- Resolver hook from the Vite plugin.
transformAngularFile takes a callback, or a precomputed map, that returns the value of an imported binding (specifier + exportName). The plugin resolves with Vite's resolver and reads the file. There is already a similar input, TransformOptions::resolved_imports (identifier → path), which only rewrites import paths today.
- Reuse
component/cross_file_elision.rs. Behind the cross_file_elision feature (off by default, "intended for compare test purposes only") it already resolves imports with oxc_resolver (tsconfig paths included), parses the target with oxc_parser, and walks re-exports. It could be extended to evaluate exported const initializers and feed the result to the evaluator.
- Leave it as is. Keep the error and document the workaround: inline the value, or declare it in the same file.
For 1 and 2: the importing file has to be re-transformed when the other file changes. TransformResult::dependencies exists today for template and style files, and the Vite plugin links some of them to this.addWatchFile. A dependency on another file's value would need the same kind of wiring. Cycles, re-exports, export *, values that are not literals, and .d.ts-only packages need a defined fallback (keep today's error).
OXC compiles one file at a time. ngtsc has the whole program, so its static evaluator follows an import into the other file and reads the value. OXC can't do that. Since #493 (
fix/decorator-metadata-inputs-outputs) and #495 (feat/decorator-metadata-queries), OXC reports a clear error in these cases. Main silently emits wrong metadata.This issue tracks the gap and possible ways to close it.
Affected forms (
other.tsexports a plainconst)@Directive({inputs: INPUTS}),INPUTS = ['a']inputs: { a: "a" }inputs)@Directive({inputs: [NAME]}),NAME = 'a'inputs: { a: "a" }@Directive({outputs: OUTPUTS}),OUTPUTS = ['o: oo']outputs: { o: "oo" }@Input(OPTS) a,OPTS = {alias: 'b', required: true}inputs: { a: [0, "b", "a"] }("required": truein.d.ts)inputs:{a:"a"}(alias and required ignored)@Input({alias: ALIAS}) a,ALIAS = 'b'inputs: { a: [0, "b", "a"] }inputs:{a:"a"}@Output(ONAME) o,ONAME = 'oo'outputs: { o: "oo" }outputs:{o:"o"}@ViewChild('a', {static: STATIC}),STATIC = trueɵɵviewQuery(_c0, 7)ɵɵviewQuery(_c0,5)(not static)@Input({transform: T}),Ta function in other.ts.d.ts:ngAcceptInputType_a: stringngAcceptInputType_a: unknownunknown@ViewChild(SEL),SEL = 'a'ɵɵviewQuery(_c0, 5),_c0 = ["a"]ɵɵviewQuery(SEL,5)The last row works at runtime (
ɵɵviewQueryaccepts a string predicate), so only the shape differs.These are not gaps, because ngtsc rejects them too and the tip matches it:
@ViewChild('a', QOPTS)→NG1001: @ViewChild options must be an object literalqueries: Q→NG1010: Decorator queries metadata must be an object literalinput(0, {alias: ALIAS})→NG1010: Alias needs to be a string that is statically analyzable.(OXC misses this one; it's in the diagnostics issue)Example:
Where
crates/oxc_angular_compiler/src/directive/decorator.rs(~L706). The static evaluator isdirective/evaluator.rs. Both are in the stack.Possible approaches (not decided)
transformAngularFiletakes a callback, or a precomputed map, that returns the value of an imported binding (specifier+exportName). The plugin resolves with Vite's resolver and reads the file. There is already a similar input,TransformOptions::resolved_imports(identifier → path), which only rewrites import paths today.component/cross_file_elision.rs. Behind thecross_file_elisionfeature (off by default, "intended for compare test purposes only") it already resolves imports withoxc_resolver(tsconfig paths included), parses the target withoxc_parser, and walks re-exports. It could be extended to evaluate exportedconstinitializers and feed the result to the evaluator.For 1 and 2: the importing file has to be re-transformed when the other file changes.
TransformResult::dependenciesexists today for template and style files, and the Vite plugin links some of them tothis.addWatchFile. A dependency on another file's value would need the same kind of wiring. Cycles, re-exports,export *, values that are not literals, and.d.ts-only packages need a defined fallback (keep today's error).