├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------