Skip to content
Merged
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0.html).

<!-- ## [Unreleased] -->
## [Unreleased]

- Added `"service": true` setting, which is well suited for long-running
processes like servers. A service is started either when it is invoked directly,
or when another script that depends on it is ready to run. A service is stopped
when all scripts that depend on it have finished, or when Wireit is exited.

## [0.7.2] - 2022-09-25

Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- [GitHub Actions caching](#github-actions-caching)
- [Cleaning output](#cleaning-output)
- [Watch mode](#watch-mode)
- [Services](#services)
- [Failures and errors](#failures-and-errors)
- [Package locks](#package-locks)
- [Recipes](#recipes)
Expand Down Expand Up @@ -388,6 +389,46 @@ The benefit of Wireit's watch mode over built-in watch modes are:
simultaneously, such as build steps being triggered before all preceding steps
have finished.

## Services

By default, Wireit assumes that your scripts will eventually exit by themselves.
This is well suited for build and test scripts, but not for long-running
processes like servers. To tell Wireit that a process is long-running and not
expected to exit by itself, set `"service": true`.

```json
{
"scripts": {
"serve": "wireit",
"build:server": "wireit",
"build:assets": "wireit"
},
"wireit": {
"serve": {
"command": "node my-server.js",
"service": true,
"files": ["server-config.json"],
"dependencies": ["build:server", "build:assets"]
}
}
}
```

If a service is run _directly_ (e.g. `npm run serve`), then it will stay running
until the user kills Wireit (e.g. `Ctrl-C`).

If a service is a _dependency_ of one or more other scripts, then it will start
up before any depending script runs, and will shut down after all depending
scripts finish.

In watch mode, a service will be restarted whenever one of its input files or
dependencies change.

Services cannot have `output` files, because there is no way for Wireit to know
when a service has finished writing its output. If you have a service that
produces output, you should define a non-service script that depends on it, and
which exits when the service's output is complete.

## Failures and errors

By default, when a script fails (meaning it returned with a non-zero exit code),
Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"test:json-schema": "wireit",
"test:optimize-mkdirs": "wireit",
"test:parallelism": "wireit",
"test:service": "wireit",
"test:watch": "wireit"
},
"wireit": {
Expand Down Expand Up @@ -88,6 +89,7 @@
"test:json-schema",
"test:optimize-mkdirs",
"test:parallelism",
"test:service",
"test:watch"
]
},
Expand Down Expand Up @@ -251,6 +253,14 @@
"files": [],
"output": []
},
"test:service": {
"command": "cross-env NODE_OPTIONS=--enable-source-maps uvu lib/test \"service\\.test\\.js$\"",
"dependencies": [
"build"
],
"files": [],
"output": []
},
"test:watch": {
"command": "cross-env NODE_OPTIONS=--enable-source-maps uvu lib/test \"watch\\.test\\.js$\"",
"dependencies": [
Expand Down
4 changes: 4 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"type": "string"
},
"type": "array"
},
"service": {
"markdownDescription": "If true, treat this script as a long-running process.\nServices are automatically brought up and down as they are depended upon by other scripts. If invoked directly, services continue running until Wireit is killed with Ctrl-C.\nFor more info, see: https://github.com/google/wireit#services",
"type": "boolean"
}
},
"type": "object"
Expand Down
98 changes: 91 additions & 7 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,13 @@ export class Analyzer {
command
);
const clean = this._processClean(placeholder, packageJson, syntaxInfo);
const service = this._processService(
placeholder,
packageJson,
syntaxInfo,
command,
output
);
this._processPackageLocks(placeholder, packageJson, syntaxInfo, files);

// It's important to in-place update the placeholder object, instead of
Expand All @@ -476,7 +483,8 @@ export class Analyzer {
dependencies,
files,
output,
clean: clean ?? true,
clean,
service,
scriptAstNode: scriptCommand,
configAstNode: wireitConfig,
declaringFile: packageJson.jsonFile,
Expand Down Expand Up @@ -741,9 +749,10 @@ export class Analyzer {
placeholder: UnvalidatedConfig,
packageJson: PackageJson,
syntaxInfo: ScriptSyntaxInfo
): undefined | boolean | 'if-file-deleted' {
): boolean | 'if-file-deleted' {
const defaultValue = true;
if (syntaxInfo.wireitConfigNode == null) {
return;
return defaultValue;
}
const clean = findNodeAtLocation(syntaxInfo.wireitConfigNode, ['clean']) as
| undefined
Expand All @@ -767,11 +776,86 @@ export class Analyzer {
},
},
});
// We shouldn't execute if there's failures, but just in case, this is
// likely the safest option.
return false;
return defaultValue;
}
return clean?.value ?? defaultValue;
}

private _processService(
placeholder: UnvalidatedConfig,
packageJson: PackageJson,
syntaxInfo: ScriptSyntaxInfo,
command: JsonAstNode<string> | undefined,
output: ArrayNode<string> | undefined
): boolean {
const defaultValue = false;
if (syntaxInfo.wireitConfigNode == null) {
return defaultValue;
}
const node = findNodeAtLocation(syntaxInfo.wireitConfigNode, [
'service',
]) as undefined | JsonAstNode<true | false>;
if (node == null) {
return defaultValue;
}
if (node.value !== true && node.value !== false) {
placeholder.failures.push({
type: 'failure',
reason: 'invalid-config-syntax',
script: placeholder,
diagnostic: {
severity: 'error',
message: `The "service" property must be either true or false.`,
location: {
file: packageJson.jsonFile,
range: {length: node.length, offset: node.offset},
},
},
});
return defaultValue;
}

const value = node?.value ?? defaultValue;

if (value === true && command == null) {
placeholder.failures.push({
type: 'failure',
reason: 'invalid-config-syntax',
script: placeholder,
diagnostic: {
severity: 'error',
message: `A "service" script must have a "command".`,
location: {
file: packageJson.jsonFile,
range: {
length: node.length,
offset: node.offset,
},
},
},
});
}
return clean?.value;

if (value === true && output != null) {
placeholder.failures.push({
type: 'failure',
reason: 'invalid-config-syntax',
script: placeholder,
diagnostic: {
severity: 'error',
message: `A "service" script cannot have an "output".`,
location: {
file: packageJson.jsonFile,
range: {
length: output.node.length,
offset: output.node.offset,
},
},
},
});
}

return value;
}

private _processPackageLocks(
Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ interface BaseScriptConfig extends ScriptReference {
*/
clean: boolean | 'if-file-deleted';

/**
* Whether the script should run in service mode.
*/
service: boolean;

/**
* The command string in the scripts section. i.e.:
*
Expand Down
96 changes: 96 additions & 0 deletions src/test/errors-analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1737,4 +1737,100 @@ test(
})
);

test(
'service is not a boolean',
timeout(async ({rig}) => {
await rig.write({
'package.json': {
scripts: {
a: 'wireit',
},
wireit: {
a: {
command: 'true',
service: 1,
},
},
},
});
const result = rig.exec('npm run a');
const done = await result.exit;
assert.equal(done.code, 1);
checkScriptOutput(
done.stderr,
`
❌ package.json:8:18 The "service" property must be either true or false.
"service": 1
~`
);
})
);

test(
'service does not have command',
timeout(async ({rig}) => {
await rig.write({
'package.json': {
scripts: {
a: 'wireit',
b: 'wireit',
},
wireit: {
a: {
service: true,
dependencies: ['b'],
},
b: {
command: 'true',
},
},
},
});
const result = rig.exec('npm run a');
const done = await result.exit;
assert.equal(done.code, 1);
checkScriptOutput(
done.stderr,
`
❌ package.json:8:18 A "service" script must have a "command".
"service": true,
~~~~`
);
})
);

test(
'service cannot have output',
timeout(async ({rig}) => {
await rig.write({
'package.json': {
scripts: {
a: 'wireit',
},
wireit: {
a: {
command: 'true',
service: true,
output: ['foo'],
},
},
},
});
const result = rig.exec('npm run a');
const done = await result.exit;
assert.equal(done.code, 1);
checkScriptOutput(
done.stderr,
`
❌ package.json:9:17 A "service" script cannot have an "output".
"output": [
~
"foo"
~~~~~~~~~~~~~
]
~~~~~~~`
);
})
);

test.run();
35 changes: 35 additions & 0 deletions src/test/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {suite} from 'uvu';
import {WireitTestRig} from './util/test-rig.js';

const test = suite<{rig: WireitTestRig}>();

test.before.each(async (ctx) => {
try {
ctx.rig = new WireitTestRig();
await ctx.rig.setup();
} catch (error) {
// Uvu has a bug where it silently ignores failures in before and after,
// see https://github.com/lukeed/uvu/issues/191.
console.error('uvu before error', error);
process.exit(1);
}
});

test.after.each(async (ctx) => {
try {
await ctx.rig.cleanup();
} catch (error) {
// Uvu has a bug where it silently ignores failures in before and after,
// see https://github.com/lukeed/uvu/issues/191.
console.error('uvu after error', error);
process.exit(1);
}
});

test.run();