@vebui/bun 0.0.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/cli.ts +266 -0
- package/package.json +36 -0
package/cli.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { watch, mkdirSync } from "fs"
|
|
3
|
+
import { readFile, writeFile } from "fs/promises"
|
|
4
|
+
import { Glob } from "bun"
|
|
5
|
+
import { resolve, dirname } from "path"
|
|
6
|
+
import { classToDeclaration, drainCSS } from "@vebui/core"
|
|
7
|
+
import * as VebUI from "@vebui/core"
|
|
8
|
+
|
|
9
|
+
// ─── Args ─────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const [,, command, appFile] = process.argv
|
|
12
|
+
|
|
13
|
+
if (command !== "dev" && command !== "build") {
|
|
14
|
+
console.error("Usage: vebui <dev|build> <file.ts>")
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const VERBOSE = Bun.env.VEBUI_VERBOSE === "true"
|
|
19
|
+
|
|
20
|
+
function resolveCorePackage(): string {
|
|
21
|
+
const workspacePath = resolve(import.meta.dir, "../../core")
|
|
22
|
+
try {
|
|
23
|
+
const stat = Bun.file(workspacePath + "/index.ts")
|
|
24
|
+
if (stat.size > 0) {
|
|
25
|
+
if (VERBOSE) console.log(`[vebui] using workspace core: ${workspacePath}`)
|
|
26
|
+
return workspacePath
|
|
27
|
+
}
|
|
28
|
+
} catch { }
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const corePath = import.meta.resolveSync("@vebui/core")
|
|
32
|
+
if (VERBOSE) console.log(`[vebui] resolved core via import.meta: ${corePath}`)
|
|
33
|
+
if (corePath.includes("/dist/")) return dirname(dirname(corePath))
|
|
34
|
+
return dirname(corePath)
|
|
35
|
+
} catch {
|
|
36
|
+
console.error("[vebui] Error: Cannot resolve @vebui/core package")
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const corePackageRoot = resolveCorePackage()
|
|
42
|
+
if (VERBOSE) console.log(`[vebui] core package root: ${corePackageRoot}`)
|
|
43
|
+
|
|
44
|
+
const userPatterns = Bun.env.VEBUI_CONTENT?.split(",").map(s => s.trim()) ?? []
|
|
45
|
+
const patterns = [
|
|
46
|
+
`${corePackageRoot}/**/*.ts`,
|
|
47
|
+
...userPatterns,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const OUT_CSS = Bun.env.VEBUI_CSS_OUT ?? "./assets/vebui/vebui.css"
|
|
51
|
+
const OUT_JS = Bun.env.VEBUI_JS_OUT ?? "./assets/vebui/vebui.client.js"
|
|
52
|
+
|
|
53
|
+
// ─── Extractor ────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const CHAIN_RE = /\b(VStack|HStack|Block|Text|Button|Input|Body|Head)(?:(?:\s*\.\s*[a-zA-Z0-9]+\s*\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\))|(?:\s*\.\s*(?:mobile|tablet|desktop|hover|active|focus|focusWithin|before|after)))+/g
|
|
56
|
+
const IMPORT_CSS_RE = /import\s+["'](.+\.css)["']/g
|
|
57
|
+
|
|
58
|
+
async function extractCSSFromSource(source: string, allClasses: Set<string>, discoveredCSS: Set<string>, currentFile: string) {
|
|
59
|
+
for (const match of source.matchAll(IMPORT_CSS_RE)) {
|
|
60
|
+
const importPath = match[1]
|
|
61
|
+
if (!importPath) continue
|
|
62
|
+
const resolvedPath = importPath.startsWith(".")
|
|
63
|
+
? resolve(dirname(currentFile), importPath)
|
|
64
|
+
: importPath
|
|
65
|
+
discoveredCSS.add(resolvedPath)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const MODIFIER_RE = /\.([a-zA-Z0-9]+)\((?:"([^"]+)"|([\d.]+))\)/g
|
|
69
|
+
const MODIFIER_TO_CLS: Record<string, string> = {
|
|
70
|
+
margin: "m", marginT: "mt", marginB: "mb", marginL: "ml", marginR: "mr", marginX: "mx", marginY: "my",
|
|
71
|
+
m: "m", mt: "mt", mb: "mb", ml: "ml", mr: "mr", mx: "mx", my: "my",
|
|
72
|
+
padding: "p", paddingT: "pt", paddingB: "pb", paddingL: "pl", paddingR: "pr", paddingX: "px", paddingY: "py",
|
|
73
|
+
p: "p", pt: "pt", pb: "pb", pl: "pl", pr: "pr", px: "px", py: "py",
|
|
74
|
+
gap: "gap", gapX: "gapx", gapY: "gapy", spaceX: "gapx", spaceY: "gapy",
|
|
75
|
+
radius: "radius", size: "text",
|
|
76
|
+
width: "w", height: "h", background: "bg",
|
|
77
|
+
}
|
|
78
|
+
const INLINE_MODIFIERS = new Set(["color", "weight", "align", "justify", "border", "opacity", "fontWeight", "fontSize", "flex"])
|
|
79
|
+
|
|
80
|
+
for (const match of source.matchAll(MODIFIER_RE)) {
|
|
81
|
+
const [, modifier, strVal, numVal] = match
|
|
82
|
+
const value = strVal ?? numVal
|
|
83
|
+
if (!modifier || value === undefined || value === null) continue
|
|
84
|
+
if (INLINE_MODIFIERS.has(modifier)) continue
|
|
85
|
+
const prefix = MODIFIER_TO_CLS[modifier]
|
|
86
|
+
if (prefix) allClasses.add(`${prefix}-${String(value).replace(/[^a-zA-Z0-9]/g, "_")}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const mockProxy: any = new Proxy(() => mockProxy, {
|
|
90
|
+
get: (_, key) => {
|
|
91
|
+
if (key === Symbol.toPrimitive) return (hint: string) => hint === "string" ? "MOCK" : 0
|
|
92
|
+
if (key === "key") return "mock-sig"
|
|
93
|
+
return mockProxy
|
|
94
|
+
},
|
|
95
|
+
apply: () => mockProxy,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const context: any = { ...VebUI }
|
|
99
|
+
|
|
100
|
+
for (const match of source.matchAll(CHAIN_RE)) {
|
|
101
|
+
const chain = match[0]
|
|
102
|
+
try {
|
|
103
|
+
const proxy = new Proxy(context, {
|
|
104
|
+
get: (target, key) => {
|
|
105
|
+
if (key === Symbol.unscopables) return undefined
|
|
106
|
+
if (key in target) return target[key]
|
|
107
|
+
return mockProxy
|
|
108
|
+
},
|
|
109
|
+
has: () => true,
|
|
110
|
+
})
|
|
111
|
+
const result = new Function("with(this) { return " + chain + " }").call(proxy)
|
|
112
|
+
if (typeof result === "function") {
|
|
113
|
+
const element = result("DUMMY")
|
|
114
|
+
if (element && typeof element.render === "function") {
|
|
115
|
+
const html = element.render()
|
|
116
|
+
for (const classMatch of html.matchAll(/class="([^"]+)"/g)) {
|
|
117
|
+
classMatch[1].split(" ").forEach((cls: string) => {
|
|
118
|
+
if (cls && !cls.startsWith("w-")) allClasses.add(cls)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (VERBOSE) console.error(`[vebui] eval failed for ${chain.slice(0, 50).trim()}:`, e)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Bundler ──────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
async function bundleClient() {
|
|
132
|
+
const clientEntry = resolve(corePackageRoot, "client/index.ts")
|
|
133
|
+
mkdirSync(dirname(OUT_JS), { recursive: true })
|
|
134
|
+
|
|
135
|
+
const result = await Bun.build({
|
|
136
|
+
entrypoints: [clientEntry],
|
|
137
|
+
minify: true,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
console.error("[vebui] client bundle failed:", result.logs)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const out = result.outputs[0]
|
|
146
|
+
if (out) {
|
|
147
|
+
const code = await out.text()
|
|
148
|
+
await writeFile(OUT_JS, code, "utf-8")
|
|
149
|
+
console.log(`[vebui] bundled client → ${OUT_JS} (${code.length} bytes)`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── CSS Generator ────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
async function resolveFiles(globs: string[]): Promise<string[]> {
|
|
156
|
+
const files = new Set<string>()
|
|
157
|
+
for (const pattern of globs) {
|
|
158
|
+
const glob = new Glob(pattern)
|
|
159
|
+
for await (const file of glob.scan({ cwd: process.cwd(), absolute: true })) {
|
|
160
|
+
if (
|
|
161
|
+
file.includes("clis/") ||
|
|
162
|
+
file.endsWith(".css") ||
|
|
163
|
+
file.endsWith(".js") ||
|
|
164
|
+
file.includes("/client/") ||
|
|
165
|
+
file.includes("\\client\\")
|
|
166
|
+
) continue
|
|
167
|
+
files.add(file)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (VERBOSE) console.log(`[vebui] resolved ${files.size} files`)
|
|
171
|
+
return [...files]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function generateCSS() {
|
|
175
|
+
const files = await resolveFiles(patterns)
|
|
176
|
+
const allClasses = new Set<string>()
|
|
177
|
+
const discoveredCSS = new Set<string>()
|
|
178
|
+
|
|
179
|
+
mkdirSync(dirname(OUT_CSS), { recursive: true })
|
|
180
|
+
drainCSS()
|
|
181
|
+
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
const source = await readFile(file, "utf-8")
|
|
184
|
+
await extractCSSFromSource(source, allClasses, discoveredCSS, file)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const hashedCSS = drainCSS()
|
|
188
|
+
const utilityRules: string[] = []
|
|
189
|
+
for (const cls of allClasses) {
|
|
190
|
+
const decl = classToDeclaration(cls)
|
|
191
|
+
if (decl) utilityRules.push(`.${cls} { ${decl} }`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const parts: string[] = ["/* Generated by vebui — do not edit */"]
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const tokens = await readFile(resolve(corePackageRoot, "tokens.css"), "utf-8")
|
|
198
|
+
parts.push("\n/* Design Tokens & Reset */")
|
|
199
|
+
parts.push(tokens)
|
|
200
|
+
} catch {
|
|
201
|
+
console.warn("[vebui] Warning: Could not find core/tokens.css")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const cssFile of discoveredCSS) {
|
|
205
|
+
try {
|
|
206
|
+
const content = await readFile(cssFile, "utf-8")
|
|
207
|
+
parts.push(`\n/* User CSS: ${cssFile} */`)
|
|
208
|
+
parts.push(content)
|
|
209
|
+
} catch {
|
|
210
|
+
console.warn(`[vebui] Warning: Could not read CSS file: ${cssFile}`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
parts.push("\n/* Utility Classes */")
|
|
215
|
+
parts.push(...utilityRules)
|
|
216
|
+
|
|
217
|
+
if (hashedCSS.trim()) {
|
|
218
|
+
parts.push("\n/* Hashed Pseudo-classes */")
|
|
219
|
+
parts.push(hashedCSS)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const css = parts.join("\n")
|
|
223
|
+
await writeFile(OUT_CSS, css, "utf-8")
|
|
224
|
+
console.log(`[vebui] generated css → ${OUT_CSS} (${files.length} files, ${utilityRules.length} utilities)`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function buildAll() {
|
|
228
|
+
await Promise.all([generateCSS(), bundleClient()])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Entry ────────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
await buildAll()
|
|
234
|
+
|
|
235
|
+
if (command === "build") process.exit(0)
|
|
236
|
+
|
|
237
|
+
// dev: spawn the app and watch for changes
|
|
238
|
+
|
|
239
|
+
if (appFile) {
|
|
240
|
+
Bun.spawn(["bun", appFile], {
|
|
241
|
+
stdout: "inherit",
|
|
242
|
+
stderr: "inherit",
|
|
243
|
+
env: process.env as Record<string, string>,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const watchDirs = new Set(
|
|
248
|
+
patterns.map(p => {
|
|
249
|
+
const full = p.startsWith("/") ? p : resolve(process.cwd(), p)
|
|
250
|
+
return p.includes("*") ? full.replace(/\/\*.*$/, "") : dirname(full)
|
|
251
|
+
})
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
for (const dir of watchDirs) {
|
|
255
|
+
watch(dir, { recursive: true }, async (_, filename) => {
|
|
256
|
+
if (filename && !filename.endsWith(".ts")) return
|
|
257
|
+
if (!filename || (!filename.includes("core/client") && !filename.includes("src/client"))) {
|
|
258
|
+
await generateCSS()
|
|
259
|
+
}
|
|
260
|
+
if (filename && (filename.includes("core/client") || filename.includes("src/client") || filename.includes("signal-rpc.ts"))) {
|
|
261
|
+
await bundleClient()
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (VERBOSE) console.log(`[vebui] watching: ${[...watchDirs].join(", ")}`)
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vebui/bun",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Bun CLI for vebui — dev server and build tool",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vebui": "./cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./cli.ts"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"cli.ts",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"bun",
|
|
19
|
+
"cli",
|
|
20
|
+
"css",
|
|
21
|
+
"atomic-css",
|
|
22
|
+
"build-tool",
|
|
23
|
+
"vebui"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@vebui/core": "^0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"bun": ">=1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "latest",
|
|
34
|
+
"typescript": "^5"
|
|
35
|
+
}
|
|
36
|
+
}
|