@astryxdesign/cli 0.7.12 → 0.7.13-canary.1719285

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 (82) hide show
  1. package/api/doctor/_adapter.d.mts +17 -0
  2. package/api/doctor/_adapter.mjs +40 -0
  3. package/api/doctor/doctor.d.mts +34 -4
  4. package/api/doctor/doctor.mjs +108 -21
  5. package/api/icon/_adapter.d.mts +6 -3
  6. package/api/icon/_adapter.mjs +11 -3
  7. package/api/icon/icon.type.d.mts +22 -1
  8. package/api/icon/icon.type.mjs +12 -4
  9. package/api/icon/iconUse.doc.mjs +27 -7
  10. package/api/icon/list/list.mjs +29 -3
  11. package/api/icon/use/use.d.mts +7 -3
  12. package/api/icon/use/use.mjs +68 -18
  13. package/api/integration/validate-integration.d.mts +4 -0
  14. package/api/integration/validate-integration.mjs +32 -2
  15. package/api/integration/validate-integration.type.d.mts +15 -0
  16. package/api/integration/validate-integration.type.mjs +6 -1
  17. package/api/template/copy/copy.mjs +4 -2
  18. package/api/template/template.type.d.mts +42 -4
  19. package/api/template/template.type.mjs +9 -6
  20. package/assets/discover/official-packages.json +4 -0
  21. package/assets/docs/tree/icon-sets.doc.mjs +91 -7
  22. package/assets/icons/definitions/alignBottom.doc.mjs +9 -0
  23. package/assets/icons/definitions/alignJustify.doc.mjs +9 -0
  24. package/assets/icons/definitions/alignMiddle.doc.mjs +9 -0
  25. package/assets/icons/definitions/alignTop.doc.mjs +9 -0
  26. package/assets/icons/definitions/angle.doc.mjs +9 -0
  27. package/assets/icons/definitions/cloud.doc.mjs +9 -0
  28. package/assets/icons/definitions/collection.doc.mjs +9 -0
  29. package/assets/icons/definitions/contrast.doc.mjs +9 -0
  30. package/assets/icons/definitions/cornerRadius.doc.mjs +9 -0
  31. package/assets/icons/definitions/cut.doc.mjs +9 -0
  32. package/assets/icons/definitions/fingerprint.doc.mjs +9 -0
  33. package/assets/icons/definitions/fire.doc.mjs +9 -0
  34. package/assets/icons/definitions/frame.doc.mjs +9 -0
  35. package/assets/icons/definitions/gift.doc.mjs +9 -0
  36. package/assets/icons/definitions/group.doc.mjs +9 -0
  37. package/assets/icons/definitions/integration.doc.mjs +9 -0
  38. package/assets/icons/definitions/lowercase.doc.mjs +9 -0
  39. package/assets/icons/definitions/paddingHorizontal.doc.mjs +9 -0
  40. package/assets/icons/definitions/paddingPerSide.doc.mjs +9 -0
  41. package/assets/icons/definitions/paddingVertical.doc.mjs +9 -0
  42. package/assets/icons/definitions/panelBottom.doc.mjs +9 -0
  43. package/assets/icons/definitions/photoEffect.doc.mjs +9 -0
  44. package/assets/icons/definitions/radiusPerCorner.doc.mjs +9 -0
  45. package/assets/icons/definitions/rotateCounterclockwise.doc.mjs +9 -0
  46. package/assets/icons/definitions/tablet.doc.mjs +9 -0
  47. package/assets/icons/definitions/text.doc.mjs +9 -0
  48. package/assets/icons/definitions/textRegular.doc.mjs +9 -0
  49. package/assets/icons/definitions/transfer.doc.mjs +9 -0
  50. package/assets/icons/definitions/uppercase.doc.mjs +9 -0
  51. package/assets/icons/definitions/userRemove.doc.mjs +9 -0
  52. package/assets/templates/blocks/components/BaseTypeahead/BaseTypeaheadMenu.doc.mjs +23 -0
  53. package/assets/templates/blocks/components/BaseTypeahead/BaseTypeaheadMenu.tsx +199 -0
  54. package/assets/templates/blocks/components/ButtonGroup/ButtonGroupShowcase.doc.mjs +1 -0
  55. package/assets/templates/blocks/components/ChatSystemMessage/ChatSystemMessageStatusUpdates.doc.mjs +7 -2
  56. package/assets/templates/pages/canvas-editor/template.doc.mjs +20 -0
  57. package/assets/templates/pages/editor/template.doc.mjs +1 -0
  58. package/assets/templates/pages/form-wizard-vertical/template.doc.mjs +1 -0
  59. package/assets/templates/pages/settings-dialog/template.doc.mjs +4 -0
  60. package/assets/templates/pages/shell-top-nav/template.doc.mjs +3 -0
  61. package/assets/templates/pages/table-collapsible/template.doc.mjs +1 -0
  62. package/assets/templates/pages/table-comparison/template.doc.mjs +1 -0
  63. package/authoring/config/config.doc.mjs +4 -4
  64. package/authoring/config/parse.d.mts +1 -1
  65. package/authoring/config/parse.mjs +3 -1
  66. package/authoring/config/type.ts +7 -5
  67. package/authoring/doctypes/template/template.doc.mjs +1 -1
  68. package/authoring/doctypes/template/type.ts +5 -3
  69. package/clients/cli/commands/experimental/icon.mjs +38 -9
  70. package/clients/cli/commands/experimental-icon-use.doc.mjs +9 -4
  71. package/clients/cli/commands/template.mjs +12 -3
  72. package/foundation/config/config-writer.d.mts +14 -0
  73. package/foundation/config/config-writer.mjs +73 -17
  74. package/foundation/discovery/icon-set-discovery.d.mts +77 -7
  75. package/foundation/discovery/icon-set-discovery.mjs +128 -42
  76. package/foundation/discovery/template-icon-notes.d.mts +46 -0
  77. package/foundation/discovery/template-icon-notes.mjs +94 -16
  78. package/foundation/discovery/template-icons.d.mts +247 -0
  79. package/foundation/discovery/template-icons.mjs +591 -37
  80. package/foundation/integrations/cli-requirement.d.mts +13 -4
  81. package/foundation/integrations/cli-requirement.mjs +18 -6
  82. package/package.json +6 -6
@@ -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
+ }