Skip to content

Commit

Permalink
cmd/hiveview: improve esbuild setup (ethereum#710)
Browse files Browse the repository at this point in the history
In ethereum#708, I introduced a lot of logic to create CSS/JS bundles on demand. It turns out that
the way I implemented bundling was a bit misguided, because I was operating under the
basic assumption that esbuild will always create exactly one output file for every entrypoint.
This breaks when it tries to copy assets or performs code splitting. So, instead of doing
incremental builds ourself, just let esbuild do its thing and copy all of the files it produces
into the output FS.

As a bonus, enabling code splitting makes it possible to have one entry point for each page.
  • Loading branch information
fjl authored and mattsse committed Mar 24, 2023
1 parent 92ce2b8 commit 2343d32
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 224 deletions.
2 changes: 1 addition & 1 deletion cmd/hiveview/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</head>

<body>
<script id="app" src="/lib/app.js" type="module" data-main="index"></script>
<script src="/lib/app-index.js" type="module"></script>
<main role="main">
<div id="hive-header">
<a href="/"><img id="hive-logo" height="35" src="/images/hive3.svg"></a>
Expand Down
26 changes: 12 additions & 14 deletions cmd/hiveview/assets/lib/app-index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import 'datatables.net'
import 'datatables.net-bs5'
import 'datatables.net-responsive'
import 'datatables.net-responsive-bs5'
import { $ } from 'jquery'
import { html, format, nav } from './utils.js'

import { html, format } from './utils.js'
import * as routes from './routes.js'
import * as common from './common.js'

$(document).ready(function () {
common.updateHeader();

export default function navigate() {
$('#loading').show();
console.log("Loading file list...");
$.ajax("listing.jsonl", {
cache: false,
success: function(data) {
$('#page-text').show();
showFileListing(data);
Expand All @@ -17,18 +26,7 @@ export default function navigate() {
$('#loading').hide();
},
});
}

function resultStats(fails, success, total) {
f = parseInt(fails), s = parseInt(success);
t = parseInt(total);
f = isNaN(f) ? "?" : f;
s = isNaN(s) ? "?" : s;
t = isNaN(t) ? "?" : t;
return '<b><span class="text-danger">' + f +
'</span>&nbsp;:&nbsp;<span class="text-success">' + s +
'</span> &nbsp;/&nbsp;' + t + '</b>';
}
})

function linkToSuite(suiteID, suiteName, linkText) {
let url = routes.suite(suiteID, suiteName);
Expand Down
11 changes: 9 additions & 2 deletions cmd/hiveview/assets/lib/app-suite.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import 'datatables.net'
import 'datatables.net-bs5'
import 'datatables.net-responsive'
import 'datatables.net-responsive-bs5'
import { $ } from 'jquery'

import { html, nav, format, loader } from './utils.js'
import * as routes from './routes.js'
import * as common from './common.js'

$(document).ready(function () {
common.updateHeader();

export default function navigate() {
let name = nav.load("suitename");
if (name) {
showSuiteName(name);
Expand Down Expand Up @@ -34,7 +41,7 @@ export default function navigate() {
showError("error fetching " + filename + " : " + error);
},
});
}
})

// showSuiteName displays the suite title.
function showSuiteName(name) {
Expand Down
7 changes: 5 additions & 2 deletions cmd/hiveview/assets/lib/app-viewer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { $ } from 'jquery'
import { html, nav, format, loader } from './utils.js'
import * as routes from './routes.js'
import * as common from './common.js'

$(document).ready(function () {
common.updateHeader();

export default function navigate() {
// Check for line number in hash.
var line = null;
if (window.location.hash.substr(1, 1) == "L") {
Expand Down Expand Up @@ -39,7 +42,7 @@ export default function navigate() {

// Show default text because nothing was loaded.
showText(document.getElementById("exampletext").innerHTML);
}
})

// setHL sets the highlight on a line number.
function setHL(num, scroll) {
Expand Down
8 changes: 4 additions & 4 deletions cmd/hiveview/assets/lib/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ main {
}

td.test-name-column {
background: url('/images/details_open.svg') no-repeat left 4px;
background: url('../images/details_open.svg') no-repeat left 4px;
background-size: 32px;
cursor: pointer;
padding-left: 32px !important;
Expand All @@ -61,15 +61,15 @@ td.test-name-column {
}

tr.failed td.test-name-column {
background-image: url('/images/details_open_err.svg');
background-image: url('../images/details_open_err.svg');
}

tr.shown td.test-name-column {
background-image: url('/images/details_close.svg');
background-image: url('../images/details_close.svg');
}

tr.shown.failed td.test-name-column {
background-image: url('/images/details_close_err.svg');
background-image: url('../images/details_close_err.svg');
}

td.ellipsis {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,9 @@
// Pull in dependencies.
import 'datatables.net'
import 'datatables.net-bs5'
import 'datatables.net-responsive'
import 'datatables.net-responsive-bs5'
import 'bootstrap'
import { $ } from 'jquery'

// Pull in app files.
import * as routes from './routes.js'
import { default as index } from './app-index.js'
import { default as suite } from './app-suite.js'
import { default as viewer } from './app-viewer.js'

$(document).ready(function() {
// Kick off the page main function.
let pages = { index, suite, viewer };
let name = $('script[type=module]').attr('data-main');
pages[name]();

export function updateHeader() {
// Update the header with version info from hive.json.
$.ajax({
type: 'GET',
Expand All @@ -31,7 +17,7 @@ $(document).ready(function() {
console.log("error fetching hive.json:", error);
},
});
})
}

function hiveInfoHTML(data) {
var txt = "";
Expand Down
2 changes: 1 addition & 1 deletion cmd/hiveview/assets/suite.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</head>

<body>
<script src="/lib/app.js" type="module" data-main="suite"></script>
<script src="/lib/app-suite.js" type="module"></script>
<main role="main">
<div id="hive-header">
<a href="/"><img id="hive-logo" height="35" src="/images/hive3.svg"></a>
Expand Down
2 changes: 1 addition & 1 deletion cmd/hiveview/assets/viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</head>

<body>
<script src="/lib/app.js" type="module" data-main="viewer"></script>
<script src="/lib/app-viewer.js" type="module"></script>
<main role="main">
<div id="hive-header">
<a href="/"><img id="hive-logo" height="35" src="/images/hive3.svg"></a>
Expand Down
70 changes: 39 additions & 31 deletions cmd/hiveview/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/fs"
"sort"
Expand All @@ -15,10 +14,14 @@ import (

// hiveviewBundler creates the esbuild bundler and registers JS/CSS targets.
func hiveviewBundler(fsys fs.FS) *bundler {
b := newBundler(fsys)
b.add("lib/app.js")
b.add("lib/app.css")
b.add("lib/viewer.css")
entrypoints := []string{
"lib/app-index.js",
"lib/app-suite.js",
"lib/app-viewer.js",
"lib/app.css",
"lib/viewer.css",
}
b := newBundler(fsys, entrypoints, moduleAliases)
return b
}

Expand Down Expand Up @@ -48,17 +51,16 @@ func importMapScript() string {
// against the bundler, and URLs in the HTML will be replaced by references to
// bundle files.
type deployFS struct {
assets fs.FS
bundler *bundler
useBundle bool
assets fs.FS
bundler *bundler
}

func newDeployFS(assets fs.FS, useBundle bool) *deployFS {
return &deployFS{
assets: assets,
bundler: hiveviewBundler(assets),
useBundle: useBundle,
dfs := &deployFS{assets: assets}
if useBundle {
dfs.bundler = hiveviewBundler(assets)
}
return dfs
}

func isBundlePath(name string) bool {
Expand All @@ -75,8 +77,9 @@ func (dfs *deployFS) Open(name string) (f fs.File, err error) {
switch {
case !strings.Contains(name, "/") && strings.HasSuffix(name, ".html"):
return dfs.openHTML(name)
case isBundlePath(name):
return dfs.bundler.fs().Open(name)
case dfs.bundler != nil && isBundlePath(name):
_, memfs, _ := dfs.bundler.rebuild()
return memfs.Open(name)
default:
return dfs.assets.Open(name)
}
Expand All @@ -92,8 +95,9 @@ func (dfs *deployFS) ReadDir(name string) ([]fs.DirEntry, error) {
switch {
case name == ".":
return dfs.readDirRoot()
case isBundlePath(name):
return fs.ReadDir(dfs.bundler.fs(), name)
case dfs.bundler != nil && isBundlePath(name):
_, memfs, _ := dfs.bundler.rebuild()
return fs.ReadDir(memfs, name)
default:
return fs.ReadDir(dfs.assets, name)
}
Expand All @@ -104,8 +108,12 @@ func (dfs *deployFS) readDirRoot() ([]fs.DirEntry, error) {
if err != nil {
return nil, err
}
bundleEntries, _ := fs.ReadDir(dfs.bundler.fs(), ".")
entries = append(entries, bundleEntries...)
if dfs.bundler != nil {
_, memfs, _ := dfs.bundler.rebuild()
bundleEntries, _ := fs.ReadDir(memfs, ".")
entries = append(entries, bundleEntries...)
}

sort.Slice(entries, func(i, j int) bool {
return entries[i].Name() < entries[j].Name()
})
Expand All @@ -128,35 +136,35 @@ func (dfs *deployFS) openHTML(name string) (fs.File, error) {
output := new(bytes.Buffer)
modTime := inputInfo.ModTime()

if !dfs.useBundle {
if dfs.bundler == nil {
// JS bundle is disabled. To make ES module loading work without the bundle,
// the document needs an importmap.
insertAfterTag(inputFile, output, "head", importMapScript())
modTime = time.Now()
} else {
// Replace script/style references with bundle paths, if possible.
buildmsg, _, _ := dfs.bundler.rebuild()
var errorShown bool
modifyHTML(inputFile, output, func(token *html.Token, errlog io.Writer) {
if len(buildmsg) > 0 && !errorShown {
io.WriteString(errlog, "** ESBUILD ERRORS **\n\n")
renderBuildMsg(buildmsg, errlog)
modTime = time.Now()
errorShown = true
}

ref := scriptOrStyleReference(token)
if ref == nil {
return // not script
}
bundle, buildmsg, err := dfs.bundler.build(ref.Val)
if errors.Is(err, fs.ErrNotExist) {
return
} else if err != nil {
if !errorShown {
io.WriteString(errlog, "** ESBUILD ERRORS **\n\n")
errorShown = true
}
renderBuildMsg(buildmsg, errlog)
modTime = time.Now()
return
bundle := dfs.bundler.bundle(ref.Val)
if bundle == nil || bundle.outputFile == "" {
return // not a bundle target
}
if bundle.buildTime.After(modTime) {
modTime = bundle.buildTime
}
ref.Val = "/bundle/" + bundle.name()
ref.Val = "/bundle/" + bundle.outputFile
})
}

Expand Down
22 changes: 21 additions & 1 deletion cmd/hiveview/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,29 @@ func TestBuildAllBundles(t *testing.T) {
b := hiveviewBundler(assets)

var output strings.Builder
msg, err := b.buildAll()
msg, _, err := b.rebuild()
if err != nil {
renderBuildMsg(msg, &output)
t.Fatal("esbuild errors:\n\n", output.String())
}
}

func TestDeployWithBundle(t *testing.T) {
assets, _ := fs.Sub(embeddedAssets, "assets")

temp := t.TempDir()
dfs := newDeployFS(assets, true)
if err := copyFS(temp, dfs); err != nil {
t.Fatal("copy error:", err)
}
}

func TestDeployWithoutBundle(t *testing.T) {
assets, _ := fs.Sub(embeddedAssets, "assets")

temp := t.TempDir()
dfs := newDeployFS(assets, false)
if err := copyFS(temp, dfs); err != nil {
t.Fatal("copy error:", err)
}
}
Loading

0 comments on commit 2343d32

Please sign in to comment.