diff --git a/README.md b/README.md index c4e9f2d..257d5d4 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,20 @@ Convenience static method like `load` but for calling `normalize` --- +--- + +### `async PackageJson.prepare()` + +Like `load` but intended for reading package.json files before publish. + +--- + +### **static** `async PackageJson.prepare(path)` + +Convenience static method like `load` but for calling `prepare` + +--- + ### `PackageJson.update(content)` Updates the contents of the `package.json` with the `content` provided. diff --git a/lib/index.js b/lib/index.js index 75edf89..34e415b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -23,43 +23,96 @@ const knownKeys = new Set([ ]) class PackageJson { + static normalizeSteps = Object.freeze([ + '_id', + '_attributes', + 'bundledDependencies', + 'bundleDependencies', + 'optionalDedupe', + 'scripts', + 'funding', + 'bin', + ]) + + static prepareSteps = Object.freeze([ + '_attributes', + 'bundledDependencies', + 'bundleDependencies', + 'gypfile', + 'serverjs', + 'scriptpath', + 'authors', + 'readme', + 'mans', + 'binDir', + 'gitHead', + 'fillTypes', + 'normalizeData', + 'binRefs', + ]) + // default behavior, just loads and parses static async load (path) { return await new PackageJson(path).load() } + // read-package-json compatible behavior + static async prepare (path, opts) { + return await new PackageJson(path).prepare(opts) + } + // read-package-json-fast compatible behavior - static async normalize (path) { - return await new PackageJson(path).normalize() + static async normalize (path, opts) { + return await new PackageJson(path).normalize(opts) } #filename #path #manifest = {} #readFileContent = '' + #fromIndex = false constructor (path) { this.#path = path this.#filename = resolve(path, 'package.json') } - async load () { + async load (parseIndex) { + let parseErr try { this.#readFileContent = await readFile(this.#filename, 'utf8') } catch (err) { err.message = `Could not read package.json: ${err}` - throw err + if (!parseIndex) { + throw err + } + parseErr = err + } + + if (parseErr) { + const indexFile = resolve(this.#path, 'index.js') + let indexFileContent + try { + indexFileContent = await readFile(indexFile, 'utf8') + } catch (err) { + throw parseErr + } + try { + this.#manifest = fromComment(indexFileContent) + } catch (err) { + throw parseErr + } + this.#fromIndex = true + return this } try { - this.#manifest = - parseJSON(this.#readFileContent) + this.#manifest = parseJSON(this.#readFileContent) } catch (err) { err.message = `Invalid package.json: ${err}` throw err } - return this } @@ -67,6 +120,10 @@ class PackageJson { return this.#manifest } + get path () { + return this.#path + } + update (content) { // validates both current manifest and content param const invalidContent = @@ -94,6 +151,9 @@ class PackageJson { } async save () { + if (this.#fromIndex) { + throw new Error('No package.json to save to') + } const { [Symbol.for('indent')]: indent, [Symbol.for('newline')]: newline, @@ -111,11 +171,42 @@ class PackageJson { } } - async normalize () { + async normalize (opts = {}) { + if (!opts.steps) { + opts.steps = this.constructor.normalizeSteps + } await this.load() - await normalize(this) + await normalize(this, opts) + return this + } + + async prepare (opts = {}) { + if (!opts.steps) { + opts.steps = this.constructor.prepareSteps + } + await this.load(true) + await normalize(this, opts) return this } } +// /**package { "name": "foo", "version": "1.2.3", ... } **/ +function fromComment (data) { + data = data.split(/^\/\*\*package(?:\s|$)/m) + + if (data.length < 2) { + throw new Error('File has no package in comments') + } + data = data[1] + data = data.split(/\*\*\/$/m) + + if (data.length < 2) { + throw new Error('File has no package in comments') + } + data = data[0] + data = data.replace(/^\s*\*/mg, '') + + return parseJSON(data) +} + module.exports = PackageJson diff --git a/lib/normalize.js b/lib/normalize.js index 85d5c8b..bc101cd 100644 --- a/lib/normalize.js +++ b/lib/normalize.js @@ -1,68 +1,284 @@ +const fs = require('fs/promises') +const { glob } = require('glob') const normalizePackageBin = require('npm-normalize-package-bin') +const normalizePackageData = require('normalize-package-data') +const path = require('path') -const normalize = async (pkg) => { +const normalize = async (pkg, { strict, steps }) => { const data = pkg.content + const scripts = data.scripts || {} - // remove _attributes - for (const key in data) { - if (key.startsWith('_')) { - delete pkg.content[key] + // remove attributes that start with "_" + if (steps.includes('_attributes')) { + for (const key in data) { + if (key.startsWith('_')) { + delete pkg.content[key] + } } } - // _id - if (data.name && data.version) { - data._id = `${data.name}@${data.version}` + // build the "_id" attribute + if (steps.includes('_id')) { + if (data.name && data.version) { + data._id = `${data.name}@${data.version}` + } } - // bundleDependencies - if (data.bundleDependencies === undefined && data.bundledDependencies !== undefined) { - data.bundleDependencies = data.bundledDependencies + // fix bundledDependencies typo + if (steps.includes('bundledDependencies')) { + if (data.bundleDependencies === undefined && data.bundledDependencies !== undefined) { + data.bundleDependencies = data.bundledDependencies + } + delete data.bundledDependencies } - delete data.bundledDependencies - const bd = data.bundleDependencies - if (bd === true) { - data.bundleDependencies = Object.keys(data.dependencies || {}) - } else if (bd && typeof bd === 'object') { - if (!Array.isArray(bd)) { - data.bundleDependencies = Object.keys(bd) + // expand "bundleDependencies: true or translate from object" + if (steps.includes('bundleDependencies')) { + const bd = data.bundleDependencies + if (bd === true) { + data.bundleDependencies = Object.keys(data.dependencies || {}) + } else if (bd && typeof bd === 'object') { + if (!Array.isArray(bd)) { + data.bundleDependencies = Object.keys(bd) + } + } else { + delete data.bundleDependencies } - } else { - data.bundleDependencies = [] } // it was once common practice to list deps both in optionalDependencies and // in dependencies, to support npm versions that did not know about // optionalDependencies. This is no longer a relevant need, so duplicating // the deps in two places is unnecessary and excessive. - if (data.dependencies && - data.optionalDependencies && typeof data.optionalDependencies === 'object') { - for (const name in data.optionalDependencies) { - delete data.dependencies[name] + if (steps.includes('optionalDedupe')) { + if (data.dependencies && + data.optionalDependencies && typeof data.optionalDependencies === 'object') { + for (const name in data.optionalDependencies) { + delete data.dependencies[name] + } + if (!Object.keys(data.dependencies).length) { + delete data.dependencies + } } - if (!Object.keys(data.dependencies).length) { - delete data.dependencies + } + + // add "install" attribute if any "*.gyp" files exist + if (steps.includes('gypfile')) { + if (!scripts.install && !scripts.preinstall && data.gypfile !== false) { + const files = await glob('*.gyp', { cwd: pkg.path }) + if (files.length) { + scripts.install = 'node-gyp rebuild' + data.scripts = scripts + data.gypfile = true + } + } + } + + // add "start" attribute if "server.js" exists + if (steps.includes('serverjs') && !scripts.start) { + try { + await fs.access(path.join(pkg.path, 'server.js')) + scripts.start = 'node server.js' + data.scripts = scripts + } catch { + // do nothing } } - // scripts - if (typeof data.scripts === 'object') { - for (const name in data.scripts) { - if (typeof data.scripts[name] !== 'string') { - delete data.scripts[name] + // strip "node_modules/.bin" from scripts entries + if (steps.includes('scripts') || steps.includes('scriptpath')) { + const spre = /^(\.[/\\])?node_modules[/\\].bin[\\/]/ + if (typeof data.scripts === 'object') { + for (const name in data.scripts) { + if (typeof data.scripts[name] !== 'string') { + delete data.scripts[name] + } else if (steps.includes('scriptpath')) { + data.scripts[name] = data.scripts[name].replace(spre, '') + } } + } else { + delete data.scripts } - } else { - delete data.scripts } - // funding - if (data.funding && typeof data.funding === 'string') { - data.funding = { url: data.funding } + if (steps.includes('funding')) { + if (data.funding && typeof data.funding === 'string') { + data.funding = { url: data.funding } + } + } + + // populate "authors" attribute + if (steps.includes('authors') && !data.contributors) { + try { + const authorData = await fs.readFile(path.join(pkg.path, 'AUTHORS'), 'utf8') + const authors = authorData.split(/\r?\n/g) + .map(line => line.replace(/^\s*#.*$/, '').trim()) + .filter(line => line) + data.contributors = authors + } catch { + // do nothing + } + } + + // populate "readme" attribute + if (steps.includes('readme') && !data.readme) { + const mdre = /\.m?a?r?k?d?o?w?n?$/i + const files = await glob('{README,README.*}', { cwd: pkg.path, nocase: true, mark: true }) + let readmeFile + for (const file of files) { + // don't accept directories. + if (!file.endsWith(path.sep)) { + if (file.match(mdre)) { + readmeFile = file + break + } + if (file.endsWith('README')) { + readmeFile = file + } + } + } + if (readmeFile) { + const readmeData = await fs.readFile(path.join(pkg.path, readmeFile), 'utf8') + data.readme = readmeData + data.readmeFilename = readmeFile + } + } + + // expand directories.man + if (steps.includes('mans') && !data.man && data.directories?.man) { + const manDir = data.directories.man + const cwd = path.resolve(pkg.path, manDir) + const files = await glob('**/*.[0-9]', { cwd }) + data.man = files.map(man => + path.relative(pkg.path, path.join(cwd, man)).split(path.sep).join('/') + ) + } + + if (steps.includes('bin') || steps.includes('binDir') || steps.includes('binRefs')) { + normalizePackageBin(data) } - // bin - normalizePackageBin(data) + // expand "directories.bin" + if (steps.includes('binDir') && data.directories?.bin) { + const binsDir = path.resolve(pkg.path, path.join('.', path.join('/', data.directories.bin))) + const bins = await glob('**', { cwd: binsDir }) + data.bin = bins.reduce((acc, binFile) => { + if (binFile && !binFile.startsWith('.')) { + const binName = path.basename(binFile) + acc[binName] = path.join(data.directories.bin, binFile) + } + return acc + }, {}) + // *sigh* + normalizePackageBin(data) + } + + // populate "gitHead" attribute + if (steps.includes('gitHead') && !data.gitHead) { + let head + try { + head = await fs.readFile(path.resolve(pkg.path, '.git/HEAD'), 'utf8') + } catch (err) { + // do nothing + } + let headData + if (head) { + if (head.startsWith('ref: ')) { + const headRef = head.replace(/^ref: /, '').trim() + const headFile = path.resolve(pkg.path, '.git', headRef) + try { + headData = await fs.readFile(headFile, 'utf8') + headData = headData.replace(/^ref: /, '').trim() + } catch (err) { + // do nothing + } + if (!headData) { + const packFile = path.resolve(pkg.path, '.git/packed-refs') + try { + let refs = await fs.readFile(packFile, 'utf8') + if (refs) { + refs = refs.split('\n') + for (let i = 0; i < refs.length; i++) { + const match = refs[i].match(/^([0-9a-f]{40}) (.+)$/) + if (match && match[2].trim() === headRef) { + headData = match[1] + break + } + } + } + } catch { + // do nothing + } + } + } else { + headData = head.trim() + } + } + if (headData) { + data.gitHead = headData + } + } + + // populate "types" attribute + if (steps.includes('fillTypes')) { + const index = data.main || 'index.js' + + if (typeof index !== 'string') { + throw new TypeError('The "main" attribute must be of type string.') + } + + // TODO exports is much more complicated than this in verbose format + // We need to support for instance + + // "exports": { + // ".": [ + // { + // "default": "./lib/npm.js" + // }, + // "./lib/npm.js" + // ], + // "./package.json": "./package.json" + // }, + // as well as conditional exports + + // if (data.exports && typeof data.exports === 'string') { + // index = data.exports + // } + + // if (data.exports && data.exports['.']) { + // index = data.exports['.'] + // if (typeof index !== 'string') { + // } + // } + const extless = path.join(path.dirname(index), path.basename(index, path.extname(index))) + const dts = `./${extless}.d.ts` + const hasDTSFields = 'types' in data || 'typings' in data + if (!hasDTSFields) { + try { + await fs.access(path.join(pkg.path, dts)) + data.types = dts.split(path.sep).join('/') + } catch { + // do nothing + } + } + } + + // "normalizeData" from read-package-json + if (steps.includes('normalizeData')) { + normalizePackageData(data, strict) + } + + // Warn if the bin references don't point to anything. This might be better + // in normalize-package-data if it had access to the file path. + if (steps.includes('binRefs') && data.bin instanceof Object) { + for (const key in data.bin) { + const binPath = path.resolve(pkg.path, data.bin[key]) + try { + await fs.access(binPath) + } catch { + delete data.bin[key] + } + } + } } module.exports = normalize diff --git a/test/normalize.js b/test/normalize.js index dca74d6..439a161 100644 --- a/test/normalize.js +++ b/test/normalize.js @@ -62,7 +62,7 @@ t.test('clean up bundleDependencies', async t => { dependencies: { a: '1.2.3' }, }), })) - t.strictSame(content.bundleDependencies, []) + t.has(content, { bundleDependencies: undefined }) }) t.test('handle bundleDependencies object', async t => { @@ -83,7 +83,7 @@ t.test('clean up scripts', async t => { scripts: 1234, }), })) - t.notHasStrict(content, 'scripts') + t.has(content, { scripts: undefined }) }) t.test('delete non-string script targets', async t => { @@ -119,14 +119,14 @@ t.test('cleanup bins', async t => { const { content } = await pkg.normalize(t.testdir({ 'package.json': JSON.stringify({ bin: 'y' }), })) - t.notHasStrict(content, 'bin') + t.has(content, { bin: undefined }) }) t.test('remove non-object bin', async t => { const { content } = await pkg.normalize(t.testdir({ 'package.json': JSON.stringify({ bin: 1234 }), })) - t.notHasStrict(content, 'bin') + t.has(content, { bin: undefined }) }) t.test('remove non-string bin values', async t => { @@ -153,7 +153,7 @@ t.test('dedupe optional deps out of regular deps', async t => { }, }), })) - t.notHasStrict(content, 'dependencies') + t.has(content, { dependencies: undefined }) t.strictSame(content.optionalDependencies, { whowins: '1.2.3-optional' }) }) @@ -181,7 +181,7 @@ t.test('dedupe optional deps out of regular deps', async t => { }, }), })) - t.notHasStrict(content, 'dependencies') + t.has(content, { dependencies: undefined }) t.strictSame(content.optionalDependencies, { whowins: '1.2.3-optional' }) }) }) @@ -298,7 +298,7 @@ t.test('strip _fields', async t => { _lodash: true, }), })) - t.notHasStrict(content, '_lodash') + t.has(content, { _lodash: undefined }) }) // For now this is just checking one of the many side effects of @@ -309,5 +309,25 @@ t.test('normalize bin', async t => { bin: false, }), })) - t.notHasStrict(content, 'bin') + t.has(content, { bin: undefined }) +}) + +t.test('skipping steps', async t => { + const packageJson = { + _lodash: true, + dependencies: { a: '' }, + optionalDependencies: { a: '' }, + bundledDependencies: true, + funding: 'just a string', + scripts: { test: './node_modules/.bin/test' }, + bin: { a: ['invalid array'] }, + } + const { content } = await pkg.normalize(t.testdir({ + 'package.json': JSON.stringify(packageJson), + }), { steps: [] }) + t.strictSame(content, packageJson) + t.has(content, { + bundleDependencies: undefined, + _id: undefined, + }) }) diff --git a/test/prepare.js b/test/prepare.js new file mode 100644 index 0000000..c73db79 --- /dev/null +++ b/test/prepare.js @@ -0,0 +1,504 @@ +const t = require('tap') +const pkg = require('../') + +t.test('errors for bad/missing data', async t => { + t.test('invalid version', t => + t.rejects(pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + version: 'not semver', + }), + })), { message: 'Invalid version' })) + + t.test('non-string main entry', t => + t.rejects(pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + main: ['this is not a thing'], + }), + })), { name: 'TypeError' })) +}) + +t.test('strip underscores', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + name: 'underscore', + version: '1.2.3', + _lodash: true, + }), + })) + t.has(content, { _lodash: undefined }) +}) + +t.test('bin', t => { + t.test('non-string', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + bin: { key: {} }, + }), + })) + t.has(content, { bin: undefined }) + }) + + t.test('good', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + name: 'bin-test', + bin: './bin/echo', + }), + bin: { echo: '#!/bin/sh\n\necho "hello world"' }, + })) + t.strictSame(content.bin, { 'bin-test': 'bin/echo' }) + }) + + t.test('missing', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + name: 'bin-test', + bin: './bin/missing', + }), + })) + t.strictSame(content.bin, {}) + }) + + t.test('empty', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + name: 'bin-test', + bin: {}, + }), + })) + t.has(content, { bin: undefined }) + }) + + t.test('directories.bin no prefix', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + name: 'bin-test', + directories: { + bin: './bin', + }, + }), + bin: { echo: '#!/bin/sh\n\necho "hello world"' }, + })) + t.strictSame(content.bin, { echo: 'bin/echo' }) + }) + + t.test('directories.bin trim prefix', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + name: 'bin-test', + directories: { + bin: '../../../../../bin', + }, + }), + bin: { echo: '#!/bin/sh\n\necho "hello world"' }, + })) + t.strictSame(content.bin, { echo: 'bin/echo' }) + }) + + t.end() +}) + +t.test('bundleDependencies', t => { + t.test('true', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + dependencies: { a: '' }, + bundleDependencies: true, + }), + })) + t.strictSame(content.bundleDependencies, ['a']) + }) + + // t.test('null', async t => { + // const { content } = await pkg.prepare(t.testdir({ + // 'package.json': JSON.stringify({ + // dependencies: { a: '' }, + // bundleDependencies: null + // }), + // })) + // t.has(content, { bundleDependencies: undefined }) + // }) + + t.test('false', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + dependencies: { a: '' }, + bundleDependencies: false, + }), + })) + t.has(content, { bundleDependencies: undefined }) + }) + + t.test('rename bundledDependencies', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + dependencies: { a: '', b: '' }, + devDependencies: { c: '' }, + bundledDependencies: ['a', 'b', 'c'], + }), + })) + t.has(content, { bundledDependencies: undefined }) + t.strictSame(content.bundleDependencies, ['a', 'b', 'c']) + }) + t.end() +}) + +t.test('server.js', t => { + t.test('adds if missing', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + 'server.js': 'a file that exists', + })) + t.strictSame(content.scripts, { start: 'node server.js' }) + }) + t.test('keeps existing', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + scripts: { + start: 'something else', + }, + }), + 'server.js': 'a file that exists', + })) + t.strictSame(content.scripts, { start: 'something else' }) + }) + t.end() +}) + +t.test('gypfile', t => { + t.test('with install', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + scripts: { install: 'existing script' }, + }), + 'test.gyp': 'a file that exists', + })) + t.strictSame(content.scripts.install, 'existing script') + }) + t.test('with preinstall', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + scripts: { preinstall: 'existing script' }, + }), + 'test.gyp': 'a file that exists', + })) + t.has(content.scripts, { install: undefined }) + t.strictSame(content.scripts, { preinstall: 'existing script' }) + }) + t.test('no other scripts', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + 'test.gyp': 'a file that exists', + })) + t.strictSame(content.scripts, { install: 'node-gyp rebuild' }) + }) + t.end() +}) + +t.test('authors', t => { + t.test('contributors already exists', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + contributors: 'exists', + }), + AUTHORS: 'name from authors file', + })) + t.strictSame(content.contributors, 'exists') + }) + t.test('contributors does not exist', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + }), + AUTHORS: 'name from authors file', + })) + t.strictSame(content.contributors, [{ name: 'name from authors file' }]) + }) + t.end() +}) + +t.test('readme', t => { + t.test('already exists', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + readme: 'a file that exists', + }), + 'README.md': 'readme file', + })) + t.strictSame(content.readme, 'a file that exists') + }) + + t.test('no readme at all', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + })) + t.match(content.readme, /No README/) + }) + + t.test('finds .md file', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + 'README.md': 'readme file', + })) + t.strictSame(content.readme, 'readme file') + }) + + t.test('finds README file', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + README: 'readme file', + })) + t.strictSame(content.readme, 'readme file') + }) + + t.test('ignores directory', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + 'README.md': {}, + })) + t.match(content.readme, /No README/) + }) + + t.test('ignores non-md', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + README: 'no extension', + 'README.txt': 'txt file', + })) + t.strictSame(content.readme, 'no extension') + }) + t.end() +}) + +t.test('man', t => { + t.test('resolves directory', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + directories: { man: './man' }, + }), + man: { man1: { 'test.1': 'man test file' } }, + })) + t.strictSame(content.man, ['man/man1/test.1']) + }) + t.end() +}) + +t.test('gitHead', t => { + t.test('HEAD with no ref', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + '.git': { HEAD: 'testgitref' }, + })) + t.strictSame(content.gitHead, 'testgitref') + }) + + t.test('HEAD with ref', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + '.git': { + HEAD: 'ref: testgitref', + testgitref: 'filegitref', + }, + })) + t.strictSame(content.gitHead, 'filegitref') + }) + + t.test('HEAD with valid packed ref', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + '.git': { + HEAD: 'ref: testgitref', + 'packed-refs': `${'a'.repeat(40)} testgitref`, + }, + })) + t.strictSame(content.gitHead, 'a'.repeat(40)) + }) + + t.test('HEAD with empty packed ref', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + '.git': { + HEAD: 'ref: testgitref', + 'packed-refs': '', + }, + })) + t.has(content, { gitHead: undefined }) + }) + + t.test('HEAD with unparseable packed ref', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + '.git': { + HEAD: 'ref: testgitref', + 'packed-refs': 'not sure what this is', + }, + })) + t.has(content, { gitHead: undefined }) + }) + t.end() +}) + +t.test('fillTypes', t => { + t.test('custom main field', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + main: './custom-path.js', + }), + 'custom-path.d.ts': 'a file that exists', + })) + t.strictSame(content.types, './custom-path.d.ts') + }) + + t.test('inferred index.js', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + 'index.d.ts': 'a file that exists', + })) + t.strictSame(content.types, './index.d.ts') + }) + + t.test('subpaths and starting with ./', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + main: './a/b/c.js', + }), + a: { b: { + 'c.d.ts': 'a file that exists', + 'c.js': 'another file that exists', + } }, + })) + t.strictSame(content.types, './a/b/c.d.ts') + }) + + t.test('existing types', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({ + types: '@types/express', + }), + 'index.d.ts': 'a file that exists', + })) + t.strictSame(content.types, '@types/express') + }) + + t.test('no types present', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + })) + t.has(content, { type: undefined }) + }) + + // https://nodejs.org/api/esm.html#esm_writing_dual_packages_while_avoiding_or_minimizing_hazards + + t.skip('handles esm modules', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + 'exports.json': JSON.stringify({ + type: 'module', + exports: { + '.': './a/b/c.js', + './a': './a.mjs', + }, + }), + a: { b: { + 'c.d.ts': 'a file that exists', + 'c.js': 'another file that exists', + } }, + })) + t.strictSame(content.types, './a/b/c/d.ts') + }) + t.skip('handles esm modules with sugared exports', async t => { + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify({}), + 'sugar.json': JSON.stringify({ + exports: './a/b.js', + }), + a: { + 'b.d.ts': 'a file that exists', + 'b.js': 'another file that exists', + }, + })) + t.strictSame(content.types, './a/b/c/d.ts') + }) + t.end() +}) + +t.test('skipping steps', async t => { + const packageJson = { + scripts: { test: './node_modules/.bin/test' }, + main: './custom-path.js', + bin: { + foo: ['invalid'], + bar: './nonexistent', + }, + directories: { + man: './man', + bin: './bin', + }, + } + const { content } = await pkg.prepare(t.testdir({ + 'package.json': JSON.stringify(packageJson), + 'build.gyp': '', + 'server.js': '', + AUTHORS: 'me', + man: { man1: { 'test.1': 'man test file' } }, + bin: { echo: '#!/bin/sh\n\necho "hello world"' }, + '.git': { HEAD: 'testgitref' }, + 'custom-path.d.ts': 'a file that exists', + }), { steps: [] }) + t.strictSame(content, packageJson) + t.has(content, { + // _id and normalizeData both do this one + _id: undefined, + authors: undefined, + bundleDependencies: undefined, + man: undefined, + readme: undefined, + gitHead: undefined, + types: undefined, + }) + t.has(content.scripts, { + install: undefined, + start: undefined, + }) +}) + +t.test('parseIndex', t => { + t.test('no files at all', t => + t.rejects(pkg.prepare(t.testdir({})), { code: 'ENOENT', message: /package.json/ })) + + t.test('index.js present but empty', t => + t.rejects(pkg.prepare(t.testdir({ + 'index.js': 'no comments here', + })), { code: 'ENOENT', message: /package.json/ })) + + t.test('index.js present but invalid', t => + t.rejects(pkg.prepare(t.testdir({ + 'index.js': `console.log("I don't close my comment") +/**package +{ +}`, + })), { code: 'ENOENT', message: /package.json/ })) + + t.test('parseable index.js', async t => { + const parsed = await pkg.prepare(t.testdir({ + 'index.js': `console.log('i am a package!') +/**package +{ + "name": "from-index", + "version": "1.0.0", + "description": "Package that is just an index.js" +} +**/`, + })) + t.strictSame(parsed.content, { + _id: 'from-index@1.0.0', + name: 'from-index', + version: '1.0.0', + description: 'Package that is just an index.js', + readme: 'ERROR: No README data found!', + }) + await t.rejects(parsed.save(), { + message: /No package.json/, + }) + }) + t.end() +})