@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.
- package/CHANGELOG.md +22 -0
- package/dist/cli.js +0 -0
- package/dist/config/config.d.ts +11 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.d.ts +3 -1
- package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.js +1 -1
- package/dist/dev-engine/handlers/node-module-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/node-module-handler.js +30 -41
- package/dist/dev-engine/middleware/middleware-setup.d.ts +1 -0
- package/dist/dev-engine/middleware/middleware-setup.d.ts.map +1 -1
- package/dist/dev-engine/server.d.ts.map +1 -1
- package/dist/dev-engine/server.js +4 -0
- package/dist/kernel/package-finder.d.ts +7 -7
- package/dist/kernel/package-finder.d.ts.map +1 -1
- package/dist/kernel/package-finder.js +56 -40
- package/dist/resolution/bare-import-resolver.d.ts.map +1 -1
- package/dist/resolution/bare-import-resolver.js +13 -4
- package/dist/resolution/path/file-path-resolver.d.ts +2 -1
- package/dist/resolution/path/file-path-resolver.d.ts.map +1 -1
- package/dist/resolution/path/file-path-resolver.js +36 -9
- package/docs/architecture/build-pipeline.md +97 -0
- package/docs/architecture/dev-server.md +87 -0
- package/docs/architecture/hmr.md +78 -0
- package/docs/architecture/import-rewriting.md +101 -0
- package/docs/architecture/index.md +16 -0
- package/docs/architecture/python-integration.md +93 -0
- package/docs/architecture/resolution.md +92 -0
- package/docs/cli/build.md +78 -0
- package/docs/cli/dev.md +90 -0
- package/docs/cli/index.md +15 -0
- package/docs/cli/start.md +45 -0
- package/docs/development/contributing.md +74 -0
- package/docs/development/index.md +12 -0
- package/docs/development/internals.md +101 -0
- package/docs/guide/configuration.md +89 -0
- package/docs/guide/index.md +13 -0
- package/docs/guide/project-structure.md +75 -0
- package/docs/guide/quickstart.md +113 -0
- package/docs/index.md +16 -0
- package/package.json +15 -24
- package/src/config/config.ts +11 -0
- package/src/dev-engine/handlers/base-handler.ts +4 -2
- package/src/dev-engine/handlers/node-module-handler.ts +51 -78
- package/src/dev-engine/middleware/middleware-setup.ts +1 -0
- package/src/dev-engine/server.ts +38 -33
- package/src/kernel/package-finder.ts +59 -43
- package/src/resolution/bare-import-resolver.ts +14 -4
- 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
|
+
```
|