async extend(config = this.config, opts = {}) {
    // by default, if extend is called it means extending the current instance
    // with a new instance without mutating the original
    if (!opts.mutateOriginal) {
      const newSD = new StyleDictionary(deepmerge(this.options, config), {
        init: false,
        verbosity: opts.verbosity,
        warnings: opts.warnings,
        volume: opts.volume,
      });
      return newSD.init(opts);
    }

    /** @type {Config} */
    let options;
    /** @type {Tokens} */
    let inlineTokens = {};
    /** @type {Tokens} */
    let includeTokens = {};
    /** @type {Tokens} */
    let sourceTokens = {};

    // Overloaded method, can accept a string as a path that points to a JS or
    // JSON file or a plain object. Potentially refactor.
    if (typeof config === 'string') {
      options = /** @type {Config} */ (await loadFile(config, this.volume));
    } else {
      options = config;
    }

    this.log = {
      // our defaults
      ...this.log,
      // user log options override the defaults
      ...options.log,
      // verbosity/warnings is a bit more complex
      // can be passed imperatively by constructor (e.g. when using CLI --verbose / --silent / --no-warn)
      // otherwise verbosity in user config or fallback to default
      verbosity: opts.verbosity ?? options.log?.verbosity ?? this.log.verbosity,
      warnings: opts.warnings ?? options.log?.warnings ?? this.log.warnings,
    };
    this.options = {
      ...options,
      log: this.log,
    };

    // SD Config options should be passed to class instance as well
    Object.entries(this.options).forEach(([key, val]) => {
      // Bit of a type hack, making the assumption that any property in options can be set as a prop on StyleDictonary instance
      const _key = /** @type {keyof StyleDictionary} */ (key);
      this[_key] = val;
    });

    // Try to detect DTCG if not specified by user in options and tokens is passed imperatively
    if (Object.entries(this.tokens).length > 0 && this.usesDtcg === undefined) {
      this.usesDtcg = detectDtcgSyntax(this.tokens);
    }

    const appliedParsers = Object.fromEntries(
      Object.entries(this.hooks.parsers ?? {}).filter(([key]) => this.parsers.includes(key)),
    );

    // grab the inline tokens, ones either defined in the configuration object
    // or that already exist from extending another style dictionary instance
    // with `tokens` keys
    inlineTokens = deepExtend([{}, this.tokens || {}]);

    // Update tokens with includes from dependencies
    if (this.include) {
      if (!Array.isArray(this.include)) throw new Error('include must be an array');
      const result = await combineJSON(
        this.include,
        true,
        undefined,
        false,
        appliedParsers,
        this.usesDtcg,
        this.volume,
      );

      includeTokens = result.tokens;
      // If it wasn't known yet whether DTCG is used, combineJSON util will have auto-detected it by now
      if (this.usesDtcg === undefined) {
        this.usesDtcg = result.usesDtcg;
      }
    }

    // Update tokens with current package's source
    // These highest precedence
    if (this.source) {
      if (!Array.isArray(this.source)) throw new Error('source must be an array');

      const result = await combineJSON(
        this.source,
        true,
        // TODO: abstract into its own reusable interface, since it is used in deepExtend util as well
        /** @param {{ target: Tokens, copy: Tokens, path: string[], key: string }} prop */
        function Collision(prop) {
          GroupMessages.add(
            PROPERTY_VALUE_COLLISIONS,
            `Collision detected at: ${prop.path.join('.')}! Original value: ${
              prop.target[prop.key]
            }, New value: ${prop.copy[prop.key]}`,
          );
        },
        true,
        appliedParsers,
        this.usesDtcg,
        this.volume,
      );

      sourceTokens = result.tokens;
      // If it wasn't known yet whether DTCG is used, combineJSON util will have auto-detected it
      if (this.usesDtcg === undefined) {
        this.usesDtcg = result.usesDtcg;
      }

      const propValCollisionCount = GroupMessages.count(PROPERTY_VALUE_COLLISIONS);
      if (propValCollisionCount > 0) {
        const collisions = GroupMessages.flush(PROPERTY_VALUE_COLLISIONS).join('\n');
        let warn = `\nToken collisions detected (${propValCollisionCount}):\n`;
        if (this.log.verbosity === verbose) {
          warn += `\n${collisions}\n\n`;
        } else {
          warn += verbosityInfo;
        }
        if (this.log?.warnings === error) {
          throw new Error(warn);
        } else if (this.log?.verbosity !== silent && this.log.warnings !== disabled) {
          // eslint-disable-next-line no-console
          console.log(chalk.rgb(255, 140, 0).bold(warn));
        }
      }
    }
    this.options = { ...this.options, usesDtcg: this.usesDtcg };

    // Merge inline, include, and source tokens
    let preprocessedTokens = /** @type {PreprocessedTokens} */ (
      deepExtend([{}, inlineTokens, includeTokens, sourceTokens])
    );

    preprocessedTokens = await preprocess(
      preprocessedTokens,
      this.preprocessors,
      this.hooks.preprocessors,
      this.options,
    );
    if (this.usesDtcg) {
      // this is where they go from type Tokens -> Preprocessed tokens because the prop $type is removed
      preprocessedTokens = typeDtcgDelegate(preprocessedTokens);
    }

    this.tokens = preprocessedTokens;
    this.allTokens = convertTokenData(this.tokens, { output: 'array', usesDtcg: this.usesDtcg });
    this.tokenMap = convertTokenData(this.allTokens, { output: 'map', usesDtcg: this.usesDtcg });

    if (this.shouldRunExpansion(this.expand)) {
      this.tokenMap = expandTokens(this.tokenMap, this.options);
      this.allTokens = convertTokenData(this.tokenMap, {
        output: 'array',
        usesDtcg: this.usesDtcg,
      });
      this.tokens = /** @type {PreprocessedTokens} */ (
        convertTokenData(this.tokenMap, { output: 'object', usesDtcg: this.usesDtcg })
      );
    }

    this.hasInitializedResolve(null);

    // For chaining
    return this;
  }
@param {Config | string} [config]
@param {{
  mutateOriginal?: boolean,
  verbosity?: LogConfig['verbosity'],
  warnings?: LogConfig['warnings'],
  volume?: Volume
}} [opts]
@returns {Promise<StyleDictionary>}