mediasnacks 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,42 +11,34 @@ _mediasnacks_commands=(
11
11
  'hev1tohvc1:Fixes video thumbnails not rendering in macOS Finder'
12
12
  'framediff:ffplay with a filter for diffing adjacent frames'
13
13
  'vdiff:Plays a video with the difference of two videos'
14
- 'vconcat:Concatenates videos',
15
- 'dlaudio: yt-dlp best audio',
16
- 'dlvideo: yt-dlp best video',
14
+ 'vconcat:Concatenates videos'
15
+ 'dlaudio: yt-dlp best audio'
16
+ 'dlvideo: yt-dlp best video'
17
17
  'unemoji:Removes emojis from filenames'
18
18
  'rmcover:Removes cover art'
19
- 'curltime:Measures request response timings',
20
- 'gif:Video to GIF',
21
- 'vcut:Split video in two parts by time',
19
+ 'curltime:Measures request response timings'
20
+ 'gif:Video to GIF'
21
+ 'vcut:Split video in two parts by time'
22
22
  'flattendir:Moves unique files to the top dir and deletes empty dirs'
23
23
  )
24
24
 
25
- _mediasnacks() {
26
- if (( CURRENT == 2 )); then
27
- _describe -t commands 'mediasnacks commands' _mediasnacks_commands
28
- return
29
- fi
25
+ if (( CURRENT == 2 )); then
26
+ _describe -t commands 'mediasnacks commands' _mediasnacks_commands
27
+ return
28
+ fi
30
29
 
31
- local cmd="$words[2]"
32
- case "$cmd" in
33
- avif|resize|sqcrop|moov2front|dropdups|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|vcut|flattendir)
34
- _files
35
- ;;
36
- qdir)
37
- _files -/
38
- ;;
39
- esac
40
- }
41
-
42
- compdef _mediasnacks mediasnacks
30
+ local cmd="$words[2]"
31
+ case "$cmd" in
32
+ avif|resize|sqcrop|moov2front|dropdups|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|vcut|flattendir)
33
+ _files
34
+ ;;
35
+ qdir)
36
+ _files -/
37
+ ;;
38
+ esac
43
39
 
44
40
 
45
41
  # INSTALL in: ~/.zshrc
46
42
  #fpath=(~/.zsh/completions $fpath)
47
43
  #zmodload zsh/complist
48
44
  #autoload -U compinit && compinit
49
-
50
- #function mediasnacks() {
51
- # $HOME/work/mediasnacks/src/./cli.js "$@"
52
- #}
package/README.md CHANGED
@@ -18,23 +18,46 @@ Commands:
18
18
  - `dropdups` Removes duplicate frames in a video
19
19
  - `seqcheck` Finds missing sequence number
20
20
  - `qdir` Sequentially runs all *.sh files in a folder
21
- - `hev1tohvc1`: Fixes video thumbnails not rendering in macOS Finder
21
+ - `hev1tohvc1`: Fixes video thumbnails not rendering in macOS Finder
22
22
 
23
23
  - `framediff`: Plays a video of adjacent frames diff
24
24
  - `vdiff`: Plays a video with the difference of two videos
25
25
  - `vconcat`: Concatenates videos
26
-
26
+
27
27
  - `dlaudio`: yt-dlp best audio
28
28
  - `dlvideo`: yt-dlp best video
29
-
29
+
30
30
  - `unemoji`: Removes emojis from filenames
31
- - `rmcover`: Removes cover art
32
-
31
+ - `rmcover`: Removes cover art
32
+
33
33
  - `curltime`: Measures request response timings
34
34
  - `gif`: Video to GIF
35
35
  - `vcut`: Split video in two parts at a given time
36
-
36
+
37
37
  - `flattendir`: Moves unique files to the top dir and deletes empty dirs
38
+
39
+ ### Glob Patterns and Literal Filenames
40
+
41
+ Most commands accept glob patterns (like `*.png` or `file[234].png`) to match multiple files. By default, these patterns are expanded by Node.js to match existing files.
42
+
43
+ To treat arguments as literal filenames instead of glob patterns, use the `--` (double dash) separator:
44
+
45
+ ```shell
46
+ # This expands the pattern to match files: file2.png, file3.png, file4.png
47
+ npx mediasnacks avif file[234].png
48
+
49
+ # This treats the argument as a literal filename: "file[234].png"
50
+ npx mediasnacks avif -- file[234].png
51
+
52
+ # You can mix both: expand first pattern, treat second as literal
53
+ npx mediasnacks avif file2.png -- file[234].png
54
+ ```
55
+
56
+ This follows the standard POSIX convention where `--` stops option/pattern processing. Arguments after `--` are treated exactly as written, which is useful when:
57
+ - Filenames contain special glob characters like `[`, `]`, `*`, or `?`
58
+ - You want to pass a pattern to be processed later
59
+ - Working with filenames that haven't been created yet
60
+
38
61
  <br/>
39
62
 
40
63
  ### Converting Images to AVIF
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Utilities for preparing videos, images, and audio for the web",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
package/src/avif.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { join } from 'node:path'
4
- import { parseArgs } from 'node:util'
5
4
 
6
- import { replaceExt, lstat, globAll } from './utils/fs-utils.js'
5
+ import { replaceExt, lstat } from './utils/fs-utils.js'
6
+ import { parseArgsWithGlobs } from './utils/args-with-globs.js'
7
7
  import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
8
8
 
9
9
 
@@ -17,13 +17,12 @@ Converts images to AVIF.
17
17
  async function main() {
18
18
  await assertUserHasFFmpeg()
19
19
 
20
- const { values, positionals } = parseArgs({
20
+ const { values, files } = await parseArgsWithGlobs({
21
21
  options: {
22
22
  'output-dir': { type: 'string', default: '' },
23
23
  overwrite: { short: 'y', type: 'boolean', default: false },
24
24
  help: { short: 'h', type: 'boolean', default: false },
25
- },
26
- allowPositionals: true
25
+ }
27
26
  })
28
27
 
29
28
  if (values.help) {
@@ -31,11 +30,11 @@ async function main() {
31
30
  process.exit(0)
32
31
  }
33
32
 
34
- if (!positionals.length)
33
+ if (!files.length)
35
34
  throw new Error('No images specified. See npx mediasnacks avif --help')
36
35
 
37
36
  console.log('AVIF…')
38
- for (const file of await globAll(positionals))
37
+ for (const file of files)
39
38
  await toAvif({
40
39
  file,
41
40
  outFile: join(values['output-dir'], replaceExt(file, 'avif')),
package/src/dropdups.js CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseArgs } from 'node:util'
4
3
  import { resolve, parse, format } from 'node:path'
5
4
 
6
- import { globAll } from './utils/fs-utils.js'
5
+ import { parseArgsWithGlobs } from './utils/args-with-globs.js'
7
6
  import { ffmpeg, assertUserHasFFmpeg, run } from './utils/ffmpeg.js'
8
7
 
9
8
 
@@ -34,13 +33,12 @@ Options:
34
33
 
35
34
  async function main() {
36
35
  await assertUserHasFFmpeg()
37
-
38
- const { values, positionals } = parseArgs({
36
+
37
+ const { values, files } = await parseArgsWithGlobs({
39
38
  options: {
40
39
  'bad-frame-number': { short: 'n', type: 'string', default: '' },
41
40
  help: { short: 'h', type: 'boolean', default: false },
42
- },
43
- allowPositionals: true
41
+ }
44
42
  })
45
43
 
46
44
  if (values.help) {
@@ -48,7 +46,7 @@ async function main() {
48
46
  process.exit(0)
49
47
  }
50
48
 
51
- if (!positionals.length)
49
+ if (!files.length)
52
50
  throw new Error('No video specified. See npx mediasnacks dropdups --help')
53
51
 
54
52
  let nBadFrame = values['bad-frame-number']
@@ -56,7 +54,7 @@ async function main() {
56
54
  throw new Error('Invalid --bad-frame-number. It must be a positive integer.')
57
55
 
58
56
  console.log('Dropping Duplicate Frames…')
59
- for (const file of await globAll(positionals))
57
+ for (const file of files)
60
58
  await drop(resolve(file), nBadFrame)
61
59
  }
62
60
 
package/src/hev1tohvc1.js CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseArgs } from 'node:util'
4
-
5
- import { uniqueFilenameFor, overwrite, globAll } from './utils/fs-utils.js'
3
+ import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
4
+ import { parseArgsWithGlobs } from './utils/args-with-globs.js'
6
5
  import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
7
6
 
8
7
 
@@ -18,12 +17,13 @@ by changing the container’s sample entry code from HEV1 to HVC1.
18
17
  async function main() {
19
18
  await assertUserHasFFmpeg()
20
19
 
21
- const { positionals } = parseArgs({ allowPositionals: true })
22
- if (!positionals.length)
20
+ const { files } = await parseArgsWithGlobs({})
21
+
22
+ if (!files.length)
23
23
  throw new Error(USAGE)
24
24
 
25
25
  console.log('HEV1 to HVC1…')
26
- for (const file of await globAll(positionals))
26
+ for (const file of files)
27
27
  await toHvc1(file)
28
28
  }
29
29
 
package/src/moov2front.js CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseArgs } from 'node:util'
4
-
5
3
  import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
6
- import { uniqueFilenameFor, overwrite, globAll } from './utils/fs-utils.js'
4
+ import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
5
+ import { parseArgsWithGlobs } from './utils/args-with-globs.js'
7
6
 
8
7
 
9
8
  const USAGE = `
@@ -17,12 +16,13 @@ Files are overwritten.
17
16
  async function main() {
18
17
  await assertUserHasFFmpeg()
19
18
 
20
- const { positionals } = parseArgs({ allowPositionals: true })
21
- if (!positionals.length)
19
+ const { files } = await parseArgsWithGlobs({})
20
+
21
+ if (!files.length)
22
22
  throw new Error(USAGE)
23
23
 
24
24
  console.log('Optimizing video for progressive download…')
25
- for (const file of await globAll(positionals))
25
+ for (const file of files)
26
26
  await moov2front(file)
27
27
  }
28
28
 
package/src/resize.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { join } from 'node:path'
4
4
  import { rename } from 'node:fs/promises'
5
- import { parseArgs } from 'node:util'
6
5
 
7
- import { isFile, uniqueFilenameFor, globAll } from './utils/fs-utils.js'
6
+ import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
7
+ import { parseArgsWithGlobs } from './utils/args-with-globs.js'
8
8
  import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/ffmpeg.js'
9
9
 
10
10
 
@@ -30,15 +30,14 @@ Details:
30
30
  async function main() {
31
31
  await assertUserHasFFmpeg()
32
32
 
33
- const { values, positionals } = parseArgs({
33
+ const { values, files } = await parseArgsWithGlobs({
34
34
  options: {
35
35
  width: { type: 'string', default: '-2' },
36
36
  height: { type: 'string', default: '-2' },
37
37
  'output-dir': { type: 'string', default: '' },
38
38
  overwrite: { short: 'y', type: 'boolean', default: false },
39
39
  help: { short: 'h', type: 'boolean', default: false },
40
- },
41
- allowPositionals: true
40
+ }
42
41
  })
43
42
 
44
43
  if (values.help) {
@@ -52,12 +51,12 @@ async function main() {
52
51
  if (width <= 0 && height <= 0)
53
52
  throw new Error('--width or --height need to be greater than 0')
54
53
 
55
- if (!positionals.length)
54
+ if (!files.length)
56
55
  throw new Error('No video files specified')
57
56
 
58
57
 
59
58
  console.log('Resizing…')
60
- for (const file of await globAll(positionals))
59
+ for (const file of files)
61
60
  await resize({
62
61
  file,
63
62
  outFile: join(values['output-dir'], file),
package/src/sqcrop.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { join } from 'node:path'
4
- import { parseArgs } from 'node:util'
5
4
 
6
5
  import { rename } from 'node:fs/promises'
7
6
  import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
8
- import { replaceExt, lstat, globAll, uniqueFilenameFor } from './utils/fs-utils.js'
7
+ import { replaceExt, lstat, uniqueFilenameFor } from './utils/fs-utils.js'
8
+ import { parseArgsWithGlobs } from './utils/args-with-globs.js'
9
9
 
10
10
 
11
11
  const USAGE = `
@@ -18,13 +18,12 @@ Square crops images
18
18
  async function main() {
19
19
  await assertUserHasFFmpeg()
20
20
 
21
- const { values, positionals } = parseArgs({
21
+ const { values, files } = await parseArgsWithGlobs({
22
22
  options: {
23
23
  'output-dir': { type: 'string', default: '' },
24
24
  overwrite: { short: 'y', type: 'boolean', default: false },
25
25
  help: { short: 'h', type: 'boolean', default: false },
26
- },
27
- allowPositionals: true
26
+ }
28
27
  })
29
28
 
30
29
  if (values.help) {
@@ -32,11 +31,11 @@ async function main() {
32
31
  process.exit(0)
33
32
  }
34
33
 
35
- if (!positionals.length)
34
+ if (!files.length)
36
35
  throw new Error('No images specified. See npx mediasnacks sqcrop --help')
37
36
 
38
37
  console.log('Cropping…')
39
- for (const file of await globAll(positionals))
38
+ for (const file of files)
40
39
  await sqcrop({
41
40
  file,
42
41
  outFile: join(values['output-dir'], file),
@@ -0,0 +1,61 @@
1
+ import { promisify } from 'node:util'
2
+ import { parseArgs } from 'node:util'
3
+ import { glob as _glob } from 'node:fs'
4
+
5
+
6
+ const glob = promisify(_glob)
7
+
8
+ export async function parseArgsWithGlobs(config) {
9
+ const { values, positionals, tokens } = parseArgs({
10
+ allowPositionals: true,
11
+ ...config,
12
+ tokens: true
13
+ })
14
+
15
+ const files = await globAll(positionals, tokens)
16
+
17
+ return { values, positionals, tokens, files }
18
+ }
19
+
20
+ export async function globAll(arr, tokens) {
21
+ const terminatorIndex = findTerminatorIndex(tokens)
22
+ const set = new Set()
23
+
24
+ if (terminatorIndex >= 0) {
25
+ // Glob arguments before the terminator
26
+ const beforeTerminator = arr.slice(0, terminatorIndex)
27
+ const afterTerminator = arr.slice(terminatorIndex)
28
+
29
+ for (const g of beforeTerminator)
30
+ for (const file of await glob(g))
31
+ set.add(file)
32
+
33
+ // Add arguments after terminator as literal strings
34
+ for (const literal of afterTerminator)
35
+ set.add(literal)
36
+ } else {
37
+ // No terminator, glob everything (current behavior)
38
+ for (const g of arr)
39
+ for (const file of await glob(g))
40
+ set.add(file)
41
+ }
42
+
43
+ return Array.from(set)
44
+ }
45
+
46
+ function findTerminatorIndex(tokens) {
47
+ if (!tokens) return -1
48
+ for (const token of tokens) {
49
+ if (token.kind === 'option-terminator') {
50
+ // Find the position in the positionals array
51
+ // The terminator itself is not in positionals, so we count preceding positionals
52
+ let positionalCount = 0
53
+ for (const t of tokens) {
54
+ if (t === token) break
55
+ if (t.kind === 'positional') positionalCount++
56
+ }
57
+ return positionalCount
58
+ }
59
+ }
60
+ return -1
61
+ }
@@ -0,0 +1,153 @@
1
+ import { equal, deepEqual } from 'node:assert/strict'
2
+ import test, { describe, before, after } from 'node:test'
3
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+ import { tmpdir } from 'node:os'
6
+ import { globAll, parseArgsWithGlobs } from './args-with-globs.js'
7
+
8
+
9
+ describe('globAll with token terminator', () => {
10
+ let testDir
11
+ const testFiles = ['test-file2.png', 'test-file3.png', 'test-file4.png', 'other-file.png']
12
+
13
+ before(async () => {
14
+ testDir = await mkdtemp(join(tmpdir(), 'glob-test-'))
15
+ for (const file of testFiles) {
16
+ await writeFile(join(testDir, file), '')
17
+ }
18
+ })
19
+
20
+ after(async () => {
21
+ await rm(testDir, { recursive: true, force: true })
22
+ })
23
+
24
+ test('globs patterns when no terminator present', async () => {
25
+ const pattern = join(testDir, 'test-file[234].png')
26
+ const result = await globAll([pattern])
27
+ const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
28
+ deepEqual(result.sort(), expected)
29
+ })
30
+
31
+ test('globs patterns before terminator and keeps literals after', async () => {
32
+ const pattern = join(testDir, 'test-file[234].png')
33
+ const literal = 'my-literal-file[234].png'
34
+
35
+ // Simulate tokens from parseArgs with terminator
36
+ const tokens = [
37
+ { kind: 'positional', index: 0, value: pattern },
38
+ { kind: 'option-terminator', index: 1 },
39
+ { kind: 'positional', index: 1, value: literal }
40
+ ]
41
+
42
+ const result = await globAll([pattern, literal], tokens)
43
+ const expected = [
44
+ ...testFiles.slice(0, 3).map(f => join(testDir, f)),
45
+ literal
46
+ ].sort()
47
+ deepEqual(result.sort(), expected)
48
+ })
49
+
50
+ test('keeps all arguments literal when terminator is first', async () => {
51
+ const literal1 = 'literal-file[234].png'
52
+ const literal2 = 'another-file[567].png'
53
+
54
+ // Simulate tokens where terminator comes before all positionals
55
+ const tokens = [
56
+ { kind: 'option-terminator', index: 0 },
57
+ { kind: 'positional', index: 0, value: literal1 },
58
+ { kind: 'positional', index: 1, value: literal2 }
59
+ ]
60
+
61
+ const result = await globAll([literal1, literal2], tokens)
62
+ deepEqual(result.sort(), [literal1, literal2].sort())
63
+ })
64
+
65
+ test('handles no terminator with tokens (backwards compat)', async () => {
66
+ const pattern = join(testDir, 'test-file[234].png')
67
+
68
+ // Tokens without terminator
69
+ const tokens = [
70
+ { kind: 'positional', index: 0, value: pattern }
71
+ ]
72
+
73
+ const result = await globAll([pattern], tokens)
74
+ const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
75
+ deepEqual(result.sort(), expected)
76
+ })
77
+
78
+ test('handles undefined tokens (backwards compat)', async () => {
79
+ const pattern = join(testDir, 'test-file[234].png')
80
+ const result = await globAll([pattern], undefined)
81
+ const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
82
+ deepEqual(result.sort(), expected)
83
+ })
84
+
85
+ test('handles empty arrays', async () => {
86
+ const result = await globAll([])
87
+ deepEqual(result, [])
88
+ })
89
+
90
+ test('deduplicates results', async () => {
91
+ const pattern = join(testDir, 'test-file2.png')
92
+ const result = await globAll([pattern, pattern])
93
+ deepEqual(result, [join(testDir, 'test-file2.png')])
94
+ })
95
+ })
96
+
97
+ describe('parseArgsWithGlobs', () => {
98
+ let testDir
99
+ const testFiles = ['file1.png', 'file2.png', 'file3.png']
100
+
101
+ before(async () => {
102
+ testDir = await mkdtemp(join(tmpdir(), 'parse-args-test-'))
103
+ for (const file of testFiles) {
104
+ await writeFile(join(testDir, file), '')
105
+ }
106
+ })
107
+
108
+ after(async () => {
109
+ await rm(testDir, { recursive: true, force: true })
110
+ })
111
+
112
+ test('parses args and globs files in one call', async () => {
113
+ const pattern = join(testDir, 'file[12].png')
114
+ const { values, files } = await parseArgsWithGlobs({
115
+ args: ['--output-dir', '/tmp', pattern],
116
+ options: {
117
+ 'output-dir': { type: 'string' }
118
+ }
119
+ })
120
+
121
+ equal(values['output-dir'], '/tmp')
122
+ deepEqual(files.sort(), [
123
+ join(testDir, 'file1.png'),
124
+ join(testDir, 'file2.png')
125
+ ].sort())
126
+ })
127
+
128
+ test('respects verbatim token in parseArgsWithGlobs', async () => {
129
+ const pattern = join(testDir, 'file[12].png')
130
+ const literal = 'literal-file[99].png'
131
+ const { files } = await parseArgsWithGlobs({
132
+ args: [pattern, '--', literal]
133
+ })
134
+
135
+ deepEqual(files.sort(), [
136
+ join(testDir, 'file1.png'),
137
+ join(testDir, 'file2.png'),
138
+ literal
139
+ ].sort())
140
+ })
141
+
142
+ test('returns empty files array when no positionals', async () => {
143
+ const { files, values } = await parseArgsWithGlobs({
144
+ args: ['--flag'],
145
+ options: {
146
+ flag: { type: 'boolean' }
147
+ }
148
+ })
149
+
150
+ equal(values.flag, true)
151
+ deepEqual(files, [])
152
+ })
153
+ })
@@ -1,20 +1,9 @@
1
- import { promisify } from 'node:util'
2
1
  import { randomUUID } from 'node:crypto'
3
2
  import { unlink, rename } from 'node:fs/promises'
4
3
  import { dirname, extname, join } from 'node:path'
5
- import { lstatSync, glob as _glob } from 'node:fs'
4
+ import { lstatSync } from 'node:fs'
6
5
 
7
6
 
8
- const glob = promisify(_glob)
9
-
10
- export async function globAll(arr) {
11
- const set = new Set()
12
- for (const g of arr)
13
- for (const file of await glob(g))
14
- set.add(file)
15
- return Array.from(set)
16
- }
17
-
18
7
  export const lstat = f => lstatSync(f, { throwIfNoEntry: false })
19
8
  export const isFile = path => lstat(path)?.isFile()
20
9
 
@@ -26,7 +15,7 @@ export const replaceExt = (f, ext) => {
26
15
  return parts.join('.')
27
16
  }
28
17
 
29
- export const uniqueFilenameFor = file =>
18
+ export const uniqueFilenameFor = file =>
30
19
  join(dirname(file), randomUUID() + extname(file))
31
20
 
32
21
 
@@ -4,18 +4,18 @@ import { replaceExt } from './fs-utils.js'
4
4
 
5
5
 
6
6
  describe('replaceExt', () => {
7
- test('replaces a simple extension', () =>
7
+ test('replaces a simple extension', () =>
8
8
  equal(replaceExt('file.txt', 'md'), 'file.md'))
9
9
 
10
- test('replaces a multi-part extension', () =>
10
+ test('replaces a multi-part extension', () =>
11
11
  equal(replaceExt('archive.tar.gz', 'zip'), 'archive.tar.zip'))
12
12
 
13
- test('adds extension when none exists', () =>
13
+ test('adds extension when none exists', () =>
14
14
  equal(replaceExt('README', 'md'), 'README.md'))
15
15
 
16
16
  test('handles empty filename', () =>
17
17
  equal(replaceExt('', 'ext'), '.ext'))
18
-
18
+
19
19
  test('handles dot-files', () =>
20
20
  equal(replaceExt('.env', 'txt'), '.env.txt'))
21
21
  })