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
42 changes: 11 additions & 31 deletions nx.json
Original file line number Diff line number Diff line change
@@ -1,41 +1,25 @@
{
"targetDefaults": {
"clean": {
"dependsOn": [
"^clean"
]
"dependsOn": ["^clean"]
},
"build": {
"dependsOn": [
"^build"
],
"inputs": [
"production",
"^production"
]
"dependsOn": ["^build"],
"inputs": ["production", "^production"]
},
"refresh-manifests": {
"dependsOn": [
"build",
"refresh-readme"
]
"dependsOn": ["build", "refresh-readme"]
},
"refresh-readme": {
"dependsOn": [
"build"
]
"dependsOn": ["build"]
},
"lint": {},
"lint:fix": {},
"type-check": {
"dependsOn": [
"^build"
]
"dependsOn": ["^build"]
},
"bundle": {
"dependsOn": [
"build"
]
"dependsOn": ["build"]
}
},
"extends": "@nx/workspace/presets/npm.json",
Expand Down Expand Up @@ -65,16 +49,12 @@
"defaultBase": "main",
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"namedInputs": {
"default": [
"{projectRoot}/**/*",
"sharedGlobals"
],
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"sharedGlobals": [],
"production": [
"default"
]
"production": ["default"]
},
"tui": {
"autoExit": true
}
},
"analytics": true
}
4 changes: 2 additions & 2 deletions packages/app/src/cli/services/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {installAppDependencies} from './dependencies.js'
import {installJavy} from './function/build.js'
import {AppInterface, Web} from '../models/app/app.js'
import {Project} from '../models/project/project.js'
import {renderConcurrent, renderSuccess} from '@shopify/cli-kit/node/ui'
import {renderConcurrentRL, renderSuccess} from '@shopify/cli-kit/node/ui'
import {AbortSignal} from '@shopify/cli-kit/node/abort'
import {Writable} from 'stream'

Expand All @@ -28,7 +28,7 @@ async function build(options: BuildOptions) {
// as it might be done multiple times in parallel. https://github.com/Shopify/cli/issues/2877
await installJavy(options.app)

await renderConcurrent({
await renderConcurrentRL({
processes: [
...options.app.webs.map((web: Web) => {
return {
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/cli/services/deploy/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {compressBundle, writeManifestToBundle} from '../bundle.js'
import {AbortSignal} from '@shopify/cli-kit/node/abort'
import {mkdir, rmdir} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'
import {renderConcurrent} from '@shopify/cli-kit/node/ui'
import {renderConcurrentRL} from '@shopify/cli-kit/node/ui'
import {Writable} from 'stream'

interface BundleOptions {
Expand All @@ -30,7 +30,7 @@ export async function bundleAndBuildExtensions(options: BundleOptions) {
await installJavy(options.app)
}

await renderConcurrent({
await renderConcurrentRL({
processes: options.app.allExtensions.map((extension) => {
return {
prefix: extension.localIdentifier,
Expand Down
10 changes: 4 additions & 6 deletions packages/app/src/cli/services/dev/ui.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {renderDev} from './ui.js'
import {Dev} from './ui/components/Dev.js'
import {DevSessionUI} from './ui/components/DevSessionUI.js'
import {renderDevSessionUI} from './ui/components/DevSessionUI.js'
import {DevSessionStatusManager} from './processes/dev-session/dev-session-status-manager.js'
import {testDeveloperPlatformClient} from '../../models/app/app.test-data.js'
import {afterEach, describe, expect, test, vi} from 'vitest'
Expand Down Expand Up @@ -237,15 +237,13 @@ describe('ui', () => {

await new Promise((resolve) => setTimeout(resolve, 10))

expect(vi.mocked(DevSessionUI)).toHaveBeenCalledWith(
expect(vi.mocked(renderDevSessionUI)).toHaveBeenCalledWith(
expect.objectContaining({
processes,
abortController,
devSessionStatusManager,
onAbort: expect.any(Function),
}),
// React 19 no longer passes legacy context as second argument
undefined,
)
expect(vi.mocked(Dev)).not.toHaveBeenCalled()
})
Expand Down Expand Up @@ -288,8 +286,8 @@ describe('ui', () => {

await new Promise((resolve) => setTimeout(resolve, 10))

// Get the onAbort callback that was passed to DevSessionUI
const onAbort = vi.mocked(DevSessionUI).mock.calls[0]?.[0]?.onAbort
// Get the onAbort callback that was passed to renderDevSessionUI
const onAbort = vi.mocked(renderDevSessionUI).mock.calls[0]?.[0]?.onAbort
await onAbort?.()

expect(app.developerPlatformClient.devSessionDelete).toHaveBeenCalledWith({
Expand Down
31 changes: 13 additions & 18 deletions packages/app/src/cli/services/dev/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Dev, DevProps} from './ui/components/Dev.js'
import {DevSessionUI} from './ui/components/DevSessionUI.js'
import {renderDevSessionUI} from './ui/components/DevSessionUI.js'
import {DevSessionStatusManager} from './processes/dev-session/dev-session-status-manager.js'
import React from 'react'
import {render} from '@shopify/cli-kit/node/ui'
Expand Down Expand Up @@ -31,24 +31,19 @@ export async function renderDev({
if (!terminalSupportsPrompting()) {
await renderDevNonInteractive({processes, app, abortController, developerPreview, shopFqdn})
} else if (app.developerPlatformClient.supportsDevSessions) {
return render(
<DevSessionUI
processes={processes}
abortController={abortController}
devSessionStatusManager={devSessionStatusManager}
shopFqdn={shopFqdn}
appURL={appURL}
appName={appName}
organizationName={organizationName}
configPath={configPath}
onAbort={async () => {
await app.developerPlatformClient.devSessionDelete({appId: app.id, shopFqdn})
}}
/>,
{
exitOnCtrlC: false,
return renderDevSessionUI({
processes,
abortController,
devSessionStatusManager,
shopFqdn,
appURL,
appName,
organizationName,
configPath,
onAbort: async () => {
await app.developerPlatformClient.devSessionDelete({appId: app.id, shopFqdn})
},
)
})
} else {
return render(
<Dev
Expand Down
133 changes: 133 additions & 0 deletions packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {renderDevSessionUI} from './DevSessionUI.js'
import {DevSessionStatus, DevSessionStatusManager} from '../../processes/dev-session/dev-session-status-manager.js'
import {AbortController} from '@shopify/cli-kit/node/abort'
import {beforeEach, describe, expect, test, vi} from 'vitest'
import {openURL} from '@shopify/cli-kit/node/system'
import {Writable} from 'stream'

vi.mock('@shopify/cli-kit/node/system')
vi.mock('@shopify/cli-kit/node/context/local')
vi.mock('@shopify/cli-kit/node/tree-kill')

let devSessionStatusManager: DevSessionStatusManager

const initialStatus: DevSessionStatus = {
isReady: true,
previewURL: 'https://shopify.com',
graphiqlURL: 'https://graphiql.shopify.com',
}

const onAbort = vi.fn()

/** Collects everything written to a writable into a single string. */
function createCapture(): {stream: NodeJS.WritableStream; text: () => string} {
const chunks: string[] = []
const stream = new Writable({
write(chunk, _encoding, cb) {
chunks.push(chunk.toString('utf8'))
cb()
},
}) as unknown as NodeJS.WritableStream

// Add columns property so the status bar can calculate width
Object.defineProperty(stream, 'columns', {value: 120})

return {
stream,
text: () => chunks.join(''),
}
}

/** Strip ANSI escape codes for easier assertions. */
function stripAnsi(str: string): string {
// eslint-disable-next-line no-control-regex
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\]8;;[^\x07]*\x07/g, '')
}

describe('DevSessionUI', () => {
beforeEach(() => {
devSessionStatusManager = new DevSessionStatusManager()
devSessionStatusManager.reset()
devSessionStatusManager.updateStatus(initialStatus)
onAbort.mockReset()
})

test('renders process output and status bar with URLs', async () => {
const capture = createCapture()
const abortController = new AbortController()

const backendProcess = {
prefix: 'backend',
action: async (stdout: Writable, _stderr: Writable) => {
stdout.write('first backend message')
stdout.write('second backend message')
},
}

// Start rendering — it blocks until abort
const promise = renderDevSessionUI({
processes: [backendProcess],
abortController,
devSessionStatusManager,
shopFqdn: 'mystore.myshopify.com',
onAbort,
// @ts-expect-error - using capture stream
_testOutput: capture.stream,
})

// Give processes time to write
await new Promise((r) => setTimeout(r, 50))

abortController.abort()
await promise

const output = stripAnsi(capture.text())
expect(output).toContain('backend')
expect(output).toContain('first backend message')
expect(output).toContain('second backend message')
})

test('calls onAbort when aborted before dev preview is ready', async () => {
const abortController = new AbortController()
devSessionStatusManager.updateStatus({isReady: false})

const promise = renderDevSessionUI({
processes: [],
abortController,
devSessionStatusManager,
shopFqdn: 'mystore.myshopify.com',
onAbort,
})

// Give a tick for setup
await new Promise((r) => setTimeout(r, 10))

abortController.abort()
await promise

expect(onAbort).toHaveBeenCalledOnce()
})

test('handles process errors by aborting', async () => {
const abortController = new AbortController()
const abort = vi.spyOn(abortController, 'abort')
const errorProcess = {
prefix: 'error',
action: async () => {
throw new Error('Test error')
},
}

const promise = renderDevSessionUI({
processes: [errorProcess],
abortController,
devSessionStatusManager,
shopFqdn: 'mystore.myshopify.com',
onAbort,
})

await promise

expect(abort).toHaveBeenCalledWith(new Error('Test error'))
})
})
Loading
Loading