@payloadcms/figma 0.1.0-alpha.5 → 0.1.0-alpha.6
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/oauth/defaults.d.ts +0 -1
- package/dist/oauth/defaults.js +0 -1
- package/dist/oauth/endpoints/getLoginEndpoint.js +0 -2
- package/dist/oauth/index.js +2 -16
- package/dist/oauth/utilities/getAuthorizeURL.d.ts +1 -3
- package/dist/oauth/utilities/getAuthorizeURL.js +2 -4
- package/dist/storage-content-api/buildBaseMediaResponseHeaders.d.ts +1 -0
- package/dist/storage-content-api/buildBaseMediaResponseHeaders.js +6 -0
- package/dist/storage-content-api/buildClientUploadFetchBackHeaders.d.ts +10 -0
- package/dist/storage-content-api/buildClientUploadFetchBackHeaders.js +21 -0
- package/dist/storage-content-api/buildMediaResponseHeaders.d.ts +4 -0
- package/dist/storage-content-api/buildMediaResponseHeaders.js +45 -0
- package/dist/storage-content-api/getInlineContentType.d.ts +1 -0
- package/dist/storage-content-api/getInlineContentType.js +33 -0
- package/dist/storage-content-api/hasSupportedContentEncoding.d.ts +1 -0
- package/dist/storage-content-api/hasSupportedContentEncoding.js +4 -0
- package/dist/storage-content-api/mediaErrorResponse.d.ts +1 -0
- package/dist/storage-content-api/mediaErrorResponse.js +12 -0
- package/dist/storage-content-api/staticHandler.js +82 -21
- package/dist/utils/adapters/nextjs.js +8 -7
- package/dist/utils/asset-collection.d.ts +9 -1
- package/dist/utils/asset-collection.js +64 -2
- package/dist/utils/s3-upload.js +4 -1
- package/package.json +7 -2
package/dist/oauth/defaults.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { CollectionConfig, EmailField, TextField } from 'payload';
|
|
2
2
|
import type { VerifyFunction } from './types.js';
|
|
3
3
|
export declare const DEFAULT_USER_INFO_COOKIE_NAME = "figma-user-info";
|
|
4
|
-
export declare const FIGMA_OAUTH_CALLBACK_PATH = "/users/sso/login";
|
|
5
4
|
export declare const defaultScope: string[];
|
|
6
5
|
export declare const defaultUsernameField: TextField;
|
|
7
6
|
export declare const defaultVerify: ({ collection, strategyName, userInfoCookieName, usernameField, }: {
|
package/dist/oauth/defaults.js
CHANGED
|
@@ -3,7 +3,6 @@ import { v4 as uuid } from 'uuid';
|
|
|
3
3
|
import { hasUserTokenPropsChanged } from './utilities/hasUserTokenPropsChanged.js';
|
|
4
4
|
import { isDuplicateKeyError } from './utilities/isDuplicateKeyError.js';
|
|
5
5
|
export const DEFAULT_USER_INFO_COOKIE_NAME = 'figma-user-info';
|
|
6
|
-
export const FIGMA_OAUTH_CALLBACK_PATH = '/users/sso/login';
|
|
7
6
|
function getFigmaUserInfo(headers, cookieName) {
|
|
8
7
|
try {
|
|
9
8
|
const cookies = parseCookies(headers);
|
|
@@ -6,7 +6,6 @@ import { createDebugLogger } from '../utilities/createDebugLogger.js';
|
|
|
6
6
|
import { establishSession } from '../utilities/establishSession.js';
|
|
7
7
|
import { exchangeCodeForAccessToken } from '../utilities/exchangeCodeForAccessToken.js';
|
|
8
8
|
import { extractOrigin } from '../utilities/extractOrigin.js';
|
|
9
|
-
import { getAdminCollectionSlug } from '../utilities/getAdminCollectionSlug.js';
|
|
10
9
|
import { buildCsrfCookieClearHeader, getOAuthCallbackPath, OAUTH_STATE_CSRF_COOKIE_NAME } from '../utilities/getAuthorizeURL.js';
|
|
11
10
|
import { isAbsoluteURL } from '../utilities/isAbsoluteURL.js';
|
|
12
11
|
export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug, pluginOptions, strategy })=>({
|
|
@@ -153,7 +152,6 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
|
|
|
153
152
|
const redirectUri = formatAdminURL({
|
|
154
153
|
apiRoute: config.routes?.api || '/api',
|
|
155
154
|
path: getOAuthCallbackPath({
|
|
156
|
-
adminCollectionSlug: getAdminCollectionSlug(config),
|
|
157
155
|
collection,
|
|
158
156
|
endpointSlug
|
|
159
157
|
}),
|
package/dist/oauth/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fieldAffectsData } from 'payload/shared';
|
|
2
|
-
import { defaultVerify
|
|
2
|
+
import { defaultVerify } from './defaults.js';
|
|
3
3
|
import { getLoginEndpoint } from './endpoints/getLoginEndpoint.js';
|
|
4
4
|
import { getLogoutEndpoint } from './endpoints/getLogoutEndpoint.js';
|
|
5
5
|
import { getMetaEndpoint } from './endpoints/getMetaEndpoint.js';
|
|
@@ -50,7 +50,6 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
|
|
|
50
50
|
path: '@payloadcms/figma/client#DefaultLoginButton'
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
let callbackEndpoint;
|
|
54
53
|
return {
|
|
55
54
|
...config,
|
|
56
55
|
admin: {
|
|
@@ -156,13 +155,6 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
|
|
|
156
155
|
pluginOptions,
|
|
157
156
|
strategy
|
|
158
157
|
});
|
|
159
|
-
const collectionCallbackPath = `/${existingCollection.slug}/${endpointSlug}/login`;
|
|
160
|
-
if (existingCollection.slug === adminCollectionSlug && FIGMA_OAUTH_CALLBACK_PATH !== collectionCallbackPath) {
|
|
161
|
-
callbackEndpoint = {
|
|
162
|
-
...loginEndpoint,
|
|
163
|
-
path: FIGMA_OAUTH_CALLBACK_PATH
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
158
|
const authStrategy = {
|
|
167
159
|
name: `${existingCollection.slug}-${strategyName}`,
|
|
168
160
|
// Bind 'this' to ensure the authenticate function has the correct context
|
|
@@ -246,12 +238,6 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
|
|
|
246
238
|
]
|
|
247
239
|
}
|
|
248
240
|
};
|
|
249
|
-
})
|
|
250
|
-
endpoints: [
|
|
251
|
-
...config.endpoints ?? [],
|
|
252
|
-
...callbackEndpoint ? [
|
|
253
|
-
callbackEndpoint
|
|
254
|
-
] : []
|
|
255
|
-
]
|
|
241
|
+
})
|
|
256
242
|
};
|
|
257
243
|
};
|
|
@@ -45,9 +45,7 @@ export interface AuthorizeURLResult {
|
|
|
45
45
|
csrfNonce: string;
|
|
46
46
|
params: AuthorizeURLParams;
|
|
47
47
|
}
|
|
48
|
-
export declare const getOAuthCallbackPath: ({
|
|
49
|
-
adminCollectionSlug: string;
|
|
50
|
-
} & Pick<Args, "collection" | "endpointSlug">) => `/${string}`;
|
|
48
|
+
export declare const getOAuthCallbackPath: ({ collection, endpointSlug, }: Pick<Args, "collection" | "endpointSlug">) => `/${string}`;
|
|
51
49
|
export declare const getAuthorizeURL: ({ collection, collectionOptions, endpointSlug, existingCsrfNonce, failedRedirect, payload, pluginOptions, redirect, serverURLOverride, strategy, }: Args) => Promise<AuthorizeURLResult>;
|
|
52
50
|
/**
|
|
53
51
|
* Name of the HttpOnly cookie that carries the browser-bound CSRF nonce
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import * as QueryString from 'qs-esm';
|
|
3
3
|
import { v4 as uuid } from 'uuid';
|
|
4
|
-
import { defaultScope
|
|
5
|
-
|
|
6
|
-
export const getOAuthCallbackPath = ({ adminCollectionSlug, collection, endpointSlug })=>collection.slug === adminCollectionSlug ? FIGMA_OAUTH_CALLBACK_PATH : `/${collection.slug}/${endpointSlug}/login`;
|
|
4
|
+
import { defaultScope } from '../defaults.js';
|
|
5
|
+
export const getOAuthCallbackPath = ({ collection, endpointSlug })=>`/${collection.slug}/${endpointSlug}/login`;
|
|
7
6
|
export const getAuthorizeURL = async ({ collection, collectionOptions, endpointSlug, existingCsrfNonce, failedRedirect, payload, pluginOptions, redirect, serverURLOverride, strategy })=>{
|
|
8
7
|
const { redirectServerURL } = pluginOptions;
|
|
9
8
|
await strategy.ensureMeta();
|
|
@@ -33,7 +32,6 @@ export const getAuthorizeURL = async ({ collection, collectionOptions, endpointS
|
|
|
33
32
|
}
|
|
34
33
|
const baseRedirectURL = redirectServerURL || payload.config.serverURL || state.serverURL;
|
|
35
34
|
const loginRedirectURL = encodeURI(`${baseRedirectURL}${payload.config.routes?.api || '/api'}${getOAuthCallbackPath({
|
|
36
|
-
adminCollectionSlug: getAdminCollectionSlug(payload.config),
|
|
37
35
|
collection,
|
|
38
36
|
endpointSlug
|
|
39
37
|
})}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const buildBaseMediaResponseHeaders: () => Headers;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload's server-side upload fetch-back uses the response Content-Type to decide whether to
|
|
3
|
+
* inspect image dimensions. For an active image type such as SVG, omit the sanitized type so
|
|
4
|
+
* Payload falls back to the original upload MIME type. Public file requests never use this helper.
|
|
5
|
+
*/
|
|
6
|
+
export declare const buildClientUploadFetchBackHeaders: ({ fallbackContentType, filename, upstream, }: {
|
|
7
|
+
fallbackContentType?: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
upstream: Headers;
|
|
10
|
+
}) => Headers;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { buildMediaResponseHeaders } from './buildMediaResponseHeaders.js';
|
|
2
|
+
import { getInlineContentType } from './getInlineContentType.js';
|
|
3
|
+
/**
|
|
4
|
+
* Payload's server-side upload fetch-back uses the response Content-Type to decide whether to
|
|
5
|
+
* inspect image dimensions. For an active image type such as SVG, omit the sanitized type so
|
|
6
|
+
* Payload falls back to the original upload MIME type. Public file requests never use this helper.
|
|
7
|
+
*/ export const buildClientUploadFetchBackHeaders = ({ fallbackContentType, filename, upstream })=>{
|
|
8
|
+
const effectiveContentType = upstream.get('Content-Type') || fallbackContentType;
|
|
9
|
+
const upstreamWithFallback = new Headers(upstream);
|
|
10
|
+
if (effectiveContentType && !upstream.get('Content-Type')) {
|
|
11
|
+
upstreamWithFallback.set('Content-Type', effectiveContentType);
|
|
12
|
+
}
|
|
13
|
+
const headers = buildMediaResponseHeaders({
|
|
14
|
+
filename,
|
|
15
|
+
upstream: upstreamWithFallback
|
|
16
|
+
});
|
|
17
|
+
if (effectiveContentType && !getInlineContentType(effectiveContentType)) {
|
|
18
|
+
headers.delete('Content-Type');
|
|
19
|
+
}
|
|
20
|
+
return headers;
|
|
21
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { buildBaseMediaResponseHeaders } from './buildBaseMediaResponseHeaders.js';
|
|
2
|
+
import { getInlineContentType } from './getInlineContentType.js';
|
|
3
|
+
const FORWARDED_MEDIA_RESPONSE_HEADERS = [
|
|
4
|
+
'Cache-Control',
|
|
5
|
+
'Content-Length',
|
|
6
|
+
'Content-Range',
|
|
7
|
+
'ETag',
|
|
8
|
+
'Last-Modified'
|
|
9
|
+
];
|
|
10
|
+
const ATTACHMENT_CSP = "sandbox; default-src 'none'; base-uri 'none'; form-action 'none'";
|
|
11
|
+
export const buildMediaResponseHeaders = ({ filename, upstream })=>{
|
|
12
|
+
const headers = buildBaseMediaResponseHeaders();
|
|
13
|
+
for (const name of FORWARDED_MEDIA_RESPONSE_HEADERS){
|
|
14
|
+
const value = upstream.get(name);
|
|
15
|
+
if (value) {
|
|
16
|
+
headers.set(name, value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const contentType = getInlineContentType(upstream.get('Content-Type'));
|
|
20
|
+
if (contentType) {
|
|
21
|
+
headers.set('Content-Type', contentType);
|
|
22
|
+
} else {
|
|
23
|
+
headers.set('Content-Type', 'application/octet-stream');
|
|
24
|
+
headers.set('Content-Disposition', attachmentDisposition(filename));
|
|
25
|
+
headers.set('Content-Security-Policy', ATTACHMENT_CSP);
|
|
26
|
+
}
|
|
27
|
+
return headers;
|
|
28
|
+
};
|
|
29
|
+
const attachmentDisposition = (filename)=>{
|
|
30
|
+
let decodedName;
|
|
31
|
+
try {
|
|
32
|
+
decodedName = decodeURIComponent(filename);
|
|
33
|
+
} catch {
|
|
34
|
+
decodedName = 'download';
|
|
35
|
+
}
|
|
36
|
+
const sanitizedName = sanitizeFilename(decodedName);
|
|
37
|
+
const safeName = Array.from(sanitizedName).slice(0, 255).join('') || 'download';
|
|
38
|
+
const asciiName = safeName.replace(/[^\x20-\x7e]/g, '_').replace(/["\\]/g, '_');
|
|
39
|
+
const utf8Name = encodeURIComponent(safeName).replace(/[!'()*]/g, (character)=>`%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
40
|
+
return `attachment; filename="${asciiName}"; filename*=UTF-8''${utf8Name}`;
|
|
41
|
+
};
|
|
42
|
+
const sanitizeFilename = (filename)=>Array.from(filename, (character)=>{
|
|
43
|
+
const codePoint = character.codePointAt(0);
|
|
44
|
+
return codePoint < 32 || codePoint === 127 || character === '/' || character === '\\' ? '_' : character;
|
|
45
|
+
}).join('');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getInlineContentType: (value: null | string | undefined) => string | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Keep this allowlist aligned with Gatekeeper's presignedMediaHeaders.ts. The plugin cannot
|
|
2
|
+
// import the private Gatekeeper service, but both same-origin media proxies need the same policy.
|
|
3
|
+
const INLINE_CONTENT_TYPES = new Set([
|
|
4
|
+
'application/font-woff',
|
|
5
|
+
'audio/aac',
|
|
6
|
+
'audio/flac',
|
|
7
|
+
'audio/mp4',
|
|
8
|
+
'audio/mpeg',
|
|
9
|
+
'audio/ogg',
|
|
10
|
+
'audio/wav',
|
|
11
|
+
'audio/webm',
|
|
12
|
+
'font/otf',
|
|
13
|
+
'font/ttf',
|
|
14
|
+
'font/woff',
|
|
15
|
+
'font/woff2',
|
|
16
|
+
'image/avif',
|
|
17
|
+
'image/bmp',
|
|
18
|
+
'image/gif',
|
|
19
|
+
'image/jpeg',
|
|
20
|
+
'image/png',
|
|
21
|
+
'image/vnd.microsoft.icon',
|
|
22
|
+
'image/webp',
|
|
23
|
+
'image/x-icon',
|
|
24
|
+
'video/mp4',
|
|
25
|
+
'video/mpeg',
|
|
26
|
+
'video/ogg',
|
|
27
|
+
'video/quicktime',
|
|
28
|
+
'video/webm'
|
|
29
|
+
]);
|
|
30
|
+
export const getInlineContentType = (value)=>{
|
|
31
|
+
const normalized = value?.split(';', 1).at(0)?.trim().toLowerCase();
|
|
32
|
+
return normalized && INLINE_CONTENT_TYPES.has(normalized) ? normalized : null;
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const hasSupportedContentEncoding: (headers: Headers) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const mediaErrorResponse: (status: number, body: string, contentRange?: string) => Response;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildBaseMediaResponseHeaders } from './buildBaseMediaResponseHeaders.js';
|
|
2
|
+
export const mediaErrorResponse = (status, body, contentRange)=>{
|
|
3
|
+
const headers = buildBaseMediaResponseHeaders();
|
|
4
|
+
headers.set('Content-Type', 'text/plain;charset=UTF-8');
|
|
5
|
+
if (contentRange) {
|
|
6
|
+
headers.set('Content-Range', contentRange);
|
|
7
|
+
}
|
|
8
|
+
return new Response(body, {
|
|
9
|
+
headers,
|
|
10
|
+
status
|
|
11
|
+
});
|
|
12
|
+
};
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { isImage } from 'payload/shared';
|
|
4
|
+
import { buildClientUploadFetchBackHeaders } from './buildClientUploadFetchBackHeaders.js';
|
|
5
|
+
import { buildMediaResponseHeaders } from './buildMediaResponseHeaders.js';
|
|
6
|
+
import { hasSupportedContentEncoding } from './hasSupportedContentEncoding.js';
|
|
7
|
+
import { mediaErrorResponse } from './mediaErrorResponse.js';
|
|
4
8
|
import { parseUploadReference } from './utilities/index.js';
|
|
9
|
+
const isRunningInLambda = ()=>process.env.AWS_EXECUTION_ENV?.startsWith('AWS_Lambda_') === true;
|
|
10
|
+
const shouldReturnRedirect = ({ uploadReference })=>{
|
|
11
|
+
if (!isRunningInLambda()) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (!uploadReference) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const { mimeType } = parseUploadReference(uploadReference);
|
|
18
|
+
return Boolean(mimeType && !isImage(mimeType));
|
|
19
|
+
};
|
|
20
|
+
const cancelResponseBody = async (response)=>{
|
|
21
|
+
try {
|
|
22
|
+
await response.body?.cancel();
|
|
23
|
+
} catch {
|
|
24
|
+
// The response will not be consumed after an error; cancellation is best-effort cleanup.
|
|
25
|
+
}
|
|
26
|
+
};
|
|
5
27
|
export const getHandler = ({ client, collection })=>{
|
|
6
28
|
return async (req, { params: { filename, uploadReference } })=>{
|
|
7
29
|
try {
|
|
@@ -25,29 +47,68 @@ export const getHandler = ({ client, collection })=>{
|
|
|
25
47
|
status: 500
|
|
26
48
|
});
|
|
27
49
|
}
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
50
|
+
// Lambda responses have a 6 MB limit, so deployed apps redirect to Gatekeeper, which
|
|
51
|
+
// validates the signed URL and streams the file. Other runtimes do not have Gatekeeper
|
|
52
|
+
// in front of them and must stream the file through Payload instead.
|
|
53
|
+
// An image uploadReference marks an internal, one-time fetch-back. Payload core consumes
|
|
54
|
+
// the body to size the image, so a redirect would leave it empty even in Lambda.
|
|
55
|
+
if (shouldReturnRedirect({
|
|
56
|
+
uploadReference
|
|
57
|
+
})) {
|
|
58
|
+
return Response.redirect(data.url, 302);
|
|
59
|
+
}
|
|
60
|
+
const requestHeaders = new Headers({
|
|
61
|
+
'Accept-Encoding': 'identity'
|
|
62
|
+
});
|
|
63
|
+
const range = req.headers.get('Range');
|
|
64
|
+
if (range) {
|
|
65
|
+
requestHeaders.set('Range', range);
|
|
66
|
+
const ifRange = req.headers.get('If-Range');
|
|
67
|
+
if (ifRange) {
|
|
68
|
+
requestHeaders.set('If-Range', ifRange);
|
|
48
69
|
}
|
|
49
70
|
}
|
|
50
|
-
|
|
71
|
+
let fileResponse;
|
|
72
|
+
try {
|
|
73
|
+
fileResponse = await fetch(data.url, {
|
|
74
|
+
credentials: 'omit',
|
|
75
|
+
headers: requestHeaders,
|
|
76
|
+
redirect: 'manual',
|
|
77
|
+
referrerPolicy: 'no-referrer',
|
|
78
|
+
signal: req.signal
|
|
79
|
+
});
|
|
80
|
+
} catch {
|
|
81
|
+
req.payload.logger.error('Failed to fetch file from storage');
|
|
82
|
+
return mediaErrorResponse(500, 'Internal Server Error');
|
|
83
|
+
}
|
|
84
|
+
if (fileResponse.status === 416 && range) {
|
|
85
|
+
await cancelResponseBody(fileResponse);
|
|
86
|
+
return mediaErrorResponse(416, 'Range Not Satisfiable', fileResponse.headers.get('Content-Range') ?? undefined);
|
|
87
|
+
}
|
|
88
|
+
if (!fileResponse.ok) {
|
|
89
|
+
await cancelResponseBody(fileResponse);
|
|
90
|
+
req.payload.logger.error(`Failed to fetch file from storage: ${fileResponse.status}`);
|
|
91
|
+
return mediaErrorResponse(500, 'Internal Server Error');
|
|
92
|
+
}
|
|
93
|
+
if (!hasSupportedContentEncoding(fileResponse.headers)) {
|
|
94
|
+
await cancelResponseBody(fileResponse);
|
|
95
|
+
req.payload.logger.error('Unsupported Content-Encoding from storage');
|
|
96
|
+
return mediaErrorResponse(502, 'Bad Gateway');
|
|
97
|
+
}
|
|
98
|
+
const clientUploadMimeType = uploadReference ? parseUploadReference(uploadReference).mimeType : undefined;
|
|
99
|
+
const responseHeaders = uploadReference ? buildClientUploadFetchBackHeaders({
|
|
100
|
+
fallbackContentType: clientUploadMimeType,
|
|
101
|
+
filename,
|
|
102
|
+
upstream: fileResponse.headers
|
|
103
|
+
}) : buildMediaResponseHeaders({
|
|
104
|
+
filename,
|
|
105
|
+
upstream: fileResponse.headers
|
|
106
|
+
});
|
|
107
|
+
return new Response(fileResponse.body, {
|
|
108
|
+
headers: responseHeaders,
|
|
109
|
+
status: fileResponse.status,
|
|
110
|
+
statusText: fileResponse.statusText
|
|
111
|
+
});
|
|
51
112
|
} catch (err) {
|
|
52
113
|
req.payload.logger.error({
|
|
53
114
|
err,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { collectSSGAssets, collectStaticAssets } from '../asset-collection.js';
|
|
3
|
+
import { collectSSGAssets, collectStaticAssets, collectStaticMetadataAssets } from '../asset-collection.js';
|
|
4
4
|
import { buildLambdaZip } from '../build-lambda-zip.js';
|
|
5
5
|
import { resolveOutputPath } from '../deploy-output/resolveOutputPath.js';
|
|
6
6
|
import { collectFilesRecursive } from '../fs-utils.js';
|
|
@@ -19,10 +19,7 @@ export class NextjsAdapter {
|
|
|
19
19
|
} catch {
|
|
20
20
|
// No public directory
|
|
21
21
|
}
|
|
22
|
-
const
|
|
23
|
-
...staticAssets,
|
|
24
|
-
...publicAssets
|
|
25
|
-
];
|
|
22
|
+
const metadataAssets = await collectStaticMetadataAssets(projectPath, output);
|
|
26
23
|
const pathMap = {};
|
|
27
24
|
for (const key of staticAssets){
|
|
28
25
|
pathMap[key] = path.relative(projectPath, path.join(outputPath, key.replace(/^_next\//, '')));
|
|
@@ -30,10 +27,14 @@ export class NextjsAdapter {
|
|
|
30
27
|
for (const key of publicAssets){
|
|
31
28
|
pathMap[key] = path.join('public', key);
|
|
32
29
|
}
|
|
30
|
+
for (const key of metadataAssets.keys){
|
|
31
|
+
pathMap[key] = metadataAssets.pathMap[key];
|
|
32
|
+
}
|
|
33
|
+
const uploadKeys = Object.keys(pathMap);
|
|
33
34
|
return {
|
|
34
35
|
pathMap,
|
|
35
|
-
routes:
|
|
36
|
-
uploadKeys
|
|
36
|
+
routes: uploadKeys.map((key)=>`/${key}`),
|
|
37
|
+
uploadKeys
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
async collectPages(projectPath, output) {
|
|
@@ -27,7 +27,7 @@ export type SSGAssets = {
|
|
|
27
27
|
/**
|
|
28
28
|
* Collect SSG asset paths by parsing .next/prerender-manifest.json
|
|
29
29
|
*
|
|
30
|
-
* For each route in the manifest's `routes` object, collects three files:
|
|
30
|
+
* For each page route in the manifest's `routes` object, collects three files:
|
|
31
31
|
* - {route}.html (full HTML)
|
|
32
32
|
* - {route}.rsc (React Server Components payload)
|
|
33
33
|
* - {route}.meta (response headers JSON)
|
|
@@ -39,6 +39,14 @@ export type SSGAssets = {
|
|
|
39
39
|
* @returns SSG asset keys and a pathMap for filesystem resolution
|
|
40
40
|
*/
|
|
41
41
|
export declare function collectSSGAssets(projectPath: string, output?: string): Promise<SSGAssets>;
|
|
42
|
+
/**
|
|
43
|
+
* Collect file-based Next.js metadata icons that were prerendered as static route handlers.
|
|
44
|
+
*
|
|
45
|
+
* Maps public asset keys such as `shop/icon1.png` to their raw
|
|
46
|
+
* `.next/server/app/shop/icon1.png.body` output.
|
|
47
|
+
* Documentation: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons
|
|
48
|
+
*/
|
|
49
|
+
export declare function collectStaticMetadataAssets(projectPath: string, output?: string): Promise<SSGAssets>;
|
|
42
50
|
/**
|
|
43
51
|
* Create Lambda deployment zip
|
|
44
52
|
*
|
|
@@ -77,7 +77,7 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
77
77
|
/**
|
|
78
78
|
* Collect SSG asset paths by parsing .next/prerender-manifest.json
|
|
79
79
|
*
|
|
80
|
-
* For each route in the manifest's `routes` object, collects three files:
|
|
80
|
+
* For each page route in the manifest's `routes` object, collects three files:
|
|
81
81
|
* - {route}.html (full HTML)
|
|
82
82
|
* - {route}.rsc (React Server Components payload)
|
|
83
83
|
* - {route}.meta (response headers JSON)
|
|
@@ -113,7 +113,13 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
113
113
|
'.rsc',
|
|
114
114
|
'.meta'
|
|
115
115
|
];
|
|
116
|
-
for (const routeKey of Object.
|
|
116
|
+
for (const [routeKey, route] of Object.entries(manifest.routes)){
|
|
117
|
+
// Current manifests classify app pages explicitly. Older App Router manifests
|
|
118
|
+
// can be identified by their RSC data route. Other entries may be route handlers.
|
|
119
|
+
const isPageRoute = route.routeType === 'page' || route.routeType === undefined && typeof route.dataRoute === 'string' && route.dataRoute.endsWith('.rsc');
|
|
120
|
+
if (!isPageRoute) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
117
123
|
// Strip leading slash for S3 key; handle root "/" → "index"
|
|
118
124
|
const stripped = routeKey === '/' ? 'index' : routeKey.replace(/^\//, '');
|
|
119
125
|
for (const ext of extensions){
|
|
@@ -127,6 +133,55 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
127
133
|
pathMap
|
|
128
134
|
};
|
|
129
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Collect file-based Next.js metadata icons that were prerendered as static route handlers.
|
|
138
|
+
*
|
|
139
|
+
* Maps public asset keys such as `shop/icon1.png` to their raw
|
|
140
|
+
* `.next/server/app/shop/icon1.png.body` output.
|
|
141
|
+
* Documentation: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons
|
|
142
|
+
*/ export async function collectStaticMetadataAssets(projectPath, output) {
|
|
143
|
+
const outputPath = resolveOutputPath(projectPath, 'nextjs', output);
|
|
144
|
+
const manifestPath = path.join(outputPath, 'prerender-manifest.json');
|
|
145
|
+
let manifest;
|
|
146
|
+
try {
|
|
147
|
+
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
148
|
+
manifest = JSON.parse(raw);
|
|
149
|
+
} catch {
|
|
150
|
+
return {
|
|
151
|
+
keys: [],
|
|
152
|
+
pathMap: {}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (!manifest.routes || typeof manifest.routes !== 'object') {
|
|
156
|
+
return {
|
|
157
|
+
keys: [],
|
|
158
|
+
pathMap: {}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const keys = [];
|
|
162
|
+
const pathMap = {};
|
|
163
|
+
for (const [routeKey, route] of Object.entries(manifest.routes)){
|
|
164
|
+
if (route.routeType !== 'route' || route.compute !== 'static' || route.response !== 'complete' || !isFileBasedMetadataIconRoute(routeKey)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const assetKey = routeKey.replace(/^\/+/, '');
|
|
168
|
+
const bodyPath = path.join(outputPath, 'server', 'app', `${assetKey}.body`);
|
|
169
|
+
try {
|
|
170
|
+
const bodyStat = await fs.stat(bodyPath);
|
|
171
|
+
if (!bodyStat.isFile()) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
keys.push(assetKey);
|
|
178
|
+
pathMap[assetKey] = path.relative(projectPath, bodyPath);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
keys,
|
|
182
|
+
pathMap
|
|
183
|
+
};
|
|
184
|
+
}
|
|
130
185
|
/**
|
|
131
186
|
* Create Lambda deployment zip
|
|
132
187
|
*
|
|
@@ -137,3 +192,10 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
137
192
|
*/ export async function createLambdaZip(projectPath) {
|
|
138
193
|
await buildLambdaZip(projectPath);
|
|
139
194
|
}
|
|
195
|
+
function isFileBasedMetadataIconRoute(routeKey) {
|
|
196
|
+
const basename = path.posix.basename(routeKey);
|
|
197
|
+
if (routeKey === '/favicon.ico') {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return /^icon\d*(?:-[0-9a-z]{6})?\.(?:ico|jpe?g|png|svg)$/.test(basename) || /^apple-icon\d*(?:-[0-9a-z]{6})?\.(?:jpe?g|png)$/.test(basename);
|
|
201
|
+
}
|
package/dist/utils/s3-upload.js
CHANGED
|
@@ -141,16 +141,19 @@ const INITIAL_RETRY_DELAY = 1000 // 1 second
|
|
|
141
141
|
const fsPath = pathMap?.[assetPath] ?? assetPath.replace(/^_next\//, '.next/');
|
|
142
142
|
const fullPath = path.join(projectPath, fsPath);
|
|
143
143
|
const signedUrl = assetUrls[assetPath];
|
|
144
|
-
|
|
144
|
+
const contentType = MIME_TYPES[path.extname(assetPath).toLowerCase()];
|
|
145
|
+
await uploadFile(fullPath, signedUrl, contentType);
|
|
145
146
|
results.assetsUploaded++;
|
|
146
147
|
// Track bytes uploaded
|
|
147
148
|
const stats = await fs.stat(fullPath);
|
|
148
149
|
results.totalBytesUploaded += stats.size;
|
|
149
150
|
// Report progress
|
|
150
151
|
onProgress?.(results.assetsUploaded, assetPaths.length);
|
|
152
|
+
return undefined;
|
|
151
153
|
} catch (error) {
|
|
152
154
|
results.assetsFailed++;
|
|
153
155
|
log.debug(`Failed to upload ${assetPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
156
|
+
return undefined;
|
|
154
157
|
}
|
|
155
158
|
}));
|
|
156
159
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payloadcms/figma",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"open": "^10.2.0",
|
|
48
48
|
"openapi-fetch": "0.15.0",
|
|
49
49
|
"picocolors": "1.1.1",
|
|
50
|
+
"qs-esm": "^8.0.1",
|
|
51
|
+
"sanitize-filename": "^1.6.4",
|
|
50
52
|
"skills": "1.4.4",
|
|
51
53
|
"tar": "^7.5.13",
|
|
52
54
|
"terminal-link": "^5.0.0",
|
|
@@ -75,7 +77,10 @@
|
|
|
75
77
|
"peerDependencies": {
|
|
76
78
|
"@payloadcms/plugin-cloud-storage": ">=4.0.0-canary.24 <4.0.0-internal",
|
|
77
79
|
"@payloadcms/richtext-lexical": ">=4.0.0-canary.24 <4.0.0-internal",
|
|
78
|
-
"
|
|
80
|
+
"@payloadcms/translations": ">=4.0.0-canary.24 <4.0.0-internal",
|
|
81
|
+
"@payloadcms/ui": ">=4.0.0-canary.24 <4.0.0-internal",
|
|
82
|
+
"payload": ">=4.0.0-canary.24 <4.0.0-internal",
|
|
83
|
+
"react": "^19.0.1 || ^19.1.2 || ^19.2.1"
|
|
79
84
|
},
|
|
80
85
|
"peerDependenciesMeta": {
|
|
81
86
|
"@payloadcms/plugin-cloud-storage": {
|