Improved the dev CLI output
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Vastly improved dev command output
|
||||
@@ -113,6 +113,7 @@
|
||||
"node-fetch": "^3.3.0",
|
||||
"npm-check-updates": "^16.12.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"p-debounce": "^4.0.0",
|
||||
"p-throttle": "^6.1.0",
|
||||
"partysocket": "^0.0.17",
|
||||
"proxy-agent": "^6.3.0",
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
detectDependencyVersion,
|
||||
serverWebsocketMessages,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import chalk from "chalk";
|
||||
import { watch } from "chokidar";
|
||||
import { Command } from "commander";
|
||||
import { BuildContext, Metafile, context } from "esbuild";
|
||||
@@ -18,7 +17,7 @@ import { createHash } from "node:crypto";
|
||||
import fs, { readFileSync } from "node:fs";
|
||||
import { ClientRequestArgs } from "node:http";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import pThrottle from "p-throttle";
|
||||
import pDebounce from "p-debounce";
|
||||
import { WebSocket } from "partysocket";
|
||||
import React, { Suspense, useEffect } from "react";
|
||||
import { ClientOptions, WebSocket as wsWebSocket } from "ws";
|
||||
@@ -27,10 +26,10 @@ import * as packageJson from "../../package.json";
|
||||
import { CliApiClient } from "../apiClient";
|
||||
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
|
||||
import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../utilities/build";
|
||||
import { chalkPurple } from "../utilities/colors";
|
||||
import { chalkGrey, chalkPurple, chalkWorker } from "../utilities/cliOutput";
|
||||
import { readConfig } from "../utilities/configFiles";
|
||||
import { readJSONFile } from "../utilities/fileSystem";
|
||||
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
|
||||
import { printDevBanner, printStandloneInitialBanner } from "../utilities/initialBanner.js";
|
||||
import {
|
||||
detectPackageNameFromImportPath,
|
||||
parsePackageName,
|
||||
@@ -104,6 +103,7 @@ async function startDev(
|
||||
}
|
||||
|
||||
await printStandloneInitialBanner(true);
|
||||
printDevBanner();
|
||||
|
||||
logger.debug("Starting dev session", { dir, options, authorization });
|
||||
|
||||
@@ -339,7 +339,7 @@ function useDev({
|
||||
|
||||
let firstBuild = true;
|
||||
|
||||
logger.log(chalk.dim("⎔ Building background worker..."));
|
||||
logger.log(chalkGrey("○ Building background worker…"));
|
||||
|
||||
ctx = await context({
|
||||
stdin: {
|
||||
@@ -375,7 +375,7 @@ function useDev({
|
||||
}
|
||||
|
||||
if (!firstBuild) {
|
||||
logger.log(chalk.dim("⎔ Rebuilding background worker..."));
|
||||
logger.log(chalkGrey("○ Building background worker…"));
|
||||
}
|
||||
|
||||
const metaOutputKey = join("out", `stdin.js`);
|
||||
@@ -406,9 +406,8 @@ function useDev({
|
||||
const contentHash = md5Hasher.digest("hex");
|
||||
|
||||
if (latestWorkerContentHash === contentHash) {
|
||||
logger.log(chalk.dim("⎔ No changes detected, skipping build..."));
|
||||
logger.log(chalkGrey("○ No changes detected, skipping build…"));
|
||||
|
||||
logger.debug(`No changes detected, skipping build`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -509,19 +508,13 @@ function useDev({
|
||||
|
||||
backgroundWorker.metadata = backgroundWorkerRecord.data;
|
||||
|
||||
if (firstBuild) {
|
||||
logger.log(
|
||||
chalk.green(
|
||||
`Background worker started (${backgroundWorkerRecord.data.version})`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
logger.log(
|
||||
chalk.dim(
|
||||
`Background worker rebuilt (${backgroundWorkerRecord.data.version})`
|
||||
)
|
||||
);
|
||||
}
|
||||
logger.log(
|
||||
`${chalkGrey(
|
||||
`○ Background worker started -> ${chalkWorker(
|
||||
backgroundWorkerRecord.data.version
|
||||
)}`
|
||||
)}`
|
||||
);
|
||||
|
||||
firstBuild = false;
|
||||
|
||||
@@ -555,13 +548,7 @@ function useDev({
|
||||
await ctx.watch();
|
||||
}
|
||||
|
||||
const throttle = pThrottle({
|
||||
limit: 1,
|
||||
interval: 1000,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const throttledRebuild = throttle(runBuild);
|
||||
const throttledRebuild = pDebounce(runBuild, 250, { before: true });
|
||||
|
||||
const taskFileWatcher = watch(
|
||||
config.triggerDirectories.map((triggerDir) => `${triggerDir}/*.ts`),
|
||||
@@ -570,7 +557,7 @@ function useDev({
|
||||
}
|
||||
);
|
||||
|
||||
taskFileWatcher.on("change", async (path) => {
|
||||
taskFileWatcher.on("add", async (path) => {
|
||||
throttledRebuild().catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
tracer,
|
||||
wrapCommandAction,
|
||||
} from "../cli/common.js";
|
||||
import { chalkLink } from "../utilities/colors.js";
|
||||
import { chalkLink } from "../utilities/cliOutput.js";
|
||||
import { readAuthConfigProfile, writeAuthConfigProfile } from "../utilities/configFiles.js";
|
||||
import { getVersion } from "../utilities/getVersion.js";
|
||||
import { printInitialBanner } from "../utilities/initialBanner.js";
|
||||
@@ -75,7 +75,14 @@ export async function login(options?: LoginOptions): Promise<LoginResult> {
|
||||
const authConfig = readAuthConfigProfile(options?.profile);
|
||||
|
||||
if (authConfig && authConfig.accessToken) {
|
||||
const whoAmIResult = await whoAmI({ profile: options?.profile ?? "default", skipTelemetry: !span.isRecording(), logLevel: logger.loggerLevel }, opts.embedded);
|
||||
const whoAmIResult = await whoAmI(
|
||||
{
|
||||
profile: options?.profile ?? "default",
|
||||
skipTelemetry: !span.isRecording(),
|
||||
logLevel: logger.loggerLevel,
|
||||
},
|
||||
opts.embedded
|
||||
);
|
||||
|
||||
if (!whoAmIResult.success) {
|
||||
throw new Error(whoAmIResult.error);
|
||||
@@ -175,9 +182,19 @@ export async function login(options?: LoginOptions): Promise<LoginResult> {
|
||||
|
||||
getPersonalAccessTokenSpinner.stop(`Logged in with token ${indexResult.obfuscatedToken}`);
|
||||
|
||||
writeAuthConfigProfile({ accessToken: indexResult.token, apiUrl: opts.defaultApiUrl }, options?.profile);
|
||||
writeAuthConfigProfile(
|
||||
{ accessToken: indexResult.token, apiUrl: opts.defaultApiUrl },
|
||||
options?.profile
|
||||
);
|
||||
|
||||
const whoAmIResult = await whoAmI({ profile: options?.profile ?? "default", skipTelemetry: !span.isRecording(), logLevel: logger.loggerLevel }, opts.embedded);
|
||||
const whoAmIResult = await whoAmI(
|
||||
{
|
||||
profile: options?.profile ?? "default",
|
||||
skipTelemetry: !span.isRecording(),
|
||||
logLevel: logger.loggerLevel,
|
||||
},
|
||||
opts.embedded
|
||||
);
|
||||
|
||||
if (!whoAmIResult.success) {
|
||||
throw new Error(whoAmIResult.error);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { confirm, spinner } from "@clack/prompts";
|
||||
import { RunOptions, run } from "npm-check-updates";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { chalkError, chalkSuccess } from "../utilities/colors.js";
|
||||
import { chalkError, chalkSuccess } from "../utilities/cliOutput.js";
|
||||
import { readJSONFileSync, writeJSONFile } from "../utilities/fileSystem.js";
|
||||
import { installDependencies } from "../utilities/installDependencies.js";
|
||||
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import { intro, note, spinner } from "@clack/prompts";
|
||||
import { chalkLink } from "../utilities/colors.js";
|
||||
import { chalkLink } from "../utilities/cliOutput.js";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { isLoggedIn } from "../utilities/session.js";
|
||||
import { Command } from "commander";
|
||||
import { printInitialBanner } from "../utilities/initialBanner.js";
|
||||
import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js";
|
||||
import {
|
||||
CommonCommandOptions,
|
||||
commonOptions,
|
||||
handleTelemetry,
|
||||
wrapCommandAction,
|
||||
} from "../cli/common.js";
|
||||
import { z } from "zod";
|
||||
import { CliApiClient } from "../apiClient.js";
|
||||
|
||||
type WhoAmIResult =
|
||||
| {
|
||||
success: true;
|
||||
data: {
|
||||
userId: string;
|
||||
email: string;
|
||||
dashboardUrl: string;
|
||||
};
|
||||
}
|
||||
success: true;
|
||||
data: {
|
||||
userId: string;
|
||||
email: string;
|
||||
dashboardUrl: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
const WhoamiCommandOptions = CommonCommandOptions;
|
||||
|
||||
type WhoamiCommandOptions = z.infer<typeof WhoamiCommandOptions>;
|
||||
|
||||
export function configureWhoamiCommand(program: Command) {
|
||||
return commonOptions(program
|
||||
.command("whoami")
|
||||
.description("display the current logged in user and project details"))
|
||||
.action(async (options) => {
|
||||
await handleTelemetry(async () => {
|
||||
await printInitialBanner(false);
|
||||
await whoAmICommand(options);
|
||||
});
|
||||
return commonOptions(
|
||||
program.command("whoami").description("display the current logged in user and project details")
|
||||
).action(async (options) => {
|
||||
await handleTelemetry(async () => {
|
||||
await printInitialBanner(false);
|
||||
await whoAmICommand(options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function whoAmICommand(options: unknown) {
|
||||
@@ -61,7 +65,11 @@ export async function whoAmI(
|
||||
if (authentication.error === "fetch failed") {
|
||||
loadingSpinner.stop("Fetch failed. Platform down?");
|
||||
} else {
|
||||
loadingSpinner.stop(`You must login first. Use \`trigger.dev login --profile ${options?.profile ?? "default"}\` to login.`);
|
||||
loadingSpinner.stop(
|
||||
`You must login first. Use \`trigger.dev login --profile ${
|
||||
options?.profile ?? "default"
|
||||
}\` to login.`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import chalk from "chalk";
|
||||
|
||||
export const green = "#4FFF54";
|
||||
export const purple = "#735BF3";
|
||||
|
||||
export function chalkGreen(text: string) {
|
||||
return chalk.hex(green)(text);
|
||||
}
|
||||
|
||||
export function chalkPurple(text: string) {
|
||||
return chalk.hex(purple)(text);
|
||||
}
|
||||
|
||||
export function chalkGrey(text: string) {
|
||||
return chalk.hex("#878C99")(text);
|
||||
}
|
||||
|
||||
export function chalkError(text: string) {
|
||||
return chalk.hex("#E11D48")(text);
|
||||
}
|
||||
|
||||
export function chalkWarning(text: string) {
|
||||
return chalk.yellow(text);
|
||||
}
|
||||
|
||||
export function chalkSuccess(text: string) {
|
||||
return chalk.hex("#28BF5C")(text);
|
||||
}
|
||||
|
||||
export function chalkLink(text: string) {
|
||||
return chalk.underline.hex("#D7D9DD")(text);
|
||||
}
|
||||
|
||||
export function chalkWorker(text: string) {
|
||||
return chalk.hex("#FFFF89")(text);
|
||||
}
|
||||
|
||||
export function chalkTask(text: string) {
|
||||
return chalk.hex("#60A5FA")(text);
|
||||
}
|
||||
|
||||
export function chalkRun(text: string) {
|
||||
return chalk.hex("#A78BFA")(text);
|
||||
}
|
||||
|
||||
export function logo() {
|
||||
return `${chalk.hex(green).bold("Trigger")}${chalk.hex(purple).bold(".dev")}`;
|
||||
}
|
||||
|
||||
// Mar 27 09:17:25.653
|
||||
export function prettyPrintDate(date: Date = new Date()) {
|
||||
let formattedDate = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
|
||||
// Append milliseconds
|
||||
formattedDate += "." + ("00" + date.getMilliseconds()).slice(-3);
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
|
||||
export const green = "#4FFF54";
|
||||
export const purple = "#735BF3";
|
||||
|
||||
export function chalkGreen(text: string) {
|
||||
return chalk.hex(green)(text);
|
||||
}
|
||||
|
||||
export function chalkPurple(text: string) {
|
||||
return chalk.hex(purple)(text);
|
||||
}
|
||||
|
||||
export function chalkGrey(text: string) {
|
||||
return chalk.hex("#666")(text);
|
||||
}
|
||||
|
||||
export function chalkError(text: string) {
|
||||
return chalk.red(text);
|
||||
}
|
||||
|
||||
export function chalkSuccess(text: string) {
|
||||
return chalk.green(text);
|
||||
}
|
||||
|
||||
export function chalkLink(text: string) {
|
||||
return chalk.underline.blue(text);
|
||||
}
|
||||
|
||||
export function logo() {
|
||||
return `${chalk.hex(green).bold("Trigger")}${chalk.hex(purple).bold(".dev")}`;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import supportsColor from "supports-color";
|
||||
import type { Result } from "update-check";
|
||||
import checkForUpdate from "update-check";
|
||||
import pkg from "../../package.json";
|
||||
import { chalkGrey, green, logo } from "./colors.js";
|
||||
import { chalkGrey, chalkRun, chalkTask, chalkWorker, green, logo } from "./cliOutput.js";
|
||||
import { getVersion } from "./getVersion.js";
|
||||
import { logger } from "./logger.js";
|
||||
|
||||
@@ -52,9 +52,16 @@ export async function printStandloneInitialBanner(performUpdateCheck = true) {
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(text + "\n" + (supportsColor.stdout ? chalkGrey("-".repeat(54)) : "-".repeat(54)));
|
||||
}
|
||||
|
||||
export function printDevBanner() {
|
||||
logger.log(
|
||||
text + "\n" + (supportsColor.stdout ? chalk.hex(green)("-".repeat(54)) : "-".repeat(54))
|
||||
`${chalkGrey("Key:")} ${chalkWorker("Worker")} ${chalkGrey("|")} ${chalkTask(
|
||||
"Task"
|
||||
)} ${chalkGrey("|")} ${chalkRun("Run")}`
|
||||
);
|
||||
logger.log(chalkGrey("-".repeat(54)));
|
||||
}
|
||||
|
||||
async function doUpdateCheck(): Promise<string | undefined> {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ZodMessageSender,
|
||||
childToWorkerMessages,
|
||||
correctErrorStackTrace,
|
||||
formatDurationMilliseconds,
|
||||
workerToChildMessages,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import chalk from "chalk";
|
||||
@@ -27,6 +28,17 @@ import { safeDeleteFileSync } from "../../utilities/fileSystem.js";
|
||||
import { installPackages } from "../../utilities/installPackages.js";
|
||||
import { logger } from "../../utilities/logger.js";
|
||||
import { UncaughtExceptionError } from "../common/errors.js";
|
||||
import {
|
||||
chalkError,
|
||||
chalkGrey,
|
||||
chalkLink,
|
||||
chalkRun,
|
||||
chalkSuccess,
|
||||
chalkTask,
|
||||
chalkWarning,
|
||||
chalkWorker,
|
||||
prettyPrintDate,
|
||||
} from "../../utilities/cliOutput.js";
|
||||
|
||||
export type CurrentWorkers = BackgroundWorkerCoordinator["currentWorkers"];
|
||||
export class BackgroundWorkerCoordinator {
|
||||
@@ -146,16 +158,23 @@ export class BackgroundWorkerCoordinator {
|
||||
|
||||
const { execution } = payload;
|
||||
|
||||
// ○ Mar 27 09:17:25.653 -> View logs | 20240326.20 | create-avatar | run_slufhjdfiv8ejnrkw9dsj.1
|
||||
|
||||
const logsUrl = `${this.baseURL}/runs/${execution.run.id}`;
|
||||
|
||||
const link = chalk.bgBlueBright(terminalLink("view logs", logsUrl));
|
||||
let timestampPrefix = chalk.gray(new Date().toISOString());
|
||||
const workerPrefix = chalk.green(`[worker:${record.version}]`);
|
||||
const taskPrefix = chalk.yellow(`[task:${execution.task.id}]`);
|
||||
const runId = chalk.blue(execution.run.id);
|
||||
const attempt = chalk.blue(`.${execution.attempt.number}`);
|
||||
const pipe = chalkGrey("|");
|
||||
const bullet = chalkGrey("○");
|
||||
const link = chalkLink(terminalLink("View logs", logsUrl));
|
||||
let timestampPrefix = chalkGrey(prettyPrintDate(payload.execution.attempt.startedAt));
|
||||
const workerPrefix = chalkWorker(record.version);
|
||||
const taskPrefix = chalkTask(execution.task.id);
|
||||
const runId = chalkRun(`${execution.run.id}.${execution.attempt.number}`);
|
||||
|
||||
logger.log(`${timestampPrefix} ${workerPrefix}${taskPrefix} ${runId}${attempt} ${link}`);
|
||||
logger.log(
|
||||
`${bullet} ${timestampPrefix} ${chalkGrey(
|
||||
"->"
|
||||
)} ${link} ${pipe} ${workerPrefix} ${pipe} ${taskPrefix} ${pipe} ${runId}`
|
||||
);
|
||||
|
||||
const now = performance.now();
|
||||
|
||||
@@ -163,20 +182,21 @@ export class BackgroundWorkerCoordinator {
|
||||
|
||||
const elapsed = performance.now() - now;
|
||||
|
||||
const retryingText =
|
||||
const retryingText = chalkGrey(
|
||||
!completion.ok && completion.skippedRetrying
|
||||
? " (retrying skipped)"
|
||||
: !completion.ok && completion.retry !== undefined
|
||||
? ` (retrying in ${completion.retry.delay}ms)`
|
||||
: "";
|
||||
: ""
|
||||
);
|
||||
|
||||
const resultText = !completion.ok
|
||||
? completion.error.type === "INTERNAL_ERROR" &&
|
||||
(completion.error.code === TaskRunErrorCodes.TASK_EXECUTION_ABORTED ||
|
||||
completion.error.code === TaskRunErrorCodes.TASK_RUN_CANCELLED)
|
||||
? chalk.yellow("cancelled")
|
||||
: chalk.red(`error${retryingText}`)
|
||||
: chalk.green("success");
|
||||
? chalkWarning("Cancelled")
|
||||
: `${chalkError("Error")}${retryingText}`
|
||||
: chalkSuccess("Success");
|
||||
|
||||
const errorText = !completion.ok
|
||||
? this.#formatErrorLog(completion.error)
|
||||
@@ -184,12 +204,14 @@ export class BackgroundWorkerCoordinator {
|
||||
? `retry in ${completion.retry}ms`
|
||||
: "";
|
||||
|
||||
const elapsedText = chalk.dim(`(${elapsed.toFixed(2)}ms)`);
|
||||
const elapsedText = chalkGrey(`(${formatDurationMilliseconds(elapsed, { style: "short" })})`);
|
||||
|
||||
timestampPrefix = chalk.gray(new Date().toISOString());
|
||||
timestampPrefix = chalkGrey(prettyPrintDate());
|
||||
|
||||
logger.log(
|
||||
`${timestampPrefix} ${workerPrefix}${taskPrefix} ${runId}${attempt} ${resultText} ${elapsedText} ${link}${errorText}`
|
||||
`${bullet} ${timestampPrefix} ${chalkGrey(
|
||||
"->"
|
||||
)} ${link} ${pipe} ${workerPrefix} ${pipe} ${taskPrefix} ${pipe} ${runId} ${pipe} ${resultText} ${elapsedText}${errorText}`
|
||||
);
|
||||
|
||||
this.onTaskCompleted.post({ completion, execution, worker, backgroundWorkerId: id });
|
||||
@@ -201,13 +223,13 @@ export class BackgroundWorkerCoordinator {
|
||||
return "";
|
||||
}
|
||||
case "STRING_ERROR": {
|
||||
return `\n\n${error.raw}\n`;
|
||||
return `\n\n${chalkError("X Error:")} ${error.raw}\n`;
|
||||
}
|
||||
case "CUSTOM_ERROR": {
|
||||
return `\n\n${error.raw}\n`;
|
||||
return `\n\n${chalkError("X Error:")} ${error.raw}\n`;
|
||||
}
|
||||
case "BUILT_IN_ERROR": {
|
||||
return `\n\n${error.stackTrace}\n`;
|
||||
return `\n\n${error.stackTrace.replace(/^Error: /, chalkError("X Error: "))}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+8
@@ -1546,6 +1546,9 @@ importers:
|
||||
object-hash:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
p-debounce:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
p-throttle:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
@@ -26134,6 +26137,11 @@ packages:
|
||||
engines: {node: '>=12.20'}
|
||||
dev: false
|
||||
|
||||
/p-debounce@4.0.0:
|
||||
resolution: {integrity: sha512-4Ispi9I9qYGO4lueiLDhe4q4iK5ERK8reLsuzH6BPaXn53EGaua8H66PXIFGrW897hwjXp+pVLrm/DLxN0RF0A==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/p-event@4.2.0:
|
||||
resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const superChildTask = task({
|
||||
date: new Date(),
|
||||
regex: /foo/,
|
||||
bigint: BigInt(123),
|
||||
set: new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
set: new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
|
||||
map: new Map([
|
||||
["foo", "bar"],
|
||||
["baz", "qux"],
|
||||
|
||||
Reference in New Issue
Block a user