slicejs-web-framework 4.0.2 → 4.1.0

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.
Files changed (73) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +97 -97
  3. package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
  4. package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +470 -470
  5. package/Slice/Components/Structural/Controller/Controller.js +1406 -1406
  6. package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -52
  7. package/Slice/Components/Structural/Debugger/Debugger.html +72 -72
  8. package/Slice/Components/Structural/Debugger/Debugger.js +1505 -1505
  9. package/Slice/Components/Structural/EventManager/EventManager.js +651 -651
  10. package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +985 -985
  11. package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -296
  12. package/Slice/Components/Structural/Logger/Log.js +10 -10
  13. package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +662 -662
  14. package/Slice/Components/Structural/Logger/Logger.js +225 -225
  15. package/Slice/Components/Structural/Router/Router.js +820 -810
  16. package/Slice/Components/Structural/Router/applyRouteMetadata.js +39 -0
  17. package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
  18. package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +175 -175
  19. package/Slice/Slice.js +711 -710
  20. package/api/framework/devDepsOptimizer.js +335 -335
  21. package/api/framework/express.js +3 -3
  22. package/api/framework/precompressedStatic.js +102 -93
  23. package/api/framework/publicEnvResolver.js +123 -123
  24. package/api/framework/securityMiddleware.js +197 -197
  25. package/api/framework/seo.js +387 -0
  26. package/api/framework/server.js +777 -410
  27. package/api/index.js +14 -4
  28. package/package.json +6 -2
  29. package/sliceConfig.schema.json +122 -122
  30. package/src/App/index.html +69 -69
  31. package/src/App/index.js +20 -20
  32. package/src/App/style.css +11 -11
  33. package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -9
  34. package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -8
  35. package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -12
  36. package/src/Components/AppComponents/AppShell/AppShell.css +10 -10
  37. package/src/Components/AppComponents/AppShell/AppShell.html +4 -4
  38. package/src/Components/AppComponents/AppShell/AppShell.js +36 -36
  39. package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -20
  40. package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -10
  41. package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -19
  42. package/src/Components/components.js +8 -8
  43. package/src/public/Styles/sliceStyles.css +34 -34
  44. package/src/public/Themes/Dark.css +42 -42
  45. package/src/public/Themes/Light.css +31 -31
  46. package/src/public/Themes/Slice.css +47 -47
  47. package/src/public/manifest.json +15 -15
  48. package/src/public/robots.txt +4 -4
  49. package/src/public/sitemap.xml +8 -8
  50. package/src/routes.js +9 -9
  51. package/src/sliceConfig.json +72 -72
  52. package/types/index.d.ts +219 -219
  53. package/.opencode/opencode.json +0 -15
  54. package/Slice/tests/build-bundled-component-without-category.test.js +0 -103
  55. package/Slice/tests/build-js-only-visual-components.test.js +0 -144
  56. package/Slice/tests/build-singleton.test.js +0 -244
  57. package/Slice/tests/bundle-v2-runtime-contract.test.js +0 -747
  58. package/Slice/tests/context-patch-use.test.js +0 -95
  59. package/Slice/tests/destroy-cascade.test.js +0 -215
  60. package/Slice/tests/event-emitters.test.js +0 -79
  61. package/Slice/tests/event-registry.test.js +0 -141
  62. package/Slice/tests/fixtures/real-runtime-loader.mjs +0 -21
  63. package/Slice/tests/leak-orphans.test.js +0 -131
  64. package/Slice/tests/props-allowed-values-validation.test.js +0 -119
  65. package/Slice/tests/public-env-runtime-accessors.test.js +0 -44
  66. package/Slice/tests/public-env-typed-accessors.test.js +0 -66
  67. package/Slice/tests/router-loading-finally.test.js +0 -68
  68. package/api/tests/dev-deps-optimizer.test.js +0 -137
  69. package/api/tests/dev-server-external.test.js +0 -56
  70. package/api/tests/precompressed-static.test.js +0 -119
  71. package/api/tests/public-env-resolver.test.js +0 -193
  72. package/tests/router-build-failure.test.js +0 -125
  73. 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
- });