@payloadcms/figma 0.1.0-alpha.7 → 0.1.0-alpha.9

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.
@@ -391,7 +391,7 @@ async function maybeWriteAgentConfigFiles(args) {
391
391
  log.error(error instanceof Error ? error.message : 'Unknown error');
392
392
  process.exit(1);
393
393
  }
394
- // Apply Lambda modifications (run.sh, standalone output, lambda:buildzip script)
394
+ // Apply Lambda modifications (run.sh, Next.js config, lambda:buildzip script)
395
395
  await applyLambdaModifications(process.cwd(), packageManager);
396
396
  // Ensure .gitignore has required entries
397
397
  await ensureGitignore(process.cwd(), [
@@ -0,0 +1,6 @@
1
+ export type FigmaNextImageLoaderProps = {
2
+ quality?: number;
3
+ src: string;
4
+ width: number;
5
+ };
6
+ export declare function figmaNextImageLoader({ quality, src, width }: FigmaNextImageLoaderProps): string;
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ const PAYLOAD_FILE_PATH_PATTERN = /^\/(?!\/)(?:.+\/)?api\/[^/]+\/file\/.+$/;
3
+ export function figmaNextImageLoader({ quality, src, width }) {
4
+ const [pathname, search = ''] = src.split('?');
5
+ if (!PAYLOAD_FILE_PATH_PATTERN.test(pathname)) {
6
+ return src;
7
+ }
8
+ const searchParams = new URLSearchParams(search);
9
+ searchParams.set('w', String(width));
10
+ if (quality !== undefined) {
11
+ searchParams.set('q', String(quality));
12
+ }
13
+ return `${pathname}?${searchParams}`;
14
+ }
@@ -6,6 +6,17 @@ import { getSearchParam } from '../../utils/getSearchParam.js';
6
6
  import { resolveParentTargetOrigin, setupIframeAutoLogin } from './iframeAutoLogin.js';
7
7
  import './index.scss';
8
8
  const baseClass = 'oauth-login';
9
+ const COLLECTION_SKELETON_KEYS = [
10
+ 'collection-1',
11
+ 'collection-2',
12
+ 'collection-3',
13
+ 'collection-4'
14
+ ];
15
+ const GLOBAL_SKELETON_KEYS = [
16
+ 'global-1',
17
+ 'global-2',
18
+ 'global-3'
19
+ ];
9
20
  // Max time we wait for the parent (figma.com) to respond with a code after
10
21
  // posting `mint-cms-oauth-code`. If we hit this without a reply, we assume
11
22
  // the parent isn't going to respond and fall back to the manual login button.
@@ -49,6 +60,57 @@ const FigmaIcon = ()=>/*#__PURE__*/ _jsx("svg", {
49
60
  fillRule: "evenodd"
50
61
  })
51
62
  });
63
+ const PayloadAdminLoadingShimmer = ()=>/*#__PURE__*/ _jsx("div", {
64
+ "aria-label": "Signing in",
65
+ "aria-live": "polite",
66
+ className: `${baseClass}__admin-shimmer`,
67
+ role: "status",
68
+ children: /*#__PURE__*/ _jsxs("div", {
69
+ "aria-hidden": "true",
70
+ className: `${baseClass}__admin-shimmer-main`,
71
+ children: [
72
+ /*#__PURE__*/ _jsxs("div", {
73
+ className: `${baseClass}__shimmer-heading-row`,
74
+ children: [
75
+ /*#__PURE__*/ _jsx("div", {
76
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-heading-icon`
77
+ }),
78
+ /*#__PURE__*/ _jsx("div", {
79
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-heading-label`
80
+ })
81
+ ]
82
+ }),
83
+ /*#__PURE__*/ _jsxs("div", {
84
+ className: `${baseClass}__shimmer-section`,
85
+ children: [
86
+ /*#__PURE__*/ _jsx("div", {
87
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-section-label`
88
+ }),
89
+ /*#__PURE__*/ _jsx("div", {
90
+ className: `${baseClass}__shimmer-card-grid`,
91
+ children: COLLECTION_SKELETON_KEYS.map((key)=>/*#__PURE__*/ _jsx("div", {
92
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-card`
93
+ }, key))
94
+ })
95
+ ]
96
+ }),
97
+ /*#__PURE__*/ _jsxs("div", {
98
+ className: `${baseClass}__shimmer-section`,
99
+ children: [
100
+ /*#__PURE__*/ _jsx("div", {
101
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-section-label`
102
+ }),
103
+ /*#__PURE__*/ _jsx("div", {
104
+ className: `${baseClass}__shimmer-card-grid`,
105
+ children: GLOBAL_SKELETON_KEYS.map((key)=>/*#__PURE__*/ _jsx("div", {
106
+ className: `${baseClass}__shimmer-block ${baseClass}__shimmer-card`
107
+ }, key))
108
+ })
109
+ ]
110
+ })
111
+ ]
112
+ })
113
+ });
52
114
  export const DefaultLoginButton = ({ disabled, endpointSlug })=>{
53
115
  const { config: { admin: { user: userSlug }, routes: { api }, serverURL } } = useConfig();
54
116
  const [authorizeURL, setAuthorizeURL] = useState('');
@@ -150,26 +212,11 @@ export const DefaultLoginButton = ({ disabled, endpointSlug })=>{
150
212
  endpointSlug
151
213
  ]);
152
214
  if (!shouldRenderButton) {
153
- // In the iframe auto-login window, show a "Signing in…" hint so the
154
- // user sees something is happening instead of a blank slot until the
155
- // flow either navigates them away or times out into the manual button.
215
+ // In the iframe auto-login window, show a Payload admin-shaped shimmer
216
+ // until the flow either navigates them away or times out into the manual
217
+ // button.
156
218
  if (isInIframe === true && !disabled) {
157
- return /*#__PURE__*/ _jsx("div", {
158
- className: baseClass,
159
- children: /*#__PURE__*/ _jsxs("div", {
160
- className: `${baseClass}__signing-in`,
161
- role: "status",
162
- children: [
163
- /*#__PURE__*/ _jsx("span", {
164
- children: "Signing in"
165
- }),
166
- /*#__PURE__*/ _jsx("div", {
167
- "aria-hidden": "true",
168
- className: `${baseClass}__spinner`
169
- })
170
- ]
171
- })
172
- });
219
+ return /*#__PURE__*/ _jsx(PayloadAdminLoadingShimmer, {});
173
220
  }
174
221
  return null;
175
222
  }
@@ -1,3 +1,10 @@
1
+ .oauth-login,
2
+ .oauth-login__admin-shimmer {
3
+ --oauth-login-shimmer-bg: var(--theme-bg, var(--theme-elevation-0, #ffffff));
4
+ --oauth-login-shimmer-block: var(--theme-elevation-100, #efefef);
5
+ --oauth-login-shimmer-highlight: var(--theme-elevation-200, #f7f7f7);
6
+ }
7
+
1
8
  .oauth-login {
2
9
  display: flex;
3
10
  justify-content: center;
@@ -55,28 +62,127 @@
55
62
  Oxygen, Ubuntu, Cantarell, sans-serif;
56
63
  }
57
64
 
58
- &__signing-in {
59
- display: flex;
60
- flex-direction: column;
61
- align-items: center;
62
- gap: 0.75rem;
63
- font-size: 1rem;
65
+ &__admin-shimmer {
66
+ position: fixed;
67
+ inset: 0;
68
+ z-index: 1000;
69
+ overflow: hidden;
70
+ background: var(--oauth-login-shimmer-bg);
64
71
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
65
72
  Oxygen, Ubuntu, Cantarell, sans-serif;
66
73
  }
67
74
 
68
- &__spinner {
69
- width: 24px;
70
- height: 24px;
71
- border: 2px solid var(--theme-elevation-200, #e5e5e5);
72
- border-top-color: currentColor;
73
- border-radius: 50%;
74
- animation: oauth-login-spin 0.8s linear infinite;
75
+ &__admin-shimmer-main {
76
+ padding: 2rem 2.5rem;
77
+ }
78
+
79
+ &__shimmer-block {
80
+ position: relative;
81
+ overflow: hidden;
82
+ border-radius: 0.25rem;
83
+ background-color: var(--oauth-login-shimmer-block);
84
+ }
85
+
86
+ &__shimmer-block::after {
87
+ content: '';
88
+ position: absolute;
89
+ inset: 0;
90
+ border-radius: inherit;
91
+ background-image: linear-gradient(
92
+ 100deg,
93
+ transparent 0%,
94
+ transparent 35%,
95
+ var(--oauth-login-shimmer-highlight) 50%,
96
+ transparent 65%,
97
+ transparent 100%
98
+ );
99
+ pointer-events: none;
100
+ transform: translateX(-100%);
101
+ animation: oauth-login-shimmer 1.45s linear infinite;
102
+ }
103
+
104
+ &__shimmer-heading-row {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 0.5rem;
108
+ height: 1.75rem;
109
+ margin-bottom: 1rem;
110
+ }
111
+
112
+ &__shimmer-heading-icon {
113
+ width: 1.5rem;
114
+ height: 1.5rem;
115
+ }
116
+
117
+ &__shimmer-heading-label {
118
+ width: 7.5rem;
119
+ height: 1.25rem;
120
+ }
121
+
122
+ &__shimmer-section {
123
+ margin-bottom: 1.5rem;
124
+ }
125
+
126
+ &__shimmer-section-label {
127
+ width: 7.5rem;
128
+ height: 1.25rem;
129
+ margin-bottom: 0.5rem;
130
+ }
131
+
132
+ &__shimmer-card-grid {
133
+ display: grid;
134
+ grid-template-columns: repeat(4, minmax(0, 1fr));
135
+ gap: 1rem;
136
+ max-width: 51.625rem;
137
+ }
138
+
139
+ &__shimmer-card {
140
+ height: 4rem;
141
+ }
142
+ }
143
+
144
+ @media (prefers-color-scheme: dark) {
145
+ .oauth-login,
146
+ .oauth-login__admin-shimmer {
147
+ --oauth-login-shimmer-bg: var(--theme-bg, var(--theme-elevation-0, #2b2b2b));
148
+ --oauth-login-shimmer-block: var(--theme-elevation-100, #383838);
149
+ --oauth-login-shimmer-highlight: var(--theme-elevation-200, #444444);
150
+ }
151
+ }
152
+
153
+ @media (max-width: 48rem) {
154
+ .oauth-login {
155
+ &__shimmer-card-grid {
156
+ grid-template-columns: repeat(2, minmax(0, 1fr));
157
+ }
75
158
  }
76
159
  }
77
160
 
78
- @keyframes oauth-login-spin {
161
+ @media (max-width: 30rem) {
162
+ .oauth-login {
163
+ &__admin-shimmer-main {
164
+ padding-right: 0.75rem;
165
+ padding-left: 0.75rem;
166
+ }
167
+
168
+ &__shimmer-card-grid {
169
+ grid-template-columns: 1fr;
170
+ }
171
+ }
172
+ }
173
+
174
+ @media (prefers-reduced-motion: reduce) {
175
+ .oauth-login__shimmer-block::after {
176
+ animation: none;
177
+ }
178
+ }
179
+
180
+ @keyframes oauth-login-shimmer {
181
+ from {
182
+ transform: translateX(-100%);
183
+ }
184
+
79
185
  to {
80
- transform: rotate(360deg);
186
+ transform: translateX(100%);
81
187
  }
82
188
  }
@@ -20,6 +20,7 @@ import { getEnvVarSync } from '../utils/env-management.js';
20
20
  import * as log from '../utils/log.js';
21
21
  import { logMissingCliAuth } from './auth-preflight.js';
22
22
  import { logMissingContentSystemId } from './bootstrap-preflight.js';
23
+ import { createCacheRevalidationHooksPlugin } from './cache-revalidation-hooks.js';
23
24
  import { LIMIT_REACHED_VIEW_KEY, LIMIT_REACHED_VIEW_PATH } from './cloud-limits/routes.js';
24
25
  import { getDevCookieNames } from './dev-cookie-names.js';
25
26
  import { passesMakeViewerWriteGate, withMakeViewerWriteBaseAccessDefaults } from './make-permission-access/make-permission-access.js';
@@ -427,7 +428,10 @@ export async function buildFigmaConfig(config) {
427
428
  // figma.storage is false and would break remote uploads for those apps.
428
429
  createSandboxUploadFetchPlugin({
429
430
  userCollections: config.collections
430
- })
431
+ }),
432
+ // Keep last so hooks added by app plugins also get the missing Next cache
433
+ // context guard used by Payload CLI operations in the Make sandbox.
434
+ createCacheRevalidationHooksPlugin()
431
435
  ]
432
436
  };
433
437
  // Inject Figma-managed identity fields only into the Payload Admin collection.
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from 'payload';
2
+ /**
3
+ * Prevent Next cache invalidation from turning a successful Payload mutation
4
+ * into an apparent failure when the mutation runs outside a Next request.
5
+ */
6
+ export declare function createCacheRevalidationHooksPlugin(): Plugin;
@@ -0,0 +1,69 @@
1
+ const missingNextCacheContext = /^Invariant: static generation store missing in (?:revalidateTag|revalidatePath|updateTag)\b/;
2
+ const unsupportedUpdateTagContext = /^updateTag can only be called from within a Server Action\./;
3
+ /**
4
+ * Next cache invalidation requires request-scoped state. Payload's Local API has
5
+ * no Next request when it runs from the CLI in a Make sandbox, so Next throws
6
+ * after the database write has already committed. Treat only that missing-
7
+ * context error as a no-op; cache invalidation still runs normally in a Next
8
+ * request, and all other hook failures retain their existing behavior.
9
+ */ function isUnsupportedNextCacheContextError(error) {
10
+ if (!(error instanceof Error)) {
11
+ return false;
12
+ }
13
+ const errorCode = error.__NEXT_ERROR_CODE;
14
+ if (missingNextCacheContext.test(error.message)) {
15
+ return errorCode === undefined || errorCode === 'E263';
16
+ }
17
+ if (unsupportedUpdateTagContext.test(error.message)) {
18
+ return errorCode === undefined || errorCode === 'E872';
19
+ }
20
+ return false;
21
+ }
22
+ function reportIgnoredRevalidation(args, hookName) {
23
+ const hookArgs = args[0];
24
+ hookArgs?.req?.payload?.logger?.warn({
25
+ hook: hookName,
26
+ msg: 'Ignored Next cache invalidation outside a supported request context'
27
+ });
28
+ }
29
+ function wrapHook(hook, hookName) {
30
+ return async (...args)=>{
31
+ try {
32
+ return await hook(...args);
33
+ } catch (error) {
34
+ if (isUnsupportedNextCacheContextError(error)) {
35
+ reportIgnoredRevalidation(args, hookName);
36
+ return undefined;
37
+ }
38
+ throw error;
39
+ }
40
+ };
41
+ }
42
+ function wrapHooks(hooks) {
43
+ if (!hooks) {
44
+ return hooks;
45
+ }
46
+ return Object.fromEntries(Object.entries(hooks).map(([name, value])=>[
47
+ name,
48
+ Array.isArray(value) ? value.map((hook)=>typeof hook === 'function' ? wrapHook(hook, name) : hook) : value
49
+ ]));
50
+ }
51
+ /**
52
+ * Prevent Next cache invalidation from turning a successful Payload mutation
53
+ * into an apparent failure when the mutation runs outside a Next request.
54
+ */ export function createCacheRevalidationHooksPlugin() {
55
+ const plugin = (config)=>({
56
+ ...config,
57
+ collections: config.collections?.map((collection)=>({
58
+ ...collection,
59
+ hooks: wrapHooks(collection.hooks)
60
+ })),
61
+ globals: config.globals?.map((global)=>({
62
+ ...global,
63
+ hooks: wrapHooks(global.hooks)
64
+ }))
65
+ });
66
+ // Run after tenant plugins so hooks they add receive the same guard.
67
+ plugin.order = Number.POSITIVE_INFINITY;
68
+ return plugin;
69
+ }
@@ -4,18 +4,19 @@ import { collectSSGAssets, collectStaticAssets, collectStaticMetadataAssets } fr
4
4
  import { buildLambdaZip } from '../build-lambda-zip.js';
5
5
  import { resolveOutputPath } from '../deploy-output/resolveOutputPath.js';
6
6
  import { collectFilesRecursive } from '../fs-utils.js';
7
+ import { isPublishableAsset } from '../secret-files.js';
7
8
  export class NextjsAdapter {
8
9
  name = 'nextjs';
9
10
  async collectAssets(projectPath, output) {
10
11
  // Bundle assets: .next/static/ → _next/static/
11
- const staticAssets = await collectStaticAssets(projectPath, output);
12
+ const staticAssets = (await collectStaticAssets(projectPath, output)).filter(isPublishableAsset);
12
13
  const outputPath = resolveOutputPath(projectPath, 'nextjs', output);
13
14
  // Public assets: public/ directory
14
15
  const publicDir = path.join(projectPath, 'public');
15
16
  let publicAssets = [];
16
17
  try {
17
18
  await fs.stat(publicDir);
18
- publicAssets = await collectFilesRecursive(publicDir, publicDir);
19
+ publicAssets = (await collectFilesRecursive(publicDir, publicDir)).filter(isPublishableAsset);
19
20
  } catch {
20
21
  // No public directory
21
22
  }
@@ -27,7 +28,7 @@ export class NextjsAdapter {
27
28
  for (const key of publicAssets){
28
29
  pathMap[key] = path.join('public', key);
29
30
  }
30
- for (const key of metadataAssets.keys){
31
+ for (const key of metadataAssets.keys.filter(isPublishableAsset)){
31
32
  pathMap[key] = metadataAssets.pathMap[key];
32
33
  }
33
34
  const uploadKeys = Object.keys(pathMap);
@@ -1,10 +1,11 @@
1
1
  import archiver from 'archiver';
2
- import { createWriteStream, statSync } from 'fs';
2
+ import { createWriteStream } from 'fs';
3
3
  import fs from 'fs/promises';
4
4
  import path from 'path';
5
5
  import { resolveNitroOutputLayout } from '../deploy-output/resolveNitroOutputLayout.js';
6
6
  import { resolveOutputPath } from '../deploy-output/resolveOutputPath.js';
7
- import { collectFilesRecursive, getDirectorySize } from '../fs-utils.js';
7
+ import { collectFilesRecursive, EscapingSymlinkError, walkBundleFiles } from '../fs-utils.js';
8
+ import { isBundlableFile, isPublishableAsset } from '../secret-files.js';
8
9
  /** Framework asset directories that are not pre-rendered pages. */ const NON_PAGE_DIRS = new Set([
9
10
  '_build',
10
11
  '_nuxt',
@@ -15,7 +16,7 @@ export class NitroAdapter {
15
16
  async collectAssets(projectPath, output) {
16
17
  const { publicPath } = await getLayout(projectPath, output);
17
18
  const pageKeys = new Set((await scanForPages(publicPath, publicPath)).map(toPageUploadKey));
18
- const assets = (await collectFilesRecursive(publicPath, publicPath)).filter((file)=>!pageKeys.has(file));
19
+ const assets = (await collectFilesRecursive(publicPath, publicPath)).filter((file)=>!pageKeys.has(file) && isPublishableAsset(file));
19
20
  const pathMap = Object.fromEntries(assets.map((file)=>[
20
21
  file,
21
22
  path.relative(projectPath, path.join(publicPath, file))
@@ -28,7 +29,7 @@ export class NitroAdapter {
28
29
  }
29
30
  async collectPages(projectPath, output) {
30
31
  const { publicPath } = await getLayout(projectPath, output);
31
- const pages = await scanForPages(publicPath, publicPath);
32
+ const pages = (await scanForPages(publicPath, publicPath)).filter((page)=>isPublishableAsset(toPageUploadKey(page)));
32
33
  const routes = pages.map((p)=>p === 'index' ? '/' : `/${p}`);
33
34
  const uploadKeys = pages.map(toPageUploadKey);
34
35
  const pathMap = {};
@@ -57,8 +58,8 @@ export class NitroAdapter {
57
58
  throw new Error(`Nitro server build not found at ${layout.outputPath}`);
58
59
  }
59
60
  const serverDir = path.dirname(layout.serverEntryPath);
60
- const serverPrefix = path.relative(layout.outputPath, serverDir).split(path.sep).join('/');
61
- const publicPrefix = path.relative(layout.outputPath, layout.publicPath).split(path.sep).join('/');
61
+ const serverPrefix = zipPrefix(path.relative(layout.outputPath, serverDir).split(path.sep).join('/'));
62
+ const publicPrefix = zipPrefix(path.relative(layout.outputPath, layout.publicPath).split(path.sep).join('/'));
62
63
  const serverEntry = path.relative(layout.outputPath, layout.serverEntryPath).split(path.sep).join('/');
63
64
  const runShPath = path.join(layout.outputPath, 'run.sh');
64
65
  await fs.writeFile(runShPath, createRunScript(serverEntry), {
@@ -66,22 +67,33 @@ export class NitroAdapter {
66
67
  });
67
68
  const zipPath = path.join(projectPath, 'lambda.zip');
68
69
  try {
70
+ const runShStat = await fs.stat(runShPath);
71
+ const [allServerFiles, publicFiles] = await Promise.all([
72
+ walkBundleFiles(serverDir),
73
+ walkPublicBundleFiles(layout.publicPath)
74
+ ]);
75
+ const bundlableServerFiles = allServerFiles.filter((file)=>isBundlableFile(file.relativePath));
76
+ const unzippedBytes = bundlableServerFiles.reduce((total, file)=>total + file.size, 0) + publicFiles.reduce((total, file)=>total + file.size, 0) + runShStat.size;
69
77
  await createZip(zipPath, [
70
78
  {
71
- prefix: `${serverPrefix}/`,
72
- source: serverDir
79
+ files: bundlableServerFiles,
80
+ prefix: serverPrefix
73
81
  },
74
82
  {
75
- prefix: `${publicPrefix}/`,
76
- source: layout.publicPath
83
+ files: publicFiles,
84
+ prefix: publicPrefix
77
85
  },
78
86
  {
79
- prefix: '',
80
- source: runShPath
87
+ files: [
88
+ {
89
+ relativePath: 'run.sh',
90
+ size: runShStat.size,
91
+ sourcePath: runShPath
92
+ }
93
+ ],
94
+ prefix: ''
81
95
  }
82
96
  ]);
83
- const runShStat = await fs.stat(runShPath);
84
- const unzippedBytes = await getDirectorySize(serverDir) + await getDirectorySize(layout.publicPath) + runShStat.size;
85
97
  return {
86
98
  unzippedBytes,
87
99
  zipPath
@@ -135,9 +147,25 @@ NODE_ENV=production exec node '${escapedEntry}'
135
147
  }
136
148
  return pages;
137
149
  }
150
+ function zipPrefix(relativeDir) {
151
+ return relativeDir ? `${relativeDir}/` : '';
152
+ }
138
153
  function toPageUploadKey(page) {
139
154
  return page === 'index' ? 'index.html' : `${page}/index.html`;
140
155
  }
156
+ async function walkPublicBundleFiles(publicPath) {
157
+ try {
158
+ // The Nitro server serves this tree statically, so secrets copied into the
159
+ // public directory must not reach the Lambda bundle either.
160
+ const files = await walkBundleFiles(publicPath);
161
+ return files.filter((file)=>isPublishableAsset(file.relativePath));
162
+ } catch (error) {
163
+ if (error instanceof EscapingSymlinkError) {
164
+ throw error;
165
+ }
166
+ return [];
167
+ }
168
+ }
141
169
  async function createZip(zipPath, entries) {
142
170
  return new Promise((resolve, reject)=>{
143
171
  const output = createWriteStream(zipPath);
@@ -150,17 +178,10 @@ async function createZip(zipPath, entries) {
150
178
  archive.on('error', (err)=>reject(err));
151
179
  archive.pipe(output);
152
180
  for (const entry of entries){
153
- try {
154
- const stat = statSync(entry.source);
155
- if (stat.isDirectory()) {
156
- archive.directory(entry.source, entry.prefix.replace(/\/$/, '') || false);
157
- } else {
158
- archive.file(entry.source, {
159
- name: entry.prefix + path.basename(entry.source)
160
- });
161
- }
162
- } catch {
163
- // Skip missing entries
181
+ for (const file of entry.files){
182
+ archive.file(file.sourcePath, {
183
+ name: entry.prefix + file.relativePath
184
+ });
164
185
  }
165
186
  }
166
187
  void archive.finalize();
@@ -1,12 +1,13 @@
1
1
  import path from 'path';
2
2
  import { resolveOutputPath } from '../deploy-output/resolveOutputPath.js';
3
3
  import { collectFilesRecursive } from '../fs-utils.js';
4
+ import { isPublishableAsset } from '../secret-files.js';
4
5
  export class ViteAdapter {
5
6
  fallback = '/index.html';
6
7
  name = 'vite';
7
8
  async collectAssets(projectPath, output) {
8
9
  const outputPath = resolveOutputPath(projectPath, 'vite', output);
9
- const files = await collectFilesRecursive(outputPath, outputPath);
10
+ const files = (await collectFilesRecursive(outputPath, outputPath)).filter(isPublishableAsset);
10
11
  const pathMap = Object.fromEntries(files.map((file)=>[
11
12
  file,
12
13
  path.relative(projectPath, path.join(outputPath, file))
@@ -30,7 +30,7 @@ type StaticMetadataAssets = Omit<SSGAssets, 'routeAssetMap'>;
30
30
  /**
31
31
  * Collect SSG asset paths by parsing .next/prerender-manifest.json
32
32
  *
33
- * For each page route in the manifest's `routes` object, collects:
33
+ * For each static page route without revalidation in the manifest's `routes` object, collects:
34
34
  * - {route}.html (full HTML)
35
35
  * - {route}.rsc (React Server Components payload)
36
36
  * - {route}.meta (response headers JSON)
@@ -77,7 +77,7 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
77
77
  /**
78
78
  * Collect SSG asset paths by parsing .next/prerender-manifest.json
79
79
  *
80
- * For each page route in the manifest's `routes` object, collects:
80
+ * For each static page route without revalidation in the manifest's `routes` object, collects:
81
81
  * - {route}.html (full HTML)
82
82
  * - {route}.rsc (React Server Components payload)
83
83
  * - {route}.meta (response headers JSON)
@@ -112,11 +112,6 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
112
112
  const keys = [];
113
113
  const pathMap = {};
114
114
  const routeAssetMap = {};
115
- const extensions = [
116
- '.html',
117
- '.rsc',
118
- '.meta'
119
- ];
120
115
  const appDir = path.join(outputPath, 'server', 'app');
121
116
  for (const [routeKey, route] of Object.entries(manifest.routes)){
122
117
  // Current manifests classify app pages explicitly. Older App Router manifests
@@ -125,9 +120,34 @@ const MAX_ASSET_SIZE = 50 * 1024 * 1024 // 50MB
125
120
  if (!isPageRoute) {
126
121
  continue;
127
122
  }
123
+ // ISR prerenders are seeds for the Next.js runtime, not permanent S3 pages.
124
+ // Keep legacy manifests without this field; only false explicitly disables revalidation.
125
+ if (route.initialRevalidateSeconds !== undefined && route.initialRevalidateSeconds !== false) {
126
+ continue;
127
+ }
128
+ // SSR and PPR can also disable revalidation. Only static computation can be
129
+ // served from S3; an initial response is safe for a static CSR shell.
130
+ const hasStaticRendering = route.compute === 'static' && (route.response === 'complete' || route.response === 'initial');
131
+ // Older SSG entries omit these fields. Ambiguous PPR entries must use Lambda.
132
+ const hasLegacyStaticRendering = route.compute === undefined && route.response === undefined && (route.renderingMode === undefined || route.renderingMode === 'STATIC');
133
+ if (!hasStaticRendering && !hasLegacyStaticRendering) {
134
+ continue;
135
+ }
128
136
  // Strip leading slash for S3 key; handle root "/" → "index"
129
137
  const stripped = routeKey === '/' ? 'index' : routeKey.replace(/^\//, '');
130
138
  const routeAssets = [];
139
+ // For partially static routes, dataRoute can name a runtime endpoint even
140
+ // when the build has no full RSC file. Next.js sets prefetchDataRoute only
141
+ // when it emits that static payload.
142
+ const hasFullRSC = route.renderingMode !== 'PARTIALLY_STATIC' || typeof route.prefetchDataRoute === 'string';
143
+ const extensions = hasFullRSC ? [
144
+ '.html',
145
+ '.rsc',
146
+ '.meta'
147
+ ] : [
148
+ '.html',
149
+ '.meta'
150
+ ];
131
151
  for (const ext of extensions){
132
152
  const s3Key = `${stripped}${ext}`;
133
153
  keys.push(s3Key);