diff --git a/package.json b/package.json index e35da3695ce8..ae8bd9aa0d85 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "clean-deps": "find . -depth -name node_modules -type d -exec rm -rf {} \\;", "clean-untracked-files": "git clean -d -f", "codegen": "yarn gulp codegen", + "precypress:open": "yarn ensure-deps", "cypress:open": "yarn gulp open --dev --global", "precypress:open:debug": "yarn ensure-deps", "cypress:open:debug": "node ./scripts/debug.js cypress:open", diff --git a/packages/app/cypress/e2e/settings.cy.ts b/packages/app/cypress/e2e/settings.cy.ts index 245d267a0451..822574a6f0b4 100644 --- a/packages/app/cypress/e2e/settings.cy.ts +++ b/packages/app/cypress/e2e/settings.cy.ts @@ -179,7 +179,7 @@ describe('App: Settings', () => { describe('external editor', () => { beforeEach(() => { cy.startAppServer('e2e') - cy.withCtx(async (ctx, o) => { + cy.withCtx((ctx, o) => { o.sinon.stub(ctx.actions.localSettings, 'setPreferences') o.sinon.stub(ctx.actions.file, 'openFile') ctx.coreData.localSettings.availableEditors = [ diff --git a/packages/app/vue-shims.d.ts b/packages/app/vue-shims.d.ts index 467df21504a5..7c91088b81ca 100644 --- a/packages/app/vue-shims.d.ts +++ b/packages/app/vue-shims.d.ts @@ -1,18 +1,16 @@ declare module 'virtual:*' { - import { Component } from 'vue' + import type { Component } from 'vue' const src: Component export default src } declare module 'virtual:icons/*' { - // eslint-disable-next-line no-duplicate-imports - import { FunctionalComponent, SVGAttributes } from 'vue' + import type { FunctionalComponent, SVGAttributes } from 'vue' const component: FunctionalComponent export default component } declare module '~icons/*' { - // eslint-disable-next-line no-duplicate-imports - import { FunctionalComponent, SVGAttributes } from 'vue' + import type { FunctionalComponent, SVGAttributes } from 'vue' const component: FunctionalComponent export default component } diff --git a/packages/data-context/src/DataContext.ts b/packages/data-context/src/DataContext.ts index 53a35120886e..4b3d4d68b70b 100644 --- a/packages/data-context/src/DataContext.ts +++ b/packages/data-context/src/DataContext.ts @@ -40,6 +40,7 @@ import type { Socket, SocketIOServer } from '@packages/socket' import { globalPubSub } from '.' import { InjectedConfigApi, ProjectLifecycleManager } from './data/ProjectLifecycleManager' import type { CypressError } from '@packages/errors' +import { ErrorDataSource } from './sources/ErrorDataSource' const IS_DEV_ENV = process.env.CYPRESS_INTERNAL_ENV !== 'production' @@ -207,6 +208,11 @@ export class DataContext { return new HtmlDataSource(this) } + @cached + get error () { + return new ErrorDataSource(this) + } + @cached get util () { return new UtilDataSource(this) diff --git a/packages/data-context/src/actions/FileActions.ts b/packages/data-context/src/actions/FileActions.ts index 64ef58c23124..4516bdc79e1b 100644 --- a/packages/data-context/src/actions/FileActions.ts +++ b/packages/data-context/src/actions/FileActions.ts @@ -3,6 +3,7 @@ import path from 'path' // @ts-ignore - no types available import launchEditor from 'launch-editor' import type { DataContext } from '..' +import assert from 'assert' export class FileActions { constructor (private ctx: DataContext) {} @@ -66,9 +67,12 @@ export class FileActions { } } - openFile (absolute: string, line: number = 1, column: number = 1) { + openFile (filePath: string, line: number = 1, column: number = 1) { + assert(this.ctx.currentProject) const binary = this.ctx.coreData.localSettings.preferences.preferredEditorBinary + const absolute = path.resolve(this.ctx.currentProject, filePath) + if (!binary || !absolute) { this.ctx.debug('cannot open file without binary') diff --git a/packages/data-context/src/sources/BrowserDataSource.ts b/packages/data-context/src/sources/BrowserDataSource.ts index ccc71a0ea742..7b0e24c19c30 100644 --- a/packages/data-context/src/sources/BrowserDataSource.ts +++ b/packages/data-context/src/sources/BrowserDataSource.ts @@ -44,9 +44,6 @@ export class BrowserDataSource { }).catch((e) => { this.ctx.update((coreData) => { coreData.machineBrowsers = null - }) - - this.ctx.update((coreData) => { coreData.baseError = e }) diff --git a/packages/data-context/src/sources/ErrorDataSource.ts b/packages/data-context/src/sources/ErrorDataSource.ts new file mode 100644 index 000000000000..f3d848eeae3a --- /dev/null +++ b/packages/data-context/src/sources/ErrorDataSource.ts @@ -0,0 +1,77 @@ +import { ErrorWrapperSource, stackUtils } from '@packages/errors' +import path from 'path' +import _ from 'lodash' + +import type { DataContext } from '..' + +export interface CodeFrameShape { + line: number + column: number + absolute: string + codeBlock: string + codeBlockStartLine: number +} + +export class ErrorDataSource { + constructor (private ctx: DataContext) {} + + isUserCodeError (source: ErrorWrapperSource) { + return Boolean(source.cypressError.originalError && !source.cypressError.originalError?.isCypressErr) + } + + async codeFrame (source: ErrorWrapperSource): Promise { + if (!this.ctx.currentProject || !this.isUserCodeError(source)) { + return null + } + + // If we saw a TSError, we will extract the error location from the message + const tsErrorLocation = source.cypressError.originalError?.tsErrorLocation + + let line: number | null | undefined + let column: number | null | undefined + let absolute: string | null | undefined + + if (tsErrorLocation) { + line = tsErrorLocation.line + column = tsErrorLocation.column + absolute = path.join(this.ctx.currentProject, tsErrorLocation.filePath) + } else { + // Skip any stack trace lines which come from node:internal code + const stackLines = stackUtils.getStackLines(source.cypressError.stack ?? '') + const filteredStackLines = stackLines.filter((stackLine) => !stackLine.includes('node:internal')) + const parsedLine = stackUtils.parseStackLine(filteredStackLines[0] ?? '') + + if (parsedLine) { + absolute = parsedLine.absolute + line = parsedLine.line + column = parsedLine.column + } + } + + if (!absolute || !_.isNumber(line) || !_.isNumber(column)) { + return null + } + + const fileContents = await this.ctx.file.readFile(absolute) + + const lines = fileContents.split('\n') + + const linesAbove = 2 + const linesBelow = 6 + + const startLine = Math.max(1, line - linesAbove) + const endLine = Math.min(lines.length, line + linesBelow) + + // Start & end line start at 1, rather than being zero indexed, so we subtract 1 from the + // line numbers when slicing + const codeBlock = lines.slice(startLine - 1, endLine - 1).join('\n') + + return { + absolute, + line, + column, + codeBlockStartLine: startLine, + codeBlock, + } + } +} diff --git a/packages/data-context/src/sources/index.ts b/packages/data-context/src/sources/index.ts index 894d20cb4dca..5066b3d28f1f 100644 --- a/packages/data-context/src/sources/index.ts +++ b/packages/data-context/src/sources/index.ts @@ -4,6 +4,7 @@ export * from './BrowserDataSource' export * from './CloudDataSource' export * from './EnvDataSource' +export * from './ErrorDataSource' export * from './FileDataSource' export * from './GitDataSource' export * from './GraphQLDataSource' diff --git a/packages/data-context/src/util/urqlCacheKeys.ts b/packages/data-context/src/util/urqlCacheKeys.ts index fcb9e01ec786..fd2a15a251cc 100644 --- a/packages/data-context/src/util/urqlCacheKeys.ts +++ b/packages/data-context/src/util/urqlCacheKeys.ts @@ -26,7 +26,7 @@ export const urqlCacheKeys: Partial = { MigrationFile: () => null, MigrationFilePart: () => null, ErrorWrapper: () => null, - ErrorCodeFrame: () => null, + CodeFrame: () => null, ProjectPreferences: (data) => data.__typename, VersionData: () => null, ScaffoldedFile: () => null, diff --git a/packages/errors/src/errorTypes.ts b/packages/errors/src/errorTypes.ts index d51776d13da3..0a66073edf91 100644 --- a/packages/errors/src/errorTypes.ts +++ b/packages/errors/src/errorTypes.ts @@ -68,9 +68,9 @@ export interface SerializedError extends Omit { return errTemplate` - An unexpected internal error occurred while executing the ${fmt.highlight(mutationField)} operation with payload + An unexpected internal error occurred while executing the ${fmt.highlight(mutationField)} operation with payload: ${fmt.stringify(args)} diff --git a/packages/frontend-shared/src/components/ShikiHighlight.cy.tsx b/packages/frontend-shared/src/components/ShikiHighlight.cy.tsx index 820838eca468..121b291101dd 100644 --- a/packages/frontend-shared/src/components/ShikiHighlight.cy.tsx +++ b/packages/frontend-shared/src/components/ShikiHighlight.cy.tsx @@ -79,4 +79,10 @@ describe('', { viewportWidth: 800, viewportHeight: 500 }, () => cy.get('.shiki').should('be.visible') cy.percySnapshot() }) + + it('show line numbers with initial line when the prop is passed', () => { + cy.mount(() =>
) + cy.get('.shiki').should('be.visible') + cy.percySnapshot() + }) }) diff --git a/packages/frontend-shared/src/components/ShikiHighlight.vue b/packages/frontend-shared/src/components/ShikiHighlight.vue index 7ab3d0bff2a4..aa74f37d8b4f 100644 --- a/packages/frontend-shared/src/components/ShikiHighlight.vue +++ b/packages/frontend-shared/src/components/ShikiHighlight.vue @@ -118,6 +118,7 @@ onBeforeMount(async () => { const props = withDefaults(defineProps<{ code: string + initialLine?: number lang: CyLangType | undefined lineNumbers?: boolean inline?: boolean @@ -131,6 +132,7 @@ const props = withDefaults(defineProps<{ inline: false, wrap: false, copyOnClick: false, + initialLine: 1, copyButton: false, skipTrim: false, class: undefined, @@ -196,7 +198,7 @@ $offset: 1.1em; @apply py-8px; code { counter-reset: step; - counter-increment: step 0; + counter-increment: step calc(v-bind('props.initialLine') - 1); // Keep bg-gray-50 synced with the box-shadows. .line::before, .line:first-child::before { diff --git a/packages/frontend-shared/src/composables/useMarkdown.ts b/packages/frontend-shared/src/composables/useMarkdown.ts index ee95ef99e010..2b08110f5279 100644 --- a/packages/frontend-shared/src/composables/useMarkdown.ts +++ b/packages/frontend-shared/src/composables/useMarkdown.ts @@ -40,8 +40,8 @@ const defaultClasses = { h5: ['font-medium', 'text-sm', 'mb-3'], h6: ['font-medium', 'text-xs', 'mb-3'], p: ['my-3 first:mt-0 text-sm mb-4'], - pre: ['rounded p-3 bg-white'], - code: [`font-medium rounded text-sm px-4px py-2px bg-white`], + pre: ['rounded p-3 bg-white mb-2'], + code: [`font-medium rounded text-sm px-4px py-2px bg-red-100`], a: ['text-blue-500', 'hover:underline text-sm'], ul: ['list-disc pl-6 my-3 text-sm'], ol: ['list-decimal pl-6 my-3 text-sm'], diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 9e2ff1144a34..c93a0e193636 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -317,6 +317,24 @@ type CloudUser implements Node { userIsViewer: Boolean! } +""" +A code frame to display for a file, used when displaying code related to errors +""" +type CodeFrame { + """Source of the code frame to display""" + codeBlock: String + + """The line number to display in the code block""" + codeBlockStartLine: Int + + """The column of the error to display""" + column: Int + file: FileParts! + + """The line number of the code snippet to display""" + line: Int +} + """Glob patterns for detecting files for code gen.""" type CodeGenGlobs implements Node { component: String! @@ -455,10 +473,6 @@ type Editor { name: String! } -type ErrorCodeFrame { - filename: String -} - enum ErrorTypeEnum { AUTOMATION_SERVER_DISCONNECTED BAD_POLICY_WARNING @@ -578,7 +592,8 @@ enum ErrorTypeEnum { """Base error""" type ErrorWrapper { - codeFrame: ErrorCodeFrame + """The code frame to display in relation to the error""" + codeFrame: CodeFrame """The markdown formatted content associated with the ErrorTypeEnum""" errorMessage: String! @@ -586,13 +601,12 @@ type ErrorWrapper { """Name of the error class""" errorName: String! - """The error stack of either the original error from the user""" + """ + The error stack of either the original error from the user or from where the internal Cypress error was created + """ errorStack: String! errorType: ErrorTypeEnum! - """Relative file path to open, if there is one associated with this error""" - fileToOpen: FileParts - """ Whether the error came from user code, can be used to determine whether to open a stack trace by default """ @@ -977,7 +991,7 @@ type Mutation { reconfigureProject: Boolean! """Re-initializes Cypress from the initial CLI options""" - reinitializeCypress: Boolean + reinitializeCypress: Query """Remove project from projects array and cache""" removeProject(path: String!): Query diff --git a/packages/graphql/src/plugins/nexusMutationErrorPlugin.ts b/packages/graphql/src/plugins/nexusMutationErrorPlugin.ts index ea599a27c37a..9241e03f128a 100644 --- a/packages/graphql/src/plugins/nexusMutationErrorPlugin.ts +++ b/packages/graphql/src/plugins/nexusMutationErrorPlugin.ts @@ -13,13 +13,13 @@ export const mutationErrorPlugin = plugin({ return (source, args, ctx: DataContext, info, next) => { return plugin.completeValue(next(source, args, ctx, info), (v) => v, (err) => { - if (!err.isCypressError) { - ctx.update((d) => { - d.baseError = { - cypressError: getError('UNEXPECTED_MUTATION_ERROR', def.fieldConfig.name, args, err), - } - }) - } + ctx.update((d) => { + d.baseError = { + cypressError: err.isCypressErr + ? err + : getError('UNEXPECTED_MUTATION_ERROR', def.fieldConfig.name, args, err), + } + }) const returnType = getNamedType(info.returnType) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-CodeFrame.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-CodeFrame.ts new file mode 100644 index 000000000000..5a5747e9da24 --- /dev/null +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-CodeFrame.ts @@ -0,0 +1,35 @@ +import { objectType } from 'nexus' +import { FileParts } from './gql-FileParts' + +export const CodeFrame = objectType({ + name: 'CodeFrame', + description: 'A code frame to display for a file, used when displaying code related to errors', + definition (t) { + t.int('line', { + description: 'The line number of the code snippet to display', + }) + + t.int('column', { + description: 'The column of the error to display', + }) + + t.string('codeBlock', { + description: 'Source of the code frame to display', + }) + + t.int('codeBlockStartLine', { + description: 'The line number to display in the code block', + }) + + t.nonNull.field('file', { + type: FileParts, + resolve (source, args, ctx) { + return { absolute: source.absolute } + }, + }) + }, + sourceType: { + module: '@packages/data-context/src/sources/ErrorDataSource', + export: 'CodeFrameShape', + }, +}) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-ErrorWrapper.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-ErrorWrapper.ts index aed38616e615..c50affaf16eb 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-ErrorWrapper.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-ErrorWrapper.ts @@ -1,9 +1,8 @@ -import { stripAnsi, stackUtils, ErrorWrapperSource } from '@packages/errors' +import { stripAnsi } from '@packages/errors' import { objectType } from 'nexus' -import path from 'path' import { ErrorTypeEnum } from '../enumTypes/gql-ErrorTypeEnum' -import { FileParts } from './gql-FileParts' +import { CodeFrame } from './gql-CodeFrame' export const ErrorWrapper = objectType({ name: 'ErrorWrapper', @@ -21,7 +20,7 @@ export const ErrorWrapper = objectType({ }) t.nonNull.string('errorStack', { - description: 'The error stack of either the original error from the user', + description: 'The error stack of either the original error from the user or from where the internal Cypress error was created', resolve (source) { return stripAnsi(source.cypressError.stack || '') }, @@ -41,51 +40,15 @@ export const ErrorWrapper = objectType({ t.nonNull.boolean('isUserCodeError', { description: 'Whether the error came from user code, can be used to determine whether to open a stack trace by default', - resolve (source) { - return isUserCodeError(source) - }, - }) - - t.field('fileToOpen', { - type: FileParts, - description: 'Relative file path to open, if there is one associated with this error', resolve (source, args, ctx) { - if (isUserCodeError(source)) { - const tsErrorLocation = source.cypressError.originalError?.tsErrorLocation - const tsErrorPath = tsErrorLocation?.filePath - - if (tsErrorPath && ctx.currentProject) { - return { - absolute: path.join(ctx.currentProject, tsErrorPath), - column: tsErrorLocation?.column, - line: tsErrorLocation?.line, - } - } - - const stackLines = stackUtils.getStackLines(source.cypressError.stack ?? '') - - const filteredStackLines = stackLines.filter((stackLine) => !stackLine.includes('node:internal')) - - return stackUtils.parseStackLine(filteredStackLines[0] ?? '') - } - - return null + return ctx.error.isUserCodeError(source) }, }) t.field('codeFrame', { - type: ErrorCodeFrame, - resolve: (source) => { - if (isUserCodeError(source)) { - const stackLines = stackUtils.getStackLines(source.cypressError.stack ?? '') - - return { filename: stackUtils.parseStackLine(stackLines[0] ?? '')?.absolute } - } - - return { - filename: __filename, - } - }, + type: CodeFrame, + description: 'The code frame to display in relation to the error', + resolve: (source, args, ctx) => ctx.error.codeFrame(source), }) }, sourceType: { @@ -93,14 +56,3 @@ export const ErrorWrapper = objectType({ export: 'ErrorWrapperSource', }, }) - -function isUserCodeError (source: ErrorWrapperSource) { - return Boolean(source.cypressError.originalError && !source.cypressError.originalError?.isCypressErr) -} - -export const ErrorCodeFrame = objectType({ - name: 'ErrorCodeFrame', - definition (t) { - t.string('filename') - }, -}) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-FileParts.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-FileParts.ts index e44f5a57b585..a101fdd4b298 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-FileParts.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-FileParts.ts @@ -5,7 +5,8 @@ export interface FilePartsShape { line?: number column?: number absolute: string - // For when we're merging the file + // For when we're merging / creating the file and might not have the file contents on-disk yet + // used in the scaffolding contents?: string } diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index 10fcdb5616a9..c8753c56d75f 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -12,12 +12,12 @@ import { ScaffoldedFile } from './gql-ScaffoldedFile' export const mutation = mutationType({ definition (t) { t.field('reinitializeCypress', { - type: 'Boolean', + type: 'Query', description: 'Re-initializes Cypress from the initial CLI options', resolve: async (_, args, ctx) => { await ctx.reinitializeCypress(ctx.modeOptions) - return true + return {} }, }) diff --git a/packages/graphql/src/schemaTypes/objectTypes/index.ts b/packages/graphql/src/schemaTypes/objectTypes/index.ts index e4fe2bd80fe6..91ac5a4eff12 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/index.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/index.ts @@ -3,6 +3,7 @@ export * from './gql-AuthState' export * from './gql-Browser' +export * from './gql-CodeFrame' export * from './gql-CodeGenGlobs' export * from './gql-CurrentProject' export * from './gql-DevState' diff --git a/packages/launchpad/src/Main.vue b/packages/launchpad/src/Main.vue index aa307b1fc81e..e443b920d179 100644 --- a/packages/launchpad/src/Main.vue +++ b/packages/launchpad/src/Main.vue @@ -126,7 +126,9 @@ query MainLaunchpadQuery { gql` mutation Main_ReinitializeCypress { - reinitializeCypress + reinitializeCypress { + ...MainLaunchpadQueryData + } } ` diff --git a/packages/launchpad/src/error/BaseError.cy.tsx b/packages/launchpad/src/error/BaseError.cy.tsx index 1b2cb3d4457e..3ad9e75e9edd 100644 --- a/packages/launchpad/src/error/BaseError.cy.tsx +++ b/packages/launchpad/src/error/BaseError.cy.tsx @@ -1,6 +1,7 @@ import BaseError from './BaseError.vue' import Button from '@cy/components/Button.vue' import { BaseErrorFragmentDoc } from '../generated/graphql-test' +import dedent from 'dedent' // Selectors const headerSelector = '[data-testid=error-header]' @@ -112,13 +113,24 @@ describe('', () => { cy.mountFragment(BaseErrorFragmentDoc, { onResult: (result) => { result.title = messages.header - result.fileToOpen = { - __typename: 'FileParts', - id: '123', - relative: 'cypress/e2e/file.cy.js', + result.codeFrame = { + __typename: 'CodeFrame', line: 12, column: 25, - absolute: '/absolute/full/path/cypress/e2e/file.cy.js', + codeBlockStartLine: 10, + codeBlock: dedent` + const x = 1; + + throw new Error("Some Error") + + const y = 1; + `, + file: { + id: `FileParts:/absolute/full/path/cypress/e2e/file.cy.js`, + __typename: 'FileParts', + relative: 'cypress/e2e/file.cy.js', + absolute: '/absolute/full/path/cypress/e2e/file.cy.js', + }, } }, render: (gqlVal) => ( diff --git a/packages/launchpad/src/error/BaseError.vue b/packages/launchpad/src/error/BaseError.vue index 6c4080ef3b0b..aa7691e7b838 100644 --- a/packages/launchpad/src/error/BaseError.vue +++ b/packages/launchpad/src/error/BaseError.vue @@ -34,8 +34,8 @@ v-html="markdown" />
@@ -15,21 +15,34 @@ {{ fileText }}
+ diff --git a/packages/launchpad/vue-shims.d.ts b/packages/launchpad/vue-shims.d.ts index 467df21504a5..6dbd5d0a2aa2 100644 --- a/packages/launchpad/vue-shims.d.ts +++ b/packages/launchpad/vue-shims.d.ts @@ -1,18 +1,18 @@ declare module 'virtual:*' { - import { Component } from 'vue' + import type { Component } from 'vue' const src: Component export default src } declare module 'virtual:icons/*' { // eslint-disable-next-line no-duplicate-imports - import { FunctionalComponent, SVGAttributes } from 'vue' + import type { FunctionalComponent, SVGAttributes } from 'vue' const component: FunctionalComponent export default component } declare module '~icons/*' { // eslint-disable-next-line no-duplicate-imports - import { FunctionalComponent, SVGAttributes } from 'vue' + import type { FunctionalComponent, SVGAttributes } from 'vue' const component: FunctionalComponent export default component } diff --git a/packages/server/lib/plugins/child/run_require_async_child.js b/packages/server/lib/plugins/child/run_require_async_child.js index 93fa120246a2..db7f6f3ee50a 100644 --- a/packages/server/lib/plugins/child/run_require_async_child.js +++ b/packages/server/lib/plugins/child/run_require_async_child.js @@ -147,7 +147,7 @@ function run (ipc, configFile, projectRoot) { const tsErrorRegex = /\n(.*?)\((\d+),(\d+)\):/g const failurePath = tsErrorRegex.exec(cleanMessage) - err.tsErrorLocation = failurePath ? { filePath: failurePath[1], column: failurePath[2], line: failurePath[3] } : null + err.tsErrorLocation = failurePath ? { filePath: failurePath[1], line: Number(failurePath[2]), column: Number(failurePath[3]) } : null err.originalMessage = err.message err.message = cleanMessage }