@ecopages/lit 0.3.0 → 0.3.1

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.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Ecopages lit integration",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -42,7 +42,7 @@
42
42
  "directory": "packages/integrations/lit"
43
43
  },
44
44
  "peerDependencies": {
45
- "@ecopages/core": "0.3.0",
45
+ "@ecopages/core": "0.3.1",
46
46
  "@lit-labs/ssr": "^3.3.0",
47
47
  "@lit-labs/ssr-client": "^1.1.7",
48
48
  "lit": "^3.2.1"
@@ -1,7 +1,3 @@
1
- /**
2
- * This module contains the Lit renderer hooks.
3
- * @module
4
- */
5
1
  /**
6
2
  * This module contains the Lit renderer hooks.
7
3
  * @module
@@ -10,7 +6,6 @@ import type { EcoPagesElement } from '@ecopages/core';
10
6
  import { createIntegrationRenderer } from '@ecopages/core/integration';
11
7
  import type { LitStaticRenderSession } from './lit-static-render-session.js';
12
8
  export type LitRenderer = InstanceType<ReturnType<typeof createLitRenderer>>;
13
- export type LitRendererOptions = ConstructorParameters<ReturnType<typeof createLitRenderer>>[0];
14
9
  /**
15
10
  * Creates the Lit renderer class with a lazy worker-session lookup.
16
11
  *
@@ -1,7 +1,3 @@
1
- /**
2
- * This module contains the Lit renderer hooks.
3
- * @module
4
- */
5
1
  import { createIntegrationRenderer } from '@ecopages/core/integration';
6
2
  import { resolveQueuedTokens } from '@ecopages/core/integration';
7
3
  import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, CustomElementScriptPreloader, } from '@ecopages/core/integration';
@@ -0,0 +1,17 @@
1
+ import type { LitStaticRenderWorkerErrorMessage } from './lit-static-render-protocol.js';
2
+ /**
3
+ * Turns an error thrown in the Lit worker into the message posted to the main thread.
4
+ *
5
+ * @remarks
6
+ * `postMessage` cannot carry an `Error` with its class, so the status of an `HttpError` travels as a field.
7
+ * Without it a page that throws `HttpError.NotFound()` would reach the main thread as a plain error and render as a 500.
8
+ */
9
+ export declare function toLitStaticRenderErrorMessage(error: unknown, id?: string): LitStaticRenderWorkerErrorMessage;
10
+ /**
11
+ * Rebuilds the error a Lit worker reported, as an `HttpError` when the worker's error carried a status.
12
+ *
13
+ * @remarks
14
+ * The worker-side name, message and stack are kept on `cause` because the rebuilt error's own stack points at the
15
+ * message handler, not at the page that threw.
16
+ */
17
+ export declare function fromLitStaticRenderErrorMessage(message: LitStaticRenderWorkerErrorMessage): Error;
@@ -0,0 +1,34 @@
1
+ import { HttpError } from '@ecopages/core/errors';
2
+ /**
3
+ * Turns an error thrown in the Lit worker into the message posted to the main thread.
4
+ *
5
+ * @remarks
6
+ * `postMessage` cannot carry an `Error` with its class, so the status of an `HttpError` travels as a field.
7
+ * Without it a page that throws `HttpError.NotFound()` would reach the main thread as a plain error and render as a 500.
8
+ */
9
+ export function toLitStaticRenderErrorMessage(error, id) {
10
+ if (!(error instanceof Error) && !HttpError.isHttpError(error)) {
11
+ return { type: 'error', id, message: String(error) };
12
+ }
13
+ const { message, name, stack } = error;
14
+ return {
15
+ type: 'error',
16
+ id,
17
+ message,
18
+ name,
19
+ stack,
20
+ status: HttpError.isHttpError(error) ? error.status : undefined,
21
+ };
22
+ }
23
+ /**
24
+ * Rebuilds the error a Lit worker reported, as an `HttpError` when the worker's error carried a status.
25
+ *
26
+ * @remarks
27
+ * The worker-side name, message and stack are kept on `cause` because the rebuilt error's own stack points at the
28
+ * message handler, not at the page that threw.
29
+ */
30
+ export function fromLitStaticRenderErrorMessage(message) {
31
+ const error = message.status === undefined ? new Error(message.message) : new HttpError(message.status, message.message);
32
+ error.cause = { name: message.name, message: message.message, stack: message.stack };
33
+ return error;
34
+ }
@@ -33,6 +33,12 @@ export type LitStaticRenderWorkerErrorMessage = {
33
33
  type: 'error';
34
34
  id?: string;
35
35
  message: string;
36
+ /** Worker-side error name, kept as the rebuilt error's cause. */
37
+ name?: string;
38
+ /** Worker-side stack, kept as the rebuilt error's cause. */
39
+ stack?: string;
40
+ /** Present when the thrown error was an `HttpError`, so the main thread can rebuild one with the same status. */
41
+ status?: number;
36
42
  };
37
43
  export type LitStaticRenderWorkerShutdownCompleteMessage = {
38
44
  type: 'shutdown-complete';
@@ -51,13 +51,11 @@ export declare class LitStaticRenderSession {
51
51
  }): Promise<void>;
52
52
  preloadStaticRoutes(context: StaticExportContext): Promise<void>;
53
53
  preloadSsrScripts(components: Array<CustomElementSsrPreloadComponent | undefined>): Promise<void>;
54
- collectSsrPreloadScripts(components: Array<CustomElementSsrPreloadComponent | undefined>): string[];
55
54
  renderPageInWorker(input: {
56
55
  filePath: string;
57
56
  params: PageParams;
58
57
  query?: PageQuery;
59
58
  }): Promise<LitStaticRenderResult>;
60
- isExpectedSsrPreloadError(error: unknown): boolean;
61
59
  dispose(): Promise<void>;
62
60
  private preloadLitRoutes;
63
61
  private isLitRoute;
@@ -79,9 +79,6 @@ export class LitStaticRenderSession {
79
79
  ensureLitDomShim();
80
80
  await this.preloader.preloadSsrScripts(components);
81
81
  }
82
- collectSsrPreloadScripts(components) {
83
- return this.preloader.collectSsrPreloadScripts(components);
84
- }
85
82
  async renderPageInWorker(input) {
86
83
  if (!this.workerClient) {
87
84
  throw new Error('Lit static render worker is not active');
@@ -100,9 +97,6 @@ export class LitStaticRenderSession {
100
97
  this.renderChain = render.then(() => undefined, () => undefined);
101
98
  return render;
102
99
  }
103
- isExpectedSsrPreloadError(error) {
104
- return this.preloader.isExpectedSsrPreloadError(error);
105
- }
106
100
  async dispose() {
107
101
  await this.workerClient?.dispose();
108
102
  this.workerClient = null;
@@ -1,5 +1,6 @@
1
1
  import { Worker } from 'node:worker_threads';
2
2
  import { fileURLToPath } from 'node:url';
3
+ import { fromLitStaticRenderErrorMessage } from './lit-static-render-error.js';
3
4
  const workerShutdownTimeoutMs = 30_000;
4
5
  const workerExitTimeoutMs = 1_000;
5
6
  export class LitStaticRenderWorkerClient {
@@ -53,7 +54,7 @@ export class LitStaticRenderWorkerClient {
53
54
  if (message.id && this.pending.has(message.id)) {
54
55
  const pending = this.pending.get(message.id);
55
56
  this.pending.delete(message.id);
56
- pending.reject(new Error(message.message));
57
+ pending.reject(fromLitStaticRenderErrorMessage(message));
57
58
  return;
58
59
  }
59
60
  const error = new Error(message.message);
@@ -1,9 +1,9 @@
1
1
  import { parentPort } from 'node:worker_threads';
2
2
  import { loadEcoPagesConfig } from '@ecopages/core/config';
3
3
  import { setupAppRuntimeIntegrations, setupAppRuntimePlugins, teardownAppRuntimeIntegrations, } from '@ecopages/core/build';
4
- import { installBuildQueue } from '@ecopages/core/build';
5
4
  import { requireAppRuntime } from '@ecopages/core/integration';
6
5
  import { RouteRendererFactory } from '@ecopages/core/integration';
6
+ import { toLitStaticRenderErrorMessage } from './lit-static-render-error.js';
7
7
  import { toLitStaticRenderResult } from './lit-static-render-result.js';
8
8
  import './dom-shim.js';
9
9
  let appConfig = null;
@@ -27,10 +27,8 @@ async function initializeWorker(configModulePath, runtimeOrigin) {
27
27
  appConfig,
28
28
  });
29
29
  await setupAppRuntimeIntegrations({ appConfig });
30
- installBuildQueue(appConfig);
31
30
  routeRendererFactory = new RouteRendererFactory({
32
31
  appConfig,
33
- runtime,
34
32
  });
35
33
  }
36
34
  async function renderPage(filePath, params, query) {
@@ -101,11 +99,6 @@ parentPort.on('message', async (message) => {
101
99
  }
102
100
  }
103
101
  catch (error) {
104
- const errorMessage = error instanceof Error ? error.message : String(error);
105
- if (message.type === 'render') {
106
- postMessage({ type: 'error', id: message.id, message: errorMessage });
107
- return;
108
- }
109
- postMessage({ type: 'error', message: errorMessage });
102
+ postMessage(toLitStaticRenderErrorMessage(error, message.type === 'render' ? message.id : undefined));
110
103
  }
111
104
  });
@@ -1,75 +0,0 @@
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
- setupAppRuntimeIntegrations()
15
- installBuildQueue()
16
- RouteRendererFactory
17
-
18
- LitPlugin.getRenderer() (creates it through createRenderer(base) on first use after setup(ctx))
19
- └─ createLitRenderer(() => renderSession) hook factory closes over lazy accessor
20
-
21
- IntegrationRenderer.execute() (main)
22
- └─ ensureIntegrationRuntimeActivated()
23
- └─ getRenderSession()
24
- ├─ locals or explicit props ─► remote hook declines; core renders in-process
25
- └─ session.renderPageInWorker()
26
- └─ postMessage(render) ─────► RouteRendererFactory.execute()
27
- (no active session in worker)
28
- Lit's preload hook uses
29
- importServerModule (no entrypoint resolver)
30
-
31
- staticExport
32
- └─ ensureWorker() with the export base URL; matching identity is reused
33
- └─ a different origin restarts the worker
34
- └─ preloadStaticRoutes()
35
- └─ preload SSR lazy scripts for Lit pages
36
- └─ returns a release that disposes the session once the export ends
37
- ```
38
-
39
- `ECOPAGES_LIT_STATIC_RENDER_WORKER=true` in the worker environment prevents nested worker creation during `LitPlugin.setup()` and skips processor `.setup()` inside `setupAppRuntimePlugins` (the main thread already prepared artifacts). Integration setup still runs in the worker isolate before `RouteRendererFactory` creates route renderers.
40
-
41
- ## Ownership
42
-
43
- | Concern | Owner |
44
- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
- | Session + worker lifecycle | `LitPlugin` (`renderSession`) |
46
- | Lazy session lookup | `createLitRenderer()` hook factory via `getRenderSession` |
47
- | Worker identity | First `{ configModulePath, runtimeOrigin }` passed to `ensureWorker`; same identity is sticky; a later `ensureWorker` with a different identity disposes and recreates |
48
-
49
- Renderers created before plugin `setup()` still use the worker after setup assigns the session, because `execute` activates the integration runtime then reads the accessor.
50
-
51
- `staticExport` calls `ensureWorker` with `context.baseUrl` even when setup already created the session. A matching identity is reused; a different export origin replaces the worker before routes are preloaded. Its release disposes the session and clears `renderSession`, so an in-process export leaves no worker thread; renders after the export run in-process.
52
-
53
- ## Locals policy
54
-
55
- When `RouteRendererOptions.locals` or `RouteRendererOptions.props` is present, the Lit remote-render hook declines the worker path and core continues in-process. Request locals are not sent through the worker protocol (structured-clone risk and request-scoped data), and explicit props are not passed because the worker re-imports the route module. Static export uses the worker only for eligible Lit page routes without locals or explicit props; explicit static views continue in-process.
56
-
57
- ## Files
58
-
59
- | File | Role |
60
- | ------------------------------------ | --------------------------------------------------------------- |
61
- | `lit-static-render-session.ts` | Plugin-runtime-scoped session: worker lifecycle + SSR preload |
62
- | `lit-static-render-worker-client.ts` | Message protocol client |
63
- | `lit-static-render-worker.ts` | Worker entry (exported as `@ecopages/lit/static-render-worker`) |
64
- | `lit-static-render-protocol.ts` | Request/response message types |
65
-
66
- ## Protocol
67
-
68
- | Message | Direction | Purpose |
69
- | ------------------- | ------------- | ------------------------------------------------------------------------------------------------------------- |
70
- | `init` | main → worker | Load `eco.config.ts`, bootstrap runtime (`configModulePath`, `runtimeOrigin`) |
71
- | `ready` | worker → main | Worker initialized |
72
- | `render` | main → worker | Render one Lit page route (`filePath`, `PageParams`, optional `PageQuery`) |
73
- | `result` / `error` | worker → main | HTML body plus optional `cacheStrategy`, `sourceDependencyPaths` and resolved `metadata`, or an error message |
74
- | `shutdown` | main → worker | Wait for active renders, tear down activated Integrations, and close the worker message port |
75
- | `shutdown-complete` | worker → main | Confirm Integration teardown completed; the client waits for worker exit and force-terminates after timeout |