@wix/origami-lint 0.0.0-stage → 1.34.2

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,511 @@
1
+ // The "data-values" subject of the converter rules (converter 0.80 at c8ed79b): astValue.ts 99, 123, 133; parseJsx.ts 915, 1223,
2
+ // 1227, 1231, 1235, 1255, 1268, 1289, 1697; projectData.ts 214, 225, 281, 435, 443; enrichImage.ts 94, 105.
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
+ import { isDeepStrictEqual } from 'node:util';
5
+ import { DATA_KEY, GLOBAL_SECTIONS, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, SHARED_COMP_ID, bare, getAttribute, getExpression, getKey, getName, getProperties, getProperty, isOneEditAway, isStructure, ownerOf, } from "../rules-api/index.js";
6
+ import { defineRule } from "./define-rule.js";
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
+ const EXCLUDED_DATA_KEYS = [
9
+ '_slotsId', '_mapId', 'hidden', 'class', 'title', 'role', 'tabindex', 'draggable', 'lang', 'dir', 'accesskey', 'contenteditable', 'spellcheck',
10
+ 'translate', 'autofocus', 'inputmode', 'enterkeyhint', 'popover', 'inert', 'part', 'nonce', 'is', 'itemscope', 'itemtype', 'itemprop', 'itemid',
11
+ 'itemref', 'tabIndex', 'contentEditable', 'accessKey', 'autoFocus', 'inputMode', 'enterKeyHint', 'spellCheck', 'children', 'styleRef', 'key',
12
+ 'ref', ['dangerouslySet', 'InnerHTML'].join(''), '__proto__', 'constructor', 'prototype',
13
+ ];
14
+ /** An elementProps entry's name is a CSS identifier (parseJsx.ts 1231): it becomes a class. */
15
+ const CSS_IDENTIFIER = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/;
16
+ /** Keys no object the import reads may have. */
17
+ const RESERVED_OBJECT_KEYS = ['__proto__', 'constructor', 'prototype'];
18
+ /** An image object's `type` (enrichImage.ts). */
19
+ const IMAGE_TYPE = 'Builder.Image';
20
+ /** The media identifiers the export writes: an image's (`wix:image://v1/<uri>/<name>#originWidth=…`), a document's (`wix:document://v1/ugd/<file>/<name>`). */
21
+ const IMAGE_PREFIX = 'wix:image://v1/';
22
+ const DOCUMENT_PREFIX = 'wix:document://v1/';
23
+ /** The reserved attributes this rule reads as data. The strings are component-attributes', `className` and `bindings` the bindings rule's. */
24
+ const READ_AS_DATA = ['data', 'props', 'elementProps'];
25
+ const FULL = { menus: true, media: true, jsx: true };
26
+ const PROPS = { menus: true, media: true };
27
+ const PLAIN = {};
28
+ const IMAGE_URL = 'getCroppedImageUrl';
29
+ const DOCUMENT_URL = 'getDocumentUrl';
30
+ const DOCUMENT_DIRECTORY = /\/(archives|ugd)\//;
31
+ const LINK_KEYS = ['href', 'target', 'rel', 'pageRole'];
32
+ const WRAPPER_KEYS = ['type', 'id', 'data'];
33
+ const LITERAL_HINT = 'The import reads it as data: write the value itself, a string, a number, true / false, null, or an object or array of these.';
34
+ export default defineRule({
35
+ name: 'data-values',
36
+ editors: ['studio2'],
37
+ callbacks: {
38
+ JSXElement: checkJSXElementDataValues,
39
+ },
40
+ });
41
+ // ── the callback's steps ──
42
+ /** A component written out under Structure: its data values, then what the import builds from the ones it read. */
43
+ function checkJSXElementDataValues(input) {
44
+ if (!input.rulesApi.getComponent(input.node) || !isWrittenOut(input.node))
45
+ return;
46
+ const elementProps = getAttribute(input.node, 'elementProps');
47
+ const keysOnce = !elementProps || checkEntryKeysOnce(input, elementProps);
48
+ // an attribute with a value the import can't read stops there: nothing is built from it
49
+ const read = checkValuesAreLiterals(input, keysOnce ? [] : ['elementProps']);
50
+ if (elementProps && read.has('elementProps'))
51
+ checkElementPropsShape(input, elementProps);
52
+ checkDataKeyOnce(input, read.has('data') ? checkDataCarrier(input) : null);
53
+ checkDocumentLinks(input, read);
54
+ checkImageUrls(input, read);
55
+ }
56
+ // ── the steps ──
57
+ const say = ({ report }, node, message) => report({ loc: node.loc, message });
58
+ const textOf = ({ context }) => (node) => context.sourceCode.getText(node);
59
+ const owner = ({ node }) => ownerOf(node);
60
+ function checkValuesAreLiterals(input, skip) {
61
+ const read = new Set();
62
+ for (const attribute of getAttributes(input.node)) {
63
+ const name = getName(attribute);
64
+ if (READ_AS_DATA.includes(name) && attribute.value?.type !== 'JSXExpressionContainer') {
65
+ say(input, attribute, `\`${name}\` on ${owner(input)} isn't in braces. The import reads it only from braces: write \`${name}={…}\`.`);
66
+ continue;
67
+ }
68
+ const value = getExpression(attribute);
69
+ if (!value || skip.includes(name) || (isReserved(name) && !READ_AS_DATA.includes(name)))
70
+ continue;
71
+ if (!isReserved(name) && (isExcluded(name) || nearMissOf(name) || value.type === 'CallExpression'))
72
+ continue;
73
+ const mode = name === 'props' ? PROPS : FULL;
74
+ const found = name === 'elementProps' ? nonLiteralInElementProps(value, mode, input.rulesApi, textOf(input)) : nonLiteralIn(value, mode, input.rulesApi, textOf(input));
75
+ if (found)
76
+ say(input, found.node, `\`${name}\` on ${owner(input)} holds ${found.what}. ${LITERAL_HINT}`);
77
+ else
78
+ read.add(name);
79
+ }
80
+ return read;
81
+ }
82
+ function checkEntryKeysOnce(input, attribute) {
83
+ for (const { entry, path } of getEntries(getObject(getExpression(attribute)))) {
84
+ const seen = new Set();
85
+ for (const property of getProperties(entry)) {
86
+ const key = getKey(property);
87
+ if (key === null)
88
+ continue;
89
+ if (seen.has(key)) {
90
+ say(input, property, `${owner(input)}'s elementProps entry \`${path.join('.')}\` has the key \`${key}\` twice. Keep one.`);
91
+ return false;
92
+ }
93
+ seen.add(key);
94
+ }
95
+ }
96
+ return true;
97
+ }
98
+ function checkElementPropsShape(input, attribute) {
99
+ const value = getExpression(attribute);
100
+ // an object written out: a media call decodes to an object, but its keys aren't inner parts
101
+ const map = getObject(value);
102
+ if (!map)
103
+ return say(input, value, `\`elementProps\` on ${owner(input)} isn't an object. Write an object of inner parts: \`elementProps={{ name: {…} }}\`.`);
104
+ checkElementPropsEntries(input, map, []);
105
+ }
106
+ function checkElementPropsEntries(input, map, path) {
107
+ for (const property of getProperties(map)) {
108
+ const name = getKey(property);
109
+ const at = [...path, name].join('.');
110
+ if (!checkEntryName(input, property, name) || builderOf(bare(property.value), input.rulesApi))
111
+ continue;
112
+ // an object written out: a media call decodes to an object, but not to an inner part's settings
113
+ const entry = getObject(property.value);
114
+ if (!entry) {
115
+ say(input, property.value, `${owner(input)}'s elementProps entry \`${at}\` isn't an object. Write the inner part's settings as an object: \`${name}: {…}\`.`);
116
+ continue;
117
+ }
118
+ checkEntryKeysAreKnown(input, entry, at);
119
+ const data = getProperty(entry, 'data');
120
+ if (data && !isObjectValue(data.value, input.rulesApi))
121
+ say(input, data.value, `${owner(input)}'s elementProps entry \`${at}\` has \`data\` that isn't an object. Write \`data: {…}\`.`);
122
+ const nested = getProperty(entry, 'elementProps');
123
+ if (!nested)
124
+ continue;
125
+ const nestedMap = getObject(nested.value);
126
+ if (!nestedMap)
127
+ say(input, nested.value, `${owner(input)}'s elementProps entry \`${at}\` has \`elementProps\` that isn't an object. Write \`elementProps: { name: {…} }\`.`);
128
+ else
129
+ checkElementPropsEntries(input, nestedMap, [...path, name]);
130
+ }
131
+ }
132
+ function checkEntryName(input, property, name) {
133
+ const what = name === '' ? 'an empty name' : name.includes('|') ? `the name \`${name}\`, which has a \`|\`` : !CSS_IDENTIFIER.test(name) || RESERVED_OBJECT_KEYS.includes(name) ? `the name \`${name}\`, which isn't a CSS identifier` : null;
134
+ if (what)
135
+ say(input, property.key, `${owner(input)} has an elementProps entry with ${what}. Name it as the inner part is named, like \`navbar\`.`);
136
+ return what === null;
137
+ }
138
+ function checkEntryKeysAreKnown(input, entry, at) {
139
+ for (const property of getProperties(entry)) {
140
+ const key = getKey(property);
141
+ if (key === null || key === '_id' || RESERVED_ENTRY_KEYS.includes(key) || bare(property.value).type === 'CallExpression')
142
+ continue;
143
+ const typo = nearMissOf(key);
144
+ if (!isReserved(key) && !isExcluded(key) && !typo)
145
+ continue;
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
+ }
148
+ }
149
+ function checkDataCarrier(input) {
150
+ const data = getAttribute(input.node, 'data');
151
+ const value = getExpression(data);
152
+ if (!value)
153
+ return null;
154
+ const merges = dataAttributes(input).length > 0 || (takesData(input) && getAttribute(input.node, '_id') !== undefined);
155
+ if (!merges)
156
+ return null;
157
+ if (!isObjectValue(value, input.rulesApi)) {
158
+ say(input, value, `${owner(input)} has data attributes, so its \`data\` must be an object: \`data={{…}}\`, not \`data={${textOf(input)(value)}}\`.`);
159
+ return null;
160
+ }
161
+ const carrier = getObject(value);
162
+ if (carrier && isWrapper(input, carrier)) {
163
+ say(input, data, `${owner(input)} mixes data attributes with the \`{ type, data }\` wrapper in \`data={…}\`. Move the wrapper's keys to attributes.`);
164
+ return null;
165
+ }
166
+ return carrier;
167
+ }
168
+ function isWrapper(input, carrier) {
169
+ const keys = getProperties(carrier).map(getKey);
170
+ if (keys.length !== carrier.properties.length || !keys.every((key) => key !== null && WRAPPER_KEYS.includes(key)))
171
+ return false;
172
+ const type = valueOf(getProperty(carrier, 'type')?.value ?? null);
173
+ const id = getProperty(carrier, 'id');
174
+ const name = componentNameOf(input);
175
+ return typeof type === 'string' && (type === `${name}.Data` || type.endsWith(`.${name}.Data`))
176
+ && getObject(getProperty(carrier, 'data')?.value) !== null && (!id || typeof valueOf(id.value) === 'string');
177
+ }
178
+ function checkDataKeyOnce(input, carrier) {
179
+ if (carrier) {
180
+ const keys = new Set(getProperties(carrier).map(getKey));
181
+ const attributes = [...dataAttributes(input), getAttribute(input.node, '_id')].filter((attribute) => attribute !== undefined);
182
+ for (const attribute of attributes) {
183
+ const name = getName(attribute);
184
+ if (keys.has(name))
185
+ say(input, attribute, `${owner(input)} has \`${name}\` both as an attribute and inside \`data={{…}}\`. Keep only one.`);
186
+ }
187
+ }
188
+ for (const { entry, path } of getEntries(getObject(getExpression(getAttribute(input.node, 'elementProps'))))) {
189
+ const data = getObject(getProperty(entry, 'data')?.value);
190
+ if (!data)
191
+ continue;
192
+ const keys = new Set(getProperties(data).map(getKey));
193
+ for (const property of entryDataKeys(entry)) {
194
+ const key = getKey(property);
195
+ if (keys.has(key))
196
+ say(input, property, `${owner(input)}'s elementProps entry \`${path.join('.')}\` has \`${key}\` both as a key and inside its \`data\`. Keep only one.`);
197
+ }
198
+ }
199
+ }
200
+ /** Not in `data` or an entry's `data`: the import stores them as they are. */
201
+ function checkDocumentLinks(input, read) {
202
+ const values = dataAttributes(input).filter((attribute) => read.has(getName(attribute))).map((attribute) => getExpression(attribute)).filter(Boolean);
203
+ const elementProps = read.has('elementProps') ? getExpression(getAttribute(input.node, 'elementProps')) : null;
204
+ for (const { entry } of getEntries(getObject(elementProps))) {
205
+ values.push(...entryDataKeys(entry).filter((property) => getKey(property) !== '_id').map((property) => property.value));
206
+ }
207
+ for (const value of values)
208
+ for (const link of linksIn(value, input))
209
+ checkDocumentHref(input, link);
210
+ }
211
+ function linksIn(node, input) {
212
+ const value = bare(node);
213
+ if (value.type === 'ArrayExpression')
214
+ return value.elements.flatMap((element) => (element && element.type !== 'SpreadElement' ? linksIn(element, input) : []));
215
+ if (value.type !== 'ObjectExpression')
216
+ return [];
217
+ if (isLink(value, input))
218
+ return [value];
219
+ return getProperties(value).flatMap((property) => linksIn(property.value, input));
220
+ }
221
+ function isLink(object, input) {
222
+ const properties = getProperties(object);
223
+ if (properties.length !== object.properties.length || !properties.every((property) => LINK_KEYS.includes(getKey(property) ?? '')))
224
+ return false;
225
+ const href = getProperty(object, 'href')?.value;
226
+ return href !== undefined && (typeof valueOf(href) === 'string' || documentIdentifierOf(bare(href), input.rulesApi) !== null);
227
+ }
228
+ function checkDocumentHref(input, link) {
229
+ const href = getProperty(link, 'href');
230
+ const bareHref = valueOf(href.value);
231
+ if (typeof bareHref === 'string') {
232
+ if (bareHref.startsWith(DOCUMENT_PREFIX))
233
+ say(input, href, `${owner(input)}'s link href is a bare 'wix:document://' string. Write \`href: media.getDocumentUrl('wix:document://…').url\`, with media imported from '@wix/sdk'.`);
234
+ return;
235
+ }
236
+ const identifier = documentIdentifierOf(bare(href.value), input.rulesApi);
237
+ if (!hasDocumentId(identifier))
238
+ say(input, href, `${owner(input)}'s document link has no document id in '${identifier}'. Use the identifier the export wrote: \`media.getDocumentUrl('wix:document://v1/ugd/<file>/<name>').url\`.`);
239
+ }
240
+ /** documentLink.ts rebuildDocumentLink: the first `/ugd/` or `/archives/` goes, then `<id>/<name>`; the name is URI-decoded. */
241
+ function hasDocumentId(identifier) {
242
+ const [id, name = ''] = identifier.replace(DOCUMENT_DIRECTORY, '/').slice(DOCUMENT_PREFIX.length).split('/');
243
+ try {
244
+ decodeURIComponent(name);
245
+ }
246
+ catch {
247
+ return false;
248
+ }
249
+ return id !== '';
250
+ }
251
+ function checkImageUrls(input, read) {
252
+ const urls = new Set();
253
+ const values = dataValues(input).filter(([, path]) => read.has(attributeOf(path)));
254
+ for (const [value, path] of values) {
255
+ for (const { node, path: at } of objectsIn(value, path)) {
256
+ if (valueOf(getProperty(node, 'type')?.value ?? null) !== IMAGE_TYPE)
257
+ continue;
258
+ const url = getProperty(node, 'url');
259
+ if (!url)
260
+ continue;
261
+ urls.add(bare(url.value));
262
+ const fields = valueOf({ ...node, properties: getProperties(node).filter((property) => property !== url) });
263
+ if (!isValue(fields))
264
+ continue;
265
+ const call = bare(url.value);
266
+ if (isImageCall(call, input.rulesApi) && sameArguments(call, derivedImageArguments(fields)))
267
+ continue;
268
+ say(input, url, `${owner(input)}'s \`${at}.url\` isn't the url the image's fields give. It is derived: change the image's uri, width, height, name or crop, and drop \`url\`.`);
269
+ }
270
+ }
271
+ const stray = imageCallsIn(input, values, read).find((call) => !urls.has(call));
272
+ if (stray)
273
+ say(input, stray, `${owner(input)} has \`media.getCroppedImageUrl(…)\` outside an image's url. Write it only as the \`url\` of a \`{ type: 'Builder.Image', … }\` object.`);
274
+ }
275
+ function imageCallsIn(input, dataValuesRead, read) {
276
+ const values = dataValuesRead.map(([value]) => value);
277
+ const props = read.has('props') ? getExpression(getAttribute(input.node, 'props')) : null;
278
+ if (props)
279
+ values.push(props);
280
+ const calls = (node) => {
281
+ if (node === null)
282
+ return [];
283
+ const value = bare(node);
284
+ if (isMediaCall(value, 'getCroppedImageUrl', input.rulesApi))
285
+ return isImageCall(value, input.rulesApi) && value.arguments.every((argument) => isValue(valueOf(argument))) ? [value] : [];
286
+ if (value.type === 'ArrayExpression')
287
+ return value.elements.flatMap((element) => (element && element.type !== 'SpreadElement' ? calls(element) : []));
288
+ if (value.type === 'ObjectExpression')
289
+ return getProperties(value).flatMap((property) => (builderOf(bare(property.value), input.rulesApi) ? [] : calls(property.value)));
290
+ return [];
291
+ };
292
+ return values.flatMap(calls);
293
+ }
294
+ // ── reading the code ──
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
+ /** The name the import gives the tag's component (parseJsx.ts namedImportOf): the imported name, else the tag's own (`<Box>`, `<ui.Box>`). */
306
+ function componentNameOf({ node, rulesApi }) {
307
+ const imported = rulesApi.getImport(node)?.imported;
308
+ return imported && imported !== 'default' && imported !== '*' ? imported : rulesApi.getTagName(node).split('.').at(-1);
309
+ }
310
+ /** Not the component's data: the converter's reserved attributes, and `sharedCompId`. */
311
+ const isReserved = (name) => RESERVED_ATTRIBUTES.includes(name) || name === SHARED_COMP_ID;
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);
314
+ const nearMissOf = (name) => RESERVED_ATTRIBUTES.find((target) => isOneEditAway(name, target));
315
+ const dataAttributes = (input) => !takesData(input) ? [] : getAttributes(input.node).filter((attribute) => isDataName(getName(attribute)) && getExpression(attribute)?.type !== 'CallExpression');
316
+ const entryDataKeys = (entry) => getProperties(entry).filter((property) => getKey(property) !== null && !RESERVED_ENTRY_KEYS.includes(getKey(property)) && bare(property.value).type !== 'CallExpression');
317
+ /** The attribute a data value's path starts at: `data.x` → `data`, `elementProps.nav.label` → `elementProps`. */
318
+ const attributeOf = (path) => path.split('.')[0];
319
+ function getEntries(map, path = []) {
320
+ if (!map)
321
+ return [];
322
+ return getProperties(map).flatMap((property) => {
323
+ const key = getKey(property);
324
+ const entry = getObject(property.value);
325
+ if (key === null || !entry)
326
+ return [];
327
+ const at = [...path, key];
328
+ return [{ entry, path: at }, ...getEntries(getObject(getProperty(entry, 'elementProps')?.value), at)];
329
+ });
330
+ }
331
+ const isProperty = (node, name) => node.type === 'MemberExpression' && !node.computed && !node.optional && node.property.type === 'Identifier' && node.property.name === name;
332
+ const isImportedAs = (node, rulesApi, kind, imported) => {
333
+ if (node.type !== 'Identifier')
334
+ return false;
335
+ const found = rulesApi.getImport(node);
336
+ return found?.kind === kind && (imported === undefined || found.imported === imported);
337
+ };
338
+ function isMediaCall(node, method, rulesApi) {
339
+ if (node.type !== 'CallExpression' || node.optional || node.callee.type !== 'MemberExpression' || !isProperty(node.callee, method))
340
+ return false;
341
+ return isImportedAs(node.callee.object, rulesApi, 'sdk', 'media');
342
+ }
343
+ function isImageCall(node, rulesApi) {
344
+ if (!isMediaCall(node, IMAGE_URL, rulesApi) || node.arguments.length !== 8)
345
+ return false;
346
+ const id = valueOf(node.arguments[0]);
347
+ return typeof id === 'string' && id.startsWith(IMAGE_PREFIX);
348
+ }
349
+ function documentIdentifierOf(node, rulesApi) {
350
+ if (!isProperty(node, 'url') || node.type !== 'MemberExpression')
351
+ return null;
352
+ const call = node.object;
353
+ if (!isMediaCall(call, DOCUMENT_URL, rulesApi) || call.arguments.length !== 1)
354
+ return null;
355
+ const identifier = valueOf(call.arguments[0]);
356
+ return typeof identifier === 'string' && identifier.startsWith(DOCUMENT_PREFIX) ? identifier : null;
357
+ }
358
+ /** What `x` decodes to is an object: an object written out, or a media call (the import keeps it as an object). */
359
+ const isObjectValue = (node, rulesApi) => bare(node).type === 'ObjectExpression' || isImageCall(bare(node), rulesApi) || documentIdentifierOf(bare(node), rulesApi) !== null;
360
+ function builderOf(node, rulesApi) {
361
+ return node.type === 'CallExpression' && node.callee.type === 'Identifier' && rulesApi.getImport(node.callee)?.kind === 'runtime';
362
+ }
363
+ function nonLiteralIn(node, mode, rulesApi, getText, path = '') {
364
+ if (node === null)
365
+ return null;
366
+ const value = bare(node);
367
+ switch (value.type) {
368
+ case 'Literal':
369
+ return ['string', 'number', 'boolean'].includes(typeof value.value) || value.raw === 'null' ? null : { node: value, what: `\`${value.raw}\`, which isn't a string, number, true / false or null` };
370
+ case 'UnaryExpression':
371
+ return value.operator === '-' && value.argument.type === 'Literal' && typeof value.argument.value === 'number' ? null : { node: value, what: `\`${getText(value)}\`` };
372
+ case 'Identifier':
373
+ if (mode.menus && isImportedAs(value, rulesApi, 'menu'))
374
+ return null;
375
+ return { node: value, what: `the variable \`${value.name}\`` };
376
+ case 'MemberExpression':
377
+ if (mode.menus && isProperty(value, 'items') && isImportedAs(value.object, rulesApi, 'menu'))
378
+ return null;
379
+ if (mode.media && documentIdentifierOf(value, rulesApi) !== null)
380
+ return null;
381
+ return { node: value, what: `\`${getText(value)}\`` };
382
+ case 'CallExpression':
383
+ if (value.callee.type === 'Identifier')
384
+ return null;
385
+ if (mode.media && isImageCall(value, rulesApi))
386
+ return value.arguments.map((argument) => nonLiteralIn(argument, PLAIN, rulesApi, getText)).find(Boolean) ?? null;
387
+ return { node: value, what: `a call to \`${getText(value.callee)}\`` };
388
+ case 'JSXElement':
389
+ return mode.jsx ? null : { node: value, what: 'an element' };
390
+ case 'FunctionExpression':
391
+ case 'ArrowFunctionExpression':
392
+ return { node: value, what: 'a function' };
393
+ case 'TemplateLiteral':
394
+ return { node: value, what: `a template string (\`${getText(value)}\`)` };
395
+ case 'ArrayExpression':
396
+ if (value.elements.some((element) => element === null || element.type === 'SpreadElement'))
397
+ return { node: value, what: 'an array with a spread or an empty place (`[a, , b]`)' };
398
+ return value.elements.map((element) => nonLiteralIn(element, mode, rulesApi, getText)).find(Boolean) ?? null;
399
+ case 'ObjectExpression':
400
+ return nonLiteralInObject(value, mode, rulesApi, getText, path);
401
+ default:
402
+ return { node: value, what: `\`${getText(value)}\`` };
403
+ }
404
+ }
405
+ /** Each level is checked whole (its entries, then its keys once) before the values inside it, as the import does. */
406
+ function nonLiteralInObject(object, mode, rulesApi, getText, path, readValue) {
407
+ const keys = new Set();
408
+ for (const property of object.properties) {
409
+ if (property.type === 'SpreadElement')
410
+ return { node: property, what: `a spread (\`${getText(property)}\`)` };
411
+ if (property.computed || property.kind !== 'init')
412
+ return { node: property, what: `\`${getText(property)}\`, which isn't a \`key: value\` entry` };
413
+ const key = getKey(property);
414
+ if (key === null)
415
+ return { node: property, what: `the key \`${getText(property.key)}\`, which isn't a name or a string` };
416
+ const dotted = path ? `${path}.${key}` : key;
417
+ if (keys.has(key))
418
+ return { node: property, what: `the key \`${dotted}\` twice` };
419
+ keys.add(key);
420
+ }
421
+ const read = readValue ?? ((property, at) => nonLiteralIn(property.value, mode, rulesApi, getText, at));
422
+ for (const property of getProperties(object)) {
423
+ const found = read(property, path ? `${path}.${getKey(property)}` : getKey(property));
424
+ if (found)
425
+ return found;
426
+ }
427
+ return null;
428
+ }
429
+ function nonLiteralInElementProps(node, mode, rulesApi, getText, path = '') {
430
+ const map = getObject(node);
431
+ if (!map)
432
+ return nonLiteralIn(node, mode, rulesApi, getText, path);
433
+ const readEntry = (property, at) => {
434
+ const key = getKey(property);
435
+ const value = bare(property.value);
436
+ if (key === 'bindings' || key === 'slots' || key === 'className' || (!RESERVED_ENTRY_KEYS.includes(key) && value.type === 'CallExpression'))
437
+ return null;
438
+ return key === 'elementProps' ? nonLiteralInElementProps(value, mode, rulesApi, getText, at) : nonLiteralIn(value, mode, rulesApi, getText, at);
439
+ };
440
+ return nonLiteralInObject(map, mode, rulesApi, getText, path, (property, at) => {
441
+ const entry = getObject(property.value);
442
+ return entry ? nonLiteralInObject(entry, mode, rulesApi, getText, at, readEntry) : nonLiteralIn(property.value, mode, rulesApi, getText, at);
443
+ });
444
+ }
445
+ const NOT_A_VALUE = Symbol('not a value');
446
+ function valueOf(node) {
447
+ if (node === null)
448
+ return NOT_A_VALUE;
449
+ const value = bare(node);
450
+ if (value.type === 'Literal')
451
+ return value.raw === 'null' || ['string', 'number', 'boolean'].includes(typeof value.value) ? value.value : NOT_A_VALUE;
452
+ if (value.type === 'UnaryExpression')
453
+ return value.operator === '-' && value.argument.type === 'Literal' && typeof value.argument.value === 'number' ? -value.argument.value : NOT_A_VALUE;
454
+ if (value.type === 'ArrayExpression') {
455
+ const items = value.elements.map((element) => (element && element.type !== 'SpreadElement' ? valueOf(element) : NOT_A_VALUE));
456
+ return items.includes(NOT_A_VALUE) ? NOT_A_VALUE : items;
457
+ }
458
+ if (value.type !== 'ObjectExpression' || getProperties(value).length !== value.properties.length)
459
+ return NOT_A_VALUE;
460
+ const entries = getProperties(value).map((property) => [getKey(property), valueOf(property.value)]);
461
+ return entries.some(([key, item]) => key === null || item === NOT_A_VALUE) ? NOT_A_VALUE : Object.fromEntries(entries);
462
+ }
463
+ const isValue = (value) => value !== NOT_A_VALUE;
464
+ /** The arguments the export writes for an image's url (build/image/imageExpression.ts derivedImageExpressionOf). */
465
+ function derivedImageArguments(image) {
466
+ const { uri, name, crop } = image;
467
+ const width = image.width || 0;
468
+ const height = image.height || 0;
469
+ const id = uri ? `${IMAGE_PREFIX}${uri}${name ? `/${encodeURIComponent(name)}` : ''}#originWidth=${width}&originHeight=${height}` : undefined;
470
+ return [
471
+ id, crop?.x || 0, crop?.y || 0, crop?.width || 0, crop?.height || 0, crop?.width || width, crop?.height || height,
472
+ { ...(name !== undefined ? { name } : {}), autoEncode: true, devicePixelRatio: 1 },
473
+ ];
474
+ }
475
+ const sameArguments = (call, expected) => isDeepStrictEqual(call.arguments.map(valueOf), expected);
476
+ function objectsIn(node, path) {
477
+ if (node === null)
478
+ return [];
479
+ const value = bare(node);
480
+ if (value.type === 'ArrayExpression')
481
+ return value.elements.flatMap((element, index) => (element && element.type !== 'SpreadElement' ? objectsIn(element, `${path}[${index}]`) : []));
482
+ if (value.type !== 'ObjectExpression')
483
+ return [];
484
+ return [{ node: value, path }, ...getProperties(value).flatMap((property) => objectsIn(property.value, `${path}.${getKey(property)}`))];
485
+ }
486
+ function dataValues(input) {
487
+ const element = input.node;
488
+ const values = dataAttributes(input).map((attribute) => [getExpression(attribute), getName(attribute)]).filter(([value]) => value);
489
+ const data = getExpression(getAttribute(element, 'data'));
490
+ if (data)
491
+ values.push([data, 'data']);
492
+ for (const { entry, path } of getEntries(getObject(getExpression(getAttribute(element, 'elementProps'))))) {
493
+ const at = `elementProps.${path.join('.')}`;
494
+ for (const property of entryDataKeys(entry))
495
+ values.push([property.value, `${at}.${getKey(property)}`]);
496
+ const entryData = getProperty(entry, 'data');
497
+ if (entryData)
498
+ values.push([entryData.value, `${at}.data`]);
499
+ }
500
+ return values;
501
+ }
502
+ function isWrittenOut(element) {
503
+ for (let up = element.parent; up.type !== 'JSXElement' && up.type !== 'Program'; up = up.parent) {
504
+ if (up.type === 'ArrowFunctionExpression')
505
+ return isStructure(up);
506
+ if (NOT_READ_IN.includes(up.type) || (up.type === 'JSXExpressionContainer' && up.parent.type !== 'JSXAttribute'))
507
+ return false;
508
+ }
509
+ return true;
510
+ }
511
+ const NOT_READ_IN = ['FunctionExpression', 'CallExpression', 'ConditionalExpression', 'LogicalExpression'];
@@ -0,0 +1,4 @@
1
+ /** Nothing changes a rule once it's defined. */
2
+ export function defineRule(rule) {
3
+ return Object.freeze(rule);
4
+ }