@ripple-ts/vite-plugin 0.2.213 → 0.2.215
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 +14 -3
- package/src/bin/preview.js +43 -0
- package/src/constants.js +2 -0
- package/src/index.js +542 -193
- package/src/load-config.js +172 -0
- package/src/server/production.js +127 -94
- package/src/server/render-route.js +1 -1
- package/src/server/virtual-entry.js +215 -0
- package/tsconfig.json +8 -0
- package/types/index.d.ts +117 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility for loading and resolving ripple.config.ts.
|
|
3
|
+
*
|
|
4
|
+
* `resolveRippleConfig` is the single source of truth for all config
|
|
5
|
+
* validation and default values. Every consumer should receive a
|
|
6
|
+
* `ResolvedRippleConfig` rather than applying ad-hoc defaults.
|
|
7
|
+
*
|
|
8
|
+
* `loadRippleConfig` is the single entry point for loading the config
|
|
9
|
+
* file. It accepts an optional Vite dev server — when provided the
|
|
10
|
+
* config is loaded via `ssrLoadModule` (no temp server overhead,
|
|
11
|
+
* HMR-aware). Otherwise a temporary Vite server is spun up, used to
|
|
12
|
+
* transpile the TypeScript config, and immediately shut down.
|
|
13
|
+
*
|
|
14
|
+
* Used by the Vite plugin (during dev + build), the preview CLI script,
|
|
15
|
+
* and the generated production server entry.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @import { RippleConfigOptions, ResolvedRippleConfig } from '@ripple-ts/vite-plugin' */
|
|
19
|
+
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import { DEFAULT_OUTDIR } from './constants.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a raw ripple config and apply all defaults.
|
|
26
|
+
*
|
|
27
|
+
* After this function returns every optional field carries its default
|
|
28
|
+
* value so callers never need to use `??` / `||` fallbacks.
|
|
29
|
+
*
|
|
30
|
+
* The function is idempotent — passing an already-resolved config
|
|
31
|
+
* through it again is safe and produces the same result.
|
|
32
|
+
*
|
|
33
|
+
* @param {RippleConfigOptions} raw - The user-provided config (from ripple.config.ts)
|
|
34
|
+
* @param {{ requireAdapter?: boolean }} [options]
|
|
35
|
+
* @returns {ResolvedRippleConfig}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveRippleConfig(raw, options = {}) {
|
|
38
|
+
const { requireAdapter = false } = options;
|
|
39
|
+
|
|
40
|
+
// ------------------------------------------------------------------
|
|
41
|
+
// Validate
|
|
42
|
+
// ------------------------------------------------------------------
|
|
43
|
+
if (!raw) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'[@ripple-ts/vite-plugin] ripple.config.ts must export a default config object.',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!raw.router?.routes) {
|
|
50
|
+
throw new Error('[@ripple-ts/vite-plugin] ripple.config.ts must define `router.routes`.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (requireAdapter) {
|
|
54
|
+
if (!raw.adapter) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'[@ripple-ts/vite-plugin] Production builds require an `adapter` in ripple.config.ts. ' +
|
|
57
|
+
'Install an adapter package (e.g. @ripple-ts/adapter-node) and set the `adapter` property.',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!raw.adapter.runtime) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'[@ripple-ts/vite-plugin] The adapter in ripple.config.ts is missing the `runtime` property. ' +
|
|
64
|
+
'Make sure your adapter exports runtime primitives.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ------------------------------------------------------------------
|
|
70
|
+
// Apply defaults
|
|
71
|
+
// ------------------------------------------------------------------
|
|
72
|
+
return {
|
|
73
|
+
build: {
|
|
74
|
+
outDir: raw.build?.outDir ?? DEFAULT_OUTDIR,
|
|
75
|
+
minify: raw.build?.minify,
|
|
76
|
+
target: raw.build?.target,
|
|
77
|
+
},
|
|
78
|
+
adapter: raw.adapter,
|
|
79
|
+
router: {
|
|
80
|
+
routes: raw.router.routes,
|
|
81
|
+
},
|
|
82
|
+
middlewares: raw.middlewares ?? [],
|
|
83
|
+
platform: {
|
|
84
|
+
env: raw.platform?.env ?? {},
|
|
85
|
+
},
|
|
86
|
+
server: {
|
|
87
|
+
trustProxy: raw.server?.trustProxy ?? false,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return the absolute path to ripple.config.ts for the given project root.
|
|
94
|
+
*
|
|
95
|
+
* This is the single source of truth for the config file name / location.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
export function getRippleConfigPath(projectRoot) {
|
|
101
|
+
return path.join(projectRoot, 'ripple.config.ts');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check whether a ripple.config.ts file exists in the given root.
|
|
106
|
+
*
|
|
107
|
+
* Use this before calling `loadRippleConfig` when the absence of a
|
|
108
|
+
* config is a valid state (e.g. the Vite plugin running in SPA mode).
|
|
109
|
+
*
|
|
110
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function rippleConfigExists(projectRoot) {
|
|
114
|
+
return fs.existsSync(getRippleConfigPath(projectRoot));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load ripple.config.ts, validate, and apply defaults via `resolveRippleConfig`.
|
|
119
|
+
*
|
|
120
|
+
* When a Vite dev server is provided via `options.vite`, the config is loaded
|
|
121
|
+
* through its `ssrLoadModule` — avoiding the cost of spinning up a temporary
|
|
122
|
+
* server and enabling HMR-aware reloads.
|
|
123
|
+
*
|
|
124
|
+
* When no dev server is available (build / preview), a temporary Vite server
|
|
125
|
+
* is created in middleware mode, used to transpile the config, then shut down.
|
|
126
|
+
*
|
|
127
|
+
* Throws if the config file does not exist or is invalid.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
130
|
+
* @param {{ vite?: import('vite').ViteDevServer, requireAdapter?: boolean }} [options]
|
|
131
|
+
* @returns {Promise<ResolvedRippleConfig>}
|
|
132
|
+
*/
|
|
133
|
+
export async function loadRippleConfig(projectRoot, options = {}) {
|
|
134
|
+
const { vite, requireAdapter } = options;
|
|
135
|
+
const configPath = getRippleConfigPath(projectRoot);
|
|
136
|
+
|
|
137
|
+
if (!fs.existsSync(configPath)) {
|
|
138
|
+
throw new Error(`[@ripple-ts/vite-plugin] ripple.config.ts not found in ${projectRoot}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// When a running Vite dev server is available, use it directly.
|
|
142
|
+
if (vite) {
|
|
143
|
+
const configModule = await vite.ssrLoadModule(configPath);
|
|
144
|
+
return resolveRippleConfig(configModule.default, { requireAdapter });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Otherwise spin up a temporary Vite server (build / preview).
|
|
148
|
+
// The temp server only transpiles ripple.config.ts (plain TypeScript) —
|
|
149
|
+
// no .ripple compilation plugin is needed.
|
|
150
|
+
const { createServer } = await import('vite');
|
|
151
|
+
|
|
152
|
+
const tempVite = await createServer({
|
|
153
|
+
root: projectRoot,
|
|
154
|
+
configFile: false,
|
|
155
|
+
appType: 'custom',
|
|
156
|
+
server: { middlewareMode: true },
|
|
157
|
+
// We don't need to load the ripple plugin for now
|
|
158
|
+
// but if we start using references to components in router.routes
|
|
159
|
+
// then we'll need to add the plugin here to handle the .ripple imports.
|
|
160
|
+
// But this will cause a circular references warning
|
|
161
|
+
// that we should resolve when we implement references to components.
|
|
162
|
+
// plugins: [ripple({ excludeRippleExternalModules: true })],
|
|
163
|
+
logLevel: 'silent',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const configModule = await tempVite.ssrLoadModule(configPath);
|
|
168
|
+
return resolveRippleConfig(configModule.default, { requireAdapter });
|
|
169
|
+
} finally {
|
|
170
|
+
await tempVite.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/server/production.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Production server runtime for Ripple metaframework
|
|
3
|
-
* This module is used in production builds to handle SSR + API routes
|
|
2
|
+
* Production server runtime for Ripple metaframework.
|
|
3
|
+
* This module is used in production builds to handle SSR + API routes + RPC.
|
|
4
|
+
*
|
|
5
|
+
* It is designed to be imported by the generated server entry and does NOT
|
|
6
|
+
* depend on Vite at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Platform-agnostic — no Node.js-specific imports. Platform capabilities
|
|
9
|
+
* (hashing, async context) are provided via `manifest.runtime` from the adapter.
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
import { createRouter } from './router.js';
|
|
7
13
|
import { createContext, runMiddlewareChain } from './middleware.js';
|
|
14
|
+
import {
|
|
15
|
+
patch_global_fetch,
|
|
16
|
+
build_rpc_lookup,
|
|
17
|
+
is_rpc_request,
|
|
18
|
+
handle_rpc_request,
|
|
19
|
+
} from '@ripple-ts/adapter/rpc';
|
|
20
|
+
|
|
21
|
+
export { resolveRippleConfig } from '../load-config.js';
|
|
8
22
|
|
|
9
23
|
/**
|
|
10
24
|
* @typedef {import('@ripple-ts/vite-plugin').Route} Route
|
|
@@ -14,39 +28,62 @@ import { createContext, runMiddlewareChain } from './middleware.js';
|
|
|
14
28
|
*/
|
|
15
29
|
|
|
16
30
|
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @typedef {Object} RenderResult
|
|
26
|
-
* @property {string} head
|
|
27
|
-
* @property {string} body
|
|
28
|
-
* @property {Set<string>} css
|
|
31
|
+
@import {
|
|
32
|
+
ServerManifest,
|
|
33
|
+
RenderResult,
|
|
34
|
+
HandlerOptions,
|
|
35
|
+
ClientAssetEntry,
|
|
36
|
+
} from '@ripple-ts/vite-plugin/production';
|
|
29
37
|
*/
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
|
-
* Create a production request handler from a manifest
|
|
40
|
+
* Create a production request handler from a manifest.
|
|
41
|
+
*
|
|
42
|
+
* The returned function is a standard Web `fetch`-style handler:
|
|
43
|
+
* `(request: Request) => Promise<Response>`
|
|
33
44
|
*
|
|
34
45
|
* @param {ServerManifest} manifest
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {(component: Function) => Promise<RenderResult>} options.render - SSR render function
|
|
37
|
-
* @param {(css: Set<string>) => string} options.getCss - Get CSS for hashes
|
|
38
|
-
* @param {string} options.clientBase - Base path for client assets
|
|
46
|
+
* @param {HandlerOptions} options
|
|
39
47
|
* @returns {(request: Request) => Promise<Response>}
|
|
40
48
|
*/
|
|
41
49
|
export function createHandler(manifest, options) {
|
|
42
|
-
const { render, getCss,
|
|
50
|
+
const { render, getCss, htmlTemplate, executeServerFunction } = options;
|
|
43
51
|
const router = createRouter(manifest.routes);
|
|
44
|
-
const globalMiddlewares = manifest.middlewares
|
|
52
|
+
const globalMiddlewares = manifest.middlewares;
|
|
53
|
+
const trustProxy = manifest.trustProxy ?? false;
|
|
54
|
+
const clientAssets = manifest.clientAssets || {};
|
|
55
|
+
|
|
56
|
+
// Use adapter's runtime primitives for platform-agnostic operation
|
|
57
|
+
const runtime = manifest.runtime;
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
// Build the RPC lookup table using the adapter's hash function
|
|
60
|
+
const rpcLookup = manifest.rpcModules
|
|
61
|
+
? build_rpc_lookup(manifest.rpcModules, runtime.hash)
|
|
62
|
+
: new Map();
|
|
63
|
+
|
|
64
|
+
// Create async context and patch fetch for relative URL resolution in #server blocks
|
|
65
|
+
const asyncContext = runtime.createAsyncContext();
|
|
66
|
+
const fetchHandle = patch_global_fetch(asyncContext);
|
|
67
|
+
|
|
68
|
+
const handler = async function handler(/** @type {Request} */ request) {
|
|
47
69
|
const url = new URL(request.url);
|
|
48
70
|
const method = request.method;
|
|
49
71
|
|
|
72
|
+
// Handle RPC requests for #server blocks
|
|
73
|
+
if (is_rpc_request(url.pathname)) {
|
|
74
|
+
return handle_rpc_request(request, {
|
|
75
|
+
resolveFunction(hash) {
|
|
76
|
+
const entry = rpcLookup.get(hash);
|
|
77
|
+
if (!entry) return null;
|
|
78
|
+
const fn = entry.serverObj[entry.funcName];
|
|
79
|
+
return typeof fn === 'function' ? fn : null;
|
|
80
|
+
},
|
|
81
|
+
executeServerFunction,
|
|
82
|
+
asyncContext,
|
|
83
|
+
trustProxy,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
50
87
|
// Match route
|
|
51
88
|
const match = router.match(method, url.pathname);
|
|
52
89
|
|
|
@@ -66,7 +103,8 @@ export function createHandler(manifest, options) {
|
|
|
66
103
|
globalMiddlewares,
|
|
67
104
|
render,
|
|
68
105
|
getCss,
|
|
69
|
-
|
|
106
|
+
htmlTemplate,
|
|
107
|
+
clientAssets,
|
|
70
108
|
);
|
|
71
109
|
} else {
|
|
72
110
|
return await handleServerRoute(match.route, context, globalMiddlewares);
|
|
@@ -76,8 +114,19 @@ export function createHandler(manifest, options) {
|
|
|
76
114
|
return new Response('Internal Server Error', { status: 500 });
|
|
77
115
|
}
|
|
78
116
|
};
|
|
117
|
+
|
|
118
|
+
// Enable same-origin fetch short-circuit: server-side fetch() calls that
|
|
119
|
+
// resolve to the same origin are routed directly through this handler
|
|
120
|
+
// in-process, instead of making a real network request.
|
|
121
|
+
fetchHandle.set_handler(handler);
|
|
122
|
+
|
|
123
|
+
return handler;
|
|
79
124
|
}
|
|
80
125
|
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Render routes
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
81
130
|
/**
|
|
82
131
|
* Handle a RenderRoute in production
|
|
83
132
|
*
|
|
@@ -87,7 +136,8 @@ export function createHandler(manifest, options) {
|
|
|
87
136
|
* @param {Middleware[]} globalMiddlewares
|
|
88
137
|
* @param {(component: Function) => Promise<RenderResult>} render
|
|
89
138
|
* @param {(css: Set<string>) => string} getCss
|
|
90
|
-
* @param {string}
|
|
139
|
+
* @param {string} htmlTemplate
|
|
140
|
+
* @param {Record<string, ClientAssetEntry>} clientAssets
|
|
91
141
|
* @returns {Promise<Response>}
|
|
92
142
|
*/
|
|
93
143
|
async function handleRenderRoute(
|
|
@@ -97,7 +147,8 @@ async function handleRenderRoute(
|
|
|
97
147
|
globalMiddlewares,
|
|
98
148
|
render,
|
|
99
149
|
getCss,
|
|
100
|
-
|
|
150
|
+
htmlTemplate,
|
|
151
|
+
clientAssets,
|
|
101
152
|
) {
|
|
102
153
|
const renderHandler = async () => {
|
|
103
154
|
// Get the page component
|
|
@@ -120,7 +171,7 @@ async function handleRenderRoute(
|
|
|
120
171
|
// Render to HTML
|
|
121
172
|
const { head, body, css } = await render(RootComponent);
|
|
122
173
|
|
|
123
|
-
// Generate CSS
|
|
174
|
+
// Generate inline scoped CSS (from SSR-rendered component hashes)
|
|
124
175
|
let cssContent = '';
|
|
125
176
|
if (css.size > 0) {
|
|
126
177
|
const cssString = getCss(css);
|
|
@@ -129,14 +180,46 @@ async function handleRenderRoute(
|
|
|
129
180
|
}
|
|
130
181
|
}
|
|
131
182
|
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
183
|
+
// Build asset preload tags from the client manifest.
|
|
184
|
+
// These ensure the browser starts downloading page-specific JS/CSS
|
|
185
|
+
// immediately, before the hydration script executes.
|
|
186
|
+
/** @type {string[]} */
|
|
187
|
+
const preloadTags = [];
|
|
188
|
+
const entryAssets = clientAssets[route.entry];
|
|
189
|
+
|
|
190
|
+
if (entryAssets?.css) {
|
|
191
|
+
for (const cssFile of entryAssets.css) {
|
|
192
|
+
preloadTags.push(`<link rel="stylesheet" href="/${cssFile}">`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (entryAssets?.js) {
|
|
196
|
+
preloadTags.push(`<link rel="modulepreload" href="/${entryAssets.js}">`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Preload the hydrate runtime so it starts downloading in parallel
|
|
200
|
+
const hydrateAsset = clientAssets.__hydrate_js;
|
|
201
|
+
if (hydrateAsset?.js) {
|
|
202
|
+
preloadTags.push(`<link rel="modulepreload" href="/${hydrateAsset.js}">`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Build head content with hydration data
|
|
206
|
+
const routeData = JSON.stringify({
|
|
207
|
+
entry: route.entry,
|
|
208
|
+
params: context.params,
|
|
139
209
|
});
|
|
210
|
+
const headContent = [
|
|
211
|
+
head,
|
|
212
|
+
cssContent,
|
|
213
|
+
...preloadTags,
|
|
214
|
+
`<script id="__ripple_data" type="application/json">${escapeScript(routeData)}</script>`,
|
|
215
|
+
]
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.join('\n');
|
|
218
|
+
|
|
219
|
+
// Inject into the HTML template
|
|
220
|
+
const html = htmlTemplate
|
|
221
|
+
.replace('<!--ssr-head-->', headContent)
|
|
222
|
+
.replace('<!--ssr-body-->', body);
|
|
140
223
|
|
|
141
224
|
return new Response(html, {
|
|
142
225
|
status: 200,
|
|
@@ -147,6 +230,10 @@ async function handleRenderRoute(
|
|
|
147
230
|
return runMiddlewareChain(context, globalMiddlewares, route.before || [], renderHandler, []);
|
|
148
231
|
}
|
|
149
232
|
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Server routes
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
150
237
|
/**
|
|
151
238
|
* Handle a ServerRoute in production
|
|
152
239
|
*
|
|
@@ -166,6 +253,10 @@ async function handleServerRoute(route, context, globalMiddlewares) {
|
|
|
166
253
|
);
|
|
167
254
|
}
|
|
168
255
|
|
|
256
|
+
// ============================================================================
|
|
257
|
+
// Component wrappers
|
|
258
|
+
// ============================================================================
|
|
259
|
+
|
|
169
260
|
/**
|
|
170
261
|
* Create a wrapper component that injects props
|
|
171
262
|
* @param {Function} Component
|
|
@@ -194,67 +285,9 @@ function createLayoutWrapper(Layout, Page, pageProps) {
|
|
|
194
285
|
};
|
|
195
286
|
}
|
|
196
287
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
* @param {string} options.head
|
|
201
|
-
* @param {string} options.body
|
|
202
|
-
* @param {RenderRoute} options.route
|
|
203
|
-
* @param {import('@ripple-ts/vite-plugin').Context} options.context
|
|
204
|
-
* @param {string} options.clientBase
|
|
205
|
-
* @returns {string}
|
|
206
|
-
*/
|
|
207
|
-
function generateHtml({ head, body, route, context, clientBase }) {
|
|
208
|
-
const routeData = JSON.stringify({
|
|
209
|
-
entry: route.entry,
|
|
210
|
-
params: context.params,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
return `<!DOCTYPE html>
|
|
214
|
-
<html lang="en">
|
|
215
|
-
<head>
|
|
216
|
-
<meta charset="UTF-8">
|
|
217
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
218
|
-
${head}
|
|
219
|
-
</head>
|
|
220
|
-
<body>
|
|
221
|
-
<div id="app">${body}</div>
|
|
222
|
-
<script id="__ripple_data" type="application/json">${escapeScript(routeData)}</script>
|
|
223
|
-
<script type="module">
|
|
224
|
-
import { hydrate, mount } from '${clientBase}ripple.js';
|
|
225
|
-
|
|
226
|
-
const data = JSON.parse(document.getElementById('__ripple_data').textContent);
|
|
227
|
-
const target = document.getElementById('app');
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
const module = await import('${clientBase}' + data.entry.replace(/^\\//, '').replace(/\\.ripple$/, '.js'));
|
|
231
|
-
const Component =
|
|
232
|
-
module.default ||
|
|
233
|
-
Object.entries(module).find(([key, value]) => typeof value === 'function' && /^[A-Z]/.test(key))?.[1];
|
|
234
|
-
|
|
235
|
-
if (!Component || !target) {
|
|
236
|
-
console.error('[ripple] Unable to hydrate route: missing component export or #app target.');
|
|
237
|
-
} else {
|
|
238
|
-
try {
|
|
239
|
-
hydrate(Component, {
|
|
240
|
-
target,
|
|
241
|
-
props: { params: data.params }
|
|
242
|
-
});
|
|
243
|
-
} catch (error) {
|
|
244
|
-
console.warn('[ripple] Hydration failed, falling back to mount.', error);
|
|
245
|
-
mount(Component, {
|
|
246
|
-
target,
|
|
247
|
-
props: { params: data.params }
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
} catch (error) {
|
|
252
|
-
console.error('[ripple] Failed to bootstrap client hydration.', error);
|
|
253
|
-
}
|
|
254
|
-
</script>
|
|
255
|
-
</body>
|
|
256
|
-
</html>`;
|
|
257
|
-
}
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Utilities
|
|
290
|
+
// ============================================================================
|
|
258
291
|
|
|
259
292
|
/**
|
|
260
293
|
* Escape script content to prevent XSS
|
|
@@ -90,7 +90,7 @@ export async function handleRenderRoute(route, context, vite) {
|
|
|
90
90
|
.join('\n');
|
|
91
91
|
|
|
92
92
|
// Load and process index.html template
|
|
93
|
-
const templatePath = join(vite.config.root, '
|
|
93
|
+
const templatePath = join(vite.config.root, 'index.html');
|
|
94
94
|
let template = await readFile(templatePath, 'utf-8');
|
|
95
95
|
|
|
96
96
|
// Apply Vite's HTML transforms (HMR client, module resolution, etc.)
|