meno-core 1.0.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 (231) hide show
  1. package/bin/cli.ts +281 -0
  2. package/build-static.ts +298 -0
  3. package/bunfig.toml +39 -0
  4. package/entries/client-router.tsx +111 -0
  5. package/entries/server-router.tsx +71 -0
  6. package/lib/client/ClientInitializer.test.ts +9 -0
  7. package/lib/client/ClientInitializer.test.ts.skip +92 -0
  8. package/lib/client/ClientInitializer.ts +60 -0
  9. package/lib/client/ErrorBoundary.test.tsx +595 -0
  10. package/lib/client/ErrorBoundary.tsx +230 -0
  11. package/lib/client/componentRegistry.test.ts +165 -0
  12. package/lib/client/componentRegistry.ts +18 -0
  13. package/lib/client/contexts/ThemeContext.tsx +73 -0
  14. package/lib/client/core/ComponentBuilder.test.ts +677 -0
  15. package/lib/client/core/ComponentBuilder.ts +660 -0
  16. package/lib/client/core/ComponentRenderer.test.tsx +176 -0
  17. package/lib/client/core/ComponentRenderer.tsx +83 -0
  18. package/lib/client/core/cmsTemplateProcessor.ts +129 -0
  19. package/lib/client/elementRegistry.ts +81 -0
  20. package/lib/client/hmr/HMRManager.tsx +179 -0
  21. package/lib/client/hmr/index.ts +5 -0
  22. package/lib/client/hmrWebSocket.test.ts +9 -0
  23. package/lib/client/hmrWebSocket.ts +250 -0
  24. package/lib/client/hooks/useColorVariables.test.ts +166 -0
  25. package/lib/client/hooks/useColorVariables.ts +249 -0
  26. package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
  27. package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
  28. package/lib/client/hydration/HydrationUtils.test.ts +154 -0
  29. package/lib/client/hydration/HydrationUtils.ts +35 -0
  30. package/lib/client/i18nConfigService.test.ts +74 -0
  31. package/lib/client/i18nConfigService.ts +78 -0
  32. package/lib/client/index.ts +56 -0
  33. package/lib/client/navigation.test.ts +441 -0
  34. package/lib/client/navigation.ts +23 -0
  35. package/lib/client/responsiveStyleResolver.test.ts +491 -0
  36. package/lib/client/responsiveStyleResolver.ts +184 -0
  37. package/lib/client/routing/RouteLoader.test.ts +635 -0
  38. package/lib/client/routing/RouteLoader.ts +347 -0
  39. package/lib/client/routing/Router.tsx +382 -0
  40. package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
  41. package/lib/client/scripts/ScriptExecutor.ts +171 -0
  42. package/lib/client/scripts/formHandler.ts +103 -0
  43. package/lib/client/styleProcessor.test.ts +126 -0
  44. package/lib/client/styleProcessor.ts +92 -0
  45. package/lib/client/styles/StyleInjector.test.ts +354 -0
  46. package/lib/client/styles/StyleInjector.ts +154 -0
  47. package/lib/client/templateEngine.test.ts +660 -0
  48. package/lib/client/templateEngine.ts +667 -0
  49. package/lib/client/theme.test.ts +173 -0
  50. package/lib/client/theme.ts +159 -0
  51. package/lib/client/utils/toast.ts +46 -0
  52. package/lib/server/createServer.ts +170 -0
  53. package/lib/server/cssGenerator.test.ts +172 -0
  54. package/lib/server/cssGenerator.ts +58 -0
  55. package/lib/server/fileWatcher.ts +134 -0
  56. package/lib/server/index.ts +55 -0
  57. package/lib/server/jsonLoader.test.ts +103 -0
  58. package/lib/server/jsonLoader.ts +350 -0
  59. package/lib/server/middleware/cors.test.ts +177 -0
  60. package/lib/server/middleware/cors.ts +69 -0
  61. package/lib/server/middleware/errorHandler.test.ts +208 -0
  62. package/lib/server/middleware/errorHandler.ts +63 -0
  63. package/lib/server/middleware/index.ts +9 -0
  64. package/lib/server/middleware/logger.test.ts +233 -0
  65. package/lib/server/middleware/logger.ts +99 -0
  66. package/lib/server/pageCache.test.ts +167 -0
  67. package/lib/server/pageCache.ts +97 -0
  68. package/lib/server/projectContext.ts +51 -0
  69. package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
  70. package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
  71. package/lib/server/providers/fileSystemPageProvider.ts +83 -0
  72. package/lib/server/routes/api/cms.test.ts +177 -0
  73. package/lib/server/routes/api/cms.ts +82 -0
  74. package/lib/server/routes/api/colors.ts +59 -0
  75. package/lib/server/routes/api/components.ts +70 -0
  76. package/lib/server/routes/api/config.test.ts +9 -0
  77. package/lib/server/routes/api/config.ts +28 -0
  78. package/lib/server/routes/api/core-routes.ts +182 -0
  79. package/lib/server/routes/api/functions.ts +170 -0
  80. package/lib/server/routes/api/index.ts +69 -0
  81. package/lib/server/routes/api/pages.ts +95 -0
  82. package/lib/server/routes/api/shared.test.ts +81 -0
  83. package/lib/server/routes/api/shared.ts +31 -0
  84. package/lib/server/routes/editor.test.ts +9 -0
  85. package/lib/server/routes/index.ts +104 -0
  86. package/lib/server/routes/pages.ts +161 -0
  87. package/lib/server/routes/static.ts +107 -0
  88. package/lib/server/services/ColorService.ts +193 -0
  89. package/lib/server/services/cmsService.test.ts +388 -0
  90. package/lib/server/services/cmsService.ts +296 -0
  91. package/lib/server/services/componentService.test.ts +276 -0
  92. package/lib/server/services/componentService.ts +346 -0
  93. package/lib/server/services/configService.ts +156 -0
  94. package/lib/server/services/fileWatcherService.ts +67 -0
  95. package/lib/server/services/index.ts +10 -0
  96. package/lib/server/services/pageService.test.ts +258 -0
  97. package/lib/server/services/pageService.ts +240 -0
  98. package/lib/server/ssrRenderer.test.ts +1005 -0
  99. package/lib/server/ssrRenderer.ts +878 -0
  100. package/lib/server/utilityClassGenerator.ts +11 -0
  101. package/lib/server/utils/index.ts +5 -0
  102. package/lib/server/utils/jsonLineMapper.test.ts +100 -0
  103. package/lib/server/utils/jsonLineMapper.ts +166 -0
  104. package/lib/server/validateStyleCoverage.test.ts +9 -0
  105. package/lib/server/validateStyleCoverage.ts +167 -0
  106. package/lib/server/websocketManager.test.ts +9 -0
  107. package/lib/server/websocketManager.ts +95 -0
  108. package/lib/shared/attributeNodeUtils.test.ts +152 -0
  109. package/lib/shared/attributeNodeUtils.ts +50 -0
  110. package/lib/shared/breakpoints.test.ts +166 -0
  111. package/lib/shared/breakpoints.ts +65 -0
  112. package/lib/shared/colorProperties.test.ts +111 -0
  113. package/lib/shared/colorProperties.ts +40 -0
  114. package/lib/shared/colorVariableUtils.test.ts +319 -0
  115. package/lib/shared/colorVariableUtils.ts +97 -0
  116. package/lib/shared/constants.test.ts +175 -0
  117. package/lib/shared/constants.ts +116 -0
  118. package/lib/shared/cssGeneration.ts +481 -0
  119. package/lib/shared/cssProperties.test.ts +252 -0
  120. package/lib/shared/cssProperties.ts +338 -0
  121. package/lib/shared/elementUtils.test.ts +245 -0
  122. package/lib/shared/elementUtils.ts +90 -0
  123. package/lib/shared/fontLoader.ts +97 -0
  124. package/lib/shared/i18n.test.ts +313 -0
  125. package/lib/shared/i18n.ts +286 -0
  126. package/lib/shared/index.ts +50 -0
  127. package/lib/shared/interfaces/contentProvider.test.ts +9 -0
  128. package/lib/shared/interfaces/contentProvider.ts +121 -0
  129. package/lib/shared/nodeUtils.test.ts +320 -0
  130. package/lib/shared/nodeUtils.ts +220 -0
  131. package/lib/shared/pathArrayUtils.test.ts +315 -0
  132. package/lib/shared/pathArrayUtils.ts +17 -0
  133. package/lib/shared/pathUtils.test.ts +260 -0
  134. package/lib/shared/pathUtils.ts +244 -0
  135. package/lib/shared/paths/Path.test.ts +74 -0
  136. package/lib/shared/paths/Path.ts +23 -0
  137. package/lib/shared/paths/PathConverter.test.ts +232 -0
  138. package/lib/shared/paths/PathConverter.ts +141 -0
  139. package/lib/shared/paths/PathUtils.ts +290 -0
  140. package/lib/shared/paths/PathValidator.test.ts +193 -0
  141. package/lib/shared/paths/PathValidator.ts +53 -0
  142. package/lib/shared/paths/index.ts +48 -0
  143. package/lib/shared/propResolver.test.ts +639 -0
  144. package/lib/shared/propResolver.ts +124 -0
  145. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
  146. package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
  147. package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
  148. package/lib/shared/registry/ClientRegistry.test.ts +26 -0
  149. package/lib/shared/registry/ClientRegistry.ts +15 -0
  150. package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
  151. package/lib/shared/registry/ComponentRegistry.ts +100 -0
  152. package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
  153. package/lib/shared/registry/NodeTypeManager.ts +94 -0
  154. package/lib/shared/registry/RegistryManager.test.ts +58 -0
  155. package/lib/shared/registry/RegistryManager.ts +60 -0
  156. package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
  157. package/lib/shared/registry/SSRRegistry.test.ts +26 -0
  158. package/lib/shared/registry/SSRRegistry.ts +15 -0
  159. package/lib/shared/registry/createNodeType.ts +175 -0
  160. package/lib/shared/registry/defineNodeType.ts +73 -0
  161. package/lib/shared/registry/fieldPresets.ts +109 -0
  162. package/lib/shared/registry/index.ts +50 -0
  163. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
  164. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
  165. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
  166. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
  167. package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
  168. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
  169. package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
  170. package/lib/shared/registry/nodeTypes/index.ts +75 -0
  171. package/lib/shared/responsiveScaling.test.ts +268 -0
  172. package/lib/shared/responsiveScaling.ts +194 -0
  173. package/lib/shared/responsiveStyleUtils.test.ts +300 -0
  174. package/lib/shared/responsiveStyleUtils.ts +139 -0
  175. package/lib/shared/slugTranslator.test.ts +325 -0
  176. package/lib/shared/slugTranslator.ts +177 -0
  177. package/lib/shared/styleNodeUtils.test.ts +132 -0
  178. package/lib/shared/styleNodeUtils.ts +102 -0
  179. package/lib/shared/styleUtils.test.ts +238 -0
  180. package/lib/shared/styleUtils.ts +63 -0
  181. package/lib/shared/themeDefaults.test.ts +113 -0
  182. package/lib/shared/themeDefaults.ts +103 -0
  183. package/lib/shared/tree/PathBuilder.ts +383 -0
  184. package/lib/shared/treePathUtils.test.ts +539 -0
  185. package/lib/shared/treePathUtils.ts +339 -0
  186. package/lib/shared/types/api.ts +58 -0
  187. package/lib/shared/types/cms.ts +95 -0
  188. package/lib/shared/types/colors.ts +45 -0
  189. package/lib/shared/types/components.ts +121 -0
  190. package/lib/shared/types/errors.test.ts +103 -0
  191. package/lib/shared/types/errors.ts +69 -0
  192. package/lib/shared/types/index.ts +96 -0
  193. package/lib/shared/types/nodes.ts +20 -0
  194. package/lib/shared/types/rendering.ts +61 -0
  195. package/lib/shared/types/styles.ts +38 -0
  196. package/lib/shared/types.ts +11 -0
  197. package/lib/shared/utilityClassConfig.ts +287 -0
  198. package/lib/shared/utilityClassMapper.test.ts +140 -0
  199. package/lib/shared/utilityClassMapper.ts +229 -0
  200. package/lib/shared/utils/fileUtils.test.ts +99 -0
  201. package/lib/shared/utils/fileUtils.ts +56 -0
  202. package/lib/shared/utils.test.ts +261 -0
  203. package/lib/shared/utils.ts +84 -0
  204. package/lib/shared/validation/index.ts +7 -0
  205. package/lib/shared/validation/propValidator.test.ts +178 -0
  206. package/lib/shared/validation/propValidator.ts +238 -0
  207. package/lib/shared/validation/schemas.test.ts +177 -0
  208. package/lib/shared/validation/schemas.ts +401 -0
  209. package/lib/shared/validation/validators.test.ts +109 -0
  210. package/lib/shared/validation/validators.ts +304 -0
  211. package/lib/test-utils/dom-setup.ts +55 -0
  212. package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
  213. package/lib/test-utils/factories/DomMockFactory.ts +487 -0
  214. package/lib/test-utils/factories/EventMockFactory.ts +244 -0
  215. package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
  216. package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
  217. package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
  218. package/lib/test-utils/factories/index.ts +11 -0
  219. package/lib/test-utils/fixtures.ts +134 -0
  220. package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
  221. package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
  222. package/lib/test-utils/helpers/index.ts +6 -0
  223. package/lib/test-utils/helpers.test.ts +73 -0
  224. package/lib/test-utils/helpers.ts +90 -0
  225. package/lib/test-utils/index.ts +17 -0
  226. package/lib/test-utils/mockFactories.ts +92 -0
  227. package/lib/test-utils/mocks.ts +341 -0
  228. package/package.json +38 -0
  229. package/templates/index-router.html +34 -0
  230. package/tsconfig.json +14 -0
  231. package/vite.config.ts +43 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Config Service
3
+ * Centralized configuration loading and access
4
+ *
5
+ * Consolidates multiple config loaders into a single service that loads
6
+ * the project.config.json file once and exposes typed sections.
7
+ */
8
+
9
+ import type { BreakpointConfig } from '../../shared/breakpoints';
10
+ import { DEFAULT_BREAKPOINTS } from '../../shared/breakpoints';
11
+ import type { ResponsiveScales } from '../../shared/responsiveScaling';
12
+ import { DEFAULT_RESPONSIVE_SCALES } from '../../shared/responsiveScaling';
13
+ import type { I18nConfig } from '../../shared/types/components';
14
+ import { DEFAULT_I18N_CONFIG, migrateI18nConfig } from '../../shared/i18n';
15
+ import { projectPaths } from '../projectContext';
16
+
17
+ /**
18
+ * Icons configuration
19
+ */
20
+ export interface IconsConfig {
21
+ favicon?: string;
22
+ appleTouchIcon?: string;
23
+ }
24
+
25
+ /**
26
+ * Raw project config structure from project.config.json
27
+ */
28
+ interface RawProjectConfig {
29
+ breakpoints?: Record<string, number>;
30
+ responsiveScales?: Partial<ResponsiveScales>;
31
+ i18n?: unknown;
32
+ icons?: IconsConfig;
33
+ }
34
+
35
+ /**
36
+ * ConfigService
37
+ * Loads project configuration once and provides typed access to sections
38
+ */
39
+ export class ConfigService {
40
+ private config: RawProjectConfig | null = null;
41
+ private loaded = false;
42
+
43
+ /**
44
+ * Load configuration from project.config.json
45
+ * Safe to call multiple times - only loads once
46
+ */
47
+ async load(): Promise<void> {
48
+ if (this.loaded) {
49
+ return;
50
+ }
51
+
52
+ try {
53
+ const file = Bun.file(projectPaths.config());
54
+ if (await file.exists()) {
55
+ const content = await file.text();
56
+ this.config = JSON.parse(content);
57
+ }
58
+ } catch {
59
+ // Fall through to defaults
60
+ this.config = null;
61
+ }
62
+
63
+ this.loaded = true;
64
+ }
65
+
66
+ /**
67
+ * Check if configuration has been loaded
68
+ */
69
+ isLoaded(): boolean {
70
+ return this.loaded;
71
+ }
72
+
73
+ /**
74
+ * Reset the service (for testing)
75
+ */
76
+ reset(): void {
77
+ this.config = null;
78
+ this.loaded = false;
79
+ }
80
+
81
+ /**
82
+ * Get breakpoint configuration
83
+ * Returns validated breakpoints or defaults
84
+ */
85
+ getBreakpoints(): BreakpointConfig {
86
+ if (!this.config?.breakpoints || typeof this.config.breakpoints !== 'object') {
87
+ return { ...DEFAULT_BREAKPOINTS };
88
+ }
89
+
90
+ // Validate breakpoint values
91
+ const breakpoints: BreakpointConfig = {};
92
+ for (const [key, value] of Object.entries(this.config.breakpoints)) {
93
+ if (typeof value === 'number' && value > 0) {
94
+ breakpoints[key] = value;
95
+ }
96
+ }
97
+
98
+ // Return validated breakpoints or defaults if none valid
99
+ return Object.keys(breakpoints).length > 0 ? breakpoints : { ...DEFAULT_BREAKPOINTS };
100
+ }
101
+
102
+ /**
103
+ * Get i18n configuration
104
+ * Automatically migrates old string[] format to LocaleConfig[] format
105
+ */
106
+ getI18n(): I18nConfig {
107
+ if (!this.config?.i18n) {
108
+ return { ...DEFAULT_I18N_CONFIG };
109
+ }
110
+
111
+ return migrateI18nConfig(this.config.i18n);
112
+ }
113
+
114
+ /**
115
+ * Get responsive scales configuration
116
+ * Merges with defaults for any missing values
117
+ */
118
+ getResponsiveScales(): ResponsiveScales {
119
+ if (!this.config?.responsiveScales || typeof this.config.responsiveScales !== 'object') {
120
+ return { ...DEFAULT_RESPONSIVE_SCALES };
121
+ }
122
+
123
+ return {
124
+ ...DEFAULT_RESPONSIVE_SCALES,
125
+ ...this.config.responsiveScales,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Get icons configuration
131
+ * Returns empty object if not configured
132
+ */
133
+ getIcons(): IconsConfig {
134
+ if (!this.config?.icons || typeof this.config.icons !== 'object') {
135
+ return {};
136
+ }
137
+
138
+ return this.config.icons;
139
+ }
140
+
141
+ /**
142
+ * Get raw config value by key (for extension)
143
+ */
144
+ getRaw<T>(key: string): T | undefined {
145
+ if (!this.config) {
146
+ return undefined;
147
+ }
148
+ return (this.config as Record<string, unknown>)[key] as T | undefined;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Singleton instance for global access
154
+ * Use this for convenience, or create your own instance for testing
155
+ */
156
+ export const configService = new ConfigService();
@@ -0,0 +1,67 @@
1
+ /**
2
+ * File Watcher Service
3
+ * Manages file watching for pages and components
4
+ */
5
+
6
+ import { join } from 'path';
7
+ import { FileWatcher, type FileWatchCallbacks } from '../fileWatcher';
8
+ import { WebSocketManager } from '../websocketManager';
9
+ import { ComponentService } from './componentService';
10
+ import { PageService } from './pageService';
11
+ import { PageCache } from '../pageCache';
12
+ import { colorService } from './ColorService';
13
+ import { loadJSONFile, mapPageNameToPath } from '../jsonLoader';
14
+ import { projectPaths } from '../projectContext';
15
+
16
+ export class FileWatcherService {
17
+ private fileWatcher: FileWatcher | null = null;
18
+
19
+ constructor(
20
+ private componentService: ComponentService,
21
+ private pageService: PageService,
22
+ private pageCache: PageCache,
23
+ private wsManager: WebSocketManager
24
+ ) {}
25
+
26
+ /**
27
+ * Initialize file watchers
28
+ */
29
+ initialize(): void {
30
+ this.fileWatcher = new FileWatcher({
31
+ onComponentChange: async () => {
32
+ await this.componentService.loadAllComponents();
33
+ this.wsManager.broadcastUpdate('all');
34
+ },
35
+ onPageChange: async (pagePath: string) => {
36
+ const pageName = pagePath === '/' ? 'index' : pagePath.substring(1);
37
+ const content = await loadJSONFile(join(projectPaths.pages(), `${pageName}.json`));
38
+ if (content) {
39
+ this.pageCache.set(pagePath, content);
40
+ this.wsManager.broadcastUpdate(pagePath);
41
+ } else {
42
+ this.pageCache.delete(pagePath);
43
+ this.wsManager.broadcastUpdate(pagePath);
44
+ }
45
+ },
46
+ onColorsChange: async () => {
47
+ // Clear the color service cache so it reloads on next request
48
+ colorService.clearCache();
49
+ // Broadcast to all clients to refresh their color data
50
+ this.wsManager.broadcastColorsUpdate();
51
+ },
52
+ });
53
+
54
+ this.fileWatcher.watchAll();
55
+ }
56
+
57
+ /**
58
+ * Stop file watchers
59
+ */
60
+ stop(): void {
61
+ if (this.fileWatcher) {
62
+ this.fileWatcher.stopAll();
63
+ this.fileWatcher = null;
64
+ }
65
+ }
66
+ }
67
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Server Services
3
+ * Centralized services for server operations
4
+ */
5
+
6
+ export { PageService } from './pageService';
7
+ export { ComponentService } from './componentService';
8
+ export { FileWatcherService } from './fileWatcherService';
9
+ export { CMSService } from './cmsService';
10
+
@@ -0,0 +1,258 @@
1
+ import { test, expect, describe, beforeEach, afterEach, mock } from 'bun:test';
2
+ import { PageService } from './pageService';
3
+ import { PageCache } from '../pageCache';
4
+ import { createMockPageData } from '../../test-utils';
5
+ import { createTypedMockPageProvider, type TypedMockPageProvider } from '../../test-utils/factories/ServerMockFactory';
6
+
7
+ describe('PageService', () => {
8
+ let pageCache: PageCache;
9
+ let pageService: PageService;
10
+ let mockProvider: TypedMockPageProvider;
11
+
12
+ beforeEach(() => {
13
+ pageCache = new PageCache();
14
+ mockProvider = createTypedMockPageProvider();
15
+ pageService = new PageService(pageCache, mockProvider);
16
+ });
17
+
18
+ afterEach(() => {
19
+ pageCache.clear();
20
+ });
21
+
22
+ describe('loadAllPages', () => {
23
+ test('should load all pages from provider', async () => {
24
+ const indexData = createMockPageData({ meta: { title: 'Home' } });
25
+ const aboutData = createMockPageData({ meta: { title: 'About' } });
26
+
27
+ // Set up provider with pages
28
+ mockProvider._pages['/'] = JSON.stringify(indexData);
29
+ mockProvider._pages['/about'] = JSON.stringify(aboutData);
30
+
31
+ await pageService.loadAllPages();
32
+
33
+ const indexPage = pageService.getPage('/');
34
+ const aboutPage = pageService.getPage('/about');
35
+
36
+ expect(indexPage).toBeDefined();
37
+ expect(aboutPage).toBeDefined();
38
+ expect(mockProvider.loadAll).toHaveBeenCalledTimes(1);
39
+ });
40
+
41
+ test('should handle empty provider gracefully', async () => {
42
+ await pageService.loadAllPages();
43
+
44
+ expect(pageService.getAllPagePaths()).toEqual([]);
45
+ expect(mockProvider.loadAll).toHaveBeenCalled();
46
+ });
47
+
48
+ test('should work without provider set', async () => {
49
+ const serviceWithoutProvider = new PageService(pageCache);
50
+
51
+ // Should not throw
52
+ await serviceWithoutProvider.loadAllPages();
53
+
54
+ expect(serviceWithoutProvider.getAllPagePaths()).toEqual([]);
55
+ });
56
+ });
57
+
58
+ describe('getPage', () => {
59
+ test('should return page content from cache', () => {
60
+ const testData = createMockPageData();
61
+ pageCache.set('/', JSON.stringify(testData), new Map());
62
+
63
+ const result = pageService.getPage('/');
64
+
65
+ expect(result).toBe(JSON.stringify(testData));
66
+ });
67
+
68
+ test('should return undefined for non-existent page', () => {
69
+ const result = pageService.getPage('/non-existent');
70
+
71
+ expect(result).toBeUndefined();
72
+ });
73
+ });
74
+
75
+ describe('getPageData', () => {
76
+ test('should return parsed page data', () => {
77
+ const testData = createMockPageData();
78
+ pageCache.set('/', JSON.stringify(testData), new Map());
79
+
80
+ const result = pageService.getPageData('/');
81
+
82
+ expect(result).toBeDefined();
83
+ expect(result?.meta?.title).toBe(testData.meta?.title);
84
+ expect(result?.root).toBeDefined();
85
+ });
86
+
87
+ test('should return null for non-existent page', () => {
88
+ const result = pageService.getPageData('/non-existent');
89
+
90
+ expect(result).toBeNull();
91
+ });
92
+
93
+ test('should return null for invalid JSON', () => {
94
+ pageCache.set('/invalid', 'invalid json', new Map());
95
+
96
+ const result = pageService.getPageData('/invalid');
97
+
98
+ expect(result).toBeNull();
99
+ });
100
+ });
101
+
102
+ describe('savePage', () => {
103
+ test('should save page via provider', async () => {
104
+ const testData = createMockPageData({ meta: { title: 'Test Page' } });
105
+
106
+ await pageService.savePage('/', testData);
107
+
108
+ expect(mockProvider.save).toHaveBeenCalledWith('/', expect.any(String));
109
+ expect(pageService.getPage('/')).toBeDefined();
110
+ });
111
+
112
+ test('should update cache after saving', async () => {
113
+ const testData = createMockPageData({ meta: { title: 'Updated Page' } });
114
+
115
+ await pageService.savePage('/', testData);
116
+
117
+ const cached = pageService.getPageData('/');
118
+ expect(cached?.meta?.title).toBe('Updated Page');
119
+ });
120
+
121
+ test('should throw error when provider not set', async () => {
122
+ const serviceWithoutProvider = new PageService(pageCache);
123
+ const testData = createMockPageData();
124
+
125
+ await expect(serviceWithoutProvider.savePage('/', testData)).rejects.toThrow(
126
+ 'PageProvider not set'
127
+ );
128
+ });
129
+
130
+ test('should strip _lineMap from saved data', async () => {
131
+ const testData = createMockPageData({ meta: { title: 'Test' } });
132
+ (testData as any)._lineMap = new Map();
133
+
134
+ await pageService.savePage('/', testData);
135
+
136
+ const savedContent = (mockProvider.save as any).mock.calls[0][1];
137
+ const savedData = JSON.parse(savedContent);
138
+ expect(savedData._lineMap).toBeUndefined();
139
+ });
140
+ });
141
+
142
+ describe('deletePage', () => {
143
+ test('should delete page from cache', async () => {
144
+ const testData = createMockPageData();
145
+ pageCache.set('/', JSON.stringify(testData), new Map());
146
+
147
+ await pageService.deletePage('/');
148
+
149
+ expect(pageService.getPage('/')).toBeUndefined();
150
+ });
151
+
152
+ test('should delete from storage when deleteFromStorage is true', async () => {
153
+ const testData = createMockPageData();
154
+ pageCache.set('/', JSON.stringify(testData), new Map());
155
+
156
+ await pageService.deletePage('/', true);
157
+
158
+ expect(mockProvider.delete).toHaveBeenCalledWith('/');
159
+ });
160
+
161
+ test('should not delete from storage when deleteFromStorage is false', async () => {
162
+ const testData = createMockPageData();
163
+ pageCache.set('/', JSON.stringify(testData), new Map());
164
+
165
+ await pageService.deletePage('/');
166
+
167
+ expect(mockProvider.delete).not.toHaveBeenCalled();
168
+ });
169
+ });
170
+
171
+ describe('getAllPagePaths', () => {
172
+ test('should return all page paths from cache', () => {
173
+ pageCache.set('/', JSON.stringify(createMockPageData()), new Map());
174
+ pageCache.set('/about', JSON.stringify(createMockPageData()), new Map());
175
+
176
+ const paths = pageService.getAllPagePaths();
177
+
178
+ expect(paths).toContain('/');
179
+ expect(paths).toContain('/about');
180
+ expect(paths.length).toBe(2);
181
+ });
182
+
183
+ test('should return empty array when cache is empty', () => {
184
+ const paths = pageService.getAllPagePaths();
185
+
186
+ expect(paths).toEqual([]);
187
+ });
188
+ });
189
+
190
+ describe('getLineMap', () => {
191
+ test('should return line map from cache', () => {
192
+ const lineMap = new Map([['root', { startLine: 1, endLine: 10 }]]);
193
+ pageCache.set('/', JSON.stringify(createMockPageData()), lineMap);
194
+
195
+ const result = pageService.getLineMap('/');
196
+
197
+ expect(result).toBeDefined();
198
+ expect(result?.get('root')).toEqual({ startLine: 1, endLine: 10 });
199
+ });
200
+
201
+ test('should return undefined for non-existent page', () => {
202
+ const result = pageService.getLineMap('/non-existent');
203
+
204
+ expect(result).toBeUndefined();
205
+ });
206
+ });
207
+
208
+ describe('setProvider', () => {
209
+ test('should allow setting provider after construction', async () => {
210
+ const serviceWithoutProvider = new PageService(pageCache);
211
+ const newProvider = createTypedMockPageProvider({
212
+ '/': JSON.stringify(createMockPageData()),
213
+ });
214
+
215
+ serviceWithoutProvider.setProvider(newProvider);
216
+ await serviceWithoutProvider.loadAllPages();
217
+
218
+ expect(serviceWithoutProvider.getPage('/')).toBeDefined();
219
+ });
220
+ });
221
+
222
+ describe('getSlugMappings', () => {
223
+ test('should return slug mappings from pages with slugs', () => {
224
+ const pageWithSlugs = createMockPageData({
225
+ meta: { title: 'About', slugs: { en: 'about', pl: 'o-nas' } },
226
+ });
227
+ pageCache.set('/about', JSON.stringify(pageWithSlugs), new Map());
228
+
229
+ const mappings = pageService.getSlugMappings();
230
+
231
+ expect(mappings).toHaveLength(1);
232
+ expect(mappings[0].pageId).toBe('about');
233
+ expect(mappings[0].slugs).toEqual({ en: 'about', pl: 'o-nas' });
234
+ });
235
+
236
+ test('should create default slug for pages without explicit slugs', () => {
237
+ const pageWithoutSlugs = createMockPageData({ meta: { title: 'About' } });
238
+ pageCache.set('/about', JSON.stringify(pageWithoutSlugs), new Map());
239
+
240
+ const mappings = pageService.getSlugMappings();
241
+
242
+ expect(mappings).toHaveLength(1);
243
+ expect(mappings[0].pageId).toBe('about');
244
+ expect(mappings[0].slugs).toEqual({ _default: 'about' });
245
+ });
246
+
247
+ test('should handle index page correctly', () => {
248
+ const indexPage = createMockPageData({ meta: { title: 'Home' } });
249
+ pageCache.set('/', JSON.stringify(indexPage), new Map());
250
+
251
+ const mappings = pageService.getSlugMappings();
252
+
253
+ expect(mappings).toHaveLength(1);
254
+ expect(mappings[0].pageId).toBe('index');
255
+ expect(mappings[0].slugs).toEqual({ _default: '' });
256
+ });
257
+ });
258
+ });