@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.51

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 (68) hide show
  1. package/README.md +152 -29
  2. package/package.json +24 -12
  3. package/src/eco-embed.d.ts +11 -0
  4. package/src/eco-embed.js +11 -0
  5. package/src/react-hmr-strategy.d.ts +65 -43
  6. package/src/react-hmr-strategy.js +298 -145
  7. package/src/react-renderer.d.ts +169 -42
  8. package/src/react-renderer.js +484 -164
  9. package/src/react.constants.d.ts +1 -0
  10. package/src/react.constants.js +4 -0
  11. package/src/react.plugin.d.ts +40 -111
  12. package/src/react.plugin.js +136 -61
  13. package/src/react.types.d.ts +88 -0
  14. package/src/react.types.js +0 -0
  15. package/src/router-adapter.d.ts +7 -14
  16. package/src/runtime/use-sync-external-store-with-selector.d.ts +3 -0
  17. package/src/runtime/use-sync-external-store-with-selector.js +56 -0
  18. package/src/services/react-bundle.service.d.ts +22 -35
  19. package/src/services/react-bundle.service.js +41 -105
  20. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  21. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  22. package/src/services/react-hydration-asset.service.d.ts +28 -19
  23. package/src/services/react-hydration-asset.service.js +85 -66
  24. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  25. package/src/services/react-mdx-config-dependency.service.js +122 -0
  26. package/src/services/react-page-module.service.d.ts +10 -2
  27. package/src/services/react-page-module.service.js +47 -39
  28. package/src/services/react-page-payload.service.d.ts +46 -0
  29. package/src/services/react-page-payload.service.js +67 -0
  30. package/src/services/react-runtime-bundle.service.d.ts +20 -13
  31. package/src/services/react-runtime-bundle.service.js +146 -179
  32. package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
  33. package/src/utils/client-graph-boundary-plugin.js +80 -3
  34. package/src/utils/component-config-traversal.d.ts +36 -0
  35. package/src/utils/component-config-traversal.js +54 -0
  36. package/src/utils/declared-modules.d.ts +1 -1
  37. package/src/utils/declared-modules.js +7 -16
  38. package/src/utils/dynamic.test.browser.d.ts +1 -0
  39. package/src/utils/dynamic.test.browser.js +33 -0
  40. package/src/utils/hydration-scripts.d.ts +27 -6
  41. package/src/utils/hydration-scripts.js +177 -44
  42. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  43. package/src/utils/hydration-scripts.test.browser.js +198 -0
  44. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  45. package/src/utils/react-dom-runtime-interop-plugin.js +38 -0
  46. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  47. package/src/utils/react-mdx-loader-plugin.js +13 -5
  48. package/src/utils/react-runtime-alias-map.d.ts +8 -0
  49. package/src/utils/react-runtime-alias-map.js +90 -0
  50. package/CHANGELOG.md +0 -67
  51. package/src/react-hmr-strategy.ts +0 -455
  52. package/src/react-renderer.ts +0 -403
  53. package/src/react.plugin.ts +0 -241
  54. package/src/router-adapter.ts +0 -95
  55. package/src/services/react-bundle.service.ts +0 -217
  56. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  57. package/src/services/react-hydration-asset.service.ts +0 -260
  58. package/src/services/react-page-module.service.ts +0 -214
  59. package/src/services/react-runtime-bundle.service.ts +0 -271
  60. package/src/utils/client-graph-boundary-plugin.ts +0 -710
  61. package/src/utils/client-only.ts +0 -27
  62. package/src/utils/declared-modules.ts +0 -99
  63. package/src/utils/dynamic.ts +0 -27
  64. package/src/utils/hmr-scripts.ts +0 -47
  65. package/src/utils/html-boundary.ts +0 -66
  66. package/src/utils/hydration-scripts.ts +0 -338
  67. package/src/utils/reachability-analyzer.ts +0 -593
  68. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -0,0 +1,67 @@
1
+ import { LocalsAccessError } from "@ecopages/core/errors";
2
+ class ReactPagePayloadService {
3
+ /**
4
+ * Creates the canonical page-props payload used by router hydration.
5
+ *
6
+ * React pages embedded in a non-React HTML shell still need to expose the same
7
+ * page-data contract as fully React-owned documents so navigation and hydration
8
+ * can read one shared document payload consistently.
9
+ */
10
+ buildRouterPageDataScript(pageProps) {
11
+ const safeJson = JSON.stringify(pageProps || {}).replace(/</g, "\\u003c");
12
+ return `<script id="__ECO_PAGE_DATA__" type="application/json">${safeJson}<\/script>`;
13
+ }
14
+ /**
15
+ * Builds the serialized page-props payload embedded into the final HTML.
16
+ *
17
+ * The document payload is intentionally narrower than the full server render
18
+ * input: only routing data, public page props, and explicitly allowed locals are
19
+ * exposed to the browser.
20
+ */
21
+ buildSerializedPageProps(options) {
22
+ return {
23
+ ...options.pageProps,
24
+ params: options.params,
25
+ query: options.query,
26
+ ...options.safeLocals && { locals: options.safeLocals }
27
+ };
28
+ }
29
+ /**
30
+ * Safely extracts the declared subset of locals for client-side hydration.
31
+ *
32
+ * On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
33
+ * request-scoped data (e.g., session). Only keys explicitly declared via
34
+ * `Page.requires` are serialized to the client so sensitive request-only data
35
+ * is not leaked into hydration payloads by default.
36
+ *
37
+ * On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
38
+ * to prevent accidental use. This method safely detects that case and returns
39
+ * `undefined` instead of throwing.
40
+ */
41
+ getSerializableLocals(locals, requiredLocals) {
42
+ try {
43
+ if (!locals) {
44
+ return void 0;
45
+ }
46
+ const requiredKeys = requiredLocals ? Array.isArray(requiredLocals) ? requiredLocals : [requiredLocals] : [];
47
+ if (requiredKeys.length === 0) {
48
+ return void 0;
49
+ }
50
+ const serializedLocals = Object.fromEntries(
51
+ requiredKeys.filter((key) => Object.prototype.hasOwnProperty.call(locals, key)).map((key) => [key, locals[key]])
52
+ );
53
+ if (Object.keys(serializedLocals).length > 0) {
54
+ return serializedLocals;
55
+ }
56
+ return void 0;
57
+ } catch (error) {
58
+ if (error instanceof LocalsAccessError) {
59
+ return void 0;
60
+ }
61
+ throw error;
62
+ }
63
+ }
64
+ }
65
+ export {
66
+ ReactPagePayloadService
67
+ };
@@ -2,37 +2,44 @@
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
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
9
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
11
10
  import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
12
11
  import type { ReactRouterAdapter } from '../router-adapter.js';
12
+ import { type BrowserRuntimeManifest } from '@ecopages/core/build/browser-runtime-manifest';
13
13
  export type ReactRuntimeImports = {
14
14
  react: string;
15
15
  reactDomClient: string;
16
16
  reactJsxRuntime: string;
17
17
  reactJsxDevRuntime: string;
18
18
  reactDom: string;
19
+ useSyncExternalStoreWithSelector: string;
19
20
  router?: string;
20
21
  };
21
22
  export interface ReactRuntimeBundleServiceConfig {
22
23
  routerAdapter?: ReactRouterAdapter;
24
+ rootDir?: string;
23
25
  }
26
+ type RuntimeMode = 'development' | 'production';
24
27
  export declare class ReactRuntimeBundleService {
25
28
  private readonly config;
26
29
  constructor(config: ReactRuntimeBundleServiceConfig);
27
- getRuntimeImports(): ReactRuntimeImports;
28
- getSpecifierMap(): Record<string, string>;
30
+ setRootDir(rootDir: string | undefined): void;
31
+ private get isDevelopment();
32
+ private getCurrentRuntimeMode;
33
+ private createRuntimeDefines;
34
+ private getReactVendorFileName;
35
+ private getReactDomVendorFileName;
36
+ private getRouterVendorFileName;
37
+ private getUseSyncExternalStoreWithSelectorVendorFileName;
38
+ private createReactVendorImportRewritePlugin;
39
+ getRuntimeImports(mode?: RuntimeMode): ReactRuntimeImports;
40
+ getRuntimeAliasMap(mode?: RuntimeMode): Record<string, string>;
41
+ getRuntimeManifest(mode?: RuntimeMode): BrowserRuntimeManifest;
29
42
  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;
43
+ createRuntimeAliasPlugin(mode?: RuntimeMode): EcoBuildPlugin;
38
44
  }
45
+ export {};
@@ -1,205 +1,172 @@
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 {
2
+ createBrowserRuntimeImportRewritePlugin,
3
+ DEFAULT_BROWSER_RUNTIME_IMPORT_REWRITE_PLUGIN_NAME
4
+ } from "@ecopages/core/build/browser-runtime-import-rewrite-plugin";
5
+ import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
6
+ import {
7
+ buildBrowserRuntimeAssetUrl,
8
+ createBrowserRuntimeModuleAsset,
9
+ createBrowserRuntimeScriptAsset
10
+ } from "@ecopages/core/services/asset-processing-service";
11
+ import { createReactDomRuntimeInteropPlugin } from "../utils/react-dom-runtime-interop-plugin.js";
12
+ import { buildReactRuntimeAliasMap, buildReactRuntimeManifest } from "../utils/react-runtime-alias-map.js";
13
+ import {
14
+ createBrowserRuntimeManifest
15
+ } from "@ecopages/core/build/browser-runtime-manifest";
5
16
  class ReactRuntimeBundleService {
17
+ config;
6
18
  constructor(config) {
7
19
  this.config = config;
8
20
  }
9
- getRuntimeImports() {
21
+ setRootDir(rootDir) {
22
+ this.config.rootDir = rootDir;
23
+ }
24
+ get isDevelopment() {
25
+ return process.env.NODE_ENV === "development";
26
+ }
27
+ getCurrentRuntimeMode() {
28
+ return this.isDevelopment ? "development" : "production";
29
+ }
30
+ createRuntimeDefines(mode) {
31
+ const nodeEnv = JSON.stringify(mode);
32
+ return {
33
+ "process.env.NODE_ENV": nodeEnv,
34
+ "import.meta.env.NODE_ENV": nodeEnv
35
+ };
36
+ }
37
+ getReactVendorFileName(mode) {
38
+ return mode === "development" ? "react.development.js" : "react.js";
39
+ }
40
+ getReactDomVendorFileName(mode) {
41
+ return mode === "development" ? "react-dom.development.js" : "react-dom.js";
42
+ }
43
+ getRouterVendorFileName(mode) {
44
+ if (!this.config.routerAdapter) {
45
+ return "";
46
+ }
47
+ return mode === "development" ? `${this.config.routerAdapter.bundle.outputName}.development.js` : `${this.config.routerAdapter.bundle.outputName}.js`;
48
+ }
49
+ getUseSyncExternalStoreWithSelectorVendorFileName(mode) {
50
+ return mode === "development" ? "use-sync-external-store-with-selector.development.js" : "use-sync-external-store-with-selector.js";
51
+ }
52
+ createReactVendorImportRewritePlugin(mode) {
53
+ return createBrowserRuntimeImportRewritePlugin({
54
+ name: `react-plugin-vendor-runtime-import-rewrite-${mode}`,
55
+ manifest: createBrowserRuntimeManifest([
56
+ {
57
+ specifier: "react",
58
+ owner: "@ecopages/react",
59
+ importPath: "react",
60
+ publicPath: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
61
+ }
62
+ ])
63
+ });
64
+ }
65
+ getRuntimeImports(mode = this.getCurrentRuntimeMode()) {
66
+ const reactVendorFileName = this.getReactVendorFileName(mode);
67
+ const reactDomVendorFileName = this.getReactDomVendorFileName(mode);
10
68
  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")
69
+ react: buildBrowserRuntimeAssetUrl(reactVendorFileName),
70
+ reactDomClient: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
71
+ reactJsxRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
72
+ reactJsxDevRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
73
+ reactDom: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
74
+ useSyncExternalStoreWithSelector: buildBrowserRuntimeAssetUrl(
75
+ this.getUseSyncExternalStoreWithSelectorVendorFileName(mode)
76
+ )
16
77
  };
17
78
  if (this.config.routerAdapter) {
18
- runtimeImports.router = this.buildImportMapSourceUrl(`${this.config.routerAdapter.bundle.outputName}.js`);
79
+ runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
19
80
  }
20
81
  return runtimeImports;
21
82
  }
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;
83
+ getRuntimeAliasMap(mode = this.getCurrentRuntimeMode()) {
84
+ return buildReactRuntimeAliasMap(this.getRuntimeImports(mode));
85
+ }
86
+ getRuntimeManifest(mode = this.getCurrentRuntimeMode()) {
87
+ return buildReactRuntimeManifest(this.getRuntimeImports(mode));
35
88
  }
36
89
  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]
90
+ const dependencies = [];
91
+ for (const mode of ["production", "development"]) {
92
+ const reactVendorImportRewritePlugin = this.createReactVendorImportRewritePlugin(mode);
93
+ const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin({
94
+ reactSpecifier: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
95
+ });
96
+ const reactRuntimeAliasPlugin = createRuntimeSpecifierAliasPlugin(
97
+ {
98
+ react: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
72
99
  },
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)
100
+ { name: `react-plugin-runtime-specifier-alias-${mode}` }
101
+ );
102
+ const reactDomBundlePlugins = [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin].filter(
103
+ (plugin) => plugin !== null
81
104
  );
105
+ const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
106
+ const mappedSpecifiers = new Set(Object.keys(this.getRuntimeAliasMap(mode)));
82
107
  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,
108
+ createBrowserRuntimeModuleAsset({
109
+ modules: [
110
+ { specifier: "react", defaultExport: true },
111
+ { specifier: "react/jsx-runtime" },
112
+ { specifier: "react/jsx-dev-runtime" }
113
+ ],
114
+ name: "react",
115
+ fileName: this.getReactVendorFileName(mode),
116
+ cacheDirName: `ecopages-react-runtime-${mode}`,
117
+ rootDir: this.config.rootDir,
88
118
  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;
119
+ define: this.createRuntimeDefines(mode),
120
+ excludeAppBuildPlugins: [DEFAULT_BROWSER_RUNTIME_IMPORT_REWRITE_PLUGIN_NAME]
112
121
  }
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;
122
+ }),
123
+ createBrowserRuntimeModuleAsset({
124
+ modules: [{ specifier: "react-dom", defaultExport: true }, { specifier: "react-dom/client" }],
125
+ name: "react-dom",
126
+ fileName: this.getReactDomVendorFileName(mode),
127
+ cacheDirName: `ecopages-react-runtime-${mode}`,
128
+ rootDir: this.config.rootDir,
129
+ bundleOptions: {
130
+ define: this.createRuntimeDefines(mode),
131
+ excludeAppBuildPlugins: [DEFAULT_BROWSER_RUNTIME_IMPORT_REWRITE_PLUGIN_NAME],
132
+ plugins: reactDomBundlePlugins
136
133
  }
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;
134
+ }),
135
+ createBrowserRuntimeScriptAsset({
136
+ importPath: "@ecopages/react/runtime/use-sync-external-store-with-selector",
137
+ name: "use-sync-external-store-with-selector",
138
+ fileName: this.getUseSyncExternalStoreWithSelectorVendorFileName(mode),
139
+ bundleOptions: {
140
+ define: this.createRuntimeDefines(mode),
141
+ excludeAppBuildPlugins: [DEFAULT_BROWSER_RUNTIME_IMPORT_REWRITE_PLUGIN_NAME],
142
+ plugins: [reactVendorImportRewritePlugin]
155
143
  }
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)
144
+ })
185
145
  );
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
- }
146
+ if (this.config.routerAdapter) {
147
+ const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
148
+ (external) => !mappedSpecifiers.has(external)
149
+ );
150
+ dependencies.push(
151
+ createBrowserRuntimeScriptAsset({
152
+ importPath: this.config.routerAdapter.bundle.importPath,
153
+ name: this.config.routerAdapter.bundle.outputName,
154
+ fileName: this.getRouterVendorFileName(mode),
155
+ bundleOptions: {
156
+ define: this.createRuntimeDefines(mode),
157
+ external: unresolvedExternals,
158
+ plugins: [runtimeAliasPlugin]
159
+ }
160
+ })
161
+ );
191
162
  }
192
163
  }
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();
164
+ return dependencies;
200
165
  }
201
- isValidExportName(name) {
202
- return /^[$A-Z_a-z][$\w]*$/.test(name);
166
+ createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()) {
167
+ return createRuntimeSpecifierAliasPlugin(this.getRuntimeAliasMap(mode), {
168
+ name: `react-plugin-runtime-alias-${mode}`
169
+ });
203
170
  }
204
171
  }
205
172
  export {
@@ -14,7 +14,7 @@
14
14
  * Additionally, this plugin provides a build-time transform that statically resolves and
15
15
  * inlines `fs.readFileSync(path.resolve(...))` calls to prevent server/client data mismatches.
16
16
  */
17
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
17
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
18
18
  /**
19
19
  * Configuration options for the Client Graph Boundary esbuild plugin.
20
20
  *
@@ -1,8 +1,16 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, extname, resolve } from "node:path";
3
3
  import { parseSync } from "oxc-parser";
4
- import { analyzeReachability } from "./reachability-analyzer";
4
+ import { analyzeReachability } from "./reachability-analyzer.js";
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 { transformed: source, modified: false };
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) {
@@ -407,7 +484,7 @@ function createClientGraphBoundaryPlugin(options) {
407
484
  }
408
485
  if (!modified) return void 0;
409
486
  const ext = extname(args.path).slice(1);
410
- return { contents: transformed, loader: ext };
487
+ return { contents: transformed, loader: ext, resolveDir: dirname(args.path) };
411
488
  });
412
489
  }
413
490
  };
@@ -0,0 +1,36 @@
1
+ import type { EcoComponent, EcoComponentConfig } from '@ecopages/core';
2
+ /**
3
+ * Walks a component config tree once, including nested layout configs and
4
+ * dependency component configs.
5
+ *
6
+ * The shared React integration code performs several different analyses over the
7
+ * same config graph. Centralizing the traversal keeps cycle handling and graph
8
+ * shape assumptions in one place instead of repeating them in the renderer and
9
+ * services.
10
+ */
11
+ export declare function walkConfigTree(config: EcoComponentConfig | undefined, visitor: (config: EcoComponentConfig) => void, visited?: Set<EcoComponentConfig>): void;
12
+ /**
13
+ * Walks a forest of root component configs using one shared visited set.
14
+ *
15
+ * This is useful when a page contributes multiple config roots, such as a page
16
+ * config plus a resolved layout config, and duplicate nested nodes should still
17
+ * be processed only once.
18
+ */
19
+ export declare function walkConfigForest(configs: Iterable<EcoComponentConfig | undefined>, visitor: (config: EcoComponentConfig) => void): void;
20
+ /**
21
+ * Collects values from a config tree while preserving the shared traversal and
22
+ * cycle protection behavior used across the React integration.
23
+ */
24
+ export declare function collectFromConfigTree<T>(config: EcoComponentConfig | undefined, collector: (config: EcoComponentConfig) => T[]): T[];
25
+ /**
26
+ * Collects values from multiple config roots with one shared visited set.
27
+ */
28
+ export declare function collectFromConfigForest<T>(configs: Iterable<EcoComponentConfig | undefined>, collector: (config: EcoComponentConfig) => T[]): T[];
29
+ /**
30
+ * Returns true when any node in the config tree matches the predicate.
31
+ */
32
+ export declare function someInConfigTree(config: EcoComponentConfig | undefined, predicate: (config: EcoComponentConfig) => boolean): boolean;
33
+ /**
34
+ * Reads config roots from partial components while tolerating undefined config.
35
+ */
36
+ export declare function getComponentConfigs(components: Partial<EcoComponent>[]): Array<EcoComponentConfig | undefined>;