@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":"package-registry.d.ts","sourceRoot":"","sources":["../../src/kernel/package-registry.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,GAAG,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAgB;IAEjC;;OAEG;IACG,aAAa,CACjB,aAAa,EAAE,MAAM,EACrB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,IAAI,CAAC;IAwChB;;OAEG;YACW,aAAa;IAmF3B;;OAEG;IACH,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAIpD;;OAEG;IACH,cAAc,IAAI,WAAW,EAAE;IAI/B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAW7B;;OAEG;IACH,eAAe,IAAI,MAAM;CAG1B;AAKD,wBAAgB,kBAAkB,IAAI,eAAe,CAKpD"}
1
+ {"version":3,"file":"package-registry.d.ts","sourceRoot":"","sources":["../../src/kernel/package-registry.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,GAAG,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAgB;IAEjC;;OAEG;IACG,aAAa,CACjB,aAAa,EAAE,MAAM,EACrB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,IAAI,CAAC;IAwChB;;OAEG;YACW,aAAa;IAmF3B;;OAEG;IACH,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAIpD;;OAEG;IACH,cAAc,IAAI,WAAW,EAAE;IAI/B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAW7B;;OAEG;IACH,eAAe,IAAI,MAAM;CAG1B;AAKD,wBAAgB,kBAAkB,IAAI,eAAe,CAKpD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
@@ -157,3 +157,11 @@ export function getPackageRegistry() {
157
157
  }
158
158
  return registryInstance;
159
159
  }
160
+ /**
161
+ * Reset the package registry singleton. For use in tests only.
162
+ * Call before creating a ModuleResolver to prevent the previous scan state
163
+ * from leaking between tests.
164
+ */
165
+ export function resetPackageRegistry() {
166
+ registryInstance = null;
167
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgD5E"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8C5E"}
@@ -10,29 +10,31 @@ import path from "node:path";
10
10
  * Updated: Now also checks for lib/ directory to ensure we find the correct SWS root
11
11
  */
12
12
  export async function findWorkspaceRoot(root) {
13
+ const debug = process.env["SWITE_DEBUG"] === "1";
13
14
  let current = root;
14
- for (let i = 0; i < 10; i++) { // Increased from 5 to 10 to go higher up
15
+ for (let i = 0; i < 10; i++) {
15
16
  const workspaceFile = path.join(current, "pnpm-workspace.yaml");
16
17
  const packageJson = path.join(current, "package.json");
17
18
  const libDir = path.join(current, "lib");
18
19
  try {
19
20
  await fs.access(workspaceFile);
20
- // Accept root if it has lib/ (SWS with lib/) or packages/ (SWS with packages/ at root)
21
21
  const packagesDir = path.join(current, "packages");
22
22
  try {
23
23
  await fs.access(libDir);
24
- console.log(`[workspace] Found workspace root with lib/: ${current}`);
24
+ if (debug)
25
+ console.log(`[workspace] Found workspace root with lib/: ${current}`);
25
26
  return current;
26
27
  }
27
28
  catch {
28
29
  try {
29
30
  await fs.access(packagesDir);
30
- console.log(`[workspace] Found workspace root with packages/: ${current}`);
31
+ if (debug)
32
+ console.log(`[workspace] Found workspace root with packages/: ${current}`);
31
33
  return current;
32
34
  }
33
35
  catch {
34
- // Workspace file exists but no lib/ or packages/, continue searching up
35
- console.log(`[workspace] Found workspace file at ${current} but no lib/ or packages/, continuing search...`);
36
+ if (debug)
37
+ console.log(`[workspace] Found workspace file at ${current} but no lib/ or packages/, continuing search...`);
36
38
  }
37
39
  }
38
40
  }
@@ -40,10 +42,10 @@ export async function findWorkspaceRoot(root) {
40
42
  try {
41
43
  const pkgJson = JSON.parse(await fs.readFile(packageJson, "utf-8"));
42
44
  if (pkgJson?.workspaces) {
43
- // Also check for lib/ when package.json has workspaces
44
45
  try {
45
46
  await fs.access(libDir);
46
- console.log(`[workspace] Found workspace root with lib/ (via package.json): ${current}`);
47
+ if (debug)
48
+ console.log(`[workspace] Found workspace root with lib/ (via package.json): ${current}`);
47
49
  return current;
48
50
  }
49
51
  catch {
@@ -60,6 +62,7 @@ export async function findWorkspaceRoot(root) {
60
62
  break;
61
63
  current = parent;
62
64
  }
63
- console.warn(`[workspace] No workspace root found starting from: ${root}`);
65
+ if (debug)
66
+ console.warn(`[workspace] No workspace root found starting from: ${root}`);
64
67
  return null;
65
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swissjs/swite",
3
- "version": "0.3.5",
3
+ "version": "0.4.1",
4
4
  "description": "SWITE - SWISS Development Server (Vite replacement)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,9 +8,19 @@
8
8
  "bin": {
9
9
  "swite": "dist/cli.js"
10
10
  },
11
+ "scripts": {
12
+ "build": "tsc -b",
13
+ "dev": "tsc -b --watch",
14
+ "clean": "rm -rf dist node_modules && rm -f tsconfig.tsbuildinfo",
15
+ "generate-import-map": "tsx src/internal/generate-import-map-cli.ts",
16
+ "test": "node --import tsx --test __tests__/import-rewriter-bug.test.ts",
17
+ "changeset": "changeset",
18
+ "release:version": "changeset version",
19
+ "release:publish": "changeset publish"
20
+ },
11
21
  "dependencies": {
12
- "@swissjs/core": "0.1.11",
13
- "@swissjs/compiler": "0.1.5",
22
+ "@swissjs/core": "0.2.0",
23
+ "@swissjs/compiler": "0.2.0",
14
24
  "@swissjs/plugin-file-router": "1.0.2",
15
25
  "chalk": "^5.3.0",
16
26
  "chokidar": "^3.5.3",
@@ -32,18 +42,20 @@
32
42
  "access": "public"
33
43
  },
34
44
  "license": "MIT",
45
+ "pnpm": {
46
+ "onlyBuiltDependencies": [
47
+ "esbuild"
48
+ ],
49
+ "overrides": {
50
+ "path-to-regexp": "^0.1.13",
51
+ "picomatch": "^2.3.2",
52
+ "qs": ">=6.15.2",
53
+ "esbuild": ">=0.25.0",
54
+ "vite": ">=6.4.2"
55
+ }
56
+ },
35
57
  "repository": {
36
58
  "type": "git",
37
59
  "url": "git+https://github.com/kibologic/swite.git"
38
- },
39
- "scripts": {
40
- "build": "tsc -b",
41
- "dev": "tsc -b --watch",
42
- "clean": "rm -rf dist node_modules && rm -f tsconfig.tsbuildinfo",
43
- "generate-import-map": "tsx src/internal/generate-import-map-cli.ts",
44
- "test": "node --import tsx --test __tests__/import-rewriter-bug.test.ts",
45
- "changeset": "changeset",
46
- "release:version": "changeset version",
47
- "release:publish": "changeset publish"
48
60
  }
49
- }
61
+ }
@@ -56,9 +56,9 @@ export class SwiteBuilder {
56
56
  await this.copyPublicAssets();
57
57
 
58
58
  const duration = Date.now() - startTime;
59
- console.log(chalk.green(`\n Build completed in ${duration}ms\n`));
59
+ console.log(chalk.green(`\n[OK] Build completed in ${duration}ms\n`));
60
60
  } catch (error) {
61
- console.error(chalk.red("\n Build failed:"), error);
61
+ console.error(chalk.red("\n[FAIL] Build failed:"), error);
62
62
  throw error;
63
63
  } finally {
64
64
  await fs.rm(tempDir, { recursive: true, force: true });
@@ -66,13 +66,13 @@ export class SwiteBuilder {
66
66
  }
67
67
 
68
68
  private async cleanOutputDir(): Promise<void> {
69
- console.log(chalk.blue("🧹 Cleaning output directory..."));
69
+ console.log(chalk.blue("[clean] Cleaning output directory..."));
70
70
  await fs.rm(this.config.outDir, { recursive: true, force: true });
71
71
  await fs.mkdir(this.config.outDir, { recursive: true });
72
72
  }
73
73
 
74
74
  private async compileSwissFiles(tempDir: string): Promise<void> {
75
- console.log(chalk.blue("🔨 Compiling Swiss files..."));
75
+ console.log(chalk.blue("[compile] Compiling Swiss files..."));
76
76
  await fs.mkdir(tempDir, { recursive: true });
77
77
 
78
78
  const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
@@ -90,7 +90,7 @@ export class SwiteBuilder {
90
90
  // Step 2: Discover and compile workspace dependencies
91
91
  const workspaceDeps = await this.discoverWorkspaceDependencies();
92
92
  for (const dep of workspaceDeps) {
93
- console.log(chalk.blue(`📦 Compiling dependency: ${dep.name}`));
93
+ console.log(chalk.blue(`[bundle] Compiling dependency: ${dep.name}`));
94
94
  // Preserve workspace structure: libraries/skltn/src or packages/cart/src or modules/cart/src
95
95
  const depRelativeToWorkspace = workspaceRoot
96
96
  ? path.relative(workspaceRoot, dep.pkgDir)
@@ -269,7 +269,7 @@ export class SwiteBuilder {
269
269
  pkgDir,
270
270
  });
271
271
  console.log(
272
- chalk.gray(` 📦 Found workspace dependency: ${depName}`),
272
+ chalk.gray(` [dep] Found workspace dependency: ${depName}`),
273
273
  );
274
274
  break;
275
275
  }
@@ -315,7 +315,7 @@ export class SwiteBuilder {
315
315
  });
316
316
  console.log(
317
317
  chalk.gray(
318
- ` 📦 Discovered transitive dependency: ${pkgName}`,
318
+ ` [dep] Discovered transitive dependency: ${pkgName}`,
319
319
  ),
320
320
  );
321
321
  break;
@@ -325,14 +325,14 @@ export class SwiteBuilder {
325
325
  }
326
326
  }
327
327
  } catch (error) {
328
- console.warn(chalk.yellow("⚠️ Could not discover dependencies:"), error);
328
+ console.warn(chalk.yellow("[warn] Could not discover dependencies:"), error);
329
329
  }
330
330
 
331
331
  return deps;
332
332
  }
333
333
 
334
334
  private async bundle(tempDir: string): Promise<void> {
335
- console.log(chalk.blue("📦 Bundling with esbuild..."));
335
+ console.log(chalk.blue("[bundle] Bundling with esbuild..."));
336
336
 
337
337
  const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
338
338
  const appRelativeToWorkspace = workspaceRoot
@@ -37,11 +37,6 @@ export interface SwiteUserConfig {
37
37
  * Path is relative to the project root.
38
38
  */
39
39
  entry?: string;
40
- /**
41
- * Module aliases resolved during bare import resolution.
42
- * e.g. { "@/": "src/" } maps @/ imports to the src/ directory.
43
- */
44
- aliases?: Record<string, string>;
45
40
  /**
46
41
  * Glob patterns to exclude from HMR watching in addition to the defaults
47
42
  * (node_modules, .git, dist). Useful for generated files or large assets.
@@ -6,8 +6,14 @@
6
6
 
7
7
  import type { Response } from "express";
8
8
  import { promises as fs } from "node:fs";
9
+ import { UiCompiler } from "@swissjs/compiler";
10
+ import chalk from "chalk";
9
11
  import { ModuleResolver } from "../../resolution/resolver.js";
10
12
  import { resolveFilePath } from "../../resolution/path/file-path-resolver.js";
13
+ import { rewriteImports } from "../../resolution/rewriting/import-rewriter.js";
14
+ import { inlineEnvReferences } from "../../config/env.js";
15
+ import { compilationCache } from "../../internal/cache/compilation-cache.js";
16
+ import { fixSwissLibPaths } from "../../resolution/path/path-fixup.js";
11
17
  import type { SwiteUserConfig } from "../../config/config.js";
12
18
 
13
19
  export interface HandlerContext {
@@ -29,12 +35,115 @@ export function setDevHeaders(res: Response): void {
29
35
  res.setHeader("Surrogate-Control", "no-store");
30
36
  }
31
37
 
38
+ const BARE_IMPORT_RE = /(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
39
+
32
40
  /**
33
41
  * Base handler utilities
34
42
  */
35
43
  export class BaseHandler {
44
+ private compiler = new UiCompiler();
45
+
36
46
  constructor(protected context: HandlerContext) {}
37
47
 
48
+ /**
49
+ * Shared compile-and-serve pipeline used by UIHandler and UIXHandler.
50
+ * Compiles a .ui/.uix file, rewrites imports, applies path fixup, and sends the response.
51
+ */
52
+ protected async compileAndServe(
53
+ url: string,
54
+ filePath: string,
55
+ res: Response,
56
+ label: string,
57
+ ): Promise<void> {
58
+ const pathFixupEnabled = this.context.userConfig?.compilerPathFixup?.enabled !== false;
59
+ const pathFixupPatterns = this.context.userConfig?.compilerPathFixup?.patterns;
60
+ const applyPathFixup = (code: string) =>
61
+ pathFixupEnabled ? fixSwissLibPaths(code, pathFixupPatterns) : code;
62
+
63
+ // Cache hit
64
+ const cached = await compilationCache.get(filePath, (c) => this.getDependencies(c));
65
+ if (cached) {
66
+ const fixed = applyPathFixup(cached);
67
+ setDevHeaders(res);
68
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
69
+ res.setHeader("Content-Length", Buffer.byteLength(fixed, "utf-8"));
70
+ res.end(fixed, "utf-8");
71
+ return;
72
+ }
73
+
74
+ // Cache miss — compile
75
+ const source = await fs.readFile(filePath, "utf-8");
76
+ let compiled = await this.compiler.compileAsync(source, filePath);
77
+
78
+ const esbuild = await import("esbuild");
79
+ const tsResult = await esbuild.transform(compiled, {
80
+ loader: "ts",
81
+ format: "esm",
82
+ target: "esnext",
83
+ sourcefile: filePath,
84
+ });
85
+ compiled = tsResult.code;
86
+
87
+ compiled = applyPathFixup(compiled);
88
+ compiled = inlineEnvReferences(compiled, this.context.env);
89
+
90
+ // Handle CSS imports:
91
+ // - Named/default imports (CSS modules): replace with const binding to empty object
92
+ // so that apps using `import styles from "./x.module.css"` get {} instead of undefined.
93
+ // - Side-effect imports (import "./x.css"): strip silently — no runtime value needed.
94
+ // - Dynamic imports (import("./x.css")): return empty object.
95
+ const beforeCss = compiled;
96
+ // Named/default imports → const <binding> = {}
97
+ compiled = compiled.replace(
98
+ /^[^\S\r\n]*import\s+((?:\w+\s*,?\s*)?(?:\{[^}]*\}\s*,?\s*)?(?:\*\s+as\s+\w+\s*)?)\bfrom\s*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm,
99
+ (_match, binding) => {
100
+ // Extract identifiers from the binding clause and emit const declarations
101
+ const ids: string[] = [];
102
+ const defaultMatch = binding.match(/^(\w+)(?:\s*,|\s*$)/);
103
+ if (defaultMatch) ids.push(defaultMatch[1]);
104
+ const nsMatch = binding.match(/\*\s+as\s+(\w+)/);
105
+ if (nsMatch) ids.push(nsMatch[1]);
106
+ const namedMatch = binding.match(/\{([^}]+)\}/);
107
+ if (namedMatch) {
108
+ namedMatch[1].split(",").forEach((s: string) => {
109
+ const alias = s.trim().split(/\s+as\s+/).pop()?.trim();
110
+ if (alias) ids.push(alias);
111
+ });
112
+ }
113
+ return ids.length ? ids.map((id) => `const ${id} = {};`).join(" ") : "";
114
+ },
115
+ );
116
+ // Side-effect imports → strip
117
+ compiled = compiled.replace(/^[^\S\r\n]*import\s*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, "");
118
+ // Dynamic imports → empty object
119
+ compiled = compiled.replace(/\bimport\s*\(\s*['"][^'"]*\.css['"]\s*\)/g, "({})");
120
+ if (beforeCss !== compiled) {
121
+ const _debug = process.env["SWITE_DEBUG"] === "1";
122
+ if (_debug) console.log(chalk.blue(`[${label}] Handled CSS imports from ${url}`));
123
+ }
124
+
125
+ if (BARE_IMPORT_RE.test(compiled)) {
126
+ console.warn(`[${label}] Compiled output contains bare imports: ${url}`);
127
+ }
128
+
129
+ const rewritten = await rewriteImports(compiled, filePath, this.context.resolver);
130
+ const finalCode = applyPathFixup(rewritten);
131
+
132
+ await compilationCache.set(filePath, compiled, finalCode, (c) => this.getDependencies(c));
133
+
134
+ if (BARE_IMPORT_RE.test(finalCode)) {
135
+ console.error(`[${label}] Bare imports still present after rewriting: ${url}`);
136
+ for (const m of Array.from(rewritten.matchAll(/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g)).slice(0, 3)) {
137
+ console.error(`[${label}] Unresolved import: ${m[1]}`);
138
+ }
139
+ }
140
+
141
+ setDevHeaders(res);
142
+ res.setHeader("Content-Type", "application/javascript; charset=utf-8");
143
+ res.setHeader("Content-Length", Buffer.byteLength(finalCode, "utf-8"));
144
+ res.end(finalCode, "utf-8");
145
+ }
146
+
38
147
  protected async resolveFilePath(url: string): Promise<string> {
39
148
  return resolveFilePath(url, this.context.root, this.context.workspaceRoot, this.context.userConfig);
40
149
  }
@@ -6,21 +6,10 @@
6
6
 
7
7
  import type { Response } from "express";
8
8
  import { promises as fs } from "node:fs";
9
- import { UiCompiler } from "@swissjs/compiler";
10
9
  import chalk from "chalk";
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";
15
- import {
16
- BaseHandler,
17
- setDevHeaders,
18
- type HandlerContext,
19
- } from "./base-handler.js";
10
+ import { BaseHandler, setDevHeaders, type HandlerContext } from "./base-handler.js";
20
11
 
21
12
  export class UIHandler extends BaseHandler {
22
- private compiler = new UiCompiler();
23
-
24
13
  constructor(context: HandlerContext) {
25
14
  super(context);
26
15
  }
@@ -36,75 +25,6 @@ export class UIHandler extends BaseHandler {
36
25
  throw new Error(`File not found: ${url} (resolved to: ${filePath})`);
37
26
  }
38
27
 
39
- const pathFixupEnabled = this.context.userConfig?.compilerPathFixup?.enabled !== false;
40
- const pathFixupPatterns = this.context.userConfig?.compilerPathFixup?.patterns;
41
- const applyPathFixup = (code: string) =>
42
- pathFixupEnabled ? fixSwissLibPaths(code, pathFixupPatterns) : code;
43
-
44
- // Cache hit
45
- const cached = await compilationCache.get(filePath, (compiled) => this.getDependencies(compiled));
46
- if (cached) {
47
- const fixed = applyPathFixup(cached);
48
- setDevHeaders(res);
49
- res.setHeader("Content-Type", "application/javascript; charset=utf-8");
50
- res.setHeader("Content-Length", Buffer.byteLength(fixed, "utf-8"));
51
- res.end(fixed, "utf-8");
52
- return;
53
- }
54
-
55
- // Cache miss — compile
56
- const source = await fs.readFile(filePath, "utf-8");
57
- let compiled = await this.compiler.compileAsync(source, filePath);
58
-
59
- const esbuild = await import("esbuild");
60
- const tsResult = await esbuild.transform(compiled, {
61
- loader: "ts",
62
- format: "esm",
63
- target: "esnext",
64
- sourcefile: filePath,
65
- });
66
- compiled = tsResult.code;
67
-
68
- // Fix compiler-emitted wrong paths before import rewriting
69
- compiled = applyPathFixup(compiled);
70
-
71
- // Inline import.meta.env references before import rewriting
72
- compiled = inlineEnvReferences(compiled, this.context.env);
73
-
74
- // Strip CSS static-asset imports — they are not ES modules
75
- compiled = stripCssImports(compiled, url);
76
-
77
- const bareImportPattern = /(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
78
- if (bareImportPattern.test(compiled)) {
79
- console.warn(`[.ui] Compiled output contains bare imports: ${url}`);
80
- }
81
-
82
- const rewritten = await rewriteImports(compiled, filePath, this.context.resolver);
83
- const finalCode = applyPathFixup(rewritten);
84
-
85
- await compilationCache.set(filePath, compiled, finalCode, (c) => this.getDependencies(c));
86
-
87
- if (bareImportPattern.test(finalCode)) {
88
- console.error(`[.ui] Bare imports still present after rewriting: ${url}`);
89
- for (const m of Array.from(rewritten.matchAll(/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g)).slice(0, 3)) {
90
- console.error(`[.ui] Unresolved import: ${m[1]}`);
91
- }
92
- }
93
-
94
- setDevHeaders(res);
95
- res.setHeader("Content-Type", "application/javascript; charset=utf-8");
96
- res.setHeader("Content-Length", Buffer.byteLength(finalCode, "utf-8"));
97
- res.end(finalCode, "utf-8");
98
- }
99
- }
100
-
101
- function stripCssImports(code: string, url: string): string {
102
- // Single well-ordered pass: static imports first, then dynamic imports
103
- const before = code;
104
- code = code.replace(/^[^\S\r\n]*import\s[^'"]*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, "");
105
- code = code.replace(/\bimport\s*\(\s*['"][^'"]*\.css['"]\s*\)/g, "undefined");
106
- if (before !== code) {
107
- console.log(chalk.blue(`[.ui] Stripped CSS imports from ${url}`));
28
+ await this.compileAndServe(url, filePath, res, ".ui");
108
29
  }
109
- return code;
110
30
  }
@@ -5,22 +5,10 @@
5
5
  */
6
6
 
7
7
  import type { Response } from "express";
8
- import { promises as fs } from "node:fs";
9
- import { UiCompiler } from "@swissjs/compiler";
10
8
  import chalk from "chalk";
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";
15
- import {
16
- BaseHandler,
17
- setDevHeaders,
18
- type HandlerContext,
19
- } from "./base-handler.js";
9
+ import { BaseHandler, type HandlerContext } from "./base-handler.js";
20
10
 
21
11
  export class UIXHandler extends BaseHandler {
22
- private compiler = new UiCompiler();
23
-
24
12
  constructor(context: HandlerContext) {
25
13
  super(context);
26
14
  }
@@ -28,68 +16,6 @@ export class UIXHandler extends BaseHandler {
28
16
  async handle(url: string, res: Response): Promise<void> {
29
17
  const filePath = await this.resolveFilePath(url);
30
18
  console.log(chalk.blue(`[.uix] ${url}`));
31
-
32
- const pathFixupEnabled = this.context.userConfig?.compilerPathFixup?.enabled !== false;
33
- const pathFixupPatterns = this.context.userConfig?.compilerPathFixup?.patterns;
34
- const applyPathFixup = (code: string) =>
35
- pathFixupEnabled ? fixSwissLibPaths(code, pathFixupPatterns) : code;
36
-
37
- // Cache hit
38
- const cached = await compilationCache.get(filePath, (compiled) => this.getDependencies(compiled));
39
- if (cached) {
40
- const fixed = applyPathFixup(cached);
41
- setDevHeaders(res);
42
- res.setHeader("Content-Type", "application/javascript; charset=utf-8");
43
- res.send(fixed);
44
- return;
45
- }
46
-
47
- // Cache miss — compile
48
- const source = await fs.readFile(filePath, "utf-8");
49
- let compiled = await this.compiler.compileAsync(source, filePath);
50
-
51
- const esbuild = await import("esbuild");
52
- const tsResult = await esbuild.transform(compiled, {
53
- loader: "ts",
54
- format: "esm",
55
- target: "esnext",
56
- sourcefile: filePath,
57
- });
58
- compiled = tsResult.code;
59
-
60
- // Fix compiler-emitted wrong paths before import rewriting
61
- compiled = applyPathFixup(compiled);
62
-
63
- // Inline import.meta.env references before import rewriting
64
- compiled = inlineEnvReferences(compiled, this.context.env);
65
-
66
- // Strip CSS static-asset imports — they are not ES modules
67
- const beforeCss = compiled;
68
- compiled = compiled.replace(/^[^\S\r\n]*import\s[^'"]*['"][^'"]*\.css['"]\s*;?[^\S\r\n]*$/gm, "");
69
- compiled = compiled.replace(/\bimport\s*\(\s*['"][^'"]*\.css['"]\s*\)/g, "undefined");
70
- if (beforeCss !== compiled) {
71
- console.log(chalk.blue(`[.uix] Stripped CSS imports from ${url}`));
72
- }
73
-
74
- const bareImportPattern = /(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/;
75
- if (bareImportPattern.test(compiled)) {
76
- console.warn(`[.uix] Compiled output contains bare imports: ${url}`);
77
- }
78
-
79
- const rewritten = await rewriteImports(compiled, filePath, this.context.resolver);
80
- const finalCode = applyPathFixup(rewritten);
81
-
82
- await compilationCache.set(filePath, compiled, finalCode, (c) => this.getDependencies(c));
83
-
84
- if (bareImportPattern.test(finalCode)) {
85
- console.error(`[.uix] Bare imports still present after rewriting: ${url}`);
86
- for (const m of Array.from(rewritten.matchAll(/(?:import|from|export).*['"](@[^'"]+\/[^'"]+)(?!\/)[^'"]*['"]/g)).slice(0, 3)) {
87
- console.error(`[.uix] Unresolved import: ${m[1]}`);
88
- }
89
- }
90
-
91
- setDevHeaders(res);
92
- res.setHeader("Content-Type", "application/javascript; charset=utf-8");
93
- res.send(finalCode);
19
+ await this.compileAndServe(url, filePath, res, ".uix");
94
20
  }
95
21
  }
@@ -13,16 +13,42 @@ export class HMREngine {
13
13
  private watcher?: chokidar.FSWatcher;
14
14
  private clients = new Set<WebSocket>();
15
15
  private port: number;
16
+ /** Origins permitted to connect (e.g. "http://localhost:3000"). */
17
+ private allowedOrigins: Set<string>;
16
18
 
17
19
  constructor(
18
20
  private root: string,
19
21
  hmrPort?: number,
22
+ allowedOrigins: string[] = [],
20
23
  ) {
21
24
  this.port = hmrPort || 24678;
25
+ // Security (R-002): build an origin allowlist.
26
+ // Always allow the two canonical loopback forms so a default dev setup
27
+ // (host: "localhost", port: 3000) works without any extra config.
28
+ this.allowedOrigins = new Set([
29
+ ...allowedOrigins,
30
+ ]);
22
31
  // WebSocketServer will be created in initialize() method
23
32
  // This allows async port checking before server creation
24
33
  }
25
34
 
35
+ /**
36
+ * Return true when `origin` is on the allowlist.
37
+ * - Absent / empty origin header → REJECT (not a browser page request).
38
+ * - Exact match (scheme + host + optional port) → ALLOW.
39
+ * - The check is case-insensitive on the scheme+host portion per RFC 6454.
40
+ */
41
+ private isOriginAllowed(origin: string | undefined): boolean {
42
+ if (!origin) return false;
43
+ // Normalise: strip trailing slash, lower-case scheme+host.
44
+ const normalise = (o: string) => o.replace(/\/$/, "").toLowerCase();
45
+ const candidate = normalise(origin);
46
+ for (const allowed of this.allowedOrigins) {
47
+ if (normalise(allowed) === candidate) return true;
48
+ }
49
+ return false;
50
+ }
51
+
26
52
  async initialize(): Promise<void> {
27
53
  // Check if port is available, if not find a free one
28
54
  const isAvailable = await this.checkPortAvailable(this.port);
@@ -51,7 +77,26 @@ export class HMREngine {
51
77
  }
52
78
 
53
79
  private setupWebSocket() {
54
- this.wss.on("connection", (ws) => {
80
+ // Security (R-002): validate the Origin header on every incoming WebSocket
81
+ // upgrade to prevent cross-site WebSocket hijacking. A malicious page
82
+ // served from a different origin cannot subscribe to HMR events (which
83
+ // include absolute filesystem paths of every changed file).
84
+ //
85
+ // Connections with a missing or non-allowlisted Origin are rejected with
86
+ // a 403 close frame. Same-origin connections from the dev server's own
87
+ // host:port are always allowed via this.allowedOrigins.
88
+ this.wss.on("connection", (ws, req) => {
89
+ const origin = req.headers["origin"];
90
+ if (!this.isOriginAllowed(origin)) {
91
+ console.warn(
92
+ chalk.red(
93
+ `[HMR] Rejected connection from disallowed origin: ${origin ?? "(none)"}`,
94
+ ),
95
+ );
96
+ ws.close(1008, "Origin not allowed");
97
+ return;
98
+ }
99
+
55
100
  this.clients.add(ws);
56
101
  console.log(chalk.green("[HMR] Client connected"));
57
102