@ripple-ts/vite-plugin 0.3.35 → 0.3.37

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @ripple-ts/vite-plugin
2
2
 
3
+ ## 0.3.37
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ [[`c631ab0`](https://github.com/Ripple-TS/ripple/commit/c631ab0076b7e2cb30f4998101b54c3a86e78c61)]:
9
+ - @tsrx/ripple@0.0.19
10
+ - @ripple-ts/adapter@0.3.37
11
+
12
+ ## 0.3.36
13
+
14
+ ### Patch Changes
15
+
16
+ - [#999](https://github.com/Ripple-TS/ripple/pull/999)
17
+ [`aa6628b`](https://github.com/Ripple-TS/ripple/commit/aa6628b3318f1bdad6a6e12286d3002f8d591e2e)
18
+ Thanks [@trueadm](https://github.com/trueadm)! - Pass RenderRoute params to
19
+ SSR-compiled page components.
20
+
21
+ - Updated dependencies []:
22
+ - @tsrx/ripple@0.0.18
23
+ - @ripple-ts/adapter@0.3.36
24
+
3
25
  ## 0.3.35
4
26
 
5
27
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Vite plugin for Ripple",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.35",
6
+ "version": "0.3.37",
7
7
  "type": "module",
8
8
  "module": "src/index.js",
9
9
  "main": "src/index.js",
@@ -32,14 +32,14 @@
32
32
  "url": "https://github.com/Ripple-TS/ripple/issues"
33
33
  },
34
34
  "dependencies": {
35
- "@ripple-ts/adapter": "0.3.35",
36
- "@tsrx/ripple": "0.0.17"
35
+ "@tsrx/ripple": "0.0.19",
36
+ "@ripple-ts/adapter": "0.3.37"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^24.3.0",
40
40
  "type-fest": "^5.6.0",
41
41
  "vite": "^8.0.0",
42
- "ripple": "0.3.35"
42
+ "ripple": "0.3.37"
43
43
  },
44
44
  "publishConfig": {
45
45
  "access": "public"
@@ -0,0 +1,51 @@
1
+ const TSRX_ELEMENT = Symbol.for('ripple.element');
2
+
3
+ /**
4
+ * Create a server TSRX element for layout children.
5
+ *
6
+ * @param {Function} render
7
+ * @returns {{ render: Function, [TSRX_ELEMENT]: true }}
8
+ */
9
+ function createServerTsrxElement(render) {
10
+ return {
11
+ render,
12
+ [TSRX_ELEMENT]: true,
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Create a wrapper component that injects props into an SSR component.
18
+ *
19
+ * @param {Function} Component
20
+ * @param {Record<string, unknown>} props
21
+ * @returns {Function}
22
+ */
23
+ export function createPropsWrapper(Component, props) {
24
+ /**
25
+ * @param {Record<string, unknown>} additionalProps
26
+ */
27
+ return function WrappedComponent(additionalProps = {}) {
28
+ return Component({ ...additionalProps, ...props });
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Create a wrapper that composes a layout with a page component.
34
+ *
35
+ * @param {Function} Layout
36
+ * @param {Function} Page
37
+ * @param {Record<string, unknown>} pageProps
38
+ * @returns {Function}
39
+ */
40
+ export function createLayoutWrapper(Layout, Page, pageProps) {
41
+ /**
42
+ * @param {Record<string, unknown>} additionalProps
43
+ */
44
+ return function LayoutWrapper(additionalProps = {}) {
45
+ const children = createServerTsrxElement((childProps = {}) => {
46
+ return Page({ ...additionalProps, ...childProps, ...pageProps });
47
+ });
48
+
49
+ return Layout({ ...additionalProps, children });
50
+ };
51
+ }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { createRouter } from './router.js';
13
13
  import { createContext, runMiddlewareChain } from './middleware.js';
14
+ import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
14
15
  import {
15
16
  patch_global_fetch,
16
17
  build_rpc_lookup,
@@ -253,38 +254,6 @@ async function handleServerRoute(route, context, globalMiddlewares) {
253
254
  );
254
255
  }
255
256
 
256
- // ============================================================================
257
- // Component wrappers
258
- // ============================================================================
259
-
260
- /**
261
- * Create a wrapper component that injects props
262
- * @param {Function} Component
263
- * @param {Record<string, unknown>} props
264
- * @returns {Function}
265
- */
266
- function createPropsWrapper(Component, props) {
267
- return function WrappedComponent(/** @type {unknown} */ output, additionalProps = {}) {
268
- return Component(output, { ...additionalProps, ...props });
269
- };
270
- }
271
-
272
- /**
273
- * Create a wrapper that composes a layout with a page component
274
- * @param {Function} Layout
275
- * @param {Function} Page
276
- * @param {Record<string, unknown>} pageProps
277
- * @returns {Function}
278
- */
279
- function createLayoutWrapper(Layout, Page, pageProps) {
280
- return function LayoutWrapper(/** @type {unknown} */ output, additionalProps = {}) {
281
- const children = (/** @type {unknown} */ childOutput) => {
282
- return Page(childOutput, { ...additionalProps, ...pageProps });
283
- };
284
- return Layout(output, { ...additionalProps, children });
285
- };
286
- }
287
-
288
257
  // ============================================================================
289
258
  // Utilities
290
259
  // ============================================================================
@@ -1,6 +1,7 @@
1
1
  /// <reference types="@tsrx/ripple/types/rpc" />
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
+ import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
4
5
 
5
6
  /**
6
7
  * @typedef {import('@ripple-ts/vite-plugin').Context} Context
@@ -142,47 +143,6 @@ function getDefaultExport(module) {
142
143
  return null;
143
144
  }
144
145
 
145
- /**
146
- * Create a wrapper component that injects props
147
- *
148
- * @param {Function} Component
149
- * @param {Record<string, unknown>} props
150
- * @returns {Function}
151
- */
152
- function createPropsWrapper(Component, props) {
153
- /**
154
- * @param {unknown} output
155
- * @param {Record<string, unknown>} additionalProps
156
- */
157
- return function WrappedComponent(output, additionalProps = {}) {
158
- return Component(output, { ...additionalProps, ...props });
159
- };
160
- }
161
-
162
- /**
163
- * Create a wrapper that composes a layout with a page component
164
- * The layout receives the page as its children prop
165
- *
166
- * @param {Function} Layout
167
- * @param {Function} Page
168
- * @param {Record<string, unknown>} pageProps
169
- * @returns {Function}
170
- */
171
- function createLayoutWrapper(Layout, Page, pageProps) {
172
- /**
173
- * @param {unknown} output
174
- * @param {Record<string, unknown>} additionalProps
175
- */
176
- return function LayoutWrapper(output, additionalProps = {}) {
177
- // Children is a component function that renders the page
178
- const children = (/** @type {unknown} */ childOutput) => {
179
- return Page(childOutput, { ...additionalProps, ...pageProps });
180
- };
181
-
182
- return Layout(output, { ...additionalProps, children });
183
- };
184
- }
185
-
186
146
  /**
187
147
  * Escape script content to prevent XSS
188
148
  * @param {string} str
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createHandler } from '../src/server/production.js';
6
+ import { handleRenderRoute } from '../src/server/render-route.js';
7
+ import { createLayoutWrapper, createPropsWrapper } from '../src/server/component-wrappers.js';
8
+
9
+ function createRuntime() {
10
+ return {
11
+ hash: () => '00000000',
12
+ createAsyncContext: () => ({
13
+ run: (_store, fn) => fn(),
14
+ getStore: () => undefined,
15
+ }),
16
+ };
17
+ }
18
+
19
+ function createHandlerOptions() {
20
+ return {
21
+ render: async (Component) => {
22
+ Component({ fromRender: true });
23
+ return { head: '', body: '<div>ok</div>', css: new Set() };
24
+ },
25
+ getCss: () => '',
26
+ htmlTemplate: '<html><head><!--ssr-head--></head><body><!--ssr-body--></body></html>',
27
+ executeServerFunction: async () => '',
28
+ };
29
+ }
30
+
31
+ describe('render route SSR props', () => {
32
+ it('passes injected props as the first SSR component argument', () => {
33
+ const Component = vi.fn();
34
+ const Wrapped = createPropsWrapper(Component, {
35
+ params: { slug: 'echo-api' },
36
+ });
37
+
38
+ Wrapped({ fromRender: true, params: { slug: 'ignored' } });
39
+
40
+ expect(Component.mock.calls).toEqual([
41
+ [
42
+ {
43
+ fromRender: true,
44
+ params: { slug: 'echo-api' },
45
+ },
46
+ ],
47
+ ]);
48
+ });
49
+
50
+ it('passes route params to production render route components', async () => {
51
+ let pageProps;
52
+ function Page(props) {
53
+ pageProps = props;
54
+ }
55
+
56
+ const handler = createHandler(
57
+ {
58
+ routes: [
59
+ {
60
+ type: 'render',
61
+ path: '/tool/:slug',
62
+ entry: '/src/ToolPage.tsrx',
63
+ before: [],
64
+ },
65
+ ],
66
+ components: { '/src/ToolPage.tsrx': Page },
67
+ layouts: {},
68
+ middlewares: [],
69
+ runtime: createRuntime(),
70
+ },
71
+ createHandlerOptions(),
72
+ );
73
+
74
+ await handler(new Request('https://example.test/tool/echo-api'));
75
+
76
+ expect(pageProps).toEqual({
77
+ fromRender: true,
78
+ params: { slug: 'echo-api' },
79
+ });
80
+ });
81
+
82
+ it('passes route params to dev render route components', async () => {
83
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ripple-render-route-'));
84
+ fs.writeFileSync(
85
+ path.join(tmpDir, 'index.html'),
86
+ '<html><head><!--ssr-head--></head><body><div id="root"><!--ssr-body--></div></body></html>',
87
+ );
88
+
89
+ let pageProps;
90
+ function Page(props) {
91
+ pageProps = props;
92
+ }
93
+
94
+ const vite = {
95
+ config: { root: tmpDir },
96
+ transformIndexHtml: vi.fn(async (_pathname, html) => html),
97
+ ssrLoadModule: vi.fn(async (id) => {
98
+ if (id === 'ripple/server') {
99
+ const { render } = createHandlerOptions();
100
+ return {
101
+ render,
102
+ get_css_for_hashes: () => '',
103
+ };
104
+ }
105
+ if (id === '/src/ToolPage.tsrx') {
106
+ return { default: Page };
107
+ }
108
+ throw new Error(`Unexpected module: ${id}`);
109
+ }),
110
+ };
111
+
112
+ try {
113
+ const response = await handleRenderRoute(
114
+ {
115
+ type: 'render',
116
+ path: '/tool/:slug',
117
+ entry: '/src/ToolPage.tsrx',
118
+ before: [],
119
+ },
120
+ {
121
+ request: new Request('https://example.test/tool/echo-api'),
122
+ params: { slug: 'echo-api' },
123
+ url: new URL('https://example.test/tool/echo-api'),
124
+ state: new Map(),
125
+ },
126
+ vite,
127
+ );
128
+
129
+ expect(response.status).toBe(200);
130
+ expect(pageProps).toEqual({
131
+ fromRender: true,
132
+ params: { slug: 'echo-api' },
133
+ });
134
+ } finally {
135
+ fs.rmSync(tmpDir, { recursive: true, force: true });
136
+ }
137
+ });
138
+
139
+ it('wraps layout children as a server TSRX element and passes page params through', () => {
140
+ const Page = vi.fn();
141
+ const Layout = vi.fn(({ children }) => {
142
+ children.render({ fromChild: true });
143
+ });
144
+ const Wrapped = createLayoutWrapper(Layout, Page, {
145
+ params: { slug: 'echo-api' },
146
+ });
147
+
148
+ Wrapped({ fromRender: true });
149
+
150
+ const layoutProps = Layout.mock.calls[0][0];
151
+ expect(layoutProps.fromRender).toBe(true);
152
+ expect(layoutProps.children[Symbol.for('ripple.element')]).toBe(true);
153
+ expect(Page.mock.calls).toEqual([
154
+ [
155
+ {
156
+ fromRender: true,
157
+ fromChild: true,
158
+ params: { slug: 'echo-api' },
159
+ },
160
+ ],
161
+ ]);
162
+ });
163
+ });