@ecopages/lit 0.2.0-beta.3 → 0.2.0-beta.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/lit",
3
- "version": "0.2.0-beta.3",
3
+ "version": "0.2.0-beta.30",
4
4
  "description": "Ecopages lit integration",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -20,18 +20,26 @@
20
20
  "default": "./src/styled-mixin.js",
21
21
  "types": "./src/styled-mixin.d.ts"
22
22
  },
23
+ "./static-render-worker": {
24
+ "default": "./src/lit-static-render-worker.js",
25
+ "types": "./src/lit-static-render-worker.d.ts"
26
+ },
23
27
  "./styled-mixin.ts": {
24
28
  "default": "./src/styled-mixin.js",
25
29
  "types": "./src/styled-mixin.d.ts"
30
+ },
31
+ "./static-render-worker.ts": {
32
+ "default": "./src/lit-static-render-worker.js",
33
+ "types": "./src/lit-static-render-worker.d.ts"
26
34
  }
27
35
  },
28
36
  "repository": {
29
37
  "type": "git",
30
- "url": "https://github.com/ecopages/ecopages.git",
38
+ "url": "git+https://github.com/ecopages/ecopages.git",
31
39
  "directory": "packages/integrations/lit"
32
40
  },
33
41
  "peerDependencies": {
34
- "@ecopages/core": "0.2.0-beta.3",
42
+ "@ecopages/core": "0.2.0-beta.30",
35
43
  "@lit-labs/ssr": "^3.3.0",
36
44
  "@lit-labs/ssr-client": "^1.1.7",
37
45
  "lit": "^3.2.1"
@@ -0,0 +1,69 @@
1
+ # Lit static render worker
2
+
3
+ Lit-first page routes (`.lit.tsx`) render through a dedicated worker thread during static export and runtime when the Lit plugin is active.
4
+
5
+ ## Flow
6
+
7
+ ```
8
+ Main thread Worker thread
9
+ ─────────── ─────────────
10
+ LitPlugin.setup()
11
+ └─ LitStaticRenderSession.ensureWorker()
12
+ └─ LitStaticRenderWorkerClient.start()
13
+ └─ postMessage(init) ──► setupAppRuntimePlugins()
14
+ installBuildRuntime()
15
+ RouteRendererFactory
16
+
17
+ LitPlugin.initializeRenderer()
18
+ └─ LitRenderer({ getRenderSession }) lazy accessor (not constructor snapshot)
19
+
20
+ LitRenderer.execute() (main)
21
+ └─ ensureIntegrationRuntimeActivated()
22
+ └─ getRenderSession()
23
+ ├─ locals present ──────────► super.execute() (in-process)
24
+ └─ session.renderPageInWorker()
25
+ └─ postMessage(render) ─────► RouteRendererFactory.execute()
26
+ (no active session in worker)
27
+
28
+ beforeStaticExport
29
+ └─ ensureWorker() only when `renderSession` is still null
30
+ └─ preloadStaticRoutes()
31
+ └─ preload SSR lazy scripts for Lit pages
32
+ ```
33
+
34
+ `ECOPAGES_LIT_STATIC_RENDER_WORKER=true` in the worker environment prevents nested worker creation during `LitPlugin.setup()` and skips processor `.setup()` inside `setupAppRuntimePlugins` (main thread already prepared artifacts).
35
+
36
+ ## Ownership
37
+
38
+ | Concern | Owner |
39
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
40
+ | Session + worker lifecycle | `LitPlugin` (`renderSession`) |
41
+ | Lazy session lookup | `LitRenderer` via `getRenderSession` |
42
+ | Worker identity | First `{ configModulePath, runtimeOrigin }` passed to `ensureWorker`; same identity is sticky; a later `ensureWorker` with a different identity disposes and recreates |
43
+
44
+ Renderers created before plugin `setup()` still use the worker after setup assigns the session, because `execute` activates the integration runtime then reads the accessor.
45
+
46
+ `beforeStaticExport` does not re-call `ensureWorker` when setup already created the session, so a different `context.baseUrl` at export time will not recreate an existing worker.
47
+
48
+ ## Locals policy
49
+
50
+ When `RouteRendererOptions.locals` is present, `LitRenderer.execute` forces the in-process path (`super.execute`). Request locals are not sent through the worker protocol (structured-clone risk and request-scoped data). Static export and routes without locals continue through the worker.
51
+
52
+ ## Files
53
+
54
+ | File | Role |
55
+ | ------------------------------------ | --------------------------------------------------------------- |
56
+ | `lit-static-render-session.ts` | Plugin-runtime-scoped session: worker lifecycle + SSR preload |
57
+ | `lit-static-render-worker-client.ts` | Message protocol client |
58
+ | `lit-static-render-worker.ts` | Worker entry (exported as `@ecopages/lit/static-render-worker`) |
59
+ | `lit-static-render-protocol.ts` | Request/response message types |
60
+
61
+ ## Protocol
62
+
63
+ | Message | Direction | Purpose |
64
+ | ------------------ | ------------- | ----------------------------------------------------------------------------- |
65
+ | `init` | main → worker | Load `eco.config.ts`, bootstrap runtime (`configModulePath`, `runtimeOrigin`) |
66
+ | `ready` | worker → main | Worker initialized |
67
+ | `render` | main → worker | Render one Lit page route (`filePath`, `PageParams`, optional `PageQuery`) |
68
+ | `result` / `error` | worker → main | HTML body plus optional `cacheStrategy`, or an error message |
69
+ | `shutdown` | main → worker | Exit worker thread |
@@ -2,19 +2,41 @@
2
2
  * This module contains the Lit renderer
3
3
  * @module
4
4
  */
5
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPagesElement, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
5
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPagesElement, IntegrationRendererRenderOptions, RouteRenderResult, RouteRendererBody, RouteRendererOptions } from '@ecopages/core';
6
6
  import './dom-shim.js';
7
- import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
7
+ import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/orchestration/integration-renderer';
8
+ import type { LitStaticRenderSession } from './lit-static-render-session.js';
9
+ export type LitRendererOptions = ConstructorParameters<typeof IntegrationRenderer>[0] & {
10
+ /**
11
+ * Lazy session lookup owned by {@link LitPlugin}.
12
+ *
13
+ * @remarks
14
+ * Must not snapshot the session at construction time. Plugin `setup()` may
15
+ * assign the session after the renderer is created; `execute` activates the
16
+ * integration runtime then reads this accessor so worker dispatch still works.
17
+ */
18
+ getRenderSession?: () => LitStaticRenderSession | undefined;
19
+ };
8
20
  /**
9
21
  * A renderer for the Lit integration.
10
22
  */
11
23
  export declare class LitRenderer extends IntegrationRenderer<EcoPagesElement> {
12
24
  name: string;
25
+ private readonly getRenderSession?;
26
+ constructor(options: LitRendererOptions);
27
+ /**
28
+ * Renders a Lit page route, preferring the plugin worker when available.
29
+ *
30
+ * @remarks
31
+ * Activates the integration runtime before reading {@link getRenderSession}
32
+ * so a renderer constructed before plugin setup still sees the session.
33
+ * When `options.locals` is present, rendering stays in-process: request locals
34
+ * are not structured-cloned across the worker boundary.
35
+ */
36
+ execute(options: RouteRendererOptions): Promise<RouteRenderResult>;
13
37
  private isFunctionComponent;
14
38
  private resolveQueuedForeignSubtreeChildren;
15
- private resolveQueuedForeignSubtreeHtml;
16
- protected shouldRenderPageComponent(): boolean;
17
- private isLitManagedComponent;
39
+ private renderLitQueuedForeignSubtreeChildren;
18
40
  /**
19
41
  * Renders a Lit component for component-level orchestration.
20
42
  *
@@ -1,5 +1,11 @@
1
1
  import "./dom-shim.js";
2
- import { IntegrationRenderer } from "@ecopages/core/route-renderer/integration-renderer";
2
+ import {
3
+ IntegrationRenderer
4
+ } from "@ecopages/core/route-renderer/orchestration/integration-renderer";
5
+ import {
6
+ getForeignSubtreeResolutionContextKey,
7
+ resolveOwningIntegrationRenderer
8
+ } from "@ecopages/core/route-renderer/orchestration/foreign-child/owning-renderer-resolution";
3
9
  import { LitSsrLazyPreloader } from "./lit-ssr-lazy-preloader.js";
4
10
  import { LIT_PLUGIN_NAME } from "./lit.constants.js";
5
11
  import {
@@ -10,6 +16,38 @@ import {
10
16
  } from "./utils/lit-html-rendering.js";
11
17
  class LitRenderer extends IntegrationRenderer {
12
18
  name = LIT_PLUGIN_NAME;
19
+ getRenderSession;
20
+ constructor(options) {
21
+ const { getRenderSession, ...rendererOptions } = options;
22
+ super(rendererOptions);
23
+ this.getRenderSession = getRenderSession;
24
+ }
25
+ /**
26
+ * Renders a Lit page route, preferring the plugin worker when available.
27
+ *
28
+ * @remarks
29
+ * Activates the integration runtime before reading {@link getRenderSession}
30
+ * so a renderer constructed before plugin setup still sees the session.
31
+ * When `options.locals` is present, rendering stays in-process: request locals
32
+ * are not structured-cloned across the worker boundary.
33
+ */
34
+ async execute(options) {
35
+ await this.ensureIntegrationRuntimeActivated();
36
+ const renderSession = this.getRenderSession?.();
37
+ if (renderSession && options.locals === void 0) {
38
+ const params = options.params ?? {};
39
+ const result = await renderSession.renderPageInWorker({
40
+ filePath: options.file,
41
+ params,
42
+ query: options.query
43
+ });
44
+ return {
45
+ body: result.html,
46
+ cacheStrategy: result.cacheStrategy
47
+ };
48
+ }
49
+ return super.execute(options);
50
+ }
13
51
  isFunctionComponent(component) {
14
52
  return typeof component === "function";
15
53
  }
@@ -28,44 +66,23 @@ class LitRenderer extends IntegrationRenderer {
28
66
  );
29
67
  return renderedChildren;
30
68
  }
31
- async resolveQueuedForeignSubtreeHtml(html, runtimeContext) {
32
- const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
33
- currentIntegrationName: this.name,
34
- html,
35
- runtimeContext,
36
- queueLabel: "Lit",
37
- getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
38
- applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
39
- dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
40
- renderQueuedChildren: async (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => {
41
- const renderedChildren = await this.resolveQueuedForeignSubtreeChildren(
42
- children,
43
- queuedResolutionsByToken,
44
- resolveToken
45
- );
46
- if (typeof renderedChildren !== "string") {
47
- return {
48
- assets: [],
49
- children: renderedChildren
50
- };
51
- }
52
- return {
53
- assets: [],
54
- html: renderedChildren
55
- };
56
- }
57
- });
69
+ async renderLitQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken) {
70
+ const renderedChildren = await this.resolveQueuedForeignSubtreeChildren(
71
+ children,
72
+ queuedResolutionsByToken,
73
+ resolveToken
74
+ );
75
+ if (typeof renderedChildren !== "string") {
76
+ return {
77
+ assets: [],
78
+ children: renderedChildren
79
+ };
80
+ }
58
81
  return {
59
- html: queuedForeignSubtreeResolution.html,
60
- assets: queuedForeignSubtreeResolution.assets.length > 0 ? queuedForeignSubtreeResolution.assets : void 0
82
+ assets: [],
83
+ html: renderedChildren
61
84
  };
62
85
  }
63
- shouldRenderPageComponent() {
64
- return false;
65
- }
66
- isLitManagedComponent(component) {
67
- return component?.config?.integration === this.name || component?.config?.__eco?.integration === this.name;
68
- }
69
86
  /**
70
87
  * Renders a Lit component for component-level orchestration.
71
88
  *
@@ -95,10 +112,26 @@ class LitRenderer extends IntegrationRenderer {
95
112
  const content = await component(props);
96
113
  const renderedHtml = await renderLitValueToString(content);
97
114
  const html = renderedChildren === void 0 ? renderedHtml : injectLitRenderedChildren(renderedHtml, renderedChildren);
98
- const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtreeHtml(
115
+ const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
116
+ currentIntegrationName: this.name,
99
117
  html,
100
- this.getQueuedForeignSubtreeResolutionContext(input)
101
- );
118
+ runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(
119
+ input,
120
+ getForeignSubtreeResolutionContextKey(this.name)
121
+ ),
122
+ queueLabel: "Lit",
123
+ getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
124
+ appConfig: this.appConfig,
125
+ runtimeOrigin: this.runtimeOrigin,
126
+ currentIntegrationName: this.name,
127
+ currentRenderer: this,
128
+ integrationName,
129
+ cache: rendererCache
130
+ }),
131
+ applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
132
+ dedupeProcessedAssets: (assets2) => this.htmlTransformer.dedupeProcessedAssets(assets2),
133
+ renderQueuedChildren: (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderLitQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken)
134
+ });
102
135
  const hasDependencies = Boolean(input.component.config?.dependencies);
103
136
  const canResolveAssets = typeof this.assetProcessingService?.processDependencies === "function";
104
137
  const assets = hasDependencies && canResolveAssets ? await this.processComponentDependencies([input.component]) : void 0;
@@ -109,7 +142,7 @@ class LitRenderer extends IntegrationRenderer {
109
142
  integrationName: this.name,
110
143
  assets: this.htmlTransformer.dedupeProcessedAssets([
111
144
  ...assets ?? [],
112
- ...queuedForeignSubtreeResolution.assets ?? []
145
+ ...queuedForeignSubtreeResolution.assets
113
146
  ])
114
147
  };
115
148
  }
@@ -140,6 +173,11 @@ class LitRenderer extends IntegrationRenderer {
140
173
  * Preloads SSR-eligible lazy scripts to register custom elements before render.
141
174
  */
142
175
  async preloadSsrLazyScripts(components) {
176
+ const renderSession = this.getRenderSession?.();
177
+ if (renderSession) {
178
+ await renderSession.preloadSsrLazyScripts(components);
179
+ return;
180
+ }
143
181
  await this.ssrLazyPreloader.preloadSsrLazyScripts(components);
144
182
  }
145
183
  /**
@@ -188,44 +226,16 @@ class LitRenderer extends IntegrationRenderer {
188
226
  }
189
227
  async renderToResponse(view, props, ctx) {
190
228
  try {
191
- if (ctx.partial) {
192
- return this.renderPartialViewResponse({
193
- view,
194
- props,
195
- ctx,
196
- transformHtml: normalizeLitHtml
197
- });
198
- }
199
- const viewConfig = view.config;
200
- const Layout = viewConfig?.layout;
201
- const HtmlTemplate = await this.getHtmlTemplate();
202
- const metadata = await this.resolveViewMetadata(view, props);
203
- const normalizedProps = props ?? {};
229
+ const layouts = view.config?.layouts;
230
+ const Layout = layouts?.[layouts.length - 1];
204
231
  await this.preloadSsrLazyScripts([view, Layout]);
205
- await this.prepareViewDependencies(view, Layout);
206
- const pageRender = await this.renderComponentWithForeignChildren({
207
- component: view,
208
- props: normalizedProps
209
- });
210
- const layoutRender = Layout ? await this.renderComponentWithForeignChildren({
211
- component: Layout,
212
- props: {},
213
- children: pageRender.html
214
- }) : void 0;
215
- const documentRender = await this.renderComponentWithForeignChildren({
216
- component: HtmlTemplate,
217
- props: {
218
- metadata,
219
- pageProps: normalizedProps
220
- },
221
- children: layoutRender?.html ?? pageRender.html
222
- });
223
- this.appendProcessedDependencies(pageRender.assets, layoutRender?.assets, documentRender.assets);
224
- const body = await this.finalizeResolvedHtml({
225
- html: `${this.DOC_TYPE}${normalizeLitHtml(documentRender.html)}`,
226
- partial: false
232
+ return await this.renderViewWithDocumentShell({
233
+ view,
234
+ props,
235
+ ctx,
236
+ layout: Layout,
237
+ transformDocumentHtml: normalizeLitHtml
227
238
  });
228
- return this.createHtmlResponse(body, ctx);
229
239
  } catch (error) {
230
240
  throw this.createRenderError("Error rendering view", error);
231
241
  }
@@ -60,8 +60,8 @@ class LitSsrLazyPreloader {
60
60
  scriptPaths.add(this.resolveDependencyPath(componentDir, script.src));
61
61
  }
62
62
  }
63
- if (config.layout) {
64
- collect(config.layout);
63
+ for (const layout of config.layouts ?? []) {
64
+ collect(layout);
65
65
  }
66
66
  for (const nestedComponent of config.dependencies?.components || []) {
67
67
  collect(nestedComponent);
@@ -0,0 +1,35 @@
1
+ import type { PageParams, PageQuery, RouteRenderResult } from '@ecopages/core';
2
+ export type LitStaticRenderCacheStrategy = RouteRenderResult['cacheStrategy'];
3
+ export type LitStaticRenderWorkerInitMessage = {
4
+ type: 'init';
5
+ configModulePath: string;
6
+ runtimeOrigin: string;
7
+ };
8
+ export type LitStaticRenderWorkerRenderMessage = {
9
+ type: 'render';
10
+ id: string;
11
+ filePath: string;
12
+ /** Route params; multi-value keys stay as `string[]`. */
13
+ params: PageParams;
14
+ /** Serializable route query; preserves multi-value keys as `string[]`. */
15
+ query?: PageQuery;
16
+ };
17
+ export type LitStaticRenderWorkerShutdownMessage = {
18
+ type: 'shutdown';
19
+ };
20
+ export type LitStaticRenderWorkerRequestMessage = LitStaticRenderWorkerInitMessage | LitStaticRenderWorkerRenderMessage | LitStaticRenderWorkerShutdownMessage;
21
+ export type LitStaticRenderWorkerReadyMessage = {
22
+ type: 'ready';
23
+ };
24
+ export type LitStaticRenderWorkerResultMessage = {
25
+ type: 'result';
26
+ id: string;
27
+ html: string;
28
+ cacheStrategy?: LitStaticRenderCacheStrategy;
29
+ };
30
+ export type LitStaticRenderWorkerErrorMessage = {
31
+ type: 'error';
32
+ id?: string;
33
+ message: string;
34
+ };
35
+ export type LitStaticRenderWorkerResponseMessage = LitStaticRenderWorkerReadyMessage | LitStaticRenderWorkerResultMessage | LitStaticRenderWorkerErrorMessage;
File without changes
@@ -0,0 +1,63 @@
1
+ import type { EcoComponent, PageParams, PageQuery } from '@ecopages/core';
2
+ import type { StaticExportContext } from '@ecopages/core/plugins/integration-plugin';
3
+ import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
4
+ import { LitStaticRenderWorkerClient } from './lit-static-render-worker-client.js';
5
+ import type { LitStaticRenderCacheStrategy } from './lit-static-render-protocol.js';
6
+ type LitStaticRenderWorkerClientContract = Pick<LitStaticRenderWorkerClient, 'start' | 'renderPage' | 'dispose'>;
7
+ type LitStaticRenderSessionOptions = {
8
+ resolveDependencyPath: (componentDir: string, sourcePath: string) => string;
9
+ processDependencies?: (dependencies: AssetDefinition[], integrationName: string) => Promise<Array<{
10
+ filepath?: string;
11
+ }>>;
12
+ preferSourceImports?: boolean;
13
+ createWorkerClient?: (input: {
14
+ configModulePath: string;
15
+ runtimeOrigin: string;
16
+ }) => LitStaticRenderWorkerClientContract;
17
+ };
18
+ /**
19
+ * Plugin-runtime-scoped Lit server render session.
20
+ *
21
+ * @remarks
22
+ * Owns worker lifecycle and SSR preload for Lit page routes during static export
23
+ * and runtime/dev requests. Injected into `LitRenderer` via a lazy accessor from
24
+ * `LitPlugin`; there is no process-global coordinator.
25
+ *
26
+ * Worker init identity is sticky for the first `{ configModulePath, runtimeOrigin }`.
27
+ * Later `ensureWorker` calls with the same identity are no-ops; a different identity
28
+ * recreates the worker so stale config/origin is never silently reused.
29
+ */
30
+ export declare class LitStaticRenderSession {
31
+ private readonly preloader;
32
+ private readonly createWorkerClient;
33
+ private workerClient;
34
+ private workerIdentity;
35
+ constructor(options: LitStaticRenderSessionOptions);
36
+ /**
37
+ * Ensures the static render worker is running for the given identity.
38
+ *
39
+ * @remarks
40
+ * First successful start sticks that identity. Matching re-entry is a no-op.
41
+ * A different `configModulePath` or `runtimeOrigin` disposes and recreates.
42
+ */
43
+ ensureWorker(input: {
44
+ configModulePath: string;
45
+ runtimeOrigin: string;
46
+ }): Promise<void>;
47
+ preloadStaticRoutes(context: StaticExportContext): Promise<void>;
48
+ preloadSsrLazyScripts(components: Array<EcoComponent | undefined>): Promise<void>;
49
+ collectSsrPreloadScripts(components: Array<EcoComponent | undefined>): string[];
50
+ renderPageInWorker(input: {
51
+ filePath: string;
52
+ params: PageParams;
53
+ query?: PageQuery;
54
+ }): Promise<{
55
+ html: string;
56
+ cacheStrategy?: LitStaticRenderCacheStrategy;
57
+ }>;
58
+ isExpectedSsrPreloadError(error: unknown): boolean;
59
+ dispose(): Promise<void>;
60
+ private preloadLitRoutes;
61
+ private isLitRoute;
62
+ }
63
+ export {};
@@ -0,0 +1,105 @@
1
+ import path from "node:path";
2
+ import { LitSsrLazyPreloader } from "./lit-ssr-lazy-preloader.js";
3
+ import { LIT_PLUGIN_NAME } from "./lit.constants.js";
4
+ import { LitStaticRenderWorkerClient } from "./lit-static-render-worker-client.js";
5
+ class LitStaticRenderSession {
6
+ preloader;
7
+ createWorkerClient;
8
+ workerClient = null;
9
+ workerIdentity = null;
10
+ constructor(options) {
11
+ this.preloader = new LitSsrLazyPreloader({
12
+ resolveDependencyPath: options.resolveDependencyPath,
13
+ processDependencies: options.processDependencies,
14
+ preferSourceImports: options.preferSourceImports
15
+ });
16
+ this.createWorkerClient = options.createWorkerClient ?? ((input) => new LitStaticRenderWorkerClient({
17
+ configModulePath: input.configModulePath,
18
+ runtimeOrigin: input.runtimeOrigin
19
+ }));
20
+ }
21
+ /**
22
+ * Ensures the static render worker is running for the given identity.
23
+ *
24
+ * @remarks
25
+ * First successful start sticks that identity. Matching re-entry is a no-op.
26
+ * A different `configModulePath` or `runtimeOrigin` disposes and recreates.
27
+ */
28
+ async ensureWorker(input) {
29
+ if (this.workerClient && this.workerIdentity && this.workerIdentity.configModulePath === input.configModulePath && this.workerIdentity.runtimeOrigin === input.runtimeOrigin) {
30
+ return;
31
+ }
32
+ if (this.workerClient) {
33
+ await this.dispose();
34
+ }
35
+ this.workerIdentity = {
36
+ configModulePath: input.configModulePath,
37
+ runtimeOrigin: input.runtimeOrigin
38
+ };
39
+ this.workerClient = this.createWorkerClient({
40
+ configModulePath: input.configModulePath,
41
+ runtimeOrigin: input.runtimeOrigin
42
+ });
43
+ await this.workerClient.start();
44
+ }
45
+ async preloadStaticRoutes(context) {
46
+ await this.preloadLitRoutes(context);
47
+ }
48
+ async preloadSsrLazyScripts(components) {
49
+ await this.preloader.preloadSsrLazyScripts(components);
50
+ }
51
+ collectSsrPreloadScripts(components) {
52
+ return this.preloader.collectSsrPreloadScripts(components);
53
+ }
54
+ async renderPageInWorker(input) {
55
+ if (!this.workerClient) {
56
+ throw new Error("Lit static render worker is not active");
57
+ }
58
+ return this.workerClient.renderPage(input);
59
+ }
60
+ isExpectedSsrPreloadError(error) {
61
+ return this.preloader.isExpectedSsrPreloadError(error);
62
+ }
63
+ async dispose() {
64
+ await this.workerClient?.dispose();
65
+ this.workerClient = null;
66
+ this.workerIdentity = null;
67
+ }
68
+ async preloadLitRoutes(context) {
69
+ if (!context.routeRendererFactory) {
70
+ return;
71
+ }
72
+ const routes = await context.router.listStaticGenerationRoutes({ runtimeOrigin: context.baseUrl });
73
+ const components = [];
74
+ for (const route of routes) {
75
+ const filePath = route.templateRoute.filePath;
76
+ if (!this.isLitRoute(filePath, context.appConfig)) {
77
+ continue;
78
+ }
79
+ const pageRenderer = context.routeRendererFactory.getPageRenderer(filePath);
80
+ const pageModule = await pageRenderer.loadPageModule(filePath);
81
+ const page = pageModule.default;
82
+ if (page) {
83
+ components.push(page);
84
+ for (const layout of page.config?.layouts ?? []) {
85
+ components.push(layout);
86
+ }
87
+ for (const nestedComponent of page.config?.dependencies?.components ?? []) {
88
+ components.push(nestedComponent);
89
+ }
90
+ }
91
+ }
92
+ await this.preloadSsrLazyScripts(components);
93
+ }
94
+ isLitRoute(filePath, appConfig) {
95
+ const litIntegration = appConfig.integrations.find((plugin) => plugin.name === LIT_PLUGIN_NAME);
96
+ if (!litIntegration) {
97
+ return false;
98
+ }
99
+ const normalizedPath = path.normalize(filePath);
100
+ return litIntegration.extensions.some((extension) => normalizedPath.endsWith(extension));
101
+ }
102
+ }
103
+ export {
104
+ LitStaticRenderSession
105
+ };
@@ -0,0 +1,26 @@
1
+ import type { PageParams, PageQuery } from '@ecopages/core';
2
+ import type { LitStaticRenderCacheStrategy } from './lit-static-render-protocol.js';
3
+ export declare class LitStaticRenderWorkerClient {
4
+ private readonly configModulePath;
5
+ private readonly runtimeOrigin;
6
+ private worker;
7
+ private readyPromise;
8
+ private pending;
9
+ private nextRenderId;
10
+ constructor({ configModulePath, runtimeOrigin }: {
11
+ configModulePath: string;
12
+ runtimeOrigin: string;
13
+ });
14
+ private rejectPending;
15
+ start(): Promise<void>;
16
+ renderPage(input: {
17
+ filePath: string;
18
+ params: PageParams;
19
+ query?: PageQuery;
20
+ }): Promise<{
21
+ html: string;
22
+ cacheStrategy?: LitStaticRenderCacheStrategy;
23
+ }>;
24
+ dispose(): Promise<void>;
25
+ }
26
+ export declare const litStaticRenderWorkerEntryPath: string;
@@ -0,0 +1,113 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { fileURLToPath } from "node:url";
3
+ class LitStaticRenderWorkerClient {
4
+ configModulePath;
5
+ runtimeOrigin;
6
+ worker = null;
7
+ readyPromise = null;
8
+ pending = /* @__PURE__ */ new Map();
9
+ nextRenderId = 0;
10
+ constructor({ configModulePath, runtimeOrigin }) {
11
+ this.configModulePath = configModulePath;
12
+ this.runtimeOrigin = runtimeOrigin;
13
+ }
14
+ rejectPending(error) {
15
+ for (const pending of this.pending.values()) {
16
+ pending.reject(error);
17
+ }
18
+ this.pending.clear();
19
+ }
20
+ async start() {
21
+ if (this.readyPromise) {
22
+ return this.readyPromise;
23
+ }
24
+ this.readyPromise = new Promise((resolveReady, rejectReady) => {
25
+ const worker = new Worker(new URL("./lit-static-render-worker.js", import.meta.url), {
26
+ execArgv: [...process.execArgv],
27
+ env: {
28
+ ...process.env,
29
+ ECOPAGES_LIT_STATIC_RENDER_WORKER: "true"
30
+ }
31
+ });
32
+ this.worker = worker;
33
+ worker.on("message", (message) => {
34
+ if (message.type === "ready") {
35
+ resolveReady();
36
+ return;
37
+ }
38
+ if (message.type === "error") {
39
+ if (message.id && this.pending.has(message.id)) {
40
+ const pending = this.pending.get(message.id);
41
+ this.pending.delete(message.id);
42
+ pending.reject(new Error(message.message));
43
+ return;
44
+ }
45
+ rejectReady(new Error(message.message));
46
+ return;
47
+ }
48
+ if (message.type === "result") {
49
+ const pending = this.pending.get(message.id);
50
+ if (!pending) {
51
+ return;
52
+ }
53
+ this.pending.delete(message.id);
54
+ pending.resolve({ html: message.html, cacheStrategy: message.cacheStrategy });
55
+ }
56
+ });
57
+ worker.on("error", (error) => {
58
+ rejectReady(error instanceof Error ? error : new Error(String(error)));
59
+ });
60
+ worker.on("exit", (code) => {
61
+ const exitError = new Error(
62
+ code === 0 ? "Lit static render worker exited" : `Lit static render worker exited with code ${code}`
63
+ );
64
+ this.rejectPending(exitError);
65
+ if (code !== 0) {
66
+ rejectReady(exitError);
67
+ }
68
+ });
69
+ const initMessage = {
70
+ type: "init",
71
+ configModulePath: this.configModulePath,
72
+ runtimeOrigin: this.runtimeOrigin
73
+ };
74
+ worker.postMessage(initMessage);
75
+ });
76
+ return this.readyPromise;
77
+ }
78
+ async renderPage(input) {
79
+ await this.start();
80
+ const worker = this.worker;
81
+ if (!worker) {
82
+ throw new Error("Lit static render worker is not available");
83
+ }
84
+ const id = String(++this.nextRenderId);
85
+ return new Promise((resolve, reject) => {
86
+ this.pending.set(id, { resolve, reject });
87
+ const renderMessage = {
88
+ type: "render",
89
+ id,
90
+ filePath: input.filePath,
91
+ params: input.params,
92
+ query: input.query
93
+ };
94
+ worker.postMessage(renderMessage);
95
+ });
96
+ }
97
+ async dispose() {
98
+ if (!this.worker) {
99
+ return;
100
+ }
101
+ this.rejectPending(new Error("Lit static render worker was disposed"));
102
+ const worker = this.worker;
103
+ this.worker = null;
104
+ this.readyPromise = null;
105
+ worker.postMessage({ type: "shutdown" });
106
+ await worker.terminate();
107
+ }
108
+ }
109
+ const litStaticRenderWorkerEntryPath = fileURLToPath(new URL("./lit-static-render-worker.js", import.meta.url));
110
+ export {
111
+ LitStaticRenderWorkerClient,
112
+ litStaticRenderWorkerEntryPath
113
+ };
@@ -0,0 +1 @@
1
+ import './dom-shim.js';
@@ -0,0 +1,75 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { pathToFileURL } from "node:url";
3
+ import { setupAppRuntimePlugins } from "@ecopages/core/build/build-adapter";
4
+ import { installBuildRuntime } from "@ecopages/core/build/build-runtime";
5
+ import { RouteRendererFactory } from "@ecopages/core/route-renderer/route-renderer";
6
+ import "./dom-shim.js";
7
+ let appConfig = null;
8
+ let routeRendererFactory = null;
9
+ async function initializeWorker(configModulePath, runtimeOrigin) {
10
+ const configModule = await import(
11
+ /* @vite-ignore */
12
+ pathToFileURL(configModulePath).href
13
+ );
14
+ appConfig = configModule.default ?? configModule;
15
+ await setupAppRuntimePlugins({
16
+ appConfig,
17
+ runtimeOrigin
18
+ });
19
+ installBuildRuntime(appConfig);
20
+ routeRendererFactory = new RouteRendererFactory({
21
+ appConfig,
22
+ runtimeOrigin
23
+ });
24
+ }
25
+ async function renderPage(filePath, params, query) {
26
+ if (!routeRendererFactory) {
27
+ throw new Error("Lit static render worker is not initialized");
28
+ }
29
+ const result = await routeRendererFactory.getPageRenderer(filePath).execute({
30
+ file: filePath,
31
+ params,
32
+ query
33
+ });
34
+ const body = result.body;
35
+ if (typeof body === "string") {
36
+ return { html: body, cacheStrategy: result.cacheStrategy };
37
+ }
38
+ if (Buffer.isBuffer(body)) {
39
+ return { html: body.toString("utf8"), cacheStrategy: result.cacheStrategy };
40
+ }
41
+ if (body instanceof ReadableStream) {
42
+ return { html: await new Response(body).text(), cacheStrategy: result.cacheStrategy };
43
+ }
44
+ throw new TypeError(`Unsupported Lit static render body type: ${typeof body}`);
45
+ }
46
+ function postMessage(message) {
47
+ parentPort?.postMessage(message);
48
+ }
49
+ if (!parentPort) {
50
+ throw new Error("Lit static render worker must be started as a worker thread");
51
+ }
52
+ parentPort.on("message", async (message) => {
53
+ try {
54
+ if (message.type === "init") {
55
+ await initializeWorker(message.configModulePath, message.runtimeOrigin);
56
+ postMessage({ type: "ready" });
57
+ return;
58
+ }
59
+ if (message.type === "shutdown") {
60
+ process.exit(0);
61
+ return;
62
+ }
63
+ if (message.type === "render") {
64
+ const result = await renderPage(message.filePath, message.params, message.query);
65
+ postMessage({ type: "result", id: message.id, ...result });
66
+ }
67
+ } catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : String(error);
69
+ if (message.type === "render") {
70
+ postMessage({ type: "error", id: message.id, message: errorMessage });
71
+ return;
72
+ }
73
+ postMessage({ type: "error", message: errorMessage });
74
+ }
75
+ });
@@ -3,7 +3,7 @@
3
3
  * @module
4
4
  */
5
5
  import './console.js';
6
- import { IntegrationPlugin, type IntegrationPluginConfig } from '@ecopages/core/plugins/integration-plugin';
6
+ import { IntegrationPlugin, type IntegrationPluginConfig, type StaticExportContext } from '@ecopages/core/plugins/integration-plugin';
7
7
  import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
8
8
  import { LitRenderer } from './lit-renderer.js';
9
9
  /**
@@ -13,11 +13,24 @@ export declare const PLUGIN_NAME = "lit";
13
13
  /**
14
14
  * The Lit plugin class
15
15
  * This plugin provides support for Lit components in Ecopages
16
+ *
17
+ * @remarks
18
+ * Owns the {@link LitStaticRenderSession} for worker SSR. Renderers receive a
19
+ * lazy `getRenderSession` accessor so construction before {@link setup} still
20
+ * observes the session once setup assigns it.
16
21
  */
17
22
  export declare class LitPlugin extends IntegrationPlugin {
18
23
  renderer: typeof LitRenderer;
24
+ private renderSession;
19
25
  constructor(options?: Omit<IntegrationPluginConfig, 'name'>);
20
26
  getDependencies(): AssetDefinition[];
27
+ private createRenderSession;
28
+ initializeRenderer(options?: {
29
+ rendererModules?: unknown;
30
+ }): LitRenderer;
31
+ setup(): Promise<void>;
32
+ teardown(): Promise<void>;
33
+ beforeStaticExport(context: StaticExportContext): Promise<void>;
21
34
  }
22
35
  /**
23
36
  * Factory function to create a Lit plugin instance
package/src/lit.plugin.js CHANGED
@@ -1,17 +1,22 @@
1
1
  import "./console.js";
2
- import { IntegrationPlugin } from "@ecopages/core/plugins/integration-plugin";
2
+ import {
3
+ IntegrationPlugin
4
+ } from "@ecopages/core/plugins/integration-plugin";
5
+ import { isLitStaticRenderWorkerThread } from "@ecopages/core/build/lit-static-render-worker-context";
6
+ import path from "node:path";
3
7
  import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
4
8
  import { litElementHydrateScript } from "./lit-element-hydrate.js";
5
9
  import { LIT_PLUGIN_NAME } from "./lit.constants.js";
6
10
  import { LitRenderer } from "./lit-renderer.js";
11
+ import { LitStaticRenderSession } from "./lit-static-render-session.js";
7
12
  const PLUGIN_NAME = LIT_PLUGIN_NAME;
8
13
  class LitPlugin extends IntegrationPlugin {
9
14
  renderer = LitRenderer;
15
+ renderSession = null;
10
16
  constructor(options) {
11
17
  super({
12
18
  name: PLUGIN_NAME,
13
19
  extensions: [".lit.tsx"],
14
- staticBuildStep: "fetch",
15
20
  ...options
16
21
  });
17
22
  this.integrationDependencies.unshift(...this.getDependencies());
@@ -40,6 +45,52 @@ class LitPlugin extends IntegrationPlugin {
40
45
  })
41
46
  ];
42
47
  }
48
+ createRenderSession() {
49
+ if (!this.appConfig || !this.assetProcessingService) {
50
+ throw new Error("Lit plugin must be initialized before starting the render worker");
51
+ }
52
+ return new LitStaticRenderSession({
53
+ resolveDependencyPath: (componentDir, sourcePath) => path.join(componentDir, sourcePath),
54
+ processDependencies: this.assetProcessingService.processDependencies.bind(this.assetProcessingService),
55
+ preferSourceImports: typeof Bun !== "undefined"
56
+ });
57
+ }
58
+ initializeRenderer(options) {
59
+ const renderer = new this.renderer({
60
+ ...this.createRendererOptions(options),
61
+ getRenderSession: () => this.renderSession ?? void 0
62
+ });
63
+ return this.attachRendererRuntimeServices(renderer);
64
+ }
65
+ async setup() {
66
+ await super.setup();
67
+ if (!this.appConfig?.absolutePaths?.config) {
68
+ throw new Error("[ecopages][lit] Lit server rendering requires appConfig.absolutePaths.config");
69
+ }
70
+ if (isLitStaticRenderWorkerThread()) {
71
+ return;
72
+ }
73
+ this.renderSession = this.createRenderSession();
74
+ await this.renderSession.ensureWorker({
75
+ configModulePath: this.appConfig.absolutePaths.config,
76
+ runtimeOrigin: this.runtimeOrigin
77
+ });
78
+ }
79
+ async teardown() {
80
+ await this.renderSession?.dispose();
81
+ this.renderSession = null;
82
+ await super.teardown();
83
+ }
84
+ async beforeStaticExport(context) {
85
+ if (!this.renderSession) {
86
+ this.renderSession = this.createRenderSession();
87
+ await this.renderSession.ensureWorker({
88
+ configModulePath: context.appConfig.absolutePaths.config,
89
+ runtimeOrigin: context.baseUrl
90
+ });
91
+ }
92
+ await this.renderSession.preloadStaticRoutes(context);
93
+ }
43
94
  }
44
95
  function litPlugin(options) {
45
96
  return new LitPlugin(options);