diff --git a/packages/rules_prerender/prerender_component2.bzl b/packages/rules_prerender/prerender_component2.bzl new file mode 100644 index 00000000..f2433f11 --- /dev/null +++ b/packages/rules_prerender/prerender_component2.bzl @@ -0,0 +1,268 @@ +"""Defines `prerender_component()` functionality.""" + +load("@aspect_rules_js//js:defs.bzl", "js_library") +load("@aspect_rules_js//js:providers.bzl", "JsInfo", "js_info") +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") +load("//common:label.bzl", "absolute") +load("//common:paths.bzl", "is_js_file", "is_ts_file", "is_ts_declaration_file") +load("//packages/rules_prerender/css:css_binaries.bzl", "css_binaries") +load("//packages/rules_prerender/css:css_group.bzl", "css_group") +load("//packages/rules_prerender/css:css_library.bzl", "css_library") +load( + ":prerender_metadata.bzl", + "PrerenderMetadataInfo", + "alias_with_metadata", + "prerender_metadata", +) +load(":web_resources.bzl", "web_resources") + +visibility("public") + +def prerender_component( + name, + prerender, + scripts = None, + styles = None, + resources = None, + testonly = None, + visibility = None, +): + """Encapsulates an HTML/JS/CSS component for use in prerendering a web page. + + This rule encapsulates the HTML, JavaScript, CSS, and static resources used + by a logical "component". A "component" is effectively a prerendered + fragment of HTML which provides some functionality (via JavaScript) and + styling (via CSS) with any required static files (ex. an image) at a + specific path (ex. /my-logo.png). Components are reusable pieces of UI which + can be composed together to build a complex static site. + + IMPORTANT: `prerender_component()` has some special rules about _how_ it can + be used. It doesn't inherently _do_ anything special. It only collects all + the various parts of a component (HTML, scripts, styles, resources) with + some extra metadata for the bundling process and re-exports them at + `%{name}_prerender`, `%{name}_scripts`, `%{name}_styles`, and + `%{name}_resources`. + + A `prerender_component()` should _not_ be depended upon directly, instead + you should depend on the re-exports for the specific parts of the component + you need. The bundling process will bundle the entire component for you as + expected. For example: + + ```BUILD + # my_component/BUILD.bazel + + prerender_component( + name = "my_component", + prerender = ":prerender", + ) + + # IMPORTANT! No target except `:my_component` should depend on this. If they + # do, the generated site may be missing scripts, styles, resources, etc. + ts_project( + name = "prerender", + srcs = ["my_component_prerender.mts"], + ) + ``` + + ```BUILD + # my_other_component/BUILD.bazel + + prerender_component( + name = "my_other_component", + prerender = ":my_other_component_prerender_lib", + + # No dependency on `//my_component/...` here. + ) + + ts_project( + name = "prerender", + srcs = ["my_other_component_prerender.mts"], + + # IMPORTANT! Depend on the `_prerender` alias generated by + # `prerender_component()`. DON'T depend on `//my_component:prerender` + # directly. + deps = ["//my_component:my_component_prerender"], + ) + ``` + + The rules here are: + 1. Any direct dependency of a `prerender_component()` target should _only_ + be used by that `prerender_component()`. + 2. Any additional desired dependencies, should go through the relevant + `_prerender`, `_scripts`, `_styles`, `_resources` re-export generated by + the `prerender_component()` macro. + * Exception: Unit tests may directly depend on targets provided they + do not use any `prerender_*` rules in the test. + 3. Never depend on a `prerender_component()` directly, always depend on the + specific re-export you want. + * Exception: You may `bazel build` a `prerender_component()` target or + have a `build_test()` depend on it to verify that it is buildable. + + Args: + name: The name of this rule. + prerender: Required. A `ts_project()` target which acts as a library for + prerendering at build time. + scripts: A `ts_project()` holding client-side scripts for this + component. + styles: A `css_library()` holding the styles for this component. + resources: A `web_resources()` target holding other static files needed + by this component at runtime. + testonly: See https://docs.bazel.build/versions/master/be/common-definitions.html. + visibility: See https://docs.bazel.build/versions/master/be/common-definitions.html. + + Outputs: + %{name}: A library which verifies that all the different aspects of the + component are buildable and runs various sanity checks. + %{name}_prerender: A reexport of the `prerender` attribute. + %{name}_scripts: A reexport of the `scripts` attribute. + %{name}_styles: A reexport of the `styles` attribute. + %{name}_resources: A reexport of the `resources` attribute. + """ + styles_reexport = "%s_styles_reexport" % name + if styles: + _inline_css_reexport( + name = styles_reexport, + styles = styles, + visibility = visibility, + testonly = testonly, + ) + + # Metadata provider. + metadata = "%s_metadata" % name + prerender_metadata( + name = metadata, + prerender = prerender, + scripts = scripts, + styles = ":%s" % styles_reexport if styles else None, + styles_import_map = ":%s" % styles_reexport if styles else None, + resources = resources, + testonly = testonly, + ) + + # Prerendering JavaScript. + alias_with_metadata( + name = "%s_prerender" % name, + metadata = ":%s" % metadata, + actual = prerender, + visibility = visibility, + testonly = testonly, + ) + + # Client-side JavaScript. + scripts_target = "%s_scripts" % name + if scripts: + alias_with_metadata( + name = scripts_target, + metadata = ":%s" % metadata, + actual = scripts, + visibility = visibility, + testonly = testonly, + ) + else: + js_library( + name = scripts_target, + srcs = [], + visibility = visibility, + testonly = testonly, + ) + + # CSS styles. + styles_target = "%s_styles" % name + if styles: + alias_with_metadata( + name = styles_target, + metadata = metadata, + actual = ":%s" % styles_reexport, + testonly = testonly, + visibility = visibility, + ) + + # Resources. + resources_target = "%s_resources" % name + if resources: + alias_with_metadata( + name = resources_target, + metadata = ":%s" % metadata, + actual = resources, + visibility = visibility, + testonly = testonly, + ) + +def _js_reexport_impl(ctx): + merged_js_info = js_info( + declarations = depset([], + transitive = [src[JsInfo].declarations + for src in ctx.attr.srcs], + ), + npm_linked_package_files = depset([], + transitive = [src[JsInfo].npm_linked_package_files + for src in ctx.attr.srcs], + ), + npm_linked_packages = depset([], + transitive = [src[JsInfo].npm_linked_packages + for src in ctx.attr.srcs], + ), + npm_package_store_deps = depset([], + transitive = [src[JsInfo].npm_package_store_deps + for src in ctx.attr.srcs], + ), + sources = depset([], + transitive = [src[JsInfo].sources + for src in ctx.attr.srcs], + ), + transitive_declarations = depset([], + transitive = [dep[JsInfo].transitive_declarations + for dep in ctx.attr.srcs + ctx.attr.deps], + ), + transitive_npm_linked_package_files = depset([], + transitive = [dep[JsInfo].transitive_npm_linked_package_files + for dep in ctx.attr.srcs + ctx.attr.deps], + ), + transitive_npm_linked_packages = depset([], + transitive = [dep[JsInfo].transitive_npm_linked_packages + for dep in ctx.attr.srcs + ctx.attr.deps], + ), + transitive_sources = depset([], + transitive = [dep[JsInfo].transitive_sources + for dep in ctx.attr.srcs + ctx.attr.deps], + ), + ) + + return [ + DefaultInfo(files = merged_js_info.sources), + merged_js_info, + ] + +_js_reexport = rule( + implementation = _js_reexport_impl, + attrs = { + "srcs": attr.label_list( + default = [], + providers = [JsInfo], + ), + "deps": attr.label_list( + default = [], + providers = [JsInfo], + ), + }, + doc = """ + Re-exports the given `ts_project()` and `js_library()` targets. Targets + in `srcs` have their direct sources re-exported as the direct sources of + this target, while targets in `deps` are only included as transitive + sources. + + This rule serves two purposes: + 1. It re-exports **both** `ts_project()` and `js_library()`. + 2. It merges multiple targets together, depending on all of them but + only re-exporting direct sources from the `srcs` attribute. Even + with `ts_project()` re-export it is not possible to re-export only + some of the given targets. + """, +) + +def _inline_css_reexport(name, styles, testonly = None, visibility = None): + css_binaries( + name = name, + testonly = testonly, + deps = [styles], + ) diff --git a/packages/rules_prerender/prerender_metadata.bzl b/packages/rules_prerender/prerender_metadata.bzl new file mode 100644 index 00000000..5a78df01 --- /dev/null +++ b/packages/rules_prerender/prerender_metadata.bzl @@ -0,0 +1,92 @@ +load("@aspect_rules_js//js:providers.bzl", "JsInfo") +load( + "//packages/rules_prerender/css:css_providers.bzl", + "CssInfo", + "CssImportMapInfo", +) +load("//packages/rules_prerender:web_resources.bzl", "WebResourceInfo") + +PrerenderMetadataInfo = provider( + "Holds all the providers for each component \"slice\".", + fields = { + "prerender": "The JSInfo of the prerender target.", + "scripts": "The JSInfo of the scripts target.", + "styles": "The CssInfo of the styles target.", + "styles_import_map": "The CssImportMapInfo of the styles target.", + "resources": "The WebResourceInfo of the resources target.", + }, +) + +def _prerender_metadata_impl(ctx): + return PrerenderMetadataInfo( + prerender = _safe_get(ctx.attr.prerender, JsInfo), + scripts = _safe_get(ctx.attr.scripts, JsInfo), + styles = _safe_get(ctx.attr.styles, CssInfo), + styles_import_map = _safe_get(ctx.attr.styles_import_map, CssImportMapInfo), + resources = _safe_get(ctx.attr.resources, WebResourceInfo), + ) + +prerender_metadata = rule( + implementation = _prerender_metadata_impl, + attrs = { + "prerender": attr.label( + mandatory = True, + providers = [JsInfo], + ), + "scripts": attr.label(providers = [JsInfo]), + "styles": attr.label(providers = [CssInfo]), + "styles_import_map": attr.label(providers = [CssImportMapInfo]), + "resources": attr.label(providers = [WebResourceInfo]), + }, + doc = """ + Collects all the various "slices" of a component together into a single + target and returns a `PrerenderMetadataInfo` linking to all their + providers. + """, +) + +def _alias_with_metadata_impl(ctx): + # Re-export the metadata provider. + providers = [ctx.attr.metadata[PrerenderMetadataInfo]] + + # Re-export all additional known providers from the actual target. + providers.extend(_safe_get_all(ctx.attr.actual, [ + DefaultInfo, + JsInfo, + CssInfo, + CssImportMapInfo, + WebResourceInfo, + ])) + + return providers + +alias_with_metadata = rule( + implementation = _alias_with_metadata_impl, + attrs = { + "metadata": attr.label( + mandatory = True, + providers = [PrerenderMetadataInfo], + ), + "actual": attr.label( + mandatory = True, + ), + }, + doc = """ + Creates an alias to the given `actual` target additionally providing the + `PrerenderMetadataInfo` from the `metadata` target. + """, +) + +def _safe_get(target, provider): + if target and provider in target: return target[provider] + + return None + +def _safe_get_all(target, providers): + output = [] + + for provider in providers: + if provider in target: + output.append(target[provider]) + + return output diff --git a/packages/rules_prerender/prerender_pages2.bzl b/packages/rules_prerender/prerender_pages2.bzl new file mode 100644 index 00000000..43482fdf --- /dev/null +++ b/packages/rules_prerender/prerender_pages2.bzl @@ -0,0 +1,133 @@ +"""Defines `prerender_pages()` functionality.""" + +load(":multi_inject_resources.bzl", "multi_inject_resources") +load(":prerender_pages_unbundled2.bzl", "prerender_pages_unbundled") +load(":scripts_bundle.bzl", "scripts_bundle") +load(":web_resources.bzl", "web_resources") + +visibility("public") + +def prerender_pages( + name, + entry_point, + prerender, + scripts = None, + styles = None, + resources = None, + bundle_js = True, + testonly = None, + visibility = None, + debug_target = None, +): + """Renders multiple resources at build time and bundles client-side content. + + This provides a higher-level implementation of `prerender_pages_unbundled`, + automatically bundling client-side resources. + + This invokes the default export function of the given `entry_point` and + generates a directory of files with the result. + + The file listed in `entry_point` must compile to an ESM module with a + default export of the type: + + ``` + () => Iterable | Promise> + | AsyncIterable + ``` + + Note: You may want to write this using the `Generator` or `AsyncGenerator` + types, as they are allowed subtypes and will enable TypeScript to catch some + foot-guns with `return` and `yield` that come with generators. + + ```typescript + export default function*(): Generator { + // ... + } + + // OR + + export default async function*(): + AsyncGenerator { + // ... + } + ``` + + Any scripts on each page that are included with `includeScript()` are + bundled together into a single JavaScript file and inserted into that page + as a `