feat(hub-client): import a project from a ZIP archive#253
Merged
Conversation
Add an "Import from ZIP" action to the project-selector landing page that creates a new hub-client project from an uploaded .zip — the inverse of the existing "Export to ZIP". - ts-packages/quarto-sync-client: new pure parseProjectZip() that unzips (fflate) into the CreateProjectOptions.files shape. Strips a common top-level dir (GitHub-style downloads), drops __MACOSX/.DS_Store/.git and directory entries, rejects zip-slip paths, and classifies text vs binary by extension with a UTF-8/NUL sniff fallback for unknown extensions (binary content base64-encoded for createNewProject). - ts-packages/preview-runtime: new importProjectFromZip() wrapper that maps to the snake_case ProjectFile shape, symmetric with exportProjectAsZip. - hub-client ProjectSelector: new "Import from ZIP" button + form (file picker, title prefilled from filename, sync-server). Reuses the existing onProjectCreated path unchanged; project type passed as an 'imported' sentinel that App.handleProjectCreated already ignores. Tests (TDD): import-zip parser (20), wrapper conversion (2), ProjectSelector component wiring (5), and an e2e (e2e/import-zip.spec.ts) that drives the real browser pipeline end-to-end (upload -> create -> render, text + binary). The jsdom component test mocks the parser: under vitest's jsdom env, TextEncoder returns a foreign-realm Uint8Array, so fflate's zipSync instanceof leaf-check misclassifies strToU8 output (jsdom/jsdom#2524). Real browsers are single-realm; the e2e confirms production is unaffected. Plan: claude-notes/plans/2026-06-01-import-from-zip.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an "Import from ZIP" action to the hub-client project-selector landing page: a user uploads a
.zipand it becomes a brand-new hub-client project. This is the inverse of the existing "Export to ZIP" onProjectTab.The import reuses the existing project-creation pipeline end-to-end —
onProjectCreated→App.handleProjectCreated→createNewProject— with no changes to the Automerge layer.createNewProjectalready accepts a mixed text/base64-binary file list, so import is just "parse ZIP → build that list → run the existing create path."Tracked as
bd-apv23. Design notes:claude-notes/plans/2026-06-01-import-from-zip.md.What's in it
ts-packages/quarto-sync-client/src/import-zip.ts— new pureparseProjectZip()(mirrorsexport-zip.ts). Unzips withfflate, then:repo-main/downloads),__MACOSX/·.DS_Store·.git/junk,..),createNewProject.ts-packages/preview-runtime— newimportProjectFromZip()wrapper mapping to the snake_caseProjectFileshape, symmetric withexportProjectAsZip.hub-clientProjectSelector— new "Import from ZIP" button + form (file picker, title prefilled from the filename, sync-server). Project type is passed as an'imported'sentinel thatApp.handleProjectCreatedalready ignores.Testing
TDD throughout (tests written first, confirmed failing, then implemented):
onProjectCreated, parse errors surfaced, submit gated on a chosen file).hub-client/e2e/import-zip.spec.ts) — drives real Chromium against a live hub: upload a fixture zip (_quarto.yml,index.qmd, a real PNG) → new project created → both files appear in the sidebar →index.qmdrenders the imported content in the preview iframe. Confirms text and binary round-trip through the realfflate → createNewProject → Automerge → renderpath.Gates run green: hub-client unit 561 / integration 66 / wasm 79, production
vite build, and the dependency packages (quarto-sync-client 136, preview-runtime 62). No Rust changes.Note on the jsdom component test
The component test mocks
importProjectFromZiprather than calling real fflate. Under vitest's jsdom environment, jsdom'sTextEncoder.encode()returns aUint8Arrayfrom a different JS realm, sox instanceof Uint8Arrayisfalse(jsdom/jsdom#2524). fflate'sstrToU8usesTextEncoder, and itszipSyncflatten step decides file-vs-directory withval instanceof u8— so astrToU8-produced array is misclassified as a directory and the archive comes out corrupt. Real browsers are single-realm, so this is a test-environment artifact only (the e2e exercises the real path). Real ZIP parsing is therefore asserted in node-env suites.Out of scope (possible follow-ups)
🤖 Generated with Claude Code