@astryxdesign/cli 0.7.12 → 0.7.13-canary.9993f4c

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 (85) hide show
  1. package/api/component/__fixtures__/resolutions/modules.txt +1 -1
  2. package/api/doctor/_adapter.d.mts +17 -0
  3. package/api/doctor/_adapter.mjs +40 -0
  4. package/api/doctor/doctor.d.mts +34 -4
  5. package/api/doctor/doctor.mjs +108 -21
  6. package/api/icon/_adapter.d.mts +6 -3
  7. package/api/icon/_adapter.mjs +11 -3
  8. package/api/icon/icon.type.d.mts +22 -1
  9. package/api/icon/icon.type.mjs +12 -4
  10. package/api/icon/iconUse.doc.mjs +27 -7
  11. package/api/icon/list/list.mjs +29 -3
  12. package/api/icon/use/use.d.mts +7 -3
  13. package/api/icon/use/use.mjs +68 -18
  14. package/api/integration/validate-integration.d.mts +4 -0
  15. package/api/integration/validate-integration.mjs +32 -2
  16. package/api/integration/validate-integration.type.d.mts +15 -0
  17. package/api/integration/validate-integration.type.mjs +6 -1
  18. package/api/template/copy/copy.mjs +4 -2
  19. package/api/template/template.type.d.mts +42 -4
  20. package/api/template/template.type.mjs +9 -6
  21. package/assets/discover/official-packages.json +4 -0
  22. package/assets/docs/tree/icon-sets.doc.mjs +91 -7
  23. package/assets/icons/definitions/alignBottom.doc.mjs +9 -0
  24. package/assets/icons/definitions/alignJustify.doc.mjs +9 -0
  25. package/assets/icons/definitions/alignMiddle.doc.mjs +9 -0
  26. package/assets/icons/definitions/alignTop.doc.mjs +9 -0
  27. package/assets/icons/definitions/angle.doc.mjs +9 -0
  28. package/assets/icons/definitions/cloud.doc.mjs +9 -0
  29. package/assets/icons/definitions/collection.doc.mjs +9 -0
  30. package/assets/icons/definitions/contrast.doc.mjs +9 -0
  31. package/assets/icons/definitions/cornerRadius.doc.mjs +9 -0
  32. package/assets/icons/definitions/cut.doc.mjs +9 -0
  33. package/assets/icons/definitions/fingerprint.doc.mjs +9 -0
  34. package/assets/icons/definitions/fire.doc.mjs +9 -0
  35. package/assets/icons/definitions/frame.doc.mjs +9 -0
  36. package/assets/icons/definitions/gift.doc.mjs +9 -0
  37. package/assets/icons/definitions/group.doc.mjs +9 -0
  38. package/assets/icons/definitions/integration.doc.mjs +9 -0
  39. package/assets/icons/definitions/lowercase.doc.mjs +9 -0
  40. package/assets/icons/definitions/paddingHorizontal.doc.mjs +9 -0
  41. package/assets/icons/definitions/paddingPerSide.doc.mjs +9 -0
  42. package/assets/icons/definitions/paddingVertical.doc.mjs +9 -0
  43. package/assets/icons/definitions/panelBottom.doc.mjs +9 -0
  44. package/assets/icons/definitions/photoEffect.doc.mjs +9 -0
  45. package/assets/icons/definitions/radiusPerCorner.doc.mjs +9 -0
  46. package/assets/icons/definitions/rotateCounterclockwise.doc.mjs +9 -0
  47. package/assets/icons/definitions/tablet.doc.mjs +9 -0
  48. package/assets/icons/definitions/text.doc.mjs +9 -0
  49. package/assets/icons/definitions/textRegular.doc.mjs +9 -0
  50. package/assets/icons/definitions/transfer.doc.mjs +9 -0
  51. package/assets/icons/definitions/uppercase.doc.mjs +9 -0
  52. package/assets/icons/definitions/userRemove.doc.mjs +9 -0
  53. package/assets/templates/blocks/components/BaseTypeahead/BaseTypeaheadMenu.doc.mjs +23 -0
  54. package/assets/templates/blocks/components/BaseTypeahead/BaseTypeaheadMenu.tsx +199 -0
  55. package/assets/templates/blocks/components/ButtonGroup/ButtonGroupShowcase.doc.mjs +1 -0
  56. package/assets/templates/blocks/components/ChatSystemMessage/ChatSystemMessageStatusUpdates.doc.mjs +7 -2
  57. package/assets/templates/blocks/components/InternationalizationProvider/useDirectionHookUsage.doc.mjs +18 -0
  58. package/assets/templates/blocks/components/InternationalizationProvider/useDirectionHookUsage.tsx +101 -0
  59. package/assets/templates/pages/canvas-editor/template.doc.mjs +20 -0
  60. package/assets/templates/pages/editor/template.doc.mjs +1 -0
  61. package/assets/templates/pages/form-wizard-vertical/template.doc.mjs +1 -0
  62. package/assets/templates/pages/settings-dialog/template.doc.mjs +4 -0
  63. package/assets/templates/pages/shell-top-nav/template.doc.mjs +3 -0
  64. package/assets/templates/pages/table-collapsible/template.doc.mjs +1 -0
  65. package/assets/templates/pages/table-comparison/template.doc.mjs +1 -0
  66. package/authoring/config/config.doc.mjs +4 -4
  67. package/authoring/config/parse.d.mts +1 -1
  68. package/authoring/config/parse.mjs +3 -1
  69. package/authoring/config/type.ts +7 -5
  70. package/authoring/doctypes/template/template.doc.mjs +1 -1
  71. package/authoring/doctypes/template/type.ts +5 -3
  72. package/clients/cli/commands/experimental/icon.mjs +38 -9
  73. package/clients/cli/commands/experimental-icon-use.doc.mjs +9 -4
  74. package/clients/cli/commands/template.mjs +12 -3
  75. package/foundation/config/config-writer.d.mts +14 -0
  76. package/foundation/config/config-writer.mjs +73 -17
  77. package/foundation/discovery/icon-set-discovery.d.mts +77 -7
  78. package/foundation/discovery/icon-set-discovery.mjs +128 -42
  79. package/foundation/discovery/template-icon-notes.d.mts +46 -0
  80. package/foundation/discovery/template-icon-notes.mjs +94 -16
  81. package/foundation/discovery/template-icons.d.mts +247 -0
  82. package/foundation/discovery/template-icons.mjs +591 -37
  83. package/foundation/integrations/cli-requirement.d.mts +13 -4
  84. package/foundation/integrations/cli-requirement.mjs +18 -6
  85. package/package.json +14 -14
@@ -43,6 +43,14 @@ export function hasLicenseNotice(dir: string): boolean;
43
43
  * file is invalid; `unresolved`: its module does not resolve from the project
44
44
  * @property {string} reason
45
45
  */
46
+ /**
47
+ * One backup set the project's list names, and what it resolves to.
48
+ * @typedef {object} IconSetBackup
49
+ * @property {string} selector the name as the list writes it
50
+ * @property {'selected' | 'ambiguous' | 'missing' | 'withdrawn'} status
51
+ * @property {DiscoveredIconSet} [set] the set, when selected
52
+ * @property {string} reason one sentence saying why
53
+ */
46
54
  /**
47
55
  * The project's icon set (FR8).
48
56
  * @typedef {object} IconSetChoice
@@ -53,7 +61,13 @@ export function hasLicenseNotice(dir: string): boolean;
53
61
  * `withdrawn`: it names a set that is installed but withdrawn (invalid, or
54
62
  * its module does not resolve), and `reason` says why
55
63
  * @property {DiscoveredIconSet} [set] the project's set, when selected
56
- * @property {string} [configured] the `experimental.iconSet` config value, when set
64
+ * @property {string} [configured] the main set `experimental.iconSet` names
65
+ * (its first entry, when it is a list), when set
66
+ * @property {IconSetBackup[]} [backups] the backup sets the list names after
67
+ * the main set, in order, when it names any
68
+ * @property {DiscoveredIconSet[]} [order] the sets a template icon is looked
69
+ * up in, in order: the main set, then each backup that resolves. Present
70
+ * only when the main set is selected
57
71
  * @property {string} reason one sentence saying why
58
72
  */
59
73
  /**
@@ -120,16 +134,39 @@ export function discoverIntegrationIconSets(integration: import("../integrations
120
134
  */
121
135
  export function withdrawnNamed(withdrawn: WithdrawnIconSet[], selector: string): WithdrawnIconSet[];
122
136
  /**
123
- * Pick the project's icon set (FR8, FR17): the set `experimental.iconSet`
124
- * names, as `"<set>"` or `"<package>/<set>"`; otherwise the only installed
137
+ * The entries of an `experimental.iconSet` value, main set first: a string is
138
+ * a one-entry list.
139
+ * @param {string | readonly string[] | undefined} configured
140
+ * @returns {string[]}
141
+ */
142
+ export function iconSetList(configured: string | readonly string[] | undefined): string[];
143
+ /**
144
+ * Installed sets the project's list leaves out: the problem, and the fix that
145
+ * adds them as backups after the list's own. Doctor and `experimental icon
146
+ * use` print the same words.
147
+ * @param {number} installed how many sets are installed
148
+ * @param {string} main the main set as the list writes it
149
+ * @param {string[]} backups the list's backups, in order
150
+ * @param {string[]} unlisted the installed sets the list leaves out
151
+ * @returns {{problem: string, command: string, fix: string}}
152
+ */
153
+ export function unlistedIconSets(installed: number, main: string, backups: string[], unlisted: string[]): {
154
+ problem: string;
155
+ command: string;
156
+ fix: string;
157
+ };
158
+ /**
159
+ * Pick the project's icon set (FR8, FR17): the main set `experimental.iconSet`
160
+ * names, as `"<set>"` or `"<package>/<set>"`, or as the first entry of a list
161
+ * whose other entries are backup sets in order; otherwise the only installed
125
162
  * set; otherwise none. A configured name that matches no valid set but an
126
163
  * installed set that was withdrawn says so, with why.
127
164
  * @param {DiscoveredIconSet[]} sets installed sets, in integration load order
128
- * @param {string | undefined} configured the `experimental.iconSet` config value
165
+ * @param {string | readonly string[] | undefined} configured the `experimental.iconSet` config value
129
166
  * @param {WithdrawnIconSet[]} [withdrawn] installed sets that were withdrawn
130
167
  * @returns {IconSetChoice}
131
168
  */
132
- export function chooseProjectIconSet(sets: DiscoveredIconSet[], configured: string | undefined, withdrawn?: WithdrawnIconSet[]): IconSetChoice;
169
+ export function chooseProjectIconSet(sets: DiscoveredIconSet[], configured: string | readonly string[] | undefined, withdrawn?: WithdrawnIconSet[]): IconSetChoice;
133
170
  /**
134
171
  * A set's full name, `<package>/<set>`.
135
172
  * @param {{package: string, name: string}} set
@@ -139,7 +176,8 @@ export function iconSetKey(set: {
139
176
  name: string;
140
177
  }): string;
141
178
  /**
142
- * Sets in tie order (FR6, FR8): the project's set first, then load order.
179
+ * Sets in tie order (FR6, FR8): the project's set and its backups first, in
180
+ * list order, then load order.
143
181
  * @param {DiscoveredIconSet[]} sets
144
182
  * @param {IconSetChoice} choice
145
183
  * @returns {DiscoveredIconSet[]}
@@ -147,6 +185,8 @@ export function iconSetKey(set: {
147
185
  export function orderIconSets(sets: DiscoveredIconSet[], choice: IconSetChoice): DiscoveredIconSet[];
148
186
  /** A license or notice file a set ships beside its data (FR15). */
149
187
  export const NOTICE_FILE_RE: RegExp;
188
+ /** The command that chooses a main set and backups, as the CLI prints it. */
189
+ export const ICON_USE_ORDER_COMMAND: "astryx experimental icon use <main> --backup <set>";
150
190
  /**
151
191
  * One installed icon set: its descriptor, its data, and where it came from.
152
192
  */
@@ -213,6 +253,24 @@ export type WithdrawnIconSet = {
213
253
  kind: "invalid" | "unresolved";
214
254
  reason: string;
215
255
  };
256
+ /**
257
+ * One backup set the project's list names, and what it resolves to.
258
+ */
259
+ export type IconSetBackup = {
260
+ /**
261
+ * the name as the list writes it
262
+ */
263
+ selector: string;
264
+ status: "selected" | "ambiguous" | "missing" | "withdrawn";
265
+ /**
266
+ * the set, when selected
267
+ */
268
+ set?: DiscoveredIconSet | undefined;
269
+ /**
270
+ * one sentence saying why
271
+ */
272
+ reason: string;
273
+ };
216
274
  /**
217
275
  * The project's icon set (FR8).
218
276
  */
@@ -230,9 +288,21 @@ export type IconSetChoice = {
230
288
  */
231
289
  set?: DiscoveredIconSet | undefined;
232
290
  /**
233
- * the `experimental.iconSet` config value, when set
291
+ * the main set `experimental.iconSet` names
292
+ * (its first entry, when it is a list), when set
234
293
  */
235
294
  configured?: string | undefined;
295
+ /**
296
+ * the backup sets the list names after
297
+ * the main set, in order, when it names any
298
+ */
299
+ backups?: IconSetBackup[] | undefined;
300
+ /**
301
+ * the sets a template icon is looked
302
+ * up in, in order: the main set, then each backup that resolves. Present
303
+ * only when the main set is selected
304
+ */
305
+ order?: DiscoveredIconSet[] | undefined;
236
306
  /**
237
307
  * one sentence saying why
238
308
  */
@@ -99,6 +99,15 @@ export function hasLicenseNotice(dir) {
99
99
  * @property {string} reason
100
100
  */
101
101
 
102
+ /**
103
+ * One backup set the project's list names, and what it resolves to.
104
+ * @typedef {object} IconSetBackup
105
+ * @property {string} selector the name as the list writes it
106
+ * @property {'selected' | 'ambiguous' | 'missing' | 'withdrawn'} status
107
+ * @property {DiscoveredIconSet} [set] the set, when selected
108
+ * @property {string} reason one sentence saying why
109
+ */
110
+
102
111
  /**
103
112
  * The project's icon set (FR8).
104
113
  * @typedef {object} IconSetChoice
@@ -109,7 +118,13 @@ export function hasLicenseNotice(dir) {
109
118
  * `withdrawn`: it names a set that is installed but withdrawn (invalid, or
110
119
  * its module does not resolve), and `reason` says why
111
120
  * @property {DiscoveredIconSet} [set] the project's set, when selected
112
- * @property {string} [configured] the `experimental.iconSet` config value, when set
121
+ * @property {string} [configured] the main set `experimental.iconSet` names
122
+ * (its first entry, when it is a list), when set
123
+ * @property {IconSetBackup[]} [backups] the backup sets the list names after
124
+ * the main set, in order, when it names any
125
+ * @property {DiscoveredIconSet[]} [order] the sets a template icon is looked
126
+ * up in, in order: the main set, then each backup that resolves. Present
127
+ * only when the main set is selected
113
128
  * @property {string} reason one sentence saying why
114
129
  */
115
130
 
@@ -407,55 +422,126 @@ export function withdrawnNamed(withdrawn, selector) {
407
422
  }
408
423
 
409
424
  /**
410
- * Pick the project's icon set (FR8, FR17): the set `experimental.iconSet`
411
- * names, as `"<set>"` or `"<package>/<set>"`; otherwise the only installed
425
+ * The entries of an `experimental.iconSet` value, main set first: a string is
426
+ * a one-entry list.
427
+ * @param {string | readonly string[] | undefined} configured
428
+ * @returns {string[]}
429
+ */
430
+ export function iconSetList(configured) {
431
+ if (configured == null) return [];
432
+ return typeof configured === 'string' ? [configured] : [...configured];
433
+ }
434
+
435
+ /**
436
+ * What one name in the list resolves to: a set, or why not.
437
+ * @param {DiscoveredIconSet[]} sets
438
+ * @param {string} selector `"<set>"` or `"<package>/<set>"`
439
+ * @param {WithdrawnIconSet[]} withdrawn
440
+ * @returns {{status: 'selected' | 'ambiguous' | 'missing' | 'withdrawn', set?: DiscoveredIconSet, reason: string}}
441
+ */
442
+ function resolveSelector(sets, selector, withdrawn) {
443
+ const exact = sets.filter(set => `${set.package}/${set.name}` === selector);
444
+ const byName =
445
+ exact.length > 0 ? exact : sets.filter(set => set.name === selector);
446
+ if (byName.length === 1) {
447
+ return {
448
+ status: 'selected',
449
+ set: byName[0],
450
+ reason: `experimental.iconSet names "${selector}".`,
451
+ };
452
+ }
453
+ if (byName.length > 1) {
454
+ return {
455
+ status: 'ambiguous',
456
+ reason: `experimental.iconSet "${selector}" names a set in ${byName.map(set => set.package).join(' and ')}; write it as "<package>/<set>".`,
457
+ };
458
+ }
459
+ const gone = withdrawnNamed(withdrawn, selector);
460
+ if (gone.length > 0) {
461
+ return {
462
+ status: 'withdrawn',
463
+ reason: `experimental.iconSet names "${selector}", which is installed but withdrawn: ${gone.map(each => each.reason).join(' ')}`,
464
+ };
465
+ }
466
+ return {
467
+ status: 'missing',
468
+ reason: `experimental.iconSet names "${selector}", but no installed set has that name${sets.length > 0 ? ` (installed: ${sets.map(iconSetKey).join(', ')})` : ''}.`,
469
+ };
470
+ }
471
+
472
+ /** The command that chooses a main set and backups, as the CLI prints it. */
473
+ export const ICON_USE_ORDER_COMMAND =
474
+ 'astryx experimental icon use <main> --backup <set>';
475
+
476
+ /**
477
+ * Installed sets the project's list leaves out: the problem, and the fix that
478
+ * adds them as backups after the list's own. Doctor and `experimental icon
479
+ * use` print the same words.
480
+ * @param {number} installed how many sets are installed
481
+ * @param {string} main the main set as the list writes it
482
+ * @param {string[]} backups the list's backups, in order
483
+ * @param {string[]} unlisted the installed sets the list leaves out
484
+ * @returns {{problem: string, command: string, fix: string}}
485
+ */
486
+ export function unlistedIconSets(installed, main, backups, unlisted) {
487
+ const them = unlisted.length === 1 ? 'it' : 'them';
488
+ const command = `astryx experimental icon use ${main}${[...backups, ...unlisted].map(each => ` --backup ${each}`).join('')}`;
489
+ return {
490
+ problem: `${installed} icon sets are installed and ${unlisted.join(', ')} ${unlisted.length === 1 ? 'is' : 'are'} not in your list`,
491
+ command,
492
+ fix: `Add ${them} as backup with \`${command}\`, or uninstall ${them}.`,
493
+ };
494
+ }
495
+
496
+ /**
497
+ * Pick the project's icon set (FR8, FR17): the main set `experimental.iconSet`
498
+ * names, as `"<set>"` or `"<package>/<set>"`, or as the first entry of a list
499
+ * whose other entries are backup sets in order; otherwise the only installed
412
500
  * set; otherwise none. A configured name that matches no valid set but an
413
501
  * installed set that was withdrawn says so, with why.
414
502
  * @param {DiscoveredIconSet[]} sets installed sets, in integration load order
415
- * @param {string | undefined} configured the `experimental.iconSet` config value
503
+ * @param {string | readonly string[] | undefined} configured the `experimental.iconSet` config value
416
504
  * @param {WithdrawnIconSet[]} [withdrawn] installed sets that were withdrawn
417
505
  * @returns {IconSetChoice}
418
506
  */
419
507
  export function chooseProjectIconSet(sets, configured, withdrawn = []) {
420
- if (configured) {
421
- const exact = sets.filter(
422
- set => `${set.package}/${set.name}` === configured,
423
- );
424
- const byName =
425
- exact.length > 0 ? exact : sets.filter(set => set.name === configured);
426
- if (byName.length === 1) {
427
- return {
428
- status: 'selected',
429
- set: byName[0],
430
- configured,
431
- reason: `experimental.iconSet names "${configured}".`,
432
- };
433
- }
434
- if (byName.length > 1) {
435
- return {
436
- status: 'ambiguous',
437
- configured,
438
- reason: `experimental.iconSet "${configured}" names a set in ${byName.map(set => set.package).join(' and ')}; write it as "<package>/<set>".`,
439
- };
440
- }
441
- const gone = withdrawnNamed(withdrawn, configured);
442
- if (gone.length > 0) {
443
- return {
444
- status: 'withdrawn',
445
- configured,
446
- reason: `experimental.iconSet names "${configured}", which is installed but withdrawn: ${gone.map(each => each.reason).join(' ')}`,
447
- };
448
- }
449
- return {
450
- status: 'missing',
451
- configured,
452
- reason: `experimental.iconSet names "${configured}", but no installed set has that name${sets.length > 0 ? ` (installed: ${sets.map(iconSetKey).join(', ')})` : ''}.`,
508
+ const [main, ...rest] = iconSetList(configured);
509
+ if (main) {
510
+ const resolved = resolveSelector(sets, main, withdrawn);
511
+ /** @type {IconSetBackup[]} */
512
+ const backups = rest.map(selector => ({
513
+ selector,
514
+ ...resolveSelector(sets, selector, withdrawn),
515
+ }));
516
+ /** @type {IconSetChoice} */
517
+ const choice = {
518
+ status: resolved.status,
519
+ ...(resolved.set ? {set: resolved.set} : {}),
520
+ configured: main,
521
+ ...(backups.length > 0 ? {backups} : {}),
522
+ reason: resolved.reason,
453
523
  };
524
+ if (resolved.set) {
525
+ /** @type {DiscoveredIconSet[]} */
526
+ const order = [resolved.set];
527
+ for (const backup of backups) {
528
+ if (backup.set && !order.includes(backup.set)) order.push(backup.set);
529
+ }
530
+ choice.order = order;
531
+ if (order.length > 1) {
532
+ choice.reason = `experimental.iconSet names "${main}", with ${order
533
+ .slice(1)
534
+ .map(set => `"${iconSetKey(set)}"`)
535
+ .join(', then ')} as backup.`;
536
+ }
537
+ }
538
+ return choice;
454
539
  }
455
540
  if (sets.length === 1) {
456
541
  return {
457
542
  status: 'selected',
458
543
  set: sets[0],
544
+ order: [sets[0]],
459
545
  reason: `"${iconSetKey(sets[0])}" is the only installed icon set.`,
460
546
  };
461
547
  }
@@ -464,7 +550,7 @@ export function chooseProjectIconSet(sets, configured, withdrawn = []) {
464
550
  }
465
551
  return {
466
552
  status: 'ambiguous',
467
- reason: `${sets.length} icon sets are installed (${sets.map(iconSetKey).join(', ')}) and astryx.config sets no experimental.iconSet.`,
553
+ reason: `${sets.length} icon sets are installed (${sets.map(iconSetKey).join(', ')}) and astryx.config sets no experimental.iconSet, so none is your main set.`,
468
554
  };
469
555
  }
470
556
 
@@ -477,13 +563,13 @@ export function iconSetKey(set) {
477
563
  }
478
564
 
479
565
  /**
480
- * Sets in tie order (FR6, FR8): the project's set first, then load order.
566
+ * Sets in tie order (FR6, FR8): the project's set and its backups first, in
567
+ * list order, then load order.
481
568
  * @param {DiscoveredIconSet[]} sets
482
569
  * @param {IconSetChoice} choice
483
570
  * @returns {DiscoveredIconSet[]}
484
571
  */
485
572
  export function orderIconSets(sets, choice) {
486
- const first = choice.set;
487
- if (!first) return [...sets];
488
- return [first, ...sets.filter(set => set !== first)];
573
+ const first = choice.order ?? (choice.set ? [choice.set] : []);
574
+ return [...first, ...sets.filter(set => !first.includes(set))];
489
575
  }
@@ -11,5 +11,51 @@ export function templateIconNotes(loaded: any, packageDir: string): Promise<Arra
11
11
  code: string;
12
12
  message: string;
13
13
  }>>;
14
+ /**
15
+ * One item the undeclared-icon check could not judge.
16
+ * @typedef {object} TemplateIconUnchecked
17
+ * @property {string} file the template, relative to the package
18
+ * @property {string} icon the imported name
19
+ * @property {string} module the module it is imported from
20
+ * @property {string} reason why, and what would let the check judge it
21
+ */
22
+ /**
23
+ * One note per icon a template imports but does not declare in `icons`, and
24
+ * each import the check cannot judge. Only a package whose templates declare
25
+ * `icons` gets either: a package that never labels its icons sees no change.
26
+ * @param {any} loaded a loaded integration (absolute roots)
27
+ * @param {string} packageDir
28
+ * @returns {Promise<{notes: Array<{code: string, message: string}>, unchecked: TemplateIconUnchecked[]}>}
29
+ */
30
+ export function templateIconUndeclaredNotes(loaded: any, packageDir: string): Promise<{
31
+ notes: Array<{
32
+ code: string;
33
+ message: string;
34
+ }>;
35
+ unchecked: TemplateIconUnchecked[];
36
+ }>;
14
37
  /** A template `icons` key its source never imports. */
15
38
  export const TEMPLATE_ICON_NOT_IMPORTED: "template_icon_not_imported";
39
+ /** An icon a template imports that its `icons` does not declare. */
40
+ export const TEMPLATE_ICON_UNDECLARED: "template_icon_undeclared";
41
+ /**
42
+ * One item the undeclared-icon check could not judge.
43
+ */
44
+ export type TemplateIconUnchecked = {
45
+ /**
46
+ * the template, relative to the package
47
+ */
48
+ file: string;
49
+ /**
50
+ * the imported name
51
+ */
52
+ icon: string;
53
+ /**
54
+ * the module it is imported from
55
+ */
56
+ module: string;
57
+ /**
58
+ * why, and what would let the check judge it
59
+ */
60
+ reason: string;
61
+ };
@@ -1,18 +1,25 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file Template `icons` keys a template's source never imports (spec:AST-084
5
- * FR10).
4
+ * @file A template's `icons` checked against the icons its source imports.
6
5
  *
7
6
  * A template's `icons` names each icon it imports, by the name it imports or,
8
7
  * for a default import, its local name. A key that no import or re-export in
9
8
  * the template's source binds can never map to a project's icon set, so each
10
- * one gets a note with its fix.
9
+ * one gets a note with its fix. An icon the template imports but does not
10
+ * declare never maps either: in a package whose templates declare `icons`,
11
+ * each one gets a note too, which `validate` marks `next`. An import counts
12
+ * only when the template renders it as an element without children or
13
+ * passes it as a value. Any other import from that module, a generic
14
+ * renderer, and an import that matches an icon by its name only, are listed
15
+ * as not checked instead.
11
16
  *
12
17
  * @input A loaded integration (absolute roots) and its package directory.
13
- * @output One `template_icon_not_imported` note per such key. A note is not an
14
- * issue: it is never an error or a warning, and never fails a check, even
15
- * under `--strict` (architecture:cli-surface INV35).
18
+ * @output One `template_icon_not_imported` note per such key, one
19
+ * `template_icon_undeclared` note per undeclared icon, and each import not
20
+ * rendered as an icon, generic renderer, or import matched by its name
21
+ * only, as not checked. A note is never an error or a warning, even under
22
+ * `--strict`.
16
23
  * @position foundation/discovery; read by `doctor integration validate` (and
17
24
  * so `integration check`) for the package being authored.
18
25
  */
@@ -21,46 +28,117 @@ import * as fs from 'node:fs';
21
28
  import * as path from 'node:path';
22
29
  import {loadTopicModule} from './docs-discovery.mjs';
23
30
  import {discoverIntegrationTemplatesForOne} from './template-adapter.mjs';
24
- import {iconsNotImported} from './template-icons.mjs';
31
+ import {iconsNotImported, undeclaredTemplateIcons} from './template-icons.mjs';
25
32
 
26
33
  /** A template `icons` key its source never imports. */
27
34
  export const TEMPLATE_ICON_NOT_IMPORTED = 'template_icon_not_imported';
28
35
 
36
+ /** An icon a template imports that its `icons` does not declare. */
37
+ export const TEMPLATE_ICON_UNDECLARED = 'template_icon_undeclared';
38
+
29
39
  /**
30
- * One note per template `icons` key the template's source never imports.
40
+ * Each template in the package, with its source and its doc's `icons`. A
41
+ * template whose doc or source cannot be read is left out.
31
42
  * @param {any} loaded a loaded integration (absolute roots)
32
43
  * @param {string} packageDir
33
- * @returns {Promise<Array<{code: string, message: string}>>}
44
+ * @returns {Promise<Array<{id: string, docPath: string, source: string, declared: Record<string, string> | null}>>}
34
45
  */
35
- export async function templateIconNotes(loaded, packageDir) {
46
+ async function readTemplates(loaded, packageDir) {
36
47
  if (!loaded.templates) return [];
37
48
  const {templates} = await discoverIntegrationTemplatesForOne(loaded).catch(
38
49
  () => ({templates: []}),
39
50
  );
40
- /** @type {Array<{code: string, message: string}>} */
41
- const notes = [];
42
51
  /** @param {string} file */
43
52
  const shown = file =>
44
53
  path.relative(packageDir, file).split(path.sep).join('/');
54
+ /** @type {Array<{id: string, docPath: string, source: string, declared: Record<string, string> | null}>} */
55
+ const read = [];
45
56
  for (const template of templates) {
46
57
  if (!template.docPath || !template.filePath) continue;
47
58
  const doc = /** @type {any} */ (
48
59
  await loadTopicModule(template.docPath).catch(() => null)
49
60
  );
50
- const declared = doc?.icons;
51
- if (declared == null || typeof declared !== 'object') continue;
52
61
  let source;
53
62
  try {
54
63
  source = fs.readFileSync(template.filePath, 'utf-8');
55
64
  } catch {
56
65
  continue;
57
66
  }
58
- for (const key of iconsNotImported(source, declared) ?? []) {
67
+ read.push({
68
+ id: shown(template.filePath),
69
+ docPath: shown(template.docPath),
70
+ source,
71
+ declared:
72
+ doc?.icons != null && typeof doc.icons === 'object' ? doc.icons : null,
73
+ });
74
+ }
75
+ return read;
76
+ }
77
+
78
+ /**
79
+ * One note per template `icons` key the template's source never imports.
80
+ * @param {any} loaded a loaded integration (absolute roots)
81
+ * @param {string} packageDir
82
+ * @returns {Promise<Array<{code: string, message: string}>>}
83
+ */
84
+ export async function templateIconNotes(loaded, packageDir) {
85
+ /** @type {Array<{code: string, message: string}>} */
86
+ const notes = [];
87
+ for (const template of await readTemplates(loaded, packageDir)) {
88
+ if (!template.declared) continue;
89
+ for (const key of iconsNotImported(template.source, template.declared) ??
90
+ []) {
59
91
  notes.push({
60
92
  code: TEMPLATE_ICON_NOT_IMPORTED,
61
- message: `${shown(template.docPath)} declares icon "${key}" in \`icons\`, which ${shown(template.filePath)} does not import, so it never maps to a project's icon set. Name a named import by the name it imports and a default import by its local name, or remove "${key}" from \`icons\`.`,
93
+ message: `${template.docPath} declares icon "${key}" in \`icons\`, which ${template.id} does not import, so it never maps to a project's icon set. Name a named import by the name it imports and a default import by its local name, or remove "${key}" from \`icons\`.`,
62
94
  });
63
95
  }
64
96
  }
65
97
  return notes;
66
98
  }
99
+
100
+ /**
101
+ * One item the undeclared-icon check could not judge.
102
+ * @typedef {object} TemplateIconUnchecked
103
+ * @property {string} file the template, relative to the package
104
+ * @property {string} icon the imported name
105
+ * @property {string} module the module it is imported from
106
+ * @property {string} reason why, and what would let the check judge it
107
+ */
108
+
109
+ /**
110
+ * One note per icon a template imports but does not declare in `icons`, and
111
+ * each import the check cannot judge. Only a package whose templates declare
112
+ * `icons` gets either: a package that never labels its icons sees no change.
113
+ * @param {any} loaded a loaded integration (absolute roots)
114
+ * @param {string} packageDir
115
+ * @returns {Promise<{notes: Array<{code: string, message: string}>, unchecked: TemplateIconUnchecked[]}>}
116
+ */
117
+ export async function templateIconUndeclaredNotes(loaded, packageDir) {
118
+ const read = (await readTemplates(loaded, packageDir)).map(each => ({
119
+ ...each,
120
+ declared: each.declared ?? {},
121
+ }));
122
+ if (!read.some(each => Object.keys(each.declared).length > 0)) {
123
+ return {notes: [], unchecked: []};
124
+ }
125
+ const docOf = new Map(read.map(each => [each.id, each.docPath]));
126
+ const {undeclared, unchecked} = undeclaredTemplateIcons(read);
127
+ return {
128
+ notes: undeclared.map(({id, key, module}) => ({
129
+ code: TEMPLATE_ICON_UNDECLARED,
130
+ message: `${id} imports icon "${key}" from ${module}, but ${docOf.get(id)} does not declare it in \`icons\`, so it never follows a project's icon set. Add \`${key}: '<definition>'\` to \`icons\` (\`astryx experimental icon --definitions\` lists the definitions).`,
131
+ })),
132
+ unchecked: unchecked.map(({id, key, module, why}) => ({
133
+ file: id,
134
+ icon: key,
135
+ module,
136
+ reason:
137
+ why === 'renderer'
138
+ ? `"${key}" is a generic renderer: it is a bare \`Icon\`, or every use gives it an icon prop, so it draws no one icon definition and needs no \`icons\` key.`
139
+ : why === 'use'
140
+ ? `"${key}" is not rendered as an icon: the template never renders it as an element without children or passes it as a value, so it needs no \`icons\` key. Declaring it would make \`astryx template\` swap it for an icon.`
141
+ : `"${key}" matches an icon by its name only: no template in the package declares an icon from ${module}, so the check cannot tell whether it draws one icon definition. Declare an icon from ${module} in a template's \`icons\` to have its imports checked.`,
142
+ })),
143
+ };
144
+ }