constructor(config = {}, { init = true, verbosity, warnings, volume } = {}) {
    super();
    this.config = config;
    this.options = {};
    /** @type {PreprocessedTokens} */
    this.tokens = {};
    /** @type {Token[]} */
    this.allTokens = [];
    /** @type {Map<string, Token>} */
    this.tokenMap = new Map();
    /** @type {boolean | undefined} */
    this.usesDtcg = undefined;
    /** @type {LogConfig} */
    this.log = {
      warnings: warn,
      verbosity: defaultVerbosity,
      errors: {
        brokenReferences: throwBrokenReference,
      },
    };
    /** @type {string[]} */
    this.source = [];
    /** @type {string[]} */
    this.include = [];
    /** @type {ExpandConfig|undefined} */
    this.expand = undefined;
    /** @type {Record<string, PlatformConfig>} */
    this.platforms = {};
    /** @type {string[]} */
    this.parsers = [];
    /** @type {string[]} */
    this.preprocessors = [];
    if (volume) {
      // when a user sets a custom FS shim, mark it for later reference
      volume.__custom_fs__ = true;
    }
    /** @type {Volume} */
    this.volume = volume ?? fs;

    /**
     * Gets set after transform because filter happens on format level,
     * so we know they are transformed by then.
     * @type {TransformedTokens}
     */
    this.unfilteredTokens = {};
    /** @type {TransformedToken[]} */
    this.unfilteredAllTokens = [];

    this.hasInitialized = new Promise((resolve) => {
      this.hasInitializedResolve = resolve;
    });

    /**
     * Storing the platform specific transformed tokens so we can prevent re-running exportPlatform when we already know the outcome
     * Same thing for platform specific configs, we don't need to call transformConfig again if we already know the outcome
     */
    /** @type {Record<string,Dictionary>} */
    this._dictionaries = {};
    /** @type {Record<string,PlatformConfig>} */
    this._platformConfigs = {};

    // By default, always call async extend function when constructing new SD instance
    // However, for testability and managing error handling,
    // you can call constructor with { init: false }
    // and call SDInstance.extend() manually (and catch the error).
    if (init) {
      this.init({ verbosity, warnings });
    }
  }
@param {Config | string} config
@param {{
  init?: boolean,
  verbosity?: LogConfig['verbosity'],
  warnings?: LogConfig['warnings'],
  volume?: Volume
}} ctorOpts