@payloadcms/figma 0.0.1-alpha.60 → 0.0.1-alpha.62
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/api/control-plane.d.ts +4 -0
- package/dist/api/control-plane.js +11 -4
- package/dist/cli.js +3 -1
- package/dist/commands/deploy.js +2 -0
- package/dist/commands/init.js +1 -1
- package/dist/config/oauth.d.ts +10 -2
- package/dist/config/oauth.js +18 -4
- package/dist/constants.d.ts +4 -2
- package/dist/constants.js +19 -1
- package/dist/db-content-api/index.js +152 -4
- package/dist/db-content-api/utilities/data/applyDefaults.js +20 -4
- package/dist/db-content-api/utilities/data/index.js +17 -0
- package/dist/db-content-api/utilities/data/validateRelationships.d.ts +7 -0
- package/dist/db-content-api/utilities/data/validateRelationships.js +102 -0
- package/dist/db-content-api/utilities/joins.d.ts +1 -3
- package/dist/db-content-api/utilities/joins.js +9 -5
- package/dist/db-content-api/utilities/where.js +4 -0
- package/dist/storage-content-api/staticHandler.js +1 -18
- package/dist/utils/build-lambda-zip.d.ts +4 -2
- package/dist/utils/build-lambda-zip.js +4 -7
- package/dist/utils/lambda-config.js +6 -17
- package/dist/utils/messages.js +1 -1
- package/dist/utils/payload-config-ast.js +22 -4
- package/dist/utils/version-check.js +1 -1
- package/package.json +7 -7
|
@@ -28,6 +28,10 @@ export interface CreateTenantOptions {
|
|
|
28
28
|
* Options for creating a new deployment
|
|
29
29
|
*/
|
|
30
30
|
export interface CreateDeploymentOptions {
|
|
31
|
+
/** Framework adapter name (e.g., "vite", "nextjs"). Forwarded to Gatekeeper. */
|
|
32
|
+
adapter?: string;
|
|
33
|
+
/** SPA fallback path (e.g., "/index.html"). Vite-only. */
|
|
34
|
+
fallback?: string;
|
|
31
35
|
/** Pages keyed by route, each with its associated asset keys */
|
|
32
36
|
pages: Record<string, {
|
|
33
37
|
assets: string[];
|
|
@@ -184,14 +184,21 @@ import * as log from '../utils/log.js';
|
|
|
184
184
|
assets: pageData.assets
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
|
+
const body = {
|
|
188
|
+
pages,
|
|
189
|
+
static_assets: options.staticAssets
|
|
190
|
+
};
|
|
191
|
+
if (options.adapter) {
|
|
192
|
+
body.adapter = options.adapter;
|
|
193
|
+
}
|
|
194
|
+
if (options.fallback) {
|
|
195
|
+
body.fallback = options.fallback;
|
|
196
|
+
}
|
|
187
197
|
// REAL API IMPLEMENTATION
|
|
188
198
|
const response = await controlPlaneFetch({
|
|
189
199
|
context: 'create deployment',
|
|
190
200
|
options: {
|
|
191
|
-
body: JSON.stringify(
|
|
192
|
-
pages,
|
|
193
|
-
static_assets: options.staticAssets
|
|
194
|
-
}),
|
|
201
|
+
body: JSON.stringify(body),
|
|
195
202
|
headers: {
|
|
196
203
|
...getAuthHeaders(credential),
|
|
197
204
|
'Content-Type': 'application/json'
|
package/dist/cli.js
CHANGED
|
@@ -68,9 +68,11 @@ class Main {
|
|
|
68
68
|
setInfraEnvironment('production');
|
|
69
69
|
} else if (infraEnvArg === 'staging') {
|
|
70
70
|
setInfraEnvironment('staging');
|
|
71
|
+
} else if (infraEnvArg === 'devbox') {
|
|
72
|
+
setInfraEnvironment('devbox');
|
|
71
73
|
} else {
|
|
72
74
|
// eslint-disable-next-line no-console
|
|
73
|
-
console.error(`Invalid --infra-env value: ${this.args['--infra-env']}. Use 'production' or '
|
|
75
|
+
console.error(`Invalid --infra-env value: ${this.args['--infra-env']}. Use 'production', 'staging', or 'devbox'.`);
|
|
74
76
|
process.exit(1);
|
|
75
77
|
}
|
|
76
78
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -241,6 +241,8 @@ import { loginCommand } from './login.js';
|
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
243
|
const createResponse = await createDeployment(credential, tenantInstanceId, {
|
|
244
|
+
adapter: adapter.name,
|
|
245
|
+
fallback: adapter.fallback,
|
|
244
246
|
pages: pagesPayload,
|
|
245
247
|
staticAssets: assets.uploadKeys
|
|
246
248
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -67,7 +67,7 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
67
67
|
*
|
|
68
68
|
* @param options - Command options
|
|
69
69
|
*/ export async function initCommand(options) {
|
|
70
|
-
// Check for
|
|
70
|
+
// Check for mismatched version (non-blocking)
|
|
71
71
|
const currentVersion = await getOwnVersion();
|
|
72
72
|
await checkForUpdates(currentVersion);
|
|
73
73
|
// Check authentication
|
package/dist/config/oauth.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { OAuthConfig } from '../auth/types.js';
|
|
2
2
|
export declare const DEFAULT_CALLBACK_PORT = 34462;
|
|
3
3
|
/**
|
|
4
|
-
* Get OAuth configuration for current environment
|
|
5
|
-
*
|
|
4
|
+
* Get OAuth configuration for current environment.
|
|
5
|
+
*
|
|
6
|
+
* Honors env overrides:
|
|
7
|
+
* FIGMA_API_BASE_URL → tokenUrl, refreshUrl
|
|
8
|
+
* FIGMA_WEB_BASE_URL → authorizationUrl (appends /oauth)
|
|
9
|
+
* FIGMA_CLIENT_ID → clientId
|
|
10
|
+
* FIGMA_REDIRECT_URI → redirectUri
|
|
11
|
+
*
|
|
12
|
+
* When FIGMA_INFRA_ENV=devbox, FIGMA_API_BASE_URL and FIGMA_WEB_BASE_URL are
|
|
13
|
+
* required (ENV_CONFIG.devbox has empty URL defaults to fail-closed).
|
|
6
14
|
*/
|
|
7
15
|
export declare function getOAuthConfig(): OAuthConfig;
|
|
8
16
|
export declare const TOKEN_EXPIRY_BUFFER_SECONDS = 300;
|
package/dist/config/oauth.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import { getEnvConfig } from '../constants.js';
|
|
1
|
+
import { getEnvConfig, getInfraEnvironment } from '../constants.js';
|
|
2
2
|
export const DEFAULT_CALLBACK_PORT = 34462;
|
|
3
3
|
/**
|
|
4
|
-
* Get OAuth configuration for current environment
|
|
5
|
-
*
|
|
4
|
+
* Get OAuth configuration for current environment.
|
|
5
|
+
*
|
|
6
|
+
* Honors env overrides:
|
|
7
|
+
* FIGMA_API_BASE_URL → tokenUrl, refreshUrl
|
|
8
|
+
* FIGMA_WEB_BASE_URL → authorizationUrl (appends /oauth)
|
|
9
|
+
* FIGMA_CLIENT_ID → clientId
|
|
10
|
+
* FIGMA_REDIRECT_URI → redirectUri
|
|
11
|
+
*
|
|
12
|
+
* When FIGMA_INFRA_ENV=devbox, FIGMA_API_BASE_URL and FIGMA_WEB_BASE_URL are
|
|
13
|
+
* required (ENV_CONFIG.devbox has empty URL defaults to fail-closed).
|
|
6
14
|
*/ export function getOAuthConfig() {
|
|
7
|
-
const { apiBaseUrl, authorizationUrl, clientId } = getEnvConfig();
|
|
15
|
+
const { apiBaseUrl: defaultApi, authorizationUrl: defaultAuth, clientId } = getEnvConfig();
|
|
16
|
+
const apiBaseUrl = process.env.FIGMA_API_BASE_URL || defaultApi;
|
|
17
|
+
const webBase = process.env.FIGMA_WEB_BASE_URL;
|
|
18
|
+
const authorizationUrl = webBase ? `${webBase}/oauth` : defaultAuth;
|
|
19
|
+
if (getInfraEnvironment() === 'devbox' && (!apiBaseUrl || !authorizationUrl)) {
|
|
20
|
+
throw new Error('FIGMA_API_BASE_URL and FIGMA_WEB_BASE_URL must be set when FIGMA_INFRA_ENV=devbox');
|
|
21
|
+
}
|
|
8
22
|
return {
|
|
9
23
|
authorizationUrl,
|
|
10
24
|
clientId: process.env.FIGMA_CLIENT_ID || clientId,
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Figma infrastructure environment (production or
|
|
2
|
+
* Figma infrastructure environment (production, staging, or devbox).
|
|
3
|
+
* devbox is a local-dev mode that requires FIGMA_API_BASE_URL and
|
|
4
|
+
* FIGMA_WEB_BASE_URL env overrides to point at a Coder devbox.
|
|
3
5
|
*/
|
|
4
|
-
export type Environment = 'production' | 'staging';
|
|
6
|
+
export type Environment = 'devbox' | 'production' | 'staging';
|
|
5
7
|
type EnvironmentConfig = {
|
|
6
8
|
/** Figma API base URL. Override: FIGMA_API_BASE_URL */
|
|
7
9
|
apiBaseUrl: string;
|
package/dist/constants.js
CHANGED
|
@@ -18,6 +18,17 @@ import { getEnvVarSync } from './utils/env-management.js';
|
|
|
18
18
|
contentApiUrl: 'https://us-east-1.cms-tenants-001-staging.figmacontentstaging.com',
|
|
19
19
|
identityMetadata: 'https://staging.figma.com/.well-known/openid-configuration',
|
|
20
20
|
jwksUri: 'https://static.figmacontentstaging.com/.well_known/jwks.json'
|
|
21
|
+
},
|
|
22
|
+
// Empty URLs are intentional: devbox mode requires FIGMA_API_BASE_URL and
|
|
23
|
+
// FIGMA_WEB_BASE_URL to be set, validated at getOAuthConfig() time.
|
|
24
|
+
// clientId defaults to the dev seed value in sinatra/db/seeds.rb.
|
|
25
|
+
devbox: {
|
|
26
|
+
apiBaseUrl: '',
|
|
27
|
+
authorizationUrl: '',
|
|
28
|
+
clientId: 'rNZBcf3xBDmI76mQ9603su',
|
|
29
|
+
contentApiUrl: '',
|
|
30
|
+
identityMetadata: '',
|
|
31
|
+
jwksUri: ''
|
|
21
32
|
}
|
|
22
33
|
};
|
|
23
34
|
/**
|
|
@@ -58,9 +69,13 @@ export function getInfraEnvironment() {
|
|
|
58
69
|
return envOverride;
|
|
59
70
|
}
|
|
60
71
|
// Check process.env first (case-insensitive)
|
|
61
|
-
|
|
72
|
+
const processEnv = process.env.FIGMA_INFRA_ENV?.toLowerCase();
|
|
73
|
+
if (processEnv === 'staging') {
|
|
62
74
|
return 'staging';
|
|
63
75
|
}
|
|
76
|
+
if (processEnv === 'devbox') {
|
|
77
|
+
return 'devbox';
|
|
78
|
+
}
|
|
64
79
|
// Fall back to .env file in cwd (check both new and old env var names)
|
|
65
80
|
const envFileValue = getEnvVarSync(process.cwd(), 'FIGMA_INFRA_ENV') ?? getEnvVarSync(process.cwd(), 'FIGMA_ENV');
|
|
66
81
|
if (envFileValue) {
|
|
@@ -68,6 +83,9 @@ export function getInfraEnvironment() {
|
|
|
68
83
|
if (env === 'staging') {
|
|
69
84
|
return 'staging';
|
|
70
85
|
}
|
|
86
|
+
if (env === 'devbox') {
|
|
87
|
+
return 'devbox';
|
|
88
|
+
}
|
|
71
89
|
if (env !== 'production') {
|
|
72
90
|
// eslint-disable-next-line no-console
|
|
73
91
|
console.warn(`Warning: Invalid FIGMA_INFRA_ENV value "${envFileValue}" in .env file. Using production.`);
|
|
@@ -8,6 +8,7 @@ import { addFallbackSort } from './temp-utilities/sorting.js';
|
|
|
8
8
|
import { unwrapDocument, unwrapFindResponse } from './temp-utilities/unwrapDocument.js';
|
|
9
9
|
import { createAuthMiddleware, createErrorMiddleware } from './utilities/auth.js';
|
|
10
10
|
import { dataToContentAPI, resolveVersionContent } from './utilities/data/index.js';
|
|
11
|
+
import { validateRelationshipIds } from './utilities/data/validateRelationships.js';
|
|
11
12
|
import { convertPayloadJoinsToContentAPI } from './utilities/joins.js';
|
|
12
13
|
import { addFallbackLocale } from './utilities/locale/index.js';
|
|
13
14
|
import { buildMeta } from './utilities/meta/buildMeta.js';
|
|
@@ -56,7 +57,7 @@ async function syncCollections() {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
function init() {
|
|
60
61
|
if (this.auth.mode === 'apiKey') {
|
|
61
62
|
this.payload.logger.info('Using API Key authentication');
|
|
62
63
|
} else if (this.auth.mode === 'tokenStore') {
|
|
@@ -64,8 +65,15 @@ async function init() {
|
|
|
64
65
|
} else {
|
|
65
66
|
this.payload.logger.info('Using Dev JWT authentication (testing)');
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
}
|
|
69
|
+
// connect is called after init — once on first startup (hotReload=false) and again on HMR
|
|
70
|
+
// (hotReload=true). Only clear the database on the first startup, never on hot reload.
|
|
71
|
+
async function connect(args) {
|
|
72
|
+
const hotReload = args?.hotReload ?? false;
|
|
73
|
+
if (!hotReload && process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
|
74
|
+
// clearDatabase already calls syncCollections internally after clearing
|
|
68
75
|
await this.clearDatabase();
|
|
76
|
+
return;
|
|
69
77
|
}
|
|
70
78
|
await syncCollections.call(this);
|
|
71
79
|
}
|
|
@@ -99,7 +107,7 @@ async function findMany({ collection, joins, limit, locale: localeArg, page, pag
|
|
|
99
107
|
body: {
|
|
100
108
|
collection,
|
|
101
109
|
contentSystemId: this.contentSystemId,
|
|
102
|
-
join: convertPayloadJoinsToContentAPI(this.payload, collection, joins),
|
|
110
|
+
join: convertPayloadJoinsToContentAPI(this.payload, collection, joins, locale),
|
|
103
111
|
limit,
|
|
104
112
|
locale,
|
|
105
113
|
page: page ?? 1,
|
|
@@ -346,6 +354,11 @@ async function findDistinct(args) {
|
|
|
346
354
|
}
|
|
347
355
|
async function updateMany(args) {
|
|
348
356
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
357
|
+
validateRelationshipIds({
|
|
358
|
+
collectionSlug: args.collection,
|
|
359
|
+
data: args.data,
|
|
360
|
+
payload: this.payload
|
|
361
|
+
});
|
|
349
362
|
const { data: response, error } = await this.client.POST('/api/v0/documents:update', {
|
|
350
363
|
body: {
|
|
351
364
|
collection: args.collection,
|
|
@@ -387,6 +400,11 @@ async function updateOne(args) {
|
|
|
387
400
|
equals: args.id
|
|
388
401
|
}
|
|
389
402
|
};
|
|
403
|
+
validateRelationshipIds({
|
|
404
|
+
collectionSlug: args.collection,
|
|
405
|
+
data: args.data,
|
|
406
|
+
payload: this.payload
|
|
407
|
+
});
|
|
390
408
|
const { data: response, error } = await this.client.POST('/api/v0/documents:update', {
|
|
391
409
|
body: {
|
|
392
410
|
collection: args.collection,
|
|
@@ -491,6 +509,11 @@ async function create(args) {
|
|
|
491
509
|
id = uuid();
|
|
492
510
|
}
|
|
493
511
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
512
|
+
validateRelationshipIds({
|
|
513
|
+
collectionSlug: args.collection,
|
|
514
|
+
data: args.data,
|
|
515
|
+
payload: this.payload
|
|
516
|
+
});
|
|
494
517
|
const { data: response, error } = await this.client.POST('/api/v0/documents:create', {
|
|
495
518
|
body: {
|
|
496
519
|
collection: args.collection,
|
|
@@ -604,6 +627,11 @@ async function upsert(args) {
|
|
|
604
627
|
documentId = uuid();
|
|
605
628
|
}
|
|
606
629
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
630
|
+
validateRelationshipIds({
|
|
631
|
+
collectionSlug: args.collection,
|
|
632
|
+
data: args.data,
|
|
633
|
+
payload: this.payload
|
|
634
|
+
});
|
|
607
635
|
const { data: response, error } = await this.client.POST('/api/v0/documents:update', {
|
|
608
636
|
body: {
|
|
609
637
|
collection: args.collection,
|
|
@@ -645,13 +673,131 @@ async function upsert(args) {
|
|
|
645
673
|
payload: this.payload
|
|
646
674
|
});
|
|
647
675
|
}
|
|
676
|
+
// Mirrors MAX_DOCUMENT_UPDATES in the content API's updateDocuments.ts.
|
|
677
|
+
// updateJobs may be called with larger limits (e.g. 150), so we batch sequentially.
|
|
678
|
+
const CONTENT_API_MAX_UPDATES = 20;
|
|
679
|
+
// Issues a single update request against payload-jobs and returns the unwrapped docs.
|
|
680
|
+
async function updateJobsBatch({ batchSize, docData, meta, sortClause, whereQuery }) {
|
|
681
|
+
const { data: response, error } = await this.client.POST('/api/v0/documents:update', {
|
|
682
|
+
body: {
|
|
683
|
+
collection: 'payload-jobs',
|
|
684
|
+
contentSystemId: this.contentSystemId,
|
|
685
|
+
createOnMissing: false,
|
|
686
|
+
doc: docData,
|
|
687
|
+
...batchSize != null && {
|
|
688
|
+
limit: batchSize
|
|
689
|
+
},
|
|
690
|
+
returning: {},
|
|
691
|
+
sort: sortClause,
|
|
692
|
+
where: whereQuery,
|
|
693
|
+
...meta
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
if (error) {
|
|
697
|
+
throw new Error(`Content API updateJobs error: ${JSON.stringify(error)}`);
|
|
698
|
+
}
|
|
699
|
+
if (!response) {
|
|
700
|
+
throw new Error('No response from updateJobs');
|
|
701
|
+
}
|
|
702
|
+
if (!response.result || !('data' in response.result)) {
|
|
703
|
+
return [];
|
|
704
|
+
}
|
|
705
|
+
return response.result.data.map((doc)=>unwrapDocument({
|
|
706
|
+
collectionSlug: 'payload-jobs',
|
|
707
|
+
doc,
|
|
708
|
+
payload: this.payload
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
async function updateJobs(args) {
|
|
712
|
+
const { id, limit, returning, sort, where } = args;
|
|
713
|
+
if (id == null && where == null) {
|
|
714
|
+
throw new Error('updateJobs requires either id or a where clause');
|
|
715
|
+
}
|
|
716
|
+
// Copy data to avoid mutating the caller's object.
|
|
717
|
+
const data = {
|
|
718
|
+
...args.data
|
|
719
|
+
};
|
|
720
|
+
// Strip log if empty/absent. The content API's deep merge replaces arrays entirely
|
|
721
|
+
// (it does not concatenate), so sending log: [] would wipe existing log entries.
|
|
722
|
+
// Only include log when it carries new content: a non-empty array or a $push operation.
|
|
723
|
+
const log = data.log;
|
|
724
|
+
if (!(Array.isArray(log) && log.length > 0) && !(log && typeof log === 'object' && '$push' in log)) {
|
|
725
|
+
delete data.log;
|
|
726
|
+
}
|
|
727
|
+
const whereClause = id != null ? {
|
|
728
|
+
id: {
|
|
729
|
+
equals: String(id)
|
|
730
|
+
}
|
|
731
|
+
} : where;
|
|
732
|
+
const docData = dataToContentAPI(this.payload, 'payload-jobs', data);
|
|
733
|
+
const sortClause = addFallbackSort(sort || '-updatedAt', this.payload, 'payload-jobs');
|
|
734
|
+
const whereQuery = convertPayloadWhereToContentAPI(whereClause);
|
|
735
|
+
const meta = buildMeta(this.payload, {
|
|
736
|
+
collection: 'payload-jobs',
|
|
737
|
+
locale: undefined,
|
|
738
|
+
where: whereClause
|
|
739
|
+
});
|
|
740
|
+
// When limit exceeds the content API's per-request cap, batch sequentially.
|
|
741
|
+
const needsBatching = limit != null && limit > CONTENT_API_MAX_UPDATES;
|
|
742
|
+
if (!needsBatching) {
|
|
743
|
+
const docs = await updateJobsBatch.call(this, {
|
|
744
|
+
batchSize: limit,
|
|
745
|
+
docData,
|
|
746
|
+
meta,
|
|
747
|
+
sortClause,
|
|
748
|
+
whereQuery
|
|
749
|
+
});
|
|
750
|
+
if (returning === false) {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
return docs;
|
|
754
|
+
}
|
|
755
|
+
// Batched path: issue sequential requests of ≤ CONTENT_API_MAX_UPDATES each.
|
|
756
|
+
// This relies on the update itself causing matched jobs to no longer satisfy the where
|
|
757
|
+
// clause on subsequent batches (e.g. setting processing: true removes them from a
|
|
758
|
+
// "processing: false" query). If a future updateJobs call in Payload core updates jobs
|
|
759
|
+
// in a way that does NOT change their match status, the same jobs could be updated
|
|
760
|
+
// multiple times across batches.
|
|
761
|
+
const allDocs = [];
|
|
762
|
+
let remaining = limit;
|
|
763
|
+
while(remaining > 0){
|
|
764
|
+
const batchSize = Math.min(remaining, CONTENT_API_MAX_UPDATES);
|
|
765
|
+
const batchDocs = await updateJobsBatch.call(this, {
|
|
766
|
+
batchSize,
|
|
767
|
+
docData,
|
|
768
|
+
meta,
|
|
769
|
+
sortClause,
|
|
770
|
+
whereQuery
|
|
771
|
+
});
|
|
772
|
+
allDocs.push(...batchDocs);
|
|
773
|
+
remaining -= batchSize;
|
|
774
|
+
// Stop early when this batch returned fewer docs than requested — no more matching docs.
|
|
775
|
+
if (batchDocs.length < batchSize) {
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (returning === false) {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
return allDocs;
|
|
783
|
+
}
|
|
648
784
|
function createGlobal(args) {
|
|
649
|
-
|
|
785
|
+
// Globals are singletons identified by their slug. Use upsert so concurrent calls
|
|
786
|
+
// (e.g. handleSchedules processing multiple queueables with the same null jobStats)
|
|
787
|
+
// don't fail with "A record with this field already exists". The deep-merge semantics
|
|
788
|
+
// of the update path are safe here: callers that want a fresh create (first run) get
|
|
789
|
+
// an effective create, and callers that race a prior create get a merge instead.
|
|
790
|
+
return this.upsert({
|
|
650
791
|
collection: getGlobalSlug(args.slug),
|
|
651
792
|
data: {
|
|
652
793
|
...args.data,
|
|
653
794
|
id: args.slug,
|
|
654
795
|
globalType: args.slug
|
|
796
|
+
},
|
|
797
|
+
where: {
|
|
798
|
+
id: {
|
|
799
|
+
equals: args.slug
|
|
800
|
+
}
|
|
655
801
|
}
|
|
656
802
|
});
|
|
657
803
|
}
|
|
@@ -749,6 +895,7 @@ export const contentAPIAdapter = (opts)=>({
|
|
|
749
895
|
clearDatabase: clearDatabase,
|
|
750
896
|
client,
|
|
751
897
|
commitTransaction: async ()=>{},
|
|
898
|
+
connect: connect,
|
|
752
899
|
contentSystemId: opts.contentSystemId,
|
|
753
900
|
count: count,
|
|
754
901
|
countGlobalVersions: countGlobalVersions,
|
|
@@ -775,6 +922,7 @@ export const contentAPIAdapter = (opts)=>({
|
|
|
775
922
|
rollbackTransaction: async ()=>{},
|
|
776
923
|
updateGlobal: updateGlobal,
|
|
777
924
|
updateGlobalVersion: updateGlobalVersion,
|
|
925
|
+
updateJobs: updateJobs,
|
|
778
926
|
updateMany: updateMany,
|
|
779
927
|
updateOne: updateOne,
|
|
780
928
|
updateVersion: updateVersion,
|
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
* This handles defaults for nested structures (arrays, groups) which traverseFields doesn't handle
|
|
4
4
|
*/ export function applyDefaults(data, fields) {
|
|
5
5
|
for (const field of fields){
|
|
6
|
+
// Tabs fields: unnamed tabs are transparent (fields at the same level); named tabs nest
|
|
7
|
+
// their fields under the tab's name key. Recurse before the name-based guard so
|
|
8
|
+
// tab-nested fields (e.g. hasError in payload-jobs) get their defaults applied.
|
|
9
|
+
if (field.type === 'tabs') {
|
|
10
|
+
for (const tab of field.tabs){
|
|
11
|
+
if ('name' in tab && tab.name) {
|
|
12
|
+
if (!data[tab.name]) {
|
|
13
|
+
data[tab.name] = {};
|
|
14
|
+
}
|
|
15
|
+
applyDefaults(data[tab.name], tab.fields);
|
|
16
|
+
} else {
|
|
17
|
+
applyDefaults(data, tab.fields);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
6
22
|
// Only process fields that affect data (have a name)
|
|
7
23
|
if (!('name' in field) || !field.name) {
|
|
8
24
|
continue;
|
|
@@ -37,11 +53,11 @@
|
|
|
37
53
|
applyDefaults(element, field.fields);
|
|
38
54
|
}
|
|
39
55
|
}
|
|
40
|
-
} else if (field.type === 'group' &&
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
applyDefaults(currentValue, field.fields);
|
|
56
|
+
} else if (field.type === 'group' && 'fields' in field) {
|
|
57
|
+
if (currentValue == null || typeof currentValue !== 'object') {
|
|
58
|
+
data[field.name] = {};
|
|
44
59
|
}
|
|
60
|
+
applyDefaults(data[field.name], field.fields);
|
|
45
61
|
} else if (field.type === 'blocks' && Array.isArray(currentValue) && 'blocks' in field) {
|
|
46
62
|
// Apply defaults to each block
|
|
47
63
|
for (const element of currentValue){
|
|
@@ -179,6 +179,23 @@ export function dataFromContentAPI(payload, collectionSlug, data, locale) {
|
|
|
179
179
|
const current = ref;
|
|
180
180
|
const value = current[field.name];
|
|
181
181
|
const isLocalized = 'localized' in field && field.localized;
|
|
182
|
+
// Group fields must always be an object so Payload can traverse sub-fields
|
|
183
|
+
// (e.g. join fields inside a group). A null/undefined group crashes afterRead.
|
|
184
|
+
if (field.type === 'group') {
|
|
185
|
+
if (value == null || typeof value !== 'object') {
|
|
186
|
+
current[field.name] = {};
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Localized join fields: wrap in locale map so afterRead can hoist the value.
|
|
191
|
+
// Content API returns join data directly at the field path (e.g. { docs: [...] }),
|
|
192
|
+
// but Payload's afterRead expects localized fields as { [locale]: value }.
|
|
193
|
+
if (field.type === 'join' && isLocalized && value !== undefined && !isAllLocales && locale) {
|
|
194
|
+
current[field.name] = {
|
|
195
|
+
[locale]: value
|
|
196
|
+
};
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
182
199
|
// Content API may omit fields; Payload expects null for optional fields.
|
|
183
200
|
// In all-locales mode, localized fields use {} so afterRead can safely iterate locale keys.
|
|
184
201
|
if (value === undefined) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
export declare function validateRelationshipIds({ collectionSlug, data, payload, }: {
|
|
3
|
+
collectionSlug: string;
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
payload: Payload;
|
|
6
|
+
}): void;
|
|
7
|
+
//# sourceMappingURL=validateRelationships.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { traverseFields } from 'payload';
|
|
2
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
function collectRelationshipValues({ data, fields }) {
|
|
4
|
+
const relationships = [];
|
|
5
|
+
const callback = ({ field, ref })=>{
|
|
6
|
+
if (!('name' in field) || !field.name) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (field.type !== 'relationship' && field.type !== 'upload') {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (!ref || typeof ref !== 'object') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const rawValue = ref[field.name];
|
|
16
|
+
if (rawValue == null || rawValue === '') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// For localized fields, the value is a locale map (e.g. { en: "uuid", fr: "uuid" }).
|
|
20
|
+
// Iterate locale values instead of treating the map as an ID.
|
|
21
|
+
if ('localized' in field && field.localized) {
|
|
22
|
+
if (typeof rawValue === 'object' && !Array.isArray(rawValue)) {
|
|
23
|
+
for (const localeValue of Object.values(rawValue)){
|
|
24
|
+
if (localeValue != null && localeValue !== '') {
|
|
25
|
+
parseRelationshipValues(localeValue, field.relationTo, relationships);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
parseRelationshipValues(rawValue, field.relationTo, relationships);
|
|
32
|
+
};
|
|
33
|
+
traverseFields({
|
|
34
|
+
callback,
|
|
35
|
+
fields,
|
|
36
|
+
fillEmpty: false,
|
|
37
|
+
ref: data
|
|
38
|
+
});
|
|
39
|
+
return relationships;
|
|
40
|
+
}
|
|
41
|
+
function parseRelationshipValues(value, relationTo, relationships) {
|
|
42
|
+
if (Array.isArray(relationTo)) {
|
|
43
|
+
// Polymorphic (single or hasMany): value is { relationTo, value } or array of them
|
|
44
|
+
const values = Array.isArray(value) ? value : [
|
|
45
|
+
value
|
|
46
|
+
];
|
|
47
|
+
for (const v of values){
|
|
48
|
+
if (v && typeof v === 'object' && 'relationTo' in v && 'value' in v) {
|
|
49
|
+
const obj = v;
|
|
50
|
+
if (obj.value != null && obj.value !== '') {
|
|
51
|
+
relationships.push({
|
|
52
|
+
collection: obj.relationTo,
|
|
53
|
+
value: obj.value
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// Non-polymorphic (single or hasMany): value is a bare ID or array of IDs
|
|
60
|
+
const values = Array.isArray(value) ? value : [
|
|
61
|
+
value
|
|
62
|
+
];
|
|
63
|
+
for (const v of values){
|
|
64
|
+
if (v != null && v !== '') {
|
|
65
|
+
relationships.push({
|
|
66
|
+
collection: relationTo,
|
|
67
|
+
value: v
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function isValidId(value, customIDType) {
|
|
74
|
+
if (customIDType === 'number') {
|
|
75
|
+
const num = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
|
|
76
|
+
return Number.isFinite(num);
|
|
77
|
+
}
|
|
78
|
+
if (customIDType === 'text') {
|
|
79
|
+
return typeof value === 'string' && value.length > 0;
|
|
80
|
+
}
|
|
81
|
+
return typeof value === 'string' && UUID_REGEX.test(value);
|
|
82
|
+
}
|
|
83
|
+
export function validateRelationshipIds({ collectionSlug, data, payload }) {
|
|
84
|
+
const isGlobal = collectionSlug.startsWith('_global-');
|
|
85
|
+
const actualSlug = isGlobal ? collectionSlug.substring(8) : collectionSlug;
|
|
86
|
+
const config = isGlobal ? payload.config.globals?.find((g)=>g.slug === actualSlug) : payload.config.collections.find((c)=>c.slug === actualSlug);
|
|
87
|
+
if (!config?.fields) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const relationships = collectRelationshipValues({
|
|
91
|
+
data,
|
|
92
|
+
fields: config.fields
|
|
93
|
+
});
|
|
94
|
+
for (const { collection, value } of relationships){
|
|
95
|
+
const customIDType = payload.collections?.[collection]?.customIDType;
|
|
96
|
+
if (!isValidId(value, customIDType)) {
|
|
97
|
+
throw new Error(`Invalid relationship ID "${String(value)}" for collection "${collection}". Expected ${customIDType === 'number' ? 'a number' : customIDType === 'text' ? 'a non-empty string' : 'a valid UUID'}.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=validateRelationships.js.map
|
|
@@ -17,8 +17,6 @@ type ContentAPIJoin = components['schemas']['JoinClause'][number];
|
|
|
17
17
|
* 3. **Polymorphic relationships** - Stored as `{ relationTo, value }` objects, but
|
|
18
18
|
* Content API expects a simple ID string.
|
|
19
19
|
*
|
|
20
|
-
* 4. **Localized relationship fields** - Locale-aware joins not implemented.
|
|
21
|
-
*
|
|
22
20
|
* Note: "Where querying through joins" (e.g., `where: { 'relatedPosts.title': { equals: 'x' } }`)
|
|
23
21
|
* is a separate Payload feature that filters documents based on joined data. This is NOT
|
|
24
22
|
* a join feature but a where clause feature, and would need separate Content API support.
|
|
@@ -50,6 +48,6 @@ type ContentAPIJoin = components['schemas']['JoinClause'][number];
|
|
|
50
48
|
* - Which collection each join field refers to
|
|
51
49
|
* - What the 'on' field is for each join
|
|
52
50
|
*/
|
|
53
|
-
export declare function convertPayloadJoinsToContentAPI(payload: Payload, collectionSlug: string, joins: false | JoinQuery | undefined): ContentAPIJoin[] | undefined;
|
|
51
|
+
export declare function convertPayloadJoinsToContentAPI(payload: Payload, collectionSlug: string, joins: false | JoinQuery | undefined, locale?: string): ContentAPIJoin[] | undefined;
|
|
54
52
|
export {};
|
|
55
53
|
//# sourceMappingURL=joins.d.ts.map
|
|
@@ -16,8 +16,6 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
16
16
|
* 3. **Polymorphic relationships** - Stored as `{ relationTo, value }` objects, but
|
|
17
17
|
* Content API expects a simple ID string.
|
|
18
18
|
*
|
|
19
|
-
* 4. **Localized relationship fields** - Locale-aware joins not implemented.
|
|
20
|
-
*
|
|
21
19
|
* Note: "Where querying through joins" (e.g., `where: { 'relatedPosts.title': { equals: 'x' } }`)
|
|
22
20
|
* is a separate Payload feature that filters documents based on joined data. This is NOT
|
|
23
21
|
* a join feature but a where clause feature, and would need separate Content API support.
|
|
@@ -48,7 +46,7 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
48
46
|
* The mapping requires the collection config to resolve:
|
|
49
47
|
* - Which collection each join field refers to
|
|
50
48
|
* - What the 'on' field is for each join
|
|
51
|
-
*/ export function convertPayloadJoinsToContentAPI(payload, collectionSlug, joins) {
|
|
49
|
+
*/ export function convertPayloadJoinsToContentAPI(payload, collectionSlug, joins, locale) {
|
|
52
50
|
const collectionConfig = payload.config.collections.find((c)=>c.slug === collectionSlug);
|
|
53
51
|
if (!collectionConfig) {
|
|
54
52
|
return undefined;
|
|
@@ -74,11 +72,14 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
74
72
|
for (const [collectionSlug, sanitizedJoins] of Object.entries(collectionConfig.joins)){
|
|
75
73
|
for (const sanitizedJoin of sanitizedJoins){
|
|
76
74
|
if (sanitizedJoin.joinPath === joinPath) {
|
|
75
|
+
const on = locale && sanitizedJoin.getForeignPath ? sanitizedJoin.getForeignPath({
|
|
76
|
+
locale
|
|
77
|
+
}) : sanitizedJoin.field.on;
|
|
77
78
|
foundJoin = {
|
|
78
79
|
collectionSlug,
|
|
79
80
|
defaultLimit: sanitizedJoin.field.defaultLimit,
|
|
80
81
|
defaultSort: sanitizedJoin.field.defaultSort,
|
|
81
|
-
on
|
|
82
|
+
on
|
|
82
83
|
};
|
|
83
84
|
break;
|
|
84
85
|
}
|
|
@@ -92,10 +93,13 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
92
93
|
for (const sanitizedJoin of collectionConfig.polymorphicJoins){
|
|
93
94
|
if (sanitizedJoin.joinPath === joinPath) {
|
|
94
95
|
const collections = sanitizedJoin.field.collection;
|
|
96
|
+
const on = locale && sanitizedJoin.getForeignPath ? sanitizedJoin.getForeignPath({
|
|
97
|
+
locale
|
|
98
|
+
}) : sanitizedJoin.field.on;
|
|
95
99
|
foundJoin = {
|
|
96
100
|
collectionSlug: collections,
|
|
97
101
|
defaultSort: sanitizedJoin.field.defaultSort,
|
|
98
|
-
on
|
|
102
|
+
on
|
|
99
103
|
};
|
|
100
104
|
break;
|
|
101
105
|
}
|
|
@@ -74,6 +74,10 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
|
|
|
74
74
|
if (op === 'exists' && typeof operatorValue === 'string') {
|
|
75
75
|
finalValue = operatorValue === 'true';
|
|
76
76
|
}
|
|
77
|
+
// REST API sends "null" as a string; convert to actual null
|
|
78
|
+
if (finalValue === 'null') {
|
|
79
|
+
finalValue = null;
|
|
80
|
+
}
|
|
77
81
|
// Content API stores all document IDs as strings.
|
|
78
82
|
// Payload sends numeric values for collections with custom numeric ID fields,
|
|
79
83
|
// so we must stringify to match.
|
|
@@ -23,24 +23,7 @@ export const getHandler = ({ client, collection })=>{
|
|
|
23
23
|
status: 500
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
const fileResponse = await fetch(data.url);
|
|
28
|
-
if (!fileResponse.ok) {
|
|
29
|
-
req.payload.logger.error(`S3 fetch failed: ${fileResponse.status} ${fileResponse.statusText}`);
|
|
30
|
-
return new Response('Failed to fetch file from storage', {
|
|
31
|
-
status: 502
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
const contentType = fileResponse.headers.get('content-type') || 'application/octet-stream';
|
|
35
|
-
const contentLength = fileResponse.headers.get('content-length');
|
|
36
|
-
return new Response(fileResponse.body, {
|
|
37
|
-
headers: {
|
|
38
|
-
'Content-Type': contentType,
|
|
39
|
-
...contentLength && {
|
|
40
|
-
'Content-Length': contentLength
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
});
|
|
26
|
+
return Response.redirect(data.url, 302);
|
|
44
27
|
} catch (err) {
|
|
45
28
|
req.payload.logger.error({
|
|
46
29
|
err,
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Cross-platform replacement for build_for_lambda.sh:
|
|
5
5
|
* 1. Copies .next/static to .next/standalone/.next/static
|
|
6
6
|
* 2. Copies run.sh to .next/standalone/run.sh
|
|
7
|
-
* 3.
|
|
8
|
-
*
|
|
7
|
+
* 3. Creates lambda.zip from .next/standalone/
|
|
8
|
+
*
|
|
9
|
+
* Note: public/ is NOT included in the zip — those files are uploaded
|
|
10
|
+
* as static assets and served via CDN, not from the Lambda function.
|
|
9
11
|
*
|
|
10
12
|
* @param projectPath - Path to project root
|
|
11
13
|
* @throws Error if standalone directory missing or zip creation fails
|
|
@@ -8,15 +8,16 @@ import path from 'path';
|
|
|
8
8
|
* Cross-platform replacement for build_for_lambda.sh:
|
|
9
9
|
* 1. Copies .next/static to .next/standalone/.next/static
|
|
10
10
|
* 2. Copies run.sh to .next/standalone/run.sh
|
|
11
|
-
* 3.
|
|
12
|
-
*
|
|
11
|
+
* 3. Creates lambda.zip from .next/standalone/
|
|
12
|
+
*
|
|
13
|
+
* Note: public/ is NOT included in the zip — those files are uploaded
|
|
14
|
+
* as static assets and served via CDN, not from the Lambda function.
|
|
13
15
|
*
|
|
14
16
|
* @param projectPath - Path to project root
|
|
15
17
|
* @throws Error if standalone directory missing or zip creation fails
|
|
16
18
|
*/ export async function buildLambdaZip(projectPath) {
|
|
17
19
|
const standalonePath = path.join(projectPath, '.next', 'standalone');
|
|
18
20
|
const staticPath = path.join(projectPath, '.next', 'static');
|
|
19
|
-
const publicPath = path.join(projectPath, 'public');
|
|
20
21
|
const runShPath = path.join(projectPath, 'run.sh');
|
|
21
22
|
const zipPath = path.join(projectPath, 'lambda.zip');
|
|
22
23
|
if (!await isDirectory(standalonePath)) {
|
|
@@ -31,10 +32,6 @@ import path from 'path';
|
|
|
31
32
|
} catch {
|
|
32
33
|
// run.sh may not exist in all setups
|
|
33
34
|
}
|
|
34
|
-
if (await isDirectory(publicPath)) {
|
|
35
|
-
const destPublic = path.join(standalonePath, 'public');
|
|
36
|
-
await copyDirectory(publicPath, destPublic);
|
|
37
|
-
}
|
|
38
35
|
await createZip(standalonePath, zipPath);
|
|
39
36
|
}
|
|
40
37
|
async function isDirectory(dirPath) {
|
|
@@ -17,7 +17,7 @@ NODE_ENV=production exec node server.js
|
|
|
17
17
|
const runShPath = path.join(projectPath, 'run.sh');
|
|
18
18
|
await fs.writeFile(runShPath, RUN_SH_CONTENT, 'utf-8');
|
|
19
19
|
await fs.chmod(runShPath, 0o755);
|
|
20
|
-
// 2. Modify next config to add standalone output
|
|
20
|
+
// 2. Modify next config to add standalone output
|
|
21
21
|
await addNextConfigProperties(projectPath);
|
|
22
22
|
// 3. Create/update .npmrc
|
|
23
23
|
const npmrcPath = path.join(projectPath, '.npmrc');
|
|
@@ -58,7 +58,6 @@ NODE_ENV=production exec node server.js
|
|
|
58
58
|
/**
|
|
59
59
|
* Add required Next.js config properties using ts-morph AST parsing
|
|
60
60
|
* - output: 'standalone' (for Lambda deployment)
|
|
61
|
-
* - eslint: { ignoreDuringBuilds: true } (allow builds with lint errors)
|
|
62
61
|
*/ async function addNextConfigProperties(projectPath) {
|
|
63
62
|
const nextConfigPath = await findNextConfigPath(projectPath);
|
|
64
63
|
const project = new Project({
|
|
@@ -76,24 +75,14 @@ NODE_ENV=production exec node server.js
|
|
|
76
75
|
if (!configObject) {
|
|
77
76
|
throw new Error(`Could not find Next.js config object in ${path.basename(nextConfigPath)}. ` + `Expected either 'export default { ... }' or 'export default wrapper(configVar, ...)'`);
|
|
78
77
|
}
|
|
79
|
-
// Check which properties already exist using AST (avoids false positives from comments)
|
|
80
78
|
const hasOutput = configObject.getProperty('output') !== undefined;
|
|
81
|
-
|
|
82
|
-
if (hasOutput && hasEslint) {
|
|
79
|
+
if (hasOutput) {
|
|
83
80
|
return;
|
|
84
81
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
if (!hasEslint) {
|
|
92
|
-
configObject.addPropertyAssignment({
|
|
93
|
-
name: 'eslint',
|
|
94
|
-
initializer: `{ ignoreDuringBuilds: true }`
|
|
95
|
-
});
|
|
96
|
-
}
|
|
82
|
+
configObject.addPropertyAssignment({
|
|
83
|
+
name: 'output',
|
|
84
|
+
initializer: "'standalone'"
|
|
85
|
+
});
|
|
97
86
|
await sourceFile.save();
|
|
98
87
|
}
|
|
99
88
|
/**
|
package/dist/utils/messages.js
CHANGED
|
@@ -59,7 +59,7 @@ export function helpMessage() {
|
|
|
59
59
|
|
|
60
60
|
${pc.bold('GLOBAL OPTIONS')}
|
|
61
61
|
|
|
62
|
-
${pc.dim('--infra-env <env>')} Target infrastructure (production or
|
|
62
|
+
${pc.dim('--infra-env <env>')} Target infrastructure (production, staging, or devbox)
|
|
63
63
|
|
|
64
64
|
${pc.bold('DOCUMENTATION')}
|
|
65
65
|
|
|
@@ -9,9 +9,9 @@ import * as log from './log.js';
|
|
|
9
9
|
hasOtherPayloadImports: false,
|
|
10
10
|
needsImportChange: false
|
|
11
11
|
};
|
|
12
|
-
// Find buildConfig import
|
|
12
|
+
// Find buildConfig import — prefer @payloadcms/figma (already migrated) over payload
|
|
13
13
|
const imports = sourceFile.getImportDeclarations();
|
|
14
|
-
const payloadImport = imports.find((imp)=>imp.getModuleSpecifierValue() === '
|
|
14
|
+
const payloadImport = imports.find((imp)=>imp.getModuleSpecifierValue() === '@payloadcms/figma') || imports.find((imp)=>imp.getModuleSpecifierValue() === 'payload');
|
|
15
15
|
if (!payloadImport) {
|
|
16
16
|
log.debug('No payload or @payloadcms/figma import found');
|
|
17
17
|
result.hasBuildConfig = false;
|
|
@@ -157,9 +157,27 @@ import * as log from './log.js';
|
|
|
157
157
|
// Remove duplicates and sort in reverse order to avoid position shifts
|
|
158
158
|
const uniqueRanges = Array.from(new Set(ranges.map((r)=>JSON.stringify(r)))).map((r)=>JSON.parse(r));
|
|
159
159
|
uniqueRanges.sort((a, b)=>b[0] - a[0]);
|
|
160
|
-
// Remove each comment range
|
|
160
|
+
// Remove each comment range, extending to the full line when the comment is the only content
|
|
161
161
|
for (const [pos, end] of uniqueRanges){
|
|
162
|
-
sourceFile.
|
|
162
|
+
const fullText = sourceFile.getFullText();
|
|
163
|
+
let removeStart = pos;
|
|
164
|
+
let removeEnd = end;
|
|
165
|
+
// Find the start of the line containing this comment
|
|
166
|
+
let lineStart = pos;
|
|
167
|
+
while(lineStart > 0 && fullText[lineStart - 1] !== '\n'){
|
|
168
|
+
lineStart--;
|
|
169
|
+
}
|
|
170
|
+
const beforeComment = fullText.substring(lineStart, pos);
|
|
171
|
+
const nextNewline = fullText.indexOf('\n', end);
|
|
172
|
+
const afterComment = fullText.substring(end, nextNewline === -1 ? fullText.length : nextNewline);
|
|
173
|
+
// If the comment is the only content on this line, remove the entire line
|
|
174
|
+
if (beforeComment.trim() === '' && afterComment.trim() === '') {
|
|
175
|
+
removeStart = lineStart > 0 ? lineStart - 1 : lineStart;
|
|
176
|
+
if (nextNewline !== -1) {
|
|
177
|
+
removeEnd = nextNewline + 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
sourceFile.removeText(removeStart, removeEnd);
|
|
163
181
|
}
|
|
164
182
|
return true;
|
|
165
183
|
}
|
|
@@ -21,7 +21,7 @@ export async function checkForUpdates(currentVersion) {
|
|
|
21
21
|
if (!latestVersion || latestVersion === currentVersion) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
|
-
p.log.warn(pc.yellow(`@payloadcms/figma ${currentVersion}
|
|
24
|
+
p.log.warn(pc.yellow(`@payloadcms/figma ${currentVersion} differs from latest. Latest: ${latestVersion}`));
|
|
25
25
|
p.log.message(pc.dim(` Run: npx @payloadcms/figma@${latestVersion} init --id <your-id>`));
|
|
26
26
|
} catch {
|
|
27
27
|
// Silent catch - network errors, timeouts, parse errors should not interrupt CLI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payloadcms/figma",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.62",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -32,26 +32,26 @@
|
|
|
32
32
|
"@sindresorhus/slugify": "^3.0.0",
|
|
33
33
|
"archiver": "7.0.1",
|
|
34
34
|
"arg": "^5.0.2",
|
|
35
|
-
"conf": "^13.0
|
|
35
|
+
"conf": "^13.1.0",
|
|
36
36
|
"cross-spawn": "7.0.6",
|
|
37
37
|
"figures": "^6.1.0",
|
|
38
38
|
"jose": "6.0.12",
|
|
39
39
|
"jsonwebtoken": "9.0.3",
|
|
40
|
-
"open": "^10.
|
|
40
|
+
"open": "^10.2.0",
|
|
41
41
|
"openapi-fetch": "0.15.0",
|
|
42
42
|
"picocolors": "1.1.1",
|
|
43
|
-
"tar": "^7.
|
|
43
|
+
"tar": "^7.5.13",
|
|
44
44
|
"terminal-link": "^5.0.0",
|
|
45
|
-
"ts-morph": "^21.0.
|
|
45
|
+
"ts-morph": "^21.0.1",
|
|
46
46
|
"uuid": "^10.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/archiver": "7.0.0",
|
|
50
50
|
"@types/cross-spawn": "6.0.6",
|
|
51
51
|
"@types/node": "22.12.0",
|
|
52
|
-
"openapi-typescript": "^7.
|
|
52
|
+
"openapi-typescript": "^7.13.0",
|
|
53
53
|
"tsx": "4.20.6",
|
|
54
|
-
"typescript": "5.
|
|
54
|
+
"typescript": "5.7.3",
|
|
55
55
|
"vitest": "4.0.15"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|