prev-cli 0.24.14 → 0.24.16

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 (44) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.js +4474 -1673
  3. package/dist/jsx/adapters/html.d.ts +15 -0
  4. package/dist/jsx/adapters/react.d.ts +26 -0
  5. package/dist/jsx/define-component.d.ts +68 -0
  6. package/dist/jsx/index.d.ts +7 -0
  7. package/dist/jsx/jsx-runtime.d.ts +34 -0
  8. package/dist/jsx/migrate.d.ts +24 -0
  9. package/dist/jsx/schemas/index.d.ts +2 -0
  10. package/dist/jsx/schemas/primitives.d.ts +355 -0
  11. package/dist/jsx/schemas/tokens.d.ts +79 -0
  12. package/dist/jsx/validation.d.ts +28 -0
  13. package/dist/jsx/vnode.d.ts +58 -0
  14. package/dist/migrate.d.ts +18 -0
  15. package/dist/primitives/index.d.ts +5 -0
  16. package/dist/primitives/migrate.d.ts +25 -0
  17. package/dist/primitives/parser.d.ts +32 -0
  18. package/dist/primitives/template-parser.d.ts +33 -0
  19. package/dist/primitives/template-renderer.d.ts +19 -0
  20. package/dist/primitives/types.d.ts +164 -0
  21. package/dist/renderers/html/index.d.ts +6 -0
  22. package/dist/renderers/index.d.ts +5 -0
  23. package/dist/renderers/react/index.d.ts +6 -0
  24. package/dist/renderers/registry.d.ts +41 -0
  25. package/dist/renderers/render.d.ts +35 -0
  26. package/dist/renderers/types.d.ts +188 -0
  27. package/dist/tokens/defaults.d.ts +2 -0
  28. package/dist/tokens/resolver.d.ts +67 -0
  29. package/dist/tokens/utils.d.ts +15 -0
  30. package/dist/tokens/validation.d.ts +36 -0
  31. package/dist/typecheck/index.d.ts +24 -0
  32. package/dist/ui/button.d.ts +1 -1
  33. package/dist/validators/index.d.ts +59 -0
  34. package/dist/validators/schema-validator.d.ts +27 -0
  35. package/dist/validators/semantic-validator.d.ts +47 -0
  36. package/dist/vite/plugins/tokens-plugin.d.ts +2 -0
  37. package/package.json +8 -4
  38. package/src/preview-runtime/build-optimized.ts +6 -1
  39. package/src/preview-runtime/fast-template.html +115 -0
  40. package/src/preview-runtime/tailwind.ts +22 -7
  41. package/src/preview-runtime/template.html +50 -8
  42. package/src/preview-runtime/vendors.ts +9 -0
  43. package/src/theme/entry.tsx +153 -12
  44. package/src/theme/previews/TokensPage.tsx +328 -0
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Token configuration interface matching shadcn design system
3
+ */
4
+ export interface TokensConfig {
5
+ colors: Record<string, string>;
6
+ backgrounds: Record<string, string>;
7
+ spacing: Record<string, string>;
8
+ typography: {
9
+ sizes: Record<string, string>;
10
+ weights: Record<string, number>;
11
+ };
12
+ radius: Record<string, string>;
13
+ shadows: Record<string, string>;
14
+ }
15
+ /**
16
+ * Partial token configuration for user overrides
17
+ * All fields are optional and can be deeply nested
18
+ */
19
+ export type PartialTokensConfig = {
20
+ colors?: Record<string, string | null>;
21
+ backgrounds?: Record<string, string | null>;
22
+ spacing?: Record<string, string | null>;
23
+ typography?: {
24
+ sizes?: Record<string, string | null>;
25
+ weights?: Record<string, number | null>;
26
+ };
27
+ radius?: Record<string, string | null>;
28
+ shadows?: Record<string, string | null>;
29
+ };
30
+ /**
31
+ * Options for resolving tokens
32
+ */
33
+ export interface ResolveTokensOptions {
34
+ /** Path to user's tokens.yaml file */
35
+ userTokensPath?: string;
36
+ /** Inline user token overrides (takes precedence over file) */
37
+ userTokens?: PartialTokensConfig;
38
+ /** Path to defaults.yaml (defaults to bundled defaults) */
39
+ defaultsPath?: string;
40
+ }
41
+ /**
42
+ * Load and parse a YAML tokens file (full validation)
43
+ * @param filePath - Absolute path to the YAML file
44
+ * @returns Parsed tokens configuration
45
+ * @throws Error if file doesn't exist or contains invalid YAML
46
+ */
47
+ export declare function loadTokens(filePath: string): TokensConfig;
48
+ /**
49
+ * Resolve tokens by merging defaults with user overrides
50
+ *
51
+ * Resolution order (later wins):
52
+ * 1. Bundled default tokens
53
+ * 2. User tokens from file (if userTokensPath provided)
54
+ * 3. Inline user tokens (if userTokens provided)
55
+ *
56
+ * @param options - Resolution options
57
+ * @returns Fully resolved tokens configuration
58
+ */
59
+ export declare function resolveTokens(options?: ResolveTokensOptions): TokensConfig;
60
+ /**
61
+ * Convenience function to resolve tokens from a project directory
62
+ * Looks for tokens.yaml in the project root
63
+ *
64
+ * @param projectDir - Path to the project directory
65
+ * @returns Resolved tokens configuration
66
+ */
67
+ export declare function resolveProjectTokens(projectDir: string): TokensConfig;
@@ -0,0 +1,15 @@
1
+ export declare function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T> | Record<string, unknown>): T;
2
+ /**
3
+ * Type-safe deep merge for token configurations.
4
+ * This is a specialized version of deepMerge that handles TokensConfig type properly.
5
+ */
6
+ export declare function mergeTokenConfigs<T>(target: T, source: unknown): T;
7
+ /**
8
+ * Calculate the Levenshtein distance between two strings.
9
+ * Used for "did you mean?" suggestions in validation errors.
10
+ *
11
+ * @param a - First string
12
+ * @param b - Second string
13
+ * @returns The edit distance between the strings
14
+ */
15
+ export declare function levenshtein(a: string, b: string): number;
@@ -0,0 +1,36 @@
1
+ import { type TokensConfig } from './resolver';
2
+ /**
3
+ * Error thrown when a token validation fails.
4
+ * Includes helpful suggestions for similar valid tokens.
5
+ */
6
+ export declare class ValidationError extends Error {
7
+ readonly category: string;
8
+ readonly invalidToken: string;
9
+ readonly suggestions: string[];
10
+ constructor(message: string, category: string, invalidToken: string, suggestions: string[]);
11
+ }
12
+ /**
13
+ * Valid category names that can be used with validateToken.
14
+ * Supports dot notation for nested categories like "typography.sizes"
15
+ */
16
+ export type TokenCategory = 'colors' | 'backgrounds' | 'spacing' | 'typography.sizes' | 'typography.weights' | 'radius' | 'shadows';
17
+ /**
18
+ * Validate that a token name is valid for a given category.
19
+ * Throws a ValidationError with helpful suggestions if invalid.
20
+ *
21
+ * @param category - The token category (e.g., "colors", "typography.sizes")
22
+ * @param tokenName - The token name to validate
23
+ * @param config - Optional pre-resolved tokens config (defaults to resolveTokens())
24
+ * @throws ValidationError if the token is invalid
25
+ * @throws Error if the category is invalid
26
+ */
27
+ export declare function validateToken(category: TokenCategory, tokenName: string, config?: TokensConfig): void;
28
+ /**
29
+ * Check if a token is valid without throwing.
30
+ *
31
+ * @param category - The token category
32
+ * @param tokenName - The token name to check
33
+ * @param config - Optional pre-resolved tokens config
34
+ * @returns true if valid, false if invalid
35
+ */
36
+ export declare function isValidToken(category: TokenCategory, tokenName: string, config?: TokensConfig): boolean;
@@ -0,0 +1,24 @@
1
+ export interface TypecheckOptions {
2
+ /** Directory containing preview files to check */
3
+ previewsDir?: string;
4
+ /** Patterns to include (default: ["**\/*.{ts,tsx}"]) */
5
+ include?: string[];
6
+ /** Enable strict mode (default: true) */
7
+ strict?: boolean;
8
+ /** Show verbose output */
9
+ verbose?: boolean;
10
+ }
11
+ export interface TypecheckResult {
12
+ success: boolean;
13
+ fileCount: number;
14
+ errorCount: number;
15
+ output: string;
16
+ }
17
+ /**
18
+ * Run type checking on preview files using embedded tsgo
19
+ */
20
+ export declare function typecheck(rootDir: string, options?: TypecheckOptions): Promise<TypecheckResult>;
21
+ /**
22
+ * Format typecheck result for CLI output
23
+ */
24
+ export declare function formatTypecheckResult(result: TypecheckResult): string;
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { type VariantProps } from 'class-variance-authority';
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
4
+ variant?: "link" | "default" | "secondary" | "destructive" | "outline" | "ghost" | null | undefined;
5
5
  size?: "default" | "sm" | "lg" | "icon" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
@@ -0,0 +1,59 @@
1
+ import { validateConfig, validateAllLayouts, clearSchemaCache, type SchemaValidationResult, type SchemaValidationError } from './schema-validator';
2
+ import { validateSemantics, createValidationContext, registerPreviewUnit, checkDuplicateIds, type SemanticValidationResult, type SemanticValidationError, type SemanticValidationWarning, type ValidationContext } from './semantic-validator';
3
+ export interface ValidationOptions {
4
+ /** Specific renderer to validate (validates all if not specified) */
5
+ renderer?: string;
6
+ /** Only validate schema (skip semantic validation) */
7
+ schemaOnly?: boolean;
8
+ /** Only validate semantic rules (skip schema validation) */
9
+ semanticOnly?: boolean;
10
+ }
11
+ export interface ValidationResult {
12
+ valid: boolean;
13
+ summary: {
14
+ components: {
15
+ total: number;
16
+ valid: number;
17
+ invalid: number;
18
+ };
19
+ screens: {
20
+ total: number;
21
+ valid: number;
22
+ invalid: number;
23
+ };
24
+ flows: {
25
+ total: number;
26
+ valid: number;
27
+ invalid: number;
28
+ };
29
+ atlas: {
30
+ total: number;
31
+ valid: number;
32
+ invalid: number;
33
+ };
34
+ };
35
+ errors: ValidationError[];
36
+ warnings: ValidationWarning[];
37
+ }
38
+ export interface ValidationError {
39
+ file: string;
40
+ path: string;
41
+ message: string;
42
+ type: 'schema' | 'semantic';
43
+ }
44
+ export interface ValidationWarning {
45
+ file: string;
46
+ path: string;
47
+ message: string;
48
+ }
49
+ /**
50
+ * Validate all preview configs
51
+ */
52
+ export declare function validate(rootDir?: string, options?: ValidationOptions): Promise<ValidationResult>;
53
+ /**
54
+ * Format validation result for CLI output
55
+ */
56
+ export declare function formatValidationResult(result: ValidationResult): string;
57
+ export type { SchemaValidationResult, SchemaValidationError, SemanticValidationResult, SemanticValidationError, SemanticValidationWarning, ValidationContext, };
58
+ export { validateConfig, validateAllLayouts, clearSchemaCache };
59
+ export { validateSemantics, createValidationContext, registerPreviewUnit, checkDuplicateIds };
@@ -0,0 +1,27 @@
1
+ export interface SchemaValidationResult {
2
+ valid: boolean;
3
+ errors: SchemaValidationError[];
4
+ }
5
+ export interface SchemaValidationError {
6
+ path: string;
7
+ message: string;
8
+ keyword: string;
9
+ }
10
+ /**
11
+ * Validate a config object against the preview schema
12
+ */
13
+ export declare function validateConfig(config: Record<string, unknown>): SchemaValidationResult;
14
+ /**
15
+ * Validate a layout subtree against a specific renderer's layout schema
16
+ * Accepts both array and object layouts (design allows adapters to use either)
17
+ */
18
+ export declare function validateLayoutForRenderer(layout: unknown, rendererName: string): SchemaValidationResult;
19
+ /**
20
+ * Validate all layoutByRenderer entries against their respective adapter schemas
21
+ * Accepts both array and object layouts per the design spec
22
+ */
23
+ export declare function validateAllLayouts(layoutByRenderer: Record<string, unknown> | undefined, targetRenderer?: string): SchemaValidationResult;
24
+ /**
25
+ * Clear the schema cache (for testing)
26
+ */
27
+ export declare function clearSchemaCache(): void;
@@ -0,0 +1,47 @@
1
+ import type { PreviewConfig } from '../renderers/types';
2
+ export interface SemanticValidationResult {
3
+ valid: boolean;
4
+ errors: SemanticValidationError[];
5
+ warnings: SemanticValidationWarning[];
6
+ }
7
+ export interface SemanticValidationError {
8
+ path: string;
9
+ message: string;
10
+ code: SemanticErrorCode;
11
+ }
12
+ export interface SemanticValidationWarning {
13
+ path: string;
14
+ message: string;
15
+ code: SemanticWarningCode;
16
+ }
17
+ export type SemanticErrorCode = 'DUPLICATE_ID' | 'INVALID_REF' | 'INVALID_STATE_REF' | 'INVALID_STEP_REF' | 'INVALID_NODE_REF' | 'CIRCULAR_DEPENDENCY' | 'UNKNOWN_RENDERER' | 'INVALID_TEMPLATE' | 'INVALID_SLOT' | 'MISSING_SLOT_DEFINITION';
18
+ export type SemanticWarningCode = 'DEPRECATED_STATUS' | 'MISSING_DESCRIPTION';
19
+ export interface ValidationContext {
20
+ /** Root directory containing previews folder */
21
+ rootDir: string;
22
+ /** Map of all known preview IDs by type */
23
+ knownIds: {
24
+ components: Set<string>;
25
+ screens: Set<string>;
26
+ flows: Set<string>;
27
+ atlas: Set<string>;
28
+ };
29
+ /** Map of screen states by screen ID */
30
+ screenStates: Map<string, Set<string>>;
31
+ }
32
+ /**
33
+ * Validate a config against semantic rules
34
+ */
35
+ export declare function validateSemantics(config: PreviewConfig, context: ValidationContext, configPath?: string): SemanticValidationResult;
36
+ /**
37
+ * Create an empty validation context
38
+ */
39
+ export declare function createValidationContext(rootDir: string): ValidationContext;
40
+ /**
41
+ * Register a preview unit in the validation context
42
+ */
43
+ export declare function registerPreviewUnit(context: ValidationContext, type: 'component' | 'screen' | 'flow' | 'atlas', id: string, states?: string[]): void;
44
+ /**
45
+ * Check for duplicate IDs within a type
46
+ */
47
+ export declare function checkDuplicateIds(ids: string[], type: string): SemanticValidationError[];
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare function tokensPlugin(rootDir: string): Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prev-cli",
3
- "version": "0.24.14",
3
+ "version": "0.24.16",
4
4
  "description": "Transform MDX directories into beautiful documentation websites",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "node": ">=18"
35
35
  },
36
36
  "scripts": {
37
- "build": "bun build src/cli.ts --outdir dist --target node --packages external && tsc --emitDeclarationOnly",
37
+ "build": "bun build src/cli.ts --outdir dist --target bun --packages external --banner 'if(typeof globalThis.Bun===\"undefined\"){console.error(\"\\n\\x1b[31mError: prev-cli requires Bun runtime\\x1b[0m\\n\\nYou are running with Node.js, but prev-cli uses Bun-specific APIs.\\n\\n\\x1b[33mTo install and run with Bun:\\x1b[0m\\n\\n # Global install (recommended)\\n bun i -g prev-cli\\n prev-cli dev\\n\\n # Or local install\\n bun add -d prev-cli\\n bunx --bun prev-cli dev\\n\\n\\x1b[90mLearn more: https://bun.sh/docs/installation\\x1b[0m\\n\");process.exit(1)}' && tsc --emitDeclarationOnly",
38
38
  "build:docs": "bun run build && bun ./dist/cli.js build",
39
39
  "prepublishOnly": "bun run build",
40
40
  "dev": "tsc --watch",
@@ -43,12 +43,17 @@
43
43
  "test:all": "bun run test && bun run test:integration"
44
44
  },
45
45
  "dependencies": {
46
+ "@types/react": "^19.0.0",
47
+ "@types/react-dom": "^19.0.0",
48
+ "@typescript/native-preview": "^7.0.0-dev",
46
49
  "@mdx-js/react": "^3.1.1",
47
50
  "@mdx-js/rollup": "^3.0.0",
48
51
  "@tailwindcss/vite": "^4.0.0",
49
52
  "@tanstack/react-router": "^1.145.7",
50
53
  "@terrastruct/d2": "^0.1.33",
51
54
  "@vitejs/plugin-react": "^5.1.2",
55
+ "ajv": "^8.17.1",
56
+ "ajv-formats": "^3.0.1",
52
57
  "class-variance-authority": "^0.7.0",
53
58
  "clsx": "^2.1.0",
54
59
  "esbuild": "^0.27.2",
@@ -70,10 +75,9 @@
70
75
  },
71
76
  "devDependencies": {
72
77
  "@types/js-yaml": "^4.0.9",
78
+ "@types/json-schema": "^7.0.15",
73
79
  "@types/node": "^22.0.0",
74
80
  "@types/picomatch": "^4.0.2",
75
- "@types/react": "^19.0.0",
76
- "@types/react-dom": "^19.0.0",
77
81
  "bun-types": "^1.3.5",
78
82
  "typescript": "^5.7.0"
79
83
  }
@@ -110,7 +110,12 @@ export async function buildOptimizedPreview(
110
110
  if (tailwindResult.success) css = tailwindResult.css
111
111
  }
112
112
 
113
- const userCss = userCssCollected.join('\n')
113
+ // Strip @import "tailwindcss" from user CSS when Tailwind is compiled
114
+ // (the import is Tailwind v4 dev syntax, not valid for static HTML)
115
+ let userCss = userCssCollected.join('\n')
116
+ if (config.tailwind) {
117
+ userCss = userCss.replace(/@import\s*["']tailwindcss["']\s*;?/g, '')
118
+ }
114
119
  const allCss = css + '\n' + userCss
115
120
 
116
121
  const html = `<!DOCTYPE html>
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Preview</title>
7
+ <!-- Tailwind CSS v4 Play CDN -->
8
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
9
+ <!-- Preload React -->
10
+ <link rel="modulepreload" href="https://esm.sh/react@18">
11
+ <link rel="modulepreload" href="https://esm.sh/react-dom@18/client">
12
+ <link rel="modulepreload" href="https://esm.sh/react@18/jsx-runtime">
13
+ <style>
14
+ body { margin: 0; }
15
+ #root { min-height: 100vh; }
16
+ .preview-loading {
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ min-height: 100vh;
21
+ font-family: system-ui, sans-serif;
22
+ color: #666;
23
+ }
24
+ .preview-error {
25
+ padding: 1rem;
26
+ background: #fef2f2;
27
+ color: #dc2626;
28
+ font-family: monospace;
29
+ font-size: 13px;
30
+ white-space: pre-wrap;
31
+ }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <div id="root"><div class="preview-loading">Loading preview...</div></div>
36
+
37
+ <!-- Import map must be BEFORE any module scripts -->
38
+ <script type="importmap">
39
+ {
40
+ "imports": {
41
+ "react": "https://esm.sh/react@18",
42
+ "react-dom": "https://esm.sh/react-dom@18",
43
+ "react-dom/client": "https://esm.sh/react-dom@18/client",
44
+ "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime"
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <script type="module">
50
+ // Fast preview runtime - uses server-side bundled code
51
+ const params = new URLSearchParams(window.location.search)
52
+ const src = params.get('src')
53
+
54
+ if (!src) {
55
+ document.getElementById('root').innerHTML = '<div class="preview-error">No preview source specified</div>'
56
+ } else {
57
+ const startTime = performance.now()
58
+
59
+ try {
60
+ // Pre-fetch tokens, JSX, and bundle in parallel
61
+ const [tokensRes, jsxModule, bundleResponse] = await Promise.all([
62
+ fetch('/_prev/tokens.json').then(r => r.json()).catch(() => null),
63
+ import(`${window.location.origin}/_prev/jsx.js`),
64
+ fetch(`/_preview-bundle/${src}`)
65
+ ])
66
+
67
+ // Set tokens
68
+ if (tokensRes && jsxModule.setTokensConfig) {
69
+ jsxModule.setTokensConfig(tokensRes)
70
+ }
71
+
72
+ const bundleTime = bundleResponse.headers.get('X-Bundle-Time') || '?'
73
+
74
+ if (!bundleResponse.ok) {
75
+ throw new Error(`Bundle failed: ${await bundleResponse.text()}`)
76
+ }
77
+
78
+ // Bundle has React imports rewritten, but @prev/jsx needs client rewrite
79
+ let code = await bundleResponse.text()
80
+ code = code.replace(/from\s*["']@prev\/jsx["']/g, `from "${window.location.origin}/_prev/jsx.js"`)
81
+
82
+ // Create module blob and import it
83
+ const blob = new Blob([code], { type: 'application/javascript' })
84
+ const moduleUrl = URL.createObjectURL(blob)
85
+
86
+ const module = await import(moduleUrl)
87
+ URL.revokeObjectURL(moduleUrl)
88
+
89
+ // Render
90
+ const { createRoot } = await import('https://esm.sh/react-dom@18/client')
91
+ const React = await import('https://esm.sh/react@18')
92
+
93
+ const App = module.default
94
+ if (App) {
95
+ const root = createRoot(document.getElementById('root'))
96
+ root.render(React.createElement(App))
97
+ }
98
+
99
+ const totalTime = Math.round(performance.now() - startTime)
100
+ parent.postMessage({
101
+ type: 'built',
102
+ result: { success: true, buildTime: parseInt(bundleTime, 10) || totalTime }
103
+ }, '*')
104
+
105
+ } catch (err) {
106
+ document.getElementById('root').innerHTML = `<div class="preview-error">${err.message}</div>`
107
+ parent.postMessage({ type: 'error', error: err.message }, '*')
108
+ }
109
+ }
110
+
111
+ // Signal ready immediately
112
+ parent.postMessage({ type: 'ready' }, '*')
113
+ </script>
114
+ </body>
115
+ </html>
@@ -1,12 +1,23 @@
1
1
  import { $ } from 'bun'
2
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'
2
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'fs'
3
3
  import { join, dirname } from 'path'
4
4
  import { tmpdir } from 'os'
5
- import { fileURLToPath } from 'url'
6
5
 
7
- // Resolve tailwindcss CLI from this package's node_modules
8
- const __dirname = dirname(fileURLToPath(import.meta.url))
9
- const tailwindBin = join(__dirname, '../../node_modules/.bin/tailwindcss')
6
+ // Resolve tailwindcss CLI - try multiple locations
7
+ function findTailwindBin(): string {
8
+ // Try require.resolve to find the tailwindcss package
9
+ try {
10
+ const tailwindPkg = require.resolve('tailwindcss/package.json')
11
+ const tailwindDir = dirname(tailwindPkg)
12
+ const binPath = join(tailwindDir, 'lib/cli.js')
13
+ if (existsSync(binPath)) return binPath
14
+ } catch {}
15
+
16
+ // Fallback: use bunx (slower but always works)
17
+ return 'bunx tailwindcss@3'
18
+ }
19
+
20
+ const tailwindCmd = findTailwindBin()
10
21
 
11
22
  export interface TailwindResult {
12
23
  success: boolean
@@ -51,8 +62,12 @@ export async function compileTailwind(files: ContentFile[]): Promise<TailwindRes
51
62
 
52
63
  const outputPath = join(tempDir, 'output.css')
53
64
 
54
- // Run Tailwind CLI from package's node_modules
55
- await $`${tailwindBin} -c ${configPath} -i ${inputPath} -o ${outputPath} --minify`.quiet()
65
+ // Run Tailwind CLI
66
+ if (tailwindCmd.startsWith('bunx')) {
67
+ await $`bunx tailwindcss@3 -c ${configPath} -i ${inputPath} -o ${outputPath} --minify`.quiet()
68
+ } else {
69
+ await $`bun ${tailwindCmd} -c ${configPath} -i ${inputPath} -o ${outputPath} --minify`.quiet()
70
+ }
56
71
 
57
72
  const css = readFileSync(outputPath, 'utf-8')
58
73
 
@@ -4,6 +4,10 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Preview</title>
7
+ <!-- Preload React for faster module resolution -->
8
+ <link rel="modulepreload" href="https://esm.sh/react@18">
9
+ <link rel="modulepreload" href="https://esm.sh/react-dom@18/client">
10
+ <link rel="modulepreload" href="https://esm.sh/react@18/jsx-runtime">
7
11
  <!-- Tailwind CSS v4 Play CDN -->
8
12
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
9
13
  <!-- esbuild-wasm -->
@@ -35,7 +39,14 @@
35
39
  <script type="module">
36
40
  // Preview runtime - bundles React/TSX in browser via esbuild-wasm
37
41
 
38
- let initialized = false
42
+ // Global error handler to catch module errors
43
+ window.onerror = (msg, url, line, col, error) => {
44
+ document.getElementById('root').innerHTML = `<div class="preview-error">Error: ${msg}\n${url}:${line}:${col}</div>`
45
+ return false
46
+ }
47
+
48
+ let esbuildReady = null // Promise that resolves when esbuild is ready
49
+ let tokensReady = null // Promise that resolves with tokens data
39
50
  let lastConfig = null
40
51
 
41
52
  function getLoader(filePath) {
@@ -44,12 +55,18 @@
44
55
  return loaders[ext] || 'tsx'
45
56
  }
46
57
 
58
+ // Pre-initialize esbuild immediately on page load
59
+ esbuildReady = esbuild.initialize({
60
+ wasmURL: 'https://unpkg.com/esbuild-wasm@0.24.2/esbuild.wasm',
61
+ }).catch(err => {
62
+ console.error('Failed to initialize esbuild:', err)
63
+ })
64
+
65
+ // Pre-fetch tokens in parallel
66
+ tokensReady = fetch('/_prev/tokens.json').then(r => r.json()).catch(() => null)
67
+
47
68
  async function initEsbuild() {
48
- if (initialized) return
49
- await esbuild.initialize({
50
- wasmURL: 'https://unpkg.com/esbuild-wasm@0.24.2/esbuild.wasm',
51
- })
52
- initialized = true
69
+ await esbuildReady
53
70
  }
54
71
 
55
72
  async function bundle(config) {
@@ -76,16 +93,30 @@
76
93
  // Determine if entry exports default or needs wrapping
77
94
  const hasDefaultExport = /export\s+default/.test(entryFile.content)
78
95
 
96
+ // Wait for pre-fetched tokens
97
+ const tokens = await tokensReady
98
+
79
99
  const entryCode = hasDefaultExport ? `
80
100
  import React from 'react'
81
101
  import { createRoot } from 'react-dom/client'
102
+ import { setTokensConfig } from '@prev/jsx'
82
103
  import App from './${config.entry}'
83
104
 
105
+ // Inject pre-fetched tokens
106
+ const tokens = ${JSON.stringify(tokens)}
107
+ if (tokens) setTokensConfig(tokens)
108
+
84
109
  const root = createRoot(document.getElementById('root'))
85
110
  root.render(React.createElement(App))
86
111
  ` : `
87
112
  // Entry file doesn't export default, execute directly
88
- import './${config.entry}'
113
+ import { setTokensConfig } from '@prev/jsx'
114
+
115
+ // Inject pre-fetched tokens
116
+ const tokens = ${JSON.stringify(tokens)}
117
+ if (tokens) setTokensConfig(tokens)
118
+
119
+ import('./${config.entry}')
89
120
  `
90
121
 
91
122
  const result = await esbuild.build({
@@ -114,9 +145,20 @@
114
145
  return { path: url, external: true }
115
146
  })
116
147
 
148
+ // Resolve @prev/jsx to pre-bundled primitives
149
+ build.onResolve({ filter: /^@prev\/jsx$/ }, args => {
150
+ return { path: `${window.location.origin}/_prev/jsx.js`, external: true }
151
+ })
152
+
153
+ // Resolve @prev/components/* to pre-bundled components
154
+ build.onResolve({ filter: /^@prev\/components\/(.+)$/ }, args => {
155
+ const componentName = args.path.replace('@prev/components/', '')
156
+ return { path: `${window.location.origin}/_prev/components/${componentName}.js`, external: true }
157
+ })
158
+
117
159
  // Auto-resolve npm packages via esm.sh
118
160
  build.onResolve({ filter: /^[^./]/ }, args => {
119
- if (args.path.startsWith('https://')) return
161
+ if (args.path.startsWith('https://') || args.path.startsWith('/')) return
120
162
  return { path: `https://esm.sh/${args.path}`, external: true }
121
163
  })
122
164
 
@@ -19,6 +19,15 @@ export async function buildVendorBundle(): Promise<VendorBundleResult> {
19
19
  import { createRoot } from 'react-dom/client'
20
20
  export { jsx, jsxs, Fragment } from 'react/jsx-runtime'
21
21
  export { React, ReactDOM, createRoot }
22
+ // Re-export React hooks as named exports (preview code imports them directly)
23
+ export {
24
+ useState, useEffect, useCallback, useMemo, useRef,
25
+ useContext, useReducer, useLayoutEffect, useInsertionEffect,
26
+ useTransition, useDeferredValue, useId, useSyncExternalStore,
27
+ useImperativeHandle, useDebugValue, memo, forwardRef,
28
+ createContext, createRef, lazy, Suspense, startTransition,
29
+ Children, cloneElement, isValidElement, createElement
30
+ } from 'react'
22
31
  export default React
23
32
  `
24
33