@vyckr/tachyon 1.1.10 → 1.2.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 +7 -4
- package/LICENSE +21 -0
- package/README.md +210 -90
- package/package.json +50 -33
- package/src/cli/bundle.ts +37 -0
- package/src/cli/serve.ts +100 -0
- package/src/{client/template.js → compiler/render-template.js} +10 -17
- package/src/compiler/template-compiler.ts +419 -0
- package/src/runtime/hot-reload-client.ts +15 -0
- package/src/{client/dev.html → runtime/shells/development.html} +2 -2
- package/src/runtime/shells/not-found.html +73 -0
- package/src/{client/prod.html → runtime/shells/production.html} +1 -1
- package/src/runtime/spa-renderer.ts +439 -0
- package/src/server/console-logger.ts +39 -0
- package/src/server/process-executor.ts +287 -0
- package/src/server/process-pool.ts +80 -0
- package/src/server/route-handler.ts +229 -0
- package/src/server/schema-validator.ts +161 -0
- package/bun.lock +0 -127
- package/components/clicker.html +0 -30
- package/deno.lock +0 -19
- package/go.mod +0 -3
- package/lib/gson-2.3.jar +0 -0
- package/main.js +0 -13
- package/routes/DELETE +0 -18
- package/routes/GET +0 -17
- package/routes/HTML +0 -131
- package/routes/POST +0 -32
- package/routes/SOCKET +0 -26
- package/routes/api/:version/DELETE +0 -10
- package/routes/api/:version/GET +0 -29
- package/routes/api/:version/PATCH +0 -24
- package/routes/api/GET +0 -29
- package/routes/api/POST +0 -16
- package/routes/api/PUT +0 -21
- package/src/client/404.html +0 -7
- package/src/client/dist.ts +0 -20
- package/src/client/hmr.ts +0 -12
- package/src/client/render.ts +0 -417
- package/src/client/routes.json +0 -1
- package/src/client/yon.ts +0 -360
- package/src/router.ts +0 -186
- package/src/serve.ts +0 -147
- package/src/server/logger.ts +0 -31
- package/src/server/tach.ts +0 -238
- package/tests/index.test.ts +0 -110
- package/tests/stream.ts +0 -24
- package/tests/worker.ts +0 -7
- package/tsconfig.json +0 -17
package/src/client/yon.ts
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
import { JSDOM } from 'jsdom'
|
|
2
|
-
import Router from "../router.js";
|
|
3
|
-
import { BunRequest } from 'bun';
|
|
4
|
-
import { exists } from 'node:fs/promises';
|
|
5
|
-
|
|
6
|
-
export default class Yon {
|
|
7
|
-
|
|
8
|
-
private static htmlMethod = 'HTML'
|
|
9
|
-
|
|
10
|
-
private static compMapping = new Map<string, string>()
|
|
11
|
-
|
|
12
|
-
static getParams(request: BunRequest, route: string) {
|
|
13
|
-
|
|
14
|
-
const url = new URL(request.url)
|
|
15
|
-
|
|
16
|
-
const params = url.pathname.split('/').slice(route.split('/').length)
|
|
17
|
-
|
|
18
|
-
return { params: Router.parseParams(params) }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
static async createStaticRoutes() {
|
|
22
|
-
|
|
23
|
-
const result = await Bun.build({
|
|
24
|
-
entrypoints: [`${import.meta.dir}/render.ts`, `${import.meta.dir}/hmr.ts`],
|
|
25
|
-
minify: true
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
for(const output of result.outputs) {
|
|
29
|
-
|
|
30
|
-
Router.reqRoutes[output.path.replace('./', '/')] = {
|
|
31
|
-
GET: async () => new Response(output, { headers: { 'Content-Type': 'application/javascript' } })
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
Router.reqRoutes["/routes.json"] = {
|
|
36
|
-
GET: async () => new Response(await Bun.file(`${import.meta.dir}/routes.json`).bytes(), { headers: { 'Content-Type': 'application/json' } })
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const main = Bun.file(`${process.cwd()}/main.js`)
|
|
40
|
-
|
|
41
|
-
if(await main.exists()) {
|
|
42
|
-
Router.reqRoutes["/main.js"] = {
|
|
43
|
-
GET: async () => new Response(await main.bytes(), { headers: { 'Content-Type': 'application/javascript' } })
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
await Promise.all([Yon.bundleDependencies(), Yon.bundleComponents(), Yon.bundlePages(), Yon.bundleAssets()])
|
|
48
|
-
|
|
49
|
-
await Bun.write(Bun.file(`${import.meta.dir}/routes.json`), JSON.stringify(Router.routeSlugs))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private static extractComponents(data: string) {
|
|
53
|
-
|
|
54
|
-
const html = new JSDOM('').window.document.createElement('div')
|
|
55
|
-
|
|
56
|
-
html.innerHTML = data
|
|
57
|
-
|
|
58
|
-
return { html, script: html.querySelectorAll('script')[0] }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private static parseHTML(
|
|
62
|
-
elements: HTMLCollection,
|
|
63
|
-
imports: Map<string, Set<string>> = new Map<string, Set<string>>()
|
|
64
|
-
): Array<{ static?: string; render?: string; element?: string }> {
|
|
65
|
-
|
|
66
|
-
const parsed: Array<{ static?: string; render?: string; element?: string }> = [];
|
|
67
|
-
|
|
68
|
-
const parseAttrs = (attrs: NamedNodeMap, hash: string) => Array.from(attrs).map(attr => {
|
|
69
|
-
|
|
70
|
-
if(attr.name.startsWith('@')) {
|
|
71
|
-
return `${attr.name}="` + "${eval(ty_invokeEvent('" + hash + "', '" + attr.value + "'))}" + '"'
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if(attr.name === ":value") {
|
|
75
|
-
return `${attr.name.replace(':', '')}="` + "${eval(ty_assignValue('" + hash + "', '" + attr.value + "'))}" + '"'
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return `${attr.name}="${attr.value}"`
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
const interpolateText = (textContext: string) => textContext.replace(/\{([^{}]+)\}/g, '${$1}').replace(/\{\{([^{}]+)\}\}/g, '{${$1}}')
|
|
82
|
-
|
|
83
|
-
for (const element of Array.from(elements)) {
|
|
84
|
-
|
|
85
|
-
if (element.tagName === "SCRIPT") {
|
|
86
|
-
continue; // Skip script tags as they're handled separately
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if(element.tagName === 'STYLE') {
|
|
90
|
-
element.innerHTML = `@scope { ${element.innerHTML} }`
|
|
91
|
-
parsed.push({ element: `\`${element.outerHTML}\`` })
|
|
92
|
-
continue
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if(element.tagName.startsWith('TY') && !element.tagName.endsWith('LOOP') && !element.tagName.endsWith('LOGIC')) {
|
|
96
|
-
|
|
97
|
-
const component = element.tagName.split('-')[1].toLowerCase()
|
|
98
|
-
|
|
99
|
-
const filepath = Yon.compMapping.get(component)
|
|
100
|
-
|
|
101
|
-
if(filepath) {
|
|
102
|
-
|
|
103
|
-
if(imports.has(filepath)) {
|
|
104
|
-
|
|
105
|
-
if(!imports.get(filepath)?.has(component)) {
|
|
106
|
-
parsed.push({ static: `const { default: ${component} } = import('/components/${filepath}')`})
|
|
107
|
-
imports.get(filepath)?.add(component)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
} else {
|
|
111
|
-
|
|
112
|
-
parsed.push({ static: `const { default: ${component} } = await import('/components/${filepath}')`})
|
|
113
|
-
imports.set(filepath, new Set<string>([component]))
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const hash = Bun.randomUUIDv7().split('-')[3]
|
|
119
|
-
|
|
120
|
-
if(!element.id && !element.tagName.startsWith('TY-')) {
|
|
121
|
-
element.setAttribute(':id', "ty_generateId('" + hash + "', 'id')")
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if(element.children.length > 0) {
|
|
125
|
-
|
|
126
|
-
const text = Array.from(element.childNodes).reduce((a, b) => {
|
|
127
|
-
return a + (b.nodeType === 3 ? b.textContent : '')
|
|
128
|
-
}, '')
|
|
129
|
-
|
|
130
|
-
parsed.push({ element: `\`<${element.tagName.toLowerCase()} ${parseAttrs(element.attributes, hash).join(" ")}>\`` })
|
|
131
|
-
if(text) parsed.push({ element: `\`${interpolateText(text)}\`` })
|
|
132
|
-
parsed.push(...this.parseHTML(element.children, imports))
|
|
133
|
-
parsed.push({ element: `\`</${element.tagName.toLowerCase()}>\`` })
|
|
134
|
-
|
|
135
|
-
} else {
|
|
136
|
-
|
|
137
|
-
if(element.outerHTML.includes('</')) {
|
|
138
|
-
parsed.push({ element: `\`<${element.tagName.toLowerCase()} ${parseAttrs(element.attributes, hash).join(" ")}>\`` })
|
|
139
|
-
if(element.textContent) parsed.push({ element: `\`${interpolateText(element.textContent)}\`` })
|
|
140
|
-
parsed.push({ element: `\`</${element.tagName.toLowerCase()}>\`` })
|
|
141
|
-
} else {
|
|
142
|
-
parsed.push({ element: `\`<${element.tagName.toLowerCase()} ${parseAttrs(element.attributes, hash).join(" ")} />\`` })
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return parsed;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private static async createJSData(html: { static?: string, render?: string, element?: string }[], scriptTag?: HTMLScriptElement) {
|
|
151
|
-
|
|
152
|
-
const inners: string[] = []
|
|
153
|
-
const outers: string[] = []
|
|
154
|
-
|
|
155
|
-
html.forEach(h => {
|
|
156
|
-
if(h.element) {
|
|
157
|
-
if(h.element.includes('<ty-') || h.element.includes('</ty-')) {
|
|
158
|
-
inners.push(h.element)
|
|
159
|
-
} else inners.push(`elements+=${h.element}`)
|
|
160
|
-
}
|
|
161
|
-
if(h.static) outers.push(h.static)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
const tempFile = await Bun.file(`${import.meta.dir}/template.js`).text()
|
|
165
|
-
|
|
166
|
-
return tempFile.replaceAll('// imports', outers.join('\n'))
|
|
167
|
-
.replaceAll('// script', scriptTag?.innerHTML ?? '')
|
|
168
|
-
.replaceAll('// inners', inners.join('\n'))
|
|
169
|
-
.replaceAll(/`<ty-loop :for="(.*?)">`|`<\/ty-loop>`/g, (match, p1) => {
|
|
170
|
-
if(p1) return `for(${p1}) {`
|
|
171
|
-
else return '}'
|
|
172
|
-
})
|
|
173
|
-
.replaceAll(/`<ty-logic :if="(.*?)">`|`<\/ty-logic>`/g, (match, p1) => {
|
|
174
|
-
if(p1) return `if(${p1}) {`
|
|
175
|
-
else return '}'
|
|
176
|
-
})
|
|
177
|
-
.replaceAll(/`<ty-logic :else-if="(.*?)">`|`<\/ty-logic>`/g, (match, p1) => {
|
|
178
|
-
if(p1) return `else if(${p1}) {`
|
|
179
|
-
else return '}'
|
|
180
|
-
})
|
|
181
|
-
.replaceAll(/`<ty-logic :else="">`|`<\/ty-logic>`/g, (match, p1) => {
|
|
182
|
-
if(p1) return `else {`
|
|
183
|
-
else return '}'
|
|
184
|
-
})
|
|
185
|
-
.replaceAll(/:([^"]*)="([^"]*)"/g, '$1="${$2}"')
|
|
186
|
-
.replaceAll(/`<\/ty-(\w+)\s*>`/g, '')
|
|
187
|
-
.replaceAll(/`<ty-([a-zA-Z0-9-]+)(?:\s+([^>]*))>`/g, (match, component, atrributes) => {
|
|
188
|
-
|
|
189
|
-
const matches = atrributes.matchAll(/([a-zA-Z0-9-]+)="([^"]*)"/g)
|
|
190
|
-
|
|
191
|
-
const exports: string[] = []
|
|
192
|
-
|
|
193
|
-
for(const [_, key, value] of matches) {
|
|
194
|
-
exports.push(`${key}=${value}`)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const hash = Bun.randomUUIDv7().split('-')[3]
|
|
198
|
-
|
|
199
|
-
return `
|
|
200
|
-
elements += '<div>'
|
|
201
|
-
|
|
202
|
-
if(!compRenders.has('${hash}')) {
|
|
203
|
-
render = await ${component}(\`${exports.join(';')}\`)
|
|
204
|
-
elements += await render(elemId, event, '${hash}')
|
|
205
|
-
compRenders.set('${hash}', render)
|
|
206
|
-
} else {
|
|
207
|
-
render = compRenders.get('${hash}')
|
|
208
|
-
elements += await render(elemId, event, '${hash}')
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
elements += '</div>'
|
|
212
|
-
`
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private static async addToStatix(html: HTMLDivElement, script: HTMLScriptElement, route: string, dir: 'pages' | 'components') {
|
|
218
|
-
|
|
219
|
-
const module = Yon.parseHTML(html.children)
|
|
220
|
-
|
|
221
|
-
const jsData = await Yon.createJSData(module, script)
|
|
222
|
-
|
|
223
|
-
route = route.replace('.html', `.${script?.lang || 'js'}`)
|
|
224
|
-
|
|
225
|
-
await Bun.write(Bun.file(`/tmp/${route}`), jsData)
|
|
226
|
-
|
|
227
|
-
const result = await Bun.build({
|
|
228
|
-
entrypoints: [`/tmp/${route}`],
|
|
229
|
-
external: ["*"],
|
|
230
|
-
minify: {
|
|
231
|
-
whitespace: true,
|
|
232
|
-
syntax: true
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
route = route.replace('.ts', '.js')
|
|
237
|
-
|
|
238
|
-
Router.reqRoutes[`/${dir}/${route}`] = {
|
|
239
|
-
GET: () => new Response(result.outputs[0], { headers: { 'Content-Type': 'application/javascript' } })
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
private static async bundleAssets() {
|
|
244
|
-
|
|
245
|
-
if(await exists(Router.assetsPath)) {
|
|
246
|
-
|
|
247
|
-
const routes = Array.from(new Bun.Glob(`**/*`).scanSync({ cwd: Router.assetsPath }))
|
|
248
|
-
|
|
249
|
-
for(const route of routes) {
|
|
250
|
-
|
|
251
|
-
const file = Bun.file(`${Router.assetsPath}/${route}`)
|
|
252
|
-
|
|
253
|
-
Router.reqRoutes[`/assets/${route}`] = {
|
|
254
|
-
GET: async () => new Response(await file.bytes(), { headers: { 'Content-Type': file.type }})
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private static async bundlePages() {
|
|
261
|
-
|
|
262
|
-
if(await exists(Router.routesPath)) {
|
|
263
|
-
|
|
264
|
-
const routes = Array.from(new Bun.Glob(`**/${Yon.htmlMethod}`).scanSync({ cwd: Router.routesPath }))
|
|
265
|
-
|
|
266
|
-
for(const route of routes) {
|
|
267
|
-
|
|
268
|
-
await Router.validateRoute(route)
|
|
269
|
-
|
|
270
|
-
const data = await Bun.file(`${Router.routesPath}/${route}`).text()
|
|
271
|
-
|
|
272
|
-
const { html, script } = Yon.extractComponents(data)
|
|
273
|
-
|
|
274
|
-
await Yon.addToStatix(html, script, `${route}.${script?.lang || 'js'}`, 'pages')
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const nfFile = Bun.file(`${process.cwd()}/404.html`)
|
|
279
|
-
|
|
280
|
-
const data = await nfFile.exists() ? await nfFile.text() : await Bun.file(`${import.meta.dir}/404.html`).text()
|
|
281
|
-
|
|
282
|
-
const { html, script } = Yon.extractComponents(data)
|
|
283
|
-
|
|
284
|
-
await Yon.addToStatix(html, script, '404.html', 'pages')
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private static async bundleComponents() {
|
|
288
|
-
|
|
289
|
-
if(await exists(Router.componentsPath)) {
|
|
290
|
-
|
|
291
|
-
const components = Array.from(new Bun.Glob(`**/*.html`).scanSync({ cwd: Router.componentsPath }))
|
|
292
|
-
|
|
293
|
-
for(let comp of components) {
|
|
294
|
-
|
|
295
|
-
const folders = comp.split('/')
|
|
296
|
-
|
|
297
|
-
const filename = folders[folders.length - 1].replace('.html', '')
|
|
298
|
-
|
|
299
|
-
Yon.compMapping.set(filename, comp.replace('.html', '.js'))
|
|
300
|
-
|
|
301
|
-
const data = await Bun.file(`${Router.componentsPath}/${comp}`).text()
|
|
302
|
-
|
|
303
|
-
const { html, script } = Yon.extractComponents(data)
|
|
304
|
-
|
|
305
|
-
await Yon.addToStatix(html, script, comp, 'components')
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
private static async bundleDependencies() {
|
|
311
|
-
|
|
312
|
-
const packageFile = Bun.file(`${process.cwd()}/package.json`)
|
|
313
|
-
|
|
314
|
-
const otherEntries = ['index.js', 'index', 'index.node']
|
|
315
|
-
|
|
316
|
-
if(await packageFile.exists()) {
|
|
317
|
-
|
|
318
|
-
const packages = await packageFile.json()
|
|
319
|
-
|
|
320
|
-
const modules = Object.keys(packages.dependencies ?? {})
|
|
321
|
-
|
|
322
|
-
for(const module of modules) {
|
|
323
|
-
|
|
324
|
-
let modPack = await Bun.file(`${process.cwd()}/node_modules/${module}/package.json`).json()
|
|
325
|
-
|
|
326
|
-
let idx = 0
|
|
327
|
-
let entryExists = false
|
|
328
|
-
|
|
329
|
-
while(!modPack.main && !entryExists && idx < otherEntries.length) {
|
|
330
|
-
|
|
331
|
-
entryExists = await Bun.file(`${process.cwd()}/node_modules/${module}/${otherEntries[idx]}`).exists()
|
|
332
|
-
|
|
333
|
-
if(entryExists) {
|
|
334
|
-
modPack.main = otherEntries[idx]
|
|
335
|
-
break
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
idx++
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if(!modPack.main) continue
|
|
342
|
-
|
|
343
|
-
try {
|
|
344
|
-
|
|
345
|
-
const result = await Bun.build({
|
|
346
|
-
entrypoints: [`${process.cwd()}/node_modules/${module}/${(modPack.main as string).replace('./', '')}`],
|
|
347
|
-
minify: true
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
for(const output of result.outputs) {
|
|
351
|
-
Router.reqRoutes[`/modules/${module}.js`] = {
|
|
352
|
-
GET: () => new Response(output, { headers: { 'Content-Type': 'application/javascript' } })
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
} catch(e) {}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
package/src/router.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
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
|
-
static readonly assetsPath = `${process.cwd()}/assets`
|
|
23
|
-
|
|
24
|
-
private static readonly allMethods = process.env.ALLOW_METHODS ? process.env.ALLOW_METHODS.split(',') : ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
|
|
25
|
-
|
|
26
|
-
static headers = {
|
|
27
|
-
"Access-Control-Allow-Headers": process.env.ALLOW_HEADERS || "",
|
|
28
|
-
"Access-Control-Allow-Origin": process.env.ALLLOW_ORGINS || "",
|
|
29
|
-
"Access-Control-Allow-Credential": process.env.ALLOW_CREDENTIALS || "false",
|
|
30
|
-
"Access-Control-Expose-Headers": process.env.ALLOW_EXPOSE_HEADERS || "",
|
|
31
|
-
"Access-Control-Max-Age": process.env.ALLOW_MAX_AGE || "",
|
|
32
|
-
"Access-Control-Allow-Methods": process.env.ALLOW_METHODS || ""
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
static async validateRoute(route: string, staticPaths: string[] = []) {
|
|
36
|
-
|
|
37
|
-
const paths = route.split('/')
|
|
38
|
-
|
|
39
|
-
const pattern = /^:.*/
|
|
40
|
-
|
|
41
|
-
const slugs: Record<string, number> = {}
|
|
42
|
-
|
|
43
|
-
if(pattern.test(paths[0])) throw new Error(`Invalid route ${route}`)
|
|
44
|
-
|
|
45
|
-
paths.forEach((path, idx) => {
|
|
46
|
-
|
|
47
|
-
if(pattern.test(path) && (pattern.test(paths[idx - 1]) || pattern.test(paths[idx + 1]))) {
|
|
48
|
-
throw new Error(`Invalid route ${route}`)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if(pattern.test(path)) slugs[path] = idx
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
const staticPath = paths.filter((path) => !pattern.test(path)).join(',')
|
|
55
|
-
|
|
56
|
-
if(staticPaths.includes(staticPath)) throw new Error(`Duplicate route ${route}`)
|
|
57
|
-
|
|
58
|
-
staticPaths.push(staticPath)
|
|
59
|
-
|
|
60
|
-
await $`chmod +x ${Router.routesPath}/${route}`
|
|
61
|
-
|
|
62
|
-
const method = paths.pop()!
|
|
63
|
-
|
|
64
|
-
route = `/${paths.join('/')}`
|
|
65
|
-
|
|
66
|
-
if(!Router.allRoutes.has(route)) Router.allRoutes.set(route, new Set<string>())
|
|
67
|
-
|
|
68
|
-
Router.allRoutes.get(route)?.add(method)
|
|
69
|
-
|
|
70
|
-
if(Object.keys(slugs).length > 0 || method === 'HTML') Router.routeSlugs[route] = slugs
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
static parseRequest(request: BunRequest) {
|
|
74
|
-
|
|
75
|
-
const req: Record<string, any> = {}
|
|
76
|
-
|
|
77
|
-
req.headers = Object.fromEntries(request.headers)
|
|
78
|
-
req.cache = request.cache
|
|
79
|
-
req.credentials = request.credentials
|
|
80
|
-
req.destination = request.destination
|
|
81
|
-
req.integrity = request.integrity
|
|
82
|
-
req.keepalive = request.keepalive
|
|
83
|
-
req.method = request.method
|
|
84
|
-
req.mode = request.mode
|
|
85
|
-
req.redirect = request.redirect
|
|
86
|
-
req.referrer = request.referrer
|
|
87
|
-
req.referrerPolicy = request.referrerPolicy
|
|
88
|
-
req.url = request.url
|
|
89
|
-
req.parans = request.params
|
|
90
|
-
|
|
91
|
-
return req
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
static processRequest(request: BunRequest, route: string, ctx: _ctx) {
|
|
95
|
-
|
|
96
|
-
const { params } = Router.getParams(request, route)
|
|
97
|
-
|
|
98
|
-
ctx.slugs = request.params
|
|
99
|
-
|
|
100
|
-
const searchParams = new URL(request.url).searchParams
|
|
101
|
-
|
|
102
|
-
let queryParams: Record<string, any> | undefined;
|
|
103
|
-
|
|
104
|
-
if(searchParams.size > 0) queryParams = Router.parseKVParams(searchParams)
|
|
105
|
-
|
|
106
|
-
ctx.params = params
|
|
107
|
-
ctx.query = queryParams
|
|
108
|
-
|
|
109
|
-
return { handler: `${Router.routesPath}${route}/${request.method}`, ctx }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private static getParams(request: BunRequest, route: string) {
|
|
113
|
-
|
|
114
|
-
const url = new URL(request.url)
|
|
115
|
-
|
|
116
|
-
const params = url.pathname.split("/").slice(route.split("/").length)
|
|
117
|
-
|
|
118
|
-
return { params: Router.parseParams(params) }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
static parseParams(input: string[]) {
|
|
122
|
-
|
|
123
|
-
const params: (string | boolean | number | null | undefined)[] = []
|
|
124
|
-
|
|
125
|
-
for(const param of input) {
|
|
126
|
-
|
|
127
|
-
const num = Number(param)
|
|
128
|
-
|
|
129
|
-
if(!Number.isNaN(num)) params.push(num)
|
|
130
|
-
|
|
131
|
-
else if(param === 'true') params.push(true)
|
|
132
|
-
|
|
133
|
-
else if(param === 'false') params.push(false)
|
|
134
|
-
|
|
135
|
-
else if(param === 'null') params.push(null)
|
|
136
|
-
|
|
137
|
-
else if(param === 'undefined') params.push(undefined)
|
|
138
|
-
|
|
139
|
-
else params.push(param)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return params
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private static parseKVParams(input: URLSearchParams | FormData) {
|
|
146
|
-
|
|
147
|
-
const params: Record<string, any> = {}
|
|
148
|
-
|
|
149
|
-
for(const [key, val] of input) {
|
|
150
|
-
|
|
151
|
-
if(typeof val === "string") {
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
|
|
155
|
-
params[key] = JSON.parse(val)
|
|
156
|
-
|
|
157
|
-
} catch {
|
|
158
|
-
|
|
159
|
-
const num = Number(val)
|
|
160
|
-
|
|
161
|
-
if(!Number.isNaN(num)) params[key] = num
|
|
162
|
-
|
|
163
|
-
else if(val === 'true') params[key] = true
|
|
164
|
-
|
|
165
|
-
else if(val === 'false') params[key] = false
|
|
166
|
-
|
|
167
|
-
else if(typeof val === "string" && val.includes(',')) params[key] = Router.parseParams(val.split(','))
|
|
168
|
-
|
|
169
|
-
else if(val === 'null') params[key] = null
|
|
170
|
-
|
|
171
|
-
if(params[key] === undefined) params[key] = val
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
} else params[key] = val
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return params
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
static async validateRoutes() {
|
|
181
|
-
|
|
182
|
-
const routes = Array.from(new Bun.Glob(`**/{${Router.allMethods.join(',')},SOCKET}`).scanSync({ cwd: Router.routesPath }))
|
|
183
|
-
|
|
184
|
-
for(const route of routes) await Router.validateRoute(route)
|
|
185
|
-
}
|
|
186
|
-
}
|
package/src/serve.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
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, exists } 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
|
-
let timeout: Timer
|
|
91
|
-
|
|
92
|
-
let websocket: ServerWebSocket<unknown>;
|
|
93
|
-
|
|
94
|
-
const socket = Bun.serve({
|
|
95
|
-
fetch(req) {
|
|
96
|
-
socket.upgrade(req)
|
|
97
|
-
return undefined
|
|
98
|
-
},
|
|
99
|
-
websocket: {
|
|
100
|
-
async open(ws) {
|
|
101
|
-
console.info("HMR Enabled")
|
|
102
|
-
websocket = ws
|
|
103
|
-
},
|
|
104
|
-
message(ws, message) {
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
close(ws, code, reason) {
|
|
108
|
-
console.info(`HMR Closed ${code} ${reason}`)
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
port: 9876
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
if(await exists(Router.routesPath)) {
|
|
115
|
-
|
|
116
|
-
watcher(Router.routesPath, { recursive: true }, () => {
|
|
117
|
-
|
|
118
|
-
if(timeout) clearTimeout(timeout)
|
|
119
|
-
|
|
120
|
-
timeout = setTimeout(async () => {
|
|
121
|
-
console.info("HMR Update")
|
|
122
|
-
await configureRoutes()
|
|
123
|
-
server.reload({ routes: Router.reqRoutes })
|
|
124
|
-
if(websocket) websocket.send('')
|
|
125
|
-
}, 1500)
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if(await exists(Router.componentsPath)) {
|
|
130
|
-
|
|
131
|
-
watcher(Router.componentsPath, { recursive: true }, () => {
|
|
132
|
-
|
|
133
|
-
if(timeout) clearTimeout(timeout)
|
|
134
|
-
|
|
135
|
-
timeout = setTimeout(async () => {
|
|
136
|
-
console.info("HMR Update")
|
|
137
|
-
await configureRoutes()
|
|
138
|
-
server.reload({ routes: Router.reqRoutes })
|
|
139
|
-
if(websocket) websocket.send('')
|
|
140
|
-
}, 1500)
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const elapsed = Date.now() - start
|
|
146
|
-
|
|
147
|
-
console.info(`Live Server is running on http://${server.hostname}:${server.port} (Press CTRL+C to quit) - ${elapsed.toFixed(2)}ms`)
|