@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 +61 -0
- package/index.d.ts +25 -0
- package/package.json +91 -0
- package/src/components/ClientOnly.js +7 -0
- package/src/components/Config/Config-client.js +8 -0
- package/src/components/Config/Config-server.js +8 -0
- package/src/components/Head/Head-client.js +3 -0
- package/src/components/Head/Head-server.js +8 -0
- package/src/config.js +53 -0
- package/src/helpers/clientOnly.js +4 -0
- package/src/hooks/useConfig/configsCumulative.js +15 -0
- package/src/hooks/useConfig/useConfig-client.js +27 -0
- package/src/hooks/useConfig/useConfig-server.js +24 -0
- package/src/hooks/useData.js +7 -0
- package/src/hooks/useHydrated.js +23 -0
- package/src/hooks/usePageContext.js +37 -0
- package/src/index.js +6 -0
- package/src/integration/getHeadSetting.js +7 -0
- package/src/integration/onRenderClient.js +100 -0
- package/src/integration/onRenderHtml.js +172 -0
- package/src/integration/ssrEffect.js +13 -0
- package/src/setup.js +414 -0
- package/src/utils/callCumulativeHooks.js +9 -0
- package/src/utils/getTagAttributesString.js +10 -0
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
|
+
}
|
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,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,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,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
|
+
}
|