@ecopages/react 0.2.0-alpha.9 → 0.2.0-beta.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.
Files changed (77) hide show
  1. package/README.md +30 -13
  2. package/package.json +23 -12
  3. package/src/eco-embed.d.ts +11 -0
  4. package/src/eco-embed.js +11 -0
  5. package/src/react-hmr-strategy.d.ts +102 -18
  6. package/src/react-hmr-strategy.js +427 -50
  7. package/src/react-renderer.d.ts +100 -92
  8. package/src/react-renderer.js +356 -340
  9. package/src/react.constants.d.ts +1 -0
  10. package/src/react.constants.js +4 -0
  11. package/src/react.plugin.d.ts +25 -107
  12. package/src/react.plugin.js +109 -61
  13. package/src/react.types.d.ts +88 -0
  14. package/src/react.types.js +0 -0
  15. package/src/router-adapter.d.ts +7 -14
  16. package/src/runtime/use-sync-external-store-with-selector.d.ts +3 -0
  17. package/src/runtime/use-sync-external-store-with-selector.js +56 -0
  18. package/src/services/pages-index.d.ts +64 -0
  19. package/src/services/pages-index.js +73 -0
  20. package/src/services/react-bundle.service.d.ts +24 -9
  21. package/src/services/react-bundle.service.js +35 -24
  22. package/src/services/react-hmr-page-metadata-cache.d.ts +10 -1
  23. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  24. package/src/services/react-hydration-asset.service.d.ts +28 -19
  25. package/src/services/react-hydration-asset.service.js +83 -64
  26. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  27. package/src/services/react-mdx-config-dependency.service.js +122 -0
  28. package/src/services/react-page-module.service.d.ts +8 -3
  29. package/src/services/react-page-module.service.js +33 -26
  30. package/src/services/react-page-payload.service.d.ts +46 -0
  31. package/src/services/react-page-payload.service.js +67 -0
  32. package/src/services/react-runtime-bundle.service.d.ts +9 -2
  33. package/src/services/react-runtime-bundle.service.js +77 -16
  34. package/src/utils/client-graph-boundary-cache.d.ts +108 -0
  35. package/src/utils/client-graph-boundary-cache.js +116 -0
  36. package/src/utils/client-graph-boundary-plugin.d.ts +13 -5
  37. package/src/utils/client-graph-boundary-plugin.js +63 -5
  38. package/src/utils/component-config-traversal.d.ts +36 -0
  39. package/src/utils/component-config-traversal.js +54 -0
  40. package/src/utils/declared-modules.d.ts +1 -1
  41. package/src/utils/declared-modules.js +7 -16
  42. package/src/utils/dynamic.test.browser.d.ts +1 -0
  43. package/src/utils/dynamic.test.browser.js +33 -0
  44. package/src/utils/hydration-scripts.d.ts +9 -5
  45. package/src/utils/hydration-scripts.js +119 -34
  46. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  47. package/src/utils/hydration-scripts.test.browser.js +198 -0
  48. package/src/utils/react-dom-runtime-interop-plugin.d.ts +1 -1
  49. package/src/utils/react-dom-runtime-interop-plugin.js +9 -0
  50. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  51. package/src/utils/{react-runtime-specifier-map.d.ts → react-runtime-alias-map.d.ts} +3 -1
  52. package/src/utils/react-runtime-alias-map.js +90 -0
  53. package/CHANGELOG.md +0 -27
  54. package/src/react-hmr-strategy.ts +0 -386
  55. package/src/react-renderer.ts +0 -803
  56. package/src/react.plugin.ts +0 -276
  57. package/src/router-adapter.ts +0 -95
  58. package/src/services/react-bundle.service.ts +0 -108
  59. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  60. package/src/services/react-hydration-asset.service.ts +0 -263
  61. package/src/services/react-page-module.service.ts +0 -224
  62. package/src/services/react-runtime-bundle.service.ts +0 -172
  63. package/src/utils/client-graph-boundary-plugin.ts +0 -831
  64. package/src/utils/client-only.ts +0 -27
  65. package/src/utils/declared-modules.ts +0 -99
  66. package/src/utils/dynamic.ts +0 -27
  67. package/src/utils/hmr-scripts.ts +0 -47
  68. package/src/utils/html-boundary.ts +0 -66
  69. package/src/utils/hydration-scripts.ts +0 -459
  70. package/src/utils/reachability-analyzer.ts +0 -593
  71. package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
  72. package/src/utils/react-mdx-loader-plugin.ts +0 -63
  73. package/src/utils/react-runtime-specifier-map.js +0 -37
  74. package/src/utils/react-runtime-specifier-map.ts +0 -45
  75. package/src/utils/use-sync-external-store-shim-plugin.d.ts +0 -5
  76. package/src/utils/use-sync-external-store-shim-plugin.js +0 -41
  77. package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
@@ -0,0 +1 @@
1
+ export declare const REACT_PLUGIN_NAME = "react";
@@ -0,0 +1,4 @@
1
+ const REACT_PLUGIN_NAME = "react";
2
+ export {
3
+ REACT_PLUGIN_NAME
4
+ };
@@ -1,93 +1,10 @@
1
- /**
2
- * This module contains the react plugin for Ecopages
3
- * @module
4
- */
5
- import { IntegrationPlugin } from '@ecopages/core/plugins/integration-plugin';
6
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
1
+ import { IntegrationPlugin, type EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
2
+ import type { BrowserRuntimeManifest } from '@ecopages/core/build/browser-runtime-manifest';
7
3
  import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
8
- import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
9
- import type { CompileOptions } from '@mdx-js/mdx';
10
4
  import type React from 'react';
11
5
  import { ReactRenderer } from './react-renderer.js';
12
- import type { ReactRouterAdapter } from './router-adapter.js';
13
- import type { ComponentBoundaryPolicyInput } from '@ecopages/core/plugins/integration-plugin';
14
- /**
15
- * MDX configuration options for the React plugin
16
- */
17
- export type ReactMdxOptions = {
18
- /**
19
- * Whether to enable MDX support.
20
- * @default false
21
- */
22
- enabled: boolean;
23
- /**
24
- * Compiler options for MDX.
25
- * @default undefined
26
- */
27
- compilerOptions?: Omit<CompileOptions, 'jsxImportSource' | 'jsxRuntime'>;
28
- /**
29
- * Remark plugins.
30
- * @default undefined
31
- */
32
- remarkPlugins?: CompileOptions['remarkPlugins'];
33
- /**
34
- * Rehype plugins.
35
- * @default undefined
36
- */
37
- rehypePlugins?: CompileOptions['rehypePlugins'];
38
- /**
39
- * Recma plugins.
40
- * @default undefined
41
- */
42
- recmaPlugins?: CompileOptions['recmaPlugins'];
43
- /**
44
- * Custom extensions to be treated as MDX files.
45
- * @default ['.mdx']
46
- */
47
- extensions?: string[];
48
- };
49
- /**
50
- * Options for the React plugin
51
- */
52
- export type ReactPluginOptions = {
53
- extensions?: string[];
54
- dependencies?: AssetDefinition[];
55
- /**
56
- * Enables explicit client graph mode for React page entries.
57
- *
58
- * When enabled, React page-entry bundling relies on explicit dependency declarations
59
- * and skips AST-based `middleware`/`requires` stripping in the React path.
60
- * @default false
61
- */
62
- explicitGraph?: boolean;
63
- /**
64
- * Router adapter for SPA navigation.
65
- * When provided, pages with layouts will be wrapped in the router for client-side navigation.
66
- * @example
67
- * ```ts
68
- * import { ecoRouter } from '@ecopages/react-router';
69
- * reactPlugin({ router: ecoRouter() })
70
- * ```
71
- */
72
- router?: ReactRouterAdapter;
73
- /**
74
- * MDX configuration for handling .mdx files within the React plugin.
75
- * When enabled, MDX files are treated as React pages with full router support.
76
- * @example
77
- * ```ts
78
- * reactPlugin({
79
- * router: ecoRouter(),
80
- * mdx: {
81
- * enabled: true,
82
- * extensions: ['.mdx', '.md'],
83
- * remarkPlugins: [remarkGfm],
84
- * rehypePlugins: [[rehypePrettyCode, { theme: '...' }]],
85
- * }
86
- * })
87
- * ```
88
- */
89
- mdx?: ReactMdxOptions;
90
- };
6
+ import type { ReactPluginOptions } from './react.types.js';
7
+ export type { ReactMdxOptions, ReactPluginOptions, ReactRendererConfig } from './react.types.js';
91
8
  /**
92
9
  * The name of the React plugin
93
10
  */
@@ -96,23 +13,36 @@ export declare const PLUGIN_NAME = "react";
96
13
  * The React plugin class
97
14
  * This plugin provides support for React components in Ecopages
98
15
  */
99
- export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
16
+ export declare class ReactPlugin extends IntegrationPlugin<React.ReactNode> {
100
17
  renderer: typeof ReactRenderer;
101
- routerAdapter: ReactRouterAdapter | undefined;
102
- private mdxEnabled;
103
- private mdxCompilerOptions?;
104
- private mdxExtensions;
18
+ private readonly routerAdapter;
19
+ private readonly mdxEnabled;
20
+ private readonly mdxCompilerOptions?;
21
+ private readonly mdxExtensions;
105
22
  private mdxLoaderPlugin;
106
- private runtimeBundleService;
23
+ private readonly runtimeBundleService;
107
24
  private readonly hmrPageMetadataCache;
25
+ private readonly clientGraphBoundaryCache;
108
26
  private runtimeDependenciesInitialized;
109
27
  /**
110
28
  * Indicates whether React explicit graph mode is enabled for renderer/HMR behavior.
111
29
  */
112
- private explicitGraphEnabled;
113
- constructor(options?: Omit<ReactPluginOptions, 'name'>);
30
+ private readonly explicitGraphEnabled;
31
+ private readonly rendererConfig;
32
+ constructor(options?: ReactPluginOptions);
33
+ /**
34
+ * Creates a React renderer with instance-owned runtime configuration.
35
+ *
36
+ * React renderers depend on plugin-owned router, MDX, and HMR metadata state.
37
+ * Keeping that state on the instance avoids cross-plugin static mutation while
38
+ * preserving the same runtime services the base initializer wires up.
39
+ */
40
+ initializeRenderer(options?: {
41
+ rendererModules?: unknown;
42
+ }): ReactRenderer;
114
43
  private ensureRuntimeDependencies;
115
44
  get plugins(): EcoBuildPlugin[];
45
+ get browserRuntimeManifest(): BrowserRuntimeManifest;
116
46
  /**
117
47
  * Ensures the optional React MDX loader exists before either config-time
118
48
  * manifest sealing or runtime setup needs it.
@@ -138,18 +68,6 @@ export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
138
68
  * @returns ReactHmrStrategy instance for handling React component updates
139
69
  */
140
70
  getHmrStrategy(): HmrStrategy | undefined;
141
- getRuntimeSpecifierMap(): Record<string, string>;
142
- /**
143
- * Declares React's boundary deferral rule for cross-integration rendering.
144
- *
145
- * React defers when a render pass owned by another integration enters a React
146
- * component boundary. That boundary is then resolved later through the marker
147
- * graph stage using the React renderer.
148
- *
149
- * @param input Boundary metadata for the active render pass.
150
- * @returns `true` when the boundary should be deferred into the marker pass.
151
- */
152
- shouldDeferComponentBoundary(input: ComponentBoundaryPolicyInput): boolean;
153
71
  }
154
72
  /**
155
73
  * Factory function to create a React plugin instance
@@ -1,11 +1,69 @@
1
- import { IntegrationPlugin } from "@ecopages/core/plugins/integration-plugin";
1
+ import {
2
+ IntegrationPlugin
3
+ } from "@ecopages/core/plugins/integration-plugin";
2
4
  import { Logger } from "@ecopages/logger";
5
+ import { REACT_PLUGIN_NAME } from "./react.constants.js";
3
6
  import { ReactRenderer } from "./react-renderer.js";
4
7
  import { ReactHmrStrategy } from "./react-hmr-strategy.js";
5
8
  import { ReactRuntimeBundleService } from "./services/react-runtime-bundle.service.js";
6
9
  import { ReactHmrPageMetadataCache } from "./services/react-hmr-page-metadata-cache.js";
10
+ import { createReactMdxLoaderPlugin } from "./utils/react-mdx-loader-plugin.js";
11
+ import { ClientGraphBoundaryCache } from "./utils/client-graph-boundary-cache.js";
7
12
  const appLogger = new Logger("[ReactPlugin]");
8
- const PLUGIN_NAME = "react";
13
+ const PLUGIN_NAME = REACT_PLUGIN_NAME;
14
+ const mergePluginLists = (...lists) => {
15
+ const merged = lists.flatMap((list) => list ? [...list] : []);
16
+ return merged.length > 0 ? merged : void 0;
17
+ };
18
+ const appendMdxExtensions = (target, mdxExtensions) => {
19
+ for (const extension of mdxExtensions) {
20
+ if (!target.includes(extension)) {
21
+ target.push(extension);
22
+ }
23
+ }
24
+ };
25
+ const resolveReactMdxCompilerOptions = (mdxOptions) => {
26
+ const { compilerOptions, remarkPlugins, rehypePlugins, recmaPlugins } = mdxOptions;
27
+ const resolved = {
28
+ ...compilerOptions,
29
+ jsxImportSource: "react",
30
+ jsxRuntime: "automatic",
31
+ development: process.env.NODE_ENV === "development"
32
+ };
33
+ const mergedRemark = mergePluginLists(compilerOptions?.remarkPlugins, remarkPlugins);
34
+ const mergedRehype = mergePluginLists(compilerOptions?.rehypePlugins, rehypePlugins);
35
+ const mergedRecma = mergePluginLists(compilerOptions?.recmaPlugins, recmaPlugins);
36
+ if (mergedRemark) resolved.remarkPlugins = mergedRemark;
37
+ if (mergedRehype) resolved.rehypePlugins = mergedRehype;
38
+ if (mergedRecma) resolved.recmaPlugins = mergedRecma;
39
+ return resolved;
40
+ };
41
+ const resolveReactPluginOptions = (options) => {
42
+ const { extensions: userExtensions, router, mdx, explicitGraph, dependencies, ...baseConfig } = options ?? {};
43
+ const extensions = [...userExtensions ?? [".tsx"]];
44
+ const mdxEnabled = mdx?.enabled ?? false;
45
+ const mdxExtensions = mdx?.extensions ?? [".mdx"];
46
+ if (mdxEnabled) {
47
+ appendMdxExtensions(extensions, mdxExtensions);
48
+ } else if (mdx?.extensions?.length) {
49
+ appLogger.warn(
50
+ "MDX extensions provided but MDX is disabled. MDX files will not be processed. Set mdx.enabled to true to enable MDX support."
51
+ );
52
+ }
53
+ const rendererConfig = {
54
+ routerAdapter: router,
55
+ mdxCompilerOptions: mdxEnabled && mdx ? resolveReactMdxCompilerOptions(mdx) : void 0,
56
+ mdxExtensions,
57
+ hmrPageMetadataCache: new ReactHmrPageMetadataCache(),
58
+ explicitGraphEnabled: explicitGraph ?? false
59
+ };
60
+ return {
61
+ ...baseConfig,
62
+ extensions,
63
+ integrationDependencies: dependencies,
64
+ rendererConfig
65
+ };
66
+ };
9
67
  class ReactPlugin extends IntegrationPlugin {
10
68
  renderer = ReactRenderer;
11
69
  routerAdapter;
@@ -14,62 +72,63 @@ class ReactPlugin extends IntegrationPlugin {
14
72
  mdxExtensions;
15
73
  mdxLoaderPlugin;
16
74
  runtimeBundleService;
17
- hmrPageMetadataCache = new ReactHmrPageMetadataCache();
75
+ hmrPageMetadataCache;
76
+ clientGraphBoundaryCache;
18
77
  runtimeDependenciesInitialized = false;
19
78
  /**
20
79
  * Indicates whether React explicit graph mode is enabled for renderer/HMR behavior.
21
80
  */
22
81
  explicitGraphEnabled;
82
+ rendererConfig;
23
83
  constructor(options) {
24
- const { extensions: _ignoredExtensions, ...restOptions } = options ?? {};
25
- const extensions = [...options?.extensions ?? [".tsx"]];
26
- const mdxExtensions = options?.mdx?.extensions ?? [".mdx"];
27
- if (options?.mdx?.enabled) {
28
- for (const extension of mdxExtensions) {
29
- if (!extensions.includes(extension)) {
30
- extensions.push(extension);
31
- }
32
- }
33
- } else if (options?.mdx?.extensions?.length) {
34
- appLogger.warn(
35
- "MDX extensions provided but MDX is disabled. MDX files will not be processed. Set mdx.enabled to true to enable MDX support."
36
- );
37
- }
84
+ const config = resolveReactPluginOptions(options);
85
+ const { extensions, rendererConfig, integrationDependencies, ...baseConfig } = config;
38
86
  super({
39
87
  name: PLUGIN_NAME,
40
88
  extensions,
41
- ...restOptions
89
+ jsxImportSource: "react",
90
+ integrationDependencies,
91
+ ...baseConfig
42
92
  });
43
- this.mdxEnabled = options?.mdx?.enabled ?? false;
44
- this.mdxExtensions = mdxExtensions;
93
+ this.routerAdapter = rendererConfig.routerAdapter;
94
+ this.mdxCompilerOptions = rendererConfig.mdxCompilerOptions;
95
+ this.mdxEnabled = Boolean(rendererConfig.mdxCompilerOptions);
96
+ this.mdxExtensions = rendererConfig.mdxExtensions ?? [".mdx"];
97
+ this.hmrPageMetadataCache = rendererConfig.hmrPageMetadataCache ?? new ReactHmrPageMetadataCache();
98
+ this.clientGraphBoundaryCache = new ClientGraphBoundaryCache();
99
+ this.explicitGraphEnabled = rendererConfig.explicitGraphEnabled ?? false;
100
+ this.rendererConfig = {
101
+ ...rendererConfig,
102
+ mdxExtensions: this.mdxExtensions,
103
+ hmrPageMetadataCache: this.hmrPageMetadataCache,
104
+ explicitGraphEnabled: this.explicitGraphEnabled
105
+ };
45
106
  if (this.mdxEnabled) {
46
- const { compilerOptions, remarkPlugins, rehypePlugins, recmaPlugins } = options?.mdx || {};
47
- this.mdxCompilerOptions = {
48
- ...compilerOptions,
49
- remarkPlugins: [...compilerOptions?.remarkPlugins || [], ...remarkPlugins || []],
50
- rehypePlugins: [...compilerOptions?.rehypePlugins || [], ...rehypePlugins || []],
51
- recmaPlugins: [...compilerOptions?.recmaPlugins || [], ...recmaPlugins || []],
52
- jsxImportSource: "react",
53
- jsxRuntime: "automatic",
54
- development: process.env.NODE_ENV === "development"
55
- };
56
107
  appLogger.debug("MDX mode enabled with React jsx runtime");
57
108
  }
58
- this.routerAdapter = options?.router;
59
109
  this.runtimeBundleService = new ReactRuntimeBundleService({
60
110
  routerAdapter: this.routerAdapter
61
111
  });
62
- this.explicitGraphEnabled = options?.explicitGraph ?? false;
63
- ReactRenderer.routerAdapter = this.routerAdapter;
64
- ReactRenderer.mdxCompilerOptions = this.mdxCompilerOptions;
65
- ReactRenderer.mdxExtensions = this.mdxExtensions;
66
- ReactRenderer.explicitGraphEnabled = this.explicitGraphEnabled;
67
- ReactRenderer.hmrPageMetadataCache = this.hmrPageMetadataCache;
112
+ }
113
+ /**
114
+ * Creates a React renderer with instance-owned runtime configuration.
115
+ *
116
+ * React renderers depend on plugin-owned router, MDX, and HMR metadata state.
117
+ * Keeping that state on the instance avoids cross-plugin static mutation while
118
+ * preserving the same runtime services the base initializer wires up.
119
+ */
120
+ initializeRenderer(options) {
121
+ const renderer = new this.renderer({
122
+ ...this.createRendererOptions(options),
123
+ reactConfig: this.rendererConfig
124
+ });
125
+ return this.attachRendererRuntimeServices(renderer);
68
126
  }
69
127
  ensureRuntimeDependencies() {
70
128
  if (this.runtimeDependenciesInitialized) {
71
129
  return;
72
130
  }
131
+ this.runtimeBundleService.setRootDir(this.appConfig?.rootDir);
73
132
  this.integrationDependencies.unshift(...this.runtimeBundleService.getDependencies());
74
133
  this.runtimeDependenciesInitialized = true;
75
134
  }
@@ -79,6 +138,10 @@ class ReactPlugin extends IntegrationPlugin {
79
138
  }
80
139
  return [];
81
140
  }
141
+ get browserRuntimeManifest() {
142
+ this.ensureRuntimeDependencies();
143
+ return this.runtimeBundleService.getRuntimeManifest();
144
+ }
82
145
  /**
83
146
  * Ensures the optional React MDX loader exists before either config-time
84
147
  * manifest sealing or runtime setup needs it.
@@ -87,7 +150,6 @@ class ReactPlugin extends IntegrationPlugin {
87
150
  if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
88
151
  return;
89
152
  }
90
- const { createReactMdxLoaderPlugin } = await import("./utils/react-mdx-loader-plugin.js");
91
153
  this.mdxLoaderPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
92
154
  }
93
155
  /**
@@ -121,30 +183,16 @@ class ReactPlugin extends IntegrationPlugin {
121
183
  return void 0;
122
184
  }
123
185
  const context = this.hmrManager.getDefaultContext();
124
- return new ReactHmrStrategy(
186
+ return new ReactHmrStrategy({
125
187
  context,
126
- this.hmrPageMetadataCache,
127
- this.mdxCompilerOptions,
128
- this.extensions,
129
- this.appConfig.templatesExt,
130
- this.explicitGraphEnabled
131
- );
132
- }
133
- getRuntimeSpecifierMap() {
134
- return this.runtimeBundleService.getSpecifierMap();
135
- }
136
- /**
137
- * Declares React's boundary deferral rule for cross-integration rendering.
138
- *
139
- * React defers when a render pass owned by another integration enters a React
140
- * component boundary. That boundary is then resolved later through the marker
141
- * graph stage using the React renderer.
142
- *
143
- * @param input Boundary metadata for the active render pass.
144
- * @returns `true` when the boundary should be deferred into the marker pass.
145
- */
146
- shouldDeferComponentBoundary(input) {
147
- return input.targetIntegration === this.name && input.currentIntegration !== this.name;
188
+ pageMetadataCache: this.hmrPageMetadataCache,
189
+ runtimeManifest: this.runtimeBundleService.getRuntimeManifest("development"),
190
+ mdxCompilerOptions: this.mdxCompilerOptions,
191
+ ownedTemplateExtensions: this.extensions,
192
+ allTemplateExtensions: this.appConfig.templatesExt,
193
+ explicitGraphEnabled: this.explicitGraphEnabled,
194
+ clientGraphBoundaryCache: this.clientGraphBoundaryCache
195
+ });
148
196
  }
149
197
  }
150
198
  function reactPlugin(options) {
@@ -0,0 +1,88 @@
1
+ import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
2
+ import type { CompileOptions } from '@mdx-js/mdx';
3
+ import type { ReactRouterAdapter } from './router-adapter.js';
4
+ import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
5
+ /**
6
+ * MDX configuration options for the React plugin.
7
+ */
8
+ export type ReactMdxOptions = {
9
+ /**
10
+ * Whether to enable MDX support.
11
+ * @default false
12
+ */
13
+ enabled: boolean;
14
+ /**
15
+ * Compiler options for MDX.
16
+ * @default undefined
17
+ */
18
+ compilerOptions?: Omit<CompileOptions, 'jsxImportSource' | 'jsxRuntime'>;
19
+ /**
20
+ * Remark plugins.
21
+ * @default undefined
22
+ */
23
+ remarkPlugins?: CompileOptions['remarkPlugins'];
24
+ /**
25
+ * Rehype plugins.
26
+ * @default undefined
27
+ */
28
+ rehypePlugins?: CompileOptions['rehypePlugins'];
29
+ /**
30
+ * Recma plugins.
31
+ * @default undefined
32
+ */
33
+ recmaPlugins?: CompileOptions['recmaPlugins'];
34
+ /**
35
+ * Custom extensions to be treated as MDX files.
36
+ * @default ['.mdx']
37
+ */
38
+ extensions?: string[];
39
+ };
40
+ /**
41
+ * Options for the React plugin.
42
+ */
43
+ export type ReactPluginOptions = {
44
+ extensions?: string[];
45
+ dependencies?: AssetDefinition[];
46
+ /**
47
+ * Enables explicit client graph mode for React page entries.
48
+ *
49
+ * When enabled, React page-entry bundling relies on explicit dependency declarations
50
+ * and skips AST-based `middleware`/`requires` stripping in the React path.
51
+ * @default false
52
+ */
53
+ explicitGraph?: boolean;
54
+ /**
55
+ * Router adapter for SPA navigation.
56
+ * When provided, pages with layouts will be wrapped in the router for client-side navigation.
57
+ * @example
58
+ * ```ts
59
+ * import { ecoRouter } from '@ecopages/react-router';
60
+ * reactPlugin({ router: ecoRouter() })
61
+ * ```
62
+ */
63
+ router?: ReactRouterAdapter;
64
+ /**
65
+ * MDX configuration for handling .mdx files within the React plugin.
66
+ * When enabled, MDX files are treated as React pages with full router support.
67
+ * @example
68
+ * ```ts
69
+ * reactPlugin({
70
+ * router: ecoRouter(),
71
+ * mdx: {
72
+ * enabled: true,
73
+ * extensions: ['.mdx', '.md'],
74
+ * remarkPlugins: [remarkGfm],
75
+ * rehypePlugins: [[rehypePrettyCode, { theme: '...' }]],
76
+ * }
77
+ * })
78
+ * ```
79
+ */
80
+ mdx?: ReactMdxOptions;
81
+ };
82
+ export type ReactRendererConfig = {
83
+ routerAdapter?: ReactRouterAdapter;
84
+ mdxCompilerOptions?: CompileOptions;
85
+ mdxExtensions?: string[];
86
+ hmrPageMetadataCache?: ReactHmrPageMetadataCache;
87
+ explicitGraphEnabled?: boolean;
88
+ };
File without changes
@@ -12,11 +12,10 @@
12
12
  * const myRouter: ReactRouterAdapter = {
13
13
  * name: 'my-router',
14
14
  * bundle: {
15
- * importPath: '@my/router/browser.ts',
15
+ * importPath: '@my/router/browser',
16
16
  * outputName: 'my-router',
17
17
  * externals: ['react', 'react-dom'],
18
18
  * },
19
- * importMapKey: '@my/router',
20
19
  * components: {
21
20
  * router: 'MyRouter',
22
21
  * pageContent: 'PageOutlet',
@@ -36,39 +35,33 @@ export interface ReactRouterAdapter {
36
35
  bundle: {
37
36
  /**
38
37
  * Node module import path for the browser-compatible entry.
39
- * @example '@ecopages/react-router/browser.ts'
38
+ * @example '@ecopages/react-router/browser'
40
39
  */
41
40
  importPath: string;
42
41
  /**
43
42
  * Output filename (without extension).
44
- * @example 'react-router-esm'
43
+ * @example 'my-router'
45
44
  */
46
45
  outputName: string;
47
46
  /**
48
47
  * Packages to externalize when bundling.
49
- * These should be available through the runtime bare-specifier map.
50
- * @example ['react', 'react-dom', 'react/jsx-runtime']
48
+ * These should stay as bare runtime dependencies for the router bundle.
49
+ * @example ['react', 'react-dom']
51
50
  */
52
51
  externals: string[];
53
52
  };
54
- /**
55
- * Bare specifier for the runtime mapping entry.
56
- * This is what the hydration script will import from.
57
- * @example '@ecopages/react-router'
58
- */
59
- importMapKey: string;
60
53
  /**
61
54
  * Component names to import from the router package.
62
55
  */
63
56
  components: {
64
57
  /**
65
58
  * The router component that wraps the layout.
66
- * @example 'EcoRouter'
59
+ * @example 'MyRouter'
67
60
  */
68
61
  router: string;
69
62
  /**
70
63
  * The component that renders the current page content.
71
- * @example 'PageContent'
64
+ * @example 'PageOutlet'
72
65
  */
73
66
  pageContent: string;
74
67
  };
@@ -0,0 +1,3 @@
1
+ type Subscribe = (onStoreChange: () => void) => () => void;
2
+ export declare function useSyncExternalStoreWithSelector<Snapshot, Selection>(subscribe: Subscribe, getSnapshot: () => Snapshot, getServerSnapshot: (() => Snapshot) | undefined, selector: (snapshot: Snapshot) => Selection, isEqual?: (left: Selection, right: Selection) => boolean): Selection;
3
+ export {};
@@ -0,0 +1,56 @@
1
+ import { useDebugValue, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
2
+ const objectIs = Object.is;
3
+ function useSyncExternalStoreWithSelector(subscribe, getSnapshot, getServerSnapshot, selector, isEqual) {
4
+ const instRef = useRef({ hasValue: false, value: void 0 });
5
+ const memoizedSelectionRef = useMemo(() => {
6
+ let hasMemo = false;
7
+ let memoizedSnapshot;
8
+ let memoizedSelection;
9
+ const maybeGetServerSnapshot = getServerSnapshot === void 0 ? null : getServerSnapshot;
10
+ const memoizedSelector = (nextSnapshot) => {
11
+ if (!hasMemo) {
12
+ hasMemo = true;
13
+ memoizedSnapshot = nextSnapshot;
14
+ const nextSelection2 = selector(nextSnapshot);
15
+ const inst = instRef.current;
16
+ if (isEqual !== void 0 && inst.hasValue) {
17
+ const currentSelection2 = inst.value;
18
+ if (isEqual(currentSelection2, nextSelection2)) {
19
+ memoizedSelection = currentSelection2;
20
+ return currentSelection2;
21
+ }
22
+ }
23
+ memoizedSelection = nextSelection2;
24
+ return nextSelection2;
25
+ }
26
+ const currentSelection = memoizedSelection;
27
+ if (objectIs(memoizedSnapshot, nextSnapshot)) {
28
+ return currentSelection;
29
+ }
30
+ const nextSelection = selector(nextSnapshot);
31
+ if (isEqual !== void 0 && isEqual(currentSelection, nextSelection)) {
32
+ memoizedSnapshot = nextSnapshot;
33
+ return currentSelection;
34
+ }
35
+ memoizedSnapshot = nextSnapshot;
36
+ memoizedSelection = nextSelection;
37
+ return nextSelection;
38
+ };
39
+ return [
40
+ () => memoizedSelector(getSnapshot()),
41
+ maybeGetServerSnapshot === null ? void 0 : () => memoizedSelector(maybeGetServerSnapshot())
42
+ ];
43
+ }, [getSnapshot, getServerSnapshot, selector, isEqual]);
44
+ const value = useSyncExternalStore(subscribe, memoizedSelectionRef[0], memoizedSelectionRef[1]);
45
+ useEffect(() => {
46
+ instRef.current = {
47
+ hasValue: true,
48
+ value
49
+ };
50
+ }, [value]);
51
+ useDebugValue(value);
52
+ return value;
53
+ }
54
+ export {
55
+ useSyncExternalStoreWithSelector
56
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Per-app index of React-owned page entrypoints.
3
+ *
4
+ * @remarks
5
+ * The HMR strategy previously called `fileSystem.glob` on every
6
+ * layout change. For apps with many pages this is wasteful: O(pages)
7
+ * on every HMR event, and the result rarely changes between HMR
8
+ * rebuilds.
9
+ *
10
+ * `PagesIndex` maintains a `Set` of owned page entrypoint paths. The
11
+ * initial population is a single glob; subsequent mutations go through
12
+ * `add` / `remove` (called by the HMR strategy when it observes a
13
+ * file create/delete under `pagesDir`). The strategy reads via
14
+ * `list()` for an O(1) snapshot of the current set.
15
+ *
16
+ * Phase 1 ships the class and the `list()` consumer. Watcher-driven
17
+ * `add` / `remove` integration lands in a follow-up — for now the
18
+ * strategy calls `refresh()` lazily, which is equivalent to the old
19
+ * glob behavior but with a stable API for the future incremental
20
+ * path.
21
+ */
22
+ export type PagesIndexOptions = {
23
+ pagesDir: string;
24
+ extensions?: string[];
25
+ /**
26
+ * Optional predicate to filter out non-page entrypoints (e.g.
27
+ * test fixtures, generated files). Defaults to including every
28
+ * file matching an extension.
29
+ */
30
+ isPageEntrypoint?: (absolutePath: string) => boolean;
31
+ };
32
+ export declare class PagesIndex {
33
+ private readonly pagesDir;
34
+ private readonly extensions;
35
+ private readonly isPageEntrypoint;
36
+ private readonly pages;
37
+ private lastRefreshAt;
38
+ constructor(options: PagesIndexOptions);
39
+ /**
40
+ * Rescan the pages directory and rebuild the index.
41
+ *
42
+ * Cheap to call multiple times in sequence (just a glob). The
43
+ * real value of `PagesIndex` is the `add` / `remove` path that
44
+ * callers can wire to the file watcher; for now this is the
45
+ * fallback used on layout changes.
46
+ */
47
+ refresh(): Promise<void>;
48
+ /**
49
+ * Add a single entrypoint. Idempotent.
50
+ */
51
+ add(absolutePath: string): void;
52
+ /**
53
+ * Remove a single entrypoint. Idempotent.
54
+ */
55
+ remove(absolutePath: string): void;
56
+ /** True if the index contains `absolutePath`. */
57
+ has(absolutePath: string): boolean;
58
+ /** Snapshot of the current set, sorted by absolute path. */
59
+ list(): string[];
60
+ /** Number of indexed entrypoints. */
61
+ get size(): number;
62
+ /** Last refresh timestamp (ms since epoch). */
63
+ get refreshedAt(): number;
64
+ }