Skip to content

Commit 1f3076e

Browse files
authored
Merge pull request #50 from reteps/js-bindings-fix
2 parents 59a51a5 + 8a47d4c commit 1f3076e

11 files changed

Lines changed: 683 additions & 377 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
on:
3+
pull_request:
4+
push:
5+
branches: [main]
6+
7+
jobs:
8+
go-test:
9+
name: Go Tests
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-go@v5
14+
with:
15+
go-version: '1.24.x'
16+
- run: go test ./...
17+
18+
js-test:
19+
name: JS Tests
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: actions/setup-go@v5
24+
with:
25+
go-version: '1.24.x'
26+
- uses: actions/setup-node@v4
27+
with:
28+
node-version: '20.x'
29+
- name: Install wasm-opt
30+
run: sudo apt-get install -y binaryen
31+
- name: Build and test
32+
run: |
33+
cd js
34+
npm ci
35+
npm run build
36+
node --test dist/format.test.js

js/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# `@reteps/dockerfmt`
22

3-
Bindings around the Golang `dockerfmt` tooling. It uses [tinygo](https://github.com/tinygo-org/tinygo) to compile the Go code to WebAssembly, which is then used in the JS bindings.
3+
Bindings around the Golang `dockerfmt` tooling. It compiles the Go code to WebAssembly (using standard Go's `GOOS=js GOARCH=wasm` target), which is then used in the JS bindings.
44

55

66
```js

js/format.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
1-
//go:build js || wasm
2-
// +build js wasm
1+
// WASM entry point for the JS bindings. Built with standard Go (GOOS=js
2+
// GOARCH=wasm), not TinyGo. TinyGo produces smaller binaries but has a
3+
// blocking bug:
4+
// - reflect.AssignableTo panics with interfaces, breaking encoding/json
5+
// used by the moby/buildkit parser (https://github.com/tinygo-org/tinygo/issues/4277)
6+
7+
//go:build js && wasm
38

49
package main
510

611
import (
712
"strings"
13+
"syscall/js"
814

915
"github.com/reteps/dockerfmt/lib"
1016
)
1117

12-
//export formatBytes
13-
func formatBytes(contents []byte, indentSize uint, newlineFlag bool, spaceRedirects bool) *byte {
14-
originalLines := strings.SplitAfter(string(contents), "\n")
18+
func formatBytes(_ js.Value, args []js.Value) any {
19+
contents := args[0].String()
20+
indentSize := uint(args[1].Int())
21+
newlineFlag := args[2].Bool()
22+
spaceRedirects := args[3].Bool()
23+
24+
originalLines := strings.SplitAfter(contents, "\n")
1525
c := &lib.Config{
1626
IndentSize: indentSize,
1727
TrailingNewline: newlineFlag,
1828
SpaceRedirects: spaceRedirects,
1929
}
20-
result := lib.FormatFileLines(originalLines, c)
21-
bytes := []byte(result)
22-
return &bytes[0]
30+
return lib.FormatFileLines(originalLines, c)
2331
}
2432

25-
// Required to build
26-
func main() {}
33+
func main() {
34+
js.Global().Set("__dockerfmt_formatBytes", js.FuncOf(formatBytes))
35+
// Block forever to keep the Go runtime alive for subsequent calls.
36+
select {}
37+
}

js/format.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, it } from 'node:test'
2+
import assert from 'node:assert/strict'
3+
import { formatDockerfileContents } from './node.js'
4+
5+
const defaultOptions = {
6+
indent: 4,
7+
trailingNewline: true,
8+
spaceRedirects: false,
9+
}
10+
11+
describe('formatDockerfileContents', () => {
12+
it('formats a basic Dockerfile', async () => {
13+
const input = `from alpine
14+
run echo hello
15+
`.trim()
16+
17+
const result = await formatDockerfileContents(input, defaultOptions)
18+
assert.equal(result, 'FROM alpine\nRUN echo hello\n')
19+
})
20+
21+
it('formats CMD JSON form with spaces', async () => {
22+
const input = `FROM alpine
23+
CMD ["ls","-la"]
24+
`.trim()
25+
26+
const result = await formatDockerfileContents(input, defaultOptions)
27+
assert.equal(result, 'FROM alpine\nCMD ["ls", "-la"]\n')
28+
})
29+
30+
it('formats RUN JSON form with spaces', async () => {
31+
const input = `FROM alpine
32+
RUN ["echo","hello"]
33+
`.trim()
34+
35+
const result = await formatDockerfileContents(input, defaultOptions)
36+
assert.equal(result, 'FROM alpine\nRUN ["echo", "hello"]\n')
37+
})
38+
39+
it('handles the issue #25 reproduction case', async () => {
40+
const input = `
41+
FROM nginx
42+
WORKDIR /app
43+
ARG PROJECT_DIR=/
44+
ARG NGINX_CONF=nginx.conf
45+
COPY $NGINX_CONF /etc/nginx/conf.d/nginx.conf
46+
COPY $PROJECT_DIR /app
47+
CMD mkdir --parents /var/log/nginx && nginx -g "daemon off;"
48+
`.trim()
49+
50+
const result = await formatDockerfileContents(input, {
51+
indent: 4,
52+
spaceRedirects: false,
53+
trailingNewline: true,
54+
})
55+
56+
assert.ok(result.includes('FROM nginx'))
57+
assert.ok(result.includes('WORKDIR /app'))
58+
assert.ok(result.endsWith('\n'))
59+
})
60+
61+
it('respects trailingNewline: false', async () => {
62+
const input = 'FROM alpine'
63+
const result = await formatDockerfileContents(input, {
64+
...defaultOptions,
65+
trailingNewline: false,
66+
})
67+
assert.ok(!result.endsWith('\n'))
68+
})
69+
70+
it('respects indent option', async () => {
71+
const input = `FROM alpine
72+
RUN echo a \\
73+
&& echo b
74+
`.trim()
75+
76+
const result = await formatDockerfileContents(input, {
77+
...defaultOptions,
78+
indent: 2,
79+
})
80+
assert.ok(result.includes(' && echo b'))
81+
})
82+
})

js/format.ts

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,54 +12,34 @@ export const formatDockerfileContents = async (
1212
getWasm: () => Promise<Buffer>,
1313
) => {
1414
const go = new Go() // Defined in wasm_exec.js
15-
const encoder = new TextEncoder()
16-
const decoder = new TextDecoder()
1715

18-
// get current working directory
1916
const wasmBuffer = await getWasm()
2017
const wasm = await WebAssembly.instantiate(wasmBuffer, go.importObject)
2118

2219
/**
2320
* Do not await this promise, because it only resolves once the go main()
2421
* function has exited. But we need the main function to stay alive to be
25-
* able to call the `parse` and `print` function.
22+
* able to call the formatBytes function.
2623
*/
2724
go.run(wasm.instance)
2825

29-
const { memory, malloc, free, formatBytes } = wasm.instance.exports as {
30-
memory: WebAssembly.Memory
31-
malloc: (size: number) => number
32-
free: (pointer: number) => void
33-
formatBytes: (
34-
pointer: number,
35-
length: number,
36-
indent: number,
37-
trailingNewline: boolean,
38-
spaceRedirects: boolean,
39-
) => number
40-
}
41-
42-
const fileBufferBytes = encoder.encode(fileContents)
43-
const filePointer = malloc(fileBufferBytes.byteLength)
26+
const formatBytes = (globalThis as any).__dockerfmt_formatBytes as (
27+
contents: string,
28+
indent: number,
29+
trailingNewline: boolean,
30+
spaceRedirects: boolean,
31+
) => string
4432

45-
new Uint8Array(memory.buffer).set(fileBufferBytes, filePointer)
33+
if (typeof formatBytes !== 'function') {
34+
throw new Error('dockerfmt WASM module did not register formatBytes')
35+
}
4636

47-
// Call formatBytes function from WebAssembly
48-
const resultPointer = formatBytes(
49-
filePointer,
50-
fileBufferBytes.byteLength,
37+
return formatBytes(
38+
fileContents,
5139
options.indent,
5240
options.trailingNewline,
5341
options.spaceRedirects,
5442
)
55-
56-
// Decode the result
57-
const resultBytes = new Uint8Array(memory.buffer).subarray(resultPointer)
58-
const end = resultBytes.indexOf(0)
59-
const result = decoder.decode(resultBytes.subarray(0, end))
60-
free(filePointer)
61-
62-
return result
6343
}
6444

6545
export const formatDockerfile = () => {

js/format.wasm

4.89 MB
Binary file not shown.

js/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@
2929
"dist"
3030
],
3131
"scripts": {
32-
"//": "Requires tinygo 0.38.0 or later",
3332
"build": "npm run build-go && npm run build-js",
34-
"build-go": "tinygo build -o format.wasm -target wasm --no-debug",
33+
"build-go": "GOOS=js GOARCH=wasm go build -ldflags='-s -w' -o format.wasm && wasm-opt --enable-bulk-memory -Oz -o format-opt.wasm format.wasm && mv format-opt.wasm format.wasm",
3534
"build-js": "tsc && cp format.wasm wasm_exec.js dist",
3635
"format": "prettier --write \"**/*.{js,ts,json}\""
3736
},

0 commit comments

Comments
 (0)