Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: login and logout commands #118

Merged
merged 48 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5854401
feat: api client logic
alvarosabu Sep 18, 2024
55f33fa
chore: lint fix
alvarosabu Sep 18, 2024
81047b7
feat: netrc credentials logic
alvarosabu Sep 23, 2024
316ce74
chore: remove unused imports on creds tests
alvarosabu Sep 23, 2024
193e1b6
feat: migrate to inquirer/prompts
alvarosabu Sep 23, 2024
303b248
feat: handling path for package.json on tests
alvarosabu Sep 23, 2024
7254e74
feat: check region on login command
alvarosabu Sep 25, 2024
56960ee
feat: login with token and with email
alvarosabu Sep 26, 2024
c908176
feat: improved error handling
alvarosabu Oct 2, 2024
fe4733d
tests: test coverage for login actions
alvarosabu Oct 2, 2024
1cbd2be
test: use correct masktoken fn
alvarosabu Oct 2, 2024
c60f830
tests: removed unused ci option since token acts like one
alvarosabu Oct 2, 2024
9740997
tests: login actions tests
alvarosabu Oct 2, 2024
5755511
tests: login --token tests
alvarosabu Oct 2, 2024
7c83aed
tests: login command tests with different login strategies
alvarosabu Oct 2, 2024
eda9600
tests: coverage
alvarosabu Oct 2, 2024
b883805
chore: minor test fix
alvarosabu Oct 2, 2024
ba7e913
tests: remove commented code
alvarosabu Oct 2, 2024
33ec33a
feat: isAutorized
alvarosabu Oct 2, 2024
9f49385
feat: logout command
alvarosabu Oct 3, 2024
0989948
feat: session credentials handler and updated tests
alvarosabu Oct 3, 2024
32ff580
chore: fix lint
alvarosabu Oct 3, 2024
fb0aa16
chore: improve auth code login message
alvarosabu Oct 4, 2024
54df30e
chore: increase session coverage
alvarosabu Oct 4, 2024
7e448ae
feat: narrow the typesafe of constants
alvarosabu Oct 9, 2024
779d41a
feat(types): improved regions code typing
alvarosabu Oct 9, 2024
aece279
feat: set module resolution to node for tsconfig and add eslint flat …
alvarosabu Oct 10, 2024
d4ecf66
chore: force npm run dev to always stub the build for dev
alvarosabu Oct 10, 2024
64ef7db
chore(tests): type assertion for mocks
alvarosabu Oct 10, 2024
dc677f9
chore: extremely weird choice of identation
alvarosabu Oct 10, 2024
76cba51
feat: improve error handling
alvarosabu Oct 14, 2024
de2f3bb
tests: vi.mocked for the typescript win
alvarosabu Oct 14, 2024
32eb04b
chore: remove unused code for linter
alvarosabu Oct 14, 2024
d786b9c
Merge branch 'next' into feature/login-cmd
alvarosabu Oct 14, 2024
7449f5f
feat: improved error handling part 1, api errors and command errors
alvarosabu Oct 15, 2024
90ccd57
tests: update tests and use msw for api mocks
alvarosabu Oct 16, 2024
2b48927
chore: lint and removing unnecesary else
alvarosabu Oct 16, 2024
4c3582a
chore: lint
alvarosabu Oct 16, 2024
658202e
feat: refactor removeNetrcEntry to make machineName required
alvarosabu Oct 16, 2024
c1f0eef
feat: change order of `removeNetrcEntry` params
alvarosabu Oct 16, 2024
f7a4910
feat: remove casting in favor of runtime checking
alvarosabu Oct 16, 2024
0201fad
feat: verbose mode
alvarosabu Oct 16, 2024
432b641
feat: added correct flow when user doesnt require otp
alvarosabu Oct 29, 2024
72d788f
feat: handle user cancelation error when inquirer is prompting
alvarosabu Oct 29, 2024
1860665
feat: remove all netrc entries on logout
alvarosabu Oct 29, 2024
c63d382
feat: added http response code to api error verbose mode
alvarosabu Oct 29, 2024
3235497
feat: remove unnecesary netrc handling warnings
alvarosabu Oct 29, 2024
c8472d0
feat: improved error handling for file permissions
alvarosabu Oct 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "storyblok",
"type": "module",
"version": "0.0.0",
"packageManager": "[email protected]",
"description": "Storyblok CLI",
Expand Down
27 changes: 15 additions & 12 deletions src/commands/login/actions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import chalk from 'chalk'
import type { RegionCode } from '../../constants'
import { regionsDomain } from '../../constants'
import type { FetchError } from 'ofetch'
import { ofetch } from 'ofetch'
import { maskToken } from '../../utils'
import { FetchError, ofetch } from 'ofetch'
import { APIError, handleAPIError, maskToken } from '../../utils'

export const loginWithToken = async (token: string, region: RegionCode) => {
try {
Expand All @@ -14,15 +13,19 @@ export const loginWithToken = async (token: string, region: RegionCode) => {
})
}
catch (error) {
if ((error as FetchError).response?.status === 401) {
throw new Error(`The token provided ${chalk.bold(maskToken(token))} is invalid: ${chalk.bold(`401 ${(error as FetchError).data.error}`)}
if (error instanceof FetchError) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this not handled by handleAPIError as the others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edodusi because it has a custom message for 401

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't you do it directly in handleAPIError by using a conditional with login_with_token? This way it should be easier to maintain/extend

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not to make handleApiError flexible, the scope of that function is to handle the error with general error messages.

What if I want to pass a custom message for 422 instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the responsibility for printing messages should be of handleAPIError, and the message to pick can depend on different factors: the http status, the action, the error, even the region potentially.

I would apply the single-responsibility principle, these actions should only handle the actual action to perform, not care about how errors are handled, that is the responsibility of another function or class.

Copy link
Contributor Author

@alvarosabu alvarosabu Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @edodusi I don't think I follow.

The responsibility of handleAPIError is to throw an instance of APIError with the correspondent error message depending on the response status, it's not a factory.

export function handleAPIError(action: keyof typeof API_ACTIONS, error: Error): void {
  if (error instanceof FetchError) {
    const status = error.response?.status

    switch (status) {
      case 401:
        throw new APIError('unauthorized', action, error)
      case 422:
        throw new APIError('invalid_credentials', action, error)
      default:
        throw new APIError('network_error', action, error)
    }
  }
  else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
    throw new APIError('network_error', action, error)
  }
  throw new APIError('generic', action, error)
}

Then this is caught by handleError at top level, whose responsibility is to check what type of error it is to be able to call konsola with the right prefix in case the flag verbose is included.

export function handleError(error: Error, verbose = false): void {
  // If verbose flag is true and the error has getInfo method
  if (verbose && typeof (error as any).getInfo === 'function') {
    const errorDetails = (error as any).getInfo()
    if (error instanceof CommandError) {
      konsola.error(`Command Error: ${error.getInfo().message}`, errorDetails)
    }
    else if (error instanceof APIError) {
      konsola.error(`API Error: ${error.getInfo().cause}`, errorDetails)
    }
    else if (error instanceof FileSystemError) {
      konsola.error(`File System Error: ${error.getInfo().cause}`, errorDetails)
    }
    else {
      konsola.error(`Unexpected Error: ${error.message}`, errorDetails)
    }
  }
  else {
    // Print the message stack if it exists
    if ((error as any).messageStack) {
      const messageStack = (error as any).messageStack
      messageStack.forEach((message: string, index: number) => {
        konsola.error(message, null, {
          header: index === 0,
          margin: false,
        })
      })
      konsola.br()
      konsola.info('For more information about the error, run the command with the `--verbose` flag')
    }
    else {
      konsola.error(error.message, null, {
        header: true,
      })
    }
  }

  if (!process.env.VITEST) {
    console.log('') // Add a line break for readability
    process.exit(1) // Exit process if not in a test environment
  }
}

Imagine you want to add an icon to each and every error messages (absurd idea), in this scenario you should be able to do this just by changing a single file, instead in this way you are forced to change at least two

Wouldn't be enough to change the error message format once on the konsola file?

error: (message: string, info?: unknown, options?: KonsolaFormatOptions) => {
    if (options?.header) {
      const errorHeader = chalk.bgRed.bold.white(` Error `)
      console.error(formatHeader(errorHeader))
      console.log('') // Add a line break
    }

    console.error(`${chalk.red.bold('▲ error')} ${message}`, info || '')
    if (options?.margin) {
      console.error('') // Add a line break
    }
  },

Copy link
Contributor Author

@alvarosabu alvarosabu Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edodusi think of it like this:

  • handleAPIError throws an APIError depending on the HTTP response code
  • handleFilesystemError throws an FileSystemError depending on the filesystem operation code
  • handleError top-level that catches all errors thrown in the CLI and sends them to the konsola
  • konsola prompt formatting and UI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alvarosabu if this last comment was true I would expect APIError to be raised exclusively by handleAPIError, and instead you throw it also in the action handler, that's what I'm saying.

I used the factory as an analogy as it's the class responsible for instantiating object, in this case it's responsible for throwing errors.

But the point is, and this is only because the code is well architected that this pops out to me, there is an exception to this pattern, that is that an action is throwing its custom errors instead of delegating this responsibility to handleAPIError.

If the code was just made of catch blocks that throw errors with custom messages and log something and do their things I would not have argued, but since you have this design pattern and I think it's good and extensible and easy to maintain, then (to me) the logical consequence is to handle everything about errors in handleAPIError.

You say this is an exception because it has to print its own custom message, ok, then if tomorrow we add another action we have to think where to put the custom message, instead a good design means we don't have to think, we simply follow, extend, etc...

Copy link
Contributor Author

@alvarosabu alvarosabu Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @edodusi thanks for the explanation.

If the code were just made of catch blocks that throw errors with custom messages and log something and do their things I would not have argued, but since you have this design pattern. I think it's good and extensible and easy to maintain, then (to me) the logical consequence is to handle everything about errors in handleAPIError.

I think I now get completely your point thanks fro clarifying. I would rather treat the handleAPIError as a utility rather than a required step in the flow of error handling, it's a DRY abstraction that solves 1 problem Apply generic status messages depending on the response status

Sometimes you need to use the utility, and sometimes you don't, really depends on the need, in this case, the need is to take control and send a custom message directly.

I personally think that passing the custom messages through the handleAPIError would be cumbersome and will add complexity we don't need to that utility.

My next question would be, do you consider this a blocker for the merge? Or it's something we can discuss and evaluate in the future?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point. To me this is NOT a blocker, probably I just have a slightly different view of the architecture of this code but we can discuss, and probably with future improvements we will better align.

Thanks for the great discussion, I love to see deep thoughts on software design ❤️

You can resolve this conversation, and soon as we need to extend this behavior we will think of the best way.

const status = error.response?.status

Please make sure you are using the correct token and try again.`)
switch (status) {
case 401:
throw new APIError('unauthorized', 'login_with_token', error, `The token provided ${chalk.bold(maskToken(token))} is invalid: ${chalk.bold(`401 ${(error as FetchError).data.error}`)}
Please make sure you are using the correct token and try again.`)
case 422:
throw new APIError('invalid_credentials', 'login_with_token', error)
default:
throw new APIError('network_error', 'login_with_token', error)
}
}
if (!(error as FetchError).response) {
throw new Error('No response from server, please check if you are correctly connected to internet', error as Error)
}
throw new Error('Error logging with token', error as Error)
}
}

Expand All @@ -34,7 +37,7 @@ export const loginWithEmailAndPassword = async (email: string, password: string,
})
}
catch (error) {
throw new Error('Error logging in with email and password', error as Error)
handleAPIError('login_email_password', error as Error)
}
}

Expand All @@ -46,6 +49,6 @@ export const loginWithOtp = async (email: string, password: string, otp: string,
})
}
catch (error) {
throw new Error('Error logging in with email, password and otp', error as Error)
handleAPIError('login_with_otp', error as Error)
}
}
7 changes: 3 additions & 4 deletions src/commands/login/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { input, password, select } from '@inquirer/prompts'
import type { RegionCode } from '../../constants'
import { commands, regionNames, regions, regionsDomain } from '../../constants'
import { getProgram } from '../../program'
import { handleError, isRegion, konsola } from '../../utils'
import { CommandError, handleError, isRegion, konsola } from '../../utils'
import { loginWithEmailAndPassword, loginWithOtp, loginWithToken } from './actions'

import { session } from '../../session'
Expand Down Expand Up @@ -40,9 +40,10 @@ export const loginCommand = program
token: string
region: RegionCode
}) => {
konsola.title(` ${commands.LOGIN} `, '#8556D3')
const { token, region } = options
if (!isRegion(region)) {
handleError(new Error(`The provided region: ${region} is not valid. Please use one of the following values: ${Object.values(regions).join(' | ')}`), true)
handleError(new CommandError(`The provided region: ${region} is not valid. Please use one of the following values: ${Object.values(regions).join(' | ')}`))
}

const { state, updateSession, persistCredentials, initializeSession } = session()
Expand All @@ -67,8 +68,6 @@ export const loginCommand = program
}
}
else {
konsola.title(` ${commands.LOGIN} `, '#8556D3')

const strategy = await select(loginStrategy)
try {
if (strategy === 'login-with-token') {
Expand Down
14 changes: 12 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import { getProgram } from './program'
import './commands/login'
import './commands/logout'
import { APIError } from './utils/error/api-error'

Check failure on line 9 in src/index.ts

View workflow job for this annotation

GitHub Actions / Lint (20)

'APIError' is defined but never used
import { loginWithEmailAndPassword, loginWithToken } from './commands/login/actions'

Check failure on line 10 in src/index.ts

View workflow job for this annotation

GitHub Actions / Lint (20)

'loginWithEmailAndPassword' is defined but never used

dotenv.config() // This will load variables from .env into process.env
const program = getProgram()
Expand All @@ -16,15 +18,23 @@
${introText} ${messageText}`))

program.option('-s, --space [value]', 'space ID')
program.option('-v, --verbose', 'Enable verbose output')

program.on('command:*', () => {
console.error(`Invalid command: ${program.args.join(' ')}`)
program.help()
konsola.br() // Add a line break
})

program.command('test').action(async () => {
handleError(new Error('There was an error with credentials'), true)
program.command('test').action(async (options) => {

Check failure on line 29 in src/index.ts

View workflow job for this annotation

GitHub Actions / Lint (20)

'options' is defined but never used. Allowed unused args must match /^_/u
konsola.title(`Test`, '#8556D3', 'Attempting a test...')
try {
// await loginWithEmailAndPassword('aw', 'passwrod', 'eu')
await loginWithToken('WYSYDHYASDHSYD', 'eu')
}
catch (error) {
handleError(error as Error)
}
})

/* console.log(`
alvarosabu marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
9 changes: 0 additions & 9 deletions src/utils/error.ts

This file was deleted.

64 changes: 64 additions & 0 deletions src/utils/error/api-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FetchError } from 'ofetch'

export const API_ACTIONS = {
login: 'login',
login_with_token: 'Failed to log in with token',
login_with_otp: 'Failed to log in with email, password and otp',
login_email_password: 'Failed to log in with email and password',
} as const

export const API_ERRORS = {
unauthorized: 'The user is not authorized to access the API',
network_error: 'No response from server, please check if you are correctly connected to internet',
invalid_credentials: 'The provided credentials are invalid',
timeout: 'The API request timed out',
} as const

export function handleAPIError(action: keyof typeof API_ACTIONS, error: Error): void {
if (error instanceof FetchError) {
const status = error.response?.status

switch (status) {
case 401:
throw new APIError('unauthorized', action, error)
case 422:
throw new APIError('invalid_credentials', action, error)
default:
throw new APIError('network_error', action, error)
}
}
else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
throw new APIError('network_error', action, error)
}
else {
alvarosabu marked this conversation as resolved.
Show resolved Hide resolved
throw new APIError('timeout', action, error)
}
}

export class APIError extends Error {
errorId: string
cause: string
code: number
messageStack: string[]
error: FetchError | undefined

constructor(errorId: keyof typeof API_ERRORS, action: keyof typeof API_ACTIONS, error?: FetchError, customMessage?: string) {
super(customMessage || API_ERRORS[errorId])
this.name = 'API Error'
this.errorId = errorId
this.cause = API_ERRORS[errorId]
this.code = error?.response?.status || 0
this.messageStack = [API_ACTIONS[action], customMessage || API_ERRORS[errorId]]
this.error = error
}

getInfo() {
return {
name: this.name,
message: this.message,
cause: this.cause,
errorId: this.errorId,
stack: this.stack,
}
}
}
14 changes: 14 additions & 0 deletions src/utils/error/command-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export class CommandError extends Error {
constructor(message: string) {
super(message)
this.name = 'Command Error'
}

getInfo() {
return {
name: this.name,
message: this.message,
stack: this.stack,
}
}
}
File renamed without changes.
48 changes: 48 additions & 0 deletions src/utils/error/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { konsola } from '..'
import { APIError } from './api-error'
import { CommandError } from './command-error'
import { FileSystemError } from './filesystem-error'

export function handleError(error: Error, verbose = false): void {
// If verbose flag is true and the error has getInfo method
if (verbose && typeof (error as any).getInfo === 'function') {
const errorDetails = (error as any).getInfo()
if (error instanceof CommandError) {
konsola.error(`Command Error: ${error.getInfo().message}`, errorDetails)
}
else if (error instanceof APIError) {
console.log('error', error)
konsola.error(`API Error: ${error.getInfo().cause}`, errorDetails)
}
else if (error instanceof FileSystemError) {
konsola.error(`File System Error: ${error.getInfo().cause}`, errorDetails)
}
else {
konsola.error(`Unexpected Error: ${error.message}`, errorDetails)
}
}
else {
// Print the message stack if it exists
if ((error as any).messageStack) {
const messageStack = (error as any).messageStack
messageStack.forEach((message: string, index: number) => {
konsola.error(message, null, {
header: index === 0,
margin: false,
})
})
konsola.br()
konsola.info('For more information about the error, run the command with the `--verbose` flag')
}
else {
konsola.error(error.message, null, {
header: true,
})
}
}

if (!process.env.VITEST) {
console.log('') // Add a line break for readability
process.exit(1) // Exit process if not in a test environment
}
}
26 changes: 26 additions & 0 deletions src/utils/error/filesystem-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const FS_ERRORS = {
file_not_found: 'The file requested was not found',
permission_denied: 'Permission denied while accessing the file',
}

export class FileSystemError extends Error {
errorId: string
cause: string

constructor(message: string, errorId: keyof typeof FS_ERRORS) {
super(message)
this.name = 'File System Error'
this.errorId = errorId
this.cause = FS_ERRORS[errorId]
}

getInfo() {
return {
name: this.name,
message: this.message,
cause: this.cause,
errorId: this.errorId,
stack: this.stack,
}
}
}
4 changes: 4 additions & 0 deletions src/utils/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './api-error'
export * from './command-error'
export * from './error'
export * from './filesystem-error'
2 changes: 1 addition & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { dirname } from 'pathe'
import type { RegionCode } from '../constants'
import { regions } from '../constants'

export * from './error'
export * from './error/'
export * from './konsola'
export const __filename = fileURLToPath(import.meta.url)
export const __dirname = dirname(__filename)
Expand Down
48 changes: 37 additions & 11 deletions src/utils/konsola.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import chalk from 'chalk'

export interface KonsolaFormatOptions {
header?: boolean
margin?: boolean
}

export function formatHeader(title: string) {
return `${title}

`
return `${title}`
}
export const konsola = {
title: (message: string, color: string) => {
console.log(formatHeader(chalk.bgHex(color).bold.white(` ${message} `)))
title: (message: string, color: string, subtitle?: string) => {
console.log('') // Add a line break
console.log('') // Add a line break
console.log(`${formatHeader(chalk.bgHex(color).bold.white(` ${message} `))} ${subtitle || ''}`)
console.log('') // Add a line break
console.log('') // Add a line break
},
br: () => {
console.log('') // Add a line break
Expand All @@ -21,6 +28,21 @@ export const konsola = {

console.log(message ? `${chalk.green('✔')} ${message}` : '')
},
info: (message: string, options: KonsolaFormatOptions = {
header: false,
margin: true,
}) => {
if (options.header) {
console.log('') // Add a line break
const infoHeader = chalk.bgBlue.bold.white(` Info `)
console.log(formatHeader(infoHeader))
}

console.log(message ? `${chalk.blue('ℹ')} ${message}` : '')
if (options.margin) {
console.error('') // Add a line break
}
},
warn: (message?: string, header: boolean = false) => {
if (header) {
console.log('') // Add a line break
Expand All @@ -30,15 +52,19 @@ export const konsola = {

console.warn(message ? `${chalk.yellow('⚠️')} ${message}` : '')
},
error: (err: Error, header: boolean = false) => {
if (header) {
console.log('') // Add a line break
error: (message: string, info: unknown, options: KonsolaFormatOptions = {
header: false,
margin: true,
}) => {
if (options.header) {
const errorHeader = chalk.bgRed.bold.white(` Error `)
console.error(formatHeader(errorHeader))
console.error('a111') // Add a line break
console.log('') // Add a line break
}

console.error(`${chalk.red('x')} ${err.message || err}`)
console.log('') // Add a line break
console.error(`${chalk.red.bold('▲ error')} ${message}`, info || '')
if (options.margin) {
console.error('') // Add a line break
}
},
}
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
"lib": ["esnext", "DOM", "DOM.Iterable"],
"baseUrl": ".",
"module": "esnext",

"moduleResolution": "Node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"types": ["node", "vitest/globals"],
"allowImportingTsExtensions": true,
Expand All @@ -16,6 +15,7 @@
"noUnusedParameters": true,
"noEmit": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true
},
Expand Down
Loading