@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 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.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.4";
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
- // 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,
@@ -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 (emailKitPart(x, parent, componentLabel)) kind = "kit";
423
- else if (x.type === "INSTANCE") kind = "other";
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
- return EMAIL_FRAME.test(n.name ?? "") && placed(n) === "kit"
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): 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.
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) => SELF_CONTAINER.test(x.name ?? "") || SELF_MARK.test(x.name ?? ""),
1241
+ (x) =>
1242
+ x.type === "INSTANCE" && SELF_COMPONENT_SET_KEYS.has(a.setOf(x)?.key),
1104
1243
  )
1105
1244
  )
1106
1245
  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 ?? [];
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
- [n, ...siblings].some((x) => x.type === "TEXT" && SELF_TEXT.test(textOf(x)))
1252
+ row &&
1253
+ [...indexTree(row).byId.values()].some((x) => selfPersonName(a, x))
1125
1254
  )
1126
1255
  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;
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodii/dev",
3
- "version": "0.6.21",
3
+ "version": "0.6.23",
4
4
  "description": "Agent CLI for Nodii Dev, reached through the platform edge",
5
5
  "type": "module",
6
6
  "bin": {