@astryxdesign/cli 0.7.12-canary.da3dbdc → 0.7.12-canary.dfd75a6

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.
@@ -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 */
@@ -486,7 +486,7 @@ export async function loadCurrentManifest(manifestFile, manifestExists) {
486
486
  * @param {string} manifestPath
487
487
  * @param {string[]} files
488
488
  * @param {boolean} dryRun
489
- * @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[]}}}
489
+ * @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}}}}
490
490
  */
491
491
  export function receipt(kind, name, root, manifestPath, files, dryRun) {
492
492
  return {
@@ -96,7 +96,7 @@ export const doc = {
96
96
  {code: 'ERR_RENDER_BROWSER_API', when: 'the page reads a browser-only API, such as window or document, while rendering'},
97
97
  {code: 'ERR_RENDER_ASSET_TOO_LARGE', when: 'a local file the page uses is over 8 MB'},
98
98
  {code: 'ERR_RENDER_TIMEOUT', when: 'the page does not finish rendering within 60 seconds'},
99
- {code: 'ERR_RENDER_FAILED', when: 'the page throws while rendering for any other reason'},
99
+ {code: 'ERR_RENDER_FAILED', when: 'the page throws while rendering for any other reason, or Node itself crashes (a crash signal, or a crash status on Windows) or is stopped by a signal while it renders the page (the message names the Node version and path)'},
100
100
  {
101
101
  code: 'ERR_RENDER_NOT_ASTRYX',
102
102
  when: 'reversing a file a render did not write, with no embedded source and no Astryx markup; the suggestion is `astryx build "<what the page is>"`',
@@ -31,7 +31,7 @@ export type RenderWarning = {
31
31
  /**
32
32
  * - What kind of problem.
33
33
  */
34
- code: "dropped-children" | "remote-asset" | "missing-asset" | "page-output" | "page-script" | "no-astryx-styles" | "chart-size" | "not-embedded";
34
+ code: "dropped-children" | "remote-asset" | "missing-asset" | "page-output" | "page-script" | "no-astryx-styles" | "chart-size" | "not-embedded" | "node-crash";
35
35
  /**
36
36
  * - What happened and what to do, in one sentence.
37
37
  */
@@ -22,7 +22,7 @@
22
22
  * author meant.
23
23
  * @experimental May change or be removed in a patch release.
24
24
  * @typedef {object} RenderWarning
25
- * @property {'dropped-children' | 'remote-asset' | 'missing-asset' | 'page-output' | 'page-script' | 'no-astryx-styles' | 'chart-size' | 'not-embedded'} code - What kind of problem.
25
+ * @property {'dropped-children' | 'remote-asset' | 'missing-asset' | 'page-output' | 'page-script' | 'no-astryx-styles' | 'chart-size' | 'not-embedded' | 'node-crash'} code - What kind of problem.
26
26
  * @property {string} message - What happened and what to do, in one sentence.
27
27
  */
28
28
 
@@ -61,7 +61,7 @@ export const docs = {
61
61
  },
62
62
  {
63
63
  type: 'prose',
64
- text: "`outline.doc.mjs` is the descriptor: name, display name and description (`--display-name`, `--description`), and `license`, the SPDX license of the artwork (it starts as your package's own). `outline.icons.json` starts with no icons and an empty `map`; its `module` is your package name. `outline.generated.js` is the set's generated module, with its declaration file `outline.generated.d.ts`: the CLI writes both, and every icon and the theme map are exports of it. Every field is in {@link schema:icon-set-doc} and {@link schema:icon-set-data}.",
64
+ text: "`outline.doc.mjs` is the descriptor: name, display name and description (`--display-name`, `--description`), and `license`, the SPDX license of the artwork (it starts as your package's own). `outline.icons.json` starts with no icons and an empty `map`; its `module` is your package name. After you rename the package, the next command that writes the data file (`new-set` on the existing set, with or without `--from`, `add`, `map`, or `new-definition --icon`) writes the new name and keeps any subpath (`@acme/old-icons/outline` becomes `@acme/new-icons/outline`), and its receipt says so. `outline.generated.js` is the set's generated module, with its declaration file `outline.generated.d.ts`: the CLI writes both, and every icon and the theme map are exports of it. Every field is in {@link schema:icon-set-doc} and {@link schema:icon-set-data}.",
65
65
  },
66
66
  {
67
67
  type: 'prose',
@@ -121,7 +121,7 @@ export const docs = {
121
121
  },
122
122
  {
123
123
  type: 'prose',
124
- text: "Every component the library exports becomes an icon (an alias of one component is recorded once), named `<Name>Icon`, with an id from its name (`Squares2X2Icon` is `squares-2x2`: a dimension stays one word). `map` is filled only where a definition's name is an icon's id (`delete` → icon `delete`), and the receipt lists those entries under Mapped by name: a library can draw another meaning under the same name (one library's `delete` is a backspace key), so check each. An icon whose id is only one of a definition's words (`trash` for `delete`) may mean something else in this library, so the receipt lists it under Suggestions, with the `experimental icon map <definition> <icon>` command for each, and writes nothing for it: check the meaning, then run the ones that fit. Run it again after a library release to refresh the icons that library supplies; `map` entries whose icon still exists are kept, and the receipt names any icon the library dropped, any icon `--exclude` left out, and the `map` entries each took with it. Icons you added from your own files or another library are left as they are. Icons you added one file at a time from the same package (`--from <package>/dist/XIcon`) move to the root import when you refresh from the package root and the root exports the same icon; each keeps its id, export, `map` entries, variants, and words on that refresh and every later one, and the receipt lists it under `moved`. A library icon whose id or name a moved icon already has is left out. When another icon of the set already reads the same root export, the icon stays on its own file, and the receipt says why. An icon you already have is known by the library export it re-exports, so it keeps its export name exactly, its `map` entries, variants, and words, even when a newer CLI spells its id differently or your `--exclude` lets a library prefix come off new ids; the receipt lists each changed id under `renamed`. An id you chose that is not a spelling of the library name (`dismiss` for `XIcon`) is kept. A refresh with `--dry-run` writes nothing and prints the Mapped by name and Suggestions lists again; `--display-name` and `--description` on a refresh rewrite the descriptor. Variants added with `add --variant … --all` are kept too; run that `add` again to pick up icons the release added. `experimental icon --list` shows how many definitions the set maps.",
124
+ text: "Every component the library exports becomes an icon (an alias of one component is recorded once), named `<Name>Icon`, with an id from its name (`Squares2X2Icon` is `squares-2x2`: a dimension stays one word). An export name cannot start with a digit, so an icon whose id would start with one once the library prefix comes off keeps the prefix (`fa0` is id `fa-0`, exported as `Fa0Icon`), and an id you give with `--id` must start with a letter. `map` is filled only where a definition's name is an icon's id (`delete` → icon `delete`), and the receipt lists those entries under Mapped by name: a library can draw another meaning under the same name (one library's `delete` is a backspace key), so check each. An icon whose id is only one of a definition's words (`trash` for `delete`) may mean something else in this library, so the receipt lists it under Suggestions, with the `experimental icon map <definition> <icon>` command for each, and writes nothing for it: check the meaning, then run the ones that fit. Run it again after a library release to refresh the icons that library supplies; `map` entries whose icon still exists are kept, and the receipt names any icon the library dropped, any icon `--exclude` left out, and the `map` entries each took with it. Icons you added from your own files or another library are left as they are. Icons you added one file at a time from the same package (`--from <package>/dist/XIcon`) move to the root import when you refresh from the package root and the root exports the same icon; each keeps its id, export, `map` entries, variants, and words on that refresh and every later one, and the receipt lists it under `moved`. A library icon whose id or name a moved icon already has is left out. When another icon of the set already reads the same root export, the icon stays on its own file, and the receipt says why. An icon you already have is known by the library export it re-exports, so it keeps its export name exactly, its `map` entries, variants, and words, even when a newer CLI spells its id differently or your `--exclude` lets a library prefix come off new ids; the receipt lists each changed id under `renamed`. An id you chose that is not a spelling of the library name (`dismiss` for `XIcon`) is kept. A refresh with `--dry-run` writes nothing and prints the Mapped by name and Suggestions lists again; `--display-name` and `--description` on a refresh rewrite the descriptor. Variants added with `add --variant … --all` are kept too; run that `add` again to pick up icons the release added. `experimental icon --list` shows how many definitions the set maps.",
125
125
  },
126
126
  {
127
127
  type: 'prose',
@@ -139,11 +139,11 @@ astryx experimental icon add --variant solid --from @heroicons/react/24/solid --
139
139
  },
140
140
  {
141
141
  type: 'prose',
142
- text: "A library that redraws its artwork for small sizes (Heroicons' 20px mini and 16px micro) ships each of those as its own set in its own package (their export names match the 24px ones), not as variants: a variant's size comes from CSS. `--variants` lists the variants; `--default-variant` names the one each icon's own export draws (when omitted, the look a library that names its looks draws; otherwise the set's current default, else, on a set without icons, the first variant; a set with icons changes it only with `--from`, and only in a library that names its looks). `--all` adds the variant for every icon whose id an export of `--from` has (`TrashIcon` in the solid module becomes `TrashSolidIcon`, the solid variant of `trash`), and the receipt lists the icons the module has no export for. Running it again changes nothing.",
142
+ text: "A library that redraws its artwork for small sizes (Heroicons' 20px mini and 16px micro) ships each of those as its own set in its own package (their export names match the 24px ones), not as variants: a variant's size comes from CSS. `--variants` lists the variants; `--default-variant` names the one each icon's own export draws (when omitted, the look a library that names its looks draws; otherwise the set's current default, else, on a set without icons, the first variant; a set with icons changes it only with `--from`, and only in a library that names its looks). `--all` adds the variant for every icon whose id an export of `--from` has (`TrashIcon` in the solid module becomes `TrashSolidIcon`, the solid variant of `trash`), and the receipt lists the icons the module has no export for. A library may ship each look as its own package (Font Awesome's solid and regular packages): `--from` may then name the other package, and an icon gains the variant from the export of the same name there; the receipt lists the icons that package has no export for. Running it again changes nothing.",
143
143
  },
144
144
  {
145
145
  type: 'prose',
146
- text: 'A library whose icons are data (objects or SVG strings) has no components to find: create the set, declare an `adapter` in its data file (see library shapes), and run the command again; every data export then becomes an icon through the adapter. When every icon export shares a leading word, such as `feather` in `featherEye` and `featherSun`, ids and export names leave it out: `featherEye` becomes id `eye`, exported as `EyeIcon`. The prefix is read over icon-like exports only: SCREAMING_CASE constants (`README`, `VERSION`), number and boolean exports, and, next to components or icon objects, plain strings are never icons. Components named `…Provider` or `…Context` are never icons either. Leave out any other export that is not an icon with `--exclude meta,version`: the data file keeps the list in `exclude`, so a refresh leaves them out too. When a library exports one icon under several names (`Trash2` and `Trash`), the icon is recorded once and its other names become words, so `experimental icon trash-2` finds it and `experimental icon map delete trash-2` maps it. `experimental icon add` reads the prefix the same way, and reuses the icon an export already has, so its commands need no `--id`.',
146
+ text: 'A library whose icons are data (objects or SVG strings) has no components to find: create the set, declare an `adapter` in its data file (see library shapes), and run the command again; every data export then becomes an icon through the adapter. When every icon export shares a leading word, such as `feather` in `featherEye` and `featherSun`, ids and export names leave it out: `featherEye` becomes id `eye`, exported as `EyeIcon`. The prefix is read over icon-like exports only: SCREAMING_CASE constants (`README`, `VERSION`), number and boolean exports, and, next to components or icon objects, plain strings are never icons. Components named `…Provider` or `…Context` are never icons either, and neither is a pack: an object whose keys are all other exports of the module, such as `fas` next to `faXmark`. Leave out any other export that is not an icon with `--exclude meta,version`: the data file keeps the list in `exclude`, so a refresh leaves them out too. When a library exports one icon under several names (`Trash2` and `Trash`), the icon is recorded once and its other names become words, so `experimental icon trash-2` finds it and `experimental icon map delete trash-2` maps it. `experimental icon add` reads the prefix the same way, and reuses the icon an export already has, so its commands need no `--id`.',
147
147
  },
148
148
  ],
149
149
  },
@@ -67,7 +67,7 @@ export const docs = {
67
67
  content: [
68
68
  {
69
69
  type: 'prose',
70
- text: 'Each warning names something that may look different from what you meant, and what to change. The most common is a component that ignores its children: `<ListItem>Save</ListItem>` renders an empty row because ListItem takes `label="Save"`. Others flag remote images (a sandboxed viewer cannot load them; save the image next to the page and import it), a `<script>`, `<link>` or `<iframe>` the page itself renders (the file promises to run and load nothing, so remove them), missing images, and output the page printed while rendering.',
70
+ text: 'Each warning names something that may look different from what you meant, and what to change. The most common is a component that ignores its children: `<ListItem>Save</ListItem>` renders an empty row because ListItem takes `label="Save"`. Others flag remote images (a sandboxed viewer cannot load them; save the image next to the page and import it), a `<script>`, `<link>` or `<iframe>` the page itself renders (the file promises to run and load nothing, so remove them), missing images, and output the page printed while rendering. A `node-crash` warning names a Node build that crashes in `Intl.Segmenter` (its version and path). The page rendered without it, so the output may differ slightly: initials and character counts (such as Avatar initials) can split a joined emoji, a flag, or a letter written with a separate accent mark. Names in ordinary composed letters (`José`, `Zoë`, `李小龙`) come out the same. Render with an official Node.js build to remove the difference.',
71
71
  },
72
72
  ],
73
73
  },
@@ -395,6 +395,22 @@ export const gothicTheme = defineTheme({
395
395
  },
396
396
  },
397
397
 
398
+ // Gothic's accent text and icons are one light color. On a media surface
399
+ // they follow that surface's accent, so a dark surface inside a light one
400
+ // gets its light accent back.
401
+ onDark: {
402
+ tokens: {
403
+ '--color-text-accent': 'var(--color-accent)',
404
+ '--color-icon-accent': 'var(--color-accent)',
405
+ },
406
+ },
407
+ onLight: {
408
+ tokens: {
409
+ '--color-text-accent': 'var(--color-accent)',
410
+ '--color-icon-accent': 'var(--color-accent)',
411
+ },
412
+ },
413
+
398
414
  icons: gothicIconRegistry,
399
415
  });
400
416
 
@@ -59,6 +59,20 @@ const defaultVariantText = value =>
59
59
  /** @param {unknown} value */
60
60
  const variantsText = value => (Array.isArray(value) ? value.join('/') : '');
61
61
 
62
+ /**
63
+ * The receipt line for a data file whose `module` the command rewrote after
64
+ * a package rename.
65
+ * @param {{moduleUpdate?: {previous: string, module: string}, dryRun?: boolean}} data
66
+ */
67
+ const moduleUpdateLines = data =>
68
+ data.moduleUpdate
69
+ ? [
70
+ text(
71
+ `${data.dryRun ? '[plan] Would update' : '[ok] Updated'} module to "${data.moduleUpdate.module}" (was "${data.moduleUpdate.previous}"), from the package name in package.json.`,
72
+ ),
73
+ ]
74
+ : [];
75
+
62
76
  /**
63
77
  * Register `icon` and its subcommands under the `experimental` group.
64
78
  * @param {import('commander').Command} group the `experimental` command
@@ -376,20 +390,24 @@ export function registerExperimentalIcon(group, program) {
376
390
  (
377
391
  /** @type {{definition: string, icon: string, previous: string | null}} */ each,
378
392
  ) =>
379
- `${each.definition} = "${each.icon}"${each.previous && each.previous !== each.icon ? ` (was "${each.previous}")` : ''}`,
393
+ `${each.definition} = "${each.icon}"${each.previous === each.icon ? ' (unchanged)' : each.previous ? ` (was "${each.previous}")` : ' (was not mapped)'}`,
380
394
  ),
381
395
  ),
396
+ ...moduleUpdateLines(data),
382
397
  )
383
398
  : emit(
399
+ // The entry line reads `previous` and `icon`: a write can
400
+ // come from a package rename alone.
384
401
  text(
385
402
  data.icon == null
386
- ? data.changed
403
+ ? data.previous != null
387
404
  ? `[ok] Removed map.${data.definition} (was "${data.previous}") from ${data.file}.`
388
405
  : `[ok] ${data.file} has no map.${data.definition}.`
389
- : data.changed
406
+ : data.previous !== data.icon
390
407
  ? `[ok] map.${data.definition} = "${data.icon}" in ${data.file}${data.previous ? ` (was "${data.previous}")` : ''}.`
391
408
  : `[ok] map.${data.definition} is already "${data.icon}".`,
392
409
  ),
410
+ ...moduleUpdateLines(data),
393
411
  ),
394
412
  ),
395
413
  });
@@ -430,6 +448,7 @@ export function registerExperimentalIcon(group, program) {
430
448
  text(
431
449
  `[ok] ${data.variant}: ${data.added} icons gained it${data.already > 0 ? `, ${data.already} already had it` : ''}, read from ${data.from}.`,
432
450
  ),
451
+ ...moduleUpdateLines(data),
433
452
  ...(data.files.length > 0 ? [list(data.files)] : []),
434
453
  ...(data.missing.length > 0
435
454
  ? [
@@ -488,10 +507,11 @@ export function registerExperimentalIcon(group, program) {
488
507
  const {data} = result;
489
508
  emit(
490
509
  text(
491
- data.changed
510
+ data.added
492
511
  ? `[ok] Added ${data.icon.export} as "${data.icon.id}"${data.variant ? ` (variant ${data.variant})` : ''}${data.definition ? `, map.${data.definition} = "${data.icon.id}"` : ''}.`
493
512
  : `[ok] ${data.icon.export} is already "${data.icon.id}"${data.definition ? ` and map.${data.definition} names it` : ''}.`,
494
513
  ),
514
+ ...moduleUpdateLines(data),
495
515
  ...(data.files.length > 0 ? [list(data.files)] : []),
496
516
  ...(data.note ? [text(`[next] ${data.note}`)] : []),
497
517
  );
@@ -543,6 +563,7 @@ export function registerExperimentalIcon(group, program) {
543
563
  : `The experimental.iconSets root ${root.path} in ${manifest} is unchanged.`
544
564
  : `${dryRun ? 'Would update' : 'Updated'} ${manifest}.`,
545
565
  ),
566
+ ...moduleUpdateLines(data),
546
567
  ...(files.length > 0 ? [list(files)] : []),
547
568
  // What to do next comes before the long lists, so it never scrolls
548
569
  // away.
@@ -53,7 +53,7 @@ export const doc = {
53
53
  {code: 0, when: 'the file was written (warnings included)'},
54
54
  {
55
55
  code: 1,
56
- when: 'no file, a wrong extension, a missing file, a page that does not compile, has no default export, imports a package that is not installed, reads a browser API while rendering, throws, or takes over 60 seconds; a file to write outside the current folder; with --reverse, an existing output without --overwrite, a damaged embedded source, or HTML that is not an Astryx page',
56
+ when: 'no file, a wrong extension, a missing file, a page that does not compile, has no default export, imports a package that is not installed, reads a browser API while rendering, throws, crashes Node, or takes over 60 seconds; a file to write outside the current folder; with --reverse, an existing output without --overwrite, a damaged embedded source, or HTML that is not an Astryx page',
57
57
  },
58
58
  ],
59
59
  related: ['template', 'build'],
@@ -11,7 +11,10 @@
11
11
  export function iconIdOf(name: string): string;
12
12
  /**
13
13
  * The name an icon set exports an icon as: PascalCase ending in `Icon`
14
- * (`Trash2` → `Trash2Icon`; `TrashIcon` stays).
14
+ * (`Trash2` → `Trash2Icon`; `TrashIcon` stays). A JavaScript name cannot
15
+ * start with a digit, so a name that starts with one is refused.
16
+ * {@link iconNaming} keeps a library prefix to prevent this (`fa0` →
17
+ * `Fa0Icon`).
15
18
  * @param {string} name
16
19
  */
17
20
  export function iconExportName(name: string): string;
@@ -27,7 +30,11 @@ export function variantExportName(iconName: string, variant: string): string;
27
30
  * `selector` names, else the only set.
28
31
  * @param {string} cwd
29
32
  * @param {string} [selector] a set name
30
- * @returns {Promise<{packageDir: string, pkg: Record<string, any>, root: string, set: DiscoveredIconSet, sets: DiscoveredIconSet[]}>}
33
+ * The set's `module` names the package as package.json names it now;
34
+ * `moduleRenamed` says the data file still names an old name, so a command
35
+ * writes the data file even when nothing else changes; `moduleUpdate` gives
36
+ * the old and the new name for the receipt, else null.
37
+ * @returns {Promise<{packageDir: string, pkg: Record<string, any>, root: string, set: DiscoveredIconSet, sets: DiscoveredIconSet[], moduleRenamed: boolean, moduleUpdate: {previous: string, module: string} | null}>}
31
38
  */
32
39
  export function findLocalIconSet(cwd: string, selector?: string): Promise<{
33
40
  packageDir: string;
@@ -35,6 +42,11 @@ export function findLocalIconSet(cwd: string, selector?: string): Promise<{
35
42
  root: string;
36
43
  set: DiscoveredIconSet;
37
44
  sets: DiscoveredIconSet[];
45
+ moduleRenamed: boolean;
46
+ moduleUpdate: {
47
+ previous: string;
48
+ module: string;
49
+ } | null;
38
50
  }>;
39
51
  /**
40
52
  * A data file's text, in one fixed shape: fields in schema order, `map` keys
@@ -147,7 +159,9 @@ export function nonIconExports(file: string, options?: {
147
159
  export function libraryPrefix(names: string[]): string[];
148
160
  /**
149
161
  * An export's id and the name the set exports it as, with the library prefix
150
- * left out: `featherEye` with prefix `feather` is id `eye`, `EyeIcon`.
162
+ * left out: `featherEye` with prefix `feather` is id `eye`, `EyeIcon`. A
163
+ * name that would start with a digit keeps the prefix (`fa500px` is
164
+ * `fa-500px`, `Fa500pxIcon`).
151
165
  * @param {string} name
152
166
  * @param {string[]} prefix
153
167
  * @returns {{id: string, name: string}}
@@ -371,7 +385,7 @@ export function writeDefinitionFile(set: DiscoveredIconSet, doc: {
371
385
  /**
372
386
  * Whether a generated declaration file declares what its generated module
373
387
  * gives, statement by statement however a formatter left either file
374
- * ({@link generatedStatements}); the header comment is ignored.
388
+ * ({@link statementTrees}); the header comment is ignored.
375
389
  * @param {string} actual the declaration file as it is
376
390
  * @param {string} expected {@link generatedTypesText} for the module
377
391
  * @param {string} label the declaration file's path, for a parse error
@@ -442,11 +456,13 @@ export function generatedNamesOf(packageDir: string, generated: string, from: st
442
456
  * differently; and a refresh changes only the icons its module supplies.
443
457
  * @param {string} packageDir
444
458
  * @param {string} generated
445
- * @returns {Map<string, {module: string, source: string}>}
459
+ * @returns {Map<string, {module: string, source: string, adapt: boolean}>}
460
+ * `adapt`: the set's adapter turns the export into a component
446
461
  */
447
462
  export function generatedOrigins(packageDir: string, generated: string): Map<string, {
448
463
  module: string;
449
464
  source: string;
465
+ adapt: boolean;
450
466
  }>;
451
467
  /**
452
468
  * Whether two module specifiers name the same module: the same package