abret 0.1.3 → 0.1.5

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/dist/html.d.ts CHANGED
@@ -6,57 +6,12 @@ export { AsyncBuffer, SafeString, VNode, Fragment, type JSXNode };
6
6
  export declare class HTMLResponse extends Response {
7
7
  private _bodySource;
8
8
  constructor(body: any, init?: ResponseInit);
9
- /**
10
- * Initializes the response with new options.
11
- *
12
- * @param newInit - The new options to apply to the response.
13
- * @returns A new HTMLResponse instance with the updated options.
14
- *
15
- * @example
16
- * ```ts
17
- * const response = new HTMLResponse(<div>Hello World</div>).init({ status: 200 });
18
- * ```
19
- */
20
9
  init(newInit: ResponseInit): HTMLResponse;
21
- /**
22
- * Adds a DOCTYPE declaration to the response body.
23
- *
24
- * @param dt - The DOCTYPE declaration to add. Can be a string or a boolean.
25
- * @returns A new HTMLResponse instance with the DOCTYPE declaration added.
26
- *
27
- * @example
28
- * ```ts
29
- * const response = new HTMLResponse(<div>Hello World</div>).doctype(true);
30
- * ```
31
- */
32
10
  doctype(dt?: string | boolean): HTMLResponse;
33
11
  }
34
12
  /**
35
13
  * Creates an implementation of `HTMLResponse`.
36
14
  */
37
15
  export declare function html(bodyOrStrings: JSXNode | TemplateStringsArray, ...args: any[]): HTMLResponse;
38
- /**
39
- * Renders a JSXNode to a SafeString or Promise<SafeString>.
40
- *
41
- * @param node - The JSXNode to render.
42
- * @returns A SafeString or Promise<SafeString> containing the rendered HTML.
43
- *
44
- * @example
45
- * ```tsx
46
- * const rendered = render(<div>Hello World</div>);
47
- * ```
48
- */
49
16
  export declare function render(node: any): SafeString | Promise<SafeString>;
50
- /**
51
- * Creates a raw HTML string that will not be escaped when rendered.
52
- * Equivalent to using `new SafeString(str)`.
53
- *
54
- * @param str - The raw HTML string.
55
- * @returns A SafeString instance.
56
- *
57
- * @example
58
- * ```tsx
59
- * <div>{raw("<span>Raw HTML</span>")}</div>
60
- * ```
61
- */
62
17
  export declare function raw(str: string): SafeString;
package/dist/html.js CHANGED
@@ -6,10 +6,15 @@ import {
6
6
  VNode
7
7
  } from "./chunk-m9t91z6h.js";
8
8
  import {
9
- getContextStore
9
+ createContext,
10
+ getContextStore,
11
+ runWithContextValue,
12
+ useContext
10
13
  } from "./chunk-xw5b0251.js";
11
14
 
12
15
  // src/html.ts
16
+ var HeadContext = createContext("abret-head");
17
+
13
18
  class HTMLResponse extends Response {
14
19
  _bodySource;
15
20
  constructor(body, init) {
@@ -68,19 +73,19 @@ var MODE_PROPVAL = 4;
68
73
  var MODE_PROPVAL_QUOTE = 5;
69
74
  var MODE_CLOSE_TAG = 6;
70
75
  function html(bodyOrStrings, ...args) {
71
- if (Array.isArray(bodyOrStrings) && bodyOrStrings.raw) {
72
- const vnode = parse(bodyOrStrings, args);
73
- const rendered2 = render(vnode);
74
- if (rendered2 instanceof Promise) {
75
- return new HTMLResponse(rendered2.then((s) => raw(processMetadata(s.toString()))));
76
+ const headCollection = [];
77
+ const runRender = () => {
78
+ if (Array.isArray(bodyOrStrings) && bodyOrStrings.raw) {
79
+ const vnode = parse(bodyOrStrings, args);
80
+ return render(vnode);
76
81
  }
77
- return new HTMLResponse(raw(processMetadata(rendered2.toString())));
78
- }
79
- const rendered = render(bodyOrStrings);
82
+ return render(bodyOrStrings);
83
+ };
84
+ const rendered = runWithContextValue(HeadContext, headCollection, runRender);
80
85
  if (rendered instanceof Promise) {
81
- return new HTMLResponse(rendered.then((s) => raw(processMetadata(s.toString()))));
86
+ return new HTMLResponse(rendered.then((s) => raw(injectMetadata(s.toString(), headCollection))));
82
87
  }
83
- return new HTMLResponse(raw(processMetadata(rendered.toString())));
88
+ return new HTMLResponse(raw(injectMetadata(rendered.toString(), headCollection)));
84
89
  }
85
90
  function parse(statics, fields) {
86
91
  let mode = MODE_TEXT;
@@ -106,7 +111,7 @@ function parse(statics, fields) {
106
111
  if (buffer === "...") {
107
112
  Object.assign(active.props, val);
108
113
  buffer = "";
109
- } else {}
114
+ }
110
115
  } else if (mode === MODE_PROPNAME) {
111
116
  propName = fields[i - 1];
112
117
  mode = MODE_PROPVAL;
@@ -301,6 +306,59 @@ function render(node) {
301
306
  if (typeof node.tag === "string") {
302
307
  const tag = node.tag;
303
308
  const { children, dangerouslySetInnerHTML, ...rest } = node.props;
309
+ if (tag === "title" || tag === "meta" || tag === "link") {
310
+ const headStore = useContext(HeadContext);
311
+ if (headStore) {
312
+ const attrs2 = Object.entries(rest).map(([key2, value]) => {
313
+ if (value === null || value === undefined || value === false)
314
+ return "";
315
+ if (key2 === "className")
316
+ key2 = "class";
317
+ if (key2 === "style")
318
+ value = renderStyle(value);
319
+ if (value === true)
320
+ return ` ${key2}`;
321
+ return ` ${key2}="${escapeHtml(String(value))}"`;
322
+ }).join("");
323
+ if (tag === "title") {
324
+ const childrenList2 = Array.isArray(children) ? children : [children];
325
+ const renderedContent = render(childrenList2);
326
+ const pushTitle = (content) => {
327
+ headStore.push({
328
+ type: "title",
329
+ content: `<title${attrs2}>${escapeHtml(content)}</title>`
330
+ });
331
+ };
332
+ if (renderedContent instanceof Promise) {
333
+ return renderedContent.then((c) => {
334
+ pushTitle(c.toString());
335
+ return raw("");
336
+ });
337
+ }
338
+ pushTitle(renderedContent.toString());
339
+ return raw("");
340
+ }
341
+ let key;
342
+ if (tag === "meta") {
343
+ if (rest.name)
344
+ key = `name:${rest.name}`;
345
+ else if (rest.property)
346
+ key = `property:${rest.property}`;
347
+ else if (rest.charset)
348
+ key = "charset";
349
+ else if (rest["http-equiv"])
350
+ key = `http-equiv:${rest["http-equiv"]}`;
351
+ } else if (tag === "link" && rest.rel?.toLowerCase() === "canonical") {
352
+ key = "canonical";
353
+ }
354
+ headStore.push({
355
+ type: tag,
356
+ key,
357
+ content: `<${tag}${attrs2} />`
358
+ });
359
+ return raw("");
360
+ }
361
+ }
304
362
  const attrs = Object.entries(rest).map(([key, value]) => {
305
363
  if (value === null || value === undefined || value === false)
306
364
  return "";
@@ -308,6 +366,9 @@ function render(node) {
308
366
  key = "class";
309
367
  if (key === "style")
310
368
  value = renderStyle(value);
369
+ if ((key === "href" || key === "src") && typeof value === "string" && value.trim().toLowerCase().startsWith("javascript:")) {
370
+ value = "";
371
+ }
311
372
  if (value === true)
312
373
  return ` ${key}`;
313
374
  return ` ${key}="${escapeHtml(String(value))}"`;
@@ -365,87 +426,43 @@ function renderStyle(style) {
365
426
  return style;
366
427
  return Object.entries(style).map(([k, v]) => `${kebabCase(k)}:${v}`).join(";");
367
428
  }
368
- function processMetadata(html2) {
369
- const metaTags = [];
370
- const linkTags = [];
371
- let titleTag = null;
372
- const extractedHtml = html2.replace(/<title(?:\s[^>]*)?>([\s\S]*?)<\/title>|<meta(?:\s[^>]*)?\/?>|<link(?:\s[^>]*)?\/?>/gi, (match) => {
373
- if (match.toLowerCase().startsWith("<title")) {
374
- titleTag = match;
375
- return "";
376
- }
377
- if (match.toLowerCase().startsWith("<meta")) {
378
- const name = match.match(/name=["']([^"']+)["']/i);
379
- const property = match.match(/property=["']([^"']+)["']/i);
380
- const charset = match.match(/charset=["']([^"']+)["']/i);
381
- const httpEquiv = match.match(/http-equiv=["']([^"']+)["']/i);
382
- let key;
383
- if (name?.[1])
384
- key = `name:${name[1]}`;
385
- else if (property?.[1])
386
- key = `property:${property[1]}`;
387
- else if (charset?.[1])
388
- key = "charset";
389
- else if (httpEquiv?.[1])
390
- key = `http-equiv:${httpEquiv[1]}`;
391
- metaTags.push({ tag: "meta", content: match, key });
392
- return "";
393
- }
394
- if (match.toLowerCase().startsWith("<link")) {
395
- const rel = match.match(/rel=["']([^"']+)["']/i);
396
- const key = rel?.[1]?.toLowerCase() === "canonical" ? "canonical" : undefined;
397
- linkTags.push({ tag: "link", content: match, key });
398
- return "";
399
- }
400
- return match;
401
- });
429
+ function injectMetadata(html2, tags) {
430
+ if (tags.length === 0)
431
+ return html2;
402
432
  const headContent = [];
403
433
  const metaMap = new Map;
404
- metaTags.forEach((m) => {
405
- if (m.key)
406
- metaMap.set(m.key, m.content);
407
- });
434
+ let titleString = null;
435
+ for (const t of tags) {
436
+ if (t.type === "title") {
437
+ titleString = t.content;
438
+ } else if (t.key) {
439
+ metaMap.set(t.key, t.content);
440
+ } else {
441
+ headContent.push(t.content);
442
+ }
443
+ }
444
+ const finalHead = [];
408
445
  if (metaMap.has("charset")) {
409
- const charsetTag = metaMap.get("charset");
410
- if (charsetTag)
411
- headContent.push(charsetTag);
446
+ finalHead.push(metaMap.get("charset"));
412
447
  metaMap.delete("charset");
413
448
  }
414
- if (titleTag)
415
- headContent.push(titleTag);
449
+ if (titleString) {
450
+ finalHead.push(titleString);
451
+ }
416
452
  if (metaMap.has("name:viewport")) {
417
- const viewportTag = metaMap.get("name:viewport");
418
- if (viewportTag)
419
- headContent.push(viewportTag);
453
+ finalHead.push(metaMap.get("name:viewport"));
420
454
  metaMap.delete("name:viewport");
421
455
  }
422
- metaMap.forEach((v) => {
423
- headContent.push(v);
424
- });
425
- metaTags.filter((m) => !m.key).forEach((m) => {
426
- headContent.push(m.content);
427
- });
428
- const linkMap = new Map;
429
- linkTags.forEach((l) => {
430
- if (l.key)
431
- linkMap.set(l.key, l.content);
432
- else
433
- headContent.push(l.content);
434
- });
435
- linkMap.forEach((v) => {
436
- headContent.push(v);
437
- });
438
- const headString = headContent.join("");
439
- if (extractedHtml.includes("<head>")) {
440
- return extractedHtml.replace("<head>", `<head>${headString}`);
441
- }
442
- if (extractedHtml.match(/<html/i)) {
443
- return extractedHtml.replace(/(<html[^>]*>)/i, `$1<head>${headString}</head>`);
456
+ metaMap.forEach((v) => finalHead.push(v));
457
+ finalHead.push(...headContent);
458
+ const headString = finalHead.join("");
459
+ if (html2.includes("<head>")) {
460
+ return html2.replace("<head>", `<head>${headString}`);
444
461
  }
445
- if (headContent.length > 0) {
446
- return `<head>${headString}</head>${extractedHtml}`;
462
+ if (html2.toLowerCase().includes("<html")) {
463
+ return html2.replace(/(<html[^>]*>)/i, `$1<head>${headString}</head>`);
447
464
  }
448
- return extractedHtml;
465
+ return `<head>${headString}</head>${html2}`;
449
466
  }
450
467
  function raw(str) {
451
468
  return new SafeString(str);
@@ -7,15 +7,22 @@ interface TranspilerOptions {
7
7
  vendorPath?: string;
8
8
  /** Optional list of modules to bundle on startup */
9
9
  prewarm?: string[];
10
+ /** Minify local modules. Defaults to false (recommended for dev) */
11
+ minify?: boolean;
12
+ /** Browser cache TTL for local modules in seconds. Defaults to 0 */
13
+ localMaxAge?: number;
14
+ /** Global identifier replacements */
15
+ define?: Record<string, string>;
16
+ /** Map modules to global variables (e.g., { 'react': 'React' }) */
17
+ globals?: Record<string, string>;
18
+ /** Automatically fallback to esm.sh if package is not found locally */
19
+ cdnFallback?: boolean;
20
+ /** Additional Bun plugins */
21
+ plugins?: any[];
10
22
  }
11
23
  /**
12
24
  * Transpiler middleware that handles on-the-fly TS/TSX transpilation
13
25
  * and automatic npm module bundling (vendor modules).
14
- *
15
- * Usage:
16
- * ```ts
17
- * transpiler({ sourcePath: "./src", staticBasePath: "/_modules" })
18
- * ```
19
26
  */
20
27
  export declare const transpiler: (options: TranspilerOptions) => import("../..").Middleware<string, undefined>;
21
28
  export {};
@@ -5,14 +5,20 @@ import {
5
5
  import"../../chunk-xw5b0251.js";
6
6
 
7
7
  // src/middleware/transpiler/index.ts
8
- import { existsSync, mkdirSync } from "fs";
8
+ import { existsSync, mkdirSync, readFileSync, statSync } from "fs";
9
9
  import path from "path";
10
10
  var transpiler = (options) => {
11
11
  const {
12
12
  sourcePath,
13
13
  staticBasePath,
14
14
  vendorPath = "vendor",
15
- prewarm = []
15
+ prewarm = [],
16
+ minify = false,
17
+ localMaxAge = 0,
18
+ define = {},
19
+ globals = {},
20
+ cdnFallback = false,
21
+ plugins = []
16
22
  } = options;
17
23
  const cacheDir = path.resolve(process.cwd(), "node_modules", ".transpiler");
18
24
  const basePrefix = staticBasePath.endsWith("/") ? staticBasePath : `${staticBasePath}/`;
@@ -20,52 +26,143 @@ var transpiler = (options) => {
20
26
  if (!existsSync(cacheDir)) {
21
27
  mkdirSync(cacheDir, { recursive: true });
22
28
  }
29
+ const publicEnv = {};
30
+ for (const [key, value] of Object.entries(process.env)) {
31
+ if (key.startsWith("PUBLIC_")) {
32
+ publicEnv[`process.env.${key}`] = JSON.stringify(value);
33
+ }
34
+ }
35
+ const defaultDefine = {
36
+ "process.env.NODE_ENV": JSON.stringify("development"),
37
+ ...publicEnv,
38
+ ...define
39
+ };
40
+ const trustedDependencies = new Set([
41
+ ...prewarm || [],
42
+ ...Object.keys(globals)
43
+ ]);
44
+ function registerTrustedDependency(moduleName) {
45
+ if (!moduleName)
46
+ return;
47
+ const normalizedName = moduleName.trim();
48
+ if (normalizedName && !trustedDependencies.has(normalizedName)) {
49
+ trustedDependencies.add(normalizedName);
50
+ }
51
+ }
52
+ if (existsSync(cacheDir)) {
53
+ const files = new Bun.Glob("*.js").scanSync(cacheDir);
54
+ for (const file of files) {
55
+ const moduleName = file.slice(0, -3).replace(/__/g, "/");
56
+ registerTrustedDependency(moduleName);
57
+ }
58
+ }
59
+ const absSourcePath = path.resolve(process.cwd(), sourcePath);
60
+ if (existsSync(absSourcePath)) {
61
+ const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx}");
62
+ const scanner = new Bun.Transpiler({ loader: "tsx" });
63
+ for (const relativePath of glob.scanSync(absSourcePath)) {
64
+ try {
65
+ const fullPath = path.join(absSourcePath, relativePath);
66
+ const contents = readFileSync(fullPath, "utf8");
67
+ const imports = scanner.scan(contents).imports;
68
+ for (const imp of imports) {
69
+ if (!imp.path.startsWith(".") && !imp.path.startsWith("/")) {
70
+ registerTrustedDependency(imp.path);
71
+ }
72
+ }
73
+ } catch {}
74
+ }
75
+ }
76
+ const activeBundles = new Map;
77
+ function resolveModulePath(moduleName) {
78
+ if (globals[moduleName])
79
+ return moduleName;
80
+ try {
81
+ Bun.resolveSync(moduleName, process.cwd());
82
+ registerTrustedDependency(moduleName);
83
+ return `${basePrefix}${vendorPath.replace(/^\/|\/$/g, "")}/${moduleName}`;
84
+ } catch {
85
+ if (cdnFallback) {
86
+ return `https://esm.sh/${moduleName}`;
87
+ }
88
+ return `${basePrefix}${vendorPath.replace(/^\/|\/$/g, "")}/${moduleName}`;
89
+ }
90
+ }
23
91
  async function bundleVendorModule(moduleName) {
24
92
  const cacheKey = moduleName.replace(/\//g, "__");
25
93
  const cachedFile = path.join(cacheDir, `${cacheKey}.js`);
26
94
  if (existsSync(cachedFile))
27
95
  return;
28
- try {
29
- const entryPoint = Bun.resolveSync(moduleName, process.cwd());
30
- const result = await Bun.build({
31
- entrypoints: [entryPoint],
32
- target: "browser",
33
- format: "esm",
34
- minify: true,
35
- plugins: [
36
- {
37
- name: "abret-external-vendor",
38
- setup(build) {
39
- build.onResolve({ filter: /^[^./]/ }, (args) => {
40
- if (args.path === moduleName)
41
- return null;
42
- return { path: args.path, external: true };
43
- });
96
+ if (activeBundles.has(cacheKey)) {
97
+ return activeBundles.get(cacheKey);
98
+ }
99
+ const promise = (async () => {
100
+ if (existsSync(cachedFile))
101
+ return;
102
+ try {
103
+ const entryPoint = Bun.resolveSync(moduleName, process.cwd());
104
+ const globalsPlugin = {
105
+ name: "abret-globals",
106
+ setup(build) {
107
+ for (const moduleName2 of Object.keys(globals)) {
108
+ build.onResolve({ filter: new RegExp(`^${moduleName2}$`) }, () => ({
109
+ path: moduleName2,
110
+ namespace: "abret-globals"
111
+ }));
44
112
  }
113
+ build.onLoad({ filter: /.*/, namespace: "abret-globals" }, (args) => {
114
+ const gName = globals[args.path];
115
+ return {
116
+ contents: `export default globalThis.${gName}; export const ${gName} = globalThis.${gName};`,
117
+ loader: "js"
118
+ };
119
+ });
45
120
  }
46
- ]
47
- });
48
- if (!result.success || result.outputs.length === 0) {
49
- console.error(`[Abret] Failed to bundle vendor module: ${moduleName}`, result.logs);
50
- return;
51
- }
52
- const output = result.outputs[0];
53
- if (!output)
54
- return;
55
- const rawContent = await output.text();
56
- const content = rawContent.replace(/((?:import|export)\s*[\s\S]*?from\s*['"]|import\s*\(['"])([^'"]+)(['"]\)?)/g, (match, prefix, path2, suffix) => {
57
- if (/^(https?:|(?:\/\/))/.test(path2))
58
- return match;
59
- if (!path2.startsWith(".") && !path2.startsWith("/")) {
60
- return `${prefix}${basePrefix}${vendorPath.replace(/^\/|\/$/g, "")}/${path2}${suffix}`;
121
+ };
122
+ const result = await Bun.build({
123
+ entrypoints: [entryPoint],
124
+ target: "browser",
125
+ format: "esm",
126
+ minify: true,
127
+ define: defaultDefine,
128
+ plugins: [
129
+ globalsPlugin,
130
+ {
131
+ name: "abret-external-vendor",
132
+ setup(build) {
133
+ build.onResolve({ filter: /^[^./]/ }, (args) => {
134
+ if (args.path === moduleName || globals[args.path])
135
+ return null;
136
+ if (/^(https?:|(?:\/\/))/.test(args.path)) {
137
+ return { path: args.path, external: true };
138
+ }
139
+ return { path: resolveModulePath(args.path), external: true };
140
+ });
141
+ }
142
+ },
143
+ ...plugins
144
+ ]
145
+ });
146
+ if (!result.success || result.outputs.length === 0) {
147
+ console.error(`[Abret] Failed to bundle vendor module: ${moduleName}`, result.logs);
148
+ return;
61
149
  }
62
- return match;
63
- });
64
- await Bun.write(cachedFile, content);
65
- console.log(`[Abret] Pre-bundled: ${moduleName}`);
66
- } catch (err) {
67
- console.error(`[Abret] Error bundling ${moduleName}:`, err);
68
- }
150
+ const output = result.outputs[0];
151
+ if (!output)
152
+ return;
153
+ const rawContent = await output.text();
154
+ const content = rawContent;
155
+ await Bun.write(cachedFile, content);
156
+ console.log(`[Abret] Pre-bundled: ${moduleName}`);
157
+ } catch (err) {
158
+ console.error(`[Abret] Error bundling ${moduleName}:`, err);
159
+ throw err;
160
+ } finally {
161
+ activeBundles.delete(cacheKey);
162
+ }
163
+ })();
164
+ activeBundles.set(cacheKey, promise);
165
+ return promise;
69
166
  }
70
167
  if (prewarm.length > 0) {
71
168
  for (const moduleName of prewarm) {
@@ -80,6 +177,9 @@ var transpiler = (options) => {
80
177
  }
81
178
  if (pathname.startsWith(vendorPrefix)) {
82
179
  const moduleName = pathname.slice(vendorPrefix.length);
180
+ if (!cdnFallback && !trustedDependencies.has(moduleName)) {
181
+ return next();
182
+ }
83
183
  const cacheKey = moduleName.replace(/\//g, "__");
84
184
  const cachedFile = path.join(cacheDir, `${cacheKey}.js`);
85
185
  if (existsSync(cachedFile)) {
@@ -90,7 +190,13 @@ var transpiler = (options) => {
90
190
  }
91
191
  });
92
192
  }
93
- await bundleVendorModule(moduleName);
193
+ try {
194
+ await bundleVendorModule(moduleName);
195
+ } catch (_err) {
196
+ if (cdnFallback) {
197
+ return Response.redirect(`https://esm.sh/${moduleName}`, 302);
198
+ }
199
+ }
94
200
  if (existsSync(cachedFile)) {
95
201
  return new Response(Bun.file(cachedFile), {
96
202
  headers: {
@@ -102,51 +208,141 @@ var transpiler = (options) => {
102
208
  return next();
103
209
  }
104
210
  const internalPath = pathname.slice(basePrefix.length);
105
- const baseFileName = internalPath.endsWith(".js") ? internalPath.slice(0, -3) : internalPath;
106
- const possibleExtensions = [".tsx", ".ts", ".jsx", ".js"];
211
+ const extname = path.extname(internalPath);
107
212
  let sourceFile = "";
108
- for (const ext of possibleExtensions) {
109
- const p = path.join(path.resolve(sourcePath), (baseFileName.startsWith("/") ? baseFileName.slice(1) : baseFileName) + ext);
110
- if (existsSync(p)) {
213
+ let contentType = "application/javascript";
214
+ const secureResolve = (relativePath) => {
215
+ const cleanRelative = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
216
+ const resolvedPath = path.join(absSourcePath, cleanRelative);
217
+ if (!resolvedPath.startsWith(absSourcePath)) {
218
+ return null;
219
+ }
220
+ return resolvedPath;
221
+ };
222
+ if (extname === ".css") {
223
+ const p = secureResolve(internalPath);
224
+ if (p && existsSync(p)) {
111
225
  sourceFile = p;
112
- break;
226
+ contentType = "text/css";
227
+ }
228
+ } else {
229
+ const baseFileName = internalPath.endsWith(".js") ? internalPath.slice(0, -3) : internalPath;
230
+ const possibleExtensions = [".tsx", ".ts", ".jsx", ".js"];
231
+ for (const ext of possibleExtensions) {
232
+ const p = secureResolve(baseFileName + ext);
233
+ if (p && existsSync(p)) {
234
+ sourceFile = p;
235
+ break;
236
+ }
113
237
  }
114
238
  }
115
239
  if (sourceFile) {
116
- try {
117
- const buildResult = await Bun.build({
118
- entrypoints: [sourceFile],
119
- target: "browser",
120
- format: "esm",
121
- external: ["*"]
240
+ const sourceStat = statSync(sourceFile);
241
+ const fastEtag = `W/"${sourceStat.size}-${sourceStat.mtimeMs}-${minify}"`;
242
+ if (req.headers.get("if-none-match") === fastEtag) {
243
+ return new Response(null, { status: 304 });
244
+ }
245
+ const lockKey = `local:${sourceFile}:${fastEtag}`;
246
+ if (activeBundles.has(lockKey)) {
247
+ const result = await activeBundles.get(lockKey);
248
+ return new Response(result.content, {
249
+ headers: {
250
+ "Content-Type": result.contentType,
251
+ ETag: fastEtag,
252
+ "Cache-Control": localMaxAge > 0 ? `public, max-age=${localMaxAge}` : "no-cache"
253
+ }
122
254
  });
123
- if (!buildResult.success || buildResult.outputs.length === 0) {
124
- console.error(`[Abret] Build error for ${sourceFile}:`, buildResult.logs);
125
- return next();
126
- }
127
- const output = buildResult.outputs[0];
128
- if (!output) {
129
- console.error(`[Abret] No output files generated for ${sourceFile}`);
130
- return next();
131
- }
132
- const transpiledCode = await output.text();
133
- const finalCode = transpiledCode.replace(/((?:import|export)\s*[\s\S]*?from\s*['"]|import\s*\(['"])([^'"]+)(['"]\)?)/g, (match, prefix, path2, suffix) => {
134
- if (/^(https?:|(?:\/\/))/.test(path2))
135
- return match;
136
- if (!path2.startsWith(".") && !path2.startsWith("/")) {
137
- return `${prefix}${basePrefix}${vendorPath.replace(/^\/|\/$/g, "")}/${path2}${suffix}`;
255
+ }
256
+ const buildPromise = (async () => {
257
+ try {
258
+ const buildResult = await Bun.build({
259
+ entrypoints: [sourceFile],
260
+ target: "browser",
261
+ format: "esm",
262
+ minify,
263
+ define: defaultDefine,
264
+ external: ["*"],
265
+ plugins: [
266
+ {
267
+ name: "abret-globals-local",
268
+ setup(build) {
269
+ for (const moduleName of Object.keys(globals)) {
270
+ build.onResolve({ filter: new RegExp(`^${moduleName}$`) }, () => ({
271
+ path: moduleName,
272
+ namespace: "abret-globals"
273
+ }));
274
+ }
275
+ build.onLoad({ filter: /.*/, namespace: "abret-globals" }, (args) => {
276
+ const gName = globals[args.path];
277
+ return {
278
+ contents: `export default globalThis.${gName};`,
279
+ loader: "js"
280
+ };
281
+ });
282
+ }
283
+ },
284
+ ...plugins
285
+ ]
286
+ });
287
+ if (!buildResult.success || buildResult.outputs.length === 0) {
288
+ throw new Error(`Build failed: ${buildResult.logs.map((l) => l.message).join(", ")}`);
138
289
  }
139
- if (path2.startsWith(".") && !path2.split("/").pop()?.includes(".")) {
140
- return `${prefix}${path2}.js${suffix}`;
290
+ const output = buildResult.outputs[0];
291
+ if (!output)
292
+ throw new Error("No output generated");
293
+ const transpiledCode = await output.text();
294
+ if (contentType === "text/css") {
295
+ return { content: transpiledCode, contentType };
296
+ }
297
+ const tokenRegex = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`|\/\/[^\n]*|\/\*[\s\S]*?\*\/)|((?:import|export)\s*[\s\S]*?from\s*['"]|import\s*\(['"])([^'"]+)(['"]\)?)/g;
298
+ const finalCode = transpiledCode.replace(tokenRegex, (match, stringOrComment, prefix, path2, suffix) => {
299
+ if (stringOrComment) {
300
+ return match;
301
+ }
302
+ if (/^(https?:|(?:\/\/))/.test(path2))
303
+ return match;
304
+ if (!path2.startsWith(".") && !path2.startsWith("/")) {
305
+ return `${prefix}${resolveModulePath(path2)}${suffix}`;
306
+ }
307
+ if (path2.startsWith(".") && !path2.split("/").pop()?.includes(".")) {
308
+ return `${prefix}${path2}.js${suffix}`;
309
+ }
310
+ return match;
311
+ });
312
+ return { content: finalCode, contentType };
313
+ } finally {
314
+ activeBundles.delete(lockKey);
315
+ }
316
+ })();
317
+ activeBundles.set(lockKey, buildPromise);
318
+ try {
319
+ const finalResult = await buildPromise;
320
+ return new Response(finalResult.content, {
321
+ headers: {
322
+ "Content-Type": finalResult.contentType,
323
+ ETag: fastEtag,
324
+ "Cache-Control": localMaxAge > 0 ? `public, max-age=${localMaxAge}` : "no-cache"
141
325
  }
142
- return match;
143
- });
144
- return new Response(finalCode, {
145
- headers: { "Content-Type": "application/javascript" }
146
326
  });
147
327
  } catch (err) {
148
- console.error(`[Abret] Transpilation error for ${sourceFile}:`, err);
149
- return next();
328
+ const errorMessage = err.message || "Unknown transpilation error";
329
+ console.error(`[Abret] ${errorMessage} for ${sourceFile}`);
330
+ return new Response(`console.error("[Abret] Build Error in ${sourceFile}:", ${JSON.stringify(errorMessage)});
331
+ if (typeof document !== 'undefined') {
332
+ const div = document.createElement('div');
333
+ div.style.position = 'fixed';
334
+ div.style.top = '0';
335
+ div.style.left = '0';
336
+ div.style.width = '100%';
337
+ div.style.padding = '1rem';
338
+ div.style.background = '#fee2e2';
339
+ div.style.color = '#991b1b';
340
+ div.style.borderBottom = '1px solid #ef4444';
341
+ div.style.zIndex = '999999';
342
+ div.style.fontFamily = 'monospace';
343
+ div.innerText = "[Abret] Build Error in ${sourceFile.split("/").pop()}: " + ${JSON.stringify(errorMessage)};
344
+ document.body.appendChild(div);
345
+ }`, { headers: { "Content-Type": "application/javascript" } });
150
346
  }
151
347
  }
152
348
  return next();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abret",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Fast, type-safe web framework for Bun with built-in JSX and middleware support.",
5
5
  "license": "MIT",
6
6
  "author": "Arisris",