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}!`;