@cioky/vike-core 0.5.6

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 ADDED
@@ -0,0 +1,61 @@
1
+ # @cioky/vike-core
2
+
3
+ > ⚠️ **HIGHLY EXPERIMENTAL**
4
+
5
+ Core Vike + Ripple TS integration. See the [monorepo root](../README.md) for all packages.
6
+
7
+ Before using this package, read the [quirks & caveats](./docs/quirks.md) — it documents every bug and fix.
8
+
9
+ ## Setup
10
+
11
+ ### 1. Run setup
12
+
13
+ ```sh
14
+ npx @cioky/vike-core setup
15
+ ```
16
+
17
+ ### 2. Configure `vite.config.ts`
18
+
19
+ ```ts
20
+ import { defineConfig } from 'vite'
21
+ import vike from 'vike/plugin'
22
+ import { ripple } from '@ripple-ts/vite-plugin'
23
+ import vikeRipple from '@cioky/vike-core'
24
+
25
+ export default defineConfig({
26
+ optimizeDeps: { exclude: ['ripple'] },
27
+ plugins: [vike(), vikeRipple(), ripple({ excludeRippleExternalModules: true })],
28
+ })
29
+ ```
30
+
31
+ (Plugin order matters — `vike()` must come first.)
32
+
33
+ ### 3. Add renderer config
34
+
35
+ Create `renderer/+config.ts`:
36
+
37
+ ```ts
38
+ export default {
39
+ extends: ['import:@cioky/vike-core/config:default'],
40
+ }
41
+ ```
42
+
43
+ ## Features
44
+
45
+ | Feature | Status |
46
+ |---|---|
47
+ | `.tsrx` page files | ✅ |
48
+ | SSR rendering | ✅ |
49
+ | Client hydration | ✅ |
50
+ | Streaming SSR | ✅ |
51
+ | `<head>` tag extraction | ✅ |
52
+ | `+Layout.tsrx` / `+Head.tsrx` | ✅ |
53
+ | Tailwind CSS v4 (via `@cioky/vike-tailwindcss`) | ✅ |
54
+ | Panda CSS (via `@cioky/vike-pandacss`) | ✅ |
55
+
56
+ ## Related
57
+
58
+ - [`@cioky/vike-tailwindcss`](../@cioky/vike-tailwindcss) — Tailwind `@apply` in `<style>` blocks
59
+ - [`@cioky/vike-pandacss`](../@cioky/vike-pandacss) — Panda CSS extraction + `@apply`
60
+ - [`create-vike-ripple`](../create-vike-ripple) — Project scaffold
61
+ - [`docs/quirks.md`](./docs/quirks.md) — Known issues and fixes
package/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ declare module '@cioky/vike-core' {
2
+ import type { Plugin } from 'vite';
3
+ const vikeRipple: () => Plugin;
4
+ export default vikeRipple;
5
+ }
6
+
7
+ declare module '@cioky/vike-core/config' {
8
+ const config: Record<string, unknown>;
9
+ export default config;
10
+ }
11
+
12
+ declare module '@cioky/vike-core/usePageContext' {
13
+ import type { PageContext } from 'vike/types';
14
+ export function usePageContext(): PageContext;
15
+ export function setPageContext(ctx: PageContext): void;
16
+ }
17
+
18
+ declare module '@cioky/vike-core/useData' {
19
+ export function useData<D = unknown>(): D;
20
+ }
21
+
22
+ declare module '@cioky/vike-core/useHydrated' {
23
+ export function useHydrated(): boolean;
24
+ export function setHydrated(): void;
25
+ }
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@cioky/vike-core",
3
+ "version": "0.5.6",
4
+ "description": "Vike extension for Ripple TS — SSR, streaming, Layout, Head, SEO configs, hooks",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./index.d.ts",
9
+ "default": "./src/index.js"
10
+ },
11
+ "./config": {
12
+ "types": "./index.d.ts",
13
+ "default": "./src/config.js"
14
+ },
15
+ "./setup": {
16
+ "types": "./index.d.ts",
17
+ "default": "./src/setup.js"
18
+ },
19
+ "./usePageContext": {
20
+ "types": "./index.d.ts",
21
+ "default": "./src/hooks/usePageContext.js"
22
+ },
23
+ "./useData": {
24
+ "types": "./index.d.ts",
25
+ "default": "./src/hooks/useData.js"
26
+ },
27
+ "./useHydrated": {
28
+ "types": "./index.d.ts",
29
+ "default": "./src/hooks/useHydrated.js"
30
+ },
31
+ "./useConfig": {
32
+ "browser": "./src/hooks/useConfig/useConfig-client.js",
33
+ "default": "./src/hooks/useConfig/useConfig-server.js",
34
+ "types": "./index.d.ts"
35
+ },
36
+ "./Config": {
37
+ "browser": "./src/components/Config/Config-client.js",
38
+ "default": "./src/components/Config/Config-server.js",
39
+ "types": "./index.d.ts"
40
+ },
41
+ "./Head": {
42
+ "browser": "./src/components/Head/Head-client.js",
43
+ "default": "./src/components/Head/Head-server.js",
44
+ "types": "./index.d.ts"
45
+ },
46
+ "./clientOnly": {
47
+ "types": "./index.d.ts",
48
+ "default": "./src/helpers/clientOnly.js"
49
+ },
50
+ "./ClientOnly": {
51
+ "types": "./index.d.ts",
52
+ "default": "./src/components/ClientOnly.js"
53
+ },
54
+ "./__internal/integration/onRenderHtml": {
55
+ "types": "./index.d.ts",
56
+ "default": "./src/integration/onRenderHtml.js"
57
+ },
58
+ "./__internal/integration/onRenderClient": {
59
+ "types": "./index.d.ts",
60
+ "default": "./src/integration/onRenderClient.js"
61
+ }
62
+ },
63
+ "bin": {
64
+ "@cioky/vike-core": "src/setup.js"
65
+ },
66
+ "files": [
67
+ "src",
68
+ "index.d.ts"
69
+ ],
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "https://github.com/Opaius/vike-ripple.git"
73
+ },
74
+ "homepage": "https://github.com/Opaius/vike-ripple",
75
+ "bugs": {
76
+ "url": "https://github.com/Opaius/vike-ripple/issues"
77
+ },
78
+ "keywords": [
79
+ "vike",
80
+ "ripple",
81
+ "ripplets",
82
+ "ssr",
83
+ "vite"
84
+ ],
85
+ "license": "MIT",
86
+ "peerDependencies": {
87
+ "vike": ">=0.4.259",
88
+ "@ripple-ts/vite-plugin": ">=0.3.0",
89
+ "ripple": ">=0.1.0"
90
+ }
91
+ }
@@ -0,0 +1,7 @@
1
+ export { ClientOnly };
2
+
3
+ import { useHydrated } from '../hooks/useHydrated.js';
4
+
5
+ function ClientOnly({ children, fallback }) {
6
+ return useHydrated() ? children : (fallback ?? null);
7
+ }
@@ -0,0 +1,8 @@
1
+ export { Config };
2
+
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-client.js';
4
+
5
+ function Config(props) {
6
+ useConfig()(props);
7
+ return null;
8
+ }
@@ -0,0 +1,8 @@
1
+ export { Config };
2
+
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-server.js';
4
+
5
+ function Config(props) {
6
+ useConfig()(props);
7
+ return null;
8
+ }
@@ -0,0 +1,3 @@
1
+ export function Head() {
2
+ return null;
3
+ }
@@ -0,0 +1,8 @@
1
+ export { Head };
2
+
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-server.js';
4
+
5
+ function Head({ children }) {
6
+ useConfig()({ Head: children });
7
+ return null;
8
+ }
package/src/config.js ADDED
@@ -0,0 +1,53 @@
1
+ // ssrEffect inlined to avoid import resolution issues during config loading
2
+ function ssrEffect({ configDefinedAt, configValue }) {
3
+ if (typeof configValue !== 'boolean')
4
+ throw new Error(`${configDefinedAt} should be a boolean`);
5
+ return {
6
+ meta: {
7
+ Page: { env: { client: true, server: configValue !== false } },
8
+ Layout: { env: { client: true, server: configValue !== false } },
9
+ Wrapper: { env: { client: true, server: configValue !== false } }
10
+ }
11
+ };
12
+ }
13
+
14
+ const config = {
15
+ name: '@cioky/vike-core',
16
+ require: { vike: '>=0.4.250' },
17
+
18
+ onRenderHtml:
19
+ 'import:@cioky/vike-core/__internal/integration/onRenderHtml:onRenderHtml',
20
+ onRenderClient:
21
+ 'import:@cioky/vike-core/__internal/integration/onRenderClient:onRenderClient',
22
+
23
+ clientRouting: true,
24
+ hydrationCanBeAborted: true,
25
+
26
+ passToClient: ['_configViaHook'],
27
+
28
+ meta: {
29
+ Head: { env: { server: true }, cumulative: true },
30
+ Layout: { env: { server: true, client: true }, cumulative: true },
31
+ Wrapper: { env: { server: true, client: true }, cumulative: true },
32
+ title: { env: { server: true, client: true } },
33
+ description: { env: { server: true } },
34
+ image: { env: { server: true } },
35
+ viewport: { env: { server: true } },
36
+ favicon: { env: { server: true }, global: true },
37
+ lang: { env: { server: true, client: true } },
38
+ ssr: { env: { config: true }, effect: ssrEffect },
39
+ stream: { env: { server: true }, cumulative: true },
40
+ onBeforeRenderHtml: { env: { server: true }, cumulative: true },
41
+ onAfterRenderHtml: { env: { server: true }, cumulative: true },
42
+ onBeforeRenderClient: { env: { client: true }, cumulative: true },
43
+ onAfterRenderClient: { env: { client: true }, cumulative: true },
44
+ bodyHtmlBegin: { env: { server: true }, cumulative: true, global: true },
45
+ bodyHtmlEnd: { env: { server: true }, cumulative: true, global: true },
46
+ headHtmlBegin: { env: { server: true }, cumulative: true, global: true },
47
+ headHtmlEnd: { env: { server: true }, cumulative: true, global: true },
48
+ htmlAttributes: { env: { server: true }, global: true, cumulative: true },
49
+ bodyAttributes: { env: { server: true }, global: true, cumulative: true }
50
+ }
51
+ };
52
+
53
+ export default config;
@@ -0,0 +1,4 @@
1
+ export function clientOnly() {
2
+ console.warn('[@cioky/vike-core] clientOnly() is deprecated — use <ClientOnly>');
3
+ return (props) => props.fallback ?? null;
4
+ }
@@ -0,0 +1,15 @@
1
+ export const configsCumulative = [
2
+ 'Head',
3
+ 'Layout',
4
+ 'Wrapper',
5
+ 'bodyHtmlBegin',
6
+ 'bodyHtmlEnd',
7
+ 'headHtmlBegin',
8
+ 'headHtmlEnd',
9
+ 'htmlAttributes',
10
+ 'bodyAttributes',
11
+ 'onBeforeRenderHtml',
12
+ 'onAfterRenderHtml',
13
+ 'onBeforeRenderClient',
14
+ 'onAfterRenderClient'
15
+ ];
@@ -0,0 +1,27 @@
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 (pageContext) {
18
+ if (!('_headAlreadySet' in pageContext)) {
19
+ pageContext._configViaHook ??= {};
20
+ Object.assign(pageContext._configViaHook, config);
21
+ } else {
22
+ if (config.title) document.title = config.title;
23
+ if (config.lang) document.documentElement.lang = config.lang;
24
+ }
25
+ }
26
+ };
27
+ }
@@ -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 && !pageContext._headAlreadySet) {
20
+ pageContext._configViaHook ??= {};
21
+ Object.assign(pageContext._configViaHook, config);
22
+ }
23
+ };
24
+ }
@@ -0,0 +1,7 @@
1
+ export { useData };
2
+
3
+ import { usePageContext } from './usePageContext.js';
4
+
5
+ function useData() {
6
+ return usePageContext()?.data;
7
+ }
@@ -0,0 +1,23 @@
1
+ export { useHydrated };
2
+ export { setHydrated };
3
+
4
+ import { track } from 'ripple';
5
+
6
+ // Lazy-initialized: created on first useHydrated() call, which runs inside
7
+ // a component render where active_block is set. Module-scope track() would
8
+ // leave tracked.b === null, crashing set() → "Cannot read properties of null (reading 'f')".
9
+ let _hydrated = null;
10
+
11
+ function useHydrated() {
12
+ if (typeof window === 'undefined') return false;
13
+ if (_hydrated === null) {
14
+ _hydrated = track(false);
15
+ }
16
+ return _hydrated ? _hydrated.value : false;
17
+ }
18
+
19
+ function setHydrated() {
20
+ if (_hydrated) {
21
+ _hydrated.value = true;
22
+ }
23
+ }
@@ -0,0 +1,37 @@
1
+ export { usePageContext };
2
+ export { setPageContext };
3
+
4
+ import { track } from 'ripple';
5
+
6
+ /** Tracked signal for the client-side page context. Created lazily inside
7
+ * usePageContext() where an active component block exists — see ponytail note. */
8
+ let _clientPageContext = null;
9
+
10
+ /** Pending value stored by setPageContext before the signal is created. */
11
+ let _pendingPageContext = null;
12
+
13
+ if (typeof window !== 'undefined') {
14
+ // ponytail: track() needs an active component block (b parameter), which only exists
15
+ // during component render. Module-scope track(null) leaves tracked.b = null,
16
+ // causing set() to crash on null.f access in the schedule-update path.
17
+ // We defer signal creation to usePageContext() where the block is available.
18
+ // Upgrade: if Ripple adds a block-less signal mode, switch to it here.
19
+ }
20
+
21
+ function usePageContext() {
22
+ if (typeof window === 'undefined') {
23
+ const storage = globalThis.__ripple_page_context_storage;
24
+ return storage ? storage.getStore() : null;
25
+ }
26
+ if (!_clientPageContext) {
27
+ _clientPageContext = track(_pendingPageContext);
28
+ }
29
+ return _clientPageContext ? _clientPageContext.value : null;
30
+ }
31
+
32
+ function setPageContext(ctx) {
33
+ _pendingPageContext = ctx;
34
+ if (_clientPageContext) {
35
+ _clientPageContext.value = ctx;
36
+ }
37
+ }
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export default function vikeRipple() {
2
+ return {
3
+ name: '@cioky/vike-core',
4
+ enforce: 'pre'
5
+ };
6
+ }
@@ -0,0 +1,7 @@
1
+ export { getHeadSetting };
2
+
3
+ function getHeadSetting(key, pageContext) {
4
+ const v = pageContext.config[key];
5
+ if (v !== undefined && v !== null) return v;
6
+ return pageContext._configViaHook?.[key] ?? null;
7
+ }
@@ -0,0 +1,100 @@
1
+ export { onRenderClient };
2
+
3
+ import { setPageContext } from '../hooks/usePageContext.js';
4
+ import { setHydrated } from '../hooks/useHydrated.js';
5
+
6
+ // tsrx_element — wraps a component fn as a Ripple TSRX element (matches ripple/internal)
7
+ const tsrx_element = (fn) => ({
8
+ render: fn,
9
+ [Symbol.for('ripple.element')]: true
10
+ });
11
+
12
+ /** @type {(() => void) | null} */
13
+ let dispose = null;
14
+
15
+ const onRenderClient = async (pageContext) => {
16
+ const { Page, config } = pageContext;
17
+ if (!Page) return;
18
+
19
+ setPageContext(pageContext);
20
+ const container = document.getElementById('root');
21
+ if (!container) return;
22
+
23
+ // ── Build same component tree as SSR ──
24
+ // Apply Layouts (innermost first → outermost last)
25
+ // Children must be tsrx_element(() => Component({})) — NOT tsrx_element(Component).
26
+ // The normal form passes Component as the render function; Ripple's render_tsrx_element
27
+ // calls it with (anchor, block), which Component interprets as props, corrupting block tracking.
28
+ // The () => Component({}) wrapper gives Component the proper props object.
29
+ const Layout = config.Layout ?? config.layout;
30
+ const Wrapper = config.Wrapper ?? config.wrapper;
31
+ let component = Page;
32
+ if (Layout) {
33
+ const layouts = Array.isArray(Layout) ? Layout : [Layout];
34
+ for (let i = 0; i < layouts.length; i++) {
35
+ const L = layouts[i];
36
+ const prev = component;
37
+ component = (props) =>
38
+ L({ ...props, children: tsrx_element(() => prev({})) });
39
+ }
40
+ }
41
+ if (Wrapper) {
42
+ const wrappers = Array.isArray(Wrapper) ? Wrapper : [Wrapper];
43
+ for (const W of wrappers) {
44
+ const prev = component;
45
+ component = (props) =>
46
+ W({ ...props, children: tsrx_element(() => prev({})) });
47
+ }
48
+ }
49
+
50
+ // ── Clean up previous root ──
51
+ if (dispose) {
52
+ dispose();
53
+ dispose = null;
54
+ }
55
+
56
+ // Always use mount() — hydrate() crashes Ripple's hmr wrapper (hydrate_node is null).
57
+ const { mount } = await import('ripple');
58
+ dispose = mount(component, { target: container, props: {} });
59
+
60
+ // Stamp root with rendered pageId so Vike's nav guard can verify rendering took effect
61
+ container.dataset.vikeRendered = pageContext.pageId;
62
+
63
+ // Update document title and lang on page transitions
64
+ const title = config.title;
65
+ if (title) {
66
+ document.title = typeof title === 'function' ? title(pageContext) : title;
67
+ }
68
+ const lang = config.lang;
69
+ if (lang) {
70
+ document.documentElement.lang = lang;
71
+ }
72
+
73
+ setHydrated();
74
+ };
75
+
76
+ // ponytail: Vike client routing fallback — intercept unhandled link clicks and hard-nav.
77
+ // Vike's initOnLinkClick sometimes doesn't intercept clicks reliably; this ensures
78
+ // navigation always works by catching any click Vike's handler didn't prevent.
79
+ if (typeof document !== 'undefined') {
80
+ document.addEventListener(
81
+ 'click',
82
+ (ev) => {
83
+ if (ev.defaultPrevented) return;
84
+ const link = ev.target?.closest?.('a');
85
+ if (!link) return;
86
+ const href = link.getAttribute('href');
87
+ if (!href) return;
88
+ if (
89
+ href.startsWith('#') ||
90
+ href.startsWith('http') ||
91
+ link.target === '_blank'
92
+ )
93
+ return;
94
+ if (link.hostname && link.hostname !== location.hostname) return;
95
+ ev.preventDefault();
96
+ window.location.href = href;
97
+ },
98
+ { passive: false }
99
+ );
100
+ }
@@ -0,0 +1,172 @@
1
+ export { onRenderHtml };
2
+
3
+ import { render, create_ssr_stream } from 'ripple/server';
4
+ import { tsrx_element } from 'ripple/internal/server';
5
+ import { escapeInject, dangerouslySkipEscape } from 'vike/server';
6
+ import { setPageContext } from '../hooks/usePageContext.js';
7
+ import { getHeadSetting } from './getHeadSetting.js';
8
+ import { getTagAttributesString } from '../utils/getTagAttributesString.js';
9
+ import { callCumulativeHooks } from '../utils/callCumulativeHooks.js';
10
+
11
+ import { AsyncLocalStorage } from 'node:async_hooks';
12
+ globalThis.__ripple_page_context_storage ??= new AsyncLocalStorage();
13
+
14
+ const onRenderHtml = async (pageContext) => {
15
+ return globalThis.__ripple_page_context_storage.run(pageContext, async () => {
16
+ const { Page } = pageContext;
17
+ if (!Page) throw new Error('No Page');
18
+
19
+ await callCumulativeHooks(
20
+ pageContext.config.onBeforeRenderHtml,
21
+ pageContext
22
+ );
23
+
24
+ setPageContext(pageContext);
25
+
26
+ const headHtml = getHeadHtml(pageContext);
27
+ const { headHtmlBegin, headHtmlEnd, bodyHtmlBegin, bodyHtmlEnd } =
28
+ getHtmlInjections(pageContext);
29
+ const { htmlAttributesString, bodyAttributesString } =
30
+ getTagAttributes(pageContext);
31
+
32
+ // Wrap in Layout(s) + Wrapper(s)
33
+ let wrappedPage = Page;
34
+ const Layout = pageContext.config.Layout;
35
+ const Wrapper = pageContext.config.Wrapper;
36
+ if (Layout) {
37
+ const layouts = Array.isArray(Layout) ? Layout : [Layout];
38
+ for (let i = 0; i < layouts.length; i++) {
39
+ const L = layouts[i];
40
+ const prev = wrappedPage;
41
+ wrappedPage = (props) =>
42
+ L({ ...props, children: tsrx_element(() => prev({})) });
43
+ }
44
+ }
45
+ if (Wrapper) {
46
+ const wrappers = Array.isArray(Wrapper) ? Wrapper : [Wrapper];
47
+ for (const W of wrappers) {
48
+ const prev = wrappedPage;
49
+ wrappedPage = (props) =>
50
+ W({ ...props, children: tsrx_element(() => prev({})) });
51
+ }
52
+ }
53
+
54
+ const enableStream = !!(
55
+ pageContext.config.stream ?? pageContext.config.rippleStream
56
+ );
57
+
58
+ if (enableStream) {
59
+ const rippleStream = create_ssr_stream();
60
+ render(wrappedPage, { stream: rippleStream.sink }).catch((e) => {
61
+ console.error('[ripple] render err:', e?.message);
62
+ });
63
+ return escapeInject`<!DOCTYPE html>
64
+ <html${dangerouslySkipEscape(htmlAttributesString)}>
65
+ <head>
66
+ <meta charset="UTF-8" />
67
+ ${dangerouslySkipEscape(headHtmlBegin)}
68
+ ${dangerouslySkipEscape(headHtml)}
69
+ ${dangerouslySkipEscape(headHtmlEnd)}
70
+ </head>
71
+ <body${dangerouslySkipEscape(bodyAttributesString)}>
72
+ ${dangerouslySkipEscape(bodyHtmlBegin)}
73
+ <div id="root">${rippleStream.stream}</div>
74
+ ${dangerouslySkipEscape(bodyHtmlEnd)}
75
+ </body>
76
+ </html>`;
77
+ }
78
+
79
+ let renderFn = () => render(wrappedPage, {});
80
+ if (typeof pageContext.ssrContextWrapper === 'function') {
81
+ renderFn = () => pageContext.ssrContextWrapper(() => render(wrappedPage, {}));
82
+ }
83
+ const { head, body, css, topLevelError } = await renderFn();
84
+ if (topLevelError) {
85
+ console.error('[@cioky/vike-core] SSR render error:', topLevelError);
86
+ throw topLevelError;
87
+ }
88
+
89
+ // Ripple's render() already extracts <head> content into `head` and CSS into `css`
90
+ const cssHtml = css?.size
91
+ ? `<style data-ripple-ssr>${[...css].join('')}<` + `/style>`
92
+ : '';
93
+
94
+ pageContext.pageHtmlString = body;
95
+ await callCumulativeHooks(
96
+ pageContext.config.onAfterRenderHtml,
97
+ pageContext
98
+ );
99
+
100
+ return escapeInject`<!DOCTYPE html>
101
+ <html${dangerouslySkipEscape(htmlAttributesString)}>
102
+ <head>
103
+ <meta charset="UTF-8" />
104
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
105
+ ${dangerouslySkipEscape(headHtmlBegin)}
106
+ ${dangerouslySkipEscape(head)}
107
+ ${dangerouslySkipEscape(cssHtml)}
108
+ ${dangerouslySkipEscape(headHtml)}
109
+ ${dangerouslySkipEscape(headHtmlEnd)}
110
+ </head>
111
+ <body${dangerouslySkipEscape(bodyAttributesString)}>
112
+ ${dangerouslySkipEscape(bodyHtmlBegin)}
113
+ <div id="root">${dangerouslySkipEscape(body)}</div>
114
+ ${dangerouslySkipEscape(bodyHtmlEnd)}
115
+ </body>
116
+ </html>`;
117
+ });
118
+ };
119
+
120
+ function getHeadHtml(pageContext) {
121
+ const favicon = getHeadSetting('favicon', pageContext);
122
+ const title = getHeadSetting('title', pageContext);
123
+ const description = getHeadSetting('description', pageContext);
124
+ const image = getHeadSetting('image', pageContext);
125
+
126
+ const parts = [];
127
+ if (favicon) parts.push(`<link rel="icon" href="${favicon}" />`);
128
+ if (title) parts.push(`<title>${title}</title>`);
129
+ if (description)
130
+ parts.push(`<meta name="description" content="${description}" />`);
131
+ if (image) parts.push(`<meta property="og:image" content="${image}">`);
132
+ const viewportTag = getViewportTag(getHeadSetting('viewport', pageContext));
133
+ if (viewportTag) parts.push(viewportTag);
134
+ const headElements = [
135
+ ...(pageContext.config.Head ?? []),
136
+ ...(pageContext._configViaHook?.Head ?? [])
137
+ ]
138
+ .filter(Boolean)
139
+ .map((h) => (typeof h === 'function' ? h(pageContext) : h))
140
+ .join('\n');
141
+ if (headElements) parts.push(headElements);
142
+ return parts.join('\n');
143
+ }
144
+
145
+ function getViewportTag(viewport) {
146
+ if (!viewport && viewport !== 0) return '';
147
+ if (viewport === 'responsive')
148
+ return '<meta name="viewport" content="width=device-width, initial-scale=1.0" />';
149
+ if (typeof viewport === 'number')
150
+ return `<meta name="viewport" content="width=${viewport}" />`;
151
+ return '';
152
+ }
153
+
154
+ function getTagAttributes(pageContext) {
155
+ return {
156
+ htmlAttributesString: getTagAttributesString(
157
+ pageContext.config.htmlAttributes
158
+ ),
159
+ bodyAttributesString: getTagAttributesString(
160
+ pageContext.config.bodyAttributes
161
+ )
162
+ };
163
+ }
164
+
165
+ function getHtmlInjections(pageContext) {
166
+ return {
167
+ headHtmlBegin: (pageContext.config.headHtmlBegin ?? []).join('\n'),
168
+ headHtmlEnd: (pageContext.config.headHtmlEnd ?? []).join('\n'),
169
+ bodyHtmlBegin: (pageContext.config.bodyHtmlBegin ?? []).join('\n'),
170
+ bodyHtmlEnd: (pageContext.config.bodyHtmlEnd ?? []).join('\n')
171
+ };
172
+ }
@@ -0,0 +1,13 @@
1
+ export { ssrEffect };
2
+
3
+ function ssrEffect({ configDefinedAt, configValue }) {
4
+ if (typeof configValue !== 'boolean')
5
+ throw new Error(`${configDefinedAt} should be a boolean`);
6
+ return {
7
+ meta: {
8
+ Page: { env: { client: true, server: configValue !== false } },
9
+ Layout: { env: { client: true, server: configValue !== false } },
10
+ Wrapper: { env: { client: true, server: configValue !== false } }
11
+ }
12
+ };
13
+ }
package/src/setup.js ADDED
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'module';
3
+ import { join } from 'path';
4
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
5
+
6
+ const projectRoot = process.cwd();
7
+ let exitCode = 0;
8
+
9
+ function log(m) {
10
+ console.log('[@cioky/vike-core]', m);
11
+ }
12
+ function warn(m) {
13
+ console.warn('[@cioky/vike-core]', m);
14
+ }
15
+
16
+ function patchVikeExtensions() {
17
+ const target = resolveVike('dist/utils/isScriptFile.js');
18
+ if (!target) {
19
+ warn('vike not found');
20
+ return;
21
+ }
22
+ let src = readFileSync(target, 'utf-8');
23
+ if (src.includes("'tsrx'")) {
24
+ log('.tsrx already registered');
25
+ return;
26
+ }
27
+ const patched = src.replace(
28
+ 'const scriptFileExtensionList = [...extJsOrTs, ...extJsxOrTsx, ...extTemplates];',
29
+ "const scriptFileExtensionList = [...extJsOrTs, ...extJsxOrTsx, ...extTemplates, 'tsrx'];"
30
+ );
31
+ if (patched === src) {
32
+ warn('Could not patch Vike');
33
+ exitCode = 1;
34
+ return;
35
+ }
36
+ writeFileSync(target, patched, 'utf-8');
37
+ log('Registered .tsrx extension');
38
+ }
39
+
40
+ function patchRippleDirect() {
41
+ const target = resolveRipple('src/index.js');
42
+ if (!target) {
43
+ warn('@ripple-ts/vite-plugin not found');
44
+ return;
45
+ }
46
+ let src = readFileSync(target, 'utf-8');
47
+ // ponytail: use the actual code pattern as the idempotency guard,
48
+ // since no comment marker was ever written into the patched file.
49
+ if (src.includes("id.includes('?direct')")) {
50
+ log('?direct fix already applied');
51
+ return;
52
+ }
53
+ const patched = src.replace(
54
+ 'if (cssCache.has(id)) {\n\t\t\t\t\treturn cssCache.get(id);\n\t\t\t\t}',
55
+ `if (cssCache.has(id)) {
56
+ \t\t\t\t\treturn cssCache.get(id);
57
+ \t\t\t\t}
58
+ // @cioky/vike-core: resolve ?direct CSS imports from the cache
59
+ \t\t\t\tif (id.includes('?direct')) {
60
+ \t\t\t\t\tconst baseId = id.replace('?direct', '');
61
+ \t\t\t\t\tif (cssCache.has(baseId)) {
62
+ \t\t\t\t\t\treturn cssCache.get(baseId);
63
+ \t\t\t\t\t}
64
+ \t\t\t\t}`
65
+ );
66
+ if (patched === src) {
67
+ warn('Could not patch Ripple plugin');
68
+ exitCode = 1;
69
+ return;
70
+ }
71
+ writeFileSync(target, patched, 'utf-8');
72
+ log('Patched Ripple plugin for ?direct CSS loading');
73
+ }
74
+
75
+ function patchRippleApply() {
76
+ const target = resolveRipple('src/index.js');
77
+ if (!target) return;
78
+ let src = readFileSync(target, 'utf-8');
79
+ if (src.includes('TW_PATCH_APPLY')) {
80
+ log('@apply patch already applied');
81
+ return;
82
+ }
83
+ if (src.includes('TW_PATCH:')) {
84
+ src = src.replace(
85
+ '// TW_PATCH: prepend tailwindcss',
86
+ '// TW_PATCH_APPLY: apply'
87
+ );
88
+ src = src.replace(
89
+ 'css = \'@import "tailwindcss";\\n\' + css;',
90
+ 'css = \'@import "tailwindcss" layer(reference);\\n\' + css;'
91
+ );
92
+ writeFileSync(target, src, 'utf-8');
93
+ log('Upgraded @apply patch');
94
+ return;
95
+ }
96
+ const orig =
97
+ "\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);";
98
+ const rep =
99
+ "\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);";
100
+ const result = src.replace(orig, rep);
101
+ if (result === src) {
102
+ warn('Could not patch @apply');
103
+ return;
104
+ }
105
+ writeFileSync(target, result, 'utf-8');
106
+ log('Patched Ripple plugin for @apply');
107
+ }
108
+ function patchRippleJsxLang() {
109
+ const target = resolveRipple('src/index.js');
110
+ if (!target) {
111
+ warn('@ripple-ts/vite-plugin not found');
112
+ return;
113
+ }
114
+ let src = readFileSync(target, 'utf-8');
115
+ if (src.includes('lang: \'jsx\'')) {
116
+ log('JSX lang hint already applied');
117
+ return;
118
+ }
119
+ const patched = src.replace(
120
+ 'return { code, map };',
121
+ 'return { code, map, lang: \'jsx\' };'
122
+ );
123
+ if (patched === src) {
124
+ warn('Could not patch transform return');
125
+ exitCode = 1;
126
+ return;
127
+ }
128
+ writeFileSync(target, patched, 'utf-8');
129
+ log('Patched Ripple plugin transform — lang: jsx');
130
+ }
131
+
132
+ function resolveVike(rel) {
133
+ const p = join(projectRoot, 'node_modules', 'vike', rel);
134
+ if (existsSync(p)) return p;
135
+ try {
136
+ return createRequire(join(projectRoot, 'package.json')).resolve(
137
+ 'vike/' + rel
138
+ );
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ function resolveRipple(rel) {
145
+ const p = join(projectRoot, 'node_modules', '@ripple-ts', 'vite-plugin', rel);
146
+ if (existsSync(p)) return p;
147
+ try {
148
+ return createRequire(join(projectRoot, 'package.json')).resolve(
149
+ '@ripple-ts/vite-plugin/' + rel
150
+ );
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ function resolveRipplePackage(rel) {
157
+ const p = join(projectRoot, 'node_modules', 'ripple', rel);
158
+ if (existsSync(p)) return p;
159
+ try {
160
+ return createRequire(join(projectRoot, 'package.json')).resolve(
161
+ 'ripple/' + rel
162
+ );
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+
168
+ function patchRippleServer() {
169
+ const serverIndexFile = resolveRipplePackage(
170
+ 'src/runtime/internal/server/index.js'
171
+ );
172
+ const serverBlocksFile = resolveRipplePackage(
173
+ 'src/runtime/internal/server/blocks.js'
174
+ );
175
+ if (!serverIndexFile || !serverBlocksFile) {
176
+ warn('ripple package not found, skipping server isolation patch');
177
+ return;
178
+ }
179
+
180
+ let indexContent = readFileSync(serverIndexFile, 'utf8');
181
+ if (indexContent.includes('const rippleSsrStorage =')) {
182
+ log('Ripple server isolation already applied to index.js');
183
+ } else {
184
+ const storageSetup = `
185
+ import { AsyncLocalStorage } from 'node:async_hooks';
186
+
187
+ const rippleSsrStorage = new AsyncLocalStorage();
188
+
189
+ const defaultContext = () => ({
190
+ active_component: null,
191
+ active_block: null,
192
+ tracking: false,
193
+ active_dependency: null,
194
+ inside_async_track: false,
195
+ current_element: undefined,
196
+ seen_warnings: new Set(),
197
+ clock: 0
198
+ });
199
+
200
+ function getStore() {
201
+ let store = rippleSsrStorage.getStore();
202
+ if (!store) {
203
+ if (!globalThis.__ripple_fallback_store) {
204
+ globalThis.__ripple_fallback_store = defaultContext();
205
+ }
206
+ return globalThis.__ripple_fallback_store;
207
+ }
208
+ return store;
209
+ }
210
+
211
+ const varsToIsolate = [
212
+ 'active_component',
213
+ 'active_block',
214
+ 'tracking',
215
+ 'active_dependency',
216
+ 'inside_async_track',
217
+ 'current_element',
218
+ 'seen_warnings',
219
+ 'clock'
220
+ ];
221
+
222
+ for (const v of varsToIsolate) {
223
+ Object.defineProperty(globalThis, v, {
224
+ get() {
225
+ return getStore()[v];
226
+ },
227
+ set(val) {
228
+ getStore()[v] = val;
229
+ },
230
+ configurable: true
231
+ });
232
+ }
233
+ `;
234
+ const lastImportIdx = indexContent.lastIndexOf('import ');
235
+ const endOfLastImportLine = indexContent.indexOf('\n', lastImportIdx);
236
+ indexContent =
237
+ indexContent.slice(0, endOfLastImportLine + 1) +
238
+ storageSetup +
239
+ indexContent.slice(endOfLastImportLine + 1);
240
+
241
+ const renderStartText =
242
+ 'export async function render(component, passed_in_options = {}) {';
243
+ const renderStartIdx = indexContent.indexOf(renderStartText);
244
+ if (renderStartIdx === -1) {
245
+ warn('Could not find render function in ripple server/index.js');
246
+ exitCode = 1;
247
+ return;
248
+ }
249
+
250
+ const renderBodyStart = renderStartIdx + renderStartText.length - 1;
251
+ let bracketCount = 1;
252
+ let i = renderBodyStart + 1;
253
+ while (bracketCount > 0 && i < indexContent.length) {
254
+ if (indexContent[i] === '{') bracketCount++;
255
+ if (indexContent[i] === '}') bracketCount--;
256
+ i++;
257
+ }
258
+ const renderBodyEnd = i - 1;
259
+
260
+ const renderBody = indexContent.slice(renderBodyStart + 1, renderBodyEnd);
261
+ const patchedRender = `{
262
+ return rippleSsrStorage.run(defaultContext(), async () => {
263
+ ${renderBody}
264
+ });
265
+ }`;
266
+ indexContent =
267
+ indexContent.slice(0, renderBodyStart) +
268
+ patchedRender +
269
+ indexContent.slice(renderBodyEnd + 1);
270
+
271
+ const vars = [
272
+ 'active_block',
273
+ 'active_component',
274
+ 'tracking',
275
+ 'active_dependency',
276
+ 'inside_async_track',
277
+ 'current_element',
278
+ 'seen_warnings',
279
+ 'clock'
280
+ ];
281
+
282
+ for (const v of vars) {
283
+ const regex = new RegExp(`\\b${v}\\b`, 'g');
284
+ indexContent = indexContent.replace(regex, '__' + v);
285
+ }
286
+
287
+ indexContent = indexContent.replace(
288
+ 'export let __active_component = null;',
289
+ 'export let active_component = null;'
290
+ );
291
+ indexContent = indexContent.replace(
292
+ 'export let __active_block = null;',
293
+ 'export let active_block = null;'
294
+ );
295
+ indexContent = indexContent.replace(
296
+ 'export let __tracking = false;',
297
+ 'export let tracking = false;'
298
+ );
299
+
300
+ writeFileSync(serverIndexFile, indexContent, 'utf8');
301
+ log('Patched Ripple server index.js for request isolation');
302
+ }
303
+
304
+ let blocksContent = readFileSync(serverBlocksFile, 'utf8');
305
+ if (
306
+ blocksContent.includes('__active_block') &&
307
+ !blocksContent.includes('\tactive_block,\n')
308
+ ) {
309
+ log('Ripple server isolation already applied to blocks.js');
310
+ } else {
311
+ // Remove isolated variables from imports list first so they fallback to globalThis lookup
312
+ blocksContent = blocksContent.replace('\tactive_block,\n', '');
313
+ blocksContent = blocksContent.replace('\tactive_component,\n', '');
314
+
315
+ const vars = [
316
+ 'active_block',
317
+ 'active_component',
318
+ 'tracking',
319
+ 'active_dependency',
320
+ 'inside_async_track',
321
+ 'current_element',
322
+ 'seen_warnings',
323
+ 'clock'
324
+ ];
325
+ for (const v of vars) {
326
+ const regex = new RegExp(`\\b${v}\\b`, 'g');
327
+ blocksContent = blocksContent.replace(regex, '__' + v);
328
+ }
329
+
330
+ writeFileSync(serverBlocksFile, blocksContent, 'utf8');
331
+ log('Patched Ripple server blocks.js for request isolation');
332
+ }
333
+ }
334
+ function patchRippleSetNullBlock() {
335
+ const runtimeFile = resolveRipplePackage(
336
+ 'src/runtime/internal/client/runtime.js'
337
+ );
338
+ if (!runtimeFile) {
339
+ warn('ripple client runtime not found, skipping set() null-block patch');
340
+ return;
341
+ }
342
+ let src = readFileSync(runtimeFile, 'utf-8');
343
+ if (src.includes('/* patch: null-block guard */')) {
344
+ log('Ripple set() null-block guard already applied');
345
+ return;
346
+ }
347
+ // Patch 1: guard against null block in teardown check
348
+ src = src.replace(
349
+ 'if ((tracked_block.f & CONTAINS_TEARDOWN) !== 0) {',
350
+ 'if (tracked_block !== null && (tracked_block.f & CONTAINS_TEARDOWN) !== 0) { /* patch: null-block guard */'
351
+ );
352
+ // Patch 2: guard against null block in schedule_update call
353
+ src = src.replace(
354
+ 'schedule_update(tracked_block);',
355
+ 'if (tracked_block !== null) schedule_update(tracked_block); /* patch: null-block guard */'
356
+ );
357
+ writeFileSync(runtimeFile, src, 'utf-8');
358
+ log('Patched Ripple set() — null-block guard applied');
359
+ }
360
+ function patchVikeClientRouting() {
361
+ const target = resolveVike(
362
+ 'dist/client/runtime-client-routing/renderPageClient.js'
363
+ );
364
+ if (!target) {
365
+ warn('vike client router not found');
366
+ return;
367
+ }
368
+ let src = readFileSync(target, 'utf-8');
369
+ if (src.includes('@cioky/vike-core nav guard')) {
370
+ log('Vike client routing guard already applied');
371
+ return;
372
+ }
373
+ // After changeUrl() + execHookOnRenderClient(), verify the page actually rendered.
374
+ // onRenderClient stamps #root dataset with the pageId on success; if missing, hard-nav.
375
+ const marker = ` if (!isErrorPage && !isFirstRender && !onRenderClientError) {
376
+ // @cioky/vike-core nav guard: verify rendering took effect
377
+ const root = document.getElementById('root');
378
+ if (root && root.dataset.vikeRendered !== pageContext.pageId) {
379
+ window.location.href = urlOriginal;
380
+ return;
381
+ }
382
+ }`;
383
+ // Insert after the onRenderClientError block that calls onError
384
+ const orig = ` if (!isErrorPage)
385
+ return;`;
386
+ // We target the specific onRenderClientError return, not the onHydrationEnd one
387
+ const fullOrig = ` if (onRenderClientError) {
388
+ await onError(onRenderClientError);
389
+ if (!isErrorPage)
390
+ return;
391
+ }`;
392
+ const fullRep = ` if (onRenderClientError) {
393
+ await onError(onRenderClientError);
394
+ if (!isErrorPage)
395
+ return;
396
+ }${marker}`;
397
+ const result = src.replace(fullOrig, fullRep);
398
+ if (result === src) {
399
+ warn('Could not patch Vike renderPageClient');
400
+ exitCode = 1;
401
+ return;
402
+ }
403
+ writeFileSync(target, result, 'utf-8');
404
+ }
405
+ log('Applying patches...');
406
+ patchVikeExtensions();
407
+ patchRippleDirect();
408
+ patchRippleApply();
409
+ patchRippleServer();
410
+ patchRippleSetNullBlock();
411
+ patchVikeClientRouting();
412
+ patchRippleJsxLang();
413
+ log('Done');
414
+ process.exit(exitCode);
@@ -0,0 +1,9 @@
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') await result;
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,10 @@
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
+ })
9
+ .join('');
10
+ }