abret 0.1.4 → 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);
@@ -5,7 +5,7 @@ import {
5
5
  import"../../chunk-xw5b0251.js";
6
6
 
7
7
  // src/middleware/transpiler/index.ts
8
- import { existsSync, mkdirSync, statSync } from "fs";
8
+ import { existsSync, mkdirSync, readFileSync, statSync } from "fs";
9
9
  import path from "path";
10
10
  var transpiler = (options) => {
11
11
  const {
@@ -37,12 +37,49 @@ var transpiler = (options) => {
37
37
  ...publicEnv,
38
38
  ...define
39
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
+ }
40
76
  const activeBundles = new Map;
41
77
  function resolveModulePath(moduleName) {
42
78
  if (globals[moduleName])
43
79
  return moduleName;
44
80
  try {
45
81
  Bun.resolveSync(moduleName, process.cwd());
82
+ registerTrustedDependency(moduleName);
46
83
  return `${basePrefix}${vendorPath.replace(/^\/|\/$/g, "")}/${moduleName}`;
47
84
  } catch {
48
85
  if (cdnFallback) {
@@ -96,7 +133,10 @@ var transpiler = (options) => {
96
133
  build.onResolve({ filter: /^[^./]/ }, (args) => {
97
134
  if (args.path === moduleName || globals[args.path])
98
135
  return null;
99
- return { path: args.path, external: true };
136
+ if (/^(https?:|(?:\/\/))/.test(args.path)) {
137
+ return { path: args.path, external: true };
138
+ }
139
+ return { path: resolveModulePath(args.path), external: true };
100
140
  });
101
141
  }
102
142
  },
@@ -111,18 +151,12 @@ var transpiler = (options) => {
111
151
  if (!output)
112
152
  return;
113
153
  const rawContent = await output.text();
114
- const content = rawContent.replace(/((?:import|export)\s*[\s\S]*?from\s*['"]|import\s*\(['"])([^'"]+)(['"]\)?)/g, (match, prefix, path2, suffix) => {
115
- if (/^(https?:|(?:\/\/))/.test(path2))
116
- return match;
117
- if (!path2.startsWith(".") && !path2.startsWith("/")) {
118
- return `${prefix}${resolveModulePath(path2)}${suffix}`;
119
- }
120
- return match;
121
- });
154
+ const content = rawContent;
122
155
  await Bun.write(cachedFile, content);
123
156
  console.log(`[Abret] Pre-bundled: ${moduleName}`);
124
157
  } catch (err) {
125
158
  console.error(`[Abret] Error bundling ${moduleName}:`, err);
159
+ throw err;
126
160
  } finally {
127
161
  activeBundles.delete(cacheKey);
128
162
  }
@@ -143,6 +177,9 @@ var transpiler = (options) => {
143
177
  }
144
178
  if (pathname.startsWith(vendorPrefix)) {
145
179
  const moduleName = pathname.slice(vendorPrefix.length);
180
+ if (!cdnFallback && !trustedDependencies.has(moduleName)) {
181
+ return next();
182
+ }
146
183
  const cacheKey = moduleName.replace(/\//g, "__");
147
184
  const cachedFile = path.join(cacheDir, `${cacheKey}.js`);
148
185
  if (existsSync(cachedFile)) {
@@ -153,7 +190,13 @@ var transpiler = (options) => {
153
190
  }
154
191
  });
155
192
  }
156
- 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
+ }
157
200
  if (existsSync(cachedFile)) {
158
201
  return new Response(Bun.file(cachedFile), {
159
202
  headers: {
@@ -168,9 +211,17 @@ var transpiler = (options) => {
168
211
  const extname = path.extname(internalPath);
169
212
  let sourceFile = "";
170
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
+ };
171
222
  if (extname === ".css") {
172
- const p = path.join(path.resolve(sourcePath), internalPath);
173
- if (existsSync(p)) {
223
+ const p = secureResolve(internalPath);
224
+ if (p && existsSync(p)) {
174
225
  sourceFile = p;
175
226
  contentType = "text/css";
176
227
  }
@@ -178,8 +229,8 @@ var transpiler = (options) => {
178
229
  const baseFileName = internalPath.endsWith(".js") ? internalPath.slice(0, -3) : internalPath;
179
230
  const possibleExtensions = [".tsx", ".ts", ".jsx", ".js"];
180
231
  for (const ext of possibleExtensions) {
181
- const p = path.join(path.resolve(sourcePath), (baseFileName.startsWith("/") ? baseFileName.slice(1) : baseFileName) + ext);
182
- if (existsSync(p)) {
232
+ const p = secureResolve(baseFileName + ext);
233
+ if (p && existsSync(p)) {
183
234
  sourceFile = p;
184
235
  break;
185
236
  }
@@ -243,7 +294,11 @@ var transpiler = (options) => {
243
294
  if (contentType === "text/css") {
244
295
  return { content: transpiledCode, contentType };
245
296
  }
246
- const finalCode = transpiledCode.replace(/((?:import|export)\s*[\s\S]*?from\s*['"]|import\s*\(['"])([^'"]+)(['"]\)?)/g, (match, prefix, path2, suffix) => {
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
+ }
247
302
  if (/^(https?:|(?:\/\/))/.test(path2))
248
303
  return match;
249
304
  if (!path2.startsWith(".") && !path2.startsWith("/")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abret",
3
- "version": "0.1.4",
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",