@swissjs/swite 0.3.5 → 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 +23 -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 +26 -14
  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
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,SAAS,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;;OAGG;IACH,iBAAiB,CAAC,EAAE;QAClB,yFAAyF;QACzF,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,8FAA8F;QAC9F,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAChD,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAErE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,SAAS,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;;OAGG;IACH,iBAAiB,CAAC,EAAE;QAClB,yFAAyF;QACzF,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,8FAA8F;QAC9F,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAChD,CAAC;CACH;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAErE"}
@@ -17,7 +17,13 @@ export declare function setDevHeaders(res: Response): void;
17
17
  */
18
18
  export declare class BaseHandler {
19
19
  protected context: HandlerContext;
20
+ private compiler;
20
21
  constructor(context: HandlerContext);
22
+ /**
23
+ * Shared compile-and-serve pipeline used by UIHandler and UIXHandler.
24
+ * Compiles a .ui/.uix file, rewrites imports, applies path fixup, and sends the response.
25
+ */
26
+ protected compileAndServe(url: string, filePath: string, res: Response, label: string): Promise<void>;
21
27
  protected resolveFilePath(url: string): Promise<string>;
22
28
  protected fileExists(filePath: string): Promise<boolean>;
23
29
  protected getDependencies(compiled: string): Promise<string[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"base-handler.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/handlers/base-handler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,cAAc,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,eAAe,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAMjD;AAED;;GAEG;AACH,qBAAa,WAAW;IACV,SAAS,CAAC,OAAO,EAAE,cAAc;gBAAvB,OAAO,EAAE,cAAc;cAE7B,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;cAI7C,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;cAS9C,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAmBrE"}
1
+ {"version":3,"file":"base-handler.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/handlers/base-handler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAIxC,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAM9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,cAAc,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,eAAe,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAMjD;AAID;;GAEG;AACH,qBAAa,WAAW;IAGV,SAAS,CAAC,OAAO,EAAE,cAAc;IAF7C,OAAO,CAAC,QAAQ,CAAoB;gBAEd,OAAO,EAAE,cAAc;IAE7C;;;OAGG;cACa,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;cA0FA,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;cAI7C,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;cAS9C,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAmBrE"}
@@ -4,8 +4,14 @@
4
4
  * Licensed under the MIT License.
5
5
  */
6
6
  import { promises as fs } from "node:fs";
7
+ import { UiCompiler } from "@swissjs/compiler";
8
+ import chalk from "chalk";
7
9
  import { ModuleResolver } from "../../resolution/resolver.js";
8
10
  import { resolveFilePath } from "../../resolution/path/file-path-resolver.js";
11
+ import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
12
+ import { inlineEnvReferences } from "../../config/env.js";
13
+ import { compilationCache } from "../../internal/cache/compilation-cache.js";
14
+ import { fixSwissLibPaths } from "../../resolution/path/path-fixup.js";
9
15
  /**
10
16
  * Set cache-busting headers for development
11
17
  */
@@ -16,12 +22,97 @@ export function setDevHeaders(res) {
16
22
  res.setHeader("Expires", "0");
17
23
  res.setHeader("Surrogate-Control", "no-store");
18
24
  }
25
+ const BARE_IMPORT_RE = /(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
19
26
  /**
20
27
  * Base handler utilities
21
28
  */
22
29
  export class BaseHandler {
23
30
  constructor(context) {
24
31
  this.context = context;
32
+ this.compiler = new UiCompiler();
33
+ }
34
+ /**
35
+ * Shared compile-and-serve pipeline used by UIHandler and UIXHandler.
36
+ * Compiles a .ui/.uix file, rewrites imports, applies path fixup, and sends the response.
37
+ */
38
+ async compileAndServe(url, filePath, res, label) {
39
+ const pathFixupEnabled = this.context.userConfig?.compilerPathFixup?.enabled !== false;
40
+ const pathFixupPatterns = this.context.userConfig?.compilerPathFixup?.patterns;
41
+ const applyPathFixup = (code) => pathFixupEnabled ? fixSwissLibPaths(code, pathFixupPatterns) : code;
42
+ // Cache hit
43
+ const cached = await compilationCache.get(filePath, (c) => this.getDependencies(c));
44
+ if (cached) {
45
+ const fixed = applyPathFixup(cached);
46
+ setDevHeaders(res);
47
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
48
+ res.setHeader("Content-Length", Buffer.byteLength(fixed, "utf-8"));
49
+ res.end(fixed, "utf-8");
50
+ return;
51
+ }
52
+ // Cache miss — compile
53
+ const source = await fs.readFile(filePath, "utf-8");
54
+ let compiled = await this.compiler.compileAsync(source, filePath);
55
+ const esbuild = await import("esbuild");
56
+ const tsResult = await esbuild.transform(compiled, {
57
+ loader: "ts",
58
+ format: "esm",
59
+ target: "esnext",
60
+ sourcefile: filePath,
61
+ });
62
+ compiled = tsResult.code;
63
+ compiled = applyPathFixup(compiled);
64
+ compiled = inlineEnvReferences(compiled, this.context.env);
65
+ // Handle CSS imports:
66
+ // - Named/default imports (CSS modules): replace with const binding to empty object
67
+ // so that apps using `import styles from "./x.module.css"` get {} instead of undefined.
68
+ // - Side-effect imports (import "./x.css"): strip silently — no runtime value needed.
69
+ // - Dynamic imports (import("./x.css")): return empty object.
70
+ const beforeCss = compiled;
71
+ // Named/default imports → const <binding> = {}
72
+ compiled = compiled.replace(/^[^\S\r\n]*import\s+((?:\w+\s*,?\s*)?(?:\{[^}]*\}\s*,?\s*)?(?:\*\s+as\s+\w+\s*)?)\bfrom\s*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, (_match, binding) => {
73
+ // Extract identifiers from the binding clause and emit const declarations
74
+ const ids = [];
75
+ const defaultMatch = binding.match(/^(\w+)(?:\s*,|\s*$)/);
76
+ if (defaultMatch)
77
+ ids.push(defaultMatch[1]);
78
+ const nsMatch = binding.match(/\*\s+as\s+(\w+)/);
79
+ if (nsMatch)
80
+ ids.push(nsMatch[1]);
81
+ const namedMatch = binding.match(/\{([^}]+)\}/);
82
+ if (namedMatch) {
83
+ namedMatch[1].split(",").forEach((s) => {
84
+ const alias = s.trim().split(/\s+as\s+/).pop()?.trim();
85
+ if (alias)
86
+ ids.push(alias);
87
+ });
88
+ }
89
+ return ids.length ? ids.map((id) => `const ${id} = {};`).join(" ") : "";
90
+ });
91
+ // Side-effect imports → strip
92
+ compiled = compiled.replace(/^[^\S\r\n]*import\s*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, "");
93
+ // Dynamic imports → empty object
94
+ compiled = compiled.replace(/\bimport\s*\(\s*['"][^'"]*\.css['"]\s*\)/g, "({})");
95
+ if (beforeCss !== compiled) {
96
+ const _debug = process.env["SWITE_DEBUG"] === "1";
97
+ if (_debug)
98
+ console.log(chalk.blue(`[${label}] Handled CSS imports from ${url}`));
99
+ }
100
+ if (BARE_IMPORT_RE.test(compiled)) {
101
+ console.warn(`[${label}] Compiled output contains bare imports: ${url}`);
102
+ }
103
+ const rewritten = await rewriteImports(compiled, filePath, this.context.resolver);
104
+ const finalCode = applyPathFixup(rewritten);
105
+ await compilationCache.set(filePath, compiled, finalCode, (c) => this.getDependencies(c));
106
+ if (BARE_IMPORT_RE.test(finalCode)) {
107
+ console.error(`[${label}] Bare imports still present after rewriting: ${url}`);
108
+ for (const m of Array.from(rewritten.matchAll(/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g)).slice(0, 3)) {
109
+ console.error(`[${label}] Unresolved import: ${m[1]}`);
110
+ }
111
+ }
112
+ setDevHeaders(res);
113
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
114
+ res.setHeader("Content-Length", Buffer.byteLength(finalCode, "utf-8"));
115
+ res.end(finalCode, "utf-8");
25
116
  }
26
117
  async resolveFilePath(url) {
27
118
  return resolveFilePath(url, this.context.root, this.context.workspaceRoot, this.context.userConfig);
@@ -1,7 +1,6 @@
1
1
  import type { Response } from "express";
2
2
  import { BaseHandler, type HandlerContext } from "./base-handler.js";
3
3
  export declare class UIHandler extends BaseHandler {
4
- private compiler;
5
4
  constructor(context: HandlerContext);
6
5
  handle(url: string, res: Response): Promise<void>;
7
6
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ui-handler.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/handlers/ui-handler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQxC,OAAO,EACL,WAAW,EAEX,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAE3B,qBAAa,SAAU,SAAQ,WAAW;IACxC,OAAO,CAAC,QAAQ,CAAoB;gBAExB,OAAO,EAAE,cAAc;IAI7B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAuExD"}
1
+ {"version":3,"file":"ui-handler.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/handlers/ui-handler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EAAE,WAAW,EAAiB,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEpF,qBAAa,SAAU,SAAQ,WAAW;gBAC5B,OAAO,EAAE,cAAc;IAI7B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAaxD"}
@@ -4,17 +4,11 @@
4
4
  * Licensed under the MIT License.
5
5
  */
6
6
  import { promises as fs } from "node:fs";
7
- import { UiCompiler } from "@swissjs/compiler";
8
7
  import chalk from "chalk";
9
- import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
10
- import { inlineEnvReferences } from "../../config/env.js";
11
- import { compilationCache } from "../../internal/cache/compilation-cache.js";
12
- import { fixSwissLibPaths } from "../../resolution/path/path-fixup.js";
13
- import { BaseHandler, setDevHeaders, } from "./base-handler.js";
8
+ import { BaseHandler, setDevHeaders } from "./base-handler.js";
14
9
  export class UIHandler extends BaseHandler {
15
10
  constructor(context) {
16
11
  super(context);
17
- this.compiler = new UiCompiler();
18
12
  }
19
13
  async handle(url, res) {
20
14
  const filePath = await this.resolveFilePath(url);
@@ -26,62 +20,6 @@ export class UIHandler extends BaseHandler {
26
20
  console.error(chalk.red(`[.ui] File not found: ${filePath}`));
27
21
  throw new Error(`File not found: ${url} (resolved to: ${filePath})`);
28
22
  }
29
- const pathFixupEnabled = this.context.userConfig?.compilerPathFixup?.enabled !== false;
30
- const pathFixupPatterns = this.context.userConfig?.compilerPathFixup?.patterns;
31
- const applyPathFixup = (code) => pathFixupEnabled ? fixSwissLibPaths(code, pathFixupPatterns) : code;
32
- // Cache hit
33
- const cached = await compilationCache.get(filePath, (compiled) => this.getDependencies(compiled));
34
- if (cached) {
35
- const fixed = applyPathFixup(cached);
36
- setDevHeaders(res);
37
- res.setHeader("Content-Type", "application/javascript; charset=utf-8");
38
- res.setHeader("Content-Length", Buffer.byteLength(fixed, "utf-8"));
39
- res.end(fixed, "utf-8");
40
- return;
41
- }
42
- // Cache miss — compile
43
- const source = await fs.readFile(filePath, "utf-8");
44
- let compiled = await this.compiler.compileAsync(source, filePath);
45
- const esbuild = await import("esbuild");
46
- const tsResult = await esbuild.transform(compiled, {
47
- loader: "ts",
48
- format: "esm",
49
- target: "esnext",
50
- sourcefile: filePath,
51
- });
52
- compiled = tsResult.code;
53
- // Fix compiler-emitted wrong paths before import rewriting
54
- compiled = applyPathFixup(compiled);
55
- // Inline import.meta.env references before import rewriting
56
- compiled = inlineEnvReferences(compiled, this.context.env);
57
- // Strip CSS static-asset imports — they are not ES modules
58
- compiled = stripCssImports(compiled, url);
59
- const bareImportPattern = /(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
60
- if (bareImportPattern.test(compiled)) {
61
- console.warn(`[.ui] Compiled output contains bare imports: ${url}`);
62
- }
63
- const rewritten = await rewriteImports(compiled, filePath, this.context.resolver);
64
- const finalCode = applyPathFixup(rewritten);
65
- await compilationCache.set(filePath, compiled, finalCode, (c) => this.getDependencies(c));
66
- if (bareImportPattern.test(finalCode)) {
67
- console.error(`[.ui] Bare imports still present after rewriting: ${url}`);
68
- for (const m of Array.from(rewritten.matchAll(/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g)).slice(0, 3)) {
69
- console.error(`[.ui] Unresolved import: ${m[1]}`);
70
- }
71
- }
72
- setDevHeaders(res);
73
- res.setHeader("Content-Type", "application/javascript; charset=utf-8");
74
- res.setHeader("Content-Length", Buffer.byteLength(finalCode, "utf-8"));
75
- res.end(finalCode, "utf-8");
76
- }
77
- }
78
- function stripCssImports(code, url) {
79
- // Single well-ordered pass: static imports first, then dynamic imports
80
- const before = code;
81
- code = code.replace(/^[^\S\r\n]*import\s[^'"]*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, "");
82
- code = code.replace(/\bimport\s*\(\s*['"][^'"]*\.css['"]\s*\)/g, "undefined");
83
- if (before !== code) {
84
- console.log(chalk.blue(`[.ui] Stripped CSS imports from ${url}`));
23
+ await this.compileAndServe(url, filePath, res, ".ui");
85
24
  }
86
- return code;
87
25
  }
@@ -1,7 +1,6 @@
1
1
  import type { Response } from "express";
2
2
  import { BaseHandler, type HandlerContext } from "./base-handler.js";
3
3
  export declare class UIXHandler extends BaseHandler {
4
- private compiler;
5
4
  constructor(context: HandlerContext);
6
5
  handle(url: string, res: Response): Promise<void>;
7
6
  }
@@ -1 +1 @@
1
- {"version":3,"file":"uix-handler.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/handlers/uix-handler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQxC,OAAO,EACL,WAAW,EAEX,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAE3B,qBAAa,UAAW,SAAQ,WAAW;IACzC,OAAO,CAAC,QAAQ,CAAoB;gBAExB,OAAO,EAAE,cAAc;IAI7B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAmExD"}
1
+ {"version":3,"file":"uix-handler.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/handlers/uix-handler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAErE,qBAAa,UAAW,SAAQ,WAAW;gBAC7B,OAAO,EAAE,cAAc;IAI7B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAKxD"}
@@ -3,71 +3,15 @@
3
3
  * SWITE - SWISS Development Server
4
4
  * Licensed under the MIT License.
5
5
  */
6
- import { promises as fs } from "node:fs";
7
- import { UiCompiler } from "@swissjs/compiler";
8
6
  import chalk from "chalk";
9
- import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
10
- import { inlineEnvReferences } from "../../config/env.js";
11
- import { compilationCache } from "../../internal/cache/compilation-cache.js";
12
- import { fixSwissLibPaths } from "../../resolution/path/path-fixup.js";
13
- import { BaseHandler, setDevHeaders, } from "./base-handler.js";
7
+ import { BaseHandler } from "./base-handler.js";
14
8
  export class UIXHandler extends BaseHandler {
15
9
  constructor(context) {
16
10
  super(context);
17
- this.compiler = new UiCompiler();
18
11
  }
19
12
  async handle(url, res) {
20
13
  const filePath = await this.resolveFilePath(url);
21
14
  console.log(chalk.blue(`[.uix] ${url}`));
22
- const pathFixupEnabled = this.context.userConfig?.compilerPathFixup?.enabled !== false;
23
- const pathFixupPatterns = this.context.userConfig?.compilerPathFixup?.patterns;
24
- const applyPathFixup = (code) => pathFixupEnabled ? fixSwissLibPaths(code, pathFixupPatterns) : code;
25
- // Cache hit
26
- const cached = await compilationCache.get(filePath, (compiled) => this.getDependencies(compiled));
27
- if (cached) {
28
- const fixed = applyPathFixup(cached);
29
- setDevHeaders(res);
30
- res.setHeader("Content-Type", "application/javascript; charset=utf-8");
31
- res.send(fixed);
32
- return;
33
- }
34
- // Cache miss — compile
35
- const source = await fs.readFile(filePath, "utf-8");
36
- let compiled = await this.compiler.compileAsync(source, filePath);
37
- const esbuild = await import("esbuild");
38
- const tsResult = await esbuild.transform(compiled, {
39
- loader: "ts",
40
- format: "esm",
41
- target: "esnext",
42
- sourcefile: filePath,
43
- });
44
- compiled = tsResult.code;
45
- // Fix compiler-emitted wrong paths before import rewriting
46
- compiled = applyPathFixup(compiled);
47
- // Inline import.meta.env references before import rewriting
48
- compiled = inlineEnvReferences(compiled, this.context.env);
49
- // Strip CSS static-asset imports — they are not ES modules
50
- const beforeCss = compiled;
51
- compiled = compiled.replace(/^[^\S\r\n]*import\s[^'"]*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, "");
52
- compiled = compiled.replace(/\bimport\s*\(\s*['"][^'"]*\.css['"]\s*\)/g, "undefined");
53
- if (beforeCss !== compiled) {
54
- console.log(chalk.blue(`[.uix] Stripped CSS imports from ${url}`));
55
- }
56
- const bareImportPattern = /(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
57
- if (bareImportPattern.test(compiled)) {
58
- console.warn(`[.uix] Compiled output contains bare imports: ${url}`);
59
- }
60
- const rewritten = await rewriteImports(compiled, filePath, this.context.resolver);
61
- const finalCode = applyPathFixup(rewritten);
62
- await compilationCache.set(filePath, compiled, finalCode, (c) => this.getDependencies(c));
63
- if (bareImportPattern.test(finalCode)) {
64
- console.error(`[.uix] Bare imports still present after rewriting: ${url}`);
65
- for (const m of Array.from(rewritten.matchAll(/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g)).slice(0, 3)) {
66
- console.error(`[.uix] Unresolved import: ${m[1]}`);
67
- }
68
- }
69
- setDevHeaders(res);
70
- res.setHeader("Content-Type", "application/javascript; charset=utf-8");
71
- res.send(finalCode);
15
+ await this.compileAndServe(url, filePath, res, ".uix");
72
16
  }
73
17
  }
@@ -4,7 +4,16 @@ export declare class HMREngine {
4
4
  private watcher?;
5
5
  private clients;
6
6
  private port;
7
- constructor(root: string, hmrPort?: number);
7
+ /** Origins permitted to connect (e.g. "http://localhost:3000"). */
8
+ private allowedOrigins;
9
+ constructor(root: string, hmrPort?: number, allowedOrigins?: string[]);
10
+ /**
11
+ * Return true when `origin` is on the allowlist.
12
+ * - Absent / empty origin header → REJECT (not a browser page request).
13
+ * - Exact match (scheme + host + optional port) → ALLOW.
14
+ * - The check is case-insensitive on the scheme+host portion per RFC 6454.
15
+ */
16
+ private isOriginAllowed;
8
17
  initialize(): Promise<void>;
9
18
  private checkPortAvailable;
10
19
  private setupWebSocket;
@@ -1 +1 @@
1
- {"version":3,"file":"hmr.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/hmr/hmr.ts"],"names":[],"mappings":"AAUA,qBAAa,SAAS;IAOlB,OAAO,CAAC,IAAI;IANd,OAAO,CAAC,GAAG,CAAmB;IAC9B,OAAO,CAAC,OAAO,CAAC,CAAqB;IACrC,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,IAAI,CAAS;gBAGX,IAAI,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM;IAOZ,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAiBnB,kBAAkB;IAUhC,OAAO,CAAC,cAAc;YAYR,YAAY;IAmB1B,OAAO,IAAI,MAAM;IAIX,KAAK,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE;IA2CrC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAYpC,eAAe,IAAI,MAAM;IAIzB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,SAAS;IAeX,IAAI;CAIX"}
1
+ {"version":3,"file":"hmr.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/hmr/hmr.ts"],"names":[],"mappings":"AAUA,qBAAa,SAAS;IASlB,OAAO,CAAC,IAAI;IARd,OAAO,CAAC,GAAG,CAAmB;IAC9B,OAAO,CAAC,OAAO,CAAC,CAAqB;IACrC,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,IAAI,CAAS;IACrB,mEAAmE;IACnE,OAAO,CAAC,cAAc,CAAc;gBAG1B,IAAI,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM,EAChB,cAAc,GAAE,MAAM,EAAO;IAa/B;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAWjB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAiBnB,kBAAkB;IAUhC,OAAO,CAAC,cAAc;YA+BR,YAAY;IAmB1B,OAAO,IAAI,MAAM;IAIX,KAAK,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE;IA2CrC,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAYpC,eAAe,IAAI,MAAM;IAIzB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,SAAS;IAeX,IAAI;CAIX"}
@@ -7,13 +7,37 @@ import * as net from "net";
7
7
  import chalk from "chalk";
8
8
  import { buildHmrClientScript } from "./hmr-client-template.js";
9
9
  export class HMREngine {
10
- constructor(root, hmrPort) {
10
+ constructor(root, hmrPort, allowedOrigins = []) {
11
11
  this.root = root;
12
12
  this.clients = new Set();
13
13
  this.port = hmrPort || 24678;
14
+ // Security (R-002): build an origin allowlist.
15
+ // Always allow the two canonical loopback forms so a default dev setup
16
+ // (host: "localhost", port: 3000) works without any extra config.
17
+ this.allowedOrigins = new Set([
18
+ ...allowedOrigins,
19
+ ]);
14
20
  // WebSocketServer will be created in initialize() method
15
21
  // This allows async port checking before server creation
16
22
  }
23
+ /**
24
+ * Return true when `origin` is on the allowlist.
25
+ * - Absent / empty origin header → REJECT (not a browser page request).
26
+ * - Exact match (scheme + host + optional port) → ALLOW.
27
+ * - The check is case-insensitive on the scheme+host portion per RFC 6454.
28
+ */
29
+ isOriginAllowed(origin) {
30
+ if (!origin)
31
+ return false;
32
+ // Normalise: strip trailing slash, lower-case scheme+host.
33
+ const normalise = (o) => o.replace(/\/$/, "").toLowerCase();
34
+ const candidate = normalise(origin);
35
+ for (const allowed of this.allowedOrigins) {
36
+ if (normalise(allowed) === candidate)
37
+ return true;
38
+ }
39
+ return false;
40
+ }
17
41
  async initialize() {
18
42
  // Check if port is available, if not find a free one
19
43
  const isAvailable = await this.checkPortAvailable(this.port);
@@ -35,7 +59,21 @@ export class HMREngine {
35
59
  });
36
60
  }
37
61
  setupWebSocket() {
38
- this.wss.on("connection", (ws) => {
62
+ // Security (R-002): validate the Origin header on every incoming WebSocket
63
+ // upgrade to prevent cross-site WebSocket hijacking. A malicious page
64
+ // served from a different origin cannot subscribe to HMR events (which
65
+ // include absolute filesystem paths of every changed file).
66
+ //
67
+ // Connections with a missing or non-allowlisted Origin are rejected with
68
+ // a 403 close frame. Same-origin connections from the dev server's own
69
+ // host:port are always allowed via this.allowedOrigins.
70
+ this.wss.on("connection", (ws, req) => {
71
+ const origin = req.headers["origin"];
72
+ if (!this.isOriginAllowed(origin)) {
73
+ console.warn(chalk.red(`[HMR] Rejected connection from disallowed origin: ${origin ?? "(none)"}`));
74
+ ws.close(1008, "Origin not allowed");
75
+ return;
76
+ }
39
77
  this.clients.add(ws);
40
78
  console.log(chalk.green("[HMR] Client connected"));
41
79
  ws.on("close", () => {
@@ -1 +1 @@
1
- {"version":3,"file":"static-files.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/middleware/static-files.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAMvC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC,CAuYf;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC,CAgTf"}
1
+ {"version":3,"file":"static-files.d.ts","sourceRoot":"","sources":["../../../src/dev-engine/middleware/static-files.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAMvC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC,CAkcf;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,IAAI,CAAC,CAuUf"}