@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.
- package/CHANGELOG.md +23 -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 +26 -14
- 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":"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,
|
|
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"}
|
package/dist/kernel/workspace.js
CHANGED
|
@@ -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++) {
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
if (debug)
|
|
32
|
+
console.log(`[workspace] Found workspace root with packages/: ${current}`);
|
|
31
33
|
return current;
|
|
32
34
|
}
|
|
33
35
|
catch {
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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.
|
|
13
|
-
"@swissjs/compiler": "0.
|
|
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
|
|
59
|
+
console.log(chalk.green(`\n[OK] Build completed in ${duration}ms\n`));
|
|
60
60
|
} catch (error) {
|
|
61
|
-
console.error(chalk.red("\n
|
|
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("
|
|
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("
|
|
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(
|
|
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(`
|
|
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
|
-
`
|
|
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("
|
|
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("
|
|
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
|
package/src/config/config.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|