feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

21
frontend/node_modules/sveltekit-superforms/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Andreas Söderlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

60
frontend/node_modules/sveltekit-superforms/README.md generated vendored Normal file
View File

@@ -0,0 +1,60 @@
<p align="center">
<img src="https://github.com/ciscoheat/sveltekit-superforms/raw/main/logo.svg" width="150px" align="center" alt="Superforms logo" />
<h1 align="center">Superforms 💥</h1>
<p align="center">Making SvelteKit forms a pleasure to use!</p>
</p>
<div align="center">
<a align="center" href="https://superforms.rocks/">https://superforms.rocks/</a>
<br />
<a href="https://discord.gg/g5GHjGtU2W">Discord</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://superforms.rocks/api">API</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://superforms.rocks/faq">FAQ</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://www.npmjs.com/package/sveltekit-superforms">npm</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/ciscoheat/sveltekit-superforms/issues">Issues</a>
</div>
# Feature list
- Server- and client-side validation with your favorite validation libraries, and more to come: 💥 [Arktype](https://arktype.io/) 💥 [class-validator](https://github.com/typestack/class-validator) 💥 [Effect](https://effect.website/) 💥 [Joi](https://joi.dev/) 💥 [Superstruct](https://docs.superstructjs.org/) 💥 [TypeBox](https://github.com/sinclairzx81/typebox) 💥 [Valibot](https://valibot.dev/) 💥 [VineJS](https://vinejs.dev/) 💥 [Yup](https://github.com/jquense/yup) 💥 [Zod](https://zod.dev/) 💥 or use [JSON Schema](https://json-schema.org/) directly.
- Seamless merging of `PageData` and `ActionData` - Forget about how to combine them, just focus on your form data, always strongly typed.
- [Auto-centering and focusing](https://superforms.rocks/concepts/error-handling#usage-client) on invalid form fields.
- [Tainted form detection](https://superforms.rocks/concepts/tainted), prevents the user from losing data if navigating away from an unsaved form. Or use [snapshots](https://superforms.rocks/concepts/snapshots) to save the form state.
- Automatically coerces `FormData` into correct types, including arrays and files.
- For advanced data structures, forget about the limitations of `FormData` - Post [nested data structures](https://superforms.rocks/concepts/nested-data) like a RPC call.
- Generates [default form values](https://superforms.rocks/default-values) from many validation schemas.
- Handles [multiple forms](https://superforms.rocks/concepts/multiple-forms) on the same page.
- Works both on the server and with [single-page applications](https://superforms.rocks/concepts/spa) (SPA)!
- Convenient handling and validation of [file uploads](https://superforms.rocks/concepts/files), both on server and client and even in nested data.
- [Proxy objects](https://superforms.rocks/concepts/proxy-objects) for handling data conversions to string and back again.
- Realtime [client-side validation](https://superforms.rocks/concepts/client-validation) for the best possible UX.
- Create loading spinners easily with three [auto-updating timers](https://superforms.rocks/concepts/timers), based on human perception research.
- Hook into [a number of events](https://superforms.rocks/concepts/events) for full control over the validation data and the `ActionResult`, with a possibility to cancel the update at every step.
- Complete customization with a [huge list of options](https://superforms.rocks/api#superformform-options).
- No JavaScript required as default, but full support for [progressive enhancement](https://superforms.rocks/concepts/enhance).
- Comes with a Super Debugging Svelte Component: [SuperDebug](https://superforms.rocks/super-debug).
# Get started
Follow the Get started tutorial on the website to get a hands-on introduction to Superforms: https://superforms.rocks/get-started
You can also watch this excellent introduction video to see what's possible: https://www.youtube.com/watch?v=MiKzH3kcVfs
# Help & support
- If you're using Superforms in non-profit circumstances, support is completely free; a star on [Github](https://github.com/ciscoheat/sveltekit-superforms) is more than enough to show your appreciation. Join the [#free-support](https://discord.gg/8X9Wfb2wbz) channel on Discord and ask away!
- If you're making or aiming to make money on your project, a donation proportional to the current profit of the project or the company you work for, will give you a month of commercial support. Donate with one of the options [on the website](https://superforms.rocks/support#commercial-support), then ask in the [#commercial-support](https://discord.gg/m6hUXE4eNQ) channel on Discord.
# Contributing
General feedback, feature requests, bug reports, PR:s, are very welcome as a Github [issue](https://github.com/ciscoheat/sveltekit-superforms/issues) or on the [Discord server](https://discord.gg/g5GHjGtU2W)!
# Donating
If you appreciate the hard work behind Superforms, please support open source software with a donation.
[!["Sponsor me on Github"](https://github.com/ciscoheat/sveltekit-superforms/raw/main/github.png)](https://github.com/sponsors/ciscoheat) [!["Buy Me A Coffee"](https://github.com/ciscoheat/sveltekit-superforms/raw/main/buymeacoffee.webp)](https://www.buymeacoffee.com/ciscoheat) [!["Support me on Ko-fi"](https://github.com/ciscoheat/sveltekit-superforms/raw/main/ko-fi.png)](https://ko-fi.com/ciscoheat)

View File

@@ -0,0 +1,15 @@
/**
* Cookie configuration options. The defaults are:
* Path=/; Max-Age=120; SameSite=Strict;
*/
export interface CookieSerializeOptions {
path?: string | undefined;
maxAge?: number | undefined;
sameSite?: 'Lax' | 'Strict' | 'None';
secure?: boolean | undefined;
}
export declare function actionResult<T extends Record<string, unknown> | App.Error | string, Type extends T extends string ? 'redirect' | 'error' : 'success' | 'failure' | 'error'>(type: Type, data?: T, options?: number | {
status?: number;
message?: Type extends 'redirect' ? App.PageData['flash'] : never;
cookieOptions?: CookieSerializeOptions;
}): Response;

View File

@@ -0,0 +1,49 @@
import { json } from '@sveltejs/kit';
import { stringify } from 'devalue';
export function actionResult(type, data, options) {
function cookieData() {
if (typeof options === 'number' || !options?.message)
return '';
const extra = [
`Path=${options?.cookieOptions?.path || '/'}`,
`Max-Age=${options?.cookieOptions?.maxAge || 120}`,
`SameSite=${options?.cookieOptions?.sameSite ?? 'Strict'}`
];
if (options?.cookieOptions?.secure) {
extra.push(`Secure`);
}
return `flash=${encodeURIComponent(JSON.stringify(options.message))}; ` + extra.join('; ');
}
const status = options && typeof options !== 'number' ? options.status : options;
const result = (struct) => {
return json({ type, ...struct }, {
status: struct.status,
headers: typeof options === 'object' && options.message
? {
'Set-Cookie': cookieData()
}
: undefined
});
};
if (type == 'error') {
return result({
status: status || 500,
error: typeof data === 'string' ? { message: data } : data
});
}
else if (type == 'redirect') {
return result({
status: status || 303,
location: data
});
}
else if (type == 'failure') {
return result({
status: status || 400,
data: stringify(data)
});
}
else {
return result({ status: status || 200, data: stringify(data) });
}
}

View File

@@ -0,0 +1,42 @@
import type { JSONSchema } from '../jsonSchema/index.js';
import { type SchemaShape } from '../jsonSchema/schemaShape.js';
import type { InputConstraints } from '../jsonSchema/constraints.js';
import type { Schema, ValidationResult, Infer as InferSchema, InferIn as InferInSchema, Registry } from './typeSchema.js';
export type { Schema, ValidationIssue, ValidationResult } from './typeSchema.js';
export type Infer<T extends Schema, K extends keyof Registry = keyof Registry> = NonNullable<InferSchema<T, K>>;
export type InferIn<T extends Schema, K extends keyof Registry = keyof Registry> = NonNullable<InferInSchema<T, K>>;
export type ValidationLibrary = 'arktype' | 'classvalidator' | 'custom' | 'joi' | 'superform' | 'typebox' | 'valibot' | 'yup' | 'zod' | 'zod4' | 'vine' | 'schemasafe' | 'superstruct' | 'effect';
export type AdapterOptions<T> = {
jsonSchema?: JSONSchema;
defaults?: T;
};
export type RequiredDefaultsOptions<T> = {
defaults: T;
jsonSchema?: JSONSchema;
};
export type ClientValidationAdapter<Out, In = Out> = {
superFormValidationLibrary: ValidationLibrary;
validate: (data: unknown) => Promise<ValidationResult<Out>>;
shape?: SchemaShape;
};
type BaseValidationAdapter<Out, In = Out> = ClientValidationAdapter<Out, In> & {
jsonSchema: JSONSchema;
defaults?: Out;
constraints?: InputConstraints<Out>;
};
export type ValidationAdapter<Out, In = Out> = BaseValidationAdapter<Out, In> & {
defaults: Out;
constraints: InputConstraints<Out>;
shape: SchemaShape;
id: string;
};
/**
* If the adapter options doesn't have a "defaults" or "jsonSchema" fields,
* this is a convenient function for creating a JSON schema.
* If no transformer exist for the adapter, use RequiredDefaultsOptions.
* @see {AdapterOptions}
* @see {RequiredDefaultsOptions}
* @__NO_SIDE_EFFECTS__
*/
export declare function createJsonSchema(options: object, transformer?: () => JSONSchema): {};
export declare function createAdapter<Out, In>(adapter: BaseValidationAdapter<Out, In>, jsonSchema?: JSONSchema): ValidationAdapter<Out, In>;

View File

@@ -0,0 +1,41 @@
import { constraints as schemaConstraints } from '../jsonSchema/constraints.js';
import { defaultValues } from '../jsonSchema/schemaDefaults.js';
import { schemaShape } from '../jsonSchema/schemaShape.js';
import { schemaHash } from '../jsonSchema/schemaHash.js';
import { SuperFormError } from '../errors.js';
import { simpleSchema } from './simple-schema/index.js';
/**
* If the adapter options doesn't have a "defaults" or "jsonSchema" fields,
* this is a convenient function for creating a JSON schema.
* If no transformer exist for the adapter, use RequiredDefaultsOptions.
* @see {AdapterOptions}
* @see {RequiredDefaultsOptions}
* @__NO_SIDE_EFFECTS__
*/
export function createJsonSchema(options, transformer) {
return 'jsonSchema' in options && options.jsonSchema
? options.jsonSchema
: !transformer && 'defaults' in options && options.defaults
? simpleSchema(options.defaults)
: transformer
? /* @__PURE__ */ transformer()
: () => {
throw new SuperFormError('The "defaults" option is required for this adapter.');
};
}
/* @__NO_SIDE_EFFECTS__ */
export function createAdapter(adapter, jsonSchema) {
if (!adapter || !('superFormValidationLibrary' in adapter)) {
throw new SuperFormError('Superforms v2 requires a validation adapter for the schema. ' +
'Import one of your choice from "sveltekit-superforms/adapters" and wrap the schema with it.');
}
if (!jsonSchema)
jsonSchema = adapter.jsonSchema;
return {
...adapter,
constraints: adapter.constraints ?? schemaConstraints(jsonSchema),
defaults: adapter.defaults ?? defaultValues(jsonSchema),
shape: schemaShape(jsonSchema),
id: schemaHash(jsonSchema)
};
}

View File

@@ -0,0 +1,10 @@
import type { type } from 'arktype';
import type { JSONSchema7 } from 'json-schema';
import { type AdapterOptions, type ClientValidationAdapter, type Infer, type ValidationAdapter } from './adapters.js';
type Options = Parameters<type.Any['toJsonSchema']>[0];
export declare const arktypeToJSONSchema: <S extends type.Any>(schema: S, options?: Options) => JSONSchema7;
export declare const arktype: <T extends type.Any>(schema: T, options?: (AdapterOptions<Infer<T, "arktype">> & {
config?: Options;
}) | undefined) => ValidationAdapter<T["infer"], T["inferIn"]>;
export declare const arktypeClient: <T extends type.Any>(schema: T) => ClientValidationAdapter<T["infer"], T["inferIn"]>;
export {};

View File

@@ -0,0 +1,79 @@
import { defaultValues } from '../jsonSchema/schemaDefaults.js';
import { memoize } from '../memoize.js';
import { createAdapter } from './adapters.js';
async function modules() {
const { type } = await import(/* webpackIgnore: true */ 'arktype');
return { type };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
const defaultJSONSchemaOptions = {
fallback: {
default: (ctx) => {
if ('domain' in ctx && ctx.domain === 'bigint') {
return {
...ctx.base,
type: 'string',
format: 'bigint'
};
}
return ctx.base;
},
date: (ctx) => ({
...ctx.base,
type: 'string',
format: 'date-time',
description: ctx.after ? `after ${ctx.after}` : 'anytime'
})
}
};
/* @__NO_SIDE_EFFECTS__ */
export const arktypeToJSONSchema = (schema, options) => {
return schema.toJsonSchema({ ...defaultJSONSchemaOptions, ...options });
};
/**
* Creates defaults for ArkType schemas by filtering out undefined values.
*
* ArkType with exactOptionalPropertyTypes requires optional properties to be
* omitted rather than set to undefined.
*
* @param jsonSchema - JSON schema to generate defaults from
* @returns Default values object without undefined properties
*/
function createArktypeDefaults(jsonSchema) {
return Object.fromEntries(Object.entries(defaultValues(jsonSchema)).filter(([, value]) => value !== undefined));
}
async function _validate(schema, data) {
const { type } = await fetchModule();
const result = schema(data);
if (!(result instanceof type.errors)) {
return {
data: result,
success: true
};
}
const issues = [];
for (const error of result) {
issues.push({ message: error.problem, path: Array.from(error.path) });
}
return {
issues,
success: false
};
}
function _arktype(schema, options) {
const jsonSchema = options?.jsonSchema ?? arktypeToJSONSchema(schema, options?.config);
return createAdapter({
superFormValidationLibrary: 'arktype',
defaults: options?.defaults ?? createArktypeDefaults(jsonSchema),
jsonSchema,
validate: async (data) => _validate(schema, data)
});
}
function _arktypeClient(schema) {
return {
superFormValidationLibrary: 'arktype',
validate: async (data) => _validate(schema, data)
};
}
export const arktype = /* @__PURE__ */ memoize(_arktype);
export const arktypeClient = /* @__PURE__ */ memoize(_arktypeClient);

View File

@@ -0,0 +1,4 @@
import { type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter, type RequiredDefaultsOptions } from './adapters.js';
import type { Schema } from '@typeschema/class-validator';
export declare const classvalidator: <T extends Schema>(schema: T, options: RequiredDefaultsOptions<Infer<T, "classvalidator">>) => ValidationAdapter<Infer<T, "classvalidator">, InferIn<T, "classvalidator">>;
export declare const classvalidatorClient: <T extends Schema>(schema: T) => ClientValidationAdapter<Infer<T, "classvalidator">, InferIn<T, "classvalidator">>;

View File

@@ -0,0 +1,40 @@
import { createAdapter, createJsonSchema } from './adapters.js';
import { memoize } from '../memoize.js';
async function modules() {
const { validate } = await import(/* webpackIgnore: true */ '@typeschema/class-validator');
return { validate };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
async function validate(schema, data) {
const { validate } = await fetchModule();
const result = await validate(schema, data);
if (result.success) {
return {
data: result.data,
success: true
};
}
return {
issues: result.issues.map(({ message, path }) => ({
message,
path
})),
success: false
};
}
function _classvalidator(schema, options) {
return createAdapter({
superFormValidationLibrary: 'classvalidator',
validate: async (data) => validate(schema, data),
jsonSchema: createJsonSchema(options),
defaults: options.defaults
});
}
function _classvalidatorClient(schema) {
return {
superFormValidationLibrary: 'classvalidator',
validate: async (data) => validate(schema, data)
};
}
export const classvalidator = /* @__PURE__ */ memoize(_classvalidator);
export const classvalidatorClient = /* @__PURE__ */ memoize(_classvalidatorClient);

View File

@@ -0,0 +1,13 @@
import { Schema } from 'effect';
import type { ParseOptions } from 'effect/SchemaAST';
import type { JSONSchema as TJSONSchema } from '../jsonSchema/index.js';
import { type AdapterOptions, type ClientValidationAdapter, type Infer, type InferIn, type ValidationAdapter } from './adapters.js';
export declare const effectToJSONSchema: <A, I>(schema: Schema.Schema<A, I>) => TJSONSchema;
type AnySchema = Schema.Schema<any, any>;
export declare const effect: <T extends AnySchema>(schema: T, options?: (AdapterOptions<Infer<T, "effect">> & {
parseOptions?: ParseOptions;
}) | undefined) => ValidationAdapter<Infer<T, "effect">, InferIn<T, "effect">>;
export declare const effectClient: <T extends AnySchema>(schema: T, options?: (AdapterOptions<Infer<T, "effect">> & {
parseOptions?: ParseOptions;
}) | undefined) => ClientValidationAdapter<Infer<T, "effect">, InferIn<T, "effect">>;
export {};

View File

@@ -0,0 +1,41 @@
import { Schema, JSONSchema, Either } from 'effect';
import { ArrayFormatter } from 'effect/ParseResult';
import { createAdapter } from './adapters.js';
import { memoize } from '../memoize.js';
export const effectToJSONSchema = (schema) => {
// effect's json schema type is slightly different so we have to cast it
return JSONSchema.make(schema);
};
async function validate(schema, data, options) {
const result = Schema.decodeUnknownEither(schema, { errors: 'all' })(data, options?.parseOptions);
if (Either.isRight(result)) {
return {
data: result.right,
success: true
};
}
return {
// get rid of the _tag property
issues: ArrayFormatter.formatErrorSync(result.left).map(({ message, path }) => ({
message,
path: [...path] // path is readonly array so we have to copy it
})),
success: false
};
}
function _effect(schema, options) {
return createAdapter({
superFormValidationLibrary: 'effect',
validate: async (data) => validate(schema, data, options),
jsonSchema: options?.jsonSchema ?? effectToJSONSchema(schema),
defaults: options?.defaults
});
}
function _effectClient(schema, options) {
return {
superFormValidationLibrary: 'effect',
validate: async (data) => validate(schema, data, options)
};
}
export const effect = /* @__PURE__ */ memoize(_effect);
export const effectClient = /* @__PURE__ */ memoize(_effectClient);

View File

@@ -0,0 +1,14 @@
export type { ValidationAdapter, ClientValidationAdapter, Infer, InferIn } from './adapters.js';
export { arktype, arktypeClient } from './arktype.js';
export { classvalidator, classvalidatorClient } from './classvalidator.js';
export { effect, effectClient } from './effect.js';
export { joi, joiClient } from './joi.js';
export { superformClient } from './superform.js';
export { typebox, typeboxClient } from './typebox.js';
export { valibot, valibotClient } from './valibot.js';
export { yup, yupClient } from './yup.js';
export { zod, zodClient, type ZodValidation, type ZodObjectTypes, type ZodObjectType } from './zod.js';
export { zod as zod4, zodClient as zod4Client, type ZodValidationSchema } from './zod4.js';
export { vine, vineClient } from './vine.js';
export { schemasafe, schemasafeClient } from './schemasafe.js';
export { superstruct, superstructClient } from './superstruct.js';

View File

@@ -0,0 +1,17 @@
export { arktype, arktypeClient } from './arktype.js';
export { classvalidator, classvalidatorClient } from './classvalidator.js';
export { effect, effectClient } from './effect.js';
export { joi, joiClient } from './joi.js';
export { superformClient } from './superform.js';
export { typebox, typeboxClient } from './typebox.js';
export { valibot, valibotClient } from './valibot.js';
export { yup, yupClient } from './yup.js';
export { zod, zodClient } from './zod.js';
export { zod as zod4, zodClient as zod4Client } from './zod4.js';
export { vine, vineClient } from './vine.js';
export { schemasafe, schemasafeClient } from './schemasafe.js';
export { superstruct, superstructClient } from './superstruct.js';
/*
// Cannot use ajv due to not being ESM compatible.
export { ajv } from './ajv.js';
*/

View File

@@ -0,0 +1,28 @@
import type { JSONSchema } from '../../jsonSchema/index.js';
type Joi = Record<string, any>;
type Transformer = (schema: JSONSchema, joi: Joi, transformer?: Transformer) => JSONSchema;
/**
* Converts the supplied joi validation object into a JSON schema object,
* optionally applying a transformation.
*
* @param {JoiValidation} joi
* @param {TransformFunction} [transformer=null]
* @returns {JSONSchema}
*/
declare function convert(joi: Record<string, any>, transformer?: Transformer): JSONSchema;
declare namespace convert {
var TYPES: Record<string, Transformer>;
}
export default convert;
/**
* Joi Validation Object
* @typedef {object} JoiValidation
*/
/**
* Transformation Function - applied just before `convert()` returns and called as `function(object):object`
* @typedef {function} TransformFunction
*/
/**
* JSON Schema Object
* @typedef {object} JSONSchema
*/

View File

@@ -0,0 +1,282 @@
// Taken from https://github.com/lightsofapollo/joi-to-json-schema and converted to ESM
// TODO: Need more tests
function assert(condition, errorMessage) {
if (!condition)
throw new Error(errorMessage);
}
const TYPES = {
alternatives: (schema, joi, transformer) => {
const result = (schema.oneOf = []);
joi.matches.forEach(function (match) {
if (match.schema) {
return result.push(convert(match.schema, transformer));
}
if (!match.is) {
throw new Error('joi.when requires an "is"');
}
if (!(match.then || match.otherwise)) {
throw new Error('joi.when requires one or both of "then" and "otherwise"');
}
if (match.then) {
result.push(convert(match.then, transformer));
}
if (match.otherwise) {
result.push(convert(match.otherwise, transformer));
}
});
return schema;
},
date: (schema) => {
schema.type = 'Date';
/*
if (joi._flags.timestamp) {
schema.type = 'integer';
schema.format = 'unix-time';
return schema;
}
schema.type = 'string';
schema.format = 'date-time';
*/
return schema;
},
any: (schema) => {
delete schema.type;
//schema.type = ['array', 'boolean', 'number', 'object', 'string', 'null'];
return schema;
},
array: (schema, joi, transformer) => {
schema.type = 'array';
joi._rules?.forEach((test) => {
switch (test.name) {
case 'unique':
schema.uniqueItems = true;
break;
case 'length':
schema.minItems = schema.maxItems = test.args.limit;
break;
case 'min':
schema.minItems = test.args.limit;
break;
case 'max':
schema.maxItems = test.args.limit;
break;
}
});
if (joi.$_terms) {
/*
Ordered is not a part of the spec.
if (joi.$_terms.ordered.length) {
schema.ordered = joi.$_terms.ordered.map((item) => convert(item, transformer));
}
*/
let list;
if (joi.$_terms._inclusions.length) {
list = joi.$_terms._inclusions;
}
else if (joi.$_terms._requireds.length) {
list = joi.$_terms._requireds;
}
if (list) {
schema.items = convert(list[0], transformer);
}
}
return schema;
},
binary: (schema, joi) => {
schema.type = 'string';
schema.contentMediaType =
joi._meta.length > 0 && joi._meta[0].contentMediaType
? joi._meta[0].contentMediaType
: 'text/plain';
schema.contentEncoding = joi._flags.encoding ? joi._flags.encoding : 'binary';
return schema;
},
boolean: (schema) => {
schema.type = 'boolean';
return schema;
},
number: (schema, joi) => {
schema.type = 'number';
joi._rules?.forEach((test) => {
switch (test.name) {
case 'integer':
schema.type = 'integer';
break;
case 'less':
//schema.exclusiveMaximum = true;
//schema.maximum = test.args.limit;
schema.exclusiveMaximum = test.args.limit;
break;
case 'greater':
//schema.exclusiveMinimum = true;
//schema.minimum = test.args.limit;
schema.exclusiveMinimum = test.args.limit;
break;
case 'min':
schema.minimum = test.args.limit;
break;
case 'max':
schema.maximum = test.args.limit;
break;
case 'precision': {
let multipleOf;
if (test.args.limit && test.args.limit > 1) {
multipleOf = JSON.parse('0.' + '0'.repeat(test.args.limit - 1) + '1');
}
else {
multipleOf = 1;
}
schema.multipleOf = multipleOf;
break;
}
}
});
return schema;
},
string: (schema, joi) => {
schema.type = 'string';
joi._rules.forEach((test) => {
switch (test.name) {
case 'email':
schema.format = 'email';
break;
case 'pattern':
case 'regex': {
const arg = test.args;
const pattern = arg && arg.regex ? arg.regex : arg;
schema.pattern = String(pattern).replace(/^\//, '').replace(/\/$/, '');
break;
}
case 'min':
schema.minLength = test.args.limit;
break;
case 'max':
schema.maxLength = test.args.limit;
break;
case 'length':
schema.minLength = schema.maxLength = test.args.limit;
break;
case 'uri':
schema.format = 'uri';
break;
}
});
return schema;
},
object: (schema, joi, transformer) => {
schema.type = 'object';
schema.properties = {};
schema.additionalProperties = Boolean(joi._flags.allowUnknown || !joi._inner.children);
schema.pattern =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
joi.patterns?.map((pattern) => {
return { regex: pattern.regex, rule: convert(pattern.rule, transformer) };
}) ?? [];
if (!joi.$_terms.keys?.length) {
return schema;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
joi.$_terms.keys.forEach((property) => {
if (property.schema._flags.presence !== 'forbidden') {
if (!schema.properties)
schema.properties = {};
schema.properties[property.key] = convert(property.schema, transformer);
if (property.schema._flags.presence === 'required' ||
(property.schema._settings &&
property.schema._settings.presence === 'required' &&
property.schema._flags.presence !== 'optional')) {
schema.required = schema.required || [];
schema.required.push(property.key);
}
}
});
return schema;
}
};
/**
* Converts the supplied joi validation object into a JSON schema object,
* optionally applying a transformation.
*
* @param {JoiValidation} joi
* @param {TransformFunction} [transformer=null]
* @returns {JSONSchema}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function convert(joi, transformer) {
assert('object' === typeof joi && 'type' in joi, 'requires a joi schema object');
if (!TYPES[joi.type]) {
throw new Error(`sorry, do not know how to convert unknown joi type: "${joi.type}"`);
}
if (transformer) {
assert('function' === typeof transformer, 'transformer must be a function');
}
// JSON Schema root for this type.
const schema = {};
// Copy over the details that all schemas may have...
if (joi._description) {
schema.description = joi._description;
}
if (joi._examples && joi._examples.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema.examples = joi._examples.map((e) => e.value);
}
if (joi._examples && joi._examples.length === 1) {
schema.examples = joi._examples[0].value;
}
// Add the label as a title if it exists
if (joi._settings && joi._settings.language && joi._settings.language.label) {
schema.title = joi._settings.language.label;
}
else if (joi._flags && joi._flags.label) {
schema.title = joi._flags.label;
}
// Checking for undefined and null explicitly to allow false and 0 values
if (joi._flags && joi._flags.default !== undefined && joi._flags.default !== null) {
schema['default'] = joi._flags.default;
}
if (joi._valids && joi._valids._set && (joi._valids._set.size || joi._valids._set.length)) {
if (Array.isArray(joi.children) || !joi._flags.allowOnly) {
return {
anyOf: [
{
type: joi.type,
enum: [...joi._valids._set]
},
TYPES[joi.type](schema, joi, transformer)
]
};
}
schema['enum'] = [...joi._valids._set];
}
let result = TYPES[joi.type](schema, joi, transformer);
if (transformer) {
result = transformer(result, joi);
}
if (joi._valids?._values && joi._valids._values.size && !joi._flags.allowOnly) {
const constants = Array.from(joi._valids._values).map((v) => ({
const: v
}));
if (result.anyOf) {
result.anyOf = [...constants, ...result.anyOf];
}
else {
result = { anyOf: [...constants, result] };
}
}
return result;
}
//module.exports = convert;
convert.TYPES = TYPES;
/**
* Joi Validation Object
* @typedef {object} JoiValidation
*/
/**
* Transformation Function - applied just before `convert()` returns and called as `function(object):object`
* @typedef {function} TransformFunction
*/
/**
* JSON Schema Object
* @typedef {object} JSONSchema
*/

View File

@@ -0,0 +1,4 @@
import { type ValidationAdapter, type AdapterOptions, type ClientValidationAdapter, type Infer } from './adapters.js';
import type { ObjectSchema } from 'joi';
export declare const joi: <T extends ObjectSchema>(schema: T, options?: AdapterOptions<Infer<T, "joi">> | undefined) => ValidationAdapter<Record<string, unknown>>;
export declare const joiClient: <T extends ObjectSchema>(schema: T) => ClientValidationAdapter<Record<string, unknown>>;

View File

@@ -0,0 +1,36 @@
import { createAdapter } from './adapters.js';
import { memoize } from '../memoize.js';
import convert from './joi-to-json-schema/index.js';
async function validate(schema, data) {
const result = schema.validate(data, { abortEarly: false });
if (result.error == null) {
return {
data: result.value,
success: true
};
}
return {
issues: result.error.details.map(({ message, path }) => ({
message,
path
})),
success: false
};
}
/* @__NO_SIDE_EFFECTS__ */
function _joi(schema, options) {
return createAdapter({
superFormValidationLibrary: 'joi',
jsonSchema: options?.jsonSchema ?? convert(schema),
defaults: options?.defaults,
validate: async (data) => validate(schema, data)
});
}
function _joiClient(schema) {
return {
superFormValidationLibrary: 'joi',
validate: async (data) => validate(schema, data)
};
}
export const joi = /* @__PURE__ */ memoize(_joi);
export const joiClient = /* @__PURE__ */ memoize(_joiClient);

View File

@@ -0,0 +1,10 @@
import { type AdapterOptions, type ClientValidationAdapter, type ValidationAdapter } from './adapters.js';
import type { ValidatorOptions } from '@exodus/schemasafe';
import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
export declare const schemasafe: <T extends JSONSchema | Record<string, unknown>, Data = unknown extends FromSchema<T> ? Record<string, unknown> : FromSchema<T>, Out = [Data] extends [never] ? Record<string, unknown> : Data>(schema: T, options?: (AdapterOptions<Out> & {
descriptionAsErrors?: boolean;
config?: ValidatorOptions;
}) | undefined) => ValidationAdapter<Out>;
export declare const schemasafeClient: <T extends JSONSchema | Record<string, unknown>, Data = unknown extends FromSchema<T> ? Record<string, unknown> : FromSchema<T>, Out = [Data] extends [never] ? Record<string, unknown> : Data>(schema: T, options?: (AdapterOptions<Out> & {
config?: ValidatorOptions;
}) | undefined) => ClientValidationAdapter<Out>;

View File

@@ -0,0 +1,90 @@
import { memoize } from '../memoize.js';
import { createAdapter } from './adapters.js';
import { pathExists } from '../traversal.js';
async function modules() {
const { validator } = await import(/* webpackIgnore: true */ '@exodus/schemasafe');
return { validator };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
/*
* Adapter specificts:
* Type inference problem unless this is applied:
* https://github.com/ThomasAribart/json-schema-to-ts/blob/main/documentation/FAQs/applying-from-schema-on-generics.md
* Must duplicate validate method, otherwise the above type inference will fail.
*/
const Email = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
const defaultOptions = {
formats: {
email: (str) => Email.test(str)
},
includeErrors: true,
allErrors: true
};
async function cachedValidator(currentSchema, config) {
const { validator } = await fetchModule();
if (!cache.has(currentSchema)) {
cache.set(currentSchema, validator(currentSchema, {
...defaultOptions,
...config
}));
}
return cache.get(currentSchema);
}
function _schemasafe(schema, options) {
return createAdapter({
superFormValidationLibrary: 'schemasafe',
jsonSchema: schema,
defaults: options?.defaults,
async validate(data) {
const validator = await cachedValidator(schema, options?.config);
const isValid = validator(data);
if (isValid) {
return {
data: data,
success: true
};
}
return {
issues: (validator.errors ?? []).map(({ instanceLocation, keywordLocation }) => ({
message: options?.descriptionAsErrors
? errorDescription(schema, keywordLocation)
: keywordLocation,
path: instanceLocation.split('/').slice(1)
})),
success: false
};
}
});
}
function _schemasafeClient(schema, options) {
return {
superFormValidationLibrary: 'schemasafe',
async validate(data) {
const validator = await cachedValidator(schema, options?.config);
const isValid = validator(data);
if (isValid) {
return {
data: data,
success: true
};
}
return {
issues: (validator.errors ?? []).map(({ instanceLocation, keywordLocation }) => ({
message: keywordLocation,
path: instanceLocation.split('/').slice(1)
})),
success: false
};
}
};
}
export const schemasafe = /* @__PURE__ */ memoize(_schemasafe);
export const schemasafeClient = /* @__PURE__ */ memoize(_schemasafeClient);
const cache = new WeakMap();
function errorDescription(schema, keywordLocation) {
if (!keywordLocation.startsWith('#/'))
return keywordLocation;
const searchPath = keywordLocation.slice(2).split('/');
const path = pathExists(schema, searchPath);
return path?.parent.description ?? keywordLocation;
}

View File

@@ -0,0 +1,5 @@
import type { JSONSchema } from '../../jsonSchema/index.js';
/**
* Simple JSON Schema generator for validation libraries without introspection.
*/
export declare function simpleSchema(value: unknown): JSONSchema;

View File

@@ -0,0 +1,31 @@
/**
* Simple JSON Schema generator for validation libraries without introspection.
*/
export function simpleSchema(value) {
if (value === null || value === undefined) {
return {};
}
switch (typeof value) {
case 'object': {
if (value instanceof Date) {
return { type: 'integer', format: 'unix-time' };
}
if (Array.isArray(value)) {
const output = { type: 'array' };
output.items = value.length ? simpleSchema(value[0]) : {};
return output;
}
else {
const obj = value;
return {
type: 'object',
properties: Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, simpleSchema(value)])),
required: Object.keys(obj).filter((key) => (!obj[key] && obj[key] !== undefined && obj[key] !== null) ||
(Array.isArray(obj[key]) && !obj[key].length)),
additionalProperties: false
};
}
}
}
return { type: typeof value };
}

View File

@@ -0,0 +1,10 @@
import type { ClientValidationAdapter } from './adapters.js';
import type { MaybePromise } from '../utils.js';
export type Validators<T extends Record<string, unknown>> = {
[P in keyof T]?: T extends any ? T[P] extends Record<string, unknown> ? Validators<T[P]> : NonNullable<T[P]> extends (infer A)[] ? A extends Record<string, unknown> ? Validators<A> : Validator<T[P]> : Validator<T[P]> : never;
};
export type Validator<V> = (value?: V) => MaybePromise<string | string[] | null | undefined>;
/**
* @deprecated This adapter requires you to do error-prone type checking yourself. If possible, use one of the supported validation libraries instead.
*/
export declare const superformClient: <T extends Record<string, unknown>, T2 extends Partial<T> = Partial<T>>(schema: Validators<T2>) => ClientValidationAdapter<T>;

View File

@@ -0,0 +1,64 @@
import { traversePath, traversePaths } from '../traversal.js';
import { memoize } from '../memoize.js';
function _superform(schema) {
return {
superFormValidationLibrary: 'superform',
async validate(data) {
// Add top-level validator fields to non-existing data fields
// so they will be validated even if the field doesn't exist
if (!data || typeof data !== 'object')
data = {};
else
data = { ...data };
const newData = data;
for (const [key, value] of Object.entries(schema)) {
if (typeof value === 'function' && !(key in newData)) {
// Setting undefined fields so they will be validated based on field existance.
newData[key] = undefined;
}
}
const output = [];
function mapErrors(path, errors) {
if (!errors)
return;
if (typeof errors === 'string')
errors = [errors];
errors.forEach((message) => {
output.push({
path,
message
});
});
}
const queue = [];
traversePaths(newData, async ({ value, path }) => {
// Filter out array indices, the validator structure doesn't contain these.
const validationPath = path.filter((p) => /\D/.test(String(p)));
const maybeValidator = traversePath(schema, validationPath);
if (typeof maybeValidator?.value === 'function') {
const check = maybeValidator.value;
queue.push({ path, errors: check(value) });
}
});
const errors = await Promise.all(queue.map((check) => check.errors));
for (let i = 0; i < errors.length; i++) {
mapErrors(queue[i].path, errors[i]);
}
//console.log('Validating', newData);
//console.log(output);
return output.length
? {
success: false,
issues: output
}
: {
success: true,
data: data
};
}
};
}
/**
* @deprecated This adapter requires you to do error-prone type checking yourself. If possible, use one of the supported validation libraries instead.
*/
export const superformClient = /* @__PURE__ */ memoize(_superform);

View File

@@ -0,0 +1,6 @@
import { type ValidationAdapter, type ClientValidationAdapter, type RequiredDefaultsOptions, type Infer } from './adapters.js';
import type { Struct } from 'superstruct';
type StructObject<T extends Record<string, unknown>> = Struct<T, any>;
export declare const superstruct: <T extends StructObject<Infer<T, "superstruct">>>(schema: T, options: RequiredDefaultsOptions<Infer<T, "superstruct">>) => ValidationAdapter<Infer<T, "superstruct">>;
export declare const superstructClient: <T extends StructObject<Infer<T, "superstruct">>>(schema: T) => ClientValidationAdapter<Infer<T, "superstruct">>;
export {};

View File

@@ -0,0 +1,35 @@
import { createAdapter, createJsonSchema } from './adapters.js';
import { memoize } from '../memoize.js';
async function validate(schema, data) {
const result = schema.validate(data, { coerce: true });
if (!result[0]) {
return {
data: result[1],
success: true
};
}
const errors = result[0];
return {
success: false,
issues: errors.failures().map((error) => ({
message: error.message,
path: error.path
}))
};
}
function _superstruct(schema, options) {
return createAdapter({
superFormValidationLibrary: 'superstruct',
defaults: options.defaults,
jsonSchema: createJsonSchema(options),
validate: async (data) => validate(schema, data)
});
}
function _superstructClient(schema) {
return {
superFormValidationLibrary: 'superstruct',
validate: async (data) => validate(schema, data)
};
}
export const superstruct = /* @__PURE__ */ memoize(_superstruct);
export const superstructClient = /* @__PURE__ */ memoize(_superstructClient);

View File

@@ -0,0 +1,144 @@
import type { TSchema, Static as Static$1 } from 'typebox';
import type { type } from 'arktype';
import type { AnySchema } from 'joi';
import type { Infer as ClassValidatorInfer, InferIn as ClassValidatorInferIn, Schema as ClassValidatorSchema } from '@typeschema/class-validator';
import type { GenericSchema, GenericSchemaAsync, InferInput as Input, InferOutput as Output } from 'valibot';
import type { Schema as Schema$2, InferType } from 'yup';
import type { ZodTypeAny, input, output } from 'zod/v3';
import type { $ZodType as $Zod4Type, input as zod4Input, output as zod4Output } from 'zod/v4/core';
import type { SchemaTypes, Infer as VineInfer } from '@vinejs/vine/types';
import type { FromSchema, JSONSchema } from 'json-schema-to-ts';
import type { Struct, Infer as Infer$2 } from 'superstruct';
import type { Schema as Schema$1 } from 'effect';
type Replace<T, From, To> = NonNullable<T> extends From ? To | Exclude<T, From> : NonNullable<T> extends object ? {
[K in keyof T]: Replace<T[K], From, To>;
} : T;
type IfDefined<T> = any extends T ? never : T;
type UnknownIfNever<T> = [T] extends [never] ? unknown : T;
type Schema = {
[K in keyof Registry]: IfDefined<InferSchema<Registry[K]>>;
}[keyof Registry];
interface Resolver<TSchema = unknown> {
schema: TSchema;
input: unknown;
output: unknown;
base: unknown;
}
type InferInput<TResolver extends Resolver, TSchema> = (TResolver & {
schema: TSchema;
})['input'];
type InferOutput<TResolver extends Resolver, TSchema> = (TResolver & {
schema: TSchema;
})['output'];
type InferSchema<TResolver extends Resolver> = TResolver['base'];
type ValidationIssue = {
message: string;
path?: Array<string | number | symbol>;
};
type ValidationResult<TOutput = any> = {
success: true;
data: TOutput;
} | {
success: false;
issues: Array<ValidationIssue>;
};
interface ArkTypeResolver extends Resolver {
base: type.Any;
input: this['schema'] extends type.Any ? this['schema']['inferIn'] : never;
output: this['schema'] extends type.Any ? this['schema']['infer'] : never;
}
interface ClassValidatorResolver extends Resolver {
base: ClassValidatorSchema;
input: this['schema'] extends ClassValidatorSchema ? ClassValidatorInferIn<this['schema']> : never;
output: this['schema'] extends ClassValidatorSchema ? ClassValidatorInfer<this['schema']> : never;
}
type CustomSchema<T = any> = (data: unknown) => Promise<T> | T;
interface CustomResolver extends Resolver {
base: CustomSchema;
input: this['schema'] extends CustomSchema ? this['schema'] extends {
_def: unknown;
} ? never : keyof this['schema'] extends never ? Awaited<ReturnType<this['schema']>> : never : never;
output: this['schema'] extends CustomSchema ? this['schema'] extends {
_def: unknown;
} ? never : keyof this['schema'] extends never ? Awaited<ReturnType<this['schema']>> : never : never;
}
interface JoiResolver extends Resolver {
base: AnySchema;
}
interface TypeBoxResolver extends Resolver {
base: TSchema & {
'~kind': string;
};
input: this['schema'] extends TSchema & {
'~kind': string;
} ? Static$1<this['schema']> : never;
output: this['schema'] extends TSchema & {
'~kind': string;
} ? Static$1<this['schema']> : never;
}
interface ValibotResolver extends Resolver {
base: GenericSchema | GenericSchemaAsync;
input: this['schema'] extends GenericSchema | GenericSchemaAsync ? Input<this['schema']> : never;
output: this['schema'] extends GenericSchema | GenericSchemaAsync ? Output<this['schema']> : never;
}
interface YupResolver extends Resolver {
base: Schema$2;
input: this['schema'] extends Schema$2 ? InferType<this['schema']> : never;
output: this['schema'] extends Schema$2 ? InferType<this['schema']> : never;
}
interface ZodResolver extends Resolver {
base: ZodTypeAny;
input: this['schema'] extends ZodTypeAny ? input<this['schema']> : never;
output: this['schema'] extends ZodTypeAny ? output<this['schema']> : never;
}
interface Zod4Resolver extends Resolver {
base: $Zod4Type;
input: this['schema'] extends $Zod4Type ? zod4Input<this['schema']> : never;
output: this['schema'] extends $Zod4Type ? zod4Output<this['schema']> : never;
}
interface VineResolver extends Resolver {
base: SchemaTypes;
input: this['schema'] extends SchemaTypes ? Replace<VineInfer<this['schema']>, Date, string> : never;
output: this['schema'] extends SchemaTypes ? VineInfer<this['schema']> : never;
}
interface SchemasafeResolver<Schema extends JSONSchema, Data = FromSchema<Schema>> extends Resolver {
base: JSONSchema;
input: this['schema'] extends Schema ? Data : never;
output: this['schema'] extends Schema ? Data : never;
}
interface SuperstructResolver extends Resolver {
base: Struct<any, any>;
input: this['schema'] extends Struct<any, any> ? Infer$2<this['schema']> : never;
output: this['schema'] extends Struct<any, any> ? Infer$2<this['schema']> : never;
}
interface EffectResolver extends Resolver {
base: Schema$1.Schema<any>;
input: this['schema'] extends Schema$1.Schema<any> ? Schema$1.Schema.Encoded<this['schema']> : never;
output: this['schema'] extends Schema$1.Schema<any> ? Schema$1.Schema.Type<this['schema']> : never;
}
export type Registry = {
arktype: ArkTypeResolver;
classvalidator: ClassValidatorResolver;
custom: CustomResolver;
joi: JoiResolver;
typebox: TypeBoxResolver;
valibot: ValibotResolver;
yup: YupResolver;
zod: ZodResolver;
zod4: Zod4Resolver;
vine: VineResolver;
schemasafe: SchemasafeResolver<JSONSchema>;
superstruct: SuperstructResolver;
effect: EffectResolver;
};
type Infer<TSchema extends Schema, Keys extends keyof Registry = keyof Registry> = UnknownIfNever<TSchema extends {
_zod: unknown;
} ? IfDefined<InferOutput<Registry['zod4'], TSchema>> : {
[K in Keys]: IfDefined<InferOutput<Registry[K], TSchema>>;
}[Keys]>;
type InferIn<TSchema extends Schema, Keys extends keyof Registry = keyof Registry> = UnknownIfNever<TSchema extends {
_zod: unknown;
} ? IfDefined<InferInput<Registry['zod4'], TSchema>> : {
[K in Keys]: IfDefined<InferInput<Registry[K], TSchema>>;
}[Keys]>;
export type { Infer, InferIn, Schema, ValidationIssue, ValidationResult };

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,17 @@
import { type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
import type { TSchema } from 'typebox';
import Type from 'typebox';
/**
* Custom TypeBox type for Date for testing purposes.
*/
export declare class TDate extends Type.Base<globalThis.Date> {
Check(value: unknown): value is globalThis.Date;
Errors(value: unknown): object[];
Create(): globalThis.Date;
}
/**
* Custom TypeBox type for Date for testing purposes.
*/
export declare function Date(): TDate;
export declare const typebox: <T extends TSchema>(schema: T) => ValidationAdapter<Infer<T, "typebox">, InferIn<T, "typebox">>;
export declare const typeboxClient: <T extends TSchema>(schema: T) => ClientValidationAdapter<Infer<T, "typebox">, InferIn<T, "typebox">>;

View File

@@ -0,0 +1,69 @@
import { createAdapter } from './adapters.js';
import { memoize } from '../memoize.js';
import Type from 'typebox';
/**
* Custom TypeBox type for Date for testing purposes.
*/
export class TDate extends Type.Base {
Check(value) {
return value instanceof globalThis.Date;
}
Errors(value) {
return this.Check(value) ? [] : [{ message: 'must be Date' }];
}
Create() {
return new globalThis.Date(0);
}
}
/**
* Custom TypeBox type for Date for testing purposes.
*/
export function Date() {
return new TDate();
}
// From https://github.com/sinclairzx81/typebox/tree/ca4d771b87ee1f8e953036c95a21da7150786d3e/example/formats
const Email = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
async function modules() {
const { Compile } = await import(/* webpackIgnore: true */ 'typebox/compile');
const Format = await import(/* webpackIgnore: true */ 'typebox/format');
return { Compile, Format };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
async function validate(schema, data) {
const { Compile, Format } = await fetchModule();
if (!compiled.has(schema)) {
compiled.set(schema, Compile(schema));
}
if (!Format.Has('email')) {
Format.Set('email', (value) => Email.test(value));
}
const validator = compiled.get(schema);
const errors = [...(validator?.Errors(data) ?? [])];
if (!errors.length) {
return { success: true, data: data };
}
return {
success: false,
issues: errors.map((issue) => ({
path: issue.instancePath ? issue.instancePath.substring(1).split('/') : [],
message: issue.message
}))
};
}
function _typebox(schema) {
return createAdapter({
superFormValidationLibrary: 'typebox',
validate: async (data) => validate(schema, data),
jsonSchema: schema
});
}
function _typeboxClient(schema) {
return {
superFormValidationLibrary: 'typebox',
validate: async (data) => validate(schema, data)
};
}
export const typebox = /* @__PURE__ */ memoize(_typebox);
export const typeboxClient = /* @__PURE__ */ memoize(_typeboxClient);
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
const compiled = new WeakMap();

View File

@@ -0,0 +1,15 @@
import { type ValidationAdapter, type AdapterOptions, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
import { type GenericSchema, type GenericSchemaAsync, type Config, type GenericIssue } from 'valibot';
import { type ConversionConfig } from '@valibot/to-json-schema';
import type { JSONSchema } from '../jsonSchema/index.js';
type SupportedSchemas = GenericSchema | GenericSchemaAsync;
export declare const valibotToJSONSchema: (options: ConversionConfig & {
schema: SupportedSchemas;
}) => JSONSchema;
export declare const valibot: <T extends SupportedSchemas>(schema: T, options?: (Omit<ConversionConfig, "schema"> & AdapterOptions<Infer<T, "valibot">> & {
config?: Config<GenericIssue<unknown>>;
}) | undefined) => ValidationAdapter<Infer<T, "valibot">, InferIn<T, "valibot">>;
export declare const valibotClient: <T extends SupportedSchemas>(schema: T, options?: (Omit<ConversionConfig, "schema"> & AdapterOptions<Infer<T, "valibot">> & {
config?: Config<GenericIssue<unknown>>;
}) | undefined) => ClientValidationAdapter<Infer<T, "valibot">, InferIn<T, "valibot">>;
export {};

View File

@@ -0,0 +1,59 @@
import { createAdapter } from './adapters.js';
import { safeParseAsync } from 'valibot';
import { memoize } from '../memoize.js';
import { toJsonSchema } from '@valibot/to-json-schema';
const defaultOptions = {
ignoreActions: ['transform', 'mime_type', 'max_size', 'min_size', 'starts_with'],
overrideSchema: (context) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const type = context.valibotSchema.type;
if (type === 'date') {
return { type: 'integer', format: 'unix-time' };
}
if (type === 'bigint') {
return { type: 'string', format: 'bigint' };
}
if (type === 'file' || type === 'blob' || type === 'instance' || type === 'custom') {
return {};
}
}
};
/* @__NO_SIDE_EFFECTS__ */
export const valibotToJSONSchema = (options) => {
const { schema, ...rest } = options;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return toJsonSchema(schema, { ...defaultOptions, ...rest });
};
async function _validate(schema, data, config) {
const result = await safeParseAsync(schema, data, config);
if (result.success) {
return {
data: result.output,
success: true
};
}
return {
issues: result.issues.map(({ message, path }) => ({
message,
path: path?.map(({ key }) => key)
})),
success: false
};
}
function _valibot(schema, options = {}) {
return createAdapter({
superFormValidationLibrary: 'valibot',
validate: async (data) => _validate(schema, data, options?.config),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
jsonSchema: options?.jsonSchema ?? valibotToJSONSchema({ schema: schema, ...options }),
defaults: 'defaults' in options ? options.defaults : undefined
});
}
function _valibotClient(schema, options = {}) {
return {
superFormValidationLibrary: 'valibot',
validate: async (data) => _validate(schema, data, options?.config)
};
}
export const valibot = /* @__PURE__ */ memoize(_valibot);
export const valibotClient = /* @__PURE__ */ memoize(_valibotClient);

View File

@@ -0,0 +1,4 @@
import { type ValidationAdapter, type ClientValidationAdapter, type RequiredDefaultsOptions, type Infer, type InferIn } from './adapters.js';
import type { SchemaTypes } from '@vinejs/vine/types';
export declare const vine: <T extends SchemaTypes>(schema: T, options: RequiredDefaultsOptions<Infer<T, "vine">>) => ValidationAdapter<Infer<T, "vine">, InferIn<T, "vine">>;
export declare const vineClient: <T extends SchemaTypes>(schema: T) => ClientValidationAdapter<Infer<T, "vine">, InferIn<T, "vine">>;

View File

@@ -0,0 +1,47 @@
import { createAdapter, createJsonSchema } from './adapters.js';
import { memoize } from '../memoize.js';
async function modules() {
const { Vine, errors } = await import(/* webpackIgnore: true */ '@vinejs/vine');
return { Vine, errors };
}
const fetchModule = /* @__PURE__ */ memoize(modules);
async function validate(schema, data) {
const { Vine, errors } = await fetchModule();
try {
const output = await new Vine().validate({ schema, data });
return {
success: true,
data: output
};
}
catch (e) {
if (e instanceof errors.E_VALIDATION_ERROR) {
return {
success: false,
issues: e.messages.map((m) => ({
path: m.field.split('.'),
message: m.message
}))
};
}
else {
return { success: false, issues: [] };
}
}
}
function _vine(schema, options) {
return createAdapter({
superFormValidationLibrary: 'vine',
validate: async (data) => validate(schema, data),
jsonSchema: createJsonSchema(options),
defaults: options.defaults
});
}
function _vineClient(schema) {
return {
superFormValidationLibrary: 'vine',
validate: async (data) => validate(schema, data)
};
}
export const vine = /* @__PURE__ */ memoize(_vine);
export const vineClient = /* @__PURE__ */ memoize(_vineClient);

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const arrayConverter: Converter;
export default arrayConverter;

View File

@@ -0,0 +1,31 @@
import commonConverter from './common.js';
const arrayConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
const { innerType } = description;
if (innerType) {
const converter = converters[innerType.type];
jsonSchema.items = converter(innerType, converters);
}
description.tests.forEach((test) => {
switch (test.name) {
case 'length':
if (test.params?.length !== undefined) {
jsonSchema.minItems = jsonSchema.maxItems = Number(test.params.length);
}
break;
case 'min':
if (test.params?.min !== undefined) {
jsonSchema.minItems = Number(test.params.min);
}
break;
case 'max':
if (test.params?.max !== undefined) {
jsonSchema.maxItems = Number(test.params.max);
}
break;
}
});
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default arrayConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const booleanConverter: Converter;
export default booleanConverter;

View File

@@ -0,0 +1,7 @@
import commonConverter from './common.js';
const booleanConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default booleanConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const commonConverter: Converter;
export default commonConverter;

View File

@@ -0,0 +1,25 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const commonConverter = (description, converters) => {
const jsonSchema = {};
jsonSchema.type = description.type;
if (description.nullable) {
jsonSchema.type = [jsonSchema.type, 'null'];
}
if (description.oneOf?.length > 0) {
jsonSchema.enum = description.oneOf;
}
if (description.notOneOf?.length > 0) {
jsonSchema.not = {
enum: description.notOneOf
};
}
if (description.label) {
jsonSchema.title = description.label;
}
if (description.default !== undefined) {
// @ts-expect-error default is unknown
jsonSchema.default = description.default;
}
return jsonSchema;
};
export default commonConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const dateConverter: Converter;
export default dateConverter;

View File

@@ -0,0 +1,9 @@
import commonConverter from './common.js';
const dateConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
jsonSchema.type = 'string';
jsonSchema.format = 'date-time';
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default dateConverter;

View File

@@ -0,0 +1,4 @@
import type { AnySchema } from 'yup';
import type { JSONSchema7 } from 'json-schema';
import type { ResolveOptions } from '../types.js';
export declare function convertSchema(yupSchema: AnySchema, options?: ResolveOptions): JSONSchema7;

View File

@@ -0,0 +1,27 @@
import stringConverter from './string.js';
import numberConverter from './number.js';
import booleanConverter from './boolean.js';
import dateConverter from './date.js';
import arrayConverter from './array.js';
import objectConverter from './object.js';
import tupleConverter from './tuple.js';
import mixedConverter from './mixed.js';
import lazyConverter from './lazy.js';
export function convertSchema(yupSchema, options) {
const { converters, ...resolveOptions } = options || {};
const allConverters = {
string: stringConverter,
number: numberConverter,
boolean: booleanConverter,
date: dateConverter,
array: arrayConverter,
object: objectConverter,
tuple: tupleConverter,
mixed: mixedConverter,
lazy: lazyConverter,
...converters
};
const description = yupSchema.describe(resolveOptions);
const converter = allConverters[description.type];
return converter(description, allConverters);
}

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const lazyConverter: Converter;
export default lazyConverter;

View File

@@ -0,0 +1,8 @@
import commonConverter from './common.js';
/* lazy is kind on an intermediate type. If you call schema.describe() with any argument, even schema.describe({}) which this library does by default, then the lazy functions always try to resolve to their return types. Because we always call schema.describe({}) or schema.describe(ResolveOptions) this is mostly unused but should still be here and return an empty type if it does exist in the schema description for some reason */
const lazyConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default lazyConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const mixedConverter: Converter;
export default mixedConverter;

View File

@@ -0,0 +1,45 @@
import commonConverter from './common.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getType = (item) => {
switch (typeof item) {
case 'string':
return 'string';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
case 'object':
if (Array.isArray(item)) {
return 'array';
}
else if (item === null) {
return 'null';
}
else if (item instanceof Date) {
return 'string';
}
else {
return 'object';
}
default:
return 'null';
}
};
const mixedConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
let types = Array.isArray(description.type) ? description.type : [description.type];
types = types.filter((type) => type !== 'mixed');
if (description.oneOf?.length > 0) {
description.oneOf.forEach((item) => {
types.push(getType(item));
});
}
if (description.default !== undefined) {
types.push(getType(description.default));
}
types = types.filter((type, index, self) => self.indexOf(type) === index);
jsonSchema.type = types;
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default mixedConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const numberConverter: Converter;
export default numberConverter;

View File

@@ -0,0 +1,35 @@
import commonConverter from './common.js';
const numberConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
description.tests.forEach((test) => {
switch (test.name) {
case 'min':
if (test.params?.min !== undefined) {
jsonSchema.minimum = Number(test.params.min);
}
if (test.params?.more !== undefined) {
jsonSchema.exclusiveMinimum = Number(test.params.more);
}
break;
case 'max':
if (test.params?.max !== undefined) {
jsonSchema.maximum = Number(test.params.max);
}
if (test.params?.less !== undefined) {
jsonSchema.exclusiveMaximum = Number(test.params.less);
}
break;
case 'integer':
if (jsonSchema.type === 'number') {
jsonSchema.type = 'integer';
}
else {
// @ts-expect-error type is known
jsonSchema.type = [...jsonSchema.type, 'integer'].filter((type) => type !== 'number');
}
}
});
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default numberConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const objectConverter: Converter;
export default objectConverter;

View File

@@ -0,0 +1,25 @@
import commonConverter from './common.js';
// @ts-expect-error description is known
const objectConverter = (description, converters) => {
/* Yup automatically adds an object where each key is undefined as the deafault in its description. So objects automatically get a default :(. The developer should use jsonSchema({ default: undefined }) to remedy this */
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
const properties = {};
const required = [];
Object.keys(description.fields).forEach((fieldName) => {
const fieldDescription = description.fields[fieldName];
const converter = converters[fieldDescription.type];
properties[fieldName] = converter(fieldDescription, converters);
if (!fieldDescription.optional) {
required.push(fieldName);
}
});
if (Object.keys(properties).length > 0) {
jsonSchema.properties = properties;
}
if (Object.keys(required).length > 0) {
jsonSchema.required = required;
}
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default objectConverter;

View File

@@ -0,0 +1,4 @@
import type { Converter } from '../types.js';
export declare const uuidRegExPattern = "^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$";
declare const stringConverter: Converter;
export default stringConverter;

View File

@@ -0,0 +1,45 @@
import commonConverter from './common.js';
export const uuidRegExPattern = '^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$';
const stringConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
description.tests.forEach((test) => {
switch (test.name) {
case 'length':
if (test.params?.length !== undefined) {
jsonSchema.minLength = Number(test.params.length);
jsonSchema.maxLength = Number(test.params.length);
}
break;
case 'min':
if (test.params?.min !== undefined) {
jsonSchema.minLength = Number(test.params.min);
}
break;
case 'max':
if (test.params?.max !== undefined) {
jsonSchema.maxLength = Number(test.params.max);
}
break;
case 'matches':
if (test.params?.regex) {
jsonSchema.pattern = test.params.regex
.toString()
.replace(/^\/(.*)\/[gimusy]*$/, '$1');
}
break;
case 'email':
jsonSchema.format = 'email';
break;
case 'url':
jsonSchema.format = 'uri';
break;
case 'uuid':
jsonSchema.format = 'uuid';
jsonSchema.pattern = uuidRegExPattern;
break;
}
});
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default stringConverter;

View File

@@ -0,0 +1,3 @@
import type { Converter } from '../types.js';
declare const tupleConverter: Converter;
export default tupleConverter;

View File

@@ -0,0 +1,15 @@
import commonConverter from './common.js';
// @ts-expect-error description is known
const tupleConverter = (description, converters) => {
const jsonSchema = commonConverter(description, converters);
const meta = description.meta || {};
jsonSchema.type = 'array';
jsonSchema.items = description.innerType.map((description) => {
const converter = converters[description.type];
return converter(description, converters);
});
jsonSchema.minItems = jsonSchema.items.length;
jsonSchema.maxItems = jsonSchema.items.length;
return Object.assign(jsonSchema, meta.jsonSchema);
};
export default tupleConverter;

View File

@@ -0,0 +1,2 @@
export { convertSchema } from './converters/index.js';
export { extendSchema } from './methods/index.js';

View File

@@ -0,0 +1,2 @@
export { convertSchema } from './converters/index.js';
export { extendSchema } from './methods/index.js';

View File

@@ -0,0 +1,6 @@
type YupParams = {
addMethod: any;
Schema: any;
};
export declare function extendSchema(yup: YupParams): void;
export {};

View File

@@ -0,0 +1,24 @@
function addMethod(yup, name) {
yup.addMethod(yup.Schema, name, function (value) {
const meta = this.describe().meta || {};
return this.meta({
...meta,
jsonSchema: {
...meta.jsonSchema,
[name]: value
}
});
});
}
export function extendSchema(yup) {
addMethod(yup, 'example');
addMethod(yup, 'examples');
addMethod(yup, 'description');
yup.addMethod(yup.Schema, 'jsonSchema', function (callback) {
const meta = this.describe().meta || {};
return this.meta({
...meta,
jsonSchema: callback(meta.jsonSchema || {})
});
});
}

View File

@@ -0,0 +1,16 @@
import type { JSONSchema7 } from 'json-schema';
import type { SchemaDescription } from 'yup';
export type YupType = 'array' | 'boolean' | 'date' | 'lazy' | 'mixed' | 'number' | 'object' | 'string' | 'tuple';
export type Converters = Record<YupType, Converter>;
export type Converter = (description: SchemaDescription, converters: Converters) => JSONSchema7;
export type Meta = {
jsonSchema?: JSONSchema7;
[key: string]: any;
};
export type ResolveOptions = {
value?: unknown;
parent?: unknown;
context?: unknown;
converters?: Partial<Converters>;
};
export type JsonSchemaCallback = (jsonSchema: JSONSchema7) => JSONSchema7;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,5 @@
import { type AdapterOptions, type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
import type { AnySchema, Schema } from 'yup';
export declare function yupToJSONSchema(schema: AnySchema): import("json-schema").JSONSchema7;
export declare const yup: <T extends Schema>(schema: T, options?: AdapterOptions<Infer<T, "yup">> | undefined) => ValidationAdapter<Infer<T, "yup">, InferIn<T, "yup">>;
export declare const yupClient: <T extends Schema>(schema: T) => ClientValidationAdapter<Infer<T, "yup">, InferIn<T, "yup">>;

View File

@@ -0,0 +1,56 @@
import { createAdapter } from './adapters.js';
import { splitPath } from '../stringPath.js';
import { memoize } from '../memoize.js';
import { convertSchema } from './yup-to-json-schema/index.js';
const modules = async () => {
const { ValidationError } = await import(/* webpackIgnore: true */ 'yup');
return { ValidationError };
};
const fetchModule = /* @__PURE__ */ memoize(modules);
/* @__NO_SIDE_EFFECTS__ */
export function yupToJSONSchema(schema) {
return convertSchema(schema, {
converters: {
date: (desc, options) => {
return options.string(desc, options);
}
}
});
}
async function validate(schema, data) {
const { ValidationError } = await fetchModule();
try {
return {
success: true,
data: await schema.validate(data, { strict: true, abortEarly: false })
};
}
catch (error) {
if (!(error instanceof ValidationError))
throw error;
return {
success: false,
issues: error.inner.map((error) => ({
message: error.message,
path: error.path !== null && error.path !== undefined ? splitPath(error.path) : undefined
}))
};
}
}
/* @__NO_SIDE_EFFECTS__ */
function _yup(schema, options) {
return createAdapter({
superFormValidationLibrary: 'yup',
validate: async (data) => validate(schema, data),
jsonSchema: options?.jsonSchema ?? yupToJSONSchema(schema),
defaults: options?.defaults
});
}
function _yupClient(schema) {
return {
superFormValidationLibrary: 'yup',
validate: async (data) => validate(schema, data)
};
}
export const yup = /* @__PURE__ */ memoize(_yup);
export const yupClient = /* @__PURE__ */ memoize(_yupClient);

View File

@@ -0,0 +1,15 @@
import { type ZodErrorMap, type ZodType, type ZodTypeDef } from 'zod/v3';
import type { JSONSchema7 } from 'json-schema';
import { type AdapterOptions, type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
import { zodToJsonSchema as zodToJson, type Options } from 'zod-v3-to-json-schema';
export declare const zodToJSONSchema: (...params: Parameters<typeof zodToJson>) => JSONSchema7;
export type ZodObjectType = ZodType<Record<string, unknown>, ZodTypeDef, Record<string, unknown> | undefined>;
export type ZodObjectTypes = ZodObjectType;
export type ZodValidation<T extends ZodObjectTypes = ZodObjectTypes> = T;
export declare const zod: <T extends ZodValidation>(schema: T, options?: (AdapterOptions<Infer<T, "zod">> & {
errorMap?: ZodErrorMap;
config?: Partial<Options>;
}) | undefined) => ValidationAdapter<Infer<T, "zod">, InferIn<T, "zod">>;
export declare const zodClient: <T extends ZodValidation>(schema: T, options?: {
errorMap?: ZodErrorMap;
} | undefined) => ClientValidationAdapter<Infer<T, "zod">, InferIn<T, "zod">>;

View File

@@ -0,0 +1,45 @@
import {} from 'zod/v3';
import { createAdapter } from './adapters.js';
import { zodToJsonSchema as zodToJson } from 'zod-v3-to-json-schema';
import { memoize } from '../memoize.js';
const defaultOptions = {
dateStrategy: 'integer',
pipeStrategy: 'output',
$refStrategy: 'none'
};
/* @__NO_SIDE_EFFECTS__ */
export const zodToJSONSchema = (...params) => {
params[1] = typeof params[1] == 'object' ? { ...defaultOptions, ...params[1] } : defaultOptions;
return zodToJson(...params);
};
async function validate(schema, data, errorMap) {
const result = await schema.safeParseAsync(data, { errorMap });
if (result.success) {
return {
data: result.data,
success: true
};
}
return {
issues: result.error.issues.map(({ message, path }) => ({ message, path })),
success: false
};
}
function _zod(schema, options) {
return createAdapter({
superFormValidationLibrary: 'zod',
validate: async (data) => {
return validate(schema, data, options?.errorMap);
},
jsonSchema: options?.jsonSchema ?? zodToJSONSchema(schema, options?.config),
defaults: options?.defaults
});
}
function _zodClient(schema, options) {
return {
superFormValidationLibrary: 'zod',
validate: async (data) => validate(schema, data, options?.errorMap)
};
}
export const zod = /* @__PURE__ */ memoize(_zod);
export const zodClient = /* @__PURE__ */ memoize(_zodClient);

View File

@@ -0,0 +1,14 @@
import { type $ZodErrorMap, type $ZodType, toJSONSchema } from 'zod/v4/core';
import type { JSONSchema7 } from 'json-schema';
import { type AdapterOptions, type ValidationAdapter, type Infer, type InferIn, type ClientValidationAdapter } from './adapters.js';
type Options = NonNullable<Parameters<typeof toJSONSchema>[1]>;
export type ZodValidationSchema = $ZodType<Record<string, unknown>>;
export declare const zodToJSONSchema: <S extends ZodValidationSchema>(schema: S, options?: Options) => JSONSchema7;
export declare const zod: <T extends ZodValidationSchema>(schema: T, options?: (AdapterOptions<Infer<T, "zod4">> & {
error?: $ZodErrorMap;
config?: Options;
}) | undefined) => ValidationAdapter<Infer<T, "zod4">, InferIn<T, "zod4">>;
export declare const zodClient: <T extends ZodValidationSchema>(schema: T, options?: {
error?: $ZodErrorMap;
} | undefined) => ClientValidationAdapter<Infer<T, "zod4">, InferIn<T, "zod4">>;
export {};

View File

@@ -0,0 +1,144 @@
import { safeParseAsync, toJSONSchema, config } from 'zod/v4/core';
import { createAdapter } from './adapters.js';
import { memoize } from '../memoize.js';
const defaultJSONSchemaOptions = {
unrepresentable: 'any',
override: (ctx) => {
const def = ctx.zodSchema._zod.def;
if (def.type === 'date') {
ctx.jsonSchema.type = 'integer';
ctx.jsonSchema.format = 'unix-time';
}
else if (def.type === 'bigint') {
ctx.jsonSchema.type = 'string';
ctx.jsonSchema.format = 'bigint';
}
else if (def.type === 'pipe') {
// Handle z.stringbool() - it's a pipe from string->transform->boolean
// Colin Hacks explained: stringbool is just string -> transform -> boolean
// When io:'input', we see the string schema; when io:'output', we see boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pipeDef = def;
const inSchema = pipeDef.in;
const outSchema = pipeDef.out;
// Check if it's: string -> (transform or pipe) -> boolean
if (inSchema?._zod?.def.type === 'string') {
// Traverse through the output side (right) to find if it ends in boolean
let currentSchema = outSchema;
let isStringBool = false;
// Traverse through transforms and pipes to find boolean
while (currentSchema?._zod?.def) {
const currentDef = currentSchema._zod.def;
if (currentDef.type === 'boolean') {
isStringBool = true;
break;
}
else if (currentDef.type === 'transform') {
// Transform doesn't have a nested schema, but we can't traverse further
// Check if the transform is inside another pipe
break;
}
else if (currentDef.type === 'pipe') {
// Continue traversing the pipe
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nestedPipeDef = currentDef;
currentSchema = nestedPipeDef.out;
}
else {
break;
}
}
// Also check if outSchema directly is boolean
if (!isStringBool && outSchema?._zod?.def.type === 'boolean') {
isStringBool = true;
}
if (isStringBool) {
// Mark as stringbool so FormData parser knows to handle it as string
ctx.jsonSchema.type = 'string';
ctx.jsonSchema.format = 'stringbool';
}
}
}
else if (def.type === 'set') {
// Handle z.set() - convert to array with uniqueItems
ctx.jsonSchema.type = 'array';
ctx.jsonSchema.uniqueItems = true;
// If there's a default value, convert Set to Array
if ('default' in ctx.jsonSchema && ctx.jsonSchema.default instanceof Set) {
ctx.jsonSchema.default = Array.from(ctx.jsonSchema.default);
}
}
else if (def.type === 'map') {
// Handle z.map() - convert to array of [key, value] tuples
ctx.jsonSchema.type = 'array';
ctx.jsonSchema.format = 'map';
// If there's a default value, convert Map to Array
if ('default' in ctx.jsonSchema && ctx.jsonSchema.default instanceof Map) {
ctx.jsonSchema.default = Array.from(ctx.jsonSchema.default);
}
}
else if (def.type === 'default') {
// Handle z.default() wrapping unrepresentable types
// The default value was already serialized by Zod, which converts Set/Map to {}
// We need to get the original value and convert it properly
const innerDef = def.innerType._zod.def;
if (innerDef.type === 'set' && def.defaultValue instanceof Set) {
// Set the proper schema type for sets
ctx.jsonSchema.type = 'array';
ctx.jsonSchema.uniqueItems = true;
// Convert the default value from Set to Array
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ctx.jsonSchema.default = Array.from(def.defaultValue);
}
else if (innerDef.type === 'map' && def.defaultValue instanceof Map) {
// Set the proper schema type for maps
ctx.jsonSchema.type = 'array';
ctx.jsonSchema.format = 'map';
// Convert the default value from Map to Array of tuples
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ctx.jsonSchema.default = Array.from(def.defaultValue);
}
}
}
};
/* @__NO_SIDE_EFFECTS__ */
export const zodToJSONSchema = (schema, options) => {
return toJSONSchema(schema, { ...defaultJSONSchemaOptions, ...options });
};
async function validate(schema, data, error) {
// Use Zod's global config error map if none provided to preserve custom messages.
// Prioritize customError over localeError when both are set.
if (error === undefined) {
const zConfig = config();
error = zConfig.customError ?? zConfig.localeError;
}
const result = await safeParseAsync(schema, data, { error });
if (result.success) {
return {
data: result.data,
success: true
};
}
return {
issues: result.error.issues.map(({ message, path }) => ({ message, path })),
success: false
};
}
function _zod4(schema, options) {
return createAdapter({
superFormValidationLibrary: 'zod4',
validate: async (data) => {
return validate(schema, data, options?.error);
},
jsonSchema: options?.jsonSchema ?? zodToJSONSchema(schema, options?.config),
defaults: options?.defaults
});
}
function _zod4Client(schema, options) {
return {
superFormValidationLibrary: 'zod4',
validate: async (data) => validate(schema, data, options?.error)
};
}
export const zod = /* @__PURE__ */ memoize(_zod4);
export const zodClient = /* @__PURE__ */ memoize(_zod4Client);

View File

@@ -0,0 +1,721 @@
<svelte:options runes={false} />
<script>
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { readable, get } from 'svelte/store';
import { clipboardCopy } from './clipboardCopy.js';
let styleInit = false;
/**
* @typedef {unknown | Promise<unknown>} EncodeableData
* @typedef {import('svelte/store').Readable<EncodeableData>} EncodeableDataStore
*
* @typedef {EncodeableData | EncodeableDataStore} DebugData
*/
/**
* Data to be displayed as pretty JSON.
*
* @type {DebugData}
*/
export let data;
/**
* Controls when the component should be displayed.
*
* Default: `true`.
*/
export let display = true;
/**
* Controls when to show the HTTP status code of the current page (reflecs the status code of the last request).
*
* Default is `true`.
*/
export let status = true;
/**
* Optional label to identify the component easily.
*/
export let label = '';
/**
* Controls the maximum length of a string field of the data prop.
*
* Default is `120` characters. Set to `0` to disable trimming.
*/
export let stringTruncate = 120;
/**
* Reference to the pre element that contains the data.
*
* @type {HTMLPreElement | undefined}
*/
export let ref = undefined;
/**
* Controls if the data prop should be treated as a promise (skips promise detection when true).
*
* Default is `false`.
* @deprecated Promises are auto-detected from 1.3.0.
*/
export let promise = false;
/**
* Controls if the data prop should be treated as a plain object (skips promise and store detection when true, prevails over promise prop).
*
* Default is `false`.
*/
export let raw = false;
/**
* Enables the display of fields of the data prop that are functions.
*
* Default is `false`.
*/
export let functions = false;
/**
* Theme, which can also be customized with CSS variables:
*
* ```txt
* --sd-bg-color
* --sd-label-color
* --sd-promise-loading-color
* --sd-promise-rejected-color
* --sd-code-default
* --sd-info
* --sd-success
* --sd-redirect
* --sd-error
* --sd-code-key
* --sd-code-string
* --sd-code-date
* --sd-code-boolean
* --sd-code-number
* --sd-code-bigint
* --sd-code-null
* --sd-code-nan
* --sd-code-undefined
* --sd-code-function
* --sd-code-symbol
* --sd-code-error
* --sd-sb-width
* --sd-sb-height
* --sd-sb-track-color
* --sd-sb-track-color-focus
* --sd-sb-thumb-color
* --sd-sb-thumb-color-focus
* ```
*
* @type {"default" | "vscode"}
*/
export let theme = 'default';
///// Collapse behavior ///////////////////////////////////////////
/**
* Will show a collapse bar at the bottom of the component, that can be used to hide and show the output. Default is `false`.
* When toggled, the state is saved in session storage for all SuperDebug components on the page.
*/
export let collapsible = false;
/**
* Initial state for the collapsed component. Use together with the `collapsible` prop.
* On subsequent page loads, the session storage will determine the state of all SuperDebug components on the page.
*/
export let collapsed = false;
if (browser && collapsible) setCollapse();
/**
* @param {boolean|undefined} status
*/
function setCollapse(status = undefined) {
let data;
const route = $page.route.id ?? '';
try {
if (sessionStorage.SuperDebug) {
data = JSON.parse(sessionStorage.SuperDebug);
}
data = { collapsed: data && data.collapsed ? data.collapsed : {} };
data.collapsed[route] = status === undefined ? (data.collapsed[route] ?? collapsed) : status;
} catch {
data = {
collapsed: {
[route]: collapsed
}
};
}
if (status !== undefined) {
sessionStorage.SuperDebug = JSON.stringify(data);
}
collapsed = data.collapsed[route];
}
/**
* @type {ReturnType<typeof setTimeout> | undefined}
*/
let copied;
/**
* @param {Event} e
*/
async function copyContent(e) {
if (!e.target) return;
const parent = /** @type {HTMLElement} */ (e.target).closest('.super-debug');
if (!parent) return;
const codeEl = /** @type {HTMLPreElement} */ (parent.querySelector('.super-debug--code'));
if (!codeEl) return;
clearTimeout(copied);
await clipboardCopy(codeEl.innerText);
copied = setTimeout(() => (copied = undefined), 900);
}
/**
* @param {File} file
*/
function fileToJSON(file) {
return {
name: file.name,
size: file.size,
type: file.type,
lastModified: new Date(file.lastModified)
};
}
///////////////////////////////////////////////////////////////////
/**
* @param {unknown} json
* @returns {string}
*/
function syntaxHighlight(json) {
switch (typeof json) {
case 'function': {
return `<span class="function">[function ${json.name ?? 'unnamed'}]</span>`;
}
case 'symbol': {
return `<span class="symbol">${json.toString()}</span>`;
}
}
const encodedString = JSON.stringify(
json,
function (key, value) {
if (value === undefined) {
return '#}#undefined';
}
if (typeof this === 'object' && this[key] instanceof Date) {
// @ts-expect-error Date checking works with isNaN
return '#}D#' + (isNaN(this[key]) ? 'Invalid Date' : value);
}
if (typeof value === 'number') {
if (value == Number.POSITIVE_INFINITY) return '#}#Inf';
if (value == Number.NEGATIVE_INFINITY) return '#}#-Inf';
if (isNaN(value)) return '#}#NaN';
}
if (typeof value === 'bigint') {
return '#}BI#' + value;
}
if (typeof value === 'function' && functions) {
return '#}F#' + `[function ${value.name}]`;
}
if (value instanceof Error) {
return '#}E#' + `${value.name}: ${value.message || value.cause || '(No error message)'}`;
}
if (value instanceof Set) {
return Array.from(value);
}
if (value instanceof Map) {
return Array.from(value.entries());
}
if (
typeof this === 'object' &&
typeof this[key] == 'object' &&
this[key] &&
'toExponential' in this[key]
) {
return '#}DE#' + this[key].toString();
}
if (browser && typeof this === 'object' && this[key] instanceof File) {
return fileToJSON(this[key]);
}
if (browser && typeof this === 'object' && this[key] instanceof FileList) {
/** @type FileList */
const list = this[key];
const output = [];
for (let i = 0; i < list.length; i++) {
const file = list.item(i);
if (file) output.push(fileToJSON(file));
}
return output;
}
return value;
},
2
)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return encodedString.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
function (match) {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
match = match.slice(1, -2) + ':';
} else {
cls = 'string';
match =
stringTruncate > 0 && match.length > stringTruncate
? match.slice(0, stringTruncate / 2) +
`[..${match.length - stringTruncate}/${match.length}..]` +
match.slice(-stringTruncate / 2)
: match;
if (match == '"#}#undefined"') {
cls = 'undefined';
match = 'undefined';
} else if (match.startsWith('"#}D#')) {
cls = 'date';
match = match.slice(5, -1);
} else if (match == '"#}#NaN"') {
cls = 'nan';
match = 'NaN';
} else if (match == '"#}#Inf"') {
cls = 'nan';
match = 'Infinity';
} else if (match == '"#}#-Inf"') {
cls = 'nan';
match = '-Infinity';
} else if (match.startsWith('"#}BI#')) {
cls = 'bigint';
match = match.slice(6, -1) + 'n';
} else if (match.startsWith('"#}F#')) {
cls = 'function';
match = match.slice(5, -1);
} else if (match.startsWith('"#}E#')) {
cls = 'error';
match = match.slice(5, -1);
} else if (match.startsWith('"#}DE#')) {
cls = 'number';
match = match.slice(6, -1);
}
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
}
);
}
/**
* @param {EncodeableData} data
* @param {boolean} raw
* @param {boolean} promise
* @returns {data is Promise<unknown>}
*/
function assertPromise(data, raw, promise) {
if (raw) {
return false;
}
return (
promise ||
(typeof data === 'object' &&
data !== null &&
'then' in data &&
typeof data['then'] === 'function')
);
}
/**
* @param {DebugData} data
* @param {boolean} raw
* @returns {data is EncodeableDataStore}
*/
function assertStore(data, raw) {
if (raw) {
return false;
}
return (
typeof data === 'object' &&
data !== null &&
'subscribe' in data &&
typeof data['subscribe'] === 'function'
);
}
$: themeStyle =
theme === 'vscode'
? `
--sd-vscode-bg-color: #1f1f1f;
--sd-vscode-label-color: #cccccc;
--sd-vscode-code-default: #8c8a89;
--sd-vscode-code-key: #9cdcfe;
--sd-vscode-code-string: #ce9171;
--sd-vscode-code-number: #b5c180;
--sd-vscode-code-boolean: #4a9cd6;
--sd-vscode-code-null: #4a9cd6;
--sd-vscode-code-undefined: #4a9cd6;
--sd-vscode-code-nan: #4a9cd6;
--sd-vscode-code-symbol: #4de0c5;
--sd-vscode-sb-thumb-color: #35373a;
--sd-vscode-sb-thumb-color-focus: #4b4d50;
`
: undefined;
/** @type {import('svelte/store').Readable<EncodeableData>} */
$: debugData = assertStore(data, raw) ? data : readable(data);
</script>
{#if !styleInit}
<style>
.super-debug--absolute {
position: absolute;
}
.super-debug--top-0 {
top: 0;
}
.super-debug--inset-x-0 {
left: 0px;
right: 0px;
}
.super-debug--hidden {
height: 0;
overflow: hidden;
}
.super-debug--hidden:not(.super-debug--with-label) {
height: 1.5em;
}
.super-debug--rotated {
transform: rotate(180deg);
}
.super-debug {
--_sd-bg-color: var(--sd-bg-color, var(--sd-vscode-bg-color, rgb(30, 41, 59)));
position: relative;
background-color: var(--_sd-bg-color);
border-radius: 0.5rem;
overflow: hidden;
}
.super-debug--pre {
overflow-x: auto;
}
.super-debug--collapse {
display: block;
width: 100%;
color: rgba(255, 255, 255, 0.25);
background-color: rgba(255, 255, 255, 0.15);
padding: 5px 0;
display: flex;
justify-content: center;
border-color: transparent;
margin: 0;
padding: 3px 0;
}
.super-debug--collapse:focus {
color: #fafafa;
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--collapse:is(:hover) {
color: rgba(255, 255, 255, 0.35);
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--status {
display: flex;
padding: 1em;
padding-bottom: 0;
justify-content: space-between;
font-family:
Inconsolata, Monaco, Consolas, 'Lucida Console', 'Courier New', Courier, monospace;
}
.super-debug--right-status {
display: flex;
gap: 0.55em;
}
.super-debug--copy {
margin: 0;
padding: 0;
padding-top: 2px;
background-color: transparent;
border: 0;
color: #666;
cursor: pointer;
}
.super-debug--copy:hover {
background-color: transparent;
color: #666;
}
.super-debug--copy:focus {
background-color: transparent;
color: #666;
}
.super-debug--label {
color: var(--sd-label-color, var(--sd-vscode-label-color, white));
}
.super-debug--promise-loading {
color: var(--sd-promise-loading-color, var(--sd-vscode-promise-loading-color, #999));
}
.super-debug--promise-rejected {
color: var(--sd-promise-rejected-color, var(--sd-vscode-promise-rejected-color, #ff475d));
}
.super-debug pre {
color: var(--sd-code-default, var(--sd-vscode-code-default, #999));
background-color: var(--_sd-bg-color);
font-size: 1em;
margin-bottom: 0;
padding: 1em 0 1em 1em;
}
.super-debug--info {
color: var(--sd-info, var(--sd-vscode-info, rgb(85, 85, 255)));
}
.super-debug--success {
color: var(--sd-success, var(--sd-vscode-success, #2cd212));
}
.super-debug--redirect {
color: var(--sd-redirect, var(--sd-vscode-redirect, #03cae5));
}
.super-debug--error {
color: var(--sd-error, var(--sd-vscode-error, #ff475d));
}
.super-debug--code .key {
color: var(--sd-code-key, var(--sd-vscode-code-key, #eab308));
}
.super-debug--code .string {
color: var(--sd-code-string, var(--sd-vscode-code-string, #6ec687));
}
.super-debug--code .date {
color: var(--sd-code-date, var(--sd-vscode-code-date, #f06962));
}
.super-debug--code .boolean {
color: var(--sd-code-boolean, var(--sd-vscode-code-boolean, #79b8ff));
}
.super-debug--code .number {
color: var(--sd-code-number, var(--sd-vscode-code-number, #af77e9));
}
.super-debug--code .bigint {
color: var(--sd-code-bigint, var(--sd-vscode-code-bigint, #af77e9));
}
.super-debug--code .null {
color: var(--sd-code-null, var(--sd-vscode-code-null, #238afe));
}
.super-debug--code .nan {
color: var(--sd-code-nan, var(--sd-vscode-code-nan, #af77e9));
}
.super-debug--code .undefined {
color: var(--sd-code-undefined, var(--sd-vscode-code-undefined, #238afe));
}
.super-debug--code .function {
color: var(--sd-code-function, var(--sd-vscode-code-function, #f06962));
}
.super-debug--code .symbol {
color: var(--sd-code-symbol, var(--sd-vscode-code-symbol, #4de0c5));
}
.super-debug--code .error {
color: var(--sd-code-error, var(--sd-vscode-code-error, #ff475d));
}
.super-debug pre::-webkit-scrollbar {
width: var(--sd-sb-width, var(--sd-vscode-sb-width, 1rem));
height: var(--sd-sb-height, var(--sd-vscode-sb-height, 1rem));
}
.super-debug pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color,
var(--sd-vscode-sb-track-color, hsl(0, 0%, 40%, 0.2))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color-focus,
var(--sd-vscode-sb-track-color-focus, hsl(0, 0%, 50%, 0.2))
);
}
.super-debug pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color,
var(--sd-vscode-sb-thumb-color, hsl(217, 50%, 50%, 0.5))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color-focus,
var(--sd-vscode-sb-thumb-color-focus, hsl(217, 50%, 50%))
);
}
</style>
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
{@const init = styleInit = true}
{/if}
<!-- eslint-disable svelte/no-at-html-tags -->
{#if display}
<div
class="super-debug"
class:super-debug--collapsible={collapsible}
style={themeStyle}
dir="ltr"
>
<div
class="super-debug--status {label === ''
? 'super-debug--absolute super-debug--inset-x-0 super-debug--top-0'
: ''}"
>
<div class="super-debug--label">{label}</div>
<div class="super-debug--right-status">
<button type="button" class="super-debug--copy" on:click={copyContent}>
{#if !copied}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path
d="M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z"
/><path
d="M4.012 16.737A2.005 2.005 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1"
/></g
></svg
>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path d="M15 12v6m-3-3h6" /><rect
width="14"
height="14"
x="8"
y="8"
rx="2"
ry="2"
/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></g
></svg
>
{/if}
</button>
{#if status}
<div
class:super-debug--info={$page.status < 200}
class:super-debug--success={$page.status >= 200 && $page.status < 300}
class:super-debug--redirect={$page.status >= 300 && $page.status < 400}
class:super-debug--error={$page.status >= 400}
>
{$page.status}
</div>
{/if}
</div>
</div>
<pre
class="super-debug--pre"
class:super-debug--with-label={label}
class:super-debug--hidden={collapsed}
bind:this={ref}><code class="super-debug--code"
><slot
>{#if assertPromise($debugData, raw, promise)}{#await $debugData}<div
class="super-debug--promise-loading">Loading data...</div>{:then result}{@html syntaxHighlight(
assertStore(result, raw) ? get(result) : result
)}{:catch error}<span class="super-debug--promise-rejected">Rejected:</span
> {@html syntaxHighlight(error)}{/await}{:else}{@html syntaxHighlight(
$debugData
)}{/if}</slot
></code
></pre>
{#if collapsible}
<button
type="button"
on:click|preventDefault={() => setCollapse(!collapsed)}
class="super-debug--collapse"
aria-label="Collapse"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
class:super-debug--rotated={collapsed}
><path
fill="currentColor"
d="M4.08 11.92L12 4l7.92 7.92l-1.42 1.41l-5.5-5.5V22h-2V7.83l-5.5 5.5l-1.42-1.41M12 4h10V2H2v2h10Z"
/></svg
>
</button>
{/if}
</div>
{/if}
<!--
@component
SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
Other use cases includes debugging plain objects, promises, stores and more.
More info: https://superforms.rocks/super-debug
**Short example:**
```svelte
<script>
import SuperDebug from 'sveltekit-superforms';
import { superForm } from 'sveltekit-superforms';
export let data;
const { errors, form, enhance } = superForm(data.form);
</script>
<SuperDebug data={$form} label="My form data" />
```
-->

View File

@@ -0,0 +1,71 @@
/** @typedef {typeof __propDef.props} SuperDebugProps */
/** @typedef {typeof __propDef.events} SuperDebugEvents */
/** @typedef {typeof __propDef.slots} SuperDebugSlots */
/**
* SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
*
* Other use cases includes debugging plain objects, promises, stores and more.
*
* More info: https://superforms.rocks/super-debug
*
* **Short example:**
*
* ```svelte
* <script>
* import SuperDebug from 'sveltekit-superforms';
* import { superForm } from 'sveltekit-superforms';
*
* export let data;
*
* const { errors, form, enhance } = superForm(data.form);
* </script>
*
* <SuperDebug data={$form} label="My form data" />
* ```
*/
export default class SuperDebug extends SvelteComponentTyped<{
data: unknown;
status?: boolean | undefined;
label?: string | undefined;
display?: boolean | undefined;
stringTruncate?: number | undefined;
ref?: HTMLPreElement | undefined;
promise?: boolean | undefined;
raw?: boolean | undefined;
functions?: boolean | undefined;
theme?: "default" | "vscode" | undefined;
collapsible?: boolean | undefined;
collapsed?: boolean | undefined;
}, {
[evt: string]: CustomEvent<any>;
}, {
default: {};
}> {
}
export type SuperDebugProps = typeof __propDef.props;
export type SuperDebugEvents = typeof __propDef.events;
export type SuperDebugSlots = typeof __propDef.slots;
import { SvelteComponentTyped } from "svelte";
declare const __propDef: {
props: {
data: unknown;
status?: boolean | undefined;
label?: string | undefined;
display?: boolean | undefined;
stringTruncate?: number | undefined;
ref?: HTMLPreElement | undefined;
promise?: boolean | undefined;
raw?: boolean | undefined;
functions?: boolean | undefined;
theme?: "default" | "vscode" | undefined;
collapsible?: boolean | undefined;
collapsed?: boolean | undefined;
};
events: {
[evt: string]: CustomEvent<any>;
};
slots: {
default: {};
};
};
export {};

View File

@@ -0,0 +1,745 @@
<svelte:options runes={true} />
<script lang="ts">
import type { Readable } from 'svelte/store';
import { browser } from '$app/environment';
import { page } from '$app/state';
import { fromStore } from 'svelte/store';
import { clipboardCopy } from './clipboardCopy.js';
type EncodeableData = unknown | Promise<unknown>;
type EncodeableDataStore = Readable<EncodeableData>;
type DebugData = EncodeableData | EncodeableDataStore;
let {
data,
display = true,
status = true,
label = '',
stringTruncate = 120,
ref = $bindable(undefined),
promise = false,
raw = false,
functions = false,
theme = 'default',
collapsible = false,
collapsed = $bindable(false),
children
}: {
/**
* Data to be displayed as pretty JSON.
*
* @type {DebugData}
*/
data: DebugData;
/**
* Controls when the component should be displayed.
*
* Default: `true`.
*/
display?: boolean;
/**
* Controls when to show the HTTP status code of the current page (reflecs the status code of the last request).
*
* Default is `true`.
*/
status?: boolean;
/**
* Optional label to identify the component easily.
*/
label?: string;
/**
* Controls the maximum length of a string field of the data prop.
*
* Default is `120` characters. Set to `0` to disable trimming.
*/
stringTruncate?: number;
/**
* Reference to the pre element that contains the data.
*
* @type {HTMLPreElement | undefined}
*/
ref?: HTMLPreElement | undefined;
/**
* Controls if the data prop should be treated as a promise (skips promise detection when true).
*
* Default is `false`.
* @deprecated Promises are auto-detected from 1.3.0.
*/
promise?: boolean;
/**
* Controls if the data prop should be treated as a plain object (skips promise and store detection when true, prevails over promise prop).
*
* Default is `false`.
*/
raw?: boolean;
/**
* Enables the display of fields of the data prop that are functions.
*
* Default is `false`.
*/
functions?: boolean;
/**
* Theme, which can also be customized with CSS variables:
*
* ```txt
* --sd-bg-color
* --sd-label-color
* --sd-promise-loading-color
* --sd-promise-rejected-color
* --sd-code-default
* --sd-info
* --sd-success
* --sd-redirect
* --sd-error
* --sd-code-key
* --sd-code-string
* --sd-code-date
* --sd-code-boolean
* --sd-code-number
* --sd-code-bigint
* --sd-code-null
* --sd-code-nan
* --sd-code-undefined
* --sd-code-function
* --sd-code-symbol
* --sd-code-error
* --sd-sb-width
* --sd-sb-height
* --sd-sb-track-color
* --sd-sb-track-color-focus
* --sd-sb-thumb-color
* --sd-sb-thumb-color-focus
* ```
*
* @type {"default" | "vscode"}
*/
theme?: 'default' | 'vscode';
/**
* Will show a collapse bar at the bottom of the component, that can be used to hide and show the output. Default is `false`.
* When toggled, the state is saved in session storage for all SuperDebug components on the page.
*/
collapsible?: boolean;
/**
* Initial state for the collapsed component. Use together with the `collapsible` prop.
* On subsequent page loads, the session storage will determine the state of all SuperDebug components on the page.
*/
collapsed?: boolean;
children?: import('svelte').Snippet;
} = $props();
let copied = $state<ReturnType<typeof setTimeout> | undefined>();
let styleInit = $state(false);
///// Collapse behavior ///////////////////////////////////////////
// svelte-ignore state_referenced_locally
if (browser && collapsible) setCollapse();
/**
* @param {boolean|undefined} status
*/
function setCollapse(status: boolean | undefined = undefined) {
let data;
const route = page.route.id ?? '';
try {
if (sessionStorage.SuperDebug) {
data = JSON.parse(sessionStorage.SuperDebug);
}
data = { collapsed: data && data.collapsed ? data.collapsed : {} };
data.collapsed[route] = status === undefined ? (data.collapsed[route] ?? collapsed) : status;
} catch {
data = {
collapsed: {
[route]: collapsed
}
};
}
if (status !== undefined) {
sessionStorage.SuperDebug = JSON.stringify(data);
}
collapsed = data.collapsed[route];
}
/**
* @param {MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}} e
*/
async function copyContent(
e: MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}
) {
if (!e.target) return;
// @ts-expect-error why not use e.currentTarget here?
const parent = e.target.closest('.super-debug');
if (!parent) return;
const codeEl = parent.querySelector<HTMLPreElement>('.super-debug--code');
if (!codeEl) return;
clearTimeout(copied);
await clipboardCopy(codeEl.innerText);
copied = setTimeout(() => (copied = undefined), 900);
}
/**
* @param {File} file
*/
function fileToJSON(file: File) {
return {
name: file.name,
size: file.size,
type: file.type,
lastModified: new Date(file.lastModified)
};
}
///////////////////////////////////////////////////////////////////
/**
* @param {unknown} json
* @returns {string}
*/
function syntaxHighlight(json: unknown): string {
switch (typeof json) {
case 'function': {
return `<span class="function">[function ${json.name ?? 'unnamed'}]</span>`;
}
case 'symbol': {
return `<span class="symbol">${json.toString()}</span>`;
}
}
const encodedString = JSON.stringify(
json,
function (key, value) {
if (value === undefined) {
return '#}#undefined';
}
if (typeof this === 'object' && this[key] instanceof Date) {
// @ts-expect-error Date checking works with isNaN
return '#}D#' + (isNaN(this[key]) ? 'Invalid Date' : value);
}
if (typeof value === 'number') {
if (value == Number.POSITIVE_INFINITY) return '#}#Inf';
if (value == Number.NEGATIVE_INFINITY) return '#}#-Inf';
if (isNaN(value)) return '#}#NaN';
}
if (typeof value === 'bigint') {
return '#}BI#' + value;
}
if (typeof value === 'function' && functions) {
return '#}F#' + `[function ${value.name}]`;
}
if (value instanceof Error) {
return '#}E#' + `${value.name}: ${value.message || value.cause || '(No error message)'}`;
}
if (value instanceof Set) {
return Array.from(value);
}
if (value instanceof Map) {
return Array.from(value.entries());
}
if (
typeof this === 'object' &&
typeof this[key] == 'object' &&
this[key] &&
'toExponential' in this[key]
) {
return '#}DE#' + this[key].toString();
}
if (browser && typeof this === 'object' && this[key] instanceof File) {
return fileToJSON(this[key]);
}
if (browser && typeof this === 'object' && this[key] instanceof FileList) {
/** @type FileList */
const list: FileList = this[key];
const output = [];
for (let i = 0; i < list.length; i++) {
const file = list.item(i);
if (file) output.push(fileToJSON(file));
}
return output;
}
return value;
},
2
)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return encodedString.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
function (match) {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
match = match.slice(1, -2) + ':';
} else {
cls = 'string';
match =
stringTruncate > 0 && match.length > stringTruncate
? match.slice(0, stringTruncate / 2) +
`[..${match.length - stringTruncate}/${match.length}..]` +
match.slice(-stringTruncate / 2)
: match;
if (match == '"#}#undefined"') {
cls = 'undefined';
match = 'undefined';
} else if (match.startsWith('"#}D#')) {
cls = 'date';
match = match.slice(5, -1);
} else if (match == '"#}#NaN"') {
cls = 'nan';
match = 'NaN';
} else if (match == '"#}#Inf"') {
cls = 'nan';
match = 'Infinity';
} else if (match == '"#}#-Inf"') {
cls = 'nan';
match = '-Infinity';
} else if (match.startsWith('"#}BI#')) {
cls = 'bigint';
match = match.slice(6, -1) + 'n';
} else if (match.startsWith('"#}F#')) {
cls = 'function';
match = match.slice(5, -1);
} else if (match.startsWith('"#}E#')) {
cls = 'error';
match = match.slice(5, -1);
} else if (match.startsWith('"#}DE#')) {
cls = 'number';
match = match.slice(6, -1);
}
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
}
);
}
/**
* @param {EncodeableData} data
* @param {boolean} raw
* @param {boolean} promise
* @returns {data is Promise<unknown>}
*/
function assertPromise(
data: EncodeableData,
raw: boolean,
promise: boolean
): data is Promise<unknown> {
if (raw) {
return false;
}
return (
promise ||
(typeof data === 'object' &&
data !== null &&
'then' in data &&
typeof data['then'] === 'function')
);
}
/**
* @param {DebugData} data
* @param {boolean} raw
* @returns {data is EncodeableDataStore}
*/
function assertStore(data: DebugData, raw: boolean): data is EncodeableDataStore {
if (raw) {
return false;
}
return (
typeof data === 'object' &&
data !== null &&
'subscribe' in data &&
typeof data['subscribe'] === 'function'
);
}
const themeStyle = $derived(
theme === 'vscode'
? `
--sd-vscode-bg-color: #1f1f1f;
--sd-vscode-label-color: #cccccc;
--sd-vscode-code-default: #8c8a89;
--sd-vscode-code-key: #9cdcfe;
--sd-vscode-code-string: #ce9171;
--sd-vscode-code-number: #b5c180;
--sd-vscode-code-boolean: #4a9cd6;
--sd-vscode-code-null: #4a9cd6;
--sd-vscode-code-undefined: #4a9cd6;
--sd-vscode-code-nan: #4a9cd6;
--sd-vscode-code-symbol: #4de0c5;
--sd-vscode-sb-thumb-color: #35373a;
--sd-vscode-sb-thumb-color-focus: #4b4d50;
`
: undefined
);
const debugData = $derived(assertStore(data, raw) ? fromStore(data) : data);
</script>
{#if !styleInit}
<style>
.super-debug--absolute {
position: absolute;
}
.super-debug--top-0 {
top: 0;
}
.super-debug--inset-x-0 {
left: 0px;
right: 0px;
}
.super-debug--hidden {
height: 0;
overflow: hidden;
}
.super-debug--hidden:not(.super-debug--with-label) {
height: 1.5em;
}
.super-debug--rotated {
transform: rotate(180deg);
}
.super-debug {
--_sd-bg-color: var(--sd-bg-color, var(--sd-vscode-bg-color, rgb(30, 41, 59)));
position: relative;
background-color: var(--_sd-bg-color);
border-radius: 0.5rem;
overflow: hidden;
}
.super-debug--pre {
overflow-x: auto;
}
.super-debug--collapse {
display: block;
width: 100%;
color: rgba(255, 255, 255, 0.25);
background-color: rgba(255, 255, 255, 0.15);
padding: 5px 0;
display: flex;
justify-content: center;
border-color: transparent;
margin: 0;
padding: 3px 0;
}
.super-debug--collapse:focus {
color: #fafafa;
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--collapse:is(:hover) {
color: rgba(255, 255, 255, 0.35);
background-color: rgba(255, 255, 255, 0.25);
}
.super-debug--status {
display: flex;
padding: 1em;
padding-bottom: 0;
justify-content: space-between;
font-family:
Inconsolata, Monaco, Consolas, 'Lucida Console', 'Courier New', Courier, monospace;
}
.super-debug--right-status {
display: flex;
gap: 0.55em;
}
.super-debug--copy {
margin: 0;
padding: 0;
padding-top: 2px;
background-color: transparent;
border: 0;
color: #666;
cursor: pointer;
}
.super-debug--copy:hover {
background-color: transparent;
color: #666;
}
.super-debug--copy:focus {
background-color: transparent;
color: #666;
}
.super-debug--label {
color: var(--sd-label-color, var(--sd-vscode-label-color, white));
}
.super-debug--promise-loading {
color: var(--sd-promise-loading-color, var(--sd-vscode-promise-loading-color, #999));
}
.super-debug--promise-rejected {
color: var(--sd-promise-rejected-color, var(--sd-vscode-promise-rejected-color, #ff475d));
}
.super-debug pre {
color: var(--sd-code-default, var(--sd-vscode-code-default, #999));
background-color: var(--_sd-bg-color);
font-size: 1em;
margin-bottom: 0;
padding: 1em 0 1em 1em;
}
.super-debug--info {
color: var(--sd-info, var(--sd-vscode-info, rgb(85, 85, 255)));
}
.super-debug--success {
color: var(--sd-success, var(--sd-vscode-success, #2cd212));
}
.super-debug--redirect {
color: var(--sd-redirect, var(--sd-vscode-redirect, #03cae5));
}
.super-debug--error {
color: var(--sd-error, var(--sd-vscode-error, #ff475d));
}
.super-debug--code .key {
color: var(--sd-code-key, var(--sd-vscode-code-key, #eab308));
}
.super-debug--code .string {
color: var(--sd-code-string, var(--sd-vscode-code-string, #6ec687));
}
.super-debug--code .date {
color: var(--sd-code-date, var(--sd-vscode-code-date, #f06962));
}
.super-debug--code .boolean {
color: var(--sd-code-boolean, var(--sd-vscode-code-boolean, #79b8ff));
}
.super-debug--code .number {
color: var(--sd-code-number, var(--sd-vscode-code-number, #af77e9));
}
.super-debug--code .bigint {
color: var(--sd-code-bigint, var(--sd-vscode-code-bigint, #af77e9));
}
.super-debug--code .null {
color: var(--sd-code-null, var(--sd-vscode-code-null, #238afe));
}
.super-debug--code .nan {
color: var(--sd-code-nan, var(--sd-vscode-code-nan, #af77e9));
}
.super-debug--code .undefined {
color: var(--sd-code-undefined, var(--sd-vscode-code-undefined, #238afe));
}
.super-debug--code .function {
color: var(--sd-code-function, var(--sd-vscode-code-function, #f06962));
}
.super-debug--code .symbol {
color: var(--sd-code-symbol, var(--sd-vscode-code-symbol, #4de0c5));
}
.super-debug--code .error {
color: var(--sd-code-error, var(--sd-vscode-code-error, #ff475d));
}
.super-debug pre::-webkit-scrollbar {
width: var(--sd-sb-width, var(--sd-vscode-sb-width, 1rem));
height: var(--sd-sb-height, var(--sd-vscode-sb-height, 1rem));
}
.super-debug pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color,
var(--sd-vscode-sb-track-color, hsl(0, 0%, 40%, 0.2))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-track {
border-radius: 12px;
background-color: var(
--sd-sb-track-color-focus,
var(--sd-vscode-sb-track-color-focus, hsl(0, 0%, 50%, 0.2))
);
}
.super-debug pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color,
var(--sd-vscode-sb-thumb-color, hsl(217, 50%, 50%, 0.5))
);
}
.super-debug:is(:focus-within, :hover) pre::-webkit-scrollbar-thumb {
border-radius: 12px;
background-color: var(
--sd-sb-thumb-color-focus,
var(--sd-vscode-sb-thumb-color-focus, hsl(217, 50%, 50%))
);
}
</style>
{/if}
<!-- eslint-disable svelte/no-at-html-tags -->
{#if display}
<div
class="super-debug"
class:super-debug--collapsible={collapsible}
style={themeStyle}
dir="ltr"
>
<div
class="super-debug--status {label === ''
? 'super-debug--absolute super-debug--inset-x-0 super-debug--top-0'
: ''}"
>
<div class="super-debug--label">{label}</div>
<div class="super-debug--right-status">
<button type="button" class="super-debug--copy" onclick={copyContent}>
{#if !copied}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path
d="M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z"
/><path
d="M4.012 16.737A2.005 2.005 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1"
/></g
></svg
>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"
><g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
><path d="M15 12v6m-3-3h6" /><rect
width="14"
height="14"
x="8"
y="8"
rx="2"
ry="2"
/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></g
></svg
>
{/if}
</button>
{#if status}
<div
class:super-debug--info={page.status < 200}
class:super-debug--success={page.status >= 200 && page.status < 300}
class:super-debug--redirect={page.status >= 300 && page.status < 400}
class:super-debug--error={page.status >= 400}
>
{page.status}
</div>
{/if}
</div>
</div>
<pre
class="super-debug--pre"
class:super-debug--with-label={label}
class:super-debug--hidden={collapsed}
bind:this={ref}><code class="super-debug--code"
>{#if children}{@render children()}{:else if assertPromise(debugData, raw, promise)}{#await debugData}<div
class="super-debug--promise-loading">Loading data...</div>{:then result}{@html syntaxHighlight(
assertStore(result, raw) ? fromStore(result) : result
)}{:catch error}<span class="super-debug--promise-rejected">Rejected:</span
> {@html syntaxHighlight(error)}{/await}{:else}{@html syntaxHighlight(
debugData
)}{/if}</code
></pre>
{#if collapsible}
<button
type="button"
onclick={(e) => {
e.preventDefault();
setCollapse(!collapsed);
}}
class="super-debug--collapse"
aria-label="Collapse"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
class:super-debug--rotated={collapsed}
><path
fill="currentColor"
d="M4.08 11.92L12 4l7.92 7.92l-1.42 1.41l-5.5-5.5V22h-2V7.83l-5.5 5.5l-1.42-1.41M12 4h10V2H2v2h10Z"
/></svg
>
</button>
{/if}
</div>
{/if}
<!--
@component
SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
Other use cases includes debugging plain objects, promises, stores and more.
More info: https://superforms.rocks/super-debug
This is the rune (Svelte 5) version of SuperDebug, which will be default in the next major release.
**Short example:**
```svelte
<script>
import SuperDebug from 'sveltekit-superforms/SuperDebug.svelte';
import { superForm } from 'sveltekit-superforms';
const { data } = $props();
const { errors, form, enhance } = superForm(data.form);
</script>
<SuperDebug data={$form} label="My form data" />
```
-->

View File

@@ -0,0 +1,144 @@
import { SvelteComponentTyped } from "svelte";
import type { Readable } from 'svelte/store';
type EncodeableData = unknown | Promise<unknown>;
type EncodeableDataStore = Readable<EncodeableData>;
type DebugData = EncodeableData | EncodeableDataStore;
type $$ComponentProps = {
/**
* Data to be displayed as pretty JSON.
*
* @type {DebugData}
*/
data: DebugData;
/**
* Controls when the component should be displayed.
*
* Default: `true`.
*/
display?: boolean;
/**
* Controls when to show the HTTP status code of the current page (reflecs the status code of the last request).
*
* Default is `true`.
*/
status?: boolean;
/**
* Optional label to identify the component easily.
*/
label?: string;
/**
* Controls the maximum length of a string field of the data prop.
*
* Default is `120` characters. Set to `0` to disable trimming.
*/
stringTruncate?: number;
/**
* Reference to the pre element that contains the data.
*
* @type {HTMLPreElement | undefined}
*/
ref?: HTMLPreElement | undefined;
/**
* Controls if the data prop should be treated as a promise (skips promise detection when true).
*
* Default is `false`.
* @deprecated Promises are auto-detected from 1.3.0.
*/
promise?: boolean;
/**
* Controls if the data prop should be treated as a plain object (skips promise and store detection when true, prevails over promise prop).
*
* Default is `false`.
*/
raw?: boolean;
/**
* Enables the display of fields of the data prop that are functions.
*
* Default is `false`.
*/
functions?: boolean;
/**
* Theme, which can also be customized with CSS variables:
*
* ```txt
* --sd-bg-color
* --sd-label-color
* --sd-promise-loading-color
* --sd-promise-rejected-color
* --sd-code-default
* --sd-info
* --sd-success
* --sd-redirect
* --sd-error
* --sd-code-key
* --sd-code-string
* --sd-code-date
* --sd-code-boolean
* --sd-code-number
* --sd-code-bigint
* --sd-code-null
* --sd-code-nan
* --sd-code-undefined
* --sd-code-function
* --sd-code-symbol
* --sd-code-error
* --sd-sb-width
* --sd-sb-height
* --sd-sb-track-color
* --sd-sb-track-color-focus
* --sd-sb-thumb-color
* --sd-sb-thumb-color-focus
* ```
*
* @type {"default" | "vscode"}
*/
theme?: 'default' | 'vscode';
/**
* Will show a collapse bar at the bottom of the component, that can be used to hide and show the output. Default is `false`.
* When toggled, the state is saved in session storage for all SuperDebug components on the page.
*/
collapsible?: boolean;
/**
* Initial state for the collapsed component. Use together with the `collapsible` prop.
* On subsequent page loads, the session storage will determine the state of all SuperDebug components on the page.
*/
collapsed?: boolean;
children?: import('svelte').Snippet;
};
declare const __propDef: {
props: $$ComponentProps;
events: {
[evt: string]: CustomEvent<any>;
};
slots: {};
};
export type SuperDebugRunedProps = typeof __propDef.props;
export type SuperDebugRunedEvents = typeof __propDef.events;
export type SuperDebugRunedSlots = typeof __propDef.slots;
/**
* SuperDebug is a debugging component that gives you colorized and nicely formatted output for any data structure, usually $form.
*
* Other use cases includes debugging plain objects, promises, stores and more.
*
* More info: https://superforms.rocks/super-debug
*
* This is the rune (Svelte 5) version of SuperDebug, which will be default in the next major release.
*
* **Short example:**
*
* ```svelte
* <script>
* import SuperDebug from 'sveltekit-superforms/SuperDebug.svelte';
* import { superForm } from 'sveltekit-superforms';
*
* const { data } = $props();
*
* const { errors, form, enhance } = superForm(data.form);
* </script>
*
* <SuperDebug data={$form} label="My form data" />
* ```
*/
export default class SuperDebugRuned extends SvelteComponentTyped<SuperDebugRunedProps, SuperDebugRunedEvents, SuperDebugRunedSlots> {
}
export {};

View File

@@ -0,0 +1,4 @@
/**
* @param {string} text
*/
export function clipboardCopy(text: string): Promise<void>;

View File

@@ -0,0 +1,69 @@
/*! clipboard-copy. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
function makeError() {
return new DOMException('The request is not allowed', 'NotAllowedError');
}
/**
* @param {string} text
*/
async function copyClipboardApi(text) {
// Use the Async Clipboard API when available. Requires a secure browsing
// context (i.e. HTTPS)
if (!navigator.clipboard) {
throw makeError();
}
return navigator.clipboard.writeText(text);
}
/**
* @param {string} text
*/
async function copyExecCommand(text) {
// Put the text to copy into a <span>
const span = document.createElement('span');
span.textContent = text;
// Preserve consecutive spaces and newlines
span.style.whiteSpace = 'pre';
span.style.webkitUserSelect = 'auto';
span.style.userSelect = 'all';
// Add the <span> to the page
document.body.appendChild(span);
// Make a selection object representing the range of text selected by the user
const selection = window.getSelection();
const range = window.document.createRange();
selection?.removeAllRanges();
range.selectNode(span);
selection?.addRange(range);
// Copy text to the clipboard
let success = false;
try {
success = window.document.execCommand('copy');
} finally {
// Cleanup
selection?.removeAllRanges();
window.document.body.removeChild(span);
}
if (!success) throw makeError();
}
/**
* @param {string} text
*/
export async function clipboardCopy(text) {
try {
await copyClipboardApi(text);
} catch (err) {
// ...Otherwise, use document.execCommand() fallback
try {
await copyExecCommand(text);
} catch (err2) {
throw err2 || err || makeError();
}
}
}

View File

@@ -0,0 +1,3 @@
import type { ValidationErrors } from './index.js';
export declare function updateCustomValidity(validityEl: HTMLElement, errors: string[] | undefined): Promise<void>;
export declare function setCustomValidityForm(formElement: HTMLFormElement, errors: ValidationErrors<Record<string, unknown>>): void;

View File

@@ -0,0 +1,34 @@
import { splitPath } from '../stringPath.js';
import { traversePath } from '../traversal.js';
const noCustomValidityDataAttribute = 'noCustomValidity';
export async function updateCustomValidity(validityEl, errors) {
// Always reset validity, in case it has been validated on the server.
if ('setCustomValidity' in validityEl) {
validityEl.setCustomValidity('');
}
if (noCustomValidityDataAttribute in validityEl.dataset)
return;
setCustomValidity(validityEl, errors);
}
export function setCustomValidityForm(formElement, errors) {
for (const el of formElement.querySelectorAll('input,select,textarea,button')) {
if (('dataset' in el && noCustomValidityDataAttribute in el.dataset) || !el.name) {
continue;
}
const path = traversePath(errors, splitPath(el.name));
const error = path && typeof path.value === 'object' && '_errors' in path.value
? path.value._errors
: path?.value;
setCustomValidity(el, error);
if (error)
return;
}
}
function setCustomValidity(el, errors) {
if (!('setCustomValidity' in el))
return;
const message = errors && errors.length ? errors.join('\n') : '';
el.setCustomValidity(message);
if (message)
el.reportValidity();
}

View File

@@ -0,0 +1,10 @@
export declare const isElementInViewport: (el: Element, topOffset?: number) => boolean;
export declare const scrollToAndCenter: (el: Element, offset?: number, behavior?: ScrollBehavior) => void;
/**
* Information about a HTML element, for determining when to display errors.
*/
export declare function inputInfo(el: EventTarget | null): {
immediate: boolean;
multiple: boolean;
file: boolean;
};

View File

@@ -0,0 +1,29 @@
// https://stackoverflow.com/a/7557433/70894
export const isElementInViewport = (el, topOffset = 0) => {
const rect = el.getBoundingClientRect();
return (rect.top >= topOffset &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) /* or $(window).height() */ &&
rect.right <=
(window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */);
};
// https://stackoverflow.com/a/36499256/70894
export const scrollToAndCenter = (el, offset = 1.125, behavior = 'smooth') => {
const elementRect = el.getBoundingClientRect();
const absoluteElementTop = elementRect.top + window.pageYOffset;
const top = absoluteElementTop - window.innerHeight / (2 * offset);
window.scrollTo({ left: 0, top, behavior });
};
const immediateInputTypes = ['checkbox', 'radio', 'range', 'file'];
/**
* Information about a HTML element, for determining when to display errors.
*/
export function inputInfo(el) {
const immediate = !!el &&
(el instanceof HTMLSelectElement ||
(el instanceof HTMLInputElement && immediateInputTypes.includes(el.type)));
const multiple = !!el && el instanceof HTMLSelectElement && el.multiple;
const file = !!el && el instanceof HTMLInputElement && el.type == 'file';
return { immediate, multiple, file };
}

View File

@@ -0,0 +1,3 @@
import type { FormOptions } from './superForm.js';
export declare function cancelFlash<T extends Record<string, unknown>, M>(options: FormOptions<T, M>): void;
export declare function shouldSyncFlash<T extends Record<string, unknown>, M>(options: FormOptions<T, M>): boolean | undefined;

View File

@@ -0,0 +1,13 @@
import { browser } from '$app/environment';
export function cancelFlash(options) {
if (!options.flashMessage || !browser)
return;
if (!shouldSyncFlash(options))
return;
document.cookie = `flash=; Max-Age=0; Path=${options.flashMessage.cookiePath ?? '/'};`;
}
export function shouldSyncFlash(options) {
if (!options.flashMessage || !browser)
return false;
return options.syncFlashMessage;
}

View File

@@ -0,0 +1,19 @@
import type { Writable } from 'svelte/store';
import type { FormOptions } from './superForm.js';
/**
* @DCI-context
*/
export declare function Form<T extends Record<string, unknown>, M>(formElement: HTMLFormElement, timers: {
submitting: Writable<boolean>;
delayed: Writable<boolean>;
timeout: Writable<boolean>;
}, options: FormOptions<T, M>): {
submitting(): void;
completed: (opts: {
cancelled: boolean;
clearAll?: boolean;
}) => void;
scrollToFirstError(): void;
isSubmitting: () => boolean;
};
export declare const scrollToFirstError: <T extends Record<string, unknown>, M>(Form: HTMLFormElement, options: FormOptions<T, M>) => Promise<void>;

View File

@@ -0,0 +1,159 @@
import { afterNavigate } from '$app/navigation';
import { onDestroy, tick } from 'svelte';
import { isElementInViewport, scrollToAndCenter } from './elements.js';
var FetchStatus;
(function (FetchStatus) {
FetchStatus[FetchStatus["Idle"] = 0] = "Idle";
FetchStatus[FetchStatus["Submitting"] = 1] = "Submitting";
FetchStatus[FetchStatus["Delayed"] = 2] = "Delayed";
FetchStatus[FetchStatus["Timeout"] = 3] = "Timeout";
})(FetchStatus || (FetchStatus = {}));
const activeTimers = new Set();
//let _initialized = false;
/**
* @DCI-context
*/
export function Form(formElement, timers, options) {
let state = FetchStatus.Idle;
let delayedTimeout, timeoutTimeout;
//#region Timers
const Timers = activeTimers;
// https://www.nngroup.com/articles/response-times-3-important-limits/
function Timers_start() {
Timers_clear();
Timers_setState(state != FetchStatus.Delayed ? FetchStatus.Submitting : FetchStatus.Delayed);
delayedTimeout = window.setTimeout(() => {
if (delayedTimeout && state == FetchStatus.Submitting)
Timers_setState(FetchStatus.Delayed);
}, options.delayMs);
timeoutTimeout = window.setTimeout(() => {
if (timeoutTimeout && state == FetchStatus.Delayed)
Timers_setState(FetchStatus.Timeout);
}, options.timeoutMs);
Timers.add(Timers_clear);
}
/**
* Clear timers and set state to Idle.
*/
function Timers_clear() {
clearTimeout(delayedTimeout);
clearTimeout(timeoutTimeout);
delayedTimeout = timeoutTimeout = 0;
Timers.delete(Timers_clear);
Timers_setState(FetchStatus.Idle);
}
function Timers_clearAll() {
Timers.forEach((t) => t());
Timers.clear();
}
function Timers_setState(s) {
state = s;
timers.submitting.set(state >= FetchStatus.Submitting);
timers.delayed.set(state >= FetchStatus.Delayed);
timers.timeout.set(state >= FetchStatus.Timeout);
}
//#endregion
//#region ErrorTextEvents
const ErrorTextEvents = formElement;
function ErrorTextEvents__selectText(e) {
const target = e.target;
if (options.selectErrorText)
target.select();
}
function ErrorTextEvents_addErrorTextListeners() {
if (!options.selectErrorText)
return;
ErrorTextEvents.querySelectorAll('input').forEach((el) => {
el.addEventListener('invalid', ErrorTextEvents__selectText);
});
}
function ErrorTextEvents_removeErrorTextListeners() {
if (!options.selectErrorText)
return;
ErrorTextEvents.querySelectorAll('input').forEach((el) => el.removeEventListener('invalid', ErrorTextEvents__selectText));
}
//#endregion
//#region Form
const Form = formElement;
//#endregion
{
ErrorTextEvents_addErrorTextListeners();
const completed = (opts) => {
if (!opts.clearAll)
Timers_clear();
else
Timers_clearAll();
if (!opts.cancelled)
setTimeout(() => scrollToFirstError(Form, options), 1);
};
onDestroy(() => {
ErrorTextEvents_removeErrorTextListeners();
completed({ cancelled: true });
});
afterNavigate(() => {
ErrorTextEvents_removeErrorTextListeners();
completed({ cancelled: false });
});
return {
submitting() {
Timers_start();
},
completed,
scrollToFirstError() {
setTimeout(() => scrollToFirstError(Form, options), 1);
},
isSubmitting: () => state === FetchStatus.Submitting || state === FetchStatus.Delayed
};
}
}
export const scrollToFirstError = async (Form, options) => {
if (options.scrollToError == 'off')
return;
const selector = options.errorSelector;
if (!selector)
return;
// Wait for form to update with errors
await tick();
// Scroll to first form message, if not visible
let el;
el = Form.querySelector(selector);
if (!el)
return;
// Find underlying element if it is a FormGroup element
el = el.querySelector(selector) ?? el;
const nav = options.stickyNavbar
? document.querySelector(options.stickyNavbar)
: null;
if (typeof options.scrollToError != 'string') {
el.scrollIntoView(options.scrollToError);
}
else if (!isElementInViewport(el, nav?.offsetHeight ?? 0)) {
scrollToAndCenter(el, undefined, options.scrollToError);
}
function Form_shouldAutoFocus(userAgent) {
if (typeof options.autoFocusOnError === 'boolean')
return options.autoFocusOnError;
else
return !/iPhone|iPad|iPod|Android/i.test(userAgent);
}
// Don't focus on the element if on mobile, it will open the keyboard
// and probably hide the error message.
if (!Form_shouldAutoFocus(navigator.userAgent))
return;
let focusEl;
focusEl = el;
if (!['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA'].includes(focusEl.tagName)) {
focusEl = focusEl.querySelector('input:not([type="hidden"]):not(.flatpickr-input), select, textarea');
}
if (focusEl) {
try {
focusEl.focus({ preventScroll: true });
if (options.selectErrorText && focusEl.tagName == 'INPUT') {
focusEl.select();
}
}
catch {
// Some hidden inputs like from flatpickr cannot be focused.
}
}
};

View File

@@ -0,0 +1,9 @@
export { superForm } from './superForm.js';
export { intProxy, numberProxy, booleanProxy, dateProxy, fieldProxy, formFieldProxy, stringProxy, arrayProxy, fileProxy, fileFieldProxy, filesProxy, filesFieldProxy, type FieldProxy, type ArrayProxy, type FormFieldProxy } from './proxies.js';
export { defaults, defaultValues } from '../defaults.js';
export { actionResult } from '../actionResult.js';
export { schemaShape } from '../jsonSchema/schemaShape.js';
export { superValidate, message, setMessage, setError, withFiles, removeFiles, fail, type SuperValidated, type SuperValidateOptions, type TaintedFields, type ValidationErrors } from '../superValidate.js';
export type { Infer, InferIn, Schema } from '../adapters/adapters.js';
export type { FormResult, FormOptions, SuperForm, SuperFormData, SuperFormErrors, SuperFormEventList, SuperFormEvents, SuperFormSnapshot, ValidateOptions, TaintOption, ChangeEvent } from './superForm.js';
export { type FormPath, type FormPathLeaves, type FormPathLeavesWithErrors, type FormPathArrays, type FormPathType } from '../stringPath.js';

View File

@@ -0,0 +1,12 @@
// Backwards compatibility, everything should be imported from top-level in v2.
export { superForm } from './superForm.js';
export { intProxy, numberProxy, booleanProxy, dateProxy, fieldProxy, formFieldProxy, stringProxy, arrayProxy, fileProxy, fileFieldProxy, filesProxy, filesFieldProxy } from './proxies.js';
/////////////////////////////////////////////////////////////////////
// Duplicated from server/index.ts,
// because "server" path cannot be imported on client.
export { defaults, defaultValues } from '../defaults.js';
export { actionResult } from '../actionResult.js';
export { schemaShape } from '../jsonSchema/schemaShape.js';
export { superValidate, message, setMessage, setError, withFiles, removeFiles, fail } from '../superValidate.js';
// Exporting from stringPath also, for convenience in components.
export {} from '../stringPath.js';

View File

@@ -0,0 +1,93 @@
import { type Readable, type Updater, type Writable } from 'svelte/store';
import type { InputConstraint } from '../jsonSchema/constraints.js';
import { type FormPath, type FormPathLeaves, type FormPathType } from '../stringPath.js';
import type { FormPathArrays } from '../stringPath.js';
import type { SuperForm, TaintOption } from './superForm.js';
import type { IsAny, Prettify } from '../utils.js';
export type ProxyOptions = {
taint?: TaintOption;
};
type FormPaths<T extends Record<string, unknown>, Type = any> = FormPath<T, Type> | FormPathLeaves<T, Type>;
type CorrectProxyType<In, Out, T extends Record<string, unknown>, Path extends FormPaths<T>> = NonNullable<FormPathType<T, Path>> extends In ? Writable<Out> : never;
type PathType<Type, T, Path extends string> = IsAny<Type> extends true ? FormPathType<T, Path> : Type;
type Nullable<T extends Record<string, unknown>, Path extends FormPaths<T> | FormPathArrays<T> | FormPathLeaves<T>> = null extends FormPathType<T, Path> ? null : never;
type Optional<T extends Record<string, unknown>, Path extends FormPaths<T> | FormPathArrays<T> | FormPathLeaves<T>> = [undefined] extends [FormPathType<T, Path>] ? undefined : never;
type DefaultOptions = {
trueStringValue: string;
dateFormat: 'date' | 'datetime' | 'time' | 'date-utc' | 'datetime-utc' | 'time-utc' | 'date-local' | 'datetime-local' | 'time-local' | 'iso';
delimiter?: '.' | ',';
empty?: 'null' | 'undefined' | 'zero';
initiallyEmptyIfZero?: boolean;
taint?: TaintOption;
step: number;
};
export declare function booleanProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options?: Prettify<Pick<DefaultOptions, 'trueStringValue' | 'empty' | 'taint'>>): CorrectProxyType<boolean, string, T, Path>;
export declare function intProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options?: Prettify<Pick<DefaultOptions, 'empty' | 'initiallyEmptyIfZero' | 'taint'>>): CorrectProxyType<number, string, T, Path>;
export declare function numberProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options?: Prettify<Pick<DefaultOptions, 'empty' | 'delimiter' | 'initiallyEmptyIfZero' | 'taint'>>): CorrectProxyType<number, string, T, Path>;
export declare function dateProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options?: {
format?: DefaultOptions['dateFormat'];
empty?: Exclude<DefaultOptions['empty'], 'zero'>;
taint?: TaintOption;
step?: number;
}): CorrectProxyType<Date, string, T, Path>;
export declare function stringProxy<T extends Record<string, unknown>, Path extends FormPaths<T>>(form: Writable<T> | SuperForm<T>, path: Path, options: {
empty: NonNullable<Exclude<DefaultOptions['empty'], 'zero'>>;
taint?: TaintOption;
}): Writable<string>;
export declare function fileFieldProxy<T extends Record<string, unknown>, Path extends FormPathLeaves<T, File>>(form: SuperForm<T>, path: Path, options?: ProxyOptions & {
empty?: 'null' | 'undefined';
}): FormFieldProxy<FileList | File | Nullable<T, Path> | Optional<T, Path>, Path>;
export declare function fileProxy<T extends Record<string, unknown>, Path extends FormPathLeaves<T, File>>(form: Writable<T> | SuperForm<T>, path: Path, options?: ProxyOptions & {
empty?: 'null' | 'undefined';
}): {
subscribe(this: void, run: (value: FileList) => void): import("svelte/store").Unsubscriber;
set(this: void, file: FileList | File | Nullable<T, Path> | Optional<T, Path>): void;
update(this: void): never;
};
export declare function filesFieldProxy<T extends Record<string, unknown>, Path extends FormPathArrays<T, File[]>>(form: SuperForm<T>, path: Path, options?: ProxyOptions): {
values: {
subscribe(run: (value: FileList) => void): import("svelte/store").Unsubscriber;
set(files: FileList | File[] | Nullable<T, Path> | Optional<T, Path>): void;
update(updater: Updater<File[] | Nullable<T, Path> | Optional<T, Path>>): void;
};
path: Path;
errors: Writable<string[] | undefined>;
valueErrors: Writable<ValueErrors>;
};
export declare function filesProxy<T extends Record<string, unknown>, Path extends FormPathArrays<T, File[]>>(form: Writable<T> | SuperForm<T>, path: Path, options?: ProxyOptions & {
empty?: 'null' | 'undefined';
}): {
subscribe(run: (value: FileList) => void): import("svelte/store").Unsubscriber;
set(files: FileList | File[] | Nullable<T, Path> | Optional<T, Path>): void;
update(updater: Updater<File[] | Nullable<T, Path> | Optional<T, Path>>): void;
};
type ValueErrors = any[];
export type ArrayProxy<T, Path = string, Errors = ValueErrors, ExtraValues = never> = {
path: Path;
values: Writable<(T[] & unknown[]) | ExtraValues>;
errors: Writable<string[] | undefined>;
valueErrors: Writable<Errors>;
};
export declare function arrayProxy<T extends Record<string, unknown>, Path extends FormPathArrays<T, ArrType>, ArrType = any>(superForm: SuperForm<T>, path: Path, options?: {
taint?: TaintOption;
}): ArrayProxy<FormPathType<T, Path> extends (infer U)[] ? U : never, Path>;
export type FormFieldProxy<T, Path = string> = {
path: Path;
value: SuperFieldProxy<T>;
errors: Writable<string[] | undefined>;
constraints: Writable<InputConstraint | undefined>;
tainted: Writable<boolean | undefined>;
};
export declare function formFieldProxy<T extends Record<string, unknown>, Path extends FormPathLeaves<T, Type>, Type = any>(superForm: SuperForm<T>, path: Path, options?: ProxyOptions): FormFieldProxy<PathType<Type, T, Path>, Path>;
type SuperFieldProxy<T> = {
subscribe: Readable<T>['subscribe'];
set(this: void, value: T, options?: {
taint?: TaintOption;
}): void;
update(this: void, updater: Updater<T>, options?: {
taint?: TaintOption;
}): void;
};
export type FieldProxy<T> = Writable<T>;
export declare function fieldProxy<T extends Record<string, unknown>, Path extends FormPaths<T, Type>, Type = any>(form: Writable<T> | SuperForm<T>, path: Path, options?: ProxyOptions): FieldProxy<PathType<Type, T, Path>>;
export {};

View File

@@ -0,0 +1,502 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { derived, get, writable } from 'svelte/store';
import { SuperFormError } from '../errors.js';
import { pathExists, traversePath } from '../traversal.js';
import { splitPath } from '../stringPath.js';
import { browser } from '$app/environment';
const defaultOptions = {
trueStringValue: 'true',
dateFormat: 'iso',
step: 60
};
///// Proxy functions ///////////////////////////////////////////////
export function booleanProxy(form, path, options) {
return _stringProxy(form, path, 'boolean', {
...defaultOptions,
...options
});
}
export function intProxy(form, path, options) {
return _stringProxy(form, path, 'int', {
...defaultOptions,
...options
});
}
export function numberProxy(form, path, options) {
return _stringProxy(form, path, 'number', {
...defaultOptions,
...options
});
}
export function dateProxy(form, path, options) {
return _stringProxy(form, path, 'date', {
...defaultOptions,
dateFormat: options?.format ?? 'iso',
empty: options?.empty,
step: options?.step ?? 60
});
}
export function stringProxy(form, path, options) {
return _stringProxy(form, path, 'string', {
...defaultOptions,
...options
});
}
export function fileFieldProxy(form, path, options) {
const fileField = fileProxy(form, path, options);
const formField = formFieldProxy(form, path, options);
return { ...formField, value: fileField };
}
export function fileProxy(form, path, options) {
const formFile = fieldProxy(form, path, options);
const fileProxy = writable(browser ? new DataTransfer().files : {});
let initialized = false;
let initialValue;
formFile.subscribe((file) => {
if (!browser)
return;
if (!initialized) {
initialValue = options?.empty ? (options.empty === 'undefined' ? undefined : null) : file;
initialized = true;
}
const dt = new DataTransfer();
if (file instanceof File)
dt.items.add(file);
fileProxy.set(dt.files);
});
const fileStore = {
subscribe(run) {
return fileProxy.subscribe(run);
},
set(file) {
if (!browser)
return;
if (!file) {
const dt = new DataTransfer();
fileProxy.set(dt.files);
formFile.set(file);
}
else if (file instanceof File) {
const dt = new DataTransfer();
dt.items.add(file);
fileProxy.set(dt.files);
formFile.set(file);
}
else if (file instanceof FileList) {
fileProxy.set(file);
if (file.length > 0)
formFile.set(file.item(0));
else
formFile.set(initialValue);
}
},
update() {
throw new SuperFormError('You cannot update a fileProxy, only set it.');
}
};
return fileStore;
}
export function filesFieldProxy(form, path, options) {
const filesStore = filesProxy(form, path, options);
const arrayField = arrayProxy(form, path, options);
return { ...arrayField, values: filesStore };
}
export function filesProxy(form, path, options) {
const formFiles = fieldProxy(form, path, options);
const filesProxy = writable(browser ? new DataTransfer().files : {});
let initialized = false;
let initialValue;
formFiles.subscribe((files) => {
if (!browser)
return;
if (!initialized) {
initialValue = options?.empty ? (options.empty === 'undefined' ? undefined : null) : files;
initialized = true;
}
const dt = new DataTransfer();
if (Array.isArray(files)) {
if (files.length && files.every((f) => !f)) {
formFiles.set([]);
return;
}
files.filter((f) => f instanceof File).forEach((file) => dt.items.add(file));
}
filesProxy.set(dt.files);
});
const filesStore = {
subscribe(run) {
return filesProxy.subscribe(run);
},
set(files) {
if (!browser)
return;
if (!(files instanceof FileList)) {
const dt = new DataTransfer();
if (Array.isArray(files))
files.forEach((file) => {
if (file instanceof File)
dt.items.add(file);
});
filesProxy.set(dt.files);
formFiles.set(files);
}
else {
if (files.length > 0) {
const output = [];
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file)
output.push(file);
}
filesProxy.set(files);
formFiles.set(output);
}
else {
formFiles.set(initialValue);
}
}
},
update(updater) {
filesStore.set(updater(get(formFiles)));
}
};
return filesStore;
}
///// Implementation ////////////////////////////////////////////////
/**
* Creates a string store that will pass its value to a field in the form.
* @param form The form
* @param field Form field
* @param type 'number' | 'int' | 'boolean'
*/
function _stringProxy(form, path, type, options) {
function toValue(value) {
if (!value && options.empty !== undefined) {
return options.empty === 'null' ? null : options.empty === 'zero' ? 0 : undefined;
}
if (typeof value === 'number') {
value = value.toString();
}
if (typeof value !== 'string') {
// Can be undefined due to Proxy in Svelte 5
value = '';
}
const stringValue = value;
if (type == 'string')
return stringValue;
else if (type == 'boolean')
return !!stringValue;
else if (type == 'date') {
if (stringValue.indexOf('-') === -1) {
const utc = options.dateFormat.indexOf('utc') >= 0;
const date = utc ? UTCDate(new Date()) : localDate(new Date());
return new Date(date + 'T' + stringValue + (utc ? 'Z' : ''));
}
else
return new Date(stringValue);
}
const numberToConvert = options.delimiter
? stringValue.replace(options.delimiter, '.')
: stringValue;
let num;
if (numberToConvert === '' && options.empty == 'zero')
num = 0;
else if (type == 'number')
num = parseFloat(numberToConvert);
else
num = parseInt(numberToConvert, 10);
return num;
}
const isSuper = isSuperForm(form, options);
const realProxy = isSuper
? superFieldProxy(form, path, { taint: options.taint })
: fieldProxy(form, path);
let updatedValue = null;
let initialized = false;
const proxy = derived(realProxy, (value) => {
if (!initialized) {
initialized = true;
if (options.initiallyEmptyIfZero && !value)
return '';
}
// Prevent proxy updating itself
if (updatedValue !== null) {
const current = updatedValue;
updatedValue = null;
return current;
}
if (value === undefined || value === null)
return '';
if (type == 'string') {
return value;
}
else if (type == 'int' || type == 'number') {
if (value === '') {
// Special case for empty string values in number proxies
// Set the value to 0, to conform to the type.
realProxy.set(0, isSuper ? { taint: false } : undefined);
}
if (typeof value === 'number' && isNaN(value))
return '';
return String(value);
}
else if (type == 'date') {
const date = typeof value === 'string' || typeof value === 'number' ? new Date(value) : value;
if (isNaN(date))
return '';
switch (options.dateFormat) {
case 'iso':
return date.toISOString();
case 'date':
return date.toISOString().slice(0, 10);
case 'datetime':
return date.toISOString().slice(0, options.step % 60 ? 19 : 16);
case 'time':
return date.toISOString().slice(11, options.step % 60 ? 19 : 16);
case 'date-utc':
return UTCDate(date);
case 'datetime-utc':
return UTCDate(date) + 'T' + UTCTime(date, options.step);
case 'time-utc':
return UTCTime(date, options.step);
case 'date-local':
return localDate(date);
case 'datetime-local':
return localDate(date) + 'T' + localTime(date, options.step);
case 'time-local':
return localTime(date, options.step);
}
}
else {
// boolean
return value ? options.trueStringValue : '';
}
});
return {
subscribe: proxy.subscribe,
set(val) {
updatedValue = val;
const newValue = toValue(updatedValue);
realProxy.set(newValue);
},
update(updater) {
realProxy.update((f) => {
updatedValue = updater(String(f));
const newValue = toValue(updatedValue);
return newValue;
});
}
};
}
export function arrayProxy(superForm, path, options) {
const formErrors = fieldProxy(superForm.errors, `${path}`);
const onlyFieldErrors = derived(formErrors, ($errors) => {
const output = [];
for (const key in $errors) {
if (key == '_errors')
continue;
output[key] = $errors[key];
}
return output;
});
function updateArrayErrors(errors, value) {
for (const key in errors) {
if (key == '_errors')
continue;
errors[key] = undefined;
}
if (value !== undefined) {
for (const key in value) {
errors[key] = value[key];
}
}
return errors;
}
const fieldErrors = {
subscribe: onlyFieldErrors.subscribe,
update(upd) {
formErrors.update(($errors) =>
// @ts-expect-error Type is correct
updateArrayErrors($errors, upd($errors)));
},
set(value) {
// @ts-expect-error Type is correct
formErrors.update(($errors) => updateArrayErrors($errors, value));
}
};
const values = superFieldProxy(superForm, path, options);
// If array is shortened, delete all keys above length
// in errors, so they won't be kept if the array is lengthened again.
let lastLength = Array.isArray(get(values)) ? get(values).length : 0;
values.subscribe(($values) => {
const currentLength = Array.isArray($values) ? $values.length : 0;
if (currentLength < lastLength) {
superForm.errors.update(($errors) => {
const node = pathExists($errors, splitPath(path));
if (!node)
return $errors;
for (const key in node.value) {
if (Number(key) < currentLength)
continue;
delete node.value[key];
}
return $errors;
}, { force: true });
}
lastLength = currentLength;
});
return {
path,
values: values,
errors: fieldProxy(superForm.errors, `${path}._errors`),
valueErrors: fieldErrors
};
}
export function formFieldProxy(superForm, path, options) {
const path2 = splitPath(path);
// Filter out array indices, the constraints structure doesn't contain these.
const constraintsPath = path2.filter((p) => /\D/.test(String(p))).join('.');
const taintedProxy = derived(superForm.tainted, ($tainted) => {
if (!$tainted)
return $tainted;
const taintedPath = traversePath($tainted, path2);
return taintedPath ? taintedPath.value : undefined;
});
const tainted = {
subscribe: taintedProxy.subscribe,
update(upd) {
superForm.tainted.update(($tainted) => {
if (!$tainted)
$tainted = {};
const output = traversePath($tainted, path2, (path) => {
if (!path.value)
path.parent[path.key] = {};
return path.parent[path.key];
});
if (output)
output.parent[output.key] = upd(output.value);
return $tainted;
});
},
set(value) {
superForm.tainted.update(($tainted) => {
if (!$tainted)
$tainted = {};
const output = traversePath($tainted, path2, (path) => {
if (!path.value)
path.parent[path.key] = {};
return path.parent[path.key];
});
if (output)
output.parent[output.key] = value;
return $tainted;
});
}
};
return {
path,
value: superFieldProxy(superForm, path, options),
errors: fieldProxy(superForm.errors, path),
constraints: fieldProxy(superForm.constraints, constraintsPath),
tainted
};
}
function updateProxyField(obj, path, updater) {
const output = traversePath(obj, path, ({ parent, key, value }) => {
if (value === undefined)
parent[key] = /\D/.test(key) ? {} : [];
return parent[key];
});
if (output) {
const newValue = updater(output.value);
output.parent[output.key] = newValue;
}
return obj;
}
function superFieldProxy(superForm, path, baseOptions) {
const form = superForm.form;
const path2 = splitPath(path);
const proxy = derived(form, ($form) => {
const data = traversePath($form, path2);
return data?.value;
});
return {
subscribe(...params) {
const unsub = proxy.subscribe(...params);
return () => unsub();
},
update(upd, options) {
form.update((data) => updateProxyField(data, path2, upd), options ?? baseOptions);
},
set(value, options) {
form.update((data) => updateProxyField(data, path2, () => value), options ?? baseOptions);
}
};
}
function isSuperForm(form, options) {
const isSuperForm = 'form' in form;
if (!isSuperForm && options?.taint !== undefined) {
throw new SuperFormError('If options.taint is set, the whole superForm object must be used as a proxy.');
}
return isSuperForm;
}
export function fieldProxy(form, path, options) {
const path2 = splitPath(path);
if (isSuperForm(form, options)) {
return superFieldProxy(form, path, options);
}
const proxy = derived(form, ($form) => {
const data = traversePath($form, path2);
return data?.value;
});
return {
subscribe(...params) {
const unsub = proxy.subscribe(...params);
return () => unsub();
},
update(upd) {
form.update((data) => updateProxyField(data, path2, upd));
},
set(value) {
form.update((data) => updateProxyField(data, path2, () => value));
}
};
}
function localDate(date) {
return (date.getFullYear() +
'-' +
String(date.getMonth() + 1).padStart(2, '0') +
'-' +
String(date.getDate()).padStart(2, '0'));
}
function localTime(date, step) {
return (String(date.getHours()).padStart(2, '0') +
':' +
String(date.getMinutes()).padStart(2, '0') +
(step % 60 ? ':' + String(date.getSeconds()).padStart(2, '0') : ''));
}
function UTCDate(date) {
return (date.getUTCFullYear() +
'-' +
String(date.getUTCMonth() + 1).padStart(2, '0') +
'-' +
String(date.getUTCDate()).padStart(2, '0'));
}
function UTCTime(date, step) {
return (String(date.getUTCHours()).padStart(2, '0') +
':' +
String(date.getUTCMinutes()).padStart(2, '0') +
(step % 60 ? ':' + String(date.getUTCSeconds()).padStart(2, '0') : ''));
}
/*
function dateToUTC(date: Date) {
return new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds()
);
}
*/

View File

@@ -0,0 +1,232 @@
import type { TaintedFields, SuperFormValidated, SuperValidated } from '../superValidate.js';
import type { ActionResult, BeforeNavigate, Page, SubmitFunction, Transport } from '@sveltejs/kit';
import { type Readable, type Writable, type Updater } from 'svelte/store';
import { type FormPathType, type FormPath, type FormPathLeaves } from '../stringPath.js';
import { enhance as kitEnhance } from '$app/forms';
import type { ValidationErrors } from '../superValidate.js';
import type { IsAny, MaybePromise } from '../utils.js';
import type { ClientValidationAdapter, ValidationAdapter } from '../adapters/adapters.js';
import type { InputConstraints } from '../jsonSchema/constraints.js';
import { type ProxyOptions } from './proxies.js';
export type SuperFormEvents<T extends Record<string, unknown>, M> = Pick<FormOptions<T, M>, 'onError' | 'onResult' | 'onSubmit' | 'onUpdate' | 'onUpdated'>;
export type SuperFormEventList<T extends Record<string, unknown>, M> = {
[Property in keyof SuperFormEvents<T, M>]-?: NonNullable<SuperFormEvents<T, M>[Property]>[];
};
type FilterType<T, Check> = {
[K in keyof NonNullable<T> as NonNullable<NonNullable<T>[K]> extends Check ? never : K]: NonNullable<T>[K];
};
/**
* Helper type for making ActionResult data strongly typed in onUpdate.
* @example const action : FormResult<ActionData> = result.data;
*/
export type FormResult<T extends Record<string, unknown> | null | undefined> = FilterType<T, SuperValidated<Record<string, unknown>, any, Record<string, unknown>>>;
export type TaintOption = boolean | 'untaint' | 'untaint-all' | 'untaint-form';
type ValidatorsOption<T extends Record<string, unknown>> = ValidationAdapter<Partial<T>, Record<string, unknown>> | false | 'clear';
export type FormOptions<T extends Record<string, unknown> = Record<string, unknown>, M = any, In extends Record<string, unknown> = T> = Partial<{
id: string;
/**
* If `false`, the form won't react to page state updates, except for page invalidations.
* If `'never'`, not even page invalidations will affect the form.
* @default true
*/
applyAction: boolean | 'never';
invalidateAll: boolean | 'force' | 'pessimistic';
resetForm: boolean | (() => boolean);
scrollToError: 'auto' | 'smooth' | 'off' | boolean | ScrollIntoViewOptions;
autoFocusOnError: boolean | 'detect';
errorSelector: string;
selectErrorText: boolean;
stickyNavbar: string;
taintedMessage: string | boolean | null | ((nav: BeforeNavigate) => MaybePromise<boolean>);
/**
* Enable single page application (SPA) mode.
* **The string and failStatus options are deprecated** and will be removed in the next major release.
* @see https://superforms.rocks/concepts/spa
*/
SPA: true | /** @deprecated */ string | /** @deprecated */ {
failStatus?: number;
};
onSubmit: (input: Parameters<SubmitFunction>[0] & {
/**
* If dataType: 'json' is set, send this data instead of $form when posting,
* and client-side validation for $form passes.
* @param data An object that can be serialized with devalue.
*/
jsonData: (data: Record<string, unknown>) => void;
/**
* Override client validation temporarily for this form submission.
*/
validators: (validators: Exclude<ValidatorsOption<T>, 'clear'>) => void;
/**
* Use a custom fetch or XMLHttpRequest implementation for this form submission. It must return an ActionResult in the response body.
* If the request is using a XMLHttpRequest, the promise must be resolved when the request has been completed, not before.
*/
customRequest: (validators: (input: Parameters<SubmitFunction>[0]) => Promise<Response | XMLHttpRequest | ActionResult>) => void;
}) => MaybePromise<unknown | void>;
onResult: (event: {
result: ActionResult;
/**
* @deprecated Use formElement instead
*/
formEl: HTMLFormElement;
formElement: HTMLFormElement;
cancel: () => void;
}) => MaybePromise<unknown | void>;
onUpdate: (event: {
form: SuperValidated<T, M, In>;
/**
* @deprecated Use formElement instead
*/
formEl: HTMLFormElement;
formElement: HTMLFormElement;
cancel: () => void;
result: Required<Extract<ActionResult, {
type: 'success' | 'failure';
}>>;
}) => MaybePromise<unknown | void>;
onUpdated: (event: {
form: Readonly<SuperValidated<T, M, In>>;
}) => MaybePromise<unknown | void>;
onError: 'apply' | ((event: {
result: {
type: 'error';
status?: number;
error: App.Error | Error | {
message: string;
};
};
}) => MaybePromise<unknown | void>);
onChange: (event: ChangeEvent<T>) => void;
dataType: 'form' | 'json';
jsonChunkSize: number;
validators: ClientValidationAdapter<Partial<T>, Record<string, unknown>> | ValidatorsOption<T>;
validationMethod: 'auto' | 'oninput' | 'onblur' | 'onsubmit' | 'submit-only';
customValidity: boolean;
clearOnSubmit: 'errors' | 'message' | 'errors-and-message' | 'none';
delayMs: number;
timeoutMs: number;
multipleSubmits: 'prevent' | 'allow' | 'abort';
syncFlashMessage?: boolean;
/**
* @deprecated SvelteKit has moved to $app/state instead of $app/stores, making it hard to support both. Use the flash library directly (setFlash or redirect) instead of integrating it with Superforms.
*/
flashMessage: {
module: {
getFlash(page: Readable<Page>): Writable<App.PageData['flash']>;
updateFlash(page: Readable<Page>, update?: () => Promise<void>): Promise<boolean>;
};
onError?: (event: {
result: {
type: 'error';
status?: number;
error: App.Error | Error | {
message: string;
};
};
flashMessage: Writable<App.PageData['flash']>;
}) => MaybePromise<unknown | void>;
cookiePath?: string;
cookieName?: string;
};
warnings: {
duplicateId?: boolean;
};
transport: IsAny<Transport> extends true ? never : Transport;
/**
* Version 1 compatibilty mode if true.
* Sets resetForm = false and taintedMessage = true.
* Add define: { SUPERFORMS_LEGACY: true } to vite.config.ts to enable globally.
*/
legacy: boolean;
}>;
export type SuperFormSnapshot<T extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message, In extends Record<string, unknown> = T> = SuperFormValidated<T, M, In> & {
tainted: TaintedFields<T> | undefined;
};
export type SuperFormData<T extends Record<string, unknown>> = {
subscribe: Readable<T>['subscribe'];
set(this: void, value: T, options?: {
taint?: TaintOption;
}): void;
update(this: void, updater: Updater<T>, options?: {
taint?: TaintOption;
}): void;
};
export type SuperFormErrors<T extends Record<string, unknown>> = {
subscribe: Writable<ValidationErrors<T>>['subscribe'];
set(this: void, value: ValidationErrors<T>, options?: {
force?: boolean;
}): void;
update(this: void, updater: Updater<ValidationErrors<T>>, options?: {
force?: boolean;
}): void;
clear: () => void;
};
type ResetOptions<T extends Record<string, unknown>> = {
keepMessage?: boolean;
data?: Partial<T>;
newState?: Partial<T>;
id?: string;
};
type Capture<T extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message> = [T] extends [T] ? () => SuperFormSnapshot<T, M> : never;
type Restore<T extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message> = (snapshot: SuperFormSnapshot<T, M>) => void;
export type SuperForm<T extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message> = {
form: SuperFormData<T>;
formId: Writable<string>;
errors: SuperFormErrors<T>;
constraints: Writable<InputConstraints<T>>;
message: Writable<M | undefined>;
tainted: Writable<TaintedFields<T> | undefined>;
submitting: Readable<boolean>;
delayed: Readable<boolean>;
timeout: Readable<boolean>;
/**
* @deprecated posted is inconsistent between server and client validation, and SPA mode. Will be removed in v3. Use a status message or return your own data in the form action to handle form post status.
*/
posted: Readable<boolean>;
allErrors: Readable<{
path: string;
messages: string[];
}[]>;
options: T extends T ? FormOptions<T, M> : never;
enhance: (el: HTMLFormElement, events?: SuperFormEvents<T, M>) => ReturnType<typeof kitEnhance>;
isTainted: (path?: FormPath<T> | Record<string, unknown> | boolean | undefined) => boolean;
reset: (options?: ResetOptions<T>) => void;
submit: (submitter?: HTMLElement | Event | EventTarget | null) => void;
capture: Capture<T, M>;
restore: T extends T ? Restore<T, M> : never;
validate: <Out extends Partial<T> = T, Path extends FormPathLeaves<T> = FormPathLeaves<T>, In extends Record<string, unknown> = Record<string, unknown>>(path: Path, opts?: ValidateOptions<FormPathType<T, Path>, Out, In>) => Promise<string[] | undefined>;
validateForm: <Out extends Partial<T> = T, In extends Record<string, unknown> = Record<string, unknown>>(opts?: {
update?: boolean;
schema?: ValidationAdapter<Out, In>;
focusOnError?: boolean;
}) => Promise<SuperFormValidated<T, M, In>>;
};
export type ValidateOptions<Value, Out extends Record<string, unknown>, In extends Record<string, unknown>> = Partial<{
value: Value;
update: boolean | 'errors' | 'value';
taint: TaintOption;
errors: string | string[];
schema: ValidationAdapter<Out, In>;
}>;
export type ChangeEvent<T extends Record<string, unknown>> = {
path: FormPath<T>;
paths: FormPath<T>[];
formElement: HTMLFormElement;
target: Element;
set: <Path extends FormPath<T>>(path: Path, value: FormPathType<T, Path>, options?: ProxyOptions) => void;
get: <Path extends FormPath<T>>(path: Path) => FormPathType<T, Path>;
} | {
target: undefined;
paths: FormPath<T>[];
set: <Path extends FormPath<T>>(path: Path, value: FormPathType<T, Path>, options?: ProxyOptions) => void;
get: <Path extends FormPath<T>>(path: Path) => FormPathType<T, Path>;
};
/**
* Initializes a SvelteKit form, for convenient handling of values, errors and sumbitting data.
* @param {SuperValidated} form Usually data.form from PageData or defaults, but can also be an object with default values, but then constraints won't be available.
* @param {FormOptions} formOptions Configuration for the form.
* @returns {SuperForm} A SuperForm object that can be used in a Svelte component.
* @DCI-context
*/
export declare function superForm<T extends Record<string, unknown> = Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message, In extends Record<string, unknown> = T>(form: SuperValidated<T, M, In> | T, formOptions?: FormOptions<T, M, In>): SuperForm<T, M>;
export {};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
import type { ValidationAdapter, ClientValidationAdapter } from './adapters/adapters.js';
import type { SuperValidateOptions, SuperValidated } from './superValidate.js';
type SuperSchemaData<T extends Record<string, unknown>> = Partial<T> | null | undefined;
type SuperSchemaOptions<T extends Record<string, unknown>> = Pick<SuperValidateOptions<T>, 'id' | 'defaults'>;
export declare function defaults<Out extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message, In extends Record<string, unknown> = Out>(adapter: ValidationAdapter<Out, In>, options?: SuperSchemaOptions<Out>): SuperValidated<Out, M, In>;
export declare function defaults<Out extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message, In extends Record<string, unknown> = Out>(defaults: SuperSchemaData<Out>, adapter: ValidationAdapter<Out, In>, options?: SuperSchemaOptions<Out>): SuperValidated<Out, M, In>;
export declare function defaults<Out extends Record<string, unknown>, M = App.Superforms.Message extends never ? any : App.Superforms.Message, In extends Record<string, unknown> = Out>(defaults: Out, adapter: ClientValidationAdapter<Out, In>, options?: SuperSchemaOptions<Out>): SuperValidated<Out, M, In>;
export declare function defaultValues<T extends Record<string, unknown>>(adapter: ValidationAdapter<T, Record<string, unknown>>): T;
export {};

View File

@@ -0,0 +1,21 @@
export function defaults(data, adapter, options) {
if (data && 'superFormValidationLibrary' in data) {
options = adapter;
adapter = data;
data = null;
}
const validator = adapter;
const optionDefaults = options?.defaults ?? validator.defaults;
return {
id: options?.id ?? validator.id ?? '',
valid: false,
posted: false,
errors: {},
data: { ...optionDefaults, ...data },
constraints: validator.constraints,
shape: validator.shape
};
}
export function defaultValues(adapter) {
return adapter.defaults;
}

View File

@@ -0,0 +1,33 @@
import type { SchemaShape } from './jsonSchema/schemaShape.js';
import type { ValidationErrors } from './superValidate.js';
import type { JSONSchema } from './jsonSchema/index.js';
import type { ValidationIssue } from './adapters/adapters.js';
export declare class SuperFormError extends Error {
constructor(message?: string);
}
export declare class SchemaError extends SuperFormError {
readonly path: string | undefined;
constructor(message: string, path?: string | (string | number | symbol)[]);
}
export declare function mapErrors(errors: ValidationIssue[], shape: SchemaShape): Record<string, unknown> & {
_errors?: string[];
};
/**
* Filter errors based on validation method.
* auto = Requires the existence of errors and tainted (field in store) to show
* oninput = Set directly
*/
export declare function updateErrors<T extends Record<string, unknown>>(New: ValidationErrors<T>, Previous: ValidationErrors<T>, force?: boolean): ValidationErrors<T>;
export declare function flattenErrors<T extends Record<string, unknown>>(errors: ValidationErrors<T>): {
path: string;
messages: string[];
}[];
/**
* Merge defaults with parsed data.
*/
export declare function mergeDefaults<T extends Record<string, unknown>>(parsedData: Record<string, unknown> | null | undefined, defaults: T): T;
/**
* Merge defaults with (important!) *already validated and merged data*.
* @DCI-context
*/
export declare function replaceInvalidDefaults<T extends Record<string, unknown>>(Data: Record<string, unknown>, Defaults: T, _schema: JSONSchema, Errors: ValidationIssue[], preprocessed: (keyof T)[] | undefined): T;

View File

@@ -0,0 +1,237 @@
import { pathExists, setPaths, traversePath, traversePaths } from './traversal.js';
import { mergePath } from './stringPath.js';
import { defaultTypes, defaultValue } from './jsonSchema/schemaDefaults.js';
import { clone } from './utils.js';
import { merge } from 'ts-deepmerge';
import { schemaInfo } from './jsonSchema/schemaInfo.js';
export class SuperFormError extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, SuperFormError.prototype);
}
}
export class SchemaError extends SuperFormError {
path;
constructor(message, path) {
super((path && path.length ? `[${Array.isArray(path) ? path.join('.') : path}] ` : '') + message);
this.path = Array.isArray(path) ? path.join('.') : path;
Object.setPrototypeOf(this, SchemaError.prototype);
}
}
export function mapErrors(errors, shape) {
//console.log('===', errors.length, 'errors', shape);
const output = {};
function addFormLevelError(error) {
if (!('_errors' in output))
output._errors = [];
if (!Array.isArray(output._errors)) {
if (typeof output._errors === 'string')
output._errors = [output._errors];
else
throw new SuperFormError('Form-level error was not an array.');
}
output._errors.push(error.message);
}
for (const error of errors) {
// Form-level error
if (!error.path || (error.path.length == 1 && !error.path[0])) {
addFormLevelError(error);
continue;
}
// Path must filter away number indices, since the object shape doesn't contain these.
// Except the last, since otherwise any error in an array will count as an object error.
const isLastIndexNumeric = /^\d$/.test(String(error.path[error.path.length - 1]));
const objectError = !isLastIndexNumeric &&
pathExists(shape, error.path.filter((p) => /\D/.test(String(p))))?.value;
//console.log(error.path, error.message, objectError ? '[OBJ]' : '');
const leaf = traversePath(output, error.path, ({ value, parent, key }) => {
if (value === undefined)
parent[key] = {};
return parent[key];
});
if (!leaf) {
addFormLevelError(error);
continue;
}
const { parent, key } = leaf;
if (objectError) {
if (!(key in parent))
parent[key] = {};
if (!('_errors' in parent[key]))
parent[key]._errors = [error.message];
else
parent[key]._errors.push(error.message);
}
else {
if (!(key in parent))
parent[key] = [error.message];
else
parent[key].push(error.message);
}
}
return output;
}
/**
* Filter errors based on validation method.
* auto = Requires the existence of errors and tainted (field in store) to show
* oninput = Set directly
*/
export function updateErrors(New, Previous, force) {
if (force)
return New;
// Set previous errors to undefined,
// which signifies that an error can be displayed there again.
traversePaths(Previous, (errors) => {
if (!Array.isArray(errors.value))
return;
errors.set(undefined);
});
traversePaths(New, (error) => {
if (!Array.isArray(error.value) && error.value !== undefined)
return;
setPaths(Previous, [error.path], error.value);
});
return Previous;
}
export function flattenErrors(errors) {
return _flattenErrors(errors, []);
}
function _flattenErrors(errors, path) {
const entries = Object.entries(errors);
return entries
.filter(([, value]) => value !== undefined)
.flatMap(([key, messages]) => {
if (Array.isArray(messages) && messages.length > 0) {
const currPath = path.concat([key]);
return { path: mergePath(currPath), messages };
}
else {
return _flattenErrors(errors[key], path.concat([key]));
}
});
}
/**
* Merge defaults with parsed data.
*/
export function mergeDefaults(parsedData, defaults) {
if (!parsedData)
return clone(defaults);
return merge.withOptions({ mergeArrays: false }, defaults, parsedData);
}
/**
* Merge defaults with (important!) *already validated and merged data*.
* @DCI-context
*/
export function replaceInvalidDefaults(Data, Defaults, _schema, Errors, preprocessed) {
const defaultType = _schema.additionalProperties && typeof _schema.additionalProperties == 'object'
? { __types: schemaInfo(_schema.additionalProperties, false, []).types }
: undefined; // Will throw if a field does not exist
///// Roles ///////////////////////////////////////////////////////
//#region Types
// TODO: Memoize Types with _schema?
const Types = defaultTypes(_schema);
function Types_correctValue(dataValue, defValue, type) {
const types = type.__types;
if (!types.length || types.every((t) => t == 'undefined' || t == 'null' || t == 'any')) {
// No types counts as an "any" type
return dataValue;
}
else if (types.length == 1 && types[0] == 'array' && !type.__items) {
/*
No type info for array exists.
Keep the value even though it may not be the correct type, but validation
won't fail and the failed data is usually returned to the form without UX problems.
*/
return dataValue;
}
const dateTypes = ['unix-time', 'Date', 'date'];
for (const schemaType of types) {
const defaultTypeValue = defaultValue(schemaType, undefined);
const sameType = typeof dataValue === typeof defaultTypeValue ||
(dateTypes.includes(schemaType) && dataValue instanceof Date);
const sameExistance = sameType && (dataValue === null) === (defaultTypeValue === null);
if (sameType && sameExistance) {
return dataValue;
}
else if (type.__items) {
// Parse array type
return Types_correctValue(dataValue, defValue, type.__items);
}
}
// null takes preference over undefined
if (defValue === undefined && types.includes('null')) {
return null;
}
return defValue;
}
//#endregion
//#region Data
function Data_traverse() {
traversePaths(Defaults, Defaults_traverseAndReplace);
Errors_traverseAndReplace();
return Data;
}
function Data_setValue(currentPath, newValue) {
setPaths(Data, [currentPath], newValue);
}
//#endregion
//#region Errors
function Errors_traverseAndReplace() {
for (const error of Errors) {
if (!error.path)
continue;
Defaults_traverseAndReplace({
path: error.path,
value: pathExists(Defaults, error.path)?.value
}, true);
}
}
//#endregion
//#region Defaults
function Defaults_traverseAndReplace(defaultPath, traversingErrors = false) {
const currentPath = defaultPath.path;
if (!currentPath || !currentPath[0])
return;
if (typeof currentPath[0] === 'string' && preprocessed?.includes(currentPath[0]))
return;
const dataPath = pathExists(Data, currentPath);
//let newValue = defValue;
if ((!dataPath && defaultPath.value !== undefined) ||
(dataPath && dataPath.value === undefined)) {
Data_setValue(currentPath, defaultPath.value);
}
else if (dataPath) {
const defValue = defaultPath.value;
const dataValue = dataPath.value;
// Check for same JS type with an existing default value.
if (defValue !== undefined &&
typeof dataValue === typeof defValue &&
(dataValue === null) === (defValue === null)) {
return;
}
const typePath = currentPath.filter((p) => /\D/.test(String(p)));
const pathTypes = traversePath(Types, typePath, (path) => {
//console.log(path.path, path.value); //debug
return path.value && '__items' in path.value ? path.value.__items : path.value;
});
if (!pathTypes) {
// Return if checking for errors, as there may be deep errors that doesn't exist in the defaults.
if (traversingErrors)
return;
throw new SchemaError('No types found for defaults', currentPath);
}
const fieldType = pathTypes.value ?? defaultType;
if (fieldType) {
const corrected = Types_correctValue(dataValue, defValue, fieldType);
// If same value, skip the potential nested path as it's already correct.
if (corrected === dataValue)
return 'skip';
Data_setValue(currentPath, corrected);
}
}
}
//#endregion
{
return Data_traverse();
}
}

View File

@@ -0,0 +1,11 @@
import type { JSONSchema } from './index.js';
import type { SuperValidateOptions } from './superValidate.js';
type ParsedData = {
id: string | undefined;
posted: boolean;
data: Record<string, unknown> | null | undefined;
};
export declare function parseRequest<T extends Record<string, unknown>>(data: unknown, schemaData: JSONSchema, options?: SuperValidateOptions<T>): Promise<ParsedData>;
export declare function parseSearchParams<T extends Record<string, unknown>>(data: URL | URLSearchParams, schemaData: JSONSchema, options?: SuperValidateOptions<T>): ParsedData;
export declare function parseFormData<T extends Record<string, unknown>>(formData: FormData, schemaData: JSONSchema, options?: SuperValidateOptions<T>, fromURL?: boolean): ParsedData;
export {};

View File

@@ -0,0 +1,330 @@
import { parse } from 'devalue';
import { SchemaError, SuperFormError } from './errors.js';
import { defaultValues } from './jsonSchema/schemaDefaults.js';
import { schemaInfo } from './jsonSchema/schemaInfo.js';
import { splitPath } from './stringPath.js';
import { setPaths } from './traversal.js';
import { assertSchema } from './utils.js';
/**
* V1 compatibilty. resetForm = false and taintedMessage = true
*/
let legacyMode = false;
try {
// @ts-expect-error Vite define check
if (SUPERFORMS_LEGACY)
legacyMode = true;
}
catch {
// No legacy mode defined
}
const unionError = 'FormData parsing failed: Unions are only supported when the dataType option for superForm is set to "json".';
/**
* Check if multiple types represent compatible variations of the same base type
*/
function isCompatibleTypeUnion(types) {
const primaryTypes = new Set(types.map((type) => {
if (['number', 'integer'].includes(type))
return 'number';
if (type === 'unix-time')
return 'number';
return type;
}));
return primaryTypes.size <= 1;
}
/**
* Check if union schema represents compatible variations of the same base type
*/
function isCompatibleUnionSchema(union) {
if (!union)
return true;
const unionTypes = new Set(union.flatMap((u) => u.type
? Array.isArray(u.type)
? u.type
: [u.type]
: u.const !== undefined
? [typeof u.const]
: []));
return unionTypes.size <= 1 || (unionTypes.size === 2 && unionTypes.has('null'));
}
export async function parseRequest(data, schemaData, options) {
let parsed;
if (data instanceof FormData) {
parsed = parseFormData(data, schemaData, options);
}
else if (data instanceof URL || data instanceof URLSearchParams) {
parsed = parseSearchParams(data, schemaData, options);
}
else if (data instanceof Request) {
parsed = await tryParseFormData(data, schemaData, options);
}
else if (
// RequestEvent
data &&
typeof data === 'object' &&
'request' in data &&
data.request instanceof Request) {
parsed = await tryParseFormData(data.request, schemaData, options);
}
else {
parsed = {
id: undefined,
data: data,
posted: false
};
}
return parsed;
}
async function tryParseFormData(request, schemaData, options) {
let formData = undefined;
try {
formData = await request.formData();
}
catch (e) {
if (e instanceof TypeError && e.message.includes('already been consumed')) {
// Pass through the "body already consumed" error, which applies to
// POST requests when event/request is used after formData has been fetched.
throw e;
}
// No data found, return an empty form
return { id: undefined, data: undefined, posted: false };
}
return parseFormData(formData, schemaData, options);
}
export function parseSearchParams(data, schemaData, options) {
if (data instanceof URL)
data = data.searchParams;
const convert = new FormData();
for (const [key, value] of data.entries()) {
convert.append(key, value);
}
const output = parseFormData(convert, schemaData, options, true);
// Set posted to false since it's a URL
output.posted = false;
return output;
}
export function parseFormData(formData, schemaData, options, fromURL = false) {
function tryParseSuperJson() {
if (formData.has('__superform_json')) {
try {
const transport = options && options.transport
? Object.fromEntries(Object.entries(options.transport).map(([k, v]) => [k, v.decode]))
: undefined;
const output = parse(formData.getAll('__superform_json').join('') ?? '', transport);
if (typeof output === 'object') {
// Restore uploaded files and add to data
const filePaths = Array.from(formData.keys());
for (const path of filePaths.filter((path) => path.startsWith('__superform_file_'))) {
const realPath = splitPath(path.substring(17));
setPaths(output, [realPath], formData.get(path));
}
for (const path of filePaths.filter((path) => path.startsWith('__superform_files_'))) {
const realPath = splitPath(path.substring(18));
const allFiles = formData.getAll(path);
setPaths(output, [realPath], Array.from(allFiles));
}
return output;
}
}
catch {
//
}
}
return null;
}
const data = tryParseSuperJson();
const id = formData.get('__superform_id')?.toString();
return data
? { id, data, posted: true }
: {
id,
data: _parseFormData(formData, schemaData, options, fromURL),
posted: true
};
}
function _parseFormData(formData, schema, options, fromURL = false) {
const output = {};
let schemaKeys;
let discriminatedUnionSchema;
if (options?.strict) {
schemaKeys = new Set([...formData.keys()].filter((key) => !key.startsWith('__superform_')));
}
else {
let unionKeys = [];
// Special fix for (discriminated) union schemas, then the keys must be gathered from the objects in the union
if (schema.anyOf || schema.oneOf) {
const info = schemaInfo(schema, false, []);
if (info.union?.some((s) => s.type !== 'object')) {
throw new SchemaError('All form types must be an object if schema is a union.');
}
unionKeys = info.union?.flatMap((s) => Object.keys(s.properties ?? {})) ?? [];
// For discriminated unions, find the matching variant based on field values in FormData
if (info.union && info.union.length > 1) {
// Try to match which union variant we're dealing with by checking their properties
// against the FormData entries
for (const variant of info.union) {
const variantProps = variant.properties ?? {};
const variantPropKeys = Object.keys(variantProps);
// Check if this variant matches the form data
// A variant matches if it has at least the discriminant field, or if its properties match
let isMatch = true;
for (const propKey of variantPropKeys) {
const prop = variantProps[propKey];
// If the property has a const value, check if it matches the form data
if (typeof prop !== 'boolean' && prop?.const !== undefined) {
const formValue = formData.get(propKey);
if (formValue !== String(prop.const)) {
isMatch = false;
break;
}
}
}
if (isMatch) {
discriminatedUnionSchema = variant;
break;
}
}
}
}
schemaKeys = new Set([
...unionKeys,
...Object.keys(schema.properties ?? {}),
...(schema.additionalProperties ? formData.keys() : [])
].filter((key) => !key.startsWith('__superform_')));
}
function parseSingleEntry(key, entry, info) {
if (options?.preprocessed && options.preprocessed.includes(key)) {
return entry;
}
//console.log(`Parsing entry for key "${key}":`, entry); //debug
if (entry && typeof entry !== 'string') {
const allowFiles = legacyMode ? options?.allowFiles === true : options?.allowFiles !== false;
return !allowFiles ? undefined : entry.size ? entry : info.isNullable ? null : undefined;
}
if (info.types.length > 1 && !isCompatibleTypeUnion(info.types)) {
throw new SchemaError(unionError, key);
}
let [type] = info.types;
if (entry && !info.types.length && info.schema.enum) {
// Special case for Typescript enums
// If the entry is an integer, parse it as such, otherwise string
if (info.schema.enum.includes(entry))
type = 'string';
else {
type = Number.isInteger(parseInt(entry, 10)) ? 'integer' : 'string';
}
}
return parseFormDataEntry(key, entry, type ?? 'any', info, fromURL);
}
const defaultPropertyType = typeof schema.additionalProperties == 'object'
? schema.additionalProperties
: { type: 'string' };
for (const key of schemaKeys) {
// For discriminated unions, try to get the property from the matching variant first
const property = discriminatedUnionSchema?.properties
? discriminatedUnionSchema.properties[key]
: schema.properties
? schema.properties[key]
: defaultPropertyType;
assertSchema(property, key);
const info = schemaInfo(property ?? defaultPropertyType, !schema.required?.includes(key), [
key
]);
if (!info)
continue;
if (!info.types.includes('boolean') && !schema.additionalProperties && !formData.has(key)) {
continue;
}
const entries = formData.getAll(key);
if (info.union && info.union.length > 1 && !isCompatibleUnionSchema(info.union)) {
throw new SchemaError(unionError, key);
}
if (info.types.includes('array') || info.types.includes('set')) {
// If no items, it could be a union containing the info
const items = property.items ?? (info.union?.length == 1 ? info.union[0] : undefined);
if (!items || typeof items == 'boolean' || (Array.isArray(items) && items.length != 1)) {
throw new SchemaError('Arrays must have a single "items" property that defines its type.', key);
}
const arrayType = Array.isArray(items) ? items[0] : items;
assertSchema(arrayType, key);
const arrayInfo = schemaInfo(arrayType, info.isOptional, [key]);
if (!arrayInfo)
continue;
// Check for empty files being posted (and filtered)
const isFileArray = entries.length && entries.some((e) => e && typeof e !== 'string');
const arrayData = entries.map((e) => parseSingleEntry(key, e, arrayInfo));
if (isFileArray && arrayData.every((file) => !file))
arrayData.length = 0;
output[key] = info.types.includes('set') ? new Set(arrayData) : arrayData;
}
else {
output[key] = parseSingleEntry(key, entries[entries.length - 1], info);
}
}
return output;
}
function parseFormDataEntry(key, value, type, info, fromURL = false) {
//console.log(`Parsing FormData ${key} (${type}): ${value ? `"${value}"` : 'undefined'}`, info); //debug
if (!value) {
//console.log(`No FormData for "${key}" (${type}).`, info); //debug
// Special case for booleans with default value true, *when posted* (not from URL)
if (!fromURL && type == 'boolean' && info.isOptional && info.schema.default === true) {
return false;
}
const defaultValue = defaultValues(info.schema, info.isOptional, [key]);
// Special case for empty posted enums, then the empty value should be returned,
// otherwise even a required field will get a default value, resulting in that
// posting missing enum values must use strict mode.
if (info.schema.enum && defaultValue !== null && defaultValue !== undefined) {
return value;
}
// Issue #664: const values (z.literal) should not be used as defaults for empty strings
// The const is a validation constraint, not a default value
if ('const' in info.schema) {
return value;
}
if (defaultValue !== undefined)
return defaultValue;
if (info.isNullable)
return null;
if (info.isOptional)
return undefined;
}
function typeError() {
throw new SchemaError(type[0].toUpperCase() +
type.slice(1) +
` type found. ` +
`Set the dataType option to "json" and add use:enhance on the client to use nested data structures. ` +
`More information: https://superforms.rocks/concepts/nested-data`, key);
}
switch (type) {
case 'string':
case 'any':
return value;
case 'integer':
return parseInt(value ?? '', 10);
case 'number':
return parseFloat(value ?? '');
case 'boolean':
return Boolean(value == 'false' ? '' : value).valueOf();
case 'stringbool':
// Zod's z.stringbool() - keep as string, let Zod validate it
// This prevents Superforms from coercing to boolean before Zod can validate
return value;
case 'unix-time': {
// Must return undefined for invalid dates due to https://github.com/Rich-Harris/devalue/issues/51
const date = new Date(value ?? '');
return !isNaN(date) ? date : undefined;
}
case 'int64':
case 'bigint':
return BigInt(value ?? '.');
case 'symbol':
return Symbol(String(value));
case 'set':
case 'array':
case 'object':
return typeError();
default:
throw new SuperFormError('Unsupported schema type for FormData: ' + type);
}
}

View File

@@ -0,0 +1,9 @@
import SuperDebug from './client/SuperDebug.svelte';
export default SuperDebug;
export { SuperFormError, SchemaError } from './errors.js';
export type { InputConstraints, InputConstraint } from './jsonSchema/constraints.js';
export type { JSONSchema } from './jsonSchema/index.js';
export { superForm, intProxy, numberProxy, booleanProxy, dateProxy, fieldProxy, formFieldProxy, stringProxy, arrayProxy, fileProxy, fileFieldProxy, filesProxy, filesFieldProxy, defaults, defaultValues, schemaShape, actionResult, superValidate, message, setMessage, setError, withFiles, removeFiles, fail, type SuperValidated, type SuperValidateOptions, type TaintedFields, type ValidationErrors, type Infer, type InferIn, type Schema, type FormResult, type FormOptions, type SuperForm, type SuperFormEventList, type SuperFormEvents, type SuperFormSnapshot, type ValidateOptions, type TaintOption, type FormPath, type FormPathLeaves, type FormPathLeavesWithErrors, type FormPathArrays, type FormPathType, type ChangeEvent, type FieldProxy, type ArrayProxy, type FormFieldProxy } from './client/index.js';
export { splitPath } from './stringPath.js';
export type { ErrorStatus, MergeUnion, MergeFormUnion } from './utils.js';
export { mergeFormUnion } from './utils.js';

View File

@@ -0,0 +1,7 @@
import SuperDebug from './client/SuperDebug.svelte';
export default SuperDebug;
export { SuperFormError, SchemaError } from './errors.js';
// Everything from client/index.ts
export { superForm, intProxy, numberProxy, booleanProxy, dateProxy, fieldProxy, formFieldProxy, stringProxy, arrayProxy, fileProxy, fileFieldProxy, filesProxy, filesFieldProxy, defaults, defaultValues, schemaShape, actionResult, superValidate, message, setMessage, setError, withFiles, removeFiles, fail } from './client/index.js';
export { splitPath } from './stringPath.js';
export { mergeFormUnion } from './utils.js';

View File

@@ -0,0 +1,13 @@
import type { SuperStruct } from '../superStruct.js';
import type { JSONSchema } from './index.js';
export type InputConstraint = Partial<{
pattern: string;
min: number | string;
max: number | string;
required: boolean;
step: number | 'any';
minlength: number;
maxlength: number;
}>;
export type InputConstraints<T> = SuperStruct<T, InputConstraint>;
export declare function constraints<T>(schema: JSONSchema): InputConstraints<T>;

View File

@@ -0,0 +1,102 @@
import { schemaInfo } from './schemaInfo.js';
import { merge as deepMerge } from 'ts-deepmerge';
export function constraints(schema) {
return _constraints(schemaInfo(schema, false, []), []);
}
function merge(...constraints) {
const filtered = constraints.filter((c) => !!c);
if (!filtered.length)
return undefined;
if (filtered.length == 1)
return filtered[0];
return deepMerge(...filtered);
}
function _constraints(info, path) {
if (!info)
return undefined;
let output = undefined;
// Union
if (info.union && info.union.length) {
const infos = info.union.map((s) => schemaInfo(s, info.isOptional, path));
const merged = infos.map((i) => _constraints(i, path));
output = merge(output, ...merged);
// Delete required if any part of the union is optional
if (output &&
(info.isNullable || info.isOptional || infos.some((i) => i?.isNullable || i?.isOptional))) {
delete output.required;
}
}
// Arrays
if (info.array) {
output = merge(output, ...info.array.map((i) => _constraints(schemaInfo(i, info.isOptional, path), path)));
}
// Objects
if (info.properties) {
const obj = {};
for (const [key, prop] of Object.entries(info.properties)) {
const propInfo = schemaInfo(prop, !info.required?.includes(key) || prop.default !== undefined, [key]);
const propConstraint = _constraints(propInfo, [...path, key]);
if (typeof propConstraint === 'object' && Object.values(propConstraint).length > 0) {
obj[key] = propConstraint;
}
}
output = merge(output, obj);
}
return output ?? constraint(info);
}
function constraint(info) {
const output = {};
const schema = info.schema;
const type = schema.type;
const format = schema.format;
// Must be before type check
if (type == 'integer' &&
format == 'unix-time' //||
//format == 'date-time' ||
//format == 'date' ||
//format == 'time'
) {
const date = schema;
if (date.minimum !== undefined)
output.min = new Date(date.minimum).toISOString();
if (date.maximum !== undefined)
output.max = new Date(date.maximum).toISOString();
}
else if (type == 'string') {
const str = schema;
const patterns = [
str.pattern,
...(str.allOf ? str.allOf.map((s) => (typeof s == 'boolean' ? undefined : s.pattern)) : [])
].filter((s) => s !== undefined);
if (patterns.length > 0)
output.pattern = patterns[0];
if (str.minLength !== undefined)
output.minlength = str.minLength;
if (str.maxLength !== undefined)
output.maxlength = str.maxLength;
}
else if (type == 'number' || type == 'integer') {
const num = schema;
if (num.minimum !== undefined)
output.min = num.minimum;
else if (num.exclusiveMinimum !== undefined)
output.min = num.exclusiveMinimum + (type == 'integer' ? 1 : Number.MIN_VALUE);
if (num.maximum !== undefined)
output.max = num.maximum;
else if (num.exclusiveMaximum !== undefined)
output.max = num.exclusiveMaximum - (type == 'integer' ? 1 : Number.MIN_VALUE);
if (num.multipleOf !== undefined)
output.step = num.multipleOf;
}
else if (type == 'array') {
const arr = schema;
if (arr.minItems !== undefined)
output.min = arr.minItems;
if (arr.maxItems !== undefined)
output.max = arr.maxItems;
}
if (!info.isNullable && !info.isOptional) {
output.required = true;
}
return Object.keys(output).length > 0 ? output : undefined;
}

View File

@@ -0,0 +1 @@
export type { JSONSchema7 as JSONSchema } from 'json-schema';

View File

@@ -0,0 +1 @@
export {};

Some files were not shown because too many files have changed in this diff Show More