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.
- package/bin/cli.ts +281 -0
- package/build-static.ts +298 -0
- package/bunfig.toml +39 -0
- package/entries/client-router.tsx +111 -0
- package/entries/server-router.tsx +71 -0
- package/lib/client/ClientInitializer.test.ts +9 -0
- package/lib/client/ClientInitializer.test.ts.skip +92 -0
- package/lib/client/ClientInitializer.ts +60 -0
- package/lib/client/ErrorBoundary.test.tsx +595 -0
- package/lib/client/ErrorBoundary.tsx +230 -0
- package/lib/client/componentRegistry.test.ts +165 -0
- package/lib/client/componentRegistry.ts +18 -0
- package/lib/client/contexts/ThemeContext.tsx +73 -0
- package/lib/client/core/ComponentBuilder.test.ts +677 -0
- package/lib/client/core/ComponentBuilder.ts +660 -0
- package/lib/client/core/ComponentRenderer.test.tsx +176 -0
- package/lib/client/core/ComponentRenderer.tsx +83 -0
- package/lib/client/core/cmsTemplateProcessor.ts +129 -0
- package/lib/client/elementRegistry.ts +81 -0
- package/lib/client/hmr/HMRManager.tsx +179 -0
- package/lib/client/hmr/index.ts +5 -0
- package/lib/client/hmrWebSocket.test.ts +9 -0
- package/lib/client/hmrWebSocket.ts +250 -0
- package/lib/client/hooks/useColorVariables.test.ts +166 -0
- package/lib/client/hooks/useColorVariables.ts +249 -0
- package/lib/client/hooks/usePropertyAutocomplete.test.ts +9 -0
- package/lib/client/hooks/usePropertyAutocomplete.ts +40 -0
- package/lib/client/hydration/HydrationUtils.test.ts +154 -0
- package/lib/client/hydration/HydrationUtils.ts +35 -0
- package/lib/client/i18nConfigService.test.ts +74 -0
- package/lib/client/i18nConfigService.ts +78 -0
- package/lib/client/index.ts +56 -0
- package/lib/client/navigation.test.ts +441 -0
- package/lib/client/navigation.ts +23 -0
- package/lib/client/responsiveStyleResolver.test.ts +491 -0
- package/lib/client/responsiveStyleResolver.ts +184 -0
- package/lib/client/routing/RouteLoader.test.ts +635 -0
- package/lib/client/routing/RouteLoader.ts +347 -0
- package/lib/client/routing/Router.tsx +382 -0
- package/lib/client/scripts/ScriptExecutor.test.ts +489 -0
- package/lib/client/scripts/ScriptExecutor.ts +171 -0
- package/lib/client/scripts/formHandler.ts +103 -0
- package/lib/client/styleProcessor.test.ts +126 -0
- package/lib/client/styleProcessor.ts +92 -0
- package/lib/client/styles/StyleInjector.test.ts +354 -0
- package/lib/client/styles/StyleInjector.ts +154 -0
- package/lib/client/templateEngine.test.ts +660 -0
- package/lib/client/templateEngine.ts +667 -0
- package/lib/client/theme.test.ts +173 -0
- package/lib/client/theme.ts +159 -0
- package/lib/client/utils/toast.ts +46 -0
- package/lib/server/createServer.ts +170 -0
- package/lib/server/cssGenerator.test.ts +172 -0
- package/lib/server/cssGenerator.ts +58 -0
- package/lib/server/fileWatcher.ts +134 -0
- package/lib/server/index.ts +55 -0
- package/lib/server/jsonLoader.test.ts +103 -0
- package/lib/server/jsonLoader.ts +350 -0
- package/lib/server/middleware/cors.test.ts +177 -0
- package/lib/server/middleware/cors.ts +69 -0
- package/lib/server/middleware/errorHandler.test.ts +208 -0
- package/lib/server/middleware/errorHandler.ts +63 -0
- package/lib/server/middleware/index.ts +9 -0
- package/lib/server/middleware/logger.test.ts +233 -0
- package/lib/server/middleware/logger.ts +99 -0
- package/lib/server/pageCache.test.ts +167 -0
- package/lib/server/pageCache.ts +97 -0
- package/lib/server/projectContext.ts +51 -0
- package/lib/server/providers/fileSystemCMSProvider.test.ts +292 -0
- package/lib/server/providers/fileSystemCMSProvider.ts +227 -0
- package/lib/server/providers/fileSystemPageProvider.ts +83 -0
- package/lib/server/routes/api/cms.test.ts +177 -0
- package/lib/server/routes/api/cms.ts +82 -0
- package/lib/server/routes/api/colors.ts +59 -0
- package/lib/server/routes/api/components.ts +70 -0
- package/lib/server/routes/api/config.test.ts +9 -0
- package/lib/server/routes/api/config.ts +28 -0
- package/lib/server/routes/api/core-routes.ts +182 -0
- package/lib/server/routes/api/functions.ts +170 -0
- package/lib/server/routes/api/index.ts +69 -0
- package/lib/server/routes/api/pages.ts +95 -0
- package/lib/server/routes/api/shared.test.ts +81 -0
- package/lib/server/routes/api/shared.ts +31 -0
- package/lib/server/routes/editor.test.ts +9 -0
- package/lib/server/routes/index.ts +104 -0
- package/lib/server/routes/pages.ts +161 -0
- package/lib/server/routes/static.ts +107 -0
- package/lib/server/services/ColorService.ts +193 -0
- package/lib/server/services/cmsService.test.ts +388 -0
- package/lib/server/services/cmsService.ts +296 -0
- package/lib/server/services/componentService.test.ts +276 -0
- package/lib/server/services/componentService.ts +346 -0
- package/lib/server/services/configService.ts +156 -0
- package/lib/server/services/fileWatcherService.ts +67 -0
- package/lib/server/services/index.ts +10 -0
- package/lib/server/services/pageService.test.ts +258 -0
- package/lib/server/services/pageService.ts +240 -0
- package/lib/server/ssrRenderer.test.ts +1005 -0
- package/lib/server/ssrRenderer.ts +878 -0
- package/lib/server/utilityClassGenerator.ts +11 -0
- package/lib/server/utils/index.ts +5 -0
- package/lib/server/utils/jsonLineMapper.test.ts +100 -0
- package/lib/server/utils/jsonLineMapper.ts +166 -0
- package/lib/server/validateStyleCoverage.test.ts +9 -0
- package/lib/server/validateStyleCoverage.ts +167 -0
- package/lib/server/websocketManager.test.ts +9 -0
- package/lib/server/websocketManager.ts +95 -0
- package/lib/shared/attributeNodeUtils.test.ts +152 -0
- package/lib/shared/attributeNodeUtils.ts +50 -0
- package/lib/shared/breakpoints.test.ts +166 -0
- package/lib/shared/breakpoints.ts +65 -0
- package/lib/shared/colorProperties.test.ts +111 -0
- package/lib/shared/colorProperties.ts +40 -0
- package/lib/shared/colorVariableUtils.test.ts +319 -0
- package/lib/shared/colorVariableUtils.ts +97 -0
- package/lib/shared/constants.test.ts +175 -0
- package/lib/shared/constants.ts +116 -0
- package/lib/shared/cssGeneration.ts +481 -0
- package/lib/shared/cssProperties.test.ts +252 -0
- package/lib/shared/cssProperties.ts +338 -0
- package/lib/shared/elementUtils.test.ts +245 -0
- package/lib/shared/elementUtils.ts +90 -0
- package/lib/shared/fontLoader.ts +97 -0
- package/lib/shared/i18n.test.ts +313 -0
- package/lib/shared/i18n.ts +286 -0
- package/lib/shared/index.ts +50 -0
- package/lib/shared/interfaces/contentProvider.test.ts +9 -0
- package/lib/shared/interfaces/contentProvider.ts +121 -0
- package/lib/shared/nodeUtils.test.ts +320 -0
- package/lib/shared/nodeUtils.ts +220 -0
- package/lib/shared/pathArrayUtils.test.ts +315 -0
- package/lib/shared/pathArrayUtils.ts +17 -0
- package/lib/shared/pathUtils.test.ts +260 -0
- package/lib/shared/pathUtils.ts +244 -0
- package/lib/shared/paths/Path.test.ts +74 -0
- package/lib/shared/paths/Path.ts +23 -0
- package/lib/shared/paths/PathConverter.test.ts +232 -0
- package/lib/shared/paths/PathConverter.ts +141 -0
- package/lib/shared/paths/PathUtils.ts +290 -0
- package/lib/shared/paths/PathValidator.test.ts +193 -0
- package/lib/shared/paths/PathValidator.ts +53 -0
- package/lib/shared/paths/index.ts +48 -0
- package/lib/shared/propResolver.test.ts +639 -0
- package/lib/shared/propResolver.ts +124 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +190 -0
- package/lib/shared/registry/BaseNodeTypeRegistry.ts +200 -0
- package/lib/shared/registry/ClientNodeTypeRegistry.ts +34 -0
- package/lib/shared/registry/ClientRegistry.test.ts +26 -0
- package/lib/shared/registry/ClientRegistry.ts +15 -0
- package/lib/shared/registry/ComponentRegistry.test.ts +293 -0
- package/lib/shared/registry/ComponentRegistry.ts +100 -0
- package/lib/shared/registry/NodeTypeDefinition.ts +198 -0
- package/lib/shared/registry/NodeTypeManager.ts +94 -0
- package/lib/shared/registry/RegistryManager.test.ts +58 -0
- package/lib/shared/registry/RegistryManager.ts +60 -0
- package/lib/shared/registry/SSRNodeTypeRegistry.ts +33 -0
- package/lib/shared/registry/SSRRegistry.test.ts +26 -0
- package/lib/shared/registry/SSRRegistry.ts +15 -0
- package/lib/shared/registry/createNodeType.ts +175 -0
- package/lib/shared/registry/defineNodeType.ts +73 -0
- package/lib/shared/registry/fieldPresets.ts +109 -0
- package/lib/shared/registry/index.ts +50 -0
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +71 -0
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +61 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +88 -0
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +66 -0
- package/lib/shared/registry/nodeTypes/ObjectLinkNodeType.ts +75 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +49 -0
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +52 -0
- package/lib/shared/registry/nodeTypes/index.ts +75 -0
- package/lib/shared/responsiveScaling.test.ts +268 -0
- package/lib/shared/responsiveScaling.ts +194 -0
- package/lib/shared/responsiveStyleUtils.test.ts +300 -0
- package/lib/shared/responsiveStyleUtils.ts +139 -0
- package/lib/shared/slugTranslator.test.ts +325 -0
- package/lib/shared/slugTranslator.ts +177 -0
- package/lib/shared/styleNodeUtils.test.ts +132 -0
- package/lib/shared/styleNodeUtils.ts +102 -0
- package/lib/shared/styleUtils.test.ts +238 -0
- package/lib/shared/styleUtils.ts +63 -0
- package/lib/shared/themeDefaults.test.ts +113 -0
- package/lib/shared/themeDefaults.ts +103 -0
- package/lib/shared/tree/PathBuilder.ts +383 -0
- package/lib/shared/treePathUtils.test.ts +539 -0
- package/lib/shared/treePathUtils.ts +339 -0
- package/lib/shared/types/api.ts +58 -0
- package/lib/shared/types/cms.ts +95 -0
- package/lib/shared/types/colors.ts +45 -0
- package/lib/shared/types/components.ts +121 -0
- package/lib/shared/types/errors.test.ts +103 -0
- package/lib/shared/types/errors.ts +69 -0
- package/lib/shared/types/index.ts +96 -0
- package/lib/shared/types/nodes.ts +20 -0
- package/lib/shared/types/rendering.ts +61 -0
- package/lib/shared/types/styles.ts +38 -0
- package/lib/shared/types.ts +11 -0
- package/lib/shared/utilityClassConfig.ts +287 -0
- package/lib/shared/utilityClassMapper.test.ts +140 -0
- package/lib/shared/utilityClassMapper.ts +229 -0
- package/lib/shared/utils/fileUtils.test.ts +99 -0
- package/lib/shared/utils/fileUtils.ts +56 -0
- package/lib/shared/utils.test.ts +261 -0
- package/lib/shared/utils.ts +84 -0
- package/lib/shared/validation/index.ts +7 -0
- package/lib/shared/validation/propValidator.test.ts +178 -0
- package/lib/shared/validation/propValidator.ts +238 -0
- package/lib/shared/validation/schemas.test.ts +177 -0
- package/lib/shared/validation/schemas.ts +401 -0
- package/lib/shared/validation/validators.test.ts +109 -0
- package/lib/shared/validation/validators.ts +304 -0
- package/lib/test-utils/dom-setup.ts +55 -0
- package/lib/test-utils/factories/ConsoleMockFactory.ts +200 -0
- package/lib/test-utils/factories/DomMockFactory.ts +487 -0
- package/lib/test-utils/factories/EventMockFactory.ts +244 -0
- package/lib/test-utils/factories/FetchMockFactory.ts +210 -0
- package/lib/test-utils/factories/ServerMockFactory.ts +223 -0
- package/lib/test-utils/factories/StoreMockFactory.ts +370 -0
- package/lib/test-utils/factories/index.ts +11 -0
- package/lib/test-utils/fixtures.ts +134 -0
- package/lib/test-utils/helpers/asyncHelpers.test.ts +112 -0
- package/lib/test-utils/helpers/asyncHelpers.ts +196 -0
- package/lib/test-utils/helpers/index.ts +6 -0
- package/lib/test-utils/helpers.test.ts +73 -0
- package/lib/test-utils/helpers.ts +90 -0
- package/lib/test-utils/index.ts +17 -0
- package/lib/test-utils/mockFactories.ts +92 -0
- package/lib/test-utils/mocks.ts +341 -0
- package/package.json +38 -0
- package/templates/index-router.html +34 -0
- package/tsconfig.json +14 -0
- package/vite.config.ts +43 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach } from "bun:test";
|
|
2
|
+
import { PageCache } from "./pageCache";
|
|
3
|
+
import type { LineMap } from "./utils/jsonLineMapper";
|
|
4
|
+
|
|
5
|
+
// Helper to create empty line map for tests
|
|
6
|
+
const emptyLineMap = (): LineMap => new Map();
|
|
7
|
+
|
|
8
|
+
describe("PageCache", () => {
|
|
9
|
+
let cache: PageCache;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
cache = new PageCache();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("set and get", () => {
|
|
16
|
+
test("should store and retrieve page content", () => {
|
|
17
|
+
cache.set("/", "home content", emptyLineMap());
|
|
18
|
+
const result = cache.getContent("/");
|
|
19
|
+
|
|
20
|
+
expect(result).toBe("home content");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("should return undefined for non-existent page", () => {
|
|
24
|
+
const result = cache.getContent("/nonexistent");
|
|
25
|
+
expect(result).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("should overwrite existing page content", () => {
|
|
29
|
+
cache.set("/about", "old content", emptyLineMap());
|
|
30
|
+
cache.set("/about", "new content", emptyLineMap());
|
|
31
|
+
|
|
32
|
+
const result = cache.getContent("/about");
|
|
33
|
+
expect(result).toBe("new content");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("has", () => {
|
|
38
|
+
test("should return true for cached page", () => {
|
|
39
|
+
cache.set("/about", "content", emptyLineMap());
|
|
40
|
+
expect(cache.has("/about")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("should return false for non-cached page", () => {
|
|
44
|
+
expect(cache.has("/nonexistent")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("delete", () => {
|
|
49
|
+
test("should delete cached page", () => {
|
|
50
|
+
cache.set("/temp", "content", emptyLineMap());
|
|
51
|
+
expect(cache.has("/temp")).toBe(true);
|
|
52
|
+
|
|
53
|
+
cache.delete("/temp");
|
|
54
|
+
expect(cache.has("/temp")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("should not throw when deleting non-existent page", () => {
|
|
58
|
+
expect(() => cache.delete("/nonexistent")).not.toThrow();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("keys", () => {
|
|
63
|
+
test("should return all cached page paths", () => {
|
|
64
|
+
cache.set("/", "home", emptyLineMap());
|
|
65
|
+
cache.set("/about", "about", emptyLineMap());
|
|
66
|
+
cache.set("/contact", "contact", emptyLineMap());
|
|
67
|
+
|
|
68
|
+
const keys = cache.keys();
|
|
69
|
+
|
|
70
|
+
expect(keys.length).toBe(3);
|
|
71
|
+
expect(keys).toContain("/");
|
|
72
|
+
expect(keys).toContain("/about");
|
|
73
|
+
expect(keys).toContain("/contact");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("should return empty array when cache is empty", () => {
|
|
77
|
+
const keys = cache.keys();
|
|
78
|
+
expect(keys).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("clear", () => {
|
|
83
|
+
test("should clear all cached pages", () => {
|
|
84
|
+
cache.set("/", "home", emptyLineMap());
|
|
85
|
+
cache.set("/about", "about", emptyLineMap());
|
|
86
|
+
|
|
87
|
+
expect(cache.size()).toBe(2);
|
|
88
|
+
|
|
89
|
+
cache.clear();
|
|
90
|
+
|
|
91
|
+
expect(cache.size()).toBe(0);
|
|
92
|
+
expect(cache.has("/")).toBe(false);
|
|
93
|
+
expect(cache.has("/about")).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("size", () => {
|
|
98
|
+
test("should return number of cached pages", () => {
|
|
99
|
+
expect(cache.size()).toBe(0);
|
|
100
|
+
|
|
101
|
+
cache.set("/", "home", emptyLineMap());
|
|
102
|
+
expect(cache.size()).toBe(1);
|
|
103
|
+
|
|
104
|
+
cache.set("/about", "about", emptyLineMap());
|
|
105
|
+
expect(cache.size()).toBe(2);
|
|
106
|
+
|
|
107
|
+
cache.delete("/");
|
|
108
|
+
expect(cache.size()).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("toObject", () => {
|
|
113
|
+
test("should convert cache to object", () => {
|
|
114
|
+
cache.set("/", "home content", emptyLineMap());
|
|
115
|
+
cache.set("/about", "about content", emptyLineMap());
|
|
116
|
+
|
|
117
|
+
const obj = cache.toObject();
|
|
118
|
+
|
|
119
|
+
expect(obj["/"]).toBe("home content");
|
|
120
|
+
expect(obj["/about"]).toBe("about content");
|
|
121
|
+
expect(Object.keys(obj).length).toBe(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("should return empty object when cache is empty", () => {
|
|
125
|
+
const obj = cache.toObject();
|
|
126
|
+
expect(obj).toEqual({});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("page path mapping", () => {
|
|
131
|
+
test("should handle index page as root path", () => {
|
|
132
|
+
cache.set("/", "home content", emptyLineMap());
|
|
133
|
+
expect(cache.getContent("/")).toBe("home content");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("should handle nested paths", () => {
|
|
137
|
+
cache.set("/blog/post-1", "blog post content", emptyLineMap());
|
|
138
|
+
expect(cache.getContent("/blog/post-1")).toBe("blog post content");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("line map", () => {
|
|
143
|
+
test("should store and retrieve line map", () => {
|
|
144
|
+
const lineMap: LineMap = new Map();
|
|
145
|
+
lineMap.set("0", { startLine: 31, endLine: 39 });
|
|
146
|
+
lineMap.set("1", { startLine: 40, endLine: 100 });
|
|
147
|
+
|
|
148
|
+
cache.set("/", "home content", lineMap);
|
|
149
|
+
const result = cache.getLineMap("/");
|
|
150
|
+
|
|
151
|
+
expect(result).toBeDefined();
|
|
152
|
+
expect(result?.get("0")).toEqual({ startLine: 31, endLine: 39 });
|
|
153
|
+
expect(result?.get("1")).toEqual({ startLine: 40, endLine: 100 });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("should get line range for specific element path", () => {
|
|
157
|
+
const lineMap: LineMap = new Map();
|
|
158
|
+
lineMap.set("0,1", { startLine: 50, endLine: 75 });
|
|
159
|
+
|
|
160
|
+
cache.set("/about", "about content", lineMap);
|
|
161
|
+
const range = cache.getLineRange("/about", "0,1");
|
|
162
|
+
|
|
163
|
+
expect(range).toEqual({ startLine: 50, endLine: 75 });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Cache
|
|
3
|
+
* Manages caching of page JSON content with line number tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LineMap, LineRange } from './utils/jsonLineMapper';
|
|
7
|
+
|
|
8
|
+
export interface CachedPage {
|
|
9
|
+
content: string;
|
|
10
|
+
lineMap: LineMap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class PageCache {
|
|
14
|
+
private cache = new Map<string, CachedPage>();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Store page content and line map in cache
|
|
18
|
+
*/
|
|
19
|
+
set(path: string, content: string, lineMap: LineMap): void {
|
|
20
|
+
this.cache.set(path, { content, lineMap });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get cached page data
|
|
25
|
+
*/
|
|
26
|
+
get(path: string): CachedPage | undefined {
|
|
27
|
+
return this.cache.get(path);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get page content from cache
|
|
32
|
+
*/
|
|
33
|
+
getContent(path: string): string | undefined {
|
|
34
|
+
return this.cache.get(path)?.content;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get line map from cache
|
|
39
|
+
*/
|
|
40
|
+
getLineMap(path: string): LineMap | undefined {
|
|
41
|
+
return this.cache.get(path)?.lineMap;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get line range for a specific element path
|
|
46
|
+
*/
|
|
47
|
+
getLineRange(pagePath: string, elementPath: string): LineRange | undefined {
|
|
48
|
+
return this.cache.get(pagePath)?.lineMap.get(elementPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Delete page from cache
|
|
53
|
+
*/
|
|
54
|
+
delete(path: string): void {
|
|
55
|
+
this.cache.delete(path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if page exists in cache
|
|
60
|
+
*/
|
|
61
|
+
has(path: string): boolean {
|
|
62
|
+
return this.cache.has(path);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get all page paths
|
|
67
|
+
*/
|
|
68
|
+
keys(): string[] {
|
|
69
|
+
return Array.from(this.cache.keys());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear all cached pages
|
|
74
|
+
*/
|
|
75
|
+
clear(): void {
|
|
76
|
+
this.cache.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get cache size
|
|
81
|
+
*/
|
|
82
|
+
size(): number {
|
|
83
|
+
return this.cache.size;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all pages content as object (without line maps)
|
|
88
|
+
*/
|
|
89
|
+
toObject(): Record<string, string> {
|
|
90
|
+
const obj: Record<string, string> = {};
|
|
91
|
+
this.cache.forEach((value, key) => {
|
|
92
|
+
obj[key] = value.content;
|
|
93
|
+
});
|
|
94
|
+
return obj;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { join, dirname } from 'path';
|
|
2
|
+
|
|
3
|
+
let projectRoot: string = process.cwd();
|
|
4
|
+
|
|
5
|
+
// Package root is where the editor files are installed (2 levels up from this file)
|
|
6
|
+
const packageRoot = join(dirname(import.meta.dir), '..');
|
|
7
|
+
|
|
8
|
+
export function setProjectRoot(root: string): void {
|
|
9
|
+
projectRoot = root;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getProjectRoot(): string {
|
|
13
|
+
return projectRoot;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getPackageRoot(): string {
|
|
17
|
+
return packageRoot;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Path getters for project files (user's project)
|
|
21
|
+
export const projectPaths = {
|
|
22
|
+
pages: () => join(projectRoot, 'pages'),
|
|
23
|
+
components: () => join(projectRoot, 'components'),
|
|
24
|
+
fonts: () => join(projectRoot, 'fonts'),
|
|
25
|
+
images: () => join(projectRoot, 'images'),
|
|
26
|
+
videos: () => join(projectRoot, 'videos'),
|
|
27
|
+
assets: () => join(projectRoot, 'assets'),
|
|
28
|
+
icons: () => join(projectRoot, 'icons'),
|
|
29
|
+
cms: () => join(projectRoot, 'cms'),
|
|
30
|
+
functions: () => join(projectRoot, 'functions'),
|
|
31
|
+
config: () => join(projectRoot, 'project.config.json'),
|
|
32
|
+
colors: () => join(projectRoot, 'colors.json'),
|
|
33
|
+
dist: () => join(projectRoot, 'dist'),
|
|
34
|
+
env: () => join(projectRoot, '.env'),
|
|
35
|
+
get project() { return projectRoot; }, // Direct access to project root
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Path getters for package files (core assets)
|
|
39
|
+
// Note: Editor-specific paths (editorHtml) are now in @meno/studio
|
|
40
|
+
export const packagePaths = {
|
|
41
|
+
indexHtml: () => join(packageRoot, 'templates', 'index-router.html'),
|
|
42
|
+
clientRouter: () => join(packageRoot, 'entries', 'client-router.tsx'),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function resolveProjectPath(...segments: string[]): string {
|
|
46
|
+
return join(projectRoot, ...segments);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolvePackagePath(...segments: string[]): string {
|
|
50
|
+
return join(packageRoot, ...segments);
|
|
51
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileSystemCMSProvider Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
6
|
+
import { FileSystemCMSProvider } from './fileSystemCMSProvider';
|
|
7
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
const TEST_DIR = '/tmp/cms-provider-test-' + Date.now();
|
|
11
|
+
const PAGES_DIR = join(TEST_DIR, 'pages');
|
|
12
|
+
const CMS_DIR = join(TEST_DIR, 'cms');
|
|
13
|
+
|
|
14
|
+
describe('FileSystemCMSProvider', () => {
|
|
15
|
+
let provider: FileSystemCMSProvider;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Create test directories
|
|
19
|
+
mkdirSync(PAGES_DIR, { recursive: true });
|
|
20
|
+
mkdirSync(join(CMS_DIR, 'blog-posts'), { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Create test page with embedded schema
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(PAGES_DIR, 'blog-post.json'),
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
meta: {
|
|
27
|
+
source: 'cms',
|
|
28
|
+
cms: {
|
|
29
|
+
id: 'blog-posts',
|
|
30
|
+
name: 'Blog Posts',
|
|
31
|
+
slugField: 'slug',
|
|
32
|
+
urlPattern: '/blog/{{slug}}',
|
|
33
|
+
fields: {
|
|
34
|
+
title: { type: 'string', required: true },
|
|
35
|
+
slug: { type: 'string', required: true },
|
|
36
|
+
content: { type: 'text' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
title: '{{cms.title}}'
|
|
40
|
+
},
|
|
41
|
+
root: { type: 'node', tag: 'div', children: [] }
|
|
42
|
+
}, null, 2)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Create static page (should be ignored)
|
|
46
|
+
writeFileSync(
|
|
47
|
+
join(PAGES_DIR, 'about.json'),
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
meta: { title: 'About' },
|
|
50
|
+
root: { type: 'node', tag: 'div', children: [] }
|
|
51
|
+
}, null, 2)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Create CMS items
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(CMS_DIR, 'blog-posts', 'hello-world.json'),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
_id: '1',
|
|
59
|
+
title: 'Hello World',
|
|
60
|
+
slug: 'hello-world',
|
|
61
|
+
content: 'First post content'
|
|
62
|
+
}, null, 2)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
writeFileSync(
|
|
66
|
+
join(CMS_DIR, 'blog-posts', 'getting-started.json'),
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
_id: '2',
|
|
69
|
+
title: 'Getting Started',
|
|
70
|
+
slug: 'getting-started',
|
|
71
|
+
content: 'Guide content'
|
|
72
|
+
}, null, 2)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
provider = new FileSystemCMSProvider(PAGES_DIR, CMS_DIR);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
// Clean up test directory
|
|
80
|
+
if (existsSync(TEST_DIR)) {
|
|
81
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('getAllSchemas', () => {
|
|
86
|
+
it('should extract schemas from CMS pages', async () => {
|
|
87
|
+
const schemas = await provider.getAllSchemas();
|
|
88
|
+
|
|
89
|
+
expect(schemas.size).toBe(1);
|
|
90
|
+
expect(schemas.has('blog-posts')).toBe(true);
|
|
91
|
+
|
|
92
|
+
const schemaInfo = schemas.get('blog-posts')!;
|
|
93
|
+
expect(schemaInfo.schema.id).toBe('blog-posts');
|
|
94
|
+
expect(schemaInfo.schema.name).toBe('Blog Posts');
|
|
95
|
+
expect(schemaInfo.schema.slugField).toBe('slug');
|
|
96
|
+
expect(schemaInfo.pagePath).toContain('blog-post.json');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should ignore static pages', async () => {
|
|
100
|
+
const schemas = await provider.getAllSchemas();
|
|
101
|
+
expect(schemas.has('about')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should cache schemas', async () => {
|
|
105
|
+
const schemas1 = await provider.getAllSchemas();
|
|
106
|
+
const schemas2 = await provider.getAllSchemas();
|
|
107
|
+
expect(schemas1).toBe(schemas2); // Same object reference
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return empty map for non-existent pages directory', async () => {
|
|
111
|
+
const emptyProvider = new FileSystemCMSProvider('/non-existent', CMS_DIR);
|
|
112
|
+
const schemas = await emptyProvider.getAllSchemas();
|
|
113
|
+
expect(schemas.size).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('getItems', () => {
|
|
118
|
+
it('should load all items for a collection', async () => {
|
|
119
|
+
const items = await provider.getItems('blog-posts');
|
|
120
|
+
|
|
121
|
+
expect(items).toHaveLength(2);
|
|
122
|
+
expect(items.some(i => i.slug === 'hello-world')).toBe(true);
|
|
123
|
+
expect(items.some(i => i.slug === 'getting-started')).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should add _slug from filename', async () => {
|
|
127
|
+
const items = await provider.getItems('blog-posts');
|
|
128
|
+
const helloWorld = items.find(i => i.slug === 'hello-world');
|
|
129
|
+
|
|
130
|
+
expect(helloWorld?._slug).toBe('hello-world');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return empty array for non-existent collection', async () => {
|
|
134
|
+
const items = await provider.getItems('unknown-collection');
|
|
135
|
+
expect(items).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('getItemBySlug', () => {
|
|
140
|
+
it('should load item by slug', async () => {
|
|
141
|
+
const item = await provider.getItemBySlug('blog-posts', 'hello-world');
|
|
142
|
+
|
|
143
|
+
expect(item).not.toBeNull();
|
|
144
|
+
expect(item?.title).toBe('Hello World');
|
|
145
|
+
expect(item?.slug).toBe('hello-world');
|
|
146
|
+
expect(item?._slug).toBe('hello-world');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return null for non-existent slug', async () => {
|
|
150
|
+
const item = await provider.getItemBySlug('blog-posts', 'non-existent');
|
|
151
|
+
expect(item).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should return null for non-existent collection', async () => {
|
|
155
|
+
const item = await provider.getItemBySlug('unknown', 'hello-world');
|
|
156
|
+
expect(item).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('getItemById', () => {
|
|
161
|
+
it('should load item by ID', async () => {
|
|
162
|
+
const item = await provider.getItemById('blog-posts', '1');
|
|
163
|
+
|
|
164
|
+
expect(item).not.toBeNull();
|
|
165
|
+
expect(item?.title).toBe('Hello World');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should return null for non-existent ID', async () => {
|
|
169
|
+
const item = await provider.getItemById('blog-posts', '999');
|
|
170
|
+
expect(item).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('saveItem', () => {
|
|
175
|
+
it('should save new item', async () => {
|
|
176
|
+
const newItem = {
|
|
177
|
+
_id: '3',
|
|
178
|
+
title: 'New Post',
|
|
179
|
+
slug: 'new-post',
|
|
180
|
+
content: 'New content',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await provider.saveItem('blog-posts', newItem);
|
|
184
|
+
|
|
185
|
+
const saved = await provider.getItemBySlug('blog-posts', 'new-post');
|
|
186
|
+
expect(saved).not.toBeNull();
|
|
187
|
+
expect(saved?.title).toBe('New Post');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should update existing item', async () => {
|
|
191
|
+
const item = await provider.getItemBySlug('blog-posts', 'hello-world');
|
|
192
|
+
const updated = { ...item!, title: 'Updated Title' };
|
|
193
|
+
|
|
194
|
+
await provider.saveItem('blog-posts', updated);
|
|
195
|
+
|
|
196
|
+
const saved = await provider.getItemBySlug('blog-posts', 'hello-world');
|
|
197
|
+
expect(saved?.title).toBe('Updated Title');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should create collection directory if not exists', async () => {
|
|
201
|
+
const newItem = {
|
|
202
|
+
_id: '1',
|
|
203
|
+
name: 'Product',
|
|
204
|
+
slug: 'test-product',
|
|
205
|
+
price: 99,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Add products schema
|
|
209
|
+
writeFileSync(
|
|
210
|
+
join(PAGES_DIR, 'product.json'),
|
|
211
|
+
JSON.stringify({
|
|
212
|
+
meta: {
|
|
213
|
+
source: 'cms',
|
|
214
|
+
cms: {
|
|
215
|
+
id: 'products',
|
|
216
|
+
name: 'Products',
|
|
217
|
+
slugField: 'slug',
|
|
218
|
+
urlPattern: '/shop/{{slug}}',
|
|
219
|
+
fields: {
|
|
220
|
+
name: { type: 'string' },
|
|
221
|
+
slug: { type: 'string' },
|
|
222
|
+
price: { type: 'number' },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
root: { type: 'node', tag: 'div', children: [] }
|
|
227
|
+
}, null, 2)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
provider.clearSchemaCache();
|
|
231
|
+
await provider.saveItem('products', newItem);
|
|
232
|
+
|
|
233
|
+
const saved = await provider.getItemBySlug('products', 'test-product');
|
|
234
|
+
expect(saved?.name).toBe('Product');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should throw for unknown collection', async () => {
|
|
238
|
+
const item = { _id: '1', slug: 'test' };
|
|
239
|
+
await expect(provider.saveItem('unknown', item)).rejects.toThrow('Unknown collection');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('deleteItem', () => {
|
|
244
|
+
it('should delete item by slug', async () => {
|
|
245
|
+
const before = await provider.getItemBySlug('blog-posts', 'hello-world');
|
|
246
|
+
expect(before).not.toBeNull();
|
|
247
|
+
|
|
248
|
+
await provider.deleteItem('blog-posts', 'hello-world');
|
|
249
|
+
|
|
250
|
+
const after = await provider.getItemBySlug('blog-posts', 'hello-world');
|
|
251
|
+
expect(after).toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should not throw for non-existent item', async () => {
|
|
255
|
+
await expect(provider.deleteItem('blog-posts', 'non-existent')).resolves.toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('clearSchemaCache', () => {
|
|
260
|
+
it('should clear cached schemas', async () => {
|
|
261
|
+
const schemas1 = await provider.getAllSchemas();
|
|
262
|
+
expect(schemas1.size).toBe(1);
|
|
263
|
+
|
|
264
|
+
// Add new page with CMS
|
|
265
|
+
writeFileSync(
|
|
266
|
+
join(PAGES_DIR, 'product.json'),
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
meta: {
|
|
269
|
+
source: 'cms',
|
|
270
|
+
cms: {
|
|
271
|
+
id: 'products',
|
|
272
|
+
name: 'Products',
|
|
273
|
+
slugField: 'slug',
|
|
274
|
+
urlPattern: '/shop/{{slug}}',
|
|
275
|
+
fields: {},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
root: { type: 'node', tag: 'div', children: [] }
|
|
279
|
+
}, null, 2)
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Still cached
|
|
283
|
+
const schemas2 = await provider.getAllSchemas();
|
|
284
|
+
expect(schemas2.size).toBe(1);
|
|
285
|
+
|
|
286
|
+
// Clear and reload
|
|
287
|
+
provider.clearSchemaCache();
|
|
288
|
+
const schemas3 = await provider.getAllSchemas();
|
|
289
|
+
expect(schemas3.size).toBe(2);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|