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