@ecopages/lit 0.2.0-rc.7 → 0.2.0-rc.9

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/README.md CHANGED
@@ -72,4 +72,4 @@ Important:
72
72
 
73
73
  A Lit Component can import a relative stylesheet with `import './counter.css'`; core extracts it into the shared asset pipeline. Direct local imports of that Component make its Dependencies available to the Lit SSR preloader, including through Foreign Child rendering.
74
74
 
75
- Keep custom-element registration explicit with `scripts: [{ src: './counter.script.ts', ssr: true, lazy: { 'on:visible': true } }]`. The module executes before server rendering and loads in the browser on visibility. `ssr: true` no longer emits an additional eager browser script. Omit `lazy` when eager browser loading is intended. SSR preload artifacts are excluded from HTML, and hydration support must be available before the browser registers the element.
75
+ Keep custom-element registration explicit with `scripts: [{ src: './counter.script.ts', ssr: true, lazy: { 'on:visible': true } }]`. Entries with `ssr: true` execute on the server before rendering (eager or lazy) and load in the browser according to `lazy`. `ssr: true` does not emit an additional eager browser script. Omit `lazy` when eager browser loading is intended. SSR preload artifacts are excluded from HTML, and hydration support must be available before the browser registers the element. Lit-owned page routes preload those scripts in the static-render worker through the app module loader so `@lit-labs/ssr` sees the registered custom element.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/lit",
3
- "version": "0.2.0-rc.7",
3
+ "version": "0.2.0-rc.9",
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.2.0-rc.7",
45
+ "@ecopages/core": "0.2.0-rc.9",
46
46
  "@lit-labs/ssr": "^3.3.0",
47
47
  "@lit-labs/ssr-client": "^1.1.7",
48
48
  "lit": "^3.2.1"
@@ -24,6 +24,8 @@ LitRenderer.execute() (main)
24
24
  └─ session.renderPageInWorker()
25
25
  └─ postMessage(render) ─────► RouteRendererFactory.execute()
26
26
  (no active session in worker)
27
+ LitRenderer.ssrScriptPreloader uses
28
+ importServerModule (no entrypoint resolver)
27
29
 
28
30
  beforeStaticExport
29
31
  └─ ensureWorker() only when `renderSession` is still null
package/src/dom-shim.js CHANGED
@@ -2,15 +2,49 @@ import { installWindowOnGlobal } from '@lit-labs/ssr/lib/dom-shim.js';
2
2
  import '@lit-labs/ssr/lib/install-global-dom-shim.js';
3
3
  /** Repairs the minimal Lit DOM surface after another Integration installs its SSR globals. */
4
4
  export function ensureLitDomShim() {
5
- if (typeof globalThis.document?.createTreeWalker !== 'function') {
6
- if (typeof globalThis.window === 'undefined') {
7
- installWindowOnGlobal();
5
+ if (typeof globalThis.window === 'undefined') {
6
+ installWindowOnGlobal();
7
+ }
8
+ if (typeof globalThis.document !== 'undefined' && typeof globalThis.document.createTreeWalker !== 'function') {
9
+ Object.defineProperty(globalThis.document, 'createTreeWalker', {
10
+ configurable: true,
11
+ writable: true,
12
+ value: () => ({}),
13
+ });
14
+ }
15
+ for (const target of [globalThis.Element, globalThis.HTMLElement]) {
16
+ if (typeof target === 'undefined' || !target?.prototype) {
17
+ continue;
18
+ }
19
+ if (!('attributes' in target.prototype)) {
20
+ Object.defineProperty(target.prototype, 'attributes', {
21
+ configurable: true,
22
+ enumerable: true,
23
+ get() {
24
+ if (typeof this.getAttributeNames === 'function') {
25
+ return this.getAttributeNames().map((name) => ({
26
+ name,
27
+ value: this.getAttribute(name),
28
+ }));
29
+ }
30
+ return [];
31
+ },
32
+ });
8
33
  }
9
- else {
10
- Object.defineProperty(globalThis.document, 'createTreeWalker', {
34
+ if (typeof target.prototype.attachShadow !== 'function') {
35
+ Object.defineProperty(target.prototype, 'attachShadow', {
11
36
  configurable: true,
12
37
  writable: true,
13
- value: () => ({}),
38
+ value: function attachShadow(init = { mode: 'open' }) {
39
+ const shadowRoot = {
40
+ mode: init?.mode ?? 'open',
41
+ host: this,
42
+ children: [],
43
+ childNodes: [],
44
+ };
45
+ this.shadowRoot = shadowRoot;
46
+ return shadowRoot;
47
+ },
14
48
  });
15
49
  }
16
50
  }
@@ -3,9 +3,8 @@
3
3
  * @module
4
4
  */
5
5
  import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPagesElement, IntegrationRendererRenderOptions, RouteRenderResult, RouteRendererBody, RouteRendererOptions } from '@ecopages/core';
6
- import './dom-shim.js';
7
6
  import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/orchestration/integration-renderer';
8
- import { type LitSsrPreloadComponent } from './lit-ssr-lazy-preloader.js';
7
+ import { type CustomElementSsrPreloadComponent } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
9
8
  import type { LitStaticRenderSession } from './lit-static-render-session.js';
10
9
  export type LitRendererOptions = ConstructorParameters<typeof IntegrationRenderer>[0] & {
11
10
  /**
@@ -24,6 +23,7 @@ export type LitRendererOptions = ConstructorParameters<typeof IntegrationRendere
24
23
  export declare class LitRenderer extends IntegrationRenderer<EcoPagesElement> {
25
24
  name: string;
26
25
  private readonly getRenderSession?;
26
+ private readonly ssrScriptPreloader;
27
27
  constructor(options: LitRendererOptions);
28
28
  /**
29
29
  * Renders a Lit page route, preferring the plugin worker when available.
@@ -41,14 +41,13 @@ export declare class LitRenderer extends IntegrationRenderer<EcoPagesElement> {
41
41
  /**
42
42
  * Renders a Lit component for component-level orchestration.
43
43
  *
44
- * SSR-eligible lazy scripts are preloaded first so custom elements registered
44
+ * SSR-eligible scripts are preloaded first so custom elements registered
45
45
  * by the component can render their server markup even when the Lit renderer is
46
46
  * entered through cross-integration foreign-child handoff.
47
47
  *
48
48
  * Includes component-scoped dependency assets when declared.
49
49
  */
50
50
  renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
51
- private readonly ssrLazyPreloader;
52
51
  /**
53
52
  * Detects preload failures that are expected for browser-only modules.
54
53
  *
@@ -57,24 +56,24 @@ export declare class LitRenderer extends IntegrationRenderer<EcoPagesElement> {
57
56
  */
58
57
  protected isExpectedSsrPreloadError(error: unknown): boolean;
59
58
  /**
60
- * Collects lazy script file paths eligible for SSR preloading.
59
+ * Collects script file paths eligible for SSR preloading.
61
60
  *
62
- * Only per-entry lazy script dependencies with `ssr: true` are collected.
61
+ * Per-entry script dependencies with `ssr: true` are collected (eager or lazy).
63
62
  * File-backed entries are required (`src` must be present);
64
63
  * inline content lazy entries are intentionally skipped.
65
64
  */
66
- protected collectSsrPreloadScripts(components: Array<LitSsrPreloadComponent | undefined>): string[];
65
+ protected collectSsrPreloadScripts(components: Array<CustomElementSsrPreloadComponent | undefined>): string[];
67
66
  /**
68
- * Preloads SSR-eligible lazy scripts to register custom elements before render.
69
- */
70
- protected preloadSsrLazyScripts(components: Array<LitSsrPreloadComponent | undefined>): Promise<void>;
71
- /**
72
- * Resolves the concrete JS entrypoint used for SSR preloading.
67
+ * Preloads SSR-eligible scripts to register custom elements before render.
73
68
  *
74
- * Scripts are passed through the asset pipeline so preload imports can use
75
- * the same processed output shape as runtime dependencies.
69
+ * @remarks
70
+ * When a render session exists (main thread), preload uses the session so
71
+ * Bun source imports and the Node asset pipeline stay on that isolate.
72
+ * With no session (static-render worker), preload uses this renderer's
73
+ * app-module importer. A native source import in the worker leaves custom
74
+ * elements undefined for `@lit-labs/ssr`.
76
75
  */
77
- protected resolveSsrPreloadEntrypoint(scriptPath: string): Promise<string | null>;
76
+ protected preloadSsrScripts(components: Array<CustomElementSsrPreloadComponent | undefined>): Promise<void>;
78
77
  render({ params, query, props, locals, metadata, Page, Layout, layoutEntries, HtmlTemplate, resolvedPageDependencyComponents, }: IntegrationRendererRenderOptions): Promise<RouteRendererBody>;
79
78
  renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
80
79
  }
@@ -1,9 +1,13 @@
1
- import { createLitServerModuleImporter } from './lit-server-module-loader.js';
2
- import './dom-shim.js';
1
+ /**
2
+ * This module contains the Lit renderer
3
+ * @module
4
+ */
3
5
  import { IntegrationRenderer, } from '@ecopages/core/route-renderer/orchestration/integration-renderer';
4
6
  import { resolveDocumentShellLayouts } from '@ecopages/core/route-renderer/orchestration/document-shell/layout-shell-props.service';
5
7
  import { getForeignSubtreeResolutionContextKey, resolveOwningIntegrationRenderer, } from '@ecopages/core/route-renderer/orchestration/foreign-child/owning-renderer-resolution';
6
- import { LitSsrLazyPreloader } from './lit-ssr-lazy-preloader.js';
8
+ import { ensureLitDomShim } from './dom-shim.js';
9
+ import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, CustomElementScriptPreloader, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
10
+ import { createCustomElementServerModuleImporter } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-server-module-importer';
7
11
  import { LIT_PLUGIN_NAME } from './lit.constants.js';
8
12
  import { injectLitRenderedChildren, LIT_COMPONENT_CHILDREN_SLOT_MARKER, normalizeLitHtml, renderLitValueToString, } from './utils/lit-html-rendering.js';
9
13
  /**
@@ -12,10 +16,18 @@ import { injectLitRenderedChildren, LIT_COMPONENT_CHILDREN_SLOT_MARKER, normaliz
12
16
  export class LitRenderer extends IntegrationRenderer {
13
17
  name = LIT_PLUGIN_NAME;
14
18
  getRenderSession;
19
+ ssrScriptPreloader;
15
20
  constructor(options) {
16
21
  const { getRenderSession, ...rendererOptions } = options;
17
22
  super(rendererOptions);
18
23
  this.getRenderSession = getRenderSession;
24
+ this.ssrScriptPreloader = new CustomElementScriptPreloader({
25
+ cacheScope: CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.lit,
26
+ logLabel: 'lit',
27
+ requireLazyScriptEntry: true,
28
+ resolveDependencyPath: this.resolveDependencyPath.bind(this),
29
+ importServerModule: createCustomElementServerModuleImporter(this.appConfig, '.lit-ssr'),
30
+ });
19
31
  }
20
32
  /**
21
33
  * Renders a Lit page route, preferring the plugin worker when available.
@@ -73,14 +85,14 @@ export class LitRenderer extends IntegrationRenderer {
73
85
  /**
74
86
  * Renders a Lit component for component-level orchestration.
75
87
  *
76
- * SSR-eligible lazy scripts are preloaded first so custom elements registered
88
+ * SSR-eligible scripts are preloaded first so custom elements registered
77
89
  * by the component can render their server markup even when the Lit renderer is
78
90
  * entered through cross-integration foreign-child handoff.
79
91
  *
80
92
  * Includes component-scoped dependency assets when declared.
81
93
  */
82
94
  async renderComponent(input) {
83
- await this.preloadSsrLazyScripts([input.component]);
95
+ await this.preloadSsrScripts([input.component]);
84
96
  if (!this.isFunctionComponent(input.component)) {
85
97
  throw new TypeError('Lit renderer expected a callable component.');
86
98
  }
@@ -133,11 +145,6 @@ export class LitRenderer extends IntegrationRenderer {
133
145
  ]),
134
146
  });
135
147
  }
136
- ssrLazyPreloader = new LitSsrLazyPreloader({
137
- importServerModule: createLitServerModuleImporter(this.appConfig),
138
- resolveDependencyPath: this.resolveDependencyPath.bind(this),
139
- processDependencies: this.assetProcessingService?.processDependencies?.bind(this.assetProcessingService),
140
- });
141
148
  /**
142
149
  * Detects preload failures that are expected for browser-only modules.
143
150
  *
@@ -145,41 +152,40 @@ export class LitRenderer extends IntegrationRenderer {
145
152
  * lazy client scripts intentionally depend on browser globals.
146
153
  */
147
154
  isExpectedSsrPreloadError(error) {
148
- return this.ssrLazyPreloader.isExpectedSsrPreloadError(error);
155
+ return this.ssrScriptPreloader.isExpectedSsrPreloadError(error);
149
156
  }
150
157
  /**
151
- * Collects lazy script file paths eligible for SSR preloading.
158
+ * Collects script file paths eligible for SSR preloading.
152
159
  *
153
- * Only per-entry lazy script dependencies with `ssr: true` are collected.
160
+ * Per-entry script dependencies with `ssr: true` are collected (eager or lazy).
154
161
  * File-backed entries are required (`src` must be present);
155
162
  * inline content lazy entries are intentionally skipped.
156
163
  */
157
164
  collectSsrPreloadScripts(components) {
158
- return this.ssrLazyPreloader.collectSsrPreloadScripts(components);
165
+ return this.ssrScriptPreloader.collectSsrPreloadScripts(components);
159
166
  }
160
167
  /**
161
- * Preloads SSR-eligible lazy scripts to register custom elements before render.
168
+ * Preloads SSR-eligible scripts to register custom elements before render.
169
+ *
170
+ * @remarks
171
+ * When a render session exists (main thread), preload uses the session so
172
+ * Bun source imports and the Node asset pipeline stay on that isolate.
173
+ * With no session (static-render worker), preload uses this renderer's
174
+ * app-module importer. A native source import in the worker leaves custom
175
+ * elements undefined for `@lit-labs/ssr`.
162
176
  */
163
- async preloadSsrLazyScripts(components) {
177
+ async preloadSsrScripts(components) {
178
+ ensureLitDomShim();
164
179
  const renderSession = this.getRenderSession?.();
165
180
  if (renderSession) {
166
- await renderSession.preloadSsrLazyScripts(components);
181
+ await renderSession.preloadSsrScripts(components);
167
182
  return;
168
183
  }
169
- await this.ssrLazyPreloader.preloadSsrLazyScripts(components);
170
- }
171
- /**
172
- * Resolves the concrete JS entrypoint used for SSR preloading.
173
- *
174
- * Scripts are passed through the asset pipeline so preload imports can use
175
- * the same processed output shape as runtime dependencies.
176
- */
177
- async resolveSsrPreloadEntrypoint(scriptPath) {
178
- return this.ssrLazyPreloader.resolveSsrPreloadEntrypoint(scriptPath);
184
+ await this.ssrScriptPreloader.preloadSsrScripts(components);
179
185
  }
180
186
  async render({ params, query, props, locals, metadata, Page, Layout, layoutEntries, HtmlTemplate, resolvedPageDependencyComponents, }) {
181
187
  try {
182
- await this.preloadSsrLazyScripts([Page, Layout, ...(resolvedPageDependencyComponents ?? [])]);
188
+ await this.preloadSsrScripts([Page, Layout, ...(resolvedPageDependencyComponents ?? [])]);
183
189
  return await this.renderPageWithDocumentShell({
184
190
  page: {
185
191
  component: Page,
@@ -212,7 +218,7 @@ export class LitRenderer extends IntegrationRenderer {
212
218
  try {
213
219
  const layouts = view.config?.layouts;
214
220
  const Layout = layouts?.[layouts.length - 1];
215
- await this.preloadSsrLazyScripts([view, Layout]);
221
+ await this.preloadSsrScripts([view, Layout]);
216
222
  return await this.renderViewWithDocumentShell({
217
223
  view,
218
224
  props,
@@ -0,0 +1 @@
1
+ export { createCustomElementSsrPreloadEntrypointResolver as createLitSsrPreloadEntrypointResolver, type CustomElementSsrPreloadEntrypointResolverOptions as LitSsrPreloadEntrypointResolverOptions, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-ssr-preload-entrypoint';
@@ -0,0 +1 @@
1
+ export { createCustomElementSsrPreloadEntrypointResolver as createLitSsrPreloadEntrypointResolver, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-ssr-preload-entrypoint';
@@ -1,17 +1,15 @@
1
1
  import type { PageParams, PageQuery } from '@ecopages/core';
2
2
  import type { StaticExportContext } from '@ecopages/core/plugins/integration-plugin';
3
- import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
4
- import { type LitSsrLazyPreloaderOptions, type LitSsrPreloadComponent } from './lit-ssr-lazy-preloader.js';
3
+ import { type CustomElementScriptPreloaderOptions, type CustomElementSsrPreloadComponent } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
5
4
  import { LitStaticRenderWorkerClient } from './lit-static-render-worker-client.js';
6
5
  import type { LitStaticRenderCacheStrategy } from './lit-static-render-protocol.js';
6
+ import type { AssetDefinition, ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
7
7
  type LitStaticRenderWorkerClientContract = Pick<LitStaticRenderWorkerClient, 'start' | 'renderPage' | 'dispose'>;
8
8
  type LitStaticRenderSessionOptions = {
9
9
  resolveDependencyPath: (componentDir: string, sourcePath: string) => string;
10
- processDependencies?: (dependencies: AssetDefinition[], integrationName: string) => Promise<Array<{
11
- filepath?: string;
12
- }>>;
13
10
  preferSourceImports?: boolean;
14
- importServerModule?: LitSsrLazyPreloaderOptions['importServerModule'];
11
+ processDependencies?: (dependencies: AssetDefinition[], integrationName: string) => Promise<ProcessedAsset[]>;
12
+ importServerModule?: CustomElementScriptPreloaderOptions['importServerModule'];
15
13
  getInvalidationVersion?: () => number;
16
14
  createWorkerClient?: (input: {
17
15
  configModulePath: string;
@@ -51,8 +49,8 @@ export declare class LitStaticRenderSession {
51
49
  runtimeOrigin: string;
52
50
  }): Promise<void>;
53
51
  preloadStaticRoutes(context: StaticExportContext): Promise<void>;
54
- preloadSsrLazyScripts(components: Array<LitSsrPreloadComponent | undefined>): Promise<void>;
55
- collectSsrPreloadScripts(components: Array<LitSsrPreloadComponent | undefined>): string[];
52
+ preloadSsrScripts(components: Array<CustomElementSsrPreloadComponent | undefined>): Promise<void>;
53
+ collectSsrPreloadScripts(components: Array<CustomElementSsrPreloadComponent | undefined>): string[];
56
54
  renderPageInWorker(input: {
57
55
  filePath: string;
58
56
  params: PageParams;
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path';
2
- import { LitSsrLazyPreloader, } from './lit-ssr-lazy-preloader.js';
2
+ import { ensureLitDomShim } from './dom-shim.js';
3
+ import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, CustomElementScriptPreloader, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
3
4
  import { LIT_PLUGIN_NAME } from './lit.constants.js';
5
+ import { createLitSsrPreloadEntrypointResolver } from './lit-ssr-preload-entrypoint.js';
4
6
  import { LitStaticRenderWorkerClient } from './lit-static-render-worker-client.js';
5
7
  /**
6
8
  * Plugin-runtime-scoped Lit server render session.
@@ -24,10 +26,17 @@ export class LitStaticRenderSession {
24
26
  renderChain = Promise.resolve();
25
27
  constructor(options) {
26
28
  this.getInvalidationVersion = options.getInvalidationVersion ?? (() => 0);
27
- this.preloader = new LitSsrLazyPreloader({
29
+ const preferSourceImports = options.preferSourceImports ?? typeof Bun !== 'undefined';
30
+ this.preloader = new CustomElementScriptPreloader({
31
+ cacheScope: CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.lit,
32
+ logLabel: 'lit',
33
+ requireLazyScriptEntry: true,
28
34
  resolveDependencyPath: options.resolveDependencyPath,
29
- processDependencies: options.processDependencies,
30
- preferSourceImports: options.preferSourceImports,
35
+ preferSourceImports,
36
+ resolvePreloadEntrypoint: createLitSsrPreloadEntrypointResolver({
37
+ preferSourceImports,
38
+ processDependencies: options.processDependencies,
39
+ }),
31
40
  importServerModule: options.importServerModule,
32
41
  });
33
42
  this.createWorkerClient =
@@ -68,8 +77,9 @@ export class LitStaticRenderSession {
68
77
  async preloadStaticRoutes(context) {
69
78
  await this.preloadLitRoutes(context);
70
79
  }
71
- async preloadSsrLazyScripts(components) {
72
- await this.preloader.preloadSsrLazyScripts(components);
80
+ async preloadSsrScripts(components) {
81
+ ensureLitDomShim();
82
+ await this.preloader.preloadSsrScripts(components);
73
83
  }
74
84
  collectSsrPreloadScripts(components) {
75
85
  return this.preloader.collectSsrPreloadScripts(components);
@@ -124,7 +134,7 @@ export class LitStaticRenderSession {
124
134
  }
125
135
  }
126
136
  }
127
- await this.preloadSsrLazyScripts(components);
137
+ await this.preloadSsrScripts(components);
128
138
  }
129
139
  isLitRoute(filePath, appConfig) {
130
140
  const litIntegration = appConfig.integrations.find((plugin) => plugin.name === LIT_PLUGIN_NAME);
@@ -1,3 +1,4 @@
1
+ import type { IHmrManager } from '@ecopages/core';
1
2
  /**
2
3
  * This module contains the Lit plugin
3
4
  * @module
@@ -38,6 +39,7 @@ export declare class LitPlugin extends IntegrationPlugin {
38
39
  initializeRenderer(options?: {
39
40
  rendererModules?: unknown;
40
41
  }): LitRenderer;
42
+ setHmrManager(hmrManager: IHmrManager): void;
41
43
  setup(): Promise<void>;
42
44
  teardown(): Promise<void>;
43
45
  beforeStaticExport(context: StaticExportContext): Promise<void>;
package/src/lit.plugin.js CHANGED
@@ -1,4 +1,5 @@
1
- import { createLitServerModuleImporter } from './lit-server-module-loader.js';
1
+ import { createCustomElementServerModuleImporter } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-server-module-importer';
2
+ import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, invalidateCustomElementScriptPreload, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
2
3
  /**
3
4
  * This module contains the Lit plugin
4
5
  * @module
@@ -66,7 +67,7 @@ export class LitPlugin extends IntegrationPlugin {
66
67
  resolveDependencyPath: (componentDir, sourcePath) => path.join(componentDir, sourcePath),
67
68
  processDependencies: this.assetProcessingService.processDependencies.bind(this.assetProcessingService),
68
69
  getInvalidationVersion: () => this.appConfig?.runtime?.serverInvalidationState?.getServerInvalidationVersion() ?? 0,
69
- importServerModule: createLitServerModuleImporter(this.appConfig),
70
+ importServerModule: createCustomElementServerModuleImporter(this.appConfig, '.lit-ssr'),
70
71
  preferSourceImports: typeof Bun !== 'undefined',
71
72
  });
72
73
  }
@@ -77,6 +78,18 @@ export class LitPlugin extends IntegrationPlugin {
77
78
  });
78
79
  return this.attachRendererRuntimeServices(renderer);
79
80
  }
81
+ setHmrManager(hmrManager) {
82
+ super.setHmrManager(hmrManager);
83
+ if (!this.appConfig) {
84
+ return;
85
+ }
86
+ const runtime = this.appConfig.runtime ?? {};
87
+ this.appConfig.runtime = runtime;
88
+ runtime.registeredScriptEntrypointChangeHandlers ??= [];
89
+ runtime.registeredScriptEntrypointChangeHandlers.push((scriptPath) => {
90
+ invalidateCustomElementScriptPreload(scriptPath, CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.lit);
91
+ });
92
+ }
80
93
  async setup() {
81
94
  await super.setup();
82
95
  if (!this.appConfig?.absolutePaths?.config) {
@@ -1,6 +0,0 @@
1
- import type { EcoPagesAppConfig } from '@ecopages/core';
2
- /**
3
- * Uses server export conditions and isolates module evaluation by custom-element
4
- * registry. Browser asset output must never be evaluated as the SSR implementation.
5
- */
6
- export declare function createLitServerModuleImporter(appConfig: EcoPagesAppConfig): (filePath: string, registryKey: string) => Promise<unknown>;
@@ -1,18 +0,0 @@
1
- import path from 'node:path';
2
- /**
3
- * Uses server export conditions and isolates module evaluation by custom-element
4
- * registry. Browser asset output must never be evaluated as the SSR implementation.
5
- */
6
- export function createLitServerModuleImporter(appConfig) {
7
- return async (filePath, registryKey) => {
8
- const loader = appConfig.runtime?.appModuleLoader;
9
- if (!loader)
10
- throw new Error('Lit SSR requires the app server module loader');
11
- return loader.importModule({
12
- filePath,
13
- rootDir: appConfig.rootDir,
14
- outdir: path.join(appConfig.absolutePaths.workDir, '.lit-ssr', registryKey),
15
- forceBuild: true,
16
- });
17
- };
18
- }
@@ -1,48 +0,0 @@
1
- import { type EcoComponent } from '@ecopages/core';
2
- import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
3
- export type LitSsrPreloadComponent = EcoComponent | Partial<EcoComponent>;
4
- type ProcessDependencies = (dependencies: AssetDefinition[], integrationName: string) => Promise<Array<{
5
- filepath?: string;
6
- }>>;
7
- export interface LitSsrLazyPreloaderOptions {
8
- resolveDependencyPath: (componentDir: string, sourcePath: string) => string;
9
- processDependencies?: ProcessDependencies;
10
- preferSourceImports?: boolean;
11
- importServerModule?: (scriptPath: string, registryKey: string) => Promise<unknown>;
12
- }
13
- /**
14
- * Encapsulates SSR lazy script preload behavior for Lit components.
15
- *
16
- * Collects `dependencies.scripts` lazy entries with `ssr: true`, resolves the
17
- * processed entrypoint through the asset pipeline, and imports the module so
18
- * custom elements are registered before SSR rendering.
19
- */
20
- export declare class LitSsrLazyPreloader {
21
- private readonly resolveDependencyPath;
22
- private readonly processDependencies?;
23
- private readonly preferSourceImports;
24
- private readonly importServerModule?;
25
- private activeRegistry;
26
- private readonly ssrPreloadedScripts;
27
- private readonly ssrPreloadFailedScripts;
28
- private readonly ssrPreloadEntrypointCache;
29
- constructor({ resolveDependencyPath, processDependencies, preferSourceImports, importServerModule, }: LitSsrLazyPreloaderOptions);
30
- private getErrorCode;
31
- /**
32
- * Detects preload failures that are expected for browser-only modules.
33
- */
34
- isExpectedSsrPreloadError(error: unknown): boolean;
35
- /**
36
- * Collects lazy script file paths eligible for SSR preloading.
37
- */
38
- collectSsrPreloadScripts(components: Array<LitSsrPreloadComponent | undefined>): string[];
39
- /**
40
- * Preloads SSR-eligible lazy scripts to register custom elements before render.
41
- */
42
- preloadSsrLazyScripts(components: Array<LitSsrPreloadComponent | undefined>): Promise<void>;
43
- /**
44
- * Resolves the concrete JS entrypoint used for SSR preloading.
45
- */
46
- resolveSsrPreloadEntrypoint(scriptPath: string): Promise<string | null>;
47
- }
48
- export {};
@@ -1,198 +0,0 @@
1
- import { ensureLitDomShim } from './dom-shim.js';
2
- import { getComponentIdentity } from '@ecopages/core';
3
- import { AssetFactory } from '@ecopages/core/services/asset-processing-service';
4
- import path from 'node:path';
5
- import { pathToFileURL } from 'node:url';
6
- import { randomUUID } from 'node:crypto';
7
- const registryKeys = new WeakMap();
8
- function getRegistryKey(registry) {
9
- let key = registryKeys.get(registry);
10
- if (!key) {
11
- key = randomUUID();
12
- registryKeys.set(registry, key);
13
- }
14
- return key;
15
- }
16
- /**
17
- * Encapsulates SSR lazy script preload behavior for Lit components.
18
- *
19
- * Collects `dependencies.scripts` lazy entries with `ssr: true`, resolves the
20
- * processed entrypoint through the asset pipeline, and imports the module so
21
- * custom elements are registered before SSR rendering.
22
- */
23
- export class LitSsrLazyPreloader {
24
- resolveDependencyPath;
25
- processDependencies;
26
- preferSourceImports;
27
- importServerModule;
28
- activeRegistry;
29
- ssrPreloadedScripts = new Set();
30
- ssrPreloadFailedScripts = new Set();
31
- ssrPreloadEntrypointCache = new Map();
32
- constructor({ resolveDependencyPath, processDependencies, preferSourceImports, importServerModule, }) {
33
- this.importServerModule = importServerModule;
34
- this.resolveDependencyPath = resolveDependencyPath;
35
- this.processDependencies = processDependencies;
36
- this.preferSourceImports = preferSourceImports ?? typeof Bun !== 'undefined';
37
- }
38
- getErrorCode(error) {
39
- if (typeof error !== 'object' || error === null) {
40
- return '';
41
- }
42
- const code = Reflect.get(error, 'code');
43
- return code === undefined ? '' : String(code);
44
- }
45
- /**
46
- * Detects preload failures that are expected for browser-only modules.
47
- */
48
- isExpectedSsrPreloadError(error) {
49
- const errorMessage = error instanceof Error ? error.message : String(error);
50
- const errorCode = this.getErrorCode(error);
51
- if (errorCode === 'ERR_UNKNOWN_FILE_EXTENSION') {
52
- return true;
53
- }
54
- return (errorMessage.includes(`reading 'Element'`) ||
55
- errorMessage.includes('window is not defined') ||
56
- errorMessage.includes('document is not defined') ||
57
- errorMessage.includes('navigator is not defined'));
58
- }
59
- /**
60
- * Collects lazy script file paths eligible for SSR preloading.
61
- */
62
- collectSsrPreloadScripts(components) {
63
- const scriptPaths = new Set();
64
- const visitedConfigs = new Set();
65
- const collect = (component) => {
66
- const config = component?.config;
67
- if (!config || visitedConfigs.has(config)) {
68
- return;
69
- }
70
- visitedConfigs.add(config);
71
- const scriptEntries = config.dependencies?.scripts ?? [];
72
- const componentFile = getComponentIdentity(component?.config)?.file;
73
- if (componentFile) {
74
- const componentDir = path.dirname(componentFile);
75
- for (const script of scriptEntries) {
76
- if (typeof script === 'string') {
77
- continue;
78
- }
79
- if (!script.lazy || script.ssr !== true) {
80
- continue;
81
- }
82
- if (!script.src) {
83
- continue;
84
- }
85
- scriptPaths.add(this.resolveDependencyPath(componentDir, script.src));
86
- }
87
- }
88
- for (const layout of config.layouts ?? []) {
89
- collect(layout);
90
- }
91
- for (const nestedComponent of config.dependencies?.components || []) {
92
- collect(nestedComponent);
93
- }
94
- };
95
- for (const component of components) {
96
- collect(component);
97
- }
98
- return Array.from(scriptPaths);
99
- }
100
- /**
101
- * Preloads SSR-eligible lazy scripts to register custom elements before render.
102
- */
103
- async preloadSsrLazyScripts(components) {
104
- ensureLitDomShim();
105
- const registry = globalThis.customElements;
106
- if (this.activeRegistry !== registry) {
107
- this.ssrPreloadedScripts.clear();
108
- this.ssrPreloadFailedScripts.clear();
109
- this.activeRegistry = registry;
110
- }
111
- const scripts = this.collectSsrPreloadScripts(components);
112
- if (scripts.length === 0) {
113
- return;
114
- }
115
- await Promise.all(scripts
116
- .filter((scriptPath) => {
117
- if (this.ssrPreloadedScripts.has(scriptPath)) {
118
- return false;
119
- }
120
- if (this.ssrPreloadFailedScripts.has(scriptPath)) {
121
- return false;
122
- }
123
- return true;
124
- })
125
- .map(async (scriptPath) => {
126
- try {
127
- const registryKey = registry ? getRegistryKey(registry) : 'default';
128
- if (this.importServerModule) {
129
- await this.importServerModule(scriptPath, registryKey);
130
- }
131
- else {
132
- const preloadEntrypoint = await this.resolveSsrPreloadEntrypoint(scriptPath);
133
- if (!preloadEntrypoint)
134
- throw new Error(`Cannot resolve SSR entry ${scriptPath}`);
135
- const importUrl = pathToFileURL(preloadEntrypoint);
136
- if (registry && typeof Bun === 'undefined')
137
- importUrl.searchParams.set('eco-ssr-registry', registryKey);
138
- await import(/* @vite-ignore */ importUrl.href);
139
- }
140
- this.ssrPreloadedScripts.add(scriptPath);
141
- }
142
- catch (error) {
143
- this.ssrPreloadFailedScripts.add(scriptPath);
144
- if (this.isExpectedSsrPreloadError(error)) {
145
- if (process.env.ECOPAGES_DEBUG === 'true') {
146
- console.warn(`[ecopages][lit] Skipping SSR preload for browser-only lazy script: ${scriptPath}`);
147
- }
148
- return;
149
- }
150
- console.warn(`[ecopages][lit] Failed to preload lazy SSR script: ${scriptPath}`, error);
151
- }
152
- }));
153
- }
154
- /**
155
- * Resolves the concrete JS entrypoint used for SSR preloading.
156
- */
157
- async resolveSsrPreloadEntrypoint(scriptPath) {
158
- const cachedEntrypoint = this.ssrPreloadEntrypointCache.get(scriptPath);
159
- if (cachedEntrypoint) {
160
- return cachedEntrypoint;
161
- }
162
- if (this.preferSourceImports) {
163
- this.ssrPreloadEntrypointCache.set(scriptPath, scriptPath);
164
- return scriptPath;
165
- }
166
- if (!this.processDependencies) {
167
- this.ssrPreloadEntrypointCache.set(scriptPath, scriptPath);
168
- return scriptPath;
169
- }
170
- try {
171
- const processed = await this.processDependencies([
172
- AssetFactory.createInlineFileScript({
173
- filepath: scriptPath,
174
- excludeFromHtml: true,
175
- position: 'head',
176
- bundle: true,
177
- attributes: {
178
- type: 'module',
179
- defer: '',
180
- },
181
- }),
182
- ], `lit-ssr-preload:${scriptPath}`);
183
- const entrypoint = processed[0]?.filepath;
184
- if (!entrypoint) {
185
- return scriptPath;
186
- }
187
- this.ssrPreloadEntrypointCache.set(scriptPath, entrypoint);
188
- return entrypoint;
189
- }
190
- catch (error) {
191
- if (process.env.ECOPAGES_DEBUG === 'true') {
192
- console.warn(`[ecopages][lit] Failed to resolve SSR preload entrypoint for: ${scriptPath}`, error);
193
- }
194
- this.ssrPreloadEntrypointCache.set(scriptPath, scriptPath);
195
- return scriptPath;
196
- }
197
- }
198
- }