docs.sheetjs.com/docz/docs/03-demos/12-static/04-esbuild.md

333 lines
8.3 KiB
Markdown
Raw Normal View History

2023-10-21 11:15:28 +00:00
---
title: Building Sheets with ESBuild
sidebar_label: ESBuild
pagination_prev: demos/net/index
pagination_next: demos/mobile/index
sidebar_custom_props:
type: bundler
---
import current from '/version.js';
import CodeBlock from '@theme/CodeBlock';
[ESBuild](https://esbuild.github.io/) is a modern build tool for generating
static sites. It has a robust JavaScript-powered plugin system[^1]
[SheetJS](https://sheetjs.com) 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"](#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](/docs/demos/frontend/bundler/esbuild) includes an example of
importing the SheetJS library in a browser script.
:::
## ESBuild Loader
2024-04-08 03:55:10 +00:00
ESBuild releases starting from `0.9.1` support custom loader plugins. The loader
receives an absolute path to the spreadsheet on the filesystem.
2023-10-21 11:15:28 +00:00
The [SheetJS NodeJS module](/docs/getting-started/installation/nodejs) can be
imported from ESBuild loader plugins.
:::info pass
ESBuild loader plugins use ECMAScript Modules. The plugin ultimately receives
raw paths to files. [`fs`](/docs/getting-started/installation/nodejs#esm-import)
must be manually imported:
```js
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:
```mermaid
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
```
2024-12-22 04:47:57 +00:00
### ESBuild Configuration
2023-10-21 11:15:28 +00:00
2024-12-22 04:47:57 +00:00
Plugins can be referenced in the `plugins` array of the build configuration:
2023-10-21 11:15:28 +00:00
```js title="build.mjs (structure)"
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:
```js
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.
```js
setup(build) {
build.onLoad({ filter: EXTS }, (args) => {
const path = args.path;
// ...
});
},
```
### SheetJS Operations
The SheetJS `readFile` method[^2] will directly read the file on the filesystem.
The return value is a SheetJS workbook object[^3].
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` method[^4].
:::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.
:::
2023-12-05 03:46:54 +00:00
```js title="build.mjs (plugin implementation)"
2023-10-21 11:15:28 +00:00
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:
```js title="src/index.js"
import data from './pres.xlsx';
2023-10-22 05:40:02 +00:00
/* `data` is an array of objects from ./pres.xlsx */
2023-10-21 11:15:28 +00:00
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
2023-12-05 03:46:54 +00:00
:::note Tested Deployments
2023-10-21 11:15:28 +00:00
2024-04-08 03:55:10 +00:00
This demo was tested in the following environments:
| `esbuild` | Date |
|:----------|:-----------|
| `0.20.2` | 2024-04-07 |
| `0.19.12` | 2024-04-07 |
| `0.18.20` | 2024-04-07 |
| `0.17.19` | 2024-04-07 |
| `0.16.17` | 2024-04-07 |
| `0.15.18` | 2024-04-07 |
| `0.14.54` | 2024-04-07 |
| `0.13.15` | 2024-04-07 |
| `0.12.29` | 2024-04-07 |
| `0.11.23` | 2024-04-07 |
| `0.10.2` | 2024-04-07 |
| `0.9.7` | 2024-04-07 |
| `0.9.1` | 2024-04-07 |
2023-10-21 11:15:28 +00:00
:::
### Initial Setup
0) Create a new skeleton project:
```bash
mkdir sheetjs-esb
cd sheetjs-esb
npm init -y
2024-04-08 03:55:10 +00:00
npm i --save esbuild@0.20.2
2023-10-21 11:15:28 +00:00
```
1) Install the SheetJS NodeJS module:
<CodeBlock language="bash">{`\
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
</CodeBlock>
2) Save the following to `index.html`:
```html title="index.html"
<!DOCTYPE html>
<html>
<head>
<title>SheetJS + ESBuild</title>
</head>
<body>
<script src="out.js"></script>
</body>
</html>
```
3) Save the following to `app.js`:
```js title="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);
```
2023-12-05 03:46:54 +00:00
4) Download [`build.mjs`](pathname:///esbuild/build.mjs) to the project folder:
2023-10-21 11:15:28 +00:00
2023-12-05 03:46:54 +00:00
```bash
curl -LO https://docs.sheetjs.com/esbuild/build.mjs
2023-10-21 11:15:28 +00:00
```
2024-04-26 04:16:13 +00:00
5) Download https://docs.sheetjs.com/pres.numbers to the project folder:
2023-10-21 11:15:28 +00:00
```bash
2024-04-26 04:16:13 +00:00
curl -LO https://docs.sheetjs.com/pres.numbers
2023-10-21 11:15:28 +00:00
```
### Static Site Test
6) Build the site:
```bash
node build.mjs
```
The final script will be saved to `out.js`
7) Start a local web server to host the project folder:
```bash
npx http-server .
```
The command will print a list of URLs.
8) 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
2023-12-05 03:46:54 +00:00
```js title="out.js (Expected output)"
2023-10-21 11:15:28 +00:00
(() => {
// 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);
})();
```
[^1]: See ["Plugins"](https://esbuild.github.io/plugins/) in the ESBuild documentation.
[^2]: See [`readFile` in "Reading Files"](/docs/api/parse-options)
[^3]: See ["Workbook Object"](/docs/csf/book)
[^4]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)