diff --git a/.gitignore b/.gitignore index 0cb4c6c0..e9666e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules/ debug/ dist/ +/sync/ /wasm /worker diff --git a/CHANGELOG.md b/CHANGELOG.md index 478b0b41..28216981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,16 @@ - The library is able to reset its internal error state, which makes the [v2 wiki caveat](https://github.com/mdaines/viz.js/wiki/Caveats#rendering-graphs-with-user-input) unnecessary. -- Rendering from main thread is no longer supported, you must use a worker - (webworker or worker_thread). +- Rendering from main thread is no longer supported on the default async API, + you must use a worker (webworker or worker_thread). - The JS code is now transpiled from TypeScript, and typings are packed within the npm package. You can find the API documentation there! +- There is a synchronous version available for legacy Node.js support. ##### Breaking changes and deprecations - **BREAKING:** Bump required version of Node.js to v12 LTS (might work on v10 - LTS using CLI flags). + LTS using CLI flags or the synchronous API). - **BREAKING:** Remove `Viz.prototype.renderSVGElement`. You can use `renderString` and `DOMParser` to achieve the same result. - **BREAKING:** Remove `Viz.prototype.renderImageElement`. You can use @@ -40,7 +41,9 @@ `package.json`#`exports`, you can use the specifier `@aduh95/viz.js/worker`. - **BREAKING:** Compiles to WebAssembly, which cannot be bundled in the `render.js` file like asm.js used to. Depending on your bundling tool, you may - need some extra config to make everything work. + need some extra config to make everything work. You might also use the + synchronous API, which bundles the asm.js code, although its usage should be + strictly limited to Node.js or webworker use. - **BREAKING:** Remove ES5 and CJS dist files, all modern browsers now support ES2015 modules. If you want to support an older browser, you would need to transpile it yourself or use an older version. @@ -52,7 +55,7 @@ ##### Added features - Add support for Node.js `worker_threads`. -- Refactor JS files to Typescript. +- Refactor JS files to TypeScript. - Refactor `viz.c` to C++ to use [Emscripten's Embind](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html). - Use `ALLOW_MEMORY_GROW` compiler option to avoid failing on large graphs. @@ -61,6 +64,7 @@ - Remove the need of creating new instances when render fails by resetting internal error state. - Switch to Mocha and Puppeteer for browser testing. +- Add synchronous API using asm.js. - Upgrade deps: - Upgrade Emscripten to 1.39.12 - Upgrade Graphviz to 2.44.0 diff --git a/Makefile b/Makefile index 1626c6f6..46d2b894 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,9 @@ all: \ dist \ dist/index.cjs dist/index.mjs dist/index.d.ts dist/types.d.ts \ dist/render.node.mjs dist/render.browser.js dist/render.wasm \ + dist/renderSync.js \ + sync \ + sync/index.js sync/index.d.ts \ wasm \ worker \ @@ -142,7 +145,7 @@ deps: expat-full graphviz-full $(YARN_PATH) .NOTPARALLEL: clean clean: @echo "\033[1;33mHint: use \033[1;32mmake clobber\033[1;33m to start from a clean slate\033[0m" >&2 - rm -rf build dist + rm -rf build dist sync rm -f wasm worker rm -f test/deno-files/render.wasm.uint8.js test/deno-files/index.d.ts @@ -150,9 +153,19 @@ clean: clobber: | clean rm -rf build build-full $(PREFIX_FULL) $(PREFIX_LITE) $(YARN_DIR) node_modules +sync/index.js: | sync + echo "module.exports=require('../dist/renderSync.js')" > $@ +sync/index.d.ts: dist/renderSync.d.ts | sync + echo 'export {default} from "../dist/renderSync"' > $@ +dist/renderSync.d.ts: src/renderSync.ts | dist + $(TSC) $(TS_FLAGS) --outDir $(DIST_FOLDER) -d --emitDeclarationOnly $< + wasm worker: echo "throw new Error('The bundler you are using does not support package.json#exports.')" > $@ +build/renderFunction.js: src/renderFunction.ts | build + $(TSC) $(TS_FLAGS) --outDir build -m es6 --target esnext $< + build/worker.js: src/worker.ts | build $(TSC) $(TS_FLAGS) --outDir build -m es6 --target esnext $< @@ -222,12 +235,22 @@ build/render.js: src/viz.cpp | build $(CC) --version | grep $(EMSCRIPTEN_VERSION) $(CC) $(CC_FLAGS) -Oz -o $@ $< $(CC_INCLUDES) +build/asm.mjs: src/viz.cpp | build + $(CC) --version | grep $(EMSCRIPTEN_VERSION) + $(CC) $(CC_FLAGS) -s WASM=0 -s WASM_ASYNC_COMPILATION=0 --memory-init-file 0 -Oz -o $@ $< $(CC_INCLUDES) + +build/renderSync.js: src/renderSync.ts | build + $(TSC) $(TS_FLAGS) --outDir build -m es6 --target esnext $< + +dist/renderSync.js: build/renderSync.js build/asm.mjs build/renderFunction.js + $(ROLLUP) -f commonjs $< | $(TERSER) --toplevel > $@ + test/deno-files/render.wasm.arraybuffer.js: dist/render.wasm echo "export default Uint16Array.from([" > $@ && \ hexdump -v -x $< | awk '$$1=" "' OFS=",0x" >> $@ && \ echo "]).buffer.slice(2$(shell stat -f%z $< | awk '{if (int($$1) % 2) print ",-1"}'))" >> $@ -$(PREFIX_FULL) build dist sources $(YARN_DIR): +$(PREFIX_FULL) build dist sources sync $(YARN_DIR): mkdir -p $@ .PHONY: expat–full diff --git a/README.md b/README.md index ed56b384..173221a2 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,29 @@ async function dot2svg(dot, options = {}) { } ``` +#### Synchronous API + +There is a synchronous version of `renderString` method available: + +```js +const vizRenderStringSync = require("@aduh95/viz.js/sync"); + +console.log(vizRenderStringSync("digraph{1 -> 2 }")); +``` + +Key differences with async API: + +- It compiles Graphviz to JavaScript instead of `WebAssembly`, this should come + with a performance hit and a bigger bundled file size (brotli size is 27% + bigger). +- It is a CommonJS module, while the rest of the API is written as standard + ECMAScript modules. The upside is this syntax is supported on a wider Node.js + version array. + +> Note: Using the sync API on the browser main thread is not recommended, it +> might degrade the overall user experience of the web page. It is strongly +> recommended to use web workers – with the sync or the async API. + ### Browsers You can either use the `worker` or the `workerURL` on the constructor. Note that diff --git a/package.json b/package.json index 9aa4b04e..531abafa 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "require": "./dist/index.cjs", "import": "./dist/index.mjs" }, + "./sync": "./dist/renderSync.js", "./wasm": "./dist/render.wasm", "./worker": { "import": "./dist/render.node.mjs", @@ -31,6 +32,7 @@ ], "files": [ "dist/", + "sync/", "wasm", "worker" ], diff --git a/src/asm.mjs.d.ts b/src/asm.mjs.d.ts new file mode 100644 index 00000000..fd4d05b8 --- /dev/null +++ b/src/asm.mjs.d.ts @@ -0,0 +1,3 @@ +import type { WebAssemblyModule } from "./render"; + +export default function (): WebAssemblyModule; diff --git a/src/asm.mjs.js b/src/asm.mjs.js new file mode 100644 index 00000000..90fb2ae6 --- /dev/null +++ b/src/asm.mjs.js @@ -0,0 +1 @@ +// Dummy file for tsc diff --git a/src/renderFunction.ts b/src/renderFunction.ts new file mode 100644 index 00000000..17a362f3 --- /dev/null +++ b/src/renderFunction.ts @@ -0,0 +1,29 @@ +import type { RenderOptions } from "./types"; +import type { WebAssemblyModule } from "./render"; + +export default function render( + Module: WebAssemblyModule, + src: string, + options: RenderOptions +): string { + for (const { path, data } of options.files) { + Module.vizCreateFile(path, data); + } + + Module.vizSetY_invert(options.yInvert ? 1 : 0); + Module.vizSetNop(options.nop || 0); + + const resultString = Module.vizRenderFromString( + src, + options.format, + options.engine + ); + + const errorMessageString = Module.vizLastErrorMessage(); + + if (errorMessageString !== "") { + throw new Error(errorMessageString); + } + + return resultString; +} diff --git a/src/renderSync.ts b/src/renderSync.ts new file mode 100644 index 00000000..dec7e74c --- /dev/null +++ b/src/renderSync.ts @@ -0,0 +1,23 @@ +import Module from "./asm.mjs"; +import render from "./renderFunction.js"; + +import type { RenderOptions } from "./types"; + +let asmModule; +export default function renderStringSync( + src: string, + options?: RenderOptions +): string { + if (asmModule == null) { + asmModule = Module(); + } + return render(asmModule, src, { + format: "svg", + engine: "dot", + files: [], + images: [], + yInvert: false, + nop: 0, + ...(options || {}), + }); +} diff --git a/src/worker.ts b/src/worker.ts index 5c0b6de0..ca6549a6 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,15 +1,11 @@ -import type { - RenderOptions, - SerializedError, - RenderResponse, - RenderRequest, -} from "./types"; +import type { SerializedError, RenderResponse, RenderRequest } from "./types"; import type { Worker } from "worker_threads"; import initializeWasm, { WebAssemblyModule, EMCCModuleOverrides, } from "./render"; +import render from "./renderFunction.js"; /* eslint-disable no-var */ // @@ -38,33 +34,6 @@ async function getModule(): Promise { return Module; } -function render( - Module: WebAssemblyModule, - src: string, - options: RenderOptions -): string { - for (const { path, data } of options.files) { - Module.vizCreateFile(path, data); - } - - Module.vizSetY_invert(options.yInvert ? 1 : 0); - Module.vizSetNop(options.nop || 0); - - const resultString = Module.vizRenderFromString( - src, - options.format, - options.engine - ); - - const errorMessageString = Module.vizLastErrorMessage(); - - if (errorMessageString !== "") { - throw new Error(errorMessageString); - } - - return resultString; -} - export function onmessage(event: MessageEvent): Promise { const { id, src, options } = event.data as RenderRequest; diff --git a/test/integration.ts b/test/integration.ts index d234d4b0..b5b0c89b 100644 --- a/test/integration.ts +++ b/test/integration.ts @@ -4,6 +4,7 @@ */ import Viz from "@aduh95/viz.js"; +import vizRenderStringSync from "@aduh95/viz.js/sync"; // @ts-expect-error Viz({ workerURL: "string" }); @@ -50,3 +51,17 @@ viz.terminateWorker(); // @ts-expect-error viz.terminateWorker("argument"); + +// @ts-expect-error +vizRenderStringSync(); + +vizRenderStringSync("string"); +// @ts-expect-error +vizRenderStringSync("string").then(() => {}); +vizRenderStringSync("string").replace("string", ""); +vizRenderStringSync("string", {}); +vizRenderStringSync("string", { format: "dot" }); +// @ts-expect-error +vizRenderStringSync("string", { format: "unknown" }); +// @ts-expect-error +vizRenderStringSync("string", { unknown: "unknown" }); diff --git a/test/node.js b/test/node.js index 2bedde2e..337b01c4 100644 --- a/test/node.js +++ b/test/node.js @@ -34,4 +34,21 @@ describe("Test graph rendering using Node.js", function () { .then((result) => assert.ok(result)) .finally(() => viz.terminateWorker()); }); + + it("should render a graph using sync version", function () { + const renderStringSync = require("@aduh95/viz.js/sync"); + + assert.ok(renderStringSync("digraph { a -> b; }")); + }); + + it("should render same graph using async and sync versions", async function () { + const viz = await getViz(); + const renderStringSync = require("@aduh95/viz.js/sync"); + + const resultSync = renderStringSync("digraph { a -> b; }"); + return viz + .renderString("digraph { a -> b; }") + .then((result) => assert.strictEqual(result, resultSync)) + .finally(() => viz.terminateWorker()); + }); });