0

We are currently using a file shadowing mechanism similar to the gatsby file shadowing: https://www.gatsbyjs.com/docs/conceptual/how-shadowing-works/ With this solution we were able to have one next.js repository and build for different clients and their customizations.

When we are trying to update to webpack 5 we experienced 2 problems:

  1. File watching of the shadowed files is not given anymore for some reasons
  2. It seems that it´s mandatory to provide a serializer to properly cache the results
[webpack.cache.PackFileCacheStrategy] Skipped not serializable cache item 'Compilation/modules|/Users/michaelkuczera/Projects/lyska/core-shop/node_modules/next/dist/build/babel/loader/index.js??ruleSet[1].rules[2].use!/Users/michaelkuczera/Projects/lyska/core-shop/src/core/config/config.helper.ts': No serializer registered for WebpackFileShadowPlugin

I tried different things to fix this issue, but couldn´t get it to work and i don´t know if the 2 problems are related and i´m actually stuck.

I tried to make a custom loader because it seems more accurate. It works fine on the first look, but with the following file structure given it came to an end:

  • src
  • core
    • component
  • client1
    • component
    • helper

In this case the overwritten component of the client is requesting a file that is only available in his own client folder. The loader can´t actually resolve the import of ./helper

The following loader is used:

const path = require('path')
const fs = require('fs')
const loaderUtils = require('loader-utils')

module.exports = function(source) {
  const callback = this.async()

  const { layers } = loaderUtils.getOptions(this)

  ;(async () => {
    const [projectRoot, component] = this.resourcePath.split(
      path.join('src', 'core')
    )

    let overwritable
    let usedLayer

    layers.forEach(layer => {
      if (!component) {
        return
      }

      // construct layer path
      const possibleLayerPath = `${path.join(
        projectRoot,
        'src',
        layer,
        component
      )}`

      // check if file exists in customizations
      const exists = fs.existsSync(possibleLayerPath)

      if (exists) {
        overwritable = possibleLayerPath
        usedLayer = layer
      }
    })

    if (overwritable) {
      // output customization and add path to watchable files
      const data = fs.readFileSync(overwritable, 'utf8')

      this.addDependency(overwritable)
      return data
    }

    return source
  })().then(
    res => callback(undefined, res),
    err => callback(err)
  )
}

Has anybody an idea to resolve the issue either with the Plugin or the Loader?

I would appreciate any help

Plugin source Code:

const fs = require('fs')
const path = require('path')
const checkPossibleComponentPath = require('./checkPossiblePath')

const pathWithoutExtension = fullPath => {
  const parsed = path.parse(fullPath)
  return path.join(parsed.dir, parsed.name)
}

module.exports = class WebpackFileShadowPlugin {
  constructor(layers) {
    this.layers = [...layers, 'core']
  }

  apply(resolver) {
    const describedRelative = resolver.getHook('describedRelative')

    resolver
      .getHook('resolve')
      .tapAsync(
        'WebpackFileShadowPlugin',
        (request, resolveContext, callback) => {
          const requestedLayer = this.getMatchingLayerForPath(request.path)

          // requested file is not in a layer, and not shadowable
          if (!requestedLayer) {
            return callback()
          }

          // get the location of the component relative to `src/${layer}`
          const [projectRoot, component] = request.path.split(
            path.join('src', requestedLayer)
          )

          // If a shadowing file requests its original file, then let the request go through
          if (
            request.context.issuer &&
            this.requestPathIsIssuerShadowPath({
              component,
              requestPath: request.path,
              issuerPath: request.context.issuer,
            })
          ) {
            return resolver.doResolve(
              describedRelative,
              request,
              null,
              {},
              callback
            )
          }

          // Shadowing algorithm
          const componentPath = this.resolveComponentPath({
            projectRoot,
            component,
          })

          if (componentPath) {
            return resolver.doResolve(
              describedRelative,
              { ...request, path: componentPath || request.path },
              null,
              {},
              callback
            )
          } else {
            return callback()
          }
        }
      )
  }

  getMatchingLayerForPath(filepath) {
    // find out which layer we're requiring from
    return this.layers.find(layer => filepath.includes(path.join('src', layer)))
  }

  requestPathIsIssuerShadowPath({ component, issuerPath }) {
    const issuerLayer = this.getMatchingLayerForPath(issuerPath)

    // Issuer is not a shadowable file
    if (!issuerLayer) return false

    const [, issuerComponent] = pathWithoutExtension(issuerPath).split(
      path.join('src', issuerLayer)
    )

    const sameModule =
      component === issuerComponent ||
      path.join(component, 'index') === issuerComponent

    return sameModule
  }

  resolveComponentPath({ projectRoot, component }) {
    // check configured layers for a shadowing file
    return this.layers
      .map(layer => path.join(projectRoot, 'src', layer, component))
      .find(checkPossibleComponentPath)
  }
}

  • Which plugin are you actually using? A quick Google doesn't give any results for the `WebpackFileShadowPlugin` the cache error/warning mentions. – Kelvin Schoofs Jul 25 '21 at 15:20
  • It´s our custom plugin based on the gatsby file shadowing linked. I attached the source code – Michael Kuczera Jul 25 '21 at 15:24
  • Perhaps you can get some inspiration for Webpack 5 (and previous versions) from [`webpack-virtual-modules`](https://www.npmjs.com/package/webpack-virtual-modules), or worst case, make use of it. Their approach is "writing" files into Webpack's cached filesystem, which is of course a whole other direction from your approach, but perhaps of use. Their code of triggering the file watcher's `change` might perhaps be modified to add actual file paths to the watcher. All this doesn't explain why it stopped watching, though. Webpack 5 did change quite a bit since v4 of course. – Kelvin Schoofs Jul 25 '21 at 15:33
  • I'm wondering though: might it be that your plugin is preventing the loader to work? I don't know myself, the idea just suddenly popped up in my head. After all, if your plugin remaps `src/core/component` to `src/client1/component`, wouldn't the loader receive the remapped value (`client1/component`) and be unaware it's actually working with `core/component` and needs to add `client1/component` as a dependency? – Kelvin Schoofs Jul 25 '21 at 15:52
  • Will check the virtual-modules plugin. Thank you. – Michael Kuczera Jul 25 '21 at 15:54
  • The other one: I actually call this.addDependency. Through this the hot reloading is working fine, but it seems that it looses the reference where he is "staying". If i add one empty helper file in the core it´s working fine. I also tried to add the folder as a dependency, but without any effect. – Michael Kuczera Jul 25 '21 at 15:55
  • I'd imagine that if your plugin says "you can find module `core/component` actually at file `client1/component`", Webpack would think the module `core/component` is (only) the file `client1/component` and only watch that. That would explain why `core/component` wouldn't be watched anymore. I'd still log the `resourcePath` in your loader, to make sure it actually calls `addDependency`. – Kelvin Schoofs Jul 25 '21 at 16:01
  • Ah, good point. The resourcePath is actually still the same as before calling the addDependency – Michael Kuczera Jul 25 '21 at 16:12

0 Answers0