vike-ripple 0.1.0
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/README.md +92 -0
- package/package.json +30 -0
- package/src/index.js +17 -0
- package/src/renderer/+config.ts +10 -0
- package/src/renderer/+onRenderClient.tsx +29 -0
- package/src/renderer/+onRenderHtml.tsx +43 -0
- package/src/setup.js +123 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @vike-ripple/vike-ripple
|
|
2
|
+
|
|
3
|
+
[Vike](https://vike.dev) integration for [Ripple TS](https://ripple-ts.com) — SSR rendering, client hydration with mount fallback, streaming, `<head>` management, and `.tsrx` page file support.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @vike-ripple/vike-ripple
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
### 1. Run setup (patches Vike + Ripple)
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx vike-ripple setup
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or add to your project's `package.json` so it runs automatically after `npm install`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
"scripts": {
|
|
23
|
+
"postinstall": "vike-ripple setup"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Add plugin to `vite.config.ts`
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { defineConfig } from 'vite'
|
|
31
|
+
import vike from 'vike/plugin'
|
|
32
|
+
import { ripple } from '@ripple-ts/vite-plugin'
|
|
33
|
+
import vikeRipple from '@vike-ripple/vike-ripple'
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
optimizeDeps: {
|
|
37
|
+
exclude: ['ripple'],
|
|
38
|
+
},
|
|
39
|
+
plugins: [
|
|
40
|
+
vikeRipple(),
|
|
41
|
+
ripple({ excludeRippleExternalModules: true }),
|
|
42
|
+
vike(),
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
> **Why `optimizeDeps.exclude: ['ripple']`?** Ripple uses module-scoped variables (`first_child_getter`) shared between `hydrate()` and DOM traversal functions. Vite's dependency optimization splits these into separate bundles, breaking the scope sharing and causing `TypeError: Cannot read properties of undefined (reading 'call')` at `get_first_child` during hydration. Excluding `ripple` from optimization ensures all Ripple internals stay in one module scope.
|
|
46
|
+
|
|
47
|
+
### 3. Add renderer files
|
|
48
|
+
|
|
49
|
+
Copy from `node_modules/@vike-ripple/vike-ripple/src/renderer/` to your project's `renderer/`:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
renderer/
|
|
53
|
+
+config.ts
|
|
54
|
+
+onRenderHtml.tsx
|
|
55
|
+
+onRenderClient.tsx
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 4. Create a page
|
|
59
|
+
|
|
60
|
+
```tsrx
|
|
61
|
+
// pages/index/+Page.tsrx
|
|
62
|
+
export function Page(props: {}) @{
|
|
63
|
+
<>
|
|
64
|
+
<head>
|
|
65
|
+
<title>Home</title>
|
|
66
|
+
</head>
|
|
67
|
+
<section class="min-h-screen p-8">
|
|
68
|
+
<h1>Hello, Ripple + Vike!</h1>
|
|
69
|
+
</section>
|
|
70
|
+
</>
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What this does
|
|
75
|
+
|
|
76
|
+
| Patch | Why |
|
|
77
|
+
|---|---|
|
|
78
|
+
| **`.tsrx` extension** | Vike doesn't know `.tsrx` is a valid page extension — adds it to `isScriptFile.js` |
|
|
79
|
+
| **`?direct` CSS loading** | Vite's SSR module loader appends `?direct` to module IDs; Ripple's `load` hook checks cache with the wrong key |
|
|
80
|
+
| **Hydrate → mount fallback** | Ripple's `hydrate` can mismatch when `<title>` or `<head>` content is extracted during SSR but missing from the client DOM; falls back to `mount` gracefully |
|
|
81
|
+
|
|
82
|
+
## API
|
|
83
|
+
|
|
84
|
+
### `vikeRipple()`
|
|
85
|
+
|
|
86
|
+
Vite plugin. Must be placed before `ripple()` in the plugins array, with `enforce: 'pre'` behavior.
|
|
87
|
+
|
|
88
|
+
### Renderer files
|
|
89
|
+
|
|
90
|
+
- **`+onRenderHtml.tsx`** — SSR via `ripple/server`'s `render()`, extracts `<head>`, `<body>`, and CSS, injects them into Vike's HTML template. Supports streaming via `rippleStream` config.
|
|
91
|
+
- **`+onRenderClient.tsx`** — Hydrates with `hydrate()` from `ripple`, falls back to `mount()` on error. Imports `tailwind.css` if present.
|
|
92
|
+
- **`+config.ts`** — Disables prerender by default, registers `rippleStream` meta config.
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vike-ripple",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vike (vike.dev) integration for Ripple TS — SSR, CSR, streaming, head management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./setup": "./src/setup.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"vike-ripple": "src/setup.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"vike",
|
|
19
|
+
"ripple",
|
|
20
|
+
"ripplets",
|
|
21
|
+
"ssr",
|
|
22
|
+
"vite"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"vike": ">=0.4.259",
|
|
27
|
+
"@ripple-ts/vite-plugin": ">=0.3.0",
|
|
28
|
+
"ripple": ">=0.1.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vike-ripple/vike-ripple — Vike integration for Ripple TS.
|
|
3
|
+
*
|
|
4
|
+
* ## Setup
|
|
5
|
+
* Run once: npx vike-ripple setup
|
|
6
|
+
* Or add to package.json: "postinstall": "vike-ripple setup"
|
|
7
|
+
*
|
|
8
|
+
* Then add the Vite plugin to vite.config.ts:
|
|
9
|
+
* import vikeRipple from '@vike-ripple/vike-ripple'
|
|
10
|
+
* // in plugins: vikeRipple(),
|
|
11
|
+
*/
|
|
12
|
+
export default function vikeRipple() {
|
|
13
|
+
return {
|
|
14
|
+
name: 'vike-ripple',
|
|
15
|
+
enforce: 'pre',
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// https://vike.dev/onRenderClient
|
|
2
|
+
export { onRenderClient }
|
|
3
|
+
|
|
4
|
+
import { hydrate } from 'ripple'
|
|
5
|
+
|
|
6
|
+
let rendered = false
|
|
7
|
+
|
|
8
|
+
const onRenderClient = async (pageContext) => {
|
|
9
|
+
const { Page } = pageContext
|
|
10
|
+
if (!Page) return
|
|
11
|
+
|
|
12
|
+
const container = document.getElementById('root')
|
|
13
|
+
if (!container) return
|
|
14
|
+
|
|
15
|
+
if (pageContext.isHydration && container.innerHTML !== '') {
|
|
16
|
+
try {
|
|
17
|
+
hydrate(Page, { target: container, props: {} })
|
|
18
|
+
rendered = true
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.warn('[ripple] hydrate failed, falling back to mount:', err)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!rendered) {
|
|
25
|
+
const { mount } = await import('ripple')
|
|
26
|
+
mount(Page, { target: container, props: {} })
|
|
27
|
+
rendered = true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// https://vike.dev/onRenderHtml
|
|
2
|
+
export { onRenderHtml }
|
|
3
|
+
|
|
4
|
+
import { render, create_ssr_stream } from 'ripple/server'
|
|
5
|
+
import { escapeInject, dangerouslySkipEscape } from 'vike/server'
|
|
6
|
+
|
|
7
|
+
const onRenderHtml = async (pageContext) => {
|
|
8
|
+
const { Page } = pageContext
|
|
9
|
+
if (!Page) throw new Error('No Page')
|
|
10
|
+
|
|
11
|
+
const enableStream = !!(pageContext.config.rippleStream ?? pageContext.config.stream)
|
|
12
|
+
|
|
13
|
+
if (enableStream) {
|
|
14
|
+
const rippleStream = create_ssr_stream()
|
|
15
|
+
render(Page, { stream: rippleStream.sink }).catch(e => {
|
|
16
|
+
console.error('[ripple] render err:', e?.message)
|
|
17
|
+
})
|
|
18
|
+
return escapeInject`<!DOCTYPE html>
|
|
19
|
+
<html>
|
|
20
|
+
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head>
|
|
21
|
+
<body><div id="root">${rippleStream.stream}</div></body>
|
|
22
|
+
</html>`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { head, body, css } = await render(Page, {})
|
|
26
|
+
|
|
27
|
+
const cssHtml = css?.size
|
|
28
|
+
? `<style data-ripple-ssr>${[...css].join('')}<` + `/style>`
|
|
29
|
+
: ''
|
|
30
|
+
|
|
31
|
+
return escapeInject`<!DOCTYPE html>
|
|
32
|
+
<html>
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="UTF-8" />
|
|
35
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
36
|
+
${dangerouslySkipEscape(head)}
|
|
37
|
+
${dangerouslySkipEscape(cssHtml)}
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div id="root">${dangerouslySkipEscape(body)}</div>
|
|
41
|
+
</body>
|
|
42
|
+
</html>`
|
|
43
|
+
}
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* vike-ripple setup — patches Vike and Ripple for .tsrx support.
|
|
4
|
+
*
|
|
5
|
+
* Run once: npx vike-ripple setup
|
|
6
|
+
* Or add to project's package.json: "postinstall": "vike-ripple setup"
|
|
7
|
+
*/
|
|
8
|
+
import { createRequire } from 'module'
|
|
9
|
+
import { join, dirname } from 'path'
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs'
|
|
11
|
+
import { fileURLToPath } from 'url'
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
const projectRoot = process.cwd()
|
|
15
|
+
let exitCode = 0
|
|
16
|
+
|
|
17
|
+
function log(msg) { console.log('[vike-ripple]', msg) }
|
|
18
|
+
function warn(msg) { console.warn('[vike-ripple]', msg) }
|
|
19
|
+
|
|
20
|
+
// ── Patch 1: Register .tsrx with Vike ─────────────────────────
|
|
21
|
+
function patchVikeExtensions() {
|
|
22
|
+
const target = resolveVike('dist/utils/isScriptFile.js')
|
|
23
|
+
if (!target) { warn('vike not found — skipping'); return }
|
|
24
|
+
|
|
25
|
+
let src = readFileSync(target, 'utf-8')
|
|
26
|
+
if (src.includes("'tsrx'")) { log('.tsrx already registered with Vike'); return }
|
|
27
|
+
|
|
28
|
+
const patched = src.replace(
|
|
29
|
+
'const scriptFileExtensionList = [...extJsOrTs, ...extJsxOrTsx, ...extTemplates];',
|
|
30
|
+
"const scriptFileExtensionList = [...extJsOrTs, ...extJsxOrTsx, ...extTemplates, 'tsrx'];",
|
|
31
|
+
)
|
|
32
|
+
if (patched === src) { warn('Could not patch Vike isScriptFile.js'); exitCode = 1; return }
|
|
33
|
+
writeFileSync(target, patched, 'utf-8')
|
|
34
|
+
log('Registered .tsrx extension with Vike')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Patch 2: Fix ?direct in Ripple's load hook ────────────────
|
|
38
|
+
function patchRippleDirect() {
|
|
39
|
+
const target = resolveRipple('src/index.js')
|
|
40
|
+
if (!target) { warn('@ripple-ts/vite-plugin not found — skipping'); return }
|
|
41
|
+
|
|
42
|
+
let src = readFileSync(target, 'utf-8')
|
|
43
|
+
if (src.includes('Handle ?direct query param')) { log('?direct fix already applied'); return }
|
|
44
|
+
|
|
45
|
+
const patched = src.replace(
|
|
46
|
+
'if (cssCache.has(id)) {\n\t\t\t\t\treturn cssCache.get(id);\n\t\t\t\t}',
|
|
47
|
+
`if (cssCache.has(id)) {
|
|
48
|
+
return cssCache.get(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle ?direct query param added by Vite's SSR module loading
|
|
52
|
+
if (id.includes('?direct')) {
|
|
53
|
+
const baseId = id.replace('?direct', '');
|
|
54
|
+
if (cssCache.has(baseId)) {
|
|
55
|
+
return cssCache.get(baseId);
|
|
56
|
+
}
|
|
57
|
+
}`,
|
|
58
|
+
)
|
|
59
|
+
if (patched === src) { warn('Could not patch Ripple plugin load hook'); exitCode = 1; return }
|
|
60
|
+
writeFileSync(target, patched, 'utf-8')
|
|
61
|
+
log('Patched Ripple plugin for ?direct CSS module loading')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Patch 3: @apply support (tailwindcss integration) ──────────
|
|
65
|
+
function patchRippleApply() {
|
|
66
|
+
const target = resolveRipple('src/index.js')
|
|
67
|
+
if (!target) return
|
|
68
|
+
|
|
69
|
+
let src = readFileSync(target, 'utf-8')
|
|
70
|
+
|
|
71
|
+
if (src.includes('TW_PATCH_APPLY')) { log('@apply patch already applied'); return }
|
|
72
|
+
|
|
73
|
+
// Upgrade from old TW_PATCH format
|
|
74
|
+
if (src.includes('TW_PATCH:')) {
|
|
75
|
+
src = src.replace(
|
|
76
|
+
'// TW_PATCH: prepend tailwindcss so @apply works',
|
|
77
|
+
'// TW_PATCH_APPLY: bring tailwindcss into scope for @apply',
|
|
78
|
+
)
|
|
79
|
+
writeFileSync(target, src, 'utf-8')
|
|
80
|
+
log('Upgraded @apply patch format')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fresh install — original unpatched code
|
|
85
|
+
const orig = (
|
|
86
|
+
'\t\t\t\t\tif (css) {\n' +
|
|
87
|
+
'\t\t\t\t\t\tconst cssId = createVirtualImportId(filename, root, \'style\');\n' +
|
|
88
|
+
'\t\t\t\t\t\tcssCache.set(cssId, css);'
|
|
89
|
+
)
|
|
90
|
+
const patched = (
|
|
91
|
+
'\t\t\t\t\tif (css) {\n' +
|
|
92
|
+
'\t\t\t\t\t\t// TW_PATCH_APPLY: bring tailwindcss into scope for @apply\n' +
|
|
93
|
+
"\t\t\t\t\t\tcss = '@import \"tailwindcss\";\\n' + css;\n" +
|
|
94
|
+
'\t\t\t\t\t\tconst cssId = createVirtualImportId(filename, root, \'style\');\n' +
|
|
95
|
+
'\t\t\t\t\t\tcssCache.set(cssId, css);'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const result = src.replace(orig, patched)
|
|
99
|
+
if (result === src) { warn('Could not patch Ripple plugin for @apply'); return }
|
|
100
|
+
writeFileSync(target, result, 'utf-8')
|
|
101
|
+
log('Patched Ripple plugin for @apply support in <style> blocks')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Resolve helpers ────────────────────────────────────────────
|
|
105
|
+
function resolveVike(rel) {
|
|
106
|
+
const p = join(projectRoot, 'node_modules', 'vike', rel)
|
|
107
|
+
if (existsSync(p)) return p
|
|
108
|
+
try { return createRequire(join(projectRoot, 'noop.js')).resolve('vike/' + rel) } catch { return null }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveRipple(rel) {
|
|
112
|
+
const p = join(projectRoot, 'node_modules', '@ripple-ts', 'vite-plugin', rel)
|
|
113
|
+
if (existsSync(p)) return p
|
|
114
|
+
try { return createRequire(join(projectRoot, 'noop.js')).resolve('@ripple-ts/vite-plugin/' + rel) } catch { return null }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Main ──────────────────────────────────────────────────────
|
|
118
|
+
log('Applying patches...')
|
|
119
|
+
patchVikeExtensions()
|
|
120
|
+
patchRippleDirect()
|
|
121
|
+
patchRippleApply()
|
|
122
|
+
log('Done')
|
|
123
|
+
process.exit(exitCode)
|