Deno is a new JavaScript runtime built on V8.
Deno uses ESM to load JavaScript modules, and natively supports browser APIs like fetch(). Together with its permissions, this makes Deno feel more like a scriptable web client, and less like a tool for building web servers.
The Deno executable is built in Rust. While this may seem like an implementation detail, you could also describe Deno as a tool to embed JavaScript inside Rust programs.
This article describes my first steps using Deno.
Deno can be installed by copying the release from GitHub to a location on your path. I configured my environment as follows:
export DENO_DIR=~/.deno
export PATH=${PATH}:${DENO_DIR}/bin
Once installed, 'deno --version' shows the installed version and 'deno upgrade' upgrades the binary to the latest release. 'deno help' shows usage for other commands.
I recommend installing the Deno VS Code extension for IDE support and debugging. The manual suggested a launch config, which works for me, most of the time.
Here is hello.js, my first Deno program. You can run it with deno run hello.js args...
const hello = "Hello Deno";
console.log(`${hello} %s hello %o`, new Date(), Deno.args);
const buf = new TextEncoder().encode("-š¦-\n");
await Deno.stdout.write(buf);
console.table(buf);
The easiest way to write to stdout is by using the built-in console.log().
For those curious about Deno internals:
console
object is created in runtime/js/99_main.js.console.log()
method lives in runtime/js/02_console.js.core.print
in core/bindings.rs.Deno.stdout provides a lower level stream interface. Notice the await
on the promise returned by 'Deno.stdout.write()'.
The code above is also valid TypeScript. Since Deno includes a built-in TypeScript compiler, you could simply rename hello.js to hello.ts and it would work the same way.
The Deno Standard Library is largely written in TypeScript, as are the declarations (and auto-generated docs) for built-ins, so it helps to know a little TypeScript syntax even if you prefer to write JavaScript.
I find the TypeScript declarations most useful for code completion in VS Code.
In the spirit of leveraging Deno as a web client, I decided to try building a simple link validator. This requires a 3rd-party library to parse HTML.
I started my search assuming that a popular npm module would be my best bet, even if it wasn't available (yet) in deno.land/x which is where library authors can register their GitHub repos to publish deno-compatible ESM modules.
After some googling, I landed on parse5 which enjoys wide usage and offers a simple, low-level tree API at its core.
I had also heard about Skypack, a CDN, specifically designed to serve npm packages as ESM modules. A quick search on skypack.dev and I had a URL for the parse5 module which works in Deno.
The code in scan.js crawls a website, validating that all the links on the site which point to the same origin can be fetched.
import parse5 from "https://cdn.skypack.dev/parse5@6.0.1";
const rootUrl = Deno.args[0];
if (!rootUrl) exit(1, "Please provide a URL");
const rootOrigin = new URL(rootUrl).origin;
const urlMap = {}; // tracks visited urls
await checkUrl(rootUrl);
const result = Object.entries(urlMap).filter((kv) => kv[1] !== "OK");
if (result.length) {
exit(1, result);
} else {
exit(0, "š no broken links found.");
}
// recursively checks url and same-origin urls inside
// resolves when done
async function checkUrl(url, base) {
base = base || url;
try {
// parse the url relative to base
const urlObj = new URL(url, base);
// ignore query params and hash
const href = urlObj.origin + urlObj.pathname;
// only process same-origin urls
if (!urlMap[href] && urlObj.origin === rootOrigin) {
// fetch from href
urlMap[href] = "pending";
const res = await fetch(href);
// bail out if fetch was not ok
if (!res.ok) {
urlMap[href] = { status: res.status, in: base };
return;
}
urlMap[href] = "OK";
// check content type
if (!res.headers.get("content-type").match(/text\/html/i)) return;
// parse response
console.log("parsing", urlObj.pathname);
const html = await res.text();
const document = parse5.parse(html);
// scan for <a> tags and call checkURL for each, with base = href
const promises = [];
scan(document, "a", (node) => {
promises.push(checkUrl(attr(node, "href"), href));
});
await Promise.all(promises);
}
} catch (err) {
urlMap[url] = { error: err.message, in: base };
}
}
// return value of attr with name for a node
function attr(node, name) {
return node.attrs.find((attr) => attr.name === name)?.value;
}
// recursive DOM scan
// calls fn(node) on nodes matching tagName
function scan(node, tagName, fn) {
if (node?.tagName === tagName) {
fn(node);
}
if (!node.childNodes) return;
for (const childNode of node.childNodes) {
scan(childNode, tagName, fn);
}
}
function exit(code, msg) {
console.log(msg);
Deno.exit(code);
}
This script is hosted at https://deno-hello.jldec.me/ using Cloudflare Pages.
To run it, call deno run --allow-net SCRIPT URL
. E.g.
$ deno run --allow-net https://deno-hello.jldec.me/scan.js https://jldec.me
parsing /
parsing /getting-started-with-deno
parsing /first-steps-using-cloudflare-pages
parsing /calling-rust-from-a-cloudflare-worker
parsing /a-web-for-everyone
parsing /why-serverless-at-the-edge
parsing /fun-with-vercel
parsing /migrating-from-cjs-to-esm
parsing /forays-from-node-to-rust
parsing /about
parsing /github-actions-101
parsing /spring-boot-101
parsing /why-the-web-needs-better-html-editing-components
š no broken links found.
NOTE: For this first implementation, there is no queueing, so I would not recommend pointing it at large site.
The deno experience still feels a little rough in places, but one new feature which I really like, is the ability to compile a script into a self-contained executable.
$ deno --unstable compile --allow-net scan.js
Bundle file:./scan.js
Compile file:./scan.js
Emit scan
Now I can call scan
without having to install Deno or remember any special options.
$ ./scan
Please provide a URL
$ ./scan https://jldec.fun
...
š no broken links found.
To leave a comment
please visit dev.to/jldec
{ "path": "/blog/getting-started-with-deno", "attrs": { "title": "Getting Started with Deno", "splash": { "image": "/images/clouds-trees.jpg" }, "date": "2021-02-28", "layout": "BlogPostLayout", "excerpt": "The [Deno](https://deno.land/) executable is built in Rust. While this may seem like an implementation detail, you could also describe Deno as a tool to embed JavaScript inside Rust programs.\n" }, "md": "# Getting Started with Deno\n\n[Deno](https://deno.land/) is a new JavaScript runtime built on [V8](https://github.com/v8/v8#readme).\n\nDeno uses [ESM](migrating-from-cjs-to-esm) to load JavaScript modules, and natively supports browser APIs like [fetch()](https://deno.land/manual/runtime/web_platform_apis). Together with its [permissions](https://deno.land/manual/getting_started/permissions), this makes Deno feel more like a scriptable web client, and less like a tool for building web servers.\n\nThe Deno executable is built in Rust. While this may seem like an implementation detail, you could also describe Deno as a tool to embed JavaScript inside Rust programs.\n\nThis article describes my first steps using Deno.\n\n## Getting Started\n\nDeno can be [installed](https://deno.land/manual/getting_started/installation) by copying the release from [GitHub](https://github.com/denoland/deno/releases/latest) to a location on your path. I configured my environment as follows:\n\n```sh\nexport DENO_DIR=~/.deno\nexport PATH=${PATH}:${DENO_DIR}/bin\n```\n\nOnce installed, 'deno --version' shows the installed version and 'deno upgrade' upgrades the binary to the latest release. 'deno help' shows usage for other commands.\n\nI recommend installing the Deno [VS Code extension](https://github.com/denoland/vscode_deno#readme) for IDE support and debugging. The [manual](https://deno.land/manual/getting_started/debugging_your_code#vscode) suggested a [launch config](https://github.com/jldec/deno-hello/blob/main/.vscode/launch.json), which works for me, _most_ of the time.\n\n![VS Code Deno extension Debug](/images/deno-debug-dark.png)\n\n## hello.js\n\nHere is [hello.js](https://github.com/jldec/deno-hello/tree/main/hello.js), my first Deno program. You can run it with `deno run hello.js args...`\n\n```js\nconst hello = \"Hello Deno\";\nconsole.log(`${hello} %s hello %o`, new Date(), Deno.args);\n\nconst buf = new TextEncoder().encode(\"-š¦-\\n\");\nawait Deno.stdout.write(buf);\nconsole.table(buf);\n```\n\nThe easiest way to write to stdout is by using the built-in console.log().\n\nFor those curious about Deno internals:\n\n- The global `console` object is created in [runtime/js/99_main.js](https://github.com/denoland/deno/blob/v1.7.5/runtime/js/99_main.js#L246).\n- The `console.log()` method lives in [runtime/js/02_console.js](https://github.com/denoland/deno/blob/v1.7.5/runtime/js/02_console.js#L1503).\n- This calls the rust function `core.print` in [core/bindings.rs](https://github.com/denoland/deno/blob/v1.7.5/core/bindings.rs#L277).\n\n[Deno.stdout]() provides a lower level stream interface. Notice the `await` on the promise returned by 'Deno.stdout.write()'.\n\n### Typescript\n\nThe code above is also valid [TypeScript](https://www.typescriptlang.org/). Since Deno includes a built-in TypeScript compiler, you could simply rename hello.js to hello.ts and it would work the same way.\n\nThe Deno [Standard Library](https://deno.land/std) is largely written in TypeScript, as are the declarations (and auto-generated docs) for [built-ins](https://doc.deno.land/builtin/stable), so it helps to know a little TypeScript syntax even if you prefer to write JavaScript.\n\nI find the TypeScript declarations most useful for code completion in VS Code.\n\n## scan.js\n\nIn the spirit of leveraging Deno as a web client, I decided to try building a simple link validator. This requires a 3rd-party library to parse HTML.\n\nI started my search assuming that a popular npm module would be my best bet, even if it wasn't available (yet) in [deno.land/x](https://deno.land/x) which is where library authors can register their GitHub repos to publish deno-compatible ESM modules.\n\nAfter some googling, I landed on [parse5](https://github.com/inikulin/parse5) which enjoys wide usage and offers a simple, low-level tree API at its core.\n\nI had also heard about [Skypack](https://docs.skypack.dev/skypack-cdn/code/deno), a CDN, specifically designed to serve npm packages as ESM modules. A quick search on [skypack.dev](https://www.skypack.dev/) and I had a URL for the parse5 module which works in Deno.\n\nThe code in [scan.js](https://github.com/jldec/deno-hello/blob/main/scan.js) crawls a website, validating that all the links on the site which point to the same origin can be fetched.\n\n```js\nimport parse5 from \"https://cdn.skypack.dev/parse5@6.0.1\";\n\nconst rootUrl = Deno.args[0];\nif (!rootUrl) exit(1, \"Please provide a URL\");\n\nconst rootOrigin = new URL(rootUrl).origin;\n\nconst urlMap = {}; // tracks visited urls\n\nawait checkUrl(rootUrl);\nconst result = Object.entries(urlMap).filter((kv) => kv[1] !== \"OK\");\n\nif (result.length) {\n exit(1, result);\n} else {\n exit(0, \"š no broken links found.\");\n}\n\n// recursively checks url and same-origin urls inside\n// resolves when done\nasync function checkUrl(url, base) {\n base = base || url;\n try {\n // parse the url relative to base\n const urlObj = new URL(url, base);\n\n // ignore query params and hash\n const href = urlObj.origin + urlObj.pathname;\n\n // only process same-origin urls\n if (!urlMap[href] && urlObj.origin === rootOrigin) {\n // fetch from href\n urlMap[href] = \"pending\";\n const res = await fetch(href);\n\n // bail out if fetch was not ok\n if (!res.ok) {\n urlMap[href] = { status: res.status, in: base };\n return;\n }\n\n urlMap[href] = \"OK\";\n\n // check content type\n if (!res.headers.get(\"content-type\").match(/text\\/html/i)) return;\n\n // parse response\n console.log(\"parsing\", urlObj.pathname);\n const html = await res.text();\n const document = parse5.parse(html);\n\n // scan for <a> tags and call checkURL for each, with base = href\n const promises = [];\n scan(document, \"a\", (node) => {\n promises.push(checkUrl(attr(node, \"href\"), href));\n });\n await Promise.all(promises);\n }\n } catch (err) {\n urlMap[url] = { error: err.message, in: base };\n }\n}\n\n// return value of attr with name for a node\nfunction attr(node, name) {\n return node.attrs.find((attr) => attr.name === name)?.value;\n}\n\n// recursive DOM scan\n// calls fn(node) on nodes matching tagName\nfunction scan(node, tagName, fn) {\n if (node?.tagName === tagName) {\n fn(node);\n }\n if (!node.childNodes) return;\n for (const childNode of node.childNodes) {\n scan(childNode, tagName, fn);\n }\n}\n\nfunction exit(code, msg) {\n console.log(msg);\n Deno.exit(code);\n}\n```\n\nThis script is hosted at https://deno-hello.jldec.me/ using [Cloudflare Pages](first-steps-using-cloudflare-pages).\n\nTo run it, call `deno run --allow-net SCRIPT URL`. E.g.\n\n```sh\n$ deno run --allow-net https://deno-hello.jldec.me/scan.js https://jldec.me\nparsing /\nparsing /getting-started-with-deno\nparsing /first-steps-using-cloudflare-pages\nparsing /calling-rust-from-a-cloudflare-worker\nparsing /a-web-for-everyone\nparsing /why-serverless-at-the-edge\nparsing /fun-with-vercel\nparsing /migrating-from-cjs-to-esm\nparsing /forays-from-node-to-rust\nparsing /about\nparsing /github-actions-101\nparsing /spring-boot-101\nparsing /why-the-web-needs-better-html-editing-components\nš no broken links found.\n```\n\nNOTE: For this first implementation, there is no queueing, so I would not recommend pointing it at large site.\n\n## Compiling\n\nThe deno experience still feels a little rough in places, but one new feature which I really like, is the ability to [compile](https://deno.land/manual/tools/compiler) a script into a self-contained executable.\n\n```sh\n$ deno --unstable compile --allow-net scan.js\nBundle file:./scan.js\nCompile file:./scan.js\nEmit scan\n```\n\nNow I can call `scan` without having to install Deno or remember any special options.\n\n```sh\n$ ./scan\nPlease provide a URL\n\n$ ./scan https://jldec.fun\n...\nš no broken links found.\n```\n\n> [![Deno logo](/images/deno-logo.png \".no-border\")](https://deno.land/)\n\n_To leave a comment \nplease visit [dev.to/jldec](https://dev.to/jldec/getting-started-with-deno-2ie7)_\n\n", "html": "<h1>Getting Started with Deno</h1>\n<p><a href=\"https://deno.land/\">Deno</a> is a new JavaScript runtime built on <a href=\"https://github.com/v8/v8#readme\">V8</a>.</p>\n<p>Deno uses <a href=\"migrating-from-cjs-to-esm\">ESM</a> to load JavaScript modules, and natively supports browser APIs like <a href=\"https://deno.land/manual/runtime/web_platform_apis\">fetch()</a>. Together with its <a href=\"https://deno.land/manual/getting_started/permissions\">permissions</a>, this makes Deno feel more like a scriptable web client, and less like a tool for building web servers.</p>\n<p>The Deno executable is built in Rust. While this may seem like an implementation detail, you could also describe Deno as a tool to embed JavaScript inside Rust programs.</p>\n<p>This article describes my first steps using Deno.</p>\n<h2>Getting Started</h2>\n<p>Deno can be <a href=\"https://deno.land/manual/getting_started/installation\">installed</a> by copying the release from <a href=\"https://github.com/denoland/deno/releases/latest\">GitHub</a> to a location on your path. I configured my environment as follows:</p>\n<pre><code class=\"language-sh\">export DENO_DIR=~/.deno\nexport PATH=${PATH}:${DENO_DIR}/bin\n</code></pre>\n<p>Once installed, 'deno --version' shows the installed version and 'deno upgrade' upgrades the binary to the latest release. 'deno help' shows usage for other commands.</p>\n<p>I recommend installing the Deno <a href=\"https://github.com/denoland/vscode_deno#readme\">VS Code extension</a> for IDE support and debugging. The <a href=\"https://deno.land/manual/getting_started/debugging_your_code#vscode\">manual</a> suggested a <a href=\"https://github.com/jldec/deno-hello/blob/main/.vscode/launch.json\">launch config</a>, which works for me, <em>most</em> of the time.</p>\n<p><img src=\"/images/deno-debug-dark.png\" alt=\"VS Code Deno extension Debug\"></p>\n<h2>hello.js</h2>\n<p>Here is <a href=\"https://github.com/jldec/deno-hello/tree/main/hello.js\">hello.js</a>, my first Deno program. You can run it with <code>deno run hello.js args...</code></p>\n<pre><code class=\"language-js\">const hello = "Hello Deno";\nconsole.log(`${hello} %s hello %o`, new Date(), Deno.args);\n\nconst buf = new TextEncoder().encode("-š¦-\\n");\nawait Deno.stdout.write(buf);\nconsole.table(buf);\n</code></pre>\n<p>The easiest way to write to stdout is by using the built-in console.log().</p>\n<p>For those curious about Deno internals:</p>\n<ul>\n<li>The global <code>console</code> object is created in <a href=\"https://github.com/denoland/deno/blob/v1.7.5/runtime/js/99_main.js#L246\">runtime/js/99_main.js</a>.</li>\n<li>The <code>console.log()</code> method lives in <a href=\"https://github.com/denoland/deno/blob/v1.7.5/runtime/js/02_console.js#L1503\">runtime/js/02_console.js</a>.</li>\n<li>This calls the rust function <code>core.print</code> in <a href=\"https://github.com/denoland/deno/blob/v1.7.5/core/bindings.rs#L277\">core/bindings.rs</a>.</li>\n</ul>\n<p><a href=\"\">Deno.stdout</a> provides a lower level stream interface. Notice the <code>await</code> on the promise returned by 'Deno.stdout.write()'.</p>\n<h3>Typescript</h3>\n<p>The code above is also valid <a href=\"https://www.typescriptlang.org/\">TypeScript</a>. Since Deno includes a built-in TypeScript compiler, you could simply rename hello.js to hello.ts and it would work the same way.</p>\n<p>The Deno <a href=\"https://deno.land/std\">Standard Library</a> is largely written in TypeScript, as are the declarations (and auto-generated docs) for <a href=\"https://doc.deno.land/builtin/stable\">built-ins</a>, so it helps to know a little TypeScript syntax even if you prefer to write JavaScript.</p>\n<p>I find the TypeScript declarations most useful for code completion in VS Code.</p>\n<h2>scan.js</h2>\n<p>In the spirit of leveraging Deno as a web client, I decided to try building a simple link validator. This requires a 3rd-party library to parse HTML.</p>\n<p>I started my search assuming that a popular npm module would be my best bet, even if it wasn't available (yet) in <a href=\"https://deno.land/x\">deno.land/x</a> which is where library authors can register their GitHub repos to publish deno-compatible ESM modules.</p>\n<p>After some googling, I landed on <a href=\"https://github.com/inikulin/parse5\">parse5</a> which enjoys wide usage and offers a simple, low-level tree API at its core.</p>\n<p>I had also heard about <a href=\"https://docs.skypack.dev/skypack-cdn/code/deno\">Skypack</a>, a CDN, specifically designed to serve npm packages as ESM modules. A quick search on <a href=\"https://www.skypack.dev/\">skypack.dev</a> and I had a URL for the parse5 module which works in Deno.</p>\n<p>The code in <a href=\"https://github.com/jldec/deno-hello/blob/main/scan.js\">scan.js</a> crawls a website, validating that all the links on the site which point to the same origin can be fetched.</p>\n<pre><code class=\"language-js\">import parse5 from "https://cdn.skypack.dev/parse5@6.0.1";\n\nconst rootUrl = Deno.args[0];\nif (!rootUrl) exit(1, "Please provide a URL");\n\nconst rootOrigin = new URL(rootUrl).origin;\n\nconst urlMap = {}; // tracks visited urls\n\nawait checkUrl(rootUrl);\nconst result = Object.entries(urlMap).filter((kv) => kv[1] !== "OK");\n\nif (result.length) {\n exit(1, result);\n} else {\n exit(0, "š no broken links found.");\n}\n\n// recursively checks url and same-origin urls inside\n// resolves when done\nasync function checkUrl(url, base) {\n base = base || url;\n try {\n // parse the url relative to base\n const urlObj = new URL(url, base);\n\n // ignore query params and hash\n const href = urlObj.origin + urlObj.pathname;\n\n // only process same-origin urls\n if (!urlMap[href] && urlObj.origin === rootOrigin) {\n // fetch from href\n urlMap[href] = "pending";\n const res = await fetch(href);\n\n // bail out if fetch was not ok\n if (!res.ok) {\n urlMap[href] = { status: res.status, in: base };\n return;\n }\n\n urlMap[href] = "OK";\n\n // check content type\n if (!res.headers.get("content-type").match(/text\\/html/i)) return;\n\n // parse response\n console.log("parsing", urlObj.pathname);\n const html = await res.text();\n const document = parse5.parse(html);\n\n // scan for <a> tags and call checkURL for each, with base = href\n const promises = [];\n scan(document, "a", (node) => {\n promises.push(checkUrl(attr(node, "href"), href));\n });\n await Promise.all(promises);\n }\n } catch (err) {\n urlMap[url] = { error: err.message, in: base };\n }\n}\n\n// return value of attr with name for a node\nfunction attr(node, name) {\n return node.attrs.find((attr) => attr.name === name)?.value;\n}\n\n// recursive DOM scan\n// calls fn(node) on nodes matching tagName\nfunction scan(node, tagName, fn) {\n if (node?.tagName === tagName) {\n fn(node);\n }\n if (!node.childNodes) return;\n for (const childNode of node.childNodes) {\n scan(childNode, tagName, fn);\n }\n}\n\nfunction exit(code, msg) {\n console.log(msg);\n Deno.exit(code);\n}\n</code></pre>\n<p>This script is hosted at <a href=\"https://deno-hello.jldec.me/\">https://deno-hello.jldec.me/</a> using <a href=\"first-steps-using-cloudflare-pages\">Cloudflare Pages</a>.</p>\n<p>To run it, call <code>deno run --allow-net SCRIPT URL</code>. E.g.</p>\n<pre><code class=\"language-sh\">$ deno run --allow-net https://deno-hello.jldec.me/scan.js https://jldec.me\nparsing /\nparsing /getting-started-with-deno\nparsing /first-steps-using-cloudflare-pages\nparsing /calling-rust-from-a-cloudflare-worker\nparsing /a-web-for-everyone\nparsing /why-serverless-at-the-edge\nparsing /fun-with-vercel\nparsing /migrating-from-cjs-to-esm\nparsing /forays-from-node-to-rust\nparsing /about\nparsing /github-actions-101\nparsing /spring-boot-101\nparsing /why-the-web-needs-better-html-editing-components\nš no broken links found.\n</code></pre>\n<p>NOTE: For this first implementation, there is no queueing, so I would not recommend pointing it at large site.</p>\n<h2>Compiling</h2>\n<p>The deno experience still feels a little rough in places, but one new feature which I really like, is the ability to <a href=\"https://deno.land/manual/tools/compiler\">compile</a> a script into a self-contained executable.</p>\n<pre><code class=\"language-sh\">$ deno --unstable compile --allow-net scan.js\nBundle file:./scan.js\nCompile file:./scan.js\nEmit scan\n</code></pre>\n<p>Now I can call <code>scan</code> without having to install Deno or remember any special options.</p>\n<pre><code class=\"language-sh\">$ ./scan\nPlease provide a URL\n\n$ ./scan https://jldec.fun\n...\nš no broken links found.\n</code></pre>\n<blockquote>\n<p><a href=\"https://deno.land/\"><img src=\"/images/deno-logo.png\" alt=\"Deno logo\" title=\".no-border\"></a></p>\n</blockquote>\n<p><em>To leave a comment<br>\nplease visit <a href=\"https://dev.to/jldec/getting-started-with-deno-2ie7\">dev.to/jldec</a></em></p>\n" }