@olinfo/quizms-training 5.0.0 → 5.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/dist/entry/provider.js +92 -80
- package/dist/entry/provider.js.map +1 -1
- package/dist/entry.d.ts +1 -1
- package/package.json +15 -12
- package/src/entry/iframe.ts +0 -131
- package/src/entry/index.tsx +0 -2
- package/src/entry/provider.tsx +0 -193
- package/src/entry/statement.tsx +0 -30
- package/src/vite/entry.ts +0 -47
- package/src/vite/index.ts +0 -4
- package/src/vite/preset.ts +0 -45
- package/tailwind.config.mjs +0 -7
- package/tsconfig.json +0 -28
- package/vite.config.mjs +0 -26
package/dist/entry/provider.js
CHANGED
|
@@ -1,110 +1,122 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { useState as
|
|
3
|
-
import { TitleProvider as
|
|
4
|
-
import { variantSchema as
|
|
5
|
-
import { StudentProvider as
|
|
6
|
-
import { Rng as
|
|
7
|
-
import { addMinutes as
|
|
8
|
-
import
|
|
9
|
-
import { getTitleIframe as
|
|
10
|
-
import { TrainingStatementContext as
|
|
11
|
-
function
|
|
12
|
-
contest:
|
|
13
|
-
children:
|
|
1
|
+
import { jsx as f } from "react/jsx-runtime";
|
|
2
|
+
import { useState as D, useMemo as v, useEffect as P, useCallback as o } from "react";
|
|
3
|
+
import { TitleProvider as R } from "@olinfo/quizms/components";
|
|
4
|
+
import { variantSchema as V } from "@olinfo/quizms/models";
|
|
5
|
+
import { StudentProvider as j } from "@olinfo/quizms/student";
|
|
6
|
+
import { Rng as E, validate as k } from "@olinfo/quizms/utils";
|
|
7
|
+
import { isFuture as x, addMinutes as C, subSeconds as F } from "date-fns";
|
|
8
|
+
import l, { preload as M } from "swr";
|
|
9
|
+
import { getTitleIframe as $, getStudentIframe as z, setAnswersIframe as L, submitIframe as N, resetIframe as O, logoutIframe as U, startIframe as q } from "./iframe.js";
|
|
10
|
+
import { TrainingStatementContext as B } from "./statement.js";
|
|
11
|
+
function G({
|
|
12
|
+
contest: t,
|
|
13
|
+
children: m
|
|
14
14
|
}) {
|
|
15
|
-
const { data: d } =
|
|
15
|
+
const { data: d } = l("getTitle", $, { suspense: !0 }), { data: s, mutate: p } = l(["student", t.id], z, {
|
|
16
16
|
suspense: !0
|
|
17
|
-
}), [
|
|
18
|
-
|
|
19
|
-
), { student:
|
|
20
|
-
() =>
|
|
21
|
-
[
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
}), [w, y] = D(
|
|
18
|
+
H(t)
|
|
19
|
+
), { student: r, type: u } = v(
|
|
20
|
+
() => s ? { type: "iframe", student: s } : { type: "anonymous", student: w },
|
|
21
|
+
[s, w]
|
|
22
|
+
);
|
|
23
|
+
P(() => {
|
|
24
|
+
if (u !== "anonymous" || !(r.participationWindow && x(r.participationWindow.end))) return;
|
|
25
|
+
const i = new AbortController();
|
|
26
|
+
return window.addEventListener(
|
|
27
|
+
"beforeunload",
|
|
28
|
+
(a) => {
|
|
29
|
+
a.preventDefault();
|
|
30
|
+
},
|
|
31
|
+
{ signal: i.signal }
|
|
32
|
+
), () => i.abort();
|
|
33
|
+
}, [u, r.participationWindow]);
|
|
34
|
+
const { data: c } = l(
|
|
35
|
+
r.variantId && ["variant", t.id, r.variantId],
|
|
36
|
+
g,
|
|
25
37
|
{
|
|
26
38
|
revalidateOnFocus: !1,
|
|
27
39
|
revalidateIfStale: !1
|
|
28
40
|
}
|
|
29
|
-
),
|
|
30
|
-
async (
|
|
31
|
-
|
|
32
|
-
optimisticData: (
|
|
41
|
+
), e = o(
|
|
42
|
+
async (n, i) => {
|
|
43
|
+
u === "anonymous" ? y(i) : await p((a) => a ? n(i(a), t) : null, {
|
|
44
|
+
optimisticData: (a) => a ? i(a) : null
|
|
33
45
|
});
|
|
34
46
|
},
|
|
35
|
-
[
|
|
36
|
-
),
|
|
47
|
+
[u, t, p]
|
|
48
|
+
), I = v(
|
|
37
49
|
() => ({
|
|
38
50
|
id: "",
|
|
39
51
|
schoolId: "",
|
|
40
|
-
contestId:
|
|
52
|
+
contestId: t.id,
|
|
41
53
|
name: "",
|
|
42
|
-
participationWindow:
|
|
54
|
+
participationWindow: r.participationWindow,
|
|
43
55
|
finalized: !1,
|
|
44
56
|
disabled: !1,
|
|
45
57
|
pdfVariants: []
|
|
46
58
|
}),
|
|
47
|
-
[
|
|
48
|
-
),
|
|
49
|
-
async (
|
|
50
|
-
if (!
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
(
|
|
59
|
+
[t.id, r.participationWindow]
|
|
60
|
+
), S = o(
|
|
61
|
+
async (n, i) => {
|
|
62
|
+
if (!c) throw new Error("Variant non loaded");
|
|
63
|
+
await e(
|
|
64
|
+
L(c),
|
|
65
|
+
(a) => ({ ...a, answers: { ...a.answers, [n]: i } })
|
|
54
66
|
);
|
|
55
67
|
},
|
|
56
|
-
[
|
|
57
|
-
), h =
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
(
|
|
61
|
-
...
|
|
62
|
-
participationWindow: { start:
|
|
68
|
+
[e, c]
|
|
69
|
+
), h = o(async () => {
|
|
70
|
+
await e(
|
|
71
|
+
N,
|
|
72
|
+
(n) => ({
|
|
73
|
+
...n,
|
|
74
|
+
participationWindow: { start: n.participationWindow.start, end: /* @__PURE__ */ new Date() }
|
|
63
75
|
})
|
|
64
76
|
);
|
|
65
|
-
}, [
|
|
66
|
-
await
|
|
67
|
-
|
|
68
|
-
(
|
|
69
|
-
...
|
|
77
|
+
}, [e]), W = o(async () => {
|
|
78
|
+
await e(
|
|
79
|
+
O,
|
|
80
|
+
(n) => ({
|
|
81
|
+
...n,
|
|
70
82
|
answers: {},
|
|
71
83
|
participationWindow: void 0
|
|
72
84
|
})
|
|
73
85
|
);
|
|
74
|
-
}, [
|
|
75
|
-
await
|
|
76
|
-
}, [
|
|
77
|
-
const
|
|
78
|
-
await
|
|
79
|
-
|
|
80
|
-
(
|
|
81
|
-
...
|
|
86
|
+
}, [e]), b = o(async () => {
|
|
87
|
+
await e(U, (n) => n);
|
|
88
|
+
}, [e]), T = o(async () => {
|
|
89
|
+
const n = /* @__PURE__ */ new Date(), i = K(t), a = await M(["variant", t.id, i], g);
|
|
90
|
+
await e(
|
|
91
|
+
q(a),
|
|
92
|
+
(A) => ({
|
|
93
|
+
...A,
|
|
82
94
|
variantId: i,
|
|
83
95
|
participationWindow: {
|
|
84
|
-
start:
|
|
85
|
-
end:
|
|
96
|
+
start: F(n, 2),
|
|
97
|
+
end: C(n, t.onlineSettings?.duration ?? 0)
|
|
86
98
|
}
|
|
87
99
|
})
|
|
88
100
|
);
|
|
89
|
-
}, [
|
|
90
|
-
return /* @__PURE__ */
|
|
91
|
-
|
|
101
|
+
}, [t, e]);
|
|
102
|
+
return /* @__PURE__ */ f(R, { title: d, children: /* @__PURE__ */ f(B.Provider, { value: { start: T }, children: /* @__PURE__ */ f(
|
|
103
|
+
j,
|
|
92
104
|
{
|
|
93
|
-
contest:
|
|
94
|
-
venue:
|
|
95
|
-
student:
|
|
96
|
-
setAnswer:
|
|
105
|
+
contest: t,
|
|
106
|
+
venue: I,
|
|
107
|
+
student: r,
|
|
108
|
+
setAnswer: S,
|
|
97
109
|
submit: h,
|
|
98
110
|
reset: W,
|
|
99
|
-
logout:
|
|
111
|
+
logout: b,
|
|
100
112
|
enforceFullscreen: !1,
|
|
101
|
-
schema:
|
|
102
|
-
children:
|
|
113
|
+
schema: c?.schema,
|
|
114
|
+
children: m
|
|
103
115
|
}
|
|
104
116
|
) }) });
|
|
105
117
|
}
|
|
106
|
-
|
|
107
|
-
function
|
|
118
|
+
G.displayName = "TrainingProvider";
|
|
119
|
+
function H(t) {
|
|
108
120
|
return {
|
|
109
121
|
id: "",
|
|
110
122
|
userData: {
|
|
@@ -112,20 +124,20 @@ function G(a) {
|
|
|
112
124
|
surname: "anonimo"
|
|
113
125
|
},
|
|
114
126
|
answers: {},
|
|
115
|
-
contestId:
|
|
127
|
+
contestId: t.id
|
|
116
128
|
};
|
|
117
129
|
}
|
|
118
|
-
const
|
|
119
|
-
function
|
|
120
|
-
return
|
|
130
|
+
const J = new E();
|
|
131
|
+
function K(t) {
|
|
132
|
+
return J.choice(t.variantIds);
|
|
121
133
|
}
|
|
122
|
-
async function
|
|
123
|
-
const d = await fetch(`/variants/${
|
|
134
|
+
async function g([, t, m]) {
|
|
135
|
+
const d = await fetch(`/variants/${t}/${m}/answers.json`);
|
|
124
136
|
if (!d.ok) throw new Error("Failed to fetch schema");
|
|
125
|
-
const
|
|
126
|
-
return
|
|
137
|
+
const s = await d.json();
|
|
138
|
+
return k(V, s);
|
|
127
139
|
}
|
|
128
140
|
export {
|
|
129
|
-
|
|
141
|
+
G as TrainingProvider
|
|
130
142
|
};
|
|
131
143
|
//# sourceMappingURL=provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sources":["../../src/entry/provider.tsx"],"sourcesContent":["import { type ReactNode, useCallback, useMemo, useState } from \"react\";\n\nimport { TitleProvider } from \"@olinfo/quizms/components\";\nimport {\n type Answer,\n type Contest,\n type Student,\n type Variant,\n type VariantsConfig,\n type Venue,\n variantSchema,\n} from \"@olinfo/quizms/models\";\nimport { StudentProvider } from \"@olinfo/quizms/student\";\nimport { Rng, validate } from \"@olinfo/quizms/utils\";\nimport { addMinutes, subSeconds } from \"date-fns\";\nimport useSWR, { preload } from \"swr\";\n\nimport {\n getStudentIframe,\n getTitleIframe,\n logoutIframe,\n resetIframe,\n setAnswersIframe,\n startIframe,\n submitIframe,\n} from \"./iframe\";\nimport { TrainingStatementContext } from \"./statement\";\n\nexport function TrainingProvider({\n contest,\n children,\n}: {\n contest: Contest & VariantsConfig;\n children: ReactNode;\n}) {\n const { data: title } = useSWR(\"getTitle\", getTitleIframe, { suspense: true });\n\n const { data: iframeStudent, mutate } = useSWR([\"student\", contest.id], getStudentIframe, {\n suspense: true,\n });\n const [anonymousStudent, setAnonymousStudent] = useState<Student>(\n createAnonymousStudent(contest),\n );\n\n const { student, type } = useMemo(\n () =>\n iframeStudent\n ? { type: \"iframe\", student: iframeStudent }\n : { type: \"anonymous\", student: anonymousStudent },\n [iframeStudent, anonymousStudent],\n );\n\n const { data: variant } = useSWR(\n student.variantId && [\"variant\", contest.id, student.variantId],\n getVariant,\n {\n revalidateOnFocus: false,\n revalidateIfStale: false,\n },\n );\n\n const updateStudent = useCallback(\n async (\n iframeFn: (student: Student, contest: Contest) => Promise<Student | null>,\n updateFn: (student: Student) => Student,\n ) => {\n if (type === \"anonymous\") {\n setAnonymousStudent(updateFn);\n } else {\n await mutate((student) => (student ? iframeFn(updateFn(student), contest) : null), {\n optimisticData: (student) => (student ? updateFn(student) : null),\n });\n }\n },\n [type, contest, mutate],\n );\n\n const mockVenue: Venue = useMemo(\n () => ({\n id: \"\",\n schoolId: \"\",\n contestId: contest.id,\n name: \"\",\n participationWindow: student.participationWindow,\n finalized: false,\n disabled: false,\n pdfVariants: [],\n }),\n [contest.id, student.participationWindow],\n );\n\n const setAnswer = useCallback(\n async (problemId: string, answer: Answer) => {\n if (!variant) throw new Error(\"Variant non loaded\");\n await updateStudent(\n setAnswersIframe(variant),\n (student) =>\n ({ ...student, answers: { ...student.answers, [problemId]: answer } }) satisfies Student,\n );\n },\n [updateStudent, variant],\n );\n\n const submit = useCallback(async () => {\n await updateStudent(\n submitIframe,\n (student) =>\n ({\n ...student,\n participationWindow: { start: student.participationWindow!.start, end: new Date() },\n }) satisfies Student,\n );\n }, [updateStudent]);\n\n const reset = useCallback(async () => {\n await updateStudent(\n resetIframe,\n (student) =>\n ({\n ...student,\n answers: {},\n participationWindow: undefined,\n }) satisfies Student,\n );\n }, [updateStudent]);\n\n const logout = useCallback(async () => {\n await updateStudent(logoutIframe, (student) => student);\n }, [updateStudent]);\n\n const start = useCallback(async () => {\n const now = new Date();\n const variantId = getRandomVariant(contest);\n const variant = await preload([\"variant\", contest.id, variantId], getVariant);\n await updateStudent(\n startIframe(variant),\n (student) =>\n ({\n ...student,\n variantId,\n participationWindow: {\n start: subSeconds(now, 2),\n end: addMinutes(now, contest.onlineSettings?.duration ?? 0),\n },\n }) satisfies Student,\n );\n }, [contest, updateStudent]);\n\n return (\n <TitleProvider title={title}>\n <TrainingStatementContext.Provider value={{ start }}>\n <StudentProvider\n contest={contest}\n venue={mockVenue}\n student={student}\n setAnswer={setAnswer}\n submit={submit}\n reset={reset}\n logout={logout}\n enforceFullscreen={false}\n schema={variant?.schema}>\n {children}\n </StudentProvider>\n </TrainingStatementContext.Provider>\n </TitleProvider>\n );\n}\nTrainingProvider.displayName = \"TrainingProvider\";\n\nfunction createAnonymousStudent(contest: VariantsConfig): Student {\n return {\n id: \"\",\n userData: {\n name: \"Utente\",\n surname: \"anonimo\",\n },\n answers: {},\n contestId: contest.id,\n };\n}\n\nconst rng = new Rng();\n\nfunction getRandomVariant(contest: VariantsConfig): string {\n return rng.choice(contest.variantIds);\n}\n\nasync function getVariant([, contestId, variantId]: [string, string, string]): Promise<Variant> {\n const resp = await fetch(`/variants/${contestId}/${variantId}/answers.json`);\n if (!resp.ok) throw new Error(\"Failed to fetch schema\");\n const json: unknown = await resp.json();\n return validate(variantSchema, json);\n}\n"],"names":["TrainingProvider","contest","children","title","useSWR","getTitleIframe","iframeStudent","mutate","getStudentIframe","anonymousStudent","setAnonymousStudent","useState","createAnonymousStudent","student","type","useMemo","variant","getVariant","updateStudent","useCallback","iframeFn","updateFn","mockVenue","setAnswer","problemId","answer","setAnswersIframe","submit","submitIframe","reset","resetIframe","logout","logoutIframe","start","now","variantId","getRandomVariant","preload","startIframe","subSeconds","addMinutes","jsx","TitleProvider","TrainingStatementContext","StudentProvider","rng","Rng","contestId","resp","json","validate","variantSchema"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider.js","sources":["../../src/entry/provider.tsx"],"sourcesContent":["import { type ReactNode, useCallback, useEffect, useMemo, useState } from \"react\";\n\nimport { TitleProvider } from \"@olinfo/quizms/components\";\nimport {\n type Answer,\n type Contest,\n type Student,\n type Variant,\n type VariantsConfig,\n type Venue,\n variantSchema,\n} from \"@olinfo/quizms/models\";\nimport { StudentProvider } from \"@olinfo/quizms/student\";\nimport { Rng, validate } from \"@olinfo/quizms/utils\";\nimport { addMinutes, isFuture, subSeconds } from \"date-fns\";\nimport useSWR, { preload } from \"swr\";\n\nimport {\n getStudentIframe,\n getTitleIframe,\n logoutIframe,\n resetIframe,\n setAnswersIframe,\n startIframe,\n submitIframe,\n} from \"./iframe\";\nimport { TrainingStatementContext } from \"./statement\";\n\nexport function TrainingProvider({\n contest,\n children,\n}: {\n contest: Contest & VariantsConfig;\n children: ReactNode;\n}) {\n const { data: title } = useSWR(\"getTitle\", getTitleIframe, { suspense: true });\n\n const { data: iframeStudent, mutate } = useSWR([\"student\", contest.id], getStudentIframe, {\n suspense: true,\n });\n const [anonymousStudent, setAnonymousStudent] = useState<Student>(\n createAnonymousStudent(contest),\n );\n\n const { student, type } = useMemo(\n () =>\n iframeStudent\n ? { type: \"iframe\", student: iframeStudent }\n : { type: \"anonymous\", student: anonymousStudent },\n [iframeStudent, anonymousStudent],\n );\n\n useEffect(() => {\n if (type !== \"anonymous\") return;\n\n const isRunning = student.participationWindow && isFuture(student.participationWindow.end);\n\n if (!isRunning) return;\n\n const controller = new AbortController();\n\n window.addEventListener(\n \"beforeunload\",\n (event) => {\n event.preventDefault();\n },\n { signal: controller.signal },\n );\n\n return () => controller.abort();\n }, [type, student.participationWindow]);\n\n const { data: variant } = useSWR(\n student.variantId && [\"variant\", contest.id, student.variantId],\n getVariant,\n {\n revalidateOnFocus: false,\n revalidateIfStale: false,\n },\n );\n\n const updateStudent = useCallback(\n async (\n iframeFn: (student: Student, contest: Contest) => Promise<Student | null>,\n updateFn: (student: Student) => Student,\n ) => {\n if (type === \"anonymous\") {\n setAnonymousStudent(updateFn);\n } else {\n await mutate((student) => (student ? iframeFn(updateFn(student), contest) : null), {\n optimisticData: (student) => (student ? updateFn(student) : null),\n });\n }\n },\n [type, contest, mutate],\n );\n\n const mockVenue: Venue = useMemo(\n () => ({\n id: \"\",\n schoolId: \"\",\n contestId: contest.id,\n name: \"\",\n participationWindow: student.participationWindow,\n finalized: false,\n disabled: false,\n pdfVariants: [],\n }),\n [contest.id, student.participationWindow],\n );\n\n const setAnswer = useCallback(\n async (problemId: string, answer: Answer) => {\n if (!variant) throw new Error(\"Variant non loaded\");\n await updateStudent(\n setAnswersIframe(variant),\n (student) =>\n ({ ...student, answers: { ...student.answers, [problemId]: answer } }) satisfies Student,\n );\n },\n [updateStudent, variant],\n );\n\n const submit = useCallback(async () => {\n await updateStudent(\n submitIframe,\n (student) =>\n ({\n ...student,\n participationWindow: { start: student.participationWindow!.start, end: new Date() },\n }) satisfies Student,\n );\n }, [updateStudent]);\n\n const reset = useCallback(async () => {\n await updateStudent(\n resetIframe,\n (student) =>\n ({\n ...student,\n answers: {},\n participationWindow: undefined,\n }) satisfies Student,\n );\n }, [updateStudent]);\n\n const logout = useCallback(async () => {\n await updateStudent(logoutIframe, (student) => student);\n }, [updateStudent]);\n\n const start = useCallback(async () => {\n const now = new Date();\n const variantId = getRandomVariant(contest);\n const variant = await preload([\"variant\", contest.id, variantId], getVariant);\n await updateStudent(\n startIframe(variant),\n (student) =>\n ({\n ...student,\n variantId,\n participationWindow: {\n start: subSeconds(now, 2),\n end: addMinutes(now, contest.onlineSettings?.duration ?? 0),\n },\n }) satisfies Student,\n );\n }, [contest, updateStudent]);\n\n return (\n <TitleProvider title={title}>\n <TrainingStatementContext.Provider value={{ start }}>\n <StudentProvider\n contest={contest}\n venue={mockVenue}\n student={student}\n setAnswer={setAnswer}\n submit={submit}\n reset={reset}\n logout={logout}\n enforceFullscreen={false}\n schema={variant?.schema}>\n {children}\n </StudentProvider>\n </TrainingStatementContext.Provider>\n </TitleProvider>\n );\n}\nTrainingProvider.displayName = \"TrainingProvider\";\n\nfunction createAnonymousStudent(contest: VariantsConfig): Student {\n return {\n id: \"\",\n userData: {\n name: \"Utente\",\n surname: \"anonimo\",\n },\n answers: {},\n contestId: contest.id,\n };\n}\n\nconst rng = new Rng();\n\nfunction getRandomVariant(contest: VariantsConfig): string {\n return rng.choice(contest.variantIds);\n}\n\nasync function getVariant([, contestId, variantId]: [string, string, string]): Promise<Variant> {\n const resp = await fetch(`/variants/${contestId}/${variantId}/answers.json`);\n if (!resp.ok) throw new Error(\"Failed to fetch schema\");\n const json: unknown = await resp.json();\n return validate(variantSchema, json);\n}\n"],"names":["TrainingProvider","contest","children","title","useSWR","getTitleIframe","iframeStudent","mutate","getStudentIframe","anonymousStudent","setAnonymousStudent","useState","createAnonymousStudent","student","type","useMemo","useEffect","isFuture","controller","event","variant","getVariant","updateStudent","useCallback","iframeFn","updateFn","mockVenue","setAnswer","problemId","answer","setAnswersIframe","submit","submitIframe","reset","resetIframe","logout","logoutIframe","start","now","variantId","getRandomVariant","preload","startIframe","subSeconds","addMinutes","jsx","TitleProvider","TrainingStatementContext","StudentProvider","rng","Rng","contestId","resp","json","validate","variantSchema"],"mappings":"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"}
|
package/dist/entry.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olinfo/quizms-training",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Alessandro Bortolin <bortolin.alessandro@outlook.it>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,15 +14,18 @@
|
|
|
14
14
|
"default": "./dist/vite.js"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
17
20
|
"dependencies": {
|
|
18
|
-
"@olinfo/react-components": "^0.12.
|
|
21
|
+
"@olinfo/react-components": "^0.12.3",
|
|
19
22
|
"@types/lodash-es": "^4.17.12",
|
|
20
|
-
"date-fns": "^4.
|
|
21
|
-
"lodash-es": "^4.
|
|
22
|
-
"swr": "^2.4.
|
|
23
|
-
"vite": "^7.3.
|
|
24
|
-
"zod": "^4.3
|
|
25
|
-
"@olinfo/quizms": "^5.0.
|
|
23
|
+
"date-fns": "^4.4.0",
|
|
24
|
+
"lodash-es": "^4.18.1",
|
|
25
|
+
"swr": "^2.4.2",
|
|
26
|
+
"vite": "^7.3.6",
|
|
27
|
+
"zod": "^4.4.3",
|
|
28
|
+
"@olinfo/quizms": "^5.0.4"
|
|
26
29
|
},
|
|
27
30
|
"peerDependencies": {
|
|
28
31
|
"react": "^19.2.4"
|
|
@@ -33,12 +36,12 @@
|
|
|
33
36
|
}
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
36
|
-
"@biomejs/biome": "^2.
|
|
37
|
-
"@types/node": "^25.
|
|
38
|
-
"@types/react": "^19.2.
|
|
39
|
+
"@biomejs/biome": "^2.5.6",
|
|
40
|
+
"@types/node": "^25.9.5",
|
|
41
|
+
"@types/react": "^19.2.18",
|
|
39
42
|
"rollup-plugin-node-externals": "^8.1.2",
|
|
40
43
|
"rollup-preserve-directives": "^1.1.3",
|
|
41
|
-
"typescript": "5.
|
|
44
|
+
"typescript": "5.9.3",
|
|
42
45
|
"vite-plugin-dts": "^4.5.4",
|
|
43
46
|
"vite-tsconfig-paths": "^6.1.1"
|
|
44
47
|
},
|
package/src/entry/iframe.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Contest,
|
|
3
|
-
calcScore,
|
|
4
|
-
type Student,
|
|
5
|
-
studentSchema,
|
|
6
|
-
type Variant,
|
|
7
|
-
} from "@olinfo/quizms/models";
|
|
8
|
-
import { validate } from "@olinfo/quizms/utils";
|
|
9
|
-
import { cloneDeepWith, isDate, sumBy } from "lodash-es";
|
|
10
|
-
import z from "zod";
|
|
11
|
-
|
|
12
|
-
export async function getTitleIframe(): Promise<string> {
|
|
13
|
-
if (window.parent === window) return "Quizms";
|
|
14
|
-
|
|
15
|
-
for (let i = 0; i < 50; i++) {
|
|
16
|
-
try {
|
|
17
|
-
return await getResponse({ type: "getTitle" }, z.string(), 200);
|
|
18
|
-
} catch {}
|
|
19
|
-
}
|
|
20
|
-
throw new Error("Failed to connect");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function getStudentIframe([_, contestId]: [string, string]): Promise<Student | null> {
|
|
24
|
-
if (window.parent === window) return null;
|
|
25
|
-
return await getResponse({ type: "getStudent", contestId }, studentSchema.nullable());
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function setAnswersIframe(variant: Variant) {
|
|
29
|
-
return async (student: Student, contest: Contest): Promise<Student | null> => {
|
|
30
|
-
if (window.parent === window) return null;
|
|
31
|
-
|
|
32
|
-
return await getResponse(
|
|
33
|
-
{
|
|
34
|
-
type: "setAnswers",
|
|
35
|
-
contestId: contest.id,
|
|
36
|
-
answers: student.answers,
|
|
37
|
-
score: calcScore(student, variant.schema),
|
|
38
|
-
maxScore: sumBy(Object.values(variant.schema).map((problem) => problem.pointsCorrect)),
|
|
39
|
-
},
|
|
40
|
-
studentSchema,
|
|
41
|
-
);
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function startIframe(variant: Variant) {
|
|
46
|
-
return async (student: Student, contest: Contest): Promise<Student | null> => {
|
|
47
|
-
if (window.parent === window) return null;
|
|
48
|
-
|
|
49
|
-
return await getResponse(
|
|
50
|
-
{
|
|
51
|
-
type: "start",
|
|
52
|
-
contestId: contest.id,
|
|
53
|
-
variantId: student.variantId,
|
|
54
|
-
duration: contest.onlineSettings?.duration ?? 0,
|
|
55
|
-
score: calcScore(student, variant.schema),
|
|
56
|
-
maxScore: sumBy(Object.values(variant.schema).map((problem) => problem.pointsCorrect)),
|
|
57
|
-
},
|
|
58
|
-
studentSchema,
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function submitIframe(_student: Student, contest: Contest): Promise<Student | null> {
|
|
64
|
-
if (window.parent === window) return null;
|
|
65
|
-
return await getResponse({ type: "submit", contestId: contest.id }, studentSchema);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function resetIframe(_student: Student, contest: Contest): Promise<Student | null> {
|
|
69
|
-
if (window.parent === window) return null;
|
|
70
|
-
return await getResponse({ type: "reset", contestId: contest.id }, studentSchema);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export async function logoutIframe(): Promise<null> {
|
|
74
|
-
if (window.parent === window) return null;
|
|
75
|
-
return await getResponse({ type: "logout" }, z.null());
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function getResponse<Message extends { type: string }, Data>(
|
|
79
|
-
message: Message,
|
|
80
|
-
schema: z.ZodType<Data>,
|
|
81
|
-
timeout = 10_000,
|
|
82
|
-
): Promise<Data> {
|
|
83
|
-
const messageId = crypto.randomUUID();
|
|
84
|
-
|
|
85
|
-
const controller = new AbortController();
|
|
86
|
-
const responsePromise = new Promise<Data>((resolve, reject) => {
|
|
87
|
-
window.addEventListener(
|
|
88
|
-
"message",
|
|
89
|
-
(ev) => {
|
|
90
|
-
try {
|
|
91
|
-
const response = validate(
|
|
92
|
-
messageSchema,
|
|
93
|
-
cloneDeepWith(ev.data, (value) => (isDate(value) ? new Date(value) : undefined)),
|
|
94
|
-
);
|
|
95
|
-
if (response.messageId !== messageId) return;
|
|
96
|
-
|
|
97
|
-
if (response.success) {
|
|
98
|
-
resolve(validate(schema, response.data));
|
|
99
|
-
} else {
|
|
100
|
-
reject(new Error(response.error));
|
|
101
|
-
}
|
|
102
|
-
} catch (err) {
|
|
103
|
-
reject(err);
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
{ signal: controller.signal },
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
window.parent.postMessage({ ...message, messageId }, "*");
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const timeoutPromise = new Promise<never>((_, reject) => setTimeout(() => reject(), timeout));
|
|
114
|
-
return await Promise.race([responsePromise, timeoutPromise]);
|
|
115
|
-
} finally {
|
|
116
|
-
controller.abort();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const messageSchema = z.discriminatedUnion("success", [
|
|
121
|
-
z.object({
|
|
122
|
-
success: z.literal(true),
|
|
123
|
-
messageId: z.string(),
|
|
124
|
-
data: z.any(),
|
|
125
|
-
}),
|
|
126
|
-
z.object({
|
|
127
|
-
success: z.literal(false),
|
|
128
|
-
messageId: z.string(),
|
|
129
|
-
error: z.string(),
|
|
130
|
-
}),
|
|
131
|
-
]);
|
package/src/entry/index.tsx
DELETED
package/src/entry/provider.tsx
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { type ReactNode, useCallback, useMemo, useState } from "react";
|
|
2
|
-
|
|
3
|
-
import { TitleProvider } from "@olinfo/quizms/components";
|
|
4
|
-
import {
|
|
5
|
-
type Answer,
|
|
6
|
-
type Contest,
|
|
7
|
-
type Student,
|
|
8
|
-
type Variant,
|
|
9
|
-
type VariantsConfig,
|
|
10
|
-
type Venue,
|
|
11
|
-
variantSchema,
|
|
12
|
-
} from "@olinfo/quizms/models";
|
|
13
|
-
import { StudentProvider } from "@olinfo/quizms/student";
|
|
14
|
-
import { Rng, validate } from "@olinfo/quizms/utils";
|
|
15
|
-
import { addMinutes, subSeconds } from "date-fns";
|
|
16
|
-
import useSWR, { preload } from "swr";
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
getStudentIframe,
|
|
20
|
-
getTitleIframe,
|
|
21
|
-
logoutIframe,
|
|
22
|
-
resetIframe,
|
|
23
|
-
setAnswersIframe,
|
|
24
|
-
startIframe,
|
|
25
|
-
submitIframe,
|
|
26
|
-
} from "./iframe";
|
|
27
|
-
import { TrainingStatementContext } from "./statement";
|
|
28
|
-
|
|
29
|
-
export function TrainingProvider({
|
|
30
|
-
contest,
|
|
31
|
-
children,
|
|
32
|
-
}: {
|
|
33
|
-
contest: Contest & VariantsConfig;
|
|
34
|
-
children: ReactNode;
|
|
35
|
-
}) {
|
|
36
|
-
const { data: title } = useSWR("getTitle", getTitleIframe, { suspense: true });
|
|
37
|
-
|
|
38
|
-
const { data: iframeStudent, mutate } = useSWR(["student", contest.id], getStudentIframe, {
|
|
39
|
-
suspense: true,
|
|
40
|
-
});
|
|
41
|
-
const [anonymousStudent, setAnonymousStudent] = useState<Student>(
|
|
42
|
-
createAnonymousStudent(contest),
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const { student, type } = useMemo(
|
|
46
|
-
() =>
|
|
47
|
-
iframeStudent
|
|
48
|
-
? { type: "iframe", student: iframeStudent }
|
|
49
|
-
: { type: "anonymous", student: anonymousStudent },
|
|
50
|
-
[iframeStudent, anonymousStudent],
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const { data: variant } = useSWR(
|
|
54
|
-
student.variantId && ["variant", contest.id, student.variantId],
|
|
55
|
-
getVariant,
|
|
56
|
-
{
|
|
57
|
-
revalidateOnFocus: false,
|
|
58
|
-
revalidateIfStale: false,
|
|
59
|
-
},
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const updateStudent = useCallback(
|
|
63
|
-
async (
|
|
64
|
-
iframeFn: (student: Student, contest: Contest) => Promise<Student | null>,
|
|
65
|
-
updateFn: (student: Student) => Student,
|
|
66
|
-
) => {
|
|
67
|
-
if (type === "anonymous") {
|
|
68
|
-
setAnonymousStudent(updateFn);
|
|
69
|
-
} else {
|
|
70
|
-
await mutate((student) => (student ? iframeFn(updateFn(student), contest) : null), {
|
|
71
|
-
optimisticData: (student) => (student ? updateFn(student) : null),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
[type, contest, mutate],
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const mockVenue: Venue = useMemo(
|
|
79
|
-
() => ({
|
|
80
|
-
id: "",
|
|
81
|
-
schoolId: "",
|
|
82
|
-
contestId: contest.id,
|
|
83
|
-
name: "",
|
|
84
|
-
participationWindow: student.participationWindow,
|
|
85
|
-
finalized: false,
|
|
86
|
-
disabled: false,
|
|
87
|
-
pdfVariants: [],
|
|
88
|
-
}),
|
|
89
|
-
[contest.id, student.participationWindow],
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const setAnswer = useCallback(
|
|
93
|
-
async (problemId: string, answer: Answer) => {
|
|
94
|
-
if (!variant) throw new Error("Variant non loaded");
|
|
95
|
-
await updateStudent(
|
|
96
|
-
setAnswersIframe(variant),
|
|
97
|
-
(student) =>
|
|
98
|
-
({ ...student, answers: { ...student.answers, [problemId]: answer } }) satisfies Student,
|
|
99
|
-
);
|
|
100
|
-
},
|
|
101
|
-
[updateStudent, variant],
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const submit = useCallback(async () => {
|
|
105
|
-
await updateStudent(
|
|
106
|
-
submitIframe,
|
|
107
|
-
(student) =>
|
|
108
|
-
({
|
|
109
|
-
...student,
|
|
110
|
-
participationWindow: { start: student.participationWindow!.start, end: new Date() },
|
|
111
|
-
}) satisfies Student,
|
|
112
|
-
);
|
|
113
|
-
}, [updateStudent]);
|
|
114
|
-
|
|
115
|
-
const reset = useCallback(async () => {
|
|
116
|
-
await updateStudent(
|
|
117
|
-
resetIframe,
|
|
118
|
-
(student) =>
|
|
119
|
-
({
|
|
120
|
-
...student,
|
|
121
|
-
answers: {},
|
|
122
|
-
participationWindow: undefined,
|
|
123
|
-
}) satisfies Student,
|
|
124
|
-
);
|
|
125
|
-
}, [updateStudent]);
|
|
126
|
-
|
|
127
|
-
const logout = useCallback(async () => {
|
|
128
|
-
await updateStudent(logoutIframe, (student) => student);
|
|
129
|
-
}, [updateStudent]);
|
|
130
|
-
|
|
131
|
-
const start = useCallback(async () => {
|
|
132
|
-
const now = new Date();
|
|
133
|
-
const variantId = getRandomVariant(contest);
|
|
134
|
-
const variant = await preload(["variant", contest.id, variantId], getVariant);
|
|
135
|
-
await updateStudent(
|
|
136
|
-
startIframe(variant),
|
|
137
|
-
(student) =>
|
|
138
|
-
({
|
|
139
|
-
...student,
|
|
140
|
-
variantId,
|
|
141
|
-
participationWindow: {
|
|
142
|
-
start: subSeconds(now, 2),
|
|
143
|
-
end: addMinutes(now, contest.onlineSettings?.duration ?? 0),
|
|
144
|
-
},
|
|
145
|
-
}) satisfies Student,
|
|
146
|
-
);
|
|
147
|
-
}, [contest, updateStudent]);
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<TitleProvider title={title}>
|
|
151
|
-
<TrainingStatementContext.Provider value={{ start }}>
|
|
152
|
-
<StudentProvider
|
|
153
|
-
contest={contest}
|
|
154
|
-
venue={mockVenue}
|
|
155
|
-
student={student}
|
|
156
|
-
setAnswer={setAnswer}
|
|
157
|
-
submit={submit}
|
|
158
|
-
reset={reset}
|
|
159
|
-
logout={logout}
|
|
160
|
-
enforceFullscreen={false}
|
|
161
|
-
schema={variant?.schema}>
|
|
162
|
-
{children}
|
|
163
|
-
</StudentProvider>
|
|
164
|
-
</TrainingStatementContext.Provider>
|
|
165
|
-
</TitleProvider>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
TrainingProvider.displayName = "TrainingProvider";
|
|
169
|
-
|
|
170
|
-
function createAnonymousStudent(contest: VariantsConfig): Student {
|
|
171
|
-
return {
|
|
172
|
-
id: "",
|
|
173
|
-
userData: {
|
|
174
|
-
name: "Utente",
|
|
175
|
-
surname: "anonimo",
|
|
176
|
-
},
|
|
177
|
-
answers: {},
|
|
178
|
-
contestId: contest.id,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const rng = new Rng();
|
|
183
|
-
|
|
184
|
-
function getRandomVariant(contest: VariantsConfig): string {
|
|
185
|
-
return rng.choice(contest.variantIds);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function getVariant([, contestId, variantId]: [string, string, string]): Promise<Variant> {
|
|
189
|
-
const resp = await fetch(`/variants/${contestId}/${variantId}/answers.json`);
|
|
190
|
-
if (!resp.ok) throw new Error("Failed to fetch schema");
|
|
191
|
-
const json: unknown = await resp.json();
|
|
192
|
-
return validate(variantSchema, json);
|
|
193
|
-
}
|
package/src/entry/statement.tsx
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext } from "react";
|
|
2
|
-
|
|
3
|
-
import { RemoteStatement, useStudent } from "@olinfo/quizms/student";
|
|
4
|
-
import { Button } from "@olinfo/react-components";
|
|
5
|
-
|
|
6
|
-
export const TrainingStatementContext = createContext<{
|
|
7
|
-
start?: () => Promise<void> | void;
|
|
8
|
-
}>({});
|
|
9
|
-
|
|
10
|
-
export function TrainingStatement() {
|
|
11
|
-
const { student, contest } = useStudent();
|
|
12
|
-
const { start } = useContext(TrainingStatementContext);
|
|
13
|
-
|
|
14
|
-
if (!student.participationWindow) {
|
|
15
|
-
return (
|
|
16
|
-
<div className="flex h-[50vh] flex-col items-center justify-center">
|
|
17
|
-
<Button className="btn-success btn-lg" onClick={start}>
|
|
18
|
-
Inizia
|
|
19
|
-
</Button>
|
|
20
|
-
</div>
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<RemoteStatement
|
|
26
|
-
statementUrl={() => `/variants/${contest.id}/${student.variantId}/statement.txt`}
|
|
27
|
-
moduleUrl={(id) => `/variants/${contest.id}/${student.variantId}/${id}`}
|
|
28
|
-
/>
|
|
29
|
-
);
|
|
30
|
-
}
|
package/src/vite/entry.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { reactComponentCase } from "@olinfo/quizms/utils";
|
|
2
|
-
import { loadContests } from "@olinfo/quizms/utils-node";
|
|
3
|
-
import { type PluginOption, transformWithEsbuild } from "vite";
|
|
4
|
-
|
|
5
|
-
export default async function trainingEntry(): Promise<PluginOption> {
|
|
6
|
-
const contests = await loadContests();
|
|
7
|
-
|
|
8
|
-
return {
|
|
9
|
-
name: "quizms:training-entry",
|
|
10
|
-
resolveId(id) {
|
|
11
|
-
const [pathname] = id.split("?");
|
|
12
|
-
if (pathname === "virtual:quizms-training-entry") {
|
|
13
|
-
return `\0${id}`;
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
load(id) {
|
|
17
|
-
const [pathname, query] = id.split("?");
|
|
18
|
-
if (pathname === "\0virtual:quizms-training-entry") {
|
|
19
|
-
const params = new URLSearchParams(query);
|
|
20
|
-
const contestId = params.get("id");
|
|
21
|
-
const contest = contests.find((c) => c.id === contestId)!;
|
|
22
|
-
const isDev = params.has("dev");
|
|
23
|
-
|
|
24
|
-
const entry = `
|
|
25
|
-
import { createApp } from "@olinfo/quizms/entry";
|
|
26
|
-
import { TrainingProvider, TrainingStatement } from "@olinfo/quizms-training/entry";
|
|
27
|
-
import { Route, Switch } from "wouter";
|
|
28
|
-
|
|
29
|
-
${isDev ? `import ${reactComponentCase(contest.id)} from "/${contest.entry}";` : ""}
|
|
30
|
-
${contest.header ? `import ${reactComponentCase(contest.id)}Header from "/${contest.header}";` : ""}
|
|
31
|
-
|
|
32
|
-
export default function createTrainingEntry() {
|
|
33
|
-
return createApp(
|
|
34
|
-
<TrainingProvider contest={${JSON.stringify(contest)}}>
|
|
35
|
-
${contest.header ? `<${reactComponentCase(contest.id)}Header />` : ""}
|
|
36
|
-
${isDev ? `<${reactComponentCase(contest.id)} />` : "<TrainingStatement />"}
|
|
37
|
-
</TrainingProvider>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
`;
|
|
41
|
-
return transformWithEsbuild(entry, "virtual-quizms-training-entry.jsx", {
|
|
42
|
-
jsx: "automatic",
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
package/src/vite/index.ts
DELETED
package/src/vite/preset.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { loadContests } from "@olinfo/quizms/utils-node";
|
|
2
|
-
import { defaultClientConditions, defaultClientMainFields, type PluginOption } from "vite";
|
|
3
|
-
|
|
4
|
-
export default async function trainingPreset(): Promise<PluginOption> {
|
|
5
|
-
const contests = await loadContests();
|
|
6
|
-
|
|
7
|
-
return {
|
|
8
|
-
name: "quizms:training-preset",
|
|
9
|
-
apply: "build",
|
|
10
|
-
config() {
|
|
11
|
-
return {
|
|
12
|
-
environments: {
|
|
13
|
-
training: {
|
|
14
|
-
resolve: {
|
|
15
|
-
conditions: [...defaultClientConditions],
|
|
16
|
-
mainFields: [...defaultClientMainFields],
|
|
17
|
-
noExternal: true,
|
|
18
|
-
},
|
|
19
|
-
build: {
|
|
20
|
-
rollupOptions: {
|
|
21
|
-
input: Object.fromEntries(
|
|
22
|
-
contests.map((contest) => [
|
|
23
|
-
contest.id,
|
|
24
|
-
`virtual:quizms-entry?id=${encodeURIComponent(`virtual:quizms-training-entry?id=${contest.id}`)}`,
|
|
25
|
-
]),
|
|
26
|
-
),
|
|
27
|
-
},
|
|
28
|
-
sourcemap: true,
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
buildStart() {
|
|
35
|
-
if (this.environment.name === "training") {
|
|
36
|
-
process.env.QUIZMS_SHOW_SOLUTIONS = "true";
|
|
37
|
-
this.emitFile({
|
|
38
|
-
type: "asset",
|
|
39
|
-
fileName: "contests.json",
|
|
40
|
-
source: JSON.stringify(contests),
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
package/tailwind.config.mjs
DELETED
package/tsconfig.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "esnext",
|
|
4
|
-
"moduleResolution": "bundler",
|
|
5
|
-
"target": "es2022",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"declarationDir": "dist",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"types": ["vite/client"],
|
|
10
|
-
"strict": true,
|
|
11
|
-
"noImplicitAny": true,
|
|
12
|
-
"allowJs": true,
|
|
13
|
-
"skipLibCheck": true,
|
|
14
|
-
"esModuleInterop": true,
|
|
15
|
-
"allowSyntheticDefaultImports": true,
|
|
16
|
-
"forceConsistentCasingInFileNames": true,
|
|
17
|
-
"noFallthroughCasesInSwitch": true,
|
|
18
|
-
"resolveJsonModule": true,
|
|
19
|
-
"isolatedModules": true,
|
|
20
|
-
"baseUrl": "./",
|
|
21
|
-
"paths": {
|
|
22
|
-
"~/*": ["./src/*"]
|
|
23
|
-
},
|
|
24
|
-
"rootDir": "src",
|
|
25
|
-
"outDir": "dist"
|
|
26
|
-
},
|
|
27
|
-
"include": ["src/"]
|
|
28
|
-
}
|
package/vite.config.mjs
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import nodeExternals from "rollup-plugin-node-externals";
|
|
2
|
-
import preserveDirectives from "rollup-preserve-directives";
|
|
3
|
-
import { defineConfig } from "vite";
|
|
4
|
-
import dts from "vite-plugin-dts";
|
|
5
|
-
import tsconfigPaths from "vite-tsconfig-paths";
|
|
6
|
-
|
|
7
|
-
export default defineConfig({
|
|
8
|
-
build: {
|
|
9
|
-
lib: {
|
|
10
|
-
entry: {
|
|
11
|
-
entry: "src/entry/index.tsx",
|
|
12
|
-
vite: "src/vite/index.ts",
|
|
13
|
-
},
|
|
14
|
-
formats: ["es"],
|
|
15
|
-
},
|
|
16
|
-
target: "esnext",
|
|
17
|
-
sourcemap: true,
|
|
18
|
-
rollupOptions: {
|
|
19
|
-
output: {
|
|
20
|
-
preserveModules: true,
|
|
21
|
-
hoistTransitiveImports: false,
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
plugins: [dts({ rollupTypes: true }), nodeExternals(), preserveDirectives(), tsconfigPaths()],
|
|
26
|
-
});
|