Skip to content

Commit bb9571e

Browse files
committed
feat: use tinygo instead of go to create smaller wasm binary
This commit refactors the wasm and JavaScript parts to make use of TinyGo for creating even smaller wasm binaries. It also introduces another wasm-opt pass that shaves off ~500K. The API has changed: Previously the exported `createLinter()` function was synchronous and returned an async function to lint a single file. Now the `createLinter()` function is asynchronous and the returned linter function is synchronous. ```js import { createLinter } from "actionlint"; const linter = await createLinter(); const results = linter("on: psuh", "borked.yml"); ``` BREAKING CHANGE: The `createLinter()` function is now asynchronous and resolves to a function that synchronously invokes the actionlint binary.
1 parent 1987395 commit bb9571e

File tree

15 files changed

+189
-147
lines changed

15 files changed

+189
-147
lines changed

.github/workflows/push.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ jobs:
4343
4444
cat << EOF > test.mjs
4545
import { createLinter } from 'actionlint';
46-
createLinter()('on: psuh', 'push.yml').then(
47-
(results) => process.exit(results.length > 0 ? 0 : 1),
48-
(err) => { console.error(err); process.exit(1); }
49-
);
46+
createLinter().then(lint => {
47+
const results = lint('on: psuh', 'push.yml');
48+
process.exit(results.length > 0 ? 0 : 1)
49+
}, (err) => { console.error(err); process.exit(1); });
5050
EOF
5151
5252
# test that the linter works

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
main.wasm
2-
wasm_exec.js
2+
tmp.wasm
33
node_modules/
4-
types/
4+
types/

.npmignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
.github/
22
.vscode/
3+
node_modules/
34
go.mod
45
go.sum
5-
globals.d.ts
66
main.go
77
Makefile
88
renovate.json
99
test.mjs
10+
tmp.wasm
1011
tsconfig.json
1112
yarn.lock

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
"GOARCH": "wasm"
66
}
77
}
8-
}
8+
}

Makefile

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
1-
build: wasm_exec.js main.wasm types/node.d.mts
1+
build: main.wasm types/node.d.mts
22

3-
wasm_exec.js:
4-
cp $$(go env GOROOT)/misc/wasm/wasm_exec.js wasm_exec.tmp.js
5-
echo "// @ts-nocheck" > wasm_exec.js
6-
cat wasm_exec.tmp.js >> wasm_exec.js
7-
rm wasm_exec.tmp.js
3+
main.wasm: tmp.wasm
4+
wasm-opt -c -O4 -o main.wasm tmp.wasm
85

9-
main.wasm: main.go go.mod
10-
GOOS=js GOARCH=wasm go build -o main.wasm
6+
tmp.wasm: main.go go.mod
7+
tinygo build -o tmp.wasm -target wasm -panic trap -opt z main.go
118

129
types/node.d.mts: *.cjs *.mjs *.d.ts *.json yarn.lock
1310
$$(yarn bin)/tsc -p .
1411

1512
clean:
1613
rm main.wasm
17-
rm wasm_exec.js
14+
rm tmp.wasm
1815
rm -rf types
1916

20-
.PHONY: build clean
17+
.PHONY: build clean

actionlint.cjs

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,54 +8,75 @@ require("./tiny_wasm_exec.js");
88

99
/**
1010
* @param {WasmLoader} loader
11-
* @returns {RunActionlint}
11+
* @returns {Promise<RunActionlint>}
1212
*/
13-
module.exports.createActionlint = function createActionlint(loader) {
13+
module.exports.createActionlint = async function createActionlint(loader) {
14+
const encoder = new TextEncoder();
15+
const decoder = new TextDecoder();
1416
const go = new Go();
1517

16-
/** @type {(() => void)[] | undefined} */
17-
let queued = undefined;
18+
const wasm = await loader(go);
19+
// Do not await this promise, because it only resolves once the go main()
20+
// function has exited. But we need the main function to stay alive to be
21+
// able to call the `runActionlint` function.
22+
go.run(wasm.instance);
1823

19-
// This function gets called from go once the wasm module is ready and it
20-
// executes the linter for all queued calls.
21-
globalThis.actionlintInitialized = () => {
22-
queued?.forEach((f) => f());
23-
queued = globalThis.actionlintInitialized = undefined;
24-
};
24+
if (!(wasm.instance.exports.memory instanceof WebAssembly.Memory)) {
25+
throw new Error("Could not get wasm memory");
26+
}
27+
const memory = wasm.instance.exports.memory;
28+
29+
if (!(wasm.instance.exports.WasmAlloc instanceof Function)) {
30+
throw new Error("Could not get wasm alloc function");
31+
}
32+
const wasmAlloc = wasm.instance.exports.WasmAlloc;
33+
34+
if (!(wasm.instance.exports.WasmFree instanceof Function)) {
35+
throw new Error("Could not get wasm free function");
36+
}
37+
const wasmFree = wasm.instance.exports.WasmFree;
2538

26-
loader(go).then((wasm) => {
27-
// Do not await this promise, because it only resolves once the go main()
28-
// function has exited. But we need the main function to stay alive to be
29-
// able to call the `runActionlint` function.
30-
go.run(wasm.instance);
31-
});
39+
if (!(wasm.instance.exports.RunActionlint instanceof Function)) {
40+
throw new Error("Could not get wasm runActionLint function");
41+
}
42+
const runActionlint = wasm.instance.exports.RunActionlint;
3243

3344
/**
34-
* @param {string} src
45+
* @param {string} input
3546
* @param {string} path
36-
* @returns {Promise<LintResult[]>}
47+
* @returns {LintResult[]}
3748
*/
38-
return async function runLint(src, path) {
39-
// Return a promise, because we need to queue calls to `runLint()` while the
40-
// wasm module is still loading and execute them once the wasm module is
41-
//ready.
42-
return new Promise((resolve, reject) => {
43-
if (typeof runActionlint === "function") {
44-
const [result, err] = runActionlint(src, path);
45-
return err ? reject(err) : resolve(result);
46-
}
47-
48-
if (!queued) {
49-
queued = [];
50-
}
51-
52-
queued.push(() => {
53-
const [result, err] = runActionlint?.(src, path) ?? [
54-
[],
55-
new Error('"runActionlint" is not defined'),
56-
];
57-
return err ? reject(err) : resolve(result);
58-
});
59-
});
49+
return function runLint(input, path) {
50+
const workflow = encoder.encode(input);
51+
const filePath = encoder.encode(path);
52+
53+
const workflowPointer = wasmAlloc(workflow.byteLength);
54+
new Uint8Array(memory.buffer).set(workflow, workflowPointer);
55+
56+
const filePathPointer = wasmAlloc(filePath.byteLength);
57+
new Uint8Array(memory.buffer).set(filePath, filePathPointer);
58+
59+
const resultPointer = runActionlint(
60+
workflowPointer,
61+
workflow.byteLength,
62+
workflow.byteLength,
63+
filePathPointer,
64+
filePath.byteLength,
65+
filePath.byteLength
66+
);
67+
68+
wasmFree(workflowPointer);
69+
wasmFree(filePathPointer);
70+
71+
const result = new Uint8Array(memory.buffer).subarray(resultPointer);
72+
const end = result.indexOf(0);
73+
74+
const string = decoder.decode(result.subarray(0, end));
75+
76+
try {
77+
return JSON.parse(string);
78+
} catch {
79+
throw new Error(string);
80+
}
6081
};
6182
};

browser.mjs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,19 @@ import { createActionlint } from "./actionlint.cjs";
66
* @typedef {import("./types").LintResult} LintResult
77
*/
88

9-
/** @type {RunActionlint | undefined} */
10-
let runLint = undefined;
11-
129
/**
1310
* @param {URL} url
14-
* @returns {RunActionlint}
11+
* @returns {Promise<RunActionlint>}
1512
*/
16-
export function createLinter(url = new URL("./main.wasm", import.meta.url)) {
17-
if (runLint) {
18-
return runLint;
19-
}
20-
21-
return (runLint = createActionlint(
13+
export async function createLinter(
14+
url = new URL("./main.wasm", import.meta.url)
15+
) {
16+
return await createActionlint(
2217
/** @type {WasmLoader} */ async (go) => {
2318
return WebAssembly.instantiateStreaming(
2419
fetch(url.toString()),
2520
go.importObject
2621
);
2722
}
28-
));
23+
);
2924
}

globals.d.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

main.go

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,84 @@
11
package main
22

33
import (
4+
"container/list"
45
"io/ioutil"
5-
"reflect"
6-
"syscall/js"
6+
"strconv"
7+
"strings"
78

89
"github.com/rhysd/actionlint"
910
)
1011

11-
func toMap(input interface{}) map[string]interface{} {
12-
out := make(map[string]interface{})
13-
value := reflect.ValueOf(input)
14-
if value.Kind() == reflect.Ptr {
15-
value = value.Elem()
16-
}
12+
var Memory = list.New()
13+
var OutputString = []byte{}
1714

18-
for i := 0; i < value.NumField(); i++ {
19-
out[value.Type().Field(i).Name] = value.Field(i).Interface()
20-
}
15+
type block struct {
16+
ptr *[]byte
17+
value []byte
18+
}
19+
20+
//export WasmAlloc
21+
func WasmAlloc(size int) *[]byte {
22+
slice := make([]byte, size)
23+
block := block{
24+
ptr: &slice,
25+
value: slice,
26+
}
27+
Memory.PushBack(block)
2128

22-
return out
29+
return block.ptr
2330
}
2431

25-
func runActionlint(source string, filePath string) (interface{}, error) {
32+
//export WasmFree
33+
func WasmFree(ptr *[]byte) {
34+
for e := Memory.Front(); e != nil; e = e.Next() {
35+
block := e.Value.(block)
36+
if block.ptr == ptr {
37+
Memory.Remove(e)
38+
return
39+
}
40+
}
41+
}
42+
43+
func serialize(errors []*actionlint.Error, target *[]byte) {
44+
*target = []byte("[")
45+
46+
for i, err := range errors {
47+
*target = append(*target, `{
48+
"file":"`+err.Filepath+`",
49+
"line":`+strconv.FormatInt(int64(err.Line), 10)+`,
50+
"column":`+strconv.FormatInt(int64(err.Column), 10)+`,
51+
"message":"`+strings.ReplaceAll(err.Message, `"`, `\"`)+`",
52+
"kind":"`+strings.ReplaceAll(err.Kind, `"`, `\"`)+`"
53+
}`...)
54+
55+
if i < len(errors)-1 {
56+
*target = append(*target, ',')
57+
}
58+
}
59+
60+
*target = append(*target, ']', 0)
61+
}
62+
63+
//export RunActionlint
64+
func RunActionlint(input []byte, path []byte) *byte {
2665
opts := actionlint.LinterOptions{}
66+
2767
linter, err := actionlint.NewLinter(ioutil.Discard, &opts)
2868
if err != nil {
29-
return nil, err
69+
OutputString = []byte(err.Error())
70+
return &OutputString[0]
3071
}
3172

32-
errs, err := linter.Lint(filePath, []byte(source), nil)
73+
errs, err := linter.Lint(string(path), input, nil)
3374
if err != nil {
34-
return nil, err
75+
OutputString = []byte(err.Error())
76+
return &OutputString[0]
3577
}
3678

37-
ret := make([]interface{}, 0, len(errs))
38-
for _, err := range errs {
39-
ret = append(ret, toMap(*err))
40-
}
79+
serialize(errs, &OutputString)
4180

42-
return ret, nil
81+
return &OutputString[0]
4382
}
4483

45-
func main() {
46-
js.Global().Set("runActionlint", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
47-
result, err := runActionlint(args[0].String(), args[1].String())
48-
return js.Global().Get("Array").New(result, err)
49-
}))
50-
51-
js.Global().Call("actionlintInitialized")
52-
53-
select {}
54-
}
84+
func main() {}

node.cjs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
const { join } = require("path");
2-
const { pathToFileURL } = require("url");
1+
const { join } = require("node:path");
2+
const { pathToFileURL } = require("node:url");
33
const { readFile } = require("node:fs/promises");
44
const { createActionlint } = require("./actionlint.cjs");
55

@@ -9,23 +9,16 @@ const { createActionlint } = require("./actionlint.cjs");
99
* @typedef {import("./types").LintResult} LintResult
1010
*/
1111

12-
/** @type {RunActionlint | undefined} */
13-
let runLint = undefined;
14-
1512
/**
1613
* @param {URL} url
17-
* @returns {RunActionlint}
14+
* @returns {Promise<RunActionlint>}
1815
*/
19-
module.exports.createLinter = function createLinter(
16+
module.exports.createLinter = async function createLinter(
2017
url = pathToFileURL(join(__dirname, "main.wasm"))
2118
) {
22-
if (runLint) {
23-
return runLint;
24-
}
25-
26-
return (runLint = createActionlint(
19+
return await createActionlint(
2720
/** @type {WasmLoader} */ async (go) => {
2821
return WebAssembly.instantiate(await readFile(url), go.importObject);
2922
}
30-
));
23+
);
3124
};

0 commit comments

Comments
 (0)