@swissjs/swite 0.3.5 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/DIRECTIVE.md +57 -2
  3. package/__tests__/import-rewriter-bug.test.ts +100 -113
  4. package/__tests__/security-r001-r002.test.ts +190 -0
  5. package/dist/build-engine/builder.js +9 -9
  6. package/dist/cli.js +0 -0
  7. package/dist/config/config.d.ts +0 -5
  8. package/dist/config/config.d.ts.map +1 -1
  9. package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
  10. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  11. package/dist/dev-engine/handlers/base-handler.js +91 -0
  12. package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
  13. package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
  14. package/dist/dev-engine/handlers/ui-handler.js +2 -64
  15. package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
  16. package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
  17. package/dist/dev-engine/handlers/uix-handler.js +2 -58
  18. package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
  19. package/dist/dev-engine/hmr/hmr.d.ts +10 -1
  20. package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
  21. package/dist/dev-engine/hmr/hmr.js +40 -2
  22. package/dist/dev-engine/middleware/middleware-setup.js +4 -3
  23. package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
  24. package/dist/dev-engine/middleware/static-files.js +145 -62
  25. package/dist/dev-engine/pythonDevManager.js +1 -1
  26. package/dist/dev-engine/router/file-router.d.ts.map +1 -1
  27. package/dist/dev-engine/router/file-router.js +2 -29
  28. package/dist/dev-engine/server.d.ts +7 -0
  29. package/dist/dev-engine/server.d.ts.map +1 -1
  30. package/dist/dev-engine/server.js +31 -3
  31. package/dist/kernel/package-finder.d.ts +0 -8
  32. package/dist/kernel/package-finder.d.ts.map +1 -1
  33. package/dist/kernel/package-finder.js +2 -2
  34. package/dist/kernel/package-registry.d.ts +6 -0
  35. package/dist/kernel/package-registry.d.ts.map +1 -1
  36. package/dist/kernel/package-registry.js +8 -0
  37. package/dist/kernel/workspace.d.ts.map +1 -1
  38. package/dist/kernel/workspace.js +12 -9
  39. package/docs/architecture/build-pipeline.md +97 -97
  40. package/docs/architecture/dev-server.md +87 -87
  41. package/docs/architecture/hmr.md +78 -78
  42. package/docs/architecture/import-rewriting.md +101 -101
  43. package/docs/architecture/index.md +16 -16
  44. package/docs/architecture/python-integration.md +93 -93
  45. package/docs/architecture/resolution.md +92 -92
  46. package/docs/cli/build.md +78 -78
  47. package/docs/cli/dev.md +90 -90
  48. package/docs/cli/index.md +15 -15
  49. package/docs/cli/start.md +45 -45
  50. package/docs/development/contributing.md +74 -74
  51. package/docs/development/index.md +12 -12
  52. package/docs/development/internals.md +101 -101
  53. package/docs/guide/configuration.md +89 -89
  54. package/docs/guide/index.md +13 -13
  55. package/docs/guide/project-structure.md +75 -75
  56. package/docs/guide/quickstart.md +113 -113
  57. package/docs/index.md +16 -16
  58. package/package.json +29 -16
  59. package/src/build-engine/builder.ts +9 -9
  60. package/src/config/config.ts +0 -5
  61. package/src/config/env.ts +98 -98
  62. package/src/dev-engine/handlers/base-handler.ts +109 -0
  63. package/src/dev-engine/handlers/ui-handler.ts +30 -110
  64. package/src/dev-engine/handlers/uix-handler.ts +21 -95
  65. package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
  66. package/src/dev-engine/hmr/hmr.ts +46 -1
  67. package/src/dev-engine/middleware/middleware-setup.ts +354 -354
  68. package/src/dev-engine/middleware/static-files.ts +203 -121
  69. package/src/dev-engine/pythonDevManager.ts +1 -1
  70. package/src/dev-engine/router/file-router.ts +2 -45
  71. package/src/dev-engine/server.ts +33 -3
  72. package/src/kernel/package-finder.ts +2 -2
  73. package/src/kernel/package-registry.ts +9 -0
  74. package/src/kernel/workspace.ts +8 -10
  75. package/src/resolution/cdn/cdn-fallback.ts +40 -40
  76. package/src/resolution/path/path-fixup.ts +27 -27
  77. package/src/resolution/rewriting/import-rewriter.ts +237 -237
  78. package/src/resolution/symlink-registry.ts +114 -114
@@ -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
  }
@@ -1,110 +1,30 @@
1
- /*
2
- * Copyright (c) 2024 Themba Mzumara
3
- * SWITE - SWISS Development Server
4
- * Licensed under the MIT License.
5
- */
6
-
7
- import type { Response } from "express";
8
- import { promises as fs } from "node:fs";
9
- import { UiCompiler } from "@swissjs/compiler";
10
- 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";
20
-
21
- export class UIHandler extends BaseHandler {
22
- private compiler = new UiCompiler();
23
-
24
- constructor(context: HandlerContext) {
25
- super(context);
26
- }
27
-
28
- async handle(url: string, res: Response): Promise<void> {
29
- const filePath = await this.resolveFilePath(url);
30
- console.log(chalk.blue(`[.ui] ${url} → ${filePath}`));
31
-
32
- try {
33
- await fs.access(filePath);
34
- } catch {
35
- console.error(chalk.red(`[.ui] File not found: ${filePath}`));
36
- throw new Error(`File not found: ${url} (resolved to: ${filePath})`);
37
- }
38
-
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}`));
108
- }
109
- return code;
110
- }
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import type { Response } from "express";
8
+ import { promises as fs } from "node:fs";
9
+ import chalk from "chalk";
10
+ import { BaseHandler, setDevHeaders, type HandlerContext } from "./base-handler.js";
11
+
12
+ export class UIHandler extends BaseHandler {
13
+ constructor(context: HandlerContext) {
14
+ super(context);
15
+ }
16
+
17
+ async handle(url: string, res: Response): Promise<void> {
18
+ const filePath = await this.resolveFilePath(url);
19
+ console.log(chalk.blue(`[.ui] ${url} ${filePath}`));
20
+
21
+ try {
22
+ await fs.access(filePath);
23
+ } catch {
24
+ console.error(chalk.red(`[.ui] File not found: ${filePath}`));
25
+ throw new Error(`File not found: ${url} (resolved to: ${filePath})`);
26
+ }
27
+
28
+ await this.compileAndServe(url, filePath, res, ".ui");
29
+ }
30
+ }
@@ -1,95 +1,21 @@
1
- /*
2
- * Copyright (c) 2024 Themba Mzumara
3
- * SWITE - SWISS Development Server
4
- * Licensed under the MIT License.
5
- */
6
-
7
- import type { Response } from "express";
8
- import { promises as fs } from "node:fs";
9
- import { UiCompiler } from "@swissjs/compiler";
10
- 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";
20
-
21
- export class UIXHandler extends BaseHandler {
22
- private compiler = new UiCompiler();
23
-
24
- constructor(context: HandlerContext) {
25
- super(context);
26
- }
27
-
28
- async handle(url: string, res: Response): Promise<void> {
29
- const filePath = await this.resolveFilePath(url);
30
- 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);
94
- }
95
- }
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import type { Response } from "express";
8
+ import chalk from "chalk";
9
+ import { BaseHandler, type HandlerContext } from "./base-handler.js";
10
+
11
+ export class UIXHandler extends BaseHandler {
12
+ constructor(context: HandlerContext) {
13
+ super(context);
14
+ }
15
+
16
+ async handle(url: string, res: Response): Promise<void> {
17
+ const filePath = await this.resolveFilePath(url);
18
+ console.log(chalk.blue(`[.uix] ${url}`));
19
+ await this.compileAndServe(url, filePath, res, ".uix");
20
+ }
21
+ }
@@ -1,122 +1,122 @@
1
- /**
2
- * Build the HMR client script served to the browser at /__swite_hmr_client.
3
- *
4
- * The client is plain JavaScript (no TS syntax) because it is injected into
5
- * browser pages as-is. Keeping it in a separate module rather than embedded
6
- * inside hmr.ts makes it editable with syntax highlighting and avoids
7
- * template-literal escaping issues.
8
- */
9
- export function buildHmrClientScript(port: number): string {
10
- return `// SWITE HMR Client
11
- console.log('[SWITE] HMR enabled');
12
-
13
- const socket = new WebSocket('ws://' + window.location.hostname + ':${port}');
14
- const moduleGraph = new Map();
15
- const hotModules = new Map();
16
-
17
- socket.addEventListener('open', () => {
18
- console.log('[SWITE] HMR connected');
19
- });
20
-
21
- socket.addEventListener('message', async (event) => {
22
- const data = JSON.parse(event.data);
23
-
24
- if (data.type === 'update') {
25
- console.log('[SWITE] Processing update:', data.path, 'Type:', data.updateType);
26
-
27
- if (data.updateType === 'style') {
28
- updateStyles();
29
- console.log('[SWITE] Styles hot updated');
30
- } else if (data.updateType === 'hot') {
31
- const moduleName = extractModuleName(data.path);
32
-
33
- if (moduleName && hotModules.has(moduleName)) {
34
- try {
35
- invalidateModule(moduleName);
36
- invalidateDependents(moduleName);
37
-
38
- const updatedModule = await import(data.path + '?t=' + Date.now());
39
- hotModules.set(moduleName, updatedModule);
40
-
41
- updateComponent(moduleName, updatedModule);
42
- console.log('[SWITE] Component hot updated:', moduleName);
43
- } catch (error) {
44
- console.error('[SWITE] Hot update failed:', error);
45
- window.location.reload();
46
- }
47
- } else {
48
- console.log('[SWITE] New component detected, reloading page');
49
- window.location.reload();
50
- }
51
- } else {
52
- console.log('[SWITE] Full page reload required');
53
- window.location.reload();
54
- }
55
- }
56
- });
57
-
58
- function updateStyles() {
59
- const links = document.querySelectorAll('link[rel="stylesheet"]');
60
- links.forEach(link => {
61
- const href = link.getAttribute('href');
62
- if (href) {
63
- const base = href.replace(/[?&]t=\\d+/, '');
64
- link.setAttribute('href', base + (base.includes('?') ? '&' : '?') + 't=' + Date.now());
65
- }
66
- });
67
- }
68
-
69
- function extractModuleName(filePath) {
70
- const parts = filePath.split('/');
71
- const fileName = parts[parts.length - 1];
72
- return fileName ? fileName.replace(/\\.[^.]+$/, '') : null;
73
- }
74
-
75
- function invalidateModule(moduleName) {
76
- if (window.__swiss_modules__) {
77
- delete window.__swiss_modules__[moduleName];
78
- }
79
- }
80
-
81
- function invalidateDependents(moduleName) {
82
- const dependents = moduleGraph.get(moduleName);
83
- if (dependents) {
84
- for (const dependent of dependents) {
85
- invalidateModule(dependent);
86
- }
87
- }
88
- }
89
-
90
- function updateComponent(moduleName, newModule) {
91
- if (window.__swiss_instances__) {
92
- const instances = window.__swiss_instances__[moduleName];
93
- if (instances && Array.isArray(instances)) {
94
- instances.forEach(instance => {
95
- if (instance && typeof instance.update === 'function') {
96
- instance.update(newModule.default || newModule);
97
- }
98
- });
99
- }
100
- }
101
- }
102
-
103
- socket.addEventListener('close', () => {
104
- console.log('[SWITE] HMR disconnected');
105
- });
106
-
107
- socket.addEventListener('error', (error) => {
108
- console.error('[SWITE] HMR error:', error);
109
- });
110
-
111
- window.__swiss_modules__ = window.__swiss_modules__ || {};
112
- window.__swiss_instances__ = window.__swiss_instances__ || {};
113
-
114
- const currentScript = document.currentScript;
115
- if (currentScript && currentScript.src) {
116
- const moduleName = extractModuleName(currentScript.src);
117
- if (moduleName) {
118
- window.__swiss_modules__[moduleName] = true;
119
- }
120
- }
121
- `;
122
- }
1
+ /**
2
+ * Build the HMR client script served to the browser at /__swite_hmr_client.
3
+ *
4
+ * The client is plain JavaScript (no TS syntax) because it is injected into
5
+ * browser pages as-is. Keeping it in a separate module rather than embedded
6
+ * inside hmr.ts makes it editable with syntax highlighting and avoids
7
+ * template-literal escaping issues.
8
+ */
9
+ export function buildHmrClientScript(port: number): string {
10
+ return `// SWITE HMR Client
11
+ console.log('[SWITE] HMR enabled');
12
+
13
+ const socket = new WebSocket('ws://' + window.location.hostname + ':${port}');
14
+ const moduleGraph = new Map();
15
+ const hotModules = new Map();
16
+
17
+ socket.addEventListener('open', () => {
18
+ console.log('[SWITE] HMR connected');
19
+ });
20
+
21
+ socket.addEventListener('message', async (event) => {
22
+ const data = JSON.parse(event.data);
23
+
24
+ if (data.type === 'update') {
25
+ console.log('[SWITE] Processing update:', data.path, 'Type:', data.updateType);
26
+
27
+ if (data.updateType === 'style') {
28
+ updateStyles();
29
+ console.log('[SWITE] Styles hot updated');
30
+ } else if (data.updateType === 'hot') {
31
+ const moduleName = extractModuleName(data.path);
32
+
33
+ if (moduleName && hotModules.has(moduleName)) {
34
+ try {
35
+ invalidateModule(moduleName);
36
+ invalidateDependents(moduleName);
37
+
38
+ const updatedModule = await import(data.path + '?t=' + Date.now());
39
+ hotModules.set(moduleName, updatedModule);
40
+
41
+ updateComponent(moduleName, updatedModule);
42
+ console.log('[SWITE] Component hot updated:', moduleName);
43
+ } catch (error) {
44
+ console.error('[SWITE] Hot update failed:', error);
45
+ window.location.reload();
46
+ }
47
+ } else {
48
+ console.log('[SWITE] New component detected, reloading page');
49
+ window.location.reload();
50
+ }
51
+ } else {
52
+ console.log('[SWITE] Full page reload required');
53
+ window.location.reload();
54
+ }
55
+ }
56
+ });
57
+
58
+ function updateStyles() {
59
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
60
+ links.forEach(link => {
61
+ const href = link.getAttribute('href');
62
+ if (href) {
63
+ const base = href.replace(/[?&]t=\\d+/, '');
64
+ link.setAttribute('href', base + (base.includes('?') ? '&' : '?') + 't=' + Date.now());
65
+ }
66
+ });
67
+ }
68
+
69
+ function extractModuleName(filePath) {
70
+ const parts = filePath.split('/');
71
+ const fileName = parts[parts.length - 1];
72
+ return fileName ? fileName.replace(/\\.[^.]+$/, '') : null;
73
+ }
74
+
75
+ function invalidateModule(moduleName) {
76
+ if (window.__swiss_modules__) {
77
+ delete window.__swiss_modules__[moduleName];
78
+ }
79
+ }
80
+
81
+ function invalidateDependents(moduleName) {
82
+ const dependents = moduleGraph.get(moduleName);
83
+ if (dependents) {
84
+ for (const dependent of dependents) {
85
+ invalidateModule(dependent);
86
+ }
87
+ }
88
+ }
89
+
90
+ function updateComponent(moduleName, newModule) {
91
+ if (window.__swiss_instances__) {
92
+ const instances = window.__swiss_instances__[moduleName];
93
+ if (instances && Array.isArray(instances)) {
94
+ instances.forEach(instance => {
95
+ if (instance && typeof instance.update === 'function') {
96
+ instance.update(newModule.default || newModule);
97
+ }
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ socket.addEventListener('close', () => {
104
+ console.log('[SWITE] HMR disconnected');
105
+ });
106
+
107
+ socket.addEventListener('error', (error) => {
108
+ console.error('[SWITE] HMR error:', error);
109
+ });
110
+
111
+ window.__swiss_modules__ = window.__swiss_modules__ || {};
112
+ window.__swiss_instances__ = window.__swiss_instances__ || {};
113
+
114
+ const currentScript = document.currentScript;
115
+ if (currentScript && currentScript.src) {
116
+ const moduleName = extractModuleName(currentScript.src);
117
+ if (moduleName) {
118
+ window.__swiss_modules__[moduleName] = true;
119
+ }
120
+ }
121
+ `;
122
+ }