From 656bf98d8873cb1cf6c405db1d5a9ca98ef0b0a7 Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Sat, 26 Jun 2021 19:50:28 -0700 Subject: [PATCH] Accepts JS sources in `prerender_component()` and a `js_library()` as a scripts dependency. This chooses to generate either a `ts_library()` or `js_library()` based on the sources provided to `prerender_component()`. Whatever is given, is re-exported via `js_reexport()` which propagates both `JSModuleInfo` and `JSEcmaScriptModuleInfo`. This needs to be custom because `js_library()` only returns `JSModuleInfo` while `ts_library()` returns both, but a `js_library()` that depends on a `ts_library()` will drop its dependency's `JSEcmaScriptModuleInfo`, meaning there is no clean way of re-exporting a target which may be a `js_library()` or a `ts_library()`. --- common/BUILD.bazel | 15 +++ common/paths.bzl | 5 + common/paths_test.bzl | 22 +++ examples/custom_bundling/BUILD.bazel | 4 +- packages/rules_prerender/BUILD.bazel | 2 + .../rules_prerender/prerender_component.bzl | 125 +++++++++++++++--- packages/rules_prerender/prerender_pages.bzl | 45 ++++++- .../prerender_pages_unbundled.bzl | 18 ++- .../rules_prerender/prerender_resources.bzl | 3 +- 9 files changed, 213 insertions(+), 26 deletions(-) create mode 100644 common/paths.bzl create mode 100644 common/paths_test.bzl diff --git a/common/BUILD.bazel b/common/BUILD.bazel index a9c3a793..2a97c4a5 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -3,6 +3,7 @@ load("@npm//@bazel/typescript:index.bzl", "ts_library") load("//tools:jasmine.bzl", "jasmine_node_test") load("//tools:publish.bzl", "publish_files") load(":label_test.bzl", "label_test_suite") +load(":paths_test.bzl", "paths_test_suite") publish_files( name = "publish_files", @@ -118,6 +119,20 @@ bzl_library( label_test_suite(name = "label_test") +bzl_library( + name = "paths", + srcs = ["paths.bzl"], + visibility = ["//visibility:public"], +) + +bzl_library( + name = "paths_test_lib", + srcs = ["paths_test.bzl"], + deps = [":paths"], +) + +paths_test_suite(name = "paths_test") + ts_library( name = "runfiles", srcs = ["runfiles.ts"], diff --git a/common/paths.bzl b/common/paths.bzl new file mode 100644 index 00000000..56707487 --- /dev/null +++ b/common/paths.bzl @@ -0,0 +1,5 @@ +"""Utilities based around file paths.""" + +def is_js_file(src): + """Returns whether or not the given src path is a JavaScript file.""" + return src.endswith(".js") or src.endswith(".mjs") or src.endswith(".cjs") diff --git a/common/paths_test.bzl b/common/paths_test.bzl new file mode 100644 index 00000000..c6762db2 --- /dev/null +++ b/common/paths_test.bzl @@ -0,0 +1,22 @@ +"""Tests for `paths.bzl`.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load(":paths.bzl", "is_js_file") + +def _is_js_file_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals(env, True, is_js_file("foo/bar/baz.js")) + asserts.equals(env, True, is_js_file("foo/bar/baz.mjs")) + asserts.equals(env, True, is_js_file("foo/bar/baz.cjs")) + + asserts.equals(env, False, is_js_file("foo/bar/baz.ts")) + asserts.equals(env, False, is_js_file("foo/bar/baz.d.ts")) + asserts.equals(env, False, is_js_file("foo/bar/baz.test")) + + return unittest.end(env) + +_is_js_file_test = unittest.make(_is_js_file_impl) + +def paths_test_suite(name): + unittest.suite(name, _is_js_file_test) diff --git a/examples/custom_bundling/BUILD.bazel b/examples/custom_bundling/BUILD.bazel index b712343a..59899eb0 100644 --- a/examples/custom_bundling/BUILD.bazel +++ b/examples/custom_bundling/BUILD.bazel @@ -33,7 +33,9 @@ ts_library( # Manually bundle all scripts. rollup_bundle( name = "bundle", - entry_point = ":page_scripts.ts", + # TODO: Need to use `_esm_sources()` to pick out the `.mjs` file and depend + # on it directly. + entry_point = ":page_scripts.mjs", config_file = "rollup.config.js", link_workspace_root = True, silent = True, diff --git a/packages/rules_prerender/BUILD.bazel b/packages/rules_prerender/BUILD.bazel index d7451591..76f816e7 100644 --- a/packages/rules_prerender/BUILD.bazel +++ b/packages/rules_prerender/BUILD.bazel @@ -64,6 +64,7 @@ bzl_library( deps = [ ":web_resources", "//common:label", + "//common:paths", ], ) @@ -96,6 +97,7 @@ bzl_library( visibility = ["//:__pkg__"], deps = [ ":web_resources", + "//common:paths", "//packages/renderer:build_vars", ], ) diff --git a/packages/rules_prerender/prerender_component.bzl b/packages/rules_prerender/prerender_component.bzl index e8b21844..38c201ae 100644 --- a/packages/rules_prerender/prerender_component.bzl +++ b/packages/rules_prerender/prerender_component.bzl @@ -1,7 +1,14 @@ """Defines `prerender_component()` functionality.""" +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load( + "@build_bazel_rules_nodejs//:providers.bzl", + "JSModuleInfo", + "JSEcmaScriptModuleInfo", +) load("@npm//@bazel/typescript:index.bzl", "ts_library") load("//common:label.bzl", "absolute") +load("//common:paths.bzl", "is_js_file") load(":web_resources.bzl", "web_resources") def prerender_component( @@ -31,11 +38,14 @@ def prerender_component( Args: name: The name of this rule. - srcs: The TypeScript source files for use in prerendering. + srcs: The source files for use in prerendering. May be `*.ts` files or + `*.js`/`*.mjs`/`*.cjs`. All source files must be JavaScript, or all + source files must be TypeScript. However, mixing the two is not + allowed. `*.d.ts` files are also allowed in either case. tsconfig: A label referencing a tsconfig.json file or `ts_config()` - target. Will be used to compile files in `srcs`. + target. Will be used to compile `*.ts` files in `srcs`. data: See https://docs.bazel.build/versions/master/be/common-definitions.html. - lib_deps: `ts_library()` dependencies for the TypeScript source files. + lib_deps: Dependencies for the source files. scripts: List of client-side JavaScript libraries which can be included in the prerendered HTML. styles: List of CSS files or `filegroup()`s of CSS files which can be @@ -66,15 +76,31 @@ def prerender_component( """ prerender_lib = "%s_prerender" % name - ts_library( - name = prerender_lib, - srcs = srcs, - tsconfig = tsconfig, - data = data, - deps = lib_deps + ["%s_prerender" % absolute(dep) for dep in deps], - testonly = testonly, - visibility = visibility, - ) + if all([src.endswith(".ts") or src.endswith(".d.ts") for src in srcs]): + ts_library( + name = prerender_lib, + srcs = srcs, + tsconfig = tsconfig, + data = data + styles, + deps = lib_deps + ["%s_prerender" % absolute(dep) for dep in deps], + testonly = testonly, + visibility = visibility, + ) + elif all([is_js_file(src) or src.endswith(".d.ts") for src in srcs]): + js_library( + name = prerender_lib, + srcs = srcs + data + styles, # `data` is included in `srcs`. + deps = lib_deps + ["%s_prerender" % absolute(dep) for dep in deps], + testonly = testonly, + visibility = visibility, + ) + else: + fail(" ".join(""" +All sources must be TypeScript (`*.ts`) or all sources must be JavaScript +(`*.js` / `*.mjs` / `*.cjs`). It is not possible to use some JavaScript sources +and some TypeScript sources in the same component (excluding `*.d.ts` files, +which are always allowed). + """.strip().split("\n"))) native.alias( name = "%s_prerender_for_test" % name, @@ -82,10 +108,10 @@ def prerender_component( testonly = True, ) - ts_library( + js_reexport( name = "%s_scripts" % name, - srcs = [], - deps = scripts + ["%s_scripts" % absolute(dep) for dep in deps], + srcs = scripts, + deps = ["%s_scripts" % absolute(dep) for dep in deps], testonly = testonly, visibility = visibility, ) @@ -103,3 +129,72 @@ def prerender_component( visibility = visibility, deps = resources + ["%s_resources" % absolute(dep) for dep in deps], ) + +def _js_reexport_impl(ctx): + # TODO + # for dep in ctx.attr.deps: + # if JSEcmaScriptModuleInfo in dep: + # print("%s - %s" % (ctx.label, dep[JSEcmaScriptModuleInfo])) + js_srcs = [src for src in ctx.files.srcs if is_js_file(src.path)] + + merged_js_module_info = JSModuleInfo( + direct_sources = depset(js_srcs, + transitive = [src[JSModuleInfo].direct_sources + for src in ctx.attr.srcs + if JSModuleInfo in src], + ), + sources = depset(js_srcs, + transitive = [dep[JSModuleInfo].sources + for dep in ctx.attr.srcs + ctx.attr.deps + if JSModuleInfo in dep], + ), + ) + + merged_js_ecma_script_module_info = JSEcmaScriptModuleInfo( + direct_sources = depset(js_srcs, + transitive = [src[JSEcmaScriptModuleInfo].direct_sources + for src in ctx.attr.srcs + if JSEcmaScriptModuleInfo in src], + ), + sources = depset(js_srcs, + transitive = [dep[JSEcmaScriptModuleInfo].sources + for dep in ctx.attr.srcs + ctx.attr.deps + if JSEcmaScriptModuleInfo in dep], + ), + ) + + # TODO: Remove + output_group_info = OutputGroupInfo( + js_module_info_direct_sources = depset(js_srcs, + transitive = [src[JSModuleInfo].direct_sources + for src in ctx.attr.srcs + if JSModuleInfo in src], + ), + js_ecma_script_module_info_direct_sources = depset(js_srcs, + transitive = [src[JSEcmaScriptModuleInfo].direct_sources + for src in ctx.attr.srcs + if JSEcmaScriptModuleInfo in src], + ), + ) + + return [ + merged_js_module_info, + merged_js_ecma_script_module_info, + output_group_info, + ] + +# TODO: Rename? It's not really a reexport? +# TODO: Move to own file? +js_reexport = rule( + implementation = _js_reexport_impl, + attrs = { + "srcs": attr.label_list( + default = [], + allow_files = True, + ), + "deps": attr.label_list( + mandatory = True, + providers = [JSModuleInfo], + ), + }, +) diff --git a/packages/rules_prerender/prerender_pages.bzl b/packages/rules_prerender/prerender_pages.bzl index 68918e4a..35da0274 100644 --- a/packages/rules_prerender/prerender_pages.bzl +++ b/packages/rules_prerender/prerender_pages.bzl @@ -1,5 +1,10 @@ """Defines `prerender_pages()` functionality.""" +load( + "@build_bazel_rules_nodejs//:providers.bzl", + "JSEcmaScriptModuleInfo", + "JSModuleInfo", +) load("@npm//@bazel/postcss:index.bzl", "postcss_binary") load("@npm//@bazel/rollup:index.bzl", "rollup_bundle") load(":multi_inject_resources.bzl", "multi_inject_resources") @@ -122,18 +127,24 @@ def prerender_pages( testonly = True, ) + _esm_sources( + name = "%s_scripts" % name, + dep = "%s_scripts" % prerender_name, + testonly = testonly, + ) + bundle = "%s_bundle" % name if bundle_js: # Bundle all client-side scripts at `%{name}_bundle.js`. rollup_bundle( name = bundle, - entry_point = ":%s_scripts.ts" % prerender_name, + entry_point = ":%s_scripts" % name, config_file = "//packages/rules_prerender:rollup-default.config.js", link_workspace_root = True, silent = True, testonly = testonly, deps = [ - ":%s_scripts" % prerender_name, + ":%s_scripts" % name, "@npm//@rollup/plugin-node-resolve", ], ) @@ -175,3 +186,33 @@ def prerender_pages( ":%s_resources" % prerender_name, ], ) + +def _esm_sources_impl(ctx): + providers = [] + + # Propagate providers of dependencies. + if JSEcmaScriptModuleInfo in ctx.attr.dep: + providers.append(ctx.attr.dep[JSEcmaScriptModuleInfo]) + if JSModuleInfo in ctx.attr.dep: + providers.append(ctx.attr.dep[JSModuleInfo]) + + # Use direct sources as `DefaultInfo`, prefering `JSEcmaScriptModuleInfo`. + if JSEcmaScriptModuleInfo in ctx.attr.dep: + providers.append(DefaultInfo( + files = ctx.attr.dep[JSEcmaScriptModuleInfo].direct_sources, + )) + elif JSModuleInfo in ctx.attr.dep: + providers.append( + DefaultInfo(files = ctx.attr.dep[JSModuleInfo].direct_sources), + ) + else: + fail("Dependency (%s) does provides neither JSEcmaScriptModuleInfo nor JSModuleInfo. It must provide at least one." % ctx.attr.dep.label) + + return providers + +_esm_sources = rule( + implementation = _esm_sources_impl, + attrs = { + "dep": attr.label(mandatory = True), + }, +) diff --git a/packages/rules_prerender/prerender_pages_unbundled.bzl b/packages/rules_prerender/prerender_pages_unbundled.bzl index 8235a7a3..234afb85 100644 --- a/packages/rules_prerender/prerender_pages_unbundled.bzl +++ b/packages/rules_prerender/prerender_pages_unbundled.bzl @@ -1,10 +1,10 @@ """Defines `prerender_pages_unbundled()` functionality.""" -load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "nodejs_binary") load("@npm//@bazel/typescript:index.bzl", "ts_library") load("//common:label.bzl", "absolute", "file_path_of") load(":entry_points.bzl", "script_entry_point", "style_entry_point") -load(":prerender_component.bzl", "prerender_component") +load(":prerender_component.bzl", "prerender_component", "js_reexport") load(":prerender_resources.bzl", "prerender_resources") load(":web_resources.bzl", "WebResourceInfo", "web_resources") @@ -58,9 +58,9 @@ def prerender_pages_unbundled( Outputs: %{name}: A `web_resources()` target containing all the files generated by the `src` file at their corresponding locations. - %{name}_scripts: A `ts_library()` rule containing all the client-side + %{name}_scripts: A `js_library()` rule containing all the client-side scripts used by the page. This includes a generated file - `%{name}_scripts.ts` which acts as an entry point, importing all + `%{name}_scripts.js` which acts as an entry point, importing all scripts that were included in the page via `includeScript()`. %{name}_styles: A `filegroup()` containing all the CSS styles used by the page. @@ -116,7 +116,11 @@ def prerender_pages_unbundled( # Execute the runner to generate annotated resources. annotated = "%s_annotated" % name - js_src = ".ts".join(src.split(".ts")[:-1]) + ".js" + js_src = ( + ".ts".join(src.split(".ts")[:-1]) + ".js" + if src.endswith(".ts") + else src + ) prerender_resources( name = annotated, entry_point = file_path_of(absolute(js_src)), @@ -136,7 +140,7 @@ def prerender_pages_unbundled( # Generate the entry point importing all included scripts. client_scripts = "%s_scripts" % name - script_entry = "%s.ts" % client_scripts + script_entry = "%s.js" % client_scripts script_entry_point( name = "%s_entry" % client_scripts, metadata = metadata, @@ -145,7 +149,7 @@ def prerender_pages_unbundled( ) # Reexport all included scripts at `%{name}_scripts`. - ts_library( + js_reexport( name = client_scripts, srcs = [script_entry], deps = [":%s" % component_scripts], diff --git a/packages/rules_prerender/prerender_resources.bzl b/packages/rules_prerender/prerender_resources.bzl index 6e34d8b2..3f5669da 100644 --- a/packages/rules_prerender/prerender_resources.bzl +++ b/packages/rules_prerender/prerender_resources.bzl @@ -1,6 +1,7 @@ """Defines `prerender_resources()` functionality.""" load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") +load("//common:paths.bzl", "is_js_file") load("//packages/renderer:build_vars.bzl", "RENDERER_RUNTIME_DEPS") load(":web_resources.bzl", "WebResourceInfo") @@ -60,7 +61,7 @@ def prerender_resources( visibility: See https://docs.bazel.build/versions/master/be/common-definitions.html. """ # Validate `entry_point`. - if "/" not in entry_point or not entry_point.endswith(".js"): + if "/" not in entry_point or not is_js_file(entry_point): fail(("`entry_point` (%s) *must* be a workspace-relative path of the" + " format: \"path/to/pkg/file.js\"") % entry_point)