@laurence79/wireit 0.14.13-shared-cache.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +1062 -0
- package/bin/wireit.js +9 -0
- package/lib/analyzer.js +1600 -0
- package/lib/caching/cache.js +7 -0
- package/lib/caching/github-actions-cache.js +832 -0
- package/lib/caching/local-cache.js +78 -0
- package/lib/caching/shared-cache.js +256 -0
- package/lib/cli-options.js +495 -0
- package/lib/cli.js +177 -0
- package/lib/config.js +18 -0
- package/lib/error.js +160 -0
- package/lib/event.js +7 -0
- package/lib/execution/base.js +108 -0
- package/lib/execution/no-command.js +32 -0
- package/lib/execution/service.js +1017 -0
- package/lib/execution/standard.js +683 -0
- package/lib/executor.js +249 -0
- package/lib/fingerprint.js +164 -0
- package/lib/ide.js +583 -0
- package/lib/language-server.js +135 -0
- package/lib/logging/combination-logger.js +41 -0
- package/lib/logging/debug-logger.js +43 -0
- package/lib/logging/logger.js +38 -0
- package/lib/logging/metrics-logger.js +108 -0
- package/lib/logging/quiet/run-tracker.js +597 -0
- package/lib/logging/quiet/stack-map.js +41 -0
- package/lib/logging/quiet/writeover-line.js +197 -0
- package/lib/logging/quiet-logger.js +78 -0
- package/lib/logging/simple-logger.js +296 -0
- package/lib/logging/watch-logger.js +81 -0
- package/lib/script-child-process.js +270 -0
- package/lib/util/ast.js +71 -0
- package/lib/util/async-cache.js +24 -0
- package/lib/util/copy.js +120 -0
- package/lib/util/deferred.js +35 -0
- package/lib/util/delete.js +120 -0
- package/lib/util/dispose.js +16 -0
- package/lib/util/fs.js +258 -0
- package/lib/util/glob.js +255 -0
- package/lib/util/line-monitor.js +69 -0
- package/lib/util/manifest.js +31 -0
- package/lib/util/optimize-mkdirs.js +55 -0
- package/lib/util/package-json-reader.js +61 -0
- package/lib/util/package-json.js +179 -0
- package/lib/util/script-data-dir.js +19 -0
- package/lib/util/shuffle.js +16 -0
- package/lib/util/unreachable.js +12 -0
- package/lib/util/windows.js +87 -0
- package/lib/util/worker-pool.js +61 -0
- package/lib/watcher.js +396 -0
- package/package.json +470 -0
- package/schema.json +132 -0
- package/wireit.svg +1 -0
package/lib/analyzer.js
ADDED
|
@@ -0,0 +1,1600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import * as pathlib from 'path';
|
|
7
|
+
import { scriptReferenceToString } from './config.js';
|
|
8
|
+
import { findNodeAtLocation } from './util/ast.js';
|
|
9
|
+
import * as fs from './util/fs.js';
|
|
10
|
+
import { CachingPackageJsonReader, } from './util/package-json-reader.js';
|
|
11
|
+
import { IS_WINDOWS } from './util/windows.js';
|
|
12
|
+
/**
|
|
13
|
+
* Globs that will be injected into both `files` and `output`, unless
|
|
14
|
+
* `allowUsuallyExcludedPaths` is `true`.
|
|
15
|
+
*
|
|
16
|
+
* See https://docs.npmjs.com/cli/v9/configuring-npm/package-json#files for the
|
|
17
|
+
* similar list of paths that npm ignores.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_EXCLUDE_PATHS = [
|
|
20
|
+
'!.git/',
|
|
21
|
+
'!.hg/',
|
|
22
|
+
'!.svn/',
|
|
23
|
+
'!.wireit/',
|
|
24
|
+
'!.yarn/',
|
|
25
|
+
'!CVS/',
|
|
26
|
+
'!node_modules/',
|
|
27
|
+
];
|
|
28
|
+
const DEFAULT_LOCKFILES = {
|
|
29
|
+
npm: ['package-lock.json'],
|
|
30
|
+
nodeRun: ['package-lock.json'],
|
|
31
|
+
yarnClassic: ['yarn.lock'],
|
|
32
|
+
yarnBerry: ['yarn.lock'],
|
|
33
|
+
pnpm: ['pnpm-lock.yaml'],
|
|
34
|
+
};
|
|
35
|
+
function isValidWireitScriptCommand(command) {
|
|
36
|
+
return (command === 'wireit' ||
|
|
37
|
+
command === 'yarn run -TB wireit' ||
|
|
38
|
+
// This form is useful when using package managers like yarn or pnpm which
|
|
39
|
+
// do not automatically add all parent directory `node_modules/.bin`
|
|
40
|
+
// folders to PATH.
|
|
41
|
+
/^(\.\.\/)+node_modules\/\.bin\/wireit$/.test(command) ||
|
|
42
|
+
(IS_WINDOWS && /^(\.\.\\)+node_modules\\\.bin\\wireit\.cmd$/.test(command)));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Analyzes and validates a script along with all of its transitive
|
|
46
|
+
* dependencies, producing a build graph that is ready to be executed.
|
|
47
|
+
*/
|
|
48
|
+
export class Analyzer {
|
|
49
|
+
#packageJsonReader;
|
|
50
|
+
#placeholders = new Map();
|
|
51
|
+
#ongoingWorkPromises = [];
|
|
52
|
+
#relevantConfigFilePaths = new Set();
|
|
53
|
+
#agent;
|
|
54
|
+
#logger;
|
|
55
|
+
constructor(agent, logger, filesystem) {
|
|
56
|
+
this.#agent = agent;
|
|
57
|
+
this.#logger = logger;
|
|
58
|
+
this.#packageJsonReader = new CachingPackageJsonReader(filesystem);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Analyze every script in each given file and return all diagnostics found.
|
|
62
|
+
*/
|
|
63
|
+
async analyzeFiles(files) {
|
|
64
|
+
await Promise.all(files.map(async (f) => {
|
|
65
|
+
const packageDir = pathlib.dirname(f);
|
|
66
|
+
const fileResult = await this.getPackageJson(packageDir);
|
|
67
|
+
if (!fileResult.ok) {
|
|
68
|
+
return; // will get this error below.
|
|
69
|
+
}
|
|
70
|
+
for (const script of fileResult.value.scripts) {
|
|
71
|
+
// This starts analysis of each of the scripts in our root files.
|
|
72
|
+
this.#getPlaceholder({ name: script.name, packageDir });
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
await this.#waitForAnalysisToComplete();
|
|
76
|
+
// Check for cycles.
|
|
77
|
+
for (const info of this.#placeholders.values()) {
|
|
78
|
+
if (info.placeholder.state === 'unvalidated') {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// We don't care about the result, if there's a cycle error it'll
|
|
82
|
+
// be added to the scripts' diagnostics.
|
|
83
|
+
this.#checkForCyclesAndSortDependencies(info.placeholder, new Set(), true);
|
|
84
|
+
}
|
|
85
|
+
return this.#getDiagnostics();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Load the Wireit configuration from the `package.json` corresponding to the
|
|
89
|
+
* given script, repeat for all transitive dependencies, and return a build
|
|
90
|
+
* graph that is ready to be executed.
|
|
91
|
+
*
|
|
92
|
+
* Returns a Failure if the given script or any of its transitive
|
|
93
|
+
* dependencies don't exist, are configured in an invalid way, or if there is
|
|
94
|
+
* a cycle in the dependency graph.
|
|
95
|
+
*/
|
|
96
|
+
async analyze(root, extraArgs) {
|
|
97
|
+
this.#logger?.log({
|
|
98
|
+
type: 'info',
|
|
99
|
+
detail: 'analysis-started',
|
|
100
|
+
script: root,
|
|
101
|
+
});
|
|
102
|
+
const analyzeResult = await this.#actuallyAnalyze(root, extraArgs);
|
|
103
|
+
this.#logger?.log({
|
|
104
|
+
type: 'info',
|
|
105
|
+
detail: 'analysis-completed',
|
|
106
|
+
script: root,
|
|
107
|
+
rootScriptConfig: analyzeResult.config.ok
|
|
108
|
+
? analyzeResult.config.value
|
|
109
|
+
: undefined,
|
|
110
|
+
});
|
|
111
|
+
return analyzeResult;
|
|
112
|
+
}
|
|
113
|
+
async #actuallyAnalyze(root, extraArgs) {
|
|
114
|
+
// We do 2 walks through the dependency graph:
|
|
115
|
+
//
|
|
116
|
+
// 1. A non-deterministically ordered walk, where we traverse edges as soon
|
|
117
|
+
// as they are known, to maximize the parallelism of package.json file
|
|
118
|
+
// read operations.
|
|
119
|
+
//
|
|
120
|
+
// 2. A depth-first walk to detect cycles.
|
|
121
|
+
//
|
|
122
|
+
// We can't check for cycles in the 1st walk because its non-deterministic
|
|
123
|
+
// traversal order means that we could miss certain cycle configurations.
|
|
124
|
+
// Plus by doing a separate DFS walk, we'll always return the exact same
|
|
125
|
+
// trail in the error message for any given graph, instead of an arbitrary
|
|
126
|
+
// one.
|
|
127
|
+
//
|
|
128
|
+
// The way we avoid getting stuck in cycles during the 1st walk is by
|
|
129
|
+
// allocating an initial placeholder object for each script, and caching it
|
|
130
|
+
// by package + name. Then, instead of blocking each script on its
|
|
131
|
+
// dependencies (which would lead to a promise cycle if there was a cycle in
|
|
132
|
+
// the configuration), we wait for all placeholders to upgrade to full
|
|
133
|
+
// configs asynchronously.
|
|
134
|
+
const rootPlaceholder = this.#getPlaceholder(root);
|
|
135
|
+
// Note we can't use Promise.all here, because new promises can be added to
|
|
136
|
+
// the promises array as long as any promise is pending.
|
|
137
|
+
await this.#waitForAnalysisToComplete();
|
|
138
|
+
{
|
|
139
|
+
const errors = await this.#getDiagnostics();
|
|
140
|
+
if (errors.size > 0) {
|
|
141
|
+
return {
|
|
142
|
+
config: { ok: false, error: [...errors] },
|
|
143
|
+
relevantConfigFilePaths: this.#relevantConfigFilePaths,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// We can safely assume all placeholders have now been upgraded to full
|
|
148
|
+
// configs.
|
|
149
|
+
const rootConfig = rootPlaceholder.placeholder;
|
|
150
|
+
if (rootConfig.state === 'unvalidated') {
|
|
151
|
+
throw new Error(`Internal error: script ${root.name} in ${root.packageDir} is still unvalidated but had no failures`);
|
|
152
|
+
}
|
|
153
|
+
const cycleResult = this.#checkForCyclesAndSortDependencies(rootConfig, new Set(), true);
|
|
154
|
+
if (!cycleResult.ok) {
|
|
155
|
+
return {
|
|
156
|
+
config: { ok: false, error: [cycleResult.error.dependencyFailure] },
|
|
157
|
+
relevantConfigFilePaths: this.#relevantConfigFilePaths,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const validRootConfig = cycleResult.value;
|
|
161
|
+
validRootConfig.extraArgs = extraArgs;
|
|
162
|
+
return {
|
|
163
|
+
config: { ok: true, value: validRootConfig },
|
|
164
|
+
relevantConfigFilePaths: this.#relevantConfigFilePaths,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async analyzeIgnoringErrors(scriptReference) {
|
|
168
|
+
await this.analyze(scriptReference, []);
|
|
169
|
+
return this.#getPlaceholder(scriptReference).placeholder;
|
|
170
|
+
}
|
|
171
|
+
async #getDiagnostics() {
|
|
172
|
+
const failures = new Set();
|
|
173
|
+
for await (const failure of this.#packageJsonReader.getFailures()) {
|
|
174
|
+
failures.add(failure);
|
|
175
|
+
}
|
|
176
|
+
for (const info of this.#placeholders.values()) {
|
|
177
|
+
for (const failure of info.placeholder.failures) {
|
|
178
|
+
failures.add(failure);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const failure of failures) {
|
|
182
|
+
const supercedes = failure
|
|
183
|
+
.supercedes;
|
|
184
|
+
if (supercedes !== undefined) {
|
|
185
|
+
failures.delete(supercedes);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return failures;
|
|
189
|
+
}
|
|
190
|
+
async #waitForAnalysisToComplete() {
|
|
191
|
+
while (this.#ongoingWorkPromises.length > 0) {
|
|
192
|
+
const promise = this.#ongoingWorkPromises[this.#ongoingWorkPromises.length - 1];
|
|
193
|
+
await promise;
|
|
194
|
+
// Need to be careful here. The contract of this method is that it does
|
|
195
|
+
// not return until all pending analysis work is completed.
|
|
196
|
+
// If there are multiple concurrent callers to this method, we want to
|
|
197
|
+
// make sure that none of them hide any of the pending work from each
|
|
198
|
+
// other by removing a promise from the array before it has settled.
|
|
199
|
+
// So we first await the promise, and then remove it from the array if
|
|
200
|
+
// it's still the final element.
|
|
201
|
+
// It might not be the final element because another caller removed it,
|
|
202
|
+
// or because more work was added onto the end of the array. Either
|
|
203
|
+
// case is fine.
|
|
204
|
+
if (promise ===
|
|
205
|
+
this.#ongoingWorkPromises[this.#ongoingWorkPromises.length - 1]) {
|
|
206
|
+
void this.#ongoingWorkPromises.pop();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async getPackageJson(packageDir) {
|
|
211
|
+
this.#relevantConfigFilePaths.add(pathlib.join(packageDir, 'package.json'));
|
|
212
|
+
return this.#packageJsonReader.read(packageDir);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Adds the given package.json files to the known set, and analyzes all
|
|
216
|
+
* scripts reachable from any of them, recursively.
|
|
217
|
+
*
|
|
218
|
+
* Useful for whole program analysis, e.g. for "find all references" in the
|
|
219
|
+
* IDE.
|
|
220
|
+
*/
|
|
221
|
+
async analyzeAllScripts(packageJsonPaths) {
|
|
222
|
+
const done = new Set();
|
|
223
|
+
const todo = [];
|
|
224
|
+
for (const file of packageJsonPaths) {
|
|
225
|
+
const packageDir = pathlib.dirname(file);
|
|
226
|
+
const packageJsonResult = await this.getPackageJson(packageDir);
|
|
227
|
+
if (!packageJsonResult.ok) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
for (const script of packageJsonResult.value.scripts) {
|
|
231
|
+
todo.push({ name: script.name, packageDir });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
while (true) {
|
|
235
|
+
await Promise.all(todo.map(async (ref) => {
|
|
236
|
+
await this.analyze(ref, undefined);
|
|
237
|
+
done.add(scriptReferenceToString(ref));
|
|
238
|
+
}));
|
|
239
|
+
todo.length = 0;
|
|
240
|
+
for (const info of this.#placeholders.values()) {
|
|
241
|
+
if (info.placeholder.state === 'unvalidated' &&
|
|
242
|
+
!done.has(scriptReferenceToString(info.placeholder))) {
|
|
243
|
+
todo.push(info.placeholder);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (todo.length === 0) {
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return this.#placeholders.values();
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Create or return a cached placeholder script configuration object for the
|
|
254
|
+
* given script reference.
|
|
255
|
+
*/
|
|
256
|
+
#getPlaceholder(reference) {
|
|
257
|
+
const scriptKey = scriptReferenceToString(reference);
|
|
258
|
+
let placeholderInfo = this.#placeholders.get(scriptKey);
|
|
259
|
+
if (placeholderInfo === undefined) {
|
|
260
|
+
const placeholder = {
|
|
261
|
+
...reference,
|
|
262
|
+
state: 'unvalidated',
|
|
263
|
+
failures: [],
|
|
264
|
+
};
|
|
265
|
+
placeholderInfo = {
|
|
266
|
+
placeholder: placeholder,
|
|
267
|
+
upgradeComplete: this.#upgradePlaceholder(placeholder),
|
|
268
|
+
};
|
|
269
|
+
this.#placeholders.set(scriptKey, placeholderInfo);
|
|
270
|
+
this.#ongoingWorkPromises.push(placeholderInfo.upgradeComplete);
|
|
271
|
+
}
|
|
272
|
+
return placeholderInfo;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* In-place upgrade the given placeholder script configuration object to a
|
|
276
|
+
* full configuration, by reading its package.json file.
|
|
277
|
+
*
|
|
278
|
+
* Note this method does not block on the script's dependencies being
|
|
279
|
+
* upgraded; dependencies are upgraded asynchronously.
|
|
280
|
+
*/
|
|
281
|
+
async #upgradePlaceholder(placeholder) {
|
|
282
|
+
const packageJsonResult = await this.getPackageJson(placeholder.packageDir);
|
|
283
|
+
if (!packageJsonResult.ok) {
|
|
284
|
+
placeholder.failures.push(packageJsonResult.error);
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const packageJson = packageJsonResult.value;
|
|
288
|
+
placeholder.failures.push(...packageJson.failures);
|
|
289
|
+
const syntaxInfo = packageJson.getScriptInfo(placeholder.name);
|
|
290
|
+
if (syntaxInfo?.wireitConfigNode !== undefined) {
|
|
291
|
+
await this.#handleWireitScript(placeholder, packageJson, syntaxInfo, syntaxInfo.wireitConfigNode);
|
|
292
|
+
}
|
|
293
|
+
else if (syntaxInfo?.scriptNode !== undefined) {
|
|
294
|
+
this.#handlePlainNpmScript(placeholder, packageJson, syntaxInfo.scriptNode);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
placeholder.failures.push({
|
|
298
|
+
type: 'failure',
|
|
299
|
+
reason: 'script-not-found',
|
|
300
|
+
script: placeholder,
|
|
301
|
+
diagnostic: {
|
|
302
|
+
severity: 'error',
|
|
303
|
+
message: `Script "${placeholder.name}" not found in the scripts section of this package.json.`,
|
|
304
|
+
location: {
|
|
305
|
+
file: packageJson.jsonFile,
|
|
306
|
+
range: { offset: 0, length: 0 },
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
#handlePlainNpmScript(placeholder, packageJson, scriptCommand) {
|
|
314
|
+
if (isValidWireitScriptCommand(scriptCommand.value)) {
|
|
315
|
+
placeholder.failures.push({
|
|
316
|
+
type: 'failure',
|
|
317
|
+
reason: 'invalid-config-syntax',
|
|
318
|
+
script: placeholder,
|
|
319
|
+
diagnostic: {
|
|
320
|
+
severity: 'error',
|
|
321
|
+
message: `This script is configured to run wireit but it has no config in the wireit section of this package.json file`,
|
|
322
|
+
location: {
|
|
323
|
+
file: packageJson.jsonFile,
|
|
324
|
+
range: {
|
|
325
|
+
length: scriptCommand.length,
|
|
326
|
+
offset: scriptCommand.offset,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// It's important to in-place update the placeholder object, instead of
|
|
333
|
+
// creating a new object, because other configs may be referencing this
|
|
334
|
+
// exact object in their dependencies.
|
|
335
|
+
const remainingConfig = {
|
|
336
|
+
...placeholder,
|
|
337
|
+
state: 'locally-valid',
|
|
338
|
+
failures: placeholder.failures,
|
|
339
|
+
command: scriptCommand,
|
|
340
|
+
extraArgs: undefined,
|
|
341
|
+
dependencies: [],
|
|
342
|
+
files: undefined,
|
|
343
|
+
output: undefined,
|
|
344
|
+
clean: false,
|
|
345
|
+
service: undefined,
|
|
346
|
+
scriptAstNode: scriptCommand,
|
|
347
|
+
configAstNode: undefined,
|
|
348
|
+
declaringFile: packageJson.jsonFile,
|
|
349
|
+
services: [],
|
|
350
|
+
env: {},
|
|
351
|
+
};
|
|
352
|
+
Object.assign(placeholder, remainingConfig);
|
|
353
|
+
}
|
|
354
|
+
async #handleWireitScript(placeholder, packageJson, syntaxInfo, wireitConfig) {
|
|
355
|
+
const scriptCommand = syntaxInfo.scriptNode;
|
|
356
|
+
if (scriptCommand !== undefined &&
|
|
357
|
+
!isValidWireitScriptCommand(scriptCommand.value)) {
|
|
358
|
+
{
|
|
359
|
+
const configName = wireitConfig.name;
|
|
360
|
+
placeholder.failures.push({
|
|
361
|
+
type: 'failure',
|
|
362
|
+
reason: 'script-not-wireit',
|
|
363
|
+
script: placeholder,
|
|
364
|
+
diagnostic: {
|
|
365
|
+
message: `This command should just be "wireit", ` +
|
|
366
|
+
`as this script is configured in the wireit section.`,
|
|
367
|
+
severity: 'warning',
|
|
368
|
+
location: {
|
|
369
|
+
file: packageJson.jsonFile,
|
|
370
|
+
range: {
|
|
371
|
+
length: scriptCommand.length,
|
|
372
|
+
offset: scriptCommand.offset,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
supplementalLocations: [
|
|
376
|
+
{
|
|
377
|
+
message: `The wireit config is here.`,
|
|
378
|
+
location: {
|
|
379
|
+
file: packageJson.jsonFile,
|
|
380
|
+
range: {
|
|
381
|
+
length: configName.length,
|
|
382
|
+
offset: configName.offset,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const { dependencies, encounteredError: dependenciesErrored } = this.#processDependencies(placeholder, packageJson, syntaxInfo);
|
|
392
|
+
let command;
|
|
393
|
+
let commandError = false;
|
|
394
|
+
const commandAst = findNodeAtLocation(wireitConfig, ['command']);
|
|
395
|
+
if (commandAst !== undefined) {
|
|
396
|
+
const result = failUnlessNonBlankString(commandAst, packageJson.jsonFile);
|
|
397
|
+
if (result.ok) {
|
|
398
|
+
command = result.value;
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
commandError = true;
|
|
402
|
+
placeholder.failures.push(result.error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const allowUsuallyExcludedPaths = this.#processAllowUsuallyExcludedPaths(placeholder, packageJson, syntaxInfo);
|
|
406
|
+
const files = this.#processFiles(placeholder, packageJson, syntaxInfo, allowUsuallyExcludedPaths);
|
|
407
|
+
if (dependencies.length === 0 &&
|
|
408
|
+
!dependenciesErrored &&
|
|
409
|
+
command === undefined &&
|
|
410
|
+
!commandError &&
|
|
411
|
+
(files === undefined || files.values.length === 0)) {
|
|
412
|
+
placeholder.failures.push({
|
|
413
|
+
type: 'failure',
|
|
414
|
+
reason: 'invalid-config-syntax',
|
|
415
|
+
script: placeholder,
|
|
416
|
+
diagnostic: {
|
|
417
|
+
severity: 'error',
|
|
418
|
+
message: `A wireit config must set at least one of "command", "dependencies", or "files". Otherwise there is nothing for wireit to do.`,
|
|
419
|
+
location: {
|
|
420
|
+
file: packageJson.jsonFile,
|
|
421
|
+
range: {
|
|
422
|
+
length: wireitConfig.name.length,
|
|
423
|
+
offset: wireitConfig.name.offset,
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const output = this.#processOutput(placeholder, packageJson, syntaxInfo, command, allowUsuallyExcludedPaths);
|
|
430
|
+
const clean = this.#processClean(placeholder, packageJson, syntaxInfo);
|
|
431
|
+
const service = this.#processService(placeholder, packageJson, syntaxInfo, command, output);
|
|
432
|
+
await this.#processPackageLocks(placeholder, packageJson, syntaxInfo, files);
|
|
433
|
+
const env = this.#processEnv(placeholder, packageJson, syntaxInfo, command);
|
|
434
|
+
if (placeholder.failures.length > 0) {
|
|
435
|
+
// A script with locally-determined errors doesn't get upgraded to
|
|
436
|
+
// locally-valid.
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// It's important to in-place update the placeholder object, instead of
|
|
440
|
+
// creating a new object, because other configs may be referencing this
|
|
441
|
+
// exact object in their dependencies.
|
|
442
|
+
const remainingConfig = {
|
|
443
|
+
...placeholder,
|
|
444
|
+
state: 'locally-valid',
|
|
445
|
+
failures: placeholder.failures,
|
|
446
|
+
command,
|
|
447
|
+
extraArgs: undefined,
|
|
448
|
+
dependencies,
|
|
449
|
+
files,
|
|
450
|
+
output,
|
|
451
|
+
clean,
|
|
452
|
+
service,
|
|
453
|
+
scriptAstNode: scriptCommand,
|
|
454
|
+
configAstNode: wireitConfig,
|
|
455
|
+
declaringFile: packageJson.jsonFile,
|
|
456
|
+
services: [],
|
|
457
|
+
env,
|
|
458
|
+
};
|
|
459
|
+
Object.assign(placeholder, remainingConfig);
|
|
460
|
+
}
|
|
461
|
+
#processDependencies(placeholder, packageJson, scriptInfo) {
|
|
462
|
+
const dependencies = [];
|
|
463
|
+
const dependenciesAst = scriptInfo.wireitConfigNode &&
|
|
464
|
+
findNodeAtLocation(scriptInfo.wireitConfigNode, ['dependencies']);
|
|
465
|
+
let encounteredError = false;
|
|
466
|
+
if (dependenciesAst === undefined) {
|
|
467
|
+
return { dependencies, encounteredError };
|
|
468
|
+
}
|
|
469
|
+
const result = failUnlessArray(dependenciesAst, packageJson.jsonFile);
|
|
470
|
+
if (!result.ok) {
|
|
471
|
+
encounteredError = true;
|
|
472
|
+
placeholder.failures.push(result.error);
|
|
473
|
+
return { dependencies, encounteredError };
|
|
474
|
+
}
|
|
475
|
+
// Error if the same dependency is declared multiple times. Duplicate
|
|
476
|
+
// dependencies aren't necessarily a serious problem (since we already
|
|
477
|
+
// prevent double-analysis here, and double-analysis in the Executor), but
|
|
478
|
+
// they may indicate that the user has made a mistake (e.g. maybe they
|
|
479
|
+
// meant a different dependency).
|
|
480
|
+
const uniqueDependencies = new Map();
|
|
481
|
+
const children = dependenciesAst.children ?? [];
|
|
482
|
+
for (const maybeUnresolved of children) {
|
|
483
|
+
// A dependency can be either a plain string, or an object with a "script"
|
|
484
|
+
// property plus optional extra annotations.
|
|
485
|
+
let specifierResult;
|
|
486
|
+
let cascade = true; // Default;
|
|
487
|
+
if (maybeUnresolved.type === 'string') {
|
|
488
|
+
specifierResult = failUnlessNonBlankString(maybeUnresolved, packageJson.jsonFile);
|
|
489
|
+
if (!specifierResult.ok) {
|
|
490
|
+
encounteredError = true;
|
|
491
|
+
placeholder.failures.push(specifierResult.error);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else if (maybeUnresolved.type === 'object') {
|
|
496
|
+
specifierResult = findNodeAtLocation(maybeUnresolved, ['script']);
|
|
497
|
+
if (specifierResult === undefined) {
|
|
498
|
+
encounteredError = true;
|
|
499
|
+
placeholder.failures.push({
|
|
500
|
+
type: 'failure',
|
|
501
|
+
reason: 'invalid-config-syntax',
|
|
502
|
+
script: { packageDir: pathlib.dirname(packageJson.jsonFile.path) },
|
|
503
|
+
diagnostic: {
|
|
504
|
+
severity: 'error',
|
|
505
|
+
message: `Dependency object must set a "script" property.`,
|
|
506
|
+
location: {
|
|
507
|
+
file: packageJson.jsonFile,
|
|
508
|
+
range: {
|
|
509
|
+
offset: maybeUnresolved.offset,
|
|
510
|
+
length: maybeUnresolved.length,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
specifierResult = failUnlessNonBlankString(specifierResult, packageJson.jsonFile);
|
|
518
|
+
if (!specifierResult.ok) {
|
|
519
|
+
encounteredError = true;
|
|
520
|
+
placeholder.failures.push(specifierResult.error);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const cascadeResult = findNodeAtLocation(maybeUnresolved, ['cascade']);
|
|
524
|
+
if (cascadeResult !== undefined) {
|
|
525
|
+
if (cascadeResult.value === true || cascadeResult.value === false) {
|
|
526
|
+
cascade = cascadeResult.value;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
encounteredError = true;
|
|
530
|
+
placeholder.failures.push({
|
|
531
|
+
type: 'failure',
|
|
532
|
+
reason: 'invalid-config-syntax',
|
|
533
|
+
script: { packageDir: pathlib.dirname(packageJson.jsonFile.path) },
|
|
534
|
+
diagnostic: {
|
|
535
|
+
severity: 'error',
|
|
536
|
+
message: `The "cascade" property must be either true or false.`,
|
|
537
|
+
location: {
|
|
538
|
+
file: packageJson.jsonFile,
|
|
539
|
+
range: {
|
|
540
|
+
offset: cascadeResult.offset,
|
|
541
|
+
length: cascadeResult.length,
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
encounteredError = true;
|
|
552
|
+
placeholder.failures.push({
|
|
553
|
+
type: 'failure',
|
|
554
|
+
reason: 'invalid-config-syntax',
|
|
555
|
+
script: { packageDir: pathlib.dirname(packageJson.jsonFile.path) },
|
|
556
|
+
diagnostic: {
|
|
557
|
+
severity: 'error',
|
|
558
|
+
message: `Expected a string or object, but was ${maybeUnresolved.type}.`,
|
|
559
|
+
location: {
|
|
560
|
+
file: packageJson.jsonFile,
|
|
561
|
+
range: {
|
|
562
|
+
offset: maybeUnresolved.offset,
|
|
563
|
+
length: maybeUnresolved.length,
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const unresolved = specifierResult.value;
|
|
571
|
+
const result = this.#resolveDependency(unresolved, placeholder, packageJson.jsonFile);
|
|
572
|
+
if (!result.ok) {
|
|
573
|
+
encounteredError = true;
|
|
574
|
+
placeholder.failures.push(result.error);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
for (const resolved of result.value) {
|
|
578
|
+
const uniqueKey = scriptReferenceToString(resolved);
|
|
579
|
+
const duplicate = uniqueDependencies.get(uniqueKey);
|
|
580
|
+
if (duplicate !== undefined) {
|
|
581
|
+
encounteredError = true;
|
|
582
|
+
placeholder.failures.push({
|
|
583
|
+
type: 'failure',
|
|
584
|
+
reason: 'duplicate-dependency',
|
|
585
|
+
script: placeholder,
|
|
586
|
+
dependency: resolved,
|
|
587
|
+
diagnostic: {
|
|
588
|
+
severity: 'error',
|
|
589
|
+
message: `This dependency is listed multiple times`,
|
|
590
|
+
location: {
|
|
591
|
+
file: packageJson.jsonFile,
|
|
592
|
+
range: {
|
|
593
|
+
offset: unresolved.offset,
|
|
594
|
+
length: unresolved.length,
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
supplementalLocations: [
|
|
598
|
+
{
|
|
599
|
+
message: `The dependency was first listed here.`,
|
|
600
|
+
location: {
|
|
601
|
+
file: packageJson.jsonFile,
|
|
602
|
+
range: {
|
|
603
|
+
offset: duplicate.offset,
|
|
604
|
+
length: duplicate.length,
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
uniqueDependencies.set(uniqueKey, unresolved);
|
|
613
|
+
const placeHolderInfo = this.#getPlaceholder(resolved);
|
|
614
|
+
dependencies.push({
|
|
615
|
+
specifier: unresolved,
|
|
616
|
+
config: placeHolderInfo.placeholder,
|
|
617
|
+
cascade,
|
|
618
|
+
});
|
|
619
|
+
this.#ongoingWorkPromises.push((async () => {
|
|
620
|
+
await placeHolderInfo.upgradeComplete;
|
|
621
|
+
const failures = placeHolderInfo.placeholder.failures;
|
|
622
|
+
for (const failure of failures) {
|
|
623
|
+
if (failure.reason === 'script-not-found') {
|
|
624
|
+
const hasColon = unresolved.value.includes(':');
|
|
625
|
+
let offset;
|
|
626
|
+
let length;
|
|
627
|
+
if (!hasColon ||
|
|
628
|
+
resolved.packageDir === placeholder.packageDir) {
|
|
629
|
+
offset = unresolved.offset;
|
|
630
|
+
length = unresolved.length;
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
// Skip past the colon
|
|
634
|
+
const colonOffsetInString = packageJson.jsonFile.contents
|
|
635
|
+
.slice(unresolved.offset)
|
|
636
|
+
.indexOf(':');
|
|
637
|
+
offset = unresolved.offset + colonOffsetInString + 1;
|
|
638
|
+
length = unresolved.length - colonOffsetInString - 2;
|
|
639
|
+
}
|
|
640
|
+
placeholder.failures.push({
|
|
641
|
+
type: 'failure',
|
|
642
|
+
reason: 'dependency-on-missing-script',
|
|
643
|
+
script: placeholder,
|
|
644
|
+
supercedes: failure,
|
|
645
|
+
diagnostic: {
|
|
646
|
+
severity: 'error',
|
|
647
|
+
message: `Cannot find script named ${JSON.stringify(resolved.name)} in package "${resolved.packageDir}"`,
|
|
648
|
+
location: {
|
|
649
|
+
file: packageJson.jsonFile,
|
|
650
|
+
range: { offset, length },
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
else if (failure.reason === 'missing-package-json') {
|
|
656
|
+
// Skip the opening "
|
|
657
|
+
const offset = unresolved.offset + 1;
|
|
658
|
+
// Take everything up to the first colon, but look in
|
|
659
|
+
// the original source, to avoid getting confused by escape
|
|
660
|
+
// sequences, which have a different length before and after
|
|
661
|
+
// encoding.
|
|
662
|
+
const length = packageJson.jsonFile.contents
|
|
663
|
+
.slice(offset)
|
|
664
|
+
.indexOf(':');
|
|
665
|
+
const range = { offset, length };
|
|
666
|
+
placeholder.failures.push({
|
|
667
|
+
type: 'failure',
|
|
668
|
+
reason: 'dependency-on-missing-package-json',
|
|
669
|
+
script: placeholder,
|
|
670
|
+
supercedes: failure,
|
|
671
|
+
diagnostic: {
|
|
672
|
+
severity: 'error',
|
|
673
|
+
message: `package.json file missing: "${pathlib.join(resolved.packageDir, 'package.json')}"`,
|
|
674
|
+
location: { file: packageJson.jsonFile, range },
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return undefined;
|
|
680
|
+
})());
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return { dependencies, encounteredError };
|
|
684
|
+
}
|
|
685
|
+
#processAllowUsuallyExcludedPaths(placeholder, packageJson, syntaxInfo) {
|
|
686
|
+
const defaultValue = false;
|
|
687
|
+
if (syntaxInfo.wireitConfigNode == null) {
|
|
688
|
+
return defaultValue;
|
|
689
|
+
}
|
|
690
|
+
const node = findNodeAtLocation(syntaxInfo.wireitConfigNode, [
|
|
691
|
+
'allowUsuallyExcludedPaths',
|
|
692
|
+
]);
|
|
693
|
+
if (node === undefined) {
|
|
694
|
+
return defaultValue;
|
|
695
|
+
}
|
|
696
|
+
if (node.value === true || node.value === false) {
|
|
697
|
+
return node.value;
|
|
698
|
+
}
|
|
699
|
+
placeholder.failures.push({
|
|
700
|
+
type: 'failure',
|
|
701
|
+
reason: 'invalid-config-syntax',
|
|
702
|
+
script: placeholder,
|
|
703
|
+
diagnostic: {
|
|
704
|
+
severity: 'error',
|
|
705
|
+
message: `Must be true or false`,
|
|
706
|
+
location: {
|
|
707
|
+
file: packageJson.jsonFile,
|
|
708
|
+
range: { length: node.length, offset: node.offset },
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
return defaultValue;
|
|
713
|
+
}
|
|
714
|
+
#processFiles(placeholder, packageJson, syntaxInfo, allowUsuallyExcludedPaths) {
|
|
715
|
+
if (syntaxInfo.wireitConfigNode === undefined) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const filesNode = findNodeAtLocation(syntaxInfo.wireitConfigNode, [
|
|
719
|
+
'files',
|
|
720
|
+
]);
|
|
721
|
+
if (filesNode === undefined) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const values = [];
|
|
725
|
+
const result = failUnlessArray(filesNode, packageJson.jsonFile);
|
|
726
|
+
if (!result.ok) {
|
|
727
|
+
placeholder.failures.push(result.error);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const children = filesNode.children ?? [];
|
|
731
|
+
for (const file of children) {
|
|
732
|
+
const result = failUnlessNonBlankString(file, packageJson.jsonFile);
|
|
733
|
+
if (!result.ok) {
|
|
734
|
+
placeholder.failures.push(result.error);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
values.push(result.value.value);
|
|
738
|
+
}
|
|
739
|
+
if (!allowUsuallyExcludedPaths && values.length > 0) {
|
|
740
|
+
values.push(...DEFAULT_EXCLUDE_PATHS);
|
|
741
|
+
}
|
|
742
|
+
return { node: filesNode, values };
|
|
743
|
+
}
|
|
744
|
+
#processOutput(placeholder, packageJson, syntaxInfo, command, allowUsuallyExcludedPaths) {
|
|
745
|
+
if (syntaxInfo.wireitConfigNode === undefined) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const outputNode = findNodeAtLocation(syntaxInfo.wireitConfigNode, [
|
|
749
|
+
'output',
|
|
750
|
+
]);
|
|
751
|
+
if (outputNode === undefined) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (command === undefined) {
|
|
755
|
+
placeholder.failures.push({
|
|
756
|
+
type: 'failure',
|
|
757
|
+
reason: 'invalid-config-syntax',
|
|
758
|
+
script: placeholder,
|
|
759
|
+
diagnostic: {
|
|
760
|
+
severity: 'error',
|
|
761
|
+
message: `"output" can only be set if "command" is also set.`,
|
|
762
|
+
location: {
|
|
763
|
+
file: packageJson.jsonFile,
|
|
764
|
+
range: {
|
|
765
|
+
// Highlight the whole `"output": []` part.
|
|
766
|
+
length: (outputNode.parent ?? outputNode).length,
|
|
767
|
+
offset: (outputNode.parent ?? outputNode).offset,
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
const values = [];
|
|
774
|
+
const result = failUnlessArray(outputNode, packageJson.jsonFile);
|
|
775
|
+
if (!result.ok) {
|
|
776
|
+
placeholder.failures.push(result.error);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const children = outputNode.children ?? [];
|
|
780
|
+
for (const anOutput of children) {
|
|
781
|
+
const result = failUnlessNonBlankString(anOutput, packageJson.jsonFile);
|
|
782
|
+
if (!result.ok) {
|
|
783
|
+
placeholder.failures.push(result.error);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
values.push(result.value.value);
|
|
787
|
+
}
|
|
788
|
+
if (!allowUsuallyExcludedPaths && values.length > 0) {
|
|
789
|
+
values.push(...DEFAULT_EXCLUDE_PATHS);
|
|
790
|
+
}
|
|
791
|
+
return { node: outputNode, values };
|
|
792
|
+
}
|
|
793
|
+
#processClean(placeholder, packageJson, syntaxInfo) {
|
|
794
|
+
const defaultValue = true;
|
|
795
|
+
if (syntaxInfo.wireitConfigNode == null) {
|
|
796
|
+
return defaultValue;
|
|
797
|
+
}
|
|
798
|
+
const clean = findNodeAtLocation(syntaxInfo.wireitConfigNode, ['clean']);
|
|
799
|
+
if (clean !== undefined &&
|
|
800
|
+
clean.value !== true &&
|
|
801
|
+
clean.value !== false &&
|
|
802
|
+
clean.value !== 'if-file-deleted') {
|
|
803
|
+
placeholder.failures.push({
|
|
804
|
+
type: 'failure',
|
|
805
|
+
reason: 'invalid-config-syntax',
|
|
806
|
+
script: placeholder,
|
|
807
|
+
diagnostic: {
|
|
808
|
+
severity: 'error',
|
|
809
|
+
message: `The "clean" property must be either true, false, or "if-file-deleted".`,
|
|
810
|
+
location: {
|
|
811
|
+
file: packageJson.jsonFile,
|
|
812
|
+
range: { length: clean.length, offset: clean.offset },
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
return defaultValue;
|
|
817
|
+
}
|
|
818
|
+
return clean?.value ?? defaultValue;
|
|
819
|
+
}
|
|
820
|
+
#processService(placeholder, packageJson, syntaxInfo, command, output) {
|
|
821
|
+
if (syntaxInfo.wireitConfigNode === undefined) {
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
824
|
+
const serviceNode = findNodeAtLocation(syntaxInfo.wireitConfigNode, [
|
|
825
|
+
'service',
|
|
826
|
+
]);
|
|
827
|
+
if (serviceNode === undefined) {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
if (serviceNode.value === false) {
|
|
831
|
+
return undefined;
|
|
832
|
+
}
|
|
833
|
+
if (serviceNode.value !== true && serviceNode.type !== 'object') {
|
|
834
|
+
placeholder.failures.push({
|
|
835
|
+
type: 'failure',
|
|
836
|
+
reason: 'invalid-config-syntax',
|
|
837
|
+
script: placeholder,
|
|
838
|
+
diagnostic: {
|
|
839
|
+
severity: 'error',
|
|
840
|
+
message: `The "service" property must be either true, false, or an object.`,
|
|
841
|
+
location: {
|
|
842
|
+
file: packageJson.jsonFile,
|
|
843
|
+
range: { length: serviceNode.length, offset: serviceNode.offset },
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
return undefined;
|
|
848
|
+
}
|
|
849
|
+
let lineMatches = undefined;
|
|
850
|
+
if (serviceNode.type === 'object') {
|
|
851
|
+
const waitForNode = findNodeAtLocation(serviceNode, ['readyWhen']);
|
|
852
|
+
if (waitForNode !== undefined) {
|
|
853
|
+
if (waitForNode.type !== 'object') {
|
|
854
|
+
placeholder.failures.push({
|
|
855
|
+
type: 'failure',
|
|
856
|
+
reason: 'invalid-config-syntax',
|
|
857
|
+
script: placeholder,
|
|
858
|
+
diagnostic: {
|
|
859
|
+
severity: 'error',
|
|
860
|
+
message: `Expected an object.`,
|
|
861
|
+
location: {
|
|
862
|
+
file: packageJson.jsonFile,
|
|
863
|
+
range: { length: serviceNode.length, offset: serviceNode.offset },
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
const lineMatchesNode = findNodeAtLocation(waitForNode, [
|
|
870
|
+
'lineMatches',
|
|
871
|
+
]);
|
|
872
|
+
if (lineMatchesNode !== undefined) {
|
|
873
|
+
if (lineMatchesNode.type !== 'string') {
|
|
874
|
+
placeholder.failures.push({
|
|
875
|
+
type: 'failure',
|
|
876
|
+
reason: 'invalid-config-syntax',
|
|
877
|
+
script: placeholder,
|
|
878
|
+
diagnostic: {
|
|
879
|
+
severity: 'error',
|
|
880
|
+
message: `Expected a string.`,
|
|
881
|
+
location: {
|
|
882
|
+
file: packageJson.jsonFile,
|
|
883
|
+
range: {
|
|
884
|
+
length: lineMatchesNode.length,
|
|
885
|
+
offset: lineMatchesNode.offset,
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
try {
|
|
893
|
+
lineMatches = new RegExp(lineMatchesNode.value);
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
placeholder.failures.push({
|
|
897
|
+
type: 'failure',
|
|
898
|
+
reason: 'invalid-config-syntax',
|
|
899
|
+
script: placeholder,
|
|
900
|
+
diagnostic: {
|
|
901
|
+
severity: 'error',
|
|
902
|
+
message: String(error),
|
|
903
|
+
location: {
|
|
904
|
+
file: packageJson.jsonFile,
|
|
905
|
+
range: {
|
|
906
|
+
length: lineMatchesNode.length,
|
|
907
|
+
offset: lineMatchesNode.offset,
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (command === undefined) {
|
|
919
|
+
placeholder.failures.push({
|
|
920
|
+
type: 'failure',
|
|
921
|
+
reason: 'invalid-config-syntax',
|
|
922
|
+
script: placeholder,
|
|
923
|
+
diagnostic: {
|
|
924
|
+
severity: 'error',
|
|
925
|
+
message: `A "service" script must have a "command".`,
|
|
926
|
+
location: {
|
|
927
|
+
file: packageJson.jsonFile,
|
|
928
|
+
range: {
|
|
929
|
+
length: serviceNode.length,
|
|
930
|
+
offset: serviceNode.offset,
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
if (output !== undefined) {
|
|
937
|
+
placeholder.failures.push({
|
|
938
|
+
type: 'failure',
|
|
939
|
+
reason: 'invalid-config-syntax',
|
|
940
|
+
script: placeholder,
|
|
941
|
+
diagnostic: {
|
|
942
|
+
severity: 'error',
|
|
943
|
+
message: `A "service" script cannot have an "output".`,
|
|
944
|
+
location: {
|
|
945
|
+
file: packageJson.jsonFile,
|
|
946
|
+
range: {
|
|
947
|
+
length: output.node.length,
|
|
948
|
+
offset: output.node.offset,
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return { readyWhen: { lineMatches } };
|
|
955
|
+
}
|
|
956
|
+
async #processPackageLocks(placeholder, packageJson, syntaxInfo, files) {
|
|
957
|
+
if (syntaxInfo.wireitConfigNode === undefined) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const packageLocksNode = findNodeAtLocation(syntaxInfo.wireitConfigNode, [
|
|
961
|
+
'packageLocks',
|
|
962
|
+
]);
|
|
963
|
+
let packageLocks;
|
|
964
|
+
if (packageLocksNode !== undefined) {
|
|
965
|
+
const result = failUnlessArray(packageLocksNode, packageJson.jsonFile);
|
|
966
|
+
if (!result.ok) {
|
|
967
|
+
placeholder.failures.push(result.error);
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
packageLocks = { node: packageLocksNode, values: [] };
|
|
971
|
+
const children = packageLocksNode.children ?? [];
|
|
972
|
+
for (const maybeFilename of children) {
|
|
973
|
+
const result = failUnlessNonBlankString(maybeFilename, packageJson.jsonFile);
|
|
974
|
+
if (!result.ok) {
|
|
975
|
+
placeholder.failures.push(result.error);
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
const filename = result.value;
|
|
979
|
+
if (filename.value !== pathlib.basename(filename.value)) {
|
|
980
|
+
placeholder.failures.push({
|
|
981
|
+
type: 'failure',
|
|
982
|
+
reason: 'invalid-config-syntax',
|
|
983
|
+
script: placeholder,
|
|
984
|
+
diagnostic: {
|
|
985
|
+
severity: 'error',
|
|
986
|
+
message: `A package lock must be a filename, not a path`,
|
|
987
|
+
location: {
|
|
988
|
+
file: packageJson.jsonFile,
|
|
989
|
+
range: { length: filename.length, offset: filename.offset },
|
|
990
|
+
},
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
packageLocks.values.push(filename.value);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (
|
|
1000
|
+
// There's no reason to check package locks when "files" is undefined,
|
|
1001
|
+
// because scripts will always run in that case anyway.
|
|
1002
|
+
files !== undefined &&
|
|
1003
|
+
// An explicitly empty "packageLocks" array disables package lock checking
|
|
1004
|
+
// entirely.
|
|
1005
|
+
packageLocks?.values.length !== 0) {
|
|
1006
|
+
const lockfileNames = packageLocks?.values ?? DEFAULT_LOCKFILES[this.#agent];
|
|
1007
|
+
// Generate "package-lock.json", "../package-lock.json",
|
|
1008
|
+
// "../../package-lock.json" etc. all the way up to the root of the
|
|
1009
|
+
// filesystem, because that's how Node package resolution works.
|
|
1010
|
+
const depth = placeholder.packageDir.split(pathlib.sep).length;
|
|
1011
|
+
const paths = [];
|
|
1012
|
+
for (let i = 0; i < depth; i++) {
|
|
1013
|
+
// Glob patterns are specified with forward-slash delimiters, even on
|
|
1014
|
+
// Windows.
|
|
1015
|
+
const prefix = Array(i + 1).join('../');
|
|
1016
|
+
for (const lockfileName of lockfileNames) {
|
|
1017
|
+
paths.push(prefix + lockfileName);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
// Only add the package locks that currently exist to the list of files
|
|
1021
|
+
// for this script. This way, in watch mode we won't create watchers for
|
|
1022
|
+
// all parent directories, just in case a package lock file is created at
|
|
1023
|
+
// some later time during watch, which is a rare and not especially
|
|
1024
|
+
// important event. Creating watchers for all parent directories is
|
|
1025
|
+
// potentially expensive, and on Windows will also result in occasional
|
|
1026
|
+
// errors.
|
|
1027
|
+
const existing = await Promise.all(paths.map(async (path) => {
|
|
1028
|
+
try {
|
|
1029
|
+
await fs.access(pathlib.join(placeholder.packageDir, path));
|
|
1030
|
+
return path;
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
return undefined;
|
|
1034
|
+
}
|
|
1035
|
+
}));
|
|
1036
|
+
for (const path of existing) {
|
|
1037
|
+
if (path !== undefined) {
|
|
1038
|
+
files.values.push(path);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
#processEnv(placeholder, packageJson, syntaxInfo, command) {
|
|
1044
|
+
if (syntaxInfo.wireitConfigNode === undefined) {
|
|
1045
|
+
return {};
|
|
1046
|
+
}
|
|
1047
|
+
const envNode = findNodeAtLocation(syntaxInfo.wireitConfigNode, ['env']);
|
|
1048
|
+
if (envNode === undefined) {
|
|
1049
|
+
return {};
|
|
1050
|
+
}
|
|
1051
|
+
if (command === undefined) {
|
|
1052
|
+
placeholder.failures.push({
|
|
1053
|
+
type: 'failure',
|
|
1054
|
+
reason: 'invalid-config-syntax',
|
|
1055
|
+
script: placeholder,
|
|
1056
|
+
diagnostic: {
|
|
1057
|
+
severity: 'error',
|
|
1058
|
+
message: 'Can\'t set "env" unless "command" is set',
|
|
1059
|
+
location: {
|
|
1060
|
+
file: packageJson.jsonFile,
|
|
1061
|
+
range: { length: envNode.length, offset: envNode.offset },
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
if (envNode.type !== 'object') {
|
|
1067
|
+
placeholder.failures.push({
|
|
1068
|
+
type: 'failure',
|
|
1069
|
+
reason: 'invalid-config-syntax',
|
|
1070
|
+
script: placeholder,
|
|
1071
|
+
diagnostic: {
|
|
1072
|
+
severity: 'error',
|
|
1073
|
+
message: 'Expected an object',
|
|
1074
|
+
location: {
|
|
1075
|
+
file: packageJson.jsonFile,
|
|
1076
|
+
range: { length: envNode.length, offset: envNode.offset },
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
if (envNode.children === undefined) {
|
|
1082
|
+
return {};
|
|
1083
|
+
}
|
|
1084
|
+
const entries = [];
|
|
1085
|
+
for (const propNode of envNode.children) {
|
|
1086
|
+
if (propNode.children === undefined || propNode.children.length !== 2) {
|
|
1087
|
+
throw new Error('Internal error: expected object JSON node children to be key/val pairs');
|
|
1088
|
+
}
|
|
1089
|
+
const keyValueResult = failUnlessKeyValue(propNode, propNode.children, packageJson.jsonFile);
|
|
1090
|
+
if (!keyValueResult.ok) {
|
|
1091
|
+
placeholder.failures.push(keyValueResult.error);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const [key, val] = keyValueResult.value;
|
|
1095
|
+
if (key.type !== 'string') {
|
|
1096
|
+
throw new Error('Internal error: expected object JSON node child key to be string');
|
|
1097
|
+
}
|
|
1098
|
+
const keyStr = key.value;
|
|
1099
|
+
if (val.type === 'string') {
|
|
1100
|
+
entries.push([keyStr, val.value]);
|
|
1101
|
+
}
|
|
1102
|
+
else if (val.type !== 'object') {
|
|
1103
|
+
placeholder.failures.push({
|
|
1104
|
+
type: 'failure',
|
|
1105
|
+
reason: 'invalid-config-syntax',
|
|
1106
|
+
script: placeholder,
|
|
1107
|
+
diagnostic: {
|
|
1108
|
+
severity: 'error',
|
|
1109
|
+
message: 'Expected a string or object',
|
|
1110
|
+
location: {
|
|
1111
|
+
file: packageJson.jsonFile,
|
|
1112
|
+
range: { length: val.length, offset: val.offset },
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
});
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
const externalNode = findNodeAtLocation(val, ['external']);
|
|
1120
|
+
if (externalNode?.value !== true) {
|
|
1121
|
+
placeholder.failures.push({
|
|
1122
|
+
type: 'failure',
|
|
1123
|
+
reason: 'invalid-config-syntax',
|
|
1124
|
+
script: placeholder,
|
|
1125
|
+
diagnostic: {
|
|
1126
|
+
severity: 'error',
|
|
1127
|
+
message: 'Expected "external" to be true',
|
|
1128
|
+
location: {
|
|
1129
|
+
file: packageJson.jsonFile,
|
|
1130
|
+
range: {
|
|
1131
|
+
length: (externalNode ?? val).length,
|
|
1132
|
+
offset: (externalNode ?? val).offset,
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
});
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
const defaultNode = findNodeAtLocation(val, ['default']);
|
|
1140
|
+
if (defaultNode && defaultNode.type !== 'string') {
|
|
1141
|
+
placeholder.failures.push({
|
|
1142
|
+
type: 'failure',
|
|
1143
|
+
reason: 'invalid-config-syntax',
|
|
1144
|
+
script: placeholder,
|
|
1145
|
+
diagnostic: {
|
|
1146
|
+
severity: 'error',
|
|
1147
|
+
message: 'Expected "default" to be a string',
|
|
1148
|
+
location: {
|
|
1149
|
+
file: packageJson.jsonFile,
|
|
1150
|
+
range: {
|
|
1151
|
+
length: (defaultNode ?? val).length,
|
|
1152
|
+
offset: (defaultNode ?? val).offset,
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
});
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
const envValue = process.env[keyStr];
|
|
1160
|
+
if (envValue !== undefined) {
|
|
1161
|
+
entries.push([keyStr, envValue]);
|
|
1162
|
+
}
|
|
1163
|
+
else if (defaultNode) {
|
|
1164
|
+
entries.push([keyStr, defaultNode.value]);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// Sort for better fingerprint match rate.
|
|
1169
|
+
entries.sort();
|
|
1170
|
+
return Object.fromEntries(entries);
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* This is where we check for cycles in dependencies, but it's also the
|
|
1174
|
+
* place where we transform LocallyValidScriptConfigs to ScriptConfigs.
|
|
1175
|
+
*/
|
|
1176
|
+
#checkForCyclesAndSortDependencies(config, trail, isPersistent) {
|
|
1177
|
+
if (config.state === 'valid') {
|
|
1178
|
+
// Already validated.
|
|
1179
|
+
return { ok: true, value: config };
|
|
1180
|
+
}
|
|
1181
|
+
else if (config.state === 'invalid') {
|
|
1182
|
+
return { ok: false, error: config };
|
|
1183
|
+
}
|
|
1184
|
+
let dependencyStillUnvalidated = undefined;
|
|
1185
|
+
const trailKey = scriptReferenceToString(config);
|
|
1186
|
+
const supplementalLocations = [];
|
|
1187
|
+
if (trail.has(trailKey)) {
|
|
1188
|
+
// Found a cycle.
|
|
1189
|
+
let cycleStart = 0;
|
|
1190
|
+
// Trail is in graph traversal order because JavaScript Map iteration
|
|
1191
|
+
// order matches insertion order.
|
|
1192
|
+
let i = 0;
|
|
1193
|
+
for (const visitedKey of trail) {
|
|
1194
|
+
if (visitedKey === trailKey) {
|
|
1195
|
+
cycleStart = i;
|
|
1196
|
+
}
|
|
1197
|
+
i++;
|
|
1198
|
+
}
|
|
1199
|
+
const trailArray = [...trail].map((key) => {
|
|
1200
|
+
const placeholderInfo = this.#placeholders.get(key);
|
|
1201
|
+
if (placeholderInfo === undefined) {
|
|
1202
|
+
throw new Error(`Internal error: placeholder not found for ${key} during cycle detection`);
|
|
1203
|
+
}
|
|
1204
|
+
return placeholderInfo.placeholder;
|
|
1205
|
+
});
|
|
1206
|
+
trailArray.push(config);
|
|
1207
|
+
const cycleEnd = trailArray.length - 1;
|
|
1208
|
+
for (let i = cycleStart; i < cycleEnd; i++) {
|
|
1209
|
+
const current = trailArray[i];
|
|
1210
|
+
const next = trailArray[i + 1];
|
|
1211
|
+
if (current.state === 'unvalidated') {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
const nextNode = current.dependencies.find((dep) => dep.config === next);
|
|
1215
|
+
// Use the actual value in the array, because this could refer to
|
|
1216
|
+
// a script in another package.
|
|
1217
|
+
const nextName = nextNode?.specifier?.value ??
|
|
1218
|
+
next?.name ??
|
|
1219
|
+
trailArray[cycleStart]?.name;
|
|
1220
|
+
const message = next === trailArray[cycleStart]
|
|
1221
|
+
? `${JSON.stringify(current.name)} points back to ${JSON.stringify(nextName)}`
|
|
1222
|
+
: `${JSON.stringify(current.name)} points to ${JSON.stringify(nextName)}`;
|
|
1223
|
+
const culpritNode =
|
|
1224
|
+
// This should always be present
|
|
1225
|
+
nextNode?.specifier ??
|
|
1226
|
+
// But failing that, fall back to the best node we have.
|
|
1227
|
+
current.configAstNode?.name ??
|
|
1228
|
+
current.scriptAstNode.name;
|
|
1229
|
+
supplementalLocations.push({
|
|
1230
|
+
message,
|
|
1231
|
+
location: {
|
|
1232
|
+
file: current.declaringFile,
|
|
1233
|
+
range: {
|
|
1234
|
+
offset: culpritNode.offset,
|
|
1235
|
+
length: culpritNode.length,
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
const diagnostic = {
|
|
1241
|
+
severity: 'error',
|
|
1242
|
+
message: `Cycle detected in dependencies of ${JSON.stringify(config.name)}.`,
|
|
1243
|
+
location: {
|
|
1244
|
+
file: config.declaringFile,
|
|
1245
|
+
range: {
|
|
1246
|
+
length: config.configAstNode?.name.length ??
|
|
1247
|
+
config.scriptAstNode.name.length,
|
|
1248
|
+
offset: config.configAstNode?.name.offset ??
|
|
1249
|
+
config.scriptAstNode.name.length,
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
supplementalLocations,
|
|
1253
|
+
};
|
|
1254
|
+
const failure = {
|
|
1255
|
+
type: 'failure',
|
|
1256
|
+
reason: 'cycle',
|
|
1257
|
+
script: config,
|
|
1258
|
+
diagnostic,
|
|
1259
|
+
};
|
|
1260
|
+
return { ok: false, error: this.#markAsInvalid(config, failure) };
|
|
1261
|
+
}
|
|
1262
|
+
if (config.dependencies.length > 0) {
|
|
1263
|
+
// Sorting means that if the user re-orders the same set of dependencies,
|
|
1264
|
+
// the trail we take in this walk remains the same, so any cycle error
|
|
1265
|
+
// message we might throw will have the same trail, too. This also helps
|
|
1266
|
+
// make the caching keys that we'll be generating in the later execution
|
|
1267
|
+
// step insensitive to dependency order as well.
|
|
1268
|
+
config.dependencies.sort((a, b) => {
|
|
1269
|
+
if (a.config.packageDir !== b.config.packageDir) {
|
|
1270
|
+
return a.config.packageDir.localeCompare(b.config.packageDir);
|
|
1271
|
+
}
|
|
1272
|
+
return a.config.name.localeCompare(b.config.name);
|
|
1273
|
+
});
|
|
1274
|
+
trail.add(trailKey);
|
|
1275
|
+
for (const dependency of config.dependencies) {
|
|
1276
|
+
if (dependency.config.state === 'unvalidated') {
|
|
1277
|
+
dependencyStillUnvalidated = dependency.config;
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
const validDependencyConfigResult = this.#checkForCyclesAndSortDependencies(dependency.config, trail,
|
|
1281
|
+
// Walk through no-command scripts and services when determining if
|
|
1282
|
+
// something is persistent.
|
|
1283
|
+
isPersistent &&
|
|
1284
|
+
(config.command === undefined || config.service !== undefined));
|
|
1285
|
+
if (!validDependencyConfigResult.ok) {
|
|
1286
|
+
return {
|
|
1287
|
+
ok: false,
|
|
1288
|
+
error: this.#markAsInvalid(config, validDependencyConfigResult.error.dependencyFailure),
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const validDependencyConfig = validDependencyConfigResult.value;
|
|
1292
|
+
if (validDependencyConfig.service !== undefined) {
|
|
1293
|
+
// We directly depend on a service.
|
|
1294
|
+
config.services.push(validDependencyConfig);
|
|
1295
|
+
}
|
|
1296
|
+
else if (validDependencyConfig.command === undefined) {
|
|
1297
|
+
// We depend on a no-command script, so in effect we depend on all of
|
|
1298
|
+
// the services it depends on.
|
|
1299
|
+
for (const service of validDependencyConfig.services) {
|
|
1300
|
+
config.services.push(service);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
trail.delete(trailKey);
|
|
1305
|
+
}
|
|
1306
|
+
if (dependencyStillUnvalidated !== undefined) {
|
|
1307
|
+
// At least one of our dependencies was unvalidated, likely because it
|
|
1308
|
+
// had a syntax error or was missing necessary information. Therefore
|
|
1309
|
+
// we can't transition to valid either.
|
|
1310
|
+
const failure = {
|
|
1311
|
+
type: 'failure',
|
|
1312
|
+
reason: 'dependency-invalid',
|
|
1313
|
+
script: config,
|
|
1314
|
+
dependency: dependencyStillUnvalidated,
|
|
1315
|
+
};
|
|
1316
|
+
return { ok: false, error: this.#markAsInvalid(config, failure) };
|
|
1317
|
+
}
|
|
1318
|
+
let validConfig;
|
|
1319
|
+
if (config.service !== undefined) {
|
|
1320
|
+
// We should already have created an invalid script at this point, so we
|
|
1321
|
+
// should never get here. We throw here to convince TypeScript that this
|
|
1322
|
+
// is guaranteed.
|
|
1323
|
+
if (config.command === undefined) {
|
|
1324
|
+
throw new Error('Internal error: Supposedly valid service did not have command');
|
|
1325
|
+
}
|
|
1326
|
+
validConfig = {
|
|
1327
|
+
...config,
|
|
1328
|
+
state: 'valid',
|
|
1329
|
+
extraArgs: undefined,
|
|
1330
|
+
dependencies: config.dependencies,
|
|
1331
|
+
// Unfortunately TypeScript doesn't narrow the ...config spread, so we
|
|
1332
|
+
// have to assign explicitly.
|
|
1333
|
+
command: config.command,
|
|
1334
|
+
isPersistent,
|
|
1335
|
+
serviceConsumers: [],
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
validConfig = {
|
|
1340
|
+
...config,
|
|
1341
|
+
state: 'valid',
|
|
1342
|
+
extraArgs: undefined,
|
|
1343
|
+
dependencies: config.dependencies,
|
|
1344
|
+
// Unfortunately TypeScript doesn't narrow the ...config spread, so we
|
|
1345
|
+
// have to assign explicitly.
|
|
1346
|
+
service: config.service,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
// Propagate reverse service dependencies.
|
|
1350
|
+
if (validConfig.command) {
|
|
1351
|
+
for (const dependency of validConfig.dependencies) {
|
|
1352
|
+
if (dependency.config.service !== undefined) {
|
|
1353
|
+
dependency.config.serviceConsumers.push(validConfig);
|
|
1354
|
+
}
|
|
1355
|
+
else if (dependency.config.command === undefined) {
|
|
1356
|
+
for (const service of dependency.config.services) {
|
|
1357
|
+
service.serviceConsumers.push(validConfig);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
// We want to keep the original reference, but get type checking that
|
|
1363
|
+
// the only difference between a ScriptConfig and a
|
|
1364
|
+
// LocallyValidScriptConfig is that the state is 'valid' and the
|
|
1365
|
+
// dependencies are also valid, which we confirmed above.
|
|
1366
|
+
Object.assign(config, validConfig);
|
|
1367
|
+
return { ok: true, value: config };
|
|
1368
|
+
}
|
|
1369
|
+
#markAsInvalid(config, failure) {
|
|
1370
|
+
const invalidConfig = {
|
|
1371
|
+
...config,
|
|
1372
|
+
state: 'invalid',
|
|
1373
|
+
dependencyFailure: failure,
|
|
1374
|
+
};
|
|
1375
|
+
Object.assign(config, invalidConfig);
|
|
1376
|
+
config.failures.push(failure);
|
|
1377
|
+
return config;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Resolve a dependency string specified in a "wireit.<script>.dependencies"
|
|
1381
|
+
* array, which may contain special syntax like relative paths or
|
|
1382
|
+
* "$WORKSPACES", into concrete packages and script names.
|
|
1383
|
+
*
|
|
1384
|
+
* Note this can return 0, 1, or >1 script references.
|
|
1385
|
+
*/
|
|
1386
|
+
#resolveDependency(dependency, context, referencingFile) {
|
|
1387
|
+
// TODO(aomarks) Implement $WORKSPACES syntax.
|
|
1388
|
+
if (dependency.value.startsWith('.')) {
|
|
1389
|
+
// TODO(aomarks) It is technically valid for an npm script to start with a
|
|
1390
|
+
// ".". We should support that edge case with backslash escaping.
|
|
1391
|
+
const result = this.#resolveCrossPackageDependency(dependency, context, referencingFile);
|
|
1392
|
+
if (!result.ok) {
|
|
1393
|
+
return result;
|
|
1394
|
+
}
|
|
1395
|
+
return { ok: true, value: [result.value] };
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
ok: true,
|
|
1399
|
+
value: [{ packageDir: context.packageDir, name: dependency.value }],
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Resolve a cross-package dependency (e.g. "../other-package:build").
|
|
1404
|
+
* Cross-package dependencies always start with a ".".
|
|
1405
|
+
*/
|
|
1406
|
+
#resolveCrossPackageDependency(dependency, context, referencingFile) {
|
|
1407
|
+
// TODO(aomarks) On some file systems, it is valid to have a ":" in a file
|
|
1408
|
+
// path. We should support that edge case with backslash escaping.
|
|
1409
|
+
const firstColonIdx = dependency.value.indexOf(':');
|
|
1410
|
+
if (firstColonIdx === -1) {
|
|
1411
|
+
return {
|
|
1412
|
+
ok: false,
|
|
1413
|
+
error: {
|
|
1414
|
+
type: 'failure',
|
|
1415
|
+
reason: 'invalid-config-syntax',
|
|
1416
|
+
script: context,
|
|
1417
|
+
diagnostic: {
|
|
1418
|
+
severity: 'error',
|
|
1419
|
+
message: `Cross-package dependency must use syntax ` +
|
|
1420
|
+
`"<relative-path>:<script-name>", ` +
|
|
1421
|
+
`but there's no ":" character in "${dependency.value}".`,
|
|
1422
|
+
location: {
|
|
1423
|
+
file: referencingFile,
|
|
1424
|
+
range: { offset: dependency.offset, length: dependency.length },
|
|
1425
|
+
},
|
|
1426
|
+
},
|
|
1427
|
+
},
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
const scriptName = dependency.value.slice(firstColonIdx + 1);
|
|
1431
|
+
if (!scriptName) {
|
|
1432
|
+
return {
|
|
1433
|
+
ok: false,
|
|
1434
|
+
error: {
|
|
1435
|
+
type: 'failure',
|
|
1436
|
+
reason: 'invalid-config-syntax',
|
|
1437
|
+
script: context,
|
|
1438
|
+
diagnostic: {
|
|
1439
|
+
severity: 'error',
|
|
1440
|
+
message: `Cross-package dependency must use syntax ` +
|
|
1441
|
+
`"<relative-path>:<script-name>", ` +
|
|
1442
|
+
`but there's no script name in "${dependency.value}".`,
|
|
1443
|
+
location: {
|
|
1444
|
+
file: referencingFile,
|
|
1445
|
+
range: { offset: dependency.offset, length: dependency.length },
|
|
1446
|
+
},
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const relativePackageDir = dependency.value.slice(0, firstColonIdx);
|
|
1452
|
+
const absolutePackageDir = pathlib.resolve(context.packageDir, relativePackageDir);
|
|
1453
|
+
if (absolutePackageDir === context.packageDir) {
|
|
1454
|
+
return {
|
|
1455
|
+
ok: false,
|
|
1456
|
+
error: {
|
|
1457
|
+
type: 'failure',
|
|
1458
|
+
reason: 'invalid-config-syntax',
|
|
1459
|
+
script: context,
|
|
1460
|
+
diagnostic: {
|
|
1461
|
+
severity: 'error',
|
|
1462
|
+
message: `Cross-package dependency "${dependency.value}" ` +
|
|
1463
|
+
`resolved to the same package.`,
|
|
1464
|
+
location: {
|
|
1465
|
+
file: referencingFile,
|
|
1466
|
+
range: { offset: dependency.offset, length: dependency.length },
|
|
1467
|
+
},
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
ok: true,
|
|
1474
|
+
value: { packageDir: absolutePackageDir, name: scriptName },
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
export function failUnlessNonBlankString(astNode, file) {
|
|
1479
|
+
if (astNode.type !== 'string') {
|
|
1480
|
+
return {
|
|
1481
|
+
ok: false,
|
|
1482
|
+
error: {
|
|
1483
|
+
type: 'failure',
|
|
1484
|
+
reason: 'invalid-config-syntax',
|
|
1485
|
+
script: { packageDir: pathlib.dirname(file.path) },
|
|
1486
|
+
diagnostic: {
|
|
1487
|
+
severity: 'error',
|
|
1488
|
+
message: `Expected a string, but was ${astNode.type}.`,
|
|
1489
|
+
location: {
|
|
1490
|
+
file,
|
|
1491
|
+
range: {
|
|
1492
|
+
offset: astNode.offset,
|
|
1493
|
+
length: astNode.length,
|
|
1494
|
+
},
|
|
1495
|
+
},
|
|
1496
|
+
},
|
|
1497
|
+
},
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
if (astNode.value.match(/^\s*$/)) {
|
|
1501
|
+
return {
|
|
1502
|
+
ok: false,
|
|
1503
|
+
error: {
|
|
1504
|
+
type: 'failure',
|
|
1505
|
+
reason: 'invalid-config-syntax',
|
|
1506
|
+
script: { packageDir: pathlib.dirname(file.path) },
|
|
1507
|
+
diagnostic: {
|
|
1508
|
+
severity: 'error',
|
|
1509
|
+
message: `Expected this field to be nonempty`,
|
|
1510
|
+
location: {
|
|
1511
|
+
file,
|
|
1512
|
+
range: {
|
|
1513
|
+
offset: astNode.offset,
|
|
1514
|
+
length: astNode.length,
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
},
|
|
1518
|
+
},
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
return { ok: true, value: astNode };
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Return a failing result if the given value is not an Array.
|
|
1525
|
+
*/
|
|
1526
|
+
const failUnlessArray = (astNode, file) => {
|
|
1527
|
+
if (astNode.type !== 'array') {
|
|
1528
|
+
return {
|
|
1529
|
+
ok: false,
|
|
1530
|
+
error: {
|
|
1531
|
+
type: 'failure',
|
|
1532
|
+
reason: 'invalid-config-syntax',
|
|
1533
|
+
script: { packageDir: pathlib.dirname(file.path) },
|
|
1534
|
+
diagnostic: {
|
|
1535
|
+
severity: 'error',
|
|
1536
|
+
message: `Expected an array, but was ${astNode.type}.`,
|
|
1537
|
+
location: {
|
|
1538
|
+
file: file,
|
|
1539
|
+
range: {
|
|
1540
|
+
offset: astNode.offset,
|
|
1541
|
+
length: astNode.length,
|
|
1542
|
+
},
|
|
1543
|
+
},
|
|
1544
|
+
},
|
|
1545
|
+
},
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
return { ok: true, value: undefined };
|
|
1549
|
+
};
|
|
1550
|
+
/**
|
|
1551
|
+
* Return a failed result if the given value is not an object literal ({...}).
|
|
1552
|
+
*/
|
|
1553
|
+
export const failUnlessJsonObject = (astNode, file) => {
|
|
1554
|
+
if (astNode.type !== 'object') {
|
|
1555
|
+
return {
|
|
1556
|
+
type: 'failure',
|
|
1557
|
+
reason: 'invalid-config-syntax',
|
|
1558
|
+
script: { packageDir: pathlib.dirname(file.path) },
|
|
1559
|
+
diagnostic: {
|
|
1560
|
+
severity: 'error',
|
|
1561
|
+
message: `Expected an object, but was ${astNode.type}.`,
|
|
1562
|
+
location: {
|
|
1563
|
+
file: file,
|
|
1564
|
+
range: {
|
|
1565
|
+
offset: astNode.offset,
|
|
1566
|
+
length: astNode.length,
|
|
1567
|
+
},
|
|
1568
|
+
},
|
|
1569
|
+
},
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
export const failUnlessKeyValue = (node, children, file) => {
|
|
1574
|
+
const [rawName, rawValue] = children;
|
|
1575
|
+
if (children.length !== 2 ||
|
|
1576
|
+
rawName === undefined ||
|
|
1577
|
+
rawValue === undefined) {
|
|
1578
|
+
return {
|
|
1579
|
+
ok: false,
|
|
1580
|
+
error: {
|
|
1581
|
+
type: 'failure',
|
|
1582
|
+
reason: 'invalid-config-syntax',
|
|
1583
|
+
script: { packageDir: pathlib.dirname(file.path) },
|
|
1584
|
+
diagnostic: {
|
|
1585
|
+
severity: 'error',
|
|
1586
|
+
message: `Expected "key": "value"`,
|
|
1587
|
+
location: {
|
|
1588
|
+
file,
|
|
1589
|
+
range: {
|
|
1590
|
+
offset: node.offset,
|
|
1591
|
+
length: node.length,
|
|
1592
|
+
},
|
|
1593
|
+
},
|
|
1594
|
+
},
|
|
1595
|
+
},
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
return { ok: true, value: [rawName, rawValue] };
|
|
1599
|
+
};
|
|
1600
|
+
//# sourceMappingURL=analyzer.js.map
|