@nodii/dev 0.6.21 → 0.6.23
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/core.js +1 -1
- package/dist/vendor/ds-conformance.mjs +199 -59
- package/package.json +1 -1
package/dist/core.js
CHANGED
|
@@ -4,7 +4,7 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
|
|
7
|
-
export const VERSION = "0.6.
|
|
7
|
+
export const VERSION = "0.6.23";
|
|
8
8
|
export const configPath = () =>
|
|
9
9
|
process.env.NODII_DEV_CONFIG_PATH ??
|
|
10
10
|
join(homedir(), ".config/nodii-dev/config.json");
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// it until it returns [] (each round can reveal instances inside slot
|
|
31
31
|
// content).
|
|
32
32
|
|
|
33
|
-
export const VERSION = "1.0.
|
|
33
|
+
export const VERSION = "1.0.10";
|
|
34
34
|
|
|
35
35
|
/** @typedef {{ nodeId: string, check: string, detail: string }} Finding */
|
|
36
36
|
/**
|
|
@@ -135,22 +135,18 @@ const NOT_A_TABLE = /\blist page\b/i;
|
|
|
135
135
|
const CUSTOMER_HEADER =
|
|
136
136
|
/^(customer|client|contact|lead|applicant|candidate)s?( name)?$/i;
|
|
137
137
|
const EXTERNAL_PLANE = /customer portal|partner room|external/i;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const SIGNED_IN = /^(signed in( as\b.*)?|not you\?.*)$/i;
|
|
151
|
-
// The signed-in account card (Account Context, kind=Account) shows the
|
|
152
|
-
// viewer's own email.
|
|
153
|
-
const SELF_COMPONENT = /^account context$/i;
|
|
138
|
+
const APP_FRAME_COMPONENT_SET_KEY = "e725461fd9786a3f1527ddee0ee764e9dcead46b";
|
|
139
|
+
const PERSON_NAME_COMPONENT_SET_KEY =
|
|
140
|
+
"08370f38bcbbcf88eaf3a6923dc91b5de5f38ea0";
|
|
141
|
+
const TABLE_COMPONENT_SET_KEY = "7ccc1dbb6ebd149fb9aaf42b9c638f5ce7169638";
|
|
142
|
+
const TABLE_CELL_COMPONENT_SET_KEY = "528898c19702ed4b169d023de30380e05169dfa5";
|
|
143
|
+
const ITEM_COMPONENT_SET_KEY = "46abc8cb0d57c22a365da94a03cfd72a0b089b87";
|
|
144
|
+
const SELF_COMPONENT_SET_KEYS = new Set([
|
|
145
|
+
"af8df2e85ba4d53b26b28ffa69f264758a50fe66", // Account Context
|
|
146
|
+
"470b2b5fd91dcae681d3d38c0297c71df3bb998f", // Account Menu
|
|
147
|
+
"8499452873bccfe5d2f29b5322fc1ca18cb103cc", // Profile Name Card
|
|
148
|
+
"6c1e074dcf69aee388e0300584da09735e314cc3", // Delete Account Card
|
|
149
|
+
]);
|
|
154
150
|
const NOT_A_PERSON = /^(you|the team)$/i;
|
|
155
151
|
const MASKED = /[*•●]/;
|
|
156
152
|
const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
@@ -164,6 +160,9 @@ const PHONE = /(?<![\w.+\-/])\+?\(?\d[\d ().-]{7,}\d(?![\w\-/])/g;
|
|
|
164
160
|
const LOCAL_PHONE = /^(\(\d{3,5}\)[ .-]?|\d{3,5}[ .-])\d{3,5}([ .-]\d{2,5})?$/;
|
|
165
161
|
const DATE = /^(\d{4}-\d{2}-\d{2}|\d{1,2}[-/.]\d{1,2}[-/.]\d{2,4})/;
|
|
166
162
|
const ROLE_ANNOTATION = /\[role:([a-z0-9._-]+)\]/gi;
|
|
163
|
+
// Published Dialog component-set key. A node id would break when the set is
|
|
164
|
+
// moved; the key is the stable reference shared across library consumers.
|
|
165
|
+
const DIALOG_COMPONENT_SET_KEY = "55d4155c4bce0136e9a3740ecc7174f9bd01727c";
|
|
167
166
|
// otherPersonRevealed is about people (Dev issue 116): a value is judged
|
|
168
167
|
// by the field it fills. A tenant's business contact (support phone or
|
|
169
168
|
// email, caller or sender id, business address) is not personal.
|
|
@@ -242,6 +241,13 @@ const styleKeys = (node, styles) => {
|
|
|
242
241
|
function fieldValue(node, field, styles) {
|
|
243
242
|
if (STYLE_FIELD.test(field) || field === "styles")
|
|
244
243
|
return styleKeys(node, styles);
|
|
244
|
+
// Figma names the override `fontName`; REST exposes its two values in the
|
|
245
|
+
// text node's TypeStyle rather than as a direct FontName property.
|
|
246
|
+
if (field === "fontName")
|
|
247
|
+
return {
|
|
248
|
+
family: node?.style?.fontFamily,
|
|
249
|
+
style: node?.style?.fontStyle,
|
|
250
|
+
};
|
|
245
251
|
if (TEXT_STYLE_FIELDS.has(field) || field.startsWith("lineHeight"))
|
|
246
252
|
return node?.style?.[field];
|
|
247
253
|
// An image fill's picture is content (DEC-0048): only its scale mode,
|
|
@@ -419,8 +425,15 @@ function outOfScopeRoot(n, parent, componentLabel, memo) {
|
|
|
419
425
|
const hit = memo.get(x.id);
|
|
420
426
|
if (hit) return hit;
|
|
421
427
|
let kind = /** @type {"kit" | "other" | "none"} */ ("none");
|
|
422
|
-
if (
|
|
423
|
-
else if (x
|
|
428
|
+
if (!visibleNode(x)) kind = "none";
|
|
429
|
+
else if (emailKitPart(x, parent, componentLabel)) kind = "kit";
|
|
430
|
+
else if (
|
|
431
|
+
x.type === "INSTANCE" ||
|
|
432
|
+
x.type === "TEXT" ||
|
|
433
|
+
visiblePaints(x.fills) ||
|
|
434
|
+
visiblePaints(x.strokes)
|
|
435
|
+
)
|
|
436
|
+
kind = "other";
|
|
424
437
|
else
|
|
425
438
|
for (const c of x.children ?? []) {
|
|
426
439
|
const k = placed(c);
|
|
@@ -435,7 +448,16 @@ function outOfScopeRoot(n, parent, componentLabel, memo) {
|
|
|
435
448
|
};
|
|
436
449
|
// An email is named so; an editor's workspace around it, or an app screen
|
|
437
450
|
// holding a kit part, stays in scope.
|
|
438
|
-
|
|
451
|
+
let content = /** @type {"kit" | "other" | "none"} */ ("none");
|
|
452
|
+
for (const child of n.children ?? []) {
|
|
453
|
+
const kind = placed(child);
|
|
454
|
+
if (kind === "other") {
|
|
455
|
+
content = "other";
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (kind === "kit") content = "kit";
|
|
459
|
+
}
|
|
460
|
+
return EMAIL_FRAME.test(n.name ?? "") && content === "kit"
|
|
439
461
|
? `email ${n.name}`
|
|
440
462
|
: null;
|
|
441
463
|
}
|
|
@@ -506,6 +528,59 @@ function matchesRole(a, inst, role, library) {
|
|
|
506
528
|
});
|
|
507
529
|
}
|
|
508
530
|
|
|
531
|
+
/** The Dialog instance's Body slot, matched by its stable property name. */
|
|
532
|
+
function dialogBody(a, inst) {
|
|
533
|
+
if (a.setOf(inst)?.key !== DIALOG_COMPONENT_SET_KEY) return null;
|
|
534
|
+
return (
|
|
535
|
+
(inst.children ?? []).find((child) => {
|
|
536
|
+
if (child.type !== "SLOT") return false;
|
|
537
|
+
const property = child.componentPropertyReferences?.slotContentId;
|
|
538
|
+
return /^body(?:#|$)/i.test(property ?? "") || /^body$/i.test(child.name);
|
|
539
|
+
}) ?? null
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** A text sublayer whose characters this instance changes through a TEXT prop. */
|
|
544
|
+
function textPropertyOverrideTarget(a, from, node) {
|
|
545
|
+
if (node.type !== "TEXT") return false;
|
|
546
|
+
const property = node.componentPropertyReferences?.characters;
|
|
547
|
+
if (typeof property !== "string") return false;
|
|
548
|
+
for (const ancestor of a.ancestors(node)) {
|
|
549
|
+
if (
|
|
550
|
+
ancestor.type === "INSTANCE" &&
|
|
551
|
+
ancestor.componentProperties?.[property]?.type === "TEXT"
|
|
552
|
+
)
|
|
553
|
+
return true;
|
|
554
|
+
if (ancestor === from) break;
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* A look field Figma auto-writes when a TEXT component property is set: the
|
|
561
|
+
* typography fields and the text's variable bindings (typography and its fill
|
|
562
|
+
* colour). Only skipped when equal to the main (checked by the caller).
|
|
563
|
+
*/
|
|
564
|
+
function autoTextPropertyField(node, field) {
|
|
565
|
+
if (
|
|
566
|
+
field === "fontSize" ||
|
|
567
|
+
field === "fontName" ||
|
|
568
|
+
field === "letterSpacing" ||
|
|
569
|
+
field.startsWith("lineHeight")
|
|
570
|
+
)
|
|
571
|
+
return true;
|
|
572
|
+
if (field !== "boundVariables") return false;
|
|
573
|
+
const keys = Object.keys(node.boundVariables ?? {});
|
|
574
|
+
return (
|
|
575
|
+
keys.length > 0 &&
|
|
576
|
+
keys.every((key) =>
|
|
577
|
+
/^(font(Family|Size|Style|Weight)|letterSpacing|lineHeight|fills)$/.test(
|
|
578
|
+
key,
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
509
584
|
/** Characters the screen chose: its own text, or an overridden label. */
|
|
510
585
|
function screenText(a, n, overridden) {
|
|
511
586
|
return !a.composed(n) || overridden.has(n.id);
|
|
@@ -558,24 +633,15 @@ export function checkConformance({
|
|
|
558
633
|
const analyses = Object.values(nodes ?? {})
|
|
559
634
|
.filter((entry) => entry?.document)
|
|
560
635
|
.map((entry) => analyse(entry, mains, library));
|
|
561
|
-
// The viewer (R5):
|
|
562
|
-
//
|
|
563
|
-
// anywhere else is still the viewer's own.
|
|
636
|
+
// The viewer (R5): identifiers inside the structural self set. The same
|
|
637
|
+
// value anywhere else is still the viewer's own.
|
|
564
638
|
const viewer = new Set();
|
|
565
639
|
for (const a of analyses)
|
|
566
640
|
for (const n of a.byId.values()) {
|
|
567
|
-
if (n.type !== "TEXT" || a.out.has(n.id)) continue;
|
|
641
|
+
if (n.type !== "TEXT" || a.out.has(n.id) || !a.shown(n)) continue;
|
|
568
642
|
const ids = identifiers(textOf(n));
|
|
569
643
|
if (ids.length === 0) continue;
|
|
570
|
-
if (
|
|
571
|
-
a
|
|
572
|
-
.ancestors(n)
|
|
573
|
-
.some(
|
|
574
|
-
(x) =>
|
|
575
|
-
VIEWER_PLACE.test(x.name ?? "") || SELF_MARK.test(x.name ?? ""),
|
|
576
|
-
)
|
|
577
|
-
)
|
|
578
|
-
for (const id of ids) viewer.add(id);
|
|
644
|
+
if (isSelf(a, n)) for (const id of ids) viewer.add(id);
|
|
579
645
|
}
|
|
580
646
|
|
|
581
647
|
for (const a of analyses) {
|
|
@@ -628,12 +694,15 @@ export function checkConformance({
|
|
|
628
694
|
let slotDefault;
|
|
629
695
|
const differ = [];
|
|
630
696
|
const equal = [];
|
|
697
|
+
const textPropertyTarget = textPropertyOverrideTarget(a, from, node);
|
|
631
698
|
for (const field of [...fields].sort()) {
|
|
632
699
|
if (ALLOWED_FIELDS.has(field)) continue;
|
|
633
700
|
if (exemptPaint(a, from, node, field)) continue;
|
|
634
701
|
const mine = fieldValue(node, field, a.styles);
|
|
635
702
|
const theirs = fieldValue(mainNode, field, mainStyles);
|
|
636
703
|
if (sameValue(mine, theirs)) {
|
|
704
|
+
if (textPropertyTarget && autoTextPropertyField(node, field))
|
|
705
|
+
continue;
|
|
637
706
|
equal.push(field);
|
|
638
707
|
continue;
|
|
639
708
|
}
|
|
@@ -677,6 +746,25 @@ export function checkConformance({
|
|
|
677
746
|
for (const r of roles)
|
|
678
747
|
if (matchesRole(a, n, r, library)) usedRoles.add(r.role);
|
|
679
748
|
|
|
749
|
+
// roleMismatch (operator ruling, 30 Oct): a Dialog is for a form,
|
|
750
|
+
// list or other body content. A title, description and actions alone
|
|
751
|
+
// ask a question, which is the Alert Dialog role.
|
|
752
|
+
if (n.type === "INSTANCE") {
|
|
753
|
+
const body = dialogBody(a, n);
|
|
754
|
+
if (
|
|
755
|
+
a.setOf(n)?.key === DIALOG_COMPONENT_SET_KEY &&
|
|
756
|
+
(!body ||
|
|
757
|
+
!a.shown(body) ||
|
|
758
|
+
!(body.children ?? []).some((child) => a.shown(child)))
|
|
759
|
+
)
|
|
760
|
+
push(
|
|
761
|
+
findings,
|
|
762
|
+
n.id,
|
|
763
|
+
"roleMismatch",
|
|
764
|
+
"Dialog Body is empty or hidden; use Alert Dialog",
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
680
768
|
// tableHeaderFill (DEC-0036): every header cell keeps the library fill.
|
|
681
769
|
if (n.type === "INSTANCE" && headerCell(a, n)) {
|
|
682
770
|
const main = mains[n.componentId]?.document;
|
|
@@ -1096,42 +1184,94 @@ function handDrawnButton(n) {
|
|
|
1096
1184
|
const words = buttonText(n).split(/\s+/).filter(Boolean);
|
|
1097
1185
|
return words.length > 0 && words.length <= 3;
|
|
1098
1186
|
}
|
|
1187
|
+
function variantValue(n, property) {
|
|
1188
|
+
for (const [name, value] of Object.entries(n.componentProperties ?? {}))
|
|
1189
|
+
if (
|
|
1190
|
+
name.split("#", 1)[0] === property &&
|
|
1191
|
+
value?.type === "VARIANT" &&
|
|
1192
|
+
typeof value.value === "string"
|
|
1193
|
+
)
|
|
1194
|
+
return value.value;
|
|
1195
|
+
const value = n.variantProperties?.[property];
|
|
1196
|
+
return typeof value === "string" ? value : null;
|
|
1197
|
+
}
|
|
1198
|
+
function selfPersonName(a, n) {
|
|
1199
|
+
return (
|
|
1200
|
+
n.type === "INSTANCE" &&
|
|
1201
|
+
a.setOf(n)?.key === PERSON_NAME_COMPONENT_SET_KEY &&
|
|
1202
|
+
variantValue(n, "state") === "self"
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
function selfRow(a, n) {
|
|
1206
|
+
const up = a.ancestors(n);
|
|
1207
|
+
const tableAt = up.findIndex(
|
|
1208
|
+
(x) => x.type === "INSTANCE" && a.setOf(x)?.key === TABLE_COMPONENT_SET_KEY,
|
|
1209
|
+
);
|
|
1210
|
+
if (tableAt !== -1) {
|
|
1211
|
+
if (tableAt < 2) return null;
|
|
1212
|
+
const body = up[tableAt - 1];
|
|
1213
|
+
const row = up[tableAt - 2];
|
|
1214
|
+
const cells = row.children ?? [];
|
|
1215
|
+
if (
|
|
1216
|
+
a.parent.get(row.id)?.id !== body.id ||
|
|
1217
|
+
a.parent.get(body.id)?.id !== up[tableAt].id ||
|
|
1218
|
+
cells.length === 0 ||
|
|
1219
|
+
!cells.every(
|
|
1220
|
+
(cell) =>
|
|
1221
|
+
cell.type === "INSTANCE" &&
|
|
1222
|
+
a.setOf(cell)?.key === TABLE_CELL_COMPONENT_SET_KEY,
|
|
1223
|
+
)
|
|
1224
|
+
)
|
|
1225
|
+
return null;
|
|
1226
|
+
return row;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return (
|
|
1230
|
+
up.find(
|
|
1231
|
+
(x) =>
|
|
1232
|
+
x.type === "INSTANCE" && a.setOf(x)?.key === ITEM_COMPONENT_SET_KEY,
|
|
1233
|
+
) ?? null
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1099
1236
|
function isSelf(a, n) {
|
|
1100
1237
|
const up = a.ancestors(n);
|
|
1238
|
+
if (up.some((x) => selfPersonName(a, x))) return true;
|
|
1101
1239
|
if (
|
|
1102
1240
|
up.some(
|
|
1103
|
-
(x) =>
|
|
1241
|
+
(x) =>
|
|
1242
|
+
x.type === "INSTANCE" && SELF_COMPONENT_SET_KEYS.has(a.setOf(x)?.key),
|
|
1104
1243
|
)
|
|
1105
1244
|
)
|
|
1106
1245
|
return true;
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
? a.parent.get(x.id)?.name
|
|
1113
|
-
: null;
|
|
1114
|
-
if (!SELF_COMPONENT.test(set ?? "")) return false;
|
|
1115
|
-
const name = x.type === "INSTANCE" ? a.componentOf(x)?.name : x.name;
|
|
1116
|
-
const kind = x.componentProperties?.kind?.value;
|
|
1117
|
-
return (
|
|
1118
|
-
kind === "Account" || /(^|,\s*)kind=account(\s*,|$)/i.test(name ?? "")
|
|
1119
|
-
);
|
|
1120
|
-
};
|
|
1121
|
-
if (up.some(accountCard)) return true;
|
|
1122
|
-
const siblings = a.parent.get(n.id)?.children ?? [];
|
|
1246
|
+
|
|
1247
|
+
// INTERIM: identify a Table row from its direct Table-cell children, or a
|
|
1248
|
+
// list row from its Item instance. Switch to the published Table Row key
|
|
1249
|
+
// when DESIGN-SYSTEM-160 ships.
|
|
1250
|
+
const row = selfRow(a, n);
|
|
1123
1251
|
if (
|
|
1124
|
-
|
|
1252
|
+
row &&
|
|
1253
|
+
[...indexTree(row).byId.values()].some((x) => selfPersonName(a, x))
|
|
1125
1254
|
)
|
|
1126
1255
|
return true;
|
|
1127
|
-
|
|
1128
|
-
//
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1256
|
+
|
|
1257
|
+
// INTERIM: switch this slot rule to the footer-user component key when
|
|
1258
|
+
// DESIGN-SYSTEM-159 ships.
|
|
1259
|
+
const slotAt = up.findIndex((x) => {
|
|
1260
|
+
if (x.type !== "SLOT") return false;
|
|
1261
|
+
// Figma formerly exposed the slot reference as mainComponent; current
|
|
1262
|
+
// REST responses use slotContentId. Accept both so a library republish
|
|
1263
|
+
// cannot turn the viewer's own identifiers into another-person findings.
|
|
1264
|
+
const property =
|
|
1265
|
+
x.componentPropertyReferences?.slotContentId ??
|
|
1266
|
+
x.componentPropertyReferences?.mainComponent;
|
|
1267
|
+
return (
|
|
1268
|
+
typeof property === "string" &&
|
|
1269
|
+
property.split("#", 1)[0] === "sidebarFooter"
|
|
1270
|
+
);
|
|
1271
|
+
});
|
|
1272
|
+
if (slotAt === -1) return false;
|
|
1273
|
+
const owner = up.slice(slotAt + 1).find((x) => x.type === "INSTANCE");
|
|
1274
|
+
return owner ? a.setOf(owner)?.key === APP_FRAME_COMPONENT_SET_KEY : false;
|
|
1135
1275
|
}
|
|
1136
1276
|
function revealedByPermission(a, n) {
|
|
1137
1277
|
return [n, ...a.ancestors(n)].some((x) => {
|