@wix/origami-site 0.1.11 → 0.1.13
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-site",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Runs an exported Wix origami site: file-based routing, Wix dynamic pages, popups and build checks. Use with @wix/origami-site-astro or @wix/origami-site-express.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"@vitejs/plugin-react": "^5.2.0",
|
|
19
19
|
"postcss": "^8.5.3",
|
|
20
20
|
"jsdom": "^23.0.1",
|
|
21
|
-
"vitest": "^4.1.11"
|
|
21
|
+
"vitest": "^4.1.11",
|
|
22
|
+
"react": "^18.3.1",
|
|
23
|
+
"react-dom": "^18.3.1"
|
|
22
24
|
},
|
|
23
25
|
"publishUnscoped": false,
|
|
24
26
|
"publishConfig": {
|
|
@@ -43,19 +45,22 @@
|
|
|
43
45
|
}
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
|
-
"@wix/origami-types": "1.
|
|
47
|
-
"react": "^18.3.1",
|
|
48
|
-
"react-dom": "^18.3.1",
|
|
48
|
+
"@wix/origami-types": "1.4.0",
|
|
49
49
|
"@wix/builder-error-pages": "^1.1.6",
|
|
50
50
|
"@wix/builder-piler-components": "^1.6.7",
|
|
51
51
|
"@wix/origami-transforms": "0.1.8",
|
|
52
|
-
"@babel/parser": "^7.25.0"
|
|
52
|
+
"@babel/parser": "^7.25.0",
|
|
53
|
+
"@wix/system-core-components": "^1.3.10"
|
|
53
54
|
},
|
|
54
55
|
"exports": {
|
|
55
56
|
"./server": {
|
|
56
57
|
"types": "./src/server/index.d.ts",
|
|
57
58
|
"default": "./src/server/index.js"
|
|
58
59
|
},
|
|
60
|
+
"./node": {
|
|
61
|
+
"types": "./src/node/index.d.ts",
|
|
62
|
+
"default": "./src/node/index.js"
|
|
63
|
+
},
|
|
59
64
|
"./browser": {
|
|
60
65
|
"types": "./src/browser/index.d.ts",
|
|
61
66
|
"default": "./src/browser/index.js"
|
|
@@ -77,7 +82,14 @@
|
|
|
77
82
|
}
|
|
78
83
|
},
|
|
79
84
|
"peerDependencies": {
|
|
85
|
+
"react": "^18.3.1",
|
|
86
|
+
"react-dom": "^18.3.1",
|
|
80
87
|
"vite": "^8"
|
|
81
88
|
},
|
|
82
|
-
"
|
|
89
|
+
"peerDependenciesMeta": {
|
|
90
|
+
"vite": {
|
|
91
|
+
"optional": true
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"falconPackageHash": "4ac96e3424d72ea4e249857636f23b2f4e22af082d644f6b99637eb0"
|
|
83
95
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
+
|
|
3
|
+
/** Node request → Web Request, for handle(). Its signal aborts when the client leaves before the response ends. */
|
|
4
|
+
export function toWebRequest(req: IncomingMessage, res: ServerResponse): Request
|
|
5
|
+
/** Web Response → Node response: status, headers with each Set-Cookie kept apart, and the body streamed. HEAD sends no body. */
|
|
6
|
+
export function sendWebResponse(res: ServerResponse, response: Response, options?: { head?: boolean }): Promise<void>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
2
|
+
import { pipeline } from 'node:stream/promises'
|
|
3
|
+
|
|
4
|
+
/** Node request → Web Request, for handle(). Its signal aborts when the client leaves before the response ends.
|
|
5
|
+
* @param {import('node:http').IncomingMessage} req @param {import('node:http').ServerResponse} res */
|
|
6
|
+
export function toWebRequest(req, res) {
|
|
7
|
+
const headers = new Headers()
|
|
8
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2) headers.append(req.rawHeaders[i], req.rawHeaders[i + 1])
|
|
9
|
+
const controller = new AbortController()
|
|
10
|
+
res.once('close', () => { if (!res.writableFinished) controller.abort() })
|
|
11
|
+
/** @type {RequestInit & { duplex?: 'half' }} */
|
|
12
|
+
const init = { method: req.method, headers, signal: controller.signal }
|
|
13
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') Object.assign(init, { body: Readable.toWeb(req), duplex: 'half' })
|
|
14
|
+
const host = req.headers.host ?? 'localhost'
|
|
15
|
+
const origin = new URL(`http://${host}`)
|
|
16
|
+
// Host must name only a host: a path, ?, # or user@ in it would change what is routed.
|
|
17
|
+
if (origin.host !== host.toLowerCase().replace(/:80$/, '')) throw new Error(`Bad Host header: ${host}`)
|
|
18
|
+
// A path is joined, not resolved: new URL('//other.host/x', origin) switches hosts. Absolute and * targets keep only their path.
|
|
19
|
+
const path = req.url?.startsWith('/') ? req.url : (url => url.pathname + url.search)(new URL(req.url ?? '/', origin))
|
|
20
|
+
return new Request(origin.origin + path, init)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Web Response → Node response: status, headers with each Set-Cookie kept apart, and the body streamed.
|
|
24
|
+
* HEAD sends no body.
|
|
25
|
+
* @param {import('node:http').ServerResponse} res @param {Response} response @param {{ head?: boolean }} [options] */
|
|
26
|
+
export async function sendWebResponse(res, response, { head = false } = {}) {
|
|
27
|
+
res.statusCode = response.status
|
|
28
|
+
response.headers.forEach((value, key) => { if (key !== 'set-cookie') res.setHeader(key, value) })
|
|
29
|
+
const cookies = response.headers.getSetCookie()
|
|
30
|
+
if (cookies.length) res.setHeader('set-cookie', cookies)
|
|
31
|
+
if (head || !response.body) {
|
|
32
|
+
await response.body?.cancel()
|
|
33
|
+
res.end()
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
await pipeline(Readable.fromWeb(/** @type {import('node:stream/web').ReadableStream} */ (response.body)), res)
|
|
37
|
+
}
|
package/src/server/index.d.ts
CHANGED
|
@@ -78,3 +78,5 @@ export function runInRequestContext<T>(ctx: RequestContext, fn: () => Promise<T>
|
|
|
78
78
|
export function getRequestContext(): RequestContext
|
|
79
79
|
export function getAccessToken(): Promise<string>
|
|
80
80
|
export function serializePageData(data: PageData): string
|
|
81
|
+
/** The core's plain HTML error page: logs the stack, shows the message in dev only, `cache-control: no-store`. */
|
|
82
|
+
export function errorResponse(error: unknown, options: { status?: 500 | 503; mode: 'dev' | 'prod'; logger?: Logger }): Response
|
package/src/server/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { SiteTreeError } from './requests/errors.js'
|
|
|
3
3
|
export { createSite, noAccessRules } from './requests/createSite.js'
|
|
4
4
|
export { runInRequestContext, getRequestContext, getAccessToken } from './requests/context.js'
|
|
5
5
|
export { serializePageData } from './requests/pageData.js'
|
|
6
|
+
export { errorResponse } from './requests/errorPage.js'
|
|
@@ -2,14 +2,19 @@ const escapeHtml = value => String(value).replace(/[&<>"']/g, char => ({
|
|
|
2
2
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
3
3
|
})[char])
|
|
4
4
|
|
|
5
|
-
/** Plain HTML: builder-error-pages exports React components, not server HTML.
|
|
6
|
-
|
|
5
|
+
/** Plain HTML: builder-error-pages exports React components, not server HTML.
|
|
6
|
+
* @param {unknown} error @param {{ status?: 500 | 503, mode: 'dev' | 'prod', logger?: import('../index.js').Logger }} options */
|
|
7
|
+
export function errorResponse(error, { status = 500, mode, logger = console }) {
|
|
7
8
|
// Logging must not turn a handled failure into a rejected request.
|
|
8
9
|
try { logger.error(error instanceof Error ? error.stack ?? error.message : String(error)) } catch {}
|
|
9
10
|
const title = status === 503 ? 'Service unavailable' : 'Internal server error'
|
|
10
11
|
const detail = mode === 'dev' ? `<pre>${escapeHtml(error instanceof Error ? error.message : error)}</pre>` : ''
|
|
11
|
-
return
|
|
12
|
+
return new Response(
|
|
12
13
|
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title></head><body><h1>${title}</h1>${detail}</body></html>`,
|
|
13
14
|
{ status, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' } },
|
|
14
|
-
)
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function errorPage(error, status, mode, logger) {
|
|
19
|
+
return { kind: /** @type {const} */ ('response'), response: errorResponse(error, { status, mode, logger }) }
|
|
15
20
|
}
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
3
|
import { basename, resolve } from 'node:path'
|
|
4
4
|
import { parse } from '@babel/parser'
|
|
5
|
+
import { findItems, pageUrl } from '@wix/origami-types'
|
|
5
6
|
import { SiteTreeError } from '../requests/errors.js'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
8
|
/** @type {Map<string, { mtimeMs: number, metadata: { title?: string, pageId?: string } }>} */
|
|
10
9
|
const metadataCache = new Map()
|
|
11
10
|
|
|
@@ -63,43 +62,31 @@ export const siteRoot = root => {
|
|
|
63
62
|
|
|
64
63
|
/** @param {string} root @returns {Promise<import('@wix/origami-types').SiteRouteTable>} */
|
|
65
64
|
export const readSiteTree = async root => {
|
|
66
|
-
const
|
|
65
|
+
const src = resolve(siteRoot(root), 'src')
|
|
67
66
|
/** @type {import('@wix/origami-types').PageRoute[]} */
|
|
68
67
|
const pages = []
|
|
69
68
|
/** @type {import('@wix/origami-types').SiteNotFoundRoute} */
|
|
70
69
|
let notFound = { key: '@wix/origami-site/default-error-404', title: 'Page not found' }
|
|
71
|
-
const
|
|
72
|
-
|
|
70
|
+
const pathsByUrl = new Map()
|
|
71
|
+
const { items, routers } = existsSync(resolve(src, 'pages'))
|
|
72
|
+
? findItems(path => readdirSync(resolve(src, path), { withFileTypes: true }), 'pages')
|
|
73
|
+
: { items: [], routers: [] }
|
|
74
|
+
if (routers.length) throw new SiteTreeError('NOT_SUPPORTED_YET', [`src/${routers[0]}`], 'Router pages are not supported yet.')
|
|
75
|
+
for (const itemPath of items) {
|
|
73
76
|
const path = `src/${itemPath}`
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
77
|
+
const metadata = pageMetadata(resolve(src, itemPath))
|
|
78
|
+
if (itemPath === 'pages/error404') notFound = { key: itemPath, itemPath, title: metadata.title }
|
|
79
|
+
else {
|
|
80
|
+
const url = pageUrl(itemPath)
|
|
81
|
+
const previous = pathsByUrl.get(url)
|
|
82
|
+
if (previous) throw new SiteTreeError('DUPLICATE_URL', [previous, path], `Both pages serve "${url}".`)
|
|
83
|
+
pathsByUrl.set(url, path)
|
|
84
|
+
pages.push({ key: itemPath, url, itemPath, ...metadata, access: [] })
|
|
82
85
|
}
|
|
83
|
-
const children = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') &&
|
|
84
|
-
!['_generated', 'node_modules', 'servicesConfigs'].includes(entry.name))
|
|
85
|
-
.filter(child => walk(resolve(folder, child.name), `${itemPath}/${child.name}`))
|
|
86
|
-
if (!isItem && !children.length) return false
|
|
87
|
-
if (/^\[.*\]$/.test(basename(folder)))
|
|
88
|
-
throw new SiteTreeError('NOT_SUPPORTED_YET', [path], 'Router pages are not supported yet.')
|
|
89
|
-
if (entries.some(entry => entry.name === 'route.json'))
|
|
90
|
-
throw new SiteTreeError('NOT_SUPPORTED_YET', [`${path}/route.json`], 'Router pages are not supported yet.')
|
|
91
|
-
if (itemPath === 'pages/error404' && children.length)
|
|
92
|
-
throw new SiteTreeError('NOT_FOUND_HAS_CHILDREN', [path, ...children.map(child => `${path}/${child.name}`)],
|
|
93
|
-
'The not-found page cannot have children.')
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
|
-
if (existsSync(pagesDir)) walk(pagesDir, 'pages')
|
|
97
|
-
const pathsByUrl = new Map()
|
|
98
|
-
for (const page of pages) {
|
|
99
|
-
const path = `src/${page.itemPath}`
|
|
100
|
-
const previous = pathsByUrl.get(page.url)
|
|
101
|
-
if (previous) throw new SiteTreeError('DUPLICATE_URL', [previous, path], `Both pages serve "${page.url}".`)
|
|
102
|
-
pathsByUrl.set(page.url, path)
|
|
103
86
|
}
|
|
87
|
+
const notFoundChildren = pages.filter(page => page.itemPath.startsWith('pages/error404/'))
|
|
88
|
+
if (notFoundChildren.length)
|
|
89
|
+
throw new SiteTreeError('NOT_FOUND_HAS_CHILDREN', ['src/pages/error404', ...notFoundChildren.map(page => `src/${page.itemPath}`)],
|
|
90
|
+
'The not-found page cannot have children.')
|
|
104
91
|
return { version: 2, pages, routers: [], notFound, popups: [] }
|
|
105
92
|
}
|