@payloadcms/figma 0.0.1-alpha.57 → 0.0.1-alpha.59
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 +8 -7
- package/dist/api/control-plane.js +16 -23
- package/dist/api/figma-api.d.ts +3 -2
- package/dist/api/figma-api.js +6 -9
- package/dist/auth/credentials.d.ts +21 -0
- package/dist/auth/credentials.js +21 -0
- package/dist/auth/oauth-flow.d.ts +13 -4
- package/dist/auth/oauth-flow.js +34 -8
- package/dist/auth/project-token.js +5 -5
- package/dist/cli.js +5 -0
- package/dist/commands/deploy.d.ts +3 -0
- package/dist/commands/deploy.js +67 -37
- package/dist/commands/env.js +4 -4
- package/dist/commands/init.js +10 -25
- package/dist/commands/login.js +3 -3
- package/dist/commands/upgrade.js +11 -11
- package/dist/db-content-api/index.js +48 -11
- package/dist/db-content-api/temp-utilities/sorting.d.ts +1 -1
- package/dist/db-content-api/temp-utilities/sorting.js +4 -1
- package/dist/db-content-api/temp-utilities/unwrapDocument.d.ts +7 -2
- package/dist/db-content-api/temp-utilities/unwrapDocument.js +11 -5
- package/dist/db-content-api/utilities/data/castFieldValue.js +11 -12
- package/dist/db-content-api/utilities/data/index.d.ts +1 -1
- package/dist/db-content-api/utilities/data/index.js +45 -20
- package/dist/db-content-api/utilities/joins.js +10 -4
- package/dist/db-content-api/utilities/meta/buildLocalizedPaths.js +14 -6
- package/dist/db-content-api/utilities/meta/buildPathTypes.js +19 -2
- package/dist/db-content-api/utilities/where.js +15 -45
- package/dist/oauth/endpoints/getLoginEndpoint.js +10 -2
- package/dist/oauth/utilities/refreshTokens.js +4 -1
- package/dist/plugin/build-config.js +4 -4
- package/dist/utils/adapters/nextjs.d.ts +9 -0
- package/dist/utils/adapters/nextjs.js +59 -0
- package/dist/utils/adapters/nitro.d.ts +9 -0
- package/dist/utils/adapters/nitro.js +164 -0
- package/dist/utils/adapters/vite.d.ts +10 -0
- package/dist/utils/adapters/vite.js +32 -0
- package/dist/utils/asset-collection.d.ts +24 -0
- package/dist/utils/asset-collection.js +53 -0
- package/dist/utils/build-detection.d.ts +5 -11
- package/dist/utils/build-detection.js +74 -11
- package/dist/utils/deploy-adapter.d.ts +38 -0
- package/dist/utils/deploy-adapter.js +58 -0
- package/dist/utils/download-template.js +3 -2
- package/dist/utils/fs-utils.d.ts +6 -0
- package/dist/utils/fs-utils.js +27 -0
- package/dist/utils/s3-upload.d.ts +7 -1
- package/dist/utils/s3-upload.js +4 -3
- package/package.json +1 -1
|
@@ -46,17 +46,32 @@ import { transformToLocalizeStatus } from './transformPublishedLocale.js';
|
|
|
46
46
|
}
|
|
47
47
|
const current = ref;
|
|
48
48
|
let value = current[field.name];
|
|
49
|
-
//
|
|
49
|
+
// null → [] for non-localized array-like fields. SQL adapters return null for empty join
|
|
50
|
+
// tables; Content API stores JSON null which breaks jsonb_array_elements queries.
|
|
51
|
+
if (value === null && !('localized' in field && field.localized)) {
|
|
52
|
+
const isHasManyArray = (field.type === 'relationship' || field.type === 'upload') && 'hasMany' in field && field.hasMany;
|
|
53
|
+
const isArrayLike = field.type === 'array' || field.type === 'blocks';
|
|
54
|
+
const isHasManySelect = field.type === 'select' && 'hasMany' in field && field.hasMany;
|
|
55
|
+
if (isHasManyArray || isArrayLike || isHasManySelect) {
|
|
56
|
+
current[field.name] = [];
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
50
60
|
if (value !== null && value !== undefined) {
|
|
51
|
-
// Step 1: Apply type casting (e.g., number -> string for text fields)
|
|
52
61
|
value = castFieldValue(field, value);
|
|
53
62
|
current[field.name] = value;
|
|
54
|
-
//
|
|
63
|
+
// Normalize non-localized hasMany relationship/upload: single value → array.
|
|
64
|
+
// Payload may not wrap in array before calling db.update (e.g. children: 'uuid').
|
|
65
|
+
const isNonLocalizedHasMany = !('localized' in field && field.localized) && (field.type === 'relationship' || field.type === 'upload') && 'hasMany' in field && field.hasMany;
|
|
66
|
+
if (isNonLocalizedHasMany && !Array.isArray(value)) {
|
|
67
|
+
current[field.name] = [
|
|
68
|
+
value
|
|
69
|
+
];
|
|
70
|
+
}
|
|
55
71
|
// RichText: object -> JSON string
|
|
56
72
|
if (field.type === 'richText' && typeof value !== 'string') {
|
|
57
73
|
current[field.name] = JSON.stringify(value);
|
|
58
74
|
}
|
|
59
|
-
// Date fields are already handled by JSON.stringify() which converts Date -> ISO string
|
|
60
75
|
}
|
|
61
76
|
};
|
|
62
77
|
traverseFields({
|
|
@@ -124,7 +139,7 @@ import { transformToLocalizeStatus } from './transformPublishedLocale.js';
|
|
|
124
139
|
];
|
|
125
140
|
return Object.fromEntries(Object.entries(versionMeta).filter(([key])=>!metaKeys.includes(key)));
|
|
126
141
|
}
|
|
127
|
-
export function dataFromContentAPI(payload, collectionSlug, data) {
|
|
142
|
+
export function dataFromContentAPI(payload, collectionSlug, data, locale) {
|
|
128
143
|
if (!data || typeof data !== 'object') {
|
|
129
144
|
return data;
|
|
130
145
|
}
|
|
@@ -137,6 +152,7 @@ export function dataFromContentAPI(payload, collectionSlug, data) {
|
|
|
137
152
|
if (!collectionConfig?.fields) {
|
|
138
153
|
return transformed;
|
|
139
154
|
}
|
|
155
|
+
const isAllLocales = locale === 'all' || locale === '*';
|
|
140
156
|
// Use Payload's traverseFields to iterate over all fields
|
|
141
157
|
const callback = ({ field, parentPath, ref })=>{
|
|
142
158
|
if (!('name' in field) || !field.name) {
|
|
@@ -147,41 +163,50 @@ export function dataFromContentAPI(payload, collectionSlug, data) {
|
|
|
147
163
|
}
|
|
148
164
|
const current = ref;
|
|
149
165
|
const value = current[field.name];
|
|
150
|
-
|
|
151
|
-
// Content API may omit fields
|
|
166
|
+
const isLocalized = 'localized' in field && field.localized;
|
|
167
|
+
// Content API may omit fields; Payload expects null for optional fields.
|
|
168
|
+
// In all-locales mode, localized fields use {} so afterRead can safely iterate locale keys.
|
|
152
169
|
if (value === undefined) {
|
|
153
170
|
const isRequired = 'required' in field && field.required;
|
|
154
171
|
if (!isRequired) {
|
|
155
|
-
current[field.name] = null;
|
|
172
|
+
current[field.name] = isLocalized && isAllLocales ? {} : null;
|
|
156
173
|
}
|
|
157
174
|
return;
|
|
158
175
|
}
|
|
176
|
+
// null localized field in all-locales mode → {} (empty locale map)
|
|
177
|
+
if (value === null && isLocalized && isAllLocales) {
|
|
178
|
+
current[field.name] = {};
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
159
181
|
if (value !== null) {
|
|
160
182
|
// hasMany relationships: normalize to array (non-localized only).
|
|
161
|
-
//
|
|
162
|
-
// so a single-item input like `{ value, relationTo }` stays as-is. Payload expects arrays.
|
|
163
|
-
// Localized fields are skipped because their value is a locale map, not a relationship value.
|
|
183
|
+
// We store raw JSON so a single value stays as-is; Payload expects arrays.
|
|
164
184
|
if (field.type === 'relationship' && field.hasMany && !Array.isArray(value) && !('localized' in field && field.localized)) {
|
|
165
185
|
current[field.name] = [
|
|
166
186
|
value
|
|
167
187
|
];
|
|
168
188
|
}
|
|
169
|
-
// Localized
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
// Localized array/blocks: empty per-locale arrays → null in single-locale mode.
|
|
190
|
+
// Payload's afterRead fallback triggers on null (not []), matching SQL adapter behavior.
|
|
191
|
+
if (isLocalized && !isAllLocales && (field.type === 'array' || field.type === 'blocks') && typeof value === 'object' && !Array.isArray(value)) {
|
|
192
|
+
const localeMap = value;
|
|
193
|
+
for (const localeKey of Object.keys(localeMap)){
|
|
194
|
+
const localeValue = localeMap[localeKey];
|
|
195
|
+
if (Array.isArray(localeValue) && localeValue.length === 0) {
|
|
196
|
+
localeMap[localeKey] = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Localized fields: JSON string → parsed object (locale map).
|
|
201
|
+
// Simple strings that fail to parse as objects are kept as-is (expected for single-locale).
|
|
175
202
|
if ('localized' in field && field.localized && typeof value === 'string') {
|
|
176
203
|
try {
|
|
177
204
|
const parsed = JSON.parse(value);
|
|
178
|
-
// Only use parsed value if it's actually an object (locale -> value mapping)
|
|
179
|
-
// Simple strings like "EN Title" would parse to themselves, which we don't want
|
|
180
205
|
if (typeof parsed === 'object' && parsed !== null) {
|
|
181
206
|
current[field.name] = parsed;
|
|
182
207
|
}
|
|
183
208
|
} catch {
|
|
184
|
-
// Not valid JSON - keep original value
|
|
209
|
+
// Not valid JSON - keep original value
|
|
185
210
|
}
|
|
186
211
|
} else if (field.type === 'richText' && typeof value === 'string') {
|
|
187
212
|
try {
|
|
@@ -76,6 +76,8 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
76
76
|
if (sanitizedJoin.joinPath === joinPath) {
|
|
77
77
|
foundJoin = {
|
|
78
78
|
collectionSlug,
|
|
79
|
+
defaultLimit: sanitizedJoin.field.defaultLimit,
|
|
80
|
+
defaultSort: sanitizedJoin.field.defaultSort,
|
|
79
81
|
on: sanitizedJoin.field.on
|
|
80
82
|
};
|
|
81
83
|
break;
|
|
@@ -93,6 +95,7 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
93
95
|
const collections = sanitizedJoin.field.collection;
|
|
94
96
|
foundJoin = {
|
|
95
97
|
collectionSlug: Array.isArray(collections) ? collections[0] : collections,
|
|
98
|
+
defaultSort: sanitizedJoin.field.defaultSort,
|
|
96
99
|
on: sanitizedJoin.field.on
|
|
97
100
|
};
|
|
98
101
|
break;
|
|
@@ -104,19 +107,22 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
104
107
|
continue;
|
|
105
108
|
}
|
|
106
109
|
// Convert pagination: Payload uses page, Content API JoinClause uses offset
|
|
110
|
+
const effectiveLimit = joinQuery.limit ?? foundJoin.defaultLimit;
|
|
107
111
|
let offset;
|
|
108
|
-
if (joinQuery.page && joinQuery.page > 1 &&
|
|
109
|
-
offset = (joinQuery.page - 1) *
|
|
112
|
+
if (joinQuery.page && joinQuery.page > 1 && effectiveLimit) {
|
|
113
|
+
offset = (joinQuery.page - 1) * effectiveLimit;
|
|
110
114
|
}
|
|
111
115
|
// Add fallback sort for the joined collection
|
|
112
|
-
const sortWithFallback = addFallbackSort(joinQuery.sort, payload, foundJoin.collectionSlug);
|
|
116
|
+
const sortWithFallback = addFallbackSort(joinQuery.sort, payload, foundJoin.collectionSlug, foundJoin.defaultSort);
|
|
113
117
|
const contentAPIJoin = {
|
|
114
118
|
collectionId: foundJoin.collectionSlug,
|
|
115
119
|
count: joinQuery.count ?? false,
|
|
116
120
|
on: foundJoin.on,
|
|
117
121
|
path: joinPath,
|
|
118
|
-
...joinQuery.limit !== undefined
|
|
122
|
+
...joinQuery.limit !== undefined ? {
|
|
119
123
|
limit: joinQuery.limit
|
|
124
|
+
} : foundJoin.defaultLimit !== undefined && {
|
|
125
|
+
limit: foundJoin.defaultLimit
|
|
120
126
|
},
|
|
121
127
|
...offset !== undefined && {
|
|
122
128
|
offset
|
|
@@ -27,13 +27,21 @@
|
|
|
27
27
|
}
|
|
28
28
|
/** Recursively traverses fields to find and collect localized paths */ function traverseFields(fields, localizedPaths, parentPath = '', parentIsLocalized = false) {
|
|
29
29
|
for (const field of fields){
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// This will skip presentational fields like ui, collapsible, row, tabs (if unnamed), groups (if unnamed)
|
|
34
|
-
// which do not store data in the database and thus are not relevant for querying localized paths
|
|
35
|
-
// See: https://payloadcms.com/docs/fields/overview#presentational-fields
|
|
30
|
+
// Unnamed layout fields (row, collapsible, unnamed group, unnamed tabs) don't store data
|
|
31
|
+
// at their own level, but their child fields may be localized. Recurse into them with
|
|
32
|
+
// the same parent path so localized fields bubble up correctly.
|
|
36
33
|
if (!('name' in field)) {
|
|
34
|
+
if ('fields' in field && Array.isArray(field.fields)) {
|
|
35
|
+
traverseFields(field.fields, localizedPaths, parentPath, parentIsLocalized);
|
|
36
|
+
}
|
|
37
|
+
if ('tabs' in field && Array.isArray(field.tabs)) {
|
|
38
|
+
for (const tab of field.tabs){
|
|
39
|
+
if (tab.fields) {
|
|
40
|
+
const tabPath = tab.name ? buildPath(parentPath, tab.name) : parentPath;
|
|
41
|
+
traverseFields(tab.fields, localizedPaths, tabPath, parentIsLocalized);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
37
45
|
continue;
|
|
38
46
|
}
|
|
39
47
|
const fieldPath = buildPath(parentPath, field.name);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
continue;
|
|
25
25
|
}
|
|
26
26
|
// Add the path
|
|
27
|
-
paths.add(key);
|
|
27
|
+
paths.add(key.replace(/__/g, '.'));
|
|
28
28
|
// Check if the value is a nested where clause (for relationships)
|
|
29
29
|
const value = where[key];
|
|
30
30
|
if (value && typeof value === 'object' && !('equals' in value) && !('in' in value)) {
|
|
@@ -74,6 +74,14 @@
|
|
|
74
74
|
on
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
// Text field with hasMany (array of strings)
|
|
78
|
+
if (field.type === 'text' && 'hasMany' in field && field.hasMany) {
|
|
79
|
+
return 'array';
|
|
80
|
+
}
|
|
81
|
+
// Number field with hasMany (array of numbers)
|
|
82
|
+
if (field.type === 'number' && 'hasMany' in field && field.hasMany) {
|
|
83
|
+
return 'array';
|
|
84
|
+
}
|
|
77
85
|
// Select field with hasMany (array)
|
|
78
86
|
if (field.type === 'select' && 'hasMany' in field && field.hasMany) {
|
|
79
87
|
return 'array';
|
|
@@ -102,7 +110,7 @@
|
|
|
102
110
|
for (const field of currentFields){
|
|
103
111
|
if (!('name' in field) || !field.name) {
|
|
104
112
|
// This is an unnamed field, check its contents
|
|
105
|
-
if ('fields' in field && field.type === 'collapsible' || 'fields' in field && field.type === 'row') {
|
|
113
|
+
if ('fields' in field && field.type === 'collapsible' || 'fields' in field && field.type === 'row' || 'fields' in field && field.type === 'group') {
|
|
106
114
|
currentField = field.fields.find((f)=>'name' in f && f.name === segment);
|
|
107
115
|
if (currentField) {
|
|
108
116
|
break;
|
|
@@ -135,6 +143,15 @@
|
|
|
135
143
|
// For blocks, we can't determine the exact field without knowing the block type
|
|
136
144
|
// Return the blocks field itself
|
|
137
145
|
return currentField;
|
|
146
|
+
} else if ((currentField.type === 'relationship' || currentField.type === 'upload') && typeof currentField.relationTo === 'string') {
|
|
147
|
+
// Traverse into the related collection's fields
|
|
148
|
+
const { relationTo } = currentField;
|
|
149
|
+
const relatedConfig = payload.config.collections.find((c)=>c.slug === relationTo);
|
|
150
|
+
if (relatedConfig) {
|
|
151
|
+
currentFields = relatedConfig.fields;
|
|
152
|
+
} else {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
138
155
|
} else {
|
|
139
156
|
// Field doesn't have nested fields, can't continue
|
|
140
157
|
return undefined;
|
|
@@ -8,63 +8,30 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
|
|
|
8
8
|
}
|
|
9
9
|
const conditions = [];
|
|
10
10
|
for (const [key, value] of Object.entries(where)){
|
|
11
|
-
if (key === 'and') {
|
|
12
|
-
// Recursively convert nested 'and' conditions, preserving options
|
|
11
|
+
if (key === 'and' || key === 'or') {
|
|
13
12
|
const nestedConditions = value.map((item)=>convertPayloadWhereToContentAPI(item, {
|
|
14
13
|
...options,
|
|
15
14
|
insideLogicalOperator: true
|
|
16
|
-
}))
|
|
17
|
-
// more permissive. For now I'll do it this way because I have an idea to simplify
|
|
18
|
-
// everything we're doing with `where` (Post EAP) anyway.
|
|
19
|
-
// Filter out empty conditions { and: [] } that would be invalid
|
|
20
|
-
.filter((condition)=>{
|
|
15
|
+
})).filter((condition)=>{
|
|
21
16
|
if ('and' in condition) {
|
|
22
17
|
return condition.and.length > 0;
|
|
23
18
|
}
|
|
24
19
|
if ('or' in condition) {
|
|
25
20
|
return condition.or.length > 0;
|
|
26
21
|
}
|
|
27
|
-
return true
|
|
28
|
-
;
|
|
22
|
+
return true;
|
|
29
23
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
and: nestedConditions
|
|
38
|
-
};
|
|
39
|
-
} else if (key === 'or') {
|
|
40
|
-
// Recursively convert nested 'or' conditions, preserving options
|
|
41
|
-
const nestedConditions = value.map((item)=>convertPayloadWhereToContentAPI(item, {
|
|
42
|
-
...options,
|
|
43
|
-
insideLogicalOperator: true
|
|
44
|
-
}))// Filter out empty conditions { and: [] } or { or: [] } that would be invalid
|
|
45
|
-
.filter((condition)=>{
|
|
46
|
-
if ('and' in condition) {
|
|
47
|
-
return condition.and.length > 0;
|
|
48
|
-
}
|
|
49
|
-
if ('or' in condition) {
|
|
50
|
-
return condition.or.length > 0;
|
|
51
|
-
}
|
|
52
|
-
return true // Keep path/operator/value conditions
|
|
53
|
-
;
|
|
54
|
-
});
|
|
55
|
-
// If filtering resulted in no conditions, return empty and
|
|
56
|
-
if (nestedConditions.length === 0) {
|
|
57
|
-
return {
|
|
58
|
-
and: []
|
|
59
|
-
};
|
|
24
|
+
if (key === 'and') {
|
|
25
|
+
conditions.push(...nestedConditions);
|
|
26
|
+
} else if (nestedConditions.length > 0) {
|
|
27
|
+
conditions.push({
|
|
28
|
+
or: nestedConditions
|
|
29
|
+
});
|
|
60
30
|
}
|
|
61
|
-
return {
|
|
62
|
-
or: nestedConditions
|
|
63
|
-
};
|
|
64
31
|
} else {
|
|
65
32
|
// TODO: fix this in content api
|
|
66
33
|
// WORKAROUND: Payload uses 'parent' for version queries, but Content API expects 'documentId'
|
|
67
|
-
let fieldPath = key;
|
|
34
|
+
let fieldPath = key.replace(/__/g, '.');
|
|
68
35
|
if (options.parentToDocumentId && key === 'parent') {
|
|
69
36
|
fieldPath = 'documentId';
|
|
70
37
|
}
|
|
@@ -120,8 +87,11 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
|
|
|
120
87
|
}
|
|
121
88
|
}
|
|
122
89
|
}
|
|
123
|
-
|
|
124
|
-
|
|
90
|
+
if (conditions.length === 0) {
|
|
91
|
+
return {
|
|
92
|
+
and: []
|
|
93
|
+
};
|
|
94
|
+
}
|
|
125
95
|
if (options.insideLogicalOperator && conditions.length === 1) {
|
|
126
96
|
return conditions[0];
|
|
127
97
|
}
|
|
@@ -170,7 +170,11 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
|
|
|
170
170
|
// Fetch Figma user info for handle and profile image
|
|
171
171
|
let figmaUserInfo = null;
|
|
172
172
|
try {
|
|
173
|
-
|
|
173
|
+
const oauthCredential = {
|
|
174
|
+
type: 'oauth',
|
|
175
|
+
token: access_token
|
|
176
|
+
};
|
|
177
|
+
figmaUserInfo = await getUserInfo(oauthCredential);
|
|
174
178
|
debugLogger.info({
|
|
175
179
|
figmaUserInfo,
|
|
176
180
|
msg: 'Fetched Figma user info'
|
|
@@ -192,7 +196,11 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
|
|
|
192
196
|
throw new APIError(errMsg);
|
|
193
197
|
}
|
|
194
198
|
// Swap for project token
|
|
195
|
-
const
|
|
199
|
+
const oauthCredential = {
|
|
200
|
+
type: 'oauth',
|
|
201
|
+
token: access_token
|
|
202
|
+
};
|
|
203
|
+
const project_token = await getProjectToken(oauthCredential, config.custom.figma.contentSystemId);
|
|
196
204
|
const refresh_token = access_token;
|
|
197
205
|
// Store project token as access_token
|
|
198
206
|
const token = project_token.token;
|
|
@@ -16,7 +16,10 @@ export const refreshTokens = async ({ payload, refreshToken, strategy })=>{
|
|
|
16
16
|
// so we will refresh both tokens
|
|
17
17
|
try {
|
|
18
18
|
// Get another project token
|
|
19
|
-
const { expiresAt, token } = await getProjectToken(
|
|
19
|
+
const { expiresAt, token } = await getProjectToken({
|
|
20
|
+
type: 'oauth',
|
|
21
|
+
token: refreshToken
|
|
22
|
+
}, payload.config.custom.figma.contentSystemId);
|
|
20
23
|
debugLogger.info({
|
|
21
24
|
expiresAt,
|
|
22
25
|
msg: 'Received new tokens from Figma API',
|
|
@@ -3,7 +3,7 @@ import { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities';
|
|
|
3
3
|
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
|
4
4
|
import { buildConfig as payloadBuildConfig } from 'payload';
|
|
5
5
|
import { getBootstrapInfo } from '../api/control-plane.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getValidCredential } 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';
|
|
@@ -50,11 +50,11 @@ function missingOAuthCredential(name) {
|
|
|
50
50
|
* Returns the requested environment's data, or null if resolution fails.
|
|
51
51
|
*/ async function resolveAndCacheBootstrap(store, projectId, environmentName) {
|
|
52
52
|
try {
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
53
|
+
const credential = await getValidCredential(store);
|
|
54
|
+
if (!credential) {
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
|
-
const bootstrapInfo = await getBootstrapInfo(
|
|
57
|
+
const bootstrapInfo = await getBootstrapInfo(credential, projectId);
|
|
58
58
|
cacheAllEnvironments(store, projectId, bootstrapInfo);
|
|
59
59
|
return store.getBootstrapData(projectId, environmentName);
|
|
60
60
|
} catch (error) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AssetCollection, DeployAdapter, PageCollection } from '../deploy-adapter.js';
|
|
2
|
+
export declare class NextjsAdapter implements DeployAdapter {
|
|
3
|
+
name: "nextjs";
|
|
4
|
+
collectAssets(projectPath: string): Promise<AssetCollection>;
|
|
5
|
+
collectPages(projectPath: string): Promise<PageCollection>;
|
|
6
|
+
prepareLambdaBundle(projectPath: string): Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare const nextjsAdapter: NextjsAdapter;
|
|
9
|
+
//# sourceMappingURL=nextjs.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { collectSSGAssets, collectStaticAssets } from '../asset-collection.js';
|
|
4
|
+
import { buildLambdaZip } from '../build-lambda-zip.js';
|
|
5
|
+
import { collectFilesRecursive } from '../fs-utils.js';
|
|
6
|
+
export class NextjsAdapter {
|
|
7
|
+
name = 'nextjs';
|
|
8
|
+
async collectAssets(projectPath) {
|
|
9
|
+
// Bundle assets: .next/static/ → _next/static/
|
|
10
|
+
const staticAssets = await collectStaticAssets(projectPath);
|
|
11
|
+
// Public assets: public/ directory
|
|
12
|
+
const publicDir = path.join(projectPath, 'public');
|
|
13
|
+
let publicAssets = [];
|
|
14
|
+
try {
|
|
15
|
+
await fs.stat(publicDir);
|
|
16
|
+
publicAssets = await collectFilesRecursive(publicDir, publicDir);
|
|
17
|
+
} catch {
|
|
18
|
+
// No public directory
|
|
19
|
+
}
|
|
20
|
+
const allKeys = [
|
|
21
|
+
...staticAssets,
|
|
22
|
+
...publicAssets
|
|
23
|
+
];
|
|
24
|
+
const pathMap = {};
|
|
25
|
+
for (const key of staticAssets){
|
|
26
|
+
pathMap[key] = key.replace(/^_next\//, '.next/');
|
|
27
|
+
}
|
|
28
|
+
for (const key of publicAssets){
|
|
29
|
+
pathMap[key] = path.join('public', key);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
pathMap,
|
|
33
|
+
routes: allKeys.map((k)=>`/${k}`),
|
|
34
|
+
uploadKeys: allKeys
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async collectPages(projectPath) {
|
|
38
|
+
const ssg = await collectSSGAssets(projectPath);
|
|
39
|
+
// Extract route paths from S3 keys: "products.html" → "/products", "index.html" → "/"
|
|
40
|
+
const routes = [
|
|
41
|
+
...new Set(ssg.keys.filter((k)=>k.endsWith('.html')).map((k)=>{
|
|
42
|
+
const stripped = k.replace(/\.html$/, '');
|
|
43
|
+
return stripped === 'index' ? '/' : `/${stripped}`;
|
|
44
|
+
}))
|
|
45
|
+
];
|
|
46
|
+
return {
|
|
47
|
+
pathMap: ssg.pathMap,
|
|
48
|
+
routes,
|
|
49
|
+
uploadKeys: ssg.keys
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async prepareLambdaBundle(projectPath) {
|
|
53
|
+
await buildLambdaZip(projectPath);
|
|
54
|
+
return path.join(projectPath, 'lambda.zip');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export const nextjsAdapter = new NextjsAdapter();
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=nextjs.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AssetCollection, DeployAdapter, PageCollection } from '../deploy-adapter.js';
|
|
2
|
+
export declare class NitroAdapter implements DeployAdapter {
|
|
3
|
+
name: "nitro";
|
|
4
|
+
collectAssets(projectPath: string): Promise<AssetCollection>;
|
|
5
|
+
collectPages(projectPath: string): Promise<PageCollection>;
|
|
6
|
+
prepareLambdaBundle(projectPath: string): Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare const nitroAdapter: NitroAdapter;
|
|
9
|
+
//# sourceMappingURL=nitro.d.ts.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import archiver from 'archiver';
|
|
2
|
+
import { createWriteStream, statSync } from 'fs';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { collectFilesRecursive } from '../fs-utils.js';
|
|
6
|
+
const RUN_SCRIPT = `#!/bin/bash -x
|
|
7
|
+
[ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache
|
|
8
|
+
NODE_ENV=production exec node server/index.mjs
|
|
9
|
+
`;
|
|
10
|
+
/** Directories in .output/public/ that are NOT pre-rendered pages */ const NON_PAGE_DIRS = new Set([
|
|
11
|
+
'_next',
|
|
12
|
+
'assets'
|
|
13
|
+
]);
|
|
14
|
+
export class NitroAdapter {
|
|
15
|
+
name = 'nitro';
|
|
16
|
+
async collectAssets(projectPath) {
|
|
17
|
+
const publicDir = path.join(projectPath, '.output', 'public');
|
|
18
|
+
const assets = [];
|
|
19
|
+
const pathMap = {};
|
|
20
|
+
// Collect flat files in .output/public/ (public dir assets)
|
|
21
|
+
try {
|
|
22
|
+
const entries = await fs.readdir(publicDir, {
|
|
23
|
+
withFileTypes: true
|
|
24
|
+
});
|
|
25
|
+
for (const entry of entries){
|
|
26
|
+
if (entry.isFile()) {
|
|
27
|
+
assets.push(entry.name);
|
|
28
|
+
pathMap[entry.name] = path.join('.output', 'public', entry.name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// No public directory
|
|
33
|
+
}
|
|
34
|
+
// Collect bundle assets from .output/public/_next/static/
|
|
35
|
+
const bundleDir = path.join(publicDir, '_next', 'static');
|
|
36
|
+
try {
|
|
37
|
+
const bundleFiles = await collectFilesRecursive(bundleDir, bundleDir);
|
|
38
|
+
for (const file of bundleFiles){
|
|
39
|
+
const key = `_next/static/${file}`;
|
|
40
|
+
assets.push(key);
|
|
41
|
+
pathMap[key] = path.join('.output', 'public', '_next', 'static', file);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// No bundle directory
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
pathMap,
|
|
48
|
+
routes: assets.map((k)=>`/${k}`),
|
|
49
|
+
uploadKeys: assets
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async collectPages(projectPath) {
|
|
53
|
+
const publicDir = path.join(projectPath, '.output', 'public');
|
|
54
|
+
const pages = await scanForPages(publicDir, publicDir);
|
|
55
|
+
const routes = pages.map((p)=>p === 'index' ? '/' : `/${p}`);
|
|
56
|
+
const uploadKeys = pages.map((p)=>p === 'index' ? 'index.html' : `${p}/index.html`);
|
|
57
|
+
const pathMap = {};
|
|
58
|
+
for(let i = 0; i < uploadKeys.length; i++){
|
|
59
|
+
pathMap[uploadKeys[i]] = path.join('.output', 'public', uploadKeys[i]);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
pathMap,
|
|
63
|
+
routes,
|
|
64
|
+
uploadKeys
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async prepareLambdaBundle(projectPath) {
|
|
68
|
+
const zipPath = path.join(projectPath, 'lambda.zip');
|
|
69
|
+
const serverDir = path.join(projectPath, '.output', 'server');
|
|
70
|
+
const publicDir = path.join(projectPath, '.output', 'public');
|
|
71
|
+
// Verify server directory exists
|
|
72
|
+
try {
|
|
73
|
+
await fs.stat(serverDir);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error('Nitro server build not found at .output/server. Run build first.');
|
|
76
|
+
}
|
|
77
|
+
// Write run.sh to a temp location
|
|
78
|
+
const runShPath = path.join(projectPath, '.output', 'run.sh');
|
|
79
|
+
await fs.writeFile(runShPath, RUN_SCRIPT, {
|
|
80
|
+
mode: 0o755
|
|
81
|
+
});
|
|
82
|
+
// Create zip: server/ + public/ + run.sh
|
|
83
|
+
await createZip(zipPath, [
|
|
84
|
+
{
|
|
85
|
+
prefix: 'server/',
|
|
86
|
+
source: serverDir
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
prefix: 'public/',
|
|
90
|
+
source: publicDir
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
prefix: '',
|
|
94
|
+
source: runShPath
|
|
95
|
+
}
|
|
96
|
+
]);
|
|
97
|
+
// Clean up temp run.sh
|
|
98
|
+
await fs.rm(runShPath, {
|
|
99
|
+
force: true
|
|
100
|
+
});
|
|
101
|
+
return zipPath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export const nitroAdapter = new NitroAdapter();
|
|
105
|
+
/**
|
|
106
|
+
* Recursively scan for pre-rendered pages (directories containing index.html).
|
|
107
|
+
* Skips known non-page directories (_next/, assets/).
|
|
108
|
+
*/ async function scanForPages(dir, baseDir) {
|
|
109
|
+
const pages = [];
|
|
110
|
+
// Check if this directory has an index.html (it's a page)
|
|
111
|
+
const indexPath = path.join(dir, 'index.html');
|
|
112
|
+
try {
|
|
113
|
+
await fs.stat(indexPath);
|
|
114
|
+
const relative = path.relative(baseDir, dir);
|
|
115
|
+
pages.push(relative || 'index'); // root dir -> 'index'
|
|
116
|
+
} catch {
|
|
117
|
+
// Not a page directory
|
|
118
|
+
}
|
|
119
|
+
// Recurse into subdirectories
|
|
120
|
+
try {
|
|
121
|
+
const entries = await fs.readdir(dir, {
|
|
122
|
+
withFileTypes: true
|
|
123
|
+
});
|
|
124
|
+
for (const entry of entries){
|
|
125
|
+
if (entry.isDirectory() && !NON_PAGE_DIRS.has(entry.name)) {
|
|
126
|
+
const subPages = await scanForPages(path.join(dir, entry.name), baseDir);
|
|
127
|
+
pages.push(...subPages);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Directory not readable
|
|
132
|
+
}
|
|
133
|
+
return pages;
|
|
134
|
+
}
|
|
135
|
+
async function createZip(zipPath, entries) {
|
|
136
|
+
return new Promise((resolve, reject)=>{
|
|
137
|
+
const output = createWriteStream(zipPath);
|
|
138
|
+
const archive = archiver('zip', {
|
|
139
|
+
zlib: {
|
|
140
|
+
level: 9
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
output.on('close', ()=>resolve());
|
|
144
|
+
archive.on('error', (err)=>reject(err));
|
|
145
|
+
archive.pipe(output);
|
|
146
|
+
for (const entry of entries){
|
|
147
|
+
try {
|
|
148
|
+
const stat = statSync(entry.source);
|
|
149
|
+
if (stat.isDirectory()) {
|
|
150
|
+
archive.directory(entry.source, entry.prefix.replace(/\/$/, '') || false);
|
|
151
|
+
} else {
|
|
152
|
+
archive.file(entry.source, {
|
|
153
|
+
name: entry.prefix + path.basename(entry.source)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Skip missing entries
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
void archive.finalize();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=nitro.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AssetCollection, DeployAdapter, PageCollection } from '../deploy-adapter.js';
|
|
2
|
+
export declare class ViteAdapter implements DeployAdapter {
|
|
3
|
+
fallback: string;
|
|
4
|
+
name: "vite";
|
|
5
|
+
collectAssets(projectPath: string): Promise<AssetCollection>;
|
|
6
|
+
collectPages(): Promise<PageCollection>;
|
|
7
|
+
prepareLambdaBundle(): Promise<null>;
|
|
8
|
+
}
|
|
9
|
+
export declare const viteAdapter: ViteAdapter;
|
|
10
|
+
//# sourceMappingURL=vite.d.ts.map
|