create-nativecore 0.1.0 → 0.2.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 (175) hide show
  1. package/README.md +10 -18
  2. package/bin/index.mjs +407 -489
  3. package/package.json +4 -3
  4. package/template/.env.example +28 -0
  5. package/template/.htmlhintrc +14 -0
  6. package/template/api/data/dashboard.json +11 -0
  7. package/template/api/data/users.json +18 -0
  8. package/template/api/mockApi.js +161 -0
  9. package/template/assets/icon.svg +13 -0
  10. package/template/assets/logo.svg +25 -0
  11. package/template/eslint.config.js +94 -0
  12. package/template/index.html +137 -0
  13. package/template/manifest.json +19 -0
  14. package/template/public/.well-known/security.txt +9 -0
  15. package/template/public/_headers +24 -0
  16. package/template/public/_redirects +14 -0
  17. package/template/public/assets/icon.svg +13 -0
  18. package/template/public/assets/logo.svg +25 -0
  19. package/template/public/manifest.json +19 -0
  20. package/template/public/robots.txt +13 -0
  21. package/template/public/sitemap.xml +27 -0
  22. package/template/scripts/build-for-bots.mjs +121 -0
  23. package/template/scripts/convert-to-ts.mjs +106 -0
  24. package/template/scripts/fix-encoding.mjs +38 -0
  25. package/template/scripts/fix-svg-paths.mjs +32 -0
  26. package/template/scripts/generate-cf-router.mjs +52 -0
  27. package/template/scripts/inject-dev-tools.mjs +41 -0
  28. package/template/scripts/inject-version.mjs +65 -0
  29. package/template/scripts/make-component.mjs +445 -0
  30. package/template/scripts/make-component.mjs.backup +432 -0
  31. package/template/scripts/make-controller.mjs +119 -0
  32. package/template/scripts/make-core-component.mjs +303 -0
  33. package/template/scripts/make-view.mjs +346 -0
  34. package/template/scripts/minify.mjs +71 -0
  35. package/template/scripts/prepare-static-assets.mjs +141 -0
  36. package/template/scripts/prompt-bot-build.mjs +223 -0
  37. package/template/scripts/remove-component.mjs +170 -0
  38. package/template/scripts/remove-core-component.mjs +156 -0
  39. package/template/scripts/remove-dev.mjs +13 -0
  40. package/template/scripts/remove-view.mjs +200 -0
  41. package/template/scripts/strip-dev-blocks.mjs +30 -0
  42. package/template/scripts/watch-compile.mjs +69 -0
  43. package/template/server.js +1066 -0
  44. package/template/src/app.ts +115 -0
  45. package/template/src/components/appRegistry.ts +8 -0
  46. package/template/src/components/core/app-footer.ts +27 -0
  47. package/template/src/components/core/app-header.ts +175 -0
  48. package/template/src/components/core/app-sidebar.ts +238 -0
  49. package/template/src/components/core/loading-spinner.ts +25 -0
  50. package/template/src/components/core/nc-a.ts +313 -0
  51. package/template/src/components/core/nc-accordion.ts +186 -0
  52. package/template/src/components/core/nc-alert.ts +153 -0
  53. package/template/src/components/core/nc-animation.ts +1150 -0
  54. package/template/src/components/core/nc-autocomplete.ts +271 -0
  55. package/template/src/components/core/nc-avatar-group.ts +113 -0
  56. package/template/src/components/core/nc-avatar.ts +148 -0
  57. package/template/src/components/core/nc-badge.ts +86 -0
  58. package/template/src/components/core/nc-bottom-nav.ts +214 -0
  59. package/template/src/components/core/nc-breadcrumb.ts +96 -0
  60. package/template/src/components/core/nc-button.ts +307 -0
  61. package/template/src/components/core/nc-card.ts +160 -0
  62. package/template/src/components/core/nc-checkbox.ts +282 -0
  63. package/template/src/components/core/nc-chip.ts +115 -0
  64. package/template/src/components/core/nc-code.ts +314 -0
  65. package/template/src/components/core/nc-collapsible.ts +154 -0
  66. package/template/src/components/core/nc-color-picker.ts +268 -0
  67. package/template/src/components/core/nc-copy-button.ts +119 -0
  68. package/template/src/components/core/nc-date-picker.ts +443 -0
  69. package/template/src/components/core/nc-div.ts +280 -0
  70. package/template/src/components/core/nc-divider.ts +81 -0
  71. package/template/src/components/core/nc-drawer.ts +230 -0
  72. package/template/src/components/core/nc-dropdown.ts +178 -0
  73. package/template/src/components/core/nc-empty-state.ts +134 -0
  74. package/template/src/components/core/nc-file-upload.ts +354 -0
  75. package/template/src/components/core/nc-form.ts +312 -0
  76. package/template/src/components/core/nc-image.ts +184 -0
  77. package/template/src/components/core/nc-input.ts +383 -0
  78. package/template/src/components/core/nc-kbd.ts +48 -0
  79. package/template/src/components/core/nc-menu-item.ts +193 -0
  80. package/template/src/components/core/nc-menu.ts +376 -0
  81. package/template/src/components/core/nc-modal.ts +238 -0
  82. package/template/src/components/core/nc-nav-item.ts +151 -0
  83. package/template/src/components/core/nc-number-input.ts +350 -0
  84. package/template/src/components/core/nc-otp-input.ts +235 -0
  85. package/template/src/components/core/nc-pagination.ts +178 -0
  86. package/template/src/components/core/nc-popover.ts +260 -0
  87. package/template/src/components/core/nc-progress-circular.ts +119 -0
  88. package/template/src/components/core/nc-progress.ts +134 -0
  89. package/template/src/components/core/nc-radio.ts +235 -0
  90. package/template/src/components/core/nc-rating.ts +266 -0
  91. package/template/src/components/core/nc-rich-text.ts +283 -0
  92. package/template/src/components/core/nc-scroll-top.ts +116 -0
  93. package/template/src/components/core/nc-select.ts +452 -0
  94. package/template/src/components/core/nc-skeleton.ts +107 -0
  95. package/template/src/components/core/nc-slider.ts +285 -0
  96. package/template/src/components/core/nc-snackbar.ts +230 -0
  97. package/template/src/components/core/nc-splash.ts +343 -0
  98. package/template/src/components/core/nc-stepper.ts +247 -0
  99. package/template/src/components/core/nc-switch.ts +281 -0
  100. package/template/src/components/core/nc-tab-item.ts +138 -0
  101. package/template/src/components/core/nc-table.ts +279 -0
  102. package/template/src/components/core/nc-tabs.ts +554 -0
  103. package/template/src/components/core/nc-tag-input.ts +279 -0
  104. package/template/src/components/core/nc-textarea.ts +216 -0
  105. package/template/src/components/core/nc-time-picker.ts +438 -0
  106. package/template/src/components/core/nc-timeline.ts +186 -0
  107. package/template/src/components/core/nc-tooltip.ts +143 -0
  108. package/template/src/components/frameworkRegistry.ts +68 -0
  109. package/template/src/components/preloadRegistry.ts +28 -0
  110. package/template/src/components/registry.ts +8 -0
  111. package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
  112. package/template/src/constants/apiEndpoints.ts +27 -0
  113. package/template/src/constants/errorMessages.ts +23 -0
  114. package/template/src/constants/index.ts +8 -0
  115. package/template/src/constants/routePaths.ts +15 -0
  116. package/template/src/constants/storageKeys.ts +18 -0
  117. package/template/src/controllers/dashboard.controller.ts +200 -0
  118. package/template/src/controllers/home.controller.ts +21 -0
  119. package/template/src/controllers/index.ts +11 -0
  120. package/template/src/controllers/login.controller.ts +131 -0
  121. package/template/src/core/component.ts +354 -0
  122. package/template/src/core/errorHandler.ts +85 -0
  123. package/template/src/core/gpu-animation.ts +604 -0
  124. package/template/src/core/http.ts +173 -0
  125. package/template/src/core/lazyComponents.ts +90 -0
  126. package/template/src/core/router.ts +642 -0
  127. package/template/src/core/signals.ts +146 -0
  128. package/template/src/core/state.ts +248 -0
  129. package/template/src/dev/component-editor.ts +1363 -0
  130. package/template/src/dev/component-overlay.ts +278 -0
  131. package/template/src/dev/context-menu.ts +223 -0
  132. package/template/src/dev/denc-tools.ts +250 -0
  133. package/template/src/dev/hmr.ts +189 -0
  134. package/template/src/dev/nfbs.code-workspace +27 -0
  135. package/template/src/dev/outline-panel.ts +1247 -0
  136. package/template/src/middleware/auth.middleware.ts +23 -0
  137. package/template/src/routes/routes.ts +38 -0
  138. package/template/src/services/api.service.ts +394 -0
  139. package/template/src/services/auth.service.ts +176 -0
  140. package/template/src/services/index.ts +8 -0
  141. package/template/src/services/logger.service.ts +74 -0
  142. package/template/src/services/storage.service.ts +88 -0
  143. package/template/src/stores/appStore.ts +57 -0
  144. package/template/src/stores/uiStore.ts +36 -0
  145. package/template/src/styles/core-variables.css +219 -0
  146. package/template/src/styles/core.css +710 -0
  147. package/template/src/styles/main.css +3164 -0
  148. package/template/src/styles/variables.css +152 -0
  149. package/template/src/types/global.d.ts +47 -0
  150. package/template/src/utils/cacheBuster.ts +20 -0
  151. package/template/src/utils/dom.ts +149 -0
  152. package/template/src/utils/events.ts +203 -0
  153. package/template/src/utils/form.ts +176 -0
  154. package/template/src/utils/formatters.ts +169 -0
  155. package/template/src/utils/helpers.ts +195 -0
  156. package/template/src/utils/markdown.ts +307 -0
  157. package/template/src/utils/sidebar.ts +96 -0
  158. package/template/src/utils/smoothScroll.ts +85 -0
  159. package/template/src/utils/templates.ts +23 -0
  160. package/template/src/utils/validation.ts +73 -0
  161. package/template/src/views/protected/dashboard.html +293 -0
  162. package/template/src/views/public/home.html +150 -0
  163. package/template/src/views/public/login.html +102 -0
  164. package/template/tests/unit/component.test.ts +87 -0
  165. package/template/tests/unit/computed.test.ts +79 -0
  166. package/template/tests/unit/form.test.ts +68 -0
  167. package/template/tests/unit/formatters.test.ts +49 -0
  168. package/template/tests/unit/lazy-components.test.ts +59 -0
  169. package/template/tests/unit/markdown.test.ts +62 -0
  170. package/template/tests/unit/router.test.ts +112 -0
  171. package/template/tests/unit/signals.test.ts +54 -0
  172. package/template/tests/unit/validation.test.ts +50 -0
  173. package/template/tsconfig.build.json +21 -0
  174. package/template/tsconfig.json +51 -0
  175. package/template/vitest.config.ts +36 -0
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { computed, effect, useState } from '../../src/core/state.js';
3
+
4
+ describe('core/state', () => {
5
+ it('computes an initial value and recomputes when dependencies change', () => {
6
+ const firstName = useState('John');
7
+ const lastName = useState('Doe');
8
+ const fullName = computed(() => `${firstName.value} ${lastName.value}`);
9
+
10
+ expect(fullName.value).toBe('John Doe');
11
+
12
+ firstName.value = 'Jane';
13
+ expect(fullName.value).toBe('Jane Doe');
14
+ });
15
+
16
+ it('releases stale dependencies when the computed branch changes', () => {
17
+ const toggle = useState(true);
18
+ const left = useState('left');
19
+ const right = useState('right');
20
+ const branch = computed(() => (toggle.value ? left.value : right.value));
21
+
22
+ expect(branch.value).toBe('left');
23
+
24
+ toggle.value = false;
25
+ expect(branch.value).toBe('right');
26
+
27
+ left.value = 'updated-left';
28
+ expect(branch.value).toBe('right');
29
+
30
+ right.value = 'updated-right';
31
+ expect(branch.value).toBe('updated-right');
32
+ });
33
+
34
+ it('chains computed values', () => {
35
+ const base = useState(2);
36
+ const doubled = computed(() => base.value * 2);
37
+ const quadrupled = computed(() => doubled.value * 2);
38
+
39
+ expect(quadrupled.value).toBe(8);
40
+
41
+ base.value = 5;
42
+ expect(quadrupled.value).toBe(20);
43
+ });
44
+
45
+ it('keeps computed values read-only', () => {
46
+ const base = useState(5);
47
+ const doubled = computed(() => base.value * 2);
48
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
49
+
50
+ doubled.value = 100;
51
+
52
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Computed values are read-only'));
53
+ expect(doubled.value).toBe(10);
54
+
55
+ consoleSpy.mockRestore();
56
+ });
57
+
58
+ it('tracks reactive side effects and runs cleanup before re-running', () => {
59
+ const count = useState(1);
60
+ const calls: number[] = [];
61
+ const cleanup = vi.fn();
62
+ const stop = effect(() => {
63
+ calls.push(count.value);
64
+ return cleanup;
65
+ });
66
+
67
+ expect(calls).toEqual([1]);
68
+
69
+ count.value = 2;
70
+ expect(calls).toEqual([1, 2]);
71
+ expect(cleanup).toHaveBeenCalledTimes(1);
72
+
73
+ stop();
74
+ expect(cleanup).toHaveBeenCalledTimes(2);
75
+
76
+ count.value = 3;
77
+ expect(calls).toEqual([1, 2]);
78
+ });
79
+ });
@@ -0,0 +1,68 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { useForm } from '../../src/utils/form.js';
3
+ import { isRequired, isValidEmail, minLength } from '../../src/utils/validation.js';
4
+
5
+ describe('utils/form', () => {
6
+ beforeEach(() => {
7
+ document.body.innerHTML = '';
8
+ });
9
+
10
+ it('tracks errors, touched flags, and dirtiness reactively', async () => {
11
+ const form = useForm({
12
+ initialValues: {
13
+ email: '',
14
+ name: ''
15
+ },
16
+ rules: {
17
+ email: [isRequired, isValidEmail],
18
+ name: [minLength(2)]
19
+ }
20
+ });
21
+
22
+ expect(form.isValid.value).toBe(false);
23
+ expect(form.errors.value.email).toBe('Validation failed for email');
24
+
25
+ form.fields.email.value = 'dev@example.com';
26
+ form.fields.name.value = 'Dev';
27
+ form.dirty.email.value = true;
28
+ form.dirty.name.value = true;
29
+
30
+ expect(form.isValid.value).toBe(true);
31
+ expect(form.isDirty.value).toBe(true);
32
+
33
+ const submitHandler = vi.fn(async () => {});
34
+ const submitted = await form.submit(submitHandler)();
35
+
36
+ expect(submitted).toBe(true);
37
+ expect(submitHandler).toHaveBeenCalledWith({
38
+ email: 'dev@example.com',
39
+ name: 'Dev'
40
+ });
41
+ expect(form.isDirty.value).toBe(false);
42
+ });
43
+
44
+ it('binds input elements to field state', () => {
45
+ const input = document.createElement('input');
46
+ document.body.appendChild(input);
47
+
48
+ const form = useForm({
49
+ initialValues: {
50
+ email: ''
51
+ },
52
+ rules: {
53
+ email: [isRequired, isValidEmail]
54
+ }
55
+ });
56
+
57
+ const cleanup = form.bindField('email', input);
58
+ input.value = 'user@example.com';
59
+ input.dispatchEvent(new Event('input'));
60
+ input.dispatchEvent(new Event('blur'));
61
+
62
+ expect(form.fields.email.value).toBe('user@example.com');
63
+ expect(form.touched.email.value).toBe(true);
64
+ expect(form.isValid.value).toBe(true);
65
+
66
+ cleanup();
67
+ });
68
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Example Formatter Test
3
+ * Tests for formatting utilities
4
+ */
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ formatCurrency,
8
+ formatNumber,
9
+ formatPercentage,
10
+ truncate,
11
+ capitalize,
12
+ } from '../../src/utils/formatters.js';
13
+
14
+ describe('Formatter Utilities', () => {
15
+ describe('formatCurrency', () => {
16
+ it('should format currency correctly', () => {
17
+ expect(formatCurrency(99.99)).toBe('$99.99');
18
+ expect(formatCurrency(1000)).toBe('$1,000.00');
19
+ });
20
+ });
21
+
22
+ describe('formatNumber', () => {
23
+ it('should format numbers with commas', () => {
24
+ expect(formatNumber(1000)).toBe('1,000');
25
+ expect(formatNumber(1000000)).toBe('1,000,000');
26
+ });
27
+ });
28
+
29
+ describe('formatPercentage', () => {
30
+ it('should format percentages', () => {
31
+ expect(formatPercentage(0.5)).toBe('50%');
32
+ expect(formatPercentage(0.123, 2)).toBe('12.30%');
33
+ });
34
+ });
35
+
36
+ describe('truncate', () => {
37
+ it('should truncate long text', () => {
38
+ expect(truncate('This is a long text', 10)).toBe('This is...');
39
+ expect(truncate('Short', 10)).toBe('Short');
40
+ });
41
+ });
42
+
43
+ describe('capitalize', () => {
44
+ it('should capitalize first letter', () => {
45
+ expect(capitalize('hello')).toBe('Hello');
46
+ expect(capitalize('HELLO')).toBe('HELLO');
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,59 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { componentRegistry } from '../../src/core/lazyComponents.js';
3
+
4
+ function resetRegistry(): void {
5
+ const registry = componentRegistry as unknown as {
6
+ components: Map<string, string>;
7
+ loaded: Set<string>;
8
+ observer: MutationObserver | null;
9
+ };
10
+
11
+ registry.components.clear();
12
+ registry.loaded.clear();
13
+ registry.observer?.disconnect();
14
+ registry.observer = null;
15
+ }
16
+
17
+ describe('Lazy component registry', () => {
18
+ beforeEach(() => {
19
+ resetRegistry();
20
+ document.body.innerHTML = '';
21
+ });
22
+
23
+ afterEach(() => {
24
+ componentRegistry.stopObserving();
25
+ vi.restoreAllMocks();
26
+ document.body.innerHTML = '';
27
+ });
28
+
29
+ it('loads registered components when matching elements are found', async () => {
30
+ componentRegistry.register('lazy-sample', './ui/lazy-sample.js');
31
+ document.body.innerHTML = '<lazy-sample></lazy-sample>';
32
+
33
+ const loadSpy = vi.spyOn(componentRegistry, 'loadComponent').mockResolvedValue();
34
+
35
+ await componentRegistry.scanAndLoad();
36
+
37
+ expect(loadSpy).toHaveBeenCalledWith('lazy-sample');
38
+ });
39
+
40
+ it('warns when attempting to load an unregistered component', async () => {
41
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
42
+
43
+ await componentRegistry.loadComponent('missing-widget');
44
+
45
+ expect(warnSpy).toHaveBeenCalledWith('Component missing-widget not registered');
46
+ });
47
+
48
+ it('observes DOM additions and scans new nodes', async () => {
49
+ const scanSpy = vi.spyOn(componentRegistry, 'scanAndLoad').mockResolvedValue();
50
+ componentRegistry.startObserving();
51
+
52
+ const wrapper = document.createElement('div');
53
+ wrapper.innerHTML = '<lazy-observed></lazy-observed>';
54
+ document.body.appendChild(wrapper);
55
+ await Promise.resolve();
56
+
57
+ expect(scanSpy).toHaveBeenCalled();
58
+ });
59
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { renderMarkdown, renderMarkdownToc, splitMarkdownIntoSections } from '../../src/utils/markdown.js';
3
+
4
+ describe('utils/markdown', () => {
5
+ it('creates stable unique ids for duplicate headings', () => {
6
+ const result = renderMarkdown(`
7
+ ## Chapter 1
8
+ ### Routing
9
+ ### Routing
10
+ `);
11
+
12
+ expect(result.headings.map(heading => heading.id)).toEqual([
13
+ 'chapter-1',
14
+ 'routing',
15
+ 'routing-2',
16
+ ]);
17
+ expect(result.html).toContain('<h3 id="routing">Routing</h3>');
18
+ expect(result.html).toContain('<h3 id="routing-2">Routing</h3>');
19
+ });
20
+
21
+ it('includes level 4 headings in the table of contents', () => {
22
+ const toc = renderMarkdownToc([
23
+ { level: 2, text: 'Chapter 6: Client-Side Routing', id: 'chapter-6-client-side-routing' },
24
+ { level: 4, text: 'Query Strings and Search Parameters', id: 'query-strings-and-search-parameters' },
25
+ ]);
26
+
27
+ expect(toc).toContain('docs-toc__link--h2');
28
+ expect(toc).toContain('docs-toc__link--h4');
29
+ expect(toc).toContain('#query-strings-and-search-parameters');
30
+ });
31
+
32
+ it('splits handbook markdown into overview and chapter sections', () => {
33
+ const sections = splitMarkdownIntoSections(`
34
+ # NativeCore Handbook
35
+
36
+ ## Preface
37
+
38
+ [Getting Started](#chapter-1-start)
39
+
40
+ Welcome.
41
+
42
+ ## Chapter 1: Getting Started with NativeCore
43
+
44
+ ### Setup
45
+
46
+ ## Chapter 2: Routing
47
+
48
+ ### Setup
49
+ `);
50
+
51
+ expect(sections.map(section => section.title)).toEqual([
52
+ 'Overview',
53
+ 'Chapter 1: Getting Started with NativeCore',
54
+ 'Chapter 2: Routing',
55
+ ]);
56
+ expect(sections[0].isChapter).toBe(false);
57
+ expect(sections[1].id).toBe('chapter-1-start');
58
+ expect(sections[1].aliases).toEqual(['chapter-1-getting-started-with-nativecore']);
59
+ expect(sections[1].headings[1].id).toBe('setup');
60
+ expect(sections[2].headings[1].id).toBe('setup-2');
61
+ });
62
+ });
@@ -0,0 +1,112 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Router } from '../../src/core/router.js';
3
+
4
+ describe('core/router', () => {
5
+ let router: Router;
6
+
7
+ beforeEach(() => {
8
+ document.body.innerHTML = `
9
+ <main class="main-content">
10
+ <div id="main-content"></div>
11
+ </main>
12
+ <div id="page-progress"></div>
13
+ `;
14
+ router = new Router();
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ document.body.innerHTML = '';
20
+ });
21
+
22
+ it('supports optional params and wildcard params', () => {
23
+ router.register('/user/:id?', '/user.html');
24
+ router.register('/docs/*', '/docs.html');
25
+
26
+ const optionalRoute = (router as any).matchRoute('/user');
27
+ const dynamicRoute = (router as any).matchRoute('/user/42');
28
+ const wildcardRoute = (router as any).matchRoute('/docs/guides/intro');
29
+
30
+ expect(optionalRoute?.params).toEqual({});
31
+ expect(dynamicRoute?.params).toEqual({ id: '42' });
32
+ expect(wildcardRoute?.params).toEqual({ wildcard: 'guides/intro' });
33
+ });
34
+
35
+ it('renders child routes into a shared layout outlet without refetching the layout', async () => {
36
+ const fetchMock = vi.fn((input: RequestInfo | URL) => {
37
+ const url = String(input).split('?')[0];
38
+ const htmlByFile: Record<string, string> = {
39
+ '/layout.html': '<section><header>Layout</header><div id="route-outlet"></div></section>',
40
+ '/settings.html': '<div class="settings-page">Settings</div>',
41
+ '/profile.html': '<div class="profile-page">Profile</div>'
42
+ };
43
+
44
+ return Promise.resolve({
45
+ ok: true,
46
+ text: () => Promise.resolve(htmlByFile[url])
47
+ } as Response);
48
+ });
49
+
50
+ vi.stubGlobal('fetch', fetchMock);
51
+
52
+ router.register('/dashboard', '/layout.html');
53
+ router.register('/dashboard/settings', '/settings.html', null, { layout: '/dashboard' });
54
+ router.register('/dashboard/profile', '/profile.html', null, { layout: '/dashboard' });
55
+
56
+ await (router as any).handleRoute('/dashboard/settings', {});
57
+ await (router as any).handleRoute('/dashboard/profile', {});
58
+
59
+ const mainContent = document.getElementById('main-content');
60
+ expect(mainContent?.querySelector('header')?.textContent).toBe('Layout');
61
+ expect(mainContent?.querySelector('#route-outlet')?.innerHTML).toContain('Profile');
62
+ expect(fetchMock.mock.calls.filter(([request]) => String(request).includes('/layout.html'))).toHaveLength(1);
63
+ });
64
+
65
+ it('prefetches route HTML and uses the prefetched response on navigation', async () => {
66
+ const fetchMock = vi.fn((input: RequestInfo | URL) => {
67
+ const url = String(input).split('?')[0];
68
+
69
+ return Promise.resolve({
70
+ ok: true,
71
+ text: () => Promise.resolve(url === '/prefetch.html' ? '<div>Prefetched</div>' : '')
72
+ } as Response);
73
+ });
74
+
75
+ vi.stubGlobal('fetch', fetchMock);
76
+
77
+ router.register('/prefetch', '/prefetch.html');
78
+
79
+ await router.prefetch('/prefetch');
80
+ await (router as any).handleRoute('/prefetch', {});
81
+
82
+ expect(fetchMock).toHaveBeenCalledTimes(1);
83
+ expect(document.getElementById('main-content')?.innerHTML).toContain('Prefetched');
84
+ });
85
+
86
+ it('resets the page scroll position on navigation', async () => {
87
+ const fetchMock = vi.fn(() => Promise.resolve({
88
+ ok: true,
89
+ text: () => Promise.resolve('<div style="height: 2000px;">Scrolled page</div>')
90
+ } as Response));
91
+
92
+ vi.stubGlobal('fetch', fetchMock);
93
+ const windowScrollSpy = vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
94
+
95
+ router.register('/scrolled', '/scrolled.html');
96
+
97
+ const mainContent = document.getElementById('main-content') as HTMLElement;
98
+ const scrollContainer = document.querySelector('.main-content') as HTMLElement;
99
+ scrollContainer.scrollTop = 240;
100
+ scrollContainer.scrollLeft = 32;
101
+ mainContent.scrollTop = 240;
102
+ mainContent.scrollLeft = 32;
103
+
104
+ await (router as any).handleRoute('/scrolled', {});
105
+
106
+ expect(scrollContainer.scrollTop).toBe(0);
107
+ expect(scrollContainer.scrollLeft).toBe(0);
108
+ expect(mainContent.scrollTop).toBe(0);
109
+ expect(mainContent.scrollLeft).toBe(0);
110
+ expect(windowScrollSpy).toHaveBeenCalledWith(0, 0);
111
+ });
112
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createComputed, createEffect, createSignal } from '../../src/core/signals.js';
3
+
4
+ describe('core/signals', () => {
5
+ it('keeps computed values live when source signals change', () => {
6
+ const count = createSignal(2);
7
+ const doubled = createComputed(() => count.get() * 2);
8
+
9
+ expect(doubled()).toBe(4);
10
+
11
+ count.set(5);
12
+ expect(doubled()).toBe(10);
13
+ });
14
+
15
+ it('releases stale dependencies when a computed branch changes', () => {
16
+ const toggle = createSignal(true);
17
+ const left = createSignal('left');
18
+ const right = createSignal('right');
19
+ const branch = createComputed(() => (toggle.get() ? left.get() : right.get()));
20
+
21
+ expect(branch()).toBe('left');
22
+
23
+ toggle.set(false);
24
+ expect(branch()).toBe('right');
25
+
26
+ left.set('updated-left');
27
+ expect(branch()).toBe('right');
28
+
29
+ right.set('updated-right');
30
+ expect(branch()).toBe('updated-right');
31
+ });
32
+
33
+ it('tracks effects and runs cleanup before rerunning', () => {
34
+ const count = createSignal(1);
35
+ const seen: number[] = [];
36
+ const cleanup = vi.fn();
37
+ const stop = createEffect(() => {
38
+ seen.push(count.get());
39
+ return cleanup;
40
+ });
41
+
42
+ expect(seen).toEqual([1]);
43
+
44
+ count.set(2);
45
+ expect(seen).toEqual([1, 2]);
46
+ expect(cleanup).toHaveBeenCalledTimes(1);
47
+
48
+ stop();
49
+ expect(cleanup).toHaveBeenCalledTimes(2);
50
+
51
+ count.set(3);
52
+ expect(seen).toEqual([1, 2]);
53
+ });
54
+ });
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ isNumber,
4
+ isRequired,
5
+ isValidEmail,
6
+ matchesPattern,
7
+ maxLength,
8
+ minLength,
9
+ validateForm
10
+ } from '../../src/utils/validation.js';
11
+
12
+ describe('utils/validation', () => {
13
+ it('validates email addresses', () => {
14
+ expect(isValidEmail('test@example.com')).toBe(true);
15
+ expect(isValidEmail('invalid')).toBe(false);
16
+ });
17
+
18
+ it('validates required values', () => {
19
+ expect(isRequired('value')).toBe(true);
20
+ expect(isRequired(' ')).toBe(false);
21
+ expect(isRequired(null)).toBe(false);
22
+ });
23
+
24
+ it('validates length rules and patterns', () => {
25
+ expect(minLength(3)('abcd')).toBe(true);
26
+ expect(maxLength(3)('abcd')).toBe(false);
27
+ expect(matchesPattern(/^\d+$/)('1234')).toBe(true);
28
+ });
29
+
30
+ it('validates numbers', () => {
31
+ expect(isNumber(123)).toBe(true);
32
+ expect(isNumber('123.45')).toBe(true);
33
+ expect(isNumber('abc')).toBe(false);
34
+ });
35
+
36
+ it('returns field-level form errors', () => {
37
+ const errors = validateForm(
38
+ { email: 'bad-email', name: 'A' },
39
+ {
40
+ email: [isRequired, isValidEmail],
41
+ name: [minLength(2)]
42
+ }
43
+ );
44
+
45
+ expect(errors).toEqual({
46
+ email: 'Validation failed for email',
47
+ name: 'Validation failed for name'
48
+ });
49
+ });
50
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "sourceMap": false,
5
+ "declaration": false,
6
+ "declarationMap": false,
7
+ "removeComments": true
8
+ },
9
+ "include": [
10
+ "src/**/*.ts"
11
+ ],
12
+ "exclude": [
13
+ "node_modules",
14
+ "dist",
15
+ "tests",
16
+ "**/*.test.ts",
17
+ "**/*.spec.ts",
18
+ "src/dev/**/*.ts",
19
+ "src/dev"
20
+ ]
21
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "moduleResolution": "node",
7
+ "incremental": true,
8
+ "tsBuildInfoFile": "./dist/.tsbuildinfo",
9
+ "allowJs": true,
10
+ "checkJs": false,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src",
16
+ "baseUrl": "./src",
17
+ "paths": {
18
+ "@core/*": ["core/*"],
19
+ "@components/*": ["components/*"],
20
+ "@config/*": ["config/*"],
21
+ "@routes/*": ["routes/*"],
22
+ "@services/*": ["services/*"],
23
+ "@utils/*": ["utils/*"],
24
+ "@stores/*": ["stores/*"],
25
+ "@middleware/*": ["middleware/*"],
26
+ "@types/*": ["types/*"],
27
+ "@constants/*": ["constants/*"]
28
+ },
29
+ "strict": true,
30
+ "ignoreDeprecations": "5.0",
31
+ "noImplicitAny": false,
32
+ "strictNullChecks": false,
33
+ "noUnusedLocals": false,
34
+ "noUnusedParameters": false,
35
+ "noImplicitReturns": true,
36
+ "noFallthroughCasesInSwitch": true,
37
+ "esModuleInterop": true,
38
+ "skipLibCheck": true,
39
+ "forceConsistentCasingInFileNames": true,
40
+ "resolveJsonModule": true,
41
+ "jsx": "preserve"
42
+ },
43
+ "include": [
44
+ "src/**/*"
45
+ ],
46
+ "exclude": [
47
+ "node_modules",
48
+ "dist",
49
+ "tests"
50
+ ]
51
+ }
@@ -0,0 +1,36 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ const srcDir = fileURLToPath(new URL('./src', import.meta.url));
5
+
6
+ export default defineConfig({
7
+ resolve: {
8
+ alias: {
9
+ '@core': fileURLToPath(new URL('./src/core', import.meta.url)),
10
+ '@components': fileURLToPath(new URL('./src/components', import.meta.url)),
11
+ '@config': fileURLToPath(new URL('./src/config', import.meta.url)),
12
+ '@routes': fileURLToPath(new URL('./src/routes', import.meta.url)),
13
+ '@services': fileURLToPath(new URL('./src/services', import.meta.url)),
14
+ '@utils': fileURLToPath(new URL('./src/utils', import.meta.url)),
15
+ '@stores': fileURLToPath(new URL('./src/stores', import.meta.url)),
16
+ '@middleware': fileURLToPath(new URL('./src/middleware', import.meta.url)),
17
+ '@types': fileURLToPath(new URL('./src/types', import.meta.url)),
18
+ '@constants': fileURLToPath(new URL('./src/constants', import.meta.url)),
19
+ },
20
+ },
21
+ test: {
22
+ environment: 'happy-dom',
23
+ globals: true,
24
+ coverage: {
25
+ provider: 'v8',
26
+ reporter: ['text', 'json', 'html'],
27
+ exclude: [
28
+ 'node_modules/',
29
+ 'tests/',
30
+ '**/*.config.js',
31
+ 'server.js',
32
+ 'api/',
33
+ ],
34
+ },
35
+ },
36
+ });