@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 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.21";
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.4";
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
- // The viewer's own record (R5): the App Frame sidebar footer, the user menu
139
- // and the profile. Not any footer: a dialog or table footer can list others.
140
- const SELF_CONTAINER =
141
- /sidebar ?footer|user button|user menu|account menu|profile|\b(my|own) (account|tokens?|security|sessions?|settings|notification settings|records?)\b/i;
142
- // A frame the screen marks as the viewer's own: "[self]", "(self)", a
143
- // trailing "· self", or "(you)".
144
- const SELF_MARK = /\[self\]|\(self\)|·\s*self\s*$|\(you\)/i;
145
- // Where the screens show who the viewer is: the App Frame footer, the user
146
- // and account menus, and frames marked self or named my/own.
147
- const VIEWER_PLACE =
148
- /sidebar ?footer|user button|user menu|account menu|\b(my|own) (profile|account|tokens?|security|sessions?|settings)\b/i;
149
- const SELF_TEXT = /\(you\)|^you$/i;
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 (x.type === "INSTANCE") kind = "other";
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
- return EMAIL_FRAME.test(n.name ?? "") && placed(n) === "kit"
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): the email and phone the screens show in the viewer's
562
- // own places (the App Frame footer, a frame marked self). The same value
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) => SELF_CONTAINER.test(x.name ?? "") || SELF_MARK.test(x.name ?? ""),
1240
+ (x) =>
1241
+ x.type === "INSTANCE" && SELF_COMPONENT_SET_KEYS.has(a.setOf(x)?.key),
1104
1242
  )
1105
1243
  )
1106
1244
  return true;
1107
- const accountCard = (x) => {
1108
- const set =
1109
- x.type === "INSTANCE"
1110
- ? a.componentLabel(x)
1111
- : x.type === "COMPONENT"
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
- [n, ...siblings].some((x) => x.type === "TEXT" && SELF_TEXT.test(textOf(x)))
1251
+ row &&
1252
+ [...indexTree(row).byId.values()].some((x) => selfPersonName(a, x))
1125
1253
  )
1126
1254
  return true;
1127
- // "Signed in" or "Not you?" in the same card: the nearest instance or
1128
- // component holding it, never a whole row or screen.
1129
- const card = up.find((x) => x.type === "INSTANCE" || x.type === "COMPONENT");
1130
- return card
1131
- ? [...indexTree(card).byId.values()].some(
1132
- (x) => x.type === "TEXT" && SIGNED_IN.test(textOf(x)),
1133
- )
1134
- : false;
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.21",
3
+ "version": "0.6.22",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {