
This change uses node modules to build polygerrit-ui release artifact. Tests still use bower_components. Change-Id: I3457931b0ff8edcb41250d1aa3518b8ea18a964e
353 lines
12 KiB
TypeScript
353 lines
12 KiB
TypeScript
/**
|
|
* @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.
|
|
*/
|
|
|
|
import * as fs from "fs";
|
|
import * as parse5 from "parse5";
|
|
import * as dom5 from "dom5";
|
|
import * as path from "path";
|
|
import {Node} from 'dom5';
|
|
import {fail, unexpectedSwitchValue} from "../utils/common";
|
|
import {readMultilineParamFile} from "../utils/command-line";
|
|
import {
|
|
HtmlSrcFilePath,
|
|
JsSrcFilePath,
|
|
HtmlTargetFilePath,
|
|
JsTargetFilePath,
|
|
FileUtils,
|
|
FilePath
|
|
} from "../utils/file-utils";
|
|
import {
|
|
AbsoluteWebPath,
|
|
getRelativeImport,
|
|
NodeModuleImportPath,
|
|
SrcWebSite
|
|
} from "../utils/web-site-utils";
|
|
|
|
/**
|
|
* Update source code by moving all scripts out of HTML files.
|
|
* Input:
|
|
* input_output_html_param_file - list of file paths, each file path on a separate line
|
|
* The first 3 line contains the path to the first input HTML file and 2 output paths
|
|
* (for HTML and JS files)
|
|
* The second 3 line contains paths for the second HTML file, and so on.
|
|
*
|
|
* input_output_js_param_file - similar to input_output_html_param_file, but has only 2 lines
|
|
* per file (input JS file and output JS file)
|
|
*
|
|
* input_web_root_path - path (in filesystem) which should be treated as a web-site root path.
|
|
|
|
* For each HTML file it creates 2 output files - HTML and JS file.
|
|
* HTML file contains everything from HTML input file, except <script> tags.
|
|
* JS file contains (in the same order, as in original HTML):
|
|
* - inline javascript code from HTML file
|
|
* - each <script src = "path/to/file.js" > from HTML is converted to
|
|
* import 'path/to/output/file.js'
|
|
* statement. Such import statement run all side-effects in file.js (i.e. it run all #
|
|
* global code).
|
|
* - each <link rel="import" href = "path/to/file.html"> adds to .js file as
|
|
* import 'path/to/output/file.html.js
|
|
* i.e. instead of html, the .js script imports
|
|
* Because output JS keeps the order of imports, all global variables are
|
|
* initialized in a correct order (this is important for gerrit; it is impossible to use
|
|
* AMD modules here).
|
|
*/
|
|
|
|
enum RefType {
|
|
Html,
|
|
InlineJS,
|
|
JSFile
|
|
}
|
|
|
|
type LinkOrScript = HtmlFileRef | HtmlFileNodeModuleRef | JsFileReference | JsFileNodeModuleReference | InlineJS;
|
|
|
|
interface HtmlFileRef {
|
|
type: RefType.Html,
|
|
path: HtmlSrcFilePath;
|
|
isNodeModule: false;
|
|
}
|
|
|
|
interface HtmlFileNodeModuleRef {
|
|
type: RefType.Html,
|
|
path: NodeModuleImportPath;
|
|
isNodeModule: true;
|
|
}
|
|
|
|
|
|
function isHtmlFileRef(ref: LinkOrScript): ref is HtmlFileRef {
|
|
return ref.type === RefType.Html;
|
|
}
|
|
|
|
interface JsFileReference {
|
|
type: RefType.JSFile,
|
|
path: JsSrcFilePath;
|
|
isModule: boolean;
|
|
isNodeModule: false;
|
|
}
|
|
|
|
interface JsFileNodeModuleReference {
|
|
type: RefType.JSFile,
|
|
path: NodeModuleImportPath;
|
|
isModule: boolean;
|
|
isNodeModule: true;
|
|
}
|
|
|
|
interface InlineJS {
|
|
type: RefType.InlineJS,
|
|
isModule: boolean;
|
|
content: string;
|
|
}
|
|
|
|
interface HtmlOutputs {
|
|
html: HtmlTargetFilePath;
|
|
js: JsTargetFilePath;
|
|
}
|
|
|
|
interface JsOutputs {
|
|
js: JsTargetFilePath;
|
|
}
|
|
|
|
type HtmlSrcToOutputMap = Map<HtmlSrcFilePath, HtmlOutputs>;
|
|
type JsSrcToOutputMap = Map<JsSrcFilePath, JsOutputs>;
|
|
|
|
interface HtmlFileInfo {
|
|
src: HtmlSrcFilePath;
|
|
ast: parse5.AST.Document;
|
|
linksAndScripts: LinkOrScript[]
|
|
}
|
|
|
|
/** HtmlScriptAndLinksCollector walks through HTML file and collect
|
|
* all links and inline scripts.
|
|
*/
|
|
class HtmlScriptAndLinksCollector {
|
|
public constructor(private readonly webSite: SrcWebSite) {
|
|
}
|
|
public collect(src: HtmlSrcFilePath): HtmlFileInfo {
|
|
const ast = HtmlScriptAndLinksCollector.getAst(src);
|
|
const isHtmlImport = (node: Node) => node.tagName == "link" &&
|
|
dom5.getAttribute(node, "rel") == "import";
|
|
const isScriptTag = (node: Node) => node.tagName == "script";
|
|
|
|
const linksAndScripts: LinkOrScript[] = dom5
|
|
.nodeWalkAll(ast as Node, (node) => isHtmlImport(node) || isScriptTag(node))
|
|
.map((node) => {
|
|
if (isHtmlImport(node)) {
|
|
const href = dom5.getAttribute(node, "href");
|
|
if (!href) {
|
|
fail(`Tag <link rel="import...> in the file '${src}' doesn't have href attribute`);
|
|
}
|
|
if(this.webSite.isNodeModuleReference(href)) {
|
|
return {
|
|
type: RefType.Html,
|
|
path: this.webSite.getNodeModuleImport(href),
|
|
isNodeModule: true,
|
|
}
|
|
} else {
|
|
return {
|
|
type: RefType.Html,
|
|
path: this.webSite.resolveHtmlImport(src, href),
|
|
isNodeModule: false,
|
|
}
|
|
}
|
|
} else {
|
|
const isModule = dom5.getAttribute(node, "type") === "module";
|
|
if (dom5.hasAttribute(node, "src")) {
|
|
let srcPath = dom5.getAttribute(node, "src")!;
|
|
if(this.webSite.isNodeModuleReference(srcPath)) {
|
|
return {
|
|
type: RefType.JSFile,
|
|
isModule: isModule,
|
|
path: this.webSite.getNodeModuleImport(srcPath),
|
|
isNodeModule: true
|
|
};
|
|
} else {
|
|
return {
|
|
type: RefType.JSFile,
|
|
isModule: isModule,
|
|
path: this.webSite.resolveScriptSrc(src, srcPath),
|
|
isNodeModule: false
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
type: RefType.InlineJS,
|
|
isModule: isModule,
|
|
content: dom5.getTextContent(node)
|
|
};
|
|
}
|
|
});
|
|
return {
|
|
src,
|
|
ast,
|
|
linksAndScripts
|
|
};
|
|
};
|
|
|
|
private static getAst(file: string): parse5.AST.Document {
|
|
const html = fs.readFileSync(file, "utf-8");
|
|
return parse5.parse(html, {locationInfo: true});
|
|
}
|
|
|
|
}
|
|
|
|
/** Generate js files */
|
|
class ScriptGenerator {
|
|
public constructor(private readonly pathMapper: SrcToTargetPathMapper) {
|
|
}
|
|
public generateFromJs(src: JsSrcFilePath) {
|
|
FileUtils.copyFile(src, this.pathMapper.getJsTargetForJs(src));
|
|
}
|
|
|
|
public generateFromHtml(html: HtmlFileInfo) {
|
|
const content: string[] = [];
|
|
const src = html.src;
|
|
const targetJsFile: JsTargetFilePath = this.pathMapper.getJsTargetForHtml(src);
|
|
html.linksAndScripts.forEach((linkOrScript) => {
|
|
switch (linkOrScript.type) {
|
|
case RefType.Html:
|
|
if(linkOrScript.isNodeModule) {
|
|
const importPath = this.pathMapper.getJsTargetForHtmlInNodeModule(linkOrScript.path)
|
|
content.push(`import '${importPath}';`);
|
|
} else {
|
|
const importPath = this.pathMapper.getJsTargetForHtml(linkOrScript.path);
|
|
const htmlRelativePath = getRelativeImport(targetJsFile, importPath);
|
|
content.push(`import '${htmlRelativePath}';`);
|
|
}
|
|
break;
|
|
case RefType.JSFile:
|
|
if(linkOrScript.isNodeModule) {
|
|
content.push(`import '${linkOrScript.path}'`);
|
|
} else {
|
|
const importFromJs = this.pathMapper.getJsTargetForJs(linkOrScript.path);
|
|
const scriptRelativePath = getRelativeImport(targetJsFile, importFromJs);
|
|
content.push(`import '${scriptRelativePath}';`);
|
|
}
|
|
break;
|
|
case RefType.InlineJS:
|
|
content.push(linkOrScript.content);
|
|
break;
|
|
default:
|
|
unexpectedSwitchValue(linkOrScript);
|
|
}
|
|
});
|
|
FileUtils.writeContent(targetJsFile, content.join("\n"));
|
|
}
|
|
}
|
|
|
|
/** Generate html files*/
|
|
class HtmlGenerator {
|
|
constructor(private readonly pathMapper: SrcToTargetPathMapper) {
|
|
}
|
|
public generateFromHtml(html: HtmlFileInfo) {
|
|
const ast = html.ast;
|
|
dom5.nodeWalkAll(ast as Node, (node) => node.tagName === "script")
|
|
.forEach((scriptNode) => dom5.remove(scriptNode));
|
|
const newContent = parse5.serialize(ast);
|
|
if(newContent.indexOf("<script") >= 0) {
|
|
fail(`Has content ${html.src}`);
|
|
}
|
|
FileUtils.writeContent(this.pathMapper.getHtmlTargetForHtml(html.src), newContent);
|
|
}
|
|
}
|
|
|
|
function readHtmlSrcToTargetMap(paramFile: string): HtmlSrcToOutputMap {
|
|
const htmlSrcToTarget: HtmlSrcToOutputMap = new Map();
|
|
const input = readMultilineParamFile(paramFile);
|
|
for(let i = 0; i < input.length; i += 3) {
|
|
const srcHtmlFile = path.resolve(input[i]) as HtmlSrcFilePath;
|
|
const targetHtmlFile = path.resolve(input[i + 1]) as HtmlTargetFilePath;
|
|
const targetJsFile = path.resolve(input[i + 2]) as JsTargetFilePath;
|
|
htmlSrcToTarget.set(srcHtmlFile, {
|
|
html: targetHtmlFile,
|
|
js: targetJsFile
|
|
});
|
|
}
|
|
return htmlSrcToTarget;
|
|
}
|
|
|
|
function readJsSrcToTargetMap(paramFile: string): JsSrcToOutputMap {
|
|
const jsSrcToTarget: JsSrcToOutputMap = new Map();
|
|
const input = readMultilineParamFile(paramFile);
|
|
for(let i = 0; i < input.length; i += 2) {
|
|
const srcJsFile = path.resolve(input[i]) as JsSrcFilePath;
|
|
const targetJsFile = path.resolve(input[i + 1]) as JsTargetFilePath;
|
|
jsSrcToTarget.set(srcJsFile as JsSrcFilePath, {
|
|
js: targetJsFile as JsTargetFilePath
|
|
});
|
|
}
|
|
return jsSrcToTarget;
|
|
}
|
|
|
|
class SrcToTargetPathMapper {
|
|
public constructor(
|
|
private readonly htmlSrcToTarget: HtmlSrcToOutputMap,
|
|
private readonly jsSrcToTarget: JsSrcToOutputMap) {
|
|
}
|
|
public getJsTargetForHtmlInNodeModule(file: NodeModuleImportPath): JsTargetFilePath {
|
|
return `${file}_gen.js` as JsTargetFilePath;
|
|
}
|
|
|
|
public getJsTargetForHtml(html: HtmlSrcFilePath): JsTargetFilePath {
|
|
return this.getHtmlOutputs(html).js;
|
|
}
|
|
public getHtmlTargetForHtml(html: HtmlSrcFilePath): HtmlTargetFilePath {
|
|
return this.getHtmlOutputs(html).html;
|
|
}
|
|
public getJsTargetForJs(js: JsSrcFilePath): JsTargetFilePath {
|
|
return this.getJsOutputs(js).js;
|
|
}
|
|
|
|
private getHtmlOutputs(html: HtmlSrcFilePath): HtmlOutputs {
|
|
if(!this.htmlSrcToTarget.has(html)) {
|
|
fail(`There are no outputs for the file '${html}'`);
|
|
}
|
|
return this.htmlSrcToTarget.get(html)!;
|
|
}
|
|
private getJsOutputs(js: JsSrcFilePath): JsOutputs {
|
|
if(!this.jsSrcToTarget.has(js)) {
|
|
fail(`There are no outputs for the file '${js}'`);
|
|
}
|
|
return this.jsSrcToTarget.get(js)!;
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
if(process.argv.length < 5) {
|
|
const execFileName = path.basename(__filename);
|
|
fail(`Usage:\nnode ${execFileName} input_web_root_path input_output_html_param_file input_output_js_param_file\n`);
|
|
}
|
|
|
|
const srcWebSite = new SrcWebSite(path.resolve(process.argv[2]) as FilePath);
|
|
const htmlSrcToTarget: HtmlSrcToOutputMap = readHtmlSrcToTargetMap(process.argv[3]);
|
|
const jsSrcToTarget: JsSrcToOutputMap = readJsSrcToTargetMap(process.argv[4]);
|
|
const pathMapper = new SrcToTargetPathMapper(htmlSrcToTarget, jsSrcToTarget);
|
|
|
|
const scriptGenerator = new ScriptGenerator(pathMapper);
|
|
const htmlGenerator = new HtmlGenerator(pathMapper);
|
|
const scriptAndLinksCollector = new HtmlScriptAndLinksCollector(srcWebSite);
|
|
|
|
htmlSrcToTarget.forEach((targets, src) => {
|
|
const htmlFileInfo = scriptAndLinksCollector.collect(src);
|
|
scriptGenerator.generateFromHtml(htmlFileInfo);
|
|
htmlGenerator.generateFromHtml(htmlFileInfo);
|
|
});
|
|
jsSrcToTarget.forEach((targets, src) => {
|
|
scriptGenerator.generateFromJs(src);
|
|
});
|
|
}
|
|
|
|
main();
|