text: const path = require(`path`)
  const debug = require(`debug`)(`gatsby:component-shadowing`)
  const fs = require(`fs`)
  const _ = require(`lodash`)
  
  const pathWithoutExtension = fullPath => {
    const parsed = path.parse(fullPath)
    return path.join(parsed.dir, parsed.name)
  }
  
  module.exports = class GatsbyThemeComponentShadowingResolverPlugin {
    cache = {}
  
    constructor({ projectRoot, themes }) {
      debug(`themes list`, themes.map(({ themeName }) => themeName))
      this.themes = themes
      this.projectRoot = projectRoot
    }
  
    apply(resolver) {
      resolver.plugin(`relative`, (request, callback) => {
        const matchingThemes = this.getMatchingThemesForPath(request.path)
  
        // 0 matching themes happens a lot for paths we don't want to handle
        // > 1 matching theme means we have a path like
        //   `gatsby-theme-blog/src/components/gatsby-theme-something/src/components`
        if (matchingThemes.length > 1) {
          throw new Error(
            `Gatsby can't differentiate between themes ${matchingThemes.join(
              ` and `
            )} for path ${request.path}`
          )
        }
  
        if (matchingThemes.length !== 1) {
          return callback()
        }
  
        // theme is the theme package from which we're requiring the relative component
        const [theme] = matchingThemes
        // get the location of the component relative to src/
        const [, component] = request.path.split(path.join(theme, `src`))
  
        if (
          /**
           * if someone adds
           * ```
           * modules: [path.resolve(__dirname, 'src'), 'node_modules'],
           * ```
           * to the webpack config, `issuer` is `null`, so we skip this check.
           * note that it's probably a bad idea in general to set `modules`
           * like this in a theme, but we also shouldn't artificially break
           * people that do.
           */
          request.context.issuer &&
          /**
           * An issuer is the file making the require request. It can
           * be in a user's site or a theme. If the issuer is requesting
           * a path in the shadow chain that it participates in, then we
           * will let the request through as normal. Otherwise, we
           * engage the shadowing algorithm.
           */
          this.requestPathIsIssuerShadowPath({
            requestPath: request.path,
            issuerPath: request.context.issuer,
            userSiteDir: path.resolve(`.`),
          })
        ) {
          return resolver.doResolve(
            `describedRelative`,
            request,
            null,
            {},
            callback
          )
        }
  
        // This is the shadowing algorithm.
        const builtComponentPath = this.resolveComponentPath({
          matchingTheme: theme,
          themes: this.themes,
          component,
        })
  
        return resolver.doResolve(
          `describedRelative`,
          { ...request, path: builtComponentPath || request.path },
          null,
          {},
          callback
        )
      })
    }
  
    // check the cache, the user's project, and finally the theme files
    resolveComponentPath({ matchingTheme: theme, themes: ogThemes, component }) {
      // don't include matching theme in possible shadowing paths
      const themes = ogThemes.filter(({ themeName }) => themeName !== theme)
      if (!this.cache[`${theme}-${component}`]) {
        this.cache[`${theme}-${component}`] = [
          path.join(path.resolve(`.`), `src`, theme),
        ]
          .concat(
            Array.from(themes)
              .reverse()
              .map(({ themeDir }) => path.join(themeDir, `src`, theme))
          )
          .map(dir => path.join(dir, component))
          .find(possibleComponentPath => {
            debug(`possibleComponentPath`, possibleComponentPath)
            let dir
            try {
              // we use fs/path instead of require.resolve to work with
              // TypeScript and alternate syntaxes
              dir = fs.readdirSync(path.dirname(possibleComponentPath))
            } catch (e) {
              return false
            }
            const exists = dir
              .map(filepath => {
                const ext = path.extname(filepath)
                const filenameWithoutExtension = path.basename(filepath, ext)
                return filenameWithoutExtension
              })
              .includes(
                path.basename(
                  possibleComponentPath,
                  path.extname(possibleComponentPath)
                )
              )
            return exists
          })
      }
  
      return this.cache[`${theme}-${component}`]
    }
  
    getMatchingThemesForPath(filepath) {
      // find out which theme's src/components dir we're requiring from
      const allMatchingThemes = this.themes.filter(({ themeName }) =>
        filepath.includes(path.join(themeName, `src`))
      )
  
      // The same theme can be included twice in the themes list causing multiple
      // matches. This case should only be counted as a single match for that theme.
      return _.uniq(allMatchingThemes.map(({ themeName }) => themeName))
    }
  
    // given a theme name, return all of the possible shadow locations
    getBaseShadowDirsForThemes(theme) {
      return Array.from(this.themes)
        .reverse()
        .map(({ themeName, themeDir }) => {
          if (themeName === theme) {
            return path.join(themeDir, `src`)
          } else {
            return path.join(themeDir, `src`, theme)
          }
        })
    }
  
    requestPathIsIssuerShadowPath({ requestPath, issuerPath, userSiteDir }) {
      // get the issuer's theme
      const matchingThemes = this.getMatchingThemesForPath(requestPath)
      if (matchingThemes.length !== 1) {
        return false
      }
      const [theme] = matchingThemes
  
      // get the location of the component relative to src/
      const [, component] = requestPath.split(path.join(theme, `src`))
  
      // get list of potential shadow locations
      const shadowFiles = this.getBaseShadowDirsForThemes(theme)
        .concat(path.join(userSiteDir, `src`, theme))
        .map(dir => path.join(dir, component))
  
      // if the issuer is requesting a path that is a potential shadow path of itself
      return shadowFiles.includes(pathWithoutExtension(issuerPath))
    }
  }
  