@ecopages/ecopages-jsx 0.2.1-beta.0 → 0.3.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.
@@ -1,120 +1,146 @@
1
- import {
2
- IntegrationPlugin
3
- } from "@ecopages/core/plugins/integration-plugin";
4
- import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
5
- import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
6
- import {
7
- appendMdxExtensions,
8
- createMdxLoaderPlugin,
9
- registerBunMdxPlugin,
10
- resolveMdxCompilerOptions
11
- } from "./ecopages-jsx-mdx.js";
12
- import { EcopagesJsxRenderer } from "./ecopages-jsx-renderer.js";
13
- const RADIANT_HYDRATOR_SCRIPT_ID = "ecopages-jsx-radiant-hydrator";
1
+ import { IntegrationPlugin, } from '@ecopages/core/integration';
2
+ import { AssetFactory } from '@ecopages/core/integration';
3
+ import { requireAppRuntime } from '@ecopages/core/integration';
4
+ import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
5
+ import { RADIANT_INSTALL_HYDRATOR_FILEPATH } from './resolve-radiant-install-hydrator.js';
6
+ import { appendMdxExtensions, createMdxLoaderPlugin, resolveMdxCompilerOptions, } from './ecopages-jsx-mdx.js';
7
+ import { createEcopagesJsxRenderer, createEcopagesJsxRendererHookFactory, EcopagesJsxRenderer, } from './ecopages-jsx-renderer.js';
8
+ import { EcopagesJsxHmrStrategy } from './ecopages-jsx-hmr-strategy.js';
9
+ import { EcopagesJsxDevTransformContributor } from './dev-transform/ecopages-jsx-dev-transform-contributor.js';
10
+ import { assignPageOwnedContentScriptGroupedBundles } from './page-owned-content-script-grouping.js';
11
+ import { invalidateRadiantRegisteredScriptSsrRegistration } from './radiant-registered-script-ssr-invalidation.js';
12
+ const RADIANT_HYDRATOR_SCRIPT_ID = 'ecopages-jsx-radiant-hydrator';
13
+ /**
14
+ * Resolves user-facing plugin options into a fully-defaulted internal config.
15
+ *
16
+ * Defaults:
17
+ * - `extensions`: `['.tsx']`
18
+ * - `radiant`: `true`
19
+ * - MDX is disabled when `mdx` is omitted
20
+ * - `mdx.extensions`: `['.mdx']`
21
+ */
14
22
  const resolvePluginOptions = (options) => {
15
- const { extensions: userExtensions, radiant, mdx, ...baseConfig } = options ?? {};
16
- const extensions = [...userExtensions ?? [".tsx"]];
17
- const mdxExtensions = mdx?.extensions ?? [".mdx"];
18
- const mdxEnabled = mdx?.enabled ?? false;
19
- if (mdxEnabled) {
20
- appendMdxExtensions(extensions, mdxExtensions);
21
- }
22
- return {
23
- ...baseConfig,
24
- extensions,
25
- includeRadiant: radiant ?? true,
26
- mdxEnabled,
27
- mdxExtensions,
28
- mdxCompilerOptions: mdxEnabled && mdx ? resolveMdxCompilerOptions(mdx) : void 0
29
- };
23
+ const { extensions: userExtensions, radiant, mdx, ...baseConfig } = options ?? {};
24
+ const extensions = [...(userExtensions ?? ['.tsx'])];
25
+ const mdxExtensions = mdx?.extensions ?? ['.mdx'];
26
+ const mdxEnabled = mdx !== undefined;
27
+ if (mdxEnabled) {
28
+ appendMdxExtensions(extensions, mdxExtensions);
29
+ }
30
+ return {
31
+ ...baseConfig,
32
+ extensions,
33
+ includeRadiant: radiant ?? true,
34
+ mdxExtensions,
35
+ mdxCompilerOptions: mdxEnabled && mdx ? resolveMdxCompilerOptions(mdx) : undefined,
36
+ };
30
37
  };
31
- class EcopagesJsxPlugin extends IntegrationPlugin {
32
- renderer = EcopagesJsxRenderer;
33
- includeRadiant;
34
- mdxEnabled;
35
- mdxCompilerOptions;
36
- mdxExtensions;
37
- mdxLoaderPlugin;
38
- /** Returns the build plugins required by the JSX integration. */
39
- get plugins() {
40
- return [this.mdxLoaderPlugin].filter((plugin) => plugin !== void 0);
41
- }
42
- /** Creates the renderer instance with the resolved JSX integration runtime options. */
43
- initializeRenderer(options) {
44
- return this.attachRendererRuntimeServices(
45
- new this.renderer({
46
- ...this.createRendererOptions(options),
47
- jsxConfig: {
48
- mdxExtensions: this.mdxExtensions,
49
- radiantSsrEnabled: this.includeRadiant
38
+ /** JSX integration plugin for Ecopages, supporting `.tsx` templates and optional Radiant web components. */
39
+ export class EcopagesJsxPlugin extends IntegrationPlugin {
40
+ renderer = EcopagesJsxRenderer;
41
+ bundlesPageDependencies = true;
42
+ includeRadiant;
43
+ mdxCompilerOptions;
44
+ mdxExtensions;
45
+ /**
46
+ * Returns the build plugins required by the JSX integration.
47
+ *
48
+ * @remarks
49
+ * Returns a new loader declaration without registering or initializing it. The build manifest owns the
50
+ * resulting instance for the app runtime.
51
+ */
52
+ plugins(app) {
53
+ if (!this.mdxCompilerOptions) {
54
+ return [];
50
55
  }
51
- })
52
- );
53
- }
54
- constructor(options) {
55
- const config = resolvePluginOptions(options);
56
- const { extensions, includeRadiant, mdxEnabled, mdxExtensions, mdxCompilerOptions, ...baseConfig } = config;
57
- super({
58
- name: ECOPAGES_JSX_PLUGIN_NAME,
59
- extensions,
60
- jsxImportSource: "@ecopages/jsx",
61
- ...baseConfig
62
- });
63
- this.includeRadiant = includeRadiant;
64
- this.mdxEnabled = mdxEnabled;
65
- this.mdxExtensions = mdxExtensions;
66
- this.mdxCompilerOptions = mdxCompilerOptions;
67
- if (this.includeRadiant) {
68
- this.integrationDependencies.unshift(...this.getDependencies());
56
+ return [
57
+ createMdxLoaderPlugin({
58
+ compilerOptions: this.mdxCompilerOptions,
59
+ extensions: this.mdxExtensions,
60
+ projectRoot: app.rootDir,
61
+ requestPipelineMetrics: requireAppRuntime(app).requestPipelineMetrics,
62
+ }),
63
+ ];
64
+ }
65
+ /** Creates the renderer instance with the resolved JSX integration runtime options. */
66
+ createRenderer(base) {
67
+ const jsxConfig = {
68
+ mdxExtensions: this.mdxExtensions,
69
+ radiantSsrEnabled: this.includeRadiant,
70
+ };
71
+ const customRendererOptions = { ...base, jsxConfig };
72
+ const renderer = this.renderer === EcopagesJsxRenderer
73
+ ? new (createEcopagesJsxRenderer(jsxConfig))(base)
74
+ : new this.renderer({
75
+ ...customRendererOptions,
76
+ hooks: createEcopagesJsxRendererHookFactory(jsxConfig),
77
+ });
78
+ renderer.name ||= this.name;
79
+ return renderer;
69
80
  }
70
- }
71
- getDependencies() {
72
- return [
73
- AssetFactory.createNodeModuleScript({
74
- position: "head",
75
- importPath: "@ecopages/radiant/client/install-hydrator",
76
- bundle: false,
77
- attributes: {
78
- "data-eco-script-id": RADIANT_HYDRATOR_SCRIPT_ID
81
+ constructor(options) {
82
+ const config = resolvePluginOptions(options);
83
+ const { extensions, includeRadiant, mdxExtensions, mdxCompilerOptions, ...baseConfig } = config;
84
+ super({
85
+ name: ECOPAGES_JSX_PLUGIN_NAME,
86
+ extensions,
87
+ jsxImportSource: '@ecopages/jsx',
88
+ ...baseConfig,
89
+ });
90
+ this.includeRadiant = includeRadiant;
91
+ this.mdxExtensions = mdxExtensions;
92
+ this.mdxCompilerOptions = mdxCompilerOptions;
93
+ if (this.includeRadiant) {
94
+ this.integrationDependencies.unshift(...this.getDependencies());
79
95
  }
80
- })
81
- ];
82
- }
83
- /** Ensures MDX build hooks are ready before Ecopages collects contributions. */
84
- async prepareBuildContributions() {
85
- this.ensureMdxLoaderPlugin();
86
- }
87
- /**
88
- * Registers MDX tooling and completes the base integration setup.
89
- */
90
- async setup() {
91
- this.ensureMdxLoaderPlugin();
92
- if (typeof Bun !== "undefined" && this.mdxEnabled && this.mdxCompilerOptions) {
93
- await this.registerMdxBunPlugin();
94
96
  }
95
- await super.setup();
96
- }
97
- ensureMdxLoaderPlugin() {
98
- if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
99
- return;
97
+ getDependencies() {
98
+ return [
99
+ AssetFactory.createNodeModuleScript({
100
+ position: 'head',
101
+ importPath: RADIANT_INSTALL_HYDRATOR_FILEPATH,
102
+ bundle: false,
103
+ attributes: {
104
+ 'data-eco-script-id': RADIANT_HYDRATOR_SCRIPT_ID,
105
+ },
106
+ }),
107
+ ];
108
+ }
109
+ /** Assigns grouped-build metadata for page-owned JSX content scripts. */
110
+ prepareAssetDependencies(dependencies) {
111
+ return assignPageOwnedContentScriptGroupedBundles(dependencies);
100
112
  }
101
- this.mdxLoaderPlugin = createMdxLoaderPlugin(this.mdxCompilerOptions, this.mdxExtensions);
102
- }
103
- /**
104
- * Registers Bun's MDX loader at runtime setup time.
105
- *
106
- * Build-time contribution collection may run where Bun is absent, so
107
- * this hook stays isolated from manifest preparation.
108
- */
109
- async registerMdxBunPlugin() {
110
- if (typeof Bun === "undefined" || !this.mdxCompilerOptions) {
111
- return;
113
+ /** Registers JSX HMR strategy, dev-transform plugins, and Radiant SSR invalidation hooks. */
114
+ hmr(context) {
115
+ super.hmr(context);
116
+ const manager = context.runtime.hmr;
117
+ if (!manager)
118
+ return;
119
+ const preloadScopes = context.runtime.customElementSsrPreloadScopes;
120
+ const hmrContext = manager.getDefaultContext();
121
+ const absolutePaths = context.appConfig.absolutePaths;
122
+ const strategy = new EcopagesJsxHmrStrategy({
123
+ getRegisteredEntrypoints: hmrContext.getRegisteredEntrypoints,
124
+ getSrcDir: hmrContext.getSrcDir,
125
+ getPagesDir: hmrContext.getPagesDir,
126
+ getLayoutsDir: hmrContext.getLayoutsDir,
127
+ getIncludesDir: () => absolutePaths.includesDir,
128
+ getTemplateExtensions: () => this.extensions,
129
+ });
130
+ manager.registerStrategy(strategy);
131
+ manager.registerDevTransformContributor(new EcopagesJsxDevTransformContributor({
132
+ strategy,
133
+ getMdxLoaderPlugin: () => context.runtime.buildManifest.server.find((plugin) => plugin.name === 'ecopages-jsx-mdx-loader'),
134
+ }));
135
+ if (!this.includeRadiant) {
136
+ return;
137
+ }
138
+ if (preloadScopes) {
139
+ manager.onScriptEntrypointChanged?.((scriptPath) => invalidateRadiantRegisteredScriptSsrRegistration(preloadScopes, scriptPath));
140
+ }
112
141
  }
113
- await registerBunMdxPlugin(this.mdxCompilerOptions, this.mdxExtensions);
114
- }
115
142
  }
116
- const ecopagesJsxPlugin = (options) => new EcopagesJsxPlugin(options);
117
- export {
118
- EcopagesJsxPlugin,
119
- ecopagesJsxPlugin
120
- };
143
+ /**
144
+ * Creates the JSX integration plugin with resolved defaults.
145
+ */
146
+ export const ecopagesJsxPlugin = (options) => new EcopagesJsxPlugin(options);
@@ -1,34 +1,13 @@
1
- import type { CompileOptions } from '@mdx-js/mdx';
2
- import type { IntegrationPluginConfig } from '@ecopages/core/plugins/integration-plugin';
3
- import type { AssetDefinition, AssetProcessingService, ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
4
- import type { EcoPagesAppConfig } from '@ecopages/core';
5
- import type { EcopagesJsxRadiantSsrPolicy } from './ecopages-jsx-radiant-ssr-policy.js';
6
- type MdxPluginList = NonNullable<CompileOptions['remarkPlugins']>;
7
- export type EcopagesJsxMdxCompileOptions = Omit<CompileOptions, 'jsxImportSource' | 'jsxRuntime' | 'remarkPlugins' | 'rehypePlugins' | 'recmaPlugins'> & {
8
- remarkPlugins?: MdxPluginList;
9
- rehypePlugins?: MdxPluginList;
10
- recmaPlugins?: MdxPluginList;
11
- };
1
+ import type { MdxCompilerOptionsInput } from '@ecopages/mdx/core';
2
+ import type { IntegrationPluginConfig } from '@ecopages/core/integration';
3
+ import type { AssetDefinition } from '@ecopages/core/integration';
12
4
  /**
13
5
  * MDX configuration for the JSX integration.
14
6
  *
15
7
  * Mirrors Ecopages' built-in combined integration pattern where a single
16
8
  * JSX-capable plugin can own both `.tsx` and `.mdx` route files.
17
9
  */
18
- export type EcopagesJsxMdxOptions = {
19
- /** Enables MDX file handling inside the JSX integration. */
20
- enabled: boolean;
21
- /** Additional MDX compiler options. JSX runtime fields are managed by the integration. */
22
- compilerOptions?: EcopagesJsxMdxCompileOptions;
23
- /** Extra remark plugins appended to `compilerOptions.remarkPlugins`. */
24
- remarkPlugins?: MdxPluginList;
25
- /** Extra rehype plugins appended to `compilerOptions.rehypePlugins`. */
26
- rehypePlugins?: MdxPluginList;
27
- /** Extra recma plugins appended to `compilerOptions.recmaPlugins`. */
28
- recmaPlugins?: MdxPluginList;
29
- /** Custom file extensions to treat as MDX. */
30
- extensions?: string[];
31
- };
10
+ export type EcopagesJsxMdxOptions = MdxCompilerOptionsInput;
32
11
  /** Options for the JSX integration plugin. */
33
12
  export type EcopagesJsxPluginOptions = Omit<IntegrationPluginConfig, 'name' | 'extensions'> & {
34
13
  /** Optional JSX route extensions. Defaults to `.tsx`. */
@@ -48,15 +27,6 @@ export type EcopagesJsxPluginOptions = Omit<IntegrationPluginConfig, 'name' | 'e
48
27
  };
49
28
  export type EcopagesJsxRendererConfig = {
50
29
  mdxExtensions?: string[];
51
- radiantSsrPolicy?: EcopagesJsxRadiantSsrPolicy;
52
30
  radiantSsrEnabled?: boolean;
53
31
  };
54
- export type EcopagesJsxRendererOptions = {
55
- appConfig: EcoPagesAppConfig;
56
- assetProcessingService: AssetProcessingService;
57
- resolvedIntegrationDependencies: ProcessedAsset[];
58
- rendererModules?: unknown;
59
- runtimeOrigin: string;
60
- jsxConfig?: EcopagesJsxRendererConfig;
61
- };
62
32
  export type { AssetDefinition };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { type AssetDefinition } from '@ecopages/core/integration';
2
+ /** Returns the app-wide grouped-build id used for page-owned content scripts. */
3
+ export declare function createPageOwnedContentScriptBundleId(_integrationName?: string): string;
4
+ /**
5
+ * Tags page-owned content scripts in one dependency batch with the app-wide client bundle id.
6
+ *
7
+ * @remarks
8
+ * Component file scripts and integration runtime assets stay ungrouped. Entries that
9
+ * already assign a grouped id are left unchanged.
10
+ */
11
+ export declare function assignPageOwnedContentScriptGroupedBundles(dependencies: AssetDefinition[]): AssetDefinition[];
@@ -0,0 +1,57 @@
1
+ import { APP_BROWSER_CLIENT_BUNDLE_ID, } from '@ecopages/core/integration';
2
+ import { contentHash } from '@ecopages/core/build';
3
+ /** Returns the app-wide grouped-build id used for page-owned content scripts. */
4
+ export function createPageOwnedContentScriptBundleId(_integrationName) {
5
+ return APP_BROWSER_CLIENT_BUNDLE_ID;
6
+ }
7
+ function isPageOwnedGroupableContentScript(dep) {
8
+ if (dep.kind !== 'script' || dep.source !== 'content' || dep.inline) {
9
+ return false;
10
+ }
11
+ if (dep.groupedBundle?.id) {
12
+ return false;
13
+ }
14
+ if (dep.packageRole === 'keep-separate' || dep.packageRole === 'runtime' || dep.packageRole === 'dynamic-chunk') {
15
+ return false;
16
+ }
17
+ return true;
18
+ }
19
+ function createPageOwnedGroupedEntryName(dep, usedEntryNames) {
20
+ const baseName = dep.name ?? `script-${contentHash(dep.content, 16)}`;
21
+ if (!usedEntryNames.has(baseName)) {
22
+ usedEntryNames.add(baseName);
23
+ return baseName;
24
+ }
25
+ let suffix = 2;
26
+ let candidate = `${baseName}-${suffix}`;
27
+ while (usedEntryNames.has(candidate)) {
28
+ suffix += 1;
29
+ candidate = `${baseName}-${suffix}`;
30
+ }
31
+ usedEntryNames.add(candidate);
32
+ return candidate;
33
+ }
34
+ /**
35
+ * Tags page-owned content scripts in one dependency batch with the app-wide client bundle id.
36
+ *
37
+ * @remarks
38
+ * Component file scripts and integration runtime assets stay ungrouped. Entries that
39
+ * already assign a grouped id are left unchanged.
40
+ */
41
+ export function assignPageOwnedContentScriptGroupedBundles(dependencies) {
42
+ const bundleId = APP_BROWSER_CLIENT_BUNDLE_ID;
43
+ const usedEntryNames = new Set();
44
+ for (const dependency of dependencies) {
45
+ if (!isPageOwnedGroupableContentScript(dependency)) {
46
+ continue;
47
+ }
48
+ dependency.groupedBundle = {
49
+ id: bundleId,
50
+ entryName: createPageOwnedGroupedEntryName(dependency, usedEntryNames),
51
+ };
52
+ if (dependency.bundle === false) {
53
+ dependency.bundle = true;
54
+ }
55
+ }
56
+ return dependencies;
57
+ }
@@ -0,0 +1,19 @@
1
+ import type { CustomElementScriptPreloadScopeState } from '@ecopages/core/integration';
2
+ /**
3
+ * Reads the custom-element tag name from a registered script module source file.
4
+ */
5
+ export declare function resolveRadiantCustomElementTag(scriptPath: string): string | undefined;
6
+ /**
7
+ * Removes one custom-element definition from the active runtime registry.
8
+ *
9
+ * @remarks
10
+ * Radiant's `@customElement` decorator and the SSR light-DOM shim both skip
11
+ * re-registration when a tag is already defined. During dev, server requests
12
+ * reuse the same process-global registry, so script edits would otherwise keep
13
+ * serving stale SSR markup until a full dev-server restart.
14
+ */
15
+ export declare function clearRadiantCustomElementDefinition(tagName: string): void;
16
+ /**
17
+ * Clears the SSR custom-element registry entry for one registered Radiant script module.
18
+ */
19
+ export declare function invalidateRadiantRegisteredScriptSsrRegistration(preloadScopes: Map<string, CustomElementScriptPreloadScopeState>, scriptPath: string): boolean;
@@ -0,0 +1,49 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, invalidateCustomElementScriptPreload, } from '@ecopages/core/integration';
3
+ const CUSTOM_ELEMENT_TAG_PATTERN = /@customElement\s*\(\s*['"`]([^'"`]+)['"`]/u;
4
+ /**
5
+ * Reads the custom-element tag name from a registered script module source file.
6
+ */
7
+ export function resolveRadiantCustomElementTag(scriptPath) {
8
+ if (!existsSync(scriptPath)) {
9
+ return undefined;
10
+ }
11
+ const content = readFileSync(scriptPath, 'utf8');
12
+ const match = content.match(CUSTOM_ELEMENT_TAG_PATTERN);
13
+ return match?.[1];
14
+ }
15
+ /**
16
+ * Removes one custom-element definition from the active runtime registry.
17
+ *
18
+ * @remarks
19
+ * Radiant's `@customElement` decorator and the SSR light-DOM shim both skip
20
+ * re-registration when a tag is already defined. During dev, server requests
21
+ * reuse the same process-global registry, so script edits would otherwise keep
22
+ * serving stale SSR markup until a full dev-server restart.
23
+ */
24
+ export function clearRadiantCustomElementDefinition(tagName) {
25
+ const registry = globalThis.customElements;
26
+ if (!registry) {
27
+ return;
28
+ }
29
+ if (typeof registry.delete === 'function') {
30
+ registry.delete(tagName);
31
+ return;
32
+ }
33
+ const definitions = registry.definitions;
34
+ if (definitions instanceof Map && definitions.has(tagName)) {
35
+ definitions.delete(tagName);
36
+ }
37
+ }
38
+ /**
39
+ * Clears the SSR custom-element registry entry for one registered Radiant script module.
40
+ */
41
+ export function invalidateRadiantRegisteredScriptSsrRegistration(preloadScopes, scriptPath) {
42
+ invalidateCustomElementScriptPreload(preloadScopes, scriptPath, CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.ecopagesJsx);
43
+ const tagName = resolveRadiantCustomElementTag(scriptPath);
44
+ if (!tagName) {
45
+ return false;
46
+ }
47
+ clearRadiantCustomElementDefinition(tagName);
48
+ return true;
49
+ }
@@ -0,0 +1,2 @@
1
+ /** Absolute path to `@ecopages/radiant/client/install-hydrator` from this integration package. */
2
+ export declare const RADIANT_INSTALL_HYDRATOR_FILEPATH: string;
@@ -0,0 +1,3 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ /** Absolute path to `@ecopages/radiant/client/install-hydrator` from this integration package. */
3
+ export const RADIANT_INSTALL_HYDRATOR_FILEPATH = fileURLToPath(import.meta.resolve('@ecopages/radiant/client/install-hydrator'));
@@ -1,46 +0,0 @@
1
- import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
2
- export declare const ECOPAGES_JSX_SSR_RENDER_STATE_KEY: unique symbol;
3
- /**
4
- * Tracks JSX SSR asset-collection state for one active render flow.
5
- *
6
- * @remarks
7
- * The renderer still mirrors this state into `@ecopages/jsx/server` so nested
8
- * JSX renders and existing SSR-scope probes can observe it, but it also keeps
9
- * an internal async-local copy. That fallback avoids losing renderer state when
10
- * host runtimes such as Vite end up loading separate module identities for the
11
- * JSX server helpers.
12
- */
13
- export declare class EcopagesJsxRenderSession {
14
- private readonly dedupeProcessedAssets;
15
- constructor(dedupeProcessedAssets: (assets: ProcessedAsset[]) => ProcessedAsset[]);
16
- /**
17
- * Runs one render inside the active session scope.
18
- *
19
- * @remarks
20
- * When a render is already active, the current session state is reused and
21
- * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
22
- * the same asset frame stack. When no session exists yet, a new state is
23
- * created and published through both the internal async-local store and the
24
- * JSX SSR scope bridge.
25
- */
26
- withActiveScope<T>(render: () => T): T;
27
- /**
28
- * Runs work with the current page-level hydrate binding namespace attached to JSX SSR.
29
- *
30
- * This lets page, layout, and document renders participate in one client-owned
31
- * hydration root even though Ecopages composes them through multiple sibling render calls.
32
- */
33
- withHydrationBindingScope<T>(render: () => T): T;
34
- beginCollectedAssetFrame(): ProcessedAsset[];
35
- endCollectedAssetFrame(frame: ProcessedAsset[]): ProcessedAsset[];
36
- recordCollectedAssets(collectedAssets: ProcessedAsset[]): ProcessedAsset[];
37
- /**
38
- * Resolves the current render-session state from the local async scope first.
39
- *
40
- * @remarks
41
- * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
42
- * that still inspects the public SSR scope directly, while the async-local
43
- * store remains the authoritative source for renderer-owned bookkeeping.
44
- */
45
- private getState;
46
- }
@@ -1,93 +0,0 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- import {
3
- createServerHydrationBindingState,
4
- getActiveSsrScopeValue,
5
- withActiveSsrScopeValue,
6
- withServerHydrationBindingState
7
- } from "@ecopages/jsx/server";
8
- const ECOPAGES_JSX_SSR_RENDER_STATE_KEY = /* @__PURE__ */ Symbol.for("@ecopages/ecopages-jsx.ssr-render-state");
9
- const renderStateStorage = new AsyncLocalStorage();
10
- class EcopagesJsxRenderSession {
11
- dedupeProcessedAssets;
12
- constructor(dedupeProcessedAssets) {
13
- this.dedupeProcessedAssets = dedupeProcessedAssets;
14
- }
15
- /**
16
- * Runs one render inside the active session scope.
17
- *
18
- * @remarks
19
- * When a render is already active, the current session state is reused and
20
- * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
21
- * the same asset frame stack. When no session exists yet, a new state is
22
- * created and published through both the internal async-local store and the
23
- * JSX SSR scope bridge.
24
- */
25
- withActiveScope(render) {
26
- const activeState = renderStateStorage.getStore();
27
- if (activeState) {
28
- return withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, activeState, render);
29
- }
30
- const jsxScopeState = getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
31
- if (jsxScopeState) {
32
- return renderStateStorage.run(jsxScopeState, () => render());
33
- }
34
- const state = {
35
- collectedAssetFrames: [],
36
- hydrationBindingState: createServerHydrationBindingState()
37
- };
38
- return renderStateStorage.run(
39
- state,
40
- () => withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, state, render)
41
- );
42
- }
43
- /**
44
- * Runs work with the current page-level hydrate binding namespace attached to JSX SSR.
45
- *
46
- * This lets page, layout, and document renders participate in one client-owned
47
- * hydration root even though Ecopages composes them through multiple sibling render calls.
48
- */
49
- withHydrationBindingScope(render) {
50
- return withServerHydrationBindingState(this.getState().hydrationBindingState, render);
51
- }
52
- beginCollectedAssetFrame() {
53
- const state = this.getState();
54
- const frame = [];
55
- state.collectedAssetFrames.push(frame);
56
- return frame;
57
- }
58
- endCollectedAssetFrame(frame) {
59
- const activeFrame = this.getState().collectedAssetFrames.pop();
60
- if (!activeFrame || activeFrame !== frame) {
61
- return this.dedupeProcessedAssets(frame);
62
- }
63
- return this.dedupeProcessedAssets(activeFrame);
64
- }
65
- recordCollectedAssets(collectedAssets) {
66
- const dedupedAssets = this.dedupeProcessedAssets(collectedAssets);
67
- const state = this.getState();
68
- const activeFrame = state.collectedAssetFrames[state.collectedAssetFrames.length - 1];
69
- if (activeFrame) {
70
- activeFrame.push(...dedupedAssets);
71
- }
72
- return dedupedAssets;
73
- }
74
- /**
75
- * Resolves the current render-session state from the local async scope first.
76
- *
77
- * @remarks
78
- * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
79
- * that still inspects the public SSR scope directly, while the async-local
80
- * store remains the authoritative source for renderer-owned bookkeeping.
81
- */
82
- getState() {
83
- const state = renderStateStorage.getStore() ?? getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
84
- if (!state) {
85
- throw new Error("Ecopages JSX SSR render state is unavailable outside the active render scope.");
86
- }
87
- return state;
88
- }
89
- }
90
- export {
91
- ECOPAGES_JSX_SSR_RENDER_STATE_KEY,
92
- EcopagesJsxRenderSession
93
- };