@wix/origami-site 0.1.0 → 0.1.2

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.0",
3
+ "version": "0.1.2",
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,12 +9,14 @@
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",
16
16
  "typescript": "^6.0.3",
17
- "vite": "^8"
17
+ "vite": "^8",
18
+ "@vitejs/plugin-react": "^5.2.0",
19
+ "postcss": "^8.5.3"
18
20
  },
19
21
  "publishUnscoped": false,
20
22
  "publishConfig": {
@@ -43,7 +45,9 @@
43
45
  "react": "^18.3.1",
44
46
  "react-dom": "^18.3.1",
45
47
  "@wix/builder-error-pages": "^1.1.6",
46
- "@wix/builder-piler-components": "^1.6.7"
48
+ "@wix/builder-piler-components": "^1.6.7",
49
+ "@wix/origami-transforms": "0.1.2",
50
+ "@babel/parser": "^7.25.0"
47
51
  },
48
52
  "exports": {
49
53
  "./server": {
@@ -58,14 +62,20 @@
58
62
  "types": "./src/popups/index.d.ts",
59
63
  "default": "./src/popups/index.js"
60
64
  },
61
- "./vite-plugins": {
62
- "types": "./src/vite-plugins/index.d.ts",
63
- "default": "./src/vite-plugins/index.js"
65
+ "./build": {
66
+ "types": "./src/build/index.d.ts",
67
+ "default": "./src/build/index.js"
64
68
  },
65
69
  "./types": {
66
70
  "types": "./src/types/index.d.ts"
67
71
  },
68
- "./default-error-404": "./src/default-error-404.jsx"
72
+ "./default-error-404": {
73
+ "types": "./src/default-error-404.d.ts",
74
+ "default": "./src/default-error-404.jsx"
75
+ }
76
+ },
77
+ "peerDependencies": {
78
+ "vite": "^8"
69
79
  },
70
- "falconPackageHash": "88d8479096308cdba298b8fe696e53dbd9a3482bd149a332bd45b100"
80
+ "falconPackageHash": "c7e37593d4cdf506898d4bc2680d2948adb3027e23c106ffad08d458"
71
81
  }
@@ -0,0 +1,18 @@
1
+ import { existsSync, statSync } from 'node:fs'
2
+ import { dirname, resolve, sep } from 'node:path'
3
+
4
+ /** @param {string} root
5
+ * @returns {import('vite').Plugin} */
6
+ export const globalImports = root => ({
7
+ name: 'origami:global-imports',
8
+ enforce: /** @type {const} */ ('pre'),
9
+ resolveId(source, importer) {
10
+ if (!importer || source === './' || !(source.startsWith('./') || source.startsWith('../'))) return undefined
11
+ const folder = resolve(dirname(importer.split('?', 1)[0]), source)
12
+ if (!folder.startsWith(`${resolve(root, 'src/global')}${sep}`)) return undefined
13
+ const entry = resolve(folder, '_generated/index.jsx')
14
+ if (existsSync(folder) && statSync(folder).isDirectory() && existsSync(entry) && statSync(entry).isFile())
15
+ return entry
16
+ return undefined
17
+ },
18
+ })
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from 'vite'
2
+ import type { OrigamiSite } from '../server/index.js'
3
+
4
+ export type OrigamiVitePluginsOptions = {
5
+ /** The site package folder, containing src/pages/, src/global/, and src/site/ glue. */
6
+ root: string
7
+ /** Shared with the adapter; the plugins do not scan or own the site tree. */
8
+ siteTree: Pick<OrigamiSite, 'table' | 'onTableChange'>
9
+ }
10
+ export function origamiVitePlugins(options: OrigamiVitePluginsOptions): Plugin[]
@@ -0,0 +1,15 @@
1
+ import { transforms } from './transforms.js'
2
+ import { globalImports } from './globalImports.js'
3
+ import { pageList } from './pageList.js'
4
+ import { sharedPackages } from './sharedPackages.js'
5
+
6
+ /** @param {import('./index.js').OrigamiVitePluginsOptions} options
7
+ * @returns {import('vite').Plugin[]} */
8
+ export function origamiVitePlugins({ root, siteTree }) {
9
+ return [
10
+ globalImports(root),
11
+ pageList(siteTree),
12
+ ...transforms({ root }),
13
+ sharedPackages(),
14
+ ]
15
+ }
@@ -0,0 +1,37 @@
1
+ const publicId = 'virtual:origami-site'
2
+ const resolvedId = `\0${publicId}`
3
+
4
+ /** @param {import('@wix/origami-types').SiteRouteTable} table */
5
+ function moduleSource(table) {
6
+ // itemPath is relative to src/ and already includes the pages/ prefix.
7
+ const entries = [...table.pages, table.notFound].map(page => {
8
+ const entry = page.itemPath
9
+ ? `/src/${page.itemPath}/_generated/index.jsx`
10
+ : '@wix/origami-site/default-error-404'
11
+ return ` [${JSON.stringify(page.key)}]: () => import(${JSON.stringify(entry)})`
12
+ })
13
+ return `export const pages = {\n${entries.join(',\n')}\n};\nexport const popups = {};\nexport const seoUriByPageId = {};\n`
14
+ }
15
+
16
+ /** @param {import('./index.js').OrigamiVitePluginsOptions['siteTree']} siteTree
17
+ * @returns {import('vite').Plugin} */
18
+ export function pageList(siteTree) {
19
+ /** @type {(() => void) | undefined} */
20
+ let unsubscribe
21
+ return {
22
+ name: 'origami:page-list',
23
+ resolveId(id) { if (id === publicId) return resolvedId },
24
+ async load(id) { if (id === resolvedId) return moduleSource(await siteTree.table()) },
25
+ configureServer(server) {
26
+ unsubscribe?.()
27
+ unsubscribe = siteTree.onTableChange(() => {
28
+ for (const environment of Object.values(server.environments)) {
29
+ const module = environment.moduleGraph.getModuleById(resolvedId)
30
+ if (module) environment.moduleGraph.invalidateModule(module)
31
+ }
32
+ server.ws.send({ type: 'full-reload', path: '*' })
33
+ })
34
+ },
35
+ closeBundle() { unsubscribe?.(); unsubscribe = undefined },
36
+ }
37
+ }
@@ -0,0 +1,14 @@
1
+ /** @returns {import('vite').Plugin} */
2
+ export function sharedPackages() {
3
+ return {
4
+ name: 'origami:shared-packages',
5
+ config: () => ({ resolve: { dedupe: [
6
+ 'react',
7
+ 'react-dom',
8
+ '@wix/services-manager-react',
9
+ '@wix/sdk-react-context',
10
+ '@preact/signals-react',
11
+ '@preact/signals-core',
12
+ ] } }),
13
+ }
14
+ }
@@ -0,0 +1,27 @@
1
+ import { resolve } from 'node:path'
2
+ import { createTransformPipeline } from '@wix/origami-transforms'
3
+
4
+ /** @param {{ root: string }} options
5
+ * @returns {import('vite').Plugin[]} */
6
+ export function transforms({ root }) {
7
+ const pipeline = createTransformPipeline({ layout: { root: resolve(root) } })
8
+ return [
9
+ {
10
+ name: 'origami:jsx-checks',
11
+ enforce: 'pre',
12
+ config(_, { command }) { pipeline.setCommand(command) },
13
+ transform(code, id, options) {
14
+ if (pipeline.appliesTo(id, { ssr: options?.ssr })) return pipeline.transformJsx(code, id, { ssr: options?.ssr })
15
+ },
16
+ },
17
+ {
18
+ name: 'origami:css-transforms',
19
+ config: () => ({ css: { postcss: { plugins: [pipeline.postcssPlugin] } } }),
20
+ handleHotUpdate({ file, server }) {
21
+ if (pipeline.failedStylesheetDependsOn(resolve(file))) {
22
+ server.ws.send({ type: 'full-reload', path: '*' })
23
+ }
24
+ },
25
+ },
26
+ ]
27
+ }
@@ -0,0 +1,4 @@
1
+ import type { ComponentType } from 'react'
2
+
3
+ declare const DefaultError404: ComponentType
4
+ export default DefaultError404
@@ -1,9 +1,4 @@
1
- import React from 'react'
2
- import { createRoot } from 'react-dom/client'
3
1
  import { DefaultError404 } from '@wix/builder-error-pages'
4
- import { PilerPage } from '@wix/builder-piler-components'
5
2
  import '@wix/builder-error-pages/style.css'
6
3
 
7
- createRoot(document.getElementById('root')).render(
8
- React.createElement(React.StrictMode, null, React.createElement(PilerPage, { Page: DefaultError404 }))
9
- )
4
+ export default DefaultError404
@@ -70,7 +70,8 @@ export interface AccessHandler {
70
70
  }
71
71
 
72
72
 
73
- export type { SiteTreeError, RouterServiceError } from './requests/errors.js'
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>
@@ -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
+ }
@@ -1,2 +0,0 @@
1
- import type { Plugin } from 'vite'
2
- export function origamiVitePlugins(options: { root: string }): Plugin[]
@@ -1 +0,0 @@
1
- export {}