fix(compiler): lower variables with a closure by exporting the variable.
This e.g. leaves comments at the right place, which is important for closure.
This commit is contained in:

committed by
Matias Niemelä

parent
b6833d1bbd
commit
5ef6e6366f
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CollectorOptions, MetadataCollector, MetadataValue, ModuleMetadata, isMetadataGlobalReferenceExpression} from '@angular/tsc-wrapped';
|
import {CollectorOptions, MetadataCollector, MetadataError, MetadataValue, ModuleMetadata, isMetadataGlobalReferenceExpression} from '@angular/tsc-wrapped';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
export interface LoweringRequest {
|
export interface LoweringRequest {
|
||||||
@ -18,14 +18,17 @@ export interface LoweringRequest {
|
|||||||
|
|
||||||
export type RequestLocationMap = Map<number, LoweringRequest>;
|
export type RequestLocationMap = Map<number, LoweringRequest>;
|
||||||
|
|
||||||
|
const enum DeclarationOrder { BeforeStmt, AfterStmt }
|
||||||
|
|
||||||
interface Declaration {
|
interface Declaration {
|
||||||
name: string;
|
name: string;
|
||||||
node: ts.Node;
|
node: ts.Node;
|
||||||
|
order: DeclarationOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeclarationInsert {
|
interface DeclarationInsert {
|
||||||
declarations: Declaration[];
|
declarations: Declaration[];
|
||||||
priorTo: ts.Node;
|
relativeTo: ts.Node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMap<T, K>(items: T[], select: (item: T) => K): Map<K, T> {
|
function toMap<T, K>(items: T[], select: (item: T) => K): Map<K, T> {
|
||||||
@ -72,17 +75,32 @@ function transformSourceFile(
|
|||||||
|
|
||||||
function visitNode(node: ts.Node): ts.Node {
|
function visitNode(node: ts.Node): ts.Node {
|
||||||
// Get the original node before tsickle
|
// Get the original node before tsickle
|
||||||
const {pos, end, kind} = ts.getOriginalNode(node);
|
const {pos, end, kind, parent: originalParent} = ts.getOriginalNode(node);
|
||||||
const nodeRequest = requests.get(pos);
|
const nodeRequest = requests.get(pos);
|
||||||
if (nodeRequest && nodeRequest.kind == kind && nodeRequest.end == end) {
|
if (nodeRequest && nodeRequest.kind == kind && nodeRequest.end == end) {
|
||||||
// This node is requested to be rewritten as a reference to the exported name.
|
// This node is requested to be rewritten as a reference to the exported name.
|
||||||
|
if (originalParent && originalParent.kind === ts.SyntaxKind.VariableDeclaration) {
|
||||||
|
// As the value represents the whole initializer of a variable declaration,
|
||||||
|
// just refer to that variable. This e.g. helps to preserve closure comments
|
||||||
|
// at the right place.
|
||||||
|
const varParent = originalParent as ts.VariableDeclaration;
|
||||||
|
if (varParent.name.kind === ts.SyntaxKind.Identifier) {
|
||||||
|
const varName = varParent.name.text;
|
||||||
|
const exportName = nodeRequest.name;
|
||||||
|
declarations.push({
|
||||||
|
name: exportName,
|
||||||
|
node: ts.createIdentifier(varName),
|
||||||
|
order: DeclarationOrder.AfterStmt
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Record that the node needs to be moved to an exported variable with the given name
|
// Record that the node needs to be moved to an exported variable with the given name
|
||||||
const name = nodeRequest.name;
|
const exportName = nodeRequest.name;
|
||||||
declarations.push({name, node});
|
declarations.push({name: exportName, node, order: DeclarationOrder.BeforeStmt});
|
||||||
return ts.createIdentifier(name);
|
return ts.createIdentifier(exportName);
|
||||||
}
|
}
|
||||||
let result = node;
|
let result = node;
|
||||||
|
|
||||||
if (shouldVisit(pos, end) && !isLexicalScope(node)) {
|
if (shouldVisit(pos, end) && !isLexicalScope(node)) {
|
||||||
result = ts.visitEachChild(node, visitNode, context);
|
result = ts.visitEachChild(node, visitNode, context);
|
||||||
}
|
}
|
||||||
@ -91,35 +109,44 @@ function transformSourceFile(
|
|||||||
|
|
||||||
// Get the original node before tsickle
|
// Get the original node before tsickle
|
||||||
const {pos, end} = ts.getOriginalNode(node);
|
const {pos, end} = ts.getOriginalNode(node);
|
||||||
const result = shouldVisit(pos, end) ? ts.visitEachChild(node, visitNode, context) : node;
|
let resultStmt: ts.Statement;
|
||||||
|
if (shouldVisit(pos, end)) {
|
||||||
|
resultStmt = ts.visitEachChild(node, visitNode, context);
|
||||||
|
} else {
|
||||||
|
resultStmt = node;
|
||||||
|
}
|
||||||
|
|
||||||
if (declarations.length) {
|
if (declarations.length) {
|
||||||
inserts.push({priorTo: result, declarations});
|
inserts.push({relativeTo: resultStmt, declarations});
|
||||||
}
|
}
|
||||||
return result;
|
return resultStmt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newStatements = sourceFile.statements.map(topLevelStatement);
|
let newStatements = sourceFile.statements.map(topLevelStatement);
|
||||||
|
|
||||||
if (inserts.length) {
|
if (inserts.length) {
|
||||||
// Insert the declarations before the rewritten statement that references them.
|
// Insert the declarations relative to the rewritten statement that references them.
|
||||||
const insertMap = toMap(inserts, i => i.priorTo);
|
const insertMap = toMap(inserts, i => i.relativeTo);
|
||||||
for (let i = newStatements.length; i >= 0; i--) {
|
const tmpStatements: ts.Statement[] = [];
|
||||||
const statement = newStatements[i];
|
newStatements.forEach(statement => {
|
||||||
const insert = insertMap.get(statement);
|
const insert = insertMap.get(statement);
|
||||||
if (insert) {
|
if (insert) {
|
||||||
const declarations = insert.declarations.map(
|
const before = insert.declarations.filter(d => d.order === DeclarationOrder.BeforeStmt);
|
||||||
i => ts.createVariableDeclaration(
|
if (before.length) {
|
||||||
i.name, /* type */ undefined, i.node as ts.Expression));
|
tmpStatements.push(createVariableStatementForDeclarations(before));
|
||||||
const statement = ts.createVariableStatement(
|
}
|
||||||
/* modifiers */ undefined,
|
tmpStatements.push(statement);
|
||||||
ts.createVariableDeclarationList(declarations, ts.NodeFlags.Const));
|
const after = insert.declarations.filter(d => d.order === DeclarationOrder.AfterStmt);
|
||||||
newStatements.splice(i, 0, statement);
|
if (after.length) {
|
||||||
|
tmpStatements.push(createVariableStatementForDeclarations(after));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tmpStatements.push(statement);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// Insert an exports clause to export the declarations
|
// Insert an exports clause to export the declarations
|
||||||
newStatements.push(ts.createExportDeclaration(
|
tmpStatements.push(ts.createExportDeclaration(
|
||||||
/* decorators */ undefined,
|
/* decorators */ undefined,
|
||||||
/* modifiers */ undefined,
|
/* modifiers */ undefined,
|
||||||
ts.createNamedExports(
|
ts.createNamedExports(
|
||||||
@ -130,6 +157,8 @@ function transformSourceFile(
|
|||||||
.map(
|
.map(
|
||||||
declaration => ts.createExportSpecifier(
|
declaration => ts.createExportSpecifier(
|
||||||
/* propertyName */ undefined, declaration.name)))));
|
/* propertyName */ undefined, declaration.name)))));
|
||||||
|
|
||||||
|
newStatements = tmpStatements;
|
||||||
}
|
}
|
||||||
// Note: We cannot use ts.updateSourcefile here as
|
// Note: We cannot use ts.updateSourcefile here as
|
||||||
// it does not work well with decorators.
|
// it does not work well with decorators.
|
||||||
@ -145,6 +174,13 @@ function transformSourceFile(
|
|||||||
return visitSourceFile(sourceFile);
|
return visitSourceFile(sourceFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createVariableStatementForDeclarations(declarations: Declaration[]): ts.VariableStatement {
|
||||||
|
const varDecls = declarations.map(
|
||||||
|
i => ts.createVariableDeclaration(i.name, /* type */ undefined, i.node as ts.Expression));
|
||||||
|
return ts.createVariableStatement(
|
||||||
|
/* modifiers */ undefined, ts.createVariableDeclarationList(varDecls, ts.NodeFlags.Const));
|
||||||
|
}
|
||||||
|
|
||||||
export function getExpressionLoweringTransformFactory(requestsMap: RequestsMap):
|
export function getExpressionLoweringTransformFactory(requestsMap: RequestsMap):
|
||||||
(context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
|
(context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
|
||||||
// Return the factory
|
// Return the factory
|
||||||
|
@ -519,7 +519,6 @@ describe('ngc transformer command-line', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function compile(): number {
|
function compile(): number {
|
||||||
errorSpy.calls.reset();
|
|
||||||
const result = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
const result = mainSync(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
||||||
expect(errorSpy).not.toHaveBeenCalled();
|
expect(errorSpy).not.toHaveBeenCalled();
|
||||||
return result;
|
return result;
|
||||||
@ -622,8 +621,9 @@ describe('ngc transformer command-line', () => {
|
|||||||
|
|
||||||
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
const mymodulejs = path.resolve(outDir, 'mymodule.js');
|
||||||
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
||||||
expect(mymoduleSource).toContain('var ɵ0 = function () { return new Foo(); }');
|
expect(mymoduleSource).toContain('var factory = function () { return new Foo(); }');
|
||||||
expect(mymoduleSource).toContain('export { ɵ0');
|
expect(mymoduleSource).toContain('var ɵ0 = factory;');
|
||||||
|
expect(mymoduleSource).toContain('export { ɵ0 };');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not lower a lambda that is already exported', () => {
|
it('should not lower a lambda that is already exported', () => {
|
||||||
@ -647,6 +647,72 @@ describe('ngc transformer command-line', () => {
|
|||||||
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8');
|
||||||
expect(mymoduleSource).not.toContain('ɵ0');
|
expect(mymoduleSource).not.toContain('ɵ0');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to lower supported expressions', () => {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"files": ["module.ts"]
|
||||||
|
}`);
|
||||||
|
write('module.ts', `
|
||||||
|
import {NgModule, InjectionToken} from '@angular/core';
|
||||||
|
import {AppComponent} from './app';
|
||||||
|
|
||||||
|
export interface Info {
|
||||||
|
route: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const T1 = new InjectionToken<string>('t1');
|
||||||
|
export const T2 = new InjectionToken<string>('t2');
|
||||||
|
export const T3 = new InjectionToken<number>('t3');
|
||||||
|
export const T4 = new InjectionToken<Info[]>('t4');
|
||||||
|
|
||||||
|
enum SomeEnum {
|
||||||
|
OK,
|
||||||
|
Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateString() {
|
||||||
|
return 'someValue';
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeLikeData = [{
|
||||||
|
route: '/home',
|
||||||
|
data: calculateString()
|
||||||
|
}];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: T1, useValue: calculateString() },
|
||||||
|
{ provide: T2, useFactory: () => 'someValue' },
|
||||||
|
{ provide: T3, useValue: SomeEnum.OK },
|
||||||
|
{ provide: T4, useValue: routeLikeData }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
write('app.ts', `
|
||||||
|
import {Component, Inject} from '@angular/core';
|
||||||
|
import * as m from './module';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
constructor(
|
||||||
|
@Inject(m.T1) private t1: string,
|
||||||
|
@Inject(m.T2) private t2: string,
|
||||||
|
@Inject(m.T3) private t3: number,
|
||||||
|
@Inject(m.T4) private t4: m.Info[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(mainSync(['-p', basePath], errorSpy)).toBe(0);
|
||||||
|
shouldExist('module.js');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to generate a flat module library', () => {
|
it('should be able to generate a flat module library', () => {
|
||||||
@ -848,78 +914,4 @@ describe('ngc transformer command-line', () => {
|
|||||||
shouldExist('app/main.js');
|
shouldExist('app/main.js');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('expression lowering', () => {
|
|
||||||
const shouldExist = (fileName: string) => {
|
|
||||||
if (!fs.existsSync(path.resolve(basePath, fileName))) {
|
|
||||||
throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should be able to lower supported expressions', () => {
|
|
||||||
writeConfig(`{
|
|
||||||
"extends": "./tsconfig-base.json",
|
|
||||||
"files": ["module.ts"]
|
|
||||||
}`);
|
|
||||||
write('module.ts', `
|
|
||||||
import {NgModule, InjectionToken} from '@angular/core';
|
|
||||||
import {AppComponent} from './app';
|
|
||||||
|
|
||||||
export interface Info {
|
|
||||||
route: string;
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const T1 = new InjectionToken<string>('t1');
|
|
||||||
export const T2 = new InjectionToken<string>('t2');
|
|
||||||
export const T3 = new InjectionToken<number>('t3');
|
|
||||||
export const T4 = new InjectionToken<Info[]>('t4');
|
|
||||||
|
|
||||||
enum SomeEnum {
|
|
||||||
OK,
|
|
||||||
Cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateString() {
|
|
||||||
return 'someValue';
|
|
||||||
}
|
|
||||||
|
|
||||||
const routeLikeData = [{
|
|
||||||
route: '/home',
|
|
||||||
data: calculateString()
|
|
||||||
}];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [AppComponent],
|
|
||||||
providers: [
|
|
||||||
{ provide: T1, useValue: calculateString() },
|
|
||||||
{ provide: T2, useFactory: () => 'someValue' },
|
|
||||||
{ provide: T3, useValue: SomeEnum.OK },
|
|
||||||
{ provide: T4, useValue: routeLikeData }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class MyModule {}
|
|
||||||
`);
|
|
||||||
write('app.ts', `
|
|
||||||
import {Component, Inject} from '@angular/core';
|
|
||||||
import * as m from './module';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-app',
|
|
||||||
template: ''
|
|
||||||
})
|
|
||||||
export class AppComponent {
|
|
||||||
constructor(
|
|
||||||
@Inject(m.T1) private t1: string,
|
|
||||||
@Inject(m.T2) private t2: string,
|
|
||||||
@Inject(m.T3) private t3: number,
|
|
||||||
@Inject(m.T4) private t4: m.Info[],
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
expect(mainSync(['-p', basePath], s => {})).toBe(0);
|
|
||||||
shouldExist('built/module.js');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -28,6 +28,11 @@ describe('Expression lowering', () => {
|
|||||||
class MyClass {}
|
class MyClass {}
|
||||||
`)).toContain('const l = () => null; exports.l = l;');
|
`)).toContain('const l = () => null; exports.l = l;');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be able to export a variable if the whole value is lowered', () => {
|
||||||
|
expect(convert('/*a*/ const a =◊b: () => null◊;'))
|
||||||
|
.toBe('/*a*/ const a = () => null; const b = a; export { b };');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('collector', () => {
|
describe('collector', () => {
|
||||||
|
Reference in New Issue
Block a user