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

perf: regexp perf issues, refactor regexp stylistic issues #10905

Merged
merged 10 commits into from Nov 14, 2022
9 changes: 6 additions & 3 deletions .eslintrc.cjs
Expand Up @@ -7,9 +7,10 @@ module.exports = defineConfig({
extends: [
'eslint:recommended',
'plugin:node/recommended',
'plugin:@typescript-eslint/recommended'
'plugin:@typescript-eslint/recommended',
'plugin:regexp/recommended'
],
plugins: ['import'],
plugins: ['import', 'regexp'],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
Expand Down Expand Up @@ -97,7 +98,9 @@ module.exports = defineConfig({
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
allowSeparatedGroups: false
}
]
],

'regexp/no-contradiction-with-assertion': 'error'
},
overrides: [
{
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -66,6 +66,7 @@
"eslint-define-config": "^1.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-regexp": "^1.10.0",
"execa": "^6.1.0",
"fast-glob": "^3.2.12",
"fs-extra": "^10.1.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/create-vite/src/index.ts
Expand Up @@ -388,7 +388,7 @@ function copy(src: string, dest: string) {
}

function isValidPackageName(projectName: string) {
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
projectName
)
}
Expand All @@ -399,7 +399,7 @@ function toValidPackageName(projectName: string) {
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z0-9-~]+/g, '-')
.replace(/[^a-z\d\-~]+/g, '-')
}

function copyDir(srcDir: string, destDir: string) {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-react/src/fast-refresh.ts
Expand Up @@ -56,7 +56,7 @@ if (import.meta.hot) {
RefreshRuntime.register(type, __SOURCE__ + " " + id)
};
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
}`.replace(/[\n]+/gm, '')
}`.replace(/\n+/g, '')

const timeout = `
if (!window.__vite_plugin_react_timeout) {
Expand Down
6 changes: 3 additions & 3 deletions packages/plugin-react/src/index.ts
Expand Up @@ -110,10 +110,10 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
// - import * as React from 'react';
// - import React from 'react';
// - import React, {useEffect} from 'react';
const importReactRE = /(^|\n)import\s+(\*\s+as\s+)?React(,|\s+)/
const importReactRE = /(?:^|\n)import\s+(?:\*\s+as\s+)?React(?:,|\s+)/

// Any extension, including compound ones like '.bs.js'
const fileExtensionRE = /\.[^\/\s\?]+$/
const fileExtensionRE = /\.[^/\s?]+$/

const viteBabel: Plugin = {
name: 'vite:react-babel',
Expand Down Expand Up @@ -202,7 +202,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
filepath.match(fileExtensionRE) ||
[]

if (/\.(mjs|[tj]sx?)$/.test(extension)) {
if (/\.(?:mjs|[tj]sx?)$/.test(extension)) {
const isJSX = extension.endsWith('x')
const isNodeModules = id.includes('/node_modules/')
const isProjectFile =
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-vue/src/handleHotUpdate.ts
Expand Up @@ -11,7 +11,7 @@ import type { ResolvedOptions } from '.'

const debug = _debug('vite:hmr')

const directRequestRE = /(\?|&)direct\b/
const directRequestRE = /(?:\?|&)direct\b/

/**
* Vite-specific HMR handling
Expand Down Expand Up @@ -148,7 +148,7 @@ export async function handleHotUpdate(
affectedModules.add(mainModule)
} else if (mainModule && !affectedModules.has(mainModule)) {
const styleImporters = [...mainModule.importers].filter((m) =>
/\.css($|\?)/.test(m.url)
/\.css(?:$|\?)/.test(m.url)
)
styleImporters.forEach((m) => affectedModules.add(m))
}
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/node/config.ts
Expand Up @@ -465,8 +465,8 @@ export async function resolveConfig(
)

const clientAlias = [
{ find: /^[\/]?@vite\/env/, replacement: () => ENV_ENTRY },
{ find: /^[\/]?@vite\/client/, replacement: () => CLIENT_ENTRY }
{ find: /^\/?@vite\/env/, replacement: () => ENV_ENTRY },
{ find: /^\/?@vite\/client/, replacement: () => CLIENT_ENTRY }
]

// resolve alias with internal client alias
Expand Down
6 changes: 3 additions & 3 deletions packages/vite/src/node/constants.ts
Expand Up @@ -46,9 +46,9 @@ export const DEFAULT_CONFIG_FILES = [

export const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/

export const OPTIMIZABLE_ENTRY_RE = /\.(?:[cm]?[jt]s)$/
export const OPTIMIZABLE_ENTRY_RE = /\.[cm]?[jt]s$/

export const SPECIAL_QUERY_RE = /[\?&](?:worker|sharedworker|raw|url)\b/
export const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/

/**
* Prefix for resolved fs paths, since windows paths may not be valid as URLs.
Expand Down Expand Up @@ -129,7 +129,7 @@ export const DEFAULT_ASSETS_RE = new RegExp(
`\\.(` + KNOWN_ASSET_TYPES.join('|') + `)(\\?.*)?$`
)

export const DEP_VERSION_RE = /[\?&](v=[\w\.-]+)\b/
export const DEP_VERSION_RE = /[?&](v=[\w.-]+)\b/

export const loopbackHosts = new Set([
'localhost',
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/optimizer/esbuildDepPlugin.ts
Expand Up @@ -265,7 +265,7 @@ export function esbuildCjsExternalPlugin(externals: string[]): Plugin {
name: 'cjs-external',
setup(build) {
const escape = (text: string) =>
`^${text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$`
`^${text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}$`
const filter = new RegExp(externals.map(escape).join('|'))

build.onResolve({ filter: /.*/, namespace: 'external' }, (args) => ({
Expand Down
14 changes: 7 additions & 7 deletions packages/vite/src/node/optimizer/scan.ts
Expand Up @@ -40,7 +40,7 @@ const htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/
// since even missed imports can be caught at runtime, and false positives will
// simply be ignored.
export const importsRE =
/(?<!\/\/.*)(?<=^|;|\*\/)\s*import(?!\s+type)(?:[\w*{}\n\r\t, ]+from\s*)?\s*("[^"]+"|'[^']+')\s*(?=$|;|\/\/|\/\*)/gm
/(?<!\/\/.*)(?<=^|;|\*\/)\s*import(?!\s+type)(?:[\w*{}\n\r\t, ]+from)?\s*("[^"]+"|'[^']+')\s*(?=$|;|\/\/|\/\*)/gm
Copy link
Member Author

Choose a reason for hiding this comment

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

This \s* can be matched by the next \s*.


export async function scanImports(config: ResolvedConfig): Promise<{
deps: Record<string, string>
Expand Down Expand Up @@ -149,13 +149,13 @@ function globEntries(pattern: string | string[], config: ResolvedConfig) {
}

const scriptModuleRE =
/(<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gims
export const scriptRE = /(<script\b(?:\s[^>]*>|>))(.*?)<\/script>/gims
/(<script\b[^>]+type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gis
Copy link
Member Author

Choose a reason for hiding this comment

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

Because \b exists [^>] will always match more than one char.

export const scriptRE = /(<script(?:\s[^>]*>|>))(.*?)<\/script>/gis
Copy link
Member Author

Choose a reason for hiding this comment

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

Because \s and > both matches \b, this \b isn't needed.

export const commentRE = /<!--.*?-->/gs
const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
const typeRE = /\btype\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
const langRE = /\blang\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
const contextRE = /\bcontext\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i
const typeRE = /\btype\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i
const langRE = /\blang\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i
const contextRE = /\bcontext\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i

function esbuildScanPlugin(
config: ResolvedConfig,
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/node/plugins/asset.ts
Expand Up @@ -29,7 +29,7 @@ export const duplicateAssets = new WeakMap<
Map<string, OutputAsset>
>()

const rawRE = /(\?|&)raw(?:&|$)/
const rawRE = /(?:\?|&)raw(?:&|$)/
const urlRE = /(\?|&)url(?:&|$)/

const assetCache = new WeakMap<ResolvedConfig, Map<string, string>>()
Expand Down Expand Up @@ -173,7 +173,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
return
}

id = id.replace(urlRE, '$1').replace(/[\?&]$/, '')
id = id.replace(urlRE, '$1').replace(/[?&]$/, '')
const url = await fileToUrl(id, config, this)
return `export default ${JSON.stringify(url)}`
},
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/assetImportMetaUrl.ts
Expand Up @@ -38,7 +38,7 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
) {
let s: MagicString | undefined
const assetImportMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*,?\s*\)/g
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/g
Copy link
Member Author

Choose a reason for hiding this comment

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

\s*,?\s* is same with \s*(?:,\s*)?.

const cleanString = stripLiteral(code)

let match: RegExpExecArray | null
Expand Down
29 changes: 16 additions & 13 deletions packages/vite/src/node/plugins/css.ts
Expand Up @@ -111,13 +111,14 @@ export interface CSSModulesOptions {

const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)($|\\?)`
const cssLangRE = new RegExp(cssLangs)
// eslint-disable-next-line regexp/no-unused-capturing-group
const cssModuleRE = new RegExp(`\\.module${cssLangs}`)
const directRequestRE = /(\?|&)direct\b/
const htmlProxyRE = /(\?|&)html-proxy\b/
const directRequestRE = /(?:\?|&)direct\b/
const htmlProxyRE = /(?:\?|&)html-proxy\b/
const commonjsProxyRE = /\?commonjs-proxy/
const inlineRE = /(\?|&)inline\b/
const inlineCSSRE = /(\?|&)inline-css\b/
const usedRE = /(\?|&)used\b/
const inlineRE = /(?:\?|&)inline\b/
const inlineCSSRE = /(?:\?|&)inline-css\b/
const usedRE = /(?:\?|&)used\b/
const varRE = /^var\(/i

const cssBundleName = 'style.css'
Expand Down Expand Up @@ -1175,11 +1176,13 @@ type CssUrlReplacer = (
) => string | Promise<string>
// https://drafts.csswg.org/css-syntax-3/#identifier-code-point
export const cssUrlRE =
/(?<=^|[^\w\-\u0080-\uffff])url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/
/(?<=^|[^\w\-\u0080-\uffff])url\((\s*('[^']+'|"[^"]+")\s*|[^'")]+)\)/
export const cssDataUriRE =
/(?<=^|[^\w\-\u0080-\uffff])data-uri\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/
/(?<=^|[^\w\-\u0080-\uffff])data-uri\((\s*('[^']+'|"[^"]+")\s*|[^'")]+)\)/
Comment on lines -1178 to +1181
Copy link
Member Author

Choose a reason for hiding this comment

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

\s*('[^']+'|"[^"]+"|[^'")]+)\s* is same with (\s*('[^']+'|"[^"]+")\s*|[^'")]+). (String::trim() needs to be run later.)

export const importCssRE = /@import ('[^']+\.css'|"[^"]+\.css"|[^'")]+\.css)/
const cssImageSetRE = /(?<=image-set\()((?:[\w\-]+\([^\)]*\)|[^)])*)(?=\))/
// Assuming a function name won't be longer than 256 chars
// eslint-disable-next-line regexp/no-unused-capturing-group -- doesn't detect asyncReplace usage
const cssImageSetRE = /(?<=image-set\()((?:[\w\-]{1,256}\([^)]\)|[^)])*)(?=\))/

const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{
replacer: CssUrlReplacer
Expand Down Expand Up @@ -1235,7 +1238,7 @@ function rewriteCssUrls(
): Promise<string> {
return asyncReplace(css, cssUrlRE, async (match) => {
const [matched, rawUrl] = match
return await doUrlReplace(rawUrl, matched, replacer)
return await doUrlReplace(rawUrl.trim(), matched, replacer)
})
}

Expand All @@ -1245,7 +1248,7 @@ function rewriteCssDataUris(
): Promise<string> {
return asyncReplace(css, cssDataUriRE, async (match) => {
const [matched, rawUrl] = match
return await doUrlReplace(rawUrl, matched, replacer, 'data-uri')
return await doUrlReplace(rawUrl.trim(), matched, replacer, 'data-uri')
})
}

Expand All @@ -1262,7 +1265,7 @@ function rewriteImportCss(
// TODO: image and cross-fade could contain a "url" that needs to be processed
// https://drafts.csswg.org/css-images-4/#image-notation
// https://drafts.csswg.org/css-images-4/#cross-fade-function
const cssNotProcessedRE = /(gradient|element|cross-fade|image)\(/
const cssNotProcessedRE = /(?:gradient|element|cross-fade|image)\(/

async function rewriteCssImageSet(
css: string,
Expand Down Expand Up @@ -1391,7 +1394,7 @@ export async function hoistAtRules(css: string): Promise<string> {
// to top when multiple files are concatenated.
// match until semicolon that's not in quotes
const atImportRE =
/@import\s*(?:url\([^\)]*\)|"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'|[^;]*).*?;/gm
/@import(?:\s*(?:url\([^)]*\)|"(?:[^"]|(?<=\\)")*"|'(?:[^']|(?<=\\)')*').*?|[^;]*);/g
Comment on lines -1394 to +1397
Copy link
Member Author

Choose a reason for hiding this comment

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

[^;]* matches \s*. So splitted [^;]*.

while ((match = atImportRE.exec(cleanCss))) {
s.remove(match.index, match.index + match[0].length)
// Use `appendLeft` instead of `prepend` to preserve original @import order
Expand All @@ -1401,7 +1404,7 @@ export async function hoistAtRules(css: string): Promise<string> {
// #6333
// CSS @charset must be the top-first in the file, hoist the first to top
const atCharsetRE =
/@charset\s*(?:"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'|[^;]*).*?;/gm
/@charset(?:\s*(?:"(?:[^"]|(?<=\\)")*"|'(?:[^']|(?<=\\)')*').*?|[^;]*);/g
let foundCharset = false
while ((match = atCharsetRE.exec(cleanCss))) {
s.remove(match.index, match.index + match[0].length)
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/define.ts
Expand Up @@ -5,7 +5,7 @@ import { transformStableResult } from '../utils'
import { isCSSRequest } from './css'
import { isHTMLRequest } from './html'

const nonJsRe = /\.(json)($|\?)/
const nonJsRe = /\.json(?:$|\?)/
const isNonJsRequest = (request: string): boolean => nonJsRe.test(request)

export function definePlugin(config: ResolvedConfig): Plugin {
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/node/plugins/esbuild.ts
Expand Up @@ -27,9 +27,9 @@ import { searchForWorkspaceRoot } from '..'
const debug = createDebugger('vite:esbuild')

const INJECT_HELPERS_IIFE_RE =
/^(.*)((?:const|var) [^\s]+=function\([^)]*?\){"use strict";)/s
/^(.*)((?:const|var) \S+=function\([^)]*\)\{"use strict";)/s
const INJECT_HELPERS_UMD_RE =
/^(.*)(\(function\([^)]*?\){.+amd.+function\([^)]*?\){"use strict";)/s
/^(.*)(\(function\([^)]*\)\{.+amd.+function\([^)]*\)\{"use strict";)/s

let server: ViteDevServer

Expand Down
11 changes: 6 additions & 5 deletions packages/vite/src/node/plugins/html.ts
Expand Up @@ -44,12 +44,13 @@ const htmlProxyRE = /\?html-proxy=?(?:&inline-css)?&index=(\d+)\.(js|css)$/
const inlineCSSRE = /__VITE_INLINE_CSS__([a-z\d]{8}_\d+)__/g
// Do not allow preceding '.', but do allow preceding '...' for spread operations
const inlineImportRE =
/(?<!(?<!\.\.)\.)\bimport\s*\(("([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*')\)/g
const htmlLangRE = /\.(html|htm)$/
/(?<!(?<!\.\.)\.)\bimport\s*\(("(?:[^"]|(?<=\\)")*"|'(?:[^']|(?<=\\)')*')\)/g
const htmlLangRE = /\.(?:html|htm)$/

const importMapRE =
/[ \t]*<script[^>]*type\s*=\s*["']?importmap["']?[^>]*>.*?<\/script>/is
const moduleScriptRE = /[ \t]*<script[^>]*type\s*=\s*["']?module["']?[^>]*>/is
/[ \t]*<script[^>]*type\s*=\s*(?:"importmap"|'importmap'|importmap)[^>]*>.*?<\/script>/is
const moduleScriptRE =
/[ \t]*<script[^>]*type\s*=\s*(?:"module"|'module'|module)[^>]*>/i

export const isHTMLProxy = (id: string): boolean => htmlProxyRE.test(id)

Expand Down Expand Up @@ -197,7 +198,7 @@ export function getScriptInfo(node: DefaultTreeAdapterMap['element']): {
return { src, sourceCodeLocation, isModule, isAsync }
}

const attrValueStartRE = /=[\s\t\n\r]*(.)/
const attrValueStartRE = /=\s*(.)/

export function overwriteAttrValue(
s: MagicString,
Expand Down
10 changes: 5 additions & 5 deletions packages/vite/src/node/plugins/importAnalysis.ts
Expand Up @@ -71,12 +71,12 @@ const debug = createDebugger('vite:import-analysis')

const clientDir = normalizePath(CLIENT_DIR)

const skipRE = /\.(map|json)($|\?)/
const skipRE = /\.(?:map|json)(?:$|\?)/
export const canSkipImportAnalysis = (id: string): boolean =>
skipRE.test(id) || isDirectCSSRequest(id)

const optimizedDepChunkRE = /\/chunk-[A-Z0-9]{8}\.js/
const optimizedDepDynamicRE = /-[A-Z0-9]{8}\.js/
const optimizedDepChunkRE = /\/chunk-[A-Z\d]{8}\.js/
const optimizedDepDynamicRE = /-[A-Z\d]{8}\.js/

export function isExplicitImportRequired(url: string): boolean {
return !isJSRequest(cleanUrl(url)) && !isCSSRequest(url)
Expand Down Expand Up @@ -347,7 +347,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
// query can break 3rd party plugin's extension checks.
if (
(isRelative || isSelfImport) &&
!/[\?&]import=?\b/.test(url) &&
!/[?&]import=?\b/.test(url) &&
!url.match(DEP_VERSION_RE)
) {
const versionMatch = importer.match(DEP_VERSION_RE)
Expand Down Expand Up @@ -583,7 +583,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '')
.trim()
if (
!/^('.*'|".*"|`.*`)$/.test(url) ||
!/^(?:'.*'|".*"|`.*`)$/.test(url) ||
isExplicitImportRequired(url.slice(1, -1))
) {
needQueryInjectHelper = true
Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/node/plugins/importAnalysisBuild.ts
Expand Up @@ -38,8 +38,8 @@ const preloadMarkerWithQuote = `"${preloadMarker}"` as const
const dynamicImportPrefixRE = /import\s*\(/

// TODO: abstract
const optimizedDepChunkRE = /\/chunk-[A-Z0-9]{8}\.js/
const optimizedDepDynamicRE = /-[A-Z0-9]{8}\.js/
const optimizedDepChunkRE = /\/chunk-[A-Z\d]{8}\.js/
const optimizedDepDynamicRE = /-[A-Z\d]{8}\.js/

function toRelativePath(filename: string, importer: string) {
const relPath = path.relative(path.dirname(importer), filename)
Expand Down
6 changes: 3 additions & 3 deletions packages/vite/src/node/plugins/json.ts
Expand Up @@ -26,9 +26,9 @@ export interface JsonOptions {
}

// Custom json filter for vite
const jsonExtRE = /\.json($|\?)(?!commonjs-(proxy|external))/
const jsonExtRE = /\.json(?:$|\?)(?!commonjs-(?:proxy|external))/

const jsonLangs = `\\.(json|json5)($|\\?)`
const jsonLangs = `\\.(?:json|json5)(?:$|\\?)`
const jsonLangRE = new RegExp(jsonLangs)
export const isJSONRequest = (request: string): boolean =>
jsonLangRE.test(request)
Expand Down Expand Up @@ -71,7 +71,7 @@ export function jsonPlugin(
map: { mappings: '' }
}
} catch (e) {
const errorMessageList = /[\d]+/.exec(e.message)
const errorMessageList = /\d+/.exec(e.message)
const position = errorMessageList && parseInt(errorMessageList[0], 10)
const msg = position
? `, invalid JSON syntax found at line ${position}`
Expand Down