@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.
Files changed (49) hide show
  1. package/dist/api/control-plane.d.ts +8 -7
  2. package/dist/api/control-plane.js +16 -23
  3. package/dist/api/figma-api.d.ts +3 -2
  4. package/dist/api/figma-api.js +6 -9
  5. package/dist/auth/credentials.d.ts +21 -0
  6. package/dist/auth/credentials.js +21 -0
  7. package/dist/auth/oauth-flow.d.ts +13 -4
  8. package/dist/auth/oauth-flow.js +34 -8
  9. package/dist/auth/project-token.js +5 -5
  10. package/dist/cli.js +5 -0
  11. package/dist/commands/deploy.d.ts +3 -0
  12. package/dist/commands/deploy.js +67 -37
  13. package/dist/commands/env.js +4 -4
  14. package/dist/commands/init.js +10 -25
  15. package/dist/commands/login.js +3 -3
  16. package/dist/commands/upgrade.js +11 -11
  17. package/dist/db-content-api/index.js +48 -11
  18. package/dist/db-content-api/temp-utilities/sorting.d.ts +1 -1
  19. package/dist/db-content-api/temp-utilities/sorting.js +4 -1
  20. package/dist/db-content-api/temp-utilities/unwrapDocument.d.ts +7 -2
  21. package/dist/db-content-api/temp-utilities/unwrapDocument.js +11 -5
  22. package/dist/db-content-api/utilities/data/castFieldValue.js +11 -12
  23. package/dist/db-content-api/utilities/data/index.d.ts +1 -1
  24. package/dist/db-content-api/utilities/data/index.js +45 -20
  25. package/dist/db-content-api/utilities/joins.js +10 -4
  26. package/dist/db-content-api/utilities/meta/buildLocalizedPaths.js +14 -6
  27. package/dist/db-content-api/utilities/meta/buildPathTypes.js +19 -2
  28. package/dist/db-content-api/utilities/where.js +15 -45
  29. package/dist/oauth/endpoints/getLoginEndpoint.js +10 -2
  30. package/dist/oauth/utilities/refreshTokens.js +4 -1
  31. package/dist/plugin/build-config.js +4 -4
  32. package/dist/utils/adapters/nextjs.d.ts +9 -0
  33. package/dist/utils/adapters/nextjs.js +59 -0
  34. package/dist/utils/adapters/nitro.d.ts +9 -0
  35. package/dist/utils/adapters/nitro.js +164 -0
  36. package/dist/utils/adapters/vite.d.ts +10 -0
  37. package/dist/utils/adapters/vite.js +32 -0
  38. package/dist/utils/asset-collection.d.ts +24 -0
  39. package/dist/utils/asset-collection.js +53 -0
  40. package/dist/utils/build-detection.d.ts +5 -11
  41. package/dist/utils/build-detection.js +74 -11
  42. package/dist/utils/deploy-adapter.d.ts +38 -0
  43. package/dist/utils/deploy-adapter.js +58 -0
  44. package/dist/utils/download-template.js +3 -2
  45. package/dist/utils/fs-utils.d.ts +6 -0
  46. package/dist/utils/fs-utils.js +27 -0
  47. package/dist/utils/s3-upload.d.ts +7 -1
  48. package/dist/utils/s3-upload.js +4 -3
  49. 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
- // Transform existing values
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
- // Step 2: Apply Content API specific transformations
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
- // Normalize undefined null for optional fields
151
- // Content API may omit fields (undefined) but Payload expects null for optional 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
- // Unlike Drizzle (which reconstructs arrays from join tables), we store raw JSON,
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 fields: JSON string -> parsed object
170
- // Content API may store localized fields as JSON strings like "{\"en\":\"value\"}"
171
- // so we need to parse them back to objects.
172
- // Note: We return the full locale object - Payload Core handles flattening to requested locale.
173
- // When requesting a specific locale (not 'all'), values are simple strings that won't parse.
174
- // In that case, we silently keep the original value.
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 (this is expected for single-locale requests)
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 && joinQuery.limit) {
109
- offset = (joinQuery.page - 1) * joinQuery.limit;
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
- // Only field types that have names can support localization
31
- // See: https://payloadcms.com/docs/configuration/localization
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
- }))// TODO: A better solution would be to make the `where` schema of `zod` in `content-api`
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 // Keep path/operator/value conditions
28
- ;
22
+ return true;
29
23
  });
30
- // If filtering resulted in no conditions, return empty and
31
- if (nestedConditions.length === 0) {
32
- return {
33
- and: []
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
- // If inside a logical operator (and/or), return conditions directly
124
- // Otherwise, wrap in 'and' to match WhereClause type
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
- figmaUserInfo = await getUserInfo(access_token);
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 project_token = await getProjectToken(access_token, config.custom.figma.contentSystemId);
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(refreshToken, payload.config.custom.figma.contentSystemId);
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 { getValidAccessToken } from '../auth/oauth-flow.js';
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 accessToken = await getValidAccessToken(store);
54
- if (!accessToken) {
53
+ const credential = await getValidCredential(store);
54
+ if (!credential) {
55
55
  return null;
56
56
  }
57
- const bootstrapInfo = await getBootstrapInfo(accessToken, projectId);
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