clubmate.fi

A good[ish] website

Web development blog, loads of UI and JavaScript topics

Import JavaScript as a string with WebPack v5 (and Gatsby)

Filed under: Tooling— Tagged with: WebPack, gatsby

How to import some JavaScript files as plain text without WebPack processing at all.

Why import as a string?

It could be handy in a coding blog or code documentations setups, you can render the component and show its code in a code block. Also .txt files are always just raw text.

Here’s our test JS file, which happens to be a React component, it does nothing interesting:

import React from 'react'
import PropTypes from 'prop-types'

const TestComp = props => <div>{props.children}</div>

TestComp.propTypes = { children: PropTypes.node }

export default TestComp

Configure WebPack

Webpack v4 had a concept of raw-loader it worked something like this: import myModule from 'raw-loader!my-module', but it was removed in v5 in favor of the asset/source concept.

Instead of using the raw-loader type, slap a query param ?raw (can be anything tho) at the end of the path when importing, this tells WebPack that it should import the file as a string:

import test from './TestComp.js?raw'

Then in WebPack config file, target raw with a resourceQuery and set the type to 'asset/source':

module: {
  rules: [
    // ...
    {
      resourceQuery: /raw/,
      type: 'asset/source'
    }
  ]
}

If you import the file now, it does import it as a string, but it might look something like this:

var TestComp = function TestComp(props) {
  return /*#__PURE__*/ React.createElement(
    'div',
    {
      __self: _this,
      __source: {
        fileName: _jsxFileName,
        lineNumber: 4,
        columnNumber: 27
      }
    },
    props.children
  )
}

That’s because WebPack transpiled the JSX into JS. You need to tell WebPack to stop processing the raw files, like so:

module: {
  rules: [
    // ...
    {
      test: /\.m?js$/,
      resourceQuery: { not: [/raw/] },      use: [ /* stuff */ ]
    },
    {
      resourceQuery: /raw/,
      type: 'asset/source',
    }
  ]
},

If you’re just interested about WebPack you can stop reading now, rest of the article deals more-or-less with Gatsby.

Configuring WebPack in Gatsby based MDX system

Gatsby is built around WebPack and it offers handy hooks to prod the WebPack config, for example onCreateWebpackConfig, where you get actions like setWebpackConfig and replaceWebpackConfig:

// gatsby-node.js
exports.onCreateWebpackConfig = ({ actions, getConfig }) => {
  const config = getConfig()

  config.module.rules = [
    ...config.module.rules.map(rule => {
      // This `/\.(js|mjs|jsx)$/` regex is added by the MDX plugin and I noticed
      // that I need to target that one
      if (String(rule.test) === String(/\.(js|mjs|jsx)$/)) {
        rule = { ...rule, resourceQuery: { not: [/raw/] } }      }

      return rule
    })
  ]

  actions.replaceWebpackConfig(config)

  actions.setWebpackConfig({
    module: {
      rules: [
        {
          resourceQuery: /raw/,          type: 'asset/source'
        }
      ]
    }
  })
}

You can console log config.module.rules to see what rules you’ve got there.

Check out Gatsby’s docs on editing WebPack config. And from the Gatsby GitHub you can find the file that generates the config.

There’s one more thing before this works 👇

The ugly react-refresh code in Gatsby

Gatsby uses react-refresh, and apparently it works by splicing a bunch of code into every module. This is all good, I don’t care how it works if it just works. Expect when you import the module as a string you really want only the code you want and nothing more.

You can see the module tucked in there, but that extra code kind of spoils our whole idea here:

$RefreshRuntime$ = require('/Users/bob/web/foo/node_modules/react-refresh/runtime.js')
$RefreshSetup$(module.id)

import React from 'react'
import PropTypes from 'prop-types'

const TestComp = props => <div>{props.children}</div>

TestComp.propTypes = { children: PropTypes.node }

export default TestComp


const currentExports = __react_refresh_utils__.getModuleExports(module.id)
__react_refresh_utils__.registerExportsForReactRefresh(
  currentExports,
  module.id
)

if (module.hot) {
  const isHotUpdate = !!module.hot.data
  const prevExports = isHotUpdate ? module.hot.data.prevExports : null

  if (__react_refresh_utils__.isReactRefreshBoundary(currentExports)) {
    module.hot.dispose(
      /**
       * A callback to performs a full refresh if React has unrecoverable errors,
       * and also caches the to-be-disposed module.
       * @param {*} data A hot module data object from Webpack HMR.
       * @returns {void}
       */
      function hotDisposeCallback(data) {
        // We have to mutate the data object to get data registered and cached
        data.prevExports = currentExports
      }
    )
    module.hot.accept(
      /**
       * An error handler to allow self-recovering behaviours.
       * @param {Error} error An error occurred during evaluation of a module.
       * @returns {void}
       */
      function hotErrorHandler(error) {
        if (
          typeof __react_refresh_error_overlay__ !== 'undefined' &&
          __react_refresh_error_overlay__
        ) {
          __react_refresh_error_overlay__.handleRuntimeError(error)
        }

        if (
          typeof __react_refresh_test__ !== 'undefined' &&
          __react_refresh_test__
        ) {
          if (window.onHotAcceptError) {
            window.onHotAcceptError(error.message)
          }
        }

        __webpack_require__.c[module.id].hot.accept(hotErrorHandler)
      }
    )

    if (isHotUpdate) {
      if (
        __react_refresh_utils__.isReactRefreshBoundary(prevExports) &&
        __react_refresh_utils__.shouldInvalidateReactRefreshBoundary(
          prevExports,
          currentExports
        )
      ) {
        module.hot.invalidate()
      } else {
        __react_refresh_utils__.enqueueUpdate(
          /**
           * A function to dismiss the error overlay after performing React refresh.
           * @returns {void}
           */
          function updateCallback() {
            if (
              typeof __react_refresh_error_overlay__ !== 'undefined' &&
              __react_refresh_error_overlay__
            ) {
              __react_refresh_error_overlay__.clearRuntimeErrors()
            }
          }
        )
      }
    }
  } else {
    if (
      isHotUpdate &&
      __react_refresh_utils__.isReactRefreshBoundary(prevExports)
    ) {
      module.hot.invalidate()
    }
  }
}

Unfortunately I’m not sure how to get rid of it. But I wrote a little helper which grabs everything between two special comments, it solves the problem for now:

/**
 * This helper matches all text between two special comments: `// start-block`
 * and `// end-block` and returns it as is.
 *
 * @param {string} string - The processed string
 * @returns {string}
 */
const trimmer = (string = '') => {
  return string.match(/(?<=\/\/ ?start-block\n)[\s\S]*(?=\/\/ ?end-block)/)?.[0]
}

export default trimmer

You need add // start-block and // end-block comments into the code, it’s a bit extra, but it works consistently:

// start-block
import React from 'react'
import PropTypes from 'prop-types'

const TestComp = props => <div>{props.children}</div>

TestComp.propTypes = { children: PropTypes.node }

export default TestComp
// end-block
This approach could actually come handy in some cases. You could put imports before the start comment, and exports after the closing comment, so they’re not included in the output to only show the essential bits. And the module would still work.

Usage example

Here’s an example MDX file using a JavaScript module imported as a string:

---
title: Import JS files as string
date: 2022-01-19 22:31:44
---

import test from './TestComp.js?raw'
import trimmer from './trimmer'

Look at my code:

<pre class="language-js">
  <code class="language-js">{trimmer(test)}</code>
</pre>

Drawbacks

You can’t use code highlight plugins like gatsby-remark-prismjs or gatsby-remark-vscode because how would you stuff a variable inside MDX codeblock? Everything between these ``` is code. But you could try prism-react-renderer for example.

Conclusions

Importing files this way really makes it easier to render code demos, you can change the file in one place, and the changes are reflected in the blog post or documentation article etc.

Hope this was helpful, thanks for grazing your eyes on the lust fields of my content!

Comments would go here, but the commenting system isn’t ready yet, sorry.

  • © 2022 Antti Hiljá
  • About
  • All rights reserved yadda yadda.
  • I can put just about anything here, no one reads the footer anyways.
  • I love u!