Repository navigation
Add JSON streaming to Function replay watch mode - #9056
isaacroldan wants to merge 11 commits into
Conversation
61c9a3a to
6f38ba4
Compare
|
/snapit |
|
🫰✨ Thanks @isaacroldan! Your snapshot has been published to npm. Built from Test the snapshot by installing your package globally: pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20261103151843Caution After installing, validate the version by running |
ed6ab2e to
fe3d5e5
Compare
|
/snapit |
|
🫰✨ Thanks @isaacroldan! Your snapshot has been published to npm. Built from Test the snapshot by installing your package globally: pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20261104125335Caution After installing, validate the version by running |
dmerand
left a comment
There was a problem hiding this comment.
This change looks great overall! Very exciting to get this in. I've got some agent comments that are worth considering before we merge. Nits can be addressed at your discretion.
| process.exitCode = {SIGINT: 130, SIGTERM: 143, SIGQUIT: 131}[receivedSignal] | ||
| options.abortController.abort() | ||
| } | ||
| process.on('shopify:signal', onSignal) |
There was a problem hiding this comment.
Replay does not stop when stdout closes. Oclif catches the stdout EPIPE error, and this writer does not abort the loop. So shopify app function replay --json ... | head -1 leaves the watcher running, and a shell pipeline waits for it. More edits still start builds.
I reproduced this with the real watch loop and file watcher. Only the runner was mocked. The process stopped only after SIGINT. #9057 does not change this file.
Can you abort replay on a stdout EPIPE error, and remove that listener in finally?
There was a problem hiding this comment.
Fixed: stdout EPIPE aborts replay, suppresses later stdout records, and removes the error listener in finally. The regression test uses the real replay loop and output writer. A separate test with a real OS pipe and native file watcher ended with exit code 0, with no new builds after cancellation and no added error listener left behind. Active runner and build cancellation remains in #9057, which is rebased onto this fix.
The review fixes are in 86775f2b5a and 585c0f3a5d.
🤖 Generated by the review-pr skill
|
|
||
| Watches for changes by default. Use --no-watch to run once. | ||
|
|
||
| Use --json for JSONL output while watching, or --no-watch --json for a single JSON result.` |
There was a problem hiding this comment.
Please add the replay stream to the streaming section of docs/cli/json-output.md. That section has a paragraph for app dev, and main also has one for theme dev.
Describe the three record types and the stream that gets each one. Also describe the lifecycle (watching, cancelled), recoverable and fatal failures, and the exit codes 130, 143, and 131. This help text says JSONL, but it does not give this contract.
There was a problem hiding this comment.
Added the replay contract to the streaming section of docs/cli/json-output.md. It describes the three stdout record types, stderr diagnostics, watching and cancelled, recoverable and fatal failures, exit codes 130/143/131, pipe closure, and finite --no-watch --json output. The PR description also explains these changes.
The review fixes are in 86775f2b5a and 585c0f3a5d.
🤖 Generated by the review-pr skill
| .object({ | ||
| type: zod.literal('execution'), | ||
| timestamp: jsonOutputTimestampSchema, | ||
| result: functionRunJsonOutputSchema.schema, |
There was a problem hiding this comment.
The execution record reuses FunctionRunResult. Its success description says "the command exits nonzero" (runner/types.ts:27). In watch mode this is not true, because replay continues to watch.
Please override the description here, or make the shared text true for both modes. Please also describe exitCode on the next line. It is the exit code of the Function run, not of the process.
There was a problem hiding this comment.
Updated the shared success description to describe a completed Function execution failure without claiming that watch replay exits. The execution record now describes exitCode as the Function run's exit code. Regenerated the README and manifest, and checked the descriptions through the full command's --json-schema output.
The review fixes are in 86775f2b5a and 585c0f3a5d.
🤖 Generated by the review-pr skill
| await watcher.start({stdout: diagnostics, stderr: diagnostics, signal}, false) | ||
| if (!signal.aborted) { | ||
| onEvent({type: 'status', status: 'watching'}) | ||
| await enqueue(extension) |
There was a problem hiding this comment.
Nit (non-blocking): watcher.onEvent is registered before watcher.start(). So a rebuild during the initial scan can run before watching. Then this line runs the original extension, not the rebuilt one.
A probe that holds Chokidar's ready event gave build, execution, watching, execution. I could not reproduce it without the hold, so the window is small. Can startup rebuilds wait for watching, and can this line use the latest extension?
There was a problem hiding this comment.
Startup rebuild records and executions wait for watching, and the first execution uses the latest rebuilt extension. A failed startup rebuild waits for recovery. Tests cover successful and failed startup rebuilds.
After the scope split, #9056 opts in to readiness and startup event completion only for JSON replay. The broader watcher defaults, startup build result handling, and text replay loop reuse are in #9108. #9056 can merge independently.
🤖 Generated by the review-pr skill
| JSON.stringify(functionReplayEventSchema.validate({type: event.type, timestamp, status: event.status})), | ||
| ) | ||
| if (event.type === 'build' && event.status === 'failed') { | ||
| renderCommandEventAsJson({type: 'diagnostic', timestamp, level: 'error', message: event.error}) |
There was a problem hiding this comment.
Nit (non-blocking): A build failure writes the same message two times on stderr. AppEventWatcher writes it to the diagnostics stream (level: "warning", line 26), and this line writes it again (level: "error"). A real CLI run showed both lines.
Question: can the failed build record carry the message instead? Then a consumer that reads only stdout can see why the build failed, and stderr has one copy.
There was a problem hiding this comment.
The failed stdout build record now includes its message in error. Removed the second stderr emission from that record. A test with the real replay loop and native file watcher confirmed one failure diagnostic on stderr and the failure message in the stdout build record.
The review fixes are in 86775f2b5a and 585c0f3a5d.
🤖 Generated by the review-pr skill
| } | ||
| watcher.once('ready', onReady) | ||
| watcher.once('error', onError) | ||
| this.options.signal.addEventListener('abort', onReady, {once: true}) |
There was a problem hiding this comment.
Question (non-blocking): start() now waits for the initial scan, and an abort resolves the wait here. After that, AppEventWatcher.start() still emits ready. So in app dev, Ctrl-C during the scan can still let DevSession.onStart bundle and upload. Before this PR, this window was very short.
#9057 adds an abort check after fileWatcher.start() in app-event-watcher.ts. Will both PRs ship in the same release? If not, can this PR take that one-line guard?
The PR description and the changeset mention only replay. Please also mention this app dev startup change.
There was a problem hiding this comment.
After the scope split, #9056 leaves the default watcher and app dev startup behavior unchanged. Only JSON replay opts in to scan readiness and startup event completion, with cancellation checked before it reports watching.
The general app dev abort guard and latest startup build result handling are now in #9108, with their patch changesets and regression tests. #9056 does not depend on that follow-up for JSON support. #9057 follows #9108 and contains active process/build shutdown.
🤖 Generated by the review-pr skill
| onTestFinished(() => { | ||
| watcherCallback.emitDestroy() | ||
| }) | ||
| const resultJson = `{ |
There was a problem hiding this comment.
Nit (non-blocking): The regex in json.ts:37-40 rewrites native runner output, but this fixture has no space and no escaped quote inside a string. If the regex strips spaces inside strings, or ignores \", all tests still pass. I checked both changes locally.
Please add a value such as "logs": "hello world \"quoted\" done", and compare the parsed result with the parsed input.
There was a problem hiding this comment.
Expanded the fixture to hello world \"quoted words\" done with a newline, and compare the parsed execution result with the parsed native input. The space inside the escaped quotes is needed to catch a regex that treats those quotes as string boundaries. Both faulty mutations now fail: stripping spaces inside strings and ignoring escaped quotes. The separate assertion still checks that the unsafe integer token is preserved.
The review fixes are in 86775f2b5a and 585c0f3a5d.
🤖 Generated by the review-pr skill
| .strict() | ||
|
|
||
| export const functionReplayEventSchema = defineJsonOutputSchema({ | ||
| name: 'FunctionReplayEvent', |
There was a problem hiding this comment.
Nit (non-blocking): In --json-schema, Event means stderr side events. The stdout records here are named FunctionReplayEvent. The GraphiQL stream names its stdout records GraphiQLRecord.
Can this be FunctionReplayRecord? Replay --json is not released yet, so the rename costs little now.
There was a problem hiding this comment.
Renamed the stdout schema and its definition to FunctionReplayRecord. Updated the writer, schema tests, documentation, generated README, and manifest. The full command's --json-schema output confirms the new name; Event continues to describe stderr side events.
The review fixes are in 86775f2b5a and 585c0f3a5d.
🤖 Generated by the review-pr skill
| if (event.type === 'diagnostic') { | ||
| const message = unstyled(event.message) | ||
| if (message.trim().length > 0) { | ||
| renderCommandEventAsJson({type: 'diagnostic', timestamp, level: 'warning', message}) |
There was a problem hiding this comment.
Question (non-blocking): Did you consider sending these diagnostics through the command event channel (emitCommandEvent)? Lines 26 and 49 call renderCommandEventAsJson directly. Other streams, for example the GraphiQL session (graphiql/session.ts:115), use the channel.
The JSON output is the same today. With the channel, the timestamp comes from the channel clock, and a test sink or a future sink also gets these events.
There was a problem hiding this comment.
Diagnostics now use emitCommandEvent. Bound the replay output callback to its command context so native watcher callbacks retain the channel. A test invokes the callback from a separate async resource and checks that the custom sink receives the diagnostics and that stderr uses the channel clock's timestamp.
The review fixes are in 86775f2b5a and 585c0f3a5d.
🤖 Generated by the review-pr skill
|
/snapit |
|
🫰✨ Thanks @isaacroldan! Your snapshot has been published to npm. Built from Test the snapshot by installing your package globally: pnpm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20261109161059Caution After installing, validate the version by running |
7faa5bb to
52e5840
Compare
WHY are these changes introduced?
Function replay watches by default, but rejects
--jsonunless--no-watchis set.WHAT is this pull request doing?
Add JSONL records for replay status, rebuild results, and native Function results. Send diagnostics through the command event channel on stderr. Preserve native fields, numeric tokens, and strings, and stop replay when stdout closes with
EPIPE.A replay-only file watcher owns scan readiness and startup event completion before JSON replay reports
watching. The shared file watcher only exposes a small lifecycle hook;app-event-watcher.tsand the existing text replay hook match main. #9108 moves text replay onto the reusable replay loop and applies the broader watcher startup fixes. This PR can merge independently.Keep the single native result for
--no-watch --json. Document the stream contract and exposeFunctionReplayRecordin--json-schema.How to manually test your changes?
Use a test app with a Function and a saved Function log.
pnpm shopify app function replay --path <function-directory> --log <log-id> --json --no-input > replay.jsonl 2> replay.stderr.erroron stdout and one failure diagnostic on stderr. Replay should keep watching after rebuild and completed Function failures.cancelledrecord and exit code 130.| head -n 1. Check that the pipeline finishes.--jsonto check the existing text workflow. Repeat with--no-watch --jsonto check the single native runner result.pnpm shopify app function replay --json-schema. Check the record types and the distinction between Function run and process exit codes without starting replay.Validation
52e5840f26: Linux/macOS/Windows Node matrices, GraphQL freshness, type checking, lint, bundle, unused-code checks, generated help, and both E2E shards. CI run. The Nx build crash in the first E2E shard passed on retry.Checklist