Skip to content

Commit ec694bb

Browse files
committedMar 4, 2025
Chore: Create standalone repo for astro lit
0 parents  commit ec694bb

19 files changed

+1071
-0
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

‎CHANGELOG.md

+323
Large diffs are not rendered by default.

‎README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# @astrojs/lit 🔥
2+
3+
> First party support for Lit was included up until Astro 5 but was removed. This community version preserves Lit SSR rendering for newer versions of Lit.
4+
5+
This **[Astro integration][https://astro.build/integrations/]** enables server-side rendering and client-side hydration for your [Lit](https://lit.dev/) custom elements.
6+
7+
## License
8+
9+
MIT

‎client-shim.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
async function polyfill() {
2+
const { hydrateShadowRoots } = await import(
3+
'@webcomponents/template-shadowroot/template-shadowroot.js'
4+
);
5+
window.addEventListener('DOMContentLoaded', () => hydrateShadowRoots(document.body), {
6+
once: true,
7+
});
8+
}
9+
10+
const polyfillCheckEl = Document.parseHTMLUnsafe(
11+
`<p><template shadowrootmode="open"></template></p>`
12+
).querySelector('p');
13+
14+
if (!polyfillCheckEl?.shadowRoot) {
15+
polyfill();
16+
}

‎client-shim.min.js

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/** @license Copyright 2020 Google LLC (BSD-3-Clause) */
2+
/** Bundled JS generated from "@astrojs/lit/client-shim.js" */
3+
var N = Object.defineProperty;
4+
var i = (t, n) => () => (t && (n = t((t = 0))), n);
5+
var b = (t, n) => {
6+
for (var a in n) N(t, a, { get: n[a], enumerable: !0 });
7+
};
8+
function s() {
9+
if (d === void 0) {
10+
let t = document.createElement('div');
11+
(t.innerHTML = '<div><template shadowrootmode="open"></template></div>'),
12+
(d = !!t.firstElementChild.shadowRoot);
13+
}
14+
return d;
15+
}
16+
var d,
17+
m = i(() => {});
18+
var p,
19+
c,
20+
f,
21+
u = i(() => {
22+
(p = (t) => t.parentElement === null),
23+
(c = (t) => t.tagName === 'TEMPLATE'),
24+
(f = (t) => t.nodeType === Node.ELEMENT_NODE);
25+
});
26+
var h,
27+
E = i(() => {
28+
m();
29+
u();
30+
h = (t) => {
31+
var n;
32+
if (s()) return;
33+
let a = [],
34+
e = t.firstElementChild;
35+
for (; e !== t && e !== null; )
36+
if (c(e)) a.push(e), (e = e.content);
37+
else if (e.firstElementChild !== null) e = e.firstElementChild;
38+
else if (f(e) && e.nextElementSibling !== null) e = e.nextElementSibling;
39+
else {
40+
let o;
41+
for (; e !== t && e !== null; )
42+
if (p(e)) {
43+
o = a.pop();
44+
let r = o.parentElement,
45+
l = o.getAttribute('shadowroot');
46+
if (((e = o), l === 'open' || l === 'closed')) {
47+
let y = o.hasAttribute('shadowrootdelegatesfocus');
48+
try {
49+
r.attachShadow({ mode: l, delegatesFocus: y }).append(o.content);
50+
} catch {}
51+
} else o = void 0;
52+
} else {
53+
let r = e.nextElementSibling;
54+
if (r != null) {
55+
(e = r), o !== void 0 && o.parentElement.removeChild(o);
56+
break;
57+
}
58+
let l =
59+
(n = e.parentElement) === null || n === void 0 ? void 0 : n.nextElementSibling;
60+
if (l != null) {
61+
(e = l), o !== void 0 && o.parentElement.removeChild(o);
62+
break;
63+
}
64+
(e = e.parentElement), o !== void 0 && (o.parentElement.removeChild(o), (o = void 0));
65+
}
66+
}
67+
};
68+
});
69+
var w = i(() => {
70+
E();
71+
});
72+
var v = {};
73+
b(v, { hasNativeDeclarativeShadowRoots: () => s, hydrateShadowRoots: () => h });
74+
var S = i(() => {
75+
m();
76+
w();
77+
});
78+
async function g() {
79+
let { hydrateShadowRoots: t } = await Promise.resolve().then(() => (S(), v));
80+
window.addEventListener('DOMContentLoaded', () => t(document.body), { once: true });
81+
}
82+
var x = Document.parseHTMLUnsafe(
83+
'<p><template shadowrootmode="open"></template></p>'
84+
).querySelector('p');
85+
(!x || !x.shadowRoot) && g();

‎dist/client.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
declare const _default: (element: HTMLElement) => (Component: any, props: Record<string, any>, { default: defaultChildren, ...slotted }: {
2+
default: string;
3+
[slotName: string]: string;
4+
}) => Promise<void>;
5+
export default _default;

‎dist/client.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const addSlotAttrsToHtmlString = (slotName, html) => {
2+
const templ = document.createElement("template");
3+
templ.innerHTML = html;
4+
Array.from(templ.content.children).forEach((node) => {
5+
node.setAttribute("slot", slotName);
6+
});
7+
return templ.innerHTML;
8+
};
9+
var client_default = (element) => async (Component, props, { default: defaultChildren, ...slotted }) => {
10+
let component = element.children[0];
11+
const isClientOnly = element.getAttribute("client") === "only";
12+
if (isClientOnly) {
13+
component = new Component();
14+
const otherSlottedChildren = Object.entries(slotted).map(([slotName, htmlStr]) => addSlotAttrsToHtmlString(slotName, htmlStr)).join("");
15+
component.innerHTML = `${defaultChildren ?? ""}${otherSlottedChildren}`;
16+
element.appendChild(component);
17+
for (let [name, value] of Object.entries(props)) {
18+
if (!(name in Component.prototype)) {
19+
component.setAttribute(name, value);
20+
}
21+
}
22+
}
23+
if (!component || !(component.hasAttribute("defer-hydration") || isClientOnly)) {
24+
return;
25+
}
26+
for (let [name, value] of Object.entries(props)) {
27+
if (name in Component.prototype) {
28+
component[name] = value;
29+
}
30+
}
31+
component.removeAttribute("defer-hydration");
32+
};
33+
export {
34+
client_default as default
35+
};

‎dist/index.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { AstroIntegration, ContainerRenderer } from 'astro';
2+
export declare function getContainerRenderer(): ContainerRenderer;
3+
export default function (): AstroIntegration;

‎dist/index.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { readFileSync } from "node:fs";
2+
function getViteConfiguration() {
3+
return {
4+
optimizeDeps: {
5+
include: [
6+
"@astrojs/lit/dist/client.js",
7+
"@astrojs/lit/client-shim.js",
8+
"@astrojs/lit/hydration-support.js",
9+
"@webcomponents/template-shadowroot/template-shadowroot.js",
10+
"@lit-labs/ssr-client/lit-element-hydrate-support.js"
11+
],
12+
exclude: ["@astrojs/lit/server.js"]
13+
},
14+
ssr: {
15+
external: ["lit-element", "@lit-labs/ssr", "@astrojs/lit", "lit/decorators.js"]
16+
}
17+
};
18+
}
19+
function getContainerRenderer() {
20+
return {
21+
name: "@astrojs/lit",
22+
serverEntrypoint: "@astrojs/lit/server.js"
23+
};
24+
}
25+
function index_default() {
26+
return {
27+
name: "@astrojs/lit",
28+
hooks: {
29+
"astro:config:setup": ({ updateConfig, addRenderer, injectScript }) => {
30+
injectScript(
31+
"head-inline",
32+
readFileSync(new URL("../client-shim.min.js", import.meta.url), { encoding: "utf-8" })
33+
);
34+
injectScript("before-hydration", `import '@astrojs/lit/hydration-support.js';`);
35+
addRenderer({
36+
name: "@astrojs/lit",
37+
serverEntrypoint: "@astrojs/lit/server.js",
38+
clientEntrypoint: "@astrojs/lit/dist/client.js"
39+
});
40+
updateConfig({
41+
vite: getViteConfiguration()
42+
});
43+
},
44+
"astro:build:setup": ({ vite, target }) => {
45+
if (target === "server") {
46+
if (!vite.ssr) {
47+
vite.ssr = {};
48+
}
49+
if (!vite.ssr.noExternal) {
50+
vite.ssr.noExternal = [];
51+
}
52+
if (Array.isArray(vite.ssr.noExternal)) {
53+
vite.ssr.noExternal.push("lit");
54+
}
55+
}
56+
}
57+
}
58+
};
59+
}
60+
export {
61+
index_default as default,
62+
getContainerRenderer
63+
};

‎hydration-support.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// @ts-check
2+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js';

‎package.json

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"name": "@astrojs/lit",
3+
"version": "5.0.0",
4+
"description": "Use Lit components within Astro",
5+
"type": "module",
6+
"types": "./dist/index.d.ts",
7+
"author": "withastro",
8+
"license": "MIT",
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/withastro/astro.git",
12+
"directory": "packages/integrations/lit"
13+
},
14+
"keywords": [
15+
"astro-integration",
16+
"astro-component",
17+
"renderer",
18+
"lit"
19+
],
20+
"bugs": "https://github.com/withastro/astro/issues",
21+
"homepage": "https://docs.astro.build/en/guides/integrations-guide/lit/",
22+
"exports": {
23+
".": "./dist/index.js",
24+
"./server.js": {
25+
"types": "./server.d.ts",
26+
"default": "./server.js"
27+
},
28+
"./client-shim.js": "./client-shim.js",
29+
"./dist/client.js": "./dist/client.js",
30+
"./hydration-support.js": "./hydration-support.js",
31+
"./package.json": "./package.json"
32+
},
33+
"files": [
34+
"dist",
35+
"client-shim.js",
36+
"client-shim.min.js",
37+
"hydration-support.js",
38+
"server.js",
39+
"server.d.ts",
40+
"server-shim.js"
41+
],
42+
"scripts": {
43+
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
44+
"build:ci": "astro-scripts build \"src/**/*.ts\"",
45+
"dev": "astro-scripts dev \"src/**/*.ts\"",
46+
"test": "astro-scripts test \"test/**/*.test.js\""
47+
},
48+
"dependencies": {
49+
"@lit-labs/ssr": "^3.3.1",
50+
"@lit-labs/ssr-client": "^1.1.7",
51+
"@lit-labs/ssr-dom-shim": "^1.3.0",
52+
"parse5": "^7.2.1"
53+
},
54+
"overrides": {
55+
"@lit-labs/ssr": {
56+
"@lit-labs/ssr-client": "1.1.7"
57+
}
58+
},
59+
"devDependencies": {
60+
"astro": "workspace:*",
61+
"astro-scripts": "workspace:*",
62+
"cheerio": "1.0.0",
63+
"lit": "^3.2.1",
64+
"sass": "^1.85.1"
65+
},
66+
"peerDependencies": {
67+
"@webcomponents/template-shadowroot": "^0.2.1",
68+
"lit": "^3.2.0"
69+
},
70+
"publishConfig": {
71+
"provenance": true
72+
}
73+
}

‎server-shim.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { customElements as litCE, HTMLElement as litShimHTMLElement } from '@lit-labs/ssr-dom-shim';
2+
3+
// Something at build time injects document.currentScript = undefined instead of
4+
// document.currentScript = null. This causes Sass build to fail because it
5+
// seems to be expecting `=== null`. This set to `undefined` doesn't seem to be
6+
// caused by Lit and only happens at build / test time, but not in dev or
7+
// preview time.
8+
if (globalThis.document) {
9+
document.currentScript = null;
10+
}
11+
12+
if (globalThis.HTMLElement) {
13+
// Seems Astro's Element shim does nothing when `.setAttribute` is called
14+
// and subsequently `.getAttribute` is called. Causes Lit to not SSR attrs
15+
globalThis.HTMLElement = litShimHTMLElement;
16+
}
17+
18+
// Astro seems to have a DOM shim and the only real difference that we need out
19+
// of the Lit DOM shim is that the Lit DOM shim reads
20+
// `HTMLElement.observedAttributes` which is meant to trigger
21+
// `ReactiveElement.finalize()`. So this is the only thing we will re-shim since
22+
// Lit will try to respect other global DOM shims.
23+
globalThis.customElements = litCE;
24+
25+
const litCeDefine = customElements.define;
26+
27+
// We need to patch customElements.define to keep track of the tagName on the
28+
// class itself so that we can transform JSX custom element class definition to
29+
// a DSD string on the server, because there is no way to get the tagName from a
30+
// CE class otherwise. Not an issue on client:only because the browser supports
31+
// appending a class instance directly to the DOM.
32+
customElements.define = function (tagName, Ctr) {
33+
Ctr[Symbol.for('tagName')] = tagName;
34+
return litCeDefine.call(this, tagName, Ctr);
35+
};

‎server.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import type { NamedSSRLoadedRendererValue } from 'astro';
2+
export default NamedSSRLoadedRendererValue;

‎server.js

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Separate import from the rest so it doesn't get re-organized after other imports
2+
import './server-shim.js';
3+
4+
import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js';
5+
import * as parse5 from 'parse5';
6+
7+
function isCustomElementTag(name) {
8+
return typeof name === 'string' && /-/.test(name);
9+
}
10+
11+
function getCustomElementConstructor(name) {
12+
if (typeof customElements !== 'undefined' && isCustomElementTag(name)) {
13+
return customElements.get(name) || null;
14+
} else if (typeof name === 'function') {
15+
return name;
16+
}
17+
return null;
18+
}
19+
20+
async function isLitElement(Component) {
21+
const Ctr = getCustomElementConstructor(Component);
22+
return !!Ctr?._$litElement$;
23+
}
24+
25+
async function check(Component) {
26+
// Lit doesn't support getting a tagName from a Constructor at this time.
27+
// So this must be a string at the moment.
28+
return !!(await isLitElement(Component));
29+
}
30+
31+
function* render(Component, attrs, slots) {
32+
let tagName = Component;
33+
if (typeof tagName !== 'string') {
34+
tagName = Component[Symbol.for('tagName')];
35+
}
36+
const instance = new LitElementRenderer(tagName);
37+
38+
// LitElementRenderer creates a new element instance, so copy over.
39+
const Ctr = getCustomElementConstructor(tagName);
40+
let shouldDeferHydration = false;
41+
42+
if (attrs) {
43+
for (let [name, value] of Object.entries(attrs)) {
44+
const isReactiveProperty = name in Ctr.prototype;
45+
const isReflectedReactiveProperty = Ctr.elementProperties.get(name)?.reflect;
46+
47+
// Only defer hydration if we are setting a reactive property that cannot
48+
// be reflected / serialized as a property.
49+
shouldDeferHydration ||= isReactiveProperty && !isReflectedReactiveProperty;
50+
51+
if (isReactiveProperty) {
52+
instance.setProperty(name, value);
53+
} else {
54+
instance.setAttribute(name, value);
55+
}
56+
}
57+
}
58+
59+
instance.connectedCallback();
60+
61+
yield `<${tagName}${shouldDeferHydration ? ' defer-hydration' : ''}`;
62+
yield* instance.renderAttributes();
63+
yield `>`;
64+
65+
// render component
66+
// see: https://github.com/lit/lit/blob/a66737fc9b861999b00ccad01edb925172b7f711/packages/labs/ssr-react/src/lib/node/render-custom-element.ts#L32
67+
const shadowContents = instance.renderShadow({
68+
elementRenderers: [LitElementRenderer],
69+
customElementInstanceStack: [instance],
70+
customElementHostStack: [instance],
71+
deferHydration: false,
72+
eventTargetStack: [],
73+
slotStack: []
74+
});
75+
76+
if (shadowContents !== undefined) {
77+
const { mode = 'open', delegatesFocus } = instance.shadowRootOptions ?? {};
78+
// `delegatesFocus` is intentionally allowed to coerce to boolean to
79+
// match web platform behavior.
80+
const delegatesfocusAttr = delegatesFocus ? ' shadowrootdelegatesfocus' : '';
81+
yield `<template shadowroot="${mode}" shadowrootmode="${mode}"${delegatesfocusAttr}>`;
82+
yield* shadowContents;
83+
yield '</template>';
84+
}
85+
if (slots) {
86+
for (let [slot, value = ''] of Object.entries(slots)) {
87+
if (slot !== 'default' && value) {
88+
// Parse the value as a concatenated string
89+
const fragment = parse5.parseFragment(`${value}`);
90+
91+
// Add the missing slot attribute to child Element nodes
92+
for (const node of fragment.childNodes) {
93+
if (node.tagName && !node.attrs.some(({ name }) => name === 'slot')) {
94+
node.attrs.push({ name: 'slot', value: slot });
95+
}
96+
}
97+
98+
value = parse5.serialize(fragment);
99+
}
100+
101+
yield value;
102+
}
103+
}
104+
yield `</${tagName}>`;
105+
}
106+
107+
async function renderToStaticMarkup(Component, props, slots) {
108+
let tagName = Component;
109+
110+
let out = '';
111+
for (let chunk of render(tagName, props, slots)) {
112+
out += chunk;
113+
}
114+
115+
return {
116+
html: out,
117+
};
118+
}
119+
120+
export default {
121+
name: '@astrojs/lit',
122+
check,
123+
renderToStaticMarkup,
124+
};

‎src/client.ts

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Adds the appropriate slot attribute to each top-level node in the given HTML
3+
* string.
4+
*
5+
* @example
6+
* addSlotAttrsToHtmlString('foo', '<div>bar</div><div>baz</div>');
7+
* // '<div slot="foo">bar</div><div slot="foo">baz</div>'
8+
*
9+
* @param slotName Name of slot to apply to HTML string.
10+
* @param html Stringified HTML that should be projected into the given slotname.
11+
* @returns A stringified HTML string with the slot attribute applied to each top-level node.
12+
*/
13+
const addSlotAttrsToHtmlString = (slotName: string, html: string) => {
14+
const templ = document.createElement('template');
15+
templ.innerHTML = html;
16+
Array.from(templ.content.children).forEach((node) => {
17+
node.setAttribute('slot', slotName);
18+
});
19+
return templ.innerHTML;
20+
};
21+
22+
export default (element: HTMLElement) =>
23+
async (
24+
Component: any,
25+
props: Record<string, any>,
26+
{ default: defaultChildren, ...slotted }: { default: string; [slotName: string]: string }
27+
) => {
28+
// Get the LitElement element instance.
29+
let component = element.children[0];
30+
// Check if hydration model is client:only
31+
const isClientOnly = element.getAttribute('client') === 'only';
32+
33+
// We need to attach the element and it's children to the DOM since it's not
34+
// SSR'd.
35+
if (isClientOnly) {
36+
component = new Component();
37+
38+
const otherSlottedChildren = Object.entries(slotted)
39+
.map(([slotName, htmlStr]) => addSlotAttrsToHtmlString(slotName, htmlStr))
40+
.join('');
41+
42+
// defaultChildren can actually be undefined, but TS will complain if we
43+
// type it as so, make sure we don't render undefined.
44+
component.innerHTML = `${defaultChildren ?? ''}${otherSlottedChildren}`;
45+
element.appendChild(component);
46+
47+
// Set props bound to non-reactive properties as attributes.
48+
for (let [name, value] of Object.entries(props)) {
49+
if (!(name in Component.prototype)) {
50+
component.setAttribute(name, value);
51+
}
52+
}
53+
}
54+
55+
// If there is no deferral of hydration, then all reactive properties are
56+
// already serialized as reflected attributes, or no reactive props were set
57+
// Alternatively, if hydration is client:only proceed to set props.
58+
if (!component || !(component.hasAttribute('defer-hydration') || isClientOnly)) {
59+
return;
60+
}
61+
62+
// Set properties on the LitElement instance for resuming hydration.
63+
for (let [name, value] of Object.entries(props)) {
64+
// Check if reactive property or class property.
65+
if (name in Component.prototype) {
66+
(component as any)[name] = value;
67+
}
68+
}
69+
70+
// Tell LitElement to resume hydration.
71+
component.removeAttribute('defer-hydration');
72+
};

‎src/index.ts

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { readFileSync } from 'node:fs';
2+
import type { AstroIntegration, ContainerRenderer } from 'astro';
3+
4+
function getViteConfiguration() {
5+
return {
6+
optimizeDeps: {
7+
include: [
8+
'@astrojs/lit/dist/client.js',
9+
'@astrojs/lit/client-shim.js',
10+
'@astrojs/lit/hydration-support.js',
11+
'@webcomponents/template-shadowroot/template-shadowroot.js',
12+
'@lit-labs/ssr-client/lit-element-hydrate-support.js',
13+
],
14+
exclude: ['@astrojs/lit/server.js'],
15+
},
16+
ssr: {
17+
external: ['lit-element', '@lit-labs/ssr', '@astrojs/lit', 'lit/decorators.js'],
18+
},
19+
};
20+
}
21+
22+
export function getContainerRenderer(): ContainerRenderer {
23+
return {
24+
name: '@astrojs/lit',
25+
serverEntrypoint: '@astrojs/lit/server.js',
26+
};
27+
}
28+
29+
export default function (): AstroIntegration {
30+
return {
31+
name: '@astrojs/lit',
32+
hooks: {
33+
'astro:config:setup': ({ updateConfig, addRenderer, injectScript }) => {
34+
// Inject the necessary polyfills on every page (inlined for speed).
35+
injectScript(
36+
'head-inline',
37+
readFileSync(new URL('../client-shim.min.js', import.meta.url), { encoding: 'utf-8' })
38+
);
39+
// Inject the hydration code, before a component is hydrated.
40+
injectScript('before-hydration', `import '@astrojs/lit/hydration-support.js';`);
41+
// Add the lit renderer so that Astro can understand lit components.
42+
addRenderer({
43+
name: '@astrojs/lit',
44+
serverEntrypoint: '@astrojs/lit/server.js',
45+
clientEntrypoint: '@astrojs/lit/dist/client.js',
46+
});
47+
// Update the vite configuration.
48+
updateConfig({
49+
vite: getViteConfiguration(),
50+
});
51+
},
52+
'astro:build:setup': ({ vite, target }) => {
53+
if (target === 'server') {
54+
if (!vite.ssr) {
55+
vite.ssr = {};
56+
}
57+
if (!vite.ssr.noExternal) {
58+
vite.ssr.noExternal = [];
59+
}
60+
if (Array.isArray(vite.ssr.noExternal)) {
61+
vite.ssr.noExternal.push('lit');
62+
}
63+
}
64+
},
65+
},
66+
};
67+
}

‎test/sass.test.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
4+
describe('check', () => {
5+
it('should be able to load sass', async () => {
6+
let error = null;
7+
try {
8+
await import(new URL('../server-shim.js', import.meta.url));
9+
await import('sass');
10+
} catch (e) {
11+
error = e;
12+
}
13+
assert.equal(error, null);
14+
});
15+
});

‎test/server.test.js

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import * as assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
import * as cheerio from 'cheerio';
4+
import { LitElement, html } from 'lit';
5+
// Must come after lit import because @lit/reactive-element defines
6+
// globalThis.customElements which the server shim expects to be defined.
7+
import server from '../server.js';
8+
9+
const { check, renderToStaticMarkup } = server;
10+
11+
describe('check', () => {
12+
it('should be false with no component', async () => {
13+
assert.equal(await check(), false);
14+
});
15+
16+
it('should be false with a registered non-lit component', async () => {
17+
const tagName = 'non-lit-component';
18+
// Lit no longer shims HTMLElement globally, so we need to do it ourselves.
19+
if (!globalThis.HTMLElement) {
20+
globalThis.HTMLElement = class {};
21+
}
22+
customElements.define(tagName, class TestComponent extends HTMLElement {});
23+
assert.equal(await check(tagName), false);
24+
});
25+
26+
it('should be true with a registered lit component', async () => {
27+
const tagName = 'lit-component';
28+
customElements.define(tagName, class extends LitElement {});
29+
assert.equal(await check(tagName), true);
30+
});
31+
});
32+
33+
describe('renderToStaticMarkup', () => {
34+
it('should throw error if trying to render an unregistered component', async () => {
35+
const tagName = 'non-registrered-component';
36+
try {
37+
await renderToStaticMarkup(tagName);
38+
} catch (e) {
39+
assert.equal(e instanceof TypeError, true);
40+
}
41+
});
42+
43+
it('should render empty component with default markup', async () => {
44+
const tagName = 'nothing-component';
45+
customElements.define(tagName, class extends LitElement {});
46+
const render = await renderToStaticMarkup(tagName);
47+
assert.deepEqual(render, {
48+
html: `<${tagName}><template shadowroot="open" shadowrootmode="open"><!--lit-part--><!--/lit-part--></template></${tagName}>`,
49+
});
50+
});
51+
52+
it('should render component with default markup', async () => {
53+
const tagName = 'simple-component';
54+
customElements.define(
55+
tagName,
56+
class extends LitElement {
57+
render() {
58+
return html`<p>hola</p>`;
59+
}
60+
}
61+
);
62+
const render = await renderToStaticMarkup(tagName);
63+
const $ = cheerio.load(render.html);
64+
assert.equal($(`${tagName} template`).html().includes('<p>hola</p>'), true);
65+
});
66+
67+
it('should render component with properties and attributes', async () => {
68+
const tagName = 'props-and-attrs-component';
69+
const attr1 = 'test';
70+
const prop1 = 'Daniel';
71+
customElements.define(
72+
tagName,
73+
class extends LitElement {
74+
static properties = {
75+
prop1: { type: String },
76+
};
77+
78+
constructor() {
79+
super();
80+
this.prop1 = 'someone';
81+
}
82+
83+
render() {
84+
return html`<p>Hello ${this.prop1}</p>`;
85+
}
86+
}
87+
);
88+
const render = await renderToStaticMarkup(tagName, { prop1, attr1 });
89+
const $ = cheerio.load(render.html);
90+
assert.equal($(tagName).attr('attr1'), attr1);
91+
assert.equal($(`${tagName} template`).text().includes(`Hello ${prop1}`), true);
92+
});
93+
94+
it('should render nested components', async () => {
95+
const tagName = 'parent-component';
96+
const childTagName = 'child-component';
97+
customElements.define(
98+
childTagName,
99+
class extends LitElement {
100+
render() {
101+
return html`<p>child</p>`;
102+
}
103+
}
104+
);
105+
customElements.define(
106+
tagName,
107+
class extends LitElement {
108+
render() {
109+
return html`<child-component></child-component>`;
110+
}
111+
}
112+
);
113+
const render = await renderToStaticMarkup(tagName);
114+
const $ = cheerio.load(render.html);
115+
assert.equal($(`${tagName} template`).text().includes('child'), true);
116+
// Child component should have `defer-hydration` attribute so it'll only
117+
// hydrate after the parent hydrates
118+
assert.equal($(childTagName).attr('defer-hydration'), '');
119+
});
120+
121+
it('should render DSD attributes based on shadowRootOptions', async () => {
122+
const tagName = 'shadow-root-options-component';
123+
customElements.define(
124+
tagName,
125+
class extends LitElement {
126+
static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true };
127+
}
128+
);
129+
const render = await renderToStaticMarkup(tagName);
130+
assert.deepEqual(render, {
131+
html: `<${tagName}><template shadowroot=\"open\" shadowrootmode=\"open\" shadowrootdelegatesfocus><!--lit-part--><!--/lit-part--></template></${tagName}>`,
132+
});
133+
});
134+
});

‎tsconfig.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../../tsconfig.base.json",
3+
"include": ["src"],
4+
"compilerOptions": {
5+
"outDir": "./dist"
6+
}
7+
}

0 commit comments

Comments
 (0)
Please sign in to comment.