Skip to content

Instantly share code, notes, and snippets.

@indexzero
Last active July 17, 2025 14:24
Show Gist options
  • Save indexzero/d051412d3fca62a89a3123398b0096a5 to your computer and use it in GitHub Desktop.
Save indexzero/d051412d3fca62a89a3123398b0096a5 to your computer and use it in GitHub Desktop.
The "as simple as possible npm-init.js file with zero dependencies"

How does npm init work anyway?

A question on the OpenJS Foundation Slack made me wonder about this enough to go down the rabbit hole

We must go deeper, yes?

npm config references a init-module which is a path to a JS file that is passed to init-package-json which in turn passes it to promzard which uses the raw Module pattern to load the contents of the file into node/v8

To test my hypothesis of how the JS interface of the exports from these .npm-init files work I made this example gist but with:

  • Zero dependencies (semver, etc) which makes it very unsafe btw
  • The original exports.type removed

The latter did the trick for what was asked!

npm init --init-module="/path/to/the/file/here/.npm-init"
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (wtf) 
version: (1.0.0) 
description: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /path/to/$(cwd)/package.json:

{
  "name": "wtf",
  "version": "1.0.0",
  "description": "",
  "license": "ISC",
  "author": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}


Is this OK? (yes) 
/* globals config, dirname, package, basename, yes, prompt */
const fs = require('fs/promises')
const path = require('path')
// more popular packages should go here, maybe?
const testPkgs = [
'coco',
'coffee-script',
'expresso',
'jasmine',
'jest',
'mocha',
'streamline',
'tap',
]
const isTestPkg = p => testPkgs.includes(p)
const invalid = (msg) => Object.assign(new Error(msg), { notValid: true })
const readDeps = (test, excluded) => async () => {
const dirs = await fs.readdir('node_modules').catch(() => null)
if (!dirs) {
return
}
const deps = {}
for (const dir of dirs) {
if (dir.match(/^\./) || test !== isTestPkg(dir) || excluded[dir]) {
continue
}
const dp = path.join(dirname, 'node_modules', dir, 'package.json')
const p = await fs.readFile(dp, 'utf8').then((d) => JSON.parse(d)).catch(() => null)
if (!p || !p.version || p?._requiredBy?.some((r) => r === '#USER')) {
continue
}
deps[dir] = config.get('save-exact') ? p.version : config.get('save-prefix') + p.version
}
return deps
}
const getConfig = (key) => {
// dots take precedence over dashes
const def = config?.defaults?.[`init.${key}`]
const val = config.get(`init.${key}`)
return (val !== def && val) ? val : config.get(`init-${key.replace(/\./g, '-')}`)
}
const getName = () => {
const rawName = package.name || basename
let name = rawName
.replace(/^node-|[.-]js$/g, '')
.replace(/\s+/g, ' ')
.replace(/ /g, '-')
.toLowerCase()
return name
}
const name = getName()
exports.name = yes ? name : prompt('package name', name, (data) => {
return data
})
const version = package.version || getConfig('version') || '1.0.0'
exports.version = yes ? version : prompt('version', version, (v) => {
return v
})
if (!package.description) {
exports.description = yes ? '' : prompt('description')
}
if (!package.main) {
exports.main = async () => {
const files = await fs.readdir(dirname)
.then(list => list.filter((f) => f.match(/\.js$/)))
.catch(() => [])
let index
if (files.includes('index.js')) {
index = 'index.js'
} else if (files.includes('main.js')) {
index = 'main.js'
} else if (files.includes(basename + '.js')) {
index = basename + '.js'
} else {
index = files[0] || 'index.js'
}
return yes ? index : prompt('entry point', index)
}
}
if (!package.bin) {
exports.bin = async () => {
try {
const d = await fs.readdir(path.resolve(dirname, 'bin'))
// just take the first js file we find there, or nada
let r = d.find(f => f.match(/\.js$/))
if (r) {
r = `bin/${r}`
}
return r
} catch {
// no bins
}
}
}
exports.directories = async () => {
const dirs = await fs.readdir(dirname)
const res = dirs.reduce((acc, d) => {
if (/^examples?$/.test(d)) {
acc.example = d
} else if (/^tests?$/.test(d)) {
acc.test = d
} else if (/^docs?$/.test(d)) {
acc.doc = d
} else if (d === 'man') {
acc.man = d
} else if (d === 'lib') {
acc.lib = d
}
return acc
}, {})
return Object.keys(res).length === 0 ? undefined : res
}
if (!package.dependencies) {
exports.dependencies = readDeps(false, package.devDependencies || {})
}
if (!package.devDependencies) {
exports.devDependencies = readDeps(true, package.dependencies || {})
}
// MUST have a test script!
if (!package.scripts) {
const scripts = package.scripts || {}
const notest = 'echo "Error: no test specified" && exit 1'
exports.scripts = async () => {
const d = await fs.readdir(path.join(dirname, 'node_modules')).catch(() => [])
// check to see what framework is in use, if any
let command
if (!scripts.test || scripts.test === notest) {
const commands = {
tap: 'tap test/*.js',
expresso: 'expresso test',
mocha: 'mocha',
}
for (const [k, v] of Object.entries(commands)) {
if (d.includes(k)) {
command = v
}
}
}
const promptArgs = ['test command', (t) => t || notest]
if (command) {
promptArgs.splice(1, 0, command)
}
scripts.test = yes ? command || notest : prompt(...promptArgs)
return scripts
}
}
if (!package.repository) {
exports.repository = async () => {
const gitConfigPath = path.resolve(dirname, '.git', 'config')
const gconf = await fs.readFile(gitConfigPath, 'utf8').catch(() => '')
const lines = gconf.split(/\r?\n/)
let url
const i = lines.indexOf('[remote "origin"]')
if (i !== -1) {
url = lines[i + 1]
if (!url.match(/^\s*url =/)) {
url = lines[i + 2]
}
if (!url.match(/^\s*url =/)) {
url = null
} else {
url = url.replace(/^\s*url = /, '')
}
}
if (url && url.match(/^git@github.com:/)) {
url = url.replace(/^git@github.com:/, 'https://github.com/')
}
return yes ? url || '' : prompt('git repository', url || undefined)
}
}
if (!package.keywords) {
exports.keywords = yes ? '' : prompt('keywords', (data) => {
if (!data) {
return
}
if (Array.isArray(data)) {
data = data.join(' ')
}
if (typeof data !== 'string') {
return data
}
return data.split(/[\s,]+/)
})
}
if (!package.author) {
const authorName = getConfig('author.name')
exports.author = authorName
? {
name: authorName,
email: getConfig('author.email'),
url: getConfig('author.url'),
}
: yes ? '' : prompt('author')
}
const license = package.license || getConfig('license') || 'ISC'
exports.license = yes ? license : prompt('license', license, (data) => {
return data
})
// Only include private field if it already exists or if explicitly set in config
const configPrivate = getConfig('private')
if (package.private !== undefined || configPrivate !== undefined) {
if (package.private !== undefined) {
exports.private = package.private
} else if (!config.isDefault || !config.isDefault('init-private')) {
exports.private = configPrivate
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment