├── .prettierignore ├── .npmignore ├── .prettierrc.json ├── .husky └── pre-commit ├── .gitattributes ├── src ├── errors.ts ├── pkg.ts ├── index.ts ├── scripts.ts ├── bin.ts ├── index.test.ts ├── fields.ts └── bin.test.ts ├── .github ├── dependabot.yml └── workflows │ └── index.yaml ├── .eslintrc.cjs ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── jest.config.mjs └── tsconfig.json /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | tsconfig.json 3 | dist 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.test.* 3 | src 4 | tsconfig.json 5 | jest.config.mjs 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # https://github.com/prettier/prettier/issues/7825 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export const doesNotExistError = (field: string) => `${field} path doesn't exist in package.json`; 2 | export const mustBeRelativeError = (field: string) => `${field} must use a relative path for value`; 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: monthly 7 | - package-ecosystem: npm 8 | directory: '/' 9 | schedule: 10 | interval: monthly 11 | -------------------------------------------------------------------------------- /src/pkg.ts: -------------------------------------------------------------------------------- 1 | import { PackageJson } from 'type-fest'; 2 | 3 | export type Pkg = PackageJson & { [key: string]: unknown }; 4 | 5 | export function isObject(value: unknown): value is { [key: string]: string } { 6 | return typeof value === 'object' && value !== null; 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | env: { 4 | es2020: true, 5 | node: true, 6 | 'jest/globals': true, 7 | }, 8 | plugins: ['@typescript-eslint', 'jest'], 9 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | sourceType: 'module', 13 | }, 14 | ignorePatterns: ['dist'], 15 | rules: {}, 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/index.yaml: -------------------------------------------------------------------------------- 1 | name: Index 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | node: [18] 10 | os: [ubuntu-latest, windows-latest, macos-latest] 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: actions/setup-node@v6 14 | with: 15 | node-version: ${{ matrix.node }} 16 | - run: npm install 17 | - run: npm test 18 | - run: npm run lint 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { checkFields } from './fields.js'; 4 | import { normalizeScripts } from './scripts.js'; 5 | 6 | export interface Options { 7 | bin?: string[]; 8 | fields?: string[]; 9 | } 10 | 11 | // Main function 12 | export function pkgOk(dir: string, { fields = [], bin = [] }: Options = {}) { 13 | const pkgPath = join(dir, 'package.json'); 14 | const pkg = JSON.parse(readFileSync(pkgPath).toString()); 15 | 16 | // Check files exist in package.json fields and additional fields 17 | const errors = checkFields(pkg, dir, fields); 18 | 19 | if (errors.length) { 20 | throw new Error(errors.join('\n')); 21 | } 22 | 23 | // Normalize line endings for bin scripts and additional scripts 24 | normalizeScripts(pkg, dir, bin); 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # npm lock file 40 | package-lock.json 41 | 42 | # Ignore compiled files 43 | dist 44 | -------------------------------------------------------------------------------- /src/scripts.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs'; 2 | import normalizeNewline from 'normalize-newline'; 3 | import { join } from 'path'; 4 | import { isObject, Pkg } from './pkg.js'; 5 | 6 | function normalize(dir: string, file: string) { 7 | const filename = join(dir, file); 8 | const data = readFileSync(filename, 'utf-8'); 9 | const normalizedData = normalizeNewline(data); 10 | writeFileSync(filename, normalizedData); 11 | } 12 | 13 | function normalizeField(pkg: Pkg, dir: string, field: string) { 14 | const value = pkg[field]; 15 | if (value) { 16 | if (isObject(value)) { 17 | Object.keys(value).forEach((key) => normalize(dir, value[key])); 18 | } else if (typeof value === 'string') { 19 | normalize(dir, value); 20 | } 21 | } 22 | } 23 | 24 | export function normalizeScripts(pkg: Pkg, dir: string, files: string[]) { 25 | normalizeField(pkg, dir, 'bin'); 26 | files.forEach((file) => normalize(dir, file)); 27 | } 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 4.0.0 4 | 5 | - Require Node 18+ 6 | 7 | ## 3.0.0 8 | 9 | - Require node 12+ 10 | - Switch to ESM 11 | - Update dependencies 12 | - Switch to GitHub Actions 13 | - Remove package-lock.json 14 | - Format with Prettier 15 | - Convert to TypeScript 16 | - Add `exports` field 17 | - Use `main` as default branch 18 | - Move files to `src` and compile to `dist` 19 | - Check `main`, `bin`, and `browser` paths are relative 20 | - Support passing a directory to the CLI `pkg-ok some/path` 21 | 22 | ## 2.3.1 23 | 24 | - Change CLI parser ([meow](https://github.com/sindresorhus/meow)) 25 | 26 | ## 2.3.0 27 | 28 | - Add `browser` field 29 | 30 | ## 2.2.0 31 | 32 | - Add `types` which is synonymous with `typings` 33 | 34 | ## 2.1.0 35 | 36 | - Use `pkg-ok` as a module 37 | - Add `es2015` field for Angular modules 38 | 39 | ## 2.0.0 40 | 41 | - Normalizes line ending for files declared in `bin` field 42 | - In addition to checking `main` and `bin`, checks `typings` and `module` fields if present 43 | - CLI signature has been changed 44 | - Drop Node 4 support 45 | 46 | ## 1.0.0 47 | 48 | - Initial version 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from 'chalk'; 4 | import meow from 'meow'; 5 | import { pkgOk } from './index.js'; 6 | 7 | const cli = meow( 8 | ` 9 | Usage 10 | $ pkg-ok 11 | 12 | Options 13 | --field, -f additional field to check 14 | --bin, -b additional bin file to check 15 | 16 | Examples 17 | $ pkg-ok 18 | $ pkg-ok --field otherField --bin otherFile 19 | `, 20 | { 21 | importMeta: import.meta, 22 | flags: { 23 | field: { 24 | alias: 'f', 25 | type: 'string', 26 | isMultiple: true, 27 | }, 28 | bin: { 29 | alias: 'b', 30 | type: 'string', 31 | isMultiple: true, 32 | }, 33 | }, 34 | }, 35 | ); 36 | 37 | const errorMessage = (error: unknown) => { 38 | if (error instanceof Error) { 39 | return error.message; 40 | } else { 41 | return 'Unknown error running pkg-ok'; 42 | } 43 | }; 44 | 45 | const directory = cli.input[0] || process.cwd(); 46 | 47 | try { 48 | pkgOk(directory, { fields: cli.flags.field, bin: cli.flags.bin }); 49 | console.log(chalk.green('Package ok')); 50 | } catch (error) { 51 | console.log(chalk.red('pkg-ok error')); 52 | console.log(chalk.red(errorMessage(error))); 53 | process.exit(1); 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pkg-ok [![Build status](https://github.com/abraham/pkg-ok/actions/workflows/index.yaml/badge.svg)](https://github.com/abraham/pkg-ok/actions/workflows/index.yaml) [![npm](https://img.shields.io/npm/v/pkg-ok.svg)](https://www.npmjs.com/package/pkg-ok) 2 | 3 | > `pkg-ok` checks paths and scripts defined in `package.json` before you publish 👌 4 | 5 | - Ensures paths defined in `main`, `bin`, `module`, `types`, `typings`, `es2015`, `browser`, and `exports` exist 6 | - Ensures paths defined in `main`, `bin`, and `browser` are relative 7 | - Ensures `bin` scripts use cross-platform line endings 8 | 9 | ## Usage 10 | 11 | ```sh 12 | npm install pkg-ok --save-dev 13 | ``` 14 | 15 | ```js 16 | // package.json 17 | { 18 | "main": "oops_this_file_doesnt_exist.js", 19 | "scripts": { 20 | "prepublishOnly": "... && pkg-ok" 21 | } 22 | } 23 | ``` 24 | 25 | ```sh 26 | npm publish 27 | # Error! 28 | # Since main file doesn't exist, publish is blocked 29 | ``` 30 | 31 | ## CLI 32 | 33 | Check the `package.json` in the current directory. 34 | 35 | ```sh 36 | pkg-ok 37 | ``` 38 | 39 | Check the `package.json` in a specific directory. 40 | 41 | ```sh 42 | pkg-ok some/directory 43 | ``` 44 | 45 | Check additional `package.json` fields or bin files. 46 | 47 | ```sh 48 | pkg-ok --field someField --bin script.sh 49 | ``` 50 | 51 | ## API 52 | 53 | ```js 54 | const pkgDirectory = __dirname; 55 | 56 | pkgOk(pkgDirectory, { 57 | fields: ['someAdditonalField'], 58 | bin: ['someAdditionalScript.sh'], 59 | }); 60 | ``` 61 | 62 | ## License 63 | 64 | MIT 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pkg-ok", 3 | "version": "3.0.0", 4 | "description": "Checks package.json main and bin paths", 5 | "exports": "./dist/index.js", 6 | "bin": "./dist/bin.js", 7 | "type": "module", 8 | "scripts": { 9 | "test": "npm run build && npm run jest && node ./dist/bin", 10 | "jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js", 11 | "format": "prettier --write .", 12 | "build": "rm -rf dist && tsc", 13 | "lint": "prettier --check . && eslint . --ext .ts --ext .cjs --ext .mjs", 14 | "prepublishOnly": "npm test && node ./dist/bin", 15 | "prepare": "husky install" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/abraham/pkg-ok.git" 20 | }, 21 | "keywords": [ 22 | "package", 23 | "main", 24 | "bin", 25 | "field", 26 | "file", 27 | "path", 28 | "exist", 29 | "test", 30 | "crlf", 31 | "lf", 32 | "line ending" 33 | ], 34 | "author": "typicode ", 35 | "contributors": [ 36 | "Abraham Williams " 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/abraham/pkg-ok/issues" 41 | }, 42 | "homepage": "https://github.com/abraham/pkg-ok#readme", 43 | "devDependencies": { 44 | "@types/mock-fs": "^4.0.0", 45 | "@types/node": "^18.0.0", 46 | "@typescript-eslint/eslint-plugin": "^5.0.0", 47 | "@typescript-eslint/parser": "^5.0.0", 48 | "eslint": "^8.0.0", 49 | "eslint-config-prettier": "^9.1.0", 50 | "eslint-plugin-jest": "^27.1.0", 51 | "eslint-plugin-n": "^17.10.3", 52 | "husky": "^9.1.6", 53 | "jest": "^29.1.2", 54 | "mock-fs": "^5.0.0", 55 | "prettier": "3.6.2", 56 | "type-fest": "^4.3.3", 57 | "typescript": "^5.0.4" 58 | }, 59 | "dependencies": { 60 | "chalk": "^5.0.0", 61 | "meow": "^11.0.0", 62 | "normalize-newline": "^4.0.0" 63 | }, 64 | "engines": { 65 | "node": "^18.0.0" 66 | }, 67 | "husky": { 68 | "hooks": { 69 | "pre-commit": "npm test" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; 2 | import { readFileSync } from 'fs'; 3 | import mock from 'mock-fs'; 4 | import { join } from 'path'; 5 | import { pkgOk } from './index.js'; 6 | 7 | describe('pkg-ok', () => { 8 | beforeEach(() => { 9 | mock({ 10 | '/A/package.json': JSON.stringify({ 11 | main: 'unknown.js', 12 | bin: 'unknown.js', 13 | types: 'unknown.js', 14 | typings: 'unknown.js', 15 | module: 'unknown.js', 16 | es2015: 'unknown.js', 17 | browser: 'unknown.js', 18 | exports: 'unknown.js', 19 | }), 20 | '/B/package.json': JSON.stringify({ 21 | bin: { 22 | X: 'unknown.js', 23 | Y: 'unknown.js', 24 | }, 25 | }), 26 | '/C/package.json': JSON.stringify({ 27 | foo: 'unknown.js', 28 | }), 29 | '/D/package.json': JSON.stringify({ 30 | foo: { 31 | bar: 'bar', 32 | baz: 'baz', 33 | }, 34 | }), 35 | '/E/package.json': JSON.stringify({ 36 | bin: './script.js', 37 | }), 38 | '/E/script.js': 'foo\r\nbar', 39 | '/E/another-script.js': 'baz\r\nqux', 40 | '/F/package.json': JSON.stringify({ 41 | browser: { 42 | './dist/lib.cjs.js': './dist/lib.cjs.browser.js', 43 | './dist/lib.esm.js': './dist/lib.esm.browser.js', 44 | }, 45 | }), 46 | '/F/dist/lib.cjs.browser.js': 'cjs', 47 | '/F/dist/lib.esm.browser.js': 'esm', 48 | '/G/package.json': JSON.stringify({ 49 | browser: { 50 | 'dist/lib.cjs.js': './dist/lib.cjs.browser.js', 51 | './dist/lib.esm.js': 'dist/lib.esm.browser.js', 52 | }, 53 | }), 54 | '/G/dist/lib.cjs.js': './dist/lib.cjs.browser.js', 55 | }); 56 | }); 57 | 58 | afterEach(() => mock.restore()); 59 | 60 | it('checks /A', () => { 61 | expect(() => pkgOk(join('/A'))).toThrowError( 62 | /main[\s\S]*bin[\s\S]*types[\s\S]*typings[\s\S]*module[\s\S]*es2015[\s\S]*browser[\s\S]*exports/, 63 | ); 64 | }); 65 | 66 | it('checks /B', () => { 67 | expect(() => pkgOk(join('/B'))).toThrowError(/bin\.X[\s\S]*bin\.Y/); 68 | }); 69 | 70 | it('checks /C', () => { 71 | expect(() => pkgOk(join('/C'), { fields: ['foo'] })).toThrowError(/foo/); 72 | }); 73 | 74 | it('checks /D', () => { 75 | expect(() => pkgOk(join('/D'), { fields: ['foo'] })).toThrowError(/foo\.bar[\s\S]*foo\.baz/); 76 | }); 77 | 78 | it('checks /E', () => { 79 | pkgOk(join('/E'), { bin: ['another-script.js'] }); 80 | expect(readFileSync('/E/script.js', 'utf-8')).toEqual('foo\nbar'); 81 | expect(readFileSync('/E/another-script.js', 'utf-8')).toEqual('baz\nqux'); 82 | }); 83 | 84 | it('checks /F', () => { 85 | pkgOk(join('/F')); 86 | expect(readFileSync('/F/dist/lib.cjs.browser.js', 'utf-8')).toEqual('cjs'); 87 | expect(readFileSync('/F/dist/lib.esm.browser.js', 'utf-8')).toEqual('esm'); 88 | }); 89 | 90 | it('checks /G', () => { 91 | expect(() => pkgOk(join('/G'))).toThrowError( 92 | /browser.*path[\s\S]*browser.*path[\s\S]*browser.*must/, 93 | ); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/fields.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { doesNotExistError, mustBeRelativeError } from './errors.js'; 4 | import { isObject, Pkg } from './pkg.js'; 5 | 6 | interface Field { 7 | name: string; 8 | relative: boolean; 9 | } 10 | 11 | const FIELDS: Readonly = [ 12 | // https://docs.npmjs.com/cli/v8/configuring-npm/package-json#main 13 | { 14 | name: 'main', 15 | relative: true, 16 | }, 17 | // https://docs.npmjs.com/cli/v8/configuring-npm/package-json#bin 18 | { 19 | name: 'bin', 20 | relative: true, 21 | }, 22 | // https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html used by TypeScript 23 | { 24 | name: 'types', 25 | relative: false, 26 | }, 27 | // https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html alternatively used by TypeScript 28 | { 29 | name: 'typings', 30 | relative: false, 31 | }, 32 | // https://github.com/stereobooster/package.json#module used by rollup, webpack 33 | { 34 | name: 'module', 35 | relative: false, 36 | }, 37 | // https://github.com/stereobooster/package.json#es2015 used by Angular 38 | { 39 | name: 'es2015', 40 | relative: false, 41 | }, 42 | // https://docs.npmjs.com/files/package.json#browser 43 | { 44 | name: 'browser', 45 | relative: true, 46 | }, 47 | // https://nodejs.org/api/packages.html#subpath-exports 48 | { 49 | name: 'exports', 50 | relative: false, 51 | }, 52 | ]; 53 | 54 | const FIELD_NAMES: Readonly = FIELDS.map((field) => field.name); 55 | 56 | function doesNotExist(dir: string, file: string) { 57 | return !existsSync(join(dir, file)); 58 | } 59 | 60 | function findField(name: string) { 61 | return FIELDS.find((field) => field.name === name); 62 | } 63 | 64 | function mustBeRelative(name: string, file: string): boolean { 65 | const field = findField(name); 66 | if (!field || !field.relative) { 67 | return false; 68 | } 69 | 70 | return !file.startsWith('./'); 71 | } 72 | 73 | function checkField(pkg: Pkg, dir: string, field: string) { 74 | const errors: string[] = []; 75 | 76 | const value = pkg[field]; 77 | if (value) { 78 | if (isObject(value)) { 79 | Object.keys(value).forEach((key) => { 80 | if (doesNotExist(dir, value[key])) { 81 | errors.push(doesNotExistError(`${field}.${key}`)); 82 | } 83 | 84 | if (mustBeRelative(field, value[key])) { 85 | errors.push(mustBeRelativeError(`${field}.${key}`)); 86 | } 87 | }); 88 | } else if (typeof value === 'string') { 89 | if (doesNotExist(dir, value)) { 90 | errors.push(doesNotExistError(field)); 91 | } 92 | 93 | if (mustBeRelative(field, value)) { 94 | errors.push(mustBeRelativeError(field)); 95 | } 96 | } 97 | } 98 | 99 | return errors; 100 | } 101 | 102 | export function checkFields(pkg: Pkg, dir: string, otherFields: string[]) { 103 | const fields = [...FIELD_NAMES, ...otherFields]; 104 | 105 | // Check fields and add errors to the errors array 106 | return fields.flatMap((field) => checkField(pkg, dir, field)); 107 | } 108 | -------------------------------------------------------------------------------- /src/bin.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from '@jest/globals'; 2 | import { execFile } from 'child_process'; 3 | import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'fs'; 4 | import { tmpdir } from 'os'; 5 | import { join } from 'path'; 6 | 7 | let directory = ''; 8 | 9 | describe('pkg-ok', () => { 10 | beforeAll(() => { 11 | directory = mkdtempSync(join(tmpdir(), 'pkg-ok-')); 12 | mkdirSync(join(directory, 'A')); 13 | writeFileSync( 14 | join(directory, 'A/package.json'), 15 | JSON.stringify({ 16 | main: 'unknown.js', 17 | bin: 'unknown.js', 18 | types: 'unknown.js', 19 | typings: 'unknown.js', 20 | module: 'unknown.js', 21 | es2015: 'unknown.js', 22 | browser: 'unknown.js', 23 | exports: 'unknown.js', 24 | }), 25 | ); 26 | mkdirSync(join(directory, 'B')); 27 | writeFileSync( 28 | join(directory, 'B/package.json'), 29 | JSON.stringify({ 30 | bin: { 31 | X: 'unknown.js', 32 | Y: 'unknown.js', 33 | }, 34 | }), 35 | ); 36 | mkdirSync(join(directory, 'C')); 37 | writeFileSync( 38 | join(directory, 'C/package.json'), 39 | JSON.stringify({ 40 | foo: 'unknown.js', 41 | }), 42 | ); 43 | mkdirSync(join(directory, 'D')); 44 | writeFileSync( 45 | join(directory, 'D/package.json'), 46 | JSON.stringify({ 47 | foo: { 48 | bar: 'bar', 49 | baz: 'baz', 50 | }, 51 | }), 52 | ); 53 | mkdirSync(join(directory, 'E')); 54 | writeFileSync( 55 | join(directory, 'E/package.json'), 56 | JSON.stringify({ 57 | bin: './script.js', 58 | }), 59 | ); 60 | writeFileSync(join(directory, 'E/script.js'), 'foo\r\nbar'); 61 | writeFileSync(join(directory, 'E/another-script.js'), 'baz\r\nqux'); 62 | mkdirSync(join(directory, 'F')); 63 | mkdirSync(join(directory, 'F', 'dist')); 64 | writeFileSync( 65 | join(directory, 'F/package.json'), 66 | JSON.stringify({ 67 | browser: { 68 | './dist/lib.cjs.js': './dist/lib.cjs.browser.js', 69 | './dist/lib.esm.js': './dist/lib.esm.browser.js', 70 | }, 71 | }), 72 | ); 73 | writeFileSync(join(directory, 'F/dist/lib.cjs.browser.js'), 'cjs'); 74 | writeFileSync(join(directory, 'F/dist/lib.esm.browser.js'), 'esm'); 75 | mkdirSync(join(directory, 'G')); 76 | mkdirSync(join(directory, 'G', 'dist')); 77 | writeFileSync( 78 | join(directory, 'G/package.json'), 79 | JSON.stringify({ 80 | browser: { 81 | 'dist/lib.cjs.js': './dist/lib.cjs.browser.js', 82 | './dist/lib.esm.js': 'dist/lib.esm.browser.js', 83 | }, 84 | }), 85 | ); 86 | writeFileSync(join(directory, 'G/dist/lib.cjs.js'), './dist/lib.cjs.browser.js'); 87 | }); 88 | 89 | it('checks /A', (done) => { 90 | execFile('node', ['dist/bin.js', join(directory, 'A')], (_error, stdout) => { 91 | expect(stdout).toMatch( 92 | /main[\s\S]*bin[\s\S]*types[\s\S]*typings[\s\S]*module[\s\S]*es2015[\s\S]*browser[\s\S]*exports/, 93 | ); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('checks /B', (done) => { 99 | execFile('node', ['dist/bin.js', join(directory, 'B')], (_error, stdout) => { 100 | expect(stdout).toMatch(/bin\.X[\s\S]*bin\.Y/); 101 | done(); 102 | }); 103 | }); 104 | 105 | it('checks /C', (done) => { 106 | execFile('node', ['dist/bin.js', join(directory, 'C'), '--field', 'foo'], (_error, stdout) => { 107 | expect(stdout).toMatch(/foo/); 108 | done(); 109 | }); 110 | }); 111 | 112 | it('checks /D', (done) => { 113 | execFile('node', ['dist/bin.js', join(directory, 'D'), '--field', 'foo'], (_error, stdout) => { 114 | expect(stdout).toMatch(/foo\.bar[\s\S]*foo\.baz/); 115 | done(); 116 | }); 117 | }); 118 | 119 | it('checks /E', (done) => { 120 | execFile('node', ['dist/bin.js', join(directory, 'E'), '--bin', 'another-script.js'], () => { 121 | expect(readFileSync(join(directory, '/E/script.js'), 'utf-8')).toEqual('foo\nbar'); 122 | expect(readFileSync(join(directory, '/E/another-script.js'), 'utf-8')).toEqual('baz\nqux'); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('checks /F', (done) => { 128 | execFile('node', ['dist/bin.js', join(directory, 'F'), '--bin', 'another-script.js'], () => { 129 | expect(readFileSync(join(directory, '/F/dist/lib.cjs.browser.js'), 'utf-8')).toEqual('cjs'); 130 | expect(readFileSync(join(directory, '/F/dist/lib.esm.browser.js'), 'utf-8')).toEqual('esm'); 131 | done(); 132 | }); 133 | }); 134 | 135 | it('checks /G', (done) => { 136 | execFile( 137 | 'node', 138 | ['dist/bin.js', join(directory, 'G'), '--bin', 'another-script.js'], 139 | (_error, stdout) => { 140 | expect(stdout).toMatch(/browser.*path[\s\S]*browser.*path[\s\S]*browser.*must/); 141 | done(); 142 | }, 143 | ); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/7g/91pdcw2929s400sdx4376k580000gp/T/jest_dy", 15 | 16 | // Automatically clear mock calls, instances and results before every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: 'v8', 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state before every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state and implementation before every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | testMatch: [ 150 | '**/__tests__/**/*.[jt]s?(x)', 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | '**/?(*.)+(spec|test).[j]s?(x)', 153 | ], 154 | 155 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 156 | // testPathIgnorePatterns: [ 157 | // "/node_modules/" 158 | // ], 159 | 160 | // The regexp pattern or array of patterns that Jest uses to detect test files 161 | // testRegex: [], 162 | 163 | // This option allows the use of a custom results processor 164 | // testResultsProcessor: undefined, 165 | 166 | // This option allows use of a custom test runner 167 | // testRunner: "jest-circus/runner", 168 | 169 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 170 | // testURL: "http://localhost", 171 | 172 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 173 | // timers: "real", 174 | 175 | // A map from regular expressions to paths to transformers 176 | transform: {}, 177 | 178 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 179 | // transformIgnorePatterns: [ 180 | // "/node_modules/", 181 | // "\\.pnp\\.[^\\/]+$" 182 | // ], 183 | 184 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 185 | // unmockedModulePathPatterns: undefined, 186 | 187 | // Indicates whether each individual test should be reported during the run 188 | // verbose: undefined, 189 | 190 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 191 | // watchPathIgnorePatterns: [], 192 | 193 | // Whether to use watchman for file crawling 194 | // watchman: true, 195 | }; 196 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "es2020", /* Specify what module code is generated. */ 28 | "rootDir": "./src", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | --------------------------------------------------------------------------------