Skip to content

fix(cli): cloud render finds media whose file name is stored in another Unicode form than the page's - #7419

Merged
miguel-heygen merged 3 commits into
mainfrom
dcloudpro/nfc-archive-keys
Nov 8, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
dcloudpro/nfc-archive-keys

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

What

The cloud archive keys a file by the spelling the file system lists, and the render server looks files up byte for byte. A file name with an accent can be stored two ways: composed (one code point for é) or decomposed (e plus a combining accent). macOS lists a file picked in Finder decomposed, while a page almost always writes the name composed. The preview and a local render accept either spelling; the archive did not, so the page's reference matched no file in the zip and the offload step found nothing to rewrite.

The archive now uses one spelling (composed, NFC) for every key it makes: the project files, the files pulled in from outside the project, the entry composition's path, and the lookups the media offload makes when it matches a page's reference to a staged file.

Why

A cloud render of a project with such file names can fail as a missing video source even though the same project renders locally. On Linux the existing check already stops earlier with "The composition references ..., which is not in the project folder".

Related work

None found. I searched the CLI for normalize( and found no Unicode normalization in the archive or offload code.

How

  • archiveKey() in utils/publishProject.ts is the one place the spelling is decided; stagePublishFiles, the external-asset localizer, the referenced-file include and the entry path in commands/cloud/render.ts all key through it. Reading a file still uses the name the disk lists.

  • cloud/offload.ts normalizes the key before each lookup in the staged file map, so a page that names the file decomposed also matches.

  • Two files that differ only in Unicode form now fail with a message naming both, instead of one silently replacing the other in the archive.

  • A page that names a file the way a decomposed disk lists it used to match its archive entry. Keys are now composed, so while staging, every non-composed path segment (and its URL-encoded form) is replaced by its composed spelling in the staged text files (html, css, js, json, svg, xml, txt). Page and key agree whichever spelling the page used.

Not proven: that every failing report involved decomposed names. The reports show names with accents and spaces, and this is the one cause I could reproduce. .hyperframesignore patterns are still matched against the spelling the disk lists.

Test plan

Fails without the fix:

  • publishProject.test.ts "keys a file stored decomposed by the composed name a page uses, so the renderer finds it"

  • publishProject.test.ts "refuses two files that differ only by Unicode form instead of letting one overwrite the other"

  • offload.test.ts "offloads a file stored decomposed when the page names it composed, and the reverse"

  • publishProject.test.ts "rewrites a page and a stylesheet that name a decomposed file the way the disk lists it" (a small file that stays in the zip)

  • Unit tests added/updated

  • Manual testing performed (not run on a Mac with a Finder-picked file)

  • Documentation updated (if applicable)

  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

Ran on Linux: publishProject.test.ts and offload.test.ts pass with the fix; the four tests above fail without it; typecheck is clean for the touched files. render.test.ts and render.reverseSession.test.ts fail the same tests with and without this change in my environment; the new entry-path key in render.ts has no test of its own.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes at 6d08ece9. There are two blockers.

1. A decomposed reference to a decomposed file used to work and now fails. Every archive key is now NFC, but the page text is rewritten only for files that get offloaded. Files below OFFLOAD_MIN_BYTES stay in the zip under the NFC key, while the page still names them decomposed. The renderer matches names byte for byte, so it misses them. Main keyed these files by the disk spelling, so a page whose name matched the disk worked.

I ran a probe with the file assets/vide\u0301o.png (decomposed) and a page containing <img src="assets/vide\u0301o.png">:

  • main: buildPublishFileMap keys are assets/vide%u0301o.png, index.html, so the page's reference is in the archive.
  • this head: the keys are assets/vid%E9o.png, index.html, so the page's reference is not in the archive.

This is likely the same population the PR targets. On macOS, a page whose names come from the disk listing (ls output pasted by an agent, a picker that inserts the listed name) is decomposed just like the file. The PR body calls the decomposed-page case unaddressed, but for files that stay in the zip it is a new break, not existing behavior. Two ways to fix it:

  • Keep the disk spelling as the key and add the NFC spelling as a second entry for the same StagedFile when the two differ. The clash check still applies, and heavy files are offloaded anyway.
  • Or NFC-normalize local references in HTML/CSS when staging, so page and key always agree.

Either way, please add a test for a decomposed page with a decomposed small file that stays in the zip. It fails on this head.

2. CI: Comments fails. The comment-share ratchet reports that packages/cli/src/utils/publishProject.ts went from 7.5% to 7.6% (2 comment lines added). The new archiveKey doc comment is what raised it.

Checked and OK

  • The 'Fails without the fix:' section names three tests, and I confirmed the test literals really differ (vid\xe9os vs vide\u0301os). publishProject.test.ts and offload.test.ts pass at this head (109 tests).
  • The clash check between two files that differ only by Unicode form is a good change, and its error names both files.
  • Offload lookups through archiveKey are consistent, and staged files are still read from the disk path, so Linux reads aren't affected.
  • The entry path in render.ts is normalized to match the keys.

);
}
diskPathOf.set(key, filePath);
fileContents.set(key, stageFile(join(absProjectDir, filePath), onDiskMinBytes));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keying only by NFC drops the decomposed spelling. A small file that stays in the zip and is named decomposed by its page (like the disk) used to match and no longer does. Consider adding the NFC key next to the disk key, or normalizing the references in the page.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still requesting changes at 704e6441.

The only change since 6d08ece9 deletes the two-line doc comment on archiveKey. That fixes the Comments ratchet (blocker 2). Blocker 1 from #7419 (review) is not addressed.

Every archive key is still NFC, and the page text is only rewritten for files that get offloaded. So a small file that stays in the zip, with a decomposed name on disk and the same decomposed name in the page, is still missing from what the renderer looks up. Main keyed it by the disk spelling and found it.

Either fix from that review closes it: key by both spellings, or NFC-normalize the page's local references while staging. Please add a test with a decomposed page and a decomposed small file that stays in the zip.

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 88 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Pushed 77bacbe. Blocker 1: a page that names a decomposed file the way the disk lists it now matches again. I took the second option: while staging, each non-composed path segment (and its URL-encoded form) is rewritten to its composed spelling in the staged text files, so page and key agree and no twin entry can keep an offloaded heavy file in the zip. New test (a decomposed page, stylesheet and small file that stays in the zip) fails with that call disabled and passes with it; publishProject and offload tests pass (110). Blocker 2 (Comments) was fixed at 704e644 and this push adds no comment lines.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 77bacbee. Both blockers are resolved.

  • Regression fixed. composeNamesInText rewrites decomposed path segments to NFC in every staged text file, plus their encodeURIComponent forms. A decomposed page or stylesheet that names a decomposed small file now matches the NFC key that stays in the zip. The new test "rewrites a page and a stylesheet that name a decomposed file the way the disk lists it" is listed under 'Fails without the fix:'. I removed the composeNamesInText(...) call and that test failed (1 failed, 73 passed); with the call back, publishProject.test.ts and offload.test.ts pass (110 tests).
  • Comments ratchet: passes on this head.

Non-blocking:

  • The rewrite is a plain text replace over whole text files, not just references, so matching caption or script text is recomposed too. That's canonically equivalent and almost always harmless, but it can change byte-sensitive content such as SRI hashes or a JS length check.
  • Encoded references are matched only in encodeURIComponent form. A name with &, ,, + etc. written via encodeURI, or with lowercase %cc%81, won't be rewritten. This is an edge case and fine to follow up.

CI (CI, CodeQL, Windows, preview-regression) was still running on this head when I posted this.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit 91ad7fb Nov 8, 2026
131 checks passed
@miguel-heygen
miguel-heygen deleted the dcloudpro/nfc-archive-keys branch November 8, 2026 23:07
@miguel-heygen miguel-heygen mentioned this pull request Nov 8, 2026
3 of 4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant