bun-plugin-stx 0.2.12 → 0.2.13

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/serve.d.ts CHANGED
@@ -13,4 +13,35 @@ export declare interface ServeOptions {
13
13
  quiet?: boolean
14
14
  routes?: Record<string, (req: Request) => Response | Promise<Response>>
15
15
  onRequest?: (req: Request) => Response | Promise<Response> | null | undefined
16
+ middleware?: Record<string, MiddlewareHandler>
17
+ globalMiddleware?: string[]
18
+ middlewareGroups?: Record<string, string[]>
19
+ auth?: false | {
20
+ cookieName?: string
21
+ redirectTo?: string
22
+ home?: string
23
+ protectedPaths?: string[]
24
+ }
16
25
  }
26
+ export declare interface MiddlewareContext {
27
+ path: string
28
+ url: URL
29
+ params: Record<string, string>
30
+ cookies: Record<string, string>
31
+ redirect: (to: string, status?: number) => Response
32
+ }
33
+ /**
34
+ * Middleware handler — a Laravel-style gate that either passes through
35
+ * (returns `void`/`null`/`undefined`) or terminates the pipeline by
36
+ * returning a `Response`.
37
+ *
38
+ * The third argument is the colon-separated arg list, so a page that
39
+ * declares `middleware: ['auth:admin']` invokes the `auth` handler
40
+ * with `args = ['admin']` — the same shape as Laravel's
41
+ * `handle($request, $next, ...$args)`.
42
+ */
43
+ export type MiddlewareHandler = (
44
+ req: Request,
45
+ ctx: MiddlewareContext,
46
+ ...args: string[]
47
+ ) => Response | null | undefined | void | Promise<Response | null | undefined | void>;
package/dist/serve.js CHANGED
@@ -5124,6 +5124,151 @@ After installing: bun add bun-plugin-stx`);
5124
5124
  const routes = new Map;
5125
5125
  let sourceFiles = null;
5126
5126
  let assetsInitialized = false;
5127
+ const pageMiddlewareByPath = new Map;
5128
+ const pageMiddlewarePatterns = [];
5129
+ function compileRoutePattern(urlPath) {
5130
+ const names = [];
5131
+ const re = urlPath.split("/").map((seg) => {
5132
+ if (!seg)
5133
+ return "";
5134
+ const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
5135
+ if (catchAll) {
5136
+ names.push(catchAll[1]);
5137
+ return "(.+)";
5138
+ }
5139
+ const param = seg.match(/^\[(.+)\]$/);
5140
+ if (param) {
5141
+ names.push(param[1]);
5142
+ return "([^/]+)";
5143
+ }
5144
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5145
+ }).join("/");
5146
+ return { re: new RegExp(`^${re}/?$`), names };
5147
+ }
5148
+ function urlPathFromFile(file) {
5149
+ let rel = file;
5150
+ for (const pattern of patterns) {
5151
+ const base = pattern.replace(/\/$/, "");
5152
+ if (rel.startsWith(`${base}/`)) {
5153
+ rel = rel.slice(base.length + 1);
5154
+ break;
5155
+ }
5156
+ }
5157
+ rel = rel.replace(/\.(stx|md|html)$/, "");
5158
+ if (rel === "index" || rel.endsWith("/index"))
5159
+ rel = rel.replace(/\/?index$/, "");
5160
+ return rel.startsWith("/") ? rel : `/${rel}`;
5161
+ }
5162
+ async function detectPageMiddleware(file) {
5163
+ try {
5164
+ const fs = await import("fs/promises");
5165
+ const src = await fs.readFile(file, "utf-8");
5166
+ const meta = src.match(/definePageMeta\s*\(\s*\{[\s\S]*?\}\s*\)/);
5167
+ if (!meta)
5168
+ return;
5169
+ const mw = meta[0].match(/middleware\s*:\s*(\[[^\]]*\]|['"][^'"]+['"])/);
5170
+ if (!mw)
5171
+ return;
5172
+ const names = mw[1].startsWith("[") ? Array.from(mw[1].matchAll(/['"]([^'"]+)['"]/g)).map((m) => m[1]) : [mw[1].replace(/['"]/g, "")];
5173
+ if (names.length === 0)
5174
+ return;
5175
+ const urlPath = urlPathFromFile(file);
5176
+ if (urlPath.includes("[")) {
5177
+ const { re, names: paramNames } = compileRoutePattern(urlPath);
5178
+ pageMiddlewarePatterns.push({ re, names: paramNames, middleware: names });
5179
+ } else {
5180
+ pageMiddlewareByPath.set(urlPath, names);
5181
+ }
5182
+ } catch {}
5183
+ }
5184
+ function resolveRouteMiddleware(path) {
5185
+ const exact = pageMiddlewareByPath.get(path);
5186
+ if (exact)
5187
+ return { names: exact, params: {} };
5188
+ const trimmed = path.replace(/\/$/, "");
5189
+ if (trimmed !== path) {
5190
+ const exactTrim = pageMiddlewareByPath.get(trimmed);
5191
+ if (exactTrim)
5192
+ return { names: exactTrim, params: {} };
5193
+ }
5194
+ for (const entry of pageMiddlewarePatterns) {
5195
+ const m = entry.re.exec(path);
5196
+ if (m) {
5197
+ const params = {};
5198
+ entry.names.forEach((n, i) => {
5199
+ params[n] = decodeURIComponent(m[i + 1] ?? "");
5200
+ });
5201
+ return { names: entry.middleware, params };
5202
+ }
5203
+ }
5204
+ return { names: [], params: {} };
5205
+ }
5206
+ function parseCookies(req) {
5207
+ const out = {};
5208
+ const header = req.headers.get("cookie") || "";
5209
+ if (!header)
5210
+ return out;
5211
+ for (const part of header.split(/;\s*/)) {
5212
+ const eq = part.indexOf("=");
5213
+ if (eq === -1)
5214
+ continue;
5215
+ const k = part.slice(0, eq);
5216
+ if (!k)
5217
+ continue;
5218
+ try {
5219
+ out[k] = decodeURIComponent(part.slice(eq + 1));
5220
+ } catch {
5221
+ out[k] = part.slice(eq + 1);
5222
+ }
5223
+ }
5224
+ return out;
5225
+ }
5226
+ function redirectWithNext(target, originalPath, search = "") {
5227
+ const sep = target.includes("?") ? "&" : "?";
5228
+ const next = encodeURIComponent(originalPath + search);
5229
+ return Response.redirect(`${target}${sep}next=${next}`, 302);
5230
+ }
5231
+ const authConfig = options.auth === false ? null : options.auth ?? {};
5232
+ const authCookieName = authConfig?.cookieName ?? "auth-token";
5233
+ const authRedirectTo = authConfig?.redirectTo ?? "/login";
5234
+ const authHome = authConfig?.home ?? "/";
5235
+ const extraProtectedPrefixes = authConfig?.protectedPaths ?? [];
5236
+ const builtInMiddleware = authConfig === null ? {} : {
5237
+ auth: (_req, ctx) => {
5238
+ const tok = ctx.cookies[authCookieName];
5239
+ if (!tok)
5240
+ return ctx.redirect(authRedirectTo);
5241
+ return null;
5242
+ },
5243
+ guest: (_req, ctx) => {
5244
+ const tok = ctx.cookies[authCookieName];
5245
+ if (tok)
5246
+ return Response.redirect(authHome, 302);
5247
+ return null;
5248
+ }
5249
+ };
5250
+ const middlewareRegistry = {
5251
+ ...builtInMiddleware,
5252
+ ...options.middleware ?? {}
5253
+ };
5254
+ function expandMiddlewareNames(names) {
5255
+ const out = [];
5256
+ const groups = options.middlewareGroups ?? {};
5257
+ const seen = new Set;
5258
+ const visit = (name) => {
5259
+ if (seen.has(name))
5260
+ return;
5261
+ seen.add(name);
5262
+ const group = groups[name];
5263
+ if (group)
5264
+ group.forEach(visit);
5265
+ else
5266
+ out.push(name);
5267
+ };
5268
+ names.forEach(visit);
5269
+ return out;
5270
+ }
5271
+ const globalMiddlewareNames = options.globalMiddleware ?? [];
5127
5272
  async function discoverFiles() {
5128
5273
  if (sourceFiles !== null)
5129
5274
  return sourceFiles;
@@ -5159,6 +5304,9 @@ After installing: bun add bun-plugin-stx`);
5159
5304
  }
5160
5305
  }
5161
5306
  sourceFiles = files;
5307
+ pageMiddlewareByPath.clear();
5308
+ pageMiddlewarePatterns.length = 0;
5309
+ await Promise.all(files.map((f) => detectPageMiddleware(f)));
5162
5310
  try {
5163
5311
  const { Router } = await import("./chunk-762q71t6.js");
5164
5312
  const pagesDir = patterns[0]?.replace(/\/$/, "") || "pages";
@@ -5541,6 +5689,40 @@ ${output}`;
5541
5689
  if (customResponse)
5542
5690
  return customResponse;
5543
5691
  }
5692
+ const route = resolveRouteMiddleware(path);
5693
+ const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
5694
+ const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
5695
+ if (requested.length > 0) {
5696
+ const expanded = expandMiddlewareNames(requested);
5697
+ const cookies = parseCookies(req);
5698
+ const ctx = {
5699
+ path,
5700
+ url,
5701
+ params: route.params,
5702
+ cookies,
5703
+ redirect: (to, status = 302) => {
5704
+ const sep = to.includes("?") ? "&" : "?";
5705
+ const next = encodeURIComponent(path + (url.search || ""));
5706
+ return new Response(null, {
5707
+ status,
5708
+ headers: { Location: `${to}${sep}next=${next}` }
5709
+ });
5710
+ }
5711
+ };
5712
+ for (const entry of expanded) {
5713
+ const colon = entry.indexOf(":");
5714
+ const name = colon === -1 ? entry : entry.slice(0, colon);
5715
+ const args = colon === -1 ? [] : entry.slice(colon + 1).split(",");
5716
+ const handler = middlewareRegistry[name];
5717
+ if (!handler) {
5718
+ console.warn(`[stx serve] unknown middleware "${name}" on ${path}`);
5719
+ continue;
5720
+ }
5721
+ const result = await handler(req, ctx, ...args);
5722
+ if (result instanceof Response)
5723
+ return result;
5724
+ }
5725
+ }
5544
5726
  if (options.routes) {
5545
5727
  const routeHandler = options.routes[path];
5546
5728
  if (routeHandler) {
@@ -5864,11 +6046,11 @@ ${pageSetupScripts.join(`
5864
6046
  break;
5865
6047
  }
5866
6048
  }
5867
- let route = normalizedPath.replace(/\.(stx|md|html)$/, "");
5868
- if (route.startsWith("pages/")) {
5869
- route = route.slice(6);
6049
+ let route2 = normalizedPath.replace(/\.(stx|md|html)$/, "");
6050
+ if (route2.startsWith("pages/")) {
6051
+ route2 = route2.slice(6);
5870
6052
  }
5871
- availableRoutes.push(`<li><a href="/${route}">/${route}</a></li>`);
6053
+ availableRoutes.push(`<li><a href="/${route2}">/${route2}</a></li>`);
5872
6054
  }
5873
6055
  const routesList = availableRoutes.join(`
5874
6056
  `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.12",
4
+ "version": "0.2.13",
5
5
  "description": "A Bun plugin that allows for using Laravel Blade-like syntax.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -72,6 +72,6 @@
72
72
  "typecheck": "bun tsc --noEmit"
73
73
  },
74
74
  "dependencies": {
75
- "@stacksjs/stx": "0.2.12"
75
+ "@stacksjs/stx": "0.2.13"
76
76
  }
77
77
  }