@swissjs/swite 0.3.5 → 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 (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/DIRECTIVE.md +57 -2
  3. package/__tests__/import-rewriter-bug.test.ts +100 -113
  4. package/__tests__/security-r001-r002.test.ts +190 -0
  5. package/dist/build-engine/builder.js +9 -9
  6. package/dist/cli.js +0 -0
  7. package/dist/config/config.d.ts +0 -5
  8. package/dist/config/config.d.ts.map +1 -1
  9. package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
  10. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  11. package/dist/dev-engine/handlers/base-handler.js +91 -0
  12. package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
  13. package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
  14. package/dist/dev-engine/handlers/ui-handler.js +2 -64
  15. package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
  16. package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
  17. package/dist/dev-engine/handlers/uix-handler.js +2 -58
  18. package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
  19. package/dist/dev-engine/hmr/hmr.d.ts +10 -1
  20. package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
  21. package/dist/dev-engine/hmr/hmr.js +40 -2
  22. package/dist/dev-engine/middleware/middleware-setup.js +4 -3
  23. package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
  24. package/dist/dev-engine/middleware/static-files.js +145 -62
  25. package/dist/dev-engine/pythonDevManager.js +1 -1
  26. package/dist/dev-engine/router/file-router.d.ts.map +1 -1
  27. package/dist/dev-engine/router/file-router.js +2 -29
  28. package/dist/dev-engine/server.d.ts +7 -0
  29. package/dist/dev-engine/server.d.ts.map +1 -1
  30. package/dist/dev-engine/server.js +31 -3
  31. package/dist/kernel/package-finder.d.ts +0 -8
  32. package/dist/kernel/package-finder.d.ts.map +1 -1
  33. package/dist/kernel/package-finder.js +2 -2
  34. package/dist/kernel/package-registry.d.ts +6 -0
  35. package/dist/kernel/package-registry.d.ts.map +1 -1
  36. package/dist/kernel/package-registry.js +8 -0
  37. package/dist/kernel/workspace.d.ts.map +1 -1
  38. package/dist/kernel/workspace.js +12 -9
  39. package/docs/architecture/build-pipeline.md +97 -97
  40. package/docs/architecture/dev-server.md +87 -87
  41. package/docs/architecture/hmr.md +78 -78
  42. package/docs/architecture/import-rewriting.md +101 -101
  43. package/docs/architecture/index.md +16 -16
  44. package/docs/architecture/python-integration.md +93 -93
  45. package/docs/architecture/resolution.md +92 -92
  46. package/docs/cli/build.md +78 -78
  47. package/docs/cli/dev.md +90 -90
  48. package/docs/cli/index.md +15 -15
  49. package/docs/cli/start.md +45 -45
  50. package/docs/development/contributing.md +74 -74
  51. package/docs/development/index.md +12 -12
  52. package/docs/development/internals.md +101 -101
  53. package/docs/guide/configuration.md +89 -89
  54. package/docs/guide/index.md +13 -13
  55. package/docs/guide/project-structure.md +75 -75
  56. package/docs/guide/quickstart.md +113 -113
  57. package/docs/index.md +16 -16
  58. package/package.json +29 -16
  59. package/src/build-engine/builder.ts +9 -9
  60. package/src/config/config.ts +0 -5
  61. package/src/config/env.ts +98 -98
  62. package/src/dev-engine/handlers/base-handler.ts +109 -0
  63. package/src/dev-engine/handlers/ui-handler.ts +30 -110
  64. package/src/dev-engine/handlers/uix-handler.ts +21 -95
  65. package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
  66. package/src/dev-engine/hmr/hmr.ts +46 -1
  67. package/src/dev-engine/middleware/middleware-setup.ts +354 -354
  68. package/src/dev-engine/middleware/static-files.ts +203 -121
  69. package/src/dev-engine/pythonDevManager.ts +1 -1
  70. package/src/dev-engine/router/file-router.ts +2 -45
  71. package/src/dev-engine/server.ts +33 -3
  72. package/src/kernel/package-finder.ts +2 -2
  73. package/src/kernel/package-registry.ts +9 -0
  74. package/src/kernel/workspace.ts +8 -10
  75. package/src/resolution/cdn/cdn-fallback.ts +40 -40
  76. package/src/resolution/path/path-fixup.ts +27 -27
  77. package/src/resolution/rewriting/import-rewriter.ts +237 -237
  78. package/src/resolution/symlink-registry.ts +114 -114
@@ -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 |
@@ -1,92 +1,92 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # Import Resolution
8
-
9
- SWITE resolves every bare module specifier to a browser-accessible URL before serving compiled source. This page describes the full resolution pipeline.
10
-
11
- ---
12
-
13
- ## ModuleResolver
14
-
15
- `ModuleResolver` (`src/resolution/resolver.ts`) is the central resolution object. Each `SwiteServer` creates one instance per app root. It is injected into all file handlers and the import rewriter.
16
-
17
- `ModuleResolver.resolve(specifier, importer)` returns a URL string. Specifiers fall into four categories:
18
-
19
- 1. **Import map hit** — If a pre-generated `.swite/import-map.json` is loaded and the specifier appears in its `imports` map, that URL is returned immediately without further resolution.
20
- 2. **Bare import** — Starts with a letter or `@`. Delegated to `resolveBareImport`.
21
- 3. **Absolute path** — Starts with `/`. Returned as-is.
22
- 4. **Relative import** — Starts with `.`. Resolved relative to the importer's directory on disk, then converted to a URL via `toUrl`.
23
-
24
- ### Variable reference detection
25
-
26
- Before delegating to `resolveBareImport`, the resolver checks whether the specifier looks like a variable reference rather than a module path. Specifiers containing a `.` but not starting with `@` (e.g. `def.componentUrl`) are returned unchanged with a warning. Specifiers that do not match the pattern `^[@a-zA-Z][a-zA-Z0-9_/@-]*(\.(js|ts|ui|uix|mjs|cjs|jsx|tsx))?$` are also returned unchanged.
27
-
28
- ---
29
-
30
- ## Bare import resolution
31
-
32
- `resolveBareImport` (`src/resolution/bare-import-resolver.ts`) resolves a bare specifier by searching for the package's `package.json` in multiple `node_modules` locations:
33
-
34
- 1. `<appRoot>/node_modules/<pkg>`
35
- 2. `<parentOfAppRoot>/node_modules/<pkg>`
36
- 3. `<workspaceRoot>/node_modules/<pkg>`
37
- 4. `<frameworkMonorepo>/node_modules/<pkg>`
38
-
39
- If a `package.json` is found, the resolver reads the `exports` field (preferring the `import` condition), or falls back to `module` then `main`. In development, if the resolved path is inside a `dist/` directory, the resolver attempts to substitute `src/` and `.ts` extension and serves from source.
40
-
41
- ---
42
-
43
- ## Workspace package resolution
44
-
45
- When a package is not found in any `node_modules`, `resolveWorkspacePackage` (`src/resolution/workspace-package-resolver.ts`) is called. It uses `PackageRegistry` (a process-wide singleton) to scan the workspace for `package.json` files. The registry recursively scans up to 15 directory levels deep, skipping `node_modules`, `dist`, `.git`, and `.swite`.
46
-
47
- If the package is still not found, the registry calls `rescan()` in case the package was added after the initial scan.
48
-
49
- ---
50
-
51
- ## toUrl: converting filesystem paths to browser URLs
52
-
53
- `toUrl` (`src/resolution/url-resolver.ts`) converts an absolute filesystem path to a browser-relative URL. The resolution order is:
54
-
55
- 1. **Symlink registry** — Looks up the path in the startup-built registry. If the path (or its `fs.realpath` equivalent) is within a registered symlink target, the corresponding `/node_modules/<pkg>` URL prefix is returned.
56
- 2. **node_modules path** — If the normalized path contains `/node_modules/`, the substring from `/node_modules/` onward is used as the URL.
57
- 3. **Workspace path** — If the path is inside the workspace root, the relative path from the workspace root is used.
58
- 4. **App root path** — If the path is inside the app root, the relative path from the app root is used.
59
- 5. **Framework monorepo packages** — If the path is inside the co-located framework monorepo's `packages/` directory, it is served under `/swiss-packages/`.
60
-
61
- ---
62
-
63
- ## CDN fallback
64
-
65
- `shouldUseCdnFallback` (`src/resolution/cdn/cdn-fallback.ts`) determines whether a package may be redirected to jsDelivr when not found locally:
66
-
67
- - Unscoped packages (e.g. `react`, `reflect-metadata`) always allow CDN fallback.
68
- - Scoped packages (e.g. `@swissjs/core`) do not allow CDN fallback by default, since they may be private.
69
- - Scoped packages can be opted in by setting `SWITE_CDN_FALLBACK_SCOPES` to a comma-separated list of scopes (e.g. `@types,@tanstack`).
70
-
71
- CDN fallback URLs use the form `https://cdn.jsdelivr.net/npm/{pkg}/+esm`, which serves an ESM build.
72
-
73
- ---
74
-
75
- ## Import map
76
-
77
- At dev server startup, `setupMiddleware` attempts to load `.swite/import-map.json`. If it exists, `ModuleResolver.setImportMap()` is called and the import map becomes the fast path for all bare import resolutions.
78
-
79
- The import map is generated by `generateImportMap` (`src/internal/generate-import-map.ts`), which:
80
-
81
- 1. Uses `PackageRegistry` to discover all workspace packages.
82
- 2. Calls `resolver.resolve(pkg.name, "")` for each package.
83
- 3. Also resolves common subpaths: `/components`, `/tokens`, `/context`, `/shell`, `/jsx-runtime`, `/jsx-dev-runtime`.
84
- 4. Saves the result as `{ version, generated, imports: { "@pkg/name": "/url" } }`.
85
-
86
- The import map is also injected into `public/index.html` by the SPA fallback handler. If HTML already contains a `<script type="importmap">`, SWITE merges the generated entries in, with existing HTML entries taking priority.
87
-
88
- ---
89
-
90
- ## Path fixup
91
-
92
- `fixSwissLibPaths` (`src/resolution/path/path-fixup.ts`) is applied to every compiled output before import rewriting. It rewrites `/swiss-lib/packages/` to `/swiss-packages/` and `/swiss-lib/` to `/swiss-packages/`. This corrects paths emitted by older versions of `@swissjs/compiler` that referenced a previous directory structure.
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # Import Resolution
8
+
9
+ SWITE resolves every bare module specifier to a browser-accessible URL before serving compiled source. This page describes the full resolution pipeline.
10
+
11
+ ---
12
+
13
+ ## ModuleResolver
14
+
15
+ `ModuleResolver` (`src/resolution/resolver.ts`) is the central resolution object. Each `SwiteServer` creates one instance per app root. It is injected into all file handlers and the import rewriter.
16
+
17
+ `ModuleResolver.resolve(specifier, importer)` returns a URL string. Specifiers fall into four categories:
18
+
19
+ 1. **Import map hit** — If a pre-generated `.swite/import-map.json` is loaded and the specifier appears in its `imports` map, that URL is returned immediately without further resolution.
20
+ 2. **Bare import** — Starts with a letter or `@`. Delegated to `resolveBareImport`.
21
+ 3. **Absolute path** — Starts with `/`. Returned as-is.
22
+ 4. **Relative import** — Starts with `.`. Resolved relative to the importer's directory on disk, then converted to a URL via `toUrl`.
23
+
24
+ ### Variable reference detection
25
+
26
+ Before delegating to `resolveBareImport`, the resolver checks whether the specifier looks like a variable reference rather than a module path. Specifiers containing a `.` but not starting with `@` (e.g. `def.componentUrl`) are returned unchanged with a warning. Specifiers that do not match the pattern `^[@a-zA-Z][a-zA-Z0-9_/@-]*(\.(js|ts|ui|uix|mjs|cjs|jsx|tsx))?$` are also returned unchanged.
27
+
28
+ ---
29
+
30
+ ## Bare import resolution
31
+
32
+ `resolveBareImport` (`src/resolution/bare-import-resolver.ts`) resolves a bare specifier by searching for the package's `package.json` in multiple `node_modules` locations:
33
+
34
+ 1. `<appRoot>/node_modules/<pkg>`
35
+ 2. `<parentOfAppRoot>/node_modules/<pkg>`
36
+ 3. `<workspaceRoot>/node_modules/<pkg>`
37
+ 4. `<frameworkMonorepo>/node_modules/<pkg>`
38
+
39
+ If a `package.json` is found, the resolver reads the `exports` field (preferring the `import` condition), or falls back to `module` then `main`. In development, if the resolved path is inside a `dist/` directory, the resolver attempts to substitute `src/` and `.ts` extension and serves from source.
40
+
41
+ ---
42
+
43
+ ## Workspace package resolution
44
+
45
+ When a package is not found in any `node_modules`, `resolveWorkspacePackage` (`src/resolution/workspace-package-resolver.ts`) is called. It uses `PackageRegistry` (a process-wide singleton) to scan the workspace for `package.json` files. The registry recursively scans up to 15 directory levels deep, skipping `node_modules`, `dist`, `.git`, and `.swite`.
46
+
47
+ If the package is still not found, the registry calls `rescan()` in case the package was added after the initial scan.
48
+
49
+ ---
50
+
51
+ ## toUrl: converting filesystem paths to browser URLs
52
+
53
+ `toUrl` (`src/resolution/url-resolver.ts`) converts an absolute filesystem path to a browser-relative URL. The resolution order is:
54
+
55
+ 1. **Symlink registry** — Looks up the path in the startup-built registry. If the path (or its `fs.realpath` equivalent) is within a registered symlink target, the corresponding `/node_modules/<pkg>` URL prefix is returned.
56
+ 2. **node_modules path** — If the normalized path contains `/node_modules/`, the substring from `/node_modules/` onward is used as the URL.
57
+ 3. **Workspace path** — If the path is inside the workspace root, the relative path from the workspace root is used.
58
+ 4. **App root path** — If the path is inside the app root, the relative path from the app root is used.
59
+ 5. **Framework monorepo packages** — If the path is inside the co-located framework monorepo's `packages/` directory, it is served under `/swiss-packages/`.
60
+
61
+ ---
62
+
63
+ ## CDN fallback
64
+
65
+ `shouldUseCdnFallback` (`src/resolution/cdn/cdn-fallback.ts`) determines whether a package may be redirected to jsDelivr when not found locally:
66
+
67
+ - Unscoped packages (e.g. `react`, `reflect-metadata`) always allow CDN fallback.
68
+ - Scoped packages (e.g. `@swissjs/core`) do not allow CDN fallback by default, since they may be private.
69
+ - Scoped packages can be opted in by setting `SWITE_CDN_FALLBACK_SCOPES` to a comma-separated list of scopes (e.g. `@types,@tanstack`).
70
+
71
+ CDN fallback URLs use the form `https://cdn.jsdelivr.net/npm/{pkg}/+esm`, which serves an ESM build.
72
+
73
+ ---
74
+
75
+ ## Import map
76
+
77
+ At dev server startup, `setupMiddleware` attempts to load `.swite/import-map.json`. If it exists, `ModuleResolver.setImportMap()` is called and the import map becomes the fast path for all bare import resolutions.
78
+
79
+ The import map is generated by `generateImportMap` (`src/internal/generate-import-map.ts`), which:
80
+
81
+ 1. Uses `PackageRegistry` to discover all workspace packages.
82
+ 2. Calls `resolver.resolve(pkg.name, "")` for each package.
83
+ 3. Also resolves common subpaths: `/components`, `/tokens`, `/context`, `/shell`, `/jsx-runtime`, `/jsx-dev-runtime`.
84
+ 4. Saves the result as `{ version, generated, imports: { "@pkg/name": "/url" } }`.
85
+
86
+ The import map is also injected into `public/index.html` by the SPA fallback handler. If HTML already contains a `<script type="importmap">`, SWITE merges the generated entries in, with existing HTML entries taking priority.
87
+
88
+ ---
89
+
90
+ ## Path fixup
91
+
92
+ `fixSwissLibPaths` (`src/resolution/path/path-fixup.ts`) is applied to every compiled output before import rewriting. It rewrites `/swiss-lib/packages/` to `/swiss-packages/` and `/swiss-lib/` to `/swiss-packages/`. This corrects paths emitted by older versions of `@swissjs/compiler` that referenced a previous directory structure.