@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.
- package/CHANGELOG.md +30 -0
- package/DIRECTIVE.md +57 -2
- package/__tests__/import-rewriter-bug.test.ts +100 -113
- package/__tests__/security-r001-r002.test.ts +190 -0
- package/dist/build-engine/builder.js +9 -9
- package/dist/cli.js +0 -0
- package/dist/config/config.d.ts +0 -5
- package/dist/config/config.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
- package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/base-handler.js +91 -0
- package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
- package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/ui-handler.js +2 -64
- package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
- package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
- package/dist/dev-engine/handlers/uix-handler.js +2 -58
- package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
- package/dist/dev-engine/hmr/hmr.d.ts +10 -1
- package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
- package/dist/dev-engine/hmr/hmr.js +40 -2
- package/dist/dev-engine/middleware/middleware-setup.js +4 -3
- package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
- package/dist/dev-engine/middleware/static-files.js +145 -62
- package/dist/dev-engine/pythonDevManager.js +1 -1
- package/dist/dev-engine/router/file-router.d.ts.map +1 -1
- package/dist/dev-engine/router/file-router.js +2 -29
- package/dist/dev-engine/server.d.ts +7 -0
- package/dist/dev-engine/server.d.ts.map +1 -1
- package/dist/dev-engine/server.js +31 -3
- package/dist/kernel/package-finder.d.ts +0 -8
- package/dist/kernel/package-finder.d.ts.map +1 -1
- package/dist/kernel/package-finder.js +2 -2
- package/dist/kernel/package-registry.d.ts +6 -0
- package/dist/kernel/package-registry.d.ts.map +1 -1
- package/dist/kernel/package-registry.js +8 -0
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +12 -9
- package/docs/architecture/build-pipeline.md +97 -97
- package/docs/architecture/dev-server.md +87 -87
- package/docs/architecture/hmr.md +78 -78
- package/docs/architecture/import-rewriting.md +101 -101
- package/docs/architecture/index.md +16 -16
- package/docs/architecture/python-integration.md +93 -93
- package/docs/architecture/resolution.md +92 -92
- package/docs/cli/build.md +78 -78
- package/docs/cli/dev.md +90 -90
- package/docs/cli/index.md +15 -15
- package/docs/cli/start.md +45 -45
- package/docs/development/contributing.md +74 -74
- package/docs/development/index.md +12 -12
- package/docs/development/internals.md +101 -101
- package/docs/guide/configuration.md +89 -89
- package/docs/guide/index.md +13 -13
- package/docs/guide/project-structure.md +75 -75
- package/docs/guide/quickstart.md +113 -113
- package/docs/index.md +16 -16
- package/package.json +29 -16
- package/src/build-engine/builder.ts +9 -9
- package/src/config/config.ts +0 -5
- package/src/config/env.ts +98 -98
- package/src/dev-engine/handlers/base-handler.ts +109 -0
- package/src/dev-engine/handlers/ui-handler.ts +30 -110
- package/src/dev-engine/handlers/uix-handler.ts +21 -95
- package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
- package/src/dev-engine/hmr/hmr.ts +46 -1
- package/src/dev-engine/middleware/middleware-setup.ts +354 -354
- package/src/dev-engine/middleware/static-files.ts +203 -121
- package/src/dev-engine/pythonDevManager.ts +1 -1
- package/src/dev-engine/router/file-router.ts +2 -45
- package/src/dev-engine/server.ts +33 -3
- package/src/kernel/package-finder.ts +2 -2
- package/src/kernel/package-registry.ts +9 -0
- package/src/kernel/workspace.ts +8 -10
- package/src/resolution/cdn/cdn-fallback.ts +40 -40
- package/src/resolution/path/path-fixup.ts +27 -27
- package/src/resolution/rewriting/import-rewriter.ts +237 -237
- package/src/resolution/symlink-registry.ts +114 -114
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-registry.d.ts","sourceRoot":"","sources":["../../src/kernel/package-registry.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,GAAG,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAgB;IAEjC;;OAEG;IACG,aAAa,CACjB,aAAa,EAAE,MAAM,EACrB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,IAAI,CAAC;IAwChB;;OAEG;YACW,aAAa;IAmF3B;;OAEG;IACH,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAIpD;;OAEG;IACH,cAAc,IAAI,WAAW,EAAE;IAI/B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAW7B;;OAEG;IACH,eAAe,IAAI,MAAM;CAG1B;AAKD,wBAAgB,kBAAkB,IAAI,eAAe,CAKpD"}
|
|
1
|
+
{"version":3,"file":"package-registry.d.ts","sourceRoot":"","sources":["../../src/kernel/package-registry.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,GAAG,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAgB;IAEjC;;OAEG;IACG,aAAa,CACjB,aAAa,EAAE,MAAM,EACrB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,IAAI,CAAC;IAwChB;;OAEG;YACW,aAAa;IAmF3B;;OAEG;IACH,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAIpD;;OAEG;IACH,cAAc,IAAI,WAAW,EAAE;IAI/B;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAW7B;;OAEG;IACH,eAAe,IAAI,MAAM;CAG1B;AAKD,wBAAgB,kBAAkB,IAAI,eAAe,CAKpD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C"}
|
|
@@ -157,3 +157,11 @@ export function getPackageRegistry() {
|
|
|
157
157
|
}
|
|
158
158
|
return registryInstance;
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Reset the package registry singleton. For use in tests only.
|
|
162
|
+
* Call before creating a ModuleResolver to prevent the previous scan state
|
|
163
|
+
* from leaking between tests.
|
|
164
|
+
*/
|
|
165
|
+
export function resetPackageRegistry() {
|
|
166
|
+
registryInstance = null;
|
|
167
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/kernel/workspace.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8C5E"}
|
package/dist/kernel/workspace.js
CHANGED
|
@@ -10,29 +10,31 @@ import path from "node:path";
|
|
|
10
10
|
* Updated: Now also checks for lib/ directory to ensure we find the correct SWS root
|
|
11
11
|
*/
|
|
12
12
|
export async function findWorkspaceRoot(root) {
|
|
13
|
+
const debug = process.env["SWITE_DEBUG"] === "1";
|
|
13
14
|
let current = root;
|
|
14
|
-
for (let i = 0; i < 10; i++) {
|
|
15
|
+
for (let i = 0; i < 10; i++) {
|
|
15
16
|
const workspaceFile = path.join(current, "pnpm-workspace.yaml");
|
|
16
17
|
const packageJson = path.join(current, "package.json");
|
|
17
18
|
const libDir = path.join(current, "lib");
|
|
18
19
|
try {
|
|
19
20
|
await fs.access(workspaceFile);
|
|
20
|
-
// Accept root if it has lib/ (SWS with lib/) or packages/ (SWS with packages/ at root)
|
|
21
21
|
const packagesDir = path.join(current, "packages");
|
|
22
22
|
try {
|
|
23
23
|
await fs.access(libDir);
|
|
24
|
-
|
|
24
|
+
if (debug)
|
|
25
|
+
console.log(`[workspace] Found workspace root with lib/: ${current}`);
|
|
25
26
|
return current;
|
|
26
27
|
}
|
|
27
28
|
catch {
|
|
28
29
|
try {
|
|
29
30
|
await fs.access(packagesDir);
|
|
30
|
-
|
|
31
|
+
if (debug)
|
|
32
|
+
console.log(`[workspace] Found workspace root with packages/: ${current}`);
|
|
31
33
|
return current;
|
|
32
34
|
}
|
|
33
35
|
catch {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if (debug)
|
|
37
|
+
console.log(`[workspace] Found workspace file at ${current} but no lib/ or packages/, continuing search...`);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -40,10 +42,10 @@ export async function findWorkspaceRoot(root) {
|
|
|
40
42
|
try {
|
|
41
43
|
const pkgJson = JSON.parse(await fs.readFile(packageJson, "utf-8"));
|
|
42
44
|
if (pkgJson?.workspaces) {
|
|
43
|
-
// Also check for lib/ when package.json has workspaces
|
|
44
45
|
try {
|
|
45
46
|
await fs.access(libDir);
|
|
46
|
-
|
|
47
|
+
if (debug)
|
|
48
|
+
console.log(`[workspace] Found workspace root with lib/ (via package.json): ${current}`);
|
|
47
49
|
return current;
|
|
48
50
|
}
|
|
49
51
|
catch {
|
|
@@ -60,6 +62,7 @@ export async function findWorkspaceRoot(root) {
|
|
|
60
62
|
break;
|
|
61
63
|
current = parent;
|
|
62
64
|
}
|
|
63
|
-
|
|
65
|
+
if (debug)
|
|
66
|
+
console.warn(`[workspace] No workspace root found starting from: ${root}`);
|
|
64
67
|
return null;
|
|
65
68
|
}
|
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Copyright (c) 2024 Themba Mzumara
|
|
3
|
-
SWITE - SWISS Development Server
|
|
4
|
-
Licensed under the MIT License.
|
|
5
|
-
-->
|
|
6
|
-
|
|
7
|
-
# Build Pipeline
|
|
8
|
-
|
|
9
|
-
`SwiteBuilder` (`src/build-engine/builder.ts`) handles production builds. It is invoked by `swite build` with a fixed entry point of `src/index.ui` and output directory of `dist/`.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Overview
|
|
14
|
-
|
|
15
|
-
The build runs three sequential phases inside a try/finally block. The temporary directory `.swite-build/` is always removed when the build finishes, whether it succeeded or failed.
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
swite build
|
|
19
|
-
└── SwiteBuilder.build()
|
|
20
|
-
├── 1. cleanOutputDir() — rm -rf dist/, mkdir dist/
|
|
21
|
-
├── 2. compileSwissFiles() — @swissjs/compiler → .tsx in .swite-build/
|
|
22
|
-
├── 3. bundle() — esbuild bundles from .swite-build/
|
|
23
|
-
└── 4. copyPublicAssets() — cp -r public/ dist/
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Phase 1: Compile Swiss files
|
|
29
|
-
|
|
30
|
-
`compileSwissFiles` traverses `src/` and every workspace dependency's `src/` directory.
|
|
31
|
-
|
|
32
|
-
For each `.ui` or `.uix` file:
|
|
33
|
-
|
|
34
|
-
1. `UiCompiler.compileAsync(source, filePath)` — transforms SwissJS component syntax to TypeScript/JSX.
|
|
35
|
-
2. Relative `.ui`/`.uix` imports in the compiled output are rewritten to `.tsx` (esbuild needs `.tsx` for JSX, not `.ui`).
|
|
36
|
-
3. If the compiled output ends with `export { Foo }`, a `export default Foo` line is appended so default imports resolve at bundle time.
|
|
37
|
-
4. Written to `.swite-build/<relative-path>.tsx`.
|
|
38
|
-
|
|
39
|
-
For each `.ts` file, `.ui`/`.uix` imports in `from` clauses are rewritten to `.tsx`, then the file is copied as-is.
|
|
40
|
-
|
|
41
|
-
CSS files are copied verbatim so that any CSS import stubs in the bundle phase can resolve.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## Phase 2: Bundle with esbuild
|
|
46
|
-
|
|
47
|
-
esbuild is invoked with:
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
{
|
|
51
|
-
bundle: true,
|
|
52
|
-
format: 'esm',
|
|
53
|
-
target: 'es2020',
|
|
54
|
-
minify: true,
|
|
55
|
-
sourcemap: false,
|
|
56
|
-
splitting: true, // ESM code splitting
|
|
57
|
-
metafile: true,
|
|
58
|
-
platform: 'node',
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Three plugins are registered (see [CLI / build](../cli/build.md) for descriptions). Node built-in modules are marked external. The `absWorkingDir` is set to the workspace root (or app root if no workspace is detected) so esbuild resolves node_modules correctly.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Phase 3: Copy public assets
|
|
67
|
-
|
|
68
|
-
`public/` is copied recursively to `dist/`. If `public/` does not exist, this phase is skipped silently.
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## Workspace dependency discovery
|
|
73
|
-
|
|
74
|
-
`discoverWorkspaceDependencies()` reads the app's `package.json` and collects all `workspace:*` entries from `dependencies`, `devDependencies`, and `peerDependencies`. It also scans source files for `@scope/pkg` import patterns to catch transitive workspace imports not listed in `package.json`.
|
|
75
|
-
|
|
76
|
-
For each candidate package name, it searches these directories under the workspace root:
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
lib/<pkgName>
|
|
80
|
-
packages/<pkgName>
|
|
81
|
-
packages/runtime/<pkgName>
|
|
82
|
-
packages/plugins/<pkgName>
|
|
83
|
-
packages/domain/<pkgName>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
A match requires the directory to have a `package.json` whose `name` field matches the expected package name, and a `src/` subdirectory.
|
|
87
|
-
|
|
88
|
-
---
|
|
89
|
-
|
|
90
|
-
## Workspace resolver plugin
|
|
91
|
-
|
|
92
|
-
The `workspace-resolver` esbuild plugin handles `@scope/pkg` imports during bundling. It maps each import to the compiled `.tsx` files in `.swite-build/` by:
|
|
93
|
-
|
|
94
|
-
1. Finding the matching workspace dependency from the discovery step.
|
|
95
|
-
2. Reading the package's `package.json` exports field to resolve the subpath.
|
|
96
|
-
3. Converting the export path from `./src/Foo.uix` to `.swite-build/<depRelPath>/src/Foo.tsx`.
|
|
97
|
-
4. Falling back to `src/index.js` if exports resolution fails.
|
|
1
|
+
<!--
|
|
2
|
+
Copyright (c) 2024 Themba Mzumara
|
|
3
|
+
SWITE - SWISS Development Server
|
|
4
|
+
Licensed under the MIT License.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Build Pipeline
|
|
8
|
+
|
|
9
|
+
`SwiteBuilder` (`src/build-engine/builder.ts`) handles production builds. It is invoked by `swite build` with a fixed entry point of `src/index.ui` and output directory of `dist/`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
The build runs three sequential phases inside a try/finally block. The temporary directory `.swite-build/` is always removed when the build finishes, whether it succeeded or failed.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
swite build
|
|
19
|
+
└── SwiteBuilder.build()
|
|
20
|
+
├── 1. cleanOutputDir() — rm -rf dist/, mkdir dist/
|
|
21
|
+
├── 2. compileSwissFiles() — @swissjs/compiler → .tsx in .swite-build/
|
|
22
|
+
├── 3. bundle() — esbuild bundles from .swite-build/
|
|
23
|
+
└── 4. copyPublicAssets() — cp -r public/ dist/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Phase 1: Compile Swiss files
|
|
29
|
+
|
|
30
|
+
`compileSwissFiles` traverses `src/` and every workspace dependency's `src/` directory.
|
|
31
|
+
|
|
32
|
+
For each `.ui` or `.uix` file:
|
|
33
|
+
|
|
34
|
+
1. `UiCompiler.compileAsync(source, filePath)` — transforms SwissJS component syntax to TypeScript/JSX.
|
|
35
|
+
2. Relative `.ui`/`.uix` imports in the compiled output are rewritten to `.tsx` (esbuild needs `.tsx` for JSX, not `.ui`).
|
|
36
|
+
3. If the compiled output ends with `export { Foo }`, a `export default Foo` line is appended so default imports resolve at bundle time.
|
|
37
|
+
4. Written to `.swite-build/<relative-path>.tsx`.
|
|
38
|
+
|
|
39
|
+
For each `.ts` file, `.ui`/`.uix` imports in `from` clauses are rewritten to `.tsx`, then the file is copied as-is.
|
|
40
|
+
|
|
41
|
+
CSS files are copied verbatim so that any CSS import stubs in the bundle phase can resolve.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Phase 2: Bundle with esbuild
|
|
46
|
+
|
|
47
|
+
esbuild is invoked with:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
{
|
|
51
|
+
bundle: true,
|
|
52
|
+
format: 'esm',
|
|
53
|
+
target: 'es2020',
|
|
54
|
+
minify: true,
|
|
55
|
+
sourcemap: false,
|
|
56
|
+
splitting: true, // ESM code splitting
|
|
57
|
+
metafile: true,
|
|
58
|
+
platform: 'node',
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Three plugins are registered (see [CLI / build](../cli/build.md) for descriptions). Node built-in modules are marked external. The `absWorkingDir` is set to the workspace root (or app root if no workspace is detected) so esbuild resolves node_modules correctly.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Phase 3: Copy public assets
|
|
67
|
+
|
|
68
|
+
`public/` is copied recursively to `dist/`. If `public/` does not exist, this phase is skipped silently.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Workspace dependency discovery
|
|
73
|
+
|
|
74
|
+
`discoverWorkspaceDependencies()` reads the app's `package.json` and collects all `workspace:*` entries from `dependencies`, `devDependencies`, and `peerDependencies`. It also scans source files for `@scope/pkg` import patterns to catch transitive workspace imports not listed in `package.json`.
|
|
75
|
+
|
|
76
|
+
For each candidate package name, it searches these directories under the workspace root:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
lib/<pkgName>
|
|
80
|
+
packages/<pkgName>
|
|
81
|
+
packages/runtime/<pkgName>
|
|
82
|
+
packages/plugins/<pkgName>
|
|
83
|
+
packages/domain/<pkgName>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A match requires the directory to have a `package.json` whose `name` field matches the expected package name, and a `src/` subdirectory.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Workspace resolver plugin
|
|
91
|
+
|
|
92
|
+
The `workspace-resolver` esbuild plugin handles `@scope/pkg` imports during bundling. It maps each import to the compiled `.tsx` files in `.swite-build/` by:
|
|
93
|
+
|
|
94
|
+
1. Finding the matching workspace dependency from the discovery step.
|
|
95
|
+
2. Reading the package's `package.json` exports field to resolve the subpath.
|
|
96
|
+
3. Converting the export path from `./src/Foo.uix` to `.swite-build/<depRelPath>/src/Foo.tsx`.
|
|
97
|
+
4. Falling back to `src/index.js` if exports resolution fails.
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Copyright (c) 2024 Themba Mzumara
|
|
3
|
-
SWITE - SWISS Development Server
|
|
4
|
-
Licensed under the MIT License.
|
|
5
|
-
-->
|
|
6
|
-
|
|
7
|
-
# Dev Server
|
|
8
|
-
|
|
9
|
-
`SwiteServer` (in `src/dev-engine/server.ts`) is the Express application that handles all incoming requests during development.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Startup sequence
|
|
14
|
-
|
|
15
|
-
When `server.start()` is called:
|
|
16
|
-
|
|
17
|
-
1. **Symlink registry** — Scans `node_modules` directories at the app root, one level up from the app root, the workspace root (if detected), and the co-located framework monorepo root. Registers every symlink target so that absolute filesystem paths obtained via `fs.realpath()` can later be mapped back to browser-relative URLs.
|
|
18
|
-
|
|
19
|
-
2. **Middleware setup** — Calls `setupMiddleware()`, which registers the full middleware chain (see below) and initializes the file router, import map, and env variable loading.
|
|
20
|
-
|
|
21
|
-
3. **HMR** — Calls `hmr.initialize()` (port probe and WebSocket server creation) then `hmr.start()` (chokidar watcher).
|
|
22
|
-
|
|
23
|
-
4. **HTTP listen** — Binds to the configured port. When `host` is `"localhost"`, SWITE binds to `0.0.0.0` so both `::1` and `127.0.0.1` respond.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## Workspace root detection
|
|
28
|
-
|
|
29
|
-
`SwiteServer.findWorkspaceRoot(startDir)` walks up the directory tree, up to six levels, looking for:
|
|
30
|
-
|
|
31
|
-
- A `pnpm-workspace.yaml` file, or
|
|
32
|
-
- A `package.json` with a `workspaces` field
|
|
33
|
-
|
|
34
|
-
If found, that directory is the workspace root. The workspace root is used to locate `node_modules`, workspace packages, and the import map.
|
|
35
|
-
|
|
36
|
-
`SwiteConfig.rootDir` can be set to override auto-detection.
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Middleware chain
|
|
41
|
-
|
|
42
|
-
The middleware is registered in a fixed order that Express processes top-to-bottom:
|
|
43
|
-
|
|
44
|
-
| Priority | Path | Handler |
|
|
45
|
-
|----------|------|---------|
|
|
46
|
-
| 1 | all | File router + HMR routes |
|
|
47
|
-
| 2 | `/packages` | TypeScript and JavaScript handler (workspace source files) |
|
|
48
|
-
| 3 | `/src` | `.ui`, `.uix`, `.ts`, `.js`, `.mjs`, static (CSS, images) |
|
|
49
|
-
| 4 | `/lib` | `.ui`, `.uix` source files (pre-static guard) |
|
|
50
|
-
| 5 | all | `.ui`/`.uix` MIME-type guard |
|
|
51
|
-
| 6 | `/.skltn/modules.css` | Returns 204 (CSS not bundled in dev) |
|
|
52
|
-
| 7 | static | `public/`, `node_modules/`, `lib/`, `libraries/`, `modules/` |
|
|
53
|
-
| 8 | all | General source-file transformation for all other paths |
|
|
54
|
-
| 9 | all | SPA fallback — serves `public/index.html` for HTML-accepting requests |
|
|
55
|
-
|
|
56
|
-
The SPA fallback refuses to serve HTML for paths under `/src/`, `/swiss-packages/`, and `/lib/` — those return 404 if they reach the fallback, preventing MIME mismatch errors.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## File handlers
|
|
61
|
-
|
|
62
|
-
Each file type has a dedicated handler class that extends `BaseHandler`:
|
|
63
|
-
|
|
64
|
-
| Handler | Extension(s) | Behaviour |
|
|
65
|
-
|---------|-------------|-----------|
|
|
66
|
-
| `UIHandler` | `.ui` | Compiles via `UiCompiler`, strips TypeScript via esbuild, fixes swiss-lib paths, inlines `import.meta.env`, strips CSS imports, rewrites bare imports |
|
|
67
|
-
| `UIXHandler` | `.uix` | Same pipeline as UIHandler |
|
|
68
|
-
| `TSHandler` | `.ts` | esbuild TypeScript strip, inlines env, rewrites imports. Falls back to `.ui` or `.uix` if the `.ts` file does not exist |
|
|
69
|
-
| `JSHandler` | `.js` | Rewrites imports. Falls back to `.ts`, `.ui`, `.uix` if `.js` does not exist |
|
|
70
|
-
| `MJSHandler` | `.mjs` | Rewrites imports. Falls back to `.js` handler |
|
|
71
|
-
| `NodeModuleHandler` | `/node_modules/…` | Walks up the directory tree to find the package. Serves without import rewriting. Falls back to jsDelivr CDN redirect if not found locally |
|
|
72
|
-
|
|
73
|
-
All handlers check an in-memory compilation cache before compiling. Cached entries are keyed by file path and invalidated when the file's mtime changes.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Static file serving
|
|
78
|
-
|
|
79
|
-
`setupStaticFiles` registers `express.static` for:
|
|
80
|
-
|
|
81
|
-
- `public/` at the root URL
|
|
82
|
-
- `node_modules/` at `/node_modules/` (app root and workspace root)
|
|
83
|
-
- `lib/` at `/lib/` (resolved by walking up from app root looking for a directory that has both `pnpm-workspace.yaml` and `lib/`)
|
|
84
|
-
- `libraries/` at `/libraries/` (workspace root, legacy)
|
|
85
|
-
- `modules/` at `/modules/` (workspace root, CSS and assets only)
|
|
86
|
-
|
|
87
|
-
Source files (`.ui`, `.uix`, `.ts`, `.js`, `.mjs`) are excluded from `express.static` by a guard middleware registered before each static mount. This ensures they always go through the compiler pipeline.
|
|
1
|
+
<!--
|
|
2
|
+
Copyright (c) 2024 Themba Mzumara
|
|
3
|
+
SWITE - SWISS Development Server
|
|
4
|
+
Licensed under the MIT License.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Dev Server
|
|
8
|
+
|
|
9
|
+
`SwiteServer` (in `src/dev-engine/server.ts`) is the Express application that handles all incoming requests during development.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Startup sequence
|
|
14
|
+
|
|
15
|
+
When `server.start()` is called:
|
|
16
|
+
|
|
17
|
+
1. **Symlink registry** — Scans `node_modules` directories at the app root, one level up from the app root, the workspace root (if detected), and the co-located framework monorepo root. Registers every symlink target so that absolute filesystem paths obtained via `fs.realpath()` can later be mapped back to browser-relative URLs.
|
|
18
|
+
|
|
19
|
+
2. **Middleware setup** — Calls `setupMiddleware()`, which registers the full middleware chain (see below) and initializes the file router, import map, and env variable loading.
|
|
20
|
+
|
|
21
|
+
3. **HMR** — Calls `hmr.initialize()` (port probe and WebSocket server creation) then `hmr.start()` (chokidar watcher).
|
|
22
|
+
|
|
23
|
+
4. **HTTP listen** — Binds to the configured port. When `host` is `"localhost"`, SWITE binds to `0.0.0.0` so both `::1` and `127.0.0.1` respond.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Workspace root detection
|
|
28
|
+
|
|
29
|
+
`SwiteServer.findWorkspaceRoot(startDir)` walks up the directory tree, up to six levels, looking for:
|
|
30
|
+
|
|
31
|
+
- A `pnpm-workspace.yaml` file, or
|
|
32
|
+
- A `package.json` with a `workspaces` field
|
|
33
|
+
|
|
34
|
+
If found, that directory is the workspace root. The workspace root is used to locate `node_modules`, workspace packages, and the import map.
|
|
35
|
+
|
|
36
|
+
`SwiteConfig.rootDir` can be set to override auto-detection.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Middleware chain
|
|
41
|
+
|
|
42
|
+
The middleware is registered in a fixed order that Express processes top-to-bottom:
|
|
43
|
+
|
|
44
|
+
| Priority | Path | Handler |
|
|
45
|
+
|----------|------|---------|
|
|
46
|
+
| 1 | all | File router + HMR routes |
|
|
47
|
+
| 2 | `/packages` | TypeScript and JavaScript handler (workspace source files) |
|
|
48
|
+
| 3 | `/src` | `.ui`, `.uix`, `.ts`, `.js`, `.mjs`, static (CSS, images) |
|
|
49
|
+
| 4 | `/lib` | `.ui`, `.uix` source files (pre-static guard) |
|
|
50
|
+
| 5 | all | `.ui`/`.uix` MIME-type guard |
|
|
51
|
+
| 6 | `/.skltn/modules.css` | Returns 204 (CSS not bundled in dev) |
|
|
52
|
+
| 7 | static | `public/`, `node_modules/`, `lib/`, `libraries/`, `modules/` |
|
|
53
|
+
| 8 | all | General source-file transformation for all other paths |
|
|
54
|
+
| 9 | all | SPA fallback — serves `public/index.html` for HTML-accepting requests |
|
|
55
|
+
|
|
56
|
+
The SPA fallback refuses to serve HTML for paths under `/src/`, `/swiss-packages/`, and `/lib/` — those return 404 if they reach the fallback, preventing MIME mismatch errors.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## File handlers
|
|
61
|
+
|
|
62
|
+
Each file type has a dedicated handler class that extends `BaseHandler`:
|
|
63
|
+
|
|
64
|
+
| Handler | Extension(s) | Behaviour |
|
|
65
|
+
|---------|-------------|-----------|
|
|
66
|
+
| `UIHandler` | `.ui` | Compiles via `UiCompiler`, strips TypeScript via esbuild, fixes swiss-lib paths, inlines `import.meta.env`, strips CSS imports, rewrites bare imports |
|
|
67
|
+
| `UIXHandler` | `.uix` | Same pipeline as UIHandler |
|
|
68
|
+
| `TSHandler` | `.ts` | esbuild TypeScript strip, inlines env, rewrites imports. Falls back to `.ui` or `.uix` if the `.ts` file does not exist |
|
|
69
|
+
| `JSHandler` | `.js` | Rewrites imports. Falls back to `.ts`, `.ui`, `.uix` if `.js` does not exist |
|
|
70
|
+
| `MJSHandler` | `.mjs` | Rewrites imports. Falls back to `.js` handler |
|
|
71
|
+
| `NodeModuleHandler` | `/node_modules/…` | Walks up the directory tree to find the package. Serves without import rewriting. Falls back to jsDelivr CDN redirect if not found locally |
|
|
72
|
+
|
|
73
|
+
All handlers check an in-memory compilation cache before compiling. Cached entries are keyed by file path and invalidated when the file's mtime changes.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Static file serving
|
|
78
|
+
|
|
79
|
+
`setupStaticFiles` registers `express.static` for:
|
|
80
|
+
|
|
81
|
+
- `public/` at the root URL
|
|
82
|
+
- `node_modules/` at `/node_modules/` (app root and workspace root)
|
|
83
|
+
- `lib/` at `/lib/` (resolved by walking up from app root looking for a directory that has both `pnpm-workspace.yaml` and `lib/`)
|
|
84
|
+
- `libraries/` at `/libraries/` (workspace root, legacy)
|
|
85
|
+
- `modules/` at `/modules/` (workspace root, CSS and assets only)
|
|
86
|
+
|
|
87
|
+
Source files (`.ui`, `.uix`, `.ts`, `.js`, `.mjs`) are excluded from `express.static` by a guard middleware registered before each static mount. This ensures they always go through the compiler pipeline.
|
package/docs/architecture/hmr.md
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Copyright (c) 2024 Themba Mzumara
|
|
3
|
-
SWITE - SWISS Development Server
|
|
4
|
-
Licensed under the MIT License.
|
|
5
|
-
-->
|
|
6
|
-
|
|
7
|
-
# HMR
|
|
8
|
-
|
|
9
|
-
`HMREngine` (`src/dev-engine/hmr/hmr.ts`) manages the WebSocket server and file watcher that power hot module replacement.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## WebSocket server
|
|
14
|
-
|
|
15
|
-
The WebSocket server is created by the `ws` package. The default port is 24678. On initialization, SWITE checks whether that port is available by attempting to bind a temporary TCP server. If the port is occupied, `findFreePort()` asks the OS for an available port by binding to port 0 and reading the assigned port back.
|
|
16
|
-
|
|
17
|
-
All connected browser clients are tracked in a `Set<WebSocket>`. When a client disconnects, it is removed from the set.
|
|
18
|
-
|
|
19
|
-
The HMR client script is served at `/__swite_hmr_client` as plain JavaScript. The actual WebSocket port is embedded into this script at server startup so the browser client connects to the correct port even when it differs from 24678.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## File watcher
|
|
24
|
-
|
|
25
|
-
`HMREngine.start()` creates a chokidar watcher on the project root with these options:
|
|
26
|
-
|
|
27
|
-
- Ignored: `node_modules/`, `.git/`, `dist/`
|
|
28
|
-
- `ignoreInitial: true` — no events are fired for files that exist at startup
|
|
29
|
-
- `awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }` — waits for the file to stop changing before firing, which prevents partial-write events
|
|
30
|
-
|
|
31
|
-
On each `change` event, SWITE determines the update type from the file extension and broadcasts a message to all connected clients.
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## Update type classification
|
|
36
|
-
|
|
37
|
-
| File extension | Update type | Browser action |
|
|
38
|
-
|---------------|-------------|----------------|
|
|
39
|
-
| `.css`, `.scss`, `.sass` | `style` | Cache-busts `href` on all `<link rel="stylesheet">` elements by appending `?t={timestamp}` |
|
|
40
|
-
| `.js`, `.ts` in `components/` or `pages/` | `hot` | Re-imports the module and calls `instance.update(newModule)` on registered component instances |
|
|
41
|
-
| Everything else | `reload` | `window.location.reload()` |
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## Broadcast message format
|
|
46
|
-
|
|
47
|
-
```json
|
|
48
|
-
{
|
|
49
|
-
"type": "update",
|
|
50
|
-
"path": "/absolute/path/to/changed/file",
|
|
51
|
-
"updateType": "hot | reload | style",
|
|
52
|
-
"timestamp": 1716211234567
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Messages are sent only to clients whose `readyState` is `WebSocket.OPEN`. Clients in any other state are skipped.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## HMR client
|
|
61
|
-
|
|
62
|
-
`buildHmrClientScript(port)` (`src/dev-engine/hmr/hmr-client-template.ts`) returns the browser-side JavaScript as a string with the port number substituted at runtime.
|
|
63
|
-
|
|
64
|
-
The client:
|
|
65
|
-
|
|
66
|
-
1. Opens a WebSocket connection to `ws://{hostname}:{port}`.
|
|
67
|
-
2. On `message`, dispatches to `updateStyles()`, the hot-update path, or `window.location.reload()`.
|
|
68
|
-
3. Maintains a `moduleGraph` map (module name → dependents) and a `hotModules` map (module name → module object) for the hot path.
|
|
69
|
-
4. On hot update: removes the module from `window.__swiss_modules__`, re-imports it with a cache-busting `?t={timestamp}` suffix, and calls `instance.update(newModule)` on each registered instance in `window.__swiss_instances__`.
|
|
70
|
-
5. If the hot update throws, falls back to `window.location.reload()`.
|
|
71
|
-
|
|
72
|
-
`window.__swiss_modules__` and `window.__swiss_instances__` are initialized to empty objects if not already present. The SwissJS runtime populates `__swiss_instances__` with component instance arrays keyed by component name.
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## notifyChange
|
|
77
|
-
|
|
78
|
-
`HMREngine.notifyChange(filePath)` is a programmatic interface for triggering HMR broadcasts without a file system event. It is available for use by other server-side logic (such as the file router) that needs to signal a module change.
|
|
1
|
+
<!--
|
|
2
|
+
Copyright (c) 2024 Themba Mzumara
|
|
3
|
+
SWITE - SWISS Development Server
|
|
4
|
+
Licensed under the MIT License.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# HMR
|
|
8
|
+
|
|
9
|
+
`HMREngine` (`src/dev-engine/hmr/hmr.ts`) manages the WebSocket server and file watcher that power hot module replacement.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## WebSocket server
|
|
14
|
+
|
|
15
|
+
The WebSocket server is created by the `ws` package. The default port is 24678. On initialization, SWITE checks whether that port is available by attempting to bind a temporary TCP server. If the port is occupied, `findFreePort()` asks the OS for an available port by binding to port 0 and reading the assigned port back.
|
|
16
|
+
|
|
17
|
+
All connected browser clients are tracked in a `Set<WebSocket>`. When a client disconnects, it is removed from the set.
|
|
18
|
+
|
|
19
|
+
The HMR client script is served at `/__swite_hmr_client` as plain JavaScript. The actual WebSocket port is embedded into this script at server startup so the browser client connects to the correct port even when it differs from 24678.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## File watcher
|
|
24
|
+
|
|
25
|
+
`HMREngine.start()` creates a chokidar watcher on the project root with these options:
|
|
26
|
+
|
|
27
|
+
- Ignored: `node_modules/`, `.git/`, `dist/`
|
|
28
|
+
- `ignoreInitial: true` — no events are fired for files that exist at startup
|
|
29
|
+
- `awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }` — waits for the file to stop changing before firing, which prevents partial-write events
|
|
30
|
+
|
|
31
|
+
On each `change` event, SWITE determines the update type from the file extension and broadcasts a message to all connected clients.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Update type classification
|
|
36
|
+
|
|
37
|
+
| File extension | Update type | Browser action |
|
|
38
|
+
|---------------|-------------|----------------|
|
|
39
|
+
| `.css`, `.scss`, `.sass` | `style` | Cache-busts `href` on all `<link rel="stylesheet">` elements by appending `?t={timestamp}` |
|
|
40
|
+
| `.js`, `.ts` in `components/` or `pages/` | `hot` | Re-imports the module and calls `instance.update(newModule)` on registered component instances |
|
|
41
|
+
| Everything else | `reload` | `window.location.reload()` |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Broadcast message format
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"type": "update",
|
|
50
|
+
"path": "/absolute/path/to/changed/file",
|
|
51
|
+
"updateType": "hot | reload | style",
|
|
52
|
+
"timestamp": 1716211234567
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Messages are sent only to clients whose `readyState` is `WebSocket.OPEN`. Clients in any other state are skipped.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## HMR client
|
|
61
|
+
|
|
62
|
+
`buildHmrClientScript(port)` (`src/dev-engine/hmr/hmr-client-template.ts`) returns the browser-side JavaScript as a string with the port number substituted at runtime.
|
|
63
|
+
|
|
64
|
+
The client:
|
|
65
|
+
|
|
66
|
+
1. Opens a WebSocket connection to `ws://{hostname}:{port}`.
|
|
67
|
+
2. On `message`, dispatches to `updateStyles()`, the hot-update path, or `window.location.reload()`.
|
|
68
|
+
3. Maintains a `moduleGraph` map (module name → dependents) and a `hotModules` map (module name → module object) for the hot path.
|
|
69
|
+
4. On hot update: removes the module from `window.__swiss_modules__`, re-imports it with a cache-busting `?t={timestamp}` suffix, and calls `instance.update(newModule)` on each registered instance in `window.__swiss_instances__`.
|
|
70
|
+
5. If the hot update throws, falls back to `window.location.reload()`.
|
|
71
|
+
|
|
72
|
+
`window.__swiss_modules__` and `window.__swiss_instances__` are initialized to empty objects if not already present. The SwissJS runtime populates `__swiss_instances__` with component instance arrays keyed by component name.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## notifyChange
|
|
77
|
+
|
|
78
|
+
`HMREngine.notifyChange(filePath)` is a programmatic interface for triggering HMR broadcasts without a file system event. It is available for use by other server-side logic (such as the file router) that needs to signal a module change.
|