@amazon/vinyl-build-utils 1.0.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 (108) hide show
  1. package/README.md +49 -0
  2. package/dist/babel/compatTransform.d.ts +20 -0
  3. package/dist/babel/compatTransform.d.ts.map +1 -0
  4. package/dist/babel/compatTransform.js +46 -0
  5. package/dist/browserstack/BrowserStackWorker.d.ts +150 -0
  6. package/dist/browserstack/BrowserStackWorker.d.ts.map +1 -0
  7. package/dist/browserstack/BrowserStackWorker.js +280 -0
  8. package/dist/browserstack/BrowserStackWorkerController.d.ts +103 -0
  9. package/dist/browserstack/BrowserStackWorkerController.d.ts.map +1 -0
  10. package/dist/browserstack/BrowserStackWorkerController.js +199 -0
  11. package/dist/browserstack/browserStackLocal.d.ts +3 -0
  12. package/dist/browserstack/browserStackLocal.d.ts.map +1 -0
  13. package/dist/browserstack/browserStackLocal.js +36 -0
  14. package/dist/browserstack/browserStackRestApi.d.ts +304 -0
  15. package/dist/browserstack/browserStackRestApi.d.ts.map +1 -0
  16. package/dist/browserstack/browserStackRestApi.js +121 -0
  17. package/dist/browserstack/configureReporterApi.d.ts +85 -0
  18. package/dist/browserstack/configureReporterApi.d.ts.map +1 -0
  19. package/dist/browserstack/configureReporterApi.js +51 -0
  20. package/dist/browserstack/runBrowserStack.d.ts +123 -0
  21. package/dist/browserstack/runBrowserStack.d.ts.map +1 -0
  22. package/dist/browserstack/runBrowserStack.js +204 -0
  23. package/dist/esbuild/CompatTransformPlugin.d.ts +6 -0
  24. package/dist/esbuild/CompatTransformPlugin.d.ts.map +1 -0
  25. package/dist/esbuild/CompatTransformPlugin.js +59 -0
  26. package/dist/esbuild/PureExportsPlugin.d.ts +11 -0
  27. package/dist/esbuild/PureExportsPlugin.d.ts.map +1 -0
  28. package/dist/esbuild/PureExportsPlugin.js +55 -0
  29. package/dist/esbuild/TypeDeclarationsPlugin.d.ts +9 -0
  30. package/dist/esbuild/TypeDeclarationsPlugin.d.ts.map +1 -0
  31. package/dist/esbuild/TypeDeclarationsPlugin.js +60 -0
  32. package/dist/esbuild/analyzeExports.d.ts +7 -0
  33. package/dist/esbuild/analyzeExports.d.ts.map +1 -0
  34. package/dist/esbuild/analyzeExports.js +78 -0
  35. package/dist/esbuild/buildAll.d.ts +12 -0
  36. package/dist/esbuild/buildAll.d.ts.map +1 -0
  37. package/dist/esbuild/buildAll.js +16 -0
  38. package/dist/esbuild/optionalEntryPoints.d.ts +8 -0
  39. package/dist/esbuild/optionalEntryPoints.d.ts.map +1 -0
  40. package/dist/esbuild/optionalEntryPoints.js +20 -0
  41. package/dist/express/serve.d.ts +33 -0
  42. package/dist/express/serve.d.ts.map +1 -0
  43. package/dist/express/serve.js +31 -0
  44. package/dist/express/startExpressServer.d.ts +80 -0
  45. package/dist/express/startExpressServer.d.ts.map +1 -0
  46. package/dist/express/startExpressServer.js +114 -0
  47. package/dist/index.d.ts +36 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +35 -0
  50. package/dist/tsconfig.tsbuildinfo +1 -0
  51. package/dist/typescript/buildTypeDeclarations.d.ts +47 -0
  52. package/dist/typescript/buildTypeDeclarations.d.ts.map +1 -0
  53. package/dist/typescript/buildTypeDeclarations.js +65 -0
  54. package/dist/typescript/parseTsConfig.d.ts +13 -0
  55. package/dist/typescript/parseTsConfig.d.ts.map +1 -0
  56. package/dist/typescript/parseTsConfig.js +23 -0
  57. package/dist/typescript/replacePathAliases.d.ts +10 -0
  58. package/dist/typescript/replacePathAliases.d.ts.map +1 -0
  59. package/dist/typescript/replacePathAliases.js +26 -0
  60. package/dist/typescript/resolveProjectReferences.d.ts +8 -0
  61. package/dist/typescript/resolveProjectReferences.d.ts.map +1 -0
  62. package/dist/typescript/resolveProjectReferences.js +42 -0
  63. package/dist/typescript/tsc.d.ts +15 -0
  64. package/dist/typescript/tsc.d.ts.map +1 -0
  65. package/dist/typescript/tsc.js +28 -0
  66. package/dist/util/Logger.d.ts +34 -0
  67. package/dist/util/Logger.d.ts.map +1 -0
  68. package/dist/util/Logger.js +88 -0
  69. package/dist/util/TaskQueue.d.ts +30 -0
  70. package/dist/util/TaskQueue.d.ts.map +1 -0
  71. package/dist/util/TaskQueue.js +55 -0
  72. package/dist/util/console.d.ts +22 -0
  73. package/dist/util/console.d.ts.map +1 -0
  74. package/dist/util/console.js +58 -0
  75. package/dist/util/copyFiles.d.ts +33 -0
  76. package/dist/util/copyFiles.d.ts.map +1 -0
  77. package/dist/util/copyFiles.js +55 -0
  78. package/dist/util/extractArgs.d.ts +12 -0
  79. package/dist/util/extractArgs.d.ts.map +1 -0
  80. package/dist/util/extractArgs.js +23 -0
  81. package/dist/util/getRootProjectDir.d.ts +2 -0
  82. package/dist/util/getRootProjectDir.d.ts.map +1 -0
  83. package/dist/util/getRootProjectDir.js +14 -0
  84. package/dist/util/prompt.d.ts +2 -0
  85. package/dist/util/prompt.d.ts.map +1 -0
  86. package/dist/util/prompt.js +15 -0
  87. package/dist/util/sleep.d.ts +6 -0
  88. package/dist/util/sleep.d.ts.map +1 -0
  89. package/dist/util/sleep.js +13 -0
  90. package/dist/util/string.d.ts +22 -0
  91. package/dist/util/string.d.ts.map +1 -0
  92. package/dist/util/string.js +35 -0
  93. package/dist/util/writeIndexFile.d.ts +13 -0
  94. package/dist/util/writeIndexFile.d.ts.map +1 -0
  95. package/dist/util/writeIndexFile.js +92 -0
  96. package/dist/vinyl/buildVinylPackage.d.ts +40 -0
  97. package/dist/vinyl/buildVinylPackage.d.ts.map +1 -0
  98. package/dist/vinyl/buildVinylPackage.js +167 -0
  99. package/dist/vinyl/vinylDefaultBrowserStackOptions.d.ts +25 -0
  100. package/dist/vinyl/vinylDefaultBrowserStackOptions.d.ts.map +1 -0
  101. package/dist/vinyl/vinylDefaultBrowserStackOptions.js +28 -0
  102. package/dist/vinyl/vinylDefaultTscOptions.d.ts +3 -0
  103. package/dist/vinyl/vinylDefaultTscOptions.d.ts.map +1 -0
  104. package/dist/vinyl/vinylDefaultTscOptions.js +11 -0
  105. package/dist/vinyl/vinylSupportedBrowsers.d.ts +18 -0
  106. package/dist/vinyl/vinylSupportedBrowsers.d.ts.map +1 -0
  107. package/dist/vinyl/vinylSupportedBrowsers.js +55 -0
  108. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ ## Shared Build Utilities
2
+
3
+ Reusable build utility for Amazon Vinyl and Amazon Vinyl-related TypeScript
4
+ libraries.
5
+
6
+ ## Packages
7
+
8
+ ### util
9
+
10
+ General utilities for build scripts and configuration.
11
+
12
+ ### vinyl
13
+
14
+ Amazon Vinyl defaults for the provided tools.
15
+
16
+ ### typescript
17
+
18
+ Utilities for compiling and running TypeScript code.
19
+
20
+ ## Build Tools
21
+
22
+ ### ESBuild (https://esbuild.github.io/)
23
+
24
+ ESBuild provides an exceptionally fast typescript build system that provides
25
+ simple hooks where we can add our custom plugins. This allows us to run babel
26
+ transpilation, replace path aliases, and emit type declarations after file
27
+ changes were detected.
28
+
29
+ ### Babel (https://babeljs.io/)
30
+
31
+ Babel is a transpiler for js or ts that produces highly-compatible applications
32
+ or libraries.
33
+
34
+ The compatibility targets are defined in the package.json "browserslist"
35
+ property. Use https://browsersl.ist/ to generate the expression for
36
+ compatibility targets.
37
+
38
+ ### BrowserStack
39
+
40
+ We use the BrowserStack service to run integration and unit tests on real
41
+ browsers and devices. It allows us to set a compatibility matrix which we can
42
+ assert that all functionality for our playback engine works as expected for
43
+ every supported platform.
44
+
45
+ [BrowserStack Readme](./src/browserstack/README.md)
46
+
47
+ ### Express
48
+
49
+ Express is an HTTP server which supports SSL, proxies, and REST apis.
@@ -0,0 +1,20 @@
1
+ import type { TransformOptions } from '@babel/core';
2
+ export interface CompatTransformOptions {
3
+ /**
4
+ * The modern esm module to transform.
5
+ */
6
+ readonly src: string;
7
+ /**
8
+ * The destination es5 js file.
9
+ */
10
+ readonly dest: string;
11
+ readonly options?: TransformOptions;
12
+ }
13
+ /**
14
+ * Transforms a source file to a destination ES5 target.
15
+ * Use the compatTransformPlugin for ESBuild integration.
16
+ *
17
+ * @param options
18
+ */
19
+ export declare function compatTransform(options: CompatTransformOptions): Promise<void>;
20
+ //# sourceMappingURL=compatTransform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compatTransform.d.ts","sourceRoot":"","sources":["../../src/babel/compatTransform.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAIpE,MAAM,WAAW,sBAAsB;IACnC;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IAEpB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAA;CACtC;AAQD;;;;;GAKG;AACH,wBAAsB,eAAe,CACjC,OAAO,EAAE,sBAAsB,GAChC,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
@@ -0,0 +1,46 @@
1
+ /*
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { transformFile } from '@babel/core';
6
+ import fs from 'node:fs';
7
+ const defaultTransformOptions = {
8
+ presets: ['@babel/preset-env'],
9
+ minified: true,
10
+ sourceType: 'unambiguous',
11
+ };
12
+ /**
13
+ * Transforms a source file to a destination ES5 target.
14
+ * Use the compatTransformPlugin for ESBuild integration.
15
+ *
16
+ * @param options
17
+ */
18
+ export async function compatTransform(options) {
19
+ if (!fs.existsSync(options.src)) {
20
+ throw new Error(`babel: src missing`);
21
+ }
22
+ // Currently does not handle source map chaining.
23
+ const result = await new Promise((resolve, reject) => {
24
+ transformFile(options.src, options.options ?? defaultTransformOptions, (err, result) => {
25
+ if (err)
26
+ reject(err);
27
+ else
28
+ resolve(result);
29
+ });
30
+ });
31
+ // Write the transformed code to a new file
32
+ const code = result?.code;
33
+ if (code) {
34
+ await new Promise((resolve, reject) => {
35
+ fs.writeFile(options.dest, code, 'utf8', (err) => {
36
+ if (err)
37
+ reject(err);
38
+ else
39
+ resolve(void 0);
40
+ });
41
+ });
42
+ }
43
+ else {
44
+ throw new Error(`babel: transpile failed`);
45
+ }
46
+ }
@@ -0,0 +1,150 @@
1
+ import { type BrowserStackAutomateClient, type SessionDetails, type WorkerOptions } from './browserStackRestApi';
2
+ import type { LogEntry } from './configureReporterApi';
3
+ /**
4
+ * Local state for a single session/worker.
5
+ */
6
+ export interface BrowserStackWorkerState {
7
+ /**
8
+ * A local identifier for the session, used to map reporter API calls to its respective worker session.
9
+ */
10
+ readonly localId: string;
11
+ /**
12
+ * The browser worker configuration provided during construction.
13
+ */
14
+ readonly workerOptions: WorkerOptions;
15
+ /**
16
+ * If non-null, indicates a fatal error.
17
+ */
18
+ error: Error | null;
19
+ /**
20
+ * The session details, as retrieved from BrowserStack.
21
+ */
22
+ session: SessionDetails | null;
23
+ /**
24
+ * True if the worker has started via {@link BrowserStackWorker.start}.
25
+ * Once true, will not become false after termination.
26
+ */
27
+ started: boolean;
28
+ /**
29
+ * True if the session is active.
30
+ */
31
+ active: boolean;
32
+ /**
33
+ * The worker id.
34
+ * When the session and worker have been created, this will be set.
35
+ */
36
+ workerId: number | null;
37
+ /**
38
+ * The hashed session id.
39
+ * When the session and worker have been created, this will be set.
40
+ */
41
+ sessionId: string | null;
42
+ /**
43
+ * The total number of tests as reported by the test REST api.
44
+ * This will be null until the value is received.
45
+ */
46
+ totalSpecs: number | null;
47
+ /**
48
+ * The total number of specs completed, as reported by the test REST api.
49
+ */
50
+ totalCompleted: number;
51
+ /**
52
+ * The number of retry attempts remaining.
53
+ * A retry will happen if there has been a capture timeout, not if there has been a test failure.
54
+ */
55
+ attemptsRemaining: number;
56
+ }
57
+ export type ReadonlyBrowserStackWorkerState = Readonly<BrowserStackWorkerState>;
58
+ export interface BrowserStackWorkerDeps {
59
+ /**
60
+ * The BrowserStack REST API.
61
+ */
62
+ readonly client: BrowserStackAutomateClient;
63
+ }
64
+ export interface BrowserStackWorkerOptions {
65
+ /**
66
+ * The number of seconds to wait for data before attempting to retry the test.
67
+ * Default: 45
68
+ */
69
+ readonly captureTimeout?: number;
70
+ /**
71
+ * The number of retries to start the session before giving up.
72
+ * Default: 2
73
+ */
74
+ readonly retries?: number;
75
+ /**
76
+ * The number of seconds between session update requests.
77
+ * Default: 5
78
+ */
79
+ readonly pollInterval?: number;
80
+ /**
81
+ * The directory to emit console logs.
82
+ * Set to null to disable console logging.
83
+ * Default: ./bStackLogs
84
+ */
85
+ readonly logsDir?: string | null;
86
+ /**
87
+ * If provided, adds the given query parameters to the launched URL.
88
+ */
89
+ readonly queryParams?: Record<string, string> | null;
90
+ }
91
+ export declare class BrowserStackWorker {
92
+ private readonly deps;
93
+ /**
94
+ * Set to observe state updates.
95
+ */
96
+ onUpdate: () => void;
97
+ private readonly _state;
98
+ private tasks;
99
+ private pollIntervalId;
100
+ private captureTimeoutId;
101
+ private options;
102
+ /**
103
+ * The worker url with an appended reportApi query param.
104
+ */
105
+ private readonly url;
106
+ constructor(deps: BrowserStackWorkerDeps, workerOptions: WorkerOptions, options?: BrowserStackWorkerOptions);
107
+ /**
108
+ * Returns true if this worker is completed. This will be true if either in an error state or the session status
109
+ * indicates completion
110
+ */
111
+ get completed(): boolean;
112
+ /**
113
+ * Returns this worker's state.
114
+ */
115
+ get state(): ReadonlyBrowserStackWorkerState;
116
+ get name(): string | undefined;
117
+ get passed(): boolean;
118
+ set totalCompleted(value: number);
119
+ set totalSpecs(value: number);
120
+ done(passed: boolean, reason: string): Promise<void>;
121
+ private refreshSession;
122
+ appendLogs(logs: readonly LogEntry[]): void;
123
+ /**
124
+ * Creates the worker/session.
125
+ */
126
+ start(): Promise<void>;
127
+ /**
128
+ * Deletes the worker.
129
+ * The worker may be created again.
130
+ */
131
+ terminate(): Promise<void>;
132
+ private clearTimers;
133
+ /**
134
+ * Deletes the session if it exists.
135
+ */
136
+ private deleteSession;
137
+ /**
138
+ * Terminates the worker, deletes the session, starts a new worker.
139
+ */
140
+ restart(): Promise<void>;
141
+ /**
142
+ * Invoked when the capture timeout has been reached. Checks if data has been received from the REST api.
143
+ */
144
+ private captureTimeoutHandler;
145
+ /**
146
+ * Sets the worker to a failed state and terminates.
147
+ */
148
+ private setError;
149
+ }
150
+ //# sourceMappingURL=BrowserStackWorker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrowserStackWorker.d.ts","sourceRoot":"","sources":["../../src/browserstack/BrowserStackWorker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,280 @@
1
+ /*
2
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { TaskQueue } from '../util/TaskQueue';
6
+ import { logger } from '../util/Logger';
7
+ import path from 'node:path';
8
+ import fs from 'node:fs';
9
+ import crypto from 'node:crypto';
10
+ export class BrowserStackWorker {
11
+ deps;
12
+ /**
13
+ * Set to observe state updates.
14
+ */
15
+ onUpdate = () => { };
16
+ _state;
17
+ tasks = new TaskQueue();
18
+ pollIntervalId = null;
19
+ captureTimeoutId = null;
20
+ options;
21
+ /**
22
+ * The worker url with an appended reportApi query param.
23
+ */
24
+ url;
25
+ constructor(deps, workerOptions, options) {
26
+ this.deps = deps;
27
+ this.tasks.onError = (error) => this.setError(error);
28
+ this.options = {
29
+ retries: 2,
30
+ captureTimeout: 45,
31
+ pollInterval: 5,
32
+ logsDir: './bStackLogs',
33
+ queryParams: null,
34
+ ...options,
35
+ };
36
+ this._state = {
37
+ localId: crypto.randomUUID(),
38
+ workerOptions: workerOptions,
39
+ error: null,
40
+ session: null,
41
+ started: false,
42
+ active: false,
43
+ workerId: null,
44
+ sessionId: null,
45
+ totalSpecs: null,
46
+ totalCompleted: 0,
47
+ attemptsRemaining: this.options.retries,
48
+ };
49
+ // Add the reportApi query param:
50
+ const finalUrl = new URL(workerOptions.url);
51
+ finalUrl.searchParams.set('reportApi', `/report/${this._state.localId}`);
52
+ // Add the provided query params from options.
53
+ if (options?.queryParams) {
54
+ for (const [name, value] of Object.entries(options.queryParams)) {
55
+ finalUrl.searchParams.set(name, value);
56
+ }
57
+ }
58
+ this.url = finalUrl.toString();
59
+ if (this.options.logsDir)
60
+ fs.mkdirSync(this.options.logsDir, { recursive: true });
61
+ }
62
+ /**
63
+ * Returns true if this worker is completed. This will be true if either in an error state or the session status
64
+ * indicates completion
65
+ */
66
+ get completed() {
67
+ const state = this._state;
68
+ const status = state.session?.status;
69
+ return (state.error != null ||
70
+ (state.started &&
71
+ status != null &&
72
+ ['timeout', 'passed', 'failed'].includes(status)));
73
+ }
74
+ /**
75
+ * Returns this worker's state.
76
+ */
77
+ get state() {
78
+ return this._state;
79
+ }
80
+ get name() {
81
+ return this._state.workerOptions.name;
82
+ }
83
+ get passed() {
84
+ return this._state.session?.status === 'passed';
85
+ }
86
+ set totalCompleted(value) {
87
+ this._state.totalCompleted = value;
88
+ }
89
+ set totalSpecs(value) {
90
+ this._state.totalSpecs = value;
91
+ }
92
+ done(passed, reason) {
93
+ const name = this.name;
94
+ if (passed)
95
+ logger.info(`✅ ${name} passed`);
96
+ else
97
+ logger.info(`❌ ${name} failed\n${reason}\n`);
98
+ const client = this.deps.client;
99
+ return this.tasks
100
+ .enqueue(async () => {
101
+ const state = this._state;
102
+ const { sessionId, workerId } = state;
103
+ if (!sessionId || !workerId) {
104
+ logger.debug('done aborting - sessionId or workerId is missing');
105
+ return;
106
+ }
107
+ logger.debug('done, passed:', passed);
108
+ state.session = await client.updateSession(sessionId, {
109
+ status: passed ? 'passed' : 'failed',
110
+ reason: reason,
111
+ });
112
+ this.onUpdate();
113
+ })
114
+ .finally(() => {
115
+ this.terminate().catch(() => { });
116
+ });
117
+ }
118
+ async refreshSession() {
119
+ const state = this._state;
120
+ if (state.sessionId) {
121
+ state.session = await this.deps.client.getSession(state.sessionId);
122
+ this.onUpdate();
123
+ }
124
+ }
125
+ appendLogs(logs) {
126
+ if (this.options.logsDir == null || logs.length === 0)
127
+ return;
128
+ const logFilePath = path.join(this.options.logsDir, `${this._state.sessionId}.log`);
129
+ // Append logs to a file.
130
+ fs.appendFile(logFilePath, logs.map((entry) => entry.join(' ')).join('\n') + '\n', (error) => {
131
+ if (error)
132
+ logger.error('Could not write to console log file:', error);
133
+ });
134
+ }
135
+ /**
136
+ * Creates the worker/session.
137
+ */
138
+ async start() {
139
+ logger.debug(`Starting session '${this.name}'...`);
140
+ const state = this._state;
141
+ state.started = true;
142
+ await this.tasks
143
+ .enqueue(async () => {
144
+ if (state.active)
145
+ return;
146
+ state.active = true;
147
+ this.onUpdate();
148
+ this.pollIntervalId = setInterval(() => {
149
+ void this.refreshSession();
150
+ }, this.options.pollInterval * 1000);
151
+ this.captureTimeoutId = setTimeout(this.captureTimeoutHandler, this.options.captureTimeout * 1000);
152
+ const client = this.deps.client;
153
+ const newWorker = await client.createWorker({
154
+ ...state.workerOptions,
155
+ url: this.url,
156
+ });
157
+ state.workerId = newWorker.id;
158
+ const workerDetails = await client.getWorker(newWorker.id);
159
+ logger.info(`Session created for '${this.name}': ${workerDetails.browser_url}`);
160
+ state.sessionId = workerDetails.sessionId;
161
+ await this.refreshSession();
162
+ this.onUpdate();
163
+ })
164
+ .catch((error) => {
165
+ logger.error(`Session start failed '${this.name}'`, error);
166
+ state.started = false;
167
+ this.clearTimers();
168
+ this.onUpdate();
169
+ });
170
+ }
171
+ /**
172
+ * Deletes the worker.
173
+ * The worker may be created again.
174
+ */
175
+ terminate() {
176
+ logger.debug(`Terminate session '${this.name}'`);
177
+ return this.tasks.enqueue(async () => {
178
+ const state = this._state;
179
+ if (!state.active)
180
+ return;
181
+ state.active = false;
182
+ this.clearTimers();
183
+ const workerId = state.workerId;
184
+ if (workerId) {
185
+ state.workerId = null;
186
+ try {
187
+ await this.deps.client.deleteWorker(workerId);
188
+ logger.debug(`Terminated worker: ${workerId}`);
189
+ }
190
+ catch (error) {
191
+ logger.warn('Could not terminate worker:', error);
192
+ }
193
+ }
194
+ logger.debug(`Terminated session '${this.name}'...`);
195
+ this.onUpdate();
196
+ });
197
+ }
198
+ clearTimers() {
199
+ if (this.pollIntervalId) {
200
+ clearInterval(this.pollIntervalId);
201
+ this.pollIntervalId = null;
202
+ }
203
+ if (this.captureTimeoutId) {
204
+ clearTimeout(this.captureTimeoutId);
205
+ this.captureTimeoutId = null;
206
+ }
207
+ }
208
+ /**
209
+ * Deletes the session if it exists.
210
+ */
211
+ deleteSession() {
212
+ const state = this._state;
213
+ logger.debug(`Delete session '${this.name}'`);
214
+ return this.tasks.enqueue(async () => {
215
+ const sessionId = state.sessionId;
216
+ if (sessionId) {
217
+ state.sessionId = null;
218
+ state.session = null;
219
+ try {
220
+ await this.deps.client.deleteSession(sessionId);
221
+ logger.debug(`Deleted session '${this.name}'`);
222
+ }
223
+ catch (_) {
224
+ logger.warn(`Could not delete session '${this.name}'`);
225
+ }
226
+ this.onUpdate();
227
+ }
228
+ });
229
+ }
230
+ /**
231
+ * Terminates the worker, deletes the session, starts a new worker.
232
+ */
233
+ async restart() {
234
+ await this.terminate().catch(() => { });
235
+ await this.deleteSession().catch(() => { });
236
+ await this.start();
237
+ }
238
+ /**
239
+ * Invoked when the capture timeout has been reached. Checks if data has been received from the REST api.
240
+ */
241
+ captureTimeoutHandler = () => {
242
+ const state = this._state;
243
+ if (state.totalSpecs == null) {
244
+ if (state.attemptsRemaining > 0) {
245
+ state.attemptsRemaining--;
246
+ logger.warn(`No test results received for ${this.name}, retrying. Attempts remaining: ${state.attemptsRemaining}`);
247
+ this.restart().catch((error) => {
248
+ this.setError(error);
249
+ });
250
+ }
251
+ else {
252
+ this.setError(new Error(`Did not capture test progress after ${this.options.retries + 1} attempts for '${this.name}'.`));
253
+ }
254
+ }
255
+ };
256
+ /**
257
+ * Sets the worker to a failed state and terminates.
258
+ */
259
+ setError(error) {
260
+ const state = this._state;
261
+ if (state.error)
262
+ return; // Already in an erred state.
263
+ state.error = error;
264
+ logger.error(error == null
265
+ ? 'An internal error has occurred.'
266
+ : (error.message ?? String(error)));
267
+ this.tasks
268
+ .enqueue(async () => {
269
+ if (state.sessionId) {
270
+ state.session = await this.deps.client.updateSession(state.sessionId, {
271
+ status: 'failed',
272
+ reason: error?.message ?? 'Internal error',
273
+ });
274
+ this.onUpdate();
275
+ }
276
+ })
277
+ .catch(() => { });
278
+ this.terminate().catch(() => { });
279
+ }
280
+ }
@@ -0,0 +1,103 @@
1
+ import type { BrowserStackWorkerOptions, ReadonlyBrowserStackWorkerState } from './BrowserStackWorker';
2
+ import type { BrowserStackAutomateClient, WorkerOptions } from './browserStackRestApi';
3
+ import type { DoneRequest, ProgressRequest, ReportingListener, StartedRequest } from './configureReporterApi';
4
+ export interface BrowserStackWorkerControllerDeps {
5
+ /**
6
+ * The BrowserStack REST API client.
7
+ */
8
+ readonly client: BrowserStackAutomateClient;
9
+ }
10
+ export interface BrowserStackWorkerControllerOptions {
11
+ /**
12
+ * Configuration to provide to each Worker.
13
+ */
14
+ readonly worker?: BrowserStackWorkerOptions;
15
+ /**
16
+ * The maximum number of sessions to run in parallel.
17
+ * If not set, the maximum number of sessions according to the automate plan will be used.
18
+ */
19
+ readonly maxSessions?: number;
20
+ }
21
+ export interface BrowserStackWorkerControllerState {
22
+ readonly workers: readonly ReadonlyBrowserStackWorkerState[];
23
+ /**
24
+ * The number of workers not yet started.
25
+ */
26
+ pending: number;
27
+ /**
28
+ * The number of workers currently active.
29
+ */
30
+ running: number;
31
+ /**
32
+ * The number of workers completed and passed.
33
+ */
34
+ passed: number;
35
+ /**
36
+ * The number of workers completed and failed.
37
+ */
38
+ failed: number;
39
+ /**
40
+ * The estimated percentage complete, a number from 0-1.
41
+ */
42
+ percentComplete: number;
43
+ /**
44
+ * The total number of workers not yet completed.
45
+ */
46
+ remaining: number;
47
+ /**
48
+ * The total number of workers.
49
+ */
50
+ readonly total: number;
51
+ }
52
+ export type ReadonlyBrowserStackWorkerControllerState = Readonly<BrowserStackWorkerControllerState>;
53
+ export declare class BrowserStackWorkerController implements ReportingListener {
54
+ private readonly deps;
55
+ private readonly options?;
56
+ /**
57
+ * Set to observe state updates.
58
+ */
59
+ onUpdate: () => void;
60
+ private readonly _state;
61
+ private readonly workers;
62
+ /**
63
+ * Maps the worker's local id to the worker.
64
+ */
65
+ private readonly localIdToWorkerMap;
66
+ private sessionPollIntervalId;
67
+ /**
68
+ * Tracks an in-flight startSessions invocation. While set, additional calls
69
+ * are deferred until the current one settles to avoid redundant API calls
70
+ * and racing state updates.
71
+ */
72
+ private startSessionsInFlight;
73
+ /**
74
+ * If set during an in-flight startSessions, indicates another invocation
75
+ * was requested and should run after the current one settles.
76
+ */
77
+ private startSessionsRequested;
78
+ constructor(deps: BrowserStackWorkerControllerDeps, browsers: readonly WorkerOptions[], options?: BrowserStackWorkerControllerOptions | undefined);
79
+ get state(): ReadonlyBrowserStackWorkerControllerState;
80
+ shouldHandleRequest(id: string): boolean;
81
+ onStarted(request: StartedRequest): void;
82
+ onProgress(request: ProgressRequest): void;
83
+ onDone(request: DoneRequest): void;
84
+ private refreshState;
85
+ /**
86
+ * Refreshes the current state and invokes the onUpdate callback.
87
+ */
88
+ private updateHandler;
89
+ /**
90
+ * Begins new workers until the maximum parallel sessions is reached.
91
+ * Reentrancy-safe: if invoked while a previous invocation is still
92
+ * running, schedules a single follow-up to run after it settles.
93
+ */
94
+ private startSessions;
95
+ private clearOpenSessionPollTimeout;
96
+ get allPassed(): boolean;
97
+ start(): void;
98
+ /**
99
+ * Terminates all running workers.
100
+ */
101
+ terminate(): Promise<void>;
102
+ }
103
+ //# sourceMappingURL=BrowserStackWorkerController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrowserStackWorkerController.d.ts","sourceRoot":"","sources":["../../src/browserstack/BrowserStackWorkerController.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACR,yBAAyB,EACzB,+BAA+B,EAClC,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACR,0BAA0B,EAC1B,aAAa,EAChB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EACR,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,cAAc,EACjB,MAAM,wBAAwB,CAAA;AAG/B,MAAM,WAAW,gCAAgC;IAC7C;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;CAC9C;AAED,MAAM,WAAW,mCAAmC;IAChD;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAA;IAE3C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,iCAAiC;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,+BAA+B,EAAE,CAAA;IAE5D;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,eAAe,EAAE,MAAM,CAAA;IAEvB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,MAAM,yCAAyC,GACjD,QAAQ,CAAC,iCAAiC,CAAC,CAAA;AAI/C,qBAAa,4BAA6B,YAAW,iBAAiB;IA8B9D,OAAO,CAAC,QAAQ,CAAC,IAAI;IAErB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IA/B7B;;OAEG;IACI,QAAQ,EAAE,MAAM,IAAI,CAAW;IAEtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAE9C;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAiC;IAEpE,OAAO,CAAC,qBAAqB,CAAuB;IAEpD;;;;OAIG;IACH,OAAO,CAAC,qBAAqB,CAA6B;IAE1D;;;OAGG;IACH,OAAO,CAAC,sBAAsB,CAAQ;gBAGjB,IAAI,EAAE,gCAAgC,EACvD,QAAQ,EAAE,SAAS,aAAa,EAAE,EACjB,OAAO,CAAC,EAAE,mCAAmC,YAAA;IA2BlE,IAAI,KAAK,IAAI,yCAAyC,CAErD;IAED,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxC,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAKxC,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAM1C,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAQlC,OAAO,CAAC,YAAY;IA8BpB;;OAEG;IACH,OAAO,CAAC,aAAa,CAMpB;IAED;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAwErB,OAAO,CAAC,2BAA2B;IAOnC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,KAAK,IAAI,IAAI;IAKb;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAenC"}