slicejs-web-framework 3.4.4 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) 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/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +56 -0
  6. package/Slice/Slice.js +14 -1
  7. package/Slice/tests/event-emitters.test.js +79 -0
  8. package/Slice/tests/event-registry.test.js +141 -0
  9. package/Slice/tests/leak-orphans.test.js +131 -0
  10. package/api/{middleware → framework}/securityMiddleware.js +18 -74
  11. package/api/framework/server.js +333 -0
  12. package/api/index.js +5 -279
  13. package/api/tests/public-env-resolver.test.js +1 -1
  14. package/package.json +4 -2
  15. package/sliceConfig.schema.json +10 -1
  16. package/src/App/index.html +31 -1
  17. package/src/manifest.json +15 -0
  18. package/src/robots.txt +4 -0
  19. package/src/sitemap.xml +8 -0
  20. package/src/sliceConfig.json +5 -1
  21. package/types/index.d.ts +12 -0
  22. package/slicejs-cli/.github/workflows/ci.yml +0 -43
  23. package/slicejs-cli/LICENSE +0 -21
  24. package/slicejs-cli/assets/Slice.js-logo.png +0 -0
  25. package/slicejs-cli/client.js +0 -740
  26. package/slicejs-cli/commands/Print.js +0 -164
  27. package/slicejs-cli/commands/Validations.js +0 -92
  28. package/slicejs-cli/commands/build/build.js +0 -40
  29. package/slicejs-cli/commands/buildProduction/buildProduction.js +0 -577
  30. package/slicejs-cli/commands/bundle/bundle.js +0 -236
  31. package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +0 -55
  32. package/slicejs-cli/commands/createComponent/createComponent.js +0 -128
  33. package/slicejs-cli/commands/deleteComponent/deleteComponent.js +0 -81
  34. package/slicejs-cli/commands/doctor/doctor.js +0 -516
  35. package/slicejs-cli/commands/getComponent/getComponent.js +0 -695
  36. package/slicejs-cli/commands/init/init.js +0 -473
  37. package/slicejs-cli/commands/listComponents/listComponents.js +0 -172
  38. package/slicejs-cli/commands/startServer/startServer.js +0 -240
  39. package/slicejs-cli/commands/startServer/watchServer.js +0 -66
  40. package/slicejs-cli/commands/types/types.js +0 -583
  41. package/slicejs-cli/commands/utils/LocalCliDelegation.js +0 -53
  42. package/slicejs-cli/commands/utils/PackageManager.js +0 -148
  43. package/slicejs-cli/commands/utils/PathHelper.js +0 -75
  44. package/slicejs-cli/commands/utils/VersionChecker.js +0 -169
  45. package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +0 -2525
  46. package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +0 -933
  47. package/slicejs-cli/commands/utils/loadConfig.js +0 -31
  48. package/slicejs-cli/commands/utils/sliceScripts.js +0 -48
  49. package/slicejs-cli/commands/utils/updateManager.js +0 -471
  50. package/slicejs-cli/package.json +0 -71
  51. package/slicejs-cli/playwright.config.js +0 -51
  52. package/slicejs-cli/pnpm-lock.yaml +0 -1629
  53. package/slicejs-cli/tests/build-command-integration.test.js +0 -87
  54. package/slicejs-cli/tests/build-mangling-e2e.test.js +0 -290
  55. package/slicejs-cli/tests/build-production-e2e.test.js +0 -140
  56. package/slicejs-cli/tests/builder-edge-cases.test.js +0 -322
  57. package/slicejs-cli/tests/bundle-generate-e2e.test.js +0 -115
  58. package/slicejs-cli/tests/bundle-generator.test.js +0 -691
  59. package/slicejs-cli/tests/bundle-v2-register-output.test.js +0 -470
  60. package/slicejs-cli/tests/bundling-dependency-edges.test.js +0 -127
  61. package/slicejs-cli/tests/bundling-imports-unit.test.js +0 -267
  62. package/slicejs-cli/tests/client-launcher-contract.test.js +0 -211
  63. package/slicejs-cli/tests/commands-component-crud.test.js +0 -128
  64. package/slicejs-cli/tests/commands-doctor.test.js +0 -152
  65. package/slicejs-cli/tests/commands-version-checker.test.js +0 -37
  66. package/slicejs-cli/tests/component-registry-parse.test.js +0 -34
  67. package/slicejs-cli/tests/dependency-analyzer.test.js +0 -24
  68. package/slicejs-cli/tests/e2e/bundles.spec.js +0 -91
  69. package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +0 -56
  70. package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
  71. package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
  72. package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
  73. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
  74. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
  75. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
  76. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
  77. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
  78. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
  79. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
  80. package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
  81. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
  82. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
  83. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
  84. package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
  85. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
  86. package/slicejs-cli/tests/e2e/fixtures/components/registry.json +0 -12
  87. package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +0 -65
  88. package/slicejs-cli/tests/e2e/navigation.spec.js +0 -44
  89. package/slicejs-cli/tests/e2e/render.spec.js +0 -34
  90. package/slicejs-cli/tests/e2e/serve.mjs +0 -264
  91. package/slicejs-cli/tests/e2e/shared-deps.spec.js +0 -61
  92. package/slicejs-cli/tests/e2e/unminified.spec.js +0 -33
  93. package/slicejs-cli/tests/e2e-serve.test.js +0 -148
  94. package/slicejs-cli/tests/fixtures/components.js +0 -8
  95. package/slicejs-cli/tests/fixtures/sliceConfig.json +0 -74
  96. package/slicejs-cli/tests/getcomponent.test.js +0 -435
  97. package/slicejs-cli/tests/helpers/setup.js +0 -102
  98. package/slicejs-cli/tests/init-command-contract.test.js +0 -101
  99. package/slicejs-cli/tests/init-e2e.test.js +0 -82
  100. package/slicejs-cli/tests/init-project-isolation.test.js +0 -98
  101. package/slicejs-cli/tests/local-cli-delegation.test.js +0 -81
  102. package/slicejs-cli/tests/package-manager.test.js +0 -127
  103. package/slicejs-cli/tests/path-helper.test.js +0 -206
  104. package/slicejs-cli/tests/perf-budget.test.js +0 -84
  105. package/slicejs-cli/tests/postinstall-command.test.js +0 -103
  106. package/slicejs-cli/tests/property-mangling-regression.test.js +0 -182
  107. package/slicejs-cli/tests/types-breakage.test.js +0 -491
  108. package/slicejs-cli/tests/types-generator-errors.test.js +0 -361
  109. package/slicejs-cli/tests/types-generator.test.js +0 -348
  110. /package/api/{utils → framework}/publicEnvResolver.js +0 -0
@@ -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
- });