@qwik-custom-elements/adapter-lit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitry A. Efimenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @qwik-custom-elements/adapter-lit
2
+
3
+ Lit-specific adapter contract and SSR surface for generated Qwik custom-element integration.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ npm install @qwik-custom-elements/adapter-lit
9
+ ```
10
+
11
+ Peer dependencies:
12
+
13
+ ```
14
+ npm install @builder.io/qwik lit
15
+ ```
16
+
17
+ For SSR support, also install the optional peers:
18
+
19
+ ```
20
+ npm install @lit-labs/ssr @lit-labs/ssr-client
21
+ ```
22
+
23
+ ## Quickstart
24
+
25
+ Reference this adapter in your `qwik-custom-elements.config.json`:
26
+
27
+ ```json
28
+ {
29
+ "projects": [
30
+ {
31
+ "id": "my-lit-lib",
32
+ "adapter": "lit",
33
+ "adapterPackage": "@qwik-custom-elements/adapter-lit",
34
+ "source": {
35
+ "type": "PACKAGE_NAME",
36
+ "packageName": "my-lit-lib"
37
+ },
38
+ "adapterOptions": {
39
+ "runtime": {
40
+ "libraryImport": "my-lit-lib"
41
+ }
42
+ },
43
+ "outDir": "./src/generated/lit/csr"
44
+ }
45
+ ]
46
+ }
47
+ ```
48
+
49
+ For SSR-capable generation, set `"adapterPackage"` to `"@qwik-custom-elements/adapter-lit/ssr"` and point `"outDir"` to a separate SSR folder.
50
+
51
+ Run `npx qwik-custom-elements` to generate Qwik wrappers from the Lit component library.
52
+
53
+ ## Support Policy
54
+
55
+ This package follows semantic versioning. See [COMPATIBILITY.md](../../COMPATIBILITY.md) for tested combinations of adapter-lit, Qwik, Lit, `@lit-labs/ssr`, and Node.js.
56
+
57
+ Breaking changes always include an explicit `BREAKING` section in the release notes and require an update to `COMPATIBILITY.md` before merging.
58
+
59
+ ## Ownership Boundary
60
+
61
+ `@qwik-custom-elements/adapter-lit` owns Lit-specific generation behavior.
62
+
63
+ That ownership includes:
64
+
65
+ - Lit capability metadata
66
+ - Lit SSR capability probing and SSR-specific output contracts
67
+ - adapter-owned generated barrels, wrapper modules, and any Lit-specific generated helper modules
68
+ - framework-specific output shape decisions that must not live in core
69
+
70
+ Core may orchestrate the run and pass typed parsed component metadata into the adapter, but core should not shape Lit-generated output directly.
71
+
72
+ ## Current Exports
73
+
74
+ This package currently exposes:
75
+
76
+ - `@qwik-custom-elements/adapter-lit`
77
+ - Lit adapter metadata
78
+ - SSR capability probe wiring
79
+ - `@qwik-custom-elements/adapter-lit/ssr`
80
+ - Lit SSR placeholder probe and SSR markup contract
81
+
82
+ ## Current Status
83
+
84
+ Lit support is still an adapter-contract surface rather than a complete generated-wrapper implementation.
85
+
86
+ Even so, the architectural boundary is already fixed: when Lit-generated wrappers or helper modules are emitted, they belong to this adapter rather than `@qwik-custom-elements/core`.
87
+
88
+ ## Documentation Expectations
89
+
90
+ When Lit adapter contracts or generated output ownership change, update this README alongside the system decisions and findings logs.
@@ -0,0 +1,3 @@
1
+ export { createLitCSRComponent, renderComponentCsrTag, type LitCSRProps, } from './lit-csr.js';
2
+ export { createLitCSRClientSetup } from './lit-csr-client-setup.js';
3
+ export { createLitSSRClientSetup } from './lit-ssr-client-setup.js';
@@ -0,0 +1,3 @@
1
+ export { createLitCSRComponent, renderComponentCsrTag, } from './lit-csr.js';
2
+ export { createLitCSRClientSetup } from './lit-csr-client-setup.js';
3
+ export { createLitSSRClientSetup } from './lit-ssr-client-setup.js';
@@ -0,0 +1,11 @@
1
+ import { type QRL } from '@builder.io/qwik';
2
+ /**
3
+ * Creates a client-side setup hook for Lit CSR components.
4
+ *
5
+ * The returned hook registers a `readystatechange` listener that calls the
6
+ * provided QRL to load and define the custom elements. The setup is idempotent
7
+ * and runs at most once per page load, tracked via a `globalThis` marker.
8
+ *
9
+ * Call the returned hook inside a Qwik component to activate element loading.
10
+ */
11
+ export declare function createLitCSRClientSetup(importLibraryQrl?: QRL<() => Promise<void>>): () => void;
@@ -0,0 +1,58 @@
1
+ import { $, useOnDocument } from '@builder.io/qwik';
2
+ const LIT_CSR_CLIENT_SETUP_DONE = '__qce_lit_csr_client_setup_done__';
3
+ function ensureLitCSRClientSetup(importLibraryQrl) {
4
+ const markerTarget = globalThis;
5
+ const existingMarker = markerTarget[LIT_CSR_CLIENT_SETUP_DONE];
6
+ if (existingMarker === true) {
7
+ return Promise.resolve();
8
+ }
9
+ if (typeof existingMarker === 'object' &&
10
+ existingMarker !== null &&
11
+ 'then' in existingMarker &&
12
+ typeof existingMarker.then === 'function') {
13
+ return existingMarker;
14
+ }
15
+ const setupPromise = (async () => {
16
+ if (importLibraryQrl) {
17
+ const fn = await importLibraryQrl.resolve();
18
+ await fn();
19
+ }
20
+ })();
21
+ markerTarget[LIT_CSR_CLIENT_SETUP_DONE] = setupPromise;
22
+ return setupPromise
23
+ .then(() => {
24
+ markerTarget[LIT_CSR_CLIENT_SETUP_DONE] = true;
25
+ })
26
+ .catch((error) => {
27
+ delete markerTarget[LIT_CSR_CLIENT_SETUP_DONE];
28
+ throw error;
29
+ });
30
+ }
31
+ async function executeLitCSRClientSetup(importLibraryQrl) {
32
+ await ensureLitCSRClientSetup(importLibraryQrl).catch((error) => {
33
+ console.error(error);
34
+ });
35
+ }
36
+ /**
37
+ * Creates a client-side setup hook for Lit CSR components.
38
+ *
39
+ * The returned hook registers a `readystatechange` listener that calls the
40
+ * provided QRL to load and define the custom elements. The setup is idempotent
41
+ * and runs at most once per page load, tracked via a `globalThis` marker.
42
+ *
43
+ * Call the returned hook inside a Qwik component to activate element loading.
44
+ */
45
+ export function createLitCSRClientSetup(importLibraryQrl) {
46
+ const runSetup$ = $(async () => {
47
+ await executeLitCSRClientSetup(importLibraryQrl);
48
+ });
49
+ const useLitCSRClientSetup = () => {
50
+ useOnDocument('readystatechange', runSetup$);
51
+ // Also run immediately when mounted after the document is already past the
52
+ // loading state (client navigation or dev fast-refresh timing).
53
+ if (typeof document !== 'undefined' && document.readyState !== 'loading') {
54
+ void executeLitCSRClientSetup(importLibraryQrl);
55
+ }
56
+ };
57
+ return useLitCSRClientSetup;
58
+ }
@@ -0,0 +1 @@
1
+ export declare function updateLitCSRHostProps(host: HTMLElement | undefined, props: Record<string, unknown> | undefined): void;
@@ -0,0 +1,17 @@
1
+ const EVENT_PROP_PREFIX = 'on';
2
+ const QRL_PRIVATE_PREFIX = '$';
3
+ function isEventLikeProp(propName) {
4
+ return (propName.startsWith(EVENT_PROP_PREFIX) ||
5
+ propName.startsWith(QRL_PRIVATE_PREFIX));
6
+ }
7
+ export function updateLitCSRHostProps(host, props) {
8
+ if (!host || !props) {
9
+ return;
10
+ }
11
+ for (const [propName, propValue] of Object.entries(props)) {
12
+ if (isEventLikeProp(propName)) {
13
+ continue;
14
+ }
15
+ host[propName] = propValue;
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ import { type QRL } from '@builder.io/qwik';
2
+ export declare function renderComponentCsrTag(options?: {
3
+ tagName?: unknown;
4
+ }): string | null;
5
+ export interface LitCSRProps {
6
+ tagName: string;
7
+ props?: Record<string, unknown>;
8
+ events?: Record<string, QRL<(...args: any[]) => void>>;
9
+ slots?: string[];
10
+ [key: string]: unknown;
11
+ }
12
+ export declare function createLitCSRComponent(): import("@builder.io/qwik").Component<LitCSRProps>;
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@builder.io/qwik/jsx-runtime";
2
+ import { Slot, component$, useSignal, useVisibleTask$, } from '@builder.io/qwik';
3
+ import { updateLitCSRHostProps } from './lit-csr-props.js';
4
+ const EVENT_QRL_IDS = new WeakMap();
5
+ let eventQrlIdCounter = 0;
6
+ const PROP_VALUE_IDS = new WeakMap();
7
+ let propValueIdCounter = 0;
8
+ function createContractError(code, message) {
9
+ const error = new Error(message);
10
+ error.code = code;
11
+ return error;
12
+ }
13
+ export function renderComponentCsrTag(options = {}) {
14
+ if (options.tagName == null) {
15
+ return null;
16
+ }
17
+ if (typeof options.tagName !== 'string' ||
18
+ options.tagName.trim().length === 0) {
19
+ throw createContractError('QCE_LIT_RUNTIME_TAGNAME_INVALID', 'Lit CSR render contract requires options.tagName to be a non-empty string when provided.');
20
+ }
21
+ return options.tagName.trim();
22
+ }
23
+ function requireLitTagName(tagName) {
24
+ const normalizedTagName = renderComponentCsrTag({ tagName });
25
+ if (normalizedTagName == null) {
26
+ throw createContractError('QCE_LIT_RUNTIME_TAGNAME_INVALID', 'Lit CSR render contract requires options.tagName to be a non-empty string when provided.');
27
+ }
28
+ return normalizedTagName;
29
+ }
30
+ function getEventQrlId(qrl) {
31
+ if (!qrl || (typeof qrl !== 'object' && typeof qrl !== 'function')) {
32
+ return -1;
33
+ }
34
+ const qrlObject = qrl;
35
+ const existing = EVENT_QRL_IDS.get(qrlObject);
36
+ if (existing) {
37
+ return existing;
38
+ }
39
+ const next = ++eventQrlIdCounter;
40
+ EVENT_QRL_IDS.set(qrlObject, next);
41
+ return next;
42
+ }
43
+ function getEventEntries(events) {
44
+ return Object.entries(events ?? {}).filter(([eventName, eventQrl]) => eventName.trim().length > 0 && Boolean(eventQrl));
45
+ }
46
+ function getEventsDependencyKey(events) {
47
+ return getEventEntries(events)
48
+ .map(([eventName, eventQrl]) => `${eventName}:${getEventQrlId(eventQrl)}`)
49
+ .sort()
50
+ .join('|');
51
+ }
52
+ function getPropValueDependencyKey(value) {
53
+ if (value == null) {
54
+ return String(value);
55
+ }
56
+ if (typeof value !== 'object' && typeof value !== 'function') {
57
+ return `${typeof value}:${String(value)}`;
58
+ }
59
+ const valueObject = value;
60
+ const existing = PROP_VALUE_IDS.get(valueObject);
61
+ if (existing) {
62
+ return `ref:${existing}`;
63
+ }
64
+ const next = ++propValueIdCounter;
65
+ PROP_VALUE_IDS.set(valueObject, next);
66
+ return `ref:${next}`;
67
+ }
68
+ function getPropsDependencyKey(props) {
69
+ return Object.entries(props ?? {})
70
+ .map(([propName, propValue]) => `${propName}:${getPropValueDependencyKey(propValue)}`)
71
+ .sort()
72
+ .join('|');
73
+ }
74
+ export function createLitCSRComponent() {
75
+ return component$((inputProps) => {
76
+ const hostRef = useSignal();
77
+ const { tagName, props, events, slots, ...restProps } = inputProps;
78
+ const namedSlots = slots ?? [];
79
+ const normalizedTagName = requireLitTagName(tagName);
80
+ useVisibleTask$(({ track, cleanup }) => {
81
+ const host = track(() => hostRef.value);
82
+ const latestTagName = track(() => inputProps.tagName);
83
+ track(() => getPropsDependencyKey(props));
84
+ if (!host) {
85
+ return;
86
+ }
87
+ let disposed = false;
88
+ cleanup(() => {
89
+ disposed = true;
90
+ });
91
+ updateLitCSRHostProps(host, props);
92
+ const resolvedTagName = requireLitTagName(latestTagName);
93
+ void customElements.whenDefined(resolvedTagName).then(() => {
94
+ if (disposed) {
95
+ return;
96
+ }
97
+ updateLitCSRHostProps(host, props);
98
+ });
99
+ });
100
+ useVisibleTask$(({ track, cleanup }) => {
101
+ const host = track(() => hostRef.value);
102
+ const eventsDependencyKey = track(() => getEventsDependencyKey(events));
103
+ if (!host) {
104
+ return;
105
+ }
106
+ const eventEntries = getEventEntries(events);
107
+ if (eventEntries.length === 0 || eventsDependencyKey.length === 0) {
108
+ return;
109
+ }
110
+ let disposed = false;
111
+ const listeners = [];
112
+ cleanup(() => {
113
+ disposed = true;
114
+ for (const { eventName, listener } of listeners) {
115
+ host.removeEventListener(eventName, listener);
116
+ }
117
+ });
118
+ for (const [eventName, eventQrl] of eventEntries) {
119
+ const listener = (event) => {
120
+ const qrl = eventQrl;
121
+ const containerEl = host.closest('[q\\:container]');
122
+ if (containerEl) {
123
+ qrl.$setContainer$?.(containerEl);
124
+ }
125
+ const result = eventQrl(event, host);
126
+ void Promise.resolve(result).catch((error) => {
127
+ console.error(error);
128
+ });
129
+ };
130
+ if (disposed) {
131
+ return;
132
+ }
133
+ host.addEventListener(eventName, listener);
134
+ listeners.push({ eventName, listener });
135
+ }
136
+ });
137
+ const ElementTag = normalizedTagName;
138
+ return (_jsxs(ElementTag, { ref: hostRef, ...restProps, children: [_jsx(Slot, {}), namedSlots.map((name) => (_jsx("span", { slot: name, style: { display: 'contents' }, children: _jsx(Slot, { name: name }) }, name)))] }));
139
+ });
140
+ }
@@ -0,0 +1,12 @@
1
+ import { type QRL } from '@builder.io/qwik';
2
+ /**
3
+ * Creates a client-side setup hook for Lit SSR components.
4
+ *
5
+ * The returned hook registers a `readystatechange` listener that:
6
+ * 1. Loads `@lit-labs/ssr-client/lit-element-hydrate-support.js` first so
7
+ * existing Declarative Shadow DOM roots are reused instead of duplicated.
8
+ * 2. Then calls the provided QRL to load and define the custom elements.
9
+ *
10
+ * Call the returned hook inside a Qwik component to activate hydration.
11
+ */
12
+ export declare function createLitSSRClientSetup(importLibraryQrl?: QRL<() => Promise<void>>): () => void;
@@ -0,0 +1,63 @@
1
+ import { $, useOnDocument } from '@builder.io/qwik';
2
+ const LIT_SSR_CLIENT_SETUP_DONE = '__qce_lit_ssr_client_setup_done__';
3
+ function ensureLitSSRClientSetup(importLibraryQrl) {
4
+ const markerTarget = globalThis;
5
+ const existingMarker = markerTarget[LIT_SSR_CLIENT_SETUP_DONE];
6
+ if (existingMarker === true) {
7
+ return Promise.resolve();
8
+ }
9
+ if (typeof existingMarker === 'object' &&
10
+ existingMarker !== null &&
11
+ 'then' in existingMarker &&
12
+ typeof existingMarker.then === 'function') {
13
+ return existingMarker;
14
+ }
15
+ const setupPromise = (async () => {
16
+ // Must load hydrate-support BEFORE custom elements are defined so that
17
+ // Lit can reuse the existing Declarative Shadow DOM rather than
18
+ // re-rendering and producing a duplicate shadow tree.
19
+ await import('@qwik-custom-elements/adapter-lit/ssr-client');
20
+ if (importLibraryQrl) {
21
+ const fn = await importLibraryQrl.resolve();
22
+ await fn();
23
+ }
24
+ })();
25
+ markerTarget[LIT_SSR_CLIENT_SETUP_DONE] = setupPromise;
26
+ return setupPromise
27
+ .then(() => {
28
+ markerTarget[LIT_SSR_CLIENT_SETUP_DONE] = true;
29
+ })
30
+ .catch((error) => {
31
+ delete markerTarget[LIT_SSR_CLIENT_SETUP_DONE];
32
+ throw error;
33
+ });
34
+ }
35
+ async function executeLitSSRClientSetup(importLibraryQrl) {
36
+ await ensureLitSSRClientSetup(importLibraryQrl).catch((error) => {
37
+ console.error(error);
38
+ });
39
+ }
40
+ /**
41
+ * Creates a client-side setup hook for Lit SSR components.
42
+ *
43
+ * The returned hook registers a `readystatechange` listener that:
44
+ * 1. Loads `@lit-labs/ssr-client/lit-element-hydrate-support.js` first so
45
+ * existing Declarative Shadow DOM roots are reused instead of duplicated.
46
+ * 2. Then calls the provided QRL to load and define the custom elements.
47
+ *
48
+ * Call the returned hook inside a Qwik component to activate hydration.
49
+ */
50
+ export function createLitSSRClientSetup(importLibraryQrl) {
51
+ const runSetup$ = $(async () => {
52
+ await executeLitSSRClientSetup(importLibraryQrl);
53
+ });
54
+ const useLitSSRClientSetup = () => {
55
+ useOnDocument('readystatechange', runSetup$);
56
+ // Also run immediately when mounted after the document is already past the
57
+ // loading state (client navigation or dev fast-refresh timing).
58
+ if (typeof document !== 'undefined' && document.readyState !== 'loading') {
59
+ void executeLitSSRClientSetup(importLibraryQrl);
60
+ }
61
+ };
62
+ return useLitSSRClientSetup;
63
+ }
@@ -0,0 +1,38 @@
1
+ interface CemComponentProp {
2
+ name: string;
3
+ type: string;
4
+ required: boolean;
5
+ }
6
+ interface CemComponentEvent {
7
+ name: string;
8
+ type: string;
9
+ }
10
+ interface CemComponentSlot {
11
+ name: string;
12
+ }
13
+ interface CemComponentDefinition {
14
+ tagName: string;
15
+ props: CemComponentProp[];
16
+ events: CemComponentEvent[];
17
+ slots: CemComponentSlot[];
18
+ }
19
+ interface CreateGeneratedOutputInput {
20
+ projectId?: string;
21
+ libraryName?: string;
22
+ componentDefinitions?: CemComponentDefinition[];
23
+ runtimeImports?: {
24
+ /**
25
+ * Import specifier for the Lit component library. When provided, the
26
+ * generated SSR runtime will include this import so custom element classes
27
+ * are registered before `@lit-labs/ssr` renders them server-side.
28
+ */
29
+ libraryImport?: string;
30
+ };
31
+ ssrAvailable?: boolean;
32
+ }
33
+ type CreateLitPlannedWritesOptions = CreateGeneratedOutputInput;
34
+ export declare function createLitPlannedWrites({ projectId, libraryName, componentDefinitions, runtimeImports, ssrAvailable, }: CreateLitPlannedWritesOptions): Array<{
35
+ relativePath: string;
36
+ content: string;
37
+ }>;
38
+ export {};
@@ -0,0 +1,226 @@
1
+ export function createLitPlannedWrites({ projectId, libraryName, componentDefinitions, runtimeImports, ssrAvailable, }) {
2
+ const componentTags = (componentDefinitions ?? []).map((componentDefinition) => componentDefinition.tagName);
3
+ const plannedWrites = [
4
+ {
5
+ relativePath: 'index.ts',
6
+ content: renderGeneratedIndex(projectId, componentTags, ssrAvailable),
7
+ },
8
+ ];
9
+ if (ssrAvailable === true) {
10
+ plannedWrites.push({
11
+ relativePath: 'runtime.ts',
12
+ content: renderRuntimeBarrelModule(projectId, true),
13
+ }, {
14
+ relativePath: 'runtime-ssr.generated.ts',
15
+ content: renderServerRuntimeModule(projectId, libraryName, runtimeImports?.libraryImport),
16
+ });
17
+ }
18
+ else {
19
+ plannedWrites.push({
20
+ relativePath: 'runtime.ts',
21
+ content: renderRuntimeBarrelModule(projectId, false),
22
+ }, {
23
+ relativePath: 'runtime-csr.generated.ts',
24
+ content: renderClientRuntimeModule(projectId, libraryName, runtimeImports?.libraryImport),
25
+ });
26
+ }
27
+ plannedWrites.push(...componentTags.map((componentTag) => ({
28
+ relativePath: renderComponentWrapperRelativePath(componentTag, ssrAvailable),
29
+ content: renderComponentWrapper({
30
+ projectId,
31
+ libraryName,
32
+ componentDefinition: normalizeComponentDefinition(componentTag, (componentDefinitions ?? []).find((definition) => definition.tagName === componentTag)),
33
+ ssrAvailable,
34
+ }),
35
+ })));
36
+ return plannedWrites;
37
+ }
38
+ function renderGeneratedIndex(projectId, componentTags, ssrAvailable) {
39
+ const mode = ssrAvailable === true ? 'ssr' : 'csr';
40
+ const exportLines = componentTags.map((componentTag) => `export { ${toWrapperName(componentTag)} } from './${componentTag}';`);
41
+ return [
42
+ `// Generated by @qwik-custom-elements/core. Project: ${projectId ?? 'unknown'}.`,
43
+ '// Do not edit this file directly. Use a manual extension layer.',
44
+ '',
45
+ `export const generatedComponentTags = ${JSON.stringify(componentTags)} as const;`,
46
+ `export const generatedMode = '${mode}' as const;`,
47
+ ...exportLines,
48
+ '',
49
+ ].join('\n');
50
+ }
51
+ function renderComponentWrapper(input) {
52
+ const { projectId, libraryName, componentDefinition, ssrAvailable } = input;
53
+ const { tagName: componentTag } = componentDefinition;
54
+ const wrapperName = toWrapperName(componentTag);
55
+ const ssrBridgeName = toBridgeComponentName(libraryName, 'SSR', 'GeneratedLitComponent');
56
+ const csrBridgeName = toBridgeComponentName(libraryName, 'CSR', 'GeneratedLitCSRComponent');
57
+ const bridgeName = ssrAvailable === true ? ssrBridgeName : csrBridgeName;
58
+ const lines = [
59
+ `// Generated by @qwik-custom-elements/core. Project: ${projectId ?? 'unknown'}.`,
60
+ '// Do not edit this file directly. Use a manual extension layer.',
61
+ ];
62
+ lines.push('', "import { Slot, component$ } from '@builder.io/qwik';");
63
+ if (componentDefinition.events.length > 0) {
64
+ lines.push("import type { QRL } from '@builder.io/qwik';");
65
+ }
66
+ lines.push(`import { ${bridgeName} } from './runtime';`, '');
67
+ const propsTypeName = `${wrapperName}Props`;
68
+ const propLines = componentDefinition.props.map((prop) => {
69
+ const key = isValidIdentifier(prop.name) ? prop.name : `'${prop.name}'`;
70
+ const optionalToken = prop.required ? '' : '?';
71
+ return ` ${key}${optionalToken}: ${prop.type};`;
72
+ });
73
+ const eventPropLines = componentDefinition.events.map((event) => ` ${toEventPropName(event.name)}?: QRL<(event: ${event.type}) => void>;`);
74
+ propLines.push(...eventPropLines, ' [key: string]: unknown;');
75
+ const slotLines = [
76
+ ' <Slot />',
77
+ ...componentDefinition.slots.map((slot) => ` <span q:slot=${JSON.stringify(slot.name)} style={{ display: 'contents' }}>\n <Slot name=${JSON.stringify(slot.name)} />\n </span>`),
78
+ ];
79
+ const slotListToken = componentDefinition.slots.length > 0
80
+ ? JSON.stringify(componentDefinition.slots.map((slot) => slot.name))
81
+ : 'undefined';
82
+ const splitPropsLines = [
83
+ ' const isEventBindingKey = (key: string) =>',
84
+ " /^on[A-Z].*\\$$/.test(key) || key.includes(':');",
85
+ ' const eventProps: Record<string, unknown> = {};',
86
+ ' const elementProps: Record<string, unknown> = {};',
87
+ '',
88
+ ' for (const [key, value] of Object.entries(props as Record<string, unknown>)) {',
89
+ " if (key === 'children') {",
90
+ ' continue;',
91
+ ' }',
92
+ '',
93
+ ' if (isEventBindingKey(key)) {',
94
+ ' eventProps[key] = value;',
95
+ ' continue;',
96
+ ' }',
97
+ '',
98
+ ' elementProps[key] = value;',
99
+ ' }',
100
+ ];
101
+ const mappedEventLines = componentDefinition.events.length > 0
102
+ ? [
103
+ '',
104
+ ` const mappedEventPropKeys = new Set(${JSON.stringify(componentDefinition.events.map((event) => toEventPropName(event.name)))});`,
105
+ ' const passthroughEventProps = Object.fromEntries(',
106
+ ' Object.entries(eventProps).filter(',
107
+ ' ([key]) => !mappedEventPropKeys.has(key),',
108
+ ' ),',
109
+ ' );',
110
+ '',
111
+ ' const events: Record<string, QRL<(...args: any[]) => void>> = {};',
112
+ ...componentDefinition.events.map((event) => ` if (props.${toEventPropName(event.name)}) { events['${event.name}'] = props.${toEventPropName(event.name)}; }`),
113
+ ' const mappedEvents = Object.keys(events).length > 0 ? events : undefined;',
114
+ ]
115
+ : [
116
+ '',
117
+ ' const passthroughEventProps = eventProps;',
118
+ ' const mappedEvents = undefined;',
119
+ ];
120
+ lines.push(`export interface ${propsTypeName} {`, ...propLines, '}', '', `export const ${wrapperName} = component$<${propsTypeName}>((props) => {`, ...splitPropsLines, ...mappedEventLines, '', ' return (', ` <${bridgeName}`, ` tagName=${JSON.stringify(componentTag)}`, ' props={elementProps}', ' events={mappedEvents}', ` slots={${slotListToken}}`, ' {...passthroughEventProps}', ' >', ...slotLines, ` </${bridgeName}>`, ' );', '});');
121
+ lines.push('');
122
+ return lines.join('\n');
123
+ }
124
+ function renderComponentWrapperRelativePath(componentTag, _ssrAvailable) {
125
+ return `${componentTag}.tsx`;
126
+ }
127
+ function renderRuntimeBarrelModule(projectId, ssrAvailable) {
128
+ const runtimeExportLine = ssrAvailable
129
+ ? "export * from './runtime-ssr.generated';"
130
+ : "export * from './runtime-csr.generated';";
131
+ return [
132
+ `// Generated by @qwik-custom-elements/core. Project: ${projectId ?? 'unknown'}.`,
133
+ '// Do not edit this file directly. Use a manual extension layer.',
134
+ '',
135
+ runtimeExportLine,
136
+ '',
137
+ ].join('\n');
138
+ }
139
+ function renderClientRuntimeModule(projectId, libraryName, libraryImport) {
140
+ const csrBridgeName = toBridgeComponentName(libraryName, 'CSR', 'GeneratedLitCSRComponent');
141
+ const clientSetupName = toCsrClientSetupHookName(libraryName);
142
+ const hasLibraryImport = libraryImport !== undefined && libraryImport.trim().length > 0;
143
+ const lines = [
144
+ `// Generated by @qwik-custom-elements/core. Project: ${projectId ?? 'unknown'}.`,
145
+ '// Do not edit this file directly. Use a manual extension layer.',
146
+ '',
147
+ ];
148
+ if (hasLibraryImport) {
149
+ lines.push("import { $ } from '@builder.io/qwik';");
150
+ lines.push(`import { createLitCSRClientSetup, createLitCSRComponent } from '@qwik-custom-elements/adapter-lit/client';`);
151
+ }
152
+ else {
153
+ lines.push("import { createLitCSRComponent } from '@qwik-custom-elements/adapter-lit/client';");
154
+ }
155
+ if (hasLibraryImport) {
156
+ lines.push('', `const importLibrary = async (): Promise<void> => {`, ` await import('${libraryImport}');`, `};`, `const importLibraryQrl = $(importLibrary);`, `export const ${clientSetupName} = createLitCSRClientSetup(importLibraryQrl);`);
157
+ }
158
+ lines.push('', `export const ${csrBridgeName} = createLitCSRComponent();`, '');
159
+ return lines.join('\n');
160
+ }
161
+ function renderServerRuntimeModule(projectId, libraryName, libraryImport) {
162
+ const ssrBridgeName = toBridgeComponentName(libraryName, 'SSR', 'GeneratedLitComponent');
163
+ const clientSetupName = toClientSetupHookName(libraryName);
164
+ const hasLibraryImport = libraryImport !== undefined && libraryImport.trim().length > 0;
165
+ const lines = [
166
+ `// Generated by @qwik-custom-elements/core. Project: ${projectId ?? 'unknown'}.`,
167
+ '// Do not edit this file directly. Use a manual extension layer.',
168
+ '',
169
+ ];
170
+ if (hasLibraryImport) {
171
+ lines.push("import { $ } from '@builder.io/qwik';");
172
+ lines.push("import { createLitSSRClientSetup } from '@qwik-custom-elements/adapter-lit/client';");
173
+ }
174
+ lines.push("import { createLitSSRComponent } from '@qwik-custom-elements/adapter-lit/ssr';");
175
+ if (hasLibraryImport) {
176
+ lines.push(`// Register Lit element classes for server-side rendering.`, `import '${libraryImport}';`, '', `const importLibrary = async (): Promise<void> => {`, ` await import('${libraryImport}');`, `};`, `const importLibraryQrl = $(importLibrary);`, `export const ${clientSetupName} = createLitSSRClientSetup(importLibraryQrl);`);
177
+ }
178
+ lines.push('', `export const ${ssrBridgeName} = createLitSSRComponent();`, '');
179
+ return lines.join('\n');
180
+ }
181
+ function toClientSetupHookName(libraryName) {
182
+ if (!libraryName || libraryName.trim().length === 0) {
183
+ return 'useLitSSRClientSetup';
184
+ }
185
+ return `use${toPascalCase(libraryName)}SSRClientSetup`;
186
+ }
187
+ function toCsrClientSetupHookName(libraryName) {
188
+ if (!libraryName || libraryName.trim().length === 0) {
189
+ return 'useLitCSRClientSetup';
190
+ }
191
+ return `use${toPascalCase(libraryName)}CSRClientSetup`;
192
+ }
193
+ function toPascalCase(name) {
194
+ return name
195
+ .split(/[-_]/)
196
+ .filter((p) => p.length > 0)
197
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
198
+ .join('');
199
+ }
200
+ function isValidIdentifier(value) {
201
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
202
+ }
203
+ function toEventPropName(eventName) {
204
+ return `on${toPascalCase(eventName)}$`;
205
+ }
206
+ function normalizeComponentDefinition(componentTag, input) {
207
+ return {
208
+ tagName: input?.tagName ?? componentTag,
209
+ props: input?.props ?? [],
210
+ events: input?.events ?? [],
211
+ slots: input?.slots ?? [],
212
+ };
213
+ }
214
+ function toBridgeComponentName(libraryName, mode, fallback) {
215
+ if (!libraryName || libraryName.trim().length === 0) {
216
+ return fallback;
217
+ }
218
+ return `${toPascalCase(libraryName)}${mode}BridgeComponent`;
219
+ }
220
+ function toWrapperName(componentTag) {
221
+ const parts = componentTag.split('-').filter((part) => part.length > 0);
222
+ const normalizedName = parts
223
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
224
+ .join('');
225
+ return `Qwik${normalizedName}`;
226
+ }
@@ -0,0 +1,46 @@
1
+ export declare const metadata: {
2
+ adapterId: string;
3
+ supportedSourceTypes: string[];
4
+ supportsSsrProbe: boolean;
5
+ ssrRuntimeSubpath: string;
6
+ };
7
+ interface ResolveRuntimeImportsInput {
8
+ source: {
9
+ type: 'CEM' | 'PACKAGE_NAME';
10
+ packageName?: string;
11
+ };
12
+ adapterOptions?: Record<string, unknown>;
13
+ }
14
+ export declare function resolveRuntimeImports({ source, adapterOptions, }: ResolveRuntimeImportsInput): Promise<{
15
+ libraryImport?: string;
16
+ }>;
17
+ export declare function probeSSR(): Promise<{
18
+ available: boolean;
19
+ }>;
20
+ export declare function createGeneratedOutput(input: {
21
+ projectId?: string;
22
+ libraryName?: string;
23
+ componentDefinitions?: Array<{
24
+ tagName: string;
25
+ props: Array<{
26
+ name: string;
27
+ type: string;
28
+ required: boolean;
29
+ }>;
30
+ events: Array<{
31
+ name: string;
32
+ type: string;
33
+ }>;
34
+ slots: Array<{
35
+ name: string;
36
+ }>;
37
+ }>;
38
+ runtimeImports?: {
39
+ libraryImport?: string;
40
+ };
41
+ ssrAvailable?: boolean;
42
+ }): Array<{
43
+ relativePath: string;
44
+ content: string;
45
+ }>;
46
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ import { createLitPlannedWrites } from './generated-output.js';
2
+ export const metadata = {
3
+ adapterId: 'lit',
4
+ supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],
5
+ supportsSsrProbe: true,
6
+ ssrRuntimeSubpath: './ssr',
7
+ };
8
+ export async function resolveRuntimeImports({ source, adapterOptions, }) {
9
+ const runtime = typeof adapterOptions?.runtime === 'object' &&
10
+ adapterOptions.runtime !== null &&
11
+ !Array.isArray(adapterOptions.runtime)
12
+ ? adapterOptions.runtime
13
+ : undefined;
14
+ const libraryImportOverride = typeof runtime?.libraryImport === 'string' &&
15
+ runtime.libraryImport.trim().length > 0
16
+ ? runtime.libraryImport
17
+ : undefined;
18
+ if (source.type === 'CEM') {
19
+ return libraryImportOverride != null
20
+ ? { libraryImport: libraryImportOverride }
21
+ : {};
22
+ }
23
+ const packageName = source.packageName;
24
+ if (typeof packageName !== 'string' || packageName.trim().length === 0) {
25
+ return {};
26
+ }
27
+ return { libraryImport: libraryImportOverride ?? packageName };
28
+ }
29
+ export async function probeSSR() {
30
+ return { available: false };
31
+ }
32
+ export function createGeneratedOutput(input) {
33
+ return createLitPlannedWrites(input);
34
+ }
@@ -0,0 +1,66 @@
1
+ import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
2
+ import { createLitSSRComponent } from './lit-ssr.js';
3
+ export declare const metadata: {
4
+ adapterId: string;
5
+ supportedSourceTypes: string[];
6
+ supportsSsrProbe: boolean;
7
+ ssrRuntimeSubpath: string;
8
+ };
9
+ interface ValidateProjectInput {
10
+ source: {
11
+ type: 'CEM' | 'PACKAGE_NAME';
12
+ packageName?: string;
13
+ };
14
+ adapterOptions?: Record<string, unknown>;
15
+ }
16
+ interface ResolveRuntimeImportsInput extends ValidateProjectInput {
17
+ runtimeResolution?: {
18
+ resolveImportSpecifier?: (specifier: string, packageRoot?: string) => string;
19
+ resolveSourcePackageRoot?: (packageName: string) => string;
20
+ };
21
+ }
22
+ interface ProbeSsrInput {
23
+ runtimeImports?: {
24
+ libraryImport?: unknown;
25
+ };
26
+ }
27
+ export type { LitSSRProps } from './lit-ssr.js';
28
+ export declare function validateProject({ source, adapterOptions, }: ValidateProjectInput): void;
29
+ export declare function resolveRuntimeImports({ source, adapterOptions, runtimeResolution, }: ResolveRuntimeImportsInput): Promise<{
30
+ libraryImport?: string;
31
+ }>;
32
+ export declare function probeSSR(_input?: ProbeSsrInput): Promise<{
33
+ available: boolean;
34
+ }>;
35
+ export declare function renderComponentSsrHtml(options?: {
36
+ tagName?: unknown;
37
+ props?: Record<string, unknown>;
38
+ }): string | null;
39
+ export type LitGeneratedSsrComponent = ReturnType<typeof createLitSSRComponent>;
40
+ export { createLitSSRComponent };
41
+ export declare function createGeneratedOutput(input: {
42
+ projectId?: string;
43
+ libraryName?: string;
44
+ componentDefinitions?: Array<{
45
+ tagName: string;
46
+ props: Array<{
47
+ name: string;
48
+ type: string;
49
+ required: boolean;
50
+ }>;
51
+ events: Array<{
52
+ name: string;
53
+ type: string;
54
+ }>;
55
+ slots: Array<{
56
+ name: string;
57
+ }>;
58
+ }>;
59
+ runtimeImports?: {
60
+ libraryImport?: string;
61
+ };
62
+ ssrAvailable?: boolean;
63
+ }): Array<{
64
+ relativePath: string;
65
+ content: string;
66
+ }>;
@@ -0,0 +1,103 @@
1
+ import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
2
+ import { createLitPlannedWrites } from '../generated-output.js';
3
+ import { createLitSSRComponent } from './lit-ssr.js';
4
+ export const metadata = {
5
+ adapterId: 'lit',
6
+ supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],
7
+ supportsSsrProbe: true,
8
+ ssrRuntimeSubpath: './ssr',
9
+ };
10
+ export function validateProject({ source, adapterOptions, }) {
11
+ const runtime = isRecord(adapterOptions?.runtime)
12
+ ? adapterOptions.runtime
13
+ : undefined;
14
+ validateOptionalRuntimeOverride(runtime, 'libraryImport', 'QCE_LIT_RUNTIME_LIBRARY_IMPORT_OVERRIDE_INVALID', `Lit ${source.type} projects must provide a non-empty adapterOptions.runtime.libraryImport override when the override is set.`);
15
+ }
16
+ export async function resolveRuntimeImports({ source, adapterOptions, runtimeResolution, }) {
17
+ const runtime = isRecord(adapterOptions?.runtime)
18
+ ? adapterOptions.runtime
19
+ : undefined;
20
+ const libraryImportOverride = validateOptionalRuntimeOverride(runtime, 'libraryImport', 'QCE_LIT_RUNTIME_LIBRARY_IMPORT_OVERRIDE_INVALID', `Lit ${source.type} projects must provide a non-empty adapterOptions.runtime.libraryImport override when the override is set.`);
21
+ if (source.type === 'CEM') {
22
+ return libraryImportOverride != null
23
+ ? { libraryImport: libraryImportOverride }
24
+ : {};
25
+ }
26
+ const packageName = source.packageName;
27
+ if (typeof packageName !== 'string' || packageName.trim().length === 0) {
28
+ throw createContractError('QCE_LIT_RUNTIME_LIBRARY_IMPORT_REQUIRED', 'Lit PACKAGE_NAME projects must provide source.packageName or adapterOptions.runtime.libraryImport.');
29
+ }
30
+ const rawLibraryImport = libraryImportOverride ?? packageName;
31
+ const libraryImport = runtimeResolution?.resolveImportSpecifier != null
32
+ ? runtimeResolution.resolveImportSpecifier(rawLibraryImport, runtimeResolution.resolveSourcePackageRoot?.(packageName))
33
+ : rawLibraryImport;
34
+ return { libraryImport };
35
+ }
36
+ export async function probeSSR(_input = {}) {
37
+ // SSR is available whenever the Lit SSR adapter subpath can be loaded.
38
+ // @lit-labs/ssr-client/lit-element-hydrate-support.js is imported at module
39
+ // load time above — if this module resolves, the hydration infrastructure is
40
+ // ready. libraryImport is optional and only affects whether the library is
41
+ // pre-registered before server rendering.
42
+ return { available: true };
43
+ }
44
+ export function renderComponentSsrHtml(options = {}) {
45
+ // Keep fallback and hard-failure deterministic for contract tests.
46
+ if (options.tagName == null) {
47
+ return null;
48
+ }
49
+ if (typeof options.tagName !== 'string' ||
50
+ options.tagName.trim().length === 0) {
51
+ throw createContractError('QCE_LIT_RUNTIME_TAGNAME_INVALID', 'Lit SSR render contract requires options.tagName to be a non-empty string when provided.');
52
+ }
53
+ const tagName = options.tagName.trim();
54
+ const serializedProps = serializePropsForHtml(options.props ?? {});
55
+ const openingTag = serializedProps.length
56
+ ? `<${tagName} ${serializedProps}>`
57
+ : `<${tagName}>`;
58
+ return `${openingTag}</${tagName}>`;
59
+ }
60
+ export { createLitSSRComponent };
61
+ function isRecord(value) {
62
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
63
+ }
64
+ function isNonEmptyString(value) {
65
+ return typeof value === 'string' && value.trim().length > 0;
66
+ }
67
+ function validateOptionalRuntimeOverride(runtime, field, errorCode, errorMessage) {
68
+ if (!isRecord(runtime) || !(field in runtime)) {
69
+ return undefined;
70
+ }
71
+ const value = runtime[field];
72
+ if (!isNonEmptyString(value)) {
73
+ throw createContractError(errorCode, errorMessage);
74
+ }
75
+ return value;
76
+ }
77
+ function createContractError(code, message) {
78
+ const error = new Error(message);
79
+ error.code = code;
80
+ return error;
81
+ }
82
+ export function createGeneratedOutput(input) {
83
+ return createLitPlannedWrites(input);
84
+ }
85
+ function serializePropsForHtml(props) {
86
+ return Object.entries(props)
87
+ .filter(([key, value]) => key.trim().length > 0 && isSerializableValue(value))
88
+ .map(([key, value]) => {
89
+ if (value === true) {
90
+ return key;
91
+ }
92
+ return `${key}=${JSON.stringify(String(value))}`;
93
+ })
94
+ .join(' ');
95
+ }
96
+ function isSerializableValue(value) {
97
+ if (value == null) {
98
+ return false;
99
+ }
100
+ return (typeof value === 'string' ||
101
+ typeof value === 'number' ||
102
+ typeof value === 'boolean');
103
+ }
@@ -0,0 +1,22 @@
1
+ import { type QRL } from '@builder.io/qwik';
2
+ export interface LitSSRProps {
3
+ tagName: string;
4
+ props?: Record<string, unknown>;
5
+ /**
6
+ * Optional custom-event names exposed by the Lit element.
7
+ * Keys are native event names; values are Qwik QRL handlers.
8
+ */
9
+ events?: Record<string, QRL<(...args: any[]) => void>>;
10
+ /**
11
+ * Named slot names exposed by the Lit element (not including the default slot).
12
+ */
13
+ slots?: string[];
14
+ [key: string]: unknown;
15
+ }
16
+ /**
17
+ * Creates a Qwik component that renders a Lit custom element with SSR support.
18
+ * On the server, uses `@lit-labs/ssr` to render the element with its shadow DOM
19
+ * as declarative shadow DOM (DSD). On the client, the element self-hydrates and
20
+ * Qwik wires props and events.
21
+ */
22
+ export declare function createLitSSRComponent(): import("@builder.io/qwik").Component<LitSSRProps>;
@@ -0,0 +1,171 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@builder.io/qwik/jsx-runtime";
2
+ import { $, component$, isBrowser, isServer, Slot, SSRRaw, SSRStream, useId, useOnDocument, useSignal, useTask$, } from '@builder.io/qwik';
3
+ import { updateLitCSRHostProps } from '../client/lit-csr-props.js';
4
+ const EVENT_QRL_IDS = new WeakMap();
5
+ let eventQrlIdCounter = 0;
6
+ function getEventQrlId(qrl) {
7
+ if (!qrl || (typeof qrl !== 'object' && typeof qrl !== 'function')) {
8
+ return -1;
9
+ }
10
+ const qrlObj = qrl;
11
+ const existing = EVENT_QRL_IDS.get(qrlObj);
12
+ if (existing) {
13
+ return existing;
14
+ }
15
+ const next = ++eventQrlIdCounter;
16
+ EVENT_QRL_IDS.set(qrlObj, next);
17
+ return next;
18
+ }
19
+ function getEventEntries(events) {
20
+ return Object.entries(events ?? {}).filter(([eventName, eventQrl]) => eventName.trim().length > 0 && Boolean(eventQrl));
21
+ }
22
+ function getEventsDependencyKey(events) {
23
+ return getEventEntries(events)
24
+ .map(([eventName, eventQrl]) => `${eventName}:${getEventQrlId(eventQrl)}`)
25
+ .sort()
26
+ .join('|');
27
+ }
28
+ function getWrapperElement(wrapperId) {
29
+ if (!isBrowser)
30
+ return undefined;
31
+ return (document.querySelector(`[data-lit-wrapper-id="${wrapperId}"]`) ?? undefined);
32
+ }
33
+ function getLitElement(wrapper, tagName) {
34
+ return wrapper?.querySelector(tagName) ?? undefined;
35
+ }
36
+ function serializePropsToAttributes(props) {
37
+ return Object.entries(props)
38
+ .filter(([key, value]) => key.trim().length > 0 && isSerializableAttributeValue(value))
39
+ .map(([key, value]) => {
40
+ if (value === true) {
41
+ return key;
42
+ }
43
+ return `${key}=${JSON.stringify(String(value))}`;
44
+ })
45
+ .join(' ');
46
+ }
47
+ function isSerializableAttributeValue(value) {
48
+ if (value == null) {
49
+ return false;
50
+ }
51
+ return (typeof value === 'string' ||
52
+ typeof value === 'number' ||
53
+ typeof value === 'boolean');
54
+ }
55
+ /**
56
+ * Creates a Qwik component that renders a Lit custom element with SSR support.
57
+ * On the server, uses `@lit-labs/ssr` to render the element with its shadow DOM
58
+ * as declarative shadow DOM (DSD). On the client, the element self-hydrates and
59
+ * Qwik wires props and events.
60
+ */
61
+ export function createLitSSRComponent() {
62
+ return component$(({ tagName, props, events, slots, ...restProps }) => {
63
+ const wrapperRef = useSignal(undefined);
64
+ const wrapperId = useId();
65
+ const clientReady = useSignal(false);
66
+ const namedSlots = slots ?? [];
67
+ useOnDocument('qinit', $(() => {
68
+ clientReady.value = true;
69
+ }));
70
+ // Sync props to the Lit element on the client whenever they change.
71
+ useTask$(({ track }) => {
72
+ const trackedProps = track(() => props);
73
+ if (!isBrowser)
74
+ return;
75
+ const wrapper = getWrapperElement(wrapperId) ?? wrapperRef.value;
76
+ const litEl = getLitElement(wrapper, tagName);
77
+ updateLitCSRHostProps(litEl, trackedProps);
78
+ });
79
+ // Wire event listeners after the client is ready (post-qinit).
80
+ useTask$(({ cleanup, track }) => {
81
+ const ready = track(() => clientReady.value);
82
+ const eventsDependencyKey = track(() => getEventsDependencyKey(events));
83
+ if (!isBrowser || !ready)
84
+ return;
85
+ const wrapper = getWrapperElement(wrapperId) ?? wrapperRef.value;
86
+ const litEl = getLitElement(wrapper, tagName);
87
+ const eventEntries = getEventEntries(events);
88
+ if (!litEl ||
89
+ eventEntries.length === 0 ||
90
+ eventsDependencyKey.length === 0) {
91
+ return;
92
+ }
93
+ let disposed = false;
94
+ const listeners = [];
95
+ cleanup(() => {
96
+ disposed = true;
97
+ for (const { eventName, listener } of listeners) {
98
+ litEl.removeEventListener(eventName, listener);
99
+ }
100
+ });
101
+ for (const [eventName, eventQrl] of eventEntries) {
102
+ const listener = (event) => {
103
+ const qrl = eventQrl;
104
+ const containerEl = litEl.closest('[q\\:container]');
105
+ if (containerEl) {
106
+ qrl.$setContainer$?.(containerEl);
107
+ }
108
+ void Promise.resolve(eventQrl(event, litEl)).catch(console.error);
109
+ };
110
+ if (disposed)
111
+ return;
112
+ litEl.addEventListener(eventName, listener);
113
+ listeners.push({ eventName, listener });
114
+ }
115
+ });
116
+ if (isServer) {
117
+ return (_jsx("div", { ref: wrapperRef, "data-lit-wrapper-id": wrapperId, style: { display: 'contents' }, ...restProps, children: _jsx(SSRStream, { children: async function* () {
118
+ // Dynamic imports ensure these server-only packages are not
119
+ // bundled into client code.
120
+ const { render } = await import('@lit-labs/ssr');
121
+ const { html } = await import('lit');
122
+ const propsHtml = serializePropsToAttributes(props ?? {});
123
+ const staticTemplate = propsHtml
124
+ ? `<${tagName} ${propsHtml}></${tagName}>`
125
+ : `<${tagName}></${tagName}>`;
126
+ // Build a TemplateResult from a static string so @lit-labs/ssr
127
+ // can invoke registered element renderers and produce DSD.
128
+ // unsafeHTML() bypasses element renderer lookup; calling html()
129
+ // directly with a static-strings array does not.
130
+ const strings = Object.assign([staticTemplate], {
131
+ raw: [staticTemplate],
132
+ });
133
+ // Collect the full SSR output from @lit-labs/ssr.
134
+ let fullHtml = '';
135
+ for (const chunk of render(html(strings))) {
136
+ fullHtml += String(chunk);
137
+ }
138
+ // Inject Qwik slot content as light-DOM children of the element
139
+ // (before the closing tag) so the shadow DOM <slot> outlets
140
+ // can pick them up after client hydration.
141
+ const closingTag = `</${tagName}>`;
142
+ const closingTagIdx = fullHtml.lastIndexOf(closingTag);
143
+ if (closingTagIdx === -1) {
144
+ // No closing tag found — emit as-is and append slot content.
145
+ yield _jsx(SSRRaw, { data: fullHtml });
146
+ yield _jsx(Slot, {});
147
+ for (const slotName of namedSlots) {
148
+ yield (_jsx("span", { slot: slotName, style: { display: 'contents' }, children: _jsx(Slot, { name: slotName }) }));
149
+ }
150
+ }
151
+ else {
152
+ // Emit everything before the closing tag.
153
+ yield _jsx(SSRRaw, { data: fullHtml.slice(0, closingTagIdx) });
154
+ // Project default and named slot content.
155
+ yield _jsx(Slot, {});
156
+ for (const slotName of namedSlots) {
157
+ yield (_jsx("span", { slot: slotName, style: { display: 'contents' }, children: _jsx(Slot, { name: slotName }) }));
158
+ }
159
+ // Emit the closing tag.
160
+ yield _jsx(SSRRaw, { data: closingTag });
161
+ }
162
+ } }) }));
163
+ }
164
+ const HostTag = tagName;
165
+ // Client path: the Lit element is already present in the DOM from SSR and
166
+ // will self-hydrate using the declarative shadow DOM. Render the same host
167
+ // custom element shape as SSR so Qwik reconciliation keeps slot content
168
+ // inside the element across signal-driven rerenders.
169
+ return (_jsx("div", { ref: wrapperRef, "data-lit-wrapper-id": wrapperId, style: { display: 'contents' }, ...restProps, children: _jsxs(HostTag, { children: [_jsx(Slot, {}), namedSlots.map((name) => (_jsx("span", { slot: name, style: { display: 'contents' }, children: _jsx(Slot, { name: name }) }, name)))] }) }));
170
+ });
171
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Client-side Lit SSR hydration support.
3
+ *
4
+ * Import this module (or ensure it is evaluated) **before** any Lit custom
5
+ * elements are defined via `customElements.define()`. It patches `LitElement`
6
+ * so that elements whose shadow root was already created from a Declarative
7
+ * Shadow DOM (DSD) template — produced by `@lit-labs/ssr` on the server — are
8
+ * hydrated in place rather than being re-rendered from scratch.
9
+ *
10
+ * Without this patch, Lit's first `performUpdate()` call appends a fresh render
11
+ * tree alongside the existing DSD content, producing a duplicate shadow DOM
12
+ * (e.g., two `.de-alert` divs visible to the user).
13
+ */
14
+ import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Client-side Lit SSR hydration support.
3
+ *
4
+ * Import this module (or ensure it is evaluated) **before** any Lit custom
5
+ * elements are defined via `customElements.define()`. It patches `LitElement`
6
+ * so that elements whose shadow root was already created from a Declarative
7
+ * Shadow DOM (DSD) template — produced by `@lit-labs/ssr` on the server — are
8
+ * hydrated in place rather than being re-rendered from scratch.
9
+ *
10
+ * Without this patch, Lit's first `performUpdate()` call appends a fresh render
11
+ * tree alongside the existing DSD content, producing a duplicate shadow DOM
12
+ * (e.g., two `.de-alert` divs visible to the user).
13
+ */
14
+ import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@qwik-custom-elements/adapter-lit",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "types": "./dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./client": {
12
+ "types": "./dist/client/index.d.ts",
13
+ "import": "./dist/client/index.js"
14
+ },
15
+ "./ssr": {
16
+ "types": "./dist/ssr/index.d.ts",
17
+ "import": "./dist/ssr/index.js"
18
+ },
19
+ "./ssr-client": {
20
+ "types": "./dist/ssr-client.d.ts",
21
+ "import": "./dist/ssr-client.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "peerDependencies": {
28
+ "@builder.io/qwik": "^1.19.2",
29
+ "@lit-labs/ssr": ">=4.0.0",
30
+ "@lit-labs/ssr-client": ">=1.1.0",
31
+ "lit": ">=3.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@lit-labs/ssr": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "@builder.io/qwik": "^1.19.2",
40
+ "@lit-labs/ssr": "^4.0.0",
41
+ "@lit-labs/ssr-client": "^1.1.8",
42
+ "lit": "^3.3.2",
43
+ "typescript": "^5.9.3",
44
+ "vitest": "^3.2.4"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.build.json",
51
+ "check-types": "tsc -p tsconfig.build.json --noEmit && tsc -p tsconfig.test.json --noEmit",
52
+ "dev": "tsc -p tsconfig.build.json --watch",
53
+ "format": "prettier --write . --config ../../.prettierrc.json --ignore-path ../../.prettierignore",
54
+ "format:check": "prettier --check . --config ../../.prettierrc.json --ignore-path ../../.prettierignore",
55
+ "lint": "eslint \"**/*.{js,mjs,cjs,ts,mts,cts,tsx}\" --no-error-on-unmatched-pattern",
56
+ "e2e": "node -e \"console.log('adapter-lit e2e noop')\"",
57
+ "test": "vitest run"
58
+ }
59
+ }