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,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File System CMS Provider
|
|
3
|
+
* Implements CMSProvider for loading CMS data from the file system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync, mkdirSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import type { CMSProvider, CMSSchemaInfo } from '../../shared/interfaces/contentProvider';
|
|
9
|
+
import type { CMSSchema, CMSItem } from '../../shared/types';
|
|
10
|
+
import { validateCMSItem } from '../../shared/validation/validators';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load JSON file content from disk
|
|
14
|
+
*/
|
|
15
|
+
async function loadJSONFile(filePath: string): Promise<unknown | null> {
|
|
16
|
+
try {
|
|
17
|
+
const file = Bun.file(filePath);
|
|
18
|
+
if (await file.exists()) {
|
|
19
|
+
return await file.json();
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* FileSystemCMSProvider
|
|
29
|
+
* Loads CMS schemas from page files and CMS items from cms/<collection>/<slug>.json
|
|
30
|
+
*/
|
|
31
|
+
export class FileSystemCMSProvider implements CMSProvider {
|
|
32
|
+
private schemaCache: Map<string, CMSSchemaInfo> | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private pagesDir: string, // e.g., './pages' - schemas extracted from here
|
|
36
|
+
private cmsDir: string // e.g., './cms' - items stored here
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load all CMS schemas from page files with source: 'cms'
|
|
41
|
+
*/
|
|
42
|
+
async getAllSchemas(): Promise<Map<string, CMSSchemaInfo>> {
|
|
43
|
+
// Return cached if available
|
|
44
|
+
if (this.schemaCache) {
|
|
45
|
+
return this.schemaCache;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const schemas = new Map<string, CMSSchemaInfo>();
|
|
49
|
+
|
|
50
|
+
if (!existsSync(this.pagesDir)) {
|
|
51
|
+
return schemas;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const files = readdirSync(this.pagesDir);
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
if (!file.endsWith('.json')) continue;
|
|
57
|
+
|
|
58
|
+
const filePath = join(this.pagesDir, file);
|
|
59
|
+
const content = await loadJSONFile(filePath);
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
content &&
|
|
63
|
+
typeof content === 'object' &&
|
|
64
|
+
'meta' in content &&
|
|
65
|
+
typeof (content as Record<string, unknown>).meta === 'object'
|
|
66
|
+
) {
|
|
67
|
+
const meta = (content as Record<string, unknown>).meta as Record<string, unknown>;
|
|
68
|
+
|
|
69
|
+
if (meta.source === 'cms' && meta.cms) {
|
|
70
|
+
const schema = meta.cms as CMSSchema;
|
|
71
|
+
schemas.set(schema.id, {
|
|
72
|
+
schema,
|
|
73
|
+
pagePath: filePath,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.schemaCache = schemas;
|
|
80
|
+
return schemas;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load all items for a collection
|
|
85
|
+
*/
|
|
86
|
+
async getItems(collection: string): Promise<CMSItem[]> {
|
|
87
|
+
const collectionDir = join(this.cmsDir, collection);
|
|
88
|
+
|
|
89
|
+
if (!existsSync(collectionDir)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const items: CMSItem[] = [];
|
|
94
|
+
const files = readdirSync(collectionDir);
|
|
95
|
+
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
if (!file.endsWith('.json')) continue;
|
|
98
|
+
|
|
99
|
+
const filePath = join(collectionDir, file);
|
|
100
|
+
const content = await loadJSONFile(filePath);
|
|
101
|
+
|
|
102
|
+
if (content && typeof content === 'object') {
|
|
103
|
+
const slug = file.replace('.json', '');
|
|
104
|
+
const item = { ...content as CMSItem, _slug: slug };
|
|
105
|
+
|
|
106
|
+
const validation = validateCMSItem(item);
|
|
107
|
+
if (validation.valid) {
|
|
108
|
+
items.push(validation.data);
|
|
109
|
+
} else {
|
|
110
|
+
console.warn(`Invalid CMS item at ${filePath}:`, validation.errors);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return items;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get single item by slug
|
|
120
|
+
*/
|
|
121
|
+
async getItemBySlug(collection: string, slug: string): Promise<CMSItem | null> {
|
|
122
|
+
const filePath = join(this.cmsDir, collection, `${slug}.json`);
|
|
123
|
+
const content = await loadJSONFile(filePath);
|
|
124
|
+
|
|
125
|
+
if (!content || typeof content !== 'object') {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const item = { ...content as CMSItem, _slug: slug };
|
|
130
|
+
const validation = validateCMSItem(item);
|
|
131
|
+
|
|
132
|
+
if (validation.valid) {
|
|
133
|
+
return validation.data;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.warn(`Invalid CMS item at ${filePath}:`, validation.errors);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get single item by ID
|
|
142
|
+
*/
|
|
143
|
+
async getItemById(collection: string, id: string): Promise<CMSItem | null> {
|
|
144
|
+
const items = await this.getItems(collection);
|
|
145
|
+
return items.find(item => item._id === id) || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Save item to file system
|
|
150
|
+
*/
|
|
151
|
+
async saveItem(collection: string, item: CMSItem): Promise<void> {
|
|
152
|
+
const { writeFile } = await import('fs/promises');
|
|
153
|
+
|
|
154
|
+
// Get schema to determine slug field
|
|
155
|
+
const schemas = await this.getAllSchemas();
|
|
156
|
+
const schemaInfo = schemas.get(collection);
|
|
157
|
+
|
|
158
|
+
if (!schemaInfo) {
|
|
159
|
+
throw new Error(`Unknown collection: ${collection}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const slugField = schemaInfo.schema.slugField;
|
|
163
|
+
const slug = String(item[slugField]);
|
|
164
|
+
|
|
165
|
+
if (!slug) {
|
|
166
|
+
throw new Error(`Missing slug field: ${slugField}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Ensure collection directory exists
|
|
170
|
+
const collectionDir = join(this.cmsDir, collection);
|
|
171
|
+
if (!existsSync(collectionDir)) {
|
|
172
|
+
mkdirSync(collectionDir, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Remove internal fields before saving
|
|
176
|
+
const { _slug, ...itemData } = item;
|
|
177
|
+
|
|
178
|
+
const filePath = join(collectionDir, `${slug}.json`);
|
|
179
|
+
await writeFile(filePath, JSON.stringify(itemData, null, 2), 'utf-8');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Delete item by slug
|
|
184
|
+
*/
|
|
185
|
+
async deleteItem(collection: string, slug: string): Promise<void> {
|
|
186
|
+
const { unlink } = await import('fs/promises');
|
|
187
|
+
const filePath = join(this.cmsDir, collection, `${slug}.json`);
|
|
188
|
+
|
|
189
|
+
if (existsSync(filePath)) {
|
|
190
|
+
await unlink(filePath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clear schema cache (useful when pages are modified)
|
|
196
|
+
*/
|
|
197
|
+
clearSchemaCache(): void {
|
|
198
|
+
this.schemaCache = null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Save a new collection schema by creating a page file
|
|
203
|
+
*/
|
|
204
|
+
async saveSchema(collectionId: string, pageData: unknown): Promise<void> {
|
|
205
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
206
|
+
|
|
207
|
+
// Create page file path (use collection ID as filename)
|
|
208
|
+
const pageFilePath = join(this.pagesDir, `${collectionId}.json`);
|
|
209
|
+
|
|
210
|
+
// Check if file already exists
|
|
211
|
+
if (existsSync(pageFilePath)) {
|
|
212
|
+
throw new Error(`Page file already exists: ${collectionId}.json`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Write the page file
|
|
216
|
+
await writeFile(pageFilePath, JSON.stringify(pageData, null, 2), 'utf-8');
|
|
217
|
+
|
|
218
|
+
// Create the CMS collection directory
|
|
219
|
+
const collectionDir = join(this.cmsDir, collectionId);
|
|
220
|
+
if (!existsSync(collectionDir)) {
|
|
221
|
+
await mkdir(collectionDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Clear the schema cache so the new collection is detected
|
|
225
|
+
this.clearSchemaCache();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File System Page Provider
|
|
3
|
+
* Implements PageProvider for loading pages from the file system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readdirSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import type { PageProvider } from '../../shared/interfaces/contentProvider';
|
|
9
|
+
import { isJSONFile, stripExtension, mapPageNameToPath, mapPathToPageName } from '../../shared/utils/fileUtils';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load JSON file content from disk
|
|
13
|
+
*/
|
|
14
|
+
async function loadJSONFile(filePath: string): Promise<string | null> {
|
|
15
|
+
try {
|
|
16
|
+
const file = Bun.file(filePath);
|
|
17
|
+
if (await file.exists()) {
|
|
18
|
+
return await file.text();
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* FileSystemPageProvider
|
|
28
|
+
* Loads and saves page JSON files from/to the file system
|
|
29
|
+
*/
|
|
30
|
+
export class FileSystemPageProvider implements PageProvider {
|
|
31
|
+
constructor(private pagesDir: string) {}
|
|
32
|
+
|
|
33
|
+
async loadAll(): Promise<Map<string, string>> {
|
|
34
|
+
const pages = new Map<string, string>();
|
|
35
|
+
|
|
36
|
+
if (!existsSync(this.pagesDir)) {
|
|
37
|
+
return pages;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const files = readdirSync(this.pagesDir);
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
if (isJSONFile(file)) {
|
|
43
|
+
const pageName = stripExtension(file);
|
|
44
|
+
const content = await loadJSONFile(join(this.pagesDir, file));
|
|
45
|
+
if (content) {
|
|
46
|
+
const pagePath = mapPageNameToPath(pageName);
|
|
47
|
+
pages.set(pagePath, content);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return pages;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async get(path: string): Promise<string | null> {
|
|
56
|
+
const pageName = mapPathToPageName(path);
|
|
57
|
+
const filePath = join(this.pagesDir, `${pageName}.json`);
|
|
58
|
+
return loadJSONFile(filePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async save(path: string, content: string): Promise<void> {
|
|
62
|
+
const { writeFile } = await import('fs/promises');
|
|
63
|
+
const pageName = mapPathToPageName(path);
|
|
64
|
+
const filePath = join(this.pagesDir, `${pageName}.json`);
|
|
65
|
+
await writeFile(filePath, content, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async delete(path: string): Promise<void> {
|
|
69
|
+
const { unlink } = await import('fs/promises');
|
|
70
|
+
const pageName = mapPathToPageName(path);
|
|
71
|
+
const filePath = join(this.pagesDir, `${pageName}.json`);
|
|
72
|
+
|
|
73
|
+
if (existsSync(filePath)) {
|
|
74
|
+
await unlink(filePath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async exists(path: string): Promise<boolean> {
|
|
79
|
+
const pageName = mapPathToPageName(path);
|
|
80
|
+
const filePath = join(this.pagesDir, `${pageName}.json`);
|
|
81
|
+
return existsSync(filePath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS API Routes Tests
|
|
3
|
+
* Tests for CMS CRUD endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach } from 'bun:test';
|
|
7
|
+
import type { CMSProvider, CMSSchemaInfo } from '../../../shared/interfaces/contentProvider';
|
|
8
|
+
import type { CMSItem, CMSSchema } from '../../../shared/types';
|
|
9
|
+
import { CMSService } from '../../services/cmsService';
|
|
10
|
+
import {
|
|
11
|
+
handleCollectionsRoute,
|
|
12
|
+
handleCollectionSchemaRoute,
|
|
13
|
+
handleItemsRoute,
|
|
14
|
+
handleItemRoute,
|
|
15
|
+
} from './cms';
|
|
16
|
+
|
|
17
|
+
// Note: Write handler tests (handleCreateItemRoute, handleUpdateItemRoute, handleDeleteItemRoute)
|
|
18
|
+
// moved to @meno/studio
|
|
19
|
+
|
|
20
|
+
// Mock schema info
|
|
21
|
+
const mockBlogSchema: CMSSchema = {
|
|
22
|
+
id: 'blog-posts',
|
|
23
|
+
name: 'Blog Posts',
|
|
24
|
+
slugField: 'slug',
|
|
25
|
+
urlPattern: '/blog/{{slug}}',
|
|
26
|
+
fields: {
|
|
27
|
+
title: { type: 'string', required: true, label: 'Title' },
|
|
28
|
+
slug: { type: 'string', required: true, label: 'Slug' },
|
|
29
|
+
content: { type: 'text', label: 'Content' },
|
|
30
|
+
featured: { type: 'boolean', label: 'Featured' },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const mockProductSchema: CMSSchema = {
|
|
35
|
+
id: 'products',
|
|
36
|
+
name: 'Products',
|
|
37
|
+
slugField: 'slug',
|
|
38
|
+
urlPattern: '/shop/{{slug}}',
|
|
39
|
+
fields: {
|
|
40
|
+
name: { type: 'string', required: true },
|
|
41
|
+
slug: { type: 'string', required: true },
|
|
42
|
+
price: { type: 'number' },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const mockSchemaInfo: CMSSchemaInfo = {
|
|
47
|
+
schema: mockBlogSchema,
|
|
48
|
+
pagePath: '/pages/blog-post.json',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const mockProductSchemaInfo: CMSSchemaInfo = {
|
|
52
|
+
schema: mockProductSchema,
|
|
53
|
+
pagePath: '/pages/product.json',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const mockBlogItems: CMSItem[] = [
|
|
57
|
+
{ _id: '1', _slug: 'hello-world', slug: 'hello-world', title: 'Hello World', content: 'First post', featured: true },
|
|
58
|
+
{ _id: '2', _slug: 'getting-started', slug: 'getting-started', title: 'Getting Started', content: 'Guide', featured: false },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
describe('CMS API Routes - Read Operations', () => {
|
|
62
|
+
let cmsService: CMSService;
|
|
63
|
+
let mockProvider: CMSProvider;
|
|
64
|
+
|
|
65
|
+
beforeEach(async () => {
|
|
66
|
+
mockProvider = {
|
|
67
|
+
getAllSchemas: async () => new Map([
|
|
68
|
+
['blog-posts', mockSchemaInfo],
|
|
69
|
+
['products', mockProductSchemaInfo],
|
|
70
|
+
]),
|
|
71
|
+
getItems: async (collection: string) => {
|
|
72
|
+
if (collection === 'blog-posts') return [...mockBlogItems];
|
|
73
|
+
return [];
|
|
74
|
+
},
|
|
75
|
+
getItemBySlug: async (collection: string, slug: string) => {
|
|
76
|
+
if (collection === 'blog-posts') {
|
|
77
|
+
return mockBlogItems.find(i => i._slug === slug) || null;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
},
|
|
81
|
+
getItemById: async (collection: string, id: string) => {
|
|
82
|
+
if (collection === 'blog-posts') {
|
|
83
|
+
return mockBlogItems.find(i => i._id === id) || null;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
},
|
|
87
|
+
saveItem: async () => {},
|
|
88
|
+
deleteItem: async () => {},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
cmsService = new CMSService(mockProvider);
|
|
92
|
+
await cmsService.initialize();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('GET /api/cms/collections', () => {
|
|
96
|
+
it('should return all collections with schemas', async () => {
|
|
97
|
+
const response = handleCollectionsRoute(cmsService);
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
|
|
100
|
+
expect(response.status).toBe(200);
|
|
101
|
+
expect(data.collections).toHaveLength(2);
|
|
102
|
+
expect(data.collections[0].id).toBe('blog-posts');
|
|
103
|
+
expect(data.collections[0].name).toBe('Blog Posts');
|
|
104
|
+
expect(data.collections[1].id).toBe('products');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should include field definitions in schema', async () => {
|
|
108
|
+
const response = handleCollectionsRoute(cmsService);
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
|
|
111
|
+
const blogCollection = data.collections.find((c: any) => c.id === 'blog-posts');
|
|
112
|
+
expect(blogCollection.fields).toBeDefined();
|
|
113
|
+
expect(blogCollection.fields.title.type).toBe('string');
|
|
114
|
+
expect(blogCollection.fields.title.required).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('GET /api/cms/collections/:collection', () => {
|
|
119
|
+
it('should return schema for specific collection', async () => {
|
|
120
|
+
const response = handleCollectionSchemaRoute('blog-posts', cmsService);
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
|
|
123
|
+
expect(response.status).toBe(200);
|
|
124
|
+
expect(data.schema.id).toBe('blog-posts');
|
|
125
|
+
expect(data.schema.name).toBe('Blog Posts');
|
|
126
|
+
expect(data.schema.slugField).toBe('slug');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return 404 for unknown collection', async () => {
|
|
130
|
+
const response = handleCollectionSchemaRoute('unknown', cmsService);
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
|
|
133
|
+
expect(response.status).toBe(404);
|
|
134
|
+
expect(data.error).toBe('Collection not found');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('GET /api/cms/:collection', () => {
|
|
139
|
+
it('should return all items in collection', async () => {
|
|
140
|
+
const response = await handleItemsRoute('blog-posts', cmsService);
|
|
141
|
+
const data = await response.json();
|
|
142
|
+
|
|
143
|
+
expect(response.status).toBe(200);
|
|
144
|
+
expect(data.items).toHaveLength(2);
|
|
145
|
+
expect(data.items[0].title).toBe('Hello World');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return empty array for empty collection', async () => {
|
|
149
|
+
const response = await handleItemsRoute('products', cmsService);
|
|
150
|
+
const data = await response.json();
|
|
151
|
+
|
|
152
|
+
expect(response.status).toBe(200);
|
|
153
|
+
expect(data.items).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('GET /api/cms/:collection/:slug', () => {
|
|
158
|
+
it('should return specific item by slug', async () => {
|
|
159
|
+
const response = await handleItemRoute('blog-posts', 'hello-world', cmsService);
|
|
160
|
+
const data = await response.json();
|
|
161
|
+
|
|
162
|
+
expect(response.status).toBe(200);
|
|
163
|
+
expect(data.item.title).toBe('Hello World');
|
|
164
|
+
expect(data.item._slug).toBe('hello-world');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should return 404 for non-existent item', async () => {
|
|
168
|
+
const response = await handleItemRoute('blog-posts', 'non-existent', cmsService);
|
|
169
|
+
const data = await response.json();
|
|
170
|
+
|
|
171
|
+
expect(response.status).toBe(404);
|
|
172
|
+
expect(data.error).toBe('Item not found');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Note: POST/PUT/DELETE tests moved to @meno/studio/lib/server/routes/api/cms.test.ts
|
|
177
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS API Routes
|
|
3
|
+
* Handles CMS content CRUD endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CMSService } from '../../services/cmsService';
|
|
7
|
+
import type { CMSSchema } from '../../../shared/types';
|
|
8
|
+
import { jsonResponse, errorResponse } from './shared';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /api/cms/collections - List all collections with their schemas
|
|
12
|
+
*/
|
|
13
|
+
export function handleCollectionsRoute(cmsService: CMSService): Response {
|
|
14
|
+
const schemas = cmsService.getAllSchemas();
|
|
15
|
+
const collections: CMSSchema[] = [];
|
|
16
|
+
|
|
17
|
+
for (const [, schemaInfo] of schemas) {
|
|
18
|
+
collections.push(schemaInfo.schema);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return jsonResponse({ collections });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GET /api/cms/collections/:collection - Get schema for specific collection
|
|
26
|
+
*/
|
|
27
|
+
export function handleCollectionSchemaRoute(
|
|
28
|
+
collection: string,
|
|
29
|
+
cmsService: CMSService
|
|
30
|
+
): Response {
|
|
31
|
+
const schema = cmsService.getSchema(collection);
|
|
32
|
+
|
|
33
|
+
if (!schema) {
|
|
34
|
+
return errorResponse('Collection not found', 404);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return jsonResponse({ schema });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Note: handleCreateCollectionRoute moved to @meno/studio
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* GET /api/cms/:collection - List all items in a collection
|
|
44
|
+
*/
|
|
45
|
+
export async function handleItemsRoute(
|
|
46
|
+
collection: string,
|
|
47
|
+
cmsService: CMSService
|
|
48
|
+
): Promise<Response> {
|
|
49
|
+
const items = await cmsService.queryItems({ collection });
|
|
50
|
+
return jsonResponse({ items });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* GET /api/cms/:collection/:slug - Get specific item by slug
|
|
55
|
+
*/
|
|
56
|
+
export async function handleItemRoute(
|
|
57
|
+
collection: string,
|
|
58
|
+
slug: string,
|
|
59
|
+
cmsService: CMSService
|
|
60
|
+
): Promise<Response> {
|
|
61
|
+
const match = await cmsService.matchRoute(
|
|
62
|
+
cmsService.getSchema(collection)?.urlPattern.replace('{{slug}}', slug) || ''
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// If no match via route, try direct provider access
|
|
66
|
+
if (!match) {
|
|
67
|
+
// For direct item access, we need to check if item exists
|
|
68
|
+
const items = await cmsService.queryItems({ collection });
|
|
69
|
+
const item = items.find(i => i._slug === slug || i[cmsService.getSchema(collection)?.slugField || 'slug'] === slug);
|
|
70
|
+
|
|
71
|
+
if (!item) {
|
|
72
|
+
return errorResponse('Item not found', 404);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return jsonResponse({ item });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return jsonResponse({ item: match.item });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Note: Write handlers (handleCreateItemRoute, handleUpdateItemRoute, handleDeleteItemRoute)
|
|
82
|
+
// moved to @meno/studio
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colors API Routes
|
|
3
|
+
* Handles color variables endpoints with theme support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { colorService } from '../../services/ColorService';
|
|
7
|
+
import { jsonResponse } from './shared';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Handle colors API endpoint - GET /api/colors?theme=<themeName>
|
|
11
|
+
*/
|
|
12
|
+
export async function handleColorsRoute(url?: URL): Promise<Response> {
|
|
13
|
+
try {
|
|
14
|
+
const themeName = url?.searchParams.get('theme') || undefined;
|
|
15
|
+
const colors = await colorService.getThemeColors(themeName);
|
|
16
|
+
return jsonResponse(colors);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error loading colors:', error);
|
|
19
|
+
return jsonResponse(
|
|
20
|
+
{ error: 'Failed to load colors' },
|
|
21
|
+
{ status: 500 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Handle themes API endpoint - GET /api/themes
|
|
28
|
+
*/
|
|
29
|
+
export async function handleThemesRoute(): Promise<Response> {
|
|
30
|
+
try {
|
|
31
|
+
const themes = await colorService.getAvailableThemes();
|
|
32
|
+
const defaultTheme = await colorService.getDefaultTheme();
|
|
33
|
+
return jsonResponse({ themes, default: defaultTheme });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error loading themes:', error);
|
|
36
|
+
return jsonResponse(
|
|
37
|
+
{ error: 'Failed to load themes' },
|
|
38
|
+
{ status: 500 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handle full colors config API endpoint - GET /api/colors-config
|
|
45
|
+
*/
|
|
46
|
+
export async function handleColorsConfigRoute(): Promise<Response> {
|
|
47
|
+
try {
|
|
48
|
+
const config = await colorService.getFullConfig();
|
|
49
|
+
return jsonResponse(config);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Error loading colors config:', error);
|
|
52
|
+
return jsonResponse(
|
|
53
|
+
{ error: 'Failed to load colors config' },
|
|
54
|
+
{ status: 500 }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Note: handleSaveColorsRoute moved to @meno/studio
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Components API Routes
|
|
3
|
+
* Handles component CRUD endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentService } from '../../services/componentService';
|
|
7
|
+
import { createCorsHeaders } from '../../middleware/cors';
|
|
8
|
+
import { jsonResponse } from './shared';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handle components API endpoint - GET /api/components
|
|
12
|
+
*/
|
|
13
|
+
export function handleComponentsRoute(componentService: ComponentService): Response {
|
|
14
|
+
const components = componentService.getAllComponents();
|
|
15
|
+
return jsonResponse(components);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handle component data API endpoint - GET /api/component-data/:name
|
|
20
|
+
* Validates that component has structure before returning
|
|
21
|
+
*/
|
|
22
|
+
export function handleComponentDataRoute(
|
|
23
|
+
url: URL,
|
|
24
|
+
componentService: ComponentService
|
|
25
|
+
): Response {
|
|
26
|
+
const componentName = url.pathname.replace('/api/component-data/', '');
|
|
27
|
+
const componentDef = componentService.getComponent(componentName);
|
|
28
|
+
|
|
29
|
+
if (!componentDef) {
|
|
30
|
+
return jsonResponse({ error: 'Component not found' }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Debug: Log what we're about to return
|
|
34
|
+
|
|
35
|
+
// Validate that component has structure
|
|
36
|
+
if (!componentService.validateComponentStructure(componentDef)) {
|
|
37
|
+
return jsonResponse(
|
|
38
|
+
{
|
|
39
|
+
error: 'Component structure is missing',
|
|
40
|
+
message: `Component "${componentName}" does not have a valid structure definition. Please add a structure field to the component definition.`
|
|
41
|
+
},
|
|
42
|
+
{ status: 400 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return jsonResponse(componentDef);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Handle component JavaScript API endpoint - GET /api/component-js/:name
|
|
51
|
+
*/
|
|
52
|
+
export async function handleComponentJavaScriptRoute(
|
|
53
|
+
url: URL,
|
|
54
|
+
componentService: ComponentService
|
|
55
|
+
): Promise<Response> {
|
|
56
|
+
const componentName = url.pathname.replace('/api/component-js/', '');
|
|
57
|
+
const jsContent = await componentService.getComponentJavaScript(componentName);
|
|
58
|
+
const corsHeaders = createCorsHeaders();
|
|
59
|
+
|
|
60
|
+
return new Response(jsContent || '', {
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'text/javascript',
|
|
63
|
+
...corsHeaders,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Note: Write handlers (handleSaveComponentRoute, handleSaveComponentJavaScriptRoute,
|
|
69
|
+
// handleSaveComponentCSSRoute, handleComponentCategoryRoute) moved to @meno/studio
|
|
70
|
+
|