",
56 | "homepage": "https://github.com/thgh/payload-plugin-oauth",
57 | "bugs": {
58 | "url": "https://github.com/thgh/payload-plugin-oauth/issues"
59 | },
60 | "repository": {
61 | "type": "git",
62 | "url": "https://github.com/thgh/payload-plugin-oauth"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/OAuthButton.tsx:
--------------------------------------------------------------------------------
1 | import Button from 'payload/dist/admin/components/elements/Button'
2 | import React from 'react'
3 | import { ButtonProps } from './types'
4 |
5 | export default function OAuthButton(props: ButtonProps) {
6 | return (
7 |
8 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { str62 } from '@bothrs/util/random'
2 | import MongoStore from 'connect-mongo'
3 | import session from 'express-session'
4 | import jwt from 'jsonwebtoken'
5 | import passport from 'passport'
6 | import OAuth2Strategy, { VerifyCallback } from 'passport-oauth2'
7 | import debug from 'debug'
8 | import payload from 'payload'
9 | import { Config } from 'payload/config'
10 | import {
11 | Field,
12 | fieldAffectsData,
13 | fieldHasSubFields,
14 | } from 'payload/dist/fields/config/types'
15 | import { PaginatedDocs } from 'payload/dist/database/types'
16 | import getCookieExpiration from 'payload/dist/utilities/getCookieExpiration'
17 | import { TextField } from 'payload/types'
18 |
19 | import OAuthButton from './OAuthButton'
20 | import type { oAuthPluginOptions } from './types'
21 | import { createElement } from 'react'
22 |
23 | export { OAuthButton, oAuthPluginOptions }
24 |
25 | interface User {
26 | id: string | number
27 | }
28 |
29 | const log = debug('plugin:oauth')
30 |
31 | // Detect client side because some dependencies may be nullified
32 | const CLIENTSIDE = typeof session !== 'function'
33 |
34 | /**
35 | * Example for Wordpress:
36 | *
37 | * ```
38 | * export function mijnNederlandsAuth() {
39 | * return oAuthPlugin({
40 | * databaseUri: process.env.MONGO_URL,
41 | * clientID: process.env.OAUTH_CLIENT_ID,
42 | * clientSecret: process.env.OAUTH_CLIENT_SECRET,
43 | * authorizationURL: process.env.OAUTH_SERVER + '/oauth/authorize',
44 | * tokenURL: process.env.OAUTH_SERVER + '/oauth/token',
45 | * callbackURL: process.env.ORIGIN + '/oauth/callback',
46 | * scope: 'basic',
47 | * async userinfo(accessToken) {
48 | * const { data: user } = await axios.get(process.env.OAUTH_SERVER + '/oauth/me', {
49 | * params: { access_token: accessToken },
50 | * })
51 | * return {
52 | * sub: user.ID,
53 | *
54 | * // Fields to fill in if user is created
55 | * name: user.display_name || user.user_nicename || 'Nameless',
56 | * email: user.user_email,
57 | * role: user.capabilities?.administrator ? 'admin' : 'user',
58 | * }
59 | * },
60 | * })
61 | * }
62 | * ```
63 | */
64 | export const oAuthPlugin =
65 | (options: oAuthPluginOptions) =>
66 | (incoming: Config): Config => {
67 | // Shorthands
68 | const collectionSlug = options.userCollection?.slug || 'users'
69 | const sub = options.subField?.name || 'sub'
70 |
71 | // Spread the existing config
72 | const config: Config = {
73 | ...incoming,
74 | collections: (incoming.collections || []).map((c) => {
75 | // Let's track the oAuth id (sub)
76 | if (
77 | c.slug === collectionSlug &&
78 | !c.fields.some((f) => (f as TextField).name === sub)
79 | ) {
80 | c.fields.push({
81 | name: sub,
82 | type: 'text',
83 | admin: { readOnly: true },
84 | access: { update: () => false },
85 | })
86 | }
87 | return c
88 | }),
89 | }
90 |
91 | return CLIENTSIDE
92 | ? oAuthPluginClient(config, options)
93 | : oAuthPluginServer(config, options)
94 | }
95 |
96 | function oAuthPluginClient(
97 | incoming: Config,
98 | options: oAuthPluginOptions
99 | ): Config {
100 | const button = options.components?.Button ?? OAuthButton
101 | return button
102 | ? {
103 | ...incoming,
104 | admin: {
105 | ...incoming.admin,
106 | components: {
107 | ...incoming.admin?.components,
108 | beforeLogin: (incoming.admin?.components?.beforeLogin || []).concat(
109 | () =>
110 | createElement(button, {
111 | authorizePath: options.authorizePath || '/oauth2/authorize',
112 | buttonLabel: options.buttonLabel || `Sign in with oAuth`,
113 | })
114 | ),
115 | },
116 | },
117 | }
118 | : incoming
119 | }
120 |
121 | function oAuthPluginServer(
122 | incoming: Config,
123 | options: oAuthPluginOptions
124 | ): Config {
125 | // Shorthands
126 | const callbackPath =
127 | options.callbackPath ||
128 | (options.callbackURL && new URL(options.callbackURL).pathname) ||
129 | '/oauth2/callback'
130 | const authorizePath = options.authorizePath ?? '/oauth2/authorize'
131 | const collectionSlug = (options.userCollection?.slug as 'users') || 'users'
132 | const sub = options.subField?.name || 'sub'
133 | const oAuthStrategyCount = (incoming.custom?.oAuthStrategyCount || 0) + 1
134 | const strategyName = `oauth2-${oAuthStrategyCount}`
135 |
136 | if (options.clientID) {
137 | // Validate paths, they must be unique
138 | const oAuthPaths = incoming.custom?.oAuthPaths || new Set()
139 | if (oAuthPaths.has(authorizePath))
140 | throw new Error(
141 | `Choose a unique authorizePath for oAuth strategy ${oAuthStrategyCount} (not ${options.authorizePath})`
142 | )
143 | oAuthPaths.add(authorizePath)
144 | if (oAuthPaths.has(callbackPath))
145 | throw new Error(
146 | `Choose a unique callbackPath for oAuth strategy ${oAuthStrategyCount} (not ${options.callbackPath})`
147 | )
148 |
149 | // Passport strategy
150 | const strategy = new OAuth2Strategy(options, async function (
151 | accessToken: string,
152 | refreshToken: string,
153 | profile: {},
154 | cb: VerifyCallback
155 | ) {
156 | let info: {
157 | sub: string
158 | email?: string
159 | password?: string
160 | name?: string
161 | }
162 | let user: User & { collection?: any; _strategy?: any }
163 | let users: PaginatedDocs
164 | try {
165 | // Get the userinfo
166 | info = await options.userinfo?.(accessToken, refreshToken)
167 | if (!info) throw new Error('Failed to get userinfo')
168 |
169 | // Match existing user
170 | users = await payload.find({
171 | collection: collectionSlug,
172 | where: { [sub]: { equals: info[sub as 'sub'] } },
173 | showHiddenFields: true,
174 | })
175 |
176 | if (users.docs && users.docs.length) {
177 | user = users.docs[0]
178 | if (options.updateOnLogin) {
179 | user = await payload.update({
180 | collection: collectionSlug,
181 | id: user.id,
182 | data: info,
183 | showHiddenFields: true,
184 | })
185 | }
186 | user.collection = collectionSlug
187 | user._strategy = strategyName
188 | } else {
189 | // Register new user
190 | user = await payload.create({
191 | collection: collectionSlug,
192 | data: {
193 | ...info,
194 | // Stuff breaks when password is missing
195 | password: info.password || str62(20),
196 | },
197 | showHiddenFields: true,
198 | })
199 | log('signin.user', user)
200 | user.collection = collectionSlug
201 | user._strategy = strategyName
202 | }
203 |
204 | cb(null, user)
205 | } catch (error: any) {
206 | log('signin.fail', error.message, error.trace)
207 | cb(error)
208 | }
209 | })
210 |
211 | // Alternative?
212 | // strategy.userProfile = async (accessToken, cb) => {
213 | // const user = await options.userinfo?.(accessToken)
214 | // if (!user) cb(new Error('Failed to get userinfo'))
215 | // else cb(null, user)
216 | // }
217 |
218 | passport.use(strategyName, strategy)
219 | // passport.serializeUser((user: Express.User, done) => {
220 | passport.serializeUser((user: any, done) => {
221 | done(null, user.id)
222 | })
223 | passport.deserializeUser(async (id: string, done) => {
224 | const ok = await payload.findByID({ collection: collectionSlug, id })
225 | done(null, ok)
226 | })
227 | } else {
228 | console.warn('No client id, oauth disabled')
229 | }
230 |
231 | return {
232 | ...incoming,
233 | custom: { ...incoming.custom, oAuthStrategyCount },
234 | admin: {
235 | ...incoming.admin,
236 | webpack: (webpackConfig) => {
237 | const config = incoming.admin?.webpack?.(webpackConfig) || webpackConfig
238 | return {
239 | ...config,
240 | resolve: {
241 | ...config.resolve,
242 | alias: {
243 | ...config.resolve?.alias,
244 | 'connect-mongo': false,
245 | 'express-session': false,
246 | 'passport-oauth2': false,
247 | jsonwebtoken: false,
248 | passport: false,
249 | },
250 | },
251 | }
252 | },
253 | },
254 | endpoints: (incoming.endpoints || []).concat([
255 | {
256 | path: authorizePath,
257 | method: 'get',
258 | root: true,
259 | handler: passport.authenticate(strategyName),
260 | },
261 | {
262 | path: callbackPath,
263 | method: 'get',
264 | root: true,
265 | handler: session(
266 | options.sessionOptions ?? {
267 | resave: false,
268 | saveUninitialized: false,
269 | secret:
270 | process.env.PAYLOAD_SECRET ||
271 | log('Missing process.env.PAYLOAD_SECRET') ||
272 | 'unsafe',
273 | store: options.databaseUri
274 | ? MongoStore.create({ mongoUrl: options.databaseUri })
275 | : undefined,
276 | }
277 | ),
278 | },
279 | {
280 | path: callbackPath,
281 | method: 'get',
282 | root: true,
283 | handler: passport.authenticate(strategyName, { failureRedirect: '/' }),
284 | },
285 | {
286 | path: callbackPath,
287 | method: 'get',
288 | root: true,
289 | async handler(req, res) {
290 | // Get the Mongoose user
291 | const collectionConfig = payload.collections[collectionSlug].config
292 |
293 | // Sanitize the user object
294 | // let user = userDoc.toJSON({ virtuals: true })
295 | let user = JSON.parse(JSON.stringify(req.user))
296 |
297 | // Decide which user fields to include in the JWT
298 | const fieldsToSign = collectionConfig.fields.reduce(
299 | (signedFields, field: Field) => {
300 | const result = {
301 | ...signedFields,
302 | }
303 |
304 | if (!fieldAffectsData(field) && fieldHasSubFields(field)) {
305 | field.fields.forEach((subField) => {
306 | if (fieldAffectsData(subField) && subField.saveToJWT) {
307 | result[subField.name] = user[subField.name]
308 | }
309 | })
310 | }
311 |
312 | if (fieldAffectsData(field) && field.saveToJWT) {
313 | result[field.name] = user[field.name]
314 | }
315 |
316 | return result
317 | },
318 | {
319 | email: user.email,
320 | id: user.id,
321 | collection: collectionConfig.slug,
322 | } as any
323 | )
324 |
325 | // Sign the JWT
326 | const token = jwt.sign(fieldsToSign, payload.secret, {
327 | expiresIn: collectionConfig.auth.tokenExpiration,
328 | })
329 |
330 | // Set cookie
331 | res.cookie(`${payload.config.cookiePrefix}-token`, token, {
332 | path: '/',
333 | httpOnly: true,
334 | expires: getCookieExpiration(collectionConfig.auth.tokenExpiration),
335 | secure: collectionConfig.auth.cookies.secure,
336 | sameSite: collectionConfig.auth.cookies.sameSite,
337 | domain: collectionConfig.auth.cookies.domain || undefined,
338 | })
339 |
340 | // Redirect to the defined path or default to the admin dashboard
341 | res.redirect(options.successRedirect || '/admin')
342 | },
343 | },
344 | ]),
345 | }
346 | }
347 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { type SessionOptions } from 'express-session'
2 | import type { StrategyOptions } from 'passport-oauth2'
3 | import type { ComponentType } from 'react'
4 |
5 | export interface oAuthPluginOptions extends StrategyOptions {
6 | /** Database connection URI in case the lib needs access to database */
7 | databaseUri: string
8 |
9 | /** Options to pass to express-session
10 | * @default
11 | * ```js
12 | * {
13 | * resave: false,
14 | * saveUninitialized: false,
15 | * secret: process.env.PAYLOAD_SECRET,
16 | * store: options.databaseUri
17 | * ? MongoStore.create({ mongoUrl: options.databaseUri })
18 | * : undefined,
19 | * }),
20 | * ```
21 | *
22 | */
23 | sessionOptions?: SessionOptions
24 |
25 | /** Endpoint to handle callback from oauth provider
26 | * Defaults to /oauth/authorize
27 | * Note that this will have /api prepended to it.
28 | * So the default value is actually /api/oauth/authorize
29 | *
30 | * @default /oauth/authorize
31 | */
32 | authorizePath?: string
33 |
34 | /** Map an authentication result to a user */
35 | userinfo: (
36 | accessToken: string,
37 | refreshToken?: string
38 | ) => Promise<{
39 | /** Unique identifier for the linked account */
40 | sub: string
41 | /** Unique identifier for the linked account */
42 | email?: string
43 | /** A password will be generated for new users */
44 | password?: string
45 | /** Example of a custom field */
46 | name?: string
47 | }>
48 |
49 | /** Which path to mount in express, defaults to the path in callbackURL */
50 | callbackPath?: string
51 |
52 | /**
53 | * Text on the sign in button
54 | * @default "Sign in with oAuth"
55 | */
56 | buttonLabel?: string
57 |
58 | components?: {
59 | Button?: false | ((props: ButtonProps) => JSX.Element)
60 | }
61 | userCollection?: {
62 | /** @default "users" */
63 | slug?: string
64 | }
65 | /** If the collection does not have a field with name "sub", it will be created */
66 | subField?: {
67 | /** @default "sub" */
68 | name?: string
69 | }
70 | /** Path or URL to redirect the authenticated user to
71 | * @default /admin
72 | */
73 | successRedirect?: string
74 |
75 | /**
76 | * Boolean to allow updating users on login
77 | * @default false
78 | */
79 | updateOnLogin?: boolean
80 | }
81 |
82 | export type ButtonProps = {
83 | /** Path that initiates the oAuth flow */
84 | authorizePath: string
85 | /** Text on the sign in button */
86 | buttonLabel: string
87 | }
88 |
--------------------------------------------------------------------------------
/test/integration.test.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import express from 'express'
3 | import { Server } from 'node:http'
4 | import { afterAll, beforeAll, expect, test } from 'vitest'
5 | import { spawn } from 'node:child_process'
6 |
7 | const oauth = getOAuthProvider()
8 | const app = getPayloadServer(oauth)
9 |
10 | test('basic sign in', async () => {
11 | // Sanity check
12 | expect(app.url).toMatch(/http:\/\/localhost:\d+/)
13 |
14 | // Build authorize URL
15 | const authorize =
16 | oauth.url +
17 | '/authorize?' +
18 | new URLSearchParams({
19 | redirect_uri: app.url + '/oauth2/callback',
20 | response_type: 'code',
21 | client_id: 'client_id',
22 | }).toString()
23 | const callbackResponse = await axios.get(authorize, {
24 | validateStatus: () => true,
25 | maxRedirects: 0,
26 | })
27 | const callback = callbackResponse.headers.location
28 |
29 | // Authorize should redirect to callback URL with code
30 | expect(callback).toMatch(/http:\/\/localhost:\d+\/oauth2\/callback/)
31 | const code = new URL(callback).searchParams.get('code')
32 | expect(code).toBe('testCode')
33 |
34 | // Exchange code for cookie, redirecting to /admin
35 | const done = await axios.get(callback, {
36 | validateStatus: () => true,
37 | maxRedirects: 0,
38 | })
39 | expect(done.status, callback).toBe(302)
40 | expect(done.headers.location).toBe('/admin')
41 |
42 | // Validate cookie contents
43 | let decoded: any = {}
44 | try {
45 | decoded = done.headers['set-cookie']
46 | decoded = decoded.find((c) => c.startsWith('payload-token='))
47 | decoded = decoded.split(';')[0].split('=')[1].split('.')[1]
48 | decoded = JSON.parse(Buffer.from(decoded, 'base64').toString())
49 | } catch (error) {
50 | console.log('decoded', decoded)
51 | }
52 | expect(decoded).toMatchObject({
53 | email: 'test@example.org',
54 | name: 'existing',
55 | collection: 'users',
56 | })
57 | })
58 |
59 | function getPayloadServer(oauth) {
60 | const ctx: {
61 | url: string
62 | process: ReturnType
63 | } = {
64 | url: '',
65 | // @ts-expect-error
66 | process: null,
67 | }
68 | beforeAll(async () => {
69 | await oauth.ready
70 | await new Promise((resolve) => {
71 | const dev = spawn('node', ['server.js'], {
72 | stdio: 'pipe',
73 | cwd: __dirname + '/payload',
74 | env: {
75 | ...process.env,
76 | DEBUG: 'plugin:oauth:*',
77 | CLIENT_ID: 'client_id',
78 | CLIENT_SECRET: 'client_secret',
79 | AUTHORIZATION_URL: oauth.url + '/authorize',
80 | TOKEN_URL: oauth.url + '/token',
81 | USERINFO_URL: oauth.url + '/userinfo',
82 | },
83 | })
84 | dev.stdout.setEncoding('utf8')
85 | dev.stdout.on('data', (data) => {
86 | const line = data.toString().trim()
87 | if (line.startsWith('http://localhost:')) {
88 | ctx.url = line
89 | resolve(ctx.url)
90 | } else if (
91 | // Reduce clutter while showing unexpected output
92 | !line.includes('in-memory Mongo') &&
93 | !line.includes('webpack') &&
94 | !line.includes('Starting Payload')
95 | ) {
96 | console.log('dev.stdout', data.toString())
97 | }
98 | })
99 |
100 | dev.stderr.setEncoding('utf8')
101 | dev.stderr.on('data', (data) => {
102 | console.log('dev.stderr', data.toString())
103 | })
104 |
105 | ctx.process = dev
106 | })
107 | }, 30000)
108 | afterAll(async () => {
109 | ctx.process.kill()
110 | })
111 |
112 | return ctx
113 | }
114 |
115 | function getOAuthProvider() {
116 | const ctx: {
117 | url: string
118 | server: Server
119 | ready: Promise
120 | } = {
121 | url: '',
122 | // @ts-expect-error
123 | server: null,
124 | // @ts-expect-error
125 | ready: null,
126 | }
127 | beforeAll(async () => {
128 | ctx.ready = new Promise((resolve) => {
129 | const app = express()
130 | app.use(express.json())
131 | app.use(express.urlencoded({ extended: true }))
132 | app.get('/authorize', (req, res) => {
133 | if (req.query.response_type !== 'code')
134 | return res
135 | .status(400)
136 | .json({ error: 'invalid_request', query: req.query })
137 | if (req.query.client_id !== 'client_id')
138 | return res
139 | .status(400)
140 | .json({ error: 'invalid_client', query: req.query })
141 | if (!req.query.redirect_uri)
142 | return res
143 | .status(400)
144 | .json({ error: 'invalid_redirect_uri', query: req.query })
145 | const to = req.query.redirect_uri + '?code=testCode'
146 | // console.log('authorize.redirect', to)
147 | res.redirect(to)
148 | })
149 | app.use('/token', (req, res) => {
150 | // console.log('token', req.body, req.query)
151 | res.json({
152 | access_token: 'a',
153 | refresh_token: 'r',
154 | token_type: 'bearer',
155 | expires_in: 3600,
156 | })
157 | })
158 | app.use('/userinfo', (req, res) => {
159 | if (req.headers.authorization !== 'Bearer a')
160 | return res.status(401).json({ error: 'invalid_token' })
161 | res.json({
162 | sub: 'existing_sub',
163 | })
164 | })
165 | app.get('/callback', async (req, res) => {
166 | console.log('callback on wrong server', req.query)
167 | res.json(req.query)
168 | })
169 | app.use((req, res) => {
170 | console.log('oauth 404', req.url)
171 | res.status(404).json({ url: req.url })
172 | })
173 | ctx.server = app.listen(0, () => resolve(ctx.server))
174 | })
175 |
176 | const server = await ctx.ready
177 |
178 | // Start server
179 | const address = server.address()
180 | if (typeof address === 'string') ctx.url = address
181 | else if (address) ctx.url = 'http://localhost:' + address.port
182 | })
183 | afterAll(async () => {
184 | // Close server
185 | ctx.server.close()
186 | })
187 |
188 | return ctx
189 | }
190 |
--------------------------------------------------------------------------------
/test/payload/payload.config.js:
--------------------------------------------------------------------------------
1 | // import { mongooseAdapter } from '@payloadcms/db-mongodb'
2 | // import { slateEditor } from '@payloadcms/richtext-slate'
3 | // import { buildConfig } from 'payload/config'
4 | // import { oAuthPlugin } from '../../src/index'
5 | const { mongooseAdapter } = require('@payloadcms/db-mongodb')
6 | const { slateEditor } = require('@payloadcms/richtext-slate')
7 | const { buildConfig } = require('payload/config')
8 | const { oAuthPlugin } = require('../..')
9 |
10 | module.exports = buildConfig({
11 | admin: {
12 | disable: true,
13 | },
14 | editor: slateEditor({}),
15 | collections: [
16 | {
17 | slug: 'users',
18 | auth: true,
19 | fields: [{ name: 'name', type: 'text', saveToJWT: true }],
20 | },
21 | ],
22 | plugins: [
23 | oAuthPlugin({
24 | authorizationURL: process.env.AUTHORIZATION_URL,
25 | tokenURL: process.env.TOKEN_URL,
26 | clientID: process.env.CLIENT_ID,
27 | clientSecret: process.env.CLIENT_SECRET,
28 | userinfo: async (accessToken) =>
29 | fetch(process.env.USERINFO_URL, {
30 | headers: { Authorization: `Bearer ${accessToken}` },
31 | }).then((r) => r.json()),
32 | }),
33 | ],
34 | db: mongooseAdapter({ url: '' }),
35 | })
36 |
--------------------------------------------------------------------------------
/test/payload/server.js:
--------------------------------------------------------------------------------
1 | // import express from 'express'
2 | // import payload from 'payload'
3 | const express = require('express')
4 | const payload = require('payload')
5 |
6 | const app = express()
7 |
8 | payload.init({
9 | secret: 'test',
10 | express: app,
11 | onInit: async () => {
12 | await payload.create({
13 | collection: 'users',
14 | data: {
15 | sub: 'existing_sub',
16 | email: 'test@example.org',
17 | password: 'password',
18 | name: 'existing',
19 | },
20 | })
21 | const server = app.listen(0, () =>
22 | console.log(`http://localhost:${server.address().port}`)
23 | )
24 | },
25 | })
26 |
--------------------------------------------------------------------------------
/test/payload/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "strict": false,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "outDir": "./dist",
10 | "jsx": "react",
11 | "paths": {
12 | "payload/generated-types": ["./src/payload-types.ts"]
13 | }
14 | },
15 | "include": ["src", "server.ts"],
16 | "exclude": ["node_modules", "dist", "build"],
17 | "ts-node": {
18 | "transpileOnly": true,
19 | "swc": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2019",
4 | "module": "commonjs",
5 | "allowJs": true /* Allow javascript files to be compiled. */,
6 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
7 | "declaration": true /* Generates corresponding '.d.ts' file. */,
8 | "outDir": "./dist" /* Redirect output structure to the directory. */,
9 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
10 | "strict": true /* Enable all strict type-checking options. */,
11 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
12 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
13 | "skipLibCheck": true /* Skip type checking of declaration files. */,
14 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
15 | },
16 | "include": ["src"],
17 | "exclude": [
18 | "dist",
19 | "build",
20 | "tests",
21 | "**/*.spec.js",
22 | "**/*.spec.ts",
23 | "node_modules",
24 | ".eslintrc.js"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------