NPM is not very friendly when working with Node.js in an offline environment. Multiple times I found myself npm installing on an internet machine just to copy node_modules to the offline environment and commit it with the entire project.

For a while npmbox worked for me, but issues like trying to reach the internet would randomly appear and cripple my workflow.

I also tried Sinopia, but couldn’t consistently publish new or updated packages to it.

Enter Yarn

With Yarn I’m able to consistently install packages in an offline environment. Their original blog post is helpful, but I met some edge case it doesn’t cover.

This is my process for using Yarn in an offline environment.

Configuring yarn-offline-mirror

On the internet machine:

yarn config set yarn-offline-mirror ~/yarn-offline-mirror/

On the offline machine:

yarn config set yarn-offline-mirror ~/yarn-offline-mirror/

Note: On the offline machine ~/yarn-offline-mirror/ can also be a shared folder or a git repository.

Creating a new project

On the internet machine:

mkdir new-project/
cd new-project/
yarn add dep1@x.y.z [dep2...]

Then copy new-project/yarn.lock, new-project/package.json and ~/yarn-offline-mirror/ to the offline machine.

(rm -rf new-project/ is ok now.)

On the offline machine:

mkdir new-project/
cp /path/to/imported/{yarn.lock,package.json} new-project/
cp -n /path/to/imported/yarn-offline-mirror/* ~/yarn-offline-mirror/
cd new-project/
yarn --offline

Adding packages to an existing project

On the internet machine:

mkdir new-packages/
cd new-packages/
yarn add dep1@x.y.z [dep2...]

Then copy new-packages/yarn.lock, new-packages/package.json and ~/yarn-offline-mirror/ to the offline machine.

On the offline machine:

  1. Append the imported yarn.lock to the existing yarn.lock. I found that without this step Yarn sometimes fails to find the new packages in the offline cache:

    cat /path/to/imported/yarn.lock >> existing-project/yarn.lock
    
  2. Update package.json with the new dependencies. This means merging both dependencies fields together. An ugly one-liner I tend to use:

    cat existing-project/package.json <(cat existing-project/package.json /path/to/imported/package.json |
            jq '.dependencies' |
            jq -s 'add | {dependencies: .}') |
        jq -s add |
        sponge existing-project/package.json
    
  3. Update yarn-offline-mirror:

    cp -n /path/to/imported/yarn-offline-mirror/* ~/yarn-offline-mirror/
    
  4. Install the new packages. This step also fixes existing-project/yarn.lock after we messed with it in step 1.

    cd existing-project/
    yarn --offline
    

I found the if I skip steps 1 and 2, and in step 4 I do yarn add --offline <dep1> [<dep2>...] then Yarn might not find the new packages in the cache and fail. This bug still exists in version 1.5.1. I believe it is related to these GitHub issues: [1][2][3][4][5][6].

Installing packages globally

Yarn discourages using global packages, so it’s hard by design to install them.

  1. Find out where is the global installation location [7]:

    yarn global bin
    

    (Or set it with yarn config set prefix <file_path>)

  2. Add it to your path. E.g.:

    echo 'export PATH=$PATH:'"$(yarn global bin)" >> ~/.bashrc
    source ~/.bashrc # reload
    
  3. Similar to Creating a new project, but with a few subtle differences:

    On the internet machine:

    mkdir new-cli/
    cd new-cli/
    yarn add cli1@x.y.z [cli2...]
    

    Then copy new-cli/yarn.lock and ~/yarn-offline-mirror/ to the offline machine.

    (rm -rf new-cli/ is ok now.)

    On the offline machine:

    cp /path/to/imported/yarn.lock .
    cp -n /path/to/imported/yarn-offline-mirror/* ~/yarn-offline-mirror/
    yarn global add --offline cli1@x.y.z [cli2...]
    rm -f ./yarn.lock
    

    Note: In this context we don’t care about package.json. We only need to make sure that Yarn can find yarn.lock in the current directory and that ~/yarn-offline-mirror/ has the needed dependencies.

This is not a silver bullet

There are probably more edge cases I still haven’t met. I wish the docs around yarn.lock were better, Because playing with this file solved most of my problems.

For example when adding packages to an existing project, I still don’t understand why appending the new lock file to the existing one solves the resolution problem. According to the docs this step is unnecessary. And sometimes it is, but some other times it isn’t.

Overall I’m very satisfied with Yarn and I have a feeling it is only going to get better.