@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,33 @@
1
+ /**
2
+ * Compilation cache with dependency tracking
3
+ * Invalidates when source file or dependencies change
4
+ */
5
+ export declare class CompilationCache {
6
+ private cache;
7
+ private readonly maxSize;
8
+ /**
9
+ * Get cached compilation result if valid
10
+ */
11
+ get(filePath: string, getDependencies: (compiled: string) => Promise<string[]>): Promise<string | null>;
12
+ /**
13
+ * Store compilation result in cache
14
+ */
15
+ set(filePath: string, compiled: string, rewritten: string, getDependencies: (compiled: string) => Promise<string[]>): Promise<void>;
16
+ /**
17
+ * Clear cache for a specific file
18
+ */
19
+ clear(filePath: string): void;
20
+ /**
21
+ * Clear entire cache
22
+ */
23
+ clearAll(): void;
24
+ /**
25
+ * Get cache statistics
26
+ */
27
+ getStats(): {
28
+ size: number;
29
+ maxSize: number;
30
+ };
31
+ }
32
+ export declare const compilationCache: CompilationCache;
33
+ //# sourceMappingURL=compilation-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compilation-cache.d.ts","sourceRoot":"","sources":["../../src/cache/compilation-cache.ts"],"names":[],"mappings":"AAmBA;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAEhC;;OAEG;IACG,GAAG,CACP,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GACvD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA+EzB;;OAEG;IACG,GAAG,CACP,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GACvD,OAAO,CAAC,IAAI,CAAC;IAiChB;;OAEG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI7B;;OAEG;IACH,QAAQ,IAAI,IAAI;IAKhB;;OAEG;IACH,QAAQ,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;CAM9C;AAGD,eAAO,MAAM,gBAAgB,kBAAyB,CAAC"}
@@ -0,0 +1,130 @@
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
+ import { promises as fs } from "node:fs";
8
+ import path from "node:path";
9
+ import chalk from "chalk";
10
+ /**
11
+ * Compilation cache with dependency tracking
12
+ * Invalidates when source file or dependencies change
13
+ */
14
+ export class CompilationCache {
15
+ constructor() {
16
+ this.cache = new Map();
17
+ this.maxSize = 1000; // Prevent memory leaks
18
+ }
19
+ /**
20
+ * Get cached compilation result if valid
21
+ */
22
+ async get(filePath, getDependencies) {
23
+ const entry = this.cache.get(filePath);
24
+ if (!entry) {
25
+ return null;
26
+ }
27
+ // Check if source file changed
28
+ try {
29
+ const stats = await fs.stat(filePath);
30
+ if (stats.mtimeMs !== entry.mtime) {
31
+ console.log(chalk.yellow(`[Cache] Invalidating ${filePath}: file modified`));
32
+ this.cache.delete(filePath);
33
+ return null;
34
+ }
35
+ }
36
+ catch (error) {
37
+ // File deleted or inaccessible
38
+ this.cache.delete(filePath);
39
+ return null;
40
+ }
41
+ // Check if dependencies changed
42
+ const currentDeps = await getDependencies(entry.compiled);
43
+ const depsChanged = currentDeps.length !== entry.dependencies.length ||
44
+ currentDeps.some((dep, i) => dep !== entry.dependencies[i]);
45
+ if (depsChanged) {
46
+ console.log(chalk.yellow(`[Cache] Invalidating ${filePath}: dependencies changed`));
47
+ this.cache.delete(filePath);
48
+ return null;
49
+ }
50
+ // Check if dependencies still exist and haven't changed
51
+ for (const dep of entry.dependencies) {
52
+ try {
53
+ const depStats = await fs.stat(dep);
54
+ // If dependency was modified after cache entry, invalidate
55
+ if (depStats.mtimeMs > entry.timestamp) {
56
+ console.log(chalk.yellow(`[Cache] Invalidating ${filePath}: dependency ${dep} modified`));
57
+ this.cache.delete(filePath);
58
+ return null;
59
+ }
60
+ }
61
+ catch {
62
+ // Dependency deleted or inaccessible
63
+ console.log(chalk.yellow(`[Cache] Invalidating ${filePath}: dependency ${dep} not found`));
64
+ this.cache.delete(filePath);
65
+ return null;
66
+ }
67
+ }
68
+ // Check if cached content has stale CDN URLs (from before import rewriter fix)
69
+ if (entry.rewritten.includes("cdn.jsdelivr.net") || entry.rewritten.includes("esm.sh")) {
70
+ console.log(chalk.yellow(`[Cache] Invalidating ${filePath}: contains stale CDN URLs`));
71
+ this.cache.delete(filePath);
72
+ return null;
73
+ }
74
+ console.log(chalk.green(`[Cache] ✅ Cache hit for ${filePath}`));
75
+ return entry.rewritten;
76
+ }
77
+ /**
78
+ * Store compilation result in cache
79
+ */
80
+ async set(filePath, compiled, rewritten, getDependencies) {
81
+ // Enforce max size (LRU eviction)
82
+ if (this.cache.size >= this.maxSize) {
83
+ // Remove oldest entry (simple FIFO)
84
+ const firstKey = this.cache.keys().next().value;
85
+ if (firstKey) {
86
+ this.cache.delete(firstKey);
87
+ console.log(chalk.gray(`[Cache] Evicted ${firstKey} (cache full)`));
88
+ }
89
+ }
90
+ try {
91
+ const stats = await fs.stat(filePath);
92
+ const dependencies = await getDependencies(compiled);
93
+ this.cache.set(filePath, {
94
+ compiled,
95
+ rewritten,
96
+ mtime: stats.mtimeMs,
97
+ dependencies,
98
+ timestamp: Date.now(),
99
+ });
100
+ console.log(chalk.green(`[Cache] ✅ Cached ${filePath} (${dependencies.length} deps)`));
101
+ }
102
+ catch (error) {
103
+ console.warn(chalk.yellow(`[Cache] Failed to cache ${filePath}:`, error));
104
+ }
105
+ }
106
+ /**
107
+ * Clear cache for a specific file
108
+ */
109
+ clear(filePath) {
110
+ this.cache.delete(filePath);
111
+ }
112
+ /**
113
+ * Clear entire cache
114
+ */
115
+ clearAll() {
116
+ this.cache.clear();
117
+ console.log(chalk.gray("[Cache] Cleared all entries"));
118
+ }
119
+ /**
120
+ * Get cache statistics
121
+ */
122
+ getStats() {
123
+ return {
124
+ size: this.cache.size,
125
+ maxSize: this.maxSize,
126
+ };
127
+ }
128
+ }
129
+ // Singleton instance
130
+ export const compilationCache = new CompilationCache();
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import chalk from "chalk";
4
+ import { SwiteServer } from "./server.js";
5
+ import { loadUserConfig } from "./config-loader.js";
6
+ import { startPythonDevService, stopPythonDevService, } from "./dev/pythonDevManager.js";
7
+ import { setProductionMode } from "./proxy/proxyToPython.js";
8
+ const [, , command, ...args] = process.argv;
9
+ const root = resolve(process.cwd());
10
+ async function dev() {
11
+ const config = await loadUserConfig(root);
12
+ const python = config.services?.python;
13
+ if (python?.autoStart) {
14
+ await startPythonDevService(python, root);
15
+ }
16
+ // Relay SIGINT: kill Python, then exit cleanly
17
+ process.on("SIGINT", () => {
18
+ stopPythonDevService();
19
+ process.exit(0);
20
+ });
21
+ // Ensure Python is killed if Node crashes
22
+ process.on("exit", () => {
23
+ stopPythonDevService();
24
+ });
25
+ const server = new SwiteServer({
26
+ root,
27
+ port: config.server?.port ?? 3000,
28
+ host: config.server?.host ?? "localhost",
29
+ publicDir: "public",
30
+ open: false,
31
+ });
32
+ await server.start();
33
+ }
34
+ async function start() {
35
+ const config = await loadUserConfig(root);
36
+ const python = config.services?.python;
37
+ setProductionMode();
38
+ if (python && !process.env["PYTHON_SERVICE_URL"]) {
39
+ console.warn(chalk.yellow("[swite] WARNING: services.python is configured but PYTHON_SERVICE_URL is not set.\n" +
40
+ " Proxy calls to Python will fail. Set PYTHON_SERVICE_URL to the running service URL."));
41
+ }
42
+ const server = new SwiteServer({
43
+ root,
44
+ port: config.server?.port ?? 3000,
45
+ host: config.server?.host ?? "localhost",
46
+ publicDir: "public",
47
+ open: false,
48
+ });
49
+ await server.start();
50
+ }
51
+ async function build() {
52
+ const { SwiteBuilder } = await import("./builder.js");
53
+ const config = await loadUserConfig(root);
54
+ const builder = new SwiteBuilder({
55
+ root,
56
+ entry: resolve(root, "src/index.ui"),
57
+ outDir: resolve(root, "dist"),
58
+ });
59
+ await builder.build();
60
+ }
61
+ switch (command) {
62
+ case "dev":
63
+ dev().catch((err) => {
64
+ console.error(chalk.red("[swite] fatal:"), err);
65
+ stopPythonDevService();
66
+ process.exit(1);
67
+ });
68
+ break;
69
+ case "start":
70
+ start().catch((err) => {
71
+ console.error(chalk.red("[swite] fatal:"), err);
72
+ process.exit(1);
73
+ });
74
+ break;
75
+ case "build":
76
+ build().catch((err) => {
77
+ console.error(chalk.red("[swite] build failed:"), err);
78
+ process.exit(1);
79
+ });
80
+ break;
81
+ default:
82
+ console.error(chalk.red(`[swite] unknown command: ${command ?? "(none)"}`));
83
+ console.error("Usage: swite <dev|build|start>");
84
+ process.exit(1);
85
+ }
@@ -0,0 +1,8 @@
1
+ import type { SwiteUserConfig } from "./config.js";
2
+ /**
3
+ * Load swiss.config.ts from the project root.
4
+ * Transpiles to a temp ESM file via esbuild, imports it, then cleans up.
5
+ * Returns empty config if no config file found.
6
+ */
7
+ export declare function loadUserConfig(root: string): Promise<SwiteUserConfig>;
8
+ //# sourceMappingURL=config-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../src/config-loader.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAgC3E"}
@@ -0,0 +1,40 @@
1
+ import { build } from "esbuild";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { existsSync } from "node:fs";
7
+ /**
8
+ * Load swiss.config.ts from the project root.
9
+ * Transpiles to a temp ESM file via esbuild, imports it, then cleans up.
10
+ * Returns empty config if no config file found.
11
+ */
12
+ export async function loadUserConfig(root) {
13
+ const tsConfig = join(root, "swiss.config.ts");
14
+ const jsConfig = join(root, "swiss.config.js");
15
+ const configPath = existsSync(tsConfig)
16
+ ? tsConfig
17
+ : existsSync(jsConfig)
18
+ ? jsConfig
19
+ : null;
20
+ if (!configPath) {
21
+ return {};
22
+ }
23
+ const tempDir = await mkdtemp(join(tmpdir(), "swite-config-"));
24
+ const outFile = join(tempDir, "swiss.config.mjs");
25
+ try {
26
+ await build({
27
+ entryPoints: [configPath],
28
+ bundle: false,
29
+ format: "esm",
30
+ outfile: outFile,
31
+ platform: "node",
32
+ logLevel: "silent",
33
+ });
34
+ const mod = await import(pathToFileURL(outFile).href);
35
+ return (mod.default ?? {});
36
+ }
37
+ finally {
38
+ await rm(tempDir, { recursive: true, force: true });
39
+ }
40
+ }
@@ -0,0 +1,29 @@
1
+ export interface PythonServiceConfig {
2
+ /** Path to the Python entry file, relative to project root */
3
+ entry: string;
4
+ /** Port the Python service listens on */
5
+ port: number;
6
+ /** Whether swite dev should spawn the Python process automatically */
7
+ autoStart: boolean;
8
+ /** Health check endpoint polled before Node server starts */
9
+ healthCheck: string;
10
+ /** Additional environment variables passed to the Python process */
11
+ env?: Record<string, string>;
12
+ }
13
+ export interface ServicesConfig {
14
+ python?: PythonServiceConfig;
15
+ }
16
+ export interface ServerConfig {
17
+ port?: number;
18
+ host?: string;
19
+ }
20
+ export interface SwiteUserConfig {
21
+ server?: ServerConfig;
22
+ services?: ServicesConfig;
23
+ }
24
+ /**
25
+ * Define swite configuration with full TypeScript validation.
26
+ * Unknown fields are rejected at compile time.
27
+ */
28
+ export declare function defineConfig(config: SwiteUserConfig): SwiteUserConfig;
29
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/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;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAErE"}
package/dist/config.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Define swite configuration with full TypeScript validation.
3
+ * Unknown fields are rejected at compile time.
4
+ */
5
+ export function defineConfig(config) {
6
+ return config;
7
+ }
@@ -0,0 +1,12 @@
1
+ import type { PythonServiceConfig } from "../config.js";
2
+ /**
3
+ * Spawn the Python service and wait until its health endpoint responds 200.
4
+ * Streams stdout/stderr line-buffered, prefixed with [python].
5
+ * Also calls initPythonProxy so proxyToPython works without PYTHON_SERVICE_URL.
6
+ */
7
+ export declare function startPythonDevService(config: PythonServiceConfig, projectRoot: string): Promise<void>;
8
+ /**
9
+ * Send SIGTERM to the Python child process if running.
10
+ */
11
+ export declare function stopPythonDevService(): void;
12
+ //# sourceMappingURL=pythonDevManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pythonDevManager.d.ts","sourceRoot":"","sources":["../../src/dev/pythonDevManager.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAQxD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,mBAAmB,EAC3B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAuCf;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAM3C"}
@@ -0,0 +1,85 @@
1
+ import { spawn } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import chalk from "chalk";
4
+ import { initPythonProxy } from "../proxy/proxyToPython.js";
5
+ const POLL_INTERVAL_MS = 500;
6
+ const HEALTH_TIMEOUT_MS = 15000;
7
+ const BACKOFF_THRESHOLD = 5;
8
+ let _child = null;
9
+ /**
10
+ * Spawn the Python service and wait until its health endpoint responds 200.
11
+ * Streams stdout/stderr line-buffered, prefixed with [python].
12
+ * Also calls initPythonProxy so proxyToPython works without PYTHON_SERVICE_URL.
13
+ */
14
+ export async function startPythonDevService(config, projectRoot) {
15
+ const entryPath = resolve(projectRoot, config.entry);
16
+ const healthUrl = `http://localhost:${config.port}${config.healthCheck}`;
17
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
18
+ console.log(chalk.blue(`[python] spawning: ${pythonCmd} ${config.entry} (port ${config.port})`));
19
+ const env = {
20
+ ...process.env,
21
+ ...config.env,
22
+ PORT: String(config.port),
23
+ };
24
+ _child = spawn(pythonCmd, [entryPath], {
25
+ env,
26
+ stdio: ["ignore", "pipe", "pipe"],
27
+ });
28
+ pipeLines(_child.stdout, chalk.cyan("[python] "));
29
+ pipeLines(_child.stderr, chalk.yellow("[python] "));
30
+ _child.on("exit", (code) => {
31
+ if (code !== null && code !== 0) {
32
+ console.error(chalk.red(`\n[python] process exited with code ${code} — Node server continuing in degraded mode\n`));
33
+ }
34
+ _child = null;
35
+ });
36
+ initPythonProxy(config);
37
+ await pollHealth(healthUrl);
38
+ console.log(chalk.green(`[python] healthy — ${healthUrl}`));
39
+ }
40
+ /**
41
+ * Send SIGTERM to the Python child process if running.
42
+ */
43
+ export function stopPythonDevService() {
44
+ if (_child) {
45
+ console.log(chalk.gray("[python] shutting down..."));
46
+ _child.kill("SIGTERM");
47
+ _child = null;
48
+ }
49
+ }
50
+ // ── internals ────────────────────────────────────────────────────────────────
51
+ function pipeLines(stream, prefix) {
52
+ if (!stream)
53
+ return;
54
+ let buffer = "";
55
+ stream.on("data", (chunk) => {
56
+ buffer += chunk.toString();
57
+ const lines = buffer.split("\n");
58
+ buffer = lines.pop() ?? "";
59
+ for (const line of lines) {
60
+ if (line.trim())
61
+ process.stdout.write(prefix + line + "\n");
62
+ }
63
+ });
64
+ }
65
+ async function pollHealth(url) {
66
+ const deadline = Date.now() + HEALTH_TIMEOUT_MS;
67
+ let attempt = 0;
68
+ while (Date.now() < deadline) {
69
+ try {
70
+ const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
71
+ if (res.ok)
72
+ return;
73
+ }
74
+ catch {
75
+ // not ready yet
76
+ }
77
+ attempt++;
78
+ const delay = attempt <= BACKOFF_THRESHOLD
79
+ ? POLL_INTERVAL_MS
80
+ : Math.min(POLL_INTERVAL_MS * Math.pow(2, attempt - BACKOFF_THRESHOLD), 3000);
81
+ await new Promise((resolve) => setTimeout(resolve, delay));
82
+ }
83
+ stopPythonDevService();
84
+ throw new Error(`[python] health check timed out after ${HEALTH_TIMEOUT_MS}ms — is ${url} reachable?`);
85
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface EnvConfig {
2
+ mode?: "development" | "production";
3
+ prefix?: string;
4
+ }
5
+ /**
6
+ * Load environment variables from .env files
7
+ * Supports .env, .env.local, .env.[mode], .env.[mode].local
8
+ */
9
+ export declare function loadEnv(root: string, mode?: string): Record<string, string>;
10
+ /**
11
+ * Generate import.meta.env replacement code
12
+ * Injects environment variables as a module that can be imported
13
+ */
14
+ export declare function generateEnvModule(env: Record<string, string>, prefix?: string): string;
15
+ /**
16
+ * Inject import.meta.env polyfill into code
17
+ */
18
+ export declare function injectEnvPolyfill(code: string): string;
19
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,OAAO,CACrB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAsB,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBxB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,MAAM,GAAE,MAAiB,GACxB,MAAM,CAwCR;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqCtD"}
package/dist/env.js ADDED
@@ -0,0 +1,112 @@
1
+ /*
2
+ * Environment Variable Support for SWITE
3
+ * Provides import.meta.env replacement for SWITE
4
+ */
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ /**
8
+ * Load environment variables from .env files
9
+ * Supports .env, .env.local, .env.[mode], .env.[mode].local
10
+ */
11
+ export function loadEnv(root, mode = "development") {
12
+ const env = {};
13
+ const envFiles = [`.env.${mode}.local`, `.env.${mode}`, `.env.local`, `.env`];
14
+ for (const file of envFiles) {
15
+ const envPath = join(root, file);
16
+ if (existsSync(envPath)) {
17
+ const content = readFileSync(envPath, "utf-8");
18
+ for (const line of content.split("\n")) {
19
+ const trimmed = line.trim();
20
+ if (trimmed && !trimmed.startsWith("#")) {
21
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
22
+ if (match) {
23
+ const [, key, value] = match;
24
+ // Remove quotes if present
25
+ const cleanValue = value.replace(/^["']|["']$/g, "");
26
+ env[key.trim()] = cleanValue;
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ return env;
33
+ }
34
+ /**
35
+ * Generate import.meta.env replacement code
36
+ * Injects environment variables as a module that can be imported
37
+ */
38
+ export function generateEnvModule(env, prefix = "SWITE_") {
39
+ // Filter env vars by prefix and expose them
40
+ const switeEnv = {};
41
+ const publicEnv = {};
42
+ for (const [key, value] of Object.entries(env)) {
43
+ if (key.startsWith(prefix)) {
44
+ // Remove prefix for SWITE_ prefixed vars
45
+ const cleanKey = key.slice(prefix.length);
46
+ switeEnv[cleanKey] = value;
47
+ publicEnv[key] = value;
48
+ }
49
+ else if (key.startsWith("PUBLIC_")) {
50
+ // PUBLIC_ vars are always exposed
51
+ publicEnv[key] = value;
52
+ }
53
+ }
54
+ // Build env object with all variables
55
+ const allEnvEntries = [
56
+ ...Object.entries(switeEnv).map(([key, value]) => [key, value]),
57
+ ...Object.entries(publicEnv).map(([key, value]) => [key, value]),
58
+ ["MODE", process.env.NODE_ENV || "development"],
59
+ ["DEV", process.env.NODE_ENV !== "production"],
60
+ ["PROD", process.env.NODE_ENV === "production"],
61
+ ];
62
+ return `
63
+ // SWITE Environment Variables
64
+ // Generated at runtime - do not edit manually
65
+ export const env = {
66
+ ${allEnvEntries
67
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
68
+ .join(",\n ")},
69
+ };
70
+
71
+ // For import.meta.env compatibility
72
+ if (typeof globalThis !== 'undefined') {
73
+ globalThis.__swite_env__ = env;
74
+ }
75
+ `;
76
+ }
77
+ /**
78
+ * Inject import.meta.env polyfill into code
79
+ */
80
+ export function injectEnvPolyfill(code) {
81
+ // Check if import.meta.env is used
82
+ if (!code.includes("import.meta.env") && !code.includes("__swite_env__")) {
83
+ return code;
84
+ }
85
+ // Inject polyfill at the top - load env module first
86
+ const polyfill = `
87
+ // SWITE import.meta.env polyfill
88
+ import { env as switeEnv } from '/__swite_env';
89
+ if (typeof globalThis !== 'undefined') {
90
+ globalThis.__swite_env__ = switeEnv;
91
+ }
92
+ if (typeof import !== 'undefined' && import.meta) {
93
+ import.meta.env = switeEnv;
94
+ } else if (typeof globalThis !== 'undefined') {
95
+ // Fallback for environments without import.meta
96
+ if (!globalThis.import) {
97
+ globalThis.import = { meta: {} };
98
+ }
99
+ if (!globalThis.import.meta) {
100
+ globalThis.import.meta = {};
101
+ }
102
+ globalThis.import.meta.env = switeEnv;
103
+ }
104
+ `;
105
+ // Find the first import statement or start of file
106
+ const firstImport = code.match(/^import\s+/m);
107
+ if (firstImport) {
108
+ const insertIndex = firstImport.index;
109
+ return (code.slice(0, insertIndex) + polyfill + "\n" + code.slice(insertIndex));
110
+ }
111
+ return polyfill + "\n" + code;
112
+ }
@@ -0,0 +1,21 @@
1
+ import type { Response } from "express";
2
+ import { ModuleResolver } from "../resolver.js";
3
+ export interface HandlerContext {
4
+ resolver: ModuleResolver;
5
+ root: string;
6
+ workspaceRoot: string | null;
7
+ }
8
+ /**
9
+ * Set cache-busting headers for development
10
+ */
11
+ export declare function setDevHeaders(res: Response): void;
12
+ /**
13
+ * Base handler utilities
14
+ */
15
+ export declare class BaseHandler {
16
+ protected context: HandlerContext;
17
+ constructor(context: HandlerContext);
18
+ protected resolveFilePath(url: string): Promise<string>;
19
+ protected fileExists(filePath: string): Promise<boolean>;
20
+ }
21
+ //# sourceMappingURL=base-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/base-handler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGhD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,cAAc,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAMjD;AAED;;GAEG;AACH,qBAAa,WAAW;IACV,SAAS,CAAC,OAAO,EAAE,cAAc;gBAAvB,OAAO,EAAE,cAAc;cAE7B,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;cAI7C,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAQ/D"}
@@ -0,0 +1,38 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+ import { promises as fs } from "node:fs";
7
+ import { ModuleResolver } from "../resolver.js";
8
+ import { resolveFilePath } from "../utils/file-path-resolver.js";
9
+ /**
10
+ * Set cache-busting headers for development
11
+ */
12
+ export function setDevHeaders(res) {
13
+ // Prevent all caching during development
14
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
15
+ res.setHeader("Pragma", "no-cache");
16
+ res.setHeader("Expires", "0");
17
+ res.setHeader("Surrogate-Control", "no-store");
18
+ }
19
+ /**
20
+ * Base handler utilities
21
+ */
22
+ export class BaseHandler {
23
+ constructor(context) {
24
+ this.context = context;
25
+ }
26
+ async resolveFilePath(url) {
27
+ return resolveFilePath(url, this.context.root, this.context.workspaceRoot);
28
+ }
29
+ async fileExists(filePath) {
30
+ try {
31
+ await fs.access(filePath);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,10 @@
1
+ import type { Response } from "express";
2
+ import { BaseHandler, type HandlerContext } from "./base-handler.js";
3
+ export declare class JSHandler extends BaseHandler {
4
+ private uiHandler;
5
+ private uixHandler;
6
+ private tsHandler;
7
+ constructor(context: HandlerContext);
8
+ handle(url: string, res: Response): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=js-handler.d.ts.map