@astryxdesign/cli 0.7.12-canary.e0821c1 → 0.7.12-canary.e171d97

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 (77) hide show
  1. package/api/build/blocks/blocks.d.mts +41 -0
  2. package/api/build/blocks/blocks.mjs +118 -68
  3. package/api/component/__fixtures__/resolutions/modules.txt +3 -3
  4. package/api/icon/_authoring.mjs +8 -1
  5. package/api/icon/add/add.mjs +45 -5
  6. package/api/icon/icon.type.d.mts +24 -0
  7. package/api/icon/icon.type.mjs +6 -0
  8. package/api/icon/iconAdd.doc.mjs +1 -1
  9. package/api/icon/map/map.mjs +11 -4
  10. package/api/integration/add-helpers.d.mts +5 -1
  11. package/api/integration/add-helpers.mjs +1 -1
  12. package/api/render/_adapter.mjs +9 -0
  13. package/api/render/render.doc.mjs +3 -3
  14. package/api/render/render.type.d.mts +1 -1
  15. package/api/render/render.type.mjs +1 -1
  16. package/api/search/ORDERING.md +3 -0
  17. package/api/search/_adapter.d.mts +10 -2
  18. package/api/search/_adapter.mjs +13 -2
  19. package/api/search/_fit-order.d.mts +47 -0
  20. package/api/search/_fit-order.mjs +257 -0
  21. package/api/search/_main-searcher.mjs +15 -5
  22. package/api/swizzle/_moduleGraph.d.mts +7 -0
  23. package/api/swizzle/_moduleGraph.mjs +191 -83
  24. package/api/swizzle/copy/copy.mjs +28 -6
  25. package/api/template/template.mjs +1 -0
  26. package/assets/discover/official-packages.json +16 -0
  27. package/assets/docs/styling-overview.doc.mjs +3 -3
  28. package/assets/docs/tree/icon-sets.doc.mjs +4 -4
  29. package/assets/docs/tree/render-pages.doc.mjs +2 -2
  30. package/assets/docs/tree/tailwind.doc.mjs +41 -1
  31. package/assets/docs/tree/type-scale.doc.mjs +19 -2
  32. package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
  33. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.doc.mjs +14 -0
  34. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.tsx +56 -0
  35. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.doc.mjs +2 -2
  36. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.tsx +66 -10
  37. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.doc.mjs +14 -0
  38. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.tsx +39 -0
  39. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.doc.mjs +14 -0
  40. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.tsx +83 -0
  41. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
  42. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +35 -0
  43. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
  44. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
  45. package/assets/templates/pages/table-filter/page.tsx +15 -23
  46. package/assets/templates/pages/table-inbox/page.tsx +3 -6
  47. package/assets/templates/themes/butter/butterTheme.ts +1 -1
  48. package/assets/templates/themes/gothic/gothicTheme.ts +16 -0
  49. package/assets/templates/themes/neutral/neutralTheme.ts +6 -0
  50. package/clients/cli/commands/experimental/icon.mjs +25 -4
  51. package/clients/cli/commands/experimental-render.doc.mjs +1 -1
  52. package/clients/cli/lib/resolve-theme.mjs +16 -7
  53. package/foundation/discovery/hook-discovery.mjs +31 -8
  54. package/foundation/discovery/icon-set-writer.d.mts +21 -5
  55. package/foundation/discovery/icon-set-writer.mjs +114 -13
  56. package/foundation/doc-compiler/import.d.mts +5 -1
  57. package/foundation/doc-compiler/import.mjs +40 -6
  58. package/foundation/fs/import-url.d.mts +20 -0
  59. package/foundation/fs/import-url.mjs +22 -0
  60. package/foundation/fs/module-loader.d.mts +24 -3
  61. package/foundation/fs/module-loader.mjs +69 -47
  62. package/foundation/render/embedded-imports.d.mts +7 -7
  63. package/foundation/render/embedded-imports.mjs +63 -54
  64. package/foundation/render/page-host.d.mts +4 -0
  65. package/foundation/render/page-host.mjs +45 -0
  66. package/foundation/render/page-worker.mjs +114 -57
  67. package/foundation/render/render-page.d.mts +11 -6
  68. package/foundation/render/render-page.mjs +138 -44
  69. package/foundation/render/ts-paths.d.mts +63 -0
  70. package/foundation/render/ts-paths.mjs +138 -0
  71. package/foundation/response/error-codes.doc.mjs +3 -3
  72. package/foundation/response/error-codes.mjs +3 -2
  73. package/foundation/text/levenshtein.d.mts +6 -1
  74. package/foundation/text/levenshtein.mjs +25 -9
  75. package/foundation/text/suggest-order.d.mts +8 -1
  76. package/foundation/text/suggest-order.mjs +72 -6
  77. package/package.json +6 -6
@@ -22,6 +22,47 @@ export function wordsOf(text: string): string[];
22
22
  * @returns {string[]}
23
23
  */
24
24
  export function termsOf(text: string): string[];
25
+ /**
26
+ * A text's docs side for `matchOf`: its word vector and that vector through
27
+ * the doc matrix. Search scores its entries' own docs with it.
28
+ * @param {string} text
29
+ * @param {BlockPickerWeights} weights
30
+ * @returns {{vector: Map<number, number>, projected: Float64Array}}
31
+ */
32
+ export function docSideOf(text: string, weights: BlockPickerWeights): {
33
+ vector: Map<number, number>;
34
+ projected: Float64Array;
35
+ };
36
+ /**
37
+ * A request's side for `matchOf`: its word vector and that vector through the
38
+ * request matrix.
39
+ * @param {string} request
40
+ * @param {BlockPickerWeights} weights
41
+ * @returns {{vector: Map<number, number>, projected: Float64Array}}
42
+ */
43
+ export function requestSideOf(request: string, weights: BlockPickerWeights): {
44
+ vector: Map<number, number>;
45
+ projected: Float64Array;
46
+ };
47
+ /**
48
+ * The picker's match of a request to a docs text: their projections' dot
49
+ * product plus the lexical weight times their plain overlap (the cosine of
50
+ * their word vectors). Build's ranking also reads the overlap alone.
51
+ * @param {{vector: Map<number, number>, projected: Float64Array}} request
52
+ * @param {{vector: Map<number, number>, projected: Float64Array}} doc
53
+ * @param {BlockPickerWeights} weights
54
+ * @returns {{match: number, overlap: number}}
55
+ */
56
+ export function matchOf(request: {
57
+ vector: Map<number, number>;
58
+ projected: Float64Array;
59
+ }, doc: {
60
+ vector: Map<number, number>;
61
+ projected: Float64Array;
62
+ }, weights: BlockPickerWeights): {
63
+ match: number;
64
+ overlap: number;
65
+ };
25
66
  /**
26
67
  * The blocks build names for a request, ranked by the picker.
27
68
  * @param {string} request
@@ -163,34 +163,10 @@ export function termsOf(text) {
163
163
  return out;
164
164
  }
165
165
 
166
- /**
167
- * The weights, decoded once per weights object.
168
- * @type {WeakMap<BlockPickerWeights, {index: Map<string, number>, idf: Float64Array, request: Int8Array, doc: Int8Array}>}
169
- */
170
- const decoded = new WeakMap();
171
-
172
- /** @param {BlockPickerWeights} weights */
173
- function decode(weights) {
174
- let d = decoded.get(weights);
175
- if (!d) {
176
- // Each byte is a signed weight stored offset by 128.
177
- const bytes = (/** @type {string} */ s) =>
178
- Int8Array.from(Buffer.from(s, 'base64'), b => b - 128);
179
- d = {
180
- index: new Map(weights.vocab.map((w, i) => [w, i])),
181
- idf: Float64Array.from(weights.idf, x => x / 1024),
182
- request: bytes(weights.request),
183
- doc: bytes(weights.doc),
184
- };
185
- decoded.set(weights, d);
186
- }
187
- return d;
188
- }
189
-
190
166
  /**
191
167
  * A text's sparse unit vector over the vocabulary: (1 + ln count) x idf.
192
168
  * @param {string[]} terms
193
- * @param {ReturnType<typeof decode>} d
169
+ * @param {{index: Map<string, number>, idf: Float64Array}} d
194
170
  * @returns {Map<number, number>}
195
171
  */
196
172
  function vectorOf(terms, d) {
@@ -214,20 +190,116 @@ function vectorOf(terms, d) {
214
190
  }
215
191
 
216
192
  /**
217
- * A sparse vector through a signed byte matrix.
218
- * @param {Map<number, number>} v
219
- * @param {Int8Array} m
193
+ * The vocabulary index and idf, read once per weights object, and each matrix
194
+ * row decoded when a side first uses it.
195
+ * @type {WeakMap<BlockPickerWeights, {index: Map<string, number>, idf: Float64Array, rows: Map<string, Int8Array>}>}
196
+ */
197
+ const vocabularies = new WeakMap();
198
+
199
+ /** @param {BlockPickerWeights} weights */
200
+ function vocabularyOf(weights) {
201
+ let v = vocabularies.get(weights);
202
+ if (!v) {
203
+ v = {
204
+ index: new Map(weights.vocab.map((w, i) => [w, i])),
205
+ idf: Float64Array.from(weights.idf, x => x / 1024),
206
+ rows: new Map(),
207
+ };
208
+ vocabularies.set(weights, v);
209
+ }
210
+ return v;
211
+ }
212
+
213
+ /**
214
+ * One row of a byte matrix, decoded from the base64 text alone: four
215
+ * characters hold three bytes, so the row's bytes sit inside the characters
216
+ * from its first byte's group to its last byte's group.
217
+ * @param {BlockPickerWeights} weights
218
+ * @param {'request' | 'doc'} matrix
219
+ * @param {number} i
220
+ * @returns {Int8Array}
221
+ */
222
+ function rowOf(weights, matrix, i) {
223
+ const {rows} = vocabularyOf(weights);
224
+ const key = `${matrix}:${i}`;
225
+ let row = rows.get(key);
226
+ if (!row) {
227
+ const {dims} = weights;
228
+ const first = Math.floor((i * dims) / 3);
229
+ const last = Math.ceil(((i + 1) * dims) / 3);
230
+ const bytes = Buffer.from(
231
+ weights[matrix].slice(first * 4, last * 4),
232
+ 'base64',
233
+ ).subarray(i * dims - first * 3, i * dims - first * 3 + dims);
234
+ // Each byte is a signed weight stored offset by 128.
235
+ row = Int8Array.from(bytes, b => b - 128);
236
+ rows.set(key, row);
237
+ }
238
+ return row;
239
+ }
240
+
241
+ /**
242
+ * A text's side for `matchOf` through one matrix: its word vector, and that
243
+ * vector times the matrix rows its words use, scaled.
244
+ * @param {string} text
245
+ * @param {BlockPickerWeights} weights
246
+ * @param {'request' | 'doc'} matrix
220
247
  * @param {number} scale
221
- * @param {number} dims
248
+ * @returns {{vector: Map<number, number>, projected: Float64Array}}
222
249
  */
223
- function project(v, m, scale, dims) {
224
- const out = new Float64Array(dims);
225
- for (const [i, x] of v) {
226
- const row = i * dims;
227
- for (let k = 0; k < dims; k++) out[k] += x * m[row + k];
250
+ function sideOf(text, weights, matrix, scale) {
251
+ const vector = vectorOf(termsOf(text), vocabularyOf(weights));
252
+ const {dims} = weights;
253
+ const projected = new Float64Array(dims);
254
+ for (const [i, x] of vector) {
255
+ const row = rowOf(weights, matrix, i);
256
+ for (let k = 0; k < dims; k++) projected[k] += x * row[k];
228
257
  }
229
- for (let k = 0; k < dims; k++) out[k] *= scale;
230
- return out;
258
+ for (let k = 0; k < dims; k++) projected[k] *= scale;
259
+ return {vector, projected};
260
+ }
261
+
262
+ /**
263
+ * A text's docs side for `matchOf`: its word vector and that vector through
264
+ * the doc matrix. Search scores its entries' own docs with it.
265
+ * @param {string} text
266
+ * @param {BlockPickerWeights} weights
267
+ * @returns {{vector: Map<number, number>, projected: Float64Array}}
268
+ */
269
+ export function docSideOf(text, weights) {
270
+ return sideOf(text, weights, 'doc', weights.docScale);
271
+ }
272
+
273
+ /**
274
+ * A request's side for `matchOf`: its word vector and that vector through the
275
+ * request matrix.
276
+ * @param {string} request
277
+ * @param {BlockPickerWeights} weights
278
+ * @returns {{vector: Map<number, number>, projected: Float64Array}}
279
+ */
280
+ export function requestSideOf(request, weights) {
281
+ return sideOf(request, weights, 'request', weights.requestScale);
282
+ }
283
+
284
+ /**
285
+ * The picker's match of a request to a docs text: their projections' dot
286
+ * product plus the lexical weight times their plain overlap (the cosine of
287
+ * their word vectors). Build's ranking also reads the overlap alone.
288
+ * @param {{vector: Map<number, number>, projected: Float64Array}} request
289
+ * @param {{vector: Map<number, number>, projected: Float64Array}} doc
290
+ * @param {BlockPickerWeights} weights
291
+ * @returns {{match: number, overlap: number}}
292
+ */
293
+ export function matchOf(request, doc, weights) {
294
+ let s = 0;
295
+ for (let k = 0; k < weights.dims; k++)
296
+ s += request.projected[k] * doc.projected[k];
297
+ let overlap = 0;
298
+ for (const [i, x] of request.vector) {
299
+ const y = doc.vector.get(i);
300
+ if (y !== undefined) overlap += x * y;
301
+ }
302
+ return {match: s + weights.lexical * overlap, overlap};
231
303
  }
232
304
 
233
305
  /**
@@ -261,7 +333,6 @@ function readCatalog(catalog, components, weights) {
261
333
  ]);
262
334
  const cached = catalogs.get(weights);
263
335
  if (cached && cached.key === key) return cached;
264
- const d = decode(weights);
265
336
  const names = components.map(c => c.name).sort((a, b) => b.length - a.length);
266
337
  const keywords = new Map(components.map(c => [c.name, c.keywords ?? []]));
267
338
  /** @type {Map<string, {component: string, vector: Map<number, number>, projected: Float64Array}>} */
@@ -276,12 +347,7 @@ function readCatalog(catalog, components, weights) {
276
347
  wordsOf(component).join(' '),
277
348
  (keywords.get(component) ?? []).join(' '),
278
349
  ].join(' ');
279
- const vector = vectorOf(termsOf(text), d);
280
- blocks.set(b.name, {
281
- component,
282
- vector,
283
- projected: project(vector, d.doc, weights.docScale, weights.dims),
284
- });
350
+ blocks.set(b.name, {component, ...docSideOf(text, weights)});
285
351
  }
286
352
  // A component is named by its own words, or by a keyword at most three
287
353
  // components share (weighted by how few share it).
@@ -378,26 +444,12 @@ export function pickBlocks(request, signals, weights) {
378
444
  /** @type {Map<string, SearchResultEntry>} */
379
445
  const entries = new Map();
380
446
  if (own.length === 0) return {names: [], entries};
381
- const d = decode(weights);
382
447
  const cat = readCatalog(signals.catalog, signals.components, weights);
383
- const q = vectorOf(termsOf(request), d);
384
- const eq = project(q, d.request, weights.requestScale, weights.dims);
385
- const dot = (/** @type {Map<number, number>} */ v) => {
386
- let s = 0;
387
- for (const [i, x] of q) {
388
- const y = v.get(i);
389
- if (y !== undefined) s += x * y;
390
- }
391
- return s;
392
- };
448
+ const side = requestSideOf(request, weights);
393
449
  /** @type {Map<string, {match: number, overlap: number}>} */
394
450
  const scored = new Map();
395
- for (const [name, b] of cat.blocks) {
396
- let s = 0;
397
- for (let k = 0; k < weights.dims; k++) s += eq[k] * b.projected[k];
398
- const overlap = dot(b.vector);
399
- scored.set(name, {match: s + weights.lexical * overlap, overlap});
400
- }
451
+ for (const [name, b] of cat.blocks)
452
+ scored.set(name, matchOf(side, b, weights));
401
453
  const get = (/** @type {string} */ b) =>
402
454
  scored.get(b) ?? {match: -Infinity, overlap: 0};
403
455
  const ranked = [...scored.keys()].sort((a, b) => get(b).match - get(a).match);
@@ -480,7 +532,7 @@ export function pickBlocks(request, signals, weights) {
480
532
  name,
481
533
  package: block.package ?? '',
482
534
  score: Math.max(1, Math.round(100 * Math.exp(x.score - max))),
483
- reason: reasonFor(request, cat.blocks.get(name), d, weights),
535
+ reason: reasonFor(request, side, cat.blocks.get(name), weights),
484
536
  description: block.description ?? '',
485
537
  displayName: block.displayName,
486
538
  kind: 'block',
@@ -493,11 +545,11 @@ export function pickBlocks(request, signals, weights) {
493
545
  /**
494
546
  * Why a block fits: the request words that tie it to its docs most.
495
547
  * @param {string} request
548
+ * @param {{vector: Map<number, number>, projected: Float64Array}} side the request's side
496
549
  * @param {{vector: Map<number, number>, projected: Float64Array} | undefined} block
497
- * @param {ReturnType<typeof decode>} d
498
550
  * @param {BlockPickerWeights} weights
499
551
  */
500
- function reasonFor(request, block, d, weights) {
552
+ function reasonFor(request, side, block, weights) {
501
553
  if (!block) return 'fits by meaning';
502
554
  const raw =
503
555
  String(request)
@@ -507,16 +559,14 @@ function reasonFor(request, block, d, weights) {
507
559
  const wordOf = new Map();
508
560
  for (const w of raw)
509
561
  if (!STOP.has(w) && !wordOf.has(stemOf(w))) wordOf.set(stemOf(w), w);
510
- const q = vectorOf(termsOf(request), d);
511
562
  /** @type {[string, number][]} */
512
563
  const parts = [];
513
- for (const [i, x] of q) {
564
+ for (const [i, x] of side.vector) {
514
565
  const term = weights.vocab[i];
515
566
  if (term.includes('_')) continue;
567
+ const row = rowOf(weights, 'request', i);
516
568
  let s = 0;
517
- const row = i * weights.dims;
518
- for (let k = 0; k < weights.dims; k++)
519
- s += d.request[row + k] * block.projected[k];
569
+ for (let k = 0; k < weights.dims; k++) s += row[k] * block.projected[k];
520
570
  s =
521
571
  x * s * weights.requestScale +
522
572
  weights.lexical * x * (block.vector.get(i) ?? 0);
@@ -271,7 +271,7 @@ useIsomorphicLayoutEffect useIsomorphicLayoutEffect hooks/useAnimatedLayout.doc.
271
271
  useLayerDismissal useLayerDismissal Layer/Layer.doc.mjs Layer/useLayerDismissal.ts
272
272
  useLightbox useLightbox Lightbox/useLightbox.doc.mjs Lightbox/useLightbox.tsx
273
273
  useLinkComponent useLinkComponent Link/useLinkComponent.doc.mjs Link/useLinkComponent.ts
274
- useLinkify useLinkify Link/Link.doc.mjs Link/useLinkify.tsx
274
+ useLinkify useLinkify Link/useLinkify.doc.mjs Link/useLinkify.tsx
275
275
  useListFormatter useListFormatter i18n/InternationalizationProvider.doc.mjs i18n/useListFormatter.ts
276
276
  useMenuDrillIn useMenuDrillIn DropdownMenu/DropdownMenu.doc.mjs DropdownMenu/useMenuDrillIn.tsx
277
277
  useMenuHover useMenuHover hooks/useAnimatedLayout.doc.mjs hooks/useMenuHover.ts
@@ -282,7 +282,7 @@ useOverlay useOverlay Overlay/useOverlay.doc.mjs Overlay/useOverlay.tsx
282
282
  useOwnScrollGesture useOwnScrollGesture DateInput/DateInput.doc.mjs DateInput/useOwnScrollGesture.ts
283
283
  usePager usePager hooks/useAnimatedLayout.doc.mjs hooks/usePager.ts
284
284
  usePointerDragScroll usePointerDragScroll DateInput/DateInput.doc.mjs DateInput/usePointerDragScroll.ts
285
- usePowerSearchConfig usePowerSearchConfig PowerSearch/PowerSearch.doc.mjs PowerSearch/usePowerSearchConfig.ts
285
+ usePowerSearchConfig usePowerSearchConfig PowerSearch/usePowerSearchConfig.doc.mjs PowerSearch/usePowerSearchConfig.ts
286
286
  usePowerSearchSource usePowerSearchSource PowerSearch/PowerSearch.doc.mjs PowerSearch/usePowerSearchSource.ts
287
287
  useRenamedProp useRenamedProp hooks/useAnimatedLayout.doc.mjs hooks/useRenamedProp.ts
288
288
  useResolvedRequired useResolvedRequired hooks/useAnimatedLayout.doc.mjs hooks/useResolvedRequired.ts
@@ -298,7 +298,7 @@ useSpeechRecognition useSpeechRecognition Chat/Chat.doc.mjs Chat/useSpeechRecogn
298
298
  useSwipeAction useSwipeAction Item/Item.doc.mjs Item/useSwipeAction.ts
299
299
  useSwitchTouch useSwitchTouch Switch/Switch.doc.mjs Switch/useSwitchTouch.ts
300
300
  useTableCellStyles useTableCellStyles Table/Table.doc.mjs Table/useTableCellStyles.ts
301
- useTableColumnSettingsState useTableColumnSettingsState Table/Table.doc.mjs Table/plugins/columnSettings/useTableColumnSettingsState.tsx
301
+ useTableColumnSettingsState useTableColumnSettingsState Table/plugins/columnSettings/useTableColumnSettingsState.doc.mjs Table/plugins/columnSettings/useTableColumnSettingsState.tsx
302
302
  useTableSortableState useTableSortableState Table/plugins/sortable/useTableSortableState.doc.mjs Table/plugins/sortable/useTableSortableState.tsx
303
303
  useToastGesture useToastGesture Toast/Toast.doc.mjs Toast/useToastGesture.ts
304
304
  useTouchTrigger useTouchTrigger Layer/Layer.doc.mjs Layer/useTouchTrigger.ts
@@ -691,6 +691,7 @@ async function redeclareIconSet(
691
691
  );
692
692
  result.data.icons = set.data.icons.length;
693
693
  result.data.updated = true;
694
+ if (where.moduleUpdate) result.data.moduleUpdate = where.moduleUpdate;
694
695
  // A look the icons' own library draws comes from that library, so its
695
696
  // command runs as printed; any other look comes from a module the
696
697
  // author names.
@@ -1388,6 +1389,7 @@ async function refreshIconSet(
1388
1389
  );
1389
1390
  result.data.icons = icons.length;
1390
1391
  result.data.refreshed = true;
1392
+ if (where.moduleUpdate) result.data.moduleUpdate = where.moduleUpdate;
1391
1393
  // Every entry an exact name gives, kept or new, so a refresh lists
1392
1394
  // them for review again.
1393
1395
  const byName = Object.fromEntries(
@@ -1542,7 +1544,7 @@ export async function addIconDefinition(name, options) {
1542
1544
  throw error;
1543
1545
  }
1544
1546
  }
1545
- return receipt(
1547
+ const result = receipt(
1546
1548
  'icon-definition',
1547
1549
  doc.name,
1548
1550
  {
@@ -1559,5 +1561,10 @@ export async function addIconDefinition(name, options) {
1559
1561
  files,
1560
1562
  dryRun,
1561
1563
  );
1564
+ // Only a new definition with an icon writes the data file.
1565
+ if (icon != null && where.moduleUpdate) {
1566
+ result.data.moduleUpdate = where.moduleUpdate;
1567
+ }
1568
+ return result;
1562
1569
  });
1563
1570
  }
@@ -147,6 +147,15 @@ export async function iconAdd(exportName, options) {
147
147
  );
148
148
  }
149
149
  }
150
+ // An export name cannot start with a digit, and an id the author gives has
151
+ // no library prefix to keep.
152
+ if (givenId != null && /^[0-9]/u.test(givenId)) {
153
+ throw new AstryxError(
154
+ `The icon id "${givenId}" starts with a digit, and an export name cannot. Pass --id with an id that starts with a letter.`,
155
+ undefined,
156
+ ERROR_CODES.ERR_INVALID_ARGUMENT,
157
+ );
158
+ }
150
159
  if (all) {
151
160
  return authoring(() => addVariantForAll({cwd, from, selector, variant}));
152
161
  }
@@ -386,6 +395,8 @@ export async function iconAdd(exportName, options) {
386
395
  map,
387
396
  icons,
388
397
  };
398
+ const dataChanged =
399
+ JSON.stringify(next) !== JSON.stringify(set.data) || where.moduleRenamed;
389
400
  /** @type {string[]} */
390
401
  const files = [];
391
402
  // Every write happens inside this try, so any failure undoes the ones
@@ -413,7 +424,7 @@ export async function iconAdd(exportName, options) {
413
424
  .join('/'),
414
425
  );
415
426
  }
416
- if (JSON.stringify(next) !== JSON.stringify(set.data)) {
427
+ if (dataChanged) {
417
428
  writeIconSetData(set, next, {
418
429
  extraDefinitions: created ? [created.name] : [],
419
430
  });
@@ -433,10 +444,12 @@ export async function iconAdd(exportName, options) {
433
444
  }
434
445
  throw error;
435
446
  }
436
- const changed =
447
+ // A renamed package's new name alone is not an added icon.
448
+ const added =
437
449
  JSON.stringify(next) !== JSON.stringify(set.data) ||
438
450
  created != null ||
439
451
  generatedChanged;
452
+ const changed = dataChanged || added;
440
453
  // A ./ file publishes only when package.json `files` lists it; the
441
454
  // author adds it (architecture:cli-surface INV16).
442
455
  const note =
@@ -457,6 +470,8 @@ export async function iconAdd(exportName, options) {
457
470
  ...(created ? {newDefinition: created.name} : {}),
458
471
  files,
459
472
  changed,
473
+ added,
474
+ ...(where.moduleUpdate ? {moduleUpdate: where.moduleUpdate} : {}),
460
475
  ...(note ? {note} : {}),
461
476
  },
462
477
  };
@@ -638,8 +653,27 @@ async function addVariantForAll({cwd, from, selector, variant}) {
638
653
  );
639
654
  // Only icons from the same package as the variant module can have a
640
655
  // variant there: an icon from your own `./` file, or from another library,
641
- // is left as it is and never listed as missing.
656
+ // is left as it is and never listed as missing. Some libraries ship each
657
+ // look as its own package (Font Awesome's solid and regular packages): an
658
+ // icon from another package meets its variant only under the same export
659
+ // name and of the same kind, a component or data for the adapter. Once
660
+ // one icon of a package meets its twin, that package is a look package of
661
+ // the variant module, so its other icons are listed as missing.
642
662
  const variantPackage = iconModulePackage(source.module);
663
+ /** @param {{module: string, source: string, adapt: boolean}} origin */
664
+ const otherPackageTwin = origin => {
665
+ if (origin.module.startsWith('.')) return false;
666
+ const twin = byLibraryExport.get(origin.source);
667
+ return twin != null && twin.adapt === origin.adapt;
668
+ };
669
+ const lookPackages = new Set(
670
+ set.data.icons.flatMap(icon => {
671
+ const origin = origins.get(icon.export);
672
+ return origin && otherPackageTwin(origin)
673
+ ? [iconModulePackage(origin.module)]
674
+ : [];
675
+ }),
676
+ );
643
677
  const generated = generatedModulePath(set, packageDir);
644
678
  /** @type {import('../../../foundation/discovery/icon-set-writer.mjs').GeneratedIcon[]} */
645
679
  const toWrite = [];
@@ -653,7 +687,11 @@ async function addVariantForAll({cwd, from, selector, variant}) {
653
687
  const own = [];
654
688
  const icons = set.data.icons.map(icon => {
655
689
  const origin = origins.get(icon.export);
656
- if (origin && iconModulePackage(origin.module) !== variantPackage) {
690
+ if (
691
+ origin &&
692
+ iconModulePackage(origin.module) !== variantPackage &&
693
+ !lookPackages.has(iconModulePackage(origin.module))
694
+ ) {
657
695
  return icon;
658
696
  }
659
697
  const librarySource =
@@ -742,7 +780,8 @@ async function addVariantForAll({cwd, from, selector, variant}) {
742
780
  const files = [];
743
781
  const written = writeGeneratedIcons(packageDir, generated, next, toWrite);
744
782
  files.push(...written.files);
745
- const dataChanged = JSON.stringify(next) !== JSON.stringify(set.data);
783
+ const dataChanged =
784
+ JSON.stringify(next) !== JSON.stringify(set.data) || where.moduleRenamed;
746
785
  if (dataChanged) {
747
786
  try {
748
787
  writeIconSetData(set, next);
@@ -766,6 +805,7 @@ async function addVariantForAll({cwd, from, selector, variant}) {
766
805
  missing,
767
806
  files,
768
807
  changed: dataChanged || written.changed,
808
+ ...(where.moduleUpdate ? {moduleUpdate: where.moduleUpdate} : {}),
769
809
  },
770
810
  };
771
811
  }
@@ -223,6 +223,10 @@ export type IconMapManyResponse = {
223
223
  changed: boolean;
224
224
  }>;
225
225
  changed: boolean;
226
+ moduleUpdate?: {
227
+ previous: string;
228
+ module: string;
229
+ } | undefined;
226
230
  files?: string[] | undefined;
227
231
  };
228
232
  };
@@ -239,6 +243,10 @@ export type IconMapResponse = {
239
243
  icon: string | null;
240
244
  previous: string | null;
241
245
  changed: boolean;
246
+ moduleUpdate?: {
247
+ previous: string;
248
+ module: string;
249
+ } | undefined;
242
250
  files?: string[] | undefined;
243
251
  };
244
252
  };
@@ -303,6 +311,10 @@ export type IconAddVariantsResponse = {
303
311
  missing: string[];
304
312
  files: string[];
305
313
  changed: boolean;
314
+ moduleUpdate?: {
315
+ previous: string;
316
+ module: string;
317
+ } | undefined;
306
318
  };
307
319
  };
308
320
  /**
@@ -323,6 +335,11 @@ export type IconAddResponse = {
323
335
  newDefinition?: string | undefined;
324
336
  files: string[];
325
337
  changed: boolean;
338
+ added: boolean;
339
+ moduleUpdate?: {
340
+ previous: string;
341
+ module: string;
342
+ } | undefined;
326
343
  note?: string | undefined;
327
344
  };
328
345
  };
@@ -557,6 +574,13 @@ export type IconNewData = {
557
574
  * - What the author does next, including each change the package needs that the CLI never makes itself (its entry, peers, scripts), as text to copy.
558
575
  */
559
576
  notes?: string[] | undefined;
577
+ /**
578
+ * - On a refresh, an update, or a new definition with `icon`: the data file's `module` before and after this command, when the package was renamed and the command writes its new name.
579
+ */
580
+ moduleUpdate?: {
581
+ previous: string;
582
+ module: string;
583
+ } | undefined;
560
584
  };
561
585
  /**
562
586
  * `astryx --json experimental icon new-set <name>`.
@@ -156,6 +156,7 @@
156
156
  * @property {string} data.file - The data file, relative to the package.
157
157
  * @property {Array<{definition: string, icon: string, previous: string | null, changed: boolean}>} data.entries - Each pair, in order, and what it replaced.
158
158
  * @property {boolean} data.changed - Whether the file was written.
159
+ * @property {{previous: string, module: string}} [data.moduleUpdate] - The data file's `module` before and after this command, when the package was renamed and the command wrote its new name.
159
160
  * @property {string[]} [data.files] - The generated module and its types, when its theme map was rewritten.
160
161
  */
161
162
 
@@ -172,6 +173,7 @@
172
173
  * @property {string | null} data.icon - The id of the icon now mapped (the icon may be named by any name `map` accepts); null after `remove`.
173
174
  * @property {string | null} data.previous - The icon id it replaced.
174
175
  * @property {boolean} data.changed - Whether the file was written.
176
+ * @property {{previous: string, module: string}} [data.moduleUpdate] - The data file's `module` before and after this command, when the package was renamed and the command wrote its new name.
175
177
  * @property {string[]} [data.files] - The generated module and its types, when its theme map was rewritten.
176
178
  */
177
179
 
@@ -207,6 +209,7 @@
207
209
  * @property {string[]} data.missing - Ids of icons `from` has no export for; they keep no such variant.
208
210
  * @property {string[]} data.files - Files written, relative to the package.
209
211
  * @property {boolean} data.changed
212
+ * @property {{previous: string, module: string}} [data.moduleUpdate] - The data file's `module` before and after this command, when the package was renamed and the command wrote its new name.
210
213
  */
211
214
 
212
215
  /**
@@ -223,6 +226,8 @@
223
226
  * @property {string} [data.newDefinition] - The set's own definition the command wrote.
224
227
  * @property {string[]} data.files - Files written, relative to the package.
225
228
  * @property {boolean} data.changed - Whether anything was written.
229
+ * @property {boolean} data.added - Whether the icon, its variant, its `map` entry, or a new definition was added. False when the set already had them, even when `changed` is true because the command wrote a renamed package's new name.
230
+ * @property {{previous: string, module: string}} [data.moduleUpdate] - The data file's `module` before and after this command, when the package was renamed and the command wrote its new name.
226
231
  * @property {string} [data.note] - What the package still needs, as text to copy: the entry line that exports the icon from the set's module, and the package.json `files` that publishes a `./` source.
227
232
  */
228
233
 
@@ -332,6 +337,7 @@
332
337
  * @property {Array<{definition: string, icon: string}>} [mapped] - With `from`: the `map` entries an icon with the definition's exact name gives (on a refresh, every such entry, kept or new); check each.
333
338
  * @property {Array<{definition: string, icon: string, word: string}>} [suggestions] - With `from`: for each unmapped Astryx definition, up to three icons it might map to, best first: the definition's name in another spelling or as another name of an icon, ids that start with it, an id or another name that is one of its words, then two-word ids that are a one-word word sized, shaped, or numbered (`grid-2x2`); never a negated form, the opposite direction, or an icon already mapped. `word` is the name or word that matched. Never written.
334
339
  * @property {string[]} [notes] - What the author does next, including each change the package needs that the CLI never makes itself (its entry, peers, scripts), as text to copy.
340
+ * @property {{previous: string, module: string}} [moduleUpdate] - On a refresh, an update, or a new definition with `icon`: the data file's `module` before and after this command, when the package was renamed and the command writes its new name.
335
341
  */
336
342
 
337
343
  /**
@@ -119,7 +119,7 @@ export const doc = {
119
119
  },
120
120
  {
121
121
  code: 'ERR_INVALID_ARGUMENT',
122
- when: "the export is data rather than a component, a new definition is named like an Astryx one, or the variant is not one the set declares, is the look each icon's own export already draws, or is no look word of a library whose export names encode looks",
122
+ when: "the export is data rather than a component, a new definition is named like an Astryx one, or the variant is not one the set declares, is the look each icon's own export already draws, or is no look word of a library whose export names encode looks, or `id` starts with a digit",
123
123
  },
124
124
  {code: 'ERR_FILE_EXISTS', when: 'another icon already has the id'},
125
125
  {
@@ -77,7 +77,8 @@ export async function iconMap(definition, icon, options = {}) {
77
77
  ERROR_CODES.ERR_MISSING_ARGUMENT,
78
78
  );
79
79
  }
80
- const {packageDir, set} = await findLocalIconSet(cwd, selector);
80
+ const {packageDir, set, moduleRenamed, moduleUpdate} =
81
+ await findLocalIconSet(cwd, selector);
81
82
  // The docs' kebab id (`eye-slash`) names the same definition, and a
82
83
  // library's other name for an icon (`trash-2`) names the same icon.
83
84
  definition = definitionNameOf(definition, mappableDefinitions(set));
@@ -112,7 +113,8 @@ export async function iconMap(definition, icon, options = {}) {
112
113
  }
113
114
  map[definition] = /** @type {string} */ (icon);
114
115
  }
115
- const changed = remove ? previous != null : previous !== icon;
116
+ const changed =
117
+ (remove ? previous != null : previous !== icon) || moduleRenamed;
116
118
  /** @type {string[]} */
117
119
  const files = [];
118
120
  if (changed) {
@@ -139,6 +141,7 @@ export async function iconMap(definition, icon, options = {}) {
139
141
  icon: remove ? null : /** @type {string} */ (icon),
140
142
  previous,
141
143
  changed,
144
+ ...(moduleUpdate ? {moduleUpdate} : {}),
142
145
  ...(files.length > 0 ? {files} : {}),
143
146
  },
144
147
  };
@@ -161,7 +164,10 @@ async function mapMany({cwd, selector, pairs}) {
161
164
  ERROR_CODES.ERR_MISSING_ARGUMENT,
162
165
  );
163
166
  }
164
- const {packageDir, set} = await findLocalIconSet(cwd, selector);
167
+ const {packageDir, set, moduleRenamed, moduleUpdate} = await findLocalIconSet(
168
+ cwd,
169
+ selector,
170
+ );
165
171
  const known = mappableDefinitions(set);
166
172
  const ids = set.data.icons.map(each => each.id);
167
173
  const map = {...(set.data.map ?? {})};
@@ -204,7 +210,7 @@ async function mapMany({cwd, selector, pairs}) {
204
210
  changed: previous !== id,
205
211
  });
206
212
  }
207
- const changed = entries.some(each => each.changed);
213
+ const changed = entries.some(each => each.changed) || moduleRenamed;
208
214
  /** @type {string[]} */
209
215
  const files = [];
210
216
  if (changed) {
@@ -228,6 +234,7 @@ async function mapMany({cwd, selector, pairs}) {
228
234
  file: path.relative(packageDir, set.dataPath).split(path.sep).join('/'),
229
235
  entries,
230
236
  changed,
237
+ ...(moduleUpdate ? {moduleUpdate} : {}),
231
238
  ...(files.length > 0 ? {files} : {}),
232
239
  },
233
240
  };
@@ -92,7 +92,7 @@ export function loadCurrentManifest(manifestFile: string, manifestExists: boolea
92
92
  * @param {string} manifestPath
93
93
  * @param {string[]} files
94
94
  * @param {boolean} dryRun
95
- * @returns {{type: 'integration.add', data: Omit<import('./integration-authoring.type.mjs').IntegrationAddData, 'kind'> & {kind: K, icons?: number, refreshed?: boolean, updated?: boolean, renamed?: Array<{from: string, to: string}>, moved?: Array<{id: string, from: string, to: string}>, licenseNoticeMissing?: {library: string, path: string}, mapped?: Array<{definition: string, icon: string}>, suggestions?: Array<{definition: string, icon: string, word: string}>, notes?: string[]}}}
95
+ * @returns {{type: 'integration.add', data: Omit<import('./integration-authoring.type.mjs').IntegrationAddData, 'kind'> & {kind: K, icons?: number, refreshed?: boolean, updated?: boolean, renamed?: Array<{from: string, to: string}>, moved?: Array<{id: string, from: string, to: string}>, licenseNoticeMissing?: {library: string, path: string}, mapped?: Array<{definition: string, icon: string}>, suggestions?: Array<{definition: string, icon: string, word: string}>, notes?: string[], moduleUpdate?: {previous: string, module: string}}}}
96
96
  */
97
97
  export function receipt<K extends import("./integration-authoring.type.mjs").IntegrationAddData["kind"] | import("../icon/icon.type.mjs").IconNewData["kind"]>(kind: K, name: string, root: {
98
98
  path: string;
@@ -127,6 +127,10 @@ export function receipt<K extends import("./integration-authoring.type.mjs").Int
127
127
  word: string;
128
128
  }>;
129
129
  notes?: string[];
130
+ moduleUpdate?: {
131
+ previous: string;
132
+ module: string;
133
+ };
130
134
  };
131
135
  };
132
136
  /** @param {string} slug */