@reteps/dockerfmt 0.3.6 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@reteps/dockerfmt`
2
2
 
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.
4
4
 
5
5
 
6
6
  ```js
package/dist/format.js CHANGED
@@ -1,29 +1,22 @@
1
1
  import './wasm_exec.js';
2
2
  export const formatDockerfileContents = async (fileContents, options, getWasm) => {
3
- const go = new Go(); // Defined in wasm_exec.js
4
- const encoder = new TextEncoder();
5
- const decoder = new TextDecoder();
6
- // get current working directory
3
+ // Use our namespaced Go class instead of globalThis.Go to avoid conflicts
4
+ // with other Go WASM packages (see wasm_exec.js modifications).
5
+ const GoClass = globalThis.__dockerfmt_Go;
6
+ const go = new GoClass();
7
7
  const wasmBuffer = await getWasm();
8
8
  const wasm = await WebAssembly.instantiate(wasmBuffer, go.importObject);
9
9
  /**
10
10
  * Do not await this promise, because it only resolves once the go main()
11
11
  * function has exited. But we need the main function to stay alive to be
12
- * able to call the `parse` and `print` function.
12
+ * able to call the formatBytes function.
13
13
  */
14
14
  go.run(wasm.instance);
15
- const { memory, malloc, free, formatBytes } = wasm.instance.exports;
16
- const fileBufferBytes = encoder.encode(fileContents);
17
- const filePointer = malloc(fileBufferBytes.byteLength);
18
- new Uint8Array(memory.buffer).set(fileBufferBytes, filePointer);
19
- // Call formatBytes function from WebAssembly
20
- const resultPointer = formatBytes(filePointer, fileBufferBytes.byteLength, options.indent, options.trailingNewline, options.spaceRedirects);
21
- // Decode the result
22
- const resultBytes = new Uint8Array(memory.buffer).subarray(resultPointer);
23
- const end = resultBytes.indexOf(0);
24
- const result = decoder.decode(resultBytes.subarray(0, end));
25
- free(filePointer);
26
- return result;
15
+ const formatBytes = globalThis.__dockerfmt_formatBytes;
16
+ if (typeof formatBytes !== 'function') {
17
+ throw new Error('dockerfmt WASM module did not register formatBytes');
18
+ }
19
+ return formatBytes(fileContents, options.indent, options.trailingNewline, options.spaceRedirects);
27
20
  };
28
21
  export const formatDockerfile = () => {
29
22
  throw new Error('`formatDockerfile` is not implemented in the browser. Use `formatDockerfileContents` instead.');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { formatDockerfileContents } from './node.js';
4
+ const defaultOptions = {
5
+ indent: 4,
6
+ trailingNewline: true,
7
+ spaceRedirects: false,
8
+ };
9
+ describe('formatDockerfileContents', () => {
10
+ it('formats a basic Dockerfile', async () => {
11
+ const input = `from alpine
12
+ run echo hello
13
+ `.trim();
14
+ const result = await formatDockerfileContents(input, defaultOptions);
15
+ assert.equal(result, 'FROM alpine\nRUN echo hello\n');
16
+ });
17
+ it('formats CMD JSON form with spaces', async () => {
18
+ const input = `FROM alpine
19
+ CMD ["ls","-la"]
20
+ `.trim();
21
+ const result = await formatDockerfileContents(input, defaultOptions);
22
+ assert.equal(result, 'FROM alpine\nCMD ["ls", "-la"]\n');
23
+ });
24
+ it('formats RUN JSON form with spaces', async () => {
25
+ const input = `FROM alpine
26
+ RUN ["echo","hello"]
27
+ `.trim();
28
+ const result = await formatDockerfileContents(input, defaultOptions);
29
+ assert.equal(result, 'FROM alpine\nRUN ["echo", "hello"]\n');
30
+ });
31
+ it('handles the issue #25 reproduction case', async () => {
32
+ const input = `
33
+ FROM nginx
34
+ WORKDIR /app
35
+ ARG PROJECT_DIR=/
36
+ ARG NGINX_CONF=nginx.conf
37
+ COPY $NGINX_CONF /etc/nginx/conf.d/nginx.conf
38
+ COPY $PROJECT_DIR /app
39
+ CMD mkdir --parents /var/log/nginx && nginx -g "daemon off;"
40
+ `.trim();
41
+ const result = await formatDockerfileContents(input, {
42
+ indent: 4,
43
+ spaceRedirects: false,
44
+ trailingNewline: true,
45
+ });
46
+ assert.ok(result.includes('FROM nginx'));
47
+ assert.ok(result.includes('WORKDIR /app'));
48
+ assert.ok(result.endsWith('\n'));
49
+ });
50
+ it('respects trailingNewline: false', async () => {
51
+ const input = 'FROM alpine';
52
+ const result = await formatDockerfileContents(input, {
53
+ ...defaultOptions,
54
+ trailingNewline: false,
55
+ });
56
+ assert.ok(!result.endsWith('\n'));
57
+ });
58
+ it('respects indent option', async () => {
59
+ const input = `FROM alpine
60
+ RUN echo a \\
61
+ && echo b
62
+ `.trim();
63
+ const result = await formatDockerfileContents(input, {
64
+ ...defaultOptions,
65
+ indent: 2,
66
+ });
67
+ assert.ok(result.includes(' && echo b'));
68
+ });
69
+ });
package/dist/format.wasm CHANGED
Binary file
package/dist/wasm_exec.js CHANGED
@@ -1,54 +1,26 @@
1
- // https://github.com/tinygo-org/tinygo/blob/64d8a043084cb9a56763192000e40ddc5dce733f/targets/wasm_exec.js
2
1
  // Copyright 2018 The Go Authors. All rights reserved.
3
2
  // Use of this source code is governed by a BSD-style
4
3
  // license that can be found in the LICENSE file.
5
- //
6
- // This file has been modified for use by the TinyGo compiler.
7
4
 
8
- (() => {
9
- // Map multiple JavaScript environments to a single common API,
10
- // preferring web standards over Node.js API.
11
- //
12
- // Environments considered:
13
- // - Browsers
14
- // - Node.js
15
- // - Electron
16
- // - Parcel
17
-
18
- if (typeof global !== "undefined") {
19
- // global already exists
20
- } else if (typeof window !== "undefined") {
21
- window.global = window;
22
- } else if (typeof self !== "undefined") {
23
- self.global = self;
24
- } else {
25
- throw new Error("cannot export Go (neither global, window nor self is defined)");
26
- }
27
-
28
- if (!global.require && typeof require !== "undefined") {
29
- global.require = require;
30
- }
31
-
32
- if (!global.fs && global.require) {
33
- global.fs = require("node:fs");
34
- }
5
+ "use strict";
35
6
 
7
+ (() => {
36
8
  const enosys = () => {
37
9
  const err = new Error("not implemented");
38
10
  err.code = "ENOSYS";
39
11
  return err;
40
12
  };
41
13
 
42
- if (!global.fs) {
14
+ if (!globalThis.fs) {
43
15
  let outputBuf = "";
44
- global.fs = {
45
- constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
16
+ globalThis.fs = {
17
+ constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
46
18
  writeSync(fd, buf) {
47
19
  outputBuf += decoder.decode(buf);
48
20
  const nl = outputBuf.lastIndexOf("\n");
49
21
  if (nl != -1) {
50
- console.log(outputBuf.substr(0, nl));
51
- outputBuf = outputBuf.substr(nl + 1);
22
+ console.log(outputBuf.substring(0, nl));
23
+ outputBuf = outputBuf.substring(nl + 1);
52
24
  }
53
25
  return buf.length;
54
26
  },
@@ -86,8 +58,8 @@
86
58
  };
87
59
  }
88
60
 
89
- if (!global.process) {
90
- global.process = {
61
+ if (!globalThis.process) {
62
+ globalThis.process = {
91
63
  getuid() { return -1; },
92
64
  getgid() { return -1; },
93
65
  geteuid() { return -1; },
@@ -101,53 +73,70 @@
101
73
  }
102
74
  }
103
75
 
104
- if (!global.crypto) {
105
- const nodeCrypto = require("node:crypto");
106
- global.crypto = {
107
- getRandomValues(b) {
108
- nodeCrypto.randomFillSync(b);
109
- },
110
- };
76
+ if (!globalThis.path) {
77
+ globalThis.path = {
78
+ resolve(...pathSegments) {
79
+ return pathSegments.join("/");
80
+ }
81
+ }
111
82
  }
112
83
 
113
- if (!global.performance) {
114
- global.performance = {
115
- now() {
116
- const [sec, nsec] = process.hrtime();
117
- return sec * 1000 + nsec / 1000000;
118
- },
119
- };
84
+ if (!globalThis.crypto) {
85
+ throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
120
86
  }
121
87
 
122
- if (!global.TextEncoder) {
123
- global.TextEncoder = require("node:util").TextEncoder;
88
+ if (!globalThis.performance) {
89
+ throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
124
90
  }
125
91
 
126
- if (!global.TextDecoder) {
127
- global.TextDecoder = require("node:util").TextDecoder;
92
+ if (!globalThis.TextEncoder) {
93
+ throw new Error("globalThis.TextEncoder is not available, polyfill required");
128
94
  }
129
95
 
130
- // End of polyfills for common API.
96
+ if (!globalThis.TextDecoder) {
97
+ throw new Error("globalThis.TextDecoder is not available, polyfill required");
98
+ }
131
99
 
132
100
  const encoder = new TextEncoder("utf-8");
133
101
  const decoder = new TextDecoder("utf-8");
134
- let reinterpretBuf = new DataView(new ArrayBuffer(8));
135
- var logLine = [];
136
- const wasmExit = {}; // thrown to exit via proc_exit (not an error)
137
102
 
138
- global.Go = class {
103
+ // Save any existing Go constructor so we can restore it after capturing our own.
104
+ // This prevents clobbering the global Go when other packages (e.g. sh-syntax)
105
+ // rely on their own wasm_exec.js version with different capabilities (like WASI support).
106
+ const __previousGo = globalThis.Go;
107
+ globalThis.Go = class {
139
108
  constructor() {
140
- this._callbackTimeouts = new Map();
109
+ this.argv = ["js"];
110
+ this.env = {};
111
+ this.exit = (code) => {
112
+ if (code !== 0) {
113
+ console.warn("exit code:", code);
114
+ }
115
+ };
116
+ this._exitPromise = new Promise((resolve) => {
117
+ this._resolveExitPromise = resolve;
118
+ });
119
+ this._pendingEvent = null;
120
+ this._scheduledTimeouts = new Map();
141
121
  this._nextCallbackTimeoutID = 1;
142
122
 
143
- const mem = () => {
144
- // The buffer may change when requesting more memory.
145
- return new DataView(this._inst.exports.memory.buffer);
123
+ const setInt64 = (addr, v) => {
124
+ this.mem.setUint32(addr + 0, v, true);
125
+ this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
146
126
  }
147
127
 
148
- const unboxValue = (v_ref) => {
149
- reinterpretBuf.setBigInt64(0, v_ref, true);
150
- const f = reinterpretBuf.getFloat64(0, true);
128
+ const setInt32 = (addr, v) => {
129
+ this.mem.setUint32(addr + 0, v, true);
130
+ }
131
+
132
+ const getInt64 = (addr) => {
133
+ const low = this.mem.getUint32(addr + 0, true);
134
+ const high = this.mem.getInt32(addr + 4, true);
135
+ return low + high * 4294967296;
136
+ }
137
+
138
+ const loadValue = (addr) => {
139
+ const f = this.mem.getFloat64(addr, true);
151
140
  if (f === 0) {
152
141
  return undefined;
153
142
  }
@@ -155,77 +144,69 @@
155
144
  return f;
156
145
  }
157
146
 
158
- const id = v_ref & 0xffffffffn;
147
+ const id = this.mem.getUint32(addr, true);
159
148
  return this._values[id];
160
149
  }
161
150
 
151
+ const storeValue = (addr, v) => {
152
+ const nanHead = 0x7FF80000;
162
153
 
163
- const loadValue = (addr) => {
164
- let v_ref = mem().getBigUint64(addr, true);
165
- return unboxValue(v_ref);
166
- }
167
-
168
- const boxValue = (v) => {
169
- const nanHead = 0x7FF80000n;
170
-
171
- if (typeof v === "number") {
154
+ if (typeof v === "number" && v !== 0) {
172
155
  if (isNaN(v)) {
173
- return nanHead << 32n;
156
+ this.mem.setUint32(addr + 4, nanHead, true);
157
+ this.mem.setUint32(addr, 0, true);
158
+ return;
174
159
  }
175
- if (v === 0) {
176
- return (nanHead << 32n) | 1n;
177
- }
178
- reinterpretBuf.setFloat64(0, v, true);
179
- return reinterpretBuf.getBigInt64(0, true);
160
+ this.mem.setFloat64(addr, v, true);
161
+ return;
180
162
  }
181
163
 
182
- switch (v) {
183
- case undefined:
184
- return 0n;
185
- case null:
186
- return (nanHead << 32n) | 2n;
187
- case true:
188
- return (nanHead << 32n) | 3n;
189
- case false:
190
- return (nanHead << 32n) | 4n;
164
+ if (v === undefined) {
165
+ this.mem.setFloat64(addr, 0, true);
166
+ return;
191
167
  }
192
168
 
193
169
  let id = this._ids.get(v);
194
170
  if (id === undefined) {
195
171
  id = this._idPool.pop();
196
172
  if (id === undefined) {
197
- id = BigInt(this._values.length);
173
+ id = this._values.length;
198
174
  }
199
175
  this._values[id] = v;
200
176
  this._goRefCounts[id] = 0;
201
177
  this._ids.set(v, id);
202
178
  }
203
179
  this._goRefCounts[id]++;
204
- let typeFlag = 1n;
180
+ let typeFlag = 0;
205
181
  switch (typeof v) {
182
+ case "object":
183
+ if (v !== null) {
184
+ typeFlag = 1;
185
+ }
186
+ break;
206
187
  case "string":
207
- typeFlag = 2n;
188
+ typeFlag = 2;
208
189
  break;
209
190
  case "symbol":
210
- typeFlag = 3n;
191
+ typeFlag = 3;
211
192
  break;
212
193
  case "function":
213
- typeFlag = 4n;
194
+ typeFlag = 4;
214
195
  break;
215
196
  }
216
- return id | ((nanHead | typeFlag) << 32n);
217
- }
218
-
219
- const storeValue = (addr, v) => {
220
- let v_ref = boxValue(v);
221
- mem().setBigUint64(addr, v_ref, true);
197
+ this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
198
+ this.mem.setUint32(addr, id, true);
222
199
  }
223
200
 
224
- const loadSlice = (array, len, cap) => {
225
- return new Uint8Array(this._inst.exports.memory.buffer, array, len);
201
+ const loadSlice = (addr) => {
202
+ const array = getInt64(addr + 0);
203
+ const len = getInt64(addr + 8);
204
+ return new Uint8Array(this._inst.exports.mem.buffer, array, len);
226
205
  }
227
206
 
228
- const loadSliceOfValues = (array, len, cap) => {
207
+ const loadSliceOfValues = (addr) => {
208
+ const array = getInt64(addr + 0);
209
+ const len = getInt64(addr + 8);
229
210
  const a = new Array(len);
230
211
  for (let i = 0; i < len; i++) {
231
212
  a[i] = loadValue(array + i * 8);
@@ -233,287 +214,353 @@
233
214
  return a;
234
215
  }
235
216
 
236
- const loadString = (ptr, len) => {
237
- return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
217
+ const loadString = (addr) => {
218
+ const saddr = getInt64(addr + 0);
219
+ const len = getInt64(addr + 8);
220
+ return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
221
+ }
222
+
223
+ const testCallExport = (a, b) => {
224
+ this._inst.exports.testExport0();
225
+ return this._inst.exports.testExport(a, b);
238
226
  }
239
227
 
240
228
  const timeOrigin = Date.now() - performance.now();
241
229
  this.importObject = {
242
- wasi_snapshot_preview1: {
243
- // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
244
- fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
245
- let nwritten = 0;
246
- if (fd == 1) {
247
- for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
248
- let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
249
- let ptr = mem().getUint32(iov_ptr + 0, true);
250
- let len = mem().getUint32(iov_ptr + 4, true);
251
- nwritten += len;
252
- for (let i=0; i<len; i++) {
253
- let c = mem().getUint8(ptr+i);
254
- if (c == 13) { // CR
255
- // ignore
256
- } else if (c == 10) { // LF
257
- // write line
258
- let line = decoder.decode(new Uint8Array(logLine));
259
- logLine = [];
260
- console.log(line);
261
- } else {
262
- logLine.push(c);
263
- }
264
- }
265
- }
266
- } else {
267
- console.error('invalid file descriptor:', fd);
268
- }
269
- mem().setUint32(nwritten_ptr, nwritten, true);
270
- return 0;
271
- },
272
- fd_close: () => 0, // dummy
273
- fd_fdstat_get: () => 0, // dummy
274
- fd_seek: () => 0, // dummy
275
- proc_exit: (code) => {
230
+ _gotest: {
231
+ add: (a, b) => a + b,
232
+ callExport: testCallExport,
233
+ },
234
+ gojs: {
235
+ // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
236
+ // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
237
+ // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
238
+ // This changes the SP, thus we have to update the SP used by the imported function.
239
+
240
+ // func wasmExit(code int32)
241
+ "runtime.wasmExit": (sp) => {
242
+ sp >>>= 0;
243
+ const code = this.mem.getInt32(sp + 8, true);
276
244
  this.exited = true;
277
- this.exitCode = code;
278
- this._resolveExitPromise();
279
- throw wasmExit;
245
+ delete this._inst;
246
+ delete this._values;
247
+ delete this._goRefCounts;
248
+ delete this._ids;
249
+ delete this._idPool;
250
+ this.exit(code);
280
251
  },
281
- random_get: (bufPtr, bufLen) => {
282
- crypto.getRandomValues(loadSlice(bufPtr, bufLen));
283
- return 0;
252
+
253
+ // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
254
+ "runtime.wasmWrite": (sp) => {
255
+ sp >>>= 0;
256
+ const fd = getInt64(sp + 8);
257
+ const p = getInt64(sp + 16);
258
+ const n = this.mem.getInt32(sp + 24, true);
259
+ fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
284
260
  },
285
- },
286
- gojs: {
287
- // func ticks() float64
288
- "runtime.ticks": () => {
289
- return timeOrigin + performance.now();
261
+
262
+ // func resetMemoryDataView()
263
+ "runtime.resetMemoryDataView": (sp) => {
264
+ sp >>>= 0;
265
+ this.mem = new DataView(this._inst.exports.mem.buffer);
266
+ },
267
+
268
+ // func nanotime1() int64
269
+ "runtime.nanotime1": (sp) => {
270
+ sp >>>= 0;
271
+ setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
272
+ },
273
+
274
+ // func walltime() (sec int64, nsec int32)
275
+ "runtime.walltime": (sp) => {
276
+ sp >>>= 0;
277
+ const msec = (new Date).getTime();
278
+ setInt64(sp + 8, msec / 1000);
279
+ this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
280
+ },
281
+
282
+ // func scheduleTimeoutEvent(delay int64) int32
283
+ "runtime.scheduleTimeoutEvent": (sp) => {
284
+ sp >>>= 0;
285
+ const id = this._nextCallbackTimeoutID;
286
+ this._nextCallbackTimeoutID++;
287
+ this._scheduledTimeouts.set(id, setTimeout(
288
+ () => {
289
+ this._resume();
290
+ while (this._scheduledTimeouts.has(id)) {
291
+ // for some reason Go failed to register the timeout event, log and try again
292
+ // (temporary workaround for https://github.com/golang/go/issues/28975)
293
+ console.warn("scheduleTimeoutEvent: missed timeout event");
294
+ this._resume();
295
+ }
296
+ },
297
+ getInt64(sp + 8),
298
+ ));
299
+ this.mem.setInt32(sp + 16, id, true);
300
+ },
301
+
302
+ // func clearTimeoutEvent(id int32)
303
+ "runtime.clearTimeoutEvent": (sp) => {
304
+ sp >>>= 0;
305
+ const id = this.mem.getInt32(sp + 8, true);
306
+ clearTimeout(this._scheduledTimeouts.get(id));
307
+ this._scheduledTimeouts.delete(id);
290
308
  },
291
309
 
292
- // func sleepTicks(timeout float64)
293
- "runtime.sleepTicks": (timeout) => {
294
- // Do not sleep, only reactivate scheduler after the given timeout.
295
- setTimeout(() => {
296
- if (this.exited) return;
297
- try {
298
- this._inst.exports.go_scheduler();
299
- } catch (e) {
300
- if (e !== wasmExit) throw e;
301
- }
302
- }, timeout);
310
+ // func getRandomData(r []byte)
311
+ "runtime.getRandomData": (sp) => {
312
+ sp >>>= 0;
313
+ crypto.getRandomValues(loadSlice(sp + 8));
303
314
  },
304
315
 
305
316
  // func finalizeRef(v ref)
306
- "syscall/js.finalizeRef": (v_ref) => {
307
- // Note: TinyGo does not support finalizers so this is only called
308
- // for one specific case, by js.go:jsString. and can/might leak memory.
309
- const id = v_ref & 0xffffffffn;
310
- if (this._goRefCounts?.[id] !== undefined) {
311
- this._goRefCounts[id]--;
312
- if (this._goRefCounts[id] === 0) {
313
- const v = this._values[id];
314
- this._values[id] = null;
315
- this._ids.delete(v);
316
- this._idPool.push(id);
317
- }
318
- } else {
319
- console.error("syscall/js.finalizeRef: unknown id", id);
317
+ "syscall/js.finalizeRef": (sp) => {
318
+ sp >>>= 0;
319
+ const id = this.mem.getUint32(sp + 8, true);
320
+ this._goRefCounts[id]--;
321
+ if (this._goRefCounts[id] === 0) {
322
+ const v = this._values[id];
323
+ this._values[id] = null;
324
+ this._ids.delete(v);
325
+ this._idPool.push(id);
320
326
  }
321
327
  },
322
328
 
323
329
  // func stringVal(value string) ref
324
- "syscall/js.stringVal": (value_ptr, value_len) => {
325
- value_ptr >>>= 0;
326
- const s = loadString(value_ptr, value_len);
327
- return boxValue(s);
330
+ "syscall/js.stringVal": (sp) => {
331
+ sp >>>= 0;
332
+ storeValue(sp + 24, loadString(sp + 8));
328
333
  },
329
334
 
330
335
  // func valueGet(v ref, p string) ref
331
- "syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
332
- let prop = loadString(p_ptr, p_len);
333
- let v = unboxValue(v_ref);
334
- let result = Reflect.get(v, prop);
335
- return boxValue(result);
336
+ "syscall/js.valueGet": (sp) => {
337
+ sp >>>= 0;
338
+ const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
339
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
340
+ storeValue(sp + 32, result);
336
341
  },
337
342
 
338
343
  // func valueSet(v ref, p string, x ref)
339
- "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
340
- const v = unboxValue(v_ref);
341
- const p = loadString(p_ptr, p_len);
342
- const x = unboxValue(x_ref);
343
- Reflect.set(v, p, x);
344
+ "syscall/js.valueSet": (sp) => {
345
+ sp >>>= 0;
346
+ Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
344
347
  },
345
348
 
346
349
  // func valueDelete(v ref, p string)
347
- "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
348
- const v = unboxValue(v_ref);
349
- const p = loadString(p_ptr, p_len);
350
- Reflect.deleteProperty(v, p);
350
+ "syscall/js.valueDelete": (sp) => {
351
+ sp >>>= 0;
352
+ Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
351
353
  },
352
354
 
353
355
  // func valueIndex(v ref, i int) ref
354
- "syscall/js.valueIndex": (v_ref, i) => {
355
- return boxValue(Reflect.get(unboxValue(v_ref), i));
356
+ "syscall/js.valueIndex": (sp) => {
357
+ sp >>>= 0;
358
+ storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
356
359
  },
357
360
 
358
361
  // valueSetIndex(v ref, i int, x ref)
359
- "syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
360
- Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
362
+ "syscall/js.valueSetIndex": (sp) => {
363
+ sp >>>= 0;
364
+ Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
361
365
  },
362
366
 
363
367
  // func valueCall(v ref, m string, args []ref) (ref, bool)
364
- "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
365
- const v = unboxValue(v_ref);
366
- const name = loadString(m_ptr, m_len);
367
- const args = loadSliceOfValues(args_ptr, args_len, args_cap);
368
+ "syscall/js.valueCall": (sp) => {
369
+ sp >>>= 0;
368
370
  try {
369
- const m = Reflect.get(v, name);
370
- storeValue(ret_addr, Reflect.apply(m, v, args));
371
- mem().setUint8(ret_addr + 8, 1);
371
+ const v = loadValue(sp + 8);
372
+ const m = Reflect.get(v, loadString(sp + 16));
373
+ const args = loadSliceOfValues(sp + 32);
374
+ const result = Reflect.apply(m, v, args);
375
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
376
+ storeValue(sp + 56, result);
377
+ this.mem.setUint8(sp + 64, 1);
372
378
  } catch (err) {
373
- storeValue(ret_addr, err);
374
- mem().setUint8(ret_addr + 8, 0);
379
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
380
+ storeValue(sp + 56, err);
381
+ this.mem.setUint8(sp + 64, 0);
375
382
  }
376
383
  },
377
384
 
378
385
  // func valueInvoke(v ref, args []ref) (ref, bool)
379
- "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
386
+ "syscall/js.valueInvoke": (sp) => {
387
+ sp >>>= 0;
380
388
  try {
381
- const v = unboxValue(v_ref);
382
- const args = loadSliceOfValues(args_ptr, args_len, args_cap);
383
- storeValue(ret_addr, Reflect.apply(v, undefined, args));
384
- mem().setUint8(ret_addr + 8, 1);
389
+ const v = loadValue(sp + 8);
390
+ const args = loadSliceOfValues(sp + 16);
391
+ const result = Reflect.apply(v, undefined, args);
392
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
393
+ storeValue(sp + 40, result);
394
+ this.mem.setUint8(sp + 48, 1);
385
395
  } catch (err) {
386
- storeValue(ret_addr, err);
387
- mem().setUint8(ret_addr + 8, 0);
396
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
397
+ storeValue(sp + 40, err);
398
+ this.mem.setUint8(sp + 48, 0);
388
399
  }
389
400
  },
390
401
 
391
402
  // func valueNew(v ref, args []ref) (ref, bool)
392
- "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
393
- const v = unboxValue(v_ref);
394
- const args = loadSliceOfValues(args_ptr, args_len, args_cap);
403
+ "syscall/js.valueNew": (sp) => {
404
+ sp >>>= 0;
395
405
  try {
396
- storeValue(ret_addr, Reflect.construct(v, args));
397
- mem().setUint8(ret_addr + 8, 1);
406
+ const v = loadValue(sp + 8);
407
+ const args = loadSliceOfValues(sp + 16);
408
+ const result = Reflect.construct(v, args);
409
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
410
+ storeValue(sp + 40, result);
411
+ this.mem.setUint8(sp + 48, 1);
398
412
  } catch (err) {
399
- storeValue(ret_addr, err);
400
- mem().setUint8(ret_addr+ 8, 0);
413
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
414
+ storeValue(sp + 40, err);
415
+ this.mem.setUint8(sp + 48, 0);
401
416
  }
402
417
  },
403
418
 
404
419
  // func valueLength(v ref) int
405
- "syscall/js.valueLength": (v_ref) => {
406
- return unboxValue(v_ref).length;
420
+ "syscall/js.valueLength": (sp) => {
421
+ sp >>>= 0;
422
+ setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
407
423
  },
408
424
 
409
425
  // valuePrepareString(v ref) (ref, int)
410
- "syscall/js.valuePrepareString": (ret_addr, v_ref) => {
411
- const s = String(unboxValue(v_ref));
412
- const str = encoder.encode(s);
413
- storeValue(ret_addr, str);
414
- mem().setInt32(ret_addr + 8, str.length, true);
426
+ "syscall/js.valuePrepareString": (sp) => {
427
+ sp >>>= 0;
428
+ const str = encoder.encode(String(loadValue(sp + 8)));
429
+ storeValue(sp + 16, str);
430
+ setInt64(sp + 24, str.length);
415
431
  },
416
432
 
417
433
  // valueLoadString(v ref, b []byte)
418
- "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
419
- const str = unboxValue(v_ref);
420
- loadSlice(slice_ptr, slice_len, slice_cap).set(str);
434
+ "syscall/js.valueLoadString": (sp) => {
435
+ sp >>>= 0;
436
+ const str = loadValue(sp + 8);
437
+ loadSlice(sp + 16).set(str);
421
438
  },
422
439
 
423
440
  // func valueInstanceOf(v ref, t ref) bool
424
- "syscall/js.valueInstanceOf": (v_ref, t_ref) => {
425
- return unboxValue(v_ref) instanceof unboxValue(t_ref);
441
+ "syscall/js.valueInstanceOf": (sp) => {
442
+ sp >>>= 0;
443
+ this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
426
444
  },
427
445
 
428
446
  // func copyBytesToGo(dst []byte, src ref) (int, bool)
429
- "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
430
- let num_bytes_copied_addr = ret_addr;
431
- let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
432
-
433
- const dst = loadSlice(dest_addr, dest_len);
434
- const src = unboxValue(src_ref);
447
+ "syscall/js.copyBytesToGo": (sp) => {
448
+ sp >>>= 0;
449
+ const dst = loadSlice(sp + 8);
450
+ const src = loadValue(sp + 32);
435
451
  if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
436
- mem().setUint8(returned_status_addr, 0); // Return "not ok" status
452
+ this.mem.setUint8(sp + 48, 0);
437
453
  return;
438
454
  }
439
455
  const toCopy = src.subarray(0, dst.length);
440
456
  dst.set(toCopy);
441
- mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
442
- mem().setUint8(returned_status_addr, 1); // Return "ok" status
457
+ setInt64(sp + 40, toCopy.length);
458
+ this.mem.setUint8(sp + 48, 1);
443
459
  },
444
460
 
445
- // copyBytesToJS(dst ref, src []byte) (int, bool)
446
- // Originally copied from upstream Go project, then modified:
447
- // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
448
- "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
449
- let num_bytes_copied_addr = ret_addr;
450
- let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
451
-
452
- const dst = unboxValue(dst_ref);
453
- const src = loadSlice(src_addr, src_len);
461
+ // func copyBytesToJS(dst ref, src []byte) (int, bool)
462
+ "syscall/js.copyBytesToJS": (sp) => {
463
+ sp >>>= 0;
464
+ const dst = loadValue(sp + 8);
465
+ const src = loadSlice(sp + 16);
454
466
  if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
455
- mem().setUint8(returned_status_addr, 0); // Return "not ok" status
467
+ this.mem.setUint8(sp + 48, 0);
456
468
  return;
457
469
  }
458
470
  const toCopy = src.subarray(0, dst.length);
459
471
  dst.set(toCopy);
460
- mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
461
- mem().setUint8(returned_status_addr, 1); // Return "ok" status
472
+ setInt64(sp + 40, toCopy.length);
473
+ this.mem.setUint8(sp + 48, 1);
474
+ },
475
+
476
+ "debug": (value) => {
477
+ console.log(value);
462
478
  },
463
479
  }
464
480
  };
465
-
466
- // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
467
- // For compatibility, we use both as long as Go 1.20 is supported.
468
- this.importObject.env = this.importObject.gojs;
469
481
  }
470
482
 
471
483
  async run(instance) {
484
+ if (!(instance instanceof WebAssembly.Instance)) {
485
+ throw new Error("Go.run: WebAssembly.Instance expected");
486
+ }
472
487
  this._inst = instance;
488
+ this.mem = new DataView(this._inst.exports.mem.buffer);
473
489
  this._values = [ // JS values that Go currently has references to, indexed by reference id
474
490
  NaN,
475
491
  0,
476
492
  null,
477
493
  true,
478
494
  false,
479
- global,
495
+ globalThis,
480
496
  this,
481
497
  ];
482
- this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
483
- this._ids = new Map(); // mapping from JS values to reference ids
484
- this._idPool = []; // unused ids that have been garbage collected
485
- this.exited = false; // whether the Go program has exited
486
- this.exitCode = 0;
487
-
488
- if (this._inst.exports._start) {
489
- let exitPromise = new Promise((resolve, reject) => {
490
- this._resolveExitPromise = resolve;
491
- });
492
-
493
- // Run program, but catch the wasmExit exception that's thrown
494
- // to return back here.
495
- try {
496
- this._inst.exports._start();
497
- } catch (e) {
498
- if (e !== wasmExit) throw e;
498
+ this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
499
+ this._ids = new Map([ // mapping from JS values to reference ids
500
+ [0, 1],
501
+ [null, 2],
502
+ [true, 3],
503
+ [false, 4],
504
+ [globalThis, 5],
505
+ [this, 6],
506
+ ]);
507
+ this._idPool = []; // unused ids that have been garbage collected
508
+ this.exited = false; // whether the Go program has exited
509
+
510
+ // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
511
+ let offset = 4096;
512
+
513
+ const strPtr = (str) => {
514
+ const ptr = offset;
515
+ const bytes = encoder.encode(str + "\0");
516
+ new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
517
+ offset += bytes.length;
518
+ if (offset % 8 !== 0) {
519
+ offset += 8 - (offset % 8);
499
520
  }
521
+ return ptr;
522
+ };
523
+
524
+ const argc = this.argv.length;
525
+
526
+ const argvPtrs = [];
527
+ this.argv.forEach((arg) => {
528
+ argvPtrs.push(strPtr(arg));
529
+ });
530
+ argvPtrs.push(0);
531
+
532
+ const keys = Object.keys(this.env).sort();
533
+ keys.forEach((key) => {
534
+ argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
535
+ });
536
+ argvPtrs.push(0);
537
+
538
+ const argv = offset;
539
+ argvPtrs.forEach((ptr) => {
540
+ this.mem.setUint32(offset, ptr, true);
541
+ this.mem.setUint32(offset + 4, 0, true);
542
+ offset += 8;
543
+ });
544
+
545
+ // The linker guarantees global data starts from at least wasmMinDataAddr.
546
+ // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
547
+ const wasmMinDataAddr = 4096 + 8192;
548
+ if (offset >= wasmMinDataAddr) {
549
+ throw new Error("total length of command line and environment variables exceeds limit");
550
+ }
500
551
 
501
- await exitPromise;
502
- return this.exitCode;
503
- } else {
504
- this._inst.exports._initialize();
552
+ this._inst.exports.run(argc, argv);
553
+ if (this.exited) {
554
+ this._resolveExitPromise();
505
555
  }
556
+ await this._exitPromise;
506
557
  }
507
558
 
508
559
  _resume() {
509
560
  if (this.exited) {
510
561
  throw new Error("Go program has already exited");
511
562
  }
512
- try {
513
- this._inst.exports.resume();
514
- } catch (e) {
515
- if (e !== wasmExit) throw e;
516
- }
563
+ this._inst.exports.resume();
517
564
  if (this.exited) {
518
565
  this._resolveExitPromise();
519
566
  }
@@ -529,4 +576,12 @@
529
576
  };
530
577
  }
531
578
  }
579
+ // Stash our Go class under a namespaced global and restore the previous one,
580
+ // so we don't break other Go WASM packages sharing the same global scope.
581
+ globalThis.__dockerfmt_Go = globalThis.Go;
582
+ if (__previousGo) {
583
+ globalThis.Go = __previousGo;
584
+ } else {
585
+ delete globalThis.Go;
586
+ }
532
587
  })();
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@reteps/dockerfmt",
3
- "version": "0.3.6",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "",
6
- "repository": "git+https://github.com/reteps/dockerfmt/tree/main/js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/reteps/dockerfmt.git",
9
+ "directory": "js"
10
+ },
7
11
  "author": "Peter Stenger <pete@stenger.io>",
8
12
  "license": "MIT",
9
13
  "engines": {
@@ -29,9 +33,8 @@
29
33
  "dist"
30
34
  ],
31
35
  "scripts": {
32
- "//": "Requires tinygo 0.38.0 or later",
33
36
  "build": "npm run build-go && npm run build-js",
34
- "build-go": "tinygo build -o format.wasm -target wasm --no-debug",
37
+ "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",
35
38
  "build-js": "tsc && cp format.wasm wasm_exec.js dist",
36
39
  "format": "prettier --write \"**/*.{js,ts,json}\""
37
40
  },