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);
+});