HTML build tool that runs entirely in the browser 🎁
- Works with a lightweight file interface
- Compiles dependencies
- Runs in the browser and server
npm install @useparcel/tape
Let's walk through how to use tape. We are going to run tape()
and configure it to use the css inlining plugin. We are also going to give it 2 files, and tell it that index.html
is the entry file where it should start looking for dependencies.
import { tape } from '@useparcel/tape'
import { cssInline } from '@useparcel/tape-css-inline'
const results = await tape({
entry: '/index.html',
plugins: [
[cssInline, { applyStyleTags: false }]
],
files: {
'/index.html': {
content: `
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
</body>
</html>`
},
'/style.css': {
content: `
body {
background: blue;
}`
}
}
})
const results = await tape({ entry, files, plugins, signal })
String
| required
The entry file to be built. It must be a valid file given to the files
object.
Object
orFunction
| required
Either an object or a function.
If it is an object, it should contain the files themselves. Each entry key should be the file path and each value should be an object with an entry with a key of content
and contain the content of the file.
const files = {
'/index.html': {
content: '<html>hello world</html>'
}
}
If it is an function it should take a path as the only parameter and return the file as a result. The function can be asynchronous.
import { readFile } from 'fs/promises';
async function fileLoader(path) {
const content = await readFile(path, 'utf8');
return { content }
}
Array
| defaults to[]
An optional array of additional plugins to run. To provide the plugin with configuration, pass in array with the plugin as the first value and the configuration as the second.
const plugins = [
myTapePlugin,
[myTapePluginWithConfig, { option: true }]
]
AbortSignal
| optional
The AbortSignal
from an AbortController
. Passing this in allows you to abort the build. This is useful if a file was changed and you want to stop the build immediately.
const controller = new AbortController();
const signal = controller.signal;
// stops the build if it took more than 500ms
setTimeout(() => {
controller.abort();
}, 500)
const results = await tape({ entry, files, signal });
Return value
Returns a Promise which resolves an object with the following keys:
entry
(String
) - Entry path for outputted filesfiles
(Object
) - Contains built file objects. See file definition for more.diagnostics
(Array
) - Contains a list of diagnostic messages. See diagnostic definition for more.
Error handling
It will reject the promise if it runs into a fatal error or if the build is aborted. Otherwise all errors will be returned inside the diagnostics
array.
Example
try {
const { entry, files, diagnostics } = await tape({ entry, files, plugins, signal })
console.log(`Here is the output:\n${files[entry]}`)
console.log(`You should probably fix these problems: ${
JSON.stringify(diagnostics, null, 2)
}`)
}
catch(error) {
console.log(`The build failed. You *must fix this problem*:`, error)
}
Runs the cleanup()
function for each plugin. Should be called after each session you are done running tape. For example, when the user navigates to a new page, you should run it then.
It should be given the same exact configuration as tape()
.
Each build goes through the following lifecycle:
- Transforming
- Packaging
- Optimizing
- Writing
During the transforming step, all assets are loaded, converted into usable types (i.e. MJML converted HTML), and added to a dependency graph. Inline assets such as CSS in <style/>
tag are extracted here.
Next, dependencies are packaging. Here inline assets are re-inserted and dependency references are replaced with their final output path.
After each file is packaged, it can be optimized. This can be any modification of the final content from HTML minification to CSS inlining.
Lastly, the files are written to some output. For example: a target output could be the file system, an in-memory object, a series of object URLs.
Plugins can hook into each step of the lifecycle.
Each plugin is a function that accepts a config object and returns an object with the following plugin structure:
- name (
String
|required
) - The unique name of the plugin. - exts (
Array
|optional
) - An array of extensions that the plugin should be run against. - transform (
Function
|optional
) - package (
Function
|optional
) - optimize (
Function
|optional
) - write (
Function
|optional
)
function myTapePlugin(config) {
return {
name: 'my-tape-plugin',
exts: ['.css', '.scss', '.less', '.sass'],
transform({ asset }) {
return {
...asset,
content: `/** my css comment */\n${asset.content}`
}
}
}
}
Transform assets, add dependencies, and extract inline assets. In the following example we are going to transform pug, a template language.
Transform assets
When transforming assets to a different type, you should update both the ext
property and the content
. In the following example we will convert pug to HTML.
const pug = require("pug");
function pugPlugin() {
return {
name: "transform-pug",
exts: [".pug"],
transform({ asset }) {
// return the transformed asset
return {
...asset,
ext: ".html",
content: pug.render(asset.content);
}
}
}
}
Add dependencies
Use the addDependency
function to add an asset as a dependency. You can provide a file path or the asset ID. The path can be a relative path or absolute. Note that if the asset does not exit the build will fail.
In the following example we will we adding dependencies for included files in pug.
const INCLUDE_REGEX = /^\s*include\s+(.*)/gm;
const isAbsoluteUrl = require("is-absolute-url");
function pugPlugin() {
return {
name: "transform-pug",
exts: [".pug"],
transform({ asset, addDependency }) {
// Add included dependencies
let match;
while ((match = INCLUDE_REGEX.exec(asset.content))) {
const path = match[1];
if (!isAbsoluteUrl(path)) {
addDependency({ path });
}
}
// return the transformed asset
...
}
}
}
Extracting inline assets
Sometimes files include more than one asset type. For example, HTML includes CSS inside of <style>
tags. You can return an array of assets from your transform
function. Each of these embedded assets should have the flag embedded: true
as well as an offset
with a start
and stop
index where it exists in the source.
In the example below we will extract embedded CSS inside of pug files. This is not necessary in practice since the pug file will be converted to HTML and then the HTML transformer handle extracting the CSS.
const STYLE_REGEX = /^(\s*)style(\n\1 .*)*/g
function pugPlugin() {
return {
name: "transform-pug",
exts: [".pug"],
transform({ asset, addDependency }) {
// Add included dependencies
...
// Extract style embedded assets
let match;
let assets = []
while ((match = STYLE_REGEX.exec(asset.content))) {
const id = `${asset.id}:${assets.length}`
const content = match[2];
const startIndex = match.index + match[0].indexOf(content);
const endIndex = startIndex + path.length;
// gather the assets and add them as dependencies
assets.push({
id,
ext: `.css`,
content: content,
embedded: true,
offset: {
start: startIndex,
end: endIndex
}
})
addDependency({ id });
}
return [
{
...asset,
ext: ".html",
content: pug.render(asset.content);
},
...assets
]
}
}
}
After all transformations are done, package
should reinsert the processed embedded assets and replace the original references to dependencies.
Odds are you should not be using this method since HTML and CSS are already packaged.
For an example, check out the HTML plugin.
optimize
is similar to transformer in that it arbitrarily modifies the file contents however it runs after paths have already been packaged, right before the file is written.
This step is where you can run the final code through a last optimization process.
In the following example we are going to prettify our HTML using pretty
. We will pass the config the plugin accepts directly to pretty
.
const pretty = require("pretty");
function prettifyHTMLPlugin(config) {
return {
name: "prettify-html",
exts: ".html",
optimize({ asset }) {
return {
...asset,
content: pretty(asset.content, config),
};
}
}
}
The write
function should output the file contents to the target. Unlike all the other plugin methods only one plugin with a write
method will be run for each file.
The function should return the absolute path to the file.
Below we will write the files to the file system.
const fs = require('fs-extra')
const path = require('path')
function writeFSPlugin({ buildDir = '.tape' }) {
return {
name: 'write-fs',
async write({ asset }) {
const dir = path.join(buildDir, asset.source.dir)
const absolutePath = path.join(dir, `${asset.source.name}${asset.ext}`)
await fs.ensureDir(dir)
await fs.write(absolutePath, asset.content)
return absolutePath;
}
}
}
cleanup
is called when tape.dispose()
is called. It is for managing the side effects and cache from a build.
Continuing with the previous example we want to clean up the files after we are done using tape.
const fs = require('fs-extra')
function writeFSPlugin({ buildDir }) {
return {
name: 'write-fs',
async write() {...},
async cleanup() {
await fs.emptyDir(buildDir)
}
}
}
During every step of the build, plugins can report diagnostic messages using the report
function.
It takes an object with the following properties:
- type (
String
| defaults to"error"
) - Must be one oferror
,warning
,info
. If iterror
, an error will be thrown. - message (
String
| defaults to"An unknown error occurred."
) – - loc (
Object
| defaults tonull
) – The location in the code that generated this error. Ifloc
is provided,start.line
andstart.column
are required. - start (
Object
| required) - line (
Number
| required) - The 1-indexed line of the start of the problem. - column (
Number
| required) - The 1-indexed column of the start of the problem. - end (
Object
| defaults toundefined
) - line (
Number
) - The 1-indexed line of the end of the problem. - column (
Number
) - The 1-indexed column of the end of the problem. - fix (
String
| optional) - The string to replace the given location in order to resolve the problem.
It also had 3 shortcuts for reporting:
report.info(diagnostic)
report.warning(diagnostic)
report.error(diagnostic)
Here we will add an info
diagnostic report for when we write the file in our write-fs
plugin.
const fs = require('fs-extra')
function writeFSPlugin({ buildDir }) {
return {
name: 'write-fs',
async write({ asset, report }) {
report({
type: 'info',
message: 'Writing to file system.'
})
const dir = path.join(buildDir, asset.dir)
const absolutePath = path.join(dir, `${asset.name}${asset.ext}`)
await fs.ensureDir(dir)
await fs.write(absolutePath, asset.content)
return absolutePath;
}
async cleanup() {...}
}
}
Definition of objects used by tape.
Each asset is an object with the following properties:
- id (
String
) - Unique asset ID. - ext (
String
) - The extension of the asset. - content (
String
) - Asset contents. - embedded (
Boolean
) - Whether or not the asset is embedded. - source (
Object
) - The original asset data. - path (
String
) - Path of the asset. Will not be set ifembedded
istrue
. - dir (
String
) - Directory that contains the asset. - name (
String
) - Name of the asset. Will not be set ifembedded
istrue
. - ext (
String
) - The original asset extension.
Each file is represented by an object in a containing object. The key in the containing object should be the file path. In the file object itself there should be a single key named content
which holds the file contents.
const files = {
'/index.html': {
content: '<html>hello world</html>'
}
}
Each diagnostic is a object that has the following keys:
String
| required
Must be one of error
, warning
, info
String
| required
Plugin source of the diagnostic. Will be "internal"
if it came from the tape instance.
String
| required
General explanation of the error.
String
| defaults to null
Path of the file that generated the error. Will be null
if the diagnostic did not originate from a file.
Object
| optional
The location of the source of the diagnostic. If there is a location given, it will contain a start line and column. It may also contain and end line and column.
The loc
object will have the following keys:
- start (
Object
| required) - line (
Number
) - The 1-indexed line of the start of the problem. - column (
Number
) - The 1-indexed column of the start of the problem. - end (
Object
| optional) - line (
Number
) - The 1-indexed line of the end of the problem. - column (
Number
) - The 1-indexed column of the end of the problem. - fix (
String
| optional) - The string to replace the given location in order to resolve the problem.