diff --git a/README.md b/README.md index 0b99877..251737e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # build-tools -`@fp-tx/build-tools` is a thin wrapper around `tsup` for the purpose of building dual ESM/CJS packages. It contains a chief export `makeConfig` which will read from a configurable source directory and determine which files to include as entrypoints. Using these entrypoints, it also adds a dynamic "exports" field with `import` and `default` fields based on the determined entrypoints and module type (determined by `package.json > type`). +`@fp-tx/build-tools` is a thin wrapper around `tsup` for the purpose of building dual ESM/CJS packages. It contains a chief export `makeConfig` which will read from a configurable source directory and determine which files to include as entrypoints. Using these entrypoints, it also adds a dynamic "exports" field with `import` and `default` fields based on the determined entrypoints and module type (determined by `package.json > type`). Additionally it adds the appropriate `main`, `module`, `types`, and `bin` (if applicable) to the emitted package.json. Additionally, `build-tools` will emit smart declaration files with rewritten import, export, and `declare module` paths. @@ -24,7 +24,7 @@ const config = makeConfig( buildType: 'dual', buildMode: { type: 'Single', - entrypoint: 'index.ts', + entrypoint: './src/index.ts', }, iife: false, @@ -71,15 +71,17 @@ npm install -D tsup @fp-tx/build-tools ## Configuration Parameters -| Parameter | Type | Description | Default | -| ------------------ | ----------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------- | -| iife | `boolean` | Include IIFE generation as fallback. This setting is recommended for single-target builds | `false` | -| emitTypes | `boolean` | Generate `.d.ts`, and `.d.cts` or `.d.mts` files | `true` | -| dtsConfig | `string` | The `tsconfig.json` for types generation | `tsconfig.json` | -| srcDir | `string` | The source directory to read from | `'src'` | -| basePath | `string` | The current working directory | `'.'` | -| buildMode | `BuildMode` | Determines the package entrypoints, "Single" and `entrypoint` or "Multi" and `entrypointGlobs` | `{ type: "Single", entrypoint: "index.ts" }` | -| buildType | `cjs`, `esm`, or `dual` | Determines the output module format along with `package.json > type` | `dual` | -| omittedPackageKeys | `ReadonlyArray` | Array of keys to omit from the package.json file | `["devDependencies", "scripts"]` | -| copyFiles | `ReadonlyArray` | Whether to copy non-typescript files | `['README.md', 'LICENSE']` | -| outDir | `string` | The output directory | `dist` | +| Parameter | Type | Description | Default | +| -------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| iife | `boolean` | Include IIFE generation as fallback. This setting is recommended for single-target builds | `false` | +| emitTypes | `boolean` | Generate `.d.ts`, and `.d.cts` or `.d.mts` files | `true` | +| dtsConfig | `string` | The `tsconfig.json` for types generation | `tsconfig.json` | +| srcDir | `string` | The source directory to read from | `'src'` | +| basePath | `string` | The current working directory | `'.'` | +| buildMode | `BuildMode` | Determines the package entrypoints, "Single" and `entrypoint` or "Multi" and `entrypointGlobs` | `{ type: "Single", entrypoint: "index.ts" }` | +| buildType | `cjs`, `esm`, or `dual` | Determines the output module format along with `package.json > type` | `dual` | +| omittedPackageKeys | `ReadonlyArray` | Array of keys to omit from the package.json file | `["devDependencies", "scripts"]` | +| copyFiles | `ReadonlyArray` | Whether to copy non-typescript files | `['README.md', 'LICENSE']` | +| outDir | `string` | The output directory | `dist` | +| dtsCompilerOverrides | `Partial` | Overrides to override build-tools imposed tsconfig defaults. **Only use this option if you know what you are doing** | `{}` | +| bin | `string` or `Record` | `ts` file entrypoints to fill the emitted package `bin` field | `null` | diff --git a/package.json b/package.json index 4cfa3d4..4603d43 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "husky": "^8.0.3", "jest": "^29.7.0", "lint-staged": "^15.1.0", - "prettier": "~3.1.0", + "prettier": "~3.1.1", "prettier-plugin-jsdoc": "^1.1.1", "tsc-files": "^1.1.4", "tsup": "^8.0.0", @@ -88,5 +88,10 @@ "eslint --fix-type layout --fix --cache", "pnpm run prerelease" ] + }, + "pnpm": { + "patchedDependencies": { + "lru-cache@10.4.3": "patches/lru-cache@10.4.3.patch" + } } } diff --git a/patches/lru-cache@10.4.3.patch b/patches/lru-cache@10.4.3.patch new file mode 100644 index 0000000..3a58a86 --- /dev/null +++ b/patches/lru-cache@10.4.3.patch @@ -0,0 +1,13 @@ +diff --git a/dist/commonjs/index.d.ts b/dist/commonjs/index.d.ts +index f59de7602a528afde714e56dcf8c25ee496e39fb..804042b5ff381b8594307296de29d31d1b564c92 100644 +--- a/dist/commonjs/index.d.ts ++++ b/dist/commonjs/index.d.ts +@@ -837,7 +837,7 @@ export declare namespace LRUCache { + * + * Changing any of these will alter the defaults for subsequent method calls. + */ +-export declare class LRUCache implements Map { ++export declare class LRUCache { + #private; + /** + * {@link LRUCache.OptionsBase.ttl} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c17742e..2176c4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + lru-cache@10.4.3: + hash: hawnkdxmi6fjojy7ubkjvqg3r4 + path: patches/lru-cache@10.4.3.patch + importers: .: @@ -91,7 +96,7 @@ importers: specifier: ^15.1.0 version: 15.1.0 prettier: - specifier: ~3.1.0 + specifier: ~3.1.1 version: 3.1.1 prettier-plugin-jsdoc: specifier: ^1.1.1 @@ -5710,7 +5715,7 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 8.1.0 - lru-cache@10.4.3: {} + lru-cache@10.4.3(patch_hash=hawnkdxmi6fjojy7ubkjvqg3r4): {} lru-cache@5.1.1: dependencies: @@ -6097,7 +6102,7 @@ snapshots: path-scurry@1.11.1: dependencies: - lru-cache: 10.4.3 + lru-cache: 10.4.3(patch_hash=hawnkdxmi6fjojy7ubkjvqg3r4) minipass: 7.1.2 path-type@4.0.0: {} diff --git a/src/BuildService.ts b/src/BuildService.ts index d52e28e..5aebbb7 100644 --- a/src/BuildService.ts +++ b/src/BuildService.ts @@ -107,6 +107,35 @@ export const BuildServiceLive: RTE.ReaderTaskEither< ), ), ), + // Validate Bin Field + RTE.tapEither(({ config, entrypoints }) => + pipe( + config.bin, + E.fromPredicate( + binField => { + if (binField === null) { + return true + } else if (typeof binField === 'string') { + return entrypoints.includes(normalizePath(binField)) + } else { + return pipe( + binField, + RR.every(file => entrypoints.includes(normalizePath(file))), + ) + } + }, + binField => + new BuildServiceError( + `The \`bin\` field (${JSON.stringify( + binField, + )}) files must be listed as entrypoints in \`buildMode\`. Received entrypoints: [${entrypoints.join( + ',', + )}]`, + null, + ), + ), + ), + ), RTE.let('resolvedIndex', ({ config, entrypoints }) => config.buildMode.type === 'Single' ? config.buildMode.entrypoint @@ -160,6 +189,7 @@ export const BuildServiceLive: RTE.ReaderTaskEither< module: __, exports: ___, types: ____, + bin: preBin, ...rest }, }) => @@ -170,7 +200,7 @@ export const BuildServiceLive: RTE.ReaderTaskEither< ), RTE.flatMapTaskEither(Exports.pkgExports), RTE.map( - ([exports, main, module, types]): RR.ReadonlyRecord => ({ + ([exports, main, module, types, bin]): RR.ReadonlyRecord => ({ name, version, description, @@ -181,6 +211,7 @@ export const BuildServiceLive: RTE.ReaderTaskEither< module, types, exports, + bin: bin ?? preBin, ...pipe( rest, RR.filterWithIndex(key => !config.omittedPackageKeys.includes(key)), @@ -394,6 +425,10 @@ export const BuildServiceLive: RTE.ReaderTaskEither< const rootDirRegex = /^\.\/(.*).(m|c)?ts/ +function normalizePath(p: string): string { + return path.join(path.dirname(p), path.basename(p)) +} + export const configuration: R.Reader< BuildService, BuildServiceMethods['configuration'] diff --git a/src/ConfigService.ts b/src/ConfigService.ts index b6d1deb..6e18e2e 100644 --- a/src/ConfigService.ts +++ b/src/ConfigService.ts @@ -120,6 +120,16 @@ export type ConfigParameters = { * @default { } */ readonly dtsCompilerOverrides?: Partial + + /** + * Allows you to specify the package-command entrypoints as TypeScript files that will + * be re-pointed to their emitted javascript files. + * + * @remarks + * **Any binary file specified in this key or record must be an entrypoint, an error + * will be raised if that file is excluded from the entrypoint globs** + */ + readonly bin?: string | Record | null } export class ConfigService { @@ -136,6 +146,7 @@ export class ConfigService { emitTypes = true, dtsConfig = 'tsconfig.json', dtsCompilerOverrides = {}, + bin = null, }: ConfigParameters) { this[ConfigServiceSymbol] = { buildType, @@ -149,6 +160,7 @@ export class ConfigService { buildMode, emitTypes, dtsCompilerOverrides, + bin, } } } diff --git a/src/ExportsService.ts b/src/ExportsService.ts index 290d5ae..6192423 100644 --- a/src/ExportsService.ts +++ b/src/ExportsService.ts @@ -22,23 +22,26 @@ export class ExportsService { main: string | undefined, module: string | undefined, types: string | undefined, + bin: string | Record | undefined, ] constructor( pkgExports: Exports, main?: string | undefined, module?: string | undefined, types?: string | undefined, + bin?: string | Record | undefined, ) { - this[ExportsServiceSymbol] = [pkgExports, main, module, types] + this[ExportsServiceSymbol] = [pkgExports, main, module, types, bin] } static of: ( main?: string | undefined, module?: string | undefined, types?: string | undefined, + bin?: string | Record | undefined, ) => (pkgExports?: Exports) => ExportsService = - (main, module, types) => + (main, module, types, bin) => (pkgExports = { './package.json': './package.json' }) => - new ExportsService(pkgExports, main, module, types) + new ExportsService(pkgExports, main, module, types, bin) } export const pkgExports: RTE.ReaderTaskEither< @@ -152,10 +155,15 @@ type ToExports = { readonly default: Endomorphism } -type ExportsConfig = { - readonly import?: ToExports - readonly require?: ToExports - readonly default?: ToExports +type ToBin = Endomorphism + +interface ExportsConfig { + readonly exports: { + readonly import?: ToExports + readonly require?: ToExports + readonly default?: ToExports + } + readonly bin: ToBin } const addGlobalExportSingle = ( @@ -164,7 +172,7 @@ const addGlobalExportSingle = ( Required['buildMode'], { type: 'Multi' } >, - { default: d }: ExportsConfig, + { exports: { default: d } }: ExportsConfig, ): DefaultExports => config.iife ? { @@ -178,7 +186,7 @@ const addGlobalExportSingle = ( const addGlobalExportMulti = ( config: Required, file: string, - { default: d }: ExportsConfig, + { exports: { default: d } }: ExportsConfig, ): DefaultExports => config.iife ? { @@ -199,7 +207,10 @@ const toExportsService = ( pipe( Common, RTE.map(({ config, deps }) => { - const { import: i, require: r } = exportsConfig + const { + exports: { import: i, require: r }, + bin, + } = exportsConfig const buildType = config.buildMode.type if (buildType === 'Single') { return pipe( @@ -229,6 +240,11 @@ const toExportsService = ( r?.default(config.buildMode.entrypoint), i?.default(config.buildMode.entrypoint), (r ?? i)?.types(config, config.buildMode.entrypoint)['types'], + config.bin === null + ? undefined + : typeof config.bin === 'string' + ? bin(config.bin) + : pipe(config.bin, RR.map(bin)), ), ) } @@ -265,83 +281,106 @@ const toExportsService = ( r?.default(deps.resolvedIndex), i?.default(deps.resolvedIndex), (r ?? i)?.types(config, deps.resolvedIndex)['types'], + config.bin === null + ? undefined + : typeof config.bin === 'string' + ? bin(config.bin) + : pipe(config.bin, RR.map(bin)), ), ) }), ) const DualTypeModuleExports = toExportsService({ - import: { - types: addDtsExports, - default: tsToJs, - }, - require: { - types: addDctsExports, - default: tsToCjs, - }, - default: { - types: addDctsExports, - default: tsToGlobalCjs, + exports: { + import: { + types: addDtsExports, + default: tsToJs, + }, + require: { + types: addDctsExports, + default: tsToCjs, + }, + default: { + types: addDctsExports, + default: tsToGlobalCjs, + }, }, + bin: tsToJs, }) const DualTypeCommonExports = toExportsService({ - import: { - types: addDmtsExports, - default: tsToMjs, - }, - require: { - types: addDtsExports, - default: tsToJs, - }, - default: { - types: addDtsExports, - default: tsToGlobal, + exports: { + import: { + types: addDmtsExports, + default: tsToMjs, + }, + require: { + types: addDtsExports, + default: tsToJs, + }, + default: { + types: addDtsExports, + default: tsToGlobal, + }, }, + bin: tsToJs, }) const CjsTypeModuleExports = toExportsService({ - require: { - types: addDctsExports, - default: tsToCjs, - }, - default: { - types: addDctsExports, - default: tsToGlobalCjs, + exports: { + require: { + types: addDctsExports, + default: tsToCjs, + }, + default: { + types: addDctsExports, + default: tsToGlobalCjs, + }, }, + bin: tsToCjs, }) const CjsTypeCommonExports = toExportsService({ - require: { - types: addDtsExports, - default: tsToJs, - }, - default: { - types: addDtsExports, - default: tsToGlobal, + exports: { + require: { + types: addDtsExports, + default: tsToJs, + }, + default: { + types: addDtsExports, + default: tsToGlobal, + }, }, + bin: tsToJs, }) const EsmTypeModuleExports = toExportsService({ - import: { - types: addDtsExports, - default: tsToJs, - }, - default: { - types: addDctsExports, - default: tsToGlobalCjs, + exports: { + import: { + types: addDtsExports, + default: tsToJs, + }, + default: { + types: addDctsExports, + default: tsToGlobalCjs, + }, }, + bin: tsToJs, }) const EsmTypeCommonExports = toExportsService({ - import: { - types: addDmtsExports, - default: tsToMjs, - }, - default: { - types: addDtsExports, - default: tsToGlobal, + exports: { + import: { + types: addDmtsExports, + default: tsToMjs, + }, + default: { + types: addDtsExports, + default: tsToGlobal, + }, }, + bin: tsToMjs, }) export const ExportsServiceLive: ( diff --git a/src/PackageJson.ts b/src/PackageJson.ts index 62be2d3..c959283 100644 --- a/src/PackageJson.ts +++ b/src/PackageJson.ts @@ -16,6 +16,7 @@ export const PackageJsonSchema = S.ParseJsonString( module: S.Unknown, exports: S.Unknown, type: S.Optional(S.Literal('module', 'commonjs'), 'commonjs'), + bin: S.Unknown, }, S.Unknown, ), diff --git a/tests/ExportsService.test.ts b/tests/ExportsService.test.ts index ccbe5cc..6e09dd0 100644 --- a/tests/ExportsService.test.ts +++ b/tests/ExportsService.test.ts @@ -11,6 +11,37 @@ function assertRight(e: E.Either): asserts e is E.Right { test.each([ + tuple( + '`bin` field resolution', + ConfigServiceLive({ + buildMode: { type: 'Single', entrypoint: 'foo.ts' }, + bin: { + foo: './foo.ts', + bar: './bar.ts', + }, + }), + ExportsService.ExportsServiceLive({ + files: ['foo.ts', 'bar.ts'], + type: 'module', + resolvedIndex: 'foo.ts', + }), + tuple( + { + '.': { + import: { types: './foo.d.ts', default: './foo.js' }, + require: { types: './foo.d.cts', default: './foo.cjs' }, + }, + './package.json': './package.json', + }, + './foo.cjs', + './foo.js', + './foo.d.cts', + { + foo: './foo.js', + bar: './bar.js', + }, + ), + ), tuple( 'Dual "type: module" Single Exports', ConfigServiceLive({ @@ -32,6 +63,7 @@ describe('ExportsService', () => { './foo.cjs', './foo.js', './foo.d.cts', + undefined, ), ), tuple( @@ -67,6 +99,7 @@ describe('ExportsService', () => { './src/foo.cjs', './src/foo.js', './src/foo.d.cts', + undefined, ), ), tuple( @@ -90,6 +123,7 @@ describe('ExportsService', () => { './foo.js', './foo.mjs', './foo.d.ts', + undefined, ), ), tuple( @@ -125,6 +159,7 @@ describe('ExportsService', () => { './src/foo.js', './src/foo.mjs', './src/foo.d.ts', + undefined, ), ), tuple( @@ -148,6 +183,7 @@ describe('ExportsService', () => { './foo.cjs', undefined, './foo.d.cts', + undefined, ), ), tuple( @@ -181,6 +217,7 @@ describe('ExportsService', () => { './src/foo.cjs', undefined, './src/foo.d.cts', + undefined, ), ), tuple( @@ -204,6 +241,7 @@ describe('ExportsService', () => { './foo.js', undefined, './foo.d.ts', + undefined, ), ), tuple( @@ -237,6 +275,7 @@ describe('ExportsService', () => { './src/foo.js', undefined, './src/foo.d.ts', + undefined, ), ), tuple( @@ -260,6 +299,7 @@ describe('ExportsService', () => { undefined, './foo.mjs', './foo.d.mts', + undefined, ), ), tuple( @@ -293,6 +333,7 @@ describe('ExportsService', () => { undefined, './src/foo.mjs', './src/foo.d.mts', + undefined, ), ), tuple( @@ -316,6 +357,7 @@ describe('ExportsService', () => { undefined, './foo.js', './foo.d.ts', + undefined, ), ), tuple( @@ -349,6 +391,7 @@ describe('ExportsService', () => { undefined, './src/foo.js', './src/foo.d.ts', + undefined, ), ), ])('%s', async (_, configService, service, expected) => { diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 9e60c25..6c5681d 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -5,8 +5,6 @@ import { promisify } from 'node:util' import { pipe } from 'fp-ts/lib/function' import * as RA from 'fp-ts/lib/ReadonlyArray' -// import { makeConfig } from '../src' - describe('test projects', () => { test.each( pipe( @@ -42,6 +40,10 @@ describe('test projects', () => { // Used to test single-entrypoint root based dual libs // ------------------------------------------------ 'single-root-dual', + // ------------------------------------------------ + // Used to test usage of the bin field + // ------------------------------------------------ + 'bin-cjs-dual', ]), ), )( diff --git a/tests/test-projects/bin-cjs-dual/.gitignore b/tests/test-projects/bin-cjs-dual/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/tests/test-projects/bin-cjs-dual/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/tests/test-projects/bin-cjs-dual/baz.ts b/tests/test-projects/bin-cjs-dual/baz.ts new file mode 100644 index 0000000..1e2ea0d --- /dev/null +++ b/tests/test-projects/bin-cjs-dual/baz.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +export function main2() { + console.log('success!') +} + +main2() diff --git a/tests/test-projects/bin-cjs-dual/foo.ts b/tests/test-projects/bin-cjs-dual/foo.ts new file mode 100644 index 0000000..8b7d4b1 --- /dev/null +++ b/tests/test-projects/bin-cjs-dual/foo.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import { bar } from './src/bar' + +export function main() { + console.log({ bar }) +} + +main() diff --git a/tests/test-projects/bin-cjs-dual/package.json b/tests/test-projects/bin-cjs-dual/package.json new file mode 100644 index 0000000..234762f --- /dev/null +++ b/tests/test-projects/bin-cjs-dual/package.json @@ -0,0 +1,7 @@ +{ + "name": "bin-cjs-dual", + "version": "0.0.1", + "devDependencies": { + "@fp-tx/build-tools": "file:../../../dist" + } +} diff --git a/tests/test-projects/bin-cjs-dual/src/bar.ts b/tests/test-projects/bin-cjs-dual/src/bar.ts new file mode 100644 index 0000000..7f17e7c --- /dev/null +++ b/tests/test-projects/bin-cjs-dual/src/bar.ts @@ -0,0 +1,2 @@ +/** @public */ +export const bar = 'foo' diff --git a/tests/test-projects/bin-cjs-dual/tsconfig.json b/tests/test-projects/bin-cjs-dual/tsconfig.json new file mode 100644 index 0000000..210ae2a --- /dev/null +++ b/tests/test-projects/bin-cjs-dual/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": false, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "types": [] + }, + "include": ["./src/*.ts", "baz.ts", "foo.ts"], + "exclude": [] +} diff --git a/tests/test-projects/bin-cjs-dual/tsup.config.js b/tests/test-projects/bin-cjs-dual/tsup.config.js new file mode 100644 index 0000000..e848567 --- /dev/null +++ b/tests/test-projects/bin-cjs-dual/tsup.config.js @@ -0,0 +1,23 @@ +import { makeConfig } from '@fp-tx/build-tools' + +export default makeConfig( + { + basePath: '.', + buildType: 'dual', + buildMode: { + type: 'Multi', + entrypointGlobs: ['./foo.ts', './baz.ts'], + }, + srcDir: './src', + outDir: './dist', + copyFiles: [], + iife: true, + bin: { + foo: './foo.ts', + baz: './baz.ts', + }, + }, + { + clean: true, + }, +)