@astryxdesign/cli 0.1.6-canary.e8d4c3f → 0.1.6-canary.eb23da7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.1.6-canary.e8d4c3f",
3
+ "version": "0.1.6-canary.eb23da7",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -79,10 +79,10 @@
79
79
  "zod": "^4.4.3"
80
80
  },
81
81
  "peerDependencies": {
82
- "@astryxdesign/charts": "0.1.6-canary.e8d4c3f",
83
- "@astryxdesign/core": "0.1.6-canary.e8d4c3f",
84
- "@astryxdesign/lab": "0.1.6-canary.e8d4c3f",
85
- "@astryxdesign/theme-neutral": "0.1.6-canary.e8d4c3f",
82
+ "@astryxdesign/charts": "0.1.6-canary.eb23da7",
83
+ "@astryxdesign/core": "0.1.6-canary.eb23da7",
84
+ "@astryxdesign/lab": "0.1.6-canary.eb23da7",
85
+ "@astryxdesign/theme-neutral": "0.1.6-canary.eb23da7",
86
86
  "gpt-tokenizer": "^3.4.0"
87
87
  },
88
88
  "peerDependenciesMeta": {
@@ -100,10 +100,10 @@
100
100
  }
101
101
  },
102
102
  "devDependencies": {
103
- "@astryxdesign/charts": "0.1.6-canary.e8d4c3f",
104
- "@astryxdesign/core": "0.1.6-canary.e8d4c3f",
105
- "@astryxdesign/lab": "0.1.6-canary.e8d4c3f",
106
- "@astryxdesign/theme-neutral": "0.1.6-canary.e8d4c3f",
103
+ "@astryxdesign/charts": "0.1.6-canary.eb23da7",
104
+ "@astryxdesign/core": "0.1.6-canary.eb23da7",
105
+ "@astryxdesign/lab": "0.1.6-canary.eb23da7",
106
+ "@astryxdesign/theme-neutral": "0.1.6-canary.eb23da7",
107
107
  "gpt-tokenizer": "^3.4.0"
108
108
  },
109
109
  "scripts": {
@@ -8,7 +8,7 @@ import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import {createJiti} from 'jiti';
10
10
  import {loadModuleWithSchema} from '../lib/module-loader.mjs';
11
- import {TemplateEnvelopeSchema} from '../schemas/template-schema.mjs';
11
+ import {TemplateEnvelopeSchema} from '../template.mjs';
12
12
  import {CLI_ROOT, discoverExternalPackages} from '../utils/paths.mjs';
13
13
  import {
14
14
  assertWithin,
@@ -20,7 +20,6 @@ describe('registry', () => {
20
20
  '0.1.2',
21
21
  '0.1.3',
22
22
  '0.1.5',
23
- '0.1.7',
24
23
  ]);
25
24
  });
26
25
  });
@@ -21,7 +21,6 @@ const registry = new Map([
21
21
  ['0.1.2', () => import('./transforms/v0.1.2/index.mjs')],
22
22
  ['0.1.3', () => import('./transforms/v0.1.3/index.mjs')],
23
23
  ['0.1.5', () => import('./transforms/v0.1.5/index.mjs')],
24
- ['0.1.7', () => import('./transforms/v0.1.7/index.mjs')],
25
24
  ]);
26
25
 
27
26
  // Re-export from the shared utility so registry callers and other consumers
package/src/config.mjs CHANGED
@@ -1,9 +1,18 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * Re-export of the config-authoring helper, which now lives in
5
- * `@astryxdesign/core/config` so an app's config file gets type feedback
6
- * without depending on the CLI. Kept here so existing
7
- * `@astryxdesign/cli/config` imports continue to work unchanged.
4
+ * Type-preserving helper for the Astryx config file.
5
+ *
6
+ * This is an intentionally tiny runtime identity function: it returns its
7
+ * argument unchanged. Its value is the exported TypeScript surface from
8
+ * `@astryxdesign/cli/config`, so config files get editor/type feedback without
9
+ * coupling to CLI internals. Validation is NOT performed here — it happens at
10
+ * the load boundary (see `loadModuleWithSchema` + `AstryxConfigSchema`).
11
+ *
12
+ * @template {import('./types/config').AstryxConfig} T
13
+ * @param {T} config
14
+ * @returns {T}
8
15
  */
9
- export {createConfig} from '@astryxdesign/core/config';
16
+ export function createConfig(config) {
17
+ return config;
18
+ }
package/src/doc.mjs CHANGED
@@ -1,27 +1,271 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file Doc-authoring API (public `@astryxdesign/cli/doc`).
4
+ * @file Public doc-authoring API.
5
5
  *
6
- * The `createComponentDoc`/`createFunctionDoc`/`createDoc` authoring helpers now
7
- * live in `@astryxdesign/core/authoring` and are re-exported here so existing
8
- * `@astryxdesign/cli/doc` imports keep working. The Zod load-boundary schemas
9
- * live in `./schemas/doc-schema.mjs` (core-free) and are re-exported here for
10
- * back-compat; internal hot-path code imports them from the schema module
11
- * directly so it never depends on core's built `dist/`.
6
+ * A doc describes a documentable unit of the design system so the CLI and docs
7
+ * surfaces can list, search, and render it. There are three kinds, each a tiny
8
+ * runtime identity helper that stamps a `type` discriminant (mirroring how
9
+ * `createPageTemplate`/`createBlockTemplate` stamp `type`):
10
+ *
11
+ * - `createComponentDoc` -> `type: 'component'` a component (or a family).
12
+ * - `createFunctionDoc` -> `type: 'function'` — any function, including
13
+ * hooks: an "inputs + outputs" surface (`params` + `returns`).
14
+ * - `createDoc` -> `type: 'generic'` — reference/topic docs.
15
+ *
16
+ * Like `createConfig`/`createIntegration`/`createBlockTemplate`, these factories
17
+ * do NOT validate. Validation happens at the LOAD boundary, where doc discovery
18
+ * runs the loaded value through {@link ComponentDocSchema}. That schema handles
19
+ * BOTH the new stamped formats and the legacy loose `export const docs = {...}`
20
+ * shape, so existing docs keep loading unchanged (a later PR migrates them).
21
+ */
22
+
23
+ import {z} from 'zod';
24
+
25
+ /**
26
+ * A single documented prop. Mirrors `PropDoc` from
27
+ * `@astryxdesign/core/docs-types`. Only `name`, `type`, and `description` are
28
+ * required; everything else is optional. Extra fields (e.g. `slotElements`)
29
+ * pass through so the schema does not have to track every evolution of the
30
+ * rich playground/element surface.
31
+ */
32
+ const PropSchema = z
33
+ .object({
34
+ name: z.string().min(1, 'prop name is required'),
35
+ type: z.string().min(1, 'prop type is required'),
36
+ description: z.string(),
37
+ default: z.string().optional(),
38
+ required: z.boolean().optional(),
39
+ })
40
+ .passthrough();
41
+
42
+ /** A single documented function/hook parameter (mirrors `HookParamDoc`). */
43
+ const ParamSchema = z
44
+ .object({
45
+ name: z.string().min(1, 'param name is required'),
46
+ type: z.string().min(1, 'param type is required'),
47
+ description: z.string(),
48
+ default: z.string().optional(),
49
+ required: z.boolean().optional(),
50
+ })
51
+ .passthrough();
52
+
53
+ /** A single documented function/hook return field (mirrors `HookReturnDoc`). */
54
+ const ReturnSchema = z
55
+ .object({
56
+ name: z.string().min(1, 'return name is required'),
57
+ type: z.string().min(1, 'return type is required'),
58
+ description: z.string(),
59
+ })
60
+ .passthrough();
61
+
62
+ /**
63
+ * Fields shared by every doc kind. Deliberately permissive on the rich blobs
64
+ * (usage/theming/playground) — those are large and still evolving, so they are
65
+ * `z.unknown()` passthrough rather than enumerated.
66
+ *
67
+ * `group` vs `parent` are distinct concepts and intentionally separate fields:
68
+ * - `group` is a FLAT sidebar bucket label. Many docs can share a group; it
69
+ * is not an inheritance key.
70
+ * - `parent` is a DIRECTED inheritance/composition pointer — a doc naming the
71
+ * doc it belongs to (e.g. a sub-component naming its parent component). The
72
+ * legacy `subComponentOf` field is a synonym; both map to the same concept.
73
+ *
74
+ * `relatedDocs` is a single flat curated cross-reference list. It replaces the
75
+ * legacy split `relatedComponents` + `relatedHooks`; a downstream reader can
76
+ * merge the legacy pair into it.
77
+ */
78
+ const BaseDocFields = {
79
+ /** Name of the documented unit, without any prefix. Required. */
80
+ name: z.string().min(1, 'name is required'),
81
+ /** Human-readable display name for gallery/sidebar. */
82
+ displayName: z.string().optional(),
83
+ /** One-line/short description. */
84
+ description: z.string().optional(),
85
+ /** Usage documentation (description, best practices, anatomy, slotElements). */
86
+ usage: z.unknown().optional(),
87
+ /** Flat sidebar grouping label (not an inheritance key). */
88
+ group: z.string().optional(),
89
+ /** Overview-page functional category. */
90
+ category: z.string().optional(),
91
+ /** CLI fuzzy-search keywords. */
92
+ keywords: z.array(z.string()).optional(),
93
+ /** Directed inheritance/composition pointer to the doc this belongs to. */
94
+ parent: z.string().optional(),
95
+ /** Flat curated cross-reference list of related doc names. */
96
+ relatedDocs: z.array(z.string()).optional(),
97
+ /** Hide the whole doc from human-facing UI (stays importable/discoverable). */
98
+ hidden: z.boolean().optional(),
99
+ /** Exclude from the categorized overview page (kept in sidebar/CLI). */
100
+ isHiddenFromOverview: z.boolean().optional(),
101
+ };
102
+
103
+ /**
104
+ * New-format schema for a stamped component doc (`type: 'component'`). The
105
+ * top-level component keys are enumerated, but nested blobs stay loose so real
106
+ * docs are not rejected. `.passthrough()` keeps unknown top-level fields
107
+ * (translations, element descriptors, ...) flowing through.
108
+ */
109
+ export const ComponentDocKindSchema = z
110
+ .object({
111
+ ...BaseDocFields,
112
+ type: z.literal('component'),
113
+ props: z.array(PropSchema),
114
+ theming: z.unknown().optional(),
115
+ playground: z.unknown().optional(),
116
+ examples: z.array(z.unknown()).optional(),
117
+ })
118
+ .passthrough();
119
+
120
+ /**
121
+ * New-format schema for a stamped function doc (`type: 'function'`). Covers any
122
+ * function, including hooks: an inputs (`params`) + outputs (`returns`) surface.
12
123
  */
124
+ export const FunctionDocKindSchema = z
125
+ .object({
126
+ ...BaseDocFields,
127
+ type: z.literal('function'),
128
+ params: z.array(ParamSchema),
129
+ returns: z.array(ReturnSchema),
130
+ })
131
+ .passthrough();
13
132
 
14
- export {
15
- createComponentDoc,
16
- createFunctionDoc,
17
- createDoc,
18
- } from '@astryxdesign/core/authoring';
133
+ /**
134
+ * New-format schema for a stamped generic doc (`type: 'generic'`) — reference
135
+ * and topic docs. Just the shared base plus the discriminant.
136
+ */
137
+ export const GenericDocKindSchema = z
138
+ .object({
139
+ ...BaseDocFields,
140
+ type: z.literal('generic'),
141
+ })
142
+ .passthrough();
19
143
 
20
- export {
144
+ /**
145
+ * The stamped-doc contract: one of the three new per-kind schemas, discriminated
146
+ * by the injected `type`. Used when a loaded doc carries a `type` field.
147
+ */
148
+ export const StampedDocSchema = z.discriminatedUnion('type', [
21
149
  ComponentDocKindSchema,
22
150
  FunctionDocKindSchema,
23
151
  GenericDocKindSchema,
24
- StampedDocSchema,
25
- LegacyDocSchema,
26
- ComponentDocSchema,
27
- } from './schemas/doc-schema.mjs';
152
+ ]);
153
+
154
+ // ── Legacy loose format (unchanged, kept for back-compat) ─────────────
155
+ //
156
+ // The pre-factory docs are authored as a loose, untyped object with no `type`
157
+ // discriminant and validated by shape. Enumerated top-level fields observed
158
+ // across the existing loose docs:
159
+ // name, displayName, description, group, category, keywords,
160
+ // isHiddenFromOverview, hidden, hiddenComponents, usage, props, components,
161
+ // subComponentOf, playground, theming, examples, params, returns,
162
+ // relatedComponents, relatedHooks, relatedDocs, parent, importPath.
163
+
164
+ const LegacyBaseDocSchema = z.object({
165
+ name: z.string().min(1, 'name is required'),
166
+ displayName: z.string().optional(),
167
+ description: z.string().optional(),
168
+ group: z.string().optional(),
169
+ category: z.string().optional(),
170
+ keywords: z.array(z.string()).optional(),
171
+ isHiddenFromOverview: z.boolean().optional(),
172
+ hidden: z.boolean().optional(),
173
+ hiddenComponents: z.array(z.string()).optional(),
174
+ usage: z.unknown().optional(),
175
+ playground: z.unknown().optional(),
176
+ theming: z.unknown().optional(),
177
+ examples: z.array(z.unknown()).optional(),
178
+ showcase: z.unknown().optional(),
179
+ // `parent` and legacy `subComponentOf` are synonyms; both accepted here so a
180
+ // parent-based doc that omits a stamped `type` still validates.
181
+ parent: z.string().optional(),
182
+ relatedDocs: z.array(z.string()).optional(),
183
+ relatedComponents: z.array(z.string()).optional(),
184
+ relatedHooks: z.array(z.string()).optional(),
185
+ });
186
+
187
+ /** Legacy: directory exporting a single primary component (props inline). */
188
+ const LegacySingleComponentDocSchema = LegacyBaseDocSchema.extend({
189
+ props: z.array(PropSchema),
190
+ }).passthrough();
191
+
192
+ /** Legacy: directory exporting multiple components (props on each entry). */
193
+ const LegacyMultiComponentDocSchema = LegacyBaseDocSchema.extend({
194
+ components: z.array(z.unknown()),
195
+ }).passthrough();
196
+
197
+ /** Legacy: a standalone hook doc — inputs (`params`) + outputs (`returns`). */
198
+ const LegacyHookDocSchema = LegacyBaseDocSchema.extend({
199
+ params: z.array(ParamSchema),
200
+ returns: z.array(ReturnSchema),
201
+ }).passthrough();
202
+
203
+ /** Legacy: a sub-component doc parented via `subComponentOf`. */
204
+ const LegacySubComponentDocSchema = LegacyBaseDocSchema.extend({
205
+ subComponentOf: z.string().min(1, 'subComponentOf is required'),
206
+ description: z.string(),
207
+ props: z.array(PropSchema),
208
+ }).passthrough();
209
+
210
+ /**
211
+ * The permissive legacy union. `subComponentOf` is listed first so a doc that
212
+ * carries both `subComponentOf` and `props` is validated as a sub-component
213
+ * (the more specific shape); hook (`params`/`returns`) before multi/single.
214
+ */
215
+ export const LegacyDocSchema = z.union([
216
+ LegacySubComponentDocSchema,
217
+ LegacyHookDocSchema,
218
+ LegacyMultiComponentDocSchema,
219
+ LegacySingleComponentDocSchema,
220
+ ]);
221
+
222
+ /**
223
+ * The LOAD-boundary contract for a doc. Handles BOTH formats:
224
+ * - NEW: a stamped doc (`type: 'component' | 'function' | 'generic'`,
225
+ * produced by the factories below) is validated against the matching
226
+ * per-kind schema in {@link StampedDocSchema}.
227
+ * - OLD: a loose, unstamped doc is validated against the permissive
228
+ * {@link LegacyDocSchema} union (the three legacy shapes + standalone hook).
229
+ *
230
+ * Discovery validates the SHAPE, not "was it made by the factory". This PR does
231
+ * NOT migrate real docs; a later PR does. Both formats normalize into the same
232
+ * internal shape consumers already expect.
233
+ */
234
+ export const ComponentDocSchema = z.union([StampedDocSchema, LegacyDocSchema]);
235
+
236
+ /**
237
+ * Author a component doc. Stamp-only: returns the def with `type: 'component'`
238
+ * injected. Validation happens at the load boundary.
239
+ *
240
+ * @template {import('./types/doc').AstryxComponentDocInput} T
241
+ * @param {T} def
242
+ * @returns {T & {type: 'component'}}
243
+ */
244
+ export function createComponentDoc(def) {
245
+ return /** @type {T & {type: 'component'}} */ ({...def, type: 'component'});
246
+ }
247
+
248
+ /**
249
+ * Author a function doc (covers any function, including hooks). Stamp-only:
250
+ * returns the def with `type: 'function'` injected. Validation happens at the
251
+ * load boundary.
252
+ *
253
+ * @template {import('./types/doc').AstryxFunctionDocInput} T
254
+ * @param {T} def
255
+ * @returns {T & {type: 'function'}}
256
+ */
257
+ export function createFunctionDoc(def) {
258
+ return /** @type {T & {type: 'function'}} */ ({...def, type: 'function'});
259
+ }
260
+
261
+ /**
262
+ * Author a generic reference/topic doc. Stamp-only: returns the def with
263
+ * `type: 'generic'` injected. Validation happens at the load boundary.
264
+ *
265
+ * @template {import('./types/doc').AstryxGenericDocInput} T
266
+ * @param {T} def
267
+ * @returns {T & {type: 'generic'}}
268
+ */
269
+ export function createDoc(def) {
270
+ return /** @type {T & {type: 'generic'}} */ ({...def, type: 'generic'});
271
+ }
@@ -1,8 +1,19 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * Re-export of the integration-authoring helper, which now lives in
5
- * `@astryxdesign/core/authoring`. Kept here so existing
6
- * `@astryxdesign/cli/integration` imports continue to work unchanged.
4
+ * Type-preserving helper for an Astryx integration manifest.
5
+ *
6
+ * This is an intentionally tiny runtime identity function: it returns its
7
+ * argument unchanged. Its value is the exported TypeScript surface from
8
+ * `@astryxdesign/cli/integration`, so manifests get editor/type feedback
9
+ * without coupling to CLI internals. Validation is NOT performed here — it
10
+ * happens at the load boundary (see `loadModuleWithSchema` +
11
+ * `AstryxIntegrationSchema`).
12
+ *
13
+ * @template {import('./types/integration').AstryxIntegration} T
14
+ * @param {T} integration
15
+ * @returns {T}
7
16
  */
8
- export {createIntegration} from '@astryxdesign/core/authoring';
17
+ export function createIntegration(integration) {
18
+ return integration;
19
+ }
@@ -34,30 +34,15 @@ function getTargetDataAttributes(target) {
34
34
  ];
35
35
  }
36
36
 
37
- /**
38
- * Escape a value for a markdown table cell.
39
- *
40
- * A prop type is a union, and a union is spelled with the same `|` that GFM
41
- * uses to separate cells — backticks do not protect it. A row with more cells
42
- * than the header has columns gets the excess *discarded*, so an unescaped
43
- * `gap: 0 | 0.5 | ...` silently eats its own Default and Description columns.
44
- * <!-- SYNC: packages/core/src/Markdown/parser.ts (splits on unescaped pipes) -->
45
- */
46
- export function mdCell(value) {
47
- return String(value ?? '').replace(/\|/g, '\\|');
48
- }
49
-
50
37
  function formatPropsTable(props) {
51
38
  if (!props || props.length === 0) return '';
52
39
  const lines = [];
53
40
  lines.push('| Prop | Type | Default | Description |');
54
41
  lines.push('|------|------|---------|-------------|');
55
42
  for (const p of props) {
56
- const def = p.default ? `\`${mdCell(p.default)}\`` : '—';
43
+ const def = p.default ? `\`${p.default}\`` : '—';
57
44
  const req = p.required ? ' **(required)**' : '';
58
- lines.push(
59
- `| \`${mdCell(p.name)}\` | \`${mdCell(p.type)}\` | ${def} | ${mdCell(p.description)}${req} |`,
60
- );
45
+ lines.push(`| \`${p.name}\` | \`${p.type}\` | ${def} | ${p.description}${req} |`);
61
46
  }
62
47
  return lines.join('\n');
63
48
  }
@@ -192,7 +177,7 @@ export function formatFull(docs, options = {}) {
192
177
  sections.push('|---------|----------|-------------|');
193
178
  for (const el of docs.usage.anatomy) {
194
179
  const req = el.required ? 'Yes' : 'No';
195
- sections.push(`| ${mdCell(el.name)} | ${req} | ${mdCell(el.description)} |`);
180
+ sections.push(`| ${el.name} | ${req} | ${el.description} |`);
196
181
  }
197
182
  sections.push('');
198
183
  }
@@ -297,9 +282,7 @@ export function formatFull(docs, options = {}) {
297
282
  varLines.push('| CSS Variable | Default | Description |');
298
283
  varLines.push('|-------------|---------|-------------|');
299
284
  for (const v of publicVars) {
300
- varLines.push(
301
- `| \`${mdCell(v.name)}\` | \`${mdCell(v.default)}\` | ${mdCell(v.description)} |`,
302
- );
285
+ varLines.push(`| \`${v.name}\` | \`${v.default}\` | ${v.description} |`);
303
286
  }
304
287
  sections.push(varLines.join('\n') + '\n');
305
288
  }
@@ -395,8 +378,8 @@ export function formatCompact(docs, componentName, importHint) {
395
378
  propLines.push('| CSS Property | Sets |');
396
379
  propLines.push('|-------------|------|');
397
380
  for (const d of docs.theming.derived) {
398
- const target = d.expand === 'container' ? 'container layout tokens' : (d.vars || []).map(v => `\`${mdCell(v)}\``).join(', ');
399
- propLines.push(`| \`${mdCell(d.property)}\` | ${target} |`);
381
+ const target = d.expand === 'container' ? 'container layout tokens' : (d.vars || []).map(v => `\`${v}\``).join(', ');
382
+ propLines.push(`| \`${d.property}\` | ${target} |`);
400
383
  }
401
384
  sections.push(propLines.join('\n') + '\n');
402
385
  }
@@ -412,19 +395,6 @@ export function formatCompact(docs, componentName, importHint) {
412
395
  *
413
396
  * For multi-component docs, extracts the entry matching componentName.
414
397
  */
415
- /**
416
- * Longest union `--detail brief` will spell out inside the one-line signature.
417
- *
418
- * The signature is a glance, not a reference: a six-member enum like
419
- * `start|center|end|between|around|evenly` reads at a glance, an eleven-member
420
- * spacing scale does not — and Stack carries four of them (gap, padding,
421
- * paddingInline, paddingBlock), so it printed the same scale four times. Longer
422
- * unions fall through to the terse prop list, exactly as they did when the type
423
- * was still the bare name `SpacingStep`. The full values are always one
424
- * `astryx component <Name>` away.
425
- */
426
- const SIGNATURE_UNION_MAX_MEMBERS = 8;
427
-
428
398
  export function formatBrief(docs, componentName, importHint, options = {}) {
429
399
  const displayName = componentName.startsWith('XDS')
430
400
  ? componentName.slice(3)
@@ -451,15 +421,13 @@ export function formatBrief(docs, componentName, importHint, options = {}) {
451
421
  const otherProps = [];
452
422
 
453
423
  for (const prop of props) {
454
- const values =
455
- prop.type.includes('|') && !prop.type.includes('ReactNode')
456
- ? prop.type
457
- .replace(/['"]/g, '')
458
- .split('|')
459
- .map(v => v.trim())
460
- : null;
461
- if (values && values.length <= SIGNATURE_UNION_MAX_MEMBERS) {
462
- signatureProps.push(`${prop.name}: ${values.join('|')}`);
424
+ if (prop.type.includes('|') && !prop.type.includes('ReactNode')) {
425
+ const values = prop.type
426
+ .replace(/['"]/g, '')
427
+ .split('|')
428
+ .map(v => v.trim())
429
+ .join('|');
430
+ signatureProps.push(`${prop.name}: ${values}`);
463
431
  } else if (prop.required) {
464
432
  otherProps.unshift(`${prop.name}: ${prop.type.split('|')[0].trim()}`);
465
433
  } else {
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  import {describe, it, expect} from 'vitest';
4
- import {formatBrief, formatFull} from './component-format.mjs';
4
+ import {formatFull} from './component-format.mjs';
5
5
 
6
6
  describe('formatFull sub-component rendering', () => {
7
7
  // Regression guard: sub-components are sometimes declared as a bare
@@ -93,97 +93,3 @@ describe('formatFull theming override keys', () => {
93
93
  expect(out).not.toContain("'astryx-table-cell': {");
94
94
  });
95
95
  });
96
-
97
- /** Pipes that actually separate cells — a `\|` is content, not a separator. */
98
- function cellPipes(row) {
99
- return (row.match(/(?<!\\)\|/g) || []).length;
100
- }
101
-
102
- describe('markdown table cells escape their pipes', () => {
103
- // A prop type is a union, and a union is spelled with `|` — the very
104
- // character GFM uses to separate cells. Backticks do not protect it: a row
105
- // with more cells than the header has columns gets its excess *discarded*,
106
- // so an unescaped `gap: 0 | 0.5 | ...` silently eats its own Default and
107
- // Description columns. See packages/core/src/Markdown/parser.ts, which
108
- // splits on unescaped pipes and preserves `\|` as literal.
109
- it('keeps a union-typed prop row at four cells', () => {
110
- const docs = {
111
- name: 'Grid',
112
- description: 'A grid.',
113
- props: [
114
- {
115
- name: 'gap',
116
- type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
117
- default: '2',
118
- description: 'Spacing between all items.',
119
- },
120
- ],
121
- };
122
- const row = formatFull(docs)
123
- .split('\n')
124
- .find(l => l.startsWith('| `gap`'));
125
-
126
- // A 4-column row has exactly 5 separators. Unescaped, this row had 15.
127
- expect(cellPipes(row)).toBe(5);
128
- expect(row).toContain('\\|');
129
- // The columns the unescaped pipes were swallowing.
130
- expect(row).toContain('`2`');
131
- expect(row).toContain('Spacing between all items.');
132
- });
133
-
134
- it('escapes pipes in a description too', () => {
135
- const docs = {
136
- name: 'AppShell',
137
- description: 'A shell.',
138
- props: [
139
- {
140
- name: 'mobileNav',
141
- type: 'ReactNode',
142
- description: "Config. breakpoint is 'sm' | 'md' | 'lg' | 'none'.",
143
- },
144
- ],
145
- };
146
- const row = formatFull(docs)
147
- .split('\n')
148
- .find(l => l.startsWith('| `mobileNav`'));
149
-
150
- expect(cellPipes(row)).toBe(5);
151
- });
152
- });
153
-
154
- describe('formatBrief signature stays terse', () => {
155
- // `--detail brief` exists to be token-cheap. It hoists union-typed props into
156
- // the signature because a short enum reads at a glance — but an 11-member
157
- // spacing scale does not, and Stack carries four of them (gap, padding,
158
- // paddingInline, paddingBlock), so the same scale got printed four times.
159
- // Long unions stay in the terse prop list, exactly as they did when the type
160
- // was still the bare name `SpacingStep`.
161
- it('hoists a short enum but not a long scale', () => {
162
- const docs = {
163
- name: 'HStack',
164
- description: 'A stack.',
165
- props: [
166
- {
167
- name: 'gap',
168
- type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
169
- description: 'Gap.',
170
- },
171
- {name: 'wrap', type: "'nowrap' | 'wrap' | 'wrap-reverse'", description: 'Wrap.'},
172
- {
173
- name: 'hAlign',
174
- type: "'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'",
175
- description: 'Align.',
176
- },
177
- ],
178
- };
179
- const signature = formatBrief(docs, 'HStack', '').split('\n')[0];
180
-
181
- // Short enums still earn their place in the signature.
182
- expect(signature).toContain('wrap: nowrap|wrap|wrap-reverse');
183
- expect(signature).toContain('hAlign: start|center|end|between|around|evenly');
184
- // The long scale does not.
185
- expect(signature).not.toContain('0|0.5|1|1.5|2|3|4|5|6|8|10');
186
- // But the prop is still named, so it is not lost.
187
- expect(formatBrief(docs, 'HStack', '')).toContain('gap');
188
- });
189
- });