From d9c2d2c613f0390766c24e1a682da9108e991ecb Mon Sep 17 00:00:00 2001 From: Erik May <36517827+erikmay@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:51:04 +0200 Subject: [PATCH] Support [build].watch for theme app extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Theme extensions currently use the base `dd` schema which has no `build` field, and don't implement `devSessionWatchConfig`. Any `[build]` block in `shopify.extension.toml` is silently stripped by zod, and the CLI's dev watcher falls back to globbing `**/*` under the extension directory. That cascade is painful when an external build tool (Vite, esbuild, tsc --watch) writes multiple output files per build — each write fires "Extension changed", triggering redundant theme-check and bundle cycles. This mirrors the function extension pattern: - Extend `BaseSchema` into `ThemeExtensionSchema` with `build: { watch: string | string[] }`. - Add `devSessionWatchConfig` to the theme spec that restricts watched paths to `build.watch` + `locales/**.json` + `**.toml` (dropping the `**/!(.)*.graphql` entry from the function version since theme extensions don't use GraphQL). With this change, authors can point the CLI at a sentinel file written by their build tool after all outputs are on disk — one CLI trigger per full build instead of N. --- .changeset/theme-extension-build-watch.md | 5 ++++ .../models/extensions/specifications/theme.ts | 26 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .changeset/theme-extension-build-watch.md diff --git a/.changeset/theme-extension-build-watch.md b/.changeset/theme-extension-build-watch.md new file mode 100644 index 00000000000..3f985201bf6 --- /dev/null +++ b/.changeset/theme-extension-build-watch.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Theme app extensions now support `[build].watch` in `shopify.extension.toml` to restrict which files trigger bundle rebuilds during `shopify app dev`. This mirrors the behavior already available for function extensions. When `build.watch` is specified, the CLI only reacts to changes in those paths (plus `locales/**.json` and `**.toml`) instead of the entire extension directory, preventing redundant bundle cycles when an external build tool (e.g. Vite) writes multiple output files. diff --git a/packages/app/src/cli/models/extensions/specifications/theme.ts b/packages/app/src/cli/models/extensions/specifications/theme.ts index 8e6d27d27e4..f6381275d2a 100644 --- a/packages/app/src/cli/models/extensions/specifications/theme.ts +++ b/packages/app/src/cli/models/extensions/specifications/theme.ts @@ -3,13 +3,24 @@ import {BaseSchema} from '../schemas.js' import {themeExtensionFiles} from '../../../utilities/extensions/theme.js' import {ExtensionInstance} from '../extension-instance.js' import {fileSize} from '@shopify/cli-kit/node/fs' -import {dirname, relativePath} from '@shopify/cli-kit/node/path' +import {dirname, joinPath, relativePath} from '@shopify/cli-kit/node/path' import {AbortError} from '@shopify/cli-kit/node/error' import {outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {zod} from '@shopify/cli-kit/node/schema' + +const ThemeExtensionSchema = BaseSchema.extend({ + build: zod + .object({ + watch: zod.union([zod.string(), zod.string().array()]).optional(), + }) + .optional(), +}) + +type ThemeExtensionConfigType = zod.infer const themeSpec = createExtensionSpecification({ identifier: 'theme', - schema: BaseSchema, + schema: ThemeExtensionSchema, partnersWebIdentifier: 'theme_app_extension', graphQLType: 'theme_app_extension', clientSteps: [ @@ -24,6 +35,17 @@ const themeSpec = createExtensionSpecification({ appModuleFeatures: (_) => { return ['theme'] }, + devSessionWatchConfig: (extension: ExtensionInstance) => { + const config = extension.configuration + if (!config.build || !config.build.watch) return undefined + + const paths = [config.build.watch].flat().map((path) => joinPath(extension.directory, path)) + + paths.push(joinPath(extension.directory, 'locales', '**.json')) + paths.push(joinPath(extension.directory, '**.toml')) + + return {paths} + }, deployConfig: async () => { return {theme_extension: {files: {}}} },