vike-ripple 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,26 +1,26 @@
1
1
  {
2
2
  "name": "vike-ripple",
3
- "version": "0.1.0",
4
- "description": "Vike (vike.dev) integration for Ripple TS — SSR, CSR, streaming, head management",
3
+ "version": "0.2.0",
4
+ "description": "Vike extension for Ripple TS — full parity with vike-react/vike-solid/vike-vue",
5
5
  "type": "module",
6
- "main": "./src/index.js",
6
+ "types": "./src/types/Config.ts",
7
7
  "exports": {
8
8
  ".": "./src/index.js",
9
- "./setup": "./src/setup.js"
9
+ "./config": "./src/+config.js",
10
+ "./setup": "./src/setup.js",
11
+ "./Head": "./src/components/Head/Head-server.js",
12
+ "./ClientOnly": "./src/components/ClientOnly.js",
13
+ "./usePageContext": "./src/hooks/usePageContext.js",
14
+ "./useData": "./src/hooks/useData.js",
15
+ "./useConfig": "./src/hooks/useConfig.js",
16
+ "./src/integration/onRenderHtml.js": "./src/integration/onRenderHtml.js",
17
+ "./src/integration/onRenderClient.js": "./src/integration/onRenderClient.js"
10
18
  },
11
19
  "bin": {
12
- "vike-ripple": "src/setup.js"
20
+ "vike-ripple": "./src/setup.js"
13
21
  },
14
- "files": [
15
- "src"
16
- ],
17
- "keywords": [
18
- "vike",
19
- "ripple",
20
- "ripplets",
21
- "ssr",
22
- "vite"
23
- ],
22
+ "files": ["src"],
23
+ "keywords": ["vike", "ripple", "ripplets", "ssr", "vite"],
24
24
  "license": "MIT",
25
25
  "peerDependencies": {
26
26
  "vike": ">=0.4.259",
package/src/+config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { ssrEffect } from './integration/ssrEffect.js'
2
+
3
+ /** @type {import('vike/types').Config} */
4
+ const config = {
5
+ name: 'vike-ripple',
6
+ require: { vike: '>=0.4.250' },
7
+
8
+ onRenderHtml: 'import:vike-ripple/src/integration/onRenderHtml.js:onRenderHtml',
9
+ onRenderClient: 'import:vike-ripple/src/integration/onRenderClient.js:onRenderClient',
10
+
11
+ clientRouting: true,
12
+ hydrationCanBeAborted: true,
13
+
14
+ passToClient: ['_configViaHook'],
15
+
16
+ meta: {
17
+ Head: { env: { server: true }, cumulative: true },
18
+ Layout: { env: { server: true, client: true }, cumulative: true },
19
+ title: { env: { server: true, client: true } },
20
+ description: { env: { server: true } },
21
+ image: { env: { server: true } },
22
+ viewport: { env: { server: true } },
23
+ favicon: { env: { server: true }, global: true },
24
+ lang: { env: { server: true, client: true } },
25
+ ssr: { env: { config: true }, effect: ssrEffect },
26
+ stream: { env: { server: true }, cumulative: true },
27
+ onBeforeRenderHtml: { env: { server: true }, cumulative: true },
28
+ onAfterRenderHtml: { env: { server: true }, cumulative: true },
29
+ onBeforeRenderClient: { env: { server: false, client: true }, cumulative: true },
30
+ onAfterRenderClient: { env: { server: false, client: true }, cumulative: true },
31
+ bodyHtmlBegin: { env: { server: true }, cumulative: true, global: true },
32
+ bodyHtmlEnd: { env: { server: true }, cumulative: true, global: true },
33
+ headHtmlBegin: { env: { server: true }, cumulative: true, global: true },
34
+ headHtmlEnd: { env: { server: true }, cumulative: true, global: true },
35
+ htmlAttributes: { env: { server: true }, global: true, cumulative: true },
36
+ bodyAttributes: { env: { server: true }, global: true, cumulative: true },
37
+ },
38
+ }
39
+
40
+ export default config
@@ -0,0 +1,5 @@
1
+ export { ClientOnly }
2
+
3
+ function ClientOnly({ children }) {
4
+ return children
5
+ }
@@ -0,0 +1,3 @@
1
+ export function Config() {
2
+ return null
3
+ }
@@ -0,0 +1,6 @@
1
+ import { useConfig } from '../hooks/useConfig.js'
2
+
3
+ export function Config(values) {
4
+ useConfig()(values)
5
+ return null
6
+ }
@@ -0,0 +1,3 @@
1
+ export function Head() {
2
+ return null
3
+ }
@@ -0,0 +1,7 @@
1
+ import { useConfig } from '../hooks/useConfig.js'
2
+
3
+ export function Head({ children }) {
4
+ const config = useConfig()
5
+ config({ Head: children })
6
+ return null
7
+ }
@@ -0,0 +1,14 @@
1
+ import { useConfig } from '../hooks/useConfig.js'
2
+
3
+ export function useConfig() {
4
+ const pageContext = typeof window !== 'undefined'
5
+ ? window.__vike_pageContext
6
+ : globalThis.__vike_pageContext
7
+
8
+ return (values) => {
9
+ if (!pageContext._configViaHook) {
10
+ pageContext._configViaHook = {}
11
+ }
12
+ Object.assign(pageContext._configViaHook, values)
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ import { usePageContext } from './usePageContext.js'
2
+
3
+ export function useData() {
4
+ const pageContext = usePageContext()
5
+ return pageContext?.data
6
+ }
@@ -0,0 +1,6 @@
1
+ export function usePageContext() {
2
+ const pageContext = typeof window !== 'undefined'
3
+ ? window.__vike_pageContext
4
+ : globalThis.__vike_pageContext
5
+ return pageContext
6
+ }
package/src/index.js CHANGED
@@ -1,13 +1,23 @@
1
1
  /**
2
- * @vike-ripple/vike-ripple — Vike integration for Ripple TS.
2
+ * vike-ripple — Vike extension for Ripple TS.
3
3
  *
4
4
  * ## Setup
5
- * Run once: npx vike-ripple setup
6
- * Or add to package.json: "postinstall": "vike-ripple setup"
5
+ * 1. Import config in your renderer/+config.ts:
6
+ * import vikeRipple from 'vike-ripple/config'
7
7
  *
8
- * Then add the Vite plugin to vite.config.ts:
9
- * import vikeRipple from '@vike-ripple/vike-ripple'
10
- * // in plugins: vikeRipple(),
8
+ * 2. Run setup (once):
9
+ * npx vike-ripple setup
10
+ *
11
+ * 3. Add optimizeDeps to vite.config.ts:
12
+ * optimizeDeps: { exclude: ['ripple'] }
13
+ *
14
+ * ## Usage
15
+ * - +Head.tsrx — inject <head> content
16
+ * - +Layout.tsrx — layout components
17
+ * - +title.ts — per-page title
18
+ * - +description.ts — per-page description
19
+ * - +ssr.ts — per-page SSR toggle
20
+ * - +stream.ts — per-page streaming toggle
11
21
  */
12
22
  export default function vikeRipple() {
13
23
  return {
@@ -0,0 +1,10 @@
1
+ export function applyHeadSettings(headList, target) {
2
+ if (!headList || !Array.isArray(headList)) return
3
+ for (const head of headList) {
4
+ if (typeof head === 'string') {
5
+ target.insertAdjacentHTML('beforeend', head)
6
+ } else if (head instanceof Node) {
7
+ target.appendChild(head.cloneNode(true))
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ export { getHeadSetting }
2
+
3
+ function getHeadSetting(key, pageContext) {
4
+ const value = pageContext.config[key]
5
+ if (value !== undefined && value !== null) return value
6
+ return pageContext._configViaHook?.[key] ?? null
7
+ }
@@ -0,0 +1,32 @@
1
+ export { getPageElement }
2
+
3
+ import { getHeadSetting } from './getHeadSetting.js'
4
+
5
+ function getPageElement(pageContext) {
6
+ const { Page } = pageContext
7
+ if (!Page) {
8
+ return { page: null, pageElement: null }
9
+ }
10
+
11
+ const Layout = pageContext.config.Layout
12
+ const Wrapper = pageContext.config.Wrapper
13
+
14
+ let page = Page
15
+
16
+ if (Layout) {
17
+ const layouts = Array.isArray(Layout) ? Layout : [Layout]
18
+ for (let i = layouts.length - 1; i >= 0; i--) {
19
+ const LayoutComponent = layouts[i]
20
+ page = function NestedPage(props) { return LayoutComponent({ ...props, children: page }) }
21
+ }
22
+ }
23
+
24
+ if (Wrapper) {
25
+ const wrappers = Array.isArray(Wrapper) ? Wrapper : [Wrapper]
26
+ for (const W of wrappers) {
27
+ page = function WrappedPage(props) { return W({ ...props, children: page }) }
28
+ }
29
+ }
30
+
31
+ return { page, pageElement: page }
32
+ }
@@ -0,0 +1,62 @@
1
+ // https://vike.dev/onRenderClient
2
+ export { onRenderClient }
3
+
4
+ import { hydrate } from 'ripple'
5
+ import { getHeadSetting } from './getHeadSetting.js'
6
+ import { applyHeadSettings } from './applyHeadSettings.js'
7
+ import { callCumulativeHooks } from '../utils/callCumulativeHooks.js'
8
+
9
+ let rendered = false
10
+
11
+ const onRenderClient = async (pageContext) => {
12
+ await callCumulativeHooks(pageContext.config.onBeforeRenderClient, pageContext)
13
+
14
+ const container = document.getElementById('root')
15
+ if (!container) return
16
+
17
+ pageContext._headAlreadySet = pageContext.isHydration
18
+
19
+ if (pageContext.isHydration && container.innerHTML !== '') {
20
+ try {
21
+ hydratePage(pageContext, container)
22
+ rendered = true
23
+ } catch (err) {
24
+ console.warn('[vike-ripple] hydrate failed, falling back to mount:', err)
25
+ }
26
+ }
27
+
28
+ if (!rendered) {
29
+ mountPage(pageContext, container)
30
+ rendered = true
31
+ }
32
+
33
+ updateHead(pageContext)
34
+
35
+ await callCumulativeHooks(pageContext.config.onAfterRenderClient, pageContext)
36
+ }
37
+
38
+ function hydratePage(pageContext, container) {
39
+ hydrate(pageContext.Page, { target: container, props: {} })
40
+ }
41
+
42
+ async function mountPage(pageContext, container) {
43
+ const { mount } = await import('ripple')
44
+ mount(pageContext.Page, { target: container, props: {} })
45
+ }
46
+
47
+ function updateHead(pageContext) {
48
+ if (pageContext._headAlreadySet) return
49
+
50
+ const title = getHeadSetting('title', pageContext)
51
+ if (title && document.title !== title) {
52
+ document.title = title
53
+ }
54
+
55
+ const lang = getHeadSetting('lang', pageContext)
56
+ if (lang) {
57
+ document.documentElement.lang = lang
58
+ }
59
+
60
+ applyHeadSettings(pageContext.config.Head, document.head)
61
+ applyHeadSettings(pageContext._configViaHook?.Head, document.head)
62
+ }
@@ -0,0 +1,120 @@
1
+ export { onRenderHtml }
2
+
3
+ import { render, create_ssr_stream } from 'ripple/server'
4
+ import { escapeInject, dangerouslySkipEscape } from 'vike/server'
5
+
6
+
7
+ import { getHeadSetting } from './getHeadSetting.js'
8
+ import { callCumulativeHooks } from '../utils/callCumulativeHooks.js'
9
+ import { getTagAttributesString } from '../utils/getTagAttributesString.js'
10
+
11
+ const onRenderHtml = async (pageContext) => {
12
+ const pageContext2 = pageContext
13
+ const { Page } = pageContext2
14
+ if (!Page) throw new Error('No Page')
15
+
16
+ await callCumulativeHooks(pageContext2.config.onBeforeRenderHtml, pageContext2)
17
+
18
+ const headHtml = getHeadHtml(pageContext2)
19
+ const { headHtmlBegin, headHtmlEnd, bodyHtmlBegin, bodyHtmlEnd } = await getHtmlInjections(pageContext2)
20
+ const { htmlAttributesString, bodyAttributesString } = getTagAttributes(pageContext2)
21
+ const enableStream = !!(pageContext2.config.stream ?? pageContext2.config.rippleStream)
22
+
23
+ if (enableStream) {
24
+ const rippleStream = create_ssr_stream()
25
+ render(Page, { stream: rippleStream.sink }).catch(e => {
26
+ console.error('[ripple] render err:', e?.message)
27
+ })
28
+ return escapeInject`<!DOCTYPE html>
29
+ <html${dangerouslySkipEscape(htmlAttributesString)}>
30
+ <head>
31
+ <meta charset="UTF-8" />
32
+ ${dangerouslySkipEscape(headHtmlBegin)}
33
+ ${dangerouslySkipEscape(headHtml)}
34
+ ${dangerouslySkipEscape(headHtmlEnd)}
35
+ </head>
36
+ <body${dangerouslySkipEscape(bodyAttributesString)}>
37
+ ${dangerouslySkipEscape(bodyHtmlBegin)}
38
+ <div id="root">${rippleStream.stream}</div>
39
+ ${dangerouslySkipEscape(bodyHtmlEnd)}
40
+ </body>
41
+ </html>`
42
+ }
43
+
44
+ const { head, body, css } = await render(Page, {})
45
+
46
+ const cssHtml = css?.size
47
+ ? `<style data-ripple-ssr>${[...css].join('')}<` + `/style>`
48
+ : ''
49
+
50
+ const pageHtml = `<div id="root">${body}${cssHtml}</div>`
51
+
52
+ pageContext2.pageHtmlString = body
53
+ await callCumulativeHooks(pageContext2.config.onAfterRenderHtml, pageContext2)
54
+
55
+ return escapeInject`<!DOCTYPE html>
56
+ <html${dangerouslySkipEscape(htmlAttributesString)}>
57
+ <head>
58
+ <meta charset="UTF-8" />
59
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
60
+ ${dangerouslySkipEscape(headHtmlBegin)}
61
+ ${dangerouslySkipEscape(head)}
62
+ ${dangerouslySkipEscape(cssHtml)}
63
+ ${dangerouslySkipEscape(headHtml)}
64
+ ${dangerouslySkipEscape(headHtmlEnd)}
65
+ </head>
66
+ <body${dangerouslySkipEscape(bodyAttributesString)}>
67
+ ${dangerouslySkipEscape(bodyHtmlBegin)}
68
+ <div id="root">${dangerouslySkipEscape(body)}</div>
69
+ ${dangerouslySkipEscape(bodyHtmlEnd)}
70
+ </body>
71
+ </html>`
72
+ }
73
+
74
+ function getHeadHtml(pageContext) {
75
+ const favicon = getHeadSetting('favicon', pageContext)
76
+ const title = getHeadSetting('title', pageContext)
77
+ const description = getHeadSetting('description', pageContext)
78
+ const image = getHeadSetting('image', pageContext)
79
+
80
+ const faviconTag = !favicon ? '' : `<link rel="icon" href="${favicon}" />`
81
+ const titleTags = !title ? '' : `<title>${title}</title><meta property="og:title" content="${title}" />`
82
+ const descriptionTags = !description
83
+ ? ''
84
+ : `<meta name="description" content="${description}" /><meta property="og:description" content="${description}" />`
85
+ const imageTags = !image
86
+ ? ''
87
+ : `<meta property="og:image" content="${image}"><meta name="twitter:card" content="summary_large_image">`
88
+ const viewportTag = getViewportTag(getHeadSetting('viewport', pageContext))
89
+ const langAttr = getHeadSetting('lang', pageContext)
90
+ const headElementsHtml = [
91
+ ...(pageContext.config.Head ?? []),
92
+ ...(pageContext._configViaHook?.Head ?? []),
93
+ ]
94
+ .filter(Boolean)
95
+ .map(head => (typeof head === 'function' ? head(pageContext) : head))
96
+ .join('\n')
97
+
98
+ return `${titleTags}${viewportTag}${headElementsHtml}${faviconTag}${descriptionTags}${imageTags}`
99
+ }
100
+
101
+ export function getViewportTag(viewport) {
102
+ if (viewport === null || viewport === undefined) return ''
103
+ if (viewport === 'responsive') return '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'
104
+ if (typeof viewport === 'number') return `<meta name="viewport" content="width=${viewport}" />`
105
+ return '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'
106
+ }
107
+
108
+ function getTagAttributes(pageContext) {
109
+ const htmlAttributesString = getTagAttributesString(pageContext.config.htmlAttributes)
110
+ const bodyAttributesString = getTagAttributesString(pageContext.config.bodyAttributes)
111
+ return { htmlAttributesString, bodyAttributesString }
112
+ }
113
+
114
+ async function getHtmlInjections(pageContext) {
115
+ const headHtmlBegin = (pageContext.config.headHtmlBegin ?? []).join('\n')
116
+ const headHtmlEnd = (pageContext.config.headHtmlEnd ?? []).join('\n')
117
+ const bodyHtmlBegin = (pageContext.config.bodyHtmlBegin ?? []).join('\n')
118
+ const bodyHtmlEnd = (pageContext.config.bodyHtmlEnd ?? []).join('\n')
119
+ return { headHtmlBegin, headHtmlEnd, bodyHtmlBegin, bodyHtmlEnd }
120
+ }
@@ -0,0 +1,15 @@
1
+ export { ssrEffect }
2
+
3
+ function ssrEffect({ configDefinedAt, configValue }) {
4
+ if (typeof configValue !== 'boolean') throw new Error(`${configDefinedAt} should be a boolean`)
5
+ return {
6
+ meta: {
7
+ Page: {
8
+ env: {
9
+ client: true,
10
+ server: configValue !== false,
11
+ },
12
+ },
13
+ },
14
+ }
15
+ }
@@ -0,0 +1,33 @@
1
+ import 'vike/types'
2
+
3
+ declare global {
4
+ namespace Vike {
5
+ interface Config {
6
+ Head?: unknown[]
7
+ Layout?: unknown[]
8
+ title?: string | null
9
+ description?: string | null
10
+ image?: string | null
11
+ viewport?: 'responsive' | number | null
12
+ favicon?: string | null
13
+ lang?: string | null
14
+ ssr?: boolean
15
+ stream?: boolean
16
+ rippleStream?: boolean
17
+ Wrapper?: unknown[]
18
+ Loading?: unknown
19
+ onBeforeRenderHtml?: ((pageContext: unknown) => void | Promise<void>)[]
20
+ onAfterRenderHtml?: ((pageContext: unknown) => void | Promise<void>)[]
21
+ onBeforeRenderClient?: ((pageContext: unknown) => void | Promise<void>)[]
22
+ onAfterRenderClient?: ((pageContext: unknown) => void | Promise<void>)[]
23
+ bodyHtmlBegin?: string[]
24
+ bodyHtmlEnd?: string[]
25
+ headHtmlBegin?: string[]
26
+ headHtmlEnd?: string[]
27
+ htmlAttributes?: Record<string, string | boolean>
28
+ bodyAttributes?: Record<string, string | boolean>
29
+ }
30
+ }
31
+ }
32
+
33
+ export type __FakeExport_Config = true
@@ -0,0 +1,13 @@
1
+ import 'vike/types'
2
+
3
+ declare global {
4
+ namespace Vike {
5
+ interface PageContext {
6
+ _configViaHook?: Record<string, unknown>
7
+ _headAlreadySet?: boolean
8
+ pageHtmlString?: string
9
+ }
10
+ }
11
+ }
12
+
13
+ export type __FakeExport_PageContext = true
@@ -0,0 +1,11 @@
1
+ export async function callCumulativeHooks(hooks, ...args) {
2
+ if (!hooks || !Array.isArray(hooks)) return
3
+ for (const hook of hooks) {
4
+ if (typeof hook === 'function') {
5
+ const result = hook(...args)
6
+ if (result && typeof result.then === 'function') {
7
+ await result
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ export function getTagAttributesString(attributes) {
2
+ if (!attributes) return ''
3
+ return Object.entries(attributes)
4
+ .map(([key, value]) => {
5
+ if (value === true) return ` ${key}`
6
+ if (value === false || value === null || value === undefined) return ''
7
+ return ` ${key}="${String(value).replace(/"/g, '&quot;')}"`
8
+ })
9
+ .join('')
10
+ }
@@ -1,10 +0,0 @@
1
- import type { Config } from 'vike/types'
2
-
3
- export default {
4
- prerender: false,
5
- meta: {
6
- rippleStream: {
7
- env: { server: true },
8
- },
9
- },
10
- } satisfies Config
@@ -1,29 +0,0 @@
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
- }
@@ -1,43 +0,0 @@
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
- }