meno-core 1.0.3 → 1.0.5
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 +63 -4
- package/build-static.ts +19 -3
- package/entries/server-router.tsx +0 -1
- package/lib/client/ErrorBoundary.test.tsx +3 -3
- package/lib/client/componentRegistry.test.ts +81 -35
- package/lib/client/core/ComponentBuilder.test.ts +12 -18
- package/lib/client/core/ComponentBuilder.ts +25 -7
- package/lib/client/core/ComponentRenderer.test.tsx +0 -1
- package/lib/client/core/builders/cmsListBuilder.ts +54 -5
- package/lib/client/core/builders/embedBuilder.ts +55 -7
- package/lib/client/core/builders/linkBuilder.ts +27 -9
- package/lib/client/core/builders/localeListBuilder.ts +59 -11
- package/lib/client/core/builders/objectLinkBuilder.ts +54 -6
- package/lib/client/core/cmsTemplateProcessor.ts +2 -2
- package/lib/client/hydration/HydrationUtils.test.ts +1 -1
- package/lib/client/i18nConfigService.test.ts +6 -7
- package/lib/client/index.ts +1 -1
- package/lib/client/responsiveStyleResolver.test.ts +6 -6
- package/lib/client/routing/RouteLoader.test.ts +11 -11
- package/lib/client/routing/Router.tsx +21 -12
- package/lib/client/scripts/ScriptExecutor.test.ts +11 -35
- package/lib/client/scripts/ScriptExecutor.ts +33 -27
- package/lib/client/services/PrefetchService.test.ts +3 -3
- package/lib/client/styleProcessor.ts +4 -3
- package/lib/client/styles/StyleInjector.test.ts +6 -2
- package/lib/client/templateEngine.test.ts +6 -5
- package/lib/client/templateEngine.ts +20 -167
- package/lib/client/utils/toast.ts +19 -18
- package/lib/server/createServer.ts +1 -1
- package/lib/server/cssGenerator.test.ts +8 -8
- package/lib/server/index.ts +1 -1
- package/lib/server/jsonLoader.test.ts +2 -2
- package/lib/server/jsonLoader.ts +5 -0
- package/lib/server/middleware/cors.ts +2 -2
- package/lib/server/middleware/logger.test.ts +9 -9
- package/lib/server/routes/api/core-routes.ts +1 -1
- package/lib/server/routes/api/index.ts +1 -1
- package/lib/server/routes/api/pages.ts +1 -1
- package/lib/server/routes/pages.ts +2 -2
- package/lib/server/services/fileWatcherService.ts +3 -1
- package/lib/server/ssr/ssrRenderer.ts +167 -25
- package/lib/server/ssrRenderer.test.ts +11 -8
- package/lib/server/validateStyleCoverage.ts +1 -1
- package/lib/shared/attributeNodeUtils.test.ts +7 -7
- package/lib/shared/constants.test.ts +1 -1
- package/lib/shared/constants.ts +1 -1
- package/lib/shared/cssGeneration.ts +1 -1
- package/lib/shared/errorLogger.test.ts +20 -12
- package/lib/shared/expressionEvaluator.ts +161 -0
- package/lib/shared/fontLoader.ts +2 -2
- package/lib/shared/index.ts +40 -3
- package/lib/shared/itemTemplateUtils.test.ts +78 -0
- package/lib/shared/itemTemplateUtils.ts +66 -30
- package/lib/shared/nodeUtils.test.ts +3 -3
- package/lib/shared/propResolver.test.ts +135 -135
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +5 -5
- package/lib/shared/registry/ClientRegistry.test.ts +1 -1
- package/lib/shared/registry/RegistryManager.test.ts +4 -4
- package/lib/shared/registry/SSRRegistry.test.ts +1 -1
- package/lib/shared/registry/index.ts +0 -1
- package/lib/shared/registry/nodeTypes/index.ts +0 -5
- package/lib/shared/responsiveScaling.ts +3 -2
- package/lib/shared/styleNodeUtils.test.ts +3 -3
- package/lib/shared/styleUtils.test.ts +7 -7
- package/lib/shared/styleUtils.ts +2 -2
- package/lib/shared/treePathUtils.test.ts +17 -17
- package/lib/shared/treePathUtils.ts +26 -20
- package/lib/shared/types/errors.ts +3 -3
- package/lib/shared/types/nodes.ts +0 -1
- package/lib/shared/types/rendering.ts +2 -1
- package/lib/shared/types/styles.ts +3 -1
- package/lib/shared/utilityClassMapper.ts +17 -5
- package/lib/shared/validation/propValidator.test.ts +11 -7
- package/lib/shared/validation/propValidator.ts +5 -5
- package/lib/shared/validation/schemas.ts +15 -9
- package/lib/shared/validation/validators.test.ts +2 -3
- package/lib/test-utils/factories/FetchMockFactory.ts +2 -2
- package/lib/test-utils/index.ts +2 -1
- package/package.json +1 -1
- package/tsconfig.json +2 -2
- package/vite.config.ts +1 -1
- package/lib/shared/registry/nodeTypes/TextNodeType.ts +0 -52
package/bin/cli.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { resolve, join } from 'path';
|
|
8
|
-
import { existsSync, mkdirSync, writeFileSync, cpSync } from 'fs';
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync } from 'fs';
|
|
9
9
|
import { setProjectRoot } from '../lib/server/projectContext';
|
|
10
10
|
|
|
11
11
|
const args = process.argv.slice(2);
|
|
@@ -35,10 +35,66 @@ Examples:
|
|
|
35
35
|
`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Parse _headers file (Netlify/Cloudflare format)
|
|
39
|
+
function parseHeadersFile(distPath: string): Map<string, Record<string, string>> {
|
|
40
|
+
const headersPath = join(distPath, '_headers');
|
|
41
|
+
const headers = new Map<string, Record<string, string>>();
|
|
42
|
+
|
|
43
|
+
if (!existsSync(headersPath)) return headers;
|
|
44
|
+
|
|
45
|
+
const content = readFileSync(headersPath, 'utf-8');
|
|
46
|
+
let currentPath = '';
|
|
47
|
+
|
|
48
|
+
for (const line of content.split('\n')) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
51
|
+
|
|
52
|
+
if (!line.startsWith(' ') && !line.startsWith('\t')) {
|
|
53
|
+
// Path line
|
|
54
|
+
currentPath = trimmed;
|
|
55
|
+
if (!headers.has(currentPath)) {
|
|
56
|
+
headers.set(currentPath, {});
|
|
57
|
+
}
|
|
58
|
+
} else if (currentPath && trimmed.includes(':')) {
|
|
59
|
+
// Header line
|
|
60
|
+
const colonIndex = trimmed.indexOf(':');
|
|
61
|
+
const name = trimmed.substring(0, colonIndex).trim();
|
|
62
|
+
const value = trimmed.substring(colonIndex + 1).trim();
|
|
63
|
+
headers.get(currentPath)![name] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return headers;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get headers matching a pathname
|
|
71
|
+
function getMatchingHeaders(
|
|
72
|
+
pathname: string,
|
|
73
|
+
headersMap: Map<string, Record<string, string>>
|
|
74
|
+
): Record<string, string> {
|
|
75
|
+
const result: Record<string, string> = {};
|
|
76
|
+
|
|
77
|
+
headersMap.forEach((headers, pattern) => {
|
|
78
|
+
if (pattern === pathname || pattern === '/*') {
|
|
79
|
+
Object.assign(result, headers);
|
|
80
|
+
} else if (pattern.endsWith('/*')) {
|
|
81
|
+
const prefix = pattern.slice(0, -1);
|
|
82
|
+
if (pathname.startsWith(prefix)) {
|
|
83
|
+
Object.assign(result, headers);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
38
91
|
// Start static file server on port 8080 (non-blocking)
|
|
39
92
|
async function startStaticServer(distPath: string) {
|
|
40
93
|
const { SERVE_PORT } = await import('../lib/shared/constants');
|
|
41
94
|
|
|
95
|
+
// Parse _headers file once on startup
|
|
96
|
+
const headersMap = parseHeadersFile(distPath);
|
|
97
|
+
|
|
42
98
|
const server = Bun.serve({
|
|
43
99
|
port: SERVE_PORT,
|
|
44
100
|
async fetch(req: Request) {
|
|
@@ -50,6 +106,9 @@ async function startStaticServer(distPath: string) {
|
|
|
50
106
|
pathname = '/index.html';
|
|
51
107
|
}
|
|
52
108
|
|
|
109
|
+
// Get custom headers for this path
|
|
110
|
+
const customHeaders = getMatchingHeaders(pathname, headersMap);
|
|
111
|
+
|
|
53
112
|
// Try to serve the file
|
|
54
113
|
const filePath = join(distPath, pathname);
|
|
55
114
|
|
|
@@ -57,7 +116,7 @@ async function startStaticServer(distPath: string) {
|
|
|
57
116
|
if (existsSync(filePath)) {
|
|
58
117
|
const file = Bun.file(filePath);
|
|
59
118
|
if (await file.exists()) {
|
|
60
|
-
return new Response(file);
|
|
119
|
+
return new Response(file, { headers: customHeaders });
|
|
61
120
|
}
|
|
62
121
|
}
|
|
63
122
|
|
|
@@ -65,14 +124,14 @@ async function startStaticServer(distPath: string) {
|
|
|
65
124
|
const htmlPath = filePath.endsWith('.html') ? filePath : `${filePath}.html`;
|
|
66
125
|
if (existsSync(htmlPath)) {
|
|
67
126
|
const file = Bun.file(htmlPath);
|
|
68
|
-
return new Response(file);
|
|
127
|
+
return new Response(file, { headers: customHeaders });
|
|
69
128
|
}
|
|
70
129
|
|
|
71
130
|
// Try index.html in directory
|
|
72
131
|
const indexPath = join(filePath, 'index.html');
|
|
73
132
|
if (existsSync(indexPath)) {
|
|
74
133
|
const file = Bun.file(indexPath);
|
|
75
|
-
return new Response(file);
|
|
134
|
+
return new Response(file, { headers: customHeaders });
|
|
76
135
|
}
|
|
77
136
|
|
|
78
137
|
return new Response('Not Found', { status: 404 });
|
package/build-static.ts
CHANGED
|
@@ -299,11 +299,27 @@ async function buildStaticPages(): Promise<void> {
|
|
|
299
299
|
const functionsDir = projectPaths.functions();
|
|
300
300
|
if (existsSync(functionsDir)) {
|
|
301
301
|
copyDirectory(functionsDir, join(distDir, "functions"));
|
|
302
|
-
console.log("✅ Assets and functions copied\n");
|
|
303
|
-
} else {
|
|
304
|
-
console.log("✅ Assets copied\n");
|
|
305
302
|
}
|
|
306
303
|
|
|
304
|
+
// Copy _headers and _redirects files for static hosting (Netlify, Cloudflare Pages)
|
|
305
|
+
const hostingFiles: string[] = [];
|
|
306
|
+
const headersFile = join(projectPaths.project, '_headers');
|
|
307
|
+
const redirectsFile = join(projectPaths.project, '_redirects');
|
|
308
|
+
|
|
309
|
+
if (existsSync(headersFile)) {
|
|
310
|
+
copyFileSync(headersFile, join(distDir, '_headers'));
|
|
311
|
+
hostingFiles.push('_headers');
|
|
312
|
+
}
|
|
313
|
+
if (existsSync(redirectsFile)) {
|
|
314
|
+
copyFileSync(redirectsFile, join(distDir, '_redirects'));
|
|
315
|
+
hostingFiles.push('_redirects');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const parts = ['Assets'];
|
|
319
|
+
if (existsSync(functionsDir)) parts.push('functions');
|
|
320
|
+
if (hostingFiles.length > 0) parts.push(hostingFiles.join(', '));
|
|
321
|
+
console.log(`✅ ${parts.join(', ')} copied\n`);
|
|
322
|
+
|
|
307
323
|
// Load all global components
|
|
308
324
|
const components = await loadComponentDirectory(projectPaths.components());
|
|
309
325
|
const globalComponents: Record<string, ComponentDefinition> = {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test";
|
|
2
2
|
import { ErrorBoundary, withErrorBoundary } from "./ErrorBoundary";
|
|
3
|
-
import { createElement as h, Component, useState } from "react";
|
|
3
|
+
import React, { createElement as h, Component, useState } from "react";
|
|
4
4
|
import { createRoot, Root } from "react-dom/client";
|
|
5
5
|
import { flushPromises, wait } from "../test-utils/helpers/asyncHelpers";
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ function cleanupContainer(container: HTMLElement) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Component that throws an error when rendered
|
|
36
|
-
function ThrowingComponent({ error }: { error: Error }) {
|
|
36
|
+
function ThrowingComponent({ error }: { error: Error }): React.ReactNode {
|
|
37
37
|
throw error;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -124,7 +124,7 @@ describe("ErrorBoundary - Error catching", () => {
|
|
|
124
124
|
|
|
125
125
|
// Verify onError callback was called with the error
|
|
126
126
|
expect(onErrorMock).toHaveBeenCalled();
|
|
127
|
-
const callArgs = onErrorMock.mock.calls[0];
|
|
127
|
+
const callArgs = (onErrorMock.mock.calls as unknown as [Error, unknown][])[0];
|
|
128
128
|
expect(callArgs[0]).toBe(testError);
|
|
129
129
|
} finally {
|
|
130
130
|
root.unmount();
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { test, expect, describe, beforeEach } from "bun:test";
|
|
2
2
|
import { ComponentRegistry } from "./componentRegistry";
|
|
3
|
+
import type { ComponentDefinition, ComponentNode, PropDefinition } from "../shared/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper function to create a valid ComponentDefinition for tests
|
|
7
|
+
*/
|
|
8
|
+
function createTestComponentDef(
|
|
9
|
+
structure?: ComponentNode,
|
|
10
|
+
componentInterface?: Record<string, PropDefinition>
|
|
11
|
+
): ComponentDefinition {
|
|
12
|
+
return {
|
|
13
|
+
component: {
|
|
14
|
+
interface: componentInterface || {},
|
|
15
|
+
structure: structure || { type: "node" as const, tag: "div", children: [] }
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
3
19
|
|
|
4
20
|
describe("ComponentRegistry", () => {
|
|
5
21
|
let registry: ComponentRegistry;
|
|
@@ -10,10 +26,9 @@ describe("ComponentRegistry", () => {
|
|
|
10
26
|
|
|
11
27
|
describe("register and get", () => {
|
|
12
28
|
test("should register and retrieve a component", () => {
|
|
13
|
-
const buttonDef =
|
|
14
|
-
type: "button",
|
|
15
|
-
|
|
16
|
-
};
|
|
29
|
+
const buttonDef = createTestComponentDef(
|
|
30
|
+
{ type: "node" as const, tag: "button", children: [], attributes: { className: "btn" } }
|
|
31
|
+
);
|
|
17
32
|
|
|
18
33
|
registry.register("Button", buttonDef);
|
|
19
34
|
const retrieved = registry.get("Button");
|
|
@@ -27,17 +42,25 @@ describe("ComponentRegistry", () => {
|
|
|
27
42
|
});
|
|
28
43
|
|
|
29
44
|
test("should overwrite existing component with same name", () => {
|
|
30
|
-
registry.register("Button",
|
|
31
|
-
|
|
45
|
+
registry.register("Button", createTestComponentDef(
|
|
46
|
+
{ type: "node" as const, tag: "button", children: [], attributes: { color: "red" } }
|
|
47
|
+
));
|
|
48
|
+
registry.register("Button", createTestComponentDef(
|
|
49
|
+
{ type: "node" as const, tag: "button", children: [], attributes: { color: "blue" } }
|
|
50
|
+
));
|
|
32
51
|
|
|
33
52
|
const result = registry.get("Button");
|
|
34
|
-
|
|
53
|
+
const structure = result?.component?.structure;
|
|
54
|
+
const attributes = structure && 'attributes' in structure ? structure.attributes as Record<string, unknown> : undefined;
|
|
55
|
+
expect(attributes?.color).toBe("blue");
|
|
35
56
|
});
|
|
36
57
|
});
|
|
37
58
|
|
|
38
59
|
describe("has", () => {
|
|
39
60
|
test("should return true for registered component", () => {
|
|
40
|
-
registry.register("Card",
|
|
61
|
+
registry.register("Card", createTestComponentDef(
|
|
62
|
+
{ type: "node" as const, tag: "div", children: [] }
|
|
63
|
+
));
|
|
41
64
|
expect(registry.has("Card")).toBe(true);
|
|
42
65
|
});
|
|
43
66
|
|
|
@@ -48,8 +71,12 @@ describe("ComponentRegistry", () => {
|
|
|
48
71
|
|
|
49
72
|
describe("clear", () => {
|
|
50
73
|
test("should clear all components", () => {
|
|
51
|
-
registry.register("Button",
|
|
52
|
-
|
|
74
|
+
registry.register("Button", createTestComponentDef(
|
|
75
|
+
{ type: "node" as const, tag: "button", children: [] }
|
|
76
|
+
));
|
|
77
|
+
registry.register("Card", createTestComponentDef(
|
|
78
|
+
{ type: "node" as const, tag: "div", children: [] }
|
|
79
|
+
));
|
|
53
80
|
|
|
54
81
|
expect(registry.getNames().length).toBe(2);
|
|
55
82
|
|
|
@@ -63,11 +90,13 @@ describe("ComponentRegistry", () => {
|
|
|
63
90
|
|
|
64
91
|
describe("merge", () => {
|
|
65
92
|
test("should merge multiple components", () => {
|
|
66
|
-
registry.register("Button",
|
|
93
|
+
registry.register("Button", createTestComponentDef(
|
|
94
|
+
{ type: "node" as const, tag: "button", children: [] }
|
|
95
|
+
));
|
|
67
96
|
|
|
68
97
|
registry.merge({
|
|
69
|
-
Card: { type: "div" },
|
|
70
|
-
Link: { type: "a" }
|
|
98
|
+
Card: createTestComponentDef({ type: "node" as const, tag: "div", children: [] }),
|
|
99
|
+
Link: createTestComponentDef({ type: "node" as const, tag: "a", children: [] })
|
|
71
100
|
});
|
|
72
101
|
|
|
73
102
|
expect(registry.getNames().length).toBe(3);
|
|
@@ -77,21 +106,31 @@ describe("ComponentRegistry", () => {
|
|
|
77
106
|
});
|
|
78
107
|
|
|
79
108
|
test("should overwrite existing components when merging", () => {
|
|
80
|
-
registry.register("Button",
|
|
109
|
+
registry.register("Button", createTestComponentDef(
|
|
110
|
+
{ type: "node" as const, tag: "button", children: [], attributes: { color: "red" } }
|
|
111
|
+
));
|
|
81
112
|
|
|
82
113
|
registry.merge({
|
|
83
|
-
Button:
|
|
114
|
+
Button: createTestComponentDef(
|
|
115
|
+
{ type: "node" as const, tag: "button", children: [], attributes: { color: "blue" } }
|
|
116
|
+
)
|
|
84
117
|
});
|
|
85
118
|
|
|
86
119
|
const button = registry.get("Button");
|
|
87
|
-
|
|
120
|
+
const structure = button?.component?.structure;
|
|
121
|
+
const attributes = structure && 'attributes' in structure ? structure.attributes as Record<string, unknown> : undefined;
|
|
122
|
+
expect(attributes?.color).toBe("blue");
|
|
88
123
|
});
|
|
89
124
|
});
|
|
90
125
|
|
|
91
126
|
describe("getAll", () => {
|
|
92
127
|
test("should return all registered components", () => {
|
|
93
|
-
registry.register("Button",
|
|
94
|
-
|
|
128
|
+
registry.register("Button", createTestComponentDef(
|
|
129
|
+
{ type: "node" as const, tag: "button", children: [] }
|
|
130
|
+
));
|
|
131
|
+
registry.register("Card", createTestComponentDef(
|
|
132
|
+
{ type: "node" as const, tag: "div", children: [] }
|
|
133
|
+
));
|
|
95
134
|
|
|
96
135
|
const all = registry.getAll();
|
|
97
136
|
|
|
@@ -101,10 +140,14 @@ describe("ComponentRegistry", () => {
|
|
|
101
140
|
});
|
|
102
141
|
|
|
103
142
|
test("should return a copy, not the original registry", () => {
|
|
104
|
-
registry.register("Button",
|
|
143
|
+
registry.register("Button", createTestComponentDef(
|
|
144
|
+
{ type: "node" as const, tag: "button", children: [] }
|
|
145
|
+
));
|
|
105
146
|
|
|
106
147
|
const all = registry.getAll();
|
|
107
|
-
all.NewComponent =
|
|
148
|
+
all.NewComponent = createTestComponentDef(
|
|
149
|
+
{ type: "node" as const, tag: "span", children: [] }
|
|
150
|
+
);
|
|
108
151
|
|
|
109
152
|
expect(registry.has("NewComponent")).toBe(false);
|
|
110
153
|
});
|
|
@@ -112,8 +155,12 @@ describe("ComponentRegistry", () => {
|
|
|
112
155
|
|
|
113
156
|
describe("getNames", () => {
|
|
114
157
|
test("should return list of component names", () => {
|
|
115
|
-
registry.register("Button",
|
|
116
|
-
|
|
158
|
+
registry.register("Button", createTestComponentDef(
|
|
159
|
+
{ type: "node" as const, tag: "button", children: [] }
|
|
160
|
+
));
|
|
161
|
+
registry.register("Card", createTestComponentDef(
|
|
162
|
+
{ type: "node" as const, tag: "div", children: [] }
|
|
163
|
+
));
|
|
117
164
|
|
|
118
165
|
const names = registry.getNames();
|
|
119
166
|
|
|
@@ -128,7 +175,9 @@ describe("ComponentRegistry", () => {
|
|
|
128
175
|
|
|
129
176
|
describe("remove", () => {
|
|
130
177
|
test("should remove a component and return true", () => {
|
|
131
|
-
registry.register("Button",
|
|
178
|
+
registry.register("Button", createTestComponentDef(
|
|
179
|
+
{ type: "node" as const, tag: "button", children: [] }
|
|
180
|
+
));
|
|
132
181
|
|
|
133
182
|
const result = registry.remove("Button");
|
|
134
183
|
|
|
@@ -142,23 +191,20 @@ describe("ComponentRegistry", () => {
|
|
|
142
191
|
});
|
|
143
192
|
});
|
|
144
193
|
|
|
145
|
-
describe("component with
|
|
146
|
-
test("should handle components with
|
|
147
|
-
const buttonDef = {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
defaultVariant: "primary"
|
|
194
|
+
describe("component with category", () => {
|
|
195
|
+
test("should handle components with category definitions", () => {
|
|
196
|
+
const buttonDef: ComponentDefinition = {
|
|
197
|
+
component: {
|
|
198
|
+
interface: {},
|
|
199
|
+
structure: { type: "node" as const, tag: "button", children: [] },
|
|
200
|
+
category: "buttons"
|
|
201
|
+
}
|
|
154
202
|
};
|
|
155
203
|
|
|
156
204
|
registry.register("Button", buttonDef);
|
|
157
205
|
const retrieved = registry.get("Button");
|
|
158
206
|
|
|
159
|
-
expect(retrieved?.
|
|
160
|
-
expect(retrieved?.variants?.primary?.backgroundColor).toBe("blue");
|
|
161
|
-
expect(retrieved?.defaultVariant).toBe("primary");
|
|
207
|
+
expect(retrieved?.component?.category).toBe("buttons");
|
|
162
208
|
});
|
|
163
209
|
});
|
|
164
210
|
});
|
|
@@ -3,22 +3,19 @@ import { ComponentBuilder } from "./ComponentBuilder";
|
|
|
3
3
|
import { ComponentRegistry } from "../componentRegistry";
|
|
4
4
|
import { ElementRegistry } from "../elementRegistry";
|
|
5
5
|
import type { ComponentNode, CMSListNode, CMSItem } from "../../shared/types";
|
|
6
|
-
import type { HighlightManager } from "../highlightManager";
|
|
7
6
|
import { NODE_TYPE } from "../../shared/constants";
|
|
8
|
-
import {
|
|
7
|
+
import { createMockElementRegistry } from "../../test-utils/mocks";
|
|
9
8
|
|
|
10
9
|
// Note: Using typed mocks from test-utils/mocks instead of inline 'as any' casts
|
|
11
10
|
|
|
12
11
|
describe("ComponentBuilder", () => {
|
|
13
12
|
let componentRegistry: ComponentRegistry;
|
|
14
|
-
let hoverHighlightManager: HighlightManager;
|
|
15
13
|
let elementRegistry: ElementRegistry;
|
|
16
14
|
let builder: ComponentBuilder;
|
|
17
15
|
let registeredPaths: Array<{ path: string; tag?: string }>;
|
|
18
16
|
|
|
19
17
|
beforeEach(() => {
|
|
20
18
|
componentRegistry = new ComponentRegistry();
|
|
21
|
-
hoverHighlightManager = createMockHighlightManager();
|
|
22
19
|
registeredPaths = [];
|
|
23
20
|
|
|
24
21
|
// Create mock element registry with path tracking
|
|
@@ -31,7 +28,6 @@ describe("ComponentBuilder", () => {
|
|
|
31
28
|
|
|
32
29
|
builder = new ComponentBuilder({
|
|
33
30
|
componentRegistry,
|
|
34
|
-
hoverHighlightManager,
|
|
35
31
|
elementRegistry,
|
|
36
32
|
});
|
|
37
33
|
});
|
|
@@ -711,7 +707,7 @@ describe("ComponentBuilder", () => {
|
|
|
711
707
|
]);
|
|
712
708
|
|
|
713
709
|
const result = builder.buildComponent({
|
|
714
|
-
node,
|
|
710
|
+
node: node as unknown as ComponentNode,
|
|
715
711
|
collectionItemsMap: { posts: items },
|
|
716
712
|
});
|
|
717
713
|
|
|
@@ -730,7 +726,7 @@ describe("ComponentBuilder", () => {
|
|
|
730
726
|
]);
|
|
731
727
|
|
|
732
728
|
const result = builder.buildComponent({
|
|
733
|
-
node,
|
|
729
|
+
node: node as unknown as ComponentNode,
|
|
734
730
|
collectionItemsMap: { posts: items },
|
|
735
731
|
});
|
|
736
732
|
|
|
@@ -758,7 +754,7 @@ describe("ComponentBuilder", () => {
|
|
|
758
754
|
]);
|
|
759
755
|
|
|
760
756
|
const result = builder.buildComponent({
|
|
761
|
-
node,
|
|
757
|
+
node: node as unknown as ComponentNode,
|
|
762
758
|
collectionItemsMap: { posts: items },
|
|
763
759
|
});
|
|
764
760
|
|
|
@@ -774,7 +770,7 @@ describe("ComponentBuilder", () => {
|
|
|
774
770
|
]);
|
|
775
771
|
|
|
776
772
|
const result = builder.buildComponent({
|
|
777
|
-
node,
|
|
773
|
+
node: node as unknown as ComponentNode,
|
|
778
774
|
collectionItemsMap: { posts: items },
|
|
779
775
|
});
|
|
780
776
|
|
|
@@ -791,7 +787,7 @@ describe("ComponentBuilder", () => {
|
|
|
791
787
|
]);
|
|
792
788
|
|
|
793
789
|
const result = builder.buildComponent({
|
|
794
|
-
node,
|
|
790
|
+
node: node as unknown as ComponentNode,
|
|
795
791
|
collectionItemsMap: { posts: [] },
|
|
796
792
|
});
|
|
797
793
|
|
|
@@ -808,7 +804,7 @@ describe("ComponentBuilder", () => {
|
|
|
808
804
|
]);
|
|
809
805
|
|
|
810
806
|
const result = builder.buildComponent({
|
|
811
|
-
node,
|
|
807
|
+
node: node as unknown as ComponentNode,
|
|
812
808
|
collectionItemsMap: {}, // No posts collection
|
|
813
809
|
});
|
|
814
810
|
|
|
@@ -823,7 +819,7 @@ describe("ComponentBuilder", () => {
|
|
|
823
819
|
], { limit: 2 });
|
|
824
820
|
|
|
825
821
|
const result = builder.buildComponent({
|
|
826
|
-
node,
|
|
822
|
+
node: node as unknown as ComponentNode,
|
|
827
823
|
collectionItemsMap: { posts: items },
|
|
828
824
|
});
|
|
829
825
|
|
|
@@ -840,7 +836,7 @@ describe("ComponentBuilder", () => {
|
|
|
840
836
|
], { offset: 2 });
|
|
841
837
|
|
|
842
838
|
const result = builder.buildComponent({
|
|
843
|
-
node,
|
|
839
|
+
node: node as unknown as ComponentNode,
|
|
844
840
|
collectionItemsMap: { posts: items },
|
|
845
841
|
});
|
|
846
842
|
|
|
@@ -857,7 +853,7 @@ describe("ComponentBuilder", () => {
|
|
|
857
853
|
], { offset: 1, limit: 2 });
|
|
858
854
|
|
|
859
855
|
const result = builder.buildComponent({
|
|
860
|
-
node,
|
|
856
|
+
node: node as unknown as ComponentNode,
|
|
861
857
|
collectionItemsMap: { posts: items },
|
|
862
858
|
});
|
|
863
859
|
|
|
@@ -924,7 +920,6 @@ describe("ComponentBuilder", () => {
|
|
|
924
920
|
// Create builder with page name getter
|
|
925
921
|
const pageBuilder = new ComponentBuilder({
|
|
926
922
|
componentRegistry,
|
|
927
|
-
hoverHighlightManager,
|
|
928
923
|
elementRegistry,
|
|
929
924
|
getCurrentPageName: () => "about",
|
|
930
925
|
getCurrentFileType: () => "page",
|
|
@@ -1005,7 +1000,7 @@ describe("ComponentBuilder", () => {
|
|
|
1005
1000
|
|
|
1006
1001
|
const result1 = builder.buildComponent({
|
|
1007
1002
|
node: node1,
|
|
1008
|
-
|
|
1003
|
+
elementPath: [0],
|
|
1009
1004
|
});
|
|
1010
1005
|
|
|
1011
1006
|
expect(result1).not.toBeNull();
|
|
@@ -1013,7 +1008,7 @@ describe("ComponentBuilder", () => {
|
|
|
1013
1008
|
// Second instance at page path [1, 2, 3]
|
|
1014
1009
|
const result2 = builder.buildComponent({
|
|
1015
1010
|
node: node1,
|
|
1016
|
-
|
|
1011
|
+
elementPath: [1, 2, 3],
|
|
1017
1012
|
});
|
|
1018
1013
|
|
|
1019
1014
|
expect(result2).not.toBeNull();
|
|
@@ -1055,7 +1050,6 @@ describe("ComponentBuilder", () => {
|
|
|
1055
1050
|
// Page element with same name
|
|
1056
1051
|
const pageBuilder = new ComponentBuilder({
|
|
1057
1052
|
componentRegistry,
|
|
1058
|
-
hoverHighlightManager,
|
|
1059
1053
|
elementRegistry,
|
|
1060
1054
|
getCurrentPageName: () => "button",
|
|
1061
1055
|
getCurrentFileType: () => "page",
|
|
@@ -244,8 +244,10 @@ export class ComponentBuilder {
|
|
|
244
244
|
elementRegistry: this.elementRegistry,
|
|
245
245
|
prefetchService: this.prefetchService,
|
|
246
246
|
getCachedStyleClasses: this.getCachedStyleClasses.bind(this),
|
|
247
|
-
buildChildren: this.buildChildren.bind(this),
|
|
247
|
+
buildChildren: this.buildChildren.bind(this) as (children: unknown, ctx: BuildChildrenContext) => BuildResult,
|
|
248
248
|
getEffectiveParentComponentName: this.getEffectiveParentComponentName.bind(this),
|
|
249
|
+
getCurrentPageName: this.getCurrentPageName,
|
|
250
|
+
getCurrentFileType: this.getCurrentFileType,
|
|
249
251
|
};
|
|
250
252
|
|
|
251
253
|
// Handle text nodes - process CMS templates and item templates
|
|
@@ -482,6 +484,21 @@ export class ComponentBuilder {
|
|
|
482
484
|
} else {
|
|
483
485
|
InteractiveStylesRegistry.set(elementClass, nodeInteractiveStyles);
|
|
484
486
|
}
|
|
487
|
+
|
|
488
|
+
// Apply preview classes when previewProp is set and truthy
|
|
489
|
+
if (ctx.componentResolvedProps) {
|
|
490
|
+
const previewClasses: string[] = [];
|
|
491
|
+
for (const rule of nodeInteractiveStyles) {
|
|
492
|
+
if (rule.previewProp && ctx.componentResolvedProps[rule.previewProp] === true) {
|
|
493
|
+
// Convert rule's style to utility classes
|
|
494
|
+
const styleClasses = this.getCachedStyleClasses(rule.style);
|
|
495
|
+
previewClasses.push(...styleClasses);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (previewClasses.length > 0) {
|
|
499
|
+
result.className = [result.className, ...previewClasses].filter(Boolean).join(' ');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
485
502
|
}
|
|
486
503
|
|
|
487
504
|
return result;
|
|
@@ -501,7 +518,7 @@ export class ComponentBuilder {
|
|
|
501
518
|
const i18nResolver: ValueResolver | undefined = effectiveLocale
|
|
502
519
|
? (value: unknown) => resolveI18nValue(value, effectiveLocale, config)
|
|
503
520
|
: undefined;
|
|
504
|
-
extractedAttributes = processItemPropsTemplate(extractedAttributes, effectiveItemContext, i18nResolver)
|
|
521
|
+
extractedAttributes = processItemPropsTemplate(extractedAttributes, effectiveItemContext, i18nResolver) as Record<string, string | number | boolean>;
|
|
505
522
|
}
|
|
506
523
|
|
|
507
524
|
if (Object.keys(extractedAttributes).length === 0) {
|
|
@@ -510,9 +527,9 @@ export class ComponentBuilder {
|
|
|
510
527
|
|
|
511
528
|
let result = { ...props };
|
|
512
529
|
|
|
513
|
-
// Handle class/className specially
|
|
514
|
-
if (
|
|
515
|
-
const attrClass = (extractedAttributes.class
|
|
530
|
+
// Handle class/className specially - check key existence, not truthiness (empty string is valid)
|
|
531
|
+
if ('class' in extractedAttributes || 'className' in extractedAttributes) {
|
|
532
|
+
const attrClass = (extractedAttributes.class ?? extractedAttributes.className ?? '') as string;
|
|
516
533
|
const existingClassName = (result.className || '') as string;
|
|
517
534
|
result.className = [existingClassName, attrClass].filter(Boolean).join(' ');
|
|
518
535
|
delete extractedAttributes.class;
|
|
@@ -552,7 +569,7 @@ export class ComponentBuilder {
|
|
|
552
569
|
let resolvedProps = resolvePropsFromDefinition(
|
|
553
570
|
structuredComponentDef,
|
|
554
571
|
nodeProps,
|
|
555
|
-
children,
|
|
572
|
+
children as ComponentNode | string | (ComponentNode | string)[] | null | undefined,
|
|
556
573
|
locale,
|
|
557
574
|
i18nConfig
|
|
558
575
|
);
|
|
@@ -573,7 +590,8 @@ export class ComponentBuilder {
|
|
|
573
590
|
}
|
|
574
591
|
|
|
575
592
|
// Process structure
|
|
576
|
-
const
|
|
593
|
+
const typedChildren = children as ComponentNode | ComponentNode[] | string | number | null | undefined;
|
|
594
|
+
const markedChildren = typedChildren ? markAsSlotContent(typedChildren) : undefined;
|
|
577
595
|
const processedStructure = processStructure(
|
|
578
596
|
structuredComponentDef.structure,
|
|
579
597
|
{ props: resolvedProps, componentDef: structuredComponentDef },
|