Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion platform/src/components/aws/static-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,53 @@ export class StaticSite extends Component implements Link.Linkable {

// Upload files based on fileOptions
const filesProcessed: string[] = [];
for (const fileOption of fileOptions.reverse()) {
const reversedFileOptions = [...fileOptions].reverse();
const htmlFileOptions: typeof reversedFileOptions = [];
const nonHtmlFileOptions: typeof reversedFileOptions = [];

/**
* StaticSite historically replays `fileOptions` in reverse order, so
* the *last* entry in config becomes the *first* upload pass. With
* the default options that put HTML last, the runtime actually pushes
* `index.html` to S3 *before* its hashed JS/CSS siblings:
*
* ```text
* deploy start
* ├─▶ (reverse) upload index.html
* │ └─▶ CloudFront serves fresh HTML referencing app.[hash].js
* └───── lag ──▶ upload app.[hash].js (still missing in S3)
* ↓
* viewer request → S3 403 → white screen
* ```
*
* To close that race we bucket options by whether they touch HTML,
* process non-HTML first, then HTML. Behavioural invariants remain:
*
* ```text
* configure: [ ...non-html..., ...html... ]
* reverse ▶ [ html-group, non-html-group ]
* reorder ▶ [ non-html-group, html-group ]
* result ▶ assets uploaded → index.html uploaded → no 403 gap
* ```
*/
for (const option of reversedFileOptions) {
const patterns = Array.isArray(option.files)
? option.files
: [option.files];
const targetsHtml = patterns.some((pattern) =>
pattern.includes(".html"),
);

if (targetsHtml) {
htmlFileOptions.push(option);
} else {
nonHtmlFileOptions.push(option);
}
}

const uploadOrder = [...nonHtmlFileOptions, ...htmlFileOptions];

for (const fileOption of uploadOrder) {
const files = globSync(fileOption.files, {
cwd: path.resolve(outputPath),
nodir: true,
Expand Down