Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bundle-size-exception-cta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/app': minor
'@shopify/cli': minor
---

Point at the bundle size exception request flow from bundle-size deploy errors
6 changes: 6 additions & 0 deletions .changeset/deploy-archive-metafiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/app': minor
'@shopify/cli': minor
---

Include esbuild metafiles in the app deploy archive to enable bundle-composition analysis
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import * as Types from './types.js'
import {JsonMapType} from '@shopify/cli-kit/node/toml'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'

export type BundleSizeExceptionRequestMutationVariables = Types.Exact<{
appId: Types.Scalars['ID']['input']
reason: Types.Scalars['String']['input']
extensions: Types.UiExtensionSizeContextInput[] | Types.UiExtensionSizeContextInput
}>

export type BundleSizeExceptionRequestMutation = {
appUiExtensionBundleSizeExceptionRequest: {
exception?: {status: Types.UiExtensionBundleSizeExceptionStatus; effectiveLimitKb: number} | null
userErrors: {
field?: string[] | null
message: string
category: string
code?: Types.Code | null
on: JsonMapType
}[]
}
}

export const BundleSizeExceptionRequest = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'mutation',
name: {kind: 'Name', value: 'BundleSizeExceptionRequest'},
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'reason'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'extensions'}},
type: {
kind: 'NonNullType',
type: {
kind: 'ListType',
type: {
kind: 'NonNullType',
type: {kind: 'NamedType', name: {kind: 'Name', value: 'UiExtensionSizeContextInput'}},
},
},
},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'appUiExtensionBundleSizeExceptionRequest'},
arguments: [
{
kind: 'Argument',
name: {kind: 'Name', value: 'appId'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'reason'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'reason'}},
},
{
kind: 'Argument',
name: {kind: 'Name', value: 'extensions'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'extensions'}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'exception'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'status'}},
{kind: 'Field', name: {kind: 'Name', value: 'effectiveLimitKb'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{
kind: 'Field',
name: {kind: 'Name', value: 'userErrors'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'field'}},
{kind: 'Field', name: {kind: 'Name', value: 'message'}},
{kind: 'Field', name: {kind: 'Name', value: 'category'}},
{kind: 'Field', name: {kind: 'Name', value: 'code'}},
{kind: 'Field', name: {kind: 'Name', value: 'on'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
],
},
},
],
} as unknown as DocumentNode<BundleSizeExceptionRequestMutation, BundleSizeExceptionRequestMutationVariables>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import * as Types from './types.js'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'

export type BundleSizeExceptionStatusQueryVariables = Types.Exact<{
appId: Types.Scalars['ID']['input']
}>

export type BundleSizeExceptionStatusQuery = {
app: {
id: string
uiExtensionBundleSizeException: {status: Types.UiExtensionBundleSizeExceptionStatus; effectiveLimitKb: number}
}
}

export const BundleSizeExceptionStatus = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: {kind: 'Name', value: 'BundleSizeExceptionStatus'},
variableDefinitions: [
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {kind: 'Name', value: 'app'},
arguments: [
{
kind: 'Argument',
name: {kind: 'Name', value: 'id'},
value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}},
},
],
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'id'}},
{
kind: 'Field',
name: {kind: 'Name', value: 'uiExtensionBundleSizeException'},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: 'status'}},
{kind: 'Field', name: {kind: 'Name', value: 'effectiveLimitKb'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
],
},
},
],
} as unknown as DocumentNode<BundleSizeExceptionStatusQuery, BundleSizeExceptionStatusQueryVariables>
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,33 @@ export type SourceExtension =
/** ZIP file extension. */
| 'ZIP';

/** The review status of an app's bundle size exception for Remote-DOM UI extensions. */
export type UiExtensionBundleSizeExceptionStatus =
/** The most recent exception request was denied or a granted exception was revoked. */
| 'DENIED'
/** An exception has been granted. */
| 'GRANTED'
/** No exception has been requested for this app. */
| 'NONE'
/** An exception request is awaiting review by Shopify. */
| 'PENDING';

/** Machine-collected size context for one Remote-DOM UI extension, used to review a bundle size exception request. */
export type UiExtensionSizeContextInput = {
/** The API version of the extension. */
apiVersion: Scalars['String']['input'];
/** The compressed (deflate) bundle size in KB, measured with the same algorithm the platform enforces. */
compressedKb: Scalars['Int']['input'];
/** The handle of the extension. */
handle: Scalars['String']['input'];
/** The uncompressed bundle size in KB. */
rawKb: Scalars['Int']['input'];
/** The extension targets declared by the extension. */
targets?: InputMaybe<Array<Scalars['String']['input']>>;
/** The module uid of the extension. */
uid: Scalars['String']['input'];
};

/** The input fields for app version metadata. */
export type VersionMetadataInput = {
/** Message associated with this app version. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
mutation BundleSizeExceptionRequest($appId: ID!, $reason: String!, $extensions: [UiExtensionSizeContextInput!]!) {
appUiExtensionBundleSizeExceptionRequest(appId: $appId, reason: $reason, extensions: $extensions) {
exception {
status
effectiveLimitKb
}
userErrors {
field
message
category
code
on
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query BundleSizeExceptionStatus($appId: ID!) {
app(id: $appId) {
id
uiExtensionBundleSizeException {
status
effectiveLimitKb
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {appFlags} from '../../../flags.js'
import {linkedAppContext} from '../../../services/app-context.js'
import {requestBundleSizeException} from '../../../services/bundle-size-exception.js'
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {Flags} from '@oclif/core'

export default class BundleSizeExceptionRequest extends AppLinkedCommand {
static hidden = true

static summary = "Request a bundle size exception for the app's Remote-DOM UI extensions."

static descriptionWithMarkdown = `Remote-DOM UI extension bundles (API version 2025-10 or later) are limited to 64 KB (compressed). If you can't reduce your bundle below the limit, you can ask Shopify for a bundle size exception.

The command measures each Remote-DOM UI extension bundle locally (using the same compression the platform enforces), collects your justification, and submits the request. Shopify reviews every request; once approved, \`shopify app deploy\` succeeds with no further changes.`

static description = this.descriptionWithoutMarkdown()

static flags = {
...globalFlags,
...appFlags,
reason: Flags.string({
description:
"Why the bundle can't be reduced below the current limit. When provided, the command runs non-interactively.",
env: 'SHOPIFY_FLAG_BUNDLE_SIZE_EXCEPTION_REASON',
}),
}

public async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(BundleSizeExceptionRequest)

const appContextResult = await linkedAppContext({
directory: flags.path,
clientId: flags['client-id'],
forceRelink: flags.reset,
userProvidedConfigName: flags.config,
})

await requestBundleSizeException({appContextResult, reason: flags.reason})

return {app: appContextResult.app}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {appFlags} from '../../../flags.js'
import {linkedAppContext} from '../../../services/app-context.js'
import {bundleSizeExceptionStatus} from '../../../services/bundle-size-exception.js'
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
import {globalFlags} from '@shopify/cli-kit/node/cli'

export default class BundleSizeExceptionStatus extends AppLinkedCommand {
static hidden = true

static summary = "Check the state of the app's bundle size exception request."

static descriptionWithMarkdown = `Shows whether the app has a Remote-DOM UI extension bundle size exception, a request pending review, or a denied request, along with the app's current bundle size limit.

Use \`shopify app bundle-size-exception request\` to request an exception.`

static description = this.descriptionWithoutMarkdown()

static flags = {
...globalFlags,
...appFlags,
}

public async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(BundleSizeExceptionStatus)

const appContextResult = await linkedAppContext({
directory: flags.path,
clientId: flags['client-id'],
forceRelink: flags.reset,
userProvidedConfigName: flags.config,
})

await bundleSizeExceptionStatus({appContextResult})

return {app: appContextResult.app}
}
}
4 changes: 4 additions & 0 deletions packages/app/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Build from './commands/app/build.js'
import BulkCancel from './commands/app/bulk/cancel.js'
import BulkStatus from './commands/app/bulk/status.js'
import BundleSizeExceptionRequest from './commands/app/bundle-size-exception/request.js'
import BundleSizeExceptionStatus from './commands/app/bundle-size-exception/status.js'
import ConfigLink from './commands/app/config/link.js'
import ConfigUse from './commands/app/config/use.js'
import ConfigPull from './commands/app/config/pull.js'
Expand Down Expand Up @@ -45,6 +47,8 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin
'app:build': Build,
'app:bulk:cancel': BulkCancel,
'app:bulk:status': BulkStatus,
'app:bundle-size-exception:request': BundleSizeExceptionRequest,
'app:bundle-size-exception:status': BundleSizeExceptionStatus,
'app:deploy': Deploy,
'app:dev': Dev,
'app:dev:clean': DevClean,
Expand Down
11 changes: 11 additions & 0 deletions packages/app/src/cli/models/app/app.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
DevSessionDeleteOptions,
DevSessionUpdateOptions,
Store,
UiExtensionBundleSizeExceptionRequestOptions,
} from '../../utilities/developer-platform-client.js'
import {AllAppExtensionRegistrationsQuerySchema} from '../../api/graphql/all_app_extension_registrations.js'
import {AppDeploySchema, AppDeployVariables} from '../../api/graphql/app_deploy.js'
Expand Down Expand Up @@ -1370,6 +1371,16 @@ export function testDeveloperPlatformClient(
Promise.resolve(
`Looks like you don't have any dev stores associated with ${org.businessName}'s Partner Dashboard. Create a store in Partner Dashboard https://partners.shopify.com/1234/stores`,
),
uiExtensionBundleSizeException: (_app: MinimalAppIdentifiers) =>
Promise.resolve({status: 'NONE' as const, effectiveLimitKb: 64}),
uiExtensionBundleSizeExceptionRequest: (
_app: MinimalAppIdentifiers,
_options: UiExtensionBundleSizeExceptionRequestOptions,
) =>
Promise.resolve({
exception: {status: 'PENDING' as const, effectiveLimitKb: 64},
userErrors: [],
}),
...stubs,
}
const retVal: Partial<TestDeveloperPlatformClient> = clientStub
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ async function validateUIExtensionPointConfig(
return ok({})
}

function isRemoteDomExtension(
export function isRemoteDomExtension(
config: ExtensionInstance['configuration'],
): config is ExtensionInstance<{api_version: string}>['configuration'] {
const apiVersion = config.api_version
Expand Down
Loading
Loading