mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(compiler): add support for host directives under isolatedDeclarations
Removes restrictions around using external references and local directives in `hostDirectives` under isolated declarations mode (`emitDeclarationOnly: true`). By wrapping the host directive reference in a `WrappedNodeExpr`, TypeScript's declaration emitter seamlessly emits `typeof hostReference.node`, preserving existing imports or local identifiers exactly as authored. Also adds support for translating `PropertyAccessExpression` inside `WrappedNodeExpr` into `QualifiedName` for `.d.ts` emission, ensuring namespace imports (`import * as n from './dir'`) are preserved correctly.
This commit is contained in:
committed by
Jessica Janiuk
parent
0152e3cbdf
commit
5d2b1c4100
@@ -2097,29 +2097,7 @@ function extractHostDirectives(
|
||||
`In ${compilationModeName} mode, host directive cannot be an expression. Use an identifier instead`,
|
||||
);
|
||||
}
|
||||
|
||||
if (emitDeclarationOnly) {
|
||||
if (ts.isIdentifier(hostReference.node)) {
|
||||
const importInfo = reflector.getImportOfIdentifier(hostReference.node);
|
||||
if (importInfo) {
|
||||
directive = new ExternalReference(importInfo.from, importInfo.name);
|
||||
} else {
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION,
|
||||
hostReference.node,
|
||||
`In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION,
|
||||
hostReference.node,
|
||||
`In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
directive = new WrappedNodeExpr(hostReference.node);
|
||||
}
|
||||
directive = new WrappedNodeExpr(hostReference.node);
|
||||
} else if (hostReference instanceof Reference) {
|
||||
directive = hostReference as Reference<ClassDeclaration>;
|
||||
nameForErrors = (fieldName: string) =>
|
||||
|
||||
@@ -252,6 +252,16 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
|
||||
const node: ts.Node = ast.node;
|
||||
if (ts.isEntityName(node)) {
|
||||
return ts.factory.createTypeReferenceNode(this.routeEntityNameThroughImportManager(node));
|
||||
} else if (ts.isPropertyAccessExpression(node)) {
|
||||
const entityName = expressionToEntityName(node);
|
||||
if (entityName !== null) {
|
||||
return ts.factory.createTypeReferenceNode(
|
||||
this.routeEntityNameThroughImportManager(entityName),
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Unsupported PropertyAccessExpression in TypeTranslatorVisitor: ${node.getText()} in ${node.getSourceFile()?.fileName}`,
|
||||
);
|
||||
} else if (ts.isTypeNode(node)) {
|
||||
// The wrapped type node may reference identifiers from another source file (e.g. when the
|
||||
// NgModule isolated-declarations transform synthesizes `ReturnType<typeof Foo.forRoot>`).
|
||||
@@ -423,3 +433,14 @@ function replaceLeftmostEntityName(name: ts.EntityName, newLeftmost: ts.EntityNa
|
||||
name.right,
|
||||
);
|
||||
}
|
||||
|
||||
function expressionToEntityName(expr: ts.Expression): ts.EntityName | null {
|
||||
if (ts.isIdentifier(expr)) {
|
||||
return ts.factory.createIdentifier(expr.text);
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
||||
const left = expressionToEntityName(expr.expression);
|
||||
return left === null ? null : ts.factory.createQualifiedName(left, expr.name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+35
-15
@@ -3,9 +3,7 @@
|
||||
"cases": [
|
||||
{
|
||||
"description": "should create a basic hostDirectives definition",
|
||||
"inputFiles": [
|
||||
"basic_host_directives.ts"
|
||||
],
|
||||
"inputFiles": ["basic_host_directives.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"failureMessage": "Incorrect definition",
|
||||
@@ -16,13 +14,17 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"compilationModeFilter": [
|
||||
"full compile",
|
||||
"linked compile",
|
||||
"local compile",
|
||||
"declaration-only emit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "should create hostDirective definitions for a chain of host directives",
|
||||
"inputFiles": [
|
||||
"chained_host_directives.ts"
|
||||
],
|
||||
"inputFiles": ["chained_host_directives.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"failureMessage": "Incorrect definition",
|
||||
@@ -33,13 +35,17 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"compilationModeFilter": [
|
||||
"full compile",
|
||||
"linked compile",
|
||||
"local compile",
|
||||
"declaration-only emit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "should handle a forwardRef used in hostDirectives",
|
||||
"inputFiles": [
|
||||
"forward_ref_host_directives.ts"
|
||||
],
|
||||
"inputFiles": ["forward_ref_host_directives.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"failureMessage": "Incorrect definition",
|
||||
@@ -50,13 +56,17 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"compilationModeFilter": [
|
||||
"full compile",
|
||||
"linked compile",
|
||||
"local compile",
|
||||
"declaration-only emit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "should handle the `inputs` and `outputs` options in host directives",
|
||||
"inputFiles": [
|
||||
"host_directives_with_inputs_outputs.ts"
|
||||
],
|
||||
"inputFiles": ["host_directives_with_inputs_outputs.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"failureMessage": "Incorrect definition",
|
||||
@@ -67,13 +77,17 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"compilationModeFilter": [
|
||||
"full compile",
|
||||
"linked compile",
|
||||
"local compile",
|
||||
"declaration-only emit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "should handle aliases to aliased `inputs` and `outputs` of a host directive",
|
||||
"inputFiles": [
|
||||
"host_directives_with_host_aliases.ts"
|
||||
],
|
||||
"inputFiles": ["host_directives_with_host_aliases.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"failureMessage": "Incorrect definition",
|
||||
@@ -84,6 +98,12 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"compilationModeFilter": [
|
||||
"full compile",
|
||||
"linked compile",
|
||||
"local compile",
|
||||
"declaration-only emit"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -445,7 +445,59 @@ runInEachFileSystem(() => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should show correct error message when using an indirect external reference in a simple host directive on a component', () => {
|
||||
it('should emit type declarations containing external reference via namespace import in host directive on a component', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
import {Component} from '@angular/core';
|
||||
import * as n from './dir';
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
selector: 'host-comp',
|
||||
hostDirectives: [n.Dir],
|
||||
})
|
||||
export class HostComp {}
|
||||
`,
|
||||
);
|
||||
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDeclaration<HostComp, "host-comp", never, {}, {}, never, never, true, [{ directive: typeof n.Dir; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit type declarations containing external reference with inputs and outputs in host directive on a component', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
import {Component} from '@angular/core';
|
||||
import {Dir} from './dir';
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
selector: 'host-comp',
|
||||
hostDirectives: [{
|
||||
directive: Dir,
|
||||
inputs: ['a: b'],
|
||||
outputs: ['c: d'],
|
||||
}],
|
||||
})
|
||||
export class HostComp {}
|
||||
`,
|
||||
);
|
||||
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDeclaration<HostComp, "host-comp", never, {}, {}, never, never, true, [{ directive: typeof i1.Dir; inputs: { "a": "b"; }; outputs: { "c": "d"; }; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit type declarations when using an indirect external reference in a simple host directive on a component', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -463,16 +515,15 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION));
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe(
|
||||
'In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead',
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDeclaration<HostComp, "host-comp", never, {}, {}, never, never, true, [{ directive: typeof DirIndirect; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show correct error message when using an indirect external reference in host directive object on a component', () => {
|
||||
it('should emit type declarations when using an indirect external reference in host directive object on a component', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -492,16 +543,15 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION));
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe(
|
||||
'In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead',
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDeclaration<HostComp, "host-comp", never, {}, {}, never, never, true, [{ directive: typeof DirIndirect; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show correct error message when using an indirect external reference in a simple host directive on a directive', () => {
|
||||
it('should emit type declarations when using an indirect external reference in a simple host directive on a directive', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -518,16 +568,15 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION));
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe(
|
||||
'In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead',
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵdir: i0.ɵɵDirectiveDeclaration<HostDir, "[host-dir]", never, {}, {}, never, never, true, [{ directive: typeof DirIndirect; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show correct error message when using an indirect external reference in host directive object on a directive', () => {
|
||||
it('should emit type declarations when using an indirect external reference in host directive object on a directive', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -546,16 +595,15 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION));
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe(
|
||||
'In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead',
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵdir: i0.ɵɵDirectiveDeclaration<HostDir, "[host-dir]", never, {}, {}, never, never, true, [{ directive: typeof DirIndirect; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show correct error message when using a property access expression resolving to an indirect external reference in a simple host directive on a component', () => {
|
||||
it('should emit type declarations when using a property access expression resolving to an indirect external reference in a simple host directive on a component', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -575,16 +623,15 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION));
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe(
|
||||
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDeclaration<HostComp, "host-comp", never, {}, {}, never, never, true, [{ directive: typeof DIR.Dir; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show correct error message when using a property access expression resolving to an indirect external reference in host directive object on a component', () => {
|
||||
it('should emit type declarations when using a property access expression resolving to an indirect external reference in host directive object on a component', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -606,16 +653,15 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION));
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe(
|
||||
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵcmp: i0.ɵɵComponentDeclaration<HostComp, "host-comp", never, {}, {}, never, never, true, [{ directive: typeof DIR.Dir; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show correct error message when using a property access expression resolving to an indirect external reference in a simple host directive on a directive', () => {
|
||||
it('should emit type declarations when using a property access expression resolving to an indirect external reference in a simple host directive on a directive', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -634,16 +680,15 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION));
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe(
|
||||
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵdir: i0.ɵɵDirectiveDeclaration<HostDir, "[host-dir]", never, {}, {}, never, never, true, [{ directive: typeof DIR.Dir; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show correct error message when using a property access expression resolving to an indirect external reference in host directive object on a directive', () => {
|
||||
it('should emit type declarations when using a property access expression resolving to an indirect external reference in host directive object on a directive', () => {
|
||||
env.write(
|
||||
'test.ts',
|
||||
`
|
||||
@@ -664,12 +709,11 @@ runInEachFileSystem(() => {
|
||||
`,
|
||||
);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
env.driveMain();
|
||||
const dtsContent = env.getContents('test.d.ts');
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION));
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe(
|
||||
'In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead',
|
||||
expect(dtsContent).toContain(
|
||||
'static ɵdir: i0.ɵɵDirectiveDeclaration<HostDir, "[host-dir]", never, {}, {}, never, never, true, [{ directive: typeof DIR.Dir; inputs: {}; outputs: {}; }]>;',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user