slicejs-web-framework 4.0.2 → 4.1.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/LICENSE +21 -21
- package/README.md +97 -97
- package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
- package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +470 -470
- package/Slice/Components/Structural/Controller/Controller.js +1406 -1406
- package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -52
- package/Slice/Components/Structural/Debugger/Debugger.html +72 -72
- package/Slice/Components/Structural/Debugger/Debugger.js +1505 -1505
- package/Slice/Components/Structural/EventManager/EventManager.js +651 -651
- package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +985 -985
- package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -296
- package/Slice/Components/Structural/Logger/Log.js +10 -10
- package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +662 -662
- package/Slice/Components/Structural/Logger/Logger.js +225 -225
- package/Slice/Components/Structural/Router/Router.js +820 -810
- package/Slice/Components/Structural/Router/applyRouteMetadata.js +39 -0
- package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +175 -175
- package/Slice/Slice.js +711 -710
- package/api/framework/devDepsOptimizer.js +335 -335
- package/api/framework/express.js +3 -3
- package/api/framework/precompressedStatic.js +102 -93
- package/api/framework/publicEnvResolver.js +123 -123
- package/api/framework/securityMiddleware.js +197 -197
- package/api/framework/seo.js +387 -0
- package/api/framework/server.js +777 -410
- package/api/index.js +14 -4
- package/package.json +6 -2
- package/sliceConfig.schema.json +122 -122
- package/src/App/index.html +69 -69
- package/src/App/index.js +20 -20
- package/src/App/style.css +11 -11
- package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -9
- package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -8
- package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -12
- package/src/Components/AppComponents/AppShell/AppShell.css +10 -10
- package/src/Components/AppComponents/AppShell/AppShell.html +4 -4
- package/src/Components/AppComponents/AppShell/AppShell.js +36 -36
- package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -20
- package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -10
- package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -19
- package/src/Components/components.js +8 -8
- package/src/public/Styles/sliceStyles.css +34 -34
- package/src/public/Themes/Dark.css +42 -42
- package/src/public/Themes/Light.css +31 -31
- package/src/public/Themes/Slice.css +47 -47
- package/src/public/manifest.json +15 -15
- package/src/public/robots.txt +4 -4
- package/src/public/sitemap.xml +8 -8
- package/src/routes.js +9 -9
- package/src/sliceConfig.json +72 -72
- package/types/index.d.ts +219 -219
- package/.opencode/opencode.json +0 -15
- package/Slice/tests/build-bundled-component-without-category.test.js +0 -103
- package/Slice/tests/build-js-only-visual-components.test.js +0 -144
- package/Slice/tests/build-singleton.test.js +0 -244
- package/Slice/tests/bundle-v2-runtime-contract.test.js +0 -747
- package/Slice/tests/context-patch-use.test.js +0 -95
- package/Slice/tests/destroy-cascade.test.js +0 -215
- package/Slice/tests/event-emitters.test.js +0 -79
- package/Slice/tests/event-registry.test.js +0 -141
- package/Slice/tests/fixtures/real-runtime-loader.mjs +0 -21
- package/Slice/tests/leak-orphans.test.js +0 -131
- package/Slice/tests/props-allowed-values-validation.test.js +0 -119
- package/Slice/tests/public-env-runtime-accessors.test.js +0 -44
- package/Slice/tests/public-env-typed-accessors.test.js +0 -66
- package/Slice/tests/router-loading-finally.test.js +0 -68
- package/api/tests/dev-deps-optimizer.test.js +0 -137
- package/api/tests/dev-server-external.test.js +0 -56
- package/api/tests/precompressed-static.test.js +0 -119
- package/api/tests/public-env-resolver.test.js +0 -193
- package/tests/router-build-failure.test.js +0 -125
- package/tests/router-emit-ordering.test.js +0 -63
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
|
|
4
|
-
import { collectInvalidAllowedValueProps } from '../Components/Structural/Controller/allowedValuesValidation.js';
|
|
5
|
-
|
|
6
|
-
test('collectInvalidAllowedValueProps returns empty when value is allowed', () => {
|
|
7
|
-
const result = collectInvalidAllowedValueProps(
|
|
8
|
-
{
|
|
9
|
-
variant: {
|
|
10
|
-
type: 'string',
|
|
11
|
-
allowedValues: ['primary', 'secondary']
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
variant: 'primary'
|
|
16
|
-
}
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
assert.deepEqual(result, []);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('collectInvalidAllowedValueProps reports invalid provided values', () => {
|
|
23
|
-
const result = collectInvalidAllowedValueProps(
|
|
24
|
-
{
|
|
25
|
-
variant: {
|
|
26
|
-
type: 'string',
|
|
27
|
-
allowedValues: ['primary', 'secondary']
|
|
28
|
-
},
|
|
29
|
-
size: {
|
|
30
|
-
type: 'number',
|
|
31
|
-
allowedValues: [12, 16]
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
variant: 'danger',
|
|
36
|
-
size: 14
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
assert.deepEqual(result, [
|
|
41
|
-
{
|
|
42
|
-
propName: 'variant',
|
|
43
|
-
value: 'danger',
|
|
44
|
-
allowedValues: ['primary', 'secondary']
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
propName: 'size',
|
|
48
|
-
value: 14,
|
|
49
|
-
allowedValues: [12, 16]
|
|
50
|
-
}
|
|
51
|
-
]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('collectInvalidAllowedValueProps ignores props not provided', () => {
|
|
55
|
-
const result = collectInvalidAllowedValueProps(
|
|
56
|
-
{
|
|
57
|
-
variant: {
|
|
58
|
-
type: 'string',
|
|
59
|
-
allowedValues: ['primary', 'secondary']
|
|
60
|
-
},
|
|
61
|
-
disabled: {
|
|
62
|
-
type: 'boolean'
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
disabled: true
|
|
67
|
-
}
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
assert.deepEqual(result, []);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('collectInvalidAllowedValueProps uses strict equality', () => {
|
|
74
|
-
const result = collectInvalidAllowedValueProps(
|
|
75
|
-
{
|
|
76
|
-
amount: {
|
|
77
|
-
type: 'number',
|
|
78
|
-
allowedValues: [1]
|
|
79
|
-
},
|
|
80
|
-
active: {
|
|
81
|
-
type: 'boolean',
|
|
82
|
-
allowedValues: [true]
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
amount: '1',
|
|
87
|
-
active: 1
|
|
88
|
-
}
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
assert.deepEqual(result, [
|
|
92
|
-
{
|
|
93
|
-
propName: 'amount',
|
|
94
|
-
value: '1',
|
|
95
|
-
allowedValues: [1]
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
propName: 'active',
|
|
99
|
-
value: 1,
|
|
100
|
-
allowedValues: [true]
|
|
101
|
-
}
|
|
102
|
-
]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('collectInvalidAllowedValueProps ignores empty allowedValues arrays', () => {
|
|
106
|
-
const result = collectInvalidAllowedValueProps(
|
|
107
|
-
{
|
|
108
|
-
variant: {
|
|
109
|
-
type: 'string',
|
|
110
|
-
allowedValues: []
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
variant: 'unexpected'
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
assert.deepEqual(result, []);
|
|
119
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
|
|
4
|
-
globalThis.alert = () => {};
|
|
5
|
-
|
|
6
|
-
const { default: Slice } = await import('../Slice.js');
|
|
7
|
-
|
|
8
|
-
function createSliceInstance() {
|
|
9
|
-
return new Slice({
|
|
10
|
-
paths: {},
|
|
11
|
-
themeManager: {},
|
|
12
|
-
stylesManager: {},
|
|
13
|
-
logger: {},
|
|
14
|
-
debugger: {},
|
|
15
|
-
loading: {},
|
|
16
|
-
events: {}
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
test('getEnv returns fallback for missing key and stored value for known key', () => {
|
|
21
|
-
const sliceInstance = createSliceInstance();
|
|
22
|
-
|
|
23
|
-
assert.equal(typeof sliceInstance.getEnv, 'function');
|
|
24
|
-
assert.equal(typeof sliceInstance.setPublicEnv, 'function');
|
|
25
|
-
assert.equal(sliceInstance.getEnv('SLICE_PUBLIC_MISSING', 'fallback'), 'fallback');
|
|
26
|
-
|
|
27
|
-
sliceInstance.setPublicEnv({ SLICE_PUBLIC_API_URL: 'https://api.example.com' });
|
|
28
|
-
assert.equal(sliceInstance.getEnv('SLICE_PUBLIC_API_URL'), 'https://api.example.com');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('getPublicEnv returns a copy and only includes SLICE_PUBLIC_ keys', () => {
|
|
32
|
-
const sliceInstance = createSliceInstance();
|
|
33
|
-
|
|
34
|
-
sliceInstance.setPublicEnv({
|
|
35
|
-
SLICE_PUBLIC_FLAG: 'true',
|
|
36
|
-
INTERNAL_SECRET: 'hidden'
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const snapshot = sliceInstance.getPublicEnv();
|
|
40
|
-
assert.deepEqual(snapshot, { SLICE_PUBLIC_FLAG: 'true' });
|
|
41
|
-
|
|
42
|
-
snapshot.SLICE_PUBLIC_FLAG = 'mutated';
|
|
43
|
-
assert.equal(sliceInstance.getEnv('SLICE_PUBLIC_FLAG'), 'true');
|
|
44
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
|
|
4
|
-
globalThis.alert = () => {};
|
|
5
|
-
|
|
6
|
-
const { default: Slice } = await import('../Slice.js');
|
|
7
|
-
|
|
8
|
-
function createSliceInstance() {
|
|
9
|
-
return new Slice({
|
|
10
|
-
paths: {},
|
|
11
|
-
themeManager: {},
|
|
12
|
-
stylesManager: {},
|
|
13
|
-
logger: {},
|
|
14
|
-
debugger: {},
|
|
15
|
-
loading: {},
|
|
16
|
-
events: {}
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
test('slice.env.get / has / all', () => {
|
|
21
|
-
const s = createSliceInstance();
|
|
22
|
-
s.setPublicEnv({ SLICE_PUBLIC_API_URL: 'https://api.example.com', INTERNAL: 'hidden' });
|
|
23
|
-
|
|
24
|
-
assert.equal(s.env.get('SLICE_PUBLIC_API_URL'), 'https://api.example.com');
|
|
25
|
-
assert.equal(s.env.get('SLICE_PUBLIC_MISSING', 'fb'), 'fb');
|
|
26
|
-
assert.equal(s.env.has('SLICE_PUBLIC_API_URL'), true);
|
|
27
|
-
assert.equal(s.env.has('SLICE_PUBLIC_MISSING'), false);
|
|
28
|
-
assert.deepEqual(s.env.all(), { SLICE_PUBLIC_API_URL: 'https://api.example.com' }); // only SLICE_PUBLIC_*
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('slice.env.bool parses truthy strings and falls back', () => {
|
|
32
|
-
const s = createSliceInstance();
|
|
33
|
-
s.setPublicEnv({
|
|
34
|
-
SLICE_PUBLIC_ON: 'true',
|
|
35
|
-
SLICE_PUBLIC_ON2: 'YES',
|
|
36
|
-
SLICE_PUBLIC_ON3: '1',
|
|
37
|
-
SLICE_PUBLIC_OFF: 'false',
|
|
38
|
-
SLICE_PUBLIC_EMPTY: ''
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
assert.equal(s.env.bool('SLICE_PUBLIC_ON'), true);
|
|
42
|
-
assert.equal(s.env.bool('SLICE_PUBLIC_ON2'), true);
|
|
43
|
-
assert.equal(s.env.bool('SLICE_PUBLIC_ON3'), true);
|
|
44
|
-
assert.equal(s.env.bool('SLICE_PUBLIC_OFF'), false);
|
|
45
|
-
assert.equal(s.env.bool('SLICE_PUBLIC_EMPTY', true), true); // empty → fallback
|
|
46
|
-
assert.equal(s.env.bool('SLICE_PUBLIC_MISSING'), false); // missing → default false
|
|
47
|
-
assert.equal(s.env.bool('SLICE_PUBLIC_MISSING', true), true); // missing → custom fallback
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('slice.env.int parses integers and falls back', () => {
|
|
51
|
-
const s = createSliceInstance();
|
|
52
|
-
s.setPublicEnv({ SLICE_PUBLIC_TIMEOUT: '5000', SLICE_PUBLIC_BAD: 'nope' });
|
|
53
|
-
|
|
54
|
-
assert.equal(s.env.int('SLICE_PUBLIC_TIMEOUT'), 5000);
|
|
55
|
-
assert.equal(s.env.int('SLICE_PUBLIC_BAD', 10), 10); // non-numeric → fallback
|
|
56
|
-
assert.equal(s.env.int('SLICE_PUBLIC_MISSING', 42), 42); // missing → fallback
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('slice.env.list splits CSV, trims, drops empties', () => {
|
|
60
|
-
const s = createSliceInstance();
|
|
61
|
-
s.setPublicEnv({ SLICE_PUBLIC_MODELS: ' a , b ,, c ', SLICE_PUBLIC_EMPTY: '' });
|
|
62
|
-
|
|
63
|
-
assert.deepEqual(s.env.list('SLICE_PUBLIC_MODELS'), ['a', 'b', 'c']);
|
|
64
|
-
assert.deepEqual(s.env.list('SLICE_PUBLIC_EMPTY', ['x']), ['x']); // empty → fallback
|
|
65
|
-
assert.deepEqual(s.env.list('SLICE_PUBLIC_MISSING'), []); // missing → default []
|
|
66
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
|
|
4
|
-
import Router from '../Components/Structural/Router/Router.js';
|
|
5
|
-
|
|
6
|
-
test('handleRoute stops loading when build throws on error path', async () => {
|
|
7
|
-
const originalSlice = globalThis.slice;
|
|
8
|
-
const originalDocument = globalThis.document;
|
|
9
|
-
|
|
10
|
-
const targetElement = {
|
|
11
|
-
innerHTML: 'existing',
|
|
12
|
-
appendChild() {},
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
globalThis.document = {
|
|
17
|
-
querySelector(selector) {
|
|
18
|
-
if (selector === '#app') {
|
|
19
|
-
return targetElement;
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const loadingCalls = [];
|
|
26
|
-
|
|
27
|
-
globalThis.slice = {
|
|
28
|
-
loading: {
|
|
29
|
-
start() {
|
|
30
|
-
loadingCalls.push('start');
|
|
31
|
-
},
|
|
32
|
-
stop() {
|
|
33
|
-
loadingCalls.push('stop');
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
controller: {
|
|
37
|
-
getComponent() {
|
|
38
|
-
return null;
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
build: async () => {
|
|
42
|
-
throw new Error('boom');
|
|
43
|
-
},
|
|
44
|
-
router: {
|
|
45
|
-
activeRoute: null,
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const router = new Router([]);
|
|
50
|
-
|
|
51
|
-
await assert.rejects(
|
|
52
|
-
() =>
|
|
53
|
-
router.handleRoute(
|
|
54
|
-
{
|
|
55
|
-
component: 'Dashboard',
|
|
56
|
-
parentRoute: null,
|
|
57
|
-
},
|
|
58
|
-
{}
|
|
59
|
-
),
|
|
60
|
-
/boom/
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
assert.deepEqual(loadingCalls, ['start', 'stop']);
|
|
64
|
-
} finally {
|
|
65
|
-
globalThis.slice = originalSlice;
|
|
66
|
-
globalThis.document = originalDocument;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fsp from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import { createDevDepsOptimizer, MODULES_PREFIX } from '../framework/devDepsOptimizer.js';
|
|
7
|
-
|
|
8
|
-
// projectRoot for esbuild resolution: this framework package's own root, whose
|
|
9
|
-
// node_modules contains es-module-lexer (used here as a real ESM package to
|
|
10
|
-
// bundle). Two dirs up from api/tests/.
|
|
11
|
-
const projectRoot = new URL('../../', import.meta.url).pathname;
|
|
12
|
-
|
|
13
|
-
test('rewriteBareImports rewrites bare specifiers to the virtual endpoint', async () => {
|
|
14
|
-
const opt = createDevDepsOptimizer({ projectRoot, enabled: true });
|
|
15
|
-
const code = [
|
|
16
|
-
"import dayjs from 'dayjs';",
|
|
17
|
-
"import { a, b } from '@scope/pkg';",
|
|
18
|
-
"import './local.js';",
|
|
19
|
-
"import '/libs/x.js';",
|
|
20
|
-
"import x from 'https://cdn/x.js';",
|
|
21
|
-
"const d = import('lodash/fp');"
|
|
22
|
-
].join('\n');
|
|
23
|
-
|
|
24
|
-
const out = await opt.rewriteBareImports(code);
|
|
25
|
-
|
|
26
|
-
assert.match(out, new RegExp(`from '${MODULES_PREFIX}dayjs'`));
|
|
27
|
-
assert.match(out, new RegExp(`from '${MODULES_PREFIX}@scope/pkg'`));
|
|
28
|
-
assert.match(out, new RegExp(`import\\('${MODULES_PREFIX}lodash/fp'\\)`));
|
|
29
|
-
// Relative, absolute (public/) and URL imports are untouched.
|
|
30
|
-
assert.match(out, /import '\.\/local\.js'/);
|
|
31
|
-
assert.match(out, /import '\/libs\/x\.js'/);
|
|
32
|
-
assert.match(out, /from 'https:\/\/cdn\/x\.js'/);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('rewriteBareImports is a no-op when disabled', async () => {
|
|
36
|
-
const opt = createDevDepsOptimizer({ projectRoot, enabled: false });
|
|
37
|
-
const code = "import dayjs from 'dayjs';";
|
|
38
|
-
assert.equal(await opt.rewriteBareImports(code), code);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('rewriteBareImports returns source unchanged when there is nothing bare', async () => {
|
|
42
|
-
const opt = createDevDepsOptimizer({ projectRoot, enabled: true });
|
|
43
|
-
const code = "import './a.js';\nexport const x = 1;\n";
|
|
44
|
-
assert.equal(await opt.rewriteBareImports(code), code);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('specifierFromRequest extracts the package from the virtual path', () => {
|
|
48
|
-
const opt = createDevDepsOptimizer({ projectRoot, enabled: true });
|
|
49
|
-
assert.equal(opt.specifierFromRequest(`${MODULES_PREFIX}dayjs`), 'dayjs');
|
|
50
|
-
assert.equal(opt.specifierFromRequest(`${MODULES_PREFIX}@scope/pkg/sub`), '@scope/pkg/sub');
|
|
51
|
-
assert.equal(opt.specifierFromRequest('/Components/x.js'), null);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('bundlePackage resolves a real ESM package to self-contained ESM', async () => {
|
|
55
|
-
const opt = createDevDepsOptimizer({ projectRoot, enabled: true });
|
|
56
|
-
// es-module-lexer is a named-export ESM package (no default) — exercises the
|
|
57
|
-
// namespace-only fallback path in bundlePackage.
|
|
58
|
-
const { code } = await opt.bundlePackage('es-module-lexer');
|
|
59
|
-
assert.ok(code.length > 0, 'expected bundled code');
|
|
60
|
-
// Self-contained: no bare imports left to resolve at runtime.
|
|
61
|
-
assert.doesNotMatch(code, /^\s*import\s+.*from\s+['"][^./]/m);
|
|
62
|
-
// Exposes the package's named API.
|
|
63
|
-
assert.match(code, /\bparse\b/);
|
|
64
|
-
assert.match(code, /export\s*\{/);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('bundlePackage caches and shares a single build per specifier', async () => {
|
|
68
|
-
const opt = createDevDepsOptimizer({ projectRoot, enabled: true });
|
|
69
|
-
const [a, b] = await Promise.all([
|
|
70
|
-
opt.bundlePackage('es-module-lexer'),
|
|
71
|
-
opt.bundlePackage('es-module-lexer')
|
|
72
|
-
]);
|
|
73
|
-
assert.equal(a, b, 'concurrent requests should share the same cached result');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('bundlePackage persists a disk cache keyed by version, reused by a fresh optimizer', async () => {
|
|
77
|
-
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'slice-devcache-'));
|
|
78
|
-
try {
|
|
79
|
-
// Minimal installed package in the temp project's node_modules.
|
|
80
|
-
const pkgDir = path.join(tmp, 'node_modules', 'mini-pkg');
|
|
81
|
-
await fsp.mkdir(pkgDir, { recursive: true });
|
|
82
|
-
await fsp.writeFile(path.join(pkgDir, 'package.json'),
|
|
83
|
-
JSON.stringify({ name: 'mini-pkg', version: '2.3.4', type: 'module', main: 'index.js' }));
|
|
84
|
-
await fsp.writeFile(path.join(pkgDir, 'index.js'), 'export const hi = () => "hi";');
|
|
85
|
-
|
|
86
|
-
const opt1 = createDevDepsOptimizer({ projectRoot: tmp, enabled: true });
|
|
87
|
-
const first = await opt1.bundlePackage('mini-pkg');
|
|
88
|
-
assert.match(first.code, /hi/);
|
|
89
|
-
|
|
90
|
-
// A cache file keyed by the installed version must now exist.
|
|
91
|
-
const cacheFile = path.join(tmp, 'node_modules', '.slice-deps', 'mini-pkg@2.3.4.js');
|
|
92
|
-
const cached = await fsp.readFile(cacheFile, 'utf8');
|
|
93
|
-
assert.equal(cached, first.code);
|
|
94
|
-
|
|
95
|
-
// A brand-new optimizer (empty in-memory cache) reads the same bytes from disk.
|
|
96
|
-
const opt2 = createDevDepsOptimizer({ projectRoot: tmp, enabled: true });
|
|
97
|
-
const second = await opt2.bundlePackage('mini-pkg');
|
|
98
|
-
assert.equal(second.code, first.code);
|
|
99
|
-
} finally {
|
|
100
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('bundlePackage prunes stale cache files when a package version changes', async () => {
|
|
105
|
-
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'slice-devcache-gc-'));
|
|
106
|
-
try {
|
|
107
|
-
const pkgDir = path.join(tmp, 'node_modules', 'mini-pkg');
|
|
108
|
-
const pkgJson = path.join(pkgDir, 'package.json');
|
|
109
|
-
const cacheDir = path.join(tmp, 'node_modules', '.slice-deps');
|
|
110
|
-
await fsp.mkdir(pkgDir, { recursive: true });
|
|
111
|
-
await fsp.writeFile(pkgJson, JSON.stringify({ name: 'mini-pkg', version: '1.0.0', type: 'module', main: 'index.js' }));
|
|
112
|
-
await fsp.writeFile(path.join(pkgDir, 'index.js'), 'export const v = 1;');
|
|
113
|
-
|
|
114
|
-
// A sibling package must survive the prune (its name is not a prefix match).
|
|
115
|
-
const otherDir = path.join(tmp, 'node_modules', 'mini-pkg-extra');
|
|
116
|
-
await fsp.mkdir(otherDir, { recursive: true });
|
|
117
|
-
await fsp.writeFile(path.join(otherDir, 'package.json'), JSON.stringify({ name: 'mini-pkg-extra', version: '9.0.0', type: 'module', main: 'index.js' }));
|
|
118
|
-
await fsp.writeFile(path.join(otherDir, 'index.js'), 'export const e = 1;');
|
|
119
|
-
|
|
120
|
-
await createDevDepsOptimizer({ projectRoot: tmp, enabled: true }).bundlePackage('mini-pkg');
|
|
121
|
-
await createDevDepsOptimizer({ projectRoot: tmp, enabled: true }).bundlePackage('mini-pkg-extra');
|
|
122
|
-
assert.ok(await fsp.stat(path.join(cacheDir, 'mini-pkg@1.0.0.js')).then(() => true));
|
|
123
|
-
|
|
124
|
-
// Simulate an upgrade: bump the installed version, then rebundle.
|
|
125
|
-
await fsp.writeFile(pkgJson, JSON.stringify({ name: 'mini-pkg', version: '1.1.0', type: 'module', main: 'index.js' }));
|
|
126
|
-
await fsp.writeFile(path.join(pkgDir, 'index.js'), 'export const v = 2;');
|
|
127
|
-
await createDevDepsOptimizer({ projectRoot: tmp, enabled: true }).bundlePackage('mini-pkg');
|
|
128
|
-
|
|
129
|
-
const remaining = (await fsp.readdir(cacheDir)).sort();
|
|
130
|
-
// Old version gone, new version present, sibling untouched.
|
|
131
|
-
assert.ok(!remaining.includes('mini-pkg@1.0.0.js'), 'stale version should be pruned');
|
|
132
|
-
assert.ok(remaining.includes('mini-pkg@1.1.0.js'), 'new version should be cached');
|
|
133
|
-
assert.ok(remaining.includes('mini-pkg-extra@9.0.0.js'), 'sibling package must not be pruned');
|
|
134
|
-
} finally {
|
|
135
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
136
|
-
}
|
|
137
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import fsp from 'node:fs/promises';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
|
|
8
|
-
// Boots the real dev server against a temp project and checks the two moving
|
|
9
|
-
// parts of external-dependency support in development: bare-import rewriting of
|
|
10
|
-
// served src modules, and on-demand esbuild bundling at /@slice-modules/.
|
|
11
|
-
test('dev server rewrites bare imports and serves bundled packages', async () => {
|
|
12
|
-
process.env.NODE_ENV = 'development';
|
|
13
|
-
const frameworkRoot = new URL('../../', import.meta.url).pathname; // slice.js root
|
|
14
|
-
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'slice-dev-'));
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
await fsp.mkdir(path.join(tmp, 'src', 'Components', 'Demo'), { recursive: true });
|
|
18
|
-
await fsp.writeFile(
|
|
19
|
-
path.join(tmp, 'src', 'sliceConfig.json'),
|
|
20
|
-
JSON.stringify({ externalDependencies: { enabled: true } })
|
|
21
|
-
);
|
|
22
|
-
await fsp.writeFile(
|
|
23
|
-
path.join(tmp, 'src', 'Components', 'Demo', 'Demo.js'),
|
|
24
|
-
"import { parse } from 'es-module-lexer';\nimport './local.js';\nexport default class Demo { run(){ return typeof parse; } }\n"
|
|
25
|
-
);
|
|
26
|
-
// Make es-module-lexer resolvable from the temp project for esbuild.
|
|
27
|
-
fs.symlinkSync(path.join(frameworkRoot, 'node_modules'), path.join(tmp, 'node_modules'), 'dir');
|
|
28
|
-
|
|
29
|
-
const { createSliceServer } = await import('../framework/server.js');
|
|
30
|
-
const { app } = createSliceServer({ projectRoot: tmp });
|
|
31
|
-
const server = app.listen(0);
|
|
32
|
-
await new Promise((resolve) => server.once('listening', resolve));
|
|
33
|
-
const base = `http://127.0.0.1:${server.address().port}`;
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
// 1. The component's bare import is rewritten; the relative one is not.
|
|
37
|
-
const compRes = await fetch(`${base}/Components/Demo/Demo.js`);
|
|
38
|
-
const compText = await compRes.text();
|
|
39
|
-
assert.equal(compRes.status, 200);
|
|
40
|
-
assert.match(compText, /from ['"]\/@slice-modules\/es-module-lexer['"]/);
|
|
41
|
-
assert.match(compText, /import ['"]\.\/local\.js['"]/);
|
|
42
|
-
|
|
43
|
-
// 2. The virtual endpoint returns a self-contained ESM bundle of the pkg.
|
|
44
|
-
const modRes = await fetch(`${base}/@slice-modules/es-module-lexer`);
|
|
45
|
-
const modText = await modRes.text();
|
|
46
|
-
assert.equal(modRes.status, 200);
|
|
47
|
-
assert.match(modRes.headers.get('content-type') || '', /javascript/);
|
|
48
|
-
assert.match(modText, /export\s*\{/);
|
|
49
|
-
assert.doesNotMatch(modText, /^\s*import\s+.*from\s+['"][^./]/m);
|
|
50
|
-
} finally {
|
|
51
|
-
await new Promise((resolve) => server.close(resolve));
|
|
52
|
-
}
|
|
53
|
-
} finally {
|
|
54
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
55
|
-
}
|
|
56
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fsp from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import { createPrecompressedStatic } from '../framework/precompressedStatic.js';
|
|
7
|
-
|
|
8
|
-
function fakeRes() {
|
|
9
|
-
return {
|
|
10
|
-
headers: {},
|
|
11
|
-
body: null,
|
|
12
|
-
sent: false,
|
|
13
|
-
setHeader(k, v) { this.headers[k.toLowerCase()] = v; },
|
|
14
|
-
send(body) { this.body = body; this.sent = true; return this; }
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function run(mw, req) {
|
|
19
|
-
const res = fakeRes();
|
|
20
|
-
let nextCalled = false;
|
|
21
|
-
mw(req, res, () => { nextCalled = true; });
|
|
22
|
-
return { res, nextCalled };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function withRoot(fn) {
|
|
26
|
-
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'slice-precomp-'));
|
|
27
|
-
try {
|
|
28
|
-
await fn(tmp);
|
|
29
|
-
} finally {
|
|
30
|
-
await fsp.rm(tmp, { recursive: true, force: true });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
test('serves the .br variant when the client accepts brotli', async () => {
|
|
35
|
-
await withRoot(async (root) => {
|
|
36
|
-
await fsp.mkdir(path.join(root, 'bundles'), { recursive: true });
|
|
37
|
-
await fsp.writeFile(path.join(root, 'bundles', 'app.js'), 'ORIGINAL');
|
|
38
|
-
await fsp.writeFile(path.join(root, 'bundles', 'app.js.br'), 'BROTLI-BYTES');
|
|
39
|
-
await fsp.writeFile(path.join(root, 'bundles', 'app.js.gz'), 'GZIP-BYTES');
|
|
40
|
-
|
|
41
|
-
const mw = createPrecompressedStatic([root]);
|
|
42
|
-
const { res, nextCalled } = run(mw, {
|
|
43
|
-
method: 'GET', path: '/bundles/app.js', headers: { 'accept-encoding': 'gzip, br' }
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
assert.equal(nextCalled, false);
|
|
47
|
-
assert.equal(res.sent, true);
|
|
48
|
-
assert.equal(res.body.toString(), 'BROTLI-BYTES'); // brotli preferred over gzip
|
|
49
|
-
assert.equal(res.headers['content-encoding'], 'br');
|
|
50
|
-
assert.equal(res.headers['content-type'], 'application/javascript; charset=utf-8');
|
|
51
|
-
assert.equal(res.headers['vary'], 'Accept-Encoding');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('falls back to gzip when brotli is not accepted', async () => {
|
|
56
|
-
await withRoot(async (root) => {
|
|
57
|
-
await fsp.writeFile(path.join(root, 'style.css'), 'ORIGINAL');
|
|
58
|
-
await fsp.writeFile(path.join(root, 'style.css.gz'), 'GZIP-BYTES');
|
|
59
|
-
|
|
60
|
-
const mw = createPrecompressedStatic([root]);
|
|
61
|
-
const { res } = run(mw, { method: 'GET', path: '/style.css', headers: { 'accept-encoding': 'gzip' } });
|
|
62
|
-
assert.equal(res.headers['content-encoding'], 'gzip');
|
|
63
|
-
assert.equal(res.headers['content-type'], 'text/css; charset=utf-8');
|
|
64
|
-
assert.equal(res.body.toString(), 'GZIP-BYTES');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('passes through when the client accepts no encoding', async () => {
|
|
69
|
-
await withRoot(async (root) => {
|
|
70
|
-
await fsp.writeFile(path.join(root, 'app.js'), 'ORIGINAL');
|
|
71
|
-
await fsp.writeFile(path.join(root, 'app.js.br'), 'BROTLI-BYTES');
|
|
72
|
-
const mw = createPrecompressedStatic([root]);
|
|
73
|
-
const { res, nextCalled } = run(mw, { method: 'GET', path: '/app.js', headers: {} });
|
|
74
|
-
assert.equal(nextCalled, true);
|
|
75
|
-
assert.equal(res.sent, false);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('passes through when no precompressed variant exists', async () => {
|
|
80
|
-
await withRoot(async (root) => {
|
|
81
|
-
await fsp.writeFile(path.join(root, 'app.js'), 'ORIGINAL'); // no .br/.gz
|
|
82
|
-
const mw = createPrecompressedStatic([root]);
|
|
83
|
-
const { res, nextCalled } = run(mw, { method: 'GET', path: '/app.js', headers: { 'accept-encoding': 'br' } });
|
|
84
|
-
assert.equal(nextCalled, true);
|
|
85
|
-
assert.equal(res.sent, false);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('passes through for non-text extensions', async () => {
|
|
90
|
-
await withRoot(async (root) => {
|
|
91
|
-
await fsp.writeFile(path.join(root, 'img.png.br'), 'X');
|
|
92
|
-
const mw = createPrecompressedStatic([root]);
|
|
93
|
-
const { nextCalled } = run(mw, { method: 'GET', path: '/img.png', headers: { 'accept-encoding': 'br' } });
|
|
94
|
-
assert.equal(nextCalled, true);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('honors root priority (public over dist root)', async () => {
|
|
99
|
-
await withRoot(async (root) => {
|
|
100
|
-
const pub = path.join(root, 'public');
|
|
101
|
-
await fsp.mkdir(pub, { recursive: true });
|
|
102
|
-
await fsp.writeFile(path.join(pub, 'theme.css.br'), 'PUBLIC');
|
|
103
|
-
await fsp.writeFile(path.join(root, 'theme.css.br'), 'ROOT');
|
|
104
|
-
const mw = createPrecompressedStatic([pub, root]);
|
|
105
|
-
const { res } = run(mw, { method: 'GET', path: '/theme.css', headers: { 'accept-encoding': 'br' } });
|
|
106
|
-
assert.equal(res.body.toString(), 'PUBLIC');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('blocks path traversal', async () => {
|
|
111
|
-
await withRoot(async (root) => {
|
|
112
|
-
const mw = createPrecompressedStatic([path.join(root, 'public')]);
|
|
113
|
-
const { nextCalled, res } = run(mw, {
|
|
114
|
-
method: 'GET', path: '/../secret.js', headers: { 'accept-encoding': 'br' }
|
|
115
|
-
});
|
|
116
|
-
assert.equal(nextCalled, true);
|
|
117
|
-
assert.equal(res.sent, false);
|
|
118
|
-
});
|
|
119
|
-
});
|