Skip to content

Commit

Permalink
Adds <Template /> to `@rules_prerender/declarative_shadow_dom/preac…
Browse files Browse the repository at this point in the history
…t.mjs`.

This is a replacement for the `<Template />` element in `@rules_prerender/preact` but with the notable difference that it automatically includes the declarative shadow DOM polyfill when `shadowrootmode` is set. This removes the need to remember to do this for every component.

This only works because scripts are loaded in the same order they are injected into the HTML (when they don't import each other). As a result, as long as the DSD polyfill is included first, it will be executed first. `<Template />` enforces that a little better than users can, but ultimately we are relying on that ordering to do the right thing. I'm not sure happy with that, but I think it is the nature of polyfills to a certain extent, especially when the dependency on DSD is the prerendered HTML, not the client-side JS where an `import` would solve this problem. For now, I think this is the best we can do.

`<Template />` from `@rules_prerender/preact` still exists for now, but will be removed after all internal usages of it are migrated off.
  • Loading branch information
dgp1130 committed Sep 4, 2023
1 parent d9c9975 commit dbc79e7
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 1 deletion.
17 changes: 17 additions & 0 deletions packages/declarative_shadow_dom/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ ts_project(
],
)

ts_project(
name = "preact_test_lib",
srcs = ["preact_test.mts"],
deps = [
":prerender",
"//common/models:prerender_annotation",
"//:node_modules/@types/jasmine",
"//:node_modules/preact-render-to-string",
"//:node_modules/node-html-parser",
],
)

jasmine_node_test(
name = "preact_test",
deps = [":preact_test_lib"],
)

ts_project(
name = "declarative_shadow_dom_test_lib",
srcs = ["declarative_shadow_dom_test.mts"],
Expand Down
4 changes: 4 additions & 0 deletions packages/declarative_shadow_dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
"main": "./declarative_shadow_dom.mjs",
"peerDependencies": {
"@rules_prerender/preact": "^0.0.0-PLACEHOLDER",
"preact": "^10.13.1",
"rules_prerender": "^0.0.0-PLACEHOLDER"
},
"peerDependenciesMeta": {
"@rules_prerender/preact": {
"optional": true
},
"preact": {
"optional": true
}
}
}
20 changes: 19 additions & 1 deletion packages/declarative_shadow_dom/preact.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { includeScript } from '@rules_prerender/preact';
import type { VNode } from 'preact';
import { type VNode, createElement } from 'preact';
import type { JSX } from 'preact/jsx-runtime';

interface TemplateAttrs extends JSX.HTMLAttributes<HTMLTemplateElement> {
shadowrootmode?: ShadowRootMode;
}

/** A component representing the native HTML `<template />` tag. */
export function Template({ children, shadowrootmode, ...attrs }: TemplateAttrs = {}): VNode {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore JSX types are weird AF.
return createElement('template', { shadowrootmode, ...attrs }, [
// NOTE: DSD polyfill *must* come before children so the bundler loads
// it before scripts included by children. This forces the DSD polyfill
// to run first, before any components are defined.
shadowrootmode ? polyfillDeclarativeShadowDom() : undefined,
children,
]);
}

/**
* Returns a prerender annotation used by the bundler to inject the declarative
Expand Down
66 changes: 66 additions & 0 deletions packages/declarative_shadow_dom/preact_test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { renderToString } from 'preact-render-to-string';
import { serialize } from '../../common/models/prerender_annotation.mjs';
import { Template } from './preact.mjs';

describe('preact', () => {
describe('Template', () => {
it('renders a `<template>` element', () => {
const template = Template({ children: 'Hello, World!' });

const html = renderToString(template);

expect(html).toBe('<template>Hello, World!</template>');
});
it('renders a `<template>` element with the DSD polyfill when `shadowrootmode` is given', () => {
const template = Template({ shadowrootmode: 'open' });

const html = renderToString(template);

expect(html).toContain('<template shadowrootmode="open">');
expect(html).toContain(`
<rules_prerender:annotation>${
serialize({
type: 'script',
path: 'packages/declarative_shadow_dom/declarative_shadow_dom_polyfill.mjs',
}).replaceAll('"', '&quot;')
}</rules_prerender:annotation>
`.trim());
});

it('renders a `<template>` element with other attributes passed through', () => {
const template = Template({ hidden: true });

const html = renderToString(template);

expect(html).toBe('<template hidden></template>');
});

it('renders DSD polyfill *before* children', () => {
// NOTE: It is very import to render the DSD polyfill *before*
// children because the bundler will generate entry points in the
// same order. If children came first, components would bootstrap
// before the DSD polyfill had executed.
const template = Template({
shadowrootmode: 'open',
children: 'Hello, World!',
});

const html = renderToString(template);

const annotationIndex = html.indexOf(`
<rules_prerender:annotation>${
serialize({
type: 'script',
path: 'packages/declarative_shadow_dom/declarative_shadow_dom_polyfill.mjs',
}).replaceAll('"', '&quot;')
}</rules_prerender:annotation>
`.trim());
expect(annotationIndex).not.toBe(-1);

const childrenIndex = html.indexOf('Hello, World!');
expect(childrenIndex).not.toBe(-1);

expect(annotationIndex).toBeLessThan(childrenIndex);
});
});
});

0 comments on commit dbc79e7

Please sign in to comment.