@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.50
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 +16 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +60 -43
- package/src/react-hmr-strategy.js +297 -144
- 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 +38 -111
- package/src/react.plugin.js +132 -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/services/react-bundle.service.d.ts +19 -31
- package/src/services/react-bundle.service.js +51 -100
- 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 +15 -13
- package/src/services/react-runtime-bundle.service.js +103 -180
- 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 +29 -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 +6 -0
- package/src/utils/react-runtime-alias-map.js +33 -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/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
|
@@ -1,205 +1,128 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
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 { buildReactRuntimeAliasMap } from "../utils/react-runtime-alias-map.js";
|
|
5
9
|
class ReactRuntimeBundleService {
|
|
10
|
+
config;
|
|
6
11
|
constructor(config) {
|
|
7
12
|
this.config = config;
|
|
8
13
|
}
|
|
9
|
-
|
|
14
|
+
setRootDir(rootDir) {
|
|
15
|
+
this.config.rootDir = rootDir;
|
|
16
|
+
}
|
|
17
|
+
get isDevelopment() {
|
|
18
|
+
return process.env.NODE_ENV === "development";
|
|
19
|
+
}
|
|
20
|
+
getCurrentRuntimeMode() {
|
|
21
|
+
return this.isDevelopment ? "development" : "production";
|
|
22
|
+
}
|
|
23
|
+
createRuntimeDefines(mode) {
|
|
24
|
+
const nodeEnv = JSON.stringify(mode);
|
|
25
|
+
return {
|
|
26
|
+
"process.env.NODE_ENV": nodeEnv,
|
|
27
|
+
"import.meta.env.NODE_ENV": nodeEnv
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
getReactVendorFileName(mode) {
|
|
31
|
+
return mode === "development" ? "react.development.js" : "react.js";
|
|
32
|
+
}
|
|
33
|
+
getReactDomVendorFileName(mode) {
|
|
34
|
+
return mode === "development" ? "react-dom.development.js" : "react-dom.js";
|
|
35
|
+
}
|
|
36
|
+
getRouterVendorFileName(mode) {
|
|
37
|
+
if (!this.config.routerAdapter) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
return mode === "development" ? `${this.config.routerAdapter.bundle.outputName}.development.js` : `${this.config.routerAdapter.bundle.outputName}.js`;
|
|
41
|
+
}
|
|
42
|
+
getRuntimeImports(mode = this.getCurrentRuntimeMode()) {
|
|
43
|
+
const reactVendorFileName = this.getReactVendorFileName(mode);
|
|
44
|
+
const reactDomVendorFileName = this.getReactDomVendorFileName(mode);
|
|
10
45
|
const runtimeImports = {
|
|
11
|
-
react:
|
|
12
|
-
reactDomClient:
|
|
13
|
-
reactJsxRuntime:
|
|
14
|
-
reactJsxDevRuntime:
|
|
15
|
-
reactDom:
|
|
46
|
+
react: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
47
|
+
reactDomClient: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
|
|
48
|
+
reactJsxRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
49
|
+
reactJsxDevRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
50
|
+
reactDom: buildBrowserRuntimeAssetUrl(reactDomVendorFileName)
|
|
16
51
|
};
|
|
17
52
|
if (this.config.routerAdapter) {
|
|
18
|
-
runtimeImports.router = this.
|
|
53
|
+
runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
|
|
19
54
|
}
|
|
20
55
|
return runtimeImports;
|
|
21
56
|
}
|
|
22
|
-
|
|
23
|
-
|
|
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;
|
|
57
|
+
getRuntimeAliasMap(mode = this.getCurrentRuntimeMode()) {
|
|
58
|
+
return buildReactRuntimeAliasMap(this.getRuntimeImports(mode));
|
|
35
59
|
}
|
|
36
60
|
getDependencies() {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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]
|
|
61
|
+
const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin();
|
|
62
|
+
const dependencies = [];
|
|
63
|
+
for (const mode of ["production", "development"]) {
|
|
64
|
+
const reactRuntimeAliasPlugin = createRuntimeSpecifierAliasPlugin(
|
|
65
|
+
{
|
|
66
|
+
react: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
|
|
72
67
|
},
|
|
73
|
-
|
|
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)
|
|
68
|
+
{ name: `react-plugin-runtime-specifier-alias-${mode}` }
|
|
81
69
|
);
|
|
70
|
+
const reactDomBundlePlugins = [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin].filter(
|
|
71
|
+
(plugin) => plugin !== null
|
|
72
|
+
);
|
|
73
|
+
const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
|
|
74
|
+
const mappedSpecifiers = new Set(Object.keys(this.getRuntimeAliasMap(mode)));
|
|
82
75
|
dependencies.push(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
createBrowserRuntimeModuleAsset({
|
|
77
|
+
modules: [
|
|
78
|
+
{ specifier: "react", defaultExport: true },
|
|
79
|
+
{ specifier: "react/jsx-runtime" },
|
|
80
|
+
{ specifier: "react/jsx-dev-runtime" }
|
|
81
|
+
],
|
|
82
|
+
name: "react",
|
|
83
|
+
fileName: this.getReactVendorFileName(mode),
|
|
84
|
+
cacheDirName: `ecopages-react-runtime-${mode}`,
|
|
85
|
+
rootDir: this.config.rootDir,
|
|
88
86
|
bundleOptions: {
|
|
89
|
-
|
|
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;
|
|
87
|
+
define: this.createRuntimeDefines(mode)
|
|
112
88
|
}
|
|
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;
|
|
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;
|
|
89
|
+
}),
|
|
90
|
+
createBrowserRuntimeModuleAsset({
|
|
91
|
+
modules: [{ specifier: "react-dom", defaultExport: true }, { specifier: "react-dom/client" }],
|
|
92
|
+
name: "react-dom",
|
|
93
|
+
fileName: this.getReactDomVendorFileName(mode),
|
|
94
|
+
cacheDirName: `ecopages-react-runtime-${mode}`,
|
|
95
|
+
rootDir: this.config.rootDir,
|
|
96
|
+
bundleOptions: {
|
|
97
|
+
define: this.createRuntimeDefines(mode),
|
|
98
|
+
plugins: reactDomBundlePlugins
|
|
155
99
|
}
|
|
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)
|
|
100
|
+
})
|
|
185
101
|
);
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
102
|
+
if (this.config.routerAdapter) {
|
|
103
|
+
const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
|
|
104
|
+
(external) => !mappedSpecifiers.has(external)
|
|
105
|
+
);
|
|
106
|
+
dependencies.push(
|
|
107
|
+
createBrowserRuntimeScriptAsset({
|
|
108
|
+
importPath: this.config.routerAdapter.bundle.importPath,
|
|
109
|
+
name: this.config.routerAdapter.bundle.outputName,
|
|
110
|
+
fileName: this.getRouterVendorFileName(mode),
|
|
111
|
+
bundleOptions: {
|
|
112
|
+
define: this.createRuntimeDefines(mode),
|
|
113
|
+
external: unresolvedExternals,
|
|
114
|
+
plugins: [runtimeAliasPlugin]
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
);
|
|
191
118
|
}
|
|
192
119
|
}
|
|
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();
|
|
120
|
+
return dependencies;
|
|
200
121
|
}
|
|
201
|
-
|
|
202
|
-
return
|
|
122
|
+
createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()) {
|
|
123
|
+
return createRuntimeSpecifierAliasPlugin(this.getRuntimeAliasMap(mode), {
|
|
124
|
+
name: `react-plugin-runtime-alias-${mode}`
|
|
125
|
+
});
|
|
203
126
|
}
|
|
204
127
|
}
|
|
205
128
|
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>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function walkConfigTree(config, visitor, visited = /* @__PURE__ */ new Set()) {
|
|
2
|
+
if (!config || visited.has(config)) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
visited.add(config);
|
|
6
|
+
visitor(config);
|
|
7
|
+
if (config.layout?.config) {
|
|
8
|
+
walkConfigTree(config.layout.config, visitor, visited);
|
|
9
|
+
}
|
|
10
|
+
for (const component of config.dependencies?.components ?? []) {
|
|
11
|
+
walkConfigTree(component.config, visitor, visited);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function walkConfigForest(configs, visitor) {
|
|
15
|
+
const visited = /* @__PURE__ */ new Set();
|
|
16
|
+
for (const config of configs) {
|
|
17
|
+
walkConfigTree(config, visitor, visited);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function collectFromConfigTree(config, collector) {
|
|
21
|
+
const values = [];
|
|
22
|
+
walkConfigTree(config, (node) => {
|
|
23
|
+
values.push(...collector(node));
|
|
24
|
+
});
|
|
25
|
+
return values;
|
|
26
|
+
}
|
|
27
|
+
function collectFromConfigForest(configs, collector) {
|
|
28
|
+
const values = [];
|
|
29
|
+
walkConfigForest(configs, (node) => {
|
|
30
|
+
values.push(...collector(node));
|
|
31
|
+
});
|
|
32
|
+
return values;
|
|
33
|
+
}
|
|
34
|
+
function someInConfigTree(config, predicate) {
|
|
35
|
+
let matched = false;
|
|
36
|
+
walkConfigTree(config, (node) => {
|
|
37
|
+
if (matched) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
matched = predicate(node);
|
|
41
|
+
});
|
|
42
|
+
return matched;
|
|
43
|
+
}
|
|
44
|
+
function getComponentConfigs(components) {
|
|
45
|
+
return components.map((component) => component.config);
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
collectFromConfigForest,
|
|
49
|
+
collectFromConfigTree,
|
|
50
|
+
getComponentConfigs,
|
|
51
|
+
someInConfigTree,
|
|
52
|
+
walkConfigForest,
|
|
53
|
+
walkConfigTree
|
|
54
|
+
};
|
|
@@ -29,7 +29,7 @@ export declare function normalizeDeclaredModuleSources(modules?: string[]): stri
|
|
|
29
29
|
* Recursively walks a component config tree (including layouts and nested
|
|
30
30
|
* `dependencies.components`) to collect all declared module sources.
|
|
31
31
|
*/
|
|
32
|
-
export declare function collectDeclaredModulesInConfig(config: EcoComponentConfig | undefined
|
|
32
|
+
export declare function collectDeclaredModulesInConfig(config: EcoComponentConfig | undefined): string[];
|
|
33
33
|
/**
|
|
34
34
|
* Collects declared module sources from an already imported page module.
|
|
35
35
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFromConfigTree } from "./component-config-traversal.js";
|
|
1
2
|
function parseDeclaredModuleSource(value) {
|
|
2
3
|
const source = value.trim();
|
|
3
4
|
if (source.length === 0) return void 0;
|
|
@@ -16,21 +17,8 @@ function normalizeDeclaredModuleSources(modules) {
|
|
|
16
17
|
}
|
|
17
18
|
return Array.from(seen);
|
|
18
19
|
}
|
|
19
|
-
function collectDeclaredModulesInConfig(config
|
|
20
|
-
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
visited.add(config);
|
|
24
|
-
const declarations = normalizeDeclaredModuleSources(config.dependencies?.modules);
|
|
25
|
-
if (config.layout?.config) {
|
|
26
|
-
declarations.push(...collectDeclaredModulesInConfig(config.layout.config, visited));
|
|
27
|
-
}
|
|
28
|
-
for (const component of config.dependencies?.components ?? []) {
|
|
29
|
-
if (component.config) {
|
|
30
|
-
declarations.push(...collectDeclaredModulesInConfig(component.config, visited));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return declarations;
|
|
20
|
+
function collectDeclaredModulesInConfig(config) {
|
|
21
|
+
return collectFromConfigTree(config, (node) => normalizeDeclaredModuleSources(node.dependencies?.modules));
|
|
34
22
|
}
|
|
35
23
|
function collectPageDeclaredModulesFromModule(pageModule) {
|
|
36
24
|
const declarations = [
|
|
@@ -41,7 +29,10 @@ function collectPageDeclaredModulesFromModule(pageModule) {
|
|
|
41
29
|
}
|
|
42
30
|
async function collectPageDeclaredModules(pagePath) {
|
|
43
31
|
try {
|
|
44
|
-
const pageModule = await import(
|
|
32
|
+
const pageModule = await import(
|
|
33
|
+
/* @vite-ignore */
|
|
34
|
+
pagePath
|
|
35
|
+
);
|
|
45
36
|
return collectPageDeclaredModulesFromModule(pageModule);
|
|
46
37
|
} catch {
|
|
47
38
|
return [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
5
|
+
import { dynamic } from "./dynamic.js";
|
|
6
|
+
function createDeferredImport() {
|
|
7
|
+
let resolve;
|
|
8
|
+
const promise = new Promise((innerResolve) => {
|
|
9
|
+
resolve = innerResolve;
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
promise,
|
|
13
|
+
resolve
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe("dynamic", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
cleanup();
|
|
19
|
+
});
|
|
20
|
+
it("returns a browser lazy component that resolves through Suspense", async () => {
|
|
21
|
+
const deferredImport = createDeferredImport();
|
|
22
|
+
const DynamicComponent = dynamic(() => deferredImport.promise);
|
|
23
|
+
render(
|
|
24
|
+
/* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("span", { children: "Loading dynamic component" }), children: /* @__PURE__ */ jsx(DynamicComponent, {}) })
|
|
25
|
+
);
|
|
26
|
+
expect(screen.getByText("Loading dynamic component")).toBeTruthy();
|
|
27
|
+
deferredImport.resolve({
|
|
28
|
+
default: () => /* @__PURE__ */ jsx("span", { children: "Dynamic content" })
|
|
29
|
+
});
|
|
30
|
+
expect(await screen.findByText("Dynamic content")).toBeTruthy();
|
|
31
|
+
expect(screen.queryByText("Loading dynamic component")).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|