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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prop resolution utility
|
|
3
|
+
* Resolves component props by applying defaults from interface definitions
|
|
4
|
+
* Used by both client (React) and server (SSR) rendering
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { StructuredComponentDefinition, ComponentNode, PropDefinition, I18nConfig } from './types';
|
|
8
|
+
import { validateComponentProps } from './validation/propValidator';
|
|
9
|
+
import { resolveI18nInProps } from './i18n';
|
|
10
|
+
|
|
11
|
+
export interface ResolvePropsOptions {
|
|
12
|
+
propDefs: Record<string, PropDefinition>;
|
|
13
|
+
passedProps: Record<string, unknown>;
|
|
14
|
+
children: Array<ComponentNode | string> | string | ComponentNode | null | undefined;
|
|
15
|
+
locale?: string;
|
|
16
|
+
i18nConfig?: I18nConfig;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves component props by applying defaults and merging with passed props
|
|
21
|
+
* Now includes validation for type safety
|
|
22
|
+
*
|
|
23
|
+
* @param options - Object containing prop definitions, passed props, and children
|
|
24
|
+
* @returns Resolved props object with defaults applied and validated
|
|
25
|
+
*/
|
|
26
|
+
export function resolveComponentProps(options: ResolvePropsOptions): Record<string, unknown> {
|
|
27
|
+
const { propDefs, passedProps, children = [], locale, i18nConfig } = options;
|
|
28
|
+
|
|
29
|
+
// Validate props against interface definition
|
|
30
|
+
// This provides runtime type checking while maintaining backward compatibility
|
|
31
|
+
try {
|
|
32
|
+
const validationResult = validateComponentProps(propDefs, passedProps);
|
|
33
|
+
|
|
34
|
+
if (!validationResult.valid && validationResult.errors.length > 0) {
|
|
35
|
+
// Log validation errors but continue (graceful degradation)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Use validated props (includes coerced values and defaults)
|
|
39
|
+
const validatedProps = validationResult.props;
|
|
40
|
+
|
|
41
|
+
// Handle children prop separately (not validated by prop validator)
|
|
42
|
+
const resolvedProps: Record<string, unknown> = { ...validatedProps };
|
|
43
|
+
|
|
44
|
+
// Apply children handling
|
|
45
|
+
if ('children' in propDefs) {
|
|
46
|
+
const childrenPropDef = propDefs.children;
|
|
47
|
+
if (childrenPropDef && typeof childrenPropDef === 'object' && 'default' in childrenPropDef) {
|
|
48
|
+
resolvedProps.children = Array.isArray(children) && children.length > 0
|
|
49
|
+
? children
|
|
50
|
+
: childrenPropDef.default;
|
|
51
|
+
} else {
|
|
52
|
+
resolvedProps.children = Array.isArray(children) && children.length > 0
|
|
53
|
+
? children
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
} else if (Array.isArray(children) && children.length > 0) {
|
|
57
|
+
// Children passed but not in interface - allow it (backward compatibility)
|
|
58
|
+
resolvedProps.children = children;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Resolve i18n translations if locale and config are provided
|
|
62
|
+
if (locale && i18nConfig) {
|
|
63
|
+
return resolveI18nInProps(resolvedProps, locale, i18nConfig);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return resolvedProps;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Fallback to original behavior if validation fails (backward compatibility)
|
|
69
|
+
const resolvedProps: Record<string, unknown> = {};
|
|
70
|
+
|
|
71
|
+
// Apply defaults and passed props (original logic)
|
|
72
|
+
for (const [propName, propDef] of Object.entries(propDefs)) {
|
|
73
|
+
if (propDef && typeof propDef === 'object' && 'default' in propDef) {
|
|
74
|
+
if (propName === 'children') {
|
|
75
|
+
resolvedProps.children = Array.isArray(children) && children.length > 0
|
|
76
|
+
? children
|
|
77
|
+
: propDef.default;
|
|
78
|
+
} else {
|
|
79
|
+
resolvedProps[propName] = passedProps[propName] !== undefined
|
|
80
|
+
? passedProps[propName]
|
|
81
|
+
: propDef.default;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
if (propName === 'children') {
|
|
85
|
+
resolvedProps.children = Array.isArray(children) && children.length > 0
|
|
86
|
+
? children
|
|
87
|
+
: undefined;
|
|
88
|
+
} else {
|
|
89
|
+
resolvedProps[propName] = passedProps[propName];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return resolvedProps;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolves props from a structured component definition
|
|
100
|
+
*
|
|
101
|
+
* @param componentDef - Structured component definition
|
|
102
|
+
* @param passedProps - Props passed to the component instance
|
|
103
|
+
* @param children - Children passed to the component instance
|
|
104
|
+
* @param locale - Optional locale for i18n resolution
|
|
105
|
+
* @param i18nConfig - Optional i18n configuration
|
|
106
|
+
* @returns Resolved props object
|
|
107
|
+
*/
|
|
108
|
+
export function resolvePropsFromDefinition(
|
|
109
|
+
componentDef: StructuredComponentDefinition,
|
|
110
|
+
passedProps: Record<string, unknown> = {},
|
|
111
|
+
children: Array<ComponentNode | string> | string | ComponentNode | null | undefined = [],
|
|
112
|
+
locale?: string,
|
|
113
|
+
i18nConfig?: I18nConfig
|
|
114
|
+
): Record<string, unknown> {
|
|
115
|
+
const propDefs = componentDef.interface || {};
|
|
116
|
+
return resolveComponentProps({
|
|
117
|
+
propDefs,
|
|
118
|
+
passedProps,
|
|
119
|
+
children,
|
|
120
|
+
locale,
|
|
121
|
+
i18nConfig,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseNodeTypeRegistry Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from 'bun:test';
|
|
6
|
+
import { ClientNodeTypeRegistry } from './ClientNodeTypeRegistry';
|
|
7
|
+
import { SSRNodeTypeRegistry } from './SSRNodeTypeRegistry';
|
|
8
|
+
import { NodeTypeManager, globalNodeTypeManager } from './NodeTypeManager';
|
|
9
|
+
import { defineNodeType } from './defineNodeType';
|
|
10
|
+
import { registerBuiltInNodeTypes, builtInNodeTypes } from './nodeTypes';
|
|
11
|
+
import { NODE_TYPE } from '../constants';
|
|
12
|
+
import type { HtmlNode, SlotMarker } from '../types/nodes';
|
|
13
|
+
|
|
14
|
+
describe('BaseNodeTypeRegistry', () => {
|
|
15
|
+
let registry: ClientNodeTypeRegistry;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
registry = new ClientNodeTypeRegistry();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('register', () => {
|
|
22
|
+
it('should register a node type definition', () => {
|
|
23
|
+
const mockDefinition = defineNodeType<HtmlNode>({
|
|
24
|
+
type: 'test-node',
|
|
25
|
+
displayName: 'Test Node',
|
|
26
|
+
category: 'core',
|
|
27
|
+
schema: {} as any,
|
|
28
|
+
typeGuard: (node): node is HtmlNode =>
|
|
29
|
+
node !== null && typeof node === 'object' && 'type' in node && node.type === 'test-node',
|
|
30
|
+
defaultFactory: () => ({ type: 'node', tag: 'div' }),
|
|
31
|
+
treeDisplay: { icon: 'HTML_ELEMENT', getLabel: () => 'Test' },
|
|
32
|
+
clientRenderer: () => null,
|
|
33
|
+
ssrRenderer: () => '',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
registry.register(mockDefinition);
|
|
37
|
+
expect(registry.has('test-node')).toBe(true);
|
|
38
|
+
expect(registry.get('test-node')).toBe(mockDefinition);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('registerAll', () => {
|
|
43
|
+
it('should register multiple node type definitions', () => {
|
|
44
|
+
registry.registerAll(builtInNodeTypes);
|
|
45
|
+
expect(registry.size).toBe(7);
|
|
46
|
+
expect(registry.has(NODE_TYPE.NODE)).toBe(true);
|
|
47
|
+
expect(registry.has(NODE_TYPE.COMPONENT)).toBe(true);
|
|
48
|
+
expect(registry.has(NODE_TYPE.EMBED)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('findByNode', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
registry.registerAll(builtInNodeTypes);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should find node type for HtmlNode', () => {
|
|
58
|
+
const htmlNode: HtmlNode = { type: 'node', tag: 'div' };
|
|
59
|
+
const definition = registry.findByNode(htmlNode);
|
|
60
|
+
expect(definition).toBeDefined();
|
|
61
|
+
expect(definition?.type).toBe(NODE_TYPE.NODE);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should find node type for SlotMarker', () => {
|
|
65
|
+
const slotNode: SlotMarker = { type: 'slot' };
|
|
66
|
+
const definition = registry.findByNode(slotNode);
|
|
67
|
+
expect(definition).toBeDefined();
|
|
68
|
+
expect(definition?.type).toBe(NODE_TYPE.SLOT);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return undefined for unknown nodes', () => {
|
|
72
|
+
const unknownNode = { type: 'unknown' } as any;
|
|
73
|
+
const definition = registry.findByNode(unknownNode);
|
|
74
|
+
expect(definition).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('isType', () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
registry.registerAll(builtInNodeTypes);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return true for matching types', () => {
|
|
84
|
+
const htmlNode = { type: 'node', tag: 'div' };
|
|
85
|
+
expect(registry.isType(htmlNode, NODE_TYPE.NODE)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return false for non-matching types', () => {
|
|
89
|
+
const htmlNode = { type: 'node', tag: 'div' };
|
|
90
|
+
expect(registry.isType(htmlNode, NODE_TYPE.COMPONENT)).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getNodeLabel', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
registry.registerAll(builtInNodeTypes);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return label info for html node', () => {
|
|
100
|
+
const htmlNode: HtmlNode = { type: 'node', tag: 'header' };
|
|
101
|
+
const labelInfo = registry.getNodeLabel(htmlNode);
|
|
102
|
+
expect(labelInfo).toEqual({ label: 'header', icon: 'HTML_ELEMENT' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return label info for embed node', () => {
|
|
106
|
+
const embedNode = { type: 'embed', html: '<div>test</div>' };
|
|
107
|
+
const labelInfo = registry.getNodeLabel(embedNode as any);
|
|
108
|
+
expect(labelInfo).toEqual({ label: 'Embed', icon: 'HTML_ELEMENT' });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('createDefault', () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
registry.registerAll(builtInNodeTypes);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should create default html node', () => {
|
|
118
|
+
const node = registry.createDefault(NODE_TYPE.NODE);
|
|
119
|
+
expect(node).toBeDefined();
|
|
120
|
+
expect(node?.type).toBe('node');
|
|
121
|
+
expect((node as HtmlNode).tag).toBe('div');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should create default embed node', () => {
|
|
125
|
+
const node = registry.createDefault(NODE_TYPE.EMBED);
|
|
126
|
+
expect(node).toBeDefined();
|
|
127
|
+
expect(node?.type).toBe('embed');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return undefined for unknown type', () => {
|
|
131
|
+
const node = registry.createDefault('unknown');
|
|
132
|
+
expect(node).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('subscribe', () => {
|
|
137
|
+
it('should notify listeners on registration', () => {
|
|
138
|
+
let called = false;
|
|
139
|
+
registry.subscribe(() => { called = true; });
|
|
140
|
+
registry.registerAll(builtInNodeTypes);
|
|
141
|
+
expect(called).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should allow unsubscribing', () => {
|
|
145
|
+
let callCount = 0;
|
|
146
|
+
const unsubscribe = registry.subscribe(() => { callCount++; });
|
|
147
|
+
registry.registerAll(builtInNodeTypes);
|
|
148
|
+
expect(callCount).toBe(1);
|
|
149
|
+
|
|
150
|
+
unsubscribe();
|
|
151
|
+
registry.clear();
|
|
152
|
+
expect(callCount).toBe(1); // Should not increment after unsubscribe
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('getByCategory', () => {
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
registry.registerAll(builtInNodeTypes);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should filter by category', () => {
|
|
162
|
+
const coreTypes = registry.getByCategory('core');
|
|
163
|
+
expect(coreTypes.length).toBeGreaterThan(0);
|
|
164
|
+
expect(coreTypes.every(t => t.category === 'core')).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('NodeTypeManager', () => {
|
|
170
|
+
it('should register types in both client and SSR registries', () => {
|
|
171
|
+
const manager = new NodeTypeManager();
|
|
172
|
+
manager.registerAll(builtInNodeTypes);
|
|
173
|
+
|
|
174
|
+
expect(manager.getClient().has(NODE_TYPE.NODE)).toBe(true);
|
|
175
|
+
expect(manager.getSSR().has(NODE_TYPE.NODE)).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('globalNodeTypeManager', () => {
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
globalNodeTypeManager.clear();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should be a singleton', () => {
|
|
185
|
+
registerBuiltInNodeTypes();
|
|
186
|
+
expect(globalNodeTypeManager.has(NODE_TYPE.NODE)).toBe(true);
|
|
187
|
+
expect(globalNodeTypeManager.has(NODE_TYPE.EMBED)).toBe(true);
|
|
188
|
+
expect(globalNodeTypeManager.getAll().length).toBe(7);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Node Type Registry
|
|
3
|
+
* Abstract base class for node type registries
|
|
4
|
+
* Provides common functionality for client and SSR registries
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ComponentNode } from '../types/nodes';
|
|
8
|
+
import type { NodeTypeDefinition, NodeCategory, TreeIcon } from './NodeTypeDefinition';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Node label info returned by getNodeLabel
|
|
12
|
+
*/
|
|
13
|
+
export interface NodeLabelInfo {
|
|
14
|
+
label: string;
|
|
15
|
+
icon: TreeIcon;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Base registry class with common functionality
|
|
20
|
+
*/
|
|
21
|
+
export abstract class BaseNodeTypeRegistry {
|
|
22
|
+
protected registry: Map<string, NodeTypeDefinition> = new Map();
|
|
23
|
+
private listeners: Set<() => void> = new Set();
|
|
24
|
+
private nodeTypeCache: WeakMap<object, NodeTypeDefinition | null> = new WeakMap();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register a node type definition
|
|
28
|
+
*/
|
|
29
|
+
register(definition: NodeTypeDefinition): void {
|
|
30
|
+
if (!definition.type) {
|
|
31
|
+
throw new Error('Node type definition must have a type');
|
|
32
|
+
}
|
|
33
|
+
this.registry.set(definition.type, definition);
|
|
34
|
+
this.nodeTypeCache = new WeakMap(); // Clear cache on registration
|
|
35
|
+
this.notify();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register multiple node type definitions at once
|
|
40
|
+
*/
|
|
41
|
+
registerAll(definitions: NodeTypeDefinition[]): void {
|
|
42
|
+
for (const definition of definitions) {
|
|
43
|
+
if (!definition.type) {
|
|
44
|
+
throw new Error('Node type definition must have a type');
|
|
45
|
+
}
|
|
46
|
+
this.registry.set(definition.type, definition);
|
|
47
|
+
}
|
|
48
|
+
this.nodeTypeCache = new WeakMap(); // Clear cache on registration
|
|
49
|
+
this.notify();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get a node type definition by type string
|
|
54
|
+
*/
|
|
55
|
+
get(type: string): NodeTypeDefinition | undefined {
|
|
56
|
+
return this.registry.get(type);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a node type is registered
|
|
61
|
+
*/
|
|
62
|
+
has(type: string): boolean {
|
|
63
|
+
return this.registry.has(type);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get all registered node types
|
|
68
|
+
*/
|
|
69
|
+
getAll(): NodeTypeDefinition[] {
|
|
70
|
+
return Array.from(this.registry.values());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get node types by category
|
|
75
|
+
*/
|
|
76
|
+
getByCategory(category: NodeCategory): NodeTypeDefinition[] {
|
|
77
|
+
return this.getAll().filter((def) => def.category === category);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get list of registered type names
|
|
82
|
+
*/
|
|
83
|
+
getNames(): string[] {
|
|
84
|
+
return Array.from(this.registry.keys());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find node type definition by node object using type guards
|
|
89
|
+
* Uses WeakMap caching for performance
|
|
90
|
+
*/
|
|
91
|
+
findByNode(node: ComponentNode): NodeTypeDefinition | undefined {
|
|
92
|
+
if (!node || typeof node !== 'object') {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check cache first
|
|
97
|
+
const cached = this.nodeTypeCache.get(node);
|
|
98
|
+
if (cached !== undefined) {
|
|
99
|
+
return cached || undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Linear search through registered types
|
|
103
|
+
for (const definition of this.registry.values()) {
|
|
104
|
+
if (definition.typeGuard(node)) {
|
|
105
|
+
this.nodeTypeCache.set(node, definition);
|
|
106
|
+
return definition;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Cache miss result too
|
|
111
|
+
this.nodeTypeCache.set(node, null);
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if a node matches a specific type
|
|
117
|
+
* More efficient than findByNode when you know the type
|
|
118
|
+
*/
|
|
119
|
+
isType<T extends ComponentNode>(node: unknown, type: string): node is T {
|
|
120
|
+
const definition = this.registry.get(type);
|
|
121
|
+
if (!definition) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return definition.typeGuard(node);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get node label info for tree display
|
|
129
|
+
*/
|
|
130
|
+
getNodeLabel(node: ComponentNode): NodeLabelInfo | undefined {
|
|
131
|
+
const definition = this.findByNode(node);
|
|
132
|
+
if (!definition) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
label: definition.treeDisplay.getLabel(node),
|
|
137
|
+
icon: definition.treeDisplay.icon,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get default node for a type
|
|
143
|
+
*/
|
|
144
|
+
createDefault(type: string): ComponentNode | undefined {
|
|
145
|
+
const definition = this.registry.get(type);
|
|
146
|
+
if (!definition) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return definition.defaultFactory();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Remove a node type by type string
|
|
154
|
+
*/
|
|
155
|
+
remove(type: string): boolean {
|
|
156
|
+
if (this.has(type)) {
|
|
157
|
+
this.registry.delete(type);
|
|
158
|
+
this.nodeTypeCache = new WeakMap(); // Clear cache
|
|
159
|
+
this.notify();
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Clear all registered node types
|
|
167
|
+
*/
|
|
168
|
+
clear(): void {
|
|
169
|
+
this.registry.clear();
|
|
170
|
+
this.nodeTypeCache = new WeakMap();
|
|
171
|
+
this.notify();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Subscribe to registry changes. Returns an unsubscribe function.
|
|
176
|
+
*/
|
|
177
|
+
subscribe(listener: () => void): () => void {
|
|
178
|
+
this.listeners.add(listener);
|
|
179
|
+
return () => {
|
|
180
|
+
this.listeners.delete(listener);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the number of registered node types
|
|
186
|
+
*/
|
|
187
|
+
get size(): number {
|
|
188
|
+
return this.registry.size;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
protected notify(): void {
|
|
192
|
+
for (const listener of this.listeners) {
|
|
193
|
+
try {
|
|
194
|
+
listener();
|
|
195
|
+
} catch {
|
|
196
|
+
// Silent fail - don't let one listener break others
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Node Type Registry
|
|
3
|
+
* Registry specialized for client-side (React) rendering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from 'react';
|
|
7
|
+
import type { ComponentNode } from '../types/nodes';
|
|
8
|
+
import { BaseNodeTypeRegistry } from './BaseNodeTypeRegistry';
|
|
9
|
+
import type { ClientRenderContext } from './NodeTypeDefinition';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Client-side node type registry
|
|
13
|
+
* Extends base registry with client rendering capabilities
|
|
14
|
+
*/
|
|
15
|
+
export class ClientNodeTypeRegistry extends BaseNodeTypeRegistry {
|
|
16
|
+
/**
|
|
17
|
+
* Render a node using the registered renderer
|
|
18
|
+
* Returns null if no renderer is found for the node type
|
|
19
|
+
*/
|
|
20
|
+
renderNode(node: ComponentNode, context: ClientRenderContext): ReactElement | null {
|
|
21
|
+
const definition = this.findByNode(node);
|
|
22
|
+
if (!definition) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return definition.clientRenderer(node as any, context);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a node can be rendered by this registry
|
|
30
|
+
*/
|
|
31
|
+
canRender(node: ComponentNode): boolean {
|
|
32
|
+
return this.findByNode(node) !== undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { ClientRegistry } from './ClientRegistry';
|
|
3
|
+
|
|
4
|
+
describe('ClientRegistry', () => {
|
|
5
|
+
test('can be instantiated', () => {
|
|
6
|
+
const registry = new ClientRegistry();
|
|
7
|
+
expect(registry).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('inherits from BaseComponentRegistry', () => {
|
|
11
|
+
const registry = new ClientRegistry();
|
|
12
|
+
const def = { component: { structure: { type: 'node', tag: 'div' } } };
|
|
13
|
+
|
|
14
|
+
registry.register('Test', def);
|
|
15
|
+
expect(registry.get('Test')).toEqual(def);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('supports all base registry methods', () => {
|
|
19
|
+
const registry = new ClientRegistry();
|
|
20
|
+
expect(typeof registry.register).toBe('function');
|
|
21
|
+
expect(typeof registry.get).toBe('function');
|
|
22
|
+
expect(typeof registry.has).toBe('function');
|
|
23
|
+
expect(typeof registry.getAll).toBe('function');
|
|
24
|
+
expect(typeof registry.clear).toBe('function');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Component Registry
|
|
3
|
+
* Extends base registry with client-specific functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BaseComponentRegistry } from './ComponentRegistry';
|
|
7
|
+
import type { ComponentDefinition } from '../types';
|
|
8
|
+
|
|
9
|
+
export class ClientRegistry extends BaseComponentRegistry {
|
|
10
|
+
/**
|
|
11
|
+
* Client-specific methods can be added here
|
|
12
|
+
* For now, it's the same as base registry
|
|
13
|
+
*/
|
|
14
|
+
}
|
|
15
|
+
|