From b9b87e3dfbf7a2f8a3def72e976aa8588a36461c Mon Sep 17 00:00:00 2001 From: Rodri Date: Fri, 6 Mar 2026 10:30:03 +0000 Subject: [PATCH 1/3] Stop service adoptee before running cascade:true deps (behind flag) WIP: adds early adoptee stop logic in _execute(); will be gated behind a per-dependency flag (name TBD, e.g. preStop) before finalising. https://claude.ai/code/session_01Be7upyt3ZRNdHuhyi1W7eF --- src/execution/service.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/execution/service.ts b/src/execution/service.ts index c88d6d6f9..fecaf4914 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -360,17 +360,34 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand dep.cascade); + this.#state = { id: 'executingDeps', deferredFingerprint: new Deferred(), - adoptee: this.#state.adoptee, + adoptee: shouldStopAdopteeEarly ? undefined : adoptee, }; - void this._executeDependencies().then((result) => { - if (result.ok) { - this.#onDepsExecuted(result.value); - } else { - this.#onDepExecErr(result); + + void (shouldStopAdopteeEarly + ? adoptee.abort() + : Promise.resolve() + ).then(() => { + if (this.#state.id !== 'executingDeps') { + // Service was aborted while waiting for the adoptee to stop. + return; } + void this._executeDependencies().then((result) => { + if (result.ok) { + this.#onDepsExecuted(result.value); + } else { + this.#onDepExecErr(result); + } + }); }); return this.#state.deferredFingerprint.promise; } From 048b1e9c4539161b09fefc7fd8029591e0e58726 Mon Sep 17 00:00:00 2001 From: Rodri Date: Fri, 6 Mar 2026 10:34:56 +0000 Subject: [PATCH 2/3] Add stopFirst flag to dependency config for service restart ordering Introduces a new per-dependency boolean option `stopFirst` (default: false). When true on a dependency of a service, the running service (adoptee) is stopped before that dependency executes. This allows dependencies to freely write files the service may have open, avoiding file-lock and port conflicts on both Unix and Windows. Usage: "dependencies": [{"script": "build", "cascade": true, "stopFirst": true}] https://claude.ai/code/session_01Be7upyt3ZRNdHuhyi1W7eF --- schema.json | 4 ++++ src/analyzer.ts | 32 ++++++++++++++++++++++++++++++++ src/config.ts | 1 + src/execution/service.ts | 4 ++-- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/schema.json b/schema.json index c0b2869ca..d47ac514f 100644 --- a/schema.json +++ b/schema.json @@ -41,6 +41,10 @@ "cascade": { "markdownDescription": "When `true` (the default), whenever this dependency runs, this script (the dependent) will be marked stale and need to re-run too, regardless of whether the dependency produced new or relevant output. When `false` Wireit won't assume that the dependent is stale just because the dependency ran. This can reduce unnecessary re-building (or restarting in the case of services) when `files` captures all of the relevant output of the dependency.\n\nFor more info, see https://github.com/google/wireit#re-run-on-change", "type": "boolean" + }, + "stopFirst": { + "markdownDescription": "When `true`, if the dependent is a running service, it will be stopped before this dependency executes. This is useful when the dependency needs to write files that the service has open (e.g. rebuilding output the service serves). Defaults to `false`.", + "type": "boolean" } } } diff --git a/src/analyzer.ts b/src/analyzer.ts index cc599d148..6c96635f7 100644 --- a/src/analyzer.ts +++ b/src/analyzer.ts @@ -696,6 +696,7 @@ export class Analyzer { // property plus optional extra annotations. let specifierResult; let cascade = true; // Default; + let stopFirst = false; // Default; if (maybeUnresolved.type === 'string') { specifierResult = failUnlessNonBlankString( maybeUnresolved, @@ -762,6 +763,36 @@ export class Analyzer { continue; } } + const stopFirstResult = findNodeAtLocation(maybeUnresolved, [ + 'stopFirst', + ]); + if (stopFirstResult !== undefined) { + if ( + stopFirstResult.value === true || + stopFirstResult.value === false + ) { + stopFirst = stopFirstResult.value; + } else { + encounteredError = true; + placeholder.failures.push({ + type: 'failure', + reason: 'invalid-config-syntax', + script: {packageDir: pathlib.dirname(packageJson.jsonFile.path)}, + diagnostic: { + severity: 'error', + message: `The "stopFirst" property must be either true or false.`, + location: { + file: packageJson.jsonFile, + range: { + offset: stopFirstResult.offset, + length: stopFirstResult.length, + }, + }, + }, + }); + continue; + } + } } else { encounteredError = true; placeholder.failures.push({ @@ -836,6 +867,7 @@ export class Analyzer { specifier: unresolved, config: placeHolderInfo.placeholder, cascade, + stopFirst, }); this.#ongoingWorkPromises.push( (async () => { diff --git a/src/config.ts b/src/config.ts index 59a7d7fb0..390e3c0aa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -55,6 +55,7 @@ export interface Dependency< config: Config; specifier: JsonAstNode; cascade: boolean; + stopFirst: boolean; } export type ScriptConfig = diff --git a/src/execution/service.ts b/src/execution/service.ts index fecaf4914..d462641d8 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -361,11 +361,11 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand dep.cascade); + this._config.dependencies.some((dep) => dep.stopFirst); this.#state = { id: 'executingDeps', From e65814070d0f732324d64a0b14a8b33d0cc16e38 Mon Sep 17 00:00:00 2001 From: Rodri Date: Fri, 6 Mar 2026 10:38:00 +0000 Subject: [PATCH 3/3] Document stopFirst dependency option in README https://claude.ai/code/session_01Be7upyt3ZRNdHuhyi1W7eF --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 9853856fb..3ec517feb 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ _Wireit upgrades your npm scripts to make them smarter and more efficient._ - [Cleaning output](#cleaning-output) - [Watch mode](#watch-mode) - [Services](#services) + - [Stop service before dependency](#stop-service-before-dependency) - [Execution cascade](#execution-cascade) - [Environment variables](#environment-variables) - [Failures and errors](#failures-and-errors) @@ -585,6 +586,36 @@ In watch mode, a service will be restarted whenever one of its input files or dependencies change, except for dependencies with [`cascade`](#execution-cascade) set to `false`. +### Stop service before dependency + +By default, when a service restarts in watch mode its previous instance keeps +running while its dependencies execute, and is only stopped once the new +fingerprint has been computed. This allows the service to keep serving requests +for as long as possible. + +However, if a dependency needs to write files that the service has open (e.g. +recompiling output that the service reads from disk), the running service can +interfere. Setting `stopFirst: true` on that dependency tells Wireit to stop +the service _before_ the dependency runs: + +```json +{ + "command": "node my-server.js", + "dependencies": [ + { + "script": "build", + "cascade": true, + "stopFirst": true + } + ] +} +``` + +> **Note** +> When `stopFirst` is `true` the service is always stopped before the +> dependency runs, even if the fingerprint ultimately does not change. Accept +> the extra downtime in exchange for avoiding file-lock or port conflicts. + ### Service output Services cannot have `output` files, because there is no way for Wireit to know @@ -855,6 +886,7 @@ The following properties can be set inside `wireit.