@wix/origami-site 0.0.0-stage → 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,72 @@
1
1
  {
2
2
  "name": "@wix/origami-site",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
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
+ "type": "module",
6
+ "files": [
7
+ "src"
8
+ ],
9
+ "scripts": {
10
+ "build": "tsc --noEmit",
11
+ "lint": "oxlint .",
12
+ "test": "node --test test/*.spec.js && tsc --noEmit -p tsconfig.test.json"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^22.0.0",
16
+ "typescript": "^6.0.3",
17
+ "vite": "^8"
18
+ },
19
+ "publishUnscoped": false,
20
+ "publishConfig": {
21
+ "registry": "https://registry.npmjs.org/",
22
+ "access": "public"
23
+ },
24
+ "wix": {
25
+ "artifact": {
26
+ "groupId": "com.wixpress.npm",
27
+ "artifactId": "origami-site",
28
+ "targets": {
29
+ "docker": false
30
+ }
31
+ },
32
+ "validations": {
33
+ "source": [
34
+ "lint"
35
+ ],
36
+ "postBuild": [
37
+ "test"
38
+ ]
39
+ }
40
+ },
41
+ "dependencies": {
42
+ "@wix/origami-types": "1.2.0",
43
+ "react": "^18.3.1",
44
+ "react-dom": "^18.3.1",
45
+ "@wix/builder-error-pages": "^1.1.6",
46
+ "@wix/builder-piler-components": "^1.6.7",
47
+ "@babel/parser": "^7.25.0"
48
+ },
49
+ "exports": {
50
+ "./server": {
51
+ "types": "./src/server/index.d.ts",
52
+ "default": "./src/server/index.js"
53
+ },
54
+ "./browser": {
55
+ "types": "./src/browser/index.d.ts",
56
+ "default": "./src/browser/index.js"
57
+ },
58
+ "./popups": {
59
+ "types": "./src/popups/index.d.ts",
60
+ "default": "./src/popups/index.js"
61
+ },
62
+ "./vite-plugins": {
63
+ "types": "./src/vite-plugins/index.d.ts",
64
+ "default": "./src/vite-plugins/index.js"
65
+ },
66
+ "./types": {
67
+ "types": "./src/types/index.d.ts"
68
+ },
69
+ "./default-error-404": "./src/default-error-404.jsx"
70
+ },
71
+ "falconPackageHash": "236041398ac8f5cb5996fd571ab7dbe9f17287f25c51e01599157dc7"
6
72
  }
@@ -0,0 +1,10 @@
1
+ import type { PageData } from '../server/index.js'
2
+ // PLAN-QUESTION: mountPage options follow the example; page modules remain unknown until milestone 1.7.
3
+ export type MountPageOptions = {
4
+ pages: Record<string, () => Promise<unknown>>
5
+ popups: Record<string, () => Promise<unknown>>
6
+ seoUriByPageId: Record<string, string>
7
+ beforeMount: () => Promise<unknown>
8
+ }
9
+ export function mountPage(element: Element | null, options: MountPageOptions): Promise<() => void>
10
+ export function readPageData(): PageData
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,9 @@
1
+ import React from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import { DefaultError404 } from '@wix/builder-error-pages'
4
+ import { PilerPage } from '@wix/builder-piler-components'
5
+ import '@wix/builder-error-pages/style.css'
6
+
7
+ createRoot(document.getElementById('root')).render(
8
+ React.createElement(React.StrictMode, null, React.createElement(PilerPage, { Page: DefaultError404 }))
9
+ )
@@ -0,0 +1,3 @@
1
+ // PLAN-QUESTION: Popup option/result types are unspecified here; leave options opaque until milestone 3.2.
2
+ export function openPopup(seoUri: string, options?: Record<string, unknown>): void
3
+ export function closePopup(): void
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,81 @@
1
+ import type { SiteRouteTable, PageRoute, RouterRoute, AccessRule } from '@wix/origami-types'
2
+ export type { AccessRule } from '@wix/origami-types'
3
+ // PLAN-QUESTION: Logger is unnamed structurally in the plan; only warn/error are used.
4
+ export type Logger = Pick<Console, 'warn' | 'error'>
5
+ export type RequestContext = { request: Request; url: URL; tokens: TokenProvider; locals: Map<string, unknown> }
6
+
7
+ export type SiteOptions = {
8
+ root: string // the site package folder
9
+ mode: 'dev' | 'prod'
10
+ table?: SiteRouteTable // prod: the table from the build. dev: omitted, the core scans.
11
+ wix: { clientId: string; siteId: string } // from VITE_WIX_CLIENT_ID / VITE_WIX_SITE_ID
12
+ tokens?: TokenProvider // default: builderAuthTokenProvider (3.3)
13
+ access?: AccessSource // default: noAccessRules (4)
14
+ accessHandlers?: AccessHandler[] // default: []
15
+ fetch?: typeof fetch // tests inject a fake
16
+ wixApiBaseUrl?: string // default 'https://www.wixapis.com'; the conformance suite points it at the fake
17
+ routerTimeoutMs?: number // default 5000
18
+ logger?: Logger // default: console with an [origami] prefix
19
+ }
20
+
21
+ export interface OrigamiSite {
22
+ /** Current route table (dev: rescans if dirty). */
23
+ table(): Promise<SiteRouteTable>
24
+ /** Reference URL matcher. Pure, no network. */
25
+ match(url: URL): Promise<RouteMatch>
26
+ /** Answer one request. Never throws: errors become a Resolution. */
27
+ handle(request: Request): Promise<Resolution>
28
+ /** Dev: tell the core a file changed (adapters forward Vite watcher events). */
29
+ fileChanged(path: string): void
30
+ /** Dev: subscribe to table changes, e.g. to invalidate the virtual:origami-site module. */
31
+ onTableChange(listener: (table: SiteRouteTable) => void): () => void
32
+ close(): Promise<void>
33
+ }
34
+
35
+ export type RouteMatch =
36
+ | { kind: 'page'; route: PageRoute }
37
+ | { kind: 'router'; route: RouterRoute; suffix: string } // '/a/b/c'; '/' for bare '/team'
38
+ | { kind: 'reserved'; path: string } // /_origami/*
39
+ | { kind: 'notFound' }
40
+
41
+ export type Resolution =
42
+ | { kind: 'page'; status: 200 | 404; page: PageRef; data: PageData; headers: Headers }
43
+ | { kind: 'response'; response: Response } // redirects, reserved endpoints, 405, 503
44
+
45
+ export type PageRef = { key: string; itemPath?: string; title: string }
46
+
47
+ export type PageData = {
48
+ page: string // PageRef.key; the browser loads this page
49
+ route: { kind: 'page' | 'router' | 'notFound'; url: string; role?: string }
50
+ services: Record<string, unknown> // runtime configs merged into the page's services
51
+ }
52
+
53
+ export interface TokenProvider {
54
+ /** The access token to use for this request. Called at most once per request (memoised in the context). */
55
+ getAccessToken(request: Request): Promise<string>
56
+ }
57
+
58
+ // AccessRule is shared with route types. // e.g. { type: 'members' }, { type: 'password' }
59
+
60
+ /** Where rules come from: a file next to the page, a central file, … — not decided. One swappable loader. */
61
+ export interface AccessSource {
62
+ load(table: SiteRouteTable, root: string): Promise<Map<string /* route key */, AccessRule[]>>
63
+ }
64
+ export const noAccessRules: AccessSource
65
+
66
+ /** One handler per rule type. Returns null to allow, or a Response (redirect, 401, a form). */
67
+ export interface AccessHandler {
68
+ type: string
69
+ check(rule: AccessRule, request: Request, ctx: RequestContext): Promise<Response | null>
70
+ }
71
+
72
+
73
+ export { SiteTreeError } from './requests/errors.js'
74
+ export type { RouterServiceError } from './requests/errors.js'
75
+ export function createSite(options: SiteOptions): Promise<OrigamiSite>
76
+ export function readSiteTree(root: string): Promise<SiteRouteTable>
77
+ export function runInRequestContext<T>(ctx: RequestContext, fn: () => Promise<T>): Promise<T>
78
+ export function getRequestContext(): RequestContext
79
+ export function getAccessToken(): Promise<string>
80
+ export function serializePageData(data: PageData): string
81
+ export function builderAuthTokenProvider(options: { clientId: string; siteId: string }): TokenProvider
@@ -0,0 +1,2 @@
1
+ export { readSiteTree } from './site-tree/readSiteTree.js'
2
+ export { SiteTreeError } from './requests/errors.js'
@@ -0,0 +1,25 @@
1
+ export class SiteTreeError extends Error {
2
+ code:
3
+ | 'NOT_SUPPORTED_YET' // router pages are not supported in stage 1
4
+ | 'DUPLICATE_URL' // two folders give the same URL
5
+ | 'ROUTE_JSON_INVALID' // not JSON, or a required field missing or of the wrong type
6
+ | 'ROUTE_JSON_MISSING_SERVICE_URL' // no routerServiceUrl: origami refuses to start
7
+ | 'ROUTE_JSON_SIBLINGS_DIFFER' // [a]/route.json and [b]/route.json differ
8
+ | 'ROLE_FOLDER_MISSING' // pages.member = 'team-member' but no [team-member]/ folder
9
+ | 'ROLE_FOLDER_NOT_IN_PAGES' // a [x]/ folder that no role in pages names
10
+ | 'BRACKET_OUTSIDE_PREFIX' // [x]/ not directly under a prefix folder
11
+ | 'PREFIX_HAS_FILES' // team/ itself holds page files
12
+ | 'ROLE_PAGE_HAS_CHILDREN' // a folder inside [x]/ that isn't _generated
13
+ | 'PREFIX_COLLIDES_WITH_PAGE' // pages/team is both a page and a prefix
14
+ | 'NOT_FOUND_HAS_CHILDREN'
15
+ | 'DUPLICATE_POPUP'
16
+ | 'UNSUPPORTED_ACCESS_RULE' // see section 4
17
+ constructor(code: SiteTreeError['code'], paths: string[], message: string)
18
+ paths: string[]
19
+ }
20
+
21
+ export class RouterServiceError extends Error {
22
+ code: 'TIMEOUT' | 'NETWORK' | 'HTTP_STATUS' | 'BAD_RESPONSE'
23
+ routerPrefix: string
24
+ status?: number
25
+ }
@@ -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 @@
1
+ export {}
@@ -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 @@
1
+ export {}
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ // The copied 404 entry imports the package's stylesheet as a bundler side effect.
2
+ declare module '@wix/builder-error-pages/style.css'
@@ -0,0 +1,48 @@
1
+ /** What each adapter package exports as its default `origami` descriptor. */
2
+ export interface OrigamiAdapter {
3
+ name: string // '@wix/origami-site-astro'
4
+ version: string // its package version
5
+ framework: { name: string; range: string } // { name: 'astro', range: '^7.3.0' }
6
+ /** Files and package.json fields the generator writes for this adapter (section 8). */
7
+ project(ctx: ProjectContext): ProjectFiles
8
+ /** How the conformance suite runs a site with this adapter. */
9
+ conformance: ConformanceDriver
10
+ }
11
+
12
+ export type ProjectContext = {
13
+ siteName: string
14
+ coreVersion: string // the @wix/origami-site range to depend on
15
+ nodeVersion: string // '22'
16
+ }
17
+
18
+ export type ProjectFiles = {
19
+ files: Record<string /* path relative to the site package */, string /* contents */> // e.g. astro.config.mjs, Dockerfile
20
+ packageJson: {
21
+ scripts: { dev: string; build: string; start: string } & Record<string, string>
22
+ dependencies: Record<string, string>
23
+ devDependencies?: Record<string, string>
24
+ }
25
+ readme: string // markdown section appended to the site README
26
+ }
27
+
28
+ export interface ConformanceDriver {
29
+ /** Start `yarn dev`. Resolves when the server answers. */
30
+ dev(root: string, env: SiteEnv): Promise<RunningSite>
31
+ // PLAN-QUESTION: Keep the explicit BuildResult union; rejection policy is deferred to adapters.
32
+ /** Run `yarn build`. */
33
+ build(root: string, env: SiteEnv): Promise<BuildResult>
34
+ /** Start `yarn start` after a build. */
35
+ start(root: string, env: SiteEnv): Promise<RunningSite>
36
+ }
37
+
38
+ export type SiteEnv = Record<string, string> // VITE_WIX_CLIENT_ID, VITE_WIX_SITE_ID, ORIGAMI_WIX_API_BASE (tests), PORT
39
+ export type RunningSite = { url: URL; logs(): string; restarts(): number; stop(): Promise<void> }
40
+ export type BuildResult = { ok: true; output: string } | { ok: false; output: string }
41
+
42
+
43
+ /** Core contracts only; runtime APIs belong to @wix/origami-site/server. */
44
+ export type {
45
+ SiteOptions, OrigamiSite, RouteMatch, Resolution, PageRef, PageData,
46
+ RequestContext, TokenProvider, AccessRule, AccessSource, AccessHandler,
47
+ Logger, SiteTreeError, RouterServiceError,
48
+ } from '../server/index.js'
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite'
2
+ export function origamiVitePlugins(options: { root: string }): Plugin[]
@@ -0,0 +1 @@
1
+ export {}
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.