@vyckr/tachyon 0.3.0 → 1.0.0
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/.env.example +3 -17
- package/README.md +87 -57
- package/bun.lock +127 -0
- package/components/counter.html +13 -0
- package/deno.lock +19 -0
- package/go.mod +3 -0
- package/lib/gson-2.3.jar +0 -0
- package/main.js +0 -0
- package/package.json +19 -20
- package/routes/DELETE +18 -0
- package/routes/GET +17 -0
- package/routes/HTML +28 -0
- package/routes/POST +32 -0
- package/routes/SOCKET +26 -0
- package/routes/api/:version/DELETE +10 -0
- package/routes/api/:version/GET +29 -0
- package/routes/api/:version/PATCH +24 -0
- package/routes/api/GET +29 -0
- package/routes/api/POST +16 -0
- package/routes/api/PUT +21 -0
- package/src/client/404.html +7 -0
- package/src/client/dev.html +14 -0
- package/src/client/dist.ts +20 -0
- package/src/client/hmr.js +12 -0
- package/src/client/prod.html +13 -0
- package/src/client/render.js +278 -0
- package/src/client/routes.json +1 -0
- package/src/client/yon.ts +341 -0
- package/src/router.ts +185 -0
- package/src/serve.ts +144 -0
- package/src/server/logger.ts +31 -0
- package/src/server/tach.ts +234 -0
- package/tests/index.test.ts +110 -0
- package/tests/stream.ts +24 -0
- package/tests/worker.ts +7 -0
- package/tsconfig.json +1 -1
- package/Dockerfile +0 -47
- package/bun.lockb +0 -0
- package/routes/byos/[primary]/doc/index.ts +0 -28
- package/routes/byos/[primary]/docs/index.ts +0 -28
- package/routes/byos/[primary]/join/[secondary]/docs/index.ts +0 -10
- package/routes/byos/[primary]/schema/index.ts +0 -17
- package/routes/byos/[primary]/stream/doc/index.ts +0 -28
- package/routes/byos/[primary]/stream/docs/index.ts +0 -28
- package/routes/proxy.ts +0 -8
- package/routes/utils/validation.ts +0 -36
- package/src/Tach.ts +0 -543
- package/src/Yon.ts +0 -25
- package/src/runtime.ts +0 -822
- package/types/index.d.ts +0 -13
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom'
|
|
2
|
+
import Router from "../router.js";
|
|
3
|
+
import { EventEmitter } from 'node:stream';
|
|
4
|
+
import { BunRequest } from 'bun';
|
|
5
|
+
|
|
6
|
+
export default class Yon {
|
|
7
|
+
|
|
8
|
+
private static htmlMethod = 'HTML'
|
|
9
|
+
|
|
10
|
+
private static emitter = new EventEmitter()
|
|
11
|
+
|
|
12
|
+
private static compMapping = new Map<string, string>()
|
|
13
|
+
|
|
14
|
+
static getParams(request: BunRequest, route: string) {
|
|
15
|
+
|
|
16
|
+
const url = new URL(request.url)
|
|
17
|
+
|
|
18
|
+
const params = url.pathname.split('/').slice(route.split('/').length)
|
|
19
|
+
|
|
20
|
+
return { params: Router.parseParams(params) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static async createStaticRoutes() {
|
|
24
|
+
|
|
25
|
+
Router.reqRoutes["/render.js"] = {
|
|
26
|
+
GET: async () => new Response(await Bun.file(`${import.meta.dir}/render.js`).bytes(), { headers: { 'Content-Type': 'application/javascript' } })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Router.reqRoutes["/hmr.js"] = {
|
|
30
|
+
GET: async () => new Response(await Bun.file(`${import.meta.dir}/hmr.js`).bytes(), { headers: { 'Content-Type': 'application/javascript' } })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Router.reqRoutes["/routes.json"] = {
|
|
34
|
+
GET: async () => new Response(await Bun.file(`${import.meta.dir}/routes.json`).bytes(), { headers: { 'Content-Type': 'application/json' } })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const main = Bun.file(`${process.cwd()}/main.js`)
|
|
38
|
+
|
|
39
|
+
if(await main.exists()) {
|
|
40
|
+
Router.reqRoutes["/main.js"] = {
|
|
41
|
+
GET: async () => new Response(await main.bytes(), { headers: { 'Content-Type': 'application/javascript' } })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let styles = ''
|
|
46
|
+
|
|
47
|
+
Yon.emitter.addListener('style', (msg) => {
|
|
48
|
+
styles += `${msg}\n`
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await Promise.all([Yon.bundleDependencies(), Yon.bundleComponents(), Yon.bundlePages()])
|
|
52
|
+
|
|
53
|
+
await Bun.write(Bun.file(`${import.meta.dir}/routes.json`), JSON.stringify(Router.routeSlugs))
|
|
54
|
+
|
|
55
|
+
Yon.emitter.removeAllListeners('style')
|
|
56
|
+
|
|
57
|
+
if(styles) {
|
|
58
|
+
Router.reqRoutes["/styles.css"] = {
|
|
59
|
+
GET: () => new Response(styles, { headers: { 'Content-Type': 'text/css' }})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private static extractComponents(data: string) {
|
|
65
|
+
|
|
66
|
+
const html = new JSDOM('').window.document.createElement('div')
|
|
67
|
+
|
|
68
|
+
html.innerHTML = data
|
|
69
|
+
|
|
70
|
+
const scripts = html.querySelectorAll('script')
|
|
71
|
+
|
|
72
|
+
const script = scripts[0]
|
|
73
|
+
|
|
74
|
+
scripts.forEach(s => s.remove())
|
|
75
|
+
|
|
76
|
+
const styles = html.querySelectorAll('style')
|
|
77
|
+
|
|
78
|
+
const style = styles[0]
|
|
79
|
+
|
|
80
|
+
styles.forEach(s => s.remove())
|
|
81
|
+
|
|
82
|
+
return { html, script, style }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static parseHTML(elements: HTMLCollection, imports: Map<string, Set<string>> = new Map<string, Set<string>>()) {
|
|
86
|
+
|
|
87
|
+
const parsed: { static?: string, render?: string, element?: string }[] = []
|
|
88
|
+
|
|
89
|
+
for (const element of elements) {
|
|
90
|
+
|
|
91
|
+
if(element.tagName.startsWith('TY-')) {
|
|
92
|
+
|
|
93
|
+
const component = element.tagName.split('-')[1].toLowerCase()
|
|
94
|
+
|
|
95
|
+
if(component === 'loop') {
|
|
96
|
+
const attribute = element.attributes[0];
|
|
97
|
+
if (attribute.name === ':for') parsed.push({ render: `for(${attribute.value}) {`})
|
|
98
|
+
} else if(component === "logic") {
|
|
99
|
+
const attribute = element.attributes[0]
|
|
100
|
+
if (attribute.name === ':if') parsed.push({ render: `if(${attribute.value}) {`});
|
|
101
|
+
if (attribute.name === ':else-if') parsed.push({ render: `else if(${attribute.value}) {`});
|
|
102
|
+
if (attribute.name === ':else') parsed.push({ render: `else {`});
|
|
103
|
+
} else {
|
|
104
|
+
|
|
105
|
+
const exports: string[] = []
|
|
106
|
+
|
|
107
|
+
const filepath = Yon.compMapping.get(component)
|
|
108
|
+
|
|
109
|
+
if(filepath) {
|
|
110
|
+
|
|
111
|
+
for(let i = 0; i < element.attributes.length; i++) {
|
|
112
|
+
|
|
113
|
+
if(element.attributes[i].name.startsWith(':')) {
|
|
114
|
+
const propName = element.attributes[i].name.slice(1)
|
|
115
|
+
exports.push(`${propName} = ${"${" + element.attributes[i].value + "}"}`)
|
|
116
|
+
} else {
|
|
117
|
+
const propName = element.attributes[i].name
|
|
118
|
+
exports.push(`${propName} = "${element.attributes[i].value}"`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if(imports.has(filepath)) {
|
|
123
|
+
|
|
124
|
+
if(!imports.get(filepath)?.has(component)) {
|
|
125
|
+
parsed.push({ static: `const { default: ${component} } = import('/components/${filepath}')`})
|
|
126
|
+
imports.get(filepath)?.add(component)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
} else {
|
|
130
|
+
|
|
131
|
+
parsed.push({ static: `const { default: ${component} } = await import('/components/${filepath}')`})
|
|
132
|
+
imports.set(filepath, new Set<string>([component]))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hash = Bun.randomUUIDv7().split('-')[1]
|
|
136
|
+
|
|
137
|
+
parsed.push({ static: `const comp_${hash} = await ${component}(\`${exports.join(';')}\`)`})
|
|
138
|
+
|
|
139
|
+
parsed.push({ render: `elements += comp_${hash}(execute && execute.compId === "ty-${hash}" ? execute : null).replaceAll('class="', 'class="ty-${hash} ')`})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const temp = new JSDOM('').window.document.createElement('div');
|
|
144
|
+
temp.innerHTML = element.innerHTML
|
|
145
|
+
|
|
146
|
+
parsed.push(...this.parseHTML(temp.children, imports))
|
|
147
|
+
|
|
148
|
+
if(component === "loop" || component === "logic") parsed.push({ render: '}'})
|
|
149
|
+
|
|
150
|
+
} else {
|
|
151
|
+
|
|
152
|
+
for(let i = 0; i < element.attributes.length; i++) {
|
|
153
|
+
|
|
154
|
+
const attr = element.attributes[i]
|
|
155
|
+
|
|
156
|
+
if(attr.name.startsWith(':')) {
|
|
157
|
+
|
|
158
|
+
const attrName = attr.name.slice(1)
|
|
159
|
+
|
|
160
|
+
element.removeAttribute(attr.name)
|
|
161
|
+
element.setAttribute(attrName, "${" + attr.value + "}")
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
parsed.push({ element: `\`${element.outerHTML}\`` })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return parsed
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private static createJSData(html: { static?: string, render?: string, element?: string }[], scriptTag?: HTMLScriptElement, style?: HTMLStyleElement) {
|
|
173
|
+
|
|
174
|
+
const hash = Bun.randomUUIDv7().split('-')[3]
|
|
175
|
+
|
|
176
|
+
if(style && style.innerHTML) Yon.emitter.emit('style', `@scope (.ty-${hash}) { ${style.innerHTML} }`)
|
|
177
|
+
|
|
178
|
+
const outers: string[] = []
|
|
179
|
+
const inners: string[] = []
|
|
180
|
+
|
|
181
|
+
html.forEach(h => {
|
|
182
|
+
if(h.static) outers.push(h.static)
|
|
183
|
+
if(h.element) {
|
|
184
|
+
const temp = new JSDOM('').window.document.createElement('div');
|
|
185
|
+
temp.innerHTML = h.element
|
|
186
|
+
temp.children[0].classList.add(`ty-${hash}`)
|
|
187
|
+
inners.push(`elements += ${temp.innerHTML}`)
|
|
188
|
+
}
|
|
189
|
+
if(h.render) inners.push(h.render)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return `
|
|
193
|
+
|
|
194
|
+
export default async function(props) {
|
|
195
|
+
|
|
196
|
+
${scriptTag ? scriptTag.innerHTML : ''}
|
|
197
|
+
|
|
198
|
+
${outers.join('\n')}
|
|
199
|
+
|
|
200
|
+
props?.split(';').map(exp => eval(exp))
|
|
201
|
+
|
|
202
|
+
return function(execute) {
|
|
203
|
+
|
|
204
|
+
if(execute) {
|
|
205
|
+
const { classId, compId, func } = execute
|
|
206
|
+
if(classId === "ty-${hash}" || compId === "ty-${hash}") {
|
|
207
|
+
eval(func)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let elements = '';
|
|
212
|
+
|
|
213
|
+
${inners.join('\n')}
|
|
214
|
+
|
|
215
|
+
return elements
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static async addToStatix(html: HTMLDivElement, script: HTMLScriptElement, style: HTMLStyleElement, route: string, dir: 'pages' | 'components') {
|
|
222
|
+
|
|
223
|
+
const module = Yon.parseHTML(html.children)
|
|
224
|
+
|
|
225
|
+
const jsData = Yon.createJSData(module, script, style)
|
|
226
|
+
|
|
227
|
+
route = route.replace('.html', `.${script?.lang || 'js'}`)
|
|
228
|
+
|
|
229
|
+
await Bun.write(Bun.file(`/tmp/${route}`), jsData)
|
|
230
|
+
|
|
231
|
+
const result = await Bun.build({
|
|
232
|
+
entrypoints: [`/tmp/${route}`],
|
|
233
|
+
external: ["*"],
|
|
234
|
+
minify: {
|
|
235
|
+
whitespace: true,
|
|
236
|
+
syntax: true
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
route = route.replace('.ts', '.js')
|
|
241
|
+
|
|
242
|
+
Router.reqRoutes[`/${dir}/${route}`] = {
|
|
243
|
+
GET: () => new Response(result.outputs[0], { headers: { 'Content-Type': 'application/javascript' } })
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private static async bundlePages() {
|
|
248
|
+
|
|
249
|
+
const routes = Array.from(new Bun.Glob(`**/${Yon.htmlMethod}`).scanSync({ cwd: Router.routesPath }))
|
|
250
|
+
|
|
251
|
+
for(const route of routes) {
|
|
252
|
+
|
|
253
|
+
await Router.validateRoute(route)
|
|
254
|
+
|
|
255
|
+
const data = await Bun.file(`${Router.routesPath}/${route}`).text()
|
|
256
|
+
|
|
257
|
+
const { html, script, style } = Yon.extractComponents(data)
|
|
258
|
+
|
|
259
|
+
await Yon.addToStatix(html, script, style, `${route}.${script.lang || 'js'}`, 'pages')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const nfFile = Bun.file(`${process.cwd()}/404.html`)
|
|
263
|
+
|
|
264
|
+
const data = await nfFile.exists() ? await nfFile.text() : await Bun.file(`${import.meta.dir}/404.html`).text()
|
|
265
|
+
|
|
266
|
+
const { html, script, style } = Yon.extractComponents(data)
|
|
267
|
+
|
|
268
|
+
await Yon.addToStatix(html, script, style, '404.html', 'pages')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private static async bundleComponents() {
|
|
272
|
+
|
|
273
|
+
const components = Array.from(new Bun.Glob(`**/*.html`).scanSync({ cwd: Router.componentsPath }))
|
|
274
|
+
|
|
275
|
+
for(let comp of components) {
|
|
276
|
+
|
|
277
|
+
const folders = comp.split('/')
|
|
278
|
+
|
|
279
|
+
const filename = folders[folders.length - 1].replace('.html', '')
|
|
280
|
+
|
|
281
|
+
Yon.compMapping.set(filename, comp.replace('.html', '.js'))
|
|
282
|
+
|
|
283
|
+
const data = await Bun.file(`${Router.componentsPath}/${comp}`).text()
|
|
284
|
+
|
|
285
|
+
const { html, script, style } = Yon.extractComponents(data)
|
|
286
|
+
|
|
287
|
+
await Yon.addToStatix(html, script, style, comp, 'components')
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private static async bundleDependencies() {
|
|
292
|
+
|
|
293
|
+
const packageFile = Bun.file(`${process.cwd()}/package.json`)
|
|
294
|
+
|
|
295
|
+
const otherEntries = ['index.js', 'index', 'index.node']
|
|
296
|
+
|
|
297
|
+
if(await packageFile.exists()) {
|
|
298
|
+
|
|
299
|
+
const packages = await packageFile.json()
|
|
300
|
+
|
|
301
|
+
const modules = Object.keys(packages.dependencies ?? {})
|
|
302
|
+
|
|
303
|
+
for(const module of modules) {
|
|
304
|
+
|
|
305
|
+
let modPack = await Bun.file(`${process.cwd()}/node_modules/${module}/package.json`).json()
|
|
306
|
+
|
|
307
|
+
let idx = 0
|
|
308
|
+
let entryExists = false
|
|
309
|
+
|
|
310
|
+
while(!modPack.main && !entryExists && idx < otherEntries.length) {
|
|
311
|
+
|
|
312
|
+
entryExists = await Bun.file(`${process.cwd()}/node_modules/${module}/${otherEntries[idx]}`).exists()
|
|
313
|
+
|
|
314
|
+
if(entryExists) {
|
|
315
|
+
modPack.main = otherEntries[idx]
|
|
316
|
+
break
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
idx++
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if(!modPack.main) continue
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
|
|
326
|
+
const result = await Bun.build({
|
|
327
|
+
entrypoints: [`${process.cwd()}/node_modules/${module}/${(modPack.main as string).replace('./', '')}`],
|
|
328
|
+
minify: true
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
for(const output of result.outputs) {
|
|
332
|
+
Router.reqRoutes[`/modules/${module}.js`] = {
|
|
333
|
+
GET: () => new Response(output, { headers: { 'Content-Type': 'application/javascript' } })
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
} catch(e) {}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { $, BunRequest, Server } from 'bun'
|
|
2
|
+
|
|
3
|
+
export interface _ctx {
|
|
4
|
+
request: Record<string, any>,
|
|
5
|
+
slugs?: Record<string, any>,
|
|
6
|
+
ipAddress: string,
|
|
7
|
+
params?: Array<any>,
|
|
8
|
+
query?: Record<string, any>,
|
|
9
|
+
body: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default class Router {
|
|
13
|
+
|
|
14
|
+
static readonly reqRoutes: Record<string, Record<string, (req?: BunRequest, server?: Server) => Promise<Response> | Response>> = {}
|
|
15
|
+
|
|
16
|
+
static readonly allRoutes = new Map<string, Set<string>>()
|
|
17
|
+
|
|
18
|
+
static readonly routeSlugs: Record<string, Record<string, number>> = {}
|
|
19
|
+
|
|
20
|
+
static readonly routesPath = `${process.cwd()}/routes`
|
|
21
|
+
static readonly componentsPath = `${process.cwd()}/components`
|
|
22
|
+
|
|
23
|
+
private static readonly allMethods = process.env.ALLOW_METHODS ? process.env.ALLOW_METHODS.split(',') : ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
|
|
24
|
+
|
|
25
|
+
static headers = {
|
|
26
|
+
"Access-Control-Allow-Headers": process.env.ALLOW_HEADERS || "",
|
|
27
|
+
"Access-Control-Allow-Origin": process.env.ALLLOW_ORGINS || "",
|
|
28
|
+
"Access-Control-Allow-Credential": process.env.ALLOW_CREDENTIALS || "false",
|
|
29
|
+
"Access-Control-Expose-Headers": process.env.ALLOW_EXPOSE_HEADERS || "",
|
|
30
|
+
"Access-Control-Max-Age": process.env.ALLOW_MAX_AGE || "",
|
|
31
|
+
"Access-Control-Allow-Methods": process.env.ALLOW_METHODS || ""
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static async validateRoute(route: string, staticPaths: string[] = []) {
|
|
35
|
+
|
|
36
|
+
const paths = route.split('/')
|
|
37
|
+
|
|
38
|
+
const pattern = /^:.*/
|
|
39
|
+
|
|
40
|
+
const slugs: Record<string, number> = {}
|
|
41
|
+
|
|
42
|
+
if(pattern.test(paths[0])) throw new Error(`Invalid route ${route}`)
|
|
43
|
+
|
|
44
|
+
paths.forEach((path, idx) => {
|
|
45
|
+
|
|
46
|
+
if(pattern.test(path) && (pattern.test(paths[idx - 1]) || pattern.test(paths[idx + 1]))) {
|
|
47
|
+
throw new Error(`Invalid route ${route}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if(pattern.test(path)) slugs[path] = idx
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const staticPath = paths.filter((path) => !pattern.test(path)).join(',')
|
|
54
|
+
|
|
55
|
+
if(staticPaths.includes(staticPath)) throw new Error(`Duplicate route ${route}`)
|
|
56
|
+
|
|
57
|
+
staticPaths.push(staticPath)
|
|
58
|
+
|
|
59
|
+
await $`chmod +x ${Router.routesPath}/${route}`
|
|
60
|
+
|
|
61
|
+
const method = paths.pop()!
|
|
62
|
+
|
|
63
|
+
route = `/${paths.join('/')}`
|
|
64
|
+
|
|
65
|
+
if(!Router.allRoutes.has(route)) Router.allRoutes.set(route, new Set<string>())
|
|
66
|
+
|
|
67
|
+
Router.allRoutes.get(route)?.add(method)
|
|
68
|
+
|
|
69
|
+
if(Object.keys(slugs).length > 0 || method === 'HTML') Router.routeSlugs[route] = slugs
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static parseRequest(request: BunRequest) {
|
|
73
|
+
|
|
74
|
+
const req: Record<string, any> = {}
|
|
75
|
+
|
|
76
|
+
req.headers = Object.fromEntries(request.headers)
|
|
77
|
+
req.cache = request.cache
|
|
78
|
+
req.credentials = request.credentials
|
|
79
|
+
req.destination = request.destination
|
|
80
|
+
req.integrity = request.integrity
|
|
81
|
+
req.keepalive = request.keepalive
|
|
82
|
+
req.method = request.method
|
|
83
|
+
req.mode = request.mode
|
|
84
|
+
req.redirect = request.redirect
|
|
85
|
+
req.referrer = request.referrer
|
|
86
|
+
req.referrerPolicy = request.referrerPolicy
|
|
87
|
+
req.url = request.url
|
|
88
|
+
req.parans = request.params
|
|
89
|
+
|
|
90
|
+
return req
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static processRequest(request: BunRequest, route: string, ctx: _ctx) {
|
|
94
|
+
|
|
95
|
+
const { params } = Router.getParams(request, route)
|
|
96
|
+
|
|
97
|
+
ctx.slugs = request.params
|
|
98
|
+
|
|
99
|
+
const searchParams = new URL(request.url).searchParams
|
|
100
|
+
|
|
101
|
+
let queryParams: Record<string, any> | undefined;
|
|
102
|
+
|
|
103
|
+
if(searchParams.size > 0) queryParams = Router.parseKVParams(searchParams)
|
|
104
|
+
|
|
105
|
+
ctx.params = params
|
|
106
|
+
ctx.query = queryParams
|
|
107
|
+
|
|
108
|
+
return { handler: `${Router.routesPath}${route}/${request.method}`, ctx }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static getParams(request: BunRequest, route: string) {
|
|
112
|
+
|
|
113
|
+
const url = new URL(request.url)
|
|
114
|
+
|
|
115
|
+
const params = url.pathname.split("/").slice(route.split("/").length)
|
|
116
|
+
|
|
117
|
+
return { params: Router.parseParams(params) }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static parseParams(input: string[]) {
|
|
121
|
+
|
|
122
|
+
const params: (string | boolean | number | null | undefined)[] = []
|
|
123
|
+
|
|
124
|
+
for(const param of input) {
|
|
125
|
+
|
|
126
|
+
const num = Number(param)
|
|
127
|
+
|
|
128
|
+
if(!Number.isNaN(num)) params.push(num)
|
|
129
|
+
|
|
130
|
+
else if(param === 'true') params.push(true)
|
|
131
|
+
|
|
132
|
+
else if(param === 'false') params.push(false)
|
|
133
|
+
|
|
134
|
+
else if(param === 'null') params.push(null)
|
|
135
|
+
|
|
136
|
+
else if(param === 'undefined') params.push(undefined)
|
|
137
|
+
|
|
138
|
+
else params.push(param)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return params
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static parseKVParams(input: URLSearchParams | FormData) {
|
|
145
|
+
|
|
146
|
+
const params: Record<string, any> = {}
|
|
147
|
+
|
|
148
|
+
for(const [key, val] of input) {
|
|
149
|
+
|
|
150
|
+
if(typeof val === "string") {
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
|
|
154
|
+
params[key] = JSON.parse(val)
|
|
155
|
+
|
|
156
|
+
} catch {
|
|
157
|
+
|
|
158
|
+
const num = Number(val)
|
|
159
|
+
|
|
160
|
+
if(!Number.isNaN(num)) params[key] = num
|
|
161
|
+
|
|
162
|
+
else if(val === 'true') params[key] = true
|
|
163
|
+
|
|
164
|
+
else if(val === 'false') params[key] = false
|
|
165
|
+
|
|
166
|
+
else if(typeof val === "string" && val.includes(',')) params[key] = Router.parseParams(val.split(','))
|
|
167
|
+
|
|
168
|
+
else if(val === 'null') params[key] = null
|
|
169
|
+
|
|
170
|
+
if(params[key] === undefined) params[key] = val
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
} else params[key] = val
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return params
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static async validateRoutes() {
|
|
180
|
+
|
|
181
|
+
const routes = Array.from(new Bun.Glob(`**/{${Router.allMethods.join(',')},SOCKET}`).scanSync({ cwd: Router.routesPath }))
|
|
182
|
+
|
|
183
|
+
for(const route of routes) await Router.validateRoute(route)
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/serve.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import Tach from "./server/tach.js"
|
|
3
|
+
import Router, { _ctx } from "./router.js"
|
|
4
|
+
import Yon from "./client/yon.js"
|
|
5
|
+
import { Logger } from "./server/logger.js"
|
|
6
|
+
import { ServerWebSocket } from "bun"
|
|
7
|
+
import { watch } from "fs/promises"
|
|
8
|
+
import { watch as watcher } from "node:fs";
|
|
9
|
+
|
|
10
|
+
type WebSocketData = {
|
|
11
|
+
handler: string,
|
|
12
|
+
ctx: _ctx,
|
|
13
|
+
path: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const start = Date.now()
|
|
17
|
+
|
|
18
|
+
Logger()
|
|
19
|
+
|
|
20
|
+
async function configureRoutes() {
|
|
21
|
+
await Router.validateRoutes()
|
|
22
|
+
Tach.createServerRoutes()
|
|
23
|
+
await Yon.createStaticRoutes()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await configureRoutes()
|
|
27
|
+
|
|
28
|
+
const server = Bun.serve({
|
|
29
|
+
routes: Router.reqRoutes,
|
|
30
|
+
// websocket: {
|
|
31
|
+
// async open(ws: ServerWebSocket<WebSocketData>) {
|
|
32
|
+
|
|
33
|
+
// const { handler, path } = ws.data
|
|
34
|
+
|
|
35
|
+
// const proc = Bun.spawn({
|
|
36
|
+
// cmd: [handler],
|
|
37
|
+
// stdout: 'inherit',
|
|
38
|
+
// stderr: "pipe",
|
|
39
|
+
// stdin: "pipe"
|
|
40
|
+
// })
|
|
41
|
+
|
|
42
|
+
// Tach.webSockets.set(ws, proc)
|
|
43
|
+
|
|
44
|
+
// console.info(`WebSocket Connected - ${path} - ${proc.pid}`)
|
|
45
|
+
|
|
46
|
+
// for await(const ev of watch(`/tmp`)) {
|
|
47
|
+
|
|
48
|
+
// if(ev.filename === proc.pid.toString()) {
|
|
49
|
+
|
|
50
|
+
// const status = ws.send(Bun.mmap(`/tmp/${proc.pid}`))
|
|
51
|
+
|
|
52
|
+
// console.info(`WebSocket Message Sent - ${path} - ${proc.pid} - ${status} byte(s)`)
|
|
53
|
+
// }
|
|
54
|
+
// }
|
|
55
|
+
// },
|
|
56
|
+
// async message(ws: ServerWebSocket<WebSocketData>, message: string) {
|
|
57
|
+
|
|
58
|
+
// const proc = Tach.webSockets.get(ws)!
|
|
59
|
+
|
|
60
|
+
// const { ctx, path } = ws.data
|
|
61
|
+
|
|
62
|
+
// ctx.body = message
|
|
63
|
+
|
|
64
|
+
// proc.stdin.write(JSON.stringify(ctx))
|
|
65
|
+
|
|
66
|
+
// proc.stdin.flush()
|
|
67
|
+
|
|
68
|
+
// console.info(`WebSocket Message Received - ${path} - ${proc.pid} - ${message.length} byte(s)`)
|
|
69
|
+
// },
|
|
70
|
+
// close(ws, code, reason) {
|
|
71
|
+
|
|
72
|
+
// const { path } = ws.data
|
|
73
|
+
|
|
74
|
+
// const proc = Tach.webSockets.get(ws)!
|
|
75
|
+
|
|
76
|
+
// proc.stdin.end()
|
|
77
|
+
|
|
78
|
+
// Tach.webSockets.delete(ws)
|
|
79
|
+
|
|
80
|
+
// console.info(`WebSocket Disconnected - ${path} - ${proc.pid} - Code (${code}): ${reason}`)
|
|
81
|
+
// },
|
|
82
|
+
// },
|
|
83
|
+
port: process.env.PORT || 8080,
|
|
84
|
+
hostname: process.env.HOSTNAME || '0.0.0.0',
|
|
85
|
+
development: process.env.NODE_ENV === 'development'
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if(server.development) {
|
|
89
|
+
|
|
90
|
+
const socket = Bun.serve({
|
|
91
|
+
fetch(req) {
|
|
92
|
+
socket.upgrade(req)
|
|
93
|
+
return undefined
|
|
94
|
+
},
|
|
95
|
+
websocket: {
|
|
96
|
+
open(ws) {
|
|
97
|
+
console.info("HMR Enabled")
|
|
98
|
+
|
|
99
|
+
watcher(Router.routesPath, { recursive: true }, () => {
|
|
100
|
+
queueMicrotask(async () => {
|
|
101
|
+
console.info("HMR Update")
|
|
102
|
+
await configureRoutes()
|
|
103
|
+
server.reload({ routes: Router.reqRoutes })
|
|
104
|
+
ws.send('')
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
watcher(Router.componentsPath, { recursive: true }, () => {
|
|
109
|
+
queueMicrotask(async () => {
|
|
110
|
+
console.info("HMR Update")
|
|
111
|
+
await configureRoutes()
|
|
112
|
+
server.reload({ routes: Router.reqRoutes })
|
|
113
|
+
ws.send('')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
message(ws, message) {
|
|
118
|
+
|
|
119
|
+
},
|
|
120
|
+
close(ws, code, reason) {
|
|
121
|
+
console.info(`HMR Closed ${code} ${reason}`)
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
port: 9876
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
watcher(Router.routesPath, { recursive: true }, () => {
|
|
128
|
+
queueMicrotask(async () => {
|
|
129
|
+
await configureRoutes()
|
|
130
|
+
server.reload({ routes: Router.reqRoutes })
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
watcher(Router.componentsPath, { recursive: true }, () => {
|
|
135
|
+
queueMicrotask(async () => {
|
|
136
|
+
await configureRoutes()
|
|
137
|
+
server.reload({ routes: Router.reqRoutes })
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const elapsed = Date.now() - start
|
|
143
|
+
|
|
144
|
+
console.info(`Live Server is running on http://${server.hostname}:${server.port} (Press CTRL+C to quit) - ${elapsed.toFixed(2)}ms`)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function Logger() {
|
|
2
|
+
|
|
3
|
+
const formatDate = () => new Date().toISOString().replace('T', ' ').replace('Z', '')
|
|
4
|
+
|
|
5
|
+
const reset = '\x1b[0m'
|
|
6
|
+
|
|
7
|
+
console.info = (...args: any[]) => {
|
|
8
|
+
const info = `[${formatDate()}]\x1b[32m INFO${reset} (${process.pid})`
|
|
9
|
+
console.log(info, ...args)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.error = (...args: any[]) => {
|
|
13
|
+
const err = `[${formatDate()}]\x1b[31m ERROR${reset} (${process.pid})`
|
|
14
|
+
console.log(err, ...args)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.debug = (...args: any[]) => {
|
|
18
|
+
const bug = `[${formatDate()}]\x1b[36m DEBUG${reset} (${process.pid})`
|
|
19
|
+
console.log(bug, ...args)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.warn = (...args: any[]) => {
|
|
23
|
+
const warn = `[${formatDate()}]\x1b[33m WARN${reset} (${process.pid})`
|
|
24
|
+
console.log(warn, ...args)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.trace = (...args: any[]) => {
|
|
28
|
+
const trace = `[${formatDate()}]\x1b[35m TRACE${reset} (${process.pid})`
|
|
29
|
+
console.log(trace, ...args)
|
|
30
|
+
}
|
|
31
|
+
}
|