@swissjs/swite 0.3.4 → 0.4.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/DIRECTIVE.md +57 -2
  3. package/__tests__/import-rewriter-bug.test.ts +122 -135
  4. package/__tests__/security-r001-r002.test.ts +190 -0
  5. package/dist/build-engine/builder.js +9 -9
  6. package/dist/config/config.d.ts +0 -5
  7. package/dist/config/config.d.ts.map +1 -1
  8. package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
  9. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  10. package/dist/dev-engine/handlers/base-handler.js +91 -0
  11. package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
  12. package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
  13. package/dist/dev-engine/handlers/ui-handler.js +2 -64
  14. package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
  15. package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
  16. package/dist/dev-engine/handlers/uix-handler.js +2 -58
  17. package/dist/dev-engine/hmr/hmr.d.ts +10 -1
  18. package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
  19. package/dist/dev-engine/hmr/hmr.js +40 -2
  20. package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
  21. package/dist/dev-engine/middleware/static-files.js +145 -62
  22. package/dist/dev-engine/pythonDevManager.js +1 -1
  23. package/dist/dev-engine/router/file-router.d.ts.map +1 -1
  24. package/dist/dev-engine/router/file-router.js +2 -29
  25. package/dist/dev-engine/server.d.ts +7 -0
  26. package/dist/dev-engine/server.d.ts.map +1 -1
  27. package/dist/dev-engine/server.js +31 -3
  28. package/dist/kernel/package-finder.d.ts +0 -8
  29. package/dist/kernel/package-finder.d.ts.map +1 -1
  30. package/dist/kernel/package-finder.js +2 -2
  31. package/dist/kernel/package-registry.d.ts +6 -0
  32. package/dist/kernel/package-registry.d.ts.map +1 -1
  33. package/dist/kernel/package-registry.js +8 -0
  34. package/dist/kernel/workspace.d.ts.map +1 -1
  35. package/dist/kernel/workspace.js +12 -9
  36. package/package.json +6 -4
  37. package/src/build-engine/builder.ts +9 -9
  38. package/src/config/config.ts +0 -5
  39. package/src/dev-engine/handlers/base-handler.ts +109 -0
  40. package/src/dev-engine/handlers/ui-handler.ts +2 -82
  41. package/src/dev-engine/handlers/uix-handler.ts +2 -76
  42. package/src/dev-engine/hmr/hmr.ts +46 -1
  43. package/src/dev-engine/middleware/static-files.ts +813 -731
  44. package/src/dev-engine/pythonDevManager.ts +1 -1
  45. package/src/dev-engine/router/file-router.ts +2 -45
  46. package/src/dev-engine/server.ts +33 -3
  47. package/src/kernel/package-finder.ts +2 -2
  48. package/src/kernel/package-registry.ts +9 -0
  49. package/src/kernel/workspace.ts +8 -10
@@ -5,7 +5,7 @@ import { initPythonProxy } from "../adapters/proxy/proxyToPython.js";
5
5
  import type { PythonServiceConfig } from "../config/config.js";
6
6
 
7
7
  const POLL_INTERVAL_MS = 500;
8
- const HEALTH_TIMEOUT_MS = 15_000;
8
+ const HEALTH_TIMEOUT_MS = 30_000;
9
9
  const BACKOFF_THRESHOLD = 5;
10
10
 
11
11
  let _child: ChildProcess | null = null;
@@ -11,7 +11,6 @@ import type { RouteDefinition } from "@swissjs/core";
11
11
  import { RouteScanner } from "@swissjs/plugin-file-router/core";
12
12
  import { createFileWatcher } from "@swissjs/plugin-file-router/dev";
13
13
  import { HMREngine } from "../hmr/hmr.js";
14
- import { findWorkspaceRoot } from "../../kernel/workspace.js";
15
14
 
16
15
  export interface FileRouterConfig {
17
16
  root: string;
@@ -38,7 +37,6 @@ export async function setupFileRouter(
38
37
  };
39
38
 
40
39
  try {
41
- const workspaceRoot = await findWorkspaceRoot(config.root);
42
40
  const appRoot = config.root;
43
41
 
44
42
  // Initialize route scanner
@@ -49,59 +47,18 @@ export async function setupFileRouter(
49
47
  lazyLoading: true,
50
48
  });
51
49
 
52
- // Scan routes from multiple locations:
53
- // 1. App's pages directory (apps/alpine/src/pages)
54
- // 2. SKLTN's pages directory (framework/skltn/src/pages) - for reusable auth pages
55
50
  const routesToScan: string[] = [];
56
51
 
57
- // App pages
52
+ // App pages directory
58
53
  const appPagesDir = path.join(appRoot, "src", "pages");
59
54
  try {
60
55
  await fs.access(appPagesDir);
61
56
  routesToScan.push(appPagesDir);
62
- console.log(chalk.gray(` 📄 Scanning app routes from ${appPagesDir}`));
57
+ console.log(chalk.gray(` Scanning app routes from ${appPagesDir}`));
63
58
  } catch {
64
59
  // pages directory doesn't exist, skip
65
60
  }
66
61
 
67
- // SKLTN pages (if workspace root exists)
68
- if (workspaceRoot && workspaceRoot !== appRoot) {
69
- // Try framework/skltn first (new location), then fallback to lib/skltn (legacy)
70
- const skltnPagesDir = path.join(
71
- workspaceRoot,
72
- "framework",
73
- "skltn",
74
- "src",
75
- "pages",
76
- );
77
- const legacySkltnPagesDir = path.join(
78
- workspaceRoot,
79
- "lib",
80
- "skltn",
81
- "src",
82
- "pages",
83
- );
84
-
85
- try {
86
- await fs.access(skltnPagesDir);
87
- routesToScan.push(skltnPagesDir);
88
- console.log(
89
- chalk.gray(` 📄 Scanning SKLTN routes from ${skltnPagesDir}`),
90
- );
91
- } catch {
92
- // Try legacy location
93
- try {
94
- await fs.access(legacySkltnPagesDir);
95
- routesToScan.push(legacySkltnPagesDir);
96
- console.log(
97
- chalk.gray(` 📄 Scanning SKLTN routes from ${legacySkltnPagesDir} (legacy)`),
98
- );
99
- } catch {
100
- // pages directory doesn't exist, skip
101
- }
102
- }
103
- }
104
-
105
62
  // Scan all route directories
106
63
  for (const pagesDir of routesToScan) {
107
64
  try {
@@ -53,7 +53,32 @@ export class SwiteServer {
53
53
  };
54
54
 
55
55
  this.resolver = new ModuleResolver(this.config.root);
56
- this.hmr = new HMREngine(this.config.root, this.config.hmrPort);
56
+ // Security (R-002): build the HMR allowed-origin list from the dev server
57
+ // host+port so the WebSocket server can reject cross-origin connections.
58
+ // When host is "localhost" we also add the numeric loopback form and vice
59
+ // versa — browsers send whichever name the user typed in the address bar.
60
+ const devOrigins = this.buildHmrAllowedOrigins();
61
+ this.hmr = new HMREngine(this.config.root, this.config.hmrPort, devOrigins);
62
+ }
63
+
64
+ /**
65
+ * Build the list of origins that are allowed to open an HMR WebSocket.
66
+ * Always includes both the configured host and its loopback alias so the
67
+ * browser can connect regardless of whether the dev typed "localhost" or
68
+ * "127.0.0.1" in the address bar.
69
+ */
70
+ private buildHmrAllowedOrigins(): string[] {
71
+ const { host, port } = this.config;
72
+ const origins: string[] = [];
73
+ const add = (h: string) => origins.push(`http://${h}:${port}`);
74
+
75
+ add(host);
76
+
77
+ // When the dev host is either loopback alias, also allow the other form.
78
+ if (host === "localhost") add("127.0.0.1");
79
+ else if (host === "127.0.0.1") add("localhost");
80
+
81
+ return origins;
57
82
  }
58
83
 
59
84
  // CG-03: find workspace root by walking up from startDir
@@ -137,8 +162,13 @@ export class SwiteServer {
137
162
  console.timeEnd("HMR Start");
138
163
 
139
164
  // Start HTTP server
140
- // Use 0.0.0.0 to bind to all interfaces (IPv4 and IPv6)
141
- const bindHost = this.config.host === "localhost" ? "0.0.0.0" : this.config.host;
165
+ // Security (R-001): honour the requested host literally.
166
+ // The default host is "localhost" which Node binds to the loopback
167
+ // interface only (127.0.0.1 / ::1). Binding all interfaces (0.0.0.0)
168
+ // must be an explicit opt-in: the developer must set host to "0.0.0.0"
169
+ // in their swite.config.ts or pass --host 0.0.0.0 on the CLI.
170
+ // We never silently rewrite a requested loopback address to 0.0.0.0.
171
+ const bindHost = this.config.host;
142
172
  console.time("HTTP Listen");
143
173
  await new Promise<void>((resolve) => {
144
174
  this.app.listen(this.config.port, bindHost, () => {
@@ -20,7 +20,7 @@ export interface PackageLocation {
20
20
  /**
21
21
  * Find any sibling monorepo by searching for its package.json
22
22
  */
23
- export async function findSiblingRepository(startPath: string, repoName: string): Promise<string | null> {
23
+ async function findSiblingRepository(startPath: string, repoName: string): Promise<string | null> {
24
24
  let current = startPath;
25
25
  for (let i = 0; i < 20; i++) {
26
26
  const siblingPath = path.join(current, repoName);
@@ -148,7 +148,7 @@ export async function findPackage(
148
148
  /**
149
149
  * Find all possible workspace roots by searching up the tree
150
150
  */
151
- export async function findWorkspaceRoots(startPath: string): Promise<string[]> {
151
+ async function findWorkspaceRoots(startPath: string): Promise<string[]> {
152
152
  const roots: string[] = [];
153
153
  let current = startPath;
154
154
 
@@ -196,3 +196,12 @@ export function getPackageRegistry(): PackageRegistry {
196
196
  }
197
197
  return registryInstance;
198
198
  }
199
+
200
+ /**
201
+ * Reset the package registry singleton. For use in tests only.
202
+ * Call before creating a ModuleResolver to prevent the previous scan state
203
+ * from leaking between tests.
204
+ */
205
+ export function resetPackageRegistry(): void {
206
+ registryInstance = null;
207
+ }
@@ -12,38 +12,36 @@ import path from "node:path";
12
12
  * Updated: Now also checks for lib/ directory to ensure we find the correct SWS root
13
13
  */
14
14
  export async function findWorkspaceRoot(root: string): Promise<string | null> {
15
+ const debug = process.env["SWITE_DEBUG"] === "1";
15
16
  let current = root;
16
- for (let i = 0; i < 10; i++) { // Increased from 5 to 10 to go higher up
17
+ for (let i = 0; i < 10; i++) {
17
18
  const workspaceFile = path.join(current, "pnpm-workspace.yaml");
18
19
  const packageJson = path.join(current, "package.json");
19
20
  const libDir = path.join(current, "lib");
20
-
21
+
21
22
  try {
22
23
  await fs.access(workspaceFile);
23
- // Accept root if it has lib/ (SWS with lib/) or packages/ (SWS with packages/ at root)
24
24
  const packagesDir = path.join(current, "packages");
25
25
  try {
26
26
  await fs.access(libDir);
27
- console.log(`[workspace] Found workspace root with lib/: ${current}`);
27
+ if (debug) console.log(`[workspace] Found workspace root with lib/: ${current}`);
28
28
  return current;
29
29
  } catch {
30
30
  try {
31
31
  await fs.access(packagesDir);
32
- console.log(`[workspace] Found workspace root with packages/: ${current}`);
32
+ if (debug) console.log(`[workspace] Found workspace root with packages/: ${current}`);
33
33
  return current;
34
34
  } catch {
35
- // Workspace file exists but no lib/ or packages/, continue searching up
36
- console.log(`[workspace] Found workspace file at ${current} but no lib/ or packages/, continuing search...`);
35
+ if (debug) console.log(`[workspace] Found workspace file at ${current} but no lib/ or packages/, continuing search...`);
37
36
  }
38
37
  }
39
38
  } catch {
40
39
  try {
41
40
  const pkgJson = JSON.parse(await fs.readFile(packageJson, "utf-8"));
42
41
  if (pkgJson?.workspaces) {
43
- // Also check for lib/ when package.json has workspaces
44
42
  try {
45
43
  await fs.access(libDir);
46
- console.log(`[workspace] Found workspace root with lib/ (via package.json): ${current}`);
44
+ if (debug) console.log(`[workspace] Found workspace root with lib/ (via package.json): ${current}`);
47
45
  return current;
48
46
  } catch {
49
47
  // Has workspaces but no lib/, continue searching
@@ -57,6 +55,6 @@ export async function findWorkspaceRoot(root: string): Promise<string | null> {
57
55
  if (parent === current) break;
58
56
  current = parent;
59
57
  }
60
- console.warn(`[workspace] No workspace root found starting from: ${root}`);
58
+ if (debug) console.warn(`[workspace] No workspace root found starting from: ${root}`);
61
59
  return null;
62
60
  }