@mux-magic/tools 0.1.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.
@@ -0,0 +1,143 @@
1
+ import {
2
+ type BackgroundColorName,
3
+ Chalk,
4
+ type ChalkInstance,
5
+ type ColorName,
6
+ type ForegroundColorName,
7
+ } from "chalk"
8
+
9
+ export const createAddColorToChalk =
10
+ (chalkColor?: ColorName) =>
11
+ (chalkInstance: ChalkInstance) =>
12
+ chalkColor && chalkColor in chalkInstance
13
+ ? chalkInstance[chalkColor]
14
+ : chalkInstance
15
+
16
+ export const messageTemplate = {
17
+ comparison: (firstItem: string, secondItem: string) =>
18
+ [firstItem].concat("\n", secondItem),
19
+ descriptiveComparison: (
20
+ description: unknown,
21
+ firstItem: string,
22
+ secondItem: string,
23
+ ) =>
24
+ [description].concat(
25
+ "\n",
26
+ "\n",
27
+ firstItem,
28
+ "\n",
29
+ secondItem,
30
+ ),
31
+ // Description followed by N items, each separated by a newline. Useful for
32
+ // download-summary logs and batch-result reports where the caller has a
33
+ // header line plus a variable-length list of details.
34
+ multipleItems: (description: string, items: string[]) =>
35
+ [description].concat(
36
+ "\n",
37
+ items.flatMap((item) => [item].concat("\n")),
38
+ ),
39
+ noItems: () => [],
40
+ singleItem: (item: unknown) => [item],
41
+ } as const
42
+
43
+ const numericalMessageTemplateFallback = {
44
+ 0: messageTemplate.noItems,
45
+ 1: messageTemplate.singleItem,
46
+ 2: messageTemplate.comparison,
47
+ 3: messageTemplate.descriptiveComparison,
48
+ }
49
+
50
+ export const createLogMessage =
51
+ <TemplateName extends keyof typeof messageTemplate>({
52
+ logType,
53
+ templateName,
54
+ titleBackgroundColor,
55
+ titleTextColor,
56
+ }: {
57
+ logType: "error" | "info" | "log" | "warn"
58
+ templateName?: TemplateName
59
+ titleBackgroundColor?: BackgroundColorName
60
+ titleTextColor?: ForegroundColorName
61
+ }) =>
62
+ (
63
+ title: string,
64
+ ...content: Parameters<
65
+ (typeof messageTemplate)[TemplateName]
66
+ >
67
+ ) => {
68
+ const optionallyColoredChalk = createAddColorToChalk(
69
+ titleBackgroundColor,
70
+ )(createAddColorToChalk(titleTextColor)(new Chalk()))
71
+
72
+ // Fallback dispatch when no templateName is set: a description string +
73
+ // an array of items at positions 0 and 1 is the multipleItems shape.
74
+ // Otherwise pick a template by arity.
75
+ const message = templateName
76
+ ? messageTemplate[templateName](
77
+ // @ts-expect-error A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)
78
+ ...content,
79
+ )
80
+ : content.at(0) !== undefined &&
81
+ Array.isArray(content.at(1))
82
+ ? messageTemplate.multipleItems(
83
+ // @ts-expect-error A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)
84
+ ...content,
85
+ )
86
+ : content.length in numericalMessageTemplateFallback
87
+ ? numericalMessageTemplateFallback[
88
+ content.length
89
+ ](
90
+ // @ts-expect-error A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)
91
+ ...content,
92
+ )
93
+ : null
94
+
95
+ console[logType](
96
+ optionallyColoredChalk(`[${title}]`),
97
+ "\n",
98
+ ...(message || content),
99
+ // (
100
+ // (
101
+ // content
102
+ // .length
103
+ // )
104
+ // ? (
105
+ // content
106
+ // .slice(0, 2)
107
+ // .join("\n\n")
108
+ // )
109
+ // ),
110
+ // ...(
111
+ // (
112
+ // 2 in content
113
+ // )
114
+ // ? (
115
+ // ["\n"]
116
+ // .concat(
117
+ // content
118
+ // .slice(2)
119
+ // .join("\n")
120
+ // )
121
+ // )
122
+ // : ""
123
+ // ),
124
+ "\n",
125
+ "\n",
126
+ )
127
+ }
128
+
129
+ export const logError = createLogMessage({
130
+ logType: "error",
131
+ titleTextColor: "red",
132
+ })
133
+
134
+ export const logInfo = createLogMessage({
135
+ logType: "info",
136
+ titleTextColor: "green",
137
+ })
138
+
139
+ export const logWarning = createLogMessage({
140
+ logType: "warn",
141
+ titleBackgroundColor: "bgYellowBright",
142
+ titleTextColor: "black",
143
+ })
@@ -0,0 +1,71 @@
1
+ import { dirname } from "node:path"
2
+ import { vol } from "memfs"
3
+ import { firstValueFrom } from "rxjs"
4
+ import { beforeEach, describe, expect, test } from "vitest"
5
+
6
+ import { makeDirectory } from "./makeDirectory.js"
7
+
8
+ describe(makeDirectory.name, () => {
9
+ beforeEach(() => {
10
+ vol.fromJSON({
11
+ "/movies/Star Wars (1977)/Star Wars (1977).mkv": "",
12
+ })
13
+ })
14
+
15
+ test("creates the parent directory when caller passes dirname of a file path", async () => {
16
+ const filePath =
17
+ "/movies/Super Mario Bros (1993)/Super Mario Bros (1993).mkv"
18
+
19
+ await firstValueFrom(makeDirectory(dirname(filePath)))
20
+
21
+ await expect(
22
+ new Promise((resolve, reject) => {
23
+ vol.readdir(
24
+ "/movies",
25
+ (error: unknown, data: unknown) => {
26
+ if (error) {
27
+ reject(error)
28
+ } else {
29
+ resolve(data)
30
+ }
31
+ },
32
+ )
33
+ }),
34
+ ).resolves.toEqual([
35
+ "Star Wars (1977)",
36
+ "Super Mario Bros (1993)",
37
+ ])
38
+ })
39
+
40
+ test("creates the directory itself given a path with no file extension", async () => {
41
+ const folderPath = "/movies/Super Mario Bros (1993)"
42
+
43
+ await firstValueFrom(makeDirectory(folderPath))
44
+
45
+ await expect(
46
+ new Promise((resolve, reject) => {
47
+ vol.readdir(
48
+ "/movies",
49
+ (error: unknown, data: unknown) => {
50
+ if (error) {
51
+ reject(error)
52
+ } else {
53
+ resolve(data)
54
+ }
55
+ },
56
+ )
57
+ }),
58
+ ).resolves.toEqual([
59
+ "Star Wars (1977)",
60
+ "Super Mario Bros (1993)",
61
+ ])
62
+ })
63
+
64
+ test("emits the directory path so callers can chain off of it", async () => {
65
+ const folderPath = "/movies/Tron (1982)"
66
+
67
+ await expect(
68
+ firstValueFrom(makeDirectory(folderPath)),
69
+ ).resolves.toBe(folderPath)
70
+ })
71
+ })
@@ -0,0 +1,7 @@
1
+ import { mkdir } from "node:fs/promises"
2
+ import { defer, map } from "rxjs"
3
+
4
+ export const makeDirectory = (directoryPath: string) =>
5
+ defer(() =>
6
+ mkdir(directoryPath, { recursive: true }),
7
+ ).pipe(map(() => directoryPath))
@@ -0,0 +1,8 @@
1
+ import { createNewSortInstance } from "fast-sort"
2
+
3
+ export const naturalSort = createNewSortInstance({
4
+ comparer: new Intl.Collator(undefined, {
5
+ numeric: true,
6
+ sensitivity: "base",
7
+ }).compare,
8
+ })
@@ -0,0 +1,9 @@
1
+ import { extname } from "node:path"
2
+
3
+ export const replaceFileExtension = ({
4
+ filePath,
5
+ fileExtension,
6
+ }: {
7
+ filePath: string
8
+ fileExtension: string
9
+ }) => filePath.replace(extname(filePath), fileExtension)
@@ -0,0 +1,43 @@
1
+ import {
2
+ firstValueFrom,
3
+ type Observable,
4
+ type Observer,
5
+ type OperatorFunction,
6
+ of,
7
+ } from "rxjs"
8
+ import {
9
+ type RunHelpers,
10
+ TestScheduler,
11
+ } from "rxjs/testing"
12
+ import { expect } from "vitest"
13
+
14
+ export const getOperatorValue = <InputValue, OutputValue>(
15
+ operator: OperatorFunction<InputValue, OutputValue>,
16
+ ...inputValues: InputValue[]
17
+ ) => firstValueFrom(of(...inputValues).pipe(operator))
18
+
19
+ export const runPromiseScheduler = <ObservableValue>({
20
+ getSubscriber,
21
+ observable,
22
+ }: {
23
+ getSubscriber: (
24
+ resolve: () => void,
25
+ reject: () => void,
26
+ ) => Observer<ObservableValue>
27
+ observable: Observable<ObservableValue>
28
+ }) =>
29
+ new Promise<void>((resolve, reject) => {
30
+ observable.subscribe(getSubscriber(resolve, reject))
31
+ })
32
+
33
+ export const runTestScheduler = <ReturnValue>(
34
+ testRunner: (helpers: RunHelpers) => ReturnValue,
35
+ ) => {
36
+ const testScheduler = new TestScheduler(
37
+ (actual, expected) => {
38
+ expect(actual).toEqual(expected)
39
+ },
40
+ )
41
+
42
+ return testScheduler.run<ReturnValue>(testRunner)
43
+ }