Running stuff on Deno
Sebastien Filion
Posted on May 17, 2021
Oh. Hey there!
I'm happy you came back for this third post. The subject today is: "Running stuff on Deno".
This post is a transcript of a Youtube video I made.
I brushed over this on the previous post because I wanted to cover it in detail.
One of the thing that I truly love about Deno is that it is chuck full of tools -- out-of-the-box; with all
of that, I can be productive in seconds without any setup.
The REPL
The first tool that I think we should explore is the REPL. It is a terminal-based interactive runtime.
It is especially useful when you need to test bits of code without having to create a file or bootstrap
a project.
To bring up the REPL, all you need to do is to execute the deno
command and, you're ready to go.
The first thing you'll see, is the current version and instruction on how to exit.
On an empty line just hit ctrl+d
or type close()
.
Here we can type-in any valid JavaScript expression.
const message = "hello world".split("").reverse().join("");
Using the tab key, you get autocompletion. Deno.writeTe [tab]
and poof:
Deno.writeTextFile(`${Deno.cwd()}/scratch`, message);
Here we can just read the file back to confirm that it was written properly.
Deno.readTextFile(`${Deno.cwd()}/scratch`).then(message => console.log(message));
Since all the IO method returns a Promise, this is a perfect segway into "top-level await" -- For this
example, I will assume that you are familiar with the async/await. Deno allows the use of await
within
the global scope although it is usually reserved for specifically flagged functions. We can leverage this
feature when using the REPL.
await Deno.readTextFile(`${Deno.cwd()}/scratch`);
In the REPL usual import syntax isn't available, we can leverage top-level await and the import
function available in the runtime to import modules.
const { getUser } = await import("https://raw.githubusercontent.com/sebastienfilion/i-y/main/02/users.js");
await getUser();
The REPL is full of shortcuts. Here's a few that I enjoy!
ctrl+R
, up/down arrows to search for something.
> "he
const message = "hello world".split("").reverse().join("");
CTRL+U
, CTRL+Y
to cut or paste a line, useful when you need to remove a line quickly.
There's also a special character _
that always refers to the last evaluated value.
const multiply = (x) => x * 2;
[ 42, 24, 12 ].map(multiply);
_.map(multiply);
> [ 168, 96, 48 ]
In the same vein, _error
refers to the last error that was thrown.
[ 42, 24 12 ]
> SyntaxError: Unexpected number
_error
It's important to note that the REPL might be executed with the --unstable
flag, if you need access unstable APIs.
Deno.consoleSize(Deno.stdout.rid);
Finally, you can pipe files or expression into the REPL using the --
.
echo "2 + 2" | deno --
You can also add the --unstable
flag.
echo "Deno.consoleSize(Deno.stdout.rid);" | deno --unstable --
Be careful though cause running code like this doesn't execute in a sandbox. So you may be giving open access to your computer to some stranger... This is a perfect segue into permissions...
echo "await Deno.readTextFile(\"./scratch\")" | deno --
Running with permissions
All the code for this demo is available on Github.
So that's for the REPL. In my experience, it is one of the most complete and friendlier REPL out there.
Now let's talk about the run
subcommand in detail. As I mentioned earlier, I brushed over it during the
previous videos because I wanted to cover it in detail. I also want to explore the permission API as
it is one of Deno main selling-point.
Take this code as an example. It uses the fetch
function to access a given movie's data over HTTP.
// movies.js
export function getMovieByTitle (APIKey, title) {
return fetch(`http://www.omdbapi.com/?apikey=65ea1e8b&t=${encodeURIComponent(title)}`)
.then(response => response.json());
}
To run this code, we'll import it into a file and pass the OMDB_API_KEY
environment variable.
// scratch.js
import { getMovieByTitle } from "./movies.js";
getMovieByTitle(Deno.env.get("OMDB_API_KEY"), "Tenet")
.then(movie => console.log(movie));
So, now we use the --allow-net
and --allow-env
flags to grant the right permissions when running the file.
OMDB_API_KEY=████████ deno run --allow-net="www.omdbapi.com" --allow-env="OMDB_API_KEY" scratch.js
Ok, so now let's say that we want to write to a file the title and the description of our favourite movies; we can create a CLI that will take the title of the movie, fetch it and write it to the File System.
// cli.js
const [ title ] = Deno.args;
getMovieByTitle(Deno.env.get("OMDB_API_KEY"), title)
.then(
movie => Deno.writeTextFile(
`${Deno.cwd()}/movies`,
`${movie.Title}: ${movie.Plot}\r\n`,
{ append: true }
)
)
.then(() => console.log("...done"));
To run this file, we'll need to grand "write" permission with --allow-write
.
OMDB_API_KEY=████████ deno run --allow-net="www.omdbapi.com" --allow-env="OMDB_API_KEY" --allow-read=$(pwd) --allow-write=$(pwd) cli.js "The Imitation Game"
Another way to grant permission is with --prompt
. This option will prompt the user for every permission not granted already when the runtime reaches the code.
OMDB_API_KEY=████████ deno run --prompt cli.js "Tron"
From this, I just want to take a quick detour to explore the runtime's Permission API.
console.log(await Deno.permissions.query({ name: "write", path: import.meta.url }));
await Deno.permissions.request({ name: "write", path: import.meta.url })
console.log(await Deno.permissions.query({ name: "write", path: import.meta.url }));
await Deno.permissions.revoke({ name: "write", path: import.meta.url })
console.log(await Deno.permissions.query({ name: "write", path: import.meta.url }));
The object part is called a "permission descriptor" -- they all have a "name" property but, the other property might be
different.
For example... to read and write it's "path"
...
const readDescriptor = { name: "read", path: import.meta.url };
const writeDescriptor = { name: "write", path: import.meta.url };
const environmentDescriptor = { name: "env", variable: "OMDB_API_KEY" };
const netDescriptor = { name: "net", command: "www.omdbapi.com" };
const runDescriptor = { name: "run", command: "git" };
Okay, we're back on track now. Now that we can add movies to our file, I think it would be useful for our tool to read them back to us. I wrote a small utility to display the file while creating an opportunity to use an unstable API.
import { getMovieByTitle } from "./movies.js";
import { prepareForViewport } from "https://raw.githubusercontent.com/sebastienfilion/i-y/main/deno/03/utilities.js";
function displayMovies (data) {
const { columns, rows } = Deno.consoleSize(Deno.stdout.rid);
return Deno.write(
Deno.stdout.rid,
prepareForViewport(data, { columns, rows, title: "My movie collection" })
);
}
if (import.meta.main) {
const [ action, title ] = Deno.args;
if (action === "fetch") getMovieByTitle(Deno.env.get("OMDB_API_KEY"), title)
.then(
movie => Deno.writeTextFile(
`${Deno.cwd()}/movies`,
`${movie.Title}: ${movie.Plot}\r\n`,
{ append: true }
)
)
.then(() => console.log("...done"));
else if (action === "display") Deno.readFile(`${Deno.cwd()}/movies`)
.then(displayMovies);
else console.error(`There are no action "${action}"`);
}
So this time, because we use Deno.consoleSize
which is marked as unstable, we need to add the --unstable
flag. Also, because we are reading from our movie file, we need to grand read permission with --allow-read
.
OMDB_API_KEY=████████ deno run --allow-net="www.omdbapi.com" --allow-env="OMDB_API_KEY" --allow-read=$(pwd) --allow-write=$(pwd) cli.js fetch "WarGames"
If you were to download the code and run it with --watch
, you'd be able to play with the options of prepareForViewport
.
You can change the title, or the ratio
for a number between 0
and 1
, the default is 0.8
.
OMDB_API_KEY=65ea1e8b deno run --allow-env=OMDB_API_KEY --allow-net="www.omdbapi.com" --allow-read=$(pwd) --allow-write=$(pwd) --unstable --watch cli.js display
Before closing on this chapter, I want to talk about one more permission flag, --allow-run
. This flag allows the code
to run a command, for example ls
, git
, etc...
The command will not be executed in the same sandbox as Deno.
Meaning that a malicious developer could do the following... which would output all the structure of your current
working directory.
const process = Deno.run({ cmd: [ "ls", "." ] });
await process.status();
Giving permission to a process to run any command could be a huge security risk.
Always use --allow-run
along with the commands that you know will be used. For example --allow-run=git
... to allow a process to use Git on the current working directory.
I will do a full video on the Deno.run
API later down the line.
Um, I've avoided using it up until now; there's also a --allow--all
flag or -A
... To grand all the
permissions...
It's safe to use while you're developing -- but don't be lazy use the appropriate flag when running code you find on the
Internet.
When you'll get bored of always typing the run command with all of it's permissions, you may want to consider simply
creating an executable.
echo "OMDB_API_KEY=65ea1e8b deno run --allow-env=OMDB_API_KEY --allow-net="www.omdbapi.com" --allow-read=$(pwd) --allow-write=$(pwd) --unstable --watch cli.js display" | ilm
chmod +x ilm
./ilm
That was a long one...
In this post we saw how to run stuff with Deno and more importantly how to run stuff safely using the Permission flags.
On the next post, we will resume our Deno-journey and explore all the tools that can help us write better code...
Like the linter, the formatter, the test runner and the documentation generator!
Posted on May 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.