@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
@@ -31,7 +31,9 @@ function topic(fields) {
31
31
  name: 'deploying',
32
32
  title: 'Deploying',
33
33
  description: 'How to ship it.',
34
- sections: [{title: 'Overview', content: [{type: 'prose', text: 'Ship it.'}]}],
34
+ sections: [
35
+ {title: 'Overview', content: [{type: 'prose', text: 'Ship it.'}]},
36
+ ],
35
37
  ...fields,
36
38
  };
37
39
  }
@@ -97,7 +99,9 @@ describe('discoverIntegrationDocs', () => {
97
99
  }),
98
100
  );
99
101
  expect(errors).toEqual([]);
100
- expect(records.map(r => [r.name, r.package, r.replaces, r.extendsTopic])).toEqual([
102
+ expect(
103
+ records.map(r => [r.name, r.package, r.replaces, r.extendsTopic]),
104
+ ).toEqual([
101
105
  ['deploying', '@acme/widgets', undefined, undefined],
102
106
  ['getting-started', '@acme/widgets', 'getting-started', undefined],
103
107
  ['theme-extra', '@acme/widgets', undefined, 'theme'],
@@ -126,7 +130,9 @@ describe('discoverIntegrationDocs', () => {
126
130
 
127
131
  it('reports a doc that exports nothing, rather than skipping it silently', async () => {
128
132
  const {records, errors} = await discoverIntegrationDocs(
129
- integration('@acme/empty', {'deploying.doc.mjs': 'export const nope = 1;\n'}),
133
+ integration('@acme/empty', {
134
+ 'deploying.doc.mjs': 'export const nope = 1;\n',
135
+ }),
130
136
  );
131
137
  expect(records).toEqual([]);
132
138
  expect(errors).toHaveLength(1);
@@ -153,6 +159,49 @@ describe('discoverIntegrationDocs', () => {
153
159
  expect(records).toEqual([]);
154
160
  expect(errors[0].message).toContain('declares both');
155
161
  });
162
+
163
+ it('rejects graph blocks at the legacy topic-reader boundary', async () => {
164
+ const {records, errors} = await discoverIntegrationDocs(
165
+ integration('@acme/graph-block', {
166
+ 'deploying.doc.mjs': topic({
167
+ sections: [
168
+ {
169
+ id: 'steps',
170
+ title: 'Steps',
171
+ content: [
172
+ {
173
+ type: 'workflow',
174
+ steps: [{title: 'Install', description: 'Run install.'}],
175
+ },
176
+ ],
177
+ },
178
+ ],
179
+ }),
180
+ }),
181
+ );
182
+ expect(records).toEqual([]);
183
+ expect(errors[0].message).toContain('requires the compiled graph renderer');
184
+ });
185
+
186
+ it('names a namespace doc instead of listing topic fields it lacks', async () => {
187
+ const {records, errors} = await discoverIntegrationDocs(
188
+ integration('@acme/namespace', {
189
+ 'guides.doc.mjs': {
190
+ type: 'namespace',
191
+ name: 'guides',
192
+ title: 'Guides',
193
+ summary: 'Every guide.',
194
+ slots: {guides: {title: 'Guides', accepts: {kinds: ['generic']}}},
195
+ },
196
+ }),
197
+ );
198
+ expect(records).toEqual([]);
199
+ expect(errors).toHaveLength(1);
200
+ expect(errors[0].message).toContain(
201
+ '"guides" is a namespace doc. Only the docs graph reads namespace docs',
202
+ );
203
+ expect(errors[0].message).not.toContain('sections:');
204
+ });
156
205
  });
157
206
 
158
207
  describe('problemsInTopic', () => {
@@ -170,7 +219,11 @@ describe('problemsInTopic', () => {
170
219
  // A misspelled required field: the block renders nothing at all.
171
220
  expect(
172
221
  problemsInTopic(
173
- topic({sections: [{title: 'Overview', content: [{type: 'prose', txt: 'oops'}]}]}),
222
+ topic({
223
+ sections: [
224
+ {title: 'Overview', content: [{type: 'prose', txt: 'oops'}]},
225
+ ],
226
+ }),
174
227
  ),
175
228
  ).toEqual([
176
229
  'sections[0].content[0].text: required for a prose block',
@@ -180,7 +233,12 @@ describe('problemsInTopic', () => {
180
233
  expect(
181
234
  problemsInTopic(
182
235
  topic({
183
- sections: [{title: 'Overview', content: [{type: 'heading', level: 2, text: 'x'}]}],
236
+ sections: [
237
+ {
238
+ title: 'Overview',
239
+ content: [{type: 'heading', level: 2, text: 'x'}],
240
+ },
241
+ ],
184
242
  }),
185
243
  ),
186
244
  ).toContain('sections[0].content[0].level: 2 is not one of 3, 4, 5, 6');
@@ -196,11 +254,27 @@ describe('problemsInTopic', () => {
196
254
  ],
197
255
  }),
198
256
  ),
199
- ).toContain('sections[0].content[0].rows[0]: has 1 cells but the table has 2 headers');
257
+ ).toContain(
258
+ 'sections[0].content[0].rows[0]: has 1 cells but the table has 2 headers',
259
+ );
260
+ });
261
+
262
+ it('rejects graph metadata at the legacy topic-reader boundary', () => {
263
+ for (const fields of [
264
+ {placement: {parent: 'namespace:cli'}},
265
+ {aliases: ['setup']},
266
+ {audience: 'internal'},
267
+ ]) {
268
+ expect(problemsInTopic(topic(fields)).join('\n')).toContain(
269
+ 'requires the compiled graph reader',
270
+ );
271
+ }
200
272
  });
201
273
 
202
274
  it('rejects a name that is not URL-safe', () => {
203
- expect(problemsInTopic(topic({name: 'not a topic'})).join('\n')).toContain('URL-safe');
275
+ expect(problemsInTopic(topic({name: 'not a topic'})).join('\n')).toContain(
276
+ 'URL-safe',
277
+ );
204
278
  });
205
279
  });
206
280
 
@@ -228,11 +302,23 @@ describe('DocsCatalog', () => {
228
302
  expect(catalog.resolve('deploying').package).toBe('@acme/widgets');
229
303
  });
230
304
 
305
+ it('treats topic identities case-insensitively without losing display case', () => {
306
+ const catalog = DocsCatalog.fromBuiltins();
307
+ expect(catalog.add(record({name: 'Deploying'}))).toBeNull();
308
+ expect(catalog.resolve('deploying').name).toBe('Deploying');
309
+ expect(catalog.resolve('DEPLOYING').name).toBe('Deploying');
310
+
311
+ const issue = catalog.add(
312
+ record({name: 'deploying', package: '@acme/duplicate'}),
313
+ );
314
+ expect(issue).toMatchObject({code: 'invalid_doc', severity: 'error'});
315
+ });
316
+
231
317
  it('refuses to shadow an existing topic by name alone', () => {
232
318
  const catalog = DocsCatalog.fromBuiltins({tokens: '/cli/tokens.doc.mjs'});
233
319
  const issue = catalog.add(record({name: 'tokens'}));
234
320
  expect(issue).toMatchObject({code: 'invalid_doc', severity: 'error'});
235
- expect(issue.message).toContain("already provided by @astryxdesign/cli");
321
+ expect(issue.message).toContain('already provided by @astryxdesign/cli');
236
322
  expect(issue.message).toContain("replaces: 'tokens'");
237
323
  // The built-in topic is untouched.
238
324
  expect(catalog.resolve('tokens').package).toBe(BUILTIN_DOCS_PACKAGE);
@@ -244,7 +330,9 @@ describe('DocsCatalog', () => {
244
330
  tokens: '/cli/tokens.doc.mjs',
245
331
  });
246
332
  expect(
247
- catalog.add(record({name: 'getting-started', replaces: 'getting-started'})),
333
+ catalog.add(
334
+ record({name: 'getting-started', replaces: 'getting-started'}),
335
+ ),
248
336
  ).toBeNull();
249
337
  expect(catalog.names()).toEqual(['getting-started', 'tokens']);
250
338
  const entry = catalog.resolve('getting-started');
@@ -276,12 +364,16 @@ describe('DocsCatalog', () => {
276
364
 
277
365
  it('warns, and lets the later package win, when two replace one topic', () => {
278
366
  const catalog = DocsCatalog.fromBuiltins({tokens: '/cli/tokens.doc.mjs'});
279
- expect(catalog.add(record({name: 'tokens', replaces: 'tokens'}))).toBeNull();
367
+ expect(
368
+ catalog.add(record({name: 'tokens', replaces: 'tokens'})),
369
+ ).toBeNull();
280
370
  const issue = catalog.add(
281
371
  record({name: 'tokens', package: '@acme/later', replaces: 'tokens'}),
282
372
  );
283
373
  expect(issue).toMatchObject({code: 'duplicate_doc', severity: 'warning'});
284
- expect(issue.message).toContain('@acme/later is configured later, so it wins');
374
+ expect(issue.message).toContain(
375
+ '@acme/later is configured later, so it wins',
376
+ );
285
377
  expect(catalog.resolve('tokens').package).toBe('@acme/later');
286
378
  });
287
379
 
@@ -327,15 +419,189 @@ describe('mergeTopic', () => {
327
419
  {title: 'Internal', content: [{type: 'prose', text: 'the meta way'}]},
328
420
  ],
329
421
  });
330
- expect(merged.sections.map(s => s.title)).toEqual(['Install', 'Tokens', 'Internal']);
422
+ expect(merged.sections.map(s => s.title)).toEqual([
423
+ 'Install',
424
+ 'Tokens',
425
+ 'Internal',
426
+ ]);
331
427
  expect(merged.sections[0].content[0].text).toBe('yarn add');
332
428
  expect(merged.sections[1].content[0].text).toBe('use tokens');
333
429
  // The base is untouched.
334
430
  expect(base.sections[0].content[0].text).toBe('npm i');
335
431
  });
336
432
 
433
+ it('replaces a section by stable ID even when its title changes', () => {
434
+ const merged = mergeTopic(
435
+ {
436
+ ...base,
437
+ sections: [
438
+ {id: 'install', title: 'Install', content: []},
439
+ {id: 'tokens', title: 'Tokens', content: []},
440
+ ],
441
+ },
442
+ {
443
+ sections: [{id: 'install', title: 'Setup', content: []}],
444
+ },
445
+ );
446
+ expect(merged.sections.map(section => [section.id, section.title])).toEqual(
447
+ [
448
+ ['install', 'Setup'],
449
+ ['tokens', 'Tokens'],
450
+ ],
451
+ );
452
+ });
453
+
454
+ it('migrates a legacy section to a stable ID without duplicating it', () => {
455
+ const merged = mergeTopic(base, {
456
+ sections: [
457
+ {
458
+ id: 'install',
459
+ title: 'Install',
460
+ content: [{type: 'prose', text: 'yarn add'}],
461
+ },
462
+ ],
463
+ });
464
+ expect(
465
+ merged.sections.map(section => [section.id ?? null, section.title]),
466
+ ).toEqual([
467
+ ['install', 'Install'],
468
+ [null, 'Tokens'],
469
+ ]);
470
+ expect(merged.sections[0].content[0].text).toBe('yarn add');
471
+ });
472
+
473
+ it('lets a legacy extension replace a section that has since gained an ID', () => {
474
+ const merged = mergeTopic(
475
+ {
476
+ ...base,
477
+ sections: [
478
+ {id: 'setup-steps', title: 'Install', content: []},
479
+ {title: 'Tokens', content: []},
480
+ ],
481
+ },
482
+ {
483
+ sections: [
484
+ {title: 'Install', content: [{type: 'prose', text: 'yarn add'}]},
485
+ ],
486
+ },
487
+ );
488
+ expect(
489
+ merged.sections.map(section => [section.id ?? null, section.title]),
490
+ ).toEqual([
491
+ ['setup-steps', 'Install'],
492
+ [null, 'Tokens'],
493
+ ]);
494
+ expect(merged.sections[0].content[0].text).toBe('yarn add');
495
+ });
496
+
497
+ it('keeps two different authored IDs distinct under one title', () => {
498
+ const merged = mergeTopic(
499
+ {...base, sections: [{id: 'install', title: 'Install', content: []}]},
500
+ {sections: [{id: 'install-yarn', title: 'Install', content: []}]},
501
+ );
502
+ expect(merged.sections.map(section => section.id)).toEqual([
503
+ 'install',
504
+ 'install-yarn',
505
+ ]);
506
+ });
507
+
337
508
  it('takes the title and description only when the overlay states them', () => {
338
509
  expect(mergeTopic(base, {sections: []}).title).toBe('Theme');
339
- expect(mergeTopic(base, {title: 'Theming', sections: []}).title).toBe('Theming');
510
+ expect(mergeTopic(base, {title: 'Theming', sections: []}).title).toBe(
511
+ 'Theming',
512
+ );
513
+ });
514
+ });
515
+
516
+ describe('problemsInTopic section keys', () => {
517
+ /** @param {object[]} sections */
518
+ const doc = sections => ({
519
+ type: 'generic',
520
+ name: 'keys',
521
+ title: 'Keys',
522
+ description: 'Section keys.',
523
+ sections: sections.map(s => ({
524
+ content: [{type: 'prose', text: 'x'}],
525
+ ...s,
526
+ })),
527
+ });
528
+
529
+ it('rejects two sections that derive the same key', () => {
530
+ expect(
531
+ problemsInTopic(doc([{title: 'Quick Start'}, {title: 'Quick-start'}])),
532
+ ).toEqual([
533
+ expect.stringContaining('"quick-start" is already used by sections[0]'),
534
+ ]);
535
+ });
536
+
537
+ it('rejects an unsafe id', () => {
538
+ expect(
539
+ problemsInTopic(doc([{id: 'Quick Start', title: 'Quick Start'}])),
540
+ ).toEqual([expect.stringContaining('is not a stable key')]);
541
+ });
542
+
543
+ it('rejects a title no key derives from, unless it has an id', () => {
544
+ expect(problemsInTopic(doc([{title: '亮/暗模式'}]))).toEqual([
545
+ expect.stringContaining('Give the section an id'),
546
+ ]);
547
+ expect(
548
+ problemsInTopic(doc([{id: 'light-dark', title: '亮/暗模式'}])),
549
+ ).toEqual([]);
550
+ });
551
+ });
552
+
553
+ describe('mergeTopic by section key', () => {
554
+ const base = {
555
+ title: 'Theme',
556
+ sections: [
557
+ {title: 'Quick Start', content: [{type: 'prose', text: 'base'}]},
558
+ {title: 'Light/Dark Mode', content: [{type: 'prose', text: 'base'}]},
559
+ ],
560
+ };
561
+ const titles = doc =>
562
+ doc.sections.map(s => [s.id ?? null, s.title, s.content[0].text]);
563
+
564
+ it('replaces the section whose derived key an extension id names', () => {
565
+ const merged = mergeTopic(base, {
566
+ sections: [
567
+ {
568
+ id: 'quick-start',
569
+ title: 'Quick Start with Acme',
570
+ content: [{type: 'prose', text: 'acme'}],
571
+ },
572
+ ],
573
+ });
574
+ expect(titles(merged)).toEqual([
575
+ ['quick-start', 'Quick Start with Acme', 'acme'],
576
+ [null, 'Light/Dark Mode', 'base'],
577
+ ]);
578
+ });
579
+
580
+ it('replaces the section a title variant derives the same key as', () => {
581
+ const merged = mergeTopic(base, {
582
+ sections: [
583
+ {title: 'Light-Dark Mode', content: [{type: 'prose', text: 'acme'}]},
584
+ ],
585
+ });
586
+ expect(titles(merged)).toEqual([
587
+ [null, 'Quick Start', 'base'],
588
+ [null, 'Light-Dark Mode', 'acme'],
589
+ ]);
590
+ });
591
+
592
+ it('prefers the key over a legacy title for an extension with an id', () => {
593
+ const merged = mergeTopic(base, {
594
+ sections: [
595
+ {
596
+ id: 'light-dark-mode',
597
+ title: 'Quick Start',
598
+ content: [{type: 'prose', text: 'acme'}],
599
+ },
600
+ ],
601
+ });
602
+ expect(titles(merged)).toEqual([
603
+ [null, 'Quick Start', 'base'],
604
+ ['light-dark-mode', 'Quick Start', 'acme'],
605
+ ]);
340
606
  });
341
607
  });
@@ -0,0 +1,28 @@
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
+ * A payload's size as `--json` prints it: the envelope data, two-space
6
+ * indented, in UTF-8 bytes.
7
+ * @param {unknown} payload
8
+ * @returns {number}
9
+ */
10
+ export function docPayloadBytes(payload: unknown): number;
11
+ /**
12
+ * @param {import('../../api/docs/docs.type.mjs').DocsIndex} index
13
+ * @returns {number}
14
+ */
15
+ export function docsIndexBytes(index: import("../../api/docs/docs.type.mjs").DocsIndex): number;
16
+ /**
17
+ * The sections a single read would return more than `budget` bytes for.
18
+ * @param {any[]} sections
19
+ * @param {number} [budget]
20
+ * @returns {{key: string, title: string, bytes: number}[]}
21
+ */
22
+ export function oversizedDocSections(sections: any[], budget?: number): {
23
+ key: string;
24
+ title: string;
25
+ bytes: number;
26
+ }[];
27
+ /** The most one index or one section read may return, in bytes. */
28
+ export const DOC_OUTPUT_BUDGET_BYTES: number;
@@ -0,0 +1,50 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The most one progressive docs read may return.
5
+ *
6
+ * @input Docs payloads: one topic's section index, or one section.
7
+ * @output Their size in bytes as `--json` prints them, and the sections over
8
+ * the budget.
9
+ * @position Shared by Doctor's docs checks and the authoring self-doc audit, so
10
+ * both hold every read to the same limit.
11
+ */
12
+
13
+ import {sectionKey} from './docs-section-key.mjs';
14
+
15
+ /** The most one index or one section read may return, in bytes. */
16
+ export const DOC_OUTPUT_BUDGET_BYTES = 32 * 1024;
17
+
18
+ /**
19
+ * A payload's size as `--json` prints it: the envelope data, two-space
20
+ * indented, in UTF-8 bytes.
21
+ * @param {unknown} payload
22
+ * @returns {number}
23
+ */
24
+ export function docPayloadBytes(payload) {
25
+ return Buffer.byteLength(JSON.stringify(payload, null, 2), 'utf8');
26
+ }
27
+
28
+ /**
29
+ * @param {import('../../api/docs/docs.type.mjs').DocsIndex} index
30
+ * @returns {number}
31
+ */
32
+ export function docsIndexBytes(index) {
33
+ return docPayloadBytes({type: 'docs.index', data: index});
34
+ }
35
+
36
+ /**
37
+ * The sections a single read would return more than `budget` bytes for.
38
+ * @param {any[]} sections
39
+ * @param {number} [budget]
40
+ * @returns {{key: string, title: string, bytes: number}[]}
41
+ */
42
+ export function oversizedDocSections(sections, budget = DOC_OUTPUT_BUDGET_BYTES) {
43
+ return sections
44
+ .map(section => ({
45
+ key: sectionKey(section),
46
+ title: section.title,
47
+ bytes: docPayloadBytes({type: 'docs.detail.section', data: section}),
48
+ }))
49
+ .filter(entry => entry.bytes > budget);
50
+ }
@@ -0,0 +1,98 @@
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
+ * Record the authored title of a section whose visible title a translation
6
+ * overlay replaces.
7
+ * @template {object} T
8
+ * @param {T} section
9
+ * @param {string} title
10
+ * @returns {T}
11
+ */
12
+ export function withSourceTitle<T extends object>(section: T, title: string): T;
13
+ /**
14
+ * @param {any} section
15
+ * @returns {string}
16
+ */
17
+ export function sourceTitle(section: any): string;
18
+ /**
19
+ * The key a title derives: accents folded, `&` spelled out, and every other
20
+ * run of non-alphanumerics collapsed to one hyphen. Empty when the title has
21
+ * no Latin letters or digits to derive from.
22
+ * @param {unknown} title
23
+ * @returns {string}
24
+ */
25
+ export function sectionTitleKey(title: unknown): string;
26
+ /**
27
+ * The key a section is addressed by: its authored `id`, else the key its
28
+ * authored title derives.
29
+ * @param {any} section
30
+ * @returns {string}
31
+ */
32
+ export function sectionKey(section: any): string;
33
+ /**
34
+ * Problems with the keys of a topic's sections: an authored id that is not a
35
+ * stable key, a title no key derives from, and two sections sharing a key.
36
+ * Keys are never suffixed to make them unique, because readers link to them.
37
+ * @param {any[]} sections
38
+ * @returns {string[]}
39
+ */
40
+ export function sectionKeyProblems(sections: any[]): string[];
41
+ /**
42
+ * Stamp every section with the key it is addressed by. Runs only after
43
+ * extensions merge: a derived key must never take part in merge matching.
44
+ * @template {{sections: any[]}} T
45
+ * @param {T} doc
46
+ * @returns {T}
47
+ */
48
+ export function withSectionKeys<T extends {
49
+ sections: any[];
50
+ }>(doc: T): T;
51
+ /**
52
+ * Find the section a reader asked for: by key, then by exact title (or the
53
+ * key the query derives), then by a title that contains the query. More than
54
+ * one match is refused rather than guessed.
55
+ * @param {any[]} sections
56
+ * @param {string} query
57
+ * @returns {{section: any | null, candidates: any[]}} `candidates` lists the
58
+ * matches when the query is ambiguous, and is empty when nothing matches
59
+ */
60
+ export function findDocSection(sections: any[], query: string): {
61
+ section: any | null;
62
+ candidates: any[];
63
+ };
64
+ /**
65
+ * One line that says what a section holds: its first prose or list text,
66
+ * whitespace collapsed, cut at a word boundary.
67
+ * @param {any} section
68
+ * @param {number} [max]
69
+ * @returns {string}
70
+ */
71
+ export function sectionSummary(section: any, max?: number): string;
72
+ /**
73
+ * The index a topic-only read returns: what the topic is, and one entry per
74
+ * section with the key to read it by.
75
+ * @param {{name: string, title: string, description: string, sections: any[]}} doc
76
+ * @returns {import('../../api/docs/docs.type.mjs').DocsIndex}
77
+ */
78
+ export function buildDocsIndexData(doc: {
79
+ name: string;
80
+ title: string;
81
+ description: string;
82
+ sections: any[];
83
+ }): import("../../api/docs/docs.type.mjs").DocsIndex;
84
+ /**
85
+ * @file Stable section keys, section lookup, and the topic index.
86
+ *
87
+ * @input Reference-doc sections, each with an optional authored `id`.
88
+ * @output The key a section is addressed by (its `id`, else a kebab-case key
89
+ * derived from its authored title), lookup by key or title, and the compact
90
+ * index a topic-only docs read returns.
91
+ * @position Shared by docs discovery (which rejects colliding keys before a
92
+ * reader sees them), the docs leaves (index, section, detail), and Doctor
93
+ * (output budgets). Imports nothing from discovery, so both can use it.
94
+ */
95
+ /** A stable key: lowercase letters and digits, joined by single hyphens. */
96
+ export const SECTION_KEY_RE: RegExp;
97
+ /** The longest summary an index entry carries, in characters. */
98
+ export const SECTION_SUMMARY_MAX: 240;