slicejs-web-framework 3.4.4 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/Slice/Components/Structural/Controller/Controller.js +135 -0
  2. package/Slice/Components/Structural/EventManager/EventManager.js +289 -4
  3. package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +368 -30
  4. package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -0
  5. package/Slice/Slice.js +14 -1
  6. package/Slice/tests/event-emitters.test.js +79 -0
  7. package/Slice/tests/event-registry.test.js +141 -0
  8. package/Slice/tests/leak-orphans.test.js +131 -0
  9. package/api/index.js +35 -8
  10. package/package.json +1 -1
  11. package/sliceConfig.schema.json +10 -1
  12. package/src/sliceConfig.json +5 -1
  13. package/types/index.d.ts +12 -0
  14. package/slicejs-cli/.github/workflows/ci.yml +0 -43
  15. package/slicejs-cli/LICENSE +0 -21
  16. package/slicejs-cli/assets/Slice.js-logo.png +0 -0
  17. package/slicejs-cli/client.js +0 -740
  18. package/slicejs-cli/commands/Print.js +0 -164
  19. package/slicejs-cli/commands/Validations.js +0 -92
  20. package/slicejs-cli/commands/build/build.js +0 -40
  21. package/slicejs-cli/commands/buildProduction/buildProduction.js +0 -577
  22. package/slicejs-cli/commands/bundle/bundle.js +0 -236
  23. package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +0 -55
  24. package/slicejs-cli/commands/createComponent/createComponent.js +0 -128
  25. package/slicejs-cli/commands/deleteComponent/deleteComponent.js +0 -81
  26. package/slicejs-cli/commands/doctor/doctor.js +0 -516
  27. package/slicejs-cli/commands/getComponent/getComponent.js +0 -695
  28. package/slicejs-cli/commands/init/init.js +0 -473
  29. package/slicejs-cli/commands/listComponents/listComponents.js +0 -172
  30. package/slicejs-cli/commands/startServer/startServer.js +0 -240
  31. package/slicejs-cli/commands/startServer/watchServer.js +0 -66
  32. package/slicejs-cli/commands/types/types.js +0 -583
  33. package/slicejs-cli/commands/utils/LocalCliDelegation.js +0 -53
  34. package/slicejs-cli/commands/utils/PackageManager.js +0 -148
  35. package/slicejs-cli/commands/utils/PathHelper.js +0 -75
  36. package/slicejs-cli/commands/utils/VersionChecker.js +0 -169
  37. package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +0 -2525
  38. package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +0 -933
  39. package/slicejs-cli/commands/utils/loadConfig.js +0 -31
  40. package/slicejs-cli/commands/utils/sliceScripts.js +0 -48
  41. package/slicejs-cli/commands/utils/updateManager.js +0 -471
  42. package/slicejs-cli/package.json +0 -71
  43. package/slicejs-cli/playwright.config.js +0 -51
  44. package/slicejs-cli/pnpm-lock.yaml +0 -1629
  45. package/slicejs-cli/tests/build-command-integration.test.js +0 -87
  46. package/slicejs-cli/tests/build-mangling-e2e.test.js +0 -290
  47. package/slicejs-cli/tests/build-production-e2e.test.js +0 -140
  48. package/slicejs-cli/tests/builder-edge-cases.test.js +0 -322
  49. package/slicejs-cli/tests/bundle-generate-e2e.test.js +0 -115
  50. package/slicejs-cli/tests/bundle-generator.test.js +0 -691
  51. package/slicejs-cli/tests/bundle-v2-register-output.test.js +0 -470
  52. package/slicejs-cli/tests/bundling-dependency-edges.test.js +0 -127
  53. package/slicejs-cli/tests/bundling-imports-unit.test.js +0 -267
  54. package/slicejs-cli/tests/client-launcher-contract.test.js +0 -211
  55. package/slicejs-cli/tests/commands-component-crud.test.js +0 -128
  56. package/slicejs-cli/tests/commands-doctor.test.js +0 -152
  57. package/slicejs-cli/tests/commands-version-checker.test.js +0 -37
  58. package/slicejs-cli/tests/component-registry-parse.test.js +0 -34
  59. package/slicejs-cli/tests/dependency-analyzer.test.js +0 -24
  60. package/slicejs-cli/tests/e2e/bundles.spec.js +0 -91
  61. package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +0 -56
  62. package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
  63. package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
  64. package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
  65. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
  66. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
  67. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
  68. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
  69. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
  70. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
  71. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
  72. package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
  73. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
  74. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
  75. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
  76. package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
  77. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
  78. package/slicejs-cli/tests/e2e/fixtures/components/registry.json +0 -12
  79. package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +0 -65
  80. package/slicejs-cli/tests/e2e/navigation.spec.js +0 -44
  81. package/slicejs-cli/tests/e2e/render.spec.js +0 -34
  82. package/slicejs-cli/tests/e2e/serve.mjs +0 -264
  83. package/slicejs-cli/tests/e2e/shared-deps.spec.js +0 -61
  84. package/slicejs-cli/tests/e2e/unminified.spec.js +0 -33
  85. package/slicejs-cli/tests/e2e-serve.test.js +0 -148
  86. package/slicejs-cli/tests/fixtures/components.js +0 -8
  87. package/slicejs-cli/tests/fixtures/sliceConfig.json +0 -74
  88. package/slicejs-cli/tests/getcomponent.test.js +0 -435
  89. package/slicejs-cli/tests/helpers/setup.js +0 -102
  90. package/slicejs-cli/tests/init-command-contract.test.js +0 -101
  91. package/slicejs-cli/tests/init-e2e.test.js +0 -82
  92. package/slicejs-cli/tests/init-project-isolation.test.js +0 -98
  93. package/slicejs-cli/tests/local-cli-delegation.test.js +0 -81
  94. package/slicejs-cli/tests/package-manager.test.js +0 -127
  95. package/slicejs-cli/tests/path-helper.test.js +0 -206
  96. package/slicejs-cli/tests/perf-budget.test.js +0 -84
  97. package/slicejs-cli/tests/postinstall-command.test.js +0 -103
  98. package/slicejs-cli/tests/property-mangling-regression.test.js +0 -182
  99. package/slicejs-cli/tests/types-breakage.test.js +0 -491
  100. package/slicejs-cli/tests/types-generator-errors.test.js +0 -361
  101. package/slicejs-cli/tests/types-generator.test.js +0 -348
@@ -1,33 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- // Runs against a second server built with E2E_MINIFY=false, so the raw
4
- // (un-minified, un-obfuscated) bundle output is exercised in the browser.
5
- function trackErrors(page) {
6
- const errors = [];
7
- page.on('console', (m) => m.type() === 'error' && errors.push(m.text()));
8
- page.on('pageerror', (e) => errors.push(String(e)));
9
- return errors;
10
- }
11
-
12
- test.describe('unminified production build', () => {
13
- test('boots and renders the home page', async ({ page }) => {
14
- const errors = trackErrors(page);
15
- await page.goto('/');
16
-
17
- await expect(page.locator('slice-app-shell')).toBeAttached();
18
- await expect(page.locator('slice-home-section h1.home__title')).toHaveText(
19
- /Welcome to your Slice app/
20
- );
21
-
22
- expect(errors, `console errors:\n${errors.join('\n')}`).toEqual([]);
23
- });
24
-
25
- test('resolves the vendor-shared dependency with unminified bundles', async ({ page }) => {
26
- const errors = trackErrors(page);
27
- await page.goto('/services');
28
-
29
- await expect(page.locator('slice-servicespage')).toHaveAttribute('data-shared-tag', 'shared-kit-v1');
30
-
31
- expect(errors, `console errors:\n${errors.join('\n')}`).toEqual([]);
32
- });
33
- });
@@ -1,148 +0,0 @@
1
- import { test, describe } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import http from 'node:http';
4
- import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import fs from 'fs-extra';
7
- import { parse } from '@babel/parser';
8
- import { withTestProject } from './helpers/setup.js';
9
- import build from '../commands/build/build.js';
10
-
11
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
- const FRAMEWORK_SLICE_JS = path.resolve(
13
- __dirname,
14
- '../node_modules/slicejs-web-framework/Slice/Slice.js'
15
- );
16
-
17
- const CONTENT_TYPES = {
18
- '.js': 'application/javascript; charset=utf-8',
19
- '.json': 'application/json; charset=utf-8',
20
- '.html': 'text/html; charset=utf-8',
21
- '.css': 'text/css; charset=utf-8',
22
- };
23
-
24
- // Minimal static server that mirrors the production serving contract the
25
- // framework's api/index.js implements (index/SPA fallback, /slice-env.json,
26
- // /Slice/Slice.js from the framework package, and dist static files), without
27
- // pulling in express. Enough to assert that a production build is servable.
28
- function startServer(distDir) {
29
- const server = http.createServer(async (req, res) => {
30
- try {
31
- const pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
32
-
33
- if (pathname === '/slice-env.json') {
34
- res.setHeader('Content-Type', CONTENT_TYPES['.json']);
35
- res.end(JSON.stringify({ mode: 'production', env: {} }));
36
- return;
37
- }
38
- if (pathname === '/Slice/Slice.js') {
39
- const body = await fs.readFile(FRAMEWORK_SLICE_JS).catch(() => null);
40
- if (!body) { res.statusCode = 404; res.end('Slice.js not found'); return; }
41
- res.setHeader('Content-Type', CONTENT_TYPES['.js']);
42
- res.end(body);
43
- return;
44
- }
45
-
46
- const filePath = path.join(distDir, pathname);
47
- if (!filePath.startsWith(distDir)) { res.statusCode = 403; res.end('forbidden'); return; }
48
-
49
- const stat = await fs.stat(filePath).catch(() => null);
50
- if (stat && stat.isFile()) {
51
- res.setHeader('Content-Type', CONTENT_TYPES[path.extname(filePath)] || 'application/octet-stream');
52
- res.end(await fs.readFile(filePath));
53
- return;
54
- }
55
-
56
- // SPA fallback -> App/index.html
57
- const index = await fs.readFile(path.join(distDir, 'App', 'index.html')).catch(() => null);
58
- if (index) { res.setHeader('Content-Type', CONTENT_TYPES['.html']); res.end(index); return; }
59
- res.statusCode = 404;
60
- res.end('not found');
61
- } catch (error) {
62
- res.statusCode = 500;
63
- res.end(String(error));
64
- }
65
- });
66
-
67
- return new Promise((resolve) => {
68
- server.listen(0, '127.0.0.1', () => {
69
- resolve({ server, port: server.address().port });
70
- });
71
- });
72
- }
73
-
74
- function closeServer(server) {
75
- return new Promise((resolve) => server.close(resolve));
76
- }
77
-
78
- describe('end-to-end: production build is correctly servable', () => {
79
- test('build() output serves the app shell, framework runtime and valid bundles', async () => {
80
- await withTestProject(async (root) => {
81
- const ok = await build({ minify: false, obfuscate: false });
82
- assert.equal(ok, true, 'build should succeed');
83
-
84
- const distDir = path.join(root, 'dist');
85
- const { server, port } = await startServer(distDir);
86
- const base = `http://127.0.0.1:${port}`;
87
-
88
- try {
89
- // 1. The HTML shell is served and mounts the app + entry module.
90
- const indexRes = await fetch(`${base}/`);
91
- assert.equal(indexRes.status, 200);
92
- const indexHtml = await indexRes.text();
93
- assert.match(indexHtml, /id="app"/);
94
- assert.match(indexHtml, /\/App\/index\.js/);
95
-
96
- // 2. The entry module loads and bootstraps the framework runtime.
97
- const entryRes = await fetch(`${base}/App/index.js`);
98
- assert.equal(entryRes.status, 200);
99
- assert.match(entryRes.headers.get('content-type') || '', /javascript/);
100
- assert.match(await entryRes.text(), /\/Slice\/Slice\.js/);
101
-
102
- // 3. The framework runtime itself is reachable.
103
- const sliceRes = await fetch(`${base}/Slice/Slice.js`);
104
- assert.equal(sliceRes.status, 200);
105
-
106
- // 4. Config + runtime mode endpoints.
107
- const cfgRes = await fetch(`${base}/sliceConfig.json`);
108
- assert.equal(cfgRes.status, 200);
109
- const cfg = await cfgRes.json();
110
- assert.ok(cfg.paths?.components, 'sliceConfig exposes component paths');
111
-
112
- const envRes = await fetch(`${base}/slice-env.json`);
113
- assert.equal((await envRes.json()).mode, 'production');
114
-
115
- // 5. The bundle manifest is served and well-formed.
116
- const manifestRes = await fetch(`${base}/bundles/bundle.config.json`);
117
- assert.equal(manifestRes.status, 200);
118
- const manifest = await manifestRes.json();
119
- assert.equal(manifest.production, true);
120
- assert.equal(manifest.format, 'v2');
121
-
122
- // 6. Every emitted bundle is served as JS and is syntactically valid.
123
- const bundlesDir = path.join(distDir, 'bundles');
124
- const bundleFiles = (await fs.readdir(bundlesDir)).filter(
125
- (f) => f.startsWith('slice-bundle.') && f.endsWith('.js')
126
- );
127
- assert.ok(bundleFiles.length > 0, 'at least one bundle is produced');
128
- for (const file of bundleFiles) {
129
- const res = await fetch(`${base}/bundles/${file}`);
130
- assert.equal(res.status, 200, `${file} should be served`);
131
- assert.match(res.headers.get('content-type') || '', /javascript/);
132
- const code = await res.text();
133
- assert.doesNotThrow(
134
- () => parse(code, { sourceType: 'module', plugins: ['jsx'] }),
135
- `${file} is not valid JS`
136
- );
137
- }
138
-
139
- // 7. Unknown client routes fall back to the SPA shell.
140
- const spaRes = await fetch(`${base}/some/client/route`);
141
- assert.equal(spaRes.status, 200);
142
- assert.match(await spaRes.text(), /id="app"/);
143
- } finally {
144
- await closeServer(server);
145
- }
146
- });
147
- });
148
- });
@@ -1,8 +0,0 @@
1
- const components = {
2
- "Button": "Visual",
3
- "Link": "Visual",
4
- "Loading": "Visual",
5
- "Navbar": "Visual",
6
- "NotFound": "Visual",
7
- "FetchManager": "Service"
8
- }; export default components;
@@ -1,74 +0,0 @@
1
- {
2
- "server": {
3
- "port": 3001,
4
- "host": "localhost"
5
- },
6
- "debugger": {
7
- "enabled": false,
8
- "click": "right"
9
- },
10
- "events": {
11
- "enabled": true,
12
- "ui": {
13
- "enabled": true,
14
- "shortcut": "alt+shift+e"
15
- }
16
- },
17
- "context": {
18
- "enabled": true,
19
- "ui": {
20
- "enabled": true,
21
- "shortcut": "alt+shift+c"
22
- }
23
- },
24
- "stylesManager": {
25
- "requestedStyles": ["sliceStyles"]
26
- },
27
- "themeManager": {
28
- "enabled": true,
29
- "defaultTheme": "Slice",
30
- "saveThemeLocally": false,
31
- "useBrowserTheme": false
32
- },
33
- "logger": {
34
- "enabled": true,
35
- "showLogs": {
36
- "console": {
37
- "error": true,
38
- "warning": true,
39
- "info": false
40
- }
41
- }
42
- },
43
- "paths": {
44
- "components": {
45
- "AppComponents": {
46
- "path": "/Components/AppComponents",
47
- "type": "Visual"
48
- },
49
- "Visual": {
50
- "path": "/Components/Visual",
51
- "type": "Visual"
52
- },
53
- "Service": {
54
- "path": "/Components/Service",
55
- "type": "Service"
56
- }
57
- },
58
- "themes": "/Themes",
59
- "styles": "/Styles",
60
- "routesFile": "/routes.js"
61
- },
62
- "router": {
63
- "defaultRoute": "/"
64
- },
65
- "loading": {
66
- "enabled": true
67
- },
68
- "publicFolders": [
69
- "/Themes",
70
- "/Styles",
71
- "/assets",
72
- "/images"
73
- ]
74
- }
@@ -1,435 +0,0 @@
1
- import { test, describe, before, after, mock } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import fs from 'fs-extra';
4
- import path from 'path';
5
- import { createTestProject, cleanupTestProject, withTestProject } from './helpers/setup.js';
6
-
7
- describe('runConcurrent', () => {
8
- let runConcurrent;
9
-
10
- before(async () => {
11
- ({ runConcurrent } = await import('../commands/getComponent/getComponent.js'));
12
- });
13
-
14
- test('runs all items', async () => {
15
- const results = [];
16
- const worker = async (item) => { results.push(item); };
17
- await runConcurrent([1, 2, 3], worker, 5);
18
- assert.deepEqual(results.sort(), [1, 2, 3]);
19
- });
20
-
21
- test('respects concurrency limit', async () => {
22
- let concurrent = 0;
23
- let maxConcurrent = 0;
24
- const worker = async (item) => {
25
- concurrent++;
26
- maxConcurrent = Math.max(maxConcurrent, concurrent);
27
- await new Promise(r => setTimeout(r, 10));
28
- concurrent--;
29
- };
30
- await runConcurrent([1, 2, 3, 4, 5], worker, 2);
31
- assert.equal(maxConcurrent <= 2, true);
32
- });
33
-
34
- test('handles empty array', async () => {
35
- const worker = async () => {};
36
- await runConcurrent([], worker, 3);
37
- assert.ok(true);
38
- });
39
-
40
- test('propagates worker errors', async () => {
41
- const worker = async () => { throw new Error('worker fail'); };
42
- await assert.rejects(() => runConcurrent([1], worker, 3), /worker fail/);
43
- });
44
-
45
- test('default concurrency is 3', async () => {
46
- let maxConcurrent = 0;
47
- let running = 0;
48
- const worker = async () => {
49
- running++;
50
- maxConcurrent = Math.max(maxConcurrent, running);
51
- await new Promise(r => setTimeout(r, 10));
52
- running--;
53
- };
54
- await runConcurrent([1, 2, 3, 4, 5], worker);
55
- assert.equal(maxConcurrent <= 3, true);
56
- });
57
- });
58
-
59
- describe('loadConfig', () => {
60
- test('returns config object when sliceConfig.json exists', async () => {
61
- await withTestProject(async (tmpDir) => {
62
- const { loadConfig } = await import('../commands/getComponent/getComponent.js');
63
- const config = await loadConfig();
64
- assert.notEqual(config, null);
65
- assert.equal(typeof config, 'object');
66
- assert.equal(config.server?.port, 3001);
67
- });
68
- });
69
-
70
- test('returns null when sliceConfig.json is missing', async () => {
71
- const tmpDir = await createTestProject();
72
- try {
73
- process.env.INIT_CWD = tmpDir;
74
- const fixtureConfig = new URL('../tests/fixtures/sliceConfig.json', import.meta.url);
75
- const fs = await import('node:fs/promises');
76
- const path = await import('node:path');
77
- const destPath = path.join(tmpDir, 'src', 'sliceConfig.json');
78
- try { await fs.unlink(destPath); } catch {}
79
- const { loadConfig } = await import('../commands/getComponent/getComponent.js');
80
- const config = await loadConfig();
81
- assert.equal(config, null);
82
- } finally {
83
- delete process.env.INIT_CWD;
84
- await cleanupTestProject(tmpDir);
85
- }
86
- });
87
- });
88
-
89
- describe('ComponentRegistry.getAvailableComponents', () => {
90
- let ComponentRegistry;
91
-
92
- before(async () => {
93
- ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
94
- });
95
-
96
- const makeRegistry = (entries) => {
97
- const registry = new ComponentRegistry();
98
- registry.componentsRegistry = { ...entries };
99
- return registry;
100
- };
101
-
102
- test('returns empty object when registry not loaded', () => {
103
- const registry = new ComponentRegistry();
104
- const result = registry.getAvailableComponents();
105
- assert.deepEqual(result, {});
106
- });
107
-
108
- test('routing components (Route, MultiRoute, Link) return only .js', () => {
109
- const registry = makeRegistry({
110
- Route: 'Visual',
111
- MultiRoute: 'Visual',
112
- Link: 'Visual'
113
- });
114
- const result = registry.getAvailableComponents('Visual');
115
- assert.deepEqual(result.Route.files, ['Route.js']);
116
- assert.deepEqual(result.MultiRoute.files, ['MultiRoute.js']);
117
- assert.deepEqual(result.Link.files, ['Link.js']);
118
- });
119
-
120
- test('NotFound returns .js, .html, .css like other visual components', () => {
121
- const registry = makeRegistry({
122
- NotFound: 'Visual',
123
- Button: 'Visual',
124
- Navbar: 'Visual'
125
- });
126
- const result = registry.getAvailableComponents('Visual');
127
- assert.deepEqual(result.NotFound.files, ['NotFound.js', 'NotFound.html', 'NotFound.css']);
128
- assert.deepEqual(result.Button.files, ['Button.js', 'Button.html', 'Button.css']);
129
- assert.deepEqual(result.Navbar.files, ['Navbar.js', 'Navbar.html', 'Navbar.css']);
130
- });
131
-
132
- test('Service components return only .js', () => {
133
- const registry = makeRegistry({
134
- FetchManager: 'Service'
135
- });
136
- const result = registry.getAvailableComponents('Service');
137
- assert.deepEqual(result.FetchManager.files, ['FetchManager.js']);
138
- });
139
-
140
- test('filters by category - Visual', () => {
141
- const registry = makeRegistry({
142
- Button: 'Visual',
143
- FetchManager: 'Service'
144
- });
145
- const result = registry.getAvailableComponents('Visual');
146
- assert.ok(result.Button);
147
- assert.ok(!result.FetchManager);
148
- });
149
-
150
- test('filters by category - Service', () => {
151
- const registry = makeRegistry({
152
- Button: 'Visual',
153
- FetchManager: 'Service'
154
- });
155
- const result = registry.getAvailableComponents('Service');
156
- assert.ok(!result.Button);
157
- assert.ok(result.FetchManager);
158
- });
159
-
160
- test('returns all categories when no filter', () => {
161
- const registry = makeRegistry({
162
- Button: 'Visual',
163
- FetchManager: 'Service'
164
- });
165
- const result = registry.getAvailableComponents();
166
- assert.ok(result.Button);
167
- assert.ok(result.FetchManager);
168
- });
169
- });
170
-
171
- describe('fetchWithRetry', () => {
172
- let fetchWithRetry;
173
- let originalFetch;
174
-
175
- before(async () => {
176
- ({ fetchWithRetry } = await import('../commands/getComponent/getComponent.js'));
177
- });
178
-
179
- after(() => {
180
- if (originalFetch) globalThis.fetch = originalFetch;
181
- });
182
-
183
- test('succeeds on first attempt', async () => {
184
- originalFetch = globalThis.fetch;
185
- globalThis.fetch = mock.fn(async () => ({
186
- ok: true,
187
- text: async () => 'response body'
188
- }));
189
- const result = await fetchWithRetry('https://example.com');
190
- assert.equal(result, 'response body');
191
- assert.equal(globalThis.fetch.mock.calls.length, 1);
192
- globalThis.fetch = originalFetch;
193
- });
194
-
195
- test('retries and eventually succeeds', async () => {
196
- let attempts = 0;
197
- originalFetch = globalThis.fetch;
198
- globalThis.fetch = mock.fn(async () => {
199
- attempts++;
200
- if (attempts < 3) throw new Error('network error');
201
- return { ok: true, text: async () => 'success after retry' };
202
- });
203
- const result = await fetchWithRetry('https://example.com', 3, 5);
204
- assert.equal(result, 'success after retry');
205
- assert.equal(attempts, 3);
206
- globalThis.fetch = originalFetch;
207
- });
208
-
209
- test('fails after exhausting retries', async () => {
210
- originalFetch = globalThis.fetch;
211
- globalThis.fetch = mock.fn(async () => { throw new Error('persistent error'); });
212
- await assert.rejects(() => fetchWithRetry('https://example.com', 2, 5), /persistent error/);
213
- globalThis.fetch = originalFetch;
214
- });
215
-
216
- test('rejects non-ok response', async () => {
217
- originalFetch = globalThis.fetch;
218
- globalThis.fetch = mock.fn(async () => ({
219
- ok: false,
220
- status: 404,
221
- statusText: 'Not Found'
222
- }));
223
- await assert.rejects(() => fetchWithRetry('https://example.com', 2, 5), /HTTP 404/);
224
- globalThis.fetch = originalFetch;
225
- });
226
- });
227
-
228
- describe('filterOfficialComponents', () => {
229
- let ComponentRegistry;
230
-
231
- before(async () => {
232
- ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
233
- });
234
-
235
- test('includes Visual and Service components', () => {
236
- const registry = new ComponentRegistry();
237
- const result = registry.filterOfficialComponents({
238
- Button: 'Visual',
239
- FetchManager: 'Service'
240
- });
241
- assert.deepEqual(result, { Button: 'Visual', FetchManager: 'Service' });
242
- });
243
-
244
- test('excludes AppComponents and other categories', () => {
245
- const registry = new ComponentRegistry();
246
- const result = registry.filterOfficialComponents({
247
- Button: 'Visual',
248
- AppShell: 'AppComponents',
249
- SomeOther: 'Unknown'
250
- });
251
- assert.deepEqual(result, { Button: 'Visual' });
252
- });
253
-
254
- test('returns empty object for empty input', () => {
255
- const registry = new ComponentRegistry();
256
- const result = registry.filterOfficialComponents({});
257
- assert.deepEqual(result, {});
258
- });
259
-
260
- test('returns empty when no Visual or Service components', () => {
261
- const registry = new ComponentRegistry();
262
- const result = registry.filterOfficialComponents({
263
- AppShell: 'AppComponents',
264
- Custom: 'Other'
265
- });
266
- assert.deepEqual(result, {});
267
- });
268
- });
269
-
270
- describe('findComponentInRegistry', () => {
271
- let ComponentRegistry;
272
-
273
- before(async () => {
274
- ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
275
- });
276
-
277
- const makeRegistry = (entries) => {
278
- const registry = new ComponentRegistry();
279
- registry.componentsRegistry = { ...entries };
280
- return registry;
281
- };
282
-
283
- test('finds component by name (case-insensitive)', () => {
284
- const registry = makeRegistry({ Button: 'Visual' });
285
- const result = registry.findComponentInRegistry('button');
286
- assert.deepEqual(result, { name: 'Button', category: 'Visual' });
287
- });
288
-
289
- test('returns null for non-existent component', () => {
290
- const registry = makeRegistry({ Button: 'Visual' });
291
- const result = registry.findComponentInRegistry('NonExistent');
292
- assert.equal(result, null);
293
- });
294
-
295
- test('returns null when registry not loaded', () => {
296
- const registry = new ComponentRegistry();
297
- const result = registry.findComponentInRegistry('Button');
298
- assert.equal(result, null);
299
- });
300
-
301
- test('finds component with exact case', () => {
302
- const registry = makeRegistry({ FetchManager: 'Service' });
303
- const result = registry.findComponentInRegistry('FetchManager');
304
- assert.deepEqual(result, { name: 'FetchManager', category: 'Service' });
305
- });
306
- });
307
-
308
- describe('getLocalComponents', () => {
309
- let ComponentRegistry;
310
-
311
- before(async () => {
312
- ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
313
- });
314
-
315
- test('returns components from local components.js', async () => {
316
- await withTestProject(async (tmpDir) => {
317
- const registry = new ComponentRegistry();
318
- const result = await registry.getLocalComponents();
319
- assert.deepEqual(result, { Button: 'Visual' });
320
- }, { visualComponents: ['Button'] });
321
- });
322
-
323
- test('returns empty object when no components.js exists', async () => {
324
- const tmpDir = await createTestProject();
325
- try {
326
- process.env.INIT_CWD = tmpDir;
327
- const componentsPath = path.join(tmpDir, 'src', 'Components', 'components.js');
328
- await fs.remove(componentsPath);
329
- const registry = new ComponentRegistry();
330
- const result = await registry.getLocalComponents();
331
- assert.deepEqual(result, {});
332
- } finally {
333
- delete process.env.INIT_CWD;
334
- await cleanupTestProject(tmpDir);
335
- }
336
- });
337
-
338
- test('returns empty object when components.js has invalid format', async () => {
339
- const tmpDir = await createTestProject();
340
- try {
341
- process.env.INIT_CWD = tmpDir;
342
- const componentsPath = path.join(tmpDir, 'src', 'Components', 'components.js');
343
- await fs.writeFile(componentsPath, 'not valid javascript', 'utf8');
344
- const registry = new ComponentRegistry();
345
- const result = await registry.getLocalComponents();
346
- assert.deepEqual(result, {});
347
- } finally {
348
- delete process.env.INIT_CWD;
349
- await cleanupTestProject(tmpDir);
350
- }
351
- });
352
- });
353
-
354
- describe('updateLocalRegistrySafe', () => {
355
- let ComponentRegistry;
356
-
357
- before(async () => {
358
- ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
359
- });
360
-
361
- test('registers a new component in components.js', async () => {
362
- await withTestProject(async (tmpDir) => {
363
- const registry = new ComponentRegistry();
364
- await registry.updateLocalRegistrySafe('NewComp', 'Visual');
365
- const components = await registry.getLocalComponents();
366
- assert.equal(components.NewComp, 'Visual');
367
- }, { visualComponents: [] });
368
- });
369
-
370
- test('does not duplicate existing component', async () => {
371
- await withTestProject(async (tmpDir) => {
372
- const registry = new ComponentRegistry();
373
- await registry.updateLocalRegistrySafe('Button', 'Visual');
374
- const components = await registry.getLocalComponents();
375
- assert.equal(components.Button, 'Visual');
376
- assert.deepEqual(Object.keys(components).filter(k => k === 'Button'), ['Button']);
377
- }, { visualComponents: ['Button'] });
378
- });
379
- });
380
-
381
- describe('findUpdatableComponents', () => {
382
- let ComponentRegistry;
383
-
384
- before(async () => {
385
- ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
386
- });
387
-
388
- test('returns empty when no local components match remote registry', async () => {
389
- await withTestProject(async (tmpDir) => {
390
- const registry = new ComponentRegistry();
391
- registry.componentsRegistry = { SomeRemote: 'Visual' };
392
- const result = await registry.findUpdatableComponents();
393
- assert.deepEqual(result, []);
394
- }, { visualComponents: ['Button'] });
395
- });
396
-
397
- test('returns components that exist both locally and remotely', async () => {
398
- await withTestProject(async (tmpDir) => {
399
- const registry = new ComponentRegistry();
400
- registry.componentsRegistry = { Button: 'Visual', FetchManager: 'Service' };
401
- const result = await registry.findUpdatableComponents();
402
- assert.equal(result.length, 1);
403
- assert.equal(result[0].name, 'Button');
404
- assert.equal(result[0].category, 'Visual');
405
- }, { visualComponents: ['Button'] });
406
- });
407
- });
408
-
409
- describe('updateLocalRegistrySafe concurrency', () => {
410
- let ComponentRegistry;
411
-
412
- before(async () => {
413
- ({ ComponentRegistry } = await import('../commands/getComponent/getComponent.js'));
414
- });
415
-
416
- test('concurrent registrations do not corrupt or drop entries in components.js', async () => {
417
- await withTestProject(async (tmpDir) => {
418
- const registry = new ComponentRegistry();
419
- const names = ['Button', 'Link', 'Loading', 'MultiRoute', 'Navbar', 'NotFound', 'Route'];
420
-
421
- // Fire all read-modify-write cycles at once — without the registry lock
422
- // this loses updates (last-writer-wins) or reads a half-written file.
423
- await Promise.all(names.map(n => registry.updateLocalRegistrySafe(n, 'Visual')));
424
-
425
- const componentsPath = path.join(tmpDir, 'src', 'Components', 'components.js');
426
- const content = await fs.readFile(componentsPath, 'utf8');
427
- const match = content.match(/const components = ({[\s\S]*?});/);
428
- assert.ok(match, 'components.js must stay parseable');
429
- const parsed = JSON.parse(match[1]);
430
- for (const n of names) {
431
- assert.equal(parsed[n], 'Visual', `${n} must be registered`);
432
- }
433
- });
434
- });
435
- });