@astryxdesign/cli 0.6.3-canary.ea2f048 → 0.6.3-canary.ebaebc4

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 (188) hide show
  1. package/README.md +2 -1
  2. package/api/build/build.type.d.mts +2 -2
  3. package/api/build/build.type.mjs +2 -2
  4. package/api/component/component.type.d.mts +6 -6
  5. package/api/component/component.type.mjs +19 -19
  6. package/api/discover/discover.type.d.mts +4 -4
  7. package/api/discover/discover.type.mjs +10 -10
  8. package/api/docs/_adapter.d.mts +37 -24
  9. package/api/docs/_adapter.mjs +169 -83
  10. package/api/docs/compiled-topics.test.mjs +78 -0
  11. package/api/docs/detail/detail.mjs +14 -63
  12. package/api/docs/detail/section/section.d.mts +1 -1
  13. package/api/docs/detail/section/section.mjs +44 -20
  14. package/api/docs/detail/section/section.test.mjs +41 -0
  15. package/api/docs/docs.d.mts +7 -2
  16. package/api/docs/docs.doc.mjs +27 -10
  17. package/api/docs/docs.mjs +16 -9
  18. package/api/docs/docs.test.mjs +6 -0
  19. package/api/docs/docs.type.d.mts +40 -3
  20. package/api/docs/docs.type.mjs +36 -8
  21. package/api/docs/index/index.d.mts +18 -0
  22. package/api/docs/index/index.mjs +32 -0
  23. package/api/docs/index/index.test.mjs +62 -0
  24. package/api/docs/integrationDocs.test.mjs +106 -0
  25. package/api/doctor/doctor.d.mts +48 -0
  26. package/api/doctor/doctor.mjs +232 -0
  27. package/api/doctor/doctor.test.mjs +196 -0
  28. package/api/hook/hook.type.d.mts +3 -3
  29. package/api/hook/hook.type.mjs +11 -11
  30. package/api/hook/list/list.d.mts +1 -1
  31. package/api/integration/add-contribution.mjs +5 -3
  32. package/api/integration/add-contribution.test.mjs +4 -4
  33. package/api/integration/integration-authoring.type.d.mts +1 -1
  34. package/api/integration/pack-check.mjs +49 -7
  35. package/api/integration/pack-check.test.mjs +249 -0
  36. package/api/search/search.d.mts +1 -1
  37. package/api/search/search.mjs +5 -5
  38. package/api/search/search.type.d.mts +2 -2
  39. package/api/search/search.type.mjs +1 -1
  40. package/api/swizzle/swizzle.type.d.mts +2 -2
  41. package/api/swizzle/swizzle.type.mjs +2 -2
  42. package/api/template/template.d.mts +1 -1
  43. package/api/template/template.type.d.mts +6 -6
  44. package/api/template/template.type.mjs +12 -12
  45. package/api/theme/build/build.mjs +20 -6
  46. package/api/theme/build/build.test.mjs +127 -0
  47. package/api/theme/palette/generate/generate.mjs +1 -1
  48. package/api/theme/palette/generate/generator.d.mts +10 -13
  49. package/api/theme/palette/generate/generator.mjs +7 -3
  50. package/api/theme/theme.type.d.mts +170 -11
  51. package/api/theme/theme.type.mjs +94 -27
  52. package/api/upgrade/_adapter.mjs +71 -5
  53. package/api/upgrade/project-context.test.mjs +272 -0
  54. package/api/upgrade/upgrade.doc.mjs +4 -3
  55. package/api/upgrade/upgrade.type.d.mts +5 -5
  56. package/api/upgrade/upgrade.type.mjs +11 -11
  57. package/assets/codemods/integration-discovery.mjs +40 -2
  58. package/assets/codemods/integration-discovery.test.mjs +58 -0
  59. package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
  60. package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
  61. package/assets/docs/README.md +9 -0
  62. package/assets/docs/authoring.doc.mjs +14 -0
  63. package/assets/docs/cli-integrations.doc.mjs +86 -15
  64. package/assets/docs/styling-libraries.doc.mjs +1 -1
  65. package/assets/docs/working-with-ai.doc.mjs +1 -1
  66. package/authoring/_shared/contract.ts +22 -0
  67. package/authoring/codemod/codemod.doc.mjs +6 -1
  68. package/authoring/codemod/parse.d.mts +8 -8
  69. package/authoring/codemod/parse.mjs +8 -6
  70. package/authoring/config/parse.d.mts +13 -13
  71. package/authoring/config/parse.mjs +8 -8
  72. package/authoring/config/type.ts +3 -3
  73. package/authoring/debug/parse.d.mts +5 -5
  74. package/authoring/debug/parse.mjs +3 -3
  75. package/authoring/doctypes/_schema.d.mts +788 -23
  76. package/authoring/doctypes/_schema.mjs +492 -39
  77. package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
  78. package/authoring/doctypes/base/graph-fields.doc.mjs +62 -0
  79. package/authoring/doctypes/base/type.ts +40 -0
  80. package/authoring/doctypes/command/command.doc.mjs +3 -2
  81. package/authoring/doctypes/command/parse.d.mts +2 -2
  82. package/authoring/doctypes/command/parse.mjs +1 -1
  83. package/authoring/doctypes/command/type.ts +3 -2
  84. package/authoring/doctypes/component/component.doc.mjs +6 -3
  85. package/authoring/doctypes/component/parse.d.mts +2 -2
  86. package/authoring/doctypes/component/parse.mjs +1 -1
  87. package/authoring/doctypes/component/type.ts +4 -3
  88. package/authoring/doctypes/enum/parse.d.mts +2 -2
  89. package/authoring/doctypes/enum/parse.mjs +1 -1
  90. package/authoring/doctypes/enum/type.ts +3 -1
  91. package/authoring/doctypes/function/function.doc.mjs +4 -0
  92. package/authoring/doctypes/function/parse.d.mts +2 -2
  93. package/authoring/doctypes/function/parse.mjs +1 -1
  94. package/authoring/doctypes/function/type.ts +6 -2
  95. package/authoring/doctypes/hook/hook.doc.mjs +4 -0
  96. package/authoring/doctypes/hook/parse.d.mts +2 -2
  97. package/authoring/doctypes/hook/parse.mjs +1 -1
  98. package/authoring/doctypes/hook/type.ts +3 -2
  99. package/authoring/doctypes/legacy.d.mts +8 -6
  100. package/authoring/doctypes/legacy.mjs +5 -4
  101. package/authoring/doctypes/load-contract.test.mjs +207 -0
  102. package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
  103. package/authoring/doctypes/namespace/namespace.doc.mjs +132 -0
  104. package/authoring/doctypes/namespace/parse.d.mts +12 -0
  105. package/authoring/doctypes/namespace/parse.mjs +25 -0
  106. package/authoring/doctypes/namespace/parse.test.mjs +165 -0
  107. package/authoring/doctypes/namespace/type.ts +71 -0
  108. package/authoring/doctypes/parse.d.mts +20 -18
  109. package/authoring/doctypes/parse.mjs +16 -10
  110. package/authoring/doctypes/parse.test.mjs +77 -3
  111. package/authoring/doctypes/reference/parse.d.mts +2 -2
  112. package/authoring/doctypes/reference/parse.mjs +8 -5
  113. package/authoring/doctypes/reference/reference.doc.mjs +17 -4
  114. package/authoring/doctypes/reference/type.ts +51 -5
  115. package/authoring/doctypes/schema/parse.d.mts +2 -2
  116. package/authoring/doctypes/schema/parse.mjs +1 -1
  117. package/authoring/doctypes/schema/type.ts +3 -2
  118. package/authoring/doctypes/template/parse.d.mts +92 -1
  119. package/authoring/doctypes/template/parse.mjs +36 -2
  120. package/authoring/doctypes/template/parse.test.mjs +8 -2
  121. package/authoring/doctypes/template/template.doc.mjs +4 -0
  122. package/authoring/doctypes/template/type.ts +5 -2
  123. package/authoring/doctypes/types.ts +10 -9
  124. package/authoring/gap-report/parse.d.mts +10 -10
  125. package/authoring/gap-report/parse.mjs +6 -6
  126. package/authoring/gap-report/type.ts +1 -1
  127. package/authoring/identity/identity.doc.d.mts +9 -0
  128. package/authoring/identity/identity.doc.mjs +61 -0
  129. package/authoring/identity/type.ts +132 -0
  130. package/authoring/index.d.mts +1 -0
  131. package/authoring/index.d.ts +49 -17
  132. package/authoring/index.mjs +1 -0
  133. package/authoring/integration/integration.doc.mjs +13 -6
  134. package/authoring/integration/parse.d.mts +2 -2
  135. package/authoring/integration/parse.mjs +1 -1
  136. package/authoring/integration/parse.test.mjs +10 -1
  137. package/authoring/integration/schema.d.mts +6 -4
  138. package/authoring/integration/schema.mjs +9 -3
  139. package/authoring/integration/type.ts +23 -6
  140. package/authoring/shadcn/receipt.d.mts +6 -6
  141. package/clients/cli/commands/docs.doc.mjs +13 -3
  142. package/clients/cli/commands/docs.mjs +121 -21
  143. package/clients/cli/commands/docs.test.mjs +88 -0
  144. package/clients/cli/commands/integration-authoring.test.mjs +13 -9
  145. package/clients/cli/commands/theme-palette-generate.doc.mjs +8 -4
  146. package/clients/cli/commands/upgrade.doc.mjs +2 -2
  147. package/clients/cli/formatters/index.mjs +162 -1
  148. package/clients/cli/formatters/index.test.mjs +91 -0
  149. package/clients/cli/lib/manifest.mjs +7 -2
  150. package/foundation/config/project.mjs +21 -6
  151. package/foundation/discovery/authoring-self-docs.d.mts +69 -0
  152. package/foundation/discovery/authoring-self-docs.mjs +214 -0
  153. package/foundation/discovery/authoring-self-docs.test.mjs +154 -0
  154. package/foundation/discovery/component-discovery.d.mts +1 -1
  155. package/foundation/discovery/component-discovery.mjs +2 -1
  156. package/foundation/discovery/docs-discovery.d.mts +11 -4
  157. package/foundation/discovery/docs-discovery.mjs +208 -88
  158. package/foundation/discovery/docs-discovery.test.mjs +279 -13
  159. package/foundation/discovery/docs-output-budget.d.mts +28 -0
  160. package/foundation/discovery/docs-output-budget.mjs +50 -0
  161. package/foundation/discovery/docs-section-key.d.mts +98 -0
  162. package/foundation/discovery/docs-section-key.mjs +221 -0
  163. package/foundation/discovery/docs-section-key.test.mjs +224 -0
  164. package/foundation/discovery/template-adapter.mjs +2 -1
  165. package/foundation/discovery/theming-targets.test.mjs +4 -0
  166. package/foundation/doc-compiler/compile.d.mts +162 -0
  167. package/foundation/doc-compiler/compile.mjs +262 -0
  168. package/foundation/doc-compiler/doc-compiler.test.mjs +687 -0
  169. package/foundation/doc-compiler/ir.d.mts +9 -0
  170. package/foundation/doc-compiler/ir.mjs +287 -0
  171. package/foundation/doc-compiler/lenses.d.mts +33 -0
  172. package/foundation/doc-compiler/lenses.mjs +127 -0
  173. package/foundation/identity/provider-identity.d.mts +90 -0
  174. package/foundation/identity/provider-identity.mjs +320 -0
  175. package/foundation/identity/provider-identity.test.mjs +254 -0
  176. package/foundation/identity/providers.d.mts +7 -0
  177. package/foundation/identity/providers.mjs +16 -0
  178. package/foundation/integrations/autolink.mjs +12 -5
  179. package/foundation/integrations/integration-warnings.mjs +6 -0
  180. package/foundation/integrations/integrations.d.mts +46 -2
  181. package/foundation/integrations/integrations.mjs +167 -8
  182. package/foundation/integrations/integrations.test.mjs +384 -1
  183. package/foundation/integrations/provider-conflicts.test.mjs +125 -0
  184. package/foundation/integrations/validate-contributions.d.mts +2 -0
  185. package/foundation/integrations/validate-contributions.mjs +10 -0
  186. package/foundation/response/json-contract.test.mjs +46 -17
  187. package/foundation/response/response-types.doc.mjs +6 -1
  188. package/package.json +9 -11
@@ -11,7 +11,8 @@
11
11
  * which fields to show and in what order.
12
12
  *
13
13
  * Constraints (deliberately narrow):
14
- * - Plain ASCII only. No color, no TTY detection, no width wrapping. Output is
14
+ * - Plain ASCII only. No color and no TTY detection. Long lines wrap at a
15
+ * fixed {@link WRAP_WIDTH}, never at the terminal's width, so output is
15
16
  * byte-for-byte deterministic whether printed or piped to an agent.
16
17
  * - Renderers return an opaque {@link Block}; `emit` accepts ONLY Blocks, so a
17
18
  * stray string can't leak onto stdout (the compiler rejects `emit('x')`).
@@ -33,6 +34,50 @@ export const BULLET = '-';
33
34
  export const ERR = '!!';
34
35
  export const WARN = '!';
35
36
 
37
+ /** The column long human-output lines wrap at. */
38
+ export const WRAP_WIDTH = 120;
39
+
40
+ /** The widest first column an inline record pads to; a longer value overhangs. */
41
+ const INLINE_LEAD_MAX = 32;
42
+
43
+ /**
44
+ * Characters a terminal draws two columns wide: CJK ideographs and
45
+ * punctuation, kana, hangul, and fullwidth forms. A line may break between
46
+ * any two of them.
47
+ */
48
+ const WIDE_CHAR =
49
+ /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-\u9fff\ua000-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6]/u;
50
+
51
+ /**
52
+ * How many terminal columns a string takes.
53
+ * @param {string} s
54
+ * @returns {number}
55
+ */
56
+ export function displayWidth(s) {
57
+ let width = 0;
58
+ for (const ch of String(s)) width += WIDE_CHAR.test(ch) ? 2 : 1;
59
+ return width;
60
+ }
61
+
62
+ /**
63
+ * Cut a line to `width` columns, ending it with `...` when anything was cut.
64
+ * @param {string} line
65
+ * @param {number} width
66
+ * @returns {string}
67
+ */
68
+ function truncateToWidth(line, width) {
69
+ if (displayWidth(line) <= width) return line;
70
+ let out = '';
71
+ let used = 0;
72
+ for (const ch of line) {
73
+ const w = WIDE_CHAR.test(ch) ? 2 : 1;
74
+ if (used + w > width - 3) break;
75
+ out += ch;
76
+ used += w;
77
+ }
78
+ return `${out.trimEnd()}...`;
79
+ }
80
+
36
81
  /**
37
82
  * An opaque, renderer-produced block of output. Nominal via a private field:
38
83
  * nothing outside this file can construct one, so `emit` can trust that whatever
@@ -67,6 +112,13 @@ export class Block {
67
112
  * @property {Record<string, string>} [labels] - Rename a key for display.
68
113
  * @property {Record<string, (value: any) => string>} [format] - Transform a
69
114
  * value before rendering (e.g. prefix a command with the package manager).
115
+ * @property {'stacked' | 'inline'} [layout] - `stacked` (the default): one
116
+ * `key: value` line per field, a blank line between records. `inline`: one
117
+ * record per line, the first field in a padded column and the rest joined by
118
+ * ` - `, for a list a reader scans.
119
+ * @property {'wrap' | 'truncate'} [overflow] - What an inline record longer
120
+ * than {@link WRAP_WIDTH} does: `wrap` (the default) continues under the
121
+ * first column; `truncate` cuts it to one line.
70
122
  */
71
123
 
72
124
  /** @param {unknown} v @returns {boolean} */
@@ -103,6 +155,72 @@ function toAscii(s) {
103
155
  .replace(/\u00a0/g, ' ');
104
156
  }
105
157
 
158
+ /**
159
+ * Word-wrap text at `width`, keeping its own line breaks. A word longer than
160
+ * the width stays whole on a line of its own. Continuation lines start with
161
+ * `indent`.
162
+ * @param {string} input
163
+ * @param {{width?: number, indent?: string}} [options]
164
+ * @returns {string}
165
+ */
166
+ export function wrapText(input, {width = WRAP_WIDTH, indent = ''} = {}) {
167
+ return String(input)
168
+ .split('\n')
169
+ .map(line => wrapLine(line, width, indent))
170
+ .join('\n');
171
+ }
172
+
173
+ /**
174
+ * @param {string} line
175
+ * @param {number} width
176
+ * @param {string} indent
177
+ * @returns {string}
178
+ */
179
+ function wrapLine(line, width, indent) {
180
+ if (displayWidth(line) <= width) return line;
181
+ const lead = /^\s*/.exec(line)?.[0] ?? '';
182
+ // Tokens a line may break between: words, and each wide character on its
183
+ // own, since CJK text has no spaces to break at. `gap` is what joined a
184
+ // token to the one before it.
185
+ /** @type {{text: string, gap: string}[]} */
186
+ const tokens = [];
187
+ let gap = '';
188
+ let word = '';
189
+ const flush = () => {
190
+ if (word === '') return;
191
+ tokens.push({text: word, gap});
192
+ gap = '';
193
+ word = '';
194
+ };
195
+ for (const ch of line.slice(lead.length)) {
196
+ if (ch === ' ') {
197
+ flush();
198
+ gap = ' ';
199
+ } else if (WIDE_CHAR.test(ch)) {
200
+ flush();
201
+ tokens.push({text: ch, gap});
202
+ gap = '';
203
+ } else {
204
+ word += ch;
205
+ }
206
+ }
207
+ flush();
208
+ /** @type {string[]} */
209
+ const out = [];
210
+ let current = lead;
211
+ tokens.forEach(({text: token, gap: before}, i) => {
212
+ const joined = i === 0 ? current + token : current + before + token;
213
+ if (i > 0 && displayWidth(joined) > width && current.trim() !== '') {
214
+ out.push(current);
215
+ current = indent + token;
216
+ } else {
217
+ current = joined;
218
+ }
219
+ });
220
+ out.push(current);
221
+ return out.join('\n');
222
+ }
223
+
106
224
  /**
107
225
  * A group label, optionally with an explanatory subtitle rendered on the line(s)
108
226
  * directly beneath the heading (no blank line between). Whatever list/records
@@ -177,10 +295,53 @@ export function record(obj, options = {}) {
177
295
  * @returns {Block}
178
296
  */
179
297
  export function records(items, options = {}) {
298
+ if (options.layout === 'inline') return inlineRecords(items, options);
180
299
  const blocks = items.map(o => record(o, options).toString()).filter(Boolean);
181
300
  return new Block(blocks.join('\n\n'));
182
301
  }
183
302
 
303
+ /**
304
+ * @param {any[]} items
305
+ * @param {RecordOptions} options
306
+ * @returns {Block}
307
+ */
308
+ function inlineRecords(items, options) {
309
+ const [lead, ...rest] = (options.fields ?? Object.keys(items[0] ?? {})).filter(
310
+ k => !options.omit?.includes(k),
311
+ );
312
+ if (lead == null) return new Block('');
313
+ /** @param {any} o @param {string} k */
314
+ const value = (o, k) => {
315
+ const fmt = options.format?.[k];
316
+ return fmt ? fmt(o[k]) : renderValue(o[k]);
317
+ };
318
+ const leads = items.map(o => (isEmpty(o[lead]) ? '' : value(o, lead)));
319
+ const column = Math.min(
320
+ Math.max(0, ...leads.map(l => displayWidth(l))),
321
+ INLINE_LEAD_MAX,
322
+ );
323
+ const indent = ' '.repeat(column + 2);
324
+ const lines = items.map((o, i) => {
325
+ const tail = toAscii(
326
+ rest
327
+ .filter(k => !isEmpty(o[k]))
328
+ .map(k => value(o, k))
329
+ .join(' - '),
330
+ );
331
+ if (tail === '') return toAscii(leads[i]);
332
+ const head = toAscii(`${leads[i].padEnd(column)} `);
333
+ if (options.overflow === 'truncate') {
334
+ return truncateToWidth(head + tail, WRAP_WIDTH);
335
+ }
336
+ // Wrap only the tail, so the padded first column survives the wrap.
337
+ const [first, ...more] = wrapText(tail, {
338
+ width: Math.max(20, WRAP_WIDTH - displayWidth(head)),
339
+ }).split('\n');
340
+ return [head + first, ...more.map(line => indent + line)].join('\n');
341
+ });
342
+ return new Block(lines.join('\n'));
343
+ }
344
+
184
345
  /**
185
346
  * A verbatim block — source dumps, layout skeletons, or a markdown doc. Output
186
347
  * is byte-for-byte (NOT ASCII-normalized), so it survives piping
@@ -8,6 +8,9 @@ import {
8
8
  list,
9
9
  record,
10
10
  records,
11
+ wrapText,
12
+ displayWidth,
13
+ WRAP_WIDTH,
11
14
  code,
12
15
  Block,
13
16
  BULLET,
@@ -148,3 +151,91 @@ describe('emit', () => {
148
151
  expect(spy).not.toHaveBeenCalled();
149
152
  });
150
153
  });
154
+
155
+ describe('wrapText', () => {
156
+ it('leaves short lines and its own line breaks alone', () => {
157
+ expect(wrapText('a b\nc')).toBe('a b\nc');
158
+ });
159
+
160
+ it('wraps at the width with a hanging indent', () => {
161
+ expect(wrapText('aaa bbb ccc', {width: 7, indent: ' '})).toBe('aaa bbb\n ccc');
162
+ });
163
+
164
+ it('keeps a word longer than the width whole', () => {
165
+ expect(wrapText('x '.concat('y'.repeat(20)), {width: 10})).toBe(
166
+ `x\n${'y'.repeat(20)}`,
167
+ );
168
+ });
169
+ });
170
+
171
+ describe('records inline layout', () => {
172
+ const items = [
173
+ {id: 'a', title: 'Alpha', summary: 'First.'},
174
+ {id: 'bbb', title: 'Beta', summary: ''},
175
+ ];
176
+
177
+ it('puts one record on each line under a padded first column', () => {
178
+ expect(
179
+ records(items, {fields: ['id', 'title', 'summary'], layout: 'inline'}).toString(),
180
+ ).toBe('a Alpha - First.\nbbb Beta');
181
+ });
182
+
183
+ it('wraps a long record under the first column', () => {
184
+ const out = records([{id: 'a', text: 'word '.repeat(40).trim()}], {
185
+ layout: 'inline',
186
+ }).toString();
187
+ expect(out.split('\n').length).toBeGreaterThan(1);
188
+ expect(out.split('\n').every(line => line.length <= WRAP_WIDTH)).toBe(true);
189
+ expect(out.split('\n')[1].startsWith(' word')).toBe(true);
190
+ });
191
+
192
+ it('keeps the first column padded on a record that wraps', () => {
193
+ const out = records(
194
+ [
195
+ {id: 'a', text: 'word '.repeat(40).trim()},
196
+ {id: 'longer', text: 'x'},
197
+ ],
198
+ {layout: 'inline'},
199
+ ).toString();
200
+ const lines = out.split('\n');
201
+ expect(lines[0].startsWith(`a${' '.repeat(7)}word`)).toBe(true);
202
+ expect(lines[1].startsWith(' '.repeat(8) + 'word')).toBe(true);
203
+ expect(lines.at(-1)).toBe('longer x');
204
+ expect(lines.every(line => line.length <= WRAP_WIDTH)).toBe(true);
205
+ });
206
+
207
+ it('cuts a long record to one line when asked', () => {
208
+ const out = records([{id: 'a', text: 'word '.repeat(40).trim()}], {
209
+ layout: 'inline',
210
+ overflow: 'truncate',
211
+ }).toString();
212
+ expect(out.includes('\n')).toBe(false);
213
+ expect(out.length).toBeLessThanOrEqual(WRAP_WIDTH);
214
+ expect(out.endsWith('...')).toBe(true);
215
+ });
216
+ });
217
+
218
+ describe('wide characters', () => {
219
+ it('counts CJK characters as two columns', () => {
220
+ expect(displayWidth('亮/暗模式')).toBe(9);
221
+ });
222
+
223
+ it('wraps CJK text, which has no spaces, by columns', () => {
224
+ const out = wrapText('中'.repeat(100), {width: 20});
225
+ expect(out.split('\n').every(line => displayWidth(line) <= 20)).toBe(true);
226
+ expect(out.split('\n')).toHaveLength(10);
227
+ });
228
+
229
+ it('keeps spaces between Latin words next to CJK', () => {
230
+ expect(wrapText('运行 astryx docs 查看', {width: 200})).toBe('运行 astryx docs 查看');
231
+ });
232
+
233
+ it('cuts an inline record to columns, not characters', () => {
234
+ const out = records([{id: 'a', text: '中'.repeat(100)}], {
235
+ layout: 'inline',
236
+ overflow: 'truncate',
237
+ }).toString();
238
+ expect(displayWidth(out)).toBeLessThanOrEqual(WRAP_WIDTH);
239
+ expect(out.endsWith('...')).toBe(true);
240
+ });
241
+ });
@@ -55,7 +55,7 @@ export const RESPONSE_TYPES = {
55
55
  'component.detail.showcase',
56
56
  'component.detail.blocks',
57
57
  ],
58
- docs: ['docs.list', 'docs.detail', 'docs.detail.section'],
58
+ docs: ['docs.list', 'docs.index', 'docs.detail', 'docs.detail.section'],
59
59
  blog: ['blog.list', 'blog.detail'],
60
60
  discover: [
61
61
  'discover.list',
@@ -105,7 +105,12 @@ const EXAMPLES = {
105
105
  'astryx component XDSButton',
106
106
  'astryx component XDSButton --props --json',
107
107
  ],
108
- docs: ['astryx docs', 'astryx docs spacing --json'],
108
+ docs: [
109
+ 'astryx docs',
110
+ 'astryx docs spacing --json',
111
+ 'astryx docs theme --index',
112
+ 'astryx docs theme quick-start',
113
+ ],
109
114
  discover: ['astryx discover --json'],
110
115
  search: [
111
116
  'astryx search modal --json',
@@ -38,6 +38,7 @@ import {parseConfig} from '../../authoring/config/parse.mjs';
38
38
  import {
39
39
  loadIntegrations,
40
40
  loadLocalIntegration,
41
+ markProviderConflicts,
41
42
  } from '../integrations/integrations.mjs';
42
43
  import {autolinkIntegrations} from '../integrations/autolink.mjs';
43
44
  import {
@@ -203,10 +204,11 @@ export class Project {
203
204
  /** @type {ProjectIntegrationIssue[]} */
204
205
  #issues = [];
205
206
  /**
206
- * Package names of integrations whose issues have already been collected
207
- * (via a discovery method or a direct issues() validation), so issues() can
208
- * fill in only the ones not yet visited and never double-collect.
209
- * @type {Set<string>}
207
+ * Loaded integrations whose issues have already been collected (via a
208
+ * discovery method or a direct issues() validation), so issues() can fill in
209
+ * only the ones not yet visited and never double-collect. Keyed by entry, not
210
+ * label: two entries can share a package name (an alias at another version).
211
+ * @type {Set<object>}
210
212
  */
211
213
  #visitedIssues = new Set();
212
214
 
@@ -277,6 +279,8 @@ export class Project {
277
279
  loadedIntegrations = await loadIntegrations(integrations, {
278
280
  cwd: projectDir,
279
281
  fresh,
282
+ // Provider identity is resolved once, below, over the final set.
283
+ resolveProviders: false,
280
284
  });
281
285
  }
282
286
 
@@ -310,6 +314,10 @@ export class Project {
310
314
  else loadedIntegrations[existing] = localIntegration;
311
315
  }
312
316
 
317
+ // Autolinked and local packages can claim a provider ID that a configured
318
+ // package already holds, so identity is resolved once over the final set.
319
+ loadedIntegrations = markProviderConflicts(loadedIntegrations);
320
+
313
321
  // The debug recorder resolves its settings synchronously, long before any
314
322
  // command gets here, so this is where a project's `debug` block gets a
315
323
  // turn. The event is not written until process exit, so settings applied
@@ -436,6 +444,13 @@ export class Project {
436
444
  * @returns {string}
437
445
  */
438
446
  #pkgLabel(integration) {
447
+ // A set-aside package is labelled name@version, so its issue never reads as
448
+ // the winner's, even when both share a package name.
449
+ if (integration?.__providerConflict) {
450
+ return integration.version
451
+ ? `${integration.name}@${integration.version}`
452
+ : (integration.__spec ?? integration.name);
453
+ }
439
454
  return integration?.name ?? integration?.__spec ?? '(integration)';
440
455
  }
441
456
 
@@ -463,8 +478,8 @@ export class Project {
463
478
  */
464
479
  async #collectIssues(integration) {
465
480
  const pkg = this.#pkgLabel(integration);
466
- if (this.#visitedIssues.has(pkg)) return;
467
- this.#visitedIssues.add(pkg);
481
+ if (this.#visitedIssues.has(integration)) return;
482
+ this.#visitedIssues.add(integration);
468
483
  // A manifest that failed to load (throwing import / invalid shape) is
469
484
  // recorded as a marker by loadIntegrations — surface it as an issue and
470
485
  // skip validation (there's no manifest to validate).
@@ -0,0 +1,69 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * Every `*.doc.mjs` under `root`, relative and sorted.
6
+ * @param {string} [root]
7
+ * @returns {string[]}
8
+ */
9
+ export function discoverAuthoringSelfDocSources(root?: string): string[];
10
+ /**
11
+ * Import each self-doc. One that fails is reported, never thrown, so one bad
12
+ * file cannot take the rest of the topic down with it.
13
+ * @param {string[]} [sources]
14
+ * @param {string} [root]
15
+ * @returns {Promise<{loaded: {source: string, doc: any}[], failed: {source: string, error: string}[]}>}
16
+ */
17
+ export function loadAuthoringSelfDocs(sources?: string[], root?: string): Promise<{
18
+ loaded: {
19
+ source: string;
20
+ doc: any;
21
+ }[];
22
+ failed: {
23
+ source: string;
24
+ error: string;
25
+ }[];
26
+ }>;
27
+ /**
28
+ * The `authoring` topic: one section per self-doc, in the order given.
29
+ * @param {any[]} docs
30
+ * @returns {import('../../authoring/doctypes/reference/type').ReferenceDoc}
31
+ */
32
+ export function buildAuthoringReferenceDoc(docs: any[]): import("../../authoring/doctypes/reference/type").ReferenceDoc;
33
+ /**
34
+ * The `authoring` topic from every self-doc that loads. One that fails is left
35
+ * out here and reported by {@link auditAuthoringSelfDocs}.
36
+ * @returns {Promise<import('../../authoring/doctypes/reference/type').ReferenceDoc>}
37
+ */
38
+ export function buildAuthoringTopic(): Promise<import("../../authoring/doctypes/reference/type").ReferenceDoc>;
39
+ /**
40
+ * What stands between a self-doc and a reader of `astryx docs authoring`.
41
+ * @param {{root?: string, sources?: string[], budget?: number}} [options]
42
+ * @returns {Promise<{
43
+ * sections: number,
44
+ * unreachable: string[],
45
+ * failed: {source: string, error: string}[],
46
+ * oversized: {key: string, title: string, bytes: number}[],
47
+ * }>}
48
+ */
49
+ export function auditAuthoringSelfDocs({ root, sources, budget, }?: {
50
+ root?: string;
51
+ sources?: string[];
52
+ budget?: number;
53
+ }): Promise<{
54
+ sections: number;
55
+ unreachable: string[];
56
+ failed: {
57
+ source: string;
58
+ error: string;
59
+ }[];
60
+ oversized: {
61
+ key: string;
62
+ title: string;
63
+ bytes: number;
64
+ }[];
65
+ }>;
66
+ /** The directory the self-docs live under. */
67
+ export const AUTHORING_ROOT: string;
68
+ /** Every authoring self-doc, relative to {@link AUTHORING_ROOT}, in reading order. */
69
+ export const AUTHORING_SELF_DOCS: string[];
@@ -0,0 +1,214 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The authoring self-docs, and the `authoring` topic built from them.
5
+ *
6
+ * @input The SchemaDoc each authoring module colocates as `*.doc.mjs` under
7
+ * packages/cli/authoring.
8
+ * @output {@link AUTHORING_SELF_DOCS} (every self-doc, in reading order), the
9
+ * `authoring` reference topic with one section per self-doc, and an audit
10
+ * naming any self-doc the topic cannot reach, any that fails to load, and any
11
+ * section over the docs output budget.
12
+ * @position Read by assets/docs/authoring.doc.mjs (the topic) and by Doctor
13
+ * (the audit). Lives in foundation because authoring/ holds only contracts.
14
+ * A new `*.doc.mjs` under authoring/ must be added to the list, or Doctor and
15
+ * the self-doc tests fail.
16
+ */
17
+
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+ import {pathToFileURL} from 'node:url';
21
+ import {CLI_ROOT} from '../fs/paths.mjs';
22
+ import {
23
+ DOC_OUTPUT_BUDGET_BYTES,
24
+ oversizedDocSections,
25
+ } from './docs-output-budget.mjs';
26
+
27
+ /** The directory the self-docs live under. */
28
+ export const AUTHORING_ROOT = path.join(CLI_ROOT, 'authoring');
29
+
30
+ /** Every authoring self-doc, relative to {@link AUTHORING_ROOT}, in reading order. */
31
+ export const AUTHORING_SELF_DOCS = [
32
+ 'integration/integration.doc.mjs',
33
+ 'config/config.doc.mjs',
34
+ 'codemod/codemod.doc.mjs',
35
+ 'identity/identity.doc.mjs',
36
+ 'doctypes/base/graph-fields.doc.mjs',
37
+ 'doctypes/component/component.doc.mjs',
38
+ 'doctypes/hook/hook.doc.mjs',
39
+ 'doctypes/function/function.doc.mjs',
40
+ 'doctypes/command/command.doc.mjs',
41
+ 'doctypes/enum/enum.doc.mjs',
42
+ 'doctypes/namespace/namespace.doc.mjs',
43
+ 'doctypes/reference/reference.doc.mjs',
44
+ 'doctypes/schema/schema.doc.mjs',
45
+ 'doctypes/template/template.doc.mjs',
46
+ ];
47
+
48
+ /** Blocks a self-doc note may carry that a topic section can render. */
49
+ const TOPIC_BLOCKS = new Set(['prose', 'list', 'code', 'heading', 'table']);
50
+
51
+ /**
52
+ * Every `*.doc.mjs` under `root`, relative and sorted.
53
+ * @param {string} [root]
54
+ * @returns {string[]}
55
+ */
56
+ export function discoverAuthoringSelfDocSources(root = AUTHORING_ROOT) {
57
+ /** @type {string[]} */
58
+ const found = [];
59
+ /** @param {string} dir */
60
+ const walk = dir => {
61
+ for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
62
+ if (entry.name === 'node_modules' || entry.name.startsWith('__')) continue;
63
+ const full = path.join(dir, entry.name);
64
+ if (entry.isDirectory()) walk(full);
65
+ else if (entry.name.endsWith('.doc.mjs')) {
66
+ found.push(path.relative(root, full).split(path.sep).join('/'));
67
+ }
68
+ }
69
+ };
70
+ walk(root);
71
+ return found.sort();
72
+ }
73
+
74
+ /**
75
+ * Import each self-doc. One that fails is reported, never thrown, so one bad
76
+ * file cannot take the rest of the topic down with it.
77
+ * @param {string[]} [sources]
78
+ * @param {string} [root]
79
+ * @returns {Promise<{loaded: {source: string, doc: any}[], failed: {source: string, error: string}[]}>}
80
+ */
81
+ export async function loadAuthoringSelfDocs(
82
+ sources = AUTHORING_SELF_DOCS,
83
+ root = AUTHORING_ROOT,
84
+ ) {
85
+ const loaded = [];
86
+ const failed = [];
87
+ for (const source of sources) {
88
+ try {
89
+ const mod = await import(pathToFileURL(path.join(root, source)).href);
90
+ const doc = mod.doc ?? mod.docs ?? mod.default;
91
+ if (typeof doc?.name !== 'string' || typeof doc?.description !== 'string') {
92
+ throw new Error('exports no doc with a name and a description');
93
+ }
94
+ loaded.push({source, doc});
95
+ } catch (error) {
96
+ failed.push({
97
+ source,
98
+ error: error instanceof Error ? error.message : String(error),
99
+ });
100
+ }
101
+ }
102
+ return {loaded, failed};
103
+ }
104
+
105
+ /**
106
+ * @param {any[]} fields
107
+ * @returns {string[][]}
108
+ */
109
+ function fieldRows(fields) {
110
+ return fields.flatMap(field => [
111
+ [
112
+ String(field.name),
113
+ String(field.type ?? ''),
114
+ field.required ? 'yes' : 'no',
115
+ [
116
+ field.description,
117
+ field.default != null ? `Default: ${field.default}.` : null,
118
+ field.example != null ? `Example: ${field.example}.` : null,
119
+ ]
120
+ .filter(Boolean)
121
+ .join(' '),
122
+ ],
123
+ ...fieldRows(field.fields ?? []),
124
+ ]);
125
+ }
126
+
127
+ /**
128
+ * One self-doc as a topic section, keyed by the doc's own name.
129
+ * @param {any} doc
130
+ * @returns {import('../../authoring/doctypes/reference/type').ReferenceSection}
131
+ */
132
+ function selfDocSection(doc) {
133
+ /** @type {any[]} */
134
+ const content = [{type: 'prose', text: doc.description}];
135
+ if (doc.appliesTo) {
136
+ content.push({type: 'prose', text: `Applies to: ${doc.appliesTo}`});
137
+ }
138
+ const rows = fieldRows(doc.fields ?? []);
139
+ if (rows.length > 0) {
140
+ content.push({
141
+ type: 'table',
142
+ headers: ['Field', 'Type', 'Required', 'Description'],
143
+ rows,
144
+ });
145
+ }
146
+ for (const example of doc.examples ?? []) {
147
+ if (typeof example?.code !== 'string' || example.code.trim() === '') continue;
148
+ content.push({
149
+ type: 'code',
150
+ lang: example.lang ?? 'js',
151
+ ...(example.label ? {label: example.label} : {}),
152
+ code: example.code,
153
+ });
154
+ }
155
+ for (const note of doc.notes ?? []) {
156
+ if (TOPIC_BLOCKS.has(note?.type)) content.push(note);
157
+ }
158
+ return {id: doc.name, title: doc.displayName ?? doc.name, content};
159
+ }
160
+
161
+ /**
162
+ * The `authoring` topic: one section per self-doc, in the order given.
163
+ * @param {any[]} docs
164
+ * @returns {import('../../authoring/doctypes/reference/type').ReferenceDoc}
165
+ */
166
+ export function buildAuthoringReferenceDoc(docs) {
167
+ return /** @type {any} */ ({
168
+ name: 'authoring',
169
+ title: 'Authoring Reference',
170
+ category: 'guide',
171
+ description:
172
+ 'Every file an integration author writes, field by field: the integration manifest, astryx.config, codemods, identity, and each doc type.',
173
+ sections: docs.map(selfDocSection),
174
+ });
175
+ }
176
+
177
+ /**
178
+ * The `authoring` topic from every self-doc that loads. One that fails is left
179
+ * out here and reported by {@link auditAuthoringSelfDocs}.
180
+ * @returns {Promise<import('../../authoring/doctypes/reference/type').ReferenceDoc>}
181
+ */
182
+ export async function buildAuthoringTopic() {
183
+ const {loaded} = await loadAuthoringSelfDocs();
184
+ return buildAuthoringReferenceDoc(loaded.map(entry => entry.doc));
185
+ }
186
+
187
+ /**
188
+ * What stands between a self-doc and a reader of `astryx docs authoring`.
189
+ * @param {{root?: string, sources?: string[], budget?: number}} [options]
190
+ * @returns {Promise<{
191
+ * sections: number,
192
+ * unreachable: string[],
193
+ * failed: {source: string, error: string}[],
194
+ * oversized: {key: string, title: string, bytes: number}[],
195
+ * }>}
196
+ */
197
+ export async function auditAuthoringSelfDocs({
198
+ root = AUTHORING_ROOT,
199
+ sources = AUTHORING_SELF_DOCS,
200
+ budget = DOC_OUTPUT_BUDGET_BYTES,
201
+ } = {}) {
202
+ const listed = new Set(sources);
203
+ const unreachable = discoverAuthoringSelfDocSources(root).filter(
204
+ source => !listed.has(source),
205
+ );
206
+ const {loaded, failed} = await loadAuthoringSelfDocs(sources, root);
207
+ const topic = buildAuthoringReferenceDoc(loaded.map(entry => entry.doc));
208
+ return {
209
+ sections: topic.sections.length,
210
+ unreachable,
211
+ failed,
212
+ oversized: oversizedDocSections(topic.sections, budget),
213
+ };
214
+ }