@pyreon/server 0.11.5 → 0.11.7
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 +15 -15
- package/lib/client.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +2 -2
- package/package.json +16 -16
- package/src/client.ts +28 -28
- package/src/handler.ts +18 -18
- package/src/html.ts +12 -12
- package/src/index.ts +9 -9
- package/src/island.ts +12 -12
- package/src/ssg.ts +9 -9
- package/src/tests/client.test.ts +90 -90
- package/src/tests/server.test.ts +303 -303
package/src/html.ts
CHANGED
|
@@ -36,26 +36,26 @@ export interface CompiledTemplate {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function compileTemplate(template: string): CompiledTemplate {
|
|
39
|
-
if (!template.includes(
|
|
40
|
-
throw new Error(
|
|
39
|
+
if (!template.includes('<!--pyreon-app-->')) {
|
|
40
|
+
throw new Error('[pyreon/server] Template must contain <!--pyreon-app--> placeholder')
|
|
41
41
|
}
|
|
42
|
-
const [beforeHead, afterHead] = splitOnce(template,
|
|
43
|
-
const [betweenHeadApp, afterApp] = splitOnce(afterHead,
|
|
44
|
-
const [betweenAppScripts, afterScripts] = splitOnce(afterApp,
|
|
42
|
+
const [beforeHead, afterHead] = splitOnce(template, '<!--pyreon-head-->')
|
|
43
|
+
const [betweenHeadApp, afterApp] = splitOnce(afterHead, '<!--pyreon-app-->')
|
|
44
|
+
const [betweenAppScripts, afterScripts] = splitOnce(afterApp, '<!--pyreon-scripts-->')
|
|
45
45
|
return { parts: [beforeHead, betweenHeadApp, betweenAppScripts, afterScripts] }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function splitOnce(str: string, delimiter: string): [string, string] {
|
|
49
49
|
const idx = str.indexOf(delimiter)
|
|
50
|
-
if (idx === -1) return [str,
|
|
50
|
+
if (idx === -1) return [str, '']
|
|
51
51
|
return [str.slice(0, idx), str.slice(idx + delimiter.length)]
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function processTemplate(template: string, data: TemplateData): string {
|
|
55
55
|
return template
|
|
56
|
-
.replace(
|
|
57
|
-
.replace(
|
|
58
|
-
.replace(
|
|
56
|
+
.replace('<!--pyreon-head-->', data.head)
|
|
57
|
+
.replace('<!--pyreon-app-->', data.app)
|
|
58
|
+
.replace('<!--pyreon-scripts-->', data.scripts)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** Fast path using a pre-compiled template */
|
|
@@ -79,13 +79,13 @@ export function buildScripts(
|
|
|
79
79
|
|
|
80
80
|
if (loaderData && Object.keys(loaderData).length > 0) {
|
|
81
81
|
// Escape </script> inside JSON to prevent premature tag close
|
|
82
|
-
const json = JSON.stringify(loaderData).replace(/<\//g,
|
|
82
|
+
const json = JSON.stringify(loaderData).replace(/<\//g, '<\\/')
|
|
83
83
|
parts.push(`<script>window.__PYREON_LOADER_DATA__=${json}</script>`)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
parts.push(`<script type="module" src="${clientEntry}"></script>`)
|
|
87
87
|
|
|
88
|
-
return parts.join(
|
|
88
|
+
return parts.join('\n ')
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** Pre-build the static client entry script tag (invariant across requests) */
|
|
@@ -99,7 +99,7 @@ export function buildScriptsFast(
|
|
|
99
99
|
loaderData: Record<string, unknown> | null,
|
|
100
100
|
): string {
|
|
101
101
|
if (loaderData && Object.keys(loaderData).length > 0) {
|
|
102
|
-
const json = JSON.stringify(loaderData).replace(/<\//g,
|
|
102
|
+
const json = JSON.stringify(loaderData).replace(/<\//g, '<\\/')
|
|
103
103
|
return `<script>window.__PYREON_LOADER_DATA__=${json}</script>\n ${clientEntryTag}`
|
|
104
104
|
}
|
|
105
105
|
return clientEntryTag
|
package/src/index.ts
CHANGED
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
* ```
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
|
-
export type { HandlerOptions } from
|
|
55
|
+
export type { HandlerOptions } from './handler'
|
|
56
56
|
// SSR handler
|
|
57
|
-
export { createHandler } from
|
|
58
|
-
export type { CompiledTemplate, TemplateData } from
|
|
57
|
+
export { createHandler } from './handler'
|
|
58
|
+
export type { CompiledTemplate, TemplateData } from './html'
|
|
59
59
|
// HTML template
|
|
60
60
|
export {
|
|
61
61
|
buildScripts,
|
|
@@ -63,13 +63,13 @@ export {
|
|
|
63
63
|
DEFAULT_TEMPLATE,
|
|
64
64
|
processCompiledTemplate,
|
|
65
65
|
processTemplate,
|
|
66
|
-
} from
|
|
67
|
-
export type { HydrationStrategy, IslandMeta, IslandOptions } from
|
|
66
|
+
} from './html'
|
|
67
|
+
export type { HydrationStrategy, IslandMeta, IslandOptions } from './island'
|
|
68
68
|
// Islands
|
|
69
|
-
export { island } from
|
|
69
|
+
export { island } from './island'
|
|
70
70
|
|
|
71
71
|
// Middleware
|
|
72
|
-
export type { Middleware, MiddlewareContext } from
|
|
73
|
-
export type { PrerenderOptions, PrerenderResult } from
|
|
72
|
+
export type { Middleware, MiddlewareContext } from './middleware'
|
|
73
|
+
export type { PrerenderOptions, PrerenderResult } from './ssg'
|
|
74
74
|
// SSG
|
|
75
|
-
export { prerender } from
|
|
75
|
+
export { prerender } from './ssg'
|
package/src/island.ts
CHANGED
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
* - "never" — never hydrate (render-only, no client JS)
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
|
-
import type { ComponentFn, Props, VNode } from
|
|
56
|
-
import { h } from
|
|
55
|
+
import type { ComponentFn, Props, VNode } from '@pyreon/core'
|
|
56
|
+
import { h } from '@pyreon/core'
|
|
57
57
|
|
|
58
58
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
59
59
|
|
|
60
|
-
export type HydrationStrategy =
|
|
60
|
+
export type HydrationStrategy = 'load' | 'idle' | 'visible' | 'never' | `media(${string})`
|
|
61
61
|
|
|
62
62
|
export interface IslandOptions {
|
|
63
63
|
/** Unique name — must match the key in the client-side hydrateIslands() registry */
|
|
@@ -86,19 +86,19 @@ export function island<P extends Props = Props>(
|
|
|
86
86
|
loader: () => Promise<{ default: ComponentFn<P> } | ComponentFn<P>>,
|
|
87
87
|
options: IslandOptions,
|
|
88
88
|
): ComponentFn<P> & IslandMeta {
|
|
89
|
-
const { name, hydrate =
|
|
89
|
+
const { name, hydrate = 'load' } = options
|
|
90
90
|
|
|
91
91
|
const IslandWrapper = async function IslandWrapper(props: P): Promise<VNode | null> {
|
|
92
92
|
const mod = await loader()
|
|
93
|
-
const Comp = typeof mod ===
|
|
93
|
+
const Comp = typeof mod === 'function' ? mod : mod.default
|
|
94
94
|
const serializedProps = serializeIslandProps(props)
|
|
95
95
|
|
|
96
96
|
return h(
|
|
97
|
-
|
|
97
|
+
'pyreon-island',
|
|
98
98
|
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
'data-component': name,
|
|
100
|
+
'data-props': serializedProps,
|
|
101
|
+
'data-hydrate': hydrate,
|
|
102
102
|
},
|
|
103
103
|
h(Comp, props),
|
|
104
104
|
)
|
|
@@ -125,9 +125,9 @@ function serializeIslandProps(props: Record<string, unknown>): string {
|
|
|
125
125
|
const clean: Record<string, unknown> = {}
|
|
126
126
|
for (const [key, value] of Object.entries(props)) {
|
|
127
127
|
// Skip non-serializable or internal props
|
|
128
|
-
if (key ===
|
|
129
|
-
if (typeof value ===
|
|
130
|
-
if (typeof value ===
|
|
128
|
+
if (key === 'children') continue
|
|
129
|
+
if (typeof value === 'function') continue
|
|
130
|
+
if (typeof value === 'symbol') continue
|
|
131
131
|
if (value === undefined) continue
|
|
132
132
|
clean[key] = value
|
|
133
133
|
}
|
package/src/ssg.ts
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
* })
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { mkdir, writeFile } from
|
|
32
|
-
import { dirname, join, resolve } from
|
|
31
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
32
|
+
import { dirname, join, resolve } from 'node:path'
|
|
33
33
|
|
|
34
34
|
export interface PrerenderOptions {
|
|
35
35
|
/** SSR handler created by createHandler() */
|
|
@@ -69,15 +69,15 @@ export interface PrerenderResult {
|
|
|
69
69
|
* Paths like "/about" become `outDir/about/index.html`.
|
|
70
70
|
*/
|
|
71
71
|
export async function prerender(options: PrerenderOptions): Promise<PrerenderResult> {
|
|
72
|
-
const { handler, outDir, origin =
|
|
72
|
+
const { handler, outDir, origin = 'http://localhost', onPage } = options
|
|
73
73
|
|
|
74
74
|
const start = Date.now()
|
|
75
75
|
|
|
76
76
|
// Resolve paths (may be async)
|
|
77
|
-
const paths = typeof options.paths ===
|
|
77
|
+
const paths = typeof options.paths === 'function' ? await options.paths() : options.paths
|
|
78
78
|
|
|
79
79
|
let pages = 0
|
|
80
|
-
const errors: PrerenderResult[
|
|
80
|
+
const errors: PrerenderResult['errors'] = []
|
|
81
81
|
|
|
82
82
|
async function renderPage(path: string): Promise<void> {
|
|
83
83
|
const url = new URL(path, origin)
|
|
@@ -110,7 +110,7 @@ export async function prerender(options: PrerenderOptions): Promise<PrerenderRes
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
await mkdir(dirname(filePath), { recursive: true })
|
|
113
|
-
await writeFile(filePath, html,
|
|
113
|
+
await writeFile(filePath, html, 'utf-8')
|
|
114
114
|
pages++
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -137,7 +137,7 @@ export async function prerender(options: PrerenderOptions): Promise<PrerenderRes
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function resolveOutputPath(outDir: string, path: string): string {
|
|
140
|
-
if (path ===
|
|
141
|
-
if (path.endsWith(
|
|
142
|
-
return join(outDir, path,
|
|
140
|
+
if (path === '/') return join(outDir, 'index.html')
|
|
141
|
+
if (path.endsWith('.html')) return join(outDir, path)
|
|
142
|
+
return join(outDir, path, 'index.html')
|
|
143
143
|
}
|