@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,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
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
|
+
}
|