@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.
- package/cli.ts +300 -0
- 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
|
+
}
|