@swissjs/swite 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/DIRECTIVE.md +57 -2
- package/__tests__/import-rewriter-bug.test.ts +122 -135
- package/__tests__/security-r001-r002.test.ts +190 -0
- package/dist/build-engine/builder.js +9 -9
- package/dist/config/config.d.ts +0 -5
- package/dist/config/config.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
- package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.js +91 -0
- package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
- package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/ui-handler.js +2 -64
- package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
- package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/uix-handler.js +2 -58
- package/dist/dev-engine/hmr/hmr.d.ts +10 -1
- package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
- package/dist/dev-engine/hmr/hmr.js +40 -2
- package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
- package/dist/dev-engine/middleware/static-files.js +145 -62
- package/dist/dev-engine/pythonDevManager.js +1 -1
- package/dist/dev-engine/router/file-router.d.ts.map +1 -1
- package/dist/dev-engine/router/file-router.js +2 -29
- package/dist/dev-engine/server.d.ts +7 -0
- package/dist/dev-engine/server.d.ts.map +1 -1
- package/dist/dev-engine/server.js +31 -3
- package/dist/kernel/package-finder.d.ts +0 -8
- package/dist/kernel/package-finder.d.ts.map +1 -1
- package/dist/kernel/package-finder.js +2 -2
- package/dist/kernel/package-registry.d.ts +6 -0
- package/dist/kernel/package-registry.d.ts.map +1 -1
- package/dist/kernel/package-registry.js +8 -0
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +12 -9
- package/package.json +6 -4
- package/src/build-engine/builder.ts +9 -9
- package/src/config/config.ts +0 -5
- package/src/dev-engine/handlers/base-handler.ts +109 -0
- package/src/dev-engine/handlers/ui-handler.ts +2 -82
- package/src/dev-engine/handlers/uix-handler.ts +2 -76
- package/src/dev-engine/hmr/hmr.ts +46 -1
- package/src/dev-engine/middleware/static-files.ts +813 -731
- package/src/dev-engine/pythonDevManager.ts +1 -1
- package/src/dev-engine/router/file-router.ts +2 -45
- package/src/dev-engine/server.ts +33 -3
- package/src/kernel/package-finder.ts +2 -2
- package/src/kernel/package-registry.ts +9 -0
- package/src/kernel/workspace.ts +8 -10
|
@@ -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,
|
|
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;
|
|
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;
|
|
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 {
|
|
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
|
-
|
|
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;
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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,
|
|
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"}
|