Packing Sheets with Webpack
Webpack is a modern build tool for generating static sites. It has a robust JavaScript-powered plugin system1
SheetJS is a JavaScript library for reading and writing data from spreadsheets.
This demo uses Webpack and SheetJS to pull data from a spreadsheet and display the content in an HTML table. We'll explore how to load SheetJS in a Webpack 5 Asset Plugin and generate data for use in webpages.
The "Webpack 5 Demo" creates a complete website powered by a XLSX spreadsheet.
This demo covers static asset imports. For processing files in the browser, the "Bundlers" demo includes an example of importing the SheetJS library in a browser script.
Webpack 5 Asset Module​
Webpack 5 supports asset modules. With a special option, the loader will receive NodeJS Buffers that can be parsed. The dev server will even watch the files and reload the page in development mode!
The SheetJS NodeJS module can be imported from Webpack loader scripts.
The following diagram depicts the workbook waltz:
flowchart LR
file[(workbook\nfile)]
subgraph SheetJS operations
buffer(NodeJS\nBuffer)
aoo(array of\nobjects)
end
html{{HTML\nTABLE}}
file --> |webpack.config.js\ncustom rule| buffer
buffer --> |sheetjs-loader.js\ncustom plugin| aoo
aoo --> |src/index.js\nfrontend code| html
Webpack Config​
The Webpack configuration is normally saved to webpack.config.js
.
Required Settings​
module.rules
is an array of rule objects that controls module synthesis.2
For the SheetJS Webpack integration, the following properties are required:
-
test
describes whether the rule is relevant. If the property is a regular expression, Webpack will test the filename against thetest
property. -
use
lists the loaders that will process files matching thetest
. The loaders are specified using theloader
property of the loader object.
The following example instructs Webpack to use the sheetjs-loader.js
script
when the file name ends in .numbers
or .xls
or .xlsx
or .xlsb
:
// ...
module.exports = {
// ...
module: {
rules: [
{
/* `test` matches file extensions */
test: /\.(numbers|xls|xlsx|xlsb)$/,
/* use the loader script */
use: [ { loader: './sheetjs-loader' } ]
}
]
}
};
Recommended Settings​
It is strongly recommended to enable other Webpack features:
-
resolve.alias
defines path aliases. If data files are stored in one folder, an alias ensures that each page can reference the files using the same name3. -
devServer.hot
enables "hot module replacement"4, ensuring that pages will refresh in development mode when spreadsheets are saved.
The following example instructs Webpack to treat ~
as the root of the project
(so ~/data/pres.xlsx
refers to pres.xlsx
in the data folder) and to enable
live reloading:
// ...
module.exports = {
// ...
resolve: {
alias: {
/* `~` root of the project */
"~": __dirname
}
},
// ...
/* enable live reloading in development mode */
devServer: { static: './dist', hot: true }
};
SheetJS Loader​
The SheetJS loader script must be saved to the script referenced in the Webpack
configuration (sheetjs-loader.js
).
As with ViteJS, Webpack will interpret data as
UTF-8 strings. This corrupts binary formats including XLSX and XLS. To suppress
this behavior and instruct Webpack to pass a NodeJS Buffer
object, the loader
script must export a raw
property that is set to true
5.
The base export is expected to be the loader function. The loader receives the
file bytes as a Buffer, which can be parsed with the SheetJS read
method6.
read
returns a SheetJS workbook object7.
The loader in this demo will parse the workbook, pull the first worksheet, and
generate an array of row objects using the sheet_to_json
method8:
const XLSX = require("xlsx");
function loader(content) {
/* since `loader.raw` is true, `content` is a Buffer */
const wb = XLSX.read(content);
/* pull data from first worksheet */
var data = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
return `export default JSON.parse('${JSON.stringify(data)}')`;
}
/* ensure the function receives a Buffer */
loader.raw = true;
/* export the loader */
module.exports = loader;
Asset Imports​
Spreadsheets can be imported using the plugin. Assuming pres.xlsx
is stored
in the data
subfolder, ~/data/pres.xlsx
can be imported from any script:
import data from '~/data/pres.xlsx';
/* `data` is an array of objects from data/pres.xlsx */
const elt = document.createElement('div');
elt.innerHTML = "<table><tr><th>Name</th><th>Index</th></tr>" +
data.map((row) => `<tr>
<td>${row.Name}</td>
<td>${row.Index}</td>
</tr>`).join("") +
"</table>";
document.body.appendChild(elt);
Webpack 5 Demo​
This demo was last tested on 2024 April 06 against Webpack 5.91.0
Initial Setup​
- Create a new skeleton project:
mkdir sheetjs-wp5
cd sheetjs-wp5
npm init -y
npm install webpack@5.91.0 webpack-cli@5.1.4 webpack-dev-server@5.0.4 --save
mkdir -p dist
mkdir -p src
mkdir -p data
- Install the SheetJS NodeJS module:
npm i --save https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
- Save the following to
dist/index.html
:
<!DOCTYPE html>
<html>
<head>
<title>SheetJS + Webpack 5</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
- Save the following to
src/index.js
:
import data from '~/data/pres.xlsx';
const elt = document.createElement('div');
elt.innerHTML = "<table><tr><th>Name</th><th>Index</th></tr>" +
data.map((row) => `<tr>
<td>${row.Name}</td>
<td>${row.Index}</td>
</tr>`).join("") +
"</table>";
document.body.appendChild(elt);
- Save the following to
webpack.config.js
:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
static: './dist',
hot: true,
},
resolve: {
alias: {
"~": __dirname
}
},
module: {
rules: [
{
test: /\.(numbers|xls|xlsx|xlsb)$/,
use: [ { loader: './sheetjs-loader' } ]
}
]
}
};
- Save the following to
sheetjs-loader.js
:
const XLSX = require("xlsx");
function loader(content) {
/* since `loader.raw` is true, `content` is a Buffer */
const wb = XLSX.read(content);
/* pull data from first worksheet */
var data = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
return `export default JSON.parse('${JSON.stringify(data)}')`;
}
/* ensure the function receives a Buffer */
loader.raw = true;
module.exports = loader;
- Download https://docs.sheetjs.com/pres.xlsx and save to the
data
folder:
curl -L -o data/pres.xlsx https://docs.sheetjs.com/pres.xlsx
Live Reload Test​
-
Open the test file
data/pres.xlsx
in a spreadsheet editor like Excel. -
Start the development server:
npx webpack serve --mode=development
The terminal will print URLs for the development server:
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
- Open the
Loopback
address (http://localhost:8080
) in a web browser.
It should display a table of Presidents with "Name" and "Index" columns
- Add a new row to the spreadsheet (set
A7
to "SheetJS Dev" andB7
to 47) and save the file.
After saving the file, the page should automatically refresh with the new data.
Static Site Test​
- Stop Webpack and build the site:
npx webpack --mode=production
The final site will be placed in the dist
folder.
- Start a local web server to host the
dist
folder:
npx http-server dist
The command will print a list of URLs.
- Open one of the URLs printed in the previous step (
http://localhost:8080
) and confirm that the same data is displayed.
To verify that the page is independent of the spreadsheet, make some changes to the file and save. The page will not automatically update.
To verify that the data was added to the page, append main.js
to the URL
(http://localhost:8080/main.js
) and view the source. The source will include
president names. It will not include SheetJS library references!
Footnotes​
-
See
module.rules
in the Webpack documentation. ↩ -
See
resolve.alias
in the Webpack documentation. ↩ -
See "Hot Module Replacement" in the Webpack documentation. ↩
-
See "Raw" Loader in the Webpack documentation. ↩
-
See "Workbook Object" ↩