diff --git a/CHANGELOG.md b/CHANGELOG.md index d31d585d2..9837cce85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ Change Log +v5.4.7 +--- +* Fixed directory obfuscation with a set `sourceMapFileName` making all files share and overwrite one `.map`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/817 +* Fixed CLI `--config` failures hiding the real cause behind a generic `Cannot open config file` message. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1101 +* Fixed `sourceMapFileName` ending in `.js.map` (e.g. `foo.min.js.map`) being mangled in the emitted `//# sourceMappingURL=` comment. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1312 +* Fixed `URIError: URI malformed` crash when `stringArray` with `base64`/`rc4` encoding processed a string literal containing lone surrogate code units (e.g. `"[^\uD800-\uDFFF]"`). Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1431 +* Bumped the production `brace-expansion` transitive dependency to a patched version, resolving `CVE-2026-25547`. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1405 + v5.4.6 --- * Fixed unicode (`\uXXXX`, `\u{XXXX}`) and hex (`\xXX`) escape sequences of string literals being un-escaped into their literal characters during obfuscation. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/345 diff --git a/package.json b/package.json index ca70734b1..fcd4b84fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.4.6", + "version": "5.4.7", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", @@ -100,6 +100,9 @@ "webpack-cli": "6.0.1", "webpack-node-externals": "3.0.0" }, + "resolutions": { + "multimatch/minimatch/brace-expansion": "^1.1.12" + }, "repository": { "type": "git", "url": "git+https://github.com/javascript-obfuscator/javascript-obfuscator.git" diff --git a/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts b/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts index c6cce7c3b..3de30cd09 100644 --- a/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts +++ b/src/analyzers/string-array-storage-analyzer/StringArrayStorageAnalyzer.ts @@ -4,6 +4,7 @@ import { ServiceIdentifiers } from '../../container/ServiceIdentifiers'; import * as estraverse from '@javascript-obfuscator/estraverse'; import * as ESTree from 'estree'; +import { TStringArrayEncoding } from '../../types/options/TStringArrayEncoding'; import { TStringLiteralNode } from '../../types/node/TStringLiteralNode'; import { IOptions } from '../../interfaces/options/IOptions'; @@ -12,6 +13,8 @@ import { IStringArrayStorage } from '../../interfaces/storages/string-array-tran import { IStringArrayStorageAnalyzer } from '../../interfaces/analyzers/string-array-storage-analyzer/IStringArrayStorageAnalyzer'; import { IStringArrayStorageItemData } from '../../interfaces/storages/string-array-transformers/IStringArrayStorageItem'; +import { StringArrayEncoding } from '../../enums/node-transformers/string-array-transformers/StringArrayEncoding'; + import { NodeGuards } from '../../node/NodeGuards'; import { NodeLiteralUtils } from '../../node/NodeLiteralUtils'; import { NodeMetadata } from '../../node/NodeMetadata'; @@ -26,6 +29,14 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer { */ private static readonly minimumLengthForStringArray: number = 3; + /** + * Matches a lone (unpaired) surrogate code unit. Because of the `u` flag, valid surrogate pairs are + * iterated as a single code point outside the `\uD800-\uDFFF` range, so only unpaired surrogates match. + * + * @type {RegExp} + */ + private static readonly loneSurrogateRegExp: RegExp = /[\uD800-\uDFFF]/u; + /** * @type {IOptions} */ @@ -128,6 +139,14 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer { * @returns {boolean} */ private shouldAddValueToStringArray(literalNode: TStringLiteralNode): boolean { + // `base64` and `rc4` encodings rely on `encodeURIComponent`/`decodeURIComponent`, which cannot + // represent lone (unpaired) surrogate code units. Keeping such values inline avoids a + // `URIError: URI malformed` crash while still producing valid obfuscated code. + // Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1431 + if (this.isProhibitedStringArrayValue(literalNode.value)) { + return false; + } + const isForceTransformNode: boolean = NodeMetadata.isForceTransformNode(literalNode); if (isForceTransformNode) { @@ -140,4 +159,17 @@ export class StringArrayStorageAnalyzer implements IStringArrayStorageAnalyzer { this.randomGenerator.getMathRandom() <= this.options.stringArrayThreshold ); } + + /** + * @param {string} value + * @returns {boolean} + */ + private isProhibitedStringArrayValue(value: string): boolean { + const hasUnicodeEncoding: boolean = this.options.stringArrayEncoding.some( + (encoding: TStringArrayEncoding): boolean => + encoding === StringArrayEncoding.Base64 || encoding === StringArrayEncoding.Rc4 + ); + + return hasUnicodeEncoding && StringArrayStorageAnalyzer.loneSurrogateRegExp.test(value); + } } diff --git a/src/cli/utils/CLIUtils.ts b/src/cli/utils/CLIUtils.ts index 4dcc4a860..dd58e54a6 100644 --- a/src/cli/utils/CLIUtils.ts +++ b/src/cli/utils/CLIUtils.ts @@ -15,26 +15,32 @@ export class CLIUtils { * @returns {TDictionary} */ public static getUserConfig(configPath: string): TDictionary { - let config: TDictionary; - const configFileExtension: string = path.extname(configPath); const isValidExtension: boolean = CLIUtils.allowedConfigFileExtensions.includes(configFileExtension); if (!isValidExtension) { - throw new ReferenceError('Given config path must be a valid `.js|.mjs|.cjs` or `.json` file path'); + throw new ReferenceError('Given config path must be a valid `.js`, `.cjs` or `.json` file path'); } + const errors: Error[] = []; + try { - config = require(configPath); - } catch { - try { - config = __non_webpack_require__(configPath); - } catch { - throw new ReferenceError(`Cannot open config file with path: ${configPath}`); - } + return require(configPath); + } catch (error) { + errors.push(error); } - return config; + try { + return __non_webpack_require__(configPath); + } catch (error) { + errors.push(error); + } + + // surface the underlying reason (invalid JSON, `ERR_REQUIRE_ESM`, missing file, ...) + // instead of masking it behind a generic message + throw new ReferenceError( + `Cannot open config file with path: ${configPath}. Reason: ${CLIUtils.getConfigErrorReason(errors)}` + ); } /** @@ -44,4 +50,20 @@ export class CLIUtils { public static stringifyOptionAvailableValues(optionEnum: TDictionary): string { return Object.values(optionEnum).join(`${StringSeparator.Comma} `); } + + /** + * @param {Error[]} errors + * @returns {string} + */ + private static getConfigErrorReason(errors: Error[]): string { + const [firstError, secondError] = errors; + + // in the webpack bundle the real reason comes from the `__non_webpack_require__` call (the second error); + // in a plain Node/ts-node context `__non_webpack_require__` is not defined, so the real reason is the first error + if (secondError && !secondError.message.includes('__non_webpack_require__')) { + return secondError.message; + } + + return firstError.message; + } } diff --git a/src/cli/utils/ObfuscatedCodeFileUtils.ts b/src/cli/utils/ObfuscatedCodeFileUtils.ts index 443f225d3..1f421362b 100644 --- a/src/cli/utils/ObfuscatedCodeFileUtils.ts +++ b/src/cli/utils/ObfuscatedCodeFileUtils.ts @@ -88,6 +88,8 @@ export class ObfuscatedCodeFileUtils { throw new Error('Output code path is empty'); } + sourceMapFileName = this.getUniqueSourceMapFileName(outputCodePath, sourceMapFileName); + let normalizedOutputCodePath: string = path.normalize(outputCodePath); let parsedOutputCodePath: path.ParsedPath = path.parse(normalizedOutputCodePath); @@ -140,4 +142,37 @@ export class ObfuscatedCodeFileUtils { encoding: JavaScriptObfuscatorCLI.encoding }); } + + /** + * For directory obfuscation a single `sourceMapFileName` would be shared by every file and the + * source maps would overwrite each other, so it is prefixed with the output code file name to + * keep each source map unique. + * https://github.com/javascript-obfuscator/javascript-obfuscator/issues/817 + * + * @param {string} outputCodePath + * @param {string} sourceMapFileName + * @returns {string} + */ + private getUniqueSourceMapFileName(outputCodePath: string, sourceMapFileName: string): string { + if (!sourceMapFileName || !this.isDirectoryInputPath()) { + return sourceMapFileName; + } + + const outputCodeName: string = path.parse(outputCodePath).name; + const parsedSourceMapFileName: path.ParsedPath = path.parse(sourceMapFileName); + + // keep any leading directory part of `sourceMapFileName`, prefix only its file name + return path.join(parsedSourceMapFileName.dir, `${outputCodeName}-${parsedSourceMapFileName.base}`); + } + + /** + * @returns {boolean} + */ + private isDirectoryInputPath(): boolean { + try { + return fs.lstatSync(this.inputPath).isDirectory(); + } catch { + return false; + } + } } diff --git a/src/options/normalizer-rules/SourceMapFileNameRule.ts b/src/options/normalizer-rules/SourceMapFileNameRule.ts index ac5bf6004..c8b40d95a 100644 --- a/src/options/normalizer-rules/SourceMapFileNameRule.ts +++ b/src/options/normalizer-rules/SourceMapFileNameRule.ts @@ -12,7 +12,19 @@ export const SourceMapFileNameRule: TOptionsNormalizerRule = (options: IOptions) let { sourceMapFileName }: { sourceMapFileName: string } = options; if (sourceMapFileName) { - sourceMapFileName = sourceMapFileName.replace(/^\/+/, '').replace(/(?:\.js)?(?:\.map)?$/, ''); + sourceMapFileName = sourceMapFileName.replace(/^\/+/, ''); + + // a fully-qualified `*.js.map` file name is already in the canonical form, so it is kept as-is + // (otherwise the extension-stripping heuristic below would mangle names like `foo.min.js.map`) + // https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1312 + if (sourceMapFileName.endsWith('.js.map')) { + return { + ...options, + sourceMapFileName + }; + } + + sourceMapFileName = sourceMapFileName.replace(/(?:\.js)?(?:\.map)?$/, ''); let sourceMapFileNameParts: string[] = sourceMapFileName.split(StringSeparator.Dot); const sourceMapFileNamePartsCount: number = sourceMapFileNameParts.length; diff --git a/test/fixtures/invalid-config.json b/test/fixtures/invalid-config.json new file mode 100644 index 000000000..e2e37f3ad --- /dev/null +++ b/test/fixtures/invalid-config.json @@ -0,0 +1,5 @@ +{ + compact: true, + selfDefending: false, + sourceMap: true +} diff --git a/test/functional-tests/issues/fixtures/issue1431.js b/test/functional-tests/issues/fixtures/issue1431.js new file mode 100644 index 000000000..266cbaf7c --- /dev/null +++ b/test/functional-tests/issues/fixtures/issue1431.js @@ -0,0 +1 @@ +const controlSymbolRegexString = "\\\\[^\uD800-\uDFFF]"; diff --git a/test/functional-tests/issues/issue1312.spec.ts b/test/functional-tests/issues/issue1312.spec.ts new file mode 100644 index 000000000..6b6c8f152 --- /dev/null +++ b/test/functional-tests/issues/issue1312.spec.ts @@ -0,0 +1,52 @@ +import { assert } from 'chai'; + +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; + +import { SourceMapMode } from '../../../src/enums/source-map/SourceMapMode'; + +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1312 +// +describe('Issue #1312', () => { + describe('`sourceMappingURL` should use the provided `sourceMapFileName`', () => { + describe('Variant #1: `*.min.js.map` file name', () => { + const sourceMappingUrlRegExp: RegExp = /\/\/# sourceMappingURL=a\.min\.js\.map$/; + + let obfuscatedCode: string; + + before(() => { + obfuscatedCode = JavaScriptObfuscator.obfuscate("console.log('Hello World');", { + ...NO_ADDITIONAL_NODES_PRESET, + sourceMap: true, + sourceMapMode: SourceMapMode.Separate, + sourceMapFileName: 'a.min.js.map' + }).getObfuscatedCode(); + }); + + it('should keep the full `.js.map` file name in the `sourceMappingURL`', () => { + assert.match(obfuscatedCode, sourceMappingUrlRegExp); + }); + }); + + describe('Variant #2: base name without extension still gets `.js.map`', () => { + const sourceMappingUrlRegExp: RegExp = /\/\/# sourceMappingURL=a\.js\.map$/; + + let obfuscatedCode: string; + + before(() => { + obfuscatedCode = JavaScriptObfuscator.obfuscate("console.log('Hello World');", { + ...NO_ADDITIONAL_NODES_PRESET, + sourceMap: true, + sourceMapMode: SourceMapMode.Separate, + sourceMapFileName: 'a' + }).getObfuscatedCode(); + }); + + it('should append `.js.map` to a bare file name', () => { + assert.match(obfuscatedCode, sourceMappingUrlRegExp); + }); + }); + }); +}); diff --git a/test/functional-tests/issues/issue1431.spec.ts b/test/functional-tests/issues/issue1431.spec.ts new file mode 100644 index 000000000..f81745a64 --- /dev/null +++ b/test/functional-tests/issues/issue1431.spec.ts @@ -0,0 +1,109 @@ +import { assert } from 'chai'; + +import { NO_ADDITIONAL_NODES_PRESET } from '../../../src/options/presets/NoCustomNodes'; + +import { StringArrayEncoding } from '../../../src/enums/node-transformers/string-array-transformers/StringArrayEncoding'; + +import { readFileAsString } from '../../helpers/readFileAsString'; + +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1431 +// +describe('Issue #1431', () => { + describe('Base64/Rc4 string-array encoding of a literal with lone surrogate code units', () => { + describe('Variant #1: `base64` encoding with `splitStrings` (minimal reproduction)', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1431.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayEncoding: [StringArrayEncoding.Base64], + stringArrayThreshold: 1, + splitStrings: true, + splitStringsChunkLength: 5, + seed: 1 + }).getObfuscatedCode(); + }); + + it('should not throw `URIError: URI malformed`', () => { + assert.doesNotThrow(testFunc); + }); + }); + + describe('Variant #2: `base64` encoding', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1431.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayEncoding: [StringArrayEncoding.Base64], + stringArrayThreshold: 1, + seed: 1 + }).getObfuscatedCode(); + }); + + it('should not throw `URIError: URI malformed`', () => { + assert.doesNotThrow(testFunc); + }); + }); + + describe('Variant #3: `rc4` encoding', () => { + let testFunc: () => string; + + before(() => { + const code: string = readFileAsString(__dirname + '/fixtures/issue1431.js'); + + testFunc = () => + JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayEncoding: [StringArrayEncoding.Rc4], + stringArrayThreshold: 1, + seed: 1 + }).getObfuscatedCode(); + }); + + it('should not throw `URIError: URI malformed`', () => { + assert.doesNotThrow(testFunc); + }); + }); + }); + + describe('Obfuscated code stays runnable and preserves the original value', () => { + const originalValue: string = '\\\\[^\uD800-\uDFFF]'; + + let result: string; + + before(() => { + const code: string = 'module.exports = "\\\\\\\\[^\\uD800-\\uDFFF]";'; + + const obfuscatedCode: string = JavaScriptObfuscator.obfuscate(code, { + ...NO_ADDITIONAL_NODES_PRESET, + stringArray: true, + stringArrayEncoding: [StringArrayEncoding.Base64], + stringArrayThreshold: 1, + seed: 1 + }).getObfuscatedCode(); + + const moduleObject: { exports: string } = { exports: '' }; + + new Function('module', obfuscatedCode)(moduleObject); + + result = moduleObject.exports; + }); + + it('should evaluate to the original string', () => { + assert.equal(result, originalValue); + }); + }); +}); diff --git a/test/functional-tests/options/OptionsNormalizer.spec.ts b/test/functional-tests/options/OptionsNormalizer.spec.ts index d09abc247..7b84abce2 100644 --- a/test/functional-tests/options/OptionsNormalizer.spec.ts +++ b/test/functional-tests/options/OptionsNormalizer.spec.ts @@ -700,6 +700,48 @@ describe('OptionsNormalizer', () => { assert.deepEqual(optionsPreset, expectedOptionsPreset); }); }); + + // https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1312 + describe('Full `.js.map` file name is preserved as-is', () => { + before(() => { + optionsPreset = getNormalizedOptions({ + ...getDefaultOptions(), + sourceMapBaseUrl: 'http://localhost:9000', + sourceMapFileName: 'outputSourceMapName.min.js.map' + }); + + expectedOptionsPreset = { + ...getDefaultOptions(), + sourceMapBaseUrl: 'http://localhost:9000/', + sourceMapFileName: 'outputSourceMapName.min.js.map' + }; + }); + + it('should normalize options preset', () => { + assert.deepEqual(optionsPreset, expectedOptionsPreset); + }); + }); + + // https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1312 + describe('Full `.js.map` file name with leading slashes', () => { + before(() => { + optionsPreset = getNormalizedOptions({ + ...getDefaultOptions(), + sourceMapBaseUrl: 'http://localhost:9000', + sourceMapFileName: '//outputSourceMapName.min.js.map' + }); + + expectedOptionsPreset = { + ...getDefaultOptions(), + sourceMapBaseUrl: 'http://localhost:9000/', + sourceMapFileName: 'outputSourceMapName.min.js.map' + }; + }); + + it('should normalize options preset', () => { + assert.deepEqual(optionsPreset, expectedOptionsPreset); + }); + }); }); describe('splitStringsChunkLengthRule', () => { diff --git a/test/unit-tests/cli/utils/CLIUtils.spec.ts b/test/unit-tests/cli/utils/CLIUtils.spec.ts index 30d1fa6ba..6adcf3d2c 100644 --- a/test/unit-tests/cli/utils/CLIUtils.spec.ts +++ b/test/unit-tests/cli/utils/CLIUtils.spec.ts @@ -106,6 +106,27 @@ describe('CLIUtils', () => { assert.throws(testFunc, /Cannot open config file/); }); }); + + // https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1101 + describe('Variant #4: config file with invalid content', () => { + const configDirName: string = 'test/fixtures'; + const configFileName: string = 'invalid-config.json'; + const configFilePath: string = `../../../${configDirName}/${configFileName}`; + + let testFunc: () => void; + + before(() => { + testFunc = () => CLIUtils.getUserConfig(configFilePath); + }); + + it('should throw an error that includes the config file path', () => { + assert.throws(testFunc, /Cannot open config file with path/); + }); + + it('should surface the underlying reason instead of masking it', () => { + assert.throws(testFunc, /Reason:.*JSON/); + }); + }); }); describe('stringifyOptionAvailableValues', () => { diff --git a/test/unit-tests/cli/utils/ObfuscatedCodeFileUtils.spec.ts b/test/unit-tests/cli/utils/ObfuscatedCodeFileUtils.spec.ts index db0fac264..14672dc87 100644 --- a/test/unit-tests/cli/utils/ObfuscatedCodeFileUtils.spec.ts +++ b/test/unit-tests/cli/utils/ObfuscatedCodeFileUtils.spec.ts @@ -715,6 +715,49 @@ describe('obfuscatedCodeFileUtils', () => { }); }); + // https://github.com/javascript-obfuscator/javascript-obfuscator/issues/817 + describe('Variant #2: input path is a directory', () => { + const rawInputPath: string = path.join(tmpDirectoryPath, 'sm-input'); + const rawOutputPath: string = path.join(tmpDirectoryPath, 'output'); + const sourceMapFileName: string = 'map'; + + let firstOutputSourceMapPath: string; + let secondOutputSourceMapPath: string; + + before(() => { + fs.mkdirSync(rawInputPath, { recursive: true }); + + const obfuscatedCodeFileUtils: ObfuscatedCodeFileUtils = new ObfuscatedCodeFileUtils(rawInputPath, { + output: rawOutputPath + }); + + firstOutputSourceMapPath = obfuscatedCodeFileUtils.getOutputSourceMapPath( + path.join(rawOutputPath, 'foo.js'), + sourceMapFileName + ); + secondOutputSourceMapPath = obfuscatedCodeFileUtils.getOutputSourceMapPath( + path.join(rawOutputPath, 'bar.js'), + sourceMapFileName + ); + }); + + after(() => { + rimraf.sync(rawInputPath); + }); + + it('match #1: should prefix the source map file name with the output code file name', () => { + assert.equal(firstOutputSourceMapPath, path.join(rawOutputPath, 'foo-map.js.map')); + }); + + it('match #2: should prefix the source map file name with the output code file name', () => { + assert.equal(secondOutputSourceMapPath, path.join(rawOutputPath, 'bar-map.js.map')); + }); + + it('should produce a unique source map path per file', () => { + assert.notEqual(firstOutputSourceMapPath, secondOutputSourceMapPath); + }); + }); + describe('Variant #3: empty paths', () => { const rawInputPath: string = path.join(tmpDirectoryPath, 'input', 'test-input.js'); const rawOutputPath: string = path.join(tmpDirectoryPath, 'output', 'test-output.js'); diff --git a/yarn.lock b/yarn.lock index 0ba02834c..123dfed6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1336,10 +1336,10 @@ baseline-browser-mapping@^2.8.19: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz#2f9cccde871bfa4aec9dbf92d0ee746e4f1892e4" integrity sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q== -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== +brace-expansion@^1.1.12, brace-expansion@^1.1.7: + version "1.1.16" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.16.tgz#723d3a30c0558c225abc9fc479a73e14e26c3c2f" + integrity sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -4946,6 +4946,7 @@ string-template@1.0.0: integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y= "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==