@astryxdesign/cli 0.7.12-canary.f543ec7 → 0.7.12

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 (113) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/api/build/blocks/blocks.d.mts +41 -0
  3. package/api/build/blocks/blocks.mjs +118 -68
  4. package/api/component/__fixtures__/resolutions/modules.txt +3 -3
  5. package/api/docs/_adapter.d.mts +22 -0
  6. package/api/docs/_adapter.mjs +81 -3
  7. package/api/docs/docs.doc.mjs +5 -5
  8. package/api/docs/docs.type.d.mts +21 -0
  9. package/api/docs/docs.type.mjs +11 -0
  10. package/api/docs/list/list.d.mts +5 -1
  11. package/api/docs/list/list.mjs +72 -6
  12. package/api/docs/node/node.mjs +2 -40
  13. package/api/doctor/doctor.d.mts +4 -4
  14. package/api/doctor/doctor.mjs +248 -78
  15. package/api/icon/_authoring.mjs +8 -1
  16. package/api/icon/add/add.mjs +45 -5
  17. package/api/icon/icon.type.d.mts +24 -0
  18. package/api/icon/icon.type.mjs +6 -0
  19. package/api/icon/iconAdd.doc.mjs +1 -1
  20. package/api/icon/map/map.mjs +11 -4
  21. package/api/index.d.mts +1 -0
  22. package/api/index.mjs +1 -0
  23. package/api/integration/add-helpers.d.mts +5 -1
  24. package/api/integration/add-helpers.mjs +1 -1
  25. package/api/render/_adapter.mjs +9 -0
  26. package/api/render/render.doc.mjs +3 -3
  27. package/api/render/render.type.d.mts +1 -1
  28. package/api/render/render.type.mjs +1 -1
  29. package/api/search/ORDERING.md +3 -0
  30. package/api/search/_adapter.d.mts +32 -3
  31. package/api/search/_adapter.mjs +50 -22
  32. package/api/search/_fit-order.d.mts +47 -0
  33. package/api/search/_fit-order.mjs +257 -0
  34. package/api/search/_main-searcher.mjs +15 -5
  35. package/api/search/exportSearchIndex.doc.d.mts +10 -0
  36. package/api/search/exportSearchIndex.doc.mjs +58 -0
  37. package/api/search/rankSearchIndex.doc.d.mts +10 -0
  38. package/api/search/rankSearchIndex.doc.mjs +79 -0
  39. package/api/search/search-index.d.mts +44 -0
  40. package/api/search/search-index.mjs +270 -0
  41. package/api/search/search.type.d.mts +28 -0
  42. package/api/search/search.type.mjs +17 -0
  43. package/api/swizzle/_moduleGraph.d.mts +7 -0
  44. package/api/swizzle/_moduleGraph.mjs +191 -83
  45. package/api/swizzle/copy/copy.mjs +28 -6
  46. package/api/template/skeleton/skeleton.mjs +1 -1
  47. package/api/template/template.mjs +1 -0
  48. package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +1 -1
  49. package/assets/codemods/transforms/v0.1.0/index.mjs +1 -1
  50. package/assets/discover/official-packages.json +16 -0
  51. package/assets/docs/styling-overview.doc.mjs +3 -3
  52. package/assets/docs/tree/icon-sets.doc.mjs +4 -4
  53. package/assets/docs/tree/render-pages.doc.mjs +3 -3
  54. package/assets/docs/tree/tailwind.doc.mjs +41 -1
  55. package/assets/docs/tree/type-scale.doc.mjs +19 -2
  56. package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
  57. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.doc.mjs +14 -0
  58. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.tsx +56 -0
  59. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.doc.mjs +2 -2
  60. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.tsx +66 -10
  61. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.doc.mjs +14 -0
  62. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.tsx +39 -0
  63. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.doc.mjs +14 -0
  64. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.tsx +83 -0
  65. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
  66. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +41 -0
  67. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
  68. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
  69. package/assets/templates/pages/table-filter/page.tsx +15 -23
  70. package/assets/templates/pages/table-grouped/page.tsx +30 -30
  71. package/assets/templates/pages/table-inbox/page.tsx +3 -6
  72. package/assets/templates/themes/butter/butterTheme.ts +1 -1
  73. package/assets/templates/themes/gothic/gothicTheme.ts +16 -0
  74. package/assets/templates/themes/matcha/matchaTheme.ts +1 -1
  75. package/assets/templates/themes/neutral/neutralTheme.ts +6 -0
  76. package/assets/templates/themes/y2k/y2kPaletteRefs.generated.ts +1 -1
  77. package/assets/templates/themes/y2k/y2kTheme.ts +1 -1
  78. package/clients/cli/commands/docs.doc.mjs +5 -1
  79. package/clients/cli/commands/docs.mjs +30 -2
  80. package/clients/cli/commands/experimental/icon.mjs +25 -4
  81. package/clients/cli/commands/experimental-render.doc.mjs +1 -1
  82. package/clients/cli/commands/template.mjs +204 -181
  83. package/clients/cli/lib/resolve-theme.mjs +16 -7
  84. package/foundation/discovery/component-discovery.mjs +1 -1
  85. package/foundation/discovery/hook-discovery.mjs +31 -8
  86. package/foundation/discovery/icon-set-writer.d.mts +21 -5
  87. package/foundation/discovery/icon-set-writer.mjs +114 -13
  88. package/foundation/discovery/template-adapter.mjs +2 -2
  89. package/foundation/doc-compiler/import.d.mts +5 -1
  90. package/foundation/doc-compiler/import.mjs +40 -6
  91. package/foundation/fs/import-url.d.mts +20 -0
  92. package/foundation/fs/import-url.mjs +22 -0
  93. package/foundation/fs/module-loader.d.mts +24 -3
  94. package/foundation/fs/module-loader.mjs +69 -47
  95. package/foundation/render/charts.d.mts +59 -0
  96. package/foundation/render/charts.mjs +216 -0
  97. package/foundation/render/embedded-imports.d.mts +7 -7
  98. package/foundation/render/embedded-imports.mjs +63 -54
  99. package/foundation/render/page-host.d.mts +4 -0
  100. package/foundation/render/page-host.mjs +45 -0
  101. package/foundation/render/page-worker.mjs +350 -57
  102. package/foundation/render/render-page.d.mts +11 -6
  103. package/foundation/render/render-page.mjs +164 -54
  104. package/foundation/render/ts-paths.d.mts +63 -0
  105. package/foundation/render/ts-paths.mjs +138 -0
  106. package/foundation/response/error-codes.doc.mjs +3 -3
  107. package/foundation/response/error-codes.mjs +3 -2
  108. package/foundation/text/levenshtein.d.mts +6 -1
  109. package/foundation/text/levenshtein.mjs +25 -9
  110. package/foundation/text/suggest-order.d.mts +8 -1
  111. package/foundation/text/suggest-order.mjs +72 -6
  112. package/foundation/xle/registry-core.mjs +1 -1
  113. package/package.json +7 -6
@@ -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
  };
package/api/index.d.mts CHANGED
@@ -47,6 +47,7 @@ export * from "./integration/authoring-checks.type.mjs";
47
47
  export * from "./integration/integration-check.type.mjs";
48
48
  export type Logger = import("./logger.mjs").Logger;
49
49
  export { themeBuild, themeAdd, themeRemove, themeUse, themeEject, themeTemplate, themeList, themeListAvailable, themeListCopySources, themeTargets, themePaletteGenerate, generateTonalPalette, listThemes } from "./theme/theme.mjs";
50
+ export { exportSearchIndex, rankSearchIndex } from "./search/search-index.mjs";
50
51
  export { integrationAdd, integrationAddAgentDoc, integrationAddCodemod, integrationAddComponent, integrationAddDoc, integrationAddTemplate } from "./integration/add-contribution.mjs";
51
52
  export { validateIntegration, summarizeIssues } from "./integration/validate-integration.mjs";
52
53
  export { integrationTemplateConflicts, integrationComponentConflicts, integrationDocConflicts } from "./integration/authoring-checks.mjs";
package/api/index.mjs CHANGED
@@ -48,6 +48,7 @@ export {
48
48
  } from './theme/theme.mjs';
49
49
  export {hook} from './hook/hook.mjs';
50
50
  export {search} from './search/search.mjs';
51
+ export {exportSearchIndex, rankSearchIndex} from './search/search-index.mjs';
51
52
  export {build} from './build/build.mjs';
52
53
  export {swizzle} from './swizzle/swizzle.mjs';
53
54
  export {gapReport} from './gap-report/gap-report.mjs';
@@ -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 {
@@ -548,6 +548,15 @@ export function reverseToFile({cwd, input, output, overwrite}) {
548
548
  ...(missing.length > 0 ? {missing} : {}),
549
549
  };
550
550
  }
551
+ // A file a render wrote, whose source block was taken out on the way
552
+ // (sanitizers and editors strip <script> elements), says so.
553
+ if (!isAstryxMarkup(html) && /<meta name="generator" content="Astryx[ "]/.test(html)) {
554
+ throw new AstryxError(
555
+ `${shown(cwd, input)} was rendered by Astryx, but its embedded source was removed (a sanitizer or an editor strips <script> blocks) and it has no Astryx components to rebuild from; nothing was written. Render the page again from its source and send the new file as is.`,
556
+ [],
557
+ ERROR_CODES.ERR_RENDER_SOURCE_DAMAGED,
558
+ );
559
+ }
551
560
  if (!isAstryxMarkup(html)) {
552
561
  throw new AstryxError(
553
562
  `${shown(cwd, input)} is not an Astryx page: it has no embedded source and no Astryx components to rebuild from. To start an Astryx page like it, run astryx build "<what the page is>".`,
@@ -96,12 +96,12 @@ 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
- when: 'reversing a file with no embedded source and no Astryx markup; the suggestion is `astryx build "<what the page is>"`',
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>"`',
103
103
  },
104
- {code: 'ERR_RENDER_SOURCE_DAMAGED', when: 'reversing a file whose embedded source is damaged, lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds (a dotfile or dot-folder, `node_modules`, a package manifest, lockfile or tool configuration, or any kind but a page module, stylesheet, JSON or image)'},
104
+ {code: 'ERR_RENDER_SOURCE_DAMAGED', when: 'reversing a file a render wrote whose embedded source was removed (and that has no Astryx components to rebuild from), a file whose embedded source is damaged, lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds (a dotfile or dot-folder, `node_modules`, a package manifest, lockfile or tool configuration, or any kind but a page module, stylesheet, JSON or image)'},
105
105
  {code: 'ERR_PATH_TRAVERSAL', when: 'a file would be written outside the current folder (symlinks followed), or through a symbolic link: `out`, or with `reverse` the page or an embedded file; or, with `reverse`, a file other than the page would go into a folder that already exists'},
106
106
  ],
107
107
  examples: [
@@ -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
 
@@ -6,12 +6,15 @@
6
6
 
7
7
  `spec:AST-083` owns search's result order; this file describes the data that implements it. Inside one score band, a searcher's static weights decide the order. Matches with the same score move together as one block, placed by the learned position of the block's first match: moving each one on its own breaks search's existing rules for tied matches. Inside a block, the lexical ranker's order holds, except below the first result: there a block of one domain puts a part of the first result first (its name words start with the first result's; at most two parts lead across an answer), then the match whose matched words are rarer in the catalog. Matches never cross bands or kinds. There is no model runtime: every number is a plain static value read at load time.
8
8
 
9
+ After that order, one fit pass reads the first ten results (`_fit-order.mjs`). The first result stays. The others split into runs of one band and kind. Inside a run, each match gets a fit to the whole query from build's block-picker match (`../build/blocks/blocks.mjs` with `blocks.weights.json`) over the match's own docs: its name words, display name or title, description, and keywords. A match whose fit is under 1.7 times the run's median fit moves, in order, after the others in its run, unless its name spells two or more of the query's words. Such a match never moves down; it can still move up when others move after it. A run whose median fit is zero or less keeps its order. A query that is an entry's whole name skips the pass, as it skips the learned order. The pass always reads ten places before the limit cuts the answer. At the default limit, and at any limit of 10 or more, the set of results stays the same. A smaller limit shows the top of the new order, so it can show a different result. Build's loader (`loadBlockPicker` in `../build/_adapter.mjs`) reads and checks the block-picker weights for both commands. When the file is missing, unreadable, or not the picker's shape, the loader returns null and the pass keeps the order. The pass reads the weights only when a run holds two or more matches, and it decodes only the matrix rows that the query's words and those matches' docs use, so a search does not pay for the whole catalog.
10
+
9
11
  ## Files
10
12
 
11
13
  | File | What it holds |
12
14
  | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
15
  | `tree.weights.json` | The root classifier: for each query word or word pair, one number per class (component, hook, doc, template, theme, none). The query's tokens vote, and a topic searcher takes the query only when its class wins by the floor in `tree.mjs`. |
14
16
  | `_main-searcher.weights.json`, `_doc-searcher.weights.json`, `_template-searcher.weights.json` | One weight, mean and spread per signal for each searcher (see `SIGNALS` and `PER_DOMAIN` in `_features.mjs`). A match's place inside its band is the weighted sum of its standardized signals. |
17
+ | `../build/blocks/blocks.weights.json` | The block picker's vocabulary, idf and two projection matrices, shared with build. The fit pass reads them through build's loader; search ships no copy. |
15
18
  | `_cards.json` | One card per Core component and hook: what it is, the words people use for it, and its main props, written from the item's own source. Cards feed one signal, word overlap with the card. |
16
19
 
17
20
  The signals are counts over the query and each entry's own text: the lexical score and rank, BM25 over the entry's name, docs text and card, how many query words the entry covers, whether the query holds the entry's name, and the classifier's guess of the domain.
@@ -16,7 +16,7 @@ export function requireSearchCoreDir(cwd: string): string;
16
16
  * in the CLI's searches. A caller that runs its own operation shares it.
17
17
  *
18
18
  * @param {{cwd?: string, type?: string}} options
19
- * @returns {Promise<{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside: boolean, coreSkipped: string | null}>}
19
+ * @returns {Promise<{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside: boolean, coreSkipped: string | null, coreSkippedCode: string | null}>}
20
20
  */
21
21
  export function gatherCandidates(options: {
22
22
  cwd?: string;
@@ -27,7 +27,28 @@ export function gatherCandidates(options: {
27
27
  searchedComponents: boolean;
28
28
  outside: boolean;
29
29
  coreSkipped: string | null;
30
+ coreSkippedCode: string | null;
30
31
  }>;
32
+ /**
33
+ * The Core a search of `type` reads, and the domains it gathers. `astryx docs`
34
+ * reads docs without @astryxdesign/core, and `astryx theme list` reads themes
35
+ * without it, so a docs or themes search never looks for Core. Every other
36
+ * domain reads Core: the project's, else the answering release's own,
37
+ * installed into the per-user cache on first need (spec AST-081 FR3). A Core
38
+ * domain asked for by name throws the lookup's coded error. An open search
39
+ * without Core gathers the docs and themes alone and records why in
40
+ * `coreSkipped`; it never fails silently.
41
+ * @param {string | undefined} type
42
+ * @param {(cwd: string) => string} lookup finds the Core folder, or throws
43
+ * @param {string} cwd
44
+ * @returns {{coreDir: string | null, coreSkipped: string | null, coreSkippedCode: string | null, wants: (domain: string) => boolean}}
45
+ */
46
+ export function resolveSearchCore(type: string | undefined, lookup: (cwd: string) => string, cwd: string): {
47
+ coreDir: string | null;
48
+ coreSkipped: string | null;
49
+ coreSkippedCode: string | null;
50
+ wants: (domain: string) => boolean;
51
+ };
31
52
  /**
32
53
  * gatherCandidates, reused within one process: a later search in the same
33
54
  * folder, for the same `type`, ranks the candidates an earlier search
@@ -46,8 +67,10 @@ export function gatherCandidatesReused(options: {
46
67
  type?: string;
47
68
  }): Promise<Gathered>;
48
69
  /**
49
- * The search tree's static data, read once: the entry cards and each node's
50
- * weights file.
70
+ * The search tree's static data, read once: the entry cards, each node's
71
+ * weights file, and build's block-picker weights for the fit pass. Build's
72
+ * loader (loadBlockPicker) reads and checks the picker weights, only when the
73
+ * pass first needs them.
51
74
  *
52
75
  * These files are static, reviewed data that only order matches inside a score
53
76
  * band; ORDERING.md beside them says what each holds and how
@@ -89,6 +112,12 @@ export type SearchData = {
89
112
  * The template searcher's weights.
90
113
  */
91
114
  template: SearcherWeights;
115
+ /**
116
+ * Build's block-picker weights, which the fit pass (_fit-order.mjs) reads;
117
+ * null when build's loader finds the file absent, unreadable, or the wrong
118
+ * shape.
119
+ */
120
+ picker: import("../build/blocks/blocks.mjs").BlockPickerWeights | null;
92
121
  };
93
122
  export type SearchCatalog = Awaited<ReturnType<typeof gatherCandidates>> & {
94
123
  data: SearchData;
@@ -16,6 +16,7 @@ import {
16
16
  } from '../../foundation/fs/read-record.mjs';
17
17
  import {isOutsideProject} from '../../foundation/env/project-scope.mjs';
18
18
  import {coreDirOrThrow} from '../error.mjs';
19
+ import {loadBlockPicker} from '../build/_adapter.mjs';
19
20
  import {CORELESS_DOMAINS} from './_query.mjs';
20
21
  import {gatherComponents} from './_gather-components.mjs';
21
22
  import {gatherHooks} from './_gather-hooks.mjs';
@@ -43,46 +44,62 @@ export function requireSearchCoreDir(cwd) {
43
44
  * in the CLI's searches. A caller that runs its own operation shares it.
44
45
  *
45
46
  * @param {{cwd?: string, type?: string}} options
46
- * @returns {Promise<{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside: boolean, coreSkipped: string | null}>}
47
+ * @returns {Promise<{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside: boolean, coreSkipped: string | null, coreSkippedCode: string | null}>}
47
48
  */
48
49
  export function gatherCandidates(options) {
49
50
  return withListingCache(() => gatherDomains(options));
50
51
  }
51
52
 
52
53
  /**
53
- * @param {{cwd?: string, type?: string}} options
54
+ * The Core a search of `type` reads, and the domains it gathers. `astryx docs`
55
+ * reads docs without @astryxdesign/core, and `astryx theme list` reads themes
56
+ * without it, so a docs or themes search never looks for Core. Every other
57
+ * domain reads Core: the project's, else the answering release's own,
58
+ * installed into the per-user cache on first need (spec AST-081 FR3). A Core
59
+ * domain asked for by name throws the lookup's coded error. An open search
60
+ * without Core gathers the docs and themes alone and records why in
61
+ * `coreSkipped`; it never fails silently.
62
+ * @param {string | undefined} type
63
+ * @param {(cwd: string) => string} lookup finds the Core folder, or throws
64
+ * @param {string} cwd
65
+ * @returns {{coreDir: string | null, coreSkipped: string | null, coreSkippedCode: string | null, wants: (domain: string) => boolean}}
54
66
  */
55
- async function gatherDomains(options) {
56
- const {cwd = process.cwd(), type} = options;
57
- // `astryx docs` reads docs without @astryxdesign/core, and `astryx theme
58
- // list` reads themes without it (bundled themes need no project), so a
59
- // search of either must too. Every other domain reads core: asked for by
60
- // name, it is an error without core; an open search then covers the docs
61
- // and themes alone.
67
+ export function resolveSearchCore(type, lookup, cwd) {
62
68
  const needsCore = !type || !CORELESS_DOMAINS.includes(type);
63
- // A domain that reads Core gets it as `component` does: the project's, else
64
- // the answering release's own, installed into the per-user cache on first
65
- // need (spec AST-081 FR3). Asked for by name, a Core that cannot be had is
66
- // the error; an open search answers from the rest and says what it skipped
67
- // and why, never silently.
68
69
  /** @type {string | null} */
69
70
  let coreDir = null;
70
71
  /** @type {string | null} */
71
72
  let coreSkipped = null;
73
+ /** @type {string | null} */
74
+ let coreSkippedCode = null;
72
75
  if (needsCore && type) {
73
- coreDir = coreDirOrThrow(requireSearchCoreDir, cwd);
76
+ coreDir = coreDirOrThrow(lookup, cwd);
74
77
  } else if (needsCore) {
75
78
  try {
76
- coreDir = requireSearchCoreDir(cwd);
79
+ coreDir = lookup(cwd);
77
80
  } catch (error) {
78
81
  coreSkipped = /** @type {Error} */ (error).message;
82
+ const code = /** @type {any} */ (error)?.code;
83
+ coreSkippedCode = typeof code === 'string' ? code : null;
79
84
  }
80
85
  }
86
+ /** @param {string} domain */
87
+ const wants = domain =>
88
+ (!type && (coreDir != null || CORELESS_DOMAINS.includes(domain))) ||
89
+ type === domain;
90
+ return {coreDir, coreSkipped, coreSkippedCode, wants};
91
+ }
81
92
 
82
- // Gather candidates from each requested domain in parallel.
83
- /** @param {string} d */
84
- const wants = d =>
85
- (!type && (coreDir != null || CORELESS_DOMAINS.includes(d))) || type === d;
93
+ /**
94
+ * @param {{cwd?: string, type?: string}} options
95
+ */
96
+ async function gatherDomains(options) {
97
+ const {cwd = process.cwd(), type} = options;
98
+ const {coreDir, coreSkipped, coreSkippedCode, wants} = resolveSearchCore(
99
+ type,
100
+ requireSearchCoreDir,
101
+ cwd,
102
+ );
86
103
  const [components, hooks, docTopics, templates, themes] = await Promise.all([
87
104
  wants('component')
88
105
  ? gatherComponents(/** @type {string} */ (coreDir), cwd)
@@ -103,6 +120,7 @@ async function gatherDomains(options) {
103
120
  searchedComponents: wants('component'),
104
121
  outside,
105
122
  coreSkipped,
123
+ coreSkippedCode,
106
124
  };
107
125
  }
108
126
 
@@ -150,6 +168,10 @@ export function gatherCandidatesReused(options) {
150
168
  * @property {SearcherWeights} main The main searcher's weights.
151
169
  * @property {SearcherWeights} doc The doc searcher's weights.
152
170
  * @property {SearcherWeights} template The template searcher's weights.
171
+ * @property {import('../build/blocks/blocks.mjs').BlockPickerWeights | null} picker
172
+ * Build's block-picker weights, which the fit pass (_fit-order.mjs) reads;
173
+ * null when build's loader finds the file absent, unreadable, or the wrong
174
+ * shape.
153
175
  * @typedef {Awaited<ReturnType<typeof gatherCandidates>> & {data: SearchData}} SearchCatalog
154
176
  */
155
177
 
@@ -157,8 +179,10 @@ export function gatherCandidatesReused(options) {
157
179
  let searchData = null;
158
180
 
159
181
  /**
160
- * The search tree's static data, read once: the entry cards and each node's
161
- * weights file.
182
+ * The search tree's static data, read once: the entry cards, each node's
183
+ * weights file, and build's block-picker weights for the fit pass. Build's
184
+ * loader (loadBlockPicker) reads and checks the picker weights, only when the
185
+ * pass first needs them.
162
186
  *
163
187
  * These files are static, reviewed data that only order matches inside a score
164
188
  * band; ORDERING.md beside them says what each holds and how
@@ -176,6 +200,10 @@ export function loadSearchData() {
176
200
  main: read('./_main-searcher.weights.json'),
177
201
  doc: read('./_doc-searcher.weights.json'),
178
202
  template: read('./_template-searcher.weights.json'),
203
+ // Read on first use: most searches never reach the fit pass.
204
+ get picker() {
205
+ return loadBlockPicker();
206
+ },
179
207
  };
180
208
  }
181
209
  return searchData;