diff --git a/packages/run/CHANGELOG.md b/packages/run/CHANGELOG.md new file mode 100644 index 000000000..87b1183a0 --- /dev/null +++ b/packages/run/CHANGELOG.md @@ -0,0 +1,17 @@ +# @rollup/plugin-run Change Log + +## 1.1.0 + +- Allow arguments and options to be passed to `child_process.fork` + +## 1.0.2 + +- Warn if Rollup version is too low + +## 1.0.1 + +- Handle output files with different names from input files + +## 1.0.0 + +- First release diff --git a/packages/run/README.md b/packages/run/README.md new file mode 100644 index 000000000..c43aeafcf --- /dev/null +++ b/packages/run/README.md @@ -0,0 +1,102 @@ +[npm]: https://img.shields.io/npm/v/@rollup/plugin-run +[npm-url]: https://www.npmjs.com/package/@rollup/plugin-run +[size]: https://packagephobia.now.sh/badge?p=@rollup/plugin-run +[size-url]: https://packagephobia.now.sh/result?p=@rollup/plugin-run + +[![npm][npm]][npm-url] +[![size][size]][size-url] +[![libera manifesto](https://img.shields.io/badge/libera-manifesto-lightgrey.svg)](https://liberamanifesto.com) + +# @rollup/plugin-run + +🍣 A Rollup plugin which runs your bundles in Node once they're built. + +Using this plugin gives much faster results compared to what you would do with [nodemon](https://nodemon.io/). + +## Install + +Using npm: + +```console +npm install @rollup/plugin-run --save-dev +``` + +## Usage + +Create a `rollup.config.js` [configuration file](https://www.rollupjs.org/guide/en/#configuration-files) and import the plugin: + +```js +import run from "@rollup/plugin-run"; + +export default { + input: "src/index.js", + output: { + file: "dist/index.js", + format: "cjs" + }, + plugins: [run()] +}; +``` + +Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#command-line-reference) or the [API](https://www.rollupjs.org/guide/en/#javascript-api). If the build produces any errors, the plugin will write a 'alias' character to stderr, which should be audible on most systems. + +The plugin `forks` a child process with the generated file, every time the bundle is rebuilt (after first closing the previous process, if it's not the first run). + +_Note: This plugin works with Rollup's code-splitting if you're using dynamic `import(...)` — the only constraint is that you have a single entry point specified in the config._ + +## Options + +This plugin supports pass through option available for [child_process.fork(...)](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options). + +Example: + +Debugging with sourcemaps using [source-map-support](https://www.npmjs.com/package/source-map-support): + +```diff +// rollup.config.js +import run from '@rollup/plugin-run'; + +export default { + input: 'src/index.js', + output: { + file: 'dist/index.js', + format: 'cjs', ++ sourcemap: true + }, + plugins: [ +- run() ++ run({ ++ execArgv: ['-r', 'source-map-support/register'] ++ }) + ] +}; +``` + +## Practical Example + +The feature is usually intended for development use, you may prefer to only include it when Rollup is being run in watch mode: + +```diff +// rollup.config.js +import run from '@rollup/plugin-run'; + ++const dev = process.env.ROLLUP_WATCH === 'true'; + +export default { + input: 'src/index.js', + output: { + file: 'dist/index.js', + format: 'cjs' + }, + plugins: [ +- run() ++ dev && run() + ] +}; +``` + +## Meta + +[CONTRIBUTING](/.github/CONTRIBUTING.md) + +[LICENSE (MIT)](/LICENSE) diff --git a/packages/run/lib/index.js b/packages/run/lib/index.js new file mode 100644 index 000000000..ec2befc60 --- /dev/null +++ b/packages/run/lib/index.js @@ -0,0 +1,69 @@ +const path = require('path'); +const childProcess = require('child_process'); + +module.exports = (opts = {}) => { + let input; + let proc; + + const args = opts.args || []; + const forkOptions = opts.options || opts; + delete forkOptions.args; + + return { + name: 'run', + + // eslint-disable-next-line no-shadow + options(opts) { + let inputs = opts.input; + + if (typeof inputs === 'string') { + inputs = [inputs]; + } + + if (typeof inputs === 'object') { + inputs = Object.values(inputs); + } + + if (inputs.length > 1) { + throw new Error(`@rollup/plugin-run only works with a single entry point`); + } + + input = path.resolve(inputs[0]); + }, + + generateBundle(outputOptions, bundle, isWrite) { + if (!isWrite) { + this.error(`@rollup/plugin-run currently only works with bundles that are written to disk`); + } + + const dir = outputOptions.dir || path.dirname(outputOptions.file); + + let dest; + + for (const fileName in bundle) { + if (Object.prototype.hasOwnProperty.call(bundle, fileName)) { + const chunk = bundle[fileName]; + + if (!('isEntry' in chunk)) { + this.error(`@rollup/plugin-run requires Rollup 0.65 or higher`); + } + + // eslint-disable-next-line no-continue + if (!chunk.isEntry) continue; + + if (chunk.modules[input]) { + dest = path.join(dir, fileName); + break; + } + } + } + + if (dest) { + if (proc) proc.kill(); + proc = childProcess.fork(dest, args, forkOptions); + } else { + this.error(`@rollup/plugin-run could not find output chunk`); + } + } + }; +}; diff --git a/packages/run/package.json b/packages/run/package.json new file mode 100644 index 000000000..e1392ccb6 --- /dev/null +++ b/packages/run/package.json @@ -0,0 +1,48 @@ +{ + "name": "@rollup/plugin-run", + "version": "1.1.0", + "publishConfig": { + "access": "public" + }, + "description": "Run your bundle after you've built it", + "license": "MIT", + "repository": "rollup/plugins", + "author": "Rich Harris", + "homepage": "https://github.com/rollup/plugins/packages/run/#readme", + "bugs": "https://github.com/rollup/plugins/issues", + "main": "lib/index.js", + "scripts": { + "ci:coverage": "nyc pnpm run test && nyc report --reporter=text-lcov > coverage.lcov", + "ci:lint": "pnpm run lint && pnpm run security", + "ci:lint:commits": "commitlint --from=${CIRCLE_BRANCH} --to=${CIRCLE_SHA1}", + "ci:test": "pnpm run test -- --verbose", + "lint": "pnpm run lint:js && pnpm run lint:docs && pnpm run lint:package", + "lint:docs": "prettier --single-quote --write README.md", + "lint:js": "eslint --fix --cache lib test", + "lint:package": "prettier --write package.json --plugin=prettier-plugin-package", + "prepublishOnly": "pnpm run lint && pnpm run test", + "security": "echo 'pnpm needs `npm audit` support'", + "test": "ava" + }, + "files": [ + "lib" + ], + "keywords": [ + "rollup", + "plugin", + "run" + ], + "peerDependencies": { + "rollup": "^1.20.0" + }, + "devDependencies": { + "del": "^5.1.0", + "rollup": "^1.20.0", + "sinon": "^7.5.0" + }, + "ava": { + "files": [ + "!**/fixtures/**" + ] + } +} diff --git a/packages/run/test/fixtures/change-detect-input.js b/packages/run/test/fixtures/change-detect-input.js new file mode 100644 index 000000000..83a20c5af --- /dev/null +++ b/packages/run/test/fixtures/change-detect-input.js @@ -0,0 +1 @@ +export const Greeting = () => 'Hola'; // eslint-disable-line \ No newline at end of file diff --git a/packages/run/test/fixtures/input.js b/packages/run/test/fixtures/input.js new file mode 100644 index 000000000..0f5dc63f6 --- /dev/null +++ b/packages/run/test/fixtures/input.js @@ -0,0 +1 @@ +export const Greeting = () => 'Hello'; // eslint-disable-line diff --git a/packages/run/test/test.js b/packages/run/test/test.js new file mode 100644 index 000000000..a20e1441b --- /dev/null +++ b/packages/run/test/test.js @@ -0,0 +1,88 @@ +const fs = require('fs'); +const { EventEmitter } = require('events'); + +const { join } = require('path'); + +const childProcess = require('child_process'); + +const writeFile = require('util').promisify(fs.writeFile); + +const del = require('del'); +const { rollup } = require('rollup'); +const test = require('ava'); +const sinon = require('sinon'); + +const run = require('../lib/'); + +const cwd = join(__dirname, 'fixtures/'); +const file = join(cwd, 'output/bundle.js'); +const input = join(cwd, 'input.js'); + +process.chdir(cwd); + +const outputOptions = { file, format: 'cjs' }; + +let mockChildProcess; +test.before(() => { + mockChildProcess = sinon + .stub(childProcess, ['fork']) + .returns({ ...new EventEmitter(), kill: sinon.fake() }); +}); + +test('builds the bundle and forks a child process', async (t) => { + const bundle = await rollup({ + input, + plugins: [run()] + }); + await bundle.write(outputOptions); + t.true(mockChildProcess.calledWithExactly(outputOptions.file, [], {})); +}); + +test('allows pass-through options for child_process.fork', async (t) => { + const forkOptions = { + cwd, + detached: false, + silent: false + }; + const bundle = await rollup({ + input, + plugins: [run(forkOptions)] + }); + + await bundle.write(outputOptions); + t.true(mockChildProcess.calledWithExactly(outputOptions.file, [], forkOptions)); +}); + +test('throws an error when bundle is not written to disk', async (t) => { + const bundle = await rollup({ + input, + plugins: [run()] + }); + await t.throwsAsync( + async () => { + await bundle.generate(outputOptions); + }, + { + instanceOf: Error, + message: '@rollup/plugin-run currently only works with bundles that are written to disk' + } + ); +}); + +test('detects changes - forks a new child process and kills older process', async (t) => { + // eslint-disable-next-line no-shadow + const input = join(cwd, 'change-detect-input.js'); + const bundle = await rollup({ + input, + plugins: [run()] + }); + await bundle.write(outputOptions); + await writeFile(input, "export const Greeting = () => 'Hola'; // eslint-disable-line"); + await bundle.write(outputOptions); + t.true(mockChildProcess.calledWithExactly(outputOptions.file, [], {})); + t.is(mockChildProcess().kill.callCount, 1); +}); + +test.after(async () => { + await del(['output']); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ddaf334f..3d88c1b50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,7 @@ importers: pnpm: 4.3.0 prettier: 1.19.1 prettier-plugin-package: 0.3.1_prettier@1.19.1 - rollup: 1.27.2 + rollup: 1.27.3 tslib: 1.10.0 tslint: 5.20.1_typescript@3.7.2 typescript: 3.7.2 @@ -165,6 +165,15 @@ importers: rollup-pluginutils: ^2.6.0 source-map: ^0.7.3 typescript: ^3.4.3 + packages/run: + devDependencies: + del: 5.1.0 + rollup: 1.27.3 + sinon: 7.5.0 + specifiers: + del: ^5.1.0 + rollup: ^1.20.0 + sinon: ^7.5.0 packages/strip: dependencies: estree-walker: 0.6.1 @@ -1012,6 +1021,31 @@ packages: node: '>=6' resolution: integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + /@sinonjs/commons/1.6.0: + dependencies: + type-detect: 4.0.8 + dev: true + resolution: + integrity: sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg== + /@sinonjs/formatio/3.2.2: + dependencies: + '@sinonjs/commons': 1.6.0 + '@sinonjs/samsam': 3.3.3 + dev: true + resolution: + integrity: sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ== + /@sinonjs/samsam/3.3.3: + dependencies: + '@sinonjs/commons': 1.6.0 + array-from: 2.1.1 + lodash: 4.17.15 + dev: true + resolution: + integrity: sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== + /@sinonjs/text-encoding/0.7.1: + dev: true + resolution: + integrity: sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== /@szmarczak/http-timer/1.1.2: dependencies: defer-to-connect: 1.1.0 @@ -1036,7 +1070,7 @@ packages: dependencies: '@types/events': 3.0.0 '@types/minimatch': 3.0.3 - '@types/node': 12.12.8 + '@types/node': 12.12.11 dev: true resolution: integrity: sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== @@ -1244,6 +1278,10 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + /array-from/2.1.1: + dev: true + resolution: + integrity: sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= /array-includes/3.0.3: dependencies: define-properties: 1.1.3 @@ -2060,6 +2098,12 @@ packages: node: '>=8' resolution: integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA== + /diff/3.5.0: + dev: true + engines: + node: '>=0.3.1' + resolution: + integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== /diff/4.0.1: dev: true engines: @@ -2181,7 +2225,7 @@ packages: dependencies: is-callable: 1.1.4 is-date-object: 1.0.1 - is-symbol: 1.0.2 + is-symbol: 1.0.3 dev: true engines: node: '>= 0.4' @@ -3171,14 +3215,14 @@ packages: node: '>=8' resolution: integrity: sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - /is-symbol/1.0.2: + /is-symbol/1.0.3: dependencies: has-symbols: 1.0.1 dev: true engines: node: '>= 0.4' resolution: - integrity: sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + integrity: sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== /is-typedarray/1.0.0: dev: true resolution: @@ -3195,6 +3239,10 @@ packages: dev: true resolution: integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + /isarray/0.0.1: + dev: true + resolution: + integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= /isarray/1.0.0: dev: true resolution: @@ -3326,6 +3374,10 @@ packages: hasBin: true resolution: integrity: sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== + /just-extend/4.0.2: + dev: true + resolution: + integrity: sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw== /keyv/3.1.0: dependencies: json-buffer: 3.0.0 @@ -3544,6 +3596,10 @@ packages: node: '>=4' resolution: integrity: sha1-iDKP19HOeTiykoN0bwsbwSayRwg= + /lolex/4.2.0: + dev: true + resolution: + integrity: sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== /loose-envify/1.4.0: dependencies: js-tokens: 4.0.0 @@ -3784,6 +3840,16 @@ packages: dev: true resolution: integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + /nise/1.5.2: + dependencies: + '@sinonjs/formatio': 3.2.2 + '@sinonjs/text-encoding': 0.7.1 + just-extend: 4.0.2 + lolex: 4.2.0 + path-to-regexp: 1.8.0 + dev: true + resolution: + integrity: sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA== /node-noop/1.0.0: dev: false resolution: @@ -4198,6 +4264,12 @@ packages: dev: true resolution: integrity: sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + /path-to-regexp/1.8.0: + dependencies: + isarray: 0.0.1 + dev: true + resolution: + integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== /path-type/2.0.0: dependencies: pify: 2.3.0 @@ -4738,6 +4810,15 @@ packages: hasBin: true resolution: integrity: sha512-sD3iyd0zlvgK1S3MmICi6F/Y+R/QWY5XxzsTGN4pAd+nCasDUizmAhgq2hdh1t2eLux974NHU2TW41fhuGPv+Q== + /rollup/1.27.3: + dependencies: + '@types/estree': 0.0.39 + '@types/node': 12.12.11 + acorn: 7.1.0 + dev: true + hasBin: true + resolution: + integrity: sha512-79AEh4m5NPCz97GTuIoXpSFIMPyk2AiqVQp040baSRPXk/I4YMGt5/CR9GX5oEYEkxwBZoWLheaS1/w/FidfJw== /run-async/2.3.0: dependencies: is-promise: 2.1.0 @@ -4841,6 +4922,18 @@ packages: dev: true resolution: integrity: sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + /sinon/7.5.0: + dependencies: + '@sinonjs/commons': 1.6.0 + '@sinonjs/formatio': 3.2.2 + '@sinonjs/samsam': 3.3.3 + diff: 3.5.0 + lolex: 4.2.0 + nise: 1.5.2 + supports-color: 5.5.0 + dev: true + resolution: + integrity: sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q== /slash/3.0.0: engines: node: '>=8' @@ -5269,6 +5362,12 @@ packages: node: '>= 0.8.0' resolution: integrity: sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + /type-detect/4.0.8: + dev: true + engines: + node: '>=4' + resolution: + integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== /type-fest/0.3.1: dev: true engines: