@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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/package.json +8 -8
  3. package/src/index.js +2 -2
  4. package/src/routing/siteLayout.js +1 -9
  5. package/src/siteViteConfig.js +4 -4
  6. package/src/cssPlugins/customMedia.js +0 -52
  7. package/src/cssPlugins/fonts.js +0 -93
  8. package/src/cssPlugins/index.js +0 -32
  9. package/src/cssPlugins/layout/README.md +0 -77
  10. package/src/cssPlugins/layout/compiler.js +0 -116
  11. package/src/cssPlugins/layout/contract.js +0 -35
  12. package/src/cssPlugins/layout/index.js +0 -12
  13. package/src/cssPlugins/layout/parser.js +0 -81
  14. package/src/cssPlugins/mixins.js +0 -63
  15. package/src/cssPlugins/presets.js +0 -73
  16. package/src/cssPlugins/spx/README.md +0 -73
  17. package/src/cssPlugins/spx/calls.js +0 -12
  18. package/src/cssPlugins/spx/cascade.js +0 -41
  19. package/src/cssPlugins/spx/index.js +0 -13
  20. package/src/cssPlugins/stackChildren.js +0 -205
  21. package/src/jsxPlugins/index.js +0 -10
  22. package/src/jsxPlugins/mediaValidation/index.js +0 -49
  23. package/src/jsxPlugins/origamiCompatibility/ast.js +0 -30
  24. package/src/jsxPlugins/origamiCompatibility/componentBindings.js +0 -40
  25. package/src/jsxPlugins/origamiCompatibility/index.js +0 -48
  26. package/src/jsxPlugins/origamiCompatibility/rules/containerProps.js +0 -15
  27. package/src/jsxPlugins/origamiCompatibility/rules/richTextHtml.js +0 -22
  28. package/src/jsxPlugins/validation/checks.js +0 -105
  29. package/src/jsxPlugins/validation/index.js +0 -65
  30. package/src/jsxPlugins/validation/rules.js +0 -24
  31. package/src/jsxPlugins/validation/tree.js +0 -15
  32. package/src/transform/context.js +0 -13
  33. package/src/transform/css/postcssPlugin.js +0 -60
  34. package/src/transform/css/runPlugins.js +0 -40
  35. package/src/transform/jsx/babelAdapter.js +0 -37
  36. package/src/transform/jsx/runPlugins.js +0 -23
  37. package/src/transform/layoutError.js +0 -19
  38. package/src/transform/pipeline.js +0 -60
  39. package/src/transform/registry.js +0 -40
  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
- }
@@ -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
- }
@@ -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 {}