@12-apps/forms-core 1.19.0 → 1.19.1

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,220 @@
1
+ /**
2
+ * Field-level form validators
3
+ *
4
+ * Pure, framework-agnostic validators — NO zod, NO react-hook-form. Each
5
+ * validator is curried and returns a `string` error message for invalid input
6
+ * or `undefined` when the value is valid.
7
+ *
8
+ * @module forms-core/validators
9
+ */
10
+ /** A field validator: returns an error message, or `undefined` when valid. */
11
+ type Validator = (value: string) => string | undefined;
12
+ /**
13
+ * Requires a non-empty value (after trimming whitespace).
14
+ *
15
+ * @param message - Custom error message for empty input.
16
+ * @returns A validator returning the message for empty input, else `undefined`.
17
+ */
18
+ declare const required: (message?: string) => Validator;
19
+ /**
20
+ * Validates that the value is a well-formed email address.
21
+ *
22
+ * @param message - Custom error message for malformed input.
23
+ * @returns A validator returning the message for invalid input, else `undefined`.
24
+ */
25
+ declare const email: (message?: string) => Validator;
26
+ /**
27
+ * Validates that the value has at least `length` characters.
28
+ *
29
+ * @param length - The minimum number of characters required.
30
+ * @param message - Custom error message for too-short input.
31
+ * @returns A validator returning the message for short input, else `undefined`.
32
+ */
33
+ declare const minLength: (length: number, message?: string) => Validator;
34
+ /** Alias of {@link minLength}. */
35
+ declare const min: (length: number, message?: string) => Validator;
36
+ /** A map of field names to the list of validators applied to that field. */
37
+ type ValidatorSchema<T> = {
38
+ [K in keyof T]?: Validator[];
39
+ };
40
+ /** A map of field names to the first error message produced for that field. */
41
+ type FieldErrors<T> = {
42
+ [K in keyof T]?: string;
43
+ };
44
+ /**
45
+ * Runs each field's validators against `values`, short-circuiting on the first
46
+ * failing validator per field.
47
+ *
48
+ * @param values - The field values to validate (keyed by field name).
49
+ * @param schema - Validators to apply per field.
50
+ * @returns A record containing one entry per failing field; `{}` when all valid.
51
+ */
52
+ declare const validateFields: <T extends Record<string, string>>(values: T, schema: ValidatorSchema<T>) => FieldErrors<T>;
53
+
54
+ /**
55
+ * Discriminated-union result type for operations that can fail.
56
+ *
57
+ * Pure and framework-agnostic. The `ok` discriminant narrows the union so
58
+ * consumers can safely read `data` on success or `error` on failure.
59
+ *
60
+ * @typeParam T - The success payload type.
61
+ * @module forms-core/result
62
+ */
63
+ type Result<T> = {
64
+ ok: true;
65
+ data: T;
66
+ } | {
67
+ ok: false;
68
+ error: string;
69
+ /**
70
+ * Per-field error messages, keyed by form field name, when the failure is
71
+ * attributable to specific input(s). Lets a form highlight the exact
72
+ * offending field(s) instead of only showing the summary `error`.
73
+ */
74
+ fieldErrors?: Record<string, string>;
75
+ };
76
+ /**
77
+ * Builds a successful {@link Result} wrapping `data`.
78
+ *
79
+ * @typeParam T - The success payload type.
80
+ * @param data - The value to wrap.
81
+ * @returns `{ ok: true, data }`.
82
+ */
83
+ declare const ok: <T>(data: T) => Result<T>;
84
+ /**
85
+ * Builds a failed {@link Result} wrapping an error `message`.
86
+ *
87
+ * @param error - A user-safe error message (never an internal/stack detail).
88
+ * @param fieldErrors - Optional per-field messages so a form can highlight the
89
+ * exact offending field(s). Omitted when the failure isn't field-specific.
90
+ * @returns `{ ok: false, error, fieldErrors? }`.
91
+ */
92
+ declare const err: (error: string, fieldErrors?: Record<string, string>) => Result<never>;
93
+
94
+ /**
95
+ * An error whose `message` is safe to show to the end user.
96
+ *
97
+ * Handlers throw this to surface a *specific, actionable* failure (e.g. "a
98
+ * product with this name already exists") through {@link createServerAction}
99
+ * instead of collapsing into the {@link GENERIC_ERROR}. Any error that is NOT
100
+ * an `ActionError` is treated as unexpected: it is logged server-side and the
101
+ * caller receives the generic message so internal details never leak.
102
+ */
103
+ declare class ActionError extends Error {
104
+ /**
105
+ * The form field this failure is attributable to (e.g. `"name"` for a
106
+ * duplicate-name conflict). When set, {@link createServerAction} surfaces it
107
+ * as `fieldErrors` so the form can highlight that exact field.
108
+ */
109
+ readonly field?: string;
110
+ /**
111
+ * Per-field messages when a single failure attributes to MORE than one field
112
+ * (e.g. several duplicate SKUs across a product's variation rows). Takes
113
+ * precedence over {@link field}; when set, {@link createServerAction} passes
114
+ * this map straight through as `fieldErrors`.
115
+ */
116
+ readonly fieldErrors?: Record<string, string>;
117
+ constructor(message: string, field?: string, fieldErrors?: Record<string, string>);
118
+ }
119
+ /**
120
+ * Wraps a handler-agnostic server action with validate-first semantics.
121
+ *
122
+ * The returned action:
123
+ * 1. Runs {@link validateFields} against `input` using `schema`. On the first
124
+ * field error it returns `err(message)` **without invoking `handler`**.
125
+ * 2. Otherwise invokes `handler(input)` and returns `ok` with its resolved
126
+ * value.
127
+ * 3. Passes through the message of an {@link ActionError} thrown by `handler`
128
+ * (a deliberate, user-safe failure), and maps any *other* exception to a
129
+ * generic `err` message — logging the real cause server-side so production
130
+ * failures stay diagnosable without leaking internals to the client.
131
+ *
132
+ * @typeParam I - The validated input shape (string fields keyed by name).
133
+ * @typeParam O - The handler's success payload type.
134
+ * @param schema - Per-field validators applied before the handler runs.
135
+ * @param handler - The business logic, run only when input is valid.
136
+ * @returns An async action that resolves to a {@link Result}.
137
+ */
138
+ declare const createServerAction: <I extends Record<string, string>, O>(schema: ValidatorSchema<I>, handler: (input: I) => Promise<O> | O) => ((input: I) => Promise<Result<O>>);
139
+
140
+ /**
141
+ * Brazilian document + address primitives (FUT-306).
142
+ *
143
+ * Masks, shape checks and check-digit validation for the two fields every
144
+ * pt-BR form asks for — CEP and CNPJ — plus the UF list. Pure and dependency
145
+ * free like the rest of `forms-core`, so the SAME rules run in the browser
146
+ * (input mask + inline validation), in a route handler (before hitting a
147
+ * lookup provider) and in a test.
148
+ *
149
+ * @module forms-core/br
150
+ */
151
+
152
+ /** Brazilian federative units, for UF selects and client-side validation. */
153
+ declare const UF_LIST: readonly ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"];
154
+ /** A Brazilian federative unit. */
155
+ type Uf = (typeof UF_LIST)[number];
156
+ /** True when `value` is one of the 27 {@link UF_LIST} entries (case-sensitive). */
157
+ declare function isUf(value: string): value is Uf;
158
+ /** Canonical CEP shape after masking/normalization: `01310-100`. */
159
+ declare const CEP_PATTERN: RegExp;
160
+ /** The bare digits of a CEP, capped at the 8 a CEP has. */
161
+ declare function cepDigits(raw: string): string;
162
+ /**
163
+ * Mask raw CEP input to `#####-###`: strips non-digits, caps at 8 digits and
164
+ * inserts the hyphen once the sixth digit arrives. Pure — safe to run on every
165
+ * keystroke and to normalize before matching {@link CEP_PATTERN}.
166
+ */
167
+ declare function formatCep(raw: string): string;
168
+ /**
169
+ * True when `raw` holds a complete, well-formed CEP.
170
+ *
171
+ * Shape only: a CEP is a postal RANGE allocated by the Correios, so there is no
172
+ * check digit to verify. Whether it actually EXISTS is answered by a lookup,
173
+ * not by this function.
174
+ */
175
+ declare function isValidCep(raw: string): boolean;
176
+ /** Empty ⇒ valid (optional field); otherwise the value must be a full CEP. */
177
+ declare const cep: (message?: string) => Validator;
178
+ /** Canonical CNPJ shape after masking: `12.ABC.345/01DE-35`. */
179
+ declare const CNPJ_PATTERN: RegExp;
180
+ /**
181
+ * Strip a CNPJ to its 14 significant characters, uppercased.
182
+ *
183
+ * Alphanumeric-aware (Receita Federal IN 2.229/2024, live in production since
184
+ * 2026-07-06): the first 12 positions may be letters OR digits, so only
185
+ * punctuation is removed — never letters.
186
+ */
187
+ declare function cnpjChars(raw: string): string;
188
+ /**
189
+ * Mask raw CNPJ input to `##.###.###/####-##`, accepting letters in the first
190
+ * twelve positions and digits in the two check positions.
191
+ */
192
+ declare function formatCnpj(raw: string): string;
193
+ /**
194
+ * Validate a CNPJ's check digits (mod 11), alphanumeric-aware.
195
+ *
196
+ * Rejects the repeated-character sequences (`00000000000000`, `AAAAAAAAAAAA35`)
197
+ * that satisfy the arithmetic but are never issued. Returns false for anything
198
+ * that is not exactly 14 characters with numeric check digits.
199
+ */
200
+ declare function isValidCnpj(raw: string): boolean;
201
+ /** Empty ⇒ valid (optional field); otherwise the check digits must match. */
202
+ declare const cnpj: (message?: string) => Validator;
203
+ /**
204
+ * Format a Brazilian phone as `(11) 3214-5600` / `(11) 91234-5678`.
205
+ *
206
+ * Both sources that hand us a phone give it as bare concatenated digits — the
207
+ * Receita Federal's `ddd_telefone_1` and an NF-e's `<fone>` — so the same
208
+ * formatting rule serves the CNPJ lookup and the nota importer.
209
+ *
210
+ * Anything that is not a 10- or 11-digit national number is returned trimmed
211
+ * but otherwise untouched: an unrecognized phone (a foreign issuer, an extension
212
+ * appended by the emitter's software) is still better dropped into the field
213
+ * than discarded.
214
+ *
215
+ * @param raw - Digits as the source supplies them, e.g. `1132145600`.
216
+ * @returns The formatted phone, or `null` when there is nothing to format.
217
+ */
218
+ declare function formatBrPhone(raw: string | null | undefined): string | null;
219
+
220
+ export { ActionError, CEP_PATTERN, CNPJ_PATTERN, type FieldErrors, type Result, UF_LIST, type Uf, type Validator, type ValidatorSchema, cep, cepDigits, cnpj, cnpjChars, createServerAction, email, err, formatBrPhone, formatCep, formatCnpj, isUf, isValidCep, isValidCnpj, min, minLength, ok, required, validateFields };
package/dist/index.js ADDED
@@ -0,0 +1,203 @@
1
+ // src/validators.ts
2
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3
+ var required = (message = "This field is required") => (value) => value.trim().length === 0 ? message : void 0;
4
+ var email = (message = "Enter a valid email address") => (value) => EMAIL_PATTERN.test(value) ? void 0 : message;
5
+ var minLength = (length, message = `Must be at least ${length} characters`) => (value) => value.length < length ? message : void 0;
6
+ var min = minLength;
7
+ var firstFieldError = (validators, value) => {
8
+ for (const validate of validators) {
9
+ const error = validate(value);
10
+ if (error !== void 0) {
11
+ return error;
12
+ }
13
+ }
14
+ return void 0;
15
+ };
16
+ var validateFields = (values, schema) => {
17
+ const errors = {};
18
+ for (const field of Object.keys(schema)) {
19
+ const validators = schema[field];
20
+ if (!validators) {
21
+ continue;
22
+ }
23
+ const error = firstFieldError(validators, values[field] ?? "");
24
+ if (error !== void 0) {
25
+ errors[field] = error;
26
+ }
27
+ }
28
+ return errors;
29
+ };
30
+
31
+ // src/result.ts
32
+ var ok = (data) => ({ ok: true, data });
33
+ var err = (error, fieldErrors) => fieldErrors ? { ok: false, error, fieldErrors } : { ok: false, error };
34
+
35
+ // src/serverAction.ts
36
+ var GENERIC_ERROR = "Something went wrong. Please try again.";
37
+ var ActionError = class extends Error {
38
+ /**
39
+ * The form field this failure is attributable to (e.g. `"name"` for a
40
+ * duplicate-name conflict). When set, {@link createServerAction} surfaces it
41
+ * as `fieldErrors` so the form can highlight that exact field.
42
+ */
43
+ field;
44
+ /**
45
+ * Per-field messages when a single failure attributes to MORE than one field
46
+ * (e.g. several duplicate SKUs across a product's variation rows). Takes
47
+ * precedence over {@link field}; when set, {@link createServerAction} passes
48
+ * this map straight through as `fieldErrors`.
49
+ */
50
+ fieldErrors;
51
+ constructor(message, field, fieldErrors) {
52
+ super(message);
53
+ this.name = "ActionError";
54
+ this.field = field;
55
+ this.fieldErrors = fieldErrors;
56
+ }
57
+ };
58
+ var createServerAction = (schema, handler) => {
59
+ return async (input) => {
60
+ const errors = validateFields(input, schema);
61
+ const firstField = Object.keys(errors).find(
62
+ (field) => typeof errors[field] === "string"
63
+ );
64
+ if (firstField !== void 0) {
65
+ const message = errors[firstField];
66
+ return err(message, { [String(firstField)]: message });
67
+ }
68
+ try {
69
+ return ok(await handler(input));
70
+ } catch (error) {
71
+ if (error instanceof ActionError) {
72
+ const fieldErrors = error.fieldErrors ?? (error.field ? { [error.field]: error.message } : void 0);
73
+ return err(error.message, fieldErrors);
74
+ }
75
+ console.error("[serverAction] handler threw an unexpected error:", error);
76
+ return err(GENERIC_ERROR);
77
+ }
78
+ };
79
+ };
80
+
81
+ // src/br.ts
82
+ var UF_LIST = [
83
+ "AC",
84
+ "AL",
85
+ "AP",
86
+ "AM",
87
+ "BA",
88
+ "CE",
89
+ "DF",
90
+ "ES",
91
+ "GO",
92
+ "MA",
93
+ "MT",
94
+ "MS",
95
+ "MG",
96
+ "PA",
97
+ "PB",
98
+ "PR",
99
+ "PE",
100
+ "PI",
101
+ "RJ",
102
+ "RN",
103
+ "RS",
104
+ "RO",
105
+ "RR",
106
+ "SC",
107
+ "SP",
108
+ "SE",
109
+ "TO"
110
+ ];
111
+ function isUf(value) {
112
+ return UF_LIST.includes(value);
113
+ }
114
+ var CEP_PATTERN = /^\d{5}-\d{3}$/;
115
+ function cepDigits(raw) {
116
+ return raw.replace(/\D/g, "").slice(0, 8);
117
+ }
118
+ function formatCep(raw) {
119
+ const digits = cepDigits(raw);
120
+ return digits.length > 5 ? `${digits.slice(0, 5)}-${digits.slice(5)}` : digits;
121
+ }
122
+ function isValidCep(raw) {
123
+ return CEP_PATTERN.test(formatCep(raw));
124
+ }
125
+ var cep = (message = "CEP inv\xE1lido \u2014 use o formato 00000-000.") => (value) => value.trim() === "" || isValidCep(value) ? void 0 : message;
126
+ var CNPJ_PATTERN = /^[0-9A-Z]{2}\.[0-9A-Z]{3}\.[0-9A-Z]{3}\/[0-9A-Z]{4}-\d{2}$/;
127
+ var CNPJ_LENGTH = 14;
128
+ function cnpjChars(raw) {
129
+ return raw.toUpperCase().replace(/[^0-9A-Z]/g, "").slice(0, CNPJ_LENGTH);
130
+ }
131
+ function formatCnpj(raw) {
132
+ const chars = cnpjChars(raw);
133
+ const parts = [
134
+ chars.slice(0, 2),
135
+ chars.slice(2, 5),
136
+ chars.slice(5, 8),
137
+ chars.slice(8, 12),
138
+ // The check digits are always numeric, even on an alphanumeric CNPJ.
139
+ chars.slice(12, 14).replace(/\D/g, "")
140
+ ];
141
+ let masked = parts[0];
142
+ if (chars.length > 2) masked += `.${parts[1]}`;
143
+ if (chars.length > 5) masked += `.${parts[2]}`;
144
+ if (chars.length > 8) masked += `/${parts[3]}`;
145
+ if (chars.length > 12) masked += `-${parts[4]}`;
146
+ return masked;
147
+ }
148
+ var CNPJ_WEIGHTS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
149
+ function cnpjValue(char) {
150
+ return char.charCodeAt(0) - 48;
151
+ }
152
+ function cnpjCheckDigit(base) {
153
+ const weights = CNPJ_WEIGHTS.slice(-base.length);
154
+ const sum = [...base].reduce((acc, char, i) => acc + cnpjValue(char) * weights[i], 0);
155
+ const remainder = sum % 11;
156
+ return remainder < 2 ? 0 : 11 - remainder;
157
+ }
158
+ function isValidCnpj(raw) {
159
+ const chars = cnpjChars(raw);
160
+ if (chars.length !== CNPJ_LENGTH) return false;
161
+ if (!/^\d{2}$/.test(chars.slice(12))) return false;
162
+ if (new Set(chars).size === 1) return false;
163
+ const base = chars.slice(0, 12);
164
+ const first = cnpjCheckDigit(base);
165
+ if (first !== Number(chars[12])) return false;
166
+ return cnpjCheckDigit(`${base}${first}`) === Number(chars[13]);
167
+ }
168
+ var cnpj = (message = "CNPJ inv\xE1lido \u2014 confira os d\xEDgitos.") => (value) => value.trim() === "" || isValidCnpj(value) ? void 0 : message;
169
+ function formatBrPhone(raw) {
170
+ const trimmed = raw?.trim();
171
+ if (!trimmed) return null;
172
+ const digits = trimmed.replace(/\D/g, "");
173
+ if (digits.length !== 10 && digits.length !== 11) return trimmed;
174
+ const ddd = digits.slice(0, 2);
175
+ const subscriber = digits.slice(2);
176
+ const split = subscriber.length - 4;
177
+ return `(${ddd}) ${subscriber.slice(0, split)}-${subscriber.slice(split)}`;
178
+ }
179
+ export {
180
+ ActionError,
181
+ CEP_PATTERN,
182
+ CNPJ_PATTERN,
183
+ UF_LIST,
184
+ cep,
185
+ cepDigits,
186
+ cnpj,
187
+ cnpjChars,
188
+ createServerAction,
189
+ email,
190
+ err,
191
+ formatBrPhone,
192
+ formatCep,
193
+ formatCnpj,
194
+ isUf,
195
+ isValidCep,
196
+ isValidCnpj,
197
+ min,
198
+ minLength,
199
+ ok,
200
+ required,
201
+ validateFields
202
+ };
203
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/validators.ts","../src/result.ts","../src/serverAction.ts","../src/br.ts"],"sourcesContent":["/**\n * Field-level form validators\n *\n * Pure, framework-agnostic validators — NO zod, NO react-hook-form. Each\n * validator is curried and returns a `string` error message for invalid input\n * or `undefined` when the value is valid.\n *\n * @module forms-core/validators\n */\n\n/** A field validator: returns an error message, or `undefined` when valid. */\nexport type Validator = (value: string) => string | undefined;\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * Requires a non-empty value (after trimming whitespace).\n *\n * @param message - Custom error message for empty input.\n * @returns A validator returning the message for empty input, else `undefined`.\n */\nexport const required =\n (message = 'This field is required'): Validator =>\n (value) =>\n value.trim().length === 0 ? message : undefined;\n\n/**\n * Validates that the value is a well-formed email address.\n *\n * @param message - Custom error message for malformed input.\n * @returns A validator returning the message for invalid input, else `undefined`.\n */\nexport const email =\n (message = 'Enter a valid email address'): Validator =>\n (value) =>\n EMAIL_PATTERN.test(value) ? undefined : message;\n\n/**\n * Validates that the value has at least `length` characters.\n *\n * @param length - The minimum number of characters required.\n * @param message - Custom error message for too-short input.\n * @returns A validator returning the message for short input, else `undefined`.\n */\nexport const minLength =\n (length: number, message = `Must be at least ${length} characters`): Validator =>\n (value) =>\n value.length < length ? message : undefined;\n\n/** Alias of {@link minLength}. */\nexport const min = minLength;\n\n/** A map of field names to the list of validators applied to that field. */\nexport type ValidatorSchema<T> = {\n [K in keyof T]?: Validator[];\n};\n\n/** A map of field names to the first error message produced for that field. */\nexport type FieldErrors<T> = {\n [K in keyof T]?: string;\n};\n\n/**\n * The first error message a field's validators produce, or `undefined` when\n * every validator passes. Short-circuits on the first failure.\n */\nconst firstFieldError = (validators: Validator[], value: string): string | undefined => {\n for (const validate of validators) {\n const error = validate(value);\n if (error !== undefined) {\n return error;\n }\n }\n return undefined;\n};\n\n/**\n * Runs each field's validators against `values`, short-circuiting on the first\n * failing validator per field.\n *\n * @param values - The field values to validate (keyed by field name).\n * @param schema - Validators to apply per field.\n * @returns A record containing one entry per failing field; `{}` when all valid.\n */\nexport const validateFields = <T extends Record<string, string>>(\n values: T,\n schema: ValidatorSchema<T>,\n): FieldErrors<T> => {\n const errors: FieldErrors<T> = {};\n\n for (const field of Object.keys(schema) as (keyof T)[]) {\n const validators = schema[field];\n if (!validators) {\n continue;\n }\n const error = firstFieldError(validators, values[field] ?? '');\n if (error !== undefined) {\n errors[field] = error;\n }\n }\n\n return errors;\n};\n","/**\n * Discriminated-union result type for operations that can fail.\n *\n * Pure and framework-agnostic. The `ok` discriminant narrows the union so\n * consumers can safely read `data` on success or `error` on failure.\n *\n * @typeParam T - The success payload type.\n * @module forms-core/result\n */\nexport type Result<T> =\n | { ok: true; data: T }\n | {\n ok: false;\n error: string;\n /**\n * Per-field error messages, keyed by form field name, when the failure is\n * attributable to specific input(s). Lets a form highlight the exact\n * offending field(s) instead of only showing the summary `error`.\n */\n fieldErrors?: Record<string, string>;\n };\n\n/**\n * Builds a successful {@link Result} wrapping `data`.\n *\n * @typeParam T - The success payload type.\n * @param data - The value to wrap.\n * @returns `{ ok: true, data }`.\n */\nexport const ok = <T>(data: T): Result<T> => ({ ok: true, data });\n\n/**\n * Builds a failed {@link Result} wrapping an error `message`.\n *\n * @param error - A user-safe error message (never an internal/stack detail).\n * @param fieldErrors - Optional per-field messages so a form can highlight the\n * exact offending field(s). Omitted when the failure isn't field-specific.\n * @returns `{ ok: false, error, fieldErrors? }`.\n */\nexport const err = (\n error: string,\n fieldErrors?: Record<string, string>,\n): Result<never> =>\n fieldErrors ? { ok: false, error, fieldErrors } : { ok: false, error };\n","import { err, ok, type Result } from './result';\nimport { validateFields, type ValidatorSchema } from './validators';\n\n/** Generic, user-safe message returned when a handler throws. */\nconst GENERIC_ERROR = 'Something went wrong. Please try again.';\n\n/**\n * An error whose `message` is safe to show to the end user.\n *\n * Handlers throw this to surface a *specific, actionable* failure (e.g. \"a\n * product with this name already exists\") through {@link createServerAction}\n * instead of collapsing into the {@link GENERIC_ERROR}. Any error that is NOT\n * an `ActionError` is treated as unexpected: it is logged server-side and the\n * caller receives the generic message so internal details never leak.\n */\nexport class ActionError extends Error {\n /**\n * The form field this failure is attributable to (e.g. `\"name\"` for a\n * duplicate-name conflict). When set, {@link createServerAction} surfaces it\n * as `fieldErrors` so the form can highlight that exact field.\n */\n readonly field?: string;\n\n /**\n * Per-field messages when a single failure attributes to MORE than one field\n * (e.g. several duplicate SKUs across a product's variation rows). Takes\n * precedence over {@link field}; when set, {@link createServerAction} passes\n * this map straight through as `fieldErrors`.\n */\n readonly fieldErrors?: Record<string, string>;\n\n constructor(message: string, field?: string, fieldErrors?: Record<string, string>) {\n super(message);\n this.name = 'ActionError';\n this.field = field;\n this.fieldErrors = fieldErrors;\n }\n}\n\n/**\n * Wraps a handler-agnostic server action with validate-first semantics.\n *\n * The returned action:\n * 1. Runs {@link validateFields} against `input` using `schema`. On the first\n * field error it returns `err(message)` **without invoking `handler`**.\n * 2. Otherwise invokes `handler(input)` and returns `ok` with its resolved\n * value.\n * 3. Passes through the message of an {@link ActionError} thrown by `handler`\n * (a deliberate, user-safe failure), and maps any *other* exception to a\n * generic `err` message — logging the real cause server-side so production\n * failures stay diagnosable without leaking internals to the client.\n *\n * @typeParam I - The validated input shape (string fields keyed by name).\n * @typeParam O - The handler's success payload type.\n * @param schema - Per-field validators applied before the handler runs.\n * @param handler - The business logic, run only when input is valid.\n * @returns An async action that resolves to a {@link Result}.\n */\nexport const createServerAction = <I extends Record<string, string>, O>(\n schema: ValidatorSchema<I>,\n handler: (input: I) => Promise<O> | O,\n): ((input: I) => Promise<Result<O>>) => {\n return async (input: I): Promise<Result<O>> => {\n const errors = validateFields(input, schema);\n // Surface the first failing field by name so the form highlights it.\n const firstField = (Object.keys(errors) as (keyof I)[]).find(\n (field) => typeof errors[field] === 'string',\n );\n if (firstField !== undefined) {\n const message = errors[firstField] as string;\n return err(message, { [String(firstField)]: message });\n }\n\n try {\n return ok(await handler(input));\n } catch (error) {\n if (error instanceof ActionError) {\n // A multi-field map (fieldErrors) wins over the single-field shorthand so\n // one failure can highlight several inputs (e.g. duplicate variation SKUs).\n const fieldErrors =\n error.fieldErrors ?? (error.field ? { [error.field]: error.message } : undefined);\n return err(error.message, fieldErrors);\n }\n // Unexpected: log the real cause server-side (never sent to the client)\n // so prod failures are diagnosable, and return a safe generic message.\n console.error('[serverAction] handler threw an unexpected error:', error);\n return err(GENERIC_ERROR);\n }\n };\n};\n","/**\n * Brazilian document + address primitives (FUT-306).\n *\n * Masks, shape checks and check-digit validation for the two fields every\n * pt-BR form asks for — CEP and CNPJ — plus the UF list. Pure and dependency\n * free like the rest of `forms-core`, so the SAME rules run in the browser\n * (input mask + inline validation), in a route handler (before hitting a\n * lookup provider) and in a test.\n *\n * @module forms-core/br\n */\n\nimport type { Validator } from './validators';\n\n/** Brazilian federative units, for UF selects and client-side validation. */\nexport const UF_LIST = [\n 'AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO',\n 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI',\n 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO',\n] as const;\n\n/** A Brazilian federative unit. */\nexport type Uf = (typeof UF_LIST)[number];\n\n/** True when `value` is one of the 27 {@link UF_LIST} entries (case-sensitive). */\nexport function isUf(value: string): value is Uf {\n return (UF_LIST as readonly string[]).includes(value);\n}\n\n// ---------------------------------------------------------------------------\n// CEP\n// ---------------------------------------------------------------------------\n\n/** Canonical CEP shape after masking/normalization: `01310-100`. */\nexport const CEP_PATTERN = /^\\d{5}-\\d{3}$/;\n\n/** The bare digits of a CEP, capped at the 8 a CEP has. */\nexport function cepDigits(raw: string): string {\n return raw.replace(/\\D/g, '').slice(0, 8);\n}\n\n/**\n * Mask raw CEP input to `#####-###`: strips non-digits, caps at 8 digits and\n * inserts the hyphen once the sixth digit arrives. Pure — safe to run on every\n * keystroke and to normalize before matching {@link CEP_PATTERN}.\n */\nexport function formatCep(raw: string): string {\n const digits = cepDigits(raw);\n return digits.length > 5 ? `${digits.slice(0, 5)}-${digits.slice(5)}` : digits;\n}\n\n/**\n * True when `raw` holds a complete, well-formed CEP.\n *\n * Shape only: a CEP is a postal RANGE allocated by the Correios, so there is no\n * check digit to verify. Whether it actually EXISTS is answered by a lookup,\n * not by this function.\n */\nexport function isValidCep(raw: string): boolean {\n return CEP_PATTERN.test(formatCep(raw));\n}\n\n/** Empty ⇒ valid (optional field); otherwise the value must be a full CEP. */\nexport const cep =\n (message = 'CEP inválido — use o formato 00000-000.'): Validator =>\n (value) =>\n value.trim() === '' || isValidCep(value) ? undefined : message;\n\n// ---------------------------------------------------------------------------\n// CNPJ\n// ---------------------------------------------------------------------------\n\n/** Canonical CNPJ shape after masking: `12.ABC.345/01DE-35`. */\nexport const CNPJ_PATTERN = /^[0-9A-Z]{2}\\.[0-9A-Z]{3}\\.[0-9A-Z]{3}\\/[0-9A-Z]{4}-\\d{2}$/;\n\n/** How many characters a CNPJ has, check digits included. */\nconst CNPJ_LENGTH = 14;\n\n/**\n * Strip a CNPJ to its 14 significant characters, uppercased.\n *\n * Alphanumeric-aware (Receita Federal IN 2.229/2024, live in production since\n * 2026-07-06): the first 12 positions may be letters OR digits, so only\n * punctuation is removed — never letters.\n */\nexport function cnpjChars(raw: string): string {\n return raw.toUpperCase().replace(/[^0-9A-Z]/g, '').slice(0, CNPJ_LENGTH);\n}\n\n/**\n * Mask raw CNPJ input to `##.###.###/####-##`, accepting letters in the first\n * twelve positions and digits in the two check positions.\n */\nexport function formatCnpj(raw: string): string {\n const chars = cnpjChars(raw);\n const parts = [\n chars.slice(0, 2),\n chars.slice(2, 5),\n chars.slice(5, 8),\n chars.slice(8, 12),\n // The check digits are always numeric, even on an alphanumeric CNPJ.\n chars.slice(12, 14).replace(/\\D/g, ''),\n ];\n let masked = parts[0] as string;\n if (chars.length > 2) masked += `.${parts[1]}`;\n if (chars.length > 5) masked += `.${parts[2]}`;\n if (chars.length > 8) masked += `/${parts[3]}`;\n if (chars.length > 12) masked += `-${parts[4]}`;\n return masked;\n}\n\n/**\n * The mod-11 weights, right-to-left cycling 2…9. `slice(-n)` takes the tail so\n * the same table serves the 12-char first pass and the 13-char second.\n */\nconst CNPJ_WEIGHTS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] as const;\n\n/**\n * A character's numeric contribution: its ASCII code minus 48. Digits `0`–`9`\n * map to 0–9 (identical to the legacy rule, so existing CNPJs keep validating)\n * and letters `A`–`Z` map to 17–42.\n */\nfunction cnpjValue(char: string): number {\n return char.charCodeAt(0) - 48;\n}\n\n/** One mod-11 check digit over the first `base.length` characters. */\nfunction cnpjCheckDigit(base: string): number {\n const weights = CNPJ_WEIGHTS.slice(-base.length);\n const sum = [...base].reduce((acc, char, i) => acc + cnpjValue(char) * (weights[i] as number), 0);\n const remainder = sum % 11;\n return remainder < 2 ? 0 : 11 - remainder;\n}\n\n/**\n * Validate a CNPJ's check digits (mod 11), alphanumeric-aware.\n *\n * Rejects the repeated-character sequences (`00000000000000`, `AAAAAAAAAAAA35`)\n * that satisfy the arithmetic but are never issued. Returns false for anything\n * that is not exactly 14 characters with numeric check digits.\n */\nexport function isValidCnpj(raw: string): boolean {\n const chars = cnpjChars(raw);\n if (chars.length !== CNPJ_LENGTH) return false;\n // Only the 12-character root/establishment may be alphanumeric; the two check\n // digits stay numeric.\n if (!/^\\d{2}$/.test(chars.slice(12))) return false;\n if (new Set(chars).size === 1) return false;\n\n const base = chars.slice(0, 12);\n const first = cnpjCheckDigit(base);\n if (first !== Number(chars[12])) return false;\n return cnpjCheckDigit(`${base}${first}`) === Number(chars[13]);\n}\n\n/** Empty ⇒ valid (optional field); otherwise the check digits must match. */\nexport const cnpj =\n (message = 'CNPJ inválido — confira os dígitos.'): Validator =>\n (value) =>\n value.trim() === '' || isValidCnpj(value) ? undefined : message;\n\n// ---------------------------------------------------------------------------\n// Telefone\n// ---------------------------------------------------------------------------\n\n/**\n * Format a Brazilian phone as `(11) 3214-5600` / `(11) 91234-5678`.\n *\n * Both sources that hand us a phone give it as bare concatenated digits — the\n * Receita Federal's `ddd_telefone_1` and an NF-e's `<fone>` — so the same\n * formatting rule serves the CNPJ lookup and the nota importer.\n *\n * Anything that is not a 10- or 11-digit national number is returned trimmed\n * but otherwise untouched: an unrecognized phone (a foreign issuer, an extension\n * appended by the emitter's software) is still better dropped into the field\n * than discarded.\n *\n * @param raw - Digits as the source supplies them, e.g. `1132145600`.\n * @returns The formatted phone, or `null` when there is nothing to format.\n */\nexport function formatBrPhone(raw: string | null | undefined): string | null {\n const trimmed = raw?.trim();\n if (!trimmed) return null;\n const digits = trimmed.replace(/\\D/g, '');\n if (digits.length !== 10 && digits.length !== 11) return trimmed;\n const ddd = digits.slice(0, 2);\n const subscriber = digits.slice(2);\n const split = subscriber.length - 4;\n return `(${ddd}) ${subscriber.slice(0, split)}-${subscriber.slice(split)}`;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@12-apps/forms-core",
3
- "version": "1.19.0",
3
+ "version": "1.19.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
- "description": "Pure, framework-agnostic form primitives — field validators, the Result type, and createServerAction. Zero dependencies, consumed from source by @12-apps/ui and apps.",
6
+ "description": "Pure, framework-agnostic form primitives — field validators, the Result type, and createServerAction. Zero dependencies, shipped compiled so a Node consumer can import it (Node cannot strip types under node_modules).",
7
7
  "exports": {
8
- ".": "./src/index.ts"
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
9
12
  },
10
13
  "scripts": {
14
+ "build": "rm -rf dist && tsup",
11
15
  "clean": "rm -rf node_modules coverage",
12
16
  "test": "node ../../scripts/vitest-with-teardown.mjs run",
13
17
  "test:watch": "vitest watch",
@@ -16,6 +20,7 @@
16
20
  },
17
21
  "devDependencies": {
18
22
  "@12-apps/typescript-config": "^1.20.0",
23
+ "tsup": "^8.0.0",
19
24
  "typescript": "^5.9.2",
20
25
  "vitest": "^3.2.4"
21
26
  },
@@ -36,6 +41,7 @@
36
41
  "*.mjs",
37
42
  "*.md",
38
43
  "!eslint.config.js",
44
+ "!tsup.config.ts",
39
45
  "!**/__tests__/**",
40
46
  "!**/tests/**",
41
47
  "!**/*.test.*",