@wix/origami-site 0.1.0 → 0.1.1
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.1",
|
|
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": [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc --noEmit",
|
|
11
11
|
"lint": "oxlint .",
|
|
12
|
-
"test": "tsc --noEmit -p tsconfig.test.json"
|
|
12
|
+
"test": "node --test test/*.spec.js && tsc --noEmit -p tsconfig.test.json"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/node": "^22.0.0",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"react": "^18.3.1",
|
|
44
44
|
"react-dom": "^18.3.1",
|
|
45
45
|
"@wix/builder-error-pages": "^1.1.6",
|
|
46
|
-
"@wix/builder-piler-components": "^1.6.7"
|
|
46
|
+
"@wix/builder-piler-components": "^1.6.7",
|
|
47
|
+
"@babel/parser": "^7.25.0"
|
|
47
48
|
},
|
|
48
49
|
"exports": {
|
|
49
50
|
"./server": {
|
|
@@ -67,5 +68,5 @@
|
|
|
67
68
|
},
|
|
68
69
|
"./default-error-404": "./src/default-error-404.jsx"
|
|
69
70
|
},
|
|
70
|
-
"falconPackageHash": "
|
|
71
|
+
"falconPackageHash": "236041398ac8f5cb5996fd571ab7dbe9f17287f25c51e01599157dc7"
|
|
71
72
|
}
|
package/src/server/index.d.ts
CHANGED
|
@@ -70,7 +70,8 @@ export interface AccessHandler {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
export
|
|
73
|
+
export { SiteTreeError } from './requests/errors.js'
|
|
74
|
+
export type { RouterServiceError } from './requests/errors.js'
|
|
74
75
|
export function createSite(options: SiteOptions): Promise<OrigamiSite>
|
|
75
76
|
export function readSiteTree(root: string): Promise<SiteRouteTable>
|
|
76
77
|
export function runInRequestContext<T>(ctx: RequestContext, fn: () => Promise<T>): Promise<T>
|
package/src/server/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {}
|
|
1
|
+
export { readSiteTree } from './site-tree/readSiteTree.js'
|
|
2
|
+
export { SiteTreeError } from './requests/errors.js'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export class SiteTreeError extends Error {
|
|
2
2
|
code:
|
|
3
|
+
| 'NOT_SUPPORTED_YET' // router pages are not supported in stage 1
|
|
3
4
|
| 'DUPLICATE_URL' // two folders give the same URL
|
|
4
5
|
| 'ROUTE_JSON_INVALID' // not JSON, or a required field missing or of the wrong type
|
|
5
6
|
| 'ROUTE_JSON_MISSING_SERVICE_URL' // no routerServiceUrl: origami refuses to start
|
|
@@ -13,6 +14,7 @@ export class SiteTreeError extends Error {
|
|
|
13
14
|
| 'NOT_FOUND_HAS_CHILDREN'
|
|
14
15
|
| 'DUPLICATE_POPUP'
|
|
15
16
|
| 'UNSUPPORTED_ACCESS_RULE' // see section 4
|
|
17
|
+
constructor(code: SiteTreeError['code'], paths: string[], message: string)
|
|
16
18
|
paths: string[]
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class SiteTreeError extends Error {
|
|
2
|
+
/**
|
|
3
|
+
* @param {import('./errors.js').SiteTreeError['code']} code
|
|
4
|
+
* @param {string[]} paths Paths relative to the site package root.
|
|
5
|
+
* @param {string} message
|
|
6
|
+
*/
|
|
7
|
+
constructor(code, paths, message) {
|
|
8
|
+
super(`SiteTreeError ${code}\n${paths.map(path => ` ${path}`).join('\n')}\n ${message}`)
|
|
9
|
+
this.name = 'SiteTreeError'
|
|
10
|
+
this.code = code
|
|
11
|
+
this.paths = [...paths]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { relative, resolve, sep } from 'node:path'
|
|
2
|
+
import { readSiteTree, siteRoot } from './readSiteTree.js'
|
|
3
|
+
|
|
4
|
+
/** Internal live table; adapters supply file events, never a second watcher. */
|
|
5
|
+
export const createSiteTree = root => {
|
|
6
|
+
const rootPath = siteRoot(root)
|
|
7
|
+
let dirty = true
|
|
8
|
+
let closed = false
|
|
9
|
+
/** @type {Promise<import('@wix/origami-types').SiteRouteTable> | undefined} */
|
|
10
|
+
let pending
|
|
11
|
+
/** @type {import('@wix/origami-types').SiteRouteTable | undefined} */
|
|
12
|
+
let current
|
|
13
|
+
let signature
|
|
14
|
+
/** @type {Set<(table: import('@wix/origami-types').SiteRouteTable) => void>} */
|
|
15
|
+
const listeners = new Set()
|
|
16
|
+
return {
|
|
17
|
+
table() {
|
|
18
|
+
if (closed) return Promise.reject(new Error('Site tree is closed.'))
|
|
19
|
+
if (dirty) {
|
|
20
|
+
dirty = false
|
|
21
|
+
// Serialize rescans, including recovery after a rejected scan.
|
|
22
|
+
pending = (pending ?? Promise.resolve()).catch(() => {}).then(async () => {
|
|
23
|
+
const next = await readSiteTree(rootPath)
|
|
24
|
+
const nextSignature = JSON.stringify(next)
|
|
25
|
+
if (signature !== nextSignature) {
|
|
26
|
+
const hadTable = current !== undefined
|
|
27
|
+
current = next
|
|
28
|
+
signature = nextSignature
|
|
29
|
+
if (hadTable && !closed) for (const listener of listeners) listener(next)
|
|
30
|
+
}
|
|
31
|
+
return /** @type {import('@wix/origami-types').SiteRouteTable} */ (current)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
return /** @type {Promise<import('@wix/origami-types').SiteRouteTable>} */ (pending)
|
|
35
|
+
},
|
|
36
|
+
fileChanged(path) {
|
|
37
|
+
if (closed) return
|
|
38
|
+
const changed = resolve(rootPath, path)
|
|
39
|
+
for (const directory of ['pages', 'popups']) {
|
|
40
|
+
const suffix = relative(resolve(rootPath, 'src', directory), changed)
|
|
41
|
+
if (suffix === '' || (suffix !== '..' && !suffix.startsWith(`..${sep}`) && !suffix.startsWith(sep))) dirty = true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
/** @param {(table: import('@wix/origami-types').SiteRouteTable) => void} listener */
|
|
45
|
+
onTableChange(listener) {
|
|
46
|
+
if (!closed) listeners.add(listener)
|
|
47
|
+
return () => { listeners.delete(listener) }
|
|
48
|
+
},
|
|
49
|
+
async close() {
|
|
50
|
+
closed = true
|
|
51
|
+
listeners.clear()
|
|
52
|
+
await pending?.catch(() => {})
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Ported from origami-site-runtime/routing/pagesToRoutes.js for the layout 2 tree.
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import { basename, resolve } from 'node:path'
|
|
4
|
+
import { parse } from '@babel/parser'
|
|
5
|
+
import { SiteTreeError } from '../requests/errors.js'
|
|
6
|
+
|
|
7
|
+
/** @type {Map<string, { mtimeMs: number, metadata: { title?: string, pageId?: string } }>} */
|
|
8
|
+
const metadataCache = new Map()
|
|
9
|
+
|
|
10
|
+
const readMetadata = structure => {
|
|
11
|
+
const ast = parse(readFileSync(structure, 'utf8'), { sourceType: 'module', plugins: ['jsx'] })
|
|
12
|
+
/** @type {{ title?: string, pageId?: string }} */
|
|
13
|
+
const metadata = {}
|
|
14
|
+
// Walk Babel's AST without a runtime dependency on @babel/types.
|
|
15
|
+
const visit = node => {
|
|
16
|
+
if (!node || typeof node !== 'object') return
|
|
17
|
+
if (node.type === 'JSXOpeningElement' && node.name.type === 'JSXIdentifier' && node.name.name === 'Page') {
|
|
18
|
+
for (const attribute of node.attributes) {
|
|
19
|
+
if (attribute.type !== 'JSXAttribute') continue
|
|
20
|
+
if (attribute.name.name === 'id' && attribute.value?.type === 'StringLiteral' && metadata.pageId === undefined)
|
|
21
|
+
metadata.pageId = attribute.value.value
|
|
22
|
+
if (metadata.title !== undefined || attribute.name.name !== 'data' ||
|
|
23
|
+
attribute.value?.type !== 'JSXExpressionContainer' ||
|
|
24
|
+
attribute.value.expression.type !== 'ObjectExpression') continue
|
|
25
|
+
const property = attribute.value.expression.properties.find(candidate =>
|
|
26
|
+
candidate.type === 'ObjectProperty' && !candidate.computed &&
|
|
27
|
+
((candidate.key.type === 'Identifier' && candidate.key.name === 'title') ||
|
|
28
|
+
(candidate.key.type === 'StringLiteral' && candidate.key.value === 'title')))
|
|
29
|
+
if (property?.value.type === 'StringLiteral') metadata.title = property.value.value
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const value of Object.values(node)) {
|
|
33
|
+
if (Array.isArray(value)) value.forEach(visit)
|
|
34
|
+
else if (value && typeof value === 'object') visit(value)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
visit(ast)
|
|
38
|
+
return metadata
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pageMetadata = folder => {
|
|
42
|
+
const structure = resolve(folder, 'structure.jsx')
|
|
43
|
+
const fallback = basename(folder).replace(/^\(([^()]+)\)$/, '$1')
|
|
44
|
+
if (!existsSync(structure)) {
|
|
45
|
+
metadataCache.delete(structure)
|
|
46
|
+
return { title: fallback }
|
|
47
|
+
}
|
|
48
|
+
const { mtimeMs } = statSync(structure)
|
|
49
|
+
let cached = metadataCache.get(structure)
|
|
50
|
+
if (cached?.mtimeMs !== mtimeMs) {
|
|
51
|
+
cached = { mtimeMs, metadata: readMetadata(structure) }
|
|
52
|
+
metadataCache.set(structure, cached)
|
|
53
|
+
}
|
|
54
|
+
return { ...cached.metadata, title: cached.metadata.title ?? fallback }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const siteRoot = root => {
|
|
58
|
+
if (typeof root !== 'string' || !root) throw new TypeError('root must be a non-empty string.')
|
|
59
|
+
return resolve(root)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @param {string} root @returns {Promise<import('@wix/origami-types').SiteRouteTable>} */
|
|
63
|
+
export const readSiteTree = async root => {
|
|
64
|
+
const pagesDir = resolve(siteRoot(root), 'src/pages')
|
|
65
|
+
/** @type {import('@wix/origami-types').PageRoute[]} */
|
|
66
|
+
const pages = []
|
|
67
|
+
/** @type {import('@wix/origami-types').SiteNotFoundRoute} */
|
|
68
|
+
let notFound = { key: '@wix/origami-site/default-error-404', title: 'Page not found' }
|
|
69
|
+
const pathsByUrl = new Map()
|
|
70
|
+
const walk = (folder, itemPath) => {
|
|
71
|
+
const entries = readdirSync(folder, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))
|
|
72
|
+
const path = `src/${itemPath}`
|
|
73
|
+
if (/^\[.*\]$/.test(basename(folder)))
|
|
74
|
+
throw new SiteTreeError('NOT_SUPPORTED_YET', [path], 'Router pages are not supported yet.')
|
|
75
|
+
if (entries.some(entry => entry.name === 'route.json'))
|
|
76
|
+
throw new SiteTreeError('NOT_SUPPORTED_YET', [`${path}/route.json`], 'Router pages are not supported yet.')
|
|
77
|
+
const children = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.') &&
|
|
78
|
+
!['_generated', 'node_modules'].includes(entry.name))
|
|
79
|
+
if (itemPath === 'pages/error404' && children.length)
|
|
80
|
+
throw new SiteTreeError('NOT_FOUND_HAS_CHILDREN', [path, ...children.map(child => `${path}/${child.name}`)],
|
|
81
|
+
'The not-found page cannot have children.')
|
|
82
|
+
if (entries.some(entry => entry.isFile() && !entry.name.startsWith('.'))) {
|
|
83
|
+
const metadata = pageMetadata(folder)
|
|
84
|
+
if (itemPath === 'pages/error404') notFound = { key: itemPath, itemPath, title: metadata.title }
|
|
85
|
+
else {
|
|
86
|
+
const url = '/' + itemPath.split('/').slice(1).filter(segment => !/^\([^()]+\)$/.test(segment)).join('/')
|
|
87
|
+
const previous = pathsByUrl.get(url)
|
|
88
|
+
if (previous) throw new SiteTreeError('DUPLICATE_URL', [previous, path], `Both pages serve "${url}".`)
|
|
89
|
+
pathsByUrl.set(url, path)
|
|
90
|
+
pages.push({ key: itemPath, url, itemPath, ...metadata, access: [] })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const child of children) walk(resolve(folder, child.name), `${itemPath}/${child.name}`)
|
|
94
|
+
}
|
|
95
|
+
if (existsSync(pagesDir)) walk(pagesDir, 'pages')
|
|
96
|
+
return { version: 2, pages, routers: [], notFound, popups: [] }
|
|
97
|
+
}
|