next-action-forge
Version:
A simple, type-safe toolkit for Next.js server actions with Zod validation
422 lines (416 loc) • 15.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __typeError = (msg) => {
throw TypeError(msg);
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
// src/index.ts
var src_exports = {};
__export(src_exports, {
ServerActionClient: () => ServerActionClient,
createActionClient: () => createActionClient,
handleServerActionError: () => handleServerActionError,
isErrorResponse: () => isErrorResponse,
isHTTPAccessFallbackError: () => isHTTPAccessFallbackError,
isNextNavigationError: () => isNextNavigationError,
isNotFoundError: () => isNotFoundError,
isRedirectError: () => isRedirectError
});
module.exports = __toCommonJS(src_exports);
// src/core/error-handler.ts
var import_zod = require("zod");
function convertZodError(error) {
const { fieldErrors, formErrors } = import_zod.z.flattenError(error);
const [field] = Object.keys(fieldErrors);
const message = (field && fieldErrors[field]?.[0]) ?? formErrors[0] ?? "Validation failed";
return {
message,
code: "VALIDATION_ERROR",
field,
fields: fieldErrors
};
}
function convertGenericError(error) {
const message = error instanceof Error ? error.message : "An unexpected error occurred";
return {
message: process.env.NODE_ENV === "production" ? "An unexpected error occurred" : message,
code: "INTERNAL_ERROR",
statusCode: 500
};
}
function handleServerActionError(error, customHandler) {
if (process.env.NODE_ENV === "development") {
console.error("[Server Action Error]", error?.constructor?.name || "Unknown", error instanceof Error ? error.message : error);
}
if (customHandler) {
try {
const customError = customHandler(error);
if (customError) {
return {
success: false,
error: customError
};
}
} catch (handlerError) {
console.error("[Server Action Error] Custom handler threw an error:", handlerError);
}
}
if (error instanceof import_zod.ZodError) {
return {
success: false,
error: convertZodError(error)
};
}
if (error instanceof Error && "toServerActionError" in error && typeof error.toServerActionError === "function") {
const serverError = error.toServerActionError();
if (serverError.code === "AUTHENTICATION_ERROR" || error.type === "AUTHENTICATION_ERROR") {
serverError.shouldRedirect = true;
serverError.redirectTo = "/login";
}
return {
success: false,
error: serverError
};
}
return {
success: false,
error: convertGenericError(error)
};
}
function isErrorResponse(response) {
return !response.success;
}
// src/next/errors/redirect.ts
var RedirectStatusCode = /* @__PURE__ */ ((RedirectStatusCode2) => {
RedirectStatusCode2[RedirectStatusCode2["SeeOther"] = 303] = "SeeOther";
RedirectStatusCode2[RedirectStatusCode2["TemporaryRedirect"] = 307] = "TemporaryRedirect";
RedirectStatusCode2[RedirectStatusCode2["PermanentRedirect"] = 308] = "PermanentRedirect";
return RedirectStatusCode2;
})(RedirectStatusCode || {});
var REDIRECT_ERROR_CODE = "NEXT_REDIRECT";
function isRedirectError(error) {
if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") {
return false;
}
const digest = error.digest.split(";");
const [errorCode, type] = digest;
const destination = digest.slice(2, -2).join(";");
const status = digest.at(-2);
const statusCode = Number(status);
return errorCode === REDIRECT_ERROR_CODE && (type === "replace" || type === "push") && typeof destination === "string" && !isNaN(statusCode) && statusCode in RedirectStatusCode;
}
// src/next/errors/http-access-fallback.ts
var HTTPAccessErrorStatus = {
NOT_FOUND: 404,
FORBIDDEN: 403,
UNAUTHORIZED: 401
};
var ALLOWED_CODES = new Set(Object.values(HTTPAccessErrorStatus));
var HTTP_ERROR_FALLBACK_ERROR_CODE = "NEXT_HTTP_ERROR_FALLBACK";
function isHTTPAccessFallbackError(error) {
if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") {
return false;
}
const [prefix, httpStatus] = error.digest.split(";");
return prefix === HTTP_ERROR_FALLBACK_ERROR_CODE && ALLOWED_CODES.has(Number(httpStatus));
}
function getAccessFallbackHTTPStatus(error) {
const httpStatus = error.digest.split(";")[1];
return Number(httpStatus);
}
// src/next/errors/index.ts
function isNextNavigationError(error) {
return isRedirectError(error) || isHTTPAccessFallbackError(error);
}
function isNotFoundError(error) {
return isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 404;
}
// src/core/server-action-client.ts
var _args;
var _ServerActionClient = class _ServerActionClient {
constructor(args = {
middlewareFns: []
}) {
__privateAdd(this, _args);
__privateSet(this, _args, args);
}
/**
* Use a middleware function.
* @param middlewareFn Middleware function
*/
use(middlewareFn) {
return new _ServerActionClient({
...__privateGet(this, _args),
middlewareFns: [...__privateGet(this, _args).middlewareFns, middlewareFn],
ctxType: {}
});
}
/**
* Define the input validation schema for the action.
* @param schema Input validation schema
*/
inputSchema(schema) {
return new _ServerActionClient({
...__privateGet(this, _args),
inputSchema: schema
});
}
/**
* Define the output validation schema for the action.
* @param schema Output validation schema
*/
outputSchema(schema) {
return new _ServerActionClient({
...__privateGet(this, _args),
outputSchema: schema
});
}
/**
* Define a custom error handler for the action.
* @param handler Error handler function
*/
onError(handler) {
return new _ServerActionClient({
...__privateGet(this, _args),
onError: handler
});
}
/**
* Define a redirect configuration for successful actions.
* @param config Redirect URL, config object, or function that returns redirect config based on result
*/
redirect(config) {
return new _ServerActionClient({
...__privateGet(this, _args),
redirectConfig: config
});
}
/**
* Define the action.
* @param serverCodeFn Code that will be executed on the server side
*/
action(serverCodeFn) {
return createServerAction({
inputSchema: __privateGet(this, _args).inputSchema,
outputSchema: __privateGet(this, _args).outputSchema,
middlewareFns: __privateGet(this, _args).middlewareFns,
onError: __privateGet(this, _args).onError,
serverCodeFn,
redirectConfig: __privateGet(this, _args).redirectConfig
});
}
/**
* Define a form action that accepts FormData.
* @param serverCodeFn Code that will be executed on the server side
*/
formAction(serverCodeFn) {
return createFormServerAction({
inputSchema: __privateGet(this, _args).inputSchema,
outputSchema: __privateGet(this, _args).outputSchema,
middlewareFns: __privateGet(this, _args).middlewareFns,
onError: __privateGet(this, _args).onError,
serverCodeFn,
redirectConfig: __privateGet(this, _args).redirectConfig
});
}
};
_args = new WeakMap();
var ServerActionClient = _ServerActionClient;
function createServerAction(config) {
const actionFn = async (...args) => {
const context = {};
const hasInputSchema = config.inputSchema !== void 0 && config.inputSchema !== null;
const input = hasInputSchema ? args[0] : void 0;
try {
let parsedInput = void 0;
if (hasInputSchema && config.inputSchema) {
const parseResult = config.inputSchema.safeParse(input);
if (!parseResult.success) {
return handleServerActionError(parseResult.error, config.onError ? (err) => config.onError(err, { parsedInput: input }) : void 0);
}
parsedInput = parseResult.data;
context.parsedInput = parsedInput;
}
let middlewareContext = {};
for (const middleware of config.middlewareFns) {
const middlewareInput = hasInputSchema ? parsedInput : void 0;
const result2 = await middleware({ context: middlewareContext, input: middlewareInput });
if ("error" in result2) {
return {
success: false,
error: result2.error
};
}
middlewareContext = { ...middlewareContext, ...result2.context };
}
const result = hasInputSchema ? await config.serverCodeFn(parsedInput, middlewareContext) : await config.serverCodeFn(middlewareContext);
let validatedOutput = result;
if (config.outputSchema) {
const parseResult = config.outputSchema.safeParse(result);
if (!parseResult.success) {
return handleServerActionError(parseResult.error);
}
validatedOutput = parseResult.data;
}
let redirectValue;
if (config.redirectConfig) {
if (typeof config.redirectConfig === "function") {
redirectValue = config.redirectConfig(validatedOutput);
} else {
redirectValue = config.redirectConfig;
}
}
return {
success: true,
data: validatedOutput,
...redirectValue && { redirect: redirectValue }
};
} catch (error) {
if (isNextNavigationError(error)) {
throw error;
}
if (process.env.NODE_ENV === "development") {
console.log("[executeAction] Error:", error?.constructor?.name, error instanceof Error ? error.message : error);
}
const errorHandler = config.onError ? (err) => {
return config.onError(err, { parsedInput: context.parsedInput });
} : void 0;
return handleServerActionError(error, errorHandler);
}
};
actionFn.__hasInputSchema = config.inputSchema !== void 0 && config.inputSchema !== null;
return actionFn;
}
function tryParseJSON(value) {
if (typeof value !== "string") return value;
const trimmed = value.trim();
if (!trimmed) return value;
if (trimmed === "true") return true;
if (trimmed === "false") return false;
if (trimmed === "null") return null;
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed === "object" && parsed !== null) {
return parsed;
}
} catch {
}
}
return value;
}
function parseFormData(formData) {
const result = {};
const keys = /* @__PURE__ */ new Set();
for (const [key] of formData.entries()) {
keys.add(key);
}
for (const key of keys) {
const values = formData.getAll(key);
if (key.endsWith("[]")) {
const cleanKey = key.slice(0, -2);
result[cleanKey] = values.map(tryParseJSON);
} else if (values.length === 1) {
result[key] = tryParseJSON(values[0]);
} else {
result[key] = values.map(tryParseJSON);
}
}
return result;
}
function createFormServerAction(config) {
return async (_prev, formData) => {
const context = {};
const hasInputSchema = config.inputSchema !== void 0 && config.inputSchema !== null;
try {
let parsedInput = parseFormData(formData);
if (config.inputSchema) {
const parseResult = config.inputSchema.safeParse(parsedInput);
if (!parseResult.success) {
return handleServerActionError(parseResult.error, config.onError ? (err) => config.onError(err, { parsedInput }) : void 0);
}
parsedInput = parseResult.data;
context.parsedInput = parsedInput;
}
let middlewareContext = {};
for (const middleware of config.middlewareFns) {
const middlewareInput = hasInputSchema ? parsedInput : void 0;
const result2 = await middleware({ context: middlewareContext, input: middlewareInput });
if ("error" in result2) {
return {
success: false,
error: result2.error
};
}
middlewareContext = { ...middlewareContext, ...result2.context };
}
const result = hasInputSchema ? await config.serverCodeFn(parsedInput, middlewareContext) : await config.serverCodeFn(middlewareContext);
let validatedOutput = result;
if (config.outputSchema) {
const parseResult = config.outputSchema.safeParse(result);
if (!parseResult.success) {
return handleServerActionError(parseResult.error);
}
validatedOutput = parseResult.data;
}
let redirectValue;
if (config.redirectConfig) {
if (typeof config.redirectConfig === "function") {
redirectValue = config.redirectConfig(validatedOutput);
} else {
redirectValue = config.redirectConfig;
}
}
return {
success: true,
data: validatedOutput,
...redirectValue && { redirect: redirectValue }
};
} catch (error) {
if (isNextNavigationError(error)) {
throw error;
}
if (process.env.NODE_ENV === "development") {
console.log("[ServerActionClient] Error:", error?.constructor?.name, error instanceof Error ? error.message : error);
}
const errorHandler = config.onError ? (err) => {
return config.onError(err, { parsedInput: context.parsedInput });
} : void 0;
return handleServerActionError(error, errorHandler);
}
};
}
function createActionClient() {
return new ServerActionClient();
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ServerActionClient,
createActionClient,
handleServerActionError,
isErrorResponse,
isHTTPAccessFallbackError,
isNextNavigationError,
isNotFoundError,
isRedirectError
});
//# sourceMappingURL=index.js.map
;