Skip to content

Commit

Permalink
Rewrites prerender_component to use a new design based on `Prerende…
Browse files Browse the repository at this point in the history
…rMetadataInfo`.

See #40 (comment) for full design explanation.

This doesn't actually change the existing `prerender_component`. Instead it creates a new `prerender_component2` with the new implementation. The same is done for `prerender_pages_unbundled2` and `prerender_pages2`. Once all existing examples have been migrated to the new implementation, the old one will be deleted, and these macros will be renamed.
  • Loading branch information
dgp1130 committed Jul 22, 2023
1 parent 3e66fa9 commit 29f383a
Show file tree
Hide file tree
Showing 4 changed files with 898 additions and 0 deletions.
268 changes: 268 additions & 0 deletions packages/rules_prerender/prerender_component2.bzl
Original file line number Diff line number Diff line change
@@ -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],
)
92 changes: 92 additions & 0 deletions packages/rules_prerender/prerender_metadata.bzl
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 29f383a

Please sign in to comment.