@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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-transforms",
|
|
3
|
-
"version": "0.1.
|
|
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": "
|
|
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
|
|
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 {
|
|
10
|
+
import { realpathSync } from 'node:fs'
|
|
11
11
|
import { createRequire } from 'node:module'
|
|
12
|
-
import { basename, dirname
|
|
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
|
|
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
|
|
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
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
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
|
|
5
|
-
// `flex-direction` says which way
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import { basename
|
|
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
|
|
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
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
const
|
|
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
|
|
116
|
-
if (!
|
|
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
|
|
97
|
+
for (const [, kind, id] of own) {
|
|
119
98
|
const byCondition = byId.get(id) ?? new Map()
|
|
120
99
|
byId.set(id, byCondition)
|
|
121
|
-
const
|
|
122
|
-
byCondition.set(condition,
|
|
100
|
+
const byKind = byCondition.get(condition) ?? { '.': [], '#': [] }
|
|
101
|
+
byCondition.set(condition, byKind)
|
|
123
102
|
rule.each(node => {
|
|
124
|
-
if (node.type === 'decl')
|
|
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
|
-
/**
|
|
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
|
|
135
|
-
for (const
|
|
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('
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|