@meistrari/tela-build 1.65.0 → 1.65.1
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/lib/__tests__/doc-generator.test.ts +330 -2
- package/lib/doc-generator.ts +396 -112
- package/package.json +87 -83
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import { dirname, join, resolve } from 'pathe'
|
|
5
5
|
import matter from 'gray-matter'
|
|
6
6
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
7
|
-
import { canonicalizeTypeString, compareCanonical, generateDocsToDirectory, resolveSiblingMdx } from '../doc-generator'
|
|
7
|
+
import { canonicalizeTypeString, compareCanonical, generateDocsToDirectory, generateGroupSlug, generateTagName, parseMdxFile, resolveSiblingMdx } from '../doc-generator'
|
|
8
8
|
import type { ComponentDoc } from '../doc-generator'
|
|
9
9
|
import { TypeResolver } from '../type-resolver'
|
|
10
10
|
|
|
@@ -82,6 +82,73 @@ describe('generateDocsToDirectory', () => {
|
|
|
82
82
|
|
|
83
83
|
expect(packageJson.files).toContain('docs')
|
|
84
84
|
})
|
|
85
|
+
|
|
86
|
+
it('groups nested components by their source family', () => {
|
|
87
|
+
const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
|
|
88
|
+
tempDirs.push(outDir)
|
|
89
|
+
|
|
90
|
+
const makeComponent = (tagName: string, directory: string, path: string): ComponentDoc => ({
|
|
91
|
+
name: tagName,
|
|
92
|
+
tagName,
|
|
93
|
+
path,
|
|
94
|
+
directory,
|
|
95
|
+
description: '',
|
|
96
|
+
props: [],
|
|
97
|
+
events: [],
|
|
98
|
+
slots: [],
|
|
99
|
+
})
|
|
100
|
+
const components = [
|
|
101
|
+
makeComponent('TelaComplexTable', 'tela/complex-table', 'components/tela/complex-table/complex-table.vue'),
|
|
102
|
+
makeComponent('TelaComplexTableHeader', 'tela/complex-table/header', 'components/tela/complex-table/header/header.vue'),
|
|
103
|
+
makeComponent('TelaComplexBadge', 'tela/badge', 'components/tela/badge/complex-badge.vue'),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
generateDocsToDirectory(components.reverse(), new TypeResolver(outDir), outDir)
|
|
107
|
+
|
|
108
|
+
const skillMd = readFileSync(join(outDir, 'tela-build', 'SKILL.md'), 'utf-8')
|
|
109
|
+
const componentPage = readFileSync(join(outDir, 'tela-build', 'components', 'tela-complex-table.md'), 'utf-8')
|
|
110
|
+
const badgePage = readFileSync(join(outDir, 'tela-build', 'components', 'tela-badge.md'), 'utf-8')
|
|
111
|
+
|
|
112
|
+
expect(skillMd).toContain('[TelaComplexTable](components/tela-complex-table.md)')
|
|
113
|
+
expect(componentPage).toContain('## TelaComplexTable')
|
|
114
|
+
expect(componentPage).toContain('## TelaComplexTableHeader')
|
|
115
|
+
expect(badgePage).toContain('## TelaComplexBadge')
|
|
116
|
+
expect(existsSync(join(outDir, 'tela-build', 'components', 'tela-complex.md'))).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('nuxt-compatible component names', () => {
|
|
121
|
+
it.each([
|
|
122
|
+
['metrics-card', 'tela/home', 'TelaHomeMetricsCard'],
|
|
123
|
+
['hero-metrics-card', 'tela/details/hero', 'TelaDetailsHeroMetricsCard'],
|
|
124
|
+
['index', 'tela/chat', 'TelaChat'],
|
|
125
|
+
['index', 'tela/chat/command/mention', 'TelaChatCommandMention'],
|
|
126
|
+
['sheet.client', 'tela/sheet', 'TelaSheet'],
|
|
127
|
+
['sheet.server', 'tela/sheet', 'TelaSheet'],
|
|
128
|
+
['avatar.global', 'tela/avatar', 'TelaAvatar'],
|
|
129
|
+
['index.client', 'tela/chat', 'TelaChat'],
|
|
130
|
+
['Item', 'tela/item/item', 'TelaItem'],
|
|
131
|
+
['DropdownMenuSeparator', 'tela/dropdown-menu', 'TelaDropdownMenuSeparator'],
|
|
132
|
+
['button', '', 'Button'],
|
|
133
|
+
])('resolves %s in %s as %s', (fileName, directory, expected) => {
|
|
134
|
+
expect(generateTagName(fileName, directory)).toBe(expected)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it.each([
|
|
138
|
+
['tela/DropdownMenu', 'tela-dropdown-menu'],
|
|
139
|
+
['tela\\DropdownMenu', 'tela-dropdown-menu'],
|
|
140
|
+
])('normalizes source family %s as %s', (directory, expected) => {
|
|
141
|
+
expect(generateGroupSlug({
|
|
142
|
+
name: 'TelaDropdownMenu',
|
|
143
|
+
tagName: 'TelaDropdownMenu',
|
|
144
|
+
path: 'components/tela/dropdown-menu/dropdown-menu.vue',
|
|
145
|
+
directory,
|
|
146
|
+
description: '',
|
|
147
|
+
props: [],
|
|
148
|
+
events: [],
|
|
149
|
+
slots: [],
|
|
150
|
+
})).toBe(expected)
|
|
151
|
+
})
|
|
85
152
|
})
|
|
86
153
|
|
|
87
154
|
// The extractors emit members in type-resolution order, which differs between
|
|
@@ -114,6 +181,35 @@ describe('resolveSiblingMdx', () => {
|
|
|
114
181
|
expect(resolveSiblingMdx(join(dir, 'Collapsible.vue'))).toBe(join(dir, 'collapsible.mdx'))
|
|
115
182
|
})
|
|
116
183
|
|
|
184
|
+
it('resolves an index component to its family MDX', () => {
|
|
185
|
+
const root = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
|
|
186
|
+
const dir = join(root, 'chat')
|
|
187
|
+
tempDirs.push(root)
|
|
188
|
+
mkdirSync(dir)
|
|
189
|
+
writeFileSync(join(dir, 'index.vue'), '')
|
|
190
|
+
writeFileSync(join(dir, 'chat.mdx'), '')
|
|
191
|
+
|
|
192
|
+
expect(resolveSiblingMdx(join(dir, 'index.vue'))).toBe(join(dir, 'chat.mdx'))
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('resolves PascalCase component files to kebab-case MDX', () => {
|
|
196
|
+
const dir = makeComponentDir(['DropdownMenu.vue', 'dropdown-menu.mdx'])
|
|
197
|
+
|
|
198
|
+
expect(resolveSiblingMdx(join(dir, 'DropdownMenu.vue'))).toBe(join(dir, 'dropdown-menu.mdx'))
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('resolves Nuxt mode-suffixed components to unsuffixed MDX', () => {
|
|
202
|
+
const dir = makeComponentDir(['sheet.client.vue', 'sheet.mdx'])
|
|
203
|
+
|
|
204
|
+
expect(resolveSiblingMdx(join(dir, 'sheet.client.vue'))).toBe(join(dir, 'sheet.mdx'))
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('prefers an exact component MDX over normalized candidates', () => {
|
|
208
|
+
const dir = makeComponentDir(['DropdownMenu.vue', 'DropdownMenu.mdx', 'dropdown-menu.mdx'])
|
|
209
|
+
|
|
210
|
+
expect(resolveSiblingMdx(join(dir, 'DropdownMenu.vue'))).toBe(join(dir, 'DropdownMenu.mdx'))
|
|
211
|
+
})
|
|
212
|
+
|
|
117
213
|
it('returns undefined when the component has no mdx', () => {
|
|
118
214
|
const dir = makeComponentDir(['CollapsibleTrigger.vue', 'collapsible.mdx'])
|
|
119
215
|
|
|
@@ -125,6 +221,238 @@ describe('resolveSiblingMdx', () => {
|
|
|
125
221
|
})
|
|
126
222
|
})
|
|
127
223
|
|
|
224
|
+
describe('authored MDX guidance', () => {
|
|
225
|
+
it('uses the MDX structure to preserve guidance and remove Storybook controls and authored API', async () => {
|
|
226
|
+
const mdxDir = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
|
|
227
|
+
const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
|
|
228
|
+
tempDirs.push(mdxDir, outDir)
|
|
229
|
+
|
|
230
|
+
const mdxPath = join(mdxDir, 'sample.mdx')
|
|
231
|
+
const mdx = [
|
|
232
|
+
'import { Meta, Canvas as Preview, ArgTypes, Controls } from \'@storybook/blocks\'',
|
|
233
|
+
'import * as Blocks from \'@storybook/blocks\'',
|
|
234
|
+
'import Storybook from \'@storybook/blocks\'',
|
|
235
|
+
'import Note from \'./note.vue\'',
|
|
236
|
+
'export const Template = () => <TelaBadge />',
|
|
237
|
+
'',
|
|
238
|
+
'# TelaSample',
|
|
239
|
+
'',
|
|
240
|
+
'Content after a JSX export remains.',
|
|
241
|
+
'',
|
|
242
|
+
'<Meta title="Sample" />',
|
|
243
|
+
'',
|
|
244
|
+
'## Rules',
|
|
245
|
+
'',
|
|
246
|
+
'- Prefer the canonical component.',
|
|
247
|
+
'',
|
|
248
|
+
'<Note>Keep non-Storybook JSX.</Note>',
|
|
249
|
+
'',
|
|
250
|
+
'### Interactive preview',
|
|
251
|
+
'',
|
|
252
|
+
'<Preview',
|
|
253
|
+
' parameters={{ format: value => value > 0 ? \'/>\' : \'>\' }}',
|
|
254
|
+
'>',
|
|
255
|
+
'```vue',
|
|
256
|
+
'<TelaStorybookOnly />',
|
|
257
|
+
'```',
|
|
258
|
+
'</Preview>',
|
|
259
|
+
'',
|
|
260
|
+
'Before <Controls /> after.',
|
|
261
|
+
'<Blocks.Source />',
|
|
262
|
+
'<Storybook.Primary />',
|
|
263
|
+
'<Storybook />',
|
|
264
|
+
'',
|
|
265
|
+
'### Basic usage',
|
|
266
|
+
'',
|
|
267
|
+
'```vue',
|
|
268
|
+
'<TelaSample />',
|
|
269
|
+
'```',
|
|
270
|
+
'',
|
|
271
|
+
'### Literal Storybook syntax',
|
|
272
|
+
'',
|
|
273
|
+
'~~~mdx',
|
|
274
|
+
'<Meta title="literal" />',
|
|
275
|
+
'~~~',
|
|
276
|
+
'',
|
|
277
|
+
'| Purpose | Component |',
|
|
278
|
+
'| --- | --- |',
|
|
279
|
+
'| Sample | TelaSample |',
|
|
280
|
+
'',
|
|
281
|
+
'## Props',
|
|
282
|
+
'',
|
|
283
|
+
'Stale authored props.',
|
|
284
|
+
'',
|
|
285
|
+
'### Nested prop notes',
|
|
286
|
+
'',
|
|
287
|
+
'These must be removed with Props.',
|
|
288
|
+
'',
|
|
289
|
+
'## Slots',
|
|
290
|
+
'',
|
|
291
|
+
'The leading slot receives row context.',
|
|
292
|
+
'',
|
|
293
|
+
'## Events',
|
|
294
|
+
'',
|
|
295
|
+
'The update event fires after validation.',
|
|
296
|
+
'',
|
|
297
|
+
'## Emits',
|
|
298
|
+
'',
|
|
299
|
+
'Catch errors from async parent handlers.',
|
|
300
|
+
'',
|
|
301
|
+
'## API',
|
|
302
|
+
'',
|
|
303
|
+
'<ArgTypes of={SampleStories} />',
|
|
304
|
+
'',
|
|
305
|
+
'## Virtualization Props',
|
|
306
|
+
'',
|
|
307
|
+
'Keep semantic prop guidance.',
|
|
308
|
+
'',
|
|
309
|
+
'## Size Prop',
|
|
310
|
+
'',
|
|
311
|
+
'Keep focused size guidance.',
|
|
312
|
+
'',
|
|
313
|
+
].join('\n')
|
|
314
|
+
writeFileSync(mdxPath, mdx, 'utf-8')
|
|
315
|
+
|
|
316
|
+
const stories = await parseMdxFile(mdxPath)
|
|
317
|
+
const component: ComponentDoc = {
|
|
318
|
+
name: 'TelaSample',
|
|
319
|
+
tagName: 'TelaSample',
|
|
320
|
+
path: 'components/tela/sample.vue',
|
|
321
|
+
directory: 'tela',
|
|
322
|
+
description: '',
|
|
323
|
+
props: [{ name: 'variant', required: false, type: 'string' }],
|
|
324
|
+
events: [{ name: 'update', type: 'string' }],
|
|
325
|
+
slots: [{ name: 'default', description: 'Sample content' }],
|
|
326
|
+
stories,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
generateDocsToDirectory([component], new TypeResolver(outDir), outDir)
|
|
330
|
+
const page = readFileSync(join(outDir, 'tela-build', 'components', 'tela-sample.md'), 'utf-8')
|
|
331
|
+
|
|
332
|
+
expect(stories?.examples).toEqual([{ name: 'Basic usage', code: '<TelaSample />' }])
|
|
333
|
+
expect(page).toContain('### Usage guidance')
|
|
334
|
+
expect(page).toContain('Content after a JSX export remains.')
|
|
335
|
+
expect(page).toContain('#### Rules')
|
|
336
|
+
expect(page).toContain('- Prefer the canonical component.')
|
|
337
|
+
expect(page).toContain('<Note>Keep non-Storybook JSX.</Note>')
|
|
338
|
+
expect(page).toContain('<Meta title="literal" />')
|
|
339
|
+
expect(page).toMatch(/\| Purpose\s+\| Component\s+\|/)
|
|
340
|
+
expect(page).toContain('#### Virtualization Props')
|
|
341
|
+
expect(page).toContain('Keep semantic prop guidance.')
|
|
342
|
+
expect(page).toContain('#### Size Prop')
|
|
343
|
+
expect(page).toContain('Keep focused size guidance.')
|
|
344
|
+
expect(page.match(/<TelaSample \/>/g)).toHaveLength(1)
|
|
345
|
+
expect(page.match(/^### Props$/gm)).toHaveLength(1)
|
|
346
|
+
expect(page.match(/^### Events$/gm)).toHaveLength(1)
|
|
347
|
+
expect(page.match(/^### Event guidance$/gm)).toHaveLength(1)
|
|
348
|
+
expect(page).toContain('The update event fires after validation.')
|
|
349
|
+
expect(page).toContain('Catch errors from async parent handlers.')
|
|
350
|
+
expect(page.match(/^### Slots$/gm)).toHaveLength(1)
|
|
351
|
+
expect(page.match(/^### Slot guidance$/gm)).toHaveLength(1)
|
|
352
|
+
expect(page).toContain('The leading slot receives row context.')
|
|
353
|
+
expect(page).not.toContain('@storybook/blocks')
|
|
354
|
+
expect(page).not.toContain('<TelaStorybookOnly />')
|
|
355
|
+
expect(page).not.toContain('<Preview')
|
|
356
|
+
expect(page).not.toContain('<Controls')
|
|
357
|
+
expect(page).not.toContain('<Blocks.Source')
|
|
358
|
+
expect(page).not.toContain('<Storybook')
|
|
359
|
+
expect(page).toMatch(/Before\s+after\./)
|
|
360
|
+
expect(page).not.toContain('Stale authored props.')
|
|
361
|
+
expect(page).not.toContain('Nested prop notes')
|
|
362
|
+
expect(page).not.toContain('<ArgTypes')
|
|
363
|
+
expect(page.match(/^# TelaSample$/gm)).toHaveLength(1)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('preserves authored event and slot semantics when extracted metadata is incomplete', async () => {
|
|
367
|
+
const mdxDir = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
|
|
368
|
+
const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
|
|
369
|
+
tempDirs.push(mdxDir, outDir)
|
|
370
|
+
const mdxPath = join(mdxDir, 'sample.mdx')
|
|
371
|
+
writeFileSync(mdxPath, [
|
|
372
|
+
'# TelaSample',
|
|
373
|
+
'',
|
|
374
|
+
'## Events',
|
|
375
|
+
'',
|
|
376
|
+
'`loadMore` requires errors from async parent handlers to be caught.',
|
|
377
|
+
'',
|
|
378
|
+
'## Slots',
|
|
379
|
+
'',
|
|
380
|
+
'`leading` receives row data and loading state.',
|
|
381
|
+
'',
|
|
382
|
+
'```vue',
|
|
383
|
+
'<TelaSample />',
|
|
384
|
+
'```',
|
|
385
|
+
].join('\n'))
|
|
386
|
+
|
|
387
|
+
const stories = await parseMdxFile(mdxPath)
|
|
388
|
+
const component: ComponentDoc = {
|
|
389
|
+
name: 'TelaSample',
|
|
390
|
+
tagName: 'TelaSample',
|
|
391
|
+
path: 'components/tela/sample.vue',
|
|
392
|
+
directory: 'tela',
|
|
393
|
+
description: '',
|
|
394
|
+
props: [],
|
|
395
|
+
events: [],
|
|
396
|
+
slots: [{ name: 'leading', description: '' }],
|
|
397
|
+
stories,
|
|
398
|
+
}
|
|
399
|
+
generateDocsToDirectory([component], new TypeResolver(outDir), outDir)
|
|
400
|
+
const page = readFileSync(join(outDir, 'tela-build', 'components', 'tela-sample.md'), 'utf-8')
|
|
401
|
+
|
|
402
|
+
expect(page).not.toContain('### Events\n')
|
|
403
|
+
expect(page).toContain('### Event guidance')
|
|
404
|
+
expect(page).toContain('requires errors from async parent handlers to be caught')
|
|
405
|
+
expect(page).toContain('### Slots')
|
|
406
|
+
expect(page).toContain('| `leading` | — | |')
|
|
407
|
+
expect(page).toContain('### Slot guidance')
|
|
408
|
+
expect(page).toContain('receives row data and loading state')
|
|
409
|
+
expect(page.match(/<TelaSample \/>/g)).toHaveLength(1)
|
|
410
|
+
expect(page).not.toContain('### Examples')
|
|
411
|
+
expect(stories?.examples).toEqual([{ name: 'Slots', code: '<TelaSample />' }])
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('does not restore fenced examples removed with a Storybook subtree', async () => {
|
|
415
|
+
const dir = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
|
|
416
|
+
tempDirs.push(dir)
|
|
417
|
+
const mdxPath = join(dir, 'sample.mdx')
|
|
418
|
+
writeFileSync(mdxPath, [
|
|
419
|
+
'import { Canvas } from \'@storybook/blocks\'',
|
|
420
|
+
'',
|
|
421
|
+
'# TelaSample',
|
|
422
|
+
'',
|
|
423
|
+
'## Interactive preview',
|
|
424
|
+
'',
|
|
425
|
+
'<Canvas>',
|
|
426
|
+
'```vue',
|
|
427
|
+
'<TelaStorybookOnly />',
|
|
428
|
+
'```',
|
|
429
|
+
'</Canvas>',
|
|
430
|
+
].join('\n'))
|
|
431
|
+
|
|
432
|
+
const stories = await parseMdxFile(mdxPath)
|
|
433
|
+
|
|
434
|
+
expect(stories?.content).toBe('')
|
|
435
|
+
expect(stories?.examples).toEqual([])
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('preserves similarly named JSX not imported from Storybook', async () => {
|
|
439
|
+
const dir = mkdtempSync(join(tmpdir(), 'tela-build-mdx-'))
|
|
440
|
+
tempDirs.push(dir)
|
|
441
|
+
const mdxPath = join(dir, 'sample.mdx')
|
|
442
|
+
writeFileSync(mdxPath, [
|
|
443
|
+
'import Canvas from \'./canvas.vue\'',
|
|
444
|
+
'',
|
|
445
|
+
'# TelaSample',
|
|
446
|
+
'',
|
|
447
|
+
'<Canvas>Keep this content.</Canvas>',
|
|
448
|
+
].join('\n'))
|
|
449
|
+
|
|
450
|
+
const stories = await parseMdxFile(mdxPath)
|
|
451
|
+
|
|
452
|
+
expect(stories?.content).toContain('<Canvas>Keep this content.</Canvas>')
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
128
456
|
describe('canonical ordering', () => {
|
|
129
457
|
function makeDoc(overrides: Partial<ComponentDoc> = {}): ComponentDoc {
|
|
130
458
|
return {
|
package/lib/doc-generator.ts
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
import { join, resolve, relative, dirname, basename } from 'pathe'
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs'
|
|
4
4
|
import { parse as parseVue } from 'vue-docgen-api'
|
|
5
|
+
import { pascalCase, splitByCase } from 'scule'
|
|
6
|
+
import ts from 'typescript'
|
|
7
|
+
import { unified } from 'unified'
|
|
8
|
+
import remarkParse from 'remark-parse'
|
|
9
|
+
import remarkMdx from 'remark-mdx'
|
|
10
|
+
import remarkGfm from 'remark-gfm'
|
|
11
|
+
import remarkStringify from 'remark-stringify'
|
|
12
|
+
import type { Code, Heading, Root, RootContent } from 'mdast'
|
|
13
|
+
import type { MdxJsxFlowElement, MdxJsxTextElement, MdxjsEsm } from 'mdast-util-mdx'
|
|
5
14
|
// Load glob dynamically for compatibility across versions (v8 vs v11)
|
|
6
15
|
import { getTypeResolver } from './type-resolver'
|
|
7
16
|
import type { TypeResolver } from './type-resolver'
|
|
@@ -29,9 +38,17 @@ export interface ComponentDoc {
|
|
|
29
38
|
parseError?: string
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
export interface StoryExample {
|
|
42
|
+
name: string
|
|
43
|
+
code: string
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
export interface StoryDoc {
|
|
33
47
|
title: string
|
|
34
|
-
|
|
48
|
+
content: string
|
|
49
|
+
eventGuidance: string
|
|
50
|
+
slotGuidance: string
|
|
51
|
+
examples: StoryExample[]
|
|
35
52
|
argTypes: any
|
|
36
53
|
}
|
|
37
54
|
|
|
@@ -285,8 +302,8 @@ export async function collectComponentDocs(layerPath: string, appRootDir?: strin
|
|
|
285
302
|
const fileName = basename(file, '.vue')
|
|
286
303
|
const directory = relative(componentPath, dirname(file))
|
|
287
304
|
|
|
288
|
-
//
|
|
289
|
-
const tagName = generateTagName(fileName)
|
|
305
|
+
// Match Nuxt's path-aware component auto-import name.
|
|
306
|
+
const tagName = generateTagName(fileName, directory)
|
|
290
307
|
|
|
291
308
|
const componentDoc: ComponentDoc = {
|
|
292
309
|
name: info.displayName || info.exportName || fileName,
|
|
@@ -355,99 +372,120 @@ async function globAsync(pattern: string, options: any): Promise<string[]> {
|
|
|
355
372
|
return []
|
|
356
373
|
}
|
|
357
374
|
|
|
358
|
-
|
|
359
|
-
// Convert file name to PascalCase tag name
|
|
360
|
-
// Components in tela/ directory should preserve the Tela prefix
|
|
361
|
-
// e.g., input -> TelaInput, button -> TelaButton
|
|
375
|
+
const COMPONENT_MODE_REPLACEMENT = /(?:\.(?:client|server))?(?:\.global|\.island)*$/
|
|
362
376
|
|
|
363
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Mirror Nuxt's directory-aware component naming. Directory segments prefix the
|
|
379
|
+
* file name, repeated suffixes are collapsed, `index.vue` resolves to its
|
|
380
|
+
* directory, and mode suffixes such as `.client.vue` are not part of the tag.
|
|
381
|
+
*/
|
|
382
|
+
export function generateTagName(fileName: string, directory: string): string {
|
|
383
|
+
const prefixParts = splitByCase(directory)
|
|
384
|
+
const normalizedFileName = fileName
|
|
385
|
+
.replace(COMPONENT_MODE_REPLACEMENT, '')
|
|
386
|
+
.replace(/^index$/i, '')
|
|
387
|
+
const fileNameParts = splitByCase(normalizedFileName)
|
|
388
|
+
const fileNamePartsContent = fileNameParts.join('/').toLowerCase()
|
|
389
|
+
const componentNameParts = prefixParts.flatMap(part => splitByCase(part))
|
|
390
|
+
|
|
391
|
+
let index = prefixParts.length - 1
|
|
392
|
+
const matchedSuffix: string[] = []
|
|
393
|
+
while (index >= 0) {
|
|
394
|
+
const prefixPart = prefixParts[index]
|
|
395
|
+
if (!prefixPart) {
|
|
396
|
+
index--
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
364
399
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
tagName = `Tela${componentName}`
|
|
400
|
+
matchedSuffix.unshift(...splitByCase(prefixPart).map(part => part.toLowerCase()))
|
|
401
|
+
const matchedSuffixContent = matchedSuffix.join('/')
|
|
402
|
+
const repeatedDirectory = prefixPart.toLowerCase() === fileNamePartsContent
|
|
403
|
+
&& prefixParts[index + 1]
|
|
404
|
+
&& prefixParts[index] === prefixParts[index + 1]
|
|
405
|
+
|
|
406
|
+
if (
|
|
407
|
+
fileNamePartsContent === matchedSuffixContent
|
|
408
|
+
|| fileNamePartsContent.startsWith(`${matchedSuffixContent}/`)
|
|
409
|
+
|| repeatedDirectory
|
|
410
|
+
) {
|
|
411
|
+
componentNameParts.length = index
|
|
412
|
+
}
|
|
413
|
+
index--
|
|
380
414
|
}
|
|
381
415
|
|
|
382
|
-
return
|
|
416
|
+
return pascalCase([...componentNameParts, ...fileNameParts])
|
|
383
417
|
}
|
|
384
418
|
|
|
385
419
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* `
|
|
390
|
-
* `collapsible.mdx` — the casings differ. A plain `existsSync()` on the derived
|
|
391
|
-
* `Collapsible.mdx` is true on macOS (case-insensitive APFS quietly resolves it)
|
|
392
|
-
* and false on Linux, so CI generated that component with its examples missing
|
|
393
|
-
* and reported the committed page as drift. Resolve through the directory
|
|
394
|
-
* listing instead, so both platforms find the same file — and pick the
|
|
395
|
-
* lowest-sorting candidate, because a case-sensitive filesystem is free to hold
|
|
396
|
-
* several files that differ only in case.
|
|
420
|
+
* Resolve the authored MDX associated with a component using the same filename
|
|
421
|
+
* normalization as Nuxt component discovery. Family docs commonly use the
|
|
422
|
+
* directory name for `index.vue`, kebab-case for PascalCase component files,
|
|
423
|
+
* and omit `.client` / `.server` mode suffixes.
|
|
397
424
|
*/
|
|
398
425
|
export function resolveSiblingMdx(vueFile: string): string | undefined {
|
|
399
|
-
const
|
|
400
|
-
const directory = dirname(mdxPath)
|
|
426
|
+
const directory = dirname(vueFile)
|
|
401
427
|
if (!existsSync(directory)) {
|
|
402
428
|
return undefined
|
|
403
429
|
}
|
|
404
430
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
431
|
+
const fileName = basename(vueFile, '.vue')
|
|
432
|
+
const normalizedFileName = fileName.replace(COMPONENT_MODE_REPLACEMENT, '')
|
|
433
|
+
const normalizedCandidates = normalizedFileName.toLowerCase() === 'index'
|
|
434
|
+
? [toKebabFromTag(basename(directory)), normalizedFileName, fileName]
|
|
435
|
+
: [fileName, normalizedFileName, toKebabFromTag(normalizedFileName)]
|
|
436
|
+
const candidates = [...new Set(normalizedCandidates.map(candidate => `${candidate}.mdx`))]
|
|
410
437
|
const entries = readdirSync(directory).sort(compareCanonical)
|
|
411
|
-
|
|
412
|
-
|
|
438
|
+
|
|
439
|
+
// Check every normalized candidate exactly before trying case-insensitive
|
|
440
|
+
// matches, so a fuzzy match cannot beat a deterministic Nuxt-style name.
|
|
441
|
+
const exactMatch = candidates
|
|
442
|
+
.map(candidate => entries.find(entry => entry === candidate))
|
|
443
|
+
.find(Boolean)
|
|
444
|
+
const caseInsensitiveMatch = candidates
|
|
445
|
+
.map(candidate => entries.find(entry => entry.toLowerCase() === candidate.toLowerCase()))
|
|
446
|
+
.find(Boolean)
|
|
447
|
+
const match = exactMatch ?? caseInsensitiveMatch
|
|
413
448
|
|
|
414
449
|
return match ? join(directory, match) : undefined
|
|
415
450
|
}
|
|
416
451
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// Extract title from the main heading
|
|
422
|
-
const titleMatch = content.match(/^#\s+(.+)$/m)
|
|
423
|
-
const title = titleMatch?.[1] ?? ''
|
|
424
|
-
|
|
425
|
-
// Extract code examples from ```vue blocks with better heading detection
|
|
426
|
-
const examples: any[] = []
|
|
427
|
-
const codeBlockRegex = /```vue\n([\s\S]*?)\n```/g
|
|
428
|
-
const matches = Array.from(content.matchAll(codeBlockRegex))
|
|
429
|
-
|
|
430
|
-
for (let i = 0; i < matches.length; i++) {
|
|
431
|
-
const match = matches[i]
|
|
432
|
-
const codeContent = match?.[1]?.trim()
|
|
433
|
-
const codeBlockStart = match?.index
|
|
434
|
-
if (!codeContent || codeBlockStart === undefined) {
|
|
435
|
-
continue
|
|
436
|
-
}
|
|
452
|
+
type MdxContent = RootContent | MdxJsxFlowElement | MdxJsxTextElement | MdxjsEsm
|
|
453
|
+
type MdxRoot = Omit<Root, 'children'> & { children: MdxContent[] }
|
|
454
|
+
type MdxParent = MdxContent & { children: MdxContent[] }
|
|
437
455
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
456
|
+
interface StorybookBindings {
|
|
457
|
+
names: Set<string>
|
|
458
|
+
namespaces: Set<string>
|
|
459
|
+
}
|
|
441
460
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
461
|
+
interface SanitizedMdx {
|
|
462
|
+
title: string
|
|
463
|
+
content: string
|
|
464
|
+
eventGuidance: string
|
|
465
|
+
slotGuidance: string
|
|
466
|
+
examples: StoryExample[]
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const AUTHORED_API_SECTIONS = new Set(['api', 'emits', 'events', 'props', 'slots'])
|
|
470
|
+
const mdxProcessor = unified()
|
|
471
|
+
.use(remarkParse)
|
|
472
|
+
.use(remarkMdx)
|
|
473
|
+
.use(remarkGfm)
|
|
474
|
+
.use(remarkStringify, {
|
|
475
|
+
bullet: '-',
|
|
476
|
+
fences: true,
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
export async function parseMdxFile(mdxPath: string): Promise<StoryDoc | undefined> {
|
|
480
|
+
try {
|
|
481
|
+
const sanitized = sanitizeMdxContent(readFileSync(mdxPath, 'utf-8'))
|
|
447
482
|
|
|
448
483
|
return {
|
|
449
|
-
title,
|
|
450
|
-
|
|
484
|
+
title: sanitized.title,
|
|
485
|
+
content: sanitized.content,
|
|
486
|
+
eventGuidance: sanitized.eventGuidance,
|
|
487
|
+
slotGuidance: sanitized.slotGuidance,
|
|
488
|
+
examples: sanitized.examples,
|
|
451
489
|
argTypes: '',
|
|
452
490
|
}
|
|
453
491
|
}
|
|
@@ -457,39 +495,264 @@ export async function parseMdxFile(mdxPath: string): Promise<StoryDoc | undefine
|
|
|
457
495
|
}
|
|
458
496
|
}
|
|
459
497
|
|
|
460
|
-
function
|
|
461
|
-
|
|
462
|
-
|
|
498
|
+
function isMdxEsm(node: MdxContent): node is MdxjsEsm {
|
|
499
|
+
return node.type === 'mdxjsEsm'
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function isMdxJsxElement(node: MdxContent): node is MdxJsxFlowElement | MdxJsxTextElement {
|
|
503
|
+
return node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement'
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isHeading(node: MdxContent): node is Heading {
|
|
507
|
+
return node.type === 'heading'
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function isCode(node: MdxContent): node is Code {
|
|
511
|
+
return node.type === 'code'
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function hasChildren(node: MdxContent): node is MdxParent {
|
|
515
|
+
return 'children' in node && Array.isArray(node.children)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function readNodeText(node: MdxContent): string {
|
|
519
|
+
if ('value' in node && typeof node.value === 'string')
|
|
520
|
+
return node.value
|
|
521
|
+
if (!hasChildren(node))
|
|
522
|
+
return ''
|
|
523
|
+
return node.children.map(readNodeText).join('')
|
|
524
|
+
}
|
|
463
525
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
526
|
+
function collectStorybookBindings(nodes: MdxContent[]): StorybookBindings {
|
|
527
|
+
const bindings: StorybookBindings = {
|
|
528
|
+
names: new Set(),
|
|
529
|
+
namespaces: new Set(),
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
for (const node of nodes) {
|
|
533
|
+
if (!isMdxEsm(node))
|
|
468
534
|
continue
|
|
535
|
+
|
|
536
|
+
const sourceFile = ts.createSourceFile('documentation.mtsx', node.value, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX)
|
|
537
|
+
for (const statement of sourceFile.statements) {
|
|
538
|
+
if (!ts.isImportDeclaration(statement)
|
|
539
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier)
|
|
540
|
+
|| statement.moduleSpecifier.text !== '@storybook/blocks'
|
|
541
|
+
|| !statement.importClause) {
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (statement.importClause.name) {
|
|
546
|
+
bindings.names.add(statement.importClause.name.text)
|
|
547
|
+
bindings.namespaces.add(statement.importClause.name.text)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const namedBindings = statement.importClause.namedBindings
|
|
551
|
+
if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
552
|
+
bindings.namespaces.add(namedBindings.name.text)
|
|
553
|
+
}
|
|
554
|
+
else if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
555
|
+
for (const element of namedBindings.elements)
|
|
556
|
+
bindings.names.add(element.name.text)
|
|
557
|
+
}
|
|
469
558
|
}
|
|
470
|
-
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return bindings
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function isStorybookElement(node: MdxJsxFlowElement | MdxJsxTextElement, bindings: StorybookBindings): boolean {
|
|
565
|
+
if (!node.name)
|
|
566
|
+
return false
|
|
567
|
+
if (bindings.names.has(node.name))
|
|
568
|
+
return true
|
|
569
|
+
|
|
570
|
+
const separator = node.name.indexOf('.')
|
|
571
|
+
return separator > 0 && bindings.namespaces.has(node.name.slice(0, separator))
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function removeMdxArtifacts(nodes: MdxContent[], storybookBindings: StorybookBindings): MdxContent[] {
|
|
575
|
+
const retained: MdxContent[] = []
|
|
471
576
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
577
|
+
for (const node of nodes) {
|
|
578
|
+
if (isMdxEsm(node) || (isMdxJsxElement(node) && isStorybookElement(node, storybookBindings)))
|
|
579
|
+
continue
|
|
580
|
+
|
|
581
|
+
if (hasChildren(node)) {
|
|
582
|
+
const children = removeMdxArtifacts(node.children, storybookBindings)
|
|
583
|
+
node.children.splice(0, node.children.length, ...children)
|
|
584
|
+
if (node.type === 'paragraph' && children.length === 0)
|
|
585
|
+
continue
|
|
476
586
|
}
|
|
477
587
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
588
|
+
retained.push(node)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return retained
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function stripFirstTitle(nodes: MdxContent[]): MdxContent[] {
|
|
595
|
+
let removed = false
|
|
596
|
+
return nodes.filter((node) => {
|
|
597
|
+
if (!removed && isHeading(node) && node.depth === 1) {
|
|
598
|
+
removed = true
|
|
599
|
+
return false
|
|
482
600
|
}
|
|
601
|
+
return true
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
interface AuthoredApiSections {
|
|
606
|
+
content: MdxContent[]
|
|
607
|
+
eventGuidance: MdxContent[]
|
|
608
|
+
slotGuidance: MdxContent[]
|
|
609
|
+
}
|
|
483
610
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
611
|
+
function extractAuthoredApiSections(nodes: MdxContent[]): AuthoredApiSections {
|
|
612
|
+
const sections: AuthoredApiSections = {
|
|
613
|
+
content: [],
|
|
614
|
+
eventGuidance: [],
|
|
615
|
+
slotGuidance: [],
|
|
616
|
+
}
|
|
617
|
+
let index = 0
|
|
618
|
+
|
|
619
|
+
while (index < nodes.length) {
|
|
620
|
+
const node = nodes[index]
|
|
621
|
+
if (!node) {
|
|
622
|
+
index++
|
|
623
|
+
continue
|
|
488
624
|
}
|
|
625
|
+
|
|
626
|
+
if (!isHeading(node)) {
|
|
627
|
+
sections.content.push(node)
|
|
628
|
+
index++
|
|
629
|
+
continue
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const sectionName = readNodeText(node).trim().toLowerCase()
|
|
633
|
+
if (!AUTHORED_API_SECTIONS.has(sectionName)) {
|
|
634
|
+
sections.content.push(node)
|
|
635
|
+
index++
|
|
636
|
+
continue
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const sectionDepth = node.depth
|
|
640
|
+
const sectionContent: MdxContent[] = []
|
|
641
|
+
index++
|
|
642
|
+
while (index < nodes.length) {
|
|
643
|
+
const candidate = nodes[index]
|
|
644
|
+
if (candidate && isHeading(candidate) && candidate.depth <= sectionDepth)
|
|
645
|
+
break
|
|
646
|
+
if (candidate)
|
|
647
|
+
sectionContent.push(candidate)
|
|
648
|
+
index++
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (sectionName === 'events' || sectionName === 'emits')
|
|
652
|
+
sections.eventGuidance.push(...sectionContent)
|
|
653
|
+
else if (sectionName === 'slots')
|
|
654
|
+
sections.slotGuidance.push(...sectionContent)
|
|
489
655
|
}
|
|
490
656
|
|
|
491
|
-
|
|
492
|
-
|
|
657
|
+
return sections
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function stripEmptyHeadings(nodes: MdxContent[]): MdxContent[] {
|
|
661
|
+
return nodes.filter((node, index) => {
|
|
662
|
+
if (!isHeading(node))
|
|
663
|
+
return true
|
|
664
|
+
|
|
665
|
+
const boundary = nodes.findIndex((candidate, candidateIndex) =>
|
|
666
|
+
candidateIndex > index && isHeading(candidate) && candidate.depth <= node.depth)
|
|
667
|
+
const sectionEnd = boundary === -1 ? nodes.length : boundary
|
|
668
|
+
return nodes.slice(index + 1, sectionEnd).some(candidate => !isHeading(candidate))
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function collectVueExamples(nodes: MdxContent[], initialHeading = '', startingIndex = 0): StoryExample[] {
|
|
673
|
+
const examples: StoryExample[] = []
|
|
674
|
+
let currentHeading = initialHeading
|
|
675
|
+
|
|
676
|
+
const visit = (children: MdxContent[]): void => {
|
|
677
|
+
for (const node of children) {
|
|
678
|
+
if (isHeading(node))
|
|
679
|
+
currentHeading = readNodeText(node).trim()
|
|
680
|
+
if (isCode(node) && node.lang?.toLowerCase() === 'vue' && node.value.trim()) {
|
|
681
|
+
examples.push({
|
|
682
|
+
name: currentHeading || `Example ${startingIndex + examples.length + 1}`,
|
|
683
|
+
code: node.value.trim(),
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
if (hasChildren(node))
|
|
687
|
+
visit(node.children)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
visit(nodes)
|
|
692
|
+
return examples
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function shiftHeadingDepth(depth: Heading['depth'], offset: number): Heading['depth'] {
|
|
696
|
+
const shifted = Math.min(6, depth + offset)
|
|
697
|
+
if (shifted === 1)
|
|
698
|
+
return 1
|
|
699
|
+
if (shifted === 2)
|
|
700
|
+
return 2
|
|
701
|
+
if (shifted === 3)
|
|
702
|
+
return 3
|
|
703
|
+
if (shifted === 4)
|
|
704
|
+
return 4
|
|
705
|
+
if (shifted === 5)
|
|
706
|
+
return 5
|
|
707
|
+
return 6
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function shiftHeadings(nodes: MdxContent[], offset: number): void {
|
|
711
|
+
for (const node of nodes) {
|
|
712
|
+
if (isHeading(node))
|
|
713
|
+
node.depth = shiftHeadingDepth(node.depth, offset)
|
|
714
|
+
if (hasChildren(node))
|
|
715
|
+
shiftHeadings(node.children, offset)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function stringifyGuidance(nodes: MdxContent[]): string {
|
|
720
|
+
const content = stripEmptyHeadings(nodes)
|
|
721
|
+
if (content.length === 0)
|
|
722
|
+
return ''
|
|
723
|
+
|
|
724
|
+
shiftHeadings(content, 1)
|
|
725
|
+
const tree: MdxRoot = { type: 'root', children: content }
|
|
726
|
+
return mdxProcessor.stringify(tree).trim()
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function sanitizeMdxContent(source: string): SanitizedMdx {
|
|
730
|
+
const tree: MdxRoot = mdxProcessor.parse(source)
|
|
731
|
+
const titleNode = tree.children.find(node => isHeading(node) && node.depth === 1)
|
|
732
|
+
const title = titleNode && isHeading(titleNode) ? readNodeText(titleNode).trim() : ''
|
|
733
|
+
const storybookBindings = collectStorybookBindings(tree.children)
|
|
734
|
+
|
|
735
|
+
tree.children = removeMdxArtifacts(tree.children, storybookBindings)
|
|
736
|
+
tree.children = stripFirstTitle(tree.children)
|
|
737
|
+
const authoredApi = extractAuthoredApiSections(tree.children)
|
|
738
|
+
tree.children = stripEmptyHeadings(authoredApi.content)
|
|
739
|
+
|
|
740
|
+
const contentExamples = collectVueExamples(tree.children)
|
|
741
|
+
const eventExamples = collectVueExamples(authoredApi.eventGuidance, 'Events', contentExamples.length)
|
|
742
|
+
const slotExamples = collectVueExamples(
|
|
743
|
+
authoredApi.slotGuidance,
|
|
744
|
+
'Slots',
|
|
745
|
+
contentExamples.length + eventExamples.length,
|
|
746
|
+
)
|
|
747
|
+
shiftHeadings(tree.children, 2)
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
title,
|
|
751
|
+
content: mdxProcessor.stringify(tree).trim(),
|
|
752
|
+
eventGuidance: stringifyGuidance(authoredApi.eventGuidance),
|
|
753
|
+
slotGuidance: stringifyGuidance(authoredApi.slotGuidance),
|
|
754
|
+
examples: [...contentExamples, ...eventExamples, ...slotExamples],
|
|
755
|
+
}
|
|
493
756
|
}
|
|
494
757
|
|
|
495
758
|
export function generateMarkdown(componentDocs: ComponentDoc[], typeResolver: TypeResolver): string {
|
|
@@ -677,11 +940,10 @@ export function generateDocsToDirectory(componentDocs: ComponentDoc[], typeResol
|
|
|
677
940
|
mkdirSync(supportingDir, { recursive: true })
|
|
678
941
|
}
|
|
679
942
|
|
|
680
|
-
// Group components by family (e.g., tela-
|
|
943
|
+
// Group components by their source family (e.g., tela/complex-table/* → tela-complex-table.md).
|
|
681
944
|
const groups = new Map<string, ComponentDoc[]>()
|
|
682
945
|
for (const comp of componentDocs.map(canonicalizeComponentDoc)) {
|
|
683
|
-
const
|
|
684
|
-
const group = toGroupSlugFromKebab(kebab)
|
|
946
|
+
const group = generateGroupSlug(comp)
|
|
685
947
|
if (!groups.has(group))
|
|
686
948
|
groups.set(group, [])
|
|
687
949
|
groups.get(group)!.push(comp)
|
|
@@ -807,9 +1069,16 @@ function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeR
|
|
|
807
1069
|
lines.push('')
|
|
808
1070
|
}
|
|
809
1071
|
|
|
1072
|
+
if (comp.stories?.content) {
|
|
1073
|
+
lines.push('### Usage guidance')
|
|
1074
|
+
lines.push('')
|
|
1075
|
+
lines.push(comp.stories.content)
|
|
1076
|
+
lines.push('')
|
|
1077
|
+
}
|
|
1078
|
+
|
|
810
1079
|
// Props
|
|
811
1080
|
if (comp.props && comp.props.length > 0) {
|
|
812
|
-
lines.push('
|
|
1081
|
+
lines.push('### Props')
|
|
813
1082
|
lines.push('')
|
|
814
1083
|
lines.push('```typescript')
|
|
815
1084
|
lines.push('interface Props {')
|
|
@@ -833,7 +1102,7 @@ function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeR
|
|
|
833
1102
|
|
|
834
1103
|
// Events
|
|
835
1104
|
if (comp.events && comp.events.length > 0) {
|
|
836
|
-
lines.push('
|
|
1105
|
+
lines.push('### Events')
|
|
837
1106
|
lines.push('')
|
|
838
1107
|
lines.push('| Event | Type | Description |')
|
|
839
1108
|
lines.push('|------:|:-----|:------------|')
|
|
@@ -846,9 +1115,16 @@ function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeR
|
|
|
846
1115
|
lines.push('')
|
|
847
1116
|
}
|
|
848
1117
|
|
|
1118
|
+
if (comp.stories?.eventGuidance) {
|
|
1119
|
+
lines.push('### Event guidance')
|
|
1120
|
+
lines.push('')
|
|
1121
|
+
lines.push(comp.stories.eventGuidance)
|
|
1122
|
+
lines.push('')
|
|
1123
|
+
}
|
|
1124
|
+
|
|
849
1125
|
// Slots
|
|
850
1126
|
if (comp.slots && comp.slots.length > 0) {
|
|
851
|
-
lines.push('
|
|
1127
|
+
lines.push('### Slots')
|
|
852
1128
|
lines.push('')
|
|
853
1129
|
lines.push('| Slot | Scoped Props | Description |')
|
|
854
1130
|
lines.push('|-----:|:-------------|:------------|')
|
|
@@ -861,12 +1137,21 @@ function generateSingleComponentMarkdown(comp: ComponentDoc, typeResolver: TypeR
|
|
|
861
1137
|
lines.push('')
|
|
862
1138
|
}
|
|
863
1139
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
lines.push('
|
|
1140
|
+
if (comp.stories?.slotGuidance) {
|
|
1141
|
+
lines.push('### Slot guidance')
|
|
1142
|
+
lines.push('')
|
|
1143
|
+
lines.push(comp.stories.slotGuidance)
|
|
1144
|
+
lines.push('')
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (!comp.stories?.content
|
|
1148
|
+
&& !comp.stories?.eventGuidance
|
|
1149
|
+
&& !comp.stories?.slotGuidance
|
|
1150
|
+
&& comp.stories?.examples.length) {
|
|
1151
|
+
lines.push('### Examples')
|
|
867
1152
|
lines.push('')
|
|
868
1153
|
comp.stories.examples.forEach((ex: any) => {
|
|
869
|
-
lines.push(
|
|
1154
|
+
lines.push(`#### ${ex.name}`)
|
|
870
1155
|
lines.push('')
|
|
871
1156
|
lines.push('```vue')
|
|
872
1157
|
lines.push(ex.code)
|
|
@@ -979,12 +1264,11 @@ function renderPantryFrontmatter(pantry: SkillPantryMetadata): string[] {
|
|
|
979
1264
|
return lines
|
|
980
1265
|
}
|
|
981
1266
|
|
|
982
|
-
function
|
|
983
|
-
const
|
|
984
|
-
if (
|
|
985
|
-
return
|
|
986
|
-
|
|
987
|
-
return `${parts[0]}-${parts[1]}`
|
|
1267
|
+
export function generateGroupSlug(comp: ComponentDoc): string {
|
|
1268
|
+
const directoryParts = comp.directory.split(/[\\/]/).filter(Boolean)
|
|
1269
|
+
if (directoryParts.length > 1)
|
|
1270
|
+
return directoryParts.slice(0, 2).map(toKebabFromTag).join('-')
|
|
1271
|
+
return toKebabFromTag(comp.tagName)
|
|
988
1272
|
}
|
|
989
1273
|
|
|
990
1274
|
function toTitleFromGroupSlug(groupSlug: string): string {
|
package/package.json
CHANGED
|
@@ -1,85 +1,89 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
2
|
+
"name": "@meistrari/tela-build",
|
|
3
|
+
"version": "1.65.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"app.config.ts",
|
|
7
|
+
"components",
|
|
8
|
+
"components.json",
|
|
9
|
+
"composables",
|
|
10
|
+
"css",
|
|
11
|
+
"docs",
|
|
12
|
+
"lib",
|
|
13
|
+
"modules",
|
|
14
|
+
"nuxt.config.ts",
|
|
15
|
+
"plugins",
|
|
16
|
+
"public",
|
|
17
|
+
"tsconfig.json",
|
|
18
|
+
"types",
|
|
19
|
+
"unocss.config.ts",
|
|
20
|
+
"utils"
|
|
21
|
+
],
|
|
22
|
+
"main": "./nuxt.config.ts",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"postinstall": "nuxt prepare",
|
|
25
|
+
"generate-docs": "bun scripts/generate-docs.ts"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@floating-ui/vue": "1.1.5",
|
|
29
|
+
"@fontsource-variable/geist-mono": "^5.2.7",
|
|
30
|
+
"@fontsource-variable/inter": "^5.2.8",
|
|
31
|
+
"@iconify-json/ph": "1.2.2",
|
|
32
|
+
"@internationalized/date": "3.6.0",
|
|
33
|
+
"@internationalized/number": "3.6.0",
|
|
34
|
+
"@number-flow/vue": "^0.3.2",
|
|
35
|
+
"@nuxt/icon": "^1.9.0",
|
|
36
|
+
"@types/mdast": "4.0.4",
|
|
37
|
+
"@nuxtjs/color-mode": "^3.5.2",
|
|
38
|
+
"@nuxtjs/tailwindcss": "6.12.2",
|
|
39
|
+
"@unocss/nuxt": "66.5.12",
|
|
40
|
+
"@unocss/transformer-directives": "66.5.12",
|
|
41
|
+
"@vueuse/components": "12.8.2",
|
|
42
|
+
"@vueuse/core": "12.8.2",
|
|
43
|
+
"@vueuse/nuxt": "12.8.2",
|
|
44
|
+
"class-variance-authority": "0.7.0",
|
|
45
|
+
"clsx": "2.1.1",
|
|
46
|
+
"glob": "11.0.0",
|
|
47
|
+
"gsap": "3.12.5",
|
|
48
|
+
"import-in-the-middle": "1.11.2",
|
|
49
|
+
"lucide-vue-next": "0.456.0",
|
|
50
|
+
"mdast-util-mdx": "3.0.0",
|
|
51
|
+
"motion": "10.16.4",
|
|
52
|
+
"motion-v": "2.3.0",
|
|
53
|
+
"nitropack": "2.9.7",
|
|
54
|
+
"number-flow": "0.4.1",
|
|
55
|
+
"nuxt-svgo": "4.2.6",
|
|
56
|
+
"pathe": "1.1.2",
|
|
57
|
+
"pdfjs-dist": "4.10.38",
|
|
58
|
+
"radix-vue": "1.9.17",
|
|
59
|
+
"reka-ui": "2.3.0",
|
|
60
|
+
"remark-gfm": "4.0.0",
|
|
61
|
+
"remark-mdx": "3.1.1",
|
|
62
|
+
"remark-parse": "11.0.0",
|
|
63
|
+
"remark-stringify": "11.0.0",
|
|
64
|
+
"resize-observer-polyfill": "1.5.1",
|
|
65
|
+
"scule": "1.3.0",
|
|
66
|
+
"shiki": "2.5.0",
|
|
67
|
+
"tailwind-merge": "2.5.4",
|
|
68
|
+
"tailwindcss-animate": "1.0.7",
|
|
69
|
+
"ts-morph": "22.0.0",
|
|
70
|
+
"typescript": "5.8.2",
|
|
71
|
+
"unified": "11.0.5",
|
|
72
|
+
"unocss": "66.5.12",
|
|
73
|
+
"vue": "3.5.17",
|
|
74
|
+
"vue-component-meta": "3.0.8",
|
|
75
|
+
"vue-docgen-api": "4.78.0",
|
|
76
|
+
"vue-input-otp": "0.3.2",
|
|
77
|
+
"vue-router": "4.5.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"nuxt": "^3.17.0 || ^4.0.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@nuxt/kit": "3.17.7",
|
|
84
|
+
"@types/node": "18.19.79",
|
|
85
|
+
"fs-extra": "11.2.0",
|
|
86
|
+
"gray-matter": "4.0.3",
|
|
87
|
+
"nuxt": "3.17.7"
|
|
88
|
+
}
|
|
85
89
|
}
|