@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.
Files changed (54) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +1062 -0
  3. package/bin/wireit.js +9 -0
  4. package/lib/analyzer.js +1600 -0
  5. package/lib/caching/cache.js +7 -0
  6. package/lib/caching/github-actions-cache.js +832 -0
  7. package/lib/caching/local-cache.js +78 -0
  8. package/lib/caching/shared-cache.js +256 -0
  9. package/lib/cli-options.js +495 -0
  10. package/lib/cli.js +177 -0
  11. package/lib/config.js +18 -0
  12. package/lib/error.js +160 -0
  13. package/lib/event.js +7 -0
  14. package/lib/execution/base.js +108 -0
  15. package/lib/execution/no-command.js +32 -0
  16. package/lib/execution/service.js +1017 -0
  17. package/lib/execution/standard.js +683 -0
  18. package/lib/executor.js +249 -0
  19. package/lib/fingerprint.js +164 -0
  20. package/lib/ide.js +583 -0
  21. package/lib/language-server.js +135 -0
  22. package/lib/logging/combination-logger.js +41 -0
  23. package/lib/logging/debug-logger.js +43 -0
  24. package/lib/logging/logger.js +38 -0
  25. package/lib/logging/metrics-logger.js +108 -0
  26. package/lib/logging/quiet/run-tracker.js +597 -0
  27. package/lib/logging/quiet/stack-map.js +41 -0
  28. package/lib/logging/quiet/writeover-line.js +197 -0
  29. package/lib/logging/quiet-logger.js +78 -0
  30. package/lib/logging/simple-logger.js +296 -0
  31. package/lib/logging/watch-logger.js +81 -0
  32. package/lib/script-child-process.js +270 -0
  33. package/lib/util/ast.js +71 -0
  34. package/lib/util/async-cache.js +24 -0
  35. package/lib/util/copy.js +120 -0
  36. package/lib/util/deferred.js +35 -0
  37. package/lib/util/delete.js +120 -0
  38. package/lib/util/dispose.js +16 -0
  39. package/lib/util/fs.js +258 -0
  40. package/lib/util/glob.js +255 -0
  41. package/lib/util/line-monitor.js +69 -0
  42. package/lib/util/manifest.js +31 -0
  43. package/lib/util/optimize-mkdirs.js +55 -0
  44. package/lib/util/package-json-reader.js +61 -0
  45. package/lib/util/package-json.js +179 -0
  46. package/lib/util/script-data-dir.js +19 -0
  47. package/lib/util/shuffle.js +16 -0
  48. package/lib/util/unreachable.js +12 -0
  49. package/lib/util/windows.js +87 -0
  50. package/lib/util/worker-pool.js +61 -0
  51. package/lib/watcher.js +396 -0
  52. package/package.json +470 -0
  53. package/schema.json +132 -0
  54. package/wireit.svg +1 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { AsyncCache } from './async-cache.js';
7
+ import { PackageJson } from './package-json.js';
8
+ import * as pathlib from 'path';
9
+ import * as fs from './fs.js';
10
+ import { parseTree } from './ast.js';
11
+ export const astKey = Symbol('ast');
12
+ /**
13
+ * Reads package.json files and caches them.
14
+ */
15
+ export class CachingPackageJsonReader {
16
+ #cache = new AsyncCache();
17
+ #fs;
18
+ constructor(filesystem = fs) {
19
+ this.#fs = filesystem;
20
+ }
21
+ async read(packageDir) {
22
+ return this.#cache.getOrCompute(packageDir, async () => {
23
+ const path = pathlib.resolve(packageDir, 'package.json');
24
+ let contents;
25
+ try {
26
+ contents = await this.#fs.readFile(path, 'utf8');
27
+ }
28
+ catch (error) {
29
+ if (error.code === 'ENOENT') {
30
+ return {
31
+ ok: false,
32
+ error: {
33
+ type: 'failure',
34
+ reason: 'missing-package-json',
35
+ script: { packageDir },
36
+ },
37
+ };
38
+ }
39
+ throw error;
40
+ }
41
+ const astResult = parseTree(path, contents);
42
+ if (!astResult.ok) {
43
+ return astResult;
44
+ }
45
+ const packageJsonFile = new PackageJson({ contents, path }, astResult.value);
46
+ return { ok: true, value: packageJsonFile };
47
+ });
48
+ }
49
+ async *getFailures() {
50
+ const values = await Promise.all([...this.#cache.values]);
51
+ for (const result of values) {
52
+ if (!result.ok) {
53
+ yield result.error;
54
+ continue;
55
+ }
56
+ const packageJson = result.value;
57
+ yield* packageJson.failures;
58
+ }
59
+ }
60
+ }
61
+ //# sourceMappingURL=package-json-reader.js.map
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { findNamedNodeAtLocation } from './ast.js';
7
+ import { failUnlessJsonObject, failUnlessKeyValue, failUnlessNonBlankString, } from '../analyzer.js';
8
+ import { offsetInsideNamedNode, offsetInsideRange } from '../error.js';
9
+ /**
10
+ * A parsed and minimally analyzed package.json file.
11
+ *
12
+ * This does some very basic syntactic analysis of the package.json file,
13
+ * finding issues like "the scripts section isn't an object mapping strings to
14
+ * strings" and "the wireit section isn't an object mapping strings to objects".
15
+ *
16
+ * Makes it easy to find the syntax nodes for a script.
17
+ *
18
+ * Does not do any validation or analysis of the wireit script configs.
19
+ *
20
+ * This class exists in part so that we walk the package.json file only once,
21
+ * and in part so that we generate file-level syntactic diagnostics only once,
22
+ * so that we can do better deduplication of errors.
23
+ */
24
+ export class PackageJson {
25
+ // We keep the file level AST node private to represent the invariant that
26
+ // we only walk the file once, in this class, and nowhere else.
27
+ #fileAstNode;
28
+ #scripts;
29
+ constructor(jsonFile, fileAstNode) {
30
+ this.#scripts = new Map();
31
+ this.scriptsSection = undefined;
32
+ this.wireitSection = undefined;
33
+ this.jsonFile = jsonFile;
34
+ this.#fileAstNode = fileAstNode;
35
+ const failures = [];
36
+ this.scriptsSection = this.#analyzeScriptsSection(failures);
37
+ this.wireitSection = this.#analyzeWireitSection(failures);
38
+ this.failures = failures;
39
+ }
40
+ getScriptInfo(name) {
41
+ return this.#scripts.get(name);
42
+ }
43
+ get scripts() {
44
+ return this.#scripts.values();
45
+ }
46
+ getInfoAboutLocation(offset) {
47
+ if (this.scriptsSection && offsetInsideRange(offset, this.scriptsSection)) {
48
+ for (const scriptSyntaxInfo of this.scripts) {
49
+ if (scriptSyntaxInfo.scriptNode &&
50
+ offsetInsideNamedNode(offset, scriptSyntaxInfo.scriptNode)) {
51
+ return { kind: 'scripts-section-script', scriptSyntaxInfo };
52
+ }
53
+ }
54
+ }
55
+ else if (this.wireitSection &&
56
+ offsetInsideRange(offset, this.wireitSection)) {
57
+ for (const scriptSyntaxInfo of this.scripts) {
58
+ if (scriptSyntaxInfo.wireitConfigNode &&
59
+ offsetInsideNamedNode(offset, scriptSyntaxInfo.wireitConfigNode)) {
60
+ return { kind: 'wireit-section-script', scriptSyntaxInfo };
61
+ }
62
+ }
63
+ }
64
+ }
65
+ #getOrMakeScriptInfo(name) {
66
+ let info = this.#scripts.get(name);
67
+ if (info === undefined) {
68
+ info = { name };
69
+ this.#scripts.set(name, info);
70
+ }
71
+ return info;
72
+ }
73
+ /**
74
+ * Do some basic structural validation of the "scripts" section of this
75
+ * package.json file. Create placeholders for each of the declared scripts and
76
+ * add them to this._scripts.
77
+ */
78
+ #analyzeScriptsSection(failures) {
79
+ const scriptsSectionResult = findNamedNodeAtLocation(this.#fileAstNode, ['scripts'], this.jsonFile);
80
+ if (!scriptsSectionResult.ok) {
81
+ failures.push(scriptsSectionResult.error);
82
+ return;
83
+ }
84
+ const scriptsSection = scriptsSectionResult.value;
85
+ if (scriptsSection === undefined) {
86
+ return;
87
+ }
88
+ const fail = failUnlessJsonObject(scriptsSection, this.jsonFile);
89
+ if (fail !== undefined) {
90
+ failures.push(fail);
91
+ return;
92
+ }
93
+ for (const child of scriptsSection.children ?? []) {
94
+ if (child.type !== 'property') {
95
+ continue;
96
+ }
97
+ if (child.children === undefined) {
98
+ continue;
99
+ }
100
+ const nameAndValueResult = failUnlessKeyValue(child, child.children, this.jsonFile);
101
+ if (!nameAndValueResult.ok) {
102
+ failures.push(nameAndValueResult.error);
103
+ continue;
104
+ }
105
+ const [rawName, rawValue] = nameAndValueResult.value;
106
+ const nameResult = failUnlessNonBlankString(rawName, this.jsonFile);
107
+ if (!nameResult.ok) {
108
+ failures.push(nameResult.error);
109
+ continue;
110
+ }
111
+ const valueResult = failUnlessNonBlankString(rawValue, this.jsonFile);
112
+ if (!valueResult.ok) {
113
+ failures.push(valueResult.error);
114
+ continue;
115
+ }
116
+ const scriptAstNode = valueResult.value;
117
+ scriptAstNode.name = nameResult.value;
118
+ this.#getOrMakeScriptInfo(nameResult.value.value).scriptNode =
119
+ scriptAstNode;
120
+ }
121
+ return scriptsSectionResult.value;
122
+ }
123
+ /**
124
+ * Do some basic structural validation of the "wireit" section of this
125
+ * package.json file.
126
+ *
127
+ * Create placeholders for each of the declared scripts and
128
+ * add them to this._scripts.
129
+ *
130
+ * Does not do any validation of any wireit configs themselves, that's done
131
+ * on demand when executing, or all at once when finding all diagnostics.
132
+ */
133
+ #analyzeWireitSection(failures) {
134
+ const wireitSectionResult = findNamedNodeAtLocation(this.#fileAstNode, ['wireit'], this.jsonFile);
135
+ if (!wireitSectionResult.ok) {
136
+ failures.push(wireitSectionResult.error);
137
+ return;
138
+ }
139
+ const wireitSection = wireitSectionResult.value;
140
+ if (wireitSection === undefined) {
141
+ return;
142
+ }
143
+ const fail = failUnlessJsonObject(wireitSection, this.jsonFile);
144
+ if (fail !== undefined) {
145
+ failures.push(fail);
146
+ return;
147
+ }
148
+ for (const child of wireitSection.children ?? []) {
149
+ if (child.type !== 'property') {
150
+ continue;
151
+ }
152
+ if (child.children === undefined) {
153
+ continue;
154
+ }
155
+ const nameAndValueResult = failUnlessKeyValue(child, child.children, this.jsonFile);
156
+ if (!nameAndValueResult.ok) {
157
+ failures.push(nameAndValueResult.error);
158
+ continue;
159
+ }
160
+ const [rawName, rawValue] = nameAndValueResult.value;
161
+ const nameResult = failUnlessNonBlankString(rawName, this.jsonFile);
162
+ if (!nameResult.ok) {
163
+ failures.push(nameResult.error);
164
+ continue;
165
+ }
166
+ const fail = failUnlessJsonObject(rawValue, this.jsonFile);
167
+ if (fail !== undefined) {
168
+ failures.push(fail);
169
+ continue;
170
+ }
171
+ const wireitConfigNode = rawValue;
172
+ wireitConfigNode.name = nameResult.value;
173
+ this.#getOrMakeScriptInfo(nameResult.value.value).wireitConfigNode =
174
+ wireitConfigNode;
175
+ }
176
+ return wireitSectionResult.value;
177
+ }
178
+ }
179
+ //# sourceMappingURL=package-json.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as pathlib from 'path';
7
+ /**
8
+ * Get the directory name where Wireit data can be saved for a script.
9
+ */
10
+ export const getScriptDataDir = (script) => pathlib.join(script.packageDir, '.wireit',
11
+ // Script names can contain any character, so they aren't safe to use
12
+ // directly in a filepath, because certain characters aren't allowed on
13
+ // certain filesystems (e.g. ":" is forbidden on Windows). Hex-encode
14
+ // instead so that we only get safe ASCII characters.
15
+ //
16
+ // Reference:
17
+ // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
18
+ Buffer.from(script.name).toString('hex'));
19
+ //# sourceMappingURL=script-data-dir.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Randomize the order of an array in-place.
8
+ */
9
+ export const shuffle = (array) => {
10
+ // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
11
+ for (let i = array.length - 1; i > 0; i--) {
12
+ const j = Math.floor(Math.random() * (i + 1));
13
+ [array[i], array[j]] = [array[j], array[i]];
14
+ }
15
+ };
16
+ //# sourceMappingURL=shuffle.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * TypeScript will error if it believes this function could be invoked. Useful
8
+ * to check for branches that should never be reached (e.g. that all possible
9
+ * cases in a switch are handled).
10
+ */
11
+ export const unreachable = (value) => value;
12
+ //# sourceMappingURL=unreachable.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Whether we're running on Windows.
8
+ */
9
+ export const IS_WINDOWS = process.platform === 'win32';
10
+ /**
11
+ * If we're on Windows, convert all back-slashes to forward-slashes (e.g.
12
+ * "foo\bar" -> "foo/bar").
13
+ */
14
+ export const posixifyPathIfOnWindows = (path) => IS_WINDOWS ? path.replace(/\\/g, '/') : path;
15
+ /**
16
+ * Overlay the given environment variables on top of the current process's
17
+ * environment variables in a way that is reliable on Windows.
18
+ *
19
+ * Windows environment variable names are **sort of** case-insensitive. When you
20
+ * `spawn` a process and pass 2 environment variables that differ only in case,
21
+ * then the value that actually gets set is ambiguous, and depends on the Node
22
+ * version. In Node 14 it seems to be the last one in iteration order, in Node
23
+ * 16 it seems to be the first one after sorting.
24
+ *
25
+ * For example, if you run:
26
+ *
27
+ * ```ts
28
+ * spawn('foo', {
29
+ * env: {
30
+ * PATH: 'C:\\extra;C:\\before',
31
+ * Path: 'C:\\before'
32
+ * }
33
+ * });
34
+ * ```
35
+ *
36
+ * Then sometimes the value that the spawned process receives could be
37
+ * `C:\before`, and other times it could be `C:\extra;C:\before`.
38
+ *
39
+ * This function ensures that the values given in `augmentations` will always
40
+ * win, by normalizing casing to match the casing that was already set in
41
+ * `process.env`.
42
+ */
43
+ export const augmentProcessEnvSafelyIfOnWindows = (augmentations) => {
44
+ if (ENVIRONMENT_VARIABLE_CASINGS_IF_WINDOWS === undefined) {
45
+ // On Linux and macOS, environment variables are case-sensitive, so there's
46
+ // nothing special to do here.
47
+ return { ...process.env, ...augmentations };
48
+ }
49
+ const augmented = { ...process.env };
50
+ for (const [name, value] of Object.entries(augmentations)) {
51
+ const existingNames = ENVIRONMENT_VARIABLE_CASINGS_IF_WINDOWS.get(name.toLowerCase());
52
+ if (existingNames === undefined) {
53
+ augmented[name] = value;
54
+ }
55
+ else {
56
+ for (const existingName of existingNames) {
57
+ augmented[existingName] = value;
58
+ }
59
+ }
60
+ }
61
+ return augmented;
62
+ };
63
+ /**
64
+ * A map from lowercase environment variable name to the specific name casing(s)
65
+ * that were found in this process's environment variables.
66
+ *
67
+ * This is an array because in Node 14 the `process.env` object can actually
68
+ * contain multiple entries for the same variable with different casings, even
69
+ * though the values are always the same. In Node 16, there is only one name
70
+ * casing, even if it was spawned with multiple.
71
+ */
72
+ const ENVIRONMENT_VARIABLE_CASINGS_IF_WINDOWS = IS_WINDOWS
73
+ ? (() => {
74
+ const map = new Map();
75
+ for (const caseSensitiveName of Object.keys(process.env)) {
76
+ const lowerCaseName = caseSensitiveName.toLowerCase();
77
+ let arr = map.get(lowerCaseName);
78
+ if (arr === undefined) {
79
+ arr = [];
80
+ map.set(lowerCaseName, arr);
81
+ }
82
+ arr.push(caseSensitiveName);
83
+ }
84
+ return map;
85
+ })()
86
+ : undefined;
87
+ //# sourceMappingURL=windows.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { Deferred } from './deferred.js';
7
+ /**
8
+ * A mechanism for ensuring that at most N tasks are taking place at once.
9
+ *
10
+ * Useful in Wireit to prevent running too many scripts at once and swamping
11
+ * the system. For unlimited parallelism, just set numWorkers to Infinity.
12
+ *
13
+ * Note that node is still single threaded by default. This is useful for
14
+ * Wireit because almost all work is happening in script commands which run
15
+ * in separate processes.
16
+ *
17
+ * No guarantee is made about ordering or fairness of scheduling, though
18
+ * as implemented it's currently LIFO. Deadlocks may occur if there are
19
+ * dependencies between tasks.
20
+ */
21
+ export class WorkerPool {
22
+ #availableWorkers;
23
+ #waitingWorkers = [];
24
+ constructor(numWorkers) {
25
+ if (numWorkers <= 0) {
26
+ throw new Error(`WorkerPool needs a positive number of workers, got ${numWorkers}`);
27
+ }
28
+ this.#availableWorkers = numWorkers;
29
+ }
30
+ /**
31
+ * Calls workFn and returns its result.
32
+ *
33
+ * However, no more than `numWorkers` simultaneous calls to workFns will
34
+ * be running at any given time, to prevent overloading the machine.
35
+ */
36
+ async run(workFn) {
37
+ if (this.#availableWorkers <= 0) {
38
+ const waiter = new Deferred();
39
+ this.#waitingWorkers.push(waiter);
40
+ await waiter.promise;
41
+ if (this.#availableWorkers <= 0) {
42
+ throw new Error(`Internal error: expected availableWorkers to be positive after task was awoken, but was ${this.#availableWorkers}`);
43
+ }
44
+ }
45
+ this.#availableWorkers--;
46
+ try {
47
+ return await workFn();
48
+ }
49
+ finally {
50
+ this.#availableWorkers++;
51
+ if (this.#availableWorkers <= 0) {
52
+ // We intend to override any return or throw with this error in this
53
+ // case.
54
+ // eslint-disable-next-line no-unsafe-finally
55
+ throw new Error(`Internal error: expected availableWorkers to be positive after incrementing, but was ${this.#availableWorkers}`);
56
+ }
57
+ this.#waitingWorkers.pop()?.resolve();
58
+ }
59
+ }
60
+ }
61
+ //# sourceMappingURL=worker-pool.js.map