Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI
on:
pull_request:
push:
branches: [main]

jobs:
go-test:
name: Go Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24.x'
- run: go test ./...

js-test:
name: JS Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24.x'
- uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install wasm-opt
run: sudo apt-get install -y binaryen
- name: Build and test
run: |
cd js
npm ci
npm run build
node --test dist/format.test.js
2 changes: 1 addition & 1 deletion js/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `@reteps/dockerfmt`

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.
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.


```js
Expand Down
31 changes: 21 additions & 10 deletions js/format.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
//go:build js || wasm
// +build js wasm
// WASM entry point for the JS bindings. Built with standard Go (GOOS=js
// GOARCH=wasm), not TinyGo. TinyGo produces smaller binaries but has a
// blocking bug:
// - reflect.AssignableTo panics with interfaces, breaking encoding/json
// used by the moby/buildkit parser (https://github.com/tinygo-org/tinygo/issues/4277)

//go:build js && wasm

package main

import (
"strings"
"syscall/js"

"github.com/reteps/dockerfmt/lib"
)

//export formatBytes
func formatBytes(contents []byte, indentSize uint, newlineFlag bool, spaceRedirects bool) *byte {
originalLines := strings.SplitAfter(string(contents), "\n")
func formatBytes(_ js.Value, args []js.Value) any {
contents := args[0].String()
indentSize := uint(args[1].Int())
newlineFlag := args[2].Bool()
spaceRedirects := args[3].Bool()

originalLines := strings.SplitAfter(contents, "\n")
c := &lib.Config{
IndentSize: indentSize,
TrailingNewline: newlineFlag,
SpaceRedirects: spaceRedirects,
}
result := lib.FormatFileLines(originalLines, c)
bytes := []byte(result)
return &bytes[0]
return lib.FormatFileLines(originalLines, c)
}

// Required to build
func main() {}
func main() {
js.Global().Set("__dockerfmt_formatBytes", js.FuncOf(formatBytes))
// Block forever to keep the Go runtime alive for subsequent calls.
select {}
}
82 changes: 82 additions & 0 deletions js/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { formatDockerfileContents } from './node.js'

const defaultOptions = {
indent: 4,
trailingNewline: true,
spaceRedirects: false,
}

describe('formatDockerfileContents', () => {
it('formats a basic Dockerfile', async () => {
const input = `from alpine
run echo hello
`.trim()

const result = await formatDockerfileContents(input, defaultOptions)
assert.equal(result, 'FROM alpine\nRUN echo hello\n')
})

it('formats CMD JSON form with spaces', async () => {
const input = `FROM alpine
CMD ["ls","-la"]
`.trim()

const result = await formatDockerfileContents(input, defaultOptions)
assert.equal(result, 'FROM alpine\nCMD ["ls", "-la"]\n')
})

it('formats RUN JSON form with spaces', async () => {
const input = `FROM alpine
RUN ["echo","hello"]
`.trim()

const result = await formatDockerfileContents(input, defaultOptions)
assert.equal(result, 'FROM alpine\nRUN ["echo", "hello"]\n')
})

it('handles the issue #25 reproduction case', async () => {
const input = `
FROM nginx
WORKDIR /app
ARG PROJECT_DIR=/
ARG NGINX_CONF=nginx.conf
COPY $NGINX_CONF /etc/nginx/conf.d/nginx.conf
COPY $PROJECT_DIR /app
CMD mkdir --parents /var/log/nginx && nginx -g "daemon off;"
`.trim()

const result = await formatDockerfileContents(input, {
indent: 4,
spaceRedirects: false,
trailingNewline: true,
})

assert.ok(result.includes('FROM nginx'))
assert.ok(result.includes('WORKDIR /app'))
assert.ok(result.endsWith('\n'))
})

it('respects trailingNewline: false', async () => {
const input = 'FROM alpine'
const result = await formatDockerfileContents(input, {
...defaultOptions,
trailingNewline: false,
})
assert.ok(!result.endsWith('\n'))
})

it('respects indent option', async () => {
const input = `FROM alpine
RUN echo a \\
&& echo b
`.trim()

const result = await formatDockerfileContents(input, {
...defaultOptions,
indent: 2,
})
assert.ok(result.includes(' && echo b'))
})
})
44 changes: 12 additions & 32 deletions js/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,34 @@ export const formatDockerfileContents = async (
getWasm: () => Promise<Buffer>,
) => {
const go = new Go() // Defined in wasm_exec.js
const encoder = new TextEncoder()
const decoder = new TextDecoder()

// get current working directory
const wasmBuffer = await getWasm()
const wasm = await WebAssembly.instantiate(wasmBuffer, go.importObject)

/**
* 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 `parse` and `print` function.
* able to call the formatBytes function.
*/
go.run(wasm.instance)

const { memory, malloc, free, formatBytes } = wasm.instance.exports as {
memory: WebAssembly.Memory
malloc: (size: number) => number
free: (pointer: number) => void
formatBytes: (
pointer: number,
length: number,
indent: number,
trailingNewline: boolean,
spaceRedirects: boolean,
) => number
}

const fileBufferBytes = encoder.encode(fileContents)
const filePointer = malloc(fileBufferBytes.byteLength)
const formatBytes = (globalThis as any).__dockerfmt_formatBytes as (
contents: string,
indent: number,
trailingNewline: boolean,
spaceRedirects: boolean,
) => string

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

// Call formatBytes function from WebAssembly
const resultPointer = formatBytes(
filePointer,
fileBufferBytes.byteLength,
return formatBytes(
fileContents,
options.indent,
options.trailingNewline,
options.spaceRedirects,
)

// Decode the result
const resultBytes = new Uint8Array(memory.buffer).subarray(resultPointer)
const end = resultBytes.indexOf(0)
const result = decoder.decode(resultBytes.subarray(0, end))
free(filePointer)

return result
}

export const formatDockerfile = () => {
Expand Down
Binary file modified js/format.wasm
Binary file not shown.
3 changes: 1 addition & 2 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@
"dist"
],
"scripts": {
"//": "Requires tinygo 0.38.0 or later",
"build": "npm run build-go && npm run build-js",
"build-go": "tinygo build -o format.wasm -target wasm --no-debug",
"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",
"build-js": "tsc && cp format.wasm wasm_exec.js dist",
"format": "prettier --write \"**/*.{js,ts,json}\""
},
Expand Down
Loading
Loading