@wix/origami-lint 0.0.0-stage → 1.34.1

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 (51) hide show
  1. package/README.md +41 -2
  2. package/dist/history/changed-files.js +42 -0
  3. package/dist/history/index.js +3 -0
  4. package/dist/history/last-passing.js +44 -0
  5. package/dist/history/utils.js +13 -0
  6. package/dist/lint.js +13 -0
  7. package/dist/page-model/build-page-model.js +11 -0
  8. package/dist/page-model/connect-css.js +43 -0
  9. package/dist/page-model/css-selectors.js +49 -0
  10. package/dist/page-model/index.js +5 -0
  11. package/dist/page-model/model.js +5 -0
  12. package/dist/page-model/pages.js +27 -0
  13. package/dist/page-model/read-css.js +84 -0
  14. package/dist/page-model/read-page.js +17 -0
  15. package/dist/page-model/read-structure.js +58 -0
  16. package/dist/page-model/read-theme.js +14 -0
  17. package/dist/page-model/utils.js +78 -0
  18. package/dist/rules/allowed-imports.js +109 -0
  19. package/dist/rules/apply-and-presets.js +310 -0
  20. package/dist/rules/bindings.js +673 -0
  21. package/dist/rules/breakpoints.js +124 -0
  22. package/dist/rules/css-file-nesting.js +196 -0
  23. package/dist/rules/css-layout-properties.js +398 -0
  24. package/dist/rules/data-values.js +511 -0
  25. package/dist/rules/define-rule.js +4 -0
  26. package/dist/rules/global-styles.js +215 -0
  27. package/dist/rules/index.js +16 -0
  28. package/dist/rules/no-absolute-section.js +31 -0
  29. package/dist/rules/no-free-code.js +112 -0
  30. package/dist/rules/page-structure.js +277 -0
  31. package/dist/rules/slot-attributes.js +96 -0
  32. package/dist/rules/theme-font-ids.js +69 -0
  33. package/dist/rules/theme-root.js +122 -0
  34. package/dist/rules-adapter/index.js +3 -0
  35. package/dist/rules-adapter/oxlint/adapt-rule.js +17 -0
  36. package/dist/rules-adapter/oxlint/callbacks.js +1 -0
  37. package/dist/rules-adapter/oxlint/origami-rules.config.js +12 -0
  38. package/dist/rules-adapter/oxlint/oxlint-plugin.js +7 -0
  39. package/dist/rules-adapter/oxlint/run-rules.js +22 -0
  40. package/dist/rules-adapter/tool-types.js +1 -0
  41. package/dist/rules-adapter/utils.js +20 -0
  42. package/dist/rules-api/index.js +5 -0
  43. package/dist/rules-api/report.js +13 -0
  44. package/dist/rules-api/rules-api.js +70 -0
  45. package/dist/rules-api/utils.js +11 -0
  46. package/dist/shared/css.js +124 -0
  47. package/dist/shared/log.js +4 -0
  48. package/dist/shared/paths.js +28 -0
  49. package/dist/shared/site-files-tree-mapping.js +12 -0
  50. package/dist/shared/utils.js +208 -0
  51. package/package.json +52 -3
@@ -0,0 +1,673 @@
1
+ import { RUNTIME_ONLY_BUILDERS, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_PACKAGE, SHARED_COMP_ID, bare, getAttribute, getExpression, getKey, getName, getProperties, getProperty, isStructure, ownerOf, } from "../rules-api/index.js";
2
+ import { defineRule } from "./define-rule.js";
3
+ /** The converter's reserved sources (identity.ts `isReservedRegistry`) and the DM's own `document`: no fn()/ctx() names. */
4
+ const RESERVED_FOR_BINDINGS = ['runtime', 'interactReact', 'interact', 'motionPresets', 'menu', 'document', 'core'];
5
+ /** The keys of a `bindings` object. */
6
+ const BINDINGS_KEYS = ['props', 'visibility', 'states'];
7
+ const VISIBILITY_KEYS = ['collapsed', 'hidden'];
8
+ const REMOVED = 'removed';
9
+ const CTX_OPTIONS = ['from', 'placeholder'];
10
+ const STATE_OPTIONS = ['kind', 'id', 'pseudoClass'];
11
+ const BINDINGS_LIST = 'BindingsList';
12
+ const STATIC_PREFIX = 'Static.';
13
+ const PLACES = {
14
+ prop: { name: "a prop's binding", builders: ['ctx', 'fn', 'raw'], literals: false },
15
+ fnArg: { name: 'an `fn()` argument', builders: ['ctx', 'fn', 'raw'], literals: true },
16
+ isBinding: { name: "`is()`'s binding", builders: ['ctx', 'fn'], literals: false },
17
+ condition: { name: 'a condition', builders: ['is', 'all', 'ctx', 'fn', 'raw'], literals: true },
18
+ allItem: { name: 'an `all()` item', builders: ['is', 'ctx', 'fn', 'raw'], literals: true },
19
+ statesItem: { name: 'a `states` item', builders: ['state', 'raw'], literals: false },
20
+ };
21
+ const PLAIN = 'a string, number or true / false';
22
+ export default defineRule({
23
+ name: 'bindings',
24
+ editors: ['studio2'],
25
+ callbacks: {
26
+ JSXElement: checkJSXElementBindings,
27
+ CallExpression: checkCallExpressionImports,
28
+ },
29
+ });
30
+ // ── the callbacks' steps ──
31
+ /** An element in the structure: a `<Remove>`, or a component and its elementProps entries. */
32
+ function checkJSXElementBindings(input) {
33
+ const { node, rulesApi } = input;
34
+ const at = { input, owner: ownerOf(node) };
35
+ if (isRemoveInStructure(node, rulesApi))
36
+ return checkRemoveCondition(at);
37
+ if (!rulesApi.getComponent(node))
38
+ return;
39
+ checkComponentBindings(at);
40
+ for (const entry of getElementPropsEntries(node))
41
+ checkEntryBindings(at, entry);
42
+ }
43
+ /** A `<Remove>`: its `when` condition, and the component it wraps. */
44
+ function checkRemoveCondition(at) {
45
+ const when = removeWhen(at);
46
+ if (when)
47
+ condition(at, when, '`when`', true);
48
+ removeTarget(at);
49
+ }
50
+ /** A component's builders, its `states` and `bindings`, and the props it binds as data attributes. */
51
+ function checkComponentBindings(at) {
52
+ const { node } = at.input;
53
+ builderInPlace(at);
54
+ statesAttribute(at, getAttribute(node, 'states'));
55
+ bindingsAttribute(at, getAttribute(node, 'bindings'));
56
+ if (!hasData(node))
57
+ return;
58
+ boundPropOnce(at);
59
+ for (const [name, , value] of boundAttributes(node))
60
+ prop(at, name, value, true);
61
+ }
62
+ /** One elementProps entry: its `bindings`, and the keys it binds. */
63
+ function checkEntryBindings(at, entry) {
64
+ const object = getObject(entry.value);
65
+ const key = getKey(entry);
66
+ if (!object || key === null)
67
+ return;
68
+ const entryAt = { input: at.input, owner: `${at.owner}'s elementProps entry \`${key}\`` };
69
+ const bindings = getProperty(object, 'bindings');
70
+ if (bindings)
71
+ bindingsValue(entryAt, bindings.value, true);
72
+ entryPropBoundOnce(entryAt, object);
73
+ for (const [key, property] of boundEntryKeys(object))
74
+ prop(entryAt, key, property.value, true);
75
+ }
76
+ /** A builder call in a structure attribute: it is imported, and so are the names it uses. */
77
+ function checkCallExpressionImports(input) {
78
+ if (input.node.callee.type !== 'Identifier' || !isInStructureAttribute(input.node))
79
+ return;
80
+ if (!isBuilderImported(input, input.node.callee))
81
+ return;
82
+ const ctxHook = bindingNameImported(input, input.node.callee.name);
83
+ if (ctxHook)
84
+ ctxSourceHasName(input, ctxHook);
85
+ }
86
+ /** Each builder's own row: the shape of its call, a key written twice in it. */
87
+ const ROW_OF_BUILDER = { all: 'CONV-decode.ts:196', ctx: 'CONV-astValue.ts:276', fn: 'CONV-astValue.ts:287', is: 'CONV-decode.ts:172', raw: 'CONV-decode.ts:28', state: 'CONV-decode.ts:145' };
88
+ const tracked = (message, row) => `${message.slice(0, -1)} (tracker ${row}).`;
89
+ const say = ({ input }, node, message, row) => input.report({ loc: node.loc, message: tracked(message, row) });
90
+ const textOf = ({ input }, node) => input.context.sourceCode.getText(node);
91
+ const describeIn = (at, node) => describe(node, (part) => textOf(at, part));
92
+ function removeWhen(at) {
93
+ const { node } = at.input;
94
+ const when = getAttribute(node, 'when');
95
+ if (!when) {
96
+ say(at, node.openingElement.name, `${at.owner} needs \`when={…}\`: the condition that removes the component it wraps.`, 'CONV-parseJsx.ts:1113');
97
+ return null;
98
+ }
99
+ const value = getExpression(when);
100
+ const what = when.value ? `\`${at.input.context.sourceCode.getText(when.value)}\`` : 'empty';
101
+ if (!value)
102
+ say(at, when, `\`when\` on ${at.owner} is ${what}. Write the condition in braces: \`when={…}\`.`, 'CONV-parseJsx.ts:1113');
103
+ return value;
104
+ }
105
+ function removeTarget(at) {
106
+ const { node, rulesApi } = at.input;
107
+ for (const component of wrappedComponents(node, rulesApi)) {
108
+ const bindings = getAttribute(component, 'bindings');
109
+ const value = bindings && getExpression(bindings);
110
+ if (!value || builderOf(value, rulesApi) !== 'raw')
111
+ continue;
112
+ const item = getObject(value.arguments[0]);
113
+ const target = `${at.owner} wraps ${ownerOf(component)}, whose \`bindings\` are \`raw()\``;
114
+ if (!item || stringValue(getProperty(item, 'type')?.value) !== BINDINGS_LIST) {
115
+ say(at, value, `${target} of an item that isn't \`{ type: '${BINDINGS_LIST}', … }\`. Write the bindings as an object, or set ${REMOVED} in a ${BINDINGS_LIST} item instead of using \`<Remove>\`.`, 'CONV-decode.ts:296');
116
+ }
117
+ else if (setsRemoved(item)) {
118
+ say(at, value, `${target} of an item that already sets \`features.visibility.${REMOVED}\`. Keep only one of them.`, 'CONV-decode.ts:280');
119
+ }
120
+ }
121
+ }
122
+ function builderInPlace(at) {
123
+ const { node, rulesApi } = at.input;
124
+ for (const [call, where] of misplacedBuilders(node, rulesApi)) {
125
+ say(at, call, `\`${builderOf(call, rulesApi)}()\` on ${at.owner} is inside ${where}, where the import reads only plain values. Write a builder as the whole value of a data attribute (\`label={ctx(…)}\`) or of an elementProps entry key (\`navbar: { label: ctx(…) }\`), or in \`bindings\`.`, 'CONV-astValue.ts:98');
126
+ }
127
+ }
128
+ function boundPropOnce(at) {
129
+ const { node, rulesApi } = at.input;
130
+ const bindings = getAttribute(node, 'bindings');
131
+ const props = getBindingsProps(bindings && getExpression(bindings), rulesApi);
132
+ if (!props)
133
+ return;
134
+ for (const [name, attribute] of boundAttributes(node)) {
135
+ const where = props.raw ? `bindings={raw({ properties: { ${name} } })}` : `bindings={{ props: { ${name} } }}`;
136
+ if (props.names.has(name))
137
+ say(at, attribute, `${at.owner} binds \`${name}\` twice: in \`${name}={…}\` and in \`${where}\`. Keep one.`, 'CONV-parseJsx.ts:753');
138
+ }
139
+ }
140
+ function entryPropBoundOnce(at, entry) {
141
+ const props = getBindingsProps(getProperty(entry, 'bindings')?.value, at.input.rulesApi);
142
+ if (!props)
143
+ return;
144
+ for (const [key, property] of boundEntryKeys(entry)) {
145
+ const where = props.raw ? `bindings: raw({ properties: { ${key} } })` : `bindings.props.${key}`;
146
+ if (props.names.has(key))
147
+ say(at, property, `${at.owner} binds \`${key}\` twice: in \`${key}: …\` and in its \`${where}\`. Keep one.`, 'CONV-parseJsx.ts:753');
148
+ }
149
+ }
150
+ function bindingsAttribute(at, attribute) {
151
+ if (!attribute)
152
+ return;
153
+ const value = getExpression(attribute);
154
+ if (value)
155
+ bindingsValue(at, value);
156
+ else
157
+ say(at, attribute, notBindings(at, attribute.value ? `\`${at.input.context.sourceCode.getText(attribute.value)}\`` : 'empty'), 'CONV-parseJsx.ts:700');
158
+ }
159
+ const notBindings = (at, what) => `\`bindings\` on ${at.owner} is ${what}. Write an object in place, \`bindings={{ props: {…}, visibility: {…}, states: {…} }}\`, or \`raw({…})\`.`;
160
+ function bindingsValue(at, node, isEntry = false) {
161
+ const value = bare(node);
162
+ if (value.type === 'ObjectExpression')
163
+ return bindingsObject(at, value, isEntry);
164
+ const builder = builderOf(value, at.input.rulesApi);
165
+ if (builder === 'raw') {
166
+ const [item] = value.arguments;
167
+ if (hasDuplicateKey(at, value, 'raw') || !isRawWrittenOut(at, value))
168
+ return;
169
+ const object = bare(item);
170
+ if (!isEntry && setsRemoved(object))
171
+ say(at, value, `${at.owner} sets \`features.visibility.${REMOVED}\` in its \`raw()\` bindings. Move the condition out, and wrap the component in \`<Remove when={…}>\`.`, 'CONV-decode.ts:280');
172
+ const propertyMap = getProperty(object, 'properties');
173
+ const properties = getObject(propertyMap?.value);
174
+ if (propertyMap && !properties)
175
+ say(at, propertyMap.value, `The \`properties\` in \`raw()\` bindings on ${at.owner} isn't an object. Write a prop map of typed bindings: \`properties: { label: { type: 'ContextBinding', … } }\`.`, 'CONV-decode.ts:252');
176
+ for (const property of properties ? getProperties(properties) : []) {
177
+ if (isStaticItem(property.value))
178
+ say(at, property.value, fixedValue(at, getKey(property)), 'CONV-decode.ts:252');
179
+ }
180
+ }
181
+ else if (builder) {
182
+ say(at, value, `\`bindings\` on ${at.owner} is a \`${builder}()\` call. Write an object and put the call under props, visibility or states, or use \`raw({…})\`.`, 'CONV-decode.ts:263');
183
+ }
184
+ else if (!isCallByName(value)) {
185
+ say(at, value, notBindings(at, describeIn(at, value)), 'CONV-parseJsx.ts:700');
186
+ }
187
+ }
188
+ function bindingsObject(at, bindings, isEntry) {
189
+ for (const property of bindings.properties) {
190
+ const what = property.type === 'SpreadElement' ? `a spread (\`${textOf(at, property)}\`)`
191
+ : property.computed ? 'a computed key'
192
+ : property.key.type === 'Literal' && typeof property.key.value === 'string' ? `the quoted key \`${textOf(at, property.key)}\``
193
+ : property.key.type !== 'Identifier' ? `the key \`${textOf(at, property.key)}\``
194
+ : BINDINGS_KEYS.includes(property.key.name) ? null
195
+ : `the key \`${property.key.name}\``;
196
+ if (what)
197
+ say(at, property, `\`bindings\` on ${at.owner} has ${what}. Use only the keys props, visibility and states, written as plain names.`, 'CONV-parseJsx.ts:708');
198
+ else
199
+ section(at, property.key.name, property.value, isEntry);
200
+ }
201
+ }
202
+ function section(at, name, node, isEntry) {
203
+ const fix = `Write an object of \`key: value\` entries: \`${name}: { label: … }\`.`;
204
+ const value = bare(node);
205
+ if (value.type !== 'ObjectExpression')
206
+ return say(at, value, `\`bindings.${name}\` on ${at.owner} isn't an object. ${fix}`, 'CONV-parseJsx.ts:669');
207
+ for (const property of value.properties) {
208
+ const problem = property.type === 'SpreadElement' ? 'has a spread'
209
+ : property.computed ? 'has a computed key'
210
+ : property.kind !== 'init' || property.method ? 'has a getter, setter or method'
211
+ : property.key.type === 'Literal' && typeof property.key.value === 'number' ? `has the number key ${property.key.value}`
212
+ : getKey(property) === '__proto__' ? 'has the key `__proto__`'
213
+ : getKey(property) === null ? `has the key \`${textOf(at, property.key)}\``
214
+ : null;
215
+ if (problem) {
216
+ say(at, property, `\`bindings.${name}\` on ${at.owner} ${problem}. ${fix}`, 'CONV-parseJsx.ts:669');
217
+ continue;
218
+ }
219
+ const key = getKey(property);
220
+ const where = `\`bindings.${name}.${key}\``;
221
+ if (name === 'props')
222
+ prop(at, key, property.value, false);
223
+ else if (name === 'visibility' && key === REMOVED && !isEntry)
224
+ say(at, property, `${at.owner} sets \`visibility.${REMOVED}\` in its \`bindings\`. Move the condition out, and wrap the component in \`<Remove when={…}>\`.`, 'CONV-decode.ts:280');
225
+ else if (name === 'visibility' && key !== REMOVED && !VISIBILITY_KEYS.includes(key))
226
+ say(at, property, `\`bindings.visibility\` on ${at.owner} has the key \`${key}\`. Use \`collapsed\` or \`hidden\`.`, 'CONV-decode.ts:221');
227
+ else
228
+ condition(at, property.value, where);
229
+ }
230
+ }
231
+ function prop(at, name, node, placeholderAllowed) {
232
+ const value = bare(node);
233
+ if (value.type === 'ArrayExpression')
234
+ return say(at, value, `\`bindings.props.${name}\` on ${at.owner} is an array. Bind the prop to one value: \`ctx()\`, \`fn()\` or \`raw()\`.`, 'CONV-decode.ts:209');
235
+ if (isPlain(value) || rawOnly(value) || isStaticRaw(at, value))
236
+ return say(at, value, fixedValue(at, name), 'CONV-decode.ts:252');
237
+ binding(at, value, 'prop', placeholderAllowed);
238
+ }
239
+ const fixedValue = (at, name) => `\`${name}\` on ${at.owner} is bound to a fixed value. Write it as the attribute \`${name}=…\`, or as an \`fn()\` argument.`;
240
+ function isStaticRaw(at, value) {
241
+ if (builderOf(value, at.input.rulesApi) !== 'raw')
242
+ return false;
243
+ const [item, ...extra] = value.arguments;
244
+ return Boolean(item) && extra.length === 0 && !nonLiteralIn(item) && isStaticItem(item);
245
+ }
246
+ function condition(at, node, where, inRemove = false) {
247
+ const value = bare(node);
248
+ if (value.type !== 'ArrayExpression')
249
+ return binding(at, value, 'condition');
250
+ if (inRemove || value.elements.length > 0) {
251
+ const spread = hasHoleOrSpread(value);
252
+ say(at, value, `${where} on ${at.owner} is ${spread ? 'a list with a spread or an empty place' : 'a list'}. Write one condition, or \`all(a, b, …)\`.`, spread ? 'CONV-parseJsx.ts:655' : 'CONV-decode.ts:190');
253
+ }
254
+ }
255
+ function binding(at, node, place, placeholderAllowed = false) {
256
+ const value = bare(node);
257
+ const { name } = PLACES[place];
258
+ if (isCallByName(value)) {
259
+ const builder = builderOf(value, at.input.rulesApi);
260
+ if (!builder)
261
+ return;
262
+ if (!PLACES[place].builders.includes(builder))
263
+ return wrongPlace(at, value, `\`${builder}()\``, place);
264
+ return call(at, value, builder, place, placeholderAllowed);
265
+ }
266
+ if (isPlain(value)) {
267
+ if (!PLACES[place].literals)
268
+ wrongPlace(at, value, 'a literal', place);
269
+ return;
270
+ }
271
+ const raw = rawOnly(value);
272
+ if (raw && place === 'statesItem')
273
+ return wrongPlace(at, value, raw, place);
274
+ if (raw)
275
+ return say(at, value, `${capitalize(name)} on ${at.owner} is ${raw}. Write ${PLAIN}, or wrap it in \`raw()\`.`, 'CONV-decode.ts:86');
276
+ say(at, value, `${capitalize(name)} on ${at.owner} is ${describeIn(at, value)}. Write it out. ${hintFor(place)}`, 'CONV-decode.ts:86');
277
+ }
278
+ const wrongPlace = (at, node, what, place) => say(at, node, `${capitalize(what)} can't be ${PLACES[place].name} on ${at.owner}. ${hintFor(place)}`, place === 'statesItem' ? 'CONV-decode.ts:132' : 'CONV-decode.ts:126');
279
+ function call(at, node, builder, place, placeholderAllowed) {
280
+ if (node.arguments.some((argument) => argument.type === 'SpreadElement')) {
281
+ return say(at, node, `\`${builder}()\` on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
282
+ }
283
+ if (hasDuplicateKey(at, node, builder))
284
+ return;
285
+ if (builder === 'ctx')
286
+ ctxCall(at, node, placeholderAllowed);
287
+ else if (builder === 'fn')
288
+ fnCall(at, node, placeholderAllowed);
289
+ else if (builder === 'is')
290
+ isCall(at, node);
291
+ else if (builder === 'all')
292
+ allCall(at, node, place);
293
+ else if (builder === 'raw')
294
+ isRawWrittenOut(at, node);
295
+ else
296
+ stateCall(at, node);
297
+ }
298
+ function hasDuplicateKey(at, node, builder) {
299
+ const twice = node.arguments.map(duplicateKeyIn).find(Boolean);
300
+ if (twice)
301
+ say(at, twice, `\`${builder}()\` on ${at.owner} has the key \`${getKey(twice)}\` twice. Keep one.`, ROW_OF_BUILDER[builder]);
302
+ return Boolean(twice);
303
+ }
304
+ function ctxCall(at, node, placeholderAllowed) {
305
+ const [path, options, ...extra] = node.arguments;
306
+ const object = options && getObject(options);
307
+ const keys = object?.properties.map((property) => (property.type === 'Property' && !property.computed ? getKey(property) : null)) ?? [];
308
+ const unknown = keys.find((key) => key === null || !CTX_OPTIONS.includes(key));
309
+ const problem = stringValue(path) === null ? 'has no path string first'
310
+ : extra.length > 0 ? 'has more than two arguments'
311
+ : !options ? 'has no `{ from }` options'
312
+ : !object ? "has options that aren't an object"
313
+ : unknown !== undefined ? (unknown === null ? "has an option that isn't `key: value`" : `has the option \`${unknown}\``)
314
+ : !keys.includes('from') ? 'has no `from`'
315
+ : keys.includes('placeholder') && !placeholderAllowed ? 'has a placeholder: only a whole attribute value, or an elementProps entry value, has one'
316
+ : null;
317
+ if (problem)
318
+ return say(at, node, `\`ctx()\` on ${at.owner} ${problem}. Write \`ctx('item.path', { from: useX })\`.`, 'CONV-astValue.ts:276');
319
+ const placeholder = object && getProperty(object, 'placeholder');
320
+ if (placeholder)
321
+ placeholderValue(at, placeholder.value);
322
+ }
323
+ function fnCall(at, node, placeholderAllowed) {
324
+ const [, ...rest] = node.arguments;
325
+ if (node.arguments.length === 0)
326
+ return say(at, node, `\`fn()\` on ${at.owner} has no arguments. Write the imported function first: \`fn(numberToText, …)\`.`, 'CONV-astValue.ts:287');
327
+ const last = rest.at(-1);
328
+ const options = placeholderAllowed && last ? getObject(last) : null;
329
+ const placeholder = options && options.properties.length === 1 ? getProperty(options, 'placeholder') : undefined;
330
+ for (const argument of rest) {
331
+ if (argument === last && placeholder)
332
+ placeholderValue(at, placeholder.value);
333
+ else
334
+ binding(at, argument, 'fnArg');
335
+ }
336
+ }
337
+ function placeholderValue(at, node) {
338
+ const value = bare(node);
339
+ if (!isPlain(value))
340
+ say(at, value, `The placeholder on ${at.owner} is ${rawOnly(value) ?? describeIn(at, value)}. Write ${PLAIN}.`, 'CONV-parseJsx.ts:729');
341
+ }
342
+ function isCall(at, node) {
343
+ const fix = 'Write `is(<ctx() or fn()>, <string or true / false>)`.';
344
+ if (node.arguments.length !== 2)
345
+ return say(at, node, `\`is()\` on ${at.owner} has ${node.arguments.length} argument${node.arguments.length === 1 ? '' : 's'}. ${fix}`, 'CONV-decode.ts:172');
346
+ const [binded, compared] = node.arguments.map((argument) => bare(argument));
347
+ if (isPlain(binded) || rawOnly(binded) || builderOf(binded, at.input.rulesApi) === 'raw')
348
+ say(at, binded, `\`is()\` on ${at.owner} compares ${isCallByName(binded) ? '`raw()`' : 'a literal'}, not a binding. ${fix}`, 'CONV-decode.ts:172');
349
+ else
350
+ binding(at, binded, 'isBinding');
351
+ const isStringOrBoolean = compared.type === 'Literal' && ['string', 'boolean'].includes(typeof compared.value);
352
+ if (!isStringOrBoolean)
353
+ say(at, compared, `\`is()\` on ${at.owner} compares to ${rawOnly(compared) ?? describeIn(at, compared)}. ${fix}`, 'CONV-decode.ts:172');
354
+ }
355
+ function allCall(at, node, place) {
356
+ const count = node.arguments.length;
357
+ if (place === 'condition' && count < 2)
358
+ say(at, node, `\`all()\` on ${at.owner} has ${count} condition${count === 1 ? '' : 's'}. Give it two or more, or write the one condition without \`all()\`.`, 'CONV-decode.ts:196');
359
+ for (const argument of node.arguments)
360
+ binding(at, argument, 'allItem');
361
+ }
362
+ function isRawWrittenOut(at, node) {
363
+ const [item, ...extra] = node.arguments;
364
+ if (!item || extra.length > 0) {
365
+ say(at, node, `\`raw()\` on ${at.owner} has ${node.arguments.length} arguments. Write one item: \`raw({ type: …, … })\`.`, 'CONV-decode.ts:28');
366
+ return false;
367
+ }
368
+ const value = bare(item);
369
+ if (value.type === 'CallExpression') {
370
+ say(at, value, `\`raw()\` on ${at.owner} wraps a call (\`${textOf(at, value)}\`). Write the item out as an object, or use the builder without \`raw()\`.`, 'CONV-decode.ts:28');
371
+ return false;
372
+ }
373
+ if (value.type !== 'ObjectExpression') {
374
+ say(at, value, `\`raw()\` on ${at.owner} wraps ${rawOnly(value) ?? describeIn(at, value)}, not an object. Write one item: \`raw({ type: …, … })\`.`, 'CONV-decode.ts:28');
375
+ return false;
376
+ }
377
+ const found = nonLiteralIn(value);
378
+ if (found)
379
+ say(at, found, `\`raw()\` on ${at.owner} holds ${describeIn(at, found)}. Write its item out with literal values.`, 'CONV-decode.ts:28');
380
+ return !found;
381
+ }
382
+ function stateCall(at, node) {
383
+ const [nameNode, options, ...extra] = node.arguments;
384
+ const name = stringValue(nameNode);
385
+ const object = options && getObject(options);
386
+ const keys = object ? getProperties(object).map(getKey) : [];
387
+ const unknown = keys.find((key) => key !== null && !STATE_OPTIONS.includes(key));
388
+ const problem = name === null ? 'has no name string first'
389
+ : extra.length > 0 ? 'has more than two arguments'
390
+ : !object || object.properties.length !== keys.length ? "has no options object, or one that isn't all `key: value`"
391
+ : unknown !== undefined ? `has the option \`${unknown}\``
392
+ : null;
393
+ if (problem)
394
+ return say(at, node, `\`state()\` on ${at.owner} ${problem}. Write \`state('hover', { kind: 'hover', id: 'state-1' })\` (pseudoClass is optional).`, 'CONV-decode.ts:145');
395
+ const found = nonLiteralIn(object);
396
+ if (found)
397
+ return say(at, found, `\`state()\` on ${at.owner} holds ${describeIn(at, found)}. Write its options out with literal values.`, 'CONV-decode.ts:145');
398
+ if (stringValue(getProperty(object, 'kind')?.value) === null)
399
+ return say(at, node, `\`state()\` on ${at.owner} has no \`kind\` string. Write \`state('hover', { kind: 'hover', id: 'state-1' })\` (pseudoClass is optional).`, 'CONV-decode.ts:145');
400
+ if (stringValue(getProperty(object, 'id')?.value) === null)
401
+ say(at, node, `\`state('${name}')\` on ${at.owner} has no \`id\` string. Add \`id: '…'\` to its options.`, 'CONV-decode.ts:142');
402
+ }
403
+ function statesAttribute(at, attribute) {
404
+ if (!attribute)
405
+ return;
406
+ const value = getExpression(attribute);
407
+ if (value?.type === 'ArrayExpression' && !hasHoleOrSpread(value)) {
408
+ for (const item of value.elements)
409
+ binding(at, item, 'statesItem');
410
+ return;
411
+ }
412
+ const what = value?.type === 'ArrayExpression' ? 'a list with a spread or an empty place' : value ? describeIn(at, value) : 'not a `{…}` value';
413
+ say(at, attribute, `\`states\` on ${at.owner} is ${what}. Write a list of \`state()\` or \`raw()\` calls: \`states={[state('hover', {…})]}\`.`, 'CONV-parseJsx.ts:641');
414
+ }
415
+ /** Only RUNTIME_ONLY_BUILDERS: other runtime names (`Remove(…)`) pass the converter's parse, then its decode has no case for them. */
416
+ function isBuilderImported({ rulesApi, report }, callee) {
417
+ const found = rulesApi.getImport(callee);
418
+ if (found?.kind === 'runtime' && found.imported === callee.name && RUNTIME_ONLY_BUILDERS.includes(callee.name))
419
+ return true;
420
+ const builder = found?.kind === 'runtime' ? found.imported : callee.name;
421
+ report({
422
+ loc: callee.loc,
423
+ message: tracked(RUNTIME_ONLY_BUILDERS.includes(builder)
424
+ ? `\`${callee.name}(…)\` isn't the builder \`${builder}\` from ${RUNTIME_PACKAGE}. Import it under its own name, \`import { ${builder} } from '${RUNTIME_PACKAGE}'\`, and call it as \`${builder}(…)\`.`
425
+ : `\`${callee.name}(…)\` isn't a builder. In an attribute, only the builders of ${RUNTIME_PACKAGE} are called (${RUNTIME_ONLY_BUILDERS.join(', ')}); write a literal value, or one of them.`, 'CONV-astValue.ts:296'),
426
+ });
427
+ return false;
428
+ }
429
+ function bindingNameImported({ node, rulesApi, report, context }, builder) {
430
+ const [first, second] = node.arguments;
431
+ const options = builder === 'ctx' ? getObject(second) : null;
432
+ const named = builder === 'fn' ? first : options ? getProperty(options, 'from')?.value : undefined;
433
+ if (!named)
434
+ return null;
435
+ const value = bare(named);
436
+ const what = builder === 'fn' ? "fn()'s function" : "ctx()'s `from` hook";
437
+ if (value.type !== 'Identifier') {
438
+ report({ loc: value.loc, message: tracked(`${what} is \`${context.sourceCode.getText(value)}\`. Write an imported name: \`fn(numberToText, …)\`, \`{ from: useProduct }\`.`, 'CONV-astValue.ts:227') });
439
+ return null;
440
+ }
441
+ const found = rulesApi.getImport(value);
442
+ const problem = !found
443
+ ? [`${what} \`${value.name}\` isn't imported. Import it by name from its library: the function's library, or the context's app.`, 'CONV-astValue.ts:227']
444
+ : found.kind && RESERVED_FOR_BINDINGS.includes(found.kind)
445
+ ? [`${what} \`${value.name}\` comes from ${found.source}, which holds no functions or contexts. Import it from its own library: the function's library, or the context's app.`, 'CONV-astValue.ts:230']
446
+ : found.imported === '*'
447
+ ? [`${what} \`${value.name}\` is a namespace (\`import * as ${value.name}\`). Import the ${builder === 'fn' ? 'function' : 'hook'} itself by name: \`import { name } from '${found.source}'\`.`, 'CONV-astValue.ts:227']
448
+ : null;
449
+ if (problem)
450
+ report({ loc: value.loc, message: tracked(...problem) });
451
+ return found && !problem && builder === 'ctx' ? { hook: value, source: found.source } : null;
452
+ }
453
+ function ctxSourceHasName({ report }, { hook, source }) {
454
+ if (source.indexOf('/') !== source.length - 1)
455
+ return;
456
+ report({ loc: hook.loc, message: tracked(`ctx()'s \`from\` hook \`${hook.name}\` is imported from '${source}', which has no context name after the \`/\`. Write '<componentType>/<contextName>'.`, 'CONV-decode.ts:100') });
457
+ }
458
+ // ── reading the code ──
459
+ function getObject(source) {
460
+ if (!source)
461
+ return null;
462
+ const value = source.type === 'JSXAttribute' ? getExpression(source) : bare(source);
463
+ return value?.type === 'ObjectExpression' ? value : null;
464
+ }
465
+ function getElementPropsEntries(element) {
466
+ return getEntries(getObject(getAttribute(element, 'elementProps')));
467
+ }
468
+ function getEntries(elementProps) {
469
+ if (!elementProps)
470
+ return [];
471
+ return getProperties(elementProps).flatMap((entry) => {
472
+ const entryObject = getObject(entry.value);
473
+ return [entry, ...getEntries(getObject(entryObject ? getProperty(entryObject, 'elementProps')?.value : undefined))];
474
+ });
475
+ }
476
+ /** null for a call that isn't a builder: isBuilderImported reports it. */
477
+ function builderOf(node, rulesApi) {
478
+ if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier')
479
+ return null;
480
+ const found = rulesApi.getImport(node.callee);
481
+ return found?.kind === 'runtime' && RUNTIME_ONLY_BUILDERS.includes(found.imported) ? found.imported : null;
482
+ }
483
+ const isCallByName = (node) => node.type === 'CallExpression' && node.callee.type === 'Identifier';
484
+ function isPlain(node) {
485
+ if (node.type === 'UnaryExpression')
486
+ return node.operator === '-' && node.argument.type === 'Literal' && typeof node.argument.value === 'number';
487
+ return node.type === 'Literal' && ['string', 'number', 'boolean'].includes(typeof node.value);
488
+ }
489
+ function rawOnly(node) {
490
+ if (node.type === 'Literal' && node.value === null && node.raw === 'null')
491
+ return 'null';
492
+ if (node.type === 'ObjectExpression')
493
+ return 'an object';
494
+ if (node.type === 'ArrayExpression')
495
+ return 'an array';
496
+ return null;
497
+ }
498
+ const hasHoleOrSpread = (node) => node.type === 'ArrayExpression' && node.elements.some((element) => element === null || element.type === 'SpreadElement');
499
+ function describe(node, getText) {
500
+ if (node.type === 'Identifier')
501
+ return `the variable \`${node.name}\``;
502
+ if (node.type === 'SpreadElement')
503
+ return `a spread (\`${getText(node)}\`)`;
504
+ if (node.type === 'TemplateLiteral')
505
+ return `a template string (\`${getText(node)}\`)`;
506
+ if (node.type === 'ArrayExpression')
507
+ return 'an array';
508
+ if (node.type === 'Property' && node.key.type === 'Literal' && typeof node.key.value === 'number')
509
+ return `the number key \`${node.key.value}\``;
510
+ return `\`${getText(node)}\``;
511
+ }
512
+ function nonLiteralIn(node) {
513
+ if (node === null)
514
+ return null;
515
+ const value = bare(node);
516
+ if (isPlain(value) || rawOnly(value) === 'null')
517
+ return null;
518
+ if (value.type === 'ArrayExpression')
519
+ return value.elements.includes(null) ? value : value.elements.map(nonLiteralIn).find(Boolean) ?? null;
520
+ if (value.type !== 'ObjectExpression')
521
+ return value;
522
+ for (const property of value.properties) {
523
+ if (property.type !== 'Property' || property.computed || property.kind !== 'init' || property.method || getKey(property) === null)
524
+ return property;
525
+ const found = nonLiteralIn(property.value);
526
+ if (found)
527
+ return found;
528
+ }
529
+ return null;
530
+ }
531
+ const capitalize = (text) => text[0].toUpperCase() + text.slice(1);
532
+ const hintFor = (place) => {
533
+ const { builders, literals } = PLACES[place];
534
+ const calls = builders.map((builder) => `\`${builder}()\``);
535
+ return `Write ${literals ? `${calls.join(', ')}, or a string, number or true / false` : `${calls.slice(0, -1).join(', ')}${calls.length > 1 ? ' or ' : ''}${calls.at(-1)}`}.`;
536
+ };
537
+ function isRemoveInStructure(element, rulesApi) {
538
+ if (rulesApi.getType(element) !== 'remove')
539
+ return false;
540
+ return rulesApi.getComponent(element) !== null || wrappedComponents(element, rulesApi).length > 0;
541
+ }
542
+ /** Through an `<Interaction>` too: it hands the `<Remove>` condition on to the component it wraps. */
543
+ const wrappedComponents = (element, rulesApi) => rulesApi.getChildren(element).flatMap((child) => {
544
+ if (child.type !== 'JSXElement')
545
+ return [];
546
+ if (rulesApi.getType(child) === 'interaction')
547
+ return wrappedComponents(child, rulesApi);
548
+ return rulesApi.getComponent(child) ? [child] : [];
549
+ });
550
+ /** The import reads data attributes only on a component with `type=`. */
551
+ const hasData = (element) => getAttribute(element, 'type') !== undefined;
552
+ /** Not the component's data: the converter's reserved attributes, `sharedCompId`, and `states`, which bindings still reads. */
553
+ const isReserved = (name) => RESERVED_ATTRIBUTES.includes(name) || name === SHARED_COMP_ID || name === 'states';
554
+ function boundAttributes(element) {
555
+ return element.openingElement.attributes.flatMap((attribute) => {
556
+ if (attribute.type !== 'JSXAttribute' || isReserved(getName(attribute)))
557
+ return [];
558
+ const value = getExpression(attribute);
559
+ return value?.type === 'CallExpression' ? [[getName(attribute), attribute, value]] : [];
560
+ });
561
+ }
562
+ function boundEntryKeys(entry) {
563
+ return getProperties(entry).flatMap((property) => {
564
+ const key = getKey(property);
565
+ return key !== null && !RESERVED_ENTRY_KEYS.includes(key) && bare(property.value).type === 'CallExpression' ? [[key, property]] : [];
566
+ });
567
+ }
568
+ function getBindingsProps(node, rulesApi) {
569
+ const value = node && bare(node);
570
+ if (!value)
571
+ return null;
572
+ const raw = builderOf(value, rulesApi) === 'raw';
573
+ const bindings = getObject(raw ? value.arguments[0] : value);
574
+ const props = bindings && getObject(getProperty(bindings, raw ? 'properties' : 'props')?.value);
575
+ return props ? { names: new Set(getProperties(props).map(getKey).filter((key) => key !== null)), raw } : null;
576
+ }
577
+ /** The import stores a prop's binding as an item: one with no string type, or a Static.* type, is a fixed value. */
578
+ function isStaticItem(node) {
579
+ const item = getObject(node);
580
+ const type = item && stringValue(getProperty(item, 'type')?.value);
581
+ return !type || type.startsWith(STATIC_PREFIX);
582
+ }
583
+ function duplicateKeyIn(node) {
584
+ if (node === null)
585
+ return null;
586
+ const value = bare(node);
587
+ if (value.type === 'ArrayExpression')
588
+ return value.elements.map(duplicateKeyIn).find(Boolean) ?? null;
589
+ if (value.type !== 'ObjectExpression')
590
+ return null;
591
+ const seen = new Set();
592
+ for (const property of getProperties(value)) {
593
+ const key = getKey(property);
594
+ if (key !== null && seen.has(key))
595
+ return property;
596
+ if (key !== null)
597
+ seen.add(key);
598
+ const found = duplicateKeyIn(property.value);
599
+ if (found)
600
+ return found;
601
+ }
602
+ return null;
603
+ }
604
+ function setsRemoved(item) {
605
+ let object = item;
606
+ for (const key of ['features', 'visibility'])
607
+ object = object && getObject(getProperty(object, key)?.value);
608
+ return Boolean(object && getProperty(object, REMOVED));
609
+ }
610
+ function stringValue(node) {
611
+ const value = node && bare(node);
612
+ return value?.type === 'Literal' && typeof value.value === 'string' ? value.value : null;
613
+ }
614
+ function misplacedBuilders(element, rulesApi) {
615
+ return element.openingElement.attributes.flatMap((attribute) => {
616
+ if (attribute.type !== 'JSXAttribute')
617
+ return [];
618
+ const name = getName(attribute);
619
+ const value = getExpression(attribute);
620
+ if (!value || name === 'bindings' || name === 'states')
621
+ return [];
622
+ if (name === 'elementProps')
623
+ return entryBuilders(value, rulesApi, '`elementProps`');
624
+ if (!isReserved(name) && builderOf(value, rulesApi))
625
+ return [];
626
+ return buildersIn(value, rulesApi).map((call) => [call, `\`${name}\``]);
627
+ });
628
+ }
629
+ function entryBuilders(elementProps, rulesApi, where) {
630
+ const map = getObject(elementProps);
631
+ if (!map)
632
+ return buildersIn(elementProps, rulesApi).map((call) => [call, where]);
633
+ return getProperties(map).flatMap((entry) => {
634
+ if (getKey(entry) === null)
635
+ return [];
636
+ const name = `${where} entry \`${getKey(entry)}\``;
637
+ const object = getObject(entry.value);
638
+ if (!object)
639
+ return buildersIn(entry.value, rulesApi).map((call) => [call, name]);
640
+ return getProperties(object).flatMap((property) => {
641
+ const key = getKey(property);
642
+ if (key === null || key === 'bindings' || key === 'slots')
643
+ return [];
644
+ if (key === 'elementProps')
645
+ return entryBuilders(property.value, rulesApi, `${name}'s \`elementProps\``);
646
+ if (!RESERVED_ENTRY_KEYS.includes(key) && builderOf(property.value, rulesApi))
647
+ return [];
648
+ return buildersIn(property.value, rulesApi).map((call) => [call, `${name}'s \`${key}\``]);
649
+ });
650
+ });
651
+ }
652
+ function buildersIn(node, rulesApi) {
653
+ if (!node || typeof node !== 'object' || !('type' in node))
654
+ return [];
655
+ const value = node;
656
+ // Functions have their own scope; nested JSX is checked by its own callback.
657
+ if (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression' || value.type === 'JSXElement' || value.type === 'JSXFragment')
658
+ return [];
659
+ if (value.type === 'CallExpression' && builderOf(value, rulesApi))
660
+ return [value];
661
+ return Object.entries(value).flatMap(([key, child]) => key === 'parent' ? []
662
+ : Array.isArray(child) ? child.flatMap((item) => buildersIn(item, rulesApi)) : buildersIn(child, rulesApi));
663
+ }
664
+ function isInStructureAttribute(node) {
665
+ let inAttribute = false;
666
+ for (let up = node.parent; up.type !== 'Program'; up = up.parent) {
667
+ if (up.type === 'JSXAttribute')
668
+ inAttribute = true;
669
+ if (up.type === 'ArrowFunctionExpression' || up.type === 'FunctionExpression')
670
+ return inAttribute && isStructure(up);
671
+ }
672
+ return false;
673
+ }