From 40c1a8732cef1fd2eac3948f4300ed32210b6196 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Thu, 6 Aug 2026 12:49:29 -0400 Subject: [PATCH] Add unsafe flag to app dev Assisted-By: devx/af011388-1430-43c8-9dd8-5d485440ed55 --- .changeset/unsafe-app-dev.md | 5 ++++ .../app-dev/generated/dev-session-create.ts | 11 ++++++++ .../app-dev/generated/dev-session-update.ts | 24 +++++++++++++++++ .../queries/dev-session-create.graphql | 4 +-- .../queries/dev-session-update.graphql | 8 ++++-- packages/app/src/cli/commands/app/dev.test.ts | 26 ++++++++++++++++++- packages/app/src/cli/commands/app/dev.ts | 6 +++++ packages/app/src/cli/services/dev.ts | 1 + .../dev-session/dev-session-process.test.ts | 3 +++ .../dev-session/dev-session-process.ts | 1 + .../dev/processes/dev-session/dev-session.ts | 12 +++++++++ .../dev/processes/setup-dev-processes.ts | 1 + .../utilities/developer-platform-client.ts | 2 ++ .../app-management-client.ts | 5 +++- 14 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 .changeset/unsafe-app-dev.md diff --git a/.changeset/unsafe-app-dev.md b/.changeset/unsafe-app-dev.md new file mode 100644 index 00000000000..9a24d942e30 --- /dev/null +++ b/.changeset/unsafe-app-dev.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Add an `--unsafe` flag to `shopify app dev`. diff --git a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-create.ts b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-create.ts index fc7b96cdd62..46dd187bc04 100644 --- a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-create.ts +++ b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-create.ts @@ -8,6 +8,7 @@ export type DevSessionCreateMutationVariables = Types.Exact<{ appId: Types.Scalars['String']['input'] assetsUrl: Types.Scalars['String']['input'] websocketUrl?: Types.InputMaybe + unsafe?: Types.InputMaybe }> export type DevSessionCreateMutation = { @@ -46,6 +47,11 @@ export const DevSessionCreate = { variable: {kind: 'Variable', name: {kind: 'Name', value: 'websocketUrl'}}, type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}, }, + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'unsafe'}}, + type: {kind: 'NamedType', name: {kind: 'Name', value: 'Boolean'}}, + }, ], selectionSet: { kind: 'SelectionSet', @@ -69,6 +75,11 @@ export const DevSessionCreate = { name: {kind: 'Name', value: 'websocketUrl'}, value: {kind: 'Variable', name: {kind: 'Name', value: 'websocketUrl'}}, }, + { + kind: 'Argument', + name: {kind: 'Name', value: 'unsafe'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'unsafe'}}, + }, ], selectionSet: { kind: 'SelectionSet', diff --git a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-update.ts b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-update.ts index 84b86c7c51d..9408ea92c9d 100644 --- a/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-update.ts +++ b/packages/app/src/cli/api/graphql/app-dev/generated/dev-session-update.ts @@ -9,6 +9,7 @@ export type DevSessionUpdateMutationVariables = Types.Exact<{ assetsUrl?: Types.InputMaybe manifest?: Types.InputMaybe inheritedModuleUids: Types.Scalars['String']['input'][] | Types.Scalars['String']['input'] + unsafe?: Types.InputMaybe }> export type DevSessionUpdateMutation = { @@ -19,6 +20,7 @@ export type DevSessionUpdateMutation = { user?: {id: string; email?: string | null} | null app: {id: string; key: string} } | null + warnings?: {message: string; code: Types.DevSessionWarningCode}[] | null userErrors: {message: string; on: JsonMapType; field?: string[] | null; category: string}[] } | null } @@ -57,6 +59,11 @@ export const DevSessionUpdate = { }, }, }, + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'unsafe'}}, + type: {kind: 'NamedType', name: {kind: 'Name', value: 'Boolean'}}, + }, ], selectionSet: { kind: 'SelectionSet', @@ -85,6 +92,11 @@ export const DevSessionUpdate = { name: {kind: 'Name', value: 'inheritedModuleUids'}, value: {kind: 'Variable', name: {kind: 'Name', value: 'inheritedModuleUids'}}, }, + { + kind: 'Argument', + name: {kind: 'Name', value: 'unsafe'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'unsafe'}}, + }, ], selectionSet: { kind: 'SelectionSet', @@ -125,6 +137,18 @@ export const DevSessionUpdate = { ], }, }, + { + kind: 'Field', + name: {kind: 'Name', value: 'warnings'}, + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'message'}}, + {kind: 'Field', name: {kind: 'Name', value: 'code'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, { kind: 'Field', name: {kind: 'Name', value: 'userErrors'}, diff --git a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-create.graphql b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-create.graphql index 6c2dec0846b..4f5472d577f 100644 --- a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-create.graphql +++ b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-create.graphql @@ -1,5 +1,5 @@ -mutation DevSessionCreate($appId: String!, $assetsUrl: String!, $websocketUrl: String) { - devSessionCreate(appId: $appId, assetsUrl: $assetsUrl, websocketUrl: $websocketUrl) { +mutation DevSessionCreate($appId: String!, $assetsUrl: String!, $websocketUrl: String, $unsafe: Boolean) { + devSessionCreate(appId: $appId, assetsUrl: $assetsUrl, websocketUrl: $websocketUrl, unsafe: $unsafe) { devSession { websocketUrl updatedAt diff --git a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-update.graphql b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-update.graphql index bf588a7ac50..734d8d6fa12 100644 --- a/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-update.graphql +++ b/packages/app/src/cli/api/graphql/app-dev/queries/dev-session-update.graphql @@ -1,5 +1,5 @@ -mutation DevSessionUpdate($appId: String!, $assetsUrl: String, $manifest: JSON, $inheritedModuleUids: [String!]!) { - devSessionUpdate(appId: $appId, assetsUrl: $assetsUrl, manifest: $manifest, inheritedModuleUids: $inheritedModuleUids) { +mutation DevSessionUpdate($appId: String!, $assetsUrl: String, $manifest: JSON, $inheritedModuleUids: [String!]!, $unsafe: Boolean) { + devSessionUpdate(appId: $appId, assetsUrl: $assetsUrl, manifest: $manifest, inheritedModuleUids: $inheritedModuleUids, unsafe: $unsafe) { devSession { websocketUrl updatedAt @@ -12,6 +12,10 @@ mutation DevSessionUpdate($appId: String!, $assetsUrl: String, $manifest: JSON, key } } + warnings { + message + code + } userErrors { message on diff --git a/packages/app/src/cli/commands/app/dev.test.ts b/packages/app/src/cli/commands/app/dev.test.ts index 26ac10c63ee..4296e3e4bfd 100644 --- a/packages/app/src/cli/commands/app/dev.test.ts +++ b/packages/app/src/cli/commands/app/dev.test.ts @@ -58,7 +58,31 @@ describe('app dev command', () => { tunnelUrl: undefined, localhostPort: undefined, }) - expect(dev).toHaveBeenCalledWith(expect.objectContaining({installMkcert: false, tunnel: {mode: 'auto'}})) + expect(dev).toHaveBeenCalledWith( + expect.objectContaining({installMkcert: false, unsafe: false, tunnel: {mode: 'auto'}}), + ) + }) + }) + + test('passes --unsafe to dev', async () => { + await inTemporaryDirectory(async (tmp) => { + const app = testAppLinked({directory: tmp}) + vi.mocked(linkedAppContext).mockResolvedValue({ + app, + remoteApp: testOrganizationApp(), + organization: testOrganization(), + project: testProject(), + activeConfig: {} as never, + specifications: [], + developerPlatformClient: testDeveloperPlatformClient(), + } as Awaited>) + const store = testOrganizationStore({shopDomain: 'dev-store.myshopify.com'}) + vi.mocked(storeContext).mockResolvedValue(store) + vi.mocked(getTunnelMode).mockResolvedValue({mode: 'auto'}) + + await Dev.run(['--path', tmp, '--store', store.shopDomain, '--unsafe'], import.meta.url) + + expect(dev).toHaveBeenCalledWith(expect.objectContaining({unsafe: true})) }) }) diff --git a/packages/app/src/cli/commands/app/dev.ts b/packages/app/src/cli/commands/app/dev.ts index ee6f217ee97..c382e0a7a48 100644 --- a/packages/app/src/cli/commands/app/dev.ts +++ b/packages/app/src/cli/commands/app/dev.ts @@ -36,6 +36,11 @@ export default class Dev extends AppLinkedCommand { env: 'SHOPIFY_FLAG_NO_UPDATE', default: false, }), + unsafe: Flags.boolean({ + description: 'Skip validations that are unsafe for production, such as the Remote DOM bundle size limit.', + env: 'SHOPIFY_FLAG_UNSAFE', + default: false, + }), 'subscription-product-url': Flags.string({ description: 'Resource URL for subscription UI extension. Format: "/products/{productId}"', env: 'SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL', @@ -137,6 +142,7 @@ export default class Dev extends AppLinkedCommand { store, directory: flags.path, update: !flags['no-update'], + unsafe: flags.unsafe, skipDependenciesInstallation: flags['skip-dependencies-installation'], commandConfig: this.config, subscriptionProductUrl: flags['subscription-product-url'], diff --git a/packages/app/src/cli/services/dev.ts b/packages/app/src/cli/services/dev.ts index 24e0b135bb0..39269a19146 100644 --- a/packages/app/src/cli/services/dev.ts +++ b/packages/app/src/cli/services/dev.ts @@ -50,6 +50,7 @@ export interface DevOptions { store: OrganizationStore directory: string update: boolean + unsafe?: boolean commandConfig: Config skipDependenciesInstallation: boolean subscriptionProductUrl?: string diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts index 0ab4f8645d6..1e2d530781d 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts @@ -419,6 +419,7 @@ describe('pushUpdatesForDevSession', () => { }, // The unaffected extension is listed in inheritedModuleUids inheritedModuleUids: [unaffectedExtension.uid], + unsafe: false, }) }) @@ -530,6 +531,7 @@ describe('pushUpdatesForDevSession', () => { assetsUrl: 'https://gcs.url', manifest: expect.any(Object), inheritedModuleUids: [], + unsafe: false, }) }) @@ -549,6 +551,7 @@ describe('pushUpdatesForDevSession', () => { appId: 'app123', assetsUrl: 'https://gcs.url', websocketUrl: 'wss://test.dev/extensions', + unsafe: false, }) }) diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.ts index 28c6a504850..ca62c48c9f3 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.ts @@ -16,6 +16,7 @@ export interface DevSessionProcessOptions { appWatcher: AppEventWatcher appPreviewURL: string devSessionStatusManager: DevSessionStatusManager + unsafe?: boolean } export interface DevSessionProcess extends BaseProcess { diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts index 6a6c15d68f7..b65201d6801 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts @@ -309,6 +309,7 @@ export class DevSession { assetsUrl: signedURL, manifest, inheritedModuleUids, + unsafe: this.options.unsafe ?? false, } return this.devSessionUpdateWithRetry(payload) } else { @@ -317,6 +318,7 @@ export class DevSession { appId: this.options.appId, assetsUrl: signedURL, websocketUrl, + unsafe: this.options.unsafe ?? false, } return this.devSessionCreateWithRetry(payload) } @@ -430,6 +432,7 @@ export class DevSession { private async devSessionUpdateWithRetry(payload: DevSessionUpdateOptions): Promise { const result = await this.options.developerPlatformClient.devSessionUpdate(payload) const errors = result.devSessionUpdate?.userErrors ?? [] + const warnings = result.devSessionUpdate?.warnings ?? [] const devSession = result.devSessionUpdate?.devSession // Check for session takeover @@ -460,6 +463,15 @@ export class DevSession { } } + if (warnings.length > 0) { + await Promise.all( + warnings.map((warning) => { + const message = warning.code === 'SESSION_TAKEOVER' ? `⚠️ ${warning.message}` : warning.message + return this.logger.warning(message) + }), + ) + } + if (errors.length) return {status: 'remote-error', error: errors} return {status: 'updated'} } diff --git a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts index 1277bb675d0..23d8f8462bc 100644 --- a/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts +++ b/packages/app/src/cli/services/dev/processes/setup-dev-processes.ts @@ -162,6 +162,7 @@ export async function setupDevProcesses({ appWatcher, appPreviewURL: appPreviewUrl, devSessionStatusManager, + unsafe: commandOptions.unsafe, }), await setupPreviewThemeAppExtensionsProcess({ remoteApp, diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index 3a1c04790e2..b90fe18a62e 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -120,12 +120,14 @@ interface DevSessionSharedOptions { export interface DevSessionCreateOptions extends DevSessionSharedOptions { assetsUrl?: string websocketUrl?: string + unsafe?: boolean } export interface DevSessionUpdateOptions extends DevSessionSharedOptions { assetsUrl?: string manifest: AppManifest inheritedModuleUids: string[] + unsafe?: boolean } export type DevSessionDeleteOptions = DevSessionSharedOptions diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index dd868bd532f..94a67b91332 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -989,12 +989,13 @@ export class AppManagementClient implements DeveloperPlatformClient { assetsUrl, shopFqdn, websocketUrl, + unsafe, }: DevSessionCreateOptions): Promise { const appIdNumber = String(numberFromGid(appId)) return this.appDevRequest({ query: DevSessionCreate, shopFqdn, - variables: {appId: appIdNumber, assetsUrl: assetsUrl ?? '', websocketUrl}, + variables: {appId: appIdNumber, assetsUrl: assetsUrl ?? '', websocketUrl, unsafe}, requestOptions: {requestMode: 'slow-request'}, }) } @@ -1005,6 +1006,7 @@ export class AppManagementClient implements DeveloperPlatformClient { shopFqdn, manifest, inheritedModuleUids, + unsafe, }: DevSessionUpdateOptions): Promise { const appIdNumber = String(numberFromGid(appId)) const variables: DevSessionUpdateMutationVariables = { @@ -1012,6 +1014,7 @@ export class AppManagementClient implements DeveloperPlatformClient { assetsUrl, manifest: JSON.stringify(manifest), inheritedModuleUids, + unsafe, } return this.appDevRequest({query: DevSessionUpdate, shopFqdn, variables}) }