diff --git a/packages/ts-migrate-plugins/package.json b/packages/ts-migrate-plugins/package.json index 91415e8..cc4be40 100644 --- a/packages/ts-migrate-plugins/package.json +++ b/packages/ts-migrate-plugins/package.json @@ -61,12 +61,14 @@ "dependencies": { "eslint": "^7.14.0", "jscodeshift": "^0.12.0", + "json-schema": "^0.3.0", "ts-migrate-server": "^0.1.18", "typescript": "4.2.4" }, "gitHead": "7acf6067f15c9bb367cda9c47fcfb4203dcc54f3", "devDependencies": { "@ts-morph/bootstrap": "^0.9.1", + "@types/json-schema": "^7.0.7", "jest": "26.6.3" } } diff --git a/packages/ts-migrate-plugins/src/plugins/add-conversions.ts b/packages/ts-migrate-plugins/src/plugins/add-conversions.ts index f8d363c..383b364 100644 --- a/packages/ts-migrate-plugins/src/plugins/add-conversions.ts +++ b/packages/ts-migrate-plugins/src/plugins/add-conversions.ts @@ -2,10 +2,9 @@ import ts from 'typescript'; import { Plugin } from 'ts-migrate-server'; import { isDiagnosticWithLinePosition } from '../utils/type-guards'; import getTokenAtPosition from './utils/token-pos'; +import { AnyAliasOptions, validateAnyAliasOptions } from '../utils/validateOptions'; -type Options = { - anyAlias?: string; -}; +type Options = AnyAliasOptions; const supportedDiagnostics = new Set([ // TS2339: Property '{0}' does not exist on type '{1}'. @@ -16,6 +15,7 @@ const supportedDiagnostics = new Set([ const addConversionsPlugin: Plugin = { name: 'add-conversions', + run({ fileName, sourceFile, text, options, getLanguageService }) { // Filter out diagnostics we care about. const diags = getLanguageService() @@ -31,6 +31,8 @@ const addConversionsPlugin: Plugin = { const printer = ts.createPrinter(); return printer.printFile(newSourceFile); }, + + validate: validateAnyAliasOptions, }; export default addConversionsPlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/declare-missing-class-properties.ts b/packages/ts-migrate-plugins/src/plugins/declare-missing-class-properties.ts index f7b32b5..c509d08 100644 --- a/packages/ts-migrate-plugins/src/plugins/declare-missing-class-properties.ts +++ b/packages/ts-migrate-plugins/src/plugins/declare-missing-class-properties.ts @@ -1,13 +1,15 @@ import jscodeshift, { ASTPath, ClassBody } from 'jscodeshift'; import { Plugin } from 'ts-migrate-server'; import { isDiagnosticWithLinePosition } from '../utils/type-guards'; +import { AnyAliasOptions, validateAnyAliasOptions } from '../utils/validateOptions'; -type Options = { anyAlias?: string }; +type Options = AnyAliasOptions; const j = jscodeshift.withParser('tsx'); const declareMissingClassPropertiesPlugin: Plugin = { name: 'declare-missing-class-properties', + async run({ text, fileName, getLanguageService, options }) { const diagnostics = getLanguageService() .getSemanticDiagnostics(fileName) @@ -82,6 +84,8 @@ const declareMissingClassPropertiesPlugin: Plugin = { return root.toSource(); }, + + validate: validateAnyAliasOptions, }; export default declareMissingClassPropertiesPlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/explicit-any.ts b/packages/ts-migrate-plugins/src/plugins/explicit-any.ts index f6fa248..b43e1a7 100644 --- a/packages/ts-migrate-plugins/src/plugins/explicit-any.ts +++ b/packages/ts-migrate-plugins/src/plugins/explicit-any.ts @@ -3,11 +3,13 @@ import { Collection } from 'jscodeshift/src/Collection'; import ts from 'typescript'; import { Plugin } from 'ts-migrate-server'; import { isDiagnosticWithLinePosition } from '../utils/type-guards'; +import { AnyAliasOptions, validateAnyAliasOptions } from '../utils/validateOptions'; -type Options = { anyAlias?: string }; +type Options = AnyAliasOptions; const explicitAnyPlugin: Plugin = { name: 'explicit-any', + run({ options, fileName, text, getLanguageService }) { const semanticDiagnostics = getLanguageService().getSemanticDiagnostics(fileName); const diagnostics = semanticDiagnostics @@ -15,6 +17,8 @@ const explicitAnyPlugin: Plugin = { .filter((d) => d.category === ts.DiagnosticCategory.Error); return withExplicitAny(text, diagnostics, options.anyAlias); }, + + validate: validateAnyAliasOptions, }; export default explicitAnyPlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/hoist-class-statics.ts b/packages/ts-migrate-plugins/src/plugins/hoist-class-statics.ts index 4199515..d6e543d 100644 --- a/packages/ts-migrate-plugins/src/plugins/hoist-class-statics.ts +++ b/packages/ts-migrate-plugins/src/plugins/hoist-class-statics.ts @@ -8,16 +8,18 @@ import { collectIdentifierNodes, KnownDefinitionMap, } from './utils/identifiers'; +import { AnyAliasOptions, validateAnyAliasOptions } from '../utils/validateOptions'; -type Options = { - anyAlias?: string; -}; +type Options = AnyAliasOptions; const hoistClassStaticsPlugin: Plugin = { name: 'hoist-class-statics', + run({ sourceFile, text, options }) { return hoistStaticClassProperties(sourceFile, text, options); }, + + validate: validateAnyAliasOptions, }; export default hoistClassStaticsPlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/jsdoc.ts b/packages/ts-migrate-plugins/src/plugins/jsdoc.ts index 1e5ebdd..38c0725 100644 --- a/packages/ts-migrate-plugins/src/plugins/jsdoc.ts +++ b/packages/ts-migrate-plugins/src/plugins/jsdoc.ts @@ -1,6 +1,12 @@ /* eslint-disable no-bitwise */ import ts from 'typescript'; import { Plugin } from 'ts-migrate-server'; +import { + AnyAliasOptions, + Properties, + anyAliasProperty, + createValidate, +} from '../utils/validateOptions'; type TypeMap = Record; @@ -39,12 +45,27 @@ const defaultTypeMap: TypeMap = { type Options = { annotateReturns?: boolean; - anyAlias?: string; typeMap?: TypeMap; +} & AnyAliasOptions; + +const optionProperties: Properties = { + ...anyAliasProperty, + annotateReturns: { type: 'boolean' }, + typeMap: { + oneOf: [ + { type: 'string' }, + { + type: 'object', + properties: { tsName: { type: 'string' }, acceptsTypeParameters: { type: 'boolean' } }, + additionalProperties: false, + }, + ], + }, }; const jsDocPlugin: Plugin = { name: 'jsdoc', + run({ sourceFile, text, options }) { const result = ts.transform(sourceFile, [jsDocTransformerFactory(options)]); const newSourceFile = result.transformed[0]; @@ -54,6 +75,8 @@ const jsDocPlugin: Plugin = { const printer = ts.createPrinter(); return printer.printFile(newSourceFile); }, + + validate: createValidate(optionProperties), }; export default jsDocPlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/member-accessibility.ts b/packages/ts-migrate-plugins/src/plugins/member-accessibility.ts index 913bec5..8511f8e 100644 --- a/packages/ts-migrate-plugins/src/plugins/member-accessibility.ts +++ b/packages/ts-migrate-plugins/src/plugins/member-accessibility.ts @@ -1,16 +1,28 @@ /* eslint-disable no-bitwise */ import ts from 'typescript'; -import { Plugin } from 'ts-migrate-server'; +import { Plugin, PluginOptionsError } from 'ts-migrate-server'; + +import { Properties, validateOptions } from '../utils/validateOptions'; + +const accessibility = ['private' as const, 'protected' as const, 'public' as const]; type Options = { - defaultAccessibility?: 'private' | 'protected' | 'public'; + defaultAccessibility?: typeof accessibility[number]; privateRegex?: string; protectedRegex?: string; publicRegex?: string; }; +const optionProperties: Properties = { + defaultAccessibility: { enum: accessibility }, + privateRegex: { type: 'string' }, + protectedRegex: { type: 'string' }, + publicRegex: { type: 'string' }, +}; + const memberAccessibilityPlugin: Plugin = { name: 'member-accessibility', + run({ sourceFile, text, options }) { const result = ts.transform(sourceFile, [memberAccessibilityTransformerFactory(options)]); const newSourceFile = result.transformed[0]; @@ -20,6 +32,29 @@ const memberAccessibilityPlugin: Plugin = { const printer = ts.createPrinter(); return printer.printFile(newSourceFile); }, + + validate(options: unknown): options is Options { + const valid = validateOptions(options, optionProperties); + + if (valid) { + // Validate regex property syntax. + // This can't be covered by JSON schema. + const validOptions = options as Options; + accessibility.forEach((accessibility) => { + const key = `${accessibility}Regex` as const; + const value = validOptions[key]; + if (value) { + try { + RegExp(value); + } catch (e) { + throw new PluginOptionsError(`${key}: ${e.message}`); + } + } + }); + } + + return true; + }, }; export default memberAccessibilityPlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/react-class-lifecycle-methods.ts b/packages/ts-migrate-plugins/src/plugins/react-class-lifecycle-methods.ts index cbeb011..078009b 100644 --- a/packages/ts-migrate-plugins/src/plugins/react-class-lifecycle-methods.ts +++ b/packages/ts-migrate-plugins/src/plugins/react-class-lifecycle-methods.ts @@ -2,16 +2,24 @@ import ts from 'typescript'; import { Plugin } from 'ts-migrate-server'; import { getReactComponentHeritageType, isReactClassComponent } from './utils/react'; import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText'; +import { createValidate, Properties } from '../utils/validateOptions'; type Options = { force?: boolean }; +const optionProperties: Properties = { + force: { type: 'boolean' }, +}; + const reactClassLifecycleMethodsPlugin: Plugin = { name: 'react-class-lifecycle-methods', + run({ fileName, sourceFile, text, options }) { return /\.tsx$/.test(fileName) ? annotateReactComponentLifecycleMethods(sourceFile, text, options.force) : undefined; }, + + validate: createValidate(optionProperties), }; export default reactClassLifecycleMethodsPlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/react-class-state.ts b/packages/ts-migrate-plugins/src/plugins/react-class-state.ts index c02db86..24457c8 100644 --- a/packages/ts-migrate-plugins/src/plugins/react-class-state.ts +++ b/packages/ts-migrate-plugins/src/plugins/react-class-state.ts @@ -7,11 +7,13 @@ import { } from './utils/react'; import { collectIdentifiers } from './utils/identifiers'; import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText'; +import { AnyAliasOptions, validateAnyAliasOptions } from '../utils/validateOptions'; -type Options = { anyAlias?: string }; +type Options = AnyAliasOptions; const reactClassStatePlugin: Plugin = { name: 'react-class-state', + async run({ fileName, sourceFile, options }) { if (!fileName.endsWith('.tsx')) return undefined; @@ -93,6 +95,8 @@ const reactClassStatePlugin: Plugin = { return updateSourceText(sourceFile.text, updates); }, + + validate: validateAnyAliasOptions, }; export default reactClassStatePlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/react-default-props.ts b/packages/ts-migrate-plugins/src/plugins/react-default-props.ts index ac543e3..f21c883 100644 --- a/packages/ts-migrate-plugins/src/plugins/react-default-props.ts +++ b/packages/ts-migrate-plugins/src/plugins/react-default-props.ts @@ -1,11 +1,16 @@ import ts from 'typescript'; import { Plugin } from 'ts-migrate-server'; import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText'; +import { createValidate, Properties } from '../utils/validateOptions'; type Options = { useDefaultPropsHelper?: boolean; }; +const optionProperties: Properties = { + useDefaultPropsHelper: { type: 'boolean' }, +}; + /** * At first, we are going to check is there any * - `CompName.defaultProps = defaultPropsName;` @@ -16,6 +21,7 @@ const WITH_DEFAULT_PROPS_HELPER = `WithDefaultProps`; const reactDefaultPropsPlugin: Plugin = { name: 'react-default-props', + run({ sourceFile, text, options }) { const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration); const expressionStatements = sourceFile.statements.filter(ts.isExpressionStatement); @@ -344,6 +350,8 @@ const reactDefaultPropsPlugin: Plugin = { return updateSourceText(text, updates); }, + + validate: createValidate(optionProperties), }; // the target project might not have this as an internal dependency in project.json diff --git a/packages/ts-migrate-plugins/src/plugins/react-props.ts b/packages/ts-migrate-plugins/src/plugins/react-props.ts index eebc199..9c131c5 100644 --- a/packages/ts-migrate-plugins/src/plugins/react-props.ts +++ b/packages/ts-migrate-plugins/src/plugins/react-props.ts @@ -13,18 +13,33 @@ import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText'; import getTypeFromPropTypesObjectLiteral, { createPropsTypeNameGetter } from './utils/react-props'; import { getTextPreservingWhitespace } from './utils/text'; import { updateImports, DefaultImport, NamedImport } from './utils/imports'; +import { + AnyAliasOptions, + AnyFunctionAliasOptions, + Properties, + anyAliasProperty, + anyFunctionAliasProperty, + createValidate, +} from '../utils/validateOptions'; type Options = { - anyAlias?: string; - anyFunctionAlias?: string; shouldUpdateAirbnbImports?: boolean; shouldKeepPropTypes?: boolean; +} & AnyAliasOptions & + AnyFunctionAliasOptions; + +const optionProperties: Properties = { + ...anyAliasProperty, + ...anyFunctionAliasProperty, + shouldUpdateAirbnbImports: { type: 'boolean' }, + shouldKeepPropTypes: { type: 'boolean' }, }; export type PropTypesIdentifierMap = { [property: string]: string }; const reactPropsPlugin: Plugin = { name: 'react-props', + run({ fileName, sourceFile, options }) { if (!fileName.endsWith('.tsx')) return undefined; @@ -79,6 +94,8 @@ const reactPropsPlugin: Plugin = { : []; return updateSourceText(updatedSourceText, importUpdates); }, + + validate: createValidate(optionProperties), }; export default reactPropsPlugin; diff --git a/packages/ts-migrate-plugins/src/plugins/react-shape.ts b/packages/ts-migrate-plugins/src/plugins/react-shape.ts index 97d5dde..e4eee52 100644 --- a/packages/ts-migrate-plugins/src/plugins/react-shape.ts +++ b/packages/ts-migrate-plugins/src/plugins/react-shape.ts @@ -4,8 +4,15 @@ import path from 'path'; import { Plugin } from 'ts-migrate-server'; import getTypeFromPropTypesObjectLiteral from './utils/react-props'; import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText'; +import { + AnyAliasOptions, + AnyFunctionAliasOptions, + anyAliasProperty, + anyFunctionAliasProperty, + createValidate, +} from '../utils/validateOptions'; -type Options = { anyAlias?: string; anyFunctionAlias?: string }; +type Options = AnyAliasOptions & AnyFunctionAliasOptions; /** * first we are checking if we have imports of `prop-types` or `react-validators` @@ -13,6 +20,7 @@ type Options = { anyAlias?: string; anyFunctionAlias?: string }; */ const reactShapePlugin: Plugin = { name: 'react-shape', + run({ fileName, sourceFile, options, text }) { const baseName = path.basename(fileName); const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration); @@ -218,6 +226,11 @@ const reactShapePlugin: Plugin = { return updateSourceText(text, updates); }, + + validate: createValidate({ + ...anyAliasProperty, + ...anyFunctionAliasProperty, + }), }; function getTypeForTheShape( diff --git a/packages/ts-migrate-plugins/src/plugins/ts-ignore.ts b/packages/ts-migrate-plugins/src/plugins/ts-ignore.ts index be3ed6b..62ca00e 100644 --- a/packages/ts-migrate-plugins/src/plugins/ts-ignore.ts +++ b/packages/ts-migrate-plugins/src/plugins/ts-ignore.ts @@ -3,17 +3,25 @@ import ts from 'typescript'; import { Plugin } from 'ts-migrate-server'; import { isDiagnosticWithLinePosition } from '../utils/type-guards'; import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText'; +import { createValidate, Properties } from '../utils/validateOptions'; type Options = { useTsIgnore?: boolean }; +const optionProperties: Properties = { + useTsIgnore: { type: 'boolean' }, +}; + const tsIgnorePlugin: Plugin = { name: 'ts-ignore', + run({ getLanguageService, fileName, sourceFile, options }) { const diagnostics = getLanguageService() .getSemanticDiagnostics(fileName) .filter(isDiagnosticWithLinePosition); return getTextWithIgnores(sourceFile, diagnostics, options); }, + + validate: createValidate(optionProperties), }; export default tsIgnorePlugin; diff --git a/packages/ts-migrate-plugins/src/utils/validateOptions.ts b/packages/ts-migrate-plugins/src/utils/validateOptions.ts new file mode 100644 index 0000000..63c161a --- /dev/null +++ b/packages/ts-migrate-plugins/src/utils/validateOptions.ts @@ -0,0 +1,41 @@ +import { JSONSchema7, validate } from 'json-schema'; +import { PluginOptionsError } from 'ts-migrate-server'; + +export type Properties = JSONSchema7['properties']; + +export type AnyAliasOptions = { anyAlias?: string }; + +export const anyAliasProperty: Properties = { + anyAlias: { type: 'string' }, +}; + +export type AnyFunctionAliasOptions = { anyFunctionAlias?: string }; + +export const anyFunctionAliasProperty: Properties = { + anyFunctionAlias: { type: 'string' }, +}; + +export function createValidate(properties: Properties) { + return (options: unknown): options is Options => validateOptions(options, properties); +} + +export const validateAnyAliasOptions = createValidate(anyAliasProperty); + +export function validateOptions(options: unknown, properties: Properties): boolean { + if (typeof options !== 'object' || !options) { + throw new PluginOptionsError('options must be an object'); + } + const schema: JSONSchema7 = { + type: 'object', + properties, + additionalProperties: false, + }; + const validation = validate(options, schema); + if (!validation.valid) { + const message = validation.errors + .map((error) => `${error.property}: ${error.message}`) + .join('\n'); + throw new PluginOptionsError(message); + } + return true; +} diff --git a/packages/ts-migrate-plugins/tests/src/member-accessibility.test.ts b/packages/ts-migrate-plugins/tests/src/member-accessibility.test.ts index dc9c273..08810bd 100644 --- a/packages/ts-migrate-plugins/tests/src/member-accessibility.test.ts +++ b/packages/ts-migrate-plugins/tests/src/member-accessibility.test.ts @@ -1,3 +1,4 @@ +import { PluginOptionsError } from 'ts-migrate-server'; import { mockPluginParams } from '../test-utils'; import memberAccessibilityPlugin from '../../src/plugins/member-accessibility'; @@ -44,4 +45,15 @@ const o = { expect(result).toBe(text); }); + + it.each([{}, { defaultAccessibility: 'private' }])('accepts valid options: %p', (options) => { + expect(memberAccessibilityPlugin.validate!(options)).toBe(true); + }); + + it.each([42, { additional: true }, { defaultAccessibility: 'static' }, { privateRegex: '+' }])( + 'rejects invalid options: %p', + (options) => { + expect(() => memberAccessibilityPlugin.validate!(options)).toThrow(PluginOptionsError); + }, + ); }); diff --git a/packages/ts-migrate-server/src/index.ts b/packages/ts-migrate-server/src/index.ts index edbd412..f538306 100644 --- a/packages/ts-migrate-server/src/index.ts +++ b/packages/ts-migrate-server/src/index.ts @@ -1,7 +1,8 @@ import migrate, { MigrateConfig } from './migrate'; +import PluginOptionsError from './utils/PluginOptionsError'; import { Plugin as PluginType, PluginParams as Params } from '../types'; export type Plugin = PluginType; export type PluginParams = Params; -export { migrate, MigrateConfig }; +export { migrate, MigrateConfig, PluginOptionsError }; diff --git a/packages/ts-migrate-server/src/migrate/MigrateConfig.ts b/packages/ts-migrate-server/src/migrate/MigrateConfig.ts index 558492b..0d41a78 100644 --- a/packages/ts-migrate-server/src/migrate/MigrateConfig.ts +++ b/packages/ts-migrate-server/src/migrate/MigrateConfig.ts @@ -5,7 +5,7 @@ type InferOptions

= P extends Plugin ? O : never; export default class MigrateConfig { plugins: { plugin: Plugin; options: unknown }[] = []; - addPlugin

>(plugin: P, options: InferOptions

) { + addPlugin

>(plugin: P, options: InferOptions

): this { this.plugins.push({ plugin, options }); return this; } diff --git a/packages/ts-migrate-server/src/utils/PluginOptionsError.ts b/packages/ts-migrate-server/src/utils/PluginOptionsError.ts new file mode 100644 index 0000000..fa891a5 --- /dev/null +++ b/packages/ts-migrate-server/src/utils/PluginOptionsError.ts @@ -0,0 +1,6 @@ +export default class PluginOptionsError extends Error { + constructor(message = 'Plugin options validation error') { + super(message); + this.name = 'PluginOptionsError'; + } +} diff --git a/packages/ts-migrate-server/types/index.ts b/packages/ts-migrate-server/types/index.ts index c9f341d..ddeee40 100644 --- a/packages/ts-migrate-server/types/index.ts +++ b/packages/ts-migrate-server/types/index.ts @@ -15,6 +15,14 @@ export type PluginResult = string | void; export interface Plugin { name: string; run(params: PluginParams): Promise | PluginResult; + + /** + * Returns true if options is a valid options object for this plugin. + * If options is invalid, it throws a PluginOptionsError. + * + * This method should be implemented if TPluginOptions is anything other than unknown. + */ + validate?(options: unknown): options is TPluginOptions; } export type PluginWithOptions = { diff --git a/yarn.lock b/yarn.lock index 699a366..d177c98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2683,6 +2683,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== +"@types/json-schema@^7.0.7": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + "@types/json5@0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" @@ -6822,6 +6827,11 @@ json-schema@0.2.3: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.3.0.tgz#90a9c5054bd065422c00241851ce8d59475b701b" + integrity sha512-TYfxx36xfl52Rf1LU9HyWSLGPdYLL+SQ8/E/0yVyKG8wCCDaSrhPap0vEdlsZWRaS6tnKKLPGiEJGiREVC8kxQ== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"