@payloadcms/figma 0.0.1-alpha.55 → 0.0.1-alpha.56
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/auth/crypto-utils.d.ts +6 -0
- package/dist/auth/crypto-utils.js +11 -9
- package/dist/auth/oauth-flow.d.ts +5 -0
- package/dist/auth/oauth-flow.js +20 -3
- package/dist/auth/token-store.d.ts +2 -0
- package/dist/auth/token-store.js +57 -13
- package/dist/cli.js +3 -0
- package/dist/commands/debug.js +15 -0
- package/dist/commands/deploy.js +11 -6
- package/dist/commands/init.js +63 -55
- package/dist/constants.d.ts +9 -1
- package/dist/constants.js +16 -1
- package/dist/db-content-api/index.d.ts +1 -0
- package/dist/db-content-api/index.js +51 -7
- package/dist/db-content-api/utilities/data/index.d.ts +7 -0
- package/dist/db-content-api/utilities/data/index.js +27 -4
- package/dist/db-content-api/utilities/where.js +7 -1
- package/dist/deploy/schedule-extract-plugin.d.ts +11 -0
- package/dist/deploy/schedule-extract-plugin.js +34 -0
- package/dist/plugin/build-config.d.ts +2 -1
- package/dist/plugin/build-config.js +4 -1
- package/dist/types.d.ts +4 -0
- package/dist/utils/formatter.js +20 -6
- package/dist/utils/messages.d.ts +0 -1
- package/dist/utils/messages.js +1 -3
- package/dist/utils/payload-config-ast.d.ts +0 -5
- package/dist/utils/payload-config-ast.js +7 -76
- package/dist/utils/payload-config-modifier.js +9 -1
- package/dist/utils/resolve-environment.d.ts +1 -1
- package/dist/utils/resolve-environment.js +4 -1
- package/package.json +1 -1
|
@@ -30,4 +30,10 @@
|
|
|
30
30
|
* @throws Error if unable to derive key due to missing machine information
|
|
31
31
|
*/
|
|
32
32
|
export declare function deriveEncryptionKey(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Get a short hash of the encryption key for diagnostics.
|
|
35
|
+
* Returns first 8 chars of SHA-256 hash — enough to compare across invocations
|
|
36
|
+
* without exposing the actual key.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getEncryptionKeyHash(): string;
|
|
33
39
|
//# sourceMappingURL=crypto-utils.d.ts.map
|
|
@@ -33,19 +33,13 @@ import os from 'os';
|
|
|
33
33
|
* @throws Error if unable to gather sufficient entropy
|
|
34
34
|
*/ function gatherMachineEntropy() {
|
|
35
35
|
try {
|
|
36
|
-
const userInfo = os.userInfo();
|
|
37
|
-
const networkInterfaces = os.networkInterfaces();
|
|
38
|
-
// Get MAC address from first available network interface (if available)
|
|
39
|
-
const macAddress = Object.values(networkInterfaces).flat().find((iface)=>iface && !iface.internal && iface.mac !== '00:00:00:00:00:00')?.mac;
|
|
40
36
|
const entropy = [
|
|
41
37
|
os.hostname(),
|
|
42
38
|
os.homedir(),
|
|
43
|
-
userInfo.username,
|
|
39
|
+
os.userInfo().username,
|
|
44
40
|
os.platform(),
|
|
45
|
-
os.arch()
|
|
46
|
-
|
|
47
|
-
].filter(Boolean) // Remove any undefined values
|
|
48
|
-
;
|
|
41
|
+
os.arch()
|
|
42
|
+
];
|
|
49
43
|
if (entropy.length < 4) {
|
|
50
44
|
throw new Error('Insufficient machine entropy available');
|
|
51
45
|
}
|
|
@@ -92,5 +86,13 @@ import os from 'os';
|
|
|
92
86
|
const key = crypto.pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
|
|
93
87
|
return key.toString('hex');
|
|
94
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Get a short hash of the encryption key for diagnostics.
|
|
91
|
+
* Returns first 8 chars of SHA-256 hash — enough to compare across invocations
|
|
92
|
+
* without exposing the actual key.
|
|
93
|
+
*/ export function getEncryptionKeyHash() {
|
|
94
|
+
const key = deriveEncryptionKey();
|
|
95
|
+
return crypto.createHash('sha256').update(key).digest('hex').substring(0, 8);
|
|
96
|
+
}
|
|
95
97
|
|
|
96
98
|
//# sourceMappingURL=crypto-utils.js.map
|
|
@@ -59,4 +59,9 @@ export declare function executeOAuthFlow(tokenStore: TokenStore, options?: OAuth
|
|
|
59
59
|
* @throws OAuthFlowError if refresh fails
|
|
60
60
|
*/
|
|
61
61
|
export declare function getValidAccessToken(tokenStore: TokenStore): Promise<null | string>;
|
|
62
|
+
/**
|
|
63
|
+
* Like getValidAccessToken but returns null instead of throwing on refresh failure.
|
|
64
|
+
* Use in commands that want to fall through to a login prompt on failure.
|
|
65
|
+
*/
|
|
66
|
+
export declare function tryGetAccessToken(tokenStore: TokenStore): Promise<null | string>;
|
|
62
67
|
//# sourceMappingURL=oauth-flow.d.ts.map
|
package/dist/auth/oauth-flow.js
CHANGED
|
@@ -155,19 +155,36 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
|
|
|
155
155
|
if (!refreshToken) {
|
|
156
156
|
return null;
|
|
157
157
|
}
|
|
158
|
-
// Try to refresh
|
|
158
|
+
// Try to refresh (with one retry for transient auth errors)
|
|
159
159
|
try {
|
|
160
160
|
const tokens = await refreshAccessToken(refreshToken);
|
|
161
161
|
tokenStore.setTokens(tokens);
|
|
162
162
|
return tokens.accessToken;
|
|
163
163
|
} catch (error) {
|
|
164
|
-
// Only clear tokens for actual auth failures, not transient errors
|
|
165
164
|
const isAuthFailure = error instanceof TokenRefreshError && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 400 && error.errorCode === 'invalid_grant');
|
|
166
165
|
if (isAuthFailure) {
|
|
167
|
-
|
|
166
|
+
// Retry once — a transient 401 (e.g. misconfigured proxy) shouldn't wipe tokens
|
|
167
|
+
try {
|
|
168
|
+
const tokens = await refreshAccessToken(refreshToken);
|
|
169
|
+
tokenStore.setTokens(tokens);
|
|
170
|
+
return tokens.accessToken;
|
|
171
|
+
} catch {
|
|
172
|
+
// Retry also failed — token is genuinely revoked
|
|
173
|
+
tokenStore.clearTokens();
|
|
174
|
+
}
|
|
168
175
|
}
|
|
169
176
|
throw new OAuthFlowError('Failed to refresh access token', error instanceof Error ? error : undefined);
|
|
170
177
|
}
|
|
171
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Like getValidAccessToken but returns null instead of throwing on refresh failure.
|
|
181
|
+
* Use in commands that want to fall through to a login prompt on failure.
|
|
182
|
+
*/ export async function tryGetAccessToken(tokenStore) {
|
|
183
|
+
try {
|
|
184
|
+
return await getValidAccessToken(tokenStore);
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
172
189
|
|
|
173
190
|
//# sourceMappingURL=oauth-flow.js.map
|
|
@@ -24,7 +24,9 @@ export declare function getTokenStore(environment?: Environment): TokenStore;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare class TokenStore {
|
|
26
26
|
private config;
|
|
27
|
+
private environment;
|
|
27
28
|
constructor(options?: TokenStoreConfig);
|
|
29
|
+
private safeGet;
|
|
28
30
|
/**
|
|
29
31
|
* Retrieve stored tokens
|
|
30
32
|
* @returns FigmaTokens if stored, null otherwise
|
package/dist/auth/token-store.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* eslint-disable perfectionist/sort-classes */ import Conf from 'conf';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
2
3
|
import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
|
|
3
4
|
import { getInfraEnvironment } from '../constants.js';
|
|
5
|
+
import * as log from '../utils/log.js';
|
|
4
6
|
import { deriveEncryptionKey } from './crypto-utils.js';
|
|
5
7
|
/**
|
|
6
8
|
* Environment-keyed instances for singleton pattern
|
|
@@ -34,24 +36,57 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
34
36
|
* The constructor is still exported for test isolation.
|
|
35
37
|
*/ export class TokenStore {
|
|
36
38
|
config;
|
|
39
|
+
environment;
|
|
37
40
|
constructor(options){
|
|
38
41
|
if (process.env.AWS_EXECUTION_ENV) {
|
|
39
42
|
throw new Error('TokenStore cannot be used in AWS Lambda environments');
|
|
40
43
|
}
|
|
41
44
|
const environment = options?.environment ?? 'production';
|
|
42
45
|
const projectName = environment === 'production' ? 'payloadcms-figma' : `payloadcms-figma-${environment}`;
|
|
43
|
-
this.
|
|
44
|
-
|
|
46
|
+
this.environment = environment;
|
|
47
|
+
const confOptions = {
|
|
48
|
+
clearInvalidConfig: false,
|
|
45
49
|
configName: options?.configName || projectName,
|
|
46
50
|
encryptionKey: options?.encryptionKey || deriveEncryptionKey(),
|
|
47
51
|
projectName
|
|
48
|
-
}
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
this.config = new Conf(confOptions);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// Deserialization failed (e.g. encryption key changed).
|
|
57
|
+
// Log diagnostics, clear the corrupted file, then create a fresh store.
|
|
58
|
+
if (environment === 'staging') {
|
|
59
|
+
log.warning(`Token store corrupted, resetting. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
60
|
+
}
|
|
61
|
+
// Temporarily allow clearing so Conf can reinitialize
|
|
62
|
+
this.config = new Conf({
|
|
63
|
+
...confOptions,
|
|
64
|
+
clearInvalidConfig: true
|
|
65
|
+
});
|
|
66
|
+
// Force a write so the corrupt file is replaced with an empty store.
|
|
67
|
+
// Without this, clearInvalidConfig only ignores bad reads in memory
|
|
68
|
+
// and the corrupt file persists, triggering this fallback every time.
|
|
69
|
+
this.config.clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
safeGet(key) {
|
|
73
|
+
try {
|
|
74
|
+
return this.config.get(key);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (this.environment === 'staging') {
|
|
77
|
+
const filePath = this.config.path;
|
|
78
|
+
const fileExists = fsSync.existsSync(filePath);
|
|
79
|
+
const fileSize = fileExists ? fsSync.statSync(filePath).size : 0;
|
|
80
|
+
log.warning(`Token store read failed (key: ${key}). ` + `File: ${filePath}, exists: ${fileExists}, size: ${fileSize}B. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
49
84
|
}
|
|
50
85
|
/**
|
|
51
86
|
* Retrieve stored tokens
|
|
52
87
|
* @returns FigmaTokens if stored, null otherwise
|
|
53
88
|
*/ getTokens() {
|
|
54
|
-
const tokens = this.
|
|
89
|
+
const tokens = this.safeGet('tokens');
|
|
55
90
|
return tokens || null;
|
|
56
91
|
}
|
|
57
92
|
/**
|
|
@@ -59,6 +94,16 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
59
94
|
* @param tokens - Figma OAuth2 tokens to store
|
|
60
95
|
*/ setTokens(tokens) {
|
|
61
96
|
this.config.set('tokens', tokens);
|
|
97
|
+
// Verify the round-trip: read back what we just wrote
|
|
98
|
+
const readBack = this.safeGet('tokens');
|
|
99
|
+
if (!readBack || readBack.accessToken !== tokens.accessToken) {
|
|
100
|
+
if (this.environment === 'staging') {
|
|
101
|
+
const filePath = this.config.path;
|
|
102
|
+
const fileExists = fsSync.existsSync(filePath);
|
|
103
|
+
const fileSize = fileExists ? fsSync.statSync(filePath).size : 0;
|
|
104
|
+
log.warning(`Token verify-after-write FAILED. ` + `File: ${filePath}, exists: ${fileExists}, size: ${fileSize}B. ` + `Written accessToken starts: ${tokens.accessToken.substring(0, 10)}..., ` + `Read back: ${readBack ? readBack.accessToken.substring(0, 10) + '...' : 'null'}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
62
107
|
}
|
|
63
108
|
/**
|
|
64
109
|
* Clear all stored tokens (used for logout)
|
|
@@ -80,8 +125,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
80
125
|
* Includes a buffer time to avoid using tokens that are about to expire
|
|
81
126
|
* @returns true if token is expired or will expire within buffer time
|
|
82
127
|
*/ isExpired() {
|
|
83
|
-
|
|
84
|
-
const tokens = this.config.get('tokens');
|
|
128
|
+
const tokens = this.safeGet('tokens');
|
|
85
129
|
if (!tokens) {
|
|
86
130
|
return true;
|
|
87
131
|
}
|
|
@@ -136,7 +180,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
136
180
|
* @param tenantId - The tenant/CMS ID
|
|
137
181
|
* @returns ProjectToken if stored and valid, null otherwise
|
|
138
182
|
*/ getProjectToken(tenantId) {
|
|
139
|
-
const projectTokens = this.
|
|
183
|
+
const projectTokens = this.safeGet('projectTokens') || {};
|
|
140
184
|
const projectToken = projectTokens[tenantId];
|
|
141
185
|
if (!projectToken) {
|
|
142
186
|
return null;
|
|
@@ -153,7 +197,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
153
197
|
* @param tenantId - The tenant/CMS ID
|
|
154
198
|
* @param projectToken - The project token to store
|
|
155
199
|
*/ setProjectToken(tenantId, projectToken) {
|
|
156
|
-
const projectTokens = this.
|
|
200
|
+
const projectTokens = this.safeGet('projectTokens') || {};
|
|
157
201
|
projectTokens[tenantId] = projectToken;
|
|
158
202
|
this.config.set('projectTokens', projectTokens);
|
|
159
203
|
}
|
|
@@ -161,7 +205,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
161
205
|
* Clear a project token for a specific tenant
|
|
162
206
|
* @param tenantId - The tenant/CMS ID
|
|
163
207
|
*/ clearProjectToken(tenantId) {
|
|
164
|
-
const projectTokens = this.
|
|
208
|
+
const projectTokens = this.safeGet('projectTokens') || {};
|
|
165
209
|
delete projectTokens[tenantId];
|
|
166
210
|
this.config.set('projectTokens', projectTokens);
|
|
167
211
|
}
|
|
@@ -204,7 +248,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
204
248
|
* Get all tenant IDs that have stored project tokens
|
|
205
249
|
* @returns Array of tenant IDs
|
|
206
250
|
*/ getAllProjectTokenTenantIds() {
|
|
207
|
-
const projectTokens = this.
|
|
251
|
+
const projectTokens = this.safeGet('projectTokens') || {};
|
|
208
252
|
return Object.keys(projectTokens);
|
|
209
253
|
}
|
|
210
254
|
/**
|
|
@@ -221,7 +265,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
221
265
|
* @param environmentName - The environment name
|
|
222
266
|
* @returns BootstrapData if stored, null otherwise
|
|
223
267
|
*/ getBootstrapData(projectId, environmentName) {
|
|
224
|
-
const allData = this.
|
|
268
|
+
const allData = this.safeGet('bootstrapData') || {};
|
|
225
269
|
return allData[this.bootstrapKey(projectId, environmentName)] || null;
|
|
226
270
|
}
|
|
227
271
|
/**
|
|
@@ -230,7 +274,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
230
274
|
* @param environmentName - The environment name
|
|
231
275
|
* @param data - The bootstrap data to store
|
|
232
276
|
*/ setBootstrapData(projectId, environmentName, data) {
|
|
233
|
-
const allData = this.
|
|
277
|
+
const allData = this.safeGet('bootstrapData') || {};
|
|
234
278
|
allData[this.bootstrapKey(projectId, environmentName)] = data;
|
|
235
279
|
this.config.set('bootstrapData', allData);
|
|
236
280
|
}
|
|
@@ -239,7 +283,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
|
|
|
239
283
|
* @param projectId - The CMS resource/project ID
|
|
240
284
|
* @param environmentName - The environment name
|
|
241
285
|
*/ clearBootstrapData(projectId, environmentName) {
|
|
242
|
-
const allData = this.
|
|
286
|
+
const allData = this.safeGet('bootstrapData') || {};
|
|
243
287
|
delete allData[this.bootstrapKey(projectId, environmentName)];
|
|
244
288
|
this.config.set('bootstrapData', allData);
|
|
245
289
|
}
|
package/dist/cli.js
CHANGED
package/dist/commands/debug.js
CHANGED
|
@@ -2,6 +2,7 @@ import fsSync from 'node:fs';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { getEncryptionKeyHash } from '../auth/crypto-utils.js';
|
|
5
6
|
import { getTokenStore } from '../auth/token-store.js';
|
|
6
7
|
import { getInfraEnvironment } from '../constants.js';
|
|
7
8
|
import { getEnvVarSync } from '../utils/env-management.js';
|
|
@@ -22,6 +23,7 @@ import { getOwnVersion } from '../utils/version-check.js';
|
|
|
22
23
|
];
|
|
23
24
|
appendSystemSection(lines, env);
|
|
24
25
|
appendAuthSection(lines, tokenStore, cwd);
|
|
26
|
+
appendTokenStoreDiagnostics(lines, tokenStore);
|
|
25
27
|
appendProjectSection(lines, tokenStore, cwd);
|
|
26
28
|
await appendPackagesSection(lines);
|
|
27
29
|
// eslint-disable-next-line no-console
|
|
@@ -52,6 +54,19 @@ function appendAuthSection(lines, tokenStore, cwd) {
|
|
|
52
54
|
lines.push(` Token Expires: ${formatExpiryTime(tokens.expiresAt)}`);
|
|
53
55
|
lines.push('');
|
|
54
56
|
}
|
|
57
|
+
function appendTokenStoreDiagnostics(lines, tokenStore) {
|
|
58
|
+
lines.push('Token Store:');
|
|
59
|
+
const filePath = tokenStore.getStoragePath();
|
|
60
|
+
const fileExists = fsSync.existsSync(filePath);
|
|
61
|
+
const fileSize = fileExists ? fsSync.statSync(filePath).size : 0;
|
|
62
|
+
lines.push(` File: ${filePath}`);
|
|
63
|
+
lines.push(` File Exists: ${fileExists}`);
|
|
64
|
+
lines.push(` File Size: ${fileSize}B`);
|
|
65
|
+
const canRead = tokenStore.getTokens() !== null || tokenStore.getRefreshToken() !== null;
|
|
66
|
+
lines.push(` Decryption: ${fileExists ? canRead || fileSize === 0 ? 'OK' : 'FAILED' : 'N/A'}`);
|
|
67
|
+
lines.push(` Key Hash: ${getEncryptionKeyHash()}`);
|
|
68
|
+
lines.push('');
|
|
69
|
+
}
|
|
55
70
|
function appendProjectSection(lines, tokenStore, cwd) {
|
|
56
71
|
const projectId = getEnvVarSync(cwd, 'FIGMA_PROJECT_ID');
|
|
57
72
|
const environmentName = getEnvVarSync(cwd, 'FIGMA_ENVIRONMENT_NAME');
|
package/dist/commands/deploy.js
CHANGED
|
@@ -3,9 +3,9 @@ import spawn from 'cross-spawn';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import { ControlPlaneError, createDeployment, getBootstrapInfo, performDeployment } from '../api/control-plane.js';
|
|
6
|
-
import {
|
|
6
|
+
import { tryGetAccessToken } from '../auth/oauth-flow.js';
|
|
7
7
|
import { getTokenStore } from '../auth/token-store.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getInfraEnvironment, getProjectNotFoundMessage } from '../constants.js';
|
|
9
9
|
import { collectStaticAssets, createLambdaZip, getFileSize } from '../utils/asset-collection.js';
|
|
10
10
|
import { detectBuild, getBuildCommand } from '../utils/build-detection.js';
|
|
11
11
|
import { getEnvVar } from '../utils/env-management.js';
|
|
@@ -71,11 +71,11 @@ import { loginCommand } from './login.js';
|
|
|
71
71
|
}
|
|
72
72
|
// Check authentication
|
|
73
73
|
const tokenStore = getTokenStore();
|
|
74
|
-
let accessToken = await
|
|
74
|
+
let accessToken = await tryGetAccessToken(tokenStore);
|
|
75
75
|
if (!accessToken) {
|
|
76
76
|
p.log.message('Please log in to continue');
|
|
77
77
|
await loginCommand();
|
|
78
|
-
accessToken = await
|
|
78
|
+
accessToken = await tryGetAccessToken(tokenStore);
|
|
79
79
|
if (!accessToken) {
|
|
80
80
|
p.log.error(pc.red('✗ Authentication failed'));
|
|
81
81
|
process.exit(1);
|
|
@@ -94,14 +94,19 @@ import { loginCommand } from './login.js';
|
|
|
94
94
|
spinner.stop(pc.green(`✓ Target: ${resolvedEnv.name}`));
|
|
95
95
|
} catch (error) {
|
|
96
96
|
if (error instanceof ControlPlaneError && (error.statusCode === 404 || error.statusCode === 410)) {
|
|
97
|
-
spinner.stop(pc.red('✗
|
|
98
|
-
p.log.error(pc.red(
|
|
97
|
+
spinner.stop(pc.red('✗ Project not found'));
|
|
98
|
+
p.log.error(pc.red(getProjectNotFoundMessage({
|
|
99
|
+
environment: getInfraEnvironment(),
|
|
100
|
+
projectId
|
|
101
|
+
})));
|
|
99
102
|
process.exit(1);
|
|
100
103
|
}
|
|
101
104
|
spinner.stop(pc.red('✗ Failed to resolve deployment target'));
|
|
102
105
|
p.log.error(error instanceof Error ? error.message : 'Unknown error');
|
|
103
106
|
process.exit(1);
|
|
104
107
|
}
|
|
108
|
+
// Signal buildFigmaConfig to extract schedules during build
|
|
109
|
+
process.env.FIGMA_EXTRACT_SCHEDULES = 'true';
|
|
105
110
|
// ===== BUILD =====
|
|
106
111
|
let buildInfo;
|
|
107
112
|
if (options.skipBuild) {
|
package/dist/commands/init.js
CHANGED
|
@@ -4,10 +4,10 @@ import path from 'path';
|
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import { ControlPlaneError, getBootstrapInfo } from '../api/control-plane.js';
|
|
6
6
|
import { FigmaApiError } from '../api/figma-api.js';
|
|
7
|
-
import {
|
|
7
|
+
import { tryGetAccessToken } from '../auth/oauth-flow.js';
|
|
8
8
|
import { getValidProjectToken, ProjectTokenError } from '../auth/project-token.js';
|
|
9
9
|
import { getTokenStore } from '../auth/token-store.js';
|
|
10
|
-
import {
|
|
10
|
+
import { getInfraEnvironment, getProjectNotFoundMessage } from '../constants.js';
|
|
11
11
|
import { ensureGitignore } from '../utils/config.js';
|
|
12
12
|
import { addOrUpdateEnvVar } from '../utils/env-management.js';
|
|
13
13
|
import { isDebug } from '../utils/is-debug.js';
|
|
@@ -77,33 +77,24 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
77
77
|
// Skip authentication for testing/development
|
|
78
78
|
p.log.warn(pc.yellow('Skipping authentication (--skip-auth mode)'));
|
|
79
79
|
} else {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!newToken) {
|
|
95
|
-
p.log.error('Authentication failed');
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
s.stop(pc.green('✓ Authenticated'));
|
|
80
|
+
// Check for valid cached tokens first (no API call needed)
|
|
81
|
+
if (!tokenStore.hasValidTokens()) {
|
|
82
|
+
// Need to refresh or authenticate - show spinner for API call
|
|
83
|
+
s.start('Checking authentication...');
|
|
84
|
+
const token = await tryGetAccessToken(tokenStore);
|
|
85
|
+
if (!token) {
|
|
86
|
+
s.stop('Authentication required to continue');
|
|
87
|
+
await loginCommand({
|
|
88
|
+
showNextSteps: false
|
|
89
|
+
});
|
|
90
|
+
const newToken = await tryGetAccessToken(tokenStore);
|
|
91
|
+
if (!newToken) {
|
|
92
|
+
p.log.error('Authentication failed');
|
|
93
|
+
process.exit(1);
|
|
100
94
|
}
|
|
95
|
+
} else {
|
|
96
|
+
s.stop(pc.green('✓ Authenticated'));
|
|
101
97
|
}
|
|
102
|
-
} catch (error) {
|
|
103
|
-
s.stop(pc.red('✗ Authentication failed'));
|
|
104
|
-
log.error(error instanceof Error ? error.message : 'Unknown error');
|
|
105
|
-
p.note('Run `figma auth` to authenticate', 'Tip');
|
|
106
|
-
process.exit(1);
|
|
107
98
|
}
|
|
108
99
|
}
|
|
109
100
|
// Note: We no longer check for figma.config.json here
|
|
@@ -122,21 +113,17 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
122
113
|
let oauthClientId = null;
|
|
123
114
|
let oauthClientSecret = null;
|
|
124
115
|
if (!options.skipAuth) {
|
|
125
|
-
const accessToken = await
|
|
116
|
+
const accessToken = await tryGetAccessToken(tokenStore);
|
|
126
117
|
if (accessToken) {
|
|
127
118
|
try {
|
|
128
119
|
bootstrapInfo = await getBootstrapInfo(accessToken, cmsResourceId);
|
|
129
|
-
const envName = options.env ?? process.env.FIGMA_ENVIRONMENT_NAME ?? undefined;
|
|
130
|
-
resolvedEnv = resolveEnvironment({
|
|
131
|
-
environmentName: envName,
|
|
132
|
-
environments: bootstrapInfo.environments
|
|
133
|
-
});
|
|
134
|
-
oauthClientId = bootstrapInfo.oauthCredentials.oauthClientId;
|
|
135
|
-
oauthClientSecret = bootstrapInfo.oauthCredentials.oauthClientSecret;
|
|
136
120
|
} catch (error) {
|
|
137
121
|
log.debug(`Control Plane Error: ${error.statusCode} - ${error.message}`);
|
|
138
122
|
if (error instanceof ControlPlaneError && (error.statusCode === 404 || error.statusCode === 410)) {
|
|
139
|
-
p.log.error(pc.red(
|
|
123
|
+
p.log.error(pc.red(getProjectNotFoundMessage({
|
|
124
|
+
environment: getInfraEnvironment(),
|
|
125
|
+
projectId: cmsResourceId
|
|
126
|
+
})));
|
|
140
127
|
process.exit(1);
|
|
141
128
|
}
|
|
142
129
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -144,6 +131,18 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
144
131
|
p.log.error(pc.dim(`Details: ${message}`));
|
|
145
132
|
process.exit(1);
|
|
146
133
|
}
|
|
134
|
+
try {
|
|
135
|
+
const envName = options.env ?? process.env.FIGMA_ENVIRONMENT_NAME ?? undefined;
|
|
136
|
+
resolvedEnv = resolveEnvironment({
|
|
137
|
+
environmentName: envName,
|
|
138
|
+
environments: bootstrapInfo.environments
|
|
139
|
+
});
|
|
140
|
+
oauthClientId = bootstrapInfo.oauthCredentials.oauthClientId;
|
|
141
|
+
oauthClientSecret = bootstrapInfo.oauthCredentials.oauthClientSecret;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
p.log.error(pc.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
// Step 4: Detect or scaffold Payload project
|
|
@@ -238,7 +237,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
238
237
|
process.exit(1);
|
|
239
238
|
}
|
|
240
239
|
// Generate import map to prevent errors on first dev run
|
|
241
|
-
await runScript(process.cwd(), 'generate:importmap', packageManager);
|
|
240
|
+
const importMapResult = await runScript(process.cwd(), 'generate:importmap', packageManager);
|
|
241
|
+
if (!importMapResult) {
|
|
242
|
+
p.log.warn(pc.yellow('⚠ Import map generation failed — it will be generated on first dev run'));
|
|
243
|
+
}
|
|
242
244
|
// Apply Lambda modifications (run.sh, standalone output, lambda:buildzip script)
|
|
243
245
|
await applyLambdaModifications(process.cwd(), packageManager);
|
|
244
246
|
// Ensure .gitignore has required entries
|
|
@@ -260,27 +262,30 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
260
262
|
} else {
|
|
261
263
|
// ===== NEW PROJECT FLOW =====
|
|
262
264
|
// p.log.info('No project detected - Creating New Project')
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
265
|
+
let projectPath;
|
|
266
|
+
if (options.name) {
|
|
267
|
+
projectPath = options.name;
|
|
268
|
+
} else {
|
|
269
|
+
const projectPathInput = await p.text({
|
|
270
|
+
initialValue: './',
|
|
271
|
+
message: 'Enter path to create project:',
|
|
272
|
+
placeholder: './my-cms-project',
|
|
273
|
+
validate: (value)=>{
|
|
274
|
+
if (!value) {
|
|
275
|
+
return 'Path is required';
|
|
276
|
+
}
|
|
277
|
+
// Allow relative or absolute paths
|
|
278
|
+
return undefined;
|
|
271
279
|
}
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
});
|
|
281
|
+
if (p.isCancel(projectPathInput)) {
|
|
282
|
+
p.cancel('Operation cancelled');
|
|
283
|
+
process.exit(0);
|
|
274
284
|
}
|
|
275
|
-
|
|
276
|
-
if (p.isCancel(projectPathInput)) {
|
|
277
|
-
p.cancel('Operation cancelled');
|
|
278
|
-
process.exit(0);
|
|
285
|
+
projectPath = projectPathInput;
|
|
279
286
|
}
|
|
280
|
-
const projectPath = projectPathInput;
|
|
281
287
|
const fullPath = path.resolve(process.cwd(), projectPath);
|
|
282
|
-
|
|
283
|
-
const projectName = options.name || path.basename(fullPath);
|
|
288
|
+
const projectName = path.basename(fullPath);
|
|
284
289
|
const packageManager = detectPackageManagerFromEnvironment();
|
|
285
290
|
// Create directory
|
|
286
291
|
try {
|
|
@@ -346,7 +351,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
|
|
|
346
351
|
p.log.warn(pc.yellow(`⚠ Failed to update .env file: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
347
352
|
}
|
|
348
353
|
// Generate import map to prevent errors on first dev run
|
|
349
|
-
await runScript(fullPath, 'generate:importmap', packageManager);
|
|
354
|
+
const importMapResult = await runScript(fullPath, 'generate:importmap', packageManager);
|
|
355
|
+
if (!importMapResult) {
|
|
356
|
+
p.log.warn(pc.yellow('⚠ Import map generation failed — it will be generated on first dev run'));
|
|
357
|
+
}
|
|
350
358
|
// Generate project token (unless skipping auth)
|
|
351
359
|
if (!options.skipAuth && resolvedEnv) {
|
|
352
360
|
await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s);
|
package/dist/constants.d.ts
CHANGED
|
@@ -23,9 +23,17 @@ type EnvironmentConfig = {
|
|
|
23
23
|
export declare const ENV_CONFIG: Record<Environment, EnvironmentConfig>;
|
|
24
24
|
/**
|
|
25
25
|
* User-facing error message when Content API returns 404 or 410.
|
|
26
|
-
* Used
|
|
26
|
+
* Used by the db adapter for consistent messaging at runtime.
|
|
27
27
|
*/
|
|
28
28
|
export declare const CONTENT_SYSTEM_NOT_FOUND_MESSAGE = "Content system not found. Verify your Content System ID is correct, or contact your team admin for assistance.";
|
|
29
|
+
/**
|
|
30
|
+
* Build a user-facing error message for CLI commands when a project is not found (404/410).
|
|
31
|
+
* Includes infra-env context and actionable suggestions.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getProjectNotFoundMessage(params: {
|
|
34
|
+
environment: Environment;
|
|
35
|
+
projectId: string;
|
|
36
|
+
}): string;
|
|
29
37
|
/**
|
|
30
38
|
* Set infrastructure environment override (used by --infra-env CLI flag)
|
|
31
39
|
* Pass undefined to clear the override
|
package/dist/constants.js
CHANGED
|
@@ -22,8 +22,22 @@ import { getEnvVarSync } from './utils/env-management.js';
|
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
24
|
* User-facing error message when Content API returns 404 or 410.
|
|
25
|
-
* Used
|
|
25
|
+
* Used by the db adapter for consistent messaging at runtime.
|
|
26
26
|
*/ export const CONTENT_SYSTEM_NOT_FOUND_MESSAGE = 'Content system not found. Verify your Content System ID is correct, or contact your team admin for assistance.';
|
|
27
|
+
/**
|
|
28
|
+
* Build a user-facing error message for CLI commands when a project is not found (404/410).
|
|
29
|
+
* Includes infra-env context and actionable suggestions.
|
|
30
|
+
*/ export function getProjectNotFoundMessage(params) {
|
|
31
|
+
const { environment, projectId } = params;
|
|
32
|
+
return [
|
|
33
|
+
`Project not found on ${environment} (ID: ${projectId}).`,
|
|
34
|
+
'',
|
|
35
|
+
'Suggestions:',
|
|
36
|
+
' • Verify your project ID is correct',
|
|
37
|
+
" • Ensure you're logged into the correct Figma account",
|
|
38
|
+
' • Contact your team admin for assistance'
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
27
41
|
let envOverride;
|
|
28
42
|
/**
|
|
29
43
|
* Set infrastructure environment override (used by --infra-env CLI flag)
|
|
@@ -55,6 +69,7 @@ export function getInfraEnvironment() {
|
|
|
55
69
|
return 'staging';
|
|
56
70
|
}
|
|
57
71
|
if (env !== 'production') {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
58
73
|
console.warn(`Warning: Invalid FIGMA_INFRA_ENV value "${envFileValue}" in .env file. Using production.`);
|
|
59
74
|
}
|
|
60
75
|
return 'production';
|
|
@@ -13,6 +13,7 @@ export type ContentAPIAdapter = {
|
|
|
13
13
|
clearDatabase: () => Promise<void>;
|
|
14
14
|
client: ReturnType<typeof createClient<paths>>;
|
|
15
15
|
contentSystemId: string;
|
|
16
|
+
idType: 'uuid';
|
|
16
17
|
url: string;
|
|
17
18
|
} & BaseDatabaseAdapter;
|
|
18
19
|
export declare const contentAPIAdapter: (opts: ContentAPIOptions) => DatabaseAdapterObj;
|
|
@@ -7,7 +7,7 @@ import { getGlobalSlug } from './temp-utilities/slug.js';
|
|
|
7
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
|
-
import { dataToContentAPI } from './utilities/data/index.js';
|
|
10
|
+
import { dataToContentAPI, resolveVersionContent } from './utilities/data/index.js';
|
|
11
11
|
import { convertPayloadJoinsToContentAPI } from './utilities/joins.js';
|
|
12
12
|
import { addFallbackLocale } from './utilities/locale/index.js';
|
|
13
13
|
import { buildMeta } from './utilities/meta/buildMeta.js';
|
|
@@ -225,8 +225,8 @@ async function updateVersion(args) {
|
|
|
225
225
|
}
|
|
226
226
|
};
|
|
227
227
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
228
|
-
// versionData contains version metadata plus nested version content
|
|
229
228
|
const { publishedLocale, version, ...versionMeta } = args.versionData;
|
|
229
|
+
const resolvedVersion = resolveVersionContent(version, versionMeta);
|
|
230
230
|
const { data: response, error } = await this.client.POST('/api/v0/document_versions:update', {
|
|
231
231
|
body: {
|
|
232
232
|
collection: args.collection,
|
|
@@ -238,7 +238,7 @@ async function updateVersion(args) {
|
|
|
238
238
|
latest: versionMeta.latest,
|
|
239
239
|
parent: versionMeta.parent != null ? String(versionMeta.parent) : undefined,
|
|
240
240
|
updatedAt: versionMeta.updatedAt,
|
|
241
|
-
version: dataToContentAPI(this.payload, args.collection,
|
|
241
|
+
version: dataToContentAPI(this.payload, args.collection, resolvedVersion, {
|
|
242
242
|
publishedLocale
|
|
243
243
|
})
|
|
244
244
|
},
|
|
@@ -299,6 +299,37 @@ async function findOne(args) {
|
|
|
299
299
|
});
|
|
300
300
|
return docs[0] ?? null;
|
|
301
301
|
}
|
|
302
|
+
async function findDistinct(args) {
|
|
303
|
+
const locale = addFallbackLocale(args.locale, this.payload);
|
|
304
|
+
const { data: response, error } = await this.client.POST('/api/v0/documents:findDistinct', {
|
|
305
|
+
body: {
|
|
306
|
+
collection: args.collection,
|
|
307
|
+
contentSystemId: this.contentSystemId,
|
|
308
|
+
distinctBy: args.field,
|
|
309
|
+
limit: args.limit,
|
|
310
|
+
locale,
|
|
311
|
+
page: args.page ?? 1,
|
|
312
|
+
sort: addFallbackSort(args.sort, this.payload, args.collection),
|
|
313
|
+
where: convertPayloadWhereToContentAPI(args.where ?? {}),
|
|
314
|
+
...buildMeta(this.payload, {
|
|
315
|
+
collection: args.collection,
|
|
316
|
+
locale,
|
|
317
|
+
where: args.where
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
if (error) {
|
|
322
|
+
throw new Error(`Content API findDistinct error: ${JSON.stringify(error)}`);
|
|
323
|
+
}
|
|
324
|
+
const { data, ...pagination } = response.result;
|
|
325
|
+
return {
|
|
326
|
+
...pagination,
|
|
327
|
+
// Content API returns flat values; Payload expects objects keyed by field name.
|
|
328
|
+
values: data.map((value)=>({
|
|
329
|
+
[args.field]: value
|
|
330
|
+
}))
|
|
331
|
+
};
|
|
332
|
+
}
|
|
302
333
|
async function updateMany(args) {
|
|
303
334
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
304
335
|
const { data: response, error } = await this.client.POST('/api/v0/documents:update', {
|
|
@@ -418,7 +449,18 @@ async function deleteOne(args) {
|
|
|
418
449
|
async function create(args) {
|
|
419
450
|
const isGlobalCollection = args.collection.startsWith('_global-');
|
|
420
451
|
const customIDType = this.payload.collections[args.collection]?.customIDType;
|
|
421
|
-
|
|
452
|
+
// See test: "should allow creating docs with payload.db.create with custom ID".
|
|
453
|
+
// TODO: customID is not yet in our installed types. Remove this when it is.
|
|
454
|
+
// When provided (e.g. from payload.db.create), it takes priority over data.id.
|
|
455
|
+
const customID = args.customID;
|
|
456
|
+
let id;
|
|
457
|
+
if (customID != null) {
|
|
458
|
+
id = String(customID);
|
|
459
|
+
} else if ((isGlobalCollection || customIDType || this.allowIDOnCreate) && args.data.id != null && (typeof args.data.id === 'string' || typeof args.data.id === 'number')) {
|
|
460
|
+
id = String(args.data.id);
|
|
461
|
+
} else {
|
|
462
|
+
id = uuid();
|
|
463
|
+
}
|
|
422
464
|
const locale = addFallbackLocale(args.locale, this.payload);
|
|
423
465
|
const { data: response, error } = await this.client.POST('/api/v0/documents:create', {
|
|
424
466
|
body: {
|
|
@@ -580,8 +622,9 @@ function findGlobal(args) {
|
|
|
580
622
|
});
|
|
581
623
|
}
|
|
582
624
|
async function updateGlobal(args) {
|
|
583
|
-
//
|
|
584
|
-
//
|
|
625
|
+
// TODO: upsert's `createOnMissing` path generates a new `createdAt` on every call,
|
|
626
|
+
// which overwrites the original timestamp on updates. The fix belongs in the Content API:
|
|
627
|
+
// `createOnMissing` should preserve `createdAt` on updates and only set it on creates.
|
|
585
628
|
return this.upsert({
|
|
586
629
|
collection: getGlobalSlug(args.slug),
|
|
587
630
|
data: args.data,
|
|
@@ -678,11 +721,12 @@ export const contentAPIAdapter = (opts)=>({
|
|
|
678
721
|
deleteOne: deleteOne,
|
|
679
722
|
deleteVersions: deleteVersions,
|
|
680
723
|
find: find,
|
|
681
|
-
findDistinct:
|
|
724
|
+
findDistinct: findDistinct,
|
|
682
725
|
findGlobal: findGlobal,
|
|
683
726
|
findGlobalVersions: findGlobalVersions,
|
|
684
727
|
findOne: findOne,
|
|
685
728
|
findVersions: findVersions,
|
|
729
|
+
idType: 'uuid',
|
|
686
730
|
init,
|
|
687
731
|
packageName: '@payloadcms/db-content-api',
|
|
688
732
|
payload,
|
|
@@ -37,6 +37,13 @@ export declare function dataToContentAPI(payload: Payload, collectionSlug: strin
|
|
|
37
37
|
* Note: Date fields come as ISO strings from Content API and stay as strings
|
|
38
38
|
* (matching MongoDB and other adapters' behavior)
|
|
39
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* Extract version content from versionData that may be flat (no nested `version` key).
|
|
42
|
+
* Some callers spread version content at the top level alongside metadata
|
|
43
|
+
* (e.g. `{ ...version.version, createdAt }`), so we reconstruct the `version`
|
|
44
|
+
* object by filtering out known metadata keys.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveVersionContent(version: unknown, versionMeta: Record<string, unknown>): unknown;
|
|
40
47
|
export declare function dataFromContentAPI(payload: Payload, collectionSlug: string, data: unknown): unknown;
|
|
41
48
|
export {};
|
|
42
49
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -84,10 +84,16 @@ import { transformToLocalizeStatus } from './transformPublishedLocale.js';
|
|
|
84
84
|
transformed.id = String(transformed.id);
|
|
85
85
|
}
|
|
86
86
|
// Add timestamps (Content API no longer auto-sets these in DB)
|
|
87
|
-
//
|
|
87
|
+
// Preserve existing timestamps when already present (e.g. version data that carries
|
|
88
|
+
// the parent document's timestamps), otherwise generate new ones.
|
|
89
|
+
// Explicit null means "don't change" — strip it so the API keeps the existing value.
|
|
88
90
|
const now = new Date().toISOString();
|
|
89
|
-
transformed.updatedAt
|
|
90
|
-
|
|
91
|
+
if (transformed.updatedAt === null) {
|
|
92
|
+
delete transformed.updatedAt;
|
|
93
|
+
} else if (transformed.updatedAt === undefined) {
|
|
94
|
+
transformed.updatedAt = now;
|
|
95
|
+
}
|
|
96
|
+
if (options?.createdAt && !transformed.createdAt) {
|
|
91
97
|
transformed.createdAt = now;
|
|
92
98
|
}
|
|
93
99
|
return transformed;
|
|
@@ -101,7 +107,24 @@ import { transformToLocalizeStatus } from './transformPublishedLocale.js';
|
|
|
101
107
|
*
|
|
102
108
|
* Note: Date fields come as ISO strings from Content API and stay as strings
|
|
103
109
|
* (matching MongoDB and other adapters' behavior)
|
|
104
|
-
*/
|
|
110
|
+
*/ /**
|
|
111
|
+
* Extract version content from versionData that may be flat (no nested `version` key).
|
|
112
|
+
* Some callers spread version content at the top level alongside metadata
|
|
113
|
+
* (e.g. `{ ...version.version, createdAt }`), so we reconstruct the `version`
|
|
114
|
+
* object by filtering out known metadata keys.
|
|
115
|
+
*/ export function resolveVersionContent(version, versionMeta) {
|
|
116
|
+
if (version !== undefined) {
|
|
117
|
+
return version;
|
|
118
|
+
}
|
|
119
|
+
const metaKeys = [
|
|
120
|
+
'createdAt',
|
|
121
|
+
'updatedAt',
|
|
122
|
+
'latest',
|
|
123
|
+
'parent'
|
|
124
|
+
];
|
|
125
|
+
return Object.fromEntries(Object.entries(versionMeta).filter(([key])=>!metaKeys.includes(key)));
|
|
126
|
+
}
|
|
127
|
+
export function dataFromContentAPI(payload, collectionSlug, data) {
|
|
105
128
|
if (!data || typeof data !== 'object') {
|
|
106
129
|
return data;
|
|
107
130
|
}
|
|
@@ -85,7 +85,7 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
|
|
|
85
85
|
conditions.push({
|
|
86
86
|
operator: 'equals',
|
|
87
87
|
path: fieldPath,
|
|
88
|
-
value
|
|
88
|
+
value: fieldPath === 'id' && typeof value === 'number' ? String(value) : value
|
|
89
89
|
});
|
|
90
90
|
continue;
|
|
91
91
|
}
|
|
@@ -106,6 +106,12 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
|
|
|
106
106
|
if (op === 'exists' && typeof operatorValue === 'string') {
|
|
107
107
|
finalValue = operatorValue === 'true';
|
|
108
108
|
}
|
|
109
|
+
// Content API stores all document IDs as strings.
|
|
110
|
+
// Payload sends numeric values for collections with custom numeric ID fields,
|
|
111
|
+
// so we must stringify to match.
|
|
112
|
+
if (fieldPath === 'id' && typeof finalValue === 'number') {
|
|
113
|
+
finalValue = String(finalValue);
|
|
114
|
+
}
|
|
109
115
|
conditions.push({
|
|
110
116
|
operator: op,
|
|
111
117
|
path: fieldPath,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Config } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Payload plugin that extracts cron schedule metadata and writes
|
|
4
|
+
* `.next/static/payload-schedules.json` during `figma deploy` builds.
|
|
5
|
+
*
|
|
6
|
+
* Only runs when `FIGMA_EXTRACT_SCHEDULES=true`, which is set exclusively by
|
|
7
|
+
* the deploy CLI before spawning `next build`. At Lambda runtime and during
|
|
8
|
+
* local development the env var is absent and this plugin is a no-op.
|
|
9
|
+
*/
|
|
10
|
+
export declare function scheduleExtractPlugin(): (config: Config) => Config;
|
|
11
|
+
//# sourceMappingURL=schedule-extract-plugin.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Payload plugin that extracts cron schedule metadata and writes
|
|
5
|
+
* `.next/static/payload-schedules.json` during `figma deploy` builds.
|
|
6
|
+
*
|
|
7
|
+
* Only runs when `FIGMA_EXTRACT_SCHEDULES=true`, which is set exclusively by
|
|
8
|
+
* the deploy CLI before spawning `next build`. At Lambda runtime and during
|
|
9
|
+
* local development the env var is absent and this plugin is a no-op.
|
|
10
|
+
*/ export function scheduleExtractPlugin() {
|
|
11
|
+
return (config)=>{
|
|
12
|
+
if (process.env.FIGMA_EXTRACT_SCHEDULES !== 'true') {
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
const schedules = [
|
|
16
|
+
...(config.jobs?.tasks ?? []).flatMap((task)=>(task.schedule ?? []).map((sched)=>({
|
|
17
|
+
slug: task.slug,
|
|
18
|
+
cron: sched.cron
|
|
19
|
+
}))),
|
|
20
|
+
...(config.jobs?.workflows ?? []).flatMap((workflow)=>(workflow.schedule ?? []).map((sched)=>({
|
|
21
|
+
slug: workflow.slug,
|
|
22
|
+
cron: sched.cron
|
|
23
|
+
})))
|
|
24
|
+
];
|
|
25
|
+
const outDir = path.join(process.cwd(), '.next', 'static');
|
|
26
|
+
fs.mkdirSync(outDir, {
|
|
27
|
+
recursive: true
|
|
28
|
+
});
|
|
29
|
+
fs.writeFileSync(path.join(outDir, 'payload-schedules.json'), JSON.stringify(schedules));
|
|
30
|
+
return config;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=schedule-extract-plugin.js.map
|
|
@@ -6,9 +6,10 @@ import type { Config, SanitizedConfig } from 'payload';
|
|
|
6
6
|
* - `db`: Omitted (auto-injected by Figma platform)
|
|
7
7
|
* - `secret`: Omitted (auto-injected by Figma platform)
|
|
8
8
|
* - `editor`: Optional (defaults to lexicalEditor() if not provided)
|
|
9
|
+
* - `contentSystemId`: Optional (resolved automatically from env vars or bootstrap cache)
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
|
-
* // Minimal config (contentSystemId resolved
|
|
12
|
+
* // Minimal config (contentSystemId resolved automatically)
|
|
12
13
|
* const config: FigmaConfig = {
|
|
13
14
|
* figma: {},
|
|
14
15
|
* collections: [...]
|
|
@@ -7,6 +7,7 @@ import { getValidAccessToken } from '../auth/oauth-flow.js';
|
|
|
7
7
|
import { getTokenStore } from '../auth/token-store.js';
|
|
8
8
|
import { getEnvConfig } from '../constants.js';
|
|
9
9
|
import { contentAPIAdapter } from '../db-content-api/index.js';
|
|
10
|
+
import { scheduleExtractPlugin } from '../deploy/schedule-extract-plugin.js';
|
|
10
11
|
import { health } from '../endpoints/health.js';
|
|
11
12
|
import { schema } from '../endpoints/schema.js';
|
|
12
13
|
import { oAuth2Plugin } from '../oauth/index.js';
|
|
@@ -67,6 +68,7 @@ export async function buildFigmaConfig(config) {
|
|
|
67
68
|
let { contentSystemId } = config.figma;
|
|
68
69
|
contentSystemId ??= process.env.FIGMA_CONTENT_API_CONTENT_SYSTEM_ID;
|
|
69
70
|
let bootstrapData = null;
|
|
71
|
+
// Local dev: resolve from project ID + bootstrap cache
|
|
70
72
|
if (!contentSystemId) {
|
|
71
73
|
const projectId = process.env.FIGMA_PROJECT_ID;
|
|
72
74
|
const environmentName = process.env.FIGMA_ENVIRONMENT_NAME;
|
|
@@ -219,7 +221,8 @@ export async function buildFigmaConfig(config) {
|
|
|
219
221
|
],
|
|
220
222
|
debug: !!process.env.DEBUG,
|
|
221
223
|
disabled: false
|
|
222
|
-
})
|
|
224
|
+
}),
|
|
225
|
+
scheduleExtractPlugin()
|
|
223
226
|
]
|
|
224
227
|
};
|
|
225
228
|
// Inject figma fields into users collection
|
package/dist/types.d.ts
CHANGED
|
@@ -3,15 +3,19 @@ export interface Args extends arg.Spec {
|
|
|
3
3
|
'--debug': BooleanConstructor;
|
|
4
4
|
'--dry-run': BooleanConstructor;
|
|
5
5
|
'--env': StringConstructor;
|
|
6
|
+
'--environment': string;
|
|
6
7
|
'--force': BooleanConstructor;
|
|
7
8
|
'--help': BooleanConstructor;
|
|
8
9
|
'--id': StringConstructor;
|
|
9
10
|
'--infra-env': StringConstructor;
|
|
10
11
|
'--list': BooleanConstructor;
|
|
11
12
|
'--logout': BooleanConstructor;
|
|
13
|
+
'--name': StringConstructor;
|
|
12
14
|
'--yes': BooleanConstructor;
|
|
15
|
+
'-e': string;
|
|
13
16
|
'-f': string;
|
|
14
17
|
'-h': string;
|
|
18
|
+
'-n': string;
|
|
15
19
|
'-y': string;
|
|
16
20
|
}
|
|
17
21
|
export type CliArgs = arg.Result<Args>;
|
package/dist/utils/formatter.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import spawn from 'cross-spawn';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import * as log from './log.js';
|
|
3
4
|
/**
|
|
4
5
|
* Format file with Prettier
|
|
5
6
|
* Returns warning if formatting fails but does not throw
|
|
6
7
|
*/ export async function formatFile(filePath, packageManager) {
|
|
8
|
+
const cmd = `${packageManager} exec prettier --write ${filePath}`;
|
|
9
|
+
const cwd = path.dirname(filePath);
|
|
10
|
+
log.debug(`Running formatter: ${cmd} (cwd: ${cwd})`);
|
|
7
11
|
return new Promise((resolve)=>{
|
|
8
12
|
try {
|
|
9
13
|
const child = spawn(packageManager, [
|
|
@@ -12,31 +16,41 @@ import path from 'path';
|
|
|
12
16
|
'--write',
|
|
13
17
|
filePath
|
|
14
18
|
], {
|
|
15
|
-
cwd
|
|
19
|
+
cwd,
|
|
16
20
|
stdio: 'pipe'
|
|
17
21
|
});
|
|
22
|
+
let stderr = '';
|
|
23
|
+
child.stderr?.on('data', (data)=>{
|
|
24
|
+
stderr += data.toString();
|
|
25
|
+
});
|
|
18
26
|
child.on('close', (code)=>{
|
|
19
27
|
if (code === 0) {
|
|
28
|
+
log.debug('Prettier formatting succeeded');
|
|
20
29
|
resolve({
|
|
21
30
|
success: true
|
|
22
31
|
});
|
|
23
32
|
} else {
|
|
33
|
+
const detail = stderr.trim();
|
|
34
|
+
log.debug(`Prettier exited with code ${code}${detail ? `: ${detail}` : ''}`);
|
|
24
35
|
resolve({
|
|
25
36
|
success: false,
|
|
26
|
-
warning:
|
|
37
|
+
warning: `Could not format file with Prettier (exit code ${code})${detail ? `: ${detail}` : ''}`
|
|
27
38
|
});
|
|
28
39
|
}
|
|
29
40
|
});
|
|
30
|
-
child.on('error', ()=>{
|
|
41
|
+
child.on('error', (err)=>{
|
|
42
|
+
log.debug(`Prettier spawn error: ${err.message}`);
|
|
31
43
|
resolve({
|
|
32
44
|
success: false,
|
|
33
|
-
warning:
|
|
45
|
+
warning: `Could not format file with Prettier: ${err.message}`
|
|
34
46
|
});
|
|
35
47
|
});
|
|
36
|
-
} catch
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
50
|
+
log.debug(`Prettier failed to spawn: ${message}`);
|
|
37
51
|
resolve({
|
|
38
52
|
success: false,
|
|
39
|
-
warning:
|
|
53
|
+
warning: `Could not format file with Prettier: ${message}`
|
|
40
54
|
});
|
|
41
55
|
}
|
|
42
56
|
});
|
package/dist/utils/messages.d.ts
CHANGED
package/dist/utils/messages.js
CHANGED
|
@@ -36,6 +36,7 @@ export function helpMessage() {
|
|
|
36
36
|
|
|
37
37
|
${pc.cyan('@payloadcms/figma init --id <cms-resource-id>')} Initialize project
|
|
38
38
|
${pc.cyan('@payloadcms/figma init --id <id> --env staging')} Initialize for specific environment
|
|
39
|
+
${pc.dim('--name, -n <name>')} Set project directory name (skips prompt)
|
|
39
40
|
${pc.dim('--force')} Force reconfiguration of existing project
|
|
40
41
|
|
|
41
42
|
${pc.bold('ENV COMMAND')}
|
|
@@ -109,8 +110,5 @@ It is recommended to do this from your IDE if your app has existing file referen
|
|
|
109
110
|
Once moved, rerun the @payloadcms/figma command again.
|
|
110
111
|
`;
|
|
111
112
|
}
|
|
112
|
-
export function feedbackOutro() {
|
|
113
|
-
return `${pc.bgCyan(pc.black(' Have feedback? '))} Visit us on ${createTerminalLink('GitHub', 'https://github.com/payloadcms/payload')}.`;
|
|
114
|
-
}
|
|
115
113
|
|
|
116
114
|
//# sourceMappingURL=messages.js.map
|
|
@@ -57,11 +57,6 @@ export type FigmaPropertyConfig = {
|
|
|
57
57
|
* Modifies the AST in memory - caller must call sourceFile.save()
|
|
58
58
|
*/
|
|
59
59
|
export declare function addFigmaProperty(sourceFile: SourceFile, config: FigmaPropertyConfig): ASTModificationResult;
|
|
60
|
-
/**
|
|
61
|
-
* Read figma configuration from payload.config.ts
|
|
62
|
-
* Returns the figma object if it exists, null otherwise
|
|
63
|
-
*/
|
|
64
|
-
export declare function readFigmaConfig(sourceFile: SourceFile): FigmaPropertyConfig | null;
|
|
65
60
|
/**
|
|
66
61
|
* Remove the contentSystemId property from figma config object.
|
|
67
62
|
* Used during upgrade migration — contentSystemId is now resolved from bootstrap data.
|
|
@@ -385,14 +385,9 @@ import * as log from './log.js';
|
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
// Add figma property
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
const figmaObj = config.useContentSystem === false ? `{
|
|
391
|
-
contentSystemId: process.env.FIGMA_CONTENT_API_CONTENT_SYSTEM_ID!,
|
|
392
|
-
useContentSystem: false,
|
|
393
|
-
}` : `{
|
|
394
|
-
contentSystemId: process.env.FIGMA_CONTENT_API_CONTENT_SYSTEM_ID!,
|
|
395
|
-
}`;
|
|
388
|
+
// contentSystemId is resolved automatically by buildFigmaConfig from env vars or bootstrap cache.
|
|
389
|
+
// useContentSystem defaults to true, so we only generate it when explicitly false.
|
|
390
|
+
const figmaObj = config.useContentSystem === false ? `{\nuseContentSystem: false,\n}` : '{}';
|
|
396
391
|
configArg.addPropertyAssignment({
|
|
397
392
|
name: 'figma',
|
|
398
393
|
initializer: figmaObj
|
|
@@ -404,74 +399,6 @@ import * as log from './log.js';
|
|
|
404
399
|
modified
|
|
405
400
|
};
|
|
406
401
|
}
|
|
407
|
-
/**
|
|
408
|
-
* Read figma configuration from payload.config.ts
|
|
409
|
-
* Returns the figma object if it exists, null otherwise
|
|
410
|
-
*/ export function readFigmaConfig(sourceFile) {
|
|
411
|
-
// Find buildFigmaConfig call (this function is only used with Figma configs)
|
|
412
|
-
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
413
|
-
const buildConfigCall = callExpressions.find((ce)=>{
|
|
414
|
-
const expr = ce.getExpression();
|
|
415
|
-
const text = expr.getText();
|
|
416
|
-
return text === 'buildFigmaConfig' || text.endsWith('.buildFigmaConfig');
|
|
417
|
-
});
|
|
418
|
-
if (!buildConfigCall) {
|
|
419
|
-
log.debug('No buildFigmaConfig call found');
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
// Get config object argument
|
|
423
|
-
const configArg = buildConfigCall.getArguments()[0];
|
|
424
|
-
if (!configArg || !Node.isObjectLiteralExpression(configArg)) {
|
|
425
|
-
log.debug('buildConfig argument is not an object literal');
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
// Find figma property
|
|
429
|
-
const figmaProperty = configArg.getProperty('figma');
|
|
430
|
-
if (!figmaProperty || !Node.isPropertyAssignment(figmaProperty)) {
|
|
431
|
-
log.debug('No figma property found in buildConfig');
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
// Get initializer (the object value)
|
|
435
|
-
const initializer = figmaProperty.getInitializer();
|
|
436
|
-
if (!initializer || !Node.isObjectLiteralExpression(initializer)) {
|
|
437
|
-
log.debug('figma property is not an object literal');
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
// Extract values
|
|
441
|
-
const contentSystemIdProp = initializer.getProperty('contentSystemId');
|
|
442
|
-
const useContentSystemProp = initializer.getProperty('useContentSystem');
|
|
443
|
-
if (!contentSystemIdProp) {
|
|
444
|
-
log.debug('Missing required figma property: contentSystemId');
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
// Extract contentSystemId - support both literal strings and env var references
|
|
448
|
-
let contentSystemId;
|
|
449
|
-
if (Node.isPropertyAssignment(contentSystemIdProp)) {
|
|
450
|
-
const initializer = contentSystemIdProp.getInitializer();
|
|
451
|
-
const text = initializer?.getText() || '';
|
|
452
|
-
// Support both patterns:
|
|
453
|
-
// 1. Literal string: 'cms_abc123' or "cms_abc123"
|
|
454
|
-
// 2. Environment variable: process.env.FIGMA_CONTENT_API_CONTENT_SYSTEM_ID
|
|
455
|
-
if (text.includes('process.env.FIGMA_CONTENT_API_CONTENT_SYSTEM_ID')) {
|
|
456
|
-
// For env var reference, return a marker that indicates it's from env
|
|
457
|
-
// The actual value will be read at runtime
|
|
458
|
-
contentSystemId = 'process.env.FIGMA_CONTENT_API_CONTENT_SYSTEM_ID';
|
|
459
|
-
} else {
|
|
460
|
-
// Remove quotes for literal strings
|
|
461
|
-
contentSystemId = text.replace(/['"]/g, '');
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const useContentSystem = Node.isPropertyAssignment(useContentSystemProp) ? useContentSystemProp.getInitializer()?.getText() === 'true' : undefined // Optional: undefined if not specified
|
|
465
|
-
;
|
|
466
|
-
if (!contentSystemId) {
|
|
467
|
-
log.debug('Could not extract contentSystemId value');
|
|
468
|
-
return null;
|
|
469
|
-
}
|
|
470
|
-
return {
|
|
471
|
-
contentSystemId,
|
|
472
|
-
useContentSystem
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
402
|
/**
|
|
476
403
|
* Remove the contentSystemId property from figma config object.
|
|
477
404
|
* Used during upgrade migration — contentSystemId is now resolved from bootstrap data.
|
|
@@ -504,6 +431,10 @@ import * as log from './log.js';
|
|
|
504
431
|
return false;
|
|
505
432
|
}
|
|
506
433
|
contentSystemIdProp.remove();
|
|
434
|
+
// Collapse empty object to avoid ts-morph whitespace artifacts
|
|
435
|
+
if (initializer.getProperties().length === 0) {
|
|
436
|
+
initializer.replaceWithText('{}');
|
|
437
|
+
}
|
|
507
438
|
return true;
|
|
508
439
|
}
|
|
509
440
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { Project } from 'ts-morph';
|
|
2
|
+
import { IndentationText, Project } from 'ts-morph';
|
|
3
3
|
import { formatFile } from './formatter.js';
|
|
4
4
|
import * as log from './log.js';
|
|
5
5
|
import { getAddCommand, getRunCommand } from './package-manager.js';
|
|
@@ -57,6 +57,10 @@ import { getOwnVersion } from './version-check.js';
|
|
|
57
57
|
// 3. Parse config with ts-morph
|
|
58
58
|
log.debug('Parsing config file with ts-morph...');
|
|
59
59
|
const project = new Project({
|
|
60
|
+
manipulationSettings: {
|
|
61
|
+
indentationText: IndentationText.TwoSpaces,
|
|
62
|
+
useTrailingCommas: true
|
|
63
|
+
},
|
|
60
64
|
skipAddingFilesFromTsConfig: true
|
|
61
65
|
});
|
|
62
66
|
const sourceFile = project.addSourceFileAtPath(configPath);
|
|
@@ -107,6 +111,10 @@ import { getOwnVersion } from './version-check.js';
|
|
|
107
111
|
}
|
|
108
112
|
// Save file if any modifications were made
|
|
109
113
|
if (modified) {
|
|
114
|
+
sourceFile.formatText({
|
|
115
|
+
indentSize: 2,
|
|
116
|
+
tabSize: 2
|
|
117
|
+
});
|
|
110
118
|
await sourceFile.save();
|
|
111
119
|
log.debug('Config file saved');
|
|
112
120
|
// 7. Handle package management
|
|
@@ -2,7 +2,7 @@ import type { BootstrapEnvironment } from '../api/control-plane.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Resolve which environment to use from the bootstrap response.
|
|
4
4
|
*
|
|
5
|
-
* - Single environment: auto-select (environmentName optional)
|
|
5
|
+
* - Single environment: auto-select (environmentName optional, validated if provided)
|
|
6
6
|
* - Multiple environments: environmentName required, must match
|
|
7
7
|
*
|
|
8
8
|
* @throws Error if resolution fails (with available environment names)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve which environment to use from the bootstrap response.
|
|
3
3
|
*
|
|
4
|
-
* - Single environment: auto-select (environmentName optional)
|
|
4
|
+
* - Single environment: auto-select (environmentName optional, validated if provided)
|
|
5
5
|
* - Multiple environments: environmentName required, must match
|
|
6
6
|
*
|
|
7
7
|
* @throws Error if resolution fails (with available environment names)
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
throw new Error('No environments found for this project.');
|
|
12
12
|
}
|
|
13
13
|
if (environments.length === 1) {
|
|
14
|
+
if (environmentName && environments[0].name !== environmentName) {
|
|
15
|
+
throw new Error(`Environment "${environmentName}" not found. Available: ${environments[0].name}`);
|
|
16
|
+
}
|
|
14
17
|
return environments[0];
|
|
15
18
|
}
|
|
16
19
|
// Multiple environments — name required
|