@ripple-ts/vite-plugin 0.3.64 → 0.3.66
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 +27 -0
- package/package.json +4 -4
- package/src/index.js +58 -149
- package/src/load-config.js +79 -6
- package/src/project-codegen.js +201 -0
- package/src/routes.js +55 -1
- package/src/server/production.js +23 -6
- package/src/server/render-route.js +30 -22
- package/src/server/virtual-entry.js +29 -7
- package/tests/project-codegen.test.js +42 -0
- package/tests/render-route-props.test.js +256 -0
- package/types/index.d.ts +15 -3
- package/types/production.d.ts +6 -1
package/src/routes.js
CHANGED
|
@@ -5,6 +5,56 @@
|
|
|
5
5
|
* @typedef {import('@ripple-ts/vite-plugin').ServerRouteOptions} ServerRouteOptions
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {string | readonly [string, string]} RenderRouteEntry
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {RenderRouteEntry | undefined} entry
|
|
14
|
+
* @returns {string | undefined}
|
|
15
|
+
*/
|
|
16
|
+
export function get_route_entry_path(entry) {
|
|
17
|
+
return typeof entry === 'string' ? entry : entry?.[1];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {RenderRouteEntry | undefined} entry
|
|
22
|
+
* @returns {string | undefined}
|
|
23
|
+
*/
|
|
24
|
+
export function get_route_entry_export_name(entry) {
|
|
25
|
+
return typeof entry === 'string' ? undefined : entry?.[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {RenderRouteEntry | undefined} entry
|
|
30
|
+
* @returns {string | undefined}
|
|
31
|
+
*/
|
|
32
|
+
export function get_route_entry_id(entry) {
|
|
33
|
+
const path = get_route_entry_path(entry);
|
|
34
|
+
const export_name = get_route_entry_export_name(entry);
|
|
35
|
+
return path && export_name ? `${path}#${export_name}` : path;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Record<string, unknown>} module
|
|
40
|
+
* @param {string | undefined} export_name
|
|
41
|
+
* @returns {Function | null}
|
|
42
|
+
*/
|
|
43
|
+
export function get_component_export(module, export_name) {
|
|
44
|
+
if (export_name && typeof module[export_name] === 'function') {
|
|
45
|
+
return module[export_name];
|
|
46
|
+
}
|
|
47
|
+
if (typeof module.default === 'function') {
|
|
48
|
+
return module.default;
|
|
49
|
+
}
|
|
50
|
+
for (const [key, value] of Object.entries(module)) {
|
|
51
|
+
if (typeof value === 'function' && /^[A-Z]/.test(key)) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
8
58
|
/**
|
|
9
59
|
* Route for rendering Ripple components with SSR
|
|
10
60
|
*/
|
|
@@ -15,7 +65,7 @@ export class RenderRoute {
|
|
|
15
65
|
/** @type {string} */
|
|
16
66
|
path;
|
|
17
67
|
|
|
18
|
-
/** @type {
|
|
68
|
+
/** @type {RenderRouteEntry | undefined} */
|
|
19
69
|
entry;
|
|
20
70
|
|
|
21
71
|
/** @type {string | undefined} */
|
|
@@ -28,6 +78,10 @@ export class RenderRoute {
|
|
|
28
78
|
* @param {RenderRouteOptions} options
|
|
29
79
|
*/
|
|
30
80
|
constructor(options) {
|
|
81
|
+
if (!options.entry) {
|
|
82
|
+
throw new Error('RenderRoute requires an `entry`.');
|
|
83
|
+
}
|
|
84
|
+
|
|
31
85
|
this.path = options.path;
|
|
32
86
|
this.entry = options.entry;
|
|
33
87
|
this.layout = options.layout;
|
package/src/server/production.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { createRouter } from './router.js';
|
|
13
13
|
import { createContext, runMiddlewareChain } from './middleware.js';
|
|
14
14
|
import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
|
|
15
|
+
import { get_route_entry_id, get_route_entry_path } from '../routes.js';
|
|
15
16
|
import {
|
|
16
17
|
patch_global_fetch,
|
|
17
18
|
build_rpc_lookup,
|
|
@@ -135,7 +136,7 @@ export function createHandler(manifest, options) {
|
|
|
135
136
|
* @param {import('@ripple-ts/vite-plugin').Context} context
|
|
136
137
|
* @param {ServerManifest} manifest
|
|
137
138
|
* @param {Middleware[]} globalMiddlewares
|
|
138
|
-
* @param {(component: Function) => Promise<RenderResult>} render
|
|
139
|
+
* @param {(component: Function, options?: { rootBoundary?: import('@ripple-ts/vite-plugin').RootBoundaryOptions }) => Promise<RenderResult>} render
|
|
139
140
|
* @param {(css: Set<string>) => string} getCss
|
|
140
141
|
* @param {string} htmlTemplate
|
|
141
142
|
* @param {Record<string, ClientAssetEntry>} clientAssets
|
|
@@ -153,9 +154,11 @@ async function handleRenderRoute(
|
|
|
153
154
|
) {
|
|
154
155
|
const renderHandler = async () => {
|
|
155
156
|
// Get the page component
|
|
156
|
-
const
|
|
157
|
+
const entryId = get_route_entry_id(route.entry);
|
|
158
|
+
const entryPath = get_route_entry_path(route.entry);
|
|
159
|
+
const PageComponent = entryId ? manifest.components[entryId] : null;
|
|
157
160
|
if (!PageComponent) {
|
|
158
|
-
throw new Error(`Component not found
|
|
161
|
+
throw new Error(`Component not found for route ${route.path}`);
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
// Get layout if specified
|
|
@@ -170,7 +173,9 @@ async function handleRenderRoute(
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
// Render to HTML
|
|
173
|
-
const { head, body, css } = await render(RootComponent
|
|
176
|
+
const { head, body, css } = await render(RootComponent, {
|
|
177
|
+
rootBoundary: manifest.rootBoundary,
|
|
178
|
+
});
|
|
174
179
|
|
|
175
180
|
// Generate inline scoped CSS (from SSR-rendered component hashes)
|
|
176
181
|
let cssContent = '';
|
|
@@ -186,7 +191,7 @@ async function handleRenderRoute(
|
|
|
186
191
|
// immediately, before the hydration script executes.
|
|
187
192
|
/** @type {string[]} */
|
|
188
193
|
const preloadTags = [];
|
|
189
|
-
const entryAssets = clientAssets[
|
|
194
|
+
const entryAssets = entryPath ? clientAssets[entryPath] : undefined;
|
|
190
195
|
|
|
191
196
|
if (entryAssets?.css) {
|
|
192
197
|
for (const cssFile of entryAssets.css) {
|
|
@@ -205,7 +210,8 @@ async function handleRenderRoute(
|
|
|
205
210
|
|
|
206
211
|
// Build head content with hydration data
|
|
207
212
|
const routeData = JSON.stringify({
|
|
208
|
-
entry:
|
|
213
|
+
entry: entryPath,
|
|
214
|
+
routeIndex: getRenderRouteIndex(manifest.routes, route),
|
|
209
215
|
params: context.params,
|
|
210
216
|
});
|
|
211
217
|
const headContent = [
|
|
@@ -231,6 +237,17 @@ async function handleRenderRoute(
|
|
|
231
237
|
return runMiddlewareChain(context, globalMiddlewares, route.before || [], renderHandler, []);
|
|
232
238
|
}
|
|
233
239
|
|
|
240
|
+
/**
|
|
241
|
+
* @param {Route[]} routes
|
|
242
|
+
* @param {RenderRoute} route
|
|
243
|
+
* @returns {number | undefined}
|
|
244
|
+
*/
|
|
245
|
+
function getRenderRouteIndex(routes, route) {
|
|
246
|
+
const renderRoutes = routes.filter((r) => r.type === 'render');
|
|
247
|
+
const index = renderRoutes.indexOf(route);
|
|
248
|
+
return index === -1 ? undefined : index;
|
|
249
|
+
}
|
|
250
|
+
|
|
234
251
|
// ============================================================================
|
|
235
252
|
// Server routes
|
|
236
253
|
// ============================================================================
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
|
|
5
|
+
import {
|
|
6
|
+
get_component_export,
|
|
7
|
+
get_route_entry_export_name,
|
|
8
|
+
get_route_entry_path,
|
|
9
|
+
} from '../routes.js';
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* @typedef {import('@ripple-ts/vite-plugin').Context} Context
|
|
8
13
|
* @typedef {import('@ripple-ts/vite-plugin').RenderRoute} RenderRoute
|
|
14
|
+
* @typedef {import('@ripple-ts/vite-plugin').ResolvedRippleConfig} ResolvedRippleConfig
|
|
9
15
|
* @typedef {import('vite').ViteDevServer} ViteDevServer
|
|
10
16
|
*/
|
|
11
17
|
|
|
@@ -22,9 +28,10 @@ import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js
|
|
|
22
28
|
* @param {RenderRoute} route
|
|
23
29
|
* @param {Context} context
|
|
24
30
|
* @param {ViteDevServer} vite
|
|
31
|
+
* @param {ResolvedRippleConfig} [rippleConfig]
|
|
25
32
|
* @returns {Promise<Response>}
|
|
26
33
|
*/
|
|
27
|
-
export async function handleRenderRoute(route, context, vite) {
|
|
34
|
+
export async function handleRenderRoute(route, context, vite, rippleConfig) {
|
|
28
35
|
try {
|
|
29
36
|
// Initialize so the server can register
|
|
30
37
|
// RPC functions from `module server` declarations during SSR module loading
|
|
@@ -36,11 +43,15 @@ export async function handleRenderRoute(route, context, vite) {
|
|
|
36
43
|
const { render, get_css_for_hashes } = await vite.ssrLoadModule('ripple/server');
|
|
37
44
|
|
|
38
45
|
// Load the page component
|
|
39
|
-
const
|
|
40
|
-
const
|
|
46
|
+
const entryPath = get_route_entry_path(route.entry);
|
|
47
|
+
const pageModule = await vite.ssrLoadModule(/** @type {string} */ (entryPath));
|
|
48
|
+
const PageComponent = get_component_export(
|
|
49
|
+
pageModule,
|
|
50
|
+
get_route_entry_export_name(route.entry),
|
|
51
|
+
);
|
|
41
52
|
|
|
42
53
|
if (!PageComponent) {
|
|
43
|
-
throw new Error(`No
|
|
54
|
+
throw new Error(`No component found for route ${route.path}`);
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
// Build the component tree (with optional layout)
|
|
@@ -50,7 +61,7 @@ export async function handleRenderRoute(route, context, vite) {
|
|
|
50
61
|
if (route.layout) {
|
|
51
62
|
// Load layout component
|
|
52
63
|
const layoutModule = await vite.ssrLoadModule(route.layout);
|
|
53
|
-
const LayoutComponent =
|
|
64
|
+
const LayoutComponent = get_component_export(layoutModule, undefined);
|
|
54
65
|
|
|
55
66
|
if (!LayoutComponent) {
|
|
56
67
|
throw new Error(`No default export found in ${route.layout}`);
|
|
@@ -66,7 +77,9 @@ export async function handleRenderRoute(route, context, vite) {
|
|
|
66
77
|
|
|
67
78
|
// Render to HTML
|
|
68
79
|
/** @type {RenderResult} */
|
|
69
|
-
const { head, body, css } = await render(RootComponent
|
|
80
|
+
const { head, body, css } = await render(RootComponent, {
|
|
81
|
+
rootBoundary: rippleConfig?.rootBoundary,
|
|
82
|
+
});
|
|
70
83
|
|
|
71
84
|
// Generate CSS tags
|
|
72
85
|
let cssContent = '';
|
|
@@ -79,7 +92,8 @@ export async function handleRenderRoute(route, context, vite) {
|
|
|
79
92
|
|
|
80
93
|
// Build head content with hydration data
|
|
81
94
|
const routeData = JSON.stringify({
|
|
82
|
-
entry:
|
|
95
|
+
entry: entryPath,
|
|
96
|
+
routeIndex: getRenderRouteIndex(rippleConfig, route),
|
|
83
97
|
params: context.params,
|
|
84
98
|
});
|
|
85
99
|
const headContent = [
|
|
@@ -124,23 +138,17 @@ export async function handleRenderRoute(route, context, vite) {
|
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* @param {Record<string, unknown>} module
|
|
131
|
-
* @returns {Function | null}
|
|
141
|
+
* @param {ResolvedRippleConfig | undefined} config
|
|
142
|
+
* @param {RenderRoute} route
|
|
143
|
+
* @returns {number | undefined}
|
|
132
144
|
*/
|
|
133
|
-
function
|
|
134
|
-
if (
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
// Look for a component-like export (capitalized function)
|
|
138
|
-
for (const [key, value] of Object.entries(module)) {
|
|
139
|
-
if (typeof value === 'function' && /^[A-Z]/.test(key)) {
|
|
140
|
-
return value;
|
|
141
|
-
}
|
|
145
|
+
function getRenderRouteIndex(config, route) {
|
|
146
|
+
if (!config) {
|
|
147
|
+
return undefined;
|
|
142
148
|
}
|
|
143
|
-
|
|
149
|
+
var renderRoutes = config.router.routes.filter((r) => r.type === 'render');
|
|
150
|
+
var index = renderRoutes.indexOf(route);
|
|
151
|
+
return index === -1 ? undefined : index;
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
/**
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
/** @import { Route } from '@ripple-ts/vite-plugin' */
|
|
12
12
|
|
|
13
|
+
import {
|
|
14
|
+
get_route_entry_export_name,
|
|
15
|
+
get_route_entry_id,
|
|
16
|
+
get_route_entry_path,
|
|
17
|
+
} from '../routes.js';
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* @typedef {Object} ClientAssetEntry
|
|
15
21
|
* @property {string} js - Path to the built JS file
|
|
@@ -63,10 +69,11 @@ export function generateServerEntry(options) {
|
|
|
63
69
|
|
|
64
70
|
for (const route of routes) {
|
|
65
71
|
if (route.type === 'render') {
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
const entryPath = get_route_entry_path(route.entry);
|
|
73
|
+
if (entryPath && !component_imports.has(entryPath)) {
|
|
74
|
+
component_imports.set(entryPath, `_page_${component_index++}`);
|
|
68
75
|
}
|
|
69
|
-
if (route.layout && !layout_imports.has(route.layout)) {
|
|
76
|
+
if (typeof route.layout === 'string' && !layout_imports.has(route.layout)) {
|
|
70
77
|
layout_imports.set(route.layout, `_layout_${layout_index++}`);
|
|
71
78
|
}
|
|
72
79
|
}
|
|
@@ -95,12 +102,25 @@ export function generateServerEntry(options) {
|
|
|
95
102
|
|
|
96
103
|
// --- Dynamic map entries ---
|
|
97
104
|
|
|
98
|
-
const component_entries =
|
|
99
|
-
.
|
|
105
|
+
const component_entries = routes
|
|
106
|
+
.filter((route) => route.type === 'render')
|
|
107
|
+
.map((route) => {
|
|
108
|
+
const entryId = get_route_entry_id(route.entry);
|
|
109
|
+
const entryPath = get_route_entry_path(route.entry);
|
|
110
|
+
const exportName = get_route_entry_export_name(route.entry);
|
|
111
|
+
const varName = entryPath ? component_imports.get(entryPath) : undefined;
|
|
112
|
+
|
|
113
|
+
if (!entryId || !varName) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ` ${JSON.stringify(entryId)}: getComponentExport(${varName}, ${JSON.stringify(exportName)}),`;
|
|
118
|
+
})
|
|
119
|
+
.filter(Boolean)
|
|
100
120
|
.join('\n');
|
|
101
121
|
|
|
102
122
|
const layout_entries = [...layout_imports]
|
|
103
|
-
.map(([layout, varName]) => ` ${JSON.stringify(layout)}:
|
|
123
|
+
.map(([layout, varName]) => ` ${JSON.stringify(layout)}: getComponentExport(${varName}),`)
|
|
104
124
|
.join('\n');
|
|
105
125
|
|
|
106
126
|
// Only check _$_server_$_ on modules known to have `module server` declarations.
|
|
@@ -142,7 +162,8 @@ try {
|
|
|
142
162
|
process.exit(1);
|
|
143
163
|
}
|
|
144
164
|
|
|
145
|
-
function
|
|
165
|
+
function getComponentExport(mod, exportName) {
|
|
166
|
+
if (exportName && typeof mod[exportName] === 'function') return mod[exportName];
|
|
146
167
|
if (typeof mod.default === 'function') return mod.default;
|
|
147
168
|
for (const [key, value] of Object.entries(mod)) {
|
|
148
169
|
if (typeof value === 'function' && /^[A-Z]/.test(key)) return value;
|
|
@@ -178,6 +199,7 @@ const handler = createHandler(
|
|
|
178
199
|
middlewares: rippleConfig.middlewares,
|
|
179
200
|
rpcModules,
|
|
180
201
|
trustProxy: rippleConfig.server.trustProxy,
|
|
202
|
+
rootBoundary: rippleConfig.rootBoundary,
|
|
181
203
|
runtime: rippleConfig.adapter.runtime,
|
|
182
204
|
clientAssets,
|
|
183
205
|
},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { create_client_entry_source } from '../src/project-codegen.js';
|
|
3
|
+
import { generateServerEntry } from '../src/server/virtual-entry.js';
|
|
4
|
+
|
|
5
|
+
describe('project codegen', () => {
|
|
6
|
+
it('generates a client entry that resolves route entries from ripple.config.ts', () => {
|
|
7
|
+
const source = create_client_entry_source({
|
|
8
|
+
configPath: '/project/ripple.config.ts',
|
|
9
|
+
staticEntries: ['/src/pages/index.tsrx'],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(source).toContain('import rippleConfig from "/project/ripple.config.ts";');
|
|
13
|
+
expect(source).toContain('"/src/pages/index.tsrx": () => import("/src/pages/index.tsrx")');
|
|
14
|
+
expect(source).not.toContain('route?.component');
|
|
15
|
+
expect(source).toContain('function getRouteEntryPath(entry)');
|
|
16
|
+
expect(source).toContain('return Array.isArray(entry) ? entry[1] : entry;');
|
|
17
|
+
expect(source).toContain('function getRouteEntryExportName(entry)');
|
|
18
|
+
expect(source).toContain('return Array.isArray(entry) ? entry[0] : undefined;');
|
|
19
|
+
expect(source).toContain('const rootBoundary = rippleConfig.rootBoundary;');
|
|
20
|
+
expect(source).toContain('rootBoundary,');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('generates server components from named entry tuples', () => {
|
|
24
|
+
const source = generateServerEntry({
|
|
25
|
+
routes: [
|
|
26
|
+
{
|
|
27
|
+
type: 'render',
|
|
28
|
+
path: '/docs/guide/dom-refs',
|
|
29
|
+
entry: ['DomRefsPage', '/src/pages/docs/guide/dom-refs.tsrx'],
|
|
30
|
+
before: [],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
rippleConfigPath: '/project/ripple.config.ts',
|
|
34
|
+
htmlTemplatePath: './index.html',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(source).toContain('import * as _page_0 from "/src/pages/docs/guide/dom-refs.tsrx";');
|
|
38
|
+
expect(source).toContain(
|
|
39
|
+
'"/src/pages/docs/guide/dom-refs.tsrx#DomRefsPage": getComponentExport(_page_0, "DomRefsPage"),',
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -5,6 +5,19 @@ import path from 'node:path';
|
|
|
5
5
|
import { createHandler } from '../src/server/production.js';
|
|
6
6
|
import { handleRenderRoute } from '../src/server/render-route.js';
|
|
7
7
|
import { createLayoutWrapper, createPropsWrapper } from '../src/server/component-wrappers.js';
|
|
8
|
+
import { RenderRoute } from '../src/routes.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} html
|
|
12
|
+
* @returns {any}
|
|
13
|
+
*/
|
|
14
|
+
function parseRippleData(html) {
|
|
15
|
+
const match = html.match(/<script id="__ripple_data" type="application\/json">([^<]+)<\/script>/);
|
|
16
|
+
if (!match) {
|
|
17
|
+
throw new Error('Missing __ripple_data script');
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(match[1]);
|
|
20
|
+
}
|
|
8
21
|
|
|
9
22
|
function createRuntime() {
|
|
10
23
|
return {
|
|
@@ -29,6 +42,17 @@ function createHandlerOptions() {
|
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
describe('render route SSR props', () => {
|
|
45
|
+
it('allows render routes to prefer a named component export from an entry tuple', () => {
|
|
46
|
+
const route = new RenderRoute({
|
|
47
|
+
path: '/docs/guide/dom-refs',
|
|
48
|
+
entry: ['DomRefsPage', '/src/pages/docs/guide/dom-refs.tsrx'],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(route.entry).toEqual(['DomRefsPage', '/src/pages/docs/guide/dom-refs.tsrx']);
|
|
52
|
+
expect(route.before).toEqual([]);
|
|
53
|
+
expect(() => new RenderRoute({ path: '/missing' })).toThrow('RenderRoute requires an `entry`.');
|
|
54
|
+
});
|
|
55
|
+
|
|
32
56
|
it('passes injected props as the first SSR component argument', () => {
|
|
33
57
|
const Component = vi.fn();
|
|
34
58
|
const Wrapped = createPropsWrapper(Component, {
|
|
@@ -79,6 +103,97 @@ describe('render route SSR props', () => {
|
|
|
79
103
|
});
|
|
80
104
|
});
|
|
81
105
|
|
|
106
|
+
it('passes route params to production render route components from named entry tuples', async () => {
|
|
107
|
+
let pageProps;
|
|
108
|
+
function Page(props) {
|
|
109
|
+
pageProps = props;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const handler = createHandler(
|
|
113
|
+
{
|
|
114
|
+
routes: [
|
|
115
|
+
{
|
|
116
|
+
type: 'render',
|
|
117
|
+
path: '/tool/:slug',
|
|
118
|
+
entry: ['NamedToolPage', '/src/ToolPage.tsrx'],
|
|
119
|
+
before: [],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
components: { '/src/ToolPage.tsrx#NamedToolPage': Page },
|
|
123
|
+
layouts: {},
|
|
124
|
+
middlewares: [],
|
|
125
|
+
runtime: createRuntime(),
|
|
126
|
+
},
|
|
127
|
+
createHandlerOptions(),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const response = await handler(new Request('https://example.test/tool/echo-api'));
|
|
131
|
+
const html = await response.text();
|
|
132
|
+
|
|
133
|
+
expect(response.status).toBe(200);
|
|
134
|
+
expect(pageProps).toEqual({
|
|
135
|
+
fromRender: true,
|
|
136
|
+
params: { slug: 'echo-api' },
|
|
137
|
+
});
|
|
138
|
+
expect(parseRippleData(html)).toEqual({
|
|
139
|
+
entry: '/src/ToolPage.tsrx',
|
|
140
|
+
routeIndex: 0,
|
|
141
|
+
params: { slug: 'echo-api' },
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('passes route params and root boundary to production render route components', async () => {
|
|
146
|
+
let pageProps;
|
|
147
|
+
let renderOptions;
|
|
148
|
+
function Page(props) {
|
|
149
|
+
pageProps = props;
|
|
150
|
+
}
|
|
151
|
+
const rootBoundary = {
|
|
152
|
+
pending() {},
|
|
153
|
+
catch() {},
|
|
154
|
+
};
|
|
155
|
+
const route = {
|
|
156
|
+
type: 'render',
|
|
157
|
+
path: '/tool/:slug',
|
|
158
|
+
entry: '/src/ToolPage.tsrx',
|
|
159
|
+
before: [],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handler = createHandler(
|
|
163
|
+
{
|
|
164
|
+
routes: [route],
|
|
165
|
+
components: { '/src/ToolPage.tsrx': Page },
|
|
166
|
+
layouts: {},
|
|
167
|
+
middlewares: [],
|
|
168
|
+
runtime: createRuntime(),
|
|
169
|
+
rootBoundary,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
...createHandlerOptions(),
|
|
173
|
+
render: async (Component, options) => {
|
|
174
|
+
renderOptions = options;
|
|
175
|
+
Component({ fromRender: true });
|
|
176
|
+
return { head: '', body: '<div>ok</div>', css: new Set() };
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const response = await handler(new Request('https://example.test/tool/echo-api'));
|
|
182
|
+
const html = await response.text();
|
|
183
|
+
|
|
184
|
+
expect(response.status).toBe(200);
|
|
185
|
+
expect(pageProps).toEqual({
|
|
186
|
+
fromRender: true,
|
|
187
|
+
params: { slug: 'echo-api' },
|
|
188
|
+
});
|
|
189
|
+
expect(renderOptions).toEqual({ rootBoundary });
|
|
190
|
+
expect(parseRippleData(html)).toEqual({
|
|
191
|
+
entry: '/src/ToolPage.tsrx',
|
|
192
|
+
routeIndex: 0,
|
|
193
|
+
params: { slug: 'echo-api' },
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
82
197
|
it('passes route params to dev render route components', async () => {
|
|
83
198
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ripple-render-route-'));
|
|
84
199
|
fs.writeFileSync(
|
|
@@ -136,6 +251,147 @@ describe('render route SSR props', () => {
|
|
|
136
251
|
}
|
|
137
252
|
});
|
|
138
253
|
|
|
254
|
+
it('uses the named export from a dev render route entry tuple', async () => {
|
|
255
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ripple-render-route-'));
|
|
256
|
+
fs.writeFileSync(
|
|
257
|
+
path.join(tmpDir, 'index.html'),
|
|
258
|
+
'<html><head><!--ssr-head--></head><body><div id="root"><!--ssr-body--></div></body></html>',
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
let pageProps;
|
|
262
|
+
function Page(props) {
|
|
263
|
+
pageProps = props;
|
|
264
|
+
}
|
|
265
|
+
function WrongPage() {
|
|
266
|
+
throw new Error('default export should not be used');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const vite = {
|
|
270
|
+
config: { root: tmpDir },
|
|
271
|
+
transformIndexHtml: vi.fn(async (_pathname, html) => html),
|
|
272
|
+
ssrLoadModule: vi.fn(async (id) => {
|
|
273
|
+
if (id === 'ripple/server') {
|
|
274
|
+
const { render } = createHandlerOptions();
|
|
275
|
+
return {
|
|
276
|
+
render,
|
|
277
|
+
get_css_for_hashes: () => '',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (id === '/src/ToolPage.tsrx') {
|
|
281
|
+
return { default: WrongPage, NamedToolPage: Page };
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`Unexpected module: ${id}`);
|
|
284
|
+
}),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const response = await handleRenderRoute(
|
|
289
|
+
{
|
|
290
|
+
type: 'render',
|
|
291
|
+
path: '/tool/:slug',
|
|
292
|
+
entry: ['NamedToolPage', '/src/ToolPage.tsrx'],
|
|
293
|
+
before: [],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
request: new Request('https://example.test/tool/echo-api'),
|
|
297
|
+
params: { slug: 'echo-api' },
|
|
298
|
+
url: new URL('https://example.test/tool/echo-api'),
|
|
299
|
+
state: new Map(),
|
|
300
|
+
},
|
|
301
|
+
vite,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(response.status).toBe(200);
|
|
305
|
+
expect(pageProps).toEqual({
|
|
306
|
+
fromRender: true,
|
|
307
|
+
params: { slug: 'echo-api' },
|
|
308
|
+
});
|
|
309
|
+
expect(vite.ssrLoadModule).toHaveBeenCalledWith('/src/ToolPage.tsrx');
|
|
310
|
+
} finally {
|
|
311
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('passes route params and root boundary to dev render route components', async () => {
|
|
316
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ripple-render-route-'));
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
path.join(tmpDir, 'index.html'),
|
|
319
|
+
'<html><head><!--ssr-head--></head><body><div id="root"><!--ssr-body--></div></body></html>',
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
let pageProps;
|
|
323
|
+
let renderOptions;
|
|
324
|
+
function Page(props) {
|
|
325
|
+
pageProps = props;
|
|
326
|
+
}
|
|
327
|
+
const rootBoundary = {
|
|
328
|
+
pending() {},
|
|
329
|
+
catch() {},
|
|
330
|
+
};
|
|
331
|
+
const route = {
|
|
332
|
+
type: 'render',
|
|
333
|
+
path: '/tool/:slug',
|
|
334
|
+
entry: '/src/ToolPage.tsrx',
|
|
335
|
+
before: [],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const vite = {
|
|
339
|
+
config: { root: tmpDir },
|
|
340
|
+
transformIndexHtml: vi.fn(async (_pathname, html) => html),
|
|
341
|
+
ssrLoadModule: vi.fn(async (id) => {
|
|
342
|
+
if (id === 'ripple/server') {
|
|
343
|
+
return {
|
|
344
|
+
render: async (Component, options) => {
|
|
345
|
+
renderOptions = options;
|
|
346
|
+
Component({ fromRender: true });
|
|
347
|
+
return { head: '', body: '<div>ok</div>', css: new Set() };
|
|
348
|
+
},
|
|
349
|
+
get_css_for_hashes: () => '',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (id === '/src/ToolPage.tsrx') {
|
|
353
|
+
return { default: Page };
|
|
354
|
+
}
|
|
355
|
+
throw new Error(`Unexpected module: ${id}`);
|
|
356
|
+
}),
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const response = await handleRenderRoute(
|
|
361
|
+
route,
|
|
362
|
+
{
|
|
363
|
+
request: new Request('https://example.test/tool/echo-api'),
|
|
364
|
+
params: { slug: 'echo-api' },
|
|
365
|
+
url: new URL('https://example.test/tool/echo-api'),
|
|
366
|
+
state: new Map(),
|
|
367
|
+
},
|
|
368
|
+
vite,
|
|
369
|
+
{
|
|
370
|
+
router: { routes: [route] },
|
|
371
|
+
rootBoundary,
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
const html = await response.text();
|
|
375
|
+
|
|
376
|
+
expect(response.status).toBe(200);
|
|
377
|
+
expect(pageProps).toEqual({
|
|
378
|
+
fromRender: true,
|
|
379
|
+
params: { slug: 'echo-api' },
|
|
380
|
+
});
|
|
381
|
+
expect(renderOptions).toEqual({ rootBoundary });
|
|
382
|
+
expect(parseRippleData(html)).toEqual({
|
|
383
|
+
entry: '/src/ToolPage.tsrx',
|
|
384
|
+
routeIndex: 0,
|
|
385
|
+
params: { slug: 'echo-api' },
|
|
386
|
+
});
|
|
387
|
+
expect(vite.ssrLoadModule).toHaveBeenCalledTimes(2);
|
|
388
|
+
expect(vite.ssrLoadModule).toHaveBeenCalledWith('ripple/server');
|
|
389
|
+
expect(vite.ssrLoadModule).toHaveBeenCalledWith('/src/ToolPage.tsrx');
|
|
390
|
+
} finally {
|
|
391
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
139
395
|
it('wraps layout children as a server TSRX element and passes page params through', () => {
|
|
140
396
|
const Page = vi.fn();
|
|
141
397
|
const Layout = vi.fn(({ children }) => {
|