@wix/origami-lint 1.34.5 → 1.34.7
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/rules/bindings.js +210 -103
- package/dist/rules/data-values.js +17 -26
- package/dist/rules-api/index.js +2 -2
- package/dist/rules-api/rules-api.js +2 -1
- package/dist/shared/constants.js +11 -3
- package/dist/shared/utils.js +67 -1
- package/package.json +3 -3
package/dist/rules/bindings.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BUILDERS, CALLBACK_PROP, EVENT_ATTRIBUTE, RESERVED_ENTRY_KEYS, RUNTIME_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, bare, getAttribute, getExpression, getKey, getCallbackKind, getName, getObject, getParamReadKind, getProperties, getProperty, isBound, isReservedAttribute, isStructure, ownerOf, } from "../rules-api/index.js";
|
|
2
2
|
import { defineRule } from "./define-rule.js";
|
|
3
3
|
/** The converter's reserved sources (identity.ts `isReservedRegistry`) and the DM's own `document`: no fn()/ctx() names. */
|
|
4
4
|
const RESERVED_FOR_BINDINGS = ['runtime', 'interactReact', 'interact', 'motionPresets', 'menu', 'document', 'core'];
|
|
5
|
-
/** The
|
|
6
|
-
const
|
|
5
|
+
/** The key of a `bindings` object (parseJsx.ts bindingsIrOfExpression); the other two sections are written in `className` since converter 0.79. */
|
|
6
|
+
const BINDINGS_KEY = 'props';
|
|
7
|
+
const CLASS_NAME_SECTIONS = ['visibility', 'states'];
|
|
8
|
+
/** A `className` term for a visibility: the viewer's utility class (reservedAttributes.ts CLASS_NAME_UTILITIES). */
|
|
9
|
+
const VISIBILITY_PREFIX = 'wix-visibility-';
|
|
7
10
|
const VISIBILITY_KEYS = ['collapsed', 'hidden'];
|
|
8
11
|
const REMOVED = 'removed';
|
|
9
12
|
const CTX_OPTIONS = ['from', 'placeholder'];
|
|
10
|
-
const STATE_OPTIONS = ['kind', 'id', 'pseudoClass'];
|
|
11
13
|
const BINDINGS_LIST = 'BindingsList';
|
|
12
14
|
const STATIC_PREFIX = 'Static.';
|
|
15
|
+
/** The types of an object the import reads as an argument's data item (decode.ts decodeDataRef); an object with no type and an `href` is a link. */
|
|
16
|
+
const DATA_ITEM_TYPES = ['Builder.Image', 'Builder.Video', 'Builder.VectorArt', 'Builder.Audio', 'Builder.RichText', 'Builder.MenuItems', 'Builder.MenuItem', 'PageLink', 'ExternalLink',
|
|
17
|
+
'EmailLink', 'AnchorLink', 'DocumentLink', 'PhoneLink', 'DynamicPageLink', 'TextLink', 'LoginToWixLink', 'WhatsAppLink', 'AddressLink', 'TpaPageLink'];
|
|
13
18
|
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 },
|
|
19
|
+
prop: { name: "a prop's binding", builders: ['ctx', 'fn', 'raw', 'callback', 'map', 'shape'], literals: false },
|
|
20
|
+
fnArg: { name: 'an `fn()` argument', builders: ['ctx', 'fn', 'raw', 'callback', 'map', 'shape'], literals: true },
|
|
21
|
+
isBinding: { name: "`is()`'s binding", builders: ['ctx', 'fn', 'callback'], literals: false },
|
|
22
|
+
condition: { name: 'a condition', builders: ['is', 'all', 'ctx', 'fn', 'raw', 'callback'], literals: true },
|
|
23
|
+
allItem: { name: 'an `all()` item', builders: ['is', 'ctx', 'fn', 'raw', 'callback'], literals: true },
|
|
20
24
|
};
|
|
21
25
|
const PLAIN = 'a string, number or true / false';
|
|
26
|
+
/** The builders whose value runs at render: a callback prop gets their value, not a function. */
|
|
27
|
+
const RENDER_BUILDERS = ['ctx', 'fn', ...TRANSFORM_BUILDERS];
|
|
22
28
|
export default defineRule({
|
|
23
29
|
name: 'bindings',
|
|
24
30
|
editors: ['studio2'],
|
|
@@ -47,13 +53,14 @@ function checkRemoveCondition(at) {
|
|
|
47
53
|
condition(at, when, '`when`', true);
|
|
48
54
|
removeTarget(at);
|
|
49
55
|
}
|
|
50
|
-
/** A component's builders, its `
|
|
56
|
+
/** A component's builders, its callback props, its `bindings` and `className` conditions, and the props it binds as data attributes. */
|
|
51
57
|
function checkComponentBindings(at) {
|
|
52
|
-
const { node } = at.input;
|
|
58
|
+
const { node, rulesApi } = at.input;
|
|
53
59
|
builderInPlace(at);
|
|
54
|
-
|
|
60
|
+
checkCallbackProps(at);
|
|
55
61
|
bindingsAttribute(at, getAttribute(node, 'bindings'));
|
|
56
|
-
|
|
62
|
+
classNameAttribute(at, getAttribute(node, 'className'));
|
|
63
|
+
if (!rulesApi.hasDataAttributes(node))
|
|
57
64
|
return;
|
|
58
65
|
boundPropOnce(at);
|
|
59
66
|
for (const [name, , value] of boundAttributes(node))
|
|
@@ -69,6 +76,9 @@ function checkEntryBindings(at, entry) {
|
|
|
69
76
|
const bindings = getProperty(object, 'bindings');
|
|
70
77
|
if (bindings)
|
|
71
78
|
bindingsValue(entryAt, bindings.value, true);
|
|
79
|
+
const className = getObject(getProperty(object, 'className')?.value);
|
|
80
|
+
if (className)
|
|
81
|
+
classNameConditions(entryAt, className, '`className`');
|
|
72
82
|
entryPropBoundOnce(entryAt, object);
|
|
73
83
|
for (const [key, property] of boundEntryKeys(object))
|
|
74
84
|
prop(entryAt, key, property.value, true);
|
|
@@ -84,7 +94,7 @@ function checkCallExpressionImports(input) {
|
|
|
84
94
|
ctxSourceHasName(input, ctxHook);
|
|
85
95
|
}
|
|
86
96
|
/** 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',
|
|
97
|
+
const ROW_OF_BUILDER = { all: 'CONV-decode.ts:196', callback: 'CONV-astValue.ts:287', ctx: 'CONV-astValue.ts:276', fn: 'CONV-astValue.ts:287', is: 'CONV-decode.ts:172', raw: 'CONV-decode.ts:28' };
|
|
88
98
|
const tracked = (message, row) => `${message.slice(0, -1)} (tracker ${row}).`;
|
|
89
99
|
const say = ({ input }, node, message, row) => input.report({ loc: node.loc, message: tracked(message, row) });
|
|
90
100
|
const textOf = ({ input }, node) => input.context.sourceCode.getText(node);
|
|
@@ -110,12 +120,9 @@ function removeTarget(at) {
|
|
|
110
120
|
if (!value || builderOf(value, rulesApi) !== 'raw')
|
|
111
121
|
continue;
|
|
112
122
|
const item = getObject(value.arguments[0]);
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
say(at, value, `${
|
|
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');
|
|
123
|
+
// An item of another type is the bindings' own line (checkRawBindingsType).
|
|
124
|
+
if (item && stringValue(getProperty(item, 'type')?.value) === BINDINGS_LIST && setsRemoved(item)) {
|
|
125
|
+
say(at, value, `${at.owner} wraps ${ownerOf(component)}, whose \`bindings\` are \`raw()\` of an item that already sets \`features.visibility.${REMOVED}\`. Keep only one of them.`, 'CONV-decode.ts:280');
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
}
|
|
@@ -125,6 +132,20 @@ function builderInPlace(at) {
|
|
|
125
132
|
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
133
|
}
|
|
127
134
|
}
|
|
135
|
+
/** A callback prop takes a function: the import reads it only in `bindings.props`, and there the site's runtime throws on anything but `callback()` (withOrigami). */
|
|
136
|
+
function checkCallbackProps(at) {
|
|
137
|
+
const { node, rulesApi } = at.input;
|
|
138
|
+
for (const [name, attribute] of getCallbackAttributes(node)) {
|
|
139
|
+
say(at, attribute, `\`${name}\` on ${at.owner} is an attribute, which the import doesn't read for a callback prop. Bind it in \`bindings\` with \`callback()\`: \`bindings={{ props: { ${name}: callback(goHome) } }}\`.`, 'OUR-031');
|
|
140
|
+
}
|
|
141
|
+
if (!isWrappedByRuntime(node, rulesApi))
|
|
142
|
+
return;
|
|
143
|
+
for (const [name, value] of getBoundCallbacks(getAttribute(node, 'bindings'))) {
|
|
144
|
+
const what = runsAtRender(at, value);
|
|
145
|
+
if (what)
|
|
146
|
+
say(at, value, `\`bindings.props.${name}\` on ${at.owner} is ${what}, which runs at render and gives a value, not a function: the site's runtime throws on it. Write \`${name}: callback(goHome)\`.`, 'OUR-031');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
128
149
|
function boundPropOnce(at) {
|
|
129
150
|
const { node, rulesApi } = at.input;
|
|
130
151
|
const bindings = getAttribute(node, 'bindings');
|
|
@@ -156,7 +177,7 @@ function bindingsAttribute(at, attribute) {
|
|
|
156
177
|
else
|
|
157
178
|
say(at, attribute, notBindings(at, attribute.value ? `\`${at.input.context.sourceCode.getText(attribute.value)}\`` : 'empty'), 'CONV-parseJsx.ts:700');
|
|
158
179
|
}
|
|
159
|
-
const notBindings = (at, what) => `\`bindings\` on ${at.owner} is ${what}. Write an object in place, \`bindings={{ props: {…}
|
|
180
|
+
const notBindings = (at, what) => `\`bindings\` on ${at.owner} is ${what}. Write an object in place, \`bindings={{ props: {…} }}\`, or \`raw({…})\`.`;
|
|
160
181
|
function bindingsValue(at, node, isEntry = false) {
|
|
161
182
|
const value = bare(node);
|
|
162
183
|
if (value.type === 'ObjectExpression')
|
|
@@ -167,6 +188,8 @@ function bindingsValue(at, node, isEntry = false) {
|
|
|
167
188
|
if (hasDuplicateKey(at, value, 'raw') || !isRawWrittenOut(at, value))
|
|
168
189
|
return;
|
|
169
190
|
const object = bare(item);
|
|
191
|
+
if (!isEntry)
|
|
192
|
+
checkRawBindingsType(at, value, object);
|
|
170
193
|
if (!isEntry && setsRemoved(object))
|
|
171
194
|
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
195
|
const propertyMap = getProperty(object, 'properties');
|
|
@@ -179,7 +202,7 @@ function bindingsValue(at, node, isEntry = false) {
|
|
|
179
202
|
}
|
|
180
203
|
}
|
|
181
204
|
else if (builder) {
|
|
182
|
-
say(at, value, `\`bindings\` on ${at.owner} is a \`${builder}()\` call. Write an object and put the call under props,
|
|
205
|
+
say(at, value, `\`bindings\` on ${at.owner} is a \`${builder}()\` call. Write an object and put the call under props, or use \`raw({…})\`.`, 'CONV-decode.ts:263');
|
|
183
206
|
}
|
|
184
207
|
else if (!isCallByName(value)) {
|
|
185
208
|
say(at, value, notBindings(at, describeIn(at, value)), 'CONV-parseJsx.ts:700');
|
|
@@ -191,15 +214,27 @@ function bindingsObject(at, bindings, isEntry) {
|
|
|
191
214
|
: property.computed ? 'a computed key'
|
|
192
215
|
: property.key.type === 'Literal' && typeof property.key.value === 'string' ? `the quoted key \`${textOf(at, property.key)}\``
|
|
193
216
|
: property.key.type !== 'Identifier' ? `the key \`${textOf(at, property.key)}\``
|
|
194
|
-
:
|
|
217
|
+
: property.key.name === BINDINGS_KEY || CLASS_NAME_SECTIONS.includes(property.key.name) ? null
|
|
195
218
|
: `the key \`${property.key.name}\``;
|
|
196
219
|
if (what)
|
|
197
|
-
say(at, property, `\`bindings\` on ${at.owner} has ${what}. Use only the
|
|
220
|
+
say(at, property, `\`bindings\` on ${at.owner} has ${what}. Use only the key props, written as a plain name.`, 'CONV-parseJsx.ts:708');
|
|
221
|
+
else if (getKey(property) === BINDINGS_KEY)
|
|
222
|
+
section(at, BINDINGS_KEY, property.value);
|
|
198
223
|
else
|
|
199
|
-
|
|
224
|
+
classNameSection(at, property, isEntry);
|
|
200
225
|
}
|
|
201
226
|
}
|
|
202
|
-
|
|
227
|
+
/** `bindings.visibility` and `bindings.states`: the import reads them in `className` since converter 0.79, and `removed` in `<Remove when>` (parseJsx.ts assertNoClassNameSections). */
|
|
228
|
+
function classNameSection(at, property, isEntry) {
|
|
229
|
+
const key = getKey(property);
|
|
230
|
+
const removed = key === 'visibility' && !isEntry && getObject(property.value) && getProperty(getObject(property.value), REMOVED);
|
|
231
|
+
if (removed)
|
|
232
|
+
return say(at, removed, `${at.owner} sets \`visibility.${REMOVED}\` in its \`bindings\`. Move the condition out, and wrap the component in \`<Remove when={…}>\`.`, 'CONV-decode.ts:280');
|
|
233
|
+
const term = key === 'states' ? '{ hover: is(…) }' : "{ 'wix-visibility-hidden': is(…) }";
|
|
234
|
+
const fix = isEntry ? `in the entry's \`className: ${term}\`` : `as a \`className\` term: \`className={['comp-x', ${term}]}\``;
|
|
235
|
+
say(at, property, `\`bindings.${key}\` on ${at.owner} isn't read: the import reads states and visibility in \`className\`. Write each condition ${fix}.`, 'CONV-parseJsx.ts:708');
|
|
236
|
+
}
|
|
237
|
+
function section(at, name, node) {
|
|
203
238
|
const fix = `Write an object of \`key: value\` entries: \`${name}: { label: … }\`.`;
|
|
204
239
|
const value = bare(node);
|
|
205
240
|
if (value.type !== 'ObjectExpression')
|
|
@@ -216,23 +251,36 @@ function section(at, name, node, isEntry) {
|
|
|
216
251
|
say(at, property, `\`bindings.${name}\` on ${at.owner} ${problem}. ${fix}`, 'CONV-parseJsx.ts:669');
|
|
217
252
|
continue;
|
|
218
253
|
}
|
|
254
|
+
prop(at, getKey(property), property.value, false);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** `className={['comp-x', 'open', { hover: is(…) }]}`: each object term's key holds a condition, as `bindings.states` and `bindings.visibility` did (parseJsx.ts mergeClassName). */
|
|
258
|
+
function classNameAttribute(at, attribute) {
|
|
259
|
+
const value = getExpression(attribute);
|
|
260
|
+
if (value?.type !== 'ArrayExpression')
|
|
261
|
+
return;
|
|
262
|
+
for (const term of value.elements.slice(1)) {
|
|
263
|
+
if (term?.type === 'ObjectExpression')
|
|
264
|
+
classNameConditions(at, term, '`className`');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function classNameConditions(at, terms, where) {
|
|
268
|
+
for (const property of getProperties(terms)) {
|
|
219
269
|
const key = getKey(property);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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');
|
|
270
|
+
if (key === null)
|
|
271
|
+
continue;
|
|
272
|
+
if (key.startsWith(VISIBILITY_PREFIX) && !VISIBILITY_KEYS.includes(key.slice(VISIBILITY_PREFIX.length))) {
|
|
273
|
+
say(at, property, `${where} on ${at.owner} has the term \`${key}\`, which isn't a visibility. Use \`${VISIBILITY_PREFIX}hidden\` or \`${VISIBILITY_PREFIX}collapsed\`.`, 'CONV-decode.ts:221');
|
|
274
|
+
}
|
|
227
275
|
else
|
|
228
|
-
condition(at, property.value, where);
|
|
276
|
+
condition(at, property.value, `${where} term \`${key}\``);
|
|
229
277
|
}
|
|
230
278
|
}
|
|
231
279
|
function prop(at, name, node, placeholderAllowed) {
|
|
232
280
|
const value = bare(node);
|
|
233
281
|
if (value.type === 'ArrayExpression')
|
|
234
282
|
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))
|
|
283
|
+
if (isPlain(value) || rawOnly(value) || isStaticRaw(at, value) || isTyped(value, at.input.rulesApi))
|
|
236
284
|
return say(at, value, fixedValue(at, name), 'CONV-decode.ts:252');
|
|
237
285
|
binding(at, value, 'prop', placeholderAllowed);
|
|
238
286
|
}
|
|
@@ -255,10 +303,17 @@ function condition(at, node, where, inRemove = false) {
|
|
|
255
303
|
function binding(at, node, place, placeholderAllowed = false) {
|
|
256
304
|
const value = bare(node);
|
|
257
305
|
const { name } = PLACES[place];
|
|
306
|
+
// A read of the renderItem row or of a map() item (`item.name`) is a binding in every place (decode.ts decodeRef).
|
|
307
|
+
if (getParamReadKind(value))
|
|
308
|
+
return;
|
|
309
|
+
if (isInvoke(value))
|
|
310
|
+
return invoke(at, value);
|
|
258
311
|
if (isCallByName(value)) {
|
|
259
312
|
const builder = builderOf(value, at.input.rulesApi);
|
|
260
313
|
if (!builder)
|
|
261
314
|
return;
|
|
315
|
+
if (TYPED_BUILDERS.includes(builder))
|
|
316
|
+
return PLACES[place].literals ? undefined : wrongPlace(at, value, `\`${builder}()\``, place);
|
|
262
317
|
if (!PLACES[place].builders.includes(builder))
|
|
263
318
|
return wrongPlace(at, value, `\`${builder}()\``, place);
|
|
264
319
|
return call(at, value, builder, place, placeholderAllowed);
|
|
@@ -269,31 +324,46 @@ function binding(at, node, place, placeholderAllowed = false) {
|
|
|
269
324
|
return;
|
|
270
325
|
}
|
|
271
326
|
const raw = rawOnly(value);
|
|
272
|
-
if (raw && place === '
|
|
273
|
-
return
|
|
327
|
+
if (raw === 'an object' && place === 'fnArg' && isDataItem(value))
|
|
328
|
+
return;
|
|
329
|
+
if (raw === 'an object' && place === 'fnArg')
|
|
330
|
+
return say(at, value, `${capitalize(name)} on ${at.owner} is an object that isn't a data item. Write ${PLAIN}, a link (\`{ href: … }\`) or a typed item (\`{ type: 'Builder.Image', … }\`), or wrap it in \`raw()\`.`, 'CONV-decode.ts:86');
|
|
274
331
|
if (raw)
|
|
275
332
|
return say(at, value, `${capitalize(name)} on ${at.owner} is ${raw}. Write ${PLAIN}, or wrap it in \`raw()\`.`, 'CONV-decode.ts:86');
|
|
276
333
|
say(at, value, `${capitalize(name)} on ${at.owner} is ${describeIn(at, value)}. Write it out. ${hintFor(place)}`, 'CONV-decode.ts:86');
|
|
277
334
|
}
|
|
278
|
-
const wrongPlace = (at, node, what, place) => say(at, node, `${capitalize(what)} can't be ${PLACES[place].name} on ${at.owner}. ${hintFor(place)}`,
|
|
335
|
+
const wrongPlace = (at, node, what, place) => say(at, node, `${capitalize(what)} can't be ${PLACES[place].name} on ${at.owner}. ${hintFor(place)}`, 'CONV-decode.ts:126');
|
|
336
|
+
/** A called ctx() or row member: `ctx('cart.add', { from: useCart })(item.id)`, `item.actions.select('quick')` (decode.ts decodeInvoke). */
|
|
337
|
+
function invoke(at, node) {
|
|
338
|
+
const { callee } = node;
|
|
339
|
+
if (!isMethodTarget(callee, at.input.rulesApi))
|
|
340
|
+
return say(at, node, `${capitalize(describeIn(at, node))} on ${at.owner} calls what isn't a context or the renderItem row. Call only \`ctx(…)\` or a member of the row: \`ctx('cart.add', { from: useCart })(item.id)\`.`, 'CONV-decode.ts:86');
|
|
341
|
+
if (callee.type === 'CallExpression')
|
|
342
|
+
ctxCall(at, callee, false);
|
|
343
|
+
if (node.arguments.some((argument) => argument.type === 'SpreadElement'))
|
|
344
|
+
return say(at, node, `A called \`ctx()\` or row member on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
|
|
345
|
+
for (const argument of node.arguments)
|
|
346
|
+
binding(at, argument, 'fnArg');
|
|
347
|
+
}
|
|
279
348
|
function call(at, node, builder, place, placeholderAllowed) {
|
|
280
349
|
if (node.arguments.some((argument) => argument.type === 'SpreadElement')) {
|
|
281
350
|
return say(at, node, `\`${builder}()\` on ${at.owner} spreads its arguments. Write each argument out.`, 'CONV-astValue.ts:310');
|
|
282
351
|
}
|
|
283
|
-
|
|
352
|
+
// map() and shape() hold a callback, read in its own scope: no tracker row checks its shape yet.
|
|
353
|
+
if (builder === 'map' || builder === 'shape' || hasDuplicateKey(at, node, builder))
|
|
284
354
|
return;
|
|
285
355
|
if (builder === 'ctx')
|
|
286
356
|
ctxCall(at, node, placeholderAllowed);
|
|
287
357
|
else if (builder === 'fn')
|
|
288
358
|
fnCall(at, node, placeholderAllowed);
|
|
359
|
+
else if (builder === 'callback')
|
|
360
|
+
callbackCall(at, node);
|
|
289
361
|
else if (builder === 'is')
|
|
290
362
|
isCall(at, node);
|
|
291
363
|
else if (builder === 'all')
|
|
292
364
|
allCall(at, node, place);
|
|
293
|
-
else if (builder === 'raw')
|
|
294
|
-
isRawWrittenOut(at, node);
|
|
295
365
|
else
|
|
296
|
-
|
|
366
|
+
isRawWrittenOut(at, node);
|
|
297
367
|
}
|
|
298
368
|
function hasDuplicateKey(at, node, builder) {
|
|
299
369
|
const twice = node.arguments.map(duplicateKeyIn).find(Boolean);
|
|
@@ -334,6 +404,21 @@ function fnCall(at, node, placeholderAllowed) {
|
|
|
334
404
|
binding(at, argument, 'fnArg');
|
|
335
405
|
}
|
|
336
406
|
}
|
|
407
|
+
/** `callback(goHome, 2)`: the function (an imported one, a `ctx()` or a member of the row), then its bound arguments (decode.ts decodeCallback). */
|
|
408
|
+
function callbackCall(at, node) {
|
|
409
|
+
const [first, ...bound] = node.arguments;
|
|
410
|
+
if (!first)
|
|
411
|
+
return say(at, node, `\`callback()\` on ${at.owner} has no arguments. Write the function first: \`callback(goHome, …)\`.`, 'CONV-astValue.ts:287');
|
|
412
|
+
const target = bare(first);
|
|
413
|
+
// A name that isn't a parameter is the imported function: bindingNameImported checks its import.
|
|
414
|
+
const isFunctionName = target.type === 'Identifier' && !getParamReadKind(target);
|
|
415
|
+
if (!isFunctionName && !isMethodTarget(target, at.input.rulesApi))
|
|
416
|
+
return say(at, target, `\`callback()\`'s function on ${at.owner} is ${describeIn(at, target)}. Write an imported function, a \`ctx()\` or a member of the renderItem row: \`callback(goHome, …)\`.`, 'CONV-decode.ts:86');
|
|
417
|
+
if (target.type === 'CallExpression')
|
|
418
|
+
ctxCall(at, target, false);
|
|
419
|
+
for (const argument of bound)
|
|
420
|
+
binding(at, argument, 'fnArg');
|
|
421
|
+
}
|
|
337
422
|
function placeholderValue(at, node) {
|
|
338
423
|
const value = bare(node);
|
|
339
424
|
if (!isPlain(value))
|
|
@@ -344,14 +429,26 @@ function isCall(at, node) {
|
|
|
344
429
|
if (node.arguments.length !== 2)
|
|
345
430
|
return say(at, node, `\`is()\` on ${at.owner} has ${node.arguments.length} argument${node.arguments.length === 1 ? '' : 's'}. ${fix}`, 'CONV-decode.ts:172');
|
|
346
431
|
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) ?
|
|
432
|
+
if (isPlain(binded) || rawOnly(binded) || builderOf(binded, at.input.rulesApi) === 'raw' || isTyped(binded, at.input.rulesApi))
|
|
433
|
+
say(at, binded, `\`is()\` on ${at.owner} compares ${isCallByName(binded) ? `\`${builderOf(binded, at.input.rulesApi)}()\`` : 'a literal'}, not a binding. ${fix}`, 'CONV-decode.ts:172');
|
|
349
434
|
else
|
|
350
435
|
binding(at, binded, 'isBinding');
|
|
351
436
|
const isStringOrBoolean = compared.type === 'Literal' && ['string', 'boolean'].includes(typeof compared.value);
|
|
352
437
|
if (!isStringOrBoolean)
|
|
353
438
|
say(at, compared, `\`is()\` on ${at.owner} compares to ${rawOnly(compared) ?? describeIn(at, compared)}. ${fix}`, 'CONV-decode.ts:172');
|
|
354
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* A component's `raw()` bindings are one item of the bindings namespace, and its one schema is BindingsList (reservedAttributes.ts
|
|
442
|
+
* BINDINGS_WRAPPER_TYPE): the import refuses an item with no type, or another one ("Cannot find schema … for namespace: bindings").
|
|
443
|
+
* An elementProps entry's bindings are stored inline in the component's inner elements, so they have no such item.
|
|
444
|
+
*/
|
|
445
|
+
function checkRawBindingsType(at, node, item) {
|
|
446
|
+
const type = getProperty(item, 'type');
|
|
447
|
+
if (stringValue(type?.value) === BINDINGS_LIST)
|
|
448
|
+
return;
|
|
449
|
+
const has = type ? `has the type \`${textOf(at, type.value)}\`` : 'has no `type`';
|
|
450
|
+
say(at, type?.value ?? node, `The \`raw()\` bindings item on ${at.owner} ${has}. Write \`raw({ type: '${BINDINGS_LIST}', … })\`: the import stores no other bindings item.`, 'CONV-decode.ts:28');
|
|
451
|
+
}
|
|
355
452
|
function allCall(at, node, place) {
|
|
356
453
|
const count = node.arguments.length;
|
|
357
454
|
if (place === 'condition' && count < 2)
|
|
@@ -379,61 +476,30 @@ function isRawWrittenOut(at, node) {
|
|
|
379
476
|
say(at, found, `\`raw()\` on ${at.owner} holds ${describeIn(at, found)}. Write its item out with literal values.`, 'CONV-decode.ts:28');
|
|
380
477
|
return !found;
|
|
381
478
|
}
|
|
382
|
-
|
|
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. */
|
|
479
|
+
/** Only BUILDERS: other runtime names (`Remove(…)`) pass the converter's parse, then its decode has no case for them. */
|
|
416
480
|
function isBuilderImported({ rulesApi, report }, callee) {
|
|
417
481
|
const found = rulesApi.getImport(callee);
|
|
418
|
-
if (found?.kind === 'runtime' && found.imported === callee.name &&
|
|
482
|
+
if (found?.kind === 'runtime' && found.imported === callee.name && BUILDERS.includes(callee.name))
|
|
419
483
|
return true;
|
|
420
484
|
const builder = found?.kind === 'runtime' ? found.imported : callee.name;
|
|
421
485
|
report({
|
|
422
486
|
loc: callee.loc,
|
|
423
|
-
message: tracked(
|
|
487
|
+
message: tracked(BUILDERS.includes(builder)
|
|
424
488
|
? `\`${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 (${
|
|
489
|
+
: `\`${callee.name}(…)\` isn't a builder. In an attribute, only the builders of ${RUNTIME_PACKAGE} are called (${BUILDERS.join(', ')}); write a literal value, or one of them.`, 'CONV-astValue.ts:296'),
|
|
426
490
|
});
|
|
427
491
|
return false;
|
|
428
492
|
}
|
|
429
493
|
function bindingNameImported({ node, rulesApi, report, context }, builder) {
|
|
430
494
|
const [first, second] = node.arguments;
|
|
431
495
|
const options = builder === 'ctx' ? getObject(second) : null;
|
|
432
|
-
|
|
496
|
+
// callback()'s first argument may also be a `ctx()` or a member of the row (astValue.ts astToCallArg).
|
|
497
|
+
const isCallbackName = builder === 'callback' && first && bare(first).type === 'Identifier' && !getParamReadKind(bare(first));
|
|
498
|
+
const named = builder === 'fn' || isCallbackName ? first : options ? getProperty(options, 'from')?.value : undefined;
|
|
433
499
|
if (!named)
|
|
434
500
|
return null;
|
|
435
501
|
const value = bare(named);
|
|
436
|
-
const what = builder === 'fn' ? "fn()'s function" : "ctx()'s `from` hook";
|
|
502
|
+
const what = builder === 'fn' ? "fn()'s function" : builder === 'callback' ? "callback()'s function" : "ctx()'s `from` hook";
|
|
437
503
|
if (value.type !== 'Identifier') {
|
|
438
504
|
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
505
|
return null;
|
|
@@ -456,12 +522,6 @@ function ctxSourceHasName({ report }, { hook, source }) {
|
|
|
456
522
|
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
523
|
}
|
|
458
524
|
// ── 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
525
|
function getElementPropsEntries(element) {
|
|
466
526
|
return getEntries(getObject(getAttribute(element, 'elementProps')));
|
|
467
527
|
}
|
|
@@ -478,9 +538,22 @@ function builderOf(node, rulesApi) {
|
|
|
478
538
|
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier')
|
|
479
539
|
return null;
|
|
480
540
|
const found = rulesApi.getImport(node.callee);
|
|
481
|
-
return found?.kind === 'runtime' &&
|
|
541
|
+
return found?.kind === 'runtime' && BUILDERS.includes(found.imported) ? found.imported : null;
|
|
482
542
|
}
|
|
483
543
|
const isCallByName = (node) => node.type === 'CallExpression' && node.callee.type === 'Identifier';
|
|
544
|
+
/** What the import calls as a method (decode.ts methodBindingOf): a `ctx()`, or a read of the renderItem row. A map() or shape() parameter has no methods. */
|
|
545
|
+
const isMethodTarget = (node, rulesApi) => builderOf(node, rulesApi) === 'ctx' || getParamReadKind(node) === 'template';
|
|
546
|
+
/** A call of a call or of a member: `ctx(…)(…)`, `item.actions.select()`. */
|
|
547
|
+
const isInvoke = (node) => node.type === 'CallExpression' && (node.callee.type === 'CallExpression' || node.callee.type === 'MemberExpression');
|
|
548
|
+
/** A value the import reads as a binding: a builder call, a called ctx() or row member, a read of a renderItem or map() parameter. */
|
|
549
|
+
const isBindingValue = (node, rulesApi) => builderOf(node, rulesApi) !== null || isInvoke(node) || getParamReadKind(node) !== null;
|
|
550
|
+
/** A typed builder, `email('a@b.c')`: a fixed value, like a literal (decode.ts decodeTypedStatic). */
|
|
551
|
+
const isTyped = (node, rulesApi) => TYPED_BUILDERS.includes(builderOf(node, rulesApi) ?? '');
|
|
552
|
+
/** An argument's object the import reads as a data item: a typed Builder item or link, or a link with an `href` and no type (decode.ts decodeDataRef). */
|
|
553
|
+
function isDataItem(item) {
|
|
554
|
+
const type = getProperty(item, 'type');
|
|
555
|
+
return type ? DATA_ITEM_TYPES.includes(stringValue(type.value) ?? '') : getProperty(item, 'href') !== undefined;
|
|
556
|
+
}
|
|
484
557
|
function isPlain(node) {
|
|
485
558
|
if (node.type === 'UnaryExpression')
|
|
486
559
|
return node.operator === '-' && node.argument.type === 'Literal' && typeof node.argument.value === 'number';
|
|
@@ -547,22 +620,45 @@ const wrappedComponents = (element, rulesApi) => rulesApi.getChildren(element).f
|
|
|
547
620
|
return wrappedComponents(child, rulesApi);
|
|
548
621
|
return rulesApi.getComponent(child) ? [child] : [];
|
|
549
622
|
});
|
|
550
|
-
/** The
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
623
|
+
/** The runtime's withOrigami wraps the component wrappers and the companion components: only on them does a callback prop throw. */
|
|
624
|
+
function isWrappedByRuntime(element, rulesApi) {
|
|
625
|
+
const kind = rulesApi.getImport(element)?.kind;
|
|
626
|
+
return kind === 'wrapper' || kind === 'siteCode';
|
|
627
|
+
}
|
|
628
|
+
/** A callback prop written as an attribute with a call: `onClick={fn(goHome)}`. */
|
|
629
|
+
function getCallbackAttributes(element) {
|
|
630
|
+
return element.openingElement.attributes.flatMap((attribute) => attribute.type === 'JSXAttribute' && CALLBACK_PROP.test(getName(attribute)) && getExpression(attribute)?.type === 'CallExpression' ? [[getName(attribute), attribute]] : []);
|
|
631
|
+
}
|
|
632
|
+
/** The callback props in `bindings={{ props: { onClick: … } }}`. */
|
|
633
|
+
function getBoundCallbacks(attribute) {
|
|
634
|
+
const bindings = getObject(attribute);
|
|
635
|
+
const props = bindings && getObject(getProperty(bindings, BINDINGS_KEY)?.value);
|
|
636
|
+
return (props ? getProperties(props) : []).flatMap((property) => {
|
|
637
|
+
const key = getKey(property);
|
|
638
|
+
return key !== null && CALLBACK_PROP.test(key) ? [[key, bare(property.value)]] : [];
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
/** What a callback prop is bound with that runs at render: `fn()`, `ctx()`, a called ctx() or row member, a read of the row; null for `callback()` and `raw()`. */
|
|
642
|
+
function runsAtRender(at, node) {
|
|
643
|
+
const builder = builderOf(node, at.input.rulesApi);
|
|
644
|
+
if (builder)
|
|
645
|
+
return RENDER_BUILDERS.includes(builder) ? `\`${builder}()\`` : null;
|
|
646
|
+
return isInvoke(node) || getParamReadKind(node) ? describeIn(at, node) : null;
|
|
647
|
+
}
|
|
648
|
+
/** The data attributes bound to a value the import reads as a binding; not an `on…` attribute, which it doesn't read at all (OUR-031 reports it). */
|
|
554
649
|
function boundAttributes(element) {
|
|
555
650
|
return element.openingElement.attributes.flatMap((attribute) => {
|
|
556
|
-
if (attribute.type !== 'JSXAttribute' ||
|
|
651
|
+
if (attribute.type !== 'JSXAttribute' || isReservedAttribute(getName(attribute)) || EVENT_ATTRIBUTE.test(getName(attribute)))
|
|
557
652
|
return [];
|
|
558
653
|
const value = getExpression(attribute);
|
|
559
|
-
return value
|
|
654
|
+
return isBound(value) ? [[getName(attribute), attribute, value]] : [];
|
|
560
655
|
});
|
|
561
656
|
}
|
|
562
657
|
function boundEntryKeys(entry) {
|
|
563
658
|
return getProperties(entry).flatMap((property) => {
|
|
564
659
|
const key = getKey(property);
|
|
565
|
-
|
|
660
|
+
const value = bare(property.value);
|
|
661
|
+
return key !== null && !RESERVED_ENTRY_KEYS.includes(key) && isBound(value) ? [[key, property]] : [];
|
|
566
662
|
});
|
|
567
663
|
}
|
|
568
664
|
function getBindingsProps(node, rulesApi) {
|
|
@@ -617,13 +713,14 @@ function misplacedBuilders(element, rulesApi) {
|
|
|
617
713
|
return [];
|
|
618
714
|
const name = getName(attribute);
|
|
619
715
|
const value = getExpression(attribute);
|
|
620
|
-
if (!value || name === 'bindings'
|
|
716
|
+
if (!value || name === 'bindings')
|
|
621
717
|
return [];
|
|
622
718
|
if (name === 'elementProps')
|
|
623
719
|
return entryBuilders(value, rulesApi, '`elementProps`');
|
|
624
|
-
if (!
|
|
720
|
+
if (!isReservedAttribute(name) && isBindingValue(value, rulesApi))
|
|
625
721
|
return [];
|
|
626
|
-
|
|
722
|
+
const builders = name === 'className' && value.type === 'ArrayExpression' ? value.elements.flatMap((term) => conditionlessBuilders(term, rulesApi)) : buildersIn(value, rulesApi);
|
|
723
|
+
return builders.map((call) => [call, `\`${name}\``]);
|
|
627
724
|
});
|
|
628
725
|
}
|
|
629
726
|
function entryBuilders(elementProps, rulesApi, where) {
|
|
@@ -643,12 +740,19 @@ function entryBuilders(elementProps, rulesApi, where) {
|
|
|
643
740
|
return [];
|
|
644
741
|
if (key === 'elementProps')
|
|
645
742
|
return entryBuilders(property.value, rulesApi, `${name}'s \`elementProps\``);
|
|
646
|
-
if (!RESERVED_ENTRY_KEYS.includes(key) &&
|
|
743
|
+
if (!RESERVED_ENTRY_KEYS.includes(key) && isBindingValue(bare(property.value), rulesApi))
|
|
647
744
|
return [];
|
|
648
|
-
|
|
745
|
+
const builders = key === 'className' ? conditionlessBuilders(property.value, rulesApi) : buildersIn(property.value, rulesApi);
|
|
746
|
+
return builders.map((call) => [call, `${name}'s \`${key}\``]);
|
|
649
747
|
});
|
|
650
748
|
});
|
|
651
749
|
}
|
|
750
|
+
/** The builders in a `className` term outside its keys' conditions: an object term's `key: value` holds a condition, the rest is plain (parseJsx.ts classNameEntriesOf). */
|
|
751
|
+
function conditionlessBuilders(term, rulesApi) {
|
|
752
|
+
if (term?.type !== 'ObjectExpression')
|
|
753
|
+
return buildersIn(term, rulesApi);
|
|
754
|
+
return term.properties.flatMap((property) => property.type === 'Property' && !property.computed && getKey(property) !== null ? [] : buildersIn(property, rulesApi));
|
|
755
|
+
}
|
|
652
756
|
function buildersIn(node, rulesApi) {
|
|
653
757
|
if (!node || typeof node !== 'object' || !('type' in node))
|
|
654
758
|
return [];
|
|
@@ -666,6 +770,9 @@ function isInStructureAttribute(node) {
|
|
|
666
770
|
for (let up = node.parent; up.type !== 'Program'; up = up.parent) {
|
|
667
771
|
if (up.type === 'JSXAttribute')
|
|
668
772
|
inAttribute = true;
|
|
773
|
+
// A renderItem template and a map() or shape() callback are read in the structure, in their parameters' scope.
|
|
774
|
+
if (up.type === 'ArrowFunctionExpression' && getCallbackKind(up))
|
|
775
|
+
continue;
|
|
669
776
|
if (up.type === 'ArrowFunctionExpression' || up.type === 'FunctionExpression')
|
|
670
777
|
return inAttribute && isStructure(up);
|
|
671
778
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 1227, 1231, 1235, 1255, 1268, 1289, 1697; projectData.ts 214, 225, 281, 435, 443; enrichImage.ts 94, 105.
|
|
3
3
|
// Converter 0.81 (checked at 0.84, 2c0e21c): the tag's import, not a `type` attr, decides if a component takes data attributes; `type` is data.
|
|
4
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
-
import { DATA_KEY,
|
|
5
|
+
import { DATA_KEY, EVENT_ATTRIBUTE, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getProperty, isBound, isOneEditAway, isReservedAttribute, isStructure, ownerOf, } from "../rules-api/index.js";
|
|
6
6
|
import { defineRule } from "./define-rule.js";
|
|
7
7
|
/** The names the import never reads as data (dataKeys.ts). The React name is joined as the converter does: Wix Standards fails the build on it in any file. */
|
|
8
8
|
const EXCLUDED_DATA_KEYS = [
|
|
@@ -66,9 +66,9 @@ function checkValuesAreLiterals(input, skip) {
|
|
|
66
66
|
continue;
|
|
67
67
|
}
|
|
68
68
|
const value = getExpression(attribute);
|
|
69
|
-
if (!value || skip.includes(name) || (
|
|
69
|
+
if (!value || skip.includes(name) || (isReservedAttribute(name) && !READ_AS_DATA.includes(name)))
|
|
70
70
|
continue;
|
|
71
|
-
if (!
|
|
71
|
+
if (!isReservedAttribute(name) && (isExcluded(name) || nearMissOf(name) || isBound(value) || getCallbackKind(value) === 'template'))
|
|
72
72
|
continue;
|
|
73
73
|
const mode = name === 'props' ? PROPS : FULL;
|
|
74
74
|
const found = name === 'elementProps' ? nonLiteralInElementProps(value, mode, input.rulesApi, textOf(input)) : nonLiteralIn(value, mode, input.rulesApi, textOf(input));
|
|
@@ -107,7 +107,7 @@ function checkElementPropsEntries(input, map, path) {
|
|
|
107
107
|
for (const property of getProperties(map)) {
|
|
108
108
|
const name = getKey(property);
|
|
109
109
|
const at = [...path, name].join('.');
|
|
110
|
-
if (!checkEntryName(input, property, name) ||
|
|
110
|
+
if (!checkEntryName(input, property, name) || isRuntimeCall(bare(property.value), input.rulesApi))
|
|
111
111
|
continue;
|
|
112
112
|
// an object written out: a media call decodes to an object, but not to an inner part's settings
|
|
113
113
|
const entry = getObject(property.value);
|
|
@@ -138,10 +138,10 @@ function checkEntryName(input, property, name) {
|
|
|
138
138
|
function checkEntryKeysAreKnown(input, entry, at) {
|
|
139
139
|
for (const property of getProperties(entry)) {
|
|
140
140
|
const key = getKey(property);
|
|
141
|
-
if (key === null || key === '_id' || RESERVED_ENTRY_KEYS.includes(key) || bare(property.value)
|
|
141
|
+
if (key === null || key === '_id' || RESERVED_ENTRY_KEYS.includes(key) || isBound(bare(property.value)))
|
|
142
142
|
continue;
|
|
143
143
|
const typo = nearMissOf(key);
|
|
144
|
-
if (!
|
|
144
|
+
if (!isReservedAttribute(key) && !isExcluded(key) && !typo)
|
|
145
145
|
continue;
|
|
146
146
|
say(input, property.key, `${owner(input)}'s elementProps entry \`${at}\` has the key \`${key}\`, which an inner part doesn't take. Use its data keys, or \`data: {…}\`${typo ? ` (did you mean \`${typo}\`?)` : ''}.`);
|
|
147
147
|
}
|
|
@@ -151,7 +151,7 @@ function checkDataCarrier(input) {
|
|
|
151
151
|
const value = getExpression(data);
|
|
152
152
|
if (!value)
|
|
153
153
|
return null;
|
|
154
|
-
const merges = dataAttributes(input).length > 0 || (
|
|
154
|
+
const merges = dataAttributes(input).length > 0 || (input.rulesApi.hasDataAttributes(input.node) && getAttribute(input.node, '_id') !== undefined);
|
|
155
155
|
if (!merges)
|
|
156
156
|
return null;
|
|
157
157
|
if (!isObjectValue(value, input.rulesApi)) {
|
|
@@ -286,34 +286,23 @@ function imageCallsIn(input, dataValuesRead, read) {
|
|
|
286
286
|
if (value.type === 'ArrayExpression')
|
|
287
287
|
return value.elements.flatMap((element) => (element && element.type !== 'SpreadElement' ? calls(element) : []));
|
|
288
288
|
if (value.type === 'ObjectExpression')
|
|
289
|
-
return getProperties(value).flatMap((property) => (
|
|
289
|
+
return getProperties(value).flatMap((property) => (isRuntimeCall(bare(property.value), input.rulesApi) ? [] : calls(property.value)));
|
|
290
290
|
return [];
|
|
291
291
|
};
|
|
292
292
|
return values.flatMap(calls);
|
|
293
293
|
}
|
|
294
294
|
// ── reading the code ──
|
|
295
295
|
const getAttributes = (element) => element.openingElement.attributes.filter((attribute) => attribute.type === 'JSXAttribute');
|
|
296
|
-
const getObject = (node) => (node && bare(node).type === 'ObjectExpression' ? bare(node) : null);
|
|
297
|
-
/** The tags the import reads as the DM's own components ('document') or a global component's reference (identity.ts identityOfTag): no data attributes. */
|
|
298
|
-
const NO_DATA_TYPES = ['page', 'section', 'header', 'footer', 'headerLandmark', 'footerLandmark', ...GLOBAL_SECTIONS];
|
|
299
|
-
/** Converter 0.81: a builder component (its import gives a componentType) takes data attributes; the `type` attr is gone. */
|
|
300
|
-
function takesData({ node, rulesApi }) {
|
|
301
|
-
const found = rulesApi.getImport(node);
|
|
302
|
-
const type = rulesApi.getType(node);
|
|
303
|
-
return found !== null && found.kind !== 'core' && !found.source.startsWith('.') && !(type && NO_DATA_TYPES.includes(type));
|
|
304
|
-
}
|
|
305
296
|
/** The name the import gives the tag's component (parseJsx.ts namedImportOf): the imported name, else the tag's own (`<Box>`, `<ui.Box>`). */
|
|
306
297
|
function componentNameOf({ node, rulesApi }) {
|
|
307
298
|
const imported = rulesApi.getImport(node)?.imported;
|
|
308
299
|
return imported && imported !== 'default' && imported !== '*' ? imported : rulesApi.getTagName(node).split('.').at(-1);
|
|
309
300
|
}
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
const isDataName = (name) => !isReserved(name) && !isExcluded(name) && nearMissOf(name) === undefined;
|
|
313
|
-
const isExcluded = (name) => EXCLUDED_DATA_KEYS.includes(name) || name.startsWith('data-') || name.startsWith('aria-') || /^on/i.test(name) || !DATA_KEY.test(name);
|
|
301
|
+
const isDataName = (name) => !isReservedAttribute(name) && !isExcluded(name) && nearMissOf(name) === undefined;
|
|
302
|
+
const isExcluded = (name) => EXCLUDED_DATA_KEYS.includes(name) || name.startsWith('data-') || name.startsWith('aria-') || EVENT_ATTRIBUTE.test(name) || !DATA_KEY.test(name);
|
|
314
303
|
const nearMissOf = (name) => RESERVED_ATTRIBUTES.find((target) => isOneEditAway(name, target));
|
|
315
|
-
const dataAttributes = (input) => !
|
|
316
|
-
const entryDataKeys = (entry) => getProperties(entry).filter((property) => getKey(property) !== null && !RESERVED_ENTRY_KEYS.includes(getKey(property)) && bare(property.value)
|
|
304
|
+
const dataAttributes = (input) => !input.rulesApi.hasDataAttributes(input.node) ? [] : getAttributes(input.node).filter((attribute) => isDataName(getName(attribute)) && !isBound(getExpression(attribute)) && getCallbackKind(getExpression(attribute) ?? attribute) !== 'template');
|
|
305
|
+
const entryDataKeys = (entry) => getProperties(entry).filter((property) => getKey(property) !== null && !RESERVED_ENTRY_KEYS.includes(getKey(property)) && !isBound(bare(property.value)));
|
|
317
306
|
/** The attribute a data value's path starts at: `data.x` → `data`, `elementProps.nav.label` → `elementProps`. */
|
|
318
307
|
const attributeOf = (path) => path.split('.')[0];
|
|
319
308
|
function getEntries(map, path = []) {
|
|
@@ -357,7 +346,8 @@ function documentIdentifierOf(node, rulesApi) {
|
|
|
357
346
|
}
|
|
358
347
|
/** What `x` decodes to is an object: an object written out, or a media call (the import keeps it as an object). */
|
|
359
348
|
const isObjectValue = (node, rulesApi) => bare(node).type === 'ObjectExpression' || isImageCall(bare(node), rulesApi) || documentIdentifierOf(bare(node), rulesApi) !== null;
|
|
360
|
-
|
|
349
|
+
/** A call of any name from the runtime, a builder or not: the bindings rule reports one that isn't a builder. */
|
|
350
|
+
function isRuntimeCall(node, rulesApi) {
|
|
361
351
|
return node.type === 'CallExpression' && node.callee.type === 'Identifier' && rulesApi.getImport(node.callee)?.kind === 'runtime';
|
|
362
352
|
}
|
|
363
353
|
function nonLiteralIn(node, mode, rulesApi, getText, path = '') {
|
|
@@ -433,7 +423,7 @@ function nonLiteralInElementProps(node, mode, rulesApi, getText, path = '') {
|
|
|
433
423
|
const readEntry = (property, at) => {
|
|
434
424
|
const key = getKey(property);
|
|
435
425
|
const value = bare(property.value);
|
|
436
|
-
if (key === 'bindings' || key === 'slots' || key === 'className' || (!RESERVED_ENTRY_KEYS.includes(key) && value
|
|
426
|
+
if (key === 'bindings' || key === 'slots' || key === 'className' || (!RESERVED_ENTRY_KEYS.includes(key) && isBound(value)))
|
|
437
427
|
return null;
|
|
438
428
|
return key === 'elementProps' ? nonLiteralInElementProps(value, mode, rulesApi, getText, at) : nonLiteralIn(value, mode, rulesApi, getText, at);
|
|
439
429
|
};
|
|
@@ -499,10 +489,11 @@ function dataValues(input) {
|
|
|
499
489
|
}
|
|
500
490
|
return values;
|
|
501
491
|
}
|
|
492
|
+
/** In Structure's own JSX, or in a renderItem template, which the import reads in the row's scope (parseJsx.ts claimSlotChild). */
|
|
502
493
|
function isWrittenOut(element) {
|
|
503
494
|
for (let up = element.parent; up.type !== 'JSXElement' && up.type !== 'Program'; up = up.parent) {
|
|
504
495
|
if (up.type === 'ArrowFunctionExpression')
|
|
505
|
-
return isStructure(up);
|
|
496
|
+
return isStructure(up) || getCallbackKind(up) === 'template';
|
|
506
497
|
if (NOT_READ_IN.includes(up.type) || (up.type === 'JSXExpressionContainer' && up.parent.type !== 'JSXAttribute'))
|
|
507
498
|
return false;
|
|
508
499
|
}
|
package/dist/rules-api/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Part 3 — rules API: one linted file's page and the functions over it, and how a rule reports.
|
|
2
2
|
export { createRulesApi, getOrCreateRulesApi } from "./rules-api.js";
|
|
3
3
|
export { createCssFileReporter, describeAtRule, reportCssProblem } from "./report.js";
|
|
4
|
-
export { bare, getAttribute, getExpression, getKey, getName, getProperties, getProperty, isOneEditAway, isStructure, ownerOf, } from "../shared/utils.js";
|
|
5
|
-
export { BUILDERS, DATA_KEY, GLOBAL_SECTIONS, INSTANCE_PROPS, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE, SECTIONS, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, WRAPPERS, } from "../shared/constants.js";
|
|
4
|
+
export { bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getParamReadKind, getProperty, isBound, isOneEditAway, isReservedAttribute, isStructure, ownerOf, } from "../shared/utils.js";
|
|
5
|
+
export { BUILDERS, CALLBACK_PROP, DATA_KEY, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE, SECTIONS, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, WRAPPERS, } from "../shared/constants.js";
|
|
6
6
|
export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readSegment } from "../shared/css.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// One per file, shared by every rule on it. Only building its page reads from disk, once per file.
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { readPage } from "../page-model/index.js";
|
|
6
|
-
import { getAttributeElements as getHeldElements, getChildren, getId, getImport, getImportKind, getImportedName, getPropNames, getSlot, getTagName, getType, isTypeScript } from "../shared/utils.js";
|
|
6
|
+
import { getAttributeElements as getHeldElements, getChildren, getId, getImport, getImportKind, getImportedName, getPropNames, getSlot, getTagName, getType, hasDataAttributes, isTypeScript } from "../shared/utils.js";
|
|
7
7
|
import { readBreakpointName, readCustomMedia, readThemeTokens } from "../shared/css.js";
|
|
8
8
|
import { WRAPPERS } from "../shared/constants.js";
|
|
9
9
|
import { getEnclosingAtRules, isBaseDeclaration } from "./utils.js";
|
|
@@ -44,6 +44,7 @@ export function createRulesApi(page) {
|
|
|
44
44
|
getAttributeElements,
|
|
45
45
|
getSlot,
|
|
46
46
|
getImport: (node) => getImport(node.type === 'JSXElement' ? getTagName(node).split('.')[0] : node.name, getProgram(node)),
|
|
47
|
+
hasDataAttributes: (node) => hasDataAttributes(node, getProgram(node)),
|
|
47
48
|
getImportKind,
|
|
48
49
|
getImportedName,
|
|
49
50
|
isTypeScript,
|
package/dist/shared/constants.js
CHANGED
|
@@ -12,10 +12,14 @@ export const INSTANCE_PROPS = ['id', 'className'];
|
|
|
12
12
|
export const SYSTEM_PACKAGE = '@wix/origami-system-components';
|
|
13
13
|
/** The runtime package: the builders (`ctx`, `fn`…) and the `<Remove>` wrapper. */
|
|
14
14
|
export const RUNTIME_PACKAGE = '@wix/origami-runtime';
|
|
15
|
-
/** The
|
|
15
|
+
/** The typed builders (converter 0.76, PRIMITIVE_DATA_TYPES): `email('a@b.c')` is a fixed value of that type, like a literal. */
|
|
16
|
+
export const TYPED_BUILDERS = ['textEnum', 'phone', 'email', 'webUrl', 'hostname', 'regex', 'guid', 'direction', 'localDate', 'localTime', 'localDateTime', 'dateTime'];
|
|
17
|
+
/** The builders whose second argument is a callback, `map(source, (item) => ({…}))` (astValue.ts TRANSFORM_CALLBACKS). */
|
|
18
|
+
export const TRANSFORM_BUILDERS = ['map', 'shape'];
|
|
19
|
+
/** The runtime's builders: the converter's BINDINGS_BUILDER_ORDER without `Remove`, which is a tag (reservedAttributes.ts); `map`, `shape` and `callback` since converter 0.82–0.86. */
|
|
16
20
|
export const BUILDERS = [
|
|
17
|
-
'all', 'ctx', 'fn', 'is', 'raw', 'state',
|
|
18
|
-
|
|
21
|
+
'all', 'ctx', 'fn', 'is', 'raw', 'state', 'map', 'shape', 'callback',
|
|
22
|
+
...TYPED_BUILDERS,
|
|
19
23
|
];
|
|
20
24
|
/** The six builders no other package exports under these names; `email`, `regex`… may well come from a library for fn(). */
|
|
21
25
|
export const RUNTIME_ONLY_BUILDERS = ['all', 'ctx', 'fn', 'is', 'raw', 'state'];
|
|
@@ -29,5 +33,9 @@ export const SHARED_COMP_ID = 'sharedCompId';
|
|
|
29
33
|
export const RESERVED_ENTRY_KEYS = ['data', 'slots', 'elementProps', 'bindings', 'className'];
|
|
30
34
|
/** A data key is a JavaScript name (dataKeys.ts). */
|
|
31
35
|
export const DATA_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
36
|
+
/** An attribute the import reads as a React event, never as data: `on` in any case (dataKeys.ts REACT_EVENT_ATTR_PATTERN, converter 0.87). */
|
|
37
|
+
export const EVENT_ATTRIBUTE = /^on/i;
|
|
38
|
+
/** A callback prop, which the runtime hands a function: `on` and a capital letter, `onClick` but not `onclick` (the runtime's withOrigami.tsx). */
|
|
39
|
+
export const CALLBACK_PROP = /^on[A-Z]/;
|
|
32
40
|
/** By import source; the wrappers also by the imported name, since their packages export other things (`Show`, `Interact`). */
|
|
33
41
|
export const LAYER_ATTRIBUTES = ['backgroundLayer', 'pinnedLayer'];
|
package/dist/shared/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RUNTIME_PACKAGE, STRUCTURE, SYSTEM_PACKAGE } from "./constants.js";
|
|
1
|
+
import { GLOBAL_SECTIONS, RESERVED_ATTRIBUTES, RUNTIME_PACKAGE, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, TRANSFORM_BUILDERS } from "./constants.js";
|
|
2
2
|
import { GLOBAL_COMPONENTS, GLOBAL_MENUS } from "./site-files-tree-mapping.js";
|
|
3
3
|
const TYPE_BY_IMPORT_SOURCE = [
|
|
4
4
|
[new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?Page$`), 'page'],
|
|
@@ -24,6 +24,8 @@ const KIND_BY_IMPORT_SOURCE = [
|
|
|
24
24
|
[new RegExp(`^(\\.\\.?/)*${GLOBAL_MENUS}(/|$)`), 'menu'],
|
|
25
25
|
[/^document$/, 'core'],
|
|
26
26
|
[/^document\//, 'document'],
|
|
27
|
+
[/^@origami-wrappers\//, 'wrapper'],
|
|
28
|
+
[/^@site\/site-code\/[^/]+$/, 'siteCode'],
|
|
27
29
|
];
|
|
28
30
|
/** <Band> with `import { Section as Band } from '@wix/origami-system-components/Section'` → 'section'. `<Band.Item>` is Band's. A wrapper is the named export: `<Show>` from the runtime is null. */
|
|
29
31
|
export function getType(element, program) {
|
|
@@ -34,6 +36,15 @@ export function getType(element, program) {
|
|
|
34
36
|
const [, type, imported] = entry;
|
|
35
37
|
return imported === undefined || found.imported === imported ? type : null;
|
|
36
38
|
}
|
|
39
|
+
/** The tags the import reads as the DM's own components ('document') or a global component's reference (identity.ts identityOfTag): no data attributes. */
|
|
40
|
+
const NO_DATA_TYPES = ['page', 'section', 'header', 'footer', 'headerLandmark', 'footerLandmark', ...GLOBAL_SECTIONS];
|
|
41
|
+
/** Converter 0.81: are the tag's attributes saved as its component's data? Yes on a builder component (its import gives a componentType):
|
|
42
|
+
* imported, not from `document` and not from a local file. No on the DM's own tags (`Page`, the sections) and a global component's reference. */
|
|
43
|
+
export function hasDataAttributes(element, program) {
|
|
44
|
+
const found = getImport(getTagName(element).split('.')[0], program);
|
|
45
|
+
const type = getType(element, program);
|
|
46
|
+
return found !== null && found.kind !== 'core' && !found.source.startsWith('.') && !(type && NO_DATA_TYPES.includes(type));
|
|
47
|
+
}
|
|
37
48
|
/** What an import's source is: 'runtime' for @wix/origami-runtime, 'interact' for @wix/interact/…; null for anything else. */
|
|
38
49
|
export const getImportKind = (source) => KIND_BY_IMPORT_SOURCE.find(([pattern]) => pattern.test(source))?.[1] ?? null;
|
|
39
50
|
/** Where a name of the file comes from; null when no import brings it in. */
|
|
@@ -111,6 +122,15 @@ export function getExpression(attribute) {
|
|
|
111
122
|
return null;
|
|
112
123
|
return bare(value.expression);
|
|
113
124
|
}
|
|
125
|
+
/** The object written out in a value, in parentheses or in an attribute's `{…}`: `x={{ a: 1 }}` → `{ a: 1 }`; null for any other value. */
|
|
126
|
+
export function getObject(node) {
|
|
127
|
+
if (!node)
|
|
128
|
+
return null;
|
|
129
|
+
const value = node.type === 'JSXAttribute' ? getExpression(node) : bare(node);
|
|
130
|
+
return value?.type === 'ObjectExpression' ? value : null;
|
|
131
|
+
}
|
|
132
|
+
/** Not the component's data: the converter's reserved attributes, and `sharedCompId` (reservedAttributes.ts). */
|
|
133
|
+
export const isReservedAttribute = (name) => RESERVED_ATTRIBUTES.includes(name) || name === SHARED_COMP_ID;
|
|
114
134
|
/** The node inside its parentheses: `((x))` → `x`. */
|
|
115
135
|
export function bare(node) {
|
|
116
136
|
return node.type === 'ParenthesizedExpression' ? bare(node.expression) : node;
|
|
@@ -131,6 +151,52 @@ export function isStructure(node) {
|
|
|
131
151
|
return declarator?.type === 'VariableDeclarator' && declarator.id?.type === 'Identifier' && declarator.id.name === STRUCTURE
|
|
132
152
|
&& declarator.parent?.parent?.type === 'ExportNamedDeclaration' && declarator.parent.parent.parent?.type === 'Program';
|
|
133
153
|
}
|
|
154
|
+
/** The node's parent, past parentheses, and its child on the way up: the node itself, or the parentheses around it. */
|
|
155
|
+
function getOuter(node) {
|
|
156
|
+
let child = node;
|
|
157
|
+
while (child.parent?.type === 'ParenthesizedExpression')
|
|
158
|
+
child = child.parent;
|
|
159
|
+
return [child.parent, child];
|
|
160
|
+
}
|
|
161
|
+
/** The callbacks whose parameters the import reads as bindings: 'template' for a slot's template, `renderItem={(item, index, length) => <X />}`, an attribute's
|
|
162
|
+
* arrow that returns one element (parseJsx.ts templateCallbackOf); 'transform' for `map(source, (item, index) => ({…}))` and `shape(source, (obj) => ({…}))`
|
|
163
|
+
* (astValue.ts transformCallbackOf). null for any other node. */
|
|
164
|
+
export function getCallbackKind(syntax) {
|
|
165
|
+
const node = syntax;
|
|
166
|
+
if (node.type !== 'ArrowFunctionExpression')
|
|
167
|
+
return null;
|
|
168
|
+
let body = node.body;
|
|
169
|
+
while (body?.type === 'ParenthesizedExpression')
|
|
170
|
+
body = body.expression;
|
|
171
|
+
const [outer, child] = getOuter(node);
|
|
172
|
+
if (body?.type === 'JSXElement' && outer?.type === 'JSXExpressionContainer' && outer.parent?.type === 'JSXAttribute')
|
|
173
|
+
return 'template';
|
|
174
|
+
const isTransform = outer?.type === 'CallExpression' && outer.callee?.type === 'Identifier' && TRANSFORM_BUILDERS.includes(outer.callee.name) && outer.arguments?.[1] === child;
|
|
175
|
+
return isTransform ? 'transform' : null;
|
|
176
|
+
}
|
|
177
|
+
/** A read of a callback's parameter, `item`, `item.name`, `item['link-1']`, which the import reads as a binding (astValue.ts templateReadOf):
|
|
178
|
+
* 'template' off a renderItem's parameter, 'transform' off a map() or shape() callback's. null for any other value, or inside any other function. */
|
|
179
|
+
export function getParamReadKind(syntax) {
|
|
180
|
+
const node = syntax;
|
|
181
|
+
let root = node;
|
|
182
|
+
while (root.type === 'MemberExpression' && root.object)
|
|
183
|
+
root = root.object;
|
|
184
|
+
if (root.type !== 'Identifier')
|
|
185
|
+
return null;
|
|
186
|
+
for (let up = node.parent; up && up.type !== 'Program'; up = up.parent) {
|
|
187
|
+
if (up.type !== 'ArrowFunctionExpression' && up.type !== 'FunctionExpression')
|
|
188
|
+
continue;
|
|
189
|
+
const kind = getCallbackKind(up);
|
|
190
|
+
if (!kind)
|
|
191
|
+
return null;
|
|
192
|
+
if (up.params?.some((param) => param.type === 'Identifier' && param.name === root.name))
|
|
193
|
+
return kind;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
/** A call, or a read of a renderItem / map() parameter (`item.name`): the import reads it as a binding, not as data (parseJsx.ts takeBoundProp).
|
|
198
|
+
* Any call: the bindings rule reports one that isn't a builder. */
|
|
199
|
+
export const isBound = (value) => !!value && (value.type === 'CallExpression' || getParamReadKind(value) !== null);
|
|
134
200
|
/** A component in a message: `<Image>` img, or `<Image>` when it has no id. */
|
|
135
201
|
export function ownerOf(element) {
|
|
136
202
|
const id = getId(element);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-lint",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.7",
|
|
4
4
|
"description": "Offline lint for an exported Origami site: lints the pages that changed since the last passing run",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test": "node --test 'test/*.test.ts'"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@wix/origami-fonts": "1.0.
|
|
24
|
+
"@wix/origami-fonts": "1.0.2",
|
|
25
25
|
"oxc-parser": "^0.151.0",
|
|
26
26
|
"oxlint": "^1.39.0",
|
|
27
27
|
"postcss": "^8.5.3"
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
]
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
|
-
"falconPackageHash": "
|
|
54
|
+
"falconPackageHash": "0f6cfa38e9a6a4139df372764355a226553e82d331beca19c870ed7b"
|
|
55
55
|
}
|