@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.
- package/.changeset/config.json +11 -0
- package/.github/workflows/ci.yml +59 -0
- package/.github/workflows/publish.yml +50 -0
- package/.github/workflows/release.yml +53 -0
- package/BUILD_ANALYSIS.md +89 -0
- package/BUILD_STRATEGY.md +75 -0
- package/CHANGELOG.md +53 -0
- package/DIRECTIVE.md +488 -0
- package/__tests__/css-extraction.test.ts +261 -0
- package/__tests__/css-injection-integration.test.ts +247 -0
- package/__tests__/css-middleware.test.ts +191 -0
- package/__tests__/import-rewriter-bug.test.ts +135 -0
- package/dist/builder.d.ts +36 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +772 -0
- package/dist/cache/compilation-cache.d.ts +33 -0
- package/dist/cache/compilation-cache.d.ts.map +1 -0
- package/dist/cache/compilation-cache.js +130 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +85 -0
- package/dist/config-loader.d.ts +8 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +40 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +7 -0
- package/dist/dev/pythonDevManager.d.ts +12 -0
- package/dist/dev/pythonDevManager.d.ts.map +1 -0
- package/dist/dev/pythonDevManager.js +85 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +112 -0
- package/dist/handlers/base-handler.d.ts +21 -0
- package/dist/handlers/base-handler.d.ts.map +1 -0
- package/dist/handlers/base-handler.js +38 -0
- package/dist/handlers/js-handler.d.ts +10 -0
- package/dist/handlers/js-handler.d.ts.map +1 -0
- package/dist/handlers/js-handler.js +87 -0
- package/dist/handlers/mjs-handler.d.ts +8 -0
- package/dist/handlers/mjs-handler.d.ts.map +1 -0
- package/dist/handlers/mjs-handler.js +44 -0
- package/dist/handlers/node-module-handler.d.ts +16 -0
- package/dist/handlers/node-module-handler.d.ts.map +1 -0
- package/dist/handlers/node-module-handler.js +267 -0
- package/dist/handlers/ts-handler.d.ts +11 -0
- package/dist/handlers/ts-handler.d.ts.map +1 -0
- package/dist/handlers/ts-handler.js +120 -0
- package/dist/handlers/ui-handler.d.ts +12 -0
- package/dist/handlers/ui-handler.d.ts.map +1 -0
- package/dist/handlers/ui-handler.js +182 -0
- package/dist/handlers/uix-handler.d.ts +12 -0
- package/dist/handlers/uix-handler.d.ts.map +1 -0
- package/dist/handlers/uix-handler.js +135 -0
- package/dist/hmr.d.ts +20 -0
- package/dist/hmr.d.ts.map +1 -0
- package/dist/hmr.js +265 -0
- package/dist/import-rewriter.d.ts +3 -0
- package/dist/import-rewriter.d.ts.map +1 -0
- package/dist/import-rewriter.js +351 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/middleware/hmr-routes.d.ts +12 -0
- package/dist/middleware/hmr-routes.d.ts.map +1 -0
- package/dist/middleware/hmr-routes.js +97 -0
- package/dist/middleware/middleware-setup.d.ts +23 -0
- package/dist/middleware/middleware-setup.d.ts.map +1 -0
- package/dist/middleware/middleware-setup.js +596 -0
- package/dist/middleware/static-files.d.ts +15 -0
- package/dist/middleware/static-files.d.ts.map +1 -0
- package/dist/middleware/static-files.js +585 -0
- package/dist/proxy/SwiteProxyError.d.ts +6 -0
- package/dist/proxy/SwiteProxyError.d.ts.map +1 -0
- package/dist/proxy/SwiteProxyError.js +9 -0
- package/dist/proxy/proxyToPython.d.ts +28 -0
- package/dist/proxy/proxyToPython.d.ts.map +1 -0
- package/dist/proxy/proxyToPython.js +66 -0
- package/dist/resolver/bare-import-resolver.d.ts +9 -0
- package/dist/resolver/bare-import-resolver.d.ts.map +1 -0
- package/dist/resolver/bare-import-resolver.js +363 -0
- package/dist/resolver/symlink-registry.d.ts +13 -0
- package/dist/resolver/symlink-registry.d.ts.map +1 -0
- package/dist/resolver/symlink-registry.js +98 -0
- package/dist/resolver/url-resolver.d.ts +11 -0
- package/dist/resolver/url-resolver.d.ts.map +1 -0
- package/dist/resolver/url-resolver.js +268 -0
- package/dist/resolver/workspace-package-resolver.d.ts +10 -0
- package/dist/resolver/workspace-package-resolver.d.ts.map +1 -0
- package/dist/resolver/workspace-package-resolver.js +185 -0
- package/dist/resolver.d.ts +17 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +191 -0
- package/dist/router/file-router.d.ts +19 -0
- package/dist/router/file-router.d.ts.map +1 -0
- package/dist/router/file-router.js +114 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +122 -0
- package/dist/utils/cdn-fallback.d.ts +14 -0
- package/dist/utils/cdn-fallback.d.ts.map +1 -0
- package/dist/utils/cdn-fallback.js +36 -0
- package/dist/utils/file-path-resolver.d.ts +9 -0
- package/dist/utils/file-path-resolver.d.ts.map +1 -0
- package/dist/utils/file-path-resolver.js +187 -0
- package/dist/utils/generate-import-map-cli.d.ts +3 -0
- package/dist/utils/generate-import-map-cli.d.ts.map +1 -0
- package/dist/utils/generate-import-map-cli.js +32 -0
- package/dist/utils/generate-import-map.d.ts +21 -0
- package/dist/utils/generate-import-map.d.ts.map +1 -0
- package/dist/utils/generate-import-map.js +119 -0
- package/dist/utils/package-finder.d.ts +24 -0
- package/dist/utils/package-finder.d.ts.map +1 -0
- package/dist/utils/package-finder.js +161 -0
- package/dist/utils/package-registry.d.ts +36 -0
- package/dist/utils/package-registry.d.ts.map +1 -0
- package/dist/utils/package-registry.js +159 -0
- package/dist/utils/workspace.d.ts +6 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +65 -0
- package/docs/IMPORT_REWRITING.md +164 -0
- package/docs/IMPORT_REWRITING_TROUBLESHOOTING.md +139 -0
- package/docs/PATH_RESOLUTION_GUIDE.md +221 -0
- package/package.json +49 -0
- package/src/adapters/proxy/SwiteProxyError.ts +12 -0
- package/src/adapters/proxy/proxyToPython.ts +88 -0
- package/src/build-engine/builder.ts +960 -0
- package/src/cli.ts +109 -0
- package/src/config/config-loader.ts +46 -0
- package/src/config/config.ts +34 -0
- package/src/config/env.ts +98 -0
- package/src/dev-engine/handlers/base-handler.ts +68 -0
- package/src/dev-engine/handlers/js-handler.ts +134 -0
- package/src/dev-engine/handlers/mjs-handler.ts +65 -0
- package/src/dev-engine/handlers/node-module-handler.ts +339 -0
- package/src/dev-engine/handlers/ts-handler.ts +143 -0
- package/src/dev-engine/handlers/ui-handler.ts +105 -0
- package/src/dev-engine/handlers/uix-handler.ts +90 -0
- package/src/dev-engine/hmr/hmr-client-template.ts +122 -0
- package/src/dev-engine/hmr/hmr.ts +173 -0
- package/src/dev-engine/middleware/hmr-routes.ts +120 -0
- package/src/dev-engine/middleware/middleware-setup.ts +351 -0
- package/src/dev-engine/middleware/static-files.ts +728 -0
- package/src/dev-engine/pythonDevManager.ts +116 -0
- package/src/dev-engine/router/file-router.ts +164 -0
- package/src/dev-engine/server.ts +152 -0
- package/src/index.ts +26 -0
- package/src/internal/cache/compilation-cache.ts +182 -0
- package/src/internal/generate-import-map-cli.ts +40 -0
- package/src/internal/generate-import-map.ts +154 -0
- package/src/kernel/package-finder.ts +164 -0
- package/src/kernel/package-registry.ts +198 -0
- package/src/kernel/workspace.ts +62 -0
- package/src/resolution/bare-import-resolver.ts +400 -0
- package/src/resolution/cdn/cdn-fallback.ts +37 -0
- package/src/resolution/path/file-path-resolver.ts +190 -0
- package/src/resolution/path/path-fixup.ts +19 -0
- package/src/resolution/resolver.ts +198 -0
- package/src/resolution/rewriting/import-rewriter.ts +237 -0
- package/src/resolution/symlink-registry.ts +114 -0
- package/src/resolution/url-resolver.ts +231 -0
- package/src/resolution/workspace-package-resolver.ts +94 -0
- 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
|
+
});
|