@nodii/dev 0.6.21 → 0.6.22
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 +196 -57
- 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.22";
|
|
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.9";
|
|
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,
|
|
@@ -420,7 +426,13 @@ function outOfScopeRoot(n, parent, componentLabel, memo) {
|
|
|
420
426
|
if (hit) return hit;
|
|
421
427
|
let kind = /** @type {"kit" | "other" | "none"} */ ("none");
|
|
422
428
|
if (emailKitPart(x, parent, componentLabel)) kind = "kit";
|
|
423
|
-
else if (
|
|
429
|
+
else if (
|
|
430
|
+
x.type === "INSTANCE" ||
|
|
431
|
+
x.type === "TEXT" ||
|
|
432
|
+
visiblePaints(x.fills) ||
|
|
433
|
+
visiblePaints(x.strokes)
|
|
434
|
+
)
|
|
435
|
+
kind = "other";
|
|
424
436
|
else
|
|
425
437
|
for (const c of x.children ?? []) {
|
|
426
438
|
const k = placed(c);
|
|
@@ -435,7 +447,16 @@ function outOfScopeRoot(n, parent, componentLabel, memo) {
|
|
|
435
447
|
};
|
|
436
448
|
// An email is named so; an editor's workspace around it, or an app screen
|
|
437
449
|
// holding a kit part, stays in scope.
|
|
438
|
-
|
|
450
|
+
let content = /** @type {"kit" | "other" | "none"} */ ("none");
|
|
451
|
+
for (const child of n.children ?? []) {
|
|
452
|
+
const kind = placed(child);
|
|
453
|
+
if (kind === "other") {
|
|
454
|
+
content = "other";
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
if (kind === "kit") content = "kit";
|
|
458
|
+
}
|
|
459
|
+
return EMAIL_FRAME.test(n.name ?? "") && content === "kit"
|
|
439
460
|
? `email ${n.name}`
|
|
440
461
|
: null;
|
|
441
462
|
}
|
|
@@ -506,6 +527,59 @@ function matchesRole(a, inst, role, library) {
|
|
|
506
527
|
});
|
|
507
528
|
}
|
|
508
529
|
|
|
530
|
+
/** The Dialog instance's Body slot, matched by its stable property name. */
|
|
531
|
+
function dialogBody(a, inst) {
|
|
532
|
+
if (a.setOf(inst)?.key !== DIALOG_COMPONENT_SET_KEY) return null;
|
|
533
|
+
return (
|
|
534
|
+
(inst.children ?? []).find((child) => {
|
|
535
|
+
if (child.type !== "SLOT") return false;
|
|
536
|
+
const property = child.componentPropertyReferences?.slotContentId;
|
|
537
|
+
return /^body(?:#|$)/i.test(property ?? "") || /^body$/i.test(child.name);
|
|
538
|
+
}) ?? null
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** A text sublayer whose characters this instance changes through a TEXT prop. */
|
|
543
|
+
function textPropertyOverrideTarget(a, from, node) {
|
|
544
|
+
if (node.type !== "TEXT") return false;
|
|
545
|
+
const property = node.componentPropertyReferences?.characters;
|
|
546
|
+
if (typeof property !== "string") return false;
|
|
547
|
+
for (const ancestor of a.ancestors(node)) {
|
|
548
|
+
if (
|
|
549
|
+
ancestor.type === "INSTANCE" &&
|
|
550
|
+
ancestor.componentProperties?.[property]?.type === "TEXT"
|
|
551
|
+
)
|
|
552
|
+
return true;
|
|
553
|
+
if (ancestor === from) break;
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* A look field Figma auto-writes when a TEXT component property is set: the
|
|
560
|
+
* typography fields and the text's variable bindings (typography and its fill
|
|
561
|
+
* colour). Only skipped when equal to the main (checked by the caller).
|
|
562
|
+
*/
|
|
563
|
+
function autoTextPropertyField(node, field) {
|
|
564
|
+
if (
|
|
565
|
+
field === "fontSize" ||
|
|
566
|
+
field === "fontName" ||
|
|
567
|
+
field === "letterSpacing" ||
|
|
568
|
+
field.startsWith("lineHeight")
|
|
569
|
+
)
|
|
570
|
+
return true;
|
|
571
|
+
if (field !== "boundVariables") return false;
|
|
572
|
+
const keys = Object.keys(node.boundVariables ?? {});
|
|
573
|
+
return (
|
|
574
|
+
keys.length > 0 &&
|
|
575
|
+
keys.every((key) =>
|
|
576
|
+
/^(font(Family|Size|Style|Weight)|letterSpacing|lineHeight|fills)$/.test(
|
|
577
|
+
key,
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
509
583
|
/** Characters the screen chose: its own text, or an overridden label. */
|
|
510
584
|
function screenText(a, n, overridden) {
|
|
511
585
|
return !a.composed(n) || overridden.has(n.id);
|
|
@@ -558,24 +632,15 @@ export function checkConformance({
|
|
|
558
632
|
const analyses = Object.values(nodes ?? {})
|
|
559
633
|
.filter((entry) => entry?.document)
|
|
560
634
|
.map((entry) => analyse(entry, mains, library));
|
|
561
|
-
// The viewer (R5):
|
|
562
|
-
//
|
|
563
|
-
// anywhere else is still the viewer's own.
|
|
635
|
+
// The viewer (R5): identifiers inside the structural self set. The same
|
|
636
|
+
// value anywhere else is still the viewer's own.
|
|
564
637
|
const viewer = new Set();
|
|
565
638
|
for (const a of analyses)
|
|
566
639
|
for (const n of a.byId.values()) {
|
|
567
640
|
if (n.type !== "TEXT" || a.out.has(n.id)) continue;
|
|
568
641
|
const ids = identifiers(textOf(n));
|
|
569
642
|
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);
|
|
643
|
+
if (isSelf(a, n)) for (const id of ids) viewer.add(id);
|
|
579
644
|
}
|
|
580
645
|
|
|
581
646
|
for (const a of analyses) {
|
|
@@ -628,12 +693,15 @@ export function checkConformance({
|
|
|
628
693
|
let slotDefault;
|
|
629
694
|
const differ = [];
|
|
630
695
|
const equal = [];
|
|
696
|
+
const textPropertyTarget = textPropertyOverrideTarget(a, from, node);
|
|
631
697
|
for (const field of [...fields].sort()) {
|
|
632
698
|
if (ALLOWED_FIELDS.has(field)) continue;
|
|
633
699
|
if (exemptPaint(a, from, node, field)) continue;
|
|
634
700
|
const mine = fieldValue(node, field, a.styles);
|
|
635
701
|
const theirs = fieldValue(mainNode, field, mainStyles);
|
|
636
702
|
if (sameValue(mine, theirs)) {
|
|
703
|
+
if (textPropertyTarget && autoTextPropertyField(node, field))
|
|
704
|
+
continue;
|
|
637
705
|
equal.push(field);
|
|
638
706
|
continue;
|
|
639
707
|
}
|
|
@@ -677,6 +745,25 @@ export function checkConformance({
|
|
|
677
745
|
for (const r of roles)
|
|
678
746
|
if (matchesRole(a, n, r, library)) usedRoles.add(r.role);
|
|
679
747
|
|
|
748
|
+
// roleMismatch (operator ruling, 30 Oct): a Dialog is for a form,
|
|
749
|
+
// list or other body content. A title, description and actions alone
|
|
750
|
+
// ask a question, which is the Alert Dialog role.
|
|
751
|
+
if (n.type === "INSTANCE") {
|
|
752
|
+
const body = dialogBody(a, n);
|
|
753
|
+
if (
|
|
754
|
+
a.setOf(n)?.key === DIALOG_COMPONENT_SET_KEY &&
|
|
755
|
+
(!body ||
|
|
756
|
+
!a.shown(body) ||
|
|
757
|
+
!(body.children ?? []).some((child) => a.shown(child)))
|
|
758
|
+
)
|
|
759
|
+
push(
|
|
760
|
+
findings,
|
|
761
|
+
n.id,
|
|
762
|
+
"roleMismatch",
|
|
763
|
+
"Dialog Body is empty or hidden; use Alert Dialog",
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
680
767
|
// tableHeaderFill (DEC-0036): every header cell keeps the library fill.
|
|
681
768
|
if (n.type === "INSTANCE" && headerCell(a, n)) {
|
|
682
769
|
const main = mains[n.componentId]?.document;
|
|
@@ -1096,42 +1183,94 @@ function handDrawnButton(n) {
|
|
|
1096
1183
|
const words = buttonText(n).split(/\s+/).filter(Boolean);
|
|
1097
1184
|
return words.length > 0 && words.length <= 3;
|
|
1098
1185
|
}
|
|
1186
|
+
function variantValue(n, property) {
|
|
1187
|
+
for (const [name, value] of Object.entries(n.componentProperties ?? {}))
|
|
1188
|
+
if (
|
|
1189
|
+
name.split("#", 1)[0] === property &&
|
|
1190
|
+
value?.type === "VARIANT" &&
|
|
1191
|
+
typeof value.value === "string"
|
|
1192
|
+
)
|
|
1193
|
+
return value.value;
|
|
1194
|
+
const value = n.variantProperties?.[property];
|
|
1195
|
+
return typeof value === "string" ? value : null;
|
|
1196
|
+
}
|
|
1197
|
+
function selfPersonName(a, n) {
|
|
1198
|
+
return (
|
|
1199
|
+
n.type === "INSTANCE" &&
|
|
1200
|
+
a.setOf(n)?.key === PERSON_NAME_COMPONENT_SET_KEY &&
|
|
1201
|
+
variantValue(n, "state") === "self"
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
function selfRow(a, n) {
|
|
1205
|
+
const up = a.ancestors(n);
|
|
1206
|
+
const tableAt = up.findIndex(
|
|
1207
|
+
(x) => x.type === "INSTANCE" && a.setOf(x)?.key === TABLE_COMPONENT_SET_KEY,
|
|
1208
|
+
);
|
|
1209
|
+
if (tableAt !== -1) {
|
|
1210
|
+
if (tableAt < 2) return null;
|
|
1211
|
+
const body = up[tableAt - 1];
|
|
1212
|
+
const row = up[tableAt - 2];
|
|
1213
|
+
const cells = row.children ?? [];
|
|
1214
|
+
if (
|
|
1215
|
+
a.parent.get(row.id)?.id !== body.id ||
|
|
1216
|
+
a.parent.get(body.id)?.id !== up[tableAt].id ||
|
|
1217
|
+
cells.length === 0 ||
|
|
1218
|
+
!cells.every(
|
|
1219
|
+
(cell) =>
|
|
1220
|
+
cell.type === "INSTANCE" &&
|
|
1221
|
+
a.setOf(cell)?.key === TABLE_CELL_COMPONENT_SET_KEY,
|
|
1222
|
+
)
|
|
1223
|
+
)
|
|
1224
|
+
return null;
|
|
1225
|
+
return row;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return (
|
|
1229
|
+
up.find(
|
|
1230
|
+
(x) =>
|
|
1231
|
+
x.type === "INSTANCE" && a.setOf(x)?.key === ITEM_COMPONENT_SET_KEY,
|
|
1232
|
+
) ?? null
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1099
1235
|
function isSelf(a, n) {
|
|
1100
1236
|
const up = a.ancestors(n);
|
|
1237
|
+
if (up.some((x) => selfPersonName(a, x))) return true;
|
|
1101
1238
|
if (
|
|
1102
1239
|
up.some(
|
|
1103
|
-
(x) =>
|
|
1240
|
+
(x) =>
|
|
1241
|
+
x.type === "INSTANCE" && SELF_COMPONENT_SET_KEYS.has(a.setOf(x)?.key),
|
|
1104
1242
|
)
|
|
1105
1243
|
)
|
|
1106
1244
|
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 ?? [];
|
|
1245
|
+
|
|
1246
|
+
// INTERIM: identify a Table row from its direct Table-cell children, or a
|
|
1247
|
+
// list row from its Item instance. Switch to the published Table Row key
|
|
1248
|
+
// when DESIGN-SYSTEM-160 ships.
|
|
1249
|
+
const row = selfRow(a, n);
|
|
1123
1250
|
if (
|
|
1124
|
-
|
|
1251
|
+
row &&
|
|
1252
|
+
[...indexTree(row).byId.values()].some((x) => selfPersonName(a, x))
|
|
1125
1253
|
)
|
|
1126
1254
|
return true;
|
|
1127
|
-
|
|
1128
|
-
//
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1255
|
+
|
|
1256
|
+
// INTERIM: switch this slot rule to the footer-user component key when
|
|
1257
|
+
// DESIGN-SYSTEM-159 ships.
|
|
1258
|
+
const slotAt = up.findIndex((x) => {
|
|
1259
|
+
if (x.type !== "SLOT") return false;
|
|
1260
|
+
// Figma formerly exposed the slot reference as mainComponent; current
|
|
1261
|
+
// REST responses use slotContentId. Accept both so a library republish
|
|
1262
|
+
// cannot turn the viewer's own identifiers into another-person findings.
|
|
1263
|
+
const property =
|
|
1264
|
+
x.componentPropertyReferences?.slotContentId ??
|
|
1265
|
+
x.componentPropertyReferences?.mainComponent;
|
|
1266
|
+
return (
|
|
1267
|
+
typeof property === "string" &&
|
|
1268
|
+
property.split("#", 1)[0] === "sidebarFooter"
|
|
1269
|
+
);
|
|
1270
|
+
});
|
|
1271
|
+
if (slotAt === -1) return false;
|
|
1272
|
+
const owner = up.slice(slotAt + 1).find((x) => x.type === "INSTANCE");
|
|
1273
|
+
return owner ? a.setOf(owner)?.key === APP_FRAME_COMPONENT_SET_KEY : false;
|
|
1135
1274
|
}
|
|
1136
1275
|
function revealedByPermission(a, n) {
|
|
1137
1276
|
return [n, ...a.ancestors(n)].some((x) => {
|