slicejs-web-framework 3.4.3 → 3.4.4
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/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
- package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +468 -468
- package/Slice/Components/Structural/Controller/Controller.js +1271 -1198
- package/Slice/Components/Structural/Debugger/Debugger.js +1503 -1503
- package/Slice/Components/Structural/EventManager/EventManager.js +366 -366
- package/Slice/Components/Structural/Logger/Logger.js +225 -225
- package/Slice/Components/Structural/Router/Router.js +775 -775
- package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +106 -106
- package/Slice/Slice.js +650 -627
- 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/fixtures/real-runtime-loader.mjs +21 -21
- package/Slice/tests/public-env-typed-accessors.test.js +66 -66
- package/api/index.js +281 -281
- package/api/utils/publicEnvResolver.js +123 -123
- package/package.json +44 -44
- package/slicejs-cli/.github/workflows/ci.yml +43 -0
- package/slicejs-cli/LICENSE +21 -0
- package/slicejs-cli/assets/Slice.js-logo.png +0 -0
- package/slicejs-cli/client.js +740 -0
- package/slicejs-cli/commands/Print.js +164 -0
- package/slicejs-cli/commands/Validations.js +92 -0
- package/slicejs-cli/commands/build/build.js +40 -0
- package/slicejs-cli/commands/buildProduction/buildProduction.js +577 -0
- package/slicejs-cli/commands/bundle/bundle.js +236 -0
- package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +55 -0
- package/slicejs-cli/commands/createComponent/createComponent.js +128 -0
- package/slicejs-cli/commands/deleteComponent/deleteComponent.js +81 -0
- package/slicejs-cli/commands/doctor/doctor.js +516 -0
- package/slicejs-cli/commands/getComponent/getComponent.js +695 -0
- package/slicejs-cli/commands/init/init.js +473 -0
- package/slicejs-cli/commands/listComponents/listComponents.js +172 -0
- package/slicejs-cli/commands/startServer/startServer.js +240 -0
- package/slicejs-cli/commands/startServer/watchServer.js +66 -0
- package/slicejs-cli/commands/types/types.js +583 -0
- package/slicejs-cli/commands/utils/LocalCliDelegation.js +53 -0
- package/slicejs-cli/commands/utils/PackageManager.js +148 -0
- package/slicejs-cli/commands/utils/PathHelper.js +75 -0
- package/slicejs-cli/commands/utils/VersionChecker.js +169 -0
- package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +2525 -0
- package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +933 -0
- package/slicejs-cli/commands/utils/loadConfig.js +31 -0
- package/slicejs-cli/commands/utils/sliceScripts.js +48 -0
- package/slicejs-cli/commands/utils/updateManager.js +471 -0
- package/slicejs-cli/package.json +71 -0
- package/slicejs-cli/playwright.config.js +51 -0
- package/slicejs-cli/pnpm-lock.yaml +1629 -0
- package/slicejs-cli/tests/build-command-integration.test.js +87 -0
- package/slicejs-cli/tests/build-mangling-e2e.test.js +290 -0
- package/slicejs-cli/tests/build-production-e2e.test.js +140 -0
- package/slicejs-cli/tests/builder-edge-cases.test.js +322 -0
- package/slicejs-cli/tests/bundle-generate-e2e.test.js +115 -0
- package/slicejs-cli/tests/bundle-generator.test.js +691 -0
- package/slicejs-cli/tests/bundle-v2-register-output.test.js +470 -0
- package/slicejs-cli/tests/bundling-dependency-edges.test.js +127 -0
- package/slicejs-cli/tests/bundling-imports-unit.test.js +267 -0
- package/slicejs-cli/tests/client-launcher-contract.test.js +211 -0
- package/slicejs-cli/tests/commands-component-crud.test.js +128 -0
- package/slicejs-cli/tests/commands-doctor.test.js +152 -0
- package/slicejs-cli/tests/commands-version-checker.test.js +37 -0
- package/slicejs-cli/tests/component-registry-parse.test.js +34 -0
- package/slicejs-cli/tests/dependency-analyzer.test.js +24 -0
- package/slicejs-cli/tests/e2e/bundles.spec.js +91 -0
- package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +56 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +136 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +149 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +45 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +106 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +5 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +158 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +33 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +56 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +83 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +164 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +167 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +116 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +44 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +180 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +20 -0
- package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +181 -0
- package/slicejs-cli/tests/e2e/fixtures/components/registry.json +12 -0
- package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +65 -0
- package/slicejs-cli/tests/e2e/navigation.spec.js +44 -0
- package/slicejs-cli/tests/e2e/render.spec.js +34 -0
- package/slicejs-cli/tests/e2e/serve.mjs +264 -0
- package/slicejs-cli/tests/e2e/shared-deps.spec.js +61 -0
- package/slicejs-cli/tests/e2e/unminified.spec.js +33 -0
- package/slicejs-cli/tests/e2e-serve.test.js +148 -0
- package/slicejs-cli/tests/fixtures/components.js +8 -0
- package/slicejs-cli/tests/fixtures/sliceConfig.json +74 -0
- package/slicejs-cli/tests/getcomponent.test.js +435 -0
- package/slicejs-cli/tests/helpers/setup.js +102 -0
- package/slicejs-cli/tests/init-command-contract.test.js +101 -0
- package/slicejs-cli/tests/init-e2e.test.js +82 -0
- package/slicejs-cli/tests/init-project-isolation.test.js +98 -0
- package/slicejs-cli/tests/local-cli-delegation.test.js +81 -0
- package/slicejs-cli/tests/package-manager.test.js +127 -0
- package/slicejs-cli/tests/path-helper.test.js +206 -0
- package/slicejs-cli/tests/perf-budget.test.js +84 -0
- package/slicejs-cli/tests/postinstall-command.test.js +103 -0
- package/slicejs-cli/tests/property-mangling-regression.test.js +182 -0
- package/slicejs-cli/tests/types-breakage.test.js +491 -0
- package/slicejs-cli/tests/types-generator-errors.test.js +361 -0
- package/slicejs-cli/tests/types-generator.test.js +348 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse } from '@babel/parser';
|
|
6
|
+
import { withTestProject } from './helpers/setup.js';
|
|
7
|
+
import build from '../commands/build/build.js';
|
|
8
|
+
import { cleanBundles, bundleInfo } from '../commands/bundle/bundle.js';
|
|
9
|
+
|
|
10
|
+
const MODULE_URL = import.meta.url;
|
|
11
|
+
|
|
12
|
+
describe('slice build (full pipeline: buildProduction + bundle -> dist)', () => {
|
|
13
|
+
test('produces a dist/ with parseable bundles', async () => {
|
|
14
|
+
await withTestProject(async (root) => {
|
|
15
|
+
const ok = await build({ minify: false, obfuscate: false });
|
|
16
|
+
assert.equal(ok, true);
|
|
17
|
+
|
|
18
|
+
const dist = path.join(root, 'dist');
|
|
19
|
+
assert.ok(await fs.pathExists(path.join(dist, 'App', 'index.js')));
|
|
20
|
+
|
|
21
|
+
const bundlesDir = path.join(dist, 'bundles');
|
|
22
|
+
assert.ok(await fs.pathExists(bundlesDir), 'dist/bundles should exist');
|
|
23
|
+
|
|
24
|
+
const bundleFiles = (await fs.readdir(bundlesDir)).filter(
|
|
25
|
+
(f) => f.startsWith('slice-bundle.') && f.endsWith('.js')
|
|
26
|
+
);
|
|
27
|
+
assert.ok(bundleFiles.length > 0, 'at least one bundle in dist/bundles');
|
|
28
|
+
|
|
29
|
+
for (const f of bundleFiles) {
|
|
30
|
+
const code = await fs.readFile(path.join(bundlesDir, f), 'utf8');
|
|
31
|
+
assert.doesNotThrow(
|
|
32
|
+
() => parse(code, { sourceType: 'module', plugins: ['jsx'] }),
|
|
33
|
+
`dist bundle ${f} is not valid JS`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('bundle clean / info subcommands', () => {
|
|
41
|
+
test('cleanBundles removes slice-bundle.* files and the config', async () => {
|
|
42
|
+
await withTestProject(async (root) => {
|
|
43
|
+
const src = path.join(root, 'src');
|
|
44
|
+
await fs.writeFile(path.join(src, 'slice-bundle.critical.js'), '// x');
|
|
45
|
+
await fs.writeFile(path.join(src, 'slice-bundle.home.js'), '// x');
|
|
46
|
+
await fs.writeFile(path.join(src, 'bundle.config.json'), '{}');
|
|
47
|
+
|
|
48
|
+
await cleanBundles();
|
|
49
|
+
|
|
50
|
+
assert.equal(await fs.pathExists(path.join(src, 'slice-bundle.critical.js')), false);
|
|
51
|
+
assert.equal(await fs.pathExists(path.join(src, 'slice-bundle.home.js')), false);
|
|
52
|
+
assert.equal(await fs.pathExists(path.join(src, 'bundle.config.json')), false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('cleanBundles is a no-op (no throw) when there are no bundles', async () => {
|
|
57
|
+
await withTestProject(async () => {
|
|
58
|
+
await assert.doesNotReject(() => cleanBundles());
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('bundleInfo does not throw when a config exists', async () => {
|
|
63
|
+
await withTestProject(async (root) => {
|
|
64
|
+
const cfg = {
|
|
65
|
+
version: '2.0.0',
|
|
66
|
+
strategy: 'hybrid',
|
|
67
|
+
generated: '2025-01-01T00:00:00.000Z',
|
|
68
|
+
stats: {
|
|
69
|
+
totalComponents: 3,
|
|
70
|
+
totalRoutes: 2,
|
|
71
|
+
sharedComponents: 1,
|
|
72
|
+
sharedPercentage: 33,
|
|
73
|
+
totalSize: 2048,
|
|
74
|
+
},
|
|
75
|
+
bundles: { critical: { components: ['A'], size: 1024 }, routes: {} },
|
|
76
|
+
};
|
|
77
|
+
await fs.writeJson(path.join(root, 'src', 'bundle.config.json'), cfg);
|
|
78
|
+
await assert.doesNotReject(() => bundleInfo());
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('bundleInfo warns (no throw) when config is missing', async () => {
|
|
83
|
+
await withTestProject(async () => {
|
|
84
|
+
await assert.doesNotReject(() => bundleInfo());
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse } from '@babel/parser';
|
|
6
|
+
import { withTestProject } from './helpers/setup.js';
|
|
7
|
+
import buildProduction from '../commands/buildProduction/buildProduction.js';
|
|
8
|
+
|
|
9
|
+
const MODULE_URL = import.meta.url;
|
|
10
|
+
|
|
11
|
+
async function writeSrc(root, rel, content) {
|
|
12
|
+
const p = path.join(root, 'src', rel);
|
|
13
|
+
await fs.ensureDir(path.dirname(p));
|
|
14
|
+
await fs.writeFile(p, content, 'utf8');
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function collectBuiltFiles(distDir, pattern) {
|
|
19
|
+
const results = [];
|
|
20
|
+
function walk(dir) {
|
|
21
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
22
|
+
for (const e of entries) {
|
|
23
|
+
const full = path.join(dir, e.name);
|
|
24
|
+
if (e.isDirectory()) walk(full);
|
|
25
|
+
else if (e.name.endsWith('.js') && (pattern ? e.name.match(pattern) : true)) results.push(full);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
walk(distDir);
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('e2e: property mangling with regex /^_/', () => {
|
|
33
|
+
|
|
34
|
+
describe('build succeeds and preserves public props', () => {
|
|
35
|
+
|
|
36
|
+
test('build completes successfully', async () => {
|
|
37
|
+
await withTestProject(async (root) => {
|
|
38
|
+
const ok = await buildProduction({ minify: true });
|
|
39
|
+
assert.equal(ok, true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('onClick preserved across all built component files', async () => {
|
|
44
|
+
await withTestProject(async (root) => {
|
|
45
|
+
await writeSrc(root, 'Components/Visual/Pagination/Pagination.js', `
|
|
46
|
+
import { Component } from '../../App/index.js';
|
|
47
|
+
class Pagination extends HTMLElement {
|
|
48
|
+
static props = {
|
|
49
|
+
currentPage: { type: 'number' },
|
|
50
|
+
totalPages: { type: 'number' },
|
|
51
|
+
onClick: { type: 'function' },
|
|
52
|
+
onChange: { type: 'function' }
|
|
53
|
+
}
|
|
54
|
+
handleClick() { this.onClick() }
|
|
55
|
+
}
|
|
56
|
+
customElements.define('slice-pagination', Pagination);
|
|
57
|
+
export { Pagination };
|
|
58
|
+
`);
|
|
59
|
+
await writeSrc(root, 'Components/Visual/Table/Table.js', `
|
|
60
|
+
import { Component } from '../../App/index.js';
|
|
61
|
+
class Table extends HTMLElement {
|
|
62
|
+
static props = {
|
|
63
|
+
items: { type: 'array' },
|
|
64
|
+
onClick: { type: 'function' },
|
|
65
|
+
onChange: { type: 'function' },
|
|
66
|
+
setPage: { type: 'function' },
|
|
67
|
+
sliceId: { type: 'string' }
|
|
68
|
+
}
|
|
69
|
+
handle(target) { target.onClick(); target.onChange(); target.setPage(1); }
|
|
70
|
+
getId() { return this.sliceId }
|
|
71
|
+
}
|
|
72
|
+
customElements.define('slice-table', Table);
|
|
73
|
+
export { Table };
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
const ok = await buildProduction({ minify: true });
|
|
77
|
+
assert.equal(ok, true);
|
|
78
|
+
|
|
79
|
+
const builtFiles = collectBuiltFiles(root + '/dist');
|
|
80
|
+
const allContent = builtFiles.map(f => fs.readFileSync(f, 'utf8')).join('\n');
|
|
81
|
+
|
|
82
|
+
assert.ok(allContent.includes('onClick'), '"onClick" must survive mangling');
|
|
83
|
+
assert.ok(allContent.includes('onChange'), '"onChange" must survive mangling');
|
|
84
|
+
assert.ok(allContent.includes('sliceId'), '"sliceId" must survive mangling');
|
|
85
|
+
assert.ok(allContent.includes('setPage'), '"setPage" must survive mangling');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('_private properties ARE mangled', async () => {
|
|
90
|
+
await withTestProject(async (root) => {
|
|
91
|
+
await writeSrc(root, 'Components/Visual/Toast/Toast.js', `
|
|
92
|
+
import { Component } from '../../App/index.js';
|
|
93
|
+
class Toast extends HTMLElement {
|
|
94
|
+
static props = { message: { type: 'string' }, type: { type: 'string' }, duration: { type: 'number' } }
|
|
95
|
+
set message(v) { this._message = v; this.render(); }
|
|
96
|
+
set type(v) { this._type = v; this.render(); }
|
|
97
|
+
set duration(v) { this._duration = v; this.initTimer(); }
|
|
98
|
+
get message() { return this._message }
|
|
99
|
+
get type() { return this._type }
|
|
100
|
+
get duration() { return this._duration }
|
|
101
|
+
render() { this.shadowRoot.innerHTML = this._message }
|
|
102
|
+
initTimer() { setTimeout(() => this.hide(), this._duration) }
|
|
103
|
+
hide() {}
|
|
104
|
+
}
|
|
105
|
+
customElements.define('slice-toast', Toast);
|
|
106
|
+
export { Toast };
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
const ok = await buildProduction({ minify: true });
|
|
110
|
+
assert.equal(ok, true);
|
|
111
|
+
|
|
112
|
+
const distJs = path.join(root, 'dist', 'Components', 'Visual', 'Toast', 'Toast.js');
|
|
113
|
+
const built = fs.readFileSync(distJs, 'utf8');
|
|
114
|
+
|
|
115
|
+
assert.doesNotMatch(built, /_message/, '_message backing field must be mangled');
|
|
116
|
+
assert.doesNotMatch(built, /_type/, '_type backing field must be mangled');
|
|
117
|
+
assert.doesNotMatch(built, /_duration/, '_duration backing field must be mangled');
|
|
118
|
+
|
|
119
|
+
assert.doesNotThrow(() => parse(built, { sourceType: 'module' }), 'output must be valid JS');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('cross-file access — public props preserved', () => {
|
|
126
|
+
|
|
127
|
+
test('Table calls Pagination.onClick — both preserve same name', async () => {
|
|
128
|
+
await withTestProject(async (root) => {
|
|
129
|
+
await writeSrc(root, 'Components/Visual/Pagination/Pagination.js', `
|
|
130
|
+
class Pagination extends HTMLElement {
|
|
131
|
+
static props = { currentPage: { type: 'number' }, onClick: { type: 'function' } }
|
|
132
|
+
fire() { this.onClick(this.currentPage) }
|
|
133
|
+
}
|
|
134
|
+
customElements.define('slice-pagination', Pagination);
|
|
135
|
+
export { Pagination };
|
|
136
|
+
`);
|
|
137
|
+
await writeSrc(root, 'Components/Visual/Table/Table.js', `
|
|
138
|
+
class Table extends HTMLElement {
|
|
139
|
+
connect(pager) { pager.onClick(this.handlePageChange) }
|
|
140
|
+
handlePageChange(p) {}
|
|
141
|
+
}
|
|
142
|
+
customElements.define('slice-table', Table);
|
|
143
|
+
export { Table };
|
|
144
|
+
`);
|
|
145
|
+
|
|
146
|
+
const ok = await buildProduction({ minify: true });
|
|
147
|
+
assert.equal(ok, true);
|
|
148
|
+
|
|
149
|
+
const distPagination = fs.readFileSync(path.join(root, 'dist', 'Components', 'Visual', 'Pagination', 'Pagination.js'), 'utf8');
|
|
150
|
+
const distTable = fs.readFileSync(path.join(root, 'dist', 'Components', 'Visual', 'Table', 'Table.js'), 'utf8');
|
|
151
|
+
|
|
152
|
+
assert.ok(distPagination.includes('onClick'), 'Pagination must preserve onClick');
|
|
153
|
+
assert.ok(distTable.includes('onClick'), 'Table must preserve onClick');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('TreeView passes onClick to TreeItem — both preserve same name', async () => {
|
|
158
|
+
await withTestProject(async (root) => {
|
|
159
|
+
await writeSrc(root, 'Components/Visual/TreeItem/TreeItem.js', `
|
|
160
|
+
class TreeItem extends HTMLElement {
|
|
161
|
+
static props = { value: { type: 'string' }, onClick: { type: 'function' } }
|
|
162
|
+
handle() { this.onClick() }
|
|
163
|
+
}
|
|
164
|
+
customElements.define('slice-tree-item', TreeItem);
|
|
165
|
+
export { TreeItem };
|
|
166
|
+
`);
|
|
167
|
+
await writeSrc(root, 'Components/Visual/TreeView/TreeView.js', `
|
|
168
|
+
import { TreeItem } from '../TreeItem/TreeItem.js';
|
|
169
|
+
class TreeView extends HTMLElement {
|
|
170
|
+
static props = { items: { type: 'array' }, onClick: { type: 'function' } }
|
|
171
|
+
makeItem() { const item = new TreeItem(); item.onClick = this.onClick; return item; }
|
|
172
|
+
}
|
|
173
|
+
customElements.define('slice-tree-view', TreeView);
|
|
174
|
+
export { TreeView };
|
|
175
|
+
`);
|
|
176
|
+
|
|
177
|
+
const ok = await buildProduction({ minify: true });
|
|
178
|
+
assert.equal(ok, true);
|
|
179
|
+
|
|
180
|
+
const distItem = fs.readFileSync(path.join(root, 'dist', 'Components', 'Visual', 'TreeItem', 'TreeItem.js'), 'utf8');
|
|
181
|
+
const distView = fs.readFileSync(path.join(root, 'dist', 'Components', 'Visual', 'TreeView', 'TreeView.js'), 'utf8');
|
|
182
|
+
|
|
183
|
+
assert.ok(distItem.includes('onClick'), 'TreeItem must preserve onClick');
|
|
184
|
+
assert.ok(distView.includes('onClick'), 'TreeView must preserve onClick');
|
|
185
|
+
|
|
186
|
+
assert.doesNotThrow(() => parse(distItem, { sourceType: 'module' }));
|
|
187
|
+
assert.doesNotThrow(() => parse(distView, { sourceType: 'module' }));
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('MiniInspector accesses sliceId on target — preserved', async () => {
|
|
192
|
+
await withTestProject(async (root) => {
|
|
193
|
+
await writeSrc(root, 'Components/Visual/MiniInspector/MiniInspector.js', `
|
|
194
|
+
class MiniInspector extends HTMLElement {
|
|
195
|
+
inspect(target) { return target.sliceId }
|
|
196
|
+
}
|
|
197
|
+
customElements.define('slice-inspector', MiniInspector);
|
|
198
|
+
export { MiniInspector };
|
|
199
|
+
`);
|
|
200
|
+
await writeSrc(root, 'Components/Visual/Button/Button.js', `
|
|
201
|
+
class Button extends HTMLElement {
|
|
202
|
+
static props = { value: { type: 'string' }, sliceId: { type: 'string' } }
|
|
203
|
+
get id() { return this.sliceId }
|
|
204
|
+
}
|
|
205
|
+
customElements.define('slice-button', Button);
|
|
206
|
+
export { Button };
|
|
207
|
+
`);
|
|
208
|
+
|
|
209
|
+
const ok = await buildProduction({ minify: true });
|
|
210
|
+
assert.equal(ok, true);
|
|
211
|
+
|
|
212
|
+
const built = collectBuiltFiles(root + '/dist', /\.js$/);
|
|
213
|
+
const allContent = built.map(f => fs.readFileSync(f, 'utf8')).join('\n');
|
|
214
|
+
assert.ok(allContent.includes('sliceId'), 'sliceId must survive in all files');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('_private props are consistently mangled within each file', () => {
|
|
221
|
+
|
|
222
|
+
test('internal _ backing fields self-consistent within each component', async () => {
|
|
223
|
+
await withTestProject(async (root) => {
|
|
224
|
+
await writeSrc(root, 'Components/Visual/Counter/Counter.js', `
|
|
225
|
+
class Counter extends HTMLElement {
|
|
226
|
+
static props = { value: { type: 'number' } }
|
|
227
|
+
set value(v) { this._value = v; this.updateDisplay(); }
|
|
228
|
+
get value() { return this._value }
|
|
229
|
+
updateDisplay() { this.textContent = this._value }
|
|
230
|
+
increment() { this._value++; this.updateDisplay(); }
|
|
231
|
+
}
|
|
232
|
+
customElements.define('slice-counter', Counter);
|
|
233
|
+
export { Counter };
|
|
234
|
+
`);
|
|
235
|
+
|
|
236
|
+
const ok = await buildProduction({ minify: true });
|
|
237
|
+
assert.equal(ok, true);
|
|
238
|
+
|
|
239
|
+
const distJs = path.join(root, 'dist', 'Components', 'Visual', 'Counter', 'Counter.js');
|
|
240
|
+
const built = fs.readFileSync(distJs, 'utf8');
|
|
241
|
+
|
|
242
|
+
assert.doesNotMatch(built, /_value/, '_value must be mangled');
|
|
243
|
+
|
|
244
|
+
const shortName = built.match(/this\.([a-zA-Z])\s*=/);
|
|
245
|
+
assert.ok(shortName, 'should have a mangled assignment like this.a =');
|
|
246
|
+
const name = shortName[1];
|
|
247
|
+
|
|
248
|
+
const reads = [...built.matchAll(new RegExp(`this\\.${name}(?!\\w)`, 'g'))];
|
|
249
|
+
const writes = [...built.matchAll(new RegExp(`this\\.${name}\\s*=`, 'g'))];
|
|
250
|
+
assert.ok(reads.length >= 2, `_value -> "${name}" must be read at least twice (getter + updateDisplay), got ${reads.length}`);
|
|
251
|
+
assert.ok(writes.length >= 1, `_value -> "${name}" must be written at least once (setter), got ${writes.length}`);
|
|
252
|
+
|
|
253
|
+
assert.doesNotThrow(() => parse(built, { sourceType: 'module' }));
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('dist/ output integrity', () => {
|
|
260
|
+
|
|
261
|
+
test('all built JS files are valid ES modules', async () => {
|
|
262
|
+
await withTestProject(async (root) => {
|
|
263
|
+
const ok = await buildProduction({ minify: true });
|
|
264
|
+
assert.equal(ok, true);
|
|
265
|
+
|
|
266
|
+
const builtFiles = collectBuiltFiles(root + '/dist', /\.js$/);
|
|
267
|
+
for (const f of builtFiles) {
|
|
268
|
+
const content = fs.readFileSync(f, 'utf8');
|
|
269
|
+
assert.doesNotThrow(
|
|
270
|
+
() => parse(content, { sourceType: 'module', ecmaVersion: 2022 }),
|
|
271
|
+
`${path.relative(root, f)} must be valid JS`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('critical Slice files survive with expected content', async () => {
|
|
278
|
+
await withTestProject(async (root) => {
|
|
279
|
+
const ok = await buildProduction({ minify: true });
|
|
280
|
+
assert.equal(ok, true);
|
|
281
|
+
|
|
282
|
+
const componentsJs = fs.readFileSync(path.join(root, 'dist', 'Components', 'components.js'), 'utf8');
|
|
283
|
+
assert.match(componentsJs, /const components/);
|
|
284
|
+
assert.match(componentsJs, /export default/);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse } from '@babel/parser';
|
|
6
|
+
import { withTestProject } from './helpers/setup.js';
|
|
7
|
+
import buildProduction from '../commands/buildProduction/buildProduction.js';
|
|
8
|
+
|
|
9
|
+
const MODULE_URL = import.meta.url;
|
|
10
|
+
|
|
11
|
+
async function writeSrc(root, rel, content) {
|
|
12
|
+
const p = path.join(root, 'src', rel);
|
|
13
|
+
await fs.ensureDir(path.dirname(p));
|
|
14
|
+
await fs.writeFile(p, content, 'utf8');
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('buildProduction end-to-end', () => {
|
|
19
|
+
test('produces a dist/ that preserves the Slice.js critical files', async () => {
|
|
20
|
+
await withTestProject(async (root) => {
|
|
21
|
+
const ok = await buildProduction({ minify: false });
|
|
22
|
+
assert.equal(ok, true, 'build should succeed on the starter project');
|
|
23
|
+
|
|
24
|
+
const dist = path.join(root, 'dist');
|
|
25
|
+
assert.ok(await fs.pathExists(path.join(dist, 'sliceConfig.json')));
|
|
26
|
+
assert.ok(await fs.pathExists(path.join(dist, 'Components', 'components.js')));
|
|
27
|
+
assert.ok(await fs.pathExists(path.join(dist, 'App', 'index.js')));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('sliceConfig.json is copied verbatim (never minified)', async () => {
|
|
32
|
+
await withTestProject(async (root) => {
|
|
33
|
+
await buildProduction({ minify: true });
|
|
34
|
+
const srcCfg = await fs.readFile(path.join(root, 'src', 'sliceConfig.json'), 'utf8');
|
|
35
|
+
const distCfg = await fs.readFile(path.join(root, 'dist', 'sliceConfig.json'), 'utf8');
|
|
36
|
+
assert.equal(distCfg, srcCfg);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('components.js keeps its registry structure after minification', async () => {
|
|
41
|
+
await withTestProject(async (root) => {
|
|
42
|
+
await buildProduction({ minify: true });
|
|
43
|
+
const built = await fs.readFile(path.join(root, 'dist', 'Components', 'components.js'), 'utf8');
|
|
44
|
+
assert.match(built, /const components/);
|
|
45
|
+
assert.match(built, /export default/);
|
|
46
|
+
assert.doesNotThrow(() => parse(built, { sourceType: 'module' }));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('--no-minify copies JS byte-for-byte', async () => {
|
|
51
|
+
await withTestProject(async (root) => {
|
|
52
|
+
const original = 'export function probe() {\n return 1 + 1;\n}\n';
|
|
53
|
+
await writeSrc(root, 'App/probe.js', original);
|
|
54
|
+
await buildProduction({ minify: false });
|
|
55
|
+
const built = await fs.readFile(path.join(root, 'dist', 'App', 'probe.js'), 'utf8');
|
|
56
|
+
assert.equal(built, original);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('minification preserves reserved Slice identifiers', async () => {
|
|
61
|
+
await withTestProject(async (root) => {
|
|
62
|
+
await writeSrc(
|
|
63
|
+
root,
|
|
64
|
+
'App/probe.js',
|
|
65
|
+
`export function probe() {\n` +
|
|
66
|
+
` const aVeryLongLocalNameThatShouldBeMangled = slice.build('Foo');\n` +
|
|
67
|
+
` return aVeryLongLocalNameThatShouldBeMangled;\n` +
|
|
68
|
+
`}\nclass Controller {}\n`
|
|
69
|
+
);
|
|
70
|
+
await buildProduction({ minify: true });
|
|
71
|
+
const built = await fs.readFile(path.join(root, 'dist', 'App', 'probe.js'), 'utf8');
|
|
72
|
+
assert.match(built, /slice\.build/, 'reserved global "slice" must survive minification');
|
|
73
|
+
assert.match(built, /Controller/, 'reserved class name "Controller" must survive');
|
|
74
|
+
assert.doesNotThrow(() => parse(built, { sourceType: 'module' }));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('CSS is minified (whitespace collapsed)', async () => {
|
|
79
|
+
await withTestProject(async (root) => {
|
|
80
|
+
await writeSrc(root, 'Styles/probe.css', '.a {\n color: red;\n margin: 0;\n}\n');
|
|
81
|
+
await buildProduction({ minify: true });
|
|
82
|
+
const built = await fs.readFile(path.join(root, 'dist', 'Styles', 'probe.css'), 'utf8');
|
|
83
|
+
assert.ok(built.length < 30, `expected minified css, got: ${JSON.stringify(built)}`);
|
|
84
|
+
assert.match(built, /\.a\{/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('HTML minification preserves slice-* attributes', async () => {
|
|
89
|
+
await withTestProject(async (root) => {
|
|
90
|
+
await writeSrc(
|
|
91
|
+
root,
|
|
92
|
+
'App/probe.html',
|
|
93
|
+
'<!DOCTYPE html>\n<html>\n <body>\n <div slice-id="my-component" >hi</div>\n </body>\n</html>\n'
|
|
94
|
+
);
|
|
95
|
+
await buildProduction({ minify: true });
|
|
96
|
+
const built = await fs.readFile(path.join(root, 'dist', 'App', 'probe.html'), 'utf8');
|
|
97
|
+
assert.match(built, /slice-id="my-component"/, 'slice-* attribute must be preserved');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('clean / skip-clean semantics', () => {
|
|
102
|
+
test('a stale dist file is removed by default', async () => {
|
|
103
|
+
await withTestProject(async (root) => {
|
|
104
|
+
const stale = path.join(root, 'dist', 'STALE_ARTIFACT.txt');
|
|
105
|
+
await fs.ensureDir(path.dirname(stale));
|
|
106
|
+
await fs.writeFile(stale, 'old');
|
|
107
|
+
await buildProduction({ minify: false });
|
|
108
|
+
assert.equal(await fs.pathExists(stale), false, 'stale dist file should be cleaned');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('--skip-clean keeps a stale dist file', async () => {
|
|
113
|
+
await withTestProject(async (root) => {
|
|
114
|
+
const stale = path.join(root, 'dist', 'STALE_ARTIFACT.txt');
|
|
115
|
+
await fs.ensureDir(path.dirname(stale));
|
|
116
|
+
await fs.writeFile(stale, 'old');
|
|
117
|
+
await buildProduction({ minify: false, skipClean: true });
|
|
118
|
+
assert.equal(await fs.pathExists(stale), true, 'stale dist file should survive --skip-clean');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('build fails (returns false) when a critical file is missing', async () => {
|
|
124
|
+
await withTestProject(async (root) => {
|
|
125
|
+
await fs.remove(path.join(root, 'src', 'App', 'index.js'));
|
|
126
|
+
const ok = await buildProduction({ minify: false });
|
|
127
|
+
assert.equal(ok, false, 'missing App/index.js must abort the build');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('bundle.config inside a bundles/ folder is renamed to bundle.build.config', async () => {
|
|
132
|
+
await withTestProject(async (root) => {
|
|
133
|
+
await writeSrc(root, 'bundles/bundle.config.json', '{"production":true}');
|
|
134
|
+
await buildProduction({ minify: false });
|
|
135
|
+
const dist = path.join(root, 'dist', 'bundles');
|
|
136
|
+
assert.equal(await fs.pathExists(path.join(dist, 'bundle.build.config.json')), true);
|
|
137
|
+
assert.equal(await fs.pathExists(path.join(dist, 'bundle.config.json')), false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|