@wix/origami-site-runtime 1.0.8 → 1.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-site-runtime",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Vite development and build helpers for wix-import static sites",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -19,6 +19,7 @@
19
19
  "@babel/types": "^7.25.2",
20
20
  "@wix/builder-error-pages": "^1.1.6",
21
21
  "@wix/builder-piler-components": "^1.6.7",
22
+ "@wix/origami-fonts": "1.0.0",
22
23
  "@wix/origami-runtime": "0.11.0",
23
24
  "postcss": "^8.5.3",
24
25
  "postcss-custom-media": "^12.0.1",
@@ -64,5 +65,5 @@
64
65
  "./default-error-404": "./src/default-error-404.jsx"
65
66
  },
66
67
  "type": "module",
67
- "falconPackageHash": "6816322c222999dcbe8c51a591cff934a894bfc9b63294e23c90b7a7"
68
+ "falconPackageHash": "641cce00719388c9e1fbc0fd6cab982b52a23c4daf02f37771b70615"
68
69
  }
@@ -0,0 +1,93 @@
1
+ // Checks the theme's font tokens (`--wst-heading-1-font: normal normal 700 --spx(88)/1em benzin`). It changes
2
+ // nothing. The editor reads the family at the end of each one as one Wix font id and fails the import otherwise, so
3
+ // the build fails, with every such token listed, unless the family is exactly the id of a font in the Wix font
4
+ // catalog bundled in @wix/origami-fonts: no quotes, commas or fallbacks, the same case. A legacy font, which the
5
+ // editor no longer offers, counts as not in the catalog. `origami search-font` finds the right id.
6
+
7
+ import { basename } from 'node:path'
8
+ import { fonts } from '@wix/origami-fonts'
9
+ import { LayoutError } from '../transform/layoutError.js'
10
+
11
+ const FONT_TOKEN = /^--wst-[\w-]+-font$/
12
+ // The size in a `font` shorthand, with its optional line height: the family follows it. The number's quantifiers
13
+ // never overlap, so a long run of digits that is not a size fails in linear time.
14
+ const SIZE = /^(?:--spx\(.*\)|calc\(.*\)|(?:\d+(?:\.\d*)?|\.\d+)(?:px|em|rem|%|vw|vh|pt)?)(?:\/\S+)?$/i
15
+ const FONT_IDS = new Set(fonts.filter(font => font.permissions !== 'legacy').map(font => font.id))
16
+
17
+ /** A value split on top-level whitespace, so `--spx(16)/1.3em` stays one token. */
18
+ const tokens = value => {
19
+ const result = []
20
+ let depth = 0
21
+ let current = ''
22
+ for (const char of value.trim()) {
23
+ if (char === '(') depth++
24
+ else if (char === ')') depth--
25
+ if (depth === 0 && /\s/.test(char)) {
26
+ if (current) result.push(current)
27
+ current = ''
28
+ } else current += char
29
+ }
30
+ if (current) result.push(current)
31
+ return result
32
+ }
33
+
34
+ /** A font shorthand's family as written; undefined for `var(…)`. */
35
+ const familyOf = value => {
36
+ if (/var\(/.test(value)) return undefined
37
+ const parts = tokens(value)
38
+ // The last size: the repo's TS lib (ES2022) has no findLastIndex.
39
+ let size = parts.length - 1
40
+ while (size >= 0 && !SIZE.test(parts[size])) size--
41
+ if (size === -1 || size === parts.length - 1) return undefined
42
+ return parts.slice(size + 1).join(' ')
43
+ }
44
+
45
+ /** ` (theme.css:12)`: the carrier the declaration was written in, which the bundler doesn't name. */
46
+ const where = declaration => {
47
+ const file = declaration.source?.input.file
48
+ const line = declaration.source?.start?.line
49
+ return file ? ` (${basename(file)}${line ? `:${line}` : ''})` : ''
50
+ }
51
+
52
+ /**
53
+ * Where `source` starts, as LayoutError's location, leaving out what it doesn't know.
54
+ * @param {import('postcss').Declaration['source']} source
55
+ */
56
+ const locationOf = source => {
57
+ /** @type {{ file?: string, line?: number, column?: number }} */
58
+ const location = {}
59
+ if (source?.input.file !== undefined) location.file = source.input.file
60
+ if (source?.start !== undefined) {
61
+ location.line = source.start.line
62
+ location.column = source.start.column
63
+ }
64
+ return location
65
+ }
66
+
67
+ /** @type {import('../transform/types.js').CssTransformPlugin} */
68
+ export const fontsPlugin = {
69
+ name: 'fonts',
70
+ // Only checks; it reads the tokens as authored, before spx rewrites the sizes the family follows.
71
+ order: 5,
72
+ transform(root) {
73
+ /** @type {import('postcss').Declaration[]} */
74
+ const declarations = []
75
+ root.walkDecls(FONT_TOKEN, declaration => {
76
+ declarations.push(declaration)
77
+ })
78
+ const violations = []
79
+ let first
80
+ for (const declaration of declarations) {
81
+ const family = familyOf(declaration.value)
82
+ if (family === undefined || FONT_IDS.has(family)) continue
83
+ first ??= declaration.source
84
+ violations.push(
85
+ `${declaration.prop}${where(declaration)} must name exactly one Wix font id from the catalog; found ` +
86
+ `${family}. Run \`origami search-font --font <name>\` for the id.`
87
+ )
88
+ }
89
+ if (violations.length) {
90
+ throw new LayoutError(violations, locationOf(first))
91
+ }
92
+ },
93
+ }
@@ -7,15 +7,23 @@
7
7
  // stage guarantees a plugin sees. The order matters once: mixin bodies are copied into their calling rules before
8
8
  // layout routes declarations to wrappers and SPX converts calls using the browser's active settings. stack-children
9
9
  // only checks, and runs between mixins and layout: it needs the mixins' declarations and the stack rules as authored,
10
- // before layout compiles them away.
10
+ // before layout compiles them away. fonts only checks the theme's font tokens, as authored.
11
11
  //
12
12
  // `@layer` needs no plugin: cascade layers have been in every browser since March 2022.
13
13
 
14
14
  import { customMediaPlugin } from './customMedia.js'
15
+ import { fontsPlugin } from './fonts.js'
15
16
  import { mixinsPlugin } from './mixins.js'
16
17
  import { layoutPlugin } from './layout/index.js'
17
18
  import { spxPlugin } from './spx/index.js'
18
19
  import { stackChildrenPlugin } from './stackChildren.js'
19
20
 
20
21
  /** @type {readonly import('../transform/types.js').CssTransformPlugin[]} */
21
- export const cssPlugins = Object.freeze([customMediaPlugin, mixinsPlugin, stackChildrenPlugin, layoutPlugin, spxPlugin])
22
+ export const cssPlugins = Object.freeze([
23
+ customMediaPlugin,
24
+ fontsPlugin,
25
+ mixinsPlugin,
26
+ stackChildrenPlugin,
27
+ layoutPlugin,
28
+ spxPlugin,
29
+ ])
@@ -1,18 +1,26 @@
1
1
  // Runs the selected CSS plugins over one shared Root, in order. Parsing, stringifying and source
2
2
  // maps belong to PostCSS, which drives this stage — the only thing added here is attributing a
3
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.
4
7
 
5
8
  import { LayoutError } from '../layoutError.js'
6
9
 
7
10
  const reason = cause => (cause instanceof Error ? cause.message : String(cause))
8
11
 
9
12
  export const runCssPlugins = ({ plugins, root, context }) => {
13
+ /** @type {LayoutError[]} */
14
+ const layoutErrors = []
10
15
  for (const phase of ['validate', 'transform']) {
11
16
  for (const plugin of plugins) {
12
17
  try {
13
18
  plugin[phase]?.(root, context)
14
19
  } catch (cause) {
15
- if (cause instanceof LayoutError) throw cause
20
+ if (cause instanceof LayoutError) {
21
+ layoutErrors.push(cause)
22
+ continue
23
+ }
16
24
  // Tooling such as Vite's error overlay prints only the message, so the reason goes in it as well. A
17
25
  // plugin may throw any value, so it is read without assuming an Error.
18
26
  throw new Error(
@@ -22,4 +30,11 @@ export const runCssPlugins = ({ plugins, root, context }) => {
22
30
  }
23
31
  }
24
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
+ }
25
40
  }
@@ -12,6 +12,8 @@ export class LayoutError extends Error {
12
12
  super(violations.length === 1 ? violations[0] : `\n ${violations.join('\n ')}`)
13
13
  this.name = 'LayoutError'
14
14
  this.stack = ''
15
+ this.violations = violations
16
+ this.location = location
15
17
  Object.assign(this, location)
16
18
  }
17
19
  }