@akanjs/devkit 2.1.1-rc.2 → 2.1.2-rc.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/aiEditor.test.ts +68 -0
- package/aiEditor.ts +82 -28
- package/akanConfig/akanConfig.test.ts +167 -32
- package/akanConfig/akanConfig.ts +130 -31
- package/artifact/implicitRootLayout.test.ts +2 -0
- package/artifact/implicitRootLayout.ts +2 -2
- package/cloud/cloudApi.ts +25 -48
- package/executors.test.ts +47 -0
- package/executors.ts +70 -3
- package/index.ts +1 -1
- package/linter.ts +308 -97
- package/package.json +2 -2
- package/prompter.ts +17 -4
- package/typecheck/typecheck.proc.ts +21 -0
package/aiEditor.test.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
parseTypescriptFileBlocks,
|
|
4
|
+
preserveTypescriptResponseContent,
|
|
5
|
+
} from "./aiEditor";
|
|
6
|
+
|
|
7
|
+
describe("parseTypescriptFileBlocks", () => {
|
|
8
|
+
test("parses TypeScript file blocks with common fence variants", () => {
|
|
9
|
+
const writes = parseTypescriptFileBlocks(`
|
|
10
|
+
\`\`\`ts
|
|
11
|
+
|
|
12
|
+
// File: lib/car/car.constant.ts
|
|
13
|
+
|
|
14
|
+
export const car = "car";
|
|
15
|
+
\`\`\`
|
|
16
|
+
|
|
17
|
+
\`\`\`tsx
|
|
18
|
+
// File: lib/car/Car.Unit.tsx
|
|
19
|
+
export const CarUnit = () => null;
|
|
20
|
+
\`\`\`
|
|
21
|
+
`);
|
|
22
|
+
|
|
23
|
+
expect(writes).toEqual([
|
|
24
|
+
{
|
|
25
|
+
filePath: "lib/car/car.constant.ts",
|
|
26
|
+
content: 'export const car = "car";',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
filePath: "lib/car/Car.Unit.tsx",
|
|
30
|
+
content: "export const CarUnit = () => null;",
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("keeps previous code response when validation responds with prose only", () => {
|
|
36
|
+
const previousContent = `
|
|
37
|
+
\`\`\`typescript
|
|
38
|
+
// File: lib/car/car.constant.ts
|
|
39
|
+
export const car = "car";
|
|
40
|
+
\`\`\`
|
|
41
|
+
`;
|
|
42
|
+
const nextContent =
|
|
43
|
+
"The generated file meets all specified requirements. No rewrite is necessary.";
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
preserveTypescriptResponseContent(previousContent, nextContent),
|
|
47
|
+
).toBe(previousContent);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("uses next code response when validation rewrites with parseable files", () => {
|
|
51
|
+
const previousContent = `
|
|
52
|
+
\`\`\`typescript
|
|
53
|
+
// File: lib/car/car.constant.ts
|
|
54
|
+
export const car = "car";
|
|
55
|
+
\`\`\`
|
|
56
|
+
`;
|
|
57
|
+
const nextContent = `
|
|
58
|
+
\`\`\`typescript
|
|
59
|
+
// File: lib/car/car.constant.ts
|
|
60
|
+
export const car = "updated";
|
|
61
|
+
\`\`\`
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
preserveTypescriptResponseContent(previousContent, nextContent),
|
|
66
|
+
).toBe(nextContent);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/aiEditor.ts
CHANGED
|
@@ -31,8 +31,41 @@ interface EditOptions {
|
|
|
31
31
|
maxTry?: number;
|
|
32
32
|
validate?: string[];
|
|
33
33
|
approve?: boolean;
|
|
34
|
+
fallbackToPreviousTypescript?: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
export const parseTypescriptFileBlocks = (text: string): FileContent[] => {
|
|
38
|
+
const fileBlocks: FileContent[] = [];
|
|
39
|
+
const codeBlockRegex = /```(?:typescript|ts|tsx)\s*\n([\s\S]*?)```/gi;
|
|
40
|
+
const filePathRegex = /^\s*\/\/\s*File:\s*(.+?)\s*$/im;
|
|
41
|
+
|
|
42
|
+
for (const codeBlock of text.matchAll(codeBlockRegex)) {
|
|
43
|
+
const content = codeBlock[1]?.trim();
|
|
44
|
+
if (!content) continue;
|
|
45
|
+
|
|
46
|
+
const filePath = filePathRegex.exec(content)?.[1]?.trim();
|
|
47
|
+
if (!filePath) continue;
|
|
48
|
+
|
|
49
|
+
fileBlocks.push({
|
|
50
|
+
filePath,
|
|
51
|
+
content: content.replace(filePathRegex, "").trim(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return fileBlocks;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const preserveTypescriptResponseContent = (
|
|
59
|
+
previousContent: string,
|
|
60
|
+
nextContent: string,
|
|
61
|
+
) => {
|
|
62
|
+
const previousWrites = parseTypescriptFileBlocks(previousContent);
|
|
63
|
+
const nextWrites = parseTypescriptFileBlocks(nextContent);
|
|
64
|
+
if (previousWrites.length > 0 && nextWrites.length === 0)
|
|
65
|
+
return previousContent;
|
|
66
|
+
return nextContent;
|
|
67
|
+
};
|
|
68
|
+
|
|
36
69
|
export class AiSession {
|
|
37
70
|
static #cacheDir = "node_modules/.cache/akan/aiSession";
|
|
38
71
|
static #chat: ChatDeepSeek | ChatOpenAI | null = null;
|
|
@@ -189,8 +222,7 @@ export class AiSession {
|
|
|
189
222
|
this.messageHistory.push(humanMessage);
|
|
190
223
|
const stream = await AiSession.#chat.stream(this.messageHistory);
|
|
191
224
|
let reasoningResponse = "",
|
|
192
|
-
fullResponse = ""
|
|
193
|
-
tokenIdx = 0;
|
|
225
|
+
fullResponse = "";
|
|
194
226
|
for await (const chunk of stream) {
|
|
195
227
|
if (loader.isSpinning())
|
|
196
228
|
loader.succeed(`${AiSession.#chat.model} responded`);
|
|
@@ -214,13 +246,12 @@ export class AiSession {
|
|
|
214
246
|
fullResponse += content;
|
|
215
247
|
onChunk(content); // Send individual chunks to callback
|
|
216
248
|
}
|
|
217
|
-
tokenIdx++;
|
|
218
249
|
}
|
|
219
250
|
fullResponse += "\n";
|
|
220
251
|
onChunk("\n");
|
|
221
252
|
this.messageHistory.push(new AIMessage(fullResponse));
|
|
222
253
|
return { content: fullResponse, messageHistory: this.messageHistory };
|
|
223
|
-
} catch
|
|
254
|
+
} catch {
|
|
224
255
|
loader.fail(`${AiSession.#chat.model} failed to respond`);
|
|
225
256
|
throw new Error("Failed to stream response");
|
|
226
257
|
}
|
|
@@ -233,6 +264,7 @@ export class AiSession {
|
|
|
233
264
|
maxTry = MAX_ASK_TRY,
|
|
234
265
|
validate,
|
|
235
266
|
approve,
|
|
267
|
+
fallbackToPreviousTypescript,
|
|
236
268
|
}: EditOptions = {},
|
|
237
269
|
) {
|
|
238
270
|
for (let tryCount = 0; tryCount < maxTry; tryCount++) {
|
|
@@ -240,7 +272,19 @@ export class AiSession {
|
|
|
240
272
|
if (validate?.length && tryCount === 0) {
|
|
241
273
|
const validateQuestion = `Double check if the response meets the requirements and conditions, and follow the instructions. If not, rewrite it.
|
|
242
274
|
${validate.map((v) => `- ${v}`).join("\n")}`;
|
|
243
|
-
|
|
275
|
+
const validateResponse = await this.ask(validateQuestion, {
|
|
276
|
+
onChunk,
|
|
277
|
+
onReasoning,
|
|
278
|
+
});
|
|
279
|
+
response = {
|
|
280
|
+
...validateResponse,
|
|
281
|
+
content: fallbackToPreviousTypescript
|
|
282
|
+
? preserveTypescriptResponseContent(
|
|
283
|
+
response.content,
|
|
284
|
+
validateResponse.content,
|
|
285
|
+
)
|
|
286
|
+
: validateResponse.content,
|
|
287
|
+
};
|
|
244
288
|
}
|
|
245
289
|
const isConfirmed = approve
|
|
246
290
|
? true
|
|
@@ -287,15 +331,35 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
|
|
|
287
331
|
executor: Executor,
|
|
288
332
|
options: EditOptions = {},
|
|
289
333
|
) {
|
|
290
|
-
const content = await this.edit(question,
|
|
334
|
+
const content = await this.edit(question, {
|
|
335
|
+
...options,
|
|
336
|
+
fallbackToPreviousTypescript: true,
|
|
337
|
+
});
|
|
291
338
|
const writes = this.#getTypescriptCodes(content);
|
|
339
|
+
if (!writes.length)
|
|
340
|
+
throw new Error(
|
|
341
|
+
"No parseable TypeScript file blocks were found in the AI response. Include `// File: <path>` in each code block.",
|
|
342
|
+
);
|
|
292
343
|
for (const write of writes)
|
|
293
344
|
await executor.writeFile(write.filePath, write.content);
|
|
294
345
|
return await this.#tryFixTypescripts(writes, executor, options);
|
|
295
346
|
}
|
|
296
|
-
async #editTypescripts(
|
|
297
|
-
|
|
298
|
-
|
|
347
|
+
async #editTypescripts(
|
|
348
|
+
question: string,
|
|
349
|
+
options: EditOptions = {},
|
|
350
|
+
fallbackWrites?: FileContent[],
|
|
351
|
+
) {
|
|
352
|
+
const content = await this.edit(question, {
|
|
353
|
+
...options,
|
|
354
|
+
fallbackToPreviousTypescript: true,
|
|
355
|
+
});
|
|
356
|
+
const writes = this.#getTypescriptCodes(content);
|
|
357
|
+
if (!writes.length && fallbackWrites?.length) return fallbackWrites;
|
|
358
|
+
if (!writes.length)
|
|
359
|
+
throw new Error(
|
|
360
|
+
"No parseable TypeScript file blocks were found in the AI response. Include `// File: <path>` in each code block.",
|
|
361
|
+
);
|
|
362
|
+
return writes;
|
|
299
363
|
}
|
|
300
364
|
async #tryFixTypescripts(
|
|
301
365
|
writes: FileContent[],
|
|
@@ -309,15 +373,16 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
|
|
|
309
373
|
}).start();
|
|
310
374
|
const fileChecks = await Promise.all(
|
|
311
375
|
writes.map(async ({ filePath }) => {
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
|
|
376
|
+
const lintResult = await executor.lint(filePath, { fix: true });
|
|
377
|
+
const typeCheckResult = await executor.typeCheckAsync(filePath);
|
|
378
|
+
const hasTypeErrors = typeCheckResult.fileErrors.length > 0;
|
|
379
|
+
const hasLintErrors = lintResult.errors.length > 0;
|
|
380
|
+
const needFix = hasTypeErrors || hasLintErrors;
|
|
316
381
|
return { filePath, typeCheckResult, lintResult, needFix };
|
|
317
382
|
}),
|
|
318
383
|
);
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
384
|
+
const hasAnyFix = fileChecks.some((fileCheck) => fileCheck.needFix);
|
|
385
|
+
if (hasAnyFix) {
|
|
321
386
|
loader.fail(
|
|
322
387
|
"Type checking and linting has some errors, try to fix them",
|
|
323
388
|
);
|
|
@@ -337,6 +402,7 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
|
|
|
337
402
|
validate: undefined,
|
|
338
403
|
approve: true,
|
|
339
404
|
},
|
|
405
|
+
writes,
|
|
340
406
|
);
|
|
341
407
|
for (const write of writes)
|
|
342
408
|
await executor.writeFile(write.filePath, write.content);
|
|
@@ -348,19 +414,7 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
|
|
|
348
414
|
throw new Error("Failed to create scalar");
|
|
349
415
|
}
|
|
350
416
|
#getTypescriptCodes(text: string): FileContent[] {
|
|
351
|
-
|
|
352
|
-
if (!codes) return [];
|
|
353
|
-
const result = codes.map((code) => {
|
|
354
|
-
const content = /```(typescript|tsx)([\s\S]*?)```/.exec(code)?.[2];
|
|
355
|
-
if (!content) return null;
|
|
356
|
-
const filePath = /\/\/ File: (.*?)(?:\n|$)/.exec(content)?.[1]?.trim();
|
|
357
|
-
if (!filePath) return null;
|
|
358
|
-
const contentWithoutFilepath = content
|
|
359
|
-
.replace(`// File: ${filePath}\n`, "")
|
|
360
|
-
.trim();
|
|
361
|
-
return { filePath, content: contentWithoutFilepath };
|
|
362
|
-
});
|
|
363
|
-
return result.filter((code) => code !== null) as FileContent[];
|
|
417
|
+
return parseTypescriptFileBlocks(text);
|
|
364
418
|
}
|
|
365
419
|
async editMarkdown(request: string, options: EditOptions = {}) {
|
|
366
420
|
const content = await this.edit(request, options);
|
|
@@ -7,7 +7,13 @@ import { AkanAppConfig, AkanLibConfig } from "./akanConfig";
|
|
|
7
7
|
import type { DeepPartial, LibConfigResult } from "./types";
|
|
8
8
|
|
|
9
9
|
const akanPackageJson = JSON.parse(
|
|
10
|
-
fs.readFileSync(
|
|
10
|
+
fs.readFileSync(
|
|
11
|
+
path.join(
|
|
12
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
13
|
+
"../../../akanjs/package.json",
|
|
14
|
+
),
|
|
15
|
+
"utf8",
|
|
16
|
+
),
|
|
11
17
|
) as PackageJson;
|
|
12
18
|
|
|
13
19
|
const packageJson: PackageJson = {
|
|
@@ -34,7 +40,13 @@ const baseDevEnv = {
|
|
|
34
40
|
|
|
35
41
|
describe("AkanAppConfig", () => {
|
|
36
42
|
test("applies defaults for route domains, i18n, image, mobile, and imports", () => {
|
|
37
|
-
const config = new AkanAppConfig(
|
|
43
|
+
const config = new AkanAppConfig(
|
|
44
|
+
app,
|
|
45
|
+
["shared"],
|
|
46
|
+
packageJson,
|
|
47
|
+
{},
|
|
48
|
+
baseDevEnv,
|
|
49
|
+
);
|
|
38
50
|
|
|
39
51
|
expect([...config.domains].sort()).toEqual([
|
|
40
52
|
"portal-debug.akanjs.com",
|
|
@@ -61,7 +73,11 @@ describe("AkanAppConfig", () => {
|
|
|
61
73
|
},
|
|
62
74
|
});
|
|
63
75
|
expect(config.barrelImports).toEqual(
|
|
64
|
-
expect.arrayContaining([
|
|
76
|
+
expect.arrayContaining([
|
|
77
|
+
"@apps/portal/ui",
|
|
78
|
+
"@libs/shared/server",
|
|
79
|
+
"akanjs/common",
|
|
80
|
+
]),
|
|
65
81
|
);
|
|
66
82
|
expect(config.docker.content).toContain("ENV AKAN_PUBLIC_APP_NAME=portal");
|
|
67
83
|
expect(process.env.AKAN_PUBLIC_DEFAULT_LOCALE).toBe("en");
|
|
@@ -75,10 +91,21 @@ describe("AkanAppConfig", () => {
|
|
|
75
91
|
{
|
|
76
92
|
routes: [
|
|
77
93
|
{ domains: { debug: ["Root.Local:8282"], qa: ["QA.Root.Local"] } },
|
|
78
|
-
{
|
|
94
|
+
{
|
|
95
|
+
basePath: "/admin/",
|
|
96
|
+
domains: {
|
|
97
|
+
debug: ["Admin.Local:8282"],
|
|
98
|
+
main: ["Admin.Main.Local"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
79
101
|
],
|
|
80
102
|
i18n: { locales: ["ko", "en"], defaultLocale: "ko" },
|
|
81
|
-
mobile: {
|
|
103
|
+
mobile: {
|
|
104
|
+
appName: "Portal App",
|
|
105
|
+
appId: "com.portal.mobile",
|
|
106
|
+
version: "1.2.3",
|
|
107
|
+
buildNum: 7,
|
|
108
|
+
},
|
|
82
109
|
images: { qualities: [80, 90], dangerouslyAllowSVG: true },
|
|
83
110
|
docker: {
|
|
84
111
|
image: { amd64: "oven/bun:amd64", arm64: "oven/bun:arm64" },
|
|
@@ -102,7 +129,12 @@ describe("AkanAppConfig", () => {
|
|
|
102
129
|
"admin.local",
|
|
103
130
|
"admin.main.local",
|
|
104
131
|
]);
|
|
105
|
-
expect([...config.branches].sort()).toEqual([
|
|
132
|
+
expect([...config.branches].sort()).toEqual([
|
|
133
|
+
"debug",
|
|
134
|
+
"develop",
|
|
135
|
+
"main",
|
|
136
|
+
"qa",
|
|
137
|
+
]);
|
|
106
138
|
expect(config.i18n.defaultLocale).toBe("ko");
|
|
107
139
|
expect(config.images.qualities).toEqual([80, 90]);
|
|
108
140
|
expect(config.images.dangerouslyAllowSVG).toBe(true);
|
|
@@ -122,9 +154,17 @@ describe("AkanAppConfig", () => {
|
|
|
122
154
|
});
|
|
123
155
|
|
|
124
156
|
test("creates production package json and reports missing external versions", () => {
|
|
125
|
-
const config = new AkanAppConfig(
|
|
157
|
+
const config = new AkanAppConfig(
|
|
158
|
+
app,
|
|
159
|
+
[],
|
|
160
|
+
packageJson,
|
|
161
|
+
{ externalLibs: ["@external/runtime"] },
|
|
162
|
+
baseDevEnv,
|
|
163
|
+
);
|
|
126
164
|
|
|
127
|
-
expect(
|
|
165
|
+
expect(
|
|
166
|
+
config.getProductionPackageJson({ scripts: { start: "bun main.js" } }),
|
|
167
|
+
).toMatchObject({
|
|
128
168
|
name: "portal",
|
|
129
169
|
main: "./main.js",
|
|
130
170
|
scripts: { start: "bun main.js" },
|
|
@@ -145,11 +185,16 @@ describe("AkanAppConfig", () => {
|
|
|
145
185
|
{ externalLibs: ["missing-lib"] },
|
|
146
186
|
baseDevEnv,
|
|
147
187
|
);
|
|
148
|
-
expect(() => brokenConfig.getProductionPackageJson()).toThrow(
|
|
188
|
+
expect(() => brokenConfig.getProductionPackageJson()).toThrow(
|
|
189
|
+
"Dependency missing-lib not found",
|
|
190
|
+
);
|
|
149
191
|
});
|
|
150
192
|
|
|
151
193
|
test("falls back to akanjs package versions for built-in runtime dependencies", () => {
|
|
152
|
-
const runtimeDependencies = {
|
|
194
|
+
const runtimeDependencies = {
|
|
195
|
+
...akanPackageJson.dependencies,
|
|
196
|
+
...akanPackageJson.peerDependencies,
|
|
197
|
+
};
|
|
153
198
|
const config = new AkanAppConfig(
|
|
154
199
|
app,
|
|
155
200
|
[],
|
|
@@ -168,44 +213,130 @@ describe("AkanAppConfig", () => {
|
|
|
168
213
|
expect(config.getProductionPackageJson().dependencies).toEqual({
|
|
169
214
|
react: runtimeDependencies.react,
|
|
170
215
|
"react-dom": runtimeDependencies["react-dom"],
|
|
171
|
-
"react-server-dom-webpack":
|
|
216
|
+
"react-server-dom-webpack":
|
|
217
|
+
runtimeDependencies["react-server-dom-webpack"],
|
|
172
218
|
croner: runtimeDependencies.croner,
|
|
173
219
|
sharp: runtimeDependencies.sharp,
|
|
174
220
|
});
|
|
175
221
|
});
|
|
176
222
|
|
|
177
223
|
test("adds backend runtime packages by database mode", () => {
|
|
178
|
-
const runtimeDependencies = {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
const runtimeDependencies = {
|
|
225
|
+
...akanPackageJson.dependencies,
|
|
226
|
+
...akanPackageJson.peerDependencies,
|
|
227
|
+
};
|
|
228
|
+
const singleConfig = new AkanAppConfig(
|
|
229
|
+
app,
|
|
230
|
+
[],
|
|
231
|
+
packageJson,
|
|
232
|
+
{ defaultDatabaseMode: "single" },
|
|
233
|
+
baseDevEnv,
|
|
234
|
+
);
|
|
235
|
+
const multipleConfig = new AkanAppConfig(
|
|
236
|
+
app,
|
|
237
|
+
[],
|
|
238
|
+
packageJson,
|
|
239
|
+
{ defaultDatabaseMode: "multiple" },
|
|
240
|
+
baseDevEnv,
|
|
241
|
+
);
|
|
242
|
+
const clusterConfig = new AkanAppConfig(
|
|
243
|
+
app,
|
|
244
|
+
[],
|
|
245
|
+
packageJson,
|
|
246
|
+
{ defaultDatabaseMode: "cluster" },
|
|
247
|
+
baseDevEnv,
|
|
248
|
+
);
|
|
182
249
|
|
|
183
250
|
expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
184
251
|
croner: runtimeDependencies.croner,
|
|
185
252
|
});
|
|
186
|
-
expect(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expect(
|
|
190
|
-
|
|
253
|
+
expect(
|
|
254
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
255
|
+
).not.toHaveProperty("ioredis");
|
|
256
|
+
expect(
|
|
257
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
258
|
+
).not.toHaveProperty("bullmq");
|
|
259
|
+
expect(
|
|
260
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
261
|
+
).not.toHaveProperty("@libsql/client");
|
|
262
|
+
expect(
|
|
263
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
264
|
+
).not.toHaveProperty("postgres");
|
|
265
|
+
expect(
|
|
266
|
+
singleConfig.getProductionPackageJson().dependencies,
|
|
267
|
+
).not.toHaveProperty("protobufjs");
|
|
191
268
|
|
|
192
|
-
expect(
|
|
269
|
+
expect(
|
|
270
|
+
multipleConfig.getProductionPackageJson().dependencies,
|
|
271
|
+
).toMatchObject({
|
|
193
272
|
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
194
273
|
bullmq: runtimeDependencies.bullmq,
|
|
195
274
|
croner: runtimeDependencies.croner,
|
|
196
275
|
ioredis: runtimeDependencies.ioredis,
|
|
197
276
|
protobufjs: runtimeDependencies.protobufjs,
|
|
198
277
|
});
|
|
199
|
-
expect(
|
|
278
|
+
expect(
|
|
279
|
+
multipleConfig.getProductionPackageJson().dependencies,
|
|
280
|
+
).not.toHaveProperty("postgres");
|
|
200
281
|
|
|
201
|
-
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
282
|
+
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject(
|
|
283
|
+
{
|
|
284
|
+
bullmq: runtimeDependencies.bullmq,
|
|
285
|
+
croner: runtimeDependencies.croner,
|
|
286
|
+
ioredis: runtimeDependencies.ioredis,
|
|
287
|
+
postgres: runtimeDependencies.postgres,
|
|
288
|
+
protobufjs: runtimeDependencies.protobufjs,
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
expect(
|
|
292
|
+
clusterConfig.getProductionPackageJson().dependencies,
|
|
293
|
+
).not.toHaveProperty("@libsql/client");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("resolves database mode runtime packages and missing install specs", () => {
|
|
297
|
+
const runtimeDependencies = {
|
|
298
|
+
...akanPackageJson.dependencies,
|
|
299
|
+
...akanPackageJson.peerDependencies,
|
|
300
|
+
};
|
|
301
|
+
const config = new AkanAppConfig(
|
|
302
|
+
app,
|
|
303
|
+
[],
|
|
304
|
+
{
|
|
305
|
+
name: "repo",
|
|
306
|
+
version: "1.0.0",
|
|
307
|
+
description: "repo",
|
|
308
|
+
dependencies: {
|
|
309
|
+
bullmq: "5.0.0",
|
|
310
|
+
},
|
|
311
|
+
devDependencies: {
|
|
312
|
+
ioredis: "5.0.0",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{},
|
|
316
|
+
baseDevEnv,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(config.getDatabaseModeRuntimePackages("single")).toEqual([]);
|
|
320
|
+
expect(config.getDatabaseModeRuntimePackages("multiple")).toEqual([
|
|
321
|
+
"@libsql/client",
|
|
322
|
+
"bullmq",
|
|
323
|
+
"ioredis",
|
|
324
|
+
"protobufjs",
|
|
325
|
+
]);
|
|
326
|
+
expect(config.getDatabaseModeRuntimePackages("cluster")).toEqual([
|
|
327
|
+
"bullmq",
|
|
328
|
+
"ioredis",
|
|
329
|
+
"postgres",
|
|
330
|
+
"protobufjs",
|
|
331
|
+
]);
|
|
332
|
+
expect(config.getMissingDatabaseModeDependencySpecs("multiple")).toEqual([
|
|
333
|
+
`@libsql/client@${runtimeDependencies["@libsql/client"]}`,
|
|
334
|
+
`protobufjs@${runtimeDependencies.protobufjs}`,
|
|
335
|
+
]);
|
|
336
|
+
expect(config.getMissingDatabaseModeDependencySpecs("cluster")).toEqual([
|
|
337
|
+
`postgres@${runtimeDependencies.postgres}`,
|
|
338
|
+
`protobufjs@${runtimeDependencies.protobufjs}`,
|
|
339
|
+
]);
|
|
209
340
|
});
|
|
210
341
|
|
|
211
342
|
test("normalizes multiple mobile targets and validates base paths", () => {
|
|
@@ -266,7 +397,11 @@ describe("AkanLibConfig", () => {
|
|
|
266
397
|
const lib = { name: "shared" } as never;
|
|
267
398
|
expect(new AkanLibConfig(lib, {}).externalLibs).toEqual([]);
|
|
268
399
|
|
|
269
|
-
const config: DeepPartial<LibConfigResult> = {
|
|
270
|
-
|
|
400
|
+
const config: DeepPartial<LibConfigResult> = {
|
|
401
|
+
externalLibs: ["firebase-admin"],
|
|
402
|
+
};
|
|
403
|
+
expect(new AkanLibConfig(lib, config).externalLibs).toEqual([
|
|
404
|
+
"firebase-admin",
|
|
405
|
+
]);
|
|
271
406
|
});
|
|
272
407
|
});
|