prev-cli 0.24.11 → 0.24.13

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.
@@ -0,0 +1,11 @@
1
+ import type { PreviewConfig } from './types';
2
+ export interface OptimizedBuildOptions {
3
+ vendorPath: string;
4
+ }
5
+ export interface OptimizedBuildResult {
6
+ success: boolean;
7
+ html: string;
8
+ css: string;
9
+ error?: string;
10
+ }
11
+ export declare function buildOptimizedPreview(config: PreviewConfig, options: OptimizedBuildOptions): Promise<OptimizedBuildResult>;
@@ -0,0 +1,11 @@
1
+ export interface TailwindResult {
2
+ success: boolean;
3
+ css: string;
4
+ error?: string;
5
+ }
6
+ interface ContentFile {
7
+ path: string;
8
+ content: string;
9
+ }
10
+ export declare function compileTailwind(files: ContentFile[]): Promise<TailwindResult>;
11
+ export {};
@@ -0,0 +1,6 @@
1
+ export interface VendorBundleResult {
2
+ success: boolean;
3
+ code: string;
4
+ error?: string;
5
+ }
6
+ export declare function buildVendorBundle(): Promise<VendorBundleResult>;
@@ -0,0 +1,13 @@
1
+ import { type PreviewConfig, type FlowDefinition, type AtlasDefinition } from './preview-types';
2
+ /**
3
+ * Parse a config.yaml or config.yml file and validate against schema
4
+ */
5
+ export declare function parsePreviewConfig(filePath: string): Promise<PreviewConfig | null>;
6
+ /**
7
+ * Parse a flow index.yaml file
8
+ */
9
+ export declare function parseFlowDefinition(filePath: string): Promise<FlowDefinition | null>;
10
+ /**
11
+ * Parse an atlas index.yaml file
12
+ */
13
+ export declare function parseAtlasDefinition(filePath: string): Promise<AtlasDefinition | null>;
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ export type PreviewType = 'component' | 'screen' | 'flow' | 'atlas';
3
+ export declare const configSchema: z.ZodObject<{
4
+ tags: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodString>, z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>]>>;
5
+ category: z.ZodOptional<z.ZodString>;
6
+ status: z.ZodOptional<z.ZodEnum<{
7
+ draft: "draft";
8
+ stable: "stable";
9
+ deprecated: "deprecated";
10
+ }>>;
11
+ title: z.ZodOptional<z.ZodString>;
12
+ description: z.ZodOptional<z.ZodString>;
13
+ order: z.ZodOptional<z.ZodNumber>;
14
+ }, z.core.$strip>;
15
+ export type PreviewConfig = z.infer<typeof configSchema>;
16
+ export interface PreviewUnit {
17
+ type: PreviewType;
18
+ name: string;
19
+ path: string;
20
+ route: string;
21
+ config: PreviewConfig | null;
22
+ files: {
23
+ index: string;
24
+ states?: string[];
25
+ schema?: string;
26
+ docs?: string;
27
+ };
28
+ }
29
+ export interface FlowStep {
30
+ screen: string;
31
+ state?: string;
32
+ note?: string;
33
+ trigger?: string;
34
+ highlight?: string[];
35
+ }
36
+ export interface FlowDefinition {
37
+ name: string;
38
+ description?: string;
39
+ steps: FlowStep[];
40
+ }
41
+ export interface AtlasArea {
42
+ title: string;
43
+ description?: string;
44
+ parent?: string;
45
+ children?: string[];
46
+ access?: string;
47
+ }
48
+ export interface AtlasDefinition {
49
+ name: string;
50
+ description?: string;
51
+ hierarchy: {
52
+ root: string;
53
+ areas: Record<string, AtlasArea>;
54
+ };
55
+ routes?: Record<string, {
56
+ area: string;
57
+ screen: string;
58
+ guard?: string;
59
+ }>;
60
+ navigation?: Record<string, Array<{
61
+ area?: string;
62
+ icon?: string;
63
+ action?: string;
64
+ }>>;
65
+ relationships?: Array<{
66
+ from: string;
67
+ to: string;
68
+ type: string;
69
+ }>;
70
+ }
@@ -1,4 +1,5 @@
1
1
  import type { PreviewFile, PreviewConfig } from '../preview-runtime/types';
2
+ import type { PreviewUnit } from './preview-types';
2
3
  export interface Preview {
3
4
  name: string;
4
5
  route: string;
@@ -21,3 +22,8 @@ export declare function detectEntry(files: PreviewFile[]): string;
21
22
  * Build a PreviewConfig for WASM runtime
22
23
  */
23
24
  export declare function buildPreviewConfig(previewDir: string): Promise<PreviewConfig>;
25
+ /**
26
+ * Scan previews with multi-type folder structure support
27
+ * Supports: components/, screens/, flows/, atlas/
28
+ */
29
+ export declare function scanPreviewUnits(rootDir: string): Promise<PreviewUnit[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.24.11",
3
+ "version": "0.24.13",
4
4
  "description": "Transform MDX directories into beautiful documentation websites",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -65,7 +65,8 @@
65
65
  "remark-gfm": "^4.0.0",
66
66
  "tailwind-merge": "^2.5.0",
67
67
  "tailwindcss": "^4.0.0",
68
- "vite": "npm:rolldown-vite@^7.3.1"
68
+ "vite": "npm:rolldown-vite@^7.3.1",
69
+ "zod": "^4.3.5"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@types/js-yaml": "^4.0.9",
@@ -0,0 +1,47 @@
1
+ import { test, expect } from 'bun:test'
2
+ import { buildOptimizedPreview } from './build-optimized'
3
+ import type { PreviewConfig } from './types'
4
+
5
+ // Tailwind CLI can be slow on first run
6
+ const TAILWIND_TIMEOUT = 30000
7
+
8
+ test('buildOptimizedPreview generates HTML with local vendor imports', async () => {
9
+ const config: PreviewConfig = {
10
+ files: [
11
+ {
12
+ path: 'index.tsx',
13
+ content: `export default function App() { return <div className="p-4">Hello</div> }`,
14
+ type: 'tsx',
15
+ },
16
+ ],
17
+ entry: 'index.tsx',
18
+ tailwind: true,
19
+ }
20
+
21
+ const result = await buildOptimizedPreview(config, { vendorPath: '../_vendors/runtime.js' })
22
+
23
+ expect(result.success).toBe(true)
24
+ expect(result.html).toContain('../_vendors/runtime.js')
25
+ expect(result.html).not.toContain('esm.sh')
26
+ expect(result.html).not.toContain('tailwindcss/browser')
27
+ }, TAILWIND_TIMEOUT)
28
+
29
+ test('buildOptimizedPreview includes compiled CSS', async () => {
30
+ const config: PreviewConfig = {
31
+ files: [
32
+ {
33
+ path: 'index.tsx',
34
+ content: `export default function App() { return <div className="flex items-center bg-red-500">Hello</div> }`,
35
+ type: 'tsx',
36
+ },
37
+ ],
38
+ entry: 'index.tsx',
39
+ tailwind: true,
40
+ }
41
+
42
+ const result = await buildOptimizedPreview(config, { vendorPath: '../_vendors/runtime.js' })
43
+
44
+ expect(result.success).toBe(true)
45
+ expect(result.css).toContain('flex')
46
+ expect(result.css).toContain('bg-red-500')
47
+ }, TAILWIND_TIMEOUT)
@@ -0,0 +1,136 @@
1
+ import { build } from 'esbuild'
2
+ import type { PreviewConfig } from './types'
3
+ import { compileTailwind } from './tailwind'
4
+
5
+ export interface OptimizedBuildOptions {
6
+ vendorPath: string
7
+ }
8
+
9
+ export interface OptimizedBuildResult {
10
+ success: boolean
11
+ html: string
12
+ css: string
13
+ error?: string
14
+ }
15
+
16
+ export async function buildOptimizedPreview(
17
+ config: PreviewConfig,
18
+ options: OptimizedBuildOptions
19
+ ): Promise<OptimizedBuildResult> {
20
+ try {
21
+ const virtualFs: Record<string, { contents: string; loader: string }> = {}
22
+ for (const file of config.files) {
23
+ const ext = file.path.split('.').pop()?.toLowerCase()
24
+ const loader = ext === 'css' ? 'css' : ext === 'json' ? 'json' : ext || 'tsx'
25
+ virtualFs[file.path] = { contents: file.content, loader }
26
+ }
27
+
28
+ const entryFile = config.files.find(f => f.path === config.entry)
29
+ if (!entryFile) {
30
+ return { success: false, html: '', css: '', error: `Entry file not found: ${config.entry}` }
31
+ }
32
+
33
+ const hasDefaultExport = /export\s+default/.test(entryFile.content)
34
+ const userCssCollected: string[] = []
35
+
36
+ const entryCode = hasDefaultExport
37
+ ? `
38
+ import React, { createRoot } from '${options.vendorPath}'
39
+ import App from './${config.entry}'
40
+ const root = createRoot(document.getElementById('root'))
41
+ root.render(React.createElement(App))
42
+ `
43
+ : `import './${config.entry}'`
44
+
45
+ const result = await build({
46
+ stdin: { contents: entryCode, loader: 'tsx', resolveDir: '/' },
47
+ bundle: true,
48
+ write: false,
49
+ format: 'esm',
50
+ jsx: 'automatic',
51
+ jsxImportSource: 'react',
52
+ target: 'es2020',
53
+ minify: true,
54
+ plugins: [
55
+ {
56
+ name: 'optimized-preview',
57
+ setup(build) {
58
+ // External: vendor runtime
59
+ build.onResolve(
60
+ { filter: new RegExp(options.vendorPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) },
61
+ args => {
62
+ return { path: args.path, external: true }
63
+ }
64
+ )
65
+
66
+ // External: React (map to vendor bundle)
67
+ build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, () => {
68
+ return { path: options.vendorPath, external: true }
69
+ })
70
+
71
+ // Resolve relative imports
72
+ build.onResolve({ filter: /^\./ }, args => {
73
+ let resolved = args.path.replace(/^\.\//, '')
74
+ if (!resolved.includes('.')) {
75
+ for (const ext of ['.tsx', '.ts', '.jsx', '.js', '.css']) {
76
+ if (virtualFs[resolved + ext]) {
77
+ resolved = resolved + ext
78
+ break
79
+ }
80
+ }
81
+ }
82
+ return { path: resolved, namespace: 'virtual' }
83
+ })
84
+
85
+ // Load from virtual FS
86
+ build.onLoad({ filter: /.*/, namespace: 'virtual' }, args => {
87
+ const file = virtualFs[args.path]
88
+ if (file) {
89
+ if (file.loader === 'css') {
90
+ userCssCollected.push(file.contents)
91
+ return { contents: '', loader: 'js' }
92
+ }
93
+ return { contents: file.contents, loader: file.loader as any }
94
+ }
95
+ return { contents: '', loader: 'empty' }
96
+ })
97
+ },
98
+ },
99
+ ],
100
+ })
101
+
102
+ const jsFile = result.outputFiles?.find(f => f.path.endsWith('.js')) || result.outputFiles?.[0]
103
+ const jsCode = jsFile?.text || ''
104
+
105
+ let css = ''
106
+ if (config.tailwind) {
107
+ const tailwindResult = await compileTailwind(
108
+ config.files.map(f => ({ path: f.path, content: f.content }))
109
+ )
110
+ if (tailwindResult.success) css = tailwindResult.css
111
+ }
112
+
113
+ const userCss = userCssCollected.join('\n')
114
+ const allCss = css + '\n' + userCss
115
+
116
+ const html = `<!DOCTYPE html>
117
+ <html lang="en">
118
+ <head>
119
+ <meta charset="UTF-8">
120
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
121
+ <title>Preview</title>
122
+ <style>${allCss}</style>
123
+ <style>body { margin: 0; } #root { min-height: 100vh; }</style>
124
+ </head>
125
+ <body>
126
+ <div id="root"></div>
127
+ <script type="module" src="${options.vendorPath}"></script>
128
+ <script type="module">${jsCode}</script>
129
+ </body>
130
+ </html>`
131
+
132
+ return { success: true, html, css: allCss }
133
+ } catch (err) {
134
+ return { success: false, html: '', css: '', error: err instanceof Error ? err.message : String(err) }
135
+ }
136
+ }
@@ -0,0 +1,30 @@
1
+ import { test, expect } from 'bun:test'
2
+ import { compileTailwind } from './tailwind'
3
+
4
+ test('compileTailwind extracts used classes from content', async () => {
5
+ const content = `
6
+ export default function App() {
7
+ return <div className="flex items-center p-4 bg-blue-500">Hello</div>
8
+ }
9
+ `
10
+
11
+ const result = await compileTailwind([{ path: 'App.tsx', content }])
12
+
13
+ expect(result.success).toBe(true)
14
+ expect(result.css).toBeDefined()
15
+ expect(result.css).toContain('flex')
16
+ expect(result.css).toContain('items-center')
17
+ expect(result.css).toContain('bg-blue-500')
18
+ })
19
+
20
+ test('compileTailwind returns empty CSS for no Tailwind classes', async () => {
21
+ const content = `
22
+ export default function App() {
23
+ return <div style={{ color: 'red' }}>Hello</div>
24
+ }
25
+ `
26
+
27
+ const result = await compileTailwind([{ path: 'App.tsx', content }])
28
+
29
+ expect(result.success).toBe(true)
30
+ })
@@ -0,0 +1,64 @@
1
+ import { $ } from 'bun'
2
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'
3
+ import { join, dirname } from 'path'
4
+ import { tmpdir } from 'os'
5
+
6
+ export interface TailwindResult {
7
+ success: boolean
8
+ css: string
9
+ error?: string
10
+ }
11
+
12
+ interface ContentFile {
13
+ path: string
14
+ content: string
15
+ }
16
+
17
+ export async function compileTailwind(files: ContentFile[]): Promise<TailwindResult> {
18
+ const tempDir = mkdtempSync(join(tmpdir(), 'prev-tailwind-'))
19
+
20
+ try {
21
+ // Write content files (create parent dirs for nested paths)
22
+ for (const file of files) {
23
+ const filePath = join(tempDir, file.path)
24
+ const parentDir = dirname(filePath)
25
+ mkdirSync(parentDir, { recursive: true })
26
+ writeFileSync(filePath, file.content)
27
+ }
28
+
29
+ // Create Tailwind config - use .cjs for compatibility
30
+ const configContent = `
31
+ module.exports = {
32
+ content: [${JSON.stringify(tempDir + '/**/*.{tsx,jsx,ts,js,html}')}],
33
+ }
34
+ `
35
+ const configPath = join(tempDir, 'tailwind.config.cjs')
36
+ writeFileSync(configPath, configContent)
37
+
38
+ // Create input CSS
39
+ const inputCss = `
40
+ @tailwind base;
41
+ @tailwind components;
42
+ @tailwind utilities;
43
+ `
44
+ const inputPath = join(tempDir, 'input.css')
45
+ writeFileSync(inputPath, inputCss)
46
+
47
+ const outputPath = join(tempDir, 'output.css')
48
+
49
+ // Run Tailwind CLI
50
+ await $`bunx tailwindcss -c ${configPath} -i ${inputPath} -o ${outputPath} --minify`.quiet()
51
+
52
+ const css = readFileSync(outputPath, 'utf-8')
53
+
54
+ return { success: true, css }
55
+ } catch (err) {
56
+ return {
57
+ success: false,
58
+ css: '',
59
+ error: err instanceof Error ? err.message : String(err),
60
+ }
61
+ } finally {
62
+ rmSync(tempDir, { recursive: true, force: true })
63
+ }
64
+ }
@@ -0,0 +1,15 @@
1
+ import { test, expect } from 'bun:test'
2
+ import { buildVendorBundle } from './vendors'
3
+
4
+ test('buildVendorBundle creates runtime.js with React', async () => {
5
+ const result = await buildVendorBundle()
6
+ expect(result.success).toBe(true)
7
+ expect(result.code).toBeDefined()
8
+ expect(result.code).toContain('createElement')
9
+ expect(result.code).toContain('createRoot')
10
+ })
11
+
12
+ test('buildVendorBundle output is valid ESM', async () => {
13
+ const result = await buildVendorBundle()
14
+ expect(result.code).toContain('export')
15
+ })
@@ -0,0 +1,52 @@
1
+ import { build } from 'esbuild'
2
+ import { dirname } from 'path'
3
+ import { fileURLToPath } from 'url'
4
+
5
+ // Resolve from CLI's location, not user's project (React is our dependency)
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+
8
+ export interface VendorBundleResult {
9
+ success: boolean
10
+ code: string
11
+ error?: string
12
+ }
13
+
14
+ export async function buildVendorBundle(): Promise<VendorBundleResult> {
15
+ try {
16
+ const entryCode = `
17
+ import * as React from 'react'
18
+ import * as ReactDOM from 'react-dom'
19
+ import { createRoot } from 'react-dom/client'
20
+ export { jsx, jsxs, Fragment } from 'react/jsx-runtime'
21
+ export { React, ReactDOM, createRoot }
22
+ export default React
23
+ `
24
+
25
+ const result = await build({
26
+ stdin: {
27
+ contents: entryCode,
28
+ loader: 'ts',
29
+ resolveDir: __dirname, // Resolve React from CLI's node_modules
30
+ },
31
+ bundle: true,
32
+ write: false,
33
+ format: 'esm',
34
+ target: 'es2020',
35
+ minify: true,
36
+ })
37
+
38
+ // Select JS output file explicitly (in case sourcemaps are added later)
39
+ const jsFile = result.outputFiles?.find(f => f.path.endsWith('.js')) || result.outputFiles?.[0]
40
+ if (!jsFile) {
41
+ return { success: false, code: '', error: 'No output generated' }
42
+ }
43
+
44
+ return { success: true, code: jsFile.text }
45
+ } catch (err) {
46
+ return {
47
+ success: false,
48
+ code: '',
49
+ error: err instanceof Error ? err.message : String(err),
50
+ }
51
+ }
52
+ }