@lunora/testing 0.0.0 → 1.0.0-alpha.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/LICENSE.md +105 -0
- package/README.md +240 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +265 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.mjs +2 -0
- package/dist/packem_shared/lunoraTest-BUg4Ebv8.mjs +415 -0
- package/package.json +42 -17
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { runShardMigrations, createShardCtxDb } from '@lunora/do';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
|
|
4
|
+
const createFakeScheduler = (getDispatch, getMutationContext, getFunctionRegistry) => {
|
|
5
|
+
let nowMs = Date.now();
|
|
6
|
+
let nextId = 1;
|
|
7
|
+
const pending = /* @__PURE__ */ new Map();
|
|
8
|
+
const recordedFailures = [];
|
|
9
|
+
const enqueue = (scheduledFor, functionPath, args = {}) => {
|
|
10
|
+
const id = `fake-job-${String(nextId)}`;
|
|
11
|
+
nextId += 1;
|
|
12
|
+
pending.set(id, {
|
|
13
|
+
args,
|
|
14
|
+
enqueuedAt: nowMs,
|
|
15
|
+
functionPath,
|
|
16
|
+
id,
|
|
17
|
+
scheduledFor
|
|
18
|
+
});
|
|
19
|
+
return id;
|
|
20
|
+
};
|
|
21
|
+
const scheduler = {
|
|
22
|
+
cancel: (id) => {
|
|
23
|
+
const existed = pending.has(id);
|
|
24
|
+
pending.delete(id);
|
|
25
|
+
return Promise.resolve({ cancelled: existed });
|
|
26
|
+
},
|
|
27
|
+
// eslint-disable-next-line unicorn/no-null -- Scheduler.get returns null when absent (public API contract)
|
|
28
|
+
get: (id) => Promise.resolve(pending.get(id) ?? null),
|
|
29
|
+
list: () => Promise.resolve([...pending.values()]),
|
|
30
|
+
runAfter: (delayMs, functionPath, args) => {
|
|
31
|
+
const id = enqueue(nowMs + delayMs, functionPath, args);
|
|
32
|
+
return Promise.resolve(id);
|
|
33
|
+
},
|
|
34
|
+
runAt: (timestampMs, functionPath, args) => {
|
|
35
|
+
const id = enqueue(timestampMs, functionPath, args);
|
|
36
|
+
return Promise.resolve(id);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const dispatchJob = async (job) => {
|
|
40
|
+
pending.delete(job.id);
|
|
41
|
+
const registry = getFunctionRegistry();
|
|
42
|
+
const entry = registry.get(job.functionPath);
|
|
43
|
+
if (entry === void 0) {
|
|
44
|
+
console.warn(`[fake-scheduler] unknown functionPath "${job.functionPath}" — job ${job.id} dropped`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (entry.kind === "mutation" || entry.kind === "action") {
|
|
48
|
+
const dispatch = getDispatch();
|
|
49
|
+
const mutationContext = getMutationContext();
|
|
50
|
+
await dispatch(entry.kind, entry, mutationContext, job.args);
|
|
51
|
+
} else {
|
|
52
|
+
console.warn(
|
|
53
|
+
`[fake-scheduler] functionPath "${job.functionPath}" is a ${entry.kind} — only mutations and actions can be scheduled; job ${job.id} dropped`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const executeDue = async (cutoff) => {
|
|
58
|
+
const due = [...pending.values()].filter((j) => j.scheduledFor <= cutoff).toSorted((a, b) => a.scheduledFor - b.scheduledFor);
|
|
59
|
+
const failed = [];
|
|
60
|
+
for (const job of due) {
|
|
61
|
+
try {
|
|
62
|
+
await dispatchJob(job);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const failure = { args: job.args, error, functionPath: job.functionPath, id: job.id };
|
|
65
|
+
failed.push(failure);
|
|
66
|
+
recordedFailures.push(failure);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { executed: due.length, failed };
|
|
70
|
+
};
|
|
71
|
+
const runSweep = async (cutoff, options) => {
|
|
72
|
+
const { executed, failed } = await executeDue(cutoff);
|
|
73
|
+
if (failed.length > 0 && (options?.throwOnError ?? true)) {
|
|
74
|
+
const [first] = failed;
|
|
75
|
+
if (failed.length === 1 && first !== void 0) {
|
|
76
|
+
throw first.error;
|
|
77
|
+
}
|
|
78
|
+
throw new AggregateError(
|
|
79
|
+
failed.map((f) => f.error),
|
|
80
|
+
`${String(failed.length)} scheduled jobs failed: ${failed.map((f) => f.functionPath).join(", ")}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return executed;
|
|
84
|
+
};
|
|
85
|
+
const controls = {
|
|
86
|
+
advance: (ms, options) => {
|
|
87
|
+
nowMs += ms;
|
|
88
|
+
return runSweep(nowMs, options);
|
|
89
|
+
},
|
|
90
|
+
failures: () => [...recordedFailures],
|
|
91
|
+
list: () => [...pending.values()],
|
|
92
|
+
runPending: (options) => runSweep(Number.POSITIVE_INFINITY, options)
|
|
93
|
+
};
|
|
94
|
+
return { controls, scheduler };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const createSqlExec = () => {
|
|
98
|
+
const database = new DatabaseSync(":memory:");
|
|
99
|
+
const cursor = (rows) => {
|
|
100
|
+
return {
|
|
101
|
+
one() {
|
|
102
|
+
if (rows.length !== 1) {
|
|
103
|
+
throw new Error(`expected exactly one row, received ${String(rows.length)}`);
|
|
104
|
+
}
|
|
105
|
+
const [only] = rows;
|
|
106
|
+
return only;
|
|
107
|
+
},
|
|
108
|
+
[Symbol.iterator]() {
|
|
109
|
+
return rows[Symbol.iterator]();
|
|
110
|
+
},
|
|
111
|
+
toArray() {
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const run = (query, ...params) => {
|
|
117
|
+
const statement = database.prepare(query);
|
|
118
|
+
const rows = statement.all(...params);
|
|
119
|
+
return cursor(rows);
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
close: () => {
|
|
123
|
+
database.close();
|
|
124
|
+
},
|
|
125
|
+
sql: { exec: run }
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const registeredFunctionKind = (value) => {
|
|
130
|
+
if (typeof value !== "object" || value === null) {
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
const { kind } = value;
|
|
134
|
+
if (kind === "query" || kind === "mutation" || kind === "action") {
|
|
135
|
+
return kind;
|
|
136
|
+
}
|
|
137
|
+
return void 0;
|
|
138
|
+
};
|
|
139
|
+
const registeredFunctionVisibility = (value) => typeof value === "object" && value !== null && value.visibility === "internal" ? "internal" : "public";
|
|
140
|
+
const unavailable = (surface) => {
|
|
141
|
+
throw new Error(`ctx.${surface} is not available in the in-memory @lunora/testing harness (v1)`);
|
|
142
|
+
};
|
|
143
|
+
const stubProxy = (surface) => /* @__PURE__ */ new Proxy((..._args) => unavailable(surface), {
|
|
144
|
+
apply: () => unavailable(surface),
|
|
145
|
+
get: () => unavailable(surface)
|
|
146
|
+
});
|
|
147
|
+
const noopLog = {
|
|
148
|
+
debug: () => void 0,
|
|
149
|
+
error: () => void 0,
|
|
150
|
+
info: () => void 0,
|
|
151
|
+
log: () => void 0,
|
|
152
|
+
warn: () => void 0
|
|
153
|
+
};
|
|
154
|
+
const buildSubscribe = (runRegistered, queryContext, mutationListeners) => {
|
|
155
|
+
const factory = (referenceOrInline, args) => {
|
|
156
|
+
let done = false;
|
|
157
|
+
let pendingResolve;
|
|
158
|
+
let pendingResult;
|
|
159
|
+
let latestSeq = 0;
|
|
160
|
+
let appliedSeq = 0;
|
|
161
|
+
const runQuery = () => {
|
|
162
|
+
if (registeredFunctionKind(referenceOrInline)) {
|
|
163
|
+
return runRegistered("query", referenceOrInline, queryContext, args, false);
|
|
164
|
+
}
|
|
165
|
+
return Promise.resolve(referenceOrInline(queryContext));
|
|
166
|
+
};
|
|
167
|
+
const emit = (seq, value) => {
|
|
168
|
+
if (seq < appliedSeq) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
appliedSeq = seq;
|
|
172
|
+
const iterResult = { done: false, value };
|
|
173
|
+
if (pendingResolve === void 0) {
|
|
174
|
+
pendingResult = iterResult;
|
|
175
|
+
} else {
|
|
176
|
+
const resolve = pendingResolve;
|
|
177
|
+
pendingResolve = void 0;
|
|
178
|
+
pendingResult = void 0;
|
|
179
|
+
resolve(iterResult);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const emitAt = (seq) => (value) => {
|
|
183
|
+
emit(seq, value);
|
|
184
|
+
};
|
|
185
|
+
const listener = () => {
|
|
186
|
+
if (done) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
latestSeq += 1;
|
|
190
|
+
const seq = latestSeq;
|
|
191
|
+
runQuery().then(emitAt(seq)).catch(() => void 0);
|
|
192
|
+
};
|
|
193
|
+
mutationListeners.add(listener);
|
|
194
|
+
const iterator = {
|
|
195
|
+
[Symbol.asyncIterator]() {
|
|
196
|
+
return iterator;
|
|
197
|
+
},
|
|
198
|
+
next: () => {
|
|
199
|
+
if (done) {
|
|
200
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
201
|
+
}
|
|
202
|
+
if (pendingResult !== void 0 && appliedSeq === latestSeq) {
|
|
203
|
+
const result = pendingResult;
|
|
204
|
+
pendingResult = void 0;
|
|
205
|
+
return Promise.resolve(result);
|
|
206
|
+
}
|
|
207
|
+
if (appliedSeq < latestSeq) {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
pendingResolve = resolve;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return runQuery().then((value) => {
|
|
213
|
+
if (pendingResult !== void 0) {
|
|
214
|
+
const result = pendingResult;
|
|
215
|
+
pendingResult = void 0;
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
return { done: false, value };
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
return: () => {
|
|
222
|
+
done = true;
|
|
223
|
+
mutationListeners.delete(listener);
|
|
224
|
+
if (pendingResolve !== void 0) {
|
|
225
|
+
const resolve = pendingResolve;
|
|
226
|
+
pendingResolve = void 0;
|
|
227
|
+
resolve({ done: true, value: void 0 });
|
|
228
|
+
}
|
|
229
|
+
return Promise.resolve({ done: true, value: void 0 });
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
runQuery().then(emitAt(0)).catch(() => void 0);
|
|
233
|
+
return iterator;
|
|
234
|
+
};
|
|
235
|
+
return factory;
|
|
236
|
+
};
|
|
237
|
+
const lunoraTest = (schema, options) => {
|
|
238
|
+
const { close, sql } = createSqlExec();
|
|
239
|
+
const ddlSchema = schema;
|
|
240
|
+
runShardMigrations(sql, ddlSchema);
|
|
241
|
+
const database = createShardCtxDb({ schema: ddlSchema, sql });
|
|
242
|
+
const execStatement = (statement) => {
|
|
243
|
+
const runner = sql.exec;
|
|
244
|
+
runner.call(sql, statement);
|
|
245
|
+
};
|
|
246
|
+
let transactionDepth = 0;
|
|
247
|
+
const runInMutationTransaction = async (function_) => {
|
|
248
|
+
if (transactionDepth > 0) {
|
|
249
|
+
return function_();
|
|
250
|
+
}
|
|
251
|
+
transactionDepth = 1;
|
|
252
|
+
execStatement("BEGIN");
|
|
253
|
+
try {
|
|
254
|
+
const result = await function_();
|
|
255
|
+
execStatement("COMMIT");
|
|
256
|
+
return result;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
try {
|
|
259
|
+
execStatement("ROLLBACK");
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
} finally {
|
|
264
|
+
transactionDepth = 0;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
let closed = false;
|
|
268
|
+
const closeDatabase = () => {
|
|
269
|
+
if (closed) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
closed = true;
|
|
273
|
+
close();
|
|
274
|
+
};
|
|
275
|
+
const functionRegistryMap = new Map(
|
|
276
|
+
Object.entries(options?.functions ?? {}).map(([path, function_]) => [path, function_])
|
|
277
|
+
);
|
|
278
|
+
const mutationListeners = /* @__PURE__ */ new Set();
|
|
279
|
+
const notifyMutationListeners = () => {
|
|
280
|
+
for (const listener of mutationListeners) {
|
|
281
|
+
listener();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
let scheduledDispatchRef;
|
|
285
|
+
let mutationContextRef;
|
|
286
|
+
const { controls: schedulerControls, scheduler: fakeScheduler } = createFakeScheduler(
|
|
287
|
+
() => {
|
|
288
|
+
if (scheduledDispatchRef === void 0) {
|
|
289
|
+
throw new Error("[fake-scheduler] dispatch not yet available — scheduler.advance called before harness construction completed");
|
|
290
|
+
}
|
|
291
|
+
return scheduledDispatchRef;
|
|
292
|
+
},
|
|
293
|
+
() => {
|
|
294
|
+
if (mutationContextRef === void 0) {
|
|
295
|
+
throw new Error("[fake-scheduler] mutationContext not yet available — scheduler.advance called before harness construction completed");
|
|
296
|
+
}
|
|
297
|
+
return mutationContextRef;
|
|
298
|
+
},
|
|
299
|
+
() => functionRegistryMap
|
|
300
|
+
);
|
|
301
|
+
const makeHarness = (identity) => {
|
|
302
|
+
const auth = {
|
|
303
|
+
// eslint-disable-next-line unicorn/no-null -- AuthState.getIdentity's anonymous sentinel is `null` (mirrors a decoded JWT being absent)
|
|
304
|
+
getIdentity: () => Promise.resolve(identity ?? null),
|
|
305
|
+
// eslint-disable-next-line unicorn/no-null -- AuthState.userId's anonymous sentinel is `null`
|
|
306
|
+
userId: identity?.userId ?? null
|
|
307
|
+
};
|
|
308
|
+
const queryContext = {
|
|
309
|
+
auth,
|
|
310
|
+
db: database,
|
|
311
|
+
log: noopLog,
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runQuery, after construction completes
|
|
313
|
+
runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
|
|
314
|
+
storage: stubProxy("storage"),
|
|
315
|
+
vectors: stubProxy("vectors")
|
|
316
|
+
};
|
|
317
|
+
const mutationContext = {
|
|
318
|
+
auth,
|
|
319
|
+
db: database,
|
|
320
|
+
log: noopLog,
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runMutation, after construction completes
|
|
322
|
+
runMutation: (reference, args) => runInternal("mutation", reference, mutationContext, args),
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runQuery, after construction completes
|
|
324
|
+
runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
|
|
325
|
+
scheduler: fakeScheduler,
|
|
326
|
+
storage: stubProxy("storage"),
|
|
327
|
+
vectors: stubProxy("vectors"),
|
|
328
|
+
workflows: stubProxy("workflows")
|
|
329
|
+
};
|
|
330
|
+
mutationContextRef ??= mutationContext;
|
|
331
|
+
const actionContext = {
|
|
332
|
+
auth,
|
|
333
|
+
db: database,
|
|
334
|
+
// Use the injected fetch when provided; fall back to the v1 stub otherwise.
|
|
335
|
+
fetch: options?.fetch ?? stubProxy("fetch"),
|
|
336
|
+
log: noopLog,
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runAction, after construction completes
|
|
338
|
+
runAction: (reference, args) => runInternal("action", reference, actionContext, args),
|
|
339
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runMutation, after construction completes
|
|
340
|
+
runMutation: (reference, args) => runInternal("mutation", reference, mutationContext, args),
|
|
341
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runQuery, after construction completes
|
|
342
|
+
runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
|
|
343
|
+
scheduler: fakeScheduler,
|
|
344
|
+
storage: stubProxy("storage"),
|
|
345
|
+
vectors: stubProxy("vectors"),
|
|
346
|
+
workflows: stubProxy("workflows")
|
|
347
|
+
};
|
|
348
|
+
const runRegistered = (expected, reference, context, args, allowInternal) => {
|
|
349
|
+
const kind = registeredFunctionKind(reference);
|
|
350
|
+
if (kind !== expected) {
|
|
351
|
+
throw new Error(`expected a registered ${expected}, received a ${kind ?? "non-function"} reference`);
|
|
352
|
+
}
|
|
353
|
+
if (!allowInternal && registeredFunctionVisibility(reference) === "internal") {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`"${expected}" is an internal function — it is unreachable from the external RPC boundary in production. Call it through ctx.run${expected.charAt(0).toUpperCase()}${expected.slice(1)} from another function instead.`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return Promise.resolve(reference.handler(context, args ?? {}));
|
|
359
|
+
};
|
|
360
|
+
const runInternal = (expected, reference, context, args) => runRegistered(expected, reference, context, args, true);
|
|
361
|
+
scheduledDispatchRef ??= (kind, reference, context, args) => {
|
|
362
|
+
if (kind === "mutation") {
|
|
363
|
+
return runInMutationTransaction(() => runRegistered("mutation", reference, context, args, true)).then((result) => {
|
|
364
|
+
notifyMutationListeners();
|
|
365
|
+
return result;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return runInternal("action", reference, context, args);
|
|
369
|
+
};
|
|
370
|
+
const query = ((referenceOrInline, args) => {
|
|
371
|
+
if (registeredFunctionKind(referenceOrInline)) {
|
|
372
|
+
return runRegistered("query", referenceOrInline, queryContext, args, false);
|
|
373
|
+
}
|
|
374
|
+
return Promise.resolve(referenceOrInline(queryContext));
|
|
375
|
+
});
|
|
376
|
+
const mutation = ((referenceOrInline, args) => {
|
|
377
|
+
if (registeredFunctionKind(referenceOrInline)) {
|
|
378
|
+
return runInMutationTransaction(() => runRegistered("mutation", referenceOrInline, mutationContext, args, false)).then((result) => {
|
|
379
|
+
notifyMutationListeners();
|
|
380
|
+
return result;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return runInMutationTransaction(() => referenceOrInline(mutationContext)).then((result) => {
|
|
384
|
+
notifyMutationListeners();
|
|
385
|
+
return result;
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
const action = ((referenceOrInline, args) => {
|
|
389
|
+
if (registeredFunctionKind(referenceOrInline)) {
|
|
390
|
+
return runRegistered("action", referenceOrInline, actionContext, args, false);
|
|
391
|
+
}
|
|
392
|
+
return Promise.resolve(referenceOrInline(actionContext));
|
|
393
|
+
});
|
|
394
|
+
const subscribe = buildSubscribe(runRegistered, queryContext, mutationListeners);
|
|
395
|
+
const harness = {
|
|
396
|
+
action,
|
|
397
|
+
close: closeDatabase,
|
|
398
|
+
mutation,
|
|
399
|
+
query,
|
|
400
|
+
run: (function_) => runInMutationTransaction(() => function_(mutationContext)).then((result) => {
|
|
401
|
+
notifyMutationListeners();
|
|
402
|
+
return result;
|
|
403
|
+
}),
|
|
404
|
+
scheduler: schedulerControls,
|
|
405
|
+
subscribe,
|
|
406
|
+
// A scoped view shares the SAME sql/db handle (created once above), so
|
|
407
|
+
// writes performed under an identity persist for every accessor.
|
|
408
|
+
withIdentity: (next) => makeHarness(next)
|
|
409
|
+
};
|
|
410
|
+
return harness;
|
|
411
|
+
};
|
|
412
|
+
return makeHarness(null);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
export { lunoraTest };
|
package/package.json
CHANGED
|
@@ -1,31 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/testing",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
4
|
"description": "Testing toolkit for Lunora: an in-memory harness for queries, mutations, and actions",
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cloudflare",
|
|
7
|
+
"durable-objects",
|
|
8
|
+
"e2e",
|
|
9
|
+
"lunora",
|
|
10
|
+
"mail",
|
|
11
|
+
"playwright",
|
|
12
|
+
"testing",
|
|
13
|
+
"workers"
|
|
14
|
+
],
|
|
6
15
|
"homepage": "https://lunora.sh",
|
|
16
|
+
"bugs": "https://github.com/anolilab/lunora/issues",
|
|
17
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Daniel Bannert",
|
|
20
|
+
"email": "d.bannert@anolilab.de"
|
|
21
|
+
},
|
|
7
22
|
"repository": {
|
|
8
23
|
"type": "git",
|
|
9
24
|
"url": "git+https://github.com/anolilab/lunora.git",
|
|
10
25
|
"directory": "packages/testing"
|
|
11
26
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"cloudflare",
|
|
18
|
-
"workers",
|
|
19
|
-
"durable-objects",
|
|
20
|
-
"testing",
|
|
21
|
-
"e2e",
|
|
22
|
-
"mail",
|
|
23
|
-
"playwright"
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE.md",
|
|
31
|
+
"__assets__"
|
|
24
32
|
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"main": "./dist/index.mjs",
|
|
36
|
+
"module": "./dist/index.mjs",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/index.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
25
45
|
"publishConfig": {
|
|
26
46
|
"access": "public"
|
|
27
47
|
},
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@lunora/do": "1.0.0-alpha.2",
|
|
50
|
+
"@lunora/mail": "1.0.0-alpha.1",
|
|
51
|
+
"@lunora/server": "1.0.0-alpha.1"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": "^22.15.0 || >=24.11.0"
|
|
55
|
+
}
|
|
31
56
|
}
|