From dee51709b24860c7f6b91ea2f709dffcdc050392 Mon Sep 17 00:00:00 2001 From: Mitch Lillie Date: Tue, 14 Apr 2026 14:46:04 -0700 Subject: [PATCH] Fix missing index.html validation for admin extension during dev When an app with admin.static_root is created and dev is run immediately, the build step may not have completed yet, causing validation to fail on the server side with an unclear error message. This change adds a buildValidation hook to the admin extension specification that checks for the presence of index.html in the static_root directory before starting the dev session. If the file is missing, it throws a clear error message explaining the issue and suggesting to configure a predev hook. --- .../extensions/specifications/admin.test.ts | 83 +++++++++++++++++++ .../models/extensions/specifications/admin.ts | 16 ++++ 2 files changed, 99 insertions(+) create mode 100644 packages/app/src/cli/models/extensions/specifications/admin.test.ts diff --git a/packages/app/src/cli/models/extensions/specifications/admin.test.ts b/packages/app/src/cli/models/extensions/specifications/admin.test.ts new file mode 100644 index 00000000000..f9af0bba308 --- /dev/null +++ b/packages/app/src/cli/models/extensions/specifications/admin.test.ts @@ -0,0 +1,83 @@ +import adminSpec, {AdminConfigType} from './admin.js' +import {ExtensionInstance} from '../extension-instance.js' +import {inTemporaryDirectory, mkdir, touchFile} from '@shopify/cli-kit/node/fs' +import {joinPath} from '@shopify/cli-kit/node/path' +import {describe, expect, test} from 'vitest' + +function createAdminExtensionInstance(directory: string, staticRoot?: string): ExtensionInstance { + return { + directory, + configuration: { + name: 'test-admin', + type: 'admin', + admin: staticRoot ? {static_root: staticRoot} : undefined, + }, + } as unknown as ExtensionInstance +} + +describe('admin buildValidation', () => { + test('passes when static_root is not configured', async () => { + await inTemporaryDirectory(async (tmpDir) => { + const extension = createAdminExtensionInstance(tmpDir, undefined) + + // Should not throw + await expect(adminSpec.buildValidation!(extension)).resolves.toBeUndefined() + }) + }) + + test('passes when static_root exists and contains index.html', async () => { + await inTemporaryDirectory(async (tmpDir) => { + const distDir = joinPath(tmpDir, 'dist') + await mkdir(distDir) + await touchFile(joinPath(distDir, 'index.html')) + + const extension = createAdminExtensionInstance(tmpDir, './dist') + + // Should not throw + await expect(adminSpec.buildValidation!(extension)).resolves.toBeUndefined() + }) + }) + + test('throws when static_root is configured but index.html is missing', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Create the dist directory but without index.html + const distDir = joinPath(tmpDir, 'dist') + await mkdir(distDir) + + const extension = createAdminExtensionInstance(tmpDir, './dist') + + await expect(adminSpec.buildValidation!(extension)).rejects.toThrow( + 'The admin extension requires an index.html file in the static_root directory (./dist), but it was not found.', + ) + }) + }) + + test('throws when static_root directory does not exist', async () => { + await inTemporaryDirectory(async (tmpDir) => { + const extension = createAdminExtensionInstance(tmpDir, './dist') + + await expect(adminSpec.buildValidation!(extension)).rejects.toThrow( + 'The admin extension requires an index.html file in the static_root directory (./dist), but it was not found.', + ) + }) + }) +}) + +describe('admin devSessionWatchConfig', () => { + test('returns empty paths when static_root is not configured', () => { + const extension = createAdminExtensionInstance('/tmp/test', undefined) + const watchConfig = adminSpec.devSessionWatchConfig!(extension) + + expect(watchConfig).toEqual({paths: []}) + }) + + test('returns watch paths when static_root is configured', () => { + const extension = createAdminExtensionInstance('/tmp/test', './dist') + const watchConfig = adminSpec.devSessionWatchConfig!(extension) + + expect(watchConfig).toBeDefined() + expect(watchConfig!.paths).toHaveLength(1) + expect(watchConfig!.paths[0]).toContain('dist') + expect(watchConfig!.paths[0]).toContain('**/*') + }) +}) diff --git a/packages/app/src/cli/models/extensions/specifications/admin.ts b/packages/app/src/cli/models/extensions/specifications/admin.ts index bdd20cb2131..515cfa6706c 100644 --- a/packages/app/src/cli/models/extensions/specifications/admin.ts +++ b/packages/app/src/cli/models/extensions/specifications/admin.ts @@ -2,6 +2,8 @@ import {createExtensionSpecification} from '../specification.js' import {BaseConfigType, ZodSchemaType} from '../schemas.js' import {zod} from '@shopify/cli-kit/node/schema' import {joinPath} from '@shopify/cli-kit/node/path' +import {fileExists} from '@shopify/cli-kit/node/fs' +import {AbortError} from '@shopify/cli-kit/node/error' export const AdminSpecIdentifier = 'admin' @@ -31,6 +33,20 @@ const adminSpecificationSpec = createExtensionSpecification({ const path = joinPath(extension.directory, staticRoot, '**/*') return {paths: [path], ignore: []} }, + buildValidation: async (extension) => { + const staticRoot = extension.configuration.admin?.static_root + if (!staticRoot) return + + const indexHtmlPath = joinPath(extension.directory, staticRoot, 'index.html') + const indexExists = await fileExists(indexHtmlPath) + + if (!indexExists) { + throw new AbortError( + `The admin extension requires an index.html file in the static_root directory (${staticRoot}), but it was not found.`, + `This usually means the build step has not completed yet. Make sure your app runs a build command (e.g., via a predev hook in your web configuration) before starting the dev server.`, + ) + } + }, transformRemoteToLocal: (remoteContent) => { return { admin: {