@swissjs/swite 0.3.5 → 0.4.2
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 +30 -0
- package/DIRECTIVE.md +57 -2
- package/__tests__/import-rewriter-bug.test.ts +100 -113
- package/__tests__/security-r001-r002.test.ts +190 -0
- package/dist/build-engine/builder.js +9 -9
- package/dist/cli.js +0 -0
- 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-client-template.js +111 -111
- 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/middleware-setup.js +4 -3
- 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/docs/architecture/build-pipeline.md +97 -97
- package/docs/architecture/dev-server.md +87 -87
- package/docs/architecture/hmr.md +78 -78
- package/docs/architecture/import-rewriting.md +101 -101
- package/docs/architecture/index.md +16 -16
- package/docs/architecture/python-integration.md +93 -93
- package/docs/architecture/resolution.md +92 -92
- package/docs/cli/build.md +78 -78
- package/docs/cli/dev.md +90 -90
- package/docs/cli/index.md +15 -15
- package/docs/cli/start.md +45 -45
- package/docs/development/contributing.md +74 -74
- package/docs/development/index.md +12 -12
- package/docs/development/internals.md +101 -101
- package/docs/guide/configuration.md +89 -89
- package/docs/guide/index.md +13 -13
- package/docs/guide/project-structure.md +75 -75
- package/docs/guide/quickstart.md +113 -113
- package/docs/index.md +16 -16
- package/package.json +29 -16
- package/src/build-engine/builder.ts +9 -9
- package/src/config/config.ts +0 -5
- package/src/config/env.ts +98 -98
- package/src/dev-engine/handlers/base-handler.ts +109 -0
- package/src/dev-engine/handlers/ui-handler.ts +30 -110
- package/src/dev-engine/handlers/uix-handler.ts +21 -95
- package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
- package/src/dev-engine/hmr/hmr.ts +46 -1
- package/src/dev-engine/middleware/middleware-setup.ts +354 -354
- package/src/dev-engine/middleware/static-files.ts +203 -121
- 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
- package/src/resolution/cdn/cdn-fallback.ts +40 -40
- package/src/resolution/path/path-fixup.ts +27 -27
- package/src/resolution/rewriting/import-rewriter.ts +237 -237
- package/src/resolution/symlink-registry.ts +114 -114
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
|
}
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CDN fallback policy.
|
|
3
|
-
*
|
|
4
|
-
* Swite can fall back to jsDelivr (+esm) for packages it can't resolve locally.
|
|
5
|
-
* This must be safe and project-agnostic:
|
|
6
|
-
* - Unscoped packages (e.g. "react") are usually public on npm; allow by default.
|
|
7
|
-
* - Scoped packages (e.g. "@scope/pkg") may be private; do NOT CDN-fallback by default.
|
|
8
|
-
*
|
|
9
|
-
* Opt-in:
|
|
10
|
-
* - Set `SWITE_CDN_FALLBACK_SCOPES` to a comma-separated list of scopes to allow,
|
|
11
|
-
* e.g. "@types,@tanstack".
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
function getScope(specifierOrPkg: string): string | null {
|
|
15
|
-
if (!specifierOrPkg.startsWith("@")) return null;
|
|
16
|
-
const firstSlash = specifierOrPkg.indexOf("/");
|
|
17
|
-
if (firstSlash === -1) return null;
|
|
18
|
-
return specifierOrPkg.slice(0, firstSlash); // "@scope"
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parseAllowList(): Set<string> {
|
|
22
|
-
const raw = process.env.SWITE_CDN_FALLBACK_SCOPES || "";
|
|
23
|
-
const scopes = raw
|
|
24
|
-
.split(",")
|
|
25
|
-
.map((s) => s.trim())
|
|
26
|
-
.filter(Boolean)
|
|
27
|
-
.map((s) => (s.startsWith("@") ? s : `@${s}`));
|
|
28
|
-
return new Set(scopes);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function shouldUseCdnFallback(specifierOrPkg: string): boolean {
|
|
32
|
-
const scope = getScope(specifierOrPkg);
|
|
33
|
-
// Unscoped packages (e.g. "react") are not automatically CDN-eligible; require
|
|
34
|
-
// explicit opt-in via SWITE_CDN_FALLBACK_SCOPES to prevent accidental exfiltration
|
|
35
|
-
// of package requests for private-registry or unscoped internal packages.
|
|
36
|
-
if (!scope) return false;
|
|
37
|
-
const allow = parseAllowList();
|
|
38
|
-
return allow.has(scope);
|
|
39
|
-
}
|
|
40
|
-
|
|
1
|
+
/**
|
|
2
|
+
* CDN fallback policy.
|
|
3
|
+
*
|
|
4
|
+
* Swite can fall back to jsDelivr (+esm) for packages it can't resolve locally.
|
|
5
|
+
* This must be safe and project-agnostic:
|
|
6
|
+
* - Unscoped packages (e.g. "react") are usually public on npm; allow by default.
|
|
7
|
+
* - Scoped packages (e.g. "@scope/pkg") may be private; do NOT CDN-fallback by default.
|
|
8
|
+
*
|
|
9
|
+
* Opt-in:
|
|
10
|
+
* - Set `SWITE_CDN_FALLBACK_SCOPES` to a comma-separated list of scopes to allow,
|
|
11
|
+
* e.g. "@types,@tanstack".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function getScope(specifierOrPkg: string): string | null {
|
|
15
|
+
if (!specifierOrPkg.startsWith("@")) return null;
|
|
16
|
+
const firstSlash = specifierOrPkg.indexOf("/");
|
|
17
|
+
if (firstSlash === -1) return null;
|
|
18
|
+
return specifierOrPkg.slice(0, firstSlash); // "@scope"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseAllowList(): Set<string> {
|
|
22
|
+
const raw = process.env.SWITE_CDN_FALLBACK_SCOPES || "";
|
|
23
|
+
const scopes = raw
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((s) => s.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((s) => (s.startsWith("@") ? s : `@${s}`));
|
|
28
|
+
return new Set(scopes);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function shouldUseCdnFallback(specifierOrPkg: string): boolean {
|
|
32
|
+
const scope = getScope(specifierOrPkg);
|
|
33
|
+
// Unscoped packages (e.g. "react") are not automatically CDN-eligible; require
|
|
34
|
+
// explicit opt-in via SWITE_CDN_FALLBACK_SCOPES to prevent accidental exfiltration
|
|
35
|
+
// of package requests for private-registry or unscoped internal packages.
|
|
36
|
+
if (!scope) return false;
|
|
37
|
+
const allow = parseAllowList();
|
|
38
|
+
return allow.has(scope);
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralised /swiss-lib/ → /swiss-packages/ path fixup.
|
|
3
|
-
*
|
|
4
|
-
* Root cause: the UiCompiler emits absolute `/swiss-lib/` paths in some code
|
|
5
|
-
* paths (compiler was written against an older directory structure). Until the
|
|
6
|
-
* compiler is fixed at source this single function is the authoritative fixup.
|
|
7
|
-
* Apply it once per compilation, before passing code to the import rewriter.
|
|
8
|
-
*
|
|
9
|
-
* Pass `patterns` from `userConfig.compilerPathFixup.patterns` to override the
|
|
10
|
-
* defaults. Pass an empty array to disable all fixups.
|
|
11
|
-
*/
|
|
12
|
-
export function fixSwissLibPaths(
|
|
13
|
-
code: string,
|
|
14
|
-
patterns?: Array<{ from: string; to: string }>,
|
|
15
|
-
): string {
|
|
16
|
-
const activePatterns = patterns ?? [
|
|
17
|
-
{ from: '/swiss-lib/packages/', to: '/swiss-packages/' },
|
|
18
|
-
{ from: '/swiss-lib/', to: '/swiss-packages/' },
|
|
19
|
-
];
|
|
20
|
-
let result = code;
|
|
21
|
-
for (const { from, to } of activePatterns) {
|
|
22
|
-
if (result.includes(from)) {
|
|
23
|
-
result = result.split(from).join(to);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Centralised /swiss-lib/ → /swiss-packages/ path fixup.
|
|
3
|
+
*
|
|
4
|
+
* Root cause: the UiCompiler emits absolute `/swiss-lib/` paths in some code
|
|
5
|
+
* paths (compiler was written against an older directory structure). Until the
|
|
6
|
+
* compiler is fixed at source this single function is the authoritative fixup.
|
|
7
|
+
* Apply it once per compilation, before passing code to the import rewriter.
|
|
8
|
+
*
|
|
9
|
+
* Pass `patterns` from `userConfig.compilerPathFixup.patterns` to override the
|
|
10
|
+
* defaults. Pass an empty array to disable all fixups.
|
|
11
|
+
*/
|
|
12
|
+
export function fixSwissLibPaths(
|
|
13
|
+
code: string,
|
|
14
|
+
patterns?: Array<{ from: string; to: string }>,
|
|
15
|
+
): string {
|
|
16
|
+
const activePatterns = patterns ?? [
|
|
17
|
+
{ from: '/swiss-lib/packages/', to: '/swiss-packages/' },
|
|
18
|
+
{ from: '/swiss-lib/', to: '/swiss-packages/' },
|
|
19
|
+
];
|
|
20
|
+
let result = code;
|
|
21
|
+
for (const { from, to } of activePatterns) {
|
|
22
|
+
if (result.includes(from)) {
|
|
23
|
+
result = result.split(from).join(to);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|