Skip to content

Add lightweight saved-site export API - #4219

Open
ashfame wants to merge 8 commits into
trunkfrom
agent/hosted-playground-api
Open

Add lightweight saved-site export API#4219
ashfame wants to merge 8 commits into
trunkfrom
agent/hosted-playground-api

Conversation

@ashfame

@ashfame ashfame commented Jul 30, 2026

Copy link
Copy Markdown
Member

What

Adds startPlaygroundAPI(), the public saved-site export types, a lightweight /api.html runtime, API forwarding coverage, and Vite chunking that keeps the OPFS export path separate from the optional Blueprint editor.

Why

Consumers need to export an existing OPFS Playground without booting WordPress, PHP, workers, or a service worker. The endpoint must share the origin and browser storage partition where the site was saved; WebKit additionally requires the same top-level origin.

Stack

Checks

  • npm exec nx test playground-client
  • npm exec nx test playground-website
  • npm exec nx lint playground-client
  • npm exec nx lint playground-website
  • npm exec nx typecheck playground-client
  • npm exec nx typecheck playground-website
  • npm exec nx build playground-client
  • npm exec nx build playground-website
@ashfame
ashfame force-pushed the agent/hosted-playground-api branch from 4b0b60f to 652e0af Compare July 30, 2026 16:50
@ashfame
ashfame force-pushed the agent/opfs-export-boundary branch from 8cb9ca4 to 3912eb4 Compare July 30, 2026 16:50
@ashfame
ashfame force-pushed the agent/opfs-export-boundary branch from 3912eb4 to fe66b7e Compare July 30, 2026 19:31
@ashfame
ashfame force-pushed the agent/hosted-playground-api branch from 652e0af to 7cbfcdf Compare July 30, 2026 19:31
@ashfame
ashfame changed the base branch from agent/opfs-export-boundary to agent/opfs-export-filtering July 30, 2026 19:56
@ashfame
ashfame force-pushed the agent/hosted-playground-api branch from 7cbfcdf to 314fd8f Compare July 30, 2026 20:03
output: {
manualChunks: (id) => {
// Rollup recursively assigns the API entry's dependencies to this chunk.
// This prevents the optional Blueprint editor chunk from claiming OPFS storage.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This results in ~70% measured savings

   Cold api.html startup    With rule    Without rule                 Savings
  ━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━  ━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━
   Raw assets               706,309 B     2,212,465 B    1,506,156 B (68.08%)
  ───────────────────────  ───────────  ──────────────  ──────────────────────
   gzip assets              174,286 B       656,404 B      482,118 B (73.45%)
  ───────────────────────  ───────────  ──────────────  ──────────────────────
   Requests                         3              11              8 (72.73%)
  ───────────────────────  ───────────  ──────────────  ──────────────────────
   HTML gzip                    285 B           420 B                   135 B

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment still doesn't make sense to me. I definitely could be missing something, but why would the Blueprint editor chunk claiming OPFS storage be an issue for the API entry point?

It might be that the build may then think the API entry point needs to import the Blueprint editor chunk in order to gain access to the OPFS-related modules. But that sounds like a build bug to me. Shouldn't we be able to build a separate set of chunks to serve the api.html entry point and completely disentangle from the larger Playground website builds?

There is something we don't understand well enough here, and I think it is worth understanding this in order to fix it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As a philosophical side note, if we have evidence to justify a particular line of code, it would be good to include that evidence in an inline comment so it continues to live and justify the code. Otherwise, it will be lost to GitHub history.

In this case, I think we probably should try to fix the build so this config hack is not needed, but if it turns out that Vite and rollup force us into this hack, we should explain it better so everyone truly can understand it.

One cool thing @adamziel mentioned the other day is asking agents to document things in language that is suitable to a junior developer. That's often great language for all of us, junior or not.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@brandonpayton Yep, pretty much what you said. The chunking done by the build process end up such that a larger chunk gets loaded for what could have been a smaller chunk ideally. I am not sure to call that a build bug or just that the build process isn't super smart to figure the right chunking in every case. I will try to rephrase the doc to make it super easy for a junior dev to understand. 👍

@ashfame
ashfame marked this pull request as ready for review July 31, 2026 06:54
@ashfame
ashfame requested a review from brandonpayton July 31, 2026 07:53
Comment on lines +138 to +157
it('loads api.html and returns its API client', async () => {
const api = createAPIClient();
mocks.consumeAPI.mockReturnValue(api);
const iframe = createIframe();

await expect(
startPlaygroundAPI({
iframe,
apiUrl: 'http://localhost/api.html',
})
).resolves.toBe(api);

expect(iframe.src).toBe('http://localhost/api.html');
expect(mocks.consumeAPI).toHaveBeenCalledWith(
iframe.contentWindow,
iframe.ownerDocument!.defaultView
);
expect(api.isConnected).toHaveBeenCalledTimes(1);
expect(api.isReady).toHaveBeenCalledTimes(1);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure this test adds enough value to justify its existence. It is also asserting on internals involving consumeAPI(). It's kind of like asserting that startPlaygroundAPI() has an implementation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

L155-156 are meh, but the assertions above it are useful, I believe? So, I didn't bother removing those two meh assertions, cuz it's fine to over do it a little.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note to self: re-evaluate the test


## Saved-site export API

Use `startPlaygroundAPI()` to work with saved OPFS Playgrounds without booting WordPress, PHP, workers, or a service worker. The API endpoint must have the same origin and browser storage partition as the Playground that saved the site. WebKit requires save and export to use the same top-level origin, including its scheme, host, and port; a site saved while Playground is top-level is not visible to an API iframe embedded under another top-level origin.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe we should omit "OPFS" here. It's an implementation detail. The API's purpose is to provide access to saved Playgrounds.

Actually... with that in mind, what if we rename api.html to something more specific like export-playground.html? This isn't being created because we want to launch a large API surface that serves different purposes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Maybe we should omit "OPFS" here. It's an implementation detail. The API's purpose is to provide access to saved Playgrounds.

We can have other saved playground sites that are not OPFS based and this API doesn't work with those, so perhaps its better to be upfront and accurate about it, even if that's an implementation detail. We are also not striving towards eventually covering all playground sites. In fact, it may not even be possible in all contexts.

Actually... with that in mind, what if we rename api.html to something more specific like export-playground.html? This isn't being created because we want to launch a large API surface that serves different purposes.

I earlier started with bridge.html but @adamziel suggested to use api.html since it can be expanded for other stuff in the future.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note to self: Rephrase highlighting how we are thinking and future potential

Comment thread packages/playground/client/README.md
Comment thread packages/playground/client/src/index.ts Outdated
import { bootPlaygroundAPI } from './src/lib/boot-playground-api';

window.playgroundAPI = bootPlaygroundAPI();
</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be nice to include minimal text here to document what this page does for folks who load it directly. Something short and low cost.

expect(mocks.exportSavedSiteAsZip).toHaveBeenCalledWith('my-site', {
excludePatterns: ['/*', '!/wp-content/**'],
});
expect(mocks.setAPIReady).toHaveBeenCalledTimes(1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How useful is it to assert that the implementation has exposeAPI() details?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe there is a reason we don't have them here, and maybe we talked about it yesterday, but I think an E2E test would be more useful for evidence that everything is wired properly (or sufficiently).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agree on the E2E test, I haven't added that yet. But the meh asserts seem fine to keep in an AI-first world. What seems very less useful for humans could be a meaningful signal for AI as it parses text/logs to build context?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note to self: Re evaluate the test

output: {
manualChunks: (id) => {
// Rollup recursively assigns the API entry's dependencies to this chunk.
// This prevents the optional Blueprint editor chunk from claiming OPFS storage.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment still doesn't make sense to me. I definitely could be missing something, but why would the Blueprint editor chunk claiming OPFS storage be an issue for the API entry point?

It might be that the build may then think the API entry point needs to import the Blueprint editor chunk in order to gain access to the OPFS-related modules. But that sounds like a build bug to me. Shouldn't we be able to build a separate set of chunks to serve the api.html entry point and completely disentangle from the larger Playground website builds?

There is something we don't understand well enough here, and I think it is worth understanding this in order to fix it.

output: {
manualChunks: (id) => {
// Rollup recursively assigns the API entry's dependencies to this chunk.
// This prevents the optional Blueprint editor chunk from claiming OPFS storage.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As a philosophical side note, if we have evidence to justify a particular line of code, it would be good to include that evidence in an inline comment so it continues to live and justify the code. Otherwise, it will be lost to GitHub history.

In this case, I think we probably should try to fix the build so this config hack is not needed, but if it turns out that Vite and rollup force us into this hack, we should explain it better so everyone truly can understand it.

One cool thing @adamziel mentioned the other day is asking agents to document things in language that is suitable to a junior developer. That's often great language for all of us, junior or not.

Co-authored-by: Brandon Payton <brandon@happycode.net>
brandonpayton pushed a commit that referenced this pull request Jul 31, 2026
## What

Adds saved OPFS site ZIP exports with optional gitignore-style exclusion
patterns, subtree pruning, stable directory permissions, and focused
coverage.

## Why

This establishes the storage-layer capability independently of the
hosted API, so it can be reviewed and tested without client, routing, or
deployment changes.

## Stack

- 1 of 3
- Base: `trunk`
- Next: #4219

## Checks

- `npm exec nx test playground-website`
- `npm exec nx lint playground-website`
- `npm exec nx typecheck playground-website`
- `npm exec nx build playground-website`
Base automatically changed from agent/opfs-export-filtering to trunk July 31, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment