├── .gitignore ├── README.md ├── api.http ├── bun.lockb ├── package.json ├── prisma ├── dev.db ├── migrations │ ├── 20240213122132_init │ │ └── migration.sql │ ├── 20240213123902_add_relation_todo_on_user │ │ └── migration.sql │ ├── 20240213125918_add_stripe_fields_on_user │ │ └── migration.sql │ ├── 20240213132327_add_stripe_subscription_status_on_user │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── source ├── config.ts ├── controllers │ ├── checkout.controller.ts │ ├── stripe.controller.ts │ ├── todo.controller.ts │ └── user.controller.ts ├── index.ts └── lib │ ├── notifylog.ts │ ├── prisma.ts │ └── stripe.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro-saas-todo 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.0.22. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /api.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:3000/users HTTP/1.1 2 | 3 | ### 4 | GET http://localhost:3000/users/ea003aa7-a137-4e21-bfd6-e8d1e9eb4961 HTTP/1.1 5 | 6 | ### 7 | POST http://localhost:3000/users HTTP/1.1 8 | content-type: application/json 9 | 10 | { 11 | "name": "Jane Doe", 12 | "email": "jane@doe.com" 13 | } 14 | 15 | ### 16 | POST http://localhost:3000/todos HTTP/1.1 17 | content-type: application/json 18 | x-user-id: 542998ff-71ef-4482-9327-b3e30e7bc44d 19 | 20 | { 21 | "title": "Task #9" 22 | } 23 | 24 | ### 25 | POST http://localhost:3000/checkout HTTP/1.1 26 | x-user-id: 542998ff-71ef-4482-9327-b3e30e7bc44d -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibe-dev/micro-saas-todo-api-stripe/32c9558f30b3bfe7a7952e1ebd57b4047f821c2f/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-saas-todo", 3 | "module": "index.ts", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "bun --watch source/index.ts" 7 | }, 8 | "devDependencies": { 9 | "@types/bun": "latest", 10 | "@types/express": "^4.17.21", 11 | "prisma": "^5.9.1" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5.0.0" 15 | }, 16 | "dependencies": { 17 | "@prisma/client": "5.9.1", 18 | "express": "^4.18.2", 19 | "notifylog": "^1.0.9", 20 | "stripe": "^14.16.0" 21 | } 22 | } -------------------------------------------------------------------------------- /prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibe-dev/micro-saas-todo-api-stripe/32c9558f30b3bfe7a7952e1ebd57b4047f821c2f/prisma/dev.db -------------------------------------------------------------------------------- /prisma/migrations/20240213122132_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" DATETIME NOT NULL 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "Todo" ( 12 | "id" TEXT NOT NULL PRIMARY KEY, 13 | "title" TEXT NOT NULL, 14 | "done" BOOLEAN NOT NULL DEFAULT false, 15 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | "updatedAt" DATETIME NOT NULL 17 | ); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 21 | -------------------------------------------------------------------------------- /prisma/migrations/20240213123902_add_relation_todo_on_user/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `ownerId` to the `Todo` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- RedefineTables 8 | PRAGMA foreign_keys=OFF; 9 | CREATE TABLE "new_Todo" ( 10 | "id" TEXT NOT NULL PRIMARY KEY, 11 | "title" TEXT NOT NULL, 12 | "done" BOOLEAN NOT NULL DEFAULT false, 13 | "ownerId" TEXT NOT NULL, 14 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | "updatedAt" DATETIME NOT NULL, 16 | CONSTRAINT "Todo_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 17 | ); 18 | INSERT INTO "new_Todo" ("createdAt", "done", "id", "title", "updatedAt") SELECT "createdAt", "done", "id", "title", "updatedAt" FROM "Todo"; 19 | DROP TABLE "Todo"; 20 | ALTER TABLE "new_Todo" RENAME TO "Todo"; 21 | PRAGMA foreign_key_check; 22 | PRAGMA foreign_keys=ON; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20240213125918_add_stripe_fields_on_user/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "stripeCustomerId" TEXT; 3 | ALTER TABLE "User" ADD COLUMN "stripeSubscriptionId" TEXT; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240213132327_add_stripe_subscription_status_on_user/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "stripeSubscriptionStatus" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = "file:./dev.db" 11 | } 12 | 13 | model User { 14 | id String @id @default(uuid()) 15 | name String 16 | email String @unique 17 | 18 | stripeCustomerId String? 19 | stripeSubscriptionId String? 20 | stripeSubscriptionStatus String? 21 | 22 | todos Todo[] 23 | 24 | createdAt DateTime @default(now()) 25 | updatedAt DateTime @updatedAt 26 | } 27 | 28 | model Todo { 29 | id String @id @default(uuid()) 30 | title String 31 | done Boolean @default(false) 32 | 33 | owner User @relation(fields: [ownerId], references: [id]) 34 | ownerId String 35 | 36 | createdAt DateTime @default(now()) 37 | updatedAt DateTime @updatedAt 38 | } 39 | -------------------------------------------------------------------------------- /source/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | stripe: { 3 | publishableKey: 'pk_test_51OjLOeG9zZ5BQQOjZPv1SMm1PAZjJWgtFHrm6HXwxB5AEHlWiRC9Rkv5WTBlfEkDccRCjrnSsJAC6DPtCTnqX9Ty00oubljWjt', 4 | secretKey: 'sk_test_51OjLOeG9zZ5BQQOjBKEcN7jwwjFupQcRLsTRmfWkOa1YAYbMPTfWggZAo42BReJ4oVXuGjlZHfQ6ePTxDPttYHds00y3bFJSDs', 5 | proPriceId: 'price_1OjLc7G9zZ5BQQOjegepXNMI', 6 | webhookSecret: 'whsec_17a14e2622bead06a4132460bb6ea5a4a9b3a14e8c81f7cb7ebaa73f4cd770e5' 7 | } 8 | } -------------------------------------------------------------------------------- /source/controllers/checkout.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import { prisma } from "../lib/prisma"; 3 | import { createCheckoutSession } from "../lib/stripe"; 4 | 5 | export const createCheckoutController = async (request: Request, response: Response) => { 6 | const userId = request.headers['x-user-id'] 7 | 8 | if(!userId) { 9 | return response.status(403).send({ 10 | error: 'Not authorized' 11 | }) 12 | } 13 | 14 | const user = await prisma.user.findUnique({ 15 | where: { 16 | id: userId as string 17 | } 18 | }) 19 | 20 | if(!user) { 21 | return response.status(403).send({ 22 | error: 'Not authorized' 23 | }) 24 | } 25 | 26 | const checkout = await createCheckoutSession(user.id, user.email) 27 | 28 | return response.send(checkout) 29 | } -------------------------------------------------------------------------------- /source/controllers/stripe.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import { handleProcessWebhookCheckout, handleProcessWebhookUpdatedSubscription, stripe } from "../lib/stripe"; 3 | import { config } from "../config"; 4 | 5 | import Stripe from "stripe"; 6 | 7 | export const stripeWebhookController = async (request: Request, response: Response) => { 8 | let event: Stripe.Event = request.body; 9 | 10 | if (!config.stripe.webhookSecret) { 11 | console.error('STRIPE_WEBHOOK_SECRET_KEY is not set.'); 12 | return response.sendStatus(400); 13 | } 14 | 15 | const signature = request.headers['stripe-signature'] as string; 16 | 17 | try { 18 | event = await stripe.webhooks.constructEventAsync( 19 | request.body, 20 | signature, 21 | config.stripe.webhookSecret, 22 | undefined, 23 | Stripe.createSubtleCryptoProvider() 24 | ); 25 | } catch (err) { 26 | const errorMessage = (err instanceof Error) ? err.message : 'Unknown error'; 27 | console.error('⚠️ Webhook signature verification failed.', errorMessage); 28 | return response.sendStatus(400); 29 | } 30 | 31 | try { 32 | switch (event.type) { 33 | case 'checkout.session.completed': 34 | await handleProcessWebhookCheckout(event.data); 35 | break; 36 | case 'customer.subscription.created': 37 | case 'customer.subscription.updated': 38 | await handleProcessWebhookUpdatedSubscription(event.data); 39 | break; 40 | default: 41 | console.log(`Unhandled event type ${event.type}`); 42 | } 43 | 44 | return response.json({ received: true }); 45 | } catch (error) { 46 | const errorMessage = (error instanceof Error) ? error.message : 'Unknown error'; 47 | console.error(errorMessage); 48 | return response.status(500).json({ error: errorMessage }); 49 | } 50 | } -------------------------------------------------------------------------------- /source/controllers/todo.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import { prisma } from "../lib/prisma"; 3 | import { notifylog } from "../lib/notifylog"; 4 | 5 | export const createTodoController = async (request: Request, response: Response) => { 6 | const userId = request.headers['x-user-id'] 7 | 8 | if(!userId) { 9 | return response.status(403).send({ 10 | error: 'Not authorized' 11 | }) 12 | } 13 | 14 | const user = await prisma.user.findUnique({ 15 | where: { 16 | id: userId as string 17 | }, 18 | select: { 19 | id: true, 20 | name: true, 21 | email: true, 22 | stripeSubscriptionId: true, 23 | stripeSubscriptionStatus: true, 24 | _count: { 25 | select: { 26 | todos: true 27 | } 28 | } 29 | } 30 | }) 31 | 32 | if(!user) { 33 | return response.status(403).send({ 34 | error: 'Not authorized' 35 | }) 36 | } 37 | 38 | const hasQuotaAvailable = user._count.todos <= 5 39 | const hasActiveSubscription = !!user.stripeSubscriptionId 40 | 41 | if(!hasQuotaAvailable && !hasActiveSubscription && user.stripeSubscriptionStatus !== 'active') { 42 | return response.status(403).send({ 43 | error: 'Not quota available. Please upgrade your plan.' 44 | }) 45 | } 46 | 47 | const { title } = request.body 48 | 49 | const todo = await prisma.todo.create({ 50 | data: { 51 | title, 52 | ownerId: user.id 53 | } 54 | }) 55 | 56 | return response.status(201).send(todo) 57 | } -------------------------------------------------------------------------------- /source/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from "express"; 2 | import { prisma } from "../lib/prisma"; 3 | import { createStripeCustomer } from "../lib/stripe"; 4 | 5 | export const listUsersController = async (request: Request, response: Response) => { 6 | const users = await prisma.user.findMany() 7 | response.send(users) 8 | } 9 | 10 | export const findOneUserController = async (request: Request, response: Response) => { 11 | const { userId } = request.params 12 | 13 | const user = await prisma.user.findUnique({ 14 | where: { 15 | id: userId 16 | } 17 | }) 18 | 19 | if(!user) { 20 | return response.status(404).send({ 21 | error: 'Not found' 22 | }) 23 | } 24 | 25 | response.send(user) 26 | } 27 | 28 | export const createUserController = async (request: Request, response: Response) => { 29 | const { name, email } = request.body 30 | 31 | if(!name || !email) { 32 | return response.send({ 33 | error: 'Name or email is invalid' 34 | }) 35 | } 36 | 37 | const userEmailAlreadyExists = await prisma.user.findUnique({ 38 | where: { 39 | email 40 | }, 41 | select: { 42 | id: true 43 | } 44 | }) 45 | 46 | if(userEmailAlreadyExists) { 47 | return response.status(400).send({ 48 | error: 'Email already in use' 49 | }) 50 | } 51 | 52 | const stripeCustomer = await createStripeCustomer({ 53 | name, 54 | email 55 | }) 56 | 57 | const user = await prisma.user.create({ 58 | data: { 59 | name, 60 | email, 61 | stripeCustomerId: stripeCustomer.id 62 | } 63 | }) 64 | 65 | response.send(user) 66 | } -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { createUserController, findOneUserController, listUsersController } from './controllers/user.controller'; 4 | import { createTodoController } from './controllers/todo.controller'; 5 | import { createCheckoutController } from './controllers/checkout.controller'; 6 | import { stripeWebhookController } from './controllers/stripe.controller'; 7 | 8 | const app = express(); 9 | const port = 3000; 10 | 11 | app.post('/stripe', express.raw({ type: 'application/json' }), stripeWebhookController) 12 | 13 | app.use(express.json()) 14 | 15 | app.get('/users', listUsersController) 16 | app.post('/users', createUserController) 17 | app.get('/users/:userId', findOneUserController) 18 | app.post('/todos', createTodoController) 19 | app.post('/checkout', createCheckoutController) 20 | 21 | app.listen(port, () => { 22 | console.log(`Server is running on http://localhost:${port}`); 23 | }); 24 | -------------------------------------------------------------------------------- /source/lib/notifylog.ts: -------------------------------------------------------------------------------- 1 | import { NotifyLog } from "notifylog"; 2 | 3 | export const notifylog = new NotifyLog('ece81836-bad7-4e17-ac06-baa367d3936c') -------------------------------------------------------------------------------- /source/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prisma = new PrismaClient() -------------------------------------------------------------------------------- /source/lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | 3 | import { config } from "../config"; 4 | import { prisma } from "./prisma"; 5 | 6 | export const stripe = new Stripe(config.stripe.secretKey, { 7 | apiVersion: '2023-10-16', 8 | httpClient: Stripe.createFetchHttpClient(), 9 | }) 10 | 11 | export const getStripeCustomerByEmail = async (email: string) => { 12 | const customers = await stripe.customers.list({ email }); 13 | return customers.data[0]; 14 | } 15 | 16 | export const createStripeCustomer = async ( 17 | input: { 18 | name?: string, 19 | email: string 20 | } 21 | ) => { 22 | let customer = await getStripeCustomerByEmail(input.email) 23 | if(customer) return customer 24 | 25 | return stripe.customers.create({ 26 | email: input.email, 27 | name: input.name 28 | }); 29 | } 30 | 31 | export const createCheckoutSession = async (userId: string, userEmail: string) => { 32 | try { 33 | let customer = await createStripeCustomer({ 34 | email: userEmail 35 | }) 36 | 37 | const session = await stripe.checkout.sessions.create({ 38 | payment_method_types: ['card'], 39 | mode: 'subscription', 40 | client_reference_id: userId, 41 | customer: customer.id, 42 | success_url: `http://localhost:3000/success.html`, 43 | cancel_url: `http://localhost:3000/cancel.html`, 44 | line_items: [{ 45 | price: config.stripe.proPriceId, 46 | quantity: 1 47 | }], 48 | }); 49 | 50 | return { 51 | url: session.url 52 | } 53 | } catch (error) { 54 | throw new Error('Error to create checkout session') 55 | } 56 | } 57 | 58 | export const handleProcessWebhookCheckout = async (event: { object: Stripe.Checkout.Session }) => { 59 | const clientReferenceId = event.object.client_reference_id as string 60 | const stripeSubscriptionId = event.object.subscription as string 61 | const stripeCustomerId = event.object.customer as string 62 | const checkoutStatus = event.object.status 63 | 64 | if(checkoutStatus !== 'complete') return 65 | 66 | if(!clientReferenceId || !stripeSubscriptionId || !stripeCustomerId) { 67 | throw new Error('clientReferenceId, stripeSubscriptionId and stripeCustomerId is required') 68 | } 69 | 70 | const userExists = await prisma.user.findUnique({ 71 | where: { 72 | id: clientReferenceId 73 | }, 74 | select: { 75 | id: true 76 | } 77 | }) 78 | 79 | if(!userExists) { 80 | throw new Error('user of clientReferenceId not found') 81 | } 82 | 83 | await prisma.user.update({ 84 | where: { 85 | id: userExists.id 86 | }, 87 | data: { 88 | stripeCustomerId, 89 | stripeSubscriptionId 90 | } 91 | }) 92 | } 93 | 94 | export const handleProcessWebhookUpdatedSubscription = async (event: { object: Stripe.Subscription }) => { 95 | const stripeCustomerId = event.object.customer as string 96 | const stripeSubscriptionId = event.object.id as string 97 | const stripeSubscriptionStatus = event.object.status 98 | 99 | const userExists = await prisma.user.findFirst({ 100 | where: { 101 | OR: [ 102 | { 103 | stripeSubscriptionId, 104 | }, 105 | { 106 | stripeCustomerId 107 | } 108 | ] 109 | }, 110 | select: { 111 | id: true 112 | } 113 | }) 114 | 115 | if(!userExists) { 116 | throw new Error('user of stripeCustomerId not found') 117 | } 118 | 119 | await prisma.user.update({ 120 | where: { 121 | id: userExists.id 122 | }, 123 | data: { 124 | stripeCustomerId, 125 | stripeSubscriptionId, 126 | stripeSubscriptionStatus, 127 | } 128 | }) 129 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleDetection": "force", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "forceConsistentCasingInFileNames": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------