@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
@@ -1,205 +1,123 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { createRequire } from "node:module";
4
- import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
1
+ import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
2
+ import {
3
+ buildBrowserRuntimeAssetUrl,
4
+ createBrowserRuntimeModuleAsset,
5
+ createBrowserRuntimeScriptAsset
6
+ } from "@ecopages/core/services/asset-processing-service";
7
+ import { createReactDomRuntimeInteropPlugin } from "../utils/react-dom-runtime-interop-plugin.js";
8
+ import { buildReactRuntimeSpecifierMap } from "../utils/react-runtime-specifier-map.js";
5
9
  class ReactRuntimeBundleService {
10
+ config;
6
11
  constructor(config) {
7
12
  this.config = config;
8
13
  }
9
- getRuntimeImports() {
14
+ get isDevelopment() {
15
+ return process.env.NODE_ENV === "development";
16
+ }
17
+ getCurrentRuntimeMode() {
18
+ return this.isDevelopment ? "development" : "production";
19
+ }
20
+ createRuntimeDefines(mode) {
21
+ const nodeEnv = JSON.stringify(mode);
22
+ return {
23
+ "process.env.NODE_ENV": nodeEnv,
24
+ "import.meta.env.NODE_ENV": nodeEnv
25
+ };
26
+ }
27
+ getReactVendorFileName(mode) {
28
+ return mode === "development" ? "react.development.js" : "react.js";
29
+ }
30
+ getReactDomVendorFileName(mode) {
31
+ return mode === "development" ? "react-dom.development.js" : "react-dom.js";
32
+ }
33
+ getRouterVendorFileName(mode) {
34
+ if (!this.config.routerAdapter) {
35
+ return "";
36
+ }
37
+ return mode === "development" ? `${this.config.routerAdapter.bundle.outputName}.development.js` : `${this.config.routerAdapter.bundle.outputName}.js`;
38
+ }
39
+ getRuntimeImports(mode = this.getCurrentRuntimeMode()) {
40
+ const reactVendorFileName = this.getReactVendorFileName(mode);
41
+ const reactDomVendorFileName = this.getReactDomVendorFileName(mode);
10
42
  const runtimeImports = {
11
- react: this.buildImportMapSourceUrl("react.js"),
12
- reactDomClient: this.buildImportMapSourceUrl("react-dom.js"),
13
- reactJsxRuntime: this.buildImportMapSourceUrl("react.js"),
14
- reactJsxDevRuntime: this.buildImportMapSourceUrl("react.js"),
15
- reactDom: this.buildImportMapSourceUrl("react-dom.js")
43
+ react: buildBrowserRuntimeAssetUrl(reactVendorFileName),
44
+ reactDomClient: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
45
+ reactJsxRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
46
+ reactJsxDevRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
47
+ reactDom: buildBrowserRuntimeAssetUrl(reactDomVendorFileName)
16
48
  };
17
49
  if (this.config.routerAdapter) {
18
- runtimeImports.router = this.buildImportMapSourceUrl(`${this.config.routerAdapter.bundle.outputName}.js`);
50
+ runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
19
51
  }
20
52
  return runtimeImports;
21
53
  }
22
- getSpecifierMap() {
23
- const runtimeImports = this.getRuntimeImports();
24
- const map = {
25
- react: runtimeImports.react,
26
- "react/jsx-runtime": runtimeImports.reactJsxRuntime,
27
- "react/jsx-dev-runtime": runtimeImports.reactJsxDevRuntime,
28
- "react-dom": runtimeImports.reactDom,
29
- "react-dom/client": runtimeImports.reactDomClient
30
- };
31
- if (this.config.routerAdapter && runtimeImports.router) {
32
- map[this.config.routerAdapter.importMapKey] = runtimeImports.router;
33
- }
34
- return map;
54
+ getSpecifierMap(mode = this.getCurrentRuntimeMode()) {
55
+ return buildReactRuntimeSpecifierMap(this.getRuntimeImports(mode), this.config.routerAdapter);
35
56
  }
36
57
  getDependencies() {
37
- const runtimeAttrs = { type: "module", defer: "" };
38
- const runtimeImports = this.getRuntimeImports();
39
- const reactRuntimeAliasPlugin = this.createRuntimeSpecifierAliasPlugin({
40
- react: runtimeImports.react
41
- });
42
- const reactDomRuntimeInteropPlugin = this.createReactDomRuntimeInteropPlugin();
43
- const reactEntry = this.createRuntimeEntry(
44
- [
45
- { specifier: "react", defaultExport: true },
46
- { specifier: "react/jsx-runtime" },
47
- { specifier: "react/jsx-dev-runtime" }
48
- ],
49
- "react-entry.mjs"
50
- );
51
- const reactDomEntry = this.createRuntimeEntry(
52
- [{ specifier: "react-dom", defaultExport: true }, { specifier: "react-dom/client" }],
53
- "react-dom-entry.mjs"
54
- );
55
- const dependencies = [
56
- AssetFactory.createNodeModuleScript({
57
- position: "head",
58
- importPath: reactEntry,
59
- name: "react",
60
- excludeFromHtml: true,
61
- bundleOptions: { naming: "react.js" },
62
- attributes: runtimeAttrs
63
- }),
64
- AssetFactory.createNodeModuleScript({
65
- position: "head",
66
- importPath: reactDomEntry,
67
- name: "react-dom",
68
- excludeFromHtml: true,
69
- bundleOptions: {
70
- naming: "react-dom.js",
71
- plugins: [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin]
58
+ const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin();
59
+ const dependencies = [];
60
+ for (const mode of ["production", "development"]) {
61
+ const reactRuntimeAliasPlugin = createRuntimeSpecifierAliasPlugin(
62
+ {
63
+ react: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
72
64
  },
73
- attributes: runtimeAttrs
74
- })
75
- ];
76
- if (this.config.routerAdapter) {
77
- const runtimeAliasPlugin = this.createRuntimeAliasPlugin();
78
- const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap()));
79
- const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
80
- (external) => !mappedSpecifiers.has(external)
65
+ { name: `react-plugin-runtime-specifier-alias-${mode}` }
81
66
  );
67
+ const reactDomBundlePlugins = [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin].filter(
68
+ (plugin) => plugin !== null
69
+ );
70
+ const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
71
+ const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap(mode)));
82
72
  dependencies.push(
83
- AssetFactory.createNodeModuleScript({
84
- position: "head",
85
- importPath: this.config.routerAdapter.bundle.importPath,
86
- name: this.config.routerAdapter.bundle.outputName,
87
- excludeFromHtml: true,
73
+ createBrowserRuntimeModuleAsset({
74
+ modules: [
75
+ { specifier: "react", defaultExport: true },
76
+ { specifier: "react/jsx-runtime" },
77
+ { specifier: "react/jsx-dev-runtime" }
78
+ ],
79
+ name: "react",
80
+ fileName: this.getReactVendorFileName(mode),
81
+ cacheDirName: `ecopages-react-runtime-${mode}`,
88
82
  bundleOptions: {
89
- naming: `${this.config.routerAdapter.bundle.outputName}.js`,
90
- external: unresolvedExternals,
91
- plugins: [runtimeAliasPlugin]
92
- },
93
- attributes: runtimeAttrs
94
- })
95
- );
96
- }
97
- return dependencies;
98
- }
99
- createRuntimeAliasPlugin() {
100
- const specifierMap = this.getSpecifierMap();
101
- const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102
- const filter = new RegExp(
103
- `^(${Object.keys(specifierMap).map((key) => escapeRegExp(key)).join("|")})$`
104
- );
105
- return {
106
- name: "react-plugin-runtime-alias",
107
- setup(build) {
108
- build.onResolve({ filter }, (args) => {
109
- const mappedPath = specifierMap[args.path];
110
- if (!mappedPath) {
111
- return void 0;
83
+ define: this.createRuntimeDefines(mode)
112
84
  }
113
- return {
114
- path: mappedPath,
115
- external: true
116
- };
117
- });
118
- }
119
- };
120
- }
121
- buildImportMapSourceUrl(fileName) {
122
- return `/${AssetFactory.RESOLVED_ASSETS_VENDORS_DIR}/${fileName}`;
123
- }
124
- createRuntimeSpecifierAliasPlugin(specifierMap, external = true) {
125
- const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
126
- const filter = new RegExp(
127
- `^(${Object.keys(specifierMap).map((key) => escapeRegExp(key)).join("|")})$`
128
- );
129
- return {
130
- name: "react-plugin-runtime-specifier-alias",
131
- setup(build) {
132
- build.onResolve({ filter }, (args) => {
133
- const mappedPath = specifierMap[args.path];
134
- if (!mappedPath) {
135
- return void 0;
136
- }
137
- return {
138
- path: mappedPath,
139
- external
140
- };
141
- });
142
- }
143
- };
144
- }
145
- createReactDomRuntimeInteropPlugin() {
146
- const reactDomFileFilter = /[\\/]react-dom[\\/].*\.js$/;
147
- const reactRequirePattern = /\brequire\((['"])react\1\)/g;
148
- return {
149
- name: "react-dom-runtime-interop",
150
- setup(build) {
151
- build.onLoad({ filter: reactDomFileFilter }, (args) => {
152
- const content = fs.readFileSync(args.path, "utf-8");
153
- if (!reactRequirePattern.test(content)) {
154
- return void 0;
85
+ }),
86
+ createBrowserRuntimeModuleAsset({
87
+ modules: [{ specifier: "react-dom", defaultExport: true }, { specifier: "react-dom/client" }],
88
+ name: "react-dom",
89
+ fileName: this.getReactDomVendorFileName(mode),
90
+ cacheDirName: `ecopages-react-runtime-${mode}`,
91
+ bundleOptions: {
92
+ define: this.createRuntimeDefines(mode),
93
+ plugins: reactDomBundlePlugins
155
94
  }
156
- reactRequirePattern.lastIndex = 0;
157
- const rewritten = content.replace(reactRequirePattern, "__ecopages_react_runtime");
158
- return {
159
- contents: `import * as __ecopages_react_runtime from 'react';
160
- ${rewritten}`,
161
- loader: "js",
162
- resolveDir: path.dirname(args.path)
163
- };
164
- });
165
- }
166
- };
167
- }
168
- getRuntimeArtifactsDir() {
169
- const tmpDir = path.join(process.cwd(), "node_modules", ".cache", "ecopages-react-runtime");
170
- fs.mkdirSync(tmpDir, { recursive: true });
171
- return tmpDir;
172
- }
173
- createRuntimeEntry(modules, fileName) {
174
- const tmpDir = this.getRuntimeArtifactsDir();
175
- const requireFromRoot = createRequire(path.join(process.cwd(), "package.json"));
176
- const seenExports = /* @__PURE__ */ new Set();
177
- const statements = [];
178
- for (const module of modules) {
179
- if (module.defaultExport) {
180
- statements.push(`import __ecopages_default_export__ from '${module.specifier}';`);
181
- statements.push("export default __ecopages_default_export__;");
182
- }
183
- const exportNames = this.getModuleExportNames(module.specifier, requireFromRoot).filter(
184
- (name) => !seenExports.has(name)
95
+ })
185
96
  );
186
- if (exportNames.length > 0) {
187
- statements.push(`export { ${exportNames.join(", ")} } from '${module.specifier}';`);
188
- for (const exportName of exportNames) {
189
- seenExports.add(exportName);
190
- }
97
+ if (this.config.routerAdapter) {
98
+ const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
99
+ (external) => !mappedSpecifiers.has(external)
100
+ );
101
+ dependencies.push(
102
+ createBrowserRuntimeScriptAsset({
103
+ importPath: this.config.routerAdapter.bundle.importPath,
104
+ name: this.config.routerAdapter.bundle.outputName,
105
+ fileName: this.getRouterVendorFileName(mode),
106
+ bundleOptions: {
107
+ define: this.createRuntimeDefines(mode),
108
+ external: unresolvedExternals,
109
+ plugins: [runtimeAliasPlugin]
110
+ }
111
+ })
112
+ );
191
113
  }
192
114
  }
193
- const filePath = path.join(tmpDir, fileName);
194
- fs.writeFileSync(filePath, statements.join("\n"), "utf-8");
195
- return filePath;
196
- }
197
- getModuleExportNames(specifier, requireFromRoot) {
198
- const moduleExports = requireFromRoot(specifier);
199
- return Object.keys(moduleExports).filter((name) => name !== "__esModule" && name !== "default").filter((name) => this.isValidExportName(name)).sort();
115
+ return dependencies;
200
116
  }
201
- isValidExportName(name) {
202
- return /^[$A-Z_a-z][$\w]*$/.test(name);
117
+ createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()) {
118
+ return createRuntimeSpecifierAliasPlugin(this.getSpecifierMap(mode), {
119
+ name: `react-plugin-runtime-alias-${mode}`
120
+ });
203
121
  }
204
122
  }
205
123
  export {
@@ -2,23 +2,22 @@
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
  */
10
9
 
11
- import fs from 'node:fs';
12
- import path from 'node:path';
13
- import { createRequire } from 'node:module';
14
10
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
15
- import { type AssetDefinition, AssetFactory } from '@ecopages/core/services/asset-processing-service';
11
+ import { createRuntimeSpecifierAliasPlugin } from '@ecopages/core/build/runtime-specifier-alias-plugin';
12
+ import {
13
+ buildBrowserRuntimeAssetUrl,
14
+ createBrowserRuntimeModuleAsset,
15
+ createBrowserRuntimeScriptAsset,
16
+ type AssetDefinition,
17
+ } from '@ecopages/core/services/asset-processing-service';
16
18
  import type { ReactRouterAdapter } from '../router-adapter.ts';
17
-
18
- type RuntimeModuleConfig = {
19
- specifier: string;
20
- defaultExport?: boolean;
21
- };
19
+ import { createReactDomRuntimeInteropPlugin } from '../utils/react-dom-runtime-interop-plugin.ts';
20
+ import { buildReactRuntimeSpecifierMap } from '../utils/react-runtime-specifier-map.ts';
22
21
 
23
22
  export type ReactRuntimeImports = {
24
23
  react: string;
@@ -33,239 +32,141 @@ export interface ReactRuntimeBundleServiceConfig {
33
32
  routerAdapter?: ReactRouterAdapter;
34
33
  }
35
34
 
36
- export class ReactRuntimeBundleService {
37
- constructor(private readonly config: ReactRuntimeBundleServiceConfig) {}
38
-
39
- getRuntimeImports(): ReactRuntimeImports {
40
- const runtimeImports: ReactRuntimeImports = {
41
- react: this.buildImportMapSourceUrl('react.js'),
42
- reactDomClient: this.buildImportMapSourceUrl('react-dom.js'),
43
- reactJsxRuntime: this.buildImportMapSourceUrl('react.js'),
44
- reactJsxDevRuntime: this.buildImportMapSourceUrl('react.js'),
45
- reactDom: this.buildImportMapSourceUrl('react-dom.js'),
46
- };
35
+ type RuntimeMode = 'development' | 'production';
47
36
 
48
- if (this.config.routerAdapter) {
49
- runtimeImports.router = this.buildImportMapSourceUrl(`${this.config.routerAdapter.bundle.outputName}.js`);
50
- }
37
+ export class ReactRuntimeBundleService {
38
+ private readonly config: ReactRuntimeBundleServiceConfig;
51
39
 
52
- return runtimeImports;
40
+ constructor(config: ReactRuntimeBundleServiceConfig) {
41
+ this.config = config;
53
42
  }
54
43
 
55
- getSpecifierMap(): Record<string, string> {
56
- const runtimeImports = this.getRuntimeImports();
57
- const map: Record<string, string> = {
58
- react: runtimeImports.react,
59
- 'react/jsx-runtime': runtimeImports.reactJsxRuntime,
60
- 'react/jsx-dev-runtime': runtimeImports.reactJsxDevRuntime,
61
- 'react-dom': runtimeImports.reactDom,
62
- 'react-dom/client': runtimeImports.reactDomClient,
63
- };
64
-
65
- if (this.config.routerAdapter && runtimeImports.router) {
66
- map[this.config.routerAdapter.importMapKey] = runtimeImports.router;
67
- }
68
-
69
- return map;
44
+ private get isDevelopment(): boolean {
45
+ return process.env.NODE_ENV === 'development';
70
46
  }
71
47
 
72
- getDependencies(): AssetDefinition[] {
73
- const runtimeAttrs = { type: 'module', defer: '' } as const;
74
- const runtimeImports = this.getRuntimeImports();
75
- const reactRuntimeAliasPlugin = this.createRuntimeSpecifierAliasPlugin({
76
- react: runtimeImports.react,
77
- });
78
- const reactDomRuntimeInteropPlugin = this.createReactDomRuntimeInteropPlugin();
79
-
80
- const reactEntry = this.createRuntimeEntry(
81
- [
82
- { specifier: 'react', defaultExport: true },
83
- { specifier: 'react/jsx-runtime' },
84
- { specifier: 'react/jsx-dev-runtime' },
85
- ],
86
- 'react-entry.mjs',
87
- );
88
- const reactDomEntry = this.createRuntimeEntry(
89
- [{ specifier: 'react-dom', defaultExport: true }, { specifier: 'react-dom/client' }],
90
- 'react-dom-entry.mjs',
91
- );
92
-
93
- const dependencies: AssetDefinition[] = [
94
- AssetFactory.createNodeModuleScript({
95
- position: 'head',
96
- importPath: reactEntry,
97
- name: 'react',
98
- excludeFromHtml: true,
99
- bundleOptions: { naming: 'react.js' },
100
- attributes: runtimeAttrs,
101
- }),
102
- AssetFactory.createNodeModuleScript({
103
- position: 'head',
104
- importPath: reactDomEntry,
105
- name: 'react-dom',
106
- excludeFromHtml: true,
107
- bundleOptions: {
108
- naming: 'react-dom.js',
109
- plugins: [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin],
110
- },
111
- attributes: runtimeAttrs,
112
- }),
113
- ];
114
-
115
- if (this.config.routerAdapter) {
116
- const runtimeAliasPlugin = this.createRuntimeAliasPlugin();
117
- const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap()));
118
- const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
119
- (external) => !mappedSpecifiers.has(external),
120
- );
121
-
122
- dependencies.push(
123
- AssetFactory.createNodeModuleScript({
124
- position: 'head',
125
- importPath: this.config.routerAdapter.bundle.importPath,
126
- name: this.config.routerAdapter.bundle.outputName,
127
- excludeFromHtml: true,
128
- bundleOptions: {
129
- naming: `${this.config.routerAdapter.bundle.outputName}.js`,
130
- external: unresolvedExternals,
131
- plugins: [runtimeAliasPlugin],
132
- },
133
- attributes: runtimeAttrs,
134
- }),
135
- );
136
- }
137
-
138
- return dependencies;
48
+ private getCurrentRuntimeMode(): RuntimeMode {
49
+ return this.isDevelopment ? 'development' : 'production';
139
50
  }
140
51
 
141
- createRuntimeAliasPlugin(): EcoBuildPlugin {
142
- const specifierMap = this.getSpecifierMap();
143
- const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
144
- const filter = new RegExp(
145
- `^(${Object.keys(specifierMap)
146
- .map((key) => escapeRegExp(key))
147
- .join('|')})$`,
148
- );
52
+ private createRuntimeDefines(mode: RuntimeMode): Record<string, string> {
53
+ const nodeEnv = JSON.stringify(mode);
149
54
 
150
55
  return {
151
- name: 'react-plugin-runtime-alias',
152
- setup(build) {
153
- build.onResolve({ filter }, (args) => {
154
- const mappedPath = specifierMap[args.path];
155
- if (!mappedPath) {
156
- return undefined;
157
- }
158
-
159
- return {
160
- path: mappedPath,
161
- external: true,
162
- };
163
- });
164
- },
56
+ 'process.env.NODE_ENV': nodeEnv,
57
+ 'import.meta.env.NODE_ENV': nodeEnv,
165
58
  };
166
59
  }
167
60
 
168
- private buildImportMapSourceUrl(fileName: string): string {
169
- return `/${AssetFactory.RESOLVED_ASSETS_VENDORS_DIR}/${fileName}`;
61
+ private getReactVendorFileName(mode: RuntimeMode): string {
62
+ return mode === 'development' ? 'react.development.js' : 'react.js';
170
63
  }
171
64
 
172
- private createRuntimeSpecifierAliasPlugin(specifierMap: Record<string, string>, external = true): EcoBuildPlugin {
173
- const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
174
- const filter = new RegExp(
175
- `^(${Object.keys(specifierMap)
176
- .map((key) => escapeRegExp(key))
177
- .join('|')})$`,
178
- );
65
+ private getReactDomVendorFileName(mode: RuntimeMode): string {
66
+ return mode === 'development' ? 'react-dom.development.js' : 'react-dom.js';
67
+ }
179
68
 
180
- return {
181
- name: 'react-plugin-runtime-specifier-alias',
182
- setup(build) {
183
- build.onResolve({ filter }, (args) => {
184
- const mappedPath = specifierMap[args.path];
185
- if (!mappedPath) {
186
- return undefined;
187
- }
69
+ private getRouterVendorFileName(mode: RuntimeMode): string {
70
+ if (!this.config.routerAdapter) {
71
+ return '';
72
+ }
188
73
 
189
- return {
190
- path: mappedPath,
191
- external,
192
- };
193
- });
194
- },
195
- };
74
+ return mode === 'development'
75
+ ? `${this.config.routerAdapter.bundle.outputName}.development.js`
76
+ : `${this.config.routerAdapter.bundle.outputName}.js`;
196
77
  }
197
78
 
198
- private createReactDomRuntimeInteropPlugin(): EcoBuildPlugin {
199
- const reactDomFileFilter = /[\\/]react-dom[\\/].*\.js$/;
200
- const reactRequirePattern = /\brequire\((['"])react\1\)/g;
201
-
202
- return {
203
- name: 'react-dom-runtime-interop',
204
- setup(build) {
205
- build.onLoad({ filter: reactDomFileFilter }, (args) => {
206
- const content = fs.readFileSync(args.path, 'utf-8');
207
- if (!reactRequirePattern.test(content)) {
208
- return undefined;
209
- }
79
+ getRuntimeImports(mode = this.getCurrentRuntimeMode()): ReactRuntimeImports {
80
+ const reactVendorFileName = this.getReactVendorFileName(mode);
81
+ const reactDomVendorFileName = this.getReactDomVendorFileName(mode);
82
+ const runtimeImports: ReactRuntimeImports = {
83
+ react: buildBrowserRuntimeAssetUrl(reactVendorFileName),
84
+ reactDomClient: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
85
+ reactJsxRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
86
+ reactJsxDevRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
87
+ reactDom: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
88
+ };
210
89
 
211
- reactRequirePattern.lastIndex = 0;
212
- const rewritten = content.replace(reactRequirePattern, '__ecopages_react_runtime');
90
+ if (this.config.routerAdapter) {
91
+ runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
92
+ }
213
93
 
214
- return {
215
- contents: `import * as __ecopages_react_runtime from 'react';\n${rewritten}`,
216
- loader: 'js',
217
- resolveDir: path.dirname(args.path),
218
- };
219
- });
220
- },
221
- };
94
+ return runtimeImports;
222
95
  }
223
96
 
224
- private getRuntimeArtifactsDir(): string {
225
- const tmpDir = path.join(process.cwd(), 'node_modules', '.cache', 'ecopages-react-runtime');
226
- fs.mkdirSync(tmpDir, { recursive: true });
227
- return tmpDir;
97
+ getSpecifierMap(mode = this.getCurrentRuntimeMode()): Record<string, string> {
98
+ return buildReactRuntimeSpecifierMap(this.getRuntimeImports(mode), this.config.routerAdapter);
228
99
  }
229
100
 
230
- private createRuntimeEntry(modules: RuntimeModuleConfig[], fileName: string): string {
231
- const tmpDir = this.getRuntimeArtifactsDir();
232
- const requireFromRoot = createRequire(path.join(process.cwd(), 'package.json'));
233
- const seenExports = new Set<string>();
234
- const statements: string[] = [];
101
+ getDependencies(): AssetDefinition[] {
102
+ const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin();
103
+ const dependencies: AssetDefinition[] = [];
235
104
 
236
- for (const module of modules) {
237
- if (module.defaultExport) {
238
- statements.push(`import __ecopages_default_export__ from '${module.specifier}';`);
239
- statements.push('export default __ecopages_default_export__;');
240
- }
105
+ for (const mode of ['production', 'development'] as const) {
106
+ const reactRuntimeAliasPlugin = createRuntimeSpecifierAliasPlugin(
107
+ {
108
+ react: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode)),
109
+ },
110
+ { name: `react-plugin-runtime-specifier-alias-${mode}` },
111
+ );
112
+ const reactDomBundlePlugins = [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin].filter(
113
+ (plugin): plugin is EcoBuildPlugin => plugin !== null,
114
+ );
115
+ const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
116
+ const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap(mode)));
241
117
 
242
- const exportNames = this.getModuleExportNames(module.specifier, requireFromRoot).filter(
243
- (name) => !seenExports.has(name),
118
+ dependencies.push(
119
+ createBrowserRuntimeModuleAsset({
120
+ modules: [
121
+ { specifier: 'react', defaultExport: true },
122
+ { specifier: 'react/jsx-runtime' },
123
+ { specifier: 'react/jsx-dev-runtime' },
124
+ ],
125
+ name: 'react',
126
+ fileName: this.getReactVendorFileName(mode),
127
+ cacheDirName: `ecopages-react-runtime-${mode}`,
128
+ bundleOptions: {
129
+ define: this.createRuntimeDefines(mode),
130
+ },
131
+ }),
132
+ createBrowserRuntimeModuleAsset({
133
+ modules: [{ specifier: 'react-dom', defaultExport: true }, { specifier: 'react-dom/client' }],
134
+ name: 'react-dom',
135
+ fileName: this.getReactDomVendorFileName(mode),
136
+ cacheDirName: `ecopages-react-runtime-${mode}`,
137
+ bundleOptions: {
138
+ define: this.createRuntimeDefines(mode),
139
+ plugins: reactDomBundlePlugins,
140
+ },
141
+ }),
244
142
  );
245
143
 
246
- if (exportNames.length > 0) {
247
- statements.push(`export { ${exportNames.join(', ')} } from '${module.specifier}';`);
248
- for (const exportName of exportNames) {
249
- seenExports.add(exportName);
250
- }
144
+ if (this.config.routerAdapter) {
145
+ const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
146
+ (external) => !mappedSpecifiers.has(external),
147
+ );
148
+
149
+ dependencies.push(
150
+ createBrowserRuntimeScriptAsset({
151
+ importPath: this.config.routerAdapter.bundle.importPath,
152
+ name: this.config.routerAdapter.bundle.outputName,
153
+ fileName: this.getRouterVendorFileName(mode),
154
+ bundleOptions: {
155
+ define: this.createRuntimeDefines(mode),
156
+ external: unresolvedExternals,
157
+ plugins: [runtimeAliasPlugin],
158
+ },
159
+ }),
160
+ );
251
161
  }
252
162
  }
253
163
 
254
- const filePath = path.join(tmpDir, fileName);
255
- fs.writeFileSync(filePath, statements.join('\n'), 'utf-8');
256
- return filePath;
257
- }
258
-
259
- private getModuleExportNames(specifier: string, requireFromRoot: ReturnType<typeof createRequire>): string[] {
260
- const moduleExports = requireFromRoot(specifier);
261
-
262
- return Object.keys(moduleExports)
263
- .filter((name) => name !== '__esModule' && name !== 'default')
264
- .filter((name) => this.isValidExportName(name))
265
- .sort();
164
+ return dependencies;
266
165
  }
267
166
 
268
- private isValidExportName(name: string): boolean {
269
- return /^[$A-Z_a-z][$\w]*$/.test(name);
167
+ createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()): EcoBuildPlugin {
168
+ return createRuntimeSpecifierAliasPlugin(this.getSpecifierMap(mode), {
169
+ name: `react-plugin-runtime-alias-${mode}`,
170
+ })!;
270
171
  }
271
172
  }