Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
import { isTraversableFilesystemBackend } from './opfs-blueprint-bundle-storage';
import type { TraversableFilesystemBackend } from '@wp-playground/storage';
import {
isTraversableFilesystemBackend,
persistBlueprintBundle,
} from './opfs-blueprint-bundle-storage';

vi.mock('@wp-playground/storage', () => ({
OpfsFilesystemBackend: {},
const mocks = vi.hoisted(() => ({
copyFilesystem: vi.fn(),
fromPath: vi.fn(),
}));

vi.mock('@wp-playground/storage', () => ({
OpfsFilesystemBackend: { fromPath: mocks.fromPath },
copyFilesystem: mocks.copyFilesystem,
}));

describe('persistBlueprintBundle', () => {
beforeEach(() => {
mocks.copyFilesystem.mockReset();
mocks.fromPath.mockReset();
});

it('returns the destination backend after copying the bundle', async () => {
const source = {} as TraversableFilesystemBackend;
const destination = {};
mocks.fromPath.mockResolvedValue(destination);

await expect(
persistBlueprintBundle('copied-site', source)
).resolves.toBe(destination);

expect(mocks.fromPath).toHaveBeenCalledWith(
'/sites/site-copied-site/blueprint-bundle',
true
);
expect(mocks.copyFilesystem).toHaveBeenCalledWith(source, destination);
});
});

describe('isTraversableFilesystemBackend', () => {
it('requires traversal members to be functions', () => {
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,28 @@ export async function hasBlueprintBundle(siteSlug: string): Promise<boolean> {
}

/**
* Copy files from a source filesystem to a site's blueprint bundle storage.
* Copies a Blueprint bundle into a site's OPFS directory.
*
* The returned backend points to the copied bundle. Callers creating a site can
* store it in the site's metadata without reopening the destination directory.
*
* A failed copy may leave partial files in the destination. Callers that need
* atomic creation are responsible for removing that destination during rollback.
*
* @param siteSlug The site that will own the copied bundle.
* @param source The bundle filesystem to copy.
* @returns The OPFS backend containing the copied bundle.
*/
export async function persistBlueprintBundle(
siteSlug: string,
source: TraversableFilesystemBackend
): Promise<void> {
): Promise<OpfsFilesystemBackend> {
const destination = await OpfsFilesystemBackend.fromPath(
getBundlePath(siteSlug),
true
);
await copyFilesystem(source, destination);
return destination;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ describe('stored site creation', () => {
name: 'Original Playground',
});
const editedBundle = createBundleBlueprint();
const copiedBundle = createBundleBlueprint();
let state = {
sites: sitesSlice.reducer(
undefined,
Expand All @@ -355,6 +356,7 @@ describe('stored site creation', () => {
const writes: string[] = [];
persistBlueprintBundle.mockImplementation(async () => {
writes.push('bundle');
return copiedBundle;
});
createSite.mockImplementation(async () => {
writes.push('metadata');
Expand All @@ -373,7 +375,7 @@ describe('stored site creation', () => {
storage: 'opfs',
persistence: 'autosave',
initialOpfsSyncPending: true,
originalBlueprint: editedBundle,
originalBlueprint: copiedBundle,
originalBlueprintSource: { type: 'opfs-site' },
runtimeConfiguration,
});
Expand Down
11 changes: 8 additions & 3 deletions packages/playground/website/src/lib/state/redux/slice-sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,12 @@ export function setTemporarySiteSpec(

/**
* Creates a new OPFS-backed Playground from a setup URL or editable Blueprint
* bundle. Bundles are stored with the new Playground before their metadata is
* written because they may include edits that cannot be represented by a URL.
* bundle.
*
* Editable bundles are copied into the new Playground's storage before its
* metadata is written. The metadata points to that persisted copy rather than
* the caller's backend, so editing the new Playground cannot mutate the source
* bundle.
*/
export function createStoredSite(
siteName: string,
Expand Down Expand Up @@ -650,7 +654,8 @@ export function createStoredSite(

try {
if (blueprintBundle) {
await persistBlueprintBundle(siteSlug, blueprintBundle);
newSiteInfo.metadata.originalBlueprint =
await persistBlueprintBundle(siteSlug, blueprintBundle);
}
Comment thread
adamziel marked this conversation as resolved.
await dispatch(addSite(newSiteInfo));
} catch (error) {
Expand Down
Loading