Converter to Polymer2 classes.

See readme.txt for more information about usages.

Change-Id: I60843b32bc56faa04ad2b70898ab1f5535a2ad71
This commit is contained in:
Dmitrii Filippov 2019-10-14 17:32:55 +02:00
parent 2f67ad2509
commit f0d5b6e49c
17 changed files with 2046 additions and 0 deletions

3
tools/polygerrit-updater/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.idea/
/node_modules/
/js/

View File

@ -0,0 +1,18 @@
{
"name": "polygerrit-updater",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/node": {
"version": "12.7.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz",
"integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ=="
},
"typescript": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
"integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg=="
}
}
}

View File

@ -0,0 +1,15 @@
{
"name": "polygerrit-updater",
"version": "1.0.0",
"description": "Polygerrit source code updater",
"scripts": {
"compile": "tsc",
"convert": "npm run compile && node js/src/index.js"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^12.7.12",
"typescript": "^3.6.4"
}
}

View File

@ -0,0 +1,56 @@
This folder contains tool to update Polymer components to class based components.
This is a temporary tools, it will be removed in a few weeks.
How to use this tool: initial steps
1) Important - Commit and push all your changes. Otherwise, you can loose you work.
2) Ensure, that tools/polygerrit-updater is your current directory
3) Run
npm install
4) If you want to convert the whole project, run
npm run convert -- --i \
--root ../../polygerrit-ui --src app/elements --r \
--exclude app/elements/core/gr-reporting/gr-reporting.js \
app/elements/diff/gr-comment-api/gr-comment-api-mock.js \
app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
You can convert only specific files (can be useful if you want to convert some files in your change)
npm run convert -- --i \
--root ../../polygerrit-ui
--src app/elements/file1.js \
app/elements/folder/file2.js
4) Search for the following string in all .js files:
//This file has the following problems with comments:
If you find such string in a .js file - you must manually fix comments in this file.
(It is expected that you shouldn't have such problems)
5) Go to the gerrit root folder and run
npm run eslintfix
(If you are doing it for the first time, run the following command before in gerrit root folder:
npm run install)
Fix error after eslintfix (if exists)
6) If you are doing conversion for the whole project, make the followin changes:
a) Add
<link rel="import" href="../../../types/polymer-behaviors.js">
to
polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
b) Update polymer.json with the following rules:
"lint": {
"rules": ["polymer-2"],
"ignoreWarnings": ["deprecated-dom-call"]
}
5) Commit changed files.
6) You can update excluded files later.

View File

@ -0,0 +1,131 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {LegacyLifecycleMethodsArray, LegacyPolymerComponent} from './polymerComponentParser';
import {LifecycleMethodsBuilder} from './lifecycleMethodsBuilder';
import {ClassBasedPolymerElement, PolymerElementBuilder} from './polymerElementBuilder';
import * as codeUtils from '../utils/codeUtils';
import * as ts from 'typescript';
export class PolymerFuncToClassBasedConverter {
public static convert(component: LegacyPolymerComponent): ClassBasedPolymerElement {
const legacySettings = component.componentSettings;
const reservedDeclarations = legacySettings.reservedDeclarations;
if(!reservedDeclarations.is) {
throw new Error("Legacy component doesn't have 'is' property");
}
const className = this.generateClassNameFromTagName(reservedDeclarations.is.data);
const updater = new PolymerElementBuilder(component, className);
updater.addIsAccessor(reservedDeclarations.is.data);
if(reservedDeclarations.properties) {
updater.addPolymerPropertiesAccessor(reservedDeclarations.properties);
}
updater.addMixin("Polymer.Element");
updater.addMixin("Polymer.LegacyElementMixin");
updater.addMixin("Polymer.GestureEventListeners");
if(reservedDeclarations._legacyUndefinedCheck) {
updater.addMixin("Polymer.LegacyDataMixin");
}
if(reservedDeclarations.behaviors) {
updater.addMixin("Polymer.mixinBehaviors", [reservedDeclarations.behaviors.data]);
const mixinNames = this.getMixinNamesFromBehaviors(reservedDeclarations.behaviors.data);
const jsDocLines = mixinNames.map(mixinName => {
return `@appliesMixin ${mixinName}`;
});
updater.addClassJSDocComments(jsDocLines);
}
if(reservedDeclarations.observers) {
updater.addPolymerPropertiesObservers(reservedDeclarations.observers.data);
}
if(reservedDeclarations.keyBindings) {
updater.addKeyBindings(reservedDeclarations.keyBindings.data);
}
const lifecycleBuilder = new LifecycleMethodsBuilder();
if (reservedDeclarations.listeners) {
lifecycleBuilder.addListeners(reservedDeclarations.listeners.data, legacySettings.ordinaryMethods);
}
if (reservedDeclarations.hostAttributes) {
lifecycleBuilder.addHostAttributes(reservedDeclarations.hostAttributes.data);
}
for(const name of LegacyLifecycleMethodsArray) {
const existingMethod = legacySettings.lifecycleMethods.get(name);
if(existingMethod) {
lifecycleBuilder.addLegacyLifecycleMethod(name, existingMethod)
}
}
const newLifecycleMethods = lifecycleBuilder.buildNewMethods();
updater.addLifecycleMethods(newLifecycleMethods);
updater.addOrdinaryMethods(legacySettings.ordinaryMethods);
updater.addOrdinaryGetAccessors(legacySettings.ordinaryGetAccessors);
updater.addOrdinaryShorthandProperties(legacySettings.ordinaryShorthandProperties);
updater.addOrdinaryPropertyAssignments(legacySettings.ordinaryPropertyAssignments);
return updater.build();
}
private static generateClassNameFromTagName(tagName: string) {
let result = "";
let nextUppercase = true;
for(const ch of tagName) {
if (ch === '-') {
nextUppercase = true;
continue;
}
result += nextUppercase ? ch.toUpperCase() : ch;
nextUppercase = false;
}
return result;
}
private static getMixinNamesFromBehaviors(behaviors: ts.ArrayLiteralExpression): string[] {
return behaviors.elements.map((expression) => {
const propertyAccessExpression = codeUtils.assertNodeKind(expression, ts.SyntaxKind.PropertyAccessExpression) as ts.PropertyAccessExpression;
const namespaceName = codeUtils.assertNodeKind(propertyAccessExpression.expression, ts.SyntaxKind.Identifier) as ts.Identifier;
const behaviorName = propertyAccessExpression.name;
if(namespaceName.text === 'Gerrit') {
let behaviorNameText = behaviorName.text;
const suffix = 'Behavior';
if(behaviorNameText.endsWith(suffix)) {
behaviorNameText =
behaviorNameText.substr(0, behaviorNameText.length - suffix.length);
}
const mixinName = behaviorNameText + 'Mixin';
return `${namespaceName.text}.${mixinName}`
} else if(namespaceName.text === 'Polymer') {
let behaviorNameText = behaviorName.text;
if(behaviorNameText === "IronFitBehavior") {
return "Polymer.IronFitMixin";
} else if(behaviorNameText === "IronOverlayBehavior") {
return "";
}
throw new Error(`Unsupported behavior: ${propertyAccessExpression.getText()}`);
}
throw new Error(`Unsupported behavior name ${expression.getFullText()}`)
}).filter(name => name.length > 0);
}
}

View File

@ -0,0 +1,74 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ts from 'typescript';
import * as codeUtils from '../utils/codeUtils'
import {LegacyPolymerComponent} from './polymerComponentParser';
import {ClassBasedPolymerElement} from './polymerElementBuilder';
export class LegacyPolymerFuncReplaceResult {
public constructor(
private readonly transformationResult: ts.TransformationResult<ts.SourceFile>,
public readonly leadingComments: string[]) {
}
public get file(): ts.SourceFile {
return this.transformationResult.transformed[0];
}
public dispose() {
this.transformationResult.dispose();
}
}
export class LegacyPolymerFuncReplacer {
private readonly callStatement: ts.ExpressionStatement;
private readonly parentBlock: ts.Block;
private readonly callStatementIndexInBlock: number;
public constructor(private readonly legacyComponent: LegacyPolymerComponent) {
this.callStatement = codeUtils.assertNodeKind(legacyComponent.polymerFuncCallExpr.parent, ts.SyntaxKind.ExpressionStatement);
this.parentBlock = codeUtils.assertNodeKind(this.callStatement.parent, ts.SyntaxKind.Block);
this.callStatementIndexInBlock = this.parentBlock.statements.indexOf(this.callStatement);
if(this.callStatementIndexInBlock < 0) {
throw new Error("Internal error! Couldn't find statement in its own parent");
}
}
public replace(classBasedElement: ClassBasedPolymerElement): LegacyPolymerFuncReplaceResult {
const classDeclarationWithComments = this.appendLeadingCommentToClassDeclaration(classBasedElement.classDeclaration);
return new LegacyPolymerFuncReplaceResult(
this.replaceLegacyPolymerFunction(classDeclarationWithComments.classDeclarationWithCommentsPlaceholder, classBasedElement.componentRegistration),
classDeclarationWithComments.leadingComments);
}
private appendLeadingCommentToClassDeclaration(classDeclaration: ts.ClassDeclaration): {classDeclarationWithCommentsPlaceholder: ts.ClassDeclaration, leadingComments: string[]} {
const text = this.callStatement.getFullText();
let classDeclarationWithCommentsPlaceholder = classDeclaration;
const leadingComments: string[] = [];
ts.forEachLeadingCommentRange(text, 0, (pos, end, kind, hasTrailingNewLine) => {
classDeclarationWithCommentsPlaceholder = codeUtils.addReplacableCommentBeforeNode(classDeclarationWithCommentsPlaceholder, String(leadingComments.length));
leadingComments.push(text.substring(pos, end));
});
return {
classDeclarationWithCommentsPlaceholder: classDeclarationWithCommentsPlaceholder,
leadingComments: leadingComments
}
}
private replaceLegacyPolymerFunction(classDeclaration: ts.ClassDeclaration, componentRegistration: ts.ExpressionStatement): ts.TransformationResult<ts.SourceFile> {
const newStatements = Array.from(this.parentBlock.statements);
newStatements.splice(this.callStatementIndexInBlock, 1, classDeclaration, componentRegistration);
const updatedBlock = ts.getMutableClone(this.parentBlock);
updatedBlock.statements = ts.createNodeArray(newStatements);
return codeUtils.replaceNode(this.legacyComponent.parsedFile, this.parentBlock, updatedBlock);
}
}

View File

@ -0,0 +1,140 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ts from 'typescript';
import * as codeUtils from '../utils/codeUtils';
import {LegacyLifecycleMethodName, OrdinaryMethods} from './polymerComponentParser';
interface LegacyLifecycleMethodContent {
codeAtMethodStart: ts.Statement[];
existingMethod?: ts.MethodDeclaration;
codeAtMethodEnd: ts.Statement[];
}
export interface LifecycleMethod {
originalPos: number;//-1 - no original method exists
method: ts.MethodDeclaration;
name: LegacyLifecycleMethodName;
}
export class LifecycleMethodsBuilder {
private readonly methods: Map<LegacyLifecycleMethodName, LegacyLifecycleMethodContent> = new Map();
private getMethodContent(name: LegacyLifecycleMethodName): LegacyLifecycleMethodContent {
if(!this.methods.has(name)) {
this.methods.set(name, {
codeAtMethodStart: [],
codeAtMethodEnd: []
});
}
return this.methods.get(name)!;
}
public addListeners(legacyListeners: ts.ObjectLiteralExpression, legacyOrdinaryMethods: OrdinaryMethods) {
for(const listener of legacyListeners.properties) {
const propertyAssignment = codeUtils.assertNodeKind(listener, ts.SyntaxKind.PropertyAssignment) as ts.PropertyAssignment;
if(!propertyAssignment.name) {
throw new Error("Listener must have event name");
}
let eventNameLiteral: ts.StringLiteral;
let commentsToRestore: string[] = [];
if(propertyAssignment.name.kind === ts.SyntaxKind.StringLiteral) {
//We don't loose comment in this case, because we keep literal as is
eventNameLiteral = propertyAssignment.name;
} else if(propertyAssignment.name.kind === ts.SyntaxKind.Identifier) {
eventNameLiteral = ts.createStringLiteral(propertyAssignment.name.text);
commentsToRestore = codeUtils.getLeadingComments(propertyAssignment);
} else {
throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.name.kind]}`);
}
const handlerLiteral = codeUtils.assertNodeKind(propertyAssignment.initializer, ts.SyntaxKind.StringLiteral) as ts.StringLiteral;
const handlerImpl = legacyOrdinaryMethods.get(handlerLiteral.text);
if(!handlerImpl) {
throw new Error(`Can't find event handler '${handlerLiteral.text}'`);
}
const eventHandlerAccess = ts.createPropertyAccess(ts.createThis(), handlerLiteral.text);
//ts.forEachChild(handler)
const args: ts.Identifier[] = handlerImpl.parameters.map((arg) => codeUtils.assertNodeKind(arg.name, ts.SyntaxKind.Identifier));
const eventHandlerCall = ts.createCall(eventHandlerAccess, [], args);
let arrowFunc = ts.createArrowFunction([], [], handlerImpl.parameters, undefined, undefined, eventHandlerCall);
arrowFunc = codeUtils.addNewLineBeforeNode(arrowFunc);
const methodContent = this.getMethodContent("created");
//See https://polymer-library.polymer-project.org/3.0/docs/devguide/gesture-events for a list of events
if(["down", "up", "tap", "track"].indexOf(eventNameLiteral.text) >= 0) {
const methodCall = ts.createCall(codeUtils.createNameExpression("Polymer.Gestures.addListener"), [], [ts.createThis(), eventNameLiteral, arrowFunc]);
methodContent.codeAtMethodEnd.push(ts.createExpressionStatement(methodCall));
}
else {
let methodCall = ts.createCall(ts.createPropertyAccess(ts.createThis(), "addEventListener"), [], [eventNameLiteral, arrowFunc]);
methodCall = codeUtils.restoreLeadingComments(methodCall, commentsToRestore);
methodContent.codeAtMethodEnd.push(ts.createExpressionStatement(methodCall));
}
}
}
public addHostAttributes(legacyHostAttributes: ts.ObjectLiteralExpression) {
for(const listener of legacyHostAttributes.properties) {
const propertyAssignment = codeUtils.assertNodeKind(listener, ts.SyntaxKind.PropertyAssignment) as ts.PropertyAssignment;
if(!propertyAssignment.name) {
throw new Error("Listener must have event name");
}
let attributeNameLiteral: ts.StringLiteral;
if(propertyAssignment.name.kind === ts.SyntaxKind.StringLiteral) {
attributeNameLiteral = propertyAssignment.name;
} else if(propertyAssignment.name.kind === ts.SyntaxKind.Identifier) {
attributeNameLiteral = ts.createStringLiteral(propertyAssignment.name.text);
} else {
throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.name.kind]}`);
}
let attributeValueLiteral: ts.StringLiteral | ts.NumericLiteral;
if(propertyAssignment.initializer.kind === ts.SyntaxKind.StringLiteral) {
attributeValueLiteral = propertyAssignment.initializer as ts.StringLiteral;
} else if(propertyAssignment.initializer.kind === ts.SyntaxKind.NumericLiteral) {
attributeValueLiteral = propertyAssignment.initializer as ts.NumericLiteral;
} else {
throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.initializer.kind]}`);
}
const methodCall = ts.createCall(ts.createPropertyAccess(ts.createThis(), "_ensureAttribute"), [], [attributeNameLiteral, attributeValueLiteral]);
this.getMethodContent("ready").codeAtMethodEnd.push(ts.createExpressionStatement(methodCall));
}
}
public addLegacyLifecycleMethod(name: LegacyLifecycleMethodName, method: ts.MethodDeclaration) {
const content = this.getMethodContent(name);
if(content.existingMethod) {
throw new Error(`Legacy lifecycle method ${name} already added`);
}
content.existingMethod = method;
}
public buildNewMethods(): LifecycleMethod[] {
const result = [];
for(const [name, content] of this.methods) {
const newMethod = this.createLifecycleMethod(name, content.existingMethod, content.codeAtMethodStart, content.codeAtMethodEnd);
if(!newMethod) continue;
result.push({
name,
originalPos: content.existingMethod ? content.existingMethod.pos : -1,
method: newMethod
})
}
return result;
}
private createLifecycleMethod(name: string, methodDecl: ts.MethodDeclaration | undefined, codeAtStart: ts.Statement[], codeAtEnd: ts.Statement[]): ts.MethodDeclaration | undefined {
return codeUtils.createMethod(name, methodDecl, codeAtStart, codeAtEnd, true);
}
}

View File

@ -0,0 +1,301 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ts from "typescript";
import * as fs from "fs";
import * as path from "path";
import { unexpectedValue } from "../utils/unexpectedValue";
import * as codeUtils from "../utils/codeUtils";
import {CommentsParser} from '../utils/commentsParser';
export class LegacyPolymerComponentParser {
public constructor(private readonly rootDir: string, private readonly htmlFiles: Set<string>) {
}
public async parse(jsFile: string): Promise<ParsedPolymerComponent | null> {
const sourceFile: ts.SourceFile = this.parseJsFile(jsFile);
const legacyComponent = this.tryParseLegacyComponent(sourceFile);
if (legacyComponent) {
return legacyComponent;
}
return null;
}
private parseJsFile(jsFile: string): ts.SourceFile {
return ts.createSourceFile(jsFile, fs.readFileSync(path.resolve(this.rootDir, jsFile)).toString(), ts.ScriptTarget.ES2015, true);
}
private tryParseLegacyComponent(sourceFile: ts.SourceFile): ParsedPolymerComponent | null {
const polymerFuncCalls: ts.CallExpression[] = [];
function addPolymerFuncCall(node: ts.Node) {
if(node.kind === ts.SyntaxKind.CallExpression) {
const callExpression: ts.CallExpression = node as ts.CallExpression;
if(callExpression.expression.kind === ts.SyntaxKind.Identifier) {
const identifier = callExpression.expression as ts.Identifier;
if(identifier.text === "Polymer") {
polymerFuncCalls.push(callExpression);
}
}
}
ts.forEachChild(node, addPolymerFuncCall);
}
addPolymerFuncCall(sourceFile);
if (polymerFuncCalls.length === 0) {
return null;
}
if (polymerFuncCalls.length > 1) {
throw new Error("Each .js file must contain only one Polymer component");
}
const parsedPath = path.parse(sourceFile.fileName);
const htmlFullPath = path.format({
dir: parsedPath.dir,
name: parsedPath.name,
ext: ".html"
});
if (!this.htmlFiles.has(htmlFullPath)) {
throw new Error("Legacy .js component dosn't have associated .html file");
}
const polymerFuncCall = polymerFuncCalls[0];
if(polymerFuncCall.arguments.length !== 1) {
throw new Error("The Polymer function must be called with exactly one parameter");
}
const argument = polymerFuncCall.arguments[0];
if(argument.kind !== ts.SyntaxKind.ObjectLiteralExpression) {
throw new Error("The parameter for Polymer function must be ObjectLiteralExpression (i.e. '{...}')");
}
const infoArg = argument as ts.ObjectLiteralExpression;
return {
jsFile: sourceFile.fileName,
htmlFile: htmlFullPath,
parsedFile: sourceFile,
polymerFuncCallExpr: polymerFuncCalls[0],
componentSettings: this.parseLegacyComponentSettings(infoArg),
};
}
private parseLegacyComponentSettings(info: ts.ObjectLiteralExpression): LegacyPolymerComponentSettings {
const props: Map<string, ts.ObjectLiteralElementLike> = new Map();
for(const property of info.properties) {
const name = property.name;
if (name === undefined) {
throw new Error("Property name is not defined");
}
switch(name.kind) {
case ts.SyntaxKind.Identifier:
case ts.SyntaxKind.StringLiteral:
if (props.has(name.text)) {
throw new Error(`Property ${name.text} appears more than once`);
}
props.set(name.text, property);
break;
case ts.SyntaxKind.ComputedPropertyName:
continue;
default:
unexpectedValue(ts.SyntaxKind[name.kind]);
}
}
if(props.has("_noAccessors")) {
throw new Error("_noAccessors is not supported");
}
const legacyLifecycleMethods: LegacyLifecycleMethods = new Map();
for(const name of LegacyLifecycleMethodsArray) {
const methodDecl = this.getLegacyMethodDeclaration(props, name);
if(methodDecl) {
legacyLifecycleMethods.set(name, methodDecl);
}
}
const ordinaryMethods: OrdinaryMethods = new Map();
const ordinaryShorthandProperties: OrdinaryShorthandProperties = new Map();
const ordinaryGetAccessors: OrdinaryGetAccessors = new Map();
const ordinaryPropertyAssignments: OrdinaryPropertyAssignments = new Map();
for(const [name, val] of props) {
if(RESERVED_NAMES.hasOwnProperty(name)) continue;
switch(val.kind) {
case ts.SyntaxKind.MethodDeclaration:
ordinaryMethods.set(name, val as ts.MethodDeclaration);
break;
case ts.SyntaxKind.ShorthandPropertyAssignment:
ordinaryShorthandProperties.set(name, val as ts.ShorthandPropertyAssignment);
break;
case ts.SyntaxKind.GetAccessor:
ordinaryGetAccessors.set(name, val as ts.GetAccessorDeclaration);
break;
case ts.SyntaxKind.PropertyAssignment:
ordinaryPropertyAssignments.set(name, val as ts.PropertyAssignment);
break;
default:
throw new Error(`Unsupported element kind: ${ts.SyntaxKind[val.kind]}`);
}
//ordinaryMethods.set(name, tsUtils.assertNodeKind(val, ts.SyntaxKind.MethodDeclaration) as ts.MethodDeclaration);
}
const eventsComments: string[] = this.getEventsComments(info.getFullText());
return {
reservedDeclarations: {
is: this.getStringLiteralValueWithComments(this.getLegacyPropertyInitializer(props, "is")),
_legacyUndefinedCheck: this.getBooleanLiteralValueWithComments(this.getLegacyPropertyInitializer(props, "_legacyUndefinedCheck")),
properties: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "properties")),
behaviors: this.getArrayLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "behaviors")),
observers: this.getArrayLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "observers")),
listeners: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "listeners")),
hostAttributes: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "hostAttributes")),
keyBindings: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "keyBindings")),
},
eventsComments: eventsComments,
lifecycleMethods: legacyLifecycleMethods,
ordinaryMethods: ordinaryMethods,
ordinaryShorthandProperties: ordinaryShorthandProperties,
ordinaryGetAccessors: ordinaryGetAccessors,
ordinaryPropertyAssignments: ordinaryPropertyAssignments,
};
}
private convertLegacyProeprtyInitializer<T>(initializer: LegacyPropertyInitializer | undefined, converter: (exp: ts.Expression) => T): DataWithComments<T> | undefined {
if(!initializer) {
return undefined;
}
return {
data: converter(initializer.data),
leadingComments: initializer.leadingComments,
}
}
private getObjectLiteralExpressionWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<ts.ObjectLiteralExpression> | undefined {
return this.convertLegacyProeprtyInitializer(initializer,
expr => codeUtils.getObjectLiteralExpression(expr));
}
private getStringLiteralValueWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<string> | undefined {
return this.convertLegacyProeprtyInitializer(initializer,
expr => codeUtils.getStringLiteralValue(expr));
}
private getBooleanLiteralValueWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<boolean> | undefined {
return this.convertLegacyProeprtyInitializer(initializer,
expr => codeUtils.getBooleanLiteralValue(expr));
}
private getArrayLiteralExpressionWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<ts.ArrayLiteralExpression> | undefined {
return this.convertLegacyProeprtyInitializer(initializer,
expr => codeUtils.getArrayLiteralExpression(expr));
}
private getLegacyPropertyInitializer(props: Map<String, ts.ObjectLiteralElementLike>, propName: string): LegacyPropertyInitializer | undefined {
const property = props.get(propName);
if (!property) {
return undefined;
}
const assignment = codeUtils.getPropertyAssignment(property);
if (!assignment) {
return undefined;
}
const comments: string[] = codeUtils.getLeadingComments(property)
.filter(c => !this.isEventComment(c));
return {
data: assignment.initializer,
leadingComments: comments,
};
}
private isEventComment(comment: string): boolean {
return comment.indexOf('@event') >= 0;
}
private getEventsComments(polymerComponentSource: string): string[] {
return CommentsParser.collectAllComments(polymerComponentSource)
.filter(c => this.isEventComment(c));
}
private getLegacyMethodDeclaration(props: Map<String, ts.ObjectLiteralElementLike>, propName: string): ts.MethodDeclaration | undefined {
const property = props.get(propName);
if (!property) {
return undefined;
}
return codeUtils.assertNodeKind(property, ts.SyntaxKind.MethodDeclaration) as ts.MethodDeclaration;
}
}
export type ParsedPolymerComponent = LegacyPolymerComponent;
export interface LegacyPolymerComponent {
jsFile: string;
htmlFile: string;
parsedFile: ts.SourceFile;
polymerFuncCallExpr: ts.CallExpression;
componentSettings: LegacyPolymerComponentSettings;
}
export interface LegacyReservedDeclarations {
is?: DataWithComments<string>;
_legacyUndefinedCheck?: DataWithComments<boolean>;
properties?: DataWithComments<ts.ObjectLiteralExpression>;
behaviors?: DataWithComments<ts.ArrayLiteralExpression>,
observers? :DataWithComments<ts.ArrayLiteralExpression>,
listeners? :DataWithComments<ts.ObjectLiteralExpression>,
hostAttributes?: DataWithComments<ts.ObjectLiteralExpression>,
keyBindings?: DataWithComments<ts.ObjectLiteralExpression>,
}
export const LegacyLifecycleMethodsArray = <const>["beforeRegister", "registered", "created", "ready", "attached" , "detached", "attributeChanged"];
export type LegacyLifecycleMethodName = typeof LegacyLifecycleMethodsArray[number];
export type LegacyLifecycleMethods = Map<LegacyLifecycleMethodName, ts.MethodDeclaration>;
export type OrdinaryMethods = Map<string, ts.MethodDeclaration>;
export type OrdinaryShorthandProperties = Map<string, ts.ShorthandPropertyAssignment>;
export type OrdinaryGetAccessors = Map<string, ts.GetAccessorDeclaration>;
export type OrdinaryPropertyAssignments = Map<string, ts.PropertyAssignment>;
export type ReservedName = LegacyLifecycleMethodName | keyof LegacyReservedDeclarations;
export const RESERVED_NAMES: {[x in ReservedName]: boolean} = {
attached: true,
detached: true,
ready: true,
created: true,
beforeRegister: true,
registered: true,
attributeChanged: true,
is: true,
_legacyUndefinedCheck: true,
properties: true,
behaviors: true,
observers: true,
listeners: true,
hostAttributes: true,
keyBindings: true,
};
export interface LegacyPolymerComponentSettings {
reservedDeclarations: LegacyReservedDeclarations;
lifecycleMethods: LegacyLifecycleMethods,
ordinaryMethods: OrdinaryMethods,
ordinaryShorthandProperties: OrdinaryShorthandProperties,
ordinaryGetAccessors: OrdinaryGetAccessors,
ordinaryPropertyAssignments: OrdinaryPropertyAssignments,
eventsComments: string[];
}
export interface DataWithComments<T> {
data: T;
leadingComments: string[];
}
type LegacyPropertyInitializer = DataWithComments<ts.Expression>;

View File

@ -0,0 +1,142 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {DataWithComments, LegacyPolymerComponent, LegacyReservedDeclarations, OrdinaryGetAccessors, OrdinaryMethods, OrdinaryPropertyAssignments, OrdinaryShorthandProperties} from './polymerComponentParser';
import * as ts from 'typescript';
import * as codeUtils from '../utils/codeUtils';
import {LifecycleMethod} from './lifecycleMethodsBuilder';
import {PolymerClassBuilder} from '../utils/polymerClassBuilder';
import {SyntaxKind} from 'typescript';
export interface ClassBasedPolymerElement {
classDeclaration: ts.ClassDeclaration;
componentRegistration: ts.ExpressionStatement;
eventsComments: string[];
generatedComments: string[];
}
export class PolymerElementBuilder {
private readonly reservedDeclarations: LegacyReservedDeclarations;
private readonly classBuilder: PolymerClassBuilder;
private mixins: ts.ExpressionWithTypeArguments | null;
public constructor(private readonly legacyComponent: LegacyPolymerComponent, className: string) {
this.reservedDeclarations = legacyComponent.componentSettings.reservedDeclarations;
this.classBuilder = new PolymerClassBuilder(className);
this.mixins = null;
}
public addIsAccessor(tagName: string) {
this.classBuilder.addIsAccessor(this.createIsAccessor(tagName));
}
public addPolymerPropertiesAccessor(legacyProperties: DataWithComments<ts.ObjectLiteralExpression>) {
const returnStatement = ts.createReturn(legacyProperties.data);
const block = ts.createBlock([returnStatement]);
let propertiesAccessor = ts.createGetAccessor(undefined, [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "properties", [], undefined, block);
if(legacyProperties.leadingComments.length > 0) {
propertiesAccessor = codeUtils.restoreLeadingComments(propertiesAccessor, legacyProperties.leadingComments);
}
this.classBuilder.addPolymerPropertiesAccessor(legacyProperties.data.pos, propertiesAccessor);
}
public addPolymerPropertiesObservers(legacyObservers: ts.ArrayLiteralExpression) {
const returnStatement = ts.createReturn(legacyObservers);
const block = ts.createBlock([returnStatement]);
const propertiesAccessor = ts.createGetAccessor(undefined, [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "observers", [], undefined, block);
this.classBuilder.addPolymerObserversAccessor(legacyObservers.pos, propertiesAccessor);
}
public addKeyBindings(keyBindings: ts.ObjectLiteralExpression) {
//In Polymer 2 keyBindings must be a property with get accessor
const returnStatement = ts.createReturn(keyBindings);
const block = ts.createBlock([returnStatement]);
const keyBindingsAccessor = ts.createGetAccessor(undefined, [], "keyBindings", [], undefined, block);
this.classBuilder.addGetAccessor(keyBindings.pos, keyBindingsAccessor);
}
public addOrdinaryMethods(ordinaryMethods: OrdinaryMethods) {
for(const [name, method] of ordinaryMethods) {
this.classBuilder.addMethod(method.pos, method);
}
}
public addOrdinaryGetAccessors(ordinaryGetAccessors: OrdinaryGetAccessors) {
for(const [name, accessor] of ordinaryGetAccessors) {
this.classBuilder.addGetAccessor(accessor.pos, accessor);
}
}
public addOrdinaryShorthandProperties(ordinaryShorthandProperties: OrdinaryShorthandProperties) {
for (const [name, property] of ordinaryShorthandProperties) {
this.classBuilder.addClassFieldInitializer(property.name, property.name);
}
}
public addOrdinaryPropertyAssignments(ordinaryPropertyAssignments: OrdinaryPropertyAssignments) {
for (const [name, property] of ordinaryPropertyAssignments) {
const propertyName = codeUtils.assertNodeKind(property.name, ts.SyntaxKind.Identifier) as ts.Identifier;
this.classBuilder.addClassFieldInitializer(propertyName, property.initializer);
}
}
public addMixin(name: string, mixinArguments?: ts.Expression[]) {
let fullMixinArguments: ts.Expression[] = [];
if(mixinArguments) {
fullMixinArguments.push(...mixinArguments);
}
if(this.mixins) {
fullMixinArguments.push(this.mixins.expression);
}
if(fullMixinArguments.length > 0) {
this.mixins = ts.createExpressionWithTypeArguments([], ts.createCall(codeUtils.createNameExpression(name), [], fullMixinArguments.length > 0 ? fullMixinArguments : undefined));
}
else {
this.mixins = ts.createExpressionWithTypeArguments([], codeUtils.createNameExpression(name));
}
}
public addClassJSDocComments(lines: string[]) {
this.classBuilder.addClassJSDocComments(lines);
}
public build(): ClassBasedPolymerElement {
if(this.mixins) {
this.classBuilder.setBaseType(this.mixins);
}
const className = this.classBuilder.className;
const callExpression = ts.createCall(ts.createPropertyAccess(ts.createIdentifier("customElements"), "define"), undefined, [ts.createPropertyAccess(ts.createIdentifier(className), "is"), ts.createIdentifier(className)]);
const classBuilderResult = this.classBuilder.build();
return {
classDeclaration: classBuilderResult.classDeclaration,
generatedComments: classBuilderResult.generatedComments,
componentRegistration: ts.createExpressionStatement(callExpression),
eventsComments: this.legacyComponent.componentSettings.eventsComments,
};
}
private createIsAccessor(tagName: string): ts.GetAccessorDeclaration {
const returnStatement = ts.createReturn(ts.createStringLiteral(tagName));
const block = ts.createBlock([returnStatement]);
const accessor = ts.createGetAccessor([], [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "is", [], undefined, block);
return codeUtils.addReplacableCommentAfterNode(accessor, "eventsComments");
}
public addLifecycleMethods(newLifecycleMethods: LifecycleMethod[]) {
for(const lifecycleMethod of newLifecycleMethods) {
this.classBuilder.addLifecycleMethod(lifecycleMethod.name, lifecycleMethod.originalPos, lifecycleMethod.method);
}
}
}

View File

@ -0,0 +1,248 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {LegacyPolymerComponent} from './polymerComponentParser';
import * as ts from 'typescript';
import * as codeUtils from '../utils/codeUtils';
import * as path from "path";
import * as fs from "fs";
import {LegacyPolymerFuncReplaceResult} from './legacyPolymerFuncReplacer';
import {CommentsParser} from '../utils/commentsParser';
export interface UpdatedFileWriterParameters {
out: string;
inplace: boolean;
writeOutput: boolean;
rootDir: string;
}
interface Replacement {
start: number;
length: number;
newText: string;
}
const elementRegistrationRegex = /^(\s*)customElements.define\((\w+).is, \w+\);$/m;
const maxLineLength = 80;
export class UpdatedFileWriter {
public constructor(private readonly component: LegacyPolymerComponent, private readonly params: UpdatedFileWriterParameters) {
}
public write(replaceResult: LegacyPolymerFuncReplaceResult, eventsComments: string[], generatedComments: string[]) {
const options: ts.PrinterOptions = {
removeComments: false,
newLine: ts.NewLineKind.LineFeed,
};
const printer = ts.createPrinter(options);
let newContent = codeUtils.applyNewLines(printer.printFile(replaceResult.file));
//ts printer doesn't keep original formatting of the file (spacing, new lines, comments, etc...).
//The following code tries restore original formatting
const existingComments = this.collectAllComments(newContent, []);
newContent = this.restoreEventsComments(newContent, eventsComments, existingComments);
newContent = this.restoreLeadingComments(newContent, replaceResult.leadingComments);
newContent = this.restoreFormating(printer, newContent);
newContent = this.splitLongLines(newContent);
newContent = this.addCommentsWarnings(newContent, generatedComments);
if (this.params.writeOutput) {
const outDir = this.params.inplace ? this.params.rootDir : this.params.out;
const fullOutPath = path.resolve(outDir, this.component.jsFile);
const fullOutDir = path.dirname(fullOutPath);
if (!fs.existsSync(fullOutDir)) {
fs.mkdirSync(fullOutDir, {
recursive: true,
mode: fs.lstatSync(this.params.rootDir).mode
});
}
fs.writeFileSync(fullOutPath, newContent);
}
}
private restoreEventsComments(content: string, eventsComments: string[], existingComments: Map<string, number>): string {
//In some cases Typescript compiler keep existing comments. These comments
// must not be restored here
eventsComments = eventsComments.filter(c => !existingComments.has(this.getNormalizedComment(c)));
return codeUtils.replaceComment(content, "eventsComments", "\n" + eventsComments.join("\n\n") + "\n");
}
private restoreLeadingComments(content: string, leadingComments: string[]): string {
return leadingComments.reduce(
(newContent, comment, commentIndex) =>
codeUtils.replaceComment(newContent, String(commentIndex), comment),
content);
}
private restoreFormating(printer: ts.Printer, newContent: string): string {
const originalFile = this.component.parsedFile;
const newFile = ts.createSourceFile(originalFile.fileName, newContent, originalFile.languageVersion, true, ts.ScriptKind.JS);
const textMap = new Map<ts.SyntaxKind, Map<string, Set<string>>>();
const comments = new Set<string>();
this.collectAllStrings(printer, originalFile, textMap);
const replacements: Replacement[] = [];
this.collectReplacements(printer, newFile, textMap, replacements);
replacements.sort((a, b) => b.start - a.start);
let result = newFile.getFullText();
let prevReplacement: Replacement | null = null;
for (const replacement of replacements) {
if (prevReplacement) {
if (replacement.start + replacement.length > prevReplacement.start) {
throw new Error('Internal error! Replacements must not intersect');
}
}
result = result.substring(0, replacement.start) + replacement.newText + result.substring(replacement.start + replacement.length);
prevReplacement = replacement;
}
return result;
}
private splitLongLines(content: string): string {
content = content.replace(elementRegistrationRegex, (match, indent, className) => {
if (match.length > maxLineLength) {
return `${indent}customElements.define(${className}.is,\n` +
`${indent} ${className});`;
}
else {
return match;
}
});
return content
.replace(
"Polymer.LegacyDataMixin(Polymer.GestureEventListeners(Polymer.LegacyElementMixin(Polymer.Element)))",
"Polymer.LegacyDataMixin(\nPolymer.GestureEventListeners(\nPolymer.LegacyElementMixin(\nPolymer.Element)))")
.replace(
"Polymer.GestureEventListeners(Polymer.LegacyElementMixin(Polymer.Element))",
"Polymer.GestureEventListeners(\nPolymer.LegacyElementMixin(\nPolymer.Element))");
}
private addCommentsWarnings(newContent: string, generatedComments: string[]): string {
const expectedComments = this.collectAllComments(this.component.parsedFile.getFullText(), generatedComments);
const newComments = this.collectAllComments(newContent, []);
const commentsWarnings = [];
for (const [text, count] of expectedComments) {
const newCount = newComments.get(text);
if (!newCount) {
commentsWarnings.push(`Comment '${text}' is missing in the new content.`);
}
else if (newCount != count) {
commentsWarnings.push(`Comment '${text}' appears ${newCount} times in the new file and ${count} times in the old file.`);
}
}
for (const [text, newCount] of newComments) {
if (!expectedComments.has(text)) {
commentsWarnings.push(`Comment '${text}' appears only in the new content`);
}
}
if (commentsWarnings.length === 0) {
return newContent;
}
let commentsProblemStr = "";
if (commentsWarnings.length > 0) {
commentsProblemStr = commentsWarnings.join("-----------------------------\n");
console.log(commentsProblemStr);
}
return "//This file has the following problems with comments:\n" + commentsProblemStr + "\n" + newContent;
}
private collectAllComments(content: string, additionalComments: string[]): Map<string, number> {
const comments = CommentsParser.collectAllComments(content);
comments.push(...additionalComments);
const result = new Map<string, number>();
for (const comment of comments) {
let normalizedComment = this.getNormalizedComment(comment);
const count = result.get(normalizedComment);
if (count) {
result.set(normalizedComment, count + 1);
} else {
result.set(normalizedComment, 1);
}
}
return result;
}
private getNormalizedComment(comment: string): string {
if(comment.startsWith('/**')) {
comment = comment.replace(/^\s+\*/gm, "*");
}
return comment;
}
private collectAllStrings(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>) {
const formattedText = printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile())
const originalText = node.getFullText();
this.addIfNotExists(map, node.kind, formattedText, originalText);
ts.forEachChild(node, child => this.collectAllStrings(printer, child, map));
}
private collectReplacements(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>, replacements: Replacement[]) {
if(node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.Identifier || node.kind === ts.SyntaxKind.StringLiteral || node.kind === ts.SyntaxKind.NumericLiteral) {
return;
}
const replacement = this.getReplacement(printer, node, map);
if(replacement) {
replacements.push(replacement);
return;
}
ts.forEachChild(node, child => this.collectReplacements(printer, child, map, replacements));
}
private addIfNotExists(map: Map<ts.SyntaxKind, Map<string, Set<string>>>, kind: ts.SyntaxKind, formattedText: string, originalText: string) {
let mapForKind = map.get(kind);
if(!mapForKind) {
mapForKind = new Map();
map.set(kind, mapForKind);
}
let existingOriginalText = mapForKind.get(formattedText);
if(!existingOriginalText) {
existingOriginalText = new Set<string>();
mapForKind.set(formattedText, existingOriginalText);
//throw new Error(`Different formatting of the same string exists. Kind: ${ts.SyntaxKind[kind]}.\nFormatting 1:\n${originalText}\nFormatting2:\n${existingOriginalText}\n `);
}
existingOriginalText.add(originalText);
}
private getReplacement(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>): Replacement | undefined {
const replacementsForKind = map.get(node.kind);
if(!replacementsForKind) {
return;
}
// Use printer instead of getFullText to "isolate" node content.
// node.getFullText returns text with indents from the original file.
const newText = printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile());
const originalSet = replacementsForKind.get(newText);
if(!originalSet || originalSet.size === 0) {
return;
}
if(originalSet.size >= 2) {
console.log(`Multiple replacements possible. Formatting of some lines can be changed`);
}
const replacementText: string = originalSet.values().next().value;
const nodeText = node.getFullText();
return {
start: node.pos,
length: nodeText.length,//Do not use newText here!
newText: replacementText,
}
}
}

View File

@ -0,0 +1,168 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as fs from "fs";
import * as path from "path";
import {LegacyPolymerComponent, LegacyPolymerComponentParser} from './funcToClassConversion/polymerComponentParser';
import {ClassBasedPolymerElement} from './funcToClassConversion/polymerElementBuilder';
import {PolymerFuncToClassBasedConverter} from './funcToClassConversion/funcToClassBasedElementConverter';
import {LegacyPolymerFuncReplacer} from './funcToClassConversion/legacyPolymerFuncReplacer';
import {UpdatedFileWriter} from './funcToClassConversion/updatedFileWriter';
import {CommandLineParser} from './utils/commandLineParser';
interface UpdaterParameters {
htmlFiles: Set<string>;
jsFiles: Set<string>;
out: string;
inplace: boolean;
writeOutput: boolean;
rootDir: string;
}
interface InputFilesFilter {
includeDir(path: string): boolean;
includeFile(path: string): boolean;
}
function addFile(filePath: string, params: UpdaterParameters, filter: InputFilesFilter) {
const parsedPath = path.parse(filePath);
const ext = parsedPath.ext.toLowerCase();
const relativePath = path.relative(params.rootDir, filePath);
if(!filter.includeFile(relativePath)) return;
if(relativePath.startsWith("../")) {
throw new Error(`${filePath} is not in rootDir ${params.rootDir}`);
}
if(ext === ".html") {
params.htmlFiles.add(relativePath);
} if(ext === ".js") {
params.jsFiles.add(relativePath);
}
}
function addDirectory(dirPath: string, params: UpdaterParameters, recursive: boolean, filter: InputFilesFilter): void {
const entries = fs.readdirSync(dirPath, {withFileTypes: true});
for(const entry of entries) {
const dirEnt = entry as fs.Dirent;
const fullPath = path.join(dirPath, dirEnt.name);
const relativePath = path.relative(params.rootDir, fullPath);
if(dirEnt.isDirectory()) {
if (!filter.includeDir(relativePath)) continue;
if(recursive) {
addDirectory(fullPath, params, recursive, filter);
}
}
else if(dirEnt.isFile()) {
addFile(fullPath, params, filter);
} else {
throw Error(`Unsupported dir entry '${entry.name}' in '${fullPath}'`);
}
}
}
async function updateLegacyComponent(component: LegacyPolymerComponent, params: UpdaterParameters) {
const classBasedElement: ClassBasedPolymerElement = PolymerFuncToClassBasedConverter.convert(component);
const replacer = new LegacyPolymerFuncReplacer(component);
const replaceResult = replacer.replace(classBasedElement);
try {
const writer = new UpdatedFileWriter(component, params);
writer.write(replaceResult, classBasedElement.eventsComments, classBasedElement.generatedComments);
}
finally {
replaceResult.dispose();
}
}
async function main() {
const params: UpdaterParameters = await getParams();
if(params.jsFiles.size === 0) {
console.log("No files found");
return;
}
const legacyPolymerComponentParser = new LegacyPolymerComponentParser(params.rootDir, params.htmlFiles)
for(const jsFile of params.jsFiles) {
console.log(`Processing ${jsFile}`);
const legacyComponent = await legacyPolymerComponentParser.parse(jsFile);
if(legacyComponent) {
await updateLegacyComponent(legacyComponent, params);
continue;
}
}
}
interface CommandLineParameters {
src: string[];
recursive: boolean;
excludes: string[];
out: string;
inplace: boolean;
noOutput: boolean;
rootDir: string;
}
async function getParams(): Promise<UpdaterParameters> {
const parser = new CommandLineParser({
src: CommandLineParser.createStringArrayOption("src", ".js file or folder to process", []),
recursive: CommandLineParser.createBooleanOption("r", "process folder recursive", false),
excludes: CommandLineParser.createStringArrayOption("exclude", "List of file prefixes to exclude. If relative file path starts with one of the prefixes, it will be excluded", []),
out: CommandLineParser.createStringOption("out", "Output folder.", null),
rootDir: CommandLineParser.createStringOption("root", "Root directory for src files", "/"),
inplace: CommandLineParser.createBooleanOption("i", "Update files inplace", false),
noOutput: CommandLineParser.createBooleanOption("noout", "Do everything, but do not write anything to files", false),
});
const commandLineParams: CommandLineParameters = parser.parse(process.argv) as CommandLineParameters;
const params: UpdaterParameters = {
htmlFiles: new Set(),
jsFiles: new Set(),
writeOutput: !commandLineParams.noOutput,
inplace: commandLineParams.inplace,
out: commandLineParams.out,
rootDir: path.resolve(commandLineParams.rootDir)
};
if(params.writeOutput && !params.inplace && !params.out) {
throw new Error("You should specify output directory (--out directory_name)");
}
const filter = new ExcludeFilesFilter(commandLineParams.excludes);
for(const srcPath of commandLineParams.src) {
const resolvedPath = path.resolve(params.rootDir, srcPath);
if(fs.lstatSync(resolvedPath).isFile()) {
addFile(resolvedPath, params, filter);
} else {
addDirectory(resolvedPath, params, commandLineParams.recursive, filter);
}
}
return params;
}
class ExcludeFilesFilter implements InputFilesFilter {
public constructor(private readonly excludes: string[]) {
}
includeDir(path: string): boolean {
return this.excludes.every(exclude => !path.startsWith(exclude));
}
includeFile(path: string): boolean {
return this.excludes.every(exclude => !path.startsWith(exclude));
}
}
main().then(() => {
process.exit(0);
}).catch(e => {
console.error(e);
process.exit(1);
});

View File

@ -0,0 +1,183 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ts from 'typescript';
import {SyntaxKind} from 'typescript';
import {Node} from 'typescript';
export function assertNodeKind<T extends U, U extends ts.Node>(node: U, expectedKind: ts.SyntaxKind): T {
if (node.kind !== expectedKind) {
throw new Error(`Invlid node kind. Expected: ${ts.SyntaxKind[expectedKind]}, actual: ${ts.SyntaxKind[node.kind]}`);
}
return node as T;
}
export function assertNodeKindOrUndefined<T extends U, U extends ts.Node>(node: U | undefined, expectedKind: ts.SyntaxKind): T | undefined {
if (!node) {
return undefined;
}
return assertNodeKind<T, U>(node, expectedKind);
}
export function getPropertyAssignment(expression?: ts.ObjectLiteralElementLike): ts.PropertyAssignment | undefined {
return assertNodeKindOrUndefined(expression, ts.SyntaxKind.PropertyAssignment);
}
export function getStringLiteralValue(expression: ts.Expression): string {
const literal: ts.StringLiteral = assertNodeKind(expression, ts.SyntaxKind.StringLiteral);
return literal.text;
}
export function getBooleanLiteralValue(expression: ts.Expression): boolean {
if (expression.kind === ts.SyntaxKind.TrueKeyword) {
return true;
}
if (expression.kind === ts.SyntaxKind.FalseKeyword) {
return false;
}
throw new Error(`Invalid expression kind - ${expression.kind}`);
}
export function getObjectLiteralExpression(expression: ts.Expression): ts.ObjectLiteralExpression {
return assertNodeKind(expression, ts.SyntaxKind.ObjectLiteralExpression);
}
export function getArrayLiteralExpression(expression: ts.Expression): ts.ArrayLiteralExpression {
return assertNodeKind(expression, ts.SyntaxKind.ArrayLiteralExpression);
}
export function replaceNode(file: ts.SourceFile, originalNode: ts.Node, newNode: ts.Node): ts.TransformationResult<ts.SourceFile> {
const nodeReplacerTransformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => {
const visitor: ts.Visitor = (node) => {
if(node === originalNode) {
return newNode;
}
return ts.visitEachChild(node, visitor, context);
};
return source => ts.visitNode(source, visitor);
};
return ts.transform(file, [nodeReplacerTransformer]);
}
export type NameExpression = ts.Identifier | ts.ThisExpression | ts.PropertyAccessExpression;
export function createNameExpression(fullPath: string): NameExpression {
const parts = fullPath.split(".");
let result: NameExpression = parts[0] === "this" ? ts.createThis() : ts.createIdentifier(parts[0]);
for(let i = 1; i < parts.length; i++) {
result = ts.createPropertyAccess(result, parts[i]);
}
return result;
}
const generatedCommentNewLineAfterText = "-Generated code - new line after - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -";
const generatedCommentNewLineBeforeText = "-Generated code - new line-before - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -";
const generatedCommentNewLineAfterRegExp = new RegExp("//" + generatedCommentNewLineAfterText, 'g');
const generatedCommentNewLineBeforeRegExp = new RegExp("//" + generatedCommentNewLineBeforeText + "\n", 'g');
const replacableCommentText = "- Replacepoint - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -";
export function addNewLineAfterNode<T extends ts.Node>(node: T): T {
const comment = ts.getSyntheticTrailingComments(node);
if(comment && comment.some(c => c.text === generatedCommentNewLineAfterText)) {
return node;
}
return ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, generatedCommentNewLineAfterText, true);
}
export function addNewLineBeforeNode<T extends ts.Node>(node: T): T {
const comment = ts.getSyntheticLeadingComments(node);
if(comment && comment.some(c => c.text === generatedCommentNewLineBeforeText)) {
return node;
}
return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, generatedCommentNewLineBeforeText, true);
}
export function applyNewLines(text: string): string {
return text.replace(generatedCommentNewLineAfterRegExp, "").replace(generatedCommentNewLineBeforeRegExp, "");
}
export function addReplacableCommentAfterNode<T extends ts.Node>(node: T, name: string): T {
return ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, replacableCommentText + name, true);
}
export function addReplacableCommentBeforeNode<T extends ts.Node>(node: T, name: string): T {
return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, replacableCommentText + name, true);
}
export function replaceComment(text: string, commentName: string, newContent: string): string {
return text.replace("//" + replacableCommentText + commentName, newContent);
}
export function createMethod(name: string, methodDecl: ts.MethodDeclaration | undefined, codeAtStart: ts.Statement[], codeAtEnd: ts.Statement[], callSuperMethod: boolean): ts.MethodDeclaration | undefined {
if(!methodDecl && (codeAtEnd.length > 0 || codeAtEnd.length > 0)) {
methodDecl = ts.createMethod([], [], undefined, name, undefined, [], [],undefined, ts.createBlock([]));
}
if(!methodDecl) {
return;
}
if (!methodDecl.body) {
throw new Error("Method must have a body");
}
if(methodDecl.parameters.length > 0) {
throw new Error("Methods with parameters are not supported");
}
let newStatements = [...codeAtStart];
if(callSuperMethod) {
const superCall: ts.CallExpression = ts.createCall(ts.createPropertyAccess(ts.createSuper(), assertNodeKind(methodDecl.name, ts.SyntaxKind.Identifier) as ts.Identifier), [], []);
const superCallExpression = ts.createExpressionStatement(superCall);
newStatements.push(superCallExpression);
}
newStatements.push(...codeAtEnd);
const newBody = ts.getMutableClone(methodDecl.body);
newStatements = newStatements.map(m => addNewLineAfterNode(m));
newStatements.splice(codeAtStart.length + 1, 0, ...newBody.statements);
newBody.statements = ts.createNodeArray(newStatements);
const newMethod = ts.getMutableClone(methodDecl);
newMethod.body = newBody;
return newMethod;
}
export function restoreLeadingComments<T extends Node>(node: T, originalComments: string[]): T {
if(originalComments.length === 0) {
return node;
}
for(const comment of originalComments) {
if(comment.startsWith("//")) {
node = ts.addSyntheticLeadingComment(node, SyntaxKind.SingleLineCommentTrivia, comment.substr(2), true);
} else if(comment.startsWith("/*")) {
if(!comment.endsWith("*/")) {
throw new Error(`Not support comment: ${comment}`);
}
node = ts.addSyntheticLeadingComment(node, SyntaxKind.MultiLineCommentTrivia, comment.substr(2, comment.length - 4), true);
} else {
throw new Error(`Not supported comment: ${comment}`);
}
}
return node;
}
export function getLeadingComments(node: ts.Node): string[] {
const nodeText = node.getFullText();
const commentRanges = ts.getLeadingCommentRanges(nodeText, 0);
if(!commentRanges) {
return [];
}
return commentRanges.map(range => nodeText.substring(range.pos, range.end))
}

View File

@ -0,0 +1,134 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed un der the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export class CommandLineParser {
public static createStringArrayOption(optionName: string, help: string, defaultValue: string[]): CommandLineArgument {
return new StringArrayOption(optionName, help, defaultValue);
}
public static createBooleanOption(optionName: string, help: string, defaultValue: boolean): CommandLineArgument {
return new BooleanOption(optionName, help, defaultValue);
}
public static createStringOption(optionName: string, help: string, defaultValue: string | null): CommandLineArgument {
return new StringOption(optionName, help, defaultValue);
}
public constructor(private readonly argumentTypes: {[name: string]: CommandLineArgument}) {
}
public parse(argv: string[]): object {
const result = Object.assign({});
let index = 2; //argv[0] - node interpreter, argv[1] - index.js
for(const argumentField in this.argumentTypes) {
result[argumentField] = this.argumentTypes[argumentField].getDefaultValue();
}
while(index < argv.length) {
let knownArgument = false;
for(const argumentField in this.argumentTypes) {
const argumentType = this.argumentTypes[argumentField];
const argumentValue = argumentType.tryRead(argv, index);
if(argumentValue) {
knownArgument = true;
index += argumentValue.consumed;
result[argumentField] = argumentValue.value;
break;
}
}
if(!knownArgument) {
throw new Error(`Unknown argument ${argv[index]}`);
}
}
return result;
}
}
interface CommandLineArgumentReadResult {
consumed: number;
value: any;
}
export interface CommandLineArgument {
getDefaultValue(): any;
tryRead(argv: string[], startIndex: number): CommandLineArgumentReadResult | null;
}
abstract class CommandLineOption implements CommandLineArgument {
protected constructor(protected readonly optionName: string, protected readonly help: string, private readonly defaultValue: any) {
}
public tryRead(argv: string[], startIndex: number): CommandLineArgumentReadResult | null {
if(argv[startIndex] !== "--" + this.optionName) {
return null;
}
const readArgumentsResult = this.readArguments(argv, startIndex + 1);
if(!readArgumentsResult) {
return null;
}
readArgumentsResult.consumed++; // Add option name
return readArgumentsResult;
}
public getDefaultValue(): any {
return this.defaultValue;
}
protected abstract readArguments(argv: string[], startIndex: number) : CommandLineArgumentReadResult | null;
}
class StringArrayOption extends CommandLineOption {
public constructor(optionName: string, help: string, defaultValue: string[]) {
super(optionName, help, defaultValue);
}
protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult {
const result = [];
let index = startIndex;
while(index < argv.length) {
if(argv[index].startsWith("--")) {
break;
}
result.push(argv[index]);
index++;
}
return {
consumed: index - startIndex,
value: result
}
}
}
class BooleanOption extends CommandLineOption {
public constructor(optionName: string, help: string, defaultValue: boolean) {
super(optionName, help, defaultValue);
}
protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult {
return {
consumed: 0,
value: true
}
}
}
class StringOption extends CommandLineOption {
public constructor(optionName: string, help: string, defaultValue: string | null) {
super(optionName, help, defaultValue);
}
protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult | null {
if(startIndex >= argv.length) {
return null;
}
return {
consumed: 1,
value: argv[startIndex]
}
}
}

View File

@ -0,0 +1,79 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
enum CommentScannerState {
Text,
SingleLineComment,
MultLineComment
}
export class CommentsParser {
public static collectAllComments(text: string): string[] {
const result: string[] = [];
let state = CommentScannerState.Text;
let pos = 0;
function readSingleLineComment() {
const startPos = pos;
while(pos < text.length && text[pos] !== '\n') {
pos++;
}
return text.substring(startPos, pos);
}
function readMultiLineComment() {
const startPos = pos;
while(pos < text.length) {
if(pos < text.length - 1 && text[pos] === '*' && text[pos + 1] === '/') {
pos += 2;
break;
}
pos++;
}
return text.substring(startPos, pos);
}
function skipString(lastChar: string) {
pos++;
while(pos < text.length) {
if(text[pos] === lastChar) {
pos++;
return;
} else if(text[pos] === '\\') {
pos+=2;
continue;
}
pos++;
}
}
while(pos < text.length - 1) {
if(text[pos] === '/' && text[pos + 1] === '/') {
result.push(readSingleLineComment());
} else if(text[pos] === '/' && text[pos + 1] === '*') {
result.push(readMultiLineComment());
} else if(text[pos] === "'") {
skipString("'");
} else if(text[pos] === '"') {
skipString('"');
} else if(text[pos] === '`') {
skipString('`');
} else if(text[pos] == '/') {
skipString('/');
} {
pos++;
}
}
return result;
}
}

View File

@ -0,0 +1,270 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ts from 'typescript';
import * as codeUtils from './codeUtils';
import {LegacyLifecycleMethodName, LegacyLifecycleMethodsArray} from '../funcToClassConversion/polymerComponentParser';
import {SyntaxKind} from 'typescript';
enum PolymerClassMemberType {
IsAccessor,
Constructor,
PolymerPropertiesAccessor,
PolymerObserversAccessor,
Method,
ExistingLifecycleMethod,
NewLifecycleMethod,
GetAccessor,
}
type PolymerClassMember = PolymerClassIsAccessor | PolymerClassConstructor | PolymerClassExistingLifecycleMethod | PolymerClassNewLifecycleMethod | PolymerClassSimpleMember;
interface PolymerClassExistingLifecycleMethod {
member: ts.MethodDeclaration;
memberType: PolymerClassMemberType.ExistingLifecycleMethod;
name: string;
lifecycleOrder: number;
originalPos: number;
}
interface PolymerClassNewLifecycleMethod {
member: ts.MethodDeclaration;
memberType: PolymerClassMemberType.NewLifecycleMethod;
name: string;
lifecycleOrder: number;
originalPos: -1
}
interface PolymerClassIsAccessor {
member: ts.GetAccessorDeclaration;
memberType: PolymerClassMemberType.IsAccessor;
originalPos: -1
}
interface PolymerClassConstructor {
member: ts.ConstructorDeclaration;
memberType: PolymerClassMemberType.Constructor;
originalPos: -1
}
interface PolymerClassSimpleMember {
memberType: PolymerClassMemberType.PolymerPropertiesAccessor | PolymerClassMemberType.PolymerObserversAccessor | PolymerClassMemberType.Method | PolymerClassMemberType.GetAccessor;
member: ts.ClassElement;
originalPos: number;
}
export interface PolymerClassBuilderResult {
classDeclaration: ts.ClassDeclaration;
generatedComments: string[];
}
export class PolymerClassBuilder {
private readonly members: PolymerClassMember[] = [];
public readonly constructorStatements: ts.Statement[] = [];
private baseType: ts.ExpressionWithTypeArguments | undefined;
private classJsDocComments: string[];
public constructor(public readonly className: string) {
this.classJsDocComments = [];
}
public addIsAccessor(accessor: ts.GetAccessorDeclaration) {
this.members.push({
member: accessor,
memberType: PolymerClassMemberType.IsAccessor,
originalPos: -1
});
}
public addPolymerPropertiesAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) {
this.members.push({
member: accessor,
memberType: PolymerClassMemberType.PolymerPropertiesAccessor,
originalPos: originalPos
});
}
public addPolymerObserversAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) {
this.members.push({
member: accessor,
memberType: PolymerClassMemberType.PolymerObserversAccessor,
originalPos: originalPos
});
}
public addClassFieldInitializer(name: string | ts.Identifier, initializer: ts.Expression) {
const assignment = ts.createAssignment(ts.createPropertyAccess(ts.createThis(), name), initializer);
this.constructorStatements.push(codeUtils.addNewLineAfterNode(ts.createExpressionStatement(assignment)));
}
public addMethod(originalPos: number, method: ts.MethodDeclaration) {
this.members.push({
member: method,
memberType: PolymerClassMemberType.Method,
originalPos: originalPos
});
}
public addGetAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) {
this.members.push({
member: accessor,
memberType: PolymerClassMemberType.GetAccessor,
originalPos: originalPos
});
}
public addLifecycleMethod(name: LegacyLifecycleMethodName, originalPos: number, method: ts.MethodDeclaration) {
const lifecycleOrder = LegacyLifecycleMethodsArray.indexOf(name);
if(lifecycleOrder < 0) {
throw new Error(`Invalid lifecycle name`);
}
if(originalPos >= 0) {
this.members.push({
member: method,
memberType: PolymerClassMemberType.ExistingLifecycleMethod,
originalPos: originalPos,
name: name,
lifecycleOrder: lifecycleOrder
})
} else {
this.members.push({
member: method,
memberType: PolymerClassMemberType.NewLifecycleMethod,
name: name,
lifecycleOrder: lifecycleOrder,
originalPos: -1
})
}
}
public setBaseType(type: ts.ExpressionWithTypeArguments) {
if(this.baseType) {
throw new Error("Class can have only one base type");
}
this.baseType = type;
}
public build(): PolymerClassBuilderResult {
let heritageClauses: ts.HeritageClause[] = [];
if (this.baseType) {
const extendClause = ts.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [this.baseType]);
heritageClauses.push(extendClause);
}
const finalMembers: PolymerClassMember[] = [];
const isAccessors = this.members.filter(member => member.memberType === PolymerClassMemberType.IsAccessor);
if(isAccessors.length !== 1) {
throw new Error("Class must have exactly one 'is'");
}
finalMembers.push(isAccessors[0]);
const constructorMember = this.createConstructor();
if(constructorMember) {
finalMembers.push(constructorMember);
}
const newLifecycleMethods: PolymerClassNewLifecycleMethod[] = [];
this.members.forEach(member => {
if(member.memberType === PolymerClassMemberType.NewLifecycleMethod) {
newLifecycleMethods.push(member);
}
});
const methodsWithKnownPosition = this.members.filter(member => member.originalPos >= 0);
methodsWithKnownPosition.sort((a, b) => a.originalPos - b.originalPos);
finalMembers.push(...methodsWithKnownPosition);
for(const newLifecycleMethod of newLifecycleMethods) {
//Number of methods is small - use brute force solution
let closestNextIndex = -1;
let closestNextOrderDiff: number = LegacyLifecycleMethodsArray.length;
let closestPrevIndex = -1;
let closestPrevOrderDiff: number = LegacyLifecycleMethodsArray.length;
for (let i = 0; i < finalMembers.length; i++) {
const member = finalMembers[i];
if (member.memberType !== PolymerClassMemberType.NewLifecycleMethod && member.memberType !== PolymerClassMemberType.ExistingLifecycleMethod) {
continue;
}
const orderDiff = member.lifecycleOrder - newLifecycleMethod.lifecycleOrder;
if (orderDiff > 0) {
if (orderDiff < closestNextOrderDiff) {
closestNextIndex = i;
closestNextOrderDiff = orderDiff;
}
} else if (orderDiff < 0) {
if (orderDiff < closestPrevOrderDiff) {
closestPrevIndex = i;
closestPrevOrderDiff = orderDiff;
}
}
}
let insertIndex;
if (closestNextIndex !== -1 || closestPrevIndex !== -1) {
insertIndex = closestNextOrderDiff < closestPrevOrderDiff ?
closestNextIndex : closestPrevIndex + 1;
} else {
insertIndex = Math.max(
finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.Constructor),
finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.IsAccessor),
finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.PolymerPropertiesAccessor),
finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.PolymerObserversAccessor),
);
if(insertIndex < 0) {
insertIndex = finalMembers.length;
} else {
insertIndex++;//Insert after
}
}
finalMembers.splice(insertIndex, 0, newLifecycleMethod);
}
//Asserts about finalMembers
const nonConstructorMembers = finalMembers.filter(m => m.memberType !== PolymerClassMemberType.Constructor);
if(nonConstructorMembers.length !== this.members.length) {
throw new Error(`Internal error! Some methods are missed`);
}
let classDeclaration = ts.createClassDeclaration(undefined, undefined, this.className, undefined, heritageClauses, finalMembers.map(m => m.member))
const generatedComments: string[] = [];
if(this.classJsDocComments.length > 0) {
const commentContent = '*\n' + this.classJsDocComments.map(line => `* ${line}`).join('\n') + '\n';
classDeclaration = ts.addSyntheticLeadingComment(classDeclaration, ts.SyntaxKind.MultiLineCommentTrivia, commentContent, true);
generatedComments.push(`/*${commentContent}*/`);
}
return {
classDeclaration,
generatedComments,
};
}
private createConstructor(): PolymerClassConstructor | null {
if(this.constructorStatements.length === 0) {
return null;
}
let superCall: ts.CallExpression = ts.createCall(ts.createSuper(), [], []);
const superCallExpression = ts.createExpressionStatement(superCall);
const statements = [superCallExpression, ...this.constructorStatements];
const constructorDeclaration = ts.createConstructor([], [], [], ts.createBlock(statements, true));
return {
memberType: PolymerClassMemberType.Constructor,
member: constructorDeclaration,
originalPos: -1
};
}
public addClassJSDocComments(lines: string[]) {
this.classJsDocComments.push(...lines);
}
}

View File

@ -0,0 +1,17 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export function unexpectedValue<T>(x: T): never {
throw new Error(`Unexpected value '${x}'`);
}

View File

@ -0,0 +1,67 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./js", /* Redirect output structure to the directory. */
"rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": {
"*": [ "node_modules/*" ]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": ["./src/**/*"]
}