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