Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -855,6 +886,7 @@ The following properties can be set inside `wireit.<script>` objects in
| `dependencies` | `string[] \| object[]` | `[]` | [Scripts that must run before this one](#dependencies). |
| `dependencies[i].script` | `string` | `undefined` | [The name of the script, when the dependency is an object.](#dependencies). |
| `dependencies[i].cascade` | `boolean` | `true` | [Whether this dependency always causes this script to re-execute](#execution-cascade). |
| `dependencies[i].stopFirst` | `boolean` | `false` | [Whether a service dependent should be stopped before this dependency runs](#stop-service-before-dependency). |
| `files` | `string[]` | `undefined` | Input file [glob patterns](#glob-patterns), used to determine the [fingerprint](#fingerprint). |
| `output` | `string[]` | `undefined` | Output file [glob patterns](#glob-patterns), used for [caching](#caching) and [cleaning](#cleaning-output). |
| `clean` | `boolean \| "if-file-deleted"` | `true` | [Delete output files before running](#cleaning-output). |
Expand Down
4 changes: 4 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -836,6 +867,7 @@ export class Analyzer {
specifier: unresolved,
config: placeHolderInfo.placeholder,
cascade,
stopFirst,
});
this.#ongoingWorkPromises.push(
(async () => {
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface Dependency<
config: Config;
specifier: JsonAstNode<string>;
cascade: boolean;
stopFirst: boolean;
}

export type ScriptConfig =
Expand Down
29 changes: 23 additions & 6 deletions src/execution/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,17 +360,34 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand<ServiceScri
void this.abort();
});

const adoptee = this.#state.adoptee;
// If any dependency has stopFirst:true, stop the adoptee before running
// deps so they can freely write to files the service may have open.
const shouldStopAdopteeEarly =
adoptee !== undefined &&
this._config.dependencies.some((dep) => dep.stopFirst);

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;
}
Expand Down