@ecopages/react 0.2.0-alpha.1 → 0.2.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -43
- package/README.md +143 -17
- package/package.json +3 -3
- package/src/react-hmr-strategy.d.ts +25 -21
- package/src/react-hmr-strategy.js +78 -110
- package/src/react-renderer.d.ts +135 -12
- package/src/react-renderer.js +439 -82
- package/src/react.plugin.d.ts +17 -5
- package/src/react.plugin.js +45 -13
- package/src/router-adapter.d.ts +2 -2
- package/src/services/react-bundle.service.d.ts +4 -25
- package/src/services/react-bundle.service.js +37 -91
- package/src/services/react-hydration-asset.service.js +3 -3
- package/src/services/react-page-module.service.d.ts +3 -0
- package/src/services/react-page-module.service.js +24 -17
- package/src/services/react-runtime-bundle.service.d.ts +12 -12
- package/src/services/react-runtime-bundle.service.js +98 -180
- package/src/utils/client-graph-boundary-plugin.js +149 -11
- package/src/utils/declared-modules.js +4 -1
- package/src/utils/foreign-jsx-override-plugin.d.ts +19 -0
- package/src/utils/foreign-jsx-override-plugin.js +43 -0
- package/src/utils/hydration-scripts.d.ts +18 -1
- package/src/utils/hydration-scripts.js +95 -37
- package/src/utils/reachability-analyzer.d.ts +12 -1
- package/src/utils/reachability-analyzer.js +101 -5
- 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.js +13 -5
- package/src/utils/react-runtime-specifier-map.d.ts +6 -0
- package/src/utils/react-runtime-specifier-map.js +37 -0
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
- package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
- package/src/react-hmr-strategy.ts +0 -444
- 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 -212
- 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 -590
- 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 -440
- package/src/utils/react-mdx-loader-plugin.ts +0 -40
|
@@ -1,205 +1,123 @@
|
|
|
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 { buildReactRuntimeSpecifierMap } from "../utils/react-runtime-specifier-map.js";
|
|
5
9
|
class ReactRuntimeBundleService {
|
|
10
|
+
config;
|
|
6
11
|
constructor(config) {
|
|
7
12
|
this.config = config;
|
|
8
13
|
}
|
|
9
|
-
|
|
14
|
+
get isDevelopment() {
|
|
15
|
+
return process.env.NODE_ENV === "development";
|
|
16
|
+
}
|
|
17
|
+
getCurrentRuntimeMode() {
|
|
18
|
+
return this.isDevelopment ? "development" : "production";
|
|
19
|
+
}
|
|
20
|
+
createRuntimeDefines(mode) {
|
|
21
|
+
const nodeEnv = JSON.stringify(mode);
|
|
22
|
+
return {
|
|
23
|
+
"process.env.NODE_ENV": nodeEnv,
|
|
24
|
+
"import.meta.env.NODE_ENV": nodeEnv
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
getReactVendorFileName(mode) {
|
|
28
|
+
return mode === "development" ? "react.development.js" : "react.js";
|
|
29
|
+
}
|
|
30
|
+
getReactDomVendorFileName(mode) {
|
|
31
|
+
return mode === "development" ? "react-dom.development.js" : "react-dom.js";
|
|
32
|
+
}
|
|
33
|
+
getRouterVendorFileName(mode) {
|
|
34
|
+
if (!this.config.routerAdapter) {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
return mode === "development" ? `${this.config.routerAdapter.bundle.outputName}.development.js` : `${this.config.routerAdapter.bundle.outputName}.js`;
|
|
38
|
+
}
|
|
39
|
+
getRuntimeImports(mode = this.getCurrentRuntimeMode()) {
|
|
40
|
+
const reactVendorFileName = this.getReactVendorFileName(mode);
|
|
41
|
+
const reactDomVendorFileName = this.getReactDomVendorFileName(mode);
|
|
10
42
|
const runtimeImports = {
|
|
11
|
-
react:
|
|
12
|
-
reactDomClient:
|
|
13
|
-
reactJsxRuntime:
|
|
14
|
-
reactJsxDevRuntime:
|
|
15
|
-
reactDom:
|
|
43
|
+
react: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
44
|
+
reactDomClient: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
|
|
45
|
+
reactJsxRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
46
|
+
reactJsxDevRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
47
|
+
reactDom: buildBrowserRuntimeAssetUrl(reactDomVendorFileName)
|
|
16
48
|
};
|
|
17
49
|
if (this.config.routerAdapter) {
|
|
18
|
-
runtimeImports.router = this.
|
|
50
|
+
runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
|
|
19
51
|
}
|
|
20
52
|
return runtimeImports;
|
|
21
53
|
}
|
|
22
|
-
getSpecifierMap() {
|
|
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;
|
|
54
|
+
getSpecifierMap(mode = this.getCurrentRuntimeMode()) {
|
|
55
|
+
return buildReactRuntimeSpecifierMap(this.getRuntimeImports(mode), this.config.routerAdapter);
|
|
35
56
|
}
|
|
36
57
|
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]
|
|
58
|
+
const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin();
|
|
59
|
+
const dependencies = [];
|
|
60
|
+
for (const mode of ["production", "development"]) {
|
|
61
|
+
const reactRuntimeAliasPlugin = createRuntimeSpecifierAliasPlugin(
|
|
62
|
+
{
|
|
63
|
+
react: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
|
|
72
64
|
},
|
|
73
|
-
|
|
74
|
-
})
|
|
75
|
-
];
|
|
76
|
-
if (this.config.routerAdapter) {
|
|
77
|
-
const runtimeAliasPlugin = this.createRuntimeAliasPlugin();
|
|
78
|
-
const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap()));
|
|
79
|
-
const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
|
|
80
|
-
(external) => !mappedSpecifiers.has(external)
|
|
65
|
+
{ name: `react-plugin-runtime-specifier-alias-${mode}` }
|
|
81
66
|
);
|
|
67
|
+
const reactDomBundlePlugins = [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin].filter(
|
|
68
|
+
(plugin) => plugin !== null
|
|
69
|
+
);
|
|
70
|
+
const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
|
|
71
|
+
const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap(mode)));
|
|
82
72
|
dependencies.push(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
createBrowserRuntimeModuleAsset({
|
|
74
|
+
modules: [
|
|
75
|
+
{ specifier: "react", defaultExport: true },
|
|
76
|
+
{ specifier: "react/jsx-runtime" },
|
|
77
|
+
{ specifier: "react/jsx-dev-runtime" }
|
|
78
|
+
],
|
|
79
|
+
name: "react",
|
|
80
|
+
fileName: this.getReactVendorFileName(mode),
|
|
81
|
+
cacheDirName: `ecopages-react-runtime-${mode}`,
|
|
88
82
|
bundleOptions: {
|
|
89
|
-
|
|
90
|
-
external: unresolvedExternals,
|
|
91
|
-
plugins: [runtimeAliasPlugin]
|
|
92
|
-
},
|
|
93
|
-
attributes: runtimeAttrs
|
|
94
|
-
})
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
return dependencies;
|
|
98
|
-
}
|
|
99
|
-
createRuntimeAliasPlugin() {
|
|
100
|
-
const specifierMap = this.getSpecifierMap();
|
|
101
|
-
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
102
|
-
const filter = new RegExp(
|
|
103
|
-
`^(${Object.keys(specifierMap).map((key) => escapeRegExp(key)).join("|")})$`
|
|
104
|
-
);
|
|
105
|
-
return {
|
|
106
|
-
name: "react-plugin-runtime-alias",
|
|
107
|
-
setup(build) {
|
|
108
|
-
build.onResolve({ filter }, (args) => {
|
|
109
|
-
const mappedPath = specifierMap[args.path];
|
|
110
|
-
if (!mappedPath) {
|
|
111
|
-
return void 0;
|
|
83
|
+
define: this.createRuntimeDefines(mode)
|
|
112
84
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return `/${AssetFactory.RESOLVED_ASSETS_VENDORS_DIR}/${fileName}`;
|
|
123
|
-
}
|
|
124
|
-
createRuntimeSpecifierAliasPlugin(specifierMap, external = true) {
|
|
125
|
-
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
126
|
-
const filter = new RegExp(
|
|
127
|
-
`^(${Object.keys(specifierMap).map((key) => escapeRegExp(key)).join("|")})$`
|
|
128
|
-
);
|
|
129
|
-
return {
|
|
130
|
-
name: "react-plugin-runtime-specifier-alias",
|
|
131
|
-
setup(build) {
|
|
132
|
-
build.onResolve({ filter }, (args) => {
|
|
133
|
-
const mappedPath = specifierMap[args.path];
|
|
134
|
-
if (!mappedPath) {
|
|
135
|
-
return void 0;
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
path: mappedPath,
|
|
139
|
-
external
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
createReactDomRuntimeInteropPlugin() {
|
|
146
|
-
const reactDomFileFilter = /[\\/]react-dom[\\/].*\.js$/;
|
|
147
|
-
const reactRequirePattern = /\brequire\((['"])react\1\)/g;
|
|
148
|
-
return {
|
|
149
|
-
name: "react-dom-runtime-interop",
|
|
150
|
-
setup(build) {
|
|
151
|
-
build.onLoad({ filter: reactDomFileFilter }, (args) => {
|
|
152
|
-
const content = fs.readFileSync(args.path, "utf-8");
|
|
153
|
-
if (!reactRequirePattern.test(content)) {
|
|
154
|
-
return void 0;
|
|
85
|
+
}),
|
|
86
|
+
createBrowserRuntimeModuleAsset({
|
|
87
|
+
modules: [{ specifier: "react-dom", defaultExport: true }, { specifier: "react-dom/client" }],
|
|
88
|
+
name: "react-dom",
|
|
89
|
+
fileName: this.getReactDomVendorFileName(mode),
|
|
90
|
+
cacheDirName: `ecopages-react-runtime-${mode}`,
|
|
91
|
+
bundleOptions: {
|
|
92
|
+
define: this.createRuntimeDefines(mode),
|
|
93
|
+
plugins: reactDomBundlePlugins
|
|
155
94
|
}
|
|
156
|
-
|
|
157
|
-
const rewritten = content.replace(reactRequirePattern, "__ecopages_react_runtime");
|
|
158
|
-
return {
|
|
159
|
-
contents: `import * as __ecopages_react_runtime from 'react';
|
|
160
|
-
${rewritten}`,
|
|
161
|
-
loader: "js",
|
|
162
|
-
resolveDir: path.dirname(args.path)
|
|
163
|
-
};
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
getRuntimeArtifactsDir() {
|
|
169
|
-
const tmpDir = path.join(process.cwd(), "node_modules", ".cache", "ecopages-react-runtime");
|
|
170
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
171
|
-
return tmpDir;
|
|
172
|
-
}
|
|
173
|
-
createRuntimeEntry(modules, fileName) {
|
|
174
|
-
const tmpDir = this.getRuntimeArtifactsDir();
|
|
175
|
-
const requireFromRoot = createRequire(path.join(process.cwd(), "package.json"));
|
|
176
|
-
const seenExports = /* @__PURE__ */ new Set();
|
|
177
|
-
const statements = [];
|
|
178
|
-
for (const module of modules) {
|
|
179
|
-
if (module.defaultExport) {
|
|
180
|
-
statements.push(`import __ecopages_default_export__ from '${module.specifier}';`);
|
|
181
|
-
statements.push("export default __ecopages_default_export__;");
|
|
182
|
-
}
|
|
183
|
-
const exportNames = this.getModuleExportNames(module.specifier, requireFromRoot).filter(
|
|
184
|
-
(name) => !seenExports.has(name)
|
|
95
|
+
})
|
|
185
96
|
);
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
97
|
+
if (this.config.routerAdapter) {
|
|
98
|
+
const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
|
|
99
|
+
(external) => !mappedSpecifiers.has(external)
|
|
100
|
+
);
|
|
101
|
+
dependencies.push(
|
|
102
|
+
createBrowserRuntimeScriptAsset({
|
|
103
|
+
importPath: this.config.routerAdapter.bundle.importPath,
|
|
104
|
+
name: this.config.routerAdapter.bundle.outputName,
|
|
105
|
+
fileName: this.getRouterVendorFileName(mode),
|
|
106
|
+
bundleOptions: {
|
|
107
|
+
define: this.createRuntimeDefines(mode),
|
|
108
|
+
external: unresolvedExternals,
|
|
109
|
+
plugins: [runtimeAliasPlugin]
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
);
|
|
191
113
|
}
|
|
192
114
|
}
|
|
193
|
-
|
|
194
|
-
fs.writeFileSync(filePath, statements.join("\n"), "utf-8");
|
|
195
|
-
return filePath;
|
|
196
|
-
}
|
|
197
|
-
getModuleExportNames(specifier, requireFromRoot) {
|
|
198
|
-
const moduleExports = requireFromRoot(specifier);
|
|
199
|
-
return Object.keys(moduleExports).filter((name) => name !== "__esModule" && name !== "default").filter((name) => this.isValidExportName(name)).sort();
|
|
115
|
+
return dependencies;
|
|
200
116
|
}
|
|
201
|
-
|
|
202
|
-
return
|
|
117
|
+
createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()) {
|
|
118
|
+
return createRuntimeSpecifierAliasPlugin(this.getSpecifierMap(mode), {
|
|
119
|
+
name: `react-plugin-runtime-alias-${mode}`
|
|
120
|
+
});
|
|
203
121
|
}
|
|
204
122
|
}
|
|
205
123
|
export {
|
|
@@ -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;
|
|
@@ -12,6 +20,10 @@ function isBareSpecifier(specifier) {
|
|
|
12
20
|
function isProjectAliasSpecifier(specifier) {
|
|
13
21
|
return specifier.startsWith("@/") || specifier.startsWith("~/") || specifier.startsWith("ecopages:");
|
|
14
22
|
}
|
|
23
|
+
function isServerOnlySpecifier(specifier) {
|
|
24
|
+
if (specifier.startsWith("node:")) return true;
|
|
25
|
+
return /(?:^|[/])[^/]+\.server(?:$|\.)/.test(specifier);
|
|
26
|
+
}
|
|
15
27
|
function toModuleBaseSpecifier(specifier) {
|
|
16
28
|
if (!isBareSpecifier(specifier) || specifier.startsWith("node:")) {
|
|
17
29
|
return specifier;
|
|
@@ -82,7 +94,93 @@ function parserLanguageForFile(filename) {
|
|
|
82
94
|
if (extension === ".jsx") return "jsx";
|
|
83
95
|
return "js";
|
|
84
96
|
}
|
|
85
|
-
function
|
|
97
|
+
function getObjectPropertyKeyName(node) {
|
|
98
|
+
if (!node) return void 0;
|
|
99
|
+
if (node.type === "Identifier") return node.name;
|
|
100
|
+
if (node.type === "StringLiteral" || node.type === "Literal") {
|
|
101
|
+
return typeof node.value === "string" ? node.value : void 0;
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
function stripServerOnlyEcoPageOptions(source, program) {
|
|
106
|
+
const edits = [];
|
|
107
|
+
function walk(node) {
|
|
108
|
+
if (!node || typeof node !== "object") return;
|
|
109
|
+
if (Array.isArray(node)) {
|
|
110
|
+
for (const child of node) walk(child);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "eco" && node.callee.property?.type === "Identifier" && node.callee.property.name === "page" && node.arguments?.[0]?.type === "ObjectExpression") {
|
|
114
|
+
const objectExpression = node.arguments[0];
|
|
115
|
+
const keptProperties = [];
|
|
116
|
+
let removedProperty = false;
|
|
117
|
+
for (const property of objectExpression.properties ?? []) {
|
|
118
|
+
if (property?.type === "Property") {
|
|
119
|
+
const keyName = getObjectPropertyKeyName(property.key);
|
|
120
|
+
if (keyName && SERVER_ONLY_ECO_PAGE_OPTION_KEYS.has(keyName)) {
|
|
121
|
+
removedProperty = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
keptProperties.push(source.slice(property.start, property.end));
|
|
126
|
+
}
|
|
127
|
+
if (removedProperty) {
|
|
128
|
+
const replacement = keptProperties.length > 0 ? `{ ${keptProperties.join(", ")} }` : "{}";
|
|
129
|
+
edits.push({
|
|
130
|
+
start: objectExpression.start,
|
|
131
|
+
end: objectExpression.end,
|
|
132
|
+
replacement
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const key in node) {
|
|
137
|
+
if (key !== "type" && key !== "start" && key !== "end") {
|
|
138
|
+
walk(node[key]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
walk(program);
|
|
143
|
+
if (edits.length === 0) {
|
|
144
|
+
return { transformed: source, modified: false };
|
|
145
|
+
}
|
|
146
|
+
edits.sort((a, b) => b.start - a.start);
|
|
147
|
+
let transformed = source;
|
|
148
|
+
for (const edit of edits) {
|
|
149
|
+
transformed = transformed.slice(0, edit.start) + edit.replacement + transformed.slice(edit.end);
|
|
150
|
+
}
|
|
151
|
+
return { transformed, modified: true };
|
|
152
|
+
}
|
|
153
|
+
function normalizeRequestedExportsKey(pathname) {
|
|
154
|
+
let normalized = pathname.replace(/\\/g, "/");
|
|
155
|
+
normalized = normalized.replace(/\.(tsx?|jsx?)$/i, "");
|
|
156
|
+
if (normalized.endsWith("/index")) {
|
|
157
|
+
normalized = normalized.slice(0, -"/index".length);
|
|
158
|
+
}
|
|
159
|
+
return normalized;
|
|
160
|
+
}
|
|
161
|
+
function resolveRequestedExportsKey(importer, specifier) {
|
|
162
|
+
if (isBareSpecifier(specifier) || isProjectAliasSpecifier(specifier)) {
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
const resolved = specifier.startsWith("/") ? specifier : resolve(dirname(importer), specifier);
|
|
166
|
+
return normalizeRequestedExportsKey(resolved);
|
|
167
|
+
}
|
|
168
|
+
function mergeRequestedExportRules(registry, moduleKey, rules) {
|
|
169
|
+
const existing = registry.get(moduleKey);
|
|
170
|
+
if (existing === "*") return;
|
|
171
|
+
if (rules === "*") {
|
|
172
|
+
registry.set(moduleKey, "*");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!existing) {
|
|
176
|
+
registry.set(moduleKey, new Set(rules));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
for (const rule of rules) {
|
|
180
|
+
existing.add(rule);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function transformModuleImports(source, filename, globallyAllowed, requestedExports) {
|
|
86
184
|
let result;
|
|
87
185
|
try {
|
|
88
186
|
result = parseSync(filename, source, {
|
|
@@ -116,12 +214,35 @@ function transformModuleImports(source, filename, globallyAllowed) {
|
|
|
116
214
|
walk(program);
|
|
117
215
|
const locallyAllowed = parseDeclaredModules(localDeclared);
|
|
118
216
|
const allowedMap = mergeDeclaredModulesMap(globallyAllowed, locallyAllowed);
|
|
119
|
-
const
|
|
217
|
+
const explicitRequestedExports = requestedExports.get(normalizeRequestedExportsKey(filename));
|
|
218
|
+
const reachability = analyzeReachability(source, filename, program, explicitRequestedExports);
|
|
219
|
+
for (const statement of program.body) {
|
|
220
|
+
if (statement.type === "ImportDeclaration") {
|
|
221
|
+
const reachableRules = reachability.reachableImports.get(statement.source.value);
|
|
222
|
+
const requestedModuleKey = resolveRequestedExportsKey(filename, statement.source.value);
|
|
223
|
+
if (!requestedModuleKey || !reachableRules) continue;
|
|
224
|
+
mergeRequestedExportRules(requestedExports, requestedModuleKey, reachableRules);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (statement.type === "ExportNamedDeclaration" && statement.source) {
|
|
228
|
+
const reachableRules = reachability.reachableImports.get(statement.source.value);
|
|
229
|
+
const requestedModuleKey = resolveRequestedExportsKey(filename, statement.source.value);
|
|
230
|
+
if (!requestedModuleKey || !reachableRules) continue;
|
|
231
|
+
mergeRequestedExportRules(requestedExports, requestedModuleKey, reachableRules);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (statement.type === "ExportAllDeclaration" && statement.source) {
|
|
235
|
+
const reachableRules = reachability.reachableImports.get(statement.source.value);
|
|
236
|
+
const requestedModuleKey = resolveRequestedExportsKey(filename, statement.source.value);
|
|
237
|
+
if (!requestedModuleKey || !reachableRules) continue;
|
|
238
|
+
mergeRequestedExportRules(requestedExports, requestedModuleKey, reachableRules);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
120
241
|
const edits = [];
|
|
121
242
|
function processSpecifier(specifier) {
|
|
122
243
|
const moduleBase = toModuleBaseSpecifier(specifier);
|
|
123
244
|
const explicitRules = allowedMap.get(moduleBase);
|
|
124
|
-
if (
|
|
245
|
+
if (isServerOnlySpecifier(specifier)) {
|
|
125
246
|
if (explicitRules) {
|
|
126
247
|
return { allowed: true, rules: explicitRules };
|
|
127
248
|
}
|
|
@@ -208,9 +329,10 @@ function transformModuleImports(source, filename, globallyAllowed) {
|
|
|
208
329
|
const specifier = node.source.value;
|
|
209
330
|
const { allowed } = processSpecifier(specifier);
|
|
210
331
|
if (!allowed) {
|
|
211
|
-
|
|
332
|
+
const reachableRules = reachability.reachableImports.get(specifier);
|
|
333
|
+
if (reachableRules && !reachability.isFallbackRoots) {
|
|
212
334
|
throw new Error(
|
|
213
|
-
`[Ecopages Client Reachability] Forbidden client export from '${specifier}' at ${filename}:${node.start}.`
|
|
335
|
+
`[Ecopages Client Reachability] Forbidden client export from '${specifier}' at ${filename}:${node.start}. This export is explicitly reachable from the React render function.`
|
|
214
336
|
);
|
|
215
337
|
} else {
|
|
216
338
|
edits.push({ start: node.start, end: node.end, replacement: "" });
|
|
@@ -222,9 +344,10 @@ function transformModuleImports(source, filename, globallyAllowed) {
|
|
|
222
344
|
const specifier = node.source.value;
|
|
223
345
|
const { allowed } = processSpecifier(specifier);
|
|
224
346
|
if (!allowed) {
|
|
225
|
-
|
|
347
|
+
const reachableRules = reachability.reachableImports.get(specifier);
|
|
348
|
+
if (reachableRules && !reachability.isFallbackRoots) {
|
|
226
349
|
throw new Error(
|
|
227
|
-
`[Ecopages Client Reachability] Forbidden client export * from '${specifier}' at ${filename}:${node.start}.`
|
|
350
|
+
`[Ecopages Client Reachability] Forbidden client export * from '${specifier}' at ${filename}:${node.start}. This export is explicitly reachable from the React render function.`
|
|
228
351
|
);
|
|
229
352
|
} else {
|
|
230
353
|
edits.push({ start: node.start, end: node.end, replacement: "" });
|
|
@@ -282,13 +405,26 @@ function transformModuleImports(source, filename, globallyAllowed) {
|
|
|
282
405
|
}
|
|
283
406
|
walkImports(program);
|
|
284
407
|
if (edits.length === 0) {
|
|
285
|
-
return
|
|
408
|
+
return stripServerOnlyEcoPageOptions(source, program);
|
|
286
409
|
}
|
|
287
410
|
edits.sort((a, b) => b.start - a.start);
|
|
288
411
|
let transformed = source;
|
|
289
412
|
for (const edit of edits) {
|
|
290
413
|
transformed = transformed.slice(0, edit.start) + edit.replacement + transformed.slice(edit.end);
|
|
291
414
|
}
|
|
415
|
+
let reparsedResult;
|
|
416
|
+
try {
|
|
417
|
+
reparsedResult = parseSync(filename, transformed, {
|
|
418
|
+
sourceType: "module",
|
|
419
|
+
lang: parserLanguageForFile(filename)
|
|
420
|
+
});
|
|
421
|
+
} catch {
|
|
422
|
+
return { transformed, modified: true };
|
|
423
|
+
}
|
|
424
|
+
const strippedPageOptions = stripServerOnlyEcoPageOptions(transformed, reparsedResult.program);
|
|
425
|
+
if (strippedPageOptions.modified) {
|
|
426
|
+
return strippedPageOptions;
|
|
427
|
+
}
|
|
292
428
|
return { transformed, modified: true };
|
|
293
429
|
}
|
|
294
430
|
function createClientGraphBoundaryPlugin(options) {
|
|
@@ -297,6 +433,7 @@ function createClientGraphBoundaryPlugin(options) {
|
|
|
297
433
|
setup(build) {
|
|
298
434
|
const absWorkingDir = options?.absWorkingDir ?? process.cwd();
|
|
299
435
|
const globallyDeclaredSources = parseDeclaredModules(options?.declaredModules);
|
|
436
|
+
const requestedExports = /* @__PURE__ */ new Map();
|
|
300
437
|
for (const alwaysAllow of options?.alwaysAllowSpecifiers ?? []) {
|
|
301
438
|
globallyDeclaredSources.set(toModuleBaseSpecifier(alwaysAllow), "*");
|
|
302
439
|
}
|
|
@@ -338,7 +475,8 @@ function createClientGraphBoundaryPlugin(options) {
|
|
|
338
475
|
const { transformed: oxcTransformed, modified: importsModified } = transformModuleImports(
|
|
339
476
|
transformed,
|
|
340
477
|
args.path,
|
|
341
|
-
globallyDeclaredSources
|
|
478
|
+
globallyDeclaredSources,
|
|
479
|
+
requestedExports
|
|
342
480
|
);
|
|
343
481
|
if (importsModified) {
|
|
344
482
|
modified = true;
|
|
@@ -346,7 +484,7 @@ function createClientGraphBoundaryPlugin(options) {
|
|
|
346
484
|
}
|
|
347
485
|
if (!modified) return void 0;
|
|
348
486
|
const ext = extname(args.path).slice(1);
|
|
349
|
-
return { contents: transformed, loader: ext };
|
|
487
|
+
return { contents: transformed, loader: ext, resolveDir: dirname(args.path) };
|
|
350
488
|
});
|
|
351
489
|
}
|
|
352
490
|
};
|
|
@@ -41,7 +41,10 @@ function collectPageDeclaredModulesFromModule(pageModule) {
|
|
|
41
41
|
}
|
|
42
42
|
async function collectPageDeclaredModules(pagePath) {
|
|
43
43
|
try {
|
|
44
|
-
const pageModule = await import(
|
|
44
|
+
const pageModule = await import(
|
|
45
|
+
/* @vite-ignore */
|
|
46
|
+
pagePath
|
|
47
|
+
);
|
|
45
48
|
return collectPageDeclaredModulesFromModule(pageModule);
|
|
46
49
|
} catch {
|
|
47
50
|
return [];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
2
|
+
interface ForeignJsxOverrideOptions {
|
|
3
|
+
jsxImportSource: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Esbuild plugin that overrides the JSX import source for non-host integration
|
|
8
|
+
* files (`.lit.tsx`, `.kita.tsx`, etc.) when bundled into a host client bundle.
|
|
9
|
+
*
|
|
10
|
+
* Without this plugin, non-host component files inherit the project-level
|
|
11
|
+
* `jsxImportSource` from tsconfig (typically `@kitajs/html`), which produces
|
|
12
|
+
* HTML strings from JSX. When the host framework calls those functions during
|
|
13
|
+
* hydration, it renders the string as a text node instead of a DOM element.
|
|
14
|
+
*
|
|
15
|
+
* This plugin prepends the host's `@jsxImportSource` pragma so esbuild compiles
|
|
16
|
+
* their JSX to the host framework's element creation calls.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createForeignJsxOverridePlugin(nonReactExtensions: string[], options: ForeignJsxOverrideOptions): EcoBuildPlugin;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
function createForeignJsxOverridePlugin(nonReactExtensions, options) {
|
|
3
|
+
const extensions = nonReactExtensions.filter((ext) => ext.endsWith(".tsx") || ext.endsWith(".jsx"));
|
|
4
|
+
if (extensions.length === 0) {
|
|
5
|
+
return {
|
|
6
|
+
name: options.name ?? "react-foreign-jsx-override",
|
|
7
|
+
setup() {
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function matchesNonReactExtension(id) {
|
|
12
|
+
for (const ext of extensions) {
|
|
13
|
+
if (id.endsWith(ext)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const pragma = `/** @jsxImportSource ${options.jsxImportSource} */
|
|
20
|
+
`;
|
|
21
|
+
const filter = new RegExp(`(${extensions.map((e) => e.replace(".", "\\.")).join("|")})$`);
|
|
22
|
+
return {
|
|
23
|
+
name: options.name ?? "react-foreign-jsx-override",
|
|
24
|
+
setup(build) {
|
|
25
|
+
build.onLoad({ filter }, (args) => {
|
|
26
|
+
if (!matchesNonReactExtension(args.path)) {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
const source = readFileSync(args.path, "utf-8");
|
|
30
|
+
if (source.includes("@jsxImportSource")) {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
contents: pragma + source,
|
|
35
|
+
loader: "tsx"
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
createForeignJsxOverridePlugin
|
|
43
|
+
};
|
|
@@ -43,7 +43,18 @@ export type IslandHydrationScriptOptions = {
|
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
45
|
* Creates a hydration script for client-side React hydration.
|
|
46
|
-
*
|
|
46
|
+
*
|
|
47
|
+
* Why this dispatcher exists:
|
|
48
|
+
* the runtime matrix is small but behaviorally different across development vs
|
|
49
|
+
* production and router vs non-router pages. Keeping that branch here preserves
|
|
50
|
+
* a compact public API while allowing each emitted script to stay focused.
|
|
51
|
+
*
|
|
52
|
+
* Selection rules:
|
|
53
|
+
* - development uses readable scripts with HMR hooks
|
|
54
|
+
* - production uses minified equivalents
|
|
55
|
+
* - router presence decides whether page updates flow through the router runtime
|
|
56
|
+
* or rebuild directly from the page module
|
|
57
|
+
*
|
|
47
58
|
* @param options - Configuration options for script generation
|
|
48
59
|
* @returns The generated hydration script as a string
|
|
49
60
|
*/
|
|
@@ -63,8 +74,14 @@ export declare function createHydrationScript(options: HydrationScriptOptions):
|
|
|
63
74
|
* - resolves the component export by metadata (`componentRef`, `componentFile`)
|
|
64
75
|
* before falling back to default/first function export
|
|
65
76
|
* - selects island root using `targetSelector`
|
|
77
|
+
* - replaces the SSR host with a dedicated client-owned container
|
|
66
78
|
* - creates a fresh React root and renders with serialized `props`
|
|
67
79
|
*
|
|
80
|
+
* Why it remounts instead of hydrating:
|
|
81
|
+
* island SSR intentionally avoids synthetic wrapper elements. The runtime swaps
|
|
82
|
+
* the authored SSR node for a dedicated client-owned container before mounting
|
|
83
|
+
* so the server markup stays clean while the client still gets a stable root.
|
|
84
|
+
*
|
|
68
85
|
* @param options Island script generation options.
|
|
69
86
|
* @returns Browser-executable JavaScript module source.
|
|
70
87
|
*/
|