diff --git a/src/create.ts b/src/create.ts index 33af4b31..b3fc260e 100644 --- a/src/create.ts +++ b/src/create.ts @@ -1,84 +1,16 @@ +import { WriteStream, WriteStreamSync } from '@isaacs/fs-minipass' +import { Minipass } from 'minipass' +import path from 'node:path' +import { list } from './list.js' +import { makeCommand } from './make-command.js' import { - dealias, - isFile, - isSync, - isSyncFile, TarOptions, TarOptionsFile, TarOptionsSync, TarOptionsSyncFile, - TarOptionsWithAliases, - TarOptionsWithAliasesFile, - TarOptionsWithAliasesSync, - TarOptionsWithAliasesSyncFile, } from './options.js' - -import { WriteStream, WriteStreamSync } from '@isaacs/fs-minipass' -import { Minipass } from 'minipass' -import path from 'node:path' -import { list } from './list.js' import { Pack, PackSync } from './pack.js' -export function create( - opt: TarOptionsWithAliasesSyncFile, - files?: string[], -): void -export function create( - opt: TarOptionsWithAliasesSync, - files?: string[], -): void -export function create( - opt: TarOptionsWithAliasesFile, - files?: string[], - cb?: () => any, -): Promise -export function create( - opt: TarOptionsWithAliasesFile, - cb: () => any, -): Promise -export function create( - opt: TarOptionsWithAliases, - files?: string[], -): Pack -export function create( - opt_: TarOptionsWithAliases, - files?: string[] | (() => any), - cb?: () => any, -): void | Promise | Pack { - if (typeof files === 'function') { - cb = files - } - - if (Array.isArray(opt_)) { - ;(files = opt_), (opt_ = {}) - } - - if (!files || !Array.isArray(files) || !files.length) { - throw new TypeError('no files or directories specified') - } - - files = Array.from(files) - - const opt = dealias(opt_) - - if (opt.sync && typeof cb === 'function') { - throw new TypeError( - 'callback not supported for sync tar functions', - ) - } - - if (!opt.file && typeof cb === 'function') { - throw new TypeError('callback only supported with file option') - } - - return ( - isSyncFile(opt) ? createFileSync(opt, files) - : isFile(opt) ? createFile(opt, files, cb) - : isSync(opt) ? createSync(opt, files) - : create_(opt, files) - ) -} - const createFileSync = (opt: TarOptionsSyncFile, files: string[]) => { const p = new PackSync(opt) const stream = new WriteStreamSync(opt.file, { @@ -88,11 +20,7 @@ const createFileSync = (opt: TarOptionsSyncFile, files: string[]) => { addFilesSync(p, files) } -const createFile = ( - opt: TarOptionsFile, - files: string[], - cb?: () => any, -) => { +const createFile = (opt: TarOptionsFile, files: string[]) => { const p = new Pack(opt) const stream = new WriteStream(opt.file, { mode: opt.mode || 0o666, @@ -107,7 +35,7 @@ const createFile = ( addFilesAsync(p, files) - return cb ? promise.then(cb, cb) : promise + return promise } const addFilesSync = (p: PackSync, files: string[]) => { @@ -153,8 +81,20 @@ const createSync = (opt: TarOptionsSync, files: string[]) => { return p } -const create_ = (opt: TarOptions, files: string[]) => { +const createAsync = (opt: TarOptions, files: string[]) => { const p = new Pack(opt) addFilesAsync(p, files) return p } + +export const create = makeCommand( + createFileSync, + createFile, + createSync, + createAsync, + (_opt, files) => { + if (!files?.length) { + throw new TypeError('no paths specified to add to archive') + } + }, +) diff --git a/src/extract.ts b/src/extract.ts index 35dc4946..85a5bcdf 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -1,123 +1,13 @@ // tar -x import * as fsm from '@isaacs/fs-minipass' import fs from 'node:fs' -import { dirname, parse } from 'node:path' -import { - dealias, - isFile, - isSync, - isSyncFile, - TarOptions, - TarOptionsFile, - TarOptionsSync, - TarOptionsSyncFile, - TarOptionsWithAliases, - TarOptionsWithAliasesFile, - TarOptionsWithAliasesSync, - TarOptionsWithAliasesSyncFile, -} from './options.js' -import { stripTrailingSlashes } from './strip-trailing-slashes.js' +import { filesFilter } from './list.js' +import { makeCommand } from './make-command.js' +import { TarOptionsFile, TarOptionsSyncFile } from './options.js' import { Unpack, UnpackSync } from './unpack.js' -export function extract( - opt: TarOptionsWithAliasesSyncFile, - files?: string[], -): void -export function extract( - opt: TarOptionsWithAliasesSync, - files?: string[], -): void -export function extract( - opt: TarOptionsWithAliasesFile, - files?: string[], - cb?: () => any, -): Promise -export function extract( - opt: TarOptionsWithAliasesFile, - cb: () => any, -): Promise -export function extract( - opt: TarOptionsWithAliases, - files?: string[], -): Unpack -export function extract( - opt_: TarOptionsWithAliases, - files?: string[] | (() => any), - cb?: () => any, -): void | Promise | Unpack { - if (typeof opt_ === 'function') { - ;(cb = opt_), (files = undefined), (opt_ = {}) - } else if (Array.isArray(opt_)) { - ;(files = opt_), (opt_ = {}) - } - - if (typeof files === 'function') { - ;(cb = files), (files = undefined) - } - - if (!files) { - files = [] - } else { - files = Array.from(files) - } - - const opt = dealias(opt_) - - if (opt.sync && typeof cb === 'function') { - throw new TypeError( - 'callback not supported for sync tar functions', - ) - } - - if (!opt.file && typeof cb === 'function') { - throw new TypeError('callback only supported with file option') - } - - if (files.length) { - filesFilter(opt, files) - } - - return ( - isSyncFile(opt) ? extractFileSync(opt) - : isFile(opt) ? extractFile(opt, cb) - : isSync(opt) ? extractSync(opt) - : extract_(opt) - ) -} - -// construct a filter that limits the file entries listed -// include child entries if a dir is included -const filesFilter = (opt: TarOptions, files: string[]) => { - const map = new Map(files.map(f => [stripTrailingSlashes(f), true])) - const filter = opt.filter - - const mapHas = (file: string, r: string = ''): boolean => { - const root = r || parse(file).root || '.' - let ret: boolean - if (file === root) ret = false - else { - const m = map.get(file) - if (m !== undefined) { - ret = m - } else { - ret = mapHas(dirname(file), root) - } - } - - map.set(file, ret) - return ret - } - - opt.filter = - filter ? - (file, entry) => - filter(file, entry) && mapHas(stripTrailingSlashes(file)) - : file => mapHas(stripTrailingSlashes(file)) -} - const extractFileSync = (opt: TarOptionsSyncFile) => { const u = new UnpackSync(opt) - const file = opt.file const stat = fs.statSync(file) // This trades a zero-byte read() syscall for a stat @@ -130,7 +20,7 @@ const extractFileSync = (opt: TarOptionsSyncFile) => { stream.pipe(u) } -const extractFile = (opt: TarOptionsFile, cb?: () => void) => { +const extractFile = (opt: TarOptionsFile, _?: string[]) => { const u = new Unpack(opt) const readSize = opt.maxReadSize || 16 * 1024 * 1024 @@ -154,9 +44,15 @@ const extractFile = (opt: TarOptionsFile, cb?: () => void) => { } }) }) - return cb ? p.then(cb, cb) : p + return p } -const extractSync = (opt: TarOptionsSync) => new UnpackSync(opt) - -const extract_ = (opt: TarOptions) => new Unpack(opt) +export const extract = makeCommand( + extractFileSync, + extractFile, + opt => new UnpackSync(opt), + opt => new Unpack(opt), + (opt, files) => { + if (files?.length) filesFilter(opt, files) + }, +) diff --git a/src/index.ts b/src/index.ts index bc2c67fe..f19166e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,30 @@ -export * from './create.js' -export * from './replace.js' -export * from './list.js' -export * from './update.js' -export * from './extract.js' +export { + type TarOptionsWithAliasesAsync, + type TarOptionsWithAliasesAsyncFile, + type TarOptionsWithAliasesAsyncNoFile, + type TarOptionsWithAliasesSyncNoFile, + type TarOptionsWithAliases, + type TarOptionsWithAliasesFile, + type TarOptionsWithAliasesSync, + type TarOptionsWithAliasesSyncFile, +} from './options.js' +export * from './create.js' export { create as c } from './create.js' -export { replace as r } from './replace.js' -export { list as t } from './list.js' -export { update as u } from './update.js' +export * from './extract.js' export { extract as x } from './extract.js' - +export * from './header.js' +export * from './list.js' +export { list as t } from './list.js' // classes export * from './pack.js' -export * from './unpack.js' export * from './parse.js' -export * from './read-entry.js' -export * from './write-entry.js' -export * from './header.js' export * from './pax.js' +export * from './read-entry.js' +export * from './replace.js' +export { replace as r } from './replace.js' export * as types from './types.js' +export * from './unpack.js' +export * from './update.js' +export { update as u } from './update.js' +export * from './write-entry.js' diff --git a/src/list.ts b/src/list.ts index f02d140a..3cdfbb14 100644 --- a/src/list.ts +++ b/src/list.ts @@ -2,90 +2,15 @@ import * as fsm from '@isaacs/fs-minipass' import fs from 'node:fs' import { dirname, parse } from 'path' +import { makeCommand } from './make-command.js' import { - dealias, - isFile, - isSyncFile, TarOptions, TarOptionsFile, TarOptionsSyncFile, - TarOptionsWithAliases, - TarOptionsWithAliasesFile, - TarOptionsWithAliasesSync, - TarOptionsWithAliasesSyncFile, } from './options.js' import { Parser } from './parse.js' import { stripTrailingSlashes } from './strip-trailing-slashes.js' -export function list( - opt: TarOptionsWithAliasesSyncFile, - files?: string[], -): void -export function list( - opt: TarOptionsWithAliasesSync, - files?: string[], -): void -export function list( - opt: TarOptionsWithAliasesFile, - files?: string[], - cb?: () => any, -): Promise -export function list( - opt: TarOptionsWithAliasesFile, - cb: () => any, -): Promise -export function list( - opt: TarOptionsWithAliases, - files?: string[], -): Parser -export function list( - opt_: TarOptionsWithAliases, - files?: string[] | (() => any), - cb?: () => any, -): void | Promise | Parser { - if (typeof opt_ === 'function') { - ;(cb = opt_), (files = undefined), (opt_ = {}) - } else if (Array.isArray(opt_)) { - ;(files = opt_), (opt_ = {}) - } - - if (typeof files === 'function') { - ;(cb = files), (files = undefined) - } - - if (!files) { - files = [] - } else { - files = Array.from(files) - } - - const opt = dealias(opt_) - - if (opt.sync && typeof cb === 'function') { - throw new TypeError( - 'callback not supported for sync tar functions', - ) - } - - if (!opt.file && typeof cb === 'function') { - throw new TypeError('callback only supported with file option') - } - - if (files.length) { - filesFilter(opt, files) - } - - if (!opt.noResume) { - onentryFunction(opt) - } - - return ( - isSyncFile(opt) ? listFileSync(opt) - : isFile(opt) ? listFile(opt, cb) - : list_(opt) - ) -} - const onentryFunction = (opt: TarOptions) => { const onentry = opt.onentry opt.onentry = @@ -99,7 +24,7 @@ const onentryFunction = (opt: TarOptions) => { // construct a filter that limits the file entries listed // include child entries if a dir is included -const filesFilter = (opt: TarOptions, files: string[]) => { +export const filesFilter = (opt: TarOptions, files: string[]) => { const map = new Map( files.map(f => [stripTrailingSlashes(f), true]), ) @@ -130,7 +55,7 @@ const filesFilter = (opt: TarOptions, files: string[]) => { } const listFileSync = (opt: TarOptionsSyncFile) => { - const p = list_(opt) + const p = new Parser(opt) const file = opt.file let fd try { @@ -161,7 +86,7 @@ const listFileSync = (opt: TarOptionsSyncFile) => { const listFile = ( opt: TarOptionsFile, - cb?: () => void, + _files: string[], ): Promise => { const parse = new Parser(opt) const readSize = opt.maxReadSize || 16 * 1024 * 1024 @@ -184,7 +109,16 @@ const listFile = ( } }) }) - return cb ? p.then(cb, cb) : p + return p } -const list_ = (opt: TarOptions) => new Parser(opt) +export const list = makeCommand( + listFileSync, + listFile, + opt => new Parser(opt) as Parser & { sync: true }, + opt => new Parser(opt), + (opt, files) => { + if (files?.length) filesFilter(opt, files) + if (!opt.noResume) onentryFunction(opt) + }, +) diff --git a/src/make-command.ts b/src/make-command.ts new file mode 100644 index 00000000..7a0e8b2e --- /dev/null +++ b/src/make-command.ts @@ -0,0 +1,246 @@ +import { + dealias, + isAsyncFile, + isAsyncNoFile, + isSyncFile, + isSyncNoFile, + TarOptions, + TarOptionsAsyncFile, + TarOptionsAsyncNoFile, + TarOptionsSyncFile, + TarOptionsSyncNoFile, + TarOptionsWithAliases, + TarOptionsWithAliasesAsync, + TarOptionsWithAliasesAsyncFile, + TarOptionsWithAliasesAsyncNoFile, + TarOptionsWithAliasesFile, + TarOptionsWithAliasesNoFile, + TarOptionsWithAliasesSync, + TarOptionsWithAliasesSyncFile, + TarOptionsWithAliasesSyncNoFile, +} from './options.js' + +export type CB = (er?: Error) => any + +export type TarCommand< + AsyncClass, + SyncClass extends { sync: true }, +> = { + // async and no file specified + (): AsyncClass + (opt: TarOptionsWithAliasesAsyncNoFile): AsyncClass + (entries: string[]): AsyncClass + ( + opt: TarOptionsWithAliasesAsyncNoFile, + entries: string[], + ): AsyncClass +} & { + // sync and no file + (opt: TarOptionsWithAliasesSyncNoFile): SyncClass + (opt: TarOptionsWithAliasesSyncNoFile, entries: string[]): SyncClass +} & { + // async and file + (opt: TarOptionsWithAliasesAsyncFile): Promise + ( + opt: TarOptionsWithAliasesAsyncFile, + entries: string[], + ): Promise + (opt: TarOptionsWithAliasesAsyncFile, cb: CB): Promise + ( + opt: TarOptionsWithAliasesAsyncFile, + entries: string[], + cb: CB, + ): Promise +} & { + // sync and file + (opt: TarOptionsWithAliasesSyncFile): void + (opt: TarOptionsWithAliasesSyncFile, entries: string[]): void +} & { + // sync, maybe file + (opt: TarOptionsWithAliasesSync): typeof opt extends ( + TarOptionsWithAliasesFile + ) ? + void + : typeof opt extends TarOptionsWithAliasesNoFile ? SyncClass + : void | SyncClass + ( + opt: TarOptionsWithAliasesSync, + entries: string[], + ): typeof opt extends TarOptionsWithAliasesFile ? void + : typeof opt extends TarOptionsWithAliasesNoFile ? SyncClass + : void | SyncClass +} & { + // async, maybe file + (opt: TarOptionsWithAliasesAsync): typeof opt extends ( + TarOptionsWithAliasesFile + ) ? + Promise + : typeof opt extends TarOptionsWithAliasesNoFile ? AsyncClass + : Promise | AsyncClass + ( + opt: TarOptionsWithAliasesAsync, + entries: string[], + ): typeof opt extends TarOptionsWithAliasesFile ? Promise + : typeof opt extends TarOptionsWithAliasesNoFile ? AsyncClass + : Promise | AsyncClass + (opt: TarOptionsWithAliasesAsync, cb: CB): Promise + ( + opt: TarOptionsWithAliasesAsync, + entries: string[], + cb: CB, + ): typeof opt extends TarOptionsWithAliasesFile ? Promise + : typeof opt extends TarOptionsWithAliasesNoFile ? never + : Promise +} & { + // maybe sync, file + (opt: TarOptionsWithAliasesFile): Promise | void + ( + opt: TarOptionsWithAliasesFile, + entries: string[], + ): typeof opt extends TarOptionsWithAliasesSync ? void + : typeof opt extends TarOptionsWithAliasesAsync ? Promise + : Promise | void + (opt: TarOptionsWithAliasesFile, cb: CB): Promise + ( + opt: TarOptionsWithAliasesFile, + entries: string[], + cb: CB, + ): typeof opt extends TarOptionsWithAliasesSync ? never + : typeof opt extends TarOptionsWithAliasesAsync ? Promise + : Promise +} & { + // maybe sync, no file + (opt: TarOptionsWithAliasesNoFile): typeof opt extends ( + TarOptionsWithAliasesSync + ) ? + SyncClass + : typeof opt extends TarOptionsWithAliasesAsync ? AsyncClass + : SyncClass | AsyncClass + ( + opt: TarOptionsWithAliasesNoFile, + entries: string[], + ): typeof opt extends TarOptionsWithAliasesSync ? SyncClass + : typeof opt extends TarOptionsWithAliasesAsync ? AsyncClass + : SyncClass | AsyncClass +} & { + // maybe sync, maybe file + (opt: TarOptionsWithAliases): typeof opt extends ( + TarOptionsWithAliasesFile + ) ? + typeof opt extends TarOptionsWithAliasesSync ? void + : typeof opt extends TarOptionsWithAliasesAsync ? Promise + : void | Promise + : typeof opt extends TarOptionsWithAliasesNoFile ? + typeof opt extends TarOptionsWithAliasesSync ? SyncClass + : typeof opt extends TarOptionsWithAliasesAsync ? AsyncClass + : SyncClass | AsyncClass + : typeof opt extends TarOptionsWithAliasesSync ? SyncClass | void + : typeof opt extends TarOptionsWithAliasesAsync ? + AsyncClass | Promise + : SyncClass | void | AsyncClass | Promise +} & { + // extras + syncFile: (opt: TarOptionsSyncFile, entries: string[]) => void + asyncFile: ( + opt: TarOptionsAsyncFile, + entries: string[], + cb?: CB, + ) => Promise + syncNoFile: ( + opt: TarOptionsSyncNoFile, + entries: string[], + ) => SyncClass + asyncNoFile: ( + opt: TarOptionsAsyncNoFile, + entries: string[], + ) => AsyncClass + validate?: (opt: TarOptions, entries?: string[]) => void +} + +export const makeCommand = < + AsyncClass, + SyncClass extends { sync: true }, +>( + syncFile: (opt: TarOptionsSyncFile, entries: string[]) => void, + asyncFile: ( + opt: TarOptionsAsyncFile, + entries: string[], + cb?: CB, + ) => Promise, + syncNoFile: ( + opt: TarOptionsSyncNoFile, + entries: string[], + ) => SyncClass, + asyncNoFile: ( + opt: TarOptionsAsyncNoFile, + entries: string[], + ) => AsyncClass, + validate?: (opt: TarOptions, entries?: string[]) => void, +): TarCommand => { + return Object.assign( + ( + opt_: TarOptionsWithAliases | string[] = [], + entries?: string[] | CB, + cb?: CB, + ) => { + if (Array.isArray(opt_)) { + entries = opt_ + opt_ = {} + } + + if (typeof entries === 'function') { + cb = entries + entries = undefined + } + + if (!entries) { + entries = [] + } else { + entries = Array.from(entries) + } + + const opt = dealias(opt_) + + validate?.(opt, entries) + + if (isSyncFile(opt)) { + if (typeof cb === 'function') { + throw new TypeError( + 'callback not supported for sync tar functions', + ) + } + return syncFile(opt, entries) + } else if (isAsyncFile(opt)) { + const p = asyncFile(opt, entries) + // weirdness to make TS happy + const c = cb ? cb : undefined + return c ? p.then(() => c(), c) : p + } else if (isSyncNoFile(opt)) { + if (typeof cb === 'function') { + throw new TypeError( + 'callback not supported for sync tar functions', + ) + } + return syncNoFile(opt, entries) + } else if (isAsyncNoFile(opt)) { + if (typeof cb === 'function') { + throw new TypeError( + 'callback only supported with file option', + ) + } + return asyncNoFile(opt, entries) + /* c8 ignore start */ + } else { + throw new Error('impossible options??') + } + /* c8 ignore stop */ + }, + { + syncFile, + asyncFile, + syncNoFile, + asyncNoFile, + validate, + }, + ) as TarCommand +} diff --git a/src/options.ts b/src/options.ts index fe33ac46..5dab6724 100644 --- a/src/options.ts +++ b/src/options.ts @@ -243,14 +243,14 @@ export interface TarOptions { chmod?: boolean /** - * When setting the {@link TarOptions#noChmod} option to `false`, you may + * When setting the {@link TarOptions#chmod} option to `true`, you may * provide a value here to avoid having to call the deprecated and * thread-unsafe `process.umask()` method. * - * This has no effect with `noChmod` is not set to false explicitly, as - * mode values are not set explicitly anyway. If `noChmod` is set to `false`, - * and a value is not provided here, then `process.umask()` must be called, - * which will result in deprecation warnings. + * This has no effect with `chmod` is not set to true, as mode values are not + * set explicitly anyway. If `chmod` is set to `true`, and a value is not + * provided here, then `process.umask()` must be called, which will result in + * deprecation warnings. * * The most common values for this are `0o22` (resulting in directories * created with mode `0o755` and files with `0o644` by default) and `0o2` @@ -469,8 +469,13 @@ export interface TarOptions { } export type TarOptionsSync = TarOptions & { sync: true } +export type TarOptionsAsync = TarOptions & { sync?: false } export type TarOptionsFile = TarOptions & { file: string } +export type TarOptionsNoFile = TarOptions & { file?: undefined } export type TarOptionsSyncFile = TarOptionsSync & TarOptionsFile +export type TarOptionsAsyncFile = TarOptionsAsync & TarOptionsFile +export type TarOptionsSyncNoFile = TarOptionsSync & TarOptionsNoFile +export type TarOptionsAsyncNoFile = TarOptionsAsync & TarOptionsNoFile export type LinkCacheKey = `${number}:${number}` @@ -614,6 +619,9 @@ export interface TarOptionsWithAliases extends TarOptions { export type TarOptionsWithAliasesSync = TarOptionsWithAliases & { sync: true } +export type TarOptionsWithAliasesAsync = TarOptionsWithAliases & { + sync?: false +} export type TarOptionsWithAliasesFile = | (TarOptionsWithAliases & { file: string @@ -621,11 +629,43 @@ export type TarOptionsWithAliasesFile = | (TarOptionsWithAliases & { f: string }) export type TarOptionsWithAliasesSyncFile = TarOptionsWithAliasesSync & TarOptionsWithAliasesFile +export type TarOptionsWithAliasesAsyncFile = + TarOptionsWithAliasesAsync & TarOptionsWithAliasesFile + +export type TarOptionsWithAliasesNoFile = TarOptionsWithAliases & { + f?: undefined + file?: undefined +} -export const isSyncFile = (o: TarOptions): o is TarOptionsSyncFile => - !!o.sync && !!o.file -export const isSync = (o: TarOptions): o is TarOptionsSync => !!o.sync -export const isFile = (o: TarOptions): o is TarOptionsFile => !!o.file +export type TarOptionsWithAliasesSyncNoFile = + TarOptionsWithAliasesSync & TarOptionsWithAliasesNoFile +export type TarOptionsWithAliasesAsyncNoFile = + TarOptionsWithAliasesAsync & TarOptionsWithAliasesNoFile + +export const isSyncFile = ( + o: O, +): o is O & TarOptionsSyncFile => !!o.sync && !!o.file +export const isAsyncFile = ( + o: O, +): o is O & TarOptionsAsyncFile => !o.sync && !!o.file +export const isSyncNoFile = ( + o: O, +): o is O & TarOptionsSyncNoFile => !!o.sync && !o.file +export const isAsyncNoFile = ( + o: O, +): o is O & TarOptionsAsyncNoFile => !o.sync && !o.file +export const isSync = ( + o: O, +): o is O & TarOptionsSync => !!o.sync +export const isAsync = ( + o: O, +): o is O & TarOptionsAsync => !o.sync +export const isFile = ( + o: O, +): o is O & TarOptionsFile => !!o.file +export const isNoFile = ( + o: O, +): o is O & TarOptionsNoFile => !o.file const dealiasKey = ( k: keyof TarOptionsWithAliases, diff --git a/src/pack.ts b/src/pack.ts index 1ad0a9c5..075d4282 100644 --- a/src/pack.ts +++ b/src/pack.ts @@ -448,6 +448,7 @@ export class Pack } export class PackSync extends Pack { + sync: true = true constructor(opt: TarOptions) { super(opt) this[WRITEENTRYCLASS] = WriteEntrySync diff --git a/src/replace.ts b/src/replace.ts index b65d2c13..1d43fbb4 100644 --- a/src/replace.ts +++ b/src/replace.ts @@ -5,15 +5,11 @@ import fs from 'node:fs' import path from 'node:path' import { Header } from './header.js' import { list } from './list.js' +import { makeCommand } from './make-command.js' import { - dealias, isFile, - isSyncFile, TarOptionsFile, TarOptionsSyncFile, - TarOptionsWithAliases, - TarOptionsWithAliasesFile, - TarOptionsWithAliasesSyncFile, } from './options.js' import { Pack, PackSync } from './pack.js' @@ -23,50 +19,6 @@ import { Pack, PackSync } from './pack.js' // and try again. // Write the new Pack stream starting there. -export function replace( - opt: TarOptionsWithAliasesSyncFile, - files?: string[], -): void -export function replace( - opt: TarOptionsWithAliasesFile, - files?: string[], - cb?: () => any, -): Promise -export function replace( - opt: TarOptionsWithAliasesFile, - cb: () => any, -): Promise -export function replace( - opt_: TarOptionsWithAliases, - files?: string[] | (() => any), - cb?: () => any, -): void | Promise { - const opt = dealias(opt_) - - if (!isFile(opt)) { - throw new TypeError('file is required') - } - - if ( - opt.gzip || - opt.brotli || - opt.file.endsWith('.br') || - opt.file.endsWith('.tbr') - ) { - throw new TypeError('cannot append to compressed archives') - } - - if (!files || !Array.isArray(files) || !files.length) { - throw new TypeError('no files or directories specified') - } - - files = Array.from(files) - - return isSyncFile(opt) ? - replaceSync(opt, files) - : replace_(opt, files, cb) -} - const replaceSync = (opt: TarOptionsSyncFile, files: string[]) => { const p = new PackSync(opt) @@ -157,10 +109,9 @@ const streamSync = ( addFilesSync(p, files) } -const replace_ = ( +const replaceAsync = ( opt: TarOptionsFile, files: string[], - cb?: () => void, ): Promise => { files = Array.from(files) const p = new Pack(opt) @@ -278,7 +229,7 @@ const replace_ = ( fs.open(opt.file, flag, onopen) }) - return cb ? promise.then(cb, cb) : promise + return promise } const addFilesSync = (p: Pack, files: string[]) => { @@ -315,3 +266,34 @@ const addFilesAsync = async ( } p.end() } + +export const replace = makeCommand( + replaceSync, + replaceAsync, + /* c8 ignore start */ + (): never => { + throw new TypeError('file is required') + }, + (): never => { + throw new TypeError('file is required') + }, + /* c8 ignore stop */ + (opt, entries) => { + if (!isFile(opt)) { + throw new TypeError('file is required') + } + + if ( + opt.gzip || + opt.brotli || + opt.file.endsWith('.br') || + opt.file.endsWith('.tbr') + ) { + throw new TypeError('cannot append to compressed archives') + } + + if (!entries?.length) { + throw new TypeError('no paths specified to add/replace') + } + }, +) diff --git a/src/unpack.ts b/src/unpack.ts index f2eb4e3a..154b9492 100644 --- a/src/unpack.ts +++ b/src/unpack.ts @@ -883,6 +883,8 @@ const callSync = (fn: () => any) => { } export class UnpackSync extends Unpack { + sync: true = true; + [MAKEFS](er: null | Error | undefined, entry: ReadEntry) { return super[MAKEFS](er, entry, () => {}) } diff --git a/src/update.ts b/src/update.ts index c824bbef..06dcc46e 100644 --- a/src/update.ts +++ b/src/update.ts @@ -1,44 +1,21 @@ // tar -u -import { - dealias, - isFile, - type TarOptionsWithAliases, -} from './options.js' +import { makeCommand } from './make-command.js' +import { type TarOptionsWithAliases } from './options.js' import { replace as r } from './replace.js' // just call tar.r with the filter and mtimeCache - -export const update = ( - opt_: TarOptionsWithAliases, - files: string[], - cb?: (er?: Error) => any, -) => { - const opt = dealias(opt_) - - if (!isFile(opt)) { - throw new TypeError('file is required') - } - - if ( - opt.gzip || - opt.brotli || - opt.file.endsWith('.br') || - opt.file.endsWith('.tbr') - ) { - throw new TypeError('cannot append to compressed archives') - } - - if (!files || !Array.isArray(files) || !files.length) { - throw new TypeError('no files or directories specified') - } - - files = Array.from(files) - mtimeFilter(opt) - - return r(opt, files, cb) -} +export const update = makeCommand( + r.syncFile, + r.asyncFile, + r.syncNoFile, + r.asyncNoFile, + (opt, entries = []) => { + r.validate?.(opt, entries) + mtimeFilter(opt) + }, +) const mtimeFilter = (opt: TarOptionsWithAliases) => { const filter = opt.filter diff --git a/src/write-entry.ts b/src/write-entry.ts index cfb797a4..4a7b2c65 100644 --- a/src/write-entry.ts +++ b/src/write-entry.ts @@ -53,11 +53,7 @@ const ONDRAIN = Symbol('ondrain') const PREFIX = Symbol('prefix') export class WriteEntry - extends Minipass< - Buffer, - Minipass.ContiguousData, - WarnEvent - > + extends Minipass implements Warner { path: string @@ -471,10 +467,7 @@ export class WriteEntry this.once('drain', cb) } - write( - buffer: Buffer | string, - cb?: () => void, - ): boolean + write(buffer: Buffer | string, cb?: () => void): boolean write( str: Buffer | string, encoding?: BufferEncoding | null, @@ -544,6 +537,8 @@ export class WriteEntry } export class WriteEntrySync extends WriteEntry implements Warner { + sync: true = true; + [LSTAT]() { this[ONLSTAT](fs.lstatSync(this.absolute)) } @@ -757,10 +752,7 @@ export class WriteEntryTar return modeFix(mode, this.type === 'Directory', this.portable) } - write( - buffer: Buffer | string, - cb?: () => void, - ): boolean + write(buffer: Buffer | string, cb?: () => void): boolean write( str: Buffer | string, encoding?: BufferEncoding | null, @@ -793,11 +785,15 @@ export class WriteEntryTar end(cb?: () => void): this end(chunk: Buffer | string, cb?: () => void): this - end(chunk: Buffer | string, encoding?: BufferEncoding, cb?: () => void): this + end( + chunk: Buffer | string, + encoding?: BufferEncoding, + cb?: () => void, + ): this end( chunk?: Buffer | string | (() => void), encoding?: BufferEncoding | (() => void), - cb?: () => void + cb?: () => void, ): this { if (this.blockRemain) { super.write(Buffer.alloc(this.blockRemain)) diff --git a/test/create.js b/test/create.ts similarity index 87% rename from test/create.js rename to test/create.ts index 9292c66d..567f545f 100644 --- a/test/create.js +++ b/test/create.ts @@ -1,9 +1,10 @@ -import t from 'tap' +import t, { Test } from 'tap' import { c, list, Pack, PackSync } from '../dist/esm/index.js' import fs from 'fs' import path from 'path' import { rimraf } from 'rimraf' import { mkdirp } from 'mkdirp' +//@ts-ignore import mutateFS from 'mutate-fs' import { spawn } from 'child_process' import { fileURLToPath } from 'url' @@ -14,9 +15,16 @@ const __dirname = path.dirname(__filename) const dir = path.resolve(__dirname, 'fixtures/create') const tars = path.resolve(__dirname, 'fixtures/tars') -const readtar = (file, cb) => { +const readtar = ( + file: string, + cb: ( + code: number | null, + signal: null | NodeJS.Signals, + output: string, + ) => any, +) => { const child = spawn('tar', ['tf', file]) - const out = [] + const out: Buffer[] = [] child.stdout.on('data', c => out.push(c)) child.on('close', (code, signal) => cb(code, signal, Buffer.concat(out).toString()), @@ -31,7 +39,9 @@ t.before(async () => { }) t.test('no cb if sync or without file', t => { + //@ts-expect-error t.throws(() => c({ sync: true }, ['asdf'], () => {})) + //@ts-expect-error t.throws(() => c(() => {})) t.throws(() => c({}, () => {})) t.throws(() => c({}, ['asdf'], () => {})) @@ -54,7 +64,7 @@ t.test('create file', t => { readtar(file, (code, signal, list) => { t.equal(code, 0) t.equal(signal, null) - t.equal(list.trim(), 'create.js') + t.equal(list.trim(), 'create.ts') t.end() }) }) @@ -74,7 +84,7 @@ t.test('create file', t => { readtar(file, (code, signal, list) => { t.equal(code, 0) t.equal(signal, null) - t.equal(list.trim(), 'create.js') + t.equal(list.trim(), 'create.ts') t.end() }) }, @@ -93,7 +103,7 @@ t.test('create file', t => { readtar(file, (code, signal, list) => { t.equal(code, 0) t.equal(signal, null) - t.equal(list.trim(), 'create.js') + t.equal(list.trim(), 'create.ts') t.end() }) }) @@ -115,7 +125,7 @@ t.test('create file', t => { readtar(file, (code, signal, list) => { t.equal(code, 0) t.equal(signal, null) - t.equal(list.trim(), 'create.js') + t.equal(list.trim(), 'create.ts') t.equal(fs.lstatSync(file).mode & 0o7777, mode) t.end() }) @@ -137,7 +147,7 @@ t.test('create file', t => { readtar(file, (code, signal, list) => { t.equal(code, 0) t.equal(signal, null) - t.equal(list.trim(), 'create.js') + t.equal(list.trim(), 'create.ts') t.equal(fs.lstatSync(file).mode & 0o7777, mode) t.end() }) @@ -151,8 +161,16 @@ t.test('create file', t => { }) t.test('create', t => { - t.type(c({ sync: true }, ['README.md']), PackSync) + const ps = c({ sync: true }, ['README.md']) + t.equal(ps.sync, true) + t.type(ps, PackSync) + const p = c(['README.md']) + //@ts-expect-error + p.then + //@ts-expect-error + p.sync t.type(c(['README.md']), Pack) + t.end() }) @@ -210,7 +228,7 @@ t.test('gzipped tarball that makes some drain/resume stuff', t => { t.test('create tarball out of another tarball', t => { const out = path.resolve(dir, 'out.tar') - const check = t => { + const check = (t: Test) => { const expect = [ 'dir/', 'Ω.txt', @@ -265,3 +283,8 @@ t.test('create tarball out of another tarball', t => { t.end() }) + +t.test('must specify some files', t => { + t.throws(() => c({}), 'no paths specified to add to archive') + t.end() +}) diff --git a/test/extract.js b/test/extract.ts similarity index 73% rename from test/extract.js rename to test/extract.ts index ce2afeb7..37d2939e 100644 --- a/test/extract.js +++ b/test/extract.ts @@ -1,24 +1,25 @@ -import t from 'tap' -import nock from 'nock' -import { extract as x } from '../dist/esm/extract.js' -import path from 'path' import fs from 'fs' -import { fileURLToPath } from 'url' -import { promisify } from 'util' +import http from 'http' import { mkdirp } from 'mkdirp' +import nock from 'nock' +import path from 'path' import { rimraf } from 'rimraf' import { pipeline as PL } from 'stream' +import t, { Test } from 'tap' +import { fileURLToPath } from 'url' +import { promisify } from 'util' +import { extract as x } from '../dist/esm/extract.js' import { Unpack, UnpackSync } from '../dist/esm/unpack.js' const pipeline = promisify(PL) -import http from 'http' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const extractdir = path.resolve(__dirname, 'fixtures/extract') const tars = path.resolve(__dirname, 'fixtures/tars') +//@ts-ignore import mutateFS from 'mutate-fs' -const tnock = (t, host, opts) => { +const tnock = (t: Test, host: string, opts?: nock.Options) => { nock.disableNetConnect() const server = nock(host, opts) t.teardown(function () { @@ -39,7 +40,7 @@ t.test('basic extracting', t => { await mkdirp(dir) }) - const check = async t => { + const check = async (t: Test) => { fs.lstatSync(dir + '/Ω.txt') fs.lstatSync(dir + '/🌟.txt') t.throws(() => @@ -55,22 +56,53 @@ t.test('basic extracting', t => { const files = ['🌟.txt', 'Ω.txt'] t.test('sync', t => { - x({ file: file, sync: true, C: dir }, files) + x({ file, sync: true, C: dir }, files) return check(t) }) t.test('async promisey', async t => { - await x({ file: file, cwd: dir }, files) + const p = x({ file, cwd: dir }, files) + //@ts-expect-error + p.sync + //@ts-expect-error + p.write + await p return check(t) }) t.test('async cb', async t => { - await x({ file: file, cwd: dir }, files, er => { + const p = x({ file, cwd: dir }, files, er => { if (er) { throw er } return check(t) }) + //@ts-expect-error + p.sync + //@ts-expect-error + p.write + await p + }) + + t.test('stream sync', t => { + const ups = x({ cwd: dir, sync: true }, files) + ups.end(fs.readFileSync(file)) + //@ts-expect-error + ups.then + t.equal(ups.sync, true) + return check(t) + }) + + t.test('stream async', async t => { + const up = x({ cwd: dir }, files) + //@ts-expect-error + up.then + //@ts-expect-error + up.sync + await new Promise(r => + up.end(fs.readFileSync(file)).on('end', r), + ) + return check(t) }) t.end() @@ -87,7 +119,7 @@ t.test('ensure an open stream is not prematurely closed', t => { await mkdirp(dir) }) - const check = async t => { + const check = async (t: Test) => { t.ok(fs.lstatSync(dir + '/long-path')) await rimraf(dir) t.end() @@ -115,7 +147,7 @@ t.test('ensure an open stream is not prematuraly closed http', t => { await mkdirp(dir) }) - const check = async t => { + const check = async (t: Test) => { t.ok(fs.lstatSync(dir + '/long-path')) await rimraf(dir) t.end() @@ -148,7 +180,7 @@ t.test('file list and filter', t => { await mkdirp(dir) }) - const check = async t => { + const check = async (t: Test) => { fs.lstatSync(dir + '/Ω.txt') t.throws(() => fs.lstatSync(dir + '/🌟.txt')) t.throws(() => @@ -162,35 +194,25 @@ t.test('file list and filter', t => { await rimraf(dir) } - const filter = path => path === 'Ω.txt' + const filter = (path: string) => path === 'Ω.txt' - t.test('sync', t => { - x({ filter: filter, file: file, sync: true, C: dir }, [ - '🌟.txt', - 'Ω.txt', - ]) + t.test('sync file', t => { + x({ filter, file, sync: true, C: dir }, ['🌟.txt', 'Ω.txt']) return check(t) }) - t.test('async promisey', async t => { - await x({ filter: filter, file: file, cwd: dir }, [ - '🌟.txt', - 'Ω.txt', - ]) + t.test('async file', async t => { + await x({ filter, file, cwd: dir }, ['🌟.txt', 'Ω.txt']) check(t) }) t.test('async cb', t => { - return x( - { filter: filter, file: file, cwd: dir }, - ['🌟.txt', 'Ω.txt'], - er => { - if (er) { - throw er - } - return check(t) - }, - ) + return x({ filter, file, cwd: dir }, ['🌟.txt', 'Ω.txt'], er => { + if (er) { + throw er + } + return check(t) + }) }) t.end() @@ -205,7 +227,7 @@ t.test('no file list', t => { await mkdirp(dir) }) - const check = async t => { + const check = async (t: Test) => { t.equal( fs.lstatSync(path.resolve(dir, '1024-bytes.txt')).size, 1024, @@ -219,13 +241,13 @@ t.test('no file list', t => { await rimraf(dir) } - t.test('sync', t => { - x({ file: file, sync: true, C: dir }) + t.test('sync file', t => { + x({ file, sync: true, C: dir }) return check(t) }) - t.test('async promisey', async t => { - await x({ file: file, cwd: dir }) + t.test('async promisey file', async t => { + await x({ file, cwd: dir }) return check(t) }) @@ -238,6 +260,27 @@ t.test('no file list', t => { }) }) + t.test('sync stream', t => { + const up = x({ sync: true, C: dir }) + t.equal(up.sync, true) + t.type(up, UnpackSync) + //@ts-expect-error + up.then + up.end(fs.readFileSync(file)) + return check(t) + }) + + t.test('async stream', t => { + const up = x({ C: dir }) + t.type(up, Unpack) + //@ts-expect-error + up.sync + //@ts-expect-error + up.then + up.end(fs.readFileSync(file)) + return new Promise(r => up.on('close', () => r(check(t)))) + }) + t.end() }) @@ -251,7 +294,7 @@ t.test('read in itty bits', t => { await mkdirp(dir) }) - const check = async t => { + const check = async (t: Test) => { t.equal( fs.lstatSync(path.resolve(dir, '1024-bytes.txt')).size, 1024, @@ -291,22 +334,39 @@ t.test('read in itty bits', t => { }) t.test('bad calls', t => { - t.throws(() => x(() => {})) + t.throws(() => x({}, () => {})) + t.throws(() => x({}, [], () => {})) + //@ts-expect-error t.throws(() => x({ sync: true }, () => {})) + //@ts-expect-error t.throws(() => x({ sync: true }, [], () => {})) t.end() }) t.test('no file', t => { - t.type(x(), Unpack) - t.type(x(['asdf']), Unpack) - t.type(x({ sync: true }), UnpackSync) + const up = x() + t.type(up, Unpack) + //@ts-expect-error + up.then + //@ts-expect-error + up.sync + const upf = x(['asdf']) + //@ts-expect-error + upf.then + //@ts-expect-error + upf.sync + t.type(upf, Unpack) + const ups = x({ sync: true }) + //@ts-expect-error + ups.then + t.equal(ups.sync, true) + t.type(ups, UnpackSync) t.end() }) -t.test('nonexistent', t => { +t.test('nonexistent', async t => { t.throws(() => x({ sync: true, file: 'does not exist' })) - x({ file: 'does not exist' }).catch(() => t.end()) + await t.rejects(x({ file: 'does not exist' })) }) t.test('read fail', t => { @@ -334,7 +394,7 @@ t.test('sync gzip error edge case test', async t => { x({ sync: true, file: file, - onwarn: (c, m, er) => { + onwarn: (_c: any, _m: any, er) => { throw er }, }) @@ -420,8 +480,10 @@ t.test('verify long linkname is not a problem', async t => { // See: https://github.com/isaacs/node-tar/issues/312 const file = path.resolve(__dirname, 'fixtures/long-linkname.tar') t.test('sync', t => { - x({ sync: true, strict: true, file, C: t.testdir({}) }) - t.ok(fs.lstatSync(t.testdirName + '/test').isSymbolicLink()) + const cwd = t.testdir({}) + const result = x({ sync: true, strict: true, file, cwd }) + t.equal(result, undefined) + t.ok(fs.lstatSync(cwd + '/test').isSymbolicLink()) t.end() }) t.test('async', async t => { diff --git a/test/list.js b/test/list.ts similarity index 76% rename from test/list.js rename to test/list.ts index 1533dc6d..cc11a491 100644 --- a/test/list.js +++ b/test/list.ts @@ -1,45 +1,52 @@ import fs, { readFileSync } from 'fs' +//@ts-ignore import mutateFS from 'mutate-fs' import { dirname, resolve } from 'path' -import t from 'tap' +import t, { Test } from 'tap' import { fileURLToPath } from 'url' import { list } from '../dist/esm/list.js' +import { Parser } from '../dist/esm/parse.js' +import { ReadEntry } from '../dist/esm/read-entry.js' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const lp = JSON.parse( readFileSync(__dirname + '/fixtures/parse/long-paths.json', 'utf8'), -) +) as ( + | ['meta', string] + | ['entry', Record] + | ['nullBlock' | 'eof' | 'end'] +)[] t.test('basic', t => { const file = resolve(__dirname, 'fixtures/tars/long-paths.tar') - const expect = lp + const expect = (lp as any[]) .filter(e => Array.isArray(e) && e[0] === 'entry') - .map(e => e[1].path) + .map((e: ['entry', Record]) => e[1].path as string) - const check = (actual, t) => { + const check = (actual: string[], t: Test) => { t.same(actual, expect) return Promise.resolve(null) } - ;[1000, null].forEach(maxReadSize => { + ;[1000, undefined].forEach(maxReadSize => { t.test('file maxReadSize=' + maxReadSize, t => { t.test('sync', t => { - const actual = [] - const onentry = entry => actual.push(entry.path) + const actual: string[] = [] + const onentry = (entry: ReadEntry) => actual.push(entry.path) list({ file: file, sync: true, - onentry: onentry, - maxReadSize: maxReadSize, + onentry, + maxReadSize, }) return check(actual, t) }) t.test('async promise', async t => { - const actual = [] - const onentry = entry => actual.push(entry.path) + const actual: string[] = [] + const onentry = (entry: ReadEntry) => actual.push(entry.path) return await list({ file, onentry, @@ -48,15 +55,15 @@ t.test('basic', t => { }) t.test('async cb', t => { - const actual = [] - const onentry = entry => actual.push(entry.path) + const actual: string[] = [] + const onentry = (entry: ReadEntry) => actual.push(entry.path) list( { file: file, onentry: onentry, maxReadSize: maxReadSize, }, - er => { + (er?: Error) => { if (er) { throw er } @@ -71,16 +78,16 @@ t.test('basic', t => { t.test('stream', t => { t.test('sync', t => { - const actual = [] - const onentry = entry => actual.push(entry.path) - const l = list({ sync: true, onentry: onentry }) + const actual: string[] = [] + const onentry = (entry: ReadEntry) => actual.push(entry.path) + const l = list({ sync: true, onentry }) l.end(fs.readFileSync(file)) return check(actual, t) }) t.test('async', t => { - const actual = [] - const onentry = entry => actual.push(entry.path) + const actual: string[] = [] + const onentry = (entry: ReadEntry) => actual.push(entry.path) const l = list() l.on('entry', onentry) l.on('end', _ => check(actual, t).then(_ => t.end())) @@ -109,8 +116,8 @@ t.test('basic', t => { ] t.test('no filter function', async t => { - const check = _ => t.same(actual, expect) - const actual = [] + const check = () => t.same(actual, expect) + const actual: string[] = [] return list( { file: file, @@ -121,9 +128,9 @@ t.test('basic', t => { }) t.test('no filter function, stream', t => { - const check = _ => t.same(actual, expect) - const actual = [] - const onentry = entry => actual.push(entry.path) + const check = () => t.same(actual, expect) + const actual: string[] = [] + const onentry = (entry: ReadEntry) => actual.push(entry.path) fs.createReadStream(file).pipe( list(fileList) .on('entry', onentry) @@ -135,8 +142,8 @@ t.test('basic', t => { }) t.test('filter function', async t => { - const check = _ => t.same(actual, expect.slice(0, 1)) - const actual = [] + const check = () => t.same(actual, expect.slice(0, 1)) + const actual: string[] = [] return list( { file: file, @@ -161,11 +168,11 @@ t.test('basic', t => { t.test('bad args', t => { t.throws( - _ => list({ file: __filename, sync: true }, _ => _), + () => list({ file: __filename, sync: true }, () => {}), new TypeError('callback not supported for sync tar functions'), ) t.throws( - _ => list(_ => _), + () => list({}, () => {}), new TypeError('callback only supported with file option'), ) t.end() @@ -176,7 +183,7 @@ t.test('stat fails', t => { t.teardown(mutateFS.statFail(poop)) t.test('sync', t => { t.plan(1) - t.throws(_ => list({ file: __filename, sync: true }), poop) + t.throws(() => list({ file: __filename, sync: true }), poop) }) t.test('cb', t => { t.plan(1) @@ -195,7 +202,7 @@ t.test('read fail', t => { t.teardown(mutateFS.fail('read', poop)) t.plan(1) t.throws( - _ => + () => list({ file: __filename, sync: true, @@ -222,12 +229,12 @@ t.test('read fail', t => { t.test('noResume option', t => { const file = resolve(__dirname, 'fixtures/tars/file.tar') t.test('sync', t => { - let e + let e!: ReadEntry list({ file: file, onentry: entry => { e = entry - process.nextTick(_ => { + process.nextTick(() => { t.notOk(entry.flowing) entry.resume() }) @@ -237,14 +244,14 @@ t.test('noResume option', t => { }) t.ok(e) t.notOk(e.flowing) - e.on('end', _ => t.end()) + e.on('end', () => t.end()) }) t.test('async', t => list({ file: file, onentry: entry => { - process.nextTick(_ => { + process.nextTick(() => { t.notOk(entry.flowing) entry.resume() }) @@ -255,3 +262,11 @@ t.test('noResume option', t => { t.end() }) + +t.test('typechecks', t => { + const p = list() + //@ts-expect-error + p.then + t.type(p, Parser) + t.end() +}) diff --git a/test/make-command.ts b/test/make-command.ts new file mode 100644 index 00000000..822abb7d --- /dev/null +++ b/test/make-command.ts @@ -0,0 +1,74 @@ +import t from 'tap' +import { makeCommand } from '../src/make-command.js' +import { + isAsyncFile, + isAsyncNoFile, + isSyncFile, + isSyncNoFile, +} from '../src/options.js' + +class Sync { + sync: true = true +} +class Async {} + +const cmd = makeCommand( + (opt, entries) => { + t.equal(isSyncFile(opt), true) + t.type(entries, Array) + }, + async (opt, entries) => { + t.equal(isAsyncFile(opt), true) + t.type(entries, Array) + }, + (opt, entries) => { + t.equal(isSyncNoFile(opt), true) + t.type(entries, Array) + return new Sync() + }, + (opt, entries) => { + t.equal(isAsyncNoFile(opt), true) + t.type(entries, Array) + return new Async() + }, + (opt, entries) => { + if (entries?.length === 2) throw new Error('should not be len 2') + if (!opt) throw new Error('should get opt') + }, +) + +t.test('validation function is called', t => { + t.throws(() => cmd({}, ['a', 'b'])) + t.throws(() => cmd({ sync: true }, ['a', 'b'])) + t.throws(() => cmd({ sync: true, file: 'x' }, ['a', 'b'])) + t.throws(() => cmd({ file: 'x' }, ['a', 'b'])) + // cases where cb is not allowed + t.throws(() => cmd({}, [], () => {})) + t.throws(() => cmd({}, () => {})) + //@ts-expect-error + t.throws(() => cmd({ sync: true }, [], () => {})) + //@ts-expect-error + t.throws(() => cmd({ sync: true }, () => {})) + t.throws(() => cmd({ sync: true, file: 'x' }, [], () => {})) + t.throws(() => cmd({ sync: true, file: 'x' }, () => {})) + t.end() +}) + +t.test('basic calls', async t => { + t.match(cmd(), Async) + t.match(cmd({}), Async) + t.match(cmd({}, []), Async) + t.match(cmd({ sync: true }), Sync) + t.match(cmd({ sync: true }, []), Sync) + t.equal(cmd({ sync: true, file: 'x' }), undefined) + t.equal(await cmd({ file: 'x' }), undefined) + t.equal(await cmd({ file: 'x' }, []), undefined) + let cbCalled = false + t.equal( + await cmd({ file: 'x' }, [], () => { + cbCalled = true + }), + undefined, + ) + t.equal(cbCalled, true, 'called callback') +}) diff --git a/test/options.js b/test/options.js index 7aa01caf..b5cac1b1 100644 --- a/test/options.js +++ b/test/options.js @@ -4,6 +4,11 @@ import { isSync, isSyncFile, isFile, + isAsyncFile, + isAsyncNoFile, + isSyncNoFile, + isAsync, + isNoFile, } from '../dist/esm/options.js' t.same(dealias(), {}) @@ -62,7 +67,13 @@ t.equal(isSync(dealias({ sync: true, f: 'x' })), true) t.equal(isSync(dealias({ file: 'x' })), false) t.equal(isSync(dealias({ sync: true })), true) t.equal(isSync(dealias({})), false) +t.equal(isAsync(dealias({})), true) t.equal(isFile(dealias({ sync: true, f: 'x' })), true) +t.equal(isNoFile(dealias({ sync: true, f: 'x' })), false) t.equal(isFile(dealias({ file: 'x' })), true) t.equal(isFile(dealias({ sync: true })), false) t.equal(isFile(dealias({})), false) +t.equal(isSyncFile(dealias({})), false) +t.equal(isSyncNoFile(dealias({ sync: true })), true) +t.equal(isAsyncFile(dealias({})), false) +t.equal(isAsyncNoFile(dealias({})), true) diff --git a/test/parse.js b/test/parse.js index 978caacd..d68e2198 100644 --- a/test/parse.js +++ b/test/parse.js @@ -306,7 +306,10 @@ t.test('fixture tests', t => { strict: strict, }) trackEvents(t, expect, p, true) - const first = tardata.subarray(0, Math.floor(tardata.length / 2)) + const first = tardata.subarray( + 0, + Math.floor(tardata.length / 2), + ) p.write(first.toString('hex'), 'hex') process.nextTick(() => p.end(tardata.subarray(Math.floor(tardata.length / 2))), diff --git a/test/replace.js b/test/replace.ts similarity index 89% rename from test/replace.js rename to test/replace.ts index abab2965..5c5c2bef 100644 --- a/test/replace.js +++ b/test/replace.ts @@ -1,7 +1,8 @@ -import t from 'tap' +import t, { Test } from 'tap' import { replace as r } from '../dist/esm/replace.js' import path, { dirname, resolve } from 'path' import fs from 'fs' +//@ts-ignore import mutateFS from 'mutate-fs' import { list } from '../dist/esm/list.js' import { fileURLToPath } from 'url' @@ -33,10 +34,10 @@ const fixtureDef = { } t.test('basic file add to archive (good or truncated)', t => { - const check = (file, t) => { + const check = (file: string, t: Test) => { const c = spawn('tar', ['tf', file], { stdio: [0, 'pipe', 2] }) - const out = [] - c.stdout.on('data', chunk => out.push(chunk)) + const out: Buffer[] = [] + c.stdout?.on('data', (chunk: Buffer) => out.push(chunk)) c.on('close', (code, signal) => { t.equal(code, 0) t.equal(signal, null) @@ -55,18 +56,13 @@ t.test('basic file add to archive (good or truncated)', t => { }) } - const files = [ + const files: (keyof typeof fixtureDef)[] = [ 'body-byte-counts.tar', 'no-null-eof.tar', 'truncated-head.tar', 'truncated-body.tar', ] - const td = files - .map(f => [f, fixtureDef[f]]) - .reduce((s, [k, v]) => { - s[k] = v - return s - }, {}) + const td = Object.fromEntries(files.map(f => [f, fixtureDef[f]])) const fileList = [path.basename(__filename)] t.test('sync', t => { t.plan(files.length) @@ -130,10 +126,10 @@ t.test('basic file add to archive (good or truncated)', t => { }) t.test('add to empty archive', t => { - const check = (file, t) => { + const check = (file: string, t: Test) => { const c = spawn('tar', ['tf', file]) - const out = [] - c.stdout.on('data', chunk => out.push(chunk)) + const out: Buffer[] = [] + c.stdout.on('data', (chunk: Buffer) => out.push(chunk)) c.on('close', (code, signal) => { t.equal(code, 0) t.equal(signal, null) @@ -143,13 +139,9 @@ t.test('add to empty archive', t => { }) } - const files = ['empty.tar', 'zero.tar'] - const td = files - .map(f => [f, fixtureDef[f]]) - .reduce((s, [k, v]) => { - s[k] = v - return s - }, {}) + const files: (keyof typeof fixtureDef)[] = ['empty.tar', 'zero.tar'] + const td = Object.fromEntries(files.map(f => [f, fixtureDef[f]])) + //@ts-ignore files.push('not-existing.tar') t.test('sync', t => { @@ -225,7 +217,7 @@ t.test('cannot append to gzipped archives', async t => { ) t.throws( - _ => + () => r( { file, @@ -238,7 +230,7 @@ t.test('cannot append to gzipped archives', async t => { ) t.throws( - _ => + () => r( { file, @@ -272,7 +264,7 @@ t.test('cannot append to brotli compressed archives', async t => { ) t.throws( - _ => + () => r( { file, @@ -285,7 +277,7 @@ t.test('cannot append to brotli compressed archives', async t => { ) t.throws( - _ => + () => r( { file, @@ -301,10 +293,10 @@ t.test('cannot append to brotli compressed archives', async t => { }) t.test('other throws', t => { - t.throws(_ => r({}, ['asdf']), new TypeError('file is required')) + t.throws(() => r({}, ['asdf']), new TypeError('file is required')) t.throws( - _ => r({ file: 'asdf' }, []), - new TypeError('no files or directories specified'), + () => r({ file: 'asdf' }, []), + new TypeError('no paths specified to add/replace'), ) t.end() }) @@ -316,7 +308,7 @@ t.test('broken open', t => { const file = resolve(dir, 'body-byte-counts.tar') const poop = new Error('poop') t.teardown(mutateFS.fail('open', poop)) - t.throws(_ => r({ sync: true, file }, ['README.md']), poop) + t.throws(() => r({ sync: true, file }, ['README.md']), poop) r({ file }, ['README.md'], er => { t.match(er, poop) t.end() @@ -332,7 +324,7 @@ t.test('broken fstat', t => { const dir = t.testdir(td) const file = resolve(dir, 'body-byte-counts.tar') t.teardown(mutateFS.fail('fstat', poop)) - t.throws(_ => r({ sync: true, file }, ['README.md']), poop) + t.throws(() => r({ sync: true, file }, ['README.md']), poop) t.end() }) t.test('async', t => { @@ -354,7 +346,7 @@ t.test('broken read', t => { const file = resolve(dir, 'body-byte-counts.tar') const poop = new Error('poop') t.teardown(mutateFS.fail('read', poop)) - t.throws(_ => r({ sync: true, file }, ['README.md']), poop) + t.throws(() => r({ sync: true, file }, ['README.md']), poop) r({ file }, ['README.md'], er => { t.match(er, poop) t.end() @@ -366,11 +358,11 @@ t.test('mtime cache', async t => { 'body-byte-counts.tar': fixtureDef['body-byte-counts.tar'], } - let mtimeCache + let mtimeCache: Map - const check = (file, t) => { + const check = (file: string, t: Test) => { const c = spawn('tar', ['tf', file]) - const out = [] + const out: Buffer[] = [] c.stdout.on('data', chunk => out.push(chunk)) c.on('close', (code, signal) => { t.equal(code, 0) @@ -386,9 +378,9 @@ t.test('mtime cache', async t => { 'zero-byte.txt', path.basename(__filename), ]) - const mtc = {} + const mtc: Record = {} mtimeCache.forEach( - (_v, k) => (mtc[k] = mtimeCache.get(k).toISOString()), + (_v, k) => (mtc[k] = mtimeCache.get(k)!.toISOString()), ) t.same(mtc, { '1024-bytes.txt': '2017-04-10T16:57:47.000Z', @@ -455,7 +447,7 @@ t.test('create tarball out of another tarball', t => { 'out.tar': fs.readFileSync(path.resolve(tars, 'dir.tar')), } - const check = (out, t) => { + const check = (out: string, t: Test) => { const expect = [ 'dir/', 'Ω.txt', diff --git a/test/update.js b/test/update.js index 48bdd04b..f171539e 100644 --- a/test/update.js +++ b/test/update.js @@ -311,7 +311,7 @@ t.test('other throws', t => { t.throws(_ => u({}, ['asdf']), new TypeError('file is required')) t.throws( _ => u({ file: 'asdf' }, []), - new TypeError('no files or directories specified'), + new TypeError('no paths specified to add/replace'), ) t.end() }) diff --git a/test/writable-assignment-check.ts b/test/writable-assignment-check.ts index 46e81595..a251bc80 100644 --- a/test/writable-assignment-check.ts +++ b/test/writable-assignment-check.ts @@ -1,5 +1,5 @@ -import { Unpack } from "../src/unpack.js"; -import { WriteEntry } from "../src/write-entry.js"; +import { Unpack } from '../src/unpack.js' +import { WriteEntry } from '../src/write-entry.js' import { Parser } from '../src/parse.js' import { fileURLToPath } from 'url'