bosia 0.9.7 → 0.9.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
package/src/cli/sync.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { scanRoutes } from "../core/scanner.ts";
1
+ import { scanRoutes, RouteConflictError } from "../core/scanner.ts";
2
2
  import { generateRoutesFile } from "../core/routeFile.ts";
3
3
  import { generateRouteTypes, ensureRootDirs } from "../core/routeTypes.ts";
4
4
  import { loadEnv, classifyEnvVars } from "../core/env.ts";
@@ -8,7 +8,14 @@ import { findBrandPlaceholders, BRAND_SENTINEL } from "../core/brandGuard.ts";
8
8
  export async function runSync() {
9
9
  const envMode = process.env.NODE_ENV === "production" ? "production" : "development";
10
10
  const classifiedEnv = classifyEnvVars(loadEnv(envMode));
11
- const manifest = scanRoutes();
11
+ let manifest;
12
+ try {
13
+ manifest = scanRoutes();
14
+ } catch (err) {
15
+ if (!(err instanceof RouteConflictError)) throw err;
16
+ console.error(`❌ ${err.message}`);
17
+ process.exit(1);
18
+ }
12
19
  generateRoutesFile(manifest);
13
20
  generateRouteTypes(manifest);
14
21
  ensureRootDirs();
package/src/core/build.ts CHANGED
@@ -2,7 +2,7 @@ import { writeFileSync, readFileSync, rmSync, mkdirSync, existsSync } from "fs";
2
2
  import { basename, join, relative } from "path";
3
3
  import type { RouteManifest } from "./types.ts";
4
4
 
5
- import { scanRoutes } from "./scanner.ts";
5
+ import { scanRoutes, RouteConflictError } from "./scanner.ts";
6
6
  import { generateRoutesFile } from "./routeFile.ts";
7
7
  import { generateRouteTypes, ensureRootDirs } from "./routeTypes.ts";
8
8
  import { makeBosiaPlugin } from "./plugin.ts";
@@ -85,7 +85,14 @@ for (const p of [
85
85
  }
86
86
 
87
87
  // 1. Scan routes (or reuse the cached manifest — see 0b-pre)
88
- const manifest = cachedManifest ?? scanRoutes();
88
+ let manifest: RouteManifest;
89
+ try {
90
+ manifest = cachedManifest ?? scanRoutes();
91
+ } catch (err) {
92
+ if (!(err instanceof RouteConflictError)) throw err;
93
+ console.error(`❌ ${err.message}`);
94
+ process.exit(1);
95
+ }
89
96
  buildCtx.manifest = manifest;
90
97
  console.log(
91
98
  `📂 Found ${manifest.pages.length} page route(s)${cachedManifest ? " (cached scan)" : ""}:`,
@@ -181,11 +181,45 @@ export function scanRoutes(): RouteManifest {
181
181
  }
182
182
  }
183
183
 
184
+ preventConflicts(pages, (p) => p.page);
185
+ preventConflicts(apis, (a) => a.server);
186
+
184
187
  const errorPage = existsSync(join(ROUTES_DIR, "+error.svelte")) ? "+error.svelte" : null;
185
188
 
186
189
  return { pages, apis, errorPage };
187
190
  }
188
191
 
192
+ /**
193
+ * Fail when two route files serve the same URL — e.g. `+page.svelte` next to
194
+ * `(public)/+page.svelte`, or `blog/[id]` next to `(app)/blog/[slug]`. Route
195
+ * groups vanish from the URL and param names don't affect matching, so one of
196
+ * the two could never be reached. Same rule as SvelteKit's prevent_conflicts
197
+ * and Next.js's "two parallel pages that resolve to the same path".
198
+ */
199
+ function preventConflicts<T extends { pattern: string }>(routes: T[], fileOf: (r: T) => string) {
200
+ const seen = new Map<string, T>();
201
+ for (const r of routes) {
202
+ const key = r.pattern.replace(/\[\.\.\.\w+\]/g, "[...]").replace(/\[\w+\]/g, "[]");
203
+ const first = seen.get(key);
204
+ if (!first) {
205
+ seen.set(key, r);
206
+ continue;
207
+ }
208
+ throw new RouteConflictError(
209
+ `The "${first.pattern}" and "${r.pattern}" routes conflict with each other:\n` +
210
+ ` src/routes/${fileOf(first)}\n` +
211
+ ` src/routes/${fileOf(r)}\n` +
212
+ ` Route groups like (public) are not part of the URL, and [id] matches the same URLs as [slug].\n` +
213
+ ` Delete or move one of them.`,
214
+ );
215
+ }
216
+ }
217
+
218
+ /** Two route files resolve to the same URL. Callers print `message` and exit. */
219
+ export class RouteConflictError extends Error {
220
+ override name = "RouteConflictError";
221
+ }
222
+
189
223
  function toUrlPath(segments: string[]): string {
190
224
  if (segments.length === 0) return "/";
191
225
  return "/" + segments.join("/");