├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .gitlab-ci.yml ├── .npmignore ├── .nvmrc ├── LICENSE ├── README.md ├── demo-cli ├── cli-evaluator.js ├── cli-evaluator.spec.js └── index.js ├── demo-rn ├── .buckconfig ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .watchmanconfig ├── App.js ├── __tests__ │ └── App-test.js ├── android │ ├── app │ │ ├── BUCK │ │ ├── build.gradle │ │ ├── build_defs.bzl │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── keystores │ │ ├── BUCK │ │ └── debug.keystore.properties │ └── settings.gradle ├── app.json ├── babel.config.js ├── index.js ├── ios │ ├── example-tvOS │ │ └── Info.plist │ ├── example-tvOSTests │ │ └── Info.plist │ ├── example.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── example-tvOS.xcscheme │ │ │ └── example.xcscheme │ ├── example │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ └── LaunchScreen.xib │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── main.m │ └── exampleTests │ │ ├── Info.plist │ │ └── exampleTests.m ├── metro.config.js ├── package.json ├── terminal.min.js └── yarn.lock ├── demo-web ├── css │ ├── main.css │ └── normalize.css ├── index.html └── js │ └── main.js ├── index.js ├── package.json ├── raw └── anim.gif ├── react-native.wrapper.js ├── src ├── commands │ ├── cat.js │ ├── cd.js │ ├── clear.js │ ├── cp.js │ ├── echo.js │ ├── head.js │ ├── history.js │ ├── index.js │ ├── ls.js │ ├── mkdir.js │ ├── printenv.js │ ├── pwd.js │ ├── rm.js │ ├── rmdir.js │ ├── tail.js │ ├── touch.js │ ├── util │ │ └── _head_tail_util.js │ └── whoami.js ├── emulator-output │ ├── index.js │ ├── output-factory.js │ └── output-type.js ├── emulator-state │ ├── EmulatorState.js │ ├── command-mapping.js │ ├── environment-variables.js │ ├── file-system.js │ ├── history.js │ ├── index.js │ ├── outputs.js │ └── util.js ├── emulator │ ├── auto-complete.js │ ├── command-runner.js │ ├── emulator-error.js │ ├── index.js │ └── plugins │ │ ├── BoundedHistoryIterator.js │ │ └── HistoryKeyboardPlugin.js ├── fs │ ├── fs-error.js │ ├── index.js │ ├── operations-with-permissions │ │ ├── directory-operations.js │ │ └── file-operations.js │ ├── operations │ │ ├── base-operations.js │ │ ├── directory-operations.js │ │ └── file-operations.js │ └── util │ │ ├── file-util.js │ │ ├── glob-util.js │ │ ├── path-util.js │ │ └── permission-util.js ├── index.js └── parser │ ├── command-parser.js │ ├── index.js │ └── option-parser.js ├── test ├── _plugins │ └── state-equality-plugin.js ├── commands │ ├── cat.spec.js │ ├── cd.spec.js │ ├── clear.spec.js │ ├── cp.spec.js │ ├── echo.spec.js │ ├── head.spec.js │ ├── history.spec.js │ ├── ls.spec.js │ ├── mapping │ │ └── index.spec.js │ ├── mkdir.spec.js │ ├── printenv.spec.js │ ├── pwd.spec.js │ ├── rm.spec.js │ ├── rmdir.spec.js │ ├── tail.spec.js │ ├── test-helper.js │ ├── touch.spec.js │ └── whoami.spec.js ├── emulator-output │ └── output-factory.spec.js ├── emulator-state │ ├── EmulatorState.spec.js │ ├── command-mapping.spec.js │ ├── environment-variables.spec.js │ ├── file-system.spec.js │ ├── history.spec.js │ └── outputs.spec.js ├── emulator │ ├── auto-complete.spec.js │ ├── command-runner.spec.js │ ├── emulator-error.spec.js │ ├── emulator.spec.js │ └── plugins │ │ ├── BoundedHistoryIterator.spec.js │ │ └── history-keyboard.spec.js ├── library.spec.js ├── os │ ├── fs-error.spec.js │ ├── mocks │ │ ├── mock-fs-permissions.js │ │ └── mock-fs.js │ ├── operations-with-permissions │ │ ├── directory-operations.spec.js │ │ └── file-operations.spec.js │ ├── operations │ │ ├── base-operations.spec.js │ │ ├── directory-operations.spec.js │ │ └── file-operations.spec.js │ └── util │ │ ├── file-util.spec.js │ │ ├── glob-util.spec.js │ │ ├── path-util.spec.js │ │ └── permission-util.spec.js └── parser │ ├── command-parser.spec.js │ └── opt-parser.spec.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["babel-plugin-add-module-exports", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | .nyc_output 4 | *.log 5 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:latest 2 | 3 | stages: 4 | - build 5 | - test 6 | - deploy 7 | 8 | before_script: 9 | - yarn install 10 | 11 | # Build stage 12 | build: 13 | script: 14 | - yarn build 15 | artifacts: 16 | paths: 17 | - lib/ 18 | 19 | # Test stage 20 | test: 21 | script: 22 | - yarn build 23 | - yarn test:coverage 24 | artifacts: 25 | paths: 26 | - coverage/ 27 | 28 | # Deploy stage 29 | pages: 30 | stage: deploy 31 | script: 32 | - mkdir public 33 | # Test coverage 34 | - yarn run artifact:test-coverage 35 | - mv coverage/ public/coverage 36 | # Library 37 | - mv lib/ public/lib 38 | # Web demo 39 | - mv demo-web/ public/demo 40 | artifacts: 41 | paths: 42 | - public 43 | expire_in: 30 days 44 | only: 45 | - master 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.10 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Rohan Chandra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://user-images.githubusercontent.com/816965/38487336-1d193960-3c23-11e8-8da6-9575b0eac3e9.png) 2 | 3 | # React Native Terminal Component 4 | 5 | An open-source JavaScript terminal emulator library, that works in your browser, Node.js and React. 6 | 7 |

8 | React Native Terminal Component 9 |

10 | 11 | 12 | This is a React Native wrapper around [rohanchandra](https://github.com/rohanchandra)'s extensible `javascript-terminal` emulator. The goal of this library is to provide a consistent and easily-portable React Native interface to `javascript-terminal`. 13 | 14 | [Demo](https://rohanchandra.gitlab.io/javascript-terminal/demo/) 15 | 16 | ## Features 17 | * In-memory file system, backed by Immutable.js 18 | * Selected *NIX commands emulated (such as `ls`, `cd`, `head`, `cat`, `echo`, `rm`) 19 | * Command parsing 20 | * Support for environment variables 21 | * Autocompletion of terminal commands 22 | 23 | For more information, please check out the original [repo](https://github.com/rohanchandra/javascript-terminal). Whilst you're there, you should follow [@rohanchandra](https://github.com/rohanchandra). 24 | 25 | ## Installation 26 | Install with `npm` or with `yarn`. 27 | 28 | ```shell 29 | npm install react-native-terminal-component --save 30 | ``` 31 | 32 | ```shell 33 | yarn add react-native-terminal-component 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```javascript 39 | import React from 'react'; 40 | import { Alert } from 'react-native'; 41 | import Terminal from 'react-native-terminal-component'; 42 | 43 | export default class App extends React.Component { 44 | render() { 45 | return ( 46 | { 53 | Alert.alert( 54 | opts 55 | .join(' '), 56 | ); 57 | } 58 | optDef: {}, 59 | }, 60 | }} 61 | /> 62 | ); 63 | } 64 | } 65 | ``` 66 | 67 | ### Examples 68 | This library does not prescribe a method for displaying terminal output or the user interface, so I've provided examples in Node.js, pure JavaScript/HTML/CSS and with React/JavaScript/HTML/CSS: 69 | 70 | 1. View the `/demo-cli` directory for an example of usage in Node.js 71 | 2. View the `/demo-web` directory for an example of usage in plain HTML and JavaScript 72 | 3. Visit the [React Terminal Component website](https://github.com/rohanchandra/react-terminal-component) for usage with HTML, CSS, JavaScript and React 73 | 4. View the `/demo-rn` directory for an example of usage in React Native. 74 | 75 | ## Building 76 | 77 | ### Set-up 78 | 79 | First, make sure you have [Node.js](https://nodejs.org/en/download/), [Yarn](https://yarnpkg.com/en/docs/install) and [Git](https://git-scm.com/downloads) installed. 80 | 81 | Now, fork and clone repo and install the dependencies. 82 | 83 | ```shell 84 | git clone https://github.com/Cawfree/react-native-terminal-component 85 | cd react-native-terminal-component/ 86 | yarn install 87 | ``` 88 | 89 | ### Scripts 90 | 91 | #### Build scripts 92 | * `yarn build` - creates a development and production build of the library in `lib` 93 | * `yarn build-rn` - generates the react-native wrapper interface stored in `demo-rn/terminal.min.js` for the component (this must be re-ran for each modification made to `react-native.wrapper.js`) 94 | 95 | #### Test scripts 96 | * `yarn test` - run tests 97 | * `yarn test:min` - run tests with summary reports 98 | * `yarn test:coverage` - shows test coverage stats 99 | * `yarn artifact:coverage-report` - creates [HTML test coverage report](https://rohanchandra.gitlab.io/javascript-terminal/coverage/) in `.nyc_output` 100 | 101 | #### Demo scripts 102 | * `yarn cli` - demo of using the emulator library in a Node.js command-line interface (this requires you have built the library with `yarn build`) 103 | 104 | ## License 105 | 106 | Copyright 2018 Rohan Chandra 107 | 108 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 109 | 110 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 111 | 112 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 113 | -------------------------------------------------------------------------------- /demo-cli/cli-evaluator.js: -------------------------------------------------------------------------------- 1 | const { Emulator, EmulatorState, OutputType } = require('../lib/terminal.js'); 2 | 3 | /** 4 | * Processes multiple outputs for display. 5 | * 6 | * Currently only text-based output is supported. 7 | * @param {number} outputCount number of outputs from the command run 8 | * @param {list} outputs all emulator outputs 9 | * @return {none} 10 | */ 11 | const commandOutputToString = (outputCount, outputs) => { 12 | return outputs 13 | .slice(-1 * outputCount) 14 | .filter(output => output.type === OutputType.TEXT_OUTPUT_TYPE || output.type === OutputType.TEXT_ERROR_OUTPUT_TYPE) 15 | .map(output => output.content) 16 | .join('\n'); 17 | }; 18 | 19 | /** 20 | * Creates an evaluator for a Node REPL 21 | * @return {function} Node REPL evaluator 22 | */ 23 | const getTerminalEvaluator = () => { 24 | const emulator = new Emulator(); 25 | let state = EmulatorState.createEmpty(); 26 | let lastOutputsSize = 0; 27 | 28 | return (commandStr) => { 29 | state = emulator.execute(state, commandStr); 30 | 31 | const outputs = state.getOutputs(); 32 | const outputStr = commandOutputToString(outputs.size - lastOutputsSize, outputs); 33 | 34 | lastOutputsSize = outputs.size; 35 | 36 | return outputStr; 37 | }; 38 | }; 39 | 40 | module.exports = getTerminalEvaluator; 41 | -------------------------------------------------------------------------------- /demo-cli/cli-evaluator.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | const getTerminalEvaluator = require('./cli-evaluator'); 3 | 4 | chai.expect(); 5 | 6 | let evaluator = getTerminalEvaluator(); 7 | 8 | describe('cli-evaluator', () => { 9 | it('should evaluate command without string output', () => { 10 | const output = evaluator('mkdir a'); 11 | 12 | chai.expect(output).to.equal(''); 13 | }); 14 | 15 | it('should evaluate command with string output', () => { 16 | const output = evaluator('echo hello world'); 17 | 18 | chai.expect(output).to.equal('hello world'); 19 | }); 20 | 21 | it('should evaluate multiple commands', () => { 22 | evaluator('mkdir testFolder'); 23 | evaluator('cd testFolder'); 24 | evaluator('mkdir foo'); 25 | evaluator('mkdir baz'); 26 | evaluator('mkdir bar'); 27 | evaluator('rmdir bar'); 28 | const output = evaluator('ls'); 29 | 30 | chai.expect(output).to.equal('baz/\nfoo/'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /demo-cli/index.js: -------------------------------------------------------------------------------- 1 | const repl = require('repl'); 2 | const getTerminalEvaluator = require('./cli-evaluator'); 3 | 4 | const replEvaluator = getTerminalEvaluator(); 5 | 6 | return repl.start({ 7 | prompt: 'emulator$ ', 8 | eval: (cmd, context, filename, callback) => { 9 | const outputStr = replEvaluator(cmd); 10 | 11 | console.log(outputStr); 12 | 13 | callback(); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /demo-rn/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /demo-rn/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | 16 | ; Ignore polyfills 17 | .*/Libraries/polyfills/.* 18 | 19 | ; Ignore metro 20 | .*/node_modules/metro/.* 21 | 22 | [include] 23 | 24 | [libs] 25 | node_modules/react-native/Libraries/react-native/react-native-interface.js 26 | node_modules/react-native/flow/ 27 | 28 | [options] 29 | emoji=true 30 | 31 | esproposal.optional_chaining=enable 32 | esproposal.nullish_coalescing=enable 33 | 34 | module.system=haste 35 | module.system.haste.use_name_reducers=true 36 | # get basename 37 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' 38 | # strip .js or .js.flow suffix 39 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' 40 | # strip .ios suffix 41 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' 42 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' 43 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' 44 | module.system.haste.paths.blacklist=.*/__tests__/.* 45 | module.system.haste.paths.blacklist=.*/__mocks__/.* 46 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* 47 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* 48 | 49 | munge_underscores=true 50 | 51 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 52 | 53 | module.file_ext=.js 54 | module.file_ext=.jsx 55 | module.file_ext=.json 56 | module.file_ext=.native.js 57 | 58 | suppress_type=$FlowIssue 59 | suppress_type=$FlowFixMe 60 | suppress_type=$FlowFixMeProps 61 | suppress_type=$FlowFixMeState 62 | 63 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 64 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 65 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 66 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 67 | 68 | [version] 69 | ^0.92.0 70 | -------------------------------------------------------------------------------- /demo-rn/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /demo-rn/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | -------------------------------------------------------------------------------- /demo-rn/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /demo-rn/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // TODO: This is an antipattern. 3 | import Terminal from './../'; 4 | 5 | export default class App extends React.Component { 6 | render() { 7 | // XXX: Check out Terminal's defaultProps for custom usage. 8 | return ( 9 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo-rn/__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /demo-rn/android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.example", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.example", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /demo-rn/android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /demo-rn/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /demo-rn/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/java/com/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "example"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/java/com/example/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.facebook.react.ReactNativeHost; 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.shell.MainReactPackage; 9 | import com.facebook.soloader.SoLoader; 10 | 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 17 | @Override 18 | public boolean getUseDeveloperSupport() { 19 | return BuildConfig.DEBUG; 20 | } 21 | 22 | @Override 23 | protected List getPackages() { 24 | return Arrays.asList( 25 | new MainReactPackage() 26 | ); 27 | } 28 | 29 | @Override 30 | protected String getJSMainModuleName() { 31 | return "index"; 32 | } 33 | }; 34 | 35 | @Override 36 | public ReactNativeHost getReactNativeHost() { 37 | return mReactNativeHost; 38 | } 39 | 40 | @Override 41 | public void onCreate() { 42 | super.onCreate(); 43 | SoLoader.init(this, /* native exopackage */ false); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | example 3 | 4 | -------------------------------------------------------------------------------- /demo-rn/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo-rn/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | supportLibVersion = "28.0.0" 10 | } 11 | repositories { 12 | google() 13 | jcenter() 14 | } 15 | dependencies { 16 | classpath 'com.android.tools.build:gradle:3.3.1' 17 | 18 | // NOTE: Do not place your application dependencies here; they belong 19 | // in the individual module build.gradle files 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | mavenLocal() 26 | google() 27 | jcenter() 28 | maven { 29 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 30 | url "$rootDir/../node_modules/react-native/android" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /demo-rn/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | -------------------------------------------------------------------------------- /demo-rn/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/demo-rn/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo-rn/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 6 | -------------------------------------------------------------------------------- /demo-rn/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /demo-rn/android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = "debug", 3 | properties = "debug.keystore.properties", 4 | store = "debug.keystore", 5 | visibility = [ 6 | "PUBLIC", 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /demo-rn/android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /demo-rn/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'example' 2 | 3 | include ':app' 4 | -------------------------------------------------------------------------------- /demo-rn/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "displayName": "example" 4 | } -------------------------------------------------------------------------------- /demo-rn/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /demo-rn/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import {name as appName} from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /demo-rn/ios/example-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /demo-rn/ios/example-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo-rn/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /demo-rn/ios/example/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /demo-rn/ios/example/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import "AppDelegate.h" 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 17 | { 18 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 19 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 20 | moduleName:@"example" 21 | initialProperties:nil]; 22 | 23 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 24 | 25 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 26 | UIViewController *rootViewController = [UIViewController new]; 27 | rootViewController.view = rootView; 28 | self.window.rootViewController = rootViewController; 29 | [self.window makeKeyAndVisible]; 30 | return YES; 31 | } 32 | 33 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 34 | { 35 | #if DEBUG 36 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 37 | #else 38 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 39 | #endif 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /demo-rn/ios/example/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /demo-rn/ios/example/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /demo-rn/ios/example/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo-rn/ios/example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSLocationWhenInUseUsageDescription 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UIViewControllerBasedStatusBarAppearance 42 | 43 | NSLocationWhenInUseUsageDescription 44 | 45 | NSAppTransportSecurity 46 | 47 | 48 | NSAllowsArbitraryLoads 49 | 50 | NSExceptionDomains 51 | 52 | localhost 53 | 54 | NSExceptionAllowsInsecureHTTPLoads 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /demo-rn/ios/example/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo-rn/ios/exampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo-rn/ios/exampleTests/exampleTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | #define TIMEOUT_SECONDS 600 15 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 16 | 17 | @interface exampleTests : XCTestCase 18 | 19 | @end 20 | 21 | @implementation exampleTests 22 | 23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 24 | { 25 | if (test(view)) { 26 | return YES; 27 | } 28 | for (UIView *subview in [view subviews]) { 29 | if ([self findSubviewInView:subview matching:test]) { 30 | return YES; 31 | } 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)testRendersWelcomeScreen 37 | { 38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 40 | BOOL foundElement = NO; 41 | 42 | __block NSString *redboxError = nil; 43 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 44 | if (level >= RCTLogLevelError) { 45 | redboxError = message; 46 | } 47 | }); 48 | 49 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 50 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 51 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 52 | 53 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 54 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 55 | return YES; 56 | } 57 | return NO; 58 | }]; 59 | } 60 | 61 | RCTSetLogFunction(RCTDefaultLogFunction); 62 | 63 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 64 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 65 | } 66 | 67 | 68 | @end 69 | -------------------------------------------------------------------------------- /demo-rn/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /demo-rn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start", 7 | "test": "jest" 8 | }, 9 | "dependencies": { 10 | "js-string-escape": "^1.0.1", 11 | "react": "16.8.3", 12 | "react-native": "0.59.8" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.4.4", 16 | "@babel/runtime": "^7.4.4", 17 | "babel-jest": "^24.8.0", 18 | "jest": "^24.8.0", 19 | "metro-react-native-babel-preset": "^0.54.1", 20 | "react-test-renderer": "16.8.3" 21 | }, 22 | "jest": { 23 | "preset": "react-native" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo-web/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | box-sizing: border-box; 3 | color: white; 4 | font-size: 1.25em; 5 | } 6 | 7 | *, *:before, *:after { 8 | box-sizing: inherit; 9 | } 10 | 11 | body { 12 | padding: 0.5%; 13 | } 14 | 15 | body, #input { 16 | background: #171e1d; 17 | font-family: monospace; 18 | } 19 | 20 | /* Input */ 21 | .input-wrapper { 22 | display: flex; 23 | } 24 | 25 | #input, .input-wrapper { 26 | color: #53eb9b; 27 | } 28 | 29 | #input { 30 | flex: 2; 31 | border: none; 32 | } 33 | 34 | #input:focus { 35 | outline: none; 36 | } 37 | 38 | /* Output */ 39 | #output-wrapper div { 40 | display: inline-block; 41 | width: 100%; 42 | } 43 | 44 | .header-output { 45 | color: #9d9d9d; 46 | } 47 | 48 | .error-output { 49 | color: #ff4e4e; 50 | } 51 | -------------------------------------------------------------------------------- /demo-web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Terminal Emulator 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demo-web/js/main.js: -------------------------------------------------------------------------------- 1 | /* global Terminal */ 2 | 3 | // Utilities 4 | const addKeyDownListener = (eventKey, target, onKeyDown) => { 5 | target.addEventListener('keydown', e => { 6 | if (e.key === eventKey) { 7 | onKeyDown(); 8 | 9 | e.preventDefault(); 10 | } 11 | }); 12 | }; 13 | 14 | const scrollToPageEnd = () => { 15 | window.scrollTo(0, document.body.scrollHeight); 16 | }; 17 | 18 | // User interface 19 | const viewRefs = { 20 | input: document.getElementById('input'), 21 | output: document.getElementById('output-wrapper') 22 | }; 23 | 24 | const createOutputDiv = (className, textContent) => { 25 | const div = document.createElement('div'); 26 | 27 | div.className = className; 28 | div.appendChild(document.createTextNode(textContent)); 29 | 30 | return div; 31 | }; 32 | 33 | const outputToHTMLNode = { 34 | [Terminal.OutputType.TEXT_OUTPUT_TYPE]: content => 35 | createOutputDiv('text-output', content), 36 | [Terminal.OutputType.TEXT_ERROR_OUTPUT_TYPE]: content => 37 | createOutputDiv('error-output', content), 38 | [Terminal.OutputType.HEADER_OUTPUT_TYPE]: content => 39 | createOutputDiv('header-output', `$ ${content.command}`) 40 | }; 41 | 42 | const displayOutputs = (outputs) => { 43 | viewRefs.output.innerHTML = ''; 44 | 45 | const outputNodes = outputs.map(output => 46 | outputToHTMLNode[output.type](output.content) 47 | ); 48 | 49 | for (const outputNode of outputNodes) { 50 | viewRefs.output.append(outputNode); 51 | } 52 | }; 53 | 54 | const getInput = () => viewRefs.input.value; 55 | 56 | const setInput = (input) => { 57 | viewRefs.input.value = input; 58 | }; 59 | 60 | const clearInput = () => { 61 | setInput(''); 62 | }; 63 | 64 | // Execution 65 | const emulator = new Terminal.Emulator(); 66 | 67 | let emulatorState = Terminal.EmulatorState.createEmpty(); 68 | const historyKeyboardPlugin = new Terminal.HistoryKeyboardPlugin(emulatorState); 69 | const plugins = [historyKeyboardPlugin]; 70 | 71 | addKeyDownListener('Enter', viewRefs.input, () => { 72 | const commandStr = getInput(); 73 | 74 | emulatorState = emulator.execute(emulatorState, commandStr, plugins); 75 | displayOutputs(emulatorState.getOutputs()); 76 | scrollToPageEnd(); 77 | clearInput(); 78 | }); 79 | 80 | addKeyDownListener('ArrowUp', viewRefs.input, () => { 81 | setInput(historyKeyboardPlugin.completeUp()); 82 | }); 83 | 84 | addKeyDownListener('ArrowDown', viewRefs.input, () => { 85 | setInput(historyKeyboardPlugin.completeDown()); 86 | }); 87 | 88 | addKeyDownListener('Tab', viewRefs.input, () => { 89 | const autoCompletionStr = emulator.autocomplete(emulatorState, getInput()); 90 | 91 | setInput(autoCompletionStr); 92 | }); 93 | 94 | const dispatch = (type, data = {}) => JSON.stringify({ 95 | type, 96 | data, 97 | }); 98 | 99 | // TODO: You need to move this to react-native-gen, this just aids readability 100 | setTimeout( 101 | () => window.postMessage(dispatch( 102 | 'ACTION_TYPE_READY', 103 | )), 104 | 1000, 105 | ); 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-terminal-component", 3 | "version": "1.0.5", 4 | "description": "Emulate a terminal environment in React Native", 5 | "scripts": { 6 | "build-rn": "node ./react-native.wrapper", 7 | "build": "webpack --mode development && webpack --mode production", 8 | "test": "cross-env NODE_PATH=./src mocha --require babel-core/register --colors './test/**/*.spec.js' './demo-**/**/*.spec.js'", 9 | "test:min": "yarn run test --reporter min", 10 | "test:coverage": "nyc yarn run test", 11 | "cli": "node demo-cli" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/Cawfree/react-native-javascript-terminal.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "react-native", 20 | "javascipt", 21 | "shell", 22 | "command", 23 | "line", 24 | "terminal", 25 | "emulation" 26 | ], 27 | "author": "Alex Thomas (@cawfree)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/Cawfree/react-native-terminal-component/issues" 31 | }, 32 | "homepage": "https://github.com/Cawfree/react-native-terminal-component", 33 | "devDependencies": { 34 | "babel-cli": "^6.26.0", 35 | "babel-core": "^6.26.0", 36 | "babel-eslint": "^8.2.2", 37 | "babel-loader": "^7.1.4", 38 | "babel-plugin-add-module-exports": "^0.2.1", 39 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 40 | "babel-preset-env": "^1.6.1", 41 | "chai": "^4.1.2", 42 | "chai-immutable": "^2.0.0-alpha.1", 43 | "chai-spies": "^1.0.0", 44 | "cross-env": "^5.1.4", 45 | "eslint": "^4.19.1", 46 | "eslint-loader": "^2.0.0", 47 | "eslint-plugin-react": "^7.7.0", 48 | "mocha": "^5.0.5", 49 | "nyc": "^11.6.0", 50 | "webpack": "^4.4.1", 51 | "webpack-cli": "^2.0.13" 52 | }, 53 | "dependencies": { 54 | "get-options": "^1.1.1", 55 | "immutable": "^4.0.0-rc.9", 56 | "js-string-escape": "^1.0.1", 57 | "minimatch": "^3.0.4", 58 | "minimatch-capture": "^1.1.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /raw/anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cawfree/react-native-terminal-component/641a3dec7a468a4e6c223e2f6374f9bbfe4bbd8d/raw/anim.gif -------------------------------------------------------------------------------- /src/commands/cat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combines one or more files to display in the terminal output 3 | * Usage: cat file1.txt file2.txt 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as FileOp from 'fs/operations-with-permissions/file-operations'; 7 | import * as OutputFactory from 'emulator-output/output-factory'; 8 | import { resolvePath } from 'emulator-state/util'; 9 | 10 | const fileToTextOutput = (fs, filePath) => { 11 | const {err, file} = FileOp.readFile(fs, filePath); 12 | 13 | if (err) { 14 | return OutputFactory.makeErrorOutput(err); 15 | }; 16 | 17 | return OutputFactory.makeTextOutput(file.get('content')); 18 | }; 19 | 20 | export const optDef = {}; 21 | 22 | export default (state, commandOptions) => { 23 | const {argv} = parseOptions(commandOptions, optDef); 24 | 25 | if (argv.length === 0) { 26 | return {}; 27 | } 28 | 29 | const filePaths = argv.map(pathArg => resolvePath(state, pathArg)); 30 | 31 | return { 32 | outputs: filePaths.map(path => fileToTextOutput(state.getFileSystem(), path)) 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/commands/cd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Changes the current working directory to another directory 3 | * Usage: cd /newDirectory 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as DirectoryOp from 'fs/operations-with-permissions/directory-operations'; 7 | import * as EnvVariableUtil from 'emulator-state/environment-variables'; 8 | import * as OutputFactory from 'emulator-output/output-factory'; 9 | import { makeError, fsErrorType } from 'fs/fs-error'; 10 | import { resolvePath } from 'emulator-state/util'; 11 | 12 | const updateStateCwd = (state, newCwdPath) => { 13 | return EnvVariableUtil.setEnvironmentVariable( 14 | state.getEnvVariables(), 'cwd', newCwdPath 15 | ); 16 | }; 17 | 18 | export const optDef = {}; 19 | 20 | export default (state, commandOptions) => { 21 | const {argv} = parseOptions(commandOptions, optDef); 22 | const newCwdPath = argv[0] ? resolvePath(state, argv[0]) : '/'; 23 | 24 | if (!DirectoryOp.hasDirectory(state.getFileSystem(), newCwdPath)) { 25 | const newCwdPathDoesNotExistErr = makeError(fsErrorType.NO_SUCH_DIRECTORY); 26 | 27 | return { 28 | output: OutputFactory.makeErrorOutput(newCwdPathDoesNotExistErr) 29 | }; 30 | } 31 | 32 | return { 33 | state: state.setEnvVariables( 34 | updateStateCwd(state, newCwdPath) 35 | ) 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/commands/clear.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes all terminal output 3 | * Usage: clear 4 | */ 5 | import { create as createOutputs } from 'emulator-state/outputs'; 6 | 7 | export const optDef = {}; 8 | 9 | export default (state, commandOptions) => { 10 | return { 11 | state: state.setOutputs(createOutputs()) 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/commands/cp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copies a file/directory to another file/directory 3 | * Usage: cp file new-file 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as FileOp from 'fs/operations-with-permissions/file-operations'; 7 | import * as DirectoryOp from 'fs/operations-with-permissions/directory-operations'; 8 | import * as PathUtil from 'fs/util/path-util'; 9 | import * as OutputFactory from 'emulator-output/output-factory'; 10 | import * as FileUtil from 'fs/util/file-util'; 11 | import { makeError, fsErrorType } from 'fs/fs-error'; 12 | import { resolvePath } from 'emulator-state/util'; 13 | 14 | /** 15 | * Copy from a source file into a directory or another file. 16 | * 17 | * A trailing slash / can be used in the destination to explicitly state the 18 | * destination is a directory and not a file. 19 | * @param {Map} state emulator state 20 | * @param {string} srcPath source file path 21 | * @param {string} destPath destination file or destination directory path 22 | * @param {Boolean} isTrailingPathDest true if the destPath ended in a / 23 | * @return {object} cp command return object 24 | */ 25 | const copySourceFile = (state, srcPath, destPath, isTrailingPathDest) => { 26 | const fs = state.getFileSystem(); 27 | 28 | if (isTrailingPathDest && !DirectoryOp.hasDirectory(fs, destPath)) { 29 | const dirAtTrailingPathNonExistentErr = makeError(fsErrorType.NO_SUCH_DIRECTORY); 30 | 31 | return { 32 | output: OutputFactory.makeErrorOutput(dirAtTrailingPathNonExistentErr) 33 | }; 34 | } 35 | 36 | const {fs: copiedFS, err} = FileOp.copyFile(fs, srcPath, destPath); 37 | 38 | if (err) { 39 | return { 40 | output: OutputFactory.makeErrorOutput(err) 41 | }; 42 | } 43 | 44 | return { 45 | state: state.setFileSystem(copiedFS) 46 | }; 47 | }; 48 | 49 | /** 50 | * Copies a directory into another directory 51 | * 52 | * When the destination path exists, cp copies the source FOLDER into the 53 | * destination. 54 | * 55 | * When the destination DOES NOT exist, cp copies the source FILES into the 56 | * destination. 57 | * @param {Map} state emulator state 58 | * @param {string} srcPath source directory path (copy from) 59 | * @param {string} destPath destination directory path (copy to) 60 | * @return {object} cp command return object 61 | */ 62 | const copySourceDirectory = (state, srcPath, destPath) => { 63 | if (DirectoryOp.hasDirectory(state.getFileSystem(), destPath)) { 64 | const lastPathComponent = PathUtil.getLastPathPart(srcPath); 65 | 66 | // Remap dest to copy source FOLDER, as destination path exists 67 | if (lastPathComponent !== '/') { 68 | destPath = `${destPath}/${lastPathComponent}`; 69 | } 70 | } 71 | 72 | // Make directory to copy into, if it doesn't already exist 73 | if (!DirectoryOp.hasDirectory(state.getFileSystem(), destPath)) { 74 | const emptyDir = FileUtil.makeDirectory(); 75 | const {fs, err} = DirectoryOp.addDirectory(state.getFileSystem(), destPath, emptyDir, false); 76 | 77 | state = state.setFileSystem(fs); 78 | 79 | if (err) { 80 | return { 81 | output: OutputFactory.makeErrorOutput(err) 82 | }; 83 | } 84 | } 85 | 86 | const {fs, err} = DirectoryOp.copyDirectory(state.getFileSystem(), srcPath, destPath); 87 | 88 | if (err) { 89 | return { 90 | output: OutputFactory.makeErrorOutput(err) 91 | }; 92 | } 93 | 94 | return { 95 | state: state.setFileSystem(fs) 96 | }; 97 | }; 98 | 99 | export const optDef = { 100 | '-r, --recursive': '' // required to copy directories 101 | }; 102 | 103 | export default (state, commandOptions) => { 104 | const {argv, options} = parseOptions(commandOptions, optDef); 105 | 106 | if (argv.length < 2) { 107 | return {}; 108 | } 109 | 110 | const srcPath = resolvePath(state, argv[0]); 111 | const destPath = resolvePath(state, argv[1]); 112 | const isTrailingDestPath = PathUtil.isTrailingPath(argv[1]); 113 | 114 | if (srcPath === destPath) { 115 | return { 116 | output: OutputFactory.makeTextOutput('Source and destination are the same (not copied).') 117 | }; 118 | } 119 | 120 | if (options.recursive) { 121 | return copySourceDirectory(state, srcPath, destPath); 122 | } 123 | 124 | return copySourceFile(state, srcPath, destPath, isTrailingDestPath); 125 | }; 126 | -------------------------------------------------------------------------------- /src/commands/echo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints arguments to text output 3 | * Usage: echo 'hello world' 4 | */ 5 | import * as OutputFactory from 'emulator-output/output-factory'; 6 | import { getEnvironmentVariable } from 'emulator-state/environment-variables'; 7 | 8 | const VARIABLE_GROUP_REGEX = /\$(\w+)/g; 9 | const DOUBLE_SPACE_REGEX = /\s\s+/g; 10 | 11 | const substituteEnvVariables = (environmentVariables, inputStr) => { 12 | return inputStr.replace(VARIABLE_GROUP_REGEX, (match, varName) => 13 | getEnvironmentVariable(environmentVariables, varName) || '' 14 | ); 15 | }; 16 | 17 | export const optDef = {}; 18 | 19 | export default (state, commandOptions) => { 20 | const input = commandOptions.join(' '); 21 | const outputStr = substituteEnvVariables( 22 | state.getEnvVariables(), input 23 | ); 24 | const cleanStr = outputStr.trim().replace(DOUBLE_SPACE_REGEX, ' '); 25 | 26 | return { 27 | output: OutputFactory.makeTextOutput(cleanStr) 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/commands/head.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints the first n lines of a file 3 | * Usage: head -n 5 file.txt 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as OutputFactory from 'emulator-output/output-factory'; 7 | import { trimFileContent } from 'commands/util/_head_tail_util.js'; 8 | import { resolvePath } from 'emulator-state/util'; 9 | 10 | export const optDef = { 11 | '-n, --lines': '' 12 | }; 13 | 14 | export default (state, commandOptions) => { 15 | const {argv, options} = parseOptions(commandOptions, optDef); 16 | 17 | if (argv.length === 0) { 18 | return {}; 19 | } 20 | 21 | const filePath = resolvePath(state, argv[0]); 22 | const headTrimmingFn = (lines, lineCount) => lines.slice(0, lineCount); 23 | const {content, err} = trimFileContent( 24 | state.getFileSystem(), filePath, options, headTrimmingFn 25 | ); 26 | 27 | if (err) { 28 | return { 29 | output: OutputFactory.makeErrorOutput(err) 30 | }; 31 | } 32 | 33 | return { 34 | output: OutputFactory.makeTextOutput(content) 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/commands/history.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lists or clears commands executed in the terminal 3 | * Usage: history -c 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as OutputFactory from 'emulator-output/output-factory'; 7 | import { create as createHistory } from 'emulator-state/history'; 8 | 9 | const clearStateHistory = (state) => 10 | state.setHistory(createHistory()); 11 | 12 | const stringifyStateHistory = (state) => 13 | state.getHistory().join('\n'); 14 | 15 | export const optDef = { 16 | '-c, --clear': '' // remove history entries 17 | }; 18 | 19 | export default (state, commandOptions) => { 20 | const {options} = parseOptions(commandOptions, optDef); 21 | 22 | if (options.clear) { 23 | return { 24 | state: clearStateHistory(state) 25 | }; 26 | }; 27 | 28 | return { 29 | output: OutputFactory.makeTextOutput(stringifyStateHistory(state)) 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/commands/index.js: -------------------------------------------------------------------------------- 1 | export const commandNames = [ 2 | 'cat', 3 | 'cd', 4 | 'clear', 5 | 'cp', 6 | 'echo', 7 | 'head', 8 | 'history', 9 | 'ls', 10 | 'mkdir', 11 | 'printenv', 12 | 'pwd', 13 | 'rm', 14 | 'rmdir', 15 | 'tail', 16 | 'touch', 17 | 'whoami' 18 | ]; 19 | 20 | export default commandNames.reduce((mapping, commandName) => { 21 | return { 22 | ...mapping, 23 | [commandName]: { 24 | function: require(`commands/${commandName}`).default, 25 | optDef: require(`commands/${commandName}`).optDef 26 | } 27 | }; 28 | }, {}); 29 | -------------------------------------------------------------------------------- /src/commands/ls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lists the contents of a directory 3 | * Usage: ls /folderName 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as DirectoryOp from 'fs/operations-with-permissions/directory-operations'; 7 | import * as EnvVariableUtil from 'emulator-state/environment-variables'; 8 | import * as PathUtil from 'fs/util/path-util'; 9 | import * as OutputFactory from 'emulator-output/output-factory'; 10 | import { Seq } from 'immutable'; 11 | 12 | const IMPLIED_DIRECTORY_ENTRIES = Seq(['.', '..']); // . = listed folder, .. = parent folder 13 | 14 | /** 15 | * Finds the directory path to list entries in. 16 | * 17 | * If ls has an argument passed in (example: ls /home/user/directory-to-list), 18 | * use the first argument as the directory to list. 19 | * 20 | * If ls is used without any path arguments (example: ls), the cwd (current 21 | * working directory) should be listed by ls. 22 | * @param {Map} envVariables environment variables 23 | * @param {array} argv argument vector 24 | * @return {string} directory path to list 25 | */ 26 | const resolveDirectoryToList = (envVariables, argv) => { 27 | const cwd = EnvVariableUtil.getEnvironmentVariable(envVariables, 'cwd'); 28 | 29 | if (argv.length > 0) { 30 | return PathUtil.toAbsolutePath(argv[0], cwd); 31 | } 32 | 33 | return cwd; 34 | }; 35 | 36 | /** 37 | * Alphabetically sorts the ls listing for display to the user 38 | * @param {array} listing list of files/directories to present to the user 39 | * @return {object} return object of ls 40 | */ 41 | const makeSortedReturn = (listing) => { 42 | const sortedListing = listing.sort(); 43 | 44 | return { 45 | output: OutputFactory.makeTextOutput(sortedListing.join('\n')) 46 | }; 47 | }; 48 | 49 | const removeHiddenFilesFilter = (record) => { 50 | return !record.startsWith('.'); 51 | }; 52 | 53 | export const optDef = { 54 | '-a, --all': '', // Include hidden directory entries starting with . 55 | '-A, --almost-all': '' // Do not include . and .. as implied directory entries 56 | }; 57 | 58 | export default (state, commandOptions) => { 59 | const {options, argv} = parseOptions(commandOptions, optDef); 60 | const dirPath = resolveDirectoryToList(state.getEnvVariables(), argv); 61 | const {err, list: dirList} = DirectoryOp.listDirectory(state.getFileSystem(), dirPath); 62 | 63 | if (err) { 64 | return { 65 | output: OutputFactory.makeErrorOutput(err) 66 | }; 67 | } 68 | 69 | if (options.all) { 70 | return makeSortedReturn(IMPLIED_DIRECTORY_ENTRIES.concat(dirList)); 71 | } else if (options.almostAll) { 72 | return makeSortedReturn(dirList); 73 | } 74 | 75 | return makeSortedReturn(dirList.filter(removeHiddenFilesFilter)); 76 | }; 77 | -------------------------------------------------------------------------------- /src/commands/mkdir.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an empty directory 3 | * Usage: mkdir /newDir 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as DirOp from 'fs/operations-with-permissions/directory-operations'; 7 | import * as OutputFactory from 'emulator-output/output-factory'; 8 | import * as FileUtil from 'fs/util/file-util'; 9 | import { resolvePath } from 'emulator-state/util'; 10 | 11 | const EMPTY_DIR = FileUtil.makeDirectory(); 12 | 13 | export const optDef = {}; 14 | 15 | export default (state, commandOptions) => { 16 | const {argv} = parseOptions(commandOptions, optDef); 17 | 18 | if (argv.length === 0) { 19 | return {}; // do nothing if no arguments are given 20 | } 21 | 22 | const newFolderPath = resolvePath(state, argv[0]); 23 | const {fs, err} = DirOp.addDirectory(state.getFileSystem(), newFolderPath, EMPTY_DIR, false); 24 | 25 | if (err) { 26 | return { 27 | output: OutputFactory.makeErrorOutput(err) 28 | }; 29 | } 30 | 31 | return { 32 | state: state.setFileSystem(fs) 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/commands/printenv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints environment variable values 3 | * Usage: printenv cwd 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as OutputFactory from 'emulator-output/output-factory'; 7 | import { getEnvironmentVariable } from 'emulator-state/environment-variables'; 8 | 9 | // Converts all key-value pairs of the environment variables to a printable format 10 | const stringifyEnvVariables = (envVariables) => { 11 | const outputs = envVariables.reduce((outputs, varVal, varKey) => [ 12 | ...outputs, `${varKey}=${varVal}` 13 | ], []); 14 | 15 | return outputs.join('\n'); 16 | }; 17 | 18 | export const optDef = {}; 19 | 20 | export default (state, commandOptions) => { 21 | const {argv} = parseOptions(commandOptions, optDef); 22 | const envVariables = state.getEnvVariables(); 23 | 24 | if (argv.length === 0) { 25 | return { 26 | output: OutputFactory.makeTextOutput(stringifyEnvVariables(envVariables)) 27 | }; 28 | } 29 | 30 | // An argument has been passed to printenv; printenv will only print the first 31 | // argument provided 32 | const varValue = getEnvironmentVariable(envVariables, argv[0]); 33 | 34 | if (varValue) { 35 | return { 36 | output: OutputFactory.makeTextOutput(varValue) 37 | }; 38 | } 39 | 40 | return {}; 41 | }; 42 | -------------------------------------------------------------------------------- /src/commands/pwd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints out the current working directory (cwd). 3 | * Usage: pwd 4 | */ 5 | import * as OutputFactory from 'emulator-output/output-factory'; 6 | import { getEnvironmentVariable } from 'emulator-state/environment-variables'; 7 | 8 | export const optDef = {}; 9 | 10 | export default (state, commandOptions) => { 11 | return { 12 | output: OutputFactory.makeTextOutput( 13 | getEnvironmentVariable(state.getEnvVariables(), 'cwd') 14 | ) 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/commands/rm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes a directory or a file 3 | * Usage: rm /existingDir 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as FileOp from 'fs/operations-with-permissions/file-operations'; 7 | import * as DirOp from 'fs/operations-with-permissions/directory-operations'; 8 | import * as OutputFactory from 'emulator-output/output-factory'; 9 | import { resolvePath } from 'emulator-state/util'; 10 | import { makeError, fsErrorType } from 'fs/fs-error'; 11 | 12 | export const optDef = { 13 | '--no-preserve-root, --noPreserveRoot': '', 14 | '-r, --recursive': '' 15 | }; 16 | 17 | const makeNoPathErrorOutput = () => { 18 | const noSuchFileOrDirError = makeError(fsErrorType.NO_SUCH_FILE_OR_DIRECTORY); 19 | 20 | return { 21 | output: OutputFactory.makeErrorOutput(noSuchFileOrDirError) 22 | }; 23 | }; 24 | 25 | export default (state, commandOptions) => { 26 | const {argv, options} = parseOptions(commandOptions, optDef); 27 | 28 | if (argv.length === 0) { 29 | return {}; // do nothing if no arguments are given 30 | } 31 | 32 | const deletionPath = resolvePath(state, argv[0]); 33 | const fs = state.getFileSystem(); 34 | 35 | if (deletionPath === '/' && options.noPreserveRoot !== true) { 36 | return {}; // do nothing as cannot safely delete the root 37 | } 38 | 39 | if (!fs.has(deletionPath)) { 40 | return makeNoPathErrorOutput(); 41 | } 42 | 43 | const {fs: deletedPathFS, err} = options.recursive === true ? 44 | DirOp.deleteDirectory(fs, deletionPath, true) : 45 | FileOp.deleteFile(fs, deletionPath); 46 | 47 | if (err) { 48 | return { 49 | output: OutputFactory.makeErrorOutput(err) 50 | }; 51 | } 52 | 53 | return { 54 | state: state.setFileSystem(deletedPathFS) 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /src/commands/rmdir.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes an empty directory 3 | * Usage: rmdir /emptyDir 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as DirOp from 'fs/operations-with-permissions/directory-operations'; 7 | import * as OutputFactory from 'emulator-output/output-factory'; 8 | import { resolvePath } from 'emulator-state/util'; 9 | 10 | export const optDef = {}; 11 | 12 | export default (state, commandOptions) => { 13 | const {argv} = parseOptions(commandOptions, optDef); 14 | 15 | if (argv.length === 0) { 16 | return {}; // do nothing if no arguments are given 17 | } 18 | 19 | const pathToDelete = resolvePath(state, argv[0]); 20 | const {fs, err} = DirOp.deleteDirectory(state.getFileSystem(), pathToDelete, false); 21 | 22 | if (err) { 23 | return { 24 | output: OutputFactory.makeErrorOutput(err) 25 | }; 26 | } 27 | 28 | return { 29 | state: state.setFileSystem(fs) 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/commands/tail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints the last n lines of a file 3 | * Usage: tail -n 5 file.txt 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as OutputFactory from 'emulator-output/output-factory'; 7 | import { trimFileContent } from 'commands/util/_head_tail_util.js'; 8 | import { resolvePath } from 'emulator-state/util'; 9 | 10 | export const optDef = { 11 | '-n, --lines': '' 12 | }; 13 | 14 | export default (state, commandOptions) => { 15 | const {argv, options} = parseOptions(commandOptions, optDef); 16 | 17 | if (argv.length === 0) { 18 | return {}; 19 | } 20 | 21 | const filePath = resolvePath(state, argv[0]); 22 | const tailTrimmingFn = (lines, lineCount) => lines.slice(-1 * lineCount); 23 | const {content, err} = trimFileContent( 24 | state.getFileSystem(), filePath, options, tailTrimmingFn 25 | ); 26 | 27 | if (err) { 28 | return { 29 | output: OutputFactory.makeErrorOutput(err) 30 | }; 31 | } 32 | 33 | return { 34 | output: OutputFactory.makeTextOutput(content) 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/commands/touch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an empty file. 3 | * Usage: touch new_file.txt 4 | */ 5 | import parseOptions from 'parser/option-parser'; 6 | import * as FileOp from 'fs/operations-with-permissions/file-operations'; 7 | import * as OutputFactory from 'emulator-output/output-factory'; 8 | import * as FileUtil from 'fs/util/file-util'; 9 | import { resolvePath } from 'emulator-state/util'; 10 | 11 | const EMPTY_FILE = FileUtil.makeFile(); 12 | 13 | export const optDef = {}; 14 | 15 | export default (state, commandOptions) => { 16 | const {argv} = parseOptions(commandOptions, optDef); 17 | 18 | if (argv.length === 0) { 19 | return {}; // do nothing if no arguments are given 20 | } 21 | 22 | const filePath = resolvePath(state, argv[0]); 23 | 24 | if (state.getFileSystem().has(filePath)) { 25 | return {}; // do nothing if already has a file at the provided path 26 | } 27 | 28 | const {fs, err} = FileOp.writeFile(state.getFileSystem(), filePath, EMPTY_FILE); 29 | 30 | if (err) { 31 | return { 32 | output: OutputFactory.makeErrorOutput(err) 33 | }; 34 | } 35 | 36 | return { 37 | state: state.setFileSystem(fs) 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/commands/util/_head_tail_util.js: -------------------------------------------------------------------------------- 1 | import * as FileOp from 'fs/operations-with-permissions/file-operations'; 2 | import * as OutputFactory from 'emulator-output/output-factory'; 3 | 4 | const DEFAULT_LINE_COUNT = 10; 5 | 6 | export const trimFileContent = (fs, filePath, options, trimmingFn) => { 7 | const {file, err} = FileOp.readFile(fs, filePath); 8 | 9 | if (err) { 10 | return { 11 | err: OutputFactory.makeErrorOutput(err) 12 | }; 13 | }; 14 | 15 | const linesCount = options.lines ? Number(options.lines) : DEFAULT_LINE_COUNT; 16 | const trimmedLines = trimmingFn(file.get('content').split('\n'), linesCount); 17 | 18 | return { 19 | content: trimmedLines.join('\n') 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/commands/whoami.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints the username of the logged in user 3 | * Usage: whoami 4 | */ 5 | import * as OutputFactory from 'emulator-output/output-factory'; 6 | import { getEnvironmentVariable } from 'emulator-state/environment-variables'; 7 | 8 | const FALLBACK_USERNAME = 'root'; 9 | 10 | export const optDef = {}; 11 | 12 | export default (state, commandOptions) => { 13 | return { 14 | output: OutputFactory.makeTextOutput( 15 | getEnvironmentVariable(state.getEnvVariables(), 'user') || FALLBACK_USERNAME 16 | ) 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/emulator-output/index.js: -------------------------------------------------------------------------------- 1 | import * as OutputFactory from 'emulator-output/output-factory'; 2 | import * as OutputType from 'emulator-output/output-type'; 3 | 4 | export default { 5 | OutputFactory, OutputType 6 | }; 7 | -------------------------------------------------------------------------------- /src/emulator-output/output-factory.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | import { HEADER_OUTPUT_TYPE, TEXT_OUTPUT_TYPE, TEXT_ERROR_OUTPUT_TYPE } from 'emulator-output/output-type'; 3 | 4 | /** 5 | * Output from a command or emulator used for display to the user 6 | * @type {OutputRecord} 7 | */ 8 | export const OutputRecord = Record({ 9 | type: undefined, 10 | content: undefined 11 | }); 12 | 13 | /** 14 | * A terminal header containing metadata 15 | * @param {string} cwd the current working directory path 16 | * @return {OutputRecord} output record 17 | */ 18 | export const makeHeaderOutput = (cwd, command) => { 19 | return new OutputRecord({ 20 | type: HEADER_OUTPUT_TYPE, 21 | content: { cwd, command } 22 | }); 23 | }; 24 | 25 | /** 26 | * Unstyled text output 27 | * @param {string} content plain string output from a command or the emulator 28 | * @return {OutputRecord} output record 29 | */ 30 | export const makeTextOutput = (content) => { 31 | return new OutputRecord({ 32 | type: TEXT_OUTPUT_TYPE, 33 | content 34 | }); 35 | }; 36 | 37 | /** 38 | * Error text output 39 | * @param {object} err internal error object 40 | * @return {OutputRecord} output record 41 | */ 42 | export const makeErrorOutput = (err) => { 43 | return new OutputRecord({ 44 | type: TEXT_ERROR_OUTPUT_TYPE, 45 | content: `${err.source}: ${err.type}` 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/emulator-output/output-type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Types of output which can be used to display content to the user 3 | * @type {String} 4 | */ 5 | export const TEXT_OUTPUT_TYPE = 'TEXT_OUTPUT'; 6 | export const TEXT_ERROR_OUTPUT_TYPE = 'TEXT_ERROR_OUTPUT'; 7 | export const HEADER_OUTPUT_TYPE = 'HEADER_OUTPUT_TYPE'; 8 | -------------------------------------------------------------------------------- /src/emulator-state/EmulatorState.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { create as createCommandMapping } from 'emulator-state/command-mapping'; 3 | import { create as createEnvironmentVariables } from 'emulator-state/environment-variables'; 4 | import { create as createFileSystem } from 'emulator-state/file-system'; 5 | import { create as createHistory } from 'emulator-state/history'; 6 | import { create as createOutputs } from 'emulator-state/outputs'; 7 | 8 | const FS_KEY = 'fs'; 9 | const ENVIRONMENT_VARIABLES_KEY = 'environmentVariables'; 10 | const HISTORY_KEY = 'history'; 11 | const OUTPUTS_KEY = 'outputs'; 12 | const COMMAND_MAPPING_KEY = 'commandMapping'; 13 | 14 | export default class EmulatorState { 15 | constructor(immutable) { 16 | if (!immutable || !(immutable instanceof Map)) { 17 | throw new Error('Do not use the constructor directly. Use the static create method.'); 18 | } 19 | 20 | this._immutable = immutable; 21 | } 22 | 23 | /** 24 | * Creates emulator state with defaults 25 | * @return {EmulatorState} default emulator state 26 | */ 27 | static createEmpty() { 28 | return EmulatorState.create({}); 29 | } 30 | 31 | /** 32 | * Creates emulator state using the user's state components, or a default 33 | * fallback if none is provided 34 | * @param {object} optionally contains each component as a key and the component as a value 35 | * @return {EmulatorState} emulator state 36 | */ 37 | static create({ 38 | fs = createFileSystem(), 39 | environmentVariables = createEnvironmentVariables(), 40 | history = createHistory(), 41 | outputs = createOutputs(), 42 | commandMapping = createCommandMapping() 43 | }) { 44 | const stateMap = new Map({ 45 | [FS_KEY]: fs, 46 | [ENVIRONMENT_VARIABLES_KEY]: environmentVariables, 47 | [HISTORY_KEY]: history, 48 | [OUTPUTS_KEY]: outputs, 49 | [COMMAND_MAPPING_KEY]: commandMapping 50 | }); 51 | 52 | return new EmulatorState(stateMap); 53 | } 54 | 55 | getFileSystem() { 56 | return this.getImmutable().get(FS_KEY); 57 | } 58 | 59 | setFileSystem(newFileSystem) { 60 | return new EmulatorState( 61 | this.getImmutable().set(FS_KEY, newFileSystem) 62 | ); 63 | } 64 | 65 | getEnvVariables() { 66 | return this.getImmutable().get(ENVIRONMENT_VARIABLES_KEY); 67 | } 68 | 69 | setEnvVariables(newEnvVariables) { 70 | return new EmulatorState( 71 | this.getImmutable().set(ENVIRONMENT_VARIABLES_KEY, newEnvVariables) 72 | ); 73 | } 74 | 75 | getHistory() { 76 | return this.getImmutable().get(HISTORY_KEY); 77 | } 78 | 79 | setHistory(newHistory) { 80 | return new EmulatorState( 81 | this.getImmutable().set(HISTORY_KEY, newHistory) 82 | ); 83 | } 84 | 85 | getOutputs() { 86 | return this.getImmutable().get(OUTPUTS_KEY); 87 | } 88 | 89 | setOutputs(newOutputs) { 90 | return new EmulatorState( 91 | this.getImmutable().set(OUTPUTS_KEY, newOutputs) 92 | ); 93 | } 94 | 95 | getCommandMapping() { 96 | return this.getImmutable().get(COMMAND_MAPPING_KEY); 97 | } 98 | 99 | setCommandMapping(newCommandMapping) { 100 | return new EmulatorState( 101 | this.getImmutable().set(COMMAND_MAPPING_KEY, newCommandMapping) 102 | ); 103 | } 104 | 105 | getImmutable() { 106 | return this._immutable; 107 | } 108 | 109 | toJS() { 110 | return this._immutable.toJS(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/emulator-state/command-mapping.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import defaultCommandMapping from 'commands'; 3 | 4 | /** 5 | * Links a command name to a function 6 | * @param {Object} [commandMapping={}] default command map 7 | * @return {Map} command mapping 8 | */ 9 | export const create = (commandMapping = defaultCommandMapping) => { 10 | for (const commandName of Object.keys(commandMapping)) { 11 | const command = commandMapping[commandName]; 12 | 13 | if (!command.hasOwnProperty('function')) { 14 | throw new Error(`Failed to create command mapping: missing command function for ${commandName}`); 15 | } 16 | 17 | if (!command.hasOwnProperty('optDef')) { 18 | throw new Error(`Failed to create command mapping: missing option definition (optDef) for ${commandName}`); 19 | } 20 | } 21 | 22 | return fromJS(commandMapping); 23 | }; 24 | 25 | /** 26 | * Checks if a comand has been defined with a function in the command mapping 27 | * @param {Map} commandMapping command mapping 28 | * @param {string} commandName command name to check if available 29 | * @return {Boolean} true, if the command is available 30 | */ 31 | export const isCommandSet = (commandMapping, commandName) => { 32 | return commandMapping.has(commandName); 33 | }; 34 | 35 | /** 36 | * Set a command function with a key of the command name into the command mapping 37 | * @param {Map} commandMapping command mapping 38 | * @param {string} commandName name of the function 39 | * @param {function} commandFn command function 40 | * @param {object} optDef option definition (optional) 41 | * @return {Map} command mapping 42 | */ 43 | export const setCommand = (commandMapping, commandName, commandFn, optDef) => { 44 | if (commandFn === undefined) { 45 | throw new Error(`Cannot set ${commandName} command without function`); 46 | } 47 | 48 | if (optDef === undefined) { 49 | throw new Error(`Cannot set ${commandName} command without optDef (pass in {} if the command takes no options)`); 50 | } 51 | 52 | return commandMapping.set(commandName, fromJS({ 53 | 'function': commandFn, 54 | 'optDef': optDef 55 | })); 56 | }; 57 | 58 | /** 59 | * Removes a command name and its function from a command mapping 60 | * @param {Map} commandMapping command mapping 61 | * @param {string} commandName name of command to remove 62 | * @return {Map} command mapping 63 | */ 64 | export const unsetCommand = (commandMapping, commandName) => { 65 | return commandMapping.delete(commandName); 66 | }; 67 | 68 | /** 69 | * Gets the function of a command based on its command name (the key) from the 70 | * command mapping 71 | * @param {Map} commandMapping command mapping 72 | * @param {string} commandName name of command 73 | * @return {function} command function 74 | */ 75 | export const getCommandFn = (commandMapping, commandName) => { 76 | if (commandMapping.has(commandName)) { 77 | return commandMapping.get(commandName).get('function'); 78 | } 79 | 80 | return undefined; 81 | }; 82 | 83 | /** 84 | * Gets the option definition of a command based on its command name 85 | * @param {Map} commandMapping command mapping 86 | * @param {string} commandName name of command 87 | * @return {Map} option definition 88 | */ 89 | export const getCommandOptDef = (commandMapping, commandName) => { 90 | if (commandMapping.has(commandName)) { 91 | return commandMapping.get(commandName).get('optDef'); 92 | } 93 | 94 | return undefined; 95 | }; 96 | 97 | /** 98 | * Gets command names 99 | * @param {Map} commandMapping command mapping 100 | * @return {Seq} sequence of command names 101 | */ 102 | export const getCommandNames = (commandMapping) => { 103 | return commandMapping.keySeq(); 104 | }; 105 | -------------------------------------------------------------------------------- /src/emulator-state/environment-variables.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | /** 4 | * Environment variable mapping containing arbitary data accessed by any 5 | * command or the emulator as a key-value pair 6 | * @param {Object} [defaultVariables={}] default environment variables 7 | * @return {Map} environment variables 8 | */ 9 | export const create = (defaultVariables = {}, cwd = '/') => { 10 | if (!cwd && !defaultVariables.hasOwnProperty('cwd')) { 11 | throw new Error( 12 | "Failed to create environment variables. Missing 'cwd' (current working directory)." 13 | ); 14 | } 15 | 16 | return Map({ 17 | 'cwd': cwd, // cwd can be undefined as it can be set in defaultVariables 18 | ...defaultVariables 19 | }); 20 | }; 21 | 22 | /** 23 | * Gets the value of an environment variable 24 | * @param {Map} environmentVariables environment variables 25 | * @param {string} key name of the environment variable 26 | * @return {T} the value stored in the environment variable 27 | */ 28 | export const getEnvironmentVariable = (environmentVariables, key) => { 29 | return environmentVariables.get(key); 30 | }; 31 | 32 | /** 33 | * Sets the value of an environment variable 34 | * @param {Map} environmentVariables environment variables 35 | * @param {string} key name of the environment variable 36 | * @param {T} val value to store in the environment variable 37 | * @return {Map} environment variables 38 | */ 39 | export const setEnvironmentVariable = (environmentVariables, key, val) => { 40 | return environmentVariables.set(key, val); 41 | }; 42 | 43 | /** 44 | * Removes an environment variable 45 | * @param {Map} environmentVariables environment variables 46 | * @param {string} key name of the environment variable 47 | * @return {Map} environment variables 48 | */ 49 | export const unsetEnvironmentVariable = (environmentVariables, key) => { 50 | return environmentVariables.delete(key); 51 | }; 52 | -------------------------------------------------------------------------------- /src/emulator-state/file-system.js: -------------------------------------------------------------------------------- 1 | import * as FileUtil from 'fs/util/file-util'; 2 | import * as DirOp from 'fs/operations/directory-operations'; 3 | import { fromJS } from 'immutable'; 4 | 5 | const DEFAULT_FILE_SYSTEM = { 6 | '/': FileUtil.makeDirectory() 7 | }; 8 | 9 | /** 10 | * Creates an immutable data structure for a file system 11 | * @param {object} jsFs a file system in a simple JavaScript object 12 | * @return {Map} an immutable file system 13 | */ 14 | export const create = (jsFs = DEFAULT_FILE_SYSTEM) => { 15 | return DirOp.fillGaps(fromJS(jsFs)); 16 | }; 17 | -------------------------------------------------------------------------------- /src/emulator-state/history.js: -------------------------------------------------------------------------------- 1 | import { Stack } from 'immutable'; 2 | 3 | /** 4 | * Creates a new history stack of previous commands that have been run in the 5 | * emulator 6 | * @param {array} [entries=[]] commands which have already been run (if any) 7 | * @return {Stack} history list 8 | */ 9 | export const create = (entries = []) => { 10 | return Stack.of(...entries); 11 | }; 12 | 13 | /** 14 | * Stores a command in history in a stack (i.e., the latest command is on top of 15 | * the history stack) 16 | * @param {Stack} history history 17 | * @param {string} commandRun the command to store 18 | * @return {Stack} history 19 | */ 20 | export const recordCommand = (history, commandRun) => { 21 | return history.push(commandRun); 22 | }; 23 | -------------------------------------------------------------------------------- /src/emulator-state/index.js: -------------------------------------------------------------------------------- 1 | import * as CommandMapping from 'emulator-state/command-mapping'; 2 | import * as EnvironmentVariables from 'emulator-state/environment-variables'; 3 | import * as FileSystem from 'emulator-state/file-system'; 4 | import * as History from 'emulator-state/history'; 5 | import * as Outputs from 'emulator-state/outputs'; 6 | import EmulatorState from 'emulator-state/EmulatorState'; 7 | 8 | export default { 9 | EmulatorState, 10 | CommandMapping, 11 | EnvironmentVariables, 12 | FileSystem, 13 | History, 14 | Outputs 15 | }; 16 | -------------------------------------------------------------------------------- /src/emulator-state/outputs.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import { Record } from 'immutable'; 3 | 4 | /** 5 | * Stores outputs from the emulator (e.g. text to display after running a command) 6 | * @param {Array} [outputs=[]] Previous outputs 7 | * @return {List} List of outputs objects 8 | */ 9 | export const create = (outputs = []) => { 10 | return List(outputs); 11 | }; 12 | 13 | /** 14 | * Adds a new output record 15 | * @param {List} outputs outputs list 16 | * @param {OutputRecord} outputRecord record conforming to output schema 17 | */ 18 | export const addRecord = (outputs, outputRecord) => { 19 | if (!Record.isRecord(outputRecord)) { 20 | throw new Error('Only records of type OutputRecord can be added to outputs'); 21 | } 22 | 23 | if (!outputRecord.has('type')) { 24 | throw new Error('Output record must include a type'); 25 | } 26 | 27 | if (!outputRecord.has('content')) { 28 | throw new Error('Output record must include content'); 29 | } 30 | 31 | return outputs.push(outputRecord); 32 | }; 33 | -------------------------------------------------------------------------------- /src/emulator-state/util.js: -------------------------------------------------------------------------------- 1 | import * as EnvVariableUtil from 'emulator-state/environment-variables'; 2 | import * as PathUtil from 'fs/util/path-util'; 3 | 4 | /** 5 | * Converts a given path to an absolute path using the 6 | * current working directory 7 | * @param {EmulatorState} state emulator state 8 | * @param {string} path path (relative or absolute) 9 | * @return {string} absolute path 10 | */ 11 | export const resolvePath = (state, path) => { 12 | const cwd = EnvVariableUtil.getEnvironmentVariable( 13 | state.getEnvVariables(), 'cwd' 14 | ); 15 | 16 | return PathUtil.toAbsolutePath(path, cwd); 17 | }; 18 | -------------------------------------------------------------------------------- /src/emulator/auto-complete.js: -------------------------------------------------------------------------------- 1 | import * as PathUtil from 'fs/util/path-util'; 2 | import * as GlobUtil from 'fs/util/glob-util'; 3 | import { isCommandSet, getCommandNames, getCommandOptDef } from 'emulator-state/command-mapping'; 4 | 5 | /** 6 | * Suggest command names 7 | * @param {Map} cmdMapping command mapping 8 | * @param {string} partialStr partial user input of a command 9 | * @return {array} list of possible text suggestions 10 | */ 11 | export const suggestCommands = (cmdMapping, partialStr) => { 12 | const commandNameSeq = getCommandNames(cmdMapping); 13 | 14 | return [...GlobUtil.globSeq(commandNameSeq, `${partialStr}*`)]; 15 | }; 16 | 17 | /** 18 | * Suggest command options 19 | * @param {Map} cmdMapping command mapping 20 | * @param {string} commandName name of the command user is running 21 | * @param {string} partialStr partial user input of a command (excluding the command name) 22 | * @return {array} list of possible text suggestions 23 | */ 24 | export const suggestCommandOptions = (cmdMapping, commandName, partialStr) => { 25 | if (!isCommandSet(cmdMapping, commandName)) { 26 | return []; 27 | } 28 | 29 | const optDefSeq = getCommandOptDef(cmdMapping, commandName) 30 | .keySeq() 31 | .flatMap(opts => 32 | opts.split(',').map(opt => opt.trim()) 33 | ); 34 | 35 | return [...GlobUtil.globSeq(optDefSeq, `${partialStr}*`)]; 36 | }; 37 | 38 | /** 39 | * Suggest file and folder names from partially completed user input 40 | * @param {Map} fileSystem file system 41 | * @param {string} cwd current working directory 42 | * @param {string} partialStr partial string to base suggestions on (excluding the command name) 43 | * @return {array} list of possible text suggestions 44 | */ 45 | export const suggestFileSystemNames = (fileSystem, cwd, partialStr) => { 46 | const path = PathUtil.toAbsolutePath(partialStr, cwd); 47 | 48 | // complete name of a folder or file 49 | const completeNamePattern = `${path}*`; 50 | // complete child folder name 51 | const completeSubfolderPattern = path === '/' ? '/*' : `${path}*/*`; 52 | // only complete child folders when the path ends with / (which marks a directory path) 53 | const globPattern = partialStr.endsWith('/') ? completeSubfolderPattern : completeNamePattern; 54 | 55 | const childPaths = GlobUtil.globPaths(fileSystem, globPattern); 56 | 57 | if (PathUtil.isAbsPath(partialStr)) { 58 | return [...childPaths]; // absolute paths 59 | } 60 | 61 | return [...childPaths.map(path => { 62 | const pathPartsWithoutTail = PathUtil.toPathParts(partialStr).slice(0, -1); 63 | const newTail = PathUtil.getLastPathPart(path); 64 | 65 | return PathUtil.toPath(pathPartsWithoutTail.concat(newTail)); 66 | })]; // relative paths 67 | }; 68 | -------------------------------------------------------------------------------- /src/emulator/command-runner.js: -------------------------------------------------------------------------------- 1 | import { makeError, emulatorErrorType } from 'emulator/emulator-error'; 2 | import { makeErrorOutput } from 'emulator-output/output-factory'; 3 | import * as CommandMappingUtil from 'emulator-state/command-mapping'; 4 | 5 | /** 6 | * Makes an internal emulator error for emulator output. Error output may be 7 | * visible to the user. 8 | * @param {string} errorType type of emulator error 9 | * @return {object} error output object 10 | */ 11 | const makeRunnerErrorOutput = (errorType) => { 12 | return makeErrorOutput(makeError(errorType)); 13 | }; 14 | 15 | /** 16 | * Runs a command and returns an object containing either: 17 | * - outputs from running the command, or 18 | * - new emulator state after running the command, or 19 | * - new emulator state and output after running the command 20 | * 21 | * The form of the object from this function is as follows: 22 | * { 23 | * outputs: [optional array of output records] 24 | * output: [optional single output record] 25 | * state: [optional Map] 26 | * } 27 | * @param {Map} commandMapping command mapping from emulator state 28 | * @param {string} commandName name of command to run 29 | * @param {array} commandArgs commands to provide to the command function 30 | * @param {function} notFoundCallback a default function to be run if no command is found 31 | * @return {object} outputs and/or new state of the emulator 32 | */ 33 | export const run = (commandMapping, commandName, commandArgs, notFoundCallback = () => ({ 34 | output: makeRunnerErrorOutput(emulatorErrorType.COMMAND_NOT_FOUND) 35 | })) => { 36 | if (!CommandMappingUtil.isCommandSet(commandMapping, commandName)) { 37 | return notFoundCallback(...commandArgs); 38 | } 39 | 40 | const command = CommandMappingUtil.getCommandFn(commandMapping, commandName); 41 | 42 | try { 43 | return command(...commandArgs); // run extracted command from the mapping 44 | } catch (fatalCommandError) { 45 | return { 46 | output: makeRunnerErrorOutput(emulatorErrorType.UNEXPECTED_COMMAND_FAILURE) 47 | }; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/emulator/emulator-error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Emulator error type 3 | * @type {Object} 4 | */ 5 | export const emulatorErrorType = { 6 | COMMAND_NOT_FOUND: 'Command not found', 7 | UNEXPECTED_COMMAND_FAILURE: 'Unhandled command error' 8 | }; 9 | 10 | /** 11 | * Creates an error to display to the user originating from the emulator 12 | * @param {string} emulatorErrorType file system error type 13 | * @param {string} [message=''] optional metadata for developers about the error 14 | * @return {object} internal error object 15 | */ 16 | export const makeError = (emulatorErrorType, message = '') => { 17 | return { 18 | source: 'emulator', 19 | type: emulatorErrorType, 20 | message 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/emulator/plugins/BoundedHistoryIterator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes a stack iterator for a point in history. 3 | * 4 | * Can go backwards and forwards through the history and is bounded by 5 | * the size of the stack. 6 | */ 7 | export default class BoundedHistoryIterator { 8 | constructor(historyStack, index = 0) { 9 | this.historyStack = historyStack.push(''); 10 | this.index = index; 11 | } 12 | 13 | hasUp() { 14 | return this.index + 1 < this.historyStack.size; 15 | } 16 | 17 | up() { 18 | if (this.hasUp()) { 19 | this.index++; 20 | } 21 | 22 | return this.historyStack.get(this.index); 23 | } 24 | 25 | hasDown() { 26 | return this.index - 1 >= 0; 27 | } 28 | 29 | down() { 30 | if (this.hasDown()) { 31 | this.index--; 32 | } 33 | 34 | return this.historyStack.get(this.index); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/emulator/plugins/HistoryKeyboardPlugin.js: -------------------------------------------------------------------------------- 1 | import BoundedHistoryIterator from 'emulator/plugins/BoundedHistoryIterator'; 2 | 3 | export default class HistoryKeyboardPlugin { 4 | constructor(state) { 5 | this._nullableHistoryIterator = null; 6 | this.historyStack = state.getHistory(); 7 | } 8 | 9 | // Plugin contract 10 | onExecuteStarted(state, str) { 11 | // no-op 12 | } 13 | 14 | // Plugin contract 15 | onExecuteCompleted(state) { 16 | this._nullableHistoryIterator = null; 17 | this.historyStack = state.getHistory(); 18 | } 19 | 20 | // Plugin API 21 | completeUp() { 22 | this.createHistoryIteratorIfNull(); 23 | 24 | return this._nullableHistoryIterator.up(); 25 | } 26 | 27 | completeDown() { 28 | this.createHistoryIteratorIfNull(); 29 | 30 | return this._nullableHistoryIterator.down(); 31 | } 32 | 33 | // Private methods 34 | createHistoryIteratorIfNull() { 35 | if (!this._nullableHistoryIterator) { 36 | this._nullableHistoryIterator = new BoundedHistoryIterator( 37 | this.historyStack 38 | ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/fs/fs-error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File system error types 3 | * @type {Object} 4 | */ 5 | export const fsErrorType = { 6 | FILE_EXISTS: 'File exists', 7 | DIRECTORY_EXISTS: 'Directory exists', 8 | DIRECTORY_NOT_EMPTY: 'Directory not empty', 9 | NO_SUCH_FILE_OR_DIRECTORY: 'No such file or directory', 10 | NO_SUCH_FILE: 'No such file', 11 | NO_SUCH_DIRECTORY: 'No such directory', 12 | FILE_OR_DIRECTORY_EXISTS: 'File or directory exists', 13 | IS_A_DIRECTORY: 'Is a directory', 14 | NOT_A_DIRECTORY: 'Not a directory', 15 | PERMISSION_DENIED: 'Permission denied', 16 | OTHER: 'Other' 17 | }; 18 | 19 | /** 20 | * Create a non-fatal file system error object 21 | * 22 | * For fatal errors do not use this. Throw an error instead. 23 | * @param {string} fsErrorType file system error type 24 | * @param {string} [message=''] optional metadata for developers about the error 25 | * @return {object} internal error object 26 | */ 27 | export const makeError = (fsErrorType, message = '') => { 28 | return { 29 | source: 'fs', 30 | type: fsErrorType, 31 | message 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/fs/index.js: -------------------------------------------------------------------------------- 1 | import * as DirOp from './operations-with-permissions/directory-operations'; 2 | import * as FileOp from './operations-with-permissions/file-operations'; 3 | 4 | export default { 5 | DirOp, 6 | FileOp 7 | }; 8 | -------------------------------------------------------------------------------- /src/fs/operations-with-permissions/directory-operations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds modification permissions to directory operations by wrapping 3 | * directory operations 4 | */ 5 | import * as DirectoryOperations from 'fs/operations/directory-operations'; 6 | import * as PermissionUtil from 'fs/util/permission-util'; 7 | import { makeError, fsErrorType } from 'fs/fs-error'; 8 | 9 | const makeDirectoryOperationPermissionError = (message = 'Cannot modify directory') => { 10 | return { 11 | err: makeError(fsErrorType.PERMISSION_DENIED, message) 12 | }; 13 | }; 14 | 15 | export const hasDirectory = (...args) => { 16 | return DirectoryOperations.hasDirectory(...args); 17 | }; 18 | 19 | export const listDirectory = (...args) => { 20 | return DirectoryOperations.listDirectory(...args); 21 | }; 22 | 23 | export const listDirectoryFiles = (...args) => { 24 | return DirectoryOperations.listDirectoryFiles(...args); 25 | }; 26 | 27 | export const listDirectoryFolders = (...args) => { 28 | return DirectoryOperations.listDirectoryFolders(...args); 29 | }; 30 | 31 | export const addDirectory = (fs, path, ...args) => { 32 | if (!PermissionUtil.canModifyPath(fs, path)) { 33 | return makeDirectoryOperationPermissionError(); 34 | } 35 | 36 | return DirectoryOperations.addDirectory(fs, path, ...args); 37 | }; 38 | 39 | export const copyDirectory = (fs, srcPath, destPath, ...args) => { 40 | if (!PermissionUtil.canModifyPath(fs, srcPath)) { 41 | return makeDirectoryOperationPermissionError('Cannot modify source directory'); 42 | } 43 | 44 | if (!PermissionUtil.canModifyPath(fs, destPath)) { 45 | return makeDirectoryOperationPermissionError('Cannot modify dest directory'); 46 | } 47 | 48 | return DirectoryOperations.copyDirectory(fs, srcPath, destPath, ...args); 49 | }; 50 | 51 | export const deleteDirectory = (fs, path, ...args) => { 52 | if (!PermissionUtil.canModifyPath(fs, path)) { 53 | return makeDirectoryOperationPermissionError(); 54 | } 55 | 56 | return DirectoryOperations.deleteDirectory(fs, path, ...args); 57 | }; 58 | 59 | export const renameDirectory = (fs, currentPath, newPath) => { 60 | if (!PermissionUtil.canModifyPath(fs, currentPath)) { 61 | return makeDirectoryOperationPermissionError('Cannot modify current path'); 62 | } 63 | 64 | if (!PermissionUtil.canModifyPath(fs, newPath)) { 65 | return makeDirectoryOperationPermissionError('Cannot modify renamed path'); 66 | } 67 | 68 | return DirectoryOperations.renameDirectory(fs, currentPath, newPath); 69 | }; 70 | -------------------------------------------------------------------------------- /src/fs/operations-with-permissions/file-operations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds modification permissions to file operations by wrapping 3 | * file operations 4 | */ 5 | import * as PermissionUtil from 'fs/util/permission-util'; 6 | import * as FileOperations from 'fs/operations/file-operations'; 7 | import { makeError, fsErrorType } from 'fs/fs-error'; 8 | 9 | const makeFileOperationPermissionError = (message = 'Cannot modify file') => { 10 | return { 11 | err: makeError(fsErrorType.PERMISSION_DENIED, message) 12 | }; 13 | }; 14 | 15 | export const hasFile = (...args) => { 16 | return FileOperations.hasFile(...args); 17 | }; 18 | 19 | export const readFile = (...args) => { 20 | return FileOperations.readFile(...args); 21 | }; 22 | 23 | export const writeFile = (fs, filePath, ...args) => { 24 | if (!PermissionUtil.canModifyPath(fs, filePath)) { 25 | return makeFileOperationPermissionError(); 26 | } 27 | 28 | return FileOperations.writeFile(fs, filePath, ...args); 29 | }; 30 | 31 | export const copyFile = (fs, sourcePath, destPath) => { 32 | if (!PermissionUtil.canModifyPath(fs, sourcePath)) { 33 | return makeFileOperationPermissionError('Cannot modify source file'); 34 | } 35 | 36 | if (!PermissionUtil.canModifyPath(fs, destPath)) { 37 | return makeFileOperationPermissionError('Cannot modify destination file'); 38 | } 39 | 40 | return FileOperations.copyFile(fs, sourcePath, destPath); 41 | }; 42 | 43 | export const deleteFile = (fs, filePath) => { 44 | if (!PermissionUtil.canModifyPath(fs, filePath)) { 45 | return makeFileOperationPermissionError(); 46 | } 47 | 48 | return FileOperations.deleteFile(fs, filePath); 49 | }; 50 | -------------------------------------------------------------------------------- /src/fs/operations/base-operations.js: -------------------------------------------------------------------------------- 1 | import * as GlobUtil from 'fs/util/glob-util'; 2 | import * as DirOp from 'fs/operations/directory-operations'; 3 | import * as FileOp from 'fs/operations/file-operations'; 4 | import * as PathUtil from 'fs/util/path-util'; 5 | import { makeError, fsErrorType } from 'fs/fs-error'; 6 | 7 | /** 8 | * Adds a file or directory to a path 9 | * @param {Map} fs file system 10 | * @param {string} pathToAdd path to add the file or directory to 11 | * @param {string} fsElementToAdd file or directory map 12 | * @param {Boolean} [addParentPaths=false] true, if path parent directories should 13 | * be made (if they don't exist) 14 | * @return {object} file system or error 15 | */ 16 | export const add = (fs, pathToAdd, fsElementToAdd, addParentPaths = false) => { 17 | if (fs.has(pathToAdd)) { 18 | return { 19 | err: makeError(fsErrorType.FILE_OR_DIRECTORY_EXISTS) 20 | }; 21 | } 22 | 23 | const parentPaths = PathUtil.getPathBreadCrumbs(pathToAdd).slice(0, -1); 24 | 25 | for (const parentPath of parentPaths) { 26 | if (FileOp.hasFile(fs, parentPath)) { 27 | return { 28 | err: makeError(fsErrorType.NOT_A_DIRECTORY, 29 | `Cannot add path to a file: ${parentPath}`) 30 | }; 31 | } 32 | 33 | if (!fs.has(parentPath) && !addParentPaths) { 34 | return { 35 | err: makeError(fsErrorType.NO_SUCH_DIRECTORY, 36 | `Parent directory does not exist: ${parentPath}`) 37 | }; 38 | } 39 | } 40 | 41 | const addedDirectoryFs = fs.set(pathToAdd, fsElementToAdd); 42 | 43 | return { 44 | fs: addParentPaths ? DirOp.fillGaps(addedDirectoryFs) : addedDirectoryFs 45 | }; 46 | }; 47 | 48 | /** 49 | * Removes a file or directory from a path 50 | * @param {Map} fs file system 51 | * @param {string} pathToRemove removes the path 52 | * @param {Boolean} [isNonEmptyDirectoryRemovable=true] true if non-empty paths can be removed 53 | * @return {object} file system or error 54 | */ 55 | export const remove = (fs, pathToRemove, isNonEmptyDirectoryRemovable = true) => { 56 | if (!fs.has(pathToRemove)) { 57 | return { 58 | err: makeError(fsErrorType.NO_SUCH_FILE_OR_DIRECTORY) 59 | }; 60 | } 61 | 62 | const childPathPattern = pathToRemove === '/' ? '/**' : `${pathToRemove}/**`; 63 | const childPaths = GlobUtil.globPaths(fs, childPathPattern); 64 | 65 | if (!isNonEmptyDirectoryRemovable && !childPaths.isEmpty()) { 66 | return { 67 | err: makeError(fsErrorType.DIRECTORY_NOT_EMPTY) 68 | }; 69 | } 70 | 71 | return { 72 | fs: fs.removeAll(childPaths.concat(pathToRemove)) 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /src/fs/operations/file-operations.js: -------------------------------------------------------------------------------- 1 | import * as PathUtil from 'fs/util/path-util'; 2 | import * as BaseOp from 'fs/operations/base-operations'; 3 | import { isFile } from 'fs/util/file-util'; 4 | import { hasDirectory } from 'fs/operations/directory-operations'; 5 | import { makeError, fsErrorType } from 'fs/fs-error'; 6 | 7 | /** 8 | * Checks whether a file exists 9 | * @param {Map} fs file system 10 | * @param {string} dirPath directory of the file to check for existence 11 | * @param {string} fileName file name to check for existence 12 | * @return {Boolean} true, if the file exists 13 | */ 14 | export const hasFile = (fs, filePath) => { 15 | if (fs.has(filePath)) { 16 | const possibleFile = fs.get(filePath); 17 | 18 | return isFile(possibleFile); 19 | } 20 | return false; 21 | }; 22 | 23 | /** 24 | * Get a file from the file system 25 | * @param {Map} fs file system 26 | * @param {string} filePath path to file to read 27 | * @return {object} file system or an error 28 | */ 29 | export const readFile = (fs, filePath) => { 30 | if (hasDirectory(fs, filePath)) { 31 | return { 32 | err: makeError(fsErrorType.IS_A_DIRECTORY) 33 | }; 34 | } 35 | 36 | if (!hasFile(fs, filePath)) { 37 | return { 38 | err: makeError(fsErrorType.NO_SUCH_FILE) 39 | }; 40 | } 41 | 42 | return { 43 | file: fs.get(filePath) 44 | }; 45 | }; 46 | 47 | /** 48 | * Write a new file to the file system 49 | * @param {Map} fs file system 50 | * @param {string} filePath path to new file 51 | * @param {Map} file the new file 52 | * @return {object} file system or an error 53 | */ 54 | export const writeFile = (fs, filePath, file) => { 55 | return BaseOp.add(fs, filePath, file); 56 | }; 57 | 58 | /** 59 | * Copies a file from a source directory to a destination directory 60 | * @param {Map} fs file system 61 | * @param {string} sourcePath path to source file (to copy from) 62 | * @param {string} destPath path to destination file (to copy to) 63 | * @return {object} file system or an error 64 | */ 65 | export const copyFile = (fs, sourcePath, destPath) => { 66 | if (!hasFile(fs, sourcePath)) { 67 | return { 68 | err: makeError(fsErrorType.NO_SUCH_FILE, 'Source file does not exist') 69 | }; 70 | } 71 | 72 | const pathParent = PathUtil.getPathParent(destPath); 73 | 74 | if (!hasDirectory(fs, pathParent)) { 75 | return { 76 | err: makeError(fsErrorType.NO_SUCH_DIRECTORY, 'Destination directory does not exist') 77 | }; 78 | } 79 | 80 | if (hasDirectory(fs, destPath)) { 81 | // Copying file to directory without specifying the filename explicitly 82 | const sourceFileName = PathUtil.getLastPathPart(sourcePath); 83 | 84 | destPath = destPath === '/' ? `/${sourceFileName}` : `${destPath}/${sourceFileName}`; 85 | } 86 | 87 | return { 88 | fs: fs.set(destPath, fs.get(sourcePath)) 89 | }; 90 | }; 91 | 92 | /** 93 | * Removes a file from the file system 94 | * @param {Map} fs file system 95 | * @param {string} filePath path to the file to delete 96 | * @return {object} file system or an error 97 | */ 98 | export const deleteFile = (fs, filePath) => { 99 | if (hasDirectory(fs, filePath)) { 100 | return { 101 | err: makeError(fsErrorType.IS_A_DIRECTORY) 102 | }; 103 | } 104 | 105 | if (!hasFile(fs, filePath)) { 106 | return { 107 | err: makeError(fsErrorType.NO_SUCH_FILE) 108 | }; 109 | } 110 | 111 | return BaseOp.remove(fs, filePath); 112 | }; 113 | -------------------------------------------------------------------------------- /src/fs/util/file-util.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | /** 4 | * Checks if a JavaScript object is a file object 5 | * @param {object} json potential file 6 | * @return {boolean} whether the object conforms to the file schema 7 | */ 8 | export const isFile = (map) => { 9 | return map.has('content'); 10 | }; 11 | 12 | /** 13 | * Checks if a JavaScript object is a directory object 14 | * @param {object} json potential directory 15 | * @return {boolean} whether the object conforms to the directory schema 16 | */ 17 | export const isDirectory = (map) => { 18 | return !map.has('content'); 19 | }; 20 | 21 | /** 22 | * Makes an file conforming to the file schema 23 | * @param {object} content content of the file 24 | * @return {object} new file 25 | */ 26 | export const makeFile = (content = '', metadata = {}) => { 27 | return fromJS({ 28 | content, 29 | ...metadata 30 | }); 31 | }; 32 | 33 | /** 34 | * Makes an directory conforming to the directory schema 35 | * @param {object} children child directories or files 36 | * @return {object} new directory 37 | */ 38 | export const makeDirectory = (metadata = {}) => { 39 | return fromJS({ 40 | ...metadata 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/fs/util/glob-util.js: -------------------------------------------------------------------------------- 1 | import minimatch from 'minimatch'; 2 | import capture from 'minimatch-capture'; 3 | import { List } from 'immutable'; 4 | 5 | const GLOB_OPTIONS = {dot: true}; 6 | 7 | export const glob = (str, globPattern) => { 8 | return minimatch(str, globPattern, GLOB_OPTIONS); 9 | }; 10 | 11 | export const globSeq = (seq, globPattern) => { 12 | return seq.filter((path) => minimatch(path, globPattern, GLOB_OPTIONS)); 13 | }; 14 | 15 | export const globPaths = (fs, globPattern) => { 16 | return globSeq(fs.keySeq(), globPattern); 17 | }; 18 | 19 | export const captureGlobPaths = (fs, globPattern, filterCondition = (path) => true) => { 20 | return fs.keySeq().reduce((captures, path) => { 21 | if (filterCondition(path)) { 22 | const pathCaptures = capture(path, globPattern, GLOB_OPTIONS); 23 | 24 | if (pathCaptures) { 25 | return captures.concat(pathCaptures); 26 | } 27 | } 28 | 29 | return captures; 30 | }, List()); 31 | }; 32 | -------------------------------------------------------------------------------- /src/fs/util/path-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests if a path is a trailing path. 3 | * 4 | * A trailing path ends with a trailing slash (/) and excludes the root 5 | * directory (/). 6 | * @param {string} path path with or without a trailing slash 7 | * @return {Boolean} true, if the path is a trailing path 8 | */ 9 | export const isTrailingPath = (path) => { 10 | return path.endsWith('/') && path !== '/'; 11 | }; 12 | 13 | /** 14 | * Removes a trailing slash (/) from a path 15 | * @param {string} path path with or without a trailing / 16 | * @return {string} path without trailing / 17 | */ 18 | export const removeTrailingSeparator = (path) => { 19 | if (path.endsWith('/') && path !== '/') { 20 | return path.slice(0, -1); 21 | } 22 | return path; 23 | }; 24 | 25 | /** 26 | * Tests if a path is absolute 27 | * @param {string} path 28 | * @return {boolean} 29 | */ 30 | export const isAbsPath = (path) => { 31 | return path.startsWith('/'); 32 | }; 33 | 34 | /** 35 | * Converts a path to an ordered array of folders and files. 36 | * 37 | * Example: Parts of '/a/b/c/e.txt' has parts of ['/', 'a', 'b', 'c', 'e.txt'] 38 | * 39 | * A relative path splits parts at /. An absolute path splits at / and also 40 | * considers the root directory (/) as a part of the path. 41 | * @param {string} path [description] 42 | * @return {array} list of path parts 43 | */ 44 | export const toPathParts = (path) => { 45 | if (path === '/') { 46 | return ['/']; 47 | }; 48 | 49 | path = removeTrailingSeparator(path); 50 | const pathParts = path.split('/'); 51 | 52 | if (isAbsPath(path)) { 53 | const [, ...nonRootPathParts] = pathParts; 54 | 55 | return ['/', ...nonRootPathParts]; 56 | } 57 | 58 | return pathParts; 59 | }; 60 | 61 | /** 62 | * Converts path parts back to a path 63 | * @param {array} pathParts path parts 64 | * @return {string} path 65 | */ 66 | export const toPath = (pathParts) => { 67 | if (pathParts[0] === '/') { // absolute path 68 | const [, ...nonRootPathParts] = pathParts; 69 | 70 | return `/${nonRootPathParts.join('/')}`; 71 | } 72 | 73 | return pathParts.join('/'); 74 | }; 75 | 76 | /** 77 | * Find breadcrumb paths, i.e. all paths that need to be walked to get to 78 | * the specified path 79 | * Example: /a/b/c will have breadcrumb paths of '/', '/a', '/a/b', '/a/b/c' 80 | * @param {string} path path to a directory 81 | * @return {array} list of paths that lead up to a path 82 | */ 83 | export const getPathBreadCrumbs = (path) => { 84 | const pathParts = toPathParts(path); 85 | 86 | if (pathParts.length <= 1) { 87 | return ['/']; 88 | } 89 | 90 | const [, secondPathPart, ...pathPartsWithoutRoot] = pathParts; 91 | 92 | return pathPartsWithoutRoot.reduce((breadCrumbs, pathPart) => { 93 | const previousBreadCrumb = breadCrumbs[breadCrumbs.length - 1]; 94 | 95 | return [...breadCrumbs, `${previousBreadCrumb}/${pathPart}`]; 96 | }, ['/', `/${secondPathPart}`]); 97 | }; 98 | 99 | /** 100 | * Removes the file name from the end of a file path, returning the path to the 101 | * directory of the file 102 | * @param {string} filePath path which ends with a file name 103 | * @return {string} directory path 104 | */ 105 | export const getPathParent = (filePath) => { 106 | if (filePath === '/') { 107 | return '/'; 108 | } 109 | 110 | const pathParts = toPathParts(filePath); // converts path string to array 111 | const pathPartsWithoutFileName = pathParts.slice(0, -1); // removes last element of array 112 | 113 | return toPath(pathPartsWithoutFileName); 114 | }; 115 | 116 | /** 117 | * Extracts the file name from the end of the file path 118 | * @param {string} filePath path which ends with a file name 119 | * @return {string} file name from the path 120 | */ 121 | export const getLastPathPart = (filePath) => { 122 | const pathParts = toPathParts(filePath); // converts path string to array 123 | 124 | return pathParts[pathParts.length - 1]; 125 | }; 126 | 127 | /** 128 | * Extracts the file name and directory path from a file path 129 | * @param {string} filePath path which ends with a file name 130 | * @return {object} object with directory and file name 131 | */ 132 | export const splitFilePath = (filePath) => { 133 | return { 134 | 'dirPath': getPathParent(filePath), 135 | 'fileName': getLastPathPart(filePath) 136 | }; 137 | }; 138 | 139 | /** 140 | * Converts a relative path to an absolute path 141 | * @param {string} relativePath 142 | * @param {string} cwd current working directory 143 | * @return {string} absolute path 144 | */ 145 | const GO_UP = '..'; 146 | const CURRENT_DIR = '.'; 147 | const isStackAtRootDirectory = stack => stack.length === 1 && stack[0] === '/'; 148 | 149 | export const toAbsolutePath = (relativePath, cwd) => { 150 | relativePath = removeTrailingSeparator(relativePath); 151 | const pathStack = isAbsPath(relativePath) ? [] : toPathParts(cwd); 152 | 153 | for (const pathPart of toPathParts(relativePath)) { 154 | if (pathPart === GO_UP) { 155 | if (!isStackAtRootDirectory(pathStack)) { 156 | pathStack.pop(); 157 | } 158 | } else if (pathPart !== CURRENT_DIR) { 159 | pathStack.push(pathPart); 160 | } 161 | } 162 | 163 | return toPath(pathStack); 164 | }; 165 | -------------------------------------------------------------------------------- /src/fs/util/permission-util.js: -------------------------------------------------------------------------------- 1 | import * as PathUtil from 'fs/util/path-util'; 2 | 3 | const DEFAULT_PERMISSION = true; 4 | 5 | /** 6 | * Checks if a single path can be modified by checking the 'canModify' key held 7 | * in the path. 8 | * 9 | * This does NOT check parents of the path. 10 | * @param {Map} fs file system 11 | * @param {string} path path to check for modification permission 12 | * @return {Boolean} true, if a single path can be modified 13 | */ 14 | const isModificationAllowed = (fs, path) => { 15 | const directory = fs.get(path, null); 16 | 17 | if (directory) { 18 | const canModify = directory.get('canModify', DEFAULT_PERMISSION); 19 | 20 | if (!canModify) { 21 | return false; 22 | } 23 | } 24 | 25 | return true; 26 | }; 27 | 28 | /** 29 | * Checks if a path and its parents can be modified. 30 | * @param {Map} fs file systems 31 | * @param {String} path path to a directory or file 32 | * @return {Boolean} true, if the path and its parents can be modified 33 | */ 34 | export const canModifyPath = (fs, path) => { 35 | const breadCrumbPaths = PathUtil.getPathBreadCrumbs(path); 36 | 37 | for (const breadCrumbPath of breadCrumbPaths) { 38 | if (!isModificationAllowed(fs, breadCrumbPath)) { 39 | return false; 40 | } 41 | } 42 | 43 | return true; 44 | }; 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Emulator from 'emulator'; 2 | import HistoryKeyboardPlugin from 'emulator/plugins/HistoryKeyboardPlugin'; 3 | import { EmulatorState, CommandMapping, EnvironmentVariables, FileSystem, History, Outputs } from 'emulator-state'; 4 | import { OutputFactory, OutputType } from 'emulator-output'; 5 | import { DirOp, FileOp } from 'fs'; 6 | import { OptionParser } from 'parser'; 7 | import defaultCommandMapping from 'commands'; 8 | 9 | // Any class/function exported here forms part of the emulator API 10 | export { 11 | Emulator, HistoryKeyboardPlugin, 12 | defaultCommandMapping, 13 | EmulatorState, CommandMapping, EnvironmentVariables, FileSystem, History, Outputs, // state API 14 | OutputFactory, OutputType, // output API 15 | DirOp, FileOp, // file system API 16 | OptionParser // parser API 17 | }; 18 | -------------------------------------------------------------------------------- /src/parser/command-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes excess whitespace (> 1 space) from edges of string and inside string. 3 | * @param {string} str string 4 | * @return {string} string without > 1 space of whitespace 5 | */ 6 | const removeExcessWhiteSpace = str => str.trim().replace(/\s\s+/g, ' '); 7 | 8 | /** 9 | * Places the command name and each following argument into a list 10 | * @param {string} command sh command 11 | * @return {array} command name and arguments (if any) 12 | */ 13 | const toCommandParts = command => removeExcessWhiteSpace(command).split(/\s/); 14 | 15 | /** 16 | * Creates a list of commands split into the command name and arguments 17 | * @param {string} commands command input 18 | * @return {array} list of parsed command 19 | */ 20 | export const parseCommands = (commands) => { 21 | return commands 22 | .split(/&&|;/) // split command delimiters: `&&` and `;` 23 | .map((command) => toCommandParts(command)) 24 | .map(([commandName, ...commandOptions]) => ({ 25 | commandName, 26 | commandOptions 27 | })); 28 | }; 29 | 30 | export default parseCommands; 31 | -------------------------------------------------------------------------------- /src/parser/index.js: -------------------------------------------------------------------------------- 1 | import * as OptionParser from 'parser/option-parser'; 2 | 3 | export default { 4 | OptionParser 5 | }; 6 | -------------------------------------------------------------------------------- /src/parser/option-parser.js: -------------------------------------------------------------------------------- 1 | import getOpts from 'get-options'; 2 | 3 | /** 4 | * Creates an options object with bindings based on optDefs 5 | * @param {string} commandOptions string representation of command arguments 6 | * @param {object} optDef see get-options documentation for schema details 7 | * @return {object} options object 8 | */ 9 | export const parseOptions = (commandOptions, optDef) => 10 | getOpts(commandOptions, optDef, { 11 | noAliasPropagation: 'first-only' 12 | }); 13 | 14 | export default parseOptions; 15 | -------------------------------------------------------------------------------- /test/_plugins/state-equality-plugin.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | 4 | chai.use(chaiImmutable); 5 | 6 | chai.use((_chai, utils) => { 7 | const { Assertion } = _chai; 8 | 9 | Assertion.addMethod('toEqualState', function (value) { 10 | const obj = this._obj; 11 | 12 | new Assertion(obj.getImmutable()).to.equal(value.getImmutable()); 13 | }); 14 | }); 15 | 16 | export default chai; 17 | -------------------------------------------------------------------------------- /test/commands/cat.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import EmulatorState from 'emulator-state/EmulatorState'; 6 | import { create as createFileSystem } from 'emulator-state/file-system'; 7 | import cat from 'commands/cat'; 8 | 9 | const state = EmulatorState.create({ 10 | fs: createFileSystem({ 11 | '/': {}, 12 | '/a': { 13 | content: 'file-one-content\nline-two' 14 | }, 15 | '/b': { 16 | content: 'file-two-content' 17 | }, 18 | '/directory': {} 19 | }) 20 | }); 21 | 22 | describe('cat', () => { 23 | it('print out single file', () => { 24 | const {outputs} = cat(state, ['a']); 25 | 26 | chai.expect(outputs[0].content).to.equal('file-one-content\nline-two'); 27 | }); 28 | 29 | it('should join multiple files ', () => { 30 | const {outputs} = cat(state, ['a', 'b']); 31 | 32 | chai.expect(outputs[0].content).to.equal('file-one-content\nline-two'); 33 | chai.expect(outputs[1].content).to.equal('file-two-content'); 34 | }); 35 | 36 | it('should have no output if no file is given', () => { 37 | const {outputs} = cat(state, []); 38 | 39 | chai.expect(outputs).to.equal(undefined); 40 | }); 41 | 42 | describe('err: no directory', () => { 43 | it('should return error output', () => { 44 | const {outputs} = cat(state, ['/no/such/dir/file.txt']); 45 | 46 | chai.expect(outputs[0].type).to.equal('TEXT_ERROR_OUTPUT'); 47 | }); 48 | }); 49 | 50 | describe('err: no file', () => { 51 | it('should read files which exist and skip files with error', () => { 52 | const {outputs} = cat(state, ['a', 'no-such-file', 'b']); 53 | 54 | chai.expect(outputs[0].content).to.equal('file-one-content\nline-two'); 55 | chai.expect(outputs[1].type).to.equal('TEXT_ERROR_OUTPUT'); 56 | chai.expect(outputs[2].content).to.equal('file-two-content'); 57 | }); 58 | 59 | it('should return error output if no file', () => { 60 | const {outputs} = cat(state, ['/no_such_file.txt']); 61 | 62 | chai.expect(outputs[0].type).to.equal('TEXT_ERROR_OUTPUT'); 63 | }); 64 | 65 | it('should return error output if directory instead of file is passed to cat', () => { 66 | const {outputs} = cat(state, ['/directory']); 67 | 68 | chai.expect(outputs[0].type).to.equal('TEXT_ERROR_OUTPUT'); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/commands/cd.spec.js: -------------------------------------------------------------------------------- 1 | import chai from '../_plugins/state-equality-plugin'; 2 | 3 | import EmulatorState from 'emulator-state/EmulatorState'; 4 | import { create as createFileSystem } from 'emulator-state/file-system'; 5 | import { create as createEnvironmentVariables } from 'emulator-state/environment-variables'; 6 | import cd from 'commands/cd'; 7 | 8 | describe('cd', () => { 9 | const fs = createFileSystem({ 10 | '/': {}, 11 | '/a/subfolder': {}, 12 | '/startingCwd': {} 13 | }); 14 | 15 | const state = EmulatorState.create({ 16 | fs, 17 | environmentVariables: createEnvironmentVariables({}, '/startingCwd') 18 | }); 19 | 20 | const makeExpectedState = (workingDirectory) => { 21 | return EmulatorState.create({ 22 | fs, 23 | environmentVariables: createEnvironmentVariables({}, workingDirectory) 24 | }); 25 | }; 26 | 27 | it('should change working directory to root if no arguments passed to cd', () => { 28 | const {state: actualState} = cd(state, []); 29 | const expectedState = makeExpectedState('/'); 30 | 31 | chai.expect(actualState).toEqualState(expectedState); 32 | }); 33 | 34 | it('should change working directory to argument directory', () => { 35 | const {state: actualState} = cd(state, ['/a']); 36 | const expectedState = makeExpectedState('/a'); 37 | 38 | chai.expect(actualState).toEqualState(expectedState); 39 | }); 40 | 41 | it('should change working directory to nested argument directory', () => { 42 | const {state: actualState} = cd(state, ['/a/subfolder']); 43 | const expectedState = makeExpectedState('/a/subfolder'); 44 | 45 | chai.expect(actualState).toEqualState(expectedState); 46 | }); 47 | 48 | it('should return error output if changing to non-existent directory', () => { 49 | const {output} = cd(state, ['/no-such-dir']); 50 | 51 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/commands/clear.spec.js: -------------------------------------------------------------------------------- 1 | import chai from '../_plugins/state-equality-plugin'; 2 | 3 | import EmulatorState from 'emulator-state/EmulatorState'; 4 | import { create as createHistory } from 'emulator-state/history'; 5 | import * as OutputFactory from 'emulator-output/output-factory'; 6 | import clear from 'commands/clear'; 7 | 8 | describe('clear', () => { 9 | it('should clear outputs', () => { 10 | const stateWithOutputs = EmulatorState.create({ 11 | clear: createHistory(OutputFactory.makeTextOutput('a')) 12 | }); 13 | 14 | const {state: actualState} = clear(stateWithOutputs, []); 15 | const expectedState = EmulatorState.createEmpty(); 16 | 17 | chai.expect(actualState).toEqualState(expectedState); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/commands/echo.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import EmulatorState from 'emulator-state/EmulatorState'; 6 | import { create as createEnvironmentVariables } from 'emulator-state/environment-variables'; 7 | import echo from 'commands/echo'; 8 | 9 | describe('echo', () => { 10 | const state = EmulatorState.create({ 11 | environmentVariables: createEnvironmentVariables({ 12 | 'STR': 'baz' 13 | }, '/dir') 14 | }); 15 | 16 | it('should echo string', () => { 17 | const {output} = echo(state, ['hello', 'world']); 18 | 19 | chai.expect(output.content).to.equal('hello world'); 20 | }); 21 | 22 | it('should replace variable in string with value', () => { 23 | const {output} = echo(state, ['this', 'is', '$STR']); 24 | 25 | chai.expect(output.content).to.equal('this is baz'); 26 | }); 27 | 28 | it('should replace variable with value', () => { 29 | const {output} = echo(state, ['$STR']); 30 | 31 | chai.expect(output.content).to.equal('baz'); 32 | }); 33 | 34 | it('should replace missing variable with no value', () => { 35 | const {output} = echo(state, ['val', '$NO_SUCH_VAR']); 36 | 37 | chai.expect(output.content).to.equal('val'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/commands/head.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import EmulatorState from 'emulator-state/EmulatorState'; 6 | import {create as createFileSystem} from 'emulator-state/file-system'; 7 | import head from 'commands/head'; 8 | 9 | const state = EmulatorState.create({ 10 | fs: createFileSystem({ 11 | '/1-line-file': { 12 | content: '1' 13 | }, 14 | '/10-line-file': { 15 | content: '1\n2\n3\n4\n5\n6\n7\n8\n9\n10' 16 | }, 17 | '/15-line-file': { 18 | content: '1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15' 19 | } 20 | }) 21 | }); 22 | 23 | describe('head', () => { 24 | it('should do nothing if no arguments given', () => { 25 | const returnVal = head(state, []); 26 | 27 | chai.expect(returnVal).to.deep.equal({}); 28 | }); 29 | 30 | it('print ten lines by default', () => { 31 | const {output} = head(state, ['15-line-file']); 32 | 33 | chai.expect(output.content).to.equal('1\n2\n3\n4\n5\n6\n7\n8\n9\n10'); 34 | }); 35 | 36 | it('print lines in count argument', () => { 37 | const {output} = head(state, ['15-line-file', '-n', '2']); 38 | 39 | chai.expect(output.content).to.equal('1\n2'); 40 | }); 41 | 42 | it('print maximum number of lines', () => { 43 | const {output} = head(state, ['1-line-file', '-n', '1000']); 44 | 45 | chai.expect(output.content).to.equal('1'); 46 | }); 47 | 48 | describe('err: no path', () => { 49 | it('should return error output if no path', () => { 50 | const {output} = head(state, ['/noSuchFile']); 51 | 52 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/commands/history.spec.js: -------------------------------------------------------------------------------- 1 | import chai from '../_plugins/state-equality-plugin'; 2 | 3 | import EmulatorState from 'emulator-state/EmulatorState'; 4 | import { create as createHistory } from 'emulator-state/history'; 5 | import history from 'commands/history'; 6 | 7 | describe('history', () => { 8 | const expectedHistory = ['pwd', 'cd /foo', 'echo abc']; 9 | const stateWithExpectedHistory = EmulatorState.create({ 10 | history: createHistory(expectedHistory) 11 | }); 12 | const stateWithNoHistory = EmulatorState.createEmpty(); 13 | 14 | it('should print history', () => { 15 | const {output} = history(stateWithExpectedHistory, []); 16 | 17 | chai.expect(output.content).to.equal(expectedHistory.join('\n')); 18 | }); 19 | 20 | describe('arg: -c', () => { 21 | it('should delete history', () => { 22 | const {state} = history(stateWithExpectedHistory, ['-c']); 23 | 24 | chai.expect(state).toEqualState(stateWithNoHistory); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/commands/ls.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import EmulatorState from 'emulator-state/EmulatorState'; 6 | import { create as createFileSystem } from 'emulator-state/file-system'; 7 | import ls from 'commands/ls'; 8 | 9 | const state = EmulatorState.create({ 10 | fs: createFileSystem({ 11 | '/': {}, 12 | '/a.txt': { 13 | content: '' 14 | }, 15 | '/b.txt': { 16 | content: '' 17 | }, 18 | '/.hidden.txt': { 19 | content: '' 20 | }, 21 | '/a/subfolder': {}, 22 | '/a/subfolder/c.txt': { 23 | content: '' 24 | }, 25 | '/a/subfolder/d.txt': { 26 | content: '' 27 | }, 28 | '/emptyFolder': {} 29 | }) 30 | }); 31 | 32 | describe('ls', () => { 33 | it('should list folders and files in root: /', () => { 34 | const {output} = ls(state, ['/']); 35 | const expectedListing = ['a.txt', 'a/', 'b.txt', 'emptyFolder/'].join('\n'); 36 | 37 | chai.expect(output.content).to.equal(expectedListing); 38 | }); 39 | 40 | it('should list folders and files in cwd if no argument', () => { 41 | const {output} = ls(state, []); 42 | const expectedListing = ['a.txt', 'a/', 'b.txt', 'emptyFolder/'].join('\n'); 43 | 44 | chai.expect(output.content).to.equal(expectedListing); 45 | }); 46 | 47 | it('should list files in subfolder folder', () => { 48 | const {output} = ls(state, ['/a/subfolder']); 49 | const expectedListing = ['c.txt', 'd.txt'].join('\n'); 50 | 51 | chai.expect(output.content).to.equal(expectedListing); 52 | }); 53 | 54 | it('should list no files in empty folder', () => { 55 | const {output} = ls(state, ['/emptyFolder']); 56 | 57 | chai.expect(output.content).to.equal(''); 58 | }); 59 | 60 | describe('err: no directory', () => { 61 | it('should return error output', () => { 62 | const {output} = ls(state, ['/no/such/dir']); 63 | 64 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 65 | }); 66 | }); 67 | 68 | describe('arg: -a', () => { 69 | it('should list hidden files/folders, implied directories (. and ..), visible files/folders', () => { 70 | const {output} = ls(state, ['/', '-a']); 71 | const expectedListing = [ 72 | '.', '..', '.hidden.txt', 73 | 'a.txt', 'a/', 'b.txt', 'emptyFolder/' 74 | ].join('\n'); 75 | 76 | chai.expect(output.content).to.equal(expectedListing); 77 | }); 78 | }); 79 | 80 | describe('arg: -A', () => { 81 | it('should list hidden and visible files/folders', () => { 82 | const {output} = ls(state, ['/', '-A']); 83 | const expectedListing = [ 84 | '.hidden.txt', 'a.txt', 'a/', 'b.txt', 'emptyFolder/' 85 | ].join('\n'); 86 | 87 | chai.expect(output.content).to.equal(expectedListing); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/commands/mapping/index.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import commandMapping, { commandNames } from 'commands'; 6 | 7 | describe('commands', () => { 8 | describe('default command mapping', () => { 9 | it('should have all command functions', () => { 10 | for (const commandName of commandNames) { 11 | const cmd = commandMapping[commandName]; 12 | 13 | chai.assert.isFunction(cmd.function); 14 | chai.assert.isObject(cmd.optDef); 15 | } 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/commands/mkdir.spec.js: -------------------------------------------------------------------------------- 1 | import chai from '../_plugins/state-equality-plugin'; 2 | 3 | import mkdir from 'commands/mkdir'; 4 | import { makeFileSystemTestState } from './test-helper'; 5 | 6 | describe('mkdir', () => { 7 | it('should do nothing if no arguments given', () => { 8 | const returnVal = mkdir(makeFileSystemTestState(), []); 9 | 10 | chai.expect(returnVal).to.deep.equal({}); 11 | }); 12 | 13 | it('should create directory in root with given name', () => { 14 | const expectedState = makeFileSystemTestState({ 15 | '/newFolderName': {} 16 | }); 17 | 18 | const {state} = mkdir(makeFileSystemTestState(), ['newFolderName']); 19 | 20 | chai.expect(state).toEqualState(expectedState); 21 | }); 22 | 23 | it('should create nested directory with given name', () => { 24 | const startState = makeFileSystemTestState({ 25 | '/a/b': {} 26 | }); 27 | 28 | const expectedState = makeFileSystemTestState({ 29 | '/a/b/c': {} 30 | }); 31 | 32 | const {state} = mkdir(startState, ['/a/b/c']); 33 | 34 | chai.expect(state).toEqualState(expectedState); 35 | }); 36 | 37 | describe('err: no parent directory', () => { 38 | it('should return error output if no parent directory', () => { 39 | const startState = makeFileSystemTestState(); 40 | 41 | const {output} = mkdir(startState, ['/new/folder/here']); 42 | 43 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/commands/printenv.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import EmulatorState from 'emulator-state/EmulatorState'; 6 | import { create as createEnvironmentVariables } from 'emulator-state/environment-variables'; 7 | import printenv from 'commands/printenv'; 8 | 9 | describe('printenv', () => { 10 | const state = EmulatorState.create({ 11 | environmentVariables: createEnvironmentVariables({ 12 | 'STR': 'baz', 13 | 'NUM': 1337 14 | }, '/dir') 15 | }); 16 | 17 | it('should print all environment variables when not given any args', () => { 18 | const {output} = printenv(state, []); 19 | 20 | const expectedCommands = ['cwd=/dir', 'STR=baz', 'NUM=1337']; 21 | 22 | chai.expect(output.content).to.deep.equal(expectedCommands.join('\n')); 23 | }); 24 | 25 | it('should print single environment variable given arg', () => { 26 | const {output} = printenv(state, ['STR']); 27 | 28 | chai.expect(output.content).to.equal('baz'); 29 | }); 30 | 31 | it('should not return any output or state if no env variable with given key', () => { 32 | chai.expect(printenv(state, ['NO_SUCH_KEY'])).to.deep.equal({}); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/commands/pwd.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import EmulatorState from 'emulator-state/EmulatorState'; 6 | import { create as createEnvironmentVariables } from 'emulator-state/environment-variables'; 7 | import pwd from 'commands/pwd'; 8 | 9 | describe('pwd', () => { 10 | it('should print the working directory', () => { 11 | const state = EmulatorState.create({ 12 | environmentVariables: createEnvironmentVariables({}, '/dir') 13 | }); 14 | 15 | const {output} = pwd(state, []); 16 | 17 | chai.expect(output.content).to.equal('/dir'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/commands/rm.spec.js: -------------------------------------------------------------------------------- 1 | import chai from '../_plugins/state-equality-plugin'; 2 | 3 | import rm from 'commands/rm'; 4 | import { makeFileSystemTestState } from './test-helper'; 5 | 6 | describe('rm', () => { 7 | it('should do nothing if no arguments given', () => { 8 | const returnVal = rm(makeFileSystemTestState(), []); 9 | 10 | chai.expect(returnVal).to.deep.equal({}); 11 | }); 12 | 13 | describe('removing files', () => { 14 | it('should create remove a file with a given path', () => { 15 | const startState = makeFileSystemTestState({ 16 | '/a/file': {content: 'file content'} 17 | }); 18 | 19 | const expectedState = makeFileSystemTestState({ 20 | '/a': {} 21 | }); 22 | 23 | const {state} = rm(startState, ['/a/file']); 24 | 25 | chai.expect(state).toEqualState(expectedState); 26 | }); 27 | }); 28 | 29 | describe('removing directories', () => { 30 | it('should return nothing removing root without no-preserve-root flag', () => { 31 | const startState = makeFileSystemTestState({ 32 | '/a/b': {} 33 | }); 34 | 35 | const returnVal = rm(startState, ['-r', '/']); 36 | 37 | chai.expect(returnVal).to.deep.equal({}); 38 | }); 39 | 40 | it('should create remove root with flag', () => { 41 | const startState = makeFileSystemTestState({ 42 | '/a/b': {} 43 | }); 44 | 45 | const expectedState = makeFileSystemTestState({}); 46 | 47 | const {state} = rm(startState, ['-r', '--no-preserve-root', '/']); 48 | 49 | chai.expect(state).toEqualState(expectedState); 50 | }); 51 | 52 | it('should create remove nested directory', () => { 53 | const startState = makeFileSystemTestState({ 54 | '/a/b': {} 55 | }); 56 | 57 | const expectedState = makeFileSystemTestState({ 58 | '/a': {} 59 | }); 60 | 61 | const {state} = rm(startState, ['-r', '/a/b']); 62 | 63 | chai.expect(state).toEqualState(expectedState); 64 | }); 65 | 66 | it('should create remove children directories', () => { 67 | const startState = makeFileSystemTestState({ 68 | '/doNotDelete/preserveFile': {content: 'should not be removed'}, 69 | '/delete': {}, 70 | '/delete/foo': {}, 71 | '/delete/baz': {}, 72 | '/delete/baz/bar': {} 73 | }); 74 | 75 | const expectedState = makeFileSystemTestState({ 76 | '/doNotDelete/preserveFile': {content: 'should not be removed'} 77 | }); 78 | 79 | const {state} = rm(startState, ['-r', 'delete']); 80 | 81 | chai.expect(state).toEqualState(expectedState); 82 | }); 83 | }); 84 | 85 | describe('err: removing directory without -r flag', () => { 86 | it('should return error if removing directory without -r flag', () => { 87 | const startState = makeFileSystemTestState({ 88 | '/a/b': {} 89 | }); 90 | 91 | const {output} = rm(startState, ['/a/b']); 92 | 93 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 94 | }); 95 | }); 96 | 97 | describe('err: no path', () => { 98 | it('should return error output if no path', () => { 99 | const startState = makeFileSystemTestState(); 100 | 101 | const {output} = rm(startState, ['/noSuchFolder']); 102 | 103 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/commands/rmdir.spec.js: -------------------------------------------------------------------------------- 1 | import chai from '../_plugins/state-equality-plugin'; 2 | 3 | import rmdir from 'commands/rmdir'; 4 | import { makeFileSystemTestState } from './test-helper'; 5 | 6 | describe('rmdir', () => { 7 | it('should do nothing if no arguments given', () => { 8 | const returnVal = rmdir(makeFileSystemTestState(), []); 9 | 10 | chai.expect(returnVal).to.deep.equal({}); 11 | }); 12 | 13 | it('should create remove a directory with a given name', () => { 14 | const startState = makeFileSystemTestState({ 15 | '/a/b': {} 16 | }); 17 | 18 | const expectedState = makeFileSystemTestState({ 19 | '/a': {} 20 | }); 21 | 22 | const {state} = rmdir(startState, ['/a/b']); 23 | 24 | chai.expect(state).toEqualState(expectedState); 25 | }); 26 | 27 | describe('err: no directory not empty', () => { 28 | it('should return error output if directory contains folders', () => { 29 | const startState = makeFileSystemTestState({ 30 | '/a/b': {} 31 | }); 32 | 33 | const {output} = rmdir(startState, ['/a']); 34 | 35 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 36 | }); 37 | 38 | it('should return error output if directory contains files', () => { 39 | const startState = makeFileSystemTestState({ 40 | '/a/b': {content: 'file b content'} 41 | }); 42 | 43 | const {output} = rmdir(startState, ['/a']); 44 | 45 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 46 | }); 47 | }); 48 | 49 | describe('err: no parent directory', () => { 50 | it('should return error output if no parent directory', () => { 51 | const startState = makeFileSystemTestState(); 52 | 53 | const {output} = rmdir(startState, ['/noSuchFolder']); 54 | 55 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/commands/tail.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import EmulatorState from 'emulator-state/EmulatorState'; 6 | import { create as createFileSystem } from 'emulator-state/file-system'; 7 | import tail from 'commands/tail'; 8 | 9 | const state = EmulatorState.create({ 10 | fs: createFileSystem({ 11 | '/1-line-file': { 12 | content: '1' 13 | }, 14 | '/10-line-file': { 15 | content: '1\n2\n3\n4\n5\n6\n7\n8\n9\n10' 16 | }, 17 | '/15-line-file': { 18 | content: '1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15' 19 | } 20 | }) 21 | }); 22 | 23 | describe('tail', () => { 24 | it('should do nothing if no arguments given', () => { 25 | const returnVal = tail(state, []); 26 | 27 | chai.expect(returnVal).to.deep.equal({}); 28 | }); 29 | 30 | it('print last ten lines by default', () => { 31 | const {output} = tail(state, ['15-line-file']); 32 | 33 | chai.expect(output.content).to.equal('6\n7\n8\n9\n10\n11\n12\n13\n14\n15'); 34 | }); 35 | 36 | it('print last lines in count argument', () => { 37 | const {output} = tail(state, ['15-line-file', '-n', '2']); 38 | 39 | chai.expect(output.content).to.equal('14\n15'); 40 | }); 41 | 42 | it('print maximum number of lines', () => { 43 | const {output} = tail(state, ['1-line-file', '-n', '1000']); 44 | 45 | chai.expect(output.content).to.equal('1'); 46 | }); 47 | 48 | describe('err: no path', () => { 49 | it('should return error output if no path', () => { 50 | const {output} = tail(state, ['/noSuchFile']); 51 | 52 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/commands/test-helper.js: -------------------------------------------------------------------------------- 1 | import EmulatorState from 'emulator-state/EmulatorState'; 2 | import { create as createFileSystem } from 'emulator-state/file-system'; 3 | 4 | export const makeFileSystemTestState = (jsFS) => EmulatorState.create({ 5 | fs: createFileSystem(jsFS) 6 | }); 7 | -------------------------------------------------------------------------------- /test/commands/touch.spec.js: -------------------------------------------------------------------------------- 1 | import chai from '../_plugins/state-equality-plugin'; 2 | 3 | import EmulatorState from 'emulator-state/EmulatorState'; 4 | import {create as createFileSystem} from 'emulator-state/file-system'; 5 | import touch from 'commands/touch'; 6 | 7 | describe('touch', () => { 8 | const emptyState = EmulatorState.createEmpty(); 9 | const stateWithEmptyFile = EmulatorState.create({ 10 | fs: createFileSystem({ 11 | '/': {}, 12 | '/fileName': { 13 | content: '' 14 | } 15 | }) 16 | }); 17 | 18 | it('should do nothing if no arguments given', () => { 19 | const returnVal = touch(emptyState, []); 20 | 21 | chai.expect(returnVal).to.deep.equal({}); 22 | }); 23 | 24 | it('should create empty file with given names', () => { 25 | const {state} = touch(emptyState, ['fileName']); 26 | 27 | chai.expect(state).toEqualState(stateWithEmptyFile); 28 | }); 29 | 30 | it('should create empty file with absolute path', () => { 31 | const {state} = touch(emptyState, ['/fileName']); 32 | 33 | chai.expect(state).toEqualState(stateWithEmptyFile); 34 | }); 35 | 36 | describe('err: no directory', () => { 37 | it('should return error output if no directory for file', () => { 38 | const {output} = touch(emptyState, ['/no-such-dir/fileName']); 39 | 40 | chai.expect(output.type).to.equal('TEXT_ERROR_OUTPUT'); 41 | }); 42 | }); 43 | 44 | describe('err: file already exists', () => { 45 | it('should NOT modify the fs and return NO error output', () => { 46 | const returnVal = touch(stateWithEmptyFile, ['fileName']); 47 | 48 | chai.expect(returnVal).to.deep.equal({}); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/commands/whoami.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import EmulatorState from 'emulator-state/EmulatorState'; 6 | import { create as createEnvironmentVariables } from 'emulator-state/environment-variables'; 7 | import whoami from 'commands/whoami'; 8 | 9 | describe('whoami', () => { 10 | it('should print root as the fallback usernname', () => { 11 | const state = EmulatorState.createEmpty(); 12 | 13 | const {output} = whoami(state, []); 14 | 15 | chai.expect(output.content).to.equal('root'); 16 | }); 17 | 18 | it('should print the user environment variable', () => { 19 | const state = EmulatorState.create({ 20 | environmentVariables: createEnvironmentVariables({ 21 | 'user': 'userNameValue' 22 | }, '/') 23 | }); 24 | 25 | const {output} = whoami(state, []); 26 | 27 | chai.expect(output.content).to.equal('userNameValue'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/emulator-output/output-factory.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import { OutputRecord, makeHeaderOutput, makeTextOutput, makeErrorOutput } from 'emulator-output/output-factory'; 4 | import * as Types from 'emulator-output/output-type'; 5 | 6 | describe('output-factory', () => { 7 | describe('OutputRecord', () => { 8 | it('should create a record with type and content', () => { 9 | const newRecord = new OutputRecord({ 10 | type: 'the type', 11 | content: ['the content'] 12 | }); 13 | 14 | chai.expect(newRecord.type).to.equal('the type'); 15 | chai.expect(newRecord.content).to.be.deep.equal(['the content']); 16 | }); 17 | 18 | it('should ignore keys not in the schema', () => { 19 | const newRecord = new OutputRecord({ 20 | notInSchema: 'do not add me' 21 | }); 22 | 23 | chai.expect(newRecord.notInSchema).to.equal(undefined); 24 | }); 25 | }); 26 | 27 | describe('makeHeaderOutput', () => { 28 | it('should create a record with the cwd', () => { 29 | const outputRecord = makeHeaderOutput('the cwd', 'the command'); 30 | 31 | chai.expect(outputRecord.content).to.deep.equal({cwd: 'the cwd', command: 'the command'}); 32 | }); 33 | 34 | it('should create a record with header type', () => { 35 | const outputRecord = makeHeaderOutput(''); 36 | 37 | chai.expect(outputRecord.type).to.equal(Types.HEADER_OUTPUT_TYPE); 38 | }); 39 | }); 40 | 41 | describe('makeTextOutput', () => { 42 | it('should create a record with content', () => { 43 | const textRecord = makeTextOutput('the content'); 44 | 45 | chai.expect(textRecord.content).to.be.deep.equal('the content'); 46 | }); 47 | 48 | it('should create a record with text type', () => { 49 | const textRecord = makeTextOutput(''); 50 | 51 | chai.expect(textRecord.type).to.equal(Types.TEXT_OUTPUT_TYPE); 52 | }); 53 | }); 54 | 55 | describe('makeTextOutput', () => { 56 | it('should combine source of error and type of error in output', () => { 57 | const errorRecord = makeErrorOutput({ 58 | source: 'the source', 59 | type: 'the type' 60 | }); 61 | 62 | chai.expect(errorRecord.content).to.be.deep.equal('the source: the type'); 63 | }); 64 | 65 | it('should create a record with error type', () => { 66 | const errorRecord = makeErrorOutput({ 67 | source: 'the source', 68 | type: 'the type' 69 | }); 70 | 71 | chai.expect(errorRecord.type).to.equal(Types.TEXT_ERROR_OUTPUT_TYPE); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/emulator-state/EmulatorState.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | import { Map } from 'immutable'; 4 | 5 | chai.use(chaiImmutable); 6 | 7 | import EmulatorState from 'emulator-state/EmulatorState'; 8 | import { create as createCommandMapping } from 'emulator-state/command-mapping'; 9 | import { create as createEnvironmentVariables } from 'emulator-state/environment-variables'; 10 | import { create as createFileSystem } from 'emulator-state/file-system'; 11 | import { create as createHistory } from 'emulator-state/history'; 12 | import { create as createOutputs } from 'emulator-state/outputs'; 13 | 14 | describe('EmulatorState', () => { 15 | describe('constructor', () => { 16 | it('should throw error if not given an immutable data structure', () => { 17 | chai.expect(() => 18 | new EmulatorState({}) 19 | ).to.throw(); 20 | }); 21 | 22 | it('should throw error if given an immutable Map', () => { 23 | // Still not valid emulator state as does not contain the required keys 24 | // The constructor should only be used internally (does not form part of 25 | // the API) so no validation of this is OK 26 | chai.expect(() => 27 | new EmulatorState(new Map()) 28 | ).to.not.throw(); 29 | }); 30 | }); 31 | 32 | describe('create', () => { 33 | it('should create state with default components', () => { 34 | const state = EmulatorState.create({}); 35 | 36 | chai.expect(state.getFileSystem()).to.equal(createFileSystem()); 37 | chai.expect(state.getEnvVariables()).to.equal(createEnvironmentVariables()); 38 | chai.expect(state.getHistory()).to.equal(createHistory()); 39 | chai.expect(state.getOutputs()).to.equal(createOutputs()); 40 | chai.expect(state.getCommandMapping()).to.equal(createCommandMapping()); 41 | }); 42 | 43 | it('should create state using user defined components', () => { 44 | const expectedFS = createFileSystem({ 45 | '/files': {} 46 | }); 47 | const expectedEnvironmentVariables = createEnvironmentVariables({ 48 | 'a': 'b' 49 | }, '/'); 50 | const expectedHistory = createHistory(['a', 'b', 'c']); 51 | const expectedOutputs = createOutputs(); 52 | const expectedCommandMapping = createCommandMapping({ 53 | 'a': { 54 | function: () => {}, 55 | optDef: {'a': 'd'} 56 | } 57 | }); 58 | 59 | const state = EmulatorState.create({ 60 | fs: expectedFS, 61 | environmentVariables: expectedEnvironmentVariables, 62 | history: expectedHistory, 63 | outputs: expectedOutputs, 64 | commandMapping: expectedCommandMapping 65 | }); 66 | 67 | chai.expect(state.getFileSystem()).to.equal(expectedFS); 68 | chai.expect(state.getEnvVariables()).to.equal(expectedEnvironmentVariables); 69 | chai.expect(state.getHistory()).to.equal(expectedHistory); 70 | chai.expect(state.getOutputs()).to.equal(expectedOutputs); 71 | chai.expect(state.getCommandMapping()).to.equal(expectedCommandMapping); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/emulator-state/environment-variables.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | import { Map } from 'immutable'; 4 | 5 | chai.use(chaiImmutable); 6 | 7 | import * as EnvironmentVariables from 'emulator-state/environment-variables'; 8 | 9 | describe('environment-variables', () => { 10 | const ENV_VARIABLES = EnvironmentVariables.create({ 11 | 'cwd': '/', 12 | 'foo': 'bar' 13 | }); 14 | 15 | describe('create', () => { 16 | it('should create an immutable map', () => { 17 | const envVariables = EnvironmentVariables.create({}, '/'); 18 | 19 | chai.expect(envVariables).to.be.instanceOf(Map); 20 | }); 21 | 22 | it('should create environment variables from JS object', () => { 23 | const envVariables = EnvironmentVariables.create({ 24 | 'a': 'b', 25 | 'c': 'd', 26 | 'cwd': '/' 27 | }); 28 | 29 | chai.expect(envVariables).to.equal(Map({ 30 | 'a': 'b', 31 | 'c': 'd', 32 | 'cwd': '/' 33 | })); 34 | }); 35 | 36 | it('should create environment variables if cwd is separately provided', () => { 37 | const envVariables = EnvironmentVariables.create({ 38 | 'a': 'b', 39 | 'c': 'd' 40 | }, '/path/to/cwd'); 41 | 42 | chai.expect(envVariables).to.equal(Map({ 43 | 'a': 'b', 44 | 'c': 'd', 45 | 'cwd': '/path/to/cwd' 46 | })); 47 | }); 48 | 49 | it('should throw error if no cwd (current working directory) is set', () => { 50 | chai.expect(() => 51 | EnvironmentVariables.create({}, null) 52 | ).to.throw(); 53 | }); 54 | }); 55 | 56 | describe('getEnvironmentVariable', () => { 57 | it('should get value of environment variable', () => { 58 | chai.expect( 59 | EnvironmentVariables.getEnvironmentVariable(ENV_VARIABLES, 'foo') 60 | ).to.equal('bar'); 61 | }); 62 | 63 | it('should return undefined if variable does not exist', () => { 64 | chai.expect( 65 | EnvironmentVariables.getEnvironmentVariable(ENV_VARIABLES, 'noVar') 66 | ).to.equal(undefined); 67 | }); 68 | }); 69 | 70 | describe('setEnvironmentVariable', () => { 71 | it('should set new variable', () => { 72 | const newEnvVariables = EnvironmentVariables.setEnvironmentVariable( 73 | ENV_VARIABLES, 'new key', 'new value' 74 | ); 75 | 76 | chai.expect( 77 | newEnvVariables.get('new key') 78 | ).to.equal('new value'); 79 | }); 80 | 81 | it('should overwrite existing variable', () => { 82 | const newEnvVariables = EnvironmentVariables.setEnvironmentVariable( 83 | ENV_VARIABLES, 'foo', 'new value'); 84 | 85 | chai.expect( 86 | newEnvVariables.get('foo') 87 | ).to.equal('new value'); 88 | }); 89 | }); 90 | 91 | describe('unsetEnvironmentVariable', () => { 92 | it('should remove existing variable', () => { 93 | const newEnvVariables = EnvironmentVariables.unsetEnvironmentVariable( 94 | ENV_VARIABLES, 'foo'); 95 | 96 | chai.expect( 97 | newEnvVariables.get('foo') 98 | ).to.equal(undefined); 99 | }); 100 | 101 | it('should have no effect if variable does not exist', () => { 102 | const newEnvVariables = EnvironmentVariables.unsetEnvironmentVariable( 103 | ENV_VARIABLES, 'noSuchKey' 104 | ); 105 | 106 | chai.expect( 107 | newEnvVariables.get('noSuchKey') 108 | ).to.equal(undefined); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/emulator-state/file-system.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { Map } from 'immutable'; 3 | import chaiImmutable from 'chai-immutable'; 4 | chai.use(chaiImmutable); 5 | 6 | import * as FileSystem from 'emulator-state/file-system'; 7 | 8 | describe('file-system', () => { 9 | describe('create', () => { 10 | it('should create an immutable map', () => { 11 | const fs = FileSystem.create({}); 12 | 13 | chai.expect(fs).to.be.instanceOf(Map); 14 | }); 15 | 16 | it('should create an immutable map from a JS object', () => { 17 | const fs = FileSystem.create({ 18 | '/dir': {} 19 | }); 20 | 21 | chai.expect(fs.get('/dir').toJS()).to.deep.equal({}); 22 | }); 23 | 24 | it('should add implied directory in nested file system', () => { 25 | const fs = FileSystem.create({ 26 | '/a/b/c': { // implies /a, /a/b and a/b/c are all directories in the file system 27 | 28 | } 29 | }); 30 | 31 | chai.expect(fs.get('/a').toJS()).to.deep.equal({}); 32 | 33 | chai.expect(fs.get('/a/b').toJS()).to.deep.equal({}); 34 | 35 | chai.expect(fs.get('/a/b/c').toJS()).to.deep.equal({}); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/emulator-state/history.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | import { Stack } from 'immutable'; 4 | 5 | chai.use(chaiImmutable); 6 | 7 | import * as History from 'emulator-state/history'; 8 | 9 | describe('history', () => { 10 | describe('create', () => { 11 | it('should create an immutable stack', () => { 12 | const history = History.create([]); 13 | 14 | chai.expect(history).to.be.instanceOf(Stack); 15 | }); 16 | 17 | it('should create command map from JS array', () => { 18 | const history = History.create([1, 2, 3]); 19 | 20 | chai.expect([...history.values()]).to.deep.equal([1, 2, 3]); 21 | }); 22 | }); 23 | 24 | describe('recordCommand', () => { 25 | it('should add command to top of stack', () => { 26 | const history = History.create(['a --help', 'b']); 27 | const newHistory = History.recordCommand(history, 'new'); 28 | 29 | chai.expect( 30 | newHistory.peek() 31 | ).to.equal('new'); 32 | }); 33 | 34 | it('should keep old commands in stack', () => { 35 | const history = History.create(['a --help', 'b']); 36 | const newHistory = History.recordCommand(history, 'new'); 37 | 38 | chai.expect( 39 | newHistory.toJS() 40 | ).to.deep.equal(['new', 'a --help', 'b']); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/emulator-state/outputs.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { List, Record } from 'immutable'; 3 | import chaiImmutable from 'chai-immutable'; 4 | chai.use(chaiImmutable); 5 | 6 | import * as Outputs from 'emulator-state/outputs'; 7 | import { OutputRecord } from 'emulator-output/output-factory'; 8 | 9 | describe('outputs', () => { 10 | describe('create', () => { 11 | it('should create an immutable list', () => { 12 | const outputs = Outputs.create([]); 13 | 14 | chai.expect(outputs).to.be.instanceOf(List); 15 | }); 16 | }); 17 | 18 | describe('addRecord', () => { 19 | const emptyOutputs = Outputs.create([]); 20 | 21 | it('should add an output record', () => { 22 | const newRecord = new OutputRecord({ 23 | type: 'the type', 24 | content: ['the content'] 25 | }); 26 | 27 | const outputs = Outputs.addRecord(emptyOutputs, newRecord); 28 | 29 | chai.expect(outputs).to.equal(new List([newRecord])); 30 | }); 31 | 32 | it('should throw error if adding plain JS object', () => { 33 | chai.expect(() => 34 | Outputs.addRecord(emptyOutputs, { 35 | type: 'the type', 36 | content: ['the content'] 37 | }) 38 | ).to.throw(); 39 | }); 40 | 41 | it('should throw error if adding record without type', () => { 42 | const CustomRecord = new Record('a'); 43 | const missingTypeRecord = new CustomRecord({ 44 | content: 'content' 45 | }); 46 | 47 | chai.expect(() => 48 | Outputs.addRecord(emptyOutputs, missingTypeRecord) 49 | ).to.throw(); 50 | }); 51 | 52 | it('should throw error if adding record without content', () => { 53 | const CustomRecord = new Record('a'); 54 | const missingContentRecord = new CustomRecord({ 55 | type: 'type' 56 | }); 57 | 58 | chai.expect(() => 59 | Outputs.addRecord(emptyOutputs, missingContentRecord) 60 | ).to.throw(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/emulator/command-runner.spec.js: -------------------------------------------------------------------------------- 1 | import { create as createCommandMapping } from 'emulator-state/command-mapping'; 2 | import chai from 'chai'; 3 | 4 | import { run } from 'emulator/command-runner'; 5 | import { emulatorErrorType } from 'emulator/emulator-error'; 6 | 7 | describe('command-runner', () => { 8 | describe('run', () => { 9 | it('should exist', () => { 10 | chai.assert.isFunction(run); 11 | }); 12 | 13 | it('should run command from command mapping with no args', () => { 14 | const commandMapping = createCommandMapping({ 15 | returnTrue: { 16 | function: () => {return true;}, 17 | optDef: {} 18 | } 19 | }); 20 | 21 | chai.expect( 22 | run(commandMapping, 'returnTrue', []) 23 | ).to.equal(true); 24 | }); 25 | 26 | it('should run command from command mapping with args', () => { 27 | const commandMapping = createCommandMapping({ 28 | sum: { 29 | function: (a, b) => {return a + b;}, 30 | optDef: {} 31 | } 32 | }); 33 | 34 | chai.expect( 35 | run(commandMapping, 'sum', [40, 2]) 36 | ).to.equal(42); 37 | }); 38 | 39 | it('should raise unexpected command failure internal error if command throws error', () => { 40 | const commandMapping = createCommandMapping({ 41 | throwsError: { 42 | function: () => {throw new Error('Unhandled error');}, 43 | optDef: {} 44 | } 45 | }); 46 | 47 | const {output} = run(commandMapping, 'throwsError', []); 48 | 49 | chai.expect(output.content).to.include(emulatorErrorType.UNEXPECTED_COMMAND_FAILURE); 50 | }); 51 | 52 | it('should raise no command error if command not in mapping', () => { 53 | const commandMapping = createCommandMapping({}); 54 | 55 | const {output} = run(commandMapping, 'noSuchKey', []); 56 | 57 | chai.expect(output.content).to.include(emulatorErrorType.COMMAND_NOT_FOUND); 58 | }); 59 | 60 | it('should run a notFoundCallback command if command not in mapping and notFoundCallback provided', () => { 61 | const commandMapping = createCommandMapping({}); 62 | const notFoundCallback = ()=> true; 63 | 64 | chai.expect(run(commandMapping, 'noSuchKey', [], notFoundCallback)).to.equal(true); 65 | }); 66 | 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/emulator/emulator-error.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import {emulatorErrorType, makeError} from 'emulator/emulator-error'; 4 | 5 | describe('emulator/emulator-error', () => { 6 | it('should create error object with selected emulator error type', () => { 7 | const emulatorError = makeError(emulatorErrorType.COMMAND_NOT_FOUND); 8 | 9 | chai.expect(emulatorError).to.deep.equal({ 10 | source: 'emulator', 11 | type: emulatorErrorType.COMMAND_NOT_FOUND, 12 | message: '' 13 | }); 14 | }); 15 | 16 | it('should create error object with error message', () => { 17 | const emulatorError = makeError(emulatorErrorType.UNEXPECTED_COMMAND_FAILURE, 'my message'); 18 | 19 | chai.expect(emulatorError).to.deep.equal({ 20 | source: 'emulator', 21 | type: emulatorErrorType.UNEXPECTED_COMMAND_FAILURE, 22 | message: 'my message' 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/emulator/plugins/BoundedHistoryIterator.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import BoundedHistoryIterator from 'emulator/plugins/BoundedHistoryIterator'; 4 | import { create as createHistory } from 'emulator-state/history'; 5 | 6 | describe('BoundedHistoryIterator', () => { 7 | it('should go up until last value', () => { 8 | const iterator = new BoundedHistoryIterator(createHistory([1, 2, 3])); 9 | 10 | chai.expect(iterator.up()).to.equal(1); 11 | chai.expect(iterator.up()).to.equal(2); 12 | chai.expect(iterator.up()).to.equal(3); 13 | chai.expect(iterator.up()).to.equal(3); 14 | chai.expect(iterator.up()).to.equal(3); 15 | }); 16 | 17 | it('should go down/up with empty history', () => { 18 | const iterator = new BoundedHistoryIterator(createHistory([])); 19 | 20 | chai.expect(iterator.down()).to.equal(''); 21 | chai.expect(iterator.up()).to.equal(''); 22 | }); 23 | 24 | it('should go down with empty history', () => { 25 | const iterator = new BoundedHistoryIterator(createHistory([])); 26 | 27 | chai.expect(iterator.down()).to.equal(''); 28 | chai.expect(iterator.down()).to.equal(''); 29 | chai.expect(iterator.down()).to.equal(''); 30 | }); 31 | 32 | it('should go up with empty history', () => { 33 | const iterator = new BoundedHistoryIterator(createHistory([])); 34 | 35 | chai.expect(iterator.up()).to.equal(''); 36 | chai.expect(iterator.up()).to.equal(''); 37 | chai.expect(iterator.up()).to.equal(''); 38 | }); 39 | 40 | it('should go down until empty string', () => { 41 | const iterator = new BoundedHistoryIterator(createHistory([1, 2, 3]), 3); 42 | 43 | chai.expect(iterator.down()).to.equal(2); 44 | chai.expect(iterator.down()).to.equal(1); 45 | chai.expect(iterator.down()).to.equal(''); 46 | }); 47 | 48 | it('should go down and up', () => { 49 | const iterator = new BoundedHistoryIterator(createHistory([1, 2, 3, 4, 5])); 50 | 51 | // up sequence 52 | chai.expect(iterator.up()).to.equal(1); 53 | chai.expect(iterator.up()).to.equal(2); 54 | chai.expect(iterator.up()).to.equal(3); 55 | chai.expect(iterator.up()).to.equal(4); 56 | chai.expect(iterator.up()).to.equal(5); 57 | 58 | // down sequence 59 | chai.expect(iterator.down()).to.equal(4); 60 | chai.expect(iterator.down()).to.equal(3); 61 | chai.expect(iterator.down()).to.equal(2); 62 | chai.expect(iterator.down()).to.equal(1); 63 | 64 | // extra down sequence 65 | chai.expect(iterator.down()).to.equal(''); 66 | chai.expect(iterator.down()).to.equal(''); 67 | 68 | // up sequence 69 | chai.expect(iterator.up()).to.equal(1); 70 | chai.expect(iterator.up()).to.equal(2); 71 | chai.expect(iterator.up()).to.equal(3); 72 | chai.expect(iterator.up()).to.equal(4); 73 | chai.expect(iterator.up()).to.equal(5); 74 | 75 | // extra up sequence 76 | chai.expect(iterator.up()).to.equal(5); 77 | chai.expect(iterator.up()).to.equal(5); 78 | 79 | // up/down sequence 80 | chai.expect(iterator.down()).to.equal(4); 81 | chai.expect(iterator.down()).to.equal(3); 82 | chai.expect(iterator.up()).to.equal(4); 83 | chai.expect(iterator.up()).to.equal(5); 84 | chai.expect(iterator.down()).to.equal(4); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/emulator/plugins/history-keyboard.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import HistoryKeyboardPlugin from 'emulator/plugins/HistoryKeyboardPlugin'; 4 | import EmulatorState from 'emulator-state/EmulatorState'; 5 | import Emulator from 'emulator'; 6 | 7 | describe('HistoryKeyboardPlugin', () => { 8 | let historyKeyboardPlugin; 9 | let emulator; 10 | let emulatorState; 11 | 12 | beforeEach(() => { 13 | emulatorState = EmulatorState.createEmpty(); 14 | historyKeyboardPlugin = new HistoryKeyboardPlugin(emulatorState); 15 | emulator = new Emulator(); 16 | }); 17 | 18 | const executeCommand = (commandStr) => { 19 | emulatorState = emulator.execute( 20 | emulatorState, commandStr, [historyKeyboardPlugin] 21 | ); 22 | }; 23 | 24 | it('should reset history iterator when command run', () => { 25 | executeCommand('1'); 26 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('1'); 27 | 28 | executeCommand('2'); 29 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('2'); 30 | 31 | executeCommand('3'); 32 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('3'); 33 | }); 34 | 35 | it('should go up and down to empty string if no commands run', () => { 36 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal(''); 37 | chai.expect(historyKeyboardPlugin.completeDown()).to.equal(''); 38 | }); 39 | 40 | it('should go up and down with single command run', () => { 41 | executeCommand('only command run'); 42 | 43 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('only command run'); 44 | chai.expect(historyKeyboardPlugin.completeDown()).to.equal(''); 45 | }); 46 | 47 | it('should go up/down sequence with two commands run', () => { 48 | executeCommand('1'); 49 | executeCommand('2'); 50 | 51 | // up, up, down, down 52 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('2'); 53 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('1'); 54 | chai.expect(historyKeyboardPlugin.completeDown()).to.equal('2'); 55 | chai.expect(historyKeyboardPlugin.completeDown()).to.equal(''); 56 | }); 57 | 58 | it('should go up/down interleaved sequence with two commands run', () => { 59 | executeCommand('1'); 60 | executeCommand('2'); 61 | 62 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('2'); 63 | chai.expect(historyKeyboardPlugin.completeDown()).to.equal(''); 64 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('2'); 65 | chai.expect(historyKeyboardPlugin.completeDown()).to.equal(''); 66 | }); 67 | 68 | it('should go up and down interleaved sequence with many commands run', () => { 69 | executeCommand('1'); 70 | executeCommand('2'); 71 | executeCommand('3'); 72 | executeCommand('4'); 73 | executeCommand('5'); 74 | executeCommand('6'); 75 | 76 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('6'); 77 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('5'); 78 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('4'); 79 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('3'); 80 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('2'); 81 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('1'); 82 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('1'); 83 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('1'); 84 | chai.expect(historyKeyboardPlugin.completeUp()).to.equal('1'); 85 | chai.expect(historyKeyboardPlugin.completeDown()).to.equal('2'); 86 | }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /test/library.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import * as Terminal from '../lib/terminal.js'; 4 | 5 | describe('Given the Terminal library', () => { 6 | it('should define all API functions', () => { 7 | chai.assert.isDefined(Terminal.Emulator); 8 | chai.assert.isDefined(new Terminal.Emulator()); 9 | 10 | // State API 11 | chai.assert.isDefined(Terminal.CommandMapping); 12 | chai.assert.isDefined(Terminal.EnvironmentVariables); 13 | chai.assert.isDefined(Terminal.Outputs); 14 | chai.assert.isDefined(Terminal.FileSystem); 15 | chai.assert.isDefined(Terminal.History); 16 | chai.assert.isDefined(Terminal.EmulatorState); 17 | 18 | // Output API 19 | chai.assert.isDefined(Terminal.OutputFactory); 20 | chai.assert.isDefined(Terminal.OutputType); 21 | 22 | // FS API 23 | chai.assert.isDefined(Terminal.DirOp); 24 | chai.assert.isDefined(Terminal.FileOp); 25 | 26 | // Parser API 27 | chai.assert.isDefined(Terminal.OptionParser); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/os/fs-error.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import {fsErrorType, makeError} from 'fs/fs-error'; 4 | 5 | describe('fs-error', () => { 6 | it('should create error object with selected FS error type', () => { 7 | const fsError = makeError(fsErrorType.NO_SUCH_FILE); 8 | 9 | chai.expect(fsError).to.deep.equal({ 10 | source: 'fs', 11 | type: fsErrorType.NO_SUCH_FILE, 12 | message: '' 13 | }); 14 | }); 15 | 16 | it('should create error object with error message', () => { 17 | const fsError = makeError(fsErrorType.IS_A_DIRECTORY, 'my message'); 18 | 19 | chai.expect(fsError).to.deep.equal({ 20 | source: 'fs', 21 | type: fsErrorType.IS_A_DIRECTORY, 22 | message: 'my message' 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/os/mocks/mock-fs-permissions.js: -------------------------------------------------------------------------------- 1 | import { create as createFileSystem } from 'emulator-state/file-system'; 2 | 3 | const FS = createFileSystem({ 4 | '/cannot-modify': { 5 | canModify: false 6 | }, 7 | '/cannot-modify/can-modify-file': { 8 | canModify: true, 9 | content: '' 10 | }, 11 | '/cannot-modify/cannot-modify-file': { 12 | canModify: true, 13 | content: '' 14 | }, 15 | '/cannot-modify/can-modify': { 16 | canModify: true 17 | }, 18 | '/cannot-modify/can-modify/can-modify-file': { 19 | canModify: true, 20 | content: '' 21 | }, 22 | '/cannot-modify/can-modify/cannot-modify-file': { 23 | canModify: true, 24 | content: '' 25 | }, 26 | '/can-modify': { 27 | canModify: true 28 | }, 29 | '/can-modify/can-modify-file': { 30 | canModify: true, 31 | content: '' 32 | }, 33 | '/can-modify/cannot-modify-file': { 34 | canModify: false, 35 | content: '' 36 | }, 37 | '/can-modify-secondary': { 38 | canModify: true 39 | } 40 | }); 41 | 42 | export default FS; 43 | -------------------------------------------------------------------------------- /test/os/mocks/mock-fs.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | // Primary folder 4 | export const PRIMARY_FOLDER_PATH = '/primary'; 5 | export const PRIMARY_SUBFOLDER_PATH = '/primary/subfolder'; 6 | export const PRIMARY_FOLDER_FILES = ['foobar']; 7 | export const PRIMARY_FILE_PATH = '/primary/foobar'; 8 | export const PRIMARY_FOLDER = { 9 | '/primary': {}, 10 | '/primary/foobar': {content: ''}, 11 | '/primary/subfolder': {} 12 | }; 13 | 14 | // Secondary folder 15 | export const SECONDARY_FOLDER_PATH = '/secondary'; 16 | export const SECONDARY_FOLDER_FILES = ['foo1', 'foo2', 'foo3']; 17 | export const SECONDARY_FOLDER = { 18 | '/secondary': {}, 19 | '/secondary/foo1': {content: ''}, 20 | '/secondary/foo2': {content: ''}, 21 | '/secondary/foo3': {content: ''} 22 | }; 23 | 24 | // Mock FS 25 | export const MOCK_FS = fromJS({ 26 | '/': {}, 27 | ...PRIMARY_FOLDER, 28 | ...SECONDARY_FOLDER 29 | }); 30 | 31 | export const MOCK_FS_EXC_SECONDARY_FOLDER = fromJS({ 32 | '/': {}, 33 | ...PRIMARY_FOLDER 34 | }); 35 | -------------------------------------------------------------------------------- /test/os/operations-with-permissions/file-operations.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | import spies from 'chai-spies'; 4 | 5 | chai.use(chaiImmutable); 6 | chai.use(spies); 7 | 8 | const sandbox = chai.spy.sandbox(); 9 | 10 | import FS from '../mocks/mock-fs-permissions'; 11 | import * as FileOpsPermissioned from 'fs/operations-with-permissions/file-operations'; 12 | import * as FileOps from 'fs/operations/file-operations'; 13 | import * as FileUtil from 'fs/util/file-util'; 14 | import { fsErrorType } from 'fs/fs-error'; 15 | 16 | describe('file-operations with modification permissions', () => { 17 | before(() => { 18 | sandbox.on(FileOps, [ 19 | 'hasFile', 20 | 'readFile', 21 | 'writeFile', 22 | 'copyFile', 23 | 'deleteFile' 24 | ]); 25 | }); 26 | 27 | describe('hasFile', () => { 28 | it('should use non-permissioned operation with same arguments', () => { 29 | const args = [FS, '/can-modify/can-modify-file']; 30 | 31 | FileOpsPermissioned.hasFile(...args); 32 | chai.expect(FileOps.hasFile).to.have.been.called.with(...args); 33 | }); 34 | }); 35 | 36 | describe('readFile', () => { 37 | it('should use non-permissioned operation with same arguments', () => { 38 | const args = [FS, '/can-modify/can-modify-file']; 39 | 40 | FileOpsPermissioned.readFile(...args); 41 | chai.expect(FileOps.readFile).to.have.been.called.with(...args); 42 | }); 43 | }); 44 | 45 | describe('writeFile', () => { 46 | const NEW_FILE = FileUtil.makeFile(); 47 | 48 | it('should use non-permissioned operation with same arguments', () => { 49 | const args = [FS, '/can-modify/new-file', NEW_FILE]; 50 | 51 | FileOpsPermissioned.writeFile(...args); 52 | chai.expect(FileOps.writeFile).to.have.been.called.with(...args); 53 | }); 54 | 55 | it('should return permissions error if cannot modify directory', () => { 56 | const {err} = FileOpsPermissioned.writeFile( 57 | FS, '/cannot-modify/new-file', NEW_FILE 58 | ); 59 | 60 | chai.expect(err.type).to.equal(fsErrorType.PERMISSION_DENIED); 61 | }); 62 | 63 | it('should return permissions error if cannot modify file', () => { 64 | const {err} = FileOpsPermissioned.writeFile( 65 | FS, '/can-modify/cannot-modify-file', NEW_FILE 66 | ); 67 | 68 | chai.expect(err.type).to.equal(fsErrorType.PERMISSION_DENIED); 69 | }); 70 | }); 71 | 72 | describe('copyFile', () => { 73 | it('should use non-permissioned operation with same arguments', () => { 74 | const args = [FS, '/can-modify/can-modify-file', '/can-modify/dest-file']; 75 | 76 | FileOpsPermissioned.copyFile(...args); 77 | chai.expect(FileOps.copyFile).to.have.been.called.with(...args); 78 | }); 79 | 80 | it('should return permissions error if cannot modify source directory', () => { 81 | const {err} = FileOpsPermissioned.copyFile( 82 | FS, '/cannot-modify/new-file', '/can-modify/dest-file' 83 | ); 84 | 85 | chai.expect(err.type).to.equal(fsErrorType.PERMISSION_DENIED); 86 | }); 87 | 88 | it('should return permissions error if cannot modify source file', () => { 89 | const {err} = FileOpsPermissioned.copyFile( 90 | FS, '/can-modify/cannot-modify-file', '/can-modify/dest-file' 91 | ); 92 | 93 | chai.expect(err.type).to.equal(fsErrorType.PERMISSION_DENIED); 94 | }); 95 | 96 | it('should return permissions error if cannot modify dest directory', () => { 97 | const {err} = FileOpsPermissioned.copyFile( 98 | FS, '/can-modify/new-file', '/cannot-modify/dest-file' 99 | ); 100 | 101 | chai.expect(err.type).to.equal(fsErrorType.PERMISSION_DENIED); 102 | }); 103 | 104 | it('should return permissions error if cannot modify dest file', () => { 105 | const {err} = FileOpsPermissioned.copyFile( 106 | FS, '/can-modify/new-file', '/can-modify/cannot-modify-file' 107 | ); 108 | 109 | chai.expect(err.type).to.equal(fsErrorType.PERMISSION_DENIED); 110 | }); 111 | }); 112 | 113 | describe('deleteFile', () => { 114 | it('should use non-permissioned operation with same arguments', () => { 115 | const args = [FS, '/can-modify/can-modify-file']; 116 | 117 | FileOpsPermissioned.deleteFile(...args); 118 | chai.expect(FileOps.deleteFile).to.have.been.called.with(...args); 119 | }); 120 | 121 | it('should return permissions error if cannot modify directory', () => { 122 | const {err} = FileOpsPermissioned.deleteFile( 123 | FS, '/cannot-modify/can-modify-file' 124 | ); 125 | 126 | chai.expect(err.type).to.equal(fsErrorType.PERMISSION_DENIED); 127 | }); 128 | 129 | it('should return permissions error if cannot modify file', () => { 130 | const {err} = FileOpsPermissioned.deleteFile( 131 | FS, '/can-modify/cannot-modify-file' 132 | ); 133 | 134 | chai.expect(err.type).to.equal(fsErrorType.PERMISSION_DENIED); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/os/operations/base-operations.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import * as BaseOp from 'fs/operations/base-operations'; 6 | import { fsErrorType } from 'fs/fs-error'; 7 | import { create as createFileSystem } from 'emulator-state/file-system'; 8 | 9 | describe('base-operations', () => { 10 | describe('add', () => { 11 | it('should add file system object to root', () => { 12 | const emptyFS = createFileSystem({}); 13 | const {fs} = BaseOp.add(emptyFS, '/', 'added path'); 14 | 15 | chai.expect(fs).to.equal( 16 | createFileSystem({'/': 'added path'}) 17 | ); 18 | }); 19 | 20 | it('should add file system object to nested path', () => { 21 | const emptyFS = createFileSystem({'/': {}}); 22 | const {fs} = BaseOp.add(emptyFS, '/a', 'added path'); 23 | 24 | chai.expect(fs).to.equal( 25 | createFileSystem({'/a': 'added path'}) 26 | ); 27 | }); 28 | 29 | it('should return error if root path already exists', () => { 30 | const rootFS = createFileSystem({'/': {}}); 31 | 32 | const {err} = BaseOp.add(rootFS, '/', 'added path'); 33 | 34 | chai.expect(err.type).to.equal(fsErrorType.FILE_OR_DIRECTORY_EXISTS); 35 | }); 36 | 37 | it('should return error adding path (or directory) to a file', () => { 38 | const rootFS = createFileSystem({'/folderName/fileName': {content: 'file content'}}); 39 | 40 | const { 41 | err: withAddParentPathsErr 42 | } = BaseOp.add(rootFS, '/folderName/fileName/newFolder', 'added path'); 43 | 44 | const { 45 | err: withoutAddParentPathsErr 46 | } = BaseOp.add(rootFS, '/folderName/fileName/newFolder', 'added path', true); 47 | 48 | chai.expect(withAddParentPathsErr.type).to.equal(fsErrorType.NOT_A_DIRECTORY); 49 | chai.expect(withoutAddParentPathsErr.type).to.equal(fsErrorType.NOT_A_DIRECTORY); 50 | }); 51 | 52 | it('should return error if non-root path already exists', () => { 53 | const fs = createFileSystem({'/a/b/c': {}}); 54 | 55 | const {err} = BaseOp.add(fs, '/a/b', 'added path'); 56 | 57 | chai.expect(err.type).to.equal(fsErrorType.FILE_OR_DIRECTORY_EXISTS); 58 | }); 59 | 60 | it('should return error if parent path does not exist', () => { 61 | const fs = createFileSystem({'/a': {}}); 62 | 63 | const {err} = BaseOp.add(fs, '/a/noParent/newDir', 'added path'); 64 | 65 | chai.expect(err.type).to.equal(fsErrorType.NO_SUCH_DIRECTORY); 66 | }); 67 | 68 | it('should add path if parent path does not exist but option set to create parent paths', () => { 69 | const fs = createFileSystem({'/a': {}}); 70 | 71 | const {fs: newFS} = BaseOp.add(fs, '/a/noParent/newDir', 'added path', true); 72 | 73 | chai.expect(newFS).to.equal( 74 | createFileSystem({ 75 | '/a': {}, 76 | '/a/noParent': {}, 77 | '/a/noParent/newDir': 'added path' 78 | }) 79 | ); 80 | }); 81 | }); 82 | 83 | describe('remove', () => { 84 | const removalFS = createFileSystem({ 85 | '/': {}, 86 | '/subdir': {}, 87 | '/subdir/file': {content: 'file content'} 88 | }); 89 | 90 | it('should remove root', () => { 91 | const {fs} = BaseOp.remove(removalFS, '/'); 92 | 93 | chai.expect(fs).to.equal(createFileSystem({})); 94 | }); 95 | 96 | it('should remove file', () => { 97 | const {fs} = BaseOp.remove(removalFS, '/subdir/file'); 98 | 99 | chai.expect(fs).to.equal(createFileSystem({ 100 | '/': {}, 101 | '/subdir': {} 102 | })); 103 | }); 104 | 105 | it('should remove subdirectory', () => { 106 | const {fs} = BaseOp.remove(removalFS, '/subdir'); 107 | 108 | chai.expect(fs).to.equal(createFileSystem({ 109 | '/': {} 110 | })); 111 | }); 112 | 113 | it('should return error if path does not exist', () => { 114 | const {err} = BaseOp.remove(removalFS, '/noSuchDirectory'); 115 | 116 | chai.expect(err.type).to.equal(fsErrorType.NO_SUCH_FILE_OR_DIRECTORY); 117 | }); 118 | 119 | it('should return error cannot remove non-empty directories', () => { 120 | const {err} = BaseOp.remove(removalFS, '/subdir', false); 121 | 122 | chai.expect(err.type).to.equal(fsErrorType.DIRECTORY_NOT_EMPTY); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/os/util/file-util.spec.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import chai from 'chai'; 3 | import chaiImmutable from 'chai-immutable'; 4 | chai.use(chaiImmutable); 5 | 6 | import * as FileUtil from 'fs/util/file-util'; 7 | 8 | describe('file-util', () => { 9 | describe('isFile', () => { 10 | it('should exist', () => { 11 | chai.assert.isFunction(FileUtil.isFile); 12 | }); 13 | 14 | it('should handle directory object', () => { 15 | const dir = fromJS({}); 16 | 17 | chai.expect(FileUtil.isFile(dir)).to.equal(false); 18 | }); 19 | 20 | it('should handle directory with metadata', () => { 21 | const dir = fromJS({ 22 | metadataKey: 'abc' 23 | }); 24 | 25 | chai.expect(FileUtil.isFile(dir)).to.equal(false); 26 | }); 27 | 28 | it('should handle file object with non-empty content', () => { 29 | const nonEmptyFileObject = fromJS({ 30 | content: 'file content' 31 | }); 32 | 33 | chai.expect(FileUtil.isFile(nonEmptyFileObject)).to.equal(true); 34 | }); 35 | 36 | it('should handle file object with empty content', () => { 37 | const emptyFileObject = fromJS({ 38 | content: '' 39 | }); 40 | 41 | chai.expect(FileUtil.isFile(emptyFileObject)).to.equal(true); 42 | }); 43 | }); 44 | 45 | describe('isDirectory', () => { 46 | it('should exist', () => { 47 | chai.assert.isFunction(FileUtil.isDirectory); 48 | }); 49 | 50 | it('should handle directory object', () => { 51 | const dir = fromJS({}); 52 | 53 | chai.expect(FileUtil.isDirectory(dir)).to.equal(true); 54 | }); 55 | 56 | it('should handle directory with metadata', () => { 57 | const dir = fromJS({ 58 | metadataKey: 'abc' 59 | }); 60 | 61 | chai.expect(FileUtil.isDirectory(dir)).to.equal(true); 62 | }); 63 | 64 | it('should handle file object with empty content', () => { 65 | const emptyFileObject = fromJS({ 66 | content: '' 67 | }); 68 | 69 | chai.expect(FileUtil.isDirectory(emptyFileObject)).to.equal(false); 70 | }); 71 | }); 72 | 73 | describe('makeFile', () => { 74 | it('should exist', () => { 75 | chai.assert.isFunction(FileUtil.makeFile); 76 | }); 77 | 78 | it('should make empty file', () => { 79 | const file = FileUtil.makeFile(); 80 | const expectedFile = fromJS({ 81 | content: '' 82 | }); 83 | 84 | chai.expect(file).to.equal(expectedFile); 85 | }); 86 | 87 | it('should make non-empty file', () => { 88 | const file = FileUtil.makeFile('hello world'); 89 | const expectedFile = fromJS({ 90 | content: 'hello world' 91 | }); 92 | 93 | chai.expect(file).to.equal(expectedFile); 94 | }); 95 | 96 | it('should make file without metadata', () => { 97 | const file = FileUtil.makeFile('hello world', {}); 98 | const expectedFile = fromJS({ 99 | content: 'hello world' 100 | }); 101 | 102 | chai.expect(file).to.equal(expectedFile); 103 | }); 104 | 105 | it('should make file with metadata', () => { 106 | const file = FileUtil.makeFile('hello world', { 107 | metadataKey: 'meta value', 108 | permission: 666 109 | }); 110 | const expectedFile = fromJS({ 111 | content: 'hello world', 112 | metadataKey: 'meta value', 113 | permission: 666 114 | }); 115 | 116 | chai.expect(file).to.equal(expectedFile); 117 | }); 118 | }); 119 | 120 | describe('makeDirectory', () => { 121 | it('should exist', () => { 122 | chai.assert.isFunction(FileUtil.makeDirectory); 123 | }); 124 | 125 | it('should make empty directory', () => { 126 | const directory = FileUtil.makeDirectory(); 127 | const expectedDirectory = fromJS({}); 128 | 129 | chai.expect(directory).to.equal(expectedDirectory); 130 | }); 131 | 132 | it('should make directory with metadata', () => { 133 | const directory = FileUtil.makeDirectory({ 134 | metadataKey: 'meta value', 135 | permission: 666 136 | }); 137 | 138 | const expectedDirectory = fromJS({ 139 | metadataKey: 'meta value', 140 | permission: 666 141 | }); 142 | 143 | chai.expect(directory).to.equal(expectedDirectory); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /test/os/util/glob-util.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { Seq, fromJS } from 'immutable'; 3 | 4 | import * as GlobUtil from 'fs/util/glob-util'; 5 | 6 | describe('glob-util', () => { 7 | describe('glob', () => { 8 | it('should exist', () => { 9 | chai.assert.isFunction(GlobUtil.glob); 10 | }); 11 | 12 | // * matches any character except / 13 | it('should match files/folders in directory with *', () => { 14 | chai.expect(GlobUtil.glob('/a/b/c', '/a/b/*')).to.equal(true); 15 | chai.expect(GlobUtil.glob('/a/b/c/d', '/a/b/*')).to.equal(false); 16 | chai.expect(GlobUtil.glob('/a/b', '/a/b/*')).to.equal(false); 17 | }); 18 | 19 | it('should match incomplete file/folder names', () => { 20 | chai.expect(GlobUtil.glob('/a/bbb/c', '/a/b*/*')).to.equal(true); 21 | chai.expect(GlobUtil.glob('/a/bbb/c', '/a/b*/c')).to.equal(true); 22 | }); 23 | 24 | // ** matches any character including / 25 | it('should match multiple subdirectory levels with **', () => { 26 | chai.expect(GlobUtil.glob('/a/b/c', '/a/b/**')).to.equal(true); 27 | chai.expect(GlobUtil.glob('/a/b/c/d', '/a/b/**')).to.equal(true); 28 | chai.expect(GlobUtil.glob('/a/b', '/a/b/**')).to.equal(false); 29 | }); 30 | }); 31 | 32 | describe('globSeq', () => { 33 | it('should exist', () => { 34 | chai.assert.isFunction(GlobUtil.globSeq); 35 | }); 36 | 37 | it('should empty sequence', () => { 38 | const [...paths] = GlobUtil.globSeq( 39 | Seq([]), 40 | '/*' 41 | ); 42 | 43 | chai.expect(paths).to.deep.equal([]); 44 | }); 45 | 46 | it('should match sequence', () => { 47 | const [...paths] = GlobUtil.globSeq( 48 | Seq(['/', '/a', '/b']), 49 | '/*' 50 | ); 51 | 52 | chai.expect(paths).to.deep.equal(['/a', '/b']); 53 | }); 54 | }); 55 | 56 | describe('globPaths', () => { 57 | const mockFS = fromJS({ 58 | '/': {}, 59 | '/a': {}, 60 | '/a/foo': {}, 61 | '/a/foo/bar': {} 62 | }); 63 | 64 | it('should exist', () => { 65 | chai.assert.isFunction(GlobUtil.globPaths); 66 | }); 67 | 68 | it('should match immediate children with * from root directory', () => { 69 | const [...paths] = GlobUtil.globPaths(mockFS, '/*'); 70 | 71 | chai.expect(paths).to.deep.equal(['/a']); 72 | }); 73 | 74 | it('should match immediate children with * from subfolder', () => { 75 | const [...paths] = GlobUtil.globPaths(mockFS, '/a/*'); 76 | 77 | chai.expect(paths).to.deep.equal(['/a/foo']); 78 | }); 79 | 80 | it('should match multiple levels with ** from subfolder', () => { 81 | const [...paths] = GlobUtil.globPaths(mockFS, '/a/**'); 82 | 83 | chai.expect(paths).to.deep.equal(['/a/foo', '/a/foo/bar']); 84 | }); 85 | 86 | it('should match hidden file', () => { 87 | const [...paths] = GlobUtil.globPaths(fromJS({ 88 | '/.hidden': {} 89 | }), '/*'); 90 | 91 | chai.expect(paths).to.deep.equal(['/.hidden']); 92 | }); 93 | }); 94 | 95 | describe('captureGlobPaths', () => { 96 | const mockFS = fromJS({ 97 | '/': {}, 98 | '/a': {}, 99 | '/a/foo': {}, 100 | '/a/foo/bar': {} 101 | }); 102 | 103 | it('should exist', () => { 104 | chai.assert.isFunction(GlobUtil.captureGlobPaths); 105 | }); 106 | 107 | it('should match immediate children names with * from root directory', () => { 108 | const [...paths] = GlobUtil.captureGlobPaths(mockFS, '/*'); 109 | 110 | chai.expect(paths).to.deep.equal(['a']); 111 | }); 112 | 113 | it('should match immediate children names with * from subfolder', () => { 114 | const [...paths] = GlobUtil.captureGlobPaths(mockFS, '/a/*'); 115 | 116 | chai.expect(paths).to.deep.equal(['foo']); 117 | }); 118 | 119 | it('should match multiple level names with ** from subfolder', () => { 120 | const [...paths] = GlobUtil.captureGlobPaths(mockFS, '/a/**'); 121 | 122 | chai.expect(paths).to.deep.equal(['foo', 'foo/bar']); 123 | }); 124 | 125 | it('should match hidden file', () => { 126 | const [...paths] = GlobUtil.captureGlobPaths(fromJS({ 127 | '/.hidden': {} 128 | }), '/*'); 129 | 130 | chai.expect(paths).to.deep.equal(['.hidden']); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/os/util/permission-util.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | chai.use(chaiImmutable); 4 | 5 | import FS from '../mocks/mock-fs-permissions'; 6 | import * as PermissionUtil from 'fs/util/permission-util'; 7 | 8 | describe('file-operations', () => { 9 | describe('directories', () => { 10 | it('should return false if root directory is not readable', () => { 11 | chai.expect( 12 | PermissionUtil.canModifyPath(FS, '/cannot-modify') 13 | ).to.equal(false); 14 | }); 15 | 16 | it('should return false if parent directory is not readable', () => { 17 | chai.expect( 18 | PermissionUtil.canModifyPath(FS, '/cannot-modify/can-modify') 19 | ).to.equal(false); 20 | }); 21 | 22 | it('should return true if directory is readable', () => { 23 | chai.expect( 24 | PermissionUtil.canModifyPath(FS, '/can-modify') 25 | ).to.equal(true); 26 | }); 27 | 28 | it('should return true if directory does not exist', () => { 29 | chai.expect( 30 | PermissionUtil.canModifyPath(FS, '/no-directory') 31 | ).to.equal(true); 32 | }); 33 | }); 34 | 35 | describe('files', () => { 36 | it('should return false can modify file but cannot modify dir', () => { 37 | chai.expect( 38 | PermissionUtil.canModifyPath(FS, '/cannot-modify/can-modify-file') 39 | ).to.equal(false); 40 | }); 41 | 42 | it('should return false cannot modify file and cannot modify dir', () => { 43 | chai.expect( 44 | PermissionUtil.canModifyPath(FS, '/cannot-modify/cannot-modify-file') 45 | ).to.equal(false); 46 | }); 47 | 48 | it('should return false if cannot write parent directory', () => { 49 | chai.expect( 50 | PermissionUtil.canModifyPath(FS, '/cannot-modify/can-modify/can-modify-file') 51 | ).to.equal(false); 52 | }); 53 | 54 | it('should return false if can modify dir but cannot modify file', () => { 55 | chai.expect( 56 | PermissionUtil.canModifyPath(FS, '/can-modify/cannot-modify-file') 57 | ).to.equal(false); 58 | }); 59 | 60 | it('should return true if can modify file and dir', () => { 61 | chai.expect( 62 | PermissionUtil.canModifyPath(FS, '/can-modify/can-modify-file') 63 | ).to.equal(true); 64 | }); 65 | 66 | it('should return true if missing file', () => { 67 | chai.expect( 68 | PermissionUtil.canModifyPath(FS, '/can-modify', 'no-such-file') 69 | ).to.equal(true); 70 | }); 71 | 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/parser/command-parser.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import parseCommands from 'parser/command-parser'; 4 | 5 | describe('command-parser', () => { 6 | it('should parse command with no args', () => { 7 | const parsedCommands = parseCommands('ls'); 8 | 9 | chai.expect(parsedCommands).to.deep.equal([ 10 | { 11 | commandName: 'ls', 12 | commandOptions: [] 13 | } 14 | ]); 15 | }); 16 | 17 | it('should parse command with single anonymous arg', () => { 18 | const parsedCommands = parseCommands('ls a'); 19 | 20 | chai.expect(parsedCommands).to.deep.equal([ 21 | { 22 | commandName: 'ls', 23 | commandOptions: ['a'] 24 | } 25 | ]); 26 | }); 27 | 28 | it('should parse command with text after command name', () => { 29 | const parsedCommands = parseCommands('echo hello world!'); 30 | 31 | chai.expect(parsedCommands).to.deep.equal([ 32 | { 33 | commandName: 'echo', 34 | commandOptions: ['hello', 'world!'] 35 | } 36 | ]); 37 | }); 38 | 39 | it('should parse command with multiple anonymous args', () => { 40 | const parsedCommands = parseCommands('ls a b c'); 41 | 42 | chai.expect(parsedCommands).to.deep.equal([ 43 | { 44 | commandName: 'ls', 45 | commandOptions: ['a', 'b', 'c'] 46 | } 47 | ]); 48 | }); 49 | 50 | it('should parse command with single flag', () => { 51 | const parsedCommands = parseCommands('foo --help'); 52 | 53 | chai.expect(parsedCommands).to.deep.equal([ 54 | { 55 | commandName: 'foo', 56 | commandOptions: ['--help'] 57 | } 58 | ]); 59 | }); 60 | 61 | it('should parse command with excess spaces', () => { 62 | const parsedCommands = parseCommands(' a --b --c'); 63 | 64 | chai.expect(parsedCommands).to.deep.equal([ 65 | { 66 | commandName: 'a', 67 | commandOptions: ['--b', '--c'] 68 | } 69 | ]); 70 | }); 71 | 72 | it('should parse command with tabs instead of spaces', () => { 73 | const parsedCommands = parseCommands('\u00a0a\u00a0--b\u00a0--c'); 74 | 75 | chai.expect(parsedCommands).to.deep.equal([ 76 | { 77 | commandName: 'a', 78 | commandOptions: ['--b', '--c'] 79 | } 80 | ]); 81 | }); 82 | 83 | it('should parse command with excess tabs', () => { 84 | const parsedCommands = parseCommands('\u00a0\u00a0\u00a0\u00a0\u00a0a\u00a0\u00a0--b\u00a0\u00a0--c'); 85 | 86 | chai.expect(parsedCommands).to.deep.equal([ 87 | { 88 | commandName: 'a', 89 | commandOptions: ['--b', '--c'] 90 | } 91 | ]); 92 | }); 93 | 94 | it('should parse command with mixed flag and args', () => { 95 | const parsedCommands = parseCommands('foo --help a/b/c hello.txt -a -h'); 96 | 97 | chai.expect(parsedCommands).to.deep.equal([ 98 | { 99 | commandName: 'foo', 100 | commandOptions: ['--help', 'a/b/c', 'hello.txt', '-a', '-h'] 101 | } 102 | ]); 103 | }); 104 | 105 | it('should parse multiple commands with && and no args', () => { 106 | const parsedCommands = parseCommands('foo && bar'); 107 | 108 | chai.expect(parsedCommands).to.deep.equal([ 109 | { 110 | commandName: 'foo', 111 | commandOptions: [] 112 | }, { 113 | commandName: 'bar', 114 | commandOptions: [] 115 | } 116 | ]); 117 | }); 118 | 119 | it('should parse multiple commands with && and args', () => { 120 | const parsedCommands = parseCommands('foo -a --help && bar --help -b'); 121 | 122 | chai.expect(parsedCommands).to.deep.equal([ 123 | { 124 | commandName: 'foo', 125 | commandOptions: ['-a', '--help'] 126 | }, { 127 | commandName: 'bar', 128 | commandOptions: ['--help', '-b'] 129 | } 130 | ]); 131 | }); 132 | 133 | it('should parse multiple commands with ; and no args', () => { 134 | const parsedCommands = parseCommands('foo; bar'); 135 | 136 | chai.expect(parsedCommands).to.deep.equal([ 137 | { 138 | commandName: 'foo', 139 | commandOptions: [] 140 | }, { 141 | commandName: 'bar', 142 | commandOptions: [] 143 | } 144 | ]); 145 | }); 146 | 147 | it('should parse multiple commands with ; and args', () => { 148 | const parsedCommands = parseCommands('foo -a --help; bar --help -b'); 149 | 150 | chai.expect(parsedCommands).to.deep.equal([ 151 | { 152 | commandName: 'foo', 153 | commandOptions: ['-a', '--help'] 154 | }, { 155 | commandName: 'bar', 156 | commandOptions: ['--help', '-b'] 157 | } 158 | ]); 159 | }); 160 | 161 | it('should parse multiple commands with excess space', () => { 162 | const parsedCommands = parseCommands('foo -a --help ; bar --help -b'); 163 | 164 | chai.expect(parsedCommands).to.deep.equal([ 165 | { 166 | commandName: 'foo', 167 | commandOptions: ['-a', '--help'] 168 | }, { 169 | commandName: 'bar', 170 | commandOptions: ['--help', '-b'] 171 | } 172 | ]); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/parser/opt-parser.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import parseOptions from 'parser/option-parser'; 4 | 5 | // NB: Only rudimentary unit tests as `option-parser` is a wrapper for the 6 | // `get-options` library 7 | 8 | describe('option-parser', () => { 9 | it('should parse options', () => { 10 | const parsedOpts = parseOptions(['-b', 'fooVal', 'barVal', '--alias'], { 11 | '-a, --alias': '', 12 | '-b': ' ' 13 | }); 14 | 15 | chai.expect(parsedOpts).to.deep.equal({ 16 | options: { 17 | b: ['fooVal', 'barVal'], 18 | alias: true 19 | }, 20 | argv: [] 21 | }); 22 | }); 23 | 24 | it('should parse options argv', () => { 25 | const parsedOpts = parseOptions(['the argv', '--alias'], { 26 | '-a, --alias': '' 27 | }); 28 | 29 | chai.expect(parsedOpts).to.deep.equal({ 30 | options: { 31 | alias: true 32 | }, 33 | argv: ['the argv'] 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global __dirname, require, module*/ 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | const { argv: args } = require('yargs'); 6 | 7 | const isProd = args.mode === 'production'; 8 | 9 | let plugins = [ 10 | new webpack.NamedModulesPlugin() 11 | ]; 12 | 13 | const libraryName = 'Terminal'; 14 | 15 | const config = { 16 | entry: { 17 | [libraryName]: [path.join(__dirname, 'src/index.js')] 18 | }, 19 | devtool: 'source-map', 20 | output: { 21 | path: path.join(__dirname, 'lib'), 22 | filename: isProd ? 'terminal.min.js' : 'terminal.js', 23 | library: libraryName, 24 | libraryTarget: 'umd', 25 | umdNamedDefine: true, 26 | // Required to create single build for Node and web targets 27 | // FIXME: https://github.com/webpack/webpack/issues/6522 28 | globalObject: 'this' 29 | }, 30 | module: { 31 | rules: [{ 32 | test: /\.js$/, 33 | exclude: /node_modules/, 34 | use: { 35 | loader: 'babel-loader' 36 | } 37 | }] 38 | }, 39 | resolve: { 40 | modules: [path.resolve('./node_modules'), path.resolve('./src')], 41 | extensions: ['.json', '.js'] 42 | }, 43 | plugins: plugins 44 | }; 45 | 46 | module.exports = config; 47 | --------------------------------------------------------------------------------