@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
@@ -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 {
@@ -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;
@@ -12,6 +20,10 @@ function isBareSpecifier(specifier) {
12
20
  function isProjectAliasSpecifier(specifier) {
13
21
  return specifier.startsWith("@/") || specifier.startsWith("~/") || specifier.startsWith("ecopages:");
14
22
  }
23
+ function isServerOnlySpecifier(specifier) {
24
+ if (specifier.startsWith("node:")) return true;
25
+ return /(?:^|[/])[^/]+\.server(?:$|\.)/.test(specifier);
26
+ }
15
27
  function toModuleBaseSpecifier(specifier) {
16
28
  if (!isBareSpecifier(specifier) || specifier.startsWith("node:")) {
17
29
  return specifier;
@@ -82,7 +94,93 @@ function parserLanguageForFile(filename) {
82
94
  if (extension === ".jsx") return "jsx";
83
95
  return "js";
84
96
  }
85
- function transformModuleImports(source, filename, globallyAllowed) {
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
+ }
153
+ function normalizeRequestedExportsKey(pathname) {
154
+ let normalized = pathname.replace(/\\/g, "/");
155
+ normalized = normalized.replace(/\.(tsx?|jsx?)$/i, "");
156
+ if (normalized.endsWith("/index")) {
157
+ normalized = normalized.slice(0, -"/index".length);
158
+ }
159
+ return normalized;
160
+ }
161
+ function resolveRequestedExportsKey(importer, specifier) {
162
+ if (isBareSpecifier(specifier) || isProjectAliasSpecifier(specifier)) {
163
+ return void 0;
164
+ }
165
+ const resolved = specifier.startsWith("/") ? specifier : resolve(dirname(importer), specifier);
166
+ return normalizeRequestedExportsKey(resolved);
167
+ }
168
+ function mergeRequestedExportRules(registry, moduleKey, rules) {
169
+ const existing = registry.get(moduleKey);
170
+ if (existing === "*") return;
171
+ if (rules === "*") {
172
+ registry.set(moduleKey, "*");
173
+ return;
174
+ }
175
+ if (!existing) {
176
+ registry.set(moduleKey, new Set(rules));
177
+ return;
178
+ }
179
+ for (const rule of rules) {
180
+ existing.add(rule);
181
+ }
182
+ }
183
+ function transformModuleImports(source, filename, globallyAllowed, requestedExports) {
86
184
  let result;
87
185
  try {
88
186
  result = parseSync(filename, source, {
@@ -116,12 +214,35 @@ function transformModuleImports(source, filename, globallyAllowed) {
116
214
  walk(program);
117
215
  const locallyAllowed = parseDeclaredModules(localDeclared);
118
216
  const allowedMap = mergeDeclaredModulesMap(globallyAllowed, locallyAllowed);
119
- const reachability = analyzeReachability(source, filename, program);
217
+ const explicitRequestedExports = requestedExports.get(normalizeRequestedExportsKey(filename));
218
+ const reachability = analyzeReachability(source, filename, program, explicitRequestedExports);
219
+ for (const statement of program.body) {
220
+ if (statement.type === "ImportDeclaration") {
221
+ const reachableRules = reachability.reachableImports.get(statement.source.value);
222
+ const requestedModuleKey = resolveRequestedExportsKey(filename, statement.source.value);
223
+ if (!requestedModuleKey || !reachableRules) continue;
224
+ mergeRequestedExportRules(requestedExports, requestedModuleKey, reachableRules);
225
+ continue;
226
+ }
227
+ if (statement.type === "ExportNamedDeclaration" && statement.source) {
228
+ const reachableRules = reachability.reachableImports.get(statement.source.value);
229
+ const requestedModuleKey = resolveRequestedExportsKey(filename, statement.source.value);
230
+ if (!requestedModuleKey || !reachableRules) continue;
231
+ mergeRequestedExportRules(requestedExports, requestedModuleKey, reachableRules);
232
+ continue;
233
+ }
234
+ if (statement.type === "ExportAllDeclaration" && statement.source) {
235
+ const reachableRules = reachability.reachableImports.get(statement.source.value);
236
+ const requestedModuleKey = resolveRequestedExportsKey(filename, statement.source.value);
237
+ if (!requestedModuleKey || !reachableRules) continue;
238
+ mergeRequestedExportRules(requestedExports, requestedModuleKey, reachableRules);
239
+ }
240
+ }
120
241
  const edits = [];
121
242
  function processSpecifier(specifier) {
122
243
  const moduleBase = toModuleBaseSpecifier(specifier);
123
244
  const explicitRules = allowedMap.get(moduleBase);
124
- if (specifier.startsWith("node:") || specifier.includes(".server.")) {
245
+ if (isServerOnlySpecifier(specifier)) {
125
246
  if (explicitRules) {
126
247
  return { allowed: true, rules: explicitRules };
127
248
  }
@@ -208,9 +329,10 @@ function transformModuleImports(source, filename, globallyAllowed) {
208
329
  const specifier = node.source.value;
209
330
  const { allowed } = processSpecifier(specifier);
210
331
  if (!allowed) {
211
- if (!reachability.isFallbackRoots) {
332
+ const reachableRules = reachability.reachableImports.get(specifier);
333
+ if (reachableRules && !reachability.isFallbackRoots) {
212
334
  throw new Error(
213
- `[Ecopages Client Reachability] Forbidden client export from '${specifier}' at ${filename}:${node.start}.`
335
+ `[Ecopages Client Reachability] Forbidden client export from '${specifier}' at ${filename}:${node.start}. This export is explicitly reachable from the React render function.`
214
336
  );
215
337
  } else {
216
338
  edits.push({ start: node.start, end: node.end, replacement: "" });
@@ -222,9 +344,10 @@ function transformModuleImports(source, filename, globallyAllowed) {
222
344
  const specifier = node.source.value;
223
345
  const { allowed } = processSpecifier(specifier);
224
346
  if (!allowed) {
225
- if (!reachability.isFallbackRoots) {
347
+ const reachableRules = reachability.reachableImports.get(specifier);
348
+ if (reachableRules && !reachability.isFallbackRoots) {
226
349
  throw new Error(
227
- `[Ecopages Client Reachability] Forbidden client export * from '${specifier}' at ${filename}:${node.start}.`
350
+ `[Ecopages Client Reachability] Forbidden client export * from '${specifier}' at ${filename}:${node.start}. This export is explicitly reachable from the React render function.`
228
351
  );
229
352
  } else {
230
353
  edits.push({ start: node.start, end: node.end, replacement: "" });
@@ -282,13 +405,26 @@ function transformModuleImports(source, filename, globallyAllowed) {
282
405
  }
283
406
  walkImports(program);
284
407
  if (edits.length === 0) {
285
- return { transformed: source, modified: false };
408
+ return stripServerOnlyEcoPageOptions(source, program);
286
409
  }
287
410
  edits.sort((a, b) => b.start - a.start);
288
411
  let transformed = source;
289
412
  for (const edit of edits) {
290
413
  transformed = transformed.slice(0, edit.start) + edit.replacement + transformed.slice(edit.end);
291
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
+ }
292
428
  return { transformed, modified: true };
293
429
  }
294
430
  function createClientGraphBoundaryPlugin(options) {
@@ -297,6 +433,7 @@ function createClientGraphBoundaryPlugin(options) {
297
433
  setup(build) {
298
434
  const absWorkingDir = options?.absWorkingDir ?? process.cwd();
299
435
  const globallyDeclaredSources = parseDeclaredModules(options?.declaredModules);
436
+ const requestedExports = /* @__PURE__ */ new Map();
300
437
  for (const alwaysAllow of options?.alwaysAllowSpecifiers ?? []) {
301
438
  globallyDeclaredSources.set(toModuleBaseSpecifier(alwaysAllow), "*");
302
439
  }
@@ -338,7 +475,8 @@ function createClientGraphBoundaryPlugin(options) {
338
475
  const { transformed: oxcTransformed, modified: importsModified } = transformModuleImports(
339
476
  transformed,
340
477
  args.path,
341
- globallyDeclaredSources
478
+ globallyDeclaredSources,
479
+ requestedExports
342
480
  );
343
481
  if (importsModified) {
344
482
  modified = true;
@@ -43,7 +43,18 @@ export type IslandHydrationScriptOptions = {
43
43
  };
44
44
  /**
45
45
  * Creates a hydration script for client-side React hydration.
46
- * Generates appropriate script based on environment and router configuration.
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
  */