@akanjs/devkit 3.0.0-alpha.7 → 3.0.0-alpha.71
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 +15 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +47 -1
- package/aiEditor.ts +1 -1
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +132 -47
- package/akanConfig/types.ts +8 -0
- package/akanContext.ts +53 -11
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.ts +2 -2
- package/biome.base.json +340 -0
- package/biomeBase.ts +9 -0
- package/executors.test.ts +87 -5
- package/executors.ts +40 -9
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/buildRouteClient.test.ts +28 -2
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/cssCompiler.ts +122 -11
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +88 -1
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -0
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleGuard.test.ts +15 -0
- package/frontendBuild/styleGuard.ts +17 -0
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/getCredentials.ts +1 -3
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +2 -2
- package/integration/devStabilityHarness.ts +2 -10
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/linter.ts +17 -12
- package/package.json +5 -5
- package/qualityScanner.test.ts +52 -0
- package/qualityScanner.ts +46 -18
- package/repoIdentity.ts +42 -0
- package/scanInfo.ts +29 -23
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/tsconfig.json +1 -1
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @akanjs/devkit
|
|
2
2
|
|
|
3
|
+
## 2.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [6d58c7e]
|
|
8
|
+
- Updated dependencies [11aa655]
|
|
9
|
+
- Updated dependencies [25d5b15]
|
|
10
|
+
- Updated dependencies [11aa655]
|
|
11
|
+
- Updated dependencies [11aa655]
|
|
12
|
+
- Updated dependencies [25d5b15]
|
|
13
|
+
- Updated dependencies [42cf7a2]
|
|
14
|
+
- Updated dependencies [25d5b15]
|
|
15
|
+
- Updated dependencies [04cb46d]
|
|
16
|
+
- akanjs@2.4.2
|
|
17
|
+
|
|
3
18
|
## 2.4.1
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/README.ko.md
CHANGED
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ defaults, the shares they derive from `AKAN_MEMORY_LIMIT`, and what a small cont
|
|
|
60
60
|
|
|
61
61
|
## Requirements
|
|
62
62
|
|
|
63
|
-
- [Bun](https://bun.sh) `>=1.
|
|
63
|
+
- [Bun](https://bun.sh) `>=1.4.0`
|
|
64
64
|
- TypeScript
|
|
65
65
|
- Optional peers are only needed for the features that use them, such as Capacitor integration.
|
|
66
66
|
|
package/agentsIndex.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
renderRecipeEntries,
|
|
11
11
|
renderScopeAgentBlock,
|
|
12
12
|
renderScopeAgentsMd,
|
|
13
|
+
renderScopeClaudeMd,
|
|
13
14
|
upsertAgentBlock,
|
|
14
15
|
} from "./agentsIndex";
|
|
15
16
|
import type { RecipeInfo } from "./recipeScanner";
|
|
@@ -48,6 +49,15 @@ describe("renderScopeAgentBlock", () => {
|
|
|
48
49
|
});
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
describe("renderScopeClaudeMd", () => {
|
|
53
|
+
test("points at AGENTS.md and restates only the comment rule", () => {
|
|
54
|
+
const claude = renderScopeClaudeMd({ type: "app", name: "minimal" });
|
|
55
|
+
expect(claude).toContain("@AGENTS.md");
|
|
56
|
+
expect(claude).toContain("## Comments — Overrides Your Default");
|
|
57
|
+
expect(claude).not.toContain("## Recipes In Scope");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
51
61
|
describe("upsertAgentBlock / extractAgentBlock", () => {
|
|
52
62
|
test("round-trips: fresh file → replace block → extract equals block", () => {
|
|
53
63
|
const fresh = renderScopeAgentsMd({ type: "app", name: "minimal" }, "OLD");
|
package/agentsIndex.ts
CHANGED
|
@@ -19,6 +19,32 @@ const loadRecipeScanner = () => (recipeScannerLoad ??= import("./recipeScanner")
|
|
|
19
19
|
export const AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
20
20
|
export const AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
21
21
|
|
|
22
|
+
const AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The generating release, stamped into the workspace block. Nothing re-runs `akan agent install` on its own, so
|
|
26
|
+
* without a stamp a workspace carrying conventions from four releases ago is indistinguishable from a current one —
|
|
27
|
+
* `akan doctor` compares this against the installed devkit and says so.
|
|
28
|
+
*/
|
|
29
|
+
export const stampBlockVersion = (block: string, version: string): string =>
|
|
30
|
+
`${AGENT_VERSION_PREFIX}${version} -->\n\n${block}`;
|
|
31
|
+
|
|
32
|
+
/** The stamped version of a committed AGENTS.md, or null when it predates stamping or carries no block. */
|
|
33
|
+
export const extractBlockVersion = (content: string): string | null =>
|
|
34
|
+
content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
|
|
35
|
+
|
|
36
|
+
/** The running `@akanjs/devkit` version; null when its package.json is unreadable, which must not fail a doctor run. */
|
|
37
|
+
export const readDevkitVersion = async (): Promise<string | null> => {
|
|
38
|
+
const { readFile } = await import("node:fs/promises");
|
|
39
|
+
const { getDirname } = await import("./getDirname");
|
|
40
|
+
try {
|
|
41
|
+
const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
|
|
42
|
+
return (JSON.parse(raw) as { version?: string }).version ?? null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
22
48
|
export interface AgentsIndexScope {
|
|
23
49
|
type: "app" | "lib";
|
|
24
50
|
name: string;
|
|
@@ -139,8 +165,28 @@ ${block}
|
|
|
139
165
|
${AGENT_BLOCK_END}
|
|
140
166
|
`;
|
|
141
167
|
|
|
142
|
-
|
|
168
|
+
export const CLAUDE_COMMENT_RULE = `## Comments — Overrides Your Default
|
|
169
|
+
|
|
170
|
+
Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
|
|
171
|
+
is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
|
|
172
|
+
|
|
173
|
+
Before typing \`//\`, \`/*\`, or a doc block, ask — **does this sentence carry a fact that is nowhere in the code?**
|
|
174
|
+
|
|
175
|
+
- Restates the identifier, the signature, or the line under it → delete it.
|
|
176
|
+
- Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) → delete it.
|
|
177
|
+
- JSDoc on an ordinary function, or a why/how preamble on ordinary logic → delete it.
|
|
178
|
+
- Explains the edit you just made, for whoever reads the diff → say it in your reply, not in the file.
|
|
179
|
+
- Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
|
|
180
|
+
derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected →
|
|
181
|
+
keep it, one line.
|
|
182
|
+
|
|
183
|
+
That keep-list is exact — \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
|
|
184
|
+
subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
|
|
185
|
+
surrounding code carries none, your diff carries none.`;
|
|
186
|
+
|
|
143
187
|
export const renderScopeClaudeMd = (scope: AgentsIndexScope): string => `# ${scope.name} — Claude Code Guide
|
|
144
188
|
|
|
145
189
|
@AGENTS.md
|
|
190
|
+
|
|
191
|
+
${CLAUDE_COMMENT_RULE}
|
|
146
192
|
`;
|
package/aiEditor.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { FileContent } from "./types";
|
|
|
19
19
|
|
|
20
20
|
const MAX_ASK_TRY = 300;
|
|
21
21
|
|
|
22
|
-
const deepSeekLlmModels = ["deepseek-
|
|
22
|
+
const deepSeekLlmModels = ["deepseek-v4-flash", "deepseek-v4-pro"] as const;
|
|
23
23
|
|
|
24
24
|
const openAiLlmModels = ["gpt-5.5"] as const;
|
|
25
25
|
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { AppExecutor, WorkspaceExecutor } from "../executors";
|
|
6
7
|
import type { PackageJson } from "../types";
|
|
7
8
|
import { AkanAppConfig, AkanLibConfig, deriveDefaultAppId } from "./akanConfig";
|
|
8
9
|
import type { DeepPartial, LibConfigResult } from "./types";
|
|
@@ -19,7 +20,6 @@ const packageJson: PackageJson = {
|
|
|
19
20
|
react: "19.0.0",
|
|
20
21
|
"react-dom": "19.0.0",
|
|
21
22
|
"react-server-dom-webpack": "19.0.0",
|
|
22
|
-
sharp: "1.0.0",
|
|
23
23
|
"@external/runtime": "2.0.0",
|
|
24
24
|
},
|
|
25
25
|
};
|
|
@@ -64,7 +64,8 @@ describe("AkanAppConfig", () => {
|
|
|
64
64
|
expect(config.barrelImports).toEqual(
|
|
65
65
|
expect.arrayContaining(["@apps/portal/ui", "@libs/shared/server", "akanjs/common", "akanjs/server"]),
|
|
66
66
|
);
|
|
67
|
-
expect(config.
|
|
67
|
+
expect(config.dockerfile).toContain("ENV AKAN_PUBLIC_APP_NAME=portal");
|
|
68
|
+
expect(config.dockerfile).toContain("ENV AKAN_LOG_TO_FILE=0");
|
|
68
69
|
expect(process.env.AKAN_PUBLIC_DEFAULT_LOCALE).toBe("en");
|
|
69
70
|
});
|
|
70
71
|
|
|
@@ -128,9 +129,67 @@ describe("AkanAppConfig", () => {
|
|
|
128
129
|
});
|
|
129
130
|
expect(config.publicEnv).toEqual(["AKAN_PUBLIC_FEATURE"]);
|
|
130
131
|
expect(config.optimizeImports).toContain("custom-icons");
|
|
131
|
-
expect(config.
|
|
132
|
-
expect(config.
|
|
133
|
-
expect(config.
|
|
132
|
+
expect(config.dockerfile).toContain('CMD ["bun","server.js"]');
|
|
133
|
+
expect(config.dockerfile).toContain("FROM oven/bun:amd64 AS amd64");
|
|
134
|
+
expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("defaults both web surfaces on and keeps the image free of web env overrides", () => {
|
|
138
|
+
const config = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
|
|
139
|
+
|
|
140
|
+
expect(config.web).toEqual({ ssr: true, csr: true });
|
|
141
|
+
expect(config.dockerfile).not.toContain("AKAN_SSR");
|
|
142
|
+
expect(config.dockerfile).not.toContain("AKAN_CSR");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("bakes the disabled surface into the image env so the default matches what was built", () => {
|
|
146
|
+
const ssrOnly = new AkanAppConfig(app, [], packageJson, { web: { csr: false } }, baseDevEnv);
|
|
147
|
+
expect(ssrOnly.web).toEqual({ ssr: true, csr: false });
|
|
148
|
+
expect(ssrOnly.dockerfile).toContain("ENV AKAN_CSR=false");
|
|
149
|
+
expect(ssrOnly.dockerfile).not.toContain("ENV AKAN_SSR=false");
|
|
150
|
+
|
|
151
|
+
const apiOnly = new AkanAppConfig(app, [], packageJson, { web: false }, baseDevEnv);
|
|
152
|
+
expect(apiOnly.web).toEqual({ ssr: false, csr: false });
|
|
153
|
+
expect(apiOnly.dockerfile).toContain("ENV AKAN_SSR=false");
|
|
154
|
+
expect(apiOnly.dockerfile).toContain("ENV AKAN_CSR=false");
|
|
155
|
+
|
|
156
|
+
expect(new AkanAppConfig(app, [], packageJson, { web: true }, baseDevEnv).web).toEqual({ ssr: true, csr: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("refuses a csr-less build that ships a mobile app", () => {
|
|
160
|
+
expect(
|
|
161
|
+
() => new AkanAppConfig(app, [], packageJson, { web: { csr: false }, mobile: { appName: "portal" } }, baseDevEnv),
|
|
162
|
+
).toThrow("the Capacitor build ships that bundle");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("installs only ca-certificates and tzdata in the default image", () => {
|
|
166
|
+
const config = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
|
|
167
|
+
|
|
168
|
+
expect(config.dockerfile).toContain(
|
|
169
|
+
"RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*",
|
|
170
|
+
);
|
|
171
|
+
// The Chromium/ffmpeg toolchain moved to per-app `preRuns`; keeping it here paid for it in every image.
|
|
172
|
+
for (const dropped of ["libnss3", "ffmpeg", "build-essential", "redis", "xdg-utils"])
|
|
173
|
+
expect(config.dockerfile).not.toContain(dropped);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("keeps a declared Dockerfile string verbatim", () => {
|
|
177
|
+
const dockerfile = 'FROM oven/bun:1-slim\nCOPY . .\nCMD ["bun","main.js"]';
|
|
178
|
+
const config = new AkanAppConfig(app, [], packageJson, { docker: dockerfile }, baseDevEnv);
|
|
179
|
+
|
|
180
|
+
expect(config.docker).toBe(dockerfile);
|
|
181
|
+
expect(config.dockerfile).toBe(dockerfile);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("resolves the image parts, defaulting the base image and the command", () => {
|
|
185
|
+
const config = new AkanAppConfig(app, [], packageJson, { docker: { preRuns: ["echo hi"] } }, baseDevEnv);
|
|
186
|
+
|
|
187
|
+
expect(config.docker).toEqual({
|
|
188
|
+
image: "oven/bun:1-slim",
|
|
189
|
+
preRuns: ["echo hi"],
|
|
190
|
+
postRuns: [],
|
|
191
|
+
command: ["bun", "main.js"],
|
|
192
|
+
});
|
|
134
193
|
});
|
|
135
194
|
|
|
136
195
|
test("creates production package json and reports missing external versions", () => {
|
|
@@ -144,8 +203,6 @@ describe("AkanAppConfig", () => {
|
|
|
144
203
|
react: "19.0.0",
|
|
145
204
|
"react-dom": "19.0.0",
|
|
146
205
|
"react-server-dom-webpack": "19.0.0",
|
|
147
|
-
croner: akanPackageJson.peerDependencies?.croner,
|
|
148
|
-
sharp: "1.0.0",
|
|
149
206
|
"@external/runtime": "2.0.0",
|
|
150
207
|
},
|
|
151
208
|
});
|
|
@@ -184,8 +241,6 @@ describe("AkanAppConfig", () => {
|
|
|
184
241
|
react: runtimeDependencies.react,
|
|
185
242
|
"react-dom": runtimeDependencies["react-dom"],
|
|
186
243
|
"react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
|
|
187
|
-
croner: runtimeDependencies.croner,
|
|
188
|
-
sharp: runtimeDependencies.sharp,
|
|
189
244
|
});
|
|
190
245
|
});
|
|
191
246
|
|
|
@@ -198,9 +253,6 @@ describe("AkanAppConfig", () => {
|
|
|
198
253
|
const multipleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "multiple" }, baseDevEnv);
|
|
199
254
|
const clusterConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "cluster" }, baseDevEnv);
|
|
200
255
|
|
|
201
|
-
expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
202
|
-
croner: runtimeDependencies.croner,
|
|
203
|
-
});
|
|
204
256
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("ioredis");
|
|
205
257
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("bullmq");
|
|
206
258
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
|
|
@@ -210,7 +262,6 @@ describe("AkanAppConfig", () => {
|
|
|
210
262
|
expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
211
263
|
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
212
264
|
bullmq: runtimeDependencies.bullmq,
|
|
213
|
-
croner: runtimeDependencies.croner,
|
|
214
265
|
ioredis: runtimeDependencies.ioredis,
|
|
215
266
|
protobufjs: runtimeDependencies.protobufjs,
|
|
216
267
|
});
|
|
@@ -218,7 +269,6 @@ describe("AkanAppConfig", () => {
|
|
|
218
269
|
|
|
219
270
|
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
220
271
|
bullmq: runtimeDependencies.bullmq,
|
|
221
|
-
croner: runtimeDependencies.croner,
|
|
222
272
|
ioredis: runtimeDependencies.ioredis,
|
|
223
273
|
postgres: runtimeDependencies.postgres,
|
|
224
274
|
protobufjs: runtimeDependencies.protobufjs,
|
|
@@ -392,6 +442,115 @@ describe("deriveDefaultAppId", () => {
|
|
|
392
442
|
});
|
|
393
443
|
});
|
|
394
444
|
|
|
445
|
+
describe("AkanAppConfig lib externalLibs", () => {
|
|
446
|
+
test("merges lib-declared external libs into the app's own, deduped", () => {
|
|
447
|
+
const libAwarePackageJson: PackageJson = {
|
|
448
|
+
...packageJson,
|
|
449
|
+
dependencies: { ...packageJson.dependencies, puppeteer: "24.0.0" },
|
|
450
|
+
};
|
|
451
|
+
const config = new AkanAppConfig(
|
|
452
|
+
app,
|
|
453
|
+
["shared"],
|
|
454
|
+
libAwarePackageJson,
|
|
455
|
+
{ externalLibs: ["@external/runtime"] },
|
|
456
|
+
baseDevEnv,
|
|
457
|
+
[],
|
|
458
|
+
{ externalLibs: ["@external/runtime", "puppeteer"], docker: { preRuns: [], postRuns: [] } },
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(config.externalLibs).toEqual(["@external/runtime", "puppeteer"]);
|
|
462
|
+
expect(config.getProductionPackageJson().dependencies).toMatchObject({
|
|
463
|
+
"@external/runtime": "2.0.0",
|
|
464
|
+
puppeteer: "24.0.0",
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("reads them off every workspace lib config on load", async () => {
|
|
469
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libext-"));
|
|
470
|
+
try {
|
|
471
|
+
fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
|
|
472
|
+
fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
|
|
473
|
+
fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default { externalLibs: ['shiki'] };\n");
|
|
474
|
+
fs.writeFileSync(
|
|
475
|
+
path.join(root, "libs/extlib/akan.config.ts"),
|
|
476
|
+
"export default { externalLibs: ['puppeteer'] };\n",
|
|
477
|
+
);
|
|
478
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
479
|
+
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
480
|
+
|
|
481
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
482
|
+
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
483
|
+
|
|
484
|
+
expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
|
|
485
|
+
} finally {
|
|
486
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("AkanAppConfig lib docker runs", () => {
|
|
492
|
+
const libDocker = (preRuns: string[], postRuns: string[] = []) => ({
|
|
493
|
+
externalLibs: [],
|
|
494
|
+
docker: { preRuns, postRuns },
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("runs lib steps before the app's own, deduped", () => {
|
|
498
|
+
const config = new AkanAppConfig(
|
|
499
|
+
app,
|
|
500
|
+
["shared"],
|
|
501
|
+
packageJson,
|
|
502
|
+
{ docker: { preRuns: ["apt-get install -y ffmpeg", "echo app"], postRuns: ["echo app-post"] } },
|
|
503
|
+
baseDevEnv,
|
|
504
|
+
[],
|
|
505
|
+
libDocker(["apt-get install -y ffmpeg", "echo lib"], ["echo lib-post"]),
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
expect(config.docker).toMatchObject({
|
|
509
|
+
preRuns: ["apt-get install -y ffmpeg", "echo lib", "echo app"],
|
|
510
|
+
postRuns: ["echo lib-post", "echo app-post"],
|
|
511
|
+
});
|
|
512
|
+
expect(config.dockerfile).toContain("RUN echo lib\nRUN echo app\n");
|
|
513
|
+
expect(config.dockerfile.match(/RUN apt-get install -y ffmpeg/g)).toHaveLength(1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("drops them when the app hands over a whole Dockerfile", () => {
|
|
517
|
+
const config = new AkanAppConfig(
|
|
518
|
+
app,
|
|
519
|
+
["shared"],
|
|
520
|
+
packageJson,
|
|
521
|
+
{ docker: "FROM scratch" },
|
|
522
|
+
baseDevEnv,
|
|
523
|
+
[],
|
|
524
|
+
libDocker(["echo lib"]),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
expect(config.dockerfile).toBe("FROM scratch");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("reads them off every workspace lib config on load", async () => {
|
|
531
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libdocker-"));
|
|
532
|
+
try {
|
|
533
|
+
fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
|
|
534
|
+
fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
|
|
535
|
+
fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default {};\n");
|
|
536
|
+
fs.writeFileSync(
|
|
537
|
+
path.join(root, "libs/extlib/akan.config.ts"),
|
|
538
|
+
"export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
|
|
539
|
+
);
|
|
540
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
541
|
+
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
542
|
+
|
|
543
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
544
|
+
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
545
|
+
|
|
546
|
+
expect(config.dockerfile).toContain("RUN echo from-lib");
|
|
547
|
+
expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
|
|
548
|
+
} finally {
|
|
549
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
395
554
|
describe("AkanLibConfig", () => {
|
|
396
555
|
test("uses empty external libs by default and preserves explicit libs", () => {
|
|
397
556
|
const lib = { name: "shared" } as never;
|
|
@@ -402,6 +561,15 @@ describe("AkanLibConfig", () => {
|
|
|
402
561
|
};
|
|
403
562
|
expect(new AkanLibConfig(lib, config).externalLibs).toEqual(["firebase-admin"]);
|
|
404
563
|
});
|
|
564
|
+
|
|
565
|
+
test("defaults docker runs to empty lists and preserves declared ones", () => {
|
|
566
|
+
const lib = { name: "shared" } as never;
|
|
567
|
+
expect(new AkanLibConfig(lib, {}).docker).toEqual({ preRuns: [], postRuns: [] });
|
|
568
|
+
expect(new AkanLibConfig(lib, { docker: { preRuns: ["echo lib"] } }).docker).toEqual({
|
|
569
|
+
preRuns: ["echo lib"],
|
|
570
|
+
postRuns: [],
|
|
571
|
+
});
|
|
572
|
+
});
|
|
405
573
|
});
|
|
406
574
|
|
|
407
575
|
describe("AkanAppConfig.importConfigModule", () => {
|