Skip to content

Commit 39dbde0

Browse files
committed
Add support for deploy without build
1 parent 817bf4b commit 39dbde0

File tree

12 files changed

+167
-27
lines changed

12 files changed

+167
-27
lines changed

.changeset/little-rockets-serve.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/app': minor
3+
'@shopify/cli': minor
4+
---
5+
6+
Add `--no-build` flag to `shopify app deploy`. When provided, the deploy command will assume you have already run `shopify app build` or otherwise put build files in place.

docs-shopify.dev/commands/interfaces/app-deploy.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export interface appdeploy {
2424
*/
2525
'--message <value>'?: string
2626

27+
/**
28+
* Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.
29+
* @environment SHOPIFY_FLAG_NO_BUILD
30+
*/
31+
'--no-build'?: ''
32+
2733
/**
2834
* Disable color output.
2935
* @environment SHOPIFY_FLAG_NO_COLOR

docs-shopify.dev/generated/generated_docs_data.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,15 @@
322322
"isOptional": true,
323323
"environmentValue": "SHOPIFY_FLAG_MESSAGE"
324324
},
325+
{
326+
"filePath": "docs-shopify.dev/commands/interfaces/app-deploy.interface.ts",
327+
"syntaxKind": "PropertySignature",
328+
"name": "--no-build",
329+
"value": "\"\"",
330+
"description": "Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.",
331+
"isOptional": true,
332+
"environmentValue": "SHOPIFY_FLAG_NO_BUILD"
333+
},
325334
{
326335
"filePath": "docs-shopify.dev/commands/interfaces/app-deploy.interface.ts",
327336
"syntaxKind": "PropertySignature",
@@ -404,7 +413,7 @@
404413
"environmentValue": "SHOPIFY_FLAG_FORCE"
405414
}
406415
],
407-
"value": "export interface appdeploy {\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Deploy without asking for confirmation.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Optional message that will be associated with this version. This is for internal use only and won't be available externally.\n * @environment SHOPIFY_FLAG_MESSAGE\n */\n '--message <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Creates a version but doesn't release it - it's not made available to merchants.\n * @environment SHOPIFY_FLAG_NO_RELEASE\n */\n '--no-release'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * URL associated with the new app version.\n * @environment SHOPIFY_FLAG_SOURCE_CONTROL_URL\n */\n '--source-control-url <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Optional version tag that will be associated with this app version. If not provided, an auto-generated identifier will be generated for this app version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version <value>'?: string\n}"
416+
"value": "export interface appdeploy {\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Deploy without asking for confirmation.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Optional message that will be associated with this version. This is for internal use only and won't be available externally.\n * @environment SHOPIFY_FLAG_MESSAGE\n */\n '--message <value>'?: string\n\n /**\n * Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.\n * @environment SHOPIFY_FLAG_NO_BUILD\n */\n '--no-build'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Creates a version but doesn't release it - it's not made available to merchants.\n * @environment SHOPIFY_FLAG_NO_RELEASE\n */\n '--no-release'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * URL associated with the new app version.\n * @environment SHOPIFY_FLAG_SOURCE_CONTROL_URL\n */\n '--source-control-url <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Optional version tag that will be associated with this app version. If not provided, an auto-generated identifier will be generated for this app version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version <value>'?: string\n}"
408417
}
409418
}
410419
}

packages/app/src/cli/commands/app/deploy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export default class Deploy extends AppLinkedCommand {
4444
env: 'SHOPIFY_FLAG_NO_RELEASE',
4545
default: false,
4646
}),
47+
'no-build': Flags.boolean({
48+
description:
49+
'Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.',
50+
env: 'SHOPIFY_FLAG_NO_BUILD',
51+
default: false,
52+
}),
4753
message: Flags.string({
4854
hidden: false,
4955
description:
@@ -111,6 +117,7 @@ export default class Deploy extends AppLinkedCommand {
111117
message: flags.message,
112118
version: flags.version,
113119
commitReference: flags['source-control-url'],
120+
skipBuild: flags['no-build'],
114121
})
115122

116123
return {app: result.app}

packages/app/src/cli/models/extensions/extension-instance.ts

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {constantize, slugify} from '@shopify/cli-kit/common/string'
2929
import {hashString, nonRandomUUID} from '@shopify/cli-kit/node/crypto'
3030
import {partnersFqdn} from '@shopify/cli-kit/node/context/fqdn'
3131
import {joinPath, basename} from '@shopify/cli-kit/node/path'
32-
import {fileExists, touchFile, moveFile, writeFile, glob} from '@shopify/cli-kit/node/fs'
32+
import {fileExists, touchFile, moveFile, writeFile, glob, copyFile, readFile} from '@shopify/cli-kit/node/fs'
3333
import {getPathValue} from '@shopify/cli-kit/common/object'
3434
import {outputDebug} from '@shopify/cli-kit/node/output'
3535

@@ -44,6 +44,8 @@ export const CONFIG_EXTENSION_IDS: string[] = [
4444
WebhooksSpecIdentifier,
4545
]
4646

47+
type BuildMode = 'theme' | 'function' | 'ui' | 'flow' | 'tax_calculation' | 'none'
48+
4749
/**
4850
* Class that represents an instance of a local extension
4951
* Before creating this class we've validated that:
@@ -335,20 +337,23 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
335337
}
336338

337339
async build(options: ExtensionBuildOptions): Promise<void> {
338-
if (this.isThemeExtension) {
339-
return buildThemeExtension(this, options)
340-
} else if (this.isFunctionExtension) {
341-
return buildFunctionExtension(this, options)
342-
} else if (this.features.includes('esbuild')) {
343-
return buildUIExtension(this, options)
344-
} else if (this.specification.identifier === 'flow_template' && options.environment === 'production') {
345-
return buildFlowTemplateExtension(this, options)
346-
}
347-
348-
// Workaround for tax_calculations because they remote spec NEEDS a valid js file to be included.
349-
if (this.type === 'tax_calculation') {
350-
await touchFile(this.outputPath)
351-
await writeFile(this.outputPath, '(()=>{})();')
340+
const mode = this.buildMode(options)
341+
342+
switch (mode) {
343+
case 'theme':
344+
return buildThemeExtension(this, options)
345+
case 'function':
346+
return buildFunctionExtension(this, options)
347+
case 'ui':
348+
return buildUIExtension(this, options)
349+
case 'flow':
350+
return buildFlowTemplateExtension(this, options)
351+
case 'tax_calculation':
352+
await touchFile(this.outputPath)
353+
await writeFile(this.outputPath, '(()=>{})();')
354+
break
355+
case 'none':
356+
break
352357
}
353358
}
354359

@@ -368,6 +373,35 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
368373
await this.keepBuiltSourcemapsLocally(bundleDirectory, extensionId)
369374
}
370375

376+
async copyIntoBundle(options: ExtensionBuildOptions, bundleDirectory: string, identifiers?: Identifiers) {
377+
const extensionId = this.getOutputFolderId(identifiers?.extensions[this.localIdentifier])
378+
379+
const defaultOutputPath = this.outputPath
380+
381+
if (this.features.includes('bundling')) {
382+
this.outputPath = this.getOutputPathForDirectory(bundleDirectory, extensionId)
383+
}
384+
385+
const buildMode = this.buildMode(options)
386+
387+
if (buildMode !== 'none') {
388+
outputDebug(`Will copy pre-built file from ${defaultOutputPath} to ${this.outputPath}`)
389+
if (await fileExists(defaultOutputPath)) {
390+
await copyFile(defaultOutputPath, this.outputPath)
391+
392+
if (buildMode === 'function') {
393+
outputDebug(`Converting WASM ${this.outputPath} to base64`)
394+
const base64Contents = await readFile(this.outputPath, {encoding: 'base64'})
395+
await writeFile(this.outputPath, base64Contents)
396+
}
397+
}
398+
399+
if (this.isThemeExtension) {
400+
await bundleThemeExtension(this, options)
401+
}
402+
}
403+
}
404+
371405
getOutputFolderId(extensionId?: string) {
372406
return extensionId ?? this.uid ?? this.handle
373407
}
@@ -437,6 +471,25 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
437471
await this.specification.contributeToSharedTypeFile?.(this, typeDefinitionsByFile)
438472
}
439473

474+
private buildMode(options: ExtensionBuildOptions): BuildMode {
475+
if (this.isThemeExtension) {
476+
return 'theme'
477+
} else if (this.isFunctionExtension) {
478+
return 'function'
479+
} else if (this.features.includes('esbuild')) {
480+
return 'ui'
481+
} else if (this.specification.identifier === 'flow_template' && options.environment === 'production') {
482+
return 'flow'
483+
}
484+
485+
// Workaround for tax_calculations because they remote spec NEEDS a valid js file to be included.
486+
if (this.type === 'tax_calculation') {
487+
return 'tax_calculation'
488+
}
489+
490+
return 'none'
491+
}
492+
440493
private buildHandle() {
441494
switch (this.specification.uidStrategy) {
442495
case 'single':

packages/app/src/cli/services/context.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const deployOptions = (app: AppLinkedInterface, reset = false, force = false): D
9595
force,
9696
noRelease: false,
9797
developerPlatformClient: buildDeveloperPlatformClient(),
98+
skipBuild: false,
9899
}
99100
}
100101

@@ -528,6 +529,7 @@ describe('ensureDeployContext', () => {
528529
force: false,
529530
noRelease: false,
530531
developerPlatformClient: buildDeveloperPlatformClient({supportsAtomicDeployments: true}),
532+
skipBuild: false,
531533
}
532534
await ensureDeployContext(options)
533535

@@ -573,6 +575,7 @@ describe('ensureDeployContext', () => {
573575
force: false,
574576
noRelease: false,
575577
developerPlatformClient: buildDeveloperPlatformClient({supportsAtomicDeployments: true}),
578+
skipBuild: false,
576579
}
577580
await ensureDeployContext(options)
578581

packages/app/src/cli/services/deploy.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,5 +548,6 @@ async function testDeployBundle({
548548
version: options?.version,
549549
...(commitReference ? {commitReference} : {}),
550550
developerPlatformClient,
551+
skipBuild: false,
551552
})
552553
}

packages/app/src/cli/services/deploy.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export interface DeployOptions {
4343

4444
/** The git reference url of the app version */
4545
commitReference?: string
46+
47+
/** If true, skip building any elements of the app that require building */
48+
skipBuild: boolean
4649
}
4750

4851
interface TasksContext {
@@ -76,7 +79,7 @@ export async function deploy(options: DeployOptions) {
7679
bundlePath = joinPath(options.app.directory, '.shopify', `deploy-bundle.${developerPlatformClient.bundleFormat}`)
7780
await mkdir(dirname(bundlePath))
7881
}
79-
await bundleAndBuildExtensions({app, bundlePath, identifiers})
82+
await bundleAndBuildExtensions({app, bundlePath, identifiers, skipBuild: options.skipBuild})
8083

8184
let uploadTaskTitle
8285

packages/app/src/cli/services/deploy/bundle.test.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('bundleAndBuildExtensions', () => {
6161
}
6262

6363
// When
64-
await bundleAndBuildExtensions({app, identifiers, bundlePath})
64+
await bundleAndBuildExtensions({app, identifiers, bundlePath, skipBuild: false})
6565

6666
// Then
6767
expect(extensionBundleMock).toHaveBeenCalledTimes(2)
@@ -95,7 +95,38 @@ describe('bundleAndBuildExtensions', () => {
9595
}
9696

9797
// When
98-
await bundleAndBuildExtensions({app, identifiers, bundlePath})
98+
await bundleAndBuildExtensions({app, identifiers, bundlePath, skipBuild: false})
99+
100+
// Then
101+
await expect(file.fileExists(bundlePath)).resolves.toBeTruthy()
102+
})
103+
})
104+
105+
test('skips building extensions if skipBuild is true', async () => {
106+
await file.inTemporaryDirectory(async (tmpDir: string) => {
107+
// Given
108+
const bundlePath = joinPath(tmpDir, 'bundle.zip')
109+
110+
const functionExtension = await testFunctionExtension()
111+
const extensionCopyIntoBundleMock = vi.fn().mockImplementation(async (options, bundleDirectory, identifiers) => {
112+
file.writeFileSync(joinPath(bundleDirectory, 'index.wasm'), '')
113+
})
114+
functionExtension.copyIntoBundle = extensionCopyIntoBundleMock
115+
const app = testApp({allExtensions: [functionExtension], directory: tmpDir})
116+
117+
const extensions: {[key: string]: string} = {}
118+
for (const extension of app.allExtensions) {
119+
extensions[extension.localIdentifier] = extension.localIdentifier
120+
}
121+
const identifiers = {
122+
app: 'app-id',
123+
extensions,
124+
extensionIds: {},
125+
extensionsNonUuidManaged: {},
126+
}
127+
128+
// When
129+
await bundleAndBuildExtensions({app, identifiers, bundlePath, skipBuild: true})
99130

100131
// Then
101132
await expect(file.fileExists(bundlePath)).resolves.toBeTruthy()

packages/app/src/cli/services/deploy/bundle.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface BundleOptions {
1212
app: AppInterface
1313
bundlePath?: string
1414
identifiers?: Identifiers
15+
skipBuild: boolean
1516
}
1617

1718
export async function bundleAndBuildExtensions(options: BundleOptions) {
@@ -23,18 +24,28 @@ export async function bundleAndBuildExtensions(options: BundleOptions) {
2324

2425
// Force the download of the javy binary in advance to avoid later problems,
2526
// as it might be done multiple times in parallel. https://github.com/Shopify/cli/issues/2877
26-
await installJavy(options.app)
27+
if (!options.skipBuild) {
28+
await installJavy(options.app)
29+
}
2730

2831
await renderConcurrent({
2932
processes: options.app.allExtensions.map((extension) => {
3033
return {
3134
prefix: extension.localIdentifier,
3235
action: async (stdout: Writable, stderr: Writable, signal: AbortSignal) => {
33-
await extension.buildForBundle(
34-
{stderr, stdout, signal, app: options.app, environment: 'production'},
35-
bundleDirectory,
36-
options.identifiers,
37-
)
36+
if (options.skipBuild) {
37+
await extension.copyIntoBundle(
38+
{stderr, stdout, signal, app: options.app, environment: 'production'},
39+
bundleDirectory,
40+
options.identifiers,
41+
)
42+
} else {
43+
await extension.buildForBundle(
44+
{stderr, stdout, signal, app: options.app, environment: 'production'},
45+
bundleDirectory,
46+
options.identifiers,
47+
)
48+
}
3849
},
3950
}
4051
}),

0 commit comments

Comments
 (0)