@payloadcms/figma 0.1.0-alpha.5 → 0.1.0-alpha.7
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/LICENSE.md +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- 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/plugin/build-config.d.ts +4 -2
- package/dist/plugin/build-config.js +34 -22
- package/dist/plugin/make-permission-access/make-permission-access.d.ts +8 -0
- package/dist/plugin/make-permission-access/make-permission-access.js +24 -0
- package/dist/storage-content-api/client-uploads/generate.js +2 -1
- package/dist/storage-content-api/index.js +1 -0
- package/dist/storage-content-api/types.d.ts +4 -1
- package/dist/utils/adapters/nextjs.js +13 -20
- package/dist/utils/asset-collection.d.ts +14 -1
- package/dist/utils/asset-collection.js +89 -5
- package/dist/utils/s3-upload.js +4 -1
- package/package.json +15 -5
package/LICENSE.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { figmaContentSystem } from './plugin/adapter.js';
|
|
|
4
4
|
export type { FigmaContentSystemOptions } from './plugin/adapter.js';
|
|
5
5
|
export { buildFigmaConfig } from './plugin/build-config.js';
|
|
6
6
|
export type { FigmaConfig } from './plugin/build-config.js';
|
|
7
|
+
export { passesMakeViewerWriteGate } from './plugin/make-permission-access/make-permission-access.js';
|
package/dist/index.js
CHANGED
|
@@ -4,3 +4,5 @@ export { contentAPIAdapter } from './db-content-api/index.js';
|
|
|
4
4
|
export { figmaContentSystem } from './plugin/adapter.js';
|
|
5
5
|
// Export buildConfig wrapper and its config type
|
|
6
6
|
export { buildFigmaConfig } from './plugin/build-config.js';
|
|
7
|
+
// Export the Make permission predicate for slug-specific baseAccess overrides.
|
|
8
|
+
export { passesMakeViewerWriteGate } from './plugin/make-permission-access/make-permission-access.js';
|
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
|
})}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config, SanitizedConfig } from 'payload';
|
|
1
|
+
import type { Config, SanitizedConfig, UploadInstructionsAccess } from 'payload';
|
|
2
2
|
/**
|
|
3
3
|
* Worker-trusted marker injected by Figma's sites-worker after it validates the
|
|
4
4
|
* X-Figma-Job-Queues-Auth-Bypass secret on a Payload jobs endpoint. The default
|
|
@@ -48,7 +48,9 @@ export type FigmaConfig = {
|
|
|
48
48
|
figma: {
|
|
49
49
|
contentSystemId?: string;
|
|
50
50
|
environmentId?: string;
|
|
51
|
-
storage?:
|
|
51
|
+
storage?: {
|
|
52
|
+
uploadInstructionsAccess?: UploadInstructionsAccess;
|
|
53
|
+
} | boolean;
|
|
52
54
|
useContentSystem?: boolean;
|
|
53
55
|
};
|
|
54
56
|
} & Omit<Config, 'db' | 'editor' | 'secret'> & Partial<Pick<Config, 'db'>>;
|
|
@@ -22,6 +22,7 @@ import { logMissingCliAuth } from './auth-preflight.js';
|
|
|
22
22
|
import { logMissingContentSystemId } from './bootstrap-preflight.js';
|
|
23
23
|
import { LIMIT_REACHED_VIEW_KEY, LIMIT_REACHED_VIEW_PATH } from './cloud-limits/routes.js';
|
|
24
24
|
import { getDevCookieNames } from './dev-cookie-names.js';
|
|
25
|
+
import { passesMakeViewerWriteGate, withMakeViewerWriteBaseAccessDefaults } from './make-permission-access/make-permission-access.js';
|
|
25
26
|
import { createSandboxUploadFetchPlugin } from './sandbox-upload-fetch.js';
|
|
26
27
|
/**
|
|
27
28
|
* Worker-trusted marker injected by Figma's sites-worker after it validates the
|
|
@@ -170,23 +171,26 @@ function messageOf(error) {
|
|
|
170
171
|
}
|
|
171
172
|
/**
|
|
172
173
|
* Creates storage configuration for upload collections
|
|
173
|
-
*/ function createStorageConfig(url, contentSystemId) {
|
|
174
|
-
const storageConfig =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
174
|
+
*/ function createStorageConfig(url, contentSystemId, uploadInstructionsAccess) {
|
|
175
|
+
const storageConfig = {
|
|
176
|
+
...process.env.FIGMA_CONTENT_API_ACCESS_KEY ? {
|
|
177
|
+
baseUrl: url,
|
|
178
|
+
contentApiKey: process.env.FIGMA_CONTENT_API_ACCESS_KEY
|
|
179
|
+
} : process.env.FIGMA_DEV_JWT === 'true' ? {
|
|
180
|
+
auth: {
|
|
181
|
+
mode: 'devJwt'
|
|
182
|
+
},
|
|
183
|
+
baseUrl: url,
|
|
184
|
+
contentSystemId
|
|
185
|
+
} : {
|
|
186
|
+
auth: {
|
|
187
|
+
mode: 'tokenStore',
|
|
188
|
+
tokenStore: getTokenStore()
|
|
189
|
+
},
|
|
190
|
+
baseUrl: url,
|
|
191
|
+
contentSystemId
|
|
187
192
|
},
|
|
188
|
-
|
|
189
|
-
contentSystemId
|
|
193
|
+
uploadInstructionsAccess
|
|
190
194
|
};
|
|
191
195
|
const adapter = contentApiStorageAdapter(storageConfig);
|
|
192
196
|
const storageClient = createStorageClient(storageConfig);
|
|
@@ -198,13 +202,13 @@ function messageOf(error) {
|
|
|
198
202
|
/**
|
|
199
203
|
* Plugin that applies cloud storage configuration to all upload-enabled collections.
|
|
200
204
|
* Runs at the end of the plugin chain so it sees collections added by user plugins too.
|
|
201
|
-
*/ function createStoragePlugin(url, contentSystemId) {
|
|
205
|
+
*/ function createStoragePlugin(url, contentSystemId, uploadInstructionsAccess) {
|
|
202
206
|
return (incomingConfig)=>{
|
|
203
207
|
const uploadCollections = (incomingConfig.collections || []).filter((c)=>c.upload);
|
|
204
208
|
if (uploadCollections.length === 0) {
|
|
205
209
|
return incomingConfig;
|
|
206
210
|
}
|
|
207
|
-
const { adapter } = createStorageConfig(url, contentSystemId);
|
|
211
|
+
const { adapter } = createStorageConfig(url, contentSystemId, uploadInstructionsAccess);
|
|
208
212
|
const collectionsMap = uploadCollections.reduce((acc, c)=>{
|
|
209
213
|
acc[c.slug] = {
|
|
210
214
|
adapter,
|
|
@@ -338,9 +342,15 @@ export async function buildFigmaConfig(config) {
|
|
|
338
342
|
]
|
|
339
343
|
}
|
|
340
344
|
},
|
|
345
|
+
// Payload applies this policy during sanitization, after user plugins and
|
|
346
|
+
// generated resources have registered their collections and globals.
|
|
347
|
+
baseAccess: withMakeViewerWriteBaseAccessDefaults(config.baseAccess),
|
|
341
348
|
custom: {
|
|
342
349
|
figma: {
|
|
343
350
|
...config.figma,
|
|
351
|
+
...typeof config.figma.storage === 'object' ? {
|
|
352
|
+
storage: true
|
|
353
|
+
} : {},
|
|
344
354
|
contentSystemId,
|
|
345
355
|
environmentId
|
|
346
356
|
}
|
|
@@ -358,8 +368,8 @@ export async function buildFigmaConfig(config) {
|
|
|
358
368
|
},
|
|
359
369
|
// Gate /api/payload-jobs/run and /api/payload-jobs/handle-schedules. Honor the
|
|
360
370
|
// sites-worker trust marker so Figma infra job runs never break, defer to the
|
|
361
|
-
// tenant's jobs.access.run if they set one, otherwise
|
|
362
|
-
//
|
|
371
|
+
// tenant's jobs.access.run if they set one, otherwise require an authenticated
|
|
372
|
+
// user who is not a known Make viewer.
|
|
363
373
|
jobs: {
|
|
364
374
|
...config.jobs ?? {},
|
|
365
375
|
access: {
|
|
@@ -371,7 +381,9 @@ export async function buildFigmaConfig(config) {
|
|
|
371
381
|
if (userJobsAccessRun) {
|
|
372
382
|
return userJobsAccessRun(args);
|
|
373
383
|
}
|
|
374
|
-
|
|
384
|
+
// Allow undefined Make permissions during rollout, but fail closed for empty,
|
|
385
|
+
// explicit view, and other unrecognized values.
|
|
386
|
+
return Boolean(args.req.user) && passesMakeViewerWriteGate(args.req.user);
|
|
375
387
|
}
|
|
376
388
|
}
|
|
377
389
|
},
|
|
@@ -385,7 +397,7 @@ export async function buildFigmaConfig(config) {
|
|
|
385
397
|
plugins: [
|
|
386
398
|
...config.plugins ?? [],
|
|
387
399
|
...config.figma.storage !== false ? [
|
|
388
|
-
createStoragePlugin(url, contentSystemId)
|
|
400
|
+
createStoragePlugin(url, contentSystemId, typeof config.figma.storage === 'object' ? config.figma.storage.uploadInstructionsAccess : undefined)
|
|
389
401
|
] : [],
|
|
390
402
|
oAuth2Plugin({
|
|
391
403
|
collections: [
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Access, BaseAccess } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* we will still let a user edit if makePermission is null or undefined, because we are
|
|
4
|
+
* specifically restricting users for a figma cloud project backed by make
|
|
5
|
+
*/
|
|
6
|
+
export declare function passesMakeViewerWriteGate(user: unknown): boolean;
|
|
7
|
+
export declare const makeViewerWriteBaseAccess: Access;
|
|
8
|
+
export declare function withMakeViewerWriteBaseAccessDefaults(existingBaseAccess?: BaseAccess): BaseAccess;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* we will still let a user edit if makePermission is null or undefined, because we are
|
|
3
|
+
* specifically restricting users for a figma cloud project backed by make
|
|
4
|
+
*/ export function passesMakeViewerWriteGate(user) {
|
|
5
|
+
const makePermission = user?.makePermission;
|
|
6
|
+
return makePermission === undefined || makePermission === null || makePermission === 'edit';
|
|
7
|
+
}
|
|
8
|
+
export const makeViewerWriteBaseAccess = ({ req })=>passesMakeViewerWriteGate(req.user);
|
|
9
|
+
export function withMakeViewerWriteBaseAccessDefaults(existingBaseAccess = {}) {
|
|
10
|
+
return {
|
|
11
|
+
...existingBaseAccess,
|
|
12
|
+
collections: {
|
|
13
|
+
...existingBaseAccess.collections,
|
|
14
|
+
create: existingBaseAccess.collections?.create ?? makeViewerWriteBaseAccess,
|
|
15
|
+
delete: existingBaseAccess.collections?.delete ?? makeViewerWriteBaseAccess,
|
|
16
|
+
unlock: existingBaseAccess.collections?.unlock ?? makeViewerWriteBaseAccess,
|
|
17
|
+
update: existingBaseAccess.collections?.update ?? makeViewerWriteBaseAccess
|
|
18
|
+
},
|
|
19
|
+
globals: {
|
|
20
|
+
...existingBaseAccess.globals,
|
|
21
|
+
update: existingBaseAccess.globals?.update ?? makeViewerWriteBaseAccess
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { APIError, Forbidden } from 'payload';
|
|
3
|
+
import { passesMakeViewerWriteGate } from '../../plugin/make-permission-access/make-permission-access.js';
|
|
3
4
|
import { getSafeFilename } from '../utilities/index.js';
|
|
4
|
-
const defaultAccess = ({ req })
|
|
5
|
+
const defaultAccess = ({ req })=>Boolean(req.user) && passesMakeViewerWriteGate(req.user);
|
|
5
6
|
/**
|
|
6
7
|
* v4 client-upload entry point. Payload core calls this from
|
|
7
8
|
* `POST /api/upload-instructions`; we sign a Content API upload URL and hand the
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { UploadInstructionsAccess } from 'payload';
|
|
1
2
|
import type { AuthMode } from '../db-content-api/utilities/auth.js';
|
|
2
3
|
export type StorageClientConfig = {
|
|
4
|
+
uploadInstructionsAccess?: UploadInstructionsAccess;
|
|
5
|
+
} & ({
|
|
3
6
|
auth: AuthMode;
|
|
4
7
|
baseUrl: string;
|
|
5
8
|
contentSystemId: string;
|
|
6
9
|
} | {
|
|
7
10
|
baseUrl: string;
|
|
8
11
|
contentApiKey: string;
|
|
9
|
-
};
|
|
12
|
+
});
|
|
@@ -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,29 +27,25 @@ 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) {
|
|
40
41
|
const ssg = await collectSSGAssets(projectPath, output);
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Strip extension to get the base, then derive the route
|
|
45
|
-
const base = key.replace(/\.(?:html|rsc|meta)$/, '');
|
|
46
|
-
const route = base === 'index' ? '/' : `/${base}`;
|
|
47
|
-
if (!routeAssetMap[route]) {
|
|
48
|
-
routeAssetMap[route] = [];
|
|
49
|
-
}
|
|
50
|
-
routeAssetMap[route].push(key);
|
|
51
|
-
}
|
|
42
|
+
// collectSSGAssets records each key's manifest route while scanning the
|
|
43
|
+
// output, preserving the existing mapping for HTML/RSC/meta files while
|
|
44
|
+
// supporting segment keys whose nested names do not encode the page route.
|
|
52
45
|
return {
|
|
53
46
|
pathMap: ssg.pathMap,
|
|
54
|
-
routeAssetMap,
|
|
55
|
-
routes: Object.keys(routeAssetMap),
|
|
47
|
+
routeAssetMap: ssg.routeAssetMap,
|
|
48
|
+
routes: Object.keys(ssg.routeAssetMap),
|
|
56
49
|
uploadKeys: ssg.keys
|
|
57
50
|
};
|
|
58
51
|
}
|
|
@@ -23,14 +23,18 @@ export type SSGAssets = {
|
|
|
23
23
|
keys: string[];
|
|
24
24
|
/** Map of S3 key → relative filesystem path from project root */
|
|
25
25
|
pathMap: Record<string, string>;
|
|
26
|
+
/** Map of page route → S3 keys generated for that page */
|
|
27
|
+
routeAssetMap: Record<string, string[]>;
|
|
26
28
|
};
|
|
29
|
+
type StaticMetadataAssets = Omit<SSGAssets, 'routeAssetMap'>;
|
|
27
30
|
/**
|
|
28
31
|
* Collect SSG asset paths by parsing .next/prerender-manifest.json
|
|
29
32
|
*
|
|
30
|
-
* For each route in the manifest's `routes` object, collects
|
|
33
|
+
* For each page route in the manifest's `routes` object, collects:
|
|
31
34
|
* - {route}.html (full HTML)
|
|
32
35
|
* - {route}.rsc (React Server Components payload)
|
|
33
36
|
* - {route}.meta (response headers JSON)
|
|
37
|
+
* - Nested `.segment.rsc` files under {route}.segments (segment-prefetch payloads)
|
|
34
38
|
*
|
|
35
39
|
* S3 keys use the route path without leading slash.
|
|
36
40
|
* Filesystem paths point to .next/server/app/{route}.{ext}
|
|
@@ -39,6 +43,14 @@ export type SSGAssets = {
|
|
|
39
43
|
* @returns SSG asset keys and a pathMap for filesystem resolution
|
|
40
44
|
*/
|
|
41
45
|
export declare function collectSSGAssets(projectPath: string, output?: string): Promise<SSGAssets>;
|
|
46
|
+
/**
|
|
47
|
+
* Collect file-based Next.js metadata icons that were prerendered as static route handlers.
|
|
48
|
+
*
|
|
49
|
+
* Maps public asset keys such as `shop/icon1.png` to their raw
|
|
50
|
+
* `.next/server/app/shop/icon1.png.body` output.
|
|
51
|
+
* Documentation: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons
|
|
52
|
+
*/
|
|
53
|
+
export declare function collectStaticMetadataAssets(projectPath: string, output?: string): Promise<StaticMetadataAssets>;
|
|
42
54
|
/**
|
|
43
55
|
* Create Lambda deployment zip
|
|
44
56
|
*
|
|
@@ -48,3 +60,4 @@ export declare function collectSSGAssets(projectPath: string, output?: string):
|
|
|
48
60
|
* @throws Error if build missing or zip creation fails
|
|
49
61
|
*/
|
|
50
62
|
export declare function createLambdaZip(projectPath: string): Promise<void>;
|
|
63
|
+
export {};
|
|
@@ -77,10 +77,11 @@ 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
|
|
80
|
+
* For each page route in the manifest's `routes` object, collects:
|
|
81
81
|
* - {route}.html (full HTML)
|
|
82
82
|
* - {route}.rsc (React Server Components payload)
|
|
83
83
|
* - {route}.meta (response headers JSON)
|
|
84
|
+
* - Nested `.segment.rsc` files under {route}.segments (segment-prefetch payloads)
|
|
84
85
|
*
|
|
85
86
|
* S3 keys use the route path without leading slash.
|
|
86
87
|
* Filesystem paths point to .next/server/app/{route}.{ext}
|
|
@@ -97,30 +98,106 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
97
98
|
} catch {
|
|
98
99
|
return {
|
|
99
100
|
keys: [],
|
|
100
|
-
pathMap: {}
|
|
101
|
+
pathMap: {},
|
|
102
|
+
routeAssetMap: {}
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
105
|
if (!manifest.routes || typeof manifest.routes !== 'object') {
|
|
104
106
|
return {
|
|
105
107
|
keys: [],
|
|
106
|
-
pathMap: {}
|
|
108
|
+
pathMap: {},
|
|
109
|
+
routeAssetMap: {}
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
const keys = [];
|
|
110
113
|
const pathMap = {};
|
|
114
|
+
const routeAssetMap = {};
|
|
111
115
|
const extensions = [
|
|
112
116
|
'.html',
|
|
113
117
|
'.rsc',
|
|
114
118
|
'.meta'
|
|
115
119
|
];
|
|
116
|
-
|
|
120
|
+
const appDir = path.join(outputPath, 'server', 'app');
|
|
121
|
+
for (const [routeKey, route] of Object.entries(manifest.routes)){
|
|
122
|
+
// Current manifests classify app pages explicitly. Older App Router manifests
|
|
123
|
+
// can be identified by their RSC data route. Other entries may be route handlers.
|
|
124
|
+
const isPageRoute = route.routeType === 'page' || route.routeType === undefined && typeof route.dataRoute === 'string' && route.dataRoute.endsWith('.rsc');
|
|
125
|
+
if (!isPageRoute) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
117
128
|
// Strip leading slash for S3 key; handle root "/" → "index"
|
|
118
129
|
const stripped = routeKey === '/' ? 'index' : routeKey.replace(/^\//, '');
|
|
130
|
+
const routeAssets = [];
|
|
119
131
|
for (const ext of extensions){
|
|
120
132
|
const s3Key = `${stripped}${ext}`;
|
|
121
133
|
keys.push(s3Key);
|
|
122
|
-
|
|
134
|
+
routeAssets.push(s3Key);
|
|
135
|
+
pathMap[s3Key] = path.relative(projectPath, path.join(appDir, s3Key));
|
|
136
|
+
}
|
|
137
|
+
// Next.js stores segment-prefetch payloads beside each prerendered page in a
|
|
138
|
+
// `<page>.segments` tree. Scan the generated tree instead of reconstructing
|
|
139
|
+
// its opaque, potentially nested segment names from route information.
|
|
140
|
+
const segmentFiles = await collectFiles(path.join(appDir, `${stripped}.segments`), appDir);
|
|
141
|
+
for (const segmentFile of segmentFiles.filter((file)=>file.endsWith('.segment.rsc'))){
|
|
142
|
+
const s3Key = segmentFile.split(path.sep).join('/');
|
|
143
|
+
keys.push(s3Key);
|
|
144
|
+
routeAssets.push(s3Key);
|
|
145
|
+
pathMap[s3Key] = path.relative(projectPath, path.join(appDir, segmentFile));
|
|
123
146
|
}
|
|
147
|
+
// Preserve the manifest route association for the original HTML/RSC/meta
|
|
148
|
+
// files and the new segment files; segment keys cannot be mapped back to a
|
|
149
|
+
// page reliably from their filename in NextjsAdapter.
|
|
150
|
+
routeAssetMap[routeKey] = routeAssets;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
keys,
|
|
154
|
+
pathMap,
|
|
155
|
+
routeAssetMap
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Collect file-based Next.js metadata icons that were prerendered as static route handlers.
|
|
160
|
+
*
|
|
161
|
+
* Maps public asset keys such as `shop/icon1.png` to their raw
|
|
162
|
+
* `.next/server/app/shop/icon1.png.body` output.
|
|
163
|
+
* Documentation: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons
|
|
164
|
+
*/ export async function collectStaticMetadataAssets(projectPath, output) {
|
|
165
|
+
const outputPath = resolveOutputPath(projectPath, 'nextjs', output);
|
|
166
|
+
const manifestPath = path.join(outputPath, 'prerender-manifest.json');
|
|
167
|
+
let manifest;
|
|
168
|
+
try {
|
|
169
|
+
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
170
|
+
manifest = JSON.parse(raw);
|
|
171
|
+
} catch {
|
|
172
|
+
return {
|
|
173
|
+
keys: [],
|
|
174
|
+
pathMap: {}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (!manifest.routes || typeof manifest.routes !== 'object') {
|
|
178
|
+
return {
|
|
179
|
+
keys: [],
|
|
180
|
+
pathMap: {}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const keys = [];
|
|
184
|
+
const pathMap = {};
|
|
185
|
+
for (const [routeKey, route] of Object.entries(manifest.routes)){
|
|
186
|
+
if (route.routeType !== 'route' || route.compute !== 'static' || route.response !== 'complete' || !isFileBasedMetadataIconRoute(routeKey)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const assetKey = routeKey.replace(/^\/+/, '');
|
|
190
|
+
const bodyPath = path.join(outputPath, 'server', 'app', `${assetKey}.body`);
|
|
191
|
+
try {
|
|
192
|
+
const bodyStat = await fs.stat(bodyPath);
|
|
193
|
+
if (!bodyStat.isFile()) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
keys.push(assetKey);
|
|
200
|
+
pathMap[assetKey] = path.relative(projectPath, bodyPath);
|
|
124
201
|
}
|
|
125
202
|
return {
|
|
126
203
|
keys,
|
|
@@ -137,3 +214,10 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
|
|
|
137
214
|
*/ export async function createLambdaZip(projectPath) {
|
|
138
215
|
await buildLambdaZip(projectPath);
|
|
139
216
|
}
|
|
217
|
+
function isFileBasedMetadataIconRoute(routeKey) {
|
|
218
|
+
const basename = path.posix.basename(routeKey);
|
|
219
|
+
if (routeKey === '/favicon.ico') {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
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);
|
|
223
|
+
}
|
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.7",
|
|
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",
|
|
@@ -56,7 +58,11 @@
|
|
|
56
58
|
"devDependencies": {
|
|
57
59
|
"@payloadcms/eslint-config": "3.28.0",
|
|
58
60
|
"@payloadcms/eslint-plugin": "3.28.0",
|
|
59
|
-
"@payloadcms/next": "4.0.0-canary.
|
|
61
|
+
"@payloadcms/next": "4.0.0-canary.28",
|
|
62
|
+
"@payloadcms/plugin-cloud-storage": "4.0.0-canary.28",
|
|
63
|
+
"@payloadcms/richtext-lexical": "4.0.0-canary.28",
|
|
64
|
+
"@payloadcms/translations": "4.0.0-canary.28",
|
|
65
|
+
"@payloadcms/ui": "4.0.0-canary.28",
|
|
60
66
|
"@swc/cli": "0.7.7",
|
|
61
67
|
"@types/archiver": "7.0.0",
|
|
62
68
|
"@types/cross-spawn": "6.0.6",
|
|
@@ -67,15 +73,19 @@
|
|
|
67
73
|
"eslint": "9.22.0",
|
|
68
74
|
"next": "^16.2.6",
|
|
69
75
|
"openapi-typescript": "^7.13.0",
|
|
76
|
+
"payload": "4.0.0-canary.28",
|
|
70
77
|
"rimraf": "^6.1.3",
|
|
71
78
|
"tsx": "4.20.6",
|
|
72
79
|
"typescript": "5.7.3",
|
|
73
80
|
"vitest": "4.0.15"
|
|
74
81
|
},
|
|
75
82
|
"peerDependencies": {
|
|
76
|
-
"@payloadcms/plugin-cloud-storage": ">=4.0.0-canary.
|
|
77
|
-
"@payloadcms/richtext-lexical": ">=4.0.0-canary.
|
|
78
|
-
"
|
|
83
|
+
"@payloadcms/plugin-cloud-storage": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
84
|
+
"@payloadcms/richtext-lexical": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
85
|
+
"@payloadcms/translations": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
86
|
+
"@payloadcms/ui": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
87
|
+
"payload": ">=4.0.0-canary.28 <4.0.0-internal",
|
|
88
|
+
"react": "^19.0.1 || ^19.1.2 || ^19.2.1"
|
|
79
89
|
},
|
|
80
90
|
"peerDependenciesMeta": {
|
|
81
91
|
"@payloadcms/plugin-cloud-storage": {
|