utilful 3.0.2 → 3.2.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.
- package/README.md +157 -24
- package/dist/_virtual/_rolldown/runtime.mjs +13 -0
- package/dist/cli/args.d.mts +44 -0
- package/dist/cli/args.mjs +109 -0
- package/dist/cli/command.d.mts +33 -0
- package/dist/cli/command.mjs +76 -0
- package/dist/cli/errors.d.mts +20 -0
- package/dist/cli/errors.mjs +43 -0
- package/dist/cli/index.d.mts +5 -0
- package/dist/cli/index.mjs +5 -0
- package/dist/cli/log.d.mts +10 -0
- package/dist/cli/log.mjs +28 -0
- package/dist/cli/style.mjs +7 -0
- package/dist/cli/testing.d.mts +29 -0
- package/dist/cli/testing.mjs +120 -0
- package/dist/cli/usage.mjs +65 -0
- package/dist/csv.d.mts +2 -2
- package/dist/csv.mjs +3 -3
- package/dist/emitter.d.mts +3 -3
- package/dist/emitter.mjs +0 -6
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/object.d.mts +7 -2
- package/dist/object.mjs +6 -1
- package/dist/path.d.mts +20 -3
- package/dist/path.mjs +73 -13
- package/dist/result.d.mts +2 -4
- package/dist/result.mjs +7 -5
- package/dist/types.d.mts +1 -3
- package/package.json +27 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { error } from "./log.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
//#region src/cli/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* A condition the CLI recognized and phrased for a human. Anything else
|
|
6
|
+
* reaching the boundary is a defect in the tool and prints its stack unasked.
|
|
7
|
+
*/
|
|
8
|
+
var CliError = class extends Error {};
|
|
9
|
+
/** Gets usage printed alongside its message. */
|
|
10
|
+
var ArgumentError = class extends CliError {};
|
|
11
|
+
/** Reports a failure the way the boundary does, for one that outlives `run`, such as a watch rebuild. */
|
|
12
|
+
function reportFailure(error$1, { verbose = false, expectedErrors = [], describe } = {}) {
|
|
13
|
+
const sections = [error$1 instanceof Error ? describe?.(error$1) ?? error$1.message : String(error$1)];
|
|
14
|
+
if (verbose || !isExpected(error$1, expectedErrors)) {
|
|
15
|
+
const causeChain = formatCauseChain(error$1);
|
|
16
|
+
if (causeChain) sections.push(causeChain);
|
|
17
|
+
if (error$1 instanceof Error && error$1.stack) sections.push(error$1.stack);
|
|
18
|
+
}
|
|
19
|
+
error(sections.join("\n\n"));
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reports whether the CLI raised this error deliberately rather than tripping
|
|
24
|
+
* over it. A system error such as `ENOENT` reaches the boundary as the honest
|
|
25
|
+
* answer to what the user asked for, so it reads as deliberate too, while an
|
|
26
|
+
* `ERR_*` error from a Node API is a wrong call and stays a defect.
|
|
27
|
+
*/
|
|
28
|
+
function isExpected(error, expectedErrors) {
|
|
29
|
+
if (error instanceof CliError) return true;
|
|
30
|
+
if (expectedErrors.some((expectedError) => error instanceof expectedError)) return true;
|
|
31
|
+
return error instanceof Error && /^E[A-Z0-9]+$/.test(String(error.code));
|
|
32
|
+
}
|
|
33
|
+
function formatCauseChain(error) {
|
|
34
|
+
const causeLines = [];
|
|
35
|
+
let current = error instanceof Error ? error.cause : void 0;
|
|
36
|
+
while (current instanceof Error) {
|
|
37
|
+
causeLines.push(`Caused by: ${current.name || "Error"}: ${current.message}`);
|
|
38
|
+
current = current.cause;
|
|
39
|
+
}
|
|
40
|
+
return causeLines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ArgumentError, CliError, reportFailure };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ArgDef, ArgsDef, BooleanArgDef, CommonArgs, ParsedArgs, PositionalArgDef, StringArgDef, commonArgs, parseArgs } from "./args.mjs";
|
|
2
|
+
import { ArgumentError, CliError, ErrorClass, ReportOptions, reportFailure } from "./errors.mjs";
|
|
3
|
+
import { CommandContext, CommandDef, CommandMeta, RunMainOptions, defineCommand, runCommand, runMain } from "./command.mjs";
|
|
4
|
+
import { log_d_exports } from "./log.mjs";
|
|
5
|
+
export { type ArgDef, type ArgsDef, ArgumentError, type BooleanArgDef, CliError, type CommandContext, type CommandDef, type CommandMeta, type CommonArgs, type ErrorClass, type ParsedArgs, type PositionalArgDef, type ReportOptions, type RunMainOptions, type StringArgDef, commonArgs, defineCommand, log_d_exports as log, parseArgs, reportFailure, runCommand, runMain };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { log_exports } from "./log.mjs";
|
|
2
|
+
import { ArgumentError, CliError, reportFailure } from "./errors.mjs";
|
|
3
|
+
import { commonArgs, parseArgs } from "./args.mjs";
|
|
4
|
+
import { defineCommand, runCommand, runMain } from "./command.mjs";
|
|
5
|
+
export { ArgumentError, CliError, commonArgs, defineCommand, log_exports as log, parseArgs, reportFailure, runCommand, runMain };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare namespace log_d_exports {
|
|
2
|
+
export { blankLine, error, info, success, warn };
|
|
3
|
+
}
|
|
4
|
+
declare function error(message: string): void;
|
|
5
|
+
declare function warn(message: string): void;
|
|
6
|
+
declare function info(message: string): void;
|
|
7
|
+
declare function success(message: string): void;
|
|
8
|
+
declare function blankLine(): void;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { blankLine, error, info, log_d_exports, success, warn };
|
package/dist/cli/log.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { __exportAll } from "../_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { paint } from "./style.mjs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
//#region src/cli/log.ts
|
|
5
|
+
var log_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
blankLine: () => blankLine,
|
|
7
|
+
error: () => error,
|
|
8
|
+
info: () => info,
|
|
9
|
+
success: () => success,
|
|
10
|
+
warn: () => warn
|
|
11
|
+
});
|
|
12
|
+
function error(message) {
|
|
13
|
+
console.error(`${paint("red", "✖", process.stderr)} ${message}`);
|
|
14
|
+
}
|
|
15
|
+
function warn(message) {
|
|
16
|
+
console.error(`${paint("yellow", "⚠", process.stderr)} ${message}`);
|
|
17
|
+
}
|
|
18
|
+
function info(message) {
|
|
19
|
+
console.error(`${paint("cyan", "●", process.stderr)} ${message}`);
|
|
20
|
+
}
|
|
21
|
+
function success(message) {
|
|
22
|
+
console.error(`${paint("green", "✔", process.stderr)} ${message}`);
|
|
23
|
+
}
|
|
24
|
+
function blankLine() {
|
|
25
|
+
console.error("");
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { blankLine, error, info, log_exports, success, warn };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ArgsDef } from "./args.mjs";
|
|
2
|
+
import { CommandDef, RunMainOptions } from "./command.mjs";
|
|
3
|
+
//#region src/cli/testing.d.ts
|
|
4
|
+
interface FileMap {
|
|
5
|
+
[relativePath: string]: string;
|
|
6
|
+
}
|
|
7
|
+
interface CliResult {
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
exitCode: number;
|
|
11
|
+
}
|
|
12
|
+
interface RunOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}
|
|
15
|
+
interface CliHarnessOptions extends Omit<RunMainOptions, "argv"> {
|
|
16
|
+
/** Only `runCliProcess` needs it. */
|
|
17
|
+
entry?: string;
|
|
18
|
+
}
|
|
19
|
+
interface CliHarness {
|
|
20
|
+
runCli: (argv: readonly string[], options?: RunOptions) => Promise<CliResult>;
|
|
21
|
+
/** Runs the entry file as a child process, where the exit code is real and stdout can be cut short by exiting. */
|
|
22
|
+
runCliProcess: (argv: readonly string[], options?: RunOptions) => Promise<CliResult>;
|
|
23
|
+
}
|
|
24
|
+
declare function createCliHarness<T extends ArgsDef>(command: CommandDef<T>, { entry, ...runOptions }?: CliHarnessOptions): CliHarness;
|
|
25
|
+
/** Registers an `afterEach` cleanup, so call it at the top level of a test file. */
|
|
26
|
+
declare function useTemporaryDirectories(prefix?: string): (files?: FileMap) => string;
|
|
27
|
+
declare function mockStdin(input: string): () => void;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { CliHarness, CliHarnessOptions, CliResult, FileMap, RunOptions, createCliHarness, mockStdin, useTemporaryDirectories };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { runMain } from "./command.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
import { afterEach, vi } from "vitest";
|
|
9
|
+
//#region src/cli/testing.ts
|
|
10
|
+
var ProcessExitError = class extends Error {
|
|
11
|
+
constructor(exitCode) {
|
|
12
|
+
super(`process.exit(${exitCode})`);
|
|
13
|
+
this.exitCode = exitCode;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function createCliHarness(command, { entry, ...runOptions } = {}) {
|
|
17
|
+
return {
|
|
18
|
+
async runCli(argv, options = {}) {
|
|
19
|
+
const stdout = [];
|
|
20
|
+
const stderr = [];
|
|
21
|
+
const previousExitCode = process.exitCode;
|
|
22
|
+
const previousCwd = process.cwd();
|
|
23
|
+
process.exitCode = void 0;
|
|
24
|
+
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
|
|
25
|
+
stdout.push(String(chunk));
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
|
29
|
+
stderr.push(String(chunk));
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation((...parts) => {
|
|
33
|
+
stdout.push(`${parts.map(String).join(" ")}\n`);
|
|
34
|
+
});
|
|
35
|
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation((...parts) => {
|
|
36
|
+
stderr.push(`${parts.map(String).join(" ")}\n`);
|
|
37
|
+
});
|
|
38
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((code) => {
|
|
39
|
+
throw new ProcessExitError(typeof code === "number" ? code : 0);
|
|
40
|
+
});
|
|
41
|
+
let exitCode;
|
|
42
|
+
try {
|
|
43
|
+
if (options.cwd !== void 0) process.chdir(options.cwd);
|
|
44
|
+
await runMain(command, {
|
|
45
|
+
...runOptions,
|
|
46
|
+
argv
|
|
47
|
+
});
|
|
48
|
+
exitCode = process.exitCode ?? 0;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!(error instanceof ProcessExitError)) throw error;
|
|
51
|
+
exitCode = error.exitCode;
|
|
52
|
+
} finally {
|
|
53
|
+
process.chdir(previousCwd);
|
|
54
|
+
process.exitCode = previousExitCode;
|
|
55
|
+
exitSpy.mockRestore();
|
|
56
|
+
consoleErrorSpy.mockRestore();
|
|
57
|
+
consoleLogSpy.mockRestore();
|
|
58
|
+
stderrSpy.mockRestore();
|
|
59
|
+
stdoutSpy.mockRestore();
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
stdout: stdout.join(""),
|
|
63
|
+
stderr: stderr.join(""),
|
|
64
|
+
exitCode
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
runCliProcess(argv, options = {}) {
|
|
68
|
+
if (entry === void 0) return Promise.reject(/* @__PURE__ */ new Error("runCliProcess needs the entry file of the CLI"));
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
execFile(process.execPath, [entry, ...argv], {
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
maxBuffer: 64 * 1024 * 1024
|
|
73
|
+
}, (spawnError, stdout, stderr) => {
|
|
74
|
+
if (spawnError && typeof spawnError.code !== "number") reject(spawnError);
|
|
75
|
+
else resolve({
|
|
76
|
+
stdout,
|
|
77
|
+
stderr,
|
|
78
|
+
exitCode: spawnError ? spawnError.code : 0
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Registers an `afterEach` cleanup, so call it at the top level of a test file. */
|
|
86
|
+
function useTemporaryDirectories(prefix = "cli-test-") {
|
|
87
|
+
const directories = [];
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
while (directories.length > 0) rmSync(directories.pop(), {
|
|
90
|
+
recursive: true,
|
|
91
|
+
force: true
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
return (files = {}) => {
|
|
95
|
+
const directory = mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
96
|
+
directories.push(directory);
|
|
97
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
98
|
+
const filePath = path.join(directory, relativePath);
|
|
99
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
100
|
+
writeFileSync(filePath, contents, "utf-8");
|
|
101
|
+
}
|
|
102
|
+
return directory;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function mockStdin(input) {
|
|
106
|
+
const stream = Readable.from([new TextEncoder().encode(input)]);
|
|
107
|
+
const originalStdin = process.stdin;
|
|
108
|
+
Object.defineProperty(process, "stdin", {
|
|
109
|
+
value: stream,
|
|
110
|
+
writable: true
|
|
111
|
+
});
|
|
112
|
+
return () => {
|
|
113
|
+
Object.defineProperty(process, "stdin", {
|
|
114
|
+
value: originalStdin,
|
|
115
|
+
writable: true
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { createCliHarness, mockStdin, useTemporaryDirectories };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { paint } from "./style.mjs";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
//#region src/cli/usage.ts
|
|
5
|
+
function renderUsage(command, { parent, stream = process.stdout } = {}) {
|
|
6
|
+
const color = (style, text) => paint(style, text, stream);
|
|
7
|
+
const heading = (title) => color(["bold", "underline"], title);
|
|
8
|
+
const meta = command.meta ?? {};
|
|
9
|
+
const parentMeta = parent?.meta ?? {};
|
|
10
|
+
const commandName = [parentMeta.name, meta.name].filter((name) => name !== void 0).join(" ");
|
|
11
|
+
const version = meta.version ?? parentMeta.version;
|
|
12
|
+
const positionalLines = [];
|
|
13
|
+
const optionLines = [];
|
|
14
|
+
const commandLines = [];
|
|
15
|
+
const usageLine = [];
|
|
16
|
+
for (const [name, definition] of Object.entries(command.args ?? {})) {
|
|
17
|
+
const isRequired = definition.type !== "boolean" && definition.required === true;
|
|
18
|
+
const hasDefault = definition.type !== "positional" && definition.default !== void 0 && definition.default !== false;
|
|
19
|
+
const hints = [
|
|
20
|
+
definition.description,
|
|
21
|
+
isRequired ? color("gray", "(Required)") : void 0,
|
|
22
|
+
hasDefault ? color("gray", `(Default: ${definition.default})`) : void 0
|
|
23
|
+
].filter((hint) => hint !== void 0).join(" ");
|
|
24
|
+
if (definition.type === "positional") {
|
|
25
|
+
const label = name.toUpperCase();
|
|
26
|
+
positionalLines.push([color("cyan", label), hints]);
|
|
27
|
+
usageLine.push(isRequired ? `<${label}>` : `[${label}]`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const spellings = [definition.alias === void 0 ? void 0 : `-${definition.alias}`, `--${name}`].filter((spelling) => spelling !== void 0).join(", ");
|
|
31
|
+
const value = definition.type === "string" ? `<${definition.valueHint ?? name}>` : void 0;
|
|
32
|
+
optionLines.push([color("cyan", value === void 0 ? spellings : `${spellings}=${value}`), hints]);
|
|
33
|
+
if (definition.type === "boolean" && definition.default === true) optionLines.push([color("cyan", `--no-${name}`), ""]);
|
|
34
|
+
if (isRequired) usageLine.push(`--${name}=${value}`);
|
|
35
|
+
}
|
|
36
|
+
for (const [name, subCommand] of Object.entries(command.subCommands ?? {})) commandLines.push([color("cyan", name), subCommand.meta?.description ?? ""]);
|
|
37
|
+
const hasArguments = positionalLines.length > 0 || optionLines.length > 0;
|
|
38
|
+
if (commandLines.length > 0 && !hasArguments) usageLine.push(Object.keys(command.subCommands).join("|"));
|
|
39
|
+
const lines = [
|
|
40
|
+
color("gray", `${meta.description ?? ""} (${commandName}${version === void 0 ? "" : ` v${version}`})`),
|
|
41
|
+
"",
|
|
42
|
+
`${heading("USAGE")} ${color("cyan", [
|
|
43
|
+
commandName,
|
|
44
|
+
hasArguments ? "[OPTIONS]" : void 0,
|
|
45
|
+
...usageLine
|
|
46
|
+
].filter((part) => part !== void 0).join(" "))}`,
|
|
47
|
+
""
|
|
48
|
+
];
|
|
49
|
+
for (const [title, rows] of [
|
|
50
|
+
["ARGUMENTS", positionalLines],
|
|
51
|
+
["OPTIONS", optionLines],
|
|
52
|
+
["COMMANDS", commandLines]
|
|
53
|
+
]) if (rows.length > 0) lines.push(heading(title), "", formatColumns(rows), "");
|
|
54
|
+
if (commandLines.length > 0) lines.push(`Use ${color("cyan", `${commandName} <command> --help`)} for more information about a command.`);
|
|
55
|
+
return lines.join("\n").trimEnd();
|
|
56
|
+
}
|
|
57
|
+
function formatColumns(rows) {
|
|
58
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => visibleLength(row[column]))));
|
|
59
|
+
return rows.map((row) => row.map((cell, column) => ` ${cell}${" ".repeat(widths[column] - visibleLength(cell))}`).join("").trimEnd()).join("\n");
|
|
60
|
+
}
|
|
61
|
+
function visibleLength(text) {
|
|
62
|
+
return stripVTControlCharacters(text).length;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { renderUsage };
|
package/dist/csv.d.mts
CHANGED
|
@@ -106,8 +106,8 @@ declare function createCSVAsync<T extends Record<string, unknown>>(data: AsyncIt
|
|
|
106
106
|
* Within quoted values, double quotes are escaped by doubling them.
|
|
107
107
|
*
|
|
108
108
|
* @example
|
|
109
|
-
* escapeCSVValue('hello, world') // "hello, world"
|
|
110
|
-
* escapeCSVValue('contains "quotes"') // "contains ""quotes"""
|
|
109
|
+
* escapeCSVValue('hello, world') // '"hello, world"'
|
|
110
|
+
* escapeCSVValue('contains "quotes"') // '"contains ""quotes"""'
|
|
111
111
|
*/
|
|
112
112
|
declare function escapeCSVValue(value: unknown, options?: {
|
|
113
113
|
/** @default ',' */
|
package/dist/csv.mjs
CHANGED
|
@@ -17,9 +17,9 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
|
17
17
|
columns = inferColumns(data);
|
|
18
18
|
options = columnsOrOptions ?? {};
|
|
19
19
|
}
|
|
20
|
-
if (columns.length === 0 && data.length === 0) return "";
|
|
21
20
|
const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
|
|
22
21
|
assertValidCSVDelimiter(delimiter);
|
|
22
|
+
if (columns.length === 0) return "";
|
|
23
23
|
if (addHeader) {
|
|
24
24
|
const header = encodeCSVHeader(columns.map(String), delimiter, quoteAll);
|
|
25
25
|
if (data.length === 0) return header;
|
|
@@ -97,8 +97,8 @@ function encodeCSVRow(row, columns, delimiter, quoteAll) {
|
|
|
97
97
|
* Within quoted values, double quotes are escaped by doubling them.
|
|
98
98
|
*
|
|
99
99
|
* @example
|
|
100
|
-
* escapeCSVValue('hello, world') // "hello, world"
|
|
101
|
-
* escapeCSVValue('contains "quotes"') // "contains ""quotes"""
|
|
100
|
+
* escapeCSVValue('hello, world') // '"hello, world"'
|
|
101
|
+
* escapeCSVValue('contains "quotes"') // '"contains ""quotes"""'
|
|
102
102
|
*/
|
|
103
103
|
function escapeCSVValue(value, options = {}) {
|
|
104
104
|
const { delimiter = COMMA, quoteAll = false } = options;
|
package/dist/emitter.d.mts
CHANGED
|
@@ -4,8 +4,8 @@ type Handler<T = unknown> = (event: T) => void;
|
|
|
4
4
|
type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
|
|
5
5
|
type EventHandlerList<T = unknown> = Handler<T>[];
|
|
6
6
|
type WildCardEventHandlerList<T = Record<string, unknown>> = WildcardHandler<T>[];
|
|
7
|
-
type EventHandlerMap<Events extends Record<EventType,
|
|
8
|
-
interface Emitter<Events extends Record<EventType,
|
|
7
|
+
type EventHandlerMap<Events extends Record<EventType, any>> = Map<keyof Events | "*", EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>>;
|
|
8
|
+
interface Emitter<Events extends Record<EventType, any>> {
|
|
9
9
|
events: EventHandlerMap<Events>;
|
|
10
10
|
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
|
|
11
11
|
on(type: "*", handler: WildcardHandler<Events>): void;
|
|
@@ -20,6 +20,6 @@ interface Emitter<Events extends Record<EventType, unknown>> {
|
|
|
20
20
|
* @remarks Ported from `mitt`.
|
|
21
21
|
* @see https://github.com/developit/mitt
|
|
22
22
|
*/
|
|
23
|
-
declare function createEmitter<Events extends Record<EventType,
|
|
23
|
+
declare function createEmitter<Events extends Record<EventType, any>>(events?: EventHandlerMap<Events>): Emitter<Events>;
|
|
24
24
|
//#endregion
|
|
25
25
|
export { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter };
|
package/dist/emitter.mjs
CHANGED
|
@@ -14,8 +14,6 @@ function createEmitter(events) {
|
|
|
14
14
|
events,
|
|
15
15
|
/**
|
|
16
16
|
* Registers an event handler for the given type.
|
|
17
|
-
*
|
|
18
|
-
* @memberOf createEmitter
|
|
19
17
|
*/
|
|
20
18
|
on(type, handler) {
|
|
21
19
|
const handlers = events.get(type);
|
|
@@ -27,8 +25,6 @@ function createEmitter(events) {
|
|
|
27
25
|
*
|
|
28
26
|
* @remarks
|
|
29
27
|
* If `handler` is omitted, all handlers of the given type are removed.
|
|
30
|
-
*
|
|
31
|
-
* @memberOf createEmitter
|
|
32
28
|
*/
|
|
33
29
|
off(type, handler) {
|
|
34
30
|
const handlers = events.get(type);
|
|
@@ -41,8 +37,6 @@ function createEmitter(events) {
|
|
|
41
37
|
* @remarks
|
|
42
38
|
* If present, `'*'` handlers are invoked after type-matched handlers.
|
|
43
39
|
* Manually firing `'*'` handlers is not supported.
|
|
44
|
-
*
|
|
45
|
-
* @memberOf createEmitter
|
|
46
40
|
*/
|
|
47
41
|
emit(type, evt) {
|
|
48
42
|
let handlers = events.get(type);
|
package/dist/index.d.mts
CHANGED
|
@@ -5,8 +5,8 @@ import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCar
|
|
|
5
5
|
import { tryParseJSON } from "./json.mjs";
|
|
6
6
|
import { interopDefault } from "./module.mjs";
|
|
7
7
|
import { deepApply, isObject, memoize, objectEntries, objectKeys } from "./object.mjs";
|
|
8
|
-
import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
8
|
+
import { ParsedQuery, QueryObject, QueryValue, getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
9
9
|
import { Err, ErrData, Ok, OkData, Result, ResultData, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { TEMPLATE_PLACEHOLDER_RE, generateRandomId, template } from "./string.mjs";
|
|
11
|
-
import {
|
|
12
|
-
export {
|
|
11
|
+
import { BrandedType, UnifyIntersection } from "./types.mjs";
|
|
12
|
+
export { BrandedType, CSVCreateOptions, CSVParseOptions, CSVRow, Defu, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, MaybeArray, Ok, OkData, ParsedQuery, QueryObject, QueryValue, Result, ResultData, TEMPLATE_PLACEHOLDER_RE, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, getQuery, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { createEmitter } from "./emitter.mjs";
|
|
|
5
5
|
import { tryParseJSON } from "./json.mjs";
|
|
6
6
|
import { interopDefault } from "./module.mjs";
|
|
7
7
|
import { deepApply, isObject, memoize, objectEntries, objectKeys } from "./object.mjs";
|
|
8
|
-
import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
8
|
+
import { getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
9
9
|
import { Err, Ok, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { TEMPLATE_PLACEHOLDER_RE, generateRandomId, template } from "./string.mjs";
|
|
11
|
-
export { Err, Ok, TEMPLATE_PLACEHOLDER_RE, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
11
|
+
export { Err, Ok, TEMPLATE_PLACEHOLDER_RE, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, getQuery, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/object.d.mts
CHANGED
|
@@ -23,9 +23,14 @@ declare function objectKeys<T extends Record<any, any>>(obj: T): Array<`${Extrac
|
|
|
23
23
|
*/
|
|
24
24
|
declare function objectEntries<T extends Record<any, any>>(obj: T): Array<[keyof T, T[keyof T]]>;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Applies a callback to every key-value pair of the given object, and to every pair
|
|
27
|
+
* inside nested objects and arrays (including arrays nested inside arrays).
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* The callback also fires for nested objects, so `item` is whichever object the pair
|
|
31
|
+
* belongs to rather than the one that was passed in.
|
|
27
32
|
*/
|
|
28
|
-
declare function deepApply<T extends Record<any, any>>(data: T, callback: (item:
|
|
33
|
+
declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: Record<string, any>, key: string, value: any) => void): void;
|
|
29
34
|
/**
|
|
30
35
|
* Checks if a value is an object with the plain `[object Object]` tag.
|
|
31
36
|
*
|
package/dist/object.mjs
CHANGED
|
@@ -31,7 +31,12 @@ function objectEntries(obj) {
|
|
|
31
31
|
return Object.entries(obj);
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Applies a callback to every key-value pair of the given object, and to every pair
|
|
35
|
+
* inside nested objects and arrays (including arrays nested inside arrays).
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* The callback also fires for nested objects, so `item` is whichever object the pair
|
|
39
|
+
* belongs to rather than the one that was passed in.
|
|
35
40
|
*/
|
|
36
41
|
function deepApply(data, callback) {
|
|
37
42
|
for (const [key, value] of Object.entries(data)) {
|
package/dist/path.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//#region src/path.d.ts
|
|
2
2
|
type QueryValue = string | number | boolean | QueryValue[] | Record<string, any> | null | undefined;
|
|
3
3
|
type QueryObject = Record<string, QueryValue | QueryValue[]>;
|
|
4
|
+
/** Query parameters as read back from a URL, where a repeated key holds all its values. */
|
|
5
|
+
type ParsedQuery = Record<string, string | string[]>;
|
|
4
6
|
/**
|
|
5
7
|
* Removes the leading slash from the given path if it has one.
|
|
6
8
|
*/
|
|
@@ -25,6 +27,11 @@ declare function withTrailingSlash(path?: string): string;
|
|
|
25
27
|
declare function joinURL(...paths: (string | undefined)[]): string;
|
|
26
28
|
/**
|
|
27
29
|
* Adds the base path to the input path, if it is not already present.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* An absolute URL is returned as it is, since a base path cannot prefix one –
|
|
33
|
+
* whether it carries a scheme (`https://example.com/foo`) or is protocol-relative
|
|
34
|
+
* (`//example.com/foo`).
|
|
28
35
|
*/
|
|
29
36
|
declare function withBase(input?: string, base?: string): string;
|
|
30
37
|
/**
|
|
@@ -35,13 +42,23 @@ declare function withoutBase(input?: string, base?: string): string;
|
|
|
35
42
|
* Returns the pathname of the given path, which is the path without the query string or hash.
|
|
36
43
|
*
|
|
37
44
|
* @remarks
|
|
38
|
-
* Absolute URLs
|
|
39
|
-
*
|
|
45
|
+
* Absolute URLs return the segment after the authority, whether they carry a
|
|
46
|
+
* scheme (`https://example.com/foo`) or are protocol-relative (`//example.com/foo`).
|
|
47
|
+
* The result is sliced out verbatim, never normalized, so percent-encoding and
|
|
48
|
+
* `..` segments survive as written.
|
|
40
49
|
*/
|
|
41
50
|
declare function getPathname(path?: string): string;
|
|
42
51
|
/**
|
|
43
52
|
* Returns the URL with the given query parameters. If a query parameter is `undefined`, it is omitted.
|
|
44
53
|
*/
|
|
45
54
|
declare function withQuery(input: string, query?: QueryObject): string;
|
|
55
|
+
/**
|
|
56
|
+
* Reads the query parameters of the given URL, ignoring the fragment.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* A parameter that appears more than once becomes an array of its values,
|
|
60
|
+
* in the order they appear. Values are percent-decoded.
|
|
61
|
+
*/
|
|
62
|
+
declare function getQuery(input: string): ParsedQuery;
|
|
46
63
|
//#endregion
|
|
47
|
-
export { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
64
|
+
export { ParsedQuery, QueryObject, QueryValue, getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/path.mjs
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
//#region src/path.ts
|
|
2
|
+
const URL_SCHEME_RE = /^[a-z][\w+.-]*:\/\//i;
|
|
3
|
+
/**
|
|
4
|
+
* Returns the index at which the authority of an absolute URL starts, or `-1`
|
|
5
|
+
* for a path that carries none. A protocol-relative input such as
|
|
6
|
+
* `//example.com/foo` counts as absolute: its authority follows the leading `//`.
|
|
7
|
+
*/
|
|
8
|
+
function getAuthorityStart(path) {
|
|
9
|
+
if (path.startsWith("//")) return 2;
|
|
10
|
+
return URL_SCHEME_RE.test(path) ? path.indexOf("://") + 3 : -1;
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* Removes the leading slash from the given path if it has one.
|
|
4
14
|
*/
|
|
@@ -66,9 +76,14 @@ function joinURL(...paths) {
|
|
|
66
76
|
}
|
|
67
77
|
/**
|
|
68
78
|
* Adds the base path to the input path, if it is not already present.
|
|
79
|
+
*
|
|
80
|
+
* @remarks
|
|
81
|
+
* An absolute URL is returned as it is, since a base path cannot prefix one –
|
|
82
|
+
* whether it carries a scheme (`https://example.com/foo`) or is protocol-relative
|
|
83
|
+
* (`//example.com/foo`).
|
|
69
84
|
*/
|
|
70
85
|
function withBase(input = "", base = "") {
|
|
71
|
-
if (!base || base === "/") return input;
|
|
86
|
+
if (!base || base === "/" || getAuthorityStart(input) !== -1) return input;
|
|
72
87
|
const _base = withoutTrailingSlash(base);
|
|
73
88
|
if (input.startsWith(_base) && (input.length === _base.length || input[_base.length] === "/" || input[_base.length] === "?" || input[_base.length] === "#")) return input;
|
|
74
89
|
return joinURL(_base, input);
|
|
@@ -83,32 +98,47 @@ function withoutBase(input = "", base = "") {
|
|
|
83
98
|
const trimmed = input.slice(_base.length);
|
|
84
99
|
return trimmed[0] === "/" ? trimmed : `/${trimmed}`;
|
|
85
100
|
}
|
|
86
|
-
const URL_SCHEME_RE = /^[a-z][\w+.-]*:\/\//i;
|
|
87
101
|
/**
|
|
88
102
|
* Returns the pathname of the given path, which is the path without the query string or hash.
|
|
89
103
|
*
|
|
90
104
|
* @remarks
|
|
91
|
-
* Absolute URLs
|
|
92
|
-
*
|
|
105
|
+
* Absolute URLs return the segment after the authority, whether they carry a
|
|
106
|
+
* scheme (`https://example.com/foo`) or are protocol-relative (`//example.com/foo`).
|
|
107
|
+
* The result is sliced out verbatim, never normalized, so percent-encoding and
|
|
108
|
+
* `..` segments survive as written.
|
|
93
109
|
*/
|
|
94
110
|
function getPathname(path = "/") {
|
|
95
|
-
|
|
111
|
+
let pathStart = 0;
|
|
112
|
+
const authorityStart = getAuthorityStart(path);
|
|
113
|
+
if (authorityStart !== -1) {
|
|
114
|
+
let authorityEnd = path.length;
|
|
115
|
+
for (let i = authorityStart; i < path.length; i++) {
|
|
116
|
+
const character = path[i];
|
|
117
|
+
if (character === "/" || character === "?" || character === "#") {
|
|
118
|
+
authorityEnd = i;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (path[authorityEnd] !== "/") return "/";
|
|
123
|
+
pathStart = authorityEnd;
|
|
124
|
+
}
|
|
96
125
|
let pathEnd = path.length;
|
|
97
|
-
const queryIndex = path.indexOf("?");
|
|
98
|
-
const hashIndex = path.indexOf("#");
|
|
126
|
+
const queryIndex = path.indexOf("?", pathStart);
|
|
127
|
+
const hashIndex = path.indexOf("#", pathStart);
|
|
99
128
|
if (queryIndex !== -1) pathEnd = queryIndex;
|
|
100
129
|
if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
|
|
101
|
-
return path.slice(
|
|
130
|
+
return path.slice(pathStart, pathEnd) || "/";
|
|
102
131
|
}
|
|
103
132
|
/**
|
|
104
133
|
* Returns the URL with the given query parameters. If a query parameter is `undefined`, it is omitted.
|
|
105
134
|
*/
|
|
106
135
|
function withQuery(input, query) {
|
|
107
136
|
if (!query || Object.keys(query).length === 0) return input;
|
|
108
|
-
const
|
|
137
|
+
const { beforeHash, hash } = splitFragment(input);
|
|
138
|
+
const searchIndex = beforeHash.indexOf("?");
|
|
109
139
|
const hasExistingParams = searchIndex !== -1;
|
|
110
|
-
const base = hasExistingParams ?
|
|
111
|
-
const searchParams = new URLSearchParams(hasExistingParams ?
|
|
140
|
+
const base = hasExistingParams ? beforeHash.slice(0, searchIndex) : beforeHash;
|
|
141
|
+
const searchParams = new URLSearchParams(hasExistingParams ? beforeHash.slice(searchIndex + 1) : void 0);
|
|
112
142
|
for (const [key, value] of Object.entries(query)) {
|
|
113
143
|
if (value === void 0) {
|
|
114
144
|
searchParams.delete(key);
|
|
@@ -120,7 +150,37 @@ function withQuery(input, query) {
|
|
|
120
150
|
} else searchParams.set(key, normalizeQueryValue(value));
|
|
121
151
|
}
|
|
122
152
|
const queryString = searchParams.toString();
|
|
123
|
-
return queryString ? `${base}?${queryString}` : base;
|
|
153
|
+
return queryString ? `${base}?${queryString}${hash}` : base + hash;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reads the query parameters of the given URL, ignoring the fragment.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* A parameter that appears more than once becomes an array of its values,
|
|
160
|
+
* in the order they appear. Values are percent-decoded.
|
|
161
|
+
*/
|
|
162
|
+
function getQuery(input) {
|
|
163
|
+
const { beforeHash } = splitFragment(input);
|
|
164
|
+
const searchIndex = beforeHash.indexOf("?");
|
|
165
|
+
if (searchIndex === -1) return {};
|
|
166
|
+
const query = Object.create(null);
|
|
167
|
+
for (const [key, value] of new URLSearchParams(beforeHash.slice(searchIndex + 1))) {
|
|
168
|
+
const existingValue = query[key];
|
|
169
|
+
if (existingValue === void 0) query[key] = value;
|
|
170
|
+
else if (Array.isArray(existingValue)) existingValue.push(value);
|
|
171
|
+
else query[key] = [existingValue, value];
|
|
172
|
+
}
|
|
173
|
+
return { ...query };
|
|
174
|
+
}
|
|
175
|
+
function splitFragment(input) {
|
|
176
|
+
const hashIndex = input.indexOf("#");
|
|
177
|
+
return hashIndex === -1 ? {
|
|
178
|
+
beforeHash: input,
|
|
179
|
+
hash: ""
|
|
180
|
+
} : {
|
|
181
|
+
beforeHash: input.slice(0, hashIndex),
|
|
182
|
+
hash: input.slice(hashIndex)
|
|
183
|
+
};
|
|
124
184
|
}
|
|
125
185
|
function normalizeQueryValue(value) {
|
|
126
186
|
if (value === null) return "";
|
|
@@ -129,4 +189,4 @@ function normalizeQueryValue(value) {
|
|
|
129
189
|
return String(value);
|
|
130
190
|
}
|
|
131
191
|
//#endregion
|
|
132
|
-
export { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
192
|
+
export { getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|