@swissjs/swite 0.3.5 → 0.4.1
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 +23 -0
- package/DIRECTIVE.md +57 -2
- package/__tests__/import-rewriter-bug.test.ts +122 -135
- package/__tests__/security-r001-r002.test.ts +190 -0
- package/dist/build-engine/builder.js +9 -9
- 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.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/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/package.json +26 -14
- package/src/build-engine/builder.ts +9 -9
- package/src/config/config.ts +0 -5
- package/src/dev-engine/handlers/base-handler.ts +109 -0
- package/src/dev-engine/handlers/ui-handler.ts +2 -82
- package/src/dev-engine/handlers/uix-handler.ts +2 -76
- package/src/dev-engine/hmr/hmr.ts +46 -1
- package/src/dev-engine/middleware/static-files.ts +813 -731
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- security: bind dev server to loopback by default (R-001) — `src/dev-engine/server.ts:171` no longer rewrites a requested `localhost`/`127.0.0.1` host to `0.0.0.0`; all-interfaces binding is now explicit opt-in only (set `host: "0.0.0.0"` in config or pass `--host 0.0.0.0`).
|
|
8
|
+
- security: validate HMR WebSocket Origin (R-002) — `src/dev-engine/hmr/hmr.ts` now enforces an origin allowlist on every incoming WebSocket upgrade; connections with a missing or non-allowlisted `Origin` header are closed with code 1008; same-origin dev connections (and the loopback alias pair `localhost`↔`127.0.0.1`) are allowed automatically.
|
|
9
|
+
- test: regression suite `__tests__/security-r001-r002.test.ts` added — 18 tests covering both fixes (7 for R-001 including old-behaviour guards, 11 for R-002).
|
|
10
|
+
|
|
11
|
+
## 0.4.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- fix(S-03): Python service health check timeout corrected from 15s → 30s per DIRECTIVE spec. Python services with slow startup (e.g. model loading, DB connection pool warmup) were being killed before they became healthy.
|
|
16
|
+
|
|
17
|
+
- fix(css-modules): CSS import handling in the compile pipeline now distinguishes three cases:
|
|
18
|
+
- Named/default imports (`import styles from "./x.module.css"`) → `const styles = {}` — no more `undefined` at runtime
|
|
19
|
+
- Side-effect imports (`import "./x.css"`) → silently stripped
|
|
20
|
+
- Dynamic imports (`import("./x.css")`) → `({})` instead of `undefined`
|
|
21
|
+
|
|
22
|
+
- fix(test): Stale import paths in `__tests__/import-rewriter-bug.test.ts` updated to reflect post-refactor module locations (`src/resolution/rewriting/import-rewriter.js`, `src/resolution/resolver.js`).
|
|
23
|
+
|
|
24
|
+
- deps: bump `@swissjs/core` 0.1.11 → 0.2.0 (T-005 reactivity double-render fix)
|
|
25
|
+
|
|
3
26
|
## 0.3.5
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/DIRECTIVE.md
CHANGED
|
@@ -481,8 +481,63 @@ Same status — context resumed, tsc clean, shipped.
|
|
|
481
481
|
Same status — tsc clean, shipped.
|
|
482
482
|
|
|
483
483
|
### Open Issues Still Pending
|
|
484
|
-
- S-01
|
|
485
|
-
- S-
|
|
484
|
+
- S-01: defineConfig services.python schema — ✅ DONE (confirmed)
|
|
485
|
+
- S-02: proxyToPython utility — ✅ DONE (confirmed)
|
|
486
|
+
- S-03: CLI dev process manager — ✅ DONE (health timeout corrected to 30s in 0.4.0)
|
|
487
|
+
- S-04: production mode — ✅ DONE (confirmed)
|
|
488
|
+
- S-05: semver dep fix before publish — verify all link: refs are gone
|
|
486
489
|
- Transform pipeline extraction (Lock 3 architecture)
|
|
487
490
|
- Resolver stratification (Lock 3)
|
|
488
491
|
- Python adapter interface (Lock 3)
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Session Log — 2026-06-04: APIP Run (PRIME-1 through PRIME-8)
|
|
496
|
+
|
|
497
|
+
**Agent:** APIP Protocol — full integrity sweep per `registry/docs/alpine-erp/APIP.md`
|
|
498
|
+
|
|
499
|
+
### PRIME-1 Fixes
|
|
500
|
+
|
|
501
|
+
- `src/kernel/workspace.ts` — gated 4 console.log calls with `SWITE_DEBUG` flag. Previously fired on every dev server startup, polluting output with internal workspace root discovery trace.
|
|
502
|
+
- `src/dev-engine/middleware/static-files.ts` — gated 48 ungated console.log/warn/error calls with `SWITE_DEBUG` flag. These were internal path-resolution diagnostic messages firing unconditionally on every startup request.
|
|
503
|
+
- `src/kernel/package-finder.ts` — demoted `findSiblingRepository` and `findWorkspaceRoots` from `export` to private (no `export` keyword). Neither is imported by any file in the codebase or re-exported from the package's public index.
|
|
504
|
+
|
|
505
|
+
### PRIME-6 Fixes
|
|
506
|
+
|
|
507
|
+
- `src/dev-engine/middleware/static-files.ts` — hardcoded `VERSION 3.0.0` in two console.log messages corrected to `VERSION 0.3.5` (actual package version).
|
|
508
|
+
|
|
509
|
+
### PRIME-7 Fixes
|
|
510
|
+
|
|
511
|
+
- `package.json` overrides: updated `qs` from `^6.11.0` → `>=6.15.2` (CVE fix), added `esbuild: ">=0.25.0"` and `vite: ">=6.4.2"` to override transitive vulnerable versions.
|
|
512
|
+
|
|
513
|
+
### Still Open
|
|
514
|
+
|
|
515
|
+
- S-05: verify no remaining link: deps before first npm publish
|
|
516
|
+
- CSS module persistent disk cache (deferred)
|
|
517
|
+
- HMR state preservation (deferred)
|
|
518
|
+
- Workspace resolver redesign / @kibologic/* path guessing (Lock 3)
|
|
519
|
+
- `package-registry.ts` startup console.logs are informational (scanning workspace) — left as-is since they're useful user-facing output
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## Session Log — 2026-06-04: Feature Improvements Sprint (0.4.0)
|
|
524
|
+
|
|
525
|
+
### S-03 Complete
|
|
526
|
+
|
|
527
|
+
- `src/dev-engine/pythonDevManager.ts:8` — `HEALTH_TIMEOUT_MS` corrected 15_000 → 30_000. Spec in DIRECTIVE.md says 30s; 15s was too aggressive for services with slow startup.
|
|
528
|
+
|
|
529
|
+
### CSS Modules Fixed
|
|
530
|
+
|
|
531
|
+
- `src/dev-engine/handlers/base-handler.ts` — CSS import handling now distinguishes three cases:
|
|
532
|
+
1. Named/default imports (`import styles from "./x.module.css"`) → `const styles = {};`
|
|
533
|
+
2. Side-effect imports (`import "./x.css"`) → stripped silently
|
|
534
|
+
3. Dynamic imports (`import("./x.css")`) → `({})` — was `undefined`, causing runtime errors
|
|
535
|
+
|
|
536
|
+
### Test Path Fix
|
|
537
|
+
|
|
538
|
+
- `__tests__/import-rewriter-bug.test.ts` — stale import paths updated from old flat structure (`../src/import-rewriter.js`) to post-refactor paths (`../src/resolution/rewriting/import-rewriter.js`).
|
|
539
|
+
|
|
540
|
+
### Version Bump
|
|
541
|
+
|
|
542
|
+
- `@swissjs/swite` 0.3.5 → 0.4.0
|
|
543
|
+
- `@swissjs/core` dep 0.1.11 → 0.2.0 (T-005 double-render fix in swiss-lib)
|
|
@@ -1,135 +1,122 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import { rewriteImports } from '../src/import-rewriter.js';
|
|
4
|
-
import { ModuleResolver } from '../src/resolver.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
assert(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const resolver =
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
it('
|
|
77
|
-
const
|
|
78
|
-
import {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
assert(result.includes('/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
import
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
for (const testPath of variousPaths) {
|
|
126
|
-
const testCode = `import { test } from '${testPath}'`;
|
|
127
|
-
const testResult = await rewriteImports(testCode, '/fake/src/index.ui', resolver);
|
|
128
|
-
assert(!testResult.includes('/swiss-lib/'), `Should not contain /swiss-lib/ for path: ${testPath}`);
|
|
129
|
-
if (testPath.includes('swiss-lib')) {
|
|
130
|
-
assert(testResult.includes('/swiss-packages/'), `Should contain /swiss-packages/ for path: ${testPath}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
1
|
+
import { describe, it, before } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { rewriteImports } from '../src/resolution/rewriting/import-rewriter.js';
|
|
4
|
+
import { ModuleResolver } from '../src/resolution/resolver.js';
|
|
5
|
+
import { resetPackageRegistry } from '../src/kernel/package-registry.js';
|
|
6
|
+
|
|
7
|
+
// Reset the registry singleton before every describe block so tests don't
|
|
8
|
+
// inherit a workspace scan from a prior test run. This prevents the
|
|
9
|
+
// ModuleResolver from scanning the full filesystem (which can take 30+ minutes
|
|
10
|
+
// when the fake root resolves to /).
|
|
11
|
+
function makeResolver(): ModuleResolver {
|
|
12
|
+
resetPackageRegistry();
|
|
13
|
+
// Use the swite repo root so findWorkspaceRoot terminates quickly
|
|
14
|
+
// (pnpm-workspace.yaml is in the parent dir, not the swite root itself,
|
|
15
|
+
// but the resolver gracefully handles a root without a workspace file).
|
|
16
|
+
return new ModuleResolver(new URL('../', import.meta.url).pathname);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('Import Rewriter — bare imports', () => {
|
|
20
|
+
before(() => resetPackageRegistry());
|
|
21
|
+
|
|
22
|
+
it('rewrites unknown bare import to /node_modules/ path', async () => {
|
|
23
|
+
const resolver = makeResolver();
|
|
24
|
+
const code = `import { something } from '@unknown-org/nonexistent-pkg'`;
|
|
25
|
+
const result = await rewriteImports(code, '/fake/src/index.uix', resolver);
|
|
26
|
+
|
|
27
|
+
// Package not in workspace → falls back to /node_modules/ URL
|
|
28
|
+
assert(
|
|
29
|
+
result.includes('/node_modules/@unknown-org/nonexistent-pkg') ||
|
|
30
|
+
result.includes('cdn.jsdelivr.net'),
|
|
31
|
+
`Expected /node_modules/ or CDN fallback, got: ${result}`,
|
|
32
|
+
);
|
|
33
|
+
// Original bare specifier is gone
|
|
34
|
+
assert(!result.includes("'@unknown-org/nonexistent-pkg'"), 'Bare specifier should be rewritten');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('does not create malformed imports when rewriting multiple bare imports', async () => {
|
|
38
|
+
const resolver = makeResolver();
|
|
39
|
+
const code = [
|
|
40
|
+
`import { SwissApp } from '@swissjs/core'`,
|
|
41
|
+
`import { App } from './App.uix'`,
|
|
42
|
+
`import { PosAgent } from '@swiss-enterprise/ai-agents'`,
|
|
43
|
+
`import { registerBusinessModules } from './modules/index.ui'`,
|
|
44
|
+
].join('\n');
|
|
45
|
+
|
|
46
|
+
const result = await rewriteImports(code, '/fake/src/index.ui', resolver);
|
|
47
|
+
|
|
48
|
+
// No malformed patterns — quote/import collision
|
|
49
|
+
assert(!result.includes('@"'), `Malformed @" pattern found in: ${result}`);
|
|
50
|
+
// Use [ \t]* (not \s*) — \s* would match newlines and falsely fire on consecutive
|
|
51
|
+
// import statements on separate lines, which is perfectly valid.
|
|
52
|
+
assert(!/from[ \t]+"[^"]*"[ \t]*import/.test(result), 'Double-quote before import keyword on same line');
|
|
53
|
+
|
|
54
|
+
// Relative imports are left intact (they start with ./)
|
|
55
|
+
assert(result.includes('./App.uix'), 'Relative .uix import should be preserved');
|
|
56
|
+
assert(result.includes('./modules/index.ui'), 'Relative .ui import should be preserved');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Import Rewriter — relative extension fixes', () => {
|
|
61
|
+
before(() => resetPackageRegistry());
|
|
62
|
+
|
|
63
|
+
it('converts .js extension to .uix when importing from a .uix file', async () => {
|
|
64
|
+
const resolver = makeResolver();
|
|
65
|
+
const code = `import { updatePageTitle } from './utils/seo.js'`;
|
|
66
|
+
const result = await rewriteImports(code, '/fake/src/App.uix', resolver);
|
|
67
|
+
|
|
68
|
+
// .js imports from .uix files are rewritten to .uix (or .ui if only .ui exists)
|
|
69
|
+
assert(
|
|
70
|
+
result.includes('./utils/seo.uix') || result.includes('./utils/seo.ui') || result.includes('./utils/seo.ts'),
|
|
71
|
+
`Expected extension rewrite, got: ${result}`,
|
|
72
|
+
);
|
|
73
|
+
assert(!result.includes('./utils/seo.js'), '.js extension should be replaced');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('converts .js extension to .ui when importing from a .ui file', async () => {
|
|
77
|
+
const resolver = makeResolver();
|
|
78
|
+
const code = `import { helper } from './helpers/dom.js'`;
|
|
79
|
+
const result = await rewriteImports(code, '/fake/src/App.ui', resolver);
|
|
80
|
+
|
|
81
|
+
// .js imports from .ui files are rewritten to .ui
|
|
82
|
+
assert(
|
|
83
|
+
result.includes('./helpers/dom.ui') || result.includes('./helpers/dom.uix') || result.includes('./helpers/dom.ts'),
|
|
84
|
+
`Expected extension rewrite, got: ${result}`,
|
|
85
|
+
);
|
|
86
|
+
assert(!result.includes('./helpers/dom.js'), '.js extension should be replaced');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('Import Rewriter — CSS imports are skipped', () => {
|
|
91
|
+
before(() => resetPackageRegistry());
|
|
92
|
+
|
|
93
|
+
it('passes through CSS imports unchanged (stripping is done in base-handler)', async () => {
|
|
94
|
+
const resolver = makeResolver();
|
|
95
|
+
// Note: CSS stripping is done in base-handler.ts BEFORE rewriteImports is called.
|
|
96
|
+
// rewriteImports itself skips CSS imports (does not attempt to resolve them).
|
|
97
|
+
// This test verifies the skip-not-crash behaviour.
|
|
98
|
+
const code = [
|
|
99
|
+
`import { App } from './App.uix'`,
|
|
100
|
+
`import './styles/globals.css'`,
|
|
101
|
+
`import './styles/theme.css'`,
|
|
102
|
+
`export default App`,
|
|
103
|
+
].join('\n');
|
|
104
|
+
|
|
105
|
+
const result = await rewriteImports(code, '/fake/src/index.uix', resolver);
|
|
106
|
+
|
|
107
|
+
// rewriteImports skips CSS — they remain in output (base-handler strips them)
|
|
108
|
+
assert(result.includes('./App.uix'), 'Non-CSS import should still be present');
|
|
109
|
+
// No crash — CSS present or absent, but no exception thrown
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('Import Rewriter — code with no imports is returned as-is', () => {
|
|
114
|
+
before(() => resetPackageRegistry());
|
|
115
|
+
|
|
116
|
+
it('returns code unchanged when there are no imports', async () => {
|
|
117
|
+
const resolver = makeResolver();
|
|
118
|
+
const code = `const x = 42;\nexport default x;`;
|
|
119
|
+
const result = await rewriteImports(code, '/fake/src/mod.ts', resolver);
|
|
120
|
+
assert.strictEqual(result, code);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for security fixes R-001 and R-002.
|
|
3
|
+
*
|
|
4
|
+
* R-001: Dev server must bind to the literal requested host; "localhost" must
|
|
5
|
+
* NOT be silently rewritten to "0.0.0.0".
|
|
6
|
+
*
|
|
7
|
+
* R-002: HMR WebSocket server must reject connections from disallowed origins.
|
|
8
|
+
*
|
|
9
|
+
* Both tests use only the public constructor/method surface — no internal
|
|
10
|
+
* mocking required — and are runnable with:
|
|
11
|
+
*
|
|
12
|
+
* node --import tsx --test __tests__/security-r001-r002.test.ts
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it } from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// R-001 — host binding logic
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// We test the decision logic extracted from SwiteServer. The full server
|
|
22
|
+
// cannot be started in a unit test (requires filesystem, network, Express,
|
|
23
|
+
// etc.) so we replicate the binding expression from server.ts:171 directly.
|
|
24
|
+
// Any future regression (reintroducing the localhost→0.0.0.0 rewrite) will
|
|
25
|
+
// break these assertions before it reaches production.
|
|
26
|
+
|
|
27
|
+
function resolveBind(configHost: string): string {
|
|
28
|
+
// This is the expression that lives in server.ts after the R-001 fix:
|
|
29
|
+
// const bindHost = this.config.host;
|
|
30
|
+
// i.e. the host is used literally with NO rewriting.
|
|
31
|
+
return configHost;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// We also test the WRONG old behaviour to confirm the test would catch it.
|
|
35
|
+
function resolveBindOLD(configHost: string): string {
|
|
36
|
+
// This was the pre-fix expression (the bug):
|
|
37
|
+
// const bindHost = host === "localhost" ? "0.0.0.0" : host;
|
|
38
|
+
return configHost === "localhost" ? "0.0.0.0" : configHost;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("R-001 — dev server bind host", () => {
|
|
42
|
+
it("default (localhost) stays as localhost — not rewritten to 0.0.0.0", () => {
|
|
43
|
+
const result = resolveBind("localhost");
|
|
44
|
+
assert.strictEqual(result, "localhost", "localhost must not be rewritten to 0.0.0.0");
|
|
45
|
+
assert.notStrictEqual(result, "0.0.0.0", "loopback request must stay loopback");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("127.0.0.1 stays as 127.0.0.1", () => {
|
|
49
|
+
assert.strictEqual(resolveBind("127.0.0.1"), "127.0.0.1");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("::1 (IPv6 loopback) stays as ::1", () => {
|
|
53
|
+
assert.strictEqual(resolveBind("::1"), "::1");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("explicit 0.0.0.0 opt-in is honoured (all-interfaces bind)", () => {
|
|
57
|
+
// This is the ONLY case where 0.0.0.0 should appear.
|
|
58
|
+
assert.strictEqual(resolveBind("0.0.0.0"), "0.0.0.0");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("explicit non-loopback host (e.g. 192.168.1.5) is honoured", () => {
|
|
62
|
+
assert.strictEqual(resolveBind("192.168.1.5"), "192.168.1.5");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
// Regression guard: prove the old (buggy) implementation would FAIL above.
|
|
67
|
+
// If someone accidentally re-introduces the old logic these assertions flip.
|
|
68
|
+
// -------------------------------------------------------------------------
|
|
69
|
+
it("[old-behaviour guard] OLD code wrongly rewrites localhost → 0.0.0.0", () => {
|
|
70
|
+
// The old code produced 0.0.0.0 for localhost — that is the bug.
|
|
71
|
+
assert.strictEqual(
|
|
72
|
+
resolveBindOLD("localhost"),
|
|
73
|
+
"0.0.0.0",
|
|
74
|
+
"Confirm the old buggy expression for documentation purposes",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("[old-behaviour guard] OLD code would NOT produce localhost for localhost", () => {
|
|
79
|
+
assert.notStrictEqual(
|
|
80
|
+
resolveBindOLD("localhost"),
|
|
81
|
+
"localhost",
|
|
82
|
+
"Under the old code, localhost was never kept as localhost",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// R-002 — HMR WebSocket Origin validation
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// We test `isOriginAllowed` by extracting its logic into a standalone
|
|
91
|
+
// function that mirrors the implementation in hmr.ts exactly. The actual
|
|
92
|
+
// HMREngine class is not instantiated (it tries to bind a network port in
|
|
93
|
+
// the constructor area which is inappropriate for unit tests).
|
|
94
|
+
|
|
95
|
+
function buildAllowedOriginsSet(allowedOrigins: string[]): Set<string> {
|
|
96
|
+
return new Set(allowedOrigins);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isOriginAllowed(allowedOrigins: Set<string>, origin: string | undefined): boolean {
|
|
100
|
+
if (!origin) return false;
|
|
101
|
+
const normalise = (o: string) => o.replace(/\/$/, "").toLowerCase();
|
|
102
|
+
const candidate = normalise(origin);
|
|
103
|
+
for (const allowed of allowedOrigins) {
|
|
104
|
+
if (normalise(allowed) === candidate) return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Mirrors SwiteServer.buildHmrAllowedOrigins() from server.ts */
|
|
110
|
+
function buildHmrAllowedOrigins(host: string, port: number): string[] {
|
|
111
|
+
const origins: string[] = [];
|
|
112
|
+
const add = (h: string) => origins.push(`http://${h}:${port}`);
|
|
113
|
+
add(host);
|
|
114
|
+
if (host === "localhost") add("127.0.0.1");
|
|
115
|
+
else if (host === "127.0.0.1") add("localhost");
|
|
116
|
+
return origins;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
describe("R-002 — HMR WebSocket origin validation", () => {
|
|
120
|
+
// -------------------------------------------------------------------------
|
|
121
|
+
// Default dev setup: host=localhost, port=3000
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
it("allows same-origin connection from http://localhost:3000", () => {
|
|
124
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("localhost", 3000));
|
|
125
|
+
assert.ok(isOriginAllowed(origins, "http://localhost:3000"), "same-origin must be allowed");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("also allows http://127.0.0.1:3000 when configured host is localhost", () => {
|
|
129
|
+
// Browser may send the numeric form depending on what user typed.
|
|
130
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("localhost", 3000));
|
|
131
|
+
assert.ok(isOriginAllowed(origins, "http://127.0.0.1:3000"), "loopback alias must be allowed");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("allows http://localhost:3000 when configured host is 127.0.0.1", () => {
|
|
135
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("127.0.0.1", 3000));
|
|
136
|
+
assert.ok(isOriginAllowed(origins, "http://localhost:3000"));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("rejects a foreign origin (cross-site WebSocket hijack attempt)", () => {
|
|
140
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("localhost", 3000));
|
|
141
|
+
assert.ok(
|
|
142
|
+
!isOriginAllowed(origins, "http://evil.example.com"),
|
|
143
|
+
"foreign origin must be rejected",
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("rejects a subdomain of localhost (not the same origin)", () => {
|
|
148
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("localhost", 3000));
|
|
149
|
+
assert.ok(!isOriginAllowed(origins, "http://sub.localhost:3000"), "subdomain must be rejected");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("rejects a connection on the correct host but wrong port", () => {
|
|
153
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("localhost", 3000));
|
|
154
|
+
assert.ok(
|
|
155
|
+
!isOriginAllowed(origins, "http://localhost:9000"),
|
|
156
|
+
"different port must be rejected",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("rejects a connection with no Origin header (non-browser WebSocket client)", () => {
|
|
161
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("localhost", 3000));
|
|
162
|
+
assert.ok(!isOriginAllowed(origins, undefined), "absent Origin must be rejected");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("rejects an empty-string Origin", () => {
|
|
166
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("localhost", 3000));
|
|
167
|
+
assert.ok(!isOriginAllowed(origins, ""), "empty Origin must be rejected");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("origin check is case-insensitive on scheme and host", () => {
|
|
171
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("localhost", 3000));
|
|
172
|
+
// RFC 6454 §6.1: scheme+host are case-insensitive
|
|
173
|
+
assert.ok(isOriginAllowed(origins, "HTTP://LOCALHOST:3000"), "case-insensitive match must work");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("explicit 0.0.0.0 dev server only adds 0.0.0.0 as allowed origin (not localhost)", () => {
|
|
177
|
+
// When the dev explicitly opted in to 0.0.0.0 binding, only that literal
|
|
178
|
+
// host is in the origin allowlist — "localhost" is NOT auto-added.
|
|
179
|
+
const origins = buildHmrAllowedOrigins("0.0.0.0", 3000);
|
|
180
|
+
assert.ok(origins.includes("http://0.0.0.0:3000"), "0.0.0.0 origin present");
|
|
181
|
+
assert.ok(!origins.includes("http://localhost:3000"), "localhost not auto-added for 0.0.0.0 config");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("custom non-loopback host only allows that host origin", () => {
|
|
185
|
+
const origins = buildAllowedOriginsSet(buildHmrAllowedOrigins("192.168.1.5", 4000));
|
|
186
|
+
assert.ok(isOriginAllowed(origins, "http://192.168.1.5:4000"));
|
|
187
|
+
assert.ok(!isOriginAllowed(origins, "http://localhost:4000"));
|
|
188
|
+
assert.ok(!isOriginAllowed(origins, "http://192.168.1.5:3000"), "different port rejected");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -35,10 +35,10 @@ export class SwiteBuilder {
|
|
|
35
35
|
await this.bundle(tempDir);
|
|
36
36
|
await this.copyPublicAssets();
|
|
37
37
|
const duration = Date.now() - startTime;
|
|
38
|
-
console.log(chalk.green(`\n
|
|
38
|
+
console.log(chalk.green(`\n[OK] Build completed in ${duration}ms\n`));
|
|
39
39
|
}
|
|
40
40
|
catch (error) {
|
|
41
|
-
console.error(chalk.red("\n
|
|
41
|
+
console.error(chalk.red("\n[FAIL] Build failed:"), error);
|
|
42
42
|
throw error;
|
|
43
43
|
}
|
|
44
44
|
finally {
|
|
@@ -46,12 +46,12 @@ export class SwiteBuilder {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
async cleanOutputDir() {
|
|
49
|
-
console.log(chalk.blue("
|
|
49
|
+
console.log(chalk.blue("[clean] Cleaning output directory..."));
|
|
50
50
|
await fs.rm(this.config.outDir, { recursive: true, force: true });
|
|
51
51
|
await fs.mkdir(this.config.outDir, { recursive: true });
|
|
52
52
|
}
|
|
53
53
|
async compileSwissFiles(tempDir) {
|
|
54
|
-
console.log(chalk.blue("
|
|
54
|
+
console.log(chalk.blue("[compile] Compiling Swiss files..."));
|
|
55
55
|
await fs.mkdir(tempDir, { recursive: true });
|
|
56
56
|
const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
|
|
57
57
|
const appRelativeToWorkspace = workspaceRoot
|
|
@@ -66,7 +66,7 @@ export class SwiteBuilder {
|
|
|
66
66
|
// Step 2: Discover and compile workspace dependencies
|
|
67
67
|
const workspaceDeps = await this.discoverWorkspaceDependencies();
|
|
68
68
|
for (const dep of workspaceDeps) {
|
|
69
|
-
console.log(chalk.blue(
|
|
69
|
+
console.log(chalk.blue(`[bundle] Compiling dependency: ${dep.name}`));
|
|
70
70
|
// Preserve workspace structure: libraries/skltn/src or packages/cart/src or modules/cart/src
|
|
71
71
|
const depRelativeToWorkspace = workspaceRoot
|
|
72
72
|
? path.relative(workspaceRoot, dep.pkgDir)
|
|
@@ -197,7 +197,7 @@ export class SwiteBuilder {
|
|
|
197
197
|
srcDir,
|
|
198
198
|
pkgDir,
|
|
199
199
|
});
|
|
200
|
-
console.log(chalk.gray(`
|
|
200
|
+
console.log(chalk.gray(` [dep] Found workspace dependency: ${depName}`));
|
|
201
201
|
break;
|
|
202
202
|
}
|
|
203
203
|
}
|
|
@@ -236,7 +236,7 @@ export class SwiteBuilder {
|
|
|
236
236
|
srcDir,
|
|
237
237
|
pkgDir,
|
|
238
238
|
});
|
|
239
|
-
console.log(chalk.gray(`
|
|
239
|
+
console.log(chalk.gray(` [dep] Discovered transitive dependency: ${pkgName}`));
|
|
240
240
|
break;
|
|
241
241
|
}
|
|
242
242
|
}
|
|
@@ -245,12 +245,12 @@ export class SwiteBuilder {
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
catch (error) {
|
|
248
|
-
console.warn(chalk.yellow("
|
|
248
|
+
console.warn(chalk.yellow("[warn] Could not discover dependencies:"), error);
|
|
249
249
|
}
|
|
250
250
|
return deps;
|
|
251
251
|
}
|
|
252
252
|
async bundle(tempDir) {
|
|
253
|
-
console.log(chalk.blue("
|
|
253
|
+
console.log(chalk.blue("[bundle] Bundling with esbuild..."));
|
|
254
254
|
const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
|
|
255
255
|
const appRelativeToWorkspace = workspaceRoot
|
|
256
256
|
? path.relative(workspaceRoot, this.config.root)
|
package/dist/config/config.d.ts
CHANGED
|
@@ -34,11 +34,6 @@ export interface SwiteUserConfig {
|
|
|
34
34
|
* Path is relative to the project root.
|
|
35
35
|
*/
|
|
36
36
|
entry?: string;
|
|
37
|
-
/**
|
|
38
|
-
* Module aliases resolved during bare import resolution.
|
|
39
|
-
* e.g. { "@/": "src/" } maps @/ imports to the src/ directory.
|
|
40
|
-
*/
|
|
41
|
-
aliases?: Record<string, string>;
|
|
42
37
|
/**
|
|
43
38
|
* Glob patterns to exclude from HMR watching in addition to the defaults
|
|
44
39
|
* (node_modules, .git, dist). Useful for generated files or large assets.
|