@ripple-ts/vite-plugin 0.2.213 → 0.2.214
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 +12 -0
- package/package.json +14 -3
- package/src/bin/preview.js +43 -0
- package/src/constants.js +2 -0
- package/src/index.js +493 -192
- package/src/load-config.js +172 -0
- package/src/server/production.js +119 -93
- package/src/server/render-route.js +1 -1
- package/src/server/virtual-entry.js +215 -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
|
-
|
|
31
|
+
@import {
|
|
32
|
+
ServerManifest,
|
|
33
|
+
RenderResult,
|
|
34
|
+
HandlerOptions,
|
|
35
|
+
ClientAssetEntry,
|
|
36
|
+
} from '@ripple-ts/vite-plugin/production';
|
|
22
37
|
*/
|
|
23
38
|
|
|
24
39
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
/**
|
|
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;
|
|
54
|
+
const clientAssets = manifest.clientAssets || {};
|
|
55
|
+
|
|
56
|
+
// Use adapter's runtime primitives for platform-agnostic operation
|
|
57
|
+
const runtime = manifest.runtime;
|
|
58
|
+
|
|
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
|
+
patch_global_fetch(asyncContext);
|
|
45
67
|
|
|
46
68
|
return async function handler(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);
|
|
@@ -78,6 +116,10 @@ export function createHandler(manifest, options) {
|
|
|
78
116
|
};
|
|
79
117
|
}
|
|
80
118
|
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Render routes
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
81
123
|
/**
|
|
82
124
|
* Handle a RenderRoute in production
|
|
83
125
|
*
|
|
@@ -87,7 +129,8 @@ export function createHandler(manifest, options) {
|
|
|
87
129
|
* @param {Middleware[]} globalMiddlewares
|
|
88
130
|
* @param {(component: Function) => Promise<RenderResult>} render
|
|
89
131
|
* @param {(css: Set<string>) => string} getCss
|
|
90
|
-
* @param {string}
|
|
132
|
+
* @param {string} htmlTemplate
|
|
133
|
+
* @param {Record<string, ClientAssetEntry>} clientAssets
|
|
91
134
|
* @returns {Promise<Response>}
|
|
92
135
|
*/
|
|
93
136
|
async function handleRenderRoute(
|
|
@@ -97,7 +140,8 @@ async function handleRenderRoute(
|
|
|
97
140
|
globalMiddlewares,
|
|
98
141
|
render,
|
|
99
142
|
getCss,
|
|
100
|
-
|
|
143
|
+
htmlTemplate,
|
|
144
|
+
clientAssets,
|
|
101
145
|
) {
|
|
102
146
|
const renderHandler = async () => {
|
|
103
147
|
// Get the page component
|
|
@@ -120,7 +164,7 @@ async function handleRenderRoute(
|
|
|
120
164
|
// Render to HTML
|
|
121
165
|
const { head, body, css } = await render(RootComponent);
|
|
122
166
|
|
|
123
|
-
// Generate CSS
|
|
167
|
+
// Generate inline scoped CSS (from SSR-rendered component hashes)
|
|
124
168
|
let cssContent = '';
|
|
125
169
|
if (css.size > 0) {
|
|
126
170
|
const cssString = getCss(css);
|
|
@@ -129,14 +173,46 @@ async function handleRenderRoute(
|
|
|
129
173
|
}
|
|
130
174
|
}
|
|
131
175
|
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
176
|
+
// Build asset preload tags from the client manifest.
|
|
177
|
+
// These ensure the browser starts downloading page-specific JS/CSS
|
|
178
|
+
// immediately, before the hydration script executes.
|
|
179
|
+
/** @type {string[]} */
|
|
180
|
+
const preloadTags = [];
|
|
181
|
+
const entryAssets = clientAssets[route.entry];
|
|
182
|
+
|
|
183
|
+
if (entryAssets?.css) {
|
|
184
|
+
for (const cssFile of entryAssets.css) {
|
|
185
|
+
preloadTags.push(`<link rel="stylesheet" href="/${cssFile}">`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (entryAssets?.js) {
|
|
189
|
+
preloadTags.push(`<link rel="modulepreload" href="/${entryAssets.js}">`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Preload the hydrate runtime so it starts downloading in parallel
|
|
193
|
+
const hydrateAsset = clientAssets.__hydrate_js;
|
|
194
|
+
if (hydrateAsset?.js) {
|
|
195
|
+
preloadTags.push(`<link rel="modulepreload" href="/${hydrateAsset.js}">`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Build head content with hydration data
|
|
199
|
+
const routeData = JSON.stringify({
|
|
200
|
+
entry: route.entry,
|
|
201
|
+
params: context.params,
|
|
139
202
|
});
|
|
203
|
+
const headContent = [
|
|
204
|
+
head,
|
|
205
|
+
cssContent,
|
|
206
|
+
...preloadTags,
|
|
207
|
+
`<script id="__ripple_data" type="application/json">${escapeScript(routeData)}</script>`,
|
|
208
|
+
]
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.join('\n');
|
|
211
|
+
|
|
212
|
+
// Inject into the HTML template
|
|
213
|
+
const html = htmlTemplate
|
|
214
|
+
.replace('<!--ssr-head-->', headContent)
|
|
215
|
+
.replace('<!--ssr-body-->', body);
|
|
140
216
|
|
|
141
217
|
return new Response(html, {
|
|
142
218
|
status: 200,
|
|
@@ -147,6 +223,10 @@ async function handleRenderRoute(
|
|
|
147
223
|
return runMiddlewareChain(context, globalMiddlewares, route.before || [], renderHandler, []);
|
|
148
224
|
}
|
|
149
225
|
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// Server routes
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
150
230
|
/**
|
|
151
231
|
* Handle a ServerRoute in production
|
|
152
232
|
*
|
|
@@ -166,6 +246,10 @@ async function handleServerRoute(route, context, globalMiddlewares) {
|
|
|
166
246
|
);
|
|
167
247
|
}
|
|
168
248
|
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Component wrappers
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
169
253
|
/**
|
|
170
254
|
* Create a wrapper component that injects props
|
|
171
255
|
* @param {Function} Component
|
|
@@ -194,67 +278,9 @@ function createLayoutWrapper(Layout, Page, pageProps) {
|
|
|
194
278
|
};
|
|
195
279
|
}
|
|
196
280
|
|
|
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
|
-
}
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Utilities
|
|
283
|
+
// ============================================================================
|
|
258
284
|
|
|
259
285
|
/**
|
|
260
286
|
* 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.)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual server entry generator for production builds.
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained server entry module that:
|
|
5
|
+
* - Imports all SSR-compiled page components and layouts
|
|
6
|
+
* - Imports the production request handler (createHandler)
|
|
7
|
+
* - Imports the adapter's serve() function
|
|
8
|
+
* - Wires routes, middlewares, RPC, and boots the HTTP server
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @import { Route } from '@ripple-ts/vite-plugin' */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ClientAssetEntry
|
|
15
|
+
* @property {string} js - Path to the built JS file
|
|
16
|
+
* @property {string[]} css - Paths to the built CSS files
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} VirtualEntryOptions
|
|
21
|
+
* @property {Route[]} routes - Route definitions from ripple.config.ts
|
|
22
|
+
* @property {string} rippleConfigPath - Absolute path to ripple.config.ts (for importing middlewares/adapter)
|
|
23
|
+
* @property {string} htmlTemplatePath - Path to the processed index.html template
|
|
24
|
+
* @property {string[]} [rpcModulePaths] - Paths (relative to root) of .ripple modules with #server blocks
|
|
25
|
+
* @property {Record<string, ClientAssetEntry>} [clientAssetMap] - Map of route entry paths to built JS/CSS asset paths
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate the virtual server entry module source code.
|
|
30
|
+
*
|
|
31
|
+
* The generated module:
|
|
32
|
+
* 1. Imports ripple SSR utilities (render, get_css_for_hashes, executeServerFunction)
|
|
33
|
+
* 2. Imports createHandler from @ripple-ts/vite-plugin/production
|
|
34
|
+
* 3. Imports ripple.config.ts to get adapter, middlewares, and routes
|
|
35
|
+
* 4. Imports each RenderRoute's entry (and layout) as SSR components
|
|
36
|
+
* 5. Builds a ServerManifest and creates the fetch handler
|
|
37
|
+
* 6. Reads the HTML template from disk
|
|
38
|
+
* 7. Boots the adapter with the handler
|
|
39
|
+
*
|
|
40
|
+
* @param {VirtualEntryOptions} options
|
|
41
|
+
* @returns {string} The generated JavaScript module source
|
|
42
|
+
*/
|
|
43
|
+
export function generateServerEntry(options) {
|
|
44
|
+
const {
|
|
45
|
+
routes,
|
|
46
|
+
rippleConfigPath,
|
|
47
|
+
htmlTemplatePath,
|
|
48
|
+
rpcModulePaths = [],
|
|
49
|
+
clientAssetMap = {},
|
|
50
|
+
} = options;
|
|
51
|
+
|
|
52
|
+
// Collect unique component entries and layouts
|
|
53
|
+
/** @type {Map<string, string>} entry path → import variable name */
|
|
54
|
+
const component_imports = new Map();
|
|
55
|
+
/** @type {Map<string, string>} layout path → import variable name */
|
|
56
|
+
const layout_imports = new Map();
|
|
57
|
+
/** @type {Map<string, string>} rpc module path → import variable name */
|
|
58
|
+
const rpc_imports = new Map();
|
|
59
|
+
|
|
60
|
+
let component_index = 0;
|
|
61
|
+
let layout_index = 0;
|
|
62
|
+
let rpc_index = 0;
|
|
63
|
+
|
|
64
|
+
for (const route of routes) {
|
|
65
|
+
if (route.type === 'render') {
|
|
66
|
+
if (!component_imports.has(route.entry)) {
|
|
67
|
+
component_imports.set(route.entry, `_page_${component_index++}`);
|
|
68
|
+
}
|
|
69
|
+
if (route.layout && !layout_imports.has(route.layout)) {
|
|
70
|
+
layout_imports.set(route.layout, `_layout_${layout_index++}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Collect RPC modules (sub-components with #server blocks, not already in page entries)
|
|
76
|
+
for (const rpcPath of rpcModulePaths) {
|
|
77
|
+
if (!component_imports.has(rpcPath) && !rpc_imports.has(rpcPath)) {
|
|
78
|
+
rpc_imports.set(rpcPath, `_rpc_${rpc_index++}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Dynamic import lines (built from route/RPC config) ---
|
|
83
|
+
|
|
84
|
+
const import_lines = [];
|
|
85
|
+
|
|
86
|
+
for (const [entry, varName] of component_imports) {
|
|
87
|
+
import_lines.push(`import * as ${varName} from ${JSON.stringify(entry)};`);
|
|
88
|
+
}
|
|
89
|
+
for (const [layout, varName] of layout_imports) {
|
|
90
|
+
import_lines.push(`import * as ${varName} from ${JSON.stringify(layout)};`);
|
|
91
|
+
}
|
|
92
|
+
for (const [rpcPath, varName] of rpc_imports) {
|
|
93
|
+
import_lines.push(`import * as ${varName} from ${JSON.stringify(rpcPath)};`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Dynamic map entries ---
|
|
97
|
+
|
|
98
|
+
const component_entries = [...component_imports]
|
|
99
|
+
.map(([entry, varName]) => ` ${JSON.stringify(entry)}: getDefaultExport(${varName}),`)
|
|
100
|
+
.join('\n');
|
|
101
|
+
|
|
102
|
+
const layout_entries = [...layout_imports]
|
|
103
|
+
.map(([layout, varName]) => ` ${JSON.stringify(layout)}: getDefaultExport(${varName}),`)
|
|
104
|
+
.join('\n');
|
|
105
|
+
|
|
106
|
+
// Only check _$_server_$_ on modules known to have #server blocks.
|
|
107
|
+
// Checking modules without #server blocks causes rollup warnings since
|
|
108
|
+
// they don't export _$_server_$_.
|
|
109
|
+
const rpcPathSet = new Set(rpcModulePaths);
|
|
110
|
+
const rpc_entries = [];
|
|
111
|
+
|
|
112
|
+
for (const [entry, varName] of component_imports) {
|
|
113
|
+
if (rpcPathSet.has(entry)) {
|
|
114
|
+
rpc_entries.push(`rpcModules[${JSON.stringify(entry)}] = ${varName}._$_server_$_;`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const [rpcPath, varName] of rpc_imports) {
|
|
118
|
+
rpc_entries.push(`rpcModules[${JSON.stringify(rpcPath)}] = ${varName}._$_server_$_;`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Assemble the full module ---
|
|
122
|
+
|
|
123
|
+
return `\
|
|
124
|
+
// Auto-generated server entry for production build
|
|
125
|
+
// Do not edit — regenerated on each build
|
|
126
|
+
|
|
127
|
+
import { render, get_css_for_hashes, executeServerFunction } from 'ripple/server';
|
|
128
|
+
import { createHandler, resolveRippleConfig } from '@ripple-ts/vite-plugin/production';
|
|
129
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
130
|
+
import { fileURLToPath } from 'node:url';
|
|
131
|
+
import { dirname, join, resolve } from 'node:path';
|
|
132
|
+
|
|
133
|
+
import _rawRippleConfig from ${JSON.stringify(rippleConfigPath)};
|
|
134
|
+
|
|
135
|
+
${import_lines.join('\n')}
|
|
136
|
+
|
|
137
|
+
let rippleConfig;
|
|
138
|
+
try {
|
|
139
|
+
rippleConfig = resolveRippleConfig(_rawRippleConfig, { requireAdapter: true });
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(e.message);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getDefaultExport(mod) {
|
|
146
|
+
if (typeof mod.default === 'function') return mod.default;
|
|
147
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
148
|
+
if (typeof value === 'function' && /^[A-Z]/.test(key)) return value;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const components = {
|
|
154
|
+
${component_entries}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const layouts = {
|
|
158
|
+
${layout_entries}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const rpcModules = {};
|
|
162
|
+
${rpc_entries.join('\n')}
|
|
163
|
+
|
|
164
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
165
|
+
if (!existsSync(join(__dirname, ${JSON.stringify(htmlTemplatePath)}))) {
|
|
166
|
+
console.error('[ripple] HTML template not found:', join(__dirname, ${JSON.stringify(htmlTemplatePath)}));
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const htmlTemplate = readFileSync(join(__dirname, ${JSON.stringify(htmlTemplatePath)}), 'utf-8');
|
|
170
|
+
|
|
171
|
+
const clientAssets = ${JSON.stringify(clientAssetMap, null, 2)};
|
|
172
|
+
|
|
173
|
+
const handler = createHandler(
|
|
174
|
+
{
|
|
175
|
+
routes: rippleConfig.router.routes,
|
|
176
|
+
components,
|
|
177
|
+
layouts,
|
|
178
|
+
middlewares: rippleConfig.middlewares,
|
|
179
|
+
rpcModules,
|
|
180
|
+
trustProxy: rippleConfig.server.trustProxy,
|
|
181
|
+
runtime: rippleConfig.adapter.runtime,
|
|
182
|
+
clientAssets,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
render,
|
|
186
|
+
getCss: get_css_for_hashes,
|
|
187
|
+
htmlTemplate,
|
|
188
|
+
executeServerFunction,
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
export { handler };
|
|
193
|
+
|
|
194
|
+
// Auto-boot when running directly (node dist/server/entry.js)
|
|
195
|
+
// Skip when imported as a module (e.g. by a serverless function wrapper)
|
|
196
|
+
const isMainModule = typeof process !== 'undefined' && process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
197
|
+
if (isMainModule) {
|
|
198
|
+
if (rippleConfig.adapter?.serve) {
|
|
199
|
+
const server = rippleConfig.adapter.serve(handler, {
|
|
200
|
+
static: { dir: join(__dirname, '../client') },
|
|
201
|
+
});
|
|
202
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
|
203
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
204
|
+
console.error('[ripple] Invalid PORT value:', process.env.PORT);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
server.listen(port);
|
|
208
|
+
console.log('[ripple] Production server listening on port ' + port);
|
|
209
|
+
} else {
|
|
210
|
+
console.error('[ripple] No adapter configured in ripple.config.ts');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
}
|