@plugjs/build
Version:
Simple shared build using PlugJS
477 lines (408 loc) • 16.7 kB
text/typescript
import '@plugjs/cov8'
import '@plugjs/eslint'
import '@plugjs/expect5'
import {
banner,
find,
fixExtensions,
isDirectory,
merge,
noop,
plugjs,
resolve,
rmrf,
} from '@plugjs/plug'
import '@plugjs/typescript'
import type { ESBuildOptions, FindOptions, Pipe } from '@plugjs/plug'
export * from '@plugjs/plug'
/** Shared ESBuild options */
const esbuildDefaults: ESBuildOptions = {
platform: 'node',
sourcemap: 'linked',
sourcesContent: false,
plugins: [ fixExtensions() ],
}
/** Options for creating our shared build file */
export interface TasksOptions {
/* ======================================================================== *
* DIRECTORIES *
* ======================================================================== */
/** The directory for the original sources (default: `src`) */
sourceDir?: string,
/** The destination directory of the transpiled sources (default: `dist`) */
destDir?: string,
/** The directory for the test files (default: `test`) */
testDir?: string,
/** The directory for the coverage report (default: `coverage`) */
coverageDir?: string,
/** The directory for the coverage data (default: `.coverage-data`) */
coverageDataDir?: string,
/** A directory containing extra types to use while transpiling (default: `types`) */
extraTypesDir?: string,
/** The `tsconfig.json` file used for _transpiling_ source TypeScript files (default: `tsconfig.json`) */
tsconfigJson?: string,
/* ======================================================================== *
* EXTRA INPUTS *
* ======================================================================== */
/** Extra `find` defintions for additional coverage sources */
extraCoverage?: (readonly [ glob: string, ...globs: string[], options: FindOptions])[]
/** Extra `find` defintions for additional linting sources */
extraLint?: (readonly [ glob: string, ...globs: string[], options: FindOptions])[]
/* ======================================================================== *
* PACKAGE.JSON OPTIONS *
* ======================================================================== */
/** The source `package.json` file (default: `package.json`) */
packageJson?: string,
/** The source `package.json` file (default: same as `packageJson` option) */
outputPackageJson?: string,
/* ======================================================================== *
* TRANSPILATION OPTIONS *
* ======================================================================== */
/** The extension used for CommonJS modules (default: `.cjs`) */
cjsExtension?: string,
/** The extension used for EcmaScript modules (default: `.mjs`) */
esmExtension?: string,
/** Enable CommonJS Modules or not (default: `true`) */
cjs?: boolean,
/** Enable EcmaScript Modules or not (default: `true`) */
esm?: boolean,
/* ======================================================================== *
* OTHER OPTIONS *
* ======================================================================== */
/** Enable or disable banners (default: `true` if `parallelize` is `false`) */
banners?: boolean,
/** Parallelize tasks (might make output confusing, default: `false`) */
parallelize?: boolean,
/** A glob pattern matching all test files (default: `**∕*.test.([cm])?ts`) */
testGlob?: string,
/** A glob pattern matching files to be exported (default: `index.*`) */
exportsGlob?: string,
/** Extra glob patterns matching files to be exported (default: `[]`) */
exportsGlobs?: string[],
/** Enable coverage when running tests (default: `true`) */
coverage?: boolean,
/** Minimum overall coverage percentage (default: `100`) */
minimumCoverage?: number,
/** Minimum per-file coverage percentage (default: `100`) */
minimumFileCoverage?: number,
/** Optimal overall coverage percentage (default: _none_) */
optimalCoverage?: number,
/** Optimal per-file coverage percentage (default: _none_) */
optimalFileCoverage?: number,
/**
* ESBuild compilation options
*
* Default:
*
* ```
* {
* platform: 'node',
* sourcemap: 'linked',
* sourcesContent: false,
* plugins: [ fixExtensions() ],
* }
* ```
*/
esbuildOptions?: ESBuildOptions,
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function tasks(options: TasksOptions = {}) {
const {
sourceDir: _sourceDir = 'src',
destDir: _destDir = 'dist',
testDir: _testDir = 'test',
coverageDir: _coverageDir = 'coverage',
coverageDataDir: _coverageDataDir = '.coverage-data',
extraTypesDir: _extraTypesDir = 'types',
tsconfigJson: _tsconfigJson = 'tsconfig.json',
extraLint: _extraLint = [],
extraCoverage: _extraCoverage = [],
packageJson: _packageJson = 'package.json',
outputPackageJson: _outputPackageJson = _packageJson,
cjsExtension: _cjsExtension = '.cjs',
esmExtension: _esmExtension = '.mjs',
cjs: _cjs = true,
esm: _esm = true,
parallelize: _parallelize = false,
banners: _banners = !_parallelize,
testGlob: _testGlob = '**/*.test.([cm])?ts',
exportsGlob: _exportsGlob = 'index.*',
exportsGlobs: _exportsGlobs = [],
coverage: _coverage = true,
minimumCoverage: _minimumCoverage = 100,
minimumFileCoverage: _minimumFileCoverage = 100,
optimalCoverage: _optimalCoverage = undefined,
optimalFileCoverage: _optimalFileCoverage = undefined,
esbuildOptions: _esbuildOptions = {},
} = options
// coverage ignore next
const emitBanner = _banners ? banner : (...args: any) => void args
// Merge esbuild defaults with specified options
const esbuildMergedOptions = Object.assign({}, esbuildDefaults, _esbuildOptions)
return plugjs({
/** The directory for the original sources (default: `src`) */
sourceDir: _sourceDir,
/** The destination directory of the transpiled sources (default: `dist`) */
destDir: _destDir,
/** The directory for the test files (default: `test`) */
testDir: _testDir,
/** The directory for the coverage report (default: `coverage`) */
coverageDir: _coverageDir,
/** The directory for the coverage data (default: `.coverage-data`) */
coverageDataDir: _coverageDataDir,
/** A directory containing extra types to use while transpiling (default: `types`) */
extraTypesDir: _extraTypesDir,
/** The `tsconfig.json` file used for _transpiling_ source TypeScript files (default: `tsconfig.json`) */
tsconfigJson: _tsconfigJson,
/** The source `package.json` file (default: `package.json`) */
packageJson: _packageJson,
/** The source `package.json` file (default: same as `packageJson` option) */
outputPackageJson: _outputPackageJson,
/** The extension used for CommonJS modules (default: `.cjs`) */
cjsExtension: _cjsExtension,
/** The extension used for EcmaScript modules (default: `.mjs`) */
esmExtension: _esmExtension,
/** The extension used for CommonJS modules (default: `.cjs`) */
cjs: _cjs ? 'true' : 'false',
/** The extension used for EcmaScript modules (default: `.mjs`) */
esm: _esm ? 'true' : 'false',
/** A glob pattern matching all test files (default: `**∕*.test.([cm])?ts`) */
testGlob: _testGlob,
/** A glob pattern matching files to be exported (default: `index.*`) */
exportsGlob: _exportsGlob,
/* ====================================================================== *
* SOURCES STRUCTURE *
* ====================================================================== */
/** Find all CommonJS source files (`*.cts`) */
_find_sources_cts(): Pipe {
return find('**/*.(c)?ts', { directory: this.sourceDir, ignore: '**/*.d.ts' })
},
/** Find all EcmaScript Module source files (`*.mts`) */
_find_sources_mts(): Pipe {
return find('**/*.(m)?ts', { directory: this.sourceDir, ignore: '**/*.d.ts' })
},
/** Find all typescript source files (`*.ts`, `*.mts` and `*.cts`) */
_find_sources(): Pipe {
return merge([
this.cjs === 'true' ? this._find_sources_cts() : noop(),
this.esm === 'true' ? this._find_sources_mts() : noop(),
])
},
/** Find all types definition files within sources */
_find_types(): Pipe {
return find('**/*.d.([cm])?ts', { directory: this.sourceDir })
},
/** Find all resource files (non-typescript files) within sources */
_find_resources(): Pipe {
return find('**/*', { directory: this.sourceDir, ignore: '**/*.([cm])?ts' })
},
/** Find all test source files */
_find_tests(): Pipe {
return find(this.testGlob, { directory: this.testDir, ignore: '**/*.d.([cm])?ts' })
},
/** Find all source files to lint */
_find_lint_sources(): Pipe {
return merge([
find('**/*.([cm])?ts', '**/*.([cm])?js', { directory: this.sourceDir }),
find('**/*.([cm])?ts', '**/*.([cm])?js', { directory: this.testDir }),
isDirectory(this.extraTypesDir) ?
find('**/*.([cm])?ts', '**/*.([cm])?js', { directory: this.extraTypesDir }) :
noop(),
..._extraLint.map((args) => find(...args)),
])
},
/** Find all source files for coverage */
_find_coverage_sources(): Pipe {
return merge([
find('**/*.([cm])?ts', '**/*.([cm])?js', {
directory: this.sourceDir,
ignore: '**/*.d.([cm])?ts',
}),
..._extraCoverage.map((args) => find(...args)),
])
},
/* ====================================================================== *
* TRANSPILE *
* ====================================================================== */
/** Transpile to CJS */
transpile_cjs(): Pipe {
return this._find_sources_cts()
.esbuild({
...esbuildMergedOptions,
format: 'cjs',
outdir: this.destDir,
outExtension: { '.js': this.cjsExtension },
})
},
/** Transpile to ESM */
transpile_esm(): Pipe {
return this._find_sources_mts()
.esbuild({
...esbuildMergedOptions,
format: 'esm',
outdir: this.destDir,
outExtension: { '.js': this.esmExtension },
})
},
/** Generate all .d.ts files */
transpile_types(): Pipe {
const extraTypesDir =
isDirectory(this.extraTypesDir) ?
this.extraTypesDir :
undefined
return merge([
this._find_sources(),
this._find_types(),
]).tsc(this.tsconfigJson, {
noEmit: false,
declaration: true,
emitDeclarationOnly: true,
outDir: this.destDir,
extraTypesDir,
})
},
/** Copy all resources coming alongside our sources */
copy_resources(): Pipe {
return merge([
this._find_resources(),
this._find_types(),
]).copy(this.destDir)
},
/** Transpile all source code */
async transpile(): Promise<Pipe> {
emitBanner('Transpiling source files')
if (isDirectory(this.destDir)) await rmrf(this.destDir)
const result = await merge([
this.cjs === 'true' ? this.transpile_cjs() : noop(),
this.esm === 'true' ? this.transpile_esm() : noop(),
this.transpile_types(),
this.copy_resources(),
])
return result
},
/* ====================================================================== *
* TEST & COVERAGE *
* ====================================================================== */
/** Check test types */
async test_types(): Promise<void> {
emitBanner('Checking test types')
const tsconfig = resolve(this.testDir, 'tsconfig.json')
const extraTypesDir =
isDirectory(this.extraTypesDir) ?
this.extraTypesDir :
undefined
await this
._find_tests()
.tsc(tsconfig, {
noEmit: true,
declaration: false,
emitDeclarationOnly: false,
extraTypesDir,
})
},
/** Run tests */
async test_cjs(): Promise<void> {
emitBanner('Running tests (CommonJS)')
await this
._find_tests()
.test({
coverageDir: _coverage ? this.coverageDataDir : undefined,
forceModule: 'commonjs',
})
},
/** Run tests */
async test_esm(): Promise<void> {
emitBanner('Running tests (ES Modules)')
await this
._find_tests()
.test({
coverageDir: _coverage ? this.coverageDataDir : undefined,
forceModule: 'module',
})
},
/** Run tests */
async test(): Promise<void> {
if (_coverage && isDirectory(this.coverageDataDir)) await rmrf(this.coverageDataDir)
if (this.esm === 'true') await this.test_esm()
if (this.cjs === 'true') await this.test_cjs()
},
/** Ensure tests have run and generate a coverage report */
async coverage(): Promise<Pipe> {
let coveragePipe: Pipe
// Capture error from running tests, but always produce coverage
try {
await this.test()
} finally {
emitBanner('Preparing coverage report')
coveragePipe = this._find_coverage_sources()
.coverage(this.coverageDataDir, {
reportDir: this.coverageDir,
minimumCoverage: _minimumCoverage,
minimumFileCoverage: _minimumFileCoverage,
optimalCoverage: _optimalCoverage,
optimalFileCoverage: _optimalFileCoverage,
})
}
// If the tests didn't throw, return the coverage
return coveragePipe
},
/* ====================================================================== *
* LINTING *
* ====================================================================== */
/** Lint all sources */
async lint(): Promise<void> {
emitBanner('Linting sources')
await this._find_lint_sources().eslint()
},
/* ====================================================================== *
* PACKAGE.JSON EXPORTS *
* ====================================================================== */
/** Inject `exports` into the `package.json` file */
async exports(): Promise<Pipe> {
const files = await this.transpile()
emitBanner('Updating exports in "package.json"')
const globs = [ this.exportsGlob, ..._exportsGlobs ] as const
return merge([ files ])
.filter(...globs, { directory: this.destDir, ignore: '**/*.map' })
.exports({
cjsExtension: this.cjsExtension,
esmExtension: this.esmExtension,
packageJson: this.packageJson,
outputPackageJson: this.outputPackageJson,
})
},
/* ====================================================================== *
* ALL: DO EVERYTHING *
* ====================================================================== */
/* coverage ignore next */
/** Build everything. */
async all(): Promise<void> {
if (_parallelize) {
await Promise.all([
this.transpile(),
this.test_types(),
_coverage ? this.coverage() : this.test(),
this.lint(),
])
} else {
await this.transpile()
await this.test_types()
await (_coverage ? this.coverage() : this.test())
await this.lint()
}
},
/* ====================================================================== *
* DEFAULT: DO EVERYTHING *
* ====================================================================== */
/* coverage ignore next */
/**
* Default task (simply invokes `this._all()`).
*
* Override this and invoke `this._all()` to inject tasks _before_ or
* _after_ the normal build execution.
*/
async default(): Promise<void> {
await this.all()
},
})
}