Skip to content
Merged
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
26 changes: 21 additions & 5 deletions packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
/**
* A Babel flavored implementation of the AstFactory.
*/
export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
export class BabelAstFactory implements AstFactory<t.Statement, t.Expression | t.SpreadElement> {
constructor(
/** The absolute path to the source file being compiled. */
private sourceUrl: string,
Expand Down Expand Up @@ -74,7 +74,11 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {

createBlock = t.blockStatement;

createCallExpression(callee: t.Expression, args: t.Expression[], pure: boolean): t.Expression {
createCallExpression(
callee: t.Expression,
args: (t.Expression | t.SpreadElement)[],
pure: boolean,
): t.Expression {
const call = t.callExpression(callee, args);
if (pure) {
t.addComment(call, 'leading', ' @__PURE__ ', /* line */ false);
Expand All @@ -90,6 +94,10 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {

createExpressionStatement = t.expressionStatement;

createSpreadElement(expression: t.Expression): t.SpreadElement {
return t.spreadElement(expression);
}

createFunctionDeclaration(
functionName: string,
parameters: string[],
Expand Down Expand Up @@ -158,11 +166,17 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
}
}

createNewExpression = t.newExpression;
createNewExpression(expression: t.Expression, args: t.Expression[]): t.Expression {
return t.newExpression(expression, args);
}

createObjectLiteral(properties: ObjectLiteralProperty<t.Expression>[]): t.Expression {
return t.objectExpression(
properties.map((prop) => {
if (prop.kind === 'spread') {
return t.spreadElement(prop.expression);
}

const key = prop.quoted
? t.stringLiteral(prop.propertyName)
: t.identifier(prop.propertyName);
Expand All @@ -177,7 +191,9 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
return t.memberExpression(expression, t.identifier(propertyName), /* computed */ false);
}

createReturnStatement = t.returnStatement;
createReturnStatement(expression: t.Expression | null): t.Statement {
return t.returnStatement(expression);
}

createTaggedTemplate(tag: t.Expression, template: TemplateLiteral<t.Expression>): t.Expression {
return t.taggedTemplateExpression(tag, this.createTemplateLiteral(template));
Expand Down Expand Up @@ -219,7 +235,7 @@ export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
return t.regExpLiteral(body, flags ?? undefined);
}

setSourceMapRange<T extends t.Statement | t.Expression | t.TemplateElement>(
setSourceMapRange<T extends t.Statement | t.Expression | t.TemplateElement | t.SpreadElement>(
node: T,
sourceMapRange: SourceMapRange | null,
): T {
Expand Down
17 changes: 9 additions & 8 deletions packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export function createEs2015LinkerPlugin({
logger,
...options
}: LinkerPluginOptions): PluginObj {
let fileLinker: FileLinker<ConstantScopePath, t.Statement, t.Expression> | null = null;
let fileLinker: FileLinker<
ConstantScopePath,
t.Statement,
t.Expression | t.SpreadElement
> | null = null;

return {
visitor: {
Expand All @@ -47,13 +51,10 @@ export function createEs2015LinkerPlugin({
}
const sourceUrl = fileSystem.resolve(file.opts.cwd ?? '.', filename);

const linkerEnvironment = LinkerEnvironment.create<t.Statement, t.Expression>(
fileSystem,
logger,
new BabelAstHost(),
new BabelAstFactory(sourceUrl),
options,
);
const linkerEnvironment = LinkerEnvironment.create<
t.Statement,
t.Expression | t.SpreadElement
>(fileSystem, logger, new BabelAstHost(), new BabelAstFactory(sourceUrl), options);
fileLinker = new FileLinker(linkerEnvironment, sourceUrl, file.code);
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,15 @@ describe('BabelAstFactory', () => {
it('should create an object literal node, with the given properties', () => {
const prop1 = expression.ast`42`;
const prop2 = expression.ast`"moo"`;
const prop3 = expression.ast`foo`;
const obj = factory.createObjectLiteral([
{propertyName: 'prop1', value: prop1, quoted: false},
{propertyName: 'prop2', value: prop2, quoted: true},
{propertyName: 'prop1', value: prop1, kind: 'property', quoted: false},
{propertyName: 'prop2', value: prop2, kind: 'property', quoted: true},
{expression: prop3, kind: 'spread'},
]);
expect(generate(obj).code).toEqual(['{', ' prop1: 42,', ' "prop2": "moo"', '}'].join('\n'));
expect(generate(obj).code).toEqual(
['{', ' prop1: 42,', ' "prop2": "moo",', ' ...foo', '}'].join('\n'),
);
});
});

Expand Down Expand Up @@ -408,6 +412,23 @@ describe('BabelAstFactory', () => {
});
});

describe('createSpreadElement()', () => {
it('should create a spread element in an array', () => {
const before = factory.createIdentifier('a');
const spread = factory.createSpreadElement(factory.createIdentifier('b'));
const array = factory.createArrayLiteral([before, spread]);
expect(generate(array).code).toEqual('[a, ...b]');
});

it('should create a spread in a call expression', () => {
const fn = factory.createIdentifier('fn');
const before = factory.createIdentifier('a');
const spread = factory.createSpreadElement(factory.createIdentifier('b'));
const call = factory.createCallExpression(fn, [before, spread], false);
expect(generate(call).code).toEqual('fn(a, ...b)');
});
});

describe('setSourceMapRange()', () => {
it('should attach the `sourceMapRange` to the given `node`', () => {
const expr = expression.ast`42`;
Expand Down
14 changes: 7 additions & 7 deletions packages/compiler-cli/linker/test/ast/ast_value_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ interface TestObject {
const host: AstHost<ts.Expression> = new TypeScriptAstHost();
const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false);
const nestedObj = factory.createObjectLiteral([
{propertyName: 'x', quoted: false, value: factory.createLiteral(42)},
{propertyName: 'y', quoted: false, value: factory.createLiteral('X')},
{propertyName: 'x', kind: 'property', quoted: false, value: factory.createLiteral(42)},
{propertyName: 'y', kind: 'property', quoted: false, value: factory.createLiteral('X')},
]);
const nestedArray = factory.createArrayLiteral([
factory.createLiteral(1),
factory.createLiteral(2),
]);
const obj = AstObject.parse<TestObject, ts.Expression>(
factory.createObjectLiteral([
{propertyName: 'a', quoted: false, value: factory.createLiteral(42)},
{propertyName: 'b', quoted: false, value: factory.createLiteral('X')},
{propertyName: 'c', quoted: false, value: factory.createLiteral(true)},
{propertyName: 'd', quoted: false, value: nestedObj},
{propertyName: 'e', quoted: false, value: nestedArray},
{propertyName: 'a', kind: 'property', quoted: false, value: factory.createLiteral(42)},
{propertyName: 'b', kind: 'property', quoted: false, value: factory.createLiteral('X')},
{propertyName: 'c', kind: 'property', quoted: false, value: factory.createLiteral(true)},
{propertyName: 'd', kind: 'property', quoted: false, value: nestedObj},
{propertyName: 'e', kind: 'property', quoted: false, value: nestedArray},
]),
host,
);
Expand Down
48 changes: 33 additions & 15 deletions packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ describe('FileLinker', () => {
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const ngImport = factory.createIdentifier('core');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'minVersion', quoted: false, value: version},
{propertyName: 'version', quoted: false, value: version},
{propertyName: 'ngImport', quoted: false, value: ngImport},
{propertyName: 'minVersion', quoted: false, kind: 'property', value: version},
{propertyName: 'version', quoted: false, kind: 'property', value: version},
{propertyName: 'ngImport', quoted: false, kind: 'property', value: ngImport},
]);
expect(() =>
fileLinker.linkPartialDeclaration('foo', [declarationArg], new MockDeclarationScope()),
Expand All @@ -58,8 +58,8 @@ describe('FileLinker', () => {
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const ngImport = factory.createIdentifier('core');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'version', quoted: false, value: version},
{propertyName: 'ngImport', quoted: false, value: ngImport},
{propertyName: 'version', quoted: false, kind: 'property', value: version},
{propertyName: 'ngImport', quoted: false, kind: 'property', value: ngImport},
]);
expect(() =>
fileLinker.linkPartialDeclaration(
Expand All @@ -75,8 +75,8 @@ describe('FileLinker', () => {
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const ngImport = factory.createIdentifier('core');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'minVersion', quoted: false, value: version},
{propertyName: 'ngImport', quoted: false, value: ngImport},
{propertyName: 'minVersion', quoted: false, kind: 'property', value: version},
{propertyName: 'ngImport', quoted: false, kind: 'property', value: ngImport},
]);
expect(() =>
fileLinker.linkPartialDeclaration(
Expand All @@ -91,8 +91,8 @@ describe('FileLinker', () => {
const {fileLinker} = createFileLinker();
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'minVersion', quoted: false, value: version},
{propertyName: 'version', quoted: false, value: version},
{propertyName: 'minVersion', quoted: false, kind: 'property', value: version},
{propertyName: 'version', quoted: false, kind: 'property', value: version},
]);
expect(() =>
fileLinker.linkPartialDeclaration(
Expand All @@ -116,9 +116,9 @@ describe('FileLinker', () => {
const ngImport = factory.createIdentifier('core');
const version = factory.createLiteral('0.0.0-PLACEHOLDER');
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: ngImport},
{propertyName: 'minVersion', quoted: false, value: version},
{propertyName: 'version', quoted: false, value: version},
{propertyName: 'ngImport', quoted: false, kind: 'property', value: ngImport},
{propertyName: 'minVersion', quoted: false, kind: 'property', value: version},
{propertyName: 'version', quoted: false, kind: 'property', value: version},
]);

const compilationResult = fileLinker.linkPartialDeclaration(
Expand Down Expand Up @@ -187,13 +187,24 @@ describe('FileLinker', () => {
// Here we use the `core` identifier for `ngImport` to trigger the use of a shared scope for
// constant statements.
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: factory.createIdentifier('core')},
{
propertyName: 'ngImport',
quoted: false,
kind: 'property',
value: factory.createIdentifier('core'),
},
{
propertyName: 'minVersion',
quoted: false,
kind: 'property',
value: factory.createLiteral('0.0.0-PLACEHOLDER'),
},
{
propertyName: 'version',
quoted: false,
kind: 'property',
value: factory.createLiteral('0.0.0-PLACEHOLDER'),
},
{propertyName: 'version', quoted: false, value: factory.createLiteral('0.0.0-PLACEHOLDER')},
]);

const replacement = fileLinker.linkPartialDeclaration(
Expand All @@ -217,15 +228,22 @@ describe('FileLinker', () => {
// Here we use a string literal `"not-a-module"` for `ngImport` to cause constant
// statements to be emitted in an IIFE rather than added to the shared constant scope.
const declarationArg = factory.createObjectLiteral([
{propertyName: 'ngImport', quoted: false, value: factory.createLiteral('not-a-module')},
{
propertyName: 'ngImport',
quoted: false,
kind: 'property',
value: factory.createLiteral('not-a-module'),
},
{
propertyName: 'minVersion',
quoted: false,
kind: 'property',
value: factory.createLiteral('0.0.0-PLACEHOLDER'),
},
{
propertyName: 'version',
quoted: false,
kind: 'property',
value: factory.createLiteral('0.0.0-PLACEHOLDER'),
},
]);
Expand Down
26 changes: 24 additions & 2 deletions packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ export interface AstFactory<TStatement, TExpression> {
*/
createRegularExpressionLiteral(body: string, flags: string | null): TExpression;

/**
* Create a spread element, typically in an array or function call. E.g. `[...a]` or `fn(...b)`.
*
* @param target Expression of the spread element.
*/
createSpreadElement(expression: TExpression): TExpression;

/**
* Attach a source map range to the given node.
*
Expand Down Expand Up @@ -361,9 +368,11 @@ export interface SourceMapRange {
}

/**
* Information used by the `AstFactory` to create a property on an object literal expression.
* Information used by the `AstFactory` to create a property assignment
* on an object literal expression.
*/
export interface ObjectLiteralProperty<TExpression> {
export interface ObjectLiteralAssignment<TExpression> {
kind: 'property';
propertyName: string;
value: TExpression;
/**
Expand All @@ -372,6 +381,19 @@ export interface ObjectLiteralProperty<TExpression> {
quoted: boolean;
}

/**
* Information used by the `AstFactory` to create a spread on an object literal expression.
*/
export interface ObjectLiteralSpread<TExpression> {
kind: 'spread';
expression: TExpression;
}

/** Possible properties in an object literal. */
export type ObjectLiteralProperty<TExpression> =
| ObjectLiteralAssignment<TExpression>
| ObjectLiteralSpread<TExpression>;

/**
* Information used by the `AstFactory` to create a template literal string (i.e. a back-ticked
* string with interpolations).
Expand Down
23 changes: 18 additions & 5 deletions packages/compiler-cli/src/ngtsc/translator/src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import * as o from '@angular/compiler';
import {
AstFactory,
BinaryOperator,
ObjectLiteralAssignment,
ObjectLiteralProperty,
ObjectLiteralSpread,
SourceMapRange,
TemplateElement,
TemplateLiteral,
Expand Down Expand Up @@ -390,11 +392,17 @@ export class ExpressionTranslatorVisitor<TFile, TStatement, TExpression>

visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): TExpression {
const properties: ObjectLiteralProperty<TExpression>[] = ast.entries.map((entry) => {
return {
propertyName: entry.key,
quoted: entry.quoted,
value: entry.value.visitExpression(this, context),
};
return entry instanceof o.LiteralMapPropertyAssignment
? ({
kind: 'property',
propertyName: entry.key,
quoted: entry.quoted,
value: entry.value.visitExpression(this, context),
} satisfies ObjectLiteralAssignment<TExpression>)
: ({
kind: 'spread',
expression: entry.expression.visitExpression(this, context),
} satisfies ObjectLiteralSpread<TExpression>);
});
return this.setSourceMapRange(this.factory.createObjectLiteral(properties), ast.sourceSpan);
}
Expand All @@ -407,6 +415,11 @@ export class ExpressionTranslatorVisitor<TFile, TStatement, TExpression>
throw new Error('Method not implemented');
}

visitSpreadElementExpr(ast: o.outputAst.SpreadElementExpr, context: any): TExpression {
const expression = ast.expression.visitExpression(this, context);
return this.setSourceMapRange(this.factory.createSpreadElement(expression), ast.sourceSpan);
}

visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, _context: Context): any {
this.recordWrappedNode(ast);
return ast.node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {

visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): ts.TypeLiteralNode {
const entries = ast.entries.map((entry) => {
if (entry instanceof o.LiteralMapSpreadAssignment) {
throw new Error('Spread is not supported in this context');
}

const {key, quoted} = entry;
const type = this.translateExpression(entry.value, context);
return ts.factory.createPropertySignature(
Expand Down Expand Up @@ -276,6 +280,11 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor {
throw new Error('Method not implemented.');
}

visitSpreadElementExpr(ast: o.outputAst.SpreadElementExpr, context: any) {
const typeNode = this.translateExpression(ast.expression, context);
return ts.factory.createRestTypeNode(typeNode);
}

private translateType(type: o.Type, context: Context): ts.TypeNode {
const typeNode = type.visitType(this, context);
if (!ts.isTypeNode(typeNode)) {
Expand Down
Loading