Skip to content

Commit 33c0938

Browse files
committed
Add build time math rendering
While very useful on its own (and combined with the passthrough render hooks), this also serves as a proof of concept of using WASI (WebAssembly System Interface) modules in Hugo. This will be marked _experimental_ in the documentation. Not because it will be removed or changed in a dramatic way, but we need to think a little more how to best set up/configure similar services, define where these WASM files gets stored, maybe we can allow user provided WASM files plugins via Hugo Modules mounts etc. See these issues for more context: * gohugoio#12736 * gohugoio#12737 See gohugoio#11927
1 parent 0c3a1c7 commit 33c0938

26 files changed

+1598
-13
lines changed

cache/filecache/filecache_config.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const (
4646
CacheKeyAssets = "assets"
4747
CacheKeyModules = "modules"
4848
CacheKeyGetResource = "getresource"
49+
CacheKeyMisc = "misc"
4950
)
5051

5152
type Configs map[string]FileCacheConfig
@@ -70,10 +71,14 @@ var defaultCacheConfigs = Configs{
7071
MaxAge: -1,
7172
Dir: resourcesGenDir,
7273
},
73-
CacheKeyGetResource: FileCacheConfig{
74+
CacheKeyGetResource: {
7475
MaxAge: -1, // Never expire
7576
Dir: cacheDirProject,
7677
},
78+
CacheKeyMisc: {
79+
MaxAge: -1,
80+
Dir: cacheDirProject,
81+
},
7782
}
7883

7984
type FileCacheConfig struct {
@@ -120,6 +125,11 @@ func (f Caches) AssetsCache() *Cache {
120125
return f[CacheKeyAssets]
121126
}
122127

128+
// MiscCache gets the file cache for miscellaneous stuff.
129+
func (f Caches) MiscCache() *Cache {
130+
return f[CacheKeyMisc]
131+
}
132+
123133
// GetResourceCache gets the file cache for remote resources.
124134
func (f Caches) GetResourceCache() *Cache {
125135
return f[CacheKeyGetResource]

cache/filecache/filecache_config_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ dir = "/path/to/c4"
5959
c.Assert(err, qt.IsNil)
6060
fs := afero.NewMemMapFs()
6161
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
62-
c.Assert(len(decoded), qt.Equals, 6)
62+
c.Assert(len(decoded), qt.Equals, 7)
6363

6464
c2 := decoded["getcsv"]
6565
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
@@ -106,7 +106,7 @@ dir = "/path/to/c4"
106106
c.Assert(err, qt.IsNil)
107107
fs := afero.NewMemMapFs()
108108
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
109-
c.Assert(len(decoded), qt.Equals, 6)
109+
c.Assert(len(decoded), qt.Equals, 7)
110110

111111
for _, v := range decoded {
112112
c.Assert(v.MaxAge, qt.Equals, time.Duration(0))
@@ -129,7 +129,7 @@ func TestDecodeConfigDefault(t *testing.T) {
129129

130130
fs := afero.NewMemMapFs()
131131
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
132-
c.Assert(len(decoded), qt.Equals, 6)
132+
c.Assert(len(decoded), qt.Equals, 7)
133133

134134
imgConfig := decoded[filecache.CacheKeyImages]
135135
jsonConfig := decoded[filecache.CacheKeyGetJSON]

common/hugio/writers.go

+30
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,33 @@ func ToReadCloser(r io.Reader) io.ReadCloser {
8181
io.NopCloser(nil),
8282
}
8383
}
84+
85+
type ReadWriteCloser interface {
86+
io.Reader
87+
io.Writer
88+
io.Closer
89+
}
90+
91+
// PipeReadWriteCloser is a convenience type to create a pipe with a ReadCloser and a WriteCloser.
92+
type PipeReadWriteCloser struct {
93+
*io.PipeReader
94+
*io.PipeWriter
95+
}
96+
97+
// NewPipeReadWriteCloser creates a new PipeReadWriteCloser.
98+
func NewPipeReadWriteCloser() PipeReadWriteCloser {
99+
pr, pw := io.Pipe()
100+
return PipeReadWriteCloser{pr, pw}
101+
}
102+
103+
func (c PipeReadWriteCloser) Close() (err error) {
104+
if err = c.PipeReader.Close(); err != nil {
105+
return
106+
}
107+
err = c.PipeWriter.Close()
108+
return
109+
}
110+
111+
func (c PipeReadWriteCloser) WriteString(s string) (int, error) {
112+
return c.PipeWriter.Write([]byte(s))
113+
}

deps/deps.go

+8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/gohugoio/hugo/helpers"
2424
"github.com/gohugoio/hugo/hugofs"
2525
"github.com/gohugoio/hugo/identity"
26+
"github.com/gohugoio/hugo/internal/warpc"
2627
"github.com/gohugoio/hugo/media"
2728
"github.com/gohugoio/hugo/resources/page"
2829
"github.com/gohugoio/hugo/resources/postpub"
@@ -93,6 +94,10 @@ type Deps struct {
9394
// This is common/global for all sites.
9495
BuildState *BuildState
9596

97+
// Holds RPC dispatchers for Katex etc.
98+
// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
99+
WasmDispatchers *warpc.Dispatchers
100+
96101
*globalErrHandler
97102
}
98103

@@ -343,6 +348,9 @@ func (d *Deps) Close() error {
343348
if d.MemCache != nil {
344349
d.MemCache.Stop()
345350
}
351+
if d.WasmDispatchers != nil {
352+
d.WasmDispatchers.Close()
353+
}
346354
return d.BuildClosers.Close()
347355
}
348356

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ require (
6969
github.com/spf13/pflag v1.0.5
7070
github.com/tdewolff/minify/v2 v2.20.37
7171
github.com/tdewolff/parse/v2 v2.7.15
72+
github.com/tetratelabs/wazero v1.7.4-0.20240805170331-2b12e189eeec
7273
github.com/yuin/goldmark v1.7.4
7374
github.com/yuin/goldmark-emoji v1.0.3
7475
go.uber.org/automaxprocs v1.5.3

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,6 @@ github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ
233233
github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
234234
github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 h1:MNdY6hYCTQEekY0oAfsxWZU1CDt6iH+tMLgyMJQh/sg=
235235
github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo=
236-
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0 h1:PCtO5l++psZf48yen2LxQ3JiOXxaRC6v0594NeHvGZg=
237-
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0/go.mod h1:g9CCh+Ci2IMbPUrVJuXbBTrA+rIIx5+hDQ4EXYaQDoM=
238236
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0 h1:7PY5PIJ2mck7v6R52yCFvvYHvsPMEbulgRviw3I9lP4=
239237
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0/go.mod h1:r8g5S7bHfdj0+9ShBog864ufCsVODKQZNjYYY8OnJpM=
240238
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
@@ -461,6 +459,8 @@ github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W
461459
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
462460
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
463461
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
462+
github.com/tetratelabs/wazero v1.7.4-0.20240805170331-2b12e189eeec h1:KeQseLFSWb9qjW4PSWxciTBk1hbG7KsVx3rs1hIQnbQ=
463+
github.com/tetratelabs/wazero v1.7.4-0.20240805170331-2b12e189eeec/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
464464
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
465465
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
466466
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

hugolib/site_new.go

+11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"html/template"
2121
"os"
22+
"path/filepath"
2223
"sort"
2324
"time"
2425

@@ -34,6 +35,7 @@ import (
3435
"github.com/gohugoio/hugo/hugolib/doctree"
3536
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
3637
"github.com/gohugoio/hugo/identity"
38+
"github.com/gohugoio/hugo/internal/warpc"
3739
"github.com/gohugoio/hugo/langs"
3840
"github.com/gohugoio/hugo/langs/i18n"
3941
"github.com/gohugoio/hugo/lazy"
@@ -157,6 +159,15 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
157159
MemCache: memCache,
158160
TemplateProvider: tplimpl.DefaultTemplateProvider,
159161
TranslationProvider: i18n.NewTranslationProvider(),
162+
WasmDispatchers: warpc.AllDispatchers(
163+
warpc.Options{
164+
CompilationCacheDir: filepath.Join(conf.Dirs().CacheDir, "_warpc"),
165+
166+
// Katex is relatively slow.
167+
PoolSize: 8,
168+
Infof: logger.InfoCommand("wasm").Logf,
169+
},
170+
),
160171
}
161172

162173
if err := firstSiteDeps.Init(); err != nil {

internal/warpc/build.sh

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# TODO1 clean up when done.
2+
go generate ./gen
3+
javy compile js/greet.bundle.js -d -o wasm/greet.wasm
4+
javy compile js/renderkatex.bundle.js -d -o wasm/renderkatex.wasm
5+
touch warpc_test.go

internal/warpc/gen/main.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//go:generate go run main.go
2+
package main
3+
4+
import (
5+
"fmt"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/evanw/esbuild/pkg/api"
12+
)
13+
14+
var scripts = []string{
15+
"greet.js",
16+
"renderkatex.js",
17+
}
18+
19+
func main() {
20+
for _, script := range scripts {
21+
filename := filepath.Join("../js", script)
22+
err := buildJSBundle(filename)
23+
if err != nil {
24+
log.Fatal(err)
25+
}
26+
}
27+
}
28+
29+
func buildJSBundle(filename string) error {
30+
minify := true
31+
result := api.Build(
32+
api.BuildOptions{
33+
EntryPoints: []string{filename},
34+
Bundle: true,
35+
MinifyWhitespace: minify,
36+
MinifyIdentifiers: minify,
37+
MinifySyntax: minify,
38+
Target: api.ES2020,
39+
Outfile: strings.Replace(filename, ".js", ".bundle.js", 1),
40+
SourceRoot: "../js",
41+
})
42+
43+
if len(result.Errors) > 0 {
44+
return fmt.Errorf("build failed: %v", result.Errors)
45+
}
46+
if len(result.OutputFiles) != 1 {
47+
return fmt.Errorf("expected 1 output file, got %d", len(result.OutputFiles))
48+
}
49+
50+
of := result.OutputFiles[0]
51+
if err := os.WriteFile(filepath.FromSlash(of.Path), of.Contents, 0o644); err != nil {
52+
return fmt.Errorf("write file failed: %v", err)
53+
}
54+
return nil
55+
}

internal/warpc/js/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
package-lock.json

internal/warpc/js/common.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Read JSONL from stdin.
2+
export function readInput(handle) {
3+
const buffSize = 1024;
4+
let currentLine = [];
5+
const buffer = new Uint8Array(buffSize);
6+
7+
// Read all the available bytes
8+
while (true) {
9+
// Stdin file descriptor
10+
const fd = 0;
11+
let bytesRead = 0;
12+
try {
13+
bytesRead = Javy.IO.readSync(fd, buffer);
14+
} catch (e) {
15+
// IO.readSync fails with os error 29 when stdin closes.
16+
if (e.message.includes('os error 29')) {
17+
break;
18+
}
19+
throw new Error('Error reading from stdin');
20+
}
21+
22+
if (bytesRead < 0) {
23+
throw new Error('Error reading from stdin');
24+
break;
25+
}
26+
27+
if (bytesRead === 0) {
28+
break;
29+
}
30+
31+
currentLine = [...currentLine, ...buffer.subarray(0, bytesRead)];
32+
33+
// Split array into chunks by newline.
34+
let i = 0;
35+
for (let j = 0; i < currentLine.length; i++) {
36+
if (currentLine[i] === 10) {
37+
const chunk = currentLine.splice(j, i + 1);
38+
const arr = new Uint8Array(chunk);
39+
const json = JSON.parse(new TextDecoder().decode(arr));
40+
handle(json);
41+
j = i + 1;
42+
}
43+
}
44+
// Remove processed data.
45+
currentLine = currentLine.slice(i);
46+
}
47+
}
48+
49+
// Write JSONL to stdout
50+
export function writeOutput(output) {
51+
const encodedOutput = new TextEncoder().encode(JSON.stringify(output) + '\n');
52+
const buffer = new Uint8Array(encodedOutput);
53+
// Stdout file descriptor
54+
const fd = 1;
55+
Javy.IO.writeSync(fd, buffer);
56+
}

internal/warpc/js/greet.bundle.js

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/warpc/js/greet.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { readInput, writeOutput } from './common';
2+
3+
const greet = function (input) {
4+
writeOutput({ header: input.header, data: { greeting: 'Hello ' + input.data.name + '!' } });
5+
};
6+
7+
console.log('Greet module loaded');
8+
9+
readInput(greet);

internal/warpc/js/package.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "js",
3+
"version": "1.0.0",
4+
"main": "greet.js",
5+
"scripts": {
6+
"test": "echo \"Error: no test specified\" && exit 1"
7+
},
8+
"author": "",
9+
"license": "ISC",
10+
"description": "",
11+
"devDependencies": {
12+
"katex": "^0.16.11"
13+
}
14+
}

0 commit comments

Comments
 (0)