diff --git a/.vscode/settings.json b/.vscode/settings.json index 5737bf3d..95706042 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -256,6 +256,7 @@ "shellformat.flag": "-ci -fn -i=2 -sr", "shellformat.useEditorConfig": true, "terminal.integrated.copyOnSelection": true, + "terminal.integrated.scrollback": 10000, "todo-tree.filtering.ignoreGitSubmodules": true, "todo-tree.filtering.includeHiddenFiles": false, "todo-tree.filtering.useBuiltInExcludes": "file and search excludes", diff --git a/__tests__/interfaces/mock.ts b/__tests__/interfaces/mock.ts index e41f522c..df54cdb0 100644 --- a/__tests__/interfaces/mock.ts +++ b/__tests__/interfaces/mock.ts @@ -1,6 +1,6 @@ /** - * @file Test Environment Interfaces - Spy - * @module tests/interfaces/Spy + * @file Test Environment Interfaces - Mock + * @module tests/interfaces/Mock */ import type { Fn } from '@flex-development/tutils' @@ -11,9 +11,11 @@ import type * as vitest from 'vitest' * * @template F - Function being mocked * + * @extends {Fn,ReturnType>} * @extends {vitest.Mock,ReturnType>} */ interface Mock - extends vitest.Mock, ReturnType> {} + extends Fn, ReturnType>, + vitest.Mock, ReturnType> {} export type { Mock as default } diff --git a/__tests__/interfaces/spy.ts b/__tests__/interfaces/spy.ts index 5af9595a..658d2751 100644 --- a/__tests__/interfaces/spy.ts +++ b/__tests__/interfaces/spy.ts @@ -4,16 +4,18 @@ */ import type { Fn } from '@flex-development/tutils' -import type { SpyInstance } from 'vitest' +import type * as vitest from 'vitest' /** - * {@linkcode SpyInstance} utility. + * {@linkcode vitest.SpyInstance} utility. * * @template F - Function being spied on * - * @extends {SpyInstance,ReturnType>} + * @extends {Fn,ReturnType>} + * @extends {vitest.SpyInstance,ReturnType>} */ interface Spy - extends SpyInstance, ReturnType> {} + extends Fn, ReturnType>, + vitest.SpyInstance, ReturnType> {} export type { Spy as default } diff --git a/__tests__/reporters/notifier.ts b/__tests__/reporters/notifier.ts index f5c074b5..0ddab858 100644 --- a/__tests__/reporters/notifier.ts +++ b/__tests__/reporters/notifier.ts @@ -3,7 +3,7 @@ * @module tests/reporters/Notifier */ -import type { OneOrMany } from '@flex-development/tutils' +import { cast, isArray, type OneOrMany } from '@flex-development/tutils' import notifier from 'node-notifier' import type NotificationCenter from 'node-notifier/notifiers/notificationcenter' import { performance } from 'node:perf_hooks' @@ -20,22 +20,28 @@ import type { File, Reporter, Task, Test, Vitest } from 'vitest' */ class Notifier implements Reporter { /** + * Test reporter context. + * * @public - * @member {Vitest} ctx - Test reporter context + * @member {Vitest} ctx */ - public ctx: Vitest = {} as Vitest + public ctx!: Vitest /** + * Test run end time (in milliseconds). + * * @public - * @member {number} end - Test run end time (in milliseconds) + * @member {number} end */ - public end: number = 0 + public end!: number /** + * Test run start time (in milliseconds). + * * @public - * @member {number} start - Test run start time (in milliseconds) + * @member {number} start */ - public start: number = 0 + public start!: number /** * Sends a notification. @@ -52,19 +58,39 @@ class Notifier implements Reporter { files: File[] = this.ctx.state.getFiles(), errors: unknown[] = this.ctx.state.getUnhandledErrors() ): Promise { - /** @const {Test[]} tests - Tests run */ + /** + * Tests that have been run. + * + * @const {Test[]} tests + */ const tests: Test[] = this.tests(files) - /** @const {number} fails - Total number of failed tests */ + /** + * Total number of failed tests. + * + * @const {number} fails + */ const fails: number = tests.filter(t => t.result?.state === 'fail').length - /** @const {number} passes - Total number of passed tests */ + /** + * Total number of passed tests. + * + * @const {number} passes + */ const passes: number = tests.filter(t => t.result?.state === 'pass').length - /** @var {string} message - Notification message */ + /** + * Notification message. + * + * @var {string} message + */ let message: string = '' - /** @var {string} title - Notification title */ + /** + * Notification title. + * + * @var {string} title + */ let title: string = '' // get notification title and message based on number of failed tests @@ -76,7 +102,11 @@ class Notifier implements Reporter { title = '\u274C Failed' } else { - /** @const {number} time - Time to run all tests (in milliseconds) */ + /** + * Time to run all tests (in milliseconds). + * + * @const {number} time + */ const time: number = this.end - this.start message = dedent` @@ -128,7 +158,7 @@ class Notifier implements Reporter { */ public onInit(context: Vitest): void { this.ctx = context - return void (this.start = performance.now()) + return void ((this.start = performance.now()) && (this.end = 0)) } /** @@ -140,17 +170,13 @@ class Notifier implements Reporter { * @return {Test[]} `Test` object array */ protected tests(tasks: OneOrMany = []): Test[] { - const { mode } = this.ctx - - return (Array.isArray(tasks) ? tasks : [tasks]).flatMap(task => { - const { type } = task - - return mode === 'typecheck' && type === 'suite' && task.tasks.length === 0 - ? ([task] as unknown as [Test]) - : type === 'test' + return (isArray(tasks) ? tasks : [tasks]).flatMap(task => { + return task.type === 'custom' + ? [cast(task)] + : task.type === 'test' ? [task] : 'tasks' in task - ? task.tasks.flatMap(t => (t.type === 'test' ? [t] : this.tests(t))) + ? task.tasks.flatMap(task => this.tests(task)) : [] }) } diff --git a/__tests__/setup/serializers/result-array.ts b/__tests__/setup/serializers/result-array.ts index fa77c484..1e6eb2d0 100644 --- a/__tests__/setup/serializers/result-array.ts +++ b/__tests__/setup/serializers/result-array.ts @@ -4,9 +4,17 @@ */ import type { Result } from '#src/interfaces' -import { isNIL, isObjectPlain, isString } from '@flex-development/tutils' +import { + cast, + get, + isArray, + isNIL, + isObjectPlain, + isString, + omit, + pick +} from '@flex-development/tutils' import pf from 'pretty-format' -import { get, omit, pick } from 'radash' expect.addSnapshotSerializer({ /** @@ -16,7 +24,7 @@ expect.addSnapshotSerializer({ * @return {string} `value` as printable string */ print(value: unknown): string { - value = (value as Result[]).map(result => { + value = cast(value).map(result => { result.cwd = result.cwd.replace(/.*?(?=\/__.+)/, '${process.cwd()}') return { @@ -37,16 +45,16 @@ expect.addSnapshotSerializer({ * @return {value is Result[]} `true` if `value` is {@linkcode Result} array */ test(value: unknown): value is Result[] { - return Array.isArray(value) + return isArray(value) ? value.every(item => { return ( isString(get(item, 'cwd')) && - Array.isArray(get(item, 'errors')) && + isArray(get(item, 'errors')) && (isObjectPlain(get(item, 'mangleCache')) || isNIL(get(item, 'mangleCache'))) && isString(get(item, 'outdir')) && - Array.isArray(get(item, 'outputs')) && - Array.isArray(get(item, 'warnings')) + isArray(get(item, 'outputs')) && + isArray(get(item, 'warnings')) ) }) : false diff --git a/__tests__/utils/create-testing-command.ts b/__tests__/utils/create-testing-command.ts index 226c12ff..484f4a2f 100644 --- a/__tests__/utils/create-testing-command.ts +++ b/__tests__/utils/create-testing-command.ts @@ -5,6 +5,7 @@ import AppModule from '#src/cli/app.module' import { CLI_NAME } from '#src/cli/constants' +import type { Omit } from '@flex-development/tutils' import { Module } from '@nestjs/common' import type { TestingModule } from '@nestjs/testing' import { CommandRunnerModule } from 'nest-commander' diff --git a/__tests__/utils/get-package-json.ts b/__tests__/utils/get-package-json.ts index 44cf38de..f6ce7c60 100644 --- a/__tests__/utils/get-package-json.ts +++ b/__tests__/utils/get-package-json.ts @@ -5,6 +5,7 @@ import type mlly from '@flex-development/mlly' import type pkg from '@flex-development/pkg-types' +import { cast } from '@flex-development/tutils' /** * Retrieves a `package.json` object. @@ -29,7 +30,7 @@ const getPackageJson = async ( */ const fs: typeof import('node:fs') = await vi.importActual('node:fs') - return JSON.parse(fs.readFileSync(id, 'utf8')) as pkg.PackageJson + return cast(JSON.parse(fs.readFileSync(id, 'utf8'))) } export default getPackageJson diff --git a/build.config.ts b/build.config.ts index 6190b99a..2416780f 100644 --- a/build.config.ts +++ b/build.config.ts @@ -4,6 +4,7 @@ */ import { defineBuildConfig, type Config } from '#src' +import pathe from '@flex-development/pathe' import pkg from './package.json' assert { type: 'json' } import tsconfig from './tsconfig.build.json' assert { type: 'json' } @@ -15,7 +16,13 @@ import tsconfig from './tsconfig.build.json' assert { type: 'json' } const config: Config = defineBuildConfig({ charset: 'utf8', entries: [ - { ignore: ['cli/**'] }, + { dts: 'only', ignore: ['cli/**'] }, + { dts: false, pattern: ['(interfaces|types)/index.ts'] }, + { + dts: false, + pattern: ['*.ts', 'config/*', 'internal/*', 'plugins/*', 'utils/*'], + sourcemap: true + }, { bundle: true, external: [ @@ -30,11 +37,13 @@ const config: Config = defineBuildConfig({ minify: true, name: 'cli', platform: 'node', - source: 'src/cli/index.ts' + source: 'src/cli/index.ts', + sourcemap: true, + sourcesContent: false } ], - sourcemap: true, - sourcesContent: false, + minifySyntax: true, + sourceRoot: 'file' + pathe.delimiter + pathe.sep.repeat(2), target: [ pkg.engines.node.replace(/^\D+/, 'node'), tsconfig.compilerOptions.target diff --git a/config/changelog.config.ts b/config/changelog.config.ts index 3bb3a5dd..535483a2 100644 --- a/config/changelog.config.ts +++ b/config/changelog.config.ts @@ -17,7 +17,18 @@ import { type Commit } from '@flex-development/commitlint-config' import pathe from '@flex-development/pathe' -import { CompareResult, isNIL } from '@flex-development/tutils' +import { + CompareResult, + at, + constant, + includes, + isBoolean, + isNIL, + isString, + select, + trim, + type Optional +} from '@flex-development/tutils' import { Inject, Module } from '@nestjs/common' import addStream from 'add-stream' import { Command, CommanderError } from 'commander' @@ -338,13 +349,11 @@ class ChangelogCommand extends CommandRunner { const changelog: Readable = conventionalChangelog( { append: false, - debug: debug ? consola.log.bind(consola) : undefined, - outputUnreleased: - typeof outputUnreleased === 'boolean' - ? outputUnreleased - : typeof outputUnreleased === 'string' - ? !!outputUnreleased.trim() - : false, + outputUnreleased: isBoolean(outputUnreleased) + ? outputUnreleased + : isString(outputUnreleased) + ? !!trim(outputUnreleased) + : false, pkg: { path: pathe.resolve('package.json') }, preset: { header: '', @@ -382,10 +391,10 @@ class ChangelogCommand extends CommandRunner { return void apply(null, { ...commit, committerDate: dateformat(commit.committerDate, 'yyyy-mm-dd', true), - mentions: commit.mentions.filter(m => m !== 'flexdevelopment'), + mentions: select(commit.mentions, m => m !== 'flexdevelopment'), // @ts-expect-error ts(2322) raw: commit, - references: commit.references.filter(ref => ref.action !== null), + references: select(commit.references, ref => ref.action !== null), version: commit.gitTags ? vgx.exec(commit.gitTags)?.[1] : undefined }) }, @@ -480,35 +489,35 @@ class ChangelogCommand extends CommandRunner { * * @const {CommitEnhanced?} first_commit */ - const first_commit: CommitEnhanced | undefined = commits.at(0) + const first_commit: Optional = at(commits, 0) /** * Last commit in release. * * @const {CommitEnhanced?} last_commit */ - const last_commit: CommitEnhanced | undefined = commits.at(-1) + const last_commit: Optional = at(commits, -1) // set current and previous tags if (key && (!currentTag || !previousTag)) { - currentTag = key.version ?? undefined + currentTag = key.version // try setting previous tag based on current tag - if (gitSemverTags.includes(currentTag ?? '')) { + if (includes(gitSemverTags, currentTag)) { const { version = '' } = key previousTag = gitSemverTags[gitSemverTags.indexOf(version) + 1] - if (!previousTag) previousTag = last_commit?.hash ?? undefined + if (!previousTag) previousTag = last_commit?.hash } } else { currentTag = /^unreleased$/i.test(version ?? '') ? currentTag ?? - (typeof outputUnreleased === 'string' && outputUnreleased + (isString(outputUnreleased) && outputUnreleased ? outputUnreleased - : first_commit?.hash ?? undefined) + : first_commit?.hash) : !currentTag && version ? pkg.tagPrefix + version : currentTag ?? version - previousTag = previousTag ?? gitSemverTags[0] + previousTag = previousTag ?? at(gitSemverTags, 0) } // set release date @@ -615,7 +624,7 @@ class ChangelogCommand extends CommandRunner { cmd.addHelpCommand(false) cmd.allowExcessArguments(false) cmd.allowUnknownOption(false) - cmd.createHelp = () => this.help + cmd.createHelp = constant(this.help) cmd.combineFlagAndOptionalValue(false) cmd.enablePositionalOptions() cmd.helpOption(false) diff --git a/loader.mjs b/loader.mjs index d3ede91e..b450549f 100644 --- a/loader.mjs +++ b/loader.mjs @@ -68,7 +68,7 @@ export const load = async (url, context) => { /** * Module source code. * - * @type {esm.Source | undefined} + * @type {tutils.Optional>} * @var source */ let source = await mlly.getSource(url, { format: context.format }) @@ -123,6 +123,7 @@ export const load = async (url, context) => { return { format: context.format, shortCircuit: true, source } } + /** * Resolves the given module `specifier`, and its module format as a hint to the * {@linkcode load} hook. diff --git a/package.json b/package.json index 5e42b128..cee1ac53 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,7 @@ "type": "module", "files": [ "CHANGELOG.md", - "dist", - "src" + "dist" ], "bin": "./dist/cli.mjs", "exports": { @@ -44,7 +43,7 @@ "module": "./dist/index.mjs", "types": "./dist/index.d.mts", "scripts": { - "build": "node --loader=./loader.mjs ./src/cli", + "build": "node --enable-source-maps --loader=./loader.mjs ./src/cli", "changelog": "node --loader=./loader.mjs ./config/changelog.config", "check:ci": "yarn dedupe --check && yarn check:format && yarn check:lint && yarn check:spelling && yarn typecheck && yarn test:cov && NODE_ENV=production yarn pack -o %s-%v.tgz && yarn clean:pack && yarn check:types:build && yarn pkg-size", "check:format": "prettier --check .", @@ -89,16 +88,13 @@ "@flex-development/pkg-types": "2.0.0", "@flex-development/toggle-pkg-type": "2.0.0", "@flex-development/tsconfig-utils": "1.1.2", - "@flex-development/tutils": "6.0.0-alpha.10", + "@flex-development/tutils": "6.0.0-alpha.18", "colorette": "2.0.20", "consola": "3.1.0", "cosmiconfig": "8.1.3", - "escape-string-regexp": "5.0.0", "exit-hook": "3.2.0", "fast-glob": "3.2.12", - "merge-anything": "5.1.6", - "pretty-bytes": "6.1.0", - "radash": "10.8.1" + "pretty-bytes": "6.1.0" }, "devDependencies": { "@commitlint/cli": "17.6.3", @@ -208,9 +204,7 @@ }, "resolutions": { "@ardatan/sync-fetch": "larsgw/sync-fetch#head=worker_threads", - "@flex-development/tutils": "6.0.0-alpha.10", - "nest-commander@npm:3.6.1": "patch:nest-commander@npm%3A3.6.1#patches/nest-commander+3.6.1.dev.patch", - "radash@npm:10.8.1": "patch:radash@npm%3A10.8.1#patches/radash+10.8.1.dev.patch" + "nest-commander@npm:3.6.1": "patch:nest-commander@npm%3A3.6.1#patches/nest-commander+3.6.1.dev.patch" }, "engines": { "node": ">=16.20.0", diff --git a/patches/radash+10.8.1.dev.patch b/patches/radash+10.8.1.dev.patch deleted file mode 100644 index 3ffa779e..00000000 --- a/patches/radash+10.8.1.dev.patch +++ /dev/null @@ -1,7 +0,0 @@ -diff --git a/dist/esm/object.mjs b/dist/esm/object.mjs -index v10.8.1..v10.8.1 100644 ---- a/dist/esm/object.mjs -+++ b/dist/esm/object.mjs -@@ -114,1 +114,1 @@ -- if (!path || !value) -+ if (!path || value === undefined) diff --git a/src/cli/__tests__/app.module.functional.spec.ts b/src/cli/__tests__/app.module.functional.spec.ts index 58a0e9d6..a13be7ef 100644 --- a/src/cli/__tests__/app.module.functional.spec.ts +++ b/src/cli/__tests__/app.module.functional.spec.ts @@ -6,12 +6,12 @@ import { ERR_MODULE_NOT_FOUND, type NodeError } from '@flex-development/errnode' import * as mlly from '@flex-development/mlly' import pathe from '@flex-development/pathe' +import { cast, template } from '@flex-development/tutils' import * as color from 'colorette' import { CommanderError } from 'commander' import consola from 'consola' import * as esbuild from 'esbuild' import { fileURLToPath } from 'node:url' -import { template } from 'radash' import TestSubject from '../app.module' vi.mock('esbuild') @@ -32,7 +32,7 @@ describe('functional:cli/AppModule', () => { TestSubject.errorHandler(error) // Expect - expect(process).to.have.property('exitCode').equal(error.exitCode) + expect(process).to.have.property('exitCode', error.exitCode) expect(consola.log).not.toHaveBeenCalled() }) @@ -41,7 +41,7 @@ describe('functional:cli/AppModule', () => { const code: string = 'commander.unknownOption' const message: string = "error: unknown option '--formatter'" const error: CommanderError = new CommanderError(1, code, message) - const log: string = template('{{0}} {{1}} {{2}}', { + const log: string = template('{0} {1} {2}', { 0: color.red('✘'), 1: color.bgRed('[ERROR]'), 2: color.white(error.message) @@ -65,7 +65,7 @@ describe('functional:cli/AppModule', () => { fileURLToPath(mlly.toURL('dist/make.mjs')), 'module' ) - const log: string = template('{{0}} {{1}} {{2}}', { + const log: string = template('{0} {1} {2}', { 0: color.red('✘'), 1: color.bgRed('[ERROR]'), 2: color.white(error.message) @@ -75,21 +75,23 @@ describe('functional:cli/AppModule', () => { TestSubject.serviceErrorHandler(error) // Expect - expect(process).to.have.property('exitCode').equal(1) + expect(process).to.have.property('exitCode', 1) expect(consola.log).toHaveBeenCalledOnce() expect(consola.log).toHaveBeenCalledWith(log) }) it('should handle esbuild.BuildFailure', () => { // Act - TestSubject.serviceErrorHandler({ - ...new Error('Cannot use "external" without "bundle"'), - errors: [], - warnings: [] - } as esbuild.BuildFailure) + TestSubject.serviceErrorHandler( + cast({ + ...new Error('Cannot use "external" without "bundle"'), + errors: [], + warnings: [] + }) + ) // Expect - expect(process).to.have.property('exitCode').equal(1) + expect(process).to.have.property('exitCode', 1) expect(consola.log).not.toHaveBeenCalled() }) }) diff --git a/src/cli/app.module.ts b/src/cli/app.module.ts index 751120f3..fdb17f96 100644 --- a/src/cli/app.module.ts +++ b/src/cli/app.module.ts @@ -3,12 +3,12 @@ * @module mkbuild/cli/AppModule */ +import { cast, isArray, template } from '@flex-development/tutils' import { Module } from '@nestjs/common' import * as color from 'colorette' import type * as commander from 'commander' import consola from 'consola' import * as esbuild from 'esbuild' -import { template } from 'radash' import { MkbuildCommand } from './commands' import { HelpService, UtilityService } from './providers' @@ -35,7 +35,7 @@ class AppModule { public static errorHandler(error: commander.CommanderError): void { if (error.exitCode) { consola.log( - template('{{0}} {{1}} {{2}}', { + template('{{0} {1} {2}', { 0: color.red('✘'), 1: color.bgRed('[ERROR]'), 2: color.white(error.message) @@ -56,12 +56,12 @@ class AppModule { * @return {void} Nothing when complete */ public static serviceErrorHandler(error: Error): void { - const { errors, warnings } = error as esbuild.BuildFailure + const { errors, warnings } = cast(error) // format and log non-esbuild error - if (!Array.isArray(errors) && !Array.isArray(warnings)) { + if (!isArray(errors) && !isArray(warnings)) { consola.log( - template('{{0}} {{1}} {{2}}', { + template('{{0} {1} {2}', { 0: color.red('✘'), 1: color.bgRed('[ERROR]'), 2: color.white(error.message) diff --git a/src/cli/commands/__tests__/mkbuild.command.functional.spec.ts b/src/cli/commands/__tests__/mkbuild.command.functional.spec.ts index 96ba690e..8417255d 100644 --- a/src/cli/commands/__tests__/mkbuild.command.functional.spec.ts +++ b/src/cli/commands/__tests__/mkbuild.command.functional.spec.ts @@ -10,11 +10,11 @@ import type { Jsx, LegalComments, OutputExtension, Sourcemap } from '#src/types' import type { Spy } from '#tests/interfaces' import createTestingCommand from '#tests/utils/create-testing-command' import * as mlly from '@flex-development/mlly' +import { cast, descriptor } from '@flex-development/tutils' import type { TestingModule } from '@nestjs/testing' import consola from 'consola' import type * as esbuild from 'esbuild' import { CommandTestFactory } from 'nest-commander-testing' -import { get } from 'radash' import TestSubject from '../mkbuild.command' vi.mock('#src/make') @@ -431,7 +431,7 @@ describe('functional:cli/commands/MkbuildCommand', () => { let subject: TestSubject beforeEach(() => { - formatHelp = vi.spyOn(command.get(HelpService), 'formatHelp') + formatHelp = cast(vi.spyOn(command.get(HelpService), 'formatHelp')) subject = command.get(TestSubject) }) @@ -441,7 +441,7 @@ describe('functional:cli/commands/MkbuildCommand', () => { // Expect expect(formatHelp).toHaveBeenCalledOnce() - expect(formatHelp).toBeCalledWith(get(subject, 'command')) + expect(formatHelp).toBeCalledWith(descriptor(subject, 'command').value) expect(consola.log).toHaveBeenCalledOnce() expect(make).not.toHaveBeenCalled() }) @@ -452,7 +452,7 @@ describe('functional:cli/commands/MkbuildCommand', () => { // Expect expect(formatHelp).toHaveBeenCalledOnce() - expect(formatHelp).toBeCalledWith(get(subject, 'command')) + expect(formatHelp).toBeCalledWith(descriptor(subject, 'command').value) expect(consola.log).toHaveBeenCalledOnce() expect(make).not.toHaveBeenCalled() }) diff --git a/src/cli/commands/mkbuild.command.ts b/src/cli/commands/mkbuild.command.ts index 8fd579f4..2d1435ac 100644 --- a/src/cli/commands/mkbuild.command.ts +++ b/src/cli/commands/mkbuild.command.ts @@ -17,7 +17,17 @@ import type { } from '#src/types' import { IGNORE_PATTERNS, loaders } from '#src/utils' import * as mlly from '@flex-development/mlly' -import type * as pathe from '@flex-development/pathe' +import * as pathe from '@flex-development/pathe' +import { + DOT, + cast, + constant, + construct, + entries, + join, + keys, + set +} from '@flex-development/tutils' import { Inject } from '@nestjs/common' import type * as commander from 'commander' import consola from 'consola' @@ -28,7 +38,6 @@ import { OptionChoiceFor, RootCommand } from 'nest-commander' -import { construct, set, shake } from 'radash' /** * `mkbuild` command model. @@ -279,7 +288,7 @@ class MkbuildCommand extends CommandRunner { name: 'charset' }) protected parseCharset(val: string): esbuild.Charset { - return this.util.parseString(val) + return cast(val) } /** @@ -399,7 +408,7 @@ class MkbuildCommand extends CommandRunner { * @return {string} Parsed option value */ @Option({ - defaultValue: '.', + defaultValue: DOT, description: 'Current working directory', flags: '--cwd ', name: 'cwd' @@ -488,7 +497,7 @@ class MkbuildCommand extends CommandRunner { name: 'ext' }) protected parseExt(val: string): OutputExtension { - return this.util.parseString(val) + return cast(val) } /** @@ -550,7 +559,7 @@ class MkbuildCommand extends CommandRunner { name: 'format' }) protected parseFormat(val: string): esbuild.Format { - return this.util.parseString(val) + return cast(val) } /** @@ -672,7 +681,7 @@ class MkbuildCommand extends CommandRunner { name: 'jsx' }) protected parseJsx(val: string): Jsx { - return this.util.parseString(val) + return cast(val) } /** @@ -817,7 +826,7 @@ class MkbuildCommand extends CommandRunner { name: 'legalComments' }) protected parseLegalComments(val: string): LegalComments { - return this.util.parseString(val) + return cast(val) } /** @@ -833,9 +842,11 @@ class MkbuildCommand extends CommandRunner { @Option({ defaultValue: loaders(), defaultValueDescription: JSON.stringify( - Object.entries(loaders()) - .map(([ext, loader]): string => `${ext}:${loader}`) - .join(',') + join( + entries(loaders()).map(([ext, loader]): string => { + return `${ext}${pathe.delimiter}${loader}` + }) + ) ), description: 'https://esbuild.github.io/api/#loader', flags: '--loader ', @@ -863,7 +874,7 @@ class MkbuildCommand extends CommandRunner { name: 'logLevel' }) protected parseLogLevel(val: string): esbuild.LogLevel { - return this.util.parseString(val) + return cast(val) } /** @@ -1216,7 +1227,7 @@ class MkbuildCommand extends CommandRunner { name: 'platform' }) protected parsePlatform(val: string): esbuild.Platform { - return this.util.parseString(val) + return cast(val) } /** @@ -1498,7 +1509,7 @@ class MkbuildCommand extends CommandRunner { try { return this.util.parseBoolean(val) } catch { - return this.util.parseString(val) + return cast(val) } } @@ -1679,7 +1690,7 @@ class MkbuildCommand extends CommandRunner { */ public async run(_: string[], flags: Flags = {}): Promise { // remove defaults to prevent accidental config file option override - for (const key of Object.keys(flags)) { + for (const key of keys(flags)) { if (this.command.getOptionValueSource(key) !== 'default') continue Reflect.deleteProperty(flags, key) } @@ -1691,7 +1702,7 @@ class MkbuildCommand extends CommandRunner { if (flags.version) return void consola.log(pkg.version) // run make - return void (await make(shake(set(construct(flags), 'write', true)))) + return void (await make(set(construct(flags), 'write', true))) } /** @@ -1708,7 +1719,7 @@ class MkbuildCommand extends CommandRunner { cmd.allowExcessArguments() cmd.allowUnknownOption(false) cmd.combineFlagAndOptionalValue(false) - cmd.createHelp = /* c8 ignore next */ () => this.help + cmd.createHelp = constant(this.help) cmd.enablePositionalOptions() cmd.helpOption(false) cmd.showHelpAfterError() diff --git a/src/cli/index.ts b/src/cli/index.ts index 12944b5a..bcd9aa9f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -5,6 +5,7 @@ * @module mkbuild/cli */ +import type { Optional } from '@flex-development/tutils' import mri from 'mri' import { CommandFactory } from 'nest-commander' import AppModule from './app.module' @@ -45,7 +46,7 @@ const { 'serve.keyfile'?: string 'serve.port'?: number 'serve.servedir'?: string - serve?: boolean | undefined + serve?: Optional watch?: boolean }>(process.argv.slice(2), { alias: { serve: 'S', watch: 'w' }, diff --git a/src/cli/providers/__tests__/help.service.spec.ts b/src/cli/providers/__tests__/help.service.spec.ts index f191ad06..20f4839a 100644 --- a/src/cli/providers/__tests__/help.service.spec.ts +++ b/src/cli/providers/__tests__/help.service.spec.ts @@ -4,9 +4,9 @@ */ import { CLI_NAME } from '#src/cli/constants' +import { cast, set } from '@flex-development/tutils' import * as commander from 'commander' import { Command } from 'commander' -import { set } from 'radash' import TestSubject from '../help.service' describe('unit:cli/providers/HelpService', () => { @@ -67,14 +67,14 @@ describe('unit:cli/providers/HelpService', () => { try { subject.optionTermByName(name, cmd) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect expect(error).to.be.instanceof(commander.CommanderError) - expect(error).to.have.property('code').equal('commander.unknownOption') - expect(error).to.have.property('exitCode').equal(1) - expect(error).to.have.property('message').equal(message) + expect(error).to.have.property('code', 'commander.unknownOption') + expect(error).to.have.property('exitCode', 1) + expect(error).to.have.property('message', message) }) }) }) diff --git a/src/cli/providers/__tests__/utility.service.spec.ts b/src/cli/providers/__tests__/utility.service.spec.ts index 8b48849a..af1e42c9 100644 --- a/src/cli/providers/__tests__/utility.service.spec.ts +++ b/src/cli/providers/__tests__/utility.service.spec.ts @@ -56,14 +56,4 @@ describe('unit:cli/providers/UtilityService', () => { }) }) }) - - describe('#parseString', () => { - it('should return val', () => { - // Arrange - const string: string = 'esm' - - // Act + Expect - expect(subject.parseString(string)).to.equal(string) - }) - }) }) diff --git a/src/cli/providers/help.service.ts b/src/cli/providers/help.service.ts index e6e2a402..4f93a025 100644 --- a/src/cli/providers/help.service.ts +++ b/src/cli/providers/help.service.ts @@ -3,9 +3,17 @@ * @module mkbuild/cli/providers/HelpService */ +import { + flat, + fork, + ifelse, + sort, + template, + trim, + type Optional +} from '@flex-development/tutils' import { Injectable } from '@nestjs/common' import * as commander from 'commander' -import { flat, fork, template } from 'radash' import { dedent } from 'ts-dedent' import wrap from 'word-wrap' @@ -90,7 +98,9 @@ class HelpService extends commander.Help { */ const options: commander.Option[] = flat( fork( - this.visibleOptions(cmd).sort((a, b) => a.long!.localeCompare(b.long!)), + sort(this.visibleOptions(cmd), (a, b) => { + return a.long!.localeCompare(b.long!) + }), option => option.name() !== 'help' && option.name() !== 'version' ) ) @@ -100,7 +110,7 @@ class HelpService extends commander.Help { ${indentation}${this.wrap(this.commandDescription(cmd), linewidth, 0)} Usage - ${indentation}${template('$ {{0}}', { 0: this.commandUsage(cmd) })} + ${indentation}${template('$ {0}', { 0: this.commandUsage(cmd) })} Options ${options.reduce((acc: string, option: commander.Option): string => { @@ -123,7 +133,7 @@ class HelpService extends commander.Help { * * @const {number} left */ - const left: number = term.length + indent * (option.short ? 2 : 3) + const left: number = term.length + indent * ifelse(option.short, 2, 3) /** * String representation of {@linkcode option}. @@ -132,7 +142,7 @@ class HelpService extends commander.Help { */ const str: string = indentation + - ' '.repeat(indent * (option.short ? 0 : 2)) + + ' '.repeat(indent * ifelse(option.short, 0, 2)) + term + wrap( this.wrap( @@ -142,9 +152,9 @@ class HelpService extends commander.Help { ), { cut: true, - indent: indentation.trim(), - newline: template('\n{{0}}', { - 0: ' '.repeat(left - indent * (option.short ? 1 : 0)) + indent: trim(indentation), + newline: template('\n{0}', { + 0: ' '.repeat(left - indent * ifelse(option.short, 1, 0)) }), width: linewidth - left } @@ -180,9 +190,9 @@ class HelpService extends commander.Help { /** * Option in {@linkcode options} with the name {@linkcode name}, if any. * - * @const {commander.Option | undefined} option + * @const {Optional} option */ - const option: commander.Option | undefined = options.find(option => { + const option: Optional = options.find(option => { return option.name() === name }) @@ -191,7 +201,7 @@ class HelpService extends commander.Help { throw new commander.CommanderError( 1, 'commander.unknownOption', - template("error: unknown option '{{name}}'", { name }) + template("error: unknown option '{name}'", { name }) ) } diff --git a/src/cli/providers/utility.service.ts b/src/cli/providers/utility.service.ts index f823806d..658914ff 100644 --- a/src/cli/providers/utility.service.ts +++ b/src/cli/providers/utility.service.ts @@ -3,10 +3,16 @@ * @module mkbuild/cli/providers/UtilityService */ -import type { JsonPrimitive } from '@flex-development/tutils' +import { + cast, + objectify, + select, + split, + trim, + type JsonPrimitive +} from '@flex-development/tutils' import { Injectable } from '@nestjs/common' import { CliUtilityService } from 'nest-commander' -import { objectify } from 'radash' /** * CLI utilities provider. @@ -32,8 +38,8 @@ class UtilityService extends CliUtilityService { delimiter: string = ',' ): Set { return val.includes(delimiter) - ? new Set(val.split(delimiter).map(item => item.trim()) as T[]) - : new Set([val.trim()] as T[]) + ? new Set(cast(select(split(val, delimiter), null, trim))) + : new Set(cast([trim(val)])) } /** @@ -52,7 +58,7 @@ class UtilityService extends CliUtilityService { public parseObject< K extends string = string, V extends JsonPrimitive = string - >(val: string): Record { + >(val: string): { [H in K]: V } { /** * Regular expression matching a key/value pair list. * @@ -62,16 +68,18 @@ class UtilityService extends CliUtilityService { /(?:^|(?:(?(?[^,].+?):(?(?:.+?(?=,(?=.+?:)))|(?:.+?(?=,?$))))/gs // convert key/pair list into object - return objectify<[string, string], K, V>( - [...val.matchAll(regex)].map(([, , key, value]) => [key!.trim(), value!]), - ([key]: [string, string]): K => this.parseString(key), - ([, value]: [string, string]): V => { - try { - return JSON.parse(value) as V - } catch { - return value as V + return cast( + objectify( + select([...val.matchAll(regex)], null, ([, , k, v]) => [trim(k!), v!]), + ([key]): K => cast(key), + ([, value]): V => { + try { + return cast(JSON.parse(cast(value))) + } catch { + return cast(value) + } } - } + ) ) } @@ -87,20 +95,6 @@ class UtilityService extends CliUtilityService { const [, pattern = '', flags] = /^\/(.+)\/(.+$)?/.exec(val) ?? [] return new RegExp(pattern, flags) } - - /** - * Helper for casting the given string value to a more complex string type. - * - * @public - * - * @template T - Complex string type - * - * @param {string} val - String to evaluate - * @return {T} `val` as `T` - */ - public parseString(val: string): T { - return val as T - } } export default UtilityService diff --git a/src/config/load.ts b/src/config/load.ts index c124f2e3..3fd480f3 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -5,6 +5,7 @@ import type { Config } from '#src/interfaces' import pathe from '@flex-development/pathe' +import { DOT, cast, get } from '@flex-development/tutils' import { cosmiconfig } from 'cosmiconfig' import es from './loader-es' @@ -18,10 +19,10 @@ import es from './loader-es' * * @async * - * @param {string} [location='.'] - Directory to search + * @param {string} [location=DOT] - Directory to search * @return {Config} Build configuration options */ -const loadBuildConfig = async (location: string = '.'): Promise => { +const loadBuildConfig = async (location: string = DOT): Promise => { /** * Module name. * @@ -44,7 +45,7 @@ const loadBuildConfig = async (location: string = '.'): Promise => { stopDir: pathe.resolve(location) }) - return ((await search(location))?.config as Config | null) ?? {} + return cast(get((await search(location)) ?? undefined, 'config', {})) } export default loadBuildConfig diff --git a/src/config/loader-es.ts b/src/config/loader-es.ts index 22bf84b7..17be8736 100644 --- a/src/config/loader-es.ts +++ b/src/config/loader-es.ts @@ -7,7 +7,7 @@ import type { Config } from '#src/interfaces' import * as mlly from '@flex-development/mlly' import * as pathe from '@flex-development/pathe' import * as tscu from '@flex-development/tsconfig-utils' -import type { EmptyString } from '@flex-development/tutils' +import { cast, get, type EmptyString } from '@flex-development/tutils' import * as esbuild from 'esbuild' import { pathToFileURL, type URL } from 'node:url' @@ -43,7 +43,7 @@ const esLoader = async (path: string, content: string): Promise => { // convert content to data url if content does not need to be transformed if (!/^\.(?:cts|mts|ts)$/.test(ext)) { content = mlly.toDataURL(await mlly.resolveModules(content, { parent })) - return ((await import(content)) as { default: Config }).default + return cast(get(await import(content), 'default')) } /** @@ -65,14 +65,17 @@ const esLoader = async (path: string, content: string): Promise => { content = await mlly.resolveModules(content, { parent }) // convert content to pure javascript - const { code } = await esbuild.transform(content, { - format: 'esm', - loader: ext.slice(/^\.[cm]/.test(ext) ? 2 : 1), - sourcefile: path, - tsconfigRaw: { compilerOptions: tscu.loadCompilerOptions(tsconfig) } - } as esbuild.TransformOptions) + const { code } = await esbuild.transform( + content, + cast({ + format: 'esm', + loader: ext.slice(/^\.[cm]/.test(ext) ? 2 : 1), + sourcefile: path, + tsconfigRaw: { compilerOptions: tscu.loadCompilerOptions(tsconfig) } + }) + ) - return ((await import(mlly.toDataURL(code))) as { default: Config }).default + return cast(get(await import(mlly.toDataURL(code)), 'default')) } export default esLoader diff --git a/src/interfaces/__tests__/config.spec-d.ts b/src/interfaces/__tests__/config.spec-d.ts index 0e50a011..c2f42c5d 100644 --- a/src/interfaces/__tests__/config.spec-d.ts +++ b/src/interfaces/__tests__/config.spec-d.ts @@ -4,6 +4,7 @@ */ import type { FileSystemAdapter } from '#src/types' +import type { Omit, Optional } from '@flex-development/tutils' import type * as esbuild from 'esbuild' import type TestSubject from '../config' import type Options from '../options' @@ -17,36 +18,36 @@ describe('unit-d:interfaces/Config', () => { it('should match [configfile?: boolean]', () => { expectTypeOf() .toHaveProperty('configfile') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [entries?: Partial>[]]', () => { expectTypeOf() .toHaveProperty('entries') - .toEqualTypeOf>[] | undefined>() + .toEqualTypeOf>[]>>() }) it('should match [fs?: FileSystemAdapter]', () => { expectTypeOf() .toHaveProperty('fs') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [serve?: esbuild.ServeOptions | boolean]', () => { expectTypeOf() .toHaveProperty('serve') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [watch?: boolean]', () => { expectTypeOf() .toHaveProperty('watch') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [write?: boolean]', () => { expectTypeOf() .toHaveProperty('write') - .toEqualTypeOf() + .toEqualTypeOf>() }) }) diff --git a/src/interfaces/__tests__/flags.spec-d.ts b/src/interfaces/__tests__/flags.spec-d.ts index 79c55015..21449e6f 100644 --- a/src/interfaces/__tests__/flags.spec-d.ts +++ b/src/interfaces/__tests__/flags.spec-d.ts @@ -3,6 +3,7 @@ * @module mkbuild/interfaces/tests/unit-d/Flags */ +import type { Omit, Optional } from '@flex-development/tutils' import type TestSubject from '../flags' import type Options from '../options' @@ -14,18 +15,18 @@ describe('unit-d:interfaces/Flags', () => { it('should match [help?: boolean]', () => { expectTypeOf() .toHaveProperty('help') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [version?: boolean]', () => { expectTypeOf() .toHaveProperty('version') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [watch?: boolean]', () => { expectTypeOf() .toHaveProperty('watch') - .toEqualTypeOf() + .toEqualTypeOf>() }) }) diff --git a/src/interfaces/__tests__/options.spec-d.ts b/src/interfaces/__tests__/options.spec-d.ts index 6c138c83..c192ba17 100644 --- a/src/interfaces/__tests__/options.spec-d.ts +++ b/src/interfaces/__tests__/options.spec-d.ts @@ -9,7 +9,7 @@ import type { OutputExtension } from '#src/types' import type * as pathe from '@flex-development/pathe' -import type { OneOrMany } from '@flex-development/tutils' +import type { OneOrMany, Optional } from '@flex-development/tutils' import type * as esbuild from 'esbuild' import type TestSubject from '../options' @@ -21,96 +21,96 @@ describe('unit-d:interfaces/Options', () => { it('should match [banner?: { [K in GeneratedFileType]?: string }]', () => { expectTypeOf() .toHaveProperty('banner') - .toEqualTypeOf<{ [K in GeneratedFileType]?: string } | undefined>() + .toEqualTypeOf>() }) it('should match [clean?: boolean]', () => { expectTypeOf() .toHaveProperty('clean') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [conditions?: Set | string[]]', () => { expectTypeOf() .toHaveProperty('conditions') - .toEqualTypeOf | string[] | undefined>() + .toEqualTypeOf | string[]>>() }) it('should match [createRequire?: boolean]', () => { expectTypeOf() .toHaveProperty('createRequire') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [cwd?: string]', () => { expectTypeOf() .toHaveProperty('cwd') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [dts?: boolean | "only"]', () => { expectTypeOf() .toHaveProperty('dts') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [ext?: OutputExtension]', () => { expectTypeOf() .toHaveProperty('ext') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [footer?: { [K in GeneratedFileType]?: string }]', () => { expectTypeOf() .toHaveProperty('footer') - .toEqualTypeOf<{ [K in GeneratedFileType]?: string } | undefined>() + .toEqualTypeOf>() }) it('should match [ignore?: Set | string[]]', () => { expectTypeOf() .toHaveProperty('ignore') - .toEqualTypeOf | string[] | undefined>() + .toEqualTypeOf | string[]>>() }) it('should match [loader?: Record]', () => { expectTypeOf() .toHaveProperty('loader') - .toEqualTypeOf | undefined>() + .toEqualTypeOf>>() }) it('should match [mainFields?: Set | string[]]', () => { expectTypeOf() .toHaveProperty('mainFields') - .toEqualTypeOf | string[] | undefined>() + .toEqualTypeOf | string[]>>() }) it('should match [name?: string]', () => { expectTypeOf() .toHaveProperty('name') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [outExtension?: Record]', () => { expectTypeOf() .toHaveProperty('outExtension') - .toEqualTypeOf | undefined>() + .toEqualTypeOf>>() }) it('should match [pattern?: OneOrMany | Set]', () => { expectTypeOf() .toHaveProperty('pattern') - .toEqualTypeOf | Set | undefined>() + .toEqualTypeOf | Set>>() }) it('should match [resolveExtensions?: Set | string[]]', () => { expectTypeOf() .toHaveProperty('resolveExtensions') - .toEqualTypeOf | string[] | undefined>() + .toEqualTypeOf | string[]>>() }) it('should match [source?: string]', () => { expectTypeOf() .toHaveProperty('source') - .toEqualTypeOf() + .toEqualTypeOf>() }) }) diff --git a/src/interfaces/config.ts b/src/interfaces/config.ts index fc15ae83..69349b82 100644 --- a/src/interfaces/config.ts +++ b/src/interfaces/config.ts @@ -4,6 +4,7 @@ */ import type { FileSystemAdapter } from '#src/types' +import type { Omit } from '@flex-development/tutils' import type * as esbuild from 'esbuild' import type Options from './options' import type Task from './task' diff --git a/src/interfaces/flags.ts b/src/interfaces/flags.ts index 0d4ee639..548c08a6 100644 --- a/src/interfaces/flags.ts +++ b/src/interfaces/flags.ts @@ -3,6 +3,7 @@ * @module mkbuild/interfaces/Flags */ +import type { Omit } from '@flex-development/tutils' import type Options from './options' /** diff --git a/src/internal/__snapshots__/defu-concat.snap b/src/internal/__snapshots__/defu-concat.snap deleted file mode 100644 index 8729e251..00000000 --- a/src/internal/__snapshots__/defu-concat.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`unit:internal/defuConcat > should return object with defaults assigned 1`] = ` -{ - "bundle": false, - "clean": true, - "cwd": ".", - "entries": [], - "ext": ".mjs", - "format": "esm", - "fs": { - "lstat": [Function lstat], - "mkdir": [Function mkdir], - "readdir": [Function readdir], - "readdirSync": [Function readdirSync], - "rm": [Function rm], - "stat": [Function stat], - "unlink": [Function unlink], - "writeFile": [Function writeFile], - }, - "ignore": [ - "src/cli.ts", - "**/.DS_*", - "**/__mocks__", - "**/__snapshots__", - "**/__tests__", - ], - "outdir": "dist", - "pattern": "*.ts", - "source": ".", - "sourcemap": true, - "sourcesContent": false, - "target": [ - "node16.20.0", - "es2022", - ], - "tsconfig": "tsconfig.build.json", -} -`; diff --git a/src/internal/__snapshots__/defu.snap b/src/internal/__snapshots__/defu.snap deleted file mode 100644 index 5bcbab2d..00000000 --- a/src/internal/__snapshots__/defu.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`unit:internal/defu > should return object with defaults assigned 1`] = ` -{ - "cwd": ".", - "entries": [ - { - "ignore": [ - "cli.ts", - ], - }, - { - "bundle": true, - "minify": true, - "platform": "node", - "source": "src/cli.ts", - }, - ], - "fs": { - "lstat": [Function lstat], - "mkdir": [Function mkdir], - "readdir": [Function readdir], - "readdirSync": [Function readdirSync], - "rm": [Function rm], - "stat": [Function stat], - "unlink": [Function unlink], - "writeFile": [Function writeFile], - }, - "outdir": "dist", - "source": ".", - "sourcemap": true, - "sourcesContent": false, - "target": "node16.20.0", - "tsconfig": "tsconfig.build.json", - "watch": false, - "write": false, -} -`; diff --git a/src/internal/__tests__/create-context.integration.spec.ts b/src/internal/__tests__/create-context.integration.spec.ts index 87b36129..687bfa8e 100644 --- a/src/internal/__tests__/create-context.integration.spec.ts +++ b/src/internal/__tests__/create-context.integration.spec.ts @@ -10,6 +10,7 @@ import getPackageJson from '#tests/utils/get-package-json' import * as mlly from '@flex-development/mlly' import * as pathe from '@flex-development/pathe' import type { PackageJson } from '@flex-development/pkg-types' +import { DOT } from '@flex-development/tutils' import * as esbuild from 'esbuild' import testSubject from '../create-context' @@ -102,7 +103,7 @@ describe('integration:internal/createContext', () => { mainFields, metafile: true, outExtension: { '.js': '.cjs' }, - outbase: '.', + outbase: DOT, outdir, platform, plugins: [ @@ -119,7 +120,7 @@ describe('integration:internal/createContext', () => { it('should create context for cjs transpilation', async () => { // Arrange const pattern: string = 'buddy.js' - const source: string = '.' + const source: string = DOT // Act await testSubject({ ...task, pattern, source }, pkg) @@ -336,7 +337,7 @@ describe('integration:internal/createContext', () => { mainFields, metafile: true, outExtension: { '.js': '.js' }, - outbase: '.', + outbase: DOT, outdir, platform, plugins: [ @@ -353,7 +354,7 @@ describe('integration:internal/createContext', () => { it('should create context for iife transpilation', async () => { // Arrange const pattern: string[] = ['find-uniq.cts'] - const source: string = '.' + const source: string = DOT // Act await testSubject({ ...task, pattern, source }) diff --git a/src/internal/__tests__/create-context.spec.ts b/src/internal/__tests__/create-context.spec.ts index f1659fc7..8774c6fe 100644 --- a/src/internal/__tests__/create-context.spec.ts +++ b/src/internal/__tests__/create-context.spec.ts @@ -5,7 +5,7 @@ import pkg from '#pkg' assert { type: 'json' } import type { Task } from '#src' -import type { PackageJson } from '@flex-development/pkg-types' +import { cast } from '@flex-development/tutils' import testSubject from '../create-context' vi.mock('#src/utils/fs') @@ -22,7 +22,7 @@ describe('unit:internal/createContext', () => { } // Act - const result = await testSubject(task, pkg as PackageJson) + const result = await testSubject(task, cast(pkg)) // Expect expect(result).to.have.property('cancel').be.instanceof(Function) diff --git a/src/internal/__tests__/defu-concat.spec.ts b/src/internal/__tests__/defu-concat.spec.ts deleted file mode 100644 index 387aaabf..00000000 --- a/src/internal/__tests__/defu-concat.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file Unit Tests - defuConcat - * @module mkbuild/internal/tests/unit/defuConcat - */ - -import type { Config } from '#src/interfaces' -import fs from '#src/utils/fs' -import testSubject from '../defu-concat' - -describe('unit:internal/defuConcat', () => { - it('should return object with defaults assigned', () => { - // Arrange - const object: Config = { - ignore: ['src/cli.ts'], - pattern: '*.ts', - source: '.', - sourcemap: true, - sourcesContent: false, - target: ['node16.20.0'], - tsconfig: 'tsconfig.build.json' - } - const defaults: Config = { - bundle: false, - clean: true, - cwd: '.', - entries: [], - ext: '.mjs', - format: 'esm', - fs, - ignore: new Set([ - '**/.DS_*', - '**/__mocks__', - '**/__snapshots__', - '**/__tests__' - ]), - outdir: 'dist', - pattern: '**', - source: 'src', - target: ['es2022'] - } - - // Act + Expect - expect(testSubject(object, defaults)).toMatchSnapshot() - }) -}) diff --git a/src/internal/__tests__/defu.spec.ts b/src/internal/__tests__/defu.spec.ts deleted file mode 100644 index ab1e1aed..00000000 --- a/src/internal/__tests__/defu.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file Unit Tests - defu - * @module mkbuild/internal/tests/unit/defu - */ - -import type { Config } from '#src/interfaces' -import fs from '#src/utils/fs' -import testSubject from '../defu' - -describe('unit:internal/defu', () => { - it('should return object with defaults assigned', () => { - // Arrange - const object: Config = { - entries: [ - { ignore: ['cli.ts'] }, - { bundle: true, minify: true, platform: 'node', source: 'src/cli.ts' } - ], - source: '.', - sourcemap: true, - sourcesContent: false, - target: 'node16.20.0', - tsconfig: 'tsconfig.build.json' - } - const defaults: Config = { - cwd: '.', - entries: [], - fs, - outdir: 'dist', - sourcemap: false, - watch: false, - write: false - } - - // Act + Expect - expect(testSubject(object, defaults)).toMatchSnapshot() - }) -}) diff --git a/src/internal/create-context.ts b/src/internal/create-context.ts index cf8b8621..ae8081db 100644 --- a/src/internal/create-context.ts +++ b/src/internal/create-context.ts @@ -13,11 +13,20 @@ import { EXT_DTS_REGEX } from '@flex-development/ext-regex' import * as mlly from '@flex-development/mlly' import * as pathe from '@flex-development/pathe' import type { PackageJson } from '@flex-development/pkg-types' -import { isString, isUndefined } from '@flex-development/tutils' +import { + DOT, + cast, + get, + isString, + isUndefined, + keys, + omit, + regexp, + shake, + sift +} from '@flex-development/tutils' import * as esbuild from 'esbuild' -import regexp from 'escape-string-regexp' import fg from 'fast-glob' -import { get, omit, shake, sift } from 'radash' import gitignore from './gitignore' /** @@ -50,7 +59,7 @@ async function createContext( clean = true, color = true, conditions = ['import', 'default'], - cwd = '.', + cwd = DOT, drop, dts = await (async () => { try { @@ -62,7 +71,7 @@ async function createContext( return false } })(), - external = bundle ? Object.keys(get(pkg, 'peerDependencies', {})!) : [], + external = bundle ? keys(get(pkg, 'peerDependencies', {})) : [], footer = {}, format = 'esm', ignore = IGNORE_PATTERNS, @@ -74,7 +83,7 @@ async function createContext( name = '[name]', outExtension = {}, outdir = 'dist', - pattern = '**', + pattern = mlly.PATTERN_CHARACTER.repeat(2), pure, platform = 'neutral', plugins = [], @@ -157,13 +166,13 @@ async function createContext( color, conditions: [...new Set(conditions)], drop: [...new Set(drop)], - entryNames: `[dir]/${name}`, + entryNames: `[dir]${pathe.sep}${name}`, entryPoints: files .map((file: string): SourceFile => { return { - ext: pathe.extname(file) as pathe.Ext, + ext: cast(pathe.extname(file)), file: - bundle && outbase !== '.' + bundle && outbase !== DOT ? // outbase support for bundles (esbuild only uses outbase for // multiple entries); https://esbuild.github.io/api/#outbase file @@ -177,12 +186,7 @@ async function createContext( return pathe.join(bundle ? outbase : source, sourcefile.file) }), external: bundle - ? [ - ...new Set([ - ...Object.keys(get(pkg, 'peerDependencies', {})!), - ...external - ]) - ] + ? [...new Set([...keys(get(pkg, 'peerDependencies', {})), ...external])] : [], footer, format, @@ -191,7 +195,7 @@ async function createContext( logLimit, logOverride, mainFields: [...new Set(mainFields)], - metafile: true, + metafile: true as const, outExtension: { ...outExtension, '.js': pathe.formatExt(ext) }, outbase, outdir, @@ -207,18 +211,16 @@ async function createContext( ...plugins, mkp.filter(dts === 'only' ? EXT_DTS_REGEX : undefined), write && mkp.write(fs) - ]) as esbuild.Plugin[], + ]), pure: [...new Set(pure)], - resolveExtensions: [...new Set(resolveExtensions)].map(ext => { - return pathe.formatExt(ext) - }), + resolveExtensions: [...new Set(resolveExtensions)].map(pathe.formatExt), target: isUndefined(target) ? undefined : isString(target) ? target : [...new Set(target)], tsconfig, - write: false + write: false as const }) ) } diff --git a/src/internal/defu-concat.ts b/src/internal/defu-concat.ts deleted file mode 100644 index 21e2e557..00000000 --- a/src/internal/defu-concat.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @file Internal - defuConcat - * @module mkbuild/internal/defuConcat - */ - -import { - isUndefined, - type ExactOptionalPropertyTypes, - type ObjectPlain -} from '@flex-development/tutils' -import { mergeAndCompare as merge, type Merge } from 'merge-anything' - -/** - * Assigns default properties to the given `object`. - * - * Leftmost arguments have more priority when assigning defaults. - * - * Supports {@linkcode Set} and array concatenation. - * - * @internal - * - * @template T - Destination object type - * @template D - Source object type(s) - * - * @param {T} object - Destination object - * @param {D} defaults - Object(s) used to assign default properties - * @return {ExactOptionalPropertyTypes & T>} `object` with defaults - */ -function defuConcat( - object: T, - ...defaults: D -): ExactOptionalPropertyTypes & T> { - return merge( - /** - * Determines how to assign a property. - * - * @param {unknown} v1 - Original value from {@linkcode object} - * @param {unknown} v2 - Incoming value from object in {@linkcode defaults} - * @return {unknown} Property value for {@linkcode object} - */ - (v1: unknown, v2: unknown): unknown => { - return (Array.isArray(v1) || v1 instanceof Set) && - (Array.isArray(v2) || v2 instanceof Set) - ? [...new Set([...v1, ...v2])] - : isUndefined(v1) - ? v2 - : v1 - }, - object, - ...defaults - ) -} - -export default defuConcat diff --git a/src/internal/defu.ts b/src/internal/defu.ts deleted file mode 100644 index e561de17..00000000 --- a/src/internal/defu.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file Internal - defu - * @module mkbuild/internal/defu - */ - -import { - isUndefined, - type ExactOptionalPropertyTypes, - type ObjectPlain -} from '@flex-development/tutils' -import { mergeAndCompare as merge, type Merge } from 'merge-anything' - -/** - * Assigns default properties to the given `object`. - * - * Leftmost arguments have more priority when assigning defaults. - * - * @internal - * - * @template T - Destination object type - * @template D - Source object type(s) - * - * @param {T} object - Destination object - * @param {D} defaults - Object(s) used to assign default properties - * @return {ExactOptionalPropertyTypes & T>} `object` with defaults - */ -function defu( - object: T, - ...defaults: D -): ExactOptionalPropertyTypes & T> { - return merge( - /** - * Determines how to assign a property. - * - * @param {unknown} v1 - Original value from {@linkcode object} - * @param {unknown} v2 - Incoming value from object in {@linkcode defaults} - * @return {unknown} Property value for {@linkcode object} - */ - (v1: unknown, v2: unknown): unknown => (isUndefined(v1) ? v2 : v1), - object, - ...defaults - ) -} - -export default defu diff --git a/src/internal/gitignore.ts b/src/internal/gitignore.ts index df747505..8346ca2b 100644 --- a/src/internal/gitignore.ts +++ b/src/internal/gitignore.ts @@ -5,6 +5,7 @@ import * as mlly from '@flex-development/mlly' import * as pathe from '@flex-development/pathe' +import { cast, isEmptyString, trim } from '@flex-development/tutils' /** * Returns a set of ignore patterns found in a `.gitignore` file. @@ -37,13 +38,13 @@ const gitignore = async (absWorkingDir: string): Promise> => { * * @const {string} gitignore */ - const content: string = (await mlly.getSource(path)) as string + const content: string = cast(await mlly.getSource(path)) // add ignore patterns from .gitignore for (const line of content.split(/\r?\n/)) { - if (!line.trim()) continue + if (isEmptyString(trim(line))) continue if (line.startsWith('#') || line.startsWith('!')) continue - ignore.add(line.trim()) + ignore.add(trim(line)) } } catch { // do nothing if .gitignore file was not found diff --git a/src/internal/index.ts b/src/internal/index.ts index 7ac609d8..02fd10de 100644 --- a/src/internal/index.ts +++ b/src/internal/index.ts @@ -4,6 +4,4 @@ */ export { default as createContext } from './create-context' -export { default as defu } from './defu' -export { default as defuConcat } from './defu-concat' export { default as gitignore } from './gitignore' diff --git a/src/make.ts b/src/make.ts index 240a1bae..04965cef 100644 --- a/src/make.ts +++ b/src/make.ts @@ -7,17 +7,23 @@ import { ERR_MODULE_NOT_FOUND, type NodeError } from '@flex-development/errnode' import * as mlly from '@flex-development/mlly' import pathe from '@flex-development/pathe' import type { PackageJson } from '@flex-development/pkg-types' -import type { Nullable } from '@flex-development/tutils' +import { + DOT, + cast, + defaults, + get, + regexp, + type Nullable +} from '@flex-development/tutils' import * as color from 'colorette' import consola from 'consola' import type * as esbuild from 'esbuild' -import regexp from 'escape-string-regexp' import { asyncExitHook as exitHook } from 'exit-hook' import { fileURLToPath } from 'node:url' import pb from 'pretty-bytes' import loadBuildConfig from './config/load' import type { Config, Context, Result, Task } from './interfaces' -import { createContext, defu, defuConcat } from './internal' +import * as internal from './internal' import type { OutputMetadata } from './types' import { analyzeOutputs, fs as fsa } from './utils' @@ -46,19 +52,19 @@ import { analyzeOutputs, fs as fsa } from './utils' */ async function make({ configfile = true, - cwd = '.', + cwd = DOT, ...config }: Config = {}): Promise { - const { entries, fs, serve, watch, write, ...options } = defu( + const { entries, fs, serve, watch, write, ...options } = defaults( config, configfile ? await loadBuildConfig(cwd) : {}, { cwd, - entries: [{}] as Partial[], + entries: cast[]>([{}]), fs: fsa, - logLevel: 'info', + logLevel: cast('info'), outdir: 'dist', - serve: false as esbuild.ServeOptions | boolean, + serve: cast(false), watch: false, write: false } @@ -115,7 +121,7 @@ async function make({ ...rest } = entry - return defuConcat( + return defaults( { bundle, cwd, @@ -147,7 +153,7 @@ async function make({ if (!watch && serve === false) { for (const task of tasks) { // create build context - context = await createContext(task, pkg, fs) + context = await internal.createContext(task, pkg, fs) /** * esbuild build result. @@ -181,7 +187,7 @@ async function make({ * * @const {OutputMetadata} metadata */ - const metadata: OutputMetadata = result.metafile.outputs[outfile]! + const metadata: OutputMetadata = get(result.metafile.outputs, outfile) return { bytes: metadata.bytes, @@ -227,7 +233,7 @@ async function make({ // enable serve or watch mode if (serve !== false || watch) { - const [task] = tasks as [Task] + const [task] = cast<[Task]>(tasks) // force clean output directory task.clean = true @@ -236,14 +242,14 @@ async function make({ task.write = true // create build context - context = await createContext(task, pkg, fs) + context = await internal.createContext(task, pkg, fs) // watch files watch && (await context.watch()) // serve files if (serve !== false) { - await context.serve({ ...(serve as esbuild.ServeOptions) }) + await context.serve({ ...cast(serve) }) } // dispose build context on process exit diff --git a/src/plugins/clean/__tests__/options.spec-d.ts b/src/plugins/clean/__tests__/options.spec-d.ts index 480e5c40..b76e27b9 100644 --- a/src/plugins/clean/__tests__/options.spec-d.ts +++ b/src/plugins/clean/__tests__/options.spec-d.ts @@ -4,24 +4,25 @@ */ import type { FileSystemAdapter } from '#src/types' +import type { Optional } from '@flex-development/tutils' import type TestSubject from '../options' describe('unit-d:plugins/clean/CleanPluginOptions', () => { it('should match [mkdir?: FileSystemAdapter["mkdir"]]', () => { expectTypeOf() .toHaveProperty('mkdir') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [rm?: FileSystemAdapter["rm"]]', () => { expectTypeOf() .toHaveProperty('rm') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [unlink?: FileSystemAdapter["unlink"]]', () => { expectTypeOf() .toHaveProperty('unlink') - .toEqualTypeOf() + .toEqualTypeOf>() }) }) diff --git a/src/plugins/create-require/__tests__/plugin.spec.ts b/src/plugins/create-require/__tests__/plugin.spec.ts index 074edaf3..d8582e6b 100644 --- a/src/plugins/create-require/__tests__/plugin.spec.ts +++ b/src/plugins/create-require/__tests__/plugin.spec.ts @@ -4,6 +4,7 @@ */ import createPluginAPI from '#tests/utils/create-plugin-api' +import { cast } from '@flex-development/tutils' import * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -46,7 +47,7 @@ describe('unit:plugins/create-require', () => { it('should throw if esbuild is writing output files', async () => { // Arrange - let error: Error + let error!: Error // Act try { @@ -56,31 +57,27 @@ describe('unit:plugins/create-require', () => { }) ) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!).to.have.property('message').equal('write must be disabled') + expect(error).to.have.property('message', 'write must be disabled') }) it('should throw if metafile is disabled', async () => { // Arrange - let error: Error + let error!: Error // Act try { await subject.setup( - createPluginAPI({ - initialOptions: { bundle: true, format: 'esm' } - }) + createPluginAPI({ initialOptions: { bundle: true, format: 'esm' } }) ) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!).to.have.property('message').equal('metafile required') + expect(error).to.have.property('message', 'metafile required') }) }) diff --git a/src/plugins/create-require/plugin.ts b/src/plugins/create-require/plugin.ts index 986b92ba..c5c76316 100644 --- a/src/plugins/create-require/plugin.ts +++ b/src/plugins/create-require/plugin.ts @@ -3,6 +3,7 @@ * @module mkbuild/plugins/create-require/plugin */ +import { constant, define, join, regexp } from '@flex-development/tutils' import { transform, type BuildOptions, @@ -11,9 +12,15 @@ import { type Plugin, type PluginBuild } from 'esbuild' -import regexp from 'escape-string-regexp' import util from 'node:util' +/** + * Plugin-specific build options. + * + * @internal + */ +type SpecificOptions = { metafile: true; write: false } + /** * Returns a plugin that defines the `require` function in ESM bundles. * @@ -91,10 +98,13 @@ const plugin = (): Plugin => { * * @const {string} snippet */ - const snippet: string = [ - "import { createRequire as __createRequire } from 'node:module'", - 'const require = __createRequire(import.meta.url)' - ].join('\n') + const snippet: string = join( + [ + "import { createRequire as __createRequire } from 'node:module'", + 'const require = __createRequire(import.meta.url)' + ], + '\n' + ) // transform snippet to re-add banner and minify let { code } = await transform(snippet, { @@ -108,7 +118,7 @@ const plugin = (): Plugin => { // remove new line from end of code snippet if output should be minified if (minify || minifyWhitespace) code = code.replace(/\n$/, '') - return void onEnd((result: BuildResult): void => { + return void onEnd((result: BuildResult): void => { /** * Regex used to deduce if an output file includes the `__require` shim. * @@ -117,7 +127,7 @@ const plugin = (): Plugin => { const filter: RegExp = /Dynamic require of ".*" is not supported/m // insert require function definitions - result.outputFiles = result.outputFiles!.map((output: OutputFile) => { + result.outputFiles = result.outputFiles.map((output: OutputFile) => { // do nothing if output file does not contain shim if (!filter.test(output.text)) return output @@ -129,20 +139,16 @@ const plugin = (): Plugin => { * * @var {string} text */ - let text: string = output.text - - // remove hashbang and re-add hashbang to code snippet - text = text.replace(hashbang, '') - code = `${hashbang}${code}` + let text: string = output.text.replace(hashbang, '') // remove banner if (banner) text = text.replace(new RegExp(regexp(banner) + '\n?'), '') - // insert require function definition - text = `${code}${text}` + // redefine output text to insert require function definition + define(output, 'text', { get: constant(hashbang + code + text) }) // reset output file contents - output.contents = new util.TextEncoder().encode(text) + output.contents = new util.TextEncoder().encode(output.text) /** * Relative path to output file. @@ -163,9 +169,9 @@ const plugin = (): Plugin => { const bytes: number = Buffer.byteLength(output.contents) // reset output file size - result.metafile!.outputs[outfile]!.bytes = bytes + result.metafile.outputs[outfile]!.bytes = bytes - return { ...output, text } + return output }) }) } diff --git a/src/plugins/decorators/plugin.ts b/src/plugins/decorators/plugin.ts index 654206ed..260cc101 100644 --- a/src/plugins/decorators/plugin.ts +++ b/src/plugins/decorators/plugin.ts @@ -8,7 +8,7 @@ import { EXT_DTS_REGEX, EXT_TS_REGEX } from '@flex-development/ext-regex' import * as mlly from '@flex-development/mlly' import pathe from '@flex-development/pathe' import * as tscu from '@flex-development/tsconfig-utils' -import type { Nullable } from '@flex-development/tutils' +import { DOT, cast, type Nullable } from '@flex-development/tutils' import type { BuildOptions, OnLoadArgs, @@ -53,7 +53,7 @@ const plugin = (options?: tscu.LoadTsconfigOptions): Plugin => { initialOptions, onLoad }: PluginBuild): Promise => { - const { absWorkingDir = '.', tsconfig = 'tsconfig.json' } = initialOptions + const { absWorkingDir = DOT, tsconfig = 'tsconfig.json' } = initialOptions /** * User compiler options. @@ -89,16 +89,16 @@ const plugin = (options?: tscu.LoadTsconfigOptions): Plugin => { */ const opts: OnLoadOptions = { filter: /.*/ } - // transpile typescript modules containing decorators + // transpile modules containing decorators onLoad(opts, async (args: OnLoadArgs): Promise> => { /** * Callback result. * - * @var {Nullable} result + * @var {?OnLoadResult} result */ let result: Nullable = null - // transpile typescript modules, but skip typescript declaration modules + // transpile modules, but skip typescript declaration modules if (EXT_TS_REGEX.test(args.path) && !EXT_DTS_REGEX.test(args.path)) { /** * URL of module to load. @@ -112,9 +112,9 @@ const plugin = (options?: tscu.LoadTsconfigOptions): Plugin => { * * @const {string} source */ - const source: string = (await mlly.getSource(url)) as string + const source: string = cast(await mlly.getSource(url)) - // do nothing if module does not use decorators + // do nothing if module does contain decorators if (!DECORATOR_REGEX.test(source)) return null // transpile module to emit decorator metadata diff --git a/src/plugins/dts/__tests__/plugin.spec.ts b/src/plugins/dts/__tests__/plugin.spec.ts index 58b291bf..e56b211b 100644 --- a/src/plugins/dts/__tests__/plugin.spec.ts +++ b/src/plugins/dts/__tests__/plugin.spec.ts @@ -4,6 +4,7 @@ */ import createPluginAPI from '#tests/utils/create-plugin-api' +import { cast } from '@flex-development/tutils' import type * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -30,33 +31,31 @@ describe('unit:plugins/dts', () => { it('should throw if esbuild is writing output files', async () => { // Arrange - let error: Error + let error!: Error // Act try { await subject.setup(createPluginAPI({ initialOptions: { write: true } })) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!).to.have.property('message').equal('write must be disabled') + expect(error).to.have.property('message', 'write must be disabled') }) it('should throw if metafile is disabled', async () => { // Arrange - let error: Error + let error!: Error // Act try { await subject.setup(createPluginAPI()) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!.message).to.equal('metafile required') + expect(error).to.have.property('message', 'metafile required') }) }) diff --git a/src/plugins/dts/plugin.ts b/src/plugins/dts/plugin.ts index 26809b0c..b17629da 100644 --- a/src/plugins/dts/plugin.ts +++ b/src/plugins/dts/plugin.ts @@ -3,8 +3,6 @@ * @module mkbuild/plugins/dts */ -import defu from '#src/internal/defu' -import type { OutputMetadata } from '#src/types' import { EXT_DTS_REGEX, EXT_JS_REGEX, @@ -12,6 +10,17 @@ import { } from '@flex-development/ext-regex' import pathe from '@flex-development/pathe' import * as tscu from '@flex-development/tsconfig-utils' +import { + DOT, + cast, + constant, + defaults, + define, + get, + includes, + keys, + shake +} from '@flex-development/tutils' import type { BuildOptions, BuildResult, @@ -27,6 +36,13 @@ import type { WriteFileCallback } from 'typescript' +/** + * Plugin-specific build options. + * + * @internal + */ +type SpecificOptions = { metafile: true; write: false } + /** * Returns a TypeScript declaration plugin. * @@ -64,7 +80,7 @@ const plugin = (): Plugin => { metafile, outExtension: { '.js': ext = '.js' } = {}, outbase = '', - outdir = '.', + outdir = DOT, preserveSymlinks = false, tsconfig = 'tsconfig.json', write @@ -110,7 +126,7 @@ const plugin = (): Plugin => { return void sourcefiles.push(pathe.resolve(args.resolveDir, args.path)) }) - return void onEnd((result: BuildResult): void => { + return void onEnd((result: BuildResult): void => { /** * TypeScript compiler options. * @@ -143,37 +159,40 @@ const plugin = (): Plugin => { delete compilerOptions.noEmitOnError // merge compiler options with defaults - compilerOptions = defu(compilerOptions, { - allowJs: true, - allowUmdGlobalAccess: format === 'iife', - allowUnreachableCode: false, - baseUrl: absWorkingDir, - checkJs: false, - declaration: true, - declarationMap: false, - emitDeclarationOnly: true, - esModuleInterop: true, - forceConsistentCasingInFileNames: true, - isolatedModules: true, - module: ts.ModuleKind.ESNext, - moduleResolution: tscu.normalizeModuleResolution( - tscu.ModuleResolutionKind.NodeJs - ), - noEmit: false, - noEmitOnError: false, - noErrorTruncation: true, - noImplicitAny: true, - noImplicitOverride: true, - noImplicitReturns: true, - outDir: pathe.resolve(absWorkingDir, outdir), - preserveConstEnums: true, - preserveSymlinks, - pretty: color, - resolveJsonModule: true, - rootDir: pathe.resolve(absWorkingDir, outbase), - skipLibCheck: true, - target: ts.ScriptTarget.ESNext - }) + compilerOptions = defaults( + compilerOptions, + shake({ + allowJs: true, + allowUmdGlobalAccess: format === 'iife', + allowUnreachableCode: false, + baseUrl: absWorkingDir, + checkJs: false, + declaration: true, + declarationMap: false, + emitDeclarationOnly: true, + esModuleInterop: true, + forceConsistentCasingInFileNames: true, + isolatedModules: true, + module: ts.ModuleKind.ESNext, + moduleResolution: tscu.normalizeModuleResolution( + tscu.ModuleResolutionKind.NodeJs + ), + noEmit: false, + noEmitOnError: false, + noErrorTruncation: true, + noImplicitAny: true, + noImplicitOverride: true, + noImplicitReturns: true, + outDir: pathe.resolve(absWorkingDir, outdir), + preserveConstEnums: true, + preserveSymlinks, + pretty: color, + resolveJsonModule: true, + rootDir: pathe.resolve(absWorkingDir, outbase), + skipLibCheck: true, + target: ts.ScriptTarget.ESNext + }) + ) /** * TypeScript compiler host. @@ -185,9 +204,9 @@ const plugin = (): Plugin => { /** * Virtual file system for declaration files. * - * @const {Map} vfs + * @const {Record} vfs */ - const vfs: Map = new Map() + const vfs: Record = {} // first letter before "js" or "ts" in output file extension const [, cm = ''] = /\.(c|m)?[jt]sx?$/.exec(ext)! @@ -203,10 +222,8 @@ const plugin = (): Plugin => { filename: string, contents: string ): void => { - return void vfs.set( - filename.replace(EXT_DTS_REGEX, `.d.${cm}ts`), - contents - ) + filename = filename.replace(EXT_DTS_REGEX, `.d.${cm}ts`) + return void (vfs[filename] = contents) } // override write file function @@ -220,56 +237,33 @@ const plugin = (): Plugin => { ).emit() // remap output files to insert declaration file outputs - return void (result.outputFiles = result.outputFiles!.flatMap(output => { + return void (result.outputFiles = result.outputFiles.flatMap(output => { /** * Absolute path to declaration file for {@linkcode output}. * * @const {string} dtspath */ - const dtspath: string = EXT_JS_REGEX.test(output.path) + const path: string = EXT_JS_REGEX.test(output.path) ? output.path.replace(EXT_JS_REGEX, '.d.$1ts') : EXT_TS_REGEX.test(output.path) && !EXT_DTS_REGEX.test(output.path) ? output.path.replace(EXT_TS_REGEX, '.d.$2ts') : '' // do nothing if missing declaration file - if (!vfs.has(dtspath)) return [output] - - /** - * Relative path to output file. - * - * **Note**: Relative to {@linkcode absWorkingDir}. - * - * @const {string} outfile - */ - const outfile: string = output.path - .replace(absWorkingDir, '') - .replace(/^\//, '') - - /** - * {@linkcode output} metadata. - * - * @const {OutputMetadata} metadata - */ - const metadata: OutputMetadata = result.metafile!.outputs[outfile]! - - /** - * Declaration output file content. - * - * @const {string} dtstext - */ - const dtstext: string = vfs.get(dtspath)! + if (!includes(keys(vfs), path)) return [output] /** * Declaration file output. * * @const {OutputFile} dts */ - const dts: OutputFile = { - contents: new util.TextEncoder().encode(dtstext), - path: dtspath, - text: dtstext - } + const dts: OutputFile = cast({ + contents: new util.TextEncoder().encode(vfs[path]), + path + }) + + // redefine output text + define(dts, 'text', { get: constant(vfs[path]) }) /** * Relative path to declaration output file. @@ -278,13 +272,16 @@ const plugin = (): Plugin => { * * @const {string} dtsoutfile */ - const dtsoutfile: string = dtspath + const outfile: string = path .replace(absWorkingDir, '') .replace(/^\//, '') // update metafile - result.metafile!.outputs[dtsoutfile] = { - ...metadata, + result.metafile.outputs[outfile] = { + ...get( + result.metafile.outputs, + output.path.replace(absWorkingDir, '').replace(/^\//, '') + ), bytes: Buffer.byteLength(dts.contents) } diff --git a/src/plugins/filter/__tests__/plugin.integration.spec.ts b/src/plugins/filter/__tests__/plugin.integration.spec.ts index 77236374..42580a87 100644 --- a/src/plugins/filter/__tests__/plugin.integration.spec.ts +++ b/src/plugins/filter/__tests__/plugin.integration.spec.ts @@ -5,6 +5,7 @@ import ESBUILD_OPTIONS from '#fixtures/options-esbuild' import pathe from '@flex-development/pathe' +import { keys } from '@flex-development/tutils' import * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -29,7 +30,7 @@ describe('integration:plugins/filter', () => { }) // Expect - expect(Object.keys(metafile.outputs)).to.each.match(filter) + expect(keys(metafile.outputs)).to.each.match(filter) expect(outputFiles).to.each.have.property('path').match(filter) }) }) diff --git a/src/plugins/filter/__tests__/plugin.spec.ts b/src/plugins/filter/__tests__/plugin.spec.ts index 9ae57919..fa59e426 100644 --- a/src/plugins/filter/__tests__/plugin.spec.ts +++ b/src/plugins/filter/__tests__/plugin.spec.ts @@ -4,6 +4,7 @@ */ import createPluginAPI from '#tests/utils/create-plugin-api' +import { cast } from '@flex-development/tutils' import type * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -24,12 +25,11 @@ describe('unit:plugins/filter', () => { createPluginAPI({ initialOptions: { metafile: true, write: true } }) ) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error).to.not.be.undefined - expect(error.message).to.equal('write must be disabled') + expect(error).to.have.property('message', 'write must be disabled') }) it('should throw if metafile is disabled', async () => { @@ -40,11 +40,10 @@ describe('unit:plugins/filter', () => { try { await subject.setup(createPluginAPI()) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error).to.not.be.undefined - expect(error.message).to.equal('metafile required') + expect(error).to.have.property('message', 'metafile required') }) }) diff --git a/src/plugins/filter/plugin.ts b/src/plugins/filter/plugin.ts index 2316ed88..45987826 100644 --- a/src/plugins/filter/plugin.ts +++ b/src/plugins/filter/plugin.ts @@ -4,18 +4,20 @@ */ import type { OutputMetadata } from '#src/types' -import type { - BuildOptions, - BuildResult, - OutputFile, - Plugin, - PluginBuild -} from 'esbuild' +import { entries, select } from '@flex-development/tutils' +import type { BuildOptions, BuildResult, Plugin, PluginBuild } from 'esbuild' /** - * Returns an {@linkcode OutputFile.path} filter plugin. + * Plugin-specific build options. * - * @param {RegExp} [filter=/.+/] - {@linkcode OutputFile.path} filter + * @internal + */ +type SpecificOptions = { metafile: true; write: false } + +/** + * Returns an output file filter plugin. + * + * @param {RegExp} [filter=/.+/] - Output file path filter * @return {Plugin} Output file path filter plugin */ const plugin = (filter: RegExp = /.+/): Plugin => { @@ -38,38 +40,30 @@ const plugin = (filter: RegExp = /.+/): Plugin => { // esbuild write must be disabled to filter result.outputFiles if (initialOptions.write) throw new Error('write must be disabled') - // filter output files - return void onEnd( - (result: BuildResult<{ metafile: true; write: false }>): void => { - /** - * Output file metadata. - * - * @const {Record} outputs - */ - const outputs: Record = {} + // filter output files and metadata + return void onEnd((result: BuildResult): void => { + /** + * Output file metadata. + * + * @const {Record} outputs + */ + const outputs: Record = {} - // filter output files - result.outputFiles = result.outputFiles.filter((output: OutputFile) => { - return filter.test(output.path) - }) + // filter output files + result.outputFiles = select(result.outputFiles, o => filter.test(o.path)) - // filter output file metadata - for (const output of Object.entries(result.metafile.outputs)) { - const [outfile, metadata] = output - if (result.outputFiles.some(o => o.path.endsWith(outfile))) { - outputs[outfile] = metadata - } + // filter output file metadata + for (const [outfile, metadata] of entries(result.metafile.outputs)) { + if (result.outputFiles.some(o => o.path.endsWith(outfile))) { + outputs[outfile] = metadata } + } - // reset metafile - result.metafile = { - inputs: result.metafile.inputs, - outputs - } + // reset metafile + result.metafile.outputs = outputs - return void result - } - ) + return void result + }) } return { name: 'filter', setup } diff --git a/src/plugins/fully-specified/__tests__/plugin.integration.spec.ts b/src/plugins/fully-specified/__tests__/plugin.integration.spec.ts index 4767d719..84e3b7b2 100644 --- a/src/plugins/fully-specified/__tests__/plugin.integration.spec.ts +++ b/src/plugins/fully-specified/__tests__/plugin.integration.spec.ts @@ -4,12 +4,12 @@ */ import ESBUILD_OPTIONS from '#fixtures/options-esbuild' -import type { Mock } from '#tests/interfaces' import { ERR_PACKAGE_IMPORT_NOT_DEFINED, type NodeError } from '@flex-development/errnode' import * as mlly from '@flex-development/mlly' +import { cast } from '@flex-development/tutils' import * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -50,21 +50,21 @@ describe('integration:plugins/fully-specified', () => { // Act try { - ;(mlly.fillModules as unknown as Mock).mockRejectedValueOnce(error) + vi.mocked(mlly.fillModules).mockRejectedValueOnce(error) await esbuild.build(options) } catch (e: unknown) { - errors = (e as esbuild.BuildFailure).errors + errors = cast(e).errors } // Expect expect(errors).to.be.an('array').of.length(1) - expect(errors[0]).to.have.property('id').equal(error.code) - expect(errors[0]).to.have.property('location').be.null + expect(errors[0]).to.have.property('id', error.code) + expect(errors[0]).to.have.property('location', null) expect(errors[0]).to.have.property('notes').be.an('array').of.length(1) - expect(errors[0]).to.have.property('pluginName').equal('fully-specified') - expect(errors[0]).to.have.property('text').equal(error.message) - expect(errors[0]!.notes[0]).to.have.property('location').be.null - expect(errors[0]!.notes[0]).to.have.property('text').equal(error.stack) + expect(errors[0]).to.have.property('pluginName', 'fully-specified') + expect(errors[0]).to.have.property('text', error.message) + expect(errors[0]!.notes[0]).to.have.property('location', null) + expect(errors[0]!.notes[0]).to.have.property('text', error.stack) }) }) }) diff --git a/src/plugins/fully-specified/__tests__/plugin.spec.ts b/src/plugins/fully-specified/__tests__/plugin.spec.ts index 36abedf6..2eed0e74 100644 --- a/src/plugins/fully-specified/__tests__/plugin.spec.ts +++ b/src/plugins/fully-specified/__tests__/plugin.spec.ts @@ -4,6 +4,7 @@ */ import createPluginAPI from '#tests/utils/create-plugin-api' +import { cast } from '@flex-development/tutils' import type * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -30,33 +31,31 @@ describe('unit:plugins/fully-specified', () => { it('should throw if esbuild is writing output files', async () => { // Arrange - let error: Error + let error!: Error // Act try { await subject.setup(createPluginAPI({ initialOptions: { write: true } })) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!).to.have.property('message').equal('write must be disabled') + expect(error).to.have.property('message', 'write must be disabled') }) it('should throw if metafile is disabled', async () => { // Arrange - let error: Error + let error!: Error // Act try { await subject.setup(createPluginAPI()) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!.message).to.equal('metafile required') + expect(error).to.have.property('message', 'metafile required') }) }) diff --git a/src/plugins/fully-specified/plugin.ts b/src/plugins/fully-specified/plugin.ts index 921136d7..e7ab03a1 100644 --- a/src/plugins/fully-specified/plugin.ts +++ b/src/plugins/fully-specified/plugin.ts @@ -7,6 +7,7 @@ import type { OutputMetadata } from '#src/types' import type { NodeError } from '@flex-development/errnode' import * as mlly from '@flex-development/mlly' import * as pathe from '@flex-development/pathe' +import { at, cast, constant, define, get, keys } from '@flex-development/tutils' import type { BuildOptions, BuildResult, @@ -17,6 +18,13 @@ import type { } from 'esbuild' import util from 'node:util' +/** + * Plugin-specific build options. + * + * @internal + */ +type SpecificOptions = { metafile: true; write: false } + /** * Plugin name. * @@ -86,88 +94,90 @@ const plugin = (): Plugin => { // metafile required to get output metadata if (!metafile) throw new Error('metafile required') - return void onEnd(async (result: BuildResult): Promise => { - /** - * Output file objects. - * - * @const {OutputFile[]} outputFiles - */ - const outputFiles: OutputFile[] = [] - - for (const output of result.outputFiles!) { - /** - * Relative path to output file. - * - * **Note**: Relative to {@linkcode absWorkingDir}. - * - * @const {string} outfile - */ - const outfile: string = output.path - .replace(absWorkingDir, '') - .replace(/^\//, '') - + return void onEnd( + async (result: BuildResult): Promise => { /** - * {@linkcode output} metadata. + * Output file objects. * - * @const {OutputMetadata} metadata + * @const {OutputFile[]} outputFiles */ - const metadata: OutputMetadata = result.metafile!.outputs[outfile]! + const outputFiles: OutputFile[] = [] - // because this plugin doesn't handle bundles, the entry point can be - // reset to the first (and only!) key in metadata.inputs - if (!metadata.entryPoint) { - const [entryPoint = ''] = Object.keys(metadata.inputs) - metadata.entryPoint = entryPoint - } - - // skip output files without entry points - if (!metadata.entryPoint) { - outputFiles.push(output) - continue - } + for (const output of result.outputFiles) { + /** + * {@linkcode output} metadata. + * + * @const {OutputMetadata} metadata + */ + const metadata: OutputMetadata = get( + result.metafile.outputs, + output.path.replace(absWorkingDir, '').replace(/^\//, '') + ) - try { /** - * {@linkcode output.text} with fully specified modules. + * Relative path to source file. * - * @const {string} text + * @const {string} entryPoint */ - const text: string = await mlly.fillModules(output.text, { - conditions: new Set(conditions), - ext, - extensions: new Set(resolveExtensions), - parent: mlly.toURL(pathe.join(absWorkingDir, metadata.entryPoint)), - preserveSymlinks - }) - - // add output file with fully specified modules - outputFiles.push({ - ...output, - contents: new util.TextEncoder().encode(text), - text - }) - } catch (e: unknown) { - const { code, message, stack = '' } = e as NodeError - - return { - errors: [ - { - id: code, - location: null, - notes: [{ location: null, text: stack }], - pluginName: PLUGIN_NAME, - text: message - } - ] + const entryPoint: string = get( + metadata, + 'entryPoint', + // because this plugin doesn't handle bundles, the entry point can + // fallback to the first (and only!) key in metadata.inputs + at(keys(metadata.inputs), 0, '') + ) + + // skip output files without entry points + if (!entryPoint) { + outputFiles.push(output) + continue + } + + // reset entry point + metadata.entryPoint = entryPoint + + try { + // redefine output text + define(output, 'text', { + get: constant( + await mlly.fillModules(output.text, { + conditions: new Set(conditions), + ext, + extensions: new Set(resolveExtensions), + parent: mlly.toURL(pathe.join(absWorkingDir, entryPoint)), + preserveSymlinks + }) + ) + }) + + // reset output contents + output.contents = new util.TextEncoder().encode(output.text) + + // add output file with fully specified modules + outputFiles.push(output) + } catch (e: unknown) { + const { code, message, stack = '' } = cast(e) + + return { + errors: [ + { + id: code, + location: null, + notes: [{ location: null, text: stack }], + pluginName: PLUGIN_NAME, + text: message + } + ] + } } } - } - // reset output files - result.outputFiles = outputFiles + // reset output files + result.outputFiles = outputFiles - return {} - }) + return {} + } + ) } return { name: PLUGIN_NAME, setup } diff --git a/src/plugins/tsconfig-paths/__tests__/plugin.integration.spec.ts b/src/plugins/tsconfig-paths/__tests__/plugin.integration.spec.ts index 920786c5..01a6a790 100644 --- a/src/plugins/tsconfig-paths/__tests__/plugin.integration.spec.ts +++ b/src/plugins/tsconfig-paths/__tests__/plugin.integration.spec.ts @@ -4,7 +4,6 @@ */ import ESBUILD_OPTIONS from '#fixtures/options-esbuild' -import type { Mock } from '#tests/interfaces' import { ERR_INVALID_MODULE_SPECIFIER, type NodeError @@ -12,6 +11,7 @@ import { import * as mlly from '@flex-development/mlly' import pathe from '@flex-development/pathe' import * as tscu from '@flex-development/tsconfig-utils' +import { cast } from '@flex-development/tutils' import * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -53,21 +53,21 @@ describe('integration:plugins/tsconfig-paths', () => { // Act try { - ;(tscu.resolvePaths as unknown as Mock).mockRejectedValueOnce(error) + vi.mocked(tscu.resolvePaths).mockRejectedValueOnce(error) await esbuild.build(options) } catch (e: unknown) { - errors = (e as esbuild.BuildFailure).errors + errors = cast(e).errors } // Expect expect(errors).to.be.an('array').of.length(1) - expect(errors[0]).to.have.property('id').equal(error.code) - expect(errors[0]).to.have.property('location').be.null + expect(errors[0]).to.have.property('id', error.code) + expect(errors[0]).to.have.property('location', null) expect(errors[0]).to.have.property('notes').be.an('array').of.length(1) expect(errors[0]).to.have.property('pluginName').equal('tsconfig-paths') - expect(errors[0]).to.have.property('text').equal(error.message) - expect(errors[0]!.notes[0]).to.have.property('location').be.null - expect(errors[0]!.notes[0]).to.have.property('text').equal(error.stack) + expect(errors[0]).to.have.property('text', error.message) + expect(errors[0]!.notes[0]).to.have.property('location', null) + expect(errors[0]!.notes[0]).to.have.property('text', error.stack) }) }) }) diff --git a/src/plugins/tsconfig-paths/__tests__/plugin.spec.ts b/src/plugins/tsconfig-paths/__tests__/plugin.spec.ts index a7e9ec69..96cda20b 100644 --- a/src/plugins/tsconfig-paths/__tests__/plugin.spec.ts +++ b/src/plugins/tsconfig-paths/__tests__/plugin.spec.ts @@ -4,6 +4,7 @@ */ import createPluginAPI from '#tests/utils/create-plugin-api' +import { cast } from '@flex-development/tutils' import type * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -30,33 +31,31 @@ describe('unit:plugins/tsconfig-paths', () => { it('should throw if esbuild is writing output files', async () => { // Arrange - let error: Error + let error!: Error // Act try { await subject.setup(createPluginAPI({ initialOptions: { write: true } })) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!).to.have.property('message').equal('write must be disabled') + expect(error).to.have.property('message', 'write must be disabled') }) it('should throw if metafile is disabled', async () => { // Arrange - let error: Error + let error!: Error // Act try { await subject.setup(createPluginAPI()) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!.message).to.equal('metafile required') + expect(error).to.have.property('message', 'metafile required') }) }) diff --git a/src/plugins/tsconfig-paths/plugin.ts b/src/plugins/tsconfig-paths/plugin.ts index 3e213d58..8a18826d 100644 --- a/src/plugins/tsconfig-paths/plugin.ts +++ b/src/plugins/tsconfig-paths/plugin.ts @@ -7,6 +7,7 @@ import type { OutputMetadata } from '#src/types' import type { NodeError } from '@flex-development/errnode' import pathe from '@flex-development/pathe' import * as tscu from '@flex-development/tsconfig-utils' +import { at, cast, constant, define, get, keys } from '@flex-development/tutils' import type { BuildOptions, BuildResult, @@ -17,6 +18,13 @@ import type { } from 'esbuild' import util from 'node:util' +/** + * Plugin-specific build options. + * + * @internal + */ +type SpecificOptions = { metafile: true; write: false } + /** * Plugin name. * @@ -66,93 +74,95 @@ const plugin = ({ file, read }: tscu.LoadTsconfigOptions = {}): Plugin => { // metafile required to get output metadata if (!metafile) throw new Error('metafile required') - return void onEnd(async (result: BuildResult): Promise => { - /** - * Output file objects. - * - * @const {OutputFile[]} outputFiles - */ - const outputFiles: OutputFile[] = [] - - // resolve path aliases in output content - for (const output of result.outputFiles!) { - /** - * Relative path to output file. - * - * **Note**: Relative to {@linkcode absWorkingDir}. - * - * @const {string} outfile - */ - const outfile: string = output.path - .replace(absWorkingDir, '') - .replace(/^\//, '') - + return void onEnd( + async (result: BuildResult): Promise => { /** - * {@linkcode output} metadata. + * Output file objects. * - * @const {OutputMetadata} metadata + * @const {OutputFile[]} outputFiles */ - const metadata: OutputMetadata = result.metafile!.outputs[outfile]! + const outputFiles: OutputFile[] = [] - // because this plugin doesn't handle bundles, the entry point can be - // reset to the first (and only!) key in metadata.inputs - if (!metadata.entryPoint) { - const [entryPoint = ''] = Object.keys(metadata.inputs) - metadata.entryPoint = entryPoint - } - - // skip output files without entry points - if (!metadata.entryPoint) { - outputFiles.push(output) - continue - } + // resolve path aliases in output content + for (const output of result.outputFiles) { + /** + * {@linkcode output} metadata. + * + * @const {OutputMetadata} metadata + */ + const metadata: OutputMetadata = get( + result.metafile.outputs, + output.path.replace(absWorkingDir, '').replace(/^\//, '') + ) - try { /** - * {@linkcode output.text} with path aliases replaced. + * Relative path to source file. * - * @const {string} text + * @const {string} entryPoint */ - const text: string = await tscu.resolvePaths(output.text, { - baseUrl: absWorkingDir, - conditions: new Set(conditions), - ext: '', - extensions: new Set(resolveExtensions), - file, - parent: pathe.resolve(absWorkingDir, metadata.entryPoint), - preserveSymlinks, - read, - tsconfig: pathe.resolve(absWorkingDir, tsconfig) - }) - - // add output file with path aliases replaced - outputFiles.push({ - ...output, - contents: new util.TextEncoder().encode(text), - text - }) - } catch (e: unknown) { - const { code, message, stack = '' } = e as NodeError - - return { - errors: [ - { - id: code, - location: null, - notes: [{ location: null, text: stack }], - pluginName: PLUGIN_NAME, - text: message - } - ] + const entryPoint: string = get( + metadata, + 'entryPoint', + // because this plugin doesn't handle bundles, the entry point can + // fallback to the first (and only!) key in metadata.inputs + at(keys(metadata.inputs), 0, '') + ) + + // skip output files without entry points + if (!entryPoint) { + outputFiles.push(output) + continue + } + + // reset entry point + metadata.entryPoint = entryPoint + + try { + // replace path aliases + define(output, 'text', { + get: constant( + await tscu.resolvePaths(output.text, { + baseUrl: absWorkingDir, + conditions: new Set(conditions), + ext: '', + extensions: new Set(resolveExtensions), + file, + parent: pathe.resolve(absWorkingDir, entryPoint), + preserveSymlinks, + read, + tsconfig: pathe.resolve(absWorkingDir, tsconfig) + }) + ) + }) + + // reset output contents + output.contents = new util.TextEncoder().encode(output.text) + + // add output file with path aliases replaced + outputFiles.push(output) + } catch (e: unknown) { + const { code, message, stack = '' } = cast(e) + + return { + errors: [ + { + id: code, + location: null, + notes: [{ location: null, text: stack }], + pluginName: PLUGIN_NAME, + text: message + } + ] + } } } - } - // reset output files - result.outputFiles = outputFiles + // reset output files + result.outputFiles = outputFiles - return {} - }) + return {} + } + ) } return { name: PLUGIN_NAME, setup } diff --git a/src/plugins/write/__tests__/options.spec-d.ts b/src/plugins/write/__tests__/options.spec-d.ts index d8bb5081..444c5a6d 100644 --- a/src/plugins/write/__tests__/options.spec-d.ts +++ b/src/plugins/write/__tests__/options.spec-d.ts @@ -4,24 +4,25 @@ */ import type { FileSystemAdapter } from '#src/types' +import type { Optional } from '@flex-development/tutils' import type TestSubject from '../options' describe('unit-d:plugins/write/WritePluginOptions', () => { - it('should match [filter?: RegExp]', () => { + it('should match [filter?: Optional]', () => { expectTypeOf() .toHaveProperty('filter') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [mkdir?: FileSystemAdapter["mkdir"]]', () => { expectTypeOf() .toHaveProperty('mkdir') - .toEqualTypeOf() + .toEqualTypeOf>() }) it('should match [writeFile?: FileSystemAdapter["writeFile"]]', () => { expectTypeOf() .toHaveProperty('writeFile') - .toEqualTypeOf() + .toEqualTypeOf>() }) }) diff --git a/src/plugins/write/__tests__/plugin.spec.ts b/src/plugins/write/__tests__/plugin.spec.ts index d19efbf4..505c3201 100644 --- a/src/plugins/write/__tests__/plugin.spec.ts +++ b/src/plugins/write/__tests__/plugin.spec.ts @@ -4,6 +4,7 @@ */ import createPluginAPI from '#tests/utils/create-plugin-api' +import { cast } from '@flex-development/tutils' import type * as esbuild from 'esbuild' import testSubject from '../plugin' @@ -16,17 +17,16 @@ describe('unit:plugins/write', () => { it('should throw if esbuild is writing output files', async () => { // Arrange - let error: Error + let error!: Error // Act try { await subject.setup(createPluginAPI({ initialOptions: { write: true } })) } catch (e: unknown) { - error = e as typeof error + error = cast(e) } // Expect - expect(error!).to.not.be.undefined - expect(error!.message).to.equal('write must be disabled') + expect(error).to.have.property('message', 'write must be disabled') }) }) diff --git a/src/plugins/write/options.ts b/src/plugins/write/options.ts index 69530304..a8bb7352 100644 --- a/src/plugins/write/options.ts +++ b/src/plugins/write/options.ts @@ -4,6 +4,7 @@ */ import type { FileSystemAdapter } from '#src/types' +import type { Optional } from '@flex-development/tutils' /** * Output file writing options. @@ -14,7 +15,7 @@ interface WritePluginOptions { * * @default /.+/ */ - filter?: RegExp | undefined + filter?: Optional /** * Creates a directory. diff --git a/src/types/__tests__/generated-file-type.spec-d.ts b/src/types/__tests__/generated-file-type.spec-d.ts index 809e84c8..e80f81c2 100644 --- a/src/types/__tests__/generated-file-type.spec-d.ts +++ b/src/types/__tests__/generated-file-type.spec-d.ts @@ -7,10 +7,10 @@ import type TestSubject from '../generated-file-type' describe('unit-d:types/GeneratedFileType', () => { it('should extract "css"', () => { - expectTypeOf().extract<'css'>().toBeString() + expectTypeOf().extract<'css'>().not.toBeNever() }) it('should extract "js"', () => { - expectTypeOf().extract<'js'>().toBeString() + expectTypeOf().extract<'js'>().not.toBeNever() }) }) diff --git a/src/types/__tests__/jsx.spec-d.ts b/src/types/__tests__/jsx.spec-d.ts index f77e9a80..6a3cd6ba 100644 --- a/src/types/__tests__/jsx.spec-d.ts +++ b/src/types/__tests__/jsx.spec-d.ts @@ -7,14 +7,14 @@ import type TestSubject from '../jsx' describe('unit-d:types/Jsx', () => { it('should extract "automatic"', () => { - expectTypeOf().extract<'automatic'>().toBeString() + expectTypeOf().extract<'automatic'>().not.toBeNever() }) it('should extract "preserve"', () => { - expectTypeOf().extract<'preserve'>().toBeString() + expectTypeOf().extract<'preserve'>().not.toBeNever() }) it('should extract "transform"', () => { - expectTypeOf().extract<'transform'>().toBeString() + expectTypeOf().extract<'transform'>().not.toBeNever() }) }) diff --git a/src/types/__tests__/legal-comments.spec-d.ts b/src/types/__tests__/legal-comments.spec-d.ts index 23a2ff44..50f735b9 100644 --- a/src/types/__tests__/legal-comments.spec-d.ts +++ b/src/types/__tests__/legal-comments.spec-d.ts @@ -7,22 +7,22 @@ import type TestSubject from '../legal-comments' describe('unit-d:types/LegalComments', () => { it('should extract "eof"', () => { - expectTypeOf().extract<'eof'>().toBeString() + expectTypeOf().extract<'eof'>().not.toBeNever() }) it('should extract "external"', () => { - expectTypeOf().extract<'external'>().toBeString() + expectTypeOf().extract<'external'>().not.toBeNever() }) it('should extract "inline"', () => { - expectTypeOf().extract<'inline'>().toBeString() + expectTypeOf().extract<'inline'>().not.toBeNever() }) it('should extract "linked"', () => { - expectTypeOf().extract<'linked'>().toBeString() + expectTypeOf().extract<'linked'>().not.toBeNever() }) it('should extract "none"', () => { - expectTypeOf().extract<'none'>().toBeString() + expectTypeOf().extract<'none'>().not.toBeNever() }) }) diff --git a/src/types/__tests__/output-extension.spec-d.ts b/src/types/__tests__/output-extension.spec-d.ts index d43fef40..7c0db56c 100644 --- a/src/types/__tests__/output-extension.spec-d.ts +++ b/src/types/__tests__/output-extension.spec-d.ts @@ -7,52 +7,52 @@ import type TestSubject from '../output-extension' describe('unit-d:types/OutputExtension', () => { it('should extract ".cjs"', () => { - expectTypeOf().extract<'.cjs'>().toBeString() + expectTypeOf().extract<'.cjs'>().not.toBeNever() }) it('should extract ".js"', () => { - expectTypeOf().extract<'.js'>().toBeString() + expectTypeOf().extract<'.js'>().not.toBeNever() }) it('should extract ".mjs"', () => { - expectTypeOf().extract<'.mjs'>().toBeString() + expectTypeOf().extract<'.mjs'>().not.toBeNever() }) it('should extract "cjs"', () => { - expectTypeOf().extract<'cjs'>().toBeString() + expectTypeOf().extract<'cjs'>().not.toBeNever() }) it('should extract "js"', () => { - expectTypeOf().extract<'js'>().toBeString() + expectTypeOf().extract<'js'>().not.toBeNever() }) it('should extract "mjs"', () => { - expectTypeOf().extract<'mjs'>().toBeString() + expectTypeOf().extract<'mjs'>().not.toBeNever() }) describe('min', () => { it('should extract ".min.cjs"', () => { - expectTypeOf().extract<'.min.cjs'>().toBeString() + expectTypeOf().extract<'.min.cjs'>().not.toBeNever() }) it('should extract ".min.js"', () => { - expectTypeOf().extract<'.min.js'>().toBeString() + expectTypeOf().extract<'.min.js'>().not.toBeNever() }) it('should extract ".min.mjs"', () => { - expectTypeOf().extract<'.min.mjs'>().toBeString() + expectTypeOf().extract<'.min.mjs'>().not.toBeNever() }) it('should extract "min.cjs"', () => { - expectTypeOf().extract<'min.cjs'>().toBeString() + expectTypeOf().extract<'min.cjs'>().not.toBeNever() }) it('should extract "min.js"', () => { - expectTypeOf().extract<'min.js'>().toBeString() + expectTypeOf().extract<'min.js'>().not.toBeNever() }) it('should extract "min.mjs"', () => { - expectTypeOf().extract<'min.mjs'>().toBeString() + expectTypeOf().extract<'min.mjs'>().not.toBeNever() }) }) }) diff --git a/src/types/__tests__/sourcemap.spec-d.ts b/src/types/__tests__/sourcemap.spec-d.ts index 14c81a94..f1fa7a81 100644 --- a/src/types/__tests__/sourcemap.spec-d.ts +++ b/src/types/__tests__/sourcemap.spec-d.ts @@ -7,18 +7,18 @@ import type TestSubject from '../sourcemap' describe('unit-d:types/Sourcemap', () => { it('should extract "both"', () => { - expectTypeOf().extract<'both'>().toBeString() + expectTypeOf().extract<'both'>().not.toBeNever() }) it('should extract "external"', () => { - expectTypeOf().extract<'external'>().toBeString() + expectTypeOf().extract<'external'>().not.toBeNever() }) it('should extract "inline"', () => { - expectTypeOf().extract<'inline'>().toBeString() + expectTypeOf().extract<'inline'>().not.toBeNever() }) it('should extract "linked"', () => { - expectTypeOf().extract<'linked'>().toBeString() + expectTypeOf().extract<'linked'>().not.toBeNever() }) }) diff --git a/src/types/jsx.ts b/src/types/jsx.ts index f62c107a..d702efdf 100644 --- a/src/types/jsx.ts +++ b/src/types/jsx.ts @@ -3,6 +3,7 @@ * @module mkbuild/types/Jsx */ +import type { Fallback } from '@flex-development/tutils' import type * as esbuild from 'esbuild' /** @@ -10,6 +11,6 @@ import type * as esbuild from 'esbuild' * * @see https://esbuild.github.io/api/#jsx */ -type Jsx = NonNullable +type Jsx = Fallback export type { Jsx as default } diff --git a/src/types/legal-comments.ts b/src/types/legal-comments.ts index aed62420..bbc840fa 100644 --- a/src/types/legal-comments.ts +++ b/src/types/legal-comments.ts @@ -3,6 +3,7 @@ * @module mkbuild/types/LegalComments */ +import type { Fallback } from '@flex-development/tutils' import type * as esbuild from 'esbuild' /** @@ -10,6 +11,6 @@ import type * as esbuild from 'esbuild' * * @see https://esbuild.github.io/api/#legal-comments */ -type LegalComments = NonNullable +type LegalComments = Fallback export type { LegalComments as default } diff --git a/src/types/options-esbuild.ts b/src/types/options-esbuild.ts index e6302cef..d523ef3c 100644 --- a/src/types/options-esbuild.ts +++ b/src/types/options-esbuild.ts @@ -3,6 +3,7 @@ * @module mkbuild/types/EsbuildOptions */ +import type { Omit } from '@flex-development/tutils' import type * as esbuild from 'esbuild' /** diff --git a/src/types/output-extension.ts b/src/types/output-extension.ts index 0cc4e5ef..cc73e24e 100644 --- a/src/types/output-extension.ts +++ b/src/types/output-extension.ts @@ -3,12 +3,12 @@ * @module mkbuild/types/OutputExtension */ -import type { EmptyString } from '@flex-development/tutils' +import type { Dot, EmptyString } from '@flex-development/tutils' /** * Output file extensions. */ -type OutputExtension = `${EmptyString | '.'}${EmptyString | 'min.'}${ +type OutputExtension = `${Dot | EmptyString}${EmptyString | `min${Dot}`}${ | 'cjs' | 'js' | 'mjs'}` diff --git a/src/types/sourcemap.ts b/src/types/sourcemap.ts index ea405825..c561eb4e 100644 --- a/src/types/sourcemap.ts +++ b/src/types/sourcemap.ts @@ -3,6 +3,7 @@ * @module mkbuild/types/Sourcemap */ +import type { Fallback, Optional } from '@flex-development/tutils' import type * as esbuild from 'esbuild' /** @@ -10,9 +11,10 @@ import type * as esbuild from 'esbuild' * * @see https://esbuild.github.io/api/#sourcemap */ -type Sourcemap = Exclude< - NonNullable, - boolean +type Sourcemap = Fallback< + esbuild.BuildOptions['sourcemap'], + never, + Optional > export type { Sourcemap as default } diff --git a/src/utils/__mocks__/fs.ts b/src/utils/__mocks__/fs.ts index e1b89a53..b3cd66c7 100644 --- a/src/utils/__mocks__/fs.ts +++ b/src/utils/__mocks__/fs.ts @@ -4,11 +4,7 @@ */ import volume from '#fixtures/volume' -import type { - IMkdirOptions, - IRmOptions, - IWriteFileOptions -} from 'memfs/lib/volume' +import { cast, type Optional } from '@flex-development/tutils' import fsc, { type MakeDirectoryOptions, type Mode, @@ -27,15 +23,15 @@ import fsp from 'node:fs/promises' * * @param {PathLike} directory - Directory to create * @param {MakeDirectoryOptions | Mode} [options] - Directory creation options - * @return {Promise} First directory path created if + * @return {Promise>} First directory path created if * `options.recursive` is `true` or `undefined` if `false` */ const mkdir = vi.fn( async ( directory: PathLike, options: MakeDirectoryOptions | Mode - ): Promise => { - return volume.mkdirSync(directory, options as IMkdirOptions) + ): Promise> => { + return volume.mkdirSync(directory, cast(options)) } ) @@ -47,11 +43,11 @@ const mkdir = vi.fn( * @async * * @param {PathLike} id - Module id to evaluate - * @param {RmOptions} [options] - Removal options + * @param {RmOptions?} [options] - Removal options * @return {Promise} Nothing when complete */ const rm = vi.fn(async (id: PathLike, options?: RmOptions): Promise => { - return void volume.rmSync(id, options as IRmOptions) + return void volume.rmSync(id, cast(options)) }) /** @@ -63,7 +59,7 @@ const rm = vi.fn(async (id: PathLike, options?: RmOptions): Promise => { * * @param {PathLike} file - Filename or handle * @param {Uint8Array | string} data - File content - * @param {WriteFileOptions} [options] - Write file options + * @param {WriteFileOptions?} [options] - Write file options * @return {Promise} Nothing when complete */ const writeFile = vi.fn( @@ -72,7 +68,7 @@ const writeFile = vi.fn( data: Uint8Array | string, options?: WriteFileOptions ): Promise => { - return void volume.writeFileSync(file, data, options as IWriteFileOptions) + return void volume.writeFileSync(file, data, cast(options)) } ) diff --git a/src/utils/__tests__/define-build-config.spec-d.ts b/src/utils/__tests__/define-build-config.spec-d.ts index 48145e65..bf0d3bd2 100644 --- a/src/utils/__tests__/define-build-config.spec-d.ts +++ b/src/utils/__tests__/define-build-config.spec-d.ts @@ -4,15 +4,16 @@ */ import type { Config } from '#src/interfaces' +import type { Optional } from '@flex-development/tutils' import type testSubject from '../define-build-config' describe('unit-d:utils/defineBuildConfig', () => { - it('should be callable with [Config | undefined]', () => { + it('should be callable with [Optional]', () => { // Arrange - type Expected = [config?: Config | undefined] + type Expect = [config?: Optional] // Expect - expectTypeOf().parameters.toEqualTypeOf() + expectTypeOf().parameters.toEqualTypeOf() }) it('should return Config', () => { diff --git a/src/utils/analyze-outputs.ts b/src/utils/analyze-outputs.ts index 122862d5..0a31ead3 100644 --- a/src/utils/analyze-outputs.ts +++ b/src/utils/analyze-outputs.ts @@ -4,9 +4,9 @@ */ import type { OutputMetadata } from '#src/types' +import { entries, join, template } from '@flex-development/tutils' import * as color from 'colorette' import pb from 'pretty-bytes' -import { template } from 'radash' /** * Generates a build analysis for the given `outputs`. @@ -39,17 +39,17 @@ const analyzeOutputs = ( * * @const {number} size */ - const size: number = Object.entries(outputs).reduce((acc, output) => { + const size: number = entries(outputs).reduce((acc, output) => { const [outfile, metadata] = output strings.push(color.gray(`${indent}└─ ${outfile} (${pb(metadata.bytes)})`)) return acc + metadata.bytes }, 0) return { - analysis: template(`${indent}{{0}} (total size: {{1}})\n{{2}}`, { + analysis: template(`${indent}{0} (total size: {1})\n{2}`, { 0: color.bold(outdir), 1: color.cyan(pb(size)), - 2: strings.join('\n') + 2: join(strings, '\n') }), bytes: size } diff --git a/vitest.config.ts b/vitest.config.ts index 10b3f4aa..faee5ccb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,9 +5,8 @@ */ import pathe from '@flex-development/pathe' -import { NodeEnv } from '@flex-development/tutils' +import { NodeEnv, template } from '@flex-development/tutils' import ci from 'is-ci' -import { template } from 'radash' import tsconfigpaths from 'vite-tsconfig-paths' import GithubActionsReporter from 'vitest-github-actions-reporter' import { @@ -141,7 +140,7 @@ const config: UserConfigExport = defineConfig((): UserConfig => { checker: 'tsc', ignoreSourceErrors: false, include: ['**/__tests__/*.spec-d.ts'], - tsconfig: template('{{0}}/tsconfig.typecheck.json', { + tsconfig: template('{0}/tsconfig.typecheck.json', { 0: pathe.resolve(TYPESCRIPT_V5 ? '' : '__tests__/ts/v4') }) }, diff --git a/yarn.lock b/yarn.lock index 88c3b929..50e8d7f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1151,7 +1151,7 @@ __metadata: "@flex-development/pkg-types": "npm:2.0.0" "@flex-development/toggle-pkg-type": "npm:2.0.0" "@flex-development/tsconfig-utils": "npm:1.1.2" - "@flex-development/tutils": "npm:6.0.0-alpha.10" + "@flex-development/tutils": "npm:6.0.0-alpha.18" "@graphql-eslint/eslint-plugin": "npm:3.18.0" "@nestjs/common": "npm:9.4.1" "@nestjs/core": "npm:9.4.0" @@ -1196,7 +1196,6 @@ __metadata: cspell: "npm:6.31.1" dateformat: "npm:5.0.3" esbuild: "npm:0.17.19" - escape-string-regexp: "npm:5.0.0" eslint: "npm:8.39.0" eslint-config-prettier: "npm:8.8.0" eslint-plugin-chai-expect: "npm:3.0.0" @@ -1220,7 +1219,6 @@ __metadata: jsonc-eslint-parser: "npm:2.3.0" lint-staged: "npm:13.2.2" memfs: "npm:3.5.1" - merge-anything: "npm:5.1.6" mri: "npm:1.2.0" nest-commander: "npm:3.6.1" nest-commander-testing: "npm:3.1.0" @@ -1230,7 +1228,6 @@ __metadata: prettier-plugin-sh: "npm:0.12.8" pretty-bytes: "npm:6.1.0" pretty-format: "npm:29.5.0" - radash: "npm:10.8.1" reflect-metadata: "npm:0.1.13" rxjs: "npm:8.0.0-alpha.9" semver: "npm:7.5.2" @@ -1367,6 +1364,26 @@ __metadata: languageName: node linkType: hard +"@flex-development/tutils@npm:6.0.0-alpha.18": + version: 6.0.0-alpha.18 + resolution: "@flex-development/tutils@npm:6.0.0-alpha.18::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40flex-development%2Ftutils%2F6.0.0-alpha.18%2Fa8693102aab522ad1ecafda09438e513014dca6c" + dependencies: + dequal: "npm:2.0.3" + peerDependencies: + typescript: ">=5.0.4" + checksum: c506828a3a86998e36d8808b7313ca0c14922f5f49008c600706eec9b06678919b56f96004919b056e59392255f63002d5fe2ea98504212f44e16f6e43d7b141 + languageName: node + linkType: hard + +"@flex-development/tutils@npm:6.0.0-alpha.7": + version: 6.0.0-alpha.7 + resolution: "@flex-development/tutils@npm:6.0.0-alpha.7::__archiveUrl=https%3A%2F%2Fnpm.pkg.github.com%2Fdownload%2F%40flex-development%2Ftutils%2F6.0.0-alpha.7%2F786916dcfd30bf076b5ac74cb31ca4031f74e1f0" + peerDependencies: + typescript: ">=4.7" + checksum: 626d2df21cd31cfe0445c94e0946b7ca77a3c95ca93c92b20c512820bad32e7fe34c4c59f870eddcdc449decc0c364614df4fc1c3f4fef1b76592d5ec53c59d3 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -4307,6 +4324,13 @@ __metadata: languageName: node linkType: hard +"dequal@npm:2.0.3": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: 7a633ec0ba78bc08ba217b762b15157d2ec99edb50a82124df2c341255b1943217215872888981cc6a6ee02406ab1b09783f5b51b7db8d8f8f1284092f379aad + languageName: node + linkType: hard + "detect-indent@npm:^6.0.0": version: 6.1.0 resolution: "detect-indent@npm:6.1.0" @@ -6752,15 +6776,6 @@ __metadata: languageName: node linkType: hard -"merge-anything@npm:5.1.6": - version: 5.1.6 - resolution: "merge-anything@npm:5.1.6" - dependencies: - is-what: "npm:^4.1.8" - checksum: 3ccff2beef56b55200993400972056ba383f837369f22f359ec87c38106d1b27e135ffb3d736cbf423465d40c9292989178e254bd7f0d5d81a41172e18ea421e - languageName: node - linkType: hard - "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -7912,20 +7927,6 @@ __metadata: languageName: node linkType: hard -"radash@npm:10.8.1": - version: 10.8.1 - resolution: "radash@npm:10.8.1" - checksum: 43c1a0a67ed5e960841f68d4dd9d19a6879e5e13b9b8d6d817c309c65fa834ad51577243d2dfd9bbb8c40d2e7959e594f202c85abe6568aee93cfe88ee5400f2 - languageName: node - linkType: hard - -"radash@patch:radash@npm%3A10.8.1#patches/radash+10.8.1.dev.patch::locator=%40flex-development%2Fmkbuild%40workspace%3A.": - version: 10.8.1 - resolution: "radash@patch:radash@npm%3A10.8.1#patches/radash+10.8.1.dev.patch::version=10.8.1&hash=29b41c&locator=%40flex-development%2Fmkbuild%40workspace%3A." - checksum: dca43393e14f057da74df3976927030f21e3eb78aeff5db24f324623818d587ccd7975feeca6b701628f2874446dc7b74e7ea36b33fe2152d6c4b6edf1f1af6d - languageName: node - linkType: hard - "range-parser@npm:1.2.0": version: 1.2.0 resolution: "range-parser@npm:1.2.0"