Dmitrii Filippov 14fd203c4b The tool to convert polygerrit code to es6 modules
This is a temporary change. It is reverted here
https://gerrit-review.googlesource.com/c/gerrit/+/258560

The entrypoint is the es6-modules-converter.sh script.
The script creates(or updates) 2 changes on top of this change:
1. Change to rename all .html files (except tests) to .js files
2. Change to convert polygerrit source code to es6 modules.

Important: the tool works only on linux. Doesn't work on mac.

How to use:
1. Rebase the relation chain to the latest master (up to and including
   this change)
2. Checkout this change
3. Run the tool:
a) If you run it for the first time
   (or if you want to create completely new change)
./es6-modules-converter

b) If you want to update existing change:
./es6-modules-converter --rename-change-id .... --convert-change-id ...
Example:

./es6-modules-converter.sh \
  --rename-change-id Ic078c03c9ead018c80142ce1976d7192cb631964 \
  --convert-change-id I0c447dd8c05757741e2c940720652d01d9fb7d67

Note: You can see some errors from polymer-modulizer - it is not
an actual error.

3. If the tool completes successful, push changes. If you have any
error - fix them and repeat starting from the step 1 or 2.

4. Rebase https://gerrit-review.googlesource.com/c/gerrit/+/258560 on
top of the created/updated change.

Change-Id: Ib186e2d28cd5eba234f65f94b9afa8fa50502515
2020-03-17 10:09:20 +00:00

431 lines
16 KiB
TypeScript

// Copyright (C) 2020 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.
// This script is used to postprocess files after the polymer-modulizer.
// The script:
// - fixes some import paths
// - extract templates to a separate files
// - makes some other minor changes (see TsFileUpdater.SimpleTransformers)
import {
CommentRange,
ImportDeclaration,
Project,
QuoteKind,
SourceFile, Statement,
ts,
VariableDeclarationKind
} from "ts-morph";
import {fail} from "../utils/common";
import * as path from "path";
import * as fs from "fs";
import {readMultilineParamFile} from "../utils/command-line";
const root = path.normalize(process.argv[2]);
const modulizedFiles = readMultilineParamFile(process.argv[3]);
const modulizerOutRoot = path.join(root, "modulizer_out");
interface NodeTransformer {
transformNode(node: ts.Node, relativePathToSourceFile: string): ts.Node | undefined;
}
class ImportHrefDeclarationTransformer implements NodeTransformer{
// Transforms
// import { importHref as importHref$0 } from "...";
// to
// import { importHref } from "../relative_path/scripts/import_href.js";
private static readonly importHrefPath = path.join(root, "scripts/import-href.js");
private isSupportedImportHrefDeclaration(node: ts.ImportDeclaration): boolean {
// Returns true if import has one of the following forms:
// import { importHref as importHref$0 } from "....";
// import { importHref } from "....";
// (path is not important)
if(!node.importClause) {
return false;
}
if(!node.importClause.namedBindings|| !ts.isNamedImports(node.importClause.namedBindings)) {
return false;
}
const namedImports = node.importClause.namedBindings;
if (namedImports.elements.length !== 1) {
return false;
}
if(namedImports.elements.length !== 1) {
return false;
}
const firstNamedImport = namedImports.elements[0];
if(!firstNamedImport.propertyName) {
// import { importHref } from "....";
return firstNamedImport.name.text === "importHref";
}
// import { importHref as importHref$0 } from "....";
return (firstNamedImport.name.text === "importHref$0" && firstNamedImport.propertyName.text === "importHref");
}
public transformNode(node: ts.Node, relativePathToSourceFile: string): ts.ImportDeclaration | undefined {
if(!ts.isImportDeclaration(node)) {
return undefined;
}
if (!ts.isStringLiteral(node.moduleSpecifier)) {
fail(`Internal error`);
}
const moduleSpecifierText = node.moduleSpecifier.text;
if(moduleSpecifierText !== "@polymer/polymer/lib/utils/import-href.js") {
return undefined;
}
if(!this.isSupportedImportHrefDeclaration(node)) {
fail(`Unsupported import. Expected:
import { importHref as importHref$0 } from "...";
or
import { importHref } from "...";
Actual:
${node.getText()}`);
}
const relativePath = path.relative(path.dirname(path.join(root, relativePathToSourceFile)), ImportHrefDeclarationTransformer.importHrefPath);
const namedBindings = ts.createNamedImports([ts.createImportSpecifier(undefined, ts.createIdentifier("importHref"))]);
return ts.updateImportDeclaration(node, node.decorators, node.modifiers, ts.createImportClause(undefined, namedBindings), ts.createStringLiteral(relativePath));
}
}
class ImportHrefCallExpressionTransformer implements NodeTransformer {
// Transforms
// (this.importHref || importHref$0)(...arguments..)
// to
// importHref(...arguments...)
private isImportHrefCallExpression(node: ts.CallExpression): boolean {
// Return true if call has one of the following forms:
// (this.importHref || importHref$0)(.....)
// (this.importHref || Base.importHref$0)(.....)
// (arguments are not important)
if(!ts.isParenthesizedExpression(node.expression)) {
return false;
}
if(!ts.isBinaryExpression(node.expression.expression)) {
return false;
}
const binareExprNode = node.expression.expression;
if(binareExprNode.operatorToken.kind !== ts.SyntaxKind.BarBarToken) {
return false;
}
if(!ts.isPropertyAccessExpression(binareExprNode.left) ||
!ts.isIdentifier(binareExprNode.right)) {
return false;
}
if(binareExprNode.left.getText() !== "this.importHref") {
return false;
}
if(binareExprNode.right.getText() !== "importHref$0") {
return false;
}
return true;
}
public transformNode(node: ts.Node): ts.CallExpression | undefined {
if(!ts.isCallExpression(node) || !this.isImportHrefCallExpression(node)) {
return undefined;
}
return ts.updateCall(node, ts.createIdentifier("importHref"),
node.typeArguments, node.arguments);
}
}
class ImportPolymerLegacyDeclarationTransformer implements NodeTransformer {
// Transforms
// import '/node_modules/@polymer/polymer/polymer-legacy.js';
// to
// import '"../relative_path/scripts/bundled-polymer.js"'
private static readonly bundledPolymerPath = path.join(root, "scripts/bundled-polymer.js");
public transformNode(node: ts.Node, relativePathToSourceFile: string): ts.ImportDeclaration | undefined {
if(!ts.isImportDeclaration(node)) {
return undefined;
}
if(node.getText() !== "import '/node_modules/@polymer/polymer/polymer-legacy.js';") {
return undefined;
}
const relativePath = path.relative(path.dirname(path.join(root, relativePathToSourceFile)), ImportPolymerLegacyDeclarationTransformer.bundledPolymerPath);
return ts.updateImportDeclaration(node, node.decorators, node.modifiers, node.importClause, ts.createStringLiteral(relativePath));
}
}
class ExtractTemplateTransformer implements NodeTransformer {
// ExtractTemplateTransformer must be created for each file
private hasHtmlTemplate: boolean = false;
private importRelativePath: string = "";
private templateFileAbsPath: string = "";
private taggedTemplateText: string = "";
private isGetTemplateAccessorDeclaration(node: ts.Node): node is ts.GetAccessorDeclaration {
// Returns true if node is a get accessor of the form:
// static get template() { ... }
if(!ts.isGetAccessorDeclaration(node)) {
return false;
}
if(!ts.isIdentifier(node.name) || node.name.text !== 'template') {
return false;
}
const modifiers = node.modifiers;
if(!modifiers || modifiers.length !== 1) {
return false;
}
const firstModifier = modifiers[0];
return firstModifier.kind === ts.SyntaxKind.StaticKeyword;
}
private getTaggedTemplateExpression(node: ts.GetAccessorDeclaration): ts.TaggedTemplateExpression | undefined {
// Returns taggedTemplateExpression (i.e. html`...`) if node has exactly the following form
// static get template() {return html`...`;}
// Otherwise returns undefined
// Assumes that isGetTemplateAccessorDeclaration(node) returns true
if(!node.body || !ts.isBlock(node.body)) {
return undefined;
}
const statements = node.body.statements;
if(statements.length !== 1) {
return undefined;
}
const firstStatement = statements[0];
if(!ts.isReturnStatement(firstStatement)) {
return undefined;
}
const returnExpression = firstStatement.expression;
if(!returnExpression || !ts.isTaggedTemplateExpression(returnExpression)) {
return undefined;
}
if(!ts.isIdentifier(returnExpression.tag) || returnExpression.tag.text !== "html") {
return undefined;
}
if(returnExpression.typeArguments) {
return undefined;
}
if(!ts.isNoSubstitutionTemplateLiteral(returnExpression.template)) {
return undefined;
}
return returnExpression;
}
public transformNode(node: ts.Node, relativePathToSourceFile: string): ts.Node | undefined {
if(!this.isGetTemplateAccessorDeclaration(node)) {
return undefined;
}
const taggedTemplateExpression = this.getTaggedTemplateExpression(node);
if(!taggedTemplateExpression) {
fail(`Not supported. Expected template method in the form 'static get template() {return html\`...\`;}'`);
}
if(this.hasHtmlTemplate) {
fail(`More than one template in the file. Not Supported!`);
}
const returnStatement = ts.createReturn(ts.createIdentifier("htmlTemplate"));
this.importRelativePath = "./" + path.parse(relativePathToSourceFile).name + "_html.js";
this.hasHtmlTemplate = true;
this.templateFileAbsPath = path.join(modulizerOutRoot, path.dirname(relativePathToSourceFile), this.importRelativePath);
this.taggedTemplateText = taggedTemplateExpression.getText();
return ts.updateGetAccessor(node, undefined, node.modifiers, node.name, node.parameters, node.type, ts.createBlock([returnStatement]));
}
public updateTemplates(file: SourceFile, project: Project): Set<string> | undefined {
// Extracts template to separate file
// Returns set of relative paths
if(!this.hasHtmlTemplate) {
return;
}
const htmlTagModuleSpecifier = "@polymer/polymer/lib/utils/html-tag.js";
const htmlTagImportDecl = file.getImportDeclaration(htmlTagModuleSpecifier);
if(!htmlTagImportDecl) {
fail(`Internal error. It is expected that file has import from '@polymer/polymer/lib/utils/html-tag.js'`);
}
htmlTagImportDecl.remove();
file.addImportDeclaration({
namedImports: ["htmlTemplate"],
moduleSpecifier: this.importRelativePath,
});
const templateSourceFile = project.createSourceFile(this.templateFileAbsPath);
templateSourceFile.addImportDeclaration({
namedImports: ["html"],
moduleSpecifier: htmlTagModuleSpecifier,
});
templateSourceFile.addVariableStatement({
declarationKind: VariableDeclarationKind.Const,
isExported: true,
declarations: [
{
name: "htmlTemplate",
initializer: this.taggedTemplateText,
}
]
});
templateSourceFile.insertStatements(0, `/**
* @license
* Copyright (C) 2020 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.
*/
`);
templateSourceFile.saveSync();
return new Set([path.relative(modulizerOutRoot, this.templateFileAbsPath)]);
}
}
class TsFileUpdater {
private static readonly SimpleTransformers = [
new ImportHrefCallExpressionTransformer(),
new ImportHrefDeclarationTransformer(),
new ImportHrefCallExpressionTransformer(),
new ImportPolymerLegacyDeclarationTransformer(),
];
private replaceImportPath(file: SourceFile, oldPath: string, newPath: string): ImportDeclaration | undefined {
const importDecl = file.getImportDeclaration(oldPath);
if(!importDecl) {
return importDecl;
}
importDecl.setModuleSpecifier(newPath);
return importDecl;
}
private replaceImportAndSetGlobalValue(file: SourceFile, oldPath: string, newPath: string, globalVarName: string): ImportDeclaration | undefined {
const importDecl = this.replaceImportPath(file, oldPath, newPath);
if(!importDecl) {
return undefined;
}
importDecl.setDefaultImport(globalVarName);
file.insertStatements(importDecl.getChildIndex() + 1, `self.${globalVarName} = ${globalVarName};`);
}
private replaceAbsolutNodeModulesPathWithPackageName(file: SourceFile) {
const nodeModulesPrefix = "/node_modules/";
const importDeclarations = file.getImportDeclarations();
for(const imp of importDeclarations) {
const moduleSpecifier = imp.getModuleSpecifierValue();
if(moduleSpecifier.startsWith(nodeModulesPrefix)) {
imp.setModuleSpecifier(moduleSpecifier.substr(nodeModulesPrefix.length));
}
}
}
private getFileLicensesComments(file: SourceFile): CommentRange[] {
let licenseCommentsRanges = [];
for(const statement of file.getStatementsWithComments()) {
const commentRanges = statement.getLeadingCommentRanges();
for(const commentRange of commentRanges) {
const licenseComment = commentRange.getText();
if(licenseComment.indexOf('@license') >= 0) {
licenseCommentsRanges.push(commentRange);
}
}
}
return licenseCommentsRanges;
}
private fixJsLicenseComment(file: SourceFile) {
const licenseCommentRanges = this.getFileLicensesComments(file);
if(licenseCommentRanges.length === 0) {
fail('Error. The file must have at least @license comment');
}
const firstLicenseComment = licenseCommentRanges[0];
const commentLines = firstLicenseComment.getText().split('\n');
for(let i = 1; i < commentLines.length; i++) {
if(!commentLines[i].trim().startsWith('*')) {
commentLines[i] = ' *' + (commentLines[i].length > 0 ? ' ': '') + commentLines[i];
} else if(commentLines[i].startsWith('*')){
commentLines[i] = ' ' + commentLines[i];
}
}
let newCommentText = commentLines.join('\n');
if(firstLicenseComment.getPos() !== 0) {
newCommentText += '\n';
}
const ranges = licenseCommentRanges
.map(range => {
return {
pos: range.getPos(),
end: range.getEnd()
}
});
ranges.sort((a, b) => b.pos - a.pos);
for(const range of ranges) {
file.removeText(range.pos, range.end);
}
file.insertText(0, newCommentText);
}
public updateFile(relativePath: string): Set<string> {
const newFiles = new Set<string>([relativePath]);
const project = new Project({manipulationSettings: {quoteKind: QuoteKind.Single }});
const sourceFile = project.addSourceFileAtPath(path.join(modulizerOutRoot, relativePath));
const extractTemplateTransformer = new ExtractTemplateTransformer();
const updatedSourceFile = sourceFile.transform(traversal => {
const node = traversal.visitChildren();
for (const transformer of TsFileUpdater.SimpleTransformers) {
const transformedNode = transformer.transformNode(node, relativePath);
if (transformedNode) {
return transformedNode;
}
const transformedTemplateNode = extractTemplateTransformer.transformNode(node, relativePath);
if(transformedTemplateNode) {
return transformedTemplateNode;
}
}
return node;
});
const newTemplateFiles = extractTemplateTransformer.updateTemplates(updatedSourceFile, project);
if(newTemplateFiles) {
newTemplateFiles.forEach((f) => newFiles.add(f));
}
this.replaceImportPath(updatedSourceFile, 'es6-promise/dist/es6-promise.min.js', 'es6-promise/lib/es6-promise.js');
this.replaceImportPath(updatedSourceFile, 'whatwg-fetch/dist/fetch.umd.js', 'whatwg-fetch/fetch.js');
this.replaceImportPath(updatedSourceFile, '/node_modules/polymer-bridges/polymer-resin/standalone/polymer-resin.js', 'polymer-resin/standalone/polymer-resin.js');
this.replaceImportAndSetGlobalValue(updatedSourceFile, 'page/page.js', 'page/page.mjs', 'page');
this.replaceImportAndSetGlobalValue(updatedSourceFile, 'moment/moment.js', 'moment/src/moment.js', 'moment');
this.replaceAbsolutNodeModulesPathWithPackageName(updatedSourceFile);
this.fixJsLicenseComment(updatedSourceFile);
updatedSourceFile.saveSync();
return newFiles;
}
}
const tsFileUpdater = new TsFileUpdater();
for(const file of modulizedFiles) {
console.log(`Updating ${file}`);
const allFiles = tsFileUpdater.updateFile(file);
allFiles.forEach(f =>
fs.copyFileSync(path.join(modulizerOutRoot, f), path.join(root, f))
);
}