Skip to content

Commit

Permalink
Merge pull request #2280 from alixander/npm-fix
Browse files Browse the repository at this point in the history
separate node esm and browser esm builds
  • Loading branch information
alixander authored Jan 13, 2025
2 parents 7e3ae79 + 44497ad commit f615775
Show file tree
Hide file tree
Showing 15 changed files with 196 additions and 79 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ test: fmt
race: fmt
prefix "$@" ./ci/test.sh --race ./...
.PHONY: js
js:
cd d2js/js && prefix "$@" ./make.sh
js: gen
cd d2js/js && prefix "$@" ./make.sh all
1 change: 1 addition & 0 deletions d2js/js/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules
.npm
bun.lockb

src/wasm-loader.browser.js
wasm/d2.wasm
dist/

Expand Down
8 changes: 6 additions & 2 deletions d2js/js/Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
.POSIX:
.PHONY: all
all: fmt build test
all: fmt build test cleanup

.PHONY: fmt
fmt: node_modules
prefix "$@" ../../ci/sub/bin/fmt.sh
prefix "$@" rm -f yarn.lock

.PHONY: build
build: node_modules
build: fmt
prefix "$@" ./ci/build.sh

.PHONY: test
Expand All @@ -18,3 +18,7 @@ test: build
.PHONY: node_modules
node_modules:
prefix "$@" bun install $${CI:+--frozen-lockfile}

.PHONY: cleanup
cleanup: test
prefix "$@" git checkout -- src/platform.js
115 changes: 95 additions & 20 deletions d2js/js/build.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,110 @@
import { build } from "bun";
import { copyFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { copyFile, mkdir, writeFile, readFile, rm } from "node:fs/promises";
import { join, resolve } from "node:path";

await mkdir("./dist/esm", { recursive: true });
await mkdir("./dist/cjs", { recursive: true });
const __dirname = new URL(".", import.meta.url).pathname;
const ROOT_DIR = resolve(__dirname);
const SRC_DIR = resolve(ROOT_DIR, "src");

await rm("./dist", { recursive: true, force: true });
await mkdir("./dist/browser", { recursive: true });
await mkdir("./dist/node-esm", { recursive: true });
await mkdir("./dist/node-cjs", { recursive: true });

const wasmBinary = await readFile("./wasm/d2.wasm");
const wasmExecJs = await readFile("./wasm/wasm_exec.js", "utf8");

await writeFile(
join(SRC_DIR, "wasm-loader.browser.js"),
`export const wasmBinary = Uint8Array.from(atob("${Buffer.from(wasmBinary).toString(
"base64"
)}"), c => c.charCodeAt(0));
export const wasmExecJs = ${JSON.stringify(wasmExecJs)};`
);

const commonConfig = {
target: "node",
splitting: false,
sourcemap: "external",
minify: true,
naming: {
entry: "[dir]/[name].js",
chunk: "[name]-[hash].js",
asset: "[name]-[hash][ext]",
},
};

async function buildAndCopy(format) {
const outdir = `./dist/${format}`;
async function buildPlatformFile(platform) {
const platformContent =
platform === "node"
? `export * from "./platform.node.js";`
: `export * from "./platform.browser.js";`;

const platformPath = join(SRC_DIR, "platform.js");
await writeFile(platformPath, platformContent);
}

await build({
async function buildAndCopy(buildType) {
const configs = {
browser: {
outdir: resolve(ROOT_DIR, "dist/browser"),
format: "esm",
target: "browser",
platform: "browser",
loader: {
".js": "jsx",
},
entrypoints: [
resolve(SRC_DIR, "index.js"),
resolve(SRC_DIR, "worker.js"),
resolve(SRC_DIR, "platform.js"),
resolve(SRC_DIR, "wasm-loader.browser.js"),
],
},
"node-esm": {
outdir: resolve(ROOT_DIR, "dist/node-esm"),
format: "esm",
target: "node",
platform: "node",
entrypoints: [
resolve(SRC_DIR, "index.js"),
resolve(SRC_DIR, "worker.js"),
resolve(SRC_DIR, "platform.js"),
],
},
"node-cjs": {
outdir: resolve(ROOT_DIR, "dist/node-cjs"),
format: "cjs",
target: "node",
platform: "node",
entrypoints: [
resolve(SRC_DIR, "index.js"),
resolve(SRC_DIR, "worker.js"),
resolve(SRC_DIR, "platform.js"),
],
},
};

const config = configs[buildType];
await buildPlatformFile(config.platform);

const result = await build({
...commonConfig,
entrypoints: ["./src/index.js", "./src/worker.js", "./src/platform.js"],
outdir,
format,
...config,
});

await copyFile("./wasm/d2.wasm", join(outdir, "d2.wasm"));
await copyFile("./wasm/wasm_exec.js", join(outdir, "wasm_exec.js"));
if (!result.outputs || result.outputs.length === 0) {
throw new Error(`No outputs generated for ${buildType} build`);
}

if (buildType !== "browser") {
await copyFile(resolve(ROOT_DIR, "wasm/d2.wasm"), join(config.outdir, "d2.wasm"));
await copyFile(
resolve(ROOT_DIR, "wasm/wasm_exec.js"),
join(config.outdir, "wasm_exec.js")
);
}
}

await buildAndCopy("esm");
await buildAndCopy("cjs");
try {
await buildAndCopy("browser");
await buildAndCopy("node-esm");
await buildAndCopy("node-cjs");
} catch (error) {
console.error("Build failed:", error);
process.exit(1);
}
15 changes: 7 additions & 8 deletions d2js/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"name": "@terrastruct/d2",
"author": "Terrastruct, Inc.",
"description": "D2.js is a wrapper around the WASM build of D2, the modern text-to-diagram language.",
"version": "0.1.0",
"version": "0.1.11",
"repository": {
"type": "git",
"url": "https://github.com/terrastruct/d2.git",
"url": "git+https://github.com/terrastruct/d2.git",
"directory": "d2js/js"
},
"bugs": {
Expand All @@ -20,8 +20,10 @@
"module": "./dist/esm/index.js",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
"browser": "./dist/browser/index.js",
"import": "./dist/node-esm/index.js",
"require": "./dist/node-cjs/index.js",
"default": "./dist/node-esm/index.js"
}
},
"files": [
Expand All @@ -33,7 +35,7 @@
"test:integration": "bun test test/integration",
"test:all": "bun run test && bun run test:integration",
"dev": "bun --watch dev-server.js",
"prepublishOnly": "./make.sh"
"prepublishOnly": "./make.sh all"
},
"keywords": [
"d2",
Expand All @@ -43,9 +45,6 @@
"text-to-diagram",
"go"
],
"engines": {
"bun": ">=1.0.0"
},
"license": "MPL-2.0",
"devDependencies": {
"bun": "latest"
Expand Down
2 changes: 1 addition & 1 deletion d2js/js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class D2 {

if (isNode) {
this.worker.on("error", (error) => {
console.error("Worker encountered an error:", error.message || error);
console.error("Worker (node) encountered an error:", error.message || error);
});
} else {
this.worker.onerror = (error) => {
Expand Down
28 changes: 28 additions & 0 deletions d2js/js/src/platform.browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { wasmBinary, wasmExecJs } from "./wasm-loader.browser.js";

export async function loadFile(path) {
if (path === "./d2.wasm") {
return wasmBinary.buffer;
}
if (path === "./wasm_exec.js") {
return new TextEncoder().encode(wasmExecJs).buffer;
}
throw new Error(`Unexpected file request: ${path}`);
}

export async function createWorker() {
// Combine wasmExecJs with worker script
const workerResponse = await fetch(new URL("./worker.js", import.meta.url));
if (!workerResponse.ok) {
throw new Error(
`Failed to load worker.js: ${workerResponse.status} ${workerResponse.statusText}`
);
}
const workerJs = await workerResponse.text();

const blob = new Blob(["(() => {", wasmExecJs, "})();", workerJs], {
type: "application/javascript",
});

return new Worker(URL.createObjectURL(blob));
}
38 changes: 1 addition & 37 deletions d2js/js/src/platform.js
Original file line number Diff line number Diff line change
@@ -1,37 +1 @@
export async function loadFile(path) {
if (typeof window === "undefined") {
const fs = await import("node:fs/promises");
const { fileURLToPath } = await import("node:url");
const { join, dirname } = await import("node:path");
const __dirname = dirname(fileURLToPath(import.meta.url));

try {
return await fs.readFile(join(__dirname, path));
} catch (err) {
if (err.code === "ENOENT") {
return await fs.readFile(join(__dirname, "../wasm", path.replace("./", "")));
}
throw err;
}
}
try {
const response = await fetch(new URL(path, import.meta.url));
return await response.arrayBuffer();
} catch {
const response = await fetch(
new URL(`../wasm/${path.replace("./", "")}`, import.meta.url)
);
return await response.arrayBuffer();
}
}

export async function createWorker() {
if (typeof window === "undefined") {
const { Worker } = await import("node:worker_threads");
const { fileURLToPath } = await import("node:url");
const { join, dirname } = await import("node:path");
const __dirname = dirname(fileURLToPath(import.meta.url));
return new Worker(join(__dirname, "worker.js"));
}
return new window.Worker(new URL("./worker.js", import.meta.url));
}
export * from "./platform.node.js";
40 changes: 40 additions & 0 deletions d2js/js/src/platform.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
let nodeModules = null;

async function loadNodeModules() {
if (!nodeModules) {
nodeModules = {
fs: await import("fs/promises"),
path: await import("path"),
url: await import("url"),
worker: await import("worker_threads"),
};
}
return nodeModules;
}

export async function loadFile(path) {
const modules = await loadNodeModules();
const readFile = modules.fs.readFile;
const { join, dirname } = modules.path;
const { fileURLToPath } = modules.url;
const __dirname = dirname(fileURLToPath(import.meta.url));

try {
return await readFile(join(__dirname, path));
} catch (err) {
if (err.code === "ENOENT") {
return await readFile(join(__dirname, "../../../wasm", path.replace("./", "")));
}
throw err;
}
}

export async function createWorker() {
const modules = await loadNodeModules();
const { Worker } = modules.worker;
const { join, dirname } = modules.path;
const { fileURLToPath } = modules.url;
const __dirname = dirname(fileURLToPath(import.meta.url));
const workerPath = join(__dirname, "worker.js");
return new Worker(workerPath);
}
8 changes: 8 additions & 0 deletions d2js/js/src/wasm-loader.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { readFile } from "fs/promises";
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";

const __dirname = dirname(fileURLToPath(import.meta.url));
export async function getWasmBinary() {
return readFile(resolve(__dirname, "./d2.wasm"));
}
2 changes: 0 additions & 2 deletions d2js/js/src/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ async function handleMessage(e) {
try {
if (isNode) {
eval(data.wasmExecContent);
} else {
importScripts(data.wasmExecUrl);
}
d2 = await initWasm(data.wasm);
currentPort.postMessage({ type: "ready" });
Expand Down
2 changes: 1 addition & 1 deletion d2js/js/test/integration/cjs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test, describe } from "bun:test";

describe("D2 CJS Integration", () => {
test("can require and use CJS build", async () => {
const { D2 } = require("../../dist/cjs/index.js");
const { D2 } = require("../../dist/node-cjs/index.js");
const d2 = new D2();
const result = await d2.compile("x -> y");
expect(result.diagram).toBeDefined();
Expand Down
2 changes: 1 addition & 1 deletion d2js/js/test/integration/esm.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test, describe } from "bun:test";
import { D2 } from "../../dist/esm/index.js";
import { D2 } from "../../dist/node-esm/index.js";

describe("D2 ESM Integration", () => {
test("can import and use ESM build", async () => {
Expand Down
2 changes: 1 addition & 1 deletion d2js/js/test/unit/basic.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test, describe } from "bun:test";
import { D2 } from "../../src/index.js";
import { D2 } from "../../dist/node-esm/index.js";

describe("D2 Unit Tests", () => {
test("basic compilation works", async () => {
Expand Down
8 changes: 4 additions & 4 deletions make.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ if ! go version | grep -q '1.2[0-9]'; then
exit 1
fi

if [ "${CI:-}" ]; then
export FORCE_COLOR=1
npx [email protected] install --with-deps chromium
fi
# if [ "${CI:-}" ]; then
# export FORCE_COLOR=1
# npx [email protected] install --with-deps chromium
# fi
_make "$@"

0 comments on commit f615775

Please sign in to comment.