Skip to content

Commit 1b9c1ff

Browse files
committed
ai dev server status
1 parent 8fa8036 commit 1b9c1ff

13 files changed

+187
-12
lines changed

packages/app/src/cli/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const blocks = {
4242
export const ports = {
4343
graphiql: 3457,
4444
localhost: 3458,
45+
devStatusServer: 3459,
4546
} as const
4647

4748
export const EsbuildEnvVarRegex = /^([a-zA-Z_$])([a-zA-Z0-9_$])*$/

packages/app/src/cli/services/dev/app-events/app-event-watcher.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,16 @@ export class AppEventWatcher extends EventEmitter {
204204
return this
205205
}
206206

207+
/**
208+
* Get the current app instance.
209+
* This will be the latest version of the app after any reloads due to configuration changes.
210+
*
211+
* @returns The current app instance
212+
*/
213+
get currentApp(): AppLinkedInterface {
214+
return this.app
215+
}
216+
207217
onError(listener: (error: Error) => Promise<void> | void) {
208218
// eslint-disable-next-line @typescript-eslint/no-misused-promises
209219
this.addListener('error', listener)

packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {DevSession} from './dev-session.js'
33
import {BaseProcess, DevProcessFunction} from '../types.js'
44
import {DeveloperPlatformClient} from '../../../../utilities/developer-platform-client.js'
55
import {AppLinkedInterface} from '../../../../models/app/app.js'
6-
import {AppEventWatcher} from '../../app-events/app-event-watcher.js'
6+
import {AppEvent, AppEventWatcher} from '../../app-events/app-event-watcher.js'
7+
import {SerialBatchProcessor} from '@shopify/cli-kit/node/serial-batch-processor'
78

89
export interface DevSessionProcessOptions {
910
developerPlatformClient: DeveloperPlatformClient
@@ -17,6 +18,7 @@ export interface DevSessionProcessOptions {
1718
appPreviewURL: string
1819
appLocalProxyURL: string
1920
devSessionStatusManager: DevSessionStatusManager
21+
appEventsProcessor: SerialBatchProcessor<AppEvent>
2022
}
2123

2224
export interface DevSessionProcess extends BaseProcess<DevSessionProcessOptions> {

packages/app/src/cli/services/dev/processes/dev-session/dev-session-status-manager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface DevSessionStatus {
1818
previewURL?: string
1919
graphiqlURL?: string
2020
statusMessage?: {message: string; type: DevSessionStatusMessageType}
21+
logs: {timestamp: string; message: string; prefix?: string}[]
2122
}
2223

2324
export class DevSessionStatusManager extends EventEmitter {
@@ -26,8 +27,11 @@ export class DevSessionStatusManager extends EventEmitter {
2627
previewURL: undefined,
2728
graphiqlURL: undefined,
2829
statusMessage: undefined,
30+
logs: [],
2931
}
3032

33+
private readonly _logs: {timestamp: number; message: string; prefix?: string}[] = []
34+
3135
constructor(defaultStatus?: DevSessionStatus) {
3236
super()
3337
if (defaultStatus) this.currentStatus = defaultStatus
@@ -42,6 +46,10 @@ export class DevSessionStatusManager extends EventEmitter {
4246
this.emit('dev-session-update', newStatus)
4347
}
4448

49+
addLog(log: {timestamp: number; message: string; prefix?: string}) {
50+
this._logs.push(log)
51+
}
52+
4553
setMessage(message: keyof typeof DevSessionStaticMessages) {
4654
this.updateStatus({statusMessage: DevSessionStaticMessages[message]})
4755
}
@@ -50,12 +58,17 @@ export class DevSessionStatusManager extends EventEmitter {
5058
return this.currentStatus
5159
}
5260

61+
get logs(): {timestamp: number; message: string; prefix?: string}[] {
62+
return this._logs
63+
}
64+
5365
reset() {
5466
this.currentStatus = {
5567
isReady: false,
5668
previewURL: undefined,
5769
graphiqlURL: undefined,
5870
statusMessage: undefined,
71+
logs: [],
5972
}
6073
}
6174
}

packages/app/src/cli/services/dev/processes/dev-session/dev-session.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export class DevSession {
4949
this.options = processOptions
5050
this.appWatcher = processOptions.appWatcher
5151
this.bundlePath = processOptions.appWatcher.buildOutputPath
52-
this.appEventsProcessor = new SerialBatchProcessor((events: AppEvent[]) => this.processEvents(events))
52+
this.appEventsProcessor = processOptions.appEventsProcessor
53+
this.appEventsProcessor.processBatch = (events: AppEvent[]) => this.processEvents(events)
5354
}
5455

5556
private async start() {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {setupDevStatusServerProcess, launchDevStatusServer} from './dev-status-server.js'
2+
import {DevSessionStatusManager} from './dev-session/dev-session-status-manager.js'
3+
import {testAppLinked} from '../../../models/app/app.test-data.js'
4+
import {AppEventWatcher} from '../app-events/app-event-watcher.js'
5+
import {describe, expect, test} from 'vitest'
6+
7+
describe('dev-status-server', () => {
8+
test('setupDevStatusServerProcess returns the correct process definition', async () => {
9+
// Given
10+
const devSessionStatusManager = new DevSessionStatusManager()
11+
const localApp = await testAppLinked()
12+
const appWatcher = new AppEventWatcher(localApp)
13+
const options = {
14+
devSessionStatusManager,
15+
localApp,
16+
appWatcher,
17+
graphiqlUrl: 'http://localhost:3000/graphiql',
18+
}
19+
20+
// When
21+
const process = await setupDevStatusServerProcess(options)
22+
23+
// Then
24+
expect(process).toMatchObject({
25+
type: 'dev-status-server',
26+
prefix: 'status',
27+
function: launchDevStatusServer,
28+
options,
29+
})
30+
})
31+
})
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {BaseProcess, DevProcessFunction} from './types.js'
2+
import {DevSessionStatusManager} from './dev-session/dev-session-status-manager.js'
3+
import {AppLinkedInterface} from '../../../models/app/app.js'
4+
import {AppEvent, AppEventWatcher} from '../app-events/app-event-watcher.js'
5+
import {ports} from '../../../constants.js'
6+
import {createApp, createRouter, defineEventHandler} from 'h3'
7+
import {outputInfo} from '@shopify/cli-kit/node/output'
8+
import {getAvailableTCPPort} from '@shopify/cli-kit/node/tcp'
9+
import {SerialBatchProcessor} from '@shopify/cli-kit/node/serial-batch-processor'
10+
import {createServer} from 'http'
11+
12+
interface DevStatusServerProcessOptions {
13+
devSessionStatusManager: DevSessionStatusManager
14+
localApp: AppLinkedInterface
15+
appWatcher: AppEventWatcher
16+
graphiqlUrl?: string
17+
appEventsProcessor: SerialBatchProcessor<AppEvent>
18+
}
19+
20+
export interface DevStatusServerProcess extends BaseProcess<DevStatusServerProcessOptions> {
21+
type: 'dev-status-server'
22+
}
23+
24+
export async function setupDevStatusServerProcess(
25+
options: DevStatusServerProcessOptions,
26+
): Promise<DevStatusServerProcess> {
27+
return {
28+
type: 'dev-status-server',
29+
prefix: 'status',
30+
options,
31+
function: launchDevStatusServer,
32+
}
33+
}
34+
35+
let lastReportedTimestamp = 0
36+
37+
export const launchDevStatusServer: DevProcessFunction<DevStatusServerProcessOptions> = async (
38+
{stdout, abortSignal},
39+
{devSessionStatusManager, appWatcher, graphiqlUrl, appEventsProcessor},
40+
) => {
41+
const app = createApp()
42+
const router = createRouter()
43+
const port = await getAvailableTCPPort(ports.devStatusServer)
44+
45+
router.get(
46+
'/dev-status',
47+
defineEventHandler(async () => {
48+
const status = devSessionStatusManager.status
49+
const currentApp = appWatcher.currentApp
50+
const manifest = await currentApp.manifest()
51+
52+
const lastLog = devSessionStatusManager.logs[devSessionStatusManager.logs.length - 1]
53+
54+
const filteredLogs = devSessionStatusManager.logs.filter((log) => log.timestamp > lastReportedTimestamp)
55+
56+
if (lastLog && lastLog.timestamp > lastReportedTimestamp) {
57+
lastReportedTimestamp = lastLog.timestamp
58+
}
59+
60+
await appEventsProcessor.waitForCompletion()
61+
62+
return {
63+
status: status.isReady ? status.statusMessage?.type : 'NOT_READY',
64+
previewURL: status.previewURL,
65+
statusMessage: status.statusMessage?.message,
66+
manifest,
67+
graphiqlUrl,
68+
appName: currentApp.name,
69+
logs: filteredLogs,
70+
cursor: lastReportedTimestamp,
71+
}
72+
}),
73+
)
74+
75+
app.use(router)
76+
77+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
78+
const server = createServer(app)
79+
80+
outputInfo(`Dev status server started on port ${port}`, stdout)
81+
82+
abortSignal.addEventListener('abort', () => {
83+
server.close()
84+
})
85+
86+
await server.listen(port)
87+
}

packages/app/src/cli/services/dev/processes/setup-dev-processes.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {DevSessionProcess, setupDevSessionProcess} from './dev-session/dev-sessi
99
import {AppLogsSubscribeProcess, setupAppLogsPollingProcess} from './app-logs-polling.js'
1010
import {AppWatcherProcess, setupAppWatcherProcess} from './app-watcher-process.js'
1111
import {DevSessionStatusManager} from './dev-session/dev-session-status-manager.js'
12+
import {DevStatusServerProcess, setupDevStatusServerProcess} from './dev-status-server.js'
1213
import {environmentVariableNames} from '../../../constants.js'
1314
import {AppLinkedInterface, getAppScopes, WebType} from '../../../models/app/app.js'
1415

@@ -18,12 +19,13 @@ import {LocalhostCert, getProxyingWebServer} from '../../../utilities/app/http-r
1819
import {buildAppURLForWeb} from '../../../utilities/app/app-url.js'
1920
import {ApplicationURLs} from '../urls.js'
2021
import {DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js'
21-
import {AppEventWatcher} from '../app-events/app-event-watcher.js'
22+
import {AppEvent, AppEventWatcher} from '../app-events/app-event-watcher.js'
2223
import {reloadApp} from '../../../models/app/loader.js'
2324
import {getAvailableTCPPort} from '@shopify/cli-kit/node/tcp'
2425
import {isTruthy} from '@shopify/cli-kit/node/context/utilities'
2526
import {getEnvironmentVariables} from '@shopify/cli-kit/node/environment'
2627
import {outputInfo} from '@shopify/cli-kit/node/output'
28+
import {SerialBatchProcessor} from '@shopify/cli-kit/node/serial-batch-processor'
2729

2830
interface ProxyServerProcess
2931
extends BaseProcess<{
@@ -45,6 +47,7 @@ type DevProcessDefinition =
4547
| DevSessionProcess
4648
| AppLogsSubscribeProcess
4749
| AppWatcherProcess
50+
| DevStatusServerProcess
4851

4952
export type DevProcesses = DevProcessDefinition[]
5053

@@ -107,7 +110,14 @@ export async function setupDevProcesses({
107110
? `http://localhost:${graphiqlPort}/graphiql${graphiqlKey ? `?key=${graphiqlKey}` : ''}`
108111
: undefined
109112

110-
const devSessionStatusManager = new DevSessionStatusManager({isReady: false, previewURL, graphiqlURL})
113+
const devSessionStatusManager = new DevSessionStatusManager({
114+
isReady: false,
115+
previewURL,
116+
graphiqlURL,
117+
logs: [],
118+
})
119+
120+
const appEventsProcessor = new SerialBatchProcessor<AppEvent>()
111121

112122
const processes = [
113123
...(await setupWebProcesses({
@@ -158,6 +168,7 @@ export async function setupDevProcesses({
158168
appPreviewURL: appPreviewUrl,
159169
appLocalProxyURL: devConsoleURL,
160170
devSessionStatusManager,
171+
appEventsProcessor,
161172
})
162173
: await setupDraftableExtensionsProcess({
163174
localApp: reloadedApp,
@@ -198,6 +209,13 @@ export async function setupDevProcesses({
198209
await setupAppWatcherProcess({
199210
appWatcher,
200211
}),
212+
await setupDevStatusServerProcess({
213+
devSessionStatusManager,
214+
localApp: reloadedApp,
215+
appWatcher,
216+
graphiqlUrl: graphiqlURL,
217+
appEventsProcessor,
218+
}),
201219
].filter(stripUndefineds)
202220

203221
// Add http server proxy & configure ports, for processes that need it

packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const initialStatus: DevSessionStatus = {
2424
isReady: true,
2525
previewURL: 'https://shopify.com',
2626
graphiqlURL: 'https://graphiql.shopify.com',
27+
logs: [],
2728
}
2829

2930
const onAbort = vi.fn()

packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ const DevSessionUI: FunctionComponent<DevSesionUIProps> = ({
134134
abortSignal={abortController.signal}
135135
keepRunningAfterProcessesResolve={true}
136136
useAlternativeColorPalette={true}
137+
onLogOutput={(log) => {
138+
devSessionStatusManager.addLog(log)
139+
}}
137140
/>
138141
{shouldShowPersistentDevInfo && (
139142
<Box marginTop={1} flexDirection="column">

packages/cli-kit/src/private/node/ui/components/ConcurrentOutput.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface ConcurrentOutputProps {
1414
showTimestamps?: boolean
1515
keepRunningAfterProcessesResolve?: boolean
1616
useAlternativeColorPalette?: boolean
17+
onLogOutput?: (log: {timestamp: number; message: string; prefix?: string}) => void
1718
}
1819

1920
interface Chunk {
@@ -89,6 +90,7 @@ const ConcurrentOutput: FunctionComponent<ConcurrentOutputProps> = ({
8990
showTimestamps = true,
9091
keepRunningAfterProcessesResolve = false,
9192
useAlternativeColorPalette = false,
93+
onLogOutput,
9294
}) => {
9395
const [processOutput, setProcessOutput] = useState<Chunk[]>([])
9496
const {exit: unmountInk} = useApp()
@@ -149,10 +151,16 @@ const ConcurrentOutput: FunctionComponent<ConcurrentOutputProps> = ({
149151
lines,
150152
},
151153
])
154+
onLogOutput?.({
155+
timestamp: new Date().getTime(),
156+
message: log,
157+
prefix,
158+
})
152159
next()
153160
},
154161
})
155162
},
163+
// eslint-disable-next-line react-hooks/exhaustive-deps
156164
[lineColor],
157165
)
158166

packages/cli-kit/src/public/node/serial-batch-processor.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('SerialBatchProcessor', () => {
1010
})
1111

1212
test('should process a single item in a batch', async () => {
13-
const processor = new SerialBatchProcessor<string>(processBatchMock)
13+
const processor = new SerialBatchProcessor<string>()
1414
processor.enqueue('item1')
1515
await processor.waitForCompletion()
1616

@@ -27,7 +27,7 @@ describe('SerialBatchProcessor', () => {
2727
})
2828
})
2929

30-
const processor = new SerialBatchProcessor<string>(processBatchMock)
30+
const processor = new SerialBatchProcessor<string>()
3131

3232
// Enqueue first item, processing will start and pause
3333
processor.enqueue('itemA')
@@ -56,7 +56,7 @@ describe('SerialBatchProcessor', () => {
5656
.mockImplementationOnce(async (_items) => firstBatchPromise)
5757
.mockImplementationOnce(async (_items) => secondBatchPromise)
5858

59-
const processor = new SerialBatchProcessor<string>(processBatchMock)
59+
const processor = new SerialBatchProcessor<string>()
6060
// Starts first batch
6161
processor.enqueue('item1')
6262
// Queued for second batch
@@ -82,7 +82,7 @@ describe('SerialBatchProcessor', () => {
8282
})
8383

8484
test('waitForCompletion should resolve immediately if no items are queued and no processing is active', async () => {
85-
const processor = new SerialBatchProcessor<string>(processBatchMock)
85+
const processor = new SerialBatchProcessor<string>()
8686
await processor.waitForCompletion()
8787
expect(processBatchMock).not.toHaveBeenCalled()
8888
})
@@ -93,7 +93,7 @@ describe('SerialBatchProcessor', () => {
9393
throw testError
9494
})
9595

96-
const processor = new SerialBatchProcessor<string>(processBatchMock)
96+
const processor = new SerialBatchProcessor<string>()
9797

9898
processor.enqueue('item1-fail')
9999

0 commit comments

Comments
 (0)