diff --git a/README.md b/README.md index 60ba592a..f2d02f20 100644 --- a/README.md +++ b/README.md @@ -79,26 +79,32 @@ load("@rules_prerender//:index.bzl", "prerender_component", "web_resources") prerender_component( name = "my_component", # The library which will prerender the HTML at build time in a Node process. - srcs = ["my_component_prerender.tsx"], - # Other `ts_project()` or `js_library()` targets used by `my_component_prerender.ts`. - lib_deps = [ - "//:node_modules/@rules_prerender/preact", - "//:node_modules/preact", - ], + prerender = ":prerender", # Client-side JavaScript to be executed in the browser. - scripts = [":scripts"], + scripts = ":scripts", # Styles for the component. - styles = [":styles"], + styles = ":styles", # Other resources required by the component (images, fonts, static JSON, etc.). - resources = [":resources"], - # Other `prerender_component()` rules used by `my_component_prerender.ts`. + resources = ":resources", +) + +# Compile the prerendering logic (can also be a `js_library`). +ts_project( + name = "prerender", + srcs = ["my_component_prerender.tsx"], deps = [ - "//:prerender_components/@rules_prerender/declarative_shadow_dom", - "//my_other_component", + # See "Component composition" to learn more about how to depend on + # another `prerender_component`. + "//my_other_component:my_other_component_prerender", + "//:prerender_components/@rules_prerender/declarative_shadow_dom_prerender", + + # Regular dependencies. + "//:node_modules/@rules_prerender/preact", + "//:node_modules/preact", ], ) -# Client-side scripts to be executed in the browser. +# Client-side scripts to be executed in the browser (can also be a `js_library`). ts_project( name = "scripts", srcs = ["my_component.mts"], @@ -106,6 +112,7 @@ ts_project( deps = ["//some/other/package:ts_proj"], ) +# Any styles needed by this component to render correctly. css_library( name = "styles", srcs = ["my_component.css"], @@ -131,10 +138,7 @@ import { Template, includeScript, inlineStyle } from '@rules_prerender/preact'; import { VNode } from 'preact'; import { OtherComponent } from '../my_other_component/my_other_component_prerender.js'; -/** - * Render partial HTML. In this case we're just using a string literal, but you - * could reasonably use lit-html, React, or any other templating library. - */ +/** Render partial HTML with Preact. */ export function MyComponent({ name }: { name: string }): VNode { return
{/* Use declarative shadow DOM to isolate styles. If you're not familiar @@ -205,7 +209,7 @@ document.getElementById('show').addEventListener('click', () => { The second part of the rule set leverages such components to prerender an entire web page. -```typescript +```tsx // my_page/my_page_prerender.tsx import { PrerenderResource, renderToHtml } from '@rules_prerender/preact'; @@ -233,6 +237,7 @@ export default function* render(): Generator { ```python # my_page/BUILD.bazel +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") load("@rules_prerender//:index.bzl", "prerender_pages", "web_resources_devserver") # Renders the page, bundles JavaScript and CSS, injects the relevant @@ -247,18 +252,29 @@ load("@rules_prerender//:index.bzl", "prerender_pages", "web_resources_devserver # dependencies. prerender_pages( name = "prerendered_page", - # Script to invoke the default export of to generate the page. - src = "my_page_prerender.tsx", - lib_deps = [ + # Import specifier for the JavaScript output from `:prerender` which + # generates the page. + entry_point = "./my_page_prerender.js", + # Depend on the library containing `my_page_prerender.js`. + prerender = ":prerender", +) + +ts_project( + name = "prerender", + srcs = ["my_page_prerender.tsx"], + deps = [ + # See "Component composition" to learn more about how to depend on + # another `prerender_component`. + "//my_component:my_component_prerender", + + # Other dependencies. "//:node_modules/@rules_prerender/preact", "//:node_modules/preact", ], - # Components used during prerendering. - deps = ["//my_component"], ) -# Simple server to test out this page. `bazel run` / `ibazel run` this target to -# check out the page at `/my_page/index.html`. +# Small dev server to test out this page. `bazel run` / `ibazel run` this target +# to check out the page at `/my_page/index.html`. web_resources_devserver( name = "devserver", resources = ":prerendered_page", @@ -307,13 +323,140 @@ generate the application as a directory and upload it to a CDN for production deployments. They could even make a separate `bazel run //my_site:deploy` target which performs the upload and run it from CI for easy deployments! +### Component composition + +The `prerender_component` target generates aliases to the targets passed in as +inputs. Consider the following example: + +```python +load("@rules_prerender//:index.bzl", "prerender_component") + +prerender_component( + name = "component", + prerender = ":my_prerender_lib", + scripts = ":my_scripts_lib", + styles = ":my_styles_lib", + resources = ":my_resources_lib", +) +``` + +This will generate the following aliases: + +* `:component_prerender` -> `:my_prerender_lib` +* `:component_scripts` -> `:my_scripts_lib` +* `:component_styles` -> `:my_styles_lib` +* `:component_resources` -> `:my_resources_lib` + +If you want to use any part of a component, you can use it directly rather than +depending on `:component`. However, you _must_ depend on that part through one +of the above aliases. + +For example, consider the following component: + +```tsx +// my_component/prerender.mts + +import { VNode } from 'preact'; + +/** Render partial HTML with Preact. */ +export function MyComponent({ name }: { name: string }): VNode { + return
Hello, {name}!
+} +``` + +With the following `BUILD.bazel` file: + +```python +# my_component/BUILD.bazel + +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") +load("@rules_prerender//:index.bzl", "prerender_component") + +prerender_component( + name = "my_component", + prerender = ":prerender_lib", + # ... +) + +ts_project( + name = "prerender_lib", + srcs = ["prerender.mts"], +) +``` + +To use this, you can import `MyComponent` directly like you would any other +function. + +```typescript +// my_other_component/prerender.mts + +import { MyComponent } from '../my_component/prerender.js'; + +// ... +``` + +However instead of depending on `//my_component:prerender_lib`, depend on +`//my_component:my_component_prerender`. + +```python +# my_other_component/BUILD.bazel + +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") +load("@rules_prerender//:index.bzl", "prerender_component") + +# Does not reference `//my_component` at all. +prerender_component( + name = "my_other_component", + prerender = ":prerender_lib", + # ... +) + +ts_project( + name = "prerender_lib", + srcs = ["prerender.mts"], + # IMPORTANT: Depend on `:my_component_prerender` instead of `:prerender_lib`. + deps = ["//my_component:my_component_prerender"], +) +``` + +While this looks like just an `alias`, it is +[actually load bearing](/docs/architecture/prerender_component.md) and +_required_. + +The same requirement to use the aliases applies to client-side JavaScript +(`_scripts`), CSS styles (`_styles`), and generated resources (`_resources`). + +### `prerender_component` rules + +As indicated by [component composition](#component-composition), the +`prerender_component` macro is a bit unique compared to most Bazel macros/rules +and has a few special rules for how it is used. + +1. Any direct dependency of a `prerender_component` target should _only_ be + used by that `prerender_component`. +1. Any additional desired dependencies should go through the relevant + `_prerender`, `_scripts`, `_styles`, `_resources` aliases generated by + `prerender_component`. + * Exception: Unit tests may directly depend on targets, provided they do + _not_ use any `prerender_*` rules as part of the test. +1. _Never_ depend on a `prerender_component` target directly. Always depend on + the alias of the specific part of the component you actually want to use. + * Exception: You may `bazel build` a `prerender_component` target directly + or have a `build_test` depend on it in order to verify that the + component is buildable. +1. Any direct dependency of a `prerender_component` target *must* be defined in + the same Bazel package and have private visibility. + * This is enforced at build time. + * Acts as a guardrail to make it less likely to run afoul of the above + rules. + ### Generating multiple pages We can generate multiple pages just as easily as the one. We just need to yield more files. Take this example where we render HTML files for a bunch of markdown posts in a blog. -```typescript +```tsx // my_blog/posts_prerender.tsx import * as fs from 'fs'; @@ -356,6 +499,7 @@ We can easily execute this at build time like so: ```python # my_blog/BUILD.bazel +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") load("@rules_prerender//:index.bzl", "prerender_pages", "web_resources_devserver") # Renders a page for every `posts/*.md` file. Also performs all the bundling and @@ -363,11 +507,17 @@ load("@rules_prerender//:index.bzl", "prerender_pages", "web_resources_devserver prerender_pages( name = "prerendered_posts", # Script to invoke the default export of to generate the page. - src = "posts_prerender.tsx", + entry_point = "./posts_prerender.js", + # Library which generates the entry point JavaScript. + prerender = ":prerender", +) + +ts_project( + name = "prerender", + srcs = ["posts_prerender.tsx"], # Include all the markdown files at runtime in runfiles. data = glob(["posts/*.md"]), - # Plain TypeScript dependencies used by `posts_prerender.ts`. - lib_deps = [ + deps = [ "//:node_modules/@types/markdown-it", "//:node_modules/@types/node", "//:node_modules/rules_prerender",