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:
1
tools/node_tools/polygerrit_app_preprocessor/.gitignore
vendored
Normal file
1
tools/node_tools/polygerrit_app_preprocessor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/out/
|
||||
74
tools/node_tools/polygerrit_app_preprocessor/BUILD
Normal file
74
tools/node_tools/polygerrit_app_preprocessor/BUILD
Normal 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 $< $@",
|
||||
)
|
||||
9
tools/node_tools/polygerrit_app_preprocessor/README.md
Normal file
9
tools/node_tools/polygerrit_app_preprocessor/README.md
Normal 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.
|
||||
184
tools/node_tools/polygerrit_app_preprocessor/index.bzl
Normal file
184
tools/node_tools/polygerrit_app_preprocessor/index.bzl
Normal 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",
|
||||
},
|
||||
)
|
||||
123
tools/node_tools/polygerrit_app_preprocessor/links-updater.ts
Normal file
123
tools/node_tools/polygerrit_app_preprocessor/links-updater.ts
Normal 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();
|
||||
352
tools/node_tools/polygerrit_app_preprocessor/preprocessor.ts
Normal file
352
tools/node_tools/polygerrit_app_preprocessor/preprocessor.ts
Normal 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();
|
||||
@@ -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']
|
||||
};
|
||||
12
tools/node_tools/polygerrit_app_preprocessor/tsconfig.json
Normal file
12
tools/node_tools/polygerrit_app_preprocessor/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"outDir": "out"
|
||||
},
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
111
tools/node_tools/polygerrit_app_preprocessor/utils.ts
Normal file
111
tools/node_tools/polygerrit_app_preprocessor/utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user