Use node modules for polygerrit-ui release build

This change uses node modules to build polygerrit-ui release artifact.
Tests still use bower_components.

Change-Id: I3457931b0ff8edcb41250d1aa3518b8ea18a964e
This commit is contained in:
Dmitrii Filippov
2020-01-20 19:38:06 +01:00
parent fe6728729d
commit fbdc89d307
35 changed files with 1453 additions and 578 deletions

View File

@@ -0,0 +1 @@
/out/

View File

@@ -0,0 +1,74 @@
load("@npm_bazel_typescript//:index.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "preprocessor",
srcs = glob(["*.ts"]),
node_modules = "@tools_npm//:node_modules",
tsconfig = "tsconfig.json",
deps = [
"//tools/node_tools/utils",
"@tools_npm//:node_modules",
],
)
#rollup_bundle - workaround for https://github.com/bazelbuild/rules_nodejs/issues/1522
rollup_bundle(
name = "preprocessor-bundle",
config_file = "rollup.config.js",
entry_point = "preprocessor.ts",
format = "cjs",
rollup_bin = "//tools/node_tools:rollup-bin",
deps = [
":preprocessor",
"@tools_npm//rollup-plugin-node-resolve",
],
)
rollup_bundle(
name = "links-updater-bundle",
config_file = "rollup.config.js",
entry_point = "links-updater.ts",
format = "cjs",
rollup_bin = "//tools/node_tools:rollup-bin",
deps = [
":preprocessor",
"@tools_npm//rollup-plugin-node-resolve",
],
)
nodejs_binary(
name = "preprocessor-bin",
data = ["@tools_npm//:node_modules"],
entry_point = "preprocessor-bundle.js",
)
nodejs_binary(
name = "links-updater-bin",
data = ["@tools_npm//:node_modules"],
entry_point = "links-updater-bundle.js",
)
# TODO(dmfilippov): Find a better way to fix it (another workaround or submit a bug to
# Bazel IJ plugin's) authors or to a ts_config rule author).
# The following genrule is a workaround for a bazel intellij plugin's bug.
# According to the documentation, the ts_config_rules section should be added
# to a .bazelproject file if a project uses typescript
# (https://ij.bazel.build/docs/dynamic-languages-typescript.html)
# Unfortunately, this doesn't work. It seems, that the plugin expects some output from
# the ts_config rule, but the rule doesn't produce any output.
# To workaround the issue, the tsconfig_editor genrule was added. The genrule only copies
# input file to the output file, but this is enough to make bazel IJ plugins works.
# So, if you have any problem a typescript editor (import errors, types not found, etc...) -
# try to build this rule from the command line
# (bazel build tools/node_tools/node_modules/licenses:tsconfig_editor) and then sync bazel project
# in intellij.
genrule(
name = "tsconfig_editor",
srcs = ["tsconfig.json"],
outs = ["tsconfig_editor.json"],
cmd = "cp $< $@",
)

View File

@@ -0,0 +1,9 @@
This directory contains bazel rules and CLI tools to preprocess HTML and JS files before bundling.
There are 2 different tools here:
* links-updater (and update_links rule) - updates link in HTML files.
Receives list of input and output files as well as a redirect.json file with information
about redirects.
* preprocessor (and prepare_for_bundling rule) - split each HTML files to a pair of one HTML
and one JS files. The output HTML doesn't contain `<script>` tags and JS file contains
all scripts and imports from HTML file. For more details see source code.

View File

@@ -0,0 +1,184 @@
"""This file contains rules to preprocess files before bundling"""
def _update_links_impl(ctx):
"""Wrapper for the links-update command-line tool"""
dir_name = ctx.label.name
output_files = []
input_js_files = []
output_js_files = []
js_files_args = ctx.actions.args()
js_files_args.set_param_file_format("multiline")
js_files_args.use_param_file("%s", use_always = True)
for f in ctx.files.srcs:
output_file = ctx.actions.declare_file(dir_name + "/" + f.path)
output_files.append(output_file)
if f.extension == "html":
input_js_files.append(f)
output_js_files.append(output_file)
js_files_args.add(f)
js_files_args.add(output_file)
else:
ctx.actions.expand_template(
output = output_file,
template = f,
substitutions = {},
)
ctx.actions.run(
executable = ctx.executable._updater,
outputs = output_js_files,
inputs = input_js_files + [ctx.file.redirects],
arguments = [js_files_args, ctx.file.redirects.path],
)
return [DefaultInfo(files = depset(output_files))]
update_links = rule(
implementation = _update_links_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
"redirects": attr.label(allow_single_file = True, mandatory = True),
"_updater": attr.label(
default = ":links-updater-bin",
executable = True,
cfg = "host",
),
},
)
def _get_node_modules_root(node_modules):
if node_modules == None or len(node_modules) == 0:
return None
node_module_root = node_modules[0].label.workspace_root
for target in node_modules:
if target.label.workspace_root != node_module_root:
fail("Only one node_modules workspace can be used")
return node_module_root + "/"
def _get_relative_path(file, root):
root_len = len(root)
if file.path.startswith(root):
return file.path[root_len - 1:]
else:
fail("The file '%s' is not under the root '%s'." % (file.path, root))
def _copy_file(ctx, src, target_name):
output_file = ctx.actions.declare_file(target_name)
ctx.actions.expand_template(
output = output_file,
template = src,
substitutions = {},
)
return output_file
def _get_generated_files(ctx, files, files_root_path, target_dir):
gen_files_for_html = dict()
gen_files_for_js = dict()
copied_files = []
for f in files:
target_name = target_dir + _get_relative_path(f, files_root_path)
if f.extension == "html":
html_output_file = ctx.actions.declare_file(target_name)
js_output_file = ctx.actions.declare_file(target_name + "_gen.js")
gen_files_for_html.update([[f, {"html": html_output_file, "js": js_output_file}]])
elif f.extension == "js":
js_output_file = ctx.actions.declare_file(target_name)
gen_files_for_js.update([[f, {"js": js_output_file}]])
else:
copied_files.append(_copy_file(ctx, f, target_name))
return (gen_files_for_html, gen_files_for_js, copied_files)
def _prepare_for_bundling_impl(ctx):
dir_name = ctx.label.name
all_output_files = []
node_modules_root = _get_node_modules_root(ctx.attr.node_modules)
html_files_dict = dict()
js_files_dict = dict()
root_path = ctx.bin_dir.path + "/" + ctx.attr.root_path
if not root_path.endswith("/"):
root_path = root_path + "/"
gen_files_for_html, gen_files_for_js, copied_files = _get_generated_files(ctx, ctx.files.srcs, root_path, dir_name)
html_files_dict.update(gen_files_for_html)
js_files_dict.update(gen_files_for_js)
all_output_files.extend(copied_files)
gen_files_for_html, gen_files_for_js, copied_files = _get_generated_files(ctx, ctx.files.additional_node_modules_to_preprocess, node_modules_root, dir_name)
html_files_dict.update(gen_files_for_html)
js_files_dict.update(gen_files_for_js)
all_output_files.extend(copied_files)
for f in ctx.files.node_modules:
target_name = dir_name + _get_relative_path(f, node_modules_root)
if html_files_dict.get(f) == None and js_files_dict.get(f) == None:
all_output_files.append(_copy_file(ctx, f, target_name))
preprocessed_output_files = []
html_files_args = ctx.actions.args()
html_files_args.set_param_file_format("multiline")
html_files_args.use_param_file("%s", use_always = True)
for src_path, output_files in html_files_dict.items():
html_files_args.add(src_path)
html_files_args.add(output_files["html"])
html_files_args.add(output_files["js"])
preprocessed_output_files.append(output_files["html"])
preprocessed_output_files.append(output_files["js"])
js_files_args = ctx.actions.args()
js_files_args.set_param_file_format("multiline")
js_files_args.use_param_file("%s", use_always = True)
for src_path, output_files in js_files_dict.items():
js_files_args.add(src_path)
js_files_args.add(output_files["js"])
preprocessed_output_files.append(output_files["js"])
all_output_files.extend(preprocessed_output_files)
ctx.actions.run(
executable = ctx.executable._preprocessor,
outputs = preprocessed_output_files,
inputs = ctx.files.srcs + ctx.files.additional_node_modules_to_preprocess,
arguments = [root_path, html_files_args, js_files_args],
)
entry_point_html = ctx.attr.entry_point
entry_point_js = ctx.attr.entry_point + "_gen.js"
ctx.actions.write(ctx.outputs.html, "<link rel=\"import\" href=\"./%s\" >" % entry_point_html)
ctx.actions.write(ctx.outputs.js, "import \"./%s\";" % entry_point_js)
return [
DefaultInfo(files = depset([ctx.outputs.html, ctx.outputs.js], transitive = [depset(all_output_files)])),
OutputGroupInfo(
js = depset([ctx.outputs.js] + [f for f in all_output_files if f.extension == "js"]),
html = depset([ctx.outputs.html] + [f for f in all_output_files if f.extension == "html"]),
),
]
prepare_for_bundling = rule(
implementation = _prepare_for_bundling_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
"node_modules": attr.label_list(allow_files = True),
"_preprocessor": attr.label(
default = ":preprocessor-bin",
executable = True,
cfg = "host",
),
"additional_node_modules_to_preprocess": attr.label_list(allow_files = True),
"root_path": attr.string(),
"entry_point": attr.string(
mandatory = True,
doc = "Path relative to root_path",
),
},
outputs = {
"html": "%{name}/entry.html",
"js": "%{name}/entry.js",
},
)

View File

@@ -0,0 +1,123 @@
/**
* @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 {HtmlFileUtils, RedirectsResolver} from "./utils";
import {Node} from 'dom5';
import {readMultilineParamFile} from "../utils/command-line";
import {FileUtils} from "../utils/file-utils";
import { fail } from "../utils/common";
import {JSONRedirects} from "./redirects";
/** Update links in HTML file
* input_output_param_files - is a list of paths; each path is placed on a separate line
* The first line is the path to a first input file (relative to process working directory)
* The second line is the path to the output file (relative to process working directory)
* The next 2 lines describe the second file and so on.
* redirectFile.json describes how to update links (see {@link JSONRedirects} for exact format)
* Additionaly, update some test links (related to web-component-tester)
*/
function main() {
console.log(process.cwd());
if (process.argv.length < 4) {
console.info("Usage:\n\tnode links_updater.js input_output_param_files redirectFile.json\n");
process.exit(1);
}
const jsonRedirects: JSONRedirects = JSON.parse(fs.readFileSync(process.argv[3], {encoding: "utf-8"}));
const redirectsResolver = new RedirectsResolver(jsonRedirects.redirects);
const input = readMultilineParamFile(process.argv[2]);
const updater = new HtmlFileUpdater(redirectsResolver);
for(let i = 0; i < input.length; i += 2) {
const srcFile = input[i];
const targetFile = input[i + 1];
updater.updateFile(srcFile, targetFile);
}
}
/** Update all links in HTML file based on redirects.
* Additionally, update references to web-component-tester */
class HtmlFileUpdater {
private static readonly Predicates = {
isScriptWithSrcTag: (node: Node) => node.tagName === "script" && dom5.hasAttribute(node, "src"),
isWebComponentTesterImport: (node: Node) => HtmlFileUpdater.Predicates.isScriptWithSrcTag(node) &&
dom5.getAttribute(node, "src")!.endsWith("/bower_components/web-component-tester/browser.js"),
isHtmlImport: (node: Node) => node.tagName === "link" && dom5.getAttribute(node, "rel") === "import" &&
dom5.hasAttribute(node, "href")
};
public constructor(private readonly redirectsResolver: RedirectsResolver) {
}
public updateFile(srcFile: string, targetFile: string) {
const html = fs.readFileSync(srcFile, "utf-8");
const ast = parse5.parseFragment(html, {locationInfo: true}) as Node;
const webComponentTesterImportNode = dom5.query(ast, HtmlFileUpdater.Predicates.isWebComponentTesterImport);
if(webComponentTesterImportNode) {
dom5.setAttribute(webComponentTesterImportNode, "src", "/components/wct-browser-legacy/browser.js");
}
// Update all HTML imports
const updateHtmlImportHref = (htmlImportNode: Node) => this.updateRefAttribute(htmlImportNode, srcFile, "href");
dom5.queryAll(ast, HtmlFileUpdater.Predicates.isHtmlImport).forEach(updateHtmlImportHref);
// Update all <script src=...> tags
const updateScriptSrc = (scriptTagNode: Node) => this.updateRefAttribute(scriptTagNode, srcFile, "src");
dom5.queryAll(ast, HtmlFileUpdater.Predicates.isScriptWithSrcTag).forEach(updateScriptSrc);
const newContent = parse5.serialize(ast);
FileUtils.writeContent(targetFile, newContent);
}
private getResolvedPath(parentHtml: string, href: string) {
const originalPath = '/' + HtmlFileUtils.getPathRelativeToRoot(parentHtml, href);
const resolvedInfo = this.redirectsResolver.resolve(originalPath, true);
if (!resolvedInfo.insideNodeModules && resolvedInfo.target === originalPath) {
return href;
}
if (resolvedInfo.insideNodeModules) {
return '/node_modules/' + resolvedInfo.target;
}
if (href.startsWith('/')) {
return resolvedInfo.target;
}
return HtmlFileUtils.getPathRelativeToRoot(parentHtml, resolvedInfo.target);
}
private updateRefAttribute(node: Node, parentHtml: string, attributeName: string) {
const ref = dom5.getAttribute(node, attributeName);
if(!ref) {
fail(`Internal error - ${node} in ${parentHtml} doesn't have attribute ${attributeName}`);
}
const newRef = this.getResolvedPath(parentHtml, ref);
if(newRef === ref) {
return;
}
dom5.setAttribute(node, attributeName, newRef);
}
}
main();

View File

@@ -0,0 +1,352 @@
/**
* @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();

View File

@@ -0,0 +1,21 @@
/**
* @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.
*/
export default {
external: ['fs', 'path', 'parse5', 'dom5']
};

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"moduleResolution": "node",
"outDir": "out"
},
"include": ["*.ts"]
}

View File

@@ -0,0 +1,111 @@
/**
* @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 path from "path";
import {FileUtils} from "../utils/file-utils";
import {
Redirect,
isRedirectToNodeModule,
isRedirectToDir,
RedirectToNodeModule,
PathRedirect
} from "./redirects";
export class HtmlFileUtils {
public static getPathRelativeToRoot(parentHtml: string, fileHref: string): string {
if (fileHref.startsWith('/')) {
return fileHref.substring(1);
}
return path.join(path.dirname(parentHtml), fileHref);
}
public static getImportPathRelativeToParent(rootDir: string, parentFile: string, importPath: string) {
if (importPath.startsWith('/')) {
importPath = importPath.substr(1);
}
const parentDir = path.dirname(
path.resolve(path.join(rootDir, parentFile)));
const fullImportPath = path.resolve(path.join(rootDir, importPath));
const relativePath = path.relative(parentDir, fullImportPath);
return relativePath.startsWith('../') ?
relativePath : "./" + relativePath;
}
}
interface RedirectForFile {
to: PathRedirect;
pathToFile: string;
}
interface ResolvedPath {
target: string;
insideNodeModules: boolean;
}
/** RedirectsResolver based on the list of redirects, calculates
* new import path
*/
export class RedirectsResolver {
public constructor(private readonly redirects: Redirect[]) {
}
/** resolve returns new path instead of pathRelativeToRoot; */
public resolve(pathRelativeToRoot: string, resolveNodeModules: boolean): ResolvedPath {
const redirect = this.findRedirect(pathRelativeToRoot);
if (!redirect) {
return {target: pathRelativeToRoot, insideNodeModules: false};
}
if (isRedirectToNodeModule(redirect.to)) {
return {
target: resolveNodeModules ? RedirectsResolver.resolveNodeModuleFile(redirect.to,
redirect.pathToFile) : pathRelativeToRoot,
insideNodeModules: resolveNodeModules
};
}
if (isRedirectToDir(redirect.to)) {
let newDir = redirect.to.dir;
if (!newDir.endsWith('/')) {
newDir = newDir + '/';
}
return {target: `${newDir}${redirect.pathToFile}`, insideNodeModules: false}
}
throw new Error(`Invalid redirect for path: ${pathRelativeToRoot}`);
}
private static resolveNodeModuleFile(npmRedirect: RedirectToNodeModule, pathToFile: string): string {
if(npmRedirect.files && npmRedirect.files[pathToFile]) {
pathToFile = npmRedirect.files[pathToFile];
}
return `${npmRedirect.npm_module}/${pathToFile}`;
}
private findRedirect(relativePathToRoot: string): RedirectForFile | undefined {
if(!relativePathToRoot.startsWith('/')) {
relativePathToRoot = '/' + relativePathToRoot;
}
for(const redirect of this.redirects) {
const normalizedFrom = redirect.from + (redirect.from.endsWith('/') ? '' : '/');
if(relativePathToRoot.startsWith(normalizedFrom)) {
return {
to: redirect.to,
pathToFile: relativePathToRoot.substring(normalizedFrom.length)
};
}
}
return undefined;
}
}