@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.8

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 (41) hide show
  1. package/CHANGELOG.md +11 -51
  2. package/README.md +135 -29
  3. package/package.json +3 -3
  4. package/src/react-hmr-strategy.d.ts +22 -30
  5. package/src/react-hmr-strategy.js +57 -120
  6. package/src/react-hmr-strategy.ts +76 -145
  7. package/src/react-renderer.d.ts +130 -11
  8. package/src/react-renderer.js +368 -64
  9. package/src/react-renderer.ts +490 -90
  10. package/src/react.plugin.d.ts +17 -5
  11. package/src/react.plugin.js +44 -13
  12. package/src/react.plugin.ts +49 -14
  13. package/src/router-adapter.d.ts +2 -2
  14. package/src/router-adapter.ts +2 -2
  15. package/src/services/react-bundle.service.d.ts +2 -30
  16. package/src/services/react-bundle.service.js +19 -94
  17. package/src/services/react-bundle.service.ts +20 -129
  18. package/src/services/react-hydration-asset.service.js +3 -3
  19. package/src/services/react-hydration-asset.service.ts +7 -4
  20. package/src/services/react-page-module.service.d.ts +3 -0
  21. package/src/services/react-page-module.service.js +20 -16
  22. package/src/services/react-page-module.service.ts +27 -17
  23. package/src/services/react-runtime-bundle.service.d.ts +12 -12
  24. package/src/services/react-runtime-bundle.service.js +98 -180
  25. package/src/services/react-runtime-bundle.service.ts +112 -211
  26. package/src/utils/client-graph-boundary-plugin.js +78 -1
  27. package/src/utils/client-graph-boundary-plugin.ts +122 -1
  28. package/src/utils/hydration-scripts.d.ts +18 -1
  29. package/src/utils/hydration-scripts.js +83 -32
  30. package/src/utils/hydration-scripts.ts +159 -38
  31. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  32. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  33. package/src/utils/react-dom-runtime-interop-plugin.ts +33 -0
  34. package/src/utils/react-mdx-loader-plugin.js +13 -5
  35. package/src/utils/react-mdx-loader-plugin.ts +28 -5
  36. package/src/utils/react-runtime-specifier-map.d.ts +6 -0
  37. package/src/utils/react-runtime-specifier-map.js +37 -0
  38. package/src/utils/react-runtime-specifier-map.ts +45 -0
  39. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  40. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  41. package/src/utils/use-sync-external-store-shim-plugin.ts +45 -0
@@ -5,7 +5,6 @@
5
5
  import { IntegrationPlugin } from '@ecopages/core/plugins/integration-plugin';
6
6
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
7
  import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
8
- import type { IHmrManager } from '@ecopages/core/internal-types';
9
8
  import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
10
9
  import type { CompileOptions } from '@mdx-js/mdx';
11
10
  import type React from 'react';
@@ -106,12 +105,28 @@ export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
106
105
  private mdxLoaderPlugin;
107
106
  private runtimeBundleService;
108
107
  private readonly hmrPageMetadataCache;
108
+ private runtimeDependenciesInitialized;
109
109
  /**
110
110
  * Indicates whether React explicit graph mode is enabled for renderer/HMR behavior.
111
111
  */
112
112
  private explicitGraphEnabled;
113
113
  constructor(options?: Omit<ReactPluginOptions, 'name'>);
114
+ private ensureRuntimeDependencies;
114
115
  get plugins(): EcoBuildPlugin[];
116
+ /**
117
+ * Ensures the optional React MDX loader exists before either config-time
118
+ * manifest sealing or runtime setup needs it.
119
+ */
120
+ private ensureMdxLoaderPlugin;
121
+ /**
122
+ * Prepares React's build-facing loader contributions before config build seals
123
+ * the app manifest.
124
+ */
125
+ prepareBuildContributions(): Promise<void>;
126
+ /**
127
+ * Performs runtime-only React setup after build contributions are already
128
+ * materialized.
129
+ */
115
130
  setup(): Promise<void>;
116
131
  /**
117
132
  * Provides React-specific HMR strategy with Fast Refresh support.
@@ -123,10 +138,7 @@ export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
123
138
  * @returns ReactHmrStrategy instance for handling React component updates
124
139
  */
125
140
  getHmrStrategy(): HmrStrategy | undefined;
126
- /**
127
- * Override to register React-specific specifier mappings for HMR.
128
- */
129
- setHmrManager(hmrManager: IHmrManager): void;
141
+ getRuntimeSpecifierMap(): Record<string, string>;
130
142
  /**
131
143
  * Declares React's boundary deferral rule for cross-integration rendering.
132
144
  *
@@ -15,15 +15,21 @@ class ReactPlugin extends IntegrationPlugin {
15
15
  mdxLoaderPlugin;
16
16
  runtimeBundleService;
17
17
  hmrPageMetadataCache = new ReactHmrPageMetadataCache();
18
+ runtimeDependenciesInitialized = false;
18
19
  /**
19
20
  * Indicates whether React explicit graph mode is enabled for renderer/HMR behavior.
20
21
  */
21
22
  explicitGraphEnabled;
22
23
  constructor(options) {
23
- const extensions = [".tsx"];
24
+ const { extensions: _ignoredExtensions, ...restOptions } = options ?? {};
25
+ const extensions = [...options?.extensions ?? [".tsx"]];
24
26
  const mdxExtensions = options?.mdx?.extensions ?? [".mdx"];
25
27
  if (options?.mdx?.enabled) {
26
- extensions.push(...mdxExtensions);
28
+ for (const extension of mdxExtensions) {
29
+ if (!extensions.includes(extension)) {
30
+ extensions.push(extension);
31
+ }
32
+ }
27
33
  } else if (options?.mdx?.extensions?.length) {
28
34
  appLogger.warn(
29
35
  "MDX extensions provided but MDX is disabled. MDX files will not be processed. Set mdx.enabled to true to enable MDX support."
@@ -32,7 +38,7 @@ class ReactPlugin extends IntegrationPlugin {
32
38
  super({
33
39
  name: PLUGIN_NAME,
34
40
  extensions,
35
- ...options
41
+ ...restOptions
36
42
  });
37
43
  this.mdxEnabled = options?.mdx?.enabled ?? false;
38
44
  this.mdxExtensions = mdxExtensions;
@@ -59,7 +65,13 @@ class ReactPlugin extends IntegrationPlugin {
59
65
  ReactRenderer.mdxExtensions = this.mdxExtensions;
60
66
  ReactRenderer.explicitGraphEnabled = this.explicitGraphEnabled;
61
67
  ReactRenderer.hmrPageMetadataCache = this.hmrPageMetadataCache;
68
+ }
69
+ ensureRuntimeDependencies() {
70
+ if (this.runtimeDependenciesInitialized) {
71
+ return;
72
+ }
62
73
  this.integrationDependencies.unshift(...this.runtimeBundleService.getDependencies());
74
+ this.runtimeDependenciesInitialized = true;
63
75
  }
64
76
  get plugins() {
65
77
  if (this.mdxLoaderPlugin) {
@@ -67,11 +79,32 @@ class ReactPlugin extends IntegrationPlugin {
67
79
  }
68
80
  return [];
69
81
  }
70
- async setup() {
71
- if (this.mdxEnabled && this.mdxCompilerOptions) {
72
- const { createReactMdxLoaderPlugin } = await import("./utils/react-mdx-loader-plugin.js");
73
- this.mdxLoaderPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
82
+ /**
83
+ * Ensures the optional React MDX loader exists before either config-time
84
+ * manifest sealing or runtime setup needs it.
85
+ */
86
+ async ensureMdxLoaderPlugin() {
87
+ if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
88
+ return;
74
89
  }
90
+ const { createReactMdxLoaderPlugin } = await import("./utils/react-mdx-loader-plugin.js");
91
+ this.mdxLoaderPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
92
+ }
93
+ /**
94
+ * Prepares React's build-facing loader contributions before config build seals
95
+ * the app manifest.
96
+ */
97
+ async prepareBuildContributions() {
98
+ this.ensureRuntimeDependencies();
99
+ await this.ensureMdxLoaderPlugin();
100
+ }
101
+ /**
102
+ * Performs runtime-only React setup after build contributions are already
103
+ * materialized.
104
+ */
105
+ async setup() {
106
+ this.ensureRuntimeDependencies();
107
+ await this.ensureMdxLoaderPlugin();
75
108
  await super.setup();
76
109
  }
77
110
  /**
@@ -92,15 +125,13 @@ class ReactPlugin extends IntegrationPlugin {
92
125
  context,
93
126
  this.hmrPageMetadataCache,
94
127
  this.mdxCompilerOptions,
128
+ this.extensions,
129
+ this.appConfig.templatesExt,
95
130
  this.explicitGraphEnabled
96
131
  );
97
132
  }
98
- /**
99
- * Override to register React-specific specifier mappings for HMR.
100
- */
101
- setHmrManager(hmrManager) {
102
- super.setHmrManager(hmrManager);
103
- hmrManager.registerSpecifierMap(this.runtimeBundleService.getSpecifierMap());
133
+ getRuntimeSpecifierMap() {
134
+ return this.runtimeBundleService.getSpecifierMap();
104
135
  }
105
136
  /**
106
137
  * Declares React's boundary deferral rule for cross-integration rendering.
@@ -5,7 +5,6 @@
5
5
  import { IntegrationPlugin } from '@ecopages/core/plugins/integration-plugin';
6
6
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
7
  import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
8
- import type { IHmrManager } from '@ecopages/core/internal-types';
9
8
  import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
10
9
  import { Logger } from '@ecopages/logger';
11
10
  import type { CompileOptions } from '@mdx-js/mdx';
@@ -116,17 +115,23 @@ export class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
116
115
  private mdxLoaderPlugin: EcoBuildPlugin | undefined;
117
116
  private runtimeBundleService: ReactRuntimeBundleService;
118
117
  private readonly hmrPageMetadataCache = new ReactHmrPageMetadataCache();
118
+ private runtimeDependenciesInitialized = false;
119
119
  /**
120
120
  * Indicates whether React explicit graph mode is enabled for renderer/HMR behavior.
121
121
  */
122
122
  private explicitGraphEnabled: boolean;
123
123
 
124
124
  constructor(options?: Omit<ReactPluginOptions, 'name'>) {
125
- const extensions = ['.tsx'];
125
+ const { extensions: _ignoredExtensions, ...restOptions } = options ?? {};
126
+ const extensions = [...(options?.extensions ?? ['.tsx'])];
126
127
  const mdxExtensions = options?.mdx?.extensions ?? ['.mdx'];
127
128
 
128
129
  if (options?.mdx?.enabled) {
129
- extensions.push(...mdxExtensions);
130
+ for (const extension of mdxExtensions) {
131
+ if (!extensions.includes(extension)) {
132
+ extensions.push(extension);
133
+ }
134
+ }
130
135
  } else if (options?.mdx?.extensions?.length) {
131
136
  appLogger.warn(
132
137
  'MDX extensions provided but MDX is disabled. MDX files will not be processed. Set mdx.enabled to true to enable MDX support.',
@@ -136,7 +141,7 @@ export class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
136
141
  super({
137
142
  name: PLUGIN_NAME,
138
143
  extensions,
139
- ...options,
144
+ ...restOptions,
140
145
  });
141
146
 
142
147
  this.mdxEnabled = options?.mdx?.enabled ?? false;
@@ -166,7 +171,15 @@ export class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
166
171
  ReactRenderer.mdxExtensions = this.mdxExtensions;
167
172
  ReactRenderer.explicitGraphEnabled = this.explicitGraphEnabled;
168
173
  ReactRenderer.hmrPageMetadataCache = this.hmrPageMetadataCache;
174
+ }
175
+
176
+ private ensureRuntimeDependencies(): void {
177
+ if (this.runtimeDependenciesInitialized) {
178
+ return;
179
+ }
180
+
169
181
  this.integrationDependencies.unshift(...this.runtimeBundleService.getDependencies());
182
+ this.runtimeDependenciesInitialized = true;
170
183
  }
171
184
 
172
185
  override get plugins(): EcoBuildPlugin[] {
@@ -176,11 +189,35 @@ export class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
176
189
  return [];
177
190
  }
178
191
 
179
- override async setup(): Promise<void> {
180
- if (this.mdxEnabled && this.mdxCompilerOptions) {
181
- const { createReactMdxLoaderPlugin } = await import('./utils/react-mdx-loader-plugin.ts');
182
- this.mdxLoaderPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
192
+ /**
193
+ * Ensures the optional React MDX loader exists before either config-time
194
+ * manifest sealing or runtime setup needs it.
195
+ */
196
+ private async ensureMdxLoaderPlugin(): Promise<void> {
197
+ if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
198
+ return;
183
199
  }
200
+
201
+ const { createReactMdxLoaderPlugin } = await import('./utils/react-mdx-loader-plugin.ts');
202
+ this.mdxLoaderPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
203
+ }
204
+
205
+ /**
206
+ * Prepares React's build-facing loader contributions before config build seals
207
+ * the app manifest.
208
+ */
209
+ override async prepareBuildContributions(): Promise<void> {
210
+ this.ensureRuntimeDependencies();
211
+ await this.ensureMdxLoaderPlugin();
212
+ }
213
+
214
+ /**
215
+ * Performs runtime-only React setup after build contributions are already
216
+ * materialized.
217
+ */
218
+ override async setup(): Promise<void> {
219
+ this.ensureRuntimeDependencies();
220
+ await this.ensureMdxLoaderPlugin();
184
221
  await super.setup();
185
222
  }
186
223
 
@@ -204,16 +241,14 @@ export class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
204
241
  context,
205
242
  this.hmrPageMetadataCache,
206
243
  this.mdxCompilerOptions,
244
+ this.extensions,
245
+ this.appConfig.templatesExt,
207
246
  this.explicitGraphEnabled,
208
247
  );
209
248
  }
210
249
 
211
- /**
212
- * Override to register React-specific specifier mappings for HMR.
213
- */
214
- override setHmrManager(hmrManager: IHmrManager): void {
215
- super.setHmrManager(hmrManager);
216
- hmrManager.registerSpecifierMap(this.runtimeBundleService.getSpecifierMap());
250
+ override getRuntimeSpecifierMap(): Record<string, string> {
251
+ return this.runtimeBundleService.getSpecifierMap();
217
252
  }
218
253
 
219
254
  /**
@@ -46,13 +46,13 @@ export interface ReactRouterAdapter {
46
46
  outputName: string;
47
47
  /**
48
48
  * Packages to externalize when bundling.
49
- * These should be available via import map.
49
+ * These should be available through the runtime bare-specifier map.
50
50
  * @example ['react', 'react-dom', 'react/jsx-runtime']
51
51
  */
52
52
  externals: string[];
53
53
  };
54
54
  /**
55
- * Bare specifier for the import map entry.
55
+ * Bare specifier for the runtime mapping entry.
56
56
  * This is what the hydration script will import from.
57
57
  * @example '@ecopages/react-router'
58
58
  */
@@ -50,14 +50,14 @@ export interface ReactRouterAdapter {
50
50
 
51
51
  /**
52
52
  * Packages to externalize when bundling.
53
- * These should be available via import map.
53
+ * These should be available through the runtime bare-specifier map.
54
54
  * @example ['react', 'react-dom', 'react/jsx-runtime']
55
55
  */
56
56
  externals: string[];
57
57
  };
58
58
 
59
59
  /**
60
- * Bare specifier for the import map entry.
60
+ * Bare specifier for the runtime mapping entry.
61
61
  * This is what the hydration script will import from.
62
62
  * @example '@ecopages/react-router'
63
63
  */
@@ -21,8 +21,8 @@ export interface ReactBundleServiceConfig {
21
21
  * Manages esbuild bundle configuration and plugin creation for React page/component builds.
22
22
  */
23
23
  export declare class ReactBundleService {
24
- private readonly config;
25
24
  private readonly runtimeBundleService;
25
+ private readonly config;
26
26
  constructor(config: ReactBundleServiceConfig);
27
27
  /**
28
28
  * Returns resolved runtime import paths for the React runtime.
@@ -41,33 +41,5 @@ export declare class ReactBundleService {
41
41
  * Creates the esbuild plugin that rewrites bare React specifiers
42
42
  * to their runtime asset URLs.
43
43
  */
44
- createRuntimeAliasPlugin(runtimeImports: ReactRuntimeImports): {
45
- name: string;
46
- setup(build: {
47
- onResolve: (options: {
48
- filter: RegExp;
49
- namespace?: string;
50
- }, callback: (args: {
51
- path: string;
52
- importer: string;
53
- namespace: string;
54
- }) => {
55
- path?: string;
56
- namespace?: string;
57
- external?: boolean;
58
- } | undefined) => void;
59
- }): void;
60
- };
61
- /**
62
- * Redirects `use-sync-external-store/shim` imports to React's built-in
63
- * `useSyncExternalStore`.
64
- *
65
- * Libraries like React Aria still list `use-sync-external-store` as a
66
- * dependency to support React 16/17. On React 18+ the `/shim` export is
67
- * already a pass-through, but without this plugin esbuild would bundle
68
- * the full CJS shim (including `process.env` branching) into the browser
69
- * bundle. The plugin short-circuits the resolution so only a single clean
70
- * ESM re-export is emitted.
71
- */
72
- private createSyncExternalStorePlugin;
44
+ createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>): import("packages/core/src/build/build-types.ts").EcoBuildPlugin | null;
73
45
  }
@@ -1,13 +1,21 @@
1
1
  import { createClientGraphBoundaryPlugin } from "../utils/client-graph-boundary-plugin.js";
2
+ import {
3
+ buildReactRuntimeSpecifierMap,
4
+ getReactClientGraphAllowSpecifiers,
5
+ getReactRuntimeExternalSpecifiers
6
+ } from "../utils/react-runtime-specifier-map.js";
7
+ import { createUseSyncExternalStoreShimPlugin } from "../utils/use-sync-external-store-shim-plugin.js";
8
+ import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
2
9
  import { ReactRuntimeBundleService } from "./react-runtime-bundle.service.js";
3
10
  class ReactBundleService {
11
+ runtimeBundleService;
12
+ config;
4
13
  constructor(config) {
5
14
  this.config = config;
6
15
  this.runtimeBundleService = new ReactRuntimeBundleService({
7
16
  routerAdapter: config.routerAdapter
8
17
  });
9
18
  }
10
- runtimeBundleService;
11
19
  /**
12
20
  * Returns resolved runtime import paths for the React runtime.
13
21
  */
@@ -24,8 +32,9 @@ class ReactBundleService {
24
32
  */
25
33
  async createBundleOptions(componentName, isMdx, declaredModules) {
26
34
  const runtimeImports = this.getRuntimeImports();
35
+ const runtimeSpecifierMap = buildReactRuntimeSpecifierMap(runtimeImports, this.config.routerAdapter);
27
36
  const options = {
28
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"],
37
+ external: getReactRuntimeExternalSpecifiers(),
29
38
  mainFields: ["module", "browser", "main"],
30
39
  naming: `${componentName}.[ext]`,
31
40
  ...import.meta.env?.NODE_ENV === "production" && {
@@ -37,18 +46,13 @@ class ReactBundleService {
37
46
  const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
38
47
  absWorkingDir: this.config.rootDir,
39
48
  declaredModules,
40
- alwaysAllowSpecifiers: [
41
- "@ecopages/core",
42
- "react",
43
- "react-dom",
44
- "react/jsx-runtime",
45
- "react/jsx-dev-runtime",
46
- "react-dom/client",
47
- ...this.config.routerAdapter ? [this.config.routerAdapter.importMapKey] : []
48
- ]
49
+ alwaysAllowSpecifiers: getReactClientGraphAllowSpecifiers([], this.config.routerAdapter)
50
+ });
51
+ const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeSpecifierMap);
52
+ const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
53
+ name: "react-renderer-use-sync-external-store-shim",
54
+ namespace: "ecopages-react-renderer-shim"
49
55
  });
50
- const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeImports);
51
- const useSyncExternalStoreShimPlugin = this.createSyncExternalStorePlugin();
52
56
  if (isMdx && this.config.mdxCompilerOptions) {
53
57
  const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
54
58
  const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
@@ -62,87 +66,8 @@ class ReactBundleService {
62
66
  * Creates the esbuild plugin that rewrites bare React specifiers
63
67
  * to their runtime asset URLs.
64
68
  */
65
- createRuntimeAliasPlugin(runtimeImports) {
66
- const aliases = /* @__PURE__ */ new Map([
67
- ["react", runtimeImports.react],
68
- ["react-dom/client", runtimeImports.reactDomClient],
69
- ["react/jsx-runtime", runtimeImports.reactJsxRuntime],
70
- ["react/jsx-dev-runtime", runtimeImports.reactJsxDevRuntime],
71
- ["react-dom", runtimeImports.reactDom]
72
- ]);
73
- if (this.config.routerAdapter && runtimeImports.router) {
74
- aliases.set(this.config.routerAdapter.importMapKey, runtimeImports.router);
75
- }
76
- const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
77
- const pattern = new RegExp(
78
- `^(${Array.from(aliases.keys()).map((key) => escapeRegExp(key)).join("|")})$`
79
- );
80
- return {
81
- name: "react-runtime-import-alias",
82
- setup(build) {
83
- build.onResolve({ filter: pattern }, (args) => {
84
- const mappedPath = aliases.get(args.path);
85
- if (!mappedPath) {
86
- return void 0;
87
- }
88
- return {
89
- path: mappedPath,
90
- external: true
91
- };
92
- });
93
- }
94
- };
95
- }
96
- /**
97
- * Redirects `use-sync-external-store/shim` imports to React's built-in
98
- * `useSyncExternalStore`.
99
- *
100
- * Libraries like React Aria still list `use-sync-external-store` as a
101
- * dependency to support React 16/17. On React 18+ the `/shim` export is
102
- * already a pass-through, but without this plugin esbuild would bundle
103
- * the full CJS shim (including `process.env` branching) into the browser
104
- * bundle. The plugin short-circuits the resolution so only a single clean
105
- * ESM re-export is emitted.
106
- */
107
- createSyncExternalStorePlugin() {
108
- return {
109
- name: "react-renderer-use-sync-external-store-shim",
110
- setup(build) {
111
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
112
- path: "use-sync-external-store/shim",
113
- namespace: "ecopages-react-renderer-shim"
114
- }));
115
- build.onLoad(
116
- { filter: /^use-sync-external-store\/shim$/, namespace: "ecopages-react-renderer-shim" },
117
- () => ({
118
- contents: "export { useSyncExternalStore } from 'react';",
119
- loader: "js"
120
- })
121
- );
122
- build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
123
- contents: "export { useSyncExternalStore } from 'react';",
124
- loader: "js"
125
- }));
126
- build.onLoad(
127
- {
128
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/
129
- },
130
- () => ({
131
- contents: "export { useSyncExternalStore } from 'react';",
132
- loader: "js"
133
- })
134
- );
135
- build.onLoad(
136
- {
137
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.production\.js$/
138
- },
139
- () => ({
140
- contents: "export { useSyncExternalStore } from 'react';",
141
- loader: "js"
142
- })
143
- );
144
- }
145
- };
69
+ createRuntimeAliasPlugin(runtimeSpecifierMap) {
70
+ return createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, { name: "react-runtime-import-alias" });
146
71
  }
147
72
  }
148
73
  export {
@@ -8,6 +8,13 @@
8
8
  */
9
9
 
10
10
  import { createClientGraphBoundaryPlugin } from '../utils/client-graph-boundary-plugin.ts';
11
+ import {
12
+ buildReactRuntimeSpecifierMap,
13
+ getReactClientGraphAllowSpecifiers,
14
+ getReactRuntimeExternalSpecifiers,
15
+ } from '../utils/react-runtime-specifier-map.ts';
16
+ import { createUseSyncExternalStoreShimPlugin } from '../utils/use-sync-external-store-shim-plugin.ts';
17
+ import { createRuntimeSpecifierAliasPlugin } from '@ecopages/core/build/runtime-specifier-alias-plugin';
11
18
  import type { ReactRouterAdapter } from '../router-adapter.ts';
12
19
  import type { CompileOptions } from '@mdx-js/mdx';
13
20
  import { ReactRuntimeBundleService, type ReactRuntimeImports } from './react-runtime-bundle.service.ts';
@@ -26,8 +33,10 @@ export interface ReactBundleServiceConfig {
26
33
  */
27
34
  export class ReactBundleService {
28
35
  private readonly runtimeBundleService: ReactRuntimeBundleService;
36
+ private readonly config: ReactBundleServiceConfig;
29
37
 
30
- constructor(private readonly config: ReactBundleServiceConfig) {
38
+ constructor(config: ReactBundleServiceConfig) {
39
+ this.config = config;
31
40
  this.runtimeBundleService = new ReactRuntimeBundleService({
32
41
  routerAdapter: config.routerAdapter,
33
42
  });
@@ -54,8 +63,9 @@ export class ReactBundleService {
54
63
  declaredModules: string[],
55
64
  ): Promise<Record<string, unknown>> {
56
65
  const runtimeImports = this.getRuntimeImports();
66
+ const runtimeSpecifierMap = buildReactRuntimeSpecifierMap(runtimeImports, this.config.routerAdapter);
57
67
  const options: Record<string, unknown> = {
58
- external: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom/client'],
68
+ external: getReactRuntimeExternalSpecifiers(),
59
69
  mainFields: ['module', 'browser', 'main'],
60
70
  naming: `${componentName}.[ext]`,
61
71
  ...(import.meta.env?.NODE_ENV === 'production' && {
@@ -68,19 +78,14 @@ export class ReactBundleService {
68
78
  const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
69
79
  absWorkingDir: this.config.rootDir,
70
80
  declaredModules,
71
- alwaysAllowSpecifiers: [
72
- '@ecopages/core',
73
- 'react',
74
- 'react-dom',
75
- 'react/jsx-runtime',
76
- 'react/jsx-dev-runtime',
77
- 'react-dom/client',
78
- ...(this.config.routerAdapter ? [this.config.routerAdapter.importMapKey] : []),
79
- ],
81
+ alwaysAllowSpecifiers: getReactClientGraphAllowSpecifiers([], this.config.routerAdapter),
80
82
  });
81
83
 
82
- const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeImports);
83
- const useSyncExternalStoreShimPlugin = this.createSyncExternalStorePlugin();
84
+ const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeSpecifierMap);
85
+ const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
86
+ name: 'react-renderer-use-sync-external-store-shim',
87
+ namespace: 'ecopages-react-renderer-shim',
88
+ });
84
89
 
85
90
  if (isMdx && this.config.mdxCompilerOptions) {
86
91
  const { createReactMdxLoaderPlugin } = await import('../utils/react-mdx-loader-plugin.ts');
@@ -97,121 +102,7 @@ export class ReactBundleService {
97
102
  * Creates the esbuild plugin that rewrites bare React specifiers
98
103
  * to their runtime asset URLs.
99
104
  */
100
- createRuntimeAliasPlugin(runtimeImports: ReactRuntimeImports) {
101
- const aliases = new Map<string, string>([
102
- ['react', runtimeImports.react],
103
- ['react-dom/client', runtimeImports.reactDomClient],
104
- ['react/jsx-runtime', runtimeImports.reactJsxRuntime],
105
- ['react/jsx-dev-runtime', runtimeImports.reactJsxDevRuntime],
106
- ['react-dom', runtimeImports.reactDom],
107
- ]);
108
-
109
- if (this.config.routerAdapter && runtimeImports.router) {
110
- aliases.set(this.config.routerAdapter.importMapKey, runtimeImports.router);
111
- }
112
-
113
- const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114
- const pattern = new RegExp(
115
- `^(${Array.from(aliases.keys())
116
- .map((key) => escapeRegExp(key))
117
- .join('|')})$`,
118
- );
119
-
120
- return {
121
- name: 'react-runtime-import-alias',
122
- setup(build: {
123
- onResolve: (
124
- options: { filter: RegExp; namespace?: string },
125
- callback: (args: {
126
- path: string;
127
- importer: string;
128
- namespace: string;
129
- }) => { path?: string; namespace?: string; external?: boolean } | undefined,
130
- ) => void;
131
- }) {
132
- build.onResolve({ filter: pattern }, (args) => {
133
- const mappedPath = aliases.get(args.path);
134
- if (!mappedPath) {
135
- return undefined;
136
- }
137
- return {
138
- path: mappedPath,
139
- external: true,
140
- };
141
- });
142
- },
143
- };
144
- }
145
-
146
- /**
147
- * Redirects `use-sync-external-store/shim` imports to React's built-in
148
- * `useSyncExternalStore`.
149
- *
150
- * Libraries like React Aria still list `use-sync-external-store` as a
151
- * dependency to support React 16/17. On React 18+ the `/shim` export is
152
- * already a pass-through, but without this plugin esbuild would bundle
153
- * the full CJS shim (including `process.env` branching) into the browser
154
- * bundle. The plugin short-circuits the resolution so only a single clean
155
- * ESM re-export is emitted.
156
- */
157
- private createSyncExternalStorePlugin() {
158
- return {
159
- name: 'react-renderer-use-sync-external-store-shim',
160
- setup(build: {
161
- onResolve: (
162
- options: { filter: RegExp; namespace?: string },
163
- callback: (args: {
164
- path: string;
165
- importer: string;
166
- namespace: string;
167
- }) => { path?: string; namespace?: string } | undefined,
168
- ) => void;
169
- onLoad: (
170
- options: { filter: RegExp; namespace?: string },
171
- callback: (args: {
172
- path: string;
173
- namespace: string;
174
- }) => { contents?: string; loader?: 'js' } | undefined,
175
- ) => void;
176
- }) {
177
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
178
- path: 'use-sync-external-store/shim',
179
- namespace: 'ecopages-react-renderer-shim',
180
- }));
181
-
182
- build.onLoad(
183
- { filter: /^use-sync-external-store\/shim$/, namespace: 'ecopages-react-renderer-shim' },
184
- () => ({
185
- contents: "export { useSyncExternalStore } from 'react';",
186
- loader: 'js',
187
- }),
188
- );
189
-
190
- build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
191
- contents: "export { useSyncExternalStore } from 'react';",
192
- loader: 'js',
193
- }));
194
-
195
- build.onLoad(
196
- {
197
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/,
198
- },
199
- () => ({
200
- contents: "export { useSyncExternalStore } from 'react';",
201
- loader: 'js',
202
- }),
203
- );
204
-
205
- build.onLoad(
206
- {
207
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.production\.js$/,
208
- },
209
- () => ({
210
- contents: "export { useSyncExternalStore } from 'react';",
211
- loader: 'js',
212
- }),
213
- );
214
- },
215
- };
105
+ createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>) {
106
+ return createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, { name: 'react-runtime-import-alias' });
216
107
  }
217
108
  }