How does it work? — the Art of Code Trace

Dexter Chang
5 min readApr 1, 2021

Comprehends the codes more than you thought.

My case when doing code trace on dotenv
Table of Contents* What is code trace
* What's dotenv
* Assumption: how to design it
* Dive into the codes
* Find the entry point
* Break down the function
* Final Thoughts
* References

What is code trace

Imagine you’re co-work with others, you can read documents, sync with the code owner, execute the programs via an easy-to-use API or function call, but do you know how does it work? Can you find the bug when the program crashes or doesn’t work?

Code trace is like we are simulating the computer, running the programs line by line. You’re not writing the code, instead, you are reading the code.

The benefits of code trace are not only you can fully understand a service, library, or project, but it can help you to learn:

  • How the others coding, naming.
  • How to design the projects’ structure.
  • Practicing the issue-tracking ability.

In this article, we are going to trace the open-source dotenv line by line, let’s practice together. 🙂

What’s dotnev

From GitHub:

Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env. Storing configuration in the environment separate from code is based on The Twelve-Factor App methodology.

In real life, we won’t deploy your code directly to your application’s production environment, because no code is 100% with zero error, we might separate into the different environments such as:

  • preview: a playground for the developers, no worries if anything breaks (yah!)
  • staging: an environment that we can deploy a new feature for the customers to do the user acceptance testing, known as UAT.
  • production: the environment for real products.

Imagine we use different configurations for the different environments:

# for preview's database
SQL_USERNAME=dexter
SQL_PASSWORD=ok
# for staging's database
SQL_USERNAME=admin
SQL_PASSWORD=123456789
# for production's database
SQL_USERNAME=prodAdmin
SQL_PASSWORD=asdf12345qwer6789

It’s not recommended that we put all environments’ secerts into the same file, also, you need to resolve the duplicate naming issue.

In this case, dotenv is a great tool that can help you.

Assumption: how to design it

NOTE:

1. We only trace the basic usage of it.

2. The version we’re tracing is 8.2.0.

Let’s take a look at the usage:

Screenshot from GitHub

dotnev loads a file with suffix .env, then loads into process.env, which is an object that contains all the user’s environment.

Then in our code, we can use process.env.<secret name> to get the value, quite easy, isn’t it?

Before we dive into the code, let’s try if we can design a library as elegant as it.

Scroll down once you come up with some ideas.

Photo by Tachina Lee on Unsplash

We can break down the task into:

  1. Loads a file with a suffix .env.
  2. Parses the file line by line.
  3. Puts the values to process.env according to their secret names.

Dive into the codes

Find the entry point

First thing first, take a look at the structure (here we only focusing on the important files or folders):

# /.
├── _lib
│ └── main.js
└── package.json

When we are tracing the code, find the entry point (in this case, we are looking for an export module’s entry point) is the key lead to success. In npm, we can find the entry point of the program in thepackage.json:

NOTE: From npm Docs:

The main field is a module ID that is the primary entry point to your program. That is, if your package is named foo, and a user installs it, and then does require("foo"), then your main module's exports object will be returned.

Break down the function

Back to the usage mentioned by GitHub README:

require('dotenv').config()

Let’s go to lib/main.js and navigate to the function config in line 76 — 110.

By default, options can be empty, and you can find another function call:

const parsed = parse(
fs.readFileSync(dotenvPath, { encoding }), { debug }
)

Move to another function parse :

In Line 31 — 74, the annotation said the function parse is for parsing a string to an object.

Back to the design aspect, this is an important feature that we’ve missed, considering the simple usage we mentioned before:

SQL_USERNAME=dexter
SQL_PASSWORD=ok

If we can have nested or JSON-like object, it would be easier to read, to extend, like:

SQL_INFO={"USERNAME": "dexter", "PASSWORD": "ok"}

So the function parse uses Regex to operate the string, turn it into an object in JavaScript, and does a good job on our requirements:

  1. Loads a file with a suffix .env.
  2. Parses the file line by line.

Back to function config:

Object.keys(parsed).forEach(function (key) {
if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
process.env[key] = parsed[key]
} else if (debug) {
log(`"${key}" is already defined in \`process.env\` and will not be overwritten`)
}
})

Since the param parsed have stored all the value-to-key as an Object, we can:

  1. Use Object.keys to get all the keys
  2. Use Object.prototype.hasOwnProperty.call(obj, key) to check if the process.env already contain the value.
  3. If can’t found, save the value.

NOTE:

Object.prototype.hasOwnProperty.call(obj, key) is similar to Obj.hasOwnProperty(key), but it will fail when the object is created via Object.create(null) , which means it has a null prototype chain. For more info see here.

Our last mission — Puts the values to process.env according to their secret names, have completed too!

Final Thoughts

Thank you for practicing the code trace with me, it’s quite a pleasant journey.

If you have any questions or issues about the article, just left a comment, you can also find me on Linkedin.

Cheers 🍻

--

--

Dexter Chang

A Software Engineer who loves coffee, snorkeling, and chats 👋.