@swissjs/swite 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/__tests__/import-rewriter-bug.test.ts +122 -122
  3. package/__tests__/security-r001-r002.test.ts +190 -190
  4. package/dist/cli.js +0 -0
  5. package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
  6. package/dist/dev-engine/middleware/middleware-setup.js +4 -3
  7. package/docs/architecture/build-pipeline.md +97 -97
  8. package/docs/architecture/dev-server.md +87 -87
  9. package/docs/architecture/hmr.md +78 -78
  10. package/docs/architecture/import-rewriting.md +101 -101
  11. package/docs/architecture/index.md +16 -16
  12. package/docs/architecture/python-integration.md +93 -93
  13. package/docs/architecture/resolution.md +92 -92
  14. package/docs/cli/build.md +78 -78
  15. package/docs/cli/dev.md +90 -90
  16. package/docs/cli/index.md +15 -15
  17. package/docs/cli/start.md +45 -45
  18. package/docs/development/contributing.md +74 -74
  19. package/docs/development/index.md +12 -12
  20. package/docs/development/internals.md +101 -101
  21. package/docs/guide/configuration.md +89 -89
  22. package/docs/guide/index.md +13 -13
  23. package/docs/guide/project-structure.md +75 -75
  24. package/docs/guide/quickstart.md +113 -113
  25. package/docs/index.md +16 -16
  26. package/package.json +10 -9
  27. package/src/config/env.ts +98 -98
  28. package/src/dev-engine/handlers/ui-handler.ts +30 -30
  29. package/src/dev-engine/handlers/uix-handler.ts +21 -21
  30. package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
  31. package/src/dev-engine/middleware/middleware-setup.ts +354 -354
  32. package/src/dev-engine/middleware/static-files.ts +813 -813
  33. package/src/resolution/cdn/cdn-fallback.ts +40 -40
  34. package/src/resolution/path/path-fixup.ts +27 -27
  35. package/src/resolution/rewriting/import-rewriter.ts +237 -237
  36. package/src/resolution/symlink-registry.ts +114 -114
package/dist/cli.js CHANGED
File without changes
@@ -7,116 +7,116 @@
7
7
  * template-literal escaping issues.
8
8
  */
9
9
  export function buildHmrClientScript(port) {
10
- return `// SWITE HMR Client
11
- console.log('[SWITE] HMR enabled');
12
-
13
- const socket = new WebSocket('ws://' + window.location.hostname + ':${port}');
14
- const moduleGraph = new Map();
15
- const hotModules = new Map();
16
-
17
- socket.addEventListener('open', () => {
18
- console.log('[SWITE] HMR connected');
19
- });
20
-
21
- socket.addEventListener('message', async (event) => {
22
- const data = JSON.parse(event.data);
23
-
24
- if (data.type === 'update') {
25
- console.log('[SWITE] Processing update:', data.path, 'Type:', data.updateType);
26
-
27
- if (data.updateType === 'style') {
28
- updateStyles();
29
- console.log('[SWITE] Styles hot updated');
30
- } else if (data.updateType === 'hot') {
31
- const moduleName = extractModuleName(data.path);
32
-
33
- if (moduleName && hotModules.has(moduleName)) {
34
- try {
35
- invalidateModule(moduleName);
36
- invalidateDependents(moduleName);
37
-
38
- const updatedModule = await import(data.path + '?t=' + Date.now());
39
- hotModules.set(moduleName, updatedModule);
40
-
41
- updateComponent(moduleName, updatedModule);
42
- console.log('[SWITE] Component hot updated:', moduleName);
43
- } catch (error) {
44
- console.error('[SWITE] Hot update failed:', error);
45
- window.location.reload();
46
- }
47
- } else {
48
- console.log('[SWITE] New component detected, reloading page');
49
- window.location.reload();
50
- }
51
- } else {
52
- console.log('[SWITE] Full page reload required');
53
- window.location.reload();
54
- }
55
- }
56
- });
57
-
58
- function updateStyles() {
59
- const links = document.querySelectorAll('link[rel="stylesheet"]');
60
- links.forEach(link => {
61
- const href = link.getAttribute('href');
62
- if (href) {
63
- const base = href.replace(/[?&]t=\\d+/, '');
64
- link.setAttribute('href', base + (base.includes('?') ? '&' : '?') + 't=' + Date.now());
65
- }
66
- });
67
- }
68
-
69
- function extractModuleName(filePath) {
70
- const parts = filePath.split('/');
71
- const fileName = parts[parts.length - 1];
72
- return fileName ? fileName.replace(/\\.[^.]+$/, '') : null;
73
- }
74
-
75
- function invalidateModule(moduleName) {
76
- if (window.__swiss_modules__) {
77
- delete window.__swiss_modules__[moduleName];
78
- }
79
- }
80
-
81
- function invalidateDependents(moduleName) {
82
- const dependents = moduleGraph.get(moduleName);
83
- if (dependents) {
84
- for (const dependent of dependents) {
85
- invalidateModule(dependent);
86
- }
87
- }
88
- }
89
-
90
- function updateComponent(moduleName, newModule) {
91
- if (window.__swiss_instances__) {
92
- const instances = window.__swiss_instances__[moduleName];
93
- if (instances && Array.isArray(instances)) {
94
- instances.forEach(instance => {
95
- if (instance && typeof instance.update === 'function') {
96
- instance.update(newModule.default || newModule);
97
- }
98
- });
99
- }
100
- }
101
- }
102
-
103
- socket.addEventListener('close', () => {
104
- console.log('[SWITE] HMR disconnected');
105
- });
106
-
107
- socket.addEventListener('error', (error) => {
108
- console.error('[SWITE] HMR error:', error);
109
- });
110
-
111
- window.__swiss_modules__ = window.__swiss_modules__ || {};
112
- window.__swiss_instances__ = window.__swiss_instances__ || {};
113
-
114
- const currentScript = document.currentScript;
115
- if (currentScript && currentScript.src) {
116
- const moduleName = extractModuleName(currentScript.src);
117
- if (moduleName) {
118
- window.__swiss_modules__[moduleName] = true;
119
- }
120
- }
10
+ return `// SWITE HMR Client
11
+ console.log('[SWITE] HMR enabled');
12
+
13
+ const socket = new WebSocket('ws://' + window.location.hostname + ':${port}');
14
+ const moduleGraph = new Map();
15
+ const hotModules = new Map();
16
+
17
+ socket.addEventListener('open', () => {
18
+ console.log('[SWITE] HMR connected');
19
+ });
20
+
21
+ socket.addEventListener('message', async (event) => {
22
+ const data = JSON.parse(event.data);
23
+
24
+ if (data.type === 'update') {
25
+ console.log('[SWITE] Processing update:', data.path, 'Type:', data.updateType);
26
+
27
+ if (data.updateType === 'style') {
28
+ updateStyles();
29
+ console.log('[SWITE] Styles hot updated');
30
+ } else if (data.updateType === 'hot') {
31
+ const moduleName = extractModuleName(data.path);
32
+
33
+ if (moduleName && hotModules.has(moduleName)) {
34
+ try {
35
+ invalidateModule(moduleName);
36
+ invalidateDependents(moduleName);
37
+
38
+ const updatedModule = await import(data.path + '?t=' + Date.now());
39
+ hotModules.set(moduleName, updatedModule);
40
+
41
+ updateComponent(moduleName, updatedModule);
42
+ console.log('[SWITE] Component hot updated:', moduleName);
43
+ } catch (error) {
44
+ console.error('[SWITE] Hot update failed:', error);
45
+ window.location.reload();
46
+ }
47
+ } else {
48
+ console.log('[SWITE] New component detected, reloading page');
49
+ window.location.reload();
50
+ }
51
+ } else {
52
+ console.log('[SWITE] Full page reload required');
53
+ window.location.reload();
54
+ }
55
+ }
56
+ });
57
+
58
+ function updateStyles() {
59
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
60
+ links.forEach(link => {
61
+ const href = link.getAttribute('href');
62
+ if (href) {
63
+ const base = href.replace(/[?&]t=\\d+/, '');
64
+ link.setAttribute('href', base + (base.includes('?') ? '&' : '?') + 't=' + Date.now());
65
+ }
66
+ });
67
+ }
68
+
69
+ function extractModuleName(filePath) {
70
+ const parts = filePath.split('/');
71
+ const fileName = parts[parts.length - 1];
72
+ return fileName ? fileName.replace(/\\.[^.]+$/, '') : null;
73
+ }
74
+
75
+ function invalidateModule(moduleName) {
76
+ if (window.__swiss_modules__) {
77
+ delete window.__swiss_modules__[moduleName];
78
+ }
79
+ }
80
+
81
+ function invalidateDependents(moduleName) {
82
+ const dependents = moduleGraph.get(moduleName);
83
+ if (dependents) {
84
+ for (const dependent of dependents) {
85
+ invalidateModule(dependent);
86
+ }
87
+ }
88
+ }
89
+
90
+ function updateComponent(moduleName, newModule) {
91
+ if (window.__swiss_instances__) {
92
+ const instances = window.__swiss_instances__[moduleName];
93
+ if (instances && Array.isArray(instances)) {
94
+ instances.forEach(instance => {
95
+ if (instance && typeof instance.update === 'function') {
96
+ instance.update(newModule.default || newModule);
97
+ }
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ socket.addEventListener('close', () => {
104
+ console.log('[SWITE] HMR disconnected');
105
+ });
106
+
107
+ socket.addEventListener('error', (error) => {
108
+ console.error('[SWITE] HMR error:', error);
109
+ });
110
+
111
+ window.__swiss_modules__ = window.__swiss_modules__ || {};
112
+ window.__swiss_instances__ = window.__swiss_instances__ || {};
113
+
114
+ const currentScript = document.currentScript;
115
+ if (currentScript && currentScript.src) {
116
+ const moduleName = extractModuleName(currentScript.src);
117
+ if (moduleName) {
118
+ window.__swiss_modules__[moduleName] = true;
119
+ }
120
+ }
121
121
  `;
122
122
  }
@@ -24,9 +24,10 @@ import { loadImportMap } from "../../internal/generate-import-map.js";
24
24
  import { loadEnv } from "../../config/env.js";
25
25
  const SOURCE_EXTS = new Set([".ui", ".uix", ".ts", ".mjs"]);
26
26
  function isFileNotFoundError(error) {
27
- return (error instanceof Error &&
28
- (("code" in error && error.code === "ENOENT") ||
29
- ("errno" in error && error.errno === -4058)));
27
+ if (!(error instanceof Error))
28
+ return false;
29
+ const sysError = error;
30
+ return sysError.code === "ENOENT" || sysError.errno === -4058;
30
31
  }
31
32
  function sendSourceError(res, error, fullPath) {
32
33
  if (res.headersSent)
@@ -1,97 +1,97 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # Build Pipeline
8
-
9
- `SwiteBuilder` (`src/build-engine/builder.ts`) handles production builds. It is invoked by `swite build` with a fixed entry point of `src/index.ui` and output directory of `dist/`.
10
-
11
- ---
12
-
13
- ## Overview
14
-
15
- The build runs three sequential phases inside a try/finally block. The temporary directory `.swite-build/` is always removed when the build finishes, whether it succeeded or failed.
16
-
17
- ```
18
- swite build
19
- └── SwiteBuilder.build()
20
- ├── 1. cleanOutputDir() — rm -rf dist/, mkdir dist/
21
- ├── 2. compileSwissFiles() — @swissjs/compiler → .tsx in .swite-build/
22
- ├── 3. bundle() — esbuild bundles from .swite-build/
23
- └── 4. copyPublicAssets() — cp -r public/ dist/
24
- ```
25
-
26
- ---
27
-
28
- ## Phase 1: Compile Swiss files
29
-
30
- `compileSwissFiles` traverses `src/` and every workspace dependency's `src/` directory.
31
-
32
- For each `.ui` or `.uix` file:
33
-
34
- 1. `UiCompiler.compileAsync(source, filePath)` — transforms SwissJS component syntax to TypeScript/JSX.
35
- 2. Relative `.ui`/`.uix` imports in the compiled output are rewritten to `.tsx` (esbuild needs `.tsx` for JSX, not `.ui`).
36
- 3. If the compiled output ends with `export { Foo }`, a `export default Foo` line is appended so default imports resolve at bundle time.
37
- 4. Written to `.swite-build/<relative-path>.tsx`.
38
-
39
- For each `.ts` file, `.ui`/`.uix` imports in `from` clauses are rewritten to `.tsx`, then the file is copied as-is.
40
-
41
- CSS files are copied verbatim so that any CSS import stubs in the bundle phase can resolve.
42
-
43
- ---
44
-
45
- ## Phase 2: Bundle with esbuild
46
-
47
- esbuild is invoked with:
48
-
49
- ```typescript
50
- {
51
- bundle: true,
52
- format: 'esm',
53
- target: 'es2020',
54
- minify: true,
55
- sourcemap: false,
56
- splitting: true, // ESM code splitting
57
- metafile: true,
58
- platform: 'node',
59
- }
60
- ```
61
-
62
- Three plugins are registered (see [CLI / build](../cli/build.md) for descriptions). Node built-in modules are marked external. The `absWorkingDir` is set to the workspace root (or app root if no workspace is detected) so esbuild resolves node_modules correctly.
63
-
64
- ---
65
-
66
- ## Phase 3: Copy public assets
67
-
68
- `public/` is copied recursively to `dist/`. If `public/` does not exist, this phase is skipped silently.
69
-
70
- ---
71
-
72
- ## Workspace dependency discovery
73
-
74
- `discoverWorkspaceDependencies()` reads the app's `package.json` and collects all `workspace:*` entries from `dependencies`, `devDependencies`, and `peerDependencies`. It also scans source files for `@scope/pkg` import patterns to catch transitive workspace imports not listed in `package.json`.
75
-
76
- For each candidate package name, it searches these directories under the workspace root:
77
-
78
- ```
79
- lib/<pkgName>
80
- packages/<pkgName>
81
- packages/runtime/<pkgName>
82
- packages/plugins/<pkgName>
83
- packages/domain/<pkgName>
84
- ```
85
-
86
- A match requires the directory to have a `package.json` whose `name` field matches the expected package name, and a `src/` subdirectory.
87
-
88
- ---
89
-
90
- ## Workspace resolver plugin
91
-
92
- The `workspace-resolver` esbuild plugin handles `@scope/pkg` imports during bundling. It maps each import to the compiled `.tsx` files in `.swite-build/` by:
93
-
94
- 1. Finding the matching workspace dependency from the discovery step.
95
- 2. Reading the package's `package.json` exports field to resolve the subpath.
96
- 3. Converting the export path from `./src/Foo.uix` to `.swite-build/<depRelPath>/src/Foo.tsx`.
97
- 4. Falling back to `src/index.js` if exports resolution fails.
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # Build Pipeline
8
+
9
+ `SwiteBuilder` (`src/build-engine/builder.ts`) handles production builds. It is invoked by `swite build` with a fixed entry point of `src/index.ui` and output directory of `dist/`.
10
+
11
+ ---
12
+
13
+ ## Overview
14
+
15
+ The build runs three sequential phases inside a try/finally block. The temporary directory `.swite-build/` is always removed when the build finishes, whether it succeeded or failed.
16
+
17
+ ```
18
+ swite build
19
+ └── SwiteBuilder.build()
20
+ ├── 1. cleanOutputDir() — rm -rf dist/, mkdir dist/
21
+ ├── 2. compileSwissFiles() — @swissjs/compiler → .tsx in .swite-build/
22
+ ├── 3. bundle() — esbuild bundles from .swite-build/
23
+ └── 4. copyPublicAssets() — cp -r public/ dist/
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Phase 1: Compile Swiss files
29
+
30
+ `compileSwissFiles` traverses `src/` and every workspace dependency's `src/` directory.
31
+
32
+ For each `.ui` or `.uix` file:
33
+
34
+ 1. `UiCompiler.compileAsync(source, filePath)` — transforms SwissJS component syntax to TypeScript/JSX.
35
+ 2. Relative `.ui`/`.uix` imports in the compiled output are rewritten to `.tsx` (esbuild needs `.tsx` for JSX, not `.ui`).
36
+ 3. If the compiled output ends with `export { Foo }`, a `export default Foo` line is appended so default imports resolve at bundle time.
37
+ 4. Written to `.swite-build/<relative-path>.tsx`.
38
+
39
+ For each `.ts` file, `.ui`/`.uix` imports in `from` clauses are rewritten to `.tsx`, then the file is copied as-is.
40
+
41
+ CSS files are copied verbatim so that any CSS import stubs in the bundle phase can resolve.
42
+
43
+ ---
44
+
45
+ ## Phase 2: Bundle with esbuild
46
+
47
+ esbuild is invoked with:
48
+
49
+ ```typescript
50
+ {
51
+ bundle: true,
52
+ format: 'esm',
53
+ target: 'es2020',
54
+ minify: true,
55
+ sourcemap: false,
56
+ splitting: true, // ESM code splitting
57
+ metafile: true,
58
+ platform: 'node',
59
+ }
60
+ ```
61
+
62
+ Three plugins are registered (see [CLI / build](../cli/build.md) for descriptions). Node built-in modules are marked external. The `absWorkingDir` is set to the workspace root (or app root if no workspace is detected) so esbuild resolves node_modules correctly.
63
+
64
+ ---
65
+
66
+ ## Phase 3: Copy public assets
67
+
68
+ `public/` is copied recursively to `dist/`. If `public/` does not exist, this phase is skipped silently.
69
+
70
+ ---
71
+
72
+ ## Workspace dependency discovery
73
+
74
+ `discoverWorkspaceDependencies()` reads the app's `package.json` and collects all `workspace:*` entries from `dependencies`, `devDependencies`, and `peerDependencies`. It also scans source files for `@scope/pkg` import patterns to catch transitive workspace imports not listed in `package.json`.
75
+
76
+ For each candidate package name, it searches these directories under the workspace root:
77
+
78
+ ```
79
+ lib/<pkgName>
80
+ packages/<pkgName>
81
+ packages/runtime/<pkgName>
82
+ packages/plugins/<pkgName>
83
+ packages/domain/<pkgName>
84
+ ```
85
+
86
+ A match requires the directory to have a `package.json` whose `name` field matches the expected package name, and a `src/` subdirectory.
87
+
88
+ ---
89
+
90
+ ## Workspace resolver plugin
91
+
92
+ The `workspace-resolver` esbuild plugin handles `@scope/pkg` imports during bundling. It maps each import to the compiled `.tsx` files in `.swite-build/` by:
93
+
94
+ 1. Finding the matching workspace dependency from the discovery step.
95
+ 2. Reading the package's `package.json` exports field to resolve the subpath.
96
+ 3. Converting the export path from `./src/Foo.uix` to `.swite-build/<depRelPath>/src/Foo.tsx`.
97
+ 4. Falling back to `src/index.js` if exports resolution fails.
@@ -1,87 +1,87 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # Dev Server
8
-
9
- `SwiteServer` (in `src/dev-engine/server.ts`) is the Express application that handles all incoming requests during development.
10
-
11
- ---
12
-
13
- ## Startup sequence
14
-
15
- When `server.start()` is called:
16
-
17
- 1. **Symlink registry** — Scans `node_modules` directories at the app root, one level up from the app root, the workspace root (if detected), and the co-located framework monorepo root. Registers every symlink target so that absolute filesystem paths obtained via `fs.realpath()` can later be mapped back to browser-relative URLs.
18
-
19
- 2. **Middleware setup** — Calls `setupMiddleware()`, which registers the full middleware chain (see below) and initializes the file router, import map, and env variable loading.
20
-
21
- 3. **HMR** — Calls `hmr.initialize()` (port probe and WebSocket server creation) then `hmr.start()` (chokidar watcher).
22
-
23
- 4. **HTTP listen** — Binds to the configured port. When `host` is `"localhost"`, SWITE binds to `0.0.0.0` so both `::1` and `127.0.0.1` respond.
24
-
25
- ---
26
-
27
- ## Workspace root detection
28
-
29
- `SwiteServer.findWorkspaceRoot(startDir)` walks up the directory tree, up to six levels, looking for:
30
-
31
- - A `pnpm-workspace.yaml` file, or
32
- - A `package.json` with a `workspaces` field
33
-
34
- If found, that directory is the workspace root. The workspace root is used to locate `node_modules`, workspace packages, and the import map.
35
-
36
- `SwiteConfig.rootDir` can be set to override auto-detection.
37
-
38
- ---
39
-
40
- ## Middleware chain
41
-
42
- The middleware is registered in a fixed order that Express processes top-to-bottom:
43
-
44
- | Priority | Path | Handler |
45
- |----------|------|---------|
46
- | 1 | all | File router + HMR routes |
47
- | 2 | `/packages` | TypeScript and JavaScript handler (workspace source files) |
48
- | 3 | `/src` | `.ui`, `.uix`, `.ts`, `.js`, `.mjs`, static (CSS, images) |
49
- | 4 | `/lib` | `.ui`, `.uix` source files (pre-static guard) |
50
- | 5 | all | `.ui`/`.uix` MIME-type guard |
51
- | 6 | `/.skltn/modules.css` | Returns 204 (CSS not bundled in dev) |
52
- | 7 | static | `public/`, `node_modules/`, `lib/`, `libraries/`, `modules/` |
53
- | 8 | all | General source-file transformation for all other paths |
54
- | 9 | all | SPA fallback — serves `public/index.html` for HTML-accepting requests |
55
-
56
- The SPA fallback refuses to serve HTML for paths under `/src/`, `/swiss-packages/`, and `/lib/` — those return 404 if they reach the fallback, preventing MIME mismatch errors.
57
-
58
- ---
59
-
60
- ## File handlers
61
-
62
- Each file type has a dedicated handler class that extends `BaseHandler`:
63
-
64
- | Handler | Extension(s) | Behaviour |
65
- |---------|-------------|-----------|
66
- | `UIHandler` | `.ui` | Compiles via `UiCompiler`, strips TypeScript via esbuild, fixes swiss-lib paths, inlines `import.meta.env`, strips CSS imports, rewrites bare imports |
67
- | `UIXHandler` | `.uix` | Same pipeline as UIHandler |
68
- | `TSHandler` | `.ts` | esbuild TypeScript strip, inlines env, rewrites imports. Falls back to `.ui` or `.uix` if the `.ts` file does not exist |
69
- | `JSHandler` | `.js` | Rewrites imports. Falls back to `.ts`, `.ui`, `.uix` if `.js` does not exist |
70
- | `MJSHandler` | `.mjs` | Rewrites imports. Falls back to `.js` handler |
71
- | `NodeModuleHandler` | `/node_modules/…` | Walks up the directory tree to find the package. Serves without import rewriting. Falls back to jsDelivr CDN redirect if not found locally |
72
-
73
- All handlers check an in-memory compilation cache before compiling. Cached entries are keyed by file path and invalidated when the file's mtime changes.
74
-
75
- ---
76
-
77
- ## Static file serving
78
-
79
- `setupStaticFiles` registers `express.static` for:
80
-
81
- - `public/` at the root URL
82
- - `node_modules/` at `/node_modules/` (app root and workspace root)
83
- - `lib/` at `/lib/` (resolved by walking up from app root looking for a directory that has both `pnpm-workspace.yaml` and `lib/`)
84
- - `libraries/` at `/libraries/` (workspace root, legacy)
85
- - `modules/` at `/modules/` (workspace root, CSS and assets only)
86
-
87
- Source files (`.ui`, `.uix`, `.ts`, `.js`, `.mjs`) are excluded from `express.static` by a guard middleware registered before each static mount. This ensures they always go through the compiler pipeline.
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # Dev Server
8
+
9
+ `SwiteServer` (in `src/dev-engine/server.ts`) is the Express application that handles all incoming requests during development.
10
+
11
+ ---
12
+
13
+ ## Startup sequence
14
+
15
+ When `server.start()` is called:
16
+
17
+ 1. **Symlink registry** — Scans `node_modules` directories at the app root, one level up from the app root, the workspace root (if detected), and the co-located framework monorepo root. Registers every symlink target so that absolute filesystem paths obtained via `fs.realpath()` can later be mapped back to browser-relative URLs.
18
+
19
+ 2. **Middleware setup** — Calls `setupMiddleware()`, which registers the full middleware chain (see below) and initializes the file router, import map, and env variable loading.
20
+
21
+ 3. **HMR** — Calls `hmr.initialize()` (port probe and WebSocket server creation) then `hmr.start()` (chokidar watcher).
22
+
23
+ 4. **HTTP listen** — Binds to the configured port. When `host` is `"localhost"`, SWITE binds to `0.0.0.0` so both `::1` and `127.0.0.1` respond.
24
+
25
+ ---
26
+
27
+ ## Workspace root detection
28
+
29
+ `SwiteServer.findWorkspaceRoot(startDir)` walks up the directory tree, up to six levels, looking for:
30
+
31
+ - A `pnpm-workspace.yaml` file, or
32
+ - A `package.json` with a `workspaces` field
33
+
34
+ If found, that directory is the workspace root. The workspace root is used to locate `node_modules`, workspace packages, and the import map.
35
+
36
+ `SwiteConfig.rootDir` can be set to override auto-detection.
37
+
38
+ ---
39
+
40
+ ## Middleware chain
41
+
42
+ The middleware is registered in a fixed order that Express processes top-to-bottom:
43
+
44
+ | Priority | Path | Handler |
45
+ |----------|------|---------|
46
+ | 1 | all | File router + HMR routes |
47
+ | 2 | `/packages` | TypeScript and JavaScript handler (workspace source files) |
48
+ | 3 | `/src` | `.ui`, `.uix`, `.ts`, `.js`, `.mjs`, static (CSS, images) |
49
+ | 4 | `/lib` | `.ui`, `.uix` source files (pre-static guard) |
50
+ | 5 | all | `.ui`/`.uix` MIME-type guard |
51
+ | 6 | `/.skltn/modules.css` | Returns 204 (CSS not bundled in dev) |
52
+ | 7 | static | `public/`, `node_modules/`, `lib/`, `libraries/`, `modules/` |
53
+ | 8 | all | General source-file transformation for all other paths |
54
+ | 9 | all | SPA fallback — serves `public/index.html` for HTML-accepting requests |
55
+
56
+ The SPA fallback refuses to serve HTML for paths under `/src/`, `/swiss-packages/`, and `/lib/` — those return 404 if they reach the fallback, preventing MIME mismatch errors.
57
+
58
+ ---
59
+
60
+ ## File handlers
61
+
62
+ Each file type has a dedicated handler class that extends `BaseHandler`:
63
+
64
+ | Handler | Extension(s) | Behaviour |
65
+ |---------|-------------|-----------|
66
+ | `UIHandler` | `.ui` | Compiles via `UiCompiler`, strips TypeScript via esbuild, fixes swiss-lib paths, inlines `import.meta.env`, strips CSS imports, rewrites bare imports |
67
+ | `UIXHandler` | `.uix` | Same pipeline as UIHandler |
68
+ | `TSHandler` | `.ts` | esbuild TypeScript strip, inlines env, rewrites imports. Falls back to `.ui` or `.uix` if the `.ts` file does not exist |
69
+ | `JSHandler` | `.js` | Rewrites imports. Falls back to `.ts`, `.ui`, `.uix` if `.js` does not exist |
70
+ | `MJSHandler` | `.mjs` | Rewrites imports. Falls back to `.js` handler |
71
+ | `NodeModuleHandler` | `/node_modules/…` | Walks up the directory tree to find the package. Serves without import rewriting. Falls back to jsDelivr CDN redirect if not found locally |
72
+
73
+ All handlers check an in-memory compilation cache before compiling. Cached entries are keyed by file path and invalidated when the file's mtime changes.
74
+
75
+ ---
76
+
77
+ ## Static file serving
78
+
79
+ `setupStaticFiles` registers `express.static` for:
80
+
81
+ - `public/` at the root URL
82
+ - `node_modules/` at `/node_modules/` (app root and workspace root)
83
+ - `lib/` at `/lib/` (resolved by walking up from app root looking for a directory that has both `pnpm-workspace.yaml` and `lib/`)
84
+ - `libraries/` at `/libraries/` (workspace root, legacy)
85
+ - `modules/` at `/modules/` (workspace root, CSS and assets only)
86
+
87
+ Source files (`.ui`, `.uix`, `.ts`, `.js`, `.mjs`) are excluded from `express.static` by a guard middleware registered before each static mount. This ensures they always go through the compiler pipeline.