@wix/origami-site-runtime 1.0.13 → 1.0.15
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 +1 -1
- package/package.json +8 -8
- package/src/index.js +2 -2
- package/src/routing/siteLayout.js +1 -9
- package/src/siteViteConfig.js +4 -4
- package/src/cssPlugins/customMedia.js +0 -52
- package/src/cssPlugins/fonts.js +0 -93
- package/src/cssPlugins/index.js +0 -32
- package/src/cssPlugins/layout/README.md +0 -77
- package/src/cssPlugins/layout/compiler.js +0 -116
- package/src/cssPlugins/layout/contract.js +0 -35
- package/src/cssPlugins/layout/index.js +0 -12
- package/src/cssPlugins/layout/parser.js +0 -81
- package/src/cssPlugins/mixins.js +0 -63
- package/src/cssPlugins/presets.js +0 -73
- package/src/cssPlugins/spx/README.md +0 -73
- package/src/cssPlugins/spx/calls.js +0 -12
- package/src/cssPlugins/spx/cascade.js +0 -41
- package/src/cssPlugins/spx/index.js +0 -13
- package/src/cssPlugins/stackChildren.js +0 -205
- package/src/jsxPlugins/index.js +0 -10
- package/src/jsxPlugins/mediaValidation/index.js +0 -49
- package/src/jsxPlugins/origamiCompatibility/ast.js +0 -30
- package/src/jsxPlugins/origamiCompatibility/componentBindings.js +0 -40
- package/src/jsxPlugins/origamiCompatibility/index.js +0 -48
- package/src/jsxPlugins/origamiCompatibility/rules/containerProps.js +0 -15
- package/src/jsxPlugins/origamiCompatibility/rules/richTextHtml.js +0 -22
- package/src/jsxPlugins/validation/checks.js +0 -105
- package/src/jsxPlugins/validation/index.js +0 -65
- package/src/jsxPlugins/validation/rules.js +0 -24
- package/src/jsxPlugins/validation/tree.js +0 -15
- package/src/transform/context.js +0 -13
- package/src/transform/css/postcssPlugin.js +0 -60
- package/src/transform/css/runPlugins.js +0 -40
- package/src/transform/jsx/babelAdapter.js +0 -37
- package/src/transform/jsx/runPlugins.js +0 -23
- package/src/transform/layoutError.js +0 -19
- package/src/transform/pipeline.js +0 -60
- package/src/transform/registry.js +0 -40
- package/src/transform/types.js +0 -47
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Bridges generated Origami structures to the piler component contract by forcing a few props onto
|
|
2
|
-
// the components that need them.
|
|
3
|
-
//
|
|
4
|
-
// Each forced prop is a rule in `rules/`, declaring the components it applies to. That table is
|
|
5
|
-
// also what defines which imported names count as Origami components, so adding a rule extends
|
|
6
|
-
// import recognition with it — there is no second list to keep in sync.
|
|
7
|
-
|
|
8
|
-
import { basename } from 'node:path'
|
|
9
|
-
import { collectComponentBindings } from './componentBindings.js'
|
|
10
|
-
import { containerPropsRule } from './rules/containerProps.js'
|
|
11
|
-
import { richTextHtmlRule } from './rules/richTextHtml.js'
|
|
12
|
-
|
|
13
|
-
const STRUCTURE_FILE = 'structure.jsx'
|
|
14
|
-
|
|
15
|
-
const RULES = [containerPropsRule, richTextHtmlRule]
|
|
16
|
-
|
|
17
|
-
/** Origami component name -> the rules that force props on it. */
|
|
18
|
-
const RULES_BY_COMPONENT = new Map(
|
|
19
|
-
RULES.flatMap(rule => rule.components).map(component => [
|
|
20
|
-
component,
|
|
21
|
-
RULES.filter(rule => rule.components.includes(component)),
|
|
22
|
-
])
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
/** @type {import('../../transform/types.js').JsxTransformPlugin} */
|
|
26
|
-
export const origamiCompatibilityPlugin = {
|
|
27
|
-
name: 'wix-import-origami-compatibility',
|
|
28
|
-
order: 0,
|
|
29
|
-
appliesTo: context => basename(context.filePath) === STRUCTURE_FILE,
|
|
30
|
-
|
|
31
|
-
transform: program => {
|
|
32
|
-
// Import identity, not tag name, decides what is an Origami component — so a module that
|
|
33
|
-
// imports none cannot match a rule, and the tree is never walked.
|
|
34
|
-
const rulesByLocalName = collectComponentBindings(program.node, RULES_BY_COMPONENT)
|
|
35
|
-
if (rulesByLocalName.size === 0) return
|
|
36
|
-
|
|
37
|
-
program.traverse({
|
|
38
|
-
JSXOpeningElement(path) {
|
|
39
|
-
if (path.node.name.type !== 'JSXIdentifier') return
|
|
40
|
-
|
|
41
|
-
const rules = rulesByLocalName.get(path.node.name.name)
|
|
42
|
-
if (rules === undefined) return
|
|
43
|
-
|
|
44
|
-
for (const rule of rules) rule.apply(path.node)
|
|
45
|
-
},
|
|
46
|
-
})
|
|
47
|
-
},
|
|
48
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Container components need a `containerProps` object to satisfy the piler component contract.
|
|
2
|
-
|
|
3
|
-
import { types as t } from '@babel/core'
|
|
4
|
-
import { findAttribute } from '../ast.js'
|
|
5
|
-
|
|
6
|
-
export const containerPropsRule = {
|
|
7
|
-
components: ['Section', 'RefComponent', 'HeaderSection', 'FooterSection'],
|
|
8
|
-
apply: openingElement => {
|
|
9
|
-
if (findAttribute(openingElement.attributes, 'containerProps')) return
|
|
10
|
-
|
|
11
|
-
openingElement.attributes.push(
|
|
12
|
-
t.jsxAttribute(t.jsxIdentifier('containerProps'), t.jsxExpressionContainer(t.objectExpression([])))
|
|
13
|
-
)
|
|
14
|
-
},
|
|
15
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// Rich text needs an `html` field, which the export only provides as `text`.
|
|
2
|
-
|
|
3
|
-
import { types as t } from '@babel/core'
|
|
4
|
-
import { indexProperties, objectPropValue } from '../ast.js'
|
|
5
|
-
|
|
6
|
-
export const richTextHtmlRule = {
|
|
7
|
-
components: ['WRichText'],
|
|
8
|
-
apply: openingElement => {
|
|
9
|
-
const richText = objectPropValue(openingElement, 'richText')
|
|
10
|
-
if (!richText) return
|
|
11
|
-
|
|
12
|
-
const fields = indexProperties(richText.properties)
|
|
13
|
-
if (fields.has('html')) return
|
|
14
|
-
|
|
15
|
-
// Only a literal text/linkList pair is safe to derive from statically.
|
|
16
|
-
const text = fields.get('text')
|
|
17
|
-
const linkList = fields.get('linkList')
|
|
18
|
-
if (text?.value.type !== 'StringLiteral' || linkList?.value.type !== 'ArrayExpression') return
|
|
19
|
-
|
|
20
|
-
richText.properties.push(t.objectProperty(t.identifier('html'), t.cloneNode(text.value, true)))
|
|
21
|
-
},
|
|
22
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// The checks behind the constraints in ./rules.js. Each gets the element a rule applies to and
|
|
2
|
-
// reports what it found; the message states the rule's whole constraint and how to fix it, so one
|
|
3
|
-
// error is enough to write the correct structure.
|
|
4
|
-
//
|
|
5
|
-
// To add a kind of constraint: add a function to CHECKS, use its key in rules.js, and extend
|
|
6
|
-
// expectation() so messages state it.
|
|
7
|
-
|
|
8
|
-
import { describe } from './tree.js'
|
|
9
|
-
|
|
10
|
-
const CHILD_CONSTRAINTS = ['allowedChildren', 'minChildren', 'maxChildren']
|
|
11
|
-
|
|
12
|
-
const list = names => {
|
|
13
|
-
const items = names.map(name => `<${name}>`)
|
|
14
|
-
return items.length === 1 ? items[0] : `${items.slice(0, -1).join(', ')} or ${items.at(-1)}`
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// "must have exactly one <InternalContainer> child"
|
|
18
|
-
const childrenExpectation = ({ allowedChildren, minChildren: min, maxChildren: max }) => {
|
|
19
|
-
const count =
|
|
20
|
-
min !== undefined && min === max
|
|
21
|
-
? `exactly ${min === 1 ? 'one' : min}`
|
|
22
|
-
: min !== undefined && max !== undefined
|
|
23
|
-
? `between ${min} and ${max}`
|
|
24
|
-
: min !== undefined
|
|
25
|
-
? `at least ${min}`
|
|
26
|
-
: max !== undefined
|
|
27
|
-
? `at most ${max}`
|
|
28
|
-
: 'only'
|
|
29
|
-
const kind = allowedChildren ? list(allowedChildren) : 'JSX element'
|
|
30
|
-
const noun = min === 1 && max === 1 ? 'child' : 'children'
|
|
31
|
-
return `must have ${count} ${kind} ${noun}`
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const expectation = rule => {
|
|
35
|
-
const parts = []
|
|
36
|
-
if (rule.allowedChildren || rule.minChildren !== undefined || rule.maxChildren !== undefined) {
|
|
37
|
-
parts.push(childrenExpectation(rule))
|
|
38
|
-
}
|
|
39
|
-
return parts.join(', and ')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const at = node => (node.loc ? ` (line ${node.loc.start.line})` : '')
|
|
43
|
-
|
|
44
|
-
/** A constrained parent's children must be literal JSX tags. */
|
|
45
|
-
const checkLiteral = ({ node, fail }) => {
|
|
46
|
-
const unseen = what => fail(`${what}; write each child as a JSX tag`)
|
|
47
|
-
for (const attribute of node.openingElement.attributes) {
|
|
48
|
-
if (attribute.type === 'JSXSpreadAttribute') unseen(`spread props${at(attribute)}`)
|
|
49
|
-
else if (attribute.name.name === 'children') unseen(`a children= prop${at(attribute)}`)
|
|
50
|
-
}
|
|
51
|
-
for (const child of node.children) {
|
|
52
|
-
if (child.type === 'JSXExpressionContainer' && child.expression.type !== 'JSXEmptyExpression') {
|
|
53
|
-
unseen(`a {…} expression${at(child)}`)
|
|
54
|
-
} else if (child.type === 'JSXSpreadChild') unseen(`a {...spread} child${at(child)}`)
|
|
55
|
-
else if (child.type === 'JSXFragment') unseen(`a fragment${at(child)}`)
|
|
56
|
-
else if (child.type === 'JSXText' && child.value.trim()) unseen(`text${at(child)}`)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const CHECKS = {
|
|
61
|
-
allowedChildren: (allowed, { elements, fail }) => {
|
|
62
|
-
for (const { element, component } of elements) {
|
|
63
|
-
if (!allowed.includes(component)) fail(`${describe(element)}`)
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
minChildren: (min, { elements, fail }) => {
|
|
67
|
-
if (elements.length < min) fail(elements.length === 0 ? 'none' : `${elements.length}`)
|
|
68
|
-
},
|
|
69
|
-
maxChildren: (max, { elements, fail }) => {
|
|
70
|
-
if (elements.length > max) fail(`${elements.length}`)
|
|
71
|
-
},
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Fail fast on a typo in rules.js.
|
|
75
|
-
export const assertKnownConstraints = rules => {
|
|
76
|
-
for (const rule of rules) {
|
|
77
|
-
for (const key of Object.keys(rule)) {
|
|
78
|
-
if (key !== 'id' && key !== 'components' && !CHECKS[key]) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
`layout rules: unknown constraint "${key}" on ${rule.id}. Known: ${Object.keys(CHECKS).join(', ')}`
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const constrainsChildren = rule => CHILD_CONSTRAINTS.some(key => key in rule)
|
|
88
|
-
|
|
89
|
-
/** Runs every constraint of `rule` on one element; `report` receives each full message. */
|
|
90
|
-
export const checkRule = (rule, { node, componentOf, report }) => {
|
|
91
|
-
const prefix = `${describe(node)} ${expectation(rule)}`
|
|
92
|
-
const fail = got => report(`${prefix}; found ${got}.`)
|
|
93
|
-
|
|
94
|
-
const elements = node.children
|
|
95
|
-
.filter(child => child.type === 'JSXElement')
|
|
96
|
-
.map(element => ({ element, component: componentOf(element) }))
|
|
97
|
-
const context = { node, elements, fail }
|
|
98
|
-
|
|
99
|
-
if (constrainsChildren(rule)) checkLiteral(context)
|
|
100
|
-
for (const [key, value] of Object.entries(rule)) CHECKS[key]?.(value, context)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** `createElement(Section, …)` would hide a constrained component's children from the checks. */
|
|
104
|
-
export const checkCreateElement = (rule, { node, component, report }) =>
|
|
105
|
-
report(`<${component}>${at(node)} ${expectation(rule)}; found createElement(${component}), write it as a JSX tag.`)
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// Checks authored structures against the Studio 2 site rulebook layout rules. It changes
|
|
2
|
-
// nothing: every violation in the file is collected, and the build fails once with all of them.
|
|
3
|
-
//
|
|
4
|
-
// One walk serves every rule. Rules are data (./rules.js); each names the components it looks at,
|
|
5
|
-
// identified for now by JSX tag name as written (<Section>, <InternalContainer>), whatever they are
|
|
6
|
-
// imported from, and the checks (./checks.js) run on each matching element.
|
|
7
|
-
|
|
8
|
-
import { basename } from 'node:path'
|
|
9
|
-
import { LayoutError } from '../../transform/layoutError.js'
|
|
10
|
-
import { assertKnownConstraints, checkCreateElement, checkRule, constrainsChildren } from './checks.js'
|
|
11
|
-
import { RULES } from './rules.js'
|
|
12
|
-
|
|
13
|
-
const STRUCTURE_FILES = new Set(['structure.tsx', 'structure.jsx'])
|
|
14
|
-
|
|
15
|
-
assertKnownConstraints(RULES)
|
|
16
|
-
|
|
17
|
-
/** Component (tag) name -> the rules that check it. */
|
|
18
|
-
const RULES_BY_COMPONENT = new Map(
|
|
19
|
-
RULES.flatMap(rule => rule.components).map(component => [
|
|
20
|
-
component,
|
|
21
|
-
RULES.filter(rule => rule.components.includes(component)),
|
|
22
|
-
])
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
const isCreateElement = callee =>
|
|
26
|
-
(callee.type === 'Identifier' && callee.name === 'createElement') ||
|
|
27
|
-
(callee.type === 'MemberExpression' && !callee.computed && callee.property.name === 'createElement')
|
|
28
|
-
|
|
29
|
-
/** @type {import('../../transform/types.js').JsxTransformPlugin} */
|
|
30
|
-
export const layoutValidationPlugin = {
|
|
31
|
-
name: 'wix-import-layout-validation',
|
|
32
|
-
// Before any plugin that rewrites the tree, so the rules see it as authored.
|
|
33
|
-
order: -100,
|
|
34
|
-
appliesTo: context => STRUCTURE_FILES.has(basename(context.filePath)),
|
|
35
|
-
|
|
36
|
-
transform: program => {
|
|
37
|
-
// The component an element is: its tag name, as written.
|
|
38
|
-
const componentOf = node => {
|
|
39
|
-
const name = node.openingElement.name
|
|
40
|
-
return name.type === 'JSXIdentifier' ? name.name : undefined
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const violations = []
|
|
44
|
-
const report = message => violations.push(message)
|
|
45
|
-
program.traverse({
|
|
46
|
-
JSXElement(path) {
|
|
47
|
-
const component = componentOf(path.node)
|
|
48
|
-
const rules = component === undefined ? undefined : RULES_BY_COMPONENT.get(component)
|
|
49
|
-
if (rules === undefined) return
|
|
50
|
-
for (const rule of rules) checkRule(rule, { node: path.node, componentOf, report })
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
CallExpression({ node }) {
|
|
54
|
-
const [type] = node.arguments
|
|
55
|
-
if (!isCreateElement(node.callee) || type?.type !== 'Identifier') return
|
|
56
|
-
const component = type.name
|
|
57
|
-
for (const rule of RULES_BY_COMPONENT.get(component) ?? []) {
|
|
58
|
-
if (constrainsChildren(rule)) checkCreateElement(rule, { node, component, report })
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
if (violations.length) throw new LayoutError(violations)
|
|
64
|
-
},
|
|
65
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// The Studio 2 site rulebook rules (rev 1.40) this build checks, as data.
|
|
2
|
-
// A rule lives here unless TypeScript can enforce all of it; a rule is never split between the two.
|
|
3
|
-
//
|
|
4
|
-
// A rule names the components it applies to, by JSX tag name, and the constraints they must meet. Each
|
|
5
|
-
// constraint is a check in ./checks.js, keyed by its name:
|
|
6
|
-
// allowedChildren: string[] — the only components allowed as direct children
|
|
7
|
-
// minChildren: number — minimum number of direct child elements
|
|
8
|
-
// maxChildren: number — maximum number of direct child elements
|
|
9
|
-
//
|
|
10
|
-
// A component whose children a rule constrains must be written as literal JSX with literal child
|
|
11
|
-
// tags: {expressions}, fragments, text, spread props, a children= prop and createElement() are
|
|
12
|
-
// errors, so the check always sees the real structure.
|
|
13
|
-
|
|
14
|
-
const STANDARD_SECTIONS = ['Section', 'HeaderSection', 'FooterSection']
|
|
15
|
-
|
|
16
|
-
export const RULES = [
|
|
17
|
-
{
|
|
18
|
-
id: 'S2-SECTION-001',
|
|
19
|
-
components: STANDARD_SECTIONS,
|
|
20
|
-
allowedChildren: ['InternalContainer'],
|
|
21
|
-
minChildren: 1,
|
|
22
|
-
maxChildren: 1,
|
|
23
|
-
},
|
|
24
|
-
]
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Reads over the authored JSX tree, shared by the layout rules.
|
|
2
|
-
|
|
3
|
-
/** `<Section id="x">` at line 12 → `<Section id="x"> (line 12)`, for error messages. */
|
|
4
|
-
export const describe = node => {
|
|
5
|
-
const opening = node.openingElement
|
|
6
|
-
const tag = opening.name.type === 'JSXIdentifier' ? opening.name.name : 'element'
|
|
7
|
-
const id = opening.attributes.find(
|
|
8
|
-
attribute =>
|
|
9
|
-
attribute.type === 'JSXAttribute' &&
|
|
10
|
-
attribute.name.name === 'id' &&
|
|
11
|
-
attribute.value?.type === 'StringLiteral'
|
|
12
|
-
)
|
|
13
|
-
const line = node.loc ? ` (line ${node.loc.start.line})` : ''
|
|
14
|
-
return `<${tag}${id ? ` id="${id.value.value}"` : ''}>${line}`
|
|
15
|
-
}
|
package/src/transform/context.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { relativeToSite } from '../routing/siteLayout.js'
|
|
2
|
-
|
|
3
|
-
export const createContextFactory =
|
|
4
|
-
({ layout, command }) =>
|
|
5
|
-
(id, { ssr = false } = {}) =>
|
|
6
|
-
Object.freeze({
|
|
7
|
-
id,
|
|
8
|
-
filePath: id.split('?', 1)[0],
|
|
9
|
-
relativePath: relativeToSite(layout.root, id),
|
|
10
|
-
layout,
|
|
11
|
-
command,
|
|
12
|
-
ssr,
|
|
13
|
-
})
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// The CSS stage, exposed as a PostCSS plugin rather than a Vite `transform` hook.
|
|
2
|
-
//
|
|
3
|
-
// That placement is load-bearing. A generated page imports one stylesheet that `@import`s the
|
|
4
|
-
// Origami carriers (layout.css, style.css); Vite inlines those with its own postcss-import, which
|
|
5
|
-
// reads them straight from disk and never runs plugin transform hooks over them. A Vite hook at
|
|
6
|
-
// `enforce: 'pre'` therefore sees the page's `@import` statements and nothing else. Vite installs
|
|
7
|
-
// postcss-import ahead of the configured PostCSS plugins, so by the time this one runs the
|
|
8
|
-
// stylesheet the plugins see is the whole page: a rule declared in one carrier and a rule that
|
|
9
|
-
// depends on it in another arrive in the same document.
|
|
10
|
-
|
|
11
|
-
import { runCssPlugins } from './runPlugins.js'
|
|
12
|
-
|
|
13
|
-
const DEPENDENCY_DIRECTORY = 'node_modules/'
|
|
14
|
-
|
|
15
|
-
// The site's own files, and only those. A stylesheet outside the root belongs to a dependency; so
|
|
16
|
-
// does one reached through the site's own node_modules, which merely happens to live inside it.
|
|
17
|
-
const isSiteOwned = relativePath =>
|
|
18
|
-
relativePath !== undefined &&
|
|
19
|
-
!relativePath.startsWith(DEPENDENCY_DIRECTORY) &&
|
|
20
|
-
!relativePath.includes(`/${DEPENDENCY_DIRECTORY}`)
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @param {{
|
|
24
|
-
* selectPlugins: (context: import('../types.js').TransformContext) => readonly object[],
|
|
25
|
-
* contextFor: (id: string) => import('../types.js').TransformContext,
|
|
26
|
-
* failures?: Map<string, Set<string>>,
|
|
27
|
-
* }} options `failures` maps each stylesheet whose last run failed to the files it declared before failing.
|
|
28
|
-
*/
|
|
29
|
-
export const createCssPostcssPlugin = ({ selectPlugins, contextFor, failures }) => ({
|
|
30
|
-
postcssPlugin: 'wix-import-css-transform',
|
|
31
|
-
|
|
32
|
-
Once(root, { result }) {
|
|
33
|
-
const from = result.opts.from
|
|
34
|
-
if (from === undefined) return
|
|
35
|
-
|
|
36
|
-
const fileContext = contextFor(from)
|
|
37
|
-
if (!isSiteOwned(fileContext.relativePath)) return
|
|
38
|
-
|
|
39
|
-
const plugins = selectPlugins(fileContext)
|
|
40
|
-
if (!plugins.length) return
|
|
41
|
-
// A plugin that reads another file declares it, so the bundler re-runs this stylesheet when that file
|
|
42
|
-
// changes. Vite reads these messages only from a run that succeeds, so a failed run's declarations are
|
|
43
|
-
// kept in `failures` instead; see vitePlugins/siteDocumentsPlugin.js.
|
|
44
|
-
const declared = new Set()
|
|
45
|
-
const addDependency = file => {
|
|
46
|
-
declared.add(file)
|
|
47
|
-
result.messages.push({ type: 'dependency', plugin: 'wix-import-css-transform', file, parent: from })
|
|
48
|
-
}
|
|
49
|
-
// A plugin reports a problem that need not stop the build as a PostCSS warning, which Vite prints with the
|
|
50
|
-
// node's code frame.
|
|
51
|
-
const warn = (node, text) => node.warn(result, text)
|
|
52
|
-
try {
|
|
53
|
-
runCssPlugins({ plugins, root, context: { ...fileContext, addDependency, warn } })
|
|
54
|
-
failures?.delete(from)
|
|
55
|
-
} catch (error) {
|
|
56
|
-
failures?.set(from, declared)
|
|
57
|
-
throw error
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
})
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// Runs the selected CSS plugins over one shared Root, in order. Parsing, stringifying and source
|
|
2
|
-
// maps belong to PostCSS, which drives this stage — the only thing added here is attributing a
|
|
3
|
-
// failure to the plugin and the file that caused it. A LayoutError is the author's, so it passes through.
|
|
4
|
-
//
|
|
5
|
-
// A LayoutError comes from a plugin that only checks, so the Root is as it was and the later plugins still run:
|
|
6
|
-
// the author gets every plugin's violations in one error, at the first one's position, not one plugin's per build.
|
|
7
|
-
|
|
8
|
-
import { LayoutError } from '../layoutError.js'
|
|
9
|
-
|
|
10
|
-
const reason = cause => (cause instanceof Error ? cause.message : String(cause))
|
|
11
|
-
|
|
12
|
-
export const runCssPlugins = ({ plugins, root, context }) => {
|
|
13
|
-
/** @type {LayoutError[]} */
|
|
14
|
-
const layoutErrors = []
|
|
15
|
-
for (const phase of ['validate', 'transform']) {
|
|
16
|
-
for (const plugin of plugins) {
|
|
17
|
-
try {
|
|
18
|
-
plugin[phase]?.(root, context)
|
|
19
|
-
} catch (cause) {
|
|
20
|
-
if (cause instanceof LayoutError) {
|
|
21
|
-
layoutErrors.push(cause)
|
|
22
|
-
continue
|
|
23
|
-
}
|
|
24
|
-
// Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
|
|
25
|
-
// plugin may throw any value, so it is read without assuming an Error.
|
|
26
|
-
throw new Error(
|
|
27
|
-
`CSS transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`,
|
|
28
|
-
{ cause }
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (layoutErrors.length === 1) throw layoutErrors[0]
|
|
34
|
-
if (layoutErrors.length > 1) {
|
|
35
|
-
throw new LayoutError(
|
|
36
|
-
layoutErrors.flatMap(error => error.violations),
|
|
37
|
-
layoutErrors[0].location
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
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. A LayoutError is the author's, so it passes through.
|
|
4
|
-
|
|
5
|
-
import { LayoutError } from '../layoutError.js'
|
|
6
|
-
|
|
7
|
-
const reason = cause => (cause instanceof Error ? cause.message : String(cause))
|
|
8
|
-
|
|
9
|
-
export const runJsxPlugins = ({ plugins, program, context }) => {
|
|
10
|
-
for (const plugin of plugins) {
|
|
11
|
-
try {
|
|
12
|
-
plugin.transform(program, context)
|
|
13
|
-
} catch (cause) {
|
|
14
|
-
if (cause instanceof LayoutError) throw cause
|
|
15
|
-
// Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
|
|
16
|
-
// plugin may throw any value, so it is read without assuming an Error.
|
|
17
|
-
throw new Error(
|
|
18
|
-
`JSX transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`,
|
|
19
|
-
{ cause }
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// A broken layout rule is the author's to fix, not a plugin crash. The stages pass it through as is,
|
|
2
|
-
// without naming the plugin, and it carries no stack: the bundler already names the file, and the
|
|
3
|
-
// messages say where in it.
|
|
4
|
-
|
|
5
|
-
export class LayoutError extends Error {
|
|
6
|
-
/**
|
|
7
|
-
* @param {readonly string[]} violations
|
|
8
|
-
* @param {{ file?: string, line?: number, column?: number }} [location] The first violation's, which the
|
|
9
|
-
* bundler prints as the error's position (Vite's CSS stage reads `file`, `line` and `column`).
|
|
10
|
-
*/
|
|
11
|
-
constructor(violations, location = {}) {
|
|
12
|
-
super(violations.length === 1 ? violations[0] : `\n ${violations.join('\n ')}`)
|
|
13
|
-
this.name = 'LayoutError'
|
|
14
|
-
this.stack = ''
|
|
15
|
-
this.violations = violations
|
|
16
|
-
this.location = location
|
|
17
|
-
Object.assign(this, location)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
/** @type {Map<string, Set<string>>} stylesheet whose last CSS run failed -> the files it declared */
|
|
46
|
-
const cssFailures = new Map()
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
postcssPlugin: createCssPostcssPlugin({ contextFor, selectPlugins: selectFor('css'), failures: cssFailures }),
|
|
50
|
-
babelOptionsFor: createBabelOptionsSelector({ contextFor, selectPlugins: selectFor('jsx') }),
|
|
51
|
-
|
|
52
|
-
/** Whether a stylesheet whose last CSS run failed declared `file`, so an edit to it may fix the stylesheet. */
|
|
53
|
-
failedStylesheetDependsOn: file => [...cssFailures.values()].some(files => files.has(file)),
|
|
54
|
-
|
|
55
|
-
setCommand: value => {
|
|
56
|
-
command = value
|
|
57
|
-
state = undefined
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
}
|
package/src/transform/types.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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: CssTransformContext) => void,
|
|
35
|
-
* transform: (root: import('postcss').Root, context: CssTransformContext) => void
|
|
36
|
-
* }} CssTransformPlugin
|
|
37
|
-
*
|
|
38
|
-
* @typedef {TransformContext & {
|
|
39
|
-
* addDependency?: (file: string) => void,
|
|
40
|
-
* warn?: (node: import('postcss').Node, text: string) => void
|
|
41
|
-
* }} CssTransformContext
|
|
42
|
-
* `addDependency` declares a file the plugin read besides the stylesheet, so the bundler re-runs the
|
|
43
|
-
* stylesheet when it changes. `warn` reports a problem at `node` without failing the run, as a PostCSS
|
|
44
|
-
* warning on the stylesheet's result. Both are absent when no bundler drives the run.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
export {}
|