prev-cli 0.24.15 → 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.
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4465 -1681
- package/dist/jsx/adapters/html.d.ts +15 -0
- package/dist/jsx/adapters/react.d.ts +26 -0
- package/dist/jsx/define-component.d.ts +68 -0
- package/dist/jsx/index.d.ts +7 -0
- package/dist/jsx/jsx-runtime.d.ts +34 -0
- package/dist/jsx/migrate.d.ts +24 -0
- package/dist/jsx/schemas/index.d.ts +2 -0
- package/dist/jsx/schemas/primitives.d.ts +355 -0
- package/dist/jsx/schemas/tokens.d.ts +79 -0
- package/dist/jsx/validation.d.ts +28 -0
- package/dist/jsx/vnode.d.ts +58 -0
- package/dist/migrate.d.ts +18 -0
- package/dist/primitives/index.d.ts +5 -0
- package/dist/primitives/migrate.d.ts +25 -0
- package/dist/primitives/parser.d.ts +32 -0
- package/dist/primitives/template-parser.d.ts +33 -0
- package/dist/primitives/template-renderer.d.ts +19 -0
- package/dist/primitives/types.d.ts +164 -0
- package/dist/renderers/html/index.d.ts +6 -0
- package/dist/renderers/index.d.ts +5 -0
- package/dist/renderers/react/index.d.ts +6 -0
- package/dist/renderers/registry.d.ts +41 -0
- package/dist/renderers/render.d.ts +35 -0
- package/dist/renderers/types.d.ts +188 -0
- package/dist/tokens/defaults.d.ts +2 -0
- package/dist/tokens/resolver.d.ts +67 -0
- package/dist/tokens/utils.d.ts +15 -0
- package/dist/tokens/validation.d.ts +36 -0
- package/dist/typecheck/index.d.ts +24 -0
- package/dist/ui/button.d.ts +1 -1
- package/dist/validators/index.d.ts +59 -0
- package/dist/validators/schema-validator.d.ts +27 -0
- package/dist/validators/semantic-validator.d.ts +47 -0
- package/dist/vite/plugins/tokens-plugin.d.ts +2 -0
- package/package.json +8 -4
- package/src/preview-runtime/fast-template.html +115 -0
- package/src/preview-runtime/template.html +50 -8
- package/src/preview-runtime/vendors.ts +9 -0
- package/src/theme/entry.tsx +153 -12
- 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;
|
package/dist/ui/button.d.ts
CHANGED
|
@@ -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" | "
|
|
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[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prev-cli",
|
|
3
|
-
"version": "0.24.
|
|
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
|
|
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
|
}
|
|
@@ -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>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|