Skip to content

Add a default dev-session message for configuration-only extensions - #8350

Draft
amcaplan wants to merge 1 commit into
mainfrom
dev-session-message-config-only-extensions
Draft

Add a default dev-session message for configuration-only extensions#8350
amcaplan wants to merge 1 commit into
mainfrom
dev-session-message-config-only-extensions

Conversation

@amcaplan

@amcaplan amcaplan commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

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. logExtensionEvents only runs inside processEvents, so at startup these modules are invisible: you run dev, 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 getDevSessionUpdateMessages for config-only modules, overridable per specification. analytics_app_events gets 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:

  • A per-spec hook still wins. If a specification defines 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.
  • The predicate is a capability check, not an allowlist: no appModuleFeatures, no clientSteps deploy group, no build output, and not an app-config module. Read off existing lazy getters, so it stays correct when the platform response rewrites experience/uidStrategy after the factory has run.
  • It fires once, on the first successful dev session create. The event type can't signal "first time" — app-event-watcher.ts stamps every initial extension as EventType.Updated — so the signal is DevSessionResult.status === 'created', which by construction happens exactly once per dev run.

The default lives on the instance rather than in createExtensionSpecification on purpose. createConfigExtensionSpecification and createContractBasedModuleSpecification both pass getDevSessionUpdateMessages through unconditionally, so an explicit undefined reaches {...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), where context is a DevSessionUpdateContext carrying a DevSessionUpdateStatus of 'created' | 'updated'. DevSessionResult reuses 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. handleDevSessionResult passes its DevSessionResult straight to the logger — no reconstructed status literal, so there's no second copy that could disagree with the branch it was written in. DevSessionLogger.logExtensionUpdateMessages decides which outcomes are worth speaking about, returning early on the two error branches, which sits next to the EventType.Deleted skip it already owned. Errors already have a per-extension voice — logUserErrors maps each UserError to its owning extension by uid and prefixes the line with that handle — so letting specs speak there too would be a second extension-prefixed channel on one event. Only the narrow DevSessionUpdateContext reaches the specs, so a status added later to the service's state machine doesn't silently widen what every spec has to handle — and specification.ts stays free of any import back into the dev-session service, which would close a cycle through app-event-watcher.ts and extension-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 the newBundleController.signal.aborted check, so the handleDevSessionResult branch that debug-logged it was unreachable — the statuses actually produced are created, updated, remote-error and unknown-error. Deleting it lets DevSessionResult reuse DevSessionUpdateStatus whole 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.ts and app_config_app_proxy.test.ts appear in this diff is that they call the hook directly and now pass the new argument.

Which specs this affects

In (local) editor_extension_collection, flow_action, flow_trigger, payments_extension
In (remote-only) every contract-based spec with no features — including analytics_app_events
Out channel_config, order_attribution_config, tax_calculation, admin_link (deploy steps and/or build output), and all app-config modules (already summarised once as App config updated)

Open questions

  • The copy is provisional and wants a product call. Configuration accepted says 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_action and flow_trigger both 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.
  • A deliberate asymmetry worth a second opinion: a flow_action prints Configuration accepted while a checkout_ui_extension in 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 in dev output 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.
  • A localization-only remote-only spec won't get the message, because 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.
  • Adjacent bug, deliberately not fixed here: createContractBasedModuleSpecification passes uidStrategy: spec.uidStrategy unconditionally, so omitting it clobbers the computed default at {...defaults, ...spec} with undefined, buildHandle's switch falls through, and constantize(undefined) throws. No production caller hits it — createRemoteOnlySpecification always 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.
  • Follow-up, not here: platform specifications already carry meta_data with description and documentation_links (23 registrations in shop/world have 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?

  1. shopify app dev on an app with a config-only module (e.g. a Flow action, or an analytics_app_events module).
  2. On startup, after ✅ Ready, watching for changes, the module prints Configuration accepted under its own handle.
  3. Edit and save the module's TOML — it prints Extension changed / ✅ Updated dev preview on … and does not repeat Configuration accepted.

Post-release steps

None.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows) — terminal output only, no path or process handling
  • I've considered possible documentation changes — none; no command, flag or schema surface changes
  • I've considered analytics changes to measure impact — none
  • The change is user-facing — minor, changeset added

@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Aug 19, 2026
@amcaplan
amcaplan force-pushed the dev-session-message-config-only-extensions branch 2 times, most recently from 5cb60db to 364dbbe Compare August 20, 2026 10:29
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>
@amcaplan
amcaplan force-pushed the dev-session-message-config-only-extensions branch from 364dbbe to 5c7c980 Compare August 20, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant