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 +0 -45
- package/dist/html.js +100 -83
- package/dist/middleware/transpiler/index.d.ts +12 -5
- package/dist/middleware/transpiler/index.js +272 -76
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
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
|
|
78
|
-
}
|
|
79
|
-
const rendered =
|
|
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(
|
|
86
|
+
return new HTMLResponse(rendered.then((s) => raw(injectMetadata(s.toString(), headCollection))));
|
|
82
87
|
}
|
|
83
|
-
return new HTMLResponse(raw(
|
|
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
|
-
}
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
410
|
-
if (charsetTag)
|
|
411
|
-
headContent.push(charsetTag);
|
|
446
|
+
finalHead.push(metaMap.get("charset"));
|
|
412
447
|
metaMap.delete("charset");
|
|
413
448
|
}
|
|
414
|
-
if (
|
|
415
|
-
|
|
449
|
+
if (titleString) {
|
|
450
|
+
finalHead.push(titleString);
|
|
451
|
+
}
|
|
416
452
|
if (metaMap.has("name:viewport")) {
|
|
417
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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 (
|
|
446
|
-
return
|
|
462
|
+
if (html2.toLowerCase().includes("<html")) {
|
|
463
|
+
return html2.replace(/(<html[^>]*>)/i, `$1<head>${headString}</head>`);
|
|
447
464
|
}
|
|
448
|
-
return
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
const possibleExtensions = [".tsx", ".ts", ".jsx", ".js"];
|
|
211
|
+
const extname = path.extname(internalPath);
|
|
107
212
|
let sourceFile = "";
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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();
|