@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.
Files changed (39) hide show
  1. package/README.md +79 -0
  2. package/package.json +68 -0
  3. package/src/cssPlugins/customMedia.js +52 -0
  4. package/src/cssPlugins/index.js +18 -0
  5. package/src/cssPlugins/layout/README.md +77 -0
  6. package/src/cssPlugins/layout/compiler.js +116 -0
  7. package/src/cssPlugins/layout/contract.js +35 -0
  8. package/src/cssPlugins/layout/index.js +12 -0
  9. package/src/cssPlugins/layout/parser.js +81 -0
  10. package/src/cssPlugins/mixins.js +23 -0
  11. package/src/cssPlugins/spx/README.md +73 -0
  12. package/src/cssPlugins/spx/calls.js +12 -0
  13. package/src/cssPlugins/spx/cascade.js +41 -0
  14. package/src/cssPlugins/spx/index.js +13 -0
  15. package/src/default-error-404.jsx +9 -0
  16. package/src/index.js +4 -0
  17. package/src/jsxPlugins/index.js +8 -0
  18. package/src/jsxPlugins/origamiCompatibility/ast.js +30 -0
  19. package/src/jsxPlugins/origamiCompatibility/componentBindings.js +43 -0
  20. package/src/jsxPlugins/origamiCompatibility/index.js +48 -0
  21. package/src/jsxPlugins/origamiCompatibility/rules/containerProps.js +15 -0
  22. package/src/jsxPlugins/origamiCompatibility/rules/richTextHtml.js +22 -0
  23. package/src/routing/documentHtml.js +19 -0
  24. package/src/routing/pagesToRoutes.js +110 -0
  25. package/src/routing/siteLayout.js +31 -0
  26. package/src/routing/urlPath.js +62 -0
  27. package/src/siteViteConfig.js +91 -0
  28. package/src/transform/context.js +13 -0
  29. package/src/transform/css/postcssPlugin.js +35 -0
  30. package/src/transform/css/runPlugins.js +22 -0
  31. package/src/transform/jsx/babelAdapter.js +37 -0
  32. package/src/transform/jsx/runPlugins.js +20 -0
  33. package/src/transform/pipeline.js +55 -0
  34. package/src/transform/registry.js +40 -0
  35. package/src/transform/types.js +39 -0
  36. package/src/vitePlugins/documentMiddleware.js +42 -0
  37. package/src/vitePlugins/itemFolderImports.js +16 -0
  38. package/src/vitePlugins/reactBabel.js +6 -0
  39. 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,6 @@
1
+ // React/Babel wiring; transform selection is delegated to the site's pipeline.
2
+
3
+ import react from '@vitejs/plugin-react'
4
+
5
+ export const reactBabelPlugin = ({ pipeline }) =>
6
+ react({ babel: (id, { ssr }) => pipeline.babelOptionsFor(id, { ssr }) })
@@ -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
+ })