@wix/origami-transforms 0.1.6 → 0.1.8

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.
@@ -0,0 +1,4 @@
1
+ export function structureFileOf(stylesheetPath: any): string | undefined;
2
+ export function parseStructure(structurePath: any): any;
3
+ export function walk(node: any, visit: any): void;
4
+ export function literalIdOf(openingElement: any): any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-transforms",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Babel and PostCSS checks and transforms for Wix origami page files.",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -54,5 +54,5 @@
54
54
  },
55
55
  "type": "module",
56
56
  "types": "./dist/index.d.ts",
57
- "falconPackageHash": "104bb1dbbdebc96c665a8cdf46acdfeb8ceef46bd8a7fcbd1ff2d10b"
57
+ "falconPackageHash": "09b1c7aec92977ffa2db7c85a87daf406d7628c92600cbe85ed3ef94"
58
58
  }
@@ -4,18 +4,16 @@
4
4
  // `customEnums.ts`), which the page imports and the bundler inlines. The blocks are removed from the output.
5
5
  // Native `if()` can't do it: Firefox doesn't have it, and a var named like its key refers to itself.
6
6
  //
7
- // The instance's component comes from the item's structure file, as in stackChildren.js: the element with
7
+ // The instance's component comes from the item's structure file (structureFile.js): the element with
8
8
  // `id="comp-1"`, and the `@origami-wrappers/…` import it is rendered from. See customEnums.md.
9
9
 
10
- import { existsSync, readFileSync, realpathSync } from 'node:fs'
10
+ import { realpathSync } from 'node:fs'
11
11
  import { createRequire } from 'node:module'
12
- import { basename, dirname, join } from 'node:path'
13
- import { parseSync } from '@babel/core'
12
+ import { basename, dirname } from 'node:path'
14
13
  import postcss from 'postcss'
15
14
  import { LayoutError } from '../transform/layoutError.js'
15
+ import { literalIdOf, parseStructure, structureFileOf, walk } from './structureFile.js'
16
16
 
17
- const STRUCTURE_FILES = ['structure.tsx', 'structure.jsx']
18
- const GENERATED = '_generated'
19
17
  // `@origami-wrappers/<module>/<symbol>`, and nothing that could resolve outside its package.
20
18
  const WRAPPER_IMPORT = /^@origami-wrappers\/[\w-]+\/[\w-]+$/
21
19
  // `#id` or `.id` (exports give an instance both), then only what narrows that same element: classes,
@@ -23,44 +21,21 @@ const WRAPPER_IMPORT = /^@origami-wrappers\/[\w-]+\/[\w-]+$/
23
21
  // and no pseudo-elements, which the component doesn't read vars from.
24
22
  const INSTANCE = /^[#.]([\w-]+)(?!.*::)(?:[.:[][^\s>+~]*)?((?:\s+[^\s>+~]+)*)$/
25
23
 
26
- /** The folder holding the structure file a stylesheet is checked against: its own, or its item's for `_generated/`. */
27
- const structureFolderOf = stylesheetPath => {
28
- const folder = dirname(stylesheetPath)
29
- return basename(folder) === GENERATED ? dirname(folder) : folder
30
- }
31
-
32
24
  /** Id → the wrapper import it is rendered from, for elements with a literal `id` and an imported wrapper tag. */
33
25
  const importsById = structurePath => {
34
- const ast = parseSync(readFileSync(structurePath, 'utf8'), {
35
- babelrc: false,
36
- configFile: false,
37
- filename: structurePath,
38
- parserOpts: { plugins: structurePath.endsWith('.tsx') ? ['jsx', 'typescript'] : ['jsx'] },
39
- })
26
+ const program = parseStructure(structurePath)
40
27
  const sources = new Map()
41
- for (const node of ast?.program.body ?? []) {
28
+ for (const node of program?.body ?? []) {
42
29
  if (node.type !== 'ImportDeclaration' || !WRAPPER_IMPORT.test(node.source.value)) continue
43
30
  for (const specifier of node.specifiers) sources.set(specifier.local.name, node.source.value)
44
31
  }
45
32
  const byId = new Map()
46
- const visit = node => {
47
- if (Array.isArray(node)) return node.forEach(visit)
48
- if (!node || typeof node.type !== 'string') return
49
- if (node.type === 'JSXOpeningElement' && node.name.type === 'JSXIdentifier') {
50
- const id = node.attributes.find(
51
- attribute =>
52
- attribute.type === 'JSXAttribute' &&
53
- attribute.name.name === 'id' &&
54
- attribute.value?.type === 'StringLiteral'
55
- )?.value.value
56
- const source = sources.get(node.name.name)
57
- if (id !== undefined && source !== undefined) byId.set(id, source)
58
- }
59
- for (const [key, value] of Object.entries(node)) {
60
- if (key !== 'loc' && value && typeof value === 'object') visit(value)
61
- }
62
- }
63
- visit(ast?.program)
33
+ walk(program, node => {
34
+ if (node.type !== 'JSXOpeningElement' || node.name.type !== 'JSXIdentifier') return
35
+ const id = literalIdOf(node)
36
+ const source = sources.get(node.name.name)
37
+ if (id !== undefined && source !== undefined) byId.set(id, source)
38
+ })
64
39
  return byId
65
40
  }
66
41
 
@@ -221,9 +196,7 @@ export const customEnumsPlugin = {
221
196
  transform(root, context) {
222
197
  const { byFolder, blocks, malformed } = definitionsIn(root)
223
198
  if (blocks.length === 0) return
224
- const structurePath = STRUCTURE_FILES.map(file => join(structureFolderOf(context.filePath), file)).find(
225
- existsSync
226
- )
199
+ const structurePath = structureFileOf(context.filePath)
227
200
  if (structurePath !== undefined) context.addDependency?.(structurePath)
228
201
  const instances = structurePath === undefined ? new Map() : enumsById(structurePath, byFolder)
229
202
  const keys = new Set([...instances.values()].flatMap(enums => enums.map(({ key }) => key)))
@@ -1,21 +1,19 @@
1
1
  // Checks the CSS of a stack's direct children (rule S2-STACK-001). It changes nothing: every violation in the
2
2
  // stylesheet is collected, and the build fails once with all of them.
3
3
  //
4
- // A stack is a parent whose rule declares `--container-layout-type: StackContainerLayout`; its
5
- // `flex-direction` says which way it stacks (column → vertical, anything else → horizontal, as in
6
- // CSS). Parent and child are the JSX tree's, so the tree comes from the item's `structure.tsx` (or `.jsx`): next
7
- // to the stylesheet, or one folder up when the stylesheet is the generated one in `_generated/`. Elements and
8
- // rules meet on `id` and `#id`. The stylesheet is the page's, with its
9
- // carriers already inlined (see ../transform/css/postcssPlugin.js), so every rule is in view.
10
-
11
- import { existsSync, readFileSync } from 'node:fs'
12
- import { basename, dirname, join } from 'node:path'
13
- import { parseSync } from '@babel/core'
4
+ // A stack is a parent laid out with a non-wrapping flex, as DM derives it; an internal container writes its
5
+ // layout in its `@origami-target internal-container-content-wrapper` block. `flex-direction` says which way
6
+ // it stacks (column → vertical, anything else → horizontal, as in CSS).
7
+ //
8
+ // Parent and child are the JSX tree's, so the tree comes from the item's structure file (see
9
+ // structureFile.js). The stylesheet is the page's, with its carriers already inlined (see
10
+ // ../transform/css/postcssPlugin.js), so every rule is in view.
11
+
12
+ import { basename } from 'node:path'
14
13
  import { LayoutError } from '../transform/layoutError.js'
14
+ import { literalIdOf, parseStructure, structureFileOf, walk } from './structureFile.js'
15
15
 
16
- const STRUCTURE_FILES = ['structure.tsx', 'structure.jsx']
17
- const GENERATED = '_generated'
18
- const STACK_CONTAINER = 'StackContainerLayout'
16
+ const CONTENT_WRAPPER = 'internal-container-content-wrapper'
19
17
  const BASE = ''
20
18
 
21
19
  // What a stack child may write, per direction. `0` is allowed wherever a length is.
@@ -56,46 +54,21 @@ const lengthOf = token => {
56
54
  return UNIT.exec(token)?.[1].toLowerCase()
57
55
  }
58
56
 
59
- /** The folder holding the structure file a stylesheet is checked against: its own, or its item's for `_generated/`. */
60
- const structureFolderOf = stylesheetPath => {
61
- const folder = dirname(stylesheetPath)
62
- return basename(folder) === GENERATED ? dirname(folder) : folder
63
- }
64
-
65
57
  /** Child id → its direct parent's id, for elements with a literal `id`. Slot props are not children. */
66
58
  const parentsIn = structurePath => {
67
- const ast = parseSync(readFileSync(structurePath, 'utf8'), {
68
- babelrc: false,
69
- configFile: false,
70
- filename: structurePath,
71
- parserOpts: { plugins: structurePath.endsWith('.tsx') ? ['jsx', 'typescript'] : ['jsx'] },
72
- })
73
59
  const parents = new Map()
74
- const idOf = element => {
75
- const attribute = element.openingElement.attributes.find(
76
- node => node.type === 'JSXAttribute' && node.name.name === 'id' && node.value?.type === 'StringLiteral'
77
- )
78
- return attribute?.value.value
79
- }
80
60
  const directChildren = node =>
81
61
  node.children.flatMap(child =>
82
62
  child.type === 'JSXElement' ? [child] : child.type === 'JSXFragment' ? directChildren(child) : []
83
63
  )
84
- const visit = node => {
85
- if (Array.isArray(node)) return node.forEach(visit)
86
- if (!node || typeof node.type !== 'string') return
87
- if (node.type === 'JSXElement') {
88
- const parent = idOf(node)
89
- for (const child of directChildren(node)) {
90
- const id = idOf(child)
91
- if (parent !== undefined && id !== undefined) parents.set(id, parent)
92
- }
64
+ walk(parseStructure(structurePath), node => {
65
+ if (node.type !== 'JSXElement') return
66
+ const parent = literalIdOf(node.openingElement)
67
+ for (const child of directChildren(node)) {
68
+ const id = literalIdOf(child.openingElement)
69
+ if (parent !== undefined && id !== undefined) parents.set(id, parent)
93
70
  }
94
- for (const [key, value] of Object.entries(node)) {
95
- if (key !== 'loc' && value && typeof value === 'object') visit(value)
96
- }
97
- }
98
- visit(ast?.program)
71
+ })
99
72
  return parents
100
73
  }
101
74
 
@@ -108,38 +81,63 @@ const conditionOf = rule => {
108
81
  return conditions.join(' / ')
109
82
  }
110
83
 
111
- /** Id → condition → the declarations that rules selecting exactly `#id` make there, in source order. */
112
- const declarationsById = root => {
84
+ // Exports give an element both `#id` and `.id`, and write its CSS on the class.
85
+ const OWN_SELECTOR = /^([#.])([\w-]+)$/
86
+
87
+ const isContentWrapper = node =>
88
+ node.type === 'atrule' && node.name === 'origami-target' && node.params.trim() === CONTENT_WRAPPER
89
+
90
+ /** Id → condition → `.`/`#` → declarations, in source order. With `content`, a content wrapper block counts. */
91
+ const declarationsById = (root, { content = false } = {}) => {
113
92
  const byId = new Map()
114
93
  root.walkRules(rule => {
115
- const ids = rule.selectors.map(selector => /^#([\w-]+)$/.exec(selector.trim())?.[1]).filter(Boolean)
116
- if (!ids.length) return
94
+ const own = rule.selectors.map(selector => OWN_SELECTOR.exec(selector.trim())).filter(Boolean)
95
+ if (!own.length) return
117
96
  const condition = conditionOf(rule)
118
- for (const id of ids) {
97
+ for (const [, kind, id] of own) {
119
98
  const byCondition = byId.get(id) ?? new Map()
120
99
  byId.set(id, byCondition)
121
- const declarations = byCondition.get(condition) ?? []
122
- byCondition.set(condition, declarations)
100
+ const byKind = byCondition.get(condition) ?? { '.': [], '#': [] }
101
+ byCondition.set(condition, byKind)
123
102
  rule.each(node => {
124
- if (node.type === 'decl') declarations.push(node)
103
+ if (node.type === 'decl') byKind[kind].push(node)
104
+ else if (content && isContentWrapper(node)) {
105
+ node.each(inner => {
106
+ if (inner.type === 'decl') byKind[kind].push(inner)
107
+ })
108
+ }
125
109
  })
126
110
  }
127
111
  })
128
112
  return byId
129
113
  }
130
114
 
131
- /** The declarations in effect under `condition`: the base ones, then the condition's own. */
115
+ /**
116
+ * The declarations in effect under `condition`: the base ones, then the condition's own; `#id` beats `.id`, and
117
+ * `!important` beats both.
118
+ */
132
119
  const effective = (byCondition, condition) => {
133
120
  const result = new Map()
134
- for (const key of condition === BASE ? [BASE] : [BASE, condition]) {
135
- for (const declaration of byCondition?.get(key) ?? []) result.set(declaration.prop, declaration)
121
+ for (const important of [false, true]) {
122
+ for (const kind of ['.', '#']) {
123
+ for (const key of condition === BASE ? [BASE] : [BASE, condition]) {
124
+ for (const declaration of byCondition?.get(key)?.[kind] ?? []) {
125
+ if (Boolean(declaration.important) === important) result.set(declaration.prop, declaration)
126
+ }
127
+ }
128
+ }
136
129
  }
137
130
  return result
138
131
  }
139
132
 
133
+ const selectorOf = (declaration, id) =>
134
+ declaration.parent.selectors.some(selector => selector.trim() === `#${id}`) ? `#${id}` : `.${id}`
135
+
140
136
  /** 'vertical' or 'horizontal' when these declarations make a stack, else undefined. */
141
137
  const directionOf = declarations => {
142
- if (declarations.get('--container-layout-type')?.value.trim() !== STACK_CONTAINER) return undefined
138
+ if (declarations.get('display')?.value.trim() !== 'flex') return undefined
139
+ const wrap = declarations.get('flex-wrap')?.value.trim()
140
+ if (wrap !== undefined && wrap !== 'nowrap') return undefined
143
141
  return /^column/.test(declarations.get('flex-direction')?.value.trim() ?? '') ? 'vertical' : 'horizontal'
144
142
  }
145
143
 
@@ -158,20 +156,19 @@ export const stackChildrenPlugin = {
158
156
  // After mixins copy their bodies in, before spx rewrites --spx() calls away.
159
157
  order: 12,
160
158
  transform(root, context) {
161
- const structurePath = STRUCTURE_FILES.map(file => join(structureFolderOf(context.filePath), file)).find(
162
- existsSync
163
- )
159
+ const structurePath = structureFileOf(context.filePath)
164
160
  if (structurePath === undefined) return
165
161
  context.addDependency?.(structurePath)
166
162
  const parents = parentsIn(structurePath)
167
163
  if (parents.size === 0) return
168
164
  const byId = declarationsById(root)
165
+ const layoutById = declarationsById(root, { content: true })
169
166
 
170
167
  const violations = new Set()
171
168
  let first
172
169
  for (const [child, parent] of parents) {
173
170
  const childRules = byId.get(child)
174
- const parentRules = byId.get(parent)
171
+ const parentRules = layoutById.get(parent)
175
172
  if (childRules === undefined || parentRules === undefined) continue
176
173
  for (const condition of new Set([BASE, ...parentRules.keys(), ...childRules.keys()])) {
177
174
  const direction = directionOf(effective(parentRules, condition))
@@ -187,8 +184,8 @@ export const stackChildrenPlugin = {
187
184
  if (!bad.length) continue
188
185
  first ??= declaration.source
189
186
  violations.add(
190
- `#${child}${where(declaration)}, a ${direction} stack child: ${declaration.prop} must be ` +
191
- `${or(allowed.map(unit => LABELS[unit]))}; found ${bad.join(' ')}.`
187
+ `${selectorOf(declaration, child)}${where(declaration)}, a ${direction} stack child: ` +
188
+ `${declaration.prop} must be ${or(allowed.map(unit => LABELS[unit]))}; found ${bad.join(' ')}.`
192
189
  )
193
190
  }
194
191
  }
@@ -0,0 +1,43 @@
1
+ // The item's structure file, for CSS plugins that check a stylesheet against its JSX tree: `structure.tsx` (or
2
+ // `.jsx`) next to the stylesheet, or one folder up when the stylesheet is the generated one in `_generated/`.
3
+ // Elements and rules meet on a literal `id` and `#id` (or `.id`).
4
+
5
+ import { existsSync, readFileSync } from 'node:fs'
6
+ import { basename, dirname, join } from 'node:path'
7
+ import { parseSync } from '@babel/core'
8
+
9
+ const STRUCTURE_FILES = ['structure.tsx', 'structure.jsx']
10
+ const GENERATED = '_generated'
11
+
12
+ /** The structure file `stylesheetPath` is checked against, or undefined when its item has none. */
13
+ export const structureFileOf = stylesheetPath => {
14
+ const folder = dirname(stylesheetPath)
15
+ const item = basename(folder) === GENERATED ? dirname(folder) : folder
16
+ return STRUCTURE_FILES.map(file => join(item, file)).find(existsSync)
17
+ }
18
+
19
+ /** The structure file's program. */
20
+ export const parseStructure = structurePath =>
21
+ parseSync(readFileSync(structurePath, 'utf8'), {
22
+ babelrc: false,
23
+ configFile: false,
24
+ filename: structurePath,
25
+ parserOpts: { plugins: structurePath.endsWith('.tsx') ? ['jsx', 'typescript'] : ['jsx'] },
26
+ })?.program
27
+
28
+ /** Calls `visit` on every node under `node`, parents first. */
29
+ export const walk = (node, visit) => {
30
+ if (Array.isArray(node)) return node.forEach(child => walk(child, visit))
31
+ if (!node || typeof node.type !== 'string') return
32
+ visit(node)
33
+ for (const [key, value] of Object.entries(node)) {
34
+ if (key !== 'loc' && value && typeof value === 'object') walk(value, visit)
35
+ }
36
+ }
37
+
38
+ /** A JSX opening element's `id`, when it is a string literal. */
39
+ export const literalIdOf = openingElement =>
40
+ openingElement.attributes.find(
41
+ attribute =>
42
+ attribute.type === 'JSXAttribute' && attribute.name.name === 'id' && attribute.value?.type === 'StringLiteral'
43
+ )?.value.value