prev-cli 0.24.18 → 0.24.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/jsx/adapters/html.test.ts +191 -0
- package/src/jsx/adapters/html.ts +404 -0
- package/src/jsx/adapters/react.test.ts +172 -0
- package/src/jsx/adapters/react.tsx +346 -0
- package/src/jsx/define-component.ts +129 -0
- package/src/jsx/index.ts +47 -0
- package/src/jsx/jsx-runtime.test.ts +63 -0
- package/src/jsx/jsx-runtime.ts +117 -0
- package/src/jsx/migrate.test.ts +72 -0
- package/src/jsx/migrate.ts +451 -0
- package/src/jsx/schemas/index.ts +4 -0
- package/src/jsx/schemas/primitives.ts +107 -0
- package/src/jsx/schemas/tokens.ts +60 -0
- package/src/jsx/validation.ts +77 -0
- package/src/jsx/vnode.ts +159 -0
- package/src/primitives/index.ts +8 -0
- package/src/primitives/migrate.test.ts +317 -0
- package/src/primitives/migrate.ts +364 -0
- package/src/primitives/parser.test.ts +265 -0
- package/src/primitives/parser.ts +442 -0
- package/src/primitives/template-parser.test.ts +297 -0
- package/src/primitives/template-parser.ts +374 -0
- package/src/primitives/template-renderer.test.ts +359 -0
- package/src/primitives/template-renderer.ts +497 -0
- package/src/primitives/tokens.css +82 -0
- package/src/primitives/types.ts +248 -0
- package/src/tokens/defaults.test.ts +137 -0
- package/src/tokens/defaults.ts +77 -0
- package/src/tokens/defaults.yaml +76 -0
- package/src/tokens/resolver.test.ts +229 -0
- package/src/tokens/resolver.ts +173 -0
- package/src/tokens/utils.test.ts +172 -0
- package/src/tokens/utils.ts +104 -0
- package/src/tokens/validation.test.ts +118 -0
- package/src/tokens/validation.ts +226 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// src/jsx/schemas/primitives.ts
|
|
2
|
+
// Zod schemas for primitive props - single source of truth
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import {
|
|
5
|
+
SpacingToken,
|
|
6
|
+
AlignToken,
|
|
7
|
+
BackgroundToken,
|
|
8
|
+
RadiusToken,
|
|
9
|
+
ColorToken,
|
|
10
|
+
SizeToken,
|
|
11
|
+
WeightToken,
|
|
12
|
+
FitToken,
|
|
13
|
+
} from './tokens'
|
|
14
|
+
|
|
15
|
+
// Children type - accepts VNode array or single VNode (normalized later)
|
|
16
|
+
const Children = z.any()
|
|
17
|
+
|
|
18
|
+
// Layout primitive: Col - Vertical stack
|
|
19
|
+
export const ColProps = z.object({
|
|
20
|
+
gap: SpacingToken.optional(),
|
|
21
|
+
align: AlignToken.optional(),
|
|
22
|
+
padding: SpacingToken.optional(),
|
|
23
|
+
children: Children.optional(),
|
|
24
|
+
})
|
|
25
|
+
export type ColProps = z.infer<typeof ColProps>
|
|
26
|
+
|
|
27
|
+
// Layout primitive: Row - Horizontal stack
|
|
28
|
+
export const RowProps = z.object({
|
|
29
|
+
gap: SpacingToken.optional(),
|
|
30
|
+
align: AlignToken.optional(),
|
|
31
|
+
padding: SpacingToken.optional(),
|
|
32
|
+
children: Children.optional(),
|
|
33
|
+
})
|
|
34
|
+
export type RowProps = z.infer<typeof RowProps>
|
|
35
|
+
|
|
36
|
+
// Layout primitive: Box - Container with styling
|
|
37
|
+
export const BoxProps = z.object({
|
|
38
|
+
padding: SpacingToken.optional(),
|
|
39
|
+
bg: BackgroundToken.optional(),
|
|
40
|
+
radius: RadiusToken.optional(),
|
|
41
|
+
children: Children.optional(),
|
|
42
|
+
})
|
|
43
|
+
export type BoxProps = z.infer<typeof BoxProps>
|
|
44
|
+
|
|
45
|
+
// Layout primitive: Spacer - Whitespace
|
|
46
|
+
export const SpacerProps = z.object({
|
|
47
|
+
size: SpacingToken.optional(), // If absent, flex grow
|
|
48
|
+
})
|
|
49
|
+
export type SpacerProps = z.infer<typeof SpacerProps>
|
|
50
|
+
|
|
51
|
+
// Layout primitive: Slot - State-dependent placeholder
|
|
52
|
+
export const SlotProps = z.object({
|
|
53
|
+
name: z.string(),
|
|
54
|
+
})
|
|
55
|
+
export type SlotProps = z.infer<typeof SlotProps>
|
|
56
|
+
|
|
57
|
+
// Content primitive: Text
|
|
58
|
+
export const TextProps = z.object({
|
|
59
|
+
children: z.string().optional(),
|
|
60
|
+
size: SizeToken.optional(),
|
|
61
|
+
weight: WeightToken.optional(),
|
|
62
|
+
color: ColorToken.optional(),
|
|
63
|
+
})
|
|
64
|
+
export type TextProps = z.infer<typeof TextProps>
|
|
65
|
+
|
|
66
|
+
// Content primitive: Icon
|
|
67
|
+
export const IconProps = z.object({
|
|
68
|
+
name: z.string(),
|
|
69
|
+
size: SizeToken.optional(),
|
|
70
|
+
color: ColorToken.optional(),
|
|
71
|
+
})
|
|
72
|
+
export type IconProps = z.infer<typeof IconProps>
|
|
73
|
+
|
|
74
|
+
// Content primitive: Image
|
|
75
|
+
export const ImageProps = z.object({
|
|
76
|
+
src: z.string(),
|
|
77
|
+
alt: z.string().optional(),
|
|
78
|
+
fit: FitToken.optional(),
|
|
79
|
+
})
|
|
80
|
+
export type ImageProps = z.infer<typeof ImageProps>
|
|
81
|
+
|
|
82
|
+
// Primitive type enum
|
|
83
|
+
export const PrimitiveType = z.enum([
|
|
84
|
+
'col',
|
|
85
|
+
'row',
|
|
86
|
+
'box',
|
|
87
|
+
'spacer',
|
|
88
|
+
'slot',
|
|
89
|
+
'text',
|
|
90
|
+
'icon',
|
|
91
|
+
'image',
|
|
92
|
+
'component',
|
|
93
|
+
])
|
|
94
|
+
export type PrimitiveType = z.infer<typeof PrimitiveType>
|
|
95
|
+
|
|
96
|
+
// Props union for validation
|
|
97
|
+
export const PrimitiveProps = z.union([
|
|
98
|
+
ColProps,
|
|
99
|
+
RowProps,
|
|
100
|
+
BoxProps,
|
|
101
|
+
SpacerProps,
|
|
102
|
+
SlotProps,
|
|
103
|
+
TextProps,
|
|
104
|
+
IconProps,
|
|
105
|
+
ImageProps,
|
|
106
|
+
])
|
|
107
|
+
export type PrimitiveProps = z.infer<typeof PrimitiveProps>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/jsx/schemas/tokens.ts
|
|
2
|
+
// Zod schemas for design tokens - single source of truth
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
// Spacing tokens
|
|
6
|
+
export const SpacingToken = z.enum(['none', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'])
|
|
7
|
+
export type SpacingToken = z.infer<typeof SpacingToken>
|
|
8
|
+
|
|
9
|
+
// Alignment tokens
|
|
10
|
+
export const AlignToken = z.enum(['start', 'center', 'end', 'stretch', 'between'])
|
|
11
|
+
export type AlignToken = z.infer<typeof AlignToken>
|
|
12
|
+
|
|
13
|
+
// Background tokens - shadcn-compatible
|
|
14
|
+
export const BackgroundToken = z.enum([
|
|
15
|
+
'transparent',
|
|
16
|
+
'background',
|
|
17
|
+
'card',
|
|
18
|
+
'primary',
|
|
19
|
+
'secondary',
|
|
20
|
+
'muted',
|
|
21
|
+
'accent',
|
|
22
|
+
'destructive',
|
|
23
|
+
'input',
|
|
24
|
+
])
|
|
25
|
+
export type BackgroundToken = z.infer<typeof BackgroundToken>
|
|
26
|
+
|
|
27
|
+
// Border radius tokens
|
|
28
|
+
export const RadiusToken = z.enum(['none', 'sm', 'md', 'lg', 'xl', 'full'])
|
|
29
|
+
export type RadiusToken = z.infer<typeof RadiusToken>
|
|
30
|
+
|
|
31
|
+
// Color tokens - shadcn-compatible (foreground colors for text)
|
|
32
|
+
export const ColorToken = z.enum([
|
|
33
|
+
'foreground',
|
|
34
|
+
'card-foreground',
|
|
35
|
+
'primary',
|
|
36
|
+
'primary-foreground',
|
|
37
|
+
'secondary',
|
|
38
|
+
'secondary-foreground',
|
|
39
|
+
'muted',
|
|
40
|
+
'muted-foreground',
|
|
41
|
+
'accent',
|
|
42
|
+
'accent-foreground',
|
|
43
|
+
'destructive',
|
|
44
|
+
'destructive-foreground',
|
|
45
|
+
'border',
|
|
46
|
+
'ring',
|
|
47
|
+
])
|
|
48
|
+
export type ColorToken = z.infer<typeof ColorToken>
|
|
49
|
+
|
|
50
|
+
// Typography size tokens
|
|
51
|
+
export const SizeToken = z.enum(['xs', 'sm', 'base', 'lg', 'xl', '2xl'])
|
|
52
|
+
export type SizeToken = z.infer<typeof SizeToken>
|
|
53
|
+
|
|
54
|
+
// Font weight tokens
|
|
55
|
+
export const WeightToken = z.enum(['normal', 'medium', 'semibold', 'bold'])
|
|
56
|
+
export type WeightToken = z.infer<typeof WeightToken>
|
|
57
|
+
|
|
58
|
+
// Image fit tokens
|
|
59
|
+
export const FitToken = z.enum(['cover', 'contain', 'fill'])
|
|
60
|
+
export type FitToken = z.infer<typeof FitToken>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/jsx/validation.ts
|
|
2
|
+
// Configurable validation for JSX primitives
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validation mode controls how schema validation behaves:
|
|
7
|
+
* - 'strict': throws on validation errors (useful for development/testing)
|
|
8
|
+
* - 'warn': logs warnings but continues (default in DEV)
|
|
9
|
+
* - 'off': no validation (default in production)
|
|
10
|
+
*/
|
|
11
|
+
export type ValidationMode = 'strict' | 'warn' | 'off'
|
|
12
|
+
|
|
13
|
+
// DEV mode flag
|
|
14
|
+
const DEV = process.env.NODE_ENV !== 'production'
|
|
15
|
+
|
|
16
|
+
// Current validation mode - defaults based on environment
|
|
17
|
+
let validationMode: ValidationMode = DEV ? 'warn' : 'off'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set the validation mode for JSX primitives
|
|
21
|
+
* @param mode - The validation mode to use
|
|
22
|
+
*/
|
|
23
|
+
export function setValidationMode(mode: ValidationMode): void {
|
|
24
|
+
validationMode = mode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the current validation mode
|
|
29
|
+
*/
|
|
30
|
+
export function getValidationMode(): ValidationMode {
|
|
31
|
+
return validationMode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate props against a Zod schema based on current mode.
|
|
36
|
+
* In 'strict' mode, throws on error.
|
|
37
|
+
* In 'warn' mode, logs a warning and continues.
|
|
38
|
+
* In 'off' mode, skips validation entirely.
|
|
39
|
+
*
|
|
40
|
+
* @param schema - Zod schema to validate against
|
|
41
|
+
* @param props - Props to validate
|
|
42
|
+
* @param componentName - Component name for error messages
|
|
43
|
+
*/
|
|
44
|
+
export function validateProps(
|
|
45
|
+
schema: z.ZodType,
|
|
46
|
+
props: unknown,
|
|
47
|
+
componentName: string
|
|
48
|
+
): void {
|
|
49
|
+
if (validationMode === 'off') {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = schema.safeParse(props)
|
|
54
|
+
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
const errorMessage = formatValidationError(componentName, result.error)
|
|
57
|
+
|
|
58
|
+
if (validationMode === 'strict') {
|
|
59
|
+
throw new Error(errorMessage)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// warn mode - log and continue
|
|
63
|
+
console.warn(errorMessage)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format Zod validation error for display
|
|
69
|
+
*/
|
|
70
|
+
function formatValidationError(componentName: string, error: z.ZodError): string {
|
|
71
|
+
const issues = error.issues.map((issue) => {
|
|
72
|
+
const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''
|
|
73
|
+
return ` - ${path}${issue.message}`
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return `[${componentName}] Invalid props:\n${issues.join('\n')}`
|
|
77
|
+
}
|
package/src/jsx/vnode.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/jsx/vnode.ts
|
|
2
|
+
// Virtual Node representation - immutable tree structure
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { PrimitiveType } from './schemas/primitives'
|
|
5
|
+
|
|
6
|
+
export interface VNodeType {
|
|
7
|
+
id: string
|
|
8
|
+
type: z.infer<typeof PrimitiveType>
|
|
9
|
+
props: Record<string, unknown>
|
|
10
|
+
children?: VNodeType[]
|
|
11
|
+
componentName?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// VNode schema - recursive definition for runtime validation
|
|
15
|
+
export const VNode: z.ZodType<VNodeType> = z.lazy(() =>
|
|
16
|
+
z.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
type: PrimitiveType,
|
|
19
|
+
props: z.record(z.string(), z.unknown()),
|
|
20
|
+
children: z.array(VNode).optional(),
|
|
21
|
+
componentName: z.string().optional(),
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// ============================================================
|
|
26
|
+
// Render Context - isolated ID generation for concurrent safety
|
|
27
|
+
// ============================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render context for isolated ID generation.
|
|
31
|
+
* Each render should use its own context to avoid ID conflicts
|
|
32
|
+
* when multiple renders execute concurrently.
|
|
33
|
+
*/
|
|
34
|
+
export interface RenderContext {
|
|
35
|
+
/** Generate unique ID for a node type */
|
|
36
|
+
nextId: (type: string) => string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a new render context with isolated ID counter.
|
|
41
|
+
* Use this for each render to ensure concurrent safety.
|
|
42
|
+
*/
|
|
43
|
+
export function createRenderContext(): RenderContext {
|
|
44
|
+
let counter = 0
|
|
45
|
+
return {
|
|
46
|
+
nextId: (type: string) => `${type}-${counter++}`,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Global context for backward compatibility
|
|
51
|
+
let globalContext = createRenderContext()
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reset ID counter - call at start of each render.
|
|
55
|
+
* @deprecated Use createRenderContext() for concurrent safety
|
|
56
|
+
*/
|
|
57
|
+
export function resetIdCounter(): void {
|
|
58
|
+
globalContext = createRenderContext()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the current global render context.
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export function getGlobalContext(): RenderContext {
|
|
66
|
+
return globalContext
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create an immutable VNode
|
|
71
|
+
* @param type - Node type (col, row, text, etc.)
|
|
72
|
+
* @param props - Node properties
|
|
73
|
+
* @param children - Child nodes
|
|
74
|
+
* @param context - Optional render context for ID generation (uses global if not provided)
|
|
75
|
+
*/
|
|
76
|
+
export function createVNode(
|
|
77
|
+
type: VNodeType['type'],
|
|
78
|
+
props: Record<string, unknown>,
|
|
79
|
+
children?: VNodeType[],
|
|
80
|
+
context?: RenderContext
|
|
81
|
+
): VNodeType {
|
|
82
|
+
const ctx = context ?? globalContext
|
|
83
|
+
const node: VNodeType = {
|
|
84
|
+
id: ctx.nextId(type),
|
|
85
|
+
type,
|
|
86
|
+
props: Object.freeze({ ...props }),
|
|
87
|
+
children: children ? Object.freeze([...children]) as VNodeType[] : undefined,
|
|
88
|
+
}
|
|
89
|
+
return Object.freeze(node) as VNodeType
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a component VNode (wraps rendered content)
|
|
94
|
+
* @param componentName - Name of the component
|
|
95
|
+
* @param props - Component properties
|
|
96
|
+
* @param children - Child nodes
|
|
97
|
+
* @param context - Optional render context for ID generation (uses global if not provided)
|
|
98
|
+
*/
|
|
99
|
+
export function createComponentVNode(
|
|
100
|
+
componentName: string,
|
|
101
|
+
props: Record<string, unknown>,
|
|
102
|
+
children?: VNodeType[],
|
|
103
|
+
context?: RenderContext
|
|
104
|
+
): VNodeType {
|
|
105
|
+
const ctx = context ?? globalContext
|
|
106
|
+
const node: VNodeType = {
|
|
107
|
+
id: ctx.nextId('component'),
|
|
108
|
+
type: 'component',
|
|
109
|
+
props: Object.freeze({ ...props }),
|
|
110
|
+
children: children ? Object.freeze([...children]) as VNodeType[] : undefined,
|
|
111
|
+
componentName,
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze(node) as VNodeType
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalize children to array, converting strings/numbers to Text nodes
|
|
118
|
+
*/
|
|
119
|
+
export function normalizeChildren(children: unknown): VNodeType[] {
|
|
120
|
+
if (children === null || children === undefined) return []
|
|
121
|
+
|
|
122
|
+
const items = Array.isArray(children) ? children.flat(Infinity) : [children]
|
|
123
|
+
|
|
124
|
+
return items
|
|
125
|
+
.filter((item) => item !== null && item !== undefined && item !== false && item !== true)
|
|
126
|
+
.map((item) => {
|
|
127
|
+
// Convert strings and numbers to Text nodes
|
|
128
|
+
if (typeof item === 'string' || typeof item === 'number') {
|
|
129
|
+
return createVNode('text', { content: String(item) })
|
|
130
|
+
}
|
|
131
|
+
return item as VNodeType
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check structural equality of two VNodes (ignores IDs)
|
|
137
|
+
*/
|
|
138
|
+
export function vnodeEquals(a: VNodeType, b: VNodeType): boolean {
|
|
139
|
+
if (a.type !== b.type) return false
|
|
140
|
+
if (a.componentName !== b.componentName) return false
|
|
141
|
+
|
|
142
|
+
// Compare props (shallow)
|
|
143
|
+
const aProps = Object.entries(a.props).filter(([k]) => k !== 'children')
|
|
144
|
+
const bProps = Object.entries(b.props).filter(([k]) => k !== 'children')
|
|
145
|
+
if (aProps.length !== bProps.length) return false
|
|
146
|
+
for (const [key, value] of aProps) {
|
|
147
|
+
if (b.props[key] !== value) return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Compare children recursively
|
|
151
|
+
const aChildren = a.children || []
|
|
152
|
+
const bChildren = b.children || []
|
|
153
|
+
if (aChildren.length !== bChildren.length) return false
|
|
154
|
+
for (let i = 0; i < aChildren.length; i++) {
|
|
155
|
+
if (!vnodeEquals(aChildren[i], bChildren[i])) return false
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return true
|
|
159
|
+
}
|