9.0 KiB
title | sidebar_label | pagination_prev | pagination_next | sidebar_custom_props | ||
---|---|---|---|---|---|---|
Building Sheets with ESBuild | ESBuild | demos/net/index | demos/mobile/index |
|
import current from '/version.js'; import CodeBlock from '@theme/CodeBlock';
ESBuild 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 ESBuild 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 ESBuild loader and generate data for use in webpages.
The "Demo" creates a complete website powered by a XLSX spreadsheet.
:::info pass
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.
:::
ESBuild Loader
ESBuild supports custom loader plugins. The loader receives an absolute path to the spreadsheet on the filesystem.
The SheetJS NodeJS module can be imported from ESBuild loader plugins.
:::info pass
ESBuild loader plugins use ECMAScript Modules. The plugin ultimately receives
raw paths to files. fs
must be manually imported:
import * as XLSX from 'xlsx';
/* load 'fs' for readFile and writeFile support */
import * as fs from 'fs';
XLSX.set_fs(fs);
:::
The following diagram depicts the workbook waltz:
flowchart LR
subgraph ESBuild Custom Plugin in build.mjs
file[(workbook\nfile)]
wb(((SheetJS\nWorkbook)))
aoo(array of\nobjects)
end
html{{HTML\nTABLE}}
file --> |`readFile`\n\n| wb
wb --> |`sheet_to_json`\n\n| aoo
aoo --> |app.js\nfrontend code| html
ESBuild Config
Plugins can be referenced in the plugins
array of the build config object:
import * as esbuild from 'esbuild'
// highlight-next-line
let sheetjsPlugin = {
name: 'sheetjs',
setup(build) {
// ...
}
};
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
// highlight-next-line
plugins: [sheetjsPlugin],
})
Registering File Extensions
The setup
method receives the build options. Handlers for custom files should
be added using build.onLoad
.
The first argument to onLoad
is a configuration object. The filter
property
is expected to be a regular expression. The following regular expression matches
NUMBERS, XLSX, XLS, and XLSB files:
const EXTS = /.(numbers|xlsx|xls|xlsb)$/;
The second argument to onLoad
is a callback that receives an arguments object.
The path
property of the object is the absolute path to the file.
setup(build) {
build.onLoad({ filter: EXTS }, (args) => {
const path = args.path;
// ...
});
},
SheetJS Operations
The SheetJS readFile
method2 will directly read the file on the filesystem.
The return value is a SheetJS workbook object3.
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
method4.
:::caution pass
JSON does not natively support Dates! JSON.stringify
will generate strings.
Through a clever workaround, it is possible to encode dates separately and recover the Date objects in the generated code module.
:::
import * as XLSX from 'xlsx';
import * as fs from 'fs';
XLSX.set_fs(fs);
/* plugin */
let sheetjsPlugin = {
name: 'sheetjs',
setup(build) {
/* match NUMBERS, XLSX, XLS, and XLSB files */
const EXTS = /.(numbers|xlsx|xls|xlsb)$/;
/* this method will be called once for each referenced file */
build.onLoad({ filter: EXTS }, (args) => {
/* parse file from filesystem */
const wb = XLSX.readFile(args.path);
/* get first worksheet */
const ws = wb.Sheets[wb.SheetNames[0]];
/* workaround for JSON limitation */
Date.prototype.toJSON2 = Date.prototype.toJSON;
Date.prototype.toJSON = function() { return {d:this.toISOString()}; };
/* generate row objects */
const data = XLSX.utils.sheet_to_json(ws);
/* generate final module code */
const res = JSON.stringify(data);
Date.prototype.toJSON = Date.prototype.toJSON2;
const contents = `const data = ${res};
data.forEach(row => {
Object.keys(row).forEach(k => {
if(row[k]?.d) row[k] = new Date(row[k].d);
})
});
export default data;`
return { contents, loader: 'js' };
});
},
};
Asset Imports
Spreadsheets can be imported using the plugin. Assuming pres.xlsx
is stored
in the same folder as the script, ./pres.xlsx
will be a data module:
import data from './pres.xlsx';
/* `data` is an array of objects from ./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);
Demo
:::note
This demo was last tested on 2023 October 21 against ESBuild 0.19.5
:::
Initial Setup
- Create a new skeleton project:
mkdir sheetjs-esb
cd sheetjs-esb
npm init -y
npm i --save esbuild@0.19.5
- Install the SheetJS NodeJS module:
{\ npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
}
- Save the following to
index.html
:
<!DOCTYPE html>
<html>
<head>
<title>SheetJS + ESBuild</title>
</head>
<body>
<script src="out.js"></script>
</body>
</html>
- Save the following to
app.js
:
import data from './pres.numbers'
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
build.mjs
:
import * as esbuild from 'esbuild'
import * as XLSX from 'xlsx';
import * as fs from 'fs';
XLSX.set_fs(fs);
let sheetjsPlugin = {
name: 'sheetjs',
setup(build) {
/* match NUMBERS, XLSX, XLS, and XLSB files */
const EXTS = /.(numbers|xlsx|xls|xlsb)$/;
/* this method will be called once for each referenced file */
build.onLoad({ filter: EXTS }, (args) => {
/* parse file from filesystem */
const wb = XLSX.readFile(args.path);
/* get first worksheet */
const ws = wb.Sheets[wb.SheetNames[0]];
/* workaround for JSON limitation */
Date.prototype.toJSON2 = Date.prototype.toJSON;
Date.prototype.toJSON = function() { return {d:this.toISOString()}; };
/* generate row objects */
const data = XLSX.utils.sheet_to_json(ws);
/* generate final module code */
const res = JSON.stringify(data);
Date.prototype.toJSON = Date.prototype.toJSON2;
const contents = `const data = ${res};
data.forEach(row => {
Object.keys(row).forEach(k => {
if(row[k]?.d) row[k] = new Date(row[k].d);
})
});
export default data;`
return { contents, loader: 'js' };
});
},
};
await esbuild.build({
entryPoints: ['app.js'],
bundle: true,
outfile: 'out.js',
plugins: [sheetjsPlugin],
});
- Download https://sheetjs.com/pres.numbers and save to the project folder:
curl -LO https://sheetjs.com/pres.numbers
Static Site Test
- Build the site:
node build.mjs
The final script will be saved to out.js
- Start a local web server to host the project folder:
npx http-server .
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 data was added to the page, append out.js
to the URL
(http://localhost:8080/out.js
) and view the source. The source will include
president names. It will not include SheetJS library references!
In the last test, the generated source looked like the following snippet
(() => {
// pres.numbers
var data = [{ "Name": "Bill Clinton", "Index": 42 }, /* ... more data */];
data.forEach((row) => {
Object.keys(row).forEach((k) => {
if (row[k]?.d)
row[k] = new Date(row[k].d);
});
});
var pres_default = data;
// app.js
var elt = document.createElement("div");
elt.innerHTML = "<table><tr><th>Name</th><th>Index</th></tr>" + pres_default.map((row) => `<tr>
<td>${row.Name}</td>
<td>${row.Index}</td>
</tr>`).join("") + "</table>";
document.body.appendChild(elt);
})();
-
See "Workbook Object" ↩︎