├── .babelrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── __tests__ ├── netlifyToml.js └── packageJson.js ├── netlify-ts.gif ├── netlify-ts.png ├── package-lock.json ├── package.json ├── src ├── index.ts ├── lib │ ├── dependencies.ts │ ├── questions.ts │ └── utils │ │ ├── command.ts │ │ ├── index.ts │ │ ├── shell.ts │ │ └── write.ts └── templates │ ├── babelrc.ts │ ├── gitignore.ts │ ├── handler.ts │ ├── netlifyToml.ts │ └── packageJson.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": true } }], 4 | "@babel/preset-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install dependencies 17 | run: yarn 18 | 19 | - name: Unit tests 20 | run: yarn test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | yarn-error.log 4 | test -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020–present Atila Fassina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # create-netlify-ts 4 | 5 | **👟 Building Netlify Functions with TypeScript easily** 6 | 7 | 📦 Package Manager agnostic 8 | 9 | 🐾 No production footprint 10 | 11 | 🚀 Ready to deploy 12 | 13 | ## ❯\_ 14 | 15 | ### npm 16 | 17 | ``` 18 | npx create-netlify-ts 19 | ``` 20 | 21 | ### yarn 22 | 23 | ``` 24 | yarn create netlify-ts 25 | ``` 26 | 27 | ❓ Answer the questions and start coding! 28 | 29 |
30 | 31 | ![Terminal showing create-netlify-ts working](/netlify-ts.gif) 32 | 33 |
34 | 35 | ## 🌲 File tree 36 | 37 | ``` 38 | {{ package-name }} 39 | ├── src 40 | │ └── {{ your-function-name }}.ts 41 | ├── package.json 42 | ├── .babelrc 43 | ├── .gitignore 44 | └── netlify.toml 45 | ``` 46 | 47 | ## 🧳 Installed dependencies 48 | 49 | All depdendencies are installed as `devDependencies`, **create-netlify-ts** has no footprint on your production code. 50 | 51 | ### Required dependencies 52 | 53 | | Package Name | Why | 54 | | ------------------------ | --------------------------------------------- | 55 | | netlify-lambda | Adds build-step to Netlify Functions | 56 | | typescript | The compiler for TypeScript (`tsc`) | 57 | | @babel/preset-env | Tells Babel which JavaScript syntax to output | 58 | | @babel/preset-typescript | Teach Babel to use TypeScript compiler | 59 | | @types/aws-lambda | Request/Response types for AWS Lambdas† | 60 | 61 | †: Netlify Functions runs on top of AWS-Lambdas 62 | 63 | ### Optional dependencies 64 | 65 | - [Netlify CLI](https://docs.netlify.com/cli/get-started/) To run Netlify Functions locally 66 | - [Prettier](https://prettier.io): with some opinionated configuration 67 | 68 | ## 🛫 Flying solo 69 | 70 | It‘s a dangerous road out there. Take these: 71 | 72 | 📹 [Write an API with Netlify Functions and TypeScript](https://www.youtube.com/watch?v=3-Ie6p5ySKQ) 73 | 74 | 🐙 [Monster As A Service](https://github.com/atilafassina/monster-as-a-service): written in TS, deployed to Netlify 75 | 76 | ✍️ [Netlify Functions 💜 TypeScript](https://atila.io/posts/netlify-functions-typescript) 77 | -------------------------------------------------------------------------------- /__tests__/netlifyToml.js: -------------------------------------------------------------------------------- 1 | import netlifyTomlTempalte from '../src/templates/netlifyToml' 2 | import toml from 'toml' 3 | 4 | let answers = { 5 | packageManager: 'yarn', 6 | shouldRewrite: true, 7 | functionName: 'foobar', 8 | } 9 | 10 | test('Generates `netlify.toml` with Yarn command', () => { 11 | const netlifyToml = netlifyTomlTempalte(answers) 12 | const tomlJsObject = toml.parse(netlifyToml) 13 | 14 | expect(tomlJsObject.build.command).toMatch('yarn build') 15 | }) 16 | 17 | test('Generates `netlify.toml` with NPM command', () => { 18 | answers.packageManager = 'npm' 19 | const netlifyToml = netlifyTomlTempalte(answers) 20 | const tomlJsObject = toml.parse(netlifyToml) 21 | 22 | expect(tomlJsObject.build.command).toMatch('npm run build') 23 | }) 24 | 25 | test('Generates `netlify.toml` with Rewrite', () => { 26 | const netlifyToml = netlifyTomlTempalte(answers) 27 | const tomlJsObject = toml.parse(netlifyToml) 28 | 29 | expect(tomlJsObject.redirects[0].status).toEqual(200) 30 | expect(tomlJsObject.redirects[0].to).toMatch( 31 | `/.netlify/functions/${answers.functionName}` 32 | ) 33 | expect(tomlJsObject.redirects[0].from).toMatch('/') 34 | }) 35 | 36 | test('Generates `netlify.toml` without Rewrite', () => { 37 | answers.shouldRewrite = false 38 | const netlifyToml = netlifyTomlTempalte(answers) 39 | const tomlJsObject = toml.parse(netlifyToml) 40 | 41 | expect(tomlJsObject.redirects).toBeUndefined() 42 | }) 43 | -------------------------------------------------------------------------------- /__tests__/packageJson.js: -------------------------------------------------------------------------------- 1 | import packageJsonTemplate from '../src/templates/packageJson' 2 | 3 | let answers = { 4 | packageName: 'New-Lambda', 5 | gitName: 'James Howlett', 6 | gitEmail: 'wolverine@xmen.io', 7 | isPrivate: true, 8 | withPrettier: true, 9 | } 10 | 11 | test('Generates `package.json` successfuly private, and with Prettier', () => { 12 | answers.isPrivate = true 13 | answers.withPrettier = true 14 | 15 | const pkgJson = packageJsonTemplate(answers) 16 | 17 | const json = JSON.parse(pkgJson) 18 | 19 | expect(json.name).toMatch(answers.packageName) 20 | expect(json.private).toBeTruthy() 21 | expect(json.prettier.semi).toBeFalsy() 22 | expect(json.prettier.singleQuote).toBeTruthy() 23 | expect(json.author).toMatch(`${answers.gitName} <${answers.gitEmail}>`) 24 | }) 25 | 26 | test('Generates `package.json` successfuly public, and without Prettier', () => { 27 | answers.isPrivate = false 28 | answers.withPrettier = false 29 | 30 | const pkgJson = packageJsonTemplate(answers) 31 | 32 | const json = JSON.parse(pkgJson) 33 | 34 | expect(json.name).toMatch(answers.packageName) 35 | expect(json.private).toBeFalsy() 36 | expect(json.prettier).toBeUndefined() 37 | expect(json.author).toMatch(`${answers.gitName} <${answers.gitEmail}>`) 38 | }) 39 | -------------------------------------------------------------------------------- /netlify-ts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atilafassina/create-netlify-ts/707bbc315b94bd249558027c0ae7c2c9c6c4a92d/netlify-ts.gif -------------------------------------------------------------------------------- /netlify-ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atilafassina/create-netlify-ts/707bbc315b94bd249558027c0ae7c2c9c6c4a92d/netlify-ts.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-netlify-ts", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:atilafassina/netlify-lambda-ts.git", 6 | "author": "Atila Fassina ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "tsc", 10 | "test": "jest", 11 | "type-check": "tsc --noEmit" 12 | }, 13 | "bin": { 14 | "create-netlify-ts": "build/index.js" 15 | }, 16 | "prettier": { 17 | "semi": false, 18 | "singleQuote": true 19 | }, 20 | "dependencies": { 21 | "is-yarn-global": "^0.3.0", 22 | "ora": "^5.0.0", 23 | "prompts": "^2.3.2" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.11.1", 27 | "@babel/preset-env": "^7.11.0", 28 | "@babel/preset-typescript": "^7.10.4", 29 | "@types/node": "^14.6.0", 30 | "@types/prompts": "^2.0.8", 31 | "babel-jest": "^26.3.0", 32 | "jest": "^26.4.0", 33 | "prettier": "^2.0.5", 34 | "toml": "^3.0.0", 35 | "typescript": "^3.9.7" 36 | }, 37 | "files": [ 38 | "build/" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import prompts from 'prompts' 3 | import ora from 'ora' 4 | import questions from './lib/questions' 5 | import { shell, write, command } from './lib/utils' 6 | import pkgJsonTemplate from './templates/packageJson' 7 | import netlifyTomlTemplate from './templates/netlifyToml' 8 | import babelrcTemplate from './templates/babelrc' 9 | import handlerTemplate from './templates/handler' 10 | import gitignoreTemplate from './templates/gitignore' 11 | import { DEV_DEPENDENCIES } from './lib/dependencies' 12 | ;(async () => { 13 | const CWD = process.cwd() 14 | 15 | const { 16 | packageName, 17 | isPrivate, 18 | withPrettier, 19 | netlifyDev, 20 | packageManager, 21 | shouldRewrite, 22 | functionName, 23 | } = await prompts(questions({ cwd: CWD }), { 24 | onCancel: () => { 25 | process.exit(0) 26 | }, 27 | }) 28 | 29 | const spinner = ora('Setting up...') 30 | spinner.color = 'green' 31 | spinner.start() 32 | 33 | await shell(`mkdir ${packageName}`) 34 | const projectDir = `${CWD}/${packageName}` 35 | 36 | await write(projectDir + '/.gitignore', gitignoreTemplate) 37 | 38 | const gitName = await shell(`git config --global user.name`) 39 | const gitEmail = await shell(`git config --global user.email`) 40 | const devInstall = command(packageManager, 'DEV_INSTALL') 41 | 42 | await write( 43 | projectDir + '/package.json', 44 | pkgJsonTemplate({ 45 | packageName, 46 | gitName, 47 | gitEmail, 48 | isPrivate, 49 | withPrettier, 50 | }) 51 | ) 52 | 53 | if (netlifyDev) { 54 | const globalInstall = command(packageManager, 'GLOBAL_INSTALL') 55 | await shell(`${packageManager} ${globalInstall} netlify-cli`) 56 | } 57 | 58 | await write(`${projectDir}/netlify.toml`, 59 | netlifyTomlTemplate({ packageManager, shouldRewrite, functionName }) 60 | ) 61 | 62 | await write(`${projectDir}/.babelrc`, babelrcTemplate) 63 | 64 | await shell('mkdir src', projectDir) 65 | await write(`${projectDir}/src/${functionName}.ts`, handlerTemplate) 66 | 67 | if (withPrettier) DEV_DEPENDENCIES.push('prettier') 68 | 69 | await shell( 70 | `${packageManager} ${devInstall} ${DEV_DEPENDENCIES.join(' ')}`, 71 | projectDir 72 | ) 73 | 74 | spinner.stop() 75 | console.log(`👟 ${functionName}’s built`) 76 | })() 77 | -------------------------------------------------------------------------------- /src/lib/dependencies.ts: -------------------------------------------------------------------------------- 1 | const DEV_DEPENDENCIES = [ 2 | 'netlify-lambda', 3 | '@babel/core', 4 | '@babel/preset-env', 5 | '@babel/preset-typescript', 6 | 'typescript', 7 | '@types/aws-lambda', 8 | ] 9 | export { DEV_DEPENDENCIES } 10 | -------------------------------------------------------------------------------- /src/lib/questions.ts: -------------------------------------------------------------------------------- 1 | const isYarnGlobal = require('is-yarn-global'); 2 | import { PromptObject } from 'prompts' 3 | 4 | export default ({ cwd }: { cwd: string }): PromptObject[] => [ 5 | { 6 | type: 'text', 7 | name: 'packageName', 8 | message: 'What is the name of the package?', 9 | validate: (value: string) => 10 | Boolean(value) && value.includes(' ') 11 | ? `Cannot be blank, nor have spaces` 12 | : true, 13 | }, 14 | { 15 | type: 'toggle', 16 | name: 'path', 17 | message: (prev: string) => 18 | `A new directory will be created at ${cwd}/${prev}`, 19 | initial: true, 20 | active: 'OK', 21 | inactive: 'Cancel', 22 | }, 23 | { 24 | type: (prev: string) => (prev ? 'text' : null), 25 | name: 'functionName', 26 | message: 'What is the name of the function?', 27 | validate: (value: string) => 28 | Boolean(value) && value.includes(' ') 29 | ? `Cannot be blank, nor have spaces` 30 | : true, 31 | }, 32 | { 33 | type: 'select', 34 | name: 'packageManager', 35 | message: 'Yarn or NPM?', 36 | choices: [ 37 | { 38 | title: 'Yarn', 39 | description: 'Use Yarn as the package manager', 40 | value: 'yarn', 41 | disabled: !isYarnGlobal() 42 | }, 43 | { 44 | title: 'NPM', 45 | description: 'Use NPM as the package manager', 46 | value: 'npm', 47 | }, 48 | ], 49 | initial: isYarnGlobal() ? 0 : 1, 50 | }, 51 | { 52 | type: 'toggle', 53 | name: 'isPrivate', 54 | message: 'Is this package private?', 55 | initial: true, 56 | active: 'yes', 57 | inactive: 'no', 58 | }, 59 | { 60 | type: 'toggle', 61 | name: 'withPrettier', 62 | message: 'Add Prettier?', 63 | initial: false, 64 | active: 'yes', 65 | inactive: 'no', 66 | }, 67 | { 68 | type: 'toggle', 69 | name: 'netlifyDev', 70 | message: 'Install or update Netlify CLI (globally)?', 71 | initial: false, 72 | active: 'Yes', 73 | inactive: 'No', 74 | }, 75 | { 76 | type: 'toggle', 77 | name: 'shouldRewrite', 78 | message: 79 | 'Would you like to make a Rewrite from `/functions/.netlify` to the root?', 80 | initial: false, 81 | active: 'Yes', 82 | inactive: 'No', 83 | }, 84 | ] 85 | -------------------------------------------------------------------------------- /src/lib/utils/command.ts: -------------------------------------------------------------------------------- 1 | interface ICommandOptions { 2 | [key: string]: any; 3 | } 4 | 5 | export function command(pkgManager: string, cmd: string): string { 6 | let _cmd = '--help' 7 | 8 | const options: ICommandOptions = { 9 | 'GLOBAL_INSTALL': pkgManager === 'yarn' ? 'global add --silent' : 'i -g', 10 | 'LOCAL_INSTALL': pkgManager === 'yarn' ? 'add --silent' : 'i', 11 | 'DEV_INSTALL': pkgManager === 'yarn' ? 'add -D --silent' : 'i -D', 12 | } 13 | 14 | return options[cmd] || _cmd; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './write' 2 | export * from './shell' 3 | export * from './command' 4 | -------------------------------------------------------------------------------- /src/lib/utils/shell.ts: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util') 2 | const exec = promisify(require('child_process').exec) 3 | 4 | export async function shell(cmd: string, cwd?: string) { 5 | const { stdout, stderr } = await exec(cmd, { cwd }) 6 | 7 | if (stderr) { 8 | // throw new Error(stderr) 9 | } 10 | 11 | return stdout 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/utils/write.ts: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util') 2 | const writeFile = promisify(require('fs').writeFile) 3 | 4 | export async function write(path: string, data: {}) { 5 | return writeFile(path, data, 'utf8') 6 | } 7 | -------------------------------------------------------------------------------- /src/templates/babelrc.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | { 3 | "presets": [ 4 | "@babel/preset-typescript", 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": true 10 | } 11 | } 12 | ] 13 | ] 14 | }` 15 | -------------------------------------------------------------------------------- /src/templates/gitignore.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | node_modules 3 | .netlify 4 | lambda 5 | ` 6 | -------------------------------------------------------------------------------- /src/templates/handler.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | import { APIGatewayEvent, Context } from 'aws-lambda' 3 | 4 | export async function handler (event: APIGatewayEvent, context: Context) { 5 | return { 6 | statusCode: 200, 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | body: JSON.stringify({ 11 | msg: 'Hello Netlify Functions' 12 | }) 13 | } 14 | } 15 | ` 16 | -------------------------------------------------------------------------------- /src/templates/netlifyToml.ts: -------------------------------------------------------------------------------- 1 | export default ({ 2 | packageManager, 3 | shouldRewrite = false, 4 | functionName = '', 5 | }: { 6 | packageManager: string 7 | shouldRewrite: boolean 8 | functionName: string 9 | }) => ` 10 | # This file configures your Netlify deploy 11 | # Settings declared here override everything on Dashboard interface 12 | # https://docs.netlify.com/configure-builds/file-based-configuration/ 13 | 14 | [build] 15 | command = "${packageManager}${packageManager === 'npm' ? ' run' : ''} build" 16 | functions = "lambda" 17 | 18 | ${ 19 | shouldRewrite 20 | ? ` 21 | # redirects with status 200 are rewrites 22 | [[redirects]] 23 | from = "/" 24 | to = "/.netlify/functions/${functionName}" 25 | status = 200 26 | ` 27 | : '' 28 | } 29 | ` 30 | -------------------------------------------------------------------------------- /src/templates/packageJson.ts: -------------------------------------------------------------------------------- 1 | type PkgJsonInfo = { 2 | [key: string]: string 3 | } 4 | 5 | export default ({ 6 | packageName, 7 | gitName, 8 | gitEmail, 9 | isPrivate, 10 | withPrettier, 11 | }: PkgJsonInfo) => ` 12 | { 13 | "name": "${packageName}", 14 | "version": "1.0.0", 15 | "main": "index.js", 16 | "author": "${gitName.trimEnd()} <${gitEmail.trimEnd()}>", 17 | "license": "MIT", 18 | "private": ${isPrivate}, 19 | "scripts": { 20 | "build": "netlify-lambda build src", 21 | "ts-check": "tsc --noEmit --lib ES2015 ./src/*.ts" 22 | }${ 23 | withPrettier 24 | ? `, 25 | "prettier": { 26 | "semi": false, 27 | "singleQuote": true 28 | }` 29 | : '' 30 | } 31 | } 32 | 33 | ` 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "build", 5 | "declarationDir": "build/types", 6 | "module": "commonjs", 7 | "target": "ES2020", 8 | "lib": ["ES2020"], 9 | "allowSyntheticDefaultImports": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "moduleResolution": "node", 14 | "esModuleInterop": true, 15 | "noUnusedLocals": true, 16 | "noImplicitAny": true, 17 | "declaration": true, 18 | "strict": true 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["node_modules", "build"] 22 | } 23 | --------------------------------------------------------------------------------