Add a default dev-session message for configuration-only extensions - #8350
Add a default dev-session message for configuration-only extensions#8350amcaplan wants to merge 1 commit into
Conversation
364dbbe to
5c7c980
Compare
| // Only acknowledge the module once, on the first successful dev session; later updates stay quiet. | ||
| if (context.status !== 'created') return undefined | ||
|
|
||
| // App config modules are already summarised together as `App config updated`, so a per-module | ||
| // line would just be noise. | ||
| if (this.isAppConfigExtension) return undefined | ||
|
|
||
| // Anything with local dev output is already visible through its build output or preview URL. | ||
| // Modules with none of that are never mentioned by `dev` otherwise, so acknowledge them here. | ||
| if (!this.hasNoLocalDevOutput) return undefined |
There was a problem hiding this comment.
Are these comments helpful, or just a distraction? I'm honestly not sure.
| /** | ||
| * A real extension instance for a module that builds nothing locally, so that the default | ||
| * `getDevSessionUpdateMessages` implementation is genuinely exercised rather than mocked away. | ||
| */ |
There was a problem hiding this comment.
This comment is not helpful. It's actually pretty obvious. Looking at you, Claude.
Config-only extension modules say nothing during `shopify app dev`:
`logExtensionEvents` only runs inside `processEvents`, so at startup a
module that is live on the platform is never mentioned in the terminal.
Add a default `getDevSessionUpdateMessages` on `ExtensionInstance`,
gated on a capability predicate (no features, no deploy steps, no build
output, not app config) and on the first successful dev session. A
per-spec hook still wins, so any specification can override the copy.
The default lives on the instance rather than in the spec factories:
those pass `getDevSessionUpdateMessages` through unconditionally, so an
explicit `undefined` clobbers a factory-level default at
`{...defaults, ...spec}`.
`DevSessionResult` composes its success branch from the exported
`DevSessionUpdateStatus` union rather than restating the literals, so the
service and the spec hook can't drift apart. The logger takes the result
itself and decides which outcomes are worth speaking about, so the status
is never rewritten at a call site where it could disagree with the branch
it sits in. Only the narrow context reaches the specs.
Drop the `'aborted'` status while here. Nothing has constructed it since
4bc22d2 ("Only one app-preview update") removed the bundle-controller
abort check, so the branch handling it was unreachable. Deleting it lets
`DevSessionResult` reuse `DevSessionUpdateStatus` whole instead of
grafting a third literal onto it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5c7c980 to
ff8555c
Compare
There was a problem hiding this comment.
Pull request overview
This PR makes shopify app dev acknowledge configuration-only (and other “no local dev output”) extension modules by adding a default dev-session message on first successful session creation, while still allowing per-spec overrides. It also threads a small dev-session context (created vs updated) through the message hook and removes the unreachable aborted dev-session status.
Changes:
- Add
DevSessionUpdateContext(status: 'created' | 'updated') and pass it through dev-session logging intogetDevSessionUpdateMessages. - Implement a default
ExtensionInstance.getDevSessionUpdateMessagesthat emits a single “Configuration accepted” message for modules with no local dev output on the first successful session. - Add/adjust tests to cover default behavior, remote-spec behavior, and override preservation through spec merging.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/app/src/cli/services/generate/fetch-extension-specifications.test.ts | Adds coverage for default message behavior on remotely sourced specs and for override preservation during local/remote merging. |
| packages/app/src/cli/services/dev/processes/dev-session/dev-session/dev-session.ts | Uses DevSessionUpdateStatus for DevSessionResult, removes unreachable aborted, and passes result context into the logger. |
| packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.ts | Passes dev-session context into extension message hooks and suppresses message evaluation on errored sessions. |
| packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts | Updates logger tests for the new signature and adds coverage for default messaging behavior and error suppression. |
| packages/app/src/cli/models/extensions/specifications/app_config_app_proxy.test.ts | Updates direct hook calls to pass the new context argument. |
| packages/app/src/cli/models/extensions/specifications/app_config_app_access.test.ts | Updates direct hook calls to pass the new context argument. |
| packages/app/src/cli/models/extensions/specification.ts | Introduces DevSessionUpdateStatus/DevSessionUpdateContext and updates the hook signature. |
| packages/app/src/cli/models/extensions/extension-instance.ts | Adds default dev-session message logic and the hasNoLocalDevOutput capability predicate. |
| packages/app/src/cli/models/extensions/extension-instance.test.ts | Adds focused tests for default messaging, exclusion predicates, and per-spec override behavior. |
| .changeset/nervous-pandas-acknowledge.md | Declares a minor user-facing change for the new dev-session confirmation message. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| import {DevSessionResult, UserError} from './dev-session.js' | ||
| import {AppEvent, EventType} from '../../app-events/app-event-watcher.js' | ||
| import {ExtensionInstance} from '../../../../models/extensions/extension-instance.js' | ||
| import {DevSessionUpdateContext} from '../../../../models/extensions/specification.js' |
| } from '../../../bundle.js' | ||
| import {DevSessionCreateOptions, DevSessionUpdateOptions} from '../../../../utilities/developer-platform-client.js' | ||
| import {AppManifest} from '../../../../models/app/app.js' | ||
| import {DevSessionUpdateStatus} from '../../../../models/extensions/specification.js' |
WHY are these changes introduced?
Config-only extension modules — the ones whose configuration is the whole extension, with nothing for the CLI to build, bundle or serve — say nothing at all during
shopify app dev.logExtensionEventsonly runs insideprocessEvents, so at startup these modules are invisible: you rundev, your module is in the manifest and live on the platform, and the terminal never mentions it.#8319 fixes this for one extension type (
analytics_app_events) by registering a local specification whose only purpose is to carry a single string. That works, but it means every future config-only, remote-only extension type needs its own local spec file for the same one line — which is exactly the coupling that remote-only specs exist to avoid.This PR solves the class instead of the instance: a default
getDevSessionUpdateMessagesfor config-only modules, overridable per specification.analytics_app_eventsgets its message with no local spec file at all, and so does the next one.WHAT is this pull request doing?
A default implementation on
ExtensionInstance.getDevSessionUpdateMessages, gated on a capability predicate and on the dev-session lifecycle:getDevSessionUpdateMessages, it is used unchanged. The three existing implementers (app_config_app_home,app_config_app_access,app_config_app_proxy) keep their current behaviour.appModuleFeatures, noclientStepsdeploy group, no build output, and not an app-config module. Read off existing lazy getters, so it stays correct when the platform response rewritesexperience/uidStrategyafter the factory has run.app-event-watcher.tsstamps every initial extension asEventType.Updated— so the signal isDevSessionResult.status === 'created', which by construction happens exactly once perdevrun.The default lives on the instance rather than in
createExtensionSpecificationon purpose.createConfigExtensionSpecificationandcreateContractBasedModuleSpecificationboth passgetDevSessionUpdateMessagesthrough unconditionally, so an explicitundefinedreaches{...defaults, ...spec}and clobbers any factory-level default — a default written there is a silent no-op for most specs. There is a test covering the override through all three factories.The hook also gains a second argument,
getDevSessionUpdateMessages(config, context), wherecontextis aDevSessionUpdateContextcarrying aDevSessionUpdateStatusof'created' | 'updated'.DevSessionResultreuses that union whole for its success branch, so the two can't drift: renaming a status now breaks the service at compile time.Responsibility splits across three layers.
handleDevSessionResultpasses itsDevSessionResultstraight to the logger — no reconstructed status literal, so there's no second copy that could disagree with the branch it was written in.DevSessionLogger.logExtensionUpdateMessagesdecides which outcomes are worth speaking about, returning early on the two error branches, which sits next to theEventType.Deletedskip it already owned. Errors already have a per-extension voice —logUserErrorsmaps eachUserErrorto its owning extension byuidand prefixes the line with that handle — so letting specs speak there too would be a second extension-prefixed channel on one event. Only the narrowDevSessionUpdateContextreaches the specs, so a status added later to the service's state machine doesn't silently widen what every spec has to handle — andspecification.tsstays free of any import back into the dev-session service, which would close a cycle throughapp-event-watcher.tsandextension-instance.ts.One drive-by removal: the
'aborted'status is gone. Nothing has constructed it since 4bc22d2 ("Only one app-preview update", May 2025) removed thenewBundleController.signal.abortedcheck, so thehandleDevSessionResultbranch that debug-logged it was unreachable — the statuses actually produced arecreated,updated,remote-errorandunknown-error. Deleting it letsDevSessionResultreuseDevSessionUpdateStatuswhole rather than grafting a third literal onto it, and leaves the logger's guard as an exact match on the two error branches. Behaviour is unchanged because the branch could not run. Happy to split this out into its own PR if you'd rather review it separately.The three existing implementations ignore the context and are unchanged — the only reason
app_config_app_access.test.tsandapp_config_app_proxy.test.tsappear in this diff is that they call the hook directly and now pass the new argument.Which specs this affects
editor_extension_collection,flow_action,flow_trigger,payments_extensionanalytics_app_eventschannel_config,order_attribution_config,tax_calculation,admin_link(deploy steps and/or build output), and all app-config modules (already summarised once asApp config updated)Open questions
Configuration acceptedsays only that the platform took the configuration. It deliberately claims nothing about local files, because the predicate admits modules where such a claim would be false:flow_actionandflow_triggerboth read a local schema file (loadSchemaFromPath, e.g.flow_action.ts:83). Anything more descriptive has to stay true for every module the predicate admits, now and as the set grows. Owners of the four affected local specs should also confirm they want the line at all.flow_actionprintsConfiguration acceptedwhile acheckout_ui_extensionin the same app prints nothing. The rule is "nothing is built for this module", not "this module is inert" — buildable modules are already visible through their build output and preview URL, whereas these had no representation indevoutput at all. That's the intended reading, but it is the boundary most likely to be contested, so it should be an explicit decision rather than a side effect.features === ['localization']. That's a deliberate consequence of the predicate rather than an oversight; widening it would start printing for modules that do have local output.createContractBasedModuleSpecificationpassesuidStrategy: spec.uidStrategyunconditionally, so omitting it clobbers the computed default at{...defaults, ...spec}withundefined,buildHandle's switch falls through, andconstantize(undefined)throws. No production caller hits it —createRemoteOnlySpecificationalways passes a strategy — so it's dormant, and fixing it means touching the merge line this PR is specifically designed to avoid. Worth its own PR. It's also a live demonstration, on a second property, of why the default here lives on the instance rather than in the factory.meta_datawithdescriptionanddocumentation_links(23 registrations inshop/worldhave it). Sourcing dev-session copy from the platform instead of hardcoding it in the CLI is the better long-term shape, and would let each extension team own its own string. Out of scope here, which is CLI-side copy only.How to test your changes?
shopify app devon an app with a config-only module (e.g. a Flow action, or ananalytics_app_eventsmodule).✅ Ready, watching for changes, the module printsConfiguration acceptedunder its own handle.Extension changed/✅ Updated dev preview on …and does not repeatConfiguration accepted.Post-release steps
None.
Checklist
minor, changeset added