@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.
- package/README.md +152 -29
- package/package.json +24 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +65 -43
- package/src/react-hmr-strategy.js +298 -145
- package/src/react-renderer.d.ts +169 -42
- package/src/react-renderer.js +484 -164
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +40 -111
- package/src/react.plugin.js +136 -61
- package/src/react.types.d.ts +88 -0
- package/src/react.types.js +0 -0
- package/src/router-adapter.d.ts +7 -14
- package/src/runtime/use-sync-external-store-with-selector.d.ts +3 -0
- package/src/runtime/use-sync-external-store-with-selector.js +56 -0
- package/src/services/react-bundle.service.d.ts +22 -35
- package/src/services/react-bundle.service.js +41 -105
- package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
- package/src/services/react-hmr-page-metadata-cache.js +18 -2
- package/src/services/react-hydration-asset.service.d.ts +28 -19
- package/src/services/react-hydration-asset.service.js +85 -66
- package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
- package/src/services/react-mdx-config-dependency.service.js +122 -0
- package/src/services/react-page-module.service.d.ts +10 -2
- package/src/services/react-page-module.service.js +47 -39
- package/src/services/react-page-payload.service.d.ts +46 -0
- package/src/services/react-page-payload.service.js +67 -0
- package/src/services/react-runtime-bundle.service.d.ts +20 -13
- package/src/services/react-runtime-bundle.service.js +146 -179
- package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
- package/src/utils/client-graph-boundary-plugin.js +80 -3
- package/src/utils/component-config-traversal.d.ts +36 -0
- package/src/utils/component-config-traversal.js +54 -0
- package/src/utils/declared-modules.d.ts +1 -1
- package/src/utils/declared-modules.js +7 -16
- package/src/utils/dynamic.test.browser.d.ts +1 -0
- package/src/utils/dynamic.test.browser.js +33 -0
- package/src/utils/hydration-scripts.d.ts +27 -6
- package/src/utils/hydration-scripts.js +177 -44
- package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
- package/src/utils/hydration-scripts.test.browser.js +198 -0
- package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
- package/src/utils/react-dom-runtime-interop-plugin.js +38 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/react-mdx-loader-plugin.js +13 -5
- package/src/utils/react-runtime-alias-map.d.ts +8 -0
- package/src/utils/react-runtime-alias-map.js +90 -0
- package/CHANGELOG.md +0 -67
- package/src/react-hmr-strategy.ts +0 -455
- package/src/react-renderer.ts +0 -403
- package/src/react.plugin.ts +0 -241
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -217
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -260
- package/src/services/react-page-module.service.ts +0 -214
- package/src/services/react-runtime-bundle.service.ts +0 -271
- package/src/utils/client-graph-boundary-plugin.ts +0 -710
- package/src/utils/client-only.ts +0 -27
- package/src/utils/declared-modules.ts +0 -99
- package/src/utils/dynamic.ts +0 -27
- package/src/utils/hmr-scripts.ts +0 -47
- package/src/utils/html-boundary.ts +0 -66
- package/src/utils/hydration-scripts.ts +0 -338
- package/src/utils/reachability-analyzer.ts +0 -593
- 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
|
|
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/
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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:
|
|
12
|
-
reactDomClient:
|
|
13
|
-
reactJsxRuntime:
|
|
14
|
-
reactJsxDevRuntime:
|
|
15
|
-
reactDom:
|
|
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.
|
|
79
|
+
runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
|
|
19
80
|
}
|
|
20
81
|
return runtimeImports;
|
|
21
82
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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 (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
return
|
|
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/
|
|
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
|
|
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>;
|