{"id":15677850,"url":"https://github.com/dgp1130/rules_prerender","last_synced_at":"2025-04-12T14:02:44.210Z","repository":{"id":41558824,"uuid":"320174753","full_name":"dgp1130/rules_prerender","owner":"dgp1130","description":"A Bazel rule set for prerending HTML pages.","archived":false,"fork":false,"pushed_at":"2025-03-15T21:47:57.000Z","size":3583,"stargazers_count":14,"open_issues_count":37,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-26T08:37:27.957Z","etag":null,"topics":["bazel","nodejs","static-site-generator"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dgp1130.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-12-10T05:58:23.000Z","updated_at":"2024-09-09T18:14:40.000Z","dependencies_parsed_at":"2022-07-17T06:00:47.189Z","dependency_job_id":"7f6d3315-953d-46ed-9e67-70c44a570da8","html_url":"https://github.com/dgp1130/rules_prerender","commit_stats":{"total_commits":663,"total_committers":1,"mean_commits":663.0,"dds":0.0,"last_synced_commit":"63cb9a6e167a926492444ad365d73dabf88df6b7"},"previous_names":[],"tags_count":68,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgp1130%2Frules_prerender","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgp1130%2Frules_prerender/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgp1130%2Frules_prerender/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dgp1130%2Frules_prerender/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dgp1130","download_url":"https://codeload.github.com/dgp1130/rules_prerender/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246096688,"owners_count":20722993,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bazel","nodejs","static-site-generator"],"created_at":"2024-10-03T16:12:59.670Z","updated_at":"2025-03-28T20:31:05.972Z","avatar_url":"https://github.com/dgp1130.png","language":"TypeScript","readme":"# rules_prerender\n\nA Bazel rule set for prerendering HTML pages.\n\n![CI](https://github.com/dgp1130/rules_prerender/workflows/CI/badge.svg)\n\nNOTE: This project is currently **experimental**. Feel free to install it to try\nit out, give feedback, and suggest improvements! Just don't use it in production\nquite yet.\n\n## Installation\n\nStart with an [`@aspect_rules_js`](https://github.com/aspect-build/rules_js/)\nproject.\n\nThen add a workspace dependency on `@rules_prerender`. See the\n[releases](https://github.com/dgp1130/rules_prerender/releases/) page for the\nlatest release and copy the snippet into your `WORKSPACE` file. Then install the\n`rules_prerender` NPM package. We'll also want `@rules_prerender/preact` and\n[`preact`](https://www.npmjs.com/package/preact) itself to use JSX as a\ntemplating system.\n\n```bash\npnpm install rules_prerender @rules_prerender/preact preact --save-dev\n```\n\n### TypeScript\n\nOptionally, you likely want to configure TypeScript by installing it and\ncreating a `tsconfig.json` file.\n\n```bash\npnpm install typescript --save-dev\nnode_modules/.bin/tsc --init\n```\n\n### Declarative Shadow DOM\n\nAlso optional, but it is recommended to include the declarative shadow DOM\npackage as it is a key part of `@rules_prerender` components.\n\n```bash\npnpm install @rules_prerender/declarative-shadow-dom --save-dev\n```\n\nThen update your root `BUILD.bazel` file to include:\n\n```python\nload(\"@rules_prerender//:index.bzl\", \"link_prerender_component\")\n\nlink_prerender_component(\n    name = \"prerender_components/@rules_prerender/declarative_shadow_dom\",\n    package = \":node_modules/@rules_prerender/declarative_shadow_dom\",\n    visibility = [\"//visibility:public\"],\n)\n```\n\nWith that all done, you should be ready to use `rules_prerender`! See the next\nsection for how to use the API, or you can check out\n[some examples](/examples/site/) which shows most of the relevant features in\naction.\n\n## API\n\nThe exact API is not currently nailed down, but it is expected to look something\nlike the following.\n\nThere are two significant portions of the rule set. The first defines a\n\"component\": an HTML template and the associated JavaScript, CSS, and other web\nresources (images, fonts, JSON) required for to it to function.\n\n```python\n# my_component/BUILD.bazel\n\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\nload(\"@rules_prerender//:index.bzl\", \"prerender_component\", \"web_resources\")\n\n# A \"library\" target encapsulating the entire component.\nprerender_component(\n    name = \"my_component\",\n    # The library which will prerender the HTML at build time in a Node process.\n    prerender = \":prerender\",\n    # Client-side JavaScript to be executed in the browser.\n    scripts = \":scripts\",\n    # Styles for the component.\n    styles = \":styles\",\n    # Other resources required by the component (images, fonts, static JSON, etc.).\n    resources = \":resources\",\n)\n\n# Compile the prerendering logic (can also be a `js_library`).\nts_project(\n    name = \"prerender\",\n    srcs = [\"my_component_prerender.tsx\"],\n    deps = [\n        # See \"Component composition\" to learn more about how to depend on\n        # another `prerender_component`.\n        \"//my_other_component:my_other_component_prerender\",\n        \"//:prerender_components/@rules_prerender/declarative_shadow_dom_prerender\",\n\n        # Regular dependencies.\n        \"//:node_modules/@rules_prerender/preact\",\n        \"//:node_modules/preact\",\n    ],\n)\n\n# Client-side scripts to be executed in the browser (can also be a `js_library`).\nts_project(\n    name = \"scripts\",\n    srcs = [\"my_component.mts\"],\n    declaration = True,\n    deps = [\"//some/other/package:ts_proj\"],\n)\n\n# Any styles needed by this component to render correctly.\ncss_library(\n    name = \"styles\",\n    srcs = [\"my_component.css\"],\n    deps = [\"//some/other/package:css_lib\"],\n)\n\n# Other resources required for this component to function at the URL paths they\n# are expected to be hosted at.\nweb_resources(\n    name = \"resources\",\n    entries = {\n        \"/images/foo.png\": \":foo.png\",\n        \"/fonts/roboto.woff\": \"//fonts:roboto\",\n    },\n)\n```\n\n```tsx\n// my_component/my_component_prerender.tsx\n\nimport { Templates } from '@rules_prerender/declarative_shadow_dom/preact.mjs';\nimport { includeScript, inlineStyle } from '@rules_prerender/preact';\nimport { VNode } from 'preact';\nimport { OtherComponent } from '../my_other_component/my_other_component_prerender.js';\n\n/** Render partial HTML with Preact. */\nexport function MyComponent({ name }: { name: string }): VNode {\n    return \u003cdiv\u003e\n        {/* Use declarative shadow DOM to isolate styles. If you're not familiar\n            with declarative shadow DOM, you don't have to use it. But if you\n            don't you'll need to manually namespace your styles or else styles\n            in different components could conflict with each other! */}\n        \u003cTemplate shadowrootmode=\"open\"\u003e\n            {/* Render some HTML. */}\n            \u003ch2 class=\"my-component-header\"\u003eHello, {name}\u003c/h2\u003e!\n            \u003cbutton id=\"show\"\u003eShow\u003c/button\u003e\n\n            {/* Use related web resources. */}\n            \u003cimg src=\"/images/foo.png\" /\u003e\n\n            {/* Compose other components from the light DOM. */}\n            \u003cslot\u003e\u003c/slot\u003e\n\n            {/* Inject the associated client-side JavaScript. */}\n            {includeScript('./my_component.mjs', import.meta)}\n\n            {/* Inline the associated styles, scoped to this shadow root. */}\n            {inlineStyle('./my_component.css', import.meta)}\n        \u003c/Template\u003e\n\n        {/* Light DOM content goes here, local styles are *not* applied to these\n            elements. */}\n        \u003cOtherComponent id=\"other\" name={name.reverse()} /\u003e\n    \u003c/div\u003e;\n}\n```\n\n```typescript\n// my_component/my_component.mts\n\nimport { showDialog } from '../some/other/package/show_dialog.mjs';\n\n// Register an event handler to show the other component. Could just as easily\n// use a framework like Angular, LitElement, React, or just define an\n// implementation for a custom element that was prerendered.\ndocument.getElementById('show').addEventListener('click', () =\u003e {\n    // Show the composed `other` component.\n    showDialog(document.getElementById('other'));\n});\n```\n\n```css\n/* my_component/my_component.css */\n\n/* @import dependencies resolved and bundled at build time. */\n@import '../some/other/package/styles.css';\n\n/* Styles for the component. */\n@font-face {\n    font-family: Roboto;\n    src: url(/fonts/roboto.woff); /* Use related web resources. */\n}\n\n.my-component-header {\n    color: red;\n    font-family: Roboto;\n}\n```\n\nThe second part of the rule set leverages such components to prerender an entire\nweb page.\n\n```tsx\n// my_page/my_page_prerender.tsx\n\nimport { PrerenderResource, renderToHtml } from '@rules_prerender/preact';\nimport { MyComponent } from '../my_component/my_component_prerender.js';\n\n// Renders HTML pages for the site at build-time.\n// If you aren't familiar with generators and the `yield` looks scary, you could\n// also write this as simply returning an `Array\u003cPrerenderResource\u003e`.\nexport default function* render(): Generator\u003cPrerenderResource, void, void\u003e {\n    // Generate an HTML page at `/my_page/index.html` with this content:\n    yield PrerenderResource.fromHtml('/my_page/index.html', renderToHtml(\n        \u003chtml\u003e\n            \u003chead\u003e\n                \u003ctitle\u003eMy Page\u003c/title\u003e\n                \u003cmeta charSet=\"utf8\" /\u003e\n            \u003c/head\u003e\n            \u003cbody\u003e\n                \u003cMyComponent name=\"World\" /\u003e\n            \u003c/body\u003e\n        \u003c/html\u003e\n    ));\n}\n```\n\n```python\n# my_page/BUILD.bazel\n\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\nload(\"@rules_prerender//:index.bzl\", \"prerender_pages\", \"web_resources_devserver\")\n\n# Renders the page, bundles JavaScript and CSS, injects the relevant\n# `\u003cscript /\u003e` and `\u003cstyle /\u003e` tags, and combines with all transitive resources\n# to create a directory with the following paths:\n#     /my_page/index.html - Final prerendered HTML page with CSS styles inlined.\n#     /my_page/index.js - All transitive client-side JS source files bundled\n#         into a single file.\n#     /images/foo.png - The image used in `my_component`.\n#     /fonts/roboto.woff - The Robot font used in `my_component`.\n#     ... - Possibly other resources from `my_other_component` and transitive\n#         dependencies.\nprerender_pages(\n    name = \"prerendered_page\",\n    # Import specifier for the JavaScript output from `:prerender` which\n    # generates the page.\n    entry_point = \"./my_page_prerender.js\",\n    # Depend on the library containing `my_page_prerender.js`.\n    prerender = \":prerender\",\n)\n\nts_project(\n    name = \"prerender\",\n    srcs = [\"my_page_prerender.tsx\"],\n    deps = [\n        # See \"Component composition\" to learn more about how to depend on\n        # another `prerender_component`.\n        \"//my_component:my_component_prerender\",\n\n        # Other dependencies.\n        \"//:node_modules/@rules_prerender/preact\",\n        \"//:node_modules/preact\",\n    ],\n)\n\n# Small dev server to test out this page. `bazel run` / `ibazel run` this target\n# to check out the page at `/my_page/index.html`.\nweb_resources_devserver(\n    name = \"devserver\",\n    resources = \":prerendered_page\",\n)\n```\n\nThe `//my_page:prerendered_page` target generates a directory which contains its\nHTML, JavaScript, CSS, and other resources from all the transitively included\ncomponents at their expected paths.\n\nMultiple `prerender_pages()` directories can then be composed together into a\nsingle `web_resources()` target which contains a final directory of everything\nmerged together, representing an entire prerendered web site.\n\nThis final directory can be served with a simple devserver for local builds or\nuploaded directly to a CDN for production deployments.\n\n```python\n# my_site/BUILD.bazel\n\nload(\"@rules_prerender//:index.bzl\", \"web_resources\", \"web_resources_devserver\")\n\n# Combines all the prerendered resources into a single directory, composing a\n# site from a bunch of `prerender_pages()` and `web_resources()` rules. Just\n# upload this to a CDN for production builds!\nweb_resources(\n    name = \"my_site\",\n    deps = [\n        \"//my_page:prerendered_page\",\n        \"//another:page\",\n        \"//blog:posts\",\n    ],\n)\n\n# A simple devserver implementation to serve the entire site.\nweb_resources_devserver(\n    name = \"devserver\",\n    resources = \":site\",\n)\n```\n\nWith this model, a user could do `ibazel run //my_site:devserver` to prerender\nthe entire application composed from various self-contained components in a fast\nand incremental fashion. They could also just run `bazel build //my_site` to\ngenerate the application as a directory and upload it to a CDN for production\ndeployments. They could even make a separate `bazel run //my_site:deploy` target\nwhich performs the upload and run it from CI for easy deployments!\n\n### Component composition\n\nThe `prerender_component` target generates aliases to the targets passed in as\ninputs. Consider the following example:\n\n```python\nload(\"@rules_prerender//:index.bzl\", \"prerender_component\")\n\nprerender_component(\n    name = \"component\",\n    prerender = \":my_prerender_lib\",\n    scripts = \":my_scripts_lib\",\n    styles = \":my_styles_lib\",\n    resources = \":my_resources_lib\",\n)\n```\n\nThis will generate the following aliases:\n\n*   `:component_prerender` -\u003e `:my_prerender_lib`\n*   `:component_scripts` -\u003e `:my_scripts_lib`\n*   `:component_styles` -\u003e `:my_styles_lib`\n*   `:component_resources` -\u003e `:my_resources_lib`\n\nIf you want to use any part of a component, you can use it directly rather than\ndepending on `:component`. However, you _must_ depend on that part through one\nof the above aliases.\n\nFor example, consider the following component:\n\n```tsx\n// my_component/prerender.mts\n\nimport { VNode } from 'preact';\n\n/** Render partial HTML with Preact. */\nexport function MyComponent({ name }: { name: string }): VNode {\n    return \u003cdiv\u003eHello, {name}!\u003c/div\u003e\n}\n```\n\nWith the following `BUILD.bazel` file:\n\n```python\n# my_component/BUILD.bazel\n\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\nload(\"@rules_prerender//:index.bzl\", \"prerender_component\")\n\nprerender_component(\n    name = \"my_component\",\n    prerender = \":prerender_lib\",\n    # ...\n)\n\nts_project(\n    name = \"prerender_lib\",\n    srcs = [\"prerender.mts\"],\n)\n```\n\nTo use this, you can import `MyComponent` directly like you would any other\nfunction.\n\n```typescript\n// my_other_component/prerender.mts\n\nimport { MyComponent } from '../my_component/prerender.js';\n\n// ...\n```\n\nHowever instead of depending on `//my_component:prerender_lib`, depend on\n`//my_component:my_component_prerender`.\n\n```python\n# my_other_component/BUILD.bazel\n\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\nload(\"@rules_prerender//:index.bzl\", \"prerender_component\")\n\n# Does not reference `//my_component` at all.\nprerender_component(\n    name = \"my_other_component\",\n    prerender = \":prerender_lib\",\n    # ...\n)\n\nts_project(\n    name = \"prerender_lib\",\n    srcs = [\"prerender.mts\"],\n    # IMPORTANT: Depend on `:my_component_prerender` instead of `:prerender_lib`.\n    deps = [\"//my_component:my_component_prerender\"],\n)\n```\n\nWhile this looks like just an `alias`, it is\n[actually load bearing](/docs/architecture/prerender_component.md) and\n_required_.\n\nThe same requirement to use the aliases applies to client-side JavaScript\n(`_scripts`), CSS styles (`_styles`), and generated resources (`_resources`).\n\n### `prerender_component` rules\n\nAs indicated by [component composition](#component-composition), the\n`prerender_component` macro is a bit unique compared to most Bazel macros/rules\nand has a few special rules for how it is used.\n\n1.  Any direct dependency of a `prerender_component` target should _only_ be\n    used by that `prerender_component`.\n1.  Any additional desired dependencies should go through the relevant\n    `_prerender`, `_scripts`, `_styles`, `_resources` aliases generated by\n    `prerender_component`.\n    *   Exception: Unit tests may directly depend on targets, provided they do\n        _not_ use any `prerender_*` rules as part of the test.\n1.  _Never_ depend on a `prerender_component` target directly. Always depend on\n    the alias of the specific part of the component you actually want to use.\n    *   Exception: You may `bazel build` a `prerender_component` target directly\n        or have a `build_test` depend on it in order to verify that the\n        component is buildable.\n1.  Any direct dependency of a `prerender_component` target *must* be defined in\n    the same Bazel package and have private visibility.\n    *   This is enforced at build time.\n    *   Acts as a guardrail to make it less likely to run afoul of the above\n        rules.\n\nA general best practice is to give every `prerender_component` target its own\ndirectory and Bazel package. Leave everything private visibility except for the\n`prerender_component` target itself. This will set visibility for the alias\ntargets as well. Doing so makes it impossible to accidentally forget to use the\ncomponent aliases, since doing so would be a visibility error. This pattern\nhelps you naturally follow the above rules without even thinking about it.\n\n### Generating multiple pages\n\nWe can generate multiple pages just as easily as the one. We just need to yield\nmore files. Take this example where we render HTML files for a bunch of markdown\nposts in a blog.\n\n```tsx\n// my_blog/posts_prerender.tsx\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { PrerenderResource, renderToHtml } from '@rules_prerender/preact';\nimport * as md from 'markdown-it';\n\nexport default async function* render():\n        AsyncGenerator\u003cPrerenderResource, void, void\u003e {\n    // List all files in the `posts/` directory.\n    const postsDir = `${process.env['RUNFILES']}/wksp/my_blog/posts`;\n    const posts = await fs.readdir(postsDir, { withFileTypes: true });\n\n    for (const post of posts) {\n        // Read the post markdown, convert it to HTML, and then emit the file to\n        // `rules_prerender` which will write it at\n        // `/post/${post_file_name_with_html_extension}`.\n        const postMarkdown =\n            await fs.readFile(path.join(postsDir, post), 'utf8');\n        const postHtml = md.render(postMarkdown);\n        const postBaseName = post.split('.').slice(0, -1).join('.');\n        const htmlName = `${postBaseName}.html`;\n        yield PrerenderResource.fromHtml(`/posts/${htmlName}`, renderToHtml(\n            \u003chtml\u003e\n                \u003chead\u003e\n                    \u003ctitle\u003ePost {postBaseName}\u003c/title\u003e\n                    \u003cmeta charSet=\"utf8\" /\u003e\n                \u003c/head\u003e\n                \u003cbody\u003e\n                    \u003carticle dangerouslySetInnerHTML={{ __html: postHtml }} /\u003e\n                \u003c/body\u003e\n            \u003c/html\u003e\n        ));\n    }\n}\n```\n\nWe can easily execute this at build time like so:\n\n```python\n# my_blog/BUILD.bazel\n\nload(\"@aspect_rules_ts//ts:defs.bzl\", \"ts_project\")\nload(\"@rules_prerender//:index.bzl\", \"prerender_pages\", \"web_resources_devserver\")\n\n# Renders a page for every `posts/*.md` file. Also performs all the bundling and\n# merging of required JS, CSS, and other resources.\nprerender_pages(\n    name = \"prerendered_posts\",\n    # Script to invoke the default export of to generate the page.\n    entry_point = \"./posts_prerender.js\",\n    # Library which generates the entry point JavaScript.\n    prerender = \":prerender\",\n)\n\nts_project(\n    name = \"prerender\",\n    srcs = [\"posts_prerender.tsx\"],\n    # Include all the markdown files at runtime in runfiles.\n    data = glob([\"posts/*.md\"]),\n    deps = [\n        \"//:node_modules/@types/markdown-it\",\n        \"//:node_modules/@types/node\",\n        \"//:node_modules/rules_prerender\",\n        \"//:node_modules/markdown-it\",\n    ],\n)\n\n# Simple server to test out this page. `bazel run` / `ibazel run` this target to\n# check out the posts at `/posts/*.html`.\nweb_resources_devserver(\n    name = \"devserver\",\n    resources = \":prerendered_posts\",\n)\n```\n\nWith this, all markdown posts in the `posts/` directory will get generated into\nHTML files. Using this strategy, we can scale static-site generation for a large\nnumber of files with common generation patterns.\n\n### Debugging\n\nSince `prerender_pages()` and related rules invoke user code at build time,\ndebugging can get a little complicated. Add the following to your `.bazelrc`:\n\n```\n# `@rules_prerender` specific options.\nbuild --flag_alias=debug_prerender=@rules_prerender//tools/flags:debug_prerender\n```\n\nThen, you can use the `--debug_prerender` flag to specify a target which you\nwant to open a breakpoint on. Use it with:\n\n```shell\nbazel run //path/to/my:devserver --debug_prerender=//path/to/my:prerender_pages_target\n```\n\nYou should then see a log statement indicating that a target is being debugged\nand a hung execution on the `Prerendering` action.\n\n```\nDEBUG: /home/doug/Source/rules_prerender/packages/rules_prerender/prerender_resources.bzl:165:14: Debugging @//examples/minimal:page_page_annotated from //examples/minimal:page\nINFO: Analyzed target //examples/minimal:devserver (1 packages loaded, 5038 targets configured).\nINFO: Found 1 target...\n[290 / 294] Prerendering (@//examples/minimal:page_page_annotated); 8s linux-sandbox\n```\n\nThis target is hanging because it has `--inspect-brk` set on the Node invocation\nand is waiting for the debugger. Connect your preferred Node debugger and you\nshould be able to step through rendering.\n\nNote that the Bazel cache can get a little tricky here, as repeated runs may\nskip the target altogether. If so, make any arbitrary whitespace change to a the\nrendering code to invalidate the cache and rerun.\n\nThe target you actually `bazel run` or `bazel build` (`//path/to/my:devserver`\nabove) doesn't actually matter, as long as it includes\n`//path/to/my:prerender_pages_target` as a transitive dependency.\n\n### Custom Bundling\n\nThe previous example automatically bundled all the JavaScript and CSS for a\ngiven page. This is very simple and easy to use, but also somewhat limited. The\n`prerender_pages_unbundled()` rule provides unbundled JavaScript and CSS\nresources so a user can manually bundle them with whatever means they like.\n\nThere is also an `extract_single_resource()` rule, which pulls out a resource\nfrom a directory generated by a `prerender_*()` rule (assuming the directory\ncontains only one resource). This can be useful to post-process a prerendered\nresource with tools that expect a single file as input, rather than a directory.\n\n## Development\n\nTo get started, simply download / fork the repository and run:\n\n```shell\nbazel run @pnpm -- install --dir $PWD --frozen-lockfile\nbazel test //...\n```\n\nPrefer using `bazel run @pnpm -- ...` and\n`bazel run @nodejs_host//:node -- ...` over using `pnpm` and `node` directly so\nthey are strongly versioned with the repository. Alternatively, you can install\n[`nvm`](https://github.com/nvm-sh/nvm) and run `nvm use` to switch the `node`\nversion to the correct one for this repository.\n\nThere are `bazel` and `ibazel` scripts in `package.json` so you can run any\nBazel command with:\n\n```shell\nnpm run -s -- bazel # ...\n```\n\nOr, if you want to live-reload on changes:\n\n```shell\nnpm run -s -- ibazel # ...\n```\n\nAlternatively, you can run `npm install -g @bazel/bazelisk @bazel/ibazel` to get\na global install of `bazel` and `ibazel` on your `$PATH` and just use them\ndirectly instead of proxying through the NPM wrapper scripts. This repository\nhas a [`.bazelversion`](./.bazelversion) file used by `bazelisk` to manage and\ndownload the correct Bazel version for you and pass through all commands to it\n(not totally sure if it applies to `ibazel` though).\n\nYou can also use `npm run build` and `npm test` to build and test everything.\n`npm test` also tests external workspaces used as test cases which would\nnormally be skipped by `bazel test //...`.\n\n## Testing\n\nMost tests are run in [Jasmine](https://jasmine.github.io/) using\n`jasmine_node_test()`, a\n[slightly customized implementation of `@aspect_rules_jasmine`](/tools/jasmine/jasmine_node_test.bzl).\nThese tests run in a Node Jasmine environment with no available browser (unless\nthey depend on WebDriverIO). The test can be executed with a simple\n`bazel test //path/to/pkg:target`.\n\n### Debugging Tests\n\nTo debug these tests, simply add `--config debug`, which will opt in to\nadditional flags specifically for testing. Most notably, this includes\n`--inspect-brk` so Node will not begin executing until a debugger has connected.\nYou can use `chrome://inspect` or the \"Attach\" run configuration in VSCode to\nattach a debugger and start test execution.\n\n### Debugging WebDriver tests\n\nEnd-to-end tests using a real browser are done with WebDriver using\n[`jasmine_web_test_suite()`](./tools/jasmine/jasmine_web_test_suite.bzl).\n\nWhen executing WebDriver tests and using `--config debug`, the browser will open\nnon-headless, giving you the opportunity to visually inspect the page under test\nand debug it directly. This is done via an X server, so make sure the `$DISPLAY`\nvariable is set. For example, if debugging over SSH, you'll need to enable X11\nforwarding.\n\nWhen using [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/install-win10)\nmake sure you have also installed [WSLg](https://github.com/microsoft/wslg).\nThat will give you the X server implementation necessary to debug.\n\nThen running a `bazel test //path/to/pkg:target --config debug` for a WebDriver\ntest should open Chrome visually and give you an opportunity to debug and\ninspect the page.\n\n### Mocking\n\nMost model types are stored under [//common/models/...](/common/models/) and\ngenerally consist of interfaces rather than classes. This provides immutable,\npure-data structured types which work well with functional design patterns. They\nare also easy to assert in Jasmine with `expect().toEqual()`.\n\nThese models typically include a `_mock.ts` file which exposes `mock*()`\nfunctions. These provide simple helpers to generate a mock for a model using\ndefault values with override values as inputs. Using these mocks, a test can\nexplicitly specify only the properties of an object that it actually cares about\nand trust that the mock function will provide reasonable and semantically\naccurate defaults for all other values. For example:\n\n```typescript\n// Some model interface.\ninterface MyModel {\n    name: string;\n    path: string;\n}\n\n// Some real function.\nfunction getName(model: MyModel): string {\n    return model.name;\n}\n\n// A mock for the model.\nfunction mockModel(overrides: Partial\u003cMyModel\u003e = {}): MyModel {\n    return {\n        name: 'MockName',\n        // Default is semantically accurate, even if it is an arbitrary value.\n        path: 'some/mocked/path.txt',\n        // Allow caller to specify any given value.\n        ...overrides,\n    };\n}\n\n// Test of a real function.\nit('`getName()` returns the name', () =\u003e {\n    const model = mockModel({\n        name: 'Ollie',\n        // path uses the default value.\n    });\n\n    expect(getName(model)).toBe('Ollie');\n\n    // There are several benefits with this approach:\n    // 1.  `path` isn't used, so no need to specify it for the test, making the\n    //     test and its intent much clearer.\n    // 2.  If `path` is accidentally used for an important operation as part of\n    //     `getName()`, the test would almost certainly fail and the `path`\n    //     value can be explicitly specified as part of the test.\n    // 3.  Even if `path` is used as part of unimportant operations in\n    //     `getName()` (such as simply validating the type), it will not break\n    //     the test because the default value is semantically accurate.\n    // 4.  This isolates the test from unrelated changes to `MyModel`.\n    //     Introducing another property is not likely to break `getName()` and\n    //     would not require changes to the test to support.\n});\n```\n\nThis is a semi-experimental mocking strategy, so whether or not it is actually a\ngood idea is still to be determined.\n\n## VSCode Snippets\n\nThe repository includes a few custom snippets available if you use VSCode. Type\nthe given name and hit \u003ckbd\u003eTab\u003c/kbd\u003e to insert the snippet. Then type out the\ndesired value for various parameters using \u003ckbd\u003eTab\u003c/kbd\u003e and\n\u003ckbd\u003eShift\u003c/kbd\u003e+\u003ckbd\u003eTab\u003c/kbd\u003e to navigate between them. The snippet will take\ncare of making sure certain values match as expected.\n\n*   Typing `ts_proj` in a `BUILD.bazel` file with a filename will generate a\n    `ts_project()` rule for that file, a rule for its test file, and a\n    `jasmine_node_test()` rule. Useful when creating a new file to auto-generate\n    its default `BUILD` rules.\n*   Typing `jas` in a TypeScript file will generate a base Jasmine setup with\n    imports and an initial test with a `TODO`.\n*   Typing `desc` in a TypeScript file will generate a Jasmine test suite,\n    moving the cursor exactly where you want it to go.\n*   Typing `it` in a TypeScript file will generate a Jasmine test, moving the\n    cursor exactly where you want it to go. It will generate an `async` test by\n    default, which you can either skip over with \u003ckbd\u003eTab\u003c/kbd\u003e to accept, or\n    delete with \u003ckbd\u003eBackspace\u003c/kbd\u003e (and then move on with \u003ckbd\u003eTab\u003c/kbd\u003e) to\n    make synchronous.\n\n### Releasing\n\nTo actually publish a release to NPM, follow these steps:\n\n1.  Go to the\n    [Publish workflow](https://github.com/dgp1130/rules_prerender/actions?query=workflow%3APublish)\n    and click `Run workflow`.\n    *   Make sure to fill out all the requested information.\n    *   This will install the package, execute all tests, and then publish as\n        the given semver to NPM.\n    *   It will also tag the commit with `releases/${semver}` and push it back\n        to the repository.\n    *   Finally, it will create a *draft* GitHub release for that tag with a\n        link to NPM for this particular version.\n1.  Once the workflow is complete, go to\n    [releases](https://github.com/dgp1130/rules_prerender/releases) to update\n    the draft and add a changelog or other relevant information before\n    publishing.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgp1130%2Frules_prerender","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdgp1130%2Frules_prerender","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdgp1130%2Frules_prerender/lists"}