vike-ripple 0.2.1 → 0.4.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 CHANGED
@@ -1,11 +1,13 @@
1
- # @vike-ripple/vike-ripple
1
+ # vike-ripple
2
+
3
+ > ⚠️ **HIGHLY EXPERIMENTAL** — This package is in early development. APIs may change without notice, parts may not work, and documentation may be incomplete. Use at your own risk.
2
4
 
3
5
  [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
6
 
5
7
  ## Install
6
8
 
7
9
  ```sh
8
- npm install @vike-ripple/vike-ripple
10
+ npm install vike-ripple
9
11
  ```
10
12
 
11
13
  ## Setup
@@ -24,35 +26,28 @@ Or add to your project's `package.json` so it runs automatically after `npm inst
24
26
  }
25
27
  ```
26
28
 
27
- ### 2. Add plugin to `vite.config.ts`
29
+ ### 2. Configure `vite.config.ts`
28
30
 
29
31
  ```ts
30
32
  import { defineConfig } from 'vite'
31
33
  import vike from 'vike/plugin'
32
34
  import { ripple } from '@ripple-ts/vite-plugin'
33
- import vikeRipple from '@vike-ripple/vike-ripple'
35
+ import vikeRipple from 'vike-ripple'
34
36
 
35
37
  export default defineConfig({
36
- optimizeDeps: {
37
- exclude: ['ripple'],
38
- },
39
- plugins: [
40
- vikeRipple(),
41
- ripple({ excludeRippleExternalModules: true }),
42
- vike(),
43
- ],
38
+ optimizeDeps: { exclude: ['ripple'] },
39
+ plugins: [vikeRipple(), ripple({ excludeRippleExternalModules: true }), vike()],
44
40
  })
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.
41
+ ```
46
42
 
47
- ### 3. Add renderer files
43
+ ### 3. Add renderer config
48
44
 
49
- Copy from `node_modules/@vike-ripple/vike-ripple/src/renderer/` to your project's `renderer/`:
45
+ Create `renderer/+config.ts`:
50
46
 
51
- ```
52
- renderer/
53
- +config.ts
54
- +onRenderHtml.tsx
55
- +onRenderClient.tsx
47
+ ```ts
48
+ export default {
49
+ extends: ['import:vike-ripple/config:default'],
50
+ }
56
51
  ```
57
52
 
58
53
  ### 4. Create a page
@@ -71,22 +66,38 @@ export function Page(props: {}) @{
71
66
  }
72
67
  ```
73
68
 
69
+ ## Features
70
+
71
+ | Feature | Status |
72
+ |---|---|
73
+ | `.tsrx` page file support | ✅ |
74
+ | SSR rendering | ✅ |
75
+ | Client hydration with mount fallback | ✅ |
76
+ | Streaming SSR (`rippleStream` config) | ✅ |
77
+ | `<head>` tag extraction | ✅ |
78
+ | `+Layout.tsrx` support | ✅ |
79
+ | `+Head.tsrx` support | ✅ |
80
+ | Config: `title`, `description`, `image`, `viewport`, `favicon`, `lang` | ✅ |
81
+ | Config: `ssr` toggle, `stream` toggle | ✅ |
82
+ | Config: `htmlAttributes`, `bodyAttributes` | ✅ |
83
+ | Config: `headHtmlBegin/End`, `bodyHtmlBegin/End` | ✅ |
84
+ | Hooks: `onBefore/AfterRenderHtml`, `onBefore/AfterRenderClient` | ✅ |
85
+ | `@tailwindcss` integration (via `vike-ripple-tailwindcss`) | ✅ |
86
+ | `@apply` in `<style>` blocks (via `vike-ripple-tailwindcss`) | ✅ |
87
+ | HMR stability during development | 🟡 |
88
+ | TypeScript types for `Vike.Config` / `Vike.PageContext` | 🟡 |
89
+ | Production build testing | 🔴 |
90
+
74
91
  ## What this does
75
92
 
76
93
  | Patch | Why |
77
94
  |---|---|
78
95
  | **`.tsrx` extension** | Vike doesn't know `.tsrx` is a valid page extension — adds it to `isScriptFile.js` |
79
96
  | **`?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.
97
+ | **`@apply` support** | Prepends `@import "tailwindcss" layer(reference)` to extracted CSS so `@apply` resolves in `<style>` blocks |
87
98
 
88
- ### Renderer files
99
+ ## Known Issues
89
100
 
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.
101
+ - **Hydration errors**: Ripple's `hydrate()` may throw `TypeError: Illegal invocation` due to Vite dep optimization. Fixed by `optimizeDeps.exclude: ['ripple']` and the mount fallback in the client renderer.
102
+ - **HMR hang**: Editing `.tsrx` files during dev may occasionally cause HMR to hang. Restarting the dev server resolves it.
103
+ - **`</style>` in template literals**: If a `.tsx` file contains `</style>` inside a JavaScript string, the Tailwind Oxide scanner may emit a `CssSyntaxError`. Workaround: break the literal with string concatenation: `"<" + "/style>"`. See [tailwindcss#20000](https://github.com/tailwindlabs/tailwindcss/issues/20000).
package/package.json CHANGED
@@ -1,38 +1,39 @@
1
1
  {
2
2
  "name": "vike-ripple",
3
- "version": "0.2.1",
4
- "description": "Vike extension for Ripple TS — full parity with vike-react/vike-solid/vike-vue",
3
+ "version": "0.4.0",
4
+ "description": "Vike extension for Ripple TS — SSR, streaming, Layout, Head, SEO configs, hooks",
5
5
  "type": "module",
6
- "types": "./src/types/Config.ts",
7
6
  "exports": {
8
7
  ".": "./src/index.js",
9
- "./config": "./src/+config.js",
8
+ "./config": "./src/config.js",
10
9
  "./setup": "./src/setup.js",
11
- "./Head": "./src/components/Head/Head-server.js",
12
- "./ClientOnly": "./src/components/ClientOnly.js",
13
10
  "./usePageContext": "./src/hooks/usePageContext.js",
14
11
  "./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"
18
- },
19
- "bin": {
20
- "vike-ripple": "./src/setup.js"
12
+ "./useHydrated": "./src/hooks/useHydrated.js",
13
+ "./useConfig": {
14
+ "browser": "./src/hooks/useConfig/useConfig-client.js",
15
+ "default": "./src/hooks/useConfig/useConfig-server.js"
16
+ },
17
+ "./Config": {
18
+ "browser": "./src/components/Config/Config-client.js",
19
+ "default": "./src/components/Config/Config-server.js"
20
+ },
21
+ "./Head": {
22
+ "browser": "./src/components/Head/Head-client.js",
23
+ "default": "./src/components/Head/Head-server.js"
24
+ },
25
+ "./clientOnly": "./src/helpers/clientOnly.js",
26
+ "./ClientOnly": "./src/components/ClientOnly.js",
27
+ "./__internal/integration/onRenderHtml": "./src/integration/onRenderHtml.js",
28
+ "./__internal/integration/onRenderClient": "./src/integration/onRenderClient.js"
21
29
  },
22
- "files": [
23
- "src"
24
- ],
25
- "keywords": [
26
- "vike",
27
- "ripple",
28
- "ripplets",
29
- "ssr",
30
- "vite"
31
- ],
30
+ "bin": { "vike-ripple": "./src/setup.js" },
31
+ "files": ["src"],
32
+ "keywords": ["vike", "ripple", "ripplets", "ssr", "vite"],
32
33
  "license": "MIT",
33
34
  "peerDependencies": {
34
35
  "vike": ">=0.4.259",
35
36
  "@ripple-ts/vite-plugin": ">=0.3.0",
36
37
  "ripple": ">=0.1.0"
37
38
  }
38
- }
39
+ }
@@ -1,5 +1,7 @@
1
1
  export { ClientOnly }
2
2
 
3
- function ClientOnly({ children }) {
4
- return children
3
+ import { useHydrated } from '../hooks/useHydrated.js'
4
+
5
+ function ClientOnly({ children, fallback }) {
6
+ return useHydrated() ? children : (fallback ?? null)
5
7
  }
@@ -1,3 +1,8 @@
1
- export function Config() {
1
+ export { Config }
2
+
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-client.js'
4
+
5
+ function Config(props) {
6
+ useConfig()(props)
2
7
  return null
3
8
  }
@@ -1,6 +1,8 @@
1
- import { useConfig } from '../hooks/useConfig.js'
1
+ export { Config }
2
2
 
3
- export function Config(values) {
4
- useConfig()(values)
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-server.js'
4
+
5
+ function Config(props) {
6
+ useConfig()(props)
5
7
  return null
6
8
  }
@@ -1,3 +1 @@
1
- export function Head() {
2
- return null
3
- }
1
+ export function Head() { return null }
@@ -1,7 +1,8 @@
1
- import { useConfig } from '../hooks/useConfig.js'
1
+ export { Head }
2
2
 
3
- export function Head({ children }) {
4
- const config = useConfig()
5
- config({ Head: children })
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-server.js'
4
+
5
+ function Head({ children }) {
6
+ useConfig()({ Head: children })
6
7
  return null
7
8
  }
@@ -1,12 +1,11 @@
1
1
  import { ssrEffect } from './integration/ssrEffect.js'
2
2
 
3
- /** @type {import('vike/types').Config} */
4
3
  const config = {
5
4
  name: 'vike-ripple',
6
5
  require: { vike: '>=0.4.250' },
7
6
 
8
- onRenderHtml: 'import:vike-ripple/src/integration/onRenderHtml.js:onRenderHtml',
9
- onRenderClient: 'import:vike-ripple/src/integration/onRenderClient.js:onRenderClient',
7
+ onRenderHtml: 'import:vike-ripple/__internal/integration/onRenderHtml:onRenderHtml',
8
+ onRenderClient: 'import:vike-ripple/__internal/integration/onRenderClient:onRenderClient',
10
9
 
11
10
  clientRouting: true,
12
11
  hydrationCanBeAborted: true,
@@ -16,6 +15,7 @@ const config = {
16
15
  meta: {
17
16
  Head: { env: { server: true }, cumulative: true },
18
17
  Layout: { env: { server: true, client: true }, cumulative: true },
18
+ Wrapper: { env: { server: true, client: true }, cumulative: true },
19
19
  title: { env: { server: true, client: true } },
20
20
  description: { env: { server: true } },
21
21
  image: { env: { server: true } },
@@ -26,8 +26,8 @@ const config = {
26
26
  stream: { env: { server: true }, cumulative: true },
27
27
  onBeforeRenderHtml: { env: { server: true }, cumulative: true },
28
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 },
29
+ onBeforeRenderClient: { env: { client: true }, cumulative: true },
30
+ onAfterRenderClient: { env: { client: true }, cumulative: true },
31
31
  bodyHtmlBegin: { env: { server: true }, cumulative: true, global: true },
32
32
  bodyHtmlEnd: { env: { server: true }, cumulative: true, global: true },
33
33
  headHtmlBegin: { env: { server: true }, cumulative: true, global: true },
@@ -0,0 +1,4 @@
1
+ export function clientOnly() {
2
+ console.warn('[vike-ripple] clientOnly() is deprecated — use <ClientOnly>')
3
+ return (props) => props.fallback ?? null
4
+ }
@@ -0,0 +1,7 @@
1
+ export const configsCumulative = [
2
+ 'Head', 'Layout', 'Wrapper',
3
+ 'bodyHtmlBegin', 'bodyHtmlEnd', 'headHtmlBegin', 'headHtmlEnd',
4
+ 'htmlAttributes', 'bodyAttributes',
5
+ 'onBeforeRenderHtml', 'onAfterRenderHtml',
6
+ 'onBeforeRenderClient', 'onAfterRenderClient',
7
+ ]
@@ -0,0 +1,25 @@
1
+ export { useConfig }
2
+
3
+ import { getPageContext } from 'vike/getPageContext'
4
+ import { usePageContext } from '../usePageContext.js'
5
+
6
+ function useConfig() {
7
+ let pageContext = getPageContext({ asyncHook: false })
8
+ if (pageContext) {
9
+ return (config) => {
10
+ pageContext._configViaHook ??= {}
11
+ Object.assign(pageContext._configViaHook, config)
12
+ }
13
+ }
14
+
15
+ pageContext = usePageContext()
16
+ return (config) => {
17
+ if (!('_headAlreadySet' in (pageContext || {}))) {
18
+ pageContext._configViaHook ??= {}
19
+ Object.assign(pageContext._configViaHook, config)
20
+ } else {
21
+ if (config.title) document.title = config.title
22
+ if (config.lang) document.documentElement.lang = config.lang
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,24 @@
1
+ export { useConfig }
2
+
3
+ import { getPageContext } from 'vike/getPageContext'
4
+ import { usePageContext } from '../usePageContext.js'
5
+
6
+ function useConfig() {
7
+ // Vike hook
8
+ let pageContext = getPageContext({ asyncHook: false })
9
+ if (pageContext) {
10
+ return (config) => {
11
+ pageContext._configViaHook ??= {}
12
+ Object.assign(pageContext._configViaHook, config)
13
+ }
14
+ }
15
+
16
+ // Component
17
+ pageContext = usePageContext()
18
+ return (config) => {
19
+ if (!pageContext?._headAlreadySet) {
20
+ pageContext._configViaHook ??= {}
21
+ Object.assign(pageContext._configViaHook, config)
22
+ }
23
+ }
24
+ }
@@ -1,6 +1,7 @@
1
+ export { useData }
2
+
1
3
  import { usePageContext } from './usePageContext.js'
2
4
 
3
- export function useData() {
4
- const pageContext = usePageContext()
5
- return pageContext?.data
5
+ function useData() {
6
+ return usePageContext()?.data
6
7
  }
@@ -0,0 +1,7 @@
1
+ export { useHydrated }
2
+ export { setHydrated }
3
+
4
+ let _hydrated = false
5
+
6
+ function useHydrated() { return _hydrated }
7
+ function setHydrated() { _hydrated = true }
@@ -1,6 +1,12 @@
1
- export function usePageContext() {
2
- const pageContext = typeof window !== 'undefined'
3
- ? window.__vike_pageContext
4
- : globalThis.__vike_pageContext
5
- return pageContext
1
+ export { usePageContext }
2
+ export { setPageContext }
3
+
4
+ let _pageContext = null
5
+
6
+ function usePageContext() {
7
+ return _pageContext
8
+ }
9
+
10
+ function setPageContext(ctx) {
11
+ _pageContext = ctx
6
12
  }
package/src/index.js CHANGED
@@ -1,27 +1,4 @@
1
- /**
2
- * vike-ripple Vike extension for Ripple TS.
3
- *
4
- * ## Setup
5
- * 1. Import config in your renderer/+config.ts:
6
- * import vikeRipple from 'vike-ripple/config'
7
- *
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
21
- */
22
- export default function vikeRipple() {
23
- return {
24
- name: 'vike-ripple',
25
- enforce: 'pre',
26
- }
27
- }
1
+ console.warn(
2
+ "[vike-ripple] Replace `import vikeRipple from 'vike-ripple'` with `import vikeRipple from 'vike-ripple/config'`",
3
+ )
4
+ export { default } from './config.js'
@@ -1,7 +1,7 @@
1
1
  export { getHeadSetting }
2
2
 
3
3
  function getHeadSetting(key, pageContext) {
4
- const value = pageContext.config[key]
5
- if (value !== undefined && value !== null) return value
4
+ const v = pageContext.config[key]
5
+ if (v !== undefined && v !== null) return v
6
6
  return pageContext._configViaHook?.[key] ?? null
7
7
  }
@@ -1,62 +1,33 @@
1
- // https://vike.dev/onRenderClient
2
1
  export { onRenderClient }
3
2
 
4
3
  import { hydrate } from 'ripple'
5
- import { getHeadSetting } from './getHeadSetting.js'
6
- import { applyHeadSettings } from './applyHeadSettings.js'
7
- import { callCumulativeHooks } from '../utils/callCumulativeHooks.js'
4
+ import { setPageContext } from '../hooks/usePageContext.js'
5
+ import { setHydrated } from '../hooks/useHydrated.js'
8
6
 
9
7
  let rendered = false
10
8
 
11
9
  const onRenderClient = async (pageContext) => {
12
- await callCumulativeHooks(pageContext.config.onBeforeRenderClient, pageContext)
10
+ const { Page } = pageContext
11
+ if (!Page) return
13
12
 
13
+ setPageContext(pageContext)
14
14
  const container = document.getElementById('root')
15
15
  if (!container) return
16
16
 
17
- pageContext._headAlreadySet = pageContext.isHydration
18
-
19
17
  if (pageContext.isHydration && container.innerHTML !== '') {
20
18
  try {
21
- hydratePage(pageContext, container)
19
+ hydrate(Page, { target: container, props: {} })
22
20
  rendered = true
21
+ setHydrated()
23
22
  } catch (err) {
24
23
  console.warn('[vike-ripple] hydrate failed, falling back to mount:', err)
25
24
  }
26
25
  }
27
26
 
28
27
  if (!rendered) {
29
- mountPage(pageContext, container)
28
+ const { mount } = await import('ripple')
29
+ mount(Page, { target: container, props: {} })
30
30
  rendered = true
31
+ setHydrated()
31
32
  }
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
33
  }
@@ -2,27 +2,48 @@ export { onRenderHtml }
2
2
 
3
3
  import { render, create_ssr_stream } from 'ripple/server'
4
4
  import { escapeInject, dangerouslySkipEscape } from 'vike/server'
5
-
6
-
5
+ import { setPageContext } from '../hooks/usePageContext.js'
7
6
  import { getHeadSetting } from './getHeadSetting.js'
8
- import { callCumulativeHooks } from '../utils/callCumulativeHooks.js'
9
7
  import { getTagAttributesString } from '../utils/getTagAttributesString.js'
8
+ import { callCumulativeHooks } from '../utils/callCumulativeHooks.js'
10
9
 
11
10
  const onRenderHtml = async (pageContext) => {
12
- const pageContext2 = pageContext
13
- const { Page } = pageContext2
11
+ const { Page } = pageContext
14
12
  if (!Page) throw new Error('No Page')
15
13
 
16
- await callCumulativeHooks(pageContext2.config.onBeforeRenderHtml, pageContext2)
14
+ await callCumulativeHooks(pageContext.config.onBeforeRenderHtml, pageContext)
15
+
16
+ setPageContext(pageContext)
17
+
18
+ const headHtml = getHeadHtml(pageContext)
19
+ const { headHtmlBegin, headHtmlEnd, bodyHtmlBegin, bodyHtmlEnd } = getHtmlInjections(pageContext)
20
+ const { htmlAttributesString, bodyAttributesString } = getTagAttributes(pageContext)
21
+
22
+ // Wrap in Layout(s) + Wrapper(s)
23
+ let wrappedPage = Page
24
+ const Layout = pageContext.config.Layout
25
+ const Wrapper = pageContext.config.Wrapper
26
+ if (Layout) {
27
+ const layouts = Array.isArray(Layout) ? Layout : [Layout]
28
+ for (let i = layouts.length - 1; i >= 0; i--) {
29
+ const L = layouts[i]
30
+ const prev = wrappedPage
31
+ wrappedPage = (props) => L({ ...props, children: prev })
32
+ }
33
+ }
34
+ if (Wrapper) {
35
+ const wrappers = Array.isArray(Wrapper) ? Wrapper : [Wrapper]
36
+ for (const W of wrappers) {
37
+ const prev = wrappedPage
38
+ wrappedPage = (props) => W({ ...props, children: prev })
39
+ }
40
+ }
17
41
 
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)
42
+ const enableStream = !!(pageContext.config.stream ?? pageContext.config.rippleStream)
22
43
 
23
44
  if (enableStream) {
24
45
  const rippleStream = create_ssr_stream()
25
- render(Page, { stream: rippleStream.sink }).catch(e => {
46
+ render(wrappedPage, { stream: rippleStream.sink }).catch(e => {
26
47
  console.error('[ripple] render err:', e?.message)
27
48
  })
28
49
  return escapeInject`<!DOCTYPE html>
@@ -41,16 +62,19 @@ const onRenderHtml = async (pageContext) => {
41
62
  </html>`
42
63
  }
43
64
 
44
- const { head, body, css } = await render(Page, {})
65
+ const { head, body, css, topLevelError } = await render(wrappedPage, {})
66
+ if (topLevelError) {
67
+ console.error('[vike-ripple] SSR render error:', topLevelError)
68
+ throw topLevelError
69
+ }
45
70
 
71
+ // Ripple's render() already extracts <head> content into `head` and CSS into `css`
46
72
  const cssHtml = css?.size
47
73
  ? `<style data-ripple-ssr>${[...css].join('')}<` + `/style>`
48
74
  : ''
49
75
 
50
- const pageHtml = `<div id="root">${body}${cssHtml}</div>`
51
-
52
- pageContext2.pageHtmlString = body
53
- await callCumulativeHooks(pageContext2.config.onAfterRenderHtml, pageContext2)
76
+ pageContext.pageHtmlString = body
77
+ await callCumulativeHooks(pageContext.config.onAfterRenderHtml, pageContext)
54
78
 
55
79
  return escapeInject`<!DOCTYPE html>
56
80
  <html${dangerouslySkipEscape(htmlAttributesString)}>
@@ -77,44 +101,43 @@ function getHeadHtml(pageContext) {
77
101
  const description = getHeadSetting('description', pageContext)
78
102
  const image = getHeadSetting('image', pageContext)
79
103
 
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">`
104
+ const parts = []
105
+ if (favicon) parts.push(`<link rel="icon" href="${favicon}" />`)
106
+ if (title) parts.push(`<title>${title}</title>`)
107
+ if (description) parts.push(`<meta name="description" content="${description}" />`)
108
+ if (image) parts.push(`<meta property="og:image" content="${image}">`)
88
109
  const viewportTag = getViewportTag(getHeadSetting('viewport', pageContext))
89
- const langAttr = getHeadSetting('lang', pageContext)
90
- const headElementsHtml = [
110
+ if (viewportTag) parts.push(viewportTag)
111
+ const headElements = [
91
112
  ...(pageContext.config.Head ?? []),
92
113
  ...(pageContext._configViaHook?.Head ?? []),
93
114
  ]
94
115
  .filter(Boolean)
95
- .map(head => (typeof head === 'function' ? head(pageContext) : head))
116
+ .map(h => (typeof h === 'function' ? h(pageContext) : h))
96
117
  .join('\n')
97
-
98
- return `${titleTags}${viewportTag}${headElementsHtml}${faviconTag}${descriptionTags}${imageTags}`
118
+ if (headElements) parts.push(headElements)
119
+ return parts.join('\n')
99
120
  }
100
121
 
101
- export function getViewportTag(viewport) {
102
- if (viewport === null || viewport === undefined) return ''
122
+ function getViewportTag(viewport) {
123
+ if (!viewport && viewport !== 0) return ''
103
124
  if (viewport === 'responsive') return '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'
104
125
  if (typeof viewport === 'number') return `<meta name="viewport" content="width=${viewport}" />`
105
- return '<meta name="viewport" content="width=device-width, initial-scale=1.0" />'
126
+ return ''
106
127
  }
107
128
 
108
129
  function getTagAttributes(pageContext) {
109
- const htmlAttributesString = getTagAttributesString(pageContext.config.htmlAttributes)
110
- const bodyAttributesString = getTagAttributesString(pageContext.config.bodyAttributes)
111
- return { htmlAttributesString, bodyAttributesString }
130
+ return {
131
+ htmlAttributesString: getTagAttributesString(pageContext.config.htmlAttributes),
132
+ bodyAttributesString: getTagAttributesString(pageContext.config.bodyAttributes),
133
+ }
112
134
  }
113
135
 
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 }
136
+ function getHtmlInjections(pageContext) {
137
+ return {
138
+ headHtmlBegin: (pageContext.config.headHtmlBegin ?? []).join('\n'),
139
+ headHtmlEnd: (pageContext.config.headHtmlEnd ?? []).join('\n'),
140
+ bodyHtmlBegin: (pageContext.config.bodyHtmlBegin ?? []).join('\n'),
141
+ bodyHtmlEnd: (pageContext.config.bodyHtmlEnd ?? []).join('\n'),
142
+ }
120
143
  }
@@ -4,12 +4,9 @@ function ssrEffect({ configDefinedAt, configValue }) {
4
4
  if (typeof configValue !== 'boolean') throw new Error(`${configDefinedAt} should be a boolean`)
5
5
  return {
6
6
  meta: {
7
- Page: {
8
- env: {
9
- client: true,
10
- server: configValue !== false,
11
- },
12
- },
7
+ Page: { env: { client: true, server: configValue !== false } },
8
+ Layout: { env: { client: true, server: configValue !== false } },
9
+ Wrapper: { env: { client: true, server: configValue !== false } },
13
10
  },
14
11
  }
15
12
  }
package/src/setup.js CHANGED
@@ -1,116 +1,82 @@
1
+ #!/usr/bin/env node
1
2
  import { createRequire } from 'module'
2
3
  import { join } from 'path'
3
4
  import { readFileSync, writeFileSync, existsSync } from 'fs'
4
- import { fileURLToPath } from 'url'
5
5
 
6
- const __dirname = fileURLToPath(new URL('.', import.meta.url))
7
6
  const projectRoot = process.cwd()
8
7
  let exitCode = 0
9
8
 
10
- function log(msg) { console.log('[vike-ripple]', msg) }
11
- function warn(msg) { console.warn('[vike-ripple]', msg) }
9
+ function log(m) { console.log('[vike-ripple]', m) }
10
+ function warn(m) { console.warn('[vike-ripple]', m) }
12
11
 
13
- // ── Patch 1: Register .tsrx with Vike ─────────────────────────
14
12
  function patchVikeExtensions() {
15
13
  const target = resolveVike('dist/utils/isScriptFile.js')
16
- if (!target) { warn('vike not found — skipping'); return }
17
-
14
+ if (!target) { warn('vike not found'); return }
18
15
  let src = readFileSync(target, 'utf-8')
19
- if (src.includes("'tsrx'")) { log('.tsrx already registered with Vike'); return }
20
-
16
+ if (src.includes("'tsrx'")) { log('.tsrx already registered'); return }
21
17
  const patched = src.replace(
22
18
  'const scriptFileExtensionList = [...extJsOrTs, ...extJsxOrTsx, ...extTemplates];',
23
19
  "const scriptFileExtensionList = [...extJsOrTs, ...extJsxOrTsx, ...extTemplates, 'tsrx'];",
24
20
  )
25
- if (patched === src) { warn('Could not patch Vike isScriptFile.js'); exitCode = 1; return }
21
+ if (patched === src) { warn('Could not patch Vike'); exitCode = 1; return }
26
22
  writeFileSync(target, patched, 'utf-8')
27
- log('Registered .tsrx extension with Vike')
23
+ log('Registered .tsrx extension')
28
24
  }
29
25
 
30
- // ── Patch 2: Fix ?direct in Ripple's load hook ────────────────
31
26
  function patchRippleDirect() {
32
27
  const target = resolveRipple('src/index.js')
33
- if (!target) { warn('@ripple-ts/vite-plugin not found — skipping'); return }
34
-
28
+ if (!target) { warn('@ripple-ts/vite-plugin not found'); return }
35
29
  let src = readFileSync(target, 'utf-8')
36
30
  if (src.includes('Handle ?direct query param')) { log('?direct fix already applied'); return }
37
-
38
31
  const patched = src.replace(
39
32
  'if (cssCache.has(id)) {\n\t\t\t\t\treturn cssCache.get(id);\n\t\t\t\t}',
40
33
  `if (cssCache.has(id)) {
41
- return cssCache.get(id);
42
- }
43
-
44
- // Handle ?direct query param added by Vite's SSR module loading
45
- if (id.includes('?direct')) {
46
- const baseId = id.replace('?direct', '');
47
- if (cssCache.has(baseId)) {
48
- return cssCache.get(baseId);
49
- }
50
- }`,
34
+ \t\t\t\t\treturn cssCache.get(id);
35
+ \t\t\t\t}
36
+ \t\t\t\tif (id.includes('?direct')) {
37
+ \t\t\t\t\tconst baseId = id.replace('?direct', '');
38
+ \t\t\t\t\tif (cssCache.has(baseId)) {
39
+ \t\t\t\t\t\treturn cssCache.get(baseId);
40
+ \t\t\t\t\t}
41
+ \t\t\t\t}`,
51
42
  )
52
- if (patched === src) { warn('Could not patch Ripple plugin load hook'); exitCode = 1; return }
43
+ if (patched === src) { warn('Could not patch Ripple plugin'); exitCode = 1; return }
53
44
  writeFileSync(target, patched, 'utf-8')
54
- log('Patched Ripple plugin for ?direct CSS module loading')
45
+ log('Patched Ripple plugin for ?direct CSS loading')
55
46
  }
56
47
 
57
- // ── Patch 3: @apply support via @reference (no HMR loops) ─────
58
48
  function patchRippleApply() {
59
49
  const target = resolveRipple('src/index.js')
60
50
  if (!target) return
61
-
62
51
  let src = readFileSync(target, 'utf-8')
63
-
64
52
  if (src.includes('TW_PATCH_APPLY')) { log('@apply patch already applied'); return }
65
-
66
- // Upgrade from old TW_PATCH format
67
53
  if (src.includes('TW_PATCH:')) {
68
- src = src.replace(
69
- '// TW_PATCH: prepend tailwindcss so @apply works',
70
- '// TW_PATCH_APPLY: bring tailwindcss into scope for @apply',
71
- )
72
- "css = '@import \"tailwindcss\";\\n' + css;",
73
- "css = '@import \"tailwindcss\" layer(reference);\\n' + css;",
74
- )
54
+ src = src.replace('// TW_PATCH: prepend tailwindcss','// TW_PATCH_APPLY: apply')
55
+ src = src.replace("css = '@import \"tailwindcss\";\\n' + css;","css = '@import \"tailwindcss\" layer(reference);\\n' + css;")
75
56
  writeFileSync(target, src, 'utf-8')
76
- log('Upgraded @apply patch to @reference (prevents HMR loops)')
57
+ log('Upgraded @apply patch');
77
58
  return
78
59
  }
79
-
80
- // Fresh install prepend @reference comment + tailwind for HMR safety
81
- const orig = (
82
- '\t\t\t\t\tif (css) {\n' +
83
- '\t\t\t\t\t\tconst cssId = createVirtualImportId(filename, root, \'style\');\n' +
84
- '\t\t\t\t\t\tcssCache.set(cssId, css);'
85
- )
86
- const patched = (
87
- '\t\t\t\t\tif (css) {\n' +
88
- '\t\t\t\t\t\t// TW_PATCH_APPLY: bring tailwindcss into scope for @apply\n' +
89
- "\t\t\t\t\t\tcss = '@import \"tailwindcss\" layer(reference);\\n' + css;\n" +
90
- '\t\t\t\t\t\tconst cssId = createVirtualImportId(filename, root, \'style\');\n' +
91
- '\t\t\t\t\t\tcssCache.set(cssId, css);'
92
- )
93
-
94
- const result = src.replace(orig, patched)
95
- if (result === src) { warn('Could not patch Ripple plugin for @apply'); return }
60
+ const orig = '\t\t\t\t\tif (css) {\n\t\t\t\t\t\tconst cssId = createVirtualImportId(filename, root, \'style\');\n\t\t\t\t\t\tcssCache.set(cssId, css);'
61
+ const rep = '\t\t\t\t\tif (css) {\n\t\t\t\t\t\t// TW_PATCH_APPLY: @apply support\n\t\t\t\t\t\tcss = \'@import "tailwindcss" layer(reference);\\n\' + css;\n\t\t\t\t\t\tconst cssId = createVirtualImportId(filename, root, \'style\');\n\t\t\t\t\t\tcssCache.set(cssId, css);'
62
+ const result = src.replace(orig, rep)
63
+ if (result === src) { warn('Could not patch @apply'); return }
96
64
  writeFileSync(target, result, 'utf-8')
97
- log('Patched Ripple plugin for @apply support in <style> blocks (HMR-safe)')
65
+ log('Patched Ripple plugin for @apply')
98
66
  }
99
67
 
100
- // ── Resolve helpers ────────────────────────────────────────────
101
68
  function resolveVike(rel) {
102
69
  const p = join(projectRoot, 'node_modules', 'vike', rel)
103
70
  if (existsSync(p)) return p
104
- try { return createRequire(join(projectRoot, 'noop.js')).resolve('vike/' + rel) } catch { return null }
71
+ try { return createRequire(join(projectRoot, 'package.json')).resolve('vike/' + rel) } catch { return null }
105
72
  }
106
73
 
107
74
  function resolveRipple(rel) {
108
75
  const p = join(projectRoot, 'node_modules', '@ripple-ts', 'vite-plugin', rel)
109
76
  if (existsSync(p)) return p
110
- try { return createRequire(join(projectRoot, 'noop.js')).resolve('@ripple-ts/vite-plugin/' + rel) } catch { return null }
77
+ try { return createRequire(join(projectRoot, 'package.json')).resolve('@ripple-ts/vite-plugin/' + rel) } catch { return null }
111
78
  }
112
79
 
113
- // ── Main ──────────────────────────────────────────────────────
114
80
  log('Applying patches...')
115
81
  patchVikeExtensions()
116
82
  patchRippleDirect()
@@ -3,9 +3,7 @@ export async function callCumulativeHooks(hooks, ...args) {
3
3
  for (const hook of hooks) {
4
4
  if (typeof hook === 'function') {
5
5
  const result = hook(...args)
6
- if (result && typeof result.then === 'function') {
7
- await result
8
- }
6
+ if (result && typeof result.then === 'function') await result
9
7
  }
10
8
  }
11
9
  }
@@ -1,10 +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;')}"`
1
+ export function getTagAttributesString(attrs) {
2
+ if (!attrs) return ''
3
+ return Object.entries(attrs)
4
+ .map(([k, v]) => {
5
+ if (v === true) return ` ${k}`
6
+ if (!v) return ''
7
+ return ` ${k}="${String(v).replace(/"/g, '&quot;')}"`
8
8
  })
9
9
  .join('')
10
10
  }
@@ -1,14 +0,0 @@
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
- }
@@ -1,10 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1,33 +0,0 @@
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
@@ -1,13 +0,0 @@
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