@ripple-ts/vite-plugin 0.3.65 → 0.3.66
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 +19 -0
- package/package.json +4 -4
- package/src/index.js +58 -149
- package/src/load-config.js +79 -6
- package/src/project-codegen.js +201 -0
- package/src/routes.js +55 -1
- package/src/server/production.js +23 -6
- package/src/server/render-route.js +30 -22
- package/src/server/virtual-entry.js +29 -7
- package/tests/project-codegen.test.js +42 -0
- package/tests/render-route-props.test.js +256 -0
- package/types/index.d.ts +15 -3
- package/types/production.d.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @ripple-ts/vite-plugin
|
|
2
2
|
|
|
3
|
+
## 0.3.66
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1168](https://github.com/Ripple-TS/ripple/pull/1168)
|
|
8
|
+
[`146cbf5`](https://github.com/Ripple-TS/ripple/commit/146cbf58120aad05161d503118a47bdc566ba869)
|
|
9
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Add global root pending/catch
|
|
10
|
+
boundary support and allow Ripple config routes to reference named entry
|
|
11
|
+
exports.
|
|
12
|
+
|
|
13
|
+
Refactor vite-plugin to keep code generation in one place, produce cache as
|
|
14
|
+
necessary and generate actual files for inspection.
|
|
15
|
+
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
[[`1dc0331`](https://github.com/Ripple-TS/ripple/commit/1dc0331f7b7296545ee459dc31a92057871cbb0d),
|
|
18
|
+
[`bf1cb96`](https://github.com/Ripple-TS/ripple/commit/bf1cb96f2ea9b325e30f5a051c451f92659d20f9)]:
|
|
19
|
+
- @tsrx/ripple@0.1.14
|
|
20
|
+
- @ripple-ts/adapter@0.3.66
|
|
21
|
+
|
|
3
22
|
## 0.3.65
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Vite plugin for Ripple",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.66",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/index.js",
|
|
9
9
|
"main": "src/index.js",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"url": "https://github.com/Ripple-TS/ripple/issues"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"@ripple
|
|
35
|
+
"@ripple-ts/adapter": "0.3.66",
|
|
36
|
+
"@tsrx/ripple": "0.1.14"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^24.3.0",
|
|
40
40
|
"type-fest": "^5.6.0",
|
|
41
41
|
"vite": "^8.0.12",
|
|
42
|
-
"ripple": "0.3.
|
|
42
|
+
"ripple": "0.3.66"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|
package/src/index.js
CHANGED
|
@@ -22,8 +22,20 @@ import {
|
|
|
22
22
|
rippleConfigExists,
|
|
23
23
|
} from './load-config.js';
|
|
24
24
|
import { ENTRY_FILENAME } from './constants.js';
|
|
25
|
+
import {
|
|
26
|
+
RESOLVED_ADAPTER_BROWSER_STUB_ID,
|
|
27
|
+
RESOLVED_VIRTUAL_COMPAT_ID,
|
|
28
|
+
SERVER_ONLY_ADAPTER_IDS,
|
|
29
|
+
VIRTUAL_COMPAT_ID,
|
|
30
|
+
create_adapter_browser_stub_source,
|
|
31
|
+
create_client_entry_source,
|
|
32
|
+
create_compat_virtual_module,
|
|
33
|
+
to_vite_root_import,
|
|
34
|
+
write_project_generated_file,
|
|
35
|
+
} from './project-codegen.js';
|
|
25
36
|
|
|
26
37
|
import { patch_global_fetch, is_rpc_request, handle_rpc_request } from '@ripple-ts/adapter/rpc';
|
|
38
|
+
import { get_route_entry_path } from './routes.js';
|
|
27
39
|
|
|
28
40
|
// Re-export route classes
|
|
29
41
|
export { RenderRoute, ServerRoute } from './routes.js';
|
|
@@ -38,8 +50,6 @@ const VITE_FS_PREFIX = '/@fs/';
|
|
|
38
50
|
const IS_WINDOWS = process.platform === 'win32';
|
|
39
51
|
const VIRTUAL_HYDRATE_ID = 'virtual:ripple-hydrate';
|
|
40
52
|
const RESOLVED_VIRTUAL_HYDRATE_ID = '\0virtual:ripple-hydrate';
|
|
41
|
-
const VIRTUAL_COMPAT_ID = 'virtual:ripple-compat';
|
|
42
|
-
const RESOLVED_VIRTUAL_COMPAT_ID = '\0virtual:ripple-compat';
|
|
43
53
|
const RIPPLE_EXTENSIONS = ['.tsrx'];
|
|
44
54
|
const RIPPLE_EXTENSION_PATTERN = /\.tsrx$/;
|
|
45
55
|
|
|
@@ -87,52 +97,6 @@ function getDevAsyncContext(config) {
|
|
|
87
97
|
return devAsyncContext;
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
/**
|
|
91
|
-
* @param {ResolvedRippleConfig | null} config
|
|
92
|
-
* @returns {string}
|
|
93
|
-
*/
|
|
94
|
-
function create_compat_virtual_module(config) {
|
|
95
|
-
const compat_entries = Object.entries(config?.compat ?? {});
|
|
96
|
-
|
|
97
|
-
if (compat_entries.length === 0) {
|
|
98
|
-
return `const compat = undefined;
|
|
99
|
-
globalThis.__RIPPLE_COMPAT__ = compat;
|
|
100
|
-
export { compat };
|
|
101
|
-
export default compat;
|
|
102
|
-
`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const imports = [];
|
|
106
|
-
const properties = [];
|
|
107
|
-
|
|
108
|
-
for (let i = 0; i < compat_entries.length; i++) {
|
|
109
|
-
const [kind, entry] = compat_entries[i];
|
|
110
|
-
const local_name = `__ripple_compat_factory_${i}`;
|
|
111
|
-
|
|
112
|
-
if (entry.factory) {
|
|
113
|
-
imports.push(
|
|
114
|
-
`import { ${entry.factory} as ${local_name} } from ${JSON.stringify(entry.from)};`,
|
|
115
|
-
);
|
|
116
|
-
} else {
|
|
117
|
-
imports.push(`import ${local_name} from ${JSON.stringify(entry.from)};`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
properties.push(` ${JSON.stringify(kind)}: ${local_name}(),`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return `${imports.join('\n')}
|
|
124
|
-
|
|
125
|
-
const compat = {
|
|
126
|
-
${properties.join('\n')}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
globalThis.__RIPPLE_COMPAT__ = compat;
|
|
130
|
-
|
|
131
|
-
export { compat };
|
|
132
|
-
export default compat;
|
|
133
|
-
`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
100
|
/**
|
|
137
101
|
* @param {ResolvedRippleConfig | null} config
|
|
138
102
|
* @returns {boolean}
|
|
@@ -464,7 +428,12 @@ export function ripple(inlineOptions = {}) {
|
|
|
464
428
|
(/** @type {Route} */ r) => r.type === 'render',
|
|
465
429
|
);
|
|
466
430
|
const uniqueEntries = [
|
|
467
|
-
...new Set(
|
|
431
|
+
...new Set(
|
|
432
|
+
renderRoutes
|
|
433
|
+
.map((/** @type {RenderRoute} */ r) => r.entry)
|
|
434
|
+
.map(get_route_entry_path)
|
|
435
|
+
.filter((entry) => typeof entry === 'string'),
|
|
436
|
+
),
|
|
468
437
|
];
|
|
469
438
|
for (const entry of uniqueEntries) {
|
|
470
439
|
const sourcePath = entry.startsWith('/') ? entry.slice(1) : entry;
|
|
@@ -563,7 +532,9 @@ export function ripple(inlineOptions = {}) {
|
|
|
563
532
|
|
|
564
533
|
renderRouteEntries = loadedRippleConfig.router.routes
|
|
565
534
|
.filter((/** @type {Route} */ r) => r.type === 'render')
|
|
566
|
-
.map((/** @type {RenderRoute} */ r) => r.entry)
|
|
535
|
+
.map((/** @type {RenderRoute} */ r) => r.entry)
|
|
536
|
+
.map(get_route_entry_path)
|
|
537
|
+
.filter((entry) => typeof entry === 'string');
|
|
567
538
|
|
|
568
539
|
// Deduplicate entries (multiple routes can share the same component)
|
|
569
540
|
renderRouteEntries = [...new Set(renderRouteEntries)];
|
|
@@ -761,7 +732,12 @@ export function ripple(inlineOptions = {}) {
|
|
|
761
732
|
globalMiddlewares,
|
|
762
733
|
freshMatch.route.before || [],
|
|
763
734
|
async () =>
|
|
764
|
-
handleRenderRoute(
|
|
735
|
+
handleRenderRoute(
|
|
736
|
+
/** @type {RenderRoute} */ (freshMatch.route),
|
|
737
|
+
context,
|
|
738
|
+
vite,
|
|
739
|
+
rippleConfig ?? undefined,
|
|
740
|
+
),
|
|
765
741
|
[],
|
|
766
742
|
);
|
|
767
743
|
} else {
|
|
@@ -956,7 +932,12 @@ export function ripple(inlineOptions = {}) {
|
|
|
956
932
|
(/** @type {Route} */ r) => r.type === 'render',
|
|
957
933
|
);
|
|
958
934
|
const uniqueEntries = [
|
|
959
|
-
...new Set(
|
|
935
|
+
...new Set(
|
|
936
|
+
renderRoutes
|
|
937
|
+
.map((/** @type {RenderRoute} */ r) => r.entry)
|
|
938
|
+
.map(get_route_entry_path)
|
|
939
|
+
.filter((entry) => typeof entry === 'string'),
|
|
940
|
+
),
|
|
960
941
|
];
|
|
961
942
|
|
|
962
943
|
for (const entry of uniqueEntries) {
|
|
@@ -1009,6 +990,11 @@ export function ripple(inlineOptions = {}) {
|
|
|
1009
990
|
rpcModulePaths: [...serverModuleModules],
|
|
1010
991
|
clientAssetMap,
|
|
1011
992
|
});
|
|
993
|
+
const serverEntryFile = write_project_generated_file(
|
|
994
|
+
config,
|
|
995
|
+
'server-entry.js',
|
|
996
|
+
serverEntryCode,
|
|
997
|
+
);
|
|
1012
998
|
|
|
1013
999
|
const VIRTUAL_SERVER_ENTRY_ID = 'virtual:ripple-server-entry';
|
|
1014
1000
|
const RESOLVED_VIRTUAL_SERVER_ENTRY_ID = '\0' + VIRTUAL_SERVER_ENTRY_ID;
|
|
@@ -1020,7 +1006,9 @@ export function ripple(inlineOptions = {}) {
|
|
|
1020
1006
|
if (id === VIRTUAL_SERVER_ENTRY_ID) return RESOLVED_VIRTUAL_SERVER_ENTRY_ID;
|
|
1021
1007
|
},
|
|
1022
1008
|
load(id) {
|
|
1023
|
-
if (id === RESOLVED_VIRTUAL_SERVER_ENTRY_ID)
|
|
1009
|
+
if (id === RESOLVED_VIRTUAL_SERVER_ENTRY_ID) {
|
|
1010
|
+
return fs.readFileSync(serverEntryFile, 'utf-8');
|
|
1011
|
+
}
|
|
1024
1012
|
},
|
|
1025
1013
|
};
|
|
1026
1014
|
|
|
@@ -1086,6 +1074,10 @@ export function ripple(inlineOptions = {}) {
|
|
|
1086
1074
|
},
|
|
1087
1075
|
|
|
1088
1076
|
async resolveId(id, importer, options) {
|
|
1077
|
+
if (!options?.ssr && SERVER_ONLY_ADAPTER_IDS.has(id)) {
|
|
1078
|
+
return RESOLVED_ADAPTER_BROWSER_STUB_ID;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1089
1081
|
// Handle virtual hydrate module
|
|
1090
1082
|
if (id === VIRTUAL_HYDRATE_ID) {
|
|
1091
1083
|
return RESOLVED_VIRTUAL_HYDRATE_ID;
|
|
@@ -1135,6 +1127,10 @@ export function ripple(inlineOptions = {}) {
|
|
|
1135
1127
|
},
|
|
1136
1128
|
|
|
1137
1129
|
async load(id, opts) {
|
|
1130
|
+
if (id === RESOLVED_ADAPTER_BROWSER_STUB_ID) {
|
|
1131
|
+
return create_adapter_browser_stub_source();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1138
1134
|
if (id === RESOLVED_VIRTUAL_COMPAT_ID) {
|
|
1139
1135
|
const compat_config = await get_current_ripple_config();
|
|
1140
1136
|
return create_compat_virtual_module(compat_config);
|
|
@@ -1142,102 +1138,15 @@ export function ripple(inlineOptions = {}) {
|
|
|
1142
1138
|
|
|
1143
1139
|
// Handle virtual hydrate module
|
|
1144
1140
|
if (id === RESOLVED_VIRTUAL_HYDRATE_ID) {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
// main bundle awaits page module import → page module awaits main bundle's
|
|
1155
|
-
// TLA to complete → circular wait.
|
|
1156
|
-
return `
|
|
1157
|
-
import ${JSON.stringify(VIRTUAL_COMPAT_ID)};
|
|
1158
|
-
import { hydrate, mount } from 'ripple';
|
|
1159
|
-
|
|
1160
|
-
const routeModules = {
|
|
1161
|
-
${importMapLines}
|
|
1162
|
-
};
|
|
1163
|
-
|
|
1164
|
-
(async () => {
|
|
1165
|
-
try {
|
|
1166
|
-
const data = JSON.parse(document.getElementById('__ripple_data').textContent);
|
|
1167
|
-
const target = document.getElementById('root');
|
|
1168
|
-
const loadModule = routeModules[data.entry];
|
|
1169
|
-
|
|
1170
|
-
if (!loadModule) {
|
|
1171
|
-
console.error('[ripple] No client module for route:', data.entry);
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
const module = await loadModule();
|
|
1176
|
-
const Component =
|
|
1177
|
-
module.default ||
|
|
1178
|
-
Object.entries(module).find(([key, value]) => typeof value === 'function' && /^[A-Z]/.test(key))?.[1];
|
|
1179
|
-
|
|
1180
|
-
if (!Component || !target) {
|
|
1181
|
-
console.error('[ripple] Unable to hydrate route: missing component export or #root target.');
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
try {
|
|
1186
|
-
hydrate(Component, {
|
|
1187
|
-
target,
|
|
1188
|
-
props: { params: data.params }
|
|
1189
|
-
});
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
console.warn('[ripple] Hydration failed, falling back to mount.', error);
|
|
1192
|
-
mount(Component, {
|
|
1193
|
-
target,
|
|
1194
|
-
props: { params: data.params }
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
} catch (error) {
|
|
1198
|
-
console.error('[ripple] Failed to bootstrap client hydration.', error);
|
|
1199
|
-
}
|
|
1200
|
-
})();
|
|
1201
|
-
`;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Dev mode: use async IIFE to avoid top-level await deadlock
|
|
1205
|
-
// (same reason as production — page modules import from the main bundle)
|
|
1206
|
-
return `
|
|
1207
|
-
import ${JSON.stringify(VIRTUAL_COMPAT_ID)};
|
|
1208
|
-
import { hydrate, mount } from 'ripple';
|
|
1209
|
-
|
|
1210
|
-
(async () => {
|
|
1211
|
-
try {
|
|
1212
|
-
const data = JSON.parse(document.getElementById('__ripple_data').textContent);
|
|
1213
|
-
const target = document.getElementById('root');
|
|
1214
|
-
const module = await import(/* @vite-ignore */ data.entry);
|
|
1215
|
-
const Component =
|
|
1216
|
-
module.default ||
|
|
1217
|
-
Object.entries(module).find(([key, value]) => typeof value === 'function' && /^[A-Z]/.test(key))?.[1];
|
|
1218
|
-
|
|
1219
|
-
if (!Component || !target) {
|
|
1220
|
-
console.error('[ripple] Unable to hydrate route: missing component export or #root target.');
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
try {
|
|
1225
|
-
hydrate(Component, {
|
|
1226
|
-
target,
|
|
1227
|
-
props: { params: data.params }
|
|
1228
|
-
});
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
console.warn('[ripple] Hydration failed, falling back to mount.', error);
|
|
1231
|
-
mount(Component, {
|
|
1232
|
-
target,
|
|
1233
|
-
props: { params: data.params }
|
|
1234
|
-
});
|
|
1235
|
-
}
|
|
1236
|
-
} catch (error) {
|
|
1237
|
-
console.error('[ripple] Failed to bootstrap client hydration.', error);
|
|
1238
|
-
}
|
|
1239
|
-
})();
|
|
1240
|
-
`;
|
|
1141
|
+
const file = write_project_generated_file(
|
|
1142
|
+
config,
|
|
1143
|
+
'client-entry.js',
|
|
1144
|
+
create_client_entry_source({
|
|
1145
|
+
configPath: to_vite_root_import(getRippleConfigPath(root), root),
|
|
1146
|
+
staticEntries: isBuild ? renderRouteEntries : [],
|
|
1147
|
+
}),
|
|
1148
|
+
);
|
|
1149
|
+
return fs.readFileSync(file, 'utf-8');
|
|
1241
1150
|
}
|
|
1242
1151
|
|
|
1243
1152
|
if (cssCache.has(id)) {
|
package/src/load-config.js
CHANGED
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import fs from 'node:fs';
|
|
22
|
+
import { compile } from '@tsrx/ripple';
|
|
22
23
|
import { DEFAULT_OUTDIR } from './constants.js';
|
|
23
24
|
|
|
25
|
+
const RIPPLE_EXTENSION_PATTERN = /\.tsrx$/;
|
|
26
|
+
|
|
24
27
|
/**
|
|
25
28
|
* @param {unknown} entry
|
|
26
29
|
* @returns {entry is CompatFactoryConfig}
|
|
@@ -76,6 +79,57 @@ function normalize_compat_entry(kind, entry) {
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/**
|
|
83
|
+
* @param {unknown} route
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*/
|
|
86
|
+
function validate_render_route(route) {
|
|
87
|
+
if (
|
|
88
|
+
!route ||
|
|
89
|
+
typeof route !== 'object' ||
|
|
90
|
+
/** @type {{ type?: unknown }} */ (route).type !== 'render'
|
|
91
|
+
) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const render_route = /** @type {{ entry?: unknown, layout?: unknown }} */ (route);
|
|
96
|
+
const has_entry =
|
|
97
|
+
typeof render_route.entry === 'string' ||
|
|
98
|
+
(Array.isArray(render_route.entry) &&
|
|
99
|
+
render_route.entry.length === 2 &&
|
|
100
|
+
typeof render_route.entry[0] === 'string' &&
|
|
101
|
+
typeof render_route.entry[1] === 'string');
|
|
102
|
+
|
|
103
|
+
if (!has_entry) {
|
|
104
|
+
throw new Error('[@ripple-ts/vite-plugin] RenderRoute requires a string/tuple `entry`.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (render_route.layout !== undefined && typeof render_route.layout !== 'string') {
|
|
108
|
+
throw new Error('[@ripple-ts/vite-plugin] RenderRoute `layout` must be a string path.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {unknown} rootBoundary
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
116
|
+
function validate_root_boundary(rootBoundary) {
|
|
117
|
+
if (rootBoundary === undefined) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!rootBoundary || typeof rootBoundary !== 'object') {
|
|
121
|
+
throw new Error('[@ripple-ts/vite-plugin] rootBoundary must be an object when provided.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const boundary = /** @type {{ pending?: unknown, catch?: unknown }} */ (rootBoundary);
|
|
125
|
+
if (boundary.pending !== undefined && typeof boundary.pending !== 'function') {
|
|
126
|
+
throw new Error('[@ripple-ts/vite-plugin] rootBoundary.pending must be a component function.');
|
|
127
|
+
}
|
|
128
|
+
if (boundary.catch !== undefined && typeof boundary.catch !== 'function') {
|
|
129
|
+
throw new Error('[@ripple-ts/vite-plugin] rootBoundary.catch must be a component function.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
79
133
|
/**
|
|
80
134
|
* Validate a raw ripple config and apply all defaults.
|
|
81
135
|
*
|
|
@@ -117,6 +171,16 @@ export function resolveRippleConfig(raw, options = {}) {
|
|
|
117
171
|
}
|
|
118
172
|
}
|
|
119
173
|
|
|
174
|
+
if (raw.router?.routes !== undefined && !Array.isArray(raw.router.routes)) {
|
|
175
|
+
throw new Error('[@ripple-ts/vite-plugin] router.routes must be an array.');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const route of raw.router?.routes ?? []) {
|
|
179
|
+
validate_render_route(route);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
validate_root_boundary(raw.rootBoundary);
|
|
183
|
+
|
|
120
184
|
// ------------------------------------------------------------------
|
|
121
185
|
// Apply defaults
|
|
122
186
|
// ------------------------------------------------------------------
|
|
@@ -130,6 +194,7 @@ export function resolveRippleConfig(raw, options = {}) {
|
|
|
130
194
|
router: {
|
|
131
195
|
routes: raw.router?.routes ?? [],
|
|
132
196
|
},
|
|
197
|
+
rootBoundary: raw.rootBoundary ?? {},
|
|
133
198
|
middlewares: raw.middlewares ?? [],
|
|
134
199
|
compat: Object.fromEntries(
|
|
135
200
|
Object.entries(raw.compat ?? {}).map(([kind, entry]) => [
|
|
@@ -211,12 +276,20 @@ export async function loadRippleConfig(projectRoot, options = {}) {
|
|
|
211
276
|
configFile: false,
|
|
212
277
|
appType: 'custom',
|
|
213
278
|
server: { middlewareMode: true },
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
279
|
+
plugins: [
|
|
280
|
+
{
|
|
281
|
+
name: 'ripple-config-tsrx-loader',
|
|
282
|
+
transform(source, id) {
|
|
283
|
+
if (!RIPPLE_EXTENSION_PATTERN.test(id)) return null;
|
|
284
|
+
const filename = id.replace(projectRoot, '');
|
|
285
|
+
return compile(source, filename, {
|
|
286
|
+
mode: 'server',
|
|
287
|
+
dev: true,
|
|
288
|
+
hmr: false,
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
],
|
|
220
293
|
logLevel: 'silent',
|
|
221
294
|
});
|
|
222
295
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/** @import { ResolvedConfig } from 'vite' */
|
|
2
|
+
/** @import { ResolvedRippleConfig } from '@ripple-ts/vite-plugin' */
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const VIRTUAL_COMPAT_ID = 'virtual:ripple-compat';
|
|
8
|
+
export const RESOLVED_VIRTUAL_COMPAT_ID = '\0virtual:ripple-compat';
|
|
9
|
+
export const RESOLVED_ADAPTER_BROWSER_STUB_ID = '\0ripple:adapter-browser-stub';
|
|
10
|
+
export const SERVER_ONLY_ADAPTER_IDS = new Set([
|
|
11
|
+
'@ripple-ts/adapter-node',
|
|
12
|
+
'@ripple-ts/adapter-bun',
|
|
13
|
+
'@ripple-ts/adapter-vercel',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/** @type {Map<string, string>} */
|
|
17
|
+
const generated_file_cache = new Map();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {ResolvedRippleConfig | null} config
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function create_compat_virtual_module(config) {
|
|
24
|
+
const compat_entries = Object.entries(config?.compat ?? {});
|
|
25
|
+
|
|
26
|
+
if (compat_entries.length === 0) {
|
|
27
|
+
return `const compat = undefined;
|
|
28
|
+
globalThis.__RIPPLE_COMPAT__ = compat;
|
|
29
|
+
export { compat };
|
|
30
|
+
export default compat;
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const imports = [];
|
|
35
|
+
const properties = [];
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < compat_entries.length; i++) {
|
|
38
|
+
const [kind, entry] = compat_entries[i];
|
|
39
|
+
const local_name = `__ripple_compat_factory_${i}`;
|
|
40
|
+
|
|
41
|
+
if (entry.factory) {
|
|
42
|
+
imports.push(
|
|
43
|
+
`import { ${entry.factory} as ${local_name} } from ${JSON.stringify(entry.from)};`,
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
imports.push(`import ${local_name} from ${JSON.stringify(entry.from)};`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
properties.push(` ${JSON.stringify(kind)}: ${local_name}(),`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return `${imports.join('\n')}
|
|
53
|
+
|
|
54
|
+
const compat = {
|
|
55
|
+
${properties.join('\n')}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
globalThis.__RIPPLE_COMPAT__ = compat;
|
|
59
|
+
|
|
60
|
+
export { compat };
|
|
61
|
+
export default compat;
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function create_adapter_browser_stub_source() {
|
|
69
|
+
return `export const runtime = undefined;
|
|
70
|
+
export function serve() {
|
|
71
|
+
throw new Error('[ripple] Server adapters cannot run in the browser.');
|
|
72
|
+
}
|
|
73
|
+
export function nodeRequestToWebRequest() {
|
|
74
|
+
throw new Error('[ripple] Node request helpers cannot run in the browser.');
|
|
75
|
+
}
|
|
76
|
+
export function webResponseToNodeResponse() {
|
|
77
|
+
throw new Error('[ripple] Node response helpers cannot run in the browser.');
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {ResolvedConfig} viteConfig
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function get_project_generated_dir(viteConfig) {
|
|
87
|
+
return path.join(viteConfig.cacheDir, 'project');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {ResolvedConfig} viteConfig
|
|
92
|
+
* @param {string} name
|
|
93
|
+
* @param {string} source
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
export function write_project_generated_file(viteConfig, name, source) {
|
|
97
|
+
const dir = get_project_generated_dir(viteConfig);
|
|
98
|
+
const file = path.join(dir, name);
|
|
99
|
+
|
|
100
|
+
if (generated_file_cache.get(file) === source && fs.existsSync(file)) {
|
|
101
|
+
return file;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
105
|
+
if (!fs.existsSync(file) || fs.readFileSync(file, 'utf-8') !== source) {
|
|
106
|
+
fs.writeFileSync(file, source);
|
|
107
|
+
}
|
|
108
|
+
generated_file_cache.set(file, source);
|
|
109
|
+
return file;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {{ configPath: string, staticEntries: string[] }} options
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
export function create_client_entry_source(options) {
|
|
117
|
+
const static_imports = options.staticEntries
|
|
118
|
+
.map((entry) => ` ${JSON.stringify(entry)}: () => import(${JSON.stringify(entry)}),`)
|
|
119
|
+
.join('\n');
|
|
120
|
+
|
|
121
|
+
return `// Auto-generated by @ripple-ts/vite-plugin.
|
|
122
|
+
// This file is written to Vite's cacheDir/project folder.
|
|
123
|
+
|
|
124
|
+
import ${JSON.stringify(VIRTUAL_COMPAT_ID)};
|
|
125
|
+
import { hydrate, mount } from 'ripple';
|
|
126
|
+
import rippleConfig from ${JSON.stringify(options.configPath)};
|
|
127
|
+
|
|
128
|
+
const routeModules = {
|
|
129
|
+
${static_imports}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const renderRoutes = (rippleConfig.router?.routes ?? []).filter((route) => route.type === 'render');
|
|
133
|
+
const rootBoundary = rippleConfig.rootBoundary;
|
|
134
|
+
|
|
135
|
+
function getRouteEntryPath(entry) {
|
|
136
|
+
return Array.isArray(entry) ? entry[1] : entry;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getRouteEntryExportName(entry) {
|
|
140
|
+
return Array.isArray(entry) ? entry[0] : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getComponentExport(module, exportName) {
|
|
144
|
+
if (exportName && typeof module[exportName] === 'function') return module[exportName];
|
|
145
|
+
if (typeof module.default === 'function') return module.default;
|
|
146
|
+
return Object.entries(module).find(([key, value]) => typeof value === 'function' && /^[A-Z]/.test(key))?.[1];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function resolveRouteComponent(data) {
|
|
150
|
+
const route =
|
|
151
|
+
typeof data.routeIndex === 'number' ? renderRoutes[data.routeIndex] : undefined;
|
|
152
|
+
|
|
153
|
+
const entry = route?.entry ?? data.entry;
|
|
154
|
+
const entryPath = getRouteEntryPath(entry);
|
|
155
|
+
const exportName = getRouteEntryExportName(entry);
|
|
156
|
+
if (!entryPath) return null;
|
|
157
|
+
|
|
158
|
+
const loadModule = routeModules[entryPath] ?? (() => import(/* @vite-ignore */ entryPath));
|
|
159
|
+
return getComponentExport(await loadModule(), exportName);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
(async () => {
|
|
163
|
+
try {
|
|
164
|
+
const data = JSON.parse(document.getElementById('__ripple_data').textContent);
|
|
165
|
+
const target = document.getElementById('root');
|
|
166
|
+
const Component = await resolveRouteComponent(data);
|
|
167
|
+
|
|
168
|
+
if (!Component || !target) {
|
|
169
|
+
console.error('[ripple] Unable to hydrate route: missing component export or #root target.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
hydrate(Component, {
|
|
175
|
+
target,
|
|
176
|
+
props: { params: data.params },
|
|
177
|
+
rootBoundary,
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.warn('[ripple] Hydration failed, falling back to mount.', error);
|
|
181
|
+
mount(Component, {
|
|
182
|
+
target,
|
|
183
|
+
props: { params: data.params },
|
|
184
|
+
rootBoundary,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('[ripple] Failed to bootstrap client hydration.', error);
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {string} filename
|
|
196
|
+
* @param {string} root
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
export function to_vite_root_import(filename, root) {
|
|
200
|
+
return '/' + path.relative(root, filename).split(path.sep).join('/');
|
|
201
|
+
}
|