@wix/origami-site-runtime 1.0.0
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/README.md +79 -0
- package/package.json +68 -0
- package/src/cssPlugins/customMedia.js +52 -0
- package/src/cssPlugins/index.js +18 -0
- package/src/cssPlugins/layout/README.md +77 -0
- package/src/cssPlugins/layout/compiler.js +116 -0
- package/src/cssPlugins/layout/contract.js +35 -0
- package/src/cssPlugins/layout/index.js +12 -0
- package/src/cssPlugins/layout/parser.js +81 -0
- package/src/cssPlugins/mixins.js +23 -0
- package/src/cssPlugins/spx/README.md +73 -0
- package/src/cssPlugins/spx/calls.js +12 -0
- package/src/cssPlugins/spx/cascade.js +41 -0
- package/src/cssPlugins/spx/index.js +13 -0
- package/src/default-error-404.jsx +9 -0
- package/src/index.js +4 -0
- package/src/jsxPlugins/index.js +8 -0
- package/src/jsxPlugins/origamiCompatibility/ast.js +30 -0
- package/src/jsxPlugins/origamiCompatibility/componentBindings.js +43 -0
- package/src/jsxPlugins/origamiCompatibility/index.js +48 -0
- package/src/jsxPlugins/origamiCompatibility/rules/containerProps.js +15 -0
- package/src/jsxPlugins/origamiCompatibility/rules/richTextHtml.js +22 -0
- package/src/routing/documentHtml.js +19 -0
- package/src/routing/pagesToRoutes.js +110 -0
- package/src/routing/siteLayout.js +31 -0
- package/src/routing/urlPath.js +62 -0
- package/src/siteViteConfig.js +91 -0
- package/src/transform/context.js +13 -0
- package/src/transform/css/postcssPlugin.js +35 -0
- package/src/transform/css/runPlugins.js +22 -0
- package/src/transform/jsx/babelAdapter.js +37 -0
- package/src/transform/jsx/runPlugins.js +20 -0
- package/src/transform/pipeline.js +55 -0
- package/src/transform/registry.js +40 -0
- package/src/transform/types.js +39 -0
- package/src/vitePlugins/documentMiddleware.js +42 -0
- package/src/vitePlugins/itemFolderImports.js +16 -0
- package/src/vitePlugins/reactBabel.js +6 -0
- package/src/vitePlugins/siteDocumentsPlugin.js +35 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The JSX stage's bridge to Babel.
|
|
2
|
+
//
|
|
3
|
+
// Exactly one Babel plugin is registered per module, however many of our plugins are selected: this
|
|
4
|
+
// adapter. Babel parses the module once, and the adapter's Program visitor is where that AST is
|
|
5
|
+
// handed to the stage — the same hand-off PostCSS gives the CSS stage. Our plugins are therefore
|
|
6
|
+
// plain functions over an AST, and none of them has to be shaped like a Babel plugin.
|
|
7
|
+
//
|
|
8
|
+
// The adapter is a stable module-level function and the per-file work is passed as Babel plugin
|
|
9
|
+
// options, so Babel still sees the same plugin identity on every file.
|
|
10
|
+
|
|
11
|
+
import { runJsxPlugins } from './runPlugins.js'
|
|
12
|
+
|
|
13
|
+
const babelAdapter = () => ({
|
|
14
|
+
name: 'wix-import-jsx-transform',
|
|
15
|
+
visitor: {
|
|
16
|
+
Program(program, state) {
|
|
17
|
+
const { plugins, context } = state.opts
|
|
18
|
+
runJsxPlugins({ plugins, program, context })
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const createBabelOptionsSelector =
|
|
24
|
+
({ selectPlugins, contextFor }) =>
|
|
25
|
+
(id, { ssr = false } = {}) => {
|
|
26
|
+
const context = contextFor(id, { ssr })
|
|
27
|
+
const plugins = selectPlugins(context)
|
|
28
|
+
if (!plugins.length) return { plugins: [] }
|
|
29
|
+
|
|
30
|
+
// Parser plugins configure the parse itself, so they are collected before anything runs.
|
|
31
|
+
const parserPlugins = [...new Set(plugins.flatMap(plugin => plugin.parserPlugins ?? []))]
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
plugins: [[babelAdapter, { plugins, context }]],
|
|
35
|
+
...(parserPlugins.length ? { parserOpts: { plugins: parserPlugins } } : {}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Runs the selected JSX plugins over one shared Program path, in order. Parsing and printing belong
|
|
2
|
+
// to Babel, which drives this stage — the only thing added here is attributing a failure to the
|
|
3
|
+
// plugin and the file that caused it.
|
|
4
|
+
|
|
5
|
+
const reason = cause => (cause instanceof Error ? cause.message : String(cause))
|
|
6
|
+
|
|
7
|
+
export const runJsxPlugins = ({ plugins, program, context }) => {
|
|
8
|
+
for (const plugin of plugins) {
|
|
9
|
+
try {
|
|
10
|
+
plugin.transform(program, context)
|
|
11
|
+
} catch (cause) {
|
|
12
|
+
// Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
|
|
13
|
+
// plugin may throw any value, so it is read without assuming an Error.
|
|
14
|
+
throw new Error(
|
|
15
|
+
`JSX transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`,
|
|
16
|
+
{ cause }
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Select and cache transform plugins by stage, file, and SSR context.
|
|
2
|
+
import { cssPlugins as defaultCssPlugins } from '../cssPlugins/index.js'
|
|
3
|
+
import { jsxPlugins as defaultJsxPlugins } from '../jsxPlugins/index.js'
|
|
4
|
+
import { createContextFactory } from './context.js'
|
|
5
|
+
import { createCssPostcssPlugin } from './css/postcssPlugin.js'
|
|
6
|
+
import { createBabelOptionsSelector } from './jsx/babelAdapter.js'
|
|
7
|
+
import { createTransformRegistry } from './registry.js'
|
|
8
|
+
|
|
9
|
+
const createState = ({ layout, registries, command }) => {
|
|
10
|
+
const contextFactory = createContextFactory({ layout, command })
|
|
11
|
+
/** @type {Map<string, readonly object[]>} `<stage>\0<ssr>\0<filePath>` -> selected plugins */
|
|
12
|
+
const selections = new Map()
|
|
13
|
+
|
|
14
|
+
const selectPlugins = (stage, context) => {
|
|
15
|
+
const key = `${stage}\0${context.ssr ? 1 : 0}\0${context.filePath}`
|
|
16
|
+
const cached = selections.get(key)
|
|
17
|
+
if (cached !== undefined) return cached
|
|
18
|
+
|
|
19
|
+
const selected = registries[stage].select(context)
|
|
20
|
+
selections.set(key, selected)
|
|
21
|
+
return selected
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { contextFactory, selectPlugins }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {{
|
|
29
|
+
* layout: ReturnType<typeof import('../routing/siteLayout.js').siteLayout>,
|
|
30
|
+
* jsxPlugins?: readonly import('./types.js').JsxTransformPlugin[],
|
|
31
|
+
* cssPlugins?: readonly import('./types.js').CssTransformPlugin[],
|
|
32
|
+
* }} options
|
|
33
|
+
*/
|
|
34
|
+
export const createTransformPipeline = ({ layout, jsxPlugins = defaultJsxPlugins, cssPlugins = defaultCssPlugins }) => {
|
|
35
|
+
const registries = {
|
|
36
|
+
jsx: createTransformRegistry(jsxPlugins),
|
|
37
|
+
css: createTransformRegistry(cssPlugins),
|
|
38
|
+
}
|
|
39
|
+
let command
|
|
40
|
+
let state
|
|
41
|
+
|
|
42
|
+
const currentState = () => (state ??= createState({ layout, registries, command }))
|
|
43
|
+
const contextFor = (id, options) => currentState().contextFactory(id, options)
|
|
44
|
+
const selectFor = stage => context => currentState().selectPlugins(stage, context)
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
postcssPlugin: createCssPostcssPlugin({ contextFor, selectPlugins: selectFor('css') }),
|
|
48
|
+
babelOptionsFor: createBabelOptionsSelector({ contextFor, selectPlugins: selectFor('jsx') }),
|
|
49
|
+
|
|
50
|
+
setCommand: value => {
|
|
51
|
+
command = value
|
|
52
|
+
state = undefined
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// One stage's plugins, kept in the order they run.
|
|
2
|
+
//
|
|
3
|
+
// Ordering is resolved once per registration rather than per selection: a site re-transforms the
|
|
4
|
+
// same modules on every hot update, and the plugin list does not change between them. Selection
|
|
5
|
+
// stays pure — the pipeline decides whether to consult a registry at all, and caches what it gets
|
|
6
|
+
// back. Each stage gets its own registry, which is why nothing here knows about stages.
|
|
7
|
+
|
|
8
|
+
/** @type {readonly any[]} */
|
|
9
|
+
const EMPTY = Object.freeze([])
|
|
10
|
+
|
|
11
|
+
const byOrderThenRegistration = (left, right) => left.plugin.order - right.plugin.order || left.index - right.index
|
|
12
|
+
|
|
13
|
+
/** @param {readonly any[]} [plugins] */
|
|
14
|
+
export const createTransformRegistry = (plugins = EMPTY) => {
|
|
15
|
+
const names = new Set()
|
|
16
|
+
/** @type {{ plugin: any, index: number }[]} */
|
|
17
|
+
const entries = []
|
|
18
|
+
|
|
19
|
+
const registry = {
|
|
20
|
+
register: plugin => {
|
|
21
|
+
if (names.has(plugin.name)) throw new Error(`Duplicate transform plugin "${plugin.name}"`)
|
|
22
|
+
names.add(plugin.name)
|
|
23
|
+
|
|
24
|
+
entries.push({ plugin, index: entries.length })
|
|
25
|
+
entries.sort(byOrderThenRegistration)
|
|
26
|
+
return registry
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/** The plugins that apply to `context`, in order. Frozen; safe to hold onto. */
|
|
30
|
+
select: context => {
|
|
31
|
+
const selected = entries
|
|
32
|
+
.filter(({ plugin }) => !plugin.appliesTo || plugin.appliesTo(context))
|
|
33
|
+
.map(({ plugin }) => plugin)
|
|
34
|
+
return selected.length ? Object.freeze(selected) : EMPTY
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const plugin of plugins) registry.register(plugin)
|
|
39
|
+
return registry
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transform-plugin contract.
|
|
3
|
+
*
|
|
4
|
+
* Both stages have the same shape: an engine parses the file once, and each plugin is handed that
|
|
5
|
+
* AST plus the file's context and does what it likes with it. Babel and PostCSS are engines here,
|
|
6
|
+
* not plugin hosts — a plugin never has to know either one's own plugin protocol. Which stage a
|
|
7
|
+
* plugin belongs to is decided by the list it is registered in, so no descriptor restates it.
|
|
8
|
+
*
|
|
9
|
+
* @typedef {object} TransformContext
|
|
10
|
+
* @property {string} id Module id as the bundler reports it, query string included.
|
|
11
|
+
* @property {string} filePath `id` with any query stripped.
|
|
12
|
+
* @property {string | undefined} relativePath Posix path relative to the site root, or undefined when the file lies outside it.
|
|
13
|
+
* @property {ReturnType<typeof import('../routing/siteLayout.js').siteLayout>} layout
|
|
14
|
+
* @property {'serve' | 'build'} command
|
|
15
|
+
* @property {boolean} ssr Always false for the CSS stage, which PostCSS drives without an SSR signal.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {object} TransformPluginBase
|
|
18
|
+
* @property {string} name
|
|
19
|
+
* @property {number} order Ascending. Ties break by registration order.
|
|
20
|
+
* @property {(context: TransformContext) => boolean} [appliesTo] Whether the plugin runs for this
|
|
21
|
+
* file. Must depend only on the file's identity, not on anything that can change between transforms
|
|
22
|
+
* of the same file: the pipeline memoizes the selection per file path.
|
|
23
|
+
*
|
|
24
|
+
* @typedef {TransformPluginBase & {
|
|
25
|
+
* transform: (program: any, context: TransformContext) => void,
|
|
26
|
+
* parserPlugins?: readonly string[]
|
|
27
|
+
* }} JsxTransformPlugin
|
|
28
|
+
* `program` is the Babel `NodePath` for the module's Program node. Traverse it if you need to —
|
|
29
|
+
* nothing walks it for you, so a plugin that can rule itself out cheaply should just return.
|
|
30
|
+
* `parserPlugins` are Babel parser plugins the file needs; they are unioned across selected plugins
|
|
31
|
+
* and applied before any of them run.
|
|
32
|
+
*
|
|
33
|
+
* @typedef {TransformPluginBase & {
|
|
34
|
+
* validate?: (root: import('postcss').Root, context: TransformContext) => void,
|
|
35
|
+
* transform: (root: import('postcss').Root, context: TransformContext) => void
|
|
36
|
+
* }} CssTransformPlugin
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export {}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Serves a route-matched HTML document for requests Vite itself did not handle.
|
|
2
|
+
|
|
3
|
+
import { Buffer } from 'node:buffer'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { renderPageDocument } from '../routing/documentHtml.js'
|
|
6
|
+
import { pagesToRoutes } from '../routing/pagesToRoutes.js'
|
|
7
|
+
import { normalizeRequestPath } from '../routing/urlPath.js'
|
|
8
|
+
|
|
9
|
+
const DOCUMENT_METHODS = new Set(['GET', 'HEAD'])
|
|
10
|
+
|
|
11
|
+
const acceptsHtml = request => {
|
|
12
|
+
const { accept } = request.headers
|
|
13
|
+
if (accept === undefined) return true
|
|
14
|
+
return accept.split(',').some(value => value.trim().startsWith('text/html'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const isDocumentRequest = request => DOCUMENT_METHODS.has(request.method) && acceptsHtml(request)
|
|
18
|
+
|
|
19
|
+
export const createDocumentMiddleware = ({ layout, transformHtml }) =>
|
|
20
|
+
async function serveDocument(request, response, next) {
|
|
21
|
+
if (!isDocumentRequest(request)) return next()
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const table = pagesToRoutes(layout.root)
|
|
25
|
+
const template = await readFile(layout.template, 'utf8')
|
|
26
|
+
const requestPath = normalizeRequestPath(request.url)
|
|
27
|
+
const matched = table.routes.find(route => route.url === requestPath)
|
|
28
|
+
const document = renderPageDocument({ template, route: matched ?? table.notFound })
|
|
29
|
+
const html = await transformHtml(requestPath ?? '/', document)
|
|
30
|
+
|
|
31
|
+
response.statusCode = matched ? 200 : 404
|
|
32
|
+
response.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
33
|
+
response.setHeader('Cache-Control', 'no-store')
|
|
34
|
+
// A HEAD response has to carry the headers a GET would, so the length is set before the
|
|
35
|
+
// body is skipped. Measured in bytes, not characters: a non-ASCII page name makes those
|
|
36
|
+
// differ, and a wrong Content-Length is worse than a missing one.
|
|
37
|
+
response.setHeader('Content-Length', Buffer.byteLength(html))
|
|
38
|
+
response.end(request.method === 'HEAD' ? undefined : html)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
next(error)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
|
+
import { dirname, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const itemFolderImports = root => ({
|
|
5
|
+
name: 'wix-item-folder-imports',
|
|
6
|
+
enforce: /** @type {const} */ ('pre'),
|
|
7
|
+
resolveId(source, importer) {
|
|
8
|
+
if (!importer || source === './' || !(source.startsWith('./') || source.startsWith('../'))) return undefined
|
|
9
|
+
const folder = resolve(dirname(importer.split('?', 1)[0]), source)
|
|
10
|
+
if (!folder.startsWith(`${resolve(root, 'src/global')}${sep}`)) return undefined
|
|
11
|
+
const entry = resolve(folder, '_generated/index.jsx')
|
|
12
|
+
if (existsSync(folder) && statSync(folder).isDirectory() && existsSync(entry) && statSync(entry).isFile())
|
|
13
|
+
return entry
|
|
14
|
+
return undefined
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The `wix-import-site` Vite plugin: serves route-matched documents in development, synthesizes one
|
|
2
|
+
// virtual HTML entry per render target for the build.
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { renderPageDocument } from '../routing/documentHtml.js'
|
|
6
|
+
import { createDocumentMiddleware } from './documentMiddleware.js'
|
|
7
|
+
|
|
8
|
+
export const siteDocumentsPlugin = ({ layout, documents, pipeline }) => ({
|
|
9
|
+
name: 'wix-import-site',
|
|
10
|
+
|
|
11
|
+
config(_, { command }) {
|
|
12
|
+
pipeline.setCommand(command)
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
configureServer(server) {
|
|
16
|
+
// Returned as a post hook so document rendering only sees requests Vite did not handle.
|
|
17
|
+
return () =>
|
|
18
|
+
server.middlewares.use(
|
|
19
|
+
createDocumentMiddleware({
|
|
20
|
+
layout,
|
|
21
|
+
transformHtml: (url, html) => server.transformIndexHtml(url, html),
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
resolveId(id) {
|
|
27
|
+
return documents.has(id) ? id : undefined
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
load(id) {
|
|
31
|
+
const route = documents.get(id)
|
|
32
|
+
if (!route) return undefined
|
|
33
|
+
return renderPageDocument({ template: readFileSync(layout.template, 'utf8'), route })
|
|
34
|
+
},
|
|
35
|
+
})
|