fix(compiler): JIT mode incorrectly interpreting host directive configuration in partial compilation (#57002)

Fixes that the runtime implementation of `ɵɵngDeclareDirective` was interpreting the `hostDirectives` mapping incorrectly. Instead of treating the inputs/outputs as `['binding', 'alias']` arrays, it was parsing them as `['binding: alias']`. This was leading to runtime errors if a user is consuming a partially-compiled library in JIT mode.

Fixes #54096.

PR Close #57002
This commit is contained in:
Kristiyan Kostadinov 2024-07-16 16:56:56 +02:00 committed by Andrew Scott
parent 6cfc4d8c85
commit daf0317bdc
2 changed files with 86 additions and 22 deletions

View file

@ -497,6 +497,26 @@ function convertDirectiveFacadeToMetadata(facade: R3DirectiveMetadataFacade): R3
}
}
const hostDirectives = facade.hostDirectives?.length
? facade.hostDirectives.map((hostDirective) => {
return typeof hostDirective === 'function'
? {
directive: wrapReference(hostDirective),
inputs: null,
outputs: null,
isForwardReference: false,
}
: {
directive: wrapReference(hostDirective.directive),
isForwardReference: false,
inputs: hostDirective.inputs ? parseMappingStringArray(hostDirective.inputs) : null,
outputs: hostDirective.outputs
? parseMappingStringArray(hostDirective.outputs)
: null,
};
})
: null;
return {
...facade,
typeArgumentCount: 0,
@ -512,7 +532,7 @@ function convertDirectiveFacadeToMetadata(facade: R3DirectiveMetadataFacade): R3
providers: facade.providers != null ? new WrappedNodeExpr(facade.providers) : null,
viewQueries: facade.viewQueries.map(convertToR3QueryMetadata),
fullInheritance: false,
hostDirectives: convertHostDirectivesToMetadata(facade),
hostDirectives,
};
}
@ -520,6 +540,15 @@ function convertDeclareDirectiveFacadeToMetadata(
declaration: R3DeclareDirectiveFacade,
typeSourceSpan: ParseSourceSpan,
): R3DirectiveMetadata {
const hostDirectives = declaration.hostDirectives?.length
? declaration.hostDirectives.map((dir) => ({
directive: wrapReference(dir.directive),
isForwardReference: false,
inputs: dir.inputs ? getHostDirectiveBindingMapping(dir.inputs) : null,
outputs: dir.outputs ? getHostDirectiveBindingMapping(dir.outputs) : null,
}))
: null;
return {
name: declaration.type.name,
type: wrapReference(declaration.type),
@ -540,7 +569,7 @@ function convertDeclareDirectiveFacadeToMetadata(
fullInheritance: false,
isStandalone: declaration.isStandalone ?? false,
isSignal: declaration.isSignal ?? false,
hostDirectives: convertHostDirectivesToMetadata(declaration),
hostDirectives,
};
}
@ -558,28 +587,19 @@ function convertHostDeclarationToMetadata(
};
}
function convertHostDirectivesToMetadata(
metadata: R3DeclareDirectiveFacade | R3DirectiveMetadataFacade,
): R3HostDirectiveMetadata[] | null {
if (metadata.hostDirectives?.length) {
return metadata.hostDirectives.map((hostDirective) => {
return typeof hostDirective === 'function'
? {
directive: wrapReference(hostDirective),
inputs: null,
outputs: null,
isForwardReference: false,
}
: {
directive: wrapReference(hostDirective.directive),
isForwardReference: false,
inputs: hostDirective.inputs ? parseMappingStringArray(hostDirective.inputs) : null,
outputs: hostDirective.outputs ? parseMappingStringArray(hostDirective.outputs) : null,
};
});
/**
* Parses a host directive mapping where each odd array key is the name of an input/output
* and each even key is its public name, e.g. `['one', 'oneAlias', 'two', 'two']`.
*/
function getHostDirectiveBindingMapping(array: string[]) {
let result: {[publicName: string]: string} | null = null;
for (let i = 1; i < array.length; i += 2) {
result = result || {};
result[array[i - 1]] = array[i];
}
return null;
return result;
}
function convertOpaqueValuesToExpressions(obj: {[key: string]: OpaqueValue}): {

View file

@ -261,6 +261,47 @@ describe('directive declaration jit compilation', () => {
features: [ɵɵNgOnChangesFeature],
});
});
it('should compile host directives', () => {
class One {}
class Two {}
const def = ɵɵngDeclareDirective({
type: TestClass,
hostDirectives: [
{
directive: One,
inputs: ['firstInput', 'firstInput', 'secondInput', 'secondInputAlias'],
outputs: ['firstOutput', 'firstOutput', 'secondOutput', 'secondOutputAlias'],
},
{
directive: Two,
},
],
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
features: [jasmine.any(Function)],
hostDirectives: [
{
directive: One,
inputs: {
'firstInput': 'firstInput',
'secondInput': 'secondInputAlias',
},
outputs: {
'firstOutput': 'firstOutput',
'secondOutput': 'secondOutputAlias',
},
},
{
directive: Two,
inputs: {},
outputs: {},
},
],
});
});
});
type DirectiveDefExpectations = jasmine.Expected<
@ -278,6 +319,7 @@ type DirectiveDefExpectations = jasmine.Expected<
| 'viewQuery'
| 'exportAs'
| 'providersResolver'
| 'hostDirectives'
>
>;
@ -303,6 +345,7 @@ function expectDirectiveDef(
viewQuery: null,
exportAs: null,
providersResolver: null,
hostDirectives: null,
...expected,
};
@ -321,6 +364,7 @@ function expectDirectiveDef(
expect(actual.providersResolver)
.withContext('providersResolver')
.toEqual(expectation.providersResolver);
expect(actual.hostDirectives).withContext('hostDirectives').toEqual(expectation.hostDirectives);
}
class TestClass {}