Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import initAPIToken from "./services/apiToken/initAPIToken";
import { initProjectConfig } from "./services/projectConfig";
import appContext from "./utils/appContext";
import type commander from "commander";
import { ErrorType, isDittoError, isDittoErrorType } from "./utils/DittoError";

type Command = "pull";

Expand Down Expand Up @@ -87,18 +88,33 @@ const executeCommand = async (
}
}
} catch (error) {
const eventId = Sentry.captureException(error);
const eventStr = `\n\nError ID: ${logger.info(eventId)}`;

if (process.env.DEBUG === "true") {
console.error(logger.info("Development stack trace:\n"), error);
}

return await quit(
logger.errorText(
"Something went wrong. Please contact support or try again later."
) + eventStr
);
let sentryOptions = undefined;
let exitCode = undefined;
let errorText =
"Something went wrong. Please contact support or try again later.";

if (isDittoError(error)) {
exitCode = error.exitCode;

if (isDittoErrorType(error, ErrorType.ConfigYamlLoadError)) {
errorText = error.message;
} else if (isDittoErrorType(error, ErrorType.ConfigParseError)) {
errorText = `${error.data.messagePrefix}\n\n${error.data.formattedError}`;
}

sentryOptions = {
extra: { message: errorText, ...(error.data || {}) },
};
}

const eventId = Sentry.captureException(error, sentryOptions);
const eventStr = `\n\nError ID: ${logger.info(eventId)}`;

return await quit(logger.errorText(errorText) + eventStr, exitCode);
}
};

Expand Down
4 changes: 2 additions & 2 deletions lib/src/outputs/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { ZBaseOutputFilters } from "./shared";
const ZBaseJSONOutput = ZBaseOutputFilters.extend({
format: z.literal("json"),
framework: z.undefined(),
});
}).strict();

const Zi18NextJSONOutput = ZBaseJSONOutput.extend({
framework: z.literal("i18next"),
type: z.literal("module").or(z.literal("commonjs")).optional(),
});
}).strict();

export const ZJSONOutput = z.discriminatedUnion("framework", [
ZBaseJSONOutput,
Expand Down
27 changes: 23 additions & 4 deletions lib/src/services/projectConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import appContext from "../utils/appContext";
import yaml from "js-yaml";
import { ZBaseOutputFilters } from "../outputs/shared";
import { ZOutput } from "../outputs";
import DittoError, { ErrorType } from "../utils/DittoError";

const ZProjectConfigYAML = ZBaseOutputFilters.extend({
outputs: z.array(ZOutput),
Expand Down Expand Up @@ -38,12 +39,30 @@ function readProjectConfigData(
file = appContext.projectConfigFile,
defaultData: ProjectConfigYAML = DEFAULT_PROJECT_CONFIG_JSON
): ProjectConfigYAML {
createFileIfMissingSync(file, yaml.dump(defaultData));
const fileContents = fs.readFileSync(file, "utf8");
const yamlData = yaml.load(fileContents);
let yamlData: unknown = defaultData;

try {
createFileIfMissingSync(file, yaml.dump(defaultData));
const fileContents = fs.readFileSync(file, "utf8");
yamlData = yaml.load(fileContents);
} catch (err) {
throw new DittoError({
type: ErrorType.ConfigYamlLoadError,
data: { rawErrorMessage: (err as any).message },
message:
"Could not load the project config file. Please check the file path and that it is a valid YAML file.",
});
}

const parsedYAML = ZProjectConfigYAML.safeParse(yamlData);
if (!parsedYAML.success) {
throw new Error("Failed to parse project config file");
throw new DittoError({
type: ErrorType.ConfigParseError,
data: {
formattedError: JSON.stringify(parsedYAML.error.flatten(), null, 2),
messagePrefix: "There is an error in your project config file.",
},
});
}
return parsedYAML.data;
}
81 changes: 81 additions & 0 deletions lib/src/utils/DittoError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { z } from "zod";

/**
* An Error extension designed to be thrown from anywhere in the CLI.
* The custom properties provide a reliable way to include additional data
* (what the error was and context around it) that can be leveraged
* to pass along data to the user, to Sentry, or to other services.
*/
export default class DittoError<T extends ErrorType> extends Error {
exitCode: number | undefined;
type: ErrorType;
// Note: if you see the type error "Type 'T' cannot be used to index type 'ErrorDataMap'",
// a value is missing from the ErrorDataMap defined below
data: ErrorDataMap[T];

/**
* Creates a new custom error with the following properties:
* @param type The type of error, from the ErrorType enum
* @param message Optional: error message to display to the user
* @param exitCode Optional: exit code to return to the shell.
* @param data Optional: additional data to pass along with the error
*/
constructor({
type,
message,
exitCode,
data,
}: {
type: T;
message?: string;
exitCode?: number;
data: ErrorDataMap[T];
}) {
const errorMessage =
message ||
"Something went wrong. Please contact support or try again later.";

super(errorMessage);

this.exitCode = exitCode;
this.type = type;
this.data = data;
}
}

/**
* Exhaustive list of DittoError types
* When adding to this list, you must also add a Data type to ErrorDataMap
*/
export enum ErrorType {
ConfigYamlLoadError = "ConfigYamlLoadError",
ConfigParseError = "ConfigParseError",
}

/**
* Map of DittoError types to the data that is required for that type
* The keys of this must exhaustively match the keys of the ErrorType enum
*/
type ErrorDataMap = {
[ErrorType.ConfigYamlLoadError]: ConfigYamlLoadErrorData;
[ErrorType.ConfigParseError]: ConfigParseErrorData;
};

type ConfigYamlLoadErrorData = {
rawErrorMessage: string;
};

type ConfigParseErrorData = {
formattedError: string;
messagePrefix: string;
};

export function isDittoError(error: unknown): error is DittoError<ErrorType> {
return error instanceof DittoError;
}
export function isDittoErrorType<T extends ErrorType>(
error: DittoError<ErrorType>,
type: T
): error is DittoError<T> {
return error.type === type;
}