@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.
@@ -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
+ }
@@ -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
- * @typedef {Object} ServerManifest
18
- * @property {Route[]} routes - Array of route definitions
19
- * @property {Record<string, Function>} components - Map of entry path to component function
20
- * @property {Record<string, Function>} layouts - Map of layout path to layout function
21
- * @property {Middleware[]} middlewares - Global middlewares
31
+ @import {
32
+ ServerManifest,
33
+ RenderResult,
34
+ HandlerOptions,
35
+ ClientAssetEntry,
36
+ } from '@ripple-ts/vite-plugin/production';
22
37
  */
23
38
 
24
39
  /**
25
- * @typedef {Object} RenderResult
26
- * @property {string} head
27
- * @property {string} body
28
- * @property {Set<string>} css
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 {Object} options
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, clientBase = '/' } = options;
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
- clientBase,
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} clientBase
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
- clientBase,
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 tags
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
- // Generate the full HTML document
133
- const html = generateHtml({
134
- head: head + cssContent,
135
- body,
136
- route,
137
- context,
138
- clientBase,
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
- * Generate the full HTML document for production
199
- * @param {Object} options
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, 'public', 'index.html');
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
+ }