diff --git a/package.json b/package.json index bc78dfee6b6..e5d67aa16e3 100644 --- a/package.json +++ b/package.json @@ -12,19 +12,23 @@ "postinstall": "cd packages/react-error-overlay/ && yarn build:prod", "publish": "tasks/publish.sh", "start": "cd packages/react-scripts && node bin/react-scripts.js start", - "screencast": "svg-term --cast hItN7sl5yfCPTHxvFg5glhhfp --out screencast.svg --window", + "screencast": "node ./tasks/screencast.js", "test": "cd packages/react-scripts && node bin/react-scripts.js test --env=jsdom", "format": "prettier --trailing-comma es5 --single-quote --write 'packages/*/*.js' 'packages/*/!(node_modules)/**/*.js'", "precommit": "lint-staged" }, "devDependencies": { "eslint": "4.15.0", + "execa": "^0.9.0", "husky": "^0.13.2", "lerna": "2.6.0", "lerna-changelog": "^0.6.0", "lint-staged": "^3.3.1", + "meow": "^4.0.0", + "multimatch": "^2.1.0", "prettier": "1.6.1", - "svg-term-cli": "^2.0.3" + "svg-term-cli": "^2.0.3", + "tempy": "^0.2.1" }, "lint-staged": { "*.js": [ diff --git a/tasks/screencast-start.js b/tasks/screencast-start.js new file mode 100644 index 00000000000..3841a6e19ad --- /dev/null +++ b/tasks/screencast-start.js @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const execa = require('execa'); +const meow = require('meow'); +const multimatch = require('multimatch'); + +main(meow()); + +function main(cli) { + let count = 0; + + const start = Date.now(); + const duration = parseInt(cli.flags.timeout, 10) * 1000; + const cp = execa.shell(cli.flags.command); + + const target = parseInt(cli.flags.patternCount || '1', 10); + + cp.stdout.on('data', data => { + process.stdout.write(data); + const matches = multimatch([String(data)], cli.flags.pattern); + const errMatches = multimatch([String(data)], cli.flags.errorPattern); + + if (matches.length > 0) { + count++; + } + + if (errMatches.length > 0) { + process.exit(1); + } + + if (count >= target) { + setTimeout(() => { + process.exit(0); + }, duration); + } + }); + + cp.on('exit', e => { + const elapsed = Date.now() - start; + + if (elapsed >= duration) { + return; + } + + setTimeout(() => { + process.exit(e.code); + }, duration - elapsed); + }); +} diff --git a/tasks/screencast.js b/tasks/screencast.js new file mode 100644 index 00000000000..53c30d6a8fd --- /dev/null +++ b/tasks/screencast.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const execa = require('execa'); +const tempy = require('tempy'); + +main(); + +function main() { + const previous = process.cwd(); + const cwd = tempy.directory(); + + const cast = path.join(cwd, 'screencast.json'); + const script = path.join(__dirname, 'screencast.sh'); + const out = path.join(previous, 'screencast.svg'); + + const resolveLine = l => l.indexOf('🔍 Resolving packages...') > -1; + const fetchLine = l => l.indexOf('🚚 Fetching packages...') > -1; + const countLine = l => l.match(/Saved [0-9]+ new dependencies/); + const doneLine = l => l.indexOf('✨ Done in') > -1; + + try { + process.chdir(cwd); + console.log(`Recording screencast ...`); + execa.sync('asciinema', ['rec', '--command', `sh ${script}`, cast], { + cwd, + stdio: 'inherit', + }); + + console.log('Cleaning data ...'); + const data = require(cast); + + cut(data.stdout, { start: resolveLine, end: fetchLine }); + cut(data.stdout, { start: countLine, end: doneLine }); + replace(data.stdout, [{ in: cwd, out: '~' }]); + + fs.writeFileSync(cast, JSON.stringify(data, null, ' ')); + + console.log('Rendering SVG ...'); + execa.sync('svg-term', ['--window', '--in', cast, '--out', out]); + + console.log(`Recorded screencast to ${cast}`); + console.log(`Rendered SVG to ${out}`); + } catch (err) { + throw err; + } finally { + process.chdir(previous); + } +} + +function cut(frames, { start, end }) { + const si = frames.findIndex(([, l]) => start(l)); + const ei = frames.findIndex(([, l]) => end(l)); + + if (si === -1 || ei === -1) { + return; + } + + frames.splice(si + 1, ei - si - 1); +} + +function replace(frames, replacements) { + frames.forEach(frame => { + replacements.forEach(r => (frame[1] = frame[1].split(r.in).join(r.out))); + }); +} diff --git a/tasks/screencast.sh b/tasks/screencast.sh new file mode 100644 index 00000000000..e1c01a5ab30 --- /dev/null +++ b/tasks/screencast.sh @@ -0,0 +1,33 @@ +#!/bin/zsh +# Copyright (c) 2015-present, Facebook, Inc. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +# ****************************************************************************** +# This is an end-to-end test intended to be run via screencast.js +# Dependencies: asciinema, pv, core-utils +# ****************************************************************************** +set -e + +printf '\e[32m%s\e[m' "λ " +echo "npx create-react-app my-app" | pv -qL $[10+(-2 + RANDOM%5)] +npx create-react-app my-app + +printf '\e[32m%s\e[m' "λ " +sleep 1 +echo "cd my-app" | pv -qL $[10+(-2 + RANDOM%5)] +cd my-app + +printf '\e[32m%s\e[m' "λ " +sleep 1 +echo "npm start" | pv -qL $[10+(-2 + RANDOM%5)] + +BROWSER="none" node "$(dirname $0)/screencast-start.js" \ + --command "npm start" \ + --pattern="Compiled successfully*" \ + --pattern-count 2 \ + --error-pattern="*already running on port" \ + --timeout 10 + +echo "" \ No newline at end of file