@richardmcquiston01/chat-and-react-core 0.5.0
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/LICENSE +201 -0
- package/README.md +51 -0
- package/dist/index.cjs +499 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +238 -0
- package/dist/index.d.ts +238 -0
- package/dist/index.mjs +456 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// src/schema-parser.ts
|
|
2
|
+
var VALID_INPUTS = /* @__PURE__ */ new Set(["text", "textarea", "dropdown", "radio", "checkbox"]);
|
|
3
|
+
var FORM_COMPLETED = "**FORM_COMPLETED**";
|
|
4
|
+
function parseSchema(raw) {
|
|
5
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
6
|
+
throw makeError("Schema must be a non-null object");
|
|
7
|
+
}
|
|
8
|
+
const obj = raw;
|
|
9
|
+
if (obj["x-chat-version"] !== "1") {
|
|
10
|
+
throw makeError('x-chat-version must be "1"');
|
|
11
|
+
}
|
|
12
|
+
if (!Array.isArray(obj["x-chat-pages"])) {
|
|
13
|
+
throw makeError("x-chat-pages must be an array");
|
|
14
|
+
}
|
|
15
|
+
const pages = obj["x-chat-pages"];
|
|
16
|
+
const rules = typeof obj["x-chat-rules"] === "object" && obj["x-chat-rules"] !== null ? obj["x-chat-rules"] : {};
|
|
17
|
+
const pageIds = /* @__PURE__ */ new Set();
|
|
18
|
+
const questionIds = /* @__PURE__ */ new Set();
|
|
19
|
+
for (const page of pages) {
|
|
20
|
+
validatePage(page, pageIds, questionIds, rules);
|
|
21
|
+
}
|
|
22
|
+
for (const page of pages) {
|
|
23
|
+
const p = page;
|
|
24
|
+
for (const rule of p.branching) {
|
|
25
|
+
if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {
|
|
26
|
+
throw makeError(
|
|
27
|
+
`branching destination_id "${rule.destination_id}" does not reference a known page id`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (rule.type === "if" && !questionIds.has(rule.form_element)) {
|
|
31
|
+
throw makeError(
|
|
32
|
+
`branching form_element "${rule.form_element}" does not reference a known question id`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
function validatePage(raw, pageIds, questionIds, rules) {
|
|
40
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
41
|
+
throw makeError("Each page must be a non-null object");
|
|
42
|
+
}
|
|
43
|
+
const page = raw;
|
|
44
|
+
if (typeof page["id"] !== "string" || !page["id"]) {
|
|
45
|
+
throw makeError("Page id must be a non-empty string");
|
|
46
|
+
}
|
|
47
|
+
if (pageIds.has(page["id"])) {
|
|
48
|
+
throw makeError(`Duplicate page id "${page["id"]}"`);
|
|
49
|
+
}
|
|
50
|
+
pageIds.add(page["id"]);
|
|
51
|
+
if (!Array.isArray(page["questions"])) {
|
|
52
|
+
throw makeError(`Page "${page["id"]}" must have a questions array`);
|
|
53
|
+
}
|
|
54
|
+
const branching = page["branching"];
|
|
55
|
+
if (!Array.isArray(branching) || branching.length === 0) {
|
|
56
|
+
throw makeError(`Page "${page["id"]}" must have a non-empty branching array`);
|
|
57
|
+
}
|
|
58
|
+
const lastRule = branching[branching.length - 1];
|
|
59
|
+
if (lastRule["type"] !== "always") {
|
|
60
|
+
throw makeError(`Page "${page["id"]}" branching must end with a rule of type "always"`);
|
|
61
|
+
}
|
|
62
|
+
for (const q of page["questions"]) {
|
|
63
|
+
validateQuestion(q, page["id"], questionIds, rules);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function validateQuestion(raw, pageId, questionIds, rules) {
|
|
67
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
68
|
+
throw makeError("Each question must be a non-null object");
|
|
69
|
+
}
|
|
70
|
+
const q = raw;
|
|
71
|
+
if (typeof q["id"] !== "string" || !q["id"]) {
|
|
72
|
+
throw makeError(`In page "${pageId}": question id must be a non-empty string`);
|
|
73
|
+
}
|
|
74
|
+
if (questionIds.has(q["id"])) {
|
|
75
|
+
throw makeError(`Duplicate question id "${q["id"]}"`);
|
|
76
|
+
}
|
|
77
|
+
questionIds.add(q["id"]);
|
|
78
|
+
if (!VALID_INPUTS.has(q["x-chat-input"])) {
|
|
79
|
+
throw makeError(
|
|
80
|
+
`Question "${q["id"]}" has invalid x-chat-input "${q["x-chat-input"]}". Must be one of: text, textarea, dropdown, radio, checkbox`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const condition = q["x-chat-condition"];
|
|
84
|
+
if (condition && typeof condition === "object" && !Array.isArray(condition)) {
|
|
85
|
+
const c = condition;
|
|
86
|
+
if ("ref" in c && typeof c["ref"] === "string") {
|
|
87
|
+
if (!(c["ref"] in rules)) {
|
|
88
|
+
throw makeError(
|
|
89
|
+
`Question "${q["id"]}" x-chat-condition refs unknown rule "${c["ref"]}"`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function makeError(message) {
|
|
96
|
+
return { code: "SCHEMA_INVALID", message };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/rule-engine.ts
|
|
100
|
+
import jsonLogic from "json-logic-js";
|
|
101
|
+
var RuleEngine = class {
|
|
102
|
+
/**
|
|
103
|
+
* Evaluates a JSON Logic rule against an answer map.
|
|
104
|
+
* @param rule The JSON Logic rule to evaluate
|
|
105
|
+
* @param answers The answer map to evaluate the rule against
|
|
106
|
+
* @returns The boolean result of the rule evaluation
|
|
107
|
+
* @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails
|
|
108
|
+
*/
|
|
109
|
+
evaluate(rule, answers) {
|
|
110
|
+
try {
|
|
111
|
+
return Boolean(jsonLogic.apply(rule, answers));
|
|
112
|
+
} catch (cause) {
|
|
113
|
+
throw { code: "RULE_EVAL_FAILED", message: "Rule evaluation failed", cause };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/session-manager.ts
|
|
119
|
+
var KEY_SESSION = "car:session-id";
|
|
120
|
+
var keyProgress = (id) => `car:progress:${id}`;
|
|
121
|
+
var keyInput = (sessionId, questionId) => `car:input:${sessionId}:${questionId}`;
|
|
122
|
+
var SessionManager = class {
|
|
123
|
+
constructor(storage = globalThis.localStorage) {
|
|
124
|
+
this.storage = storage;
|
|
125
|
+
this.available = this.checkStorage();
|
|
126
|
+
this.sessionId = this.loadOrCreateSessionId();
|
|
127
|
+
}
|
|
128
|
+
getSessionId() {
|
|
129
|
+
return this.sessionId;
|
|
130
|
+
}
|
|
131
|
+
get isStorageAvailable() {
|
|
132
|
+
return this.available;
|
|
133
|
+
}
|
|
134
|
+
saveProgress(currentPageId, answers) {
|
|
135
|
+
if (!this.available) return;
|
|
136
|
+
this.storage.setItem(
|
|
137
|
+
keyProgress(this.sessionId),
|
|
138
|
+
JSON.stringify({ currentPageId, answers })
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
loadProgress() {
|
|
142
|
+
if (!this.available) return null;
|
|
143
|
+
const raw = this.storage.getItem(keyProgress(this.sessionId));
|
|
144
|
+
if (!raw) return null;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(raw);
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
saveInput(questionId, value) {
|
|
152
|
+
if (!this.available) return;
|
|
153
|
+
this.storage.setItem(
|
|
154
|
+
keyInput(this.sessionId, questionId),
|
|
155
|
+
JSON.stringify(value)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
loadInput(questionId) {
|
|
159
|
+
if (!this.available) return null;
|
|
160
|
+
const raw = this.storage.getItem(keyInput(this.sessionId, questionId));
|
|
161
|
+
if (!raw) return null;
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(raw);
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
clearAll() {
|
|
169
|
+
if (!this.available) return;
|
|
170
|
+
const progressKey = keyProgress(this.sessionId);
|
|
171
|
+
const inputPrefix = `car:input:${this.sessionId}:`;
|
|
172
|
+
const keysToRemove = [progressKey];
|
|
173
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
174
|
+
const k = this.storage.key(i);
|
|
175
|
+
if (k?.startsWith(inputPrefix)) keysToRemove.push(k);
|
|
176
|
+
}
|
|
177
|
+
for (const k of keysToRemove) {
|
|
178
|
+
this.storage.removeItem(k);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
loadOrCreateSessionId() {
|
|
182
|
+
if (!this.available) return crypto.randomUUID();
|
|
183
|
+
const existing = this.storage.getItem(KEY_SESSION);
|
|
184
|
+
if (existing) return existing;
|
|
185
|
+
const id = crypto.randomUUID();
|
|
186
|
+
this.storage.setItem(KEY_SESSION, id);
|
|
187
|
+
return id;
|
|
188
|
+
}
|
|
189
|
+
checkStorage() {
|
|
190
|
+
try {
|
|
191
|
+
const probe = "__car_probe__";
|
|
192
|
+
this.storage.setItem(probe, "1");
|
|
193
|
+
this.storage.removeItem(probe);
|
|
194
|
+
return true;
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/adapters/identity-input-adapter.ts
|
|
202
|
+
var IdentityInputAdapter = class {
|
|
203
|
+
transform(raw) {
|
|
204
|
+
return raw;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/adapters/browser-event-adapter.ts
|
|
209
|
+
var BrowserEventAdapter = class {
|
|
210
|
+
emit(event) {
|
|
211
|
+
if (typeof document === "undefined") return;
|
|
212
|
+
const name = event.type === "page:submit" ? "chat-page-submit" : "chat-form-complete";
|
|
213
|
+
document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// src/adapters/console-error-adapter.ts
|
|
218
|
+
var ConsoleErrorAdapter = class {
|
|
219
|
+
log(error) {
|
|
220
|
+
console.error("[chat-and-react]", error);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/chat-controller.ts
|
|
225
|
+
var DEFAULT_OPTIONS = {
|
|
226
|
+
userResumable: true,
|
|
227
|
+
localStorage: true,
|
|
228
|
+
autoComplete: true
|
|
229
|
+
};
|
|
230
|
+
var ChatController = class {
|
|
231
|
+
/**
|
|
232
|
+
* @param config - Controller configuration including schema and optional adapters.
|
|
233
|
+
* @param sessionManager - Optional session manager for test injection.
|
|
234
|
+
*/
|
|
235
|
+
constructor(config, sessionManager) {
|
|
236
|
+
this.schema = null;
|
|
237
|
+
this.options = { ...DEFAULT_OPTIONS };
|
|
238
|
+
this.ruleEngine = new RuleEngine();
|
|
239
|
+
this.currentPage = null;
|
|
240
|
+
this.allAnswers = {};
|
|
241
|
+
this.visitIndex = 0;
|
|
242
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
243
|
+
this.config = {
|
|
244
|
+
inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),
|
|
245
|
+
outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),
|
|
246
|
+
errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),
|
|
247
|
+
schema: config.schema
|
|
248
|
+
};
|
|
249
|
+
this.session = sessionManager ?? new SessionManager();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Initialises the controller: parses the schema, resolves options,
|
|
253
|
+
* restores any saved session, and positions to the first page.
|
|
254
|
+
*/
|
|
255
|
+
start() {
|
|
256
|
+
let raw;
|
|
257
|
+
try {
|
|
258
|
+
raw = this.config.inputAdapter.transform(this.config.schema);
|
|
259
|
+
} catch (cause) {
|
|
260
|
+
this.handleFatal({ code: "ADAPTER_ERROR", message: "InputAdapter.transform() threw", cause });
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
this.schema = parseSchema(raw);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
this.handleFatal(e);
|
|
266
|
+
}
|
|
267
|
+
if (!this.session.isStorageAvailable) {
|
|
268
|
+
this.config.errorAdapter.log({
|
|
269
|
+
code: "STORAGE_UNAVAILABLE",
|
|
270
|
+
message: "localStorage unavailable; running in memory-only mode"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const schemaOpts = this.schema["x-chat-options"] ?? {};
|
|
274
|
+
this.options = {
|
|
275
|
+
userResumable: schemaOpts.userResumable ?? DEFAULT_OPTIONS.userResumable,
|
|
276
|
+
localStorage: schemaOpts.localStorage ?? DEFAULT_OPTIONS.localStorage,
|
|
277
|
+
autoComplete: schemaOpts.autoComplete ?? DEFAULT_OPTIONS.autoComplete
|
|
278
|
+
};
|
|
279
|
+
if (this.options.userResumable) {
|
|
280
|
+
const saved = this.session.loadProgress();
|
|
281
|
+
if (saved) {
|
|
282
|
+
const page = this.schema["x-chat-pages"].find((p) => p.id === saved.currentPageId);
|
|
283
|
+
if (page) {
|
|
284
|
+
this.allAnswers = saved.answers;
|
|
285
|
+
this.currentPage = page;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
this.currentPage = this.schema["x-chat-pages"][0];
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Returns the page currently being presented to the user, or null if complete.
|
|
294
|
+
*/
|
|
295
|
+
getCurrentPage() {
|
|
296
|
+
return this.currentPage;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Returns the subset of questions on the current page that pass their
|
|
300
|
+
* `x-chat-condition` rule (if any). Questions without a condition are always visible.
|
|
301
|
+
*/
|
|
302
|
+
getVisibleQuestions() {
|
|
303
|
+
if (!this.currentPage) return [];
|
|
304
|
+
return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Returns the resolved options for the current session (schema options merged with defaults).
|
|
308
|
+
*/
|
|
309
|
+
getOptions() {
|
|
310
|
+
return { ...this.options };
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Submits the answers for the current page and advances the session.
|
|
314
|
+
* - Accumulates answers into `allAnswers`.
|
|
315
|
+
* - Emits `page:submit` (suppressed when `userResumable` is false).
|
|
316
|
+
* - Resolves branching to find the next page.
|
|
317
|
+
* - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.
|
|
318
|
+
*
|
|
319
|
+
* @param pageAnswers - Answers collected for the current page.
|
|
320
|
+
*/
|
|
321
|
+
submitPage(pageAnswers) {
|
|
322
|
+
if (!this.currentPage || !this.schema) return;
|
|
323
|
+
this.allAnswers = { ...this.allAnswers, ...pageAnswers };
|
|
324
|
+
const pageId = this.currentPage.id;
|
|
325
|
+
const currentVisitIndex = this.visitIndex;
|
|
326
|
+
const nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);
|
|
327
|
+
if (this.options.userResumable) {
|
|
328
|
+
const progressPageId = nextPageId === "**FORM_COMPLETED**" ? pageId : nextPageId;
|
|
329
|
+
this.session.saveProgress(progressPageId, this.allAnswers);
|
|
330
|
+
const submitEvent = {
|
|
331
|
+
type: "page:submit",
|
|
332
|
+
sessionId: this.session.getSessionId(),
|
|
333
|
+
pageId,
|
|
334
|
+
visitIndex: currentVisitIndex,
|
|
335
|
+
answers: pageAnswers
|
|
336
|
+
};
|
|
337
|
+
this.safeEmit(submitEvent);
|
|
338
|
+
this.emit("page:submit", submitEvent);
|
|
339
|
+
}
|
|
340
|
+
this.visitIndex++;
|
|
341
|
+
if (nextPageId === "**FORM_COMPLETED**") {
|
|
342
|
+
this.session.clearAll();
|
|
343
|
+
const completeEvent = {
|
|
344
|
+
type: "form:complete",
|
|
345
|
+
sessionId: this.session.getSessionId(),
|
|
346
|
+
allAnswers: this.allAnswers
|
|
347
|
+
};
|
|
348
|
+
this.safeEmit(completeEvent);
|
|
349
|
+
this.emit("form:complete", completeEvent);
|
|
350
|
+
this.currentPage = null;
|
|
351
|
+
} else {
|
|
352
|
+
this.currentPage = this.schema["x-chat-pages"].find((p) => p.id === nextPageId) ?? null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Registers an event listener for the given event type.
|
|
357
|
+
*
|
|
358
|
+
* @param event - The event to listen for.
|
|
359
|
+
* @param listener - The callback to invoke when the event fires.
|
|
360
|
+
*/
|
|
361
|
+
on(event, listener) {
|
|
362
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
363
|
+
this.listeners.get(event).add(listener);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Removes a previously registered event listener.
|
|
367
|
+
*
|
|
368
|
+
* @param event - The event type to deregister from.
|
|
369
|
+
* @param listener - The specific listener to remove.
|
|
370
|
+
*/
|
|
371
|
+
off(event, listener) {
|
|
372
|
+
this.listeners.get(event)?.delete(listener);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Tears down the controller: removes all listeners and nulls internal state.
|
|
376
|
+
*/
|
|
377
|
+
destroy() {
|
|
378
|
+
this.listeners.clear();
|
|
379
|
+
this.currentPage = null;
|
|
380
|
+
this.schema = null;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Evaluates whether a question should be shown based on its `x-chat-condition`.
|
|
384
|
+
* Returns true when no condition is present. On evaluation failure, logs and returns false.
|
|
385
|
+
*/
|
|
386
|
+
isQuestionVisible(question) {
|
|
387
|
+
const condition = question["x-chat-condition"];
|
|
388
|
+
if (!condition) return true;
|
|
389
|
+
const rules = this.schema?.["x-chat-rules"] ?? {};
|
|
390
|
+
let rule;
|
|
391
|
+
if ("ref" in condition && typeof condition.ref === "string") {
|
|
392
|
+
const ref = condition.ref;
|
|
393
|
+
rule = rules[ref];
|
|
394
|
+
if (!rule) return false;
|
|
395
|
+
} else {
|
|
396
|
+
rule = condition;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
return this.ruleEngine.evaluate(rule, this.allAnswers);
|
|
400
|
+
} catch (e) {
|
|
401
|
+
this.config.errorAdapter.log(e);
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Walks the branching rules top-to-bottom. Returns the first match's destination_id.
|
|
407
|
+
* Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).
|
|
408
|
+
*/
|
|
409
|
+
resolveBranching(page, answers) {
|
|
410
|
+
for (const rule of page.branching) {
|
|
411
|
+
if (rule.type === "always") return rule.destination_id;
|
|
412
|
+
if (rule.type === "if" && answers[rule.form_element] === rule.value) {
|
|
413
|
+
return rule.destination_id;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return "**FORM_COMPLETED**";
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).
|
|
420
|
+
*/
|
|
421
|
+
safeEmit(event) {
|
|
422
|
+
try {
|
|
423
|
+
this.config.outputAdapter.emit(event);
|
|
424
|
+
} catch (cause) {
|
|
425
|
+
this.config.errorAdapter.log({
|
|
426
|
+
code: "ADAPTER_ERROR",
|
|
427
|
+
message: `OutputAdapter.emit() threw for ${event.type}`,
|
|
428
|
+
cause
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Notifies all registered listeners for the given event.
|
|
434
|
+
*/
|
|
435
|
+
emit(event, payload) {
|
|
436
|
+
this.listeners.get(event)?.forEach((l) => l(payload));
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Logs the error via the error adapter and then throws it.
|
|
440
|
+
* Used for unrecoverable errors (schema invalid, adapter failure on start).
|
|
441
|
+
*/
|
|
442
|
+
handleFatal(error) {
|
|
443
|
+
this.config.errorAdapter.log(error);
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
export {
|
|
448
|
+
BrowserEventAdapter,
|
|
449
|
+
ChatController,
|
|
450
|
+
ConsoleErrorAdapter,
|
|
451
|
+
IdentityInputAdapter,
|
|
452
|
+
RuleEngine,
|
|
453
|
+
SessionManager,
|
|
454
|
+
parseSchema
|
|
455
|
+
};
|
|
456
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema-parser.ts","../src/rule-engine.ts","../src/session-manager.ts","../src/adapters/identity-input-adapter.ts","../src/adapters/browser-event-adapter.ts","../src/adapters/console-error-adapter.ts","../src/chat-controller.ts"],"sourcesContent":["import type { ChatSchema, ChatPage } from './types.js';\r\n\r\nconst VALID_INPUTS = new Set(['text', 'textarea', 'dropdown', 'radio', 'checkbox']);\r\nconst FORM_COMPLETED = '**FORM_COMPLETED**';\r\n\r\n/**\r\n * Parses and validates an unknown input as a {@link ChatSchema}.\r\n *\r\n * @param raw - The raw input to validate.\r\n * @returns The typed {@link ChatSchema} if valid.\r\n * @throws `{ code: 'SCHEMA_INVALID', message: string }` on any validation failure.\r\n */\r\nexport function parseSchema(raw: unknown): ChatSchema {\r\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\r\n throw makeError('Schema must be a non-null object');\r\n }\r\n\r\n const obj = raw as Record<string, unknown>;\r\n\r\n if (obj['x-chat-version'] !== '1') {\r\n throw makeError('x-chat-version must be \"1\"');\r\n }\r\n\r\n if (!Array.isArray(obj['x-chat-pages'])) {\r\n throw makeError('x-chat-pages must be an array');\r\n }\r\n\r\n const pages = obj['x-chat-pages'] as unknown[];\r\n const rules = (\r\n typeof obj['x-chat-rules'] === 'object' && obj['x-chat-rules'] !== null\r\n ? obj['x-chat-rules']\r\n : {}\r\n ) as Record<string, unknown>;\r\n\r\n const pageIds = new Set<string>();\r\n const questionIds = new Set<string>();\r\n\r\n for (const page of pages) {\r\n validatePage(page, pageIds, questionIds, rules);\r\n }\r\n\r\n // Second pass: validate branching destinations now that all page ids are known\r\n for (const page of pages) {\r\n const p = page as ChatPage;\r\n for (const rule of p.branching) {\r\n if (rule.destination_id !== FORM_COMPLETED && !pageIds.has(rule.destination_id)) {\r\n throw makeError(\r\n `branching destination_id \"${rule.destination_id}\" does not reference a known page id`,\r\n );\r\n }\r\n if (rule.type === 'if' && !questionIds.has(rule.form_element)) {\r\n throw makeError(\r\n `branching form_element \"${rule.form_element}\" does not reference a known question id`,\r\n );\r\n }\r\n }\r\n }\r\n\r\n return raw as ChatSchema;\r\n}\r\n\r\n/**\r\n * Validates a single page object, collecting its id and question ids.\r\n *\r\n * @param raw - The raw page value.\r\n * @param pageIds - Set of already-seen page ids (mutated in place).\r\n * @param questionIds - Set of already-seen question ids (mutated in place).\r\n * @param rules - The x-chat-rules map for condition ref resolution.\r\n */\r\nfunction validatePage(\r\n raw: unknown,\r\n pageIds: Set<string>,\r\n questionIds: Set<string>,\r\n rules: Record<string, unknown>,\r\n): void {\r\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\r\n throw makeError('Each page must be a non-null object');\r\n }\r\n const page = raw as Record<string, unknown>;\r\n\r\n if (typeof page['id'] !== 'string' || !page['id']) {\r\n throw makeError('Page id must be a non-empty string');\r\n }\r\n if (pageIds.has(page['id'] as string)) {\r\n throw makeError(`Duplicate page id \"${page['id']}\"`);\r\n }\r\n pageIds.add(page['id'] as string);\r\n\r\n if (!Array.isArray(page['questions'])) {\r\n throw makeError(`Page \"${page['id']}\" must have a questions array`);\r\n }\r\n\r\n const branching = page['branching'];\r\n if (!Array.isArray(branching) || (branching as unknown[]).length === 0) {\r\n throw makeError(`Page \"${page['id']}\" must have a non-empty branching array`);\r\n }\r\n\r\n const lastRule = (branching as unknown[])[branching.length - 1] as Record<string, unknown>;\r\n if (lastRule['type'] !== 'always') {\r\n throw makeError(`Page \"${page['id']}\" branching must end with a rule of type \"always\"`);\r\n }\r\n\r\n for (const q of page['questions'] as unknown[]) {\r\n validateQuestion(q, page['id'] as string, questionIds, rules);\r\n }\r\n}\r\n\r\n/**\r\n * Validates a single question object, collecting its id.\r\n *\r\n * @param raw - The raw question value.\r\n * @param pageId - The id of the containing page (for error messages).\r\n * @param questionIds - Set of already-seen question ids (mutated in place).\r\n * @param rules - The x-chat-rules map for condition ref resolution.\r\n */\r\nfunction validateQuestion(\r\n raw: unknown,\r\n pageId: string,\r\n questionIds: Set<string>,\r\n rules: Record<string, unknown>,\r\n): void {\r\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\r\n throw makeError('Each question must be a non-null object');\r\n }\r\n const q = raw as Record<string, unknown>;\r\n\r\n if (typeof q['id'] !== 'string' || !q['id']) {\r\n throw makeError(`In page \"${pageId}\": question id must be a non-empty string`);\r\n }\r\n if (questionIds.has(q['id'] as string)) {\r\n throw makeError(`Duplicate question id \"${q['id']}\"`);\r\n }\r\n questionIds.add(q['id'] as string);\r\n\r\n if (!VALID_INPUTS.has(q['x-chat-input'] as string)) {\r\n throw makeError(\r\n `Question \"${q['id']}\" has invalid x-chat-input \"${q['x-chat-input']}\". ` +\r\n `Must be one of: text, textarea, dropdown, radio, checkbox`,\r\n );\r\n }\r\n\r\n const condition = q['x-chat-condition'];\r\n if (condition && typeof condition === 'object' && !Array.isArray(condition)) {\r\n const c = condition as Record<string, unknown>;\r\n if ('ref' in c && typeof c['ref'] === 'string') {\r\n if (!(c['ref'] in rules)) {\r\n throw makeError(\r\n `Question \"${q['id']}\" x-chat-condition refs unknown rule \"${c['ref']}\"`,\r\n );\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Creates a schema validation error object.\r\n *\r\n * @param message - Human-readable description of the validation failure.\r\n * @returns An error object with `code: 'SCHEMA_INVALID'`.\r\n */\r\nfunction makeError(message: string): { code: 'SCHEMA_INVALID'; message: string } {\r\n return { code: 'SCHEMA_INVALID', message };\r\n}\r\n","import jsonLogic from 'json-logic-js';\r\nimport type { AnswerMap, JsonLogicRule } from './types.js';\r\n\r\n/**\r\n * Evaluates JSON Logic rules against provided answers.\r\n * Wraps json-logic-js to provide type-safe rule evaluation.\r\n */\r\nexport class RuleEngine {\r\n /**\r\n * Evaluates a JSON Logic rule against an answer map.\r\n * @param rule The JSON Logic rule to evaluate\r\n * @param answers The answer map to evaluate the rule against\r\n * @returns The boolean result of the rule evaluation\r\n * @throws {{ code: 'RULE_EVAL_FAILED', message: string, cause: unknown }} if rule evaluation fails\r\n */\r\n evaluate(rule: JsonLogicRule, answers: AnswerMap): boolean {\r\n try {\r\n // json-logic-js apply() returns any; cast to boolean\r\n return Boolean((jsonLogic as { apply: (rule: unknown, data: unknown) => unknown }).apply(rule, answers));\r\n } catch (cause) {\r\n throw { code: 'RULE_EVAL_FAILED', message: 'Rule evaluation failed', cause };\r\n }\r\n }\r\n}\r\n","// packages/core/src/session-manager.ts\r\nimport type { AnswerMap } from './types.js';\r\n\r\nexport interface StorageBackend {\r\n getItem(key: string): string | null;\r\n setItem(key: string, value: string): void;\r\n removeItem(key: string): void;\r\n key(index: number): string | null;\r\n readonly length: number;\r\n}\r\n\r\nexport interface SessionProgress {\r\n currentPageId: string;\r\n answers: AnswerMap;\r\n}\r\n\r\nconst KEY_SESSION = 'car:session-id';\r\nconst keyProgress = (id: string) => `car:progress:${id}`;\r\nconst keyInput = (sessionId: string, questionId: string) =>\r\n `car:input:${sessionId}:${questionId}`;\r\n\r\nexport class SessionManager {\r\n private readonly sessionId: string;\r\n private readonly available: boolean;\r\n\r\n constructor(private readonly storage: StorageBackend = globalThis.localStorage) {\r\n this.available = this.checkStorage();\r\n this.sessionId = this.loadOrCreateSessionId();\r\n }\r\n\r\n getSessionId(): string {\r\n return this.sessionId;\r\n }\r\n\r\n get isStorageAvailable(): boolean {\r\n return this.available;\r\n }\r\n\r\n saveProgress(currentPageId: string, answers: AnswerMap): void {\r\n if (!this.available) return;\r\n this.storage.setItem(\r\n keyProgress(this.sessionId),\r\n JSON.stringify({ currentPageId, answers }),\r\n );\r\n }\r\n\r\n loadProgress(): SessionProgress | null {\r\n if (!this.available) return null;\r\n const raw = this.storage.getItem(keyProgress(this.sessionId));\r\n if (!raw) return null;\r\n try {\r\n return JSON.parse(raw) as SessionProgress;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n saveInput(questionId: string, value: string | string[]): void {\r\n if (!this.available) return;\r\n this.storage.setItem(\r\n keyInput(this.sessionId, questionId),\r\n JSON.stringify(value),\r\n );\r\n }\r\n\r\n loadInput(questionId: string): string | string[] | null {\r\n if (!this.available) return null;\r\n const raw = this.storage.getItem(keyInput(this.sessionId, questionId));\r\n if (!raw) return null;\r\n try {\r\n return JSON.parse(raw) as string | string[];\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n clearAll(): void {\r\n if (!this.available) return;\r\n const progressKey = keyProgress(this.sessionId);\r\n const inputPrefix = `car:input:${this.sessionId}:`;\r\n const keysToRemove: string[] = [progressKey];\r\n\r\n for (let i = 0; i < this.storage.length; i++) {\r\n const k = this.storage.key(i);\r\n if (k?.startsWith(inputPrefix)) keysToRemove.push(k);\r\n }\r\n\r\n for (const k of keysToRemove) {\r\n this.storage.removeItem(k);\r\n }\r\n }\r\n\r\n private loadOrCreateSessionId(): string {\r\n if (!this.available) return crypto.randomUUID();\r\n const existing = this.storage.getItem(KEY_SESSION);\r\n if (existing) return existing;\r\n const id = crypto.randomUUID();\r\n this.storage.setItem(KEY_SESSION, id);\r\n return id;\r\n }\r\n\r\n private checkStorage(): boolean {\r\n try {\r\n const probe = '__car_probe__';\r\n this.storage.setItem(probe, '1');\r\n this.storage.removeItem(probe);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n}\r\n","import type { InputAdapter } from '../types.js';\r\n\r\nexport class IdentityInputAdapter implements InputAdapter {\r\n transform(raw: unknown): unknown {\r\n return raw;\r\n }\r\n}\r\n","import type { OutputAdapter, ChatEvent } from '../types.js';\r\n\r\nexport class BrowserEventAdapter implements OutputAdapter {\r\n emit(event: ChatEvent): void {\r\n if (typeof document === 'undefined') return;\r\n const name = event.type === 'page:submit' ? 'chat-page-submit' : 'chat-form-complete';\r\n document.dispatchEvent(new CustomEvent(name, { detail: event, bubbles: true }));\r\n }\r\n}\r\n","import type { ErrorLogAdapter, ChatError } from '../types.js';\r\n\r\nexport class ConsoleErrorAdapter implements ErrorLogAdapter {\r\n log(error: ChatError): void {\r\n console.error('[chat-and-react]', error);\r\n }\r\n}\r\n","// packages/core/src/chat-controller.ts\r\nimport type {\r\n ChatControllerConfig,\r\n ChatOptions,\r\n ChatSchema,\r\n ChatPage,\r\n ChatQuestion,\r\n AnswerMap,\r\n ChatEvent,\r\n ChatError,\r\n} from './types.js';\r\nimport { parseSchema } from './schema-parser.js';\r\nimport { RuleEngine } from './rule-engine.js';\r\nimport { SessionManager } from './session-manager.js';\r\nimport { IdentityInputAdapter } from './adapters/identity-input-adapter.js';\r\nimport { BrowserEventAdapter } from './adapters/browser-event-adapter.js';\r\nimport { ConsoleErrorAdapter } from './adapters/console-error-adapter.js';\r\n\r\ntype EventName = 'page:submit' | 'form:complete';\r\ntype Listener = (event: ChatEvent) => void;\r\n\r\nconst DEFAULT_OPTIONS: ChatOptions = {\r\n userResumable: true,\r\n localStorage: true,\r\n autoComplete: true,\r\n};\r\n\r\n/**\r\n * Central orchestrator for the chat-and-react form flow.\r\n * Drives page navigation, branching, session persistence, and event emission.\r\n */\r\nexport class ChatController {\r\n private schema: ChatSchema | null = null;\r\n private options: ChatOptions = { ...DEFAULT_OPTIONS };\r\n private readonly config: Required<ChatControllerConfig>;\r\n private readonly session: SessionManager;\r\n private readonly ruleEngine = new RuleEngine();\r\n private currentPage: ChatPage | null = null;\r\n private allAnswers: AnswerMap = {};\r\n private visitIndex = 0;\r\n private readonly listeners = new Map<EventName, Set<Listener>>();\r\n\r\n /**\r\n * @param config - Controller configuration including schema and optional adapters.\r\n * @param sessionManager - Optional session manager for test injection.\r\n */\r\n constructor(config: ChatControllerConfig, sessionManager?: SessionManager) {\r\n this.config = {\r\n inputAdapter: config.inputAdapter ?? new IdentityInputAdapter(),\r\n outputAdapter: config.outputAdapter ?? new BrowserEventAdapter(),\r\n errorAdapter: config.errorAdapter ?? new ConsoleErrorAdapter(),\r\n schema: config.schema,\r\n };\r\n this.session = sessionManager ?? new SessionManager();\r\n }\r\n\r\n /**\r\n * Initialises the controller: parses the schema, resolves options,\r\n * restores any saved session, and positions to the first page.\r\n */\r\n start(): void {\r\n let raw: unknown;\r\n try {\r\n raw = this.config.inputAdapter.transform(this.config.schema);\r\n } catch (cause) {\r\n this.handleFatal({ code: 'ADAPTER_ERROR', message: 'InputAdapter.transform() threw', cause });\r\n }\r\n\r\n try {\r\n this.schema = parseSchema(raw!);\r\n } catch (e) {\r\n this.handleFatal(e as ChatError);\r\n }\r\n\r\n if (!this.session.isStorageAvailable) {\r\n this.config.errorAdapter.log({\r\n code: 'STORAGE_UNAVAILABLE',\r\n message: 'localStorage unavailable; running in memory-only mode',\r\n });\r\n }\r\n\r\n const schemaOpts = this.schema!['x-chat-options'] ?? {};\r\n this.options = {\r\n userResumable: schemaOpts.userResumable ?? DEFAULT_OPTIONS.userResumable,\r\n localStorage: schemaOpts.localStorage ?? DEFAULT_OPTIONS.localStorage,\r\n autoComplete: schemaOpts.autoComplete ?? DEFAULT_OPTIONS.autoComplete,\r\n };\r\n\r\n if (this.options.userResumable) {\r\n const saved = this.session.loadProgress();\r\n if (saved) {\r\n const page = this.schema!['x-chat-pages'].find((p) => p.id === saved.currentPageId);\r\n if (page) {\r\n this.allAnswers = saved.answers;\r\n this.currentPage = page;\r\n return;\r\n }\r\n }\r\n }\r\n\r\n this.currentPage = this.schema!['x-chat-pages'][0];\r\n }\r\n\r\n /**\r\n * Returns the page currently being presented to the user, or null if complete.\r\n */\r\n getCurrentPage(): ChatPage | null {\r\n return this.currentPage;\r\n }\r\n\r\n /**\r\n * Returns the subset of questions on the current page that pass their\r\n * `x-chat-condition` rule (if any). Questions without a condition are always visible.\r\n */\r\n getVisibleQuestions(): ChatQuestion[] {\r\n if (!this.currentPage) return [];\r\n return this.currentPage.questions.filter((q) => this.isQuestionVisible(q));\r\n }\r\n\r\n /**\r\n * Returns the resolved options for the current session (schema options merged with defaults).\r\n */\r\n getOptions(): ChatOptions {\r\n return { ...this.options };\r\n }\r\n\r\n /**\r\n * Submits the answers for the current page and advances the session.\r\n * - Accumulates answers into `allAnswers`.\r\n * - Emits `page:submit` (suppressed when `userResumable` is false).\r\n * - Resolves branching to find the next page.\r\n * - Emits `form:complete` and clears session when `**FORM_COMPLETED**` is reached.\r\n *\r\n * @param pageAnswers - Answers collected for the current page.\r\n */\r\n submitPage(pageAnswers: AnswerMap): void {\r\n if (!this.currentPage || !this.schema) return;\r\n\r\n this.allAnswers = { ...this.allAnswers, ...pageAnswers };\r\n const pageId = this.currentPage.id;\r\n const currentVisitIndex = this.visitIndex;\r\n const nextPageId = this.resolveBranching(this.currentPage, this.allAnswers);\r\n\r\n if (this.options.userResumable) {\r\n const progressPageId =\r\n nextPageId === '**FORM_COMPLETED**' ? pageId : nextPageId;\r\n this.session.saveProgress(progressPageId, this.allAnswers);\r\n\r\n const submitEvent: ChatEvent = {\r\n type: 'page:submit',\r\n sessionId: this.session.getSessionId(),\r\n pageId,\r\n visitIndex: currentVisitIndex,\r\n answers: pageAnswers,\r\n };\r\n this.safeEmit(submitEvent);\r\n this.emit('page:submit', submitEvent);\r\n }\r\n\r\n this.visitIndex++;\r\n\r\n if (nextPageId === '**FORM_COMPLETED**') {\r\n this.session.clearAll();\r\n const completeEvent: ChatEvent = {\r\n type: 'form:complete',\r\n sessionId: this.session.getSessionId(),\r\n allAnswers: this.allAnswers,\r\n };\r\n this.safeEmit(completeEvent);\r\n this.emit('form:complete', completeEvent);\r\n this.currentPage = null;\r\n } else {\r\n this.currentPage =\r\n this.schema!['x-chat-pages'].find((p) => p.id === nextPageId) ?? null;\r\n }\r\n }\r\n\r\n /**\r\n * Registers an event listener for the given event type.\r\n *\r\n * @param event - The event to listen for.\r\n * @param listener - The callback to invoke when the event fires.\r\n */\r\n on(event: EventName, listener: Listener): void {\r\n if (!this.listeners.has(event)) this.listeners.set(event, new Set());\r\n this.listeners.get(event)!.add(listener);\r\n }\r\n\r\n /**\r\n * Removes a previously registered event listener.\r\n *\r\n * @param event - The event type to deregister from.\r\n * @param listener - The specific listener to remove.\r\n */\r\n off(event: EventName, listener: Listener): void {\r\n this.listeners.get(event)?.delete(listener);\r\n }\r\n\r\n /**\r\n * Tears down the controller: removes all listeners and nulls internal state.\r\n */\r\n destroy(): void {\r\n this.listeners.clear();\r\n this.currentPage = null;\r\n this.schema = null;\r\n }\r\n\r\n /**\r\n * Evaluates whether a question should be shown based on its `x-chat-condition`.\r\n * Returns true when no condition is present. On evaluation failure, logs and returns false.\r\n */\r\n private isQuestionVisible(question: ChatQuestion): boolean {\r\n const condition = question['x-chat-condition'];\r\n if (!condition) return true;\r\n\r\n const rules = this.schema?.['x-chat-rules'] ?? {};\r\n let rule: Record<string, unknown>;\r\n\r\n if ('ref' in condition && typeof (condition as { ref?: unknown }).ref === 'string') {\r\n const ref = (condition as { ref: string }).ref;\r\n rule = rules[ref];\r\n if (!rule) return false;\r\n } else {\r\n rule = condition as Record<string, unknown>;\r\n }\r\n\r\n try {\r\n return this.ruleEngine.evaluate(rule, this.allAnswers);\r\n } catch (e) {\r\n this.config.errorAdapter.log(e as ChatError);\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Walks the branching rules top-to-bottom. Returns the first match's destination_id.\r\n * Falls back to `**FORM_COMPLETED**` if nothing matches (should not happen with valid schemas).\r\n */\r\n private resolveBranching(page: ChatPage, answers: AnswerMap): string {\r\n for (const rule of page.branching) {\r\n if (rule.type === 'always') return rule.destination_id;\r\n if (rule.type === 'if' && answers[rule.form_element] === rule.value) {\r\n return rule.destination_id;\r\n }\r\n }\r\n return '**FORM_COMPLETED**';\r\n }\r\n\r\n /**\r\n * Calls `outputAdapter.emit()` and swallows adapter errors (logging them instead).\r\n */\r\n private safeEmit(event: ChatEvent): void {\r\n try {\r\n this.config.outputAdapter.emit(event);\r\n } catch (cause) {\r\n this.config.errorAdapter.log({\r\n code: 'ADAPTER_ERROR',\r\n message: `OutputAdapter.emit() threw for ${event.type}`,\r\n cause,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Notifies all registered listeners for the given event.\r\n */\r\n private emit(event: EventName, payload: ChatEvent): void {\r\n this.listeners.get(event)?.forEach((l) => l(payload));\r\n }\r\n\r\n /**\r\n * Logs the error via the error adapter and then throws it.\r\n * Used for unrecoverable errors (schema invalid, adapter failure on start).\r\n */\r\n private handleFatal(error: ChatError): never {\r\n this.config.errorAdapter.log(error);\r\n throw error;\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@richardmcquiston01/chat-and-react-core",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Headless controller, JSON Schema validation, and session management for chat-and-react — a framework-agnostic conversational form library.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Richard McQuiston <richard.mcquiston01@gmail.com>",
|
|
7
|
+
"homepage": "https://github.com/RichardMcQuiston01/chat-and-react#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/RichardMcQuiston01/chat-and-react.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/RichardMcQuiston01/chat-and-react/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"chat",
|
|
18
|
+
"form",
|
|
19
|
+
"conversational-form",
|
|
20
|
+
"json-schema",
|
|
21
|
+
"typescript",
|
|
22
|
+
"headless"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/index.cjs",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"require": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"prepublishOnly": "bun run build",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"json-logic-js": "^2.0.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/json-logic-js": "^2.0.8",
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"vitest": "^1.6.0"
|
|
54
|
+
}
|
|
55
|
+
}
|