@pyreon/zero 0.24.5 → 0.24.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/package.json +10 -39
- package/src/actions.ts +0 -196
- package/src/adapters/bun.ts +0 -114
- package/src/adapters/cloudflare.ts +0 -166
- package/src/adapters/index.ts +0 -61
- package/src/adapters/netlify.ts +0 -154
- package/src/adapters/node.ts +0 -163
- package/src/adapters/static.ts +0 -42
- package/src/adapters/validate.ts +0 -23
- package/src/adapters/vercel.ts +0 -182
- package/src/adapters/warn-missing-env.ts +0 -49
- package/src/ai.ts +0 -623
- package/src/api-routes.ts +0 -219
- package/src/app.ts +0 -92
- package/src/cache.ts +0 -136
- package/src/client.ts +0 -143
- package/src/compression.ts +0 -116
- package/src/config.ts +0 -35
- package/src/cors.ts +0 -94
- package/src/csp.ts +0 -226
- package/src/entry-server.ts +0 -224
- package/src/env.ts +0 -344
- package/src/error-overlay.ts +0 -118
- package/src/favicon.ts +0 -841
- package/src/font.ts +0 -511
- package/src/fs-router.ts +0 -1519
- package/src/i18n-routing.ts +0 -533
- package/src/icon.tsx +0 -182
- package/src/icons-plugin.ts +0 -296
- package/src/image-plugin.ts +0 -751
- package/src/image-types.ts +0 -60
- package/src/image.tsx +0 -340
- package/src/index.ts +0 -92
- package/src/isr.ts +0 -394
- package/src/link.tsx +0 -304
- package/src/logger.ts +0 -144
- package/src/manifest.ts +0 -787
- package/src/meta.tsx +0 -354
- package/src/middleware.ts +0 -65
- package/src/not-found.ts +0 -44
- package/src/og-image.ts +0 -378
- package/src/rate-limit.ts +0 -140
- package/src/script.tsx +0 -260
- package/src/seo.ts +0 -617
- package/src/server.ts +0 -89
- package/src/sharp.d.ts +0 -22
- package/src/ssg-plugin.ts +0 -1582
- package/src/testing.ts +0 -146
- package/src/theme.tsx +0 -257
- package/src/types.ts +0 -624
- package/src/utils/use-intersection-observer.ts +0 -36
- package/src/utils/with-headers.ts +0 -13
- package/src/vercel-revalidate-handler.ts +0 -204
- package/src/vite-plugin.ts +0 -848
package/src/adapters/index.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
export { bunAdapter } from './bun'
|
|
2
|
-
export { cloudflareAdapter } from './cloudflare'
|
|
3
|
-
export { netlifyAdapter } from './netlify'
|
|
4
|
-
export { nodeAdapter } from './node'
|
|
5
|
-
export { staticAdapter } from './static'
|
|
6
|
-
export { vercelAdapter } from './vercel'
|
|
7
|
-
|
|
8
|
-
import type { Adapter, ZeroConfig } from '../types'
|
|
9
|
-
import { bunAdapter } from './bun'
|
|
10
|
-
import { cloudflareAdapter } from './cloudflare'
|
|
11
|
-
import { netlifyAdapter } from './netlify'
|
|
12
|
-
import { nodeAdapter } from './node'
|
|
13
|
-
import { staticAdapter } from './static'
|
|
14
|
-
import { vercelAdapter } from './vercel'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Resolve the adapter from config.
|
|
18
|
-
* Returns a built-in adapter or throws if unknown.
|
|
19
|
-
*
|
|
20
|
-
* Accepts BOTH forms — the `ZeroConfig.adapter` type advertises string
|
|
21
|
-
* names (`'vercel'` / `'cloudflare'` / …) but the scaffolded templates
|
|
22
|
-
* historically emit `adapter: vercelAdapter()` (an Adapter instance via
|
|
23
|
-
* the named factory). Both work: a string goes through the switch lookup;
|
|
24
|
-
* an Adapter object (duck-typed via `name` + `build` fields) passes
|
|
25
|
-
* through. Pre-PR-J `resolveAdapter` was never called from production
|
|
26
|
-
* code so the string-vs-object mismatch was invisible; PR J wires the
|
|
27
|
-
* call into `ssgPlugin.closeBundle`, surfacing the contract divergence.
|
|
28
|
-
* The passthrough preserves both shapes without a breaking type change.
|
|
29
|
-
*/
|
|
30
|
-
export function resolveAdapter(config: ZeroConfig): Adapter {
|
|
31
|
-
const value = config.adapter ?? 'node'
|
|
32
|
-
|
|
33
|
-
// Passthrough for already-constructed Adapter instances. Scaffolded
|
|
34
|
-
// templates emit `adapter: vercelAdapter()` — detect by duck-typing
|
|
35
|
-
// the two required Adapter fields (`name: string` + `build: function`).
|
|
36
|
-
if (
|
|
37
|
-
typeof value === 'object'
|
|
38
|
-
&& value !== null
|
|
39
|
-
&& typeof (value as Adapter).name === 'string'
|
|
40
|
-
&& typeof (value as Adapter).build === 'function'
|
|
41
|
-
) {
|
|
42
|
-
return value as Adapter
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
switch (value) {
|
|
46
|
-
case 'node':
|
|
47
|
-
return nodeAdapter()
|
|
48
|
-
case 'bun':
|
|
49
|
-
return bunAdapter()
|
|
50
|
-
case 'static':
|
|
51
|
-
return staticAdapter()
|
|
52
|
-
case 'vercel':
|
|
53
|
-
return vercelAdapter()
|
|
54
|
-
case 'cloudflare':
|
|
55
|
-
return cloudflareAdapter()
|
|
56
|
-
case 'netlify':
|
|
57
|
-
return netlifyAdapter()
|
|
58
|
-
default:
|
|
59
|
-
throw new Error(`[Pyreon] Unknown adapter: "${String(value)}". Use "node", "bun", "static", "vercel", "cloudflare", or "netlify".`)
|
|
60
|
-
}
|
|
61
|
-
}
|
package/src/adapters/netlify.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import type { Adapter, AdapterBuildOptions, AdapterRevalidateResult } from '../types'
|
|
2
|
-
import { validateBuildInputs } from './validate'
|
|
3
|
-
import { warnMissingEnv } from './warn-missing-env'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Netlify adapter — generates output for Netlify Functions (v2).
|
|
7
|
-
*
|
|
8
|
-
* Produces:
|
|
9
|
-
* - Client assets in `publish/` directory
|
|
10
|
-
* - `netlify/functions/ssr.mjs` — Netlify Function for SSR
|
|
11
|
-
* - `netlify.toml` — routing configuration
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* // zero.config.ts
|
|
16
|
-
* import { defineConfig } from "@pyreon/zero"
|
|
17
|
-
*
|
|
18
|
-
* export default defineConfig({
|
|
19
|
-
* adapter: "netlify",
|
|
20
|
-
* })
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export function netlifyAdapter(): Adapter {
|
|
24
|
-
return {
|
|
25
|
-
name: 'netlify',
|
|
26
|
-
async build(options: AdapterBuildOptions) {
|
|
27
|
-
if (options.kind === 'ssg') {
|
|
28
|
-
// PR J — SSG branch. Emit `netlify.toml` with `publish = "."`
|
|
29
|
-
// (the dist root) and asset-cache headers — no functions. Tells
|
|
30
|
-
// Netlify "this dist directory IS the publishable output, serve
|
|
31
|
-
// it as a static site". Without this file, Netlify falls back
|
|
32
|
-
// to whatever the user has at the repo root (might miss the
|
|
33
|
-
// dist/ direct-upload shape).
|
|
34
|
-
//
|
|
35
|
-
// PR B's `dist/_redirects` (loader-redirect manifest) is
|
|
36
|
-
// emitted by ssgPlugin BEFORE the adapter runs, so this branch
|
|
37
|
-
// doesn't need to write it. The two files coexist cleanly —
|
|
38
|
-
// Netlify reads both.
|
|
39
|
-
const { writeFile } = await import('node:fs/promises')
|
|
40
|
-
const { join } = await import('node:path')
|
|
41
|
-
const toml = `[build]
|
|
42
|
-
publish = "."
|
|
43
|
-
|
|
44
|
-
[[headers]]
|
|
45
|
-
for = "/assets/*"
|
|
46
|
-
[headers.values]
|
|
47
|
-
Cache-Control = "public, max-age=31536000, immutable"
|
|
48
|
-
`
|
|
49
|
-
await writeFile(join(options.outDir, 'netlify.toml'), toml)
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
await validateBuildInputs(options)
|
|
53
|
-
const { writeFile, cp, mkdir } = await import('node:fs/promises')
|
|
54
|
-
const { join } = await import('node:path')
|
|
55
|
-
|
|
56
|
-
const outDir = options.outDir
|
|
57
|
-
const publishDir = join(outDir, 'publish')
|
|
58
|
-
const functionsDir = join(outDir, 'netlify', 'functions')
|
|
59
|
-
|
|
60
|
-
await mkdir(publishDir, { recursive: true })
|
|
61
|
-
await mkdir(functionsDir, { recursive: true })
|
|
62
|
-
|
|
63
|
-
// Copy client assets to publish/
|
|
64
|
-
await cp(options.clientOutDir, publishDir, { recursive: true })
|
|
65
|
-
|
|
66
|
-
// Copy server build to functions directory
|
|
67
|
-
await cp(join(options.serverEntry, '..'), join(functionsDir, '_server'), {
|
|
68
|
-
recursive: true,
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// Generate Netlify Function (v2 format — ESM, Web-standard Request/Response).
|
|
72
|
-
const funcEntry = `
|
|
73
|
-
import handler from "./_server/entry-server.js"
|
|
74
|
-
|
|
75
|
-
export default async function(req, context) {
|
|
76
|
-
try {
|
|
77
|
-
return await handler(req)
|
|
78
|
-
} catch (err) {
|
|
79
|
-
// Surface the error to Netlify Function logs so production
|
|
80
|
-
// crashes give real diagnostic info — pre-fix the catch
|
|
81
|
-
// swallowed \`err\` entirely and the operator saw only a
|
|
82
|
-
// bare "Internal Server Error". \`console.error\` lands in
|
|
83
|
-
// Netlify's function runtime logs panel + \`netlify functions:log\`.
|
|
84
|
-
console.error("[Pyreon SSR] handler failed:", err)
|
|
85
|
-
return new Response("Internal Server Error", { status: 500 })
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export const config = {
|
|
90
|
-
path: "/*",
|
|
91
|
-
preferStatic: true,
|
|
92
|
-
}
|
|
93
|
-
`.trimStart()
|
|
94
|
-
|
|
95
|
-
await writeFile(join(functionsDir, 'ssr.mjs'), funcEntry)
|
|
96
|
-
|
|
97
|
-
// Generate netlify.toml
|
|
98
|
-
const toml = `
|
|
99
|
-
[build]
|
|
100
|
-
publish = "publish"
|
|
101
|
-
functions = "netlify/functions"
|
|
102
|
-
|
|
103
|
-
[[headers]]
|
|
104
|
-
for = "/assets/*"
|
|
105
|
-
[headers.values]
|
|
106
|
-
Cache-Control = "public, max-age=31536000, immutable"
|
|
107
|
-
|
|
108
|
-
[[redirects]]
|
|
109
|
-
from = "/*"
|
|
110
|
-
to = "/.netlify/functions/ssr"
|
|
111
|
-
status = 200
|
|
112
|
-
conditions = {Role = ["admin", "user", ""]}
|
|
113
|
-
`.trimStart()
|
|
114
|
-
|
|
115
|
-
await writeFile(join(outDir, 'netlify.toml'), toml)
|
|
116
|
-
},
|
|
117
|
-
async revalidate(path: string): Promise<AdapterRevalidateResult> {
|
|
118
|
-
// Netlify ISR via Build Hook trigger. Reads
|
|
119
|
-
// `NETLIFY_BUILD_HOOK_URL` from env (created in Site settings →
|
|
120
|
-
// Build hooks). Posting to the hook triggers a partial rebuild
|
|
121
|
-
// — Netlify rebuilds only the pages whose source has changed
|
|
122
|
-
// since last deploy. The path arg is included as a `trigger_title`
|
|
123
|
-
// for audit traceability in the Netlify deploy log; Netlify
|
|
124
|
-
// doesn't accept per-path revalidation natively (the hook
|
|
125
|
-
// re-runs the full build).
|
|
126
|
-
//
|
|
127
|
-
// Reference: https://docs.netlify.com/configure-builds/build-hooks/
|
|
128
|
-
const hookUrl = process.env.NETLIFY_BUILD_HOOK_URL
|
|
129
|
-
if (!hookUrl) {
|
|
130
|
-
// M2.4 — warn even in production (dedupe per process). See vercel.ts
|
|
131
|
-
// for the rationale.
|
|
132
|
-
return warnMissingEnv(
|
|
133
|
-
'netlify',
|
|
134
|
-
['NETLIFY_BUILD_HOOK_URL'],
|
|
135
|
-
'Create a build hook in Site settings → Build & deploy → Build hooks → Add build hook. Note: Netlify Build Hooks trigger a FULL site rebuild — the path arg is recorded as `trigger_title` for audit traceability but Netlify doesn\'t support per-page ISR natively.',
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
const triggerTitle = `revalidate:${path}`
|
|
140
|
-
const res = await fetch(`${hookUrl}?trigger_title=${encodeURIComponent(triggerTitle)}`, {
|
|
141
|
-
method: 'POST',
|
|
142
|
-
})
|
|
143
|
-
return { regenerated: res.ok }
|
|
144
|
-
} catch (err) {
|
|
145
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
146
|
-
console.warn(
|
|
147
|
-
`[Pyreon] netlifyAdapter.revalidate(${path}) failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
return { regenerated: false }
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
}
|
package/src/adapters/node.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import type { Adapter, AdapterBuildOptions, AdapterRevalidateResult } from '../types'
|
|
2
|
-
import { validateBuildInputs } from './validate'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Node.js adapter — generates a standalone server entry using node:http.
|
|
6
|
-
*
|
|
7
|
-
* **SSG mode (PR J)**: no-op. Node adapter exists for serving the SSR
|
|
8
|
-
* runtime; SSG output is already complete static HTML — serve it with
|
|
9
|
-
* any static-file server (`bun preview` / nginx / Caddy / `npx serve`).
|
|
10
|
-
* Use `staticAdapter()` if you want explicit SSG semantics.
|
|
11
|
-
*/
|
|
12
|
-
export function nodeAdapter(): Adapter {
|
|
13
|
-
return {
|
|
14
|
-
name: 'node',
|
|
15
|
-
async build(options: AdapterBuildOptions) {
|
|
16
|
-
if (options.kind === 'ssg') {
|
|
17
|
-
// Node runner has nothing to add for prerendered SSG dist.
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
await validateBuildInputs(options)
|
|
21
|
-
const { writeFile, cp, mkdir } = await import('node:fs/promises')
|
|
22
|
-
const { join } = await import('node:path')
|
|
23
|
-
|
|
24
|
-
const outDir = options.outDir
|
|
25
|
-
await mkdir(outDir, { recursive: true })
|
|
26
|
-
|
|
27
|
-
// Copy server and client builds
|
|
28
|
-
await cp(options.clientOutDir, join(outDir, 'client'), {
|
|
29
|
-
recursive: true,
|
|
30
|
-
})
|
|
31
|
-
await cp(join(options.serverEntry, '..'), join(outDir, 'server'), {
|
|
32
|
-
recursive: true,
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
// Generate standalone server entry
|
|
36
|
-
const port = options.config.port ?? 3000
|
|
37
|
-
const serverEntry = `
|
|
38
|
-
import { createServer } from "node:http"
|
|
39
|
-
import { readFile } from "node:fs/promises"
|
|
40
|
-
import { join, extname } from "node:path"
|
|
41
|
-
import { fileURLToPath } from "node:url"
|
|
42
|
-
|
|
43
|
-
const __dirname = fileURLToPath(new URL(".", import.meta.url))
|
|
44
|
-
const handler = (await import("./server/entry-server.js")).default
|
|
45
|
-
const clientDir = join(__dirname, "client")
|
|
46
|
-
|
|
47
|
-
const MIME_TYPES = {
|
|
48
|
-
".html": "text/html",
|
|
49
|
-
".js": "application/javascript",
|
|
50
|
-
".css": "text/css",
|
|
51
|
-
".json": "application/json",
|
|
52
|
-
".png": "image/png",
|
|
53
|
-
".jpg": "image/jpeg",
|
|
54
|
-
".svg": "image/svg+xml",
|
|
55
|
-
".woff2": "font/woff2",
|
|
56
|
-
".woff": "font/woff",
|
|
57
|
-
".ico": "image/x-icon",
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const server = createServer(async (req, res) => {
|
|
61
|
-
const url = new URL(req.url ?? "/", "http://localhost")
|
|
62
|
-
|
|
63
|
-
// Try to serve static files first (GET only).
|
|
64
|
-
if (req.method === "GET") {
|
|
65
|
-
try {
|
|
66
|
-
const filePath = join(clientDir, url.pathname === "/" ? "index.html" : url.pathname)
|
|
67
|
-
// Prevent path traversal — ensure resolved path stays within clientDir.
|
|
68
|
-
const { resolve } = await import("node:path")
|
|
69
|
-
const resolved = resolve(filePath)
|
|
70
|
-
if (!resolved.startsWith(resolve(clientDir))) {
|
|
71
|
-
res.writeHead(403)
|
|
72
|
-
res.end("Forbidden")
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
const ext = extname(filePath)
|
|
76
|
-
// Pre-fix shape was \`if (ext && ext !== ".html")\` which made the
|
|
77
|
-
// static branch silently refuse to serve .html files — INCLUDING
|
|
78
|
-
// the root \`/\` → \`index.html\` mapping the line above explicitly
|
|
79
|
-
// sets up. Result: GET / always fell through to SSR, even when an
|
|
80
|
-
// \`index.html\` shell existed in clientDir. Matches the bun
|
|
81
|
-
// adapter's behavior (which serves index.html at /) and the
|
|
82
|
-
// standard static + dynamic deployment pattern.
|
|
83
|
-
if (ext) {
|
|
84
|
-
const data = await readFile(filePath)
|
|
85
|
-
const mime = MIME_TYPES[ext] || "application/octet-stream"
|
|
86
|
-
res.writeHead(200, {
|
|
87
|
-
"content-type": mime,
|
|
88
|
-
"cache-control": ext === ".js" || ext === ".css"
|
|
89
|
-
? "public, max-age=31536000, immutable"
|
|
90
|
-
: "public, max-age=3600",
|
|
91
|
-
})
|
|
92
|
-
res.end(data)
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
} catch {}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Fall through to SSR handler.
|
|
99
|
-
const headers = {}
|
|
100
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
101
|
-
if (value) headers[key] = Array.isArray(value) ? value.join(", ") : value
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const request = new Request(url.href, {
|
|
105
|
-
method: req.method,
|
|
106
|
-
headers,
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
const response = await handler(request)
|
|
110
|
-
|
|
111
|
-
const responseHeaders = {}
|
|
112
|
-
response.headers.forEach((v, k) => { responseHeaders[k] = v })
|
|
113
|
-
|
|
114
|
-
res.writeHead(response.status, responseHeaders)
|
|
115
|
-
|
|
116
|
-
// Pipe the Response body stream directly to res instead of buffering
|
|
117
|
-
// the whole body via response.text(). For mode: 'stream' SSR (Suspense
|
|
118
|
-
// out-of-order streaming) the pre-fix \`await response.text()\` drained
|
|
119
|
-
// every Suspense chunk server-side and arrived at the client all at
|
|
120
|
-
// once at the end — silently defeating streaming. For mode: 'string'
|
|
121
|
-
// the body is a single chunk and this loop runs once with identical
|
|
122
|
-
// observable behaviour.
|
|
123
|
-
if (response.body) {
|
|
124
|
-
const reader = response.body.getReader()
|
|
125
|
-
try {
|
|
126
|
-
while (true) {
|
|
127
|
-
const { value, done } = await reader.read()
|
|
128
|
-
if (done) break
|
|
129
|
-
res.write(value)
|
|
130
|
-
}
|
|
131
|
-
} finally {
|
|
132
|
-
res.end()
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
res.end()
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
server.listen(${port}, () => {
|
|
140
|
-
console.log("\\n ⚡ Zero production server running on http://localhost:${port}\\n")
|
|
141
|
-
})
|
|
142
|
-
`.trimStart()
|
|
143
|
-
|
|
144
|
-
await writeFile(join(outDir, 'index.js'), serverEntry)
|
|
145
|
-
await writeFile(join(outDir, 'package.json'), JSON.stringify({ type: 'module' }, null, 2))
|
|
146
|
-
},
|
|
147
|
-
async revalidate(_path: string): Promise<AdapterRevalidateResult> {
|
|
148
|
-
// Self-hosted Node has no platform-driven ISR. Real ISR support
|
|
149
|
-
// requires a reverse-proxy cache (nginx/varnish) + your own
|
|
150
|
-
// cache-purge wiring, OR mode: 'isr' for runtime LRU caching.
|
|
151
|
-
// This no-op preserves the Adapter API contract; user code that
|
|
152
|
-
// calls `adapter.revalidate(path)` against a self-hosted Node
|
|
153
|
-
// deploy gets the same `regenerated: false` shape as the static
|
|
154
|
-
// adapter, so migrating between adapters doesn't surprise.
|
|
155
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
156
|
-
console.warn(
|
|
157
|
-
'[Pyreon] nodeAdapter.revalidate() is a no-op — self-hosted Node has no platform-driven ISR. Use mode: "isr" for runtime LRU caching, or vercelAdapter / cloudflareAdapter / netlifyAdapter for platform-driven build-time ISR.',
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
return { regenerated: false }
|
|
161
|
-
},
|
|
162
|
-
}
|
|
163
|
-
}
|
package/src/adapters/static.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { Adapter, AdapterBuildOptions, AdapterRevalidateResult } from '../types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Static adapter — just copies the client build output.
|
|
5
|
-
* Used with SSG mode where all pages are pre-rendered at build time.
|
|
6
|
-
*
|
|
7
|
-
* **SSG mode (PR J)**: no-op — `outDir` already IS the dist directory
|
|
8
|
-
* the SSG plugin produced. Copying it onto itself would only fail. The
|
|
9
|
-
* static adapter is the canonical zero-overhead deploy target for
|
|
10
|
-
* pure-static sites.
|
|
11
|
-
*
|
|
12
|
-
* **SSR mode**: copies clientOutDir → outDir. Calling `static` with SSR
|
|
13
|
-
* mode is unusual — the static adapter doesn't support server-side
|
|
14
|
-
* execution — but preserved as a "client-only output packager".
|
|
15
|
-
*/
|
|
16
|
-
export function staticAdapter(): Adapter {
|
|
17
|
-
return {
|
|
18
|
-
name: 'static',
|
|
19
|
-
async build(options: AdapterBuildOptions) {
|
|
20
|
-
if (options.kind === 'ssg') {
|
|
21
|
-
// SSG dist is already at outDir — nothing to copy or rewrite.
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
const { cp, mkdir } = await import('node:fs/promises')
|
|
25
|
-
|
|
26
|
-
await mkdir(options.outDir, { recursive: true })
|
|
27
|
-
await cp(options.clientOutDir, options.outDir, { recursive: true })
|
|
28
|
-
},
|
|
29
|
-
async revalidate(_path: string): Promise<AdapterRevalidateResult> {
|
|
30
|
-
// Static hosts have no platform-driven ISR. Revalidation requires
|
|
31
|
-
// a full rebuild + redeploy. Returns `regenerated: false` so user
|
|
32
|
-
// code can branch on the no-op shape and degrade gracefully when
|
|
33
|
-
// migrating between adapters.
|
|
34
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
35
|
-
console.warn(
|
|
36
|
-
'[Pyreon] staticAdapter.revalidate() is a no-op — static hosts require a full rebuild + redeploy to refresh prerendered pages. Use vercelAdapter / cloudflareAdapter / netlifyAdapter for platform-driven ISR.',
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
return { regenerated: false }
|
|
40
|
-
},
|
|
41
|
-
}
|
|
42
|
-
}
|
package/src/adapters/validate.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { AdapterBuildOptions } from '../types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Validate that SSR-mode adapter build inputs exist before copying.
|
|
5
|
-
* Throws with a clear error message if directories are missing.
|
|
6
|
-
*
|
|
7
|
-
* SSG-mode passes through unchanged — the SSG branch doesn't need a
|
|
8
|
-
* server entry (every page is prerendered) and `outDir` IS the dist
|
|
9
|
-
* directory the SSG plugin already populated. Validating it here would
|
|
10
|
-
* be redundant.
|
|
11
|
-
*
|
|
12
|
-
* @internal
|
|
13
|
-
*/
|
|
14
|
-
export async function validateBuildInputs(options: AdapterBuildOptions): Promise<void> {
|
|
15
|
-
if (options.kind !== 'ssr') return
|
|
16
|
-
const { existsSync } = await import('node:fs')
|
|
17
|
-
if (!existsSync(options.clientOutDir)) {
|
|
18
|
-
throw new Error(`[Pyreon] Client build output not found: ${options.clientOutDir}. Run "vite build" first.`)
|
|
19
|
-
}
|
|
20
|
-
if (!existsSync(options.serverEntry)) {
|
|
21
|
-
throw new Error(`[Pyreon] Server entry not found: ${options.serverEntry}. Run "vite build --ssr" first.`)
|
|
22
|
-
}
|
|
23
|
-
}
|
package/src/adapters/vercel.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import type { Adapter, AdapterBuildOptions, AdapterRevalidateResult } from '../types'
|
|
2
|
-
import { validateBuildInputs } from './validate'
|
|
3
|
-
import { warnMissingEnv } from './warn-missing-env'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Vercel adapter — generates output for Vercel's Build Output API v3.
|
|
7
|
-
*
|
|
8
|
-
* Produces a `.vercel/output` directory with:
|
|
9
|
-
* - `static/` — client-side assets (JS, CSS, images)
|
|
10
|
-
* - `functions/ssr.func/` — serverless function for SSR
|
|
11
|
-
* - `config.json` — routing configuration
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* // zero.config.ts
|
|
16
|
-
* import { defineConfig } from "@pyreon/zero"
|
|
17
|
-
*
|
|
18
|
-
* export default defineConfig({
|
|
19
|
-
* adapter: "vercel",
|
|
20
|
-
* })
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export function vercelAdapter(): Adapter {
|
|
24
|
-
return {
|
|
25
|
-
name: 'vercel',
|
|
26
|
-
async build(options: AdapterBuildOptions) {
|
|
27
|
-
if (options.kind === 'ssg') {
|
|
28
|
-
// PR J — SSG branch. Emit Vercel Build Output API v3 STATIC
|
|
29
|
-
// variant: `.vercel/output/config.json` listing routes config
|
|
30
|
-
// for the prerendered dist; no functions (every page is
|
|
31
|
-
// already static). Vercel's deployer reads this config + the
|
|
32
|
-
// built dist content as the static asset root — no runtime SSR.
|
|
33
|
-
//
|
|
34
|
-
// We do NOT copy files into `.vercel/output/static/` — the
|
|
35
|
-
// standard Vercel CLI deploy flow detects the dist root
|
|
36
|
-
// automatically. Adapters that move files break user-side
|
|
37
|
-
// post-build steps (sourcemap upload, perf scripts, custom
|
|
38
|
-
// asset handling). Writing config.json alone is the
|
|
39
|
-
// minimum-impact signal "this is a prerendered site".
|
|
40
|
-
const { writeFile, mkdir } = await import('node:fs/promises')
|
|
41
|
-
const { join } = await import('node:path')
|
|
42
|
-
const vercelDir = join(options.outDir, '.vercel', 'output')
|
|
43
|
-
await mkdir(vercelDir, { recursive: true })
|
|
44
|
-
const config = {
|
|
45
|
-
version: 3,
|
|
46
|
-
routes: [
|
|
47
|
-
// Long-cache hashed assets; mirrors the SSR config above.
|
|
48
|
-
{
|
|
49
|
-
src: '/assets/(.*)',
|
|
50
|
-
headers: { 'Cache-Control': 'public, max-age=31536000, immutable' },
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
}
|
|
54
|
-
await writeFile(join(vercelDir, 'config.json'), JSON.stringify(config, null, 2))
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
await validateBuildInputs(options)
|
|
58
|
-
const { writeFile, cp, mkdir } = await import('node:fs/promises')
|
|
59
|
-
const { join } = await import('node:path')
|
|
60
|
-
|
|
61
|
-
const vercelDir = join(options.outDir, '.vercel', 'output')
|
|
62
|
-
const staticDir = join(vercelDir, 'static')
|
|
63
|
-
const funcDir = join(vercelDir, 'functions', 'ssr.func')
|
|
64
|
-
|
|
65
|
-
await mkdir(staticDir, { recursive: true })
|
|
66
|
-
await mkdir(funcDir, { recursive: true })
|
|
67
|
-
|
|
68
|
-
// Copy client assets to static/
|
|
69
|
-
await cp(options.clientOutDir, staticDir, { recursive: true })
|
|
70
|
-
|
|
71
|
-
// Copy server build to function directory
|
|
72
|
-
await cp(join(options.serverEntry, '..'), funcDir, { recursive: true })
|
|
73
|
-
|
|
74
|
-
// Generate serverless function entry.
|
|
75
|
-
//
|
|
76
|
-
// Pre-fix the handler dynamically imported \`./entry-server.js\` on
|
|
77
|
-
// EVERY invocation. Node's module cache makes calls after the
|
|
78
|
-
// first one near-free, but the FIRST request on every fresh
|
|
79
|
-
// serverless instance (i.e. every cold start) paid the full
|
|
80
|
-
// module evaluation cost inside the request budget — observable
|
|
81
|
-
// as a TTFB spike on cold starts. Hoisting the import to module
|
|
82
|
-
// scope evaluates the SSR module once at function-init time,
|
|
83
|
-
// before the first request lands.
|
|
84
|
-
//
|
|
85
|
-
// Also surface SSR errors to Vercel function logs via
|
|
86
|
-
// \`console.error\` (mirrors the cloudflare + netlify fix). Pre-fix
|
|
87
|
-
// an unhandled SSR throw propagated to Vercel's launcher (which
|
|
88
|
-
// logs it generically); adding our own prefix makes the cause
|
|
89
|
-
// trivially greppable in the dashboard log stream.
|
|
90
|
-
const funcEntry = `
|
|
91
|
-
import handler from "./entry-server.js"
|
|
92
|
-
|
|
93
|
-
export default async function vercelHandler(req) {
|
|
94
|
-
try {
|
|
95
|
-
return await handler(req)
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.error("[Pyreon SSR] handler failed:", err)
|
|
98
|
-
return new Response("Internal Server Error", { status: 500 })
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
`.trimStart()
|
|
102
|
-
|
|
103
|
-
await writeFile(join(funcDir, 'index.js'), funcEntry)
|
|
104
|
-
|
|
105
|
-
// Function config
|
|
106
|
-
await writeFile(
|
|
107
|
-
join(funcDir, '.vc-config.json'),
|
|
108
|
-
JSON.stringify(
|
|
109
|
-
{
|
|
110
|
-
runtime: 'nodejs20.x',
|
|
111
|
-
handler: 'index.js',
|
|
112
|
-
launcherType: 'Nodejs',
|
|
113
|
-
},
|
|
114
|
-
null,
|
|
115
|
-
2,
|
|
116
|
-
),
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
// Vercel Build Output config
|
|
120
|
-
const config = {
|
|
121
|
-
version: 3,
|
|
122
|
-
routes: [
|
|
123
|
-
// Serve static assets directly
|
|
124
|
-
{
|
|
125
|
-
src: '/assets/(.*)',
|
|
126
|
-
headers: { 'Cache-Control': 'public, max-age=31536000, immutable' },
|
|
127
|
-
},
|
|
128
|
-
// Favicon and manifest
|
|
129
|
-
{ src: '/(favicon\\..*|site\\.webmanifest|robots\\.txt|sitemap\\.xml)', dest: '/$1' },
|
|
130
|
-
// All other routes → SSR function
|
|
131
|
-
{ src: '/(.*)', dest: '/ssr' },
|
|
132
|
-
],
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
await writeFile(join(vercelDir, 'config.json'), JSON.stringify(config, null, 2))
|
|
136
|
-
},
|
|
137
|
-
async revalidate(path: string): Promise<AdapterRevalidateResult> {
|
|
138
|
-
// Vercel ISR API — POST to a deployment-relative
|
|
139
|
-
// revalidation endpoint with a secret token. Reads
|
|
140
|
-
// `VERCEL_DEPLOYMENT_URL` (auto-injected by Vercel runtime) and
|
|
141
|
-
// `VERCEL_REVALIDATE_TOKEN` (user-set in dashboard) from env.
|
|
142
|
-
// Mirrors Next.js's `res.revalidate()` shape — a HEAD request
|
|
143
|
-
// with the path + token, Vercel rebuilds the page in the
|
|
144
|
-
// background and serves stale-while-revalidate to subsequent
|
|
145
|
-
// visitors until the rebuild lands.
|
|
146
|
-
//
|
|
147
|
-
// No `regenerated: true` until Vercel acks 200 — partial-purge
|
|
148
|
-
// behaviour (the platform queues the regenerate but doesn't
|
|
149
|
-
// confirm it landed) is documented as a "false-positive
|
|
150
|
-
// possible" caveat in the Adapter.revalidate JSDoc.
|
|
151
|
-
const deploymentUrl = process.env.VERCEL_DEPLOYMENT_URL ?? process.env.VERCEL_URL
|
|
152
|
-
const token = process.env.VERCEL_REVALIDATE_TOKEN
|
|
153
|
-
if (!deploymentUrl || !token) {
|
|
154
|
-
// M2.4 — warn even in production (dedupe per process). Pre-fix the
|
|
155
|
-
// warn was DEV-gated, but production is exactly where missing env
|
|
156
|
-
// vars surface — CMS triggers revalidate, nothing happens, no
|
|
157
|
-
// signal. Now the FIRST call always warns; subsequent calls dedupe.
|
|
158
|
-
const missing: string[] = []
|
|
159
|
-
if (!deploymentUrl) missing.push('VERCEL_DEPLOYMENT_URL (or VERCEL_URL)')
|
|
160
|
-
if (!token) missing.push('VERCEL_REVALIDATE_TOKEN')
|
|
161
|
-
return warnMissingEnv(
|
|
162
|
-
'vercel',
|
|
163
|
-
missing,
|
|
164
|
-
'Set the token in Vercel project settings → Environment Variables. VERCEL_DEPLOYMENT_URL / VERCEL_URL is auto-injected by the Vercel runtime.',
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
const protocol = deploymentUrl.startsWith('http') ? '' : 'https://'
|
|
168
|
-
const url = `${protocol}${deploymentUrl}/api/_pyreon-revalidate?path=${encodeURIComponent(path)}&secret=${encodeURIComponent(token)}`
|
|
169
|
-
try {
|
|
170
|
-
const res = await fetch(url, { method: 'POST' })
|
|
171
|
-
return { regenerated: res.ok }
|
|
172
|
-
} catch (err) {
|
|
173
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
174
|
-
console.warn(
|
|
175
|
-
`[Pyreon] vercelAdapter.revalidate(${path}) failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
|
-
return { regenerated: false }
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
}
|
|
182
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { AdapterRevalidateResult } from '../types'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* M2.4 — Loud first-call warning for missing adapter env vars.
|
|
5
|
-
*
|
|
6
|
-
* Pre-M2.4 the adapter `revalidate()` methods returned `{ regenerated: false }`
|
|
7
|
-
* silently in production when required env vars were missing. The dev-mode
|
|
8
|
-
* warning was gated on `process.env.NODE_ENV !== 'production'` — exactly the
|
|
9
|
-
* env condition that DEPLOYED apps run under, where users would most need
|
|
10
|
-
* the signal. Symptom: CMS triggers `adapter.revalidate(path)`, nothing
|
|
11
|
-
* happens, no console output, no failure mode reported back to the
|
|
12
|
-
* triggering webhook handler. The bug only surfaces when someone notices
|
|
13
|
-
* stale content.
|
|
14
|
-
*
|
|
15
|
-
* Fix: warn ALWAYS (regardless of NODE_ENV) on the FIRST invocation per
|
|
16
|
-
* `(adapterName + varSet)` combination. Dedupe via a module-level Set so
|
|
17
|
-
* a busy revalidation handler doesn't spam logs — one warn per process
|
|
18
|
-
* per missing-env-set is enough to expose the misconfiguration.
|
|
19
|
-
*
|
|
20
|
-
* Returns the canonical `{ regenerated: false }` so adapters can write
|
|
21
|
-
* `return warnMissingEnv(...)` as a one-liner.
|
|
22
|
-
*
|
|
23
|
-
* @internal Exposed for unit tests via `_internal.warnMissingEnv` (not yet wired) + `_warnedKeys` reset.
|
|
24
|
-
*/
|
|
25
|
-
const _warnedKeys = new Set<string>()
|
|
26
|
-
|
|
27
|
-
export function warnMissingEnv(
|
|
28
|
-
adapterName: string,
|
|
29
|
-
missingVars: readonly string[],
|
|
30
|
-
hint: string,
|
|
31
|
-
): AdapterRevalidateResult {
|
|
32
|
-
const key = `${adapterName}:${missingVars.join(',')}`
|
|
33
|
-
if (!_warnedKeys.has(key)) {
|
|
34
|
-
_warnedKeys.add(key)
|
|
35
|
-
// oxlint-disable-next-line no-console
|
|
36
|
-
console.warn(
|
|
37
|
-
`[Pyreon] ${adapterName}Adapter.revalidate() needs ${missingVars.join(' + ')} env var(s). ${hint}`,
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
return { regenerated: false }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Reset the dedup Set. Test-only — production code never reaches this.
|
|
45
|
-
* @internal
|
|
46
|
-
*/
|
|
47
|
-
export function _resetWarnedKeys(): void {
|
|
48
|
-
_warnedKeys.clear()
|
|
49
|
-
}
|