bosia 0.9.6 → 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 +1 -1
- package/src/cli/create.ts +2 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/sync.ts +9 -2
- package/src/core/build.ts +14 -10
- package/src/core/paths.ts +18 -5
- package/src/core/plugins/inspector/bun-plugin.ts +2 -1
- package/src/core/plugins/inspector/sourcemap.ts +11 -9
- package/src/core/port.ts +30 -8
- package/src/core/routeFile.ts +2 -1
- package/src/core/routeTypes.ts +2 -1
- package/src/core/safePath.ts +2 -2
- package/src/core/scanner.ts +46 -8
package/package.json
CHANGED
package/src/cli/create.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { spawn } from "bun";
|
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
import { installFeature, initFeatRegistry, resolveLocalRegistry } from "./feat.ts";
|
|
7
7
|
import { initAddRegistry } from "./add.ts";
|
|
8
|
+
import { toPosix } from "../core/paths.ts";
|
|
8
9
|
|
|
9
10
|
// ─── bun x bosia@latest create <name> [--template <name>] ─
|
|
10
11
|
|
|
@@ -283,7 +284,7 @@ function copyDir(src: string, dest: string, projectName: string, isLocal: boolea
|
|
|
283
284
|
|
|
284
285
|
if (entry.name === "package.json" && isLocal) {
|
|
285
286
|
const bosiaPath = resolve(import.meta.dir, "../../");
|
|
286
|
-
const relPath = relative(dest, bosiaPath);
|
|
287
|
+
const relPath = toPosix(relative(dest, bosiaPath));
|
|
287
288
|
content = content.replaceAll('"^{{BOSIA_VERSION}}"', `"file:${relPath}"`);
|
|
288
289
|
} else {
|
|
289
290
|
content = content.replaceAll("{{BOSIA_VERSION}}", BOSIA_VERSION);
|
package/src/cli/registry.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface InstallOptions {
|
|
|
21
21
|
// ─── Local registry resolution ────────────────────────────
|
|
22
22
|
|
|
23
23
|
export function resolveLocalRegistry(): string {
|
|
24
|
-
let dir =
|
|
24
|
+
let dir = import.meta.dir;
|
|
25
25
|
for (let i = 0; i < 10; i++) {
|
|
26
26
|
const candidate = join(dir, "registry");
|
|
27
27
|
if (existsSync(join(candidate, "index.json"))) return candidate;
|
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
|
-
|
|
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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { writeFileSync, readFileSync, rmSync, mkdirSync, existsSync } from "fs";
|
|
2
|
-
import { join, relative } from "path";
|
|
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";
|
|
@@ -11,7 +11,7 @@ import { finalizeComponentCss } from "./componentCss.ts";
|
|
|
11
11
|
import { prerenderStaticRoutes, generateStaticSite } from "./prerender.ts";
|
|
12
12
|
import { loadEnv, classifyEnvVars } from "./env.ts";
|
|
13
13
|
import { generateEnvModules } from "./envCodegen.ts";
|
|
14
|
-
import { BOSIA_NODE_PATH, OUT_DIR, resolveBosiaBin } from "./paths.ts";
|
|
14
|
+
import { BOSIA_NODE_PATH, OUT_DIR, resolveBosiaBin, toPosix } from "./paths.ts";
|
|
15
15
|
import { currentBase } from "./appBase.ts";
|
|
16
16
|
import { finalizeTailwindCss, TW_TEMP_BASENAME } from "./twHash.ts";
|
|
17
17
|
import { loadPlugins } from "./config.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
|
-
|
|
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)" : ""}:`,
|
|
@@ -251,7 +258,7 @@ const cssFiles: string[] = [];
|
|
|
251
258
|
// `output.kind === "entry-point"` instead so we pin the actual entry.
|
|
252
259
|
let clientEntry: string | null = null;
|
|
253
260
|
for (const output of clientResult.outputs) {
|
|
254
|
-
const rel = relative(`${OUT_DIR}/client`, output.path);
|
|
261
|
+
const rel = toPosix(relative(`${OUT_DIR}/client`, output.path)); // URL path, not fs path
|
|
255
262
|
if (output.path.endsWith(".js")) jsFiles.push(rel);
|
|
256
263
|
if (output.path.endsWith(".css")) cssFiles.push(rel);
|
|
257
264
|
if ((output as { kind?: string }).kind === "entry-point" && output.path.endsWith(".js")) {
|
|
@@ -271,11 +278,8 @@ if (componentCssFile) {
|
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
// Entry is always "index.js" due to naming: { entry: "index.[ext]" }
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
.find((o) => o.path.endsWith("index.js"))
|
|
277
|
-
?.path.split("/")
|
|
278
|
-
.pop() ?? "index.js";
|
|
281
|
+
const serverEntryOutput = serverResult.outputs.find((o) => o.path.endsWith("index.js"));
|
|
282
|
+
const serverEntry = serverEntryOutput ? basename(serverEntryOutput.path) : "index.js";
|
|
279
283
|
|
|
280
284
|
// 8. Write dist/manifest.json
|
|
281
285
|
mkdirSync(OUT_DIR, { recursive: true });
|
package/src/core/paths.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, dirname } from "path";
|
|
1
|
+
import { join, dirname, delimiter } from "path";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
|
|
4
4
|
// This file lives at src/core/paths.ts → package root is ../..
|
|
@@ -26,18 +26,31 @@ const ANCESTOR_NM = collectAncestorNodeModules(dirname(BOSIA_PKG_DIR));
|
|
|
26
26
|
const ALL_NM = [NESTED_NM, ...ANCESTOR_NM];
|
|
27
27
|
|
|
28
28
|
/** NODE_PATH value covering nested and every ancestor node_modules */
|
|
29
|
-
export const BOSIA_NODE_PATH = ALL_NM.join(":"
|
|
29
|
+
export const BOSIA_NODE_PATH = ALL_NM.join(delimiter); // ";" on Windows, ":" elsewhere
|
|
30
30
|
|
|
31
31
|
// On-disk output directory. URL namespace (/dist/client/...) stays stable;
|
|
32
32
|
// only the on-disk location moves so dev (.bosia/dev) and a parallel
|
|
33
33
|
// `bun run build` (./dist) don't clobber each other.
|
|
34
34
|
export const OUT_DIR = process.env.BOSIA_OUT_DIR ?? "./dist";
|
|
35
35
|
|
|
36
|
+
/** Normalize a filesystem path to forward slashes (for URLs, manifests, imports). */
|
|
37
|
+
export function toPosix(p: string): string {
|
|
38
|
+
return p.replace(/\\/g, "/");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `.bin` file names to try for `name`. Windows shims carry an extension. */
|
|
42
|
+
export function binCandidates(name: string, platform: string = process.platform): string[] {
|
|
43
|
+
return platform === "win32" ? [`${name}.exe`, `${name}.cmd`, `${name}.bunx`, name] : [name];
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
/** Find a binary from bosia's dependencies (handles hoisting) */
|
|
37
47
|
export function resolveBosiaBin(name: string): string {
|
|
48
|
+
const candidates = binCandidates(name);
|
|
38
49
|
for (const nm of ALL_NM) {
|
|
39
|
-
const
|
|
40
|
-
|
|
50
|
+
for (const file of candidates) {
|
|
51
|
+
const bin = join(nm, ".bin", file);
|
|
52
|
+
if (existsSync(bin)) return bin;
|
|
53
|
+
}
|
|
41
54
|
}
|
|
42
|
-
return join(NESTED_NM, ".bin",
|
|
55
|
+
return join(NESTED_NM, ".bin", candidates[0]); // fallback — will produce a clear ENOENT
|
|
43
56
|
}
|
|
@@ -4,6 +4,7 @@ import { relative } from "node:path";
|
|
|
4
4
|
import type { BunPlugin } from "bun";
|
|
5
5
|
import { svelteMapCache } from "../../svelteCompiler.ts";
|
|
6
6
|
import { lineColFromOffset } from "../../sourceLoc.ts";
|
|
7
|
+
import { toPosix } from "../../paths.ts";
|
|
7
8
|
import { collectComponentCss } from "../../componentCss.ts";
|
|
8
9
|
|
|
9
10
|
type AnyNode = {
|
|
@@ -129,7 +130,7 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
129
130
|
setup(build) {
|
|
130
131
|
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
|
|
131
132
|
const source = await Bun.file(args.path).text();
|
|
132
|
-
const rel = relative(cwd, args.path);
|
|
133
|
+
const rel = toPosix(relative(cwd, args.path));
|
|
133
134
|
const transformed = injectLocs(source, rel);
|
|
134
135
|
|
|
135
136
|
const result = compile(transformed, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TraceMap, originalPositionFor, GREATEST_LOWER_BOUND } from "@jridgewell/trace-mapping";
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
-
import { dirname, resolve as pathResolve } from "node:path";
|
|
4
|
-
import { OUT_DIR } from "../../paths.ts";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve as pathResolve } from "node:path";
|
|
4
|
+
import { OUT_DIR, toPosix } from "../../paths.ts";
|
|
5
5
|
|
|
6
6
|
const cache = new Map<string, TraceMap | null>();
|
|
7
7
|
|
|
@@ -64,7 +64,7 @@ function mapPathFor(file: string): string | null {
|
|
|
64
64
|
? OUT_DIR + pathname.slice("/dist".length)
|
|
65
65
|
: "." + pathname;
|
|
66
66
|
fsPath = pathResolve(process.cwd(), relFromCwd);
|
|
67
|
-
} else if (file
|
|
67
|
+
} else if (isAbsolute(file)) {
|
|
68
68
|
fsPath = file;
|
|
69
69
|
} else {
|
|
70
70
|
fsPath = pathResolve(process.cwd(), file);
|
|
@@ -109,16 +109,18 @@ export function resolveFrame(
|
|
|
109
109
|
}
|
|
110
110
|
if (refined.source && refined.line != null) {
|
|
111
111
|
const refinedAbs = pathResolve(dirname(abs), refined.source);
|
|
112
|
-
|
|
113
|
-
? refinedAbs.slice(process.cwd().length + 1)
|
|
114
|
-
: refinedAbs;
|
|
115
|
-
return { file: rel, line: refined.line, col: refined.column ?? 1 };
|
|
112
|
+
return { file: relToCwd(refinedAbs), line: refined.line, col: refined.column ?? 1 };
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
return { file: relToCwd(abs), line: pos.line, col: pos.column ?? 1 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// cwd-relative "/"-separated path when `abs` is inside cwd, else `abs` unchanged.
|
|
121
|
+
function relToCwd(abs: string): string {
|
|
122
|
+
const rel = relative(process.cwd(), abs);
|
|
123
|
+
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? toPosix(rel) : abs;
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
// Rewrite frames in stack strings: "(url:L:C)", "at url:L:C", "@url:L:C".
|
package/src/core/port.ts
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
|
-
/** PIDs listening on `port`. Fails open to [] — lsof may be absent. */
|
|
1
|
+
/** PIDs listening on `port`. Fails open to [] — lsof / netstat may be absent. */
|
|
2
2
|
export async function pidsOnPort(port: number): Promise<number[]> {
|
|
3
|
+
const isWindows = process.platform === "win32";
|
|
3
4
|
try {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
stderr: "ignore",
|
|
7
|
-
});
|
|
5
|
+
const cmd = isWindows ? ["netstat", "-ano"] : ["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"];
|
|
6
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "ignore" });
|
|
8
7
|
const out = await new Response(proc.stdout).text();
|
|
9
8
|
await proc.exited;
|
|
10
|
-
return
|
|
11
|
-
.map(Number)
|
|
12
|
-
.filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
9
|
+
return isWindows ? parseNetstat(out, port) : parseLsof(out);
|
|
13
10
|
} catch {
|
|
14
11
|
return [];
|
|
15
12
|
}
|
|
16
13
|
}
|
|
14
|
+
|
|
15
|
+
/** `lsof -t` output: one PID per line. */
|
|
16
|
+
export function parseLsof(out: string): number[] {
|
|
17
|
+
return uniquePids(out.split("\n").map((s) => s.trim()));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `netstat -ano` (Windows) rows: `TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 1234`.
|
|
22
|
+
* The state column is localized on non-English Windows, so a listener is
|
|
23
|
+
* detected by its foreign address ending in `:0` instead of the word LISTENING.
|
|
24
|
+
*/
|
|
25
|
+
export function parseNetstat(out: string, port: number): number[] {
|
|
26
|
+
const pids: string[] = [];
|
|
27
|
+
for (const line of out.split(/\r?\n/)) {
|
|
28
|
+
const cols = line.trim().split(/\s+/);
|
|
29
|
+
if (cols.length < 5 || cols[0].toUpperCase() !== "TCP") continue;
|
|
30
|
+
const [, local, foreign] = cols;
|
|
31
|
+
if (local.endsWith(`:${port}`) && foreign.endsWith(":0")) pids.push(cols[cols.length - 1]);
|
|
32
|
+
}
|
|
33
|
+
return uniquePids(pids);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function uniquePids(raw: string[]): number[] {
|
|
37
|
+
return [...new Set(raw)].map(Number).filter((pid) => Number.isInteger(pid) && pid > 0);
|
|
38
|
+
}
|
package/src/core/routeFile.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync } from "fs";
|
|
2
2
|
import type { RouteManifest } from "./types.ts";
|
|
3
3
|
import { currentBase } from "./appBase.ts";
|
|
4
|
+
import { toPosix } from "./paths.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The pattern the *client* router matches against. It sees real browser URLs, so
|
|
@@ -250,5 +251,5 @@ function generateClientRoutesFile(
|
|
|
250
251
|
|
|
251
252
|
// Import path from .bosia/routes.ts to src/routes/<routePath>
|
|
252
253
|
function toImportPath(routePath: string): string {
|
|
253
|
-
return "../src/routes/" + routePath
|
|
254
|
+
return "../src/routes/" + toPosix(routePath);
|
|
254
255
|
}
|
package/src/core/routeTypes.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import type { RouteManifest } from "./types.ts";
|
|
4
|
+
import { toPosix } from "./paths.ts";
|
|
4
5
|
|
|
5
6
|
// ─── Route Types Generator ────────────────────────────────
|
|
6
7
|
// Generates .bosia/types/src/routes/**/$types.d.ts for each
|
|
@@ -9,7 +10,7 @@ import type { RouteManifest } from "./types.ts";
|
|
|
9
10
|
// work in +page.svelte files — identical to SvelteKit's API.
|
|
10
11
|
|
|
11
12
|
function routeDirOf(filePath: string): string {
|
|
12
|
-
const parts = filePath
|
|
13
|
+
const parts = toPosix(filePath).split("/");
|
|
13
14
|
parts.pop();
|
|
14
15
|
return parts.join("/") || ".";
|
|
15
16
|
}
|
package/src/core/safePath.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, resolve as resolvePath } from "path";
|
|
1
|
+
import { join, resolve as resolvePath, sep } from "path";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Resolve `untrusted` relative to `base` and verify the result stays inside
|
|
@@ -10,5 +10,5 @@ import { join, resolve as resolvePath } from "path";
|
|
|
10
10
|
export function safePath(base: string, untrusted: string): string | null {
|
|
11
11
|
const root = resolvePath(base);
|
|
12
12
|
const full = resolvePath(join(base, untrusted));
|
|
13
|
-
return full.startsWith(root +
|
|
13
|
+
return full.startsWith(root + sep) || full === root ? full : null;
|
|
14
14
|
}
|
package/src/core/scanner.ts
CHANGED
|
@@ -70,6 +70,10 @@ export function scanRoutes(): RouteManifest {
|
|
|
70
70
|
const fullDir = join(ROUTES_DIR, dir);
|
|
71
71
|
if (!existsSync(fullDir)) return;
|
|
72
72
|
|
|
73
|
+
// Manifest paths are always "/"-separated (they become import specifiers),
|
|
74
|
+
// so build them by hand — path.join would emit "\" on Windows.
|
|
75
|
+
const rel = (name: string) => (dir ? `${dir}/${name}` : name);
|
|
76
|
+
|
|
73
77
|
const items = readdirSync(fullDir, { withFileTypes: true });
|
|
74
78
|
|
|
75
79
|
// Accumulate layouts for this level
|
|
@@ -81,14 +85,14 @@ export function scanRoutes(): RouteManifest {
|
|
|
81
85
|
// as the layout chain. Without this a section with 40 routes needed 40
|
|
82
86
|
// identical +loading.svelte files to cover its navigations.
|
|
83
87
|
const currentLoading = items.some((i) => i.isFile() && i.name === "+loading.svelte")
|
|
84
|
-
?
|
|
88
|
+
? rel("+loading.svelte")
|
|
85
89
|
: inheritedLoading;
|
|
86
90
|
|
|
87
91
|
if (items.some((i) => i.isFile() && i.name === "+layout.svelte")) {
|
|
88
|
-
currentLayouts.push(
|
|
92
|
+
currentLayouts.push(rel("+layout.svelte"));
|
|
89
93
|
}
|
|
90
94
|
if (items.some((i) => i.isFile() && i.name === "+layout.server.ts")) {
|
|
91
|
-
const layoutServerPath =
|
|
95
|
+
const layoutServerPath = rel("+layout.server.ts");
|
|
92
96
|
currentLayoutServers.push({
|
|
93
97
|
path: layoutServerPath,
|
|
94
98
|
depth: currentLayouts.length - 1,
|
|
@@ -100,7 +104,7 @@ export function scanRoutes(): RouteManifest {
|
|
|
100
104
|
// depth = number of layouts wrapping this dir (this dir's layout included).
|
|
101
105
|
// An error page at depth K renders inside layouts[0..K-1].
|
|
102
106
|
currentErrorPages.push({
|
|
103
|
-
path:
|
|
107
|
+
path: rel("+error.svelte"),
|
|
104
108
|
depth: currentLayouts.length,
|
|
105
109
|
});
|
|
106
110
|
}
|
|
@@ -109,20 +113,20 @@ export function scanRoutes(): RouteManifest {
|
|
|
109
113
|
if (items.some((i) => i.isFile() && i.name === "+server.ts")) {
|
|
110
114
|
apis.push({
|
|
111
115
|
pattern: toUrlPath(urlSegments),
|
|
112
|
-
server:
|
|
116
|
+
server: rel("+server.ts"),
|
|
113
117
|
});
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
// Page route (+page.svelte)
|
|
117
121
|
if (items.some((i) => i.isFile() && i.name === "+page.svelte")) {
|
|
118
122
|
const pageServerFile = items.some((i) => i.isFile() && i.name === "+page.server.ts")
|
|
119
|
-
?
|
|
123
|
+
? rel("+page.server.ts")
|
|
120
124
|
: null;
|
|
121
125
|
|
|
122
126
|
const pageTs = pageServerFile ? readTrailingSlash(join(ROUTES_DIR, pageServerFile)) : null;
|
|
123
127
|
const effectiveTs: TrailingSlash = pageTs ?? currentTrailingSlash;
|
|
124
128
|
|
|
125
|
-
const pageFile =
|
|
129
|
+
const pageFile = rel("+page.svelte");
|
|
126
130
|
pages.push({
|
|
127
131
|
pattern: toUrlPath(urlSegments),
|
|
128
132
|
page: pageFile,
|
|
@@ -146,7 +150,7 @@ export function scanRoutes(): RouteManifest {
|
|
|
146
150
|
const isGroup = /^\(.*\)$/.test(dirName);
|
|
147
151
|
|
|
148
152
|
walk(
|
|
149
|
-
|
|
153
|
+
rel(dirName),
|
|
150
154
|
isGroup ? [...urlSegments] : [...urlSegments, dirName],
|
|
151
155
|
currentLayouts,
|
|
152
156
|
currentLayoutServers,
|
|
@@ -177,11 +181,45 @@ export function scanRoutes(): RouteManifest {
|
|
|
177
181
|
}
|
|
178
182
|
}
|
|
179
183
|
|
|
184
|
+
preventConflicts(pages, (p) => p.page);
|
|
185
|
+
preventConflicts(apis, (a) => a.server);
|
|
186
|
+
|
|
180
187
|
const errorPage = existsSync(join(ROUTES_DIR, "+error.svelte")) ? "+error.svelte" : null;
|
|
181
188
|
|
|
182
189
|
return { pages, apis, errorPage };
|
|
183
190
|
}
|
|
184
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
|
+
|
|
185
223
|
function toUrlPath(segments: string[]): string {
|
|
186
224
|
if (segments.length === 0) return "/";
|
|
187
225
|
return "/" + segments.join("/");
|