A good[ish] website
Web development blog, loads of UI and JavaScript topics
How to import some JavaScript files as plain text without WebPack processing at all.
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
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.
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 👇
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
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>
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.
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.