diff --git a/.vscode/settings.json b/.vscode/settings.json
index 276844e8..574d30cf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -21,6 +21,7 @@
"semver",
"testonly",
"transpiled",
+ "tsjs",
"unmocked",
"unproxied"
],
diff --git a/examples/tsjs/BUILD.bazel b/examples/tsjs/BUILD.bazel
new file mode 100644
index 00000000..07c939d1
--- /dev/null
+++ b/examples/tsjs/BUILD.bazel
@@ -0,0 +1,67 @@
+load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
+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.ts",
+ scripts = [
+ ":ts_parent_script",
+ ":js_parent_script",
+ ],
+ bundle_css = False, # Optimization: No CSS styling on this page.
+ lib_deps = ["//packages/rules_prerender"],
+ deps = [
+ "//examples/tsjs/js_parent",
+ "//examples/tsjs/ts_parent",
+ ],
+)
+
+ts_library(
+ name = "ts_parent_script",
+ srcs = ["ts_parent_script.ts"],
+ deps = [":js_child_script"],
+)
+
+js_library(
+ name = "js_child_script",
+ srcs = [
+ "js_child_script.js",
+ "js_child_script.d.ts",
+ ],
+)
+
+js_library(
+ name = "js_parent_script",
+ srcs = ["js_parent_script.js"],
+ deps = [":ts_child_script"],
+)
+
+ts_library(
+ name = "ts_child_script",
+ srcs = ["ts_child_script.ts"],
+)
+
+web_resources_devserver(
+ name = "devserver",
+ resources = ":page",
+)
+
+ts_library(
+ name = "test_lib",
+ srcs = ["test.ts"],
+ data = [":devserver"],
+ testonly = True,
+ deps = [
+ "//common:runfiles",
+ "//common/testing:devserver",
+ "//common/testing:puppeteer",
+ "@npm//@types/jasmine",
+ ],
+)
+
+jasmine_node_test(
+ name = "test",
+ deps = [":test_lib"],
+)
diff --git a/examples/tsjs/README.md b/examples/tsjs/README.md
new file mode 100644
index 00000000..b2d13d57
--- /dev/null
+++ b/examples/tsjs/README.md
@@ -0,0 +1,13 @@
+# TS/JS
+
+An example which verifies interoperability between TypeScript and JavaScript
+`prerender_component()` targets. This includes a TypeScript component which
+depends on a JavaScript component. It also includes a JavaScript component which
+depends on a TypeScript component.
+
+JavaScript depending on TypeScript works as you would expect. TypeScript
+depending on JavaScript also works but has the additional requirement that the
+JavaScript code must include a `.d.ts` file to provide typings for it.
+
+JavaScript for prerendering needs to be written in CommonJS format, while
+client-side JavaScript should be written ESM format.
diff --git a/examples/tsjs/js_child/BUILD.bazel b/examples/tsjs/js_child/BUILD.bazel
new file mode 100644
index 00000000..07cb3c04
--- /dev/null
+++ b/examples/tsjs/js_child/BUILD.bazel
@@ -0,0 +1,10 @@
+load("//:index.bzl", "prerender_component")
+
+prerender_component(
+ name = "js_child",
+ srcs = [
+ "js_child.js",
+ "js_child.d.ts",
+ ],
+ visibility = ["//examples/tsjs:__subpackages__"],
+)
diff --git a/examples/tsjs/js_child/js_child.d.ts b/examples/tsjs/js_child/js_child.d.ts
new file mode 100644
index 00000000..5d17a8cb
--- /dev/null
+++ b/examples/tsjs/js_child/js_child.d.ts
@@ -0,0 +1 @@
+export function renderJsChild(): string;
diff --git a/examples/tsjs/js_child/js_child.js b/examples/tsjs/js_child/js_child.js
new file mode 100644
index 00000000..4650d0bc
--- /dev/null
+++ b/examples/tsjs/js_child/js_child.js
@@ -0,0 +1,9 @@
+function renderJsChild() {
+ return `
+
+ JS child
+
+ `.trim();
+}
+
+module.exports = { renderJsChild };
diff --git a/examples/tsjs/js_child_script.d.ts b/examples/tsjs/js_child_script.d.ts
new file mode 100644
index 00000000..963ad7fc
--- /dev/null
+++ b/examples/tsjs/js_child_script.d.ts
@@ -0,0 +1 @@
+export const target: string;
diff --git a/examples/tsjs/js_child_script.js b/examples/tsjs/js_child_script.js
new file mode 100644
index 00000000..ad1a74ca
--- /dev/null
+++ b/examples/tsjs/js_child_script.js
@@ -0,0 +1 @@
+export const target = 'World';
diff --git a/examples/tsjs/js_parent/BUILD.bazel b/examples/tsjs/js_parent/BUILD.bazel
new file mode 100644
index 00000000..b0cbb8ef
--- /dev/null
+++ b/examples/tsjs/js_parent/BUILD.bazel
@@ -0,0 +1,11 @@
+load("//:index.bzl", "prerender_component")
+
+prerender_component(
+ name = "js_parent",
+ srcs = [
+ "js_parent.js",
+ "js_parent.d.ts",
+ ],
+ visibility = ["//examples/tsjs:__subpackages__"],
+ deps = ["//examples/tsjs/ts_child"],
+)
diff --git a/examples/tsjs/js_parent/js_parent.d.ts b/examples/tsjs/js_parent/js_parent.d.ts
new file mode 100644
index 00000000..be1eefed
--- /dev/null
+++ b/examples/tsjs/js_parent/js_parent.d.ts
@@ -0,0 +1 @@
+export function renderJsParent(): string;
diff --git a/examples/tsjs/js_parent/js_parent.js b/examples/tsjs/js_parent/js_parent.js
new file mode 100644
index 00000000..5d63dace
--- /dev/null
+++ b/examples/tsjs/js_parent/js_parent.js
@@ -0,0 +1,12 @@
+const { renderTsChild } = require('../ts_child/ts_child');
+
+function renderJsParent() {
+ return `
+
+ JS parent
+ ${renderTsChild()}
+
+ `.trim();
+}
+
+module.exports = { renderJsParent };
diff --git a/examples/tsjs/js_parent_script.js b/examples/tsjs/js_parent_script.js
new file mode 100644
index 00000000..667f602f
--- /dev/null
+++ b/examples/tsjs/js_parent_script.js
@@ -0,0 +1,4 @@
+import { target } from './ts_child_script';
+
+const replace = document.getElementById('replace-js-parent-script');
+if (replace) replace.innerText = `Hello, ${target}!`;
diff --git a/examples/tsjs/page.ts b/examples/tsjs/page.ts
new file mode 100644
index 00000000..4c1a7067
--- /dev/null
+++ b/examples/tsjs/page.ts
@@ -0,0 +1,92 @@
+import { PrerenderResource, includeScript } from 'rules_prerender';
+import { renderJsParent } from 'rules_prerender/examples/tsjs/js_parent/js_parent';
+import { renderTsParent } from 'rules_prerender/examples/tsjs/ts_parent/ts_parent';
+
+export default function*(): Generator {
+ // Index page to list the various test cases.
+ yield PrerenderResource.of('/index.html', `
+
+
+
+ TS/JS
+
+
+
+
+
+
+ `.trim());
+
+ // Test case for JS depending on TS.
+ yield PrerenderResource.of('/js-depends-on-ts.html', `
+
+
+
+ JS depends on TS
+
+
+
+ ${renderJsParent()}
+
+
+ `.trim());
+
+ // Test case for TS depending on JS.
+ yield PrerenderResource.of('/ts-depends-on-js.html', `
+
+
+
+ TS depends on JS
+
+
+
+ ${renderTsParent()}
+
+
+ `.trim());
+
+ // Test case for client-side TS depending on JS.
+ yield PrerenderResource.of('/ts-script-depends-on-js-script.html', `
+
+
+
+ TS script depends on JS script
+
+
+
+
+ Text to be replaced by client-side JS.
+
+
+ ${includeScript('rules_prerender/examples/tsjs/ts_parent_script')}
+
+
+ `.trim());
+
+ // Test case for client-side JS depending on TS.
+ yield PrerenderResource.of('/js-script-depends-on-ts-script.html', `
+
+
+
+
JS script depends on TS script
+
+
+
+
+ Text to be replaced by client-side JS.
+
+
+ ${includeScript('rules_prerender/examples/tsjs/js_parent_script')}
+
+
+ `.trim());
+}
diff --git a/examples/tsjs/test.ts b/examples/tsjs/test.ts
new file mode 100644
index 00000000..f683354e
--- /dev/null
+++ b/examples/tsjs/test.ts
@@ -0,0 +1,78 @@
+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/tsjs/devserver');
+
+describe('TS/JS', () => {
+ const server = useDevserver(devserverBinary);
+ const browser = useBrowser();
+ const page = usePage(browser);
+
+ it('renders TypeScript depending on JavaScript', async () => {
+ await page.get().goto(
+ `http://${server.get().host}:${server.get().port}/ts-depends-on-js.html`,
+ { waitUntil: 'load' },
+ );
+
+ const title = await page.get().title();
+ expect(title).toBe('TS depends on JS');
+
+ const tsParent = await page.get().$eval(
+ '.ts-parent > span', (el) => el.textContent);
+ expect(tsParent).toBe('TS parent');
+
+ const jsChild = await page.get().$eval(
+ '.js-child', (el) => el.textContent?.trim());
+ expect(jsChild).toBe('JS child');
+ }, puppeteerTestTimeout);
+
+ it('renders JavaScript depending on TypeScript', async () => {
+ await page.get().goto(
+ `http://${server.get().host}:${server.get().port}/js-depends-on-ts.html`,
+ { waitUntil: 'load' },
+ );
+
+ const title = await page.get().title();
+ expect(title).toBe('JS depends on TS');
+
+ const jsParent = await page.get().$eval(
+ '.js-parent > span', (el) => el.textContent);
+ expect(jsParent).toBe('JS parent');
+
+ const tsChild = await page.get().$eval(
+ '.ts-child', (el) => el.textContent?.trim());
+ expect(tsChild).toBe('TS child');
+ }, puppeteerTestTimeout);
+
+ it('renders client-side TypeScript depending on JavaScript', async () => {
+ await page.get().goto(
+ `http://${server.get().host}:${server.get().port}/ts-script-depends-on-js-script.html`,
+ { waitUntil: 'load' },
+ );
+
+ const title = await page.get().title();
+ expect(title).toBe('TS script depends on JS script');
+
+ const replaced = await page.get().$eval(
+ '#replace-ts-parent-script', (el) => el.textContent);
+ expect(replaced).toBe('Hello, World!');
+ }, puppeteerTestTimeout);
+
+ it('renders client-side JavaScript depending on TypeScript', async () => {
+ await page.get().goto(
+ `http://${server.get().host}:${server.get().port}/js-script-depends-on-ts-script.html`,
+ { waitUntil: 'load' },
+ );
+
+ const title = await page.get().title();
+ expect(title).toBe('JS script depends on TS script');
+
+ const replaced = await page.get().$eval(
+ '#replace-js-parent-script', (el) => el.textContent);
+ expect(replaced).toBe('Hello, World!');
+ }, puppeteerTestTimeout);
+});
diff --git a/examples/tsjs/ts_child/BUILD.bazel b/examples/tsjs/ts_child/BUILD.bazel
new file mode 100644
index 00000000..f58a78ce
--- /dev/null
+++ b/examples/tsjs/ts_child/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//:index.bzl", "prerender_component")
+
+prerender_component(
+ name = "ts_child",
+ srcs = ["ts_child.ts"],
+ visibility = ["//examples/tsjs:__subpackages__"],
+)
diff --git a/examples/tsjs/ts_child/ts_child.ts b/examples/tsjs/ts_child/ts_child.ts
new file mode 100644
index 00000000..3db5eb96
--- /dev/null
+++ b/examples/tsjs/ts_child/ts_child.ts
@@ -0,0 +1,7 @@
+export function renderTsChild(): string {
+ return `
+
+ TS child
+
+ `.trim();
+}
diff --git a/examples/tsjs/ts_child_script.ts b/examples/tsjs/ts_child_script.ts
new file mode 100644
index 00000000..9e1cce3b
--- /dev/null
+++ b/examples/tsjs/ts_child_script.ts
@@ -0,0 +1 @@
+export const target: string = 'World';
diff --git a/examples/tsjs/ts_parent/BUILD.bazel b/examples/tsjs/ts_parent/BUILD.bazel
new file mode 100644
index 00000000..7bc36bb8
--- /dev/null
+++ b/examples/tsjs/ts_parent/BUILD.bazel
@@ -0,0 +1,8 @@
+load("//:index.bzl", "prerender_component")
+
+prerender_component(
+ name = "ts_parent",
+ srcs = ["ts_parent.ts"],
+ visibility = ["//examples/tsjs:__subpackages__"],
+ deps = ["//examples/tsjs/js_child"],
+)
diff --git a/examples/tsjs/ts_parent/ts_parent.ts b/examples/tsjs/ts_parent/ts_parent.ts
new file mode 100644
index 00000000..3d65c080
--- /dev/null
+++ b/examples/tsjs/ts_parent/ts_parent.ts
@@ -0,0 +1,10 @@
+import { renderJsChild } from '../js_child/js_child';
+
+export function renderTsParent(): string {
+ return `
+
+ TS parent
+ ${renderJsChild()}
+
+ `.trim();
+}
diff --git a/examples/tsjs/ts_parent_script.ts b/examples/tsjs/ts_parent_script.ts
new file mode 100644
index 00000000..4ed031a1
--- /dev/null
+++ b/examples/tsjs/ts_parent_script.ts
@@ -0,0 +1,4 @@
+import { target } from './js_child_script';
+
+const replace = document.getElementById('replace-ts-parent-script');
+if (replace) replace.innerText = `Hello, ${target}!`;