@ecopages/react 0.2.0-alpha.1 → 0.2.0-alpha.10

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 (47) hide show
  1. package/CHANGELOG.md +11 -46
  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-renderer.d.ts +130 -11
  7. package/src/react-renderer.js +368 -64
  8. package/src/react.plugin.d.ts +17 -5
  9. package/src/react.plugin.js +44 -13
  10. package/src/router-adapter.d.ts +2 -2
  11. package/src/services/react-bundle.service.d.ts +2 -25
  12. package/src/services/react-bundle.service.js +21 -91
  13. package/src/services/react-hydration-asset.service.js +3 -3
  14. package/src/services/react-page-module.service.d.ts +3 -0
  15. package/src/services/react-page-module.service.js +20 -16
  16. package/src/services/react-runtime-bundle.service.d.ts +12 -12
  17. package/src/services/react-runtime-bundle.service.js +98 -180
  18. package/src/utils/client-graph-boundary-plugin.js +147 -9
  19. package/src/utils/hydration-scripts.d.ts +18 -1
  20. package/src/utils/hydration-scripts.js +83 -32
  21. package/src/utils/reachability-analyzer.d.ts +12 -1
  22. package/src/utils/reachability-analyzer.js +101 -5
  23. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  24. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  25. package/src/utils/react-mdx-loader-plugin.js +13 -5
  26. package/src/utils/react-runtime-specifier-map.d.ts +6 -0
  27. package/src/utils/react-runtime-specifier-map.js +37 -0
  28. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  29. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  30. package/src/react-hmr-strategy.ts +0 -444
  31. package/src/react-renderer.ts +0 -403
  32. package/src/react.plugin.ts +0 -241
  33. package/src/router-adapter.ts +0 -95
  34. package/src/services/react-bundle.service.ts +0 -212
  35. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  36. package/src/services/react-hydration-asset.service.ts +0 -260
  37. package/src/services/react-page-module.service.ts +0 -214
  38. package/src/services/react-runtime-bundle.service.ts +0 -271
  39. package/src/utils/client-graph-boundary-plugin.ts +0 -590
  40. package/src/utils/client-only.ts +0 -27
  41. package/src/utils/declared-modules.ts +0 -99
  42. package/src/utils/dynamic.ts +0 -27
  43. package/src/utils/hmr-scripts.ts +0 -47
  44. package/src/utils/html-boundary.ts +0 -66
  45. package/src/utils/hydration-scripts.ts +0 -338
  46. package/src/utils/reachability-analyzer.ts +0 -440
  47. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -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.
@@ -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
  */
@@ -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,28 +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
- * Creates the esbuild plugin that shims `use-sync-external-store/shim`
63
- * to re-export from React's built-in `useSyncExternalStore`.
64
- * This is needed because some packages use `use-sync-external-store/shim`
65
- * but React 18+ has built-in `useSyncExternalStore`.
66
- */
67
- private createSyncExternalStorePlugin;
44
+ createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>): import("packages/core/src/build/build-types.ts").EcoBuildPlugin | null;
68
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,24 +46,19 @@ 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);
55
- options.plugins = [runtimeAliasPlugin, mdxPlugin, useSyncExternalStoreShimPlugin, graphBoundaryPlugin];
59
+ options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, mdxPlugin, useSyncExternalStoreShimPlugin];
56
60
  } else {
57
- options.plugins = [runtimeAliasPlugin, useSyncExternalStoreShimPlugin, graphBoundaryPlugin];
61
+ options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, useSyncExternalStoreShimPlugin];
58
62
  }
59
63
  return options;
60
64
  }
@@ -62,82 +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
- * Creates the esbuild plugin that shims `use-sync-external-store/shim`
98
- * to re-export from React's built-in `useSyncExternalStore`.
99
- * This is needed because some packages use `use-sync-external-store/shim`
100
- * but React 18+ has built-in `useSyncExternalStore`.
101
- */
102
- createSyncExternalStorePlugin() {
103
- return {
104
- name: "react-renderer-use-sync-external-store-shim",
105
- setup(build) {
106
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
107
- path: "use-sync-external-store/shim",
108
- namespace: "ecopages-react-renderer-shim"
109
- }));
110
- build.onLoad(
111
- { filter: /^use-sync-external-store\/shim$/, namespace: "ecopages-react-renderer-shim" },
112
- () => ({
113
- contents: "export { useSyncExternalStore } from 'react';",
114
- loader: "js"
115
- })
116
- );
117
- build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
118
- contents: "export { useSyncExternalStore } from 'react';",
119
- loader: "js"
120
- }));
121
- build.onLoad(
122
- {
123
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/
124
- },
125
- () => ({
126
- contents: "export { useSyncExternalStore } from 'react';",
127
- loader: "js"
128
- })
129
- );
130
- build.onLoad(
131
- {
132
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.production\.js$/
133
- },
134
- () => ({
135
- contents: "export { useSyncExternalStore } from 'react';",
136
- loader: "js"
137
- })
138
- );
139
- }
140
- };
69
+ createRuntimeAliasPlugin(runtimeSpecifierMap) {
70
+ return createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, { name: "react-runtime-import-alias" });
141
71
  }
142
72
  }
143
73
  export {
@@ -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: {
@@ -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
  }
@@ -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 {};