diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2882c9652c..c12663ff991 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,13 +18,16 @@ again. ## Getting setup 1. Clone this repository + ```bash git clone https://github.com/GoogleChrome/puppeteer cd puppeteer ``` -2. Install dependencies + +2. Install dependencies + ```bash -yarn # or 'npm install' +npm install ``` ## Code reviews @@ -41,7 +44,8 @@ information on using pull requests. - comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory To run code linter, use: -``` + +```bash npm run lint ``` @@ -73,11 +77,11 @@ footer - `test` - changes to puppeteer tests infrastructure - `style` - puppeteer code style: spaces/alignment/wrapping etc - `chore` - build-related work, e.g. doclint changes / travis / appveyor -1. *namespace* is put in parenthesis after label and is optional -2. *title* is a brief summary of changes -3. *description* is **optional**, new-line separated from title and is in present tense -4. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues -5. *footer* should also include "BREAKING CHANGE" if current API clients will break due to this change. It should explain what changed and how to get the old behavior. +2. *namespace* is put in parenthesis after label and is optional +3. *title* is a brief summary of changes +4. *description* is **optional**, new-line separated from title and is in present tense +5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues +6. *footer* should also include "BREAKING CHANGE" if current API clients will break due to this change. It should explain what changed and how to get the old behavior. Example: @@ -93,13 +97,13 @@ To deliver to a different location, use "deliver" option: `page.pizza({deliver: 'work'})`. ``` - ## Writing Documentation All public API should have a descriptive entry in the [docs/api.md](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md). There's a [documentation linter](https://github.com/GoogleChrome/puppeteer/tree/master/utils/doclint) which makes sure documentation is aligned with the codebase. To run documentation linter, use -``` + +```bash npm run doc ``` @@ -110,7 +114,7 @@ For all dependencies (both installation and development): - if adding a dependency, it should be well-maintained and trustworthy A barrier for introducing new installation dependencies is especially high: -- **do not add** installation dependency unless it's critical to project success +- **Do not add** installation dependency unless it's critical to project success ## Writing Tests @@ -124,14 +128,25 @@ and are written with a [TestRunner](https://github.com/GoogleChrome/puppeteer/tr Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected. - To run all tests: -``` + +```bash npm run unit ``` -- To run tests in parallel, use `-j` flag: + +- To filter tests by name: + +```bash +npm run unit --filter=waitFor ``` + +- To run tests in parallel, use `-j` flag: + +```bash npm run unit -- -j 4 ``` + - To run a specific test, substitute the `it` with `fit` (mnemonic rule: '*focus it*'): + ```js ... // Using "fit" to run specific test @@ -140,7 +155,9 @@ npm run unit -- -j 4 expect(response.ok).toBe(true); })) ``` + - To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '*cross it*'): + ```js ... // Using "xit" to skip specific test @@ -149,20 +166,28 @@ npm run unit -- -j 4 expect(response.ok).toBe(true); })) ``` + - To run tests in non-headless mode: -``` + +```bash HEADLESS=false npm run unit ``` + - To run tests with custom Chromium executable: -``` + +```bash CHROME= npm run unit ``` + - To run tests in slow-mode: -``` + +```bash HEADLESS=false SLOW_MO=500 npm run unit ``` + - To debug a test, "focus" a test first and then run: -``` + +```bash node --inspect-brk test/test.js ``` @@ -172,10 +197,10 @@ Every public API method or event should be called at least once in tests. To ens Run coverage: -``` +```bash npm run coverage ``` ## Debugging Puppeteer -See [Debugging Tips](README.md#debugging-tips) in the readme +See [Debugging Tips](README.md#debugging-tips) in the readme diff --git a/README.md b/README.md index cd31f95c964..4b8887460da 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Puppeteer +# Puppeteer [![Linux Build Status](https://img.shields.io/travis/GoogleChrome/puppeteer/master.svg)](https://travis-ci.org/GoogleChrome/puppeteer) [![Windows Build Status](https://img.shields.io/appveyor/ci/aslushnikov/puppeteer/master.svg?logo=appveyor)](https://ci.appveyor.com/project/aslushnikov/puppeteer/branch/master) [![NPM puppeteer package](https://img.shields.io/npm/v/puppeteer.svg)](https://npmjs.org/package/puppeteer) @@ -31,7 +31,8 @@ Give it a spin: https://try-puppeteer.appspot.com/ ### Installation To use Puppeteer in your project, run: -``` + +```bash npm i --save puppeteer # or "yarn add puppeteer" ``` @@ -131,8 +132,7 @@ const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); See [`Puppeteer.launch()`](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions) for more information. -See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description -of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkcr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. +See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkcr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. **3. Creates a fresh user profile** @@ -154,7 +154,7 @@ Explore the [API documentation](docs/api.md) and [examples](https://github.com/G const browser = await puppeteer.launch({headless: false}); -1. Slow it down - the `slowMo` option slows down Puppeteer operations by the +2. Slow it down - the `slowMo` option slows down Puppeteer operations by the specified amount of milliseconds. It's another way to help see what's going on. const browser = await puppeteer.launch({ @@ -162,15 +162,16 @@ Explore the [API documentation](docs/api.md) and [examples](https://github.com/G slowMo: 250 // slow down by 250ms }); -1. Capture console output - You can listen for the `console` event. +3. Capture console output - You can listen for the `console` event. This is also handy when debugging code in `page.evaluate()`: page.on('console', msg => console.log('PAGE LOG:', msg.text())); - + await page.evaluate(() => console.log(`url is ${location.href}`)); + await page.evaluate(() => console.log(`url is ${location.href}`)); -1. Enable verbose logging - All public API calls and internal protocol traffic +4. Enable verbose logging - All public API calls and internal protocol traffic will be logged via the [`debug`](https://github.com/visionmedia/debug) module under the `puppeteer` namespace. # Basic verbose logging @@ -218,8 +219,7 @@ See [Contributing](https://github.com/GoogleChrome/puppeteer/blob/master/CONTRIB The goals of the project are simple: - Provide a slim, canonical library that highlights the capabilities of the [DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). -- Provide a reference implementation for similar testing libraries. Eventually, these -other frameworks could adopt Puppeteer as their foundational layer. +- Provide a reference implementation for similar testing libraries. Eventually, these other frameworks could adopt Puppeteer as their foundational layer. - Grow the adoption of headless/automated browser testing. - Help dogfood new DevTools Protocol features...and catch bugs! - Learn more about the pain points of automated browser testing and help fill those gaps. diff --git a/docs/api.md b/docs/api.md index 03dddf81dd9..c2bdf5f7d27 100644 --- a/docs/api.md +++ b/docs/api.md @@ -249,7 +249,6 @@ The Puppeteer API is hierarchical and mirrors the browser structure. On the foll (Diagram source: [link](https://docs.google.com/drawings/d/1Q_AM6KYs9kbyLZF-Lpp5mtpAWth73Cq8IKCsWYgi8MM/edit?usp=sharing)) - ### Environment Variables Puppeteer looks for certain [environment variables](https://en.wikipedia.org/wiki/Environment_variable) to aid its operations. These variables can either be set in the environment or in the [npm config](https://docs.npmjs.com/cli/config). @@ -315,14 +314,13 @@ This methods attaches Puppeteer to an existing Chromium instance. The method launches a browser instance with given arguments. The browser will be closed when the parent node.js process is closed. -> **NOTE** Puppeteer can also be used to control the Chrome browser, but it works best with the version of Chromium it is bundled with. There is no - guarantee it will work with any other version. Use `executablePath` option with extreme caution. -If Google Chrome (rather than Chromium) is preferred, a [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested. +> **NOTE** Puppeteer can also be used to control the Chrome browser, but it works best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use `executablePath` option with extreme caution. +> +> If Google Chrome (rather than Chromium) is preferred, a [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested. > > In [puppeteer.launch([options])](#puppeteerlaunchoptions) above, any mention of Chromium also applies to Chrome. > -> See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description - of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkcr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. +> See [`this article`](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for a description of the differences between Chromium and Chrome. [`This article`](https://chromium.googlesource.com/chromium/src/+/lkcr/docs/chromium_browser_vs_google_chrome.md) describes some differences for Linux users. ### class: BrowserFetcher @@ -600,7 +598,6 @@ The method runs `document.querySelectorAll` within the page. If no elements matc Shortcut for [page.mainFrame().$$(selector)](#frameselector-1). - #### page.$$eval(selector, pageFunction[, ...args]) - `selector` <[string]> A [selector] to query frame for - `pageFunction` <[function]> Function to be evaluated in browser context @@ -775,8 +772,8 @@ puppeteer.launch().then(async browser => { List of all available devices is available in the source code: [DeviceDescriptors.js](https://github.com/GoogleChrome/puppeteer/blob/master/DeviceDescriptors.js). #### page.emulateMedia(mediaType) - - `mediaType` Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables media emulation. - - returns: <[Promise]> +- `mediaType` Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables media emulation. +- returns: <[Promise]> #### page.evaluate(pageFunction, ...args) - `pageFunction` <[function]|[string]> Function to be evaluated in the page context @@ -840,7 +837,6 @@ await resultHandle.dispose(); Shortcut for [page.mainFrame().executionContext().evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args). - #### page.evaluateOnNewDocument(pageFunction, ...args) - `pageFunction` <[function]|[string]> Function to be evaluated in browser context - `...args` <...[Serializable]> Arguments to pass to `pageFunction` @@ -1124,16 +1120,16 @@ Shortcut for [page.mainFrame().executionContext().queryObjects(prototypeHandle)] #### page.screenshot([options]) - `options` <[Object]> Options object which might have the following properties: - - `path` <[string]> The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk. - - `type` <[string]> Specify screenshot type, can be either `jpeg` or `png`. Defaults to 'png'. - - `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images. - - `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page. Defaults to `false`. - - `clip` <[Object]> An object which specifies clipping region of the page. Should have the following fields: - - `x` <[number]> x-coordinate of top-left corner of clip area - - `y` <[number]> y-coordinate of top-left corner of clip area - - `width` <[number]> width of clipping area - - `height` <[number]> height of clipping area - - `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`. + - `path` <[string]> The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk. + - `type` <[string]> Specify screenshot type, can be either `jpeg` or `png`. Defaults to 'png'. + - `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images. + - `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page. Defaults to `false`. + - `clip` <[Object]> An object which specifies clipping region of the page. Should have the following fields: + - `x` <[number]> x-coordinate of top-left corner of clip area + - `y` <[number]> y-coordinate of top-left corner of clip area + - `width` <[number]> width of clipping area + - `height` <[number]> height of clipping area + - `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`. - returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with captured screenshot #### page.select(selector, ...values) @@ -1409,7 +1405,6 @@ puppeteer.launch().then(async browser => { ``` Shortcut for [page.mainFrame().waitForXPath(xpath[, options])](#framewaitforxpathxpath-options). - ### class: Keyboard Keyboard provides an api for managing a virtual keyboard. The high level api is [`keyboard.type`](#keyboardtypetext-options), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page. @@ -1897,7 +1892,6 @@ This method behaves differently with respect to the type of the first parameter: - if `selectorOrFunctionOrTimeout` is a `number`, then the first argument is treated as a timeout in milliseconds and the method returns a promise which resolves after the timeout - otherwise, an exception is thrown - #### frame.waitForFunction(pageFunction[, options[, ...args]]) - `pageFunction` <[function]|[string]> Function to be evaluated in browser context - `options` <[Object]> Optional waiting parameters @@ -2160,10 +2154,10 @@ The method evaluates the XPath expression relative to the elementHandle. If ther #### elementHandle.boundingBox() - returns: <[Promise]> - - x <[number]> the x coordinate of the element in pixels. - - y <[number]> the y coordinate of the element in pixels. - - width <[number]> the width of the element in pixels. - - height <[number]> the height of the element in pixels. + - x <[number]> the x coordinate of the element in pixels. + - y <[number]> the y coordinate of the element in pixels. + - width <[number]> the width of the element in pixels. + - height <[number]> the height of the element in pixels. This method returns the bounding box of the element (relative to the main frame), or `null` if the element is not visible. @@ -2459,7 +2453,6 @@ Identifies what kind of target this is. Can be `"page"`, `"service_worker"`, or #### target.url() - returns: <[string]> - ### class: CDPSession * extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) @@ -2492,7 +2485,6 @@ to send messages. - `params` <[Object]> Optional method parameters - returns: <[Promise]<[Object]>> - ### class: Coverage Coverage gathers information about parts of JavaScript and CSS that were used by the page. @@ -2544,7 +2536,6 @@ console.log(`Bytes used: ${usedBytes / totalBytes * 100}%`); > **NOTE** CSS Coverage doesn't include dynamically injected style tags without sourceURLs. - #### coverage.stopJSCoverage() - returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all non-anonymous scripts - `url` <[string]> Script URL @@ -2556,7 +2547,6 @@ console.log(`Bytes used: ${usedBytes / totalBytes * 100}%`); > **NOTE** JavaScript Coverage doesn't include anonymous scripts. However, scripts with sourceURLs are reported. - [Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array" [boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean" [Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer" diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index da421e9e5f3..3dba8299449 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -88,6 +88,7 @@ xorg-x11-fonts-misc - make sure kernel version is up-to-date - read about linux sandbox here: https://chromium.googlesource.com/chromium/src/+/master/docs/linux_suid_sandbox_development.md - try running without the sandbox (**Note: running without the sandbox is not recommended due to security reasons!**) + ```js const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}); ``` @@ -101,7 +102,7 @@ shared library dependencies. To fix, you'll need to install the missing dependencies and the latest Chromium package in your Dockerfile: -``` +```Dockerfile FROM node:8-slim # See https://crbug.com/795759 @@ -168,7 +169,7 @@ The [newest Chromium package](https://pkgs.alpinelinux.org/package/edge/communit Example Dockerfile: -``` +```Dockerfile FROM node:9-alpine # Installs latest Chromium (63) package. diff --git a/examples/README.md b/examples/README.md index c7111ff256f..1f2d7f433ee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,7 +8,7 @@ NODE_PATH=../ node examples/search.js # Tips & Tricks -### Load a Chrome extension +## Load a Chrome extension By default, Puppeteer disables extensions when launching Chrome. You can load a specific extension using: @@ -28,6 +28,7 @@ const browser = await puppeteer.launch({ > Other useful tools, articles, and projects that use Puppeteer. ## Rendering and web scraping + - [Puppetron](https://github.com/cheeaun/puppetron) - Demo site that shows how to use Puppeteer and Headless Chrome to render pages. Inspired by [GoogleChrome/rendertron](https://github.com/GoogleChrome/rendertron). - [Thal](https://medium.com/@e_mad_ehsan/getting-started-with-puppeteer-and-chrome-headless-for-web-scrapping-6bf5979dee3e "An article on medium") - Getting started with Puppeteer and Chrome Headless for Web Scraping. - [pupperender](https://github.com/LasaleFamine/pupperender) - Express middleware that checks the User-Agent header of incoming requests, and if it matches one of a configurable set of bots, render the page using Puppeteer. Useful for PWA rendering. @@ -36,5 +37,6 @@ const browser = await puppeteer.launch({ - [browserless](https://github.com/joelgriffith/browserless) - Headless Chrome as a service letting you execute Puppeteer scripts remotely. Provides a docker image with configuration for concurrency, launch arguments and more. ## Testing + - [angular-puppeteer-demo](https://github.com/Quramy/angular-puppeteer-demo) - Demo repository explaining how to use Puppeteer in Karma. - [mocha-headless-chrome](https://github.com/direct-adv-interfaces/mocha-headless-chrome) - Tool which runs client-side **mocha** tests in the command line through headless Chrome. diff --git a/lib/Coverage.js b/lib/Coverage.js index 00fb3e13221..120514548be 100644 --- a/lib/Coverage.js +++ b/lib/Coverage.js @@ -223,7 +223,7 @@ class CSSCoverage { ]); helper.removeEventListeners(this._eventListeners); - // aggregarte by styleSheetId + // aggregate by styleSheetId const styleSheetIdToCoverage = new Map(); for (const entry of ruleTrackingResponse.ruleUsage) { let ranges = styleSheetIdToCoverage.get(entry.styleSheetId); diff --git a/lib/EmulationManager.js b/lib/EmulationManager.js index 9d3341cbcab..eda4d9709eb 100644 --- a/lib/EmulationManager.js +++ b/lib/EmulationManager.js @@ -62,11 +62,11 @@ class EmulationManager { function injectedTouchEventsFunction() { const touchEvents = ['ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel']; // @ts-ignore - const recepients = [window.__proto__, document.__proto__]; + const recipients = [window.__proto__, document.__proto__]; for (let i = 0; i < touchEvents.length; ++i) { - for (let j = 0; j < recepients.length; ++j) { - if (!(touchEvents[i] in recepients[j])) { - Object.defineProperty(recepients[j], touchEvents[i], { + for (let j = 0; j < recipients.length; ++j) { + if (!(touchEvents[i] in recipients[j])) { + Object.defineProperty(recipients[j], touchEvents[i], { value: null, writable: true, configurable: true, enumerable: true }); } diff --git a/test/server/SimpleServer.js b/test/server/SimpleServer.js index 28ab0c8d109..09990283575 100644 --- a/test/server/SimpleServer.js +++ b/test/server/SimpleServer.js @@ -22,7 +22,7 @@ const path = require('path'); const mime = require('mime'); const WebSocketServer = require('ws').Server; -const fulfillSymbol = Symbol('fullfill callback'); +const fulfillSymbol = Symbol('fullfil callback'); const rejectSymbol = Symbol('reject callback'); class SimpleServer { diff --git a/test/test.js b/test/test.js index 971494d6680..750ed1c96e0 100644 --- a/test/test.js +++ b/test/test.js @@ -2957,7 +2957,7 @@ describe('Page', function() { expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); await page.setViewport(iPhone.viewport); expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true); - expect(await page.evaluate(dispatchTouch)).toBe('Recieved touch'); + expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); await page.setViewport({width: 100, height: 100}); expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); @@ -2965,11 +2965,11 @@ describe('Page', function() { let fulfill; const promise = new Promise(x => fulfill = x); window.ontouchstart = function(e) { - fulfill('Recieved touch'); + fulfill('Received touch'); }; window.dispatchEvent(new Event('touchstart')); - fulfill('Did not recieve touch'); + fulfill('Did not receive touch'); return promise; } diff --git a/utils/doclint/check_public_api/Documentation.js b/utils/doclint/check_public_api/Documentation.js index e5a88a5617f..4033802e9c0 100644 --- a/utils/doclint/check_public_api/Documentation.js +++ b/utils/doclint/check_public_api/Documentation.js @@ -16,7 +16,7 @@ class Documentation { /** - * @param {!Array} clasesArray + * @param {!Array} classesArray */ constructor(classesArray) { this.classesArray = classesArray; diff --git a/utils/doclint/check_public_api/MDBuilder.js b/utils/doclint/check_public_api/MDBuilder.js index b82516e3149..eda479e2a58 100644 --- a/utils/doclint/check_public_api/MDBuilder.js +++ b/utils/doclint/check_public_api/MDBuilder.js @@ -53,7 +53,7 @@ class MDOutline { currentClass.members.push(member); } else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) { member.args.push(element.firstChild.textContent); - } else if (element.matches('li') && element.firstChild.nodeType === Element.TEXT_NODE && element.firstChild.textContent.toLowerCase().startsWith('retur')) { + } else if (element.matches('li') && element.firstChild.nodeType === Element.TEXT_NODE && element.firstChild.textContent.toLowerCase().startsWith('return')) { member.hasReturn = true; const expectedText = 'returns: '; let actualText = element.firstChild.textContent; diff --git a/utils/doclint/preprocessor/index.js b/utils/doclint/preprocessor/index.js index 9815a713e81..061a10f9774 100644 --- a/utils/doclint/preprocessor/index.js +++ b/utils/doclint/preprocessor/index.js @@ -31,7 +31,7 @@ module.exports = function(sources) { commandEndRegex.lastIndex = commandStartRegex.lastIndex; const end = commandEndRegex.exec(text); if (!end) { - messages.push(Message.error(`Failed to find 'gen:stop' for comamnd ${start[0]}`)); + messages.push(Message.error(`Failed to find 'gen:stop' for command ${start[0]}`)); break; } const name = start[1];