bosia 0.4.6 → 0.5.1

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.
@@ -112,6 +112,23 @@ function isValidRoutePath(path: string, origin: string): boolean {
112
112
  }
113
113
  }
114
114
 
115
+ /**
116
+ * Decode an `_invalidated` bitmask string. Char 0 = page, char i+1 = layout
117
+ * depth i, '1' = run, '0' = skip. Missing/extra chars default to run.
118
+ */
119
+ function buildMaskFromBits(
120
+ bits: string,
121
+ layoutCount: number,
122
+ ): { page: boolean; layouts: boolean[] } {
123
+ const page = bits[0] !== "0";
124
+ const layouts: boolean[] = [];
125
+ for (let i = 0; i < layoutCount; i++) {
126
+ const c = bits[i + 1];
127
+ layouts.push(c !== "0");
128
+ }
129
+ return { page, layouts };
130
+ }
131
+
115
132
  /** Extract action name from URL searchParams — `?/login` → "login", no slash key → "default". */
116
133
  function parseActionName(url: URL): string {
117
134
  for (const key of url.searchParams.keys()) {
@@ -146,15 +163,41 @@ async function resolve(event: RequestEvent): Promise<Response> {
146
163
  return Response.json({ error: "Invalid path", status: 400 }, { status: 400 });
147
164
  }
148
165
  const routeUrl = new URL(routePathStr, url.origin);
166
+ let invalidatedBits: string | null = null;
149
167
  for (const [key, val] of url.searchParams.entries()) {
168
+ if (key === "_invalidated") {
169
+ invalidatedBits = val;
170
+ continue;
171
+ }
150
172
  routeUrl.searchParams.append(key, val);
151
173
  }
152
174
  // Rewrite event.url so logging middleware sees the real page path, not /__bosia/data
153
175
  event.url = routeUrl;
154
176
  try {
155
177
  const pageMatch = findMatch(serverRoutes, routeUrl.pathname);
178
+ // Build mask from `?_invalidated=<bits>` where char 0 = page,
179
+ // char i+1 = layout depth i, '1' = run, '0' = skip. Absent → run all.
180
+ // Mask is sized to the total layout count (matching client `layoutIds`),
181
+ // not the count of layout servers, so depths without a server loader
182
+ // still occupy a bit position and stay aligned with the client.
183
+ const mask = invalidatedBits
184
+ ? buildMaskFromBits(
185
+ invalidatedBits,
186
+ pageMatch?.route
187
+ ? ((pageMatch.route as any).layoutModules?.length ?? 0)
188
+ : 0,
189
+ )
190
+ : undefined;
156
191
  const runLoad = async () => {
157
- const data = await loadRouteData(routeUrl, locals, request, cookies);
192
+ const data = await loadRouteData(
193
+ routeUrl,
194
+ locals,
195
+ request,
196
+ cookies,
197
+ null,
198
+ pageMatch,
199
+ mask,
200
+ );
158
201
 
159
202
  let metadata = null;
160
203
  if (pageMatch) {
@@ -176,12 +219,17 @@ async function resolve(event: RequestEvent): Promise<Response> {
176
219
  return { data, metadata, cookiesAccessed: (cookies as CookieJar).accessed };
177
220
  };
178
221
 
179
- // Dedup public routes by URL only. `(private)` scope routes (per-user content)
180
- // skip the cache to prevent cross-user data leaks. See dedup.ts.
222
+ // Dedup public routes by URL + mask. `(private)` scope routes (per-user
223
+ // content) skip the cache to prevent cross-user data leaks. The mask is
224
+ // part of the key so concurrent requests for the same URL with different
225
+ // invalidation patterns don't collapse onto each other. See dedup.ts.
226
+ const dedupK = invalidatedBits
227
+ ? `${dedupKey(routeUrl)}|m=${invalidatedBits}`
228
+ : dedupKey(routeUrl);
181
229
  const result =
182
230
  pageMatch?.route.scope === "private"
183
231
  ? await runLoad()
184
- : await dedup(dedupKey(routeUrl), runLoad);
232
+ : await dedup(dedupK, runLoad);
185
233
 
186
234
  const cookiesWereAccessed = (cookies as CookieJar).accessed || result.cookiesAccessed;
187
235
  const cc = cookiesWereAccessed
@@ -515,12 +563,21 @@ async function resolve(event: RequestEvent): Promise<Response> {
515
563
 
516
564
  // ─── Request Entry ────────────────────────────────────────
517
565
 
566
+ // Set DISABLE_X_FRAME_OPTIONS=true to omit `X-Frame-Options: SAMEORIGIN`.
567
+ // Useful when the app is intentionally embedded as an iframe by a different origin
568
+ // (preview/proxy hubs, design tools, etc.). Other security headers stay on.
569
+ const _xfoDisabled = process.env.DISABLE_X_FRAME_OPTIONS === "true";
570
+
518
571
  const SECURITY_HEADERS: Record<string, string> = {
519
572
  "X-Content-Type-Options": "nosniff",
520
- "X-Frame-Options": "SAMEORIGIN",
573
+ ...(_xfoDisabled ? {} : { "X-Frame-Options": "SAMEORIGIN" }),
521
574
  "Referrer-Policy": "strict-origin-when-cross-origin",
522
575
  };
523
576
 
577
+ if (_xfoDisabled) {
578
+ console.log("🪟 X-Frame-Options disabled (DISABLE_X_FRAME_OPTIONS=true)");
579
+ }
580
+
524
581
  async function handleRequest(request: Request, url: URL): Promise<Response> {
525
582
  // Reject new non-health requests during shutdown
526
583
  if (shuttingDown && url.pathname !== "/_health") {
package/src/lib/client.ts CHANGED
@@ -10,3 +10,4 @@
10
10
 
11
11
  export { enhance } from "../core/client/enhance.ts";
12
12
  export type { SubmitFunction, ActionResult } from "../core/client/enhance.ts";
13
+ export { invalidate, invalidateAll } from "../core/client/navigation.ts";
package/src/lib/index.ts CHANGED
@@ -10,6 +10,7 @@ export type { HttpError, Redirect, RedirectOptions, ActionFailure } from "../cor
10
10
  export type {
11
11
  RequestEvent,
12
12
  LoadEvent,
13
+ LoaderDeps,
13
14
  MetadataEvent,
14
15
  Metadata,
15
16
  Handle,
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "bosia";
2
+ import { inspector } from "bosia/plugins/inspector";
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ // Dev-only: Alt+click any element on the page to open its source in your editor.
7
+ // Change `editor` to "cursor" or "zed" if you don't use VS Code.
8
+ inspector({ editor: "code" }),
9
+ ],
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "bosia";
2
+ import { inspector } from "bosia/plugins/inspector";
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ // Dev-only: Alt+click any element on the page to open its source in your editor.
7
+ // Change `editor` to "cursor" or "zed" if you don't use VS Code.
8
+ inspector({ editor: "code" }),
9
+ ],
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "bosia";
2
+ import { inspector } from "bosia/plugins/inspector";
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ // Dev-only: Alt+click any element on the page to open its source in your editor.
7
+ // Change `editor` to "cursor" or "zed" if you don't use VS Code.
8
+ inspector({ editor: "code" }),
9
+ ],
10
+ });