@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
@@ -18,27 +18,47 @@ export const doc = {
18
18
  isReady: true,
19
19
  category: 'Tools - Canvas Editor',
20
20
  icons: {
21
+ Aperture: 'photoEffect',
22
+ ArrowDownToLine: 'alignBottom',
23
+ ArrowUpToLine: 'alignTop',
24
+ Baseline: 'textRegular',
25
+ CaseLower: 'lowercase',
21
26
  CaseSensitive: 'textCase',
27
+ CaseUpper: 'uppercase',
28
+ Contrast: 'contrast',
22
29
  Download: 'download',
23
30
  File: 'file',
24
31
  FlipHorizontal2: 'flipHorizontal',
25
32
  FlipVertical2: 'flipVertical',
33
+ FoldVertical: 'alignMiddle',
34
+ Frame: 'frame',
26
35
  Grip: 'drag',
36
+ Group: 'group',
27
37
  Image: 'image',
28
38
  Italic: 'italic',
29
39
  Lock: 'lock',
30
40
  LockOpen: 'unlock',
31
41
  Menu: 'menu',
32
42
  Minus: 'remove',
43
+ PanelBottom: 'panelBottom',
33
44
  PanelLeft: 'panelLeft',
45
+ PanelLeftRightDashed: 'paddingHorizontal',
34
46
  PanelRight: 'panelRight',
47
+ PanelTopBottomDashed: 'paddingVertical',
35
48
  Plus: 'add',
36
49
  Redo2: 'redo',
50
+ RotateCcw: 'rotateCounterclockwise',
37
51
  RotateCw: 'rotate',
52
+ Scan: 'cornerRadius',
53
+ SquareRoundCorner: 'radiusPerCorner',
54
+ SquareSquare: 'paddingPerSide',
38
55
  Strikethrough: 'strikethrough',
39
56
  TextAlignCenter: 'alignCenter',
40
57
  TextAlignEnd: 'alignEnd',
58
+ TextAlignJustify: 'alignJustify',
41
59
  TextAlignStart: 'alignStart',
60
+ TriangleRight: 'angle',
61
+ Type: 'text',
42
62
  Underline: 'underline',
43
63
  Undo2: 'undo',
44
64
  X: 'close',
@@ -25,6 +25,7 @@ export const doc = {
25
25
  ComputerDesktopIcon: 'desktop',
26
26
  CursorArrowRaysIcon: 'click',
27
27
  DevicePhoneMobileIcon: 'mobile',
28
+ DeviceTabletIcon: 'tablet',
28
29
  DocumentTextIcon: 'document',
29
30
  EllipsisHorizontalIcon: 'moreHorizontal',
30
31
  EyeIcon: 'eye',
@@ -18,6 +18,7 @@ export const doc = {
18
18
  ExclamationTriangleIcon: 'warning',
19
19
  GlobeAltIcon: 'globe',
20
20
  LanguageIcon: 'language',
21
+ RectangleStackIcon: 'collection',
21
22
  TagIcon: 'tag',
22
23
  UserIcon: 'user',
23
24
  },
@@ -21,6 +21,7 @@ export const doc = {
21
21
  ArrowDownTrayIcon: 'download',
22
22
  ArrowUturnLeftIcon: 'undo',
23
23
  AtSymbolIcon: 'mention',
24
+ Bars3BottomLeftIcon: 'panelLeft',
24
25
  Bars3Icon: 'menu',
25
26
  BeakerIcon: 'experiment',
26
27
  BellIcon: 'notification',
@@ -40,6 +41,7 @@ export const doc = {
40
41
  DocumentTextIcon: 'document',
41
42
  EnvelopeIcon: 'mail',
42
43
  EyeIcon: 'eye',
44
+ FingerPrintIcon: 'fingerprint',
43
45
  FolderIcon: 'folder',
44
46
  GlobeAltIcon: 'globe',
45
47
  HandRaisedIcon: 'raiseHand',
@@ -50,7 +52,9 @@ export const doc = {
50
52
  MagnifyingGlassIcon: 'search',
51
53
  MapPinIcon: 'location',
52
54
  MoonIcon: 'moon',
55
+ PaintBrushIcon: 'swatch',
53
56
  PencilSquareIcon: 'compose',
57
+ PuzzlePieceIcon: 'integration',
54
58
  QuestionMarkCircleIcon: 'info',
55
59
  ReceiptPercentIcon: 'discount',
56
60
  ShieldCheckIcon: 'shield',
@@ -20,7 +20,10 @@ export const doc = {
20
20
  category: 'Shell - Top Nav',
21
21
  icons: {
22
22
  BoltIcon: 'bolt',
23
+ CloudIcon: 'cloud',
23
24
  FaceSmileIcon: 'emoji',
25
+ FireIcon: 'fire',
26
+ GiftIcon: 'gift',
24
27
  GlobeAltIcon: 'globe',
25
28
  HomeModernIcon: 'home',
26
29
  MagnifyingGlassIcon: 'search',
@@ -10,6 +10,7 @@ export const doc = {
10
10
  isReady: true,
11
11
  category: 'Table - Grouped',
12
12
  icons: {
13
+ ArrowsRightLeftIcon: 'transfer',
13
14
  BuildingLibraryIcon: 'bank',
14
15
  CreditCardIcon: 'creditCard',
15
16
  PresentationChartLineIcon: 'activity',
@@ -19,6 +19,7 @@ export const doc = {
19
19
  Image: 'image',
20
20
  Plus: 'add',
21
21
  Sparkles: 'sparkles',
22
+ Type: 'text',
22
23
  Video: 'video',
23
24
  Zap: 'bolt',
24
25
  },
@@ -68,7 +68,7 @@ export const doc = {
68
68
  description:
69
69
  'Tell `{@command discover}` which integrations this project could add: an async function that returns a catalog. An integration can provide one too, as a `discover` named export from its manifest. Discover calls every source, yours first, and one that fails never hides the others. Discover only reads; your package manager installs.',
70
70
  example:
71
- "async ({signal, package: name, version}) => fetchCatalog({signal, name, version})",
71
+ 'async ({signal, package: name, version}) => fetchCatalog({signal, name, version})',
72
72
  },
73
73
  {
74
74
  name: 'experimental',
@@ -77,11 +77,11 @@ export const doc = {
77
77
  fields: [
78
78
  {
79
79
  name: 'experimental.iconSet',
80
- type: 'string',
80
+ type: 'string | string[]',
81
81
  stability: 'experimental',
82
82
  description:
83
- "Experimental: may change or be removed in a patch release. The project's icon set, by set name or as `<package>/<set>`. It is the opt-in: copied templates use its icons, and `astryx discover` and `astryx doctor` report icon sets, only when it is set. `astryx experimental icon` ranks its icons first; without it, the only installed set is the project's there. `astryx search` never returns icons.",
84
- example: "'@acme/outline-icons/outline'",
83
+ "Experimental: may change or be removed in a patch release. The project's main icon set, by set name or as `<package>/<set>`; or a list of them, the main set first and then backup sets in order. A copied template takes each icon from the first set in the list that has its meaning, and draws a placeholder the copied file declares when none has it. It is the opt-in: copied templates use these icons, and `astryx discover` and `astryx doctor` report icon sets, only when it is set. `astryx experimental icon use <main> --backup <set>` writes it. `astryx experimental icon` ranks the main set's icons first; without it, the only installed set is the project's there. `astryx search` never returns icons.",
84
+ example: "['@acme/outline-icons/outline', '@acme/brand-icons/brand']",
85
85
  },
86
86
  {
87
87
  name: 'experimental.xle.components',
@@ -58,7 +58,7 @@ declare const configSchema: z.ZodObject<{
58
58
  default: z.ZodOptional<z.ZodBoolean>;
59
59
  }, z.core.$strict>>>;
60
60
  }, z.core.$strict>>;
61
- iconSet: z.ZodOptional<z.ZodString>;
61
+ iconSet: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
62
62
  }, z.core.$strict>>;
63
63
  }, z.core.$strict>;
64
64
  export {};
@@ -103,7 +103,9 @@ const configSchema = z
103
103
  })
104
104
  .strict()
105
105
  .optional(),
106
- iconSet: z.string().min(1).optional(),
106
+ iconSet: z
107
+ .union([z.string().min(1), z.array(z.string().min(1)).min(1)])
108
+ .optional(),
107
109
  })
108
110
  .strict()
109
111
  .optional(),
@@ -123,12 +123,14 @@ export interface AstryxConfig {
123
123
  components?: Record<string, XleComponent>;
124
124
  };
125
125
  /**
126
- * Experimental icon sets (spec:AST-084): the project's icon set, a set
127
- * name (`"outline"`) or `"<package>/<set>"`, and the opt-in. Copied
128
- * templates use its icons, and discover and Doctor report icon sets, only
129
- * when it is set.
126
+ * Experimental icon sets (spec:AST-084): the project's main icon set, a
127
+ * set name (`"outline"`) or `"<package>/<set>"`, and the opt-in. A list
128
+ * (`["outline", "@acme/icons/brand"]`) names the main set first, then
129
+ * backup sets in order: a copied template takes each icon from the first
130
+ * set that has its meaning. Copied templates use these icons, and
131
+ * discover and Doctor report icon sets, only when it is set.
130
132
  * @experimental May change or be removed in a patch release.
131
133
  */
132
- iconSet?: string;
134
+ iconSet?: string | string[];
133
135
  };
134
136
  }
@@ -69,7 +69,7 @@ export const doc = {
69
69
  type: 'Record<string, string>',
70
70
  stability: 'experimental',
71
71
  description:
72
- "The icon definition each icon the source imports draws, such as `{TrashIcon: 'delete'}`: a named import is keyed by the name it imports (`{Trash as TrashIcon}` is `Trash`), a default import by its local name (`import DeleteIcon from …` is `DeleteIcon`). When the project sets `experimental.iconSet`, `astryx template` (shown or copied) replaces each declared icon with the set's icon for that definition (`astryx docs cli/experimental/icon-definitions`) and reports every key as mapped or kept; an undeclared import is left alone. A name imported from two modules (`TrashIcon` from an outline and a solid module) needs one key, and each import keeps its variant when its module belongs to an installed set.",
72
+ "The icon definition each icon the source imports draws, such as `{TrashIcon: 'delete'}`: a named import is keyed by the name it imports (`{Trash as TrashIcon}` is `Trash`), a default import by its local name (`import DeleteIcon from …` is `DeleteIcon`). When the project sets `experimental.iconSet`, `astryx template` (shown or copied) replaces each declared icon with the icon for that definition (`astryx docs cli/experimental/icon-definitions`) from the first set in the project's list that maps it, or with a placeholder the copied file declares when no set does, and reports every key as mapped, a placeholder, or kept; an undeclared import is left alone, and `astryx integration check` notes it (`template_icon_undeclared`, failing `--strict --next`) in a package whose templates declare `icons`. A name imported from two modules (`TrashIcon` from an outline and a solid module) needs one key, and each import keeps its variant when its module belongs to an installed set.",
73
73
  },
74
74
  {
75
75
  name: 'isReady',
@@ -55,9 +55,11 @@ export interface BaseTemplateDoc extends AuthoredDocGraphFields {
55
55
  * (`{TrashIcon: 'delete'}`), keyed by the name a named import reads
56
56
  * (`{Trash as TrashIcon}` is `Trash`) or a default import's local name
57
57
  * (`import DeleteIcon from …` is `DeleteIcon`). When the project has an
58
- * icon set, each declared icon is replaced by the set's icon for that
59
- * definition (`astryx docs cli/experimental/icon-definitions`), and every
60
- * key is reported as mapped or kept. A name imported from two modules
58
+ * icon set, each declared icon is replaced by the icon for that
59
+ * definition (`astryx docs cli/experimental/icon-definitions`) from the
60
+ * first set in the project's list that maps it, or by a placeholder the
61
+ * copied file declares when no set does, and every key is reported as mapped, a
62
+ * placeholder, or kept. A name imported from two modules
61
63
  * (`TrashIcon` from an outline and a solid module) needs one key.
62
64
  * @experimental May change or be removed in a patch release. */
63
65
  icons?: Record<string, string>;
@@ -256,10 +256,17 @@ export function registerExperimentalIcon(group, program) {
256
256
  return answered;
257
257
  }
258
258
 
259
- const {status, projectSet, reason, sets, withdrawn} = result.data;
259
+ const {status, projectSet, reason, sets, withdrawn, order, next} =
260
+ result.data;
260
261
  emit(
261
262
  section('Icon sets'),
262
- record({status, projectSet, reason}),
263
+ record({
264
+ status,
265
+ projectSet,
266
+ ...(order.length > 1 ? {order: order.join(' > ')} : {}),
267
+ reason,
268
+ }),
269
+ ...(next ? [text(`[warn] ${next}`)] : []),
263
270
  ...(sets.length > 0
264
271
  ? [
265
272
  records(
@@ -283,6 +290,7 @@ export function registerExperimentalIcon(group, program) {
283
290
  'definitions',
284
291
  'own',
285
292
  'missing',
293
+ 'role',
286
294
  'project',
287
295
  'description',
288
296
  ],
@@ -346,21 +354,42 @@ export function registerExperimentalIcon(group, program) {
346
354
 
347
355
  defineCommand(iconCli, iconUseCommand, {
348
356
  fn: iconUseFn,
349
- action: (/** @type {string} */ set) =>
357
+ action: (
358
+ /** @type {string} */ set,
359
+ /** @type {{backup?: string[]}} */ options,
360
+ ) =>
350
361
  run(
351
- () => iconUse(set, {cwd: process.cwd()}),
352
- ({data}) =>
362
+ () => iconUse(set, {cwd: process.cwd(), backups: options.backup ?? []}),
363
+ ({data}) => {
364
+ /** @type {Array<{iconSet: string, set: string}>} */
365
+ const backups = data.backups;
366
+ const shown = (/** @type {string | string[]} */ value) =>
367
+ Array.isArray(value) ? JSON.stringify(value) : `"${value}"`;
368
+ const main = `"${data.iconSet}" (${data.set})`;
369
+ const order =
370
+ backups.length > 0
371
+ ? `main set ${main}, then ${backups.map(each => `"${each.iconSet}" (${each.set})`).join(', then ')}`
372
+ : main;
353
373
  emit(
354
374
  text(
355
375
  data.changed
356
- ? `[ok] experimental.iconSet is now "${data.iconSet}" (${data.set}) in ${data.config}${data.created ? ', created' : ''}.`
357
- : `[ok] experimental.iconSet already names "${data.iconSet}" in ${data.config}.`,
376
+ ? `[ok] experimental.iconSet is now ${order} in ${data.config}${data.created ? ', created' : ''}.`
377
+ : `[ok] experimental.iconSet already names ${order} in ${data.config}.`,
358
378
  ),
379
+ ...(backups.length > 0
380
+ ? [
381
+ text(
382
+ 'A copied template takes each icon from the first set in this order that has its meaning.',
383
+ ),
384
+ ]
385
+ : []),
359
386
  ...(data.previous && data.changed
360
- ? [text(`It was "${data.previous}".`)]
387
+ ? [text(`It was ${shown(data.previous)}.`)]
361
388
  : []),
389
+ ...(data.next ? [text(`[warn] ${data.next}`)] : []),
362
390
  ...(data.warning ? [text(`[warn] ${data.warning}`)] : []),
363
- ),
391
+ );
392
+ },
364
393
  ),
365
394
  });
366
395
 
@@ -9,13 +9,18 @@ export const doc = {
9
9
  displayName: 'astryx experimental icon use',
10
10
  namespace: 'cli/commands',
11
11
  stability: 'experimental',
12
- summary: "Choose the project's icon set",
12
+ summary: "Choose the project's main icon set and backups",
13
13
  description:
14
- "Writes `experimental.iconSet` in astryx.config, so copied templates and blocks use that set's icons. The set must be installed. Experimental: it may change or be removed in a patch release.",
14
+ "Writes `experimental.iconSet` in astryx.config, so copied templates and blocks use that set's icons. With `--backup`, it writes a list: the main set first, then each backup in the order given. A copied template takes each icon from the first set in the list that has its meaning. Every set must be installed. Experimental: it may change or be removed in a patch release.",
15
15
  fn: 'iconUse',
16
16
  args: [{name: 'set', param: 'set', required: true}],
17
+ options: [{flag: '--backup <set...>', param: 'options.backups'}],
17
18
  examples: [
18
19
  {label: 'Use a set', cli: 'astryx experimental icon use outline'},
20
+ {
21
+ label: 'Use a main set with a backup',
22
+ cli: 'astryx experimental icon use outline --backup @acme/brand-icons/brand',
23
+ },
19
24
  {
20
25
  label: 'Name its package',
21
26
  cli: 'astryx experimental icon use @acme/icons/outline',
@@ -25,11 +30,11 @@ export const doc = {
25
30
  exitCodes: [
26
31
  {
27
32
  code: 0,
28
- when: 'experimental.iconSet is written, or already names the set',
33
+ when: 'experimental.iconSet is written, or already names the sets',
29
34
  },
30
35
  {
31
36
  code: 1,
32
- when: 'the set is not installed, two packages ship it, the config cannot be updated (not a static object literal, or it spreads another object, which you then edit by hand), or the config is protected (a symbolic link, or marked generated, vendored, or ignored: `ERR_CODEMOD_PROTECTED`, naming the declaration)',
37
+ when: 'a set is not installed, two packages ship it, a set is named twice, the config cannot be updated (not a static object literal, or it spreads another object, which you then edit by hand), or the config is protected (a symbolic link, or marked generated, vendored, or ignored: `ERR_CODEMOD_PROTECTED`, naming the declaration)',
33
38
  },
34
39
  ],
35
40
  related: ['experimental icon', 'template'],
@@ -57,9 +57,10 @@ function demoContentNote(count, file) {
57
57
  }
58
58
 
59
59
  /**
60
- * The icon report as text lines (AST-084 FR10): one line per mapped or kept
61
- * icon, or one line saying why nothing was mapped. Nothing when the project
62
- * has no icon set at all (no report: icon sets are experimental).
60
+ * The icon report as text lines (AST-084 FR10): one line per mapped,
61
+ * placeholder, or kept icon, or one line saying why nothing was mapped.
62
+ * Nothing when the project has no icon set at all (no report: icon sets are
63
+ * experimental). A placeholder line names its meaning and both fixes.
63
64
  * @param {import('../../../api/template/template.type.mjs').TemplateIconsReport | undefined} icons
64
65
  * @returns {string[]}
65
66
  */
@@ -74,10 +75,18 @@ function iconLines(icons) {
74
75
  ...icons.mapped.map(
75
76
  each =>
76
77
  `icons: mapped ${each.source.export} (${each.source.module}) -> ${each.target.export} (${each.target.module}) by ${each.by}` +
78
+ (icons.order ? ` from ${each.target.package}/${each.target.set}` : '') +
79
+ (each.variant
80
+ ? `; ${each.variant.asked} look not in the set, drew ${each.variant.used ?? 'its default look'}`
81
+ : '') +
77
82
  (each.dropped && each.dropped.length > 0
78
83
  ? `; dropped ${each.dropped.join(', ')} (size comes from CSS)`
79
84
  : ''),
80
85
  ),
86
+ ...(icons.placeholder ?? []).map(
87
+ each =>
88
+ `icons: placeholder ${each.source.export} (${each.source.module}) -> ${each.target.local}: no set in your list has "${each.definitions.join('", "')}", so the file draws a placeholder it declares. Fix: add a backup set that has it, \`${each.fixes[1]}\`; or, if you author your main set, map it there, \`${each.fixes[0]}\`.`,
89
+ ),
81
90
  ...icons.kept.map(
82
91
  each =>
83
92
  `icons: kept ${each.export}${each.module == null ? '' : ` (${each.module})`}: ${each.reason}` +
@@ -14,6 +14,20 @@ export function setConfigString(projectDir: string, key: string, value: string):
14
14
  previous: string | null;
15
15
  changed: boolean;
16
16
  };
17
+ /**
18
+ * Set `key` to `value`, a string or a list of strings, in the project's
19
+ * astryx.config.
20
+ * @param {string} projectDir
21
+ * @param {string} key a top-level key, or a dotted path (`experimental.iconSet`)
22
+ * @param {string | readonly string[]} value
23
+ * @returns {{file: string, created: boolean, previous: string | string[] | null, changed: boolean}}
24
+ */
25
+ export function setConfigValue(projectDir: string, key: string, value: string | readonly string[]): {
26
+ file: string;
27
+ created: boolean;
28
+ previous: string | string[] | null;
29
+ changed: boolean;
30
+ };
17
31
  /** A protected astryx.config the writer refused to touch. */
18
32
  export class ConfigProtectedError extends Error {
19
33
  /**
@@ -1,7 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file Write one string field of a project's astryx.config.
4
+ * @file Write one string field, or one list of strings, in a project's
5
+ * astryx.config.
5
6
  *
6
7
  * The field is a top-level key or a dotted path into nested object literals
7
8
  * (`experimental.iconSet`); a missing object on the path is created.
@@ -18,7 +19,8 @@
18
19
  * `ERR_CODEMOD_PROTECTED` naming the protecting declaration, and an unreadable
19
20
  * protection source fails with `ERR_CODEMOD_PROTECTION_SOURCE`.
20
21
  *
21
- * @input A project directory, a field name, and its string value
22
+ * @input A project directory, a field name, and its value: a string or a
23
+ * list of strings
22
24
  * @output The config file written and the value it replaced
23
25
  * @position packages/cli/foundation/config — read by `astryx experimental icon use`
24
26
  */
@@ -87,6 +89,47 @@ function propertyNamed(property, name) {
87
89
  );
88
90
  }
89
91
 
92
+ /** @param {string} text */
93
+ const quote = text => `'${text.replace(/\\/gu, '\\\\').replace(/'/gu, "\\'")}'`;
94
+
95
+ /** @param {string | readonly string[]} value */
96
+ const written = value =>
97
+ typeof value === 'string'
98
+ ? quote(value)
99
+ : `[${value.map(each => quote(each)).join(', ')}]`;
100
+
101
+ /**
102
+ * The string, or list of strings, a config property holds; null for any
103
+ * other value.
104
+ * @param {any} node
105
+ * @returns {string | string[] | null}
106
+ */
107
+ function literalValue(node) {
108
+ if (node?.type === 'StringLiteral' || node?.type === 'Literal') {
109
+ return typeof node.value === 'string' ? node.value : null;
110
+ }
111
+ if (node?.type === 'ArrayExpression') {
112
+ const items = node.elements.map((/** @type {any} */ each) =>
113
+ each?.type === 'StringLiteral' ||
114
+ (each?.type === 'Literal' && typeof each.value === 'string')
115
+ ? String(each.value)
116
+ : null,
117
+ );
118
+ return items.every((/** @type {string | null} */ each) => each != null)
119
+ ? /** @type {string[]} */ (items)
120
+ : null;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * @param {string | string[] | null} a
127
+ * @param {string | readonly string[]} b
128
+ */
129
+ function sameValue(a, b) {
130
+ return JSON.stringify(a) === JSON.stringify(b);
131
+ }
132
+
90
133
  /**
91
134
  * Set `key` to `value` in the project's astryx.config.
92
135
  * @param {string} projectDir
@@ -95,6 +138,22 @@ function propertyNamed(property, name) {
95
138
  * @returns {{file: string, created: boolean, previous: string | null, changed: boolean}}
96
139
  */
97
140
  export function setConfigString(projectDir, key, value) {
141
+ const result = setConfigValue(projectDir, key, value);
142
+ return {
143
+ ...result,
144
+ previous: typeof result.previous === 'string' ? result.previous : null,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Set `key` to `value`, a string or a list of strings, in the project's
150
+ * astryx.config.
151
+ * @param {string} projectDir
152
+ * @param {string} key a top-level key, or a dotted path (`experimental.iconSet`)
153
+ * @param {string | readonly string[]} value
154
+ * @returns {{file: string, created: boolean, previous: string | string[] | null, changed: boolean}}
155
+ */
156
+ export function setConfigValue(projectDir, key, value) {
98
157
  const keys = key.split('.');
99
158
  const leaf = /** @type {string} */ (keys[keys.length - 1]);
100
159
  const existing = CONFIG_BASENAMES.map(name =>
@@ -103,7 +162,6 @@ export function setConfigString(projectDir, key, value) {
103
162
  if (!existing) {
104
163
  const file = path.join(projectDir, 'astryx.config.mjs');
105
164
  assertWritable(projectDir, file);
106
- const quoted = `'${value.replace(/\\/gu, '\\\\').replace(/'/gu, "\\'")}'`;
107
165
  // Each key on the path opens one nested object: `experimental: {`.
108
166
  const open = keys
109
167
  .slice(0, -1)
@@ -116,7 +174,7 @@ export function setConfigString(projectDir, key, value) {
116
174
  .join('');
117
175
  writeAtomically(
118
176
  file,
119
- `export default {\n${open}${' '.repeat(keys.length)}${leaf}: ${quoted},\n${close}};\n`,
177
+ `export default {\n${open}${' '.repeat(keys.length)}${leaf}: ${written(value)},\n${close}};\n`,
120
178
  );
121
179
  return {file, created: true, previous: null, changed: true};
122
180
  }
@@ -127,7 +185,7 @@ export function setConfigString(projectDir, key, value) {
127
185
  let object = findManifestObject(j, ast);
128
186
  const refuse = () =>
129
187
  new Error(
130
- `Could not update ${path.basename(existing)} safely: its default export is not a static object literal. Set "${key}: '${value}'" in it yourself.`,
188
+ `Could not update ${path.basename(existing)} safely: its default export is not a static object literal. Set "${key}: ${written(value)}" in it yourself.`,
131
189
  );
132
190
  if (!object) throw refuse();
133
191
  // A spread on the path can hold the same keys: a key added beside it would
@@ -137,7 +195,7 @@ export function setConfigString(projectDir, key, value) {
137
195
  const assertNoSpread = (holder, where) => {
138
196
  if (holder.properties.some((/** @type {any} */ each) => isSpread(each))) {
139
197
  throw new Error(
140
- `Could not update ${path.basename(existing)} safely: ${where} spreads another object (\`...\`), whose keys a written "${key}" could replace. Set "${key}: '${value}'" in it yourself.`,
198
+ `Could not update ${path.basename(existing)} safely: ${where} spreads another object (\`...\`), whose keys a written "${key}" could replace. Set "${key}: ${written(value)}" in it yourself.`,
141
199
  );
142
200
  }
143
201
  };
@@ -160,22 +218,20 @@ export function setConfigString(projectDir, key, value) {
160
218
  const property = object.properties.find((/** @type {any} */ each) =>
161
219
  propertyNamed(each, leaf),
162
220
  );
163
- /** @type {string | null} */
221
+ const node =
222
+ typeof value === 'string'
223
+ ? j.stringLiteral(value)
224
+ : j.arrayExpression(value.map(each => j.stringLiteral(each)));
225
+ /** @type {string | string[] | null} */
164
226
  let previous = null;
165
227
  if (property) {
166
- const current = property.value;
167
- previous =
168
- current?.type === 'StringLiteral' || current?.type === 'Literal'
169
- ? String(current.value)
170
- : null;
171
- if (previous === value) {
228
+ previous = literalValue(property.value);
229
+ if (sameValue(previous, value)) {
172
230
  return {file: existing, created: false, previous, changed: false};
173
231
  }
174
- property.value = j.stringLiteral(value);
232
+ property.value = node;
175
233
  } else {
176
- object.properties.push(
177
- j.objectProperty(j.identifier(leaf), j.stringLiteral(value)),
178
- );
234
+ object.properties.push(j.objectProperty(j.identifier(leaf), node));
179
235
  }
180
236
  let next = ast.toSource({quote: 'single', reuseWhitespace: true});
181
237
  if (source.endsWith('\n') && !next.endsWith('\n')) next += '\n';