diff --git a/.github/actions/setup-tinygo/action.yml b/.github/actions/setup-tinygo/action.yml new file mode 100644 index 0000000..643066e --- /dev/null +++ b/.github/actions/setup-tinygo/action.yml @@ -0,0 +1,51 @@ +name: 'Setup TinyGo environment' +description: 'Setup a TinyGo environment and add it to the PATH' +inputs: + version: + description: 'The Go version to download and use. Must be a valid commitish, e.g. "dev" or "v1.0.0"' + required: true +runs: + using: "composite" + steps: + - name: Install LLVM 13 + run: | + source /etc/os-release + echo "deb http://apt.llvm.org/${UBUNTU_CODENAME}/ llvm-toolchain-${UBUNTU_CODENAME}-13 main" | sudo tee /etc/apt/sources.list.d/llvm.list + wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - + sudo apt-get -qq update + sudo apt-get -yqq install clang-13 llvm-13-dev lld-13 libclang-13-dev + shell: bash + + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + + - name: Install Binaryen + run: | + pushd /dev/shm + VERSION="$(curl -fsSLI -o /dev/null -w "%{url_effective}" https://github.com/WebAssembly/binaryen/releases/latest)" + VERSION="${VERSION#https://github.com/WebAssembly/binaryen/releases/tag/}" + curl -sfOL "https://github.com/WebAssembly/binaryen/releases/download/${VERSION}/binaryen-${VERSION}-$(uname -m)-linux.tar.gz" + tar -xvf "binaryen-${VERSION}-$(uname -m)-linux.tar.gz" + mkdir -p "${RUNNER_TOOL_CACHE}/binaryen" + mv "binaryen-${VERSION}" "${RUNNER_TOOL_CACHE}/binaryen/${VERSION}" + echo "${RUNNER_TOOL_CACHE}/binaryen/${VERSION}/bin" >> $GITHUB_PATH + popd + shell: bash + + - name: Install TinyGo + run: | + pushd /dev/shm + mkdir tinygo + cd tinygo + git init + git remote add origin https://github.com/tinygo-org/tinygo.git + git fetch --prune --progress --no-tags --depth=1 origin "${{ inputs.version }}" + git checkout --progress --force -B "${{ inputs.version }}" FETCH_HEAD + git submodule update --init --recursive --depth 1 + make wasi-libc + mkdir -p "${RUNNER_TOOL_CACHE}/tinygo/${{ inputs.version }}" + GOBIN="${RUNNER_TOOL_CACHE}/tinygo/${{ inputs.version }}" go install + echo "${RUNNER_TOOL_CACHE}/tinygo/${{ inputs.version }}" >> $GITHUB_PATH + popd + shell: bash diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 8dc4804..59ed028 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -8,8 +8,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Setup Go - uses: actions/setup-go@v3 + - name: Setup TinyGo + uses: ./.github/actions/setup-tinygo + with: + version: fce42fc7fa22f9a8061ef8700e2d17a6082f782e # latest commit on dev at the time of writing - name: Setup Node uses: actions/setup-node@v3 @@ -41,10 +43,10 @@ jobs: cat << EOF > test.mjs import { createLinter } from 'actionlint'; - createLinter()('on: psuh', 'push.yml').then( - (results) => process.exit(results.length > 0 ? 0 : 1), - (err) => { console.error(err); process.exit(1); } - ); + createLinter().then(lint => { + const results = lint('on: psuh', 'push.yml'); + process.exit(results.length > 0 ? 0 : 1) + }, (err) => { console.error(err); process.exit(1); }); EOF # test that the linter works diff --git a/.gitignore b/.gitignore index 2ace2e6..cf1cca7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ main.wasm -wasm_exec.js +tmp.wasm node_modules/ -types/ \ No newline at end of file +types/ diff --git a/.npmignore b/.npmignore index b947b62..2992f89 100644 --- a/.npmignore +++ b/.npmignore @@ -1,11 +1,12 @@ .github/ .vscode/ +node_modules/ go.mod go.sum -globals.d.ts main.go Makefile renovate.json test.mjs +tmp.wasm tsconfig.json yarn.lock diff --git a/.vscode/settings.json b/.vscode/settings.json index 255414f..9bfb438 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,4 +5,4 @@ "GOARCH": "wasm" } } -} \ No newline at end of file +} diff --git a/Makefile b/Makefile index 90e4f1e..ec2e981 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,17 @@ -build: wasm_exec.js main.wasm types/node.d.mts +build: main.wasm types/node.d.mts -wasm_exec.js: - cp $$(go env GOROOT)/misc/wasm/wasm_exec.js wasm_exec.tmp.js - echo "// @ts-nocheck" > wasm_exec.js - cat wasm_exec.tmp.js >> wasm_exec.js - rm wasm_exec.tmp.js +main.wasm: tmp.wasm + wasm-opt -c -O4 -o main.wasm tmp.wasm -main.wasm: main.go go.mod - GOOS=js GOARCH=wasm go build -o main.wasm +tmp.wasm: main.go go.mod + tinygo build -o tmp.wasm -target wasm -panic trap -opt z main.go types/node.d.mts: *.cjs *.mjs *.d.ts *.json yarn.lock $$(yarn bin)/tsc -p . clean: rm main.wasm - rm wasm_exec.js + rm tmp.wasm rm -rf types -.PHONY: build clean \ No newline at end of file +.PHONY: build clean diff --git a/actionlint.cjs b/actionlint.cjs index 08addff..2985569 100644 --- a/actionlint.cjs +++ b/actionlint.cjs @@ -1,4 +1,4 @@ -require("./wasm_exec.js"); +require("./tiny_wasm_exec.js"); /** * @typedef {(go: Go) => Promise} WasmLoader @@ -8,54 +8,68 @@ require("./wasm_exec.js"); /** * @param {WasmLoader} loader - * @returns {RunActionlint} + * @returns {Promise} */ -module.exports.createActionlint = function createActionlint(loader) { +module.exports.createActionlint = async function createActionlint(loader) { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); const go = new Go(); - /** @type {(() => void)[] | undefined} */ - let queued = undefined; + const wasm = await loader(go); + // Do not await this promise, because it only resolves once the go main() + // function has exited. But we need the main function to stay alive to be + // able to call the `runActionlint` function. + go.run(wasm.instance); - // This function gets called from go once the wasm module is ready and it - // executes the linter for all queued calls. - globalThis.actionlintInitialized = () => { - queued?.forEach((f) => f()); - queued = globalThis.actionlintInitialized = undefined; - }; + const { memory, WasmAlloc, WasmFree, RunActionlint } = wasm.instance.exports; - loader(go).then((wasm) => { - // Do not await this promise, because it only resolves once the go main() - // function has exited. But we need the main function to stay alive to be - // able to call the `runActionlint` function. - go.run(wasm.instance); - }); + if ( + !(memory instanceof WebAssembly.Memory) || + !(WasmAlloc instanceof Function) || + !(WasmFree instanceof Function) || + !(RunActionlint instanceof Function) + ) { + throw new Error( + "Invalid wasm exports. Expected memory, WasmAlloc, WasmFree, RunActionlint." + ); + } /** - * @param {string} src + * @param {string} input * @param {string} path - * @returns {Promise} + * @returns {LintResult[]} */ - return async function runLint(src, path) { - // Return a promise, because we need to queue calls to `runLint()` while the - // wasm module is still loading and execute them once the wasm module is - //ready. - return new Promise((resolve, reject) => { - if (typeof runActionlint === "function") { - const [result, err] = runActionlint(src, path); - return err ? reject(err) : resolve(result); - } - - if (!queued) { - queued = []; - } - - queued.push(() => { - const [result, err] = runActionlint?.(src, path) ?? [ - [], - new Error('"runActionlint" is not defined'), - ]; - return err ? reject(err) : resolve(result); - }); - }); + return function runLint(input, path) { + const workflow = encoder.encode(input); + const filePath = encoder.encode(path); + + const workflowPointer = WasmAlloc(workflow.byteLength); + new Uint8Array(memory.buffer).set(workflow, workflowPointer); + + const filePathPointer = WasmAlloc(filePath.byteLength); + new Uint8Array(memory.buffer).set(filePath, filePathPointer); + + const resultPointer = RunActionlint( + workflowPointer, + workflow.byteLength, + workflow.byteLength, + filePathPointer, + filePath.byteLength, + filePath.byteLength + ); + + WasmFree(workflowPointer); + WasmFree(filePathPointer); + + const result = new Uint8Array(memory.buffer).subarray(resultPointer); + const end = result.indexOf(0); + + const string = decoder.decode(result.subarray(0, end)); + + try { + return JSON.parse(string); + } catch { + throw new Error(string); + } }; }; diff --git a/browser.mjs b/browser.mjs index 751fba8..5f5eb11 100644 --- a/browser.mjs +++ b/browser.mjs @@ -6,24 +6,19 @@ import { createActionlint } from "./actionlint.cjs"; * @typedef {import("./types").LintResult} LintResult */ -/** @type {RunActionlint | undefined} */ -let runLint = undefined; - /** * @param {URL} url - * @returns {RunActionlint} + * @returns {Promise} */ -export function createLinter(url = new URL("./main.wasm", import.meta.url)) { - if (runLint) { - return runLint; - } - - return (runLint = createActionlint( +export async function createLinter( + url = new URL("./main.wasm", import.meta.url) +) { + return await createActionlint( /** @type {WasmLoader} */ async (go) => { return WebAssembly.instantiateStreaming( fetch(url.toString()), go.importObject ); } - )); + ); } diff --git a/globals.d.ts b/globals.d.ts deleted file mode 100644 index ac3ec31..0000000 --- a/globals.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export declare global { - var runActionlint: - | ((src: string, path: string) => [LintResult[], Error | null]) - | undefined; - var actionlintInitialized: (() => void) | undefined; -} diff --git a/go.mod b/go.mod index 2f09a16..4938b6f 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,18 @@ module source.xing.com/fea/act-app go 1.17 +require github.com/rhysd/actionlint v1.6.12 + require ( github.com/fatih/color v1.13.0 // indirect - github.com/mattn/go-colorable v0.1.11 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/rhysd/actionlint v1.6.9 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/robfig/cron v1.2.0 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 // indirect + golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) + +replace gopkg.in/yaml.v3 => github.com/ZauberNerd/yaml v0.0.0-20220305122605-f022dd71f1c2 diff --git a/go.sum b/go.sum index 839ab45..1c872ec 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,36 @@ +github.com/ZauberNerd/yaml v0.0.0-20220305122605-f022dd71f1c2 h1:XKY7uPNa/yq6NV/j6HHrAck1Aco1Xlj5Y+bDHnN9HHA= +github.com/ZauberNerd/yaml v0.0.0-20220305122605-f022dd71f1c2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/rhysd/actionlint v1.6.9 h1:8rQQ76o88zctUCzukt0A5O/FO003wTGbkLQuwQkMf9c= -github.com/rhysd/actionlint v1.6.9/go.mod h1:0AA4pvZ2nrZHT6D86eUhieH2NFmLqhxrNex0NEa2A2g= +github.com/rhysd/actionlint v1.6.12 h1:Qoa69UsvF/7tNc0008v7s1hHDANHuqaq0qHhu4syf7w= +github.com/rhysd/actionlint v1.6.12/go.mod h1:M+vAgTIFE2yOdr91fpDF4CUsyZVsPmS+D/x2K7qhhP0= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 h1:7NCfEGl0sfUojmX78nK9pBJuUlSZWEJA/TwASvfiPLo= -golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f h1:rlezHXNlxYWvBCzNses9Dlc7nGFaNMJeqLolcmQSSZY= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 6535937..815e454 100644 --- a/main.go +++ b/main.go @@ -1,54 +1,84 @@ package main import ( + "container/list" "io/ioutil" - "reflect" - "syscall/js" + "strconv" + "strings" "github.com/rhysd/actionlint" ) -func toMap(input interface{}) map[string]interface{} { - out := make(map[string]interface{}) - value := reflect.ValueOf(input) - if value.Kind() == reflect.Ptr { - value = value.Elem() - } +var Memory = list.New() +var OutputString = []byte{} - for i := 0; i < value.NumField(); i++ { - out[value.Type().Field(i).Name] = value.Field(i).Interface() - } +type block struct { + ptr *[]byte + value []byte +} + +//export WasmAlloc +func WasmAlloc(size int) *[]byte { + slice := make([]byte, size) + block := block{ + ptr: &slice, + value: slice, + } + Memory.PushBack(block) - return out + return block.ptr } -func runActionlint(source string, filePath string) (interface{}, error) { +//export WasmFree +func WasmFree(ptr *[]byte) { + for e := Memory.Front(); e != nil; e = e.Next() { + block := e.Value.(block) + if block.ptr == ptr { + Memory.Remove(e) + return + } + } +} + +func serialize(errors []*actionlint.Error, target *[]byte) { + *target = []byte("[") + + for i, err := range errors { + *target = append(*target, `{ + "file":"`+err.Filepath+`", + "line":`+strconv.FormatInt(int64(err.Line), 10)+`, + "column":`+strconv.FormatInt(int64(err.Column), 10)+`, + "message":"`+strings.ReplaceAll(err.Message, `"`, `\"`)+`", + "kind":"`+strings.ReplaceAll(err.Kind, `"`, `\"`)+`" +}`...) + + if i < len(errors)-1 { + *target = append(*target, ',') + } + } + + *target = append(*target, ']', 0) +} + +//export RunActionlint +func RunActionlint(input []byte, path []byte) *byte { opts := actionlint.LinterOptions{} + linter, err := actionlint.NewLinter(ioutil.Discard, &opts) if err != nil { - return nil, err + OutputString = []byte(err.Error()) + return &OutputString[0] } - errs, err := linter.Lint(filePath, []byte(source), nil) + errs, err := linter.Lint(string(path), input, nil) if err != nil { - return nil, err + OutputString = []byte(err.Error()) + return &OutputString[0] } - ret := make([]interface{}, 0, len(errs)) - for _, err := range errs { - ret = append(ret, toMap(*err)) - } + serialize(errs, &OutputString) - return ret, nil + return &OutputString[0] } -func main() { - js.Global().Set("runActionlint", js.FuncOf(func(this js.Value, args []js.Value) interface{} { - result, err := runActionlint(args[0].String(), args[1].String()) - return js.Global().Get("Array").New(result, err) - })) - - js.Global().Call("actionlintInitialized") - - select {} -} +func main() {} diff --git a/node.cjs b/node.cjs index 9b3afb4..9500b84 100644 --- a/node.cjs +++ b/node.cjs @@ -1,5 +1,5 @@ -const { join } = require("path"); -const { pathToFileURL } = require("url"); +const { join } = require("node:path"); +const { pathToFileURL } = require("node:url"); const { readFile } = require("node:fs/promises"); const { createActionlint } = require("./actionlint.cjs"); @@ -9,23 +9,16 @@ const { createActionlint } = require("./actionlint.cjs"); * @typedef {import("./types").LintResult} LintResult */ -/** @type {RunActionlint | undefined} */ -let runLint = undefined; - /** * @param {URL} url - * @returns {RunActionlint} + * @returns {Promise} */ -module.exports.createLinter = function createLinter( +module.exports.createLinter = async function createLinter( url = pathToFileURL(join(__dirname, "main.wasm")) ) { - if (runLint) { - return runLint; - } - - return (runLint = createActionlint( + return await createActionlint( /** @type {WasmLoader} */ async (go) => { return WebAssembly.instantiate(await readFile(url), go.importObject); } - )); + ); }; diff --git a/node.mjs b/node.mjs index c682553..024568a 100644 --- a/node.mjs +++ b/node.mjs @@ -7,21 +7,16 @@ import { createActionlint } from "./actionlint.cjs"; * @typedef {import("./types").LintResult} LintResult */ -/** @type {RunActionlint | undefined} */ -let runLint = undefined; - /** * @param {URL} url - * @returns {RunActionlint} + * @returns {Promise} */ -export function createLinter(url = new URL("./main.wasm", import.meta.url)) { - if (runLint) { - return runLint; - } - - return (runLint = createActionlint( +export async function createLinter( + url = new URL("./main.wasm", import.meta.url) +) { + return await createActionlint( /** @type {WasmLoader} */ async (go) => { return WebAssembly.instantiate(await readFile(url), go.importObject); } - )); + ); } diff --git a/package.json b/package.json index e0980e8..aad628e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "actionlint", "version": "0.0.0", "license": "MIT", - "sideEffects": true, + "sideEffects": false, "scripts": { "test": "tape test.mjs | tap-spec" }, diff --git a/test.mjs b/test.mjs index 8425c1a..e35ad3e 100644 --- a/test.mjs +++ b/test.mjs @@ -5,10 +5,10 @@ import { createLinter } from "./node.cjs"; test("actionlint no errors", async (t) => { t.plan(1); - const linter = createLinter(); + const linter = await createLinter(); const content = await readFile("./.github/workflows/push.yml", "utf8"); - const result = await linter(content, "./.github/workflows/push.yml"); + const result = linter(content, "./.github/workflows/push.yml"); t.equal(result.length, 0, "no errors"); @@ -16,13 +16,30 @@ test("actionlint no errors", async (t) => { }); test("actionlint errors", async (t) => { - t.plan(1); + t.plan(3); - const linter = createLinter(); + const linter = await createLinter(); - const result = await linter("on: psuh", "broken.yml"); + const result = linter("on: psuh", "broken.yml"); t.equal(result.length, 2, "2 errors"); + t.deepEqual(result[0], { + file: "broken.yml", + line: 1, + column: 1, + message: '"jobs" section is missing in workflow', + kind: "syntax-check", + }); + + t.deepEqual(result[1], { + file: "broken.yml", + line: 1, + column: 5, + message: + 'unknown Webhook event "psuh". see https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#webhook-events for list of all Webhook event names', + kind: "events", + }); + t.end(); }); diff --git a/tiny_wasm_exec.js b/tiny_wasm_exec.js new file mode 100644 index 0000000..a2fddde --- /dev/null +++ b/tiny_wasm_exec.js @@ -0,0 +1,536 @@ +// @ts-nocheck +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// This file has been modified for use by the TinyGo compiler. + +(() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + + if (typeof global !== "undefined") { + // global already exists + } else if (typeof window !== "undefined") { + window.global = window; + } else if (typeof self !== "undefined") { + self.global = self; + } else { + throw new Error("cannot export Go (neither global, window nor self is defined)"); + } + + if (!global.require && typeof require !== "undefined") { + global.require = require; + } + + if (!global.fs && global.require) { + global.fs = require("fs"); + } + + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!global.fs) { + let outputBuf = ""; + global.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!global.process) { + global.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!global.crypto) { + const nodeCrypto = require("crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } + + if (!global.TextEncoder) { + global.TextEncoder = require("util").TextEncoder; + } + + if (!global.TextDecoder) { + global.TextDecoder = require("util").TextDecoder; + } + + // End of polyfills for common API. + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + var logLine = []; + + global.Go = class { + constructor() { + this._callbackTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.memory.buffer); + } + + const setInt64 = (addr, v) => { + mem().setUint32(addr + 0, v, true); + mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const getInt64 = (addr) => { + const low = mem().getUint32(addr + 0, true); + const high = mem().getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = mem().getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = mem().getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number") { + if (isNaN(v)) { + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 0, true); + return; + } + if (v === 0) { + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 1, true); + return; + } + mem().setFloat64(addr, v, true); + return; + } + + switch (v) { + case undefined: + mem().setFloat64(addr, 0, true); + return; + case null: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 2, true); + return; + case true: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 3, true); + return; + case false: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 4, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 1; + switch (typeof v) { + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + mem().setUint32(addr + 4, nanHead | typeFlag, true); + mem().setUint32(addr, id, true); + } + + const loadSlice = (array, len, cap) => { + return new Uint8Array(this._inst.exports.memory.buffer, array, len); + } + + const loadSliceOfValues = (array, len, cap) => { + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (ptr, len) => { + return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + wasi_snapshot_preview1: { + // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write + fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { + let nwritten = 0; + if (fd == 1) { + for (let iovs_i=0; iovs_i 0, // dummy + fd_fdstat_get: () => 0, // dummy + fd_seek: () => 0, // dummy + "proc_exit": (code) => { + if (global.process) { + // Node.js + process.exit(code); + } else { + // Can't exit in a browser. + throw 'trying to exit with code ' + code; + } + }, + random_get: (bufPtr, bufLen) => { + crypto.getRandomValues(loadSlice(bufPtr, bufLen)); + return 0; + }, + }, + env: { + // func ticks() float64 + "runtime.ticks": () => { + return timeOrigin + performance.now(); + }, + + // func sleepTicks(timeout float64) + "runtime.sleepTicks": (timeout) => { + // Do not sleep, only reactivate scheduler after the given timeout. + setTimeout(this._inst.exports.go_scheduler, timeout); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + // Note: TinyGo does not support finalizers so this should never be + // called. + console.error('syscall/js.finalizeRef not implemented'); + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => { + const s = loadString(value_ptr, value_len); + storeValue(ret_ptr, s); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (retval, v_addr, p_ptr, p_len) => { + let prop = loadString(p_ptr, p_len); + let value = loadValue(v_addr); + let result = Reflect.get(value, prop); + storeValue(retval, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (v_addr, p_ptr, p_len, x_addr) => { + const v = loadValue(v_addr); + const p = loadString(p_ptr, p_len); + const x = loadValue(x_addr); + Reflect.set(v, p, x); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (v_addr, p_ptr, p_len) => { + const v = loadValue(v_addr); + const p = loadString(p_ptr, p_len); + Reflect.deleteProperty(v, p); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (ret_addr, v_addr, i) => { + storeValue(ret_addr, Reflect.get(loadValue(v_addr), i)); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (v_addr, i, x_addr) => { + Reflect.set(loadValue(v_addr), i, loadValue(x_addr)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => { + const v = loadValue(v_addr); + const name = loadString(m_ptr, m_len); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + const m = Reflect.get(v, name); + storeValue(ret_addr, Reflect.apply(m, v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { + try { + const v = loadValue(v_addr); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + storeValue(ret_addr, Reflect.apply(v, undefined, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr + 8, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { + const v = loadValue(v_addr); + const args = loadSliceOfValues(args_ptr, args_len, args_cap); + try { + storeValue(ret_addr, Reflect.construct(v, args)); + mem().setUint8(ret_addr + 8, 1); + } catch (err) { + storeValue(ret_addr, err); + mem().setUint8(ret_addr+ 8, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (v_addr) => { + return loadValue(v_addr).length; + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (ret_addr, v_addr) => { + const s = String(loadValue(v_addr)); + const str = encoder.encode(s); + storeValue(ret_addr, str); + setInt64(ret_addr + 8, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (v_addr, slice_ptr, slice_len, slice_cap) => { + const str = loadValue(v_addr); + loadSlice(slice_ptr, slice_len, slice_cap).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (v_addr, t_addr) => { + return loadValue(v_addr) instanceof loadValue(t_addr); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, source_addr) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = loadSlice(dest_addr, dest_len); + const src = loadValue(source_addr); + if (!(src instanceof Uint8Array)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(num_bytes_copied_addr, toCopy.length); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + + // copyBytesToJS(dst ref, src []byte) (int, bool) + // Originally copied from upstream Go project, then modified: + // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 + "syscall/js.copyBytesToJS": (ret_addr, dest_addr, source_addr, source_len, source_cap) => { + let num_bytes_copied_addr = ret_addr; + let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable + + const dst = loadValue(dest_addr); + const src = loadSlice(source_addr, source_len); + if (!(dst instanceof Uint8Array)) { + mem().setUint8(returned_status_addr, 0); // Return "not ok" status + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(num_bytes_copied_addr, toCopy.length); + mem().setUint8(returned_status_addr, 1); // Return "ok" status + }, + } + }; + } + + async run(instance) { + this._inst = instance; + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ]; + this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map(); // mapping from JS values to reference ids + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + const mem = new DataView(this._inst.exports.memory.buffer) + + while (true) { + const callbackPromise = new Promise((resolve) => { + this._resolveCallbackPromise = () => { + if (this.exited) { + throw new Error("bad callback: Go program has already exited"); + } + setTimeout(resolve, 0); // make sure it is asynchronous + }; + }); + this._inst.exports._start(); + if (this.exited) { + break; + } + await callbackPromise; + } + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } + + if ( + global.require && + global.require.main === module && + global.process && + global.process.versions && + !global.process.versions.electron + ) { + if (process.argv.length != 3) { + console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); + process.exit(1); + } + + const go = new Go(); + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { + return go.run(result.instance); + }).catch((err) => { + console.error(err); + process.exit(1); + }); + } +})(); diff --git a/tsconfig.json b/tsconfig.json index cdffc86..5ea6211 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,4 +15,4 @@ "strict": true, "skipLibCheck": true } -} \ No newline at end of file +} diff --git a/types.d.ts b/types.d.ts index b5a7085..2210cba 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,12 +1,11 @@ -export type RunActionlint = ( - source: string, - path: string -) => Promise; +export type RunActionlint = (input: string, path: string) => LintResult[]; + export type LintResult = { - Message: string; - Filepath: string; - Line: number; - Column: number; - Kind: string; + file: string; + line: number; + column: number; + message: string; + kind: string; }; -export function createLinter(url?: URL): RunActionlint; + +export function createLinter(url?: URL): Promise;