slicejs-web-framework 3.4.4 → 3.5.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/Slice/Components/Structural/Controller/Controller.js +135 -0
- package/Slice/Components/Structural/EventManager/EventManager.js +289 -4
- package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +368 -30
- package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -0
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +56 -0
- package/Slice/Slice.js +14 -1
- package/Slice/tests/event-emitters.test.js +79 -0
- package/Slice/tests/event-registry.test.js +141 -0
- package/Slice/tests/leak-orphans.test.js +131 -0
- package/api/{middleware → framework}/securityMiddleware.js +18 -74
- package/api/framework/server.js +333 -0
- package/api/index.js +5 -279
- package/api/tests/public-env-resolver.test.js +1 -1
- package/package.json +4 -2
- package/sliceConfig.schema.json +10 -1
- package/src/App/index.html +31 -1
- package/src/manifest.json +15 -0
- package/src/robots.txt +4 -0
- package/src/sitemap.xml +8 -0
- package/src/sliceConfig.json +5 -1
- package/types/index.d.ts +12 -0
- package/slicejs-cli/.github/workflows/ci.yml +0 -43
- package/slicejs-cli/LICENSE +0 -21
- package/slicejs-cli/assets/Slice.js-logo.png +0 -0
- package/slicejs-cli/client.js +0 -740
- package/slicejs-cli/commands/Print.js +0 -164
- package/slicejs-cli/commands/Validations.js +0 -92
- package/slicejs-cli/commands/build/build.js +0 -40
- package/slicejs-cli/commands/buildProduction/buildProduction.js +0 -577
- package/slicejs-cli/commands/bundle/bundle.js +0 -236
- package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +0 -55
- package/slicejs-cli/commands/createComponent/createComponent.js +0 -128
- package/slicejs-cli/commands/deleteComponent/deleteComponent.js +0 -81
- package/slicejs-cli/commands/doctor/doctor.js +0 -516
- package/slicejs-cli/commands/getComponent/getComponent.js +0 -695
- package/slicejs-cli/commands/init/init.js +0 -473
- package/slicejs-cli/commands/listComponents/listComponents.js +0 -172
- package/slicejs-cli/commands/startServer/startServer.js +0 -240
- package/slicejs-cli/commands/startServer/watchServer.js +0 -66
- package/slicejs-cli/commands/types/types.js +0 -583
- package/slicejs-cli/commands/utils/LocalCliDelegation.js +0 -53
- package/slicejs-cli/commands/utils/PackageManager.js +0 -148
- package/slicejs-cli/commands/utils/PathHelper.js +0 -75
- package/slicejs-cli/commands/utils/VersionChecker.js +0 -169
- package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +0 -2525
- package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +0 -933
- package/slicejs-cli/commands/utils/loadConfig.js +0 -31
- package/slicejs-cli/commands/utils/sliceScripts.js +0 -48
- package/slicejs-cli/commands/utils/updateManager.js +0 -471
- package/slicejs-cli/package.json +0 -71
- package/slicejs-cli/playwright.config.js +0 -51
- package/slicejs-cli/pnpm-lock.yaml +0 -1629
- package/slicejs-cli/tests/build-command-integration.test.js +0 -87
- package/slicejs-cli/tests/build-mangling-e2e.test.js +0 -290
- package/slicejs-cli/tests/build-production-e2e.test.js +0 -140
- package/slicejs-cli/tests/builder-edge-cases.test.js +0 -322
- package/slicejs-cli/tests/bundle-generate-e2e.test.js +0 -115
- package/slicejs-cli/tests/bundle-generator.test.js +0 -691
- package/slicejs-cli/tests/bundle-v2-register-output.test.js +0 -470
- package/slicejs-cli/tests/bundling-dependency-edges.test.js +0 -127
- package/slicejs-cli/tests/bundling-imports-unit.test.js +0 -267
- package/slicejs-cli/tests/client-launcher-contract.test.js +0 -211
- package/slicejs-cli/tests/commands-component-crud.test.js +0 -128
- package/slicejs-cli/tests/commands-doctor.test.js +0 -152
- package/slicejs-cli/tests/commands-version-checker.test.js +0 -37
- package/slicejs-cli/tests/component-registry-parse.test.js +0 -34
- package/slicejs-cli/tests/dependency-analyzer.test.js +0 -24
- package/slicejs-cli/tests/e2e/bundles.spec.js +0 -91
- package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +0 -56
- package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
- package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
- package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
- package/slicejs-cli/tests/e2e/fixtures/components/registry.json +0 -12
- package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +0 -65
- package/slicejs-cli/tests/e2e/navigation.spec.js +0 -44
- package/slicejs-cli/tests/e2e/render.spec.js +0 -34
- package/slicejs-cli/tests/e2e/serve.mjs +0 -264
- package/slicejs-cli/tests/e2e/shared-deps.spec.js +0 -61
- package/slicejs-cli/tests/e2e/unminified.spec.js +0 -33
- package/slicejs-cli/tests/e2e-serve.test.js +0 -148
- package/slicejs-cli/tests/fixtures/components.js +0 -8
- package/slicejs-cli/tests/fixtures/sliceConfig.json +0 -74
- package/slicejs-cli/tests/getcomponent.test.js +0 -435
- package/slicejs-cli/tests/helpers/setup.js +0 -102
- package/slicejs-cli/tests/init-command-contract.test.js +0 -101
- package/slicejs-cli/tests/init-e2e.test.js +0 -82
- package/slicejs-cli/tests/init-project-isolation.test.js +0 -98
- package/slicejs-cli/tests/local-cli-delegation.test.js +0 -81
- package/slicejs-cli/tests/package-manager.test.js +0 -127
- package/slicejs-cli/tests/path-helper.test.js +0 -206
- package/slicejs-cli/tests/perf-budget.test.js +0 -84
- package/slicejs-cli/tests/postinstall-command.test.js +0 -103
- package/slicejs-cli/tests/property-mangling-regression.test.js +0 -182
- package/slicejs-cli/tests/types-breakage.test.js +0 -491
- package/slicejs-cli/tests/types-generator-errors.test.js +0 -361
- package/slicejs-cli/tests/types-generator.test.js +0 -348
- /package/api/{utils → framework}/publicEnvResolver.js +0 -0
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { test, describe, before, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import { createTestProject, cleanupTestProject } from './helpers/setup.js';
|
|
6
|
-
|
|
7
|
-
/** @type {string} */
|
|
8
|
-
let tmpRoot;
|
|
9
|
-
const origInitCwd = process.env.INIT_CWD;
|
|
10
|
-
|
|
11
|
-
before(async () => {
|
|
12
|
-
tmpRoot = await createTestProject();
|
|
13
|
-
process.env.INIT_CWD = tmpRoot;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
after(async () => {
|
|
17
|
-
if (origInitCwd === undefined) {
|
|
18
|
-
delete process.env.INIT_CWD;
|
|
19
|
-
} else {
|
|
20
|
-
process.env.INIT_CWD = origInitCwd;
|
|
21
|
-
}
|
|
22
|
-
await cleanupTestProject(tmpRoot);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('getProjectRoot', () => {
|
|
26
|
-
test('resolves project root via INIT_CWD when pointing to a real project', async () => {
|
|
27
|
-
const { getProjectRoot } = await import('../commands/utils/PathHelper.js');
|
|
28
|
-
const root = getProjectRoot(import.meta.url);
|
|
29
|
-
assert.ok(root, 'should return a path');
|
|
30
|
-
assert.ok(fs.existsSync(root), `resolved path should exist: ${root}`);
|
|
31
|
-
assert.equal(path.resolve(root), path.resolve(tmpRoot));
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('resolves project root via cwd when INIT_CWD is not set', async () => {
|
|
35
|
-
const prev = process.env.INIT_CWD;
|
|
36
|
-
delete process.env.INIT_CWD;
|
|
37
|
-
try {
|
|
38
|
-
const { getProjectRoot } = await import('../commands/utils/PathHelper.js');
|
|
39
|
-
const root = getProjectRoot(import.meta.url);
|
|
40
|
-
assert.ok(root, 'should return a path');
|
|
41
|
-
assert.ok(fs.existsSync(root), `resolved path should exist: ${root}`);
|
|
42
|
-
} finally {
|
|
43
|
-
process.env.INIT_CWD = prev;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('getSrcPath', () => {
|
|
49
|
-
test('returns src path relative to project root', async () => {
|
|
50
|
-
const { getSrcPath } = await import('../commands/utils/PathHelper.js');
|
|
51
|
-
const srcPath = getSrcPath(import.meta.url);
|
|
52
|
-
assert.ok(srcPath.endsWith('src'), `should end with 'src': ${srcPath}`);
|
|
53
|
-
assert.ok(fs.existsSync(srcPath), `src path should exist: ${srcPath}`);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('accepts subpath segments', async () => {
|
|
57
|
-
const { getSrcPath } = await import('../commands/utils/PathHelper.js');
|
|
58
|
-
const result = getSrcPath(import.meta.url, 'Components', 'components.js');
|
|
59
|
-
assert.ok(result.endsWith('src/Components/components.js') || result.endsWith('src\\Components\\components.js'));
|
|
60
|
-
assert.ok(fs.existsSync(result), `file should exist: ${result}`);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('getApiPath', () => {
|
|
65
|
-
test('returns api path relative to project root', async () => {
|
|
66
|
-
const { getApiPath } = await import('../commands/utils/PathHelper.js');
|
|
67
|
-
const apiPath = getApiPath(import.meta.url);
|
|
68
|
-
assert.ok(apiPath.endsWith('api'), `should end with 'api': ${apiPath}`);
|
|
69
|
-
assert.ok(fs.existsSync(apiPath), `api path should exist: ${apiPath}`);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('appends subpath to api', async () => {
|
|
73
|
-
const { getApiPath } = await import('../commands/utils/PathHelper.js');
|
|
74
|
-
const result = getApiPath(import.meta.url, 'index.js');
|
|
75
|
-
assert.ok(result.endsWith('api/index.js') || result.endsWith('api\\index.js'));
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('getDistPath', () => {
|
|
80
|
-
test('returns dist path relative to project root', async () => {
|
|
81
|
-
const { getDistPath } = await import('../commands/utils/PathHelper.js');
|
|
82
|
-
const distPath = getDistPath(import.meta.url);
|
|
83
|
-
assert.ok(distPath.endsWith('dist'), `should end with 'dist': ${distPath}`);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('getConfigPath', () => {
|
|
88
|
-
test('returns sliceConfig.json path', async () => {
|
|
89
|
-
const { getConfigPath } = await import('../commands/utils/PathHelper.js');
|
|
90
|
-
const configPath = getConfigPath(import.meta.url);
|
|
91
|
-
assert.ok(configPath.endsWith('sliceConfig.json'), `should end with 'sliceConfig.json': ${configPath}`);
|
|
92
|
-
assert.ok(fs.existsSync(configPath), `config file should exist: ${configPath}`);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('getComponentsJsPath', () => {
|
|
97
|
-
test('returns components.js path', async () => {
|
|
98
|
-
const { getComponentsJsPath } = await import('../commands/utils/PathHelper.js');
|
|
99
|
-
const compPath = getComponentsJsPath(import.meta.url);
|
|
100
|
-
assert.ok(compPath.endsWith('components.js'), `should end with 'components.js': ${compPath}`);
|
|
101
|
-
assert.ok(fs.existsSync(compPath), `components.js should exist: ${compPath}`);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('getPath', () => {
|
|
106
|
-
test('joins arbitrary segments to project root', async () => {
|
|
107
|
-
const { getPath } = await import('../commands/utils/PathHelper.js');
|
|
108
|
-
const result = getPath(import.meta.url, 'src', 'routes.js');
|
|
109
|
-
assert.ok(result.endsWith('routes.js'), `should end with 'routes.js': ${result}`);
|
|
110
|
-
assert.ok(fs.existsSync(result), `routes.js should exist: ${result}`);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('sanitizes leading slashes from segments', async () => {
|
|
114
|
-
const { getPath } = await import('../commands/utils/PathHelper.js');
|
|
115
|
-
const result = getPath(import.meta.url, '/src/', '/routes.js');
|
|
116
|
-
assert.ok(result.endsWith('routes.js'), `should handle sanitized segments: ${result}`);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('resolveProjectRoot (fallback via candidates)', () => {
|
|
121
|
-
test('uses candidates() when INIT_CWD is not a valid path', async () => {
|
|
122
|
-
const orig = process.env.INIT_CWD;
|
|
123
|
-
process.env.INIT_CWD = 'C:\\nonexistent\\path';
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const { getProjectRoot } = await import('../commands/utils/PathHelper.js');
|
|
127
|
-
const root = getProjectRoot(import.meta.url);
|
|
128
|
-
assert.ok(root, 'should return a path');
|
|
129
|
-
assert.ok(fs.existsSync(root), `fallback path should exist: ${root}`);
|
|
130
|
-
} finally {
|
|
131
|
-
process.env.INIT_CWD = orig;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('joinRoot', () => {
|
|
137
|
-
test('joins segments relative to explicit root path', async () => {
|
|
138
|
-
const { joinRoot } = await import('../commands/utils/PathHelper.js');
|
|
139
|
-
const root = path.resolve('/test/project');
|
|
140
|
-
const result = joinRoot(root, 'src', 'components');
|
|
141
|
-
assert.equal(result, path.join(root, 'src', 'components'));
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('sanitizes leading slashes from segments', async () => {
|
|
145
|
-
const { joinRoot } = await import('../commands/utils/PathHelper.js');
|
|
146
|
-
const root = path.resolve('/test/project');
|
|
147
|
-
const result = joinRoot(root, '/src', '/components');
|
|
148
|
-
assert.equal(result, path.join(root, 'src', 'components'));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('handles single segment', async () => {
|
|
152
|
-
const { joinRoot } = await import('../commands/utils/PathHelper.js');
|
|
153
|
-
const root = path.resolve('/test/project');
|
|
154
|
-
const result = joinRoot(root, 'package.json');
|
|
155
|
-
assert.equal(result, path.join(root, 'package.json'));
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('handles empty segments', async () => {
|
|
159
|
-
const { joinRoot } = await import('../commands/utils/PathHelper.js');
|
|
160
|
-
const root = path.resolve('/test/project');
|
|
161
|
-
const result = joinRoot(root);
|
|
162
|
-
assert.equal(result, root);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('getConfigPath with explicit root', () => {
|
|
167
|
-
test('returns sliceConfig.json under explicit root', async () => {
|
|
168
|
-
const { getConfigPath } = await import('../commands/utils/PathHelper.js');
|
|
169
|
-
const explicitRoot = path.resolve('/custom/project');
|
|
170
|
-
const result = getConfigPath(import.meta.url, explicitRoot);
|
|
171
|
-
assert.equal(result, path.join(explicitRoot, 'src', 'sliceConfig.json'));
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('returns sliceConfig.json without root (auto-resolve)', async () => {
|
|
175
|
-
const { getConfigPath } = await import('../commands/utils/PathHelper.js');
|
|
176
|
-
const result = getConfigPath(import.meta.url);
|
|
177
|
-
assert.ok(result.endsWith('sliceConfig.json'));
|
|
178
|
-
assert.ok(fs.existsSync(result));
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('getComponentsJsPath with explicit root', () => {
|
|
183
|
-
test('returns components.js under explicit root', async () => {
|
|
184
|
-
const { getComponentsJsPath } = await import('../commands/utils/PathHelper.js');
|
|
185
|
-
const explicitRoot = path.resolve('/custom/project');
|
|
186
|
-
const result = getComponentsJsPath(import.meta.url, explicitRoot);
|
|
187
|
-
assert.equal(result, path.join(explicitRoot, 'src', 'Components', 'components.js'));
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('returns components.js without root (auto-resolve)', async () => {
|
|
191
|
-
const { getComponentsJsPath } = await import('../commands/utils/PathHelper.js');
|
|
192
|
-
const result = getComponentsJsPath(import.meta.url);
|
|
193
|
-
assert.ok(result.endsWith('components.js'));
|
|
194
|
-
assert.ok(fs.existsSync(result));
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe('getSrcPath integration', () => {
|
|
199
|
-
test('getSrcPath matches joinRoot with src prefix', async () => {
|
|
200
|
-
const { getSrcPath, getProjectRoot, joinRoot } = await import('../commands/utils/PathHelper.js');
|
|
201
|
-
const root = getProjectRoot(import.meta.url);
|
|
202
|
-
const viaHelper = getSrcPath(import.meta.url, 'Components', 'components.js');
|
|
203
|
-
const viaJoinRoot = joinRoot(root, 'src', 'Components', 'components.js');
|
|
204
|
-
assert.equal(viaHelper, viaJoinRoot);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import fs from 'fs-extra';
|
|
6
|
-
import { createTestProject, cleanupTestProject } from './helpers/setup.js';
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const FIXTURES = path.resolve(__dirname, 'e2e/fixtures/components');
|
|
10
|
-
const FW_PKG = path.resolve(__dirname, '../node_modules/slicejs-web-framework');
|
|
11
|
-
|
|
12
|
-
// Performance budgets for the realistic starter app (App Shell + the vendored
|
|
13
|
-
// starter components). Bundle byte sizes are fully deterministic, so a surprise
|
|
14
|
-
// failure here means a change degraded the output — review it, and only bump a
|
|
15
|
-
// budget when the growth is intentional. (Actuals shown for reference.)
|
|
16
|
-
const BUDGET = {
|
|
17
|
-
bundleCount: 5, // actual: 4
|
|
18
|
-
totalBundleBytes: 165_000, // actual: ~133.6 KB
|
|
19
|
-
criticalBytes: 51_200, // actual: ~40.8 KB (bundler's own critical cap is 50 KB)
|
|
20
|
-
frameworkBytes: 85_000, // actual: ~67.4 KB (tracks the slicejs-web-framework package)
|
|
21
|
-
buildMs: 30_000, // actual: ~1 s — generous ceiling to catch pathological slowdowns
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
async function assembleAndBuild() {
|
|
25
|
-
process.env.NODE_ENV = 'production';
|
|
26
|
-
const app = await createTestProject();
|
|
27
|
-
|
|
28
|
-
await fs.ensureDir(path.join(app, 'node_modules'));
|
|
29
|
-
await fs.copy(FW_PKG, path.join(app, 'node_modules', 'slicejs-web-framework'), { dereference: true });
|
|
30
|
-
|
|
31
|
-
await fs.copy(path.join(FIXTURES, 'Visual'), path.join(app, 'src', 'Components', 'Visual'));
|
|
32
|
-
await fs.copy(path.join(FIXTURES, 'Service'), path.join(app, 'src', 'Components', 'Service'));
|
|
33
|
-
|
|
34
|
-
process.env.INIT_CWD = app;
|
|
35
|
-
(await import('../commands/listComponents/listComponents.js')).default();
|
|
36
|
-
|
|
37
|
-
const build = (await import('../commands/build/build.js')).default;
|
|
38
|
-
const start = Date.now();
|
|
39
|
-
const ok = await build({ minify: true, obfuscate: true });
|
|
40
|
-
const buildMs = Date.now() - start;
|
|
41
|
-
assert.equal(ok, true, 'build should succeed');
|
|
42
|
-
return { app, buildMs };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
test('production build stays within performance budgets', async () => {
|
|
46
|
-
const { app, buildMs } = await assembleAndBuild();
|
|
47
|
-
try {
|
|
48
|
-
const bundlesDir = path.join(app, 'dist', 'bundles');
|
|
49
|
-
const files = (await fs.readdir(bundlesDir)).filter(
|
|
50
|
-
(f) => f.startsWith('slice-bundle.') && f.endsWith('.js')
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const sizes = {};
|
|
54
|
-
let totalBundleBytes = 0;
|
|
55
|
-
for (const f of files) {
|
|
56
|
-
sizes[f] = (await fs.stat(path.join(bundlesDir, f))).size;
|
|
57
|
-
totalBundleBytes += sizes[f];
|
|
58
|
-
}
|
|
59
|
-
const cfg = await fs.readJson(path.join(bundlesDir, 'bundle.config.json'));
|
|
60
|
-
const criticalBytes = sizes['slice-bundle.critical.js'] || 0;
|
|
61
|
-
const frameworkBytes = sizes['slice-bundle.framework.js'] || 0;
|
|
62
|
-
|
|
63
|
-
// Always print the metrics so trends are visible even on a passing run.
|
|
64
|
-
console.log(
|
|
65
|
-
`\n [perf] bundles=${files.length} total=${Math.round(totalBundleBytes / 1024)}KB ` +
|
|
66
|
-
`critical=${Math.round(criticalBytes / 1024)}KB framework=${Math.round(frameworkBytes / 1024)}KB ` +
|
|
67
|
-
`requestReduction=${100 - Math.round((files.length / cfg.stats.totalComponents) * 100)}% build=${buildMs}ms`
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
assert.ok(files.length <= BUDGET.bundleCount, `bundle count ${files.length} exceeds budget ${BUDGET.bundleCount}`);
|
|
71
|
-
assert.ok(
|
|
72
|
-
totalBundleBytes <= BUDGET.totalBundleBytes,
|
|
73
|
-
`total bundle bytes ${totalBundleBytes} exceeds budget ${BUDGET.totalBundleBytes}`
|
|
74
|
-
);
|
|
75
|
-
assert.ok(criticalBytes <= BUDGET.criticalBytes, `critical bundle ${criticalBytes} exceeds budget ${BUDGET.criticalBytes}`);
|
|
76
|
-
assert.ok(frameworkBytes <= BUDGET.frameworkBytes, `framework bundle ${frameworkBytes} exceeds budget ${BUDGET.frameworkBytes}`);
|
|
77
|
-
assert.ok(buildMs <= BUDGET.buildMs, `build took ${buildMs}ms, over budget ${BUDGET.buildMs}ms`);
|
|
78
|
-
|
|
79
|
-
// Sanity: the realistic app actually bundled its full component set.
|
|
80
|
-
assert.ok(cfg.stats.totalComponents >= 20, `unexpectedly few components bundled: ${cfg.stats.totalComponents}`);
|
|
81
|
-
} finally {
|
|
82
|
-
await cleanupTestProject(app);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { test, describe } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { SLICE_SCRIPTS, verifySliceScriptsInPackageJson } from '../commands/utils/sliceScripts.js';
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const clientPath = path.join(__dirname, '..', 'client.js');
|
|
10
|
-
const source = fs.readFileSync(clientPath, 'utf-8');
|
|
11
|
-
const initSource = fs.readFileSync(path.join(__dirname, '..', 'commands', 'init', 'init.js'), 'utf-8');
|
|
12
|
-
|
|
13
|
-
test('postinstall command is registered in client.js', () => {
|
|
14
|
-
const hasSetupCommand = source.includes('.command("postinstall")');
|
|
15
|
-
assert.ok(hasSetupCommand, 'client.js must register a "postinstall" command');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('postinstall command has a description', () => {
|
|
19
|
-
const match = source.match(/\.command\(["']postinstall["']\)\s*\.description\(["']([^"']+)["']\)/);
|
|
20
|
-
assert.ok(match, 'postinstall command must have a .description() call');
|
|
21
|
-
assert.ok(match[1].length > 0, 'postinstall command description must not be empty');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('postinstall command checks npm_config_global environment variable', () => {
|
|
25
|
-
const hasGlobalCheck = source.includes('npm_config_global');
|
|
26
|
-
assert.ok(hasGlobalCheck, 'postinstall command must check npm_config_global to detect global installation');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('postinstall command prints global install warning', () => {
|
|
30
|
-
const hasGlobalWarning = source.includes('Global installation');
|
|
31
|
-
assert.ok(hasGlobalWarning, 'postinstall command must warn about global installation');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('postinstall command prints local install success message', () => {
|
|
35
|
-
const hasSuccessMsg = source.includes('installed successfully');
|
|
36
|
-
assert.ok(hasSuccessMsg, 'postinstall command must show success message for local installs');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('postinstall command uses getProjectRoot from PathHelper', () => {
|
|
40
|
-
const hasPathHelper = source.includes("getProjectRoot(import.meta.url)");
|
|
41
|
-
assert.ok(hasPathHelper, 'postinstall command must use getProjectRoot resolver');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('postinstall command writes npm scripts to package.json via fs', () => {
|
|
45
|
-
const hasFsWrite = source.includes('writeFileSync(pkgPath');
|
|
46
|
-
assert.ok(hasFsWrite, 'postinstall command must write scripts to package.json');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('SLICE_SCRIPTS contains every slice:* command', () => {
|
|
50
|
-
const expected = [
|
|
51
|
-
'slice:init', 'slice:dev', 'slice:build', 'slice:start',
|
|
52
|
-
'slice:create', 'slice:list', 'slice:delete',
|
|
53
|
-
'slice:get', 'slice:browse', 'slice:sync',
|
|
54
|
-
'slice:doctor', 'slice:version', 'slice:help', 'slice:types'
|
|
55
|
-
];
|
|
56
|
-
for (const script of expected) {
|
|
57
|
-
assert.ok(SLICE_SCRIPTS[script], `SLICE_SCRIPTS must define ${script}`);
|
|
58
|
-
assert.ok(SLICE_SCRIPTS[script].includes('slicejs-cli/client.js'), `${script} must run the local slicejs-cli client`);
|
|
59
|
-
}
|
|
60
|
-
assert.equal(Object.keys(SLICE_SCRIPTS).length, expected.length, 'no unexpected scripts in SLICE_SCRIPTS');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('postinstall command and slice init share the same SLICE_SCRIPTS', () => {
|
|
64
|
-
assert.ok(source.includes('SLICE_SCRIPTS'), 'client.js postinstall must use SLICE_SCRIPTS');
|
|
65
|
-
assert.ok(source.includes("from './commands/utils/sliceScripts.js'"), 'client.js must import sliceScripts.js');
|
|
66
|
-
assert.ok(source.includes('verifySliceScriptsInPackageJson'), 'client.js postinstall must import verifySliceScriptsInPackageJson');
|
|
67
|
-
assert.ok(initSource.includes('Object.assign(pkg.scripts, SLICE_SCRIPTS)'), 'init.js must apply SLICE_SCRIPTS');
|
|
68
|
-
assert.ok(initSource.includes("from '../utils/sliceScripts.js'"), 'init.js must import sliceScripts.js');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('postinstall command action is a function', () => {
|
|
72
|
-
const SetupActionPattern = /\.command\(["']postinstall["']\)[\s\S]*?\.action\(\(\)\s*=>\s*\{[\s\S]*?\}\)/;
|
|
73
|
-
const match = source.match(SetupActionPattern);
|
|
74
|
-
assert.ok(match, 'postinstall command must have an .action() with arrow function');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('postinstall command provides npm uninstall -g instruction for global installs', () => {
|
|
78
|
-
const hasUninstallInstruction = source.includes('npm uninstall -g slicejs-cli');
|
|
79
|
-
assert.ok(hasUninstallInstruction, 'postinstall command must tell users how to uninstall global CLI');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('verifySliceScriptsInPackageJson', () => {
|
|
83
|
-
test('returns empty missing when all scripts are present', () => {
|
|
84
|
-
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
85
|
-
const { missing } = verifySliceScriptsInPackageJson(pkgPath);
|
|
86
|
-
const expected = Object.keys(SLICE_SCRIPTS);
|
|
87
|
-
for (const script of expected) {
|
|
88
|
-
assert.ok(!missing.includes(script), `${script} should not be missing from the CLI package.json`);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('returns all scripts as missing when pkg has no scripts', () => {
|
|
93
|
-
const tmpDir = fs.mkdtempSync(path.join(__dirname, '..', 'tmp-test-'));
|
|
94
|
-
try {
|
|
95
|
-
const pkgPath = path.join(tmpDir, 'package.json');
|
|
96
|
-
fs.writeFileSync(pkgPath, JSON.stringify({ name: 'test' }));
|
|
97
|
-
const { missing } = verifySliceScriptsInPackageJson(pkgPath);
|
|
98
|
-
assert.equal(missing.length, Object.keys(SLICE_SCRIPTS).length, 'all scripts should be missing');
|
|
99
|
-
} finally {
|
|
100
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
});
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { test, describe } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { minify } from 'terser';
|
|
4
|
-
|
|
5
|
-
const BUGGY_REGEX = /^(slice|_|\$|on[A-Z]|get|set|has|is)/;
|
|
6
|
-
|
|
7
|
-
const terserConfig = (regex) => ({
|
|
8
|
-
compress: { drop_console: false, drop_debugger: true, unused: false, side_effects: false, reduce_vars: false, collapse_vars: false },
|
|
9
|
-
mangle: {
|
|
10
|
-
reserved: ['slice', 'window', 'document', 'HTMLElement', 'addEventListener', 'removeEventListener', 'customElements', 'define', 'fetch', 'setTimeout', 'clearTimeout', 'console'],
|
|
11
|
-
properties: regex ? { regex } : false
|
|
12
|
-
},
|
|
13
|
-
format: { comments: false, beautify: false },
|
|
14
|
-
keep_fnames: true,
|
|
15
|
-
keep_classnames: true
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
async function mangleProps(code, regex) {
|
|
19
|
-
const result = await minify(code, terserConfig(regex));
|
|
20
|
-
if (result.error) throw result.error;
|
|
21
|
-
return result.code;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('property mangling — cross-file bug', () => {
|
|
25
|
-
|
|
26
|
-
describe('RED: buggy regex proves cross-file renaming differs', () => {
|
|
27
|
-
|
|
28
|
-
test('onClick is mangled to different short names in each file', async () => {
|
|
29
|
-
const fileA = `
|
|
30
|
-
class Pagination {
|
|
31
|
-
static props = { _p1: 0, sliceX: 0, onClick: { type: 'function' }, onChange: { type: 'function' } }
|
|
32
|
-
fire() { this.onClick() }
|
|
33
|
-
}
|
|
34
|
-
`;
|
|
35
|
-
const fileB = `
|
|
36
|
-
class Table {
|
|
37
|
-
trigger(comp) { comp.onClick() }
|
|
38
|
-
}
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
const [minA, minB] = await Promise.all([
|
|
42
|
-
mangleProps(fileA, BUGGY_REGEX),
|
|
43
|
-
mangleProps(fileB, BUGGY_REGEX),
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
assert.doesNotMatch(minA, /onClick/, 'A: onClick must be mangled');
|
|
47
|
-
assert.doesNotMatch(minB, /onClick/, 'B: onClick must be mangled');
|
|
48
|
-
|
|
49
|
-
const thisCalls = [...minA.matchAll(/this\.([a-zA-Z])\s*\(/g)].map(m => m[1]);
|
|
50
|
-
const paramCalls = [...minB.matchAll(/\w+\.([a-zA-Z])\s*\(/g)].filter(m => m[1] !== 'props').map(m => m[1]);
|
|
51
|
-
|
|
52
|
-
assert.equal(thisCalls.length, 1, 'A: fire() calls this.onMangledClick()');
|
|
53
|
-
assert.equal(paramCalls.length, 1, 'B: trigger() calls comp.onMangledClick()');
|
|
54
|
-
|
|
55
|
-
assert.notEqual(
|
|
56
|
-
thisCalls[0], paramCalls[0],
|
|
57
|
-
`Mangled names differ: A=this.${thisCalls[0]}(), B=comp.${paramCalls[0]}()\n` +
|
|
58
|
-
` A: ${minA}\n B: ${minB}`
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('sliceId is mangled (should be preserved for cross-file access)', async () => {
|
|
63
|
-
const code = `class Foo { get() { return this.sliceId } }`;
|
|
64
|
-
const min = await mangleProps(code, BUGGY_REGEX);
|
|
65
|
-
assert.doesNotMatch(min, /sliceId/, 'sliceId should be mangled by buggy regex');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('setPage is mangled to different short names in each file', async () => {
|
|
69
|
-
const fileA = `
|
|
70
|
-
class DataGridEngine {
|
|
71
|
-
doIt() { this._state(); this.setSort(); this.setPage(); this.setPageSize() }
|
|
72
|
-
}
|
|
73
|
-
`;
|
|
74
|
-
const fileB = `
|
|
75
|
-
class Table {
|
|
76
|
-
handle(p) { p.setPage() }
|
|
77
|
-
}
|
|
78
|
-
`;
|
|
79
|
-
|
|
80
|
-
const [minA, minB] = await Promise.all([
|
|
81
|
-
mangleProps(fileA, BUGGY_REGEX),
|
|
82
|
-
mangleProps(fileB, BUGGY_REGEX),
|
|
83
|
-
]);
|
|
84
|
-
|
|
85
|
-
assert.doesNotMatch(minA, /setPage/, 'A: setPage must be mangled');
|
|
86
|
-
assert.doesNotMatch(minB, /setPage/, 'B: setPage must be mangled');
|
|
87
|
-
|
|
88
|
-
const thisCallsA = [...minA.matchAll(/this\.([a-zA-Z])\s*\(/g)].map(m => m[1]);
|
|
89
|
-
const paramCallsB = [...minB.matchAll(/\w+\.([a-zA-Z])\s*\(/g)].map(m => m[1]);
|
|
90
|
-
|
|
91
|
-
assert.equal(thisCallsA.length, 4, 'A should have 4 this.X() calls');
|
|
92
|
-
assert.equal(paramCallsB.length, 1, 'B should have 1 p.X() call');
|
|
93
|
-
assert.notEqual(
|
|
94
|
-
thisCallsA[2], paramCallsB[0],
|
|
95
|
-
`setPage is "${thisCallsA[2]}" in A but "${paramCallsB[0]}" in B\n` +
|
|
96
|
-
` A: ${minA}\n B: ${minB}`
|
|
97
|
-
);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('GREEN: fix variations prevent the bug', () => {
|
|
103
|
-
|
|
104
|
-
const FIXES = {
|
|
105
|
-
'properties: false (no mangling)': null,
|
|
106
|
-
'regex: /^_/ (private only)': /^_/,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
for (const [label, regex] of Object.entries(FIXES)) {
|
|
110
|
-
|
|
111
|
-
test(`${label}: onClick preserved (cross-file safe)`, async () => {
|
|
112
|
-
const fileA = `
|
|
113
|
-
class Pagination {
|
|
114
|
-
static props = { _p1: 0, sliceX: 0, onClick: { type: 'function' }, onChange: { type: 'function' } }
|
|
115
|
-
fire() { this.onClick() }
|
|
116
|
-
}
|
|
117
|
-
`;
|
|
118
|
-
const fileB = `class Table { trigger(comp) { comp.onClick() } }`;
|
|
119
|
-
|
|
120
|
-
const [minA, minB] = await Promise.all([
|
|
121
|
-
mangleProps(fileA, regex),
|
|
122
|
-
mangleProps(fileB, regex),
|
|
123
|
-
]);
|
|
124
|
-
|
|
125
|
-
assert.ok(minA.includes('onClick'), `A must preserve onClick: ${minA}`);
|
|
126
|
-
assert.ok(minB.includes('onClick'), `B must preserve onClick: ${minB}`);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test(`${label}: sliceId preserved (cross-file safe)`, async () => {
|
|
130
|
-
const fileA = `class Foo { get() { return this.sliceId } }`;
|
|
131
|
-
const fileB = `class Bar { read(target) { return target.sliceId } }`;
|
|
132
|
-
|
|
133
|
-
const [minA, minB] = await Promise.all([
|
|
134
|
-
mangleProps(fileA, regex),
|
|
135
|
-
mangleProps(fileB, regex),
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
assert.ok(minA.includes('sliceId'), `A must preserve sliceId`);
|
|
139
|
-
assert.ok(minB.includes('sliceId'), `B must preserve sliceId`);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test(`${label}: setPage preserved (cross-file safe)`, async () => {
|
|
143
|
-
const fileA = `class Engine { handle(t) { t.setPage(1) } }`;
|
|
144
|
-
const fileB = `class Table { trigger(comp) { comp.setPage() } }`;
|
|
145
|
-
|
|
146
|
-
const [minA, minB] = await Promise.all([
|
|
147
|
-
mangleProps(fileA, regex),
|
|
148
|
-
mangleProps(fileB, regex),
|
|
149
|
-
]);
|
|
150
|
-
|
|
151
|
-
assert.ok(minA.includes('setPage'), `A must preserve setPage: ${minA}`);
|
|
152
|
-
assert.ok(minB.includes('setPage'), `B must preserve setPage: ${minB}`);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('underscore-prefixed private props still get mangled', () => {
|
|
160
|
-
|
|
161
|
-
test('regex /^_/: _internal is mangled', async () => {
|
|
162
|
-
const code = `class Foo { constructor() { this._internal = 42; this.publicProp = 'x' } }`;
|
|
163
|
-
const min = await mangleProps(code, /^_/);
|
|
164
|
-
assert.doesNotMatch(min, /_internal/, '_internal should be mangled');
|
|
165
|
-
assert.ok(min.includes('publicProp'), 'publicProp preserved');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('regex /^_/: $secret is NOT mangled', async () => {
|
|
169
|
-
const code = `class Foo { save() { this.$secret = 42 } }`;
|
|
170
|
-
const min = await mangleProps(code, /^_/);
|
|
171
|
-
assert.ok(min.includes('$secret'), '$secret preserved with /^_/');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('properties: false: _internal is preserved (no mangling at all)', async () => {
|
|
175
|
-
const code = `class Foo { constructor() { this._internal = 42 } }`;
|
|
176
|
-
const min = await mangleProps(code, null);
|
|
177
|
-
assert.ok(min.includes('_internal'), '_internal survives with properties:false');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
});
|