Browse Source

Pure node rendering (#443)

Christopher Chedeau 5 years ago
parent
commit
7f6e1f420e

+ 438 - 0
package-lock.json

@@ -3607,6 +3607,12 @@
         "safe-buffer": "^5.0.1"
       }
     },
+    "circular-json": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
+      "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==",
+      "dev": true
+    },
     "class-utils": {
       "version": "0.3.6",
       "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -10903,6 +10909,12 @@
         "semver-compare": "^1.0.0"
       }
     },
+    "pluralize": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz",
+      "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
+      "dev": true
+    },
     "pn": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz",
@@ -12744,6 +12756,39 @@
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
       "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
     },
+    "require-uncached": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz",
+      "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=",
+      "dev": true,
+      "requires": {
+        "caller-path": "^0.1.0",
+        "resolve-from": "^1.0.0"
+      },
+      "dependencies": {
+        "caller-path": {
+          "version": "0.1.0",
+          "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
+          "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
+          "dev": true,
+          "requires": {
+            "callsites": "^0.2.0"
+          }
+        },
+        "callsites": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz",
+          "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
+          "dev": true
+        },
+        "resolve-from": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
+          "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=",
+          "dev": true
+        }
+      }
+    },
     "requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -12842,6 +12887,384 @@
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
       "dev": true
     },
+    "rewire": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/rewire/-/rewire-4.0.1.tgz",
+      "integrity": "sha512-+7RQ/BYwTieHVXetpKhT11UbfF6v1kGhKFrtZN7UDL2PybMsSt/rpLWeEUGF5Ndsl1D5BxiCB14VDJyoX+noYw==",
+      "dev": true,
+      "requires": {
+        "eslint": "^4.19.1"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "5.7.3",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+          "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+          "dev": true
+        },
+        "acorn-jsx": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
+          "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
+          "dev": true,
+          "requires": {
+            "acorn": "^3.0.4"
+          },
+          "dependencies": {
+            "acorn": {
+              "version": "3.3.0",
+              "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+              "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
+              "dev": true
+            }
+          }
+        },
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "dev": true,
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        },
+        "ajv-keywords": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz",
+          "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=",
+          "dev": true
+        },
+        "ansi-escapes": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+          "dev": true
+        },
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "chardet": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
+          "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
+          "dev": true
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "cross-spawn": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+          "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^4.0.1",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "doctrine": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+          "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+          "dev": true,
+          "requires": {
+            "esutils": "^2.0.2"
+          }
+        },
+        "eslint": {
+          "version": "4.19.1",
+          "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz",
+          "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==",
+          "dev": true,
+          "requires": {
+            "ajv": "^5.3.0",
+            "babel-code-frame": "^6.22.0",
+            "chalk": "^2.1.0",
+            "concat-stream": "^1.6.0",
+            "cross-spawn": "^5.1.0",
+            "debug": "^3.1.0",
+            "doctrine": "^2.1.0",
+            "eslint-scope": "^3.7.1",
+            "eslint-visitor-keys": "^1.0.0",
+            "espree": "^3.5.4",
+            "esquery": "^1.0.0",
+            "esutils": "^2.0.2",
+            "file-entry-cache": "^2.0.0",
+            "functional-red-black-tree": "^1.0.1",
+            "glob": "^7.1.2",
+            "globals": "^11.0.1",
+            "ignore": "^3.3.3",
+            "imurmurhash": "^0.1.4",
+            "inquirer": "^3.0.6",
+            "is-resolvable": "^1.0.0",
+            "js-yaml": "^3.9.1",
+            "json-stable-stringify-without-jsonify": "^1.0.1",
+            "levn": "^0.3.0",
+            "lodash": "^4.17.4",
+            "minimatch": "^3.0.2",
+            "mkdirp": "^0.5.1",
+            "natural-compare": "^1.4.0",
+            "optionator": "^0.8.2",
+            "path-is-inside": "^1.0.2",
+            "pluralize": "^7.0.0",
+            "progress": "^2.0.0",
+            "regexpp": "^1.0.1",
+            "require-uncached": "^1.0.3",
+            "semver": "^5.3.0",
+            "strip-ansi": "^4.0.0",
+            "strip-json-comments": "~2.0.1",
+            "table": "4.0.2",
+            "text-table": "~0.2.0"
+          }
+        },
+        "eslint-scope": {
+          "version": "3.7.3",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
+          "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==",
+          "dev": true,
+          "requires": {
+            "esrecurse": "^4.1.0",
+            "estraverse": "^4.1.1"
+          }
+        },
+        "espree": {
+          "version": "3.5.4",
+          "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
+          "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
+          "dev": true,
+          "requires": {
+            "acorn": "^5.5.0",
+            "acorn-jsx": "^3.0.0"
+          }
+        },
+        "external-editor": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
+          "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
+          "dev": true,
+          "requires": {
+            "chardet": "^0.4.0",
+            "iconv-lite": "^0.4.17",
+            "tmp": "^0.0.33"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+          "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+          "dev": true
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5"
+          }
+        },
+        "file-entry-cache": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
+          "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+          "dev": true,
+          "requires": {
+            "flat-cache": "^1.2.1",
+            "object-assign": "^4.0.1"
+          }
+        },
+        "flat-cache": {
+          "version": "1.3.4",
+          "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz",
+          "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==",
+          "dev": true,
+          "requires": {
+            "circular-json": "^0.3.1",
+            "graceful-fs": "^4.1.2",
+            "rimraf": "~2.6.2",
+            "write": "^0.2.1"
+          }
+        },
+        "ignore": {
+          "version": "3.3.10",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+          "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+          "dev": true
+        },
+        "inquirer": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
+          "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "^3.0.0",
+            "chalk": "^2.0.0",
+            "cli-cursor": "^2.1.0",
+            "cli-width": "^2.0.0",
+            "external-editor": "^2.0.4",
+            "figures": "^2.0.0",
+            "lodash": "^4.3.0",
+            "mute-stream": "0.0.7",
+            "run-async": "^2.2.0",
+            "rx-lite": "^4.0.8",
+            "rx-lite-aggregates": "^4.0.8",
+            "string-width": "^2.1.0",
+            "strip-ansi": "^4.0.0",
+            "through": "^2.3.6"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+          "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+          "dev": true
+        },
+        "lru-cache": {
+          "version": "4.1.5",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+          "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+          "dev": true,
+          "requires": {
+            "pseudomap": "^1.0.2",
+            "yallist": "^2.1.2"
+          }
+        },
+        "mimic-fn": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+          "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+          "dev": true
+        },
+        "mute-stream": {
+          "version": "0.0.7",
+          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+          "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+          "dev": true
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "regexpp": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz",
+          "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==",
+          "dev": true
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        },
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        },
+        "slice-ansi": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz",
+          "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0"
+          }
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+          "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+          "dev": true
+        },
+        "table": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz",
+          "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==",
+          "dev": true,
+          "requires": {
+            "ajv": "^5.2.3",
+            "ajv-keywords": "^2.1.0",
+            "chalk": "^2.1.0",
+            "lodash": "^4.17.4",
+            "slice-ansi": "1.0.0",
+            "string-width": "^2.1.1"
+          }
+        },
+        "write": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz",
+          "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+          "dev": true,
+          "requires": {
+            "mkdirp": "^0.5.1"
+          }
+        },
+        "yallist": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+          "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+          "dev": true
+        }
+      }
+    },
     "rework": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz",
@@ -12922,6 +13345,21 @@
         "aproba": "^1.1.1"
       }
     },
+    "rx-lite": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
+      "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=",
+      "dev": true
+    },
+    "rx-lite-aggregates": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
+      "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=",
+      "dev": true,
+      "requires": {
+        "rx-lite": "*"
+      }
+    },
     "rxjs": {
       "version": "6.5.4",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",

+ 2 - 0
package.json

@@ -23,6 +23,7 @@
     "lint-staged": "9.5.0",
     "node-sass": "4.13.1",
     "prettier": "1.19.1",
+    "rewire": "^4.0.1",
     "typescript": "3.7.5"
   },
   "eslintConfig": {
@@ -47,6 +48,7 @@
   "name": "excalidraw",
   "scripts": {
     "build": "react-scripts build",
+    "build-node": "./scripts/build-node.js",
     "eject": "react-scripts eject",
     "fix": "npm run prettier -- --write",
     "prettier": "prettier \"**/*.{js,css,scss,json,md,ts,tsx,html,yml}\"",

+ 40 - 0
scripts/build-node.js

@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+
+// In order to use this, you need to install Cairo on your machine. See
+// instructions here: https://github.com/Automattic/node-canvas#compiling
+
+// In order to run:
+//   npm install canvas # please do not check it in
+//   npm run build-node
+//   node build/static/js/build-node.js
+//   open test.png
+
+var rewire = require("rewire");
+var defaults = rewire("react-scripts/scripts/build.js");
+var config = defaults.__get__("config");
+
+// Disable multiple chunks
+config.optimization.runtimeChunk = false;
+config.optimization.splitChunks = {
+  cacheGroups: {
+    default: false
+  }
+};
+// Set the filename to be deterministic
+config.output.filename = "static/js/build-node.js";
+// Don't choke on node-specific requires
+config.target = "node";
+// Set the node entrypoint
+config.entry = "./src/index-node";
+// By default, webpack is going to replace the require of the canvas.node file
+// to just a string with the path of the canvas.node file. We need to tell
+// webpack to avoid rewriting that dependency.
+config.externals = function(context, request, callback) {
+  if (/\.node$/.test(request)) {
+    return callback(
+      null,
+      "commonjs ../../../node_modules/canvas/build/Release/canvas.node"
+    );
+  }
+  callback();
+};

+ 2 - 2
src/actions/actionSelectAll.ts

@@ -1,5 +1,5 @@
 import { Action } from "./types";
-import { META_KEY } from "../keys";
+import { KEYS } from "../keys";
 
 export const actionSelectAll: Action = {
   name: "selectAll",
@@ -9,5 +9,5 @@ export const actionSelectAll: Action = {
     };
   },
   contextItemLabel: "Select All",
-  keyTest: event => event[META_KEY] && event.code === "KeyA"
+  keyTest: event => event[KEYS.META] && event.code === "KeyA"
 };

+ 3 - 3
src/actions/actionStyles.ts

@@ -1,6 +1,6 @@
 import { Action } from "./types";
 import { isTextElement, redrawTextBoundingBox } from "../element";
-import { META_KEY } from "../keys";
+import { KEYS } from "../keys";
 
 let copiedStyles: string = "{}";
 
@@ -14,7 +14,7 @@ export const actionCopyStyles: Action = {
     return {};
   },
   contextItemLabel: "Copy Styles",
-  keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyC",
+  keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyC",
   contextMenuOrder: 0
 };
 
@@ -46,6 +46,6 @@ export const actionPasteStyles: Action = {
     };
   },
   contextItemLabel: "Paste Styles",
-  keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyV",
+  keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyV",
   contextMenuOrder: 1
 };

+ 5 - 5
src/actions/actionZindex.tsx

@@ -6,7 +6,7 @@ import {
   moveAllRight
 } from "../zindex";
 import { getSelectedIndices } from "../scene";
-import { META_KEY } from "../keys";
+import { KEYS } from "../keys";
 
 export const actionSendBackward: Action = {
   name: "sendBackward",
@@ -19,7 +19,7 @@ export const actionSendBackward: Action = {
   contextItemLabel: "Send Backward",
   keyPriority: 40,
   keyTest: event =>
-    event[META_KEY] && event.shiftKey && event.altKey && event.code === "KeyB"
+    event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyB"
 };
 
 export const actionBringForward: Action = {
@@ -33,7 +33,7 @@ export const actionBringForward: Action = {
   contextItemLabel: "Bring Forward",
   keyPriority: 40,
   keyTest: event =>
-    event[META_KEY] && event.shiftKey && event.altKey && event.code === "KeyF"
+    event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyF"
 };
 
 export const actionSendToBack: Action = {
@@ -45,7 +45,7 @@ export const actionSendToBack: Action = {
     };
   },
   contextItemLabel: "Send to Back",
-  keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyB"
+  keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyB"
 };
 
 export const actionBringToFront: Action = {
@@ -57,5 +57,5 @@ export const actionBringToFront: Action = {
     };
   },
   contextItemLabel: "Bring to Front",
-  keyTest: event => event[META_KEY] && event.shiftKey && event.code === "KeyF"
+  keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyF"
 };

+ 1 - 1
src/components/ExportDialog.tsx

@@ -8,7 +8,7 @@ import { clipboard, exportFile, downloadFile } from "./icons";
 import { Island } from "./Island";
 import { ExcalidrawElement } from "../element/types";
 import { AppState } from "../types";
-import { getExportCanvasPreview } from "../scene/data";
+import { getExportCanvasPreview } from "../scene/getExportCanvasPreview";
 import { ActionsManagerInterface, UpdaterFn } from "../actions/types";
 import Stack from "./Stack";
 

+ 74 - 0
src/index-node.ts

@@ -0,0 +1,74 @@
+import { getExportCanvasPreview } from "../src/scene/getExportCanvasPreview";
+
+const { registerFont, createCanvas } = require("canvas");
+
+const elements = [
+  {
+    id: "eVzaxG3YnHhqjEmD7NdYo",
+    type: "diamond",
+    x: 519,
+    y: 199,
+    width: 113,
+    height: 115,
+    strokeColor: "#000000",
+    backgroundColor: "transparent",
+    fillStyle: "hachure",
+    strokeWidth: 1,
+    roughness: 1,
+    opacity: 100,
+    isSelected: false,
+    seed: 749612521
+  },
+  {
+    id: "7W-iw5pEBPTU3eaCaLtFo",
+    type: "ellipse",
+    x: 552,
+    y: 238,
+    width: 49,
+    height: 44,
+    strokeColor: "#000000",
+    backgroundColor: "transparent",
+    fillStyle: "hachure",
+    strokeWidth: 1,
+    roughness: 1,
+    opacity: 100,
+    isSelected: false,
+    seed: 952056308
+  },
+  {
+    id: "kqKI231mvTrcsYo2DkUsR",
+    type: "text",
+    x: 557.5,
+    y: 317.5,
+    width: 43,
+    height: 31,
+    strokeColor: "#000000",
+    backgroundColor: "transparent",
+    fillStyle: "hachure",
+    strokeWidth: 1,
+    roughness: 1,
+    opacity: 100,
+    isSelected: false,
+    seed: 1683771448,
+    text: "test",
+    font: "20px Virgil",
+    baseline: 22
+  }
+];
+
+registerFont("./public/FG_Virgil.ttf", { family: "Virgil" });
+const canvas = getExportCanvasPreview(
+  elements as any,
+  {
+    exportBackground: true,
+    viewBackgroundColor: "#ffffff",
+    scale: 1
+  },
+  createCanvas
+);
+
+const fs = require("fs");
+const out = fs.createWriteStream("test.png");
+const stream = canvas.createPNGStream();
+stream.pipe(out);
+out.on("finish", () => console.log("test.png was created."));

+ 2 - 2
src/index.tsx

@@ -35,7 +35,7 @@ import { AppState } from "./types";
 import { ExcalidrawElement, ExcalidrawTextElement } from "./element/types";
 
 import { isInputLike, measureText, debounce, capitalizeString } from "./utils";
-import { KEYS, META_KEY, isArrowKey } from "./keys";
+import { KEYS, isArrowKey } from "./keys";
 
 import { findShapeByKey, shapesShortcutKeys, SHAPES } from "./shapes";
 import { createHistory } from "./history";
@@ -303,7 +303,7 @@ export class App extends React.Component<{}, AppState> {
         this.state.elementType !== "selection")
     ) {
       this.setState({ elementType: findShapeByKey(event.key) });
-    } else if (event[META_KEY] && event.code === "KeyZ") {
+    } else if (event[KEYS.META] && event.code === "KeyZ") {
       if (event.shiftKey) {
         // Redo action
         const data = history.redoOnce();

+ 6 - 5
src/keys.ts

@@ -6,13 +6,14 @@ export const KEYS = {
   ENTER: "Enter",
   ESCAPE: "Escape",
   DELETE: "Delete",
-  BACKSPACE: "Backspace"
+  BACKSPACE: "Backspace",
+  get META() {
+    return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
+      ? "metaKey"
+      : "ctrlKey";
+  }
 };
 
-export const META_KEY = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
-  ? "metaKey"
-  : "ctrlKey";
-
 export function isArrowKey(keyCode: string) {
   return (
     keyCode === KEYS.ARROW_LEFT ||

+ 3 - 66
src/scene/data.ts

@@ -1,13 +1,10 @@
-import rough from "roughjs/bin/rough";
-
 import { ExcalidrawElement } from "../element/types";
 
-import { getElementAbsoluteCoords } from "../element";
 import { getDefaultAppState } from "../appState";
 
-import { renderScene } from "../renderer";
 import { AppState } from "../types";
 import { ExportType } from "./types";
+import { getExportCanvasPreview } from "./getExportCanvasPreview";
 import nanoid from "nanoid";
 
 const LOCAL_STORAGE_KEY = "excalidraw";
@@ -169,66 +166,6 @@ export async function loadFromJSON() {
   }
 }
 
-export function getExportCanvasPreview(
-  elements: readonly ExcalidrawElement[],
-  {
-    exportBackground,
-    exportPadding = 10,
-    viewBackgroundColor,
-    scale = 1
-  }: {
-    exportBackground: boolean;
-    exportPadding?: number;
-    scale?: number;
-    viewBackgroundColor: string;
-  }
-) {
-  // calculate smallest area to fit the contents in
-  let subCanvasX1 = Infinity;
-  let subCanvasX2 = 0;
-  let subCanvasY1 = Infinity;
-  let subCanvasY2 = 0;
-
-  elements.forEach(element => {
-    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
-    subCanvasX1 = Math.min(subCanvasX1, x1);
-    subCanvasY1 = Math.min(subCanvasY1, y1);
-    subCanvasX2 = Math.max(subCanvasX2, x2);
-    subCanvasY2 = Math.max(subCanvasY2, y2);
-  });
-
-  function distance(x: number, y: number) {
-    return Math.abs(x > y ? x - y : y - x);
-  }
-
-  const tempCanvas = document.createElement("canvas");
-  const width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2;
-  const height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2;
-  tempCanvas.style.width = width + "px";
-  tempCanvas.style.height = height + "px";
-  tempCanvas.width = width * scale;
-  tempCanvas.height = height * scale;
-  tempCanvas.getContext("2d")?.scale(scale, scale);
-
-  renderScene(
-    elements,
-    rough.canvas(tempCanvas),
-    tempCanvas,
-    {
-      viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
-      scrollX: 0,
-      scrollY: 0
-    },
-    {
-      offsetX: -subCanvasX1 + exportPadding,
-      offsetY: -subCanvasY1 + exportPadding,
-      renderScrollbars: false,
-      renderSelection: false
-    }
-  );
-  return tempCanvas;
-}
-
 export async function exportCanvas(
   type: ExportType,
   elements: readonly ExcalidrawElement[],
@@ -262,7 +199,7 @@ export async function exportCanvas(
   if (type === "png") {
     const fileName = `${name}.png`;
     if ("chooseFileSystemEntries" in window) {
-      tempCanvas.toBlob(async blob => {
+      tempCanvas.toBlob(async (blob: any) => {
         if (blob) {
           await saveFileNative(fileName, blob);
         }
@@ -272,7 +209,7 @@ export async function exportCanvas(
     }
   } else if (type === "clipboard") {
     try {
-      tempCanvas.toBlob(async function(blob) {
+      tempCanvas.toBlob(async function(blob: any) {
         try {
           await navigator.clipboard.write([
             new window.ClipboardItem({ "image/png": blob })

+ 71 - 0
src/scene/getExportCanvasPreview.ts

@@ -0,0 +1,71 @@
+import rough from "roughjs/bin/rough";
+import { ExcalidrawElement } from "../element/types";
+import { getElementAbsoluteCoords } from "../element/bounds";
+import { renderScene } from "../renderer/renderScene";
+
+export function getExportCanvasPreview(
+  elements: readonly ExcalidrawElement[],
+  {
+    exportBackground,
+    exportPadding = 10,
+    viewBackgroundColor,
+    scale = 1
+  }: {
+    exportBackground: boolean;
+    exportPadding?: number;
+    scale?: number;
+    viewBackgroundColor: string;
+  },
+  createCanvas: (width: number, height: number) => any = function(
+    width,
+    height
+  ) {
+    const tempCanvas = document.createElement("canvas");
+    tempCanvas.style.width = width + "px";
+    tempCanvas.style.height = height + "px";
+    tempCanvas.width = width * scale;
+    tempCanvas.height = height * scale;
+    return tempCanvas;
+  }
+) {
+  // calculate smallest area to fit the contents in
+  let subCanvasX1 = Infinity;
+  let subCanvasX2 = 0;
+  let subCanvasY1 = Infinity;
+  let subCanvasY2 = 0;
+
+  elements.forEach(element => {
+    const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
+    subCanvasX1 = Math.min(subCanvasX1, x1);
+    subCanvasY1 = Math.min(subCanvasY1, y1);
+    subCanvasX2 = Math.max(subCanvasX2, x2);
+    subCanvasY2 = Math.max(subCanvasY2, y2);
+  });
+
+  function distance(x: number, y: number) {
+    return Math.abs(x > y ? x - y : y - x);
+  }
+
+  const width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2;
+  const height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2;
+  const tempCanvas: any = createCanvas(width, height);
+  tempCanvas.getContext("2d")?.scale(scale, scale);
+
+  renderScene(
+    elements,
+    rough.canvas(tempCanvas),
+    tempCanvas,
+    {
+      viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
+      scrollX: 0,
+      scrollY: 0
+    },
+    {
+      offsetX: -subCanvasX1 + exportPadding,
+      offsetY: -subCanvasY1 + exportPadding,
+      renderScrollbars: false,
+      renderSelection: false
+    }
+  );
+  return tempCanvas;
+}