meno-core 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/build-static.test.ts +424 -0
  2. package/build-static.ts +100 -13
  3. package/lib/client/ClientInitializer.ts +4 -0
  4. package/lib/client/core/ComponentBuilder.ts +155 -16
  5. package/lib/client/core/builders/embedBuilder.ts +48 -6
  6. package/lib/client/core/builders/linkBuilder.ts +2 -2
  7. package/lib/client/core/builders/linkNodeBuilder.ts +45 -5
  8. package/lib/client/core/builders/listBuilder.ts +12 -3
  9. package/lib/client/routing/Router.tsx +8 -1
  10. package/lib/client/templateEngine.ts +89 -98
  11. package/lib/server/__integration__/api-routes.test.ts +148 -0
  12. package/lib/server/__integration__/cms-integration.test.ts +161 -0
  13. package/lib/server/__integration__/server-lifecycle.test.ts +101 -0
  14. package/lib/server/__integration__/ssr-rendering.test.ts +131 -0
  15. package/lib/server/__integration__/static-assets.test.ts +80 -0
  16. package/lib/server/__integration__/test-helpers.ts +205 -0
  17. package/lib/server/ab/generateFunctions.ts +346 -0
  18. package/lib/server/ab/trackingScript.ts +45 -0
  19. package/lib/server/index.ts +2 -2
  20. package/lib/server/jsonLoader.ts +124 -46
  21. package/lib/server/routes/api/cms.ts +3 -2
  22. package/lib/server/routes/api/components.ts +13 -2
  23. package/lib/server/services/cmsService.ts +0 -5
  24. package/lib/server/services/componentService.ts +255 -29
  25. package/lib/server/services/configService.test.ts +950 -0
  26. package/lib/server/services/configService.ts +39 -0
  27. package/lib/server/services/index.ts +1 -1
  28. package/lib/server/ssr/htmlGenerator.test.ts +992 -0
  29. package/lib/server/ssr/htmlGenerator.ts +3 -3
  30. package/lib/server/ssr/imageMetadata.test.ts +168 -0
  31. package/lib/server/ssr/imageMetadata.ts +58 -0
  32. package/lib/server/ssr/jsCollector.test.ts +287 -0
  33. package/lib/server/ssr/ssrRenderer.test.ts +3702 -0
  34. package/lib/server/ssr/ssrRenderer.ts +131 -15
  35. package/lib/shared/constants.ts +3 -0
  36. package/lib/shared/fontLoader.test.ts +335 -0
  37. package/lib/shared/i18n.test.ts +106 -0
  38. package/lib/shared/i18n.ts +17 -11
  39. package/lib/shared/index.ts +3 -0
  40. package/lib/shared/itemTemplateUtils.ts +43 -1
  41. package/lib/shared/libraryLoader.test.ts +392 -0
  42. package/lib/shared/linkUtils.ts +24 -0
  43. package/lib/shared/nodeUtils.test.ts +100 -0
  44. package/lib/shared/nodeUtils.ts +43 -0
  45. package/lib/shared/registry/NodeTypeDefinition.ts +2 -2
  46. package/lib/shared/registry/nodeTypes/ListNodeType.ts +20 -2
  47. package/lib/shared/richtext/htmlToTiptap.test.ts +948 -0
  48. package/lib/shared/richtext/htmlToTiptap.ts +46 -2
  49. package/lib/shared/richtext/tiptapToHtml.ts +65 -0
  50. package/lib/shared/richtext/types.ts +4 -1
  51. package/lib/shared/types/cms.ts +2 -0
  52. package/lib/shared/types/components.ts +12 -3
  53. package/lib/shared/types/experiments.ts +55 -0
  54. package/lib/shared/types/index.ts +10 -0
  55. package/lib/shared/utils.ts +2 -6
  56. package/lib/shared/validation/propValidator.test.ts +50 -0
  57. package/lib/shared/validation/propValidator.ts +2 -2
  58. package/lib/shared/validation/schemas.ts +10 -2
  59. package/package.json +1 -1
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Integration Tests: SSR Rendering
3
+ * Tests full page rendering pipeline through HTTP
4
+ */
5
+
6
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
7
+ import { createTestServer, fetch, TEST_PAGE_JSON } from './test-helpers';
8
+
9
+ let ctx: Awaited<ReturnType<typeof createTestServer>>;
10
+
11
+ const PAGE_WITH_META = JSON.stringify({
12
+ meta: {
13
+ title: 'SEO Page',
14
+ description: 'A page with meta description',
15
+ og: {
16
+ title: 'OG Title',
17
+ description: 'OG Description',
18
+ },
19
+ },
20
+ root: {
21
+ type: 'node',
22
+ tag: 'div',
23
+ children: [
24
+ {
25
+ type: 'node',
26
+ tag: 'h1',
27
+ children: 'SEO Content',
28
+ },
29
+ ],
30
+ },
31
+ }, null, 2);
32
+
33
+ const PAGE_WITH_I18N = JSON.stringify({
34
+ meta: {
35
+ title: { _i18n: true, en: 'English Title', pl: 'Polish Title' },
36
+ slugs: { en: 'about', pl: 'o-nas' },
37
+ },
38
+ root: {
39
+ type: 'node',
40
+ tag: 'div',
41
+ children: [
42
+ {
43
+ type: 'node',
44
+ tag: 'h1',
45
+ children: { _i18n: true, en: 'Hello', pl: 'Cześć' },
46
+ },
47
+ ],
48
+ },
49
+ }, null, 2);
50
+
51
+ beforeAll(async () => {
52
+ ctx = await createTestServer({
53
+ pages: {
54
+ '/': TEST_PAGE_JSON,
55
+ '/seo': PAGE_WITH_META,
56
+ '/about': PAGE_WITH_I18N,
57
+ },
58
+ });
59
+ });
60
+
61
+ afterAll(() => {
62
+ ctx?.cleanup();
63
+ });
64
+
65
+ describe('SSR Rendering', () => {
66
+ test('GET / returns complete HTML document', async () => {
67
+ const res = await fetch(ctx.baseUrl);
68
+ expect(res.status).toBe(200);
69
+
70
+ const html = await res.text();
71
+ expect(html).toContain('<!DOCTYPE html>');
72
+ expect(html).toContain('<html');
73
+ expect(html).toContain('</html>');
74
+ expect(html).toContain('<title>Test Page</title>');
75
+ expect(html).toContain('<div id="root">');
76
+ });
77
+
78
+ test('HTML contains rendered body content', async () => {
79
+ const res = await fetch(ctx.baseUrl);
80
+ const html = await res.text();
81
+
82
+ // The page has an h1 with "Hello World"
83
+ expect(html).toContain('Hello World');
84
+ expect(html).toContain('<h1');
85
+ });
86
+
87
+ test('HTML contains utility CSS style block', async () => {
88
+ const res = await fetch(ctx.baseUrl);
89
+ const html = await res.text();
90
+
91
+ expect(html).toContain('<style>');
92
+ // Base reset CSS should be present
93
+ expect(html).toContain('box-sizing: border-box');
94
+ });
95
+
96
+ test('HTML contains meta tags for pages with meta description', async () => {
97
+ const res = await fetch(`${ctx.baseUrl}/seo`);
98
+ const html = await res.text();
99
+
100
+ expect(html).toContain('<title>SEO Page</title>');
101
+ expect(html).toContain('A page with meta description');
102
+ });
103
+
104
+ test('page responses have text/html content-type', async () => {
105
+ const res = await fetch(ctx.baseUrl);
106
+ const contentType = res.headers.get('Content-Type');
107
+ expect(contentType).toContain('text/html');
108
+ });
109
+
110
+ test('page responses have no-store cache control', async () => {
111
+ const res = await fetch(ctx.baseUrl);
112
+ const cacheControl = res.headers.get('Cache-Control');
113
+ expect(cacheControl).toContain('no-store');
114
+ });
115
+
116
+ test('non-existent page returns HTML shell (not 404)', async () => {
117
+ // Dev server returns index HTML for unknown pages (client-side routing)
118
+ const res = await fetch(`${ctx.baseUrl}/nonexistent-page`);
119
+ // Should still return something (either HTML shell or 200)
120
+ expect(res.status).toBeLessThanOrEqual(404);
121
+ });
122
+
123
+ test('page with i18n content renders correctly', async () => {
124
+ const res = await fetch(`${ctx.baseUrl}/about`);
125
+ const html = await res.text();
126
+
127
+ // Should render with default locale content
128
+ expect(html).toContain('<html');
129
+ expect(html).toContain('</html>');
130
+ });
131
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Integration Tests: Static Asset Serving
3
+ * Tests static file serving with correct MIME types
4
+ */
5
+
6
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
7
+ import { createTestServer, fetch, createTempProject } from './test-helpers';
8
+ import { setProjectRoot } from '../projectContext';
9
+
10
+ let ctx: Awaited<ReturnType<typeof createTestServer>>;
11
+ let tempProject: ReturnType<typeof createTempProject>;
12
+ let originalProjectRoot: string;
13
+
14
+ // 1x1 transparent PNG (minimal valid PNG file)
15
+ const TINY_PNG = Buffer.from(
16
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
17
+ 'base64'
18
+ );
19
+
20
+ // Minimal WOFF2 header (just enough to test MIME type detection)
21
+ const TINY_WOFF2 = Buffer.from('wOF2', 'ascii');
22
+
23
+ beforeAll(async () => {
24
+ const { getProjectRoot } = await import('../projectContext');
25
+ originalProjectRoot = getProjectRoot();
26
+
27
+ tempProject = createTempProject({
28
+ 'images/test.png': TINY_PNG.toString('binary'),
29
+ 'fonts/test.woff2': TINY_WOFF2.toString('binary'),
30
+ 'project.config.json': JSON.stringify({}),
31
+ });
32
+
33
+ // Write binary files properly
34
+ const { writeFileSync } = await import('fs');
35
+ const { join } = await import('path');
36
+ writeFileSync(join(tempProject.projectDir, 'images/test.png'), TINY_PNG);
37
+ writeFileSync(join(tempProject.projectDir, 'fonts/test.woff2'), TINY_WOFF2);
38
+
39
+ setProjectRoot(tempProject.projectDir);
40
+
41
+ ctx = await createTestServer({
42
+ pages: {},
43
+ });
44
+ });
45
+
46
+ afterAll(() => {
47
+ ctx?.cleanup();
48
+ tempProject?.cleanup();
49
+ setProjectRoot(originalProjectRoot);
50
+ });
51
+
52
+ describe('Static Asset Serving', () => {
53
+ test('GET /images/test.png serves with image/png content-type', async () => {
54
+ const res = await fetch(`${ctx.baseUrl}/images/test.png`);
55
+ expect(res.status).toBe(200);
56
+
57
+ const contentType = res.headers.get('Content-Type');
58
+ expect(contentType).toBe('image/png');
59
+ });
60
+
61
+ test('GET /fonts/test.woff2 serves with font/woff2 content-type', async () => {
62
+ const res = await fetch(`${ctx.baseUrl}/fonts/test.woff2`);
63
+ expect(res.status).toBe(200);
64
+
65
+ const contentType = res.headers.get('Content-Type');
66
+ expect(contentType).toBe('font/woff2');
67
+ });
68
+
69
+ test('GET /images/nonexistent.png returns no response (falls through)', async () => {
70
+ const res = await fetch(`${ctx.baseUrl}/images/nonexistent.png`);
71
+ // Static handler returns undefined for missing files, so main handler returns 404
72
+ expect(res.status).toBe(404);
73
+ });
74
+
75
+ test('static assets have immutable cache headers', async () => {
76
+ const res = await fetch(`${ctx.baseUrl}/images/test.png`);
77
+ const cacheControl = res.headers.get('Cache-Control');
78
+ expect(cacheControl).toContain('immutable');
79
+ });
80
+ });
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Integration Test Helpers
3
+ * Shared utilities for server integration tests
4
+ */
5
+
6
+ import { mkdirSync, writeFileSync, rmSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { createServer, WebSocketManager, type ServerConfig } from '../createServer';
9
+ import { PageService } from '../services/pageService';
10
+ import { ComponentService } from '../services/componentService';
11
+ import { PageCache } from '../pageCache';
12
+ import {
13
+ createTypedMockPageProvider,
14
+ createTypedMockComponentProvider,
15
+ } from '../../test-utils/factories/ServerMockFactory';
16
+ import { setProjectRoot } from '../projectContext';
17
+
18
+ // Save original fetch before any test mocking can override it
19
+ const originalFetch = globalThis.fetch;
20
+
21
+ /**
22
+ * Fetch wrapper that uses the original (non-mocked) fetch
23
+ * Necessary because some unit tests mock global.fetch
24
+ */
25
+ export { originalFetch as fetch };
26
+
27
+ // Use port 0 to let OS assign random available ports
28
+
29
+ /**
30
+ * Minimal page JSON for testing
31
+ */
32
+ export const TEST_PAGE_JSON = JSON.stringify({
33
+ meta: {
34
+ title: 'Test Page',
35
+ description: 'A test page for integration tests',
36
+ },
37
+ root: {
38
+ type: 'node',
39
+ tag: 'div',
40
+ children: [
41
+ {
42
+ type: 'node',
43
+ tag: 'h1',
44
+ children: 'Hello World',
45
+ },
46
+ ],
47
+ },
48
+ }, null, 2);
49
+
50
+ /**
51
+ * Page with component reference
52
+ */
53
+ export const TEST_PAGE_WITH_COMPONENT = JSON.stringify({
54
+ meta: {
55
+ title: 'Component Page',
56
+ },
57
+ root: {
58
+ type: 'node',
59
+ tag: 'div',
60
+ children: [
61
+ {
62
+ type: 'component',
63
+ component: 'TestButton',
64
+ props: {
65
+ label: 'Click Me',
66
+ },
67
+ },
68
+ ],
69
+ },
70
+ }, null, 2);
71
+
72
+ /**
73
+ * Simple component definition
74
+ */
75
+ export const TEST_COMPONENT_DEF = {
76
+ component: {
77
+ interface: {
78
+ label: { type: 'string', default: 'Button' },
79
+ },
80
+ structure: {
81
+ type: 'node',
82
+ tag: 'button',
83
+ children: '{{label}}',
84
+ },
85
+ },
86
+ };
87
+
88
+ /**
89
+ * CMS template page JSON
90
+ */
91
+ export const TEST_CMS_TEMPLATE = JSON.stringify({
92
+ meta: {
93
+ title: '{{cms.title}}',
94
+ source: 'cms',
95
+ cms: {
96
+ id: 'posts',
97
+ name: 'Posts',
98
+ collection: 'posts',
99
+ slugField: 'slug',
100
+ urlPattern: '/blog/{{slug}}',
101
+ fields: {
102
+ title: { type: 'string', required: true },
103
+ slug: { type: 'string', required: true },
104
+ body: { type: 'string' },
105
+ },
106
+ },
107
+ },
108
+ root: {
109
+ type: 'node',
110
+ tag: 'article',
111
+ children: [
112
+ {
113
+ type: 'node',
114
+ tag: 'h1',
115
+ children: '{{cms.title}}',
116
+ },
117
+ {
118
+ type: 'node',
119
+ tag: 'p',
120
+ children: '{{cms.body}}',
121
+ },
122
+ ],
123
+ },
124
+ }, null, 2);
125
+
126
+ /**
127
+ * Create a test server with mock providers
128
+ */
129
+ export async function createTestServer(options: {
130
+ pages?: Record<string, string>;
131
+ components?: Map<string, any>;
132
+ port?: number;
133
+ cmsService?: any;
134
+ cmsProvider?: any;
135
+ } = {}) {
136
+ const port = options.port ?? 0;
137
+ const wsManager = new WebSocketManager();
138
+
139
+ const pageProvider = createTypedMockPageProvider(options.pages ?? {});
140
+ const pageCache = new PageCache();
141
+ const pageService = new PageService(pageCache, pageProvider);
142
+ await pageService.loadAllPages();
143
+
144
+ const componentProvider = createTypedMockComponentProvider(options.components ?? new Map());
145
+ const componentService = new ComponentService({
146
+ loader: {
147
+ loadDirectory: async () => options.components ?? new Map(),
148
+ loadFile: async () => null,
149
+ },
150
+ });
151
+ if (options.components) {
152
+ // Manually populate component service by using its internal methods
153
+ await componentService.loadAllComponents();
154
+ }
155
+
156
+ const config: ServerConfig = {
157
+ port,
158
+ pageService,
159
+ componentService,
160
+ wsManager,
161
+ cmsService: options.cmsService,
162
+ cmsProvider: options.cmsProvider,
163
+ };
164
+
165
+ const { server, port: actualPort } = createServer(config);
166
+ const baseUrl = `http://localhost:${actualPort}`;
167
+
168
+ return {
169
+ server,
170
+ port: actualPort,
171
+ baseUrl,
172
+ pageService,
173
+ componentService,
174
+ wsManager,
175
+ cleanup: () => {
176
+ server.stop();
177
+ },
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Create a temporary project directory with the given structure
183
+ */
184
+ export function createTempProject(files: Record<string, string> = {}) {
185
+ const projectDir = join('/tmp', `meno-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
186
+ mkdirSync(projectDir, { recursive: true });
187
+
188
+ for (const [path, content] of Object.entries(files)) {
189
+ const fullPath = join(projectDir, path);
190
+ const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
191
+ mkdirSync(dir, { recursive: true });
192
+ writeFileSync(fullPath, content);
193
+ }
194
+
195
+ return {
196
+ projectDir,
197
+ cleanup: () => {
198
+ try {
199
+ rmSync(projectDir, { recursive: true, force: true });
200
+ } catch {
201
+ // Ignore cleanup errors
202
+ }
203
+ },
204
+ };
205
+ }