abret 0.1.3 → 0.1.4

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.
@@ -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, 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,109 @@ 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 activeBundles = new Map;
41
+ function resolveModulePath(moduleName) {
42
+ if (globals[moduleName])
43
+ return moduleName;
44
+ try {
45
+ Bun.resolveSync(moduleName, process.cwd());
46
+ return `${basePrefix}${vendorPath.replace(/^\/|\/$/g, "")}/${moduleName}`;
47
+ } catch {
48
+ if (cdnFallback) {
49
+ return `https://esm.sh/${moduleName}`;
50
+ }
51
+ return `${basePrefix}${vendorPath.replace(/^\/|\/$/g, "")}/${moduleName}`;
52
+ }
53
+ }
23
54
  async function bundleVendorModule(moduleName) {
24
55
  const cacheKey = moduleName.replace(/\//g, "__");
25
56
  const cachedFile = path.join(cacheDir, `${cacheKey}.js`);
26
57
  if (existsSync(cachedFile))
27
58
  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
- });
59
+ if (activeBundles.has(cacheKey)) {
60
+ return activeBundles.get(cacheKey);
61
+ }
62
+ const promise = (async () => {
63
+ if (existsSync(cachedFile))
64
+ return;
65
+ try {
66
+ const entryPoint = Bun.resolveSync(moduleName, process.cwd());
67
+ const globalsPlugin = {
68
+ name: "abret-globals",
69
+ setup(build) {
70
+ for (const moduleName2 of Object.keys(globals)) {
71
+ build.onResolve({ filter: new RegExp(`^${moduleName2}$`) }, () => ({
72
+ path: moduleName2,
73
+ namespace: "abret-globals"
74
+ }));
44
75
  }
76
+ build.onLoad({ filter: /.*/, namespace: "abret-globals" }, (args) => {
77
+ const gName = globals[args.path];
78
+ return {
79
+ contents: `export default globalThis.${gName}; export const ${gName} = globalThis.${gName};`,
80
+ loader: "js"
81
+ };
82
+ });
45
83
  }
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}`;
84
+ };
85
+ const result = await Bun.build({
86
+ entrypoints: [entryPoint],
87
+ target: "browser",
88
+ format: "esm",
89
+ minify: true,
90
+ define: defaultDefine,
91
+ plugins: [
92
+ globalsPlugin,
93
+ {
94
+ name: "abret-external-vendor",
95
+ setup(build) {
96
+ build.onResolve({ filter: /^[^./]/ }, (args) => {
97
+ if (args.path === moduleName || globals[args.path])
98
+ return null;
99
+ return { path: args.path, external: true };
100
+ });
101
+ }
102
+ },
103
+ ...plugins
104
+ ]
105
+ });
106
+ if (!result.success || result.outputs.length === 0) {
107
+ console.error(`[Abret] Failed to bundle vendor module: ${moduleName}`, result.logs);
108
+ return;
61
109
  }
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
- }
110
+ const output = result.outputs[0];
111
+ if (!output)
112
+ return;
113
+ 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
+ });
122
+ await Bun.write(cachedFile, content);
123
+ console.log(`[Abret] Pre-bundled: ${moduleName}`);
124
+ } catch (err) {
125
+ console.error(`[Abret] Error bundling ${moduleName}:`, err);
126
+ } finally {
127
+ activeBundles.delete(cacheKey);
128
+ }
129
+ })();
130
+ activeBundles.set(cacheKey, promise);
131
+ return promise;
69
132
  }
70
133
  if (prewarm.length > 0) {
71
134
  for (const moduleName of prewarm) {
@@ -102,51 +165,129 @@ var transpiler = (options) => {
102
165
  return next();
103
166
  }
104
167
  const internalPath = pathname.slice(basePrefix.length);
105
- const baseFileName = internalPath.endsWith(".js") ? internalPath.slice(0, -3) : internalPath;
106
- const possibleExtensions = [".tsx", ".ts", ".jsx", ".js"];
168
+ const extname = path.extname(internalPath);
107
169
  let sourceFile = "";
108
- for (const ext of possibleExtensions) {
109
- const p = path.join(path.resolve(sourcePath), (baseFileName.startsWith("/") ? baseFileName.slice(1) : baseFileName) + ext);
170
+ let contentType = "application/javascript";
171
+ if (extname === ".css") {
172
+ const p = path.join(path.resolve(sourcePath), internalPath);
110
173
  if (existsSync(p)) {
111
174
  sourceFile = p;
112
- break;
175
+ contentType = "text/css";
176
+ }
177
+ } else {
178
+ const baseFileName = internalPath.endsWith(".js") ? internalPath.slice(0, -3) : internalPath;
179
+ const possibleExtensions = [".tsx", ".ts", ".jsx", ".js"];
180
+ for (const ext of possibleExtensions) {
181
+ const p = path.join(path.resolve(sourcePath), (baseFileName.startsWith("/") ? baseFileName.slice(1) : baseFileName) + ext);
182
+ if (existsSync(p)) {
183
+ sourceFile = p;
184
+ break;
185
+ }
113
186
  }
114
187
  }
115
188
  if (sourceFile) {
116
- try {
117
- const buildResult = await Bun.build({
118
- entrypoints: [sourceFile],
119
- target: "browser",
120
- format: "esm",
121
- external: ["*"]
189
+ const sourceStat = statSync(sourceFile);
190
+ const fastEtag = `W/"${sourceStat.size}-${sourceStat.mtimeMs}-${minify}"`;
191
+ if (req.headers.get("if-none-match") === fastEtag) {
192
+ return new Response(null, { status: 304 });
193
+ }
194
+ const lockKey = `local:${sourceFile}:${fastEtag}`;
195
+ if (activeBundles.has(lockKey)) {
196
+ const result = await activeBundles.get(lockKey);
197
+ return new Response(result.content, {
198
+ headers: {
199
+ "Content-Type": result.contentType,
200
+ ETag: fastEtag,
201
+ "Cache-Control": localMaxAge > 0 ? `public, max-age=${localMaxAge}` : "no-cache"
202
+ }
122
203
  });
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}`;
204
+ }
205
+ const buildPromise = (async () => {
206
+ try {
207
+ const buildResult = await Bun.build({
208
+ entrypoints: [sourceFile],
209
+ target: "browser",
210
+ format: "esm",
211
+ minify,
212
+ define: defaultDefine,
213
+ external: ["*"],
214
+ plugins: [
215
+ {
216
+ name: "abret-globals-local",
217
+ setup(build) {
218
+ for (const moduleName of Object.keys(globals)) {
219
+ build.onResolve({ filter: new RegExp(`^${moduleName}$`) }, () => ({
220
+ path: moduleName,
221
+ namespace: "abret-globals"
222
+ }));
223
+ }
224
+ build.onLoad({ filter: /.*/, namespace: "abret-globals" }, (args) => {
225
+ const gName = globals[args.path];
226
+ return {
227
+ contents: `export default globalThis.${gName};`,
228
+ loader: "js"
229
+ };
230
+ });
231
+ }
232
+ },
233
+ ...plugins
234
+ ]
235
+ });
236
+ if (!buildResult.success || buildResult.outputs.length === 0) {
237
+ throw new Error(`Build failed: ${buildResult.logs.map((l) => l.message).join(", ")}`);
138
238
  }
139
- if (path2.startsWith(".") && !path2.split("/").pop()?.includes(".")) {
140
- return `${prefix}${path2}.js${suffix}`;
239
+ const output = buildResult.outputs[0];
240
+ if (!output)
241
+ throw new Error("No output generated");
242
+ const transpiledCode = await output.text();
243
+ if (contentType === "text/css") {
244
+ return { content: transpiledCode, contentType };
245
+ }
246
+ const finalCode = transpiledCode.replace(/((?:import|export)\s*[\s\S]*?from\s*['"]|import\s*\(['"])([^'"]+)(['"]\)?)/g, (match, prefix, path2, suffix) => {
247
+ if (/^(https?:|(?:\/\/))/.test(path2))
248
+ return match;
249
+ if (!path2.startsWith(".") && !path2.startsWith("/")) {
250
+ return `${prefix}${resolveModulePath(path2)}${suffix}`;
251
+ }
252
+ if (path2.startsWith(".") && !path2.split("/").pop()?.includes(".")) {
253
+ return `${prefix}${path2}.js${suffix}`;
254
+ }
255
+ return match;
256
+ });
257
+ return { content: finalCode, contentType };
258
+ } finally {
259
+ activeBundles.delete(lockKey);
260
+ }
261
+ })();
262
+ activeBundles.set(lockKey, buildPromise);
263
+ try {
264
+ const finalResult = await buildPromise;
265
+ return new Response(finalResult.content, {
266
+ headers: {
267
+ "Content-Type": finalResult.contentType,
268
+ ETag: fastEtag,
269
+ "Cache-Control": localMaxAge > 0 ? `public, max-age=${localMaxAge}` : "no-cache"
141
270
  }
142
- return match;
143
- });
144
- return new Response(finalCode, {
145
- headers: { "Content-Type": "application/javascript" }
146
271
  });
147
272
  } catch (err) {
148
- console.error(`[Abret] Transpilation error for ${sourceFile}:`, err);
149
- return next();
273
+ const errorMessage = err.message || "Unknown transpilation error";
274
+ console.error(`[Abret] ${errorMessage} for ${sourceFile}`);
275
+ return new Response(`console.error("[Abret] Build Error in ${sourceFile}:", ${JSON.stringify(errorMessage)});
276
+ if (typeof document !== 'undefined') {
277
+ const div = document.createElement('div');
278
+ div.style.position = 'fixed';
279
+ div.style.top = '0';
280
+ div.style.left = '0';
281
+ div.style.width = '100%';
282
+ div.style.padding = '1rem';
283
+ div.style.background = '#fee2e2';
284
+ div.style.color = '#991b1b';
285
+ div.style.borderBottom = '1px solid #ef4444';
286
+ div.style.zIndex = '999999';
287
+ div.style.fontFamily = 'monospace';
288
+ div.innerText = "[Abret] Build Error in ${sourceFile.split("/").pop()}: " + ${JSON.stringify(errorMessage)};
289
+ document.body.appendChild(div);
290
+ }`, { headers: { "Content-Type": "application/javascript" } });
150
291
  }
151
292
  }
152
293
  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.4",
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",