@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 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.65",
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
- "@tsrx/ripple": "0.1.13",
36
- "@ripple-ts/adapter": "0.3.65"
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.65"
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(renderRoutes.map((/** @type {RenderRoute} */ r) => r.entry)),
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(/** @type {RenderRoute} */ (freshMatch.route), context, vite),
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(renderRoutes.map((/** @type {RenderRoute} */ r) => r.entry)),
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) return serverEntryCode;
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
- if (isBuild && renderRouteEntries.length > 0) {
1146
- // Production: generate static import map so Vite bundles page components
1147
- const importMapLines = renderRouteEntries
1148
- .map((entry) => ` ${JSON.stringify(entry)}: () => import(${JSON.stringify(entry)}),`)
1149
- .join('\n');
1150
-
1151
- // IMPORTANT: Use async IIFE instead of top-level await.
1152
- // The page modules statically import from the main bundle (which contains
1153
- // the runtime). If we used top-level await here, it would deadlock:
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)) {
@@ -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
- // We don't need to load the ripple plugin for now
215
- // but if we start using references to components in router.routes
216
- // then we'll need to add the plugin here to handle the .tsrx imports.
217
- // But this will cause a circular references warning
218
- // that we should resolve when we implement references to components.
219
- // plugins: [ripple({ excludeRippleExternalModules: true })],
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
+ }