diff --git a/examples/javascript/BUILD.bazel b/examples/javascript/BUILD.bazel new file mode 100644 index 00000000..475fafb7 --- /dev/null +++ b/examples/javascript/BUILD.bazel @@ -0,0 +1,34 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("//:index.bzl", "prerender_pages", "web_resources_devserver") +load("//tools:jasmine.bzl", "jasmine_node_test") + +prerender_pages( + name = "page", + src = "page.js", + bundle_css = False, # Optimization: No CSS styling on this page. + lib_deps = ["//packages/rules_prerender"], + deps = ["//examples/javascript/component"], +) + +web_resources_devserver( + name = "devserver", + resources = ":page", +) + +ts_library( + name = "test_lib", + srcs = ["test.ts"], + testonly = True, + deps = [ + "//common:runfiles", + "//common/testing:devserver", + "//common/testing:puppeteer", + "@npm//@types/jasmine", + ], +) + +jasmine_node_test( + name = "test", + data = [":devserver"], + deps = [":test_lib"], +) diff --git a/examples/javascript/README.md b/examples/javascript/README.md new file mode 100644 index 00000000..1b16adcc --- /dev/null +++ b/examples/javascript/README.md @@ -0,0 +1,8 @@ +# JavaScript + +An example which uses JavaScript source files (as opposed to TypeScript) to +render a component and execute client-side scripts. + +This example uses CommonJS to load dependencies, but it should be possible to +use ESM with the right Node version and configuration. Doing so is not fully +tested or supported however. diff --git a/examples/javascript/component/BUILD.bazel b/examples/javascript/component/BUILD.bazel new file mode 100644 index 00000000..9752a316 --- /dev/null +++ b/examples/javascript/component/BUILD.bazel @@ -0,0 +1,31 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//:index.bzl", "prerender_component") + +prerender_component( + name = "component", + srcs = ["component.js"], + lib_deps = [ + ":prerender_lib", + "//packages/rules_prerender", + ], + scripts = [ + ":component_script", + ":component_script_unused", + ], + visibility = ["//examples/javascript:__pkg__"], +) + +js_library( + name = "prerender_lib", + srcs = ["prerender_lib.js"], +) + +js_library( + name = "component_script", + srcs = ["component_script.js"], +) + +js_library( + name = "component_script_unused", + srcs = ["component_script_unused.js"], +) diff --git a/examples/javascript/component/component.js b/examples/javascript/component/component.js new file mode 100644 index 00000000..ef8afbb0 --- /dev/null +++ b/examples/javascript/component/component.js @@ -0,0 +1,26 @@ +const { includeScript } = require('rules_prerender'); +const { content } = require('rules_prerender/examples/javascript/component/prerender_lib'); + +/** Renders an example component with a script. */ +function renderComponent() { + return ` +
${content}
+
+ This text to be overwritten by client-side JavaScript. +
+${includeScript('rules_prerender/examples/javascript/component/component_script')} + `.trim(); +} + +/** + * Renders an example component with a script. This is never called and should + * not be seen in the output. Used to validate tree-shaking of JS scripts. + */ +function renderUnused() { + return ` +
ERROR: Should never be rendered.
+${includeScript('rules_prerender/examples/javascript/component/component_script_unused')} + `.trim(); +} + +module.exports = { renderComponent, renderUnused }; diff --git a/examples/javascript/component/component_script.js b/examples/javascript/component/component_script.js new file mode 100644 index 00000000..d59efae2 --- /dev/null +++ b/examples/javascript/component/component_script.js @@ -0,0 +1,4 @@ +/** @fileoverview Replaces a DOM element at load time. */ + +const el = document.getElementById('component-replace'); +el.innerText = 'This text rendered by component JavaScript!'; diff --git a/examples/javascript/component/component_script_unused.js b/examples/javascript/component/component_script_unused.js new file mode 100644 index 00000000..8ccd66f2 --- /dev/null +++ b/examples/javascript/component/component_script_unused.js @@ -0,0 +1,8 @@ +/** + * @fileoverview This file should be tree-shaken because the function which + * prerenders this is never called at build time. + */ + +// If this is loaded, that's an error, so delete the whole document to fail any +// test which asserts on it. +document.body.innerText = 'Error: Unused script was not tree-shaken.'; diff --git a/examples/javascript/component/prerender_lib.js b/examples/javascript/component/prerender_lib.js new file mode 100644 index 00000000..d58023e0 --- /dev/null +++ b/examples/javascript/component/prerender_lib.js @@ -0,0 +1,5 @@ +/** @fileoverview JS file to be used as a dependency of a prerender library. */ + +const content = 'Hello from a JS component!'; + +module.exports = { content }; diff --git a/examples/javascript/page.js b/examples/javascript/page.js new file mode 100644 index 00000000..e8f7e7e1 --- /dev/null +++ b/examples/javascript/page.js @@ -0,0 +1,19 @@ +const { PrerenderResource } = require('rules_prerender'); +const { renderComponent } = require('rules_prerender/examples/javascript/component/component'); + +/* Renders the page. */ +module.exports = function*() { + yield PrerenderResource.of('/index.html', ` + + + + + JavaScript + + +

JavaScript

+ ${renderComponent()} + + + `.trim()); +} diff --git a/examples/javascript/test.ts b/examples/javascript/test.ts new file mode 100644 index 00000000..e46a4ebe --- /dev/null +++ b/examples/javascript/test.ts @@ -0,0 +1,32 @@ +import 'jasmine'; + +import { resolveRunfile } from 'rules_prerender/common/runfiles'; +import { useDevserver } from 'rules_prerender/common/testing/devserver'; +import { useBrowser, usePage, puppeteerTestTimeout } from 'rules_prerender/common/testing/puppeteer'; + +const devserverBinary = resolveRunfile( + 'rules_prerender/examples/javascript/devserver'); + +describe('JavaScript', () => { + const server = useDevserver(devserverBinary); + const browser = useBrowser(); + const page = usePage(browser); + + it('renders component', async () => { + await page.get().goto( + `http://${server.get().host}:${server.get().port}`, + { waitUntil: 'load' }, + ); + + const title = await page.get().title(); + expect(title).toBe('JavaScript'); + + const prerendered = + await page.get().$eval('#component', (el) => el.textContent); + expect(prerendered).toBe('Hello from a JS component!'); + + const replaced = await page.get().$eval( + '#component-replace', (el) => el.textContent); + expect(replaced).toBe('This text rendered by component JavaScript!'); + }, puppeteerTestTimeout); +});