@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.
- package/CHANGELOG.md +29 -0
- package/DIRECTIVE.md +57 -2
- package/__tests__/import-rewriter-bug.test.ts +122 -135
- package/__tests__/security-r001-r002.test.ts +190 -0
- package/dist/build-engine/builder.js +9 -9
- package/dist/config/config.d.ts +0 -5
- package/dist/config/config.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
- package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.js +91 -0
- package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
- package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/ui-handler.js +2 -64
- package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
- package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/uix-handler.js +2 -58
- package/dist/dev-engine/hmr/hmr.d.ts +10 -1
- package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
- package/dist/dev-engine/hmr/hmr.js +40 -2
- package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
- package/dist/dev-engine/middleware/static-files.js +145 -62
- package/dist/dev-engine/pythonDevManager.js +1 -1
- package/dist/dev-engine/router/file-router.d.ts.map +1 -1
- package/dist/dev-engine/router/file-router.js +2 -29
- package/dist/dev-engine/server.d.ts +7 -0
- package/dist/dev-engine/server.d.ts.map +1 -1
- package/dist/dev-engine/server.js +31 -3
- package/dist/kernel/package-finder.d.ts +0 -8
- package/dist/kernel/package-finder.d.ts.map +1 -1
- package/dist/kernel/package-finder.js +2 -2
- package/dist/kernel/package-registry.d.ts +6 -0
- package/dist/kernel/package-registry.d.ts.map +1 -1
- package/dist/kernel/package-registry.js +8 -0
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +12 -9
- package/package.json +6 -4
- package/src/build-engine/builder.ts +9 -9
- package/src/config/config.ts +0 -5
- package/src/dev-engine/handlers/base-handler.ts +109 -0
- package/src/dev-engine/handlers/ui-handler.ts +2 -82
- package/src/dev-engine/handlers/uix-handler.ts +2 -76
- package/src/dev-engine/hmr/hmr.ts +46 -1
- package/src/dev-engine/middleware/static-files.ts +813 -731
- package/src/dev-engine/pythonDevManager.ts +1 -1
- package/src/dev-engine/router/file-router.ts +2 -45
- package/src/dev-engine/server.ts +33 -3
- package/src/kernel/package-finder.ts +2 -2
- package/src/kernel/package-registry.ts +9 -0
- 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 =
|
|
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(`
|
|
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 {
|
package/src/dev-engine/server.ts
CHANGED
|
@@ -53,7 +53,32 @@ export class SwiteServer {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
this.resolver = new ModuleResolver(this.config.root);
|
|
56
|
-
|
|
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
|
-
//
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/kernel/workspace.ts
CHANGED
|
@@ -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++) {
|
|
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
|
-
|
|
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
|
}
|