@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.
@@ -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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "noEmit": true
5
+ },
6
+ "include": ["./src/**/*", "./tests/**/*"],
7
+ "exclude": ["dist", "node_modules"]
8
+ }
package/types/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Plugin } from 'vite';
1
+ import type { Plugin, BuildEnvironmentOptions, ViteDevServer } from 'vite';
2
+ import type { RuntimePrimitives } from '@ripple-ts/adapter';
2
3
 
3
4
  declare module '@ripple-ts/vite-plugin' {
4
5
  // ============================================================================
@@ -7,6 +8,16 @@ declare module '@ripple-ts/vite-plugin' {
7
8
 
8
9
  export function ripple(options?: RipplePluginOptions): Plugin[];
9
10
  export function defineConfig(options: RippleConfigOptions): RippleConfigOptions;
11
+ export function resolveRippleConfig(
12
+ raw: RippleConfigOptions,
13
+ options?: { requireAdapter?: boolean },
14
+ ): ResolvedRippleConfig;
15
+ export function getRippleConfigPath(projectRoot: string): string;
16
+ export function rippleConfigExists(projectRoot: string): boolean;
17
+ export function loadRippleConfig(
18
+ projectRoot: string,
19
+ options?: { vite?: ViteDevServer; requireAdapter?: boolean },
20
+ ): Promise<ResolvedRippleConfig>;
10
21
 
11
22
  // ============================================================================
12
23
  // Route classes
@@ -90,10 +101,23 @@ declare module '@ripple-ts/vite-plugin' {
90
101
 
91
102
  export interface RippleConfigOptions {
92
103
  build?: {
104
+ /** Output directory for the production build. @default 'dist' */
105
+ outDir?: string;
93
106
  minify?: boolean;
107
+ target?: BuildEnvironmentOptions['target'];
94
108
  };
95
109
  adapter?: {
96
110
  serve: AdapterServeFunction;
111
+ /**
112
+ * Platform-specific runtime primitives provided by the adapter.
113
+ *
114
+ * These allow the server runtime to operate without depending
115
+ * on Node.js-specific APIs like `node:crypto` or `node:async_hooks`.
116
+ *
117
+ * Required for production builds. In development, the vite plugin
118
+ * falls back to Node.js defaults if not provided.
119
+ */
120
+ runtime: RuntimePrimitives;
97
121
  };
98
122
  router: {
99
123
  routes: Route[];
@@ -118,8 +142,100 @@ declare module '@ripple-ts/vite-plugin' {
118
142
  };
119
143
  }
120
144
 
145
+ /**
146
+ * Resolved configuration with all defaults applied.
147
+ * Returned by `resolveRippleConfig` and `loadRippleConfig`.
148
+ * Consumers should use this type instead of applying ad-hoc defaults.
149
+ */
150
+ export interface ResolvedRippleConfig {
151
+ build: {
152
+ /** @default 'dist' */
153
+ outDir: string;
154
+ minify?: boolean;
155
+ target?: BuildEnvironmentOptions['target'];
156
+ };
157
+ adapter?: {
158
+ serve: AdapterServeFunction;
159
+ runtime: RuntimePrimitives;
160
+ };
161
+ router: {
162
+ routes: Route[];
163
+ };
164
+ /** @default [] */
165
+ middlewares: Middleware[];
166
+ platform: {
167
+ /** @default {} */
168
+ env: Record<string, string>;
169
+ };
170
+ server: {
171
+ /** @default false */
172
+ trustProxy: boolean;
173
+ };
174
+ }
175
+
121
176
  export type AdapterServeFunction = (
122
177
  handler: (request: Request, platform?: unknown) => Response | Promise<Response>,
123
178
  options?: Record<string, unknown>,
124
179
  ) => { listen: (port?: number) => unknown; close: () => void };
125
180
  }
181
+
182
+ declare module '@ripple-ts/vite-plugin/production' {
183
+ import type {
184
+ Route,
185
+ Middleware,
186
+ RuntimePrimitives,
187
+ ResolvedRippleConfig,
188
+ RippleConfigOptions,
189
+ } from '@ripple-ts/vite-plugin';
190
+
191
+ export function resolveRippleConfig(
192
+ raw: RippleConfigOptions,
193
+ options?: { requireAdapter?: boolean },
194
+ ): ResolvedRippleConfig;
195
+
196
+ export interface ClientAssetEntry {
197
+ /** Path to the built JS file (relative to client output dir) */
198
+ js: string;
199
+ /** Paths to the built CSS files (relative to client output dir) */
200
+ css: string[];
201
+ }
202
+
203
+ export interface ServerManifest {
204
+ routes: Route[];
205
+ components: Record<string, Function>;
206
+ layouts: Record<string, Function>;
207
+ middlewares: Middleware[];
208
+ /** Map of entry path → _$_server_$_ object for RPC support */
209
+ rpcModules?: Record<string, Record<string, Function>>;
210
+ /** Trust X-Forwarded-* headers when deriving origin for RPC fetch */
211
+ trustProxy?: boolean;
212
+ /** Platform-specific runtime primitives from the adapter */
213
+ runtime: RuntimePrimitives;
214
+ /**
215
+ * Map of route entry paths to their built client asset paths.
216
+ * Used to emit `<link rel="stylesheet">` and `<link rel="modulepreload">`
217
+ * tags in the production HTML. Populated from Vite's client manifest
218
+ * during the build. The special key `__hydrate_js` holds the hydrate
219
+ * runtime entry.
220
+ */
221
+ clientAssets?: Record<string, ClientAssetEntry>;
222
+ }
223
+
224
+ export interface RenderResult {
225
+ head: string;
226
+ body: string;
227
+ css: Set<string>;
228
+ }
229
+
230
+ export interface HandlerOptions {
231
+ render: (component: Function) => Promise<RenderResult>;
232
+ getCss: (css: Set<string>) => string;
233
+ htmlTemplate: string;
234
+ executeServerFunction: (fn: Function, body: string) => Promise<string>;
235
+ }
236
+
237
+ export function createHandler(
238
+ manifest: ServerManifest,
239
+ options: HandlerOptions,
240
+ ): (request: Request) => Promise<Response>;
241
+ }