diff --git a/packages/parser/src/interface/runtimeInterface.ts b/packages/parser/src/interface/runtimeInterface.ts index 981bd5754..ebe7033ed 100644 --- a/packages/parser/src/interface/runtimeInterface.ts +++ b/packages/parser/src/interface/runtimeInterface.ts @@ -12,4 +12,6 @@ export interface sceneEntry { * 场景栈条目接口 (兼容性别名) * @interface ISceneEntry */ -export interface ISceneEntry extends sceneEntry {} +export interface ISceneEntry extends sceneEntry { + sceneParams: Record; // 场景参数 +} diff --git a/packages/parser/src/interface/sceneInterface.ts b/packages/parser/src/interface/sceneInterface.ts index 1b05024c8..16d9caff6 100644 --- a/packages/parser/src/interface/sceneInterface.ts +++ b/packages/parser/src/interface/sceneInterface.ts @@ -1,7 +1,7 @@ /** * 语句类型 */ -import { sceneEntry, ISceneEntry } from './runtimeInterface'; +import { ISceneEntry } from './runtimeInterface'; import { fileType } from './assets'; export enum commandType { diff --git a/packages/webgal/package.json b/packages/webgal/package.json index 7a216c9f4..9a1c2de7a 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -12,7 +12,7 @@ "@emotion/css": "^11.11.2", "@icon-park/react": "^1.4.2", "@reduxjs/toolkit": "^1.8.1", - "angular-expressions": "^1.4.3", + "angular-expressions": "^1.5.5", "axios": "^1.13.5", "cloudlogjs": "^1.0.9", "gifuct-js": "^2.1.2", diff --git a/packages/webgal/src/Core/Modules/scene.ts b/packages/webgal/src/Core/Modules/scene.ts index 10c4f7bd4..bf0bac445 100644 --- a/packages/webgal/src/Core/Modules/scene.ts +++ b/packages/webgal/src/Core/Modules/scene.ts @@ -5,6 +5,7 @@ export interface ISceneEntry { sceneName: string; // 场景名称 sceneUrl: string; // 场景url continueLine: number; // 继续原场景的行号 + sceneParams: Record; // 场景参数 } /** @@ -29,6 +30,7 @@ export class SceneManager { public sceneData: ISceneData = cloneDeep(initSceneData); public lockSceneWrite = false; public sceneWritePromise: Promise | null = null; + public currentSceneParams: Record = {}; // 当前场景参数 public resetScene() { this.sceneData.currentSentenceId = 0; @@ -37,5 +39,6 @@ export class SceneManager { this.sceneWritePromise = null; this.settledScenes.clear(); this.settledAssets.clear(); + this.currentSceneParams = {}; } } diff --git a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts index 8bfbb5cd8..a54d36668 100644 --- a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts +++ b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts @@ -3,8 +3,6 @@ import { runScript } from './runScript'; import { logger } from '../../util/logger'; import { restoreScene } from '../scene/restoreScene'; import { webgalStore } from '@/store/store'; -import { getValueFromStateElseKey } from '@/Core/gameScripts/setVar'; -import { strIf } from '@/Core/controller/gamePlay/strIf'; import cloneDeep from 'lodash/cloneDeep'; import { ISceneEntry } from '@/Core/Modules/scene'; import { WebGAL } from '@/Core/WebGAL'; @@ -12,6 +10,7 @@ import { getBooleanArgByKey, getStringArgByKey } from '@/Core/util/getSentenceAr import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; import { jumpToLabel } from '@/Core/gameScripts/label/jumpToLabel'; import { prefetchCurrentSceneByProgress } from '@/Core/util/prefetcher/progressPrefetcher'; +import { evaluateStageExpression } from '@/Core/util/evalSentenceFn'; const MAX_FORWARD_SCRIPT_EXECUTION = 1000; @@ -19,20 +18,7 @@ export const whenChecker = (whenValue: string | undefined): boolean => { if (whenValue === undefined) { return true; } - // 先把变量解析出来 - const valExpArr = whenValue.split(/([+\-*\/()>=|<=|==|&&|\|\||!=)/g); - const valExp = valExpArr - .map((_e) => { - const e = _e.trim(); - if (e.match(/[a-zA-Z]/)) { - if (e.match(/^(true|false)$/)) { - return e; - } - return getValueFromStateElseKey(e, true, true); - } else return e; - }) - .reduce((pre, curr) => pre + curr, ''); - return !!strIf(valExp); + return evaluateStageExpression(whenValue, { returnType: 'boolean' }); }; /** @@ -69,8 +55,15 @@ export const scriptExecutor = (depth = 0) => { if (contentExp !== null) { contentExp.forEach((e) => { - const contentVarValue = getValueFromStateElseKey(e.replace(/(? { +export const callScene = (sceneUrl: string, sceneName: string, params: Record = {}) => { if (WebGAL.sceneManager.lockSceneWrite) { return; } @@ -23,6 +25,7 @@ export const callScene = (sceneUrl: string, sceneName: string) => { sceneName: WebGAL.sceneManager.sceneData.currentScene.sceneName, sceneUrl: WebGAL.sceneManager.sceneData.currentScene.sceneUrl, continueLine: WebGAL.sceneManager.sceneData.currentSentenceId, + sceneParams: cloneDeep(WebGAL.sceneManager.currentSceneParams), // 保存当前场景参数 }); // 场景写入到运行时 const sceneWritePromise = sceneFetcher(sceneUrl) @@ -33,6 +36,15 @@ export const callScene = (sceneUrl: string, sceneName: string) => { WebGAL.sceneManager.settledScenes.add(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 logger.debug('现在调用场景,调用结果:', WebGAL.sceneManager.sceneData); shouldAutoNext = !isFastPreviewSceneWrite; + WebGAL.sceneManager.currentSceneParams = Object.entries(params) + .map(([key, value]) => ({ + key, + value: evaluateStageExpressionWithoutDot(value), + })) + .reduce((res: Record, item: Record) => { + res[item.key] = item.value; + return res; + }, {} as Record); // 设置新场景参数并立即求值 }) .catch((e) => { logger.error('场景调用错误', e); diff --git a/packages/webgal/src/Core/controller/scene/changeScene.ts b/packages/webgal/src/Core/controller/scene/changeScene.ts index 7a6c1c981..7fe68e291 100644 --- a/packages/webgal/src/Core/controller/scene/changeScene.ts +++ b/packages/webgal/src/Core/controller/scene/changeScene.ts @@ -5,13 +5,15 @@ import { nextSentence } from '@/Core/controller/gamePlay/nextSentence'; import { clearPrefetchLinks } from '@/Core/util/prefetcher/assetsPrefetcher'; import { WebGAL } from '@/Core/WebGAL'; +import { evaluateStageExpressionWithoutDot } from '@/Core/util/evalSentenceFn'; /** * 切换场景 * @param sceneUrl 场景路径 * @param sceneName 场景名称 + * @param args 场景参数 */ -export const changeScene = (sceneUrl: string, sceneName: string) => { +export const changeScene = (sceneUrl: string, sceneName: string, params: Record = {}) => { if (WebGAL.sceneManager.lockSceneWrite) { return; } @@ -27,6 +29,15 @@ export const changeScene = (sceneUrl: string, sceneName: string) => { WebGAL.sceneManager.settledScenes.add(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 logger.debug('现在切换场景,切换后的结果:', WebGAL.sceneManager.sceneData); shouldAutoNext = !isFastPreviewSceneWrite; + WebGAL.sceneManager.currentSceneParams = Object.entries(params) + .map(([key, value]) => ({ + key, + value: evaluateStageExpressionWithoutDot(value), + })) + .reduce((res: Record, item: Record) => { + res[item.key] = item.value; + return res; + }, {} as Record); // 设置新场景参数并立即求值 }) .catch((e) => { logger.error('场景调用错误', e); diff --git a/packages/webgal/src/Core/controller/scene/restoreScene.ts b/packages/webgal/src/Core/controller/scene/restoreScene.ts index d1d671e6a..407b1ea8a 100644 --- a/packages/webgal/src/Core/controller/scene/restoreScene.ts +++ b/packages/webgal/src/Core/controller/scene/restoreScene.ts @@ -24,6 +24,7 @@ export const restoreScene = (entry: ISceneEntry) => { WebGAL.sceneManager.sceneData.currentSentenceId = entry.continueLine + 1; // 重设场景 logger.debug('现在恢复场景,恢复后场景:', WebGAL.sceneManager.sceneData.currentScene); shouldAutoNext = !isFastPreviewSceneWrite; + WebGAL.sceneManager.currentSceneParams = entry.sceneParams ?? {}; }) .catch((e) => { logger.error('场景调用错误', e); diff --git a/packages/webgal/src/Core/gameScripts/callSceneScript.ts b/packages/webgal/src/Core/gameScripts/callSceneScript.ts index e7f9a5848..6b37de637 100644 --- a/packages/webgal/src/Core/gameScripts/callSceneScript.ts +++ b/packages/webgal/src/Core/gameScripts/callSceneScript.ts @@ -9,6 +9,11 @@ import { callScene } from '../controller/scene/callScene'; export const callSceneScript = (sentence: ISentence): IPerform => { const sceneNameArray: Array = sentence.content.split('/'); const sceneName = sceneNameArray[sceneNameArray.length - 1]; - callScene(sentence.content, sceneName); + // 从 args 中提取场景参数 + const params: Record = {}; + sentence.args.forEach((arg) => { + if (arg.key.startsWith('@')) params[arg.key.slice(1)] = arg.value; + }); + callScene(sentence.content, sceneName, params); return createNonePerform({ isHoldOn: true }); }; diff --git a/packages/webgal/src/Core/gameScripts/changeSceneScript.ts b/packages/webgal/src/Core/gameScripts/changeSceneScript.ts index ed50f8fb2..0c4073b89 100644 --- a/packages/webgal/src/Core/gameScripts/changeSceneScript.ts +++ b/packages/webgal/src/Core/gameScripts/changeSceneScript.ts @@ -9,6 +9,13 @@ import { changeScene } from '../controller/scene/changeScene'; export const changeSceneScript = (sentence: ISentence): IPerform => { const sceneNameArray: Array = sentence.content.split('/'); const sceneName = sceneNameArray[sceneNameArray.length - 1]; - changeScene(sentence.content, sceneName); + // 从 args 中提取场景参数 + const params: Record = {}; + sentence.args.forEach((arg) => { + if (arg.key.startsWith('@')) { + params[arg.key.slice(1)] = arg.value; + } + }); + changeScene(sentence.content, sceneName, params); return createNonePerform({ isHoldOn: true }); }; diff --git a/packages/webgal/src/Core/gameScripts/choose/index.tsx b/packages/webgal/src/Core/gameScripts/choose/index.tsx index c803a8507..99b91f76a 100644 --- a/packages/webgal/src/Core/gameScripts/choose/index.tsx +++ b/packages/webgal/src/Core/gameScripts/choose/index.tsx @@ -3,7 +3,6 @@ import { createNonePerform, IPerform } from '@/Core/Modules/perform/performInter import { changeScene } from '@/Core/controller/scene/changeScene'; import { jmp } from '@/Core/gameScripts/label/jmp'; import ReactDOM from 'react-dom'; -import React from 'react'; import styles from './choose.module.scss'; import { webgalStore } from '@/store/store'; import { useSEByWebgalStore } from '@/hooks/useSoundEffect'; @@ -16,6 +15,7 @@ import { useFontFamily } from '@/hooks/useFontFamily'; import { getNumberArgByKey } from '@/Core/util/getSentenceArg'; class ChooseOption { + public params: Record = {}; /** * 格式: * (showConditionVar>1)[enableConditionVar>2]->text:jump @@ -38,6 +38,13 @@ class ChooseOption { } return option; } + public appendArgs(args: { key: string; value: any }[]) { + // 解析后面的 -xxx 参数 + args.forEach(({ key, value }) => { + if (key.startsWith('@')) this.params[key.slice(1)] = value; + }); + return this; + } public text: string; public jump: string; public jumpToScene: boolean; @@ -57,10 +64,9 @@ class ChooseOption { */ export const choose = (sentence: ISentence): IPerform => { const chooseOptionScripts = sentence.content.split(/(? ChooseOption.parse(e.trim())); + const chooseOptions = chooseOptionScripts.map((e) => ChooseOption.parse(e.trim()).appendArgs(sentence.args)); const defaultChoose = getNumberArgByKey(sentence, 'defaultChoose'); const defaultPreviewChoice = getDefaultPreviewChoice(chooseOptions, defaultChoose); - if (defaultPreviewChoice) { selectChooseOption(defaultPreviewChoice, false); if (!defaultPreviewChoice.jumpToScene) { @@ -112,7 +118,7 @@ function getDefaultPreviewChoice(chooseOptions: ChooseOption[], defaultChoose: n function selectChooseOption(option: ChooseOption, autoNext = true) { if (option.jumpToScene) { - changeScene(option.jump, option.text); + changeScene(option.jump, option.text, option.params); } else { jmp(option.jump, autoNext); } @@ -125,7 +131,7 @@ function Choose(props: { chooseOptions: ChooseOption[] }) { // 运行时计算JSX.Element[] const runtimeBuildList = (chooseListFull: ChooseOption[]) => { return chooseListFull - .filter((e, i) => whenChecker(e.showCondition)) + .filter((e) => whenChecker(e.showCondition)) .map((e, i) => { const enable = whenChecker(e.enableCondition); const className = enable @@ -135,7 +141,7 @@ function Choose(props: { chooseOptions: ChooseOption[] }) { ? () => { playSeClick(); WebGAL.gameplay.performController.unmountPerform('choose'); - selectChooseOption(e); + selectChooseOption(e, true); } : () => {}; return ( diff --git a/packages/webgal/src/Core/gameScripts/setVar.ts b/packages/webgal/src/Core/gameScripts/setVar.ts index f86a3d356..e691dd6d1 100644 --- a/packages/webgal/src/Core/gameScripts/setVar.ts +++ b/packages/webgal/src/Core/gameScripts/setVar.ts @@ -2,15 +2,12 @@ import { ISentence } from '@/Core/controller/scene/sceneInterface'; import { createNonePerform, IPerform } from '@/Core/Modules/perform/performInterface'; import { webgalStore } from '@/store/store'; import { logger } from '@/Core/util/logger'; -import { compile } from 'angular-expressions'; import { setScriptManagedGlobalVar } from '@/store/userDataReducer'; import { ISetGameVar } from '@/Core/Modules/stage/stageInterface'; import { dumpToStorageFast } from '@/Core/controller/storage/storageController'; -import expression from 'angular-expressions'; -import get from 'lodash/get'; -import random from 'lodash/random'; import { getBooleanArgByKey } from '../util/getSentenceArg'; import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; +import { evaluateStageExpressionWithoutDot } from '../util/evalSentenceFn'; interface ISetGameVarFromExpressionPayload { key: string; @@ -40,14 +37,20 @@ export const setGameVarFromExpression = ({ if (!normalizedKey) { return; } - setGameVar({ key: normalizedKey, value: resolveSetVarValue(value) }); + setGameVar({ key: normalizedKey, value: evaluateStageExpressionWithoutDot(value, { returnType: 'origin' }) }); if (isGlobal) { - logger.debug('设置全局变量:', { key: normalizedKey, value: webgalStore.getState().userData.globalGameVar[normalizedKey] }); + logger.debug('设置全局变量:', { + key: normalizedKey, + value: webgalStore.getState().userData.globalGameVar[normalizedKey], + }); if (persistGlobal) { dumpToStorageFast(); } } else { - logger.debug('设置变量:', { key: normalizedKey, value: stageStateManager.getCalculationStageState().GameVar[normalizedKey] }); + logger.debug('设置变量:', { + key: normalizedKey, + value: stageStateManager.getCalculationStageState().GameVar[normalizedKey], + }); } }; @@ -64,96 +67,3 @@ export const setVar = (sentence: ISentence): IPerform => { } return createNonePerform(); }; - -type BaseVal = string | number | boolean | undefined; - -export function resolveSetVarValue(valExp: string): string | boolean | number { - if (/^\s*[a-zA-Z_$][\w$]*\s*\(.*\)\s*$/.test(valExp)) { - return EvaluateExpression(valExp); - } else if (valExp.match(/[+\-*\/()]/)) { - const valExpArr = valExp.split(/([+\-*\/()])/g); - const valExp2 = valExpArr - .map((e) => { - if (!e.trim().match(/^[a-zA-Z_$][a-zA-Z0-9_.]*$/)) { - return e; - } - const _r = getValueFromStateElseKey(e.trim(), true); - return typeof _r === 'string' ? `'${_r}'` : _r; - }) - .reduce((pre, curr) => pre + curr, ''); - let result = ''; - try { - const exp = compile(valExp2); - result = exp(); - } catch (e) { - logger.error('expression compile error', e); - } - return result; - } else if (valExp.match(/true|false/)) { - if (valExp.match(/true/)) { - return true; - } - if (valExp.match(/false/)) { - return false; - } - } else if (valExp.length === 0) { - return ''; - } else { - if (!isNaN(Number(valExp))) { - return Number(valExp); - } else { - return getValueFromStateElseKey(valExp, true) ?? ''; - } - } - return ''; -} - -/** - * 执行函数 - */ -function EvaluateExpression(val: string) { - const instance = expression.compile(val); - return instance({ - random: (...args: any[]) => { - return args.length ? random(...args) : Math.random(); - }, - }); -} - -/** - * 取不到时返回 undefined - */ -export function getValueFromState(key: string) { - let ret: any; - const stage = stageStateManager.getCalculationStageState(); - const userData = webgalStore.getState().userData; - const _Merge = { stage, userData }; // 不要直接合并到一起,防止可能的键冲突 - if (stage.GameVar.hasOwnProperty(key)) { - ret = stage.GameVar[key]; - } else if (userData.globalGameVar.hasOwnProperty(key)) { - ret = userData.globalGameVar[key]; - } else if (key.startsWith('$')) { - const propertyKey = key.replace('$', ''); - ret = get(_Merge, propertyKey, undefined) as BaseVal; - } - return ret; -} - -/** - * 取不到时返回 {key} - */ -export function getValueFromStateElseKey(key: string, useKeyNameAsReturn = false, quoteString = false) { - const valueFromState = getValueFromState(key); - if (valueFromState === null || valueFromState === undefined) { - logger.warn('valueFromState result null, key = ' + key); - if (useKeyNameAsReturn) { - return key; - } - return `{${key}}`; - } - // 用 "" 包裹字符串,用于使用 compile 条件判断,处理字符串类型的变量 - if (quoteString && typeof valueFromState === 'string') { - return `"${valueFromState.replaceAll('"', '\\"')}"`; - } - return valueFromState; -} diff --git a/packages/webgal/src/Core/util/evalSentenceFn.ts b/packages/webgal/src/Core/util/evalSentenceFn.ts new file mode 100644 index 000000000..66a5da972 --- /dev/null +++ b/packages/webgal/src/Core/util/evalSentenceFn.ts @@ -0,0 +1,121 @@ +import { webgalStore } from '@/store/store'; +import random from 'lodash/random'; +import { WebGAL } from '../WebGAL'; +import expression from 'angular-expressions'; +import { logger } from '@/Core/util/logger'; +import { stageStateManager } from '../Modules/stage/stageStateManager'; + +// 是否是函数调用 +export const isFunctionCall = (valExp: string) => { + return /^\s*[a-zA-Z_$][\w$]*\s*\(.*\)\s*$/.test(valExp); +}; + +export const isObject = (valExp: string) => { + try { + return new Function(`return ${valExp}`)(); + } catch { + return false; + } +}; + +export interface EvaluateExpressionOptions { + /** + * 当是无效值 `null | undefined | Error` 时返回类型(函数调用时无效,将返回求值结果) + * @default `result` + * @description + * `result` 返回求值结果(报错时无效) + * `origin` 返回原expr值 + * `block` 返回 {...} 包裹原值 + * `boolean` 返回 false + */ + returnType?: 'resullt' | 'origin' | 'block' | 'boolean'; +} + +/** + * 在当前`Stage`运行时执行表达式 + * + * 可执行:基础表达式,函数调用,变量调用,`{...}`包裹 + * @description + * 当`expr`不为string时,将直接返回`expr` + * @param val 表达式 + * @param options 配置 + */ +export const evaluateStageExpression = ( + expr: string | number | boolean, + options: EvaluateExpressionOptions = { + returnType: 'resullt', + }, +) => { + if (typeof expr === 'number' || typeof expr === 'boolean') return expr; + let val = expr.trim(); + if (val.startsWith('{') && val.endsWith('}')) { + if (!isObject(val)) { + val = val.slice(1, -1); + } + } + const sceneArguments = WebGAL.sceneManager.currentSceneParams; + const stage = stageStateManager.getCalculationStageState(); + const userData = webgalStore.getState().userData; + const globalVars = userData.globalGameVar; + const localVars = stage.GameVar; + const _Merge = { $stage: stage, $userData: userData }; // 不要直接合并到一起,防止可能的键冲突 + try { + const instance = expression.compile(val); + const evalResult = instance({ + /* 内置变量 */ + ...globalVars, + ...localVars, + ..._Merge, + /* 内置函数 */ + random(...args: any[]) { + return args.length ? random(...args) : Math.random(); + }, + // 获取场景调用参数 + getParentParams(key: string) { + return sceneArguments[key]; + }, + }); + + if (evalResult === null || evalResult === undefined) { + if (isFunctionCall(val)) return evalResult; + switch (options.returnType) { + case 'origin': + return val; + case 'boolean': + return false; + case 'block': + return `{${val}}`; + } + } + return evalResult; + } catch (e) { + logger.warn('evaluateExpression throw error, expr = ' + val + ', error = ' + e); + switch (options.returnType) { + case 'origin': + return val; + case 'boolean': + return false; + case 'block': + return `{${val}}`; + } + } +}; + +type ESEParameters = Parameters; + +/** + * 无引号字符串求值 + * + * 用于`设置变量,参数处理`处理 + */ +export const evaluateStageExpressionWithoutDot = ( + expr: ESEParameters[0], + op: ESEParameters[1] = { returnType: 'block' }, +) => { + let val = expr; + // 当expr没有标点符号,运算符时,将作为字符串处理 + if (typeof val === 'string' && !/[a-zA-Z_$][\w$]*\s*\(.*\)\s*$/.test(val) && !/[.,<>;"'{}():+\-*/%?![\]]/.test(val)) { + val = `'${expr}'`; + } + return evaluateStageExpression(val, op); +}; diff --git a/packages/webgal/src/Stage/TextBox/TextBox.tsx b/packages/webgal/src/Stage/TextBox/TextBox.tsx index c0f76b8fd..36fb1b610 100644 --- a/packages/webgal/src/Stage/TextBox/TextBox.tsx +++ b/packages/webgal/src/Stage/TextBox/TextBox.tsx @@ -268,7 +268,7 @@ interface Segment { } function parseString(input: string): Segment[] { - const regex = /(\[(.*?)\]\((.*?)\))|([^\[\]]+)/g; + const regex = /(\[([^\]]+)\]\(([^)]+)\))|([\s\S]+?(?=\[|$))/g; const result: Segment[] = []; let match: RegExpExecArray | null; diff --git a/yarn.lock b/yarn.lock index 9ef0a5ec1..52bc5a762 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1718,10 +1718,10 @@ ajv@^6.14.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -angular-expressions@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/angular-expressions/-/angular-expressions-1.4.3.tgz#24fb9e8e9e0278885dd0f7e28512ac88a74037f6" - integrity sha512-r7j+dqOuHy0OYiR5AazDixU/Us3TDN2FfuxGX4Dq6d61Y2MhBQHMdUNBfkkLPjDqVm2Is394h31gC3bcBwy9zw== +angular-expressions@^1.5.5: + version "1.5.5" + resolved "https://registry.npmmirror.com/angular-expressions/-/angular-expressions-1.5.5.tgz#26de781221bfe2ad69111e95202b2955bd63f4ee" + integrity sha512-MZqPMw7FYq6XkqE/v4udnanaIRrhJ22hmn3SeJkqDYEtZsoewafa+QgZqR9HrCXP114jaChzOjCjrM6LqvnflA== ansi-escapes@^4.3.0: version "4.3.2"