@ecopages/react 0.2.0-alpha.4 → 0.2.0-alpha.7

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 (44) hide show
  1. package/CHANGELOG.md +23 -37
  2. package/README.md +143 -17
  3. package/package.json +3 -3
  4. package/src/react-hmr-strategy.d.ts +22 -19
  5. package/src/react-hmr-strategy.js +57 -109
  6. package/src/react-hmr-strategy.ts +76 -134
  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 -25
  16. package/src/services/react-bundle.service.js +21 -91
  17. package/src/services/react-bundle.service.ts +22 -126
  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 +147 -9
  27. package/src/utils/client-graph-boundary-plugin.ts +252 -11
  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/reachability-analyzer.d.ts +12 -1
  32. package/src/utils/reachability-analyzer.js +101 -5
  33. package/src/utils/reachability-analyzer.ts +161 -8
  34. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  35. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  36. package/src/utils/react-dom-runtime-interop-plugin.ts +33 -0
  37. package/src/utils/react-mdx-loader-plugin.js +13 -5
  38. package/src/utils/react-mdx-loader-plugin.ts +28 -5
  39. package/src/utils/react-runtime-specifier-map.d.ts +6 -0
  40. package/src/utils/react-runtime-specifier-map.js +37 -0
  41. package/src/utils/react-runtime-specifier-map.ts +45 -0
  42. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  43. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  44. package/src/utils/use-sync-external-store-shim-plugin.ts +45 -0
@@ -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,26 +78,21 @@ 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');
87
92
  const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
88
- options.plugins = [runtimeAliasPlugin, mdxPlugin, useSyncExternalStoreShimPlugin, graphBoundaryPlugin];
93
+ options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, mdxPlugin, useSyncExternalStoreShimPlugin];
89
94
  } else {
90
- options.plugins = [runtimeAliasPlugin, useSyncExternalStoreShimPlugin, graphBoundaryPlugin];
95
+ options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, useSyncExternalStoreShimPlugin];
91
96
  }
92
97
 
93
98
  return options;
@@ -97,116 +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
- * Creates the esbuild plugin that shims `use-sync-external-store/shim`
148
- * to re-export from React's built-in `useSyncExternalStore`.
149
- * This is needed because some packages use `use-sync-external-store/shim`
150
- * but React 18+ has built-in `useSyncExternalStore`.
151
- */
152
- private createSyncExternalStorePlugin() {
153
- return {
154
- name: 'react-renderer-use-sync-external-store-shim',
155
- setup(build: {
156
- onResolve: (
157
- options: { filter: RegExp; namespace?: string },
158
- callback: (args: {
159
- path: string;
160
- importer: string;
161
- namespace: string;
162
- }) => { path?: string; namespace?: string } | undefined,
163
- ) => void;
164
- onLoad: (
165
- options: { filter: RegExp; namespace?: string },
166
- callback: (args: {
167
- path: string;
168
- namespace: string;
169
- }) => { contents?: string; loader?: 'js' } | undefined,
170
- ) => void;
171
- }) {
172
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
173
- path: 'use-sync-external-store/shim',
174
- namespace: 'ecopages-react-renderer-shim',
175
- }));
176
-
177
- build.onLoad(
178
- { filter: /^use-sync-external-store\/shim$/, namespace: 'ecopages-react-renderer-shim' },
179
- () => ({
180
- contents: "export { useSyncExternalStore } from 'react';",
181
- loader: 'js',
182
- }),
183
- );
184
-
185
- build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
186
- contents: "export { useSyncExternalStore } from 'react';",
187
- loader: 'js',
188
- }));
189
-
190
- build.onLoad(
191
- {
192
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/,
193
- },
194
- () => ({
195
- contents: "export { useSyncExternalStore } from 'react';",
196
- loader: 'js',
197
- }),
198
- );
199
-
200
- build.onLoad(
201
- {
202
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.production\.js$/,
203
- },
204
- () => ({
205
- contents: "export { useSyncExternalStore } from 'react';",
206
- loader: 'js',
207
- }),
208
- );
209
- },
210
- };
105
+ createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>) {
106
+ return createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, { name: 'react-runtime-import-alias' });
211
107
  }
212
108
  }
@@ -4,10 +4,10 @@ import { RESOLVED_ASSETS_DIR } from "@ecopages/core/constants";
4
4
  import {
5
5
  AssetFactory
6
6
  } from "@ecopages/core/services/asset-processing-service";
7
- import { createHydrationScript } from "../utils/hydration-scripts.js";
8
- import { createIslandHydrationScript } from "../utils/hydration-scripts.js";
7
+ import { createHydrationScript, createIslandHydrationScript } from "../utils/hydration-scripts.js";
9
8
  import { collectDeclaredModulesInConfig } from "../utils/declared-modules.js";
10
9
  class ReactHydrationAssetService {
10
+ config;
11
11
  constructor(config) {
12
12
  this.config = config;
13
13
  }
@@ -59,7 +59,7 @@ class ReactHydrationAssetService {
59
59
  dependencies.push(
60
60
  AssetFactory.createContentScript({
61
61
  position: "head",
62
- content: `window.__ECO_PAGE__={module:"${importPath}",props:${JSON.stringify(props)}};`,
62
+ content: `window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.page={module:"${importPath}",props:${JSON.stringify(props)}};`,
63
63
  name: `${componentName}-props`,
64
64
  bundle: false,
65
65
  attributes: {
@@ -18,8 +18,7 @@ import {
18
18
  type ProcessedAsset,
19
19
  } from '@ecopages/core/services/asset-processing-service';
20
20
  import type { AssetProcessingService } from '@ecopages/core/services/asset-processing-service';
21
- import { createHydrationScript } from '../utils/hydration-scripts.ts';
22
- import { createIslandHydrationScript } from '../utils/hydration-scripts.ts';
21
+ import { createHydrationScript, createIslandHydrationScript } from '../utils/hydration-scripts.ts';
23
22
  import { collectDeclaredModulesInConfig } from '../utils/declared-modules.ts';
24
23
  import type { ReactBundleService } from './react-bundle.service.ts';
25
24
  import type { ReactHmrPageMetadataCache } from './react-hmr-page-metadata-cache.ts';
@@ -40,7 +39,11 @@ export interface ReactHydrationAssetServiceConfig {
40
39
  * Manages the creation of client-side hydration assets for React pages and component islands.
41
40
  */
42
41
  export class ReactHydrationAssetService {
43
- constructor(private readonly config: ReactHydrationAssetServiceConfig) {}
42
+ private readonly config: ReactHydrationAssetServiceConfig;
43
+
44
+ constructor(config: ReactHydrationAssetServiceConfig) {
45
+ this.config = config;
46
+ }
44
47
 
45
48
  /**
46
49
  * Resolves the import path for the bundled page component.
@@ -105,7 +108,7 @@ export class ReactHydrationAssetService {
105
108
  dependencies.push(
106
109
  AssetFactory.createContentScript({
107
110
  position: 'head',
108
- content: `window.__ECO_PAGE__={module:"${importPath}",props:${JSON.stringify(props)}};`,
111
+ content: `window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.page={module:"${importPath}",props:${JSON.stringify(props)}};`,
109
112
  name: `${componentName}-props`,
110
113
  bundle: false,
111
114
  attributes: {
@@ -7,6 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import type { EcoComponentConfig, EcoPageFile } from '@ecopages/core';
10
+ import type { BuildExecutor } from '@ecopages/core/build/build-adapter';
10
11
  import type { CompileOptions } from '@mdx-js/mdx';
11
12
  /**
12
13
  * Configuration for the ReactPageModuleService.
@@ -14,6 +15,8 @@ import type { CompileOptions } from '@mdx-js/mdx';
14
15
  export interface ReactPageModuleServiceConfig {
15
16
  rootDir: string;
16
17
  distDir: string;
18
+ workDir: string;
19
+ buildExecutor: BuildExecutor;
17
20
  layoutsDir?: string;
18
21
  componentsDir?: string;
19
22
  mdxCompilerOptions?: CompileOptions;
@@ -1,10 +1,11 @@
1
1
  import path from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { rapidhash } from "@ecopages/core/hash";
4
- import { defaultBuildAdapter } from "@ecopages/core/build/build-adapter";
4
+ import { build } from "@ecopages/core/build/build-adapter";
5
5
  import { fileSystem } from "@ecopages/file-system";
6
6
  import { collectDeclaredModulesInConfig } from "../utils/declared-modules.js";
7
7
  class ReactPageModuleService {
8
+ config;
8
9
  constructor(config) {
9
10
  this.config = config;
10
11
  }
@@ -31,24 +32,27 @@ class ReactPageModuleService {
31
32
  development: process?.env?.NODE_ENV === "development"
32
33
  }
33
34
  );
34
- const outdir = path.join(this.config.distDir, ".server-modules-react-mdx");
35
+ const outdir = path.join(this.config.workDir, ".server-modules-react-mdx");
35
36
  const fileBaseName = path.basename(filePath, path.extname(filePath));
36
37
  const fileHash = fileSystem.hash(filePath);
37
38
  const cacheBuster = process?.env?.NODE_ENV === "development" ? `-${Date.now()}` : "";
38
39
  const outputFileName = `${fileBaseName}-${fileHash}${cacheBuster}.js`;
39
- const buildResult = await defaultBuildAdapter.build({
40
- entrypoints: [filePath],
41
- root: this.config.rootDir,
42
- outdir,
43
- target: "node",
44
- format: "esm",
45
- sourcemap: "none",
46
- splitting: false,
47
- minify: false,
48
- treeshaking: false,
49
- naming: outputFileName,
50
- plugins: [mdxPlugin]
51
- });
40
+ const buildResult = await build(
41
+ {
42
+ entrypoints: [filePath],
43
+ root: this.config.rootDir,
44
+ outdir,
45
+ target: "node",
46
+ format: "esm",
47
+ sourcemap: "none",
48
+ splitting: false,
49
+ minify: false,
50
+ treeshaking: false,
51
+ naming: outputFileName,
52
+ plugins: [mdxPlugin]
53
+ },
54
+ this.config.buildExecutor
55
+ );
52
56
  if (!buildResult.success) {
53
57
  const details = buildResult.logs.map((log) => log.message).join(" | ");
54
58
  throw new Error(`Failed to compile MDX page module: ${details}`);
@@ -91,7 +95,7 @@ class ReactPageModuleService {
91
95
  if (fileSystem.exists(resolvedDependency)) {
92
96
  return {
93
97
  ...config,
94
- __eco: buildEcoMeta(resolvedDependency)
98
+ __eco: buildEcoMeta(path.join(candidateDir, path.basename(pagePath)))
95
99
  };
96
100
  }
97
101
  }
@@ -10,8 +10,9 @@
10
10
  import path from 'node:path';
11
11
  import { pathToFileURL } from 'node:url';
12
12
  import type { EcoComponentConfig, EcoPageFile } from '@ecopages/core';
13
+ import type { BuildExecutor } from '@ecopages/core/build/build-adapter';
13
14
  import { rapidhash } from '@ecopages/core/hash';
14
- import { defaultBuildAdapter } from '@ecopages/core/build/build-adapter';
15
+ import { build } from '@ecopages/core/build/build-adapter';
15
16
  import { fileSystem } from '@ecopages/file-system';
16
17
  import type { CompileOptions } from '@mdx-js/mdx';
17
18
  import { collectDeclaredModulesInConfig } from '../utils/declared-modules.ts';
@@ -22,6 +23,8 @@ import { collectDeclaredModulesInConfig } from '../utils/declared-modules.ts';
22
23
  export interface ReactPageModuleServiceConfig {
23
24
  rootDir: string;
24
25
  distDir: string;
26
+ workDir: string;
27
+ buildExecutor: BuildExecutor;
25
28
  layoutsDir?: string;
26
29
  componentsDir?: string;
27
30
  mdxCompilerOptions?: CompileOptions;
@@ -35,7 +38,11 @@ export interface ReactPageModuleServiceConfig {
35
38
  * resolution, and hydration analysis for React pages.
36
39
  */
37
40
  export class ReactPageModuleService {
38
- constructor(private readonly config: ReactPageModuleServiceConfig) {}
41
+ private readonly config: ReactPageModuleServiceConfig;
42
+
43
+ constructor(config: ReactPageModuleServiceConfig) {
44
+ this.config = config;
45
+ }
39
46
 
40
47
  /**
41
48
  * Checks if the given file path corresponds to an MDX file based on configured extensions.
@@ -62,25 +69,28 @@ export class ReactPageModuleService {
62
69
  },
63
70
  );
64
71
 
65
- const outdir = path.join(this.config.distDir, '.server-modules-react-mdx');
72
+ const outdir = path.join(this.config.workDir, '.server-modules-react-mdx');
66
73
  const fileBaseName = path.basename(filePath, path.extname(filePath));
67
74
  const fileHash = fileSystem.hash(filePath);
68
75
  const cacheBuster = process?.env?.NODE_ENV === 'development' ? `-${Date.now()}` : '';
69
76
  const outputFileName = `${fileBaseName}-${fileHash}${cacheBuster}.js`;
70
77
 
71
- const buildResult = await defaultBuildAdapter.build({
72
- entrypoints: [filePath],
73
- root: this.config.rootDir,
74
- outdir,
75
- target: 'node',
76
- format: 'esm',
77
- sourcemap: 'none',
78
- splitting: false,
79
- minify: false,
80
- treeshaking: false,
81
- naming: outputFileName,
82
- plugins: [mdxPlugin],
83
- });
78
+ const buildResult = await build(
79
+ {
80
+ entrypoints: [filePath],
81
+ root: this.config.rootDir,
82
+ outdir,
83
+ target: 'node',
84
+ format: 'esm',
85
+ sourcemap: 'none',
86
+ splitting: false,
87
+ minify: false,
88
+ treeshaking: false,
89
+ naming: outputFileName,
90
+ plugins: [mdxPlugin],
91
+ },
92
+ this.config.buildExecutor,
93
+ );
84
94
 
85
95
  if (!buildResult.success) {
86
96
  const details = buildResult.logs.map((log) => log.message).join(' | ');
@@ -138,7 +148,7 @@ export class ReactPageModuleService {
138
148
  if (fileSystem.exists(resolvedDependency)) {
139
149
  return {
140
150
  ...config,
141
- __eco: buildEcoMeta(resolvedDependency),
151
+ __eco: buildEcoMeta(path.join(candidateDir, path.basename(pagePath))),
142
152
  };
143
153
  }
144
154
  }
@@ -2,8 +2,7 @@
2
2
  * Runtime bundle service for React integration.
3
3
  *
4
4
  * Owns creation of the browser runtime assets for React and React DOM,
5
- * including temporary entry generation, specifier mapping, and React DOM
6
- * interop rewriting.
5
+ * including shared runtime entry generation and specifier mapping.
7
6
  *
8
7
  * @module
9
8
  */
@@ -21,18 +20,19 @@ export type ReactRuntimeImports = {
21
20
  export interface ReactRuntimeBundleServiceConfig {
22
21
  routerAdapter?: ReactRouterAdapter;
23
22
  }
23
+ type RuntimeMode = 'development' | 'production';
24
24
  export declare class ReactRuntimeBundleService {
25
25
  private readonly config;
26
26
  constructor(config: ReactRuntimeBundleServiceConfig);
27
- getRuntimeImports(): ReactRuntimeImports;
28
- getSpecifierMap(): Record<string, string>;
27
+ private get isDevelopment();
28
+ private getCurrentRuntimeMode;
29
+ private createRuntimeDefines;
30
+ private getReactVendorFileName;
31
+ private getReactDomVendorFileName;
32
+ private getRouterVendorFileName;
33
+ getRuntimeImports(mode?: RuntimeMode): ReactRuntimeImports;
34
+ getSpecifierMap(mode?: RuntimeMode): Record<string, string>;
29
35
  getDependencies(): AssetDefinition[];
30
- createRuntimeAliasPlugin(): EcoBuildPlugin;
31
- private buildImportMapSourceUrl;
32
- private createRuntimeSpecifierAliasPlugin;
33
- private createReactDomRuntimeInteropPlugin;
34
- private getRuntimeArtifactsDir;
35
- private createRuntimeEntry;
36
- private getModuleExportNames;
37
- private isValidExportName;
36
+ createRuntimeAliasPlugin(mode?: RuntimeMode): EcoBuildPlugin;
38
37
  }
38
+ export {};