slicejs-web-framework 4.0.0 → 4.0.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/.opencode/opencode.json +14 -14
- 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 +793 -793
- package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +162 -162
- package/Slice/Slice.js +686 -697
- package/Slice/tests/build-bundled-component-without-category.test.js +103 -103
- package/Slice/tests/build-js-only-visual-components.test.js +144 -144
- package/Slice/tests/build-singleton.test.js +244 -244
- package/Slice/tests/bundle-v2-runtime-contract.test.js +738 -738
- package/Slice/tests/context-patch-use.test.js +95 -95
- package/Slice/tests/destroy-cascade.test.js +215 -215
- package/Slice/tests/event-emitters.test.js +79 -79
- package/Slice/tests/event-registry.test.js +141 -141
- package/Slice/tests/fixtures/real-runtime-loader.mjs +21 -21
- package/Slice/tests/leak-orphans.test.js +131 -131
- package/Slice/tests/props-allowed-values-validation.test.js +119 -119
- package/Slice/tests/public-env-runtime-accessors.test.js +44 -44
- package/Slice/tests/public-env-typed-accessors.test.js +66 -66
- package/Slice/tests/router-loading-finally.test.js +68 -68
- package/api/framework/devDepsOptimizer.js +335 -335
- package/api/framework/express.js +3 -3
- package/api/framework/precompressedStatic.js +93 -93
- package/api/framework/publicEnvResolver.js +123 -123
- package/api/framework/securityMiddleware.js +197 -197
- package/api/framework/server.js +411 -411
- package/api/index.js +7 -7
- package/api/tests/dev-deps-optimizer.test.js +137 -137
- package/api/tests/dev-server-external.test.js +56 -56
- package/api/tests/precompressed-static.test.js +119 -119
- package/api/tests/public-env-resolver.test.js +193 -193
- package/package.json +49 -49
- 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/tests/router-emit-ordering.test.js +63 -63
- package/types/index.d.ts +219 -219
package/api/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// api/index.js - Punto de entrada del servidor
|
|
2
|
-
import { createSliceServer } from 'slicejs-web-framework/api/framework/server.js';
|
|
3
|
-
|
|
4
|
-
const server = createSliceServer();
|
|
5
|
-
server.start();
|
|
6
|
-
|
|
7
|
-
export default server.app;
|
|
1
|
+
// api/index.js - Punto de entrada del servidor
|
|
2
|
+
import { createSliceServer } from 'slicejs-web-framework/api/framework/server.js';
|
|
3
|
+
|
|
4
|
+
const server = createSliceServer();
|
|
5
|
+
server.start();
|
|
6
|
+
|
|
7
|
+
export default server.app;
|
|
@@ -1,137 +1,137 @@
|
|
|
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
|
+
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 +1,56 @@
|
|
|
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
|
+
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 +1,119 @@
|
|
|
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
|
-
});
|
|
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
|
+
});
|