├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TEST_PLAN.md ├── bin ├── run └── run.cmd ├── package-lock.json ├── package.json ├── src ├── commands │ ├── dev │ │ ├── exec.js │ │ └── index.js │ └── functions │ │ ├── build.js │ │ ├── create.js │ │ ├── index.js │ │ ├── invoke.js │ │ └── list.js ├── detect-functions-builder.js ├── detect-server.js ├── detectors │ ├── README.md │ ├── brunch.js │ ├── cra.js │ ├── docusaurus.js │ ├── eleventy.js │ ├── gatsby.js │ ├── gridsome.js │ ├── hexo.js │ ├── hugo.js │ ├── jekyll.js │ ├── middleman.js │ ├── next.js │ ├── nuxt.js │ ├── phenomic.js │ ├── quasar-v0.17.js │ ├── quasar.js │ ├── react-static.js │ ├── sapper.js │ ├── stencil.js │ ├── svelte.js │ ├── utils │ │ └── jsdetect.js │ ├── vue.js │ └── vuepress.js ├── function-builder-detectors │ ├── README.md │ └── netlify-lambda.js ├── functions-templates │ ├── js │ │ ├── README.md │ │ ├── apollo-graphql-rest │ │ │ ├── .netlify-function-template.js │ │ │ ├── apollo-graphql-rest.js │ │ │ ├── package.json │ │ │ └── random-user.js │ │ ├── apollo-graphql │ │ │ ├── .netlify-function-template.js │ │ │ ├── apollo-graphql.js │ │ │ └── package.json │ │ ├── auth-fetch │ │ │ ├── .netlify-function-template.js │ │ │ ├── auth-fetch.js │ │ │ ├── package-lock.json │ │ │ └── package.json │ │ ├── create-user │ │ │ ├── .netlify-function-template.js │ │ │ ├── create-user.js │ │ │ └── package.json │ │ ├── fauna-crud │ │ │ ├── .netlify-function-template.js │ │ │ ├── create-schema.js │ │ │ ├── create.js │ │ │ ├── delete.js │ │ │ ├── fauna-crud.js │ │ │ ├── package.json │ │ │ ├── read-all.js │ │ │ ├── read.js │ │ │ └── update.js │ │ ├── fauna-graphql │ │ │ ├── .netlify-function-template.js │ │ │ ├── fauna-graphql.js │ │ │ ├── package.json │ │ │ ├── schema.graphql │ │ │ └── sync-schema.js │ │ ├── google-analytics │ │ │ ├── .netlify-function-template.js │ │ │ ├── google-analytics.js │ │ │ ├── package-lock.json │ │ │ └── package.json │ │ ├── graphql-gateway │ │ │ ├── .netlify-function-template.js │ │ │ ├── example-sibling-function-graphql-1.js │ │ │ ├── example-sibling-function-graphql-2.js │ │ │ ├── graphql-gateway.js │ │ │ └── package.json │ │ ├── hasura-event-triggered │ │ │ ├── .netlify-function-template.js │ │ │ ├── hasura-event-triggered.js │ │ │ └── package.json │ │ ├── hello-world │ │ │ ├── .netlify-function-template.js │ │ │ └── hello-world.js │ │ ├── identity-signup │ │ │ ├── .netlify-function-template.js │ │ │ └── identity-signup.js │ │ ├── node-fetch │ │ │ ├── .netlify-function-template.js │ │ │ ├── node-fetch.js │ │ │ └── package.json │ │ ├── oauth-passport │ │ │ ├── .netlify-function-template.js │ │ │ ├── oauth-passport.js │ │ │ ├── package.json │ │ │ └── utils │ │ │ │ ├── auth.js │ │ │ │ └── config.js │ │ ├── protected-function │ │ │ ├── .netlify-function-template.js │ │ │ └── protected-function.js │ │ ├── send-email │ │ │ ├── .netlify-function-template.js │ │ │ ├── package.json │ │ │ ├── send-email.js │ │ │ └── validations.js │ │ ├── serverless-ssr │ │ │ ├── .netlify-function-template.js │ │ │ ├── app │ │ │ │ └── index.js │ │ │ ├── package.json │ │ │ ├── serverless-http.js │ │ │ └── serverless-ssr.js │ │ ├── set-cookie │ │ │ ├── .netlify-function-template.js │ │ │ ├── package.json │ │ │ └── set-cookie.js │ │ ├── slack-rate-limit │ │ │ ├── .netlify-function-template.js │ │ │ ├── package.json │ │ │ └── slack-rate-limit.js │ │ ├── stripe-charge │ │ │ ├── .netlify-function-template.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── stripe-charge.js │ │ ├── stripe-subscription │ │ │ ├── .netlify-function-template.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── stripe-subscription.js │ │ ├── submission-created │ │ │ ├── .netlify-function-template.js │ │ │ ├── package.json │ │ │ └── submission-created.js │ │ ├── token-hider │ │ │ ├── .netlify-function-template.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── token-hider.js │ │ ├── url-shortener │ │ │ ├── .netlify-function-template.js │ │ │ ├── generate-route.js │ │ │ ├── get-route.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── url-shortener.js │ │ └── using-middleware │ │ │ ├── .netlify-function-template.js │ │ │ ├── package.json │ │ │ └── using-middleware.js │ ├── unused_go │ │ └── hello-world │ │ │ └── hello-world.go │ └── unused_ts │ │ ├── hello-world │ │ ├── hello-world.ts │ │ └── package.json │ │ └── node-fetch │ │ ├── node-fetch.ts │ │ └── package.json ├── live-tunnel.js └── utils │ ├── addons.js │ ├── dev.js │ ├── finders.js │ ├── get-functions.js │ ├── read-repo-url.js │ └── serve-functions.js └── test ├── commands └── functions.test.js └── mocha.opts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/functions-templates -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["oclif", "plugin:prettier/recommended"], 3 | "rules": { 4 | "no-warning-comments": "off", 5 | "prettier/prettier": "error", 6 | "no-process-exit": "off", 7 | "unicorn/no-process-exit": "off", 8 | "camelcase": "off", 9 | "guard-for-in": "off", 10 | "valid-jsdoc": "off", 11 | "no-inner-declarations": "off", 12 | "no-missing-require": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 18 | 19 | **- Do you want to request a *feature* or report a *bug*?** 20 | 21 | **- What is the current behavior?** 22 | 23 | **- If the current behavior is a bug, please provide the steps to reproduce.** 24 | 25 | **- What is the expected behavior?** 26 | 27 | **- Please mention your node.js, and operating system version.** 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | **- Summary** 15 | 16 | 20 | 21 | **- Test plan** 22 | 23 | 27 | 28 | **- Description for the changelog** 29 | 30 | 34 | 35 | **- A picture of a cute animal (not mandatory but encouraged)** 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /tmp 6 | node_modules 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /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, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | 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 david@netlify.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 [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Contributions are always welcome, no matter how large or small. Before contributing, 4 | please read the [code of conduct](CODE_OF_CONDUCT.md). 5 | 6 | ## Setup 7 | 8 | Install Node.js 8+ on your system: https://nodejs.org/en/download/ 9 | 10 | 1. Clone down the repo 11 | 12 | ```sh-session 13 | $ git clone https://github.com/netlify/netlify-dev-plugin.git 14 | ``` 15 | 16 | 2. Install dependencies 17 | 18 | ```sh-session 19 | $ npm install 20 | ``` 21 | 22 | 3. Run CLI locally during development 23 | 24 | ```sh-session 25 | $ ./bin/run [command] 26 | ``` 27 | 28 | When developing, you can use watch mode which will automatically run ava tests: 29 | 30 | ```sh-session 31 | $ npm run watch 32 | ``` 33 | 34 | ## Architecture 35 | 36 | Netlify Dev Plugin is written using the [oclif](https://oclif.io/) cli framework and the [netlify/js-client](https://github.com/netlify/js-client) open-api derived API client. 37 | 38 | - Commands live in the [`src/commands`](src/commands) folder. 39 | - The base command class which provides consistent config loading and an API client lives in [`src/base`](src/base). 40 | - Small utilities and other functionality live in [`src/utils`](src/utils). 41 | 42 | A good place to start is reading the base command README and looking at the commands folder. 43 | 44 | ### Testing 45 | 46 | This repo uses [Mocha](https://mochajs.org/) for testing. Any files in the 47 | `src` directory that have a `.test.js` file extension are automatically 48 | detected and run as tests. 49 | 50 | We also test for a few other things: 51 | 52 | - Dependencies (used an unused) 53 | - Linting 54 | - Test coverage 55 | - Must work with Windows + Unix environments. 56 | 57 | ## Pull Requests 58 | 59 | We actively welcome your pull requests. 60 | 61 | 1. Fork the repo and create your branch from `master`. 62 | 2. If you've added code that should be tested, add tests. 63 | 3. If you've changed APIs, update the documentation. 64 | 4. Ensure the test suite passes. 65 | 5. Make sure your code lints. 66 | 67 | ## Releasing 68 | 69 | 1. `npm version [major, minor, patch]` Generate changelog and bump version. 70 | 2. `npm publish` Publish to npm, push version commit + tag, push latest CHANGELOG entry to GitHub release page. 71 | 72 | ## License 73 | 74 | By contributing to Netlify Dev Plugin, you agree that your contributions will be licensed 75 | under its [MIT license](LICENSE). 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Netlify 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Netlify Dev has been moved 2 | Neylify dev has been moved into the [Netlify CLI repo](https://github.com/netlify/cli). Please look look at the [Netlify Dev intro](https://github.com/netlify/cli/blob/master/docs/netlify-dev.md) or [usage instructions for `netlify dev`](https://github.com/netlify/cli/blob/master/docs/commands/dev.md) for more information. 3 | -------------------------------------------------------------------------------- /TEST_PLAN.md: -------------------------------------------------------------------------------- 1 | # test fixtures 2 | 3 | ## test netlify dev --live 4 | 5 | 1. CRA 6 | 2. CRA + redirect for SPA 7 | 3. CRA + functions 8 | 4. CRA + redirected functions 9 | 5. Gatsby 10 | 11 | ## netlify dev 12 | 13 | 1. netlify dev 14 | 2. netlify dev --port 23 15 | 3. netlify dev --cmd vuepress 16 | 4. netlify dev --offline 17 | 5. netlify dev, with toml, good port 18 | 19 | [dev] 20 | port = 8000 # Port that the dev server will be listening on 21 | 22 | 6. netlify dev, with toml, bad port 23 | 24 | [dev] 25 | port = 9999 # Port that the dev server will be listening on 26 | 27 | 7. netlify dev, with toml, \_redirects inside publish 28 | 29 | [dev] 30 | publish = "public" 31 | 32 | 8. netlify dev, with toml, custom command 33 | 34 | [dev] 35 | command = "yarn start" 36 | 37 | 9. netlify dev, 38 | with custom command in flag, --cmd vuepress, 39 | but also custom command in toml (flag should win) 40 | 41 | [dev] 42 | command = "yarn start" 43 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .catch(require('@oclif/errors/handle')) 5 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-dev-plugin", 3 | "description": "Netlify development tools plugin", 4 | "version": "1.0.28", 5 | "author": "David Calavera @calavera", 6 | "bugs": "https://github.com/netlify/netlify-dev-plugin/issues", 7 | "dependencies": { 8 | "@netlify/cli-utils": "^1.0.7", 9 | "@netlify/rules-proxy": "^0.1.3", 10 | "@netlify/zip-it-and-ship-it": "^0.3.0", 11 | "@oclif/command": "^1", 12 | "@oclif/config": "^1", 13 | "ascii-table": "0.0.9", 14 | "body-parser": "^1.18.3", 15 | "boxen": "^3.0.0", 16 | "chalk": "^2.4.2", 17 | "chokidar": "^2.1.5", 18 | "copy-template-dir": "^1.4.0", 19 | "debug": "^4.1.1", 20 | "execa": "^1.0.0", 21 | "express": "^4.16.4", 22 | "express-logging": "^1.1.1", 23 | "fs-extra": "^7.0.1", 24 | "fuzzy": "^0.1.3", 25 | "get-port": "^4.2.0", 26 | "gh-release-fetch": "^1.0.3", 27 | "http-proxy": "^1.17.0", 28 | "inquirer": "^6.2.2", 29 | "inquirer-autocomplete-prompt": "^1.0.1", 30 | "jwt-decode": "^2.2.0", 31 | "netlify": "2.4.1", 32 | "netlify-cli-logo": "^1.0.0", 33 | "node-fetch": "^2.3.0", 34 | "npm-packlist": "^1.4.1", 35 | "ora": "^3.4.0", 36 | "precinct": "^6.1.2", 37 | "read-pkg-up": "^5.0.0", 38 | "require-package-name": "^2.0.1", 39 | "resolve": "^1.10.0", 40 | "safe-join": "^0.1.2", 41 | "static-server": "^2.2.1", 42 | "wait-port": "^0.2.2", 43 | "wrap-ansi": "^5.1.0" 44 | }, 45 | "devDependencies": { 46 | "@oclif/dev-cli": "^1", 47 | "@oclif/plugin-help": "^2", 48 | "@oclif/test": "^1", 49 | "auto-changelog": "^1.12.0", 50 | "chai": "^4", 51 | "dependency-check": "^3.3.0", 52 | "eslint": "^5.5", 53 | "eslint-config-oclif": "^3.1", 54 | "eslint-config-prettier": "^4.1.0", 55 | "eslint-plugin-prettier": "^3.0.1", 56 | "gh-release": "^3.5.0", 57 | "globby": "^8", 58 | "husky": "^2.2.0", 59 | "lint-staged": "^8.1.6", 60 | "mocha": "^5", 61 | "nock": "^10.0.6", 62 | "npm-run-all": "^4.1.5", 63 | "nyc": "^13", 64 | "prettier": "^1.16.4" 65 | }, 66 | "engines": { 67 | "node": ">=8.3.0" 68 | }, 69 | "files": [ 70 | "/npm-shrinkwrap.json", 71 | "/oclif.manifest.json", 72 | "/src", 73 | "/yarn.lock" 74 | ], 75 | "homepage": "https://github.com/netlify/netlify-dev-plugin", 76 | "keywords": [ 77 | "oclif-plugin" 78 | ], 79 | "license": "MIT", 80 | "oclif": { 81 | "commands": "./src/commands", 82 | "bin": "netlify", 83 | "devPlugins": [ 84 | "@oclif/plugin-help" 85 | ] 86 | }, 87 | "husky": { 88 | "hooks": { 89 | "pre-commit": "lint-staged" 90 | } 91 | }, 92 | "lint-staged": { 93 | "src/*.js": [ 94 | "npm run format", 95 | "git add" 96 | ] 97 | }, 98 | "repository": "netlify/netlify-dev-plugin", 99 | "scripts": { 100 | "format": "npm run format:prettier -- --write", 101 | "format:prettier": "prettier \"{{src,test}/**/,}*.js\"", 102 | "postpack": "rm -f oclif.manifest.json", 103 | "posttest": "eslint .", 104 | "prepack": "oclif-dev manifest && oclif-dev readme", 105 | "prepublishOnly": "git push && git push --tags && gh-release", 106 | "test": "run-s test:*", 107 | "test:deps": "dependency-check ./package.json --entry \"src/commands/**/!(*.test).js\" --unused --missing --no-dev --no-peer -i @oclif/plugin-not-found -i @oclif/config -i @oclif/plugin-help -i @oclif/plugin-plugins", 108 | "test-skip:mocha": "nyc mocha --forbid-only \"test/**/*.test.js\"", 109 | "version": "oclif-dev readme && auto-changelog -p --template keepachangelog && git add README.md CHANGELOG.md" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/commands/dev/exec.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | const Command = require("@netlify/cli-utils"); 3 | const { track } = require("@netlify/cli-utils/src/utils/telemetry"); 4 | const { 5 | // NETLIFYDEV, 6 | NETLIFYDEVLOG, 7 | // NETLIFYDEVWARN, 8 | NETLIFYDEVERR 9 | } = require("netlify-cli-logo"); 10 | 11 | class ExecCommand extends Command { 12 | async run() { 13 | const { site, api } = this.netlify; 14 | if (site.id) { 15 | this.log( 16 | `${NETLIFYDEVLOG} Checking your site's environment variables...` 17 | ); // just to show some visual response first 18 | const accessToken = api.accessToken; 19 | const { addEnvVariables } = require("../../utils/dev"); 20 | await addEnvVariables(api, site, accessToken); 21 | } else { 22 | this.log( 23 | `${NETLIFYDEVERR} No Site ID detected. You probably forgot to run \`netlify link\` or \`netlify init\`. ` 24 | ); 25 | } 26 | execa(this.argv[0], this.argv.slice(1), { 27 | env: process.env, 28 | stdio: "inherit" 29 | }); 30 | // Todo hoist this telemetry `command` to CLI hook 31 | track("command", { 32 | command: "dev:exec" 33 | }); 34 | } 35 | } 36 | 37 | ExecCommand.description = `Exec command 38 | Runs a command within the netlify dev environment, e.g. with env variables from any installed addons 39 | `; 40 | 41 | ExecCommand.examples = ["$ netlify exec npm run bootstrap"]; 42 | 43 | ExecCommand.strict = false; 44 | ExecCommand.parse = false; 45 | 46 | module.exports = ExecCommand; 47 | -------------------------------------------------------------------------------- /src/commands/dev/index.js: -------------------------------------------------------------------------------- 1 | const { flags } = require("@oclif/command"); 2 | const execa = require("execa"); 3 | const http = require("http"); 4 | const httpProxy = require("http-proxy"); 5 | const waitPort = require("wait-port"); 6 | const getPort = require("get-port"); 7 | const chokidar = require("chokidar"); 8 | const { serveFunctions } = require("../../utils/serve-functions"); 9 | const { serverSettings } = require("../../detect-server"); 10 | const { detectFunctionsBuilder } = require("../../detect-functions-builder"); 11 | const Command = require("@netlify/cli-utils"); 12 | const { track } = require("@netlify/cli-utils/src/utils/telemetry"); 13 | const chalk = require("chalk"); 14 | const { 15 | NETLIFYDEV, 16 | NETLIFYDEVLOG, 17 | NETLIFYDEVWARN 18 | // NETLIFYDEVERR 19 | } = require("netlify-cli-logo"); 20 | const boxen = require("boxen"); 21 | const { createTunnel, connectTunnel } = require("../../live-tunnel"); 22 | 23 | function isFunction(settings, req) { 24 | return settings.functionsPort && req.url.match(/^\/.netlify\/functions\/.+/); 25 | } 26 | 27 | function addonUrl(addonUrls, req) { 28 | const m = req.url.match(/^\/.netlify\/([^\/]+)(\/.*)/); // eslint-disable-line no-useless-escape 29 | const addonUrl = m && addonUrls[m[1]]; 30 | return addonUrl ? `${addonUrl}${m[2]}` : null; 31 | } 32 | 33 | // Used as an optimization to avoid dual lookups for missing assets 34 | const assetExtensionRegExp = /\.(html?|png|jpg|js|css|svg|gif|ico|woff|woff2)$/; 35 | 36 | function alternativePathsFor(url) { 37 | const paths = []; 38 | if (url[url.length - 1] === "/") { 39 | const end = url.length - 1; 40 | if (url !== "/") { 41 | paths.push(url.slice(0, end) + ".html"); 42 | paths.push(url.slice(0, end) + ".htm"); 43 | } 44 | paths.push(url + "index.html"); 45 | paths.push(url + "index.htm"); 46 | } else if (!url.match(assetExtensionRegExp)) { 47 | paths.push(url + ".html"); 48 | paths.push(url + ".htm"); 49 | paths.push(url + "/index.html"); 50 | paths.push(url + "/index.htm"); 51 | } 52 | 53 | return paths; 54 | } 55 | 56 | function initializeProxy(port) { 57 | const proxy = httpProxy.createProxyServer({ 58 | selfHandleResponse: true, 59 | target: { 60 | host: "localhost", 61 | port: port 62 | } 63 | }); 64 | 65 | proxy.on("proxyRes", (proxyRes, req, res) => { 66 | if ( 67 | proxyRes.statusCode === 404 && 68 | req.alternativePaths && 69 | req.alternativePaths.length > 0 70 | ) { 71 | req.url = req.alternativePaths.shift(); 72 | return proxy.web(req, res, req.proxyOptions); 73 | } 74 | res.writeHead(proxyRes.statusCode, proxyRes.headers); 75 | proxyRes.on("data", function(data) { 76 | res.write(data); 77 | }); 78 | proxyRes.on("end", function() { 79 | res.end(); 80 | }); 81 | }); 82 | 83 | return { 84 | web: (req, res, options) => { 85 | req.proxyOptions = options; 86 | req.alternativePaths = alternativePathsFor(req.url); 87 | return proxy.web(req, res, options); 88 | }, 89 | ws: (req, socket, head) => proxy.ws(req, socket, head) 90 | }; 91 | } 92 | 93 | async function startProxy(settings, addonUrls) { 94 | const rulesProxy = require("@netlify/rules-proxy"); 95 | 96 | await waitPort({ port: settings.proxyPort }); 97 | if (settings.functionsPort) { 98 | await waitPort({ port: settings.functionsPort }); 99 | } 100 | const port = await getPort({ port: settings.port || 8888 }); 101 | const functionsServer = settings.functionsPort 102 | ? `http://localhost:${settings.functionsPort}` 103 | : null; 104 | 105 | const proxy = initializeProxy(settings.proxyPort); 106 | 107 | const rewriter = rulesProxy({ publicFolder: settings.dist }); 108 | 109 | const server = http.createServer(function(req, res) { 110 | if (isFunction(settings, req)) { 111 | return proxy.web(req, res, { target: functionsServer }); 112 | } 113 | let url = addonUrl(addonUrls, req); 114 | if (url) { 115 | return proxy.web(req, res, { target: url }); 116 | } 117 | 118 | rewriter(req, res, () => { 119 | if (isFunction(settings, req)) { 120 | return proxy.web(req, res, { target: functionsServer }); 121 | } 122 | url = addonUrl(addonUrls, req); 123 | if (url) { 124 | return proxy.web(req, res, { target: url }); 125 | } 126 | 127 | proxy.web(req, res, { target: `http://localhost:${settings.proxyPort}` }); 128 | }); 129 | }); 130 | 131 | server.on("upgrade", function(req, socket, head) { 132 | proxy.ws(req, socket, head); 133 | }); 134 | 135 | server.listen(port); 136 | return { url: `http://localhost:${port}`, port }; 137 | } 138 | 139 | function startDevServer(settings, log) { 140 | if (settings.noCmd) { 141 | const StaticServer = require("static-server"); 142 | 143 | const server = new StaticServer({ 144 | rootPath: settings.dist, 145 | name: "netlify-dev", 146 | port: settings.proxyPort, 147 | templates: { 148 | notFound: "404.html" 149 | } 150 | }); 151 | 152 | server.start(function() { 153 | log(`\n${NETLIFYDEVLOG} Server listening to`, settings.proxyPort); 154 | }); 155 | return; 156 | } 157 | log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.type}`); 158 | const args = 159 | settings.command === "npm" ? ["run", ...settings.args] : settings.args; 160 | const ps = execa(settings.command, args, { 161 | env: { ...settings.env, FORCE_COLOR: "true" }, 162 | stdio: ["inherit", "pipe", "pipe"] 163 | }); 164 | ps.stdout.on("data", function(buffer) { 165 | process.stdout.write(buffer.toString("utf8")); 166 | }); 167 | ps.stderr.on("data", function(buffer) { 168 | process.stderr.write(buffer.toString("utf8")); 169 | }); 170 | ps.on("close", code => process.exit(code)); 171 | ps.on("SIGINT", process.exit); 172 | ps.on("SIGTERM", process.exit); 173 | } 174 | 175 | class DevCommand extends Command { 176 | async run() { 177 | this.log(`${NETLIFYDEV}`); 178 | let { flags } = this.parse(DevCommand); 179 | const { api, site, config } = this.netlify; 180 | const functionsDir = 181 | flags.functions || 182 | (config.dev && config.dev.functions) || 183 | (config.build && config.build.functions); 184 | let addonUrls = {}; 185 | 186 | let accessToken = api.accessToken; 187 | if (site.id && !flags.offline) { 188 | const { addEnvVariables } = require("../../utils/dev"); 189 | addonUrls = await addEnvVariables(api, site, accessToken); 190 | } 191 | process.env.NETLIFY_DEV = "true"; 192 | 193 | let settings = await serverSettings(Object.assign({}, config.dev, flags)); 194 | 195 | if (!(settings && settings.command)) { 196 | this.log( 197 | `${NETLIFYDEVWARN} No dev server detected, using simple static server` 198 | ); 199 | let dist = 200 | (config.dev && config.dev.publish) || 201 | (config.build && config.build.publish); 202 | if (!dist) { 203 | this.log(`${NETLIFYDEVLOG} Using current working directory`); 204 | this.log( 205 | `${NETLIFYDEVWARN} Unable to determine public folder to serve files from.` 206 | ); 207 | this.log( 208 | `${NETLIFYDEVWARN} Setup a netlify.toml file with a [dev] section to specify your dev server settings.` 209 | ); 210 | this.log( 211 | `${NETLIFYDEVWARN} See docs at: https://github.com/netlify/netlify-dev-plugin#project-detection` 212 | ); 213 | this.log( 214 | `${NETLIFYDEVWARN} Using current working directory for now...` 215 | ); 216 | dist = process.cwd(); 217 | } 218 | settings = { 219 | noCmd: true, 220 | port: 8888, 221 | proxyPort: await getPort({ port: 3999 }), 222 | dist 223 | }; 224 | } 225 | 226 | // Reset port if not manually specified, to make it dynamic 227 | if (!(config.dev && config.dev.port) && !flags.port) { 228 | settings = { 229 | port: await getPort({ port: settings.port }), 230 | ...settings 231 | }; 232 | } 233 | 234 | startDevServer(settings, this.log); 235 | 236 | // serve functions from zip-it-and-ship-it 237 | // env variables relies on `url`, careful moving this code 238 | if (functionsDir) { 239 | const functionBuilder = await detectFunctionsBuilder(settings); 240 | if (functionBuilder) { 241 | this.log( 242 | `${NETLIFYDEVLOG} Function builder ${chalk.yellow( 243 | functionBuilder.builderName 244 | )} detected: Running npm script ${chalk.yellow( 245 | functionBuilder.npmScript 246 | )}` 247 | ); 248 | this.warn( 249 | `${NETLIFYDEVWARN} This is a beta feature, please give us feedback on how to improve at https://github.com/netlify/netlify-dev-plugin/` 250 | ); 251 | await functionBuilder.build(); 252 | const functionWatcher = chokidar.watch(functionBuilder.src); 253 | functionWatcher.on("add", functionBuilder.build); 254 | functionWatcher.on("change", functionBuilder.build); 255 | functionWatcher.on("unlink", functionBuilder.build); 256 | } 257 | const functionsPort = await getPort({ port: 34567 }); 258 | 259 | // returns a value but we dont use it 260 | await serveFunctions({ 261 | ...settings, 262 | port: functionsPort, 263 | functionsDir 264 | }); 265 | settings.functionsPort = functionsPort; 266 | } 267 | 268 | let { url, port } = await startProxy(settings, addonUrls); 269 | if (!url) { 270 | url = proxyUrl; 271 | } 272 | 273 | if (flags.live) { 274 | await waitPort({ port }); 275 | const liveSession = await createTunnel(site.id, accessToken, this.log); 276 | url = liveSession.session_url; 277 | process.env.BASE_URL = url; 278 | 279 | await connectTunnel(liveSession, accessToken, port, this.log); 280 | } 281 | 282 | // Todo hoist this telemetry `command` to CLI hook 283 | track("command", { 284 | command: "dev", 285 | projectType: settings.type || "custom", 286 | live: flags.live || false 287 | }); 288 | 289 | // boxen doesnt support text wrapping yet https://github.com/sindresorhus/boxen/issues/16 290 | const banner = require("wrap-ansi")( 291 | chalk.bold(`${NETLIFYDEVLOG} Server now ready on ${url}`), 292 | 70 293 | ); 294 | process.env.URL = url; 295 | process.env.DEPLOY_URL = process.env.URL; 296 | 297 | this.log( 298 | boxen(banner, { 299 | padding: 1, 300 | margin: 1, 301 | align: "center", 302 | borderColor: "#00c7b7" 303 | }) 304 | ); 305 | } 306 | } 307 | 308 | DevCommand.description = `Local dev server 309 | The dev command will run a local dev server with Netlify's proxy and redirect rules 310 | `; 311 | 312 | DevCommand.examples = [ 313 | "$ netlify dev", 314 | '$ netlify dev -c "yarn start"', 315 | "$ netlify dev -c hugo" 316 | ]; 317 | 318 | DevCommand.strict = false; 319 | 320 | DevCommand.flags = { 321 | command: flags.string({ 322 | char: "c", 323 | description: "command to run" 324 | }), 325 | port: flags.integer({ 326 | char: "p", 327 | description: "port of netlify dev" 328 | }), 329 | dir: flags.string({ 330 | char: "d", 331 | description: "dir with static files" 332 | }), 333 | functions: flags.string({ 334 | char: "f", 335 | description: "Specify a functions folder to serve" 336 | }), 337 | offline: flags.boolean({ 338 | char: "o", 339 | description: "disables any features that require network access" 340 | }), 341 | live: flags.boolean({ 342 | char: "l", 343 | description: "Start a public live session" 344 | }) 345 | }; 346 | 347 | module.exports = DevCommand; 348 | -------------------------------------------------------------------------------- /src/commands/functions/build.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { flags } = require("@oclif/command"); 3 | const Command = require("@netlify/cli-utils"); 4 | const { zipFunctions } = require("@netlify/zip-it-and-ship-it"); 5 | const { 6 | // NETLIFYDEV, 7 | NETLIFYDEVLOG, 8 | // NETLIFYDEVWARN, 9 | NETLIFYDEVERR 10 | } = require("netlify-cli-logo"); 11 | 12 | class FunctionsBuildCommand extends Command { 13 | async run() { 14 | const { flags } = this.parse(FunctionsBuildCommand); 15 | const { config } = this.netlify; 16 | 17 | const src = flags.src || config.build.functionsSource; 18 | const dst = flags.functions || config.build.functions; 19 | 20 | if (src === dst) { 21 | this.log( 22 | `${NETLIFYDEVERR} Source and destination for function build can't be the same` 23 | ); 24 | process.exit(1); 25 | } 26 | 27 | if (!src || !dst) { 28 | if (!src) 29 | this.log( 30 | `${NETLIFYDEVERR} Error: You must specify a source folder with a --src flag or a functionsSource field in your config` 31 | ); 32 | if (!dst) 33 | this.log( 34 | `${NETLIFYDEVERR} Error: You must specify a destination functions folder with a --functions flag or a functions field in your config` 35 | ); 36 | process.exit(1); 37 | } 38 | 39 | fs.mkdirSync(dst, { recursive: true }); 40 | 41 | this.log(`${NETLIFYDEVLOG} Building functions`); 42 | zipFunctions(src, dst, { skipGo: true }); 43 | this.log(`${NETLIFYDEVLOG} Functions built to `, dst); 44 | } 45 | } 46 | 47 | FunctionsBuildCommand.description = `build functions locally 48 | `; 49 | FunctionsBuildCommand.aliases = ["function:build"]; 50 | FunctionsBuildCommand.flags = { 51 | functions: flags.string({ 52 | char: "f", 53 | description: "Specify a functions folder to build to" 54 | }), 55 | src: flags.string({ 56 | char: "s", 57 | description: "Specify the source folder for the functions" 58 | }) 59 | }; 60 | 61 | module.exports = FunctionsBuildCommand; 62 | -------------------------------------------------------------------------------- /src/commands/functions/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const { Command } = require("@oclif/command"); 3 | const { execSync } = require("child_process"); 4 | 5 | function showHelp(command) { 6 | execSync(`netlify ${command} --help`, { stdio: [0, 1, 2] }); 7 | } 8 | 9 | function isEmptyCommand(flags, args) { 10 | if (!hasFlags(flags) && !hasArgs(args)) { 11 | return true; 12 | } 13 | return false; 14 | } 15 | 16 | function hasFlags(flags) { 17 | return Object.keys(flags).length; 18 | } 19 | 20 | function hasArgs(args) { 21 | return Object.keys(args).length; 22 | } 23 | 24 | class FunctionsCommand extends Command { 25 | async run() { 26 | const { flags, args } = this.parse(FunctionsCommand); 27 | // run help command if no args passed 28 | if (isEmptyCommand(flags, args)) { 29 | showHelp(this.id); 30 | this.exit(); 31 | } 32 | } 33 | } 34 | 35 | const name = chalk.greenBright("`functions`"); 36 | FunctionsCommand.aliases = ["function"]; 37 | FunctionsCommand.description = `Manage netlify functions 38 | The ${name} command will help you manage the functions in this site 39 | `; 40 | FunctionsCommand.examples = [ 41 | "netlify functions:create --name function-xyz", 42 | "netlify functions:build --name function-abc --timeout 30s" 43 | ]; 44 | 45 | module.exports = FunctionsCommand; 46 | -------------------------------------------------------------------------------- /src/commands/functions/invoke.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const Command = require("@netlify/cli-utils"); 3 | const { flags } = require("@oclif/command"); 4 | const inquirer = require("inquirer"); 5 | const { serverSettings } = require("../../detect-server"); 6 | const fetch = require("node-fetch"); 7 | const fs = require("fs"); 8 | const path = require("path"); 9 | 10 | const { getFunctions } = require("../../utils/get-functions"); 11 | 12 | // https://www.netlify.com/docs/functions/#event-triggered-functions 13 | const eventTriggeredFunctions = [ 14 | "deploy-building", 15 | "deploy-succeeded", 16 | "deploy-failed", 17 | "deploy-locked", 18 | "deploy-unlocked", 19 | "split-test-activated", 20 | "split-test-deactivated", 21 | "split-test-modified", 22 | "submission-created", 23 | "identity-validate", 24 | "identity-signup", 25 | "identity-login" 26 | ]; 27 | class FunctionsInvokeCommand extends Command { 28 | async run() { 29 | let { flags, args } = this.parse(FunctionsInvokeCommand); 30 | const { api, site, config } = this.netlify; 31 | 32 | const functionsDir = 33 | flags.functions || 34 | (config.dev && config.dev.functions) || 35 | (config.build && config.build.functions); 36 | if (typeof functionsDir === "undefined") { 37 | this.error( 38 | "functions directory is undefined, did you forget to set it in netlify.toml?" 39 | ); 40 | process.exit(1); 41 | } 42 | 43 | let settings = await serverSettings(Object.assign({}, config.dev, flags)); 44 | 45 | if (!(settings && settings.command)) { 46 | settings = { 47 | noCmd: true, 48 | port: 8888, 49 | proxyPort: 3999 50 | }; 51 | } 52 | 53 | const functions = getFunctions(functionsDir); 54 | const functionToTrigger = await getNameFromArgs(functions, args, flags); 55 | 56 | let headers = {}; 57 | let body = {}; 58 | 59 | if (eventTriggeredFunctions.includes(functionToTrigger)) { 60 | /** handle event triggered fns */ 61 | // https://www.netlify.com/docs/functions/#event-triggered-functions 62 | const parts = functionToTrigger.split("-"); 63 | if (parts[0] === "identity") { 64 | // https://www.netlify.com/docs/functions/#identity-event-functions 65 | body.event = parts[1]; 66 | body.user = { 67 | email: "foo@trust-this-company.com", 68 | user_metadata: { 69 | TODO: "mock our netlify identity user data better" 70 | } 71 | }; 72 | } else { 73 | // non identity functions seem to have a different shape 74 | // https://www.netlify.com/docs/functions/#event-function-payloads 75 | body.payload = { 76 | TODO: "mock up payload data better" 77 | }; 78 | body.site = { 79 | TODO: "mock up site data better" 80 | }; 81 | } 82 | } else { 83 | // NOT an event triggered function, but may still want to simulate authentication locally 84 | let _isAuthed = false; 85 | if (typeof flags.identity === "undefined") { 86 | const { isAuthed } = await inquirer.prompt([ 87 | { 88 | type: "confirm", 89 | name: "isAuthed", 90 | message: `Invoke with emulated Netlify Identity authentication headers? (pass --identity/--no-identity to override)`, 91 | default: true 92 | } 93 | ]); 94 | _isAuthed = isAuthed; 95 | } else { 96 | _isAuthed = flags.identity; 97 | } 98 | if (_isAuthed) { 99 | headers = { 100 | authorization: 101 | "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGZ1bmN0aW9uczp0cmlnZ2VyIiwidGVzdERhdGEiOiJORVRMSUZZX0RFVl9MT0NBTExZX0VNVUxBVEVEX0pXVCJ9.Xb6vOFrfLUZmyUkXBbCvU4bM7q8tPilF0F03Wupap_c" 102 | }; 103 | // you can decode this https://jwt.io/ 104 | // { 105 | // "source": "netlify functions:trigger", 106 | // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_JWT" 107 | // } 108 | } 109 | } 110 | const payload = processPayloadFromFlag(flags.payload); 111 | body = Object.assign({}, body, payload); 112 | 113 | // fetch 114 | fetch( 115 | `http://localhost:${ 116 | settings.port 117 | }/.netlify/functions/${functionToTrigger}` + 118 | formatQstring(flags.querystring), 119 | { 120 | method: "post", 121 | headers, 122 | body: JSON.stringify(body) 123 | } 124 | ) 125 | .then(response => { 126 | let data; 127 | data = response.text(); 128 | try { 129 | // data = response.json(); 130 | data = JSON.parse(data); 131 | } catch (err) {} 132 | return data; 133 | }) 134 | .then(console.log) 135 | .catch(err => { 136 | console.error("ran into an error invoking your function"); 137 | console.error(err); 138 | }); 139 | } 140 | } 141 | 142 | function formatQstring(querystring) { 143 | if (querystring) { 144 | return "?" + querystring; 145 | } else { 146 | return ""; 147 | } 148 | } 149 | 150 | /** process payloads from flag */ 151 | function processPayloadFromFlag(payloadString) { 152 | if (payloadString) { 153 | // case 1: jsonstring 154 | let payload = tryParseJSON(payloadString); 155 | if (!!payload) return payload; 156 | // case 2: jsonpath 157 | const payloadpath = path.join(process.cwd(), payloadString); 158 | const pathexists = fs.existsSync(payloadpath); 159 | if (!payload && pathexists) { 160 | try { 161 | payload = require(payloadpath); // there is code execution potential here 162 | return payload; 163 | } catch (err) { 164 | console.error(err); 165 | payload = false; 166 | } 167 | } 168 | // case 3: invalid string, invalid path 169 | return false; 170 | } 171 | } 172 | 173 | // prompt for a name if name not supplied 174 | // also used in functions:create 175 | async function getNameFromArgs(functions, args, flags) { 176 | // let functionToTrigger = flags.name; 177 | // const isValidFn = Object.keys(functions).includes(functionToTrigger); 178 | if (flags.name && args.name) { 179 | console.error( 180 | "function name specified in both flag and arg format, pick one" 181 | ); 182 | process.exit(1); 183 | } 184 | let functionToTrigger; 185 | if (flags.name && !args.name) functionToTrigger = flags.name; 186 | // use flag if exists 187 | else if (!flags.name && args.name) functionToTrigger = args.name; 188 | 189 | const isValidFn = Object.keys(functions).includes(functionToTrigger); 190 | if (!functionToTrigger || !isValidFn) { 191 | if (!isValidFn) { 192 | console.warn( 193 | `Function name ${chalk.yellow( 194 | functionToTrigger 195 | )} supplied but no matching function found in your functions folder, forcing you to pick a valid one...` 196 | ); 197 | } 198 | const { trigger } = await inquirer.prompt([ 199 | { 200 | type: "list", 201 | message: "Pick a function to trigger", 202 | name: "trigger", 203 | choices: Object.keys(functions) 204 | } 205 | ]); 206 | functionToTrigger = trigger; 207 | } 208 | 209 | return functionToTrigger; 210 | } 211 | 212 | FunctionsInvokeCommand.description = `trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions`; 213 | FunctionsInvokeCommand.aliases = ["function:trigger"]; 214 | 215 | FunctionsInvokeCommand.examples = [ 216 | "$ netlify functions:invoke", 217 | "$ netlify functions:invoke myfunction", 218 | "$ netlify functions:invoke --name myfunction", 219 | "$ netlify functions:invoke --name myfunction --identity", 220 | "$ netlify functions:invoke --name myfunction --no-identity", 221 | '$ netlify functions:invoke myfunction --payload "{"foo": 1}"', 222 | '$ netlify functions:invoke myfunction --querystring "foo=1', 223 | '$ netlify functions:invoke myfunction --payload "./pathTo.json"' 224 | ]; 225 | FunctionsInvokeCommand.args = [ 226 | { 227 | name: "name", 228 | description: "function name to invoke" 229 | } 230 | ]; 231 | 232 | FunctionsInvokeCommand.flags = { 233 | name: flags.string({ 234 | char: "n", 235 | description: "function name to invoke" 236 | }), 237 | functions: flags.string({ 238 | char: "f", 239 | description: "Specify a functions folder to parse, overriding netlify.toml" 240 | }), 241 | querystring: flags.string({ 242 | char: "q", 243 | description: "Querystring to add to your function invocation" 244 | }), 245 | payload: flags.string({ 246 | char: "p", 247 | description: 248 | "Supply POST payload in stringified json, or a path to a json file" 249 | }), 250 | identity: flags.boolean({ 251 | description: 252 | "simulate Netlify Identity authentication JWT. pass --no-identity to affirm unauthenticated request", 253 | allowNo: true 254 | }) 255 | }; 256 | 257 | module.exports = FunctionsInvokeCommand; 258 | 259 | // https://stackoverflow.com/questions/3710204/how-to-check-if-a-string-is-a-valid-json-string-in-javascript-without-using-try 260 | function tryParseJSON(jsonString) { 261 | try { 262 | var o = JSON.parse(jsonString); 263 | 264 | // Handle non-exception-throwing cases: 265 | // Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking, 266 | // but... JSON.parse(null) returns null, and typeof null === "object", 267 | // so we must check for that, too. Thankfully, null is falsey, so this suffices: 268 | if (o && typeof o === "object") { 269 | return o; 270 | } 271 | } catch (e) {} 272 | 273 | return false; 274 | } 275 | -------------------------------------------------------------------------------- /src/commands/functions/list.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const Command = require("@netlify/cli-utils"); 3 | const { flags } = require("@oclif/command"); 4 | const AsciiTable = require("ascii-table"); 5 | const { getFunctions } = require("../../utils/get-functions"); 6 | class FunctionsListCommand extends Command { 7 | async run() { 8 | let { flags } = this.parse(FunctionsListCommand); 9 | const { api, site, config } = this.netlify; 10 | 11 | // get deployed site details 12 | // copied from `netlify status` 13 | const siteId = site.id; 14 | if (!siteId) { 15 | this.warn("Did you run `netlify link` yet?"); 16 | this.error(`You don't appear to be in a folder that is linked to a site`); 17 | } 18 | let siteData; 19 | try { 20 | siteData = await api.getSite({ siteId }); 21 | } catch (e) { 22 | if (e.status === 401 /* unauthorized*/) { 23 | this.warn( 24 | `Log in with a different account or re-link to a site you have permission for` 25 | ); 26 | this.error( 27 | `Not authorized to view the currently linked site (${siteId})` 28 | ); 29 | } 30 | if (e.status === 404 /* missing */) { 31 | this.error(`The site this folder is linked to can't be found`); 32 | } 33 | this.error(e); 34 | } 35 | const deploy = siteData.published_deploy || {}; 36 | const deployed_functions = deploy.available_functions || []; 37 | 38 | const functionsDir = 39 | flags.functions || 40 | (config.dev && config.dev.functions) || 41 | (config.build && config.build.functions); 42 | if (typeof functionsDir === "undefined") { 43 | this.error( 44 | "functions directory is undefined, did you forget to set it in netlify.toml?" 45 | ); 46 | process.exit(1); 47 | } 48 | var table = new AsciiTable( 49 | `Netlify Functions (based on local functions folder "${functionsDir}")` 50 | ); 51 | const functions = getFunctions(functionsDir); 52 | 53 | table.setHeading("Name", "Url", "moduleDir", "deployed"); 54 | Object.entries(functions).forEach(([functionName, { moduleDir }]) => { 55 | const isDeployed = deployed_functions 56 | .map(({ n }) => n) 57 | .includes(functionName); 58 | 59 | // this.log(`${chalk.yellow("function name")}: ${functionName}`); 60 | // this.log( 61 | // ` ${chalk.yellow( 62 | // "url" 63 | // )}: ${`/.netlify/functions/${functionName}`}` 64 | // ); 65 | // this.log(` ${chalk.yellow("moduleDir")}: ${moduleDir}`); 66 | // this.log( 67 | // ` ${chalk.yellow("deployed")}: ${ 68 | // isDeployed ? chalk.green("yes") : chalk.yellow("no") 69 | // }` 70 | // ); 71 | // this.log("----------"); 72 | table.addRow( 73 | functionName, 74 | `/.netlify/functions/${functionName}`, 75 | moduleDir, 76 | isDeployed ? "yes" : "no" 77 | ); 78 | }); 79 | this.log(table.toString()); 80 | } 81 | } 82 | 83 | FunctionsListCommand.description = `list functions that exist locally 84 | 85 | Helpful for making sure that you have formatted your functions correctly 86 | 87 | NOT the same as listing the functions that have been deployed. For that info you need to go to your Netlify deploy log. 88 | `; 89 | FunctionsListCommand.aliases = ["function:list"]; 90 | FunctionsListCommand.flags = { 91 | name: flags.string({ 92 | char: "n", 93 | description: "name to print" 94 | }), 95 | functions: flags.string({ 96 | char: "f", 97 | description: "Specify a functions folder to serve" 98 | }) 99 | }; 100 | 101 | // TODO make visible once implementation complete 102 | FunctionsListCommand.hidden = true; 103 | 104 | module.exports = FunctionsListCommand; 105 | -------------------------------------------------------------------------------- /src/detect-functions-builder.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const detectors = require("fs") 4 | .readdirSync(path.join(__dirname, "function-builder-detectors")) 5 | .filter(x => x.endsWith(".js")) // only accept .js detector files 6 | .map(det => 7 | require(path.join(__dirname, `function-builder-detectors/${det}`)) 8 | ); 9 | 10 | module.exports.detectFunctionsBuilder = function() { 11 | for (const i in detectors) { 12 | const settings = detectors[i](); 13 | if (settings) { 14 | return settings; 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/detect-server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const chalk = require("chalk"); 3 | const { NETLIFYDEVLOG } = require("netlify-cli-logo"); 4 | const inquirer = require("inquirer"); 5 | const fs = require("fs"); 6 | const detectors = fs 7 | .readdirSync(path.join(__dirname, "detectors")) 8 | .filter(x => x.endsWith(".js")) // only accept .js detector files 9 | .map(det => { 10 | try { 11 | return require(path.join(__dirname, `detectors/${det}`)); 12 | } catch (err) { 13 | console.error( 14 | `failed to load detector: ${chalk.yellow( 15 | det 16 | )}, this is likely a bug in the detector, please file an issue in netlify-dev-plugin`, 17 | err 18 | ); 19 | return null; 20 | } 21 | }) 22 | .filter(Boolean); 23 | 24 | module.exports.serverSettings = async devConfig => { 25 | let settingsArr = []; 26 | let settings = null; 27 | for (const i in detectors) { 28 | const detectorResult = detectors[i](); 29 | if (detectorResult) settingsArr.push(detectorResult); 30 | } 31 | if (settingsArr.length === 1) { 32 | // vast majority of projects will only have one matching detector 33 | settings = settingsArr[0]; 34 | settings.args = settings.possibleArgsArrs[0]; // just pick the first one 35 | if (!settings.args) { 36 | const { scripts } = JSON.parse( 37 | fs.readFileSync("package.json", { encoding: "utf8" }) 38 | ); 39 | // eslint-disable-next-line no-console 40 | console.error( 41 | "empty args assigned, this is an internal Netlify Dev bug, please report your settings and scripts so we can improve", 42 | { scripts, settings } 43 | ); 44 | // eslint-disable-next-line no-process-exit 45 | process.exit(1); 46 | } 47 | } else if (settingsArr.length > 1) { 48 | /** multiple matching detectors, make the user choose */ 49 | // lazy loading on purpose 50 | inquirer.registerPrompt( 51 | "autocomplete", 52 | require("inquirer-autocomplete-prompt") 53 | ); 54 | const fuzzy = require("fuzzy"); 55 | const scriptInquirerOptions = formatSettingsArrForInquirer(settingsArr); 56 | const { chosenSetting } = await inquirer.prompt({ 57 | name: "chosenSetting", 58 | message: `Multiple possible start commands found`, 59 | type: "autocomplete", 60 | source: async function(_, input) { 61 | if (!input || input === "") { 62 | return scriptInquirerOptions; 63 | } 64 | // only show filtered results 65 | return filterSettings(scriptInquirerOptions, input); 66 | } 67 | }); 68 | settings = chosenSetting; // finally! we have a selected option 69 | // TODO: offer to save this setting to netlify.toml so you dont keep doing this 70 | 71 | /** utiltities for the inquirer section above */ 72 | function filterSettings(scriptInquirerOptions, input) { 73 | const filteredSettings = fuzzy.filter( 74 | input, 75 | scriptInquirerOptions.map(x => x.name) 76 | ); 77 | const filteredSettingNames = filteredSettings.map(x => 78 | input ? x.string : x 79 | ); 80 | return scriptInquirerOptions.filter(t => 81 | filteredSettingNames.includes(t.name) 82 | ); 83 | } 84 | 85 | /** utiltities for the inquirer section above */ 86 | function formatSettingsArrForInquirer(settingsArr) { 87 | let ans = []; 88 | settingsArr.forEach(setting => { 89 | setting.possibleArgsArrs.forEach(args => { 90 | ans.push({ 91 | name: `[${chalk.yellow(setting.type)}] ${ 92 | setting.command 93 | } ${args.join(" ")}`, 94 | value: { ...setting, args }, 95 | short: setting.type + "-" + args.join(" ") 96 | }); 97 | }); 98 | }); 99 | return ans; 100 | } 101 | } 102 | 103 | /** everything below assumes we have settled on one detector */ 104 | const tellUser = settingsField => dV => 105 | // eslint-disable-next-line no-console 106 | console.log( 107 | `${NETLIFYDEVLOG} Overriding ${chalk.yellow( 108 | settingsField 109 | )} with setting derived from netlify.toml [dev] block: `, 110 | dV 111 | ); 112 | 113 | if (devConfig) { 114 | settings = settings || {}; 115 | if (devConfig.command) { 116 | settings.command = assignLoudly( 117 | devConfig.command.split(/\s/)[0], 118 | settings.command || null, 119 | tellUser("command") 120 | ); // if settings.command is empty, its bc no settings matched 121 | let devConfigArgs = devConfig.command.split(/\s/).slice(1); 122 | if (devConfigArgs[0] === "run") devConfigArgs = devConfigArgs.slice(1); 123 | settings.args = assignLoudly( 124 | devConfigArgs, 125 | settings.command || null, 126 | tellUser("command") 127 | ); // if settings.command is empty, its bc no settings matched 128 | } 129 | if (devConfig.port) { 130 | settings.proxyPort = devConfig.port || settings.proxyPort; 131 | const regexp = 132 | devConfig.urlRegexp || 133 | new RegExp(`(http://)([^:]+:)${devConfig.port}(/)?`, "g"); 134 | settings.urlRegexp = settings.urlRegexp || regexp; 135 | } 136 | settings.dist = devConfig.publish || settings.dist; // dont loudassign if they dont need it 137 | } 138 | return settings; 139 | }; 140 | 141 | // if first arg is undefined, use default, but tell user about it in case it is unintentional 142 | function assignLoudly( 143 | optionalValue, 144 | defaultValue, 145 | // eslint-disable-next-line no-console 146 | tellUser = dV => console.log(`No value specified, using fallback of `, dV) 147 | ) { 148 | if (defaultValue === undefined) throw new Error("must have a defaultValue"); 149 | if (defaultValue !== optionalValue && optionalValue === undefined) { 150 | tellUser(defaultValue); 151 | return defaultValue; 152 | } 153 | return optionalValue; 154 | } 155 | -------------------------------------------------------------------------------- /src/detectors/README.md: -------------------------------------------------------------------------------- 1 | ## writing a detector 2 | 3 | - write as many checks as possible to fit your project 4 | - return false if its not your project 5 | - if it definitely is, return an object with this shape: 6 | 7 | ```ts 8 | { 9 | type: String, // e.g. gatsby, vue-cli 10 | command: String, // e.g. yarn, npm 11 | port: Number, // e.g. 8888 12 | proxyPort: Number, // e.g. 3000 13 | env: Object, // env variables, see examples 14 | possibleArgsArrs: [[String]], // e.g [['run develop]], so that the combined command is 'npm run develop', but we allow for multiple 15 | urlRegexp: RegExp, // see examples 16 | dist: String, // static folder where a _redirect file would be placed, e.g. 'public' or 'static'. NOT the build output folder 17 | } 18 | ``` 19 | 20 | ## things to note 21 | 22 | - Dev block overrides will supercede anything you write in your detector: https://github.com/netlify/netlify-dev-plugin#project-detection 23 | - detectors are language agnostic. don't assume npm or yarn. 24 | - if default args (like 'develop') are missing, that means the user has configured it, best to tell them to use the -c flag. 25 | 26 | ## detector notes 27 | 28 | - metalsmith is popular but has no dev story so we have skipped it 29 | - hub press doesnt even have cli https://github.com/HubPress/hubpress.io#what-is-hubpress 30 | - gitbook: 31 | 32 | not sure if we want to support gitbook yet 33 | 34 | requires a global install: https://github.com/GitbookIO/gitbook/blob/master/docs/setup.md 35 | 36 | ```js 37 | const { 38 | hasRequiredDeps, 39 | hasRequiredFiles, 40 | getYarnOrNPMCommand, 41 | scanScripts 42 | } = require("./utils/jsdetect"); 43 | module.exports = function() { 44 | // REQUIRED FILES 45 | if (!hasRequiredFiles(["README.md", "SUMMARY.md"])) return false; 46 | // // REQUIRED DEPS 47 | // if (!hasRequiredDeps(["hexo"])) return false; 48 | 49 | /** everything below now assumes that we are within gatsby */ 50 | 51 | const possibleArgsArrs = [["gitbook", "serve"]]; 52 | // scanScripts({ 53 | // preferredScriptsArr: ["start", "dev", "develop"], 54 | // preferredCommand: "hexo server" 55 | // }); 56 | 57 | return { 58 | type: "gitbook", 59 | command: getYarnOrNPMCommand(), 60 | port: 8888, 61 | proxyPort: 4000, 62 | env: { ...process.env }, 63 | possibleArgsArrs, 64 | urlRegexp: new RegExp(`(http://)([^:]+:)${4000}(/)?`, "g"), 65 | dist: "public" 66 | }; 67 | }; 68 | ``` 69 | -------------------------------------------------------------------------------- /src/detectors/brunch.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json", "brunch-config.js"])) return false; 10 | // REQUIRED DEPS 11 | if (!hasRequiredDeps(["brunch"])) return false; 12 | 13 | /** everything below now assumes that we are within gatsby */ 14 | 15 | const possibleArgsArrs = scanScripts({ 16 | preferredScriptsArr: ["start"], 17 | preferredCommand: "brunch watch --server" 18 | }); 19 | 20 | return { 21 | type: "brunch", 22 | command: getYarnOrNPMCommand(), 23 | port: 8888, 24 | proxyPort: 3333, 25 | env: { ...process.env }, 26 | possibleArgsArrs, 27 | urlRegexp: new RegExp(`(http://)([^:]+:)${3333}(/)?`, "g"), 28 | dist: "app/assets" 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/detectors/cra.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | /** 9 | * detection logic - artificial intelligence! 10 | * */ 11 | module.exports = function() { 12 | // REQUIRED FILES 13 | if (!hasRequiredFiles(["package.json"])) return false; 14 | // REQUIRED DEPS 15 | if (!hasRequiredDeps(["react-scripts"])) return false; 16 | 17 | /** everything below now assumes that we are within create-react-app */ 18 | 19 | const possibleArgsArrs = scanScripts({ 20 | preferredScriptsArr: ["start", "serve", "run"], 21 | preferredCommand: "react-scripts start" 22 | }); 23 | 24 | if (possibleArgsArrs.length === 0) { 25 | // ofer to run it when the user doesnt have any scripts setup! 🤯 26 | possibleArgsArrs.push(["react-scripts", "start"]); 27 | } 28 | 29 | return { 30 | type: "create-react-app", 31 | command: getYarnOrNPMCommand(), 32 | port: 8888, // the port that the Netlify Dev User will use 33 | proxyPort: 3000, // the port that create-react-app normally outputs 34 | env: { ...process.env, BROWSER: "none", PORT: 3000 }, 35 | possibleArgsArrs, 36 | urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), 37 | dist: "public" 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/detectors/docusaurus.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json", "siteConfig.js"])) return false; 10 | // REQUIRED DEPS 11 | if (!hasRequiredDeps(["docusaurus"])) return false; 12 | 13 | /** everything below now assumes that we are within gatsby */ 14 | 15 | const possibleArgsArrs = scanScripts({ 16 | preferredScriptsArr: ["start"], 17 | preferredCommand: "docusaurus-start" 18 | }); 19 | 20 | return { 21 | type: "docusaurus", 22 | command: getYarnOrNPMCommand(), 23 | port: 8888, 24 | proxyPort: 3000, 25 | env: { ...process.env }, 26 | possibleArgsArrs, 27 | urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), 28 | dist: "static" 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/detectors/eleventy.js: -------------------------------------------------------------------------------- 1 | const { 2 | // hasRequiredDeps, 3 | hasRequiredFiles 4 | // scanScripts 5 | } = require("./utils/jsdetect"); 6 | 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json", ".eleventy.js"])) return false; 10 | // commented this out because we're not sure if we want to require it 11 | // // REQUIRED DEPS 12 | // if (!hasRequiredDeps(["@11y/eleventy"])) return false; 13 | 14 | return { 15 | type: "eleventy", 16 | port: 8888, 17 | proxyPort: 8080, 18 | env: { ...process.env }, 19 | command: "npx", 20 | possibleArgsArrs: [["eleventy", "--serve", "--watch"]], 21 | urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), 22 | dist: "_site" 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/detectors/gatsby.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json", "gatsby-config.js"])) return false; 10 | // REQUIRED DEPS 11 | if (!hasRequiredDeps(["gatsby"])) return false; 12 | 13 | /** everything below now assumes that we are within gatsby */ 14 | 15 | const possibleArgsArrs = scanScripts({ 16 | preferredScriptsArr: ["start", "develop", "dev"], 17 | preferredCommand: "gatsby develop" 18 | }); 19 | 20 | if (possibleArgsArrs.length === 0) { 21 | // ofer to run it when the user doesnt have any scripts setup! 🤯 22 | possibleArgsArrs.push(["gatsby", "develop"]); 23 | } 24 | return { 25 | type: "gatsby", 26 | command: getYarnOrNPMCommand(), 27 | port: 8888, 28 | proxyPort: 8000, 29 | env: { ...process.env }, 30 | possibleArgsArrs, 31 | urlRegexp: new RegExp(`(http://)([^:]+:)${8000}(/)?`, "g"), 32 | dist: "public" 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/detectors/gridsome.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json", "gridsome.config.js"])) return false; 10 | // REQUIRED DEPS 11 | if (!hasRequiredDeps(["gridsome"])) return false; 12 | 13 | /** everything below now assumes that we are within gridsome */ 14 | 15 | const possibleArgsArrs = scanScripts({ 16 | preferredScriptsArr: ["develop"], 17 | preferredCommand: "gridsome develop" 18 | }); 19 | 20 | return { 21 | type: "gridsome", 22 | command: getYarnOrNPMCommand(), 23 | port: 8888, 24 | proxyPort: 8080, 25 | env: { ...process.env }, 26 | possibleArgsArrs, 27 | urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), 28 | dist: "static" 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/detectors/hexo.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json", "_config.yml"])) return false; 10 | // REQUIRED DEPS 11 | if (!hasRequiredDeps(["hexo"])) return false; 12 | 13 | /** everything below now assumes that we are within gatsby */ 14 | 15 | const possibleArgsArrs = scanScripts({ 16 | preferredScriptsArr: ["start", "dev", "develop"], 17 | preferredCommand: "hexo server" 18 | }); 19 | 20 | if (possibleArgsArrs.length === 0) { 21 | // ofer to run it when the user doesnt have any scripts setup! 🤯 22 | possibleArgsArrs.push(["hexo", "server"]); 23 | } 24 | return { 25 | type: "hexo", 26 | command: getYarnOrNPMCommand(), 27 | port: 8888, 28 | proxyPort: 4000, 29 | env: { ...process.env }, 30 | possibleArgsArrs, 31 | urlRegexp: new RegExp(`(http://)([^:]+:)${4000}(/)?`, "g"), 32 | dist: "public" 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/detectors/hugo.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require("fs"); 2 | 3 | module.exports = function() { 4 | if (!existsSync("config.toml") && !existsSync("config.yaml")) { 5 | return false; 6 | } 7 | 8 | return { 9 | type: "hugo", 10 | port: 8888, 11 | proxyPort: 1313, 12 | env: { ...process.env }, 13 | command: "hugo", 14 | possibleArgsArrs: [["server", "-w"]], 15 | urlRegexp: new RegExp(`(http://)([^:]+:)${1313}(/)?`, "g"), 16 | dist: "public" 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/detectors/jekyll.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require("fs"); 2 | 3 | module.exports = function() { 4 | if (!existsSync("_config.yml")) { 5 | return false; 6 | } 7 | 8 | return { 9 | type: "jekyll", 10 | port: 8888, 11 | proxyPort: 4000, 12 | env: { ...process.env }, 13 | command: "bundle", 14 | possibleArgsArrs: [["exec", "jekyll", "serve", "-w", "-l"]], 15 | urlRegexp: new RegExp(`(http://)([^:]+:)${4000}(/)?`, "g"), 16 | dist: "_site" 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/detectors/middleman.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require("fs"); 2 | 3 | module.exports = function() { 4 | if (!existsSync("config.rb")) { 5 | return false; 6 | } 7 | 8 | return { 9 | type: "middleman", 10 | port: 8888, 11 | proxyPort: 4567, 12 | env: { ...process.env }, 13 | command: "bundle", 14 | possibleArgsArrs: [["exec", "middleman", "server"]], 15 | urlRegexp: new RegExp(`(http://)([^:]+:)${4567}(/)?`, "g"), 16 | dist: "build" 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/detectors/next.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json"])) return false; 10 | // REQUIRED DEPS 11 | if (!hasRequiredDeps(["next"])) return false; 12 | 13 | /** everything below now assumes that we are within gatsby */ 14 | 15 | const possibleArgsArrs = scanScripts({ 16 | preferredScriptsArr: ["dev", "develop", "start"], 17 | preferredCommand: "next" 18 | }); 19 | 20 | if (possibleArgsArrs.length === 0) { 21 | // ofer to run it when the user doesnt have any scripts setup! 🤯 22 | possibleArgsArrs.push(["next"]); 23 | } 24 | return { 25 | type: "next.js", 26 | command: getYarnOrNPMCommand(), 27 | port: 8888, 28 | proxyPort: 3000, 29 | env: { ...process.env }, 30 | possibleArgsArrs, 31 | urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), 32 | dist: "out" 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/detectors/nuxt.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | module.exports = function() { 9 | // REQUIRED FILES 10 | if (!hasRequiredFiles(["package.json"])) return false; 11 | // REQUIRED DEPS 12 | if (!hasRequiredDeps(["nuxt"])) return false; 13 | 14 | /** everything below now assumes that we are within vue */ 15 | 16 | const possibleArgsArrs = scanScripts({ 17 | preferredScriptsArr: ["start", "dev", "run"], 18 | preferredCommand: "nuxt start" 19 | }); 20 | 21 | if (possibleArgsArrs.length === 0) { 22 | // ofer to run it when the user doesnt have any scripts setup! 🤯 23 | possibleArgsArrs.push(["nuxt", "start"]); 24 | } 25 | 26 | return { 27 | type: "yarn", 28 | command: getYarnOrNPMCommand(), 29 | port: 8888, 30 | proxyPort: 3000, 31 | env: { ...process.env }, 32 | possibleArgsArrs, 33 | urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), 34 | dist: ".nuxt" 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/detectors/phenomic.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json"])) return false; 10 | // REQUIRED DEPS 11 | if (!hasRequiredDeps(["@phenomic/core"])) return false; 12 | 13 | /** everything below now assumes that we are within gatsby */ 14 | 15 | const possibleArgsArrs = scanScripts({ 16 | preferredScriptsArr: ["start"], 17 | preferredCommand: "phenomic start" 18 | }); 19 | 20 | return { 21 | type: "phenomic", 22 | command: getYarnOrNPMCommand(), 23 | port: 8888, 24 | proxyPort: 3333, 25 | env: { ...process.env }, 26 | possibleArgsArrs, 27 | urlRegexp: new RegExp(`(http://)([^:]+:)${3333}(/)?`, "g"), 28 | dist: "public" 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/detectors/quasar-v0.17.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | module.exports = function() { 9 | // REQUIRED FILES 10 | if (!hasRequiredFiles(["package.json"])) return false; 11 | // REQUIRED DEPS 12 | if (!hasRequiredDeps(["quasar-cli"])) return false; 13 | 14 | /** everything below now assumes that we are within Quasar */ 15 | 16 | const possibleArgsArrs = scanScripts({ 17 | preferredScriptsArr: ["serve", "start", "run", "dev"] 18 | // NOTE: this is comented out as it was picking this up in cordova related scripts. 19 | // preferredCommand: "quasar dev" 20 | }); 21 | 22 | if (possibleArgsArrs.length === 0) { 23 | // ofer to run this default when the user doesnt have any matching scripts setup! 24 | possibleArgsArrs.push(["quasar", "dev"]); 25 | } 26 | 27 | return { 28 | type: "quasar-cli-v0.17", 29 | command: getYarnOrNPMCommand(), 30 | port: 8888, 31 | proxyPort: 8080, 32 | env: { ...process.env }, 33 | possibleArgsArrs, 34 | urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), 35 | dist: ".quasar" 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/detectors/quasar.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | module.exports = function() { 9 | // REQUIRED FILES 10 | if (!hasRequiredFiles(["package.json"])) return false; 11 | // REQUIRED DEPS 12 | if (!hasRequiredDeps(["@quasar/app"])) return false; 13 | 14 | /** everything below now assumes that we are within Quasar */ 15 | 16 | const possibleArgsArrs = scanScripts({ 17 | preferredScriptsArr: ["serve", "start", "run", "dev"] 18 | // NOTE: this is comented out as it was picking this up in cordova related scripts. 19 | // preferredCommand: "quasar dev" 20 | }); 21 | 22 | if (possibleArgsArrs.length === 0) { 23 | // ofer to run this default when the user doesnt have any matching scripts setup! 24 | possibleArgsArrs.push(["quasar", "dev"]); 25 | } 26 | 27 | return { 28 | type: "quasar-cli", 29 | command: getYarnOrNPMCommand(), 30 | port: 8888, 31 | proxyPort: 8080, 32 | env: { ...process.env }, 33 | possibleArgsArrs, 34 | urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), 35 | dist: ".quasar" 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/detectors/react-static.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | module.exports = function() { 8 | // REQUIRED FILES 9 | if (!hasRequiredFiles(["package.json", "static.config.js"])) return false; 10 | // REQUIRED DEPS 11 | if (!hasRequiredDeps(["react-static"])) return false; 12 | 13 | /** everything below now assumes that we are within react-static */ 14 | 15 | const possibleArgsArrs = scanScripts({ 16 | preferredScriptsArr: ["start", "develop", "dev"], 17 | preferredCommand: "react-static start" 18 | }); 19 | 20 | if (possibleArgsArrs.length === 0) { 21 | // ofer to run it when the user doesnt have any scripts setup! 🤯 22 | possibleArgsArrs.push(["react-static", "start"]); 23 | } 24 | return { 25 | type: "react-static", 26 | command: getYarnOrNPMCommand(), 27 | port: 8888, 28 | proxyPort: 3000, 29 | env: { ...process.env }, 30 | possibleArgsArrs, 31 | urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), 32 | dist: "dist" 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/detectors/sapper.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | module.exports = function() { 9 | // REQUIRED FILES 10 | if (!hasRequiredFiles(["package.json"])) return false; 11 | // REQUIRED DEPS 12 | if (!hasRequiredDeps(["sapper"])) return false; 13 | 14 | /** everything below now assumes that we are within Sapper */ 15 | 16 | const possibleArgsArrs = scanScripts({ 17 | preferredScriptsArr: ["dev", "start"], 18 | preferredCommand: "sapper dev" 19 | }); 20 | 21 | if (possibleArgsArrs.length === 0) { 22 | // ofer to run it when the user doesnt have any scripts setup! 🤯 23 | possibleArgsArrs.push(["sapper", "dev"]); 24 | } 25 | 26 | return { 27 | type: "sapper", 28 | command: getYarnOrNPMCommand(), 29 | port: 8888, 30 | proxyPort: 3000, 31 | env: { ...process.env }, 32 | possibleArgsArrs, 33 | urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), 34 | dist: "static" 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/detectors/stencil.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | /** 9 | * detection logic - artificial intelligence! 10 | * */ 11 | module.exports = function() { 12 | // REQUIRED FILES 13 | if (!hasRequiredFiles(["package.json", "stencil.config.ts"])) return false; 14 | // REQUIRED DEPS 15 | if (!hasRequiredDeps(["@stencil/core"])) return false; 16 | 17 | /** everything below now assumes that we are within stencil */ 18 | 19 | const possibleArgsArrs = scanScripts({ 20 | preferredScriptsArr: ["start"], 21 | preferredCommand: "stencil build --dev --watch --serve" 22 | }); 23 | 24 | return { 25 | type: "stencil", 26 | command: getYarnOrNPMCommand(), 27 | port: 8888, // the port that the Netlify Dev User will use 28 | proxyPort: 3333, // the port that stencil normally outputs 29 | env: { ...process.env, BROWSER: "none", PORT: 3000 }, 30 | possibleArgsArrs, 31 | urlRegexp: new RegExp(`(http://)([^:]+:)${3000}(/)?`, "g"), 32 | dist: "www" 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/detectors/svelte.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | module.exports = function() { 9 | // REQUIRED FILES 10 | if (!hasRequiredFiles(["package.json"])) return false; 11 | // REQUIRED DEPS 12 | if (!hasRequiredDeps(["svelte"])) return false; 13 | // HAS DETECTOR, IT WILL BE PICKED UP BY SAPPER DETECTOR, avoid duplication https://github.com/netlify/cli/issues/347 14 | if (hasRequiredDeps(["sapper"])) return false; 15 | 16 | /** everything below now assumes that we are within svelte */ 17 | 18 | const possibleArgsArrs = scanScripts({ 19 | preferredScriptsArr: ["dev", "start", "run"], 20 | preferredCommand: "npm run dev" 21 | }); 22 | 23 | if (possibleArgsArrs.length === 0) { 24 | // ofer to run it when the user doesnt have any scripts setup! 🤯 25 | possibleArgsArrs.push(["npm", "dev"]); 26 | } 27 | 28 | return { 29 | type: "svelte", 30 | command: getYarnOrNPMCommand(), 31 | port: 8888, 32 | proxyPort: 5000, 33 | env: { ...process.env }, 34 | possibleArgsArrs, 35 | urlRegexp: new RegExp(`(http://)([^:]+:)${5000}(/)?`, "g"), 36 | dist: "public" 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /src/detectors/utils/jsdetect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * responsible for any js based projects 3 | * and can therefore build in assumptions that only js projects have 4 | * 5 | */ 6 | const { existsSync, readFileSync } = require("fs"); 7 | let pkgJSON = null; 8 | let yarnExists = false; 9 | let warnedAboutEmptyScript = false; 10 | const { NETLIFYDEVWARN } = require("netlify-cli-logo"); 11 | 12 | /** hold package.json in a singleton so we dont do expensive parsing repeatedly */ 13 | function getPkgJSON() { 14 | if (pkgJSON) { 15 | return pkgJSON; 16 | } 17 | if (!existsSync("package.json")) 18 | throw new Error( 19 | "dont call this method unless you already checked for pkg json" 20 | ); 21 | pkgJSON = JSON.parse(readFileSync("package.json", { encoding: "utf8" })); 22 | return pkgJSON; 23 | } 24 | function getYarnOrNPMCommand() { 25 | if (!yarnExists) { 26 | yarnExists = existsSync("yarn.lock") ? "yes" : "no"; 27 | } 28 | return yarnExists === "yes" ? "yarn" : "npm"; 29 | } 30 | 31 | /** 32 | * real utiltiies are down here 33 | * 34 | */ 35 | 36 | function hasRequiredDeps(requiredDepArray) { 37 | const { dependencies, devDependencies } = getPkgJSON(); 38 | for (let depName of requiredDepArray) { 39 | const hasItInDeps = dependencies && dependencies[depName]; 40 | const hasItInDevDeps = devDependencies && devDependencies[depName]; 41 | if (!hasItInDeps && !hasItInDevDeps) { 42 | return false; 43 | } 44 | } 45 | return true; 46 | } 47 | function hasRequiredFiles(filenameArr) { 48 | for (const filename of filenameArr) { 49 | if (!existsSync(filename)) { 50 | return false; 51 | } 52 | } 53 | return true; 54 | } 55 | 56 | // preferredScriptsArr is in decreasing order of preference 57 | function scanScripts({ preferredScriptsArr, preferredCommand }) { 58 | const { scripts } = getPkgJSON(); 59 | 60 | if (!scripts && !warnedAboutEmptyScript) { 61 | // eslint-disable-next-line no-console 62 | console.log( 63 | `${NETLIFYDEVWARN} You have a package.json without any npm scripts.` 64 | ); 65 | // eslint-disable-next-line no-console 66 | console.log( 67 | `${NETLIFYDEVWARN} Netlify Dev's detector system works best with a script, or you can specify a command to run in the netlify.toml [dev] block ` 68 | ); 69 | warnedAboutEmptyScript = true; // dont spam message with every detector 70 | return []; // not going to match any scripts anyway 71 | } 72 | /** 73 | * 74 | * NOTE: we return an array of arrays (args) 75 | * because we may want to supply extra args in some setups 76 | * 77 | * e.g. ['eleventy', '--serve', '--watch'] 78 | * 79 | * array will in future be sorted by likelihood of what we want 80 | * 81 | * */ 82 | // this is very simplistic logic, we can offer far more intelligent logic later 83 | // eg make a dependency tree of npm scripts and offer the parentest node first 84 | let possibleArgsArrs = preferredScriptsArr 85 | .filter(s => Object.keys(scripts).includes(s)) 86 | .filter(s => !scripts[s].includes("netlify dev")) // prevent netlify dev calling netlify dev 87 | .map(x => [x]); // make into arr of arrs 88 | 89 | Object.entries(scripts) 90 | .filter(([k]) => !preferredScriptsArr.includes(k)) 91 | .forEach(([k, v]) => { 92 | if (v.includes(preferredCommand)) possibleArgsArrs.push([k]); 93 | }); 94 | 95 | return possibleArgsArrs; 96 | } 97 | 98 | module.exports = { 99 | hasRequiredDeps, 100 | hasRequiredFiles, 101 | getYarnOrNPMCommand, 102 | scanScripts 103 | }; 104 | -------------------------------------------------------------------------------- /src/detectors/vue.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | module.exports = function() { 9 | // REQUIRED FILES 10 | if (!hasRequiredFiles(["package.json"])) return false; 11 | // REQUIRED DEPS 12 | if (!hasRequiredDeps(["@vue/cli-service"])) return false; 13 | 14 | /** everything below now assumes that we are within vue */ 15 | 16 | const possibleArgsArrs = scanScripts({ 17 | preferredScriptsArr: ["serve", "start", "run"], 18 | preferredCommand: "vue-cli-service serve" 19 | }); 20 | 21 | if (possibleArgsArrs.length === 0) { 22 | // ofer to run it when the user doesnt have any scripts setup! 🤯 23 | possibleArgsArrs.push(["vue-cli-service", "serve"]); 24 | } 25 | 26 | return { 27 | type: "vue-cli", 28 | command: getYarnOrNPMCommand(), 29 | port: 8888, 30 | proxyPort: 8080, 31 | env: { ...process.env }, 32 | possibleArgsArrs, 33 | urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), 34 | dist: "dist" 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/detectors/vuepress.js: -------------------------------------------------------------------------------- 1 | const { 2 | hasRequiredDeps, 3 | hasRequiredFiles, 4 | getYarnOrNPMCommand, 5 | scanScripts 6 | } = require("./utils/jsdetect"); 7 | 8 | module.exports = function() { 9 | // REQUIRED FILES 10 | if (!hasRequiredFiles(["package.json"])) return false; 11 | // REQUIRED DEPS 12 | if (!hasRequiredDeps(["vuepress"])) return false; 13 | 14 | /** everything below now assumes that we are within vue */ 15 | 16 | const possibleArgsArrs = scanScripts({ 17 | preferredScriptsArr: ["docs:dev", "dev", "run"], 18 | preferredCommand: "vuepress dev" 19 | }); 20 | 21 | if (possibleArgsArrs.length === 0) { 22 | // ofer to run it when the user doesnt have any scripts setup! 🤯 23 | possibleArgsArrs.push(["vuepress", "dev"]); 24 | } 25 | 26 | return { 27 | type: "vuepress", 28 | command: getYarnOrNPMCommand(), 29 | port: 8888, 30 | proxyPort: 8080, 31 | env: { ...process.env }, 32 | possibleArgsArrs, 33 | urlRegexp: new RegExp(`(http://)([^:]+:)${8080}(/)?`, "g"), 34 | dist: ".vuepress/dist" 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/function-builder-detectors/README.md: -------------------------------------------------------------------------------- 1 | ## function builder detectors 2 | 3 | similar to project detectors, each file here detects function builders. this is so that netlify dev never manages the webpack or other config. the expected output is very simple: 4 | 5 | ```js 6 | module.exports = { 7 | src: "functions-source", // source for your functions 8 | build: () => {}, // chokidar will call this to build and rebuild your function 9 | npmScript: "build:functions" // optional, the matching package.json script that calls your function builder 10 | } 11 | ``` 12 | 13 | example 14 | 15 | - [src](https://github.com/netlify/netlify-dev-plugin/blob/6a3992746ae490881105fbed2e11ca444f79e44e/src/function-builder-detectors/netlify-lambda.js#L29) 16 | - [npmScript](https://github.com/netlify/netlify-dev-plugin/blob/6a3992746ae490881105fbed2e11ca444f79e44e/src/function-builder-detectors/netlify-lambda.js#L30) 17 | -------------------------------------------------------------------------------- /src/function-builder-detectors/netlify-lambda.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readFileSync } = require("fs"); 2 | const execa = require("execa"); 3 | 4 | module.exports = function() { 5 | if (!existsSync("package.json")) { 6 | return false; 7 | } 8 | 9 | const packageSettings = JSON.parse( 10 | readFileSync("package.json", { encoding: "utf8" }) 11 | ); 12 | const { dependencies, devDependencies, scripts } = packageSettings; 13 | if ( 14 | !( 15 | (dependencies && dependencies["netlify-lambda"]) || 16 | (devDependencies && devDependencies["netlify-lambda"]) 17 | ) 18 | ) { 19 | return false; 20 | } 21 | 22 | const yarnExists = existsSync("yarn.lock"); 23 | const settings = {}; 24 | 25 | for (const key in scripts) { 26 | const script = scripts[key]; 27 | const match = script.match(/netlify-lambda build (\S+)/); 28 | if (match) { 29 | settings.src = match[1]; 30 | settings.npmScript = key; 31 | break; 32 | } 33 | } 34 | 35 | if (settings.npmScript) { 36 | settings.build = () => 37 | execa(yarnExists ? "yarn" : "npm", ["run", settings.npmScript]); 38 | settings.builderName = "netlify-lambda"; 39 | return settings; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/functions-templates/js/README.md: -------------------------------------------------------------------------------- 1 | ## note to devs 2 | 3 | place new templates here and our CLI will pick it up. each template must be in its own folder. 4 | 5 | ## not a long term solution 6 | 7 | we dont want people to update their CLI every time we add a template. see https://github.com/netlify/netlify-dev-plugin/issues/42 for how we may solve in future 8 | 9 | ## template lifecycles 10 | 11 | - onComplete 12 | - meant for messages, logging, light cleanup 13 | - onAllAddonsInstalled? 14 | - not implemented yet 15 | - meant for heavier work, but not sure if different from onComplete 16 | 17 | ## template addons 18 | 19 | specify an array of objects of this shape: 20 | 21 | ```ts 22 | { 23 | addonName: String, 24 | addonDidInstall?: Function // for executing arbitrary postinstall code for a SINGLE addon 25 | } 26 | ``` 27 | 28 | ## why place templates in a separate folder 29 | 30 | we dont colocate this inside `src/commands/functions` because oclif will think it's a new command. 31 | 32 | every function should be registered with their respective `template-registry.js`. 33 | 34 | ## typescript and go 35 | 36 | we have some templates here but they are unused for now until Netlify Dev supports them. 37 | -------------------------------------------------------------------------------- /src/functions-templates/js/apollo-graphql-rest/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "apollo-graphql-rest", 3 | description: 4 | "GraphQL function to wrap REST API using apollo-server-lambda and apollo-datasource-rest!" 5 | }; 6 | -------------------------------------------------------------------------------- /src/functions-templates/js/apollo-graphql-rest/apollo-graphql-rest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { ApolloServer, gql } = require("apollo-server-lambda"); 3 | const RandomUser = require("./random-user.js"); 4 | // example from: https://medium.com/yld-engineering-blog/easier-graphql-wrappers-for-your-rest-apis-1410b0b5446d 5 | 6 | const typeDefs = gql` 7 | """ 8 | Example Description for Name Type 9 | 10 | It's multiline and you can use **markdown**! [more docs](https://www.apollographql.com/docs/apollo-server/essentials/schema#documentation)! 11 | """ 12 | type Name { 13 | "Description for first" 14 | title: String 15 | "Description for title" 16 | first: String 17 | "Description for last" 18 | last: String 19 | } 20 | type Location { 21 | street: String 22 | city: String 23 | state: String 24 | postcode: String 25 | } 26 | type Picture { 27 | large: String 28 | medium: String 29 | thumbnail: String 30 | } 31 | type User { 32 | gender: String 33 | name: Name 34 | location: Location 35 | email: String 36 | phone: String 37 | cell: String 38 | picture: Picture 39 | nat: String 40 | } 41 | type Query { 42 | """ 43 | Example Description for getUser 44 | 45 | It's multiline and you can use **markdown**! 46 | """ 47 | getUser(gender: String): User 48 | getUsers(people: Int, gender: String): [User] 49 | } 50 | `; 51 | const resolvers = { 52 | Query: { 53 | getUser: async (_, { gender }, { dataSources }) => 54 | dataSources.RandomUser.getUser(gender), 55 | getUsers: async (_, { people, gender }, { dataSources }) => 56 | dataSources.RandomUser.getUsers(people, gender) 57 | } 58 | }; 59 | 60 | const server = new ApolloServer({ 61 | typeDefs, 62 | resolvers, 63 | dataSources: () => ({ 64 | RandomUser: new RandomUser() 65 | }) 66 | }); 67 | 68 | exports.handler = server.createHandler(); 69 | -------------------------------------------------------------------------------- /src/functions-templates/js/apollo-graphql-rest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-graphql-rest", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - GraphQL function to wrap REST API using apollo-server-lambda and apollo-datasource-rest!", 5 | "main": "apollo-graphql-rest.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js", 13 | "apollo" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "apollo-server-lambda": "^2.4.8", 19 | "apollo-datasource-rest": "^0.3.2", 20 | "graphql": "^14.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/functions-templates/js/apollo-graphql-rest/random-user.js: -------------------------------------------------------------------------------- 1 | const { RESTDataSource } = require("apollo-datasource-rest"); 2 | 3 | class RandomUser extends RESTDataSource { 4 | constructor() { 5 | super(); 6 | this.baseURL = "https://randomuser.me/api"; 7 | } 8 | 9 | async getUser(gender = "all") { 10 | const user = await this.get(`/?gender=${gender}`); 11 | return user.results[0]; 12 | } 13 | 14 | async getUsers(people = 10, gender = "all") { 15 | const user = await this.get(`/?results=${people}&gender=${gender}`); 16 | return user.results; 17 | } 18 | } 19 | 20 | module.exports = RandomUser; 21 | -------------------------------------------------------------------------------- /src/functions-templates/js/apollo-graphql/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "apollo-graphql", 3 | description: "GraphQL function using Apollo-Server-Lambda!" 4 | }; 5 | -------------------------------------------------------------------------------- /src/functions-templates/js/apollo-graphql/apollo-graphql.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer, gql } = require("apollo-server-lambda"); 2 | 3 | const typeDefs = gql` 4 | type Query { 5 | hello: String 6 | allAuthors: [Author!] 7 | author(id: Int!): Author 8 | authorByName(name: String!): Author 9 | } 10 | type Author { 11 | id: ID! 12 | name: String! 13 | married: Boolean! 14 | } 15 | `; 16 | 17 | const authors = [ 18 | { id: 1, name: "Terry Pratchett", married: false }, 19 | { id: 2, name: "Stephen King", married: true }, 20 | { id: 3, name: "JK Rowling", married: false } 21 | ]; 22 | 23 | const resolvers = { 24 | Query: { 25 | hello: (root, args, context) => { 26 | return "Hello, world!"; 27 | }, 28 | allAuthors: (root, args, context) => { 29 | return authors; 30 | }, 31 | author: (root, args, context) => { 32 | return; 33 | }, 34 | authorByName: (root, args, context) => { 35 | console.log("hihhihi", args.name); 36 | return authors.find(x => x.name === args.name) || "NOTFOUND"; 37 | } 38 | } 39 | }; 40 | 41 | const server = new ApolloServer({ 42 | typeDefs, 43 | resolvers 44 | }); 45 | 46 | exports.handler = server.createHandler(); 47 | -------------------------------------------------------------------------------- /src/functions-templates/js/apollo-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-graphql", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - set up for apollo graphql", 5 | "main": "apollo-graphql.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js", 13 | "apollo" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "apollo-server-lambda": "^2.4.8", 19 | "graphql": "^14.1.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/auth-fetch/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "auth-fetch", 3 | description: "Use `node-fetch` library and Netlify Identity to access APIs", 4 | onComplete() { 5 | console.log(`auth-fetch function created from template!`); 6 | console.log( 7 | "REMINDER: Make sure to call this function with the Netlify Identity JWT. See https://netlify-gotrue-in-react.netlify.com/ for demo" 8 | ); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/functions-templates/js/auth-fetch/auth-fetch.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // for a full working demo of Netlify Identity + Functions, see https://netlify-gotrue-in-react.netlify.com/ 3 | 4 | const fetch = require("node-fetch"); 5 | exports.handler = async function(event, context) { 6 | if (!context.clientContext && !context.clientContext.identity) { 7 | return { 8 | statusCode: 500, 9 | body: JSON.stringify({ 10 | msg: "No identity instance detected. Did you enable it?" 11 | }) // Could be a custom message or object i.e. JSON.stringify(err) 12 | }; 13 | } 14 | const { identity, user } = context.clientContext; 15 | try { 16 | const response = await fetch("https://api.chucknorris.io/jokes/random"); 17 | if (!response.ok) { 18 | // NOT res.status >= 200 && res.status < 300 19 | return { statusCode: response.status, body: response.statusText }; 20 | } 21 | const data = await response.json(); 22 | 23 | return { 24 | statusCode: 200, 25 | body: JSON.stringify({ identity, user, msg: data.value }) 26 | }; 27 | } catch (err) { 28 | console.log(err); // output to netlify function log 29 | return { 30 | statusCode: 500, 31 | body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err) 32 | }; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/functions-templates/js/auth-fetch/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-fetch", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "node-fetch": { 8 | "version": "2.3.0", 9 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", 10 | "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/functions-templates/js/auth-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-fetch", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - default template for auth fetch function", 5 | "main": "auth-fetch.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js", 13 | "identity", 14 | "authentication" 15 | ], 16 | "author": "Netlify", 17 | "license": "MIT", 18 | "dependencies": { 19 | "node-fetch": "^2.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/create-user/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "create-user", 3 | description: 4 | "Programmatically create a Netlify Identity user by invoking a function", 5 | onComplete() { 6 | console.log(`create-user function created from template!`); 7 | console.log( 8 | "REMINDER: Make sure to call this function with a Netlify Identity JWT. See https://netlify-gotrue-in-react.netlify.com/ for demo" 9 | ); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/functions-templates/js/create-user/create-user.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | 3 | exports.handler = async (event, context) => { 4 | if (event.httpMethod !== "POST") 5 | return { statusCode: 400, body: "Must POST to this function" }; 6 | 7 | // send account information along with the POST 8 | const { email, password, full_name } = JSON.parse(event.body); 9 | if (!email) return { statusCode: 400, body: "email missing" }; 10 | if (!password) return { statusCode: 400, body: "password missing" }; 11 | if (!full_name) return { statusCode: 400, body: "full_name missing" }; 12 | 13 | // identity.token is a short lived admin token which 14 | // is provided to all Netlify Functions to interact 15 | // with the Identity API 16 | const { identity } = context.clientContext; 17 | 18 | await fetch(`${identity.url}/admin/users`, { 19 | method: "POST", 20 | headers: { Authorization: `Bearer ${identity.token}` }, 21 | body: JSON.stringify({ 22 | email, 23 | password, 24 | confirm: true, 25 | user_metadata: { 26 | full_name 27 | } 28 | }) 29 | }); 30 | 31 | return { 32 | statusCode: 200, 33 | body: "success!" 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/functions-templates/js/create-user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-user", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - Programmatically create a Netlify Identity user by invoking a function", 5 | "main": "create-user.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js", 13 | "identity", 14 | "authentication" 15 | ], 16 | "author": "Netlify", 17 | "license": "MIT", 18 | "dependencies": { 19 | "node-fetch": "^2.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | module.exports = { 3 | name: "fauna-crud", 4 | description: "CRUD function using Fauna DB", 5 | addons: [ 6 | { 7 | addonName: "fauna", 8 | addonDidInstall(fnPath) { 9 | execa.sync(fnPath + "/create-schema.js", undefined, { 10 | env: process.env, 11 | stdio: "inherit" 12 | }); 13 | } 14 | } 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/create-schema.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* bootstrap database in your FaunaDB account - use with `netlify dev:exec ` */ 4 | const faunadb = require("faunadb"); 5 | 6 | const q = faunadb.query; 7 | 8 | function createFaunaDB() { 9 | if (!process.env.FAUNADB_SERVER_SECRET) { 10 | console.log("No FAUNADB_SERVER_SECRET in environment, skipping DB setup"); 11 | } 12 | console.log("Create the database!"); 13 | const client = new faunadb.Client({ 14 | secret: process.env.FAUNADB_SERVER_SECRET 15 | }); 16 | 17 | /* Based on your requirements, change the schema here */ 18 | return client 19 | .query(q.Create(q.Ref("classes"), { name: "items" })) 20 | .then(() => { 21 | console.log("Created items class"); 22 | return client.query( 23 | q.Create(q.Ref("indexes"), { 24 | name: "all_items", 25 | source: q.Ref("classes/items"), 26 | active: true 27 | }) 28 | ); 29 | }) 30 | 31 | .catch(e => { 32 | if ( 33 | e.requestResult.statusCode === 400 && 34 | e.message === "instance not unique" 35 | ) { 36 | console.log("DB already exists"); 37 | } 38 | throw e; 39 | }); 40 | } 41 | 42 | createFaunaDB(); 43 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/create.js: -------------------------------------------------------------------------------- 1 | const faunadb = require("faunadb"); 2 | 3 | /* configure faunaDB Client with our secret */ 4 | const q = faunadb.query; 5 | const client = new faunadb.Client({ 6 | secret: process.env.FAUNADB_SERVER_SECRET 7 | }); 8 | 9 | /* export our lambda function as named "handler" export */ 10 | exports.handler = async (event, context) => { 11 | /* parse the string body into a useable JS object */ 12 | const data = JSON.parse(event.body); 13 | console.log("Function `create` invoked", data); 14 | const item = { 15 | data: data 16 | }; 17 | /* construct the fauna query */ 18 | return client 19 | .query(q.Create(q.Ref("classes/items"), item)) 20 | .then(response => { 21 | console.log("success", response); 22 | /* Success! return the response with statusCode 200 */ 23 | return { 24 | statusCode: 200, 25 | body: JSON.stringify(response) 26 | }; 27 | }) 28 | .catch(error => { 29 | console.log("error", error); 30 | /* Error! return the error with statusCode 400 */ 31 | return { 32 | statusCode: 400, 33 | body: JSON.stringify(error) 34 | }; 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/delete.js: -------------------------------------------------------------------------------- 1 | /* Import faunaDB sdk */ 2 | const faunadb = require("faunadb"); 3 | 4 | const q = faunadb.query; 5 | const client = new faunadb.Client({ 6 | secret: process.env.FAUNADB_SERVER_SECRET 7 | }); 8 | 9 | exports.handler = async (event, context) => { 10 | const id = event.id; 11 | console.log(`Function 'delete' invoked. delete id: ${id}`); 12 | return client 13 | .query(q.Delete(q.Ref(`classes/items/${id}`))) 14 | .then(response => { 15 | console.log("success", response); 16 | return { 17 | statusCode: 200, 18 | body: JSON.stringify(response) 19 | }; 20 | }) 21 | .catch(error => { 22 | console.log("error", error); 23 | return { 24 | statusCode: 400, 25 | body: JSON.stringify(error) 26 | }; 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/fauna-crud.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | exports.handler = async (event, context) => { 3 | const path = event.path.replace(/\.netlify\/functions\/[^\/]+/, ""); 4 | const segments = path.split("/").filter(e => e); 5 | 6 | switch (event.httpMethod) { 7 | case "GET": 8 | // e.g. GET /.netlify/functions/fauna-crud 9 | if (segments.length === 0) { 10 | return require("./read-all").handler(event, context); 11 | } 12 | // e.g. GET /.netlify/functions/fauna-crud/123456 13 | if (segments.length === 1) { 14 | event.id = segments[0]; 15 | return require("./read").handler(event, context); 16 | } else { 17 | return { 18 | statusCode: 500, 19 | body: 20 | "too many segments in GET request, must be either /.netlify/functions/fauna-crud or /.netlify/functions/fauna-crud/123456" 21 | }; 22 | } 23 | case "POST": 24 | // e.g. POST /.netlify/functions/fauna-crud with a body of key value pair objects, NOT strings 25 | return require("./create").handler(event, context); 26 | case "PUT": 27 | // e.g. PUT /.netlify/functions/fauna-crud/123456 with a body of key value pair objects, NOT strings 28 | if (segments.length === 1) { 29 | event.id = segments[0]; 30 | return require("./update").handler(event, context); 31 | } else { 32 | return { 33 | statusCode: 500, 34 | body: 35 | "invalid segments in POST request, must be /.netlify/functions/fauna-crud/123456" 36 | }; 37 | } 38 | case "DELETE": 39 | // e.g. DELETE /.netlify/functions/fauna-crud/123456 40 | if (segments.length === 1) { 41 | event.id = segments[0]; 42 | return require("./delete").handler(event, context); 43 | } else { 44 | return { 45 | statusCode: 500, 46 | body: 47 | "invalid segments in DELETE request, must be /.netlify/functions/fauna-crud/123456" 48 | }; 49 | } 50 | } 51 | return { 52 | statusCode: 500, 53 | body: "unrecognized HTTP Method, must be one of GET/POST/PUT/DELETE" 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fauna-crud", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - CRUD functionality with Fauna DB", 5 | "main": "fauna-crud.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js", 13 | "faunadb" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "faunadb": "^2.6.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/read-all.js: -------------------------------------------------------------------------------- 1 | /* Import faunaDB sdk */ 2 | const faunadb = require("faunadb"); 3 | 4 | const q = faunadb.query; 5 | const client = new faunadb.Client({ 6 | secret: process.env.FAUNADB_SERVER_SECRET 7 | }); 8 | 9 | exports.handler = async (event, context) => { 10 | console.log("Function `read-all` invoked"); 11 | return client 12 | .query(q.Paginate(q.Match(q.Ref("indexes/all_items")))) 13 | .then(response => { 14 | const itemRefs = response.data; 15 | // create new query out of item refs. http://bit.ly/2LG3MLg 16 | const getAllItemsDataQuery = itemRefs.map(ref => { 17 | return q.Get(ref); 18 | }); 19 | // then query the refs 20 | return client.query(getAllItemsDataQuery).then(ret => { 21 | return { 22 | statusCode: 200, 23 | body: JSON.stringify(ret) 24 | }; 25 | }); 26 | }) 27 | .catch(error => { 28 | console.log("error", error); 29 | return { 30 | statusCode: 400, 31 | body: JSON.stringify(error) 32 | }; 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/read.js: -------------------------------------------------------------------------------- 1 | /* Import faunaDB sdk */ 2 | const faunadb = require("faunadb"); 3 | 4 | const q = faunadb.query; 5 | const client = new faunadb.Client({ 6 | secret: process.env.FAUNADB_SERVER_SECRET 7 | }); 8 | 9 | exports.handler = async (event, context) => { 10 | const id = event.id; 11 | console.log(`Function 'read' invoked. Read id: ${id}`); 12 | return client 13 | .query(q.Get(q.Ref(`classes/items/${id}`))) 14 | .then(response => { 15 | console.log("success", response); 16 | return { 17 | statusCode: 200, 18 | body: JSON.stringify(response) 19 | }; 20 | }) 21 | .catch(error => { 22 | console.log("error", error); 23 | return { 24 | statusCode: 400, 25 | body: JSON.stringify(error) 26 | }; 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-crud/update.js: -------------------------------------------------------------------------------- 1 | /* Import faunaDB sdk */ 2 | const faunadb = require("faunadb"); 3 | 4 | const q = faunadb.query; 5 | const client = new faunadb.Client({ 6 | secret: process.env.FAUNADB_SERVER_SECRET 7 | }); 8 | 9 | exports.handler = async (event, context) => { 10 | const data = JSON.parse(event.body); 11 | const id = event.id; 12 | console.log(`Function 'update' invoked. update id: ${id}`); 13 | return client 14 | .query(q.Update(q.Ref(`classes/items/${id}`), { data })) 15 | .then(response => { 16 | console.log("success", response); 17 | return { 18 | statusCode: 200, 19 | body: JSON.stringify(response) 20 | }; 21 | }) 22 | .catch(error => { 23 | console.log("error", error); 24 | return { 25 | statusCode: 400, 26 | body: JSON.stringify(error) 27 | }; 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-graphql/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | module.exports = { 3 | name: "fauna-graphql", 4 | description: "GraphQL Backend using Fauna DB", 5 | addons: [ 6 | { 7 | addonName: "fauna", 8 | addonDidInstall(fnPath) { 9 | execa.sync(fnPath + "/sync-schema.js", undefined, { 10 | env: process.env, 11 | stdio: "inherit" 12 | }); 13 | } 14 | } 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-graphql/fauna-graphql.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer, gql } = require("apollo-server-lambda"); 2 | const { createHttpLink } = require("apollo-link-http"); 3 | const fetch = require("node-fetch"); 4 | const { 5 | introspectSchema, 6 | makeRemoteExecutableSchema 7 | } = require("graphql-tools"); 8 | 9 | exports.handler = async function(event, context) { 10 | /** required for Fauna GraphQL auth */ 11 | if (!process.env.FAUNADB_SERVER_SECRET) { 12 | const msg = ` 13 | FAUNADB_SERVER_SECRET missing. 14 | Did you forget to install the fauna addon or forgot to run inside Netlify Dev? 15 | `; 16 | console.error(msg); 17 | return { 18 | statusCode: 500, 19 | body: JSON.stringify({ msg }) 20 | }; 21 | } 22 | const b64encodedSecret = Buffer.from( 23 | process.env.FAUNADB_SERVER_SECRET + ":" // weird but they 24 | ).toString("base64"); 25 | const headers = { Authorization: `Basic ${b64encodedSecret}` }; 26 | 27 | /** standard creation of apollo-server executable schema */ 28 | const link = createHttpLink({ 29 | uri: "https://graphql.fauna.com/graphql", // modify as you see fit 30 | fetch, 31 | headers 32 | }); 33 | const schema = await introspectSchema(link); 34 | const executableSchema = makeRemoteExecutableSchema({ 35 | schema, 36 | link 37 | }); 38 | const server = new ApolloServer({ 39 | schema: executableSchema 40 | }); 41 | return new Promise((yay, nay) => { 42 | const cb = (err, args) => (err ? nay(err) : yay(args)); 43 | server.createHandler()(event, context, cb); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fauna-graphql", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - set up for fauna db + apollo graphql", 5 | "main": "fauna-graphql.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js", 13 | "apollo", 14 | "fauna" 15 | ], 16 | "author": "Netlify", 17 | "license": "MIT", 18 | "dependencies": { 19 | "apollo-link-http": "^1.5.14", 20 | "apollo-link-context": "^1.0.17", 21 | "apollo-server-lambda": "^2.4.8", 22 | "graphql": "^14.1.1", 23 | "graphql-tools": "^4.0.4", 24 | "node-fetch": "^2.3.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Todo { 2 | title: String! 3 | completed: Boolean! 4 | } 5 | type Query { 6 | allTodos: [Todo!] 7 | todosByCompletedFlag(completed: Boolean!): [Todo!] 8 | } 9 | -------------------------------------------------------------------------------- /src/functions-templates/js/fauna-graphql/sync-schema.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* sync GraphQL schema to your FaunaDB account - use with `netlify dev:exec ` */ 4 | function createFaunaGraphQL() { 5 | if (!process.env.FAUNADB_SERVER_SECRET) { 6 | console.log("No FAUNADB_SERVER_SECRET in environment, skipping DB setup"); 7 | } 8 | console.log("Upload GraphQL Schema!"); 9 | 10 | const fetch = require("node-fetch"); 11 | const fs = require("fs"); 12 | const path = require("path"); 13 | var dataString = fs 14 | .readFileSync(path.join(__dirname, "schema.graphql")) 15 | .toString(); // name of your schema file 16 | 17 | // encoded authorization header similar to https://www.npmjs.com/package/request#http-authentication 18 | const token = Buffer.from( 19 | process.env.FAUNADB_SERVER_SECRET + ":" 20 | ).toString("base64"); 21 | 22 | var options = { 23 | method: "POST", 24 | body: dataString, 25 | headers: { Authorization: `Basic ${token}` } 26 | }; 27 | 28 | fetch("https://graphql.fauna.com/import", options) 29 | // // uncomment for debugging 30 | .then(res => res.text()) 31 | .then(body => { 32 | console.log( 33 | "Netlify Functions:Create - `fauna-graphql/sync-schema.js` success!" 34 | ); 35 | console.log(body); 36 | }) 37 | .catch(err => console.error("something wrong happened: ", { err })); 38 | } 39 | 40 | createFaunaGraphQL(); 41 | -------------------------------------------------------------------------------- /src/functions-templates/js/google-analytics/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "google-analytics", 3 | description: "Google Analytics: proxy for GA on your domain to avoid adblock" 4 | }; 5 | -------------------------------------------------------------------------------- /src/functions-templates/js/google-analytics/google-analytics.js: -------------------------------------------------------------------------------- 1 | // with thanks to https://github.com/codeniko/simple-tracker/blob/master/examples/server-examples/aws-lambda/google-analytics.js 2 | const request = require("request"); 3 | const querystring = require("querystring"); 4 | const uuidv4 = require("uuid/v4"); 5 | 6 | const GA_ENDPOINT = `https://www.google-analytics.com/collect`; 7 | 8 | // Domains to whitelist. Replace with your own! 9 | const originWhitelist = []; // keep this empty and append domains to whitelist using whiteListDomain() 10 | whitelistDomain("test.com"); 11 | whitelistDomain("nfeld.com"); 12 | 13 | function whitelistDomain(domain, addWww = true) { 14 | const prefixes = ["https://", "http://"]; 15 | if (addWww) { 16 | prefixes.push("https://www."); 17 | prefixes.push("http://www."); 18 | } 19 | prefixes.forEach(prefix => originWhitelist.push(prefix + domain)); 20 | } 21 | 22 | function proxyToGoogleAnalytics(event, done) { 23 | // get GA params whether GET or POST request 24 | const params = 25 | event.httpMethod.toUpperCase() === "GET" 26 | ? event.queryStringParameters 27 | : JSON.parse(event.body); 28 | const headers = event.headers || {}; 29 | 30 | // attach other GA params, required for IP address since client doesn't have access to it. UA and CID can be sent from client 31 | params.uip = headers["x-forwarded-for"] || headers["x-bb-ip"] || ""; // ip override. Look into headers for clients IP address, as opposed to IP address of host running lambda function 32 | params.ua = params.ua || headers["user-agent"] || ""; // user agent override 33 | params.cid = params.cid || uuidv4(); // REQUIRED: use given cid, or generate a new one as last resort. Generating should be avoided because one user can show up in GA multiple times. If user refresh page `n` times, you'll get `n` pageviews logged into GA from "different" users. Client should generate a uuid and store in cookies, local storage, or generate a fingerprint. Check simple-tracker client example 34 | 35 | console.info("proxying params:", params); 36 | const qs = querystring.stringify(params); 37 | 38 | const reqOptions = { 39 | method: "POST", 40 | headers: { 41 | "Content-Type": "image/gif" 42 | }, 43 | url: GA_ENDPOINT, 44 | body: qs 45 | }; 46 | 47 | request(reqOptions, (error, result) => { 48 | if (error) { 49 | console.info("googleanalytics error!", error); 50 | } else { 51 | console.info( 52 | "googleanalytics status code", 53 | result.statusCode, 54 | result.statusMessage 55 | ); 56 | } 57 | }); 58 | 59 | done(); 60 | } 61 | 62 | exports.handler = function(event, context, callback) { 63 | const origin = event.headers["origin"] || event.headers["Origin"] || ""; 64 | console.log(`Received ${event.httpMethod} request from, origin: ${origin}`); 65 | 66 | const isOriginWhitelisted = originWhitelist.indexOf(origin) >= 0; 67 | console.info("is whitelisted?", isOriginWhitelisted); 68 | 69 | const headers = { 70 | //'Access-Control-Allow-Origin': '*', // allow all domains to POST. Use for localhost development only 71 | "Access-Control-Allow-Origin": isOriginWhitelisted 72 | ? origin 73 | : originWhitelist[0], 74 | "Access-Control-Allow-Methods": "GET,POST,OPTIONS", 75 | "Access-Control-Allow-Headers": "Content-Type,Accept" 76 | }; 77 | 78 | const done = () => { 79 | callback(null, { 80 | statusCode: 200, 81 | headers, 82 | body: "" 83 | }); 84 | }; 85 | 86 | const httpMethod = event.httpMethod.toUpperCase(); 87 | 88 | if (event.httpMethod === "OPTIONS") { 89 | // CORS (required if you use a different subdomain to host this function, or a different domain entirely) 90 | done(); 91 | } else if ( 92 | (httpMethod === "GET" || httpMethod === "POST") && 93 | isOriginWhitelisted 94 | ) { 95 | // allow GET or POST, but only for whitelisted domains 96 | proxyToGoogleAnalytics(event, done); 97 | } else { 98 | callback("Not found"); 99 | } 100 | }; 101 | 102 | /* 103 | Docs on GA endpoint and example params 104 | 105 | https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide 106 | 107 | v: 1 108 | _v: j67 109 | a: 751874410 110 | t: pageview 111 | _s: 1 112 | dl: https://nfeld.com/contact.html 113 | dr: https://google.com 114 | ul: en-us 115 | de: UTF-8 116 | dt: Nikolay Feldman - Software Engineer 117 | sd: 24-bit 118 | sr: 1440x900 119 | vp: 945x777 120 | je: 0 121 | _u: blabla~ 122 | jid: 123 | gjid: 124 | cid: 1837873423.1522911810 125 | tid: UA-116530991-1 126 | _gid: 1828045325.1524815793 127 | gtm: u4d 128 | z: 1379041260 129 | */ 130 | -------------------------------------------------------------------------------- /src/functions-templates/js/google-analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-analytics", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - Google Analytics: proxy for GA on your domain to avoid adblock", 5 | "main": "google-analytics.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "apis", 13 | "email", 14 | "js" 15 | ], 16 | "author": "Netlify", 17 | "license": "MIT", 18 | "dependencies": { 19 | "querystring": "^0.2.0", 20 | "request": "^2.88.0", 21 | "uuid": "^3.3.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/functions-templates/js/graphql-gateway/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "graphql-gateway", 3 | description: 4 | "Apollo Server Lambda Gateway stitching schemas from other GraphQL Functions!" 5 | }; 6 | -------------------------------------------------------------------------------- /src/functions-templates/js/graphql-gateway/example-sibling-function-graphql-1.js: -------------------------------------------------------------------------------- 1 | // not meant to be run inside the graqhql-gateway function 2 | // but just shows a copy-pastable example sibling function 3 | // that would work with graphql-gateway 4 | const { ApolloServer, gql } = require("apollo-server-lambda"); 5 | 6 | const typeDefs = gql` 7 | type Query { 8 | hello: String 9 | allAuthors: [Author!] 10 | author(id: Int!): Author 11 | authorByName(name: String!): Author 12 | } 13 | type Author { 14 | id: ID! 15 | name: String! 16 | age: Int! 17 | } 18 | `; 19 | 20 | const authors = [ 21 | { id: 1, name: "Terry Pratchett", age: 67 }, 22 | { id: 2, name: "Stephen King", age: 71 }, 23 | { id: 3, name: "JK Rowling", age: 53 } 24 | ]; 25 | 26 | const resolvers = { 27 | Query: { 28 | hello: (root, args, context) => { 29 | return "Hello, world!"; 30 | }, 31 | allAuthors: (root, args, context) => { 32 | return authors; 33 | }, 34 | author: (root, args, context) => { 35 | return; 36 | }, 37 | authorByName: (root, args, context) => { 38 | return authors.find(x => x.name === args.name) || "NOTFOUND"; 39 | } 40 | } 41 | }; 42 | 43 | const server = new ApolloServer({ 44 | typeDefs, 45 | resolvers 46 | }); 47 | 48 | exports.handler = server.createHandler(); 49 | -------------------------------------------------------------------------------- /src/functions-templates/js/graphql-gateway/example-sibling-function-graphql-2.js: -------------------------------------------------------------------------------- 1 | // not meant to be run inside the graqhql-gateway function 2 | // but just shows a copy-pastable example sibling function 3 | // that would work with graphql-gateway 4 | const { ApolloServer, gql } = require("apollo-server-lambda"); 5 | 6 | const typeDefs = gql` 7 | type Query { 8 | hello: String 9 | allBooks: [Book] 10 | book(id: Int!): Book 11 | } 12 | type Book { 13 | id: ID! 14 | year: Int! 15 | title: String! 16 | authorName: String! 17 | } 18 | `; 19 | 20 | const books = [ 21 | { 22 | id: 1, 23 | title: "The Philosopher's Stone", 24 | year: 1997, 25 | authorName: "JK Rowling" 26 | }, 27 | { 28 | id: 2, 29 | title: "Pet Sematary", 30 | year: 1983, 31 | authorName: "Stephen King" 32 | }, 33 | { 34 | id: 3, 35 | title: "Going Postal", 36 | year: 2004, 37 | authorName: "Terry Pratchett" 38 | }, 39 | { 40 | id: 4, 41 | title: "Small Gods", 42 | year: 1992, 43 | authorName: "Terry Pratchett" 44 | }, 45 | { 46 | id: 5, 47 | title: "Night Watch", 48 | year: 2002, 49 | authorName: "Terry Pratchett" 50 | }, 51 | { 52 | id: 6, 53 | title: "The Shining", 54 | year: 1977, 55 | authorName: "Stephen King" 56 | }, 57 | { 58 | id: 7, 59 | title: "The Deathly Hallows", 60 | year: 2007, 61 | authorName: "JK Rowling" 62 | } 63 | ]; 64 | 65 | const resolvers = { 66 | Query: { 67 | hello: (root, args, context) => { 68 | return "Hello, world!"; 69 | }, 70 | allBooks: (root, args, context) => { 71 | return books; 72 | }, 73 | book: (root, args, context) => { 74 | return find(books, { id: args.id }); 75 | } 76 | } 77 | }; 78 | 79 | const server = new ApolloServer({ 80 | typeDefs, 81 | resolvers 82 | }); 83 | 84 | exports.handler = server.createHandler(); 85 | -------------------------------------------------------------------------------- /src/functions-templates/js/graphql-gateway/graphql-gateway.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This code assumes you have other graphql Netlify functions 3 | * and shows you how to stitch them together in a "gateway". 4 | * 5 | * Of course, feel free to modify this gateway to suit your needs. 6 | */ 7 | 8 | const { 9 | introspectSchema, 10 | makeRemoteExecutableSchema, 11 | mergeSchemas 12 | } = require("graphql-tools"); 13 | const { createHttpLink } = require("apollo-link-http"); 14 | const fetch = require("node-fetch"); 15 | const { ApolloServer, gql } = require("apollo-server-lambda"); 16 | 17 | exports.handler = async function(event, context) { 18 | const schema1 = await getSchema("graphql-1"); // other Netlify functions which are graphql lambdas 19 | const schema2 = await getSchema("graphql-2"); // other Netlify functions which are graphql lambdas 20 | const schemas = [schema1, schema2]; 21 | 22 | /** 23 | * resolving -between- schemas 24 | * https://www.apollographql.com/docs/graphql-tools/schema-stitching#adding-resolvers 25 | */ 26 | const linkTypeDefs = ` 27 | extend type Book { 28 | author: Author 29 | } 30 | `; 31 | schemas.push(linkTypeDefs); 32 | const resolvers = { 33 | Book: { 34 | author: { 35 | fragment: `... on Book { authorName }`, 36 | resolve(book, args, context, info) { 37 | return info.mergeInfo.delegateToSchema({ 38 | schema: schema1, 39 | operation: "query", 40 | fieldName: "authorByName", // reuse what's implemented in schema1 41 | args: { 42 | name: book.authorName 43 | }, 44 | context, 45 | info 46 | }); 47 | } 48 | } 49 | } 50 | }; 51 | 52 | // more docs https://www.apollographql.com/docs/graphql-tools/schema-stitching#api 53 | const schema = mergeSchemas({ 54 | schemas, 55 | resolvers 56 | }); 57 | const server = new ApolloServer({ schema }); 58 | return new Promise((yay, nay) => { 59 | const cb = (err, args) => (err ? nay(err) : yay(args)); 60 | server.createHandler()(event, context, cb); 61 | }); 62 | }; 63 | 64 | async function getSchema(endpoint) { 65 | // you can't use relative URLs within Netlify Functions so need a base URL 66 | // process.env.URL is one of many build env variables: 67 | // https://www.netlify.com/docs/continuous-deployment/#build-environment-variables 68 | // Netlify Dev only supports URL and DEPLOY URL for now 69 | const uri = process.env.URL + "/.netlify/functions/" + endpoint; 70 | const link = createHttpLink({ uri, fetch }); 71 | const schema = await introspectSchema(link); 72 | const executableSchema = makeRemoteExecutableSchema({ schema, link }); 73 | return executableSchema; 74 | } 75 | -------------------------------------------------------------------------------- /src/functions-templates/js/graphql-gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-gateway", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - Apollo Server Lambda Gateway stitching schemas from other GraphQL Functions!", 5 | "main": "graphql-gateway.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js", 13 | "apollo" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "apollo-link-http": "^1.5.14", 19 | "apollo-server-lambda": "^2.4.8", 20 | "graphql": "^14.2.1", 21 | "graphql-tools": "^4.0.4", 22 | "node-fetch": "^2.3.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/functions-templates/js/hasura-event-triggered/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "hasura-event-triggered", 3 | description: 4 | "Hasura Cleaning: process a Hasura event and fire off a GraphQL mutation with processed text data" 5 | }; 6 | -------------------------------------------------------------------------------- /src/functions-templates/js/hasura-event-triggered/hasura-event-triggered.js: -------------------------------------------------------------------------------- 1 | // with thanks to https://github.com/vnovick/netlify-function-example/blob/master/functions/bad-words.js 2 | const axios = require("axios"); 3 | const Filter = require("bad-words"); 4 | const filter = new Filter(); 5 | const hgeEndpoint = "https://live-coding-netlify.herokuapp.com"; 6 | 7 | const query = ` 8 | mutation verifiedp($id: uuid!, $title: String!, $content: String!) { 9 | update_posts(_set: { verified: true, content: $content, title: $title }, 10 | where:{ id: { _eq: $id } }) { 11 | returning { 12 | id 13 | } 14 | } 15 | } 16 | `; 17 | 18 | exports.handler = async (event, context) => { 19 | let request; 20 | try { 21 | request = JSON.parse(event.body); 22 | } catch (e) { 23 | return { statusCode: 400, body: "c annot parse hasura event" }; 24 | } 25 | 26 | const variables = { 27 | id: request.event.data.new.id, 28 | title: filter.clean(request.event.data.new.title), 29 | content: filter.clean(request.event.data.new.content) 30 | }; 31 | try { 32 | await axios.post(hgeEndpoint + "/v1alpha1/graphql", { query, variables }); 33 | return { statusCode: 200, body: "success" }; 34 | } catch (err) { 35 | return { statusCode: 500, body: err.toString() }; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/functions-templates/js/hasura-event-triggered/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hasura-event-triggered", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - Serverless function to process a Hasura event and fire off a GraphQL mutation with cleaned text data", 5 | "main": "hasura-event-triggered.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js", 13 | "hasura" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "axios": "^0.18.0", 19 | "bad-words": "^3.0.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/hello-world/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "hello-world", 3 | priority: 1, 4 | description: 5 | "Basic function that shows async/await usage, and response formatting" 6 | }; 7 | -------------------------------------------------------------------------------- /src/functions-templates/js/hello-world/hello-world.js: -------------------------------------------------------------------------------- 1 | // Docs on event and context https://www.netlify.com/docs/functions/#the-handler-method 2 | exports.handler = async (event, context) => { 3 | try { 4 | const subject = event.queryStringParameters.name || "World"; 5 | return { 6 | statusCode: 200, 7 | body: JSON.stringify({ message: `Hello ${subject}` }) 8 | // // more keys you can return: 9 | // headers: { "headerName": "headerValue", ... }, 10 | // isBase64Encoded: true, 11 | }; 12 | } catch (err) { 13 | return { statusCode: 500, body: err.toString() }; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/functions-templates/js/identity-signup/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "identity-signup", 3 | description: 4 | "Identity Signup: Triggered when a new Netlify Identity user confirms. Assigns roles and extra metadata" 5 | }; 6 | -------------------------------------------------------------------------------- /src/functions-templates/js/identity-signup/identity-signup.js: -------------------------------------------------------------------------------- 1 | // note - this function MUST be named `identity-signup` to work 2 | // we do not yet offer local emulation of this functionality in Netlify Dev 3 | // 4 | // more: 5 | // https://www.netlify.com/blog/2019/02/21/the-role-of-roles-and-how-to-set-them-in-netlify-identity/ 6 | // https://www.netlify.com/docs/functions/#identity-and-functions 7 | 8 | exports.handler = async function(event, context) { 9 | const data = JSON.parse(event.body); 10 | const { user } = data; 11 | 12 | const responseBody = { 13 | app_metadata: { 14 | roles: 15 | user.email.split("@")[1] === "trust-this-company.com" 16 | ? ["editor"] 17 | : ["visitor"], 18 | my_user_info: "this is some user info" 19 | }, 20 | user_metadata: { 21 | ...user.user_metadata, // append current user metadata 22 | custom_data_from_function: "hurray this is some extra metadata" 23 | } 24 | }; 25 | return { 26 | statusCode: 200, 27 | body: JSON.stringify(responseBody) 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/functions-templates/js/node-fetch/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "node-fetch", 3 | description: 4 | "Fetch function: uses node-fetch to hit an external API without CORS issues" 5 | }; 6 | -------------------------------------------------------------------------------- /src/functions-templates/js/node-fetch/node-fetch.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const fetch = require("node-fetch"); 3 | exports.handler = async function(event, context) { 4 | try { 5 | const response = await fetch("https://icanhazdadjoke.com", { 6 | headers: { Accept: "application/json" } 7 | }); 8 | if (!response.ok) { 9 | // NOT res.status >= 200 && res.status < 300 10 | return { statusCode: response.status, body: response.statusText }; 11 | } 12 | const data = await response.json(); 13 | 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify({ msg: data.joke }) 17 | }; 18 | } catch (err) { 19 | console.log(err); // output to netlify function log 20 | return { 21 | statusCode: 500, 22 | body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err) 23 | }; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/functions-templates/js/node-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-fetch", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - default template for node fetch function", 5 | "main": "node-fetch.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "node-fetch": "^2.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/functions-templates/js/oauth-passport/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "oauth-passport", 3 | description: 4 | "oauth-passport: template for Oauth workflow using Passport + Express.js" 5 | }; 6 | -------------------------------------------------------------------------------- /src/functions-templates/js/oauth-passport/oauth-passport.js: -------------------------------------------------------------------------------- 1 | // details: https://markus.oberlehner.net/blog/implementing-an-authentication-flow-with-passport-and-netlify-functions/ 2 | 3 | const bodyParser = require("body-parser"); 4 | const cookieParser = require("cookie-parser"); 5 | const express = require("express"); 6 | const passport = require("passport"); 7 | const serverless = require("serverless-http"); 8 | 9 | require("./utils/auth"); 10 | 11 | const { COOKIE_SECURE, ENDPOINT } = require("./utils/config"); 12 | 13 | const app = express(); 14 | 15 | app.use(bodyParser.urlencoded({ extended: true })); 16 | app.use(bodyParser.json()); 17 | app.use(cookieParser()); 18 | app.use(passport.initialize()); 19 | 20 | const handleCallback = () => (req, res) => { 21 | res 22 | .cookie("jwt", req.user.jwt, { httpOnly: true, COOKIE_SECURE }) 23 | .redirect("/"); 24 | }; 25 | 26 | app.get( 27 | `${ENDPOINT}/auth/github`, 28 | passport.authenticate("github", { session: false }) 29 | ); 30 | app.get( 31 | `${ENDPOINT}/auth/github/callback`, 32 | passport.authenticate("github", { failureRedirect: "/", session: false }), 33 | handleCallback() 34 | ); 35 | 36 | app.get( 37 | `${ENDPOINT}/auth/status`, 38 | passport.authenticate("jwt", { session: false }), 39 | (req, res) => res.json({ email: req.user.email }) 40 | ); 41 | 42 | module.exports.handler = serverless(app); 43 | -------------------------------------------------------------------------------- /src/functions-templates/js/oauth-passport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth-passport", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - template for Oauth workflow using Passport + Express.js", 5 | "main": "oauth-passport.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "cookie-parser": "^1.4.4", 18 | "express": "^4.17.1", 19 | "jsonwebtoken": "^8.5.1", 20 | "passport": "^0.4.0", 21 | "passport-github2": "^0.1.11", 22 | "passport-jwt": "^4.0.0", 23 | "serverless-http": "^2.0.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/functions-templates/js/oauth-passport/utils/auth.js: -------------------------------------------------------------------------------- 1 | const { sign } = require('jsonwebtoken') 2 | const { Strategy: GitHubStrategy } = require('passport-github2') 3 | const passport = require('passport') 4 | const passportJwt = require('passport-jwt') 5 | 6 | const { BASE_URL, ENDPOINT, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, SECRET } = require('./config') 7 | 8 | function authJwt(email) { 9 | return sign({ user: { email } }, SECRET) 10 | } 11 | 12 | passport.use( 13 | new GitHubStrategy( 14 | { 15 | clientID: GITHUB_CLIENT_ID, 16 | clientSecret: GITHUB_CLIENT_SECRET, 17 | callbackURL: `${BASE_URL}${ENDPOINT}/auth/github/callback`, 18 | scope: ['user:email'], 19 | }, 20 | async (accessToken, refreshToken, profile, done) => { 21 | try { 22 | const email = profile.emails[0].value 23 | // Here you'd typically create a new or load an existing user and 24 | // store the bare necessary informations about the user in the JWT. 25 | const jwt = authJwt(email) 26 | 27 | return done(null, { email, jwt }) 28 | } catch (error) { 29 | return done(error) 30 | } 31 | }, 32 | ), 33 | ) 34 | 35 | passport.use( 36 | new passportJwt.Strategy( 37 | { 38 | jwtFromRequest(req) { 39 | if (!req.cookies) throw new Error('Missing cookie-parser middleware') 40 | return req.cookies.jwt 41 | }, 42 | secretOrKey: SECRET, 43 | }, 44 | async ({ user: { email } }, done) => { 45 | try { 46 | // Here you'd typically load an existing user 47 | // and use the data to create the JWT. 48 | const jwt = authJwt(email) 49 | 50 | return done(null, { email, jwt }) 51 | } catch (error) { 52 | return done(error) 53 | } 54 | }, 55 | ), 56 | ) 57 | -------------------------------------------------------------------------------- /src/functions-templates/js/oauth-passport/utils/config.js: -------------------------------------------------------------------------------- 1 | // lambda/utils/config.js 2 | // Circumvent problem with Netlify CLI. 3 | // https://github.com/netlify/netlify-dev-plugin/issues/147 4 | exports.BASE_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:8888' : process.env.BASE_URL 5 | 6 | exports.COOKIE_SECURE = process.env.NODE_ENV !== 'development' 7 | 8 | exports.ENDPOINT = process.env.NODE_ENV === 'development' ? '/.netlify/functions' : '/api' 9 | 10 | exports.GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID 11 | exports.GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET 12 | 13 | exports.SECRET = process.env.SECRET || 'SUPERSECRET' 14 | -------------------------------------------------------------------------------- /src/functions-templates/js/protected-function/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "protected-function", 3 | description: "Function protected Netlify Identity authentication" 4 | }; 5 | -------------------------------------------------------------------------------- /src/functions-templates/js/protected-function/protected-function.js: -------------------------------------------------------------------------------- 1 | exports.handler = async (event, context) => { 2 | console.log("protected function!"); 3 | // Reading the context.clientContext will give us the current user 4 | const claims = context.clientContext && context.clientContext.user; 5 | console.log("user claims", claims); 6 | 7 | if (!claims) { 8 | console.log("No claims! Begone!"); 9 | return { 10 | statusCode: 401, 11 | body: JSON.stringify({ 12 | data: "NOT ALLOWED" 13 | }) 14 | }; 15 | } 16 | 17 | return { 18 | statusCode: 200, 19 | body: JSON.stringify({ 20 | data: "auth true" 21 | }) 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/functions-templates/js/send-email/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "send-email", 3 | description: "Send Email: Send email with no SMTP server via 'sendmail' pkg" 4 | }; 5 | -------------------------------------------------------------------------------- /src/functions-templates/js/send-email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "send-email", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - Send email with no SMTP server via 'sendmail' pkg", 5 | "main": "send-email.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "apis", 13 | "email", 14 | "js" 15 | ], 16 | "author": "Netlify", 17 | "license": "MIT", 18 | "dependencies": { 19 | "sendmail": "1.4.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/send-email/send-email.js: -------------------------------------------------------------------------------- 1 | // with thanks to https://github.com/Urigo/graphql-modules/blob/8cb2fd7d9938a856f83e4eee2081384533771904/website/lambda/contact.js 2 | const sendMail = require("sendmail")(); 3 | const { validateEmail, validateLength } = require("./validations"); 4 | 5 | exports.handler = (event, context, callback) => { 6 | if (!process.env.CONTACT_EMAIL) { 7 | return callback(null, { 8 | statusCode: 500, 9 | body: "process.env.CONTACT_EMAIL must be defined" 10 | }); 11 | } 12 | 13 | const body = JSON.parse(event.body); 14 | 15 | try { 16 | validateLength("body.name", body.name, 3, 50); 17 | } catch (e) { 18 | return callback(null, { 19 | statusCode: 403, 20 | body: e.message 21 | }); 22 | } 23 | 24 | try { 25 | validateEmail("body.email", body.email); 26 | } catch (e) { 27 | return callback(null, { 28 | statusCode: 403, 29 | body: e.message 30 | }); 31 | } 32 | 33 | try { 34 | validateLength("body.details", body.details, 10, 1000); 35 | } catch (e) { 36 | return callback(null, { 37 | statusCode: 403, 38 | body: e.message 39 | }); 40 | } 41 | 42 | const descriptor = { 43 | from: `"${body.email}" `, 44 | to: process.env.CONTACT_EMAIL, 45 | subject: `${body.name} sent you a message from gql-modules.com`, 46 | text: body.details 47 | }; 48 | 49 | sendMail(descriptor, e => { 50 | if (e) { 51 | callback(null, { 52 | statusCode: 500, 53 | body: e.message 54 | }); 55 | } else { 56 | callback(null, { 57 | statusCode: 200, 58 | body: "" 59 | }); 60 | } 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /src/functions-templates/js/send-email/validations.js: -------------------------------------------------------------------------------- 1 | exports.validateEmail = (ctx, str) => { 2 | if (typeof str !== "string" && !(str instanceof String)) { 3 | throw TypeError(`${ctx} must be a string`); 4 | } 5 | 6 | exports.validateLength(ctx, str, 5, 30); 7 | 8 | if (!/^[\w.-]+@[\w.-]+\.\w+$/.test(str)) { 9 | throw TypeError(`${ctx} is not an email address`); 10 | } 11 | }; 12 | 13 | exports.validateLength = (ctx, str, ...args) => { 14 | let min, max; 15 | 16 | if (args.length === 1) { 17 | min = 0; 18 | max = args[0]; 19 | } else { 20 | min = args[0]; 21 | max = args[1]; 22 | } 23 | 24 | if (typeof str !== "string" && !(str instanceof String)) { 25 | throw TypeError(`${ctx} must be a string`); 26 | } 27 | 28 | if (str.length < min) { 29 | throw TypeError(`${ctx} must be at least ${min} chars long`); 30 | } 31 | 32 | if (str.length > max) { 33 | throw TypeError(`${ctx} must contain ${max} chars at most`); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/functions-templates/js/serverless-ssr/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "serverless-ssr", 3 | description: "Dynamic serverside rendering via functions" 4 | }; 5 | -------------------------------------------------------------------------------- /src/functions-templates/js/serverless-ssr/app/index.js: -------------------------------------------------------------------------------- 1 | /* Express App */ 2 | const express = require("express"); 3 | const cors = require("cors"); 4 | const morgan = require("morgan"); 5 | const bodyParser = require("body-parser"); 6 | const compression = require("compression"); 7 | 8 | /* My express App */ 9 | module.exports = function expressApp(functionName) { 10 | const app = express(); 11 | const router = express.Router(); 12 | 13 | // gzip responses 14 | router.use(compression()); 15 | 16 | // Set router base path for local dev 17 | const routerBasePath = 18 | process.env.NODE_ENV === "dev" 19 | ? `/${functionName}` 20 | : `/.netlify/functions/${functionName}/`; 21 | 22 | /* define routes */ 23 | router.get("/", (req, res) => { 24 | const html = ` 25 | 26 | 27 | 32 | 33 | 34 |

Express via '${functionName}' ⊂◉‿◉つ

35 | 36 |

I'm using Express running via a Netlify Function.

37 | 38 |

Choose a route:

39 | 40 |
41 | View /users route 42 |
43 | 44 |
45 | View /hello route 46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 | Go back to demo homepage 54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 | 62 | See the source code on github 63 | 64 |
65 | 66 | 67 | `; 68 | res.send(html); 69 | }); 70 | 71 | router.get("/users", (req, res) => { 72 | res.json({ 73 | users: [ 74 | { 75 | name: "steve" 76 | }, 77 | { 78 | name: "joe" 79 | } 80 | ] 81 | }); 82 | }); 83 | 84 | router.get("/hello/", function(req, res) { 85 | res.send("hello world"); 86 | }); 87 | 88 | // Attach logger 89 | app.use(morgan(customLogger)); 90 | 91 | // Setup routes 92 | app.use(routerBasePath, router); 93 | 94 | // Apply express middlewares 95 | router.use(cors()); 96 | router.use(bodyParser.json()); 97 | router.use(bodyParser.urlencoded({ extended: true })); 98 | 99 | return app; 100 | }; 101 | 102 | function customLogger(tokens, req, res) { 103 | const log = [ 104 | tokens.method(req, res), 105 | tokens.url(req, res), 106 | tokens.status(req, res), 107 | tokens.res(req, res, "content-length"), 108 | "-", 109 | tokens["response-time"](req, res), 110 | "ms" 111 | ].join(" "); 112 | 113 | if (process.env.NODE_ENV !== "dev") { 114 | // Log only in AWS context to get back function logs 115 | console.log(log); 116 | } 117 | return log; 118 | } 119 | -------------------------------------------------------------------------------- /src/functions-templates/js/serverless-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-ssr", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - default template for a serverless SSR function", 5 | "main": "serverless-ssr.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "body-parser": "^1.18.3", 18 | "compression": "^1.7.4", 19 | "cors": "^2.8.5", 20 | "express": "^4.16.4", 21 | "morgan": "^1.9.1", 22 | "node-fetch": "^2.3.0", 23 | "serverless-http": "^1.9.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/functions-templates/js/serverless-ssr/serverless-http.js: -------------------------------------------------------------------------------- 1 | // for a full working demo check https://express-via-functions.netlify.com/.netlify/functions/serverless-http 2 | const serverless = require("serverless-http"); 3 | const expressApp = require("./app"); 4 | 5 | // We need to define our function name for express routes to set the correct base path 6 | const functionName = "serverless-http"; 7 | 8 | // Initialize express app 9 | const app = expressApp(functionName); 10 | 11 | // Export lambda handler 12 | exports.handler = serverless(app); 13 | -------------------------------------------------------------------------------- /src/functions-templates/js/serverless-ssr/serverless-ssr.js: -------------------------------------------------------------------------------- 1 | // for a full working demo check https://express-via-functions.netlify.com/.netlify/functions/serverless-http 2 | const serverless = require("serverless-http"); 3 | const expressApp = require("./app"); 4 | 5 | // We need to define our function name for express routes to set the correct base path 6 | const functionName = "serverless-http"; 7 | 8 | // Initialize express app 9 | const app = expressApp(functionName); 10 | 11 | // Export lambda handler 12 | exports.handler = serverless(app); 13 | -------------------------------------------------------------------------------- /src/functions-templates/js/set-cookie/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "set-cookie", 3 | description: "Set a cookie alongside your function" 4 | }; 5 | -------------------------------------------------------------------------------- /src/functions-templates/js/set-cookie/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "set-cookie", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - set a cookie with your Netlify Function", 5 | "main": "set-cookie", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "cookie": "^0.3.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/functions-templates/js/set-cookie/set-cookie.js: -------------------------------------------------------------------------------- 1 | const cookie = require("cookie"); 2 | 3 | exports.handler = async (event, context) => { 4 | var hour = 3600000; 5 | var twoWeeks = 14 * 24 * hour; 6 | const myCookie = cookie.serialize("my_cookie", "lolHi", { 7 | secure: true, 8 | httpOnly: true, 9 | path: "/", 10 | maxAge: twoWeeks 11 | }); 12 | 13 | const redirectUrl = "https://google.com"; 14 | // Do redirects via html 15 | const html = ` 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 30 | `; 31 | 32 | return { 33 | statusCode: 200, 34 | headers: { 35 | "Set-Cookie": myCookie, 36 | "Cache-Control": "no-cache", 37 | "Content-Type": "text/html" 38 | }, 39 | body: html 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /src/functions-templates/js/slack-rate-limit/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "slack-rate-limit", 3 | description: 4 | "Slack Rate-limit: post to Slack, at most once an hour, using Neltify Identity metadata" 5 | }; 6 | -------------------------------------------------------------------------------- /src/functions-templates/js/slack-rate-limit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-rate-limit", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - post to Slack, at most once an hour, using Neltify Identity metadata", 5 | "main": "node-fetch.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "slack", 13 | "js" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "node-fetch": "^2.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/functions-templates/js/slack-rate-limit/slack-rate-limit.js: -------------------------------------------------------------------------------- 1 | // code walkthrough: https://www.netlify.com/blog/2018/03/29/jamstack-architecture-on-netlify-how-identity-and-functions-work-together/#updating-user-data-with-the-identity-api 2 | // demo repo: https://github.com/biilmann/testing-slack-tutorial/tree/v3-one-message-an-hour 3 | // note: requires SLACK_WEBHOOK_URL environment variable 4 | const slackURL = process.env.SLACK_WEBHOOK_URL; 5 | const fetch = require("node-fetch"); 6 | 7 | class IdentityAPI { 8 | constructor(apiURL, token) { 9 | this.apiURL = apiURL; 10 | this.token = token; 11 | } 12 | 13 | headers(headers = {}) { 14 | return { 15 | "Content-Type": "application/json", 16 | Authorization: `Bearer ${this.token}`, 17 | ...headers 18 | }; 19 | } 20 | 21 | parseJsonResponse(response) { 22 | return response.json().then(json => { 23 | if (!response.ok) { 24 | return Promise.reject({ status: response.status, json }); 25 | } 26 | 27 | return json; 28 | }); 29 | } 30 | 31 | request(path, options = {}) { 32 | const headers = this.headers(options.headers || {}); 33 | return fetch(this.apiURL + path, { ...options, headers }).then(response => { 34 | const contentType = response.headers.get("Content-Type"); 35 | if (contentType && contentType.match(/json/)) { 36 | return this.parseJsonResponse(response); 37 | } 38 | 39 | if (!response.ok) { 40 | return response.text().then(data => { 41 | return Promise.reject({ stauts: response.status, data }); 42 | }); 43 | } 44 | return response.text().then(data => { 45 | data; 46 | }); 47 | }); 48 | } 49 | } 50 | 51 | /* 52 | Fetch a user from GoTrue via id 53 | */ 54 | function fetchUser(identity, id) { 55 | const api = new IdentityAPI(identity.url, identity.token); 56 | return api.request(`/admin/users/${id}`); 57 | } 58 | 59 | /* 60 | Update the app_metadata of a user 61 | */ 62 | function updateUser(identity, user, app_metadata) { 63 | const api = new IdentityAPI(identity.url, identity.token); 64 | const new_app_metadata = { ...user.app_metadata, ...app_metadata }; 65 | 66 | return api.request(`/admin/users/${user.id}`, { 67 | method: "PUT", 68 | body: JSON.stringify({ app_metadata: new_app_metadata }) 69 | }); 70 | } 71 | 72 | const oneHour = 60 * 60 * 1000; 73 | export function handler(event, context, callback) { 74 | if (event.httpMethod !== "POST") { 75 | return callback(null, { 76 | statusCode: 410, 77 | body: "Unsupported Request Method" 78 | }); 79 | } 80 | 81 | const claims = context.clientContext && context.clientContext.user; 82 | if (!claims) { 83 | return callback(null, { 84 | statusCode: 401, 85 | body: "You must be signed in to call this function" 86 | }); 87 | } 88 | 89 | fetchUser(context.clientContext.identity, claims.sub).then(user => { 90 | const lastMessage = new Date( 91 | user.app_metadata.last_message_at || 0 92 | ).getTime(); 93 | const cutOff = new Date().getTime() - oneHour; 94 | if (lastMessage > cutOff) { 95 | return callback(null, { 96 | statusCode: 401, 97 | body: "Only one message an hour allowed" 98 | }); 99 | } 100 | 101 | try { 102 | const payload = JSON.parse(event.body); 103 | 104 | fetch(slackURL, { 105 | method: "POST", 106 | body: JSON.stringify({ 107 | text: payload.text, 108 | attachments: [{ text: `From ${user.email}` }] 109 | }) 110 | }) 111 | .then(() => 112 | updateUser(context.clientContext.identity, user, { 113 | last_message_at: new Date().getTime() 114 | }) 115 | ) 116 | .then(() => { 117 | callback(null, { statusCode: 204 }); 118 | }) 119 | .catch(err => { 120 | callback(null, { 121 | statusCode: 500, 122 | body: "Internal Server Error: " + e 123 | }); 124 | }); 125 | } catch (e) { 126 | callback(null, { statusCode: 500, body: "Internal Server Error: " + e }); 127 | } 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /src/functions-templates/js/stripe-charge/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | module.exports = { 4 | name: "stripe-charge", 5 | description: "Stripe Charge: Charge a user with Stripe", 6 | async onComplete() { 7 | console.log( 8 | `${chalk.yellow("stripe-charge")} function created from template!` 9 | ); 10 | if (!process.env.STRIPE_SECRET_KEY) { 11 | console.log( 12 | `note this function requires ${chalk.yellow( 13 | "STRIPE_SECRET_KEY" 14 | )} build environment variable set in your Netlify Site.` 15 | ); 16 | let siteData = { name: "YOURSITENAMEHERE" }; 17 | try { 18 | siteData = await this.netlify.api.getSite({ 19 | siteId: this.netlify.site.id 20 | }); 21 | } catch (e) { 22 | // silent error, not important 23 | } 24 | console.log( 25 | `Set it at: https://app.netlify.com/sites/${ 26 | siteData.name 27 | }/settings/deploys#environment-variables (must have CD setup)` 28 | ); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/functions-templates/js/stripe-charge/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-charge", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "lodash.isplainobject": { 8 | "version": "4.0.6", 9 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 10 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 11 | }, 12 | "qs": { 13 | "version": "6.7.0", 14 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 15 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 16 | }, 17 | "safe-buffer": { 18 | "version": "5.1.2", 19 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 20 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 21 | }, 22 | "stripe": { 23 | "version": "6.28.0", 24 | "resolved": "https://registry.npmjs.org/stripe/-/stripe-6.28.0.tgz", 25 | "integrity": "sha512-4taF37geIr9DqvWEm3G9VCz2iJSV/DFc3PcElCQdQK5GUMI/MOj6XE0oJRYMOAHz0Oq8pT+4yDQmkh3SDI3nQA==", 26 | "requires": { 27 | "lodash.isplainobject": "^4.0.6", 28 | "qs": "^6.6.0", 29 | "safe-buffer": "^5.1.1", 30 | "uuid": "^3.3.2" 31 | } 32 | }, 33 | "uuid": { 34 | "version": "3.3.2", 35 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 36 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/functions-templates/js/stripe-charge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-charge", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - Charge a user with Stripe", 5 | "main": "stripe-charge.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "apis", 13 | "stripe", 14 | "js" 15 | ], 16 | "author": "Netlify", 17 | "license": "MIT", 18 | "dependencies": { 19 | "stripe": "^6.28.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/stripe-charge/stripe-charge.js: -------------------------------------------------------------------------------- 1 | // with thanks https://github.com/alexmacarthur/netlify-lambda-function-example/blob/68a0cdc05e201d68fe80b0926b0af7ff88f15802/lambda-src/purchase.js 2 | 3 | const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); 4 | 5 | const statusCode = 200; 6 | const headers = { 7 | "Access-Control-Allow-Origin": "*", 8 | "Access-Control-Allow-Headers": "Content-Type" 9 | }; 10 | 11 | exports.handler = function(event, context, callback) { 12 | //-- We only care to do anything if this is our POST request. 13 | if (event.httpMethod !== "POST" || !event.body) { 14 | callback(null, { 15 | statusCode, 16 | headers, 17 | body: "" 18 | }); 19 | } 20 | 21 | //-- Parse the body contents into an object. 22 | const data = JSON.parse(event.body); 23 | 24 | //-- Make sure we have all required data. Otherwise, escape. 25 | if (!data.token || !data.amount || !data.idempotency_key) { 26 | console.error("Required information is missing."); 27 | 28 | callback(null, { 29 | statusCode, 30 | headers, 31 | body: JSON.stringify({ status: "missing-information" }) 32 | }); 33 | 34 | return; 35 | } 36 | 37 | stripe.charges.create( 38 | { 39 | currency: "usd", 40 | amount: data.amount, 41 | source: data.token.id, 42 | receipt_email: data.token.email, 43 | description: `charge for a widget` 44 | }, 45 | { 46 | idempotency_key: data.idempotency_key 47 | }, 48 | (err, charge) => { 49 | if (err !== null) { 50 | console.log(err); 51 | } 52 | 53 | let status = 54 | charge === null || charge.status !== "succeeded" 55 | ? "failed" 56 | : charge.status; 57 | 58 | callback(null, { 59 | statusCode, 60 | headers, 61 | body: JSON.stringify({ status }) 62 | }); 63 | } 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/functions-templates/js/stripe-subscription/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | module.exports = { 4 | name: "stripe-subscription", 5 | description: "Stripe subscription: Create a subscription with Stripe", 6 | async onComplete() { 7 | console.log( 8 | `${chalk.yellow("stripe-subscription")} function created from template!` 9 | ); 10 | if (!process.env.STRIPE_SECRET_KEY) { 11 | console.log( 12 | `note this function requires ${chalk.yellow( 13 | "STRIPE_SECRET_KEY" 14 | )} build environment variable set in your Netlify Site.` 15 | ); 16 | let siteData = { name: "YOURSITENAMEHERE" }; 17 | try { 18 | siteData = await this.netlify.api.getSite({ 19 | siteId: this.netlify.site.id 20 | }); 21 | } catch (e) { 22 | // silent error, not important 23 | } 24 | console.log( 25 | `Set it at: https://app.netlify.com/sites/${ 26 | siteData.name 27 | }/settings/deploys#environment-variables (must have CD setup)` 28 | ); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/functions-templates/js/stripe-subscription/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-charge", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "lodash.isplainobject": { 8 | "version": "4.0.6", 9 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 10 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 11 | }, 12 | "qs": { 13 | "version": "6.7.0", 14 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 15 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 16 | }, 17 | "safe-buffer": { 18 | "version": "5.1.2", 19 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 20 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 21 | }, 22 | "stripe": { 23 | "version": "6.28.0", 24 | "resolved": "https://registry.npmjs.org/stripe/-/stripe-6.28.0.tgz", 25 | "integrity": "sha512-4taF37geIr9DqvWEm3G9VCz2iJSV/DFc3PcElCQdQK5GUMI/MOj6XE0oJRYMOAHz0Oq8pT+4yDQmkh3SDI3nQA==", 26 | "requires": { 27 | "lodash.isplainobject": "^4.0.6", 28 | "qs": "^6.6.0", 29 | "safe-buffer": "^5.1.1", 30 | "uuid": "^3.3.2" 31 | } 32 | }, 33 | "uuid": { 34 | "version": "3.3.2", 35 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 36 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/functions-templates/js/stripe-subscription/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-subscription", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - Create a subscription with Stripe", 5 | "main": "stripe-subscription.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "apis", 13 | "stripe", 14 | "js" 15 | ], 16 | "author": "Netlify", 17 | "license": "MIT", 18 | "dependencies": { 19 | "stripe": "^6.28.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/stripe-subscription/stripe-subscription.js: -------------------------------------------------------------------------------- 1 | // with thanks https://github.com/LukeMwila/stripe-subscriptions-backend/blob/master/stripe-api/index.ts 2 | 3 | const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); 4 | 5 | const respond = (fulfillmentText: any): any => { 6 | return { 7 | statusCode: 200, 8 | body: JSON.stringify(fulfillmentText), 9 | headers: { 10 | "Access-Control-Allow-Credentials": true, 11 | "Access-Control-Allow-Origin": "*", 12 | "Content-Type": "application/json" 13 | } 14 | }; 15 | }; 16 | 17 | exports.handler = async function(event, context) { 18 | try { 19 | const incoming = JSON.parse(event.body); 20 | const { stripeToken, email, productPlan } = incoming; 21 | } catch (err) { 22 | console.error(`error with parsing function parameters: `, err); 23 | return { 24 | statusCode: 400, 25 | body: JSON.stringify(err) 26 | }; 27 | } 28 | try { 29 | const data = await createCustomerAndSubscribeToPlan( 30 | stripeToken, 31 | email, 32 | productPlan 33 | ); 34 | return respond(data); 35 | } catch (err) { 36 | return respond(err); 37 | } 38 | }; 39 | 40 | async function createCustomerAndSubscribeToPlan( 41 | stripeToken: string, 42 | email: string, 43 | productPlan: string 44 | ) { 45 | // create a customer 46 | const customer = await stripe.customers.create({ 47 | email: email, 48 | source: stripeToken 49 | }); 50 | // retrieve created customer id to add customer to subscription plan 51 | const customerId = customer.id; 52 | // create a subscription for the newly created customer 53 | const subscription = await stripe.subscriptions.create({ 54 | customer: customerId, 55 | items: [{ plan: productPlan }] 56 | }); 57 | return subscription; 58 | } 59 | -------------------------------------------------------------------------------- /src/functions-templates/js/submission-created/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'submission-created', 3 | description: 'submission-created: template for event triggered function when a new Netlify Form is submitted', 4 | } 5 | -------------------------------------------------------------------------------- /src/functions-templates/js/submission-created/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "submission-created", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - template for submission-created event triggered function", 5 | "main": "submission-created.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "node-fetch": "^2.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/functions-templates/js/submission-created/submission-created.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // // optionally configure local env vars 4 | // require('dotenv').config() 5 | 6 | // // details in https://css-tricks.com/using-netlify-forms-and-netlify-functions-to-build-an-email-sign-up-widget 7 | const fetch = require('node-fetch') 8 | const { EMAIL_TOKEN } = process.env 9 | exports.handler = async (event) => { 10 | const email = JSON.parse(event.body).payload.email 11 | console.log(`Recieved a submission: ${email}`) 12 | return fetch('https://api.buttondown.email/v1/subscribers', { 13 | method: 'POST', 14 | headers: { 15 | Authorization: `Token ${EMAIL_TOKEN}`, 16 | 'Content-Type': 'application/json', 17 | }, 18 | body: JSON.stringify({ email }), 19 | }) 20 | .then((response) => response.json()) 21 | .then((data) => { 22 | console.log(`Submitted to Buttondown:\n ${data}`) 23 | }) 24 | .catch((error) => ({ statusCode: 422, body: String(error) })) 25 | } 26 | -------------------------------------------------------------------------------- /src/functions-templates/js/token-hider/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | module.exports = { 4 | name: "token-hider", 5 | description: "Token Hider: access APIs without exposing your API keys", 6 | async onComplete() { 7 | console.log( 8 | `${chalk.yellow("token-hider")} function created from template!` 9 | ); 10 | if (!process.env.API_URL || !process.env.API_TOKEN) { 11 | console.log( 12 | `note this function requires ${chalk.yellow( 13 | "API_URL" 14 | )} and ${chalk.yellow( 15 | "API_TOKEN" 16 | )} build environment variables set in your Netlify Site.` 17 | ); 18 | 19 | let siteData = { name: "YOURSITENAMEHERE" }; 20 | try { 21 | siteData = await this.netlify.api.getSite({ 22 | siteId: this.netlify.site.id 23 | }); 24 | } catch (e) { 25 | // silent error, not important 26 | } 27 | console.log( 28 | `Set them at: https://app.netlify.com/sites/${ 29 | siteData.name 30 | }/settings/deploys#environment-variables (must have CD setup)` 31 | ); 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/functions-templates/js/token-hider/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "token-hider", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.18.0", 9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", 10 | "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", 11 | "requires": { 12 | "follow-redirects": "^1.3.0", 13 | "is-buffer": "^1.1.5" 14 | } 15 | }, 16 | "debug": { 17 | "version": "3.2.6", 18 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 19 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 20 | "requires": { 21 | "ms": "^2.1.1" 22 | } 23 | }, 24 | "follow-redirects": { 25 | "version": "1.7.0", 26 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", 27 | "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", 28 | "requires": { 29 | "debug": "^3.2.6" 30 | } 31 | }, 32 | "is-buffer": { 33 | "version": "1.1.6", 34 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 35 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 36 | }, 37 | "ms": { 38 | "version": "2.1.1", 39 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 40 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 41 | }, 42 | "qs": { 43 | "version": "6.7.0", 44 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 45 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/functions-templates/js/token-hider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "token-hider", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - how to hide API tokens from your users", 5 | "main": "token-hider.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "apis", 13 | "js" 14 | ], 15 | "author": "Netlify", 16 | "license": "MIT", 17 | "dependencies": { 18 | "axios": "^0.18.0", 19 | "qs": "^6.7.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/token-hider/token-hider.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const qs = require("qs"); 3 | 4 | exports.handler = function(event, context, callback) { 5 | // apply our function to the queryStringParameters and assign it to a variable 6 | const API_PARAMS = qs.stringify(event.queryStringParameters); 7 | // Get env var values defined in our Netlify site UI 8 | const { API_TOKEN, API_URL } = process.env; 9 | // In this example, the API Key needs to be passed in the params with a key of key. 10 | // We're assuming that the ApiParams var will contain the initial ? 11 | const URL = `${API_URL}?${API_PARAMS}&key=${API_TOKEN}`; 12 | 13 | // Let's log some stuff we already have. 14 | console.log("Injecting token to", API_URL); 15 | console.log("logging event.....", event); 16 | console.log("Constructed URL is ...", URL); 17 | 18 | // Here's a function we'll use to define how our response will look like when we call callback 19 | const pass = body => { 20 | callback(null, { 21 | statusCode: 200, 22 | body: JSON.stringify(body) 23 | }); 24 | }; 25 | 26 | // Perform the API call. 27 | const get = () => { 28 | axios 29 | .get(URL) 30 | .then(response => { 31 | console.log(response.data); 32 | pass(response.data); 33 | }) 34 | .catch(err => pass(err)); 35 | }; 36 | if (event.httpMethod == "GET") { 37 | get(); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/functions-templates/js/url-shortener/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | module.exports = { 4 | name: "url-shortener", 5 | description: "URL Shortener: simple URL shortener with Netlify Forms!", 6 | async onComplete() { 7 | console.log( 8 | `${chalk.yellow("url-shortener")} function created from template!` 9 | ); 10 | if (!process.env.ROUTES_FORM_ID || !process.env.API_AUTH) { 11 | console.log( 12 | `note this function requires ${chalk.yellow( 13 | "ROUTES_FORM_ID" 14 | )} and ${chalk.yellow( 15 | "API_AUTH" 16 | )} build environment variables set in your Netlify Site.` 17 | ); 18 | 19 | let siteData = { name: "YOURSITENAMEHERE" }; 20 | try { 21 | siteData = await this.netlify.api.getSite({ 22 | siteId: this.netlify.site.id 23 | }); 24 | } catch (e) { 25 | // silent error, not important 26 | } 27 | console.log( 28 | `Set them at: https://app.netlify.com/sites/${ 29 | siteData.name 30 | }/settings/deploys#environment-variables (must have CD setup)` 31 | ); 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/functions-templates/js/url-shortener/generate-route.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var request = require("request"); 4 | var Hashids = require("hashids"); 5 | 6 | export function handler(event, context, callback) { 7 | // Set the root URL according to the Netlify site we are within 8 | var rootURL = process.env.URL + "/"; 9 | 10 | // get the details of what we are creating 11 | var destination = event.queryStringParameters["to"]; 12 | 13 | // generate a unique short code (stupidly for now) 14 | var hash = new Hashids(); 15 | var number = Math.round(new Date().getTime() / 100); 16 | var code = hash.encode(number); 17 | 18 | // ensure that a protocol was provided 19 | if (destination.indexOf("://") == -1) { 20 | destination = "http://" + destination; 21 | } 22 | 23 | // prepare a payload to post 24 | var payload = { 25 | "form-name": "routes", 26 | destination: destination, 27 | code: code, 28 | expires: "" 29 | }; 30 | 31 | // post the new route to the Routes form 32 | request.post({ url: rootURL, formData: payload }, function( 33 | err, 34 | httpResponse, 35 | body 36 | ) { 37 | var msg; 38 | if (err) { 39 | msg = "Post to Routes stash failed: " + err; 40 | } else { 41 | msg = "Route registered. Site deploying to include it. " + rootURL + code; 42 | } 43 | console.log(msg); 44 | // tell the user what their shortcode will be 45 | return callback(null, { 46 | statusCode: 200, 47 | headers: { "Content-Type": "application/json" }, 48 | body: JSON.stringify({ url: rootURL + code }) 49 | }); 50 | }); 51 | 52 | // ENHANCEMENT: check for uniqueness of shortcode 53 | // ENHANCEMENT: let the user provide their own shortcode 54 | // ENHANCEMENT: dont' duplicate existing routes, return the current one 55 | // ENHANCEMENT: allow the user to specify how long the redirect should exist for 56 | } 57 | -------------------------------------------------------------------------------- /src/functions-templates/js/url-shortener/get-route.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var request = require("request"); 4 | 5 | export function handler(event, context, callback) { 6 | // which URL code are we trying to retrieve? 7 | var code = event.queryStringParameters["code"]; 8 | 9 | // where is the data? 10 | var url = 11 | "https://api.netlify.com/api/v1/forms/" + 12 | process.env.ROUTES_FORM_ID + 13 | "/submissions/?access_token=" + 14 | process.env.API_AUTH; 15 | 16 | request(url, function(err, response, body) { 17 | // look for this code in our stash 18 | if (!err && response.statusCode === 200) { 19 | var routes = JSON.parse(body); 20 | 21 | for (var item in routes) { 22 | // return the result when we find the match 23 | if (routes[item].data.code == code) { 24 | console.log( 25 | "We searched for " + 26 | code + 27 | " and we found " + 28 | routes[item].data.destination 29 | ); 30 | return callback(null, { 31 | statusCode: 200, 32 | headers: { "Content-Type": "application/json" }, 33 | body: JSON.stringify({ 34 | code: code, 35 | url: routes[item].data.destination 36 | }) 37 | }); 38 | } 39 | } 40 | } else { 41 | return callback(null, { 42 | statusCode: 200, 43 | body: err 44 | }); 45 | } 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/functions-templates/js/url-shortener/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-shortener", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "url-shortener.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "apis", 13 | "url", 14 | "js" 15 | ], 16 | "author": "Netlify", 17 | "license": "MIT", 18 | "dependencies": { 19 | "hashids": "^1.2.2", 20 | "request": "^2.88.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/functions-templates/js/url-shortener/url-shortener.js: -------------------------------------------------------------------------------- 1 | exports.handler = (event, context, callback) => { 2 | const path = event.path.replace(/\.netlify\/functions\/[^\/]+/, ""); 3 | const segments = path.split("/").filter(e => e); 4 | 5 | switch (event.httpMethod) { 6 | case "GET": 7 | // e.g. GET /.netlify/functions/url-shortener 8 | return require("./get-route").handler(event, context, callback); 9 | case "POST": 10 | // e.g. POST /.netlify/functions/url-shortener 11 | return require("./generate-route").handler(event, context, callback); 12 | case "PUT": 13 | // your code here 14 | case "DELETE": 15 | // your code here 16 | } 17 | return callback({ 18 | statusCode: 500, 19 | body: "unrecognized HTTP Method, must be one of GET/POST/PUT/DELETE" 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/functions-templates/js/using-middleware/.netlify-function-template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "using-middleware", 3 | description: "Using Middleware with middy" 4 | }; 5 | -------------------------------------------------------------------------------- /src/functions-templates/js/using-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "using-middleware", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - using middleware with your netlify function", 5 | "main": "using-middleware.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "js" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "middy": "^0.23.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/functions-templates/js/using-middleware/using-middleware.js: -------------------------------------------------------------------------------- 1 | const middy = require("middy"); 2 | const { 3 | jsonBodyParser, 4 | validator, 5 | httpErrorHandler, 6 | httpHeaderNormalizer 7 | } = require("middy/middlewares"); 8 | 9 | /* Normal lambda code */ 10 | const businessLogic = (event, context, callback) => { 11 | // event.body has already been turned into an object by `jsonBodyParser` middleware 12 | const { name } = event.body; 13 | return callback(null, { 14 | statusCode: 200, 15 | body: JSON.stringify({ 16 | result: "success", 17 | message: `Hi ${name} ⊂◉‿◉つ` 18 | }) 19 | }); 20 | }; 21 | 22 | /* Input & Output Schema */ 23 | const schema = { 24 | input: { 25 | type: "object", 26 | properties: { 27 | body: { 28 | type: "object", 29 | required: ["name"], 30 | properties: { 31 | name: { type: "string" } 32 | } 33 | } 34 | }, 35 | required: ["body"] 36 | }, 37 | output: { 38 | type: "object", 39 | properties: { 40 | body: { 41 | type: "string", 42 | required: ["result", "message"], 43 | properties: { 44 | result: { type: "string" }, 45 | message: { type: "string" } 46 | } 47 | } 48 | }, 49 | required: ["body"] 50 | } 51 | }; 52 | 53 | /* Export inputSchema & outputSchema for automatic documentation */ 54 | exports.schema = schema; 55 | 56 | exports.handler = middy(businessLogic) 57 | .use(httpHeaderNormalizer()) 58 | // parses the request body when it's a JSON and converts it to an object 59 | .use(jsonBodyParser()) 60 | // validates the input 61 | .use(validator({ inputSchema: schema.input })) 62 | // handles common http errors and returns proper responses 63 | .use(httpErrorHandler()); 64 | -------------------------------------------------------------------------------- /src/functions-templates/unused_go/hello-world/hello-world.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aws/aws-lambda-go/events" 5 | "github.com/aws/aws-lambda-go/lambda" 6 | ) 7 | 8 | func handler(request events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) { 9 | return &events.APIGatewayProxyResponse{ 10 | StatusCode: 200, 11 | Body: "Hello, World", 12 | }, nil 13 | } 14 | 15 | func main() { 16 | // Make the handler available for Remote Procedure Call by AWS Lambda 17 | lambda.Start(handler) 18 | } -------------------------------------------------------------------------------- /src/functions-templates/unused_ts/hello-world/hello-world.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context, Callback, APIGatewayEvent } from 'aws-lambda' 2 | 3 | interface HelloResponse { 4 | statusCode: number 5 | body: string 6 | } 7 | 8 | const handler: Handler = (event: APIGatewayEvent, context: Context, callback: Callback) => { 9 | const params = event.queryStringParameters 10 | const response: HelloResponse = { 11 | statusCode: 200, 12 | body: JSON.stringify({ 13 | msg: `Hello world ${Math.floor(Math.random() * 10)}`, 14 | params 15 | }) 16 | } 17 | 18 | callback(undefined, response) 19 | } 20 | 21 | export { handler } 22 | -------------------------------------------------------------------------------- /src/functions-templates/unused_ts/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - hello world in typescript", 5 | "main": "hello-world.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "typescript" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "node-fetch": "^2.3.0", 18 | "@types/node": "^10.12.12", 19 | "typescript": "^3.2.2", 20 | "@types/aws-lambda": "^8.10.15" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/functions-templates/unused_ts/node-fetch/node-fetch.ts: -------------------------------------------------------------------------------- 1 | // example of async handler using async-await 2 | // https://github.com/netlify/netlify-lambda/issues/43#issuecomment-444618311 3 | 4 | import fetch from 'node-fetch' 5 | import { Context } from 'aws-lambda' 6 | export async function handler(event: any, context: Context) { 7 | try { 8 | const response = await fetch('https://api.chucknorris.io/jokes/random') 9 | if (!response.ok) { 10 | // NOT res.status >= 200 && res.status < 300 11 | return { statusCode: response.status, body: response.statusText } 12 | } 13 | const data = await response.json() 14 | 15 | return { 16 | statusCode: 200, 17 | body: JSON.stringify({ msg: data.value }) 18 | } 19 | } catch (err) { 20 | console.log(err) // output to netlify function log 21 | return { 22 | statusCode: 500, 23 | body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/functions-templates/unused_ts/node-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-fetch", 3 | "version": "1.0.0", 4 | "description": "netlify functions:create - using node-fetch in typescript", 5 | "main": "node-fetch.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "netlify", 11 | "serverless", 12 | "typescript" 13 | ], 14 | "author": "Netlify", 15 | "license": "MIT", 16 | "dependencies": { 17 | "node-fetch": "^2.3.0", 18 | "@types/node-fetch": "^2.1.4", 19 | "@types/node": "^10.12.12", 20 | "typescript": "^3.2.2", 21 | "@types/aws-lambda": "^8.10.15" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/live-tunnel.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const fs = require("fs"); 3 | const os = require("os"); 4 | const path = require("path"); 5 | const execa = require("execa"); 6 | const chalk = require("chalk"); 7 | const { fetchLatest, updateAvailable } = require("gh-release-fetch"); 8 | const { 9 | NETLIFYDEVLOG, 10 | // NETLIFYDEVWARN, 11 | NETLIFYDEVERR 12 | } = require("netlify-cli-logo"); 13 | 14 | async function createTunnel(siteId, netlifyApiToken, log) { 15 | await installTunnelClient(log); 16 | 17 | if (!siteId) { 18 | // eslint-disable-next-line no-console 19 | console.error( 20 | `${NETLIFYDEVERR} Error: no siteId defined, did you forget to run ${chalk.yellow( 21 | "netlify init" 22 | )} or ${chalk.yellow("netlify link")}?` 23 | ); 24 | process.exit(1); 25 | } 26 | log(`${NETLIFYDEVLOG} Creating Live Tunnel for ` + siteId); 27 | const url = `https://api.netlify.com/api/v1/live_sessions?site_id=${siteId}`; 28 | 29 | const response = await fetch(url, { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: `Bearer ${netlifyApiToken}` 34 | }, 35 | body: JSON.stringify({}) 36 | }); 37 | 38 | const data = await response.json(); 39 | 40 | if (response.status !== 201) { 41 | throw new Error(data.message); 42 | } 43 | 44 | return data; 45 | } 46 | 47 | async function connectTunnel(session, netlifyApiToken, localPort, log) { 48 | const execPath = path.join( 49 | os.homedir(), 50 | ".netlify", 51 | "tunnel", 52 | "bin", 53 | "live-tunnel-client" 54 | ); 55 | const args = [ 56 | "connect", 57 | "-s", 58 | session.id, 59 | "-t", 60 | netlifyApiToken, 61 | "-l", 62 | localPort 63 | ]; 64 | if (process.env.DEBUG) { 65 | args.push("-v"); 66 | log(execPath, args); 67 | } 68 | 69 | const ps = execa(execPath, args, { stdio: "inherit" }); 70 | ps.on("close", code => process.exit(code)); 71 | ps.on("SIGINT", process.exit); 72 | ps.on("SIGTERM", process.exit); 73 | } 74 | 75 | async function installTunnelClient(log) { 76 | const binPath = path.join(os.homedir(), ".netlify", "tunnel", "bin"); 77 | const execPath = path.join(binPath, "live-tunnel-client"); 78 | const newVersion = await fetchTunnelClient(execPath); 79 | if (!newVersion) { 80 | return; 81 | } 82 | 83 | log(`${NETLIFYDEVLOG} Installing Live Tunnel Client`); 84 | 85 | const win = isWindows(); 86 | const platform = win ? "windows" : process.platform; 87 | const extension = win ? "zip" : "tar.gz"; 88 | const release = { 89 | repository: "netlify/live-tunnel-client", 90 | package: `live-tunnel-client-${platform}-amd64.${extension}`, 91 | destination: binPath, 92 | extract: true 93 | }; 94 | await fetchLatest(release); 95 | } 96 | 97 | async function fetchTunnelClient(execPath) { 98 | if (!execExist(execPath)) { 99 | return true; 100 | } 101 | 102 | const { stdout } = await execa(execPath, ["version"]); 103 | if (!stdout) { 104 | return false; 105 | } 106 | 107 | const match = stdout.match(/^live-tunnel-client\/v?([^\s]+)/); 108 | if (!match) { 109 | return false; 110 | } 111 | 112 | return updateAvailable("netlify/live-tunnel-client", match[1]); 113 | } 114 | 115 | function execExist(binPath) { 116 | if (!fs.existsSync(binPath)) { 117 | return false; 118 | } 119 | const stat = fs.statSync(binPath); 120 | return stat && stat.isFile() && isExe(stat.mode, stat.gid, stat.uid); 121 | } 122 | 123 | function isExe(mode, gid, uid) { 124 | if (isWindows()) { 125 | return true; 126 | } 127 | 128 | const isGroup = gid ? process.getgid && gid === process.getgid() : true; 129 | const isUser = uid ? process.getuid && uid === process.getuid() : true; 130 | 131 | return Boolean( 132 | mode & 0o0001 || (mode & 0o0010 && isGroup) || (mode & 0o0100 && isUser) 133 | ); 134 | } 135 | 136 | function isWindows() { 137 | return process.platform === "win32"; 138 | } 139 | 140 | module.exports = { 141 | createTunnel: createTunnel, 142 | connectTunnel: connectTunnel 143 | }; 144 | -------------------------------------------------------------------------------- /src/utils/dev.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | // reusable code for netlify dev 4 | // bit of a hasty abstraction but recommended by oclif 5 | const { getAddons } = require("netlify/src/addons"); 6 | const chalk = require("chalk"); 7 | const { 8 | NETLIFYDEVLOG, 9 | // NETLIFYDEVWARN, 10 | NETLIFYDEVERR 11 | } = require("netlify-cli-logo"); 12 | /** 13 | * inject environment variables from netlify addons and buildbot 14 | * into your local dev process.env 15 | * 16 | * ``` 17 | * // usage example 18 | * const { site, api } = this.netlify 19 | * if (site.id) { 20 | * const accessToken = api.accessToken 21 | * const addonUrls = await addEnvVariables(site, accessToken) 22 | * // addonUrls is only for startProxy in netlify dev:index 23 | * } 24 | * ``` 25 | */ 26 | async function addEnvVariables(api, site, accessToken) { 27 | /** from addons */ 28 | const addonUrls = {}; 29 | const addons = await getAddons(site.id, accessToken).catch(error => { 30 | console.error(error); 31 | switch (error.status) { 32 | default: 33 | console.error( 34 | `${NETLIFYDEVERR} Error retrieving addons data for site ${chalk.yellow( 35 | site.id 36 | )}. Double-check your login status with 'netlify status' or contact support with details of your error.` 37 | ); 38 | process.exit(); 39 | } 40 | }); 41 | if (Array.isArray(addons)) { 42 | addons.forEach(addon => { 43 | addonUrls[addon.slug] = `${addon.config.site_url}/.netlify/${addon.slug}`; 44 | for (const key in addon.env) { 45 | const msg = () => 46 | console.log( 47 | `${NETLIFYDEVLOG} Injected ${chalk.yellow.bold("addon")} env var: `, 48 | chalk.yellow(key) 49 | ); 50 | process.env[key] = assignLoudly(process.env[key], addon.env[key], msg); 51 | } 52 | }); 53 | } 54 | 55 | /** from web UI */ 56 | const apiSite = await api.getSite({ site_id: site.id }).catch(error => { 57 | console.error(error); 58 | switch (error.status) { 59 | case 401: 60 | console.error( 61 | `${NETLIFYDEVERR} Unauthorized error: This Site ID ${chalk.yellow( 62 | site.id 63 | )} does not belong to your account.` 64 | ); 65 | console.error( 66 | `${NETLIFYDEVERR} If you cloned someone else's code, try running 'npm unlink' and then 'npm init' or 'npm link'.` 67 | ); 68 | 69 | process.exit(); 70 | default: 71 | console.error( 72 | `${NETLIFYDEVERR} Error retrieving site data for site ${chalk.yellow( 73 | site.id 74 | )}. Double-check your login status with 'netlify status' or contact support with details of your error.` 75 | ); 76 | process.exit(); 77 | } 78 | }); 79 | // TODO: We should move the environment outside of build settings and possibly have a 80 | // `/api/v1/sites/:site_id/environment` endpoint for it that we can also gate access to 81 | // In the future and that we could make context dependend 82 | if (apiSite.build_settings && apiSite.build_settings.env) { 83 | for (const key in apiSite.build_settings.env) { 84 | const msg = () => 85 | console.log( 86 | `${NETLIFYDEVLOG} Injected ${chalk.blue.bold( 87 | "build setting" 88 | )} env var: `, 89 | chalk.yellow(key) 90 | ); 91 | process.env[key] = assignLoudly( 92 | process.env[key], 93 | apiSite.build_settings.env[key], 94 | msg 95 | ); 96 | } 97 | } 98 | 99 | return addonUrls; 100 | } 101 | 102 | module.exports = { 103 | addEnvVariables 104 | }; 105 | 106 | // if first arg is undefined, use default, but tell user about it in case it is unintentional 107 | function assignLoudly( 108 | optionalValue, 109 | defaultValue, 110 | tellUser = dV => console.log(`No value specified, using fallback of `, dV) 111 | ) { 112 | if (defaultValue === undefined) throw new Error("must have a defaultValue"); 113 | if (defaultValue !== optionalValue && optionalValue === undefined) { 114 | tellUser(defaultValue); 115 | return defaultValue; 116 | } 117 | return optionalValue; 118 | } 119 | -------------------------------------------------------------------------------- /src/utils/finders.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const packList = require("npm-packlist"); 4 | const precinct = require("precinct"); 5 | const resolve = require("resolve"); 6 | const readPkgUp = require("read-pkg-up"); 7 | const requirePackageName = require("require-package-name"); 8 | const alwaysIgnored = new Set(["aws-sdk"]); 9 | const debug = require("debug")("netlify-dev-plugin:src/utils/finders"); 10 | 11 | const ignoredExtensions = new Set([ 12 | ".log", 13 | ".lock", 14 | ".html", 15 | ".md", 16 | ".map", 17 | ".ts", 18 | ".png", 19 | ".jpeg", 20 | ".jpg", 21 | ".gif", 22 | ".css", 23 | ".patch" 24 | ]); 25 | 26 | function ignoreMissing(dependency, optional) { 27 | return alwaysIgnored.has(dependency) || (optional && dependency in optional); 28 | } 29 | 30 | function includeModuleFile(packageJson, moduleFilePath) { 31 | if (packageJson.files) { 32 | return true; 33 | } 34 | 35 | return !ignoredExtensions.has(path.extname(moduleFilePath)); 36 | } 37 | 38 | function getDependencies(filename, basedir) { 39 | const servicePath = basedir; 40 | 41 | const filePaths = new Set(); 42 | const modulePaths = new Set(); 43 | const pkgs = {}; 44 | 45 | const modulesToProcess = []; 46 | const localFilesToProcess = [filename]; 47 | 48 | function handle(name, basedir, optionalDependencies) { 49 | const moduleName = requirePackageName(name.replace(/\\/, "/")); 50 | 51 | if (alwaysIgnored.has(moduleName)) { 52 | return; 53 | } 54 | 55 | try { 56 | const pathToModule = resolve.sync(path.join(moduleName, "package.json"), { 57 | basedir 58 | }); 59 | const pkg = readPkgUp.sync({ cwd: pathToModule }); 60 | 61 | if (pkg) { 62 | modulesToProcess.push(pkg); 63 | } 64 | } catch (e) { 65 | if (e.code === "MODULE_NOT_FOUND") { 66 | if (ignoreMissing(moduleName, optionalDependencies)) { 67 | debug(`WARNING missing optional dependency: ${moduleName}`); 68 | return null; 69 | } 70 | try { 71 | // this resolves the requested import also against any set up NODE_PATH extensions, etc. 72 | const resolved = require.resolve(name); 73 | localFilesToProcess.push(resolved); 74 | return; 75 | } catch (e) { 76 | throw new Error(`Could not find "${moduleName}" module in file: ${filename.replace( 77 | path.dirname(basedir), 78 | "" 79 | )}. 80 | 81 | Please ensure "${moduleName}" is installed in the project.`); 82 | } 83 | } 84 | throw e; 85 | } 86 | } 87 | 88 | while (localFilesToProcess.length) { 89 | const currentLocalFile = localFilesToProcess.pop(); 90 | 91 | if (filePaths.has(currentLocalFile)) { 92 | continue; 93 | } 94 | 95 | filePaths.add(currentLocalFile); 96 | precinct 97 | .paperwork(currentLocalFile, { includeCore: false }) 98 | .forEach(dependency => { 99 | if (dependency.indexOf(".") === 0) { 100 | const abs = resolve.sync(dependency, { 101 | basedir: path.dirname(currentLocalFile) 102 | }); 103 | localFilesToProcess.push(abs); 104 | } else { 105 | handle(dependency, servicePath); 106 | } 107 | }); 108 | } 109 | 110 | while (modulesToProcess.length) { 111 | const currentModule = modulesToProcess.pop(); 112 | const currentModulePath = path.join(currentModule.path, ".."); 113 | const packageJson = currentModule.pkg; 114 | 115 | if (modulePaths.has(currentModulePath)) { 116 | continue; 117 | } 118 | modulePaths.add(currentModulePath); 119 | pkgs[currentModulePath] = packageJson; 120 | ["dependencies", "peerDependencies", "optionalDependencies"].forEach( 121 | key => { 122 | const dependencies = packageJson[key]; 123 | 124 | if (dependencies) { 125 | Object.keys(dependencies).forEach(dependency => { 126 | handle( 127 | dependency, 128 | currentModulePath, 129 | packageJson.optionalDependencies 130 | ); 131 | }); 132 | } 133 | } 134 | ); 135 | } 136 | 137 | modulePaths.forEach(modulePath => { 138 | const packageJson = pkgs[modulePath]; 139 | let moduleFilePaths; 140 | 141 | moduleFilePaths = packList.sync({ path: modulePath }); 142 | 143 | moduleFilePaths.forEach(moduleFilePath => { 144 | if (includeModuleFile(packageJson, moduleFilePath)) { 145 | filePaths.add(path.join(modulePath, moduleFilePath)); 146 | } 147 | }); 148 | }); 149 | 150 | // TODO: get rid of this 151 | const sizes = {}; 152 | filePaths.forEach(filepath => { 153 | const stat = fs.lstatSync(filepath); 154 | const ext = path.extname(filepath); 155 | sizes[ext] = (sizes[ext] || 0) + stat.size; 156 | }); 157 | debug("Sizes per extension: ", sizes); 158 | 159 | return [...filePaths]; 160 | } 161 | 162 | function findModuleDir(dir) { 163 | let basedir = dir; 164 | while (!fs.existsSync(path.join(basedir, "package.json"))) { 165 | const newBasedir = path.dirname(basedir); 166 | if (newBasedir === basedir) { 167 | return null; 168 | } 169 | basedir = newBasedir; 170 | } 171 | return basedir; 172 | } 173 | 174 | function findHandler(functionPath) { 175 | if (fs.lstatSync(functionPath).isFile()) { 176 | return functionPath; 177 | } 178 | 179 | const handlerPath = path.join( 180 | functionPath, 181 | `${path.basename(functionPath)}.js` 182 | ); 183 | if (!fs.existsSync(handlerPath)) { 184 | return; 185 | } 186 | return handlerPath; 187 | } 188 | 189 | module.exports = { getDependencies, findModuleDir, findHandler }; 190 | -------------------------------------------------------------------------------- /src/utils/get-functions.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { findModuleDir, findHandler } = require("./finders"); 4 | 5 | module.exports = { 6 | getFunctions(dir) { 7 | const functions = {}; 8 | if (fs.existsSync(dir)) { 9 | fs.readdirSync(dir).forEach(file => { 10 | if (dir === "node_modules") { 11 | return; 12 | } 13 | const functionPath = path.resolve(path.join(dir, file)); 14 | const handlerPath = findHandler(functionPath); 15 | if (!handlerPath) { 16 | return; 17 | } 18 | if (path.extname(functionPath) === ".js") { 19 | functions[file.replace(/\.js$/, "")] = { 20 | functionPath, 21 | moduleDir: findModuleDir(functionPath) 22 | }; 23 | } else if (fs.lstatSync(functionPath).isDirectory()) { 24 | functions[file] = { 25 | functionPath: handlerPath, 26 | moduleDir: findModuleDir(functionPath) 27 | }; 28 | } 29 | }); 30 | } 31 | return functions; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/read-repo-url.js: -------------------------------------------------------------------------------- 1 | const url = require("url"); 2 | const fetch = require("node-fetch"); 3 | const { safeJoin } = require("safe-join"); 4 | 5 | // supported repo host types 6 | const GITHUB = Symbol("GITHUB"); 7 | // const BITBUCKET = Symbol('BITBUCKET') 8 | // const GITLAB = Symbol('GITLAB') 9 | 10 | /** 11 | * Takes a url like https://github.com/netlify-labs/all-the-functions/tree/master/functions/9-using-middleware 12 | * and returns https://api.github.com/repos/netlify-labs/all-the-functions/contents/functions/9-using-middleware 13 | */ 14 | async function readRepoURL(_url) { 15 | const URL = url.parse(_url); 16 | const repoHost = validateRepoURL(_url); 17 | if (repoHost !== GITHUB) 18 | throw new Error("only github repos are supported for now"); 19 | const [owner_and_repo, contents_path] = parseRepoURL(repoHost, URL); 20 | const folderContents = await getRepoURLContents( 21 | repoHost, 22 | owner_and_repo, 23 | contents_path 24 | ); 25 | return folderContents; 26 | } 27 | 28 | async function getRepoURLContents(repoHost, owner_and_repo, contents_path) { 29 | // naive joining strategy for now 30 | if (repoHost === GITHUB) { 31 | // https://developer.github.com/v3/repos/contents/#get-contents 32 | const APIURL = safeJoin( 33 | "https://api.github.com/repos", 34 | owner_and_repo, 35 | "contents", 36 | contents_path 37 | ); 38 | return fetch(APIURL) 39 | .then(x => x.json()) 40 | .catch( 41 | error => console.error("Error occurred while fetching ", APIURL, error) // eslint-disable-line no-console 42 | ); 43 | } 44 | throw new Error("unsupported host ", repoHost); 45 | } 46 | 47 | function validateRepoURL(_url) { 48 | const URL = url.parse(_url); 49 | if (URL.host !== "github.com") return null; 50 | // other validation logic here 51 | return GITHUB; 52 | } 53 | function parseRepoURL(repoHost, URL) { 54 | // naive splitting strategy for now 55 | if (repoHost === GITHUB) { 56 | // https://developer.github.com/v3/repos/contents/#get-contents 57 | const [owner_and_repo, contents_path] = URL.path.split("/tree/master"); // what if it's not master? note that our contents retrieval may assume it is master 58 | return [owner_and_repo, contents_path]; 59 | } 60 | throw new Error("unsupported host ", repoHost); 61 | } 62 | 63 | module.exports = { 64 | readRepoURL, 65 | validateRepoURL 66 | }; 67 | -------------------------------------------------------------------------------- /src/utils/serve-functions.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const express = require("express"); 3 | const bodyParser = require("body-parser"); 4 | const expressLogging = require("express-logging"); 5 | const queryString = require("querystring"); 6 | const path = require("path"); 7 | const getPort = require("get-port"); 8 | const chokidar = require("chokidar"); 9 | const jwtDecode = require("jwt-decode"); 10 | const chalk = require("chalk"); 11 | const { 12 | NETLIFYDEVLOG, 13 | // NETLIFYDEVWARN, 14 | NETLIFYDEVERR 15 | } = require("netlify-cli-logo"); 16 | const { getFunctions } = require("./get-functions"); 17 | 18 | const defaultPort = 34567; 19 | 20 | function handleErr(err, response) { 21 | response.statusCode = 500; 22 | response.write( 23 | `${NETLIFYDEVERR} Function invocation failed: ` + err.toString() 24 | ); 25 | response.end(); 26 | console.log(`${NETLIFYDEVERR} Error during invocation: `, err); // eslint-disable-line no-console 27 | } 28 | 29 | // function getHandlerPath(functionPath) { 30 | // if (functionPath.match(/\.js$/)) { 31 | // return functionPath; 32 | // } 33 | // return path.join(functionPath, `${path.basename(functionPath)}.js`); 34 | // } 35 | 36 | function buildClientContext(headers) { 37 | // inject a client context based on auth header, ported over from netlify-lambda (https://github.com/netlify/netlify-lambda/pull/57) 38 | if (!headers.authorization) return; 39 | 40 | const parts = headers.authorization.split(" "); 41 | if (parts.length !== 2 || parts[0] !== "Bearer") return; 42 | 43 | try { 44 | return { 45 | identity: { 46 | url: 47 | "https://netlify-dev-locally-emulated-identity.netlify.com/.netlify/identity", 48 | token: 49 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb3VyY2UiOiJuZXRsaWZ5IGRldiIsInRlc3REYXRhIjoiTkVUTElGWV9ERVZfTE9DQUxMWV9FTVVMQVRFRF9JREVOVElUWSJ9.2eSDqUOZAOBsx39FHFePjYj12k0LrxldvGnlvDu3GMI" 50 | // you can decode this with https://jwt.io/ 51 | // just says 52 | // { 53 | // "source": "netlify dev", 54 | // "testData": "NETLIFY_DEV_LOCALLY_EMULATED_IDENTITY" 55 | // } 56 | }, 57 | user: jwtDecode(parts[1]) 58 | }; 59 | } catch (_) { 60 | // Ignore errors - bearer token is not a JWT, probably not intended for us 61 | } 62 | } 63 | 64 | function createHandler(dir) { 65 | const functions = getFunctions(dir); 66 | 67 | const clearCache = action => path => { 68 | console.log(`${NETLIFYDEVLOG} ${path} ${action}, reloading...`); // eslint-disable-line no-console 69 | Object.keys(require.cache).forEach(k => { 70 | delete require.cache[k]; 71 | }); 72 | }; 73 | const watcher = chokidar.watch(dir, { ignored: /node_modules/ }); 74 | watcher 75 | .on("change", clearCache("modified")) 76 | .on("unlink", clearCache("deleted")); 77 | 78 | return function(request, response) { 79 | // handle proxies without path re-writes (http-servr) 80 | const cleanPath = request.path.replace(/^\/.netlify\/functions/, ""); 81 | 82 | const func = cleanPath.split("/").filter(function(e) { 83 | return e; 84 | })[0]; 85 | if (!functions[func]) { 86 | response.statusCode = 404; 87 | response.end("Function not found..."); 88 | return; 89 | } 90 | const { functionPath, moduleDir } = functions[func]; 91 | let handler; 92 | let before = module.paths; 93 | try { 94 | module.paths = [moduleDir]; 95 | handler = require(functionPath); 96 | if (typeof handler.handler !== "function") { 97 | throw new Error( 98 | `function ${functionPath} must export a function named handler` 99 | ); 100 | } 101 | module.paths = before; 102 | } catch (error) { 103 | module.paths = before; 104 | handleErr(error, response); 105 | return; 106 | } 107 | 108 | var isBase64Encoded = false; 109 | var body = request.body; 110 | 111 | if (body instanceof Buffer) { 112 | isBase64Encoded = true; 113 | body = body.toString("base64"); 114 | } else if (typeof body === "string") { 115 | // body is already processed as string 116 | } else { 117 | body = ""; 118 | } 119 | 120 | const lambdaRequest = { 121 | path: request.path, 122 | httpMethod: request.method, 123 | queryStringParameters: queryString.parse(request.url.split(/\?(.+)/)[1]), 124 | headers: request.headers, 125 | body: body, 126 | isBase64Encoded: isBase64Encoded 127 | }; 128 | 129 | let callbackWasCalled = false; 130 | const callback = createCallback(response); 131 | // we already checked that it exports a function named handler above 132 | const promise = handler.handler( 133 | lambdaRequest, 134 | { clientContext: buildClientContext(request.headers) || {} }, 135 | callback 136 | ); 137 | /** guard against using BOTH async and callback */ 138 | if (callbackWasCalled && promise && typeof promise.then === "function") { 139 | throw new Error( 140 | "Error: your function seems to be using both a callback and returning a promise (aka async function). This is invalid, pick one. (Hint: async!)" 141 | ); 142 | } else { 143 | // it is definitely an async function with no callback called, good. 144 | promiseCallback(promise, callback); 145 | } 146 | 147 | /** need to keep createCallback in scope so we can know if cb was called AND handler is async */ 148 | function createCallback(response) { 149 | return function(err, lambdaResponse) { 150 | callbackWasCalled = true; 151 | if (err) { 152 | return handleErr(err, response); 153 | } 154 | if (lambdaResponse === undefined) { 155 | return handleErr( 156 | "lambda response was undefined. check your function code again.", 157 | response 158 | ); 159 | } 160 | if (!Number(lambdaResponse.statusCode)) { 161 | console.log( 162 | `${NETLIFYDEVERR} Your function response must have a numerical statusCode. You gave: $`, 163 | lambdaResponse.statusCode 164 | ); 165 | return handleErr("Incorrect function response statusCode", response); 166 | } 167 | if (typeof lambdaResponse.body !== "string") { 168 | console.log( 169 | `${NETLIFYDEVERR} Your function response must have a string body. You gave:`, 170 | lambdaResponse.body 171 | ); 172 | return handleErr("Incorrect function response body", response); 173 | } 174 | 175 | response.statusCode = lambdaResponse.statusCode; 176 | // eslint-disable-line guard-for-in 177 | for (const key in lambdaResponse.headers) { 178 | response.setHeader(key, lambdaResponse.headers[key]); 179 | } 180 | response.write( 181 | lambdaResponse.isBase64Encoded 182 | ? Buffer.from(lambdaResponse.body, "base64") 183 | : lambdaResponse.body 184 | ); 185 | response.end(); 186 | }; 187 | } 188 | }; 189 | } 190 | 191 | function promiseCallback(promise, callback) { 192 | if (!promise) return; // means no handler was written 193 | if (typeof promise.then !== "function") return; 194 | if (typeof callback !== "function") return; 195 | 196 | promise.then( 197 | function(data) { 198 | callback(null, data); 199 | }, 200 | function(err) { 201 | callback(err, null); 202 | } 203 | ); 204 | } 205 | 206 | async function serveFunctions(settings) { 207 | const app = express(); 208 | const dir = settings.functionsDir; 209 | const port = await getPort({ 210 | port: assignLoudly(settings.port, defaultPort) 211 | }); 212 | 213 | app.use( 214 | bodyParser.text({ 215 | limit: "6mb", 216 | type: ["text/*", "application/json", "multipart/form-data"] 217 | }) 218 | ); 219 | app.use(bodyParser.raw({ limit: "6mb", type: "*/*" })); 220 | app.use( 221 | expressLogging(console, { 222 | blacklist: ["/favicon.ico"] 223 | }) 224 | ); 225 | 226 | app.get("/favicon.ico", function(req, res) { 227 | res.status(204).end(); 228 | }); 229 | app.all("*", createHandler(dir)); 230 | 231 | app.listen(port, function(err) { 232 | if (err) { 233 | console.error(`${NETLIFYDEVERR} Unable to start lambda server: `, err); // eslint-disable-line no-console 234 | process.exit(1); 235 | } 236 | 237 | // add newline because this often appears alongside the client devserver's output 238 | console.log(`\n${NETLIFYDEVLOG} Lambda server is listening on ${port}`); // eslint-disable-line no-console 239 | }); 240 | 241 | return Promise.resolve({ 242 | port 243 | }); 244 | } 245 | 246 | module.exports = { serveFunctions }; 247 | 248 | // if first arg is undefined, use default, but tell user about it in case it is unintentional 249 | function assignLoudly( 250 | optionalValue, 251 | fallbackValue, 252 | tellUser = dV => 253 | console.log(`${NETLIFYDEVLOG} No port specified, using defaultPort of `, dV) // eslint-disable-line no-console 254 | ) { 255 | if (fallbackValue === undefined) throw new Error("must have a fallbackValue"); 256 | if (fallbackValue !== optionalValue && optionalValue === undefined) { 257 | tellUser(fallbackValue); 258 | return fallbackValue; 259 | } 260 | return optionalValue; 261 | } 262 | -------------------------------------------------------------------------------- /test/commands/functions.test.js: -------------------------------------------------------------------------------- 1 | const { expect, test } = require("@oclif/test"); 2 | // const nock = require("nock"); 3 | 4 | describe("auth:whoami", () => { 5 | test 6 | .command(["functions:list"]) 7 | .it("shows user email when logged in", ctx => { 8 | expect(ctx.stdout).to.equal("jeff@example.com\n"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --reporter spec 3 | --timeout 5000 4 | --------------------------------------------------------------------------------