@ecopages/react 0.2.0-alpha.5 → 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.
- package/CHANGELOG.md +22 -41
- package/README.md +135 -29
- package/package.json +3 -3
- package/src/react-hmr-strategy.d.ts +22 -30
- package/src/react-hmr-strategy.js +57 -120
- package/src/react-hmr-strategy.ts +76 -145
- package/src/react-renderer.d.ts +130 -11
- package/src/react-renderer.js +368 -64
- package/src/react-renderer.ts +490 -90
- package/src/react.plugin.d.ts +17 -5
- package/src/react.plugin.js +44 -13
- package/src/react.plugin.ts +49 -14
- package/src/router-adapter.d.ts +2 -2
- package/src/router-adapter.ts +2 -2
- package/src/services/react-bundle.service.d.ts +2 -30
- package/src/services/react-bundle.service.js +19 -94
- package/src/services/react-bundle.service.ts +20 -129
- package/src/services/react-hydration-asset.service.js +3 -3
- package/src/services/react-hydration-asset.service.ts +7 -4
- package/src/services/react-page-module.service.d.ts +3 -0
- package/src/services/react-page-module.service.js +20 -16
- package/src/services/react-page-module.service.ts +27 -17
- package/src/services/react-runtime-bundle.service.d.ts +12 -12
- package/src/services/react-runtime-bundle.service.js +98 -180
- package/src/services/react-runtime-bundle.service.ts +112 -211
- package/src/utils/client-graph-boundary-plugin.js +78 -1
- package/src/utils/client-graph-boundary-plugin.ts +122 -1
- package/src/utils/hydration-scripts.d.ts +18 -1
- package/src/utils/hydration-scripts.js +83 -32
- package/src/utils/hydration-scripts.ts +159 -38
- package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
- package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
- package/src/utils/react-dom-runtime-interop-plugin.ts +33 -0
- package/src/utils/react-mdx-loader-plugin.js +13 -5
- package/src/utils/react-mdx-loader-plugin.ts +28 -5
- package/src/utils/react-runtime-specifier-map.d.ts +6 -0
- package/src/utils/react-runtime-specifier-map.js +37 -0
- package/src/utils/react-runtime-specifier-map.ts +45 -0
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
- package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
- package/src/utils/use-sync-external-store-shim-plugin.ts +45 -0
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
37
|
+
export class ReactRuntimeBundleService {
|
|
38
|
+
private readonly config: ReactRuntimeBundleServiceConfig;
|
|
51
39
|
|
|
52
|
-
|
|
40
|
+
constructor(config: ReactRuntimeBundleServiceConfig) {
|
|
41
|
+
this.config = config;
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
142
|
-
const
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
169
|
-
return
|
|
61
|
+
private getReactVendorFileName(mode: RuntimeMode): string {
|
|
62
|
+
return mode === 'development' ? 'react.development.js' : 'react.js';
|
|
170
63
|
}
|
|
171
64
|
|
|
172
|
-
private
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
212
|
-
|
|
90
|
+
if (this.config.routerAdapter) {
|
|
91
|
+
runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
|
|
92
|
+
}
|
|
213
93
|
|
|
214
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
231
|
-
const
|
|
232
|
-
const
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
(
|
|
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 (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
return
|
|
167
|
+
createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()): EcoBuildPlugin {
|
|
168
|
+
return createRuntimeSpecifierAliasPlugin(this.getSpecifierMap(mode), {
|
|
169
|
+
name: `react-plugin-runtime-alias-${mode}`,
|
|
170
|
+
})!;
|
|
270
171
|
}
|
|
271
172
|
}
|
|
@@ -3,6 +3,14 @@ import { dirname, extname, resolve } from "node:path";
|
|
|
3
3
|
import { parseSync } from "oxc-parser";
|
|
4
4
|
import { analyzeReachability } from "./reachability-analyzer";
|
|
5
5
|
const SOURCE_FILE_FILTER = /\.(tsx?|jsx?)$/;
|
|
6
|
+
const SERVER_ONLY_ECO_PAGE_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
7
|
+
"cache",
|
|
8
|
+
"middleware",
|
|
9
|
+
"requires",
|
|
10
|
+
"metadata",
|
|
11
|
+
"staticProps",
|
|
12
|
+
"staticPaths"
|
|
13
|
+
]);
|
|
6
14
|
function isBareSpecifier(specifier) {
|
|
7
15
|
if (specifier.startsWith(".")) return false;
|
|
8
16
|
if (specifier.startsWith("/")) return false;
|
|
@@ -86,6 +94,62 @@ function parserLanguageForFile(filename) {
|
|
|
86
94
|
if (extension === ".jsx") return "jsx";
|
|
87
95
|
return "js";
|
|
88
96
|
}
|
|
97
|
+
function getObjectPropertyKeyName(node) {
|
|
98
|
+
if (!node) return void 0;
|
|
99
|
+
if (node.type === "Identifier") return node.name;
|
|
100
|
+
if (node.type === "StringLiteral" || node.type === "Literal") {
|
|
101
|
+
return typeof node.value === "string" ? node.value : void 0;
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
function stripServerOnlyEcoPageOptions(source, program) {
|
|
106
|
+
const edits = [];
|
|
107
|
+
function walk(node) {
|
|
108
|
+
if (!node || typeof node !== "object") return;
|
|
109
|
+
if (Array.isArray(node)) {
|
|
110
|
+
for (const child of node) walk(child);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "eco" && node.callee.property?.type === "Identifier" && node.callee.property.name === "page" && node.arguments?.[0]?.type === "ObjectExpression") {
|
|
114
|
+
const objectExpression = node.arguments[0];
|
|
115
|
+
const keptProperties = [];
|
|
116
|
+
let removedProperty = false;
|
|
117
|
+
for (const property of objectExpression.properties ?? []) {
|
|
118
|
+
if (property?.type === "Property") {
|
|
119
|
+
const keyName = getObjectPropertyKeyName(property.key);
|
|
120
|
+
if (keyName && SERVER_ONLY_ECO_PAGE_OPTION_KEYS.has(keyName)) {
|
|
121
|
+
removedProperty = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
keptProperties.push(source.slice(property.start, property.end));
|
|
126
|
+
}
|
|
127
|
+
if (removedProperty) {
|
|
128
|
+
const replacement = keptProperties.length > 0 ? `{ ${keptProperties.join(", ")} }` : "{}";
|
|
129
|
+
edits.push({
|
|
130
|
+
start: objectExpression.start,
|
|
131
|
+
end: objectExpression.end,
|
|
132
|
+
replacement
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const key in node) {
|
|
137
|
+
if (key !== "type" && key !== "start" && key !== "end") {
|
|
138
|
+
walk(node[key]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
walk(program);
|
|
143
|
+
if (edits.length === 0) {
|
|
144
|
+
return { transformed: source, modified: false };
|
|
145
|
+
}
|
|
146
|
+
edits.sort((a, b) => b.start - a.start);
|
|
147
|
+
let transformed = source;
|
|
148
|
+
for (const edit of edits) {
|
|
149
|
+
transformed = transformed.slice(0, edit.start) + edit.replacement + transformed.slice(edit.end);
|
|
150
|
+
}
|
|
151
|
+
return { transformed, modified: true };
|
|
152
|
+
}
|
|
89
153
|
function normalizeRequestedExportsKey(pathname) {
|
|
90
154
|
let normalized = pathname.replace(/\\/g, "/");
|
|
91
155
|
normalized = normalized.replace(/\.(tsx?|jsx?)$/i, "");
|
|
@@ -341,13 +405,26 @@ function transformModuleImports(source, filename, globallyAllowed, requestedExpo
|
|
|
341
405
|
}
|
|
342
406
|
walkImports(program);
|
|
343
407
|
if (edits.length === 0) {
|
|
344
|
-
return
|
|
408
|
+
return stripServerOnlyEcoPageOptions(source, program);
|
|
345
409
|
}
|
|
346
410
|
edits.sort((a, b) => b.start - a.start);
|
|
347
411
|
let transformed = source;
|
|
348
412
|
for (const edit of edits) {
|
|
349
413
|
transformed = transformed.slice(0, edit.start) + edit.replacement + transformed.slice(edit.end);
|
|
350
414
|
}
|
|
415
|
+
let reparsedResult;
|
|
416
|
+
try {
|
|
417
|
+
reparsedResult = parseSync(filename, transformed, {
|
|
418
|
+
sourceType: "module",
|
|
419
|
+
lang: parserLanguageForFile(filename)
|
|
420
|
+
});
|
|
421
|
+
} catch {
|
|
422
|
+
return { transformed, modified: true };
|
|
423
|
+
}
|
|
424
|
+
const strippedPageOptions = stripServerOnlyEcoPageOptions(transformed, reparsedResult.program);
|
|
425
|
+
if (strippedPageOptions.modified) {
|
|
426
|
+
return strippedPageOptions;
|
|
427
|
+
}
|
|
351
428
|
return { transformed, modified: true };
|
|
352
429
|
}
|
|
353
430
|
function createClientGraphBoundaryPlugin(options) {
|
|
@@ -22,6 +22,14 @@ import { parseSync } from 'oxc-parser';
|
|
|
22
22
|
import { analyzeReachability } from './reachability-analyzer';
|
|
23
23
|
|
|
24
24
|
const SOURCE_FILE_FILTER = /\.(tsx?|jsx?)$/;
|
|
25
|
+
const SERVER_ONLY_ECO_PAGE_OPTION_KEYS = new Set([
|
|
26
|
+
'cache',
|
|
27
|
+
'middleware',
|
|
28
|
+
'requires',
|
|
29
|
+
'metadata',
|
|
30
|
+
'staticProps',
|
|
31
|
+
'staticPaths',
|
|
32
|
+
]);
|
|
25
33
|
|
|
26
34
|
/**
|
|
27
35
|
* Configuration options for the Client Graph Boundary esbuild plugin.
|
|
@@ -205,6 +213,104 @@ function parserLanguageForFile(filename: string): 'js' | 'jsx' | 'ts' | 'tsx' {
|
|
|
205
213
|
return 'js';
|
|
206
214
|
}
|
|
207
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Extracts a static property key name from an object literal property node.
|
|
218
|
+
*
|
|
219
|
+
* The client graph boundary rewrite only strips known `eco.page(...)` keys when
|
|
220
|
+
* it can prove the property name statically. Computed or otherwise dynamic keys
|
|
221
|
+
* are ignored so the transform remains conservative.
|
|
222
|
+
*
|
|
223
|
+
* @param node - OXC AST node representing an object property key.
|
|
224
|
+
* @returns Static property key name when it can be resolved, otherwise `undefined`.
|
|
225
|
+
*/
|
|
226
|
+
function getObjectPropertyKeyName(node: any): string | undefined {
|
|
227
|
+
if (!node) return undefined;
|
|
228
|
+
if (node.type === 'Identifier') return node.name;
|
|
229
|
+
if (node.type === 'StringLiteral' || node.type === 'Literal') {
|
|
230
|
+
return typeof node.value === 'string' ? node.value : undefined;
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Removes server-only `eco.page(...)` options from browser-bound modules.
|
|
237
|
+
*
|
|
238
|
+
* Import pruning alone is not sufficient because a page module can still retain
|
|
239
|
+
* references to stripped server imports through config fields like `middleware`
|
|
240
|
+
* or `metadata`. This pass rewrites the `eco.page(...)` object literal so only
|
|
241
|
+
* browser-relevant properties remain.
|
|
242
|
+
*
|
|
243
|
+
* @param source - Original or already-transformed module source.
|
|
244
|
+
* @param program - Parsed OXC program for the same source text.
|
|
245
|
+
* @returns Updated source plus a flag indicating whether any rewrite occurred.
|
|
246
|
+
*/
|
|
247
|
+
function stripServerOnlyEcoPageOptions(source: string, program: any): { transformed: string; modified: boolean } {
|
|
248
|
+
const edits: { start: number; end: number; replacement: string }[] = [];
|
|
249
|
+
|
|
250
|
+
function walk(node: any) {
|
|
251
|
+
if (!node || typeof node !== 'object') return;
|
|
252
|
+
if (Array.isArray(node)) {
|
|
253
|
+
for (const child of node) walk(child);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (
|
|
258
|
+
node.type === 'CallExpression' &&
|
|
259
|
+
node.callee?.type === 'MemberExpression' &&
|
|
260
|
+
node.callee.object?.type === 'Identifier' &&
|
|
261
|
+
node.callee.object.name === 'eco' &&
|
|
262
|
+
node.callee.property?.type === 'Identifier' &&
|
|
263
|
+
node.callee.property.name === 'page' &&
|
|
264
|
+
node.arguments?.[0]?.type === 'ObjectExpression'
|
|
265
|
+
) {
|
|
266
|
+
const objectExpression = node.arguments[0];
|
|
267
|
+
const keptProperties: string[] = [];
|
|
268
|
+
let removedProperty = false;
|
|
269
|
+
|
|
270
|
+
for (const property of objectExpression.properties ?? []) {
|
|
271
|
+
if (property?.type === 'Property') {
|
|
272
|
+
const keyName = getObjectPropertyKeyName(property.key);
|
|
273
|
+
if (keyName && SERVER_ONLY_ECO_PAGE_OPTION_KEYS.has(keyName)) {
|
|
274
|
+
removedProperty = true;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
keptProperties.push(source.slice(property.start, property.end));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (removedProperty) {
|
|
283
|
+
const replacement = keptProperties.length > 0 ? `{ ${keptProperties.join(', ')} }` : '{}';
|
|
284
|
+
edits.push({
|
|
285
|
+
start: objectExpression.start,
|
|
286
|
+
end: objectExpression.end,
|
|
287
|
+
replacement,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const key in node) {
|
|
293
|
+
if (key !== 'type' && key !== 'start' && key !== 'end') {
|
|
294
|
+
walk(node[key]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
walk(program);
|
|
300
|
+
|
|
301
|
+
if (edits.length === 0) {
|
|
302
|
+
return { transformed: source, modified: false };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
edits.sort((a, b) => b.start - a.start);
|
|
306
|
+
let transformed = source;
|
|
307
|
+
for (const edit of edits) {
|
|
308
|
+
transformed = transformed.slice(0, edit.start) + edit.replacement + transformed.slice(edit.end);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { transformed, modified: true };
|
|
312
|
+
}
|
|
313
|
+
|
|
208
314
|
/**
|
|
209
315
|
* Tracks the subset of exports that a downstream local module is allowed to expose.
|
|
210
316
|
*
|
|
@@ -610,7 +716,7 @@ function transformModuleImports(
|
|
|
610
716
|
walkImports(program);
|
|
611
717
|
|
|
612
718
|
if (edits.length === 0) {
|
|
613
|
-
return
|
|
719
|
+
return stripServerOnlyEcoPageOptions(source, program);
|
|
614
720
|
}
|
|
615
721
|
|
|
616
722
|
edits.sort((a, b) => b.start - a.start);
|
|
@@ -619,6 +725,21 @@ function transformModuleImports(
|
|
|
619
725
|
transformed = transformed.slice(0, edit.start) + edit.replacement + transformed.slice(edit.end);
|
|
620
726
|
}
|
|
621
727
|
|
|
728
|
+
let reparsedResult;
|
|
729
|
+
try {
|
|
730
|
+
reparsedResult = parseSync(filename, transformed, {
|
|
731
|
+
sourceType: 'module',
|
|
732
|
+
lang: parserLanguageForFile(filename),
|
|
733
|
+
});
|
|
734
|
+
} catch {
|
|
735
|
+
return { transformed, modified: true };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const strippedPageOptions = stripServerOnlyEcoPageOptions(transformed, reparsedResult.program);
|
|
739
|
+
if (strippedPageOptions.modified) {
|
|
740
|
+
return strippedPageOptions;
|
|
741
|
+
}
|
|
742
|
+
|
|
622
743
|
return { transformed, modified: true };
|
|
623
744
|
}
|
|
624
745
|
|
|
@@ -43,7 +43,18 @@ export type IslandHydrationScriptOptions = {
|
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
45
|
* Creates a hydration script for client-side React hydration.
|
|
46
|
-
*
|
|
46
|
+
*
|
|
47
|
+
* Why this dispatcher exists:
|
|
48
|
+
* the runtime matrix is small but behaviorally different across development vs
|
|
49
|
+
* production and router vs non-router pages. Keeping that branch here preserves
|
|
50
|
+
* a compact public API while allowing each emitted script to stay focused.
|
|
51
|
+
*
|
|
52
|
+
* Selection rules:
|
|
53
|
+
* - development uses readable scripts with HMR hooks
|
|
54
|
+
* - production uses minified equivalents
|
|
55
|
+
* - router presence decides whether page updates flow through the router runtime
|
|
56
|
+
* or rebuild directly from the page module
|
|
57
|
+
*
|
|
47
58
|
* @param options - Configuration options for script generation
|
|
48
59
|
* @returns The generated hydration script as a string
|
|
49
60
|
*/
|
|
@@ -63,8 +74,14 @@ export declare function createHydrationScript(options: HydrationScriptOptions):
|
|
|
63
74
|
* - resolves the component export by metadata (`componentRef`, `componentFile`)
|
|
64
75
|
* before falling back to default/first function export
|
|
65
76
|
* - selects island root using `targetSelector`
|
|
77
|
+
* - replaces the SSR host with a dedicated client-owned container
|
|
66
78
|
* - creates a fresh React root and renders with serialized `props`
|
|
67
79
|
*
|
|
80
|
+
* Why it remounts instead of hydrating:
|
|
81
|
+
* island SSR intentionally avoids synthetic wrapper elements. The runtime swaps
|
|
82
|
+
* the authored SSR node for a dedicated client-owned container before mounting
|
|
83
|
+
* so the server markup stays clean while the client still gets a stable root.
|
|
84
|
+
*
|
|
68
85
|
* @param options Island script generation options.
|
|
69
86
|
* @returns Browser-executable JavaScript module source.
|
|
70
87
|
*/
|