From 982e77b9b3a34e115f85b0e8bd1236c3113141a5 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Thu, 25 Jul 2024 02:09:48 +0100 Subject: [PATCH] Demos: Add Rollup and Webpack example mixing ESM import and CJS require() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Gołębiowski-Owczarek --- .eslintrc.json | 5 +- .gitignore | 2 + demos/bundlers.js | 116 +++++++++++++++++++ demos/bundlers/Gruntfile.js | 39 +++++++ demos/bundlers/README.md | 17 +++ demos/bundlers/build.mjs | 139 +++++++++++++++++++++++ demos/bundlers/favicon.ico | 0 demos/bundlers/package.json | 17 +++ demos/bundlers/test/.eslintrc.json | 9 ++ demos/bundlers/test/import-default.js | 12 ++ demos/bundlers/test/import-indirect.js | 26 +++++ demos/bundlers/test/import-named.js | 12 ++ demos/bundlers/test/package.json | 3 + demos/bundlers/test/require-default.cjs | 12 ++ demos/bundlers/test/require-indirect.cjs | 26 +++++ demos/bundlers/test/src.js | 3 + demos/grunt-contrib-qunit.js | 17 ++- demos/karma-qunit.js | 17 +-- demos/nyc.js | 15 ++- demos/testem.js | 8 +- 20 files changed, 473 insertions(+), 22 deletions(-) create mode 100644 demos/bundlers.js create mode 100644 demos/bundlers/Gruntfile.js create mode 100644 demos/bundlers/README.md create mode 100644 demos/bundlers/build.mjs create mode 100644 demos/bundlers/favicon.ico create mode 100644 demos/bundlers/package.json create mode 100644 demos/bundlers/test/.eslintrc.json create mode 100644 demos/bundlers/test/import-default.js create mode 100644 demos/bundlers/test/import-indirect.js create mode 100644 demos/bundlers/test/import-named.js create mode 100644 demos/bundlers/test/package.json create mode 100644 demos/bundlers/test/require-default.cjs create mode 100644 demos/bundlers/test/require-indirect.cjs create mode 100644 demos/bundlers/test/src.js diff --git a/.eslintrc.json b/.eslintrc.json index f758a8d45..6b35b9e9b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "coverage/**", "demos/*/qunit/**", "demos/*/coverage/**", + "demos/*/tmp/**", "demos/*/package-lock.json", "docs/.jekyll-cache/**", "docs/_site/**", @@ -190,14 +191,14 @@ } }, { - "files": ["demos/**/*.js"], + "files": ["demos/**/*.js", "demos/**/*.mjs"], "env": { "browser": true, "es2017": true, "node": true }, "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2022 }, "rules": { "compat/compat": "off" diff --git a/.gitignore b/.gitignore index c5c2d9ded..df68f9894 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ /coverage/ /demos/*/package-lock.json /demos/*/node_modules/ +/demos/*/tmp/ +/demos/bundlers/test-*.html /docs/.jekyll-cache/ /docs/_site/ /docs/Gemfile.lock diff --git a/demos/bundlers.js b/demos/bundlers.js new file mode 100644 index 000000000..8f6292bfa --- /dev/null +++ b/demos/bundlers.js @@ -0,0 +1,116 @@ +const cp = require('child_process'); +const path = require('path'); +const DIR = path.join(__dirname, 'bundlers'); + +function normalize (str) { + return str + .replace(/^localhost:\d+/g, 'localhost:8000') + .replace(/\b\d+ms\b/g, '42ms'); +} + +QUnit.module('bundlers', { + before: async (assert) => { + assert.timeout(60_000); + + cp.execSync('npm install --no-audit --update-notifier=false', { cwd: DIR, encoding: 'utf8' }); + + await import('./bundlers/build.mjs'); + } +}); + +QUnit.test('test', assert => { + const expected = `Running "connect:all" (connect) task +Started connect web server on http://localhost:8000 + +Running "qunit:all" (qunit) task +Testing http://localhost:8000/tmp/test-import-default.es.html .OK +>> passed test "import default > example" +Testing http://localhost:8000/tmp/test-import-default.iife.html .OK +>> passed test "import default > example" +Testing http://localhost:8000/tmp/test-import-default.umd.html .OK +>> passed test "import default > example" +Testing http://localhost:8000/tmp/test-import-default.webpack.html .OK +>> passed test "import default > example" +Testing http://localhost:8000/tmp/test-import-indirect.es.html ......OK +>> passed test "import default > example" +>> passed test "import named { module,test } > example" +>> passed test "require default > example" +>> passed test "import-indirect > import-default" +>> passed test "import-indirect > import-named" +>> passed test "import-indirect > require-default" +Testing http://localhost:8000/tmp/test-import-indirect.iife.html ......OK +>> passed test "import default > example" +>> passed test "import named { module,test } > example" +>> passed test "require default > example" +>> passed test "import-indirect > import-default" +>> passed test "import-indirect > import-named" +>> passed test "import-indirect > require-default" +Testing http://localhost:8000/tmp/test-import-indirect.umd.html ......OK +>> passed test "import default > example" +>> passed test "import named { module,test } > example" +>> passed test "require default > example" +>> passed test "import-indirect > import-default" +>> passed test "import-indirect > import-named" +>> passed test "import-indirect > require-default" +Testing http://localhost:8000/tmp/test-import-indirect.webpack.html ......OK +>> passed test "import default > example" +>> passed test "import named { module,test } > example" +>> passed test "require default > example" +>> passed test "import-indirect > import-default" +>> passed test "import-indirect > import-named" +>> passed test "import-indirect > require-default" +Testing http://localhost:8000/tmp/test-import-named.es.html .OK +>> passed test "import named { module,test } > example" +Testing http://localhost:8000/tmp/test-import-named.iife.html .OK +>> passed test "import named { module,test } > example" +Testing http://localhost:8000/tmp/test-import-named.umd.html .OK +>> passed test "import named { module,test } > example" +Testing http://localhost:8000/tmp/test-import-named.webpack.html .OK +>> passed test "import named { module,test } > example" +Testing http://localhost:8000/tmp/test-require-default.es.html .OK +>> passed test "require default > example" +Testing http://localhost:8000/tmp/test-require-default.iife.html .OK +>> passed test "require default > example" +Testing http://localhost:8000/tmp/test-require-default.umd.html .OK +>> passed test "require default > example" +Testing http://localhost:8000/tmp/test-require-default.webpack.html .OK +>> passed test "require default > example" +Testing http://localhost:8000/tmp/test-require-indirect.es.html ......OK +>> passed test "import default > example" +>> passed test "import named { module,test } > example" +>> passed test "require default > example" +>> passed test "require-indirect > import-default" +>> passed test "require-indirect > import-named" +>> passed test "require-indirect > require-default" +Testing http://localhost:8000/tmp/test-require-indirect.iife.html ......OK +>> passed test "import default > example" +>> passed test "import named { module,test } > example" +>> passed test "require default > example" +>> passed test "require-indirect > import-default" +>> passed test "require-indirect > import-named" +>> passed test "require-indirect > require-default" +Testing http://localhost:8000/tmp/test-require-indirect.umd.html ......OK +>> passed test "import default > example" +>> passed test "import named { module,test } > example" +>> passed test "require default > example" +>> passed test "require-indirect > import-default" +>> passed test "require-indirect > import-named" +>> passed test "require-indirect > require-default" +Testing http://localhost:8000/tmp/test-require-indirect.webpack.html ......OK +>> passed test "import default > example" +>> passed test "import named { module,test } > example" +>> passed test "require default > example" +>> passed test "require-indirect > import-default" +>> passed test "require-indirect > import-named" +>> passed test "require-indirect > require-default" +>> 60 tests completed in 42ms, with 0 failed, 0 skipped, and 0 todo. + +Done.`; + + const actual = cp.execSync('node_modules/.bin/grunt test', { + cwd: DIR, + env: { PATH: process.env.PATH }, + encoding: 'utf8' + }); + assert.equal(normalize(actual).trim(), expected); +}); diff --git a/demos/bundlers/Gruntfile.js b/demos/bundlers/Gruntfile.js new file mode 100644 index 000000000..e679d83bc --- /dev/null +++ b/demos/bundlers/Gruntfile.js @@ -0,0 +1,39 @@ +/* eslint-env node */ +module.exports = function (grunt) { + grunt.loadNpmTasks('grunt-contrib-connect'); + grunt.loadNpmTasks('grunt-contrib-qunit'); + + grunt.initConfig({ + connect: { + all: { + options: { + useAvailablePort: true, + base: '.' + } + } + }, + qunit: { + options: { + }, + all: ['tmp/test-*.html'] + } + }); + + grunt.event.once('connect.all.listening', function (_host, port) { + grunt.config('qunit.options.httpBase', `http://localhost:${port}`); + // console.log(grunt.config()); // DEBUG + }); + + let results = []; + grunt.event.on('qunit.on.testEnd', function (test) { + results.push( + `>> ${test.status} test "${test.fullName.join(' > ')}"` + ); + }); + grunt.event.on('qunit.on.runEnd', function () { + grunt.log.writeln(results.join('\n')); + results = []; + }); + + grunt.registerTask('test', ['connect', 'qunit']); +}; diff --git a/demos/bundlers/README.md b/demos/bundlers/README.md new file mode 100644 index 000000000..16fe22b74 --- /dev/null +++ b/demos/bundlers/README.md @@ -0,0 +1,17 @@ +# QUnit ♥️ Rollup & Webpack + +See also and . + +```bash +npm run build +npm test +``` + +``` +Running "qunit:all" (qunit) task +Testing http://localhost:8000/test.html .OK +>> passed test "example" +>> 1 test completed in 0ms, with 0 failed, 0 skipped, and 0 todo. + +Done. +``` diff --git a/demos/bundlers/build.mjs b/demos/bundlers/build.mjs new file mode 100644 index 000000000..a14bc4e54 --- /dev/null +++ b/demos/bundlers/build.mjs @@ -0,0 +1,139 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + +import { rollup } from 'rollup'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import webpack from 'webpack'; + +const dirname = path.dirname(url.fileURLToPath(import.meta.url)); +const tmpDir = path.join(dirname, 'tmp'); + +const inputs = [ + `${dirname}/test/import-default.js`, + `${dirname}/test/import-named.js`, + `${dirname}/test/import-indirect.js`, + `${dirname}/test/require-default.cjs`, + `${dirname}/test/require-indirect.cjs` +]; + +const htmlTemplate = ` + + +{{title}} + +{{scriptTag}} + + +
+ + +`; + +// Rollup configuration +const rollupOutputs = [ + { + dir: tmpDir, + entryFileNames: '[name].[format].js', + format: 'es' + }, + { + dir: tmpDir, + entryFileNames: '[name].[format].js', + format: 'cjs' + }, + { + dir: tmpDir, + entryFileNames: '[name].[format].js', + format: 'iife' + }, + { + dir: tmpDir, + entryFileNames: '[name].[format].js', + format: 'umd', + name: 'UNUSED' + } +]; +async function * buildRollup () { + const plugins = [commonjs(), resolve()]; + + for (const input of inputs) { + const bundle = await rollup({ + input, + plugins, + // Ignore "output.name" warning for require-default.iife.js + onwarn: () => {} + }); + + for (const outputOptions of rollupOutputs) { + const { output } = await bundle.write(outputOptions); + const fileName = output[0].fileName; + yield fileName; + } + } +} + +// https://webpack.js.org/api/node/#webpack +async function * buildWebpack () { + for (const input of inputs) { + const config = { + entry: input, + output: { + filename: path.basename(input).replace(/\.(cjs|js)$/, '.webpack.js'), + path: tmpDir + } + }; + await new Promise((resolve, reject) => { + webpack(config, (err, stats) => { + if (err || stats.hasErrors()) { + reject(err); + return; + } + resolve(); + }); + }); + yield config.output.filename; + } +} + +await (async function main () { + // Clean up + fs.rmSync(tmpDir, { force: true, recursive: true }); + + const gRollup = buildRollup(); + const gWebpack = buildWebpack(); + + for await (const fileName of gRollup) { + console.log('... built ' + fileName); + + if (!fileName.endsWith('.cjs.js')) { + const html = htmlTemplate + .replace('{{title}}', fileName) + .replace('{{scriptTag}}', ( + fileName.endsWith('.es.js') + ? `` + : `` + )); + + fs.writeFileSync( + `${tmpDir}/test-${fileName.replace('.js', '')}.html`, + html + ); + } + } + for await (const fileName of gWebpack) { + console.log('... built ' + fileName); + + const html = htmlTemplate + .replace('{{title}}', fileName) + .replace('{{scriptTag}}', ( + `` + )); + + fs.writeFileSync( + `${tmpDir}/test-${fileName.replace('.js', '')}.html`, + html + ); + } +}()); diff --git a/demos/bundlers/favicon.ico b/demos/bundlers/favicon.ico new file mode 100644 index 000000000..e69de29bb diff --git a/demos/bundlers/package.json b/demos/bundlers/package.json new file mode 100644 index 000000000..d9c9e28b8 --- /dev/null +++ b/demos/bundlers/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "devDependencies": { + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "grunt": "1.6.1", + "grunt-contrib-connect": "^5.0.0", + "grunt-contrib-qunit": "10.1.1", + "qunit": "file:../..", + "rollup": "^4.18.0", + "webpack": "^5.92.0" + }, + "scripts": { + "build": "node build.mjs", + "test": "grunt test" + } +} diff --git a/demos/bundlers/test/.eslintrc.json b/demos/bundlers/test/.eslintrc.json new file mode 100644 index 000000000..74ae1d3a3 --- /dev/null +++ b/demos/bundlers/test/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "env": { + "es2020": true + }, + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + } +} diff --git a/demos/bundlers/test/import-default.js b/demos/bundlers/test/import-default.js new file mode 100644 index 000000000..05c11e773 --- /dev/null +++ b/demos/bundlers/test/import-default.js @@ -0,0 +1,12 @@ +import QUnit from 'qunit'; +import { add } from './src.js'; + +(globalThis.TEST_OBJECTS || (globalThis.TEST_OBJECTS = {})).i_d = QUnit; + +QUnit._hello_i_d = QUnit.assert._hello_i_d = 'import-default'; + +QUnit.module('import default', function () { + QUnit.test('example', function (assert) { + assert.equal(add(2, 3), 5); + }); +}); diff --git a/demos/bundlers/test/import-indirect.js b/demos/bundlers/test/import-indirect.js new file mode 100644 index 000000000..7da35c35e --- /dev/null +++ b/demos/bundlers/test/import-indirect.js @@ -0,0 +1,26 @@ +/* global TEST_OBJECTS */ +import './import-default.js'; +import './import-named.js'; +import './require-default.cjs'; + +import QUnit from 'qunit'; + +QUnit.module('import-indirect'); + +QUnit.test('import-default', function (assert) { + assert.strictEqual(TEST_OBJECTS.i_d, QUnit, 'identity'); + assert.strictEqual(QUnit._hello_i_d, 'import-default', 'extend QUnit'); + assert.strictEqual(assert._hello_i_d, 'import-default', 'extend assert'); +}); + +QUnit.test('import-named', function (assert) { + assert.strictEqual(TEST_OBJECTS.i_n, QUnit, 'identity'); + assert.strictEqual(QUnit._hello_i_n, 'import-named', 'extend QUnit'); + assert.strictEqual(assert._hello_i_n, 'import-named', 'extend assert'); +}); + +QUnit.test('require-default', function (assert) { + assert.strictEqual(TEST_OBJECTS.r_d, QUnit, 'identity'); + assert.strictEqual(QUnit._hello_r_d, 'require-default', 'extend QUnit'); + assert.strictEqual(assert._hello_r_d, 'require-default', 'extend assert'); +}); diff --git a/demos/bundlers/test/import-named.js b/demos/bundlers/test/import-named.js new file mode 100644 index 000000000..ff8cfc3cb --- /dev/null +++ b/demos/bundlers/test/import-named.js @@ -0,0 +1,12 @@ +import { QUnit, assert, module, test } from 'qunit'; +import { add } from './src.js'; + +(globalThis.TEST_OBJECTS || (globalThis.TEST_OBJECTS = {})).i_n = QUnit; + +QUnit._hello_i_n = assert._hello_i_n = 'import-named'; + +module('import named { module,test }', function () { + test('example', function (assert) { + assert.equal(add(2, 3), 5); + }); +}); diff --git a/demos/bundlers/test/package.json b/demos/bundlers/test/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/demos/bundlers/test/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/demos/bundlers/test/require-default.cjs b/demos/bundlers/test/require-default.cjs new file mode 100644 index 000000000..d09b40c21 --- /dev/null +++ b/demos/bundlers/test/require-default.cjs @@ -0,0 +1,12 @@ +const QUnit = require('qunit'); +const { add } = require('./src.js'); + +(globalThis.TEST_OBJECTS || (globalThis.TEST_OBJECTS = {})).r_d = QUnit; + +QUnit._hello_r_d = QUnit.assert._hello_r_d = 'require-default'; + +QUnit.module('require default', function () { + QUnit.test('example', function (assert) { + assert.equal(add(2, 3), 5); + }); +}); diff --git a/demos/bundlers/test/require-indirect.cjs b/demos/bundlers/test/require-indirect.cjs new file mode 100644 index 000000000..85aa74873 --- /dev/null +++ b/demos/bundlers/test/require-indirect.cjs @@ -0,0 +1,26 @@ +/* global TEST_OBJECTS */ +const QUnit = require('qunit'); + +require('./import-default.js'); +require('./import-named.js'); +require('./require-default.cjs'); + +QUnit.module('require-indirect'); + +QUnit.test('import-default', function (assert) { + assert.strictEqual(TEST_OBJECTS.i_d, QUnit, 'identity'); + assert.strictEqual(QUnit._hello_i_d, 'import-default', 'extend QUnit'); + assert.strictEqual(assert._hello_i_d, 'import-default', 'extend assert'); +}); + +QUnit.test('import-named', function (assert) { + assert.strictEqual(TEST_OBJECTS.i_n, QUnit, 'identity'); + assert.strictEqual(QUnit._hello_i_n, 'import-named', 'extend QUnit'); + assert.strictEqual(assert._hello_i_n, 'import-named', 'extend assert'); +}); + +QUnit.test('require-default', function (assert) { + assert.strictEqual(TEST_OBJECTS.r_d, QUnit, 'identity'); + assert.strictEqual(QUnit._hello_r_d, 'require-default', 'extend QUnit'); + assert.strictEqual(assert._hello_r_d, 'require-default', 'extend assert'); +}); diff --git a/demos/bundlers/test/src.js b/demos/bundlers/test/src.js new file mode 100644 index 000000000..3b9c0b421 --- /dev/null +++ b/demos/bundlers/test/src.js @@ -0,0 +1,3 @@ +export function add (a, b) { + return a + b; +} diff --git a/demos/grunt-contrib-qunit.js b/demos/grunt-contrib-qunit.js index f92cf691e..9e441d306 100644 --- a/demos/grunt-contrib-qunit.js +++ b/demos/grunt-contrib-qunit.js @@ -2,10 +2,14 @@ const cp = require('child_process'); const path = require('path'); const DIR = path.join(__dirname, 'grunt-contrib-qunit'); +// Fast re-runs +process.env.npm_config_prefer_offline = 'true'; +process.env.npm_config_update_notifier = 'false'; +process.env.npm_config_audit = 'false'; + QUnit.module('grunt-contrib-qunit', { before: () => { - // Let this be quick for re-runs - cp.execSync('npm install --prefer-offline --no-audit --update-notifier=false', { cwd: DIR, encoding: 'utf8' }); + cp.execSync('npm install', { cwd: DIR, encoding: 'utf8' }); } }); @@ -29,15 +33,10 @@ QUnit.test.each('failing tests', { >> at file:fail-uncaught.html:16`] }, (assert, [command, expected]) => { try { + // This will use env CI, CHROMIUM_FLAGS, and PUPPETEER_DOWNLOAD_PATH const ret = cp.execSync('node_modules/.bin/grunt qunit:' + command, { cwd: DIR, - encoding: 'utf8', - env: { - CHROMIUM_FLAGS: process.env.CHROMIUM_FLAGS, - CI: process.env.CI, - PATH: process.env.PATH, - PUPPETEER_DOWNLOAD_PATH: process.env.PUPPETEER_DOWNLOAD_PATH - } + encoding: 'utf8' }); assert.equal(ret, null); } catch (e) { diff --git a/demos/karma-qunit.js b/demos/karma-qunit.js index a9ebf5edf..9e8504b59 100644 --- a/demos/karma-qunit.js +++ b/demos/karma-qunit.js @@ -11,9 +11,14 @@ function normalize (str) { .replace(/^\s+/gm, ' '); } +// Fast re-runs +process.env.npm_config_prefer_offline = 'true'; +process.env.npm_config_update_notifier = 'false'; +process.env.npm_config_audit = 'false'; + QUnit.module('karma-qunit', { before: () => { - cp.execSync('npm install --prefer-offline --no-audit --omit=dev --legacy-peer-deps --update-notifier=false', { cwd: DIR, encoding: 'utf8' }); + cp.execSync('npm install --omit=dev --legacy-peer-deps', { cwd: DIR, encoding: 'utf8' }); } }); @@ -40,11 +45,10 @@ Firefox: Executed 1 of 1 SUCCESS`, { KARMA_QUNIT_CONFIG: '1' } try { ret = cp.execSync('npm test', { cwd: DIR, - env: { - PATH: process.env.PATH, + env: Object.assign({}, process.env, { KARMA_FILES: file, ...env - }, + }), encoding: 'utf8' }); } catch (e) { @@ -73,10 +77,9 @@ Firefox example FAILED try { const ret = cp.execSync('npm test', { cwd: DIR, - env: { - PATH: process.env.PATH, + env: Object.assign({}, process.env, { KARMA_FILES: file - }, + }), encoding: 'utf8' }); assert.equal(ret, null); diff --git a/demos/nyc.js b/demos/nyc.js index 834ad996b..4aa29dd50 100644 --- a/demos/nyc.js +++ b/demos/nyc.js @@ -2,9 +2,14 @@ const cp = require('child_process'); const path = require('path'); const DIR = path.join(__dirname, 'nyc'); +// Fast re-runs +process.env.npm_config_prefer_offline = 'true'; +process.env.npm_config_update_notifier = 'false'; +process.env.npm_config_audit = 'false'; + QUnit.module('nyc', { before: () => { - cp.execSync('npm install --prefer-offline --no-audit --update-notifier=false', { cwd: DIR, encoding: 'utf8' }); + cp.execSync('npm install', { cwd: DIR, encoding: 'utf8' }); } }); @@ -29,6 +34,12 @@ All files | 85.71 | 100 | 50 | 85.71 | --------------|---------|----------|---------|---------|------------------- `.trim(); - const actual = cp.execSync('npm test', { cwd: DIR, env: { PATH: process.env.PATH }, encoding: 'utf8' }); + const actual = cp.execSync('npm test', { + cwd: DIR, + env: Object.assign({}, process.env, { + FORCE_COLOR: '0' + }) + encoding: 'utf8' + }); assert.pushResult({ result: actual.includes(expected), actual, expected }); }); diff --git a/demos/testem.js b/demos/testem.js index 2b1c971bf..4ca4d4365 100644 --- a/demos/testem.js +++ b/demos/testem.js @@ -9,10 +9,14 @@ function normalize (str) { .replace(/(\d+ ms)/g, '0 ms'); } +// Fast re-runs +process.env.npm_config_prefer_offline = 'true'; +process.env.npm_config_update_notifier = 'false'; +process.env.npm_config_audit = 'false'; + QUnit.module('testem', { before: () => { - // Let this be quick for re-runs - cp.execSync('npm install --prefer-offline --no-audit --update-notifier=false', { cwd: DIR, encoding: 'utf8' }); + cp.execSync('npm install', { cwd: DIR, encoding: 'utf8' }); } });