├── .codeclimate.json ├── .eslintrc.js ├── .gitattributes ├── .github └── config.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bin └── create-twilio-function ├── jest.config.js ├── package.json ├── src ├── cli.js ├── command.js ├── create-twilio-function.js └── create-twilio-function │ ├── create-files.js │ ├── create-gitignore.js │ ├── import-credentials.js │ ├── install-dependencies.js │ ├── prompt.js │ ├── success-message.js │ ├── validate-project-name.js │ ├── versions.js │ └── window-size.js ├── templates ├── javascript │ ├── assets │ │ ├── index.html │ │ ├── message.private.js │ │ └── style.css │ └── functions │ │ ├── hello-world.js │ │ ├── private-message.js │ │ └── sms │ │ └── reply.protected.js └── typescript │ └── src │ ├── assets │ ├── index.html │ ├── message.private.ts │ └── style.css │ └── functions │ ├── hello-world.ts │ ├── private-message.ts │ └── sms │ └── reply.protected.ts └── tests ├── create-files.test.js ├── create-gitignore.test.js ├── create-twilio-function.test.js ├── import-credentials.test.js ├── install-dependencies.test.js ├── prompt.test.js ├── success-message.test.js ├── validate-project-name.test.js └── window-size.test.js /.codeclimate.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_patterns": ["templates/", "tests/", "**/node_modules/"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': 'twilio', 3 | 'plugins': ['jest'], 4 | 'env': { 5 | 'jest/globals': true 6 | }, 7 | 'parserOptions': { 8 | 'ecmaVersion': 9, 9 | 'sourceType': 'module', 10 | }, 11 | 'rules': { 12 | 'no-console': 'off' 13 | } 14 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.js text 3 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | newIssueWelcomeComment: | 2 | Thank you so much for opening your first issue in this project! We'll try to get back to it as quickly as possible. While you are waiting...here's a random picture of a corgi (powered by [dog.ceo](https://dog.ceo)) 3 | 4 | ![picture of dog](https://images.dog.ceo/breeds/corgi-cardigan/n02113186_1447.jpg) 5 | 6 | firstPRMergeComment: > 7 | Congratulations on your first contribution to the Serverless Toolkit! 8 | 9 | We'd love to say thank you and send you some swag. If you are interested please fill out this form at https://twil.io/hacktoberfest-swag 10 | 11 | If you are on the look out for more ways to contribute to open-source, 12 | check out a list of some of our repositories at https://github.com/twilio/opensource. 13 | 14 | And if you love Twilio as much as we do, make sure to check out our 15 | [Twilio Champions program](https://www.twilio.com/champions)! 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | 82 | # Package locks 83 | package-lock.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 12 5 | - 'node' 6 | os: 7 | - linux 8 | - windows 9 | script: 10 | - npm test 11 | - npm run lint 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `create-twilio-function` 2 | 3 | ## Ongoing [☰](https://github.com/twilio-labs/create-twilio-function/compare/v2.3.0...master) 4 | 5 | ## 2.2.0 (May 25, 2020) [☰](https://github.com/twilio-labs/create-twilio-function/compare/v2.2.0...v2.3.0) 6 | 7 | - minor updates 8 | - Adds `--typescript` flag that generates a TypeScript project that can be built and deployed to Twilio Functions 9 | 10 | ## 2.2.0 (May 11, 2020) [☰](https://github.com/twilio-labs/create-twilio-function/compare/v2.1.0...v2.2.0) 11 | 12 | - minor updates 13 | - Loosen the Node version to 10 14 | - Updates twilio-run to 2.5.0 15 | - Adds `--empty` option to create empty template 16 | 17 | ## 2.1.0 (January 14, 2020) [☰](https://github.com/twilio-labs/create-twilio-function/compare/v2.0.0...v2.1.0) 18 | 19 | - minor updates 20 | - Validates project names. Names can only include letters, numbers and hyphens 21 | - Adds `npm run deploy` command to generated project which will run `twilio-run deploy` 22 | - Updates Node version output for new functions to 10.17 to match Twilio Functions environment 23 | - Adds a link to the Twilio console to the output asking for credentials 24 | - Lints the code according to eslint-config-twilio 25 | - Improves getting the size of the terminal for setting the output 26 | 27 | ## 2.0.0 (August 4, 2019) [☰](https://github.com/twilio-labs/create-twilio-function/compare/v1.0.2...v2.0.0) 28 | 29 | - Exports details about the cli command so that other projects can consume it. Fixes #12 30 | - Generates new project from the ./templates directory in this project 31 | - Can generate projects based on a template from twilio-labs/function-templates 32 | 33 | ## 1.0.2 (July 10, 2019) [☰](https://github.com/twilio-labs/create-twilio-function/compare/v1.0.1...v1.0.2) 34 | 35 | - Minor updates 36 | - Better error messages if the cli fails to create a directory. Fixes #14 37 | 38 | ## 1.0.1 (May 4, 2019) [☰](https://github.com/twilio-labs/create-twilio-function/compare/v1.0.0...v1.0.1) 39 | 40 | - Minor updates 41 | - Corrected order of arguments in generated example function. Fixes #10 42 | 43 | ## 1.0.0 (April 9, 2019) [☰](https://github.com/twilio-labs/create-twilio-function/commits/v1.0.0) 44 | 45 | Initial release. Includes basic features for creating a new Twilio Functions project setup to use [`twilio-run`](https://github.com/twilio-labs/twilio-run) to run locally. 46 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at philnash@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Twilio Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛑 *`create-twilio-function` is part of the [Serverless Toolkit](https://github.com/twilio-labs/serverless-toolkit/)* 🛑 2 | 3 | *The Serverless Toolkit is a monorepo containing `create-twilio-function`, `twilio-run`, `plugin-serverless`, and other supporting packages that help you create, run, deploy and update functions and assets to [Twilio Functions](https://www.twilio.com/docs/runtime/functions).* 4 | 5 | *This repo is deprecated and development continues under the [Serverless Toolkit repo](https://github.com/twilio-labs/serverless-toolkit).* 6 | 7 | 8 | # `create-twilio-function` 9 | 10 | A command line tool to setup a new [Twilio Function](https://www.twilio.com/docs/api/runtime/functions) with local testing using [`twilio-run`](https://github.com/twilio-labs/twilio-run). 11 | 12 | [![Build Status](https://travis-ci.com/twilio-labs/create-twilio-function.svg?branch=master)](https://travis-ci.com/twilio-labs/create-twilio-function) [![Maintainability](https://api.codeclimate.com/v1/badges/e6f9eb67589927df5d72/maintainability)](https://codeclimate.com/github/twilio-labs/create-twilio-function/maintainability) 13 | 14 | Read more about this tool in the post [start a new Twilio Functions project the easy way](https://www.twilio.com/blog/start-a-new-twilio-functions-project-the-easy-way) 15 | 16 | * [Usage](#usage) 17 | * [`npm init`](#npm-init) 18 | * [Twilio CLI](#twilio-cli) 19 | * [`npx`](#npx) 20 | * [Global installation](#global-installation) 21 | * [Function Templates](#function-templates) 22 | * [TypeScript](#typescript) 23 | * [Command line arguments](#command-line-arguments) 24 | * [Contributing](#contributing) 25 | * [LICENSE](#license) 26 | 27 | ## Usage 28 | 29 | ### `npm init` 30 | 31 | There are a number of ways to use this tool. The quickest and easiest is with `npm init`: 32 | 33 | ```bash 34 | npm init twilio-function function-name 35 | cd function-name 36 | npm start 37 | ``` 38 | 39 | This will create a new directory named "function-name" and include all the files you need to write and run a Twilio Function locally. Starting the application will host the example function at localhost:3000/example. 40 | 41 | ### Twilio CLI 42 | 43 | Make sure you have the [Twilio CLI installed](https://www.twilio.com/docs/twilio-cli/quickstart) with either: 44 | 45 | ```bash 46 | npm install twilio-cli -g 47 | ``` 48 | 49 | or 50 | 51 | ```bash 52 | brew tap twilio/brew && brew install twilio 53 | ``` 54 | 55 | Install the [Twilio Serverless Toolkit](https://www.twilio.com/docs/labs/serverless-toolkit) plugin: 56 | 57 | ```bash 58 | twilio plugins:install @twilio-labs/plugin-serverless 59 | ``` 60 | 61 | Then initialise a new Functions project with: 62 | 63 | ```bash 64 | twilio serverless:init function-name 65 | ``` 66 | 67 | ### `npx` 68 | 69 | You can also use `npx` to run `create-twilio-function`: 70 | 71 | ```bash 72 | npx create-twilio-function function-name 73 | ``` 74 | 75 | ### Global installation 76 | 77 | Or you can install the module globally: 78 | 79 | ```bash 80 | npm install create-twilio-function -g 81 | create-twilio-function function-name 82 | ``` 83 | 84 | ## Function Templates 85 | 86 | `create-twilio-function` enables you to generate a new empty project or to build a project using any of the templates from [the Function Templates](https://github.com/twilio-labs/function-templates) repo. All you need to do is pass a `--template` option with the name of the template you want to download. Like this: 87 | 88 | ```bash 89 | npm init twilio-function function-name --template blank 90 | ``` 91 | 92 | This works with any of the other ways of calling `create-twilio-function`. Check out the [ever expanding list of function templates here](https://github.com/twilio-labs/function-templates). 93 | 94 | ## TypeScript 95 | 96 | If you want to [build your Twilio Functions project in TypeScript](https://www.twilio.com/docs/labs/serverless-toolkit/guides/typescript) you can. `create-twilio-function` supports generating a new project that is set up to use TypeScript too. To generate a TypeScript project, use the `--typescript` flag, like this: 97 | 98 | ```bash 99 | npm init twilio-function function-name --typescript 100 | ``` 101 | 102 | Note: there are no Function templates written in TypeScript, so do not use the `--template` flag alongside the `--typescript` flag. The basic TypeScript project does come with some example files, but you can generate an empty project combining the `--typescript` and `--empty` flags. 103 | 104 | ## Command line arguments 105 | 106 | ``` 107 | Creates a new Twilio Function project 108 | 109 | Commands: 110 | create-twilio-function Creates a new Twilio Function project 111 | [default] 112 | create-twilio-function list-templates Lists the available Twilio Function 113 | templates 114 | 115 | Positionals: 116 | name Name of your project. [string] 117 | 118 | Options: 119 | --account-sid, -a The Account SID for your Twilio account [string] 120 | --auth-token, -t Your Twilio account Auth Token [string] 121 | --skip-credentials Don't ask for Twilio account credentials or import them 122 | from the environment [boolean] [default: false] 123 | --import-credentials Import credentials from the environment variables 124 | TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN 125 | [boolean] [default: false] 126 | --template Initialize your new project with a template from 127 | github.com/twilio-labs/function-templates [string] 128 | --empty Initialize your new project with empty functions and 129 | assets directories [boolean] [default: false] 130 | --typescript Initialize your Serverless project with TypeScript 131 | [boolean] [default: false] 132 | -h, --help Show help [boolean] 133 | -v, --version Show version number [boolean] 134 | --path [default: (cwd)] 135 | ``` 136 | 137 | ## Contributing 138 | 139 | Any help contributing to this project is welcomed. Make sure you read and agree with the [code of conduct](CODE_OF_CONDUCT.md). 140 | 141 | 1. Fork the project 142 | 2. Clone the fork like so: 143 | 144 | ```bash 145 | git clone git@github.com:YOUR_USERNAME/create-twilio-function.git 146 | ``` 147 | 148 | 3. Install the dependencies 149 | 150 | ```bash 151 | cd create-twilio-function 152 | npm install 153 | ``` 154 | 155 | 4. Make your changes 156 | 5. Test your changes with 157 | 158 | ```bash 159 | npm test 160 | ``` 161 | 162 | 6. Commit your changes and open a pull request 163 | 164 | ## LICENSE 165 | 166 | MIT 167 | -------------------------------------------------------------------------------- /bin/create-twilio-function: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../src/cli')(process.cwd).parse(); 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/zj/m36rb_wx6ddb953lr4qlg0b9y6hcv1/T/jest_yio0o1", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: true, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | collectCoverageFrom: [ 25 | 'src/**/*.js', 26 | '!**/node_modules/**', 27 | '!templates/**/*.{js,html,css}' 28 | ], 29 | 30 | // The directory where Jest should output its coverage files 31 | coverageDirectory: 'coverage', 32 | 33 | // An array of regexp pattern strings used to skip coverage collection 34 | // coveragePathIgnorePatterns: [ 35 | // "/node_modules/" 36 | // ], 37 | 38 | // A list of reporter names that Jest uses when writing coverage reports 39 | // coverageReporters: [ 40 | // "json", 41 | // "text", 42 | // "lcov", 43 | // "clover" 44 | // ], 45 | 46 | // An object that configures minimum threshold enforcement for coverage results 47 | // coverageThreshold: null, 48 | 49 | // A path to a custom dependency extractor 50 | // dependencyExtractor: null, 51 | 52 | // Make calling deprecated APIs throw helpful error messages 53 | // errorOnDeprecated: false, 54 | 55 | // Force coverage collection from ignored files using an array of glob patterns 56 | // forceCoverageMatch: [], 57 | 58 | // A path to a module which exports an async function that is triggered once before all test suites 59 | // globalSetup: null, 60 | 61 | // A path to a module which exports an async function that is triggered once after all test suites 62 | // globalTeardown: null, 63 | 64 | // A set of global variables that need to be available in all test environments 65 | // globals: {}, 66 | 67 | // An array of directory names to be searched recursively up from the requiring module's location 68 | // moduleDirectories: [ 69 | // "node_modules" 70 | // ], 71 | 72 | // An array of file extensions your modules use 73 | // moduleFileExtensions: [ 74 | // "js", 75 | // "json", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "node" 80 | // ], 81 | 82 | // A map from regular expressions to module names that allow to stub out resources with a single module 83 | // moduleNameMapper: {}, 84 | 85 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 86 | // modulePathIgnorePatterns: [], 87 | 88 | // Activates notifications for test results 89 | // notify: false, 90 | 91 | // An enum that specifies notification mode. Requires { notify: true } 92 | // notifyMode: "failure-change", 93 | 94 | // A preset that is used as a base for Jest's configuration 95 | // preset: null, 96 | 97 | // Run tests from one or more projects 98 | // projects: null, 99 | 100 | // Use this configuration option to add custom reporters to Jest 101 | // reporters: undefined, 102 | 103 | // Automatically reset mock state between every test 104 | // resetMocks: false, 105 | 106 | // Reset the module registry before running each individual test 107 | // resetModules: false, 108 | 109 | // A path to a custom resolver 110 | // resolver: null, 111 | 112 | // Automatically restore mock state between every test 113 | // restoreMocks: false, 114 | 115 | // The root directory that Jest should scan for tests and modules within 116 | // rootDir: null, 117 | 118 | // A list of paths to directories that Jest should use to search for files in 119 | // roots: [ 120 | // "" 121 | // ], 122 | 123 | // Allows you to use a custom runner instead of Jest's default test runner 124 | // runner: "jest-runner", 125 | 126 | // The paths to modules that run some code to configure or set up the testing environment before each test 127 | // setupFiles: [], 128 | 129 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 130 | // setupFilesAfterEnv: [], 131 | 132 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 133 | // snapshotSerializers: [], 134 | 135 | // The test environment that will be used for testing 136 | testEnvironment: 'node' 137 | 138 | // Options that will be passed to the testEnvironment 139 | // testEnvironmentOptions: {}, 140 | 141 | // Adds a location field to test results 142 | // testLocationInResults: false, 143 | 144 | // The glob patterns Jest uses to detect test files 145 | // testMatch: [ 146 | // "**/__tests__/**/*.[jt]s?(x)", 147 | // "**/?(*.)+(spec|test).[tj]s?(x)" 148 | // ], 149 | 150 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 151 | // testPathIgnorePatterns: [ 152 | // "/node_modules/" 153 | // ], 154 | 155 | // The regexp pattern or array of patterns that Jest uses to detect test files 156 | // testRegex: [], 157 | 158 | // This option allows the use of a custom results processor 159 | // testResultsProcessor: null, 160 | 161 | // This option allows use of a custom test runner 162 | // testRunner: "jasmine2", 163 | 164 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 165 | // testURL: "http://localhost", 166 | 167 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 168 | // timers: "real", 169 | 170 | // A map from regular expressions to paths to transformers 171 | // transform: null, 172 | 173 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 174 | // transformIgnorePatterns: [ 175 | // "/node_modules/" 176 | // ], 177 | 178 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 179 | // unmockedModulePathPatterns: undefined, 180 | 181 | // Indicates whether each individual test should be reported during the run 182 | // verbose: null, 183 | 184 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 185 | // watchPathIgnorePatterns: [], 186 | 187 | // Whether to use watchman for file crawling 188 | // watchman: true, 189 | }; 190 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-twilio-function", 3 | "version": "2.3.0", 4 | "description": "A CLI tool to generate a new Twilio Function using that can be run locally with twilio-run.", 5 | "bin": "./bin/create-twilio-function", 6 | "main": "./src/create-twilio-function.js", 7 | "scripts": { 8 | "test": "jest", 9 | "lint": "eslint src tests", 10 | "lint:fix": "npm run lint -- --fix" 11 | }, 12 | "keywords": [ 13 | "twilio", 14 | "twilio-functions", 15 | "serverless" 16 | ], 17 | "author": "Phil Nash (https://philna.sh)", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/twilio-labs/create-twilio-function.git" 21 | }, 22 | "homepage": "https://github.com/twilio-labs/create-twilio-function", 23 | "bugs": { 24 | "url": "https://github.com/twilio-labs/create-twilio-function/issues" 25 | }, 26 | "license": "MIT", 27 | "devDependencies": { 28 | "eslint": "^6.5.1", 29 | "eslint-config-twilio": "^1.23.0", 30 | "eslint-plugin-import": "^2.18.2", 31 | "eslint-plugin-jest": "^22.17.0", 32 | "eslint-plugin-node": "^10.0.0", 33 | "eslint-plugin-promise": "^4.2.1", 34 | "eslint-plugin-standard": "^4.0.1", 35 | "jest": "^24.5.0", 36 | "nock": "^11.3.4" 37 | }, 38 | "dependencies": { 39 | "boxen": "^3.0.0", 40 | "chalk": "^2.4.2", 41 | "gitignore": "^0.6.0", 42 | "inquirer": "^6.2.2", 43 | "ora": "^3.2.0", 44 | "pkg-install": "^1.0.0", 45 | "rimraf": "^2.6.3", 46 | "terminal-link": "^2.0.0", 47 | "twilio-run": "^2.5.0", 48 | "window-size": "^1.1.1", 49 | "wrap-ansi": "^6.0.0", 50 | "yargs": "^12.0.5" 51 | }, 52 | "engines": { 53 | "node": ">=10.17.0" 54 | }, 55 | "files": [ 56 | "bin/", 57 | "src/", 58 | "templates/" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs'); 2 | const ListTemplateCommand = require('twilio-run/dist/commands/list-templates'); 3 | 4 | const DefaultCommand = require('./command'); 5 | 6 | function cli(cwd) { 7 | yargs.help(); 8 | yargs.alias('h', 'help'); 9 | yargs.version(); 10 | yargs.alias('v', 'version'); 11 | yargs.default('path', cwd); 12 | yargs.usage(DefaultCommand.describe); 13 | yargs.command(DefaultCommand); 14 | yargs.command(ListTemplateCommand); 15 | return yargs; 16 | } 17 | 18 | module.exports = cli; 19 | -------------------------------------------------------------------------------- /src/command.js: -------------------------------------------------------------------------------- 1 | const handler = require('./create-twilio-function'); 2 | 3 | const command = '$0 '; 4 | const describe = 'Creates a new Twilio Function project'; 5 | 6 | const cliInfo = { 7 | options: { 8 | 'account-sid': { 9 | alias: 'a', 10 | describe: 'The Account SID for your Twilio account', 11 | type: 'string', 12 | }, 13 | 'auth-token': { 14 | alias: 't', 15 | describe: 'Your Twilio account Auth Token', 16 | type: 'string', 17 | }, 18 | 'skip-credentials': { 19 | describe: "Don't ask for Twilio account credentials or import them from the environment", 20 | type: 'boolean', 21 | default: false, 22 | }, 23 | 'import-credentials': { 24 | describe: 'Import credentials from the environment variables TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN', 25 | type: 'boolean', 26 | default: false, 27 | }, 28 | template: { 29 | describe: 'Initialize your new project with a template from github.com/twilio-labs/function-templates', 30 | type: 'string', 31 | }, 32 | empty: { 33 | describe: 'Initialize your new project with empty functions and assets directories', 34 | type: 'boolean', 35 | default: false, 36 | }, 37 | typescript: { 38 | describe: 'Initialize your Serverless project with TypeScript', 39 | type: 'boolean', 40 | default: false, 41 | }, 42 | }, 43 | }; 44 | 45 | function builder(cmd) { 46 | cmd.positional('name', { 47 | describe: 'Name of your project.', 48 | type: 'string', 49 | }); 50 | cmd.options(cliInfo.options); 51 | } 52 | 53 | module.exports = { 54 | command, 55 | describe, 56 | handler, 57 | cliInfo, 58 | builder, 59 | }; 60 | -------------------------------------------------------------------------------- /src/create-twilio-function.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util'); 2 | const path = require('path'); 3 | 4 | const ora = require('ora'); 5 | const boxen = require('boxen'); 6 | const rimraf = promisify(require('rimraf')); 7 | const { downloadTemplate } = require('twilio-run/dist/templating/actions'); 8 | 9 | const { promptForAccountDetails, promptForProjectName } = require('./create-twilio-function/prompt'); 10 | const validateProjectName = require('./create-twilio-function/validate-project-name'); 11 | const { 12 | createDirectory, 13 | createEnvFile, 14 | createExampleFromTemplates, 15 | createPackageJSON, 16 | createNvmrcFile, 17 | createTsconfigFile, 18 | createEmptyFileStructure, 19 | } = require('./create-twilio-function/create-files'); 20 | const createGitignore = require('./create-twilio-function/create-gitignore'); 21 | const importCredentials = require('./create-twilio-function/import-credentials'); 22 | const { installDependencies } = require('./create-twilio-function/install-dependencies'); 23 | const successMessage = require('./create-twilio-function/success-message'); 24 | 25 | async function cleanUpAndExit(projectDir, spinner, errorMessage) { 26 | spinner.fail(errorMessage); 27 | spinner.start('Cleaning up project directories and files'); 28 | await rimraf(projectDir); 29 | spinner.stop().clear(); 30 | process.exitCode = 1; 31 | } 32 | 33 | async function performTaskWithSpinner(spinner, message, task) { 34 | spinner.start(message); 35 | await task(); 36 | spinner.succeed(); 37 | } 38 | 39 | async function createTwilioFunction(config) { 40 | const { valid, errors } = validateProjectName(config.name); 41 | if (!valid) { 42 | const { name } = await promptForProjectName(errors); 43 | config.name = name; 44 | } 45 | const projectDir = path.join(config.path, config.name); 46 | const projectType = config.typescript ? 'typescript' : 'javascript'; 47 | const spinner = ora(); 48 | 49 | try { 50 | await performTaskWithSpinner(spinner, 'Creating project directory', async () => { 51 | await createDirectory(config.path, config.name); 52 | }); 53 | } catch (e) { 54 | if (e.code === 'EEXIST') { 55 | spinner.fail( 56 | `A directory called '${config.name}' already exists. Please create your function in a new directory.`, 57 | ); 58 | } else if (e.code === 'EACCES') { 59 | spinner.fail(`You do not have permission to create files or directories in the path '${config.path}'.`); 60 | } else { 61 | spinner.fail(e.message); 62 | } 63 | process.exitCode = 1; 64 | return; 65 | } 66 | 67 | // Check to see if the request is valid for a template or an empty project 68 | if (config.empty && config.template) { 69 | await cleanUpAndExit( 70 | projectDir, 71 | spinner, 72 | 'You cannot scaffold an empty Functions project with a template. Please choose empty or a template.', 73 | ); 74 | return; 75 | } 76 | // Check to see if the project wants typescript and a template 77 | if (config.template && projectType === 'typescript') { 78 | await cleanUpAndExit( 79 | projectDir, 80 | spinner, 81 | 'There are no TypeScript templates available. You can generate an example project or an empty one with the --empty flag.', 82 | ); 83 | return; 84 | } 85 | 86 | // Get account sid and auth token 87 | let accountDetails = await importCredentials(config); 88 | if (Object.keys(accountDetails).length === 0) { 89 | accountDetails = await promptForAccountDetails(config); 90 | } 91 | config = { ...accountDetails, ...config }; 92 | 93 | // Scaffold project 94 | spinner.start('Creating project directories and files'); 95 | 96 | await createEnvFile(projectDir, { 97 | accountSid: config.accountSid, 98 | authToken: config.authToken, 99 | }); 100 | await createNvmrcFile(projectDir); 101 | await createPackageJSON(projectDir, config.name, projectType); 102 | if (projectType === 'typescript') { 103 | await createTsconfigFile(projectDir); 104 | } 105 | if (config.template) { 106 | spinner.succeed(); 107 | spinner.start(`Downloading template: "${config.template}"`); 108 | await createDirectory(projectDir, 'functions'); 109 | await createDirectory(projectDir, 'assets'); 110 | try { 111 | await downloadTemplate(config.template, '', projectDir); 112 | } catch (err) { 113 | await cleanUpAndExit(projectDir, spinner, `The template "${config.template}" doesn't exist`); 114 | return; 115 | } 116 | } else if (config.empty) { 117 | await createEmptyFileStructure(projectDir, projectType); 118 | } else { 119 | await createExampleFromTemplates(projectDir, projectType); 120 | } 121 | spinner.succeed(); 122 | 123 | // Download .gitignore file from https://github.com/github/gitignore/ 124 | try { 125 | await performTaskWithSpinner(spinner, 'Downloading .gitignore file', async () => { 126 | await createGitignore(projectDir); 127 | }); 128 | } catch (err) { 129 | cleanUpAndExit(projectDir, spinner, 'Could not download .gitignore file'); 130 | return; 131 | } 132 | 133 | // Install dependencies with npm 134 | try { 135 | await performTaskWithSpinner(spinner, 'Installing dependencies', async () => { 136 | await installDependencies(projectDir); 137 | }); 138 | } catch (err) { 139 | spinner.fail(); 140 | console.log( 141 | `There was an error installing the dependencies, but your project is otherwise complete in ./${config.name}`, 142 | ); 143 | } 144 | 145 | // Success message 146 | 147 | console.log(boxen(await successMessage(config), { padding: 1, borderStyle: 'round' })); 148 | } 149 | 150 | module.exports = createTwilioFunction; 151 | -------------------------------------------------------------------------------- /src/create-twilio-function/create-files.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { promisify } = require('util'); 4 | 5 | const versions = require('./versions'); 6 | 7 | const mkdir = promisify(fs.mkdir); 8 | const writeFile = promisify(fs.writeFile); 9 | const readdir = promisify(fs.readdir); 10 | const copyFile = promisify(fs.copyFile); 11 | const { COPYFILE_EXCL } = fs.constants; 12 | const stat = promisify(fs.stat); 13 | 14 | function createDirectory(pathName, dirName) { 15 | return mkdir(path.join(pathName, dirName)); 16 | } 17 | 18 | async function createFile(fullPath, content) { 19 | return writeFile(fullPath, content, { flag: 'wx' }); 20 | } 21 | 22 | const javaScriptDeps = {}; 23 | const typescriptDeps = { '@twilio-labs/serverless-runtime-types': versions.serverlessRuntimeTypes }; 24 | const javaScriptDevDeps = { 'twilio-run': versions.twilioRun }; 25 | const typescriptDevDeps = { 26 | 'twilio-run': versions.twilioRun, 27 | typescript: versions.typescript, 28 | copyfiles: versions.copyfiles, 29 | }; 30 | 31 | function createPackageJSON(pathName, name, projectType = 'javascript') { 32 | const fullPath = path.join(pathName, 'package.json'); 33 | const scripts = { 34 | test: 'echo "Error: no test specified" && exit 1', 35 | start: 'twilio-run', 36 | deploy: 'twilio-run deploy', 37 | }; 38 | if (projectType === 'typescript') { 39 | scripts.test = 'tsc --noEmit'; 40 | scripts.build = 'tsc && npm run build:copy-assets'; 41 | scripts['build:copy-assets'] = 'copyfiles src/assets/* src/assets/**/* --up 2 --exclude **/*.ts dist/assets/'; 42 | scripts.prestart = 'npm run build'; 43 | scripts.predeploy = 'npm run build'; 44 | scripts.start += ' --functions-folder dist/functions --assets-folder dist/assets'; 45 | scripts.deploy += ' --functions-folder dist/functions --assets-folder dist/assets'; 46 | } 47 | const packageJSON = JSON.stringify( 48 | { 49 | name, 50 | version: '0.0.0', 51 | private: true, 52 | scripts, 53 | dependencies: projectType === 'typescript' ? typescriptDeps : javaScriptDeps, 54 | devDependencies: projectType === 'typescript' ? typescriptDevDeps : javaScriptDevDeps, 55 | engines: { node: versions.node }, 56 | }, 57 | null, 58 | 2, 59 | ); 60 | return createFile(fullPath, packageJSON); 61 | } 62 | 63 | function copyRecursively(src, dest) { 64 | return readdir(src).then((children) => { 65 | return Promise.all( 66 | children.map((child) => 67 | stat(path.join(src, child)).then((stats) => { 68 | if (stats.isDirectory()) { 69 | return mkdir(path.join(dest, child)).then(() => 70 | copyRecursively(path.join(src, child), path.join(dest, child)), 71 | ); 72 | } 73 | return copyFile(path.join(src, child), path.join(dest, child), COPYFILE_EXCL); 74 | }), 75 | ), 76 | ); 77 | }); 78 | } 79 | 80 | function createExampleFromTemplates(pathName, projectType = 'javascript') { 81 | return copyRecursively(path.join(__dirname, '..', '..', 'templates', projectType), pathName); 82 | } 83 | 84 | function createEnvFile(pathName, { accountSid, authToken }) { 85 | const fullPath = path.join(pathName, '.env'); 86 | const content = `ACCOUNT_SID=${accountSid} 87 | AUTH_TOKEN=${authToken}`; 88 | return createFile(fullPath, content); 89 | } 90 | 91 | function createNvmrcFile(pathName) { 92 | const fullPath = path.join(pathName, '.nvmrc'); 93 | const content = versions.node; 94 | return createFile(fullPath, content); 95 | } 96 | 97 | function createTsconfigFile(pathName) { 98 | const fullPath = path.join(pathName, 'tsconfig.json'); 99 | return createFile( 100 | fullPath, 101 | JSON.stringify( 102 | { 103 | compilerOptions: { 104 | target: 'es5', 105 | module: 'commonjs', 106 | strict: true, 107 | esModuleInterop: true, 108 | outDir: 'dist', 109 | skipLibCheck: true, 110 | sourceMap: true, 111 | }, 112 | }, 113 | null, 114 | 2, 115 | ), 116 | ); 117 | } 118 | 119 | async function createEmptyFileStructure(pathName, projectType) { 120 | if (projectType === 'typescript') { 121 | await createDirectory(pathName, 'src'); 122 | await createDirectory(pathName, path.join('src', 'functions')); 123 | await createDirectory(pathName, path.join('src', 'assets')); 124 | } else { 125 | await createDirectory(pathName, 'functions'); 126 | await createDirectory(pathName, 'assets'); 127 | } 128 | } 129 | 130 | module.exports = { 131 | createDirectory, 132 | createPackageJSON, 133 | createExampleFromTemplates, 134 | createEnvFile, 135 | createNvmrcFile, 136 | createTsconfigFile, 137 | createEmptyFileStructure, 138 | }; 139 | -------------------------------------------------------------------------------- /src/create-twilio-function/create-gitignore.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { promisify } = require('util'); 4 | 5 | const writeGitignore = promisify(require('gitignore').writeFile); 6 | 7 | const open = promisify(fs.open); 8 | 9 | function createGitignore(dirPath) { 10 | const fullPath = path.join(dirPath, '.gitignore'); 11 | return open(fullPath, 'wx').then((fd) => { 12 | const stream = fs.createWriteStream(null, { fd }); 13 | return writeGitignore({ 14 | type: 'Node', 15 | file: stream, 16 | }); 17 | }); 18 | } 19 | 20 | module.exports = createGitignore; 21 | -------------------------------------------------------------------------------- /src/create-twilio-function/import-credentials.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const questions = [ 4 | { 5 | type: 'confirm', 6 | name: 'importedCredentials', 7 | message: 'Your account credentials have been found in your environment variables. Import them?', 8 | default: true, 9 | }, 10 | ]; 11 | 12 | async function importCredentials(config) { 13 | if (config.skipCredentials) { 14 | return {}; 15 | } 16 | 17 | const credentials = { 18 | accountSid: process.env.TWILIO_ACCOUNT_SID, 19 | authToken: process.env.TWILIO_AUTH_TOKEN, 20 | }; 21 | if (typeof credentials.accountSid === 'undefined' && typeof credentials.authToken === 'undefined') { 22 | return {}; 23 | } 24 | 25 | if (config.importedCredentials) { 26 | return credentials; 27 | } 28 | 29 | const { importedCredentials } = await inquirer.prompt(questions); 30 | return importedCredentials ? credentials : {}; 31 | } 32 | 33 | module.exports = importCredentials; 34 | -------------------------------------------------------------------------------- /src/create-twilio-function/install-dependencies.js: -------------------------------------------------------------------------------- 1 | const { projectInstall } = require('pkg-install'); 2 | 3 | async function installDependencies(targetDirectory) { 4 | const options = { cwd: targetDirectory }; 5 | const { stdout } = await projectInstall(options); 6 | return stdout; 7 | } 8 | 9 | module.exports = { installDependencies }; 10 | -------------------------------------------------------------------------------- /src/create-twilio-function/prompt.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const terminalLink = require('terminal-link'); 3 | 4 | const validateProjectName = require('./validate-project-name'); 5 | 6 | function validateAccountSid(input) { 7 | if (input.startsWith('AC') || input === '') { 8 | return true; 9 | } 10 | return 'An Account SID starts with "AC".'; 11 | } 12 | 13 | const accountSidQuestion = { 14 | type: 'input', 15 | name: 'accountSid', 16 | message: 'Twilio Account SID', 17 | validate: validateAccountSid, 18 | }; 19 | 20 | const authTokenQuestion = { 21 | type: 'password', 22 | name: 'authToken', 23 | message: 'Twilio auth token', 24 | }; 25 | 26 | function promptForAccountDetails(config) { 27 | if (config.skipCredentials) { 28 | return {}; 29 | } 30 | const questions = []; 31 | if (typeof config.accountSid === 'undefined') { 32 | questions.push(accountSidQuestion); 33 | } 34 | if (typeof config.authToken === 'undefined') { 35 | questions.push(authTokenQuestion); 36 | } 37 | if (questions.length > 0) { 38 | console.log( 39 | `Please enter your Twilio credentials which you can find in your ${terminalLink( 40 | 'Twilio console', 41 | 'https://twil.io/your-console', 42 | )}.`, 43 | ); 44 | } 45 | return inquirer.prompt(questions); 46 | } 47 | 48 | function promptForProjectName(err) { 49 | const questions = [ 50 | { 51 | type: 'input', 52 | name: 'name', 53 | message: `Project names ${err.join(', ')}. Please choose a new project name.`, 54 | validate: (name) => { 55 | const { valid, errors } = validateProjectName(name); 56 | if (valid) { 57 | return valid; 58 | } 59 | return `Project ${errors.join(', ')}.`; 60 | }, 61 | }, 62 | ]; 63 | return inquirer.prompt(questions); 64 | } 65 | 66 | module.exports = { 67 | promptForAccountDetails, 68 | promptForProjectName, 69 | validateAccountSid, 70 | }; 71 | -------------------------------------------------------------------------------- /src/create-twilio-function/success-message.js: -------------------------------------------------------------------------------- 1 | const { getPackageManager } = require('pkg-install'); 2 | const chalk = require('chalk'); 3 | const wrap = require('wrap-ansi'); 4 | 5 | const getWindowSize = require('./window-size'); 6 | 7 | async function successMessage(config) { 8 | const packageManager = await getPackageManager({ cwd: process.cwd() }); 9 | return wrap( 10 | chalk`{green Success!} 11 | 12 | Created {bold ${config.name}} at {bold ${config.path}} 13 | 14 | Inside that directory, you can run the following command: 15 | 16 | {blue ${packageManager} start} 17 | Serves all functions in the ./functions subdirectory and assets in the 18 | ./assets directory 19 | 20 | Get started by running: 21 | 22 | {blue cd ${config.name}} 23 | {blue ${packageManager} start}`, 24 | getWindowSize().width - 8, 25 | { 26 | trim: false, 27 | hard: true, 28 | }, 29 | ); 30 | } 31 | 32 | module.exports = successMessage; 33 | -------------------------------------------------------------------------------- /src/create-twilio-function/validate-project-name.js: -------------------------------------------------------------------------------- 1 | function assertContainsLettersNumbersHyphens(name) { 2 | const nameRegex = /^[A-Za-z0-9-]+$/; 3 | return Boolean(name.match(nameRegex)); 4 | } 5 | 6 | function assertDoesntStartWithHyphen(name) { 7 | return !name.startsWith('-'); 8 | } 9 | 10 | function assertDoesntEndWithHyphen(name) { 11 | return !name.endsWith('-'); 12 | } 13 | 14 | function assertNotLongerThan(name, chars = 32) { 15 | return name.length <= chars; 16 | } 17 | 18 | function validateProjectName(name) { 19 | let valid = true; 20 | const errors = []; 21 | if (!assertNotLongerThan(name, 32)) { 22 | valid = false; 23 | errors.push('must be shorter than 32 characters'); 24 | } 25 | if (!assertContainsLettersNumbersHyphens(name)) { 26 | valid = false; 27 | errors.push('must only include letters, numbers and hyphens'); 28 | } 29 | if (!assertDoesntStartWithHyphen(name)) { 30 | valid = false; 31 | errors.push('must not start with a hyphen'); 32 | } 33 | if (!assertDoesntEndWithHyphen(name)) { 34 | valid = false; 35 | errors.push('must not end with a hyphen'); 36 | } 37 | return { 38 | valid, 39 | errors, 40 | }; 41 | } 42 | 43 | module.exports = validateProjectName; 44 | -------------------------------------------------------------------------------- /src/create-twilio-function/versions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | twilioRun: '^2.6.0', 3 | node: '10', 4 | typescript: '^3.8', 5 | serverlessRuntimeTypes: '^1.1', 6 | copyfiles: '^2.2.0', 7 | }; 8 | -------------------------------------------------------------------------------- /src/create-twilio-function/window-size.js: -------------------------------------------------------------------------------- 1 | const windowSize = require('window-size'); 2 | 3 | function getWindowSize() { 4 | const defaultSize = { 5 | width: 80, 6 | height: 300, 7 | }; 8 | const currentSize = windowSize.get(); 9 | 10 | if (!currentSize) { 11 | return defaultSize; 12 | } 13 | if (!currentSize.width) { 14 | currentSize.width = defaultSize.width; 15 | } 16 | if (!currentSize.height) { 17 | currentSize.height = defaultSize.height; 18 | } 19 | return currentSize; 20 | } 21 | 22 | module.exports = getWindowSize; 23 | -------------------------------------------------------------------------------- /templates/javascript/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello Twilio Serverless! 9 | 10 | 11 |

Hello Twilio Serverless!

12 | 13 |
14 |

15 | Congratulations you just started a new Twilio 16 | Serverless project. 17 |

18 | 19 |

Assets

20 |

21 | Assets are static files, like HTML, CSS, JavaScript, images or audio 22 | files. 23 |

24 | 25 |

26 | This HTML page is an example of a public asset, you can 27 | access this by loading it in the browser. The HTML also refers to 28 | another public asset for CSS styles. 29 |

30 |

31 | You can also have private assets, there is an example 32 | private asset called message.private.js in the 33 | /assets directory. This file cannot be loaded in the 34 | browser, but you can load it as part of a function by finding its path 35 | using Runtime.getAssets() and then requiring the file. 36 | There is an example of this in 37 | /functions/private-message.js. 38 |

39 | 40 |

Functions

41 |

42 | Functions are JavaScript files that will respond to incoming HTTP 43 | requests. There are public and 44 | protected functions. 45 |

46 | 47 |

48 | Public functions respond to all HTTP requests. There is 49 | an example of a public function in 50 | /functions/hello-world.js. 51 |

52 | 53 |

54 | Protected functions will only respond to HTTP requests 55 | with a valid Twilio signature in the header. You can read more about 56 | validating requests from Twilio in the documentation. There is an example of a protected function in 59 | /functions/sms/reply.protected.js 60 |

61 | 62 |

twilio-run

63 | 64 |

65 | Functions and assets are served, deployed and debugged using 66 | twilio-run. You can serve the project locally with the command 69 | npm start which is really running 70 | twilio-run --env under the hood. If you want to see what 71 | else you can do with twilio-run enter 72 | npx twilio-run --help on the command line or check out 73 | the project documentation on GitHub. 76 |

77 |
78 | 79 |
80 |

81 | Made with 💖 by your friends at 82 | Twilio 83 |

84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /templates/javascript/assets/message.private.js: -------------------------------------------------------------------------------- 1 | const privateMessage = () => { 2 | return 'This is private!'; 3 | }; 4 | 5 | module.exports = privateMessage; 6 | -------------------------------------------------------------------------------- /templates/javascript/assets/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | ::selection { 8 | background: #f22f46; 9 | color: white; 10 | } 11 | 12 | body { 13 | font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 14 | 'Helvetica Neue', Arial, sans-serif; 15 | color: #0d122b; 16 | border-top: 5px solid #f22f46; 17 | } 18 | 19 | header { 20 | padding: 2em; 21 | margin-bottom: 2em; 22 | max-width: 800px; 23 | margin: 0 auto; 24 | } 25 | 26 | header h1 { 27 | padding-bottom: 14px; 28 | border-bottom: 1px solid rgba(148, 151, 155, 0.2); 29 | } 30 | 31 | a { 32 | color: #008cff; 33 | } 34 | 35 | main { 36 | margin: 0 auto 6em; 37 | padding: 0 2em; 38 | max-width: 800px; 39 | } 40 | 41 | main p { 42 | margin-bottom: 2em; 43 | } 44 | 45 | main p code { 46 | font-size: 16px; 47 | font-family: 'Fira Mono', monospace; 48 | color: #f22f46; 49 | background-color: #f9f9f9; 50 | box-shadow: inset 0 0 0 1px #e8e8e8; 51 | font-size: inherit; 52 | line-height: 1.2; 53 | padding: 0.15em 0.4em; 54 | border-radius: 4px; 55 | display: inline-block; 56 | white-space: pre-wrap; 57 | } 58 | 59 | main h2 { 60 | margin-bottom: 1em; 61 | } 62 | 63 | footer { 64 | margin: 0 auto; 65 | max-width: 800px; 66 | text-align: center; 67 | } 68 | 69 | footer p { 70 | border-top: 1px solid rgba(148, 151, 155, 0.2); 71 | padding-top: 2em; 72 | margin: 0 2em; 73 | } 74 | -------------------------------------------------------------------------------- /templates/javascript/functions/hello-world.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | const twiml = new Twilio.twiml.VoiceResponse(); 3 | twiml.say('Hello World!'); 4 | callback(null, twiml); 5 | }; 6 | -------------------------------------------------------------------------------- /templates/javascript/functions/private-message.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | const assets = Runtime.getAssets(); 3 | const privateMessageAsset = assets['/message.js']; 4 | const privateMessagePath = privateMessageAsset.path; 5 | const privateMessage = require(privateMessagePath); 6 | const twiml = new Twilio.twiml.MessagingResponse(); 7 | twiml.message(privateMessage()); 8 | callback(null, twiml); 9 | }; 10 | -------------------------------------------------------------------------------- /templates/javascript/functions/sms/reply.protected.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | const twiml = new Twilio.twiml.MessagingResponse(); 3 | twiml.message('Hello World!'); 4 | callback(null, twiml); 5 | }; 6 | -------------------------------------------------------------------------------- /templates/typescript/src/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello Twilio Serverless! 9 | 10 | 11 |

Hello Twilio Serverless!

12 | 13 |
14 |

15 | Congratulations you just started a new Twilio 16 | Serverless project in TypeScript. 17 |

18 | 19 |

Assets

20 |

21 | Assets are static files, like HTML, CSS, JavaScript, images or audio 22 | files. TypeScript files will be compiled to JavaScript before deploying. 23 |

24 | 25 |

26 | This HTML page is an example of a public asset, you can 27 | access this by loading it in the browser. The HTML also refers to 28 | another public asset for CSS styles. 29 |

30 |

31 | You can also have private assets, there is an example 32 | private asset called message.private.ts in the 33 | /assets directory. This file cannot be loaded in the 34 | browser, but you can load it as part of a function by finding its path 35 | using Runtime.getAssets() and then requiring the file. 36 | There is an example of this in 37 | /functions/private-message.ts. 38 |

39 | 40 |

Functions

41 |

42 | Functions are JavaScript files that will respond to incoming HTTP 43 | requests. There are public and 44 | protected functions. 45 |

46 | 47 |

48 | Public functions respond to all HTTP requests. There is 49 | an example of a public function in 50 | /functions/hello-world.ts. 51 |

52 | 53 |

54 | Protected functions will only respond to HTTP requests 55 | with a valid Twilio signature in the header. You can read more about 56 | validating requests from Twilio in the documentation. There is an example of a protected function in 59 | /functions/sms/reply.protected.ts 60 |

61 | 62 |

twilio-run

63 | 64 |

65 | Functions and assets are served, deployed and debugged using 66 | twilio-run. You can serve the project locally with the command 69 | npm start which is really running 70 | twilio-run --env under the hood. If you want to see what 71 | else you can do with twilio-run enter 72 | npx twilio-run --help on the command line or check out 73 | the project documentation on GitHub. 76 |

77 |
78 | 79 |
80 |

81 | Made with 💖 by your friends at 82 | Twilio 83 |

84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /templates/typescript/src/assets/message.private.ts: -------------------------------------------------------------------------------- 1 | export const privateMessage = () => { 2 | return 'This is private!'; 3 | }; -------------------------------------------------------------------------------- /templates/typescript/src/assets/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | ::selection { 8 | background: #f22f46; 9 | color: white; 10 | } 11 | 12 | body { 13 | font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 14 | 'Helvetica Neue', Arial, sans-serif; 15 | color: #0d122b; 16 | border-top: 5px solid #f22f46; 17 | } 18 | 19 | header { 20 | padding: 2em; 21 | margin-bottom: 2em; 22 | max-width: 800px; 23 | margin: 0 auto; 24 | } 25 | 26 | header h1 { 27 | padding-bottom: 14px; 28 | border-bottom: 1px solid rgba(148, 151, 155, 0.2); 29 | } 30 | 31 | a { 32 | color: #008cff; 33 | } 34 | 35 | main { 36 | margin: 0 auto 6em; 37 | padding: 0 2em; 38 | max-width: 800px; 39 | } 40 | 41 | main p { 42 | margin-bottom: 2em; 43 | } 44 | 45 | main p code { 46 | font-size: 16px; 47 | font-family: 'Fira Mono', monospace; 48 | color: #f22f46; 49 | background-color: #f9f9f9; 50 | box-shadow: inset 0 0 0 1px #e8e8e8; 51 | font-size: inherit; 52 | line-height: 1.2; 53 | padding: 0.15em 0.4em; 54 | border-radius: 4px; 55 | display: inline-block; 56 | white-space: pre-wrap; 57 | } 58 | 59 | main h2 { 60 | margin-bottom: 1em; 61 | } 62 | 63 | footer { 64 | margin: 0 auto; 65 | max-width: 800px; 66 | text-align: center; 67 | } 68 | 69 | footer p { 70 | border-top: 1px solid rgba(148, 151, 155, 0.2); 71 | padding-top: 2em; 72 | margin: 0 2em; 73 | } 74 | -------------------------------------------------------------------------------- /templates/typescript/src/functions/hello-world.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import '@twilio-labs/serverless-runtime-types'; 3 | // Fetches specific types 4 | import { 5 | Context, 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from '@twilio-labs/serverless-runtime-types/types'; 9 | 10 | type MyEvent = { 11 | Body?: string 12 | } 13 | 14 | // If you want to use environment variables, you will need to type them like 15 | // this and add them to the Context in the function signature as 16 | // Context as you see below. 17 | type MyContext = { 18 | GREETING?: string 19 | } 20 | 21 | export const handler: ServerlessFunctionSignature = function( 22 | context: Context, 23 | event: MyEvent, 24 | callback: ServerlessCallback 25 | ) { 26 | const twiml = new Twilio.twiml.VoiceResponse(); 27 | twiml.say(`${context.GREETING ? context.GREETING : 'Hello'} ${event.Body ? event.Body : 'World'}!`); 28 | callback(null, twiml); 29 | }; -------------------------------------------------------------------------------- /templates/typescript/src/functions/private-message.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import '@twilio-labs/serverless-runtime-types'; 3 | // Fetches specific types 4 | import { 5 | Context, 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from '@twilio-labs/serverless-runtime-types/types'; 9 | 10 | export const handler: ServerlessFunctionSignature = function( 11 | context: Context, 12 | event: {}, 13 | callback: ServerlessCallback 14 | ) { 15 | const assets = Runtime.getAssets(); 16 | // After compiling the assets, the result will be "message.js" not a TypeScript file. 17 | const privateMessageAsset = assets['/message.js']; 18 | const privateMessagePath = privateMessageAsset.path; 19 | const message = require(privateMessagePath); 20 | const twiml = new Twilio.twiml.MessagingResponse(); 21 | twiml.message(message.privateMessage()); 22 | callback(null, twiml); 23 | }; -------------------------------------------------------------------------------- /templates/typescript/src/functions/sms/reply.protected.ts: -------------------------------------------------------------------------------- 1 | // Imports global types 2 | import '@twilio-labs/serverless-runtime-types'; 3 | // Fetches specific types 4 | import { 5 | Context, 6 | ServerlessCallback, 7 | ServerlessFunctionSignature, 8 | } from '@twilio-labs/serverless-runtime-types/types'; 9 | 10 | export const handler: ServerlessFunctionSignature = function( 11 | context: Context, 12 | event: {}, 13 | callback: ServerlessCallback 14 | ) { 15 | const twiml = new Twilio.twiml.MessagingResponse(); 16 | twiml.message('Hello World!'); 17 | callback(null, twiml); 18 | }; -------------------------------------------------------------------------------- /tests/create-files.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { promisify } = require('util'); 4 | 5 | const rimraf = promisify(require('rimraf')); 6 | 7 | const mkdir = promisify(fs.mkdir); 8 | const readFile = promisify(fs.readFile); 9 | const stat = promisify(fs.stat); 10 | const readdir = promisify(fs.readdir); 11 | 12 | const versions = require('../src/create-twilio-function/versions'); 13 | const { 14 | createPackageJSON, 15 | createDirectory, 16 | createExampleFromTemplates, 17 | createEnvFile, 18 | createNvmrcFile, 19 | createTsconfigFile, 20 | createEmptyFileStructure, 21 | } = require('../src/create-twilio-function/create-files'); 22 | 23 | const scratchDir = path.join(process.cwd(), 'scratch'); 24 | 25 | beforeAll(async () => { 26 | await rimraf(scratchDir); 27 | }); 28 | 29 | beforeEach(async () => { 30 | await mkdir(scratchDir); 31 | }); 32 | 33 | afterEach(async () => { 34 | await rimraf(scratchDir); 35 | }); 36 | 37 | describe('createDirectory', () => { 38 | test('it creates a new directory with the project name', async () => { 39 | await createDirectory(scratchDir, 'test-project'); 40 | const dir = await stat(path.join(scratchDir, 'test-project')); 41 | expect(dir.isDirectory()); 42 | }); 43 | 44 | test('it throws an error if the directory exists', async () => { 45 | await mkdir(path.join(scratchDir, 'test-project')); 46 | expect.assertions(1); 47 | try { 48 | await createDirectory(scratchDir, 'test-project'); 49 | } catch (e) { 50 | expect(e.toString()).toMatch('EEXIST'); 51 | } 52 | }); 53 | }); 54 | 55 | describe('createPackageJSON', () => { 56 | test('it creates a new package.json file with the name of the project', async () => { 57 | await createPackageJSON(scratchDir, 'project-name'); 58 | const file = await stat(path.join(scratchDir, 'package.json')); 59 | expect(file.isFile()); 60 | const packageJSON = JSON.parse(await readFile(path.join(scratchDir, 'package.json'), 'utf-8')); 61 | expect(packageJSON.name).toEqual('project-name'); 62 | expect(packageJSON.engines.node).toEqual(versions.node); 63 | expect(packageJSON.devDependencies['twilio-run']).toEqual(versions.twilioRun); 64 | }); 65 | 66 | test('it creates a package.json file with typescript dependencies', async () => { 67 | await createPackageJSON(scratchDir, 'project-name', 'typescript'); 68 | const file = await stat(path.join(scratchDir, 'package.json')); 69 | expect(file.isFile()); 70 | const packageJSON = JSON.parse(await readFile(path.join(scratchDir, 'package.json'), 'utf-8')); 71 | expect(packageJSON.name).toEqual('project-name'); 72 | expect(packageJSON.engines.node).toEqual(versions.node); 73 | expect(packageJSON.devDependencies['twilio-run']).toEqual(versions.twilioRun); 74 | expect(packageJSON.devDependencies.typescript).toEqual(versions.typescript); 75 | expect(packageJSON.dependencies['@twilio-labs/serverless-runtime-types']).toEqual(versions.serverlessRuntimeTypes); 76 | }); 77 | 78 | test('it rejects if there is already a package.json', async () => { 79 | fs.closeSync(fs.openSync(path.join(scratchDir, 'package.json'), 'w')); 80 | expect.assertions(1); 81 | try { 82 | await createPackageJSON(scratchDir, 'project-name'); 83 | } catch (e) { 84 | expect(e.toString()).toMatch('file already exists'); 85 | } 86 | }); 87 | }); 88 | 89 | describe('createExampleFromTemplates', () => { 90 | describe('javascript', () => { 91 | const templatesDir = path.join(process.cwd(), 'templates', 'javascript'); 92 | test('it creates functions and assets directories', async () => { 93 | await createExampleFromTemplates(scratchDir); 94 | 95 | const dirs = await readdir(scratchDir); 96 | const templateDirContents = await readdir(templatesDir); 97 | expect(dirs).toEqual(templateDirContents); 98 | }); 99 | 100 | test('it copies the functions from the templates/javascript/functions directory', async () => { 101 | await createExampleFromTemplates(scratchDir); 102 | 103 | const functions = await readdir(path.join(scratchDir, 'functions')); 104 | const templateFunctions = await readdir(path.join(templatesDir, 'functions')); 105 | expect(functions).toEqual(templateFunctions); 106 | }); 107 | 108 | test('it rejects if there is already a functions directory', async () => { 109 | await mkdir(path.join(scratchDir, 'functions')); 110 | expect.assertions(1); 111 | try { 112 | await createExampleFromTemplates(scratchDir); 113 | } catch (e) { 114 | expect(e.toString()).toMatch('file already exists'); 115 | } 116 | }); 117 | }); 118 | describe('typescript', () => { 119 | const templatesDir = path.join(process.cwd(), 'templates', 'typescript'); 120 | test('it creates functions and assets directories', async () => { 121 | await createExampleFromTemplates(scratchDir, 'typescript'); 122 | 123 | const dirs = await readdir(scratchDir); 124 | const templateDirContents = await readdir(templatesDir); 125 | expect(dirs).toEqual(templateDirContents); 126 | }); 127 | 128 | test('it copies the typescript files from the templates/typescript/src directory', async () => { 129 | await createExampleFromTemplates(scratchDir, 'typescript'); 130 | 131 | const src = await readdir(path.join(scratchDir, 'src')); 132 | const templateSrc = await readdir(path.join(templatesDir, 'src')); 133 | expect(src).toEqual(templateSrc); 134 | }); 135 | 136 | test('it rejects if there is already a src directory', async () => { 137 | await mkdir(path.join(scratchDir, 'src')); 138 | expect.assertions(1); 139 | try { 140 | await createExampleFromTemplates(scratchDir, 'typescript'); 141 | } catch (e) { 142 | expect(e.toString()).toMatch('file already exists'); 143 | } 144 | }); 145 | }); 146 | }); 147 | 148 | describe('createEnvFile', () => { 149 | test('it creates a new .env file', async () => { 150 | await createEnvFile(scratchDir, { 151 | accountSid: 'AC123', 152 | authToken: 'qwerty123456', 153 | }); 154 | const file = await stat(path.join(scratchDir, '.env')); 155 | expect(file.isFile()); 156 | const contents = await readFile(path.join(scratchDir, '.env'), { encoding: 'utf-8' }); 157 | expect(contents).toMatch('ACCOUNT_SID=AC123'); 158 | expect(contents).toMatch('AUTH_TOKEN=qwerty123456'); 159 | }); 160 | 161 | test('it rejects if there is already an .env file', async () => { 162 | fs.closeSync(fs.openSync(path.join(scratchDir, '.env'), 'w')); 163 | expect.assertions(1); 164 | try { 165 | await createEnvFile(scratchDir, { 166 | accountSid: 'AC123', 167 | authToken: 'qwerty123456', 168 | }); 169 | } catch (e) { 170 | expect(e.toString()).toMatch('file already exists'); 171 | } 172 | }); 173 | }); 174 | 175 | describe('createNvmrcFile', () => { 176 | test('it creates a new .nvmrc file', async () => { 177 | await createNvmrcFile(scratchDir); 178 | const file = await stat(path.join(scratchDir, '.nvmrc')); 179 | expect(file.isFile()); 180 | const contents = await readFile(path.join(scratchDir, '.nvmrc'), { encoding: 'utf-8' }); 181 | expect(contents).toMatch(versions.node); 182 | }); 183 | 184 | test('it rejects if there is already an .nvmrc file', async () => { 185 | fs.closeSync(fs.openSync(path.join(scratchDir, '.nvmrc'), 'w')); 186 | expect.assertions(1); 187 | try { 188 | await createNvmrcFile(scratchDir); 189 | } catch (e) { 190 | expect(e.toString()).toMatch('file already exists'); 191 | } 192 | }); 193 | }); 194 | 195 | describe('createTsconfig', () => { 196 | test('it creates a new tsconfig.json file', async () => { 197 | await createTsconfigFile(scratchDir); 198 | const file = await stat(path.join(scratchDir, 'tsconfig.json')); 199 | expect(file.isFile()); 200 | const contents = await readFile(path.join(scratchDir, 'tsconfig.json'), { encoding: 'utf-8' }); 201 | expect(contents).toMatch('"compilerOptions"'); 202 | }); 203 | 204 | test('it rejects if there is already an tsconfig.json file', async () => { 205 | fs.closeSync(fs.openSync(path.join(scratchDir, 'tsconfig.json'), 'w')); 206 | expect.assertions(1); 207 | try { 208 | await createTsconfigFile(scratchDir); 209 | } catch (e) { 210 | expect(e.toString()).toMatch('file already exists'); 211 | } 212 | }); 213 | }); 214 | 215 | describe('createEmptyFileStructure', () => { 216 | test('creates functions and assets directory for javascript', async () => { 217 | await createEmptyFileStructure(scratchDir, 'javascript'); 218 | const functions = await stat(path.join(scratchDir, 'functions')); 219 | expect(functions.isDirectory()); 220 | const assets = await stat(path.join(scratchDir, 'assets')); 221 | expect(assets.isDirectory()); 222 | }); 223 | 224 | test('creates src, functions and assets directory for typescript', async () => { 225 | await createEmptyFileStructure(scratchDir, 'typescript'); 226 | const src = await stat(path.join(scratchDir, 'src')); 227 | expect(src.isDirectory()); 228 | const functions = await stat(path.join(scratchDir, 'src', 'functions')); 229 | expect(functions.isDirectory()); 230 | const assets = await stat(path.join(scratchDir, 'src', 'assets')); 231 | expect(assets.isDirectory()); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /tests/create-gitignore.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { promisify } = require('util'); 4 | 5 | const rimraf = promisify(require('rimraf')); 6 | const nock = require('nock'); 7 | 8 | const mkdir = promisify(fs.mkdir); 9 | const readFile = promisify(fs.readFile); 10 | const stat = promisify(fs.stat); 11 | 12 | const createGitignore = require('../src/create-twilio-function/create-gitignore'); 13 | 14 | const scratchDir = path.join(process.cwd(), 'scratch'); 15 | 16 | beforeAll(async () => { 17 | await rimraf(scratchDir); 18 | nock.disableNetConnect(); 19 | }); 20 | 21 | afterAll(() => { 22 | nock.enableNetConnect(); 23 | }); 24 | 25 | beforeEach(async () => { 26 | await mkdir(scratchDir); 27 | nock('https://raw.githubusercontent.com').get('/github/gitignore/master/Node.gitignore').reply(200, '*.log\n.env'); 28 | }); 29 | 30 | afterEach(async () => { 31 | await rimraf(scratchDir); 32 | nock.cleanAll(); 33 | }); 34 | 35 | describe('createGitignore', () => { 36 | test('it creates a new .gitignore file', async () => { 37 | await createGitignore(scratchDir); 38 | const file = await stat(path.join(scratchDir, '.gitignore')); 39 | expect(file.isFile()); 40 | const contents = await readFile(path.join(scratchDir, '.gitignore'), { encoding: 'utf-8' }); 41 | expect(contents).toMatch('*.log'); 42 | expect(contents).toMatch('.env'); 43 | }); 44 | 45 | test('it rejects if there is already a .gitignore file', async () => { 46 | fs.closeSync(fs.openSync(path.join(scratchDir, '.gitignore'), 'w')); 47 | expect.assertions(1); 48 | try { 49 | await createGitignore(scratchDir); 50 | } catch (e) { 51 | expect(e.toString()).toMatch('file already exists'); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/create-twilio-function.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { promisify } = require('util'); 4 | 5 | const inquirer = require('inquirer'); 6 | const ora = require('ora'); 7 | const nock = require('nock'); 8 | const rimraf = promisify(require('rimraf')); 9 | 10 | const mkdir = promisify(fs.mkdir); 11 | const stat = promisify(fs.stat); 12 | const readdir = promisify(fs.readdir); 13 | 14 | const { installDependencies } = require('../src/create-twilio-function/install-dependencies'); 15 | const createTwilioFunction = require('../src/create-twilio-function'); 16 | 17 | jest.mock('window-size', () => ({ get: () => ({ width: 80 }) })); 18 | jest.mock('inquirer'); 19 | jest.mock('ora'); 20 | jest.mock('boxen', () => { 21 | return () => 'success message'; 22 | }); 23 | const spinner = { 24 | start: () => spinner, 25 | succeed: () => spinner, 26 | fail: () => spinner, 27 | clear: () => spinner, 28 | stop: () => spinner, 29 | }; 30 | ora.mockImplementation(() => { 31 | return spinner; 32 | }); 33 | jest.mock('../src/create-twilio-function/install-dependencies.js', () => { 34 | return { installDependencies: jest.fn() }; 35 | }); 36 | console.log = jest.fn(); 37 | 38 | const scratchDir = path.join(process.cwd(), 'scratch'); 39 | 40 | beforeAll(async () => { 41 | await rimraf(scratchDir); 42 | nock.disableNetConnect(); 43 | }); 44 | 45 | afterAll(() => { 46 | nock.enableNetConnect(); 47 | }); 48 | 49 | describe('createTwilioFunction', () => { 50 | beforeEach(async () => { 51 | await mkdir(scratchDir); 52 | }); 53 | 54 | afterEach(async () => { 55 | await rimraf(scratchDir); 56 | nock.cleanAll(); 57 | }); 58 | 59 | describe('with an acceptable project name', () => { 60 | beforeEach(() => { 61 | inquirer.prompt = jest.fn(() => 62 | Promise.resolve({ 63 | accountSid: 'test-sid', 64 | authToken: 'test-auth-token', 65 | }), 66 | ); 67 | 68 | nock('https://raw.githubusercontent.com') 69 | .get('/github/gitignore/master/Node.gitignore') 70 | .reply(200, '*.log\n.env'); 71 | }); 72 | 73 | describe('javascript', () => { 74 | it('scaffolds a Twilio Function', async () => { 75 | const name = 'test-function'; 76 | await createTwilioFunction({ 77 | name, 78 | path: scratchDir, 79 | }); 80 | 81 | const dir = await stat(path.join(scratchDir, name)); 82 | expect(dir.isDirectory()); 83 | const env = await stat(path.join(scratchDir, name, '.env')); 84 | expect(env.isFile()); 85 | const nvmrc = await stat(path.join(scratchDir, name, '.nvmrc')); 86 | expect(nvmrc.isFile()); 87 | 88 | const packageJSON = await stat(path.join(scratchDir, name, 'package.json')); 89 | expect(packageJSON.isFile()); 90 | 91 | const gitignore = await stat(path.join(scratchDir, name, '.gitignore')); 92 | expect(gitignore.isFile()); 93 | 94 | const functions = await stat(path.join(scratchDir, name, 'functions')); 95 | expect(functions.isDirectory()); 96 | 97 | const assets = await stat(path.join(scratchDir, name, 'assets')); 98 | expect(assets.isDirectory()); 99 | 100 | const example = await stat(path.join(scratchDir, name, 'functions', 'hello-world.js')); 101 | expect(example.isFile()); 102 | 103 | const asset = await stat(path.join(scratchDir, name, 'assets', 'index.html')); 104 | expect(asset.isFile()); 105 | 106 | expect(installDependencies).toHaveBeenCalledWith(path.join(scratchDir, name)); 107 | 108 | expect(console.log).toHaveBeenCalledWith('success message'); 109 | }); 110 | 111 | it('scaffolds an empty Twilio Function', async () => { 112 | const name = 'test-function'; 113 | await createTwilioFunction({ 114 | name, 115 | path: scratchDir, 116 | empty: true, 117 | }); 118 | 119 | const dir = await stat(path.join(scratchDir, name)); 120 | expect(dir.isDirectory()); 121 | const env = await stat(path.join(scratchDir, name, '.env')); 122 | expect(env.isFile()); 123 | const nvmrc = await stat(path.join(scratchDir, name, '.nvmrc')); 124 | expect(nvmrc.isFile()); 125 | 126 | const packageJSON = await stat(path.join(scratchDir, name, 'package.json')); 127 | expect(packageJSON.isFile()); 128 | 129 | const gitignore = await stat(path.join(scratchDir, name, '.gitignore')); 130 | expect(gitignore.isFile()); 131 | 132 | const functions = await stat(path.join(scratchDir, name, 'functions')); 133 | expect(functions.isDirectory()); 134 | 135 | const assets = await stat(path.join(scratchDir, name, 'assets')); 136 | expect(assets.isDirectory()); 137 | 138 | const functionsDir = await readdir(path.join(scratchDir, name, 'functions')); 139 | expect(functionsDir.length).toEqual(0); 140 | 141 | const assetsDir = await readdir(path.join(scratchDir, name, 'assets')); 142 | expect(assetsDir.length).toEqual(0); 143 | 144 | expect(installDependencies).toHaveBeenCalledWith(path.join(scratchDir, name)); 145 | 146 | expect(console.log).toHaveBeenCalledWith('success message'); 147 | }); 148 | 149 | describe('templates', () => { 150 | it('scaffolds a Twilio Function with a template', async () => { 151 | /* eslint-disable camelcase */ 152 | const gitHubAPI = nock('https://api.github.com'); 153 | gitHubAPI.get('/repos/twilio-labs/function-templates/contents/blank?ref=master').reply(200, [ 154 | { name: 'functions' }, 155 | { 156 | name: '.env', 157 | download_url: 'https://raw.githubusercontent.com/twilio-labs/function-templates/master/blank/.env', 158 | }, 159 | ]); 160 | gitHubAPI.get('/repos/twilio-labs/function-templates/contents/blank/functions?ref=master').reply(200, [ 161 | { 162 | name: 'blank.js', 163 | download_url: 164 | 'https://raw.githubusercontent.com/twilio-labs/function-templates/master/blank/functions/blank.js', 165 | }, 166 | ]); 167 | /* eslint-enable camelcase */ 168 | const gitHubRaw = nock('https://raw.githubusercontent.com'); 169 | gitHubRaw.get('/twilio-labs/function-templates/master/blank/functions/blank.js').reply( 170 | 200, 171 | `exports.handler = function(context, event, callback) { 172 | callback(null, {}); 173 | };`, 174 | ); 175 | gitHubRaw.get('/github/gitignore/master/Node.gitignore').reply(200, 'node_modules/'); 176 | gitHubRaw.get('/twilio-labs/function-templates/master/blank/.env').reply(200, ''); 177 | 178 | const name = 'test-function'; 179 | await createTwilioFunction({ 180 | name, 181 | path: scratchDir, 182 | template: 'blank', 183 | }); 184 | const dir = await stat(path.join(scratchDir, name)); 185 | expect(dir.isDirectory()); 186 | const env = await stat(path.join(scratchDir, name, '.env')); 187 | expect(env.isFile()); 188 | const nvmrc = await stat(path.join(scratchDir, name, '.nvmrc')); 189 | expect(nvmrc.isFile()); 190 | 191 | const packageJSON = await stat(path.join(scratchDir, name, 'package.json')); 192 | expect(packageJSON.isFile()); 193 | 194 | const gitignore = await stat(path.join(scratchDir, name, '.gitignore')); 195 | expect(gitignore.isFile()); 196 | 197 | const functions = await stat(path.join(scratchDir, name, 'functions')); 198 | expect(functions.isDirectory()); 199 | 200 | const assets = await stat(path.join(scratchDir, name, 'assets')); 201 | expect(assets.isDirectory()); 202 | 203 | const exampleFiles = await readdir(path.join(scratchDir, name, 'functions')); 204 | expect(exampleFiles).toEqual(expect.not.arrayContaining(['hello-world.js'])); 205 | 206 | const templateFunction = await stat(path.join(scratchDir, name, 'functions', 'blank.js')); 207 | expect(templateFunction.isFile()); 208 | 209 | const exampleAssets = await readdir(path.join(scratchDir, name, 'assets')); 210 | expect(exampleAssets).toEqual(expect.not.arrayContaining(['index.html'])); 211 | 212 | expect(installDependencies).toHaveBeenCalledWith(path.join(scratchDir, name)); 213 | 214 | expect(console.log).toHaveBeenCalledWith('success message'); 215 | }); 216 | 217 | it('handles a missing template gracefully', async () => { 218 | const templateName = 'missing'; 219 | const name = 'test-function'; 220 | const gitHubAPI = nock('https://api.github.com'); 221 | gitHubAPI.get(`/repos/twilio-labs/function-templates/contents/${templateName}`).reply(404); 222 | 223 | const fail = jest.spyOn(spinner, 'fail'); 224 | 225 | await createTwilioFunction({ 226 | name, 227 | path: scratchDir, 228 | template: templateName, 229 | }); 230 | 231 | expect.assertions(3); 232 | 233 | expect(fail).toHaveBeenCalledTimes(1); 234 | expect(fail).toHaveBeenCalledWith(`The template "${templateName}" doesn't exist`); 235 | try { 236 | await stat(path.join(scratchDir, name)); 237 | } catch (e) { 238 | expect(e.toString()).toMatch('no such file or directory'); 239 | } 240 | }); 241 | }); 242 | }); 243 | 244 | describe('typescript', () => { 245 | it('scaffolds a Twilio Function', async () => { 246 | const name = 'test-function'; 247 | await createTwilioFunction({ 248 | name, 249 | path: scratchDir, 250 | typescript: true, 251 | }); 252 | 253 | const dir = await stat(path.join(scratchDir, name)); 254 | expect(dir.isDirectory()); 255 | const env = await stat(path.join(scratchDir, name, '.env')); 256 | expect(env.isFile()); 257 | const nvmrc = await stat(path.join(scratchDir, name, '.nvmrc')); 258 | expect(nvmrc.isFile()); 259 | const tsconfig = await stat(path.join(scratchDir, name, 'tsconfig.json')); 260 | expect(tsconfig.isFile()); 261 | 262 | const packageJSON = await stat(path.join(scratchDir, name, 'package.json')); 263 | expect(packageJSON.isFile()); 264 | 265 | const gitignore = await stat(path.join(scratchDir, name, '.gitignore')); 266 | expect(gitignore.isFile()); 267 | 268 | const src = await stat(path.join(scratchDir, name, 'src')); 269 | expect(src.isDirectory()); 270 | 271 | const functions = await stat(path.join(scratchDir, name, 'src', 'functions')); 272 | expect(functions.isDirectory()); 273 | 274 | const assets = await stat(path.join(scratchDir, name, 'src', 'assets')); 275 | expect(assets.isDirectory()); 276 | 277 | const example = await stat(path.join(scratchDir, name, 'src', 'functions', 'hello-world.ts')); 278 | expect(example.isFile()); 279 | 280 | const asset = await stat(path.join(scratchDir, name, 'src', 'assets', 'index.html')); 281 | expect(asset.isFile()); 282 | 283 | expect(installDependencies).toHaveBeenCalledWith(path.join(scratchDir, name)); 284 | 285 | expect(console.log).toHaveBeenCalledWith('success message'); 286 | }); 287 | 288 | it('scaffolds an empty Twilio Function', async () => { 289 | const name = 'test-function'; 290 | await createTwilioFunction({ 291 | name, 292 | path: scratchDir, 293 | empty: true, 294 | typescript: true, 295 | }); 296 | 297 | const dir = await stat(path.join(scratchDir, name)); 298 | expect(dir.isDirectory()); 299 | const env = await stat(path.join(scratchDir, name, '.env')); 300 | expect(env.isFile()); 301 | const nvmrc = await stat(path.join(scratchDir, name, '.nvmrc')); 302 | expect(nvmrc.isFile()); 303 | const tsconfig = await stat(path.join(scratchDir, name, 'tsconfig.json')); 304 | expect(tsconfig.isFile()); 305 | 306 | const packageJSON = await stat(path.join(scratchDir, name, 'package.json')); 307 | expect(packageJSON.isFile()); 308 | 309 | const gitignore = await stat(path.join(scratchDir, name, '.gitignore')); 310 | expect(gitignore.isFile()); 311 | 312 | const src = await stat(path.join(scratchDir, name, 'src')); 313 | expect(src.isDirectory()); 314 | 315 | const functions = await stat(path.join(scratchDir, name, 'src', 'functions')); 316 | expect(functions.isDirectory()); 317 | 318 | const assets = await stat(path.join(scratchDir, name, 'src', 'assets')); 319 | expect(assets.isDirectory()); 320 | 321 | const functionsDir = await readdir(path.join(scratchDir, name, 'src', 'functions')); 322 | expect(functionsDir.length).toEqual(0); 323 | 324 | const assetsDir = await readdir(path.join(scratchDir, name, 'src', 'assets')); 325 | expect(assetsDir.length).toEqual(0); 326 | 327 | expect(installDependencies).toHaveBeenCalledWith(path.join(scratchDir, name)); 328 | 329 | expect(console.log).toHaveBeenCalledWith('success message'); 330 | }); 331 | 332 | describe('templates', () => { 333 | it("doesn't scaffold", async () => { 334 | const fail = jest.spyOn(spinner, 'fail'); 335 | const name = 'test-function'; 336 | await createTwilioFunction({ 337 | name, 338 | path: scratchDir, 339 | typescript: true, 340 | template: 'blank', 341 | }); 342 | 343 | expect.assertions(4); 344 | 345 | expect(fail).toHaveBeenCalledTimes(1); 346 | expect(fail).toHaveBeenCalledWith( 347 | 'There are no TypeScript templates available. You can generate an example project or an empty one with the --empty flag.', 348 | ); 349 | expect(console.log).not.toHaveBeenCalled(); 350 | 351 | try { 352 | await stat(path.join(scratchDir, name, 'package.json')); 353 | } catch (e) { 354 | expect(e.toString()).toMatch('no such file or directory'); 355 | } 356 | }); 357 | }); 358 | }); 359 | 360 | it("doesn't scaffold if the target folder name already exists", async () => { 361 | const name = 'test-function'; 362 | await mkdir(path.join(scratchDir, name)); 363 | const fail = jest.spyOn(spinner, 'fail'); 364 | 365 | await createTwilioFunction({ 366 | name, 367 | path: scratchDir, 368 | }); 369 | 370 | expect.assertions(4); 371 | 372 | expect(fail).toHaveBeenCalledTimes(1); 373 | expect(fail).toHaveBeenCalledWith( 374 | `A directory called '${name}' already exists. Please create your function in a new directory.`, 375 | ); 376 | expect(console.log).not.toHaveBeenCalled(); 377 | 378 | try { 379 | await stat(path.join(scratchDir, name, 'package.json')); 380 | } catch (e) { 381 | expect(e.toString()).toMatch('no such file or directory'); 382 | } 383 | }); 384 | 385 | it("fails gracefully if it doesn't have permission to create directories", async () => { 386 | // chmod with 0o555 does not work on Windows. 387 | if (process.platform === 'win32') { 388 | return; 389 | } 390 | 391 | const name = 'test-function'; 392 | const chmod = promisify(fs.chmod); 393 | await chmod(scratchDir, 0o555); 394 | const fail = jest.spyOn(spinner, 'fail'); 395 | 396 | await createTwilioFunction({ 397 | name, 398 | path: scratchDir, 399 | }); 400 | 401 | expect.assertions(4); 402 | 403 | expect(fail).toHaveBeenCalledTimes(1); 404 | expect(fail).toHaveBeenCalledWith( 405 | `You do not have permission to create files or directories in the path '${scratchDir}'.`, 406 | ); 407 | expect(console.log).not.toHaveBeenCalled(); 408 | 409 | try { 410 | await stat(path.join(scratchDir, name, 'package.json')); 411 | } catch (e) { 412 | expect(e.toString()).toMatch('no such file or directory'); 413 | } 414 | }); 415 | 416 | it("doesn't scaffold if empty is true and a template is defined", async () => { 417 | const fail = jest.spyOn(spinner, 'fail'); 418 | const name = 'test-function'; 419 | await createTwilioFunction({ 420 | name, 421 | path: scratchDir, 422 | empty: true, 423 | template: 'blank', 424 | }); 425 | 426 | expect.assertions(4); 427 | 428 | expect(fail).toHaveBeenCalledTimes(1); 429 | expect(fail).toHaveBeenCalledWith( 430 | 'You cannot scaffold an empty Functions project with a template. Please choose empty or a template.', 431 | ); 432 | expect(console.log).not.toHaveBeenCalled(); 433 | 434 | try { 435 | await stat(path.join(scratchDir, name, 'package.json')); 436 | } catch (e) { 437 | expect(e.toString()).toMatch('no such file or directory'); 438 | } 439 | }); 440 | }); 441 | 442 | describe('with an unacceptable project name', () => { 443 | beforeEach(() => { 444 | inquirer.prompt = jest.fn(); 445 | inquirer.prompt.mockReturnValueOnce(Promise.resolve({ name: 'test-function' })).mockReturnValueOnce( 446 | Promise.resolve({ 447 | accountSid: 'test-sid', 448 | authToken: 'test-auth-token', 449 | }), 450 | ); 451 | 452 | nock('https://raw.githubusercontent.com') 453 | .get('/github/gitignore/master/Node.gitignore') 454 | .reply(200, '*.log\n.env'); 455 | }); 456 | 457 | it('scaffolds a Twilio Function and prompts for a new name', async () => { 458 | const badName = 'GreatTest!!!'; 459 | const name = 'test-function'; 460 | await createTwilioFunction({ 461 | name: badName, 462 | path: scratchDir, 463 | }); 464 | 465 | const dir = await stat(path.join(scratchDir, name)); 466 | expect(dir.isDirectory()); 467 | const env = await stat(path.join(scratchDir, name, '.env')); 468 | expect(env.isFile()); 469 | const nvmrc = await stat(path.join(scratchDir, name, '.nvmrc')); 470 | expect(nvmrc.isFile()); 471 | 472 | const packageJSON = await stat(path.join(scratchDir, name, 'package.json')); 473 | expect(packageJSON.isFile()); 474 | 475 | const gitignore = await stat(path.join(scratchDir, name, '.gitignore')); 476 | expect(gitignore.isFile()); 477 | 478 | const functions = await stat(path.join(scratchDir, name, 'functions')); 479 | expect(functions.isDirectory()); 480 | 481 | const assets = await stat(path.join(scratchDir, name, 'assets')); 482 | expect(assets.isDirectory()); 483 | 484 | const example = await stat(path.join(scratchDir, name, 'functions', 'hello-world.js')); 485 | expect(example.isFile()); 486 | 487 | const asset = await stat(path.join(scratchDir, name, 'assets', 'index.html')); 488 | expect(asset.isFile()); 489 | 490 | expect(installDependencies).toHaveBeenCalledWith(path.join(scratchDir, name)); 491 | 492 | expect(console.log).toHaveBeenCalledWith('success message'); 493 | }); 494 | }); 495 | }); 496 | -------------------------------------------------------------------------------- /tests/import-credentials.test.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const importCredentials = require('../src/create-twilio-function/import-credentials'); 4 | 5 | describe('importCredentials', () => { 6 | describe('if credentials are present in the env', () => { 7 | afterEach(() => { 8 | delete process.env.TWILIO_ACCOUNT_SID; 9 | delete process.env.TWILIO_AUTH_TOKEN; 10 | }); 11 | 12 | test('it should prompt to ask if to use credentials and return them if affirmative', async () => { 13 | process.env.TWILIO_ACCOUNT_SID = 'AC1234'; 14 | process.env.TWILIO_AUTH_TOKEN = 'auth-token'; 15 | 16 | inquirer.prompt = jest.fn(() => Promise.resolve({ importedCredentials: true })); 17 | 18 | const credentials = await importCredentials({}); 19 | expect(inquirer.prompt).toHaveBeenCalledTimes(1); 20 | expect(inquirer.prompt).toHaveBeenCalledWith(expect.any(Array)); 21 | expect(credentials.accountSid).toBe('AC1234'); 22 | expect(credentials.authToken).toBe('auth-token'); 23 | }); 24 | 25 | test('it should prompt to ask if to use credentials and return an empty object if negative', async () => { 26 | process.env.TWILIO_ACCOUNT_SID = 'AC1234'; 27 | process.env.TWILIO_AUTH_TOKEN = 'auth-token'; 28 | 29 | inquirer.prompt = jest.fn(() => Promise.resolve({ importedCredentials: false })); 30 | 31 | const credentials = await importCredentials({}); 32 | expect(inquirer.prompt).toHaveBeenCalledTimes(1); 33 | expect(inquirer.prompt).toHaveBeenCalledWith(expect.any(Array)); 34 | expect(credentials.accountSid).toBe(undefined); 35 | expect(credentials.authToken).toBe(undefined); 36 | }); 37 | 38 | test('it should return credentials if the option importCredentials is true', async () => { 39 | process.env.TWILIO_ACCOUNT_SID = 'AC1234'; 40 | process.env.TWILIO_AUTH_TOKEN = 'auth-token'; 41 | 42 | const credentials = await importCredentials({ importedCredentials: true }); 43 | expect(inquirer.prompt).not.toHaveBeenCalled(); 44 | expect(credentials.accountSid).toBe('AC1234'); 45 | expect(credentials.authToken).toBe('auth-token'); 46 | }); 47 | 48 | test('it should not return credentials if skipCredentials is true', async () => { 49 | process.env.TWILIO_ACCOUNT_SID = 'AC1234'; 50 | process.env.TWILIO_AUTH_TOKEN = 'auth-token'; 51 | 52 | const credentials = await importCredentials({ 53 | skipCredentials: true, 54 | importedCredentials: true, 55 | }); 56 | expect(inquirer.prompt).not.toHaveBeenCalled(); 57 | expect(credentials.accountSid).toBe(undefined); 58 | expect(credentials.authToken).toBe(undefined); 59 | }); 60 | }); 61 | 62 | describe('if there are no credentials in the env', () => { 63 | test('it should not ask about importing credentials', async () => { 64 | delete process.env.TWILIO_ACCOUNT_SID; 65 | delete process.env.TWILIO_AUTH_TOKEN; 66 | await importCredentials({}); 67 | expect(inquirer.prompt).not.toHaveBeenCalled(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/install-dependencies.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const pkgInstall = require('pkg-install'); 4 | 5 | const { installDependencies } = require('../src/create-twilio-function/install-dependencies'); 6 | 7 | const scratchDir = path.join(process.cwd(), 'scratch'); 8 | 9 | jest.mock('pkg-install'); 10 | 11 | describe('installDependencies', () => { 12 | test('it calls `npm install` in the target directory', async () => { 13 | pkgInstall.projectInstall.mockResolvedValue({ stdout: 'done' }); 14 | 15 | await installDependencies(scratchDir); 16 | 17 | expect(pkgInstall.projectInstall).toHaveBeenCalledWith({ cwd: scratchDir }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/prompt.test.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | const { 4 | validateAccountSid, 5 | promptForAccountDetails, 6 | promptForProjectName, 7 | } = require('../src/create-twilio-function/prompt'); 8 | 9 | console.log = jest.fn(); 10 | 11 | describe('accountSid validation', () => { 12 | test('an accountSid should start with "AC"', () => { 13 | expect(validateAccountSid('AC123')).toBe(true); 14 | }); 15 | 16 | test('an accountSid can be left blank', () => { 17 | expect(validateAccountSid('')).toBe(true); 18 | }); 19 | 20 | test('an accountSid should not begin with anything but "AC"', () => { 21 | expect(validateAccountSid('blah')).toEqual('An Account SID starts with "AC".'); 22 | }); 23 | }); 24 | 25 | describe('promptForAccountDetails', () => { 26 | test('should ask for an accountSid if not specified', async () => { 27 | inquirer.prompt = jest.fn(() => 28 | Promise.resolve({ 29 | accountSid: 'AC1234', 30 | authToken: 'test-auth-token', 31 | }), 32 | ); 33 | await promptForAccountDetails({ name: 'function-test' }); 34 | expect(inquirer.prompt).toHaveBeenCalledTimes(1); 35 | expect(inquirer.prompt).toHaveBeenCalledWith(expect.any(Array)); 36 | expect(console.log).toHaveBeenCalledTimes(1); 37 | expect(console.log).toHaveBeenCalledWith(expect.any(String)); 38 | }); 39 | 40 | test('should ask for an auth if not specified', async () => { 41 | inquirer.prompt = jest.fn(() => Promise.resolve({ authToken: 'test-auth-token' })); 42 | await promptForAccountDetails({ 43 | name: 'function-test', 44 | accountSid: 'AC1234', 45 | }); 46 | expect(inquirer.prompt).toHaveBeenCalledTimes(1); 47 | expect(inquirer.prompt).toHaveBeenCalledWith(expect.any(Array)); 48 | expect(console.log).toHaveBeenCalledTimes(1); 49 | expect(console.log).toHaveBeenCalledWith(expect.any(String)); 50 | }); 51 | 52 | test('should not prompt if account sid and auth token specified', async () => { 53 | inquirer.prompt = jest.fn(() => 54 | Promise.resolve({ 55 | accountSid: 'AC1234', 56 | authToken: 'test-auth-token', 57 | }), 58 | ); 59 | await promptForAccountDetails({ 60 | name: 'function-test', 61 | accountSid: 'AC5678', 62 | authToken: 'other-test-token', 63 | }); 64 | expect(inquirer.prompt).toHaveBeenCalledTimes(1); 65 | expect(inquirer.prompt).toHaveBeenCalledWith([]); 66 | expect(console.log).not.toHaveBeenCalled(); 67 | }); 68 | 69 | test('should not ask for credentials if skip-credentials flag is true', async () => { 70 | inquirer.prompt = jest.fn(() => { 71 | return 0; 72 | }); 73 | await promptForAccountDetails({ skipCredentials: true }); 74 | expect(inquirer.prompt).not.toHaveBeenCalled(); 75 | expect(console.log).not.toHaveBeenCalled(); 76 | }); 77 | }); 78 | 79 | describe('promptForProjectName', () => { 80 | test('should ask for a project name', async () => { 81 | inquirer.prompt = jest.fn(() => Promise.resolve({ name: 'test-name' })); 82 | await promptForProjectName(['must be valid']); 83 | expect(inquirer.prompt).toHaveBeenCalledTimes(1); 84 | expect(inquirer.prompt).toHaveBeenCalledWith(expect.any(Array)); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/success-message.test.js: -------------------------------------------------------------------------------- 1 | const pkgInstall = require('pkg-install'); 2 | const chalk = require('chalk'); 3 | 4 | const successMessage = require('../src/create-twilio-function/success-message'); 5 | 6 | jest.mock('pkg-install'); 7 | jest.mock('window-size', () => ({ get: () => ({ width: 80 }) })); 8 | 9 | describe('successMessage', () => { 10 | test('creates a success message based on the package manager', async () => { 11 | pkgInstall.getPackageManager.mockResolvedValue('yarn'); 12 | const config = { 13 | name: 'test-function', 14 | path: './test-path', 15 | }; 16 | const message = await successMessage(config); 17 | expect(message).toEqual(expect.stringContaining('yarn start')); 18 | expect(message).toEqual(expect.stringContaining(chalk`Created {bold ${config.name}} at {bold ${config.path}}`)); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/validate-project-name.test.js: -------------------------------------------------------------------------------- 1 | const validateProjectName = require('../src/create-twilio-function/validate-project-name'); 2 | 3 | describe('validateProjectName', () => { 4 | it('should allow names shorter than 33 characters', () => { 5 | const { valid } = validateProjectName('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); 6 | expect(valid).toBe(true); 7 | }); 8 | 9 | it('should disallow names longer than 32 characters', () => { 10 | const { valid, errors } = validateProjectName('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); 11 | expect(valid).toBe(false); 12 | expect(errors[0]).toEqual('must be shorter than 32 characters'); 13 | }); 14 | 15 | it('should allow names with letters, numbers and hyphens', () => { 16 | const { valid } = validateProjectName('Project-1'); 17 | expect(valid).toBe(true); 18 | }); 19 | 20 | it('should disallow names with special characters or underscores', () => { 21 | const names = ['project!', 'project@', '#hello', '__hey']; 22 | names.forEach((name) => { 23 | const { valid, errors } = validateProjectName(name); 24 | expect(valid).toBe(false); 25 | expect(errors[0]).toEqual('must only include letters, numbers and hyphens'); 26 | }); 27 | }); 28 | 29 | it('should disallow names beginning with a hyphen', () => { 30 | const { valid, errors } = validateProjectName('-otherwisecool'); 31 | expect(valid).toBe(false); 32 | expect(errors[0]).toBe('must not start with a hyphen'); 33 | }); 34 | 35 | it('should disallow names ending with a hyphen', () => { 36 | const { valid, errors } = validateProjectName('otherwisecool-'); 37 | expect(valid).toBe(false); 38 | expect(errors[0]).toBe('must not end with a hyphen'); 39 | }); 40 | 41 | it('should return multiple messages if there are multiple errors', () => { 42 | const { valid, errors } = validateProjectName('-not#Cool-'); 43 | expect(valid).toBe(false); 44 | expect(errors.length).toBe(3); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/window-size.test.js: -------------------------------------------------------------------------------- 1 | const getWindowSize = require('../src/create-twilio-function/window-size'); 2 | 3 | jest.mock('window-size', () => ({ 4 | get: jest 5 | .fn() 6 | .mockReturnValueOnce({ 7 | width: 40, 8 | height: 100, 9 | }) 10 | .mockReturnValueOnce() 11 | .mockReturnValueOnce({ height: 250 }) 12 | .mockReturnValueOnce({ width: 50 }) 13 | .mockReturnValueOnce({ 14 | width: 80, 15 | height: 300, 16 | }), 17 | })); 18 | 19 | describe('getWindowSize', () => { 20 | it('gets a valid windowSize', () => { 21 | const windowSize = getWindowSize(); 22 | expect(windowSize).toEqual({ 23 | width: 40, 24 | height: 100, 25 | }); 26 | }); 27 | it('cannot get a null windowSize', () => { 28 | const windowSize = getWindowSize(); 29 | expect(windowSize).toEqual({ 30 | width: 80, 31 | height: 300, 32 | }); 33 | }); 34 | it('gets a windowSize without a width', () => { 35 | const windowSize = getWindowSize(); 36 | expect(windowSize).toEqual({ 37 | width: 80, 38 | height: 250, 39 | }); 40 | }); 41 | it('gets a windowSize without a height', () => { 42 | const windowSize = getWindowSize(); 43 | expect(windowSize).toEqual({ 44 | width: 50, 45 | height: 300, 46 | }); 47 | }); 48 | it('gets a windowSize without a width nor a height', () => { 49 | const windowSize = getWindowSize(); 50 | expect(windowSize).toEqual({ 51 | width: 80, 52 | height: 300, 53 | }); 54 | }); 55 | }); 56 | --------------------------------------------------------------------------------