Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions goldens/public-api/compiler-cli/compiler_options.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface BazelAndG3Options {
_experimentalAllowEmitDeclarationOnly?: boolean;
generateDeepReexports?: boolean;
generateExtraImportsInLocalMode?: boolean;
legacyOptionalChaining?: boolean;
onlyExplicitDeferDependencyImports?: boolean;
onlyPublishPublicTypingsForNgModules?: boolean;
}
Expand Down
39 changes: 39 additions & 0 deletions packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ export class BabelAstFactory implements AstFactory<

createBlock = t.blockStatement;

createCallChain(
callee: t.Expression,
args: (t.Expression | t.SpreadElement)[],
pure: boolean,
isOptional: boolean,
): t.Expression {
const call = t.optionalCallExpression(callee, args, /* optional */ isOptional);
if (pure) {
t.addComment(call, 'leading', ' @__PURE__ ', /* line */ false);
}
return call;
}

createCallExpression(
callee: t.Expression,
args: (t.Expression | t.SpreadElement)[],
Expand All @@ -102,6 +115,19 @@ export class BabelAstFactory implements AstFactory<
return t.memberExpression(expression, element, /* computed */ true);
}

createElementAccessChain(
expression: t.Expression,
element: t.Expression,
isOptional: boolean,
): t.Expression {
return t.optionalMemberExpression(
expression,
element,
/* computed */ true,
/* optional */ isOptional,
);
}

createExpressionStatement = t.expressionStatement;

createSpreadElement(expression: t.Expression): t.SpreadElement {
Expand Down Expand Up @@ -201,6 +227,19 @@ export class BabelAstFactory implements AstFactory<
return t.memberExpression(expression, t.identifier(propertyName), /* computed */ false);
}

createPropertyAccessChain(
expression: t.Expression,
propertyName: string,
isOptional: boolean,
): t.Expression {
return t.optionalMemberExpression(
expression,
t.identifier(propertyName),
/* computed */ false,
/* optional */ isOptional,
);
}

createReturnStatement(expression: t.Expression | null): t.Statement {
return t.returnStatement(expression);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export class PartialComponentLinkerVersion1<

return {
...baseMeta,
legacyOptionalChaining: major < 22 && version !== PLACEHOLDER_VERSION,
viewProviders: metaObj.has('viewProviders') ? metaObj.getOpaque('viewProviders') : null,
template: {
nodes: template.nodes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
ForwardRefHandling,
LegacyInputPartialMapping,
makeBindingParser,
outputAst as o,
ParseLocation,
ParseSourceFile,
ParseSourceSpan,
Expand All @@ -26,13 +25,19 @@ import {
R3QueryMetadata,
} from '@angular/compiler';

import semver from 'semver';
import {Range} from '../../ast/ast_host';
import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';

import {LinkedDefinition, PartialLinker} from './partial_linker';
import {extractForwardRef, getDefaultStandaloneValue, wrapReference} from './util';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system/src/types';
import {LinkedDefinition, PartialLinker} from './partial_linker';
import {
extractForwardRef,
getDefaultStandaloneValue,
PLACEHOLDER_VERSION,
wrapReference,
} from './util';

/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareDirective()` call expressions.
Expand Down Expand Up @@ -62,6 +67,7 @@ export function toR3DirectiveMeta<TExpression>(
sourceUrl: AbsoluteFsPath,
version: string,
): R3DirectiveMetadata {
const {major} = new semver.SemVer(version);
const typeExpr = metaObj.getValue('type');
const typeName = typeExpr.getSymbolName();
if (typeName === null) {
Expand Down Expand Up @@ -107,6 +113,7 @@ export function toR3DirectiveMeta<TExpression>(
hostDirectives: metaObj.has('hostDirectives')
? toHostDirectivesMetadata(metaObj.getValue('hostDirectives'))
: null,
legacyOptionalChaining: major < 22 && version !== PLACEHOLDER_VERSION,
};
}

Expand Down
107 changes: 107 additions & 0 deletions packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,113 @@ describe('FileLinker', () => {
});
});

describe('legacyOptionalChaining support', () => {
function linkComponentWithTemplate(version: string, template: string): string {
// Note that the `minVersion` is set to the placeholder,
// because that's what we have in the source code as well.
const source = `
ɵɵngDeclareComponent({
minVersion: "0.0.0-PLACEHOLDER",
version: "${version}",
ngImport: core,
template: \`${template}\`,
isInline: true,
type: SomeComp
});
`;

// We need to create a new source file here, because template parsing requires
// the template string to have offsets which synthetic nodes do not.
const {fileLinker} = createFileLinker(source);
const sourceFile = ts.createSourceFile('', source, ts.ScriptTarget.Latest, true);
const call = (sourceFile.statements[0] as ts.ExpressionStatement)
.expression as ts.CallExpression;
const result = fileLinker.linkPartialDeclaration(
'ɵɵngDeclareComponent',
[call.arguments[0]],
new MockDeclarationScope(),
);
return ts.createPrinter().printNode(ts.EmitHint.Unspecified, result, sourceFile);
}

it('should use null for optional chaining if compiled with a version older than 22', () => {
for (const version of ['21.0.0', '21.2.0', '16.2.0']) {
const result = linkComponentWithTemplate(version, '{{ foo?.bar }}');
expect(result).toContain(' == null');
}
});

it('should use undefined for optional chaining if compiled with version 22 or above', () => {
for (const version of ['22.0.0', '22.0.1', '22.1.0', '22.0.0-next.0', '23.0.0']) {
const result = linkComponentWithTemplate(version, '{{ foo?.bar }}');
expect(result).not.toContain(' == null');
}
});

it('should not use null for optional chaining if compiled with a local version (0.0.0-PLACEHOLDER)', () => {
const result = linkComponentWithTemplate('0.0.0-PLACEHOLDER', '{{ foo?.bar }}');
expect(result).not.toContain(' == null');
});

it('should use null for optional chaining when the $safeNavigationMigration magic function is used, regardless of the version', () => {
for (const version of ['16.2.0', '22.0.0', '0.0.0-PLACEHOLDER']) {
const result = linkComponentWithTemplate(
version,
'{{ $safeNavigationMigration(foo?.bar) }}',
);
expect(result).toContain(' == null');
}
});

function linkDirectiveWithHostBinding(version: string, expression: string): string {
const source = `
ɵɵngDeclareDirective({
minVersion: "0.0.0-PLACEHOLDER",
version: "${version}",
ngImport: core,
host: {
properties: {
"attr.foo": "${expression}"
}
},
type: SomeDir
});
`;

const {fileLinker} = createFileLinker(source);
const sourceFile = ts.createSourceFile('', source, ts.ScriptTarget.Latest, true);
const call = (sourceFile.statements[0] as ts.ExpressionStatement)
.expression as ts.CallExpression;
const result = fileLinker.linkPartialDeclaration(
'ɵɵngDeclareDirective',
[call.arguments[0]],
new MockDeclarationScope(),
);
return ts.createPrinter().printNode(ts.EmitHint.Unspecified, result, sourceFile);
}

it('should use null for optional chaining in directive host bindings if compiled with a version older than 22', () => {
for (const version of ['21.0.0', '21.2.0', '16.2.0']) {
const result = linkDirectiveWithHostBinding(version, 'foo?.bar');
expect(result).toContain(' == null');
}
});

it('should use undefined for optional chaining in directive host bindings if compiled with version 22 or above', () => {
for (const version of [
'22.0.0',
'22.0.1',
'22.1.0',
'22.0.0-next.0',
'23.0.0',
'0.0.0-PLACEHOLDER',
]) {
const result = linkDirectiveWithHostBinding(version, 'foo?.bar');
expect(result).not.toContain(' == null');
}
});
});

describe('getConstantStatements()', () => {
it('should capture shared constant values', () => {
const {fileLinker} = createFileLinker();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,44 @@
*/

import {
LegacyAnimationTriggerNames,
BoundTarget,
compileClassDebugInfo,
compileHmrInitializer,
compileComponentClassMetadata,
compileComponentDeclareClassMetadata,
compileComponentFromMetadata,
compileDeclareComponentFromMetadata,
compileDeferResolverFunction,
compileHmrInitializer,
ConstantPool,
createHostElement,
CssSelector,
DeclarationListEmitMode,
DeclareComponentTemplateInfo,
DeferBlockDepsEmitMode,
DirectiveMatcher,
DomElementSchemaRegistry,
ExternalExpr,
FactoryTarget,
LegacyAnimationTriggerNames,
makeBindingParser,
MatchSource,
outputAst as o,
R3ComponentDeferMetadata,
R3ComponentMetadata,
R3DeferPerComponentDependency,
R3DirectiveDependencyMetadata,
R3NgModuleDependencyMetadata,
R3PipeDependencyMetadata,
createHostElement,
R3TargetBinder,
R3TemplateDependency,
R3TemplateDependencyKind,
R3TemplateDependencyMetadata,
SchemaMetadata,
SelectorlessMatcher,
SelectorMatcher,
TmplAstDeferredBlock,
ViewEncapsulation,
DirectiveMatcher,
SelectorlessMatcher,
MatchSource,
TypeCheckId,
ViewEncapsulation,
} from '@angular/compiler';
import ts from 'typescript';

Expand Down Expand Up @@ -117,10 +117,10 @@ import {
ResolveResult,
} from '../../../transform';
import {
HostBindingsContext,
TemplateContext,
TypeCheckableDirectiveMeta,
TypeCheckContext,
TemplateContext,
HostBindingsContext,
} from '../../../typecheck/api';
import {ExtendedTemplateChecker} from '../../../typecheck/extended/api';
import {TemplateSemanticsChecker} from '../../../typecheck/template_semantics/api/api';
Expand Down Expand Up @@ -165,6 +165,12 @@ import {
} from '../../directive';
import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module';

import {extractHmrMetatadata, getHmrUpdateDeclaration} from '../../../hmr';
import {ComponentScope} from '../../../scope/src/api';
import {getTemplateDiagnostics} from '../../../typecheck';
import {getProjectRelativePath} from '../../../util/src/path';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
import {analyzeTemplateForAnimations} from './animations';
import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics';
import {
ComponentAnalysisData,
Expand All @@ -185,19 +191,13 @@ import {
StyleUrlMeta,
transformDecoratorResources,
} from './resources';
import {analyzeTemplateForSelectorless} from './selectorless';
import {ComponentSymbol} from './symbol';
import {
legacyAnimationTriggerResolver,
collectLegacyAnimationNames,
legacyAnimationTriggerResolver,
validateAndFlattenComponentImports,
} from './util';
import {getTemplateDiagnostics} from '../../../typecheck';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
import {extractHmrMetatadata, getHmrUpdateDeclaration} from '../../../hmr';
import {getProjectRelativePath} from '../../../util/src/path';
import {ComponentScope} from '../../../scope/src/api';
import {analyzeTemplateForSelectorless} from './selectorless';
import {analyzeTemplateForAnimations} from './animations';

const EMPTY_ARRAY: any[] = [];

Expand Down Expand Up @@ -283,6 +283,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
private readonly typeCheckHostBindings: boolean,
private readonly enableSelectorless: boolean,
private readonly emitDeclarationOnly: boolean,
private readonly legacyOptionalChaining: boolean,
) {
this.extractTemplateOptions = {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
Expand Down Expand Up @@ -499,6 +500,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
this.strictStandalone,
this.implicitStandaloneValue,
this.emitDeclarationOnly,
this.legacyOptionalChaining,
);
// `extractDirectiveMetadata` returns `jitForced = true` when the `@Component` has
// set `jit: true`. In this case, compilation of the decorator is skipped. Returning
Expand Down Expand Up @@ -984,6 +986,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<
changeDetection,
styles,
externalStyles,
legacyOptionalChaining: this.legacyOptionalChaining,
// These will be replaced during the compilation step, after all `NgModule`s have been
// analyzed and the full compilation scope for the component can be realized.
animations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ function setup(
/* typeCheckHostBindings */ true,
/* enableSelectorless */ false,
/* emitDeclarationOnly */ false,
/* enableInlineStyles */ true,
);
return {reflectionHost, handler, resourceLoader, metaRegistry};
}
Expand Down
Loading
Loading