@frigear-nu/forms 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/dist/module.d.mts +281 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +102 -0
- package/dist/runtime/app/components/FormField.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormField.vue +80 -0
- package/dist/runtime/app/components/FormField.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormRenderer.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormRenderer.vue +145 -0
- package/dist/runtime/app/components/FormRenderer.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormSubmissionView.d.vue.ts +7 -0
- package/dist/runtime/app/components/FormSubmissionView.vue +77 -0
- package/dist/runtime/app/components/FormSubmissionView.vue.d.ts +7 -0
- package/dist/runtime/app/composables/index.d.ts +2 -0
- package/dist/runtime/app/composables/index.js +2 -0
- package/dist/runtime/app/composables/useFormStepper.d.ts +21 -0
- package/dist/runtime/app/composables/useFormStepper.js +33 -0
- package/dist/runtime/app/composables/useForms.d.ts +21 -0
- package/dist/runtime/app/composables/useForms.js +52 -0
- package/dist/runtime/app/pages/admin-form.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-form.vue +150 -0
- package/dist/runtime/app/pages/admin-form.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-submission.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-submission.vue +59 -0
- package/dist/runtime/app/pages/admin-submission.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-submissions.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-submissions.vue +71 -0
- package/dist/runtime/app/pages/admin-submissions.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin.vue +194 -0
- package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
- package/dist/runtime/app/pages/form.d.vue.ts +3 -0
- package/dist/runtime/app/pages/form.vue +18 -0
- package/dist/runtime/app/pages/form.vue.d.ts +3 -0
- package/dist/runtime/i18n/lang/da.json +84 -0
- package/dist/runtime/i18n/lang/en.json +84 -0
- package/dist/runtime/menus.d.ts +2 -0
- package/dist/runtime/menus.js +15 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +504 -0
- package/dist/runtime/server/db/schema.sqlite.js +46 -0
- package/dist/runtime/server/plugins/email-delivery.d.ts +2 -0
- package/dist/runtime/server/plugins/email-delivery.js +21 -0
- package/dist/runtime/server/plugins/seed.d.ts +2 -0
- package/dist/runtime/server/plugins/seed.js +11 -0
- package/dist/runtime/server/routes/admin/form.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form.get.js +4 -0
- package/dist/runtime/server/routes/admin/form.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form.patch.js +17 -0
- package/dist/runtime/server/routes/admin/forms.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/forms.get.js +19 -0
- package/dist/runtime/server/routes/admin/forms.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/forms.post.js +13 -0
- package/dist/runtime/server/routes/admin/import.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/import.post.js +6 -0
- package/dist/runtime/server/routes/admin/submission-file.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/submission-file.get.js +17 -0
- package/dist/runtime/server/routes/admin/submission-redeliver.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/submission-redeliver.post.js +29 -0
- package/dist/runtime/server/routes/admin/submission.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/submission.get.js +22 -0
- package/dist/runtime/server/routes/admin/submissions.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/submissions.get.js +16 -0
- package/dist/runtime/server/routes/public/form.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/form.get.js +3 -0
- package/dist/runtime/server/routes/public/submit.post.d.ts +2 -0
- package/dist/runtime/server/routes/public/submit.post.js +7 -0
- package/dist/runtime/server/utils/admin-schemas.d.ts +291 -0
- package/dist/runtime/server/utils/admin-schemas.js +14 -0
- package/dist/runtime/server/utils/auth.d.ts +11 -0
- package/dist/runtime/server/utils/auth.js +7 -0
- package/dist/runtime/server/utils/forms.d.ts +9 -0
- package/dist/runtime/server/utils/forms.js +30 -0
- package/dist/runtime/server/utils/rate-limit.d.ts +6 -0
- package/dist/runtime/server/utils/rate-limit.js +14 -0
- package/dist/runtime/server/utils/render.d.ts +11 -0
- package/dist/runtime/server/utils/render.js +26 -0
- package/dist/runtime/server/utils/seed.d.ts +9 -0
- package/dist/runtime/server/utils/seed.js +22 -0
- package/dist/runtime/server/utils/submit.d.ts +9 -0
- package/dist/runtime/server/utils/submit.js +120 -0
- package/dist/runtime/utils/conditions.d.ts +19 -0
- package/dist/runtime/utils/conditions.js +36 -0
- package/dist/runtime/utils/definition.d.ts +91 -0
- package/dist/runtime/utils/definition.js +103 -0
- package/dist/runtime/utils/index.d.ts +13 -0
- package/dist/runtime/utils/index.js +8 -0
- package/dist/runtime/utils/markdown.d.ts +5 -0
- package/dist/runtime/utils/markdown.js +5 -0
- package/dist/runtime/utils/schema.d.ts +27 -0
- package/dist/runtime/utils/schema.js +95 -0
- package/dist/runtime/utils/text.d.ts +12 -0
- package/dist/runtime/utils/text.js +12 -0
- package/dist/types.d.mts +3 -0
- package/package.json +86 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "@nuxthub/db";
|
|
3
|
+
import { blob } from "@nuxthub/blob";
|
|
4
|
+
import { buildFormSchema, newId, validatedFields } from "../../utils/index.js";
|
|
5
|
+
const safeFilename = (name) => name.replace(/[^\w.-]+/g, "_").replace(/^\.+/, "").slice(0, 120) || "file";
|
|
6
|
+
async function readSubmission(event) {
|
|
7
|
+
const uploads = /* @__PURE__ */ new Map();
|
|
8
|
+
const contentType = getRequestHeader(event, "content-type") ?? "";
|
|
9
|
+
if (contentType.includes("multipart/form-data")) {
|
|
10
|
+
const parts = await readMultipartFormData(event) ?? [];
|
|
11
|
+
let answers = {};
|
|
12
|
+
let locale = "en";
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
if (part.name === "answers") {
|
|
15
|
+
try {
|
|
16
|
+
answers = JSON.parse(part.data.toString("utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
throw createError({ statusCode: 400, statusMessage: "Invalid answers." });
|
|
19
|
+
}
|
|
20
|
+
} else if (part.name === "locale") {
|
|
21
|
+
locale = part.data.toString("utf8").slice(0, 20);
|
|
22
|
+
} else if (part.name?.startsWith("file:") && part.filename) {
|
|
23
|
+
const key = part.name.slice(5);
|
|
24
|
+
uploads.set(key, [...uploads.get(key) ?? [], { filename: part.filename, type: part.type ?? "application/octet-stream", data: part.data }]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { answers, locale, uploads };
|
|
28
|
+
}
|
|
29
|
+
const body = await readBody(event);
|
|
30
|
+
return { answers: body?.answers ?? {}, locale: String(body?.locale ?? "en").slice(0, 20), uploads };
|
|
31
|
+
}
|
|
32
|
+
export async function processSubmission(event, form, userId) {
|
|
33
|
+
const definition = form.definition;
|
|
34
|
+
const { answers: raw, locale, uploads } = await readSubmission(event);
|
|
35
|
+
const answers = { ...raw };
|
|
36
|
+
for (const field of validatedFields(definition, raw)) {
|
|
37
|
+
if (field.type !== "file") continue;
|
|
38
|
+
const files2 = uploads.get(field.key) ?? [];
|
|
39
|
+
answers[field.key] = files2.map((file) => ({ name: file.filename, size: file.data.byteLength, type: file.type }));
|
|
40
|
+
}
|
|
41
|
+
const parsed = buildFormSchema(definition, answers, { locale }).safeParse(answers);
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
throw createError({
|
|
44
|
+
statusCode: 422,
|
|
45
|
+
statusMessage: "Invalid submission.",
|
|
46
|
+
data: { issues: parsed.error.issues.map((issue) => ({ path: issue.path, message: issue.message })) }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const validated = parsed.data;
|
|
50
|
+
const stored = definition.storage !== "none";
|
|
51
|
+
const submissionId = stored ? newId("fsb") : null;
|
|
52
|
+
const files = [];
|
|
53
|
+
if (stored) {
|
|
54
|
+
for (const field of validatedFields(definition, answers)) {
|
|
55
|
+
if (field.type !== "file") continue;
|
|
56
|
+
for (const upload of uploads.get(field.key) ?? []) {
|
|
57
|
+
const saved = await blob.put(safeFilename(upload.filename), upload.data, {
|
|
58
|
+
contentType: upload.type,
|
|
59
|
+
prefix: `forms/${form.slug}/${submissionId}/${field.key}`,
|
|
60
|
+
addRandomSuffix: true
|
|
61
|
+
});
|
|
62
|
+
files.push({ fieldKey: field.key, filename: upload.filename, contentType: upload.type, size: upload.data.byteLength, pathname: saved.pathname });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await db.insert(schema.formSubmissions).values({
|
|
66
|
+
id: submissionId,
|
|
67
|
+
formId: form.id,
|
|
68
|
+
formSlug: form.slug,
|
|
69
|
+
formVersion: form.version,
|
|
70
|
+
definitionSnapshot: definition,
|
|
71
|
+
answers: validated,
|
|
72
|
+
userId,
|
|
73
|
+
locale,
|
|
74
|
+
deliveryStatus: "pending"
|
|
75
|
+
});
|
|
76
|
+
if (files.length) {
|
|
77
|
+
await db.insert(schema.formSubmissionFiles).values(files.map((file) => ({
|
|
78
|
+
id: newId("fsf"),
|
|
79
|
+
submissionId,
|
|
80
|
+
fieldKey: file.fieldKey,
|
|
81
|
+
pathname: file.pathname,
|
|
82
|
+
filename: file.filename,
|
|
83
|
+
contentType: file.contentType,
|
|
84
|
+
size: file.size
|
|
85
|
+
})));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const ctx = {
|
|
89
|
+
event,
|
|
90
|
+
form: { id: form.id, slug: form.slug, name: form.name, version: form.version, definition },
|
|
91
|
+
answers: validated,
|
|
92
|
+
files,
|
|
93
|
+
locale,
|
|
94
|
+
userId,
|
|
95
|
+
submissionId,
|
|
96
|
+
stored,
|
|
97
|
+
delivered: false
|
|
98
|
+
};
|
|
99
|
+
const hooks = useNitroApp().hooks;
|
|
100
|
+
let error;
|
|
101
|
+
try {
|
|
102
|
+
await hooks.callHook("forms:submitted", ctx);
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
105
|
+
console.error(`[forms] delivery of "${form.slug}" failed:`, cause);
|
|
106
|
+
}
|
|
107
|
+
if (stored) {
|
|
108
|
+
await db.update(schema.formSubmissions).set({
|
|
109
|
+
deliveryStatus: error ? "failed" : ctx.delivered ? "delivered" : "none",
|
|
110
|
+
deliveryError: error ?? null
|
|
111
|
+
}).where(eq(schema.formSubmissions.id, submissionId));
|
|
112
|
+
} else if (error) {
|
|
113
|
+
throw createError({ statusCode: 502, statusMessage: "The submission could not be delivered." });
|
|
114
|
+
}
|
|
115
|
+
if (ctx.delivered || error) {
|
|
116
|
+
const result = { submissionId, formSlug: form.slug, status: error ? "failed" : "delivered", error };
|
|
117
|
+
await hooks.callHook("forms:delivered", result);
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, id: submissionId };
|
|
120
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Condition, FormDefinition, FormField, FormStep } from '../../types.js';
|
|
2
|
+
export type Answers = Record<string, unknown>;
|
|
3
|
+
/** Evaluates a `showIf` condition against the answers of the currently visible fields. */
|
|
4
|
+
export declare function evaluateCondition(condition: Condition | undefined, answers: Answers): boolean;
|
|
5
|
+
export type VisibleStep = {
|
|
6
|
+
step: FormStep;
|
|
7
|
+
index: number;
|
|
8
|
+
fields: FormField[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Resolves which steps and fields are visible for the given answers. Fields are walked in
|
|
12
|
+
* definition order and a condition only sees answers of fields that are themselves visible, so a
|
|
13
|
+
* hidden field's stale answer can never keep another field shown. A condition on a field that
|
|
14
|
+
* comes later in the form therefore never matches.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveVisibility(definition: FormDefinition, answers: Answers): VisibleStep[];
|
|
17
|
+
export declare function isFieldVisible(definition: FormDefinition, key: string, answers: Answers): boolean;
|
|
18
|
+
/** Fields that collect an answer (everything except static `markdown`). */
|
|
19
|
+
export declare const isInputField: (field: FormField) => boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const isEmpty = (value) => value === void 0 || value === null || value === "" || value === false || Array.isArray(value) && value.length === 0;
|
|
2
|
+
export function evaluateCondition(condition, answers) {
|
|
3
|
+
if (!condition) return true;
|
|
4
|
+
const answer = answers[condition.field];
|
|
5
|
+
switch (condition.op) {
|
|
6
|
+
case "truthy":
|
|
7
|
+
return !isEmpty(answer);
|
|
8
|
+
case "eq":
|
|
9
|
+
return answer === condition.value;
|
|
10
|
+
case "neq":
|
|
11
|
+
return answer !== condition.value;
|
|
12
|
+
case "in":
|
|
13
|
+
return Array.isArray(condition.value) && condition.value.includes(answer);
|
|
14
|
+
default:
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function resolveVisibility(definition, answers) {
|
|
19
|
+
const effective = {};
|
|
20
|
+
const result = [];
|
|
21
|
+
definition.steps.forEach((step, index) => {
|
|
22
|
+
if (!evaluateCondition(step.showIf, effective)) return;
|
|
23
|
+
const fields = [];
|
|
24
|
+
for (const field of step.fields) {
|
|
25
|
+
if (!evaluateCondition(field.showIf, effective)) continue;
|
|
26
|
+
fields.push(field);
|
|
27
|
+
if (field.key in answers) effective[field.key] = answers[field.key];
|
|
28
|
+
}
|
|
29
|
+
result.push({ step, index, fields });
|
|
30
|
+
});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
export function isFieldVisible(definition, key, answers) {
|
|
34
|
+
return resolveVisibility(definition, answers).some(({ fields }) => fields.some((field) => field.key === key));
|
|
35
|
+
}
|
|
36
|
+
export const isInputField = (field) => field.type !== "markdown";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { FormDefinition } from '../../types.js';
|
|
3
|
+
export declare const formDefinitionSchema: z.ZodObject<{
|
|
4
|
+
mode: z.ZodEnum<{
|
|
5
|
+
single: "single";
|
|
6
|
+
stepped: "stepped";
|
|
7
|
+
}>;
|
|
8
|
+
storage: z.ZodDefault<z.ZodEnum<{
|
|
9
|
+
db: "db";
|
|
10
|
+
none: "none";
|
|
11
|
+
}>>;
|
|
12
|
+
steps: z.ZodArray<z.ZodObject<{
|
|
13
|
+
id: z.ZodString;
|
|
14
|
+
title: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
15
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
16
|
+
showIf: z.ZodOptional<z.ZodObject<{
|
|
17
|
+
field: z.ZodString;
|
|
18
|
+
op: z.ZodEnum<{
|
|
19
|
+
eq: "eq";
|
|
20
|
+
neq: "neq";
|
|
21
|
+
in: "in";
|
|
22
|
+
truthy: "truthy";
|
|
23
|
+
}>;
|
|
24
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
25
|
+
}, z.core.$strip>>;
|
|
26
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
27
|
+
key: z.ZodString;
|
|
28
|
+
type: z.ZodEnum<{
|
|
29
|
+
number: "number";
|
|
30
|
+
text: "text";
|
|
31
|
+
email: "email";
|
|
32
|
+
tel: "tel";
|
|
33
|
+
textarea: "textarea";
|
|
34
|
+
select: "select";
|
|
35
|
+
radio: "radio";
|
|
36
|
+
checkbox: "checkbox";
|
|
37
|
+
date: "date";
|
|
38
|
+
file: "file";
|
|
39
|
+
markdown: "markdown";
|
|
40
|
+
}>;
|
|
41
|
+
content: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
42
|
+
label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
43
|
+
description: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
44
|
+
placeholder: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
45
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
46
|
+
options: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
47
|
+
value: z.ZodString;
|
|
48
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
49
|
+
}, z.core.$strip>>>;
|
|
50
|
+
multiple: z.ZodOptional<z.ZodBoolean>;
|
|
51
|
+
autocomplete: z.ZodOptional<z.ZodString>;
|
|
52
|
+
validation: z.ZodOptional<z.ZodObject<{
|
|
53
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
55
|
+
minLength: z.ZodOptional<z.ZodNumber>;
|
|
56
|
+
maxLength: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
pattern: z.ZodOptional<z.ZodString>;
|
|
58
|
+
maxFiles: z.ZodOptional<z.ZodNumber>;
|
|
59
|
+
maxSize: z.ZodOptional<z.ZodNumber>;
|
|
60
|
+
accept: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
61
|
+
message: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
62
|
+
}, z.core.$strip>>;
|
|
63
|
+
showIf: z.ZodOptional<z.ZodObject<{
|
|
64
|
+
field: z.ZodString;
|
|
65
|
+
op: z.ZodEnum<{
|
|
66
|
+
eq: "eq";
|
|
67
|
+
neq: "neq";
|
|
68
|
+
in: "in";
|
|
69
|
+
truthy: "truthy";
|
|
70
|
+
}>;
|
|
71
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
72
|
+
}, z.core.$strip>>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
submit: z.ZodOptional<z.ZodObject<{
|
|
76
|
+
label: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
77
|
+
successMessage: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
78
|
+
redirect: z.ZodOptional<z.ZodString>;
|
|
79
|
+
allowResubmit: z.ZodOptional<z.ZodObject<{
|
|
80
|
+
keep: z.ZodArray<z.ZodString>;
|
|
81
|
+
}, z.core.$strip>>;
|
|
82
|
+
}, z.core.$strip>>;
|
|
83
|
+
delivery: z.ZodOptional<z.ZodObject<{
|
|
84
|
+
email: z.ZodOptional<z.ZodObject<{
|
|
85
|
+
to: z.ZodArray<z.ZodString>;
|
|
86
|
+
subject: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
87
|
+
}, z.core.$strip>>;
|
|
88
|
+
}, z.core.$strip>>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
export type FormDefinitionInput = z.input<typeof formDefinitionSchema>;
|
|
91
|
+
export declare function parseFormDefinition(input: unknown): FormDefinition;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const translated = z.union([z.string(), z.record(z.string(), z.string())]);
|
|
3
|
+
const KEY = /^[a-z]\w*$/i;
|
|
4
|
+
const ID = /^[a-z0-9][\w-]*$/i;
|
|
5
|
+
const condition = z.object({
|
|
6
|
+
field: z.string().regex(KEY),
|
|
7
|
+
op: z.enum(["eq", "neq", "in", "truthy"]),
|
|
8
|
+
value: z.unknown().optional()
|
|
9
|
+
});
|
|
10
|
+
const field = z.object({
|
|
11
|
+
key: z.string().regex(KEY, "Field keys must start with a letter and contain only letters, digits and underscores."),
|
|
12
|
+
type: z.enum(["text", "email", "tel", "number", "textarea", "select", "radio", "checkbox", "date", "file", "markdown"]),
|
|
13
|
+
content: translated.optional(),
|
|
14
|
+
label: translated.optional(),
|
|
15
|
+
description: translated.optional(),
|
|
16
|
+
placeholder: translated.optional(),
|
|
17
|
+
required: z.boolean().optional(),
|
|
18
|
+
options: z.array(z.object({ value: z.string().min(1), label: translated })).optional(),
|
|
19
|
+
multiple: z.boolean().optional(),
|
|
20
|
+
autocomplete: z.string().max(40).optional(),
|
|
21
|
+
validation: z.object({
|
|
22
|
+
min: z.number().optional(),
|
|
23
|
+
max: z.number().optional(),
|
|
24
|
+
minLength: z.number().int().min(0).optional(),
|
|
25
|
+
maxLength: z.number().int().min(1).optional(),
|
|
26
|
+
pattern: z.string().max(500).optional(),
|
|
27
|
+
maxFiles: z.number().int().min(1).max(50).optional(),
|
|
28
|
+
maxSize: z.number().int().min(1).optional(),
|
|
29
|
+
accept: z.array(z.string()).optional(),
|
|
30
|
+
message: translated.optional()
|
|
31
|
+
}).optional(),
|
|
32
|
+
showIf: condition.optional()
|
|
33
|
+
});
|
|
34
|
+
const step = z.object({
|
|
35
|
+
id: z.string().regex(ID),
|
|
36
|
+
title: translated.optional(),
|
|
37
|
+
description: translated.optional(),
|
|
38
|
+
showIf: condition.optional(),
|
|
39
|
+
fields: z.array(field)
|
|
40
|
+
});
|
|
41
|
+
const sameOriginPath = (value) => value.startsWith("/") && !value.startsWith("//") && !value.startsWith("/\\");
|
|
42
|
+
const emailAddress = z.string().regex(/^[^\s@${}]+@[^\s$@{}][^\s$.@{}]*\.[^\s$@{}]+$/, "Recipients must be plain email addresses.");
|
|
43
|
+
export const formDefinitionSchema = z.object({
|
|
44
|
+
mode: z.enum(["single", "stepped"]),
|
|
45
|
+
storage: z.enum(["db", "none"]).default("db"),
|
|
46
|
+
steps: z.array(step).min(1),
|
|
47
|
+
submit: z.object({
|
|
48
|
+
label: translated.optional(),
|
|
49
|
+
successMessage: translated.optional(),
|
|
50
|
+
redirect: z.string().refine(sameOriginPath, "Redirect must be a path on this site.").optional(),
|
|
51
|
+
allowResubmit: z.object({ keep: z.array(z.string()) }).optional()
|
|
52
|
+
}).optional(),
|
|
53
|
+
delivery: z.object({
|
|
54
|
+
email: z.object({ to: z.array(emailAddress).min(1), subject: translated.optional() }).optional()
|
|
55
|
+
}).optional()
|
|
56
|
+
}).superRefine((definition, ctx) => {
|
|
57
|
+
const fail = (message, path) => ctx.addIssue({ code: "custom", message, path });
|
|
58
|
+
if (definition.mode === "single" && definition.steps.length !== 1) {
|
|
59
|
+
fail("A single page form has exactly one step.", ["steps"]);
|
|
60
|
+
}
|
|
61
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
62
|
+
const keys = /* @__PURE__ */ new Map();
|
|
63
|
+
definition.steps.forEach((s, stepIndex) => {
|
|
64
|
+
if (stepIds.has(s.id)) fail(`Duplicate step id "${s.id}".`, ["steps", stepIndex, "id"]);
|
|
65
|
+
stepIds.add(s.id);
|
|
66
|
+
s.fields.forEach((f, fieldIndex) => {
|
|
67
|
+
const path = ["steps", stepIndex, "fields", fieldIndex];
|
|
68
|
+
if (keys.has(f.key)) fail(`Duplicate field key "${f.key}".`, [...path, "key"]);
|
|
69
|
+
keys.set(f.key, f.type);
|
|
70
|
+
if ((f.type === "select" || f.type === "radio") && !f.options?.length) {
|
|
71
|
+
fail("Select and radio fields need options.", [...path, "options"]);
|
|
72
|
+
}
|
|
73
|
+
if (f.type === "markdown" && !f.content) fail("Markdown fields need content.", [...path, "content"]);
|
|
74
|
+
if (f.type === "file" && definition.storage === "none") {
|
|
75
|
+
fail('File fields need a form that stores submissions (storage "db").', [...path, "type"]);
|
|
76
|
+
}
|
|
77
|
+
if (f.validation?.pattern) {
|
|
78
|
+
try {
|
|
79
|
+
new RegExp(f.validation.pattern);
|
|
80
|
+
} catch {
|
|
81
|
+
fail("Invalid regular expression.", [...path, "validation", "pattern"]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
definition.steps.forEach((s, stepIndex) => {
|
|
88
|
+
const check = (cond, path) => {
|
|
89
|
+
if (cond && !seen.has(cond.field)) fail(`Condition refers to unknown or later field "${cond.field}".`, path);
|
|
90
|
+
};
|
|
91
|
+
check(s.showIf, ["steps", stepIndex, "showIf", "field"]);
|
|
92
|
+
s.fields.forEach((f, fieldIndex) => {
|
|
93
|
+
check(f.showIf, ["steps", stepIndex, "fields", fieldIndex, "showIf", "field"]);
|
|
94
|
+
if (f.type !== "markdown") seen.add(f.key);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
definition.submit?.allowResubmit?.keep.forEach((key, index) => {
|
|
98
|
+
if (!keys.has(key)) fail(`Unknown field "${key}".`, ["submit", "allowResubmit", "keep", index]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
export function parseFormDefinition(input) {
|
|
102
|
+
return formDefinitionSchema.parse(input);
|
|
103
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FormDefinition } from '../../types.js';
|
|
2
|
+
export type * from '../../types';
|
|
3
|
+
export { translate, interpolate, defaultMessages, newId } from './text.js';
|
|
4
|
+
export type { MessageTranslator, TranslateParams } from './text.js';
|
|
5
|
+
export { evaluateCondition, resolveVisibility, isFieldVisible, isInputField } from './conditions.js';
|
|
6
|
+
export type { Answers, VisibleStep } from './conditions.js';
|
|
7
|
+
export { buildFormSchema, buildFieldSchema, validatedFields } from './schema.js';
|
|
8
|
+
export type { SchemaOptions } from './schema.js';
|
|
9
|
+
export { formDefinitionSchema, parseFormDefinition } from './definition.js';
|
|
10
|
+
export type { FormDefinitionInput } from './definition.js';
|
|
11
|
+
/** Typed identity helper for authoring definitions (seed files, tests). */
|
|
12
|
+
export declare function defineForm<const T extends FormDefinition>(definition: T): T;
|
|
13
|
+
export { renderMarkdown } from './markdown.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { translate, interpolate, defaultMessages, newId } from "./text.js";
|
|
2
|
+
export { evaluateCondition, resolveVisibility, isFieldVisible, isInputField } from "./conditions.js";
|
|
3
|
+
export { buildFormSchema, buildFieldSchema, validatedFields } from "./schema.js";
|
|
4
|
+
export { formDefinitionSchema, parseFormDefinition } from "./definition.js";
|
|
5
|
+
export function defineForm(definition) {
|
|
6
|
+
return definition;
|
|
7
|
+
}
|
|
8
|
+
export { renderMarkdown } from "./markdown.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const escape = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2
|
+
const safeHref = (href) => /^(?:https?:\/\/|\/(?![/\\])|mailto:)/.test(href);
|
|
3
|
+
export function renderMarkdown(source) {
|
|
4
|
+
return source.split(/\n{2,}/).map((paragraph) => escape(paragraph.trim()).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\b_(.+?)_\b/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, label, href) => safeHref(href.replace(/&/g, "&")) ? `<a href="${href}" target="_blank" rel="noopener" class="underline">${label}</a>` : match).replace(/\n/g, "<br>")).filter(Boolean).map((paragraph) => `<p>${paragraph}</p>`).join("");
|
|
5
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { FormDefinition, FormField } from '../../types.js';
|
|
3
|
+
import type { Answers } from './conditions.js';
|
|
4
|
+
import type { MessageTranslator } from './text.js';
|
|
5
|
+
export type SchemaOptions = {
|
|
6
|
+
/** Translates the messages of rules the compiler adds itself. Defaults to English. */
|
|
7
|
+
t?: MessageTranslator;
|
|
8
|
+
/** Locale used for `validation.message` overrides from the definition. */
|
|
9
|
+
locale?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Applied when a file field sets no `maxSize` / `maxFiles`, so uploads are always bounded. */
|
|
12
|
+
export declare const DEFAULT_MAX_FILE_SIZE = 10000000;
|
|
13
|
+
export declare const DEFAULT_MAX_FILES = 10;
|
|
14
|
+
/** Builds the zod schema of a single field. */
|
|
15
|
+
export declare function buildFieldSchema(field: FormField, options?: SchemaOptions): z.ZodType;
|
|
16
|
+
/** Fields (with their step index) that take part in validation for these answers. */
|
|
17
|
+
export declare function validatedFields(definition: FormDefinition, answers: Answers, stepIndex?: number): FormField[];
|
|
18
|
+
/**
|
|
19
|
+
* Compiles the definition into a zod object for the answers given. Only visible fields are part
|
|
20
|
+
* of the schema, so hidden fields are never required and their answers are stripped.
|
|
21
|
+
* Pass `stepIndex` to validate one step (stepped client), omit it for the whole form (server).
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildFormSchema(definition: FormDefinition, answers: Answers, options?: SchemaOptions & {
|
|
24
|
+
stepIndex?: number;
|
|
25
|
+
}): z.ZodObject<{
|
|
26
|
+
[x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
27
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isInputField, resolveVisibility } from "./conditions.js";
|
|
3
|
+
import { defaultMessages, translate } from "./text.js";
|
|
4
|
+
const EMAIL = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
|
5
|
+
const PHONE = /^[+()\d\s-]{5,}$/;
|
|
6
|
+
const DATE = /^\d{4}-\d{2}-\d{2}/;
|
|
7
|
+
export const DEFAULT_MAX_FILE_SIZE = 1e7;
|
|
8
|
+
export const DEFAULT_MAX_FILES = 10;
|
|
9
|
+
const fileSchema = z.object({ name: z.string(), size: z.number(), type: z.string() });
|
|
10
|
+
const matchesAccept = (type, accept) => accept.some((rule) => rule.endsWith("/*") ? type.startsWith(rule.slice(0, -1)) : type === rule);
|
|
11
|
+
export function buildFieldSchema(field, options = {}) {
|
|
12
|
+
const t = options.t ?? defaultMessages;
|
|
13
|
+
const rules = field.validation ?? {};
|
|
14
|
+
const required = Boolean(field.required);
|
|
15
|
+
const override = rules.message ? translate(rules.message, options.locale) : void 0;
|
|
16
|
+
const message = (key, fallback, params) => override ?? t(`validation.${key}`, fallback, params);
|
|
17
|
+
const requiredMessage = () => message("required", "This field is required.");
|
|
18
|
+
if (field.type === "checkbox") {
|
|
19
|
+
const boolean = z.boolean({ error: requiredMessage });
|
|
20
|
+
return required ? boolean.refine((value) => value === true, { error: requiredMessage }) : boolean.optional();
|
|
21
|
+
}
|
|
22
|
+
if (field.type === "file") {
|
|
23
|
+
const files = z.array(fileSchema, { error: requiredMessage }).superRefine((value, ctx) => {
|
|
24
|
+
if (required && value.length === 0) ctx.addIssue({ code: "custom", message: requiredMessage() });
|
|
25
|
+
const maxFiles = rules.maxFiles ?? (field.multiple ? DEFAULT_MAX_FILES : 1);
|
|
26
|
+
if (value.length > maxFiles) {
|
|
27
|
+
ctx.addIssue({ code: "custom", message: message("maxFiles", "Upload at most {max} file(s).", { max: maxFiles }) });
|
|
28
|
+
}
|
|
29
|
+
const maxSize = rules.maxSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
30
|
+
for (const file of value) {
|
|
31
|
+
if (file.size > maxSize) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: "custom",
|
|
34
|
+
message: message("maxSize", "{name} is larger than {max} MB.", { name: file.name, max: Math.round(maxSize / 1e6 * 10) / 10 })
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (rules.accept?.length && !matchesAccept(file.type, rules.accept)) {
|
|
38
|
+
ctx.addIssue({ code: "custom", message: message("accept", "{name} is not an accepted file type.", { name: file.name }) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return required ? files : files.optional();
|
|
43
|
+
}
|
|
44
|
+
if (field.type === "number") {
|
|
45
|
+
const number = z.number({ error: requiredMessage }).superRefine((value, ctx) => {
|
|
46
|
+
if (rules.min !== void 0 && value < rules.min) {
|
|
47
|
+
ctx.addIssue({ code: "custom", message: message("min", "Must be at least {min}.", { min: rules.min }) });
|
|
48
|
+
}
|
|
49
|
+
if (rules.max !== void 0 && value > rules.max) {
|
|
50
|
+
ctx.addIssue({ code: "custom", message: message("max", "Must be at most {max}.", { max: rules.max }) });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return required ? number : number.nullish();
|
|
54
|
+
}
|
|
55
|
+
const allowed = field.options?.map((option) => option.value);
|
|
56
|
+
const text = z.string({ error: requiredMessage }).superRefine((raw, ctx) => {
|
|
57
|
+
const value = raw.trim();
|
|
58
|
+
if (!value) {
|
|
59
|
+
if (required) ctx.addIssue({ code: "custom", message: requiredMessage() });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (field.type === "email" && !EMAIL.test(value)) {
|
|
63
|
+
ctx.addIssue({ code: "custom", message: message("email", "Enter a valid email address.") });
|
|
64
|
+
}
|
|
65
|
+
if (field.type === "tel" && !PHONE.test(value)) {
|
|
66
|
+
ctx.addIssue({ code: "custom", message: message("tel", "Enter a valid phone number.") });
|
|
67
|
+
}
|
|
68
|
+
if (field.type === "date" && !DATE.test(value)) {
|
|
69
|
+
ctx.addIssue({ code: "custom", message: message("date", "Enter a valid date.") });
|
|
70
|
+
}
|
|
71
|
+
if ((field.type === "select" || field.type === "radio") && allowed && !allowed.includes(value)) {
|
|
72
|
+
ctx.addIssue({ code: "custom", message: message("option", "Choose one of the available options.") });
|
|
73
|
+
}
|
|
74
|
+
if (rules.minLength !== void 0 && value.length < rules.minLength) {
|
|
75
|
+
ctx.addIssue({ code: "custom", message: message("minLength", "Enter at least {min} characters.", { min: rules.minLength }) });
|
|
76
|
+
}
|
|
77
|
+
if (rules.maxLength !== void 0 && value.length > rules.maxLength) {
|
|
78
|
+
ctx.addIssue({ code: "custom", message: message("maxLength", "Enter at most {max} characters.", { max: rules.maxLength }) });
|
|
79
|
+
}
|
|
80
|
+
if (rules.pattern && !new RegExp(rules.pattern).test(value)) {
|
|
81
|
+
ctx.addIssue({ code: "custom", message: message("pattern", "The value has an invalid format.") });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return required ? text : text.optional();
|
|
85
|
+
}
|
|
86
|
+
export function validatedFields(definition, answers, stepIndex) {
|
|
87
|
+
return resolveVisibility(definition, answers).filter((visible) => stepIndex === void 0 || visible.index === stepIndex).flatMap((visible) => visible.fields).filter(isInputField);
|
|
88
|
+
}
|
|
89
|
+
export function buildFormSchema(definition, answers, options = {}) {
|
|
90
|
+
const shape = {};
|
|
91
|
+
for (const field of validatedFields(definition, answers, options.stepIndex)) {
|
|
92
|
+
shape[field.key] = buildFieldSchema(field, options);
|
|
93
|
+
}
|
|
94
|
+
return z.object(shape);
|
|
95
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Translated } from '../../types.js';
|
|
2
|
+
/** Picks the text for a locale, falling back to `en`, then to the first available entry. */
|
|
3
|
+
export declare function translate(value: Translated | null | undefined, locale?: string): string;
|
|
4
|
+
export type TranslateParams = Record<string, string | number>;
|
|
5
|
+
/** `(key, englishFallback, params) => text`, the shape of `useModuleT('forms')`. */
|
|
6
|
+
export type MessageTranslator = (key: string, fallback: string, params?: TranslateParams) => string;
|
|
7
|
+
/** Replaces `{name}` placeholders. */
|
|
8
|
+
export declare function interpolate(text: string, params?: TranslateParams): string;
|
|
9
|
+
/** English-only translator, used on the server and wherever no i18n is available. */
|
|
10
|
+
export declare const defaultMessages: MessageTranslator;
|
|
11
|
+
/** Random, prefixed id (`frm_...`), Web Crypto only so it works on Workers. */
|
|
12
|
+
export declare function newId(prefix: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function translate(value, locale = "en") {
|
|
2
|
+
if (!value) return "";
|
|
3
|
+
if (typeof value === "string") return value;
|
|
4
|
+
return value[locale] ?? value.en ?? Object.values(value)[0] ?? "";
|
|
5
|
+
}
|
|
6
|
+
export function interpolate(text, params = {}) {
|
|
7
|
+
return Object.entries(params).reduce((result, [name, value]) => result.replaceAll(`{${name}}`, String(value)), text);
|
|
8
|
+
}
|
|
9
|
+
export const defaultMessages = (_key, fallback, params) => interpolate(fallback, params);
|
|
10
|
+
export function newId(prefix) {
|
|
11
|
+
return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
12
|
+
}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { default } from './module.mjs'
|
|
2
|
+
|
|
3
|
+
export { type AuthorizeContext, type Condition, type ConditionOp, type DeliveredContext, type DeliveryStatus, type FieldOption, type FieldType, type FieldValidation, type FileDescriptor, type FormAdminDto, type FormDefinition, type FormDelivery, type FormField, type FormMode, type FormStatus, type FormStep, type FormStorage, type FormSubmitSettings, type FormSummaryDto, type FormsRuntimeConfig, type ModuleOptions, type PublicFormDto, type PublicModuleOptions, type SeedForm, type SubmissionDto, type SubmissionFileDto, type SubmissionSummaryDto, type SubmitResponse, type SubmittedContext, type SubmittedFile, type Translated } from './module.mjs'
|
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/forms",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Dynamic, database-stored forms (single page or stepped) with submissions, file uploads and email delivery for the frigear ecosystem.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/frigear-nu/frigear-nu.git",
|
|
8
|
+
"directory": "packages/forms"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/module.mjs",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/types.d.mts",
|
|
16
|
+
"import": "./dist/module.mjs",
|
|
17
|
+
"default": "./dist/module.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./utils": {
|
|
20
|
+
"types": "./dist/runtime/utils/index.d.ts",
|
|
21
|
+
"import": "./dist/runtime/utils/index.js",
|
|
22
|
+
"default": "./dist/runtime/utils/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"workspaces": [
|
|
29
|
+
"playground"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@frigear-nu/menu": "0.0.3",
|
|
33
|
+
"@frigear-nu/module-kit": "0.1.2",
|
|
34
|
+
"@frigear-nu/notify": "0.0.3",
|
|
35
|
+
"@frigear-nu/users": "1.0.3",
|
|
36
|
+
"@libsql/client": "^0.18.0",
|
|
37
|
+
"@nuxt/kit": "^4.6.0",
|
|
38
|
+
"drizzle-kit": "^0.31.11",
|
|
39
|
+
"drizzle-orm": "^0.45.3",
|
|
40
|
+
"h3": "^1.15.11",
|
|
41
|
+
"unstorage": "^1.17.5",
|
|
42
|
+
"zod": "^4.6.5"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@nuxt/ui": "^4.10.0",
|
|
46
|
+
"@nuxthub/core": "^0.10.8"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@nuxt/ui": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@nuxt/devtools": "^3.4.2",
|
|
55
|
+
"@nuxt/eslint-config": "^1.17.0",
|
|
56
|
+
"@nuxt/module-builder": "^1.0.3",
|
|
57
|
+
"@nuxt/schema": "^4.6.0",
|
|
58
|
+
"@nuxt/test-utils": "^4.3.2",
|
|
59
|
+
"@nuxt/ui": "^4.11.0",
|
|
60
|
+
"@nuxthub/core": "^0.10.8",
|
|
61
|
+
"@types/node": "latest",
|
|
62
|
+
"changelogen": "^0.6.2",
|
|
63
|
+
"eslint": "^10.10.0",
|
|
64
|
+
"nuxt": "^4.6.0",
|
|
65
|
+
"typescript": "^6.0.3",
|
|
66
|
+
"vitest": "^5.0.1",
|
|
67
|
+
"vue-tsc": "^3.3.11"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"postinstall": "nuxt-module-build build --stub && nuxt prepare",
|
|
71
|
+
"build": "nuxt-module-build build",
|
|
72
|
+
"dev": "npm run dev:prepare && nuxt dev playground",
|
|
73
|
+
"dev:build": "nuxt build playground",
|
|
74
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
75
|
+
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
76
|
+
"lint": "eslint .",
|
|
77
|
+
"lint:fix": "eslint . --fix",
|
|
78
|
+
"test": "vitest run",
|
|
79
|
+
"test:watch": "vitest watch",
|
|
80
|
+
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
|
81
|
+
},
|
|
82
|
+
"typesVersions": {
|
|
83
|
+
"*": {
|
|
84
|
+
".": [
|
|
85
|
+
"./dist/types.d.mts"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
6
89
|
}
|