Skip to main content

Smuggling .gitignore, .npmrc and friends in npm packages

· 5 min read
John Reilly
OSS Engineer - TypeScript, Azure, React, Node.js, .NET

I recently needed to include a number of .gitignore and an .npmrc files in an npm package. I was surprised to find that the npm publish command strips these out of the published package by default. As a consequence, This broke my package, and so I needed to find a way to get round this shortcoming.

I ended up using zipping and unzipping with postinstall and prepare scripts to include these files into my npm package.

title image reading "Smuggling dotfiles in npm packages" with the Node.js and npm logos

This post shows how to use zipping and unzipping with postinstall and prepare scripts to include these files into your npm package.

A little backstory

I'm currently beavering away on a "create-*-app" tool that generates new projects from a number of available templates. That tool takes the form of a CLI tool built with TypeScript, published as a package to an npm registry and consumed with npx. Significantly, the templates that ship with the CLI take the form of a templates folder in the package, and the folders in those templates include .npmrc and .gitignore files; which are key to the functionality of the templates.

When publishing my npm package, I discovered that the .npmrc and .gitignore files in subfolders were being stripped from the package. After a little research, I happened upon this GitHub issue about npm which describes some of the behaviour I was seeing. After a touch more digging, I came to understand that this behaviour is a result of npm treating the .gitignore and .npmrc files as configuration files rather than part of the package's intended content.

However, given these files are essential to the templates' functionality, I needed to find a way to include them in the package.

I mused with explicitly including the specific files in the files section of the package.json file, but this would have been a maintenance headache. I wanted a more automated solution. Given that I have a single "special" folder called templates that contains all the templates, I pondered whether I could zip the folder on publish and unzip it on install. This would allow me to include the .gitignore and .npmrc files in the templates and have them copied into the new project when the template was used. And if there was another other curious behaviour around publishing, this solution should cover that too.

prepare and postinstall scripts

I achieved this by using prepare and postinstall scripts in the package.json file.

The prepare and postinstall scripts are two of the lifecycle scripts that npm runs when installing a package. The prepare script runs before the package is packaged and published, and the postinstall script runs after the package is installed. I opted to use these scripts to zip and unzip the templates folder in my package.

I performed the actual zipping and unzipping with some Node.js scripts. We'll look into the implementation of these scripts in a moment, but first please note the scripts we added to the package.json file:

  "scripts": {
"postinstall": "node ./scripts/postinstall.js",
"prepare": "node ./scripts/prepare.js"
},

These scripts contain the paths to the Node.js scripts that perform the zipping and unzipping. The postinstall script runs after the package is installed, and the prepare script runs before the package is packaged and published.

When it comes to zipping and unzipping, I used the adm-zip package. This package provides a simple API for zipping and unzipping files and folders.

prepare.js

We'll first look at the prepare.js script. This script zips the templates folder in the package into a templates.zip file. The script then writes the zip file to the package's root directory.

import AdmZip from 'adm-zip';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';

function packTemplates() {
console.log('prepare running - packing templates');

const templatesZipPath = fileURLToPath(
new URL('../templates.zip', import.meta.url),
);
const templatesDir = fileURLToPath(new URL('../templates', import.meta.url));

const zip = new AdmZip();

console.log(`removing existing ${templatesZipPath}`);
fs.rmSync(templatesZipPath, {
force: true,
});

console.log(`adding ${templatesDir} to zip file`);
zip.addLocalFolder(templatesDir);

console.log(`writing zip to ${templatesZipPath}`);
zip.writeZip(templatesZipPath);
}

packTemplates();

It also removes any existing templates.zip file in the package's root directory before creating a new one. This is to ensure that the zip file is always up to date.

postinstall.js

Now we'll look at the postinstall.js script. This script unzips the templates.zip file in the package into a templates folder. The script then writes the unzipped folder to the package's root directory.

import AdmZip from 'adm-zip';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';

function extractTemplates() {
console.log('postinstall running - extracting templates');
const templatesZipPath = fileURLToPath(
new URL('../templates.zip', import.meta.url),
);
const templatesDir = fileURLToPath(new URL('../templates', import.meta.url));

let templatesExistsAlready = true;
try {
fs.accessSync(templatesDir);
} catch {
templatesExistsAlready = false;
}

if (templatesExistsAlready) {
console.log('templates already extracted');
return;
}

console.log(`extracting from ${templatesZipPath} to ${templatesDir}`);

const extractZip = new AdmZip(templatesZipPath);
extractZip.extractAllTo(templatesDir, /* overwrite */ false);

console.log('templates extracted');
}

extractTemplates();

You'll notice that the script checks whether the templates folder already exists before unzipping the templates.zip file. This is to ensure that the folder is only unzipped once.

Conclusion

So here we have a method for including .gitignore and .npmrc files in an npm package. By using zipping and unzipping with postinstall and prepare scripts, we can include these files in the package and have them copied into the new project when the package is installed.

My example is a templates folder - yours could be anything. And likewise if you have other files that are being stripped from your package, you could use this method to include them too.