Support blueprint.json in ZIP at root or inside a directory#3223
Open
Support blueprint.json in ZIP at root or inside a directory#3223
Conversation
When resolving a blueprint from a ZIP URL (?blueprint-url=...), look for blueprint.json both at the zip root and inside any directory. Prefer root when both exist. - storage: Add ZipFilesystem.getEntryPaths() to list zip entry paths - storage: Add PrefixFilesystem to expose a subdirectory as the bundle root - blueprints: createBlueprintBundleFromZip() finds blueprint.json and wraps with PrefixFilesystem when it lives in a subdir, so getBlueprintDeclaration() and vfs paths continue to work unchanged
…bundles When a blueprint in a subdir (e.g. personal-readymade/blueprint.json) references ./readymade.zip, the backend was receiving personal-readymade/./readymade.zip, which does not match zip entries stored as personal-readymade/readymade.zip. Strip leading ./ and collapse /./ in PrefixFilesystem.read() so relative paths resolve correctly against the underlying backend.
adamziel
reviewed
Jan 30, 2026
| * A ReadableFilesystemBackend that exposes a subdirectory of another backend | ||
| * as the root. Paths are resolved by prepending the prefix (e.g. "foo/"). | ||
| */ | ||
| export class PrefixFilesystem implements ReadableFilesystemBackend { |
Collaborator
There was a problem hiding this comment.
Let's call it ChrootFilesystem
adamziel
reviewed
Jan 30, 2026
Comment on lines
+276
to
+284
| // Strip leading / and ./, collapse /./ so paths like ./readymade.zip | ||
| // resolve correctly (zip entries are stored without ./). | ||
| const normalizedPath = path | ||
| .replace(/^\//, '') | ||
| .replace(/^\.\//, '') | ||
| .replace(/\/\.\//g, '/'); | ||
| const prefixedPath = | ||
| this.prefix === '' ? normalizedPath : this.prefix + normalizedPath; | ||
| return this.backend.read(prefixedPath); |
Collaborator
There was a problem hiding this comment.
How about this?
Suggested change
| // Strip leading / and ./, collapse /./ so paths like ./readymade.zip | |
| // resolve correctly (zip entries are stored without ./). | |
| const normalizedPath = path | |
| .replace(/^\//, '') | |
| .replace(/^\.\//, '') | |
| .replace(/\/\.\//g, '/'); | |
| const prefixedPath = | |
| this.prefix === '' ? normalizedPath : this.prefix + normalizedPath; | |
| return this.backend.read(prefixedPath); | |
| const chrootedPath = joinPaths( this.chroot, path ); | |
| return this.backend.read( chrootedPath ); |
adamziel
reviewed
Jan 30, 2026
| * Returns the paths of all entries in the zip (file and directory names). | ||
| * Used to locate blueprint.json when it may be at root or inside a directory. | ||
| */ | ||
| async getEntryPaths(): Promise<string[]> { |
adamziel
reviewed
Jan 30, 2026
|
|
||
| /** | ||
| * Creates a BlueprintBundle from a zip ArrayBuffer. Looks for blueprint.json | ||
| * at the root or inside any directory so both flat and nested zip layouts work. |
Collaborator
There was a problem hiding this comment.
I'm hesitant to support deeply nested blueprint.json files:
- There could be multiple blueprint.json files found, what should we do then?
- Deep nesting could be intentional. Any context-referencing path starting with
../won't work, confusing the user. - We can't support that with URL-based bundles as we can't list files via HTTP.
My suggestion:
- Try
/blueprint.json. If found, stop. - List all top-level directories, skipping
__MACOSX. If there's more than one, Error. - Try
/<the only top-level directory>/blueprint.json. If found, stop. - Error – missing blueprint.json. Link to the docs page that explains the bundle format.
adamziel
reviewed
Jan 30, 2026
| * @returns The path to blueprint.json (e.g. "blueprint.json" or "my-dir/blueprint.json"), or null. | ||
| */ | ||
| function findBlueprintJsonPath(entryPaths: string[]): string | null { | ||
| const normalized = entryPaths.map((p) => p.replace(/\\/g, '/').replace(/\/$/, '')); |
Collaborator
There was a problem hiding this comment.
We have utils for path normalization. Let's not do any ad-hoc replacements – they're likely to be wrong.
adamziel
reviewed
Jan 30, 2026
| function findBlueprintJsonPath(entryPaths: string[]): string | null { | ||
| const normalized = entryPaths.map((p) => p.replace(/\\/g, '/').replace(/\/$/, '')); | ||
| // Prefer root blueprint.json | ||
| if (normalized.includes('blueprint.json')) { |
Collaborator
There was a problem hiding this comment.
basename( path ) === 'blueprint.json'
adamziel
reviewed
Jan 30, 2026
| return 'blueprint.json'; | ||
| } | ||
| for (const path of normalized) { | ||
| if (path.endsWith('/blueprint.json')) { |
Collaborator
There was a problem hiding this comment.
basename( path ) === 'blueprint.json'
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.
When resolving a blueprint from a ZIP URL (?blueprint-url=...), look for blueprint.json both at the zip root and inside any directory. Prefer root when both exist.
Motivation for the change, related issues
Implementation details
Testing Instructions (or ideally a Blueprint)