diff --git a/package.json b/package.json index 6e6884f2ca2..198855b6ca1 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "get-port": "^4.2.0", "globby": "^9.1.0", "husky": "^1.3.1", - "jest": "^24.5.0", + "jest": "24.7.1", "lerna": "2.9.1", "lerna-changelog": "~0.8.2", "lint-staged": "^8.0.4", diff --git a/packages/babel-plugin-named-asset-import/package.json b/packages/babel-plugin-named-asset-import/package.json index a6c9ccc7c57..e78d36b5ff4 100644 --- a/packages/babel-plugin-named-asset-import/package.json +++ b/packages/babel-plugin-named-asset-import/package.json @@ -15,8 +15,8 @@ "@babel/core": "^7.1.0" }, "devDependencies": { - "babel-plugin-tester": "^5.5.1", - "jest": "^24.5.0" + "babel-plugin-tester": "^6.0.1", + "jest": "24.7.1" }, "scripts": { "test": "jest" diff --git a/packages/confusing-browser-globals/package.json b/packages/confusing-browser-globals/package.json index 54edd9c8893..8a01fe564e2 100644 --- a/packages/confusing-browser-globals/package.json +++ b/packages/confusing-browser-globals/package.json @@ -16,6 +16,6 @@ "index.js" ], "devDependencies": { - "jest": "24.5.0" + "jest": "24.7.1" } } diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 1c0f172069f..a6412d7feea 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -74,7 +74,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "jest": "^24.5.0" + "jest": "24.7.1" }, "scripts": { "test": "cross-env FORCE_COLOR=true jest" diff --git a/packages/react-error-overlay/package.json b/packages/react-error-overlay/package.json index 879f3992602..89dc73d2f2b 100644 --- a/packages/react-error-overlay/package.json +++ b/packages/react-error-overlay/package.json @@ -33,9 +33,8 @@ "@babel/code-frame": "7.0.0", "@babel/core": "7.3.4", "anser": "1.4.8", - "babel-core": "7.0.0-bridge.0", "babel-eslint": "10.0.1", - "babel-jest": "24.5.0", + "babel-jest": "24.7.1", "babel-loader": "8.0.5", "babel-preset-react-app": "^7.0.2", "chalk": "^2.4.2", @@ -49,7 +48,7 @@ "eslint-plugin-react": "7.12.4", "flow-bin": "^0.63.1", "html-entities": "1.2.1", - "jest": "24.5.0", + "jest": "24.7.1", "jest-fetch-mock": "2.1.1", "object-assign": "4.1.1", "promise": "8.0.2", diff --git a/packages/react-scripts/fixtures/kitchensink/.template.dependencies.json b/packages/react-scripts/fixtures/kitchensink/.template.dependencies.json index b892bb90a42..05b7ba5571a 100644 --- a/packages/react-scripts/fixtures/kitchensink/.template.dependencies.json +++ b/packages/react-scripts/fixtures/kitchensink/.template.dependencies.json @@ -1,7 +1,7 @@ { "dependencies": { "bootstrap": "4.1.1", - "jest": "24.5.0", + "jest": "24.7.1", "node-sass": "4.8.3", "normalize.css": "7.0.0", "prop-types": "15.5.6", diff --git a/packages/react-scripts/fixtures/kitchensink/README.md b/packages/react-scripts/fixtures/kitchensink/README.md index 5a3edd13e83..9f7a3f9fb6c 100644 --- a/packages/react-scripts/fixtures/kitchensink/README.md +++ b/packages/react-scripts/fixtures/kitchensink/README.md @@ -8,7 +8,7 @@ Tests are automatically run by the CI tools. In order to run them locally, without having to manually install and configure everything, the `yarn e2e:docker` CLI command can be used. This is a simple script that runs a **Docker** container, where the node version, git branch to clone, test suite, and whether to run it with `yarn` or `npm` can be chosen. -Simply run `yarn e2e:docker -- --help` to get additional info. +Simply run `yarn e2e:docker --help` to get additional info. If you need guidance installing **Docker**, you should follow their [official docs](https://docs.docker.com/engine/installation/). diff --git a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js index 79de16706dc..f86a09d52f6 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/env.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/env.test.js @@ -9,8 +9,15 @@ import initDOM from './initDOM'; describe('Integration', () => { describe('Environment variables', () => { + let doc; + + afterEach(() => { + doc && doc.defaultView.close(); + doc = undefined; + }); + it('file env variables', async () => { - const doc = await initDOM('file-env-variables'); + doc = await initDOM('file-env-variables'); expect( doc.getElementById('feature-file-env-original-1').textContent @@ -34,18 +41,16 @@ describe('Integration', () => { 'x-from-development-env' ); } - doc.defaultView.close(); }); it('NODE_PATH', async () => { - const doc = await initDOM('node-path'); + doc = await initDOM('node-path'); expect(doc.getElementById('feature-node-path').childElementCount).toBe(4); - doc.defaultView.close(); }); it('PUBLIC_URL', async () => { - const doc = await initDOM('public-url'); + doc = await initDOM('public-url'); const prefix = process.env.NODE_ENV === 'development' @@ -57,20 +62,18 @@ describe('Integration', () => { expect( doc.querySelector('head link[rel="shortcut icon"]').getAttribute('href') ).toBe(`${prefix}/favicon.ico`); - doc.defaultView.close(); }); it('shell env variables', async () => { - const doc = await initDOM('shell-env-variables'); + doc = await initDOM('shell-env-variables'); expect( doc.getElementById('feature-shell-env-variables').textContent ).toBe('fromtheshell.'); - doc.defaultView.close(); }); it('expand .env variables', async () => { - const doc = await initDOM('expand-env-variables'); + doc = await initDOM('expand-env-variables'); expect(doc.getElementById('feature-expand-env-1').textContent).toBe( 'basic' @@ -84,7 +87,6 @@ describe('Integration', () => { expect( doc.getElementById('feature-expand-env-existing').textContent ).toBe('fromtheshell'); - doc.defaultView.close(); }); }); }); diff --git a/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js b/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js index e807888d397..a5d019828da 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/initDOM.js @@ -6,46 +6,31 @@ */ const fs = require('fs'); -const http = require('http'); -const jsdom = require('jsdom/lib/old-api.js'); +const { JSDOM, ResourceLoader } = require('jsdom'); const path = require('path'); +const url = require('url'); -let getMarkup; -export let resourceLoader; - -if (process.env.E2E_FILE) { - const file = path.isAbsolute(process.env.E2E_FILE) +const file = + process.env.E2E_FILE && + (path.isAbsolute(process.env.E2E_FILE) ? process.env.E2E_FILE - : path.join(process.cwd(), process.env.E2E_FILE); - - const markup = fs.readFileSync(file, 'utf8'); - getMarkup = () => markup; + : path.join(process.cwd(), process.env.E2E_FILE)); +export const fetchFile = url => { const pathPrefix = process.env.PUBLIC_URL.replace(/^https?:\/\/[^/]+\/?/, ''); + return fs.readFileSync( + path.join(path.dirname(file), url.pathname.replace(pathPrefix, '')), + 'utf8' + ); +}; - resourceLoader = (resource, callback) => - callback( - null, - fs.readFileSync( - path.join( - path.dirname(file), - resource.url.pathname.replace(pathPrefix, '') - ), - 'utf8' - ) - ); -} else if (process.env.E2E_URL) { - getMarkup = () => - new Promise(resolve => { - http.get(process.env.E2E_URL, res => { - let rawData = ''; - res.on('data', chunk => (rawData += chunk)); - res.on('end', () => resolve(rawData)); - }); - }); +const fileResourceLoader = new class FileResourceLoader extends ResourceLoader { + fetch(href, options) { + return Promise.resolve(fetchFile(url.parse(href))); + } +}(); - resourceLoader = (resource, callback) => resource.defaultFetch(callback); -} else { +if (!process.env.E2E_FILE && !process.env.E2E_URL) { it.only('can run jsdom (at least one of "E2E_FILE" or "E2E_URL" environment variables must be provided)', () => { expect( new Error("This isn't the error you are looking for.") @@ -54,18 +39,46 @@ if (process.env.E2E_FILE) { } export default feature => - new Promise(async resolve => { - const markup = await getMarkup(); - const host = process.env.E2E_URL || 'http://www.example.org/spa:3000'; - const doc = jsdom.jsdom(markup, { - created: (_, win) => - win.addEventListener('ReactFeatureDidMount', () => resolve(doc), true), - deferClose: true, - pretendToBeVisual: true, - resourceLoader, - url: `${host}#${feature}`, - virtualConsole: jsdom.createVirtualConsole().sendTo(console), - }); + new Promise(async (resolve, reject) => { + try { + const host = process.env.E2E_URL || 'http://www.example.org/spa:3000'; + const url = `${host}#${feature}`; + + let window; + + if (process.env.E2E_FILE) { + window = (await JSDOM.fromFile(file, { + pretendToBeVisual: true, + resources: fileResourceLoader, + runScripts: 'dangerously', + url, + })).window; + } else { + window = (await JSDOM.fromURL(url, { + pretendToBeVisual: true, + resources: 'usable', + runScripts: 'dangerously', + })).window; + } + + const { document } = window; + + document.addEventListener( + 'ReactFeatureDidMount', + () => resolve(document), + { capture: true, once: true } + ); + document.addEventListener( + 'ReactFeatureError', + () => { + // Cleanup jsdom instance since we don't need it anymore + window.close(); - doc.close(); + reject(`Error loading feature: ${feature}`); + }, + { capture: true, once: true } + ); + } catch (e) { + reject(e); + } }); diff --git a/packages/react-scripts/fixtures/kitchensink/integration/syntax.test.js b/packages/react-scripts/fixtures/kitchensink/integration/syntax.test.js index 54920726fa5..9415c88863f 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/syntax.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/syntax.test.js @@ -9,137 +9,129 @@ import initDOM from './initDOM'; describe('Integration', () => { describe('Language syntax', () => { + let doc; + + afterEach(() => { + doc && doc.defaultView.close(); + doc = undefined; + }); + it('array destructuring', async () => { - const doc = await initDOM('array-destructuring'); + doc = await initDOM('array-destructuring'); expect( doc.getElementById('feature-array-destructuring').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('array spread', async () => { - const doc = await initDOM('array-spread'); + doc = await initDOM('array-spread'); expect(doc.getElementById('feature-array-spread').childElementCount).toBe( 4 ); - doc.defaultView.close(); }); it('async/await', async () => { - const doc = await initDOM('async-await'); + doc = await initDOM('async-await'); expect(doc.getElementById('feature-async-await').childElementCount).toBe( 4 ); - doc.defaultView.close(); }); it('class properties', async () => { - const doc = await initDOM('class-properties'); + doc = await initDOM('class-properties'); expect( doc.getElementById('feature-class-properties').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('computed properties', async () => { - const doc = await initDOM('computed-properties'); + doc = await initDOM('computed-properties'); expect( doc.getElementById('feature-computed-properties').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('custom interpolation', async () => { - const doc = await initDOM('custom-interpolation'); + doc = await initDOM('custom-interpolation'); expect( doc.getElementById('feature-custom-interpolation').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('default parameters', async () => { - const doc = await initDOM('default-parameters'); + doc = await initDOM('default-parameters'); expect( doc.getElementById('feature-default-parameters').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('destructuring and await', async () => { - const doc = await initDOM('destructuring-and-await'); + doc = await initDOM('destructuring-and-await'); expect( doc.getElementById('feature-destructuring-and-await').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('generators', async () => { - const doc = await initDOM('generators'); + doc = await initDOM('generators'); expect(doc.getElementById('feature-generators').childElementCount).toBe( 4 ); - doc.defaultView.close(); }); it('object destructuring', async () => { - const doc = await initDOM('object-destructuring'); + doc = await initDOM('object-destructuring'); expect( doc.getElementById('feature-object-destructuring').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('object spread', async () => { - const doc = await initDOM('object-spread'); + doc = await initDOM('object-spread'); expect( doc.getElementById('feature-object-spread').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('promises', async () => { - const doc = await initDOM('promises'); + doc = await initDOM('promises'); expect(doc.getElementById('feature-promises').childElementCount).toBe(4); - doc.defaultView.close(); }); it('rest + default', async () => { - const doc = await initDOM('rest-and-default'); + doc = await initDOM('rest-and-default'); expect( doc.getElementById('feature-rest-and-default').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('rest parameters', async () => { - const doc = await initDOM('rest-parameters'); + doc = await initDOM('rest-parameters'); expect( doc.getElementById('feature-rest-parameters').childElementCount ).toBe(4); - doc.defaultView.close(); }); it('template interpolation', async () => { - const doc = await initDOM('template-interpolation'); + doc = await initDOM('template-interpolation'); expect( doc.getElementById('feature-template-interpolation').childElementCount ).toBe(4); - doc.defaultView.close(); }); }); }); diff --git a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js index 32c9b47a48d..5f1b7df1eda 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import initDOM, { resourceLoader } from './initDOM'; +import initDOM, { fetchFile } from './initDOM'; import url from 'url'; const matchCSS = (doc, regexes) => { @@ -17,11 +17,11 @@ const matchCSS = (doc, regexes) => { href = elem.href; } } - resourceLoader({ url: url.parse(href) }, (_, textContent) => { - for (const regex of regexes) { - expect(textContent).toMatch(regex); - } - }); + + const textContent = fetchFile(url.parse(href)); + for (const regex of regexes) { + expect(textContent).toMatch(regex); + } } else { for (let i = 0; i < regexes.length; ++i) { expect( @@ -33,118 +33,111 @@ const matchCSS = (doc, regexes) => { describe('Integration', () => { describe('Webpack plugins', () => { + let doc; + + afterEach(() => { + doc && doc.defaultView.close(); + doc = undefined; + }); + it('css inclusion', async () => { - const doc = await initDOM('css-inclusion'); + doc = await initDOM('css-inclusion'); matchCSS(doc, [ /html\{/, /#feature-css-inclusion\{background:.+;color:.+}/, ]); - doc.defaultView.close(); }); it('css modules inclusion', async () => { - const doc = await initDOM('css-modules-inclusion'); + doc = await initDOM('css-modules-inclusion'); matchCSS(doc, [ /.+style_cssModulesInclusion__.+\{background:.+;color:.+}/, /.+assets_cssModulesIndexInclusion__.+\{background:.+;color:.+}/, ]); - doc.defaultView.close(); }); it('scss inclusion', async () => { - const doc = await initDOM('scss-inclusion'); + doc = await initDOM('scss-inclusion'); matchCSS(doc, [/#feature-scss-inclusion\{background:.+;color:.+}/]); - doc.defaultView.close(); }); it('scss modules inclusion', async () => { - const doc = await initDOM('scss-modules-inclusion'); + doc = await initDOM('scss-modules-inclusion'); matchCSS(doc, [ /.+scss-styles_scssModulesInclusion.+\{background:.+;color:.+}/, /.+assets_scssModulesIndexInclusion.+\{background:.+;color:.+}/, ]); - doc.defaultView.close(); }); it('sass inclusion', async () => { - const doc = await initDOM('sass-inclusion'); + doc = await initDOM('sass-inclusion'); matchCSS(doc, [/#feature-sass-inclusion\{background:.+;color:.+}/]); - doc.defaultView.close(); }); it('sass modules inclusion', async () => { - const doc = await initDOM('sass-modules-inclusion'); + doc = await initDOM('sass-modules-inclusion'); matchCSS(doc, [ /.+sass-styles_sassModulesInclusion.+\{background:.+;color:.+}/, /.+assets_sassModulesIndexInclusion.+\{background:.+;color:.+}/, ]); - doc.defaultView.close(); }); it('image inclusion', async () => { - const doc = await initDOM('image-inclusion'); + doc = await initDOM('image-inclusion'); expect(doc.getElementById('feature-image-inclusion').src).toMatch( /^data:image\/jpeg;base64.+==$/ ); - doc.defaultView.close(); }); it('no ext inclusion', async () => { - const doc = await initDOM('no-ext-inclusion'); + doc = await initDOM('no-ext-inclusion'); expect(doc.getElementById('feature-no-ext-inclusion').href).toMatch( /\/static\/media\/aFileWithoutExt\.[a-f0-9]{8}\.bin$/ ); - doc.defaultView.close(); }); it('json inclusion', async () => { - const doc = await initDOM('json-inclusion'); + doc = await initDOM('json-inclusion'); expect(doc.getElementById('feature-json-inclusion').textContent).toBe( 'This is an abstract.' ); - doc.defaultView.close(); }); it('linked modules', async () => { - const doc = await initDOM('linked-modules'); + doc = await initDOM('linked-modules'); expect(doc.getElementById('feature-linked-modules').textContent).toBe( '2.0.0' ); - doc.defaultView.close(); }); it('svg inclusion', async () => { - const doc = await initDOM('svg-inclusion'); + doc = await initDOM('svg-inclusion'); expect(doc.getElementById('feature-svg-inclusion').src).toMatch( /\/static\/media\/logo\..+\.svg$/ ); - doc.defaultView.close(); }); it('svg component', async () => { - const doc = await initDOM('svg-component'); + doc = await initDOM('svg-component'); expect(doc.getElementById('feature-svg-component').textContent).toBe(''); - doc.defaultView.close(); }); it('svg in css', async () => { - const doc = await initDOM('svg-in-css'); + doc = await initDOM('svg-in-css'); matchCSS(doc, [/\/static\/media\/logo\..+\.svg/]); - doc.defaultView.close(); }); it('unknown ext inclusion', async () => { - const doc = await initDOM('unknown-ext-inclusion'); + doc = await initDOM('unknown-ext-inclusion'); expect(doc.getElementById('feature-unknown-ext-inclusion').href).toMatch( /\/static\/media\/aFileWithExt\.[a-f0-9]{8}\.unknown$/ ); - doc.defaultView.close(); }); }); }); diff --git a/packages/react-scripts/fixtures/kitchensink/src/App.js b/packages/react-scripts/fixtures/kitchensink/src/App.js index 380a49fc639..547138832f3 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/App.js +++ b/packages/react-scripts/fixtures/kitchensink/src/App.js @@ -10,11 +10,17 @@ import PropTypes from 'prop-types'; class BuiltEmitter extends Component { static propTypes = { - feature: PropTypes.func.isRequired, + error: PropTypes.string, + feature: PropTypes.func, }; componentDidMount() { - const { feature } = this.props; + const { error, feature } = this.props; + + if (error) { + this.handleError(error); + return; + } // Class components must call this.props.onReady when they're ready for the test. // We will assume functional components are ready immediately after mounting. @@ -23,6 +29,10 @@ class BuiltEmitter extends Component { } } + handleError(error) { + document.dispatchEvent(new Event('ReactFeatureError')); + } + handleReady() { document.dispatchEvent(new Event('ReactFeatureDidMount')); } @@ -34,9 +44,10 @@ class BuiltEmitter extends Component { } = this; return (
- {createElement(feature, { - onReady: handleReady, - })} + {feature && + createElement(feature, { + onReady: handleReady, + })}
); } @@ -52,7 +63,13 @@ class App extends Component { } componentDidMount() { - const feature = window.location.hash.slice(1); + const url = window.location.href; + // const feature = window.location.hash.slice(1); + // This works around an issue of a duplicate hash in the href + // Ex: http://localhost:3001/#array-destructuring#array-destructuring + // This seems like a jsdom bug as the URL in initDom.js appears to be correct + const feature = url.slice(url.lastIndexOf("#") + 1); + switch (feature) { case 'array-destructuring': import('./features/syntax/ArrayDestructuring').then(f => @@ -223,7 +240,7 @@ class App extends Component { ); break; default: - throw new Error(`Missing feature "${feature}"`); + this.setState({ error: `Missing feature "${feature}"` }); } } @@ -232,9 +249,9 @@ class App extends Component { } render() { - const { feature } = this.state; - if (feature !== null) { - return ; + const { error, feature } = this.state; + if (error || feature) { + return ; } return null; } diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 0d480c87a80..237710da9d8 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -28,9 +28,8 @@ "@svgr/webpack": "4.1.0", "@typescript-eslint/eslint-plugin": "1.4.1", "@typescript-eslint/parser": "1.4.1", - "babel-core": "7.0.0-bridge.0", "babel-eslint": "10.0.1", - "babel-jest": "24.5.0", + "babel-jest": "24.7.1", "babel-loader": "8.0.5", "babel-plugin-named-asset-import": "^0.3.1", "babel-preset-react-app": "^7.0.2", @@ -50,9 +49,10 @@ "fs-extra": "7.0.1", "html-webpack-plugin": "4.0.0-beta.5", "identity-obj-proxy": "3.0.0", - "jest": "24.5.0", - "jest-resolve": "24.5.0", - "jest-watch-typeahead": "^0.2.1", + "jest": "24.7.1", + "jest-environment-jsdom-fourteen": "0.1.0", + "jest-resolve": "24.7.1", + "jest-watch-typeahead": "0.3.0", "mini-css-extract-plugin": "0.5.0", "optimize-css-assets-webpack-plugin": "5.0.1", "pnp-webpack-plugin": "1.2.1", diff --git a/packages/react-scripts/scripts/utils/createJestConfig.js b/packages/react-scripts/scripts/utils/createJestConfig.js index 705f0f87ae3..4300c7e480c 100644 --- a/packages/react-scripts/scripts/utils/createJestConfig.js +++ b/packages/react-scripts/scripts/utils/createJestConfig.js @@ -35,8 +35,7 @@ module.exports = (resolve, rootDir, isEjecting) => { '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', '/src/**/?(*.)(spec|test).{js,jsx,ts,tsx}', ], - testEnvironment: 'jsdom', - testURL: 'http://localhost', + testEnvironment: 'jest-environment-jsdom-fourteen', transform: { '^.+\\.(js|jsx|ts|tsx)$': isEjecting ? '/node_modules/babel-jest' @@ -58,8 +57,8 @@ module.exports = (resolve, rootDir, isEjecting) => { ext => !ext.includes('mjs') ), watchPlugins: [ - require.resolve('jest-watch-typeahead/filename'), - require.resolve('jest-watch-typeahead/testname'), + 'jest-watch-typeahead/filename', + 'jest-watch-typeahead/testname', ], }; if (rootDir) { diff --git a/tasks/local-test.sh b/tasks/local-test.sh index aad6a477076..d1c01946c2f 100755 --- a/tasks/local-test.sh +++ b/tasks/local-test.sh @@ -114,6 +114,7 @@ docker run \ --env NPM_CONFIG_PREFIX=/home/node/.npm \ --env NPM_CONFIG_QUIET=true \ --tty \ + --rm \ --user node \ --volume ${PWD}/..:/var/create-react-app \ --workdir /home/node \