@orion-studios/payload-studio 0.5.0-beta.7 → 0.5.0-beta.70
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/README.md +20 -0
- package/dist/admin/client.d.mts +2 -0
- package/dist/admin/client.d.ts +2 -0
- package/dist/admin/client.js +779 -137
- package/dist/admin/client.mjs +769 -129
- package/dist/admin/index.d.mts +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.js +100 -8
- package/dist/admin/index.mjs +3 -1
- package/dist/admin-app/client.d.mts +7 -0
- package/dist/admin-app/client.d.ts +7 -0
- package/dist/admin-app/client.js +1262 -3
- package/dist/admin-app/client.mjs +1164 -2
- package/dist/admin-app/index.d.mts +1 -1
- package/dist/admin-app/index.d.ts +1 -1
- package/dist/admin-app/index.js +167 -0
- package/dist/admin-app/index.mjs +13 -1
- package/dist/admin-app/styles.css +229 -0
- package/dist/blocks/index.js +633 -8
- package/dist/blocks/index.mjs +2 -2
- package/dist/{chunk-ZLLNO5FM.mjs → chunk-CKX5Y2HU.mjs} +44 -13
- package/dist/{chunk-J7W5EE3B.mjs → chunk-HCEPGEAI.mjs} +100 -8
- package/dist/chunk-HXGAG6I7.mjs +325 -0
- package/dist/chunk-ROTPP5CU.mjs +99 -0
- package/dist/{chunk-ETRRXURT.mjs → chunk-SIL2J5MF.mjs} +14 -0
- package/dist/chunk-VDGSMD6H.mjs +1072 -0
- package/dist/{chunk-PC5622T7.mjs → chunk-XK3K5GRP.mjs} +620 -9
- package/dist/chunk-XVH5SCBD.mjs +234 -0
- package/dist/{index-CmR6NInu.d.ts → index-BIwu3qIH.d.mts} +29 -3
- package/dist/{index-CmR6NInu.d.mts → index-BIwu3qIH.d.ts} +29 -3
- package/dist/index-CdnUNrvX.d.mts +134 -0
- package/dist/{index-DbH0Ljwp.d.ts → index-CpG3UHcS.d.mts} +1 -0
- package/dist/{index-DbH0Ljwp.d.mts → index-CpG3UHcS.d.ts} +1 -0
- package/dist/index-DyHbWliW.d.ts +134 -0
- package/dist/index-ZbOx4OCF.d.mts +128 -0
- package/dist/index-ZbOx4OCF.d.ts +128 -0
- package/dist/{index-DJFhANvJ.d.mts → index-cDYkEj29.d.mts} +20 -2
- package/dist/{index-DJFhANvJ.d.ts → index-cDYkEj29.d.ts} +20 -2
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2145 -305
- package/dist/index.mjs +9 -9
- package/dist/nextjs/index.d.mts +1 -1
- package/dist/nextjs/index.d.ts +1 -1
- package/dist/nextjs/index.js +996 -13
- package/dist/nextjs/index.mjs +4 -1
- package/dist/studio/index.d.mts +2 -1
- package/dist/studio/index.d.ts +2 -1
- package/dist/studio/index.js +171 -2
- package/dist/studio/index.mjs +7 -3
- package/dist/studio-pages/builder.css +358 -8
- package/dist/studio-pages/client.d.mts +17 -0
- package/dist/studio-pages/client.d.ts +17 -0
- package/dist/studio-pages/client.js +5657 -1478
- package/dist/studio-pages/client.mjs +5549 -1461
- package/dist/studio-pages/index.d.mts +3 -2
- package/dist/studio-pages/index.d.ts +3 -2
- package/dist/studio-pages/index.js +799 -29
- package/dist/studio-pages/index.mjs +6 -2
- package/package.json +26 -12
- package/dist/chunk-AAOHJDNS.mjs +0 -67
- package/dist/chunk-N67KVM2S.mjs +0 -156
- package/dist/chunk-UJFU323N.mjs +0 -301
- package/dist/index-B9N5MyjF.d.mts +0 -39
- package/dist/index-BallJs-K.d.mts +0 -43
- package/dist/index-BallJs-K.d.ts +0 -43
- package/dist/index-g8tBHLKD.d.ts +0 -39
package/dist/blocks/index.mjs
CHANGED
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
defaultPageLayoutBlocks,
|
|
18
18
|
sectionPresets,
|
|
19
19
|
templateStarterPresets
|
|
20
|
-
} from "../chunk-
|
|
21
|
-
import "../chunk-
|
|
20
|
+
} from "../chunk-XK3K5GRP.mjs";
|
|
21
|
+
import "../chunk-SIL2J5MF.mjs";
|
|
22
22
|
import "../chunk-6BWS3CLP.mjs";
|
|
23
23
|
export {
|
|
24
24
|
BeforeAfterBlock,
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertStudioDocumentV1
|
|
3
|
+
} from "./chunk-HXGAG6I7.mjs";
|
|
4
|
+
import {
|
|
5
|
+
studioDocumentToLayout
|
|
6
|
+
} from "./chunk-VDGSMD6H.mjs";
|
|
1
7
|
import {
|
|
2
8
|
__export
|
|
3
9
|
} from "./chunk-6BWS3CLP.mjs";
|
|
@@ -27,6 +33,24 @@ function createPayloadClient(config) {
|
|
|
27
33
|
|
|
28
34
|
// src/nextjs/queries/pages.ts
|
|
29
35
|
import { unstable_cache } from "next/cache";
|
|
36
|
+
var PAGE_QUERY_CACHE_VERSION = "v4-published-only-public";
|
|
37
|
+
function withStudioDocumentLayout(page) {
|
|
38
|
+
if (!page) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const studioDocument = assertStudioDocumentV1(page.studioDocument);
|
|
43
|
+
const compiledLayout = studioDocumentToLayout(studioDocument);
|
|
44
|
+
if (Array.isArray(compiledLayout) && compiledLayout.length > 0) {
|
|
45
|
+
return {
|
|
46
|
+
...page,
|
|
47
|
+
layout: compiledLayout
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
return page;
|
|
53
|
+
}
|
|
30
54
|
function normalizePath(segments) {
|
|
31
55
|
if (!segments || segments.length === 0) {
|
|
32
56
|
return "/";
|
|
@@ -35,35 +59,42 @@ function normalizePath(segments) {
|
|
|
35
59
|
return cleaned.length > 0 ? `/${cleaned}` : "/";
|
|
36
60
|
}
|
|
37
61
|
async function queryPageByPath(payload, path, draft) {
|
|
62
|
+
const pathWhere = {
|
|
63
|
+
path: {
|
|
64
|
+
equals: path
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const publishedWhere = {
|
|
68
|
+
_status: {
|
|
69
|
+
equals: "published"
|
|
70
|
+
}
|
|
71
|
+
};
|
|
38
72
|
const result = await payload.find({
|
|
39
73
|
collection: "pages",
|
|
40
74
|
depth: 2,
|
|
41
75
|
draft,
|
|
42
76
|
limit: 1,
|
|
43
77
|
overrideAccess: false,
|
|
44
|
-
where: {
|
|
45
|
-
path: {
|
|
46
|
-
equals: path
|
|
47
|
-
}
|
|
48
|
-
}
|
|
78
|
+
where: draft ? pathWhere : { and: [pathWhere, publishedWhere] }
|
|
49
79
|
});
|
|
50
80
|
if (result.docs.length > 0) {
|
|
51
|
-
return result.docs[0] || null;
|
|
81
|
+
return withStudioDocumentLayout(result.docs[0] || null);
|
|
52
82
|
}
|
|
53
83
|
if (path === "/") {
|
|
84
|
+
const homeWhere = {
|
|
85
|
+
slug: {
|
|
86
|
+
equals: "home"
|
|
87
|
+
}
|
|
88
|
+
};
|
|
54
89
|
const homeResult = await payload.find({
|
|
55
90
|
collection: "pages",
|
|
56
91
|
depth: 2,
|
|
57
92
|
draft,
|
|
58
93
|
limit: 1,
|
|
59
94
|
overrideAccess: false,
|
|
60
|
-
where: {
|
|
61
|
-
slug: {
|
|
62
|
-
equals: "home"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
95
|
+
where: draft ? homeWhere : { and: [homeWhere, publishedWhere] }
|
|
65
96
|
});
|
|
66
|
-
return homeResult.docs[0] || null;
|
|
97
|
+
return withStudioDocumentLayout(homeResult.docs[0] || null);
|
|
67
98
|
}
|
|
68
99
|
return null;
|
|
69
100
|
}
|
|
@@ -73,7 +104,7 @@ function createPageQueries(getPayloadClient, contentTag = "website-content") {
|
|
|
73
104
|
const payload = await getPayloadClient();
|
|
74
105
|
return queryPageByPath(payload, path, false);
|
|
75
106
|
},
|
|
76
|
-
["page-by-path"],
|
|
107
|
+
["page-by-path", PAGE_QUERY_CACHE_VERSION],
|
|
77
108
|
{ tags: [contentTag] }
|
|
78
109
|
);
|
|
79
110
|
async function getPageBySegments(segments, draft = false) {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
var admin_exports = {};
|
|
8
8
|
__export(admin_exports, {
|
|
9
9
|
configureAdmin: () => configureAdmin,
|
|
10
|
+
createHeaderNavItemsField: () => createHeaderNavItemsField,
|
|
10
11
|
createThemePreferenceField: () => createThemePreferenceField,
|
|
11
12
|
themePreferenceField: () => themePreferenceField,
|
|
12
13
|
withTooltips: () => withTooltips
|
|
@@ -61,14 +62,25 @@ function configureAdmin(config) {
|
|
|
61
62
|
defaultTheme = "brand-light",
|
|
62
63
|
logoUrl
|
|
63
64
|
} = config;
|
|
64
|
-
const studioEnabled =
|
|
65
|
+
const studioEnabled = config.studio?.enabled ?? true;
|
|
65
66
|
const pagesCollectionSlug = config.studio?.pages?.collectionSlug || "pages";
|
|
66
67
|
const mediaCollectionSlug = config.studio?.media?.collectionSlug || "media";
|
|
67
|
-
const
|
|
68
|
+
const contactFormStudioPath = "/studio-contact-form";
|
|
69
|
+
const configuredGlobals = config.studio?.globals || [
|
|
68
70
|
{ slug: "site-settings", label: "Website Settings" },
|
|
69
71
|
{ slug: "header", label: "Header & Navigation" },
|
|
70
|
-
{ slug: "footer", label: "Footer" }
|
|
72
|
+
{ slug: "footer", label: "Footer" },
|
|
73
|
+
{ slug: "contact-form", label: "Contact Form" }
|
|
71
74
|
];
|
|
75
|
+
const globals = configuredGlobals.map((global) => {
|
|
76
|
+
if (global.slug !== "contact-form" || global.href) {
|
|
77
|
+
return global;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...global,
|
|
81
|
+
href: contactFormStudioPath
|
|
82
|
+
};
|
|
83
|
+
});
|
|
72
84
|
let cssPath;
|
|
73
85
|
const pkgDist = getPkgDistDir();
|
|
74
86
|
const sourceCssPath = path.resolve(pkgDist, "admin.css");
|
|
@@ -98,7 +110,8 @@ function configureAdmin(config) {
|
|
|
98
110
|
clientProps: {
|
|
99
111
|
brandName,
|
|
100
112
|
logoUrl,
|
|
101
|
-
globalsBasePath: "/
|
|
113
|
+
globalsBasePath: "/studio-globals",
|
|
114
|
+
globalsExtraMatchPrefixes: [contactFormStudioPath],
|
|
102
115
|
mediaCollectionSlug,
|
|
103
116
|
pagesCollectionSlug
|
|
104
117
|
}
|
|
@@ -130,7 +143,8 @@ function configureAdmin(config) {
|
|
|
130
143
|
clientProps: {
|
|
131
144
|
brandName,
|
|
132
145
|
logoUrl,
|
|
133
|
-
globalsBasePath: "/
|
|
146
|
+
globalsBasePath: "/studio-globals",
|
|
147
|
+
globalsExtraMatchPrefixes: [contactFormStudioPath],
|
|
134
148
|
mediaCollectionSlug,
|
|
135
149
|
pagesCollectionSlug
|
|
136
150
|
}
|
|
@@ -144,7 +158,18 @@ function configureAdmin(config) {
|
|
|
144
158
|
path: clientPath,
|
|
145
159
|
clientProps: {
|
|
146
160
|
globals,
|
|
147
|
-
globalsBasePath: "/
|
|
161
|
+
globalsBasePath: "/studio-globals"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
studioContactForm: {
|
|
166
|
+
path: "/studio-contact-form",
|
|
167
|
+
Component: {
|
|
168
|
+
exportName: "AdminStudioContactFormView",
|
|
169
|
+
path: clientPath,
|
|
170
|
+
clientProps: {
|
|
171
|
+
globalSlug: "contact-form",
|
|
172
|
+
globalsBasePath: "/studio-globals"
|
|
148
173
|
}
|
|
149
174
|
}
|
|
150
175
|
}
|
|
@@ -191,16 +216,47 @@ function configureAdmin(config) {
|
|
|
191
216
|
const labelMap = {
|
|
192
217
|
header: { group: "Site Design", label: "Header & Navigation" },
|
|
193
218
|
footer: { group: "Site Design", label: "Footer" },
|
|
194
|
-
"site-settings": { group: "Site Design", label: "Website Settings" }
|
|
219
|
+
"site-settings": { group: "Site Design", label: "Website Settings" },
|
|
220
|
+
"contact-form": { group: "Lead Forms", label: "Contact Form" }
|
|
195
221
|
};
|
|
196
222
|
return globals2.map((global) => {
|
|
197
223
|
const mapping = labelMap[global.slug];
|
|
198
224
|
if (!mapping) return global;
|
|
225
|
+
const shouldAttachContactFormRedirect = studioEnabled && global.slug === "contact-form";
|
|
226
|
+
const existingViews = global.admin?.components?.views;
|
|
227
|
+
const existingEditViews = existingViews?.edit;
|
|
228
|
+
const hasCustomContactFormEditView = Boolean(
|
|
229
|
+
existingEditViews?.root || existingEditViews?.default && typeof existingEditViews.default === "object" && existingEditViews.default.Component
|
|
230
|
+
);
|
|
231
|
+
const contactFormEditViews = shouldAttachContactFormRedirect && !hasCustomContactFormEditView ? {
|
|
232
|
+
...existingEditViews,
|
|
233
|
+
default: {
|
|
234
|
+
...typeof existingEditViews?.default === "object" ? existingEditViews.default : {},
|
|
235
|
+
Component: {
|
|
236
|
+
exportName: "StudioContactFormRedirect",
|
|
237
|
+
path: clientPath,
|
|
238
|
+
clientProps: {
|
|
239
|
+
studioContactFormPath: contactFormStudioPath
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} : existingEditViews;
|
|
199
244
|
return {
|
|
200
245
|
...global,
|
|
201
246
|
admin: {
|
|
202
247
|
...global.admin,
|
|
203
|
-
group: mapping.group
|
|
248
|
+
group: mapping.group,
|
|
249
|
+
components: {
|
|
250
|
+
...global.admin?.components,
|
|
251
|
+
...shouldAttachContactFormRedirect ? {
|
|
252
|
+
views: {
|
|
253
|
+
...existingViews,
|
|
254
|
+
...contactFormEditViews ? {
|
|
255
|
+
edit: contactFormEditViews
|
|
256
|
+
} : {}
|
|
257
|
+
}
|
|
258
|
+
} : {}
|
|
259
|
+
}
|
|
204
260
|
},
|
|
205
261
|
label: mapping.label
|
|
206
262
|
};
|
|
@@ -269,10 +325,46 @@ function addTooltipToField(field, tooltips) {
|
|
|
269
325
|
return field;
|
|
270
326
|
}
|
|
271
327
|
|
|
328
|
+
// src/admin/fields/headerNav.ts
|
|
329
|
+
var createHeaderNavItemsField = () => ({
|
|
330
|
+
name: "navItems",
|
|
331
|
+
type: "array",
|
|
332
|
+
labels: { singular: "Navigation Link", plural: "Navigation Links" },
|
|
333
|
+
admin: {
|
|
334
|
+
description: "The links displayed in your website's main navigation menu."
|
|
335
|
+
},
|
|
336
|
+
fields: [
|
|
337
|
+
{
|
|
338
|
+
name: "label",
|
|
339
|
+
type: "text",
|
|
340
|
+
required: true,
|
|
341
|
+
admin: {
|
|
342
|
+
description: "The text shown for this navigation link."
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "href",
|
|
347
|
+
type: "text",
|
|
348
|
+
required: true,
|
|
349
|
+
admin: {
|
|
350
|
+
description: 'The URL this link points to (e.g., "/about" or "https://example.com").'
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "parentHref",
|
|
355
|
+
type: "text",
|
|
356
|
+
admin: {
|
|
357
|
+
description: "Optional parent link URL. If set to another nav item href, this item appears in that dropdown."
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
});
|
|
362
|
+
|
|
272
363
|
export {
|
|
273
364
|
createThemePreferenceField,
|
|
274
365
|
themePreferenceField,
|
|
275
366
|
configureAdmin,
|
|
276
367
|
withTooltips,
|
|
368
|
+
createHeaderNavItemsField,
|
|
277
369
|
admin_exports
|
|
278
370
|
};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__export
|
|
3
|
+
} from "./chunk-6BWS3CLP.mjs";
|
|
4
|
+
|
|
5
|
+
// src/studio/index.ts
|
|
6
|
+
var studio_exports = {};
|
|
7
|
+
__export(studio_exports, {
|
|
8
|
+
assertStudioDocumentV1: () => assertStudioDocumentV1,
|
|
9
|
+
compileStudioDocument: () => compileStudioDocument,
|
|
10
|
+
createEmptyStudioDocument: () => createEmptyStudioDocument,
|
|
11
|
+
createImageUploadOptimizationHook: () => createImageUploadOptimizationHook,
|
|
12
|
+
createStudioRegistry: () => createStudioRegistry,
|
|
13
|
+
migrateStudioDocument: () => migrateStudioDocument,
|
|
14
|
+
validateStudioDocument: () => validateStudioDocument,
|
|
15
|
+
withImageUploadOptimization: () => withImageUploadOptimization
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// src/studio/imageUploadOptimization.ts
|
|
19
|
+
import { readFile, unlink } from "fs/promises";
|
|
20
|
+
var DEFAULT_SUPPORTED_MIME_TYPES = [
|
|
21
|
+
"image/jpeg",
|
|
22
|
+
"image/jpg",
|
|
23
|
+
"image/png",
|
|
24
|
+
"image/webp",
|
|
25
|
+
"image/avif",
|
|
26
|
+
"image/tiff"
|
|
27
|
+
];
|
|
28
|
+
var DEFAULT_OPTIONS = {
|
|
29
|
+
avifQuality: 50,
|
|
30
|
+
enforceSmallerForLossy: true,
|
|
31
|
+
jpegQuality: 78,
|
|
32
|
+
minBytes: 48 * 1024,
|
|
33
|
+
minQualityFloor: 42,
|
|
34
|
+
onlyIfSmaller: true,
|
|
35
|
+
pngCompressionLevel: 9,
|
|
36
|
+
skipAnimated: true,
|
|
37
|
+
tiffQuality: 75,
|
|
38
|
+
webpQuality: 78
|
|
39
|
+
};
|
|
40
|
+
var clamp = (value, min, max) => Math.max(min, Math.min(max, Math.round(value)));
|
|
41
|
+
var isUploadMutationOperation = (operation) => operation === "create" || operation === "update" || operation === "updateByID";
|
|
42
|
+
var isLossyMimeType = (mimetype) => mimetype === "image/jpeg" || mimetype === "image/jpg" || mimetype === "image/webp" || mimetype === "image/avif" || mimetype === "image/tiff";
|
|
43
|
+
var readIncomingBuffer = async (file) => {
|
|
44
|
+
if (file.tempFilePath) {
|
|
45
|
+
return readFile(file.tempFilePath);
|
|
46
|
+
}
|
|
47
|
+
return file.data;
|
|
48
|
+
};
|
|
49
|
+
var writeOptimizedBuffer = async (file, buffer) => {
|
|
50
|
+
if (file.tempFilePath) {
|
|
51
|
+
await unlink(file.tempFilePath).catch(() => void 0);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...file,
|
|
55
|
+
data: buffer,
|
|
56
|
+
size: buffer.length,
|
|
57
|
+
tempFilePath: void 0
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
var createImageUploadOptimizationHook = (options = {}) => {
|
|
61
|
+
const supportedMimeTypes = new Set(options.supportedMimeTypes || DEFAULT_SUPPORTED_MIME_TYPES);
|
|
62
|
+
const settings = { ...DEFAULT_OPTIONS, ...options };
|
|
63
|
+
return async ({ operation, req }) => {
|
|
64
|
+
if (!isUploadMutationOperation(operation)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const file = req.file;
|
|
68
|
+
if (!file) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!supportedMimeTypes.has(file.mimetype)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (typeof file.size === "number" && file.size < settings.minBytes) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const sharpFactory = req.payload?.config?.sharp;
|
|
78
|
+
if (typeof sharpFactory !== "function") {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const originalBuffer = await readIncomingBuffer(file);
|
|
82
|
+
if (!Buffer.isBuffer(originalBuffer) || originalBuffer.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const animatedInput = file.mimetype === "image/avif" || file.mimetype === "image/webp";
|
|
86
|
+
const metadataProbe = await sharpFactory(
|
|
87
|
+
originalBuffer,
|
|
88
|
+
animatedInput ? {
|
|
89
|
+
animated: true
|
|
90
|
+
} : void 0
|
|
91
|
+
).metadata();
|
|
92
|
+
if (settings.skipAnimated && typeof metadataProbe.pages === "number" && metadataProbe.pages > 1) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const buildPipeline = (qualityOverride) => {
|
|
96
|
+
let pipeline = sharpFactory(
|
|
97
|
+
originalBuffer,
|
|
98
|
+
animatedInput ? {
|
|
99
|
+
animated: true
|
|
100
|
+
} : void 0
|
|
101
|
+
);
|
|
102
|
+
switch (file.mimetype) {
|
|
103
|
+
case "image/jpeg":
|
|
104
|
+
case "image/jpg":
|
|
105
|
+
pipeline = pipeline.jpeg({
|
|
106
|
+
mozjpeg: true,
|
|
107
|
+
progressive: true,
|
|
108
|
+
quality: clamp(qualityOverride ?? settings.jpegQuality, 20, 100)
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
case "image/png":
|
|
112
|
+
pipeline = pipeline.png({
|
|
113
|
+
compressionLevel: clamp(settings.pngCompressionLevel, 0, 9),
|
|
114
|
+
palette: false
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
case "image/webp":
|
|
118
|
+
pipeline = pipeline.webp({
|
|
119
|
+
quality: clamp(qualityOverride ?? settings.webpQuality, 20, 100)
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
case "image/avif":
|
|
123
|
+
pipeline = pipeline.avif({
|
|
124
|
+
quality: clamp(qualityOverride ?? settings.avifQuality, 20, 100)
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
case "image/tiff":
|
|
128
|
+
pipeline = pipeline.tiff({
|
|
129
|
+
quality: clamp(qualityOverride ?? settings.tiffQuality, 20, 100)
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return pipeline;
|
|
136
|
+
};
|
|
137
|
+
const initialPipeline = buildPipeline();
|
|
138
|
+
if (!initialPipeline) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let optimizedBuffer = await initialPipeline.toBuffer();
|
|
142
|
+
const shouldSweepQuality = settings.enforceSmallerForLossy && isLossyMimeType(file.mimetype);
|
|
143
|
+
if (shouldSweepQuality) {
|
|
144
|
+
const initialQuality = file.mimetype === "image/jpeg" || file.mimetype === "image/jpg" ? settings.jpegQuality : file.mimetype === "image/webp" ? settings.webpQuality : file.mimetype === "image/avif" ? settings.avifQuality : settings.tiffQuality;
|
|
145
|
+
let bestBuffer = optimizedBuffer;
|
|
146
|
+
let quality = clamp(initialQuality - 5, settings.minQualityFloor, 100);
|
|
147
|
+
while (quality >= settings.minQualityFloor) {
|
|
148
|
+
const retryPipeline = buildPipeline(quality);
|
|
149
|
+
if (!retryPipeline) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
const retryBuffer = await retryPipeline.toBuffer();
|
|
153
|
+
if (retryBuffer.length < bestBuffer.length) {
|
|
154
|
+
bestBuffer = retryBuffer;
|
|
155
|
+
}
|
|
156
|
+
quality -= 5;
|
|
157
|
+
}
|
|
158
|
+
optimizedBuffer = bestBuffer;
|
|
159
|
+
}
|
|
160
|
+
if (settings.onlyIfSmaller && optimizedBuffer.length >= originalBuffer.length) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
req.file = await writeOptimizedBuffer(file, optimizedBuffer);
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
var withImageUploadOptimization = (collection, options = {}) => {
|
|
167
|
+
if (!collection.upload) {
|
|
168
|
+
return collection;
|
|
169
|
+
}
|
|
170
|
+
const existingHooks = collection.hooks || {};
|
|
171
|
+
const beforeOperation = existingHooks.beforeOperation || [];
|
|
172
|
+
return {
|
|
173
|
+
...collection,
|
|
174
|
+
hooks: {
|
|
175
|
+
...existingHooks,
|
|
176
|
+
beforeOperation: [createImageUploadOptimizationHook(options), ...beforeOperation]
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/studio/index.ts
|
|
182
|
+
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
183
|
+
var makeIssue = (message, path, code = "studio.invalid") => ({
|
|
184
|
+
code,
|
|
185
|
+
message,
|
|
186
|
+
path,
|
|
187
|
+
severity: "error"
|
|
188
|
+
});
|
|
189
|
+
var createEmptyStudioDocument = (title) => ({
|
|
190
|
+
metadata: {},
|
|
191
|
+
schemaVersion: 1,
|
|
192
|
+
title,
|
|
193
|
+
nodes: [],
|
|
194
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
195
|
+
});
|
|
196
|
+
function assertStudioDocumentV1(input) {
|
|
197
|
+
if (!isRecord(input)) {
|
|
198
|
+
throw new Error("Studio document must be an object");
|
|
199
|
+
}
|
|
200
|
+
if (input.schemaVersion !== 1) {
|
|
201
|
+
throw new Error("Unsupported studio schemaVersion");
|
|
202
|
+
}
|
|
203
|
+
if (!Array.isArray(input.nodes)) {
|
|
204
|
+
throw new Error("Studio document nodes must be an array");
|
|
205
|
+
}
|
|
206
|
+
const nodes = input.nodes.map((node, index) => {
|
|
207
|
+
if (!isRecord(node)) {
|
|
208
|
+
throw new Error(`Node at index ${index} must be an object`);
|
|
209
|
+
}
|
|
210
|
+
if (typeof node.id !== "string" || node.id.length === 0) {
|
|
211
|
+
throw new Error(`Node at index ${index} has invalid id`);
|
|
212
|
+
}
|
|
213
|
+
if (typeof node.type !== "string" || node.type.length === 0) {
|
|
214
|
+
throw new Error(`Node at index ${index} has invalid type`);
|
|
215
|
+
}
|
|
216
|
+
if (!isRecord(node.data)) {
|
|
217
|
+
throw new Error(`Node at index ${index} has invalid data`);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
id: node.id,
|
|
221
|
+
type: node.type,
|
|
222
|
+
data: node.data
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
metadata: isRecord(input.metadata) ? input.metadata : void 0,
|
|
227
|
+
schemaVersion: 1,
|
|
228
|
+
title: typeof input.title === "string" ? input.title : void 0,
|
|
229
|
+
nodes,
|
|
230
|
+
updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : void 0
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function createStudioRegistry(modules) {
|
|
234
|
+
const moduleByID = new Map(modules.map((mod) => [mod.id, mod]));
|
|
235
|
+
const nodeTypes = modules.flatMap((mod) => mod.nodeTypes);
|
|
236
|
+
const nodeTypeByName = new Map(nodeTypes.map((definition) => [definition.type, definition]));
|
|
237
|
+
return {
|
|
238
|
+
getModuleByID: (id) => moduleByID.get(id),
|
|
239
|
+
getNodeTypeByName: (type) => nodeTypeByName.get(type),
|
|
240
|
+
listInspectorPanels: () => modules.flatMap((mod) => mod.inspectorPanels),
|
|
241
|
+
listModules: () => [...modules],
|
|
242
|
+
listPaletteGroups: () => modules.flatMap((mod) => mod.paletteGroups),
|
|
243
|
+
listNodeTypes: () => [...nodeTypes]
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function validateStudioDocument(document, modules) {
|
|
247
|
+
const issues = [];
|
|
248
|
+
if (document.schemaVersion !== 1) {
|
|
249
|
+
issues.push(makeIssue("Unsupported schema version", "schemaVersion", "studio.schemaVersion"));
|
|
250
|
+
}
|
|
251
|
+
if (!Array.isArray(document.nodes)) {
|
|
252
|
+
issues.push(makeIssue("Nodes must be an array", "nodes", "studio.nodes"));
|
|
253
|
+
return issues;
|
|
254
|
+
}
|
|
255
|
+
const registry = createStudioRegistry(modules);
|
|
256
|
+
const nodeIDs = /* @__PURE__ */ new Set();
|
|
257
|
+
document.nodes.forEach((node, index) => {
|
|
258
|
+
if (!node.id) {
|
|
259
|
+
issues.push(makeIssue("Node id is required", `nodes.${index}.id`, "studio.node.id"));
|
|
260
|
+
}
|
|
261
|
+
if (nodeIDs.has(node.id)) {
|
|
262
|
+
issues.push(makeIssue("Node id must be unique", `nodes.${index}.id`, "studio.node.id.duplicate"));
|
|
263
|
+
}
|
|
264
|
+
nodeIDs.add(node.id);
|
|
265
|
+
if (!registry.getNodeTypeByName(node.type)) {
|
|
266
|
+
issues.push(makeIssue("Unsupported node type", `nodes.${index}.type`, "studio.node.type"));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
for (const module of modules) {
|
|
270
|
+
for (const validate of module.validators) {
|
|
271
|
+
issues.push(...validate(document));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return issues;
|
|
275
|
+
}
|
|
276
|
+
function compileStudioDocument(document, modules) {
|
|
277
|
+
const issues = validateStudioDocument(document, modules);
|
|
278
|
+
const compilerEntries = modules.filter((mod) => typeof mod.compiler?.compileNode === "function").map((mod) => mod.compiler?.compileNode);
|
|
279
|
+
const layout = document.nodes.map((node) => {
|
|
280
|
+
for (const compileNode of compilerEntries) {
|
|
281
|
+
if (!compileNode) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const compiled = compileNode(node);
|
|
285
|
+
if (compiled) {
|
|
286
|
+
return compiled;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
id: node.id,
|
|
291
|
+
blockType: node.type,
|
|
292
|
+
...node.data
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
issues,
|
|
297
|
+
layout
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function migrateStudioDocument(value, migrations) {
|
|
301
|
+
const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
|
|
302
|
+
if (isRecord(value) && value.schemaVersion === 1) {
|
|
303
|
+
return assertStudioDocumentV1(value);
|
|
304
|
+
}
|
|
305
|
+
let current = value;
|
|
306
|
+
for (const migration of sorted) {
|
|
307
|
+
if (!isRecord(current) || current.schemaVersion !== migration.fromVersion) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
current = migration.migrate(current);
|
|
311
|
+
}
|
|
312
|
+
return assertStudioDocumentV1(current);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export {
|
|
316
|
+
createImageUploadOptimizationHook,
|
|
317
|
+
withImageUploadOptimization,
|
|
318
|
+
createEmptyStudioDocument,
|
|
319
|
+
assertStudioDocumentV1,
|
|
320
|
+
createStudioRegistry,
|
|
321
|
+
validateStudioDocument,
|
|
322
|
+
compileStudioDocument,
|
|
323
|
+
migrateStudioDocument,
|
|
324
|
+
studio_exports
|
|
325
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/shared/clientImageUploadOptimization.ts
|
|
4
|
+
var MAX_DIRECT_UPLOAD_BYTES = 4e6;
|
|
5
|
+
var extensionForMimeType = (mimeType) => {
|
|
6
|
+
switch (mimeType) {
|
|
7
|
+
case "image/webp":
|
|
8
|
+
return ".webp";
|
|
9
|
+
case "image/png":
|
|
10
|
+
return ".png";
|
|
11
|
+
default:
|
|
12
|
+
return ".jpg";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var detectCanvasTransparency = (context, width, height) => {
|
|
16
|
+
try {
|
|
17
|
+
const { data } = context.getImageData(0, 0, width, height);
|
|
18
|
+
for (let index = 3; index < data.length; index += 4) {
|
|
19
|
+
if (data[index] < 255) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
};
|
|
27
|
+
var resolveOutputMimeTypes = (sourceMime, hasTransparency) => {
|
|
28
|
+
const candidates = [];
|
|
29
|
+
if (hasTransparency) {
|
|
30
|
+
candidates.push("image/webp", "image/png");
|
|
31
|
+
} else if (sourceMime === "image/webp") {
|
|
32
|
+
candidates.push("image/webp", "image/jpeg");
|
|
33
|
+
} else if (sourceMime === "image/png") {
|
|
34
|
+
candidates.push("image/webp", "image/jpeg", "image/png");
|
|
35
|
+
} else {
|
|
36
|
+
candidates.push("image/jpeg", "image/webp");
|
|
37
|
+
}
|
|
38
|
+
return [...new Set(candidates)];
|
|
39
|
+
};
|
|
40
|
+
async function optimizeImageForUpload(file) {
|
|
41
|
+
if (!file.type.startsWith("image/")) {
|
|
42
|
+
return file;
|
|
43
|
+
}
|
|
44
|
+
const objectURL = URL.createObjectURL(file);
|
|
45
|
+
try {
|
|
46
|
+
const image = await new Promise((resolve, reject) => {
|
|
47
|
+
const nextImage = new Image();
|
|
48
|
+
nextImage.onload = () => resolve(nextImage);
|
|
49
|
+
nextImage.onerror = () => reject(new Error("Could not read image for upload optimization."));
|
|
50
|
+
nextImage.src = objectURL;
|
|
51
|
+
});
|
|
52
|
+
const canvas = document.createElement("canvas");
|
|
53
|
+
canvas.width = Math.max(1, image.width);
|
|
54
|
+
canvas.height = Math.max(1, image.height);
|
|
55
|
+
const context = canvas.getContext("2d");
|
|
56
|
+
if (!context) {
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
60
|
+
const sourceMime = file.type.toLowerCase();
|
|
61
|
+
const hasTransparency = detectCanvasTransparency(context, canvas.width, canvas.height);
|
|
62
|
+
const outputMimes = resolveOutputMimeTypes(sourceMime, hasTransparency);
|
|
63
|
+
const qualityPasses = [0.82, 0.74, 0.66, 0.58, 0.5, 0.42, 0.36, 0.3, 0.26];
|
|
64
|
+
let bestFile = null;
|
|
65
|
+
for (const outputMime of outputMimes) {
|
|
66
|
+
const passes = outputMime === "image/png" ? [void 0] : qualityPasses;
|
|
67
|
+
for (const quality of passes) {
|
|
68
|
+
const blob = await new Promise((resolve) => {
|
|
69
|
+
canvas.toBlob((value) => resolve(value), outputMime, quality);
|
|
70
|
+
});
|
|
71
|
+
if (!blob) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const optimizedName = file.name.replace(/\.[^/.]+$/, extensionForMimeType(outputMime));
|
|
75
|
+
const optimized = new File([blob], optimizedName, {
|
|
76
|
+
lastModified: Date.now(),
|
|
77
|
+
type: outputMime
|
|
78
|
+
});
|
|
79
|
+
if (!bestFile || optimized.size < bestFile.size) {
|
|
80
|
+
bestFile = optimized;
|
|
81
|
+
}
|
|
82
|
+
if (optimized.size <= MAX_DIRECT_UPLOAD_BYTES) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!bestFile) {
|
|
88
|
+
return file;
|
|
89
|
+
}
|
|
90
|
+
return bestFile.size < file.size ? bestFile : file;
|
|
91
|
+
} finally {
|
|
92
|
+
URL.revokeObjectURL(objectURL);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
MAX_DIRECT_UPLOAD_BYTES,
|
|
98
|
+
optimizeImageForUpload
|
|
99
|
+
};
|