@swissjs/swite 0.3.0

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 (163) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/publish.yml +50 -0
  4. package/.github/workflows/release.yml +53 -0
  5. package/BUILD_ANALYSIS.md +89 -0
  6. package/BUILD_STRATEGY.md +75 -0
  7. package/CHANGELOG.md +53 -0
  8. package/DIRECTIVE.md +488 -0
  9. package/__tests__/css-extraction.test.ts +261 -0
  10. package/__tests__/css-injection-integration.test.ts +247 -0
  11. package/__tests__/css-middleware.test.ts +191 -0
  12. package/__tests__/import-rewriter-bug.test.ts +135 -0
  13. package/dist/builder.d.ts +36 -0
  14. package/dist/builder.d.ts.map +1 -0
  15. package/dist/builder.js +772 -0
  16. package/dist/cache/compilation-cache.d.ts +33 -0
  17. package/dist/cache/compilation-cache.d.ts.map +1 -0
  18. package/dist/cache/compilation-cache.js +130 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +85 -0
  22. package/dist/config-loader.d.ts +8 -0
  23. package/dist/config-loader.d.ts.map +1 -0
  24. package/dist/config-loader.js +40 -0
  25. package/dist/config.d.ts +29 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +7 -0
  28. package/dist/dev/pythonDevManager.d.ts +12 -0
  29. package/dist/dev/pythonDevManager.d.ts.map +1 -0
  30. package/dist/dev/pythonDevManager.js +85 -0
  31. package/dist/env.d.ts +19 -0
  32. package/dist/env.d.ts.map +1 -0
  33. package/dist/env.js +112 -0
  34. package/dist/handlers/base-handler.d.ts +21 -0
  35. package/dist/handlers/base-handler.d.ts.map +1 -0
  36. package/dist/handlers/base-handler.js +38 -0
  37. package/dist/handlers/js-handler.d.ts +10 -0
  38. package/dist/handlers/js-handler.d.ts.map +1 -0
  39. package/dist/handlers/js-handler.js +87 -0
  40. package/dist/handlers/mjs-handler.d.ts +8 -0
  41. package/dist/handlers/mjs-handler.d.ts.map +1 -0
  42. package/dist/handlers/mjs-handler.js +44 -0
  43. package/dist/handlers/node-module-handler.d.ts +16 -0
  44. package/dist/handlers/node-module-handler.d.ts.map +1 -0
  45. package/dist/handlers/node-module-handler.js +267 -0
  46. package/dist/handlers/ts-handler.d.ts +11 -0
  47. package/dist/handlers/ts-handler.d.ts.map +1 -0
  48. package/dist/handlers/ts-handler.js +120 -0
  49. package/dist/handlers/ui-handler.d.ts +12 -0
  50. package/dist/handlers/ui-handler.d.ts.map +1 -0
  51. package/dist/handlers/ui-handler.js +182 -0
  52. package/dist/handlers/uix-handler.d.ts +12 -0
  53. package/dist/handlers/uix-handler.d.ts.map +1 -0
  54. package/dist/handlers/uix-handler.js +135 -0
  55. package/dist/hmr.d.ts +20 -0
  56. package/dist/hmr.d.ts.map +1 -0
  57. package/dist/hmr.js +265 -0
  58. package/dist/import-rewriter.d.ts +3 -0
  59. package/dist/import-rewriter.d.ts.map +1 -0
  60. package/dist/import-rewriter.js +351 -0
  61. package/dist/index.d.ts +14 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +13 -0
  64. package/dist/middleware/hmr-routes.d.ts +12 -0
  65. package/dist/middleware/hmr-routes.d.ts.map +1 -0
  66. package/dist/middleware/hmr-routes.js +97 -0
  67. package/dist/middleware/middleware-setup.d.ts +23 -0
  68. package/dist/middleware/middleware-setup.d.ts.map +1 -0
  69. package/dist/middleware/middleware-setup.js +596 -0
  70. package/dist/middleware/static-files.d.ts +15 -0
  71. package/dist/middleware/static-files.d.ts.map +1 -0
  72. package/dist/middleware/static-files.js +585 -0
  73. package/dist/proxy/SwiteProxyError.d.ts +6 -0
  74. package/dist/proxy/SwiteProxyError.d.ts.map +1 -0
  75. package/dist/proxy/SwiteProxyError.js +9 -0
  76. package/dist/proxy/proxyToPython.d.ts +28 -0
  77. package/dist/proxy/proxyToPython.d.ts.map +1 -0
  78. package/dist/proxy/proxyToPython.js +66 -0
  79. package/dist/resolver/bare-import-resolver.d.ts +9 -0
  80. package/dist/resolver/bare-import-resolver.d.ts.map +1 -0
  81. package/dist/resolver/bare-import-resolver.js +363 -0
  82. package/dist/resolver/symlink-registry.d.ts +13 -0
  83. package/dist/resolver/symlink-registry.d.ts.map +1 -0
  84. package/dist/resolver/symlink-registry.js +98 -0
  85. package/dist/resolver/url-resolver.d.ts +11 -0
  86. package/dist/resolver/url-resolver.d.ts.map +1 -0
  87. package/dist/resolver/url-resolver.js +268 -0
  88. package/dist/resolver/workspace-package-resolver.d.ts +10 -0
  89. package/dist/resolver/workspace-package-resolver.d.ts.map +1 -0
  90. package/dist/resolver/workspace-package-resolver.js +185 -0
  91. package/dist/resolver.d.ts +17 -0
  92. package/dist/resolver.d.ts.map +1 -0
  93. package/dist/resolver.js +191 -0
  94. package/dist/router/file-router.d.ts +19 -0
  95. package/dist/router/file-router.d.ts.map +1 -0
  96. package/dist/router/file-router.js +114 -0
  97. package/dist/server.d.ts +22 -0
  98. package/dist/server.d.ts.map +1 -0
  99. package/dist/server.js +122 -0
  100. package/dist/utils/cdn-fallback.d.ts +14 -0
  101. package/dist/utils/cdn-fallback.d.ts.map +1 -0
  102. package/dist/utils/cdn-fallback.js +36 -0
  103. package/dist/utils/file-path-resolver.d.ts +9 -0
  104. package/dist/utils/file-path-resolver.d.ts.map +1 -0
  105. package/dist/utils/file-path-resolver.js +187 -0
  106. package/dist/utils/generate-import-map-cli.d.ts +3 -0
  107. package/dist/utils/generate-import-map-cli.d.ts.map +1 -0
  108. package/dist/utils/generate-import-map-cli.js +32 -0
  109. package/dist/utils/generate-import-map.d.ts +21 -0
  110. package/dist/utils/generate-import-map.d.ts.map +1 -0
  111. package/dist/utils/generate-import-map.js +119 -0
  112. package/dist/utils/package-finder.d.ts +24 -0
  113. package/dist/utils/package-finder.d.ts.map +1 -0
  114. package/dist/utils/package-finder.js +161 -0
  115. package/dist/utils/package-registry.d.ts +36 -0
  116. package/dist/utils/package-registry.d.ts.map +1 -0
  117. package/dist/utils/package-registry.js +159 -0
  118. package/dist/utils/workspace.d.ts +6 -0
  119. package/dist/utils/workspace.d.ts.map +1 -0
  120. package/dist/utils/workspace.js +65 -0
  121. package/docs/IMPORT_REWRITING.md +164 -0
  122. package/docs/IMPORT_REWRITING_TROUBLESHOOTING.md +139 -0
  123. package/docs/PATH_RESOLUTION_GUIDE.md +221 -0
  124. package/package.json +49 -0
  125. package/src/adapters/proxy/SwiteProxyError.ts +12 -0
  126. package/src/adapters/proxy/proxyToPython.ts +88 -0
  127. package/src/build-engine/builder.ts +960 -0
  128. package/src/cli.ts +109 -0
  129. package/src/config/config-loader.ts +46 -0
  130. package/src/config/config.ts +34 -0
  131. package/src/config/env.ts +98 -0
  132. package/src/dev-engine/handlers/base-handler.ts +68 -0
  133. package/src/dev-engine/handlers/js-handler.ts +134 -0
  134. package/src/dev-engine/handlers/mjs-handler.ts +65 -0
  135. package/src/dev-engine/handlers/node-module-handler.ts +339 -0
  136. package/src/dev-engine/handlers/ts-handler.ts +143 -0
  137. package/src/dev-engine/handlers/ui-handler.ts +105 -0
  138. package/src/dev-engine/handlers/uix-handler.ts +90 -0
  139. package/src/dev-engine/hmr/hmr-client-template.ts +122 -0
  140. package/src/dev-engine/hmr/hmr.ts +173 -0
  141. package/src/dev-engine/middleware/hmr-routes.ts +120 -0
  142. package/src/dev-engine/middleware/middleware-setup.ts +351 -0
  143. package/src/dev-engine/middleware/static-files.ts +728 -0
  144. package/src/dev-engine/pythonDevManager.ts +116 -0
  145. package/src/dev-engine/router/file-router.ts +164 -0
  146. package/src/dev-engine/server.ts +152 -0
  147. package/src/index.ts +26 -0
  148. package/src/internal/cache/compilation-cache.ts +182 -0
  149. package/src/internal/generate-import-map-cli.ts +40 -0
  150. package/src/internal/generate-import-map.ts +154 -0
  151. package/src/kernel/package-finder.ts +164 -0
  152. package/src/kernel/package-registry.ts +198 -0
  153. package/src/kernel/workspace.ts +62 -0
  154. package/src/resolution/bare-import-resolver.ts +400 -0
  155. package/src/resolution/cdn/cdn-fallback.ts +37 -0
  156. package/src/resolution/path/file-path-resolver.ts +190 -0
  157. package/src/resolution/path/path-fixup.ts +19 -0
  158. package/src/resolution/resolver.ts +198 -0
  159. package/src/resolution/rewriting/import-rewriter.ts +237 -0
  160. package/src/resolution/symlink-registry.ts +114 -0
  161. package/src/resolution/url-resolver.ts +231 -0
  162. package/src/resolution/workspace-package-resolver.ts +94 -0
  163. package/tsconfig.json +37 -0
@@ -0,0 +1,116 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import chalk from "chalk";
4
+ import { initPythonProxy } from "../adapters/proxy/proxyToPython.js";
5
+ import type { PythonServiceConfig } from "../config/config.js";
6
+
7
+ const POLL_INTERVAL_MS = 500;
8
+ const HEALTH_TIMEOUT_MS = 15_000;
9
+ const BACKOFF_THRESHOLD = 5;
10
+
11
+ let _child: ChildProcess | null = null;
12
+
13
+ /**
14
+ * Spawn the Python service and wait until its health endpoint responds 200.
15
+ * Streams stdout/stderr line-buffered, prefixed with [python].
16
+ * Also calls initPythonProxy so proxyToPython works without PYTHON_SERVICE_URL.
17
+ */
18
+ export async function startPythonDevService(
19
+ config: PythonServiceConfig,
20
+ projectRoot: string,
21
+ ): Promise<void> {
22
+ const entryPath = resolve(projectRoot, config.entry);
23
+ const healthUrl = `http://localhost:${config.port}${config.healthCheck}`;
24
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
25
+
26
+ console.log(
27
+ chalk.blue(`[python] spawning: ${pythonCmd} ${config.entry} (port ${config.port})`),
28
+ );
29
+
30
+ const env: NodeJS.ProcessEnv = {
31
+ ...process.env,
32
+ ...config.env,
33
+ PORT: String(config.port),
34
+ };
35
+
36
+ _child = spawn(pythonCmd, [entryPath], {
37
+ env,
38
+ stdio: ["ignore", "pipe", "pipe"],
39
+ });
40
+
41
+ pipeLines(_child.stdout, chalk.cyan("[python] "));
42
+ pipeLines(_child.stderr, chalk.yellow("[python] "));
43
+
44
+ _child.on("exit", (code) => {
45
+ if (code !== null && code !== 0) {
46
+ console.error(
47
+ chalk.red(
48
+ `\n[python] process exited with code ${code} — Node server continuing in degraded mode\n`,
49
+ ),
50
+ );
51
+ }
52
+ _child = null;
53
+ });
54
+
55
+ initPythonProxy(config);
56
+
57
+ await pollHealth(healthUrl);
58
+
59
+ console.log(chalk.green(`[python] healthy — ${healthUrl}`));
60
+ }
61
+
62
+ /**
63
+ * Send SIGTERM to the Python child process if running.
64
+ */
65
+ export function stopPythonDevService(): void {
66
+ if (_child) {
67
+ console.log(chalk.gray("[python] shutting down..."));
68
+ _child.kill("SIGTERM");
69
+ _child = null;
70
+ }
71
+ }
72
+
73
+ // ── internals ────────────────────────────────────────────────────────────────
74
+
75
+ function pipeLines(
76
+ stream: NodeJS.ReadableStream | null,
77
+ prefix: string,
78
+ ): void {
79
+ if (!stream) return;
80
+ let buffer = "";
81
+ stream.on("data", (chunk: Buffer) => {
82
+ buffer += chunk.toString();
83
+ const lines = buffer.split("\n");
84
+ buffer = lines.pop() ?? "";
85
+ for (const line of lines) {
86
+ if (line.trim()) process.stdout.write(prefix + line + "\n");
87
+ }
88
+ });
89
+ }
90
+
91
+ async function pollHealth(url: string): Promise<void> {
92
+ const deadline = Date.now() + HEALTH_TIMEOUT_MS;
93
+ let attempt = 0;
94
+
95
+ while (Date.now() < deadline) {
96
+ try {
97
+ const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
98
+ if (res.ok) return;
99
+ } catch {
100
+ // not ready yet
101
+ }
102
+
103
+ attempt++;
104
+ const delay =
105
+ attempt <= BACKOFF_THRESHOLD
106
+ ? POLL_INTERVAL_MS
107
+ : Math.min(POLL_INTERVAL_MS * Math.pow(2, attempt - BACKOFF_THRESHOLD), 3000);
108
+
109
+ await new Promise<void>((resolve) => setTimeout(resolve, delay));
110
+ }
111
+
112
+ stopPythonDevService();
113
+ throw new Error(
114
+ `[python] health check timed out after ${HEALTH_TIMEOUT_MS}ms — is ${url} reachable?`,
115
+ );
116
+ }
@@ -0,0 +1,164 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import { promises as fs } from "node:fs";
8
+ import path from "node:path";
9
+ import chalk from "chalk";
10
+ import type { RouteDefinition } from "@swissjs/core";
11
+ import { RouteScanner } from "@swissjs/plugin-file-router/core";
12
+ import { createFileWatcher } from "@swissjs/plugin-file-router/dev";
13
+ import { HMREngine } from "../hmr/hmr.js";
14
+ import { findWorkspaceRoot } from "../../kernel/workspace.js";
15
+
16
+ export interface FileRouterConfig {
17
+ root: string;
18
+ hmr: HMREngine;
19
+ }
20
+
21
+ export interface FileRouterResult {
22
+ routeScanner: RouteScanner | null;
23
+ routeWatcher: Awaited<ReturnType<typeof createFileWatcher>> | null;
24
+ routes: RouteDefinition[];
25
+ }
26
+
27
+ /**
28
+ * Setup file-based routing
29
+ * Scans for route files in pages/ directories and watches for changes
30
+ */
31
+ export async function setupFileRouter(
32
+ config: FileRouterConfig,
33
+ ): Promise<FileRouterResult> {
34
+ const result: FileRouterResult = {
35
+ routeScanner: null,
36
+ routeWatcher: null,
37
+ routes: [],
38
+ };
39
+
40
+ try {
41
+ const workspaceRoot = await findWorkspaceRoot(config.root);
42
+ const appRoot = config.root;
43
+
44
+ // Initialize route scanner
45
+ result.routeScanner = new RouteScanner({
46
+ routesDir: "./src/pages",
47
+ extensions: [".ui", ".uix"],
48
+ layouts: true,
49
+ lazyLoading: true,
50
+ });
51
+
52
+ // Scan routes from multiple locations:
53
+ // 1. App's pages directory (apps/alpine/src/pages)
54
+ // 2. SKLTN's pages directory (framework/skltn/src/pages) - for reusable auth pages
55
+ const routesToScan: string[] = [];
56
+
57
+ // App pages
58
+ const appPagesDir = path.join(appRoot, "src", "pages");
59
+ try {
60
+ await fs.access(appPagesDir);
61
+ routesToScan.push(appPagesDir);
62
+ console.log(chalk.gray(` 📄 Scanning app routes from ${appPagesDir}`));
63
+ } catch {
64
+ // pages directory doesn't exist, skip
65
+ }
66
+
67
+ // SKLTN pages (if workspace root exists)
68
+ if (workspaceRoot && workspaceRoot !== appRoot) {
69
+ // Try framework/skltn first (new location), then fallback to lib/skltn (legacy)
70
+ const skltnPagesDir = path.join(
71
+ workspaceRoot,
72
+ "framework",
73
+ "skltn",
74
+ "src",
75
+ "pages",
76
+ );
77
+ const legacySkltnPagesDir = path.join(
78
+ workspaceRoot,
79
+ "lib",
80
+ "skltn",
81
+ "src",
82
+ "pages",
83
+ );
84
+
85
+ try {
86
+ await fs.access(skltnPagesDir);
87
+ routesToScan.push(skltnPagesDir);
88
+ console.log(
89
+ chalk.gray(` 📄 Scanning SKLTN routes from ${skltnPagesDir}`),
90
+ );
91
+ } catch {
92
+ // Try legacy location
93
+ try {
94
+ await fs.access(legacySkltnPagesDir);
95
+ routesToScan.push(legacySkltnPagesDir);
96
+ console.log(
97
+ chalk.gray(` 📄 Scanning SKLTN routes from ${legacySkltnPagesDir} (legacy)`),
98
+ );
99
+ } catch {
100
+ // pages directory doesn't exist, skip
101
+ }
102
+ }
103
+ }
104
+
105
+ // Scan all route directories
106
+ for (const pagesDir of routesToScan) {
107
+ try {
108
+ const scannedRoutes = await result.routeScanner.scanRoutes(pagesDir);
109
+ result.routes.push(...scannedRoutes);
110
+ console.log(
111
+ chalk.green(
112
+ ` ✓ Found ${scannedRoutes.length} routes in ${pagesDir}`,
113
+ ),
114
+ );
115
+ } catch (error) {
116
+ console.warn(
117
+ chalk.yellow(` ⚠ Failed to scan routes from ${pagesDir}:`),
118
+ error,
119
+ );
120
+ }
121
+ }
122
+
123
+ // Setup file watcher for route changes
124
+ if (routesToScan.length > 0) {
125
+ // Watch the first pages directory (can be extended to watch multiple)
126
+ result.routeWatcher = await createFileWatcher({
127
+ directory: routesToScan[0],
128
+ extensions: [".ui", ".uix"],
129
+ });
130
+
131
+ result.routeWatcher.on("change", async (filePath) => {
132
+ console.log(chalk.yellow(` 🔄 Route file changed: ${filePath}`));
133
+ // Rescan routes
134
+ result.routes = [];
135
+ for (const pagesDir of routesToScan) {
136
+ try {
137
+ const scannedRoutes =
138
+ await result.routeScanner!.scanRoutes(pagesDir);
139
+ result.routes.push(...scannedRoutes);
140
+ } catch (error) {
141
+ console.warn(`Failed to rescan routes:`, error);
142
+ }
143
+ }
144
+ // Notify HMR about route changes
145
+ config.hmr.notifyChange(filePath);
146
+ });
147
+
148
+ console.log(
149
+ chalk.green(
150
+ ` ✓ File router initialized with ${result.routes.length} routes`,
151
+ ),
152
+ );
153
+ } else {
154
+ console.log(
155
+ chalk.gray(` ⚠ No pages directories found, file router disabled`),
156
+ );
157
+ }
158
+ } catch (error) {
159
+ console.warn(chalk.yellow(` ⚠ File router setup failed:`), error);
160
+ // Continue without file router
161
+ }
162
+
163
+ return result;
164
+ }
@@ -0,0 +1,152 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import type { RouteDefinition } from "@swissjs/core";
8
+ import { RouteScanner } from "@swissjs/plugin-file-router/core";
9
+ import { createFileWatcher } from "@swissjs/plugin-file-router/dev";
10
+ import express from "express";
11
+ import path from "node:path";
12
+ import { promises as fs } from "node:fs";
13
+ import { ModuleResolver } from "../resolution/resolver.js";
14
+ import { HMREngine } from "./hmr/hmr.js";
15
+ import chalk from "chalk";
16
+ import { setupMiddleware } from "./middleware/middleware-setup.js";
17
+ import { buildSymlinkRegistry } from "../resolution/symlink-registry.js";
18
+ import { findSwissLibMonorepo } from "../kernel/package-finder.js";
19
+
20
+ export interface SwiteConfig {
21
+ root: string;
22
+ // Workspace/monorepo root. When set, Swite will use this for resolving
23
+ // node_modules, workspace packages, and import-map generation.
24
+ // This avoids relying on auto-detection (pnpm-workspace.yaml) which may not
25
+ // exist in some deployment contexts.
26
+ rootDir?: string;
27
+ publicDir: string;
28
+ port: number;
29
+ host: string;
30
+ open: boolean;
31
+ hmrPort?: number; // Optional HMR WebSocket port
32
+ }
33
+
34
+ export class SwiteServer {
35
+ private app = express();
36
+ private resolver: ModuleResolver;
37
+ private hmr: HMREngine;
38
+ private config: SwiteConfig;
39
+ private routeScanner: RouteScanner | null = null;
40
+ private routeWatcher: Awaited<ReturnType<typeof createFileWatcher>> | null =
41
+ null;
42
+ private routes: RouteDefinition[] = [];
43
+
44
+ constructor(config: Partial<SwiteConfig> = {}) {
45
+ this.config = {
46
+ root: process.cwd(),
47
+ publicDir: "public",
48
+ port: 3000,
49
+ host: "localhost",
50
+ open: true,
51
+ ...config,
52
+ };
53
+
54
+ this.resolver = new ModuleResolver(this.config.root);
55
+ this.hmr = new HMREngine(this.config.root, this.config.hmrPort);
56
+ }
57
+
58
+ // CG-03: find workspace root by walking up from startDir
59
+ private async findWorkspaceRoot(startDir: string): Promise<string | null> {
60
+ if (this.config.rootDir) {
61
+ return path.resolve(this.config.rootDir);
62
+ }
63
+ let current = startDir;
64
+ for (let i = 0; i < 6; i++) {
65
+ try {
66
+ await fs.access(path.join(current, "pnpm-workspace.yaml"));
67
+ return current;
68
+ } catch {}
69
+ try {
70
+ const pkgJson = JSON.parse(
71
+ await fs.readFile(path.join(current, "package.json"), "utf-8")
72
+ );
73
+ if (pkgJson.workspaces) return current;
74
+ } catch {}
75
+ const parent = path.dirname(current);
76
+ if (parent === current) break;
77
+ current = parent;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ async start() {
83
+ const startTime = Date.now();
84
+ console.log(chalk.cyan("\n⚡ SWITE - SWISS Development Server\n"));
85
+ console.time("Startup");
86
+
87
+ // CG-03: Build symlink registry before serving any requests.
88
+ // Maps realpath(node_modules/pkg symlink) → /node_modules/pkg browser URL
89
+ // so toUrl() can map absolute filesystem paths back to browser URLs.
90
+ console.time("Symlink Registry");
91
+ try {
92
+ const nodeModulesDirs: string[] = [
93
+ path.join(this.config.root, "node_modules"),
94
+ // Also scan the server package's own node_modules (one level up from the
95
+ // app root, e.g. apps/server/node_modules) — pnpm places workspace package
96
+ // symlinks there, not in the app root's node_modules subfolder.
97
+ path.join(path.dirname(this.config.root), "node_modules"),
98
+ ];
99
+ const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
100
+ if (workspaceRoot) {
101
+ nodeModulesDirs.push(path.join(workspaceRoot, "node_modules"));
102
+ }
103
+ const swissLib = await findSwissLibMonorepo(this.config.root);
104
+ if (swissLib) {
105
+ nodeModulesDirs.push(path.join(swissLib, "node_modules"));
106
+ }
107
+ await buildSymlinkRegistry(nodeModulesDirs);
108
+ } catch (err: any) {
109
+ console.warn(`[SWITE] Symlink registry build failed: ${err.message}`);
110
+ }
111
+ console.timeEnd("Symlink Registry");
112
+
113
+ // Setup middleware
114
+ console.time("Middleware Setup");
115
+ const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
116
+ const middlewareResult = await setupMiddleware(this.app, {
117
+ root: this.config.root,
118
+ workspaceRoot,
119
+ publicDir: this.config.publicDir,
120
+ resolver: this.resolver,
121
+ hmr: this.hmr,
122
+ });
123
+ this.routes = middlewareResult.routes;
124
+ this.routeScanner = middlewareResult.routeScanner;
125
+ this.routeWatcher = middlewareResult.routeWatcher;
126
+ console.timeEnd("Middleware Setup");
127
+
128
+ // Start HMR
129
+ console.time("HMR Start");
130
+ await this.hmr.initialize();
131
+ await this.hmr.start();
132
+ console.timeEnd("HMR Start");
133
+
134
+ // Start HTTP server
135
+ // Use 0.0.0.0 to bind to all interfaces (IPv4 and IPv6)
136
+ const bindHost = this.config.host === "localhost" ? "0.0.0.0" : this.config.host;
137
+ console.time("HTTP Listen");
138
+ await new Promise<void>((resolve) => {
139
+ this.app.listen(this.config.port, bindHost, () => {
140
+ console.timeEnd("HTTP Listen");
141
+ console.timeEnd("Startup");
142
+ console.log(
143
+ chalk.green(
144
+ ` ➜ Local: http://localhost:${this.config.port}/`,
145
+ ),
146
+ );
147
+ console.log(chalk.gray(` ➜ Ready in ${Date.now() - startTime}ms\n`));
148
+ resolve();
149
+ });
150
+ });
151
+ }
152
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ /*
2
+ * SWITE - SWISS Development Server
3
+ * Main exports
4
+ */
5
+
6
+ export { SwiteServer } from "./dev-engine/server.js";
7
+ export type { SwiteConfig } from "./dev-engine/server.js";
8
+ export { SwiteBuilder, build } from "./build-engine/builder.js";
9
+ export type { BuildConfig } from "./build-engine/builder.js";
10
+ export { ModuleResolver } from "./resolution/resolver.js";
11
+ export { HMREngine } from "./dev-engine/hmr/hmr.js";
12
+ export { defineConfig } from "./config/config.js";
13
+ export type {
14
+ SwiteUserConfig,
15
+ ServerConfig,
16
+ ServicesConfig,
17
+ PythonServiceConfig,
18
+ } from "./config/config.js";
19
+ export { proxyToPython, initPythonProxy, setProductionMode } from "./adapters/proxy/proxyToPython.js";
20
+ export type { ProxyOptions } from "./adapters/proxy/proxyToPython.js";
21
+ export { SwiteProxyError } from "./adapters/proxy/SwiteProxyError.js";
22
+ export { loadUserConfig } from "./config/config-loader.js";
23
+ export {
24
+ startPythonDevService,
25
+ stopPythonDevService,
26
+ } from "./dev-engine/pythonDevManager.js";
@@ -0,0 +1,182 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Compilation Cache for .ui, .uix, .ts files
5
+ * Licensed under the MIT License.
6
+ */
7
+
8
+ import { promises as fs } from "node:fs";
9
+ import path from "node:path";
10
+ import chalk from "chalk";
11
+
12
+ interface CacheEntry {
13
+ compiled: string;
14
+ rewritten: string;
15
+ mtime: number;
16
+ dependencies: string[];
17
+ timestamp: number;
18
+ }
19
+
20
+ /**
21
+ * Compilation cache with dependency tracking
22
+ * Invalidates when source file or dependencies change
23
+ */
24
+ export class CompilationCache {
25
+ private cache = new Map<string, CacheEntry>();
26
+ private readonly maxSize = 1000; // Prevent memory leaks
27
+
28
+ /**
29
+ * Get cached compilation result if valid
30
+ */
31
+ async get(
32
+ filePath: string,
33
+ getDependencies: (compiled: string) => Promise<string[]>,
34
+ ): Promise<string | null> {
35
+ const entry = this.cache.get(filePath);
36
+ if (!entry) {
37
+ return null;
38
+ }
39
+
40
+ // Check if source file changed
41
+ try {
42
+ const stats = await fs.stat(filePath);
43
+ if (stats.mtimeMs !== entry.mtime) {
44
+ console.log(
45
+ chalk.yellow(`[Cache] Invalidating ${filePath}: file modified`),
46
+ );
47
+ this.cache.delete(filePath);
48
+ return null;
49
+ }
50
+ } catch (error) {
51
+ // File deleted or inaccessible
52
+ this.cache.delete(filePath);
53
+ return null;
54
+ }
55
+
56
+ // Check if dependencies changed (order-independent comparison)
57
+ const currentDeps = await getDependencies(entry.compiled);
58
+ const prevSet = new Set(entry.dependencies);
59
+ const depsChanged =
60
+ currentDeps.length !== entry.dependencies.length ||
61
+ currentDeps.some((dep) => !prevSet.has(dep));
62
+
63
+ if (depsChanged) {
64
+ console.log(
65
+ chalk.yellow(
66
+ `[Cache] Invalidating ${filePath}: dependencies changed`,
67
+ ),
68
+ );
69
+ this.cache.delete(filePath);
70
+ return null;
71
+ }
72
+
73
+ // Check if dependencies still exist and haven't changed
74
+ for (const dep of entry.dependencies) {
75
+ try {
76
+ const depStats = await fs.stat(dep);
77
+ // If dependency was modified after cache entry, invalidate
78
+ if (depStats.mtimeMs > entry.timestamp) {
79
+ console.log(
80
+ chalk.yellow(
81
+ `[Cache] Invalidating ${filePath}: dependency ${dep} modified`,
82
+ ),
83
+ );
84
+ this.cache.delete(filePath);
85
+ return null;
86
+ }
87
+ } catch {
88
+ // Dependency deleted or inaccessible
89
+ console.log(
90
+ chalk.yellow(
91
+ `[Cache] Invalidating ${filePath}: dependency ${dep} not found`,
92
+ ),
93
+ );
94
+ this.cache.delete(filePath);
95
+ return null;
96
+ }
97
+ }
98
+
99
+ // Check if cached content has stale CDN URLs (from before import rewriter fix)
100
+ if (entry.rewritten.includes("cdn.jsdelivr.net") || entry.rewritten.includes("esm.sh")) {
101
+ console.log(
102
+ chalk.yellow(
103
+ `[Cache] Invalidating ${filePath}: contains stale CDN URLs`,
104
+ ),
105
+ );
106
+ this.cache.delete(filePath);
107
+ return null;
108
+ }
109
+
110
+ console.log(chalk.green(`[Cache] ✅ Cache hit for ${filePath}`));
111
+ return entry.rewritten;
112
+ }
113
+
114
+ /**
115
+ * Store compilation result in cache
116
+ */
117
+ async set(
118
+ filePath: string,
119
+ compiled: string,
120
+ rewritten: string,
121
+ getDependencies: (compiled: string) => Promise<string[]>,
122
+ ): Promise<void> {
123
+ // Enforce max size (LRU eviction)
124
+ if (this.cache.size >= this.maxSize) {
125
+ // Remove oldest entry (simple FIFO)
126
+ const firstKey = this.cache.keys().next().value;
127
+ if (firstKey) {
128
+ this.cache.delete(firstKey);
129
+ console.log(chalk.gray(`[Cache] Evicted ${firstKey} (cache full)`));
130
+ }
131
+ }
132
+
133
+ try {
134
+ const stats = await fs.stat(filePath);
135
+ const dependencies = await getDependencies(compiled);
136
+
137
+ this.cache.set(filePath, {
138
+ compiled,
139
+ rewritten,
140
+ mtime: stats.mtimeMs,
141
+ dependencies,
142
+ timestamp: Date.now(),
143
+ });
144
+
145
+ console.log(
146
+ chalk.green(
147
+ `[Cache] ✅ Cached ${filePath} (${dependencies.length} deps)`,
148
+ ),
149
+ );
150
+ } catch (error) {
151
+ console.warn(chalk.yellow(`[Cache] Failed to cache ${filePath}:`, error));
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Clear cache for a specific file
157
+ */
158
+ clear(filePath: string): void {
159
+ this.cache.delete(filePath);
160
+ }
161
+
162
+ /**
163
+ * Clear entire cache
164
+ */
165
+ clearAll(): void {
166
+ this.cache.clear();
167
+ console.log(chalk.gray("[Cache] Cleared all entries"));
168
+ }
169
+
170
+ /**
171
+ * Get cache statistics
172
+ */
173
+ getStats(): { size: number; maxSize: number } {
174
+ return {
175
+ size: this.cache.size,
176
+ maxSize: this.maxSize,
177
+ };
178
+ }
179
+ }
180
+
181
+ // Singleton instance
182
+ export const compilationCache = new CompilationCache();
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2024 Themba Mzumara
4
+ * SWITE - SWISS Development Server
5
+ * CLI tool to generate import maps
6
+ * Licensed under the MIT License.
7
+ */
8
+
9
+ import { generateImportMap, saveImportMap } from "./generate-import-map.js";
10
+ import { findWorkspaceRoot } from "../kernel/workspace.js";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ async function main() {
18
+ // Find app root (where swite is running from)
19
+ // This script is typically run from the app directory
20
+ const appRoot = process.cwd();
21
+ const workspaceRoot = await findWorkspaceRoot(appRoot);
22
+
23
+ console.log(`[ImportMap] App root: ${appRoot}`);
24
+ console.log(`[ImportMap] Workspace root: ${workspaceRoot || "none"}`);
25
+
26
+ // Generate import map
27
+ const importMap = await generateImportMap(appRoot, workspaceRoot);
28
+
29
+ // Save to .swite/import-map.json in app root
30
+ const outputPath = path.join(appRoot, ".swite", "import-map.json");
31
+ await saveImportMap(importMap, outputPath);
32
+
33
+ console.log(`[ImportMap] ✅ Import map generated successfully`);
34
+ process.exit(0);
35
+ }
36
+
37
+ main().catch((error) => {
38
+ console.error("[ImportMap] Error:", error);
39
+ process.exit(1);
40
+ });