Watch admin static_root and serve assets via dev server WebSocket#7152
Watch admin static_root and serve assets via dev server WebSocket#7152melissaluu wants to merge 4 commits intomainfrom
Conversation
|
We detected some changes at Caution DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release. |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds support to the UI extensions dev server for app-level “asset directories” (starting with admin extension static_root), including HTTP serving and WebSocket payload updates so clients can invalidate/reload assets when files change.
Changes:
- Extend dev server WebSocket payload/types with optional
app.assets(keyed map of{url, lastUpdated}). - Serve app-level assets from
/extensions/assets/:assetKey/**:filePathwhen configured. - Watch configured asset directories and broadcast
lastUpdatedtimestamp bumps on changes; re-resolve assets on app reload.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ui-extensions-server-kit/src/types.ts | Adds App.assets to the shared server-kit type surface. |
| packages/app/src/cli/services/dev/processes/previewable-extension.ts | Resolves and passes appAssets into the dev server process options. |
| packages/app/src/cli/services/dev/extension/server/middlewares.ts | Introduces getAppAssetsMiddleware for serving app-level assets. |
| packages/app/src/cli/services/dev/extension/server/middlewares.test.ts | Adds middleware tests for serving app assets + unknown key 404. |
| packages/app/src/cli/services/dev/extension/server.ts | Wires the new assets route into the HTTP server when appAssets is present. |
| packages/app/src/cli/services/dev/extension/payload/store.ts | Adds appAssets to payload initialization and new store update APIs. |
| packages/app/src/cli/services/dev/extension/payload/store.test.ts | Adds unit tests for app asset timestamp updates + payload initialization behavior. |
| packages/app/src/cli/services/dev/extension/payload/models.ts | Adds app.assets to the server-side payload model interface. |
| packages/app/src/cli/services/dev/extension.ts | Implements resolveAppAssets, chokidar watchers, and app reload plumbing. |
| packages/app/src/cli/services/dev/extension.test.ts | Adds tests for resolveAppAssets and passing appAssets through to HTTP server setup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/app/src/cli/services/dev/extension/server/middlewares.ts
Outdated
Show resolved
Hide resolved
| if (options.appAssets) { | ||
| const assets: Record<string, {url: string; lastUpdated: number}> = {} | ||
| for (const assetKey of Object.keys(options.appAssets)) { | ||
| assets[assetKey] = { | ||
| url: new URL(`/extensions/assets/${assetKey}/`, options.url).toString(), | ||
| lastUpdated: Date.now(), | ||
| } | ||
| } | ||
| payload.app.assets = assets | ||
| } |
There was a problem hiding this comment.
getExtensionsPayloadStoreRawPayload sets payload.app.assets whenever options.appAssets is truthy, even if it’s an empty object. That produces app.assets = {} whereas updateAppAssets treats an empty map as “no assets” and deletes the field. Consider aligning the behavior by checking Object.keys(options.appAssets).length > 0 before setting payload.app.assets.
| const {assetKey = '', filePath = ''} = getRouterParams(event) | ||
|
|
||
| const directory = appAssets[assetKey] | ||
|
|
||
| if (!directory) { | ||
| return sendError(event, {statusCode: 404, statusMessage: `No app assets configured for key: ${assetKey}`}) | ||
| } |
There was a problem hiding this comment.
const directory = appAssets[assetKey] reads a URL-controlled key from a plain object. Keys like __proto__/constructor can resolve to inherited properties (non-string values) and bypass the !directory guard. Use an own-property check (e.g., Object.hasOwn(appAssets, assetKey)) and/or construct appAssets with a null prototype to avoid prototype-chain lookups.
… leak - Changed appAssets from a captured reference to a getAppAssets() callback in setupHTTPServer and getAppAssetsMiddleware, so the middleware always reads fresh asset config after app reloads - Replaced unbounded debounceTimers array with a Map<string, setTimeout> keyed by assetKey, so each key has at most one timer entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
static_rootconfigured in its TOML, the dev server watches that directory for file changes, serves its contents via HTTP, and broadcasts timestamp updates to all WebSocket clientsapp.assetsfield on the WebSocket payload, keyed by asset type (currentlystaticRoot), each with aurlandlastUpdatedtimestampHow it works
Payload model (
models.ts,ui-extensions-server-kit/types.ts): Added optionalapp.assetsto both server-side and client-side types — aRecord<string, { url, lastUpdated }>.Payload store (
store.ts): AcceptsappAssetsin options to populateapp.assetson init. ExposesupdateAppAssetTimestamp(key)to bump a single asset's timestamp and emit an update, andupdateAppAssets(appAssets, url)to fully rebuild or remove the assets map (used on app reload).HTTP serving (
middlewares.ts,server.ts): Parameterized route at/extensions/assets/:assetKey/**:filePathserves files from the resolved directory, using the existingfileServerMiddlewarehelper.File watching (
extension.ts): Creates a dedicated chokidar watcher per asset directory with debounced updates. Watchers and debounce timers are cleaned up on abort and restarted on app reload when the admin extension config changes.Plumbing (
previewable-extension.ts): Resolves admin extension'sstatic_rootinto theappAssetsmap at process setup time via sharedresolveAppAssets()helper.End-to-end flow
Test plan
extension.test.ts—resolveAppAssets()returns correct map for admin extensions with/withoutstatic_root;appAssetspassed through to HTTP serverstore.test.ts—updateAppAssetTimestampupdates timestamp and emits;updateAppAssetsrebuilds or removes assets; raw payload populatesapp.assetswhen configuredmiddlewares.test.ts—getAppAssetsMiddlewareserves files for valid keys, returns 404 for unknown keys🤖 Generated with Claude Code