diff --git a/defs.bzl b/defs.bzl index 5d0416b..52a94b2 100644 --- a/defs.bzl +++ b/defs.bzl @@ -1,4 +1,5 @@ -load("//internal/js_library:rule.bzl", "JsLibraryInfo", "js_library") +load("//internal/common:context.bzl", "JsLibraryInfo", "JsModuleInfo") +load("//internal/js_library:rule.bzl", "js_library") load("//internal/ts_library:rule.bzl", "ts_library") load("//internal/js_module:rule.bzl", "js_module") load("//internal/js_binary:rule.bzl", "js_binary") diff --git a/examples/node-typescript-app/WORKSPACE b/examples/node-typescript-app/WORKSPACE index 9f40b45..c678bf1 100644 --- a/examples/node-typescript-app/WORKSPACE +++ b/examples/node-typescript-app/WORKSPACE @@ -21,12 +21,18 @@ git_repository( load("@build_bazel_rules_nodejs//:package.bzl", "rules_nodejs_dependencies") rules_nodejs_dependencies() -load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories") +load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories", "yarn_install") node_repositories( package_json = [], ) +yarn_install( + name = "npm", + package_json = "//:package.json", + yarn_lock = "//:yarn.lock", +) + load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( diff --git a/examples/node-typescript-app/libs/shared-package/BUILD.bazel b/examples/node-typescript-app/libs/shared-package/BUILD.bazel index 1614e79..89633ae 100644 --- a/examples/node-typescript-app/libs/shared-package/BUILD.bazel +++ b/examples/node-typescript-app/libs/shared-package/BUILD.bazel @@ -5,5 +5,7 @@ load("@bazel_javascript//:defs.bzl", "ts_library") ts_library( name = "shared-package", srcs = glob(["**/*.ts"]), + module_name = "shared-package", tsconfig = "//:tsconfig.json", + deps = ["//:packages"], ) diff --git a/examples/node-typescript-app/libs/shared-package/common.ts b/examples/node-typescript-app/libs/shared-package/common.ts new file mode 100644 index 0000000..e14b069 --- /dev/null +++ b/examples/node-typescript-app/libs/shared-package/common.ts @@ -0,0 +1,3 @@ +export function tryMe(aString: String) { + return `I am trying: ${aString}`; +} diff --git a/examples/node-typescript-app/libs/shared-package/greeter.ts b/examples/node-typescript-app/libs/shared-package/greeter.ts index ae9dfa6..8a2c619 100644 --- a/examples/node-typescript-app/libs/shared-package/greeter.ts +++ b/examples/node-typescript-app/libs/shared-package/greeter.ts @@ -1,3 +1,5 @@ +import chalk from "chalk"; + export function greet(name: string): string { - return `Hello, ${name}`; + return `Hello, ${chalk.red(name)}`; } diff --git a/examples/node-typescript-app/package.json b/examples/node-typescript-app/package.json index b72b46e..23c18f5 100644 --- a/examples/node-typescript-app/package.json +++ b/examples/node-typescript-app/package.json @@ -5,6 +5,7 @@ "@types/koa": "^2.0.46", "@types/node": "^10.7.1", "@types/node-sass": "^3.10.32", + "chalk": "^2.4.1", "koa": "^2.5.2", "node-sass": "^4.9.3", "source-map-support": "^0.5.9" diff --git a/examples/node-typescript-app/services/my-service/server/server.ts b/examples/node-typescript-app/services/my-service/server/server.ts index 5eff999..8022526 100644 --- a/examples/node-typescript-app/services/my-service/server/server.ts +++ b/examples/node-typescript-app/services/my-service/server/server.ts @@ -1,6 +1,6 @@ import * as Koa from "koa"; import * as sass from "node-sass"; -import { greet } from "../../../libs/shared-package/greeter"; +import { greet } from "shared-package/greeter"; const app = new Koa(); diff --git a/examples/node-typescript-app/tsconfig.json b/examples/node-typescript-app/tsconfig.json index c76b808..79d219d 100644 --- a/examples/node-typescript-app/tsconfig.json +++ b/examples/node-typescript-app/tsconfig.json @@ -13,6 +13,10 @@ "noImplicitThis": true, "alwaysStrict": true, "jsx": "react", - "allowSyntheticDefaultImports": false + "allowSyntheticDefaultImports": false, + "baseUrl": ".", + "paths": { + "*": ["*", "libs/*"] + } } } diff --git a/examples/node-typescript-app/yarn.lock b/examples/node-typescript-app/yarn.lock index dd23c95..ae92e32 100644 --- a/examples/node-typescript-app/yarn.lock +++ b/examples/node-typescript-app/yarn.lock @@ -135,6 +135,12 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + any-promise@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -247,6 +253,14 @@ chalk@^1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -263,6 +277,16 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + combined-stream@1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" @@ -370,7 +394,7 @@ escape-html@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" -escape-string-regexp@^1.0.2: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -520,6 +544,10 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -1276,6 +1304,12 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + dependencies: + has-flag "^3.0.0" + tar@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" diff --git a/internal/common/BUILD.bazel b/internal/common/BUILD.bazel new file mode 100644 index 0000000..f3035f0 --- /dev/null +++ b/internal/common/BUILD.bazel @@ -0,0 +1,11 @@ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "_common_js_files", + srcs = glob(["**/*.js"]) +) + +exports_files([ + "context.bzl", + "index.js", +]) diff --git a/internal/common/actions/BUILD.bazel b/internal/common/actions/BUILD.bazel new file mode 100644 index 0000000..28418bc --- /dev/null +++ b/internal/common/actions/BUILD.bazel @@ -0,0 +1,3 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files(["actions.bzl"]) diff --git a/internal/common/actions/actions.bzl b/internal/common/actions/actions.bzl new file mode 100644 index 0000000..9b1fc53 --- /dev/null +++ b/internal/common/actions/actions.bzl @@ -0,0 +1,2 @@ +load("//internal/common/actions/run_js:action.bzl", "run_js") +load("//internal/common/actions/create_source_dir:action.bzl", "create_source_dir") diff --git a/internal/common/actions/create_source_dir/BUILD.bazel b/internal/common/actions/create_source_dir/BUILD.bazel new file mode 100644 index 0000000..6acc720 --- /dev/null +++ b/internal/common/actions/create_source_dir/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "action.bzl", + "create_source_dir.js", +]) diff --git a/internal/common/actions/create_source_dir/action.bzl b/internal/common/actions/create_source_dir/action.bzl new file mode 100644 index 0000000..d7b89fc --- /dev/null +++ b/internal/common/actions/create_source_dir/action.bzl @@ -0,0 +1,50 @@ +def _map_modules(js_module_info): + return "%s/%s:%s" % (js_module_info.module_root.path, js_module_info.js.package_path, js_module_info.module_name) + +def create_source_dir(js, js_source, create_dir): + """Creates a directory with the sources described in the JsSource object + + Args: + js: JsContext object + js_source: JsSource object describing the sources to symlink in the directory + create_dir: File object to populate with the sources (eg. ctx.outputs.compilation_src_dir) + + Returns: + Array with JsSource provider in it + """ + + library = js_source.library + gen_scripts = [] + if js_source.gen_scripts: + gen_scripts += js_source.gen_scripts + + direct_inputs = [] + direct_inputs += library.node_modules_dirs + transitive_inputs = [library.all_sources] + # Depset with all of the sources in it + + script_args = js.script_args(js) + script_args.add("--into", create_dir) + script_args.add("--from", js.package_path) + + for gen_script in gen_scripts: + if type(gen_script) == type(""): + direct_inputs.append(gen_script) + script_args.add("g:./{}".format(gen_script.path)) + elif type(gen_script) == type([]) and len(gen_script) > 0: + argValue = "g" + for value in gen_script: + direct_inputs.append(value) + argValue += ":./{}".format(value.path) + script_args.add(argValue) + + script_args.add_all(library.all_sources, format_each = "s:%s") + script_args.add_all(library.node_modules_dirs, format_each = "mrs:%s/node_modules/") + script_args.add_all(library.all_dep_modules, map_each = _map_modules, format_each = "m:%s") + + inputs = depset( + direct = direct_inputs, + transitive = [library.all_sources, library.all_dep_module_targets], + ) + + js.run_js(js, inputs = inputs, outputs = [create_dir], script = js._create_source_dir_js, script_args = script_args) diff --git a/internal/common/actions/create_source_dir/create_source_dir.js b/internal/common/actions/create_source_dir/create_source_dir.js new file mode 100644 index 0000000..667f9ef --- /dev/null +++ b/internal/common/actions/create_source_dir/create_source_dir.js @@ -0,0 +1,170 @@ +const fs = require("fs-extra"); +const path = require("path"); +const { BazelAction, ensureArray } = require("../run_js/BazelAction"); + +/** + * Action for creating a directory with the passed in files for symlinking and + * copying. + * + * The files to be populated have flags set on them for the appropriate actions: + * [flags]:./file/path + * Possible flags: + * s: Symlink + * s:./some/dir/ + * c: Copy + * c:/some/file + * r: Recurse + * rs:./some/dir/ + * m: Module (put it in node_modules) + * mrs: Symlink all folders in directory into node_modules, decending into folders that start with '@' + * mrs:some/node_modules + * g: Generate (run the passed in script to generate source files) + * g:some/script.js + * + * eg. node create_source_dir.js s:file/to/symlink. + */ +BazelAction( + { + string: [ + // Root path to symlink/copy from + "from", + // The folder to populate + "into" + ] + }, + async args => { + const { current_target, workspace_name, package_path, from, into } = args; + const sources = ensureArray(args._); + const nodeModulesPath = path.join(into, "node_modules"); + const package = { + workspace: workspace_name, + path: package_path + }; + + makeDirectory(into); + const existingDirs = new Set([into]); + + const populateFiles = async source => { + const parsed = parseSource(source); + /** + * Node Module Population + */ + if (parsed.module) { + if (parsed.recurse) { + const fromNodeModulesDir = parsed.path; + const allModuleDirectories = fs.readdirSync(fromNodeModulesDir); + for (const moduleDirectory of allModuleDirectories) { + if (isOrgScopeDirectory(moduleDirectory)) { + const orgScopeFrom = path.join( + fromNodeModulesDir, + moduleDirectory + ); + const orgScopeInto = path.join(nodeModulesPath, moduleDirectory); + makeDirectory(orgScopeInto); + const allOrgModuleDirectories = fs.readdirSync(orgScopeFrom); + for (const orgModuleDirectory of allOrgModuleDirectories) { + makeSymlink( + path.join(orgScopeFrom, orgModuleDirectory), + path.join(orgScopeInto, orgModuleDirectory) + ); + } + } else { + makeSymlink( + path.join(fromNodeModulesDir, moduleDirectory), + path.join(nodeModulesPath, moduleDirectory) + ); + } + } + } else { + const fromModuleDir = parsed.path; + const moduleName = parsed.params[0] + ? parsed.params[0] + : path.basename(parsed.path); + makeSymlink(fromModuleDir, path.join(nodeModulesPath, moduleName)); + } + } else if (parsed.symlink) { + makeSymlink(parsed.path, path.join(into, parsed.path)); + } else if (parsed.generate) { + await makeGeneratedFiles(package, into, parsed.path, parsed.params); + } + }; + + for (const source of sources) { + await populateFiles(source); + } + } +); + +function makeDirectory(directory) { + fs.ensureDirSync(directory); +} + +function makeSymlink(from, to) { + fs.ensureSymlinkSync(from, to); +} + +async function writeFile(into, output) { + if (!path) { + throw new Error(`Attempted to write file with no path set`); + } + await fs.writeFile(path.join(into, output.path), output.body); +} + +async function makeGeneratedFiles(package, into, generatorScript, inputFiles) { + const resolvedScript = path.join(process.cwd(), generatorScript); + const generator = require(resolvedScript); + if (!generator) { + throw new Error(`No generator script found at ${generatorScript}`); + } + const inputFileContents = await Promise.all( + inputFiles.map(async path => ({ + path, + body: await fs.readFile(path) + })) + ); + const outputFiles = await generator({ + package, + into, + inputs: inputFileContents + }); + await Promise.all(outputFiles.map(output => writeFile(into, output))); +} + +function isOrgScopeDirectory(directory) { + return directory.startsWith("@"); +} + +function parseSource(source) { + const [flags, sourcePath, ...params] = source.split(":"); + const actions = { path: sourcePath, params }; + let flagIndex = 0; + while (flagIndex < flags.length) { + const currentFlag = flags[flagIndex]; + switch (currentFlag) { + case "s": + actions["symlink"] = true; + break; + case "c": + actions["copy"] = true; + break; + case "r": + actions["recurse"] = true; + break; + case "m": + actions["module"] = true; + break; + case "g": + actions["generate"] = true; + break; + default: + throw new Error( + `Encountered invalid flag "${currentFlag}" in "${source}"` + ); + } + flagIndex++; + } + if (actions.symlink && actions.copy) { + throw new Error(`Attempted to both symlink and copy ${source}`); + } + return actions; +} diff --git a/internal/common/actions/run_js/BUILD.bazel b/internal/common/actions/run_js/BUILD.bazel new file mode 100644 index 0000000..6f1734e --- /dev/null +++ b/internal/common/actions/run_js/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "action.bzl", + "BazelAction.js", +]) diff --git a/internal/common/actions/run_js/BazelAction.js b/internal/common/actions/run_js/BazelAction.js new file mode 100644 index 0000000..788b904 --- /dev/null +++ b/internal/common/actions/run_js/BazelAction.js @@ -0,0 +1,126 @@ +const fs = require("fs-extra"); +const getopts = require("getopts"); +const path = require("path"); +const readline = require("readline"); + +/** + * Wrap a callback that will be called with the script arguments + * + * The arguments passed to the callback have only the arguments intended for + * the script and have removed any escaping created by the run_js_action bazel + * rule. + * + * The opts.args will be passed to the options parser. + * + * @param { { args: Object } } opts + * @param {(ctx: { args: Object }) => void | Promise} cb + */ +async function BazelAction(opts, cb) { + try { + const args = await parseBazelArgs(opts.args); + await cb(args); + } catch (e) { + console.error(e); + process.exit(1); + } +} + +/** + * Reads in param file produced by bazel + * + * The param file format should be "multiline" + * + * @param {string} filePath + */ +function readParamFile(filePath) { + return new Promise((resolve, reject) => { + try { + const rl = readline.createInterface({ + input: fs.createReadStream(filePath), + terminal: false, + crlfDelay: Infinity + }); + + argfileArgs = []; + + rl.on("line", line => argfileArgs.push(line)); + rl.on("close", () => resolve(argfileArgs)); + } catch (e) { + reject(e); + } + }); +} + +/** + * Wrap the passed in argument in an array or return the argument if it is + * already an array + * + * @param {any} arrayOrFirstElement + */ +function ensureArray(arrayOrFirstElement) { + if (Array.isArray(arrayOrFirstElement)) { + return arrayOrFirstElement; + } else { + return [arrayOrFirstElement]; + } +} + +/** + * Merge the two objects, if both objects have the same key then the values are + * concatenated into an array. + * @param {Object} into + * @param {Object} from + */ +function mergeArgs(into, from) { + for (const argName in from) { + if (into[argName]) { + into[argName] = ensureArray(into[argName]).concat(from[argName]); + } else { + into[argName] = from[argName]; + } + } + return into; +} + +/** + * Parse shell arguments with automatic handling for --params {paramFile} + * + * Uses mri (basically same options as minimist) to parse args. If there is a + * parmeter --params={paramFile} in the arguments, then the param file will + * automatically be read and the arguments in it returned in the + * + * @param {Object} opts Options for getopts (https://www.npmjs.com/package/getopts) + * @param {string[]} args Array of arguments to parse (defaults to process.argv.slice(2)) + * @returns {{[argName: string]: ArgValue}} Object with arg names as keys + */ +async function parseBazelArgs(opts) { + const result = getopts(process.argv.slice(2), opts); + + // --params was chosen as the use_param_file param_file_arg to match precident in rules_go + // https://github.com/bazelbuild/rules_go/blob/2179a6e1b576fc2a309c6cf677ad40e5b7f999ba/go/private/context.bzl#L87 + if (result.params) { + const argFileArgs = await readParamFile(result.params); + const argfileResult = getopts(argFileArgs, opts); + mergeArgs(result, argfileResult); + } + + return result; +} + +function safeSymlink(fromPath, toPath) { + const oldWorkingDir = process.cwd(); + const destinationPathDir = path.dirname(toPath); + fs.ensureDirSync(destinationPathDir); + process.chdir(destinationPathDir); + fs.symlinkSync( + path.relative(destinationPathDir, fromPath), + path.basename(toPath) + ); + process.chdir(oldWorkingDir); +} + +module.exports = { + safeSymlink, + ensureArray, + BazelAction +}; diff --git a/internal/common/actions/run_js/README.md b/internal/common/actions/run_js/README.md new file mode 100644 index 0000000..ccbdb39 --- /dev/null +++ b/internal/common/actions/run_js/README.md @@ -0,0 +1,3 @@ +# run_js Action + +This should be invoked using the [js_context](../../context.bzl) \ No newline at end of file diff --git a/internal/common/actions/run_js/action.bzl b/internal/common/actions/run_js/action.bzl new file mode 100644 index 0000000..b18225e --- /dev/null +++ b/internal/common/actions/run_js/action.bzl @@ -0,0 +1,43 @@ +load("//internal/npm_packages:rule.bzl", "NpmPackagesInfo") + +def run_js( + js, + inputs, + outputs, + script, + script_args): + """Create action that uses nodejs to run an internal .js file + + This is intended to be used internally by bazel-javascript and should + only have access to the node_modules that bazel-javascript installs internally + + Args: + js: JsContext object. + inputs: additional depset of action inputs (e.g. source files) + outputs: the outputs that the action generates as a sequence of Files + script: .js File to run + script_args: arguments to pass to the js file + """ + + action_inputs = depset( + direct = [ + script, + js._actions_bazel_action_js, + js._internal_packages[NpmPackagesInfo].installed_dir, + ], + transitive = [ + inputs, + ], + ) + + env = { + "NODE_PATH": js._internal_packages[NpmPackagesInfo].installed_dir.path + "/node_modules", + } + + js.actions.run( + inputs = action_inputs, + outputs = outputs, + executable = js._internal_nodejs, + arguments = [script.path, script_args], + env = env, + ) diff --git a/internal/common/context.bzl b/internal/common/context.bzl new file mode 100644 index 0000000..ba59f59 --- /dev/null +++ b/internal/common/context.bzl @@ -0,0 +1,314 @@ +load("//internal/common/actions:actions.bzl", "create_source_dir", "run_js") +load("//internal/npm_packages:rule.bzl", "NpmPackagesInfo") + +############################################################################### +# Providers + +# The Providers used are inspired by the rules_go providers: +# https://github.com/bazelbuild/rules_go/blob/master/go/providers.rst#GoLibrary + +# Wrapper for rule ctx that should be created through js_context +JsContext = provider() + +JsLibraryInfo = provider( + fields = [ + "js", + # The label that produced the JsLibrary + "label", + # Path of the BUILD.bazel file relative to the workspace root. + "package_path", + # Source files provided as input. + "src_files", + # All source files provided as input + "all_src_files", + # Entire paths to add to npm modules (eg. a node_modules path) + "node_modules_dirs", + # Modules that are depended upon directly + "dep_modules", + # Transitive module dependencies + "all_dep_modules", + # The target directories for the dependent modules + "dep_module_targets", + # The target directories for the transitive and directly dependent modules + "all_dep_module_targets", + ], +) +"""Metadata about an individual js library. + +This provider keeps track of bazel information for Javascript targets, such +as: module dependencies, source files, paths to merge into node_modules. +""" + +JsSourceInfo = provider(fields = [ + "js", + # The source js files + "src_files", + # List of scripts that will be used to generate files in the source directory + # For an example of these scripts see internal/ts_library/tsconfig.gen.js + "gen_scripts", + # The library rule that generated this source + "library", +]) +"""Source that will be used either directly or for transpilation to javascript +""" + +JsModuleInfo = provider( + fields = [ + "js", + # The root of the workspace + "workspace_name", + # The root directory of the module + "module_root", + # Name that will be used for non-relative imports + "module_name", + # Modules that this module directly depends on + "dep_modules", + # All modules that this module and its dependencies require + "all_dep_modules", + # The target directories for the dependent modules + "dep_module_targets", + # The target directories for the transitive and directly dependent modules + "all_dep_module_targets", + ], +) +""" Wraps a JsLibrary with a package_name for nonrelative imports + +Including this as a dependency should add the "package_name" as a key for +nonrelative imports +""" + +############################################################################### +# Common Attributes + +# Attributes that should be included for any rule that wants to create a JsContext +JS_CONTEXT_ATTRIBUTES = { + "_common_js_files": attr.label( + allow_files = True, + default = Label("//internal/common:_common_js_files") + ), + "_actions_bazel_action_js": attr.label( + allow_files = True, + single_file = True, + default = Label("//internal/common/actions/run_js:BazelAction.js"), + ), + "_create_source_dir_js": attr.label( + allow_files = True, + single_file = True, + default = Label("//internal/common/actions/create_source_dir:create_source_dir.js"), + ), + "_internal_nodejs": attr.label( + allow_files = True, + single_file = True, + default = Label("@nodejs//:node"), + ), + "_internal_packages": attr.label( + default = Label("//internal:packages"), + ), + "_empty_npm_packages": attr.label( + allow_files = True, + single_file = True, + default = Label("//internal/npm_packages/empty:packages"), + ), +} + +# Attributes that should be included for any rule that wants to create a JsLibrary +JS_LIBRARY_ATTRIBUTES = dict(JS_CONTEXT_ATTRIBUTES, **{ + "srcs": attr.label_list( + allow_files = True, + mandatory = True, + ), + "deps": attr.label_list( + default = [], + ), + "module_name": attr.string(), +}) + +RULES_NODEJS_MODULE_ATTRIBUTES = { + # The official bazel rules use module_name and module_root for non-relative + # module mapping. If the module_root is supplied and the module_name is not + # present then the module_name is assumed to be the target name. See: + # https://github.com/bazelbuild/rules_nodejs/blob/master/internal/common/module_mappings.bzl + "module_name": attr.string(), + "module_root": attr.string(), + # The official bazel rules look for this tag + "tags": ["NODE_MODULE_MARKER"], +} + +############################################################################### +# Helpers + +def _js_library_info(js, attr = None): + """Create a JsLibrary provider with attr.srcs and the sources from attr.deps + + Args: + js: JsContext object + attr: rule attributes to extract srcs and deps from + """ + + if not attr: + attr = js.attr + + # The srcs should contain what has been explicitly added for a rule + src_files = js._ctx.files.srcs + + # The deps is list of labels that should have providers that we can get sources from + deps_attr = getattr(attr, "deps", []) + + transitive_sources = [] + node_modules_dirs = [] + dep_modules = [] + transitive_dep_modules = [] + dep_module_targets = [] + transitive_dep_module_targets = [] + + # Iterate through the deps to add them to their correct JsLibrary attributes + for dep in deps_attr: + if JsLibraryInfo in dep: + dep_js_library = dep[JsLibraryInfo] + + # The dependency is another JsLibrary + transitive_sources.append(dep_js_library.all_sources) + transitive_dep_modules.append(dep_js_library.all_dep_modules) + elif JsModuleInfo in dep: + dep_js_module = dep[JsModuleInfo] + transitive_dep_modules.append(dep_js_module.all_dep_modules) + transitive_dep_module_targets.append(dep_js_module.all_dep_module_targets) + elif hasattr(dep, "tags") and "NODE_MODULE_MARKER" in getattr(dep, "tags"): + # The dependency is a module defined by rules_nodejs + direct_modules += dep + + if JsModuleInfo in dep: + dep_js_module = dep[JsModuleInfo] + dep_module_targets.append(dep_js_module.module_root) + dep_modules.append(dep_js_module) + + if NpmPackagesInfo in dep: + # The dependency is a node_modules directory installed by npm_packages + node_modules_dirs.append(dep[NpmPackagesInfo].installed_dir) + + all_src_files = depset( + direct = src_files, + transitive = transitive_sources, + ) + + all_dep_modules = depset( + direct = dep_modules, + transitive = transitive_dep_modules, + ) + + all_dep_module_targets = depset( + direct = dep_module_targets, + transitive = transitive_dep_module_targets, + ) + + return JsLibraryInfo( + js = js, + package_path = js.package_path, + src_files = src_files, + all_src_files = all_src_files, + node_modules_dirs = node_modules_dirs, + dep_modules = dep_modules, + all_dep_modules = all_dep_modules, + dep_module_targets = dep_module_targets, + all_dep_module_targets = all_dep_module_targets, + ) + +def _library_to_source_info(js, library, gen_scripts = None): + """Create a JsSource provider for a given library + The library is a target, but this resolves the actual source files needed + to build the js library. + + The gen_scripts is a list of js files that export a single function whose output + generates a list of files: + modules.exports = async ({ package, into, inputs}) + """ + + return JsSourceInfo( + js = js, + src_files = library.all_src_files, + gen_scripts = gen_scripts, + library = library, + ) + +def _library_to_module_info(js, library, module_root, module_name): + return JsModuleInfo( + js = js, + workspace_name = js.workspace_name, + all_dep_modules = library.all_dep_modules, + all_dep_module_targets = library.all_dep_module_targets, + module_root = module_root, + module_name = module_name, + ) + +def _script_args(js): + """Create Args object that can be used with js.run_js() + + Args: + js: JsContext object + """ + args = js.actions.args() + + # If the args get too big then spill over into the param file + args.use_param_file("--param=%s") + args.set_param_file_format("multiline") + + args.add("--current_target", js.label) + args.add("--workspace_name", js.workspace_name) + args.add("--package_path", js.package_path) + + return args + +def _module_mappings(js): + """Get the hash of {module_name - module_root} + + The underlying function from rules_nodejs goes through all the + dependencies looking for + """ + print("The nodejs rules don't export the module mappings") + # get_module_mappings(js.label, js.attr) + +# Following pattern similar to rules_go +# https://github.com/bazelbuild/rules_go/blob/2179a6e1b576fc2a309c6cf677ad40e5b7f999ba/go/private/context.bzl#L207 +def js_context(ctx, attr = None): + if not attr: + attr = ctx.attr + + # Node js to be used to run javascript backed bazel actions + _internal_nodejs = getattr(ctx.file, "_internal_nodejs") + + # Packages that will be made available to javascript backed bazel actions + _internal_packages = getattr(attr, "_internal_packages") + + # Packages that will be used if none are provided + _empty_npm_packages = getattr(attr, "_empty_npm_packages") + + _actions_bazel_action_js = getattr(ctx.file, "_actions_bazel_action_js") + _create_source_dir_js = getattr(ctx.file, "_create_source_dir_js") + + return JsContext( + # Base Context + label = ctx.label, + attr = ctx.attr, + + # Fields + workspace_name = ctx.workspace_name, + package_path = ctx.label.package, + module_name = getattr(attr, "module_name", None), + _ctx = ctx, + _internal_nodejs = _internal_nodejs, + _internal_packages = _internal_packages, + _empty_npm_packages = _empty_npm_packages, + _actions_bazel_action_js = _actions_bazel_action_js, + _create_source_dir_js = _create_source_dir_js, + + # Actions + actions = ctx.actions, + create_source_dir = create_source_dir, + run_js = run_js, + + # Helpers + script_args = _script_args, + library_info = _js_library_info, + library_to_source_info = _library_to_source_info, + library_to_module_info = _library_to_module_info, + ) diff --git a/internal/common/index.js b/internal/common/index.js new file mode 100644 index 0000000..feb2076 --- /dev/null +++ b/internal/common/index.js @@ -0,0 +1,5 @@ +const BazelAction = require("./actions/run_js/BazelAction"); + +module.exports = { + ...BazelAction +}; diff --git a/internal/js_binary/compile.js b/internal/js_binary/compile.js index d606c08..8ea7771 100644 --- a/internal/js_binary/compile.js +++ b/internal/js_binary/compile.js @@ -1,58 +1,60 @@ -const child_process = require("child_process"); -const fs = require("fs-extra"); +const { BazelAction } = require("../common/actions/run_js/BazelAction"); const path = require("path"); const webpack = require("webpack"); -const [ - nodePath, - scriptPath, - libBuildfilePath, - entry, - mode, - installedNpmPackagesDir, - compiledDir, - outputFile -] = process.argv; - -webpack( - { - entry: path.resolve( - path.join(compiledDir, path.dirname(libBuildfilePath), entry) - ), - output: { - filename: path.basename(outputFile), - path: path.resolve(path.dirname(outputFile)) - }, +BazelAction( + {}, + async ({ + srcDir, + outDir, + libBuildfilePath, + entry, mode, - target: "node", - resolve: { - modules: [ - path.resolve(path.join(installedNpmPackagesDir, "node_modules")) - ] - }, - plugins: [ - new webpack.BannerPlugin({ - banner: "#!/usr/bin/env node", - raw: true - }) - ] - }, - (err, stats) => { - // See https://webpack.js.org/api/node/#error-handling. - if (err) { - console.error(err.stack || err); - if (err.details) { - console.error(err.details); + installedNpmPackagesDir, + compiledDir, + outputFile + }) => { + webpack( + { + entry: path.resolve( + path.join(compiledDir, path.dirname(libBuildfilePath), entry) + ), + output: { + filename: path.basename(outputFile), + path: path.resolve(path.dirname(outputFile)) + }, + mode, + target: "node", + resolve: { + modules: [ + path.resolve(path.join(installedNpmPackagesDir, "node_modules")) + ] + }, + plugins: [ + new webpack.BannerPlugin({ + banner: "#!/usr/bin/env node", + raw: true + }) + ] + }, + (err, stats) => { + // See https://webpack.js.org/api/node/#error-handling. + if (err) { + console.error(err.stack || err); + if (err.details) { + console.error(err.details); + } + process.exit(1); + } + const info = stats.toJson(); + if (stats.hasErrors()) { + console.error(info.errors); + process.exit(1); + } + if (stats.hasWarnings()) { + console.warn(info.warnings); + } } - process.exit(1); - } - const info = stats.toJson(); - if (stats.hasErrors()) { - console.error(info.errors); - process.exit(1); - } - if (stats.hasWarnings()) { - console.warn(info.warnings); - } + ); } ); diff --git a/internal/js_binary/rule.bzl b/internal/js_binary/rule.bzl index d39383c..820e80a 100644 --- a/internal/js_binary/rule.bzl +++ b/internal/js_binary/rule.bzl @@ -1,7 +1,12 @@ -load("//internal/js_library:rule.bzl", "JsLibraryInfo") load("//internal/npm_packages:rule.bzl", "NpmPackagesInfo") +load("//internal/common:context.bzl", "JsLibraryInfo", "JS_LIBRARY_ATTRIBUTES", "js_context") def _js_binary_impl(ctx): + js = js_context(ctx) + providers = [] + + compile_args = js.script_args(js) + ctx.actions.run( inputs = [ ctx.file._js_binary_compile_script, diff --git a/internal/js_library/compile.js b/internal/js_library/compile.js index 37a7e31..caef5a4 100644 --- a/internal/js_library/compile.js +++ b/internal/js_library/compile.js @@ -1,58 +1,56 @@ +const { + BazelAction, + safeSymlink +} = require("../common/actions/run_js/BazelAction"); +const child_process = require("child_process"); const fs = require("fs-extra"); const path = require("path"); const babel = require("babel-core"); -const { safeSymlink } = require("../common/symlink"); -const [ - nodePath, - scriptPath, - fullSrcDir, - destinationDir, - joinedSrcs -] = process.argv; +BazelAction({}, async ({ srcDir, outDir }) => { + const srcs = new Set(joinedSrcs.split("|")); -const srcs = new Set(joinedSrcs.split("|")); - -// Compile with Babel. -transformDir("."); - -function transformDir(dirRelativePath) { - for (const fileName of fs.readdirSync( - path.join(fullSrcDir, dirRelativePath) - )) { - const relativeFilePath = path.join(dirRelativePath, fileName); - const srcFilePath = path.join(fullSrcDir, relativeFilePath); - let destFilePath = path.join(destinationDir, relativeFilePath); - fs.ensureDirSync(path.dirname(destFilePath)); - if (fs.lstatSync(srcFilePath).isDirectory()) { - transformDir(relativeFilePath); - } else if ( - srcs.has(relativeFilePath) && - (fileName.endsWith(".es6") || + function transformDir(dirRelativePath) { + currentDir = path.join(srcDir, dirRelativePath); + for (const fileName of fs.readdirSync(currentDir)) { + const relativeFilePath = path.join(dirRelativePath, fileName); + const srcFilePath = path.join(srcDir, relativeFilePath); + let destFilePath = path.join(outDir, relativeFilePath); + fs.ensureDirSync(path.dirname(destFilePath)); + if (fs.lstatSync(srcFilePath).isDirectory()) { + transformDir(relativeFilePath); + } else if ( + fileName.endsWith(".es6") || fileName.endsWith(".js") || - fileName.endsWith(".jsx")) - ) { - const transformed = babel.transformFileSync(srcFilePath, { - plugins: [ - "transform-decorators-legacy", - "transform-es2015-modules-commonjs" - ], - presets: ["env", "stage-2", "react"], - ignore: "node_modules" - }); - if (!transformed.code) { - throw new Error(`Could not compile ${srcFilePath}.`); - } - if (!destFilePath.endsWith(".js")) { - destFilePath = - destFilePath.substr(0, destFilePath.lastIndexOf(".")) + ".js"; + fileName.endsWith(".jsx") + ) { + const transformed = babel.transformFileSync(srcFilePath, { + plugins: [ + "transform-decorators-legacy", + "transform-es2015-modules-commonjs" + ], + presets: ["env", "stage-2", "react"], + ignore: "node_modules" + }); + if (!transformed.code) { + throw new Error(`Could not compile ${srcFilePath}.`); + } + if (!destFilePath.endsWith(".js")) { + destFilePath = + destFilePath.substr(0, destFilePath.lastIndexOf(".")) + ".js"; + } + fs.writeFileSync(destFilePath, transformed.code, "utf8"); + } else { + // Symlink any file that: + // - isn't a source file of this package; or + // - is not a JavaScript file (e.g. CSS assets). + safeSymlink(srcFilePath, destFilePath); } - fs.writeFileSync(destFilePath, transformed.code, "utf8"); - } else { - // Symlink any file that: - // - isn't a source file of this package; or - // - is not a JavaScript file (e.g. CSS assets). - safeSymlink(srcFilePath, destFilePath); } } -} + + // Compile with Babel. + transformDir("."); +}); + +const [nodePath, scriptPath, fullSrcDir, outDir, joinedSrcs] = process.argv; diff --git a/internal/js_library/create_full_src.js b/internal/js_library/create_full_src.js deleted file mode 100644 index ecd6e79..0000000 --- a/internal/js_library/create_full_src.js +++ /dev/null @@ -1,47 +0,0 @@ -const fs = require("fs-extra"); -const path = require("path"); -const { safeSymlink } = require("../common/symlink"); - -const [ - nodePath, - scriptPath, - targetLabel, - joinedInternalDeps, - joinedSrcs, - destinationDir -] = process.argv; - -const internalDeps = joinedInternalDeps.split("|"); -const srcs = joinedSrcs.split("|"); - -fs.mkdirSync(destinationDir); - -// Copy every internal dependency into the appropriate location. -for (const internalDep of internalDeps) { - if (!internalDep) { - continue; - } - const [joinedSrcs, compiledDir] = internalDep.split(":"); - const srcs = joinedSrcs.split(";"); - for (const src of srcs) { - if (!src) { - continue; - } - safeSymlink(path.join(compiledDir, src), path.join(destinationDir, src)); - } -} - -// Copy source code. -for (const src of srcs) { - if (!src) { - continue; - } - if (!fs.existsSync(src)) { - console.error(` -Missing file ${src} required by ${targetLabel}. -`); - process.exit(1); - } - const destinationFilePath = path.join(destinationDir, src); - safeSymlink(src, destinationFilePath); -} diff --git a/internal/js_library/rule.bzl b/internal/js_library/rule.bzl index e1af732..a64c1c4 100644 --- a/internal/js_library/rule.bzl +++ b/internal/js_library/rule.bzl @@ -1,189 +1,38 @@ -load("//internal/npm_packages:rule.bzl", "NpmPackagesInfo") - -JsLibraryInfo = provider(fields = [ - # Path of the BUILD.bazel file relative to the workspace root. - "build_file_path", - # Directory containing the JavaScript files (and potentially other assets). - "compiled_javascript_dir", - # Source files provided as input. - "javascript_source_files", - # Other js_library targets depended upon. - "internal_deps", - # Depset of npm_packages depended upon (at most one element). - "npm_packages", - # Directory in which node_modules/ with external NPM packages can be found. - "npm_packages_installed_dir", -]) +load("//internal/common:context.bzl", "JS_LIBRARY_ATTRIBUTES", "js_context") def _js_library_impl(ctx): - # Ensure that we depend on at most one npm_packages, since we don't want to - # have conflicting package versions coming from separate node_modules - # directories. - direct_npm_packages = [ - dep - for dep in ctx.attr.deps - if NpmPackagesInfo in dep - ] - if len(direct_npm_packages) > 1: - fail("Found more than one set of NPM packages in target definition: " + ",".join([ - dep.label - for dep in direct_npm_packages - ])) - extended_npm_packages = depset( - direct = direct_npm_packages, - transitive = [ - dep[JsLibraryInfo].npm_packages - for dep in ctx.attr.deps - if JsLibraryInfo in dep - ], - ) - npm_packages_list = extended_npm_packages.to_list() - if len(npm_packages_list) > 1: - fail("Found more than one set of NPM packages through dependencies: " + ",".join([ - dep.label - for dep in npm_packages_list - ])) + js = js_context(ctx) + providers = [] - # If we depend on an npm_packages target, we'll use its node_modules - # directory to find modules. Otherwise, we'll use an empty node_modules - # directory. - npm_packages = ( - npm_packages_list[0] if len(npm_packages_list) == 1 else ctx.attr._empty_npm_packages - ) + js_library = js.library_info(js) + providers.append(js_library) - # Gather all internal deps (other js_library rules). - internal_deps = depset( - direct = [ - dep - for dep in ctx.attr.deps - if JsLibraryInfo in dep - ], - transitive = [ - dep[JsLibraryInfo].internal_deps - for dep in ctx.attr.deps - if JsLibraryInfo in dep - ], - ) + js_source = js.library_to_source_info(js, js_library) + + js.create_source_dir(js, js_source, ctx.outputs.compilation_src_dir) - # Create a directory that contains: - # - source files (including all internal dependencies) - # - node_modules (symlinked to installed external dependencies directory) - _js_library_create_full_src( - ctx, - internal_deps, - npm_packages, - ) - _js_library_compile( - ctx, - internal_deps, - npm_packages, - ) - return [ - JsLibraryInfo( - build_file_path = ctx.build_file_path, - javascript_source_files = [_compiled_extension(f.path) for f in ctx.files.srcs], - compiled_javascript_dir = ctx.outputs.compiled_dir, - internal_deps = internal_deps, - npm_packages = extended_npm_packages, - npm_packages_installed_dir = npm_packages[NpmPackagesInfo].installed_dir, - ), - ] + compile_args = js.script_args(js) + compile_args.add("--srcDir", ctx.outputs.compilation_src_dir) + compile_args.add("--outDir", ctx.outputs.compiled_dir) + compile_args.add_all(js_library.all_sources) -def _compiled_extension(path): - if path.endswith(".es6") or path.endswith(".jsx"): - return path[:-4] + ".js" - else: - return path - -def _js_library_create_full_src(ctx, internal_deps, npm_packages): - ctx.actions.run( - inputs = [ - ctx.attr._internal_packages[NpmPackagesInfo].installed_dir, - ctx.file._js_library_create_full_src_script, - npm_packages[NpmPackagesInfo].installed_dir, - ] + [ - d[JsLibraryInfo].compiled_javascript_dir - for d in internal_deps - ] + ctx.files.srcs, - outputs = [ctx.outputs.compiled_javascript_dir], - executable = ctx.file._internal_nodejs, - env = { - "NODE_PATH": ctx.attr._internal_packages[NpmPackagesInfo].installed_dir.path + "/node_modules", - }, - arguments = [ - # Run `node process.js`. - ctx.file._js_library_create_full_src_script.path, - # Label of the build target (for helpful errors). - "//" + npm_packages.label.package + ":" + npm_packages.label.name, - # Source directories of the js_library targets we depend on. - ("|".join([ - (";".join(d[JsLibraryInfo].javascript_source_files)) + ":" + - d[JsLibraryInfo].compiled_javascript_dir.path - for d in internal_deps - ])), - # List of source files, which will be processed ("import" statements - # automatically replaced) and copied into the new directory. - ("|".join([ - f.path - for f in ctx.files.srcs - ])), - # Directory in which to place the result. - ctx.outputs.compiled_javascript_dir.path, - ], + js_compile_inputs = depset( + direct = [ctx.outputs.compilation_src_dir], + transitive = [js_library.all_sources], ) -def _js_library_compile(ctx, internal_deps, npm_packages): - ctx.actions.run( - inputs = [ - ctx.file._js_library_compile_script, - ctx.outputs.compiled_javascript_dir, - ctx.attr._internal_packages[NpmPackagesInfo].installed_dir, - npm_packages[NpmPackagesInfo].installed_dir, - ] + [ - d[JsLibraryInfo].compiled_javascript_dir - for d in internal_deps - ], + js.run_js( + js, + inputs = js_compile_inputs, outputs = [ctx.outputs.compiled_dir], - executable = ctx.file._internal_nodejs, - env = { - "NODE_PATH": ctx.attr._internal_packages[NpmPackagesInfo].installed_dir.path + "/node_modules", - }, - arguments = [ - # Run `node js_library/compile.js`. - ctx.file._js_library_compile_script.path, - # Directory in which the source code can be found. - ctx.outputs.compiled_javascript_dir.path, - # Directory in which to output the compiled JavaScript. - ctx.outputs.compiled_dir.path, - # List of source files, excluding source files from dependencies. - ("|".join([ - f.path - for f in ctx.files.srcs - ])), - ], + script = ctx.file._js_library_compile_script, + script_args = compile_args, ) + return providers + js_library = rule( - attrs = { - "srcs": attr.label_list( - allow_files = True, - mandatory = True, - ), - "deps": attr.label_list( - providers = [ - [JsLibraryInfo], - [NpmPackagesInfo], - ], - default = [], - ), - "_internal_packages": attr.label( - default = Label("//internal:packages"), - ), - "_internal_nodejs": attr.label( - allow_files = True, - single_file = True, - default = Label("@nodejs//:node"), - ), + attrs = dict(JS_LIBRARY_ATTRIBUTES, **{ "_js_library_create_full_src_script": attr.label( allow_files = True, single_file = True, @@ -194,13 +43,10 @@ js_library = rule( single_file = True, default = Label("//internal/js_library:compile.js"), ), - "_empty_npm_packages": attr.label( - default = Label("//internal/npm_packages/empty:packages"), - ), - }, + }), outputs = { + "compilation_src_dir": "%{name}_compilation_src", "compiled_dir": "%{name}_compiled", - "compiled_javascript_dir": "%{name}_full_src", }, implementation = _js_library_impl, ) diff --git a/internal/js_module/rule.bzl b/internal/js_module/rule.bzl index 66d5a4b..35deb74 100644 --- a/internal/js_module/rule.bzl +++ b/internal/js_module/rule.bzl @@ -1,11 +1,7 @@ -load("//internal/js_library:rule.bzl", "JsLibraryInfo") - -JsModuleInfo = provider(fields = [ - "name", - "single_file", -]) +load("//internal/common:context.bzl", "JsLibraryInfo", "JsModuleInfo", "JS_LIBRARY_ATTRIBUTES", "js_context") def _js_module_impl(ctx): + return [ ctx.attr.lib[JsLibraryInfo], JsModuleInfo( diff --git a/internal/js_script_and_test/rule.bzl b/internal/js_script_and_test/rule.bzl index 16b2347..26be53c 100644 --- a/internal/js_script_and_test/rule.bzl +++ b/internal/js_script_and_test/rule.bzl @@ -1,5 +1,5 @@ -load("//internal/js_library:rule.bzl", "JsLibraryInfo") load("//internal/npm_packages:rule.bzl", "NpmPackagesInfo") +load("//internal/common:context.bzl", "JsLibraryInfo", "JsModuleInfo", "JS_LIBRARY_ATTRIBUTES", "js_context") def _js_script_impl(ctx): # Create a directory that contains: diff --git a/internal/package.json b/internal/package.json index 1e14be8..ed7ef23 100644 --- a/internal/package.json +++ b/internal/package.json @@ -10,6 +10,7 @@ "css-loader": "^1.0.0", "file-loader": "^1.1.11", "fs-extra": "^6.0.1", + "getopts": "^2.2.1", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.4.1", "optimize-css-assets-webpack-plugin": "^4.0.3", diff --git a/internal/ts_library/BUILD.bazel b/internal/ts_library/BUILD.bazel index 86d5d70..8d87b78 100644 --- a/internal/ts_library/BUILD.bazel +++ b/internal/ts_library/BUILD.bazel @@ -6,4 +6,5 @@ exports_files([ "default_tsconfig.json", "rule.bzl", "transpile.js", + "tsconfig.gen.js", ]) diff --git a/internal/ts_library/compile.js b/internal/ts_library/compile.js index db78c00..a7fc23f 100644 --- a/internal/ts_library/compile.js +++ b/internal/ts_library/compile.js @@ -1,40 +1,41 @@ +const { + BazelAction, + safeSymlink +} = require("../common/actions/run_js/BazelAction"); const child_process = require("child_process"); const fs = require("fs-extra"); const path = require("path"); -const { safeSymlink } = require("../common/symlink"); -const [nodePath, scriptPath, fullSrcDir, destinationDir] = process.argv; - -// Copy over any non-TypeScript files (e.g. CSS assets). -copyNonTypeScriptFiles("."); +BazelAction({}, async ({ project, outDir }) => { + const copyNonTypeScriptFiles = dirRelativePath => { + for (const fileName of fs.readdirSync( + path.join(project, dirRelativePath) + )) { + const relativeFilePath = path.join(dirRelativePath, fileName); + const srcFilePath = path.join(project, relativeFilePath); + let destFilePath = path.join(outDir, relativeFilePath); + fs.ensureDirSync(path.dirname(destFilePath)); + if (fs.lstatSync(srcFilePath).isDirectory()) { + copyNonTypeScriptFiles(relativeFilePath); + } else if ( + fileName !== "node_modules" && + !fileName.endsWith(".ts") && + !fileName.endsWith(".tsx") + ) { + // Symlink any file that isn't a TypeScript file (e.g. precompile JS or CSS assets). + safeSymlink(srcFilePath, destFilePath); + } + } + }; -// Compile with TypeScript. -child_process.execSync( - `${ - process.env.NODE_PATH - }/.bin/tsc --project ${fullSrcDir} --outDir ${destinationDir}`, - { - stdio: "inherit" - } -); + // Copy over any non-TypeScript files (e.g. CSS assets). + copyNonTypeScriptFiles("."); -function copyNonTypeScriptFiles(dirRelativePath) { - for (const fileName of fs.readdirSync( - path.join(fullSrcDir, dirRelativePath) - )) { - const relativeFilePath = path.join(dirRelativePath, fileName); - const srcFilePath = path.join(fullSrcDir, relativeFilePath); - let destFilePath = path.join(destinationDir, relativeFilePath); - fs.ensureDirSync(path.dirname(destFilePath)); - if (fs.lstatSync(srcFilePath).isDirectory()) { - copyNonTypeScriptFiles(relativeFilePath); - } else if ( - fileName !== "node_modules" && - !fileName.endsWith(".ts") && - !fileName.endsWith(".tsx") - ) { - // Symlink any file that isn't a TypeScript file (e.g. precompile JS or CSS assets). - safeSymlink(srcFilePath, destFilePath); + // Compile with TypeScript. + child_process.execSync( + `${process.env.NODE_PATH}/.bin/tsc --project ${project} --outDir ${outDir}`, + { + stdio: "inherit" } - } -} + ); +}); diff --git a/internal/ts_library/create_full_src.js b/internal/ts_library/create_full_src.js deleted file mode 100644 index b9ffbbb..0000000 --- a/internal/ts_library/create_full_src.js +++ /dev/null @@ -1,79 +0,0 @@ -const fs = require("fs-extra"); -const path = require("path"); -const { safeSymlink } = require("../common/symlink"); - -const [ - nodePath, - scriptPath, - targetLabel, - installedNpmPackagesDir, - tsconfigPath, - joinedInternalDeps, - joinedSrcs, - destinationDir -] = process.argv; - -const internalDeps = joinedInternalDeps.split("|"); -const srcs = joinedSrcs.split("|"); - -fs.mkdirSync(destinationDir); -safeSymlink( - path.join(installedNpmPackagesDir, "node_modules"), - path.join(destinationDir, "node_modules") -); - -// Copy every internal dependency into the appropriate location. -for (const internalDep of internalDeps) { - if (!internalDep) { - continue; - } - const [joinedSrcs, compiledDir] = internalDep.split(":"); - const srcs = joinedSrcs.split(";"); - for (const src of srcs) { - if (!src) { - continue; - } - safeSymlink(path.join(compiledDir, src), path.join(destinationDir, src)); - } -} - -// Extract compiler options from tsconfig.json, overriding anything other -// than compiler options. -const originalTsConfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf8")); - -// Copy source code and update import statements in this target's sources. -for (const src of srcs) { - if (!src) { - continue; - } - if (!fs.existsSync(src)) { - console.error(` -Missing file ${src} required by ${targetLabel}. -`); - process.exit(1); - } - const destinationFilePath = path.join(destinationDir, src); - fs.ensureDirSync(path.dirname(destinationFilePath)); - safeSymlink(src, destinationFilePath); -} - -const compilerOptions = {}; -Object.assign(compilerOptions, originalTsConfig.compilerOptions || {}); -Object.assign(compilerOptions, { - moduleResolution: "node", - declaration: true, - rootDir: "." -}); -delete compilerOptions.allowJs; -fs.writeFileSync( - path.join(destinationDir, "tsconfig.json"), - JSON.stringify( - { - compilerOptions, - files: srcs.filter(src => src.endsWith(".ts") || src.endsWith(".tsx")) - }, - null, - 2 - ), - "utf8" -); diff --git a/internal/ts_library/rule.bzl b/internal/ts_library/rule.bzl index 591b3b5..8d6f028 100644 --- a/internal/ts_library/rule.bzl +++ b/internal/ts_library/rule.bzl @@ -1,5 +1,5 @@ -load("//internal/js_library:rule.bzl", "JsLibraryInfo") load("//internal/npm_packages:rule.bzl", "NpmPackagesInfo") +load("//internal/common:context.bzl", "JS_LIBRARY_ATTRIBUTES", "js_context") TsLibraryInfo = provider(fields = [ # Directory containing the TypeScript files (and potentially other assets). @@ -11,246 +11,55 @@ TsLibraryInfo = provider(fields = [ ]) def _ts_library_impl(ctx): - # Ensure that we depend on at most one npm_packages, since we don't want to - # have conflicting package versions coming from separate node_modules - # directories. - direct_npm_packages = [ - dep - for dep in ctx.attr.deps - if NpmPackagesInfo in dep - ] - if len(direct_npm_packages) > 1: - fail("Found more than one set of NPM packages in target definition: " + ",".join([ - dep.label - for dep in direct_npm_packages - ])) - extended_npm_packages = depset( - direct = direct_npm_packages, - transitive = [ - dep[JsLibraryInfo].npm_packages - for dep in ctx.attr.deps - if JsLibraryInfo in dep - ], - ) - npm_packages_list = extended_npm_packages.to_list() - if len(npm_packages_list) > 1: - fail("Found more than one set of NPM packages through dependencies: " + ",".join([ - dep.label - for dep in npm_packages_list - ])) - - # If we depend on an npm_packages target, we'll use its node_modules - # directory to find modules. Otherwise, we'll use an empty node_modules - # directory. - npm_packages = ( - npm_packages_list[0] if len(npm_packages_list) == 1 else ctx.attr._empty_npm_packages - ) + js = js_context(ctx) + providers = [] - # Gather all internal deps (other ts_library rules). - internal_deps = depset( - direct = [ - dep - for dep in ctx.attr.deps - if JsLibraryInfo in dep - ], - transitive = [ - dep[JsLibraryInfo].internal_deps - for dep in ctx.attr.deps - if JsLibraryInfo in dep - ], - ) + js_library = js.library_info(js) + providers.append(js_library) - # Create two directories that contain: - # - source files (including all internal dependencies) - # - node_modules (symlinked to installed external dependencies directory) + js_source = js.library_to_source_info(js, js_library, gen_scripts = [ + [ctx.file._ts_config_genscript, ctx.file.tsconfig], + ]) + providers.append(js_source) - # First version includes all dependencies' TypeScript definitions, which - # requires compiling everything up the tree (slow). Necessary to be able - # to compile TypeScript, including type verification. - _ts_library_create_full_src( - ctx, - internal_deps, - npm_packages, - ctx.outputs.compilation_src_dir, - True, - ) - - # Second version only includes dependencies' transpiled JavaScript code, - # which is a lot faster but does not do any type checking. - _ts_library_create_full_src( - ctx, - internal_deps, - npm_packages, - ctx.outputs.transpilation_src_dir, - False, - ) + js.create_source_dir(js, js_source, ctx.outputs.compilation_src_dir) - # Compile the directory with `tsc` (slower but stricter). - _ts_library_compile( - ctx, - internal_deps, - npm_packages, - ) - - # Transpile the directory with `tsc` (faster, no type checking). - _ts_library_transpile( - ctx, - internal_deps, - npm_packages, - ) - - return [ - JsLibraryInfo( - build_file_path = ctx.build_file_path, - javascript_source_files = [_compiled_extension(f.path) for f in ctx.files.srcs], - compiled_javascript_dir = ctx.outputs.transpiled_dir, - internal_deps = internal_deps, - npm_packages = extended_npm_packages, - npm_packages_installed_dir = npm_packages[NpmPackagesInfo].installed_dir, - ), - TsLibraryInfo( - original_typescript_dir = ctx.outputs.compilation_src_dir, - compiled_typescript_dir = ctx.outputs.compiled_dir, - typescript_source_files = [f.path for f in ctx.files.srcs], - ), - ] + if js.module_name: + js_module = js.library_to_module_info( + js, + js_library, + module_name = js.module_name, + module_root = ctx.outputs.compiled_dir, + ) + providers.append(js_module) -def _compiled_extension(path): - if path.endswith(".tsx"): - return path[:-4] + ".js" - elif path.endswith(".ts"): - return path[:-3] + ".js" - else: - return path + compile_args = js.script_args(js) + compile_args.add("--project", ctx.outputs.compilation_src_dir) + compile_args.add("--outDir", ctx.outputs.compiled_dir) -def _ts_library_create_full_src(ctx, internal_deps, npm_packages, output_dir, for_compilation): - ctx.actions.run( - inputs = [ - ctx.attr._internal_packages[NpmPackagesInfo].installed_dir, - ctx.file._ts_library_create_full_src_script, - npm_packages[NpmPackagesInfo].installed_dir, - ctx.file.tsconfig, - ] + [ - d[TsLibraryInfo].original_typescript_dir if for_compilation and TsLibraryInfo in d else d[JsLibraryInfo].compiled_javascript_dir - for d in internal_deps - ] + ctx.files.srcs, - outputs = [output_dir], - executable = ctx.file._internal_nodejs, - env = { - "NODE_PATH": ctx.attr._internal_packages[NpmPackagesInfo].installed_dir.path + "/node_modules", - }, - arguments = [ - # Run `node create_full_src.js`. - ctx.file._ts_library_create_full_src_script.path, - # Label of the build target (for helpful errors). - "//" + ctx.label.package + ":" + ctx.label.name, - # Directory containing node_modules/ with all external NPM packages - # installed. - npm_packages[NpmPackagesInfo].installed_dir.path, - # tsconfig.json path. - ctx.file.tsconfig.path, - # Source directories of the ts_library targets we depend on. - ("|".join([ - (";".join( - d[TsLibraryInfo].typescript_source_files if for_compilation and TsLibraryInfo in d else d[JsLibraryInfo].javascript_source_files, - )) + - ":" + - ( - d[TsLibraryInfo].original_typescript_dir.path if for_compilation and TsLibraryInfo in d else d[JsLibraryInfo].compiled_javascript_dir.path - ) - for d in internal_deps - ])), - # List of source files, which will be processed ("import" statements - # automatically replaced) and copied into the new directory. - ("|".join([ - f.path - for f in ctx.files.srcs - ])), - # Directory in which to place the result. - output_dir.path, - ], + ts_compile_inputs = depset( + direct = [ctx.outputs.compilation_src_dir], + transitive = [], ) -def _ts_library_compile(ctx, internal_deps, npm_packages): - ctx.actions.run( - inputs = [ - ctx.file._ts_library_compile_script, - ctx.outputs.compilation_src_dir, - ctx.attr._internal_packages[NpmPackagesInfo].installed_dir, - npm_packages[NpmPackagesInfo].installed_dir, - ] + [ - d[TsLibraryInfo].original_typescript_dir if TsLibraryInfo in d else d[JsLibraryInfo].compiled_javascript_dir - for d in internal_deps - ], + js.run_js( + js, + inputs = ts_compile_inputs, outputs = [ctx.outputs.compiled_dir], - executable = ctx.file._internal_nodejs, - env = { - "NODE_PATH": ctx.attr._internal_packages[NpmPackagesInfo].installed_dir.path + "/node_modules", - }, - arguments = [ - # Run `node ts_library/compile.js`. - ctx.file._ts_library_compile_script.path, - # Directory in which the source code as well as tsconfig.json can be found. - ctx.outputs.compilation_src_dir.path, - # Directory in which to generate the compiled JavaScript and TypeScript - # definitions. - ctx.outputs.compiled_dir.path, - ], + script = ctx.file._ts_library_compile_script, + script_args = compile_args, ) -def _ts_library_transpile(ctx, internal_deps, npm_packages): - ctx.actions.run( - inputs = [ - ctx.file._ts_library_transpile_script, - ctx.outputs.transpilation_src_dir, - ctx.attr._internal_packages[NpmPackagesInfo].installed_dir, - npm_packages[NpmPackagesInfo].installed_dir, - ] + [ - d[JsLibraryInfo].compiled_javascript_dir - for d in internal_deps - ], - outputs = [ctx.outputs.transpiled_dir], - executable = ctx.file._internal_nodejs, - env = { - "NODE_PATH": ctx.attr._internal_packages[NpmPackagesInfo].installed_dir.path + "/node_modules", - }, - arguments = [ - # Run `node ts_library/transpile.js`. - ctx.file._ts_library_transpile_script.path, - # Directory in which the source code as well as tsconfig.json can be found. - ctx.outputs.transpilation_src_dir.path, - # Directory in which to generate the transpiled JavaScript and TypeScript - # definitions. - ctx.outputs.transpiled_dir.path, - ], - ) + return providers ts_library = rule( - attrs = { - "srcs": attr.label_list( - allow_files = True, - mandatory = True, - ), - "deps": attr.label_list( - providers = [ - [JsLibraryInfo], - [NpmPackagesInfo], - ], - default = [], - ), + implementation = _ts_library_impl, + attrs = dict(JS_LIBRARY_ATTRIBUTES, **{ "tsconfig": attr.label( allow_files = [".json"], single_file = True, default = Label("//internal/ts_library:default_tsconfig.json"), ), - "_internal_nodejs": attr.label( - allow_files = True, - single_file = True, - default = Label("@nodejs//:node"), - ), - "_internal_packages": attr.label( - default = Label("//internal:packages"), - ), "_ts_library_create_full_src_script": attr.label( allow_files = True, single_file = True, @@ -266,15 +75,16 @@ ts_library = rule( single_file = True, default = Label("//internal/ts_library:transpile.js"), ), - "_empty_npm_packages": attr.label( - default = Label("//internal/npm_packages/empty:packages"), + "_ts_config_genscript": attr.label( + allow_files = True, + single_file = True, + default = Label("//internal/ts_library:tsconfig.gen.js"), ), - }, + }), outputs = { "compilation_src_dir": "%{name}_compilation_src", "compiled_dir": "%{name}_compiled", - "transpilation_src_dir": "%{name}_transpilation_src", - "transpiled_dir": "%{name}_transpiled", + # "transpilation_src_dir": "%{name}_transpilation_src", + # "transpiled_dir": "%{name}_transpiled", }, - implementation = _ts_library_impl, ) diff --git a/internal/ts_library/tsconfig.gen.js b/internal/ts_library/tsconfig.gen.js new file mode 100644 index 0000000..36123c3 --- /dev/null +++ b/internal/ts_library/tsconfig.gen.js @@ -0,0 +1,53 @@ +const fs = require("fs-extra"); +const path = require("path"); + +const DEFAULT_TSCONFIG = { + compilerOptions: { + target: "es2015", + module: "es2015", + moduleResolution: "node", + declaration: true, + strict: true, + noImplicitAny: true, + strictNullChecks: true, + strictFunctionTypes: true, + strictPropertyInitialization: true, + noImplicitThis: true, + alwaysStrict: true, + jsx: "react", + allowSyntheticDefaultImports: true, + baseUrl: ".", + paths: { + "@/*": ["src/*"] + } + } +}; + +module.exports = async ({ package, into, inputs }) => { + const inputFiles = inputs; + if (inputFiles.length > 1) { + throw new Error(`Got too many files for tsconfig generation ${inputFiles}`); + } + const tsconfigInput = inputFiles[0] + ? JSON.parse(inputFiles[0].body) + : DEFAULT_TSCONFIG; + + const compilerOptions = {}; + Object.assign(compilerOptions, tsconfigInput.compilerOptions || {}); + Object.assign(compilerOptions, { + moduleResolution: "node", + declaration: true, + rootDir: "." + }); + delete compilerOptions.allowJs; + return [ + { + path: "tsconfig.json", + body: JSON.stringify({ + compilerOptions, + exclude: ["node_modules"], + include: [`${package.path}/**/*.ts`, `${package.path}/**/*.tsx`] + }) + } + ]; +}; diff --git a/internal/web_bundle/rule.bzl b/internal/web_bundle/rule.bzl index 7ec7475..926d3af 100644 --- a/internal/web_bundle/rule.bzl +++ b/internal/web_bundle/rule.bzl @@ -1,6 +1,5 @@ -load("//internal/js_library:rule.bzl", "JsLibraryInfo") -load("//internal/js_module:rule.bzl", "JsModuleInfo") load("//internal/npm_packages:rule.bzl", "NpmPackagesInfo") +load("//internal/common:context.bzl", "JsLibraryInfo", "JsModuleInfo", "JS_LIBRARY_ATTRIBUTES", "js_context") def _web_bundle_impl(ctx): webpack_config = _create_webpack_config(ctx) diff --git a/internal/yarn.lock b/internal/yarn.lock index ff476ba..2365732 100644 --- a/internal/yarn.lock +++ b/internal/yarn.lock @@ -2185,6 +2185,10 @@ get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" +getopts@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.1.tgz#68120d77abf420e1ade52291977ce050f33ce54e" + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"