@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
@@ -1,78 +1,78 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # HMR
8
-
9
- `HMREngine` (`src/dev-engine/hmr/hmr.ts`) manages the WebSocket server and file watcher that power hot module replacement.
10
-
11
- ---
12
-
13
- ## WebSocket server
14
-
15
- The WebSocket server is created by the `ws` package. The default port is 24678. On initialization, SWITE checks whether that port is available by attempting to bind a temporary TCP server. If the port is occupied, `findFreePort()` asks the OS for an available port by binding to port 0 and reading the assigned port back.
16
-
17
- All connected browser clients are tracked in a `Set<WebSocket>`. When a client disconnects, it is removed from the set.
18
-
19
- The HMR client script is served at `/__swite_hmr_client` as plain JavaScript. The actual WebSocket port is embedded into this script at server startup so the browser client connects to the correct port even when it differs from 24678.
20
-
21
- ---
22
-
23
- ## File watcher
24
-
25
- `HMREngine.start()` creates a chokidar watcher on the project root with these options:
26
-
27
- - Ignored: `node_modules/`, `.git/`, `dist/`
28
- - `ignoreInitial: true` — no events are fired for files that exist at startup
29
- - `awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }` — waits for the file to stop changing before firing, which prevents partial-write events
30
-
31
- On each `change` event, SWITE determines the update type from the file extension and broadcasts a message to all connected clients.
32
-
33
- ---
34
-
35
- ## Update type classification
36
-
37
- | File extension | Update type | Browser action |
38
- |---------------|-------------|----------------|
39
- | `.css`, `.scss`, `.sass` | `style` | Cache-busts `href` on all `<link rel="stylesheet">` elements by appending `?t={timestamp}` |
40
- | `.js`, `.ts` in `components/` or `pages/` | `hot` | Re-imports the module and calls `instance.update(newModule)` on registered component instances |
41
- | Everything else | `reload` | `window.location.reload()` |
42
-
43
- ---
44
-
45
- ## Broadcast message format
46
-
47
- ```json
48
- {
49
- "type": "update",
50
- "path": "/absolute/path/to/changed/file",
51
- "updateType": "hot | reload | style",
52
- "timestamp": 1716211234567
53
- }
54
- ```
55
-
56
- Messages are sent only to clients whose `readyState` is `WebSocket.OPEN`. Clients in any other state are skipped.
57
-
58
- ---
59
-
60
- ## HMR client
61
-
62
- `buildHmrClientScript(port)` (`src/dev-engine/hmr/hmr-client-template.ts`) returns the browser-side JavaScript as a string with the port number substituted at runtime.
63
-
64
- The client:
65
-
66
- 1. Opens a WebSocket connection to `ws://{hostname}:{port}`.
67
- 2. On `message`, dispatches to `updateStyles()`, the hot-update path, or `window.location.reload()`.
68
- 3. Maintains a `moduleGraph` map (module name → dependents) and a `hotModules` map (module name → module object) for the hot path.
69
- 4. On hot update: removes the module from `window.__swiss_modules__`, re-imports it with a cache-busting `?t={timestamp}` suffix, and calls `instance.update(newModule)` on each registered instance in `window.__swiss_instances__`.
70
- 5. If the hot update throws, falls back to `window.location.reload()`.
71
-
72
- `window.__swiss_modules__` and `window.__swiss_instances__` are initialized to empty objects if not already present. The SwissJS runtime populates `__swiss_instances__` with component instance arrays keyed by component name.
73
-
74
- ---
75
-
76
- ## notifyChange
77
-
78
- `HMREngine.notifyChange(filePath)` is a programmatic interface for triggering HMR broadcasts without a file system event. It is available for use by other server-side logic (such as the file router) that needs to signal a module change.
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # HMR
8
+
9
+ `HMREngine` (`src/dev-engine/hmr/hmr.ts`) manages the WebSocket server and file watcher that power hot module replacement.
10
+
11
+ ---
12
+
13
+ ## WebSocket server
14
+
15
+ The WebSocket server is created by the `ws` package. The default port is 24678. On initialization, SWITE checks whether that port is available by attempting to bind a temporary TCP server. If the port is occupied, `findFreePort()` asks the OS for an available port by binding to port 0 and reading the assigned port back.
16
+
17
+ All connected browser clients are tracked in a `Set<WebSocket>`. When a client disconnects, it is removed from the set.
18
+
19
+ The HMR client script is served at `/__swite_hmr_client` as plain JavaScript. The actual WebSocket port is embedded into this script at server startup so the browser client connects to the correct port even when it differs from 24678.
20
+
21
+ ---
22
+
23
+ ## File watcher
24
+
25
+ `HMREngine.start()` creates a chokidar watcher on the project root with these options:
26
+
27
+ - Ignored: `node_modules/`, `.git/`, `dist/`
28
+ - `ignoreInitial: true` — no events are fired for files that exist at startup
29
+ - `awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }` — waits for the file to stop changing before firing, which prevents partial-write events
30
+
31
+ On each `change` event, SWITE determines the update type from the file extension and broadcasts a message to all connected clients.
32
+
33
+ ---
34
+
35
+ ## Update type classification
36
+
37
+ | File extension | Update type | Browser action |
38
+ |---------------|-------------|----------------|
39
+ | `.css`, `.scss`, `.sass` | `style` | Cache-busts `href` on all `<link rel="stylesheet">` elements by appending `?t={timestamp}` |
40
+ | `.js`, `.ts` in `components/` or `pages/` | `hot` | Re-imports the module and calls `instance.update(newModule)` on registered component instances |
41
+ | Everything else | `reload` | `window.location.reload()` |
42
+
43
+ ---
44
+
45
+ ## Broadcast message format
46
+
47
+ ```json
48
+ {
49
+ "type": "update",
50
+ "path": "/absolute/path/to/changed/file",
51
+ "updateType": "hot | reload | style",
52
+ "timestamp": 1716211234567
53
+ }
54
+ ```
55
+
56
+ Messages are sent only to clients whose `readyState` is `WebSocket.OPEN`. Clients in any other state are skipped.
57
+
58
+ ---
59
+
60
+ ## HMR client
61
+
62
+ `buildHmrClientScript(port)` (`src/dev-engine/hmr/hmr-client-template.ts`) returns the browser-side JavaScript as a string with the port number substituted at runtime.
63
+
64
+ The client:
65
+
66
+ 1. Opens a WebSocket connection to `ws://{hostname}:{port}`.
67
+ 2. On `message`, dispatches to `updateStyles()`, the hot-update path, or `window.location.reload()`.
68
+ 3. Maintains a `moduleGraph` map (module name → dependents) and a `hotModules` map (module name → module object) for the hot path.
69
+ 4. On hot update: removes the module from `window.__swiss_modules__`, re-imports it with a cache-busting `?t={timestamp}` suffix, and calls `instance.update(newModule)` on each registered instance in `window.__swiss_instances__`.
70
+ 5. If the hot update throws, falls back to `window.location.reload()`.
71
+
72
+ `window.__swiss_modules__` and `window.__swiss_instances__` are initialized to empty objects if not already present. The SwissJS runtime populates `__swiss_instances__` with component instance arrays keyed by component name.
73
+
74
+ ---
75
+
76
+ ## notifyChange
77
+
78
+ `HMREngine.notifyChange(filePath)` is a programmatic interface for triggering HMR broadcasts without a file system event. It is available for use by other server-side logic (such as the file router) that needs to signal a module change.
@@ -1,101 +1,101 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # Import Rewriting
8
-
9
- The browser cannot fetch bare module specifiers like `@swissjs/core` or `react`. SWITE rewrites every bare import in compiled source to an absolute browser URL before serving.
10
-
11
- ---
12
-
13
- ## How it works
14
-
15
- `rewriteImports` (`src/resolution/rewriting/import-rewriter.ts`) is called by every file handler after compilation. It uses `es-module-lexer` to find all imports in the code, then applies a collect-and-replace strategy:
16
-
17
- 1. Parse the code with `es-module-lexer` to get the position of every import specifier.
18
- 2. For each specifier, determine the quoted span in the original string (including the surrounding quote characters).
19
- 3. Collect each replacement as `{ start, end, text }` in original-string coordinates.
20
- 4. Sort replacements by descending start position.
21
- 5. Apply replacements right-to-left, so that each substitution does not shift the positions of specifiers to its left.
22
-
23
- This approach eliminates the offset-tracking errors that arise when applying replacements left-to-right.
24
-
25
- ---
26
-
27
- ## What is rewritten
28
-
29
- Only bare module specifiers (those not starting with `.` or `/`) are passed to `ModuleResolver.resolve`. Relative and absolute path imports are left unchanged.
30
-
31
- CSS imports are skipped entirely — they are not ES modules.
32
-
33
- ---
34
-
35
- ## Variable references are not rewritten
36
-
37
- SWITE only rewrites **string literal** specifiers. Variable references in dynamic imports are left unchanged.
38
-
39
- ```javascript
40
- // This WILL be rewritten — string literal
41
- const mod = await import('@swissjs/router');
42
-
43
- // This will NOT be rewritten — variable reference
44
- const mod = await import(def.componentUrl);
45
- ```
46
-
47
- `es-module-lexer` identifies only string literals in import positions. Variable references do not appear as import specifiers in the lexer output.
48
-
49
- `ModuleResolver.resolve` also guards against variable-like specifiers: a specifier containing `.` but not starting with `@` (e.g. `def.componentUrl`) is returned unchanged with a warning log rather than being resolved as a module path.
50
-
51
- ### Required pattern for runtime-determined paths
52
-
53
- When the import target is a runtime value, assign it to a simple local variable first:
54
-
55
- ```typescript
56
- // Correct
57
- const url = def.componentUrl;
58
- const mod = await import(url);
59
-
60
- // Avoid — property access may pass the lexer differently across compiler versions
61
- const mod = await import(def.componentUrl);
62
- ```
63
-
64
- ---
65
-
66
- ## Extension fixup
67
-
68
- The `@swissjs/compiler` occasionally emits `.js` or `.tsx` extensions in relative import paths for files that exist on disk as `.ui` or `.uix`. Before collecting replacements, `rewriteImports` checks each relative import ending in `.js` or `.tsx`: if the corresponding `.ui` or `.uix` file exists on disk, the extension is corrected.
69
-
70
- The correction depends on the importer context:
71
-
72
- - If the importer is a file in `swiss-packages/` or `lib/`, the extension is rewritten to `.ts`.
73
- - If the importer is a `.ui` or `.uix` file, the extension is rewritten to `.ui` or `.uix` based on which file exists on disk.
74
- - Otherwise `.ts`.
75
-
76
- A regex fallback pass applies the same logic for any `.js`/`.tsx` relative imports the lexer may have missed.
77
-
78
- ---
79
-
80
- ## Safety net
81
-
82
- After the primary lexer pass, `rewriteImports` scans the result with a regex for any remaining bare scoped imports (`@scope/pkg/…`). If any are found, they are rewritten using the CDN fallback or `/node_modules/` URL. This catches cases where the lexer silently failed to parse a particular import form.
83
-
84
- ---
85
-
86
- ## Debugging bare imports
87
-
88
- If compiled output still contains bare imports after rewriting, SWITE logs:
89
-
90
- ```
91
- [SWITE] import-rewriter: CRITICAL — bare import "@scope/pkg" still present after rewriting
92
- ```
93
-
94
- Each handler also checks its final output and logs any remaining bare imports:
95
-
96
- ```
97
- [.ui] Bare imports still present after rewriting: /src/MyComponent.ui
98
- [.ui] Unresolved import: @scope/pkg
99
- ```
100
-
101
- The `/__swite_diagnose?url=<path>` endpoint fetches any URL the server serves and reports whether bare imports are present in the response, along with the first ten import specifiers found.
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # Import Rewriting
8
+
9
+ The browser cannot fetch bare module specifiers like `@swissjs/core` or `react`. SWITE rewrites every bare import in compiled source to an absolute browser URL before serving.
10
+
11
+ ---
12
+
13
+ ## How it works
14
+
15
+ `rewriteImports` (`src/resolution/rewriting/import-rewriter.ts`) is called by every file handler after compilation. It uses `es-module-lexer` to find all imports in the code, then applies a collect-and-replace strategy:
16
+
17
+ 1. Parse the code with `es-module-lexer` to get the position of every import specifier.
18
+ 2. For each specifier, determine the quoted span in the original string (including the surrounding quote characters).
19
+ 3. Collect each replacement as `{ start, end, text }` in original-string coordinates.
20
+ 4. Sort replacements by descending start position.
21
+ 5. Apply replacements right-to-left, so that each substitution does not shift the positions of specifiers to its left.
22
+
23
+ This approach eliminates the offset-tracking errors that arise when applying replacements left-to-right.
24
+
25
+ ---
26
+
27
+ ## What is rewritten
28
+
29
+ Only bare module specifiers (those not starting with `.` or `/`) are passed to `ModuleResolver.resolve`. Relative and absolute path imports are left unchanged.
30
+
31
+ CSS imports are skipped entirely — they are not ES modules.
32
+
33
+ ---
34
+
35
+ ## Variable references are not rewritten
36
+
37
+ SWITE only rewrites **string literal** specifiers. Variable references in dynamic imports are left unchanged.
38
+
39
+ ```javascript
40
+ // This WILL be rewritten — string literal
41
+ const mod = await import('@swissjs/router');
42
+
43
+ // This will NOT be rewritten — variable reference
44
+ const mod = await import(def.componentUrl);
45
+ ```
46
+
47
+ `es-module-lexer` identifies only string literals in import positions. Variable references do not appear as import specifiers in the lexer output.
48
+
49
+ `ModuleResolver.resolve` also guards against variable-like specifiers: a specifier containing `.` but not starting with `@` (e.g. `def.componentUrl`) is returned unchanged with a warning log rather than being resolved as a module path.
50
+
51
+ ### Required pattern for runtime-determined paths
52
+
53
+ When the import target is a runtime value, assign it to a simple local variable first:
54
+
55
+ ```typescript
56
+ // Correct
57
+ const url = def.componentUrl;
58
+ const mod = await import(url);
59
+
60
+ // Avoid — property access may pass the lexer differently across compiler versions
61
+ const mod = await import(def.componentUrl);
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Extension fixup
67
+
68
+ The `@swissjs/compiler` occasionally emits `.js` or `.tsx` extensions in relative import paths for files that exist on disk as `.ui` or `.uix`. Before collecting replacements, `rewriteImports` checks each relative import ending in `.js` or `.tsx`: if the corresponding `.ui` or `.uix` file exists on disk, the extension is corrected.
69
+
70
+ The correction depends on the importer context:
71
+
72
+ - If the importer is a file in `swiss-packages/` or `lib/`, the extension is rewritten to `.ts`.
73
+ - If the importer is a `.ui` or `.uix` file, the extension is rewritten to `.ui` or `.uix` based on which file exists on disk.
74
+ - Otherwise `.ts`.
75
+
76
+ A regex fallback pass applies the same logic for any `.js`/`.tsx` relative imports the lexer may have missed.
77
+
78
+ ---
79
+
80
+ ## Safety net
81
+
82
+ After the primary lexer pass, `rewriteImports` scans the result with a regex for any remaining bare scoped imports (`@scope/pkg/…`). If any are found, they are rewritten using the CDN fallback or `/node_modules/` URL. This catches cases where the lexer silently failed to parse a particular import form.
83
+
84
+ ---
85
+
86
+ ## Debugging bare imports
87
+
88
+ If compiled output still contains bare imports after rewriting, SWITE logs:
89
+
90
+ ```
91
+ [SWITE] import-rewriter: CRITICAL — bare import "@scope/pkg" still present after rewriting
92
+ ```
93
+
94
+ Each handler also checks its final output and logs any remaining bare imports:
95
+
96
+ ```
97
+ [.ui] Bare imports still present after rewriting: /src/MyComponent.ui
98
+ [.ui] Unresolved import: @scope/pkg
99
+ ```
100
+
101
+ The `/__swite_diagnose?url=<path>` endpoint fetches any URL the server serves and reports whether bare imports are present in the response, along with the first ten import specifiers found.
@@ -1,16 +1,16 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # Architecture
8
-
9
- Internal design of SWITE.
10
-
11
- - [Dev server](./dev-server.md) — SwiteServer, workspace root detection, middleware chain, file handlers
12
- - [Import resolution](./resolution.md) — ModuleResolver, bare-import resolution, workspace packages, CDN fallback, import map
13
- - [Import rewriting](./import-rewriting.md) — how module specifiers in compiled output are rewritten to browser URLs
14
- - [Build pipeline](./build-pipeline.md) — SwiteBuilder, esbuild integration, `.ui`/`.uix` compilation
15
- - [HMR](./hmr.md) — WebSocket server, chokidar watcher, client injection, file-change-to-reload flow
16
- - [Python integration](./python-integration.md) — Python service lifecycle, health polling, proxy adapter, production mode
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # Architecture
8
+
9
+ Internal design of SWITE.
10
+
11
+ - [Dev server](./dev-server.md) — SwiteServer, workspace root detection, middleware chain, file handlers
12
+ - [Import resolution](./resolution.md) — ModuleResolver, bare-import resolution, workspace packages, CDN fallback, import map
13
+ - [Import rewriting](./import-rewriting.md) — how module specifiers in compiled output are rewritten to browser URLs
14
+ - [Build pipeline](./build-pipeline.md) — SwiteBuilder, esbuild integration, `.ui`/`.uix` compilation
15
+ - [HMR](./hmr.md) — WebSocket server, chokidar watcher, client injection, file-change-to-reload flow
16
+ - [Python integration](./python-integration.md) — Python service lifecycle, health polling, proxy adapter, production mode
@@ -1,93 +1,93 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # Python Integration
8
-
9
- SWITE supports a co-located Python backend service. The integration has two parts: lifecycle management during development and a proxy adapter used by application code to forward requests.
10
-
11
- ---
12
-
13
- ## Dev lifecycle
14
-
15
- `startPythonDevService(config, projectRoot)` (`src/dev-engine/pythonDevManager.ts`) is called by `swite dev` when `services.python.autoStart` is `true`.
16
-
17
- ### Process spawning
18
-
19
- ```
20
- python3 {config.entry} (python on Windows)
21
- ```
22
-
23
- The process is spawned with:
24
- - `stdio: ['ignore', 'pipe', 'pipe']` — stdin is closed; stdout and stderr are piped
25
- - Environment: current process environment merged with `config.env`, plus `PORT={config.port}`
26
-
27
- Stdout and stderr are read line-buffered and printed to Node's stdout with `[python]` prefixes.
28
-
29
- ### Health polling
30
-
31
- After spawning, `pollHealth(url)` polls `http://localhost:{port}{config.healthCheck}` every 500 ms. The request uses a 1-second `AbortSignal` timeout per attempt. When the response is `ok`, polling stops and the Node server proceeds.
32
-
33
- After five attempts the poll interval grows exponentially (doubling with each attempt) up to a maximum of 3 seconds per attempt. The total deadline is 15 seconds. If the deadline passes, SWITE calls `stopPythonDevService()` and throws.
34
-
35
- ### Shutdown
36
-
37
- `stopPythonDevService()` sends `SIGTERM` to the child process. It is registered on both `SIGINT` (Ctrl-C) and the Node `exit` event so the Python process is always cleaned up.
38
-
39
- ---
40
-
41
- ## initPythonProxy
42
-
43
- `initPythonProxy(config)` stores the `PythonServiceConfig` in module-level state. It is called immediately after `startPythonDevService` resolves, so `proxyToPython` can derive the base URL from the config's port before `PYTHON_SERVICE_URL` is set.
44
-
45
- ---
46
-
47
- ## proxyToPython
48
-
49
- `proxyToPython<T>(options)` (`src/adapters/proxy/proxyToPython.ts`) is the adapter application code uses to forward requests to the Python service.
50
-
51
- ### Base URL resolution
52
-
53
- | Condition | Base URL |
54
- |-----------|----------|
55
- | `PYTHON_SERVICE_URL` env var is set | `PYTHON_SERVICE_URL` (trailing slash stripped) |
56
- | Not set, dev mode, config initialized | `http://localhost:{config.port}` |
57
- | Not set, production mode | throws immediately |
58
-
59
- ### Request
60
-
61
- Every call injects the `X-Internal-Token` header with the value of `INTERNAL_API_TOKEN` (empty string if not set). When `options.body` is provided, `Content-Type: application/json` is added and the body is serialized with `JSON.stringify`.
62
-
63
- Non-2xx responses throw `SwiteProxyError` with the HTTP status, a description, and the parsed response body.
64
-
65
- ### Options
66
-
67
- ```typescript
68
- interface ProxyOptions {
69
- method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
70
- path: string; // e.g. '/api/orders'
71
- body?: unknown; // JSON-serializable
72
- headers?: Record<string, string>;
73
- }
74
- ```
75
-
76
- ---
77
-
78
- ## Production mode
79
-
80
- `setProductionMode()` is called by `swite start` before the server starts. In production mode:
81
-
82
- - The localhost fallback is disabled. If `PYTHON_SERVICE_URL` is not set, `proxyToPython` throws `"PYTHON_SERVICE_URL is required in production mode but is not set."` rather than attempting a localhost connection.
83
- - The Python process is never spawned. The `services.python` config is read only to emit the startup warning if `PYTHON_SERVICE_URL` is absent.
84
-
85
- ---
86
-
87
- ## Environment variable summary
88
-
89
- | Variable | Used by | Description |
90
- |----------|---------|-------------|
91
- | `PYTHON_SERVICE_URL` | `proxyToPython` | Base URL of the Python service. Required in production. Overrides localhost fallback in dev. |
92
- | `INTERNAL_API_TOKEN` | `proxyToPython` | Value of the `X-Internal-Token` header on every proxy request |
93
- | `PORT` | Python process | Set automatically to `config.port` by SWITE when spawning the Python process |
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # Python Integration
8
+
9
+ SWITE supports a co-located Python backend service. The integration has two parts: lifecycle management during development and a proxy adapter used by application code to forward requests.
10
+
11
+ ---
12
+
13
+ ## Dev lifecycle
14
+
15
+ `startPythonDevService(config, projectRoot)` (`src/dev-engine/pythonDevManager.ts`) is called by `swite dev` when `services.python.autoStart` is `true`.
16
+
17
+ ### Process spawning
18
+
19
+ ```
20
+ python3 {config.entry} (python on Windows)
21
+ ```
22
+
23
+ The process is spawned with:
24
+ - `stdio: ['ignore', 'pipe', 'pipe']` — stdin is closed; stdout and stderr are piped
25
+ - Environment: current process environment merged with `config.env`, plus `PORT={config.port}`
26
+
27
+ Stdout and stderr are read line-buffered and printed to Node's stdout with `[python]` prefixes.
28
+
29
+ ### Health polling
30
+
31
+ After spawning, `pollHealth(url)` polls `http://localhost:{port}{config.healthCheck}` every 500 ms. The request uses a 1-second `AbortSignal` timeout per attempt. When the response is `ok`, polling stops and the Node server proceeds.
32
+
33
+ After five attempts the poll interval grows exponentially (doubling with each attempt) up to a maximum of 3 seconds per attempt. The total deadline is 15 seconds. If the deadline passes, SWITE calls `stopPythonDevService()` and throws.
34
+
35
+ ### Shutdown
36
+
37
+ `stopPythonDevService()` sends `SIGTERM` to the child process. It is registered on both `SIGINT` (Ctrl-C) and the Node `exit` event so the Python process is always cleaned up.
38
+
39
+ ---
40
+
41
+ ## initPythonProxy
42
+
43
+ `initPythonProxy(config)` stores the `PythonServiceConfig` in module-level state. It is called immediately after `startPythonDevService` resolves, so `proxyToPython` can derive the base URL from the config's port before `PYTHON_SERVICE_URL` is set.
44
+
45
+ ---
46
+
47
+ ## proxyToPython
48
+
49
+ `proxyToPython<T>(options)` (`src/adapters/proxy/proxyToPython.ts`) is the adapter application code uses to forward requests to the Python service.
50
+
51
+ ### Base URL resolution
52
+
53
+ | Condition | Base URL |
54
+ |-----------|----------|
55
+ | `PYTHON_SERVICE_URL` env var is set | `PYTHON_SERVICE_URL` (trailing slash stripped) |
56
+ | Not set, dev mode, config initialized | `http://localhost:{config.port}` |
57
+ | Not set, production mode | throws immediately |
58
+
59
+ ### Request
60
+
61
+ Every call injects the `X-Internal-Token` header with the value of `INTERNAL_API_TOKEN` (empty string if not set). When `options.body` is provided, `Content-Type: application/json` is added and the body is serialized with `JSON.stringify`.
62
+
63
+ Non-2xx responses throw `SwiteProxyError` with the HTTP status, a description, and the parsed response body.
64
+
65
+ ### Options
66
+
67
+ ```typescript
68
+ interface ProxyOptions {
69
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
70
+ path: string; // e.g. '/api/orders'
71
+ body?: unknown; // JSON-serializable
72
+ headers?: Record<string, string>;
73
+ }
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Production mode
79
+
80
+ `setProductionMode()` is called by `swite start` before the server starts. In production mode:
81
+
82
+ - The localhost fallback is disabled. If `PYTHON_SERVICE_URL` is not set, `proxyToPython` throws `"PYTHON_SERVICE_URL is required in production mode but is not set."` rather than attempting a localhost connection.
83
+ - The Python process is never spawned. The `services.python` config is read only to emit the startup warning if `PYTHON_SERVICE_URL` is absent.
84
+
85
+ ---
86
+
87
+ ## Environment variable summary
88
+
89
+ | Variable | Used by | Description |
90
+ |----------|---------|-------------|
91
+ | `PYTHON_SERVICE_URL` | `proxyToPython` | Base URL of the Python service. Required in production. Overrides localhost fallback in dev. |
92
+ | `INTERNAL_API_TOKEN` | `proxyToPython` | Value of the `X-Internal-Token` header on every proxy request |
93
+ | `PORT` | Python process | Set automatically to `config.port` by SWITE when spawning the Python process |