cms-renderer 0.0.0 → 0.1.1
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/dist/chunk-HVKFEZBT.js +116 -0
- package/dist/chunk-HVKFEZBT.js.map +1 -0
- package/dist/chunk-JHKDRASN.js +39 -0
- package/dist/chunk-JHKDRASN.js.map +1 -0
- package/dist/chunk-RPM73PQZ.js +17 -0
- package/dist/chunk-RPM73PQZ.js.map +1 -0
- package/dist/lib/block-renderer.d.ts +32 -0
- package/dist/lib/block-renderer.js +7 -0
- package/dist/lib/block-renderer.js.map +1 -0
- package/dist/lib/cms-api.d.ts +25 -0
- package/dist/lib/cms-api.js +7 -0
- package/dist/lib/cms-api.js.map +1 -0
- package/dist/lib/data-utils.d.ts +218 -0
- package/dist/lib/data-utils.js +247 -0
- package/dist/lib/data-utils.js.map +1 -0
- package/dist/lib/image/lazy-load.d.ts +75 -0
- package/dist/lib/image/lazy-load.js +83 -0
- package/dist/lib/image/lazy-load.js.map +1 -0
- package/dist/lib/markdown-utils.d.ts +172 -0
- package/dist/lib/markdown-utils.js +137 -0
- package/dist/lib/markdown-utils.js.map +1 -0
- package/dist/lib/renderer.d.ts +40 -0
- package/dist/lib/renderer.js +371 -0
- package/dist/lib/renderer.js.map +1 -0
- package/{lib/result.ts → dist/lib/result.d.ts} +32 -146
- package/dist/lib/result.js +37 -0
- package/dist/lib/result.js.map +1 -0
- package/dist/lib/schema.d.ts +15 -0
- package/dist/lib/schema.js +35 -0
- package/dist/lib/schema.js.map +1 -0
- package/{lib/trpc.ts → dist/lib/trpc.d.ts} +6 -4
- package/dist/lib/trpc.js +7 -0
- package/dist/lib/trpc.js.map +1 -0
- package/dist/lib/types.d.ts +163 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +50 -11
- package/.turbo/turbo-check-types.log +0 -2
- package/lib/__tests__/enrich-block-images.test.ts +0 -394
- package/lib/block-renderer.tsx +0 -60
- package/lib/cms-api.ts +0 -86
- package/lib/data-utils.ts +0 -572
- package/lib/image/lazy-load.ts +0 -209
- package/lib/markdown-utils.ts +0 -368
- package/lib/renderer.tsx +0 -189
- package/lib/schema.ts +0 -74
- package/lib/types.ts +0 -201
- package/next.config.ts +0 -39
- package/postcss.config.mjs +0 -5
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as _repo_cms_schema_blocks from '@repo/cms-schema/blocks';
|
|
2
|
+
import { ComponentType } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Valid block type strings.
|
|
6
|
+
* Must match the schema_name field in block data from the tRPC API.
|
|
7
|
+
*/
|
|
8
|
+
type BlockType = 'navigation' | 'header' | 'article' | 'hero-block' | 'features-block' | 'cta-block' | 'logo-trust-block';
|
|
9
|
+
/**
|
|
10
|
+
* Navigation link button structure.
|
|
11
|
+
*/
|
|
12
|
+
interface NavigationButton {
|
|
13
|
+
label: string;
|
|
14
|
+
href: string;
|
|
15
|
+
ariaLabel: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Navigation link with type and button.
|
|
19
|
+
*/
|
|
20
|
+
interface NavigationLink {
|
|
21
|
+
button: NavigationButton;
|
|
22
|
+
type: 'Default' | 'Flyout';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Level 3 navigation item (leaf node).
|
|
26
|
+
*/
|
|
27
|
+
interface Level3Link {
|
|
28
|
+
link: NavigationLink;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Level 2 navigation item with optional Level 3 children.
|
|
32
|
+
*/
|
|
33
|
+
interface Level2Link {
|
|
34
|
+
link: NavigationLink;
|
|
35
|
+
children?: Level3Link[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Level 1 navigation item with optional Level 2 children.
|
|
39
|
+
*/
|
|
40
|
+
interface Level1Link {
|
|
41
|
+
link: NavigationLink;
|
|
42
|
+
children?: Level2Link[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Navigation block content.
|
|
46
|
+
*/
|
|
47
|
+
interface NavigationContent {
|
|
48
|
+
logo?: {
|
|
49
|
+
url: string;
|
|
50
|
+
alt: string;
|
|
51
|
+
};
|
|
52
|
+
ariaLabel: string;
|
|
53
|
+
links: Level1Link[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Header block content.
|
|
57
|
+
*/
|
|
58
|
+
interface HeaderContent {
|
|
59
|
+
headline: string;
|
|
60
|
+
subheadline?: string;
|
|
61
|
+
backgroundImage?: {
|
|
62
|
+
url: string;
|
|
63
|
+
alt: string;
|
|
64
|
+
};
|
|
65
|
+
ctaButton?: {
|
|
66
|
+
label: string;
|
|
67
|
+
href: string;
|
|
68
|
+
};
|
|
69
|
+
alignment: 'left' | 'center' | 'right';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Article block content.
|
|
73
|
+
*/
|
|
74
|
+
interface ArticleContent {
|
|
75
|
+
headline: string;
|
|
76
|
+
author?: string;
|
|
77
|
+
publishedAt?: string;
|
|
78
|
+
body: string;
|
|
79
|
+
tags?: readonly string[] | string[];
|
|
80
|
+
status?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Hero block content (from CMS schema).
|
|
84
|
+
*/
|
|
85
|
+
type HeroBlockContent = _repo_cms_schema_blocks.HeroBlockContent;
|
|
86
|
+
/**
|
|
87
|
+
* Features block content (from CMS schema).
|
|
88
|
+
*/
|
|
89
|
+
type FeaturesBlockContent = _repo_cms_schema_blocks.FeaturesBlockContent;
|
|
90
|
+
/**
|
|
91
|
+
* CTA block content (from CMS schema).
|
|
92
|
+
*/
|
|
93
|
+
type CTABlockContent = _repo_cms_schema_blocks.CTABlockContent;
|
|
94
|
+
/**
|
|
95
|
+
* Logo Trust block content (from CMS schema).
|
|
96
|
+
*/
|
|
97
|
+
type LogoTrustBlockContent = _repo_cms_schema_blocks.LogoTrustBlockContent;
|
|
98
|
+
/**
|
|
99
|
+
* Discriminated union of all block types.
|
|
100
|
+
* Use the `type` field to narrow to specific content types.
|
|
101
|
+
*
|
|
102
|
+
* Each block also carries its stable CMS `id`, which should be used as the
|
|
103
|
+
* React `key` when rendering lists of blocks.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* function renderBlock(block: BlockData) {
|
|
108
|
+
* if (block.type === 'header') {
|
|
109
|
+
* // TypeScript knows block.content is HeaderContent
|
|
110
|
+
* return <h1>{block.content.headline}</h1>;
|
|
111
|
+
* }
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
type BlockData = {
|
|
116
|
+
id: string;
|
|
117
|
+
type: 'navigation';
|
|
118
|
+
content: NavigationContent;
|
|
119
|
+
} | {
|
|
120
|
+
id: string;
|
|
121
|
+
type: 'header';
|
|
122
|
+
content: HeaderContent;
|
|
123
|
+
} | {
|
|
124
|
+
id: string;
|
|
125
|
+
type: 'article';
|
|
126
|
+
content: ArticleContent;
|
|
127
|
+
} | {
|
|
128
|
+
id: string;
|
|
129
|
+
type: 'hero-block';
|
|
130
|
+
content: HeroBlockContent;
|
|
131
|
+
} | {
|
|
132
|
+
id: string;
|
|
133
|
+
type: 'features-block';
|
|
134
|
+
content: FeaturesBlockContent;
|
|
135
|
+
} | {
|
|
136
|
+
id: string;
|
|
137
|
+
type: 'cta-block';
|
|
138
|
+
content: CTABlockContent;
|
|
139
|
+
} | {
|
|
140
|
+
id: string;
|
|
141
|
+
type: 'logo-trust-block';
|
|
142
|
+
content: LogoTrustBlockContent;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Props for a block component.
|
|
146
|
+
* Each block component receives its typed content.
|
|
147
|
+
*/
|
|
148
|
+
interface BlockComponentProps<T> {
|
|
149
|
+
content: T;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A React component that renders a specific block type.
|
|
153
|
+
*/
|
|
154
|
+
type BlockComponent<T> = ComponentType<BlockComponentProps<T>>;
|
|
155
|
+
/**
|
|
156
|
+
* Registry of block type to component mappings.
|
|
157
|
+
* This is the core of the ComponentMap pattern.
|
|
158
|
+
*/
|
|
159
|
+
type BlockComponentRegistry = {
|
|
160
|
+
[K in BlockType]: BlockComponent<K extends 'navigation' ? NavigationContent : K extends 'header' ? HeaderContent : K extends 'article' ? ArticleContent : K extends 'hero-block' ? HeroBlockContent : K extends 'features-block' ? FeaturesBlockContent : K extends 'cta-block' ? CTABlockContent : K extends 'logo-trust-block' ? LogoTrustBlockContent : never>;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type { ArticleContent, BlockComponent, BlockComponentProps, BlockComponentRegistry, BlockData, BlockType, CTABlockContent, FeaturesBlockContent, HeaderContent, HeroBlockContent, Level1Link, Level2Link, Level3Link, LogoTrustBlockContent, NavigationButton, NavigationContent, NavigationLink };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,26 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cms-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
"./
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"./
|
|
7
|
+
"./lib/block-renderer": {
|
|
8
|
+
"types": "./dist/lib/block-renderer.d.ts",
|
|
9
|
+
"import": "./dist/lib/block-renderer.js"
|
|
10
|
+
},
|
|
11
|
+
"./lib/cms-api": {
|
|
12
|
+
"types": "./dist/lib/cms-api.d.ts",
|
|
13
|
+
"import": "./dist/lib/cms-api.js"
|
|
14
|
+
},
|
|
15
|
+
"./lib/data-utils": {
|
|
16
|
+
"types": "./dist/lib/data-utils.d.ts",
|
|
17
|
+
"import": "./dist/lib/data-utils.js"
|
|
18
|
+
},
|
|
19
|
+
"./lib/markdown-utils": {
|
|
20
|
+
"types": "./dist/lib/markdown-utils.d.ts",
|
|
21
|
+
"import": "./dist/lib/markdown-utils.js"
|
|
22
|
+
},
|
|
23
|
+
"./lib/renderer": {
|
|
24
|
+
"types": "./dist/lib/renderer.d.ts",
|
|
25
|
+
"import": "./dist/lib/renderer.js"
|
|
26
|
+
},
|
|
27
|
+
"./lib/result": {
|
|
28
|
+
"types": "./dist/lib/result.d.ts",
|
|
29
|
+
"import": "./dist/lib/result.js"
|
|
30
|
+
},
|
|
31
|
+
"./lib/schema": {
|
|
32
|
+
"types": "./dist/lib/schema.d.ts",
|
|
33
|
+
"import": "./dist/lib/schema.js"
|
|
34
|
+
},
|
|
35
|
+
"./lib/trpc": {
|
|
36
|
+
"types": "./dist/lib/trpc.d.ts",
|
|
37
|
+
"import": "./dist/lib/trpc.js"
|
|
38
|
+
},
|
|
39
|
+
"./lib/types": {
|
|
40
|
+
"types": "./dist/lib/types.d.ts",
|
|
41
|
+
"import": "./dist/lib/types.js"
|
|
42
|
+
},
|
|
43
|
+
"./lib/image/lazy-load": {
|
|
44
|
+
"types": "./dist/lib/image/lazy-load.d.ts",
|
|
45
|
+
"import": "./dist/lib/image/lazy-load.js"
|
|
46
|
+
}
|
|
12
47
|
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist"
|
|
50
|
+
],
|
|
13
51
|
"publishConfig": {
|
|
14
52
|
"access": "public"
|
|
15
53
|
},
|
|
16
54
|
"scripts": {
|
|
55
|
+
"build": "tsup",
|
|
56
|
+
"prepublishOnly": "npm run build",
|
|
17
57
|
"lint": "biome check .",
|
|
18
58
|
"check-types": "tsc --noEmit",
|
|
19
|
-
"clean": "rm -rf .next .turbo node_modules"
|
|
59
|
+
"clean": "rm -rf .next .turbo node_modules dist"
|
|
20
60
|
},
|
|
21
61
|
"dependencies": {
|
|
22
|
-
"@auteur/trpc-utils": "0.0.0",
|
|
23
|
-
"@repo/supabase-utils": "0.0.0",
|
|
24
62
|
"@hookform/resolvers": "^5.1.0",
|
|
25
63
|
"@lexical/code": "0.39.0",
|
|
26
64
|
"@lexical/link": "0.39.0",
|
|
@@ -30,8 +68,6 @@
|
|
|
30
68
|
"@lexical/rich-text": "0.39.0",
|
|
31
69
|
"@lexical/selection": "0.39.0",
|
|
32
70
|
"@lexical/utils": "0.39.0",
|
|
33
|
-
"@repo/cms-schema": "0.0.0",
|
|
34
|
-
"@repo/markdown-wasm": "0.0.0",
|
|
35
71
|
"@supabase/ssr": "0.8.0",
|
|
36
72
|
"@supabase/supabase-js": "2.90.1",
|
|
37
73
|
"@tanstack/react-query": "5.90.16",
|
|
@@ -39,6 +75,7 @@
|
|
|
39
75
|
"@trpc/client": "11.8.1",
|
|
40
76
|
"@trpc/react-query": "11.8.1",
|
|
41
77
|
"@trpc/server": "11.8.1",
|
|
78
|
+
"md4w": "^0.2.7",
|
|
42
79
|
"next": "^16.1.1",
|
|
43
80
|
"react": "^19.1.0",
|
|
44
81
|
"react-dom": "^19.1.0",
|
|
@@ -47,7 +84,9 @@
|
|
|
47
84
|
},
|
|
48
85
|
"devDependencies": {
|
|
49
86
|
"@happy-dom/global-registrator": "20.1.0",
|
|
87
|
+
"@repo/cms-schema": "0.0.0",
|
|
50
88
|
"@repo/typescript-config": "0.0.0",
|
|
89
|
+
"tsup": "^8.3.5",
|
|
51
90
|
"@tailwindcss/postcss": "4",
|
|
52
91
|
"@testing-library/jest-dom": "6.9.1",
|
|
53
92
|
"@types/node": "^25",
|
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
import { beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// Mocks - Must be set up BEFORE any imports that use these modules
|
|
5
|
-
// =============================================================================
|
|
6
|
-
|
|
7
|
-
// Mock 'server-only' to allow running in test environment
|
|
8
|
-
// This must be called before the module that imports it is loaded
|
|
9
|
-
mock.module('server-only', () => ({}));
|
|
10
|
-
|
|
11
|
-
// Mock generateSignedImageData to return predictable signed data
|
|
12
|
-
const mockGenerateSignedImageData = mock(
|
|
13
|
-
(config: { assetId: string; width: number; height: number; url: string }) => ({
|
|
14
|
-
signedSrcset: `signed-srcset-${config.assetId}`,
|
|
15
|
-
signedFallbackSrcset: `signed-fallback-srcset-${config.assetId}`,
|
|
16
|
-
signedDefaultSrc: `signed-default-src-${config.assetId}`,
|
|
17
|
-
})
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
mock.module('../image-transforms.server', () => ({
|
|
21
|
-
generateSignedImageData: mockGenerateSignedImageData,
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// Dynamic Import - Must happen after mocks are registered
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
type EnrichFunction = <T extends Record<string, unknown>>(content: T | null) => T | null;
|
|
29
|
-
let enrichBlockWithSignedUrls: EnrichFunction;
|
|
30
|
-
|
|
31
|
-
beforeAll(async () => {
|
|
32
|
-
const mod = await import('../enrich-block-images');
|
|
33
|
-
enrichBlockWithSignedUrls = mod.enrichBlockWithSignedUrls;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// =============================================================================
|
|
37
|
-
// Test Data
|
|
38
|
-
// =============================================================================
|
|
39
|
-
|
|
40
|
-
const createResolvedImageRef = (id: string = '12345678-1234-1234-1234-123456789abc') => ({
|
|
41
|
-
alt: 'Test image',
|
|
42
|
-
_asset: {
|
|
43
|
-
id,
|
|
44
|
-
url: '/api/dev/storage/test.jpg',
|
|
45
|
-
width: 1920,
|
|
46
|
-
height: 1080,
|
|
47
|
-
lqip: 'data:image/jpeg;base64,/9j/...',
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// =============================================================================
|
|
52
|
-
// Tests
|
|
53
|
-
// =============================================================================
|
|
54
|
-
|
|
55
|
-
describe('enrichBlockWithSignedUrls', () => {
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
mockGenerateSignedImageData.mockClear();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('Null/Undefined Handling', () => {
|
|
61
|
-
it('returns null when content is null', () => {
|
|
62
|
-
const result = enrichBlockWithSignedUrls(null);
|
|
63
|
-
expect(result).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns content unchanged when it has no image references', () => {
|
|
67
|
-
const content = { title: 'Hello', count: 42 };
|
|
68
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
69
|
-
expect(result).toEqual(content);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe('Image Reference Detection', () => {
|
|
74
|
-
it('detects resolved image reference with alt and _asset', () => {
|
|
75
|
-
const imageRef = createResolvedImageRef();
|
|
76
|
-
const content = { backgroundImage: imageRef };
|
|
77
|
-
|
|
78
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
79
|
-
|
|
80
|
-
expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(1);
|
|
81
|
-
expect(result?.backgroundImage._signed).toBeDefined();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('ignores objects missing _asset.id', () => {
|
|
85
|
-
const content = {
|
|
86
|
-
notAnImage: {
|
|
87
|
-
alt: 'Test',
|
|
88
|
-
_asset: {
|
|
89
|
-
url: '/test.jpg',
|
|
90
|
-
width: 1920,
|
|
91
|
-
height: 1080,
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
97
|
-
|
|
98
|
-
expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
|
|
99
|
-
expect((result?.notAnImage as Record<string, unknown>)._signed).toBeUndefined();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('ignores objects missing alt', () => {
|
|
103
|
-
const content = {
|
|
104
|
-
notAnImage: {
|
|
105
|
-
_asset: {
|
|
106
|
-
id: '12345678-1234-1234-1234-123456789abc',
|
|
107
|
-
url: '/test.jpg',
|
|
108
|
-
width: 1920,
|
|
109
|
-
height: 1080,
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
115
|
-
|
|
116
|
-
expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
|
|
117
|
-
expect((result?.notAnImage as Record<string, unknown>)._signed).toBeUndefined();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('ignores objects where _asset is null', () => {
|
|
121
|
-
const content = {
|
|
122
|
-
notAnImage: {
|
|
123
|
-
alt: 'Test',
|
|
124
|
-
_asset: null,
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
enrichBlockWithSignedUrls(content);
|
|
129
|
-
|
|
130
|
-
expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('ignores objects where _asset is missing id', () => {
|
|
134
|
-
const content = {
|
|
135
|
-
notAnImage: {
|
|
136
|
-
alt: 'Test',
|
|
137
|
-
_asset: {
|
|
138
|
-
url: '/test.jpg',
|
|
139
|
-
width: 1920,
|
|
140
|
-
height: 1080,
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
enrichBlockWithSignedUrls(content);
|
|
146
|
-
|
|
147
|
-
expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('ignores objects where _asset is missing url', () => {
|
|
151
|
-
const content = {
|
|
152
|
-
notAnImage: {
|
|
153
|
-
alt: 'Test',
|
|
154
|
-
_asset: {
|
|
155
|
-
id: '12345678-1234-1234-1234-123456789abc',
|
|
156
|
-
width: 1920,
|
|
157
|
-
height: 1080,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
enrichBlockWithSignedUrls(content);
|
|
163
|
-
|
|
164
|
-
expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('ignores objects where _asset is missing width', () => {
|
|
168
|
-
const content = {
|
|
169
|
-
notAnImage: {
|
|
170
|
-
alt: 'Test',
|
|
171
|
-
_asset: {
|
|
172
|
-
id: '12345678-1234-1234-1234-123456789abc',
|
|
173
|
-
url: '/test.jpg',
|
|
174
|
-
height: 1080,
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
enrichBlockWithSignedUrls(content);
|
|
180
|
-
|
|
181
|
-
expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('ignores objects where _asset is missing height', () => {
|
|
185
|
-
const content = {
|
|
186
|
-
notAnImage: {
|
|
187
|
-
alt: 'Test',
|
|
188
|
-
_asset: {
|
|
189
|
-
id: '12345678-1234-1234-1234-123456789abc',
|
|
190
|
-
url: '/test.jpg',
|
|
191
|
-
width: 1920,
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
enrichBlockWithSignedUrls(content);
|
|
197
|
-
|
|
198
|
-
expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('requires _asset.id to be a 36-character UUID', () => {
|
|
202
|
-
const content = {
|
|
203
|
-
notAnImage: {
|
|
204
|
-
alt: 'Test',
|
|
205
|
-
_asset: {
|
|
206
|
-
id: 'short-id', // Not 36 characters
|
|
207
|
-
url: '/test.jpg',
|
|
208
|
-
width: 1920,
|
|
209
|
-
height: 1080,
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
enrichBlockWithSignedUrls(content);
|
|
215
|
-
|
|
216
|
-
expect(mockGenerateSignedImageData).not.toHaveBeenCalled();
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe('Recursive Traversal', () => {
|
|
221
|
-
it('traverses nested objects', () => {
|
|
222
|
-
const imageRef = createResolvedImageRef();
|
|
223
|
-
const content = {
|
|
224
|
-
section: {
|
|
225
|
-
hero: {
|
|
226
|
-
backgroundImage: imageRef,
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
232
|
-
|
|
233
|
-
expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(1);
|
|
234
|
-
expect(result?.section.hero.backgroundImage._signed).toBeDefined();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('traverses arrays', () => {
|
|
238
|
-
const imageRef1 = createResolvedImageRef('11111111-1111-1111-1111-111111111111');
|
|
239
|
-
const imageRef2 = createResolvedImageRef('22222222-2222-2222-2222-222222222222');
|
|
240
|
-
|
|
241
|
-
const content = {
|
|
242
|
-
images: [imageRef1, imageRef2],
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
246
|
-
|
|
247
|
-
expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(2);
|
|
248
|
-
expect(result?.images[0]._signed).toBeDefined();
|
|
249
|
-
expect(result?.images[1]._signed).toBeDefined();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('traverses deeply nested arrays within objects', () => {
|
|
253
|
-
const imageRef = createResolvedImageRef();
|
|
254
|
-
const content = {
|
|
255
|
-
sections: [
|
|
256
|
-
{
|
|
257
|
-
cards: [
|
|
258
|
-
{
|
|
259
|
-
thumbnail: imageRef,
|
|
260
|
-
},
|
|
261
|
-
],
|
|
262
|
-
},
|
|
263
|
-
],
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
267
|
-
|
|
268
|
-
expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(1);
|
|
269
|
-
expect(result?.sections[0].cards[0].thumbnail._signed).toBeDefined();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('handles multiple image references at different levels', () => {
|
|
273
|
-
const heroImage = createResolvedImageRef('11111111-1111-1111-1111-111111111111');
|
|
274
|
-
const thumbnailImage = createResolvedImageRef('22222222-2222-2222-2222-222222222222');
|
|
275
|
-
|
|
276
|
-
const content = {
|
|
277
|
-
hero: heroImage,
|
|
278
|
-
sidebar: {
|
|
279
|
-
thumbnail: thumbnailImage,
|
|
280
|
-
},
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
284
|
-
|
|
285
|
-
expect(mockGenerateSignedImageData).toHaveBeenCalledTimes(2);
|
|
286
|
-
expect(result?.hero._signed).toBeDefined();
|
|
287
|
-
expect(result?.sidebar.thumbnail._signed).toBeDefined();
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
describe('Signed URL Data', () => {
|
|
292
|
-
it('adds _signed with signedSrcset, signedFallbackSrcset, and signedDefaultSrc', () => {
|
|
293
|
-
const imageRef = createResolvedImageRef();
|
|
294
|
-
const content = { image: imageRef };
|
|
295
|
-
|
|
296
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
297
|
-
|
|
298
|
-
expect(result?.image._signed).toEqual({
|
|
299
|
-
signedSrcset: 'signed-srcset-asset-123',
|
|
300
|
-
signedFallbackSrcset: 'signed-fallback-srcset-asset-123',
|
|
301
|
-
signedDefaultSrc: 'signed-default-src-asset-123',
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('passes correct config to generateSignedImageData', () => {
|
|
306
|
-
const imageRef = createResolvedImageRef();
|
|
307
|
-
const content = { image: imageRef };
|
|
308
|
-
|
|
309
|
-
enrichBlockWithSignedUrls(content);
|
|
310
|
-
|
|
311
|
-
expect(mockGenerateSignedImageData).toHaveBeenCalledWith(
|
|
312
|
-
{
|
|
313
|
-
assetId: 'asset-123',
|
|
314
|
-
width: 1920,
|
|
315
|
-
height: 1080,
|
|
316
|
-
url: '/api/dev/storage/test.jpg',
|
|
317
|
-
},
|
|
318
|
-
expect.objectContaining({})
|
|
319
|
-
);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it('passes focalPoint option when transformation includes hx/hy', () => {
|
|
323
|
-
const imageRef = {
|
|
324
|
-
...createResolvedImageRef(),
|
|
325
|
-
_asset: {
|
|
326
|
-
...createResolvedImageRef()._asset,
|
|
327
|
-
transformation: 'hx=0.3&hy=0.7',
|
|
328
|
-
},
|
|
329
|
-
};
|
|
330
|
-
const content = { image: imageRef };
|
|
331
|
-
|
|
332
|
-
enrichBlockWithSignedUrls(content);
|
|
333
|
-
|
|
334
|
-
expect(mockGenerateSignedImageData).toHaveBeenCalledWith(expect.objectContaining({}), {
|
|
335
|
-
focalPoint: { x: 0.3, y: 0.7 },
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
describe('Immutability', () => {
|
|
341
|
-
it('does not mutate the original object', () => {
|
|
342
|
-
const imageRef = createResolvedImageRef();
|
|
343
|
-
const content = { image: imageRef };
|
|
344
|
-
const originalContent = JSON.stringify(content);
|
|
345
|
-
|
|
346
|
-
enrichBlockWithSignedUrls(content);
|
|
347
|
-
|
|
348
|
-
expect(JSON.stringify(content)).toBe(originalContent);
|
|
349
|
-
expect((content.image as Record<string, unknown>)._signed).toBeUndefined();
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('returns a new object with signed data', () => {
|
|
353
|
-
const imageRef = createResolvedImageRef();
|
|
354
|
-
const content = { image: imageRef };
|
|
355
|
-
|
|
356
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
357
|
-
|
|
358
|
-
expect(result).not.toBe(content);
|
|
359
|
-
expect(result?.image).not.toBe(content.image);
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
describe('Primitive Values', () => {
|
|
364
|
-
it('preserves primitive values', () => {
|
|
365
|
-
const content = {
|
|
366
|
-
title: 'Hello World',
|
|
367
|
-
count: 42,
|
|
368
|
-
enabled: true,
|
|
369
|
-
ratio: 3.14,
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
373
|
-
|
|
374
|
-
expect(result).toEqual(content);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('preserves nested primitive values alongside images', () => {
|
|
378
|
-
const imageRef = createResolvedImageRef();
|
|
379
|
-
const content = {
|
|
380
|
-
title: 'Hero Section',
|
|
381
|
-
order: 1,
|
|
382
|
-
visible: true,
|
|
383
|
-
image: imageRef,
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const result = enrichBlockWithSignedUrls(content);
|
|
387
|
-
|
|
388
|
-
expect(result?.title).toBe('Hero Section');
|
|
389
|
-
expect(result?.order).toBe(1);
|
|
390
|
-
expect(result?.visible).toBe(true);
|
|
391
|
-
expect(result?.image._signed).toBeDefined();
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
});
|