slicejs-web-framework 3.4.2 → 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.
Files changed (108) hide show
  1. package/.opencode/opencode.json +14 -14
  2. package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
  3. package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +468 -468
  4. package/Slice/Components/Structural/Controller/Controller.js +1271 -1198
  5. package/Slice/Components/Structural/Debugger/Debugger.js +1503 -1503
  6. package/Slice/Components/Structural/EventManager/EventManager.js +366 -366
  7. package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +126 -5
  8. package/Slice/Components/Structural/Logger/Logger.js +225 -225
  9. package/Slice/Components/Structural/Router/Router.js +775 -775
  10. package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
  11. package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +106 -106
  12. package/Slice/Slice.js +650 -627
  13. package/Slice/tests/build-singleton.test.js +244 -244
  14. package/Slice/tests/bundle-v2-runtime-contract.test.js +738 -738
  15. package/Slice/tests/context-patch-use.test.js +95 -95
  16. package/Slice/tests/fixtures/real-runtime-loader.mjs +21 -21
  17. package/Slice/tests/public-env-typed-accessors.test.js +66 -66
  18. package/api/index.js +281 -281
  19. package/api/utils/publicEnvResolver.js +123 -123
  20. package/package.json +44 -44
  21. package/slicejs-cli/.github/workflows/ci.yml +43 -0
  22. package/slicejs-cli/LICENSE +21 -0
  23. package/slicejs-cli/assets/Slice.js-logo.png +0 -0
  24. package/slicejs-cli/client.js +740 -0
  25. package/slicejs-cli/commands/Print.js +164 -0
  26. package/slicejs-cli/commands/Validations.js +92 -0
  27. package/slicejs-cli/commands/build/build.js +40 -0
  28. package/slicejs-cli/commands/buildProduction/buildProduction.js +577 -0
  29. package/slicejs-cli/commands/bundle/bundle.js +236 -0
  30. package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +55 -0
  31. package/slicejs-cli/commands/createComponent/createComponent.js +128 -0
  32. package/slicejs-cli/commands/deleteComponent/deleteComponent.js +81 -0
  33. package/slicejs-cli/commands/doctor/doctor.js +516 -0
  34. package/slicejs-cli/commands/getComponent/getComponent.js +695 -0
  35. package/slicejs-cli/commands/init/init.js +473 -0
  36. package/slicejs-cli/commands/listComponents/listComponents.js +172 -0
  37. package/slicejs-cli/commands/startServer/startServer.js +240 -0
  38. package/slicejs-cli/commands/startServer/watchServer.js +66 -0
  39. package/slicejs-cli/commands/types/types.js +583 -0
  40. package/slicejs-cli/commands/utils/LocalCliDelegation.js +53 -0
  41. package/slicejs-cli/commands/utils/PackageManager.js +148 -0
  42. package/slicejs-cli/commands/utils/PathHelper.js +75 -0
  43. package/slicejs-cli/commands/utils/VersionChecker.js +169 -0
  44. package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +2525 -0
  45. package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +933 -0
  46. package/slicejs-cli/commands/utils/loadConfig.js +31 -0
  47. package/slicejs-cli/commands/utils/sliceScripts.js +48 -0
  48. package/slicejs-cli/commands/utils/updateManager.js +471 -0
  49. package/slicejs-cli/package.json +71 -0
  50. package/slicejs-cli/playwright.config.js +51 -0
  51. package/slicejs-cli/pnpm-lock.yaml +1629 -0
  52. package/slicejs-cli/tests/build-command-integration.test.js +87 -0
  53. package/slicejs-cli/tests/build-mangling-e2e.test.js +290 -0
  54. package/slicejs-cli/tests/build-production-e2e.test.js +140 -0
  55. package/slicejs-cli/tests/builder-edge-cases.test.js +322 -0
  56. package/slicejs-cli/tests/bundle-generate-e2e.test.js +115 -0
  57. package/slicejs-cli/tests/bundle-generator.test.js +691 -0
  58. package/slicejs-cli/tests/bundle-v2-register-output.test.js +470 -0
  59. package/slicejs-cli/tests/bundling-dependency-edges.test.js +127 -0
  60. package/slicejs-cli/tests/bundling-imports-unit.test.js +267 -0
  61. package/slicejs-cli/tests/client-launcher-contract.test.js +211 -0
  62. package/slicejs-cli/tests/commands-component-crud.test.js +128 -0
  63. package/slicejs-cli/tests/commands-doctor.test.js +152 -0
  64. package/slicejs-cli/tests/commands-version-checker.test.js +37 -0
  65. package/slicejs-cli/tests/component-registry-parse.test.js +34 -0
  66. package/slicejs-cli/tests/dependency-analyzer.test.js +24 -0
  67. package/slicejs-cli/tests/e2e/bundles.spec.js +91 -0
  68. package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +56 -0
  69. package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +136 -0
  70. package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +149 -0
  71. package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +45 -0
  72. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +106 -0
  73. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +5 -0
  74. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +158 -0
  75. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +33 -0
  76. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +56 -0
  77. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +83 -0
  78. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +164 -0
  79. package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +167 -0
  80. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +116 -0
  81. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +44 -0
  82. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +180 -0
  83. package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +20 -0
  84. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +181 -0
  85. package/slicejs-cli/tests/e2e/fixtures/components/registry.json +12 -0
  86. package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +65 -0
  87. package/slicejs-cli/tests/e2e/navigation.spec.js +44 -0
  88. package/slicejs-cli/tests/e2e/render.spec.js +34 -0
  89. package/slicejs-cli/tests/e2e/serve.mjs +264 -0
  90. package/slicejs-cli/tests/e2e/shared-deps.spec.js +61 -0
  91. package/slicejs-cli/tests/e2e/unminified.spec.js +33 -0
  92. package/slicejs-cli/tests/e2e-serve.test.js +148 -0
  93. package/slicejs-cli/tests/fixtures/components.js +8 -0
  94. package/slicejs-cli/tests/fixtures/sliceConfig.json +74 -0
  95. package/slicejs-cli/tests/getcomponent.test.js +435 -0
  96. package/slicejs-cli/tests/helpers/setup.js +102 -0
  97. package/slicejs-cli/tests/init-command-contract.test.js +101 -0
  98. package/slicejs-cli/tests/init-e2e.test.js +82 -0
  99. package/slicejs-cli/tests/init-project-isolation.test.js +98 -0
  100. package/slicejs-cli/tests/local-cli-delegation.test.js +81 -0
  101. package/slicejs-cli/tests/package-manager.test.js +127 -0
  102. package/slicejs-cli/tests/path-helper.test.js +206 -0
  103. package/slicejs-cli/tests/perf-budget.test.js +84 -0
  104. package/slicejs-cli/tests/postinstall-command.test.js +103 -0
  105. package/slicejs-cli/tests/property-mangling-regression.test.js +182 -0
  106. package/slicejs-cli/tests/types-breakage.test.js +491 -0
  107. package/slicejs-cli/tests/types-generator-errors.test.js +361 -0
  108. package/slicejs-cli/tests/types-generator.test.js +348 -0
@@ -0,0 +1,267 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { parse } from '@babel/parser';
4
+ import BundleGenerator from '../commands/utils/bundling/BundleGenerator.js';
5
+
6
+ // These tests exercise the cross-module import / variable handling of the
7
+ // bundler in isolation. They are written to assert *correct* behaviour, so a
8
+ // failure here documents a real gap/bug in the bundler rather than the test.
9
+
10
+ function makeGenerator(sliceConfig = {}) {
11
+ const gen = new BundleGenerator(import.meta.url, null, {});
12
+ gen.sliceConfig = sliceConfig;
13
+ return gen;
14
+ }
15
+
16
+ // Evaluate a generated JS snippet in a throwaway scope and return whatever the
17
+ // trailing `return` yields. Used to verify the *runtime* behaviour of the code
18
+ // the bundler emits (not just its shape).
19
+ function evalSnippet(code) {
20
+ // eslint-disable-next-line no-new-func
21
+ return new Function(code)();
22
+ }
23
+
24
+ describe('classifyImport', () => {
25
+ const gen = makeGenerator({ publicFolders: ['/assets', '/Themes'] });
26
+ const publicFolders = gen.getConfiguredPublicFolders();
27
+
28
+ test('relative imports are dropped silently (no warning)', () => {
29
+ const r = gen.classifyImport('./Foo.js', publicFolders);
30
+ assert.equal(r.keep, false);
31
+ assert.equal(r.warning, null);
32
+ });
33
+
34
+ test('parent-relative imports are dropped silently', () => {
35
+ const r = gen.classifyImport('../shared/util.js', publicFolders);
36
+ assert.equal(r.keep, false);
37
+ assert.equal(r.warning, null);
38
+ });
39
+
40
+ test('absolute imports inside publicFolders are kept', () => {
41
+ const r = gen.classifyImport('/assets/lib/x.js', publicFolders);
42
+ assert.equal(r.keep, true);
43
+ assert.equal(r.warning, null);
44
+ });
45
+
46
+ test('absolute import exactly matching a publicFolder is kept', () => {
47
+ const r = gen.classifyImport('/assets', publicFolders);
48
+ assert.equal(r.keep, true);
49
+ });
50
+
51
+ test('absolute imports outside publicFolders are dropped with warning', () => {
52
+ const r = gen.classifyImport('/secret/key.js', publicFolders);
53
+ assert.equal(r.keep, false);
54
+ assert.match(r.warning, /outside publicFolders/);
55
+ });
56
+
57
+ test('a publicFolder name must not match by prefix only (/assetsX)', () => {
58
+ const r = gen.classifyImport('/assetsX/x.js', publicFolders);
59
+ assert.equal(r.keep, false, '/assetsX should NOT be treated as inside /assets');
60
+ });
61
+
62
+ test('bare specifier imports are dropped with warning', () => {
63
+ const r = gen.classifyImport('lodash', publicFolders);
64
+ assert.equal(r.keep, false);
65
+ assert.match(r.warning, /bare import/);
66
+ });
67
+
68
+ test('non-string import path is handled defensively', () => {
69
+ const r = gen.classifyImport(undefined, publicFolders);
70
+ assert.equal(r.keep, false);
71
+ });
72
+ });
73
+
74
+ describe('stripImports', () => {
75
+ test('removes relative + bare imports, keeps body intact', () => {
76
+ const gen = makeGenerator({ publicFolders: [] });
77
+ const code = [
78
+ "import Foo from './Foo.js';",
79
+ "import { x } from 'pkg';",
80
+ 'const value = 42;',
81
+ 'export default value;',
82
+ ].join('\n');
83
+ const out = gen.stripImports(code, { collectHoistedImports: true });
84
+ assert.ok(!out.code.includes("from './Foo.js'"));
85
+ assert.ok(!out.code.includes("from 'pkg'"));
86
+ assert.ok(out.code.includes('const value = 42;'));
87
+ assert.deepEqual(out.hoistedImports, []);
88
+ });
89
+
90
+ test('hoists kept public-folder imports instead of inlining them', () => {
91
+ const gen = makeGenerator({ publicFolders: ['/assets'] });
92
+ const code = "import lib from '/assets/lib.js';\nconst a = lib;";
93
+ const out = gen.stripImports(code, { collectHoistedImports: true });
94
+ assert.equal(out.hoistedImports.length, 1);
95
+ assert.match(out.hoistedImports[0], /\/assets\/lib\.js/);
96
+ // The hoisted import must NOT remain inline in the body.
97
+ assert.ok(!out.code.includes("import lib from '/assets/lib.js'"));
98
+ });
99
+
100
+ test('code with no imports is returned unchanged', () => {
101
+ const gen = makeGenerator();
102
+ const code = 'const a = 1;\nconst b = 2;';
103
+ const out = gen.stripImports(code, { collectHoistedImports: true });
104
+ assert.equal(out.code, code);
105
+ });
106
+
107
+ test('falls back to regex scanner when Babel cannot parse', () => {
108
+ const gen = makeGenerator({ publicFolders: [] });
109
+ // `@decorator` + class field syntax with no plugin -> Babel throws -> fallback.
110
+ const code = "import x from './x.js';\nconst valid = 1;\nthis is not ::: valid js @@@";
111
+ // Should not throw; relative import must still be stripped.
112
+ const out = gen.stripImports(code, { collectHoistedImports: true });
113
+ assert.ok(!out.code.includes("from './x.js'"));
114
+ });
115
+ });
116
+
117
+ describe('extractLocalBindingsFromImportStatement', () => {
118
+ const gen = makeGenerator();
119
+
120
+ test('default import', () => {
121
+ assert.deepEqual(gen.extractLocalBindingsFromImportStatement("import Foo from 'x';"), ['Foo']);
122
+ });
123
+
124
+ test('named imports', () => {
125
+ assert.deepEqual(
126
+ gen.extractLocalBindingsFromImportStatement("import { a, b } from 'x';").sort(),
127
+ ['a', 'b']
128
+ );
129
+ });
130
+
131
+ test('aliased named import uses the local name', () => {
132
+ assert.deepEqual(gen.extractLocalBindingsFromImportStatement("import { a as b } from 'x';"), ['b']);
133
+ });
134
+
135
+ test('namespace import', () => {
136
+ assert.deepEqual(gen.extractLocalBindingsFromImportStatement("import * as NS from 'x';"), ['NS']);
137
+ });
138
+
139
+ test('mixed default + named', () => {
140
+ assert.deepEqual(
141
+ gen.extractLocalBindingsFromImportStatement("import Foo, { a, b as c } from 'x';").sort(),
142
+ ['Foo', 'a', 'c']
143
+ );
144
+ });
145
+
146
+ test('side-effect import has no bindings', () => {
147
+ assert.deepEqual(gen.extractLocalBindingsFromImportStatement("import 'x';"), []);
148
+ });
149
+ });
150
+
151
+ describe('validateHoistedImportCollisions', () => {
152
+ const gen = makeGenerator();
153
+
154
+ test('throws on the same binding from two different statements', () => {
155
+ assert.throws(
156
+ () => gen.validateHoistedImportCollisions([
157
+ "import Foo from '/assets/a.js';",
158
+ "import Foo from '/assets/b.js';",
159
+ ]),
160
+ /binding collision/
161
+ );
162
+ });
163
+
164
+ test('does not throw when the identical statement appears twice', () => {
165
+ assert.doesNotThrow(() => gen.validateHoistedImportCollisions([
166
+ "import Foo from '/assets/a.js';",
167
+ "import Foo from '/assets/a.js';",
168
+ ]));
169
+ });
170
+
171
+ test('throws when a hoisted binding collides with a reserved identifier', () => {
172
+ assert.throws(
173
+ () => gen.validateHoistedImportCollisions(
174
+ ["import SLICE_BUNDLE_META from '/assets/a.js';"],
175
+ new Set(['SLICE_BUNDLE_META'])
176
+ ),
177
+ /reserved identifier collision/
178
+ );
179
+ });
180
+ });
181
+
182
+ describe('transformDependencyContent', () => {
183
+ const gen = makeGenerator();
184
+
185
+ function transformAndEval(content, moduleName = 'shared/util.js') {
186
+ const transformed = gen.transformDependencyContent(content, '__d', moduleName);
187
+ return evalSnippet(`const __d = {};\n${transformed}\nreturn __d;`);
188
+ }
189
+
190
+ test('export const / let / var are attached to the exports object', () => {
191
+ const d = transformAndEval('export const A = 1;\nexport let B = 2;\nexport var C = 3;');
192
+ assert.equal(d.A, 1);
193
+ assert.equal(d.B, 2);
194
+ assert.equal(d.C, 3);
195
+ });
196
+
197
+ test('export function is attached and callable', () => {
198
+ const d = transformAndEval('export function fn() { return 7; }');
199
+ assert.equal(typeof d.fn, 'function');
200
+ assert.equal(d.fn(), 7);
201
+ });
202
+
203
+ test('export default maps to the <basename>Data fallback key', () => {
204
+ const d = transformAndEval('export default 99;', 'shared/util.js');
205
+ assert.equal(d.utilData, 99);
206
+ });
207
+
208
+ test('plain export { a, b } re-exports local bindings', () => {
209
+ const d = transformAndEval('const a = 10;\nconst b = 20;\nexport { a, b };');
210
+ assert.equal(d.a, 10);
211
+ assert.equal(d.b, 20);
212
+ });
213
+
214
+ test('aliased export { internal as Public } exposes the PUBLIC name', () => {
215
+ // A consumer writes `import { Public } from '...'`, so the exports object
216
+ // must carry `Public`, mapped to the value of `internal`.
217
+ const d = transformAndEval('const internal = 55;\nexport { internal as Public };');
218
+ assert.equal(d.Public, 55, 'exported alias name should be the public key');
219
+ });
220
+ });
221
+
222
+ describe('default export resolver (__sliceResolveDefaultExport)', () => {
223
+ const gen = makeGenerator();
224
+ function buildResolver() {
225
+ const lines = gen.getDefaultExportResolverLines().join('\n');
226
+ return evalSnippet(`${lines}\nreturn __sliceResolveDefaultExport;`);
227
+ }
228
+
229
+ test('returns .default when present', () => {
230
+ const resolve = buildResolver();
231
+ assert.equal(resolve({ default: 5, other: 9 }, 'm', null), 5);
232
+ });
233
+
234
+ test('returns the single non-default key when only one exists', () => {
235
+ const resolve = buildResolver();
236
+ assert.equal(resolve({ only: 42 }, 'm', null), 42);
237
+ });
238
+
239
+ test('honours the preferred key', () => {
240
+ const resolve = buildResolver();
241
+ assert.equal(resolve({ a: 1, utilData: 2 }, 'm', 'utilData'), 2);
242
+ });
243
+
244
+ test('primitive dependency is returned as-is', () => {
245
+ const resolve = buildResolver();
246
+ assert.equal(resolve(7, 'm', null), 7);
247
+ });
248
+ });
249
+
250
+ describe('toSafeIdentifier', () => {
251
+ const gen = makeGenerator();
252
+
253
+ test('produces a valid JS identifier', () => {
254
+ const id = gen.toSafeIdentifier('My-Component');
255
+ assert.doesNotThrow(() => parse(`const ${id} = 1;`));
256
+ });
257
+
258
+ test('distinct component names must not collide to the same identifier', () => {
259
+ // Two registered components differing only by a non-alphanumeric char would
260
+ // otherwise emit two `const <id>` declarations -> duplicate-decl syntax error.
261
+ assert.notEqual(
262
+ gen.toSafeIdentifier('my-btn'),
263
+ gen.toSafeIdentifier('my_btn'),
264
+ 'distinct names collapse to the same bundle identifier'
265
+ );
266
+ });
267
+ });
@@ -0,0 +1,211 @@
1
+ import { test } 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 { parse } from '@babel/parser';
7
+ import babelTraverse from '@babel/traverse';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const clientPath = path.join(__dirname, '..', 'client.js');
11
+ const source = fs.readFileSync(clientPath, 'utf-8');
12
+ const ast = parse(source, {
13
+ sourceType: 'module',
14
+ plugins: []
15
+ });
16
+ const traverse = babelTraverse.default || babelTraverse;
17
+
18
+ function walk(node, visit) {
19
+ if (!node || typeof node !== 'object') {
20
+ return;
21
+ }
22
+
23
+ visit(node);
24
+
25
+ for (const value of Object.values(node)) {
26
+ if (Array.isArray(value)) {
27
+ for (const item of value) {
28
+ walk(item, visit);
29
+ }
30
+ continue;
31
+ }
32
+
33
+ walk(value, visit);
34
+ }
35
+ }
36
+
37
+ function getImportedBindings(fromSource) {
38
+ const bindings = new Map();
39
+
40
+ for (const statement of ast.program.body) {
41
+ if (statement.type !== 'ImportDeclaration') {
42
+ continue;
43
+ }
44
+
45
+ if (statement.source.value !== fromSource) {
46
+ continue;
47
+ }
48
+
49
+ for (const specifier of statement.specifiers) {
50
+ if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier') {
51
+ bindings.set(specifier.imported.name, specifier.local.name);
52
+ }
53
+ }
54
+ }
55
+
56
+ return bindings;
57
+ }
58
+
59
+ function getBoundImportedCallPositions(fromSource, importedName) {
60
+ const positions = [];
61
+ const importedBindings = getImportedBindings(fromSource);
62
+ const localName = importedBindings.get(importedName);
63
+
64
+ if (!localName) {
65
+ return positions;
66
+ }
67
+
68
+ traverse(ast, {
69
+ CallExpression(callPath) {
70
+ if (callPath.node.callee.type !== 'Identifier') {
71
+ return;
72
+ }
73
+
74
+ if (callPath.node.callee.name !== localName) {
75
+ return;
76
+ }
77
+
78
+ const binding = callPath.scope.getBinding(localName);
79
+ if (!binding) {
80
+ return;
81
+ }
82
+
83
+ if (binding.path.node.type !== 'ImportSpecifier') {
84
+ return;
85
+ }
86
+
87
+ if (binding.path.parent.type !== 'ImportDeclaration') {
88
+ return;
89
+ }
90
+
91
+ if (binding.path.parent.source.value !== fromSource) {
92
+ return;
93
+ }
94
+
95
+ if (binding.path.node.imported.type !== 'Identifier') {
96
+ return;
97
+ }
98
+
99
+ if (binding.path.node.imported.name !== importedName) {
100
+ return;
101
+ }
102
+
103
+ positions.push(callPath.node.start);
104
+ }
105
+ });
106
+
107
+ return positions;
108
+ }
109
+
110
+ function getCommandRegistrationPositions() {
111
+ const positions = [];
112
+
113
+ walk(ast.program, (node) => {
114
+ if (node.type !== 'CallExpression') {
115
+ return;
116
+ }
117
+
118
+ if (
119
+ node.callee.type === 'MemberExpression' &&
120
+ !node.callee.computed &&
121
+ node.callee.property.type === 'Identifier' &&
122
+ node.callee.property.name === 'command'
123
+ ) {
124
+ positions.push(node.start);
125
+ }
126
+ });
127
+
128
+ return positions;
129
+ }
130
+
131
+ const launcherModulePath = './commands/utils/LocalCliDelegation.js';
132
+ const commandRegistrationPositions = getCommandRegistrationPositions();
133
+ const firstCommandRegistrationPos = Math.min(...commandRegistrationPositions);
134
+
135
+ test('client imports LocalCliDelegation utility', () => {
136
+ const importedBindings = getImportedBindings(launcherModulePath);
137
+
138
+ assert.ok(
139
+ importedBindings.size > 0,
140
+ 'Contract clause failed: client.js must import launcher helpers from ./commands/utils/LocalCliDelegation.js'
141
+ );
142
+
143
+ for (const requiredImport of [
144
+ 'isLocalDelegationDisabled',
145
+ 'findNearestLocalCliEntry',
146
+ 'shouldDelegateToLocalCli'
147
+ ]) {
148
+ assert.ok(
149
+ importedBindings.has(requiredImport),
150
+ `Contract clause failed: client.js must import ${requiredImport} from ${launcherModulePath}`
151
+ );
152
+ }
153
+ });
154
+
155
+ test('client checks SLICE_NO_LOCAL_DELEGATION behavior before command runtime', () => {
156
+ const isDisabledCalls = getBoundImportedCallPositions(
157
+ launcherModulePath,
158
+ 'isLocalDelegationDisabled'
159
+ );
160
+
161
+ assert.ok(
162
+ isDisabledCalls.length > 0,
163
+ 'Contract clause failed: client.js must call imported isLocalDelegationDisabled() in launcher path'
164
+ );
165
+
166
+ assert.ok(
167
+ commandRegistrationPositions.length > 0,
168
+ 'Contract clause failed: client.js must define at least one .command(...) registration before launcher ordering checks'
169
+ );
170
+
171
+ assert.ok(
172
+ isDisabledCalls.some((pos) => pos < firstCommandRegistrationPos),
173
+ 'Contract clause failed: isLocalDelegationDisabled() must be evaluated before command registration/runtime wiring'
174
+ );
175
+ });
176
+
177
+ test('client performs local candidate resolution and delegation decision', () => {
178
+ const findNearestCalls = getBoundImportedCallPositions(
179
+ launcherModulePath,
180
+ 'findNearestLocalCliEntry'
181
+ );
182
+ const shouldDelegateCalls = getBoundImportedCallPositions(
183
+ launcherModulePath,
184
+ 'shouldDelegateToLocalCli'
185
+ );
186
+
187
+ assert.ok(
188
+ findNearestCalls.length > 0,
189
+ 'Contract clause failed: client.js must call imported findNearestLocalCliEntry() to resolve local CLI candidate in launcher path'
190
+ );
191
+
192
+ assert.ok(
193
+ shouldDelegateCalls.length > 0,
194
+ 'Contract clause failed: client.js must call imported shouldDelegateToLocalCli() to gate delegation in launcher path'
195
+ );
196
+
197
+ assert.ok(
198
+ commandRegistrationPositions.length > 0,
199
+ 'Contract clause failed: client.js must define at least one .command(...) registration before launcher ordering checks'
200
+ );
201
+
202
+ assert.ok(
203
+ findNearestCalls.some((pos) => pos < firstCommandRegistrationPos),
204
+ 'Contract clause failed: findNearestLocalCliEntry() must execute before command registration/runtime wiring'
205
+ );
206
+
207
+ assert.ok(
208
+ shouldDelegateCalls.some((pos) => pos < firstCommandRegistrationPos),
209
+ 'Contract clause failed: shouldDelegateToLocalCli() must execute before command registration/runtime wiring'
210
+ );
211
+ });
@@ -0,0 +1,128 @@
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 { withTestProject } from './helpers/setup.js';
6
+ import createComponent from '../commands/createComponent/createComponent.js';
7
+ import deleteComponent from '../commands/deleteComponent/deleteComponent.js';
8
+ import listComponentsReal from '../commands/listComponents/listComponents.js';
9
+
10
+ const visualDir = (root, name) =>
11
+ path.join(root, 'src', 'Components', 'Visual', name);
12
+
13
+ describe('component create', () => {
14
+ test('creates a Visual component with .js/.css/.html', async () => {
15
+ await withTestProject(async (root) => {
16
+ const ok = createComponent('Card', 'Visual');
17
+ assert.equal(ok, true);
18
+ const dir = visualDir(root, 'Card');
19
+ assert.ok(await fs.pathExists(path.join(dir, 'Card.js')));
20
+ assert.ok(await fs.pathExists(path.join(dir, 'Card.css')));
21
+ assert.ok(await fs.pathExists(path.join(dir, 'Card.html')));
22
+ });
23
+ });
24
+
25
+ test('creates a Service component with only a .js file', async () => {
26
+ await withTestProject(async (root) => {
27
+ const ok = createComponent('Api', 'Service');
28
+ assert.equal(ok, true);
29
+ const dir = path.join(root, 'src', 'Components', 'Service', 'Api');
30
+ assert.ok(await fs.pathExists(path.join(dir, 'Api.js')));
31
+ assert.equal(await fs.pathExists(path.join(dir, 'Api.css')), false);
32
+ });
33
+ });
34
+
35
+ test('rejects an invalid component name', async () => {
36
+ await withTestProject(() => {
37
+ assert.equal(createComponent('1Bad', 'Visual'), false);
38
+ assert.equal(createComponent('bad-name', 'Visual'), false);
39
+ });
40
+ });
41
+
42
+ test('rejects an unknown category', async () => {
43
+ await withTestProject(() => {
44
+ assert.equal(createComponent('Card', 'Nope'), false);
45
+ });
46
+ });
47
+
48
+ test('rejects when the component files already exist', async () => {
49
+ await withTestProject(() => {
50
+ assert.equal(createComponent('Card', 'Visual'), true);
51
+ // Second creation hits the on-disk existence guard.
52
+ assert.equal(createComponent('Card', 'Visual'), false);
53
+ });
54
+ });
55
+ });
56
+
57
+ describe('component delete', () => {
58
+ test('deletes an existing component directory', async () => {
59
+ await withTestProject(async (root) => {
60
+ createComponent('Card', 'Visual');
61
+ assert.ok(await fs.pathExists(visualDir(root, 'Card')));
62
+
63
+ const ok = deleteComponent('Card', 'Visual');
64
+ assert.equal(ok, true);
65
+ assert.equal(await fs.pathExists(visualDir(root, 'Card')), false);
66
+ });
67
+ });
68
+
69
+ test('returns false when the component does not exist', async () => {
70
+ await withTestProject(() => {
71
+ assert.equal(deleteComponent('DoesNotExist', 'Visual'), false);
72
+ });
73
+ });
74
+
75
+ // Components are PascalCase by convention: both create and delete normalize
76
+ // the initial to uppercase, so a lower-case name round-trips correctly.
77
+ test('create/delete round-trips a lower-case initial (PascalCase normalization)', async () => {
78
+ await withTestProject(async (root) => {
79
+ assert.equal(createComponent('card', 'Visual'), true);
80
+ assert.ok(await fs.pathExists(visualDir(root, 'Card')), 'folder is created in PascalCase');
81
+ assert.equal(deleteComponent('card', 'Visual'), true);
82
+ assert.equal(await fs.pathExists(visualDir(root, 'Card')), false);
83
+ });
84
+ });
85
+ });
86
+
87
+ describe('component list (registry regeneration)', () => {
88
+ test('regenerates components.js from the files on disk', async () => {
89
+ await withTestProject(async (root) => {
90
+ createComponent('Card', 'Visual');
91
+ listComponentsReal();
92
+
93
+ const registryPath = path.join(root, 'src', 'Components', 'components.js');
94
+ const content = await fs.readFile(registryPath, 'utf8');
95
+ const json = JSON.parse(content.match(/const components = ({[\s\S]*?});/)[1]);
96
+
97
+ assert.equal(json.Card, 'Visual');
98
+ // Starter AppComponents must still be present.
99
+ assert.equal(json.AppShell, 'AppComponents');
100
+ });
101
+ });
102
+ });
103
+
104
+ describe('slice component delete via CLI (full command path)', () => {
105
+ test('deletes a component non-interactively without crashing', async () => {
106
+ // Regression: client.js used to call an undefined loadConfig() in this
107
+ // path (ReferenceError), which unit tests of deleteComponent never hit.
108
+ const { createTestProject, cleanupTestProject } = await import('./helpers/setup.js');
109
+ const { spawnSync } = await import('node:child_process');
110
+ const { fileURLToPath } = await import('node:url');
111
+ const dir = await createTestProject({ visualComponents: ['Button'] });
112
+ try {
113
+ const client = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'client.js');
114
+ const result = spawnSync(process.execPath, [client, 'component', 'delete', 'Button', '--category', 'Visual', '--yes'], {
115
+ cwd: dir,
116
+ encoding: 'utf-8',
117
+ env: { ...process.env, INIT_CWD: dir, SLICE_NO_LOCAL_DELEGATION: '1' },
118
+ timeout: 60_000
119
+ });
120
+ const output = `${result.stdout}\n${result.stderr}`;
121
+ assert.equal(result.status, 0, `command must exit 0. Output:\n${output}`);
122
+ assert.ok(!output.includes('ReferenceError'), `must not crash:\n${output}`);
123
+ assert.ok(!(await fs.pathExists(path.join(dir, 'src', 'Components', 'Visual', 'Button'))), 'Button folder must be deleted');
124
+ } finally {
125
+ await cleanupTestProject(dir);
126
+ }
127
+ });
128
+ });