Skip to content
Merged
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
151 changes: 150 additions & 1 deletion packages/app/src/cli/services/deploy/bundle.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {bundleAndBuildExtensions} from './bundle.js'
import {testApp, testFunctionExtension, testThemeExtensions, testUIExtension} from '../../models/app/app.test-data.js'
import {AppInterface, AppManifest} from '../../models/app/app.js'
import {AppInterface, AppManifest, WebType} from '../../models/app/app.js'
import * as bundle from '../bundle.js'
import * as functionBuild from '../function/build.js'
import * as webService from '../web.js'
import {describe, expect, test, vi} from 'vitest'
import * as file from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'

vi.mock('../function/build.js')
vi.mock('../web.js')

describe('bundleAndBuildExtensions', () => {
let app: AppInterface
Expand Down Expand Up @@ -254,6 +256,153 @@ describe('bundleAndBuildExtensions', () => {
})
})

test('runs web build command concurrently with extensions when build command is defined', async () => {
await file.inTemporaryDirectory(async (tmpDir: string) => {
// Given
const bundlePath = joinPath(tmpDir, 'bundle.zip')
const mockBuildWeb = vi.mocked(webService.default)

const functionExtension = await testFunctionExtension()
const extensionBuildMock = vi.fn().mockImplementation(async (options, bundleDirectory) => {
file.writeFileSync(joinPath(bundleDirectory, 'index.wasm'), '')
})
functionExtension.buildForBundle = extensionBuildMock

const app = testApp({
allExtensions: [functionExtension],
directory: tmpDir,
webs: [
{
directory: '/tmp/web',
configuration: {
roles: [WebType.Backend],
commands: {dev: 'npm run dev', build: 'npm run build'},
},
},
],
})

const identifiers = {
app: 'app-id',
extensions: {[functionExtension.localIdentifier]: functionExtension.localIdentifier},
extensionIds: {},
extensionsNonUuidManaged: {},
}
appManifest = await app.manifest(identifiers)

// When
await bundleAndBuildExtensions({
app,
appManifest,
identifiers,
bundlePath,
skipBuild: false,
isDevDashboardApp: false,
})

// Then
expect(mockBuildWeb).toHaveBeenCalledWith('build', expect.objectContaining({web: app.webs[0]}))
})
})

test('skips web build for webs without a build command defined', async () => {
await file.inTemporaryDirectory(async (tmpDir: string) => {
// Given
const bundlePath = joinPath(tmpDir, 'bundle.zip')
const mockBuildWeb = vi.mocked(webService.default)

const functionExtension = await testFunctionExtension()
const extensionBuildMock = vi.fn().mockImplementation(async (options, bundleDirectory) => {
file.writeFileSync(joinPath(bundleDirectory, 'index.wasm'), '')
})
functionExtension.buildForBundle = extensionBuildMock

const app = testApp({
allExtensions: [functionExtension],
directory: tmpDir,
webs: [
{
directory: '/tmp/web',
configuration: {
roles: [WebType.Backend],
commands: {dev: 'npm run dev'},
},
},
],
})

const identifiers = {
app: 'app-id',
extensions: {[functionExtension.localIdentifier]: functionExtension.localIdentifier},
extensionIds: {},
extensionsNonUuidManaged: {},
}
appManifest = await app.manifest(identifiers)

// When
await bundleAndBuildExtensions({
app,
appManifest,
identifiers,
bundlePath,
skipBuild: false,
isDevDashboardApp: false,
})

// Then
expect(mockBuildWeb).not.toHaveBeenCalled()
})
})

test('skips web build command when skipBuild is true', async () => {
await file.inTemporaryDirectory(async (tmpDir: string) => {
// Given
const bundlePath = joinPath(tmpDir, 'bundle.zip')
const mockBuildWeb = vi.mocked(webService.default)

const functionExtension = await testFunctionExtension()
const extensionCopyMock = vi.fn().mockImplementation(async (options, bundleDirectory) => {
file.writeFileSync(joinPath(bundleDirectory, 'index.wasm'), '')
})
functionExtension.copyIntoBundle = extensionCopyMock

const app = testApp({
allExtensions: [functionExtension],
directory: tmpDir,
webs: [
{
directory: '/tmp/web',
configuration: {
roles: [WebType.Backend],
commands: {dev: 'npm run dev', build: 'npm run build'},
},
},
],
})

const identifiers = {
app: 'app-id',
extensions: {[functionExtension.localIdentifier]: functionExtension.localIdentifier},
extensionIds: {},
extensionsNonUuidManaged: {},
}
appManifest = await app.manifest(identifiers)

// When
await bundleAndBuildExtensions({
app,
appManifest,
identifiers,
bundlePath,
skipBuild: true,
isDevDashboardApp: false,
})

// Then
expect(mockBuildWeb).not.toHaveBeenCalled()
})
})

test('handles multiple extension types together', async () => {
await file.inTemporaryDirectory(async (tmpDir: string) => {
// Given
Expand Down
63 changes: 38 additions & 25 deletions packages/app/src/cli/services/deploy/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {AppInterface, AppManifest} from '../../models/app/app.js'
import {Identifiers} from '../../models/app/identifiers.js'
import {installJavy} from '../function/build.js'
import buildWeb from '../web.js'
import {compressBundle, writeManifestToBundle} from '../bundle.js'
import {AbortSignal} from '@shopify/cli-kit/node/abort'
import {mkdir, rmdir} from '@shopify/cli-kit/node/fs'
Expand Down Expand Up @@ -30,33 +31,45 @@ export async function bundleAndBuildExtensions(options: BundleOptions) {
await installJavy(options.app)
}

await renderConcurrent({
processes: options.app.allExtensions.map((extension) => {
return {
prefix: extension.localIdentifier,
action: async (stdout: Writable, stderr: Writable, signal: AbortSignal) => {
// This outputId is the UID for AppManagement, and UUID for Partners
// Comes from the matching logic in `ensureDeployContext`
const outputId = options.isDevDashboardApp
? undefined
: options.identifiers?.extensions[extension.localIdentifier]
const webBuildProcesses = options.skipBuild
? []
: options.app.webs
.filter((web) => web.configuration.commands.build)
.map((web) => ({
prefix: ['web', ...web.configuration.roles].join('-'),
action: async (stdout: Writable, stderr: Writable, signal: AbortSignal) => {
if (options.skipBuild) return
await buildWeb('build', {web, stdout, stderr, signal})
Comment thread
MitchLillie marked this conversation as resolved.
},
}))

const extensionBuildProcesses = options.app.allExtensions.map((extension) => ({
prefix: extension.localIdentifier,
action: async (stdout: Writable, stderr: Writable, signal: AbortSignal) => {
// This outputId is the UID for AppManagement, and UUID for Partners
// Comes from the matching logic in `ensureDeployContext`
const outputId = options.isDevDashboardApp
? undefined
: options.identifiers?.extensions[extension.localIdentifier]

if (options.skipBuild) {
await extension.copyIntoBundle(
{stderr, stdout, signal, app: options.app, environment: 'production'},
bundleDirectory,
outputId,
)
} else {
await extension.buildForBundle(
{stderr, stdout, signal, app: options.app, environment: 'production'},
bundleDirectory,
outputId,
)
}
},
if (options.skipBuild) {
await extension.copyIntoBundle(
{stderr, stdout, signal, app: options.app, environment: 'production'},
bundleDirectory,
outputId,
)
} else {
await extension.buildForBundle(
{stderr, stdout, signal, app: options.app, environment: 'production'},
bundleDirectory,
outputId,
)
}
}),
},
}))

await renderConcurrent({
processes: [webBuildProcesses, extensionBuildProcesses].flat(),
showTimestamps: false,
})

Expand Down
Loading