Skip to content

Support blueprint.json in ZIP at root or inside a directory#3223

Open
ashfame wants to merge 3 commits intotrunkfrom
blueprint_bundle_handle_when_files_under_dir
Open

Support blueprint.json in ZIP at root or inside a directory#3223
ashfame wants to merge 3 commits intotrunkfrom
blueprint_bundle_handle_when_files_under_dir

Conversation

@ashfame
Copy link
Member

@ashfame ashfame commented Jan 30, 2026

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

Motivation for the change, related issues

Implementation details

Testing Instructions (or ideally a Blueprint)

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
@ashfame ashfame self-assigned this Jan 30, 2026
…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.
* 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's call it ChrootFilesystem

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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

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 );

* 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[]> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

getAllFilePaths()?


/**
* 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

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:

  1. Try /blueprint.json. If found, stop.
  2. List all top-level directories, skipping __MACOSX. If there's more than one, Error.
  3. Try /<the only top-level directory>/blueprint.json. If found, stop.
  4. Error – missing blueprint.json. Link to the docs page that explains the bundle format.

* @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(/\/$/, ''));
Copy link
Collaborator

Choose a reason for hiding this comment

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

We have utils for path normalization. Let's not do any ad-hoc replacements – they're likely to be wrong.

function findBlueprintJsonPath(entryPaths: string[]): string | null {
const normalized = entryPaths.map((p) => p.replace(/\\/g, '/').replace(/\/$/, ''));
// Prefer root blueprint.json
if (normalized.includes('blueprint.json')) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

basename( path ) === 'blueprint.json'

return 'blueprint.json';
}
for (const path of normalized) {
if (path.endsWith('/blueprint.json')) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

basename( path ) === 'blueprint.json'

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.

2 participants