Skip to content

Fix web/extension build ordering for deploy and dev#7320

Open
MitchLillie wants to merge 1 commit intomainfrom
ml-fix-web-extension-build-ordering
Open

Fix web/extension build ordering for deploy and dev#7320
MitchLillie wants to merge 1 commit intomainfrom
ml-fix-web-extension-build-ordering

Conversation

@MitchLillie
Copy link
Copy Markdown
Contributor

Problem

When deploying or running dev with an app that has [admin] static_root, the web build (e.g. Vite) and extension builds run concurrently. The admin extension's include_assets step copies files from static_root (e.g. dist/), but the web build may not have finished populating it yet. Vite's emptyOutDir: true deletes dist/ at the start of its build, so the admin extension either sees a missing or empty directory and skips the copy. The result:

index_missing: index.html must be present in the bundle.

This affects both shopify app deploy and shopify app dev.

Fixes https://github.com/shop/issues-admin-extensibility/issues/2411
Supersedes #7315 and #7305

Root Cause

The admin extension's include_assets build step has an implicit dependency on web build output (static_rootdist/), but the build system runs everything concurrently:

Deploy (bundle.ts): renderConcurrent([webBuildProcesses, extensionBuildProcesses].flat()) — one flat concurrent batch.

Dev (app-event-watcher.ts): All processes (web dev, app watcher) start via Promise.all. The watcher immediately builds all extensions on start, while the web dev process is still starting up.

Solution

Deploy (bundle.ts)

Split the single renderConcurrent call into two sequential phases:

  1. Web builds first — run all web build processes to completion
  2. Extension builds second — run extension builds which can now safely copy from web output

Extensions within each phase still run concurrently with each other.

Dev (app-event-watcher.ts)

Added waitForStaticRoots() which polls until admin's static_root directory is populated before building extensions.

We cannot use the same "build first" approach as deploy because the web dev process (commands.dev) runs as a concurrent sibling process. Running commands.build in the watcher races with the dev process on the same output directory (both write to dist/, and Vite's emptyOutDir causes them to clobber each other).

Instead, we wait for the web dev process to produce its initial output (up to 30s, polling every 200ms), then proceed with extension builds. No-op when no admin extension references a static_root.

Testing

  • 10 deploy bundle tests pass (including new ordering assertion)
  • 24 app-event-watcher tests pass (including 3 new waitForStaticRoots tests)
  • The ordering test uses a callOrder array to prove web-build-end happens before extension-build-start

Tophatting

Prerequisites

  • dev up
  • Create a test app (one-time):
    HOSTED_APPS=1 pnpm shopify app init --name test-fix-index-html --path="$HOME/tmp"
    Select "Build an extension-only app (Shopify-hosted Preact app home and extensions, no back-end)"

Reset state between tests

rm -rf ~/tmp/test-fix-index-html/dist/ ~/tmp/test-fix-index-html/.shopify/dev-bundle/ ~/tmp/test-fix-index-html/.shopify/dev-bundle.br ~/tmp/test-fix-index-html/.shopify/deploy-bundle.br ~/tmp/test-fix-index-html/.shopify/deploy-bundle

Test dev

HOSTED_APPS=1 pnpm shopify app dev --path="$HOME/tmp/test-fix-index-html"

Before fix: index_missing: index.html must be present in the bundle.
After fix: Dev session starts successfully. The watcher waits briefly for the web dev process to populate dist/, then builds the admin extension.

Test deploy

HOSTED_APPS=1 pnpm shopify app deploy --path="$HOME/tmp/test-fix-index-html"

Before fix: Same index_missing race condition (intermittent).
After fix: Web builds complete before extension builds start. No race condition.

Edge cases

  1. No build command on web — should skip web build phase, behave same as before
  2. No static_root configuredwaitForStaticRoots is a no-op, copyConfigKeyEntry skips
  3. Repeated runs — reset state between runs with the command above to verify the fix works consistently

@MitchLillie MitchLillie requested a review from a team as a code owner April 15, 2026 20:38
Extensions like the admin module copy output from web builds (e.g. dist/)
into the bundle via include_assets. Both deploy and dev ran web builds
concurrently with extension builds, causing a race condition where dist/
was empty or missing when the admin extension tried to copy it.

Deploy: split the single renderConcurrent call into two sequential phases —
web builds first, then extension builds.

Dev: wait for the web dev process to populate static_root before building
extensions. We cannot run commands.build ourselves because it races with
the concurrent web dev process on the same output directory.

Fixes: index_missing: index.html must be present in the bundle.
@MitchLillie MitchLillie force-pushed the ml-fix-web-extension-build-ordering branch from d588506 to 98607cc Compare April 15, 2026 20:45
Copy link
Copy Markdown
Contributor

@elanalynn elanalynn left a comment

Choose a reason for hiding this comment

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

🎩 'd. Both dev and deploy work without running build manually.

import EventEmitter from 'events'
import {Writable} from 'stream'

const POLL_TIMEOUT_MS = 30_000
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Polling isn't ideal, but it does work.

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