bosia 0.9.6 → 0.9.7

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.6",
3
+ "version": "0.9.7",
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/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);
@@ -21,7 +21,7 @@ export interface InstallOptions {
21
21
  // ─── Local registry resolution ────────────────────────────
22
22
 
23
23
  export function resolveLocalRegistry(): string {
24
- let dir = dirname(new URL(import.meta.url).pathname);
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/core/build.ts CHANGED
@@ -1,5 +1,5 @@
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
5
  import { scanRoutes } from "./scanner.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";
@@ -251,7 +251,7 @@ const cssFiles: string[] = [];
251
251
  // `output.kind === "entry-point"` instead so we pin the actual entry.
252
252
  let clientEntry: string | null = null;
253
253
  for (const output of clientResult.outputs) {
254
- const rel = relative(`${OUT_DIR}/client`, output.path);
254
+ const rel = toPosix(relative(`${OUT_DIR}/client`, output.path)); // URL path, not fs path
255
255
  if (output.path.endsWith(".js")) jsFiles.push(rel);
256
256
  if (output.path.endsWith(".css")) cssFiles.push(rel);
257
257
  if ((output as { kind?: string }).kind === "entry-point" && output.path.endsWith(".js")) {
@@ -271,11 +271,8 @@ if (componentCssFile) {
271
271
  }
272
272
 
273
273
  // Entry is always "index.js" due to naming: { entry: "index.[ext]" }
274
- const serverEntry =
275
- serverResult.outputs
276
- .find((o) => o.path.endsWith("index.js"))
277
- ?.path.split("/")
278
- .pop() ?? "index.js";
274
+ const serverEntryOutput = serverResult.outputs.find((o) => o.path.endsWith("index.js"));
275
+ const serverEntry = serverEntryOutput ? basename(serverEntryOutput.path) : "index.js";
279
276
 
280
277
  // 8. Write dist/manifest.json
281
278
  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 bin = join(nm, ".bin", name);
40
- if (existsSync(bin)) return bin;
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", name); // fallback — will produce a clear ENOENT
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.startsWith("/")) {
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
- const rel = refinedAbs.startsWith(process.cwd() + "/")
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
- const rel = abs.startsWith(process.cwd() + "/") ? abs.slice(process.cwd().length + 1) : abs;
121
- return { file: rel, line: pos.line, col: pos.column ?? 1 };
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 proc = Bun.spawn(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"], {
5
- stdout: "pipe",
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 [...new Set(out.split("\n").map((s) => s.trim()))]
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
+ }
@@ -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.replace(/\\/g, "/");
254
+ return "../src/routes/" + toPosix(routePath);
254
255
  }
@@ -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.replace(/\\/g, "/").split("/");
13
+ const parts = toPosix(filePath).split("/");
13
14
  parts.pop();
14
15
  return parts.join("/") || ".";
15
16
  }
@@ -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 + "/") || full === root ? full : null;
13
+ return full.startsWith(root + sep) || full === root ? full : null;
14
14
  }
@@ -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
- ? join(dir, "+loading.svelte")
88
+ ? rel("+loading.svelte")
85
89
  : inheritedLoading;
86
90
 
87
91
  if (items.some((i) => i.isFile() && i.name === "+layout.svelte")) {
88
- currentLayouts.push(join(dir, "+layout.svelte"));
92
+ currentLayouts.push(rel("+layout.svelte"));
89
93
  }
90
94
  if (items.some((i) => i.isFile() && i.name === "+layout.server.ts")) {
91
- const layoutServerPath = join(dir, "+layout.server.ts");
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: join(dir, "+error.svelte"),
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: join(dir, "+server.ts"),
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
- ? join(dir, "+page.server.ts")
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 = join(dir, "+page.svelte");
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
- dir ? join(dir, dirName) : dirName,
153
+ rel(dirName),
150
154
  isGroup ? [...urlSegments] : [...urlSegments, dirName],
151
155
  currentLayouts,
152
156
  currentLayoutServers,