@beignet/cli 0.0.31 → 0.0.33
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/CHANGELOG.md +107 -0
- package/README.md +70 -17
- package/dist/check.d.ts +2 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +16 -0
- package/dist/check.js.map +1 -1
- package/dist/choices.d.ts +22 -2
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +62 -0
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +6 -2
- package/dist/create-prompts.d.ts.map +1 -1
- package/dist/create-prompts.js +1 -1
- package/dist/create-prompts.js.map +1 -1
- package/dist/db.d.ts +5 -5
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +5 -5
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -27
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +5 -0
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +805 -62
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +5 -3
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +58 -29
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +12 -3
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +52 -11
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.d.ts.map +1 -1
- package/dist/make/tenancy.js +6 -4
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +136 -65
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +114 -1
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +190 -0
- package/dist/outbox.js.map +1 -1
- package/dist/preflight.d.ts +90 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +423 -0
- package/dist/preflight.js.map +1 -0
- package/dist/provider-add.d.ts +10 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +255 -16
- package/dist/provider-add.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +2 -3
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +6 -3
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +5 -11
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +2 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +10 -5
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +5 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +13 -1
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.d.ts +5 -0
- package/dist/templates/testing.d.ts.map +1 -0
- package/dist/templates/testing.js +542 -0
- package/dist/templates/testing.js.map +1 -0
- package/dist/templates/todos.js +2 -2
- package/package.json +3 -2
- package/skills/app-structure/SKILL.md +12 -4
- package/src/check.ts +23 -0
- package/src/choices.ts +84 -0
- package/src/config.ts +4 -0
- package/src/create-prompts.ts +8 -2
- package/src/db.ts +12 -12
- package/src/index.ts +592 -37
- package/src/inspect.ts +1253 -156
- package/src/make/inbox.ts +5 -3
- package/src/make/payments.ts +58 -29
- package/src/make/shared.ts +87 -13
- package/src/make/tenancy.ts +6 -4
- package/src/make.ts +179 -65
- package/src/outbox.ts +363 -0
- package/src/preflight.ts +596 -0
- package/src/provider-add.ts +270 -16
- package/src/task.ts +2 -3
- package/src/templates/agents.ts +6 -3
- package/src/templates/base.ts +5 -13
- package/src/templates/index.ts +2 -0
- package/src/templates/server.ts +10 -5
- package/src/templates/shared.ts +15 -1
- package/src/templates/testing.ts +545 -0
- package/src/templates/todos.ts +2 -2
- package/src/test-helpers/generated-app.ts +10 -6
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import type { TemplateFile } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
export const testHelperTemplateFile: TemplateFile = {
|
|
4
|
+
path: "lib/beignet-test.ts",
|
|
5
|
+
content: `import assert from "node:assert/strict";
|
|
6
|
+
import {
|
|
7
|
+
after,
|
|
8
|
+
afterEach,
|
|
9
|
+
before,
|
|
10
|
+
beforeEach,
|
|
11
|
+
describe as nodeDescribe,
|
|
12
|
+
it as nodeIt,
|
|
13
|
+
test as nodeTest,
|
|
14
|
+
} from "node:test";
|
|
15
|
+
import { inspect, isDeepStrictEqual } from "node:util";
|
|
16
|
+
|
|
17
|
+
export { after, afterEach, before, beforeEach };
|
|
18
|
+
|
|
19
|
+
type TestBody = () => void | Promise<void>;
|
|
20
|
+
type TestEach = {
|
|
21
|
+
<T>(cases: readonly T[]): (
|
|
22
|
+
name: string,
|
|
23
|
+
body: (value: T) => void | Promise<void>,
|
|
24
|
+
) => void;
|
|
25
|
+
<T extends readonly unknown[]>(cases: readonly T[]): (
|
|
26
|
+
name: string,
|
|
27
|
+
body: (...values: T) => void | Promise<void>,
|
|
28
|
+
) => void;
|
|
29
|
+
};
|
|
30
|
+
type TestRunner = ((name: string, body: TestBody) => unknown) & {
|
|
31
|
+
each: TestEach;
|
|
32
|
+
skip: (name: string, body: TestBody) => unknown;
|
|
33
|
+
skipIf: (condition: boolean) => TestRunner;
|
|
34
|
+
};
|
|
35
|
+
type NodeRunner = {
|
|
36
|
+
(name: string, body: TestBody): unknown;
|
|
37
|
+
(
|
|
38
|
+
name: string,
|
|
39
|
+
options: { skip?: boolean },
|
|
40
|
+
body: TestBody,
|
|
41
|
+
): unknown;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type AsymmetricMatcher =
|
|
45
|
+
| { kind: "any"; expected: unknown }
|
|
46
|
+
| { kind: "arrayContaining"; expected: readonly unknown[] }
|
|
47
|
+
| { kind: "objectContaining"; expected: Record<string, unknown> };
|
|
48
|
+
|
|
49
|
+
type BaseMatchers = {
|
|
50
|
+
toBe(expected: unknown): void;
|
|
51
|
+
toEqual(expected: unknown): void;
|
|
52
|
+
toMatchObject(expected: unknown): void;
|
|
53
|
+
toContain(expected: unknown): void;
|
|
54
|
+
toContainEqual(expected: unknown): void;
|
|
55
|
+
toHaveLength(expected: number): void;
|
|
56
|
+
toBeDefined(): void;
|
|
57
|
+
toBeUndefined(): void;
|
|
58
|
+
toBeNull(): void;
|
|
59
|
+
toBeGreaterThan(expected: number): void;
|
|
60
|
+
toMatch(expected: string | RegExp): void;
|
|
61
|
+
toThrow(expected?: string | RegExp | (new (...args: unknown[]) => object)): void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type AsyncMatchers = {
|
|
65
|
+
toBe(expected: unknown): Promise<void>;
|
|
66
|
+
toEqual(expected: unknown): Promise<void>;
|
|
67
|
+
toMatchObject(expected: unknown): Promise<void>;
|
|
68
|
+
toBeNull(): Promise<void>;
|
|
69
|
+
toBeUndefined(): Promise<void>;
|
|
70
|
+
toThrow(expected?: string | RegExp | (new (...args: unknown[]) => object)): Promise<void>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type Matchers = BaseMatchers & {
|
|
74
|
+
not: BaseMatchers;
|
|
75
|
+
resolves: AsyncMatchers;
|
|
76
|
+
rejects: AsyncMatchers;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type Expect = ((actual: unknown) => Matchers) & {
|
|
80
|
+
any(expected: unknown): AsymmetricMatcher;
|
|
81
|
+
arrayContaining(expected: readonly unknown[]): AsymmetricMatcher;
|
|
82
|
+
objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const describe = createRunner(nodeDescribe as NodeRunner);
|
|
86
|
+
export const it = createRunner(nodeIt as NodeRunner);
|
|
87
|
+
export const test = createRunner(nodeTest as NodeRunner);
|
|
88
|
+
|
|
89
|
+
export const expect = Object.assign(
|
|
90
|
+
(actual: unknown) => createMatchers(actual),
|
|
91
|
+
{
|
|
92
|
+
any: (expected: unknown): AsymmetricMatcher => ({
|
|
93
|
+
kind: "any",
|
|
94
|
+
expected,
|
|
95
|
+
}),
|
|
96
|
+
arrayContaining: (expected: readonly unknown[]): AsymmetricMatcher => ({
|
|
97
|
+
kind: "arrayContaining",
|
|
98
|
+
expected,
|
|
99
|
+
}),
|
|
100
|
+
objectContaining: (
|
|
101
|
+
expected: Record<string, unknown>,
|
|
102
|
+
): AsymmetricMatcher => ({ kind: "objectContaining", expected }),
|
|
103
|
+
},
|
|
104
|
+
) satisfies Expect;
|
|
105
|
+
|
|
106
|
+
function createRunner(
|
|
107
|
+
base: NodeRunner,
|
|
108
|
+
options?: { skip?: boolean },
|
|
109
|
+
): TestRunner {
|
|
110
|
+
const runner = ((name: string, body: TestBody) =>
|
|
111
|
+
runTest(base, name, body, options)) as TestRunner;
|
|
112
|
+
runner.each = ((cases: readonly unknown[]) => {
|
|
113
|
+
return (
|
|
114
|
+
name: string,
|
|
115
|
+
body: (...values: unknown[]) => void | Promise<void>,
|
|
116
|
+
) => {
|
|
117
|
+
for (const testCase of cases) {
|
|
118
|
+
const values = (
|
|
119
|
+
Array.isArray(testCase) ? testCase : [testCase]
|
|
120
|
+
) as unknown[];
|
|
121
|
+
runTest(base, formatEachName(name, values), () => body(...values), options);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}) as TestEach;
|
|
125
|
+
runner.skip = (name: string, body: TestBody) =>
|
|
126
|
+
runTest(base, name, body, { skip: true });
|
|
127
|
+
runner.skipIf = (condition: boolean) =>
|
|
128
|
+
condition ? createRunner(base, { skip: true }) : runner;
|
|
129
|
+
return runner;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function runTest(
|
|
133
|
+
base: NodeRunner,
|
|
134
|
+
name: string,
|
|
135
|
+
body: TestBody,
|
|
136
|
+
options?: { skip?: boolean },
|
|
137
|
+
): unknown {
|
|
138
|
+
return options ? base(name, options, body) : base(name, body);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatEachName(name: string, values: readonly unknown[]): string {
|
|
142
|
+
let index = 0;
|
|
143
|
+
return name.replace(/%[sdifoO]/g, () => formatInline(values[index++]));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createMatchers(actual: unknown): Matchers {
|
|
147
|
+
return {
|
|
148
|
+
...createBaseMatchers(actual, false),
|
|
149
|
+
not: createBaseMatchers(actual, true),
|
|
150
|
+
resolves: createAsyncMatchers(actual, "resolves"),
|
|
151
|
+
rejects: createAsyncMatchers(actual, "rejects"),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createBaseMatchers(actual: unknown, negated: boolean): BaseMatchers {
|
|
156
|
+
return {
|
|
157
|
+
toBe(expected) {
|
|
158
|
+
check(
|
|
159
|
+
Object.is(actual, expected),
|
|
160
|
+
negated,
|
|
161
|
+
"Expected values to be identical.",
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
toEqual(expected) {
|
|
165
|
+
check(matchesExpected(actual, expected), negated, "Expected values to be equal.");
|
|
166
|
+
},
|
|
167
|
+
toMatchObject(expected) {
|
|
168
|
+
check(
|
|
169
|
+
matchesObject(actual, expected),
|
|
170
|
+
negated,
|
|
171
|
+
"Expected value to match object.",
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
toContain(expected) {
|
|
175
|
+
const matches =
|
|
176
|
+
typeof actual === "string"
|
|
177
|
+
? actual.includes(String(expected))
|
|
178
|
+
: Array.isArray(actual) && actual.includes(expected);
|
|
179
|
+
check(matches, negated, "Expected value to contain item.");
|
|
180
|
+
},
|
|
181
|
+
toContainEqual(expected) {
|
|
182
|
+
check(
|
|
183
|
+
Array.isArray(actual) &&
|
|
184
|
+
actual.some((item) => matchesExpected(item, expected)),
|
|
185
|
+
negated,
|
|
186
|
+
"Expected array to contain equal item.",
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
toHaveLength(expected) {
|
|
190
|
+
const length =
|
|
191
|
+
actual !== null && typeof actual === "object"
|
|
192
|
+
? (actual as { length?: unknown }).length
|
|
193
|
+
: undefined;
|
|
194
|
+
check(length === expected, negated, "Expected value to have length.");
|
|
195
|
+
},
|
|
196
|
+
toBeDefined() {
|
|
197
|
+
check(actual !== undefined, negated, "Expected value to be defined.");
|
|
198
|
+
},
|
|
199
|
+
toBeUndefined() {
|
|
200
|
+
check(actual === undefined, negated, "Expected value to be undefined.");
|
|
201
|
+
},
|
|
202
|
+
toBeNull() {
|
|
203
|
+
check(actual === null, negated, "Expected value to be null.");
|
|
204
|
+
},
|
|
205
|
+
toBeGreaterThan(expected) {
|
|
206
|
+
check(
|
|
207
|
+
typeof actual === "number" && actual > expected,
|
|
208
|
+
negated,
|
|
209
|
+
"Expected value to be greater than " + expected + ".",
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
toMatch(expected) {
|
|
213
|
+
const text = String(actual);
|
|
214
|
+
const matches =
|
|
215
|
+
typeof expected === "string" ? text.includes(expected) : expected.test(text);
|
|
216
|
+
check(matches, negated, "Expected value to match pattern.");
|
|
217
|
+
},
|
|
218
|
+
toThrow(expected) {
|
|
219
|
+
assert.equal(typeof actual, "function", "toThrow expects a function.");
|
|
220
|
+
let didThrow = false;
|
|
221
|
+
let thrown: unknown;
|
|
222
|
+
try {
|
|
223
|
+
(actual as () => unknown)();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
didThrow = true;
|
|
226
|
+
thrown = error;
|
|
227
|
+
}
|
|
228
|
+
check(
|
|
229
|
+
didThrow && thrownMatches(thrown, expected),
|
|
230
|
+
negated,
|
|
231
|
+
"Expected function to throw.",
|
|
232
|
+
);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function createAsyncMatchers(
|
|
238
|
+
actual: unknown,
|
|
239
|
+
kind: "resolves" | "rejects",
|
|
240
|
+
): AsyncMatchers {
|
|
241
|
+
const resolveActual = async () =>
|
|
242
|
+
kind === "resolves" ? await Promise.resolve(actual) : await rejected(actual);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
async toBe(expected) {
|
|
246
|
+
createBaseMatchers(await resolveActual(), false).toBe(expected);
|
|
247
|
+
},
|
|
248
|
+
async toEqual(expected) {
|
|
249
|
+
createBaseMatchers(await resolveActual(), false).toEqual(expected);
|
|
250
|
+
},
|
|
251
|
+
async toMatchObject(expected) {
|
|
252
|
+
createBaseMatchers(await resolveActual(), false).toMatchObject(expected);
|
|
253
|
+
},
|
|
254
|
+
async toBeNull() {
|
|
255
|
+
createBaseMatchers(await resolveActual(), false).toBeNull();
|
|
256
|
+
},
|
|
257
|
+
async toBeUndefined() {
|
|
258
|
+
createBaseMatchers(await resolveActual(), false).toBeUndefined();
|
|
259
|
+
},
|
|
260
|
+
async toThrow(expected) {
|
|
261
|
+
const thrown =
|
|
262
|
+
kind === "rejects" ? await rejected(actual) : await resolveActual();
|
|
263
|
+
check(
|
|
264
|
+
thrownMatches(thrown, expected),
|
|
265
|
+
false,
|
|
266
|
+
"Expected promise to reject with matching error.",
|
|
267
|
+
);
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function rejected(actual: unknown): Promise<unknown> {
|
|
273
|
+
try {
|
|
274
|
+
await Promise.resolve(actual);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return error;
|
|
277
|
+
}
|
|
278
|
+
assert.fail("Expected promise to reject.");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function check(condition: boolean, negated: boolean, message: string): void {
|
|
282
|
+
assert.ok(
|
|
283
|
+
negated ? !condition : condition,
|
|
284
|
+
negated ? "Negated assertion failed: " + message : message,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function matchesExpected(actual: unknown, expected: unknown): boolean {
|
|
289
|
+
if (!containsAsymmetric(expected)) {
|
|
290
|
+
return isDeepStrictEqual(actual, expected);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (isAsymmetricMatcher(expected)) {
|
|
294
|
+
switch (expected.kind) {
|
|
295
|
+
case "any":
|
|
296
|
+
return matchesAny(actual, expected.expected);
|
|
297
|
+
case "arrayContaining":
|
|
298
|
+
return (
|
|
299
|
+
Array.isArray(actual) &&
|
|
300
|
+
expected.expected.every((expectedItem) =>
|
|
301
|
+
actual.some((actualItem) => matchesExpected(actualItem, expectedItem)),
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
case "objectContaining":
|
|
305
|
+
return matchesObject(actual, expected.expected);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (Array.isArray(expected)) {
|
|
310
|
+
return (
|
|
311
|
+
Array.isArray(actual) &&
|
|
312
|
+
actual.length === expected.length &&
|
|
313
|
+
expected.every((item, index) => matchesExpected(actual[index], item))
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (isObjectRecord(expected)) {
|
|
318
|
+
if (!isObjectRecord(actual)) return false;
|
|
319
|
+
const actualKeys = Object.keys(actual);
|
|
320
|
+
const expectedKeys = Object.keys(expected);
|
|
321
|
+
return (
|
|
322
|
+
actualKeys.length === expectedKeys.length &&
|
|
323
|
+
expectedKeys.every(
|
|
324
|
+
(key) =>
|
|
325
|
+
Object.hasOwn(actual, key) &&
|
|
326
|
+
matchesExpected(actual[key], expected[key]),
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return Object.is(actual, expected);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function matchesObject(actual: unknown, expected: unknown): boolean {
|
|
335
|
+
if (isAsymmetricMatcher(expected)) return matchesExpected(actual, expected);
|
|
336
|
+
if (Array.isArray(expected)) {
|
|
337
|
+
return (
|
|
338
|
+
Array.isArray(actual) &&
|
|
339
|
+
actual.length >= expected.length &&
|
|
340
|
+
expected.every((item, index) => matchesObject(actual[index], item))
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (!isObjectRecord(expected)) return matchesExpected(actual, expected);
|
|
344
|
+
if (!isObjectRecord(actual)) return false;
|
|
345
|
+
return Object.entries(expected).every(([key, expectedValue]) =>
|
|
346
|
+
matchesObject(actual[key], expectedValue),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function containsAsymmetric(value: unknown): boolean {
|
|
351
|
+
if (isAsymmetricMatcher(value)) return true;
|
|
352
|
+
if (Array.isArray(value)) return value.some(containsAsymmetric);
|
|
353
|
+
if (!isObjectRecord(value)) return false;
|
|
354
|
+
return Object.values(value).some(containsAsymmetric);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isAsymmetricMatcher(value: unknown): value is AsymmetricMatcher {
|
|
358
|
+
return (
|
|
359
|
+
isObjectRecord(value) &&
|
|
360
|
+
(value.kind === "any" ||
|
|
361
|
+
value.kind === "arrayContaining" ||
|
|
362
|
+
value.kind === "objectContaining")
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function matchesAny(actual: unknown, expected: unknown): boolean {
|
|
367
|
+
if (expected === String) return typeof actual === "string";
|
|
368
|
+
if (expected === Number) return typeof actual === "number";
|
|
369
|
+
if (expected === Boolean) return typeof actual === "boolean";
|
|
370
|
+
if (expected === Array) return Array.isArray(actual);
|
|
371
|
+
if (expected === Object) return isObjectRecord(actual);
|
|
372
|
+
if (expected instanceof Function) {
|
|
373
|
+
return actual instanceof (expected as new (...args: unknown[]) => object);
|
|
374
|
+
}
|
|
375
|
+
return actual !== undefined && actual !== null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function thrownMatches(
|
|
379
|
+
error: unknown,
|
|
380
|
+
expected: string | RegExp | (new (...args: unknown[]) => object) | undefined,
|
|
381
|
+
): boolean {
|
|
382
|
+
if (expected === undefined) return true;
|
|
383
|
+
if (typeof expected === "string") return errorMessage(error).includes(expected);
|
|
384
|
+
if (expected instanceof RegExp) return expected.test(errorMessage(error));
|
|
385
|
+
return error instanceof expected;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function errorMessage(error: unknown): string {
|
|
389
|
+
return error instanceof Error ? error.message : String(error);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
393
|
+
return value !== null && typeof value === "object";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function formatInline(value: unknown): string {
|
|
397
|
+
return typeof value === "string"
|
|
398
|
+
? value
|
|
399
|
+
: inspect(value, { breakLength: Number.POSITIVE_INFINITY, depth: 2 });
|
|
400
|
+
}
|
|
401
|
+
`,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export const testRunnerTemplateFile: TemplateFile = {
|
|
405
|
+
path: "lib/beignet-test-runner.ts",
|
|
406
|
+
content: `import { spawnSync } from "node:child_process";
|
|
407
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
408
|
+
import path from "node:path";
|
|
409
|
+
|
|
410
|
+
const defaultTestRoots = [
|
|
411
|
+
"app",
|
|
412
|
+
"features",
|
|
413
|
+
"infra",
|
|
414
|
+
"lib",
|
|
415
|
+
"ports",
|
|
416
|
+
"server",
|
|
417
|
+
"tests",
|
|
418
|
+
"src/tests",
|
|
419
|
+
];
|
|
420
|
+
const ignoredDirectories = new Set([
|
|
421
|
+
".beignet-local",
|
|
422
|
+
".git",
|
|
423
|
+
".next",
|
|
424
|
+
".turbo",
|
|
425
|
+
"coverage",
|
|
426
|
+
"dist",
|
|
427
|
+
"node_modules",
|
|
428
|
+
]);
|
|
429
|
+
const testFilePattern = /\\.(test|spec)\\.[cm]?[tj]sx?$/;
|
|
430
|
+
|
|
431
|
+
const explicitArgs = process.argv.slice(2);
|
|
432
|
+
const testArgs =
|
|
433
|
+
explicitArgs.length > 0
|
|
434
|
+
? resolveTestArgs(process.cwd(), explicitArgs)
|
|
435
|
+
: discoverTestFiles(process.cwd());
|
|
436
|
+
|
|
437
|
+
if (testArgs.length === 0) {
|
|
438
|
+
console.log("No test files found.");
|
|
439
|
+
process.exit(0);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const result = spawnSync(
|
|
443
|
+
process.execPath,
|
|
444
|
+
["--import", "tsx", "--test", ...testArgs],
|
|
445
|
+
{
|
|
446
|
+
cwd: process.cwd(),
|
|
447
|
+
env: process.env,
|
|
448
|
+
stdio: "inherit",
|
|
449
|
+
},
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (result.error) {
|
|
453
|
+
console.error(result.error.message);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
process.exit(result.status ?? 1);
|
|
458
|
+
|
|
459
|
+
function discoverTestFiles(cwd: string): string[] {
|
|
460
|
+
const files: string[] = [];
|
|
461
|
+
for (const root of testRoots(cwd)) {
|
|
462
|
+
const rootPath = path.join(cwd, root);
|
|
463
|
+
if (existsSync(rootPath)) {
|
|
464
|
+
walk(rootPath, cwd, files);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return files.sort();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function resolveTestArgs(cwd: string, args: string[]): string[] {
|
|
471
|
+
const resolved: string[] = [];
|
|
472
|
+
for (const arg of args) {
|
|
473
|
+
if (arg.startsWith("-")) {
|
|
474
|
+
resolved.push(arg);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const argPath = path.resolve(cwd, arg);
|
|
479
|
+
if (!existsSync(argPath)) {
|
|
480
|
+
resolved.push(arg);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const stats = statSync(argPath);
|
|
485
|
+
if (stats.isDirectory()) {
|
|
486
|
+
const files: string[] = [];
|
|
487
|
+
walk(argPath, cwd, files);
|
|
488
|
+
resolved.push(...files.sort());
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
resolved.push(path.relative(cwd, argPath).split(path.sep).join("/"));
|
|
493
|
+
}
|
|
494
|
+
return Array.from(new Set(resolved));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function testRoots(cwd: string): string[] {
|
|
498
|
+
return Array.from(new Set([...defaultTestRoots, ...configuredRoots(cwd)]));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function configuredRoots(cwd: string): string[] {
|
|
502
|
+
const configPath = path.join(cwd, "beignet.config.json");
|
|
503
|
+
if (!existsSync(configPath)) return [];
|
|
504
|
+
try {
|
|
505
|
+
const config = JSON.parse(readFileSync(configPath, "utf8")) as {
|
|
506
|
+
paths?: Record<string, string>;
|
|
507
|
+
};
|
|
508
|
+
const paths = config.paths ?? {};
|
|
509
|
+
return [
|
|
510
|
+
paths.tests,
|
|
511
|
+
paths.features,
|
|
512
|
+
paths.routes ? path.dirname(paths.routes) : undefined,
|
|
513
|
+
paths.server ? path.dirname(paths.server) : undefined,
|
|
514
|
+
paths.infrastructurePorts
|
|
515
|
+
? path.dirname(paths.infrastructurePorts)
|
|
516
|
+
: undefined,
|
|
517
|
+
paths.ports ? path.dirname(paths.ports) : undefined,
|
|
518
|
+
paths.useCaseBuilder ? path.dirname(paths.useCaseBuilder) : undefined,
|
|
519
|
+
].filter((root): root is string => Boolean(root) && root !== ".");
|
|
520
|
+
} catch {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function walk(directory: string, cwd: string, files: string[]): void {
|
|
526
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
527
|
+
const entryPath = path.join(directory, entry.name);
|
|
528
|
+
if (entry.isDirectory()) {
|
|
529
|
+
if (!ignoredDirectories.has(entry.name)) {
|
|
530
|
+
walk(entryPath, cwd, files);
|
|
531
|
+
}
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (entry.isFile() && testFilePattern.test(entry.name)) {
|
|
535
|
+
files.push(path.relative(cwd, entryPath).split(path.sep).join("/"));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
`,
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
export const testSupportTemplateFiles: TemplateFile[] = [
|
|
543
|
+
testHelperTemplateFile,
|
|
544
|
+
testRunnerTemplateFile,
|
|
545
|
+
];
|
package/src/templates/todos.ts
CHANGED
|
@@ -359,7 +359,7 @@ export function createTestTodoRepository(): TodoRepository {
|
|
|
359
359
|
};
|
|
360
360
|
}
|
|
361
361
|
`,
|
|
362
|
-
useCaseTest: `import { describe, expect, it } from "
|
|
362
|
+
useCaseTest: `import { describe, expect, it } from "@/lib/beignet-test";
|
|
363
363
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
364
364
|
import { createTestUserActor } from "@beignet/core/ports/testing";
|
|
365
365
|
import {
|
|
@@ -476,7 +476,7 @@ describe("todos use cases", () => {
|
|
|
476
476
|
});
|
|
477
477
|
});
|
|
478
478
|
`,
|
|
479
|
-
routesTest: `import { describe, expect, it } from "
|
|
479
|
+
routesTest: `import { describe, expect, it } from "@/lib/beignet-test";
|
|
480
480
|
import { createStaticAuth } from "@beignet/core/ports";
|
|
481
481
|
import { createIdempotencyHooks, defineRoutes } from "@beignet/core/server";
|
|
482
482
|
import { createTestPorts } from "@beignet/core/testing";
|
|
@@ -12,7 +12,8 @@ const repoRoot = path.resolve(import.meta.dir, "../../../..");
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Symlink the workspace and vendored dependencies a generated app needs for a
|
|
15
|
-
* real `tsc --noEmit` and
|
|
15
|
+
* real `tsc --noEmit` and generated test script run, without a package-manager
|
|
16
|
+
* install.
|
|
16
17
|
* Pass `shell: true` for full-stack apps so the shadcn shell dependencies
|
|
17
18
|
* (icons, radix, theming) resolve too.
|
|
18
19
|
*/
|
|
@@ -56,7 +57,6 @@ export async function linkSmokeDependencies(
|
|
|
56
57
|
"@libsql/client",
|
|
57
58
|
"@tanstack/react-query",
|
|
58
59
|
"better-auth",
|
|
59
|
-
"@types/bun",
|
|
60
60
|
"@types/node",
|
|
61
61
|
"@types/pg",
|
|
62
62
|
"@types/react",
|
|
@@ -71,6 +71,7 @@ export async function linkSmokeDependencies(
|
|
|
71
71
|
"react-hook-form",
|
|
72
72
|
"resend",
|
|
73
73
|
"typescript",
|
|
74
|
+
"tsx",
|
|
74
75
|
"zod",
|
|
75
76
|
];
|
|
76
77
|
if (options.shell) {
|
|
@@ -114,10 +115,8 @@ export async function linkSmokeDependencies(
|
|
|
114
115
|
repoRoot,
|
|
115
116
|
"packages/provider-mail-resend/node_modules/resend",
|
|
116
117
|
),
|
|
117
|
-
"next-themes": path.join(
|
|
118
|
-
|
|
119
|
-
"apps/example-ehr/node_modules/next-themes",
|
|
120
|
-
),
|
|
118
|
+
"next-themes": path.join(repoRoot, "packages/cli/node_modules/next-themes"),
|
|
119
|
+
tsx: path.join(repoRoot, "node_modules/tsx"),
|
|
121
120
|
};
|
|
122
121
|
|
|
123
122
|
for (const packageName of externalPackages) {
|
|
@@ -128,6 +127,11 @@ export async function linkSmokeDependencies(
|
|
|
128
127
|
);
|
|
129
128
|
}
|
|
130
129
|
|
|
130
|
+
await linkDependency(
|
|
131
|
+
path.join(nodeModules, "tsx/dist/cli.mjs"),
|
|
132
|
+
path.join(nodeModules, ".bin/tsx"),
|
|
133
|
+
);
|
|
134
|
+
|
|
131
135
|
await linkDependency(
|
|
132
136
|
path.join(
|
|
133
137
|
repoRoot,
|