@swissjs/swite 0.3.1 → 0.3.3

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 (49) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cli.js +0 -0
  3. package/dist/config/config.d.ts +11 -0
  4. package/dist/config/config.d.ts.map +1 -1
  5. package/dist/dev-engine/handlers/base-handler.d.ts +3 -1
  6. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  7. package/dist/dev-engine/handlers/base-handler.js +1 -1
  8. package/dist/dev-engine/handlers/node-module-handler.d.ts.map +1 -1
  9. package/dist/dev-engine/handlers/node-module-handler.js +30 -41
  10. package/dist/dev-engine/middleware/middleware-setup.d.ts +1 -0
  11. package/dist/dev-engine/middleware/middleware-setup.d.ts.map +1 -1
  12. package/dist/dev-engine/server.d.ts.map +1 -1
  13. package/dist/dev-engine/server.js +4 -0
  14. package/dist/kernel/package-finder.d.ts +7 -7
  15. package/dist/kernel/package-finder.d.ts.map +1 -1
  16. package/dist/kernel/package-finder.js +56 -40
  17. package/dist/resolution/bare-import-resolver.d.ts.map +1 -1
  18. package/dist/resolution/bare-import-resolver.js +13 -4
  19. package/dist/resolution/path/file-path-resolver.d.ts +2 -1
  20. package/dist/resolution/path/file-path-resolver.d.ts.map +1 -1
  21. package/dist/resolution/path/file-path-resolver.js +36 -9
  22. package/docs/architecture/build-pipeline.md +97 -0
  23. package/docs/architecture/dev-server.md +87 -0
  24. package/docs/architecture/hmr.md +78 -0
  25. package/docs/architecture/import-rewriting.md +101 -0
  26. package/docs/architecture/index.md +16 -0
  27. package/docs/architecture/python-integration.md +93 -0
  28. package/docs/architecture/resolution.md +92 -0
  29. package/docs/cli/build.md +78 -0
  30. package/docs/cli/dev.md +90 -0
  31. package/docs/cli/index.md +15 -0
  32. package/docs/cli/start.md +45 -0
  33. package/docs/development/contributing.md +74 -0
  34. package/docs/development/index.md +12 -0
  35. package/docs/development/internals.md +101 -0
  36. package/docs/guide/configuration.md +89 -0
  37. package/docs/guide/index.md +13 -0
  38. package/docs/guide/project-structure.md +75 -0
  39. package/docs/guide/quickstart.md +113 -0
  40. package/docs/index.md +16 -0
  41. package/package.json +15 -24
  42. package/src/config/config.ts +11 -0
  43. package/src/dev-engine/handlers/base-handler.ts +4 -2
  44. package/src/dev-engine/handlers/node-module-handler.ts +51 -78
  45. package/src/dev-engine/middleware/middleware-setup.ts +1 -0
  46. package/src/dev-engine/server.ts +38 -33
  47. package/src/kernel/package-finder.ts +59 -43
  48. package/src/resolution/bare-import-resolver.ts +14 -4
  49. package/src/resolution/path/file-path-resolver.ts +44 -10
@@ -0,0 +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.
@@ -0,0 +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.
@@ -0,0 +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.
@@ -0,0 +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.
@@ -0,0 +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
@@ -0,0 +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 |
@@ -0,0 +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.
@@ -0,0 +1,78 @@
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # swite build
8
+
9
+ Compiles the application for production.
10
+
11
+ ```bash
12
+ swite build
13
+ ```
14
+
15
+ ---
16
+
17
+ ## What it does
18
+
19
+ `swite build` runs `SwiteBuilder` with a fixed configuration derived from the project root:
20
+
21
+ | Parameter | Value |
22
+ |-----------|-------|
23
+ | Entry point | `<root>/src/index.ui` |
24
+ | Output directory | `<root>/dist` |
25
+ | Format | ESM |
26
+ | Target | `es2020` |
27
+ | Minify | `true` |
28
+ | Sourcemap | `false` |
29
+
30
+ These defaults are not yet configurable via `swiss.config.ts`; the builder reads config only for future extensibility.
31
+
32
+ ---
33
+
34
+ ## Build pipeline
35
+
36
+ The build runs in three phases:
37
+
38
+ ### Phase 1: Compile Swiss files
39
+
40
+ All `.ui` and `.uix` files in `src/` are passed through `@swissjs/compiler` (`UiCompiler.compileAsync`), which outputs TypeScript/JSX. The resulting code is written to a temporary `.swite-build/` directory as `.tsx` files. Plain `.ts` files are copied as-is, with `.ui`/`.uix` import references rewritten to `.tsx`. CSS files are copied verbatim.
41
+
42
+ Workspace dependencies declared as `workspace:*` in `package.json` are discovered and compiled into the same temp directory, preserving the workspace directory structure so esbuild can resolve cross-package imports.
43
+
44
+ ### Phase 2: Bundle with esbuild
45
+
46
+ esbuild is invoked with `bundle: true`, targeting the compiled entry point in `.swite-build/`. Three custom plugins are active:
47
+
48
+ - `js-tsx-fallback` — redirects `.js` import resolutions to `.tsx` when the `.tsx` file exists in the temp directory (handles UiCompiler emitting `.js` references)
49
+ - `css-stub` — stubs all `.css` imports with `export {}` so they do not block the bundle
50
+ - `workspace-resolver` — resolves `@scope/pkg` imports to compiled files in `.swite-build/`, using `package.json` exports fields and falling back to `src/index.js`
51
+
52
+ Node built-in modules (`fs`, `path`, `os`, etc., including `node:` prefixed forms) are marked external.
53
+
54
+ ### Phase 3: Copy public assets
55
+
56
+ The contents of `public/` are copied verbatim to `dist/`.
57
+
58
+ ---
59
+
60
+ ## Output
61
+
62
+ `dist/` contains the bundled JavaScript (one or more chunks for ESM splitting), copied public assets, and any CSS files the build chose to emit. The `.swite-build/` temporary directory is removed regardless of whether the build succeeded or failed.
63
+
64
+ Build stats (file count and sizes) are printed to stdout after a successful bundle.
65
+
66
+ ---
67
+
68
+ ## Workspace dependencies
69
+
70
+ The builder scans `package.json` `dependencies`, `devDependencies`, and `peerDependencies` for `workspace:*` entries and also scans source files for `@scope/pkg` import patterns to discover transitive workspace dependencies. For each discovered package, it looks in these directories under the workspace root (in order):
71
+
72
+ ```
73
+ lib/<pkgName>
74
+ packages/<pkgName>
75
+ packages/runtime/<pkgName>
76
+ packages/plugins/<pkgName>
77
+ packages/domain/<pkgName>
78
+ ```