@vyckr/tachyon 1.1.11 → 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 -135
- 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 -364
- 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
|
@@ -10,36 +10,29 @@ export default async function(props) {
|
|
|
10
10
|
|
|
11
11
|
return async function(elemId, event, compId) {
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
elemIds.set('@', new Map())
|
|
16
|
-
elemIds.set('id', new Map())
|
|
17
|
-
elemIds.set('bind', new Map())
|
|
13
|
+
const counters = { id: {}, ev: {}, bind: {} }
|
|
18
14
|
|
|
19
15
|
const ty_generateId = (hash, source) => {
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if(elemIds.get(source).has(hash)) {
|
|
24
|
-
|
|
25
|
-
const degree = elemIds.get(source).get(hash)
|
|
17
|
+
const key = compId ? hash + '-' + compId : hash
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
const map = counters[source]
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
if(key in map) {
|
|
22
|
+
return 'ty-' + key + '-' + map[key]++
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
25
|
+
map[key] = 1
|
|
33
26
|
|
|
34
|
-
return
|
|
27
|
+
return 'ty-' + key + '-0'
|
|
35
28
|
}
|
|
36
29
|
|
|
37
30
|
const ty_invokeEvent = (hash, action) => {
|
|
38
31
|
|
|
39
|
-
if(elemId === ty_generateId(hash, '
|
|
32
|
+
if(elemId === ty_generateId(hash, 'ev')) {
|
|
40
33
|
|
|
41
34
|
if(event && !action.endsWith(')')) {
|
|
42
|
-
return
|
|
35
|
+
return action + "('" + event + "')"
|
|
43
36
|
}
|
|
44
37
|
return action
|
|
45
38
|
}
|
|
@@ -49,7 +42,7 @@ export default async function(props) {
|
|
|
49
42
|
const ty_assignValue = (hash, variable) => {
|
|
50
43
|
|
|
51
44
|
if(elemId === ty_generateId(hash, 'bind') && event) {
|
|
52
|
-
return
|
|
45
|
+
return variable + " = '" + event.value + "'"
|
|
53
46
|
}
|
|
54
47
|
|
|
55
48
|
return variable
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import Router from "../server/route-handler.js";
|
|
2
|
+
import { BunRequest } from 'bun';
|
|
3
|
+
import { exists } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
interface ParsedElement {
|
|
6
|
+
static?: string;
|
|
7
|
+
element?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ComponentData {
|
|
11
|
+
html: string;
|
|
12
|
+
script?: string;
|
|
13
|
+
scriptLang?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TEMPLATE_PATH = `${import.meta.dir}/render-template.js`;
|
|
17
|
+
const ROUTES_JSON_PATH = `${import.meta.dir}/../runtime/route-manifest.json`;
|
|
18
|
+
const LAYOUTS_JSON_PATH = `${import.meta.dir}/../runtime/layout-manifest.json`;
|
|
19
|
+
const NOT_FOUND_PATH = `${import.meta.dir}/../runtime/shells/not-found.html`;
|
|
20
|
+
|
|
21
|
+
const jsResponse = (body: BodyInit) =>
|
|
22
|
+
new Response(body, { headers: { 'Content-Type': 'application/javascript' } });
|
|
23
|
+
|
|
24
|
+
const jsonResponse = (path: string) =>
|
|
25
|
+
async () => new Response(await Bun.file(path).bytes(), { headers: { 'Content-Type': 'application/json' } });
|
|
26
|
+
|
|
27
|
+
export default class Yon {
|
|
28
|
+
|
|
29
|
+
private static readonly htmlMethod = 'HTML'
|
|
30
|
+
private static readonly layoutMethod = 'LAYOUT'
|
|
31
|
+
private static readonly compMapping = new Map<string, string>()
|
|
32
|
+
private static layoutMapping: Record<string, string> = {}
|
|
33
|
+
|
|
34
|
+
static getParams(request: BunRequest, route: string) {
|
|
35
|
+
const url = new URL(request.url)
|
|
36
|
+
const params = url.pathname.split('/').slice(route.split('/').length)
|
|
37
|
+
return { params: Router.parseParams(params) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static async createStaticRoutes() {
|
|
41
|
+
Yon.compMapping.clear()
|
|
42
|
+
Yon.layoutMapping = {}
|
|
43
|
+
|
|
44
|
+
// Build client-side render + HMR scripts
|
|
45
|
+
const result = await Bun.build({
|
|
46
|
+
entrypoints: [`${import.meta.dir}/../runtime/spa-renderer.ts`, `${import.meta.dir}/../runtime/hot-reload-client.ts`],
|
|
47
|
+
minify: true
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
for (const output of result.outputs) {
|
|
51
|
+
Router.reqRoutes[output.path.replace('./', '/')] = {
|
|
52
|
+
GET: async () => jsResponse(output)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// JSON manifests
|
|
57
|
+
Router.reqRoutes["/routes.json"] = { GET: jsonResponse(ROUTES_JSON_PATH) }
|
|
58
|
+
Router.reqRoutes["/layouts.json"] = { GET: jsonResponse(LAYOUTS_JSON_PATH) }
|
|
59
|
+
|
|
60
|
+
// Optional user main.js
|
|
61
|
+
const main = Bun.file(`${process.cwd()}/main.js`)
|
|
62
|
+
if (await main.exists()) {
|
|
63
|
+
Router.reqRoutes["/main.js"] = {
|
|
64
|
+
GET: async () => new Response(await main.bytes(), { headers: { 'Content-Type': 'application/javascript' } })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Bundle all client assets in parallel
|
|
69
|
+
await Promise.all([
|
|
70
|
+
Yon.bundleDependencies(),
|
|
71
|
+
Yon.bundleComponents(),
|
|
72
|
+
Yon.bundleLayouts(),
|
|
73
|
+
Yon.bundlePages(),
|
|
74
|
+
Yon.bundleAssets()
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
// Write manifests after all routes are registered
|
|
78
|
+
await Promise.all([
|
|
79
|
+
Bun.write(Bun.file(ROUTES_JSON_PATH), JSON.stringify(Router.routeSlugs)),
|
|
80
|
+
Bun.write(Bun.file(LAYOUTS_JSON_PATH), JSON.stringify(Yon.layoutMapping))
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Template extraction ────────────────────────────────────────────────────
|
|
85
|
+
private static async extractComponents(data: string): Promise<ComponentData> {
|
|
86
|
+
let scriptContent = '';
|
|
87
|
+
let scriptLang = 'js';
|
|
88
|
+
|
|
89
|
+
const rewriter = new HTMLRewriter()
|
|
90
|
+
.on('script', {
|
|
91
|
+
element(element) {
|
|
92
|
+
const lang = element.getAttribute('lang');
|
|
93
|
+
if (lang) scriptLang = lang;
|
|
94
|
+
},
|
|
95
|
+
text(text) { scriptContent += text.text; }
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const htmlContent = await rewriter.transform(new Response(data)).text();
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
html: htmlContent,
|
|
102
|
+
script: scriptContent || undefined,
|
|
103
|
+
scriptLang
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── HTML → AST parsing ─────────────────────────────────────────────────────
|
|
108
|
+
private static parseHTML(
|
|
109
|
+
htmlContent: string,
|
|
110
|
+
imports: Map<string, Set<string>> = new Map()
|
|
111
|
+
): Promise<ParsedElement[]> {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const parsed: ParsedElement[] = [];
|
|
114
|
+
const tagStack: string[] = [];
|
|
115
|
+
let insideScript = false;
|
|
116
|
+
let insideStyle = false;
|
|
117
|
+
|
|
118
|
+
const genHash = () => Bun.randomUUIDv7().replace(/-/g, '').slice(-8);
|
|
119
|
+
|
|
120
|
+
const formatAttr = (name: string, value: string, hash: string): string => {
|
|
121
|
+
if (name.startsWith('@'))
|
|
122
|
+
return `${name}="\${eval(ty_invokeEvent('${hash}', '${value}'))}"`;
|
|
123
|
+
if (name === ':value')
|
|
124
|
+
return `value="\${eval(ty_assignValue('${hash}', '${value}'))}"`;
|
|
125
|
+
return `${name}="${value}"`;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const interpolate = (text: string) =>
|
|
129
|
+
text.replace(/\{([^{}]+)\}/g, '${$1}').replace(/\{\{([^{}]+)\}\}/g, '{${$1}}');
|
|
130
|
+
|
|
131
|
+
const rewriter = new HTMLRewriter()
|
|
132
|
+
.on('script', {
|
|
133
|
+
element(el) {
|
|
134
|
+
insideScript = true;
|
|
135
|
+
el.onEndTag(() => { insideScript = false; });
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
.on('style', {
|
|
139
|
+
element(el) {
|
|
140
|
+
insideStyle = true;
|
|
141
|
+
el.onEndTag(() => { insideStyle = false; });
|
|
142
|
+
},
|
|
143
|
+
text(text) {
|
|
144
|
+
parsed.push({ element: `\`<style>@scope { ${text.text} }</style>\`` });
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
.on('*', {
|
|
148
|
+
element(element) {
|
|
149
|
+
const tag = element.tagName.toUpperCase();
|
|
150
|
+
|
|
151
|
+
if (tag === 'SCRIPT' || tag === 'STYLE') return;
|
|
152
|
+
|
|
153
|
+
if (tag === 'SLOT') {
|
|
154
|
+
parsed.push({ element: '`<div id="ty-layout-slot"></div>`' });
|
|
155
|
+
element.remove();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const hash = genHash();
|
|
160
|
+
const tagLower = element.tagName.toLowerCase();
|
|
161
|
+
const attrs: Record<string, string> = {};
|
|
162
|
+
|
|
163
|
+
for (const [name, value] of element.attributes) attrs[name] = value;
|
|
164
|
+
|
|
165
|
+
// Component import
|
|
166
|
+
if (tag.endsWith('_')) {
|
|
167
|
+
const compName = tagLower.slice(0, -1);
|
|
168
|
+
const filepath = Yon.compMapping.get(compName);
|
|
169
|
+
const isLazy = 'lazy' in attrs;
|
|
170
|
+
|
|
171
|
+
if (filepath && compName && !isLazy) {
|
|
172
|
+
const existing = imports.get(filepath);
|
|
173
|
+
if (!existing || !existing.has(compName)) {
|
|
174
|
+
const keyword = existing ? 'const' : 'const';
|
|
175
|
+
const awaitPrefix = existing ? '' : 'await ';
|
|
176
|
+
parsed.push({ static: `${keyword} { default: ${compName} } = ${awaitPrefix}import('/components/${filepath}')` });
|
|
177
|
+
if (existing) existing.add(compName);
|
|
178
|
+
else imports.set(filepath, new Set([compName]));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Auto-generate id for non-control, non-component elements
|
|
184
|
+
if (!attrs.id && !tag.endsWith('_') && tag !== 'LOOP' && tag !== 'LOGIC') {
|
|
185
|
+
attrs[':id'] = `ty_generateId('${hash}', 'id')`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const attrStr = Object.entries(attrs).map(([n, v]) => formatAttr(n, v, hash)).join(' ');
|
|
189
|
+
tagStack.push(tagLower);
|
|
190
|
+
|
|
191
|
+
if (element.selfClosing) {
|
|
192
|
+
parsed.push({ element: `\`<${tagLower} ${attrStr} />\`` });
|
|
193
|
+
tagStack.pop();
|
|
194
|
+
} else {
|
|
195
|
+
parsed.push({ element: `\`<${tagLower} ${attrStr}>\`` });
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
text(text) {
|
|
199
|
+
if (text.text.trim() && !insideScript && !insideStyle) {
|
|
200
|
+
parsed.push({ element: `\`${interpolate(text.text)}\`` });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.on('*', {
|
|
205
|
+
element(element) {
|
|
206
|
+
if (element.selfClosing) return;
|
|
207
|
+
const tag = element.tagName.toUpperCase();
|
|
208
|
+
if (tag === 'SCRIPT' || tag === 'STYLE') return;
|
|
209
|
+
element.onEndTag(() => {
|
|
210
|
+
const tagName = tagStack.pop();
|
|
211
|
+
if (tagName) parsed.push({ element: `\`</${tagName}>\`` });
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
rewriter.transform(new Response(htmlContent)).text().then(() => resolve(parsed));
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── JS code generation ─────────────────────────────────────────────────────
|
|
221
|
+
private static async createJSData(elements: ParsedElement[], scriptContent?: string): Promise<string> {
|
|
222
|
+
const statics: string[] = [];
|
|
223
|
+
const body: string[] = [];
|
|
224
|
+
|
|
225
|
+
for (const el of elements) {
|
|
226
|
+
if (el.static) statics.push(el.static);
|
|
227
|
+
if (el.element) {
|
|
228
|
+
// Control flow and component tags are raw JS, not concatenated
|
|
229
|
+
if (el.element.includes('<loop') || el.element.includes('</loop') ||
|
|
230
|
+
el.element.includes('<logic') || el.element.includes('</logic') ||
|
|
231
|
+
/<([A-Za-z0-9-]+)_\s+([^/>]*)\/>/.test(el.element)) {
|
|
232
|
+
body.push(el.element);
|
|
233
|
+
} else {
|
|
234
|
+
body.push(`elements+=${el.element}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let code = await Bun.file(TEMPLATE_PATH).text();
|
|
240
|
+
|
|
241
|
+
code = code
|
|
242
|
+
.replaceAll('// imports', statics.join('\n'))
|
|
243
|
+
.replaceAll('// script', scriptContent ?? '')
|
|
244
|
+
.replaceAll('// inners', body.join('\n'));
|
|
245
|
+
|
|
246
|
+
// Transform control flow tags to JS
|
|
247
|
+
code = code
|
|
248
|
+
.replaceAll(/`<loop :for="(.*?)">`|`<\/loop>`/g, (_, expr) => expr ? `for(${expr}) {` : '}')
|
|
249
|
+
.replaceAll(/`<logic :if="(.*?)">`|`<\/logic>`/g, (_, expr) => expr ? `if(${expr}) {` : '}')
|
|
250
|
+
.replaceAll(/`<logic :else-if="(.*?)">`|`<\/logic>`/g, (_, expr) => expr ? `else if(${expr}) {` : '}')
|
|
251
|
+
.replaceAll(/`<logic else="">`|`<\/logic>`/g, (_, expr) => expr ? `else {` : '}');
|
|
252
|
+
|
|
253
|
+
// Bind dynamic attributes :attr="expr" → attr="${expr}"
|
|
254
|
+
code = code.replaceAll(/:(\w[\w-]*)="([^"]*)"/g, '$1="${$2}"');
|
|
255
|
+
|
|
256
|
+
// Transform component invocations
|
|
257
|
+
code = code.replaceAll(/`<([A-Za-z0-9-]+)_\s+([^/>]*)\/>`/g, (_, component, attrStr) => {
|
|
258
|
+
const matches = attrStr.matchAll(/([a-zA-Z0-9-@]+)="([^"]*)"/g);
|
|
259
|
+
const props: string[] = [];
|
|
260
|
+
const events: string[] = [];
|
|
261
|
+
const hash = genHash();
|
|
262
|
+
const isLazy = /\blazy\b/.test(attrStr);
|
|
263
|
+
|
|
264
|
+
for (const [, key, value] of matches) {
|
|
265
|
+
if (key === 'lazy') continue;
|
|
266
|
+
if (key.startsWith('@')) {
|
|
267
|
+
events.push(`${key}="${value.replace(/(ty_invokeEvent\(')([^"]+)(',[^)]+\))/g, `$1${hash}$3`)}"`);
|
|
268
|
+
} else {
|
|
269
|
+
props.push(`${key}=${value}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const genId = "${ty_generateId('" + hash + "', 'id')}";
|
|
274
|
+
|
|
275
|
+
if (isLazy) {
|
|
276
|
+
const filepath = Yon.compMapping.get(component);
|
|
277
|
+
const propsEncoded = props.length ? props.join(';') : '';
|
|
278
|
+
return `
|
|
279
|
+
elements += \`<div id="${genId}" data-lazy-component="${component}" data-lazy-path="/components/${filepath}" data-lazy-props="${propsEncoded}" ${events.join(' ')}></div>\`
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return `
|
|
284
|
+
elements += \`<div id="${genId}" ${events.join(' ')}>\`
|
|
285
|
+
if(!compRenders.has('${hash}')) {
|
|
286
|
+
render = await ${component}(\`${props.join(';')}\`)
|
|
287
|
+
elements += await render(elemId, event, '${hash}')
|
|
288
|
+
compRenders.set('${hash}', render)
|
|
289
|
+
} else {
|
|
290
|
+
render = compRenders.get('${hash}')
|
|
291
|
+
elements += await render(elemId, event, '${hash}')
|
|
292
|
+
}
|
|
293
|
+
elements += '</div>'
|
|
294
|
+
`;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return code;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Build & register a single template module ──────────────────────────────
|
|
301
|
+
private static async registerModule(data: ComponentData, route: string, dir: 'pages' | 'components' | 'layouts') {
|
|
302
|
+
const parsed = await Yon.parseHTML(data.html);
|
|
303
|
+
const jsCode = await Yon.createJSData(parsed, data.script);
|
|
304
|
+
|
|
305
|
+
const srcRoute = route.replace('.html', `.${data.scriptLang || 'js'}`);
|
|
306
|
+
const tmpPath = `/tmp/${srcRoute}`;
|
|
307
|
+
|
|
308
|
+
await Bun.write(Bun.file(tmpPath), jsCode);
|
|
309
|
+
|
|
310
|
+
const result = await Bun.build({
|
|
311
|
+
entrypoints: [tmpPath],
|
|
312
|
+
external: ["*"],
|
|
313
|
+
minify: { whitespace: true, syntax: true }
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const outRoute = srcRoute.replace('.ts', '.js');
|
|
317
|
+
|
|
318
|
+
Router.reqRoutes[`/${dir}/${outRoute}`] = {
|
|
319
|
+
GET: () => jsResponse(result.outputs[0])
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Asset bundlers ─────────────────────────────────────────────────────────
|
|
324
|
+
private static async bundleAssets() {
|
|
325
|
+
if (!await exists(Router.assetsPath)) return;
|
|
326
|
+
|
|
327
|
+
for (const route of new Bun.Glob('**/*').scanSync({ cwd: Router.assetsPath })) {
|
|
328
|
+
const file = Bun.file(`${Router.assetsPath}/${route}`);
|
|
329
|
+
Router.reqRoutes[`/assets/${route}`] = {
|
|
330
|
+
GET: async () => new Response(await file.bytes(), { headers: { 'Content-Type': file.type } })
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private static async bundlePages() {
|
|
336
|
+
if (await exists(Router.routesPath)) {
|
|
337
|
+
for (const route of new Bun.Glob(`**/${Yon.htmlMethod}`).scanSync({ cwd: Router.routesPath })) {
|
|
338
|
+
await Router.validateRoute(route);
|
|
339
|
+
const data = await Yon.extractComponents(await Bun.file(`${Router.routesPath}/${route}`).text());
|
|
340
|
+
await Yon.registerModule(data, `${route}.${data.scriptLang || 'js'}`, 'pages');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 404 page
|
|
345
|
+
const nfFile = Bun.file(`${process.cwd()}/404.html`);
|
|
346
|
+
const nfContent = await nfFile.exists()
|
|
347
|
+
? await nfFile.text()
|
|
348
|
+
: await Bun.file(NOT_FOUND_PATH).text();
|
|
349
|
+
const nfData = await Yon.extractComponents(nfContent);
|
|
350
|
+
await Yon.registerModule(nfData, '404.html', 'pages');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private static async bundleLayouts() {
|
|
354
|
+
if (!await exists(Router.routesPath)) return;
|
|
355
|
+
|
|
356
|
+
for (const layout of new Bun.Glob(`**/${Yon.layoutMethod}`).scanSync({ cwd: Router.routesPath })) {
|
|
357
|
+
const prefix = layout === Yon.layoutMethod ? '/' : `/${layout.replace(`/${Yon.layoutMethod}`, '')}`;
|
|
358
|
+
const data = await Yon.extractComponents(await Bun.file(`${Router.routesPath}/${layout}`).text());
|
|
359
|
+
const layoutRoute = layout === Yon.layoutMethod ? Yon.layoutMethod : layout;
|
|
360
|
+
await Yon.registerModule(data, `${layoutRoute}.${data.scriptLang || 'js'}`, 'layouts');
|
|
361
|
+
Yon.layoutMapping[prefix] = `/layouts/${layoutRoute}.js`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private static async bundleComponents() {
|
|
366
|
+
if (!await exists(Router.componentsPath)) return;
|
|
367
|
+
|
|
368
|
+
for (const comp of new Bun.Glob('**/*.html').scanSync({ cwd: Router.componentsPath })) {
|
|
369
|
+
const filename = comp.split('/').pop()!.replace('.html', '');
|
|
370
|
+
Yon.compMapping.set(filename, comp.replace('.html', '.js'));
|
|
371
|
+
const data = await Yon.extractComponents(await Bun.file(`${Router.componentsPath}/${comp}`).text());
|
|
372
|
+
await Yon.registerModule(data, comp, 'components');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private static async bundleDependencies() {
|
|
377
|
+
const packageFile = Bun.file(`${process.cwd()}/package.json`);
|
|
378
|
+
if (!await packageFile.exists()) return;
|
|
379
|
+
|
|
380
|
+
const packages = await packageFile.json();
|
|
381
|
+
const modules = Object.keys(packages.dependencies ?? {});
|
|
382
|
+
const fallbackEntries = ['index.js', 'index', 'index.node'];
|
|
383
|
+
|
|
384
|
+
for (const mod of modules) {
|
|
385
|
+
const modPackPath = `${process.cwd()}/node_modules/${mod}/package.json`;
|
|
386
|
+
const modPack = await Bun.file(modPackPath).json();
|
|
387
|
+
|
|
388
|
+
if (!modPack.main) {
|
|
389
|
+
for (const entry of fallbackEntries) {
|
|
390
|
+
if (await Bun.file(`${process.cwd()}/node_modules/${mod}/${entry}`).exists()) {
|
|
391
|
+
modPack.main = entry;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!modPack.main) continue;
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const result = await Bun.build({
|
|
401
|
+
entrypoints: [`${process.cwd()}/node_modules/${mod}/${(modPack.main as string).replace('./', '')}`],
|
|
402
|
+
minify: true
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
for (const output of result.outputs) {
|
|
406
|
+
Router.reqRoutes[`/modules/${mod}.js`] = {
|
|
407
|
+
GET: () => jsResponse(output)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.warn(`Failed to bundle module '${mod}': ${(e as Error).message}`, process.pid);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function genHash(): string {
|
|
418
|
+
return Bun.randomUUIDv7().replace(/-/g, '').slice(-8);
|
|
419
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const HMR_RECONNECT_MS = 3000
|
|
2
|
+
|
|
3
|
+
function connectHMR() {
|
|
4
|
+
fetch('/hmr').then(async res => {
|
|
5
|
+
|
|
6
|
+
for await(const _ of res.body!) {
|
|
7
|
+
window.location.reload()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
}).catch(() => {
|
|
11
|
+
setTimeout(connectHMR, HMR_RECONNECT_MS)
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
connectHMR()
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title></title>
|
|
8
8
|
<script src="/main.js"></script>
|
|
9
|
-
<script src="/
|
|
10
|
-
<script src="/
|
|
9
|
+
<script src="/spa-renderer.js"></script>
|
|
10
|
+
<script src="/hot-reload-client.js"></script>
|
|
11
11
|
</head>
|
|
12
12
|
<body></body>
|
|
13
13
|
</html>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
document.title = "404 — Page Not Found"
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<style>
|
|
6
|
+
.nf-container {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
min-height: 80vh;
|
|
12
|
+
text-align: center;
|
|
13
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
14
|
+
color: #334155;
|
|
15
|
+
}
|
|
16
|
+
.nf-code {
|
|
17
|
+
font-size: 8rem;
|
|
18
|
+
font-weight: 800;
|
|
19
|
+
letter-spacing: -0.04em;
|
|
20
|
+
background: linear-gradient(135deg, #6366f1, #8b5cf6, #a855f7);
|
|
21
|
+
-webkit-background-clip: text;
|
|
22
|
+
-webkit-text-fill-color: transparent;
|
|
23
|
+
background-clip: text;
|
|
24
|
+
line-height: 1;
|
|
25
|
+
margin: 0;
|
|
26
|
+
}
|
|
27
|
+
.nf-label {
|
|
28
|
+
font-size: 1.25rem;
|
|
29
|
+
font-weight: 500;
|
|
30
|
+
color: #64748b;
|
|
31
|
+
margin: 0.5rem 0 1.5rem;
|
|
32
|
+
}
|
|
33
|
+
.nf-message {
|
|
34
|
+
font-size: 0.95rem;
|
|
35
|
+
color: #94a3b8;
|
|
36
|
+
max-width: 360px;
|
|
37
|
+
margin-bottom: 2rem;
|
|
38
|
+
line-height: 1.5;
|
|
39
|
+
}
|
|
40
|
+
.nf-home {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.5rem;
|
|
44
|
+
padding: 0.65rem 1.5rem;
|
|
45
|
+
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
46
|
+
color: #fff;
|
|
47
|
+
text-decoration: none;
|
|
48
|
+
border-radius: 9999px;
|
|
49
|
+
font-size: 0.9rem;
|
|
50
|
+
font-weight: 500;
|
|
51
|
+
transition: transform 0.15s, box-shadow 0.15s;
|
|
52
|
+
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
|
|
53
|
+
}
|
|
54
|
+
.nf-home:hover {
|
|
55
|
+
transform: translateY(-1px);
|
|
56
|
+
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.45);
|
|
57
|
+
}
|
|
58
|
+
.nf-divider {
|
|
59
|
+
width: 64px;
|
|
60
|
+
height: 4px;
|
|
61
|
+
border-radius: 2px;
|
|
62
|
+
background: linear-gradient(90deg, #6366f1, #a855f7);
|
|
63
|
+
margin-bottom: 1.5rem;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
66
|
+
|
|
67
|
+
<div class="nf-container">
|
|
68
|
+
<p class="nf-code">404</p>
|
|
69
|
+
<div class="nf-divider"></div>
|
|
70
|
+
<p class="nf-label">Page Not Found</p>
|
|
71
|
+
<p class="nf-message">The page you're looking for doesn't exist or has been moved.</p>
|
|
72
|
+
<a class="nf-home" href="/">← Back to Home</a>
|
|
73
|
+
</div>
|