@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/DIRECTIVE.md +57 -2
  3. package/__tests__/import-rewriter-bug.test.ts +100 -113
  4. package/__tests__/security-r001-r002.test.ts +190 -0
  5. package/dist/build-engine/builder.js +9 -9
  6. package/dist/cli.js +0 -0
  7. package/dist/config/config.d.ts +0 -5
  8. package/dist/config/config.d.ts.map +1 -1
  9. package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
  10. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  11. package/dist/dev-engine/handlers/base-handler.js +91 -0
  12. package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
  13. package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
  14. package/dist/dev-engine/handlers/ui-handler.js +2 -64
  15. package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
  16. package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
  17. package/dist/dev-engine/handlers/uix-handler.js +2 -58
  18. package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
  19. package/dist/dev-engine/hmr/hmr.d.ts +10 -1
  20. package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
  21. package/dist/dev-engine/hmr/hmr.js +40 -2
  22. package/dist/dev-engine/middleware/middleware-setup.js +4 -3
  23. package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
  24. package/dist/dev-engine/middleware/static-files.js +145 -62
  25. package/dist/dev-engine/pythonDevManager.js +1 -1
  26. package/dist/dev-engine/router/file-router.d.ts.map +1 -1
  27. package/dist/dev-engine/router/file-router.js +2 -29
  28. package/dist/dev-engine/server.d.ts +7 -0
  29. package/dist/dev-engine/server.d.ts.map +1 -1
  30. package/dist/dev-engine/server.js +31 -3
  31. package/dist/kernel/package-finder.d.ts +0 -8
  32. package/dist/kernel/package-finder.d.ts.map +1 -1
  33. package/dist/kernel/package-finder.js +2 -2
  34. package/dist/kernel/package-registry.d.ts +6 -0
  35. package/dist/kernel/package-registry.d.ts.map +1 -1
  36. package/dist/kernel/package-registry.js +8 -0
  37. package/dist/kernel/workspace.d.ts.map +1 -1
  38. package/dist/kernel/workspace.js +12 -9
  39. package/docs/architecture/build-pipeline.md +97 -97
  40. package/docs/architecture/dev-server.md +87 -87
  41. package/docs/architecture/hmr.md +78 -78
  42. package/docs/architecture/import-rewriting.md +101 -101
  43. package/docs/architecture/index.md +16 -16
  44. package/docs/architecture/python-integration.md +93 -93
  45. package/docs/architecture/resolution.md +92 -92
  46. package/docs/cli/build.md +78 -78
  47. package/docs/cli/dev.md +90 -90
  48. package/docs/cli/index.md +15 -15
  49. package/docs/cli/start.md +45 -45
  50. package/docs/development/contributing.md +74 -74
  51. package/docs/development/index.md +12 -12
  52. package/docs/development/internals.md +101 -101
  53. package/docs/guide/configuration.md +89 -89
  54. package/docs/guide/index.md +13 -13
  55. package/docs/guide/project-structure.md +75 -75
  56. package/docs/guide/quickstart.md +113 -113
  57. package/docs/index.md +16 -16
  58. package/package.json +29 -16
  59. package/src/build-engine/builder.ts +9 -9
  60. package/src/config/config.ts +0 -5
  61. package/src/config/env.ts +98 -98
  62. package/src/dev-engine/handlers/base-handler.ts +109 -0
  63. package/src/dev-engine/handlers/ui-handler.ts +30 -110
  64. package/src/dev-engine/handlers/uix-handler.ts +21 -95
  65. package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
  66. package/src/dev-engine/hmr/hmr.ts +46 -1
  67. package/src/dev-engine/middleware/middleware-setup.ts +354 -354
  68. package/src/dev-engine/middleware/static-files.ts +203 -121
  69. package/src/dev-engine/pythonDevManager.ts +1 -1
  70. package/src/dev-engine/router/file-router.ts +2 -45
  71. package/src/dev-engine/server.ts +33 -3
  72. package/src/kernel/package-finder.ts +2 -2
  73. package/src/kernel/package-registry.ts +9 -0
  74. package/src/kernel/workspace.ts +8 -10
  75. package/src/resolution/cdn/cdn-fallback.ts +40 -40
  76. package/src/resolution/path/path-fixup.ts +27 -27
  77. package/src/resolution/rewriting/import-rewriter.ts +237 -237
  78. package/src/resolution/symlink-registry.ts +114 -114
@@ -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
  }
@@ -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
+ }