@wix/origami-transforms 0.1.15 → 0.1.17
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/dist/cssPlugins/layout/contract.d.ts +1 -0
- package/dist/cssPlugins/shapeDividers.d.ts +2 -0
- package/dist/cssPlugins/structureFile.d.ts +1 -0
- package/dist/cssPlugins/svg.d.ts +14 -0
- package/dist/cssPlugins/wixVectorUrls.d.ts +2 -0
- package/dist/transform/css/postcssPlugin.d.ts +1 -1
- package/dist/transform/css/runPlugins.d.ts +1 -0
- package/dist/transform/pipeline.d.ts +1 -1
- package/dist/transform/types.d.ts +4 -6
- package/package.json +3 -3
- package/src/cssPlugins/customEnums.js +2 -22
- package/src/cssPlugins/index.js +6 -0
- package/src/cssPlugins/layout/README.md +15 -9
- package/src/cssPlugins/layout/compiler.js +13 -13
- package/src/cssPlugins/layout/contract.js +1 -0
- package/src/cssPlugins/shapeDividers.js +136 -0
- package/src/cssPlugins/spx/README.md +15 -8
- package/src/cssPlugins/spx/cascade.js +7 -2
- package/src/cssPlugins/structureFile.js +21 -0
- package/src/cssPlugins/svg.js +87 -0
- package/src/cssPlugins/wixVectorUrls.js +39 -0
- package/src/transform/css/postcssPlugin.js +21 -5
- package/src/transform/css/runPlugins.js +9 -6
- package/src/transform/types.js +4 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const CONTENT: "internal-container-content-wrapper";
|
|
2
2
|
export const OVERFLOW: "internal-container-overflow-wrapper";
|
|
3
|
+
export const DEFAULTS_LAYER: "origami-container-defaults";
|
|
3
4
|
export const targets: Readonly<{
|
|
4
5
|
"internal-container-content-wrapper": Set<string>;
|
|
5
6
|
"internal-container-overflow-wrapper": Set<string>;
|
|
@@ -2,3 +2,4 @@ export function structureFileOf(stylesheetPath: any): string | undefined;
|
|
|
2
2
|
export function parseStructure(structurePath: any): any;
|
|
3
3
|
export function walk(node: any, visit: any): void;
|
|
4
4
|
export function literalIdOf(openingElement: any): any;
|
|
5
|
+
export function importsById(structurePath: any): Map<any, any>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function loadSvgs(ids: any): Promise<undefined> | undefined;
|
|
2
|
+
export function loadedSvg(id: any): any;
|
|
3
|
+
export function svgInfo(svg: any): {
|
|
4
|
+
type: any;
|
|
5
|
+
viewBox: any;
|
|
6
|
+
bbox: any;
|
|
7
|
+
};
|
|
8
|
+
export function viewBoxOf({ type, viewBox, bbox }: {
|
|
9
|
+
type: any;
|
|
10
|
+
viewBox: any;
|
|
11
|
+
bbox: any;
|
|
12
|
+
}): any;
|
|
13
|
+
export function scaleSvg(svg: any, info: any, preserveAspectRatio: any): any;
|
|
14
|
+
export function svgDataUrl(svg: any): string;
|
|
@@ -23,7 +23,7 @@ export function createTransformPipeline({ layout, jsxPlugins, cssPlugins }: {
|
|
|
23
23
|
postcssPlugin: string;
|
|
24
24
|
Once(root: any, { result }: {
|
|
25
25
|
result: any;
|
|
26
|
-
}): void
|
|
26
|
+
}): void | Promise<void>;
|
|
27
27
|
};
|
|
28
28
|
babelOptionsFor: (id: any, { ssr }?: {
|
|
29
29
|
ssr?: boolean | undefined;
|
|
@@ -64,14 +64,12 @@ export type JsxTransformPlugin = TransformPluginBase & {
|
|
|
64
64
|
parserPlugins?: readonly string[];
|
|
65
65
|
};
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* AST plus the file's context and does what it likes with it. Babel and PostCSS are engines here,
|
|
71
|
-
* not plugin hosts — a plugin never has to know either one's own plugin protocol. Which stage a
|
|
72
|
-
* plugin belongs to is decided by the list it is registered in, so no descriptor restates it.
|
|
67
|
+
* `prepare` loads what the plugin needs that can't be read synchronously, such as a file from a CDN, before any
|
|
68
|
+
* plugin's `validate` or `transform` runs. It returns a promise only when it has something to load, so a stylesheet
|
|
69
|
+
* that needs nothing loaded stays synchronous for PostCSS.
|
|
73
70
|
*/
|
|
74
71
|
export type CssTransformPlugin = TransformPluginBase & {
|
|
72
|
+
prepare?: (root: import("postcss").Root, context: CssTransformContext) => Promise<void> | void;
|
|
75
73
|
validate?: (root: import("postcss").Root, context: CssTransformContext) => void;
|
|
76
74
|
transform: (root: import("postcss").Root, context: CssTransformContext) => void;
|
|
77
75
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-transforms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Babel and PostCSS checks and transforms for Wix origami page files.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@babel/parser": "^7.25.0",
|
|
19
19
|
"@babel/types": "^7.25.2",
|
|
20
20
|
"@wix/origami-fonts": "1.0.1",
|
|
21
|
-
"@wix/origami-system-components": "1.
|
|
21
|
+
"@wix/origami-system-components": "1.10.0",
|
|
22
22
|
"postcss": "^8.5.3",
|
|
23
23
|
"postcss-custom-media": "^12.0.1"
|
|
24
24
|
},
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
},
|
|
65
65
|
"type": "module",
|
|
66
66
|
"types": "./dist/index.d.ts",
|
|
67
|
-
"falconPackageHash": "
|
|
67
|
+
"falconPackageHash": "7fd53a47a2a80ee2e831d1b608e15378825592abfb78d94393377d13"
|
|
68
68
|
}
|
|
@@ -15,12 +15,10 @@ import { createRequire } from 'node:module'
|
|
|
15
15
|
import { basename, dirname, join } from 'node:path'
|
|
16
16
|
import postcss from 'postcss'
|
|
17
17
|
import { LayoutError } from '../transform/layoutError.js'
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { wrapperImport } from '../wrapperImport.js'
|
|
19
|
+
import { importsById, structureFileOf } from './structureFile.js'
|
|
20
20
|
|
|
21
21
|
const CUSTOM_ENUMS_CSS = 'custom-enums.css'
|
|
22
|
-
// `@origami-wrappers/<module>/<symbol>`, and nothing that could resolve outside its package.
|
|
23
|
-
const WRAPPER_IMPORT = new RegExp(`^${WRAPPERS_SCOPE}/[\\w-]+/[\\w-]+$`)
|
|
24
22
|
// A component type, `wixEditorElements.Button`: its module and symbol.
|
|
25
23
|
const COMPONENT_TYPE = /^([\w-]+)\.([\w-]+)$/
|
|
26
24
|
/** The selector's compounds, split at its combinators, outside parentheses and brackets. */
|
|
@@ -59,24 +57,6 @@ const lastMatch = (element, compounds) => {
|
|
|
59
57
|
return -1
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
/** Id → the wrapper import it is rendered from, for elements with a literal `id` and an imported wrapper tag. */
|
|
63
|
-
const importsById = structurePath => {
|
|
64
|
-
const program = parseStructure(structurePath)
|
|
65
|
-
const sources = new Map()
|
|
66
|
-
for (const node of program?.body ?? []) {
|
|
67
|
-
if (node.type !== 'ImportDeclaration' || !WRAPPER_IMPORT.test(node.source.value)) continue
|
|
68
|
-
for (const specifier of node.specifiers) sources.set(specifier.local.name, node.source.value)
|
|
69
|
-
}
|
|
70
|
-
const byId = new Map()
|
|
71
|
-
walk(program, node => {
|
|
72
|
-
if (node.type !== 'JSXOpeningElement' || node.name.type !== 'JSXIdentifier') return
|
|
73
|
-
const id = literalIdOf(node)
|
|
74
|
-
const source = sources.get(node.name.name)
|
|
75
|
-
if (id !== undefined && source !== undefined) byId.set(id, source)
|
|
76
|
-
})
|
|
77
|
-
return byId
|
|
78
|
-
}
|
|
79
|
-
|
|
80
60
|
/** A path with its symlinks resolved, so a wrapper reached through a package manager's links matches its folder. */
|
|
81
61
|
const real = path => {
|
|
82
62
|
try {
|
package/src/cssPlugins/index.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
// and layout move declarations out of the instance's rule. fonts appends the `@font-face` rules that load the Wix fonts
|
|
13
13
|
// the stylesheet names from Wix's CDN.
|
|
14
14
|
// component-position runs after mixins, so a position a mixin body sets is kept, and before layout compiles the rules.
|
|
15
|
+
// shape-dividers and wix-vector-urls run last: they load the SVGs a stylesheet names from Wix's CDN in `prepare`, and
|
|
16
|
+
// write them in as data URIs.
|
|
15
17
|
//
|
|
16
18
|
// `@layer` needs no plugin: cascade layers have been in every browser since March 2022.
|
|
17
19
|
|
|
@@ -21,9 +23,11 @@ import { customMediaPlugin } from './customMedia.js'
|
|
|
21
23
|
import { fontsPlugin } from './fonts.js'
|
|
22
24
|
import { mixinsPlugin } from './mixins.js'
|
|
23
25
|
import { presetsPlugin } from './presets.js'
|
|
26
|
+
import { shapeDividersPlugin } from './shapeDividers.js'
|
|
24
27
|
import { layoutPlugin } from './layout/index.js'
|
|
25
28
|
import { spxPlugin } from './spx/index.js'
|
|
26
29
|
import { stackChildrenPlugin } from './stackChildren.js'
|
|
30
|
+
import { wixVectorUrlsPlugin } from './wixVectorUrls.js'
|
|
27
31
|
|
|
28
32
|
/** @type {readonly import('../transform/types.js').CssTransformPlugin[]} */
|
|
29
33
|
export const cssPlugins = Object.freeze([
|
|
@@ -36,4 +40,6 @@ export const cssPlugins = Object.freeze([
|
|
|
36
40
|
componentPositionPlugin,
|
|
37
41
|
layoutPlugin,
|
|
38
42
|
spxPlugin,
|
|
43
|
+
shapeDividersPlugin,
|
|
44
|
+
wixVectorUrlsPlugin,
|
|
39
45
|
])
|
|
@@ -32,15 +32,21 @@ the outer element.
|
|
|
32
32
|
## What the plugin adds
|
|
33
33
|
|
|
34
34
|
The outer element and overflow wrapper get a one-cell grid so the wrappers fill
|
|
35
|
-
the container. The content wrapper
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Each container
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
the container. The content wrapper lets pointer events pass through empty space
|
|
36
|
+
while its children stay clickable. Vertical overflow also sets `--sticky-offset`
|
|
37
|
+
to `0px` for `scroll` and resets it for other values.
|
|
38
|
+
|
|
39
|
+
Each container's alignment defaults to center in a separate first CSS layer, so
|
|
40
|
+
authored values always win. Media queries and scopes around a rule still apply.
|
|
41
|
+
|
|
42
|
+
### Content max width
|
|
43
|
+
|
|
44
|
+
Where DM writes `--content-max-width` (usually a section's content wrapper), the
|
|
45
|
+
plugin adds `max-width` and an alignment `margin-left` next to it, and nowhere else.
|
|
46
|
+
|
|
47
|
+
- Those two properties do not inherit, so nested containers are not boxed in.
|
|
48
|
+
- `--content-max-width` does inherit, so nested containers use it as their SPX cap
|
|
49
|
+
(see the SPX plugin README). With none above, SPX is uncapped.
|
|
44
50
|
|
|
45
51
|
## Why validation runs before transformations
|
|
46
52
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import postcss from 'postcss'
|
|
2
2
|
import { buildContainerClasses } from '@wix/origami-system-components/layout-contract'
|
|
3
|
-
import { CONTENT } from './contract.js'
|
|
3
|
+
import { CONTENT, DEFAULTS_LAYER } from './contract.js'
|
|
4
4
|
import { isLayoutSource, parseTargets } from './parser.js'
|
|
5
5
|
|
|
6
6
|
const grid = { display: 'grid', 'grid-template-rows': '1fr', 'grid-template-columns': 'minmax(0, 1fr)' }
|
|
7
|
+
const contentMargin =
|
|
8
|
+
'clamp(0px, (100% - var(--content-max-width)) * var(--origami-content-align), 100 * var(--one-unit, 1%))'
|
|
7
9
|
|
|
8
10
|
/** Preserve source locations on every emitted declaration and selector. */
|
|
9
11
|
const ruleFor = (owner, selector, declarations) => {
|
|
@@ -21,12 +23,12 @@ const ruleFor = (owner, selector, declarations) => {
|
|
|
21
23
|
export const compileContainers = (root, context) => {
|
|
22
24
|
const owners = parseTargets(root, context)
|
|
23
25
|
if (!owners.size) return
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
26
|
+
// max-width and margin-left are written only where a content max width is set.
|
|
27
|
+
// Alignment is a custom property so it can change independently at breakpoints.
|
|
28
|
+
// That inherits, so each container needs a default to avoid taking its parent's alignment.
|
|
27
29
|
// This first layer keeps defaults weaker than authored rules, including rules in later layers.
|
|
28
30
|
// Clone only conditional/scope envelopes into it so the defaults apply under the same conditions.
|
|
29
|
-
const defaultsLayer = postcss.atRule({ name: 'layer', params:
|
|
31
|
+
const defaultsLayer = postcss.atRule({ name: 'layer', params: DEFAULTS_LAYER })
|
|
30
32
|
if (root.source) defaultsLayer.source = root.source
|
|
31
33
|
root.prepend(defaultsLayer)
|
|
32
34
|
// Source identity prevents a matching selector in another carrier from becoming an owner.
|
|
@@ -64,10 +66,7 @@ export const compileContainers = (root, context) => {
|
|
|
64
66
|
defaultsParent = clone
|
|
65
67
|
}
|
|
66
68
|
defaultsParent.append(
|
|
67
|
-
ruleFor(owner, `:where(${content})`, {
|
|
68
|
-
'--content-max-width': 'none',
|
|
69
|
-
'--origami-content-align': '0.5',
|
|
70
|
-
})
|
|
69
|
+
ruleFor(owner, `:where(${content})`, { '--origami-content-align': '0.5' })
|
|
71
70
|
)
|
|
72
71
|
owner.before(ruleFor(owner, selector, grid))
|
|
73
72
|
owner.before(ruleFor(owner, overflow, { ...grid, position: 'relative', 'align-self': 'stretch' }))
|
|
@@ -76,10 +75,6 @@ export const compileContainers = (root, context) => {
|
|
|
76
75
|
'box-sizing': 'border-box',
|
|
77
76
|
position: 'relative',
|
|
78
77
|
'pointer-events': 'none',
|
|
79
|
-
'max-width': 'var(--content-max-width)',
|
|
80
|
-
'--section-max-width': 'var(--content-max-width)',
|
|
81
|
-
'margin-left':
|
|
82
|
-
'clamp(0px, (100% - var(--content-max-width, 100%)) * var(--origami-content-align), 100 * var(--one-unit, 1%))',
|
|
83
78
|
})
|
|
84
79
|
)
|
|
85
80
|
owner.before(ruleFor(owner, `${content} > *`, { 'pointer-events': 'auto' }))
|
|
@@ -98,6 +93,11 @@ export const compileContainers = (root, context) => {
|
|
|
98
93
|
// Left margin uses none, half or all of the extra space to align the content.
|
|
99
94
|
clone.value = { left: '0', center: '0.5', right: '1' }[clone.value]
|
|
100
95
|
}
|
|
96
|
+
if (clone.prop === '--content-max-width')
|
|
97
|
+
generated.append(
|
|
98
|
+
clone.clone({ prop: 'max-width', value: 'var(--content-max-width)' }),
|
|
99
|
+
clone.clone({ prop: 'margin-left', value: contentMargin })
|
|
100
|
+
)
|
|
101
101
|
if (clone.prop === 'overflow-y')
|
|
102
102
|
generated.append(
|
|
103
103
|
clone.clone({
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Consumer routing only: component eligibility is validated by DM against JSX.
|
|
3
3
|
export const CONTENT = 'internal-container-content-wrapper'
|
|
4
4
|
export const OVERFLOW = 'internal-container-overflow-wrapper'
|
|
5
|
+
export const DEFAULTS_LAYER = 'origami-container-defaults'
|
|
5
6
|
export const targets = Object.freeze({
|
|
6
7
|
[CONTENT]: new Set([
|
|
7
8
|
'position',
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Draws the shape divider. A site sets only a BackgroundShapeDivider's vars (`--svgId`, `--side`, `--color`,
|
|
2
|
+
// `--repeats`, …); its wrapper's CSS paints each `[data-divider-layer]` from vars nothing sets, so the divider
|
|
3
|
+
// stays empty. A live Wix site writes one more rule per instance, from the divider's SVG
|
|
4
|
+
// (Thunderbolt's `SVGBackgroundImage` feature, keyed on the same component type); this writes the same rule after
|
|
5
|
+
// each instance rule that sets a divider var, in the same place, from the instance's base vars and that rule's.
|
|
6
|
+
// The color is written into the SVG, so a `--color` that is a var stays unresolved there.
|
|
7
|
+
|
|
8
|
+
import postcss from 'postcss'
|
|
9
|
+
import { wrapperImport } from '../wrapperImport.js'
|
|
10
|
+
import { loadedSvg, loadSvgs, scaleSvg, svgDataUrl, svgInfo, viewBoxOf } from './svg.js'
|
|
11
|
+
import { importsById, structureFileOf } from './structureFile.js'
|
|
12
|
+
|
|
13
|
+
const SHAPE_DIVIDER_IMPORT = wrapperImport({ module: 'wixSystemElements', symbol: 'BackgroundShapeDivider' })
|
|
14
|
+
const LAYER = '[data-divider-layer]'
|
|
15
|
+
const DIVIDER_VARS = ['--svgId', '--side', '--color', '--invert', '--keepAspectRatio', '--repeats', '--flip', '--layerPinFactor']
|
|
16
|
+
// One class or id: DM writes `.compId`.
|
|
17
|
+
const SINGLE_CLASS_OR_ID = /^[.#]([\w-]+)$/
|
|
18
|
+
// Closes the hairline Safari draws between the inverted path and its frame.
|
|
19
|
+
const HAIRLINE_FIX_FACTOR = 1
|
|
20
|
+
|
|
21
|
+
/** `--svgId`'s file id, `"0da768_….svg"` unquoted; '' for the `""` that turns the divider off. */
|
|
22
|
+
const svgIdOf = value => value.trim().replace(/^(['"])(.*)\1$/, '$2')
|
|
23
|
+
|
|
24
|
+
/** The instance rules that set a divider var: `.compId { … }`, at the top level or in an at-rule. */
|
|
25
|
+
const instanceRules = (root, dividerIds) => {
|
|
26
|
+
const rules = []
|
|
27
|
+
root.walkRules(rule => {
|
|
28
|
+
const id = rule.selector.match(SINGLE_CLASS_OR_ID)?.[1]
|
|
29
|
+
if (id === undefined || !dividerIds.has(id)) return
|
|
30
|
+
const vars = {}
|
|
31
|
+
for (const node of rule.nodes) if (node.type === 'decl' && DIVIDER_VARS.includes(node.prop)) vars[node.prop] = node.value.trim()
|
|
32
|
+
if (Object.keys(vars).length) rules.push({ rule, id, vars, base: rule.parent?.type === 'root' })
|
|
33
|
+
})
|
|
34
|
+
return rules
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The divider's ids in the stylesheet's structure file. */
|
|
38
|
+
const dividerIdsOf = (filePath, context) => {
|
|
39
|
+
const structurePath = filePath === undefined ? undefined : structureFileOf(filePath)
|
|
40
|
+
if (structurePath === undefined) return new Set()
|
|
41
|
+
context.addDependency?.(structurePath)
|
|
42
|
+
return new Set([...importsById(structurePath)].filter(([, source]) => source === SHAPE_DIVIDER_IMPORT).map(([id]) => id))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The path filled outside its shape, closing it with the frame of its box; Thunderbolt's `invertSvgContent`. */
|
|
46
|
+
const invertSvg = (svg, x, y, width, height) => {
|
|
47
|
+
const pathTag = svg.match(/(<path)[^>]*>/)?.[1] || ''
|
|
48
|
+
const pathData = svg.match(/<path.*\s*d="([^"]+)"/)?.[1] || ''
|
|
49
|
+
const frame = `M${x},${y - HAIRLINE_FIX_FACTOR} h${width} v${height + HAIRLINE_FIX_FACTOR} h${-width} Z`
|
|
50
|
+
return svg.replace(pathData, `${pathData} ${frame}`).replace(pathTag, `${pathTag} fill-rule="evenodd"`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The SVG with its id set, colored, inverted when asked, and sized to fill the layer; Thunderbolt's `getBackgroundProps`.
|
|
54
|
+
* Undefined for an SVG with no box to size it by. */
|
|
55
|
+
const dividerSvg = (svg, { id, color, invert, aspectRatio }) => {
|
|
56
|
+
const info = svgInfo(svg)
|
|
57
|
+
const [x, y, width, height] = viewBoxOf(info).trim().split(/[\s,]+/).map(Number.parseFloat)
|
|
58
|
+
if (![x, y, width, height].every(Number.isFinite)) return undefined
|
|
59
|
+
let content = scaleSvg(svg, { ...info, viewBox: `${x} ${y} ${width} ${height}`, bbox: `${x} ${y} ${width} ${height}` }, aspectRatio)
|
|
60
|
+
.replace(/^(.*\s*)<svg/, '<svg')
|
|
61
|
+
.replace(/<\/svg>(\s*)$/, '</svg>')
|
|
62
|
+
if (invert) content = invertSvg(content, x, y, width, height)
|
|
63
|
+
const idMatch = content.match(/<svg[^>]*>/)[0].match(/id="[-\w]+"/)
|
|
64
|
+
content = idMatch ? content.replace(idMatch[0], `id="${id}"`) : content.replace('<svg ', `<svg id="${id}" `)
|
|
65
|
+
const inverting = invert ? `#${id} > g { transform: scaleY(-1) translateY(${-(2 * y + height)}px); } ` : ''
|
|
66
|
+
const coloring = info.type === 'color' && !/data-color="2"/.test(svg) ? `, #${id} [data-color="1"] ` : ''
|
|
67
|
+
content = content.replace(/(<svg[^>]*>)/, `$1<defs><style>${inverting}#${id}${coloring} { fill: ${color}; }</style></defs>`)
|
|
68
|
+
content = content
|
|
69
|
+
.replace(/(<svg[^>]*\s)width="[^"]*"/, '$1width="100%"')
|
|
70
|
+
.replace(/(<svg[^>]*\s)height="[^"]*"/, '$1height="100%"')
|
|
71
|
+
if (!/<svg[^>]*\swidth="/.test(content)) content = content.replace(/(<svg[^>]*)(>)/, `$1 width="${width.toFixed(2)}"$2`)
|
|
72
|
+
if (!/<svg[^>]*\sheight="/.test(content)) content = content.replace(/(<svg[^>]*)(>)/, `$1 height="${height.toFixed(2)}"$2`)
|
|
73
|
+
return { image: svgDataUrl(content), width, height }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The `[data-divider-layer]` declarations for an instance's vars, or undefined when its SVG isn't loaded or has no box. */
|
|
77
|
+
const layerDeclarations = (compId, vars) => {
|
|
78
|
+
const svgId = svgIdOf(vars['--svgId'] ?? '')
|
|
79
|
+
if (vars['--svgId'] !== undefined && svgId === '') return { 'background-image': 'none' }
|
|
80
|
+
const svg = svgId && loadedSvg(svgId)
|
|
81
|
+
if (!svg) return undefined
|
|
82
|
+
const side = vars['--side'] === 'top' ? 'top' : 'bottom'
|
|
83
|
+
const keepAspectRatio = vars['--keepAspectRatio'] !== 'stretch'
|
|
84
|
+
const repeats = vars['--repeats'] === undefined ? 1 : Number.parseInt(vars['--repeats'], 10)
|
|
85
|
+
const drawn = dividerSvg(svg, {
|
|
86
|
+
id: `${compId}-${side}`,
|
|
87
|
+
color: vars['--color'] || '#000',
|
|
88
|
+
invert: vars['--invert'] === 'invert',
|
|
89
|
+
aspectRatio: keepAspectRatio ? (repeats !== 0 ? 'xMinYMax meet' : 'xMidYMax slice') : 'none',
|
|
90
|
+
})
|
|
91
|
+
if (drawn === undefined) return undefined
|
|
92
|
+
const { image, width, height } = drawn
|
|
93
|
+
const size = keepAspectRatio ? `calc(${width} * var(--height) / ${height})` : `${((1 / (repeats + 1)) * 100).toFixed(2)}%`
|
|
94
|
+
const pinFactor = { out: '1', in: '-1' }[vars['--layerPinFactor']] ?? '0'
|
|
95
|
+
return {
|
|
96
|
+
'background-image': image,
|
|
97
|
+
'background-size': `${size} 100%`,
|
|
98
|
+
...(side === 'top' ? { top: '0', '--scaleY': '-1', '--side-flip': '-1' } : { bottom: '0', '--scaleY': '1', '--side-flip': '1' }),
|
|
99
|
+
'--divider-flip': vars['--flip'] === 'flip' ? '-1' : '1',
|
|
100
|
+
'--divider-layers-pin-factor': pinFactor,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @type {import('../transform/types.js').CssTransformPlugin} */
|
|
105
|
+
export const shapeDividersPlugin = {
|
|
106
|
+
name: 'shape-dividers',
|
|
107
|
+
// Last, with wix-vector-urls: it only adds rules, which no other plugin needs to see, and it reads only `--height` as
|
|
108
|
+
// `var(--height)`, so spx converts that var's `--spx()` where it is declared.
|
|
109
|
+
order: 25,
|
|
110
|
+
prepare(root) {
|
|
111
|
+
const ids = new Set()
|
|
112
|
+
root.walkDecls('--svgId', declaration => {
|
|
113
|
+
const id = svgIdOf(declaration.value)
|
|
114
|
+
if (id) ids.add(id)
|
|
115
|
+
})
|
|
116
|
+
return loadSvgs(ids)
|
|
117
|
+
},
|
|
118
|
+
transform(root, context) {
|
|
119
|
+
const dividerIds = dividerIdsOf(context.filePath, context)
|
|
120
|
+
if (!dividerIds.size) return
|
|
121
|
+
const rules = instanceRules(root, dividerIds)
|
|
122
|
+
const baseVars = new Map()
|
|
123
|
+
for (const { id, vars, base } of rules) if (base) baseVars.set(id, { ...baseVars.get(id), ...vars })
|
|
124
|
+
for (const { rule, id, vars } of rules) {
|
|
125
|
+
const allVars = { ...baseVars.get(id), ...vars }
|
|
126
|
+
const declarations = layerDeclarations(id, allVars)
|
|
127
|
+
if (declarations === undefined) {
|
|
128
|
+
if (allVars['--svgId']) context.warn?.(rule, `Couldn't draw the shape divider's SVG ${allVars['--svgId']}: it didn't load from Wix's CDN, or has no view box`)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
const layerRule = postcss.rule({ selector: `${rule.selector} ${LAYER}`, source: rule.source })
|
|
132
|
+
for (const [prop, value] of Object.entries(declarations)) layerRule.append({ prop, value, source: rule.source })
|
|
133
|
+
rule.after(layerRule)
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
}
|
|
@@ -29,13 +29,21 @@ styles onto their runtime wrappers; this plugin then converts every `--spx(numbe
|
|
|
29
29
|
In scale mode, the size is approximately:
|
|
30
30
|
|
|
31
31
|
```text
|
|
32
|
-
number × (scaling-factor − scrollbar-width) / spx-ref-width
|
|
32
|
+
number × (clamp(spx-stopper-min, scaling-factor, min(spx-stopper-max, content-max-width)) − scrollbar-width) / spx-ref-width
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
This matches DM's `--spx()` in the `theme.css` preamble.
|
|
36
|
+
|
|
37
|
+
- **Stoppers:** DM writes them inside `@layer spx-preamble`, so the viewer's unlayered global CSS
|
|
38
|
+
values (`0px` / `9999px`) win.
|
|
39
|
+
- **Cap:** `--content-max-width`, set by DM on each section's content wrapper and inherited by its
|
|
40
|
+
descendants. With none above, the cap is 99999px. A `--content-max-width` that uses `--spx()` is
|
|
41
|
+
compiled without the cap, since a custom property can't read itself.
|
|
42
|
+
|
|
35
43
|
Nonzero scaled values have a minimum magnitude of 0.5px, preserving their sign.
|
|
36
44
|
In fixed mode, the number is a pixel value, including fractional pixels. Zero stays zero.
|
|
37
45
|
|
|
38
|
-
The plugin keeps reference width, scaling factor
|
|
46
|
+
The plugin keeps reference width, scaling factor, scrollbar width and stoppers as `var()` reads
|
|
39
47
|
inside each expression. It translates `--spx-resolver-type: scale` to a private numeric
|
|
40
48
|
switch of 1, and `fixed` to 0. That switch selects scaled lengths or fixed pixels using
|
|
41
49
|
ordinary CSS arithmetic, without requiring CSS custom functions or style queries.
|
|
@@ -53,9 +61,9 @@ own effective settings. No per-container SPX reset is needed.
|
|
|
53
61
|
|
|
54
62
|
Existing preamble declarations remain in CSS and participate in the normal cascade.
|
|
55
63
|
When settings are absent, expressions fall back to reference width 1280, scale mode,
|
|
56
|
-
viewport width 100vw
|
|
57
|
-
they override them. As with any CSS custom property, an authored
|
|
58
|
-
SPX expression resolves where the
|
|
64
|
+
viewport width 100vw, scrollbar width 0px, stoppers 0px / 99999px and no cap. Nested components inherit settings unless
|
|
65
|
+
they override them. As with any CSS custom property, an authored variable containing an
|
|
66
|
+
SPX expression resolves where the variable is declared; inheriting the variable does not
|
|
59
67
|
re-evaluate it using a child's settings.
|
|
60
68
|
|
|
61
69
|
## Files and boundaries
|
|
@@ -65,9 +73,8 @@ re-evaluate it using a child's settings.
|
|
|
65
73
|
- `calls.js` parses literal numeric SPX calls and rejects unsupported arguments.
|
|
66
74
|
|
|
67
75
|
Reference widths must be positive numbers and resolver modes must be `scale` or `fixed`.
|
|
68
|
-
|
|
69
|
-
`--scaling-factor` supplied by CSS and does not generate Thunderbolt's clamp formula.
|
|
76
|
+
Stoppers and `--content-max-width` are not validated.
|
|
70
77
|
|
|
71
78
|
`test/spx.browser.mjs` checks computed styles for the button example, nested overrides,
|
|
72
|
-
settings in separate stylesheets, cascade priority, fixed fractions, signs and
|
|
79
|
+
settings in separate stylesheets, cascade priority, fixed fractions, signs, zero, stoppers and cap.
|
|
73
80
|
Run it together with the container browser checks using the package's `test:browser` script.
|
|
@@ -3,8 +3,12 @@ import { lowerCalls } from './calls.js'
|
|
|
3
3
|
// Keep arithmetic at the call site: an inherited derived custom property would already
|
|
4
4
|
// be computed against its parent's settings. Only the numeric resolver switch inherits.
|
|
5
5
|
const SCALE = 'var(--origami-spx-scale, 1)'
|
|
6
|
-
const UNIT = `(${SCALE} * (var(--scaling-factor, 100vw) - var(--scrollbar-width, 0px)) / var(--spx-ref-width, 1280) + (1 - ${SCALE}) * 1px)`
|
|
7
6
|
const FLOOR = `calc(0.5px * ${SCALE})`
|
|
7
|
+
/** @param {string} cap */
|
|
8
|
+
const unitCappedAt = cap =>
|
|
9
|
+
`(${SCALE} * (clamp(var(--spx-stopper-min, 0px), var(--scaling-factor, 100vw), min(var(--spx-stopper-max, 99999px), ${cap})) - var(--scrollbar-width, 0px)) / var(--spx-ref-width, 1280) + (1 - ${SCALE}) * 1px)`
|
|
10
|
+
const UNIT = unitCappedAt('var(--content-max-width, 99999px)')
|
|
11
|
+
const UNCAPPED_UNIT = unitCappedAt('99999px')
|
|
8
12
|
|
|
9
13
|
/** Compile settings and calls in place, retaining their cascade and source locations.
|
|
10
14
|
* @param {import('postcss').Declaration} declaration
|
|
@@ -30,11 +34,12 @@ export const compileCascadeDeclaration = declaration => {
|
|
|
30
34
|
) {
|
|
31
35
|
throw declaration.error('SPX reference width must be positive')
|
|
32
36
|
}
|
|
37
|
+
const unit = declaration.prop === '--content-max-width' ? UNCAPPED_UNIT : UNIT
|
|
33
38
|
lowerCalls(
|
|
34
39
|
declaration,
|
|
35
40
|
number => {
|
|
36
41
|
if (number === 0) return '0px'
|
|
37
|
-
return number < 0 ? `min(calc(-1 * ${FLOOR}), ${number} * ${
|
|
42
|
+
return number < 0 ? `min(calc(-1 * ${FLOOR}), ${number} * ${unit})` : `max(${FLOOR}, ${number} * ${unit})`
|
|
38
43
|
},
|
|
39
44
|
() => declaration.error('SPX calls --spx() with a non-literal argument')
|
|
40
45
|
)
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs'
|
|
6
6
|
import { basename, dirname, join } from 'node:path'
|
|
7
7
|
import { parseSync } from '@babel/core'
|
|
8
|
+
import { WRAPPERS_SCOPE } from '../wrapperImport.js'
|
|
8
9
|
|
|
9
10
|
const STRUCTURE_FILES = ['structure.tsx', 'structure.jsx']
|
|
10
11
|
const GENERATED = '_generated'
|
|
12
|
+
// `@origami-wrappers/<module>/<symbol>`, and nothing that could resolve outside its package.
|
|
13
|
+
const WRAPPER_IMPORT = new RegExp(`^${WRAPPERS_SCOPE}/[\\w-]+/[\\w-]+$`)
|
|
11
14
|
|
|
12
15
|
/** The structure file `stylesheetPath` is checked against, or undefined when its item has none. */
|
|
13
16
|
export const structureFileOf = stylesheetPath => {
|
|
@@ -41,3 +44,21 @@ export const literalIdOf = openingElement =>
|
|
|
41
44
|
attribute =>
|
|
42
45
|
attribute.type === 'JSXAttribute' && attribute.name.name === 'id' && attribute.value?.type === 'StringLiteral'
|
|
43
46
|
)?.value.value
|
|
47
|
+
|
|
48
|
+
/** Id → the wrapper import it is rendered from, for elements with a literal `id` and an imported wrapper tag. */
|
|
49
|
+
export const importsById = structurePath => {
|
|
50
|
+
const program = parseStructure(structurePath)
|
|
51
|
+
const sources = new Map()
|
|
52
|
+
for (const node of program?.body ?? []) {
|
|
53
|
+
if (node.type !== 'ImportDeclaration' || !WRAPPER_IMPORT.test(node.source.value)) continue
|
|
54
|
+
for (const specifier of node.specifiers) sources.set(specifier.local.name, node.source.value)
|
|
55
|
+
}
|
|
56
|
+
const byId = new Map()
|
|
57
|
+
walk(program, node => {
|
|
58
|
+
if (node.type !== 'JSXOpeningElement' || node.name.type !== 'JSXIdentifier') return
|
|
59
|
+
const id = literalIdOf(node)
|
|
60
|
+
const source = sources.get(node.name.name)
|
|
61
|
+
if (id !== undefined && source !== undefined) byId.set(id, source)
|
|
62
|
+
})
|
|
63
|
+
return byId
|
|
64
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The vector art a site's CSS names by id (`0da768_67d39d1bdd9c48cc96409e0810c6144a.svg`): loading it from Wix's CDN,
|
|
2
|
+
// and the edits Thunderbolt makes before it inlines one as a data URI (thunderbolt-commons' `vectorImage`). A CSS
|
|
3
|
+
// plugin loads the ids it needs in `prepare`, then reads them with `loadedSvg` in its sync `transform`.
|
|
4
|
+
|
|
5
|
+
const SHAPES_URL = 'https://static.wixstatic.com/shapes/'
|
|
6
|
+
const FETCH_TIMEOUT_MS = 10_000
|
|
7
|
+
const SVG_TAG = /(<svg[^>]*)(>)/
|
|
8
|
+
|
|
9
|
+
/** id → the SVG's markup, or null when the CDN has no SVG under it. Vector art never changes under its id. A load
|
|
10
|
+
* the network failed is tried again next run. */
|
|
11
|
+
const loaded = new Map()
|
|
12
|
+
/** id → its pending load. */
|
|
13
|
+
const loading = new Map()
|
|
14
|
+
|
|
15
|
+
/** The id as the CDN names the file, with its `.svg`. */
|
|
16
|
+
const svgFileName = id => (id.endsWith('.svg') ? id : `${id}.svg`)
|
|
17
|
+
|
|
18
|
+
const load = async id => {
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(SHAPES_URL + svgFileName(id), { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
|
|
21
|
+
const svg = response.ok ? await response.text() : ''
|
|
22
|
+
loaded.set(id, SVG_TAG.test(svg) ? svg : null)
|
|
23
|
+
} catch {
|
|
24
|
+
// Not loaded: the plugin warns and keeps the value as written.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Loads the SVGs of `ids` that aren't loaded yet. */
|
|
29
|
+
export const loadSvgs = ids => {
|
|
30
|
+
const pending = [...ids]
|
|
31
|
+
.filter(id => !loaded.has(id))
|
|
32
|
+
.map(id => {
|
|
33
|
+
if (!loading.has(id)) loading.set(id, load(id).finally(() => loading.delete(id)))
|
|
34
|
+
return loading.get(id)
|
|
35
|
+
})
|
|
36
|
+
return pending.length ? Promise.all(pending).then(() => undefined) : undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The SVG's markup, once `loadSvgs` loaded it. */
|
|
40
|
+
export const loadedSvg = id => loaded.get(id) ?? undefined
|
|
41
|
+
|
|
42
|
+
const VIEW_BOX = /(viewBox=")([^"]*)(")/i
|
|
43
|
+
const PRESERVE_ASPECT_RATIO = /(preserveAspectRatio=")([^"]*)(")/i
|
|
44
|
+
const attribute = (tag, name) => tag.match(new RegExp(`\\s${name}="([^"]*)"`))?.[1]
|
|
45
|
+
|
|
46
|
+
/** What Thunderbolt's svgParser reads from the `<svg>` tag: its type, view box and the drawing's box. */
|
|
47
|
+
export const svgInfo = svg => {
|
|
48
|
+
const tag = svg.match(SVG_TAG)?.[0] ?? ''
|
|
49
|
+
const type = attribute(tag, 'data-type') ?? 'shape'
|
|
50
|
+
const width = attribute(tag, 'width')
|
|
51
|
+
const height = attribute(tag, 'height')
|
|
52
|
+
const viewBox = attribute(tag, 'viewBox') ?? (type === 'ugc' && width && height ? `0 0 ${width} ${height}` : '')
|
|
53
|
+
return { type, viewBox, bbox: attribute(tag, 'data-bbox') ?? '' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The box the art is drawn in: the drawing's own box, except for an uploaded SVG (ugc). */
|
|
57
|
+
export const viewBoxOf = ({ type, viewBox, bbox }) => (type !== 'ugc' && bbox ? bbox : viewBox)
|
|
58
|
+
|
|
59
|
+
/** The SVG fit to the box `viewBoxOf` gives, with `preserveAspectRatio`; Thunderbolt's `getScaledSvgViewBox`. */
|
|
60
|
+
export const scaleSvg = (svg, info, preserveAspectRatio) => {
|
|
61
|
+
const tag = svg.match(SVG_TAG)?.[0]
|
|
62
|
+
if (tag === undefined) return svg
|
|
63
|
+
svg = PRESERVE_ASPECT_RATIO.test(tag)
|
|
64
|
+
? svg.replace(tag, tag.replace(PRESERVE_ASPECT_RATIO, `$1${preserveAspectRatio}$3`))
|
|
65
|
+
: svg.replace(/<svg/, `<svg preserveAspectRatio="${preserveAspectRatio}"`)
|
|
66
|
+
if (!VIEW_BOX.test(tag)) {
|
|
67
|
+
if (info.type === 'ugc' && info.viewBox) svg = svg.replace(/<svg/, `<svg viewBox="${info.viewBox}"`)
|
|
68
|
+
if (info.type !== 'ugc' && info.bbox) svg = svg.replace(/<svg/, `<svg viewBox="${info.bbox}"`)
|
|
69
|
+
}
|
|
70
|
+
if (info.type !== 'ugc' && info.bbox) {
|
|
71
|
+
const scaledTag = svg.match(SVG_TAG)[0]
|
|
72
|
+
svg = svg.replace(scaledTag, scaledTag.replace(VIEW_BOX, `$1${info.bbox}$3`))
|
|
73
|
+
}
|
|
74
|
+
return svg
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const URL_SYMBOLS = /[\r\n%#()<>?[\\\]^`{|}]/g
|
|
78
|
+
|
|
79
|
+
/** The SVG as a CSS `url()` of a data URI, encoded as Thunderbolt's `encodeSVG` does. */
|
|
80
|
+
export const svgDataUrl = svg => {
|
|
81
|
+
const escaped = svg
|
|
82
|
+
.replace(/"/g, "'")
|
|
83
|
+
.replace(/>\s+</g, '><')
|
|
84
|
+
.replace(/\s{2,}/g, ' ')
|
|
85
|
+
.replace(URL_SYMBOLS, encodeURIComponent)
|
|
86
|
+
return `url("data:image/svg+xml,${escaped}")`
|
|
87
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Inlines the vector art a value names with Wix's own url scheme, `url(wix:vector://v1/<id>.svg)`, which no browser
|
|
2
|
+
// can load: Image's `--maskImage`, for one. A live Wix site writes it as a data URI of the SVG stretched to its
|
|
3
|
+
// drawing's box (Thunderbolt's `replaceWixUrl` and `createSvgCssVar`); this writes the same data URI in its place.
|
|
4
|
+
// An SVG that can't be loaded stays as written, with a warning.
|
|
5
|
+
|
|
6
|
+
import { loadedSvg, loadSvgs, scaleSvg, svgDataUrl, svgInfo } from './svg.js'
|
|
7
|
+
|
|
8
|
+
const WIX_VECTOR_URL = /url\(\s*(['"]?)wix:vector:\/\/v1\/([^'")\s]+)\1\s*\)/gi
|
|
9
|
+
const HAS_WIX_VECTOR_URL = /wix:vector:\/\//i
|
|
10
|
+
|
|
11
|
+
/** The ids every `wix:vector` url in the stylesheet names. */
|
|
12
|
+
const idsIn = root => {
|
|
13
|
+
const ids = new Set()
|
|
14
|
+
root.walkDecls(declaration => {
|
|
15
|
+
for (const [, , id] of declaration.value.matchAll(WIX_VECTOR_URL)) ids.add(id)
|
|
16
|
+
})
|
|
17
|
+
return ids
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @type {import('../transform/types.js').CssTransformPlugin} */
|
|
21
|
+
export const wixVectorUrlsPlugin = {
|
|
22
|
+
name: 'wix-vector-urls',
|
|
23
|
+
// Last, so a url a mixin or preset copies elsewhere is inlined wherever it ends up.
|
|
24
|
+
order: 25,
|
|
25
|
+
prepare: root => loadSvgs(idsIn(root)),
|
|
26
|
+
transform(root, context) {
|
|
27
|
+
root.walkDecls(declaration => {
|
|
28
|
+
if (!HAS_WIX_VECTOR_URL.test(declaration.value)) return
|
|
29
|
+
declaration.value = declaration.value.replace(WIX_VECTOR_URL, (url, _quote, id) => {
|
|
30
|
+
const svg = loadedSvg(id)
|
|
31
|
+
if (svg === undefined) {
|
|
32
|
+
context.warn?.(declaration, `Couldn't load the SVG ${id} from Wix's CDN, so ${url} stays as written`)
|
|
33
|
+
return url
|
|
34
|
+
}
|
|
35
|
+
return svgDataUrl(scaleSvg(svg, svgInfo(svg), 'none'))
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// depends on it in another arrive in the same document.
|
|
10
10
|
|
|
11
11
|
import { removeCustomEnums } from '../../cssPlugins/customEnums.js'
|
|
12
|
-
import { runCssPlugins } from './runPlugins.js'
|
|
12
|
+
import { pluginError, runCssPlugins } from './runPlugins.js'
|
|
13
13
|
|
|
14
14
|
const DEPENDENCY_DIRECTORY = 'node_modules/'
|
|
15
15
|
|
|
@@ -55,12 +55,28 @@ export const createCssPostcssPlugin = ({ selectPlugins, contextFor, failures })
|
|
|
55
55
|
// A plugin reports a problem that need not stop the build as a PostCSS warning, which Vite prints with the
|
|
56
56
|
// node's code frame.
|
|
57
57
|
const warn = (node, text) => node.warn(result, text)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
failures?.delete(from)
|
|
61
|
-
} catch (error) {
|
|
58
|
+
const context = { ...fileContext, addDependency, warn }
|
|
59
|
+
const fail = error => {
|
|
62
60
|
failures?.set(from, declared)
|
|
63
61
|
throw error
|
|
64
62
|
}
|
|
63
|
+
const run = () => {
|
|
64
|
+
try {
|
|
65
|
+
runCssPlugins({ plugins, root, context })
|
|
66
|
+
failures?.delete(from)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
fail(error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// PostCSS awaits the hook only when it returns a promise; a stylesheet nothing loads for stays synchronous.
|
|
72
|
+
const loading = plugins.flatMap(plugin => {
|
|
73
|
+
try {
|
|
74
|
+
const prepared = plugin.prepare?.(root, context)
|
|
75
|
+
return prepared ? [prepared.catch(cause => fail(pluginError(plugin, context, cause)))] : []
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
return fail(pluginError(plugin, context, cause))
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
return loading.length ? Promise.all(loading).then(run) : run()
|
|
65
81
|
},
|
|
66
82
|
})
|
|
@@ -9,6 +9,14 @@ import { LayoutError } from '../layoutError.js'
|
|
|
9
9
|
|
|
10
10
|
const reason = cause => (cause instanceof Error ? cause.message : String(cause))
|
|
11
11
|
|
|
12
|
+
/** A plugin's failure, in `prepare` or a phase, attributed to the plugin and the file. */
|
|
13
|
+
export const pluginError = (plugin, context, cause) =>
|
|
14
|
+
// Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
|
|
15
|
+
// plugin may throw any value, so it is read without assuming an Error.
|
|
16
|
+
new Error(`CSS transform "${plugin.name}" failed for ${context.relativePath ?? context.filePath}: ${reason(cause)}`, {
|
|
17
|
+
cause,
|
|
18
|
+
})
|
|
19
|
+
|
|
12
20
|
export const runCssPlugins = ({ plugins, root, context }) => {
|
|
13
21
|
/** @type {LayoutError[]} */
|
|
14
22
|
const layoutErrors = []
|
|
@@ -21,12 +29,7 @@ export const runCssPlugins = ({ plugins, root, context }) => {
|
|
|
21
29
|
layoutErrors.push(cause)
|
|
22
30
|
continue
|
|
23
31
|
}
|
|
24
|
-
|
|
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
|
-
)
|
|
32
|
+
throw pluginError(plugin, context, cause)
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
}
|
package/src/transform/types.js
CHANGED
|
@@ -36,9 +36,13 @@
|
|
|
36
36
|
* and applied before any of them run.
|
|
37
37
|
*
|
|
38
38
|
* @typedef {TransformPluginBase & {
|
|
39
|
+
* prepare?: (root: import('postcss').Root, context: CssTransformContext) => Promise<void> | void,
|
|
39
40
|
* validate?: (root: import('postcss').Root, context: CssTransformContext) => void,
|
|
40
41
|
* transform: (root: import('postcss').Root, context: CssTransformContext) => void
|
|
41
42
|
* }} CssTransformPlugin
|
|
43
|
+
* `prepare` loads what the plugin needs that can't be read synchronously, such as a file from a CDN, before any
|
|
44
|
+
* plugin's `validate` or `transform` runs. It returns a promise only when it has something to load, so a stylesheet
|
|
45
|
+
* that needs nothing loaded stays synchronous for PostCSS.
|
|
42
46
|
*
|
|
43
47
|
* @typedef {TransformContext & {
|
|
44
48
|
* addDependency?: (file: string) => void,
|