@akanjs/devkit 3.0.0-alpha.7 → 3.0.0-alpha.70
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 +181 -14
- package/akanConfig/akanConfig.ts +128 -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 +336 -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-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,7 @@ 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
68
|
expect(process.env.AKAN_PUBLIC_DEFAULT_LOCALE).toBe("en");
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -128,9 +128,67 @@ describe("AkanAppConfig", () => {
|
|
|
128
128
|
});
|
|
129
129
|
expect(config.publicEnv).toEqual(["AKAN_PUBLIC_FEATURE"]);
|
|
130
130
|
expect(config.optimizeImports).toContain("custom-icons");
|
|
131
|
-
expect(config.
|
|
132
|
-
expect(config.
|
|
133
|
-
expect(config.
|
|
131
|
+
expect(config.dockerfile).toContain('CMD ["bun","server.js"]');
|
|
132
|
+
expect(config.dockerfile).toContain("FROM oven/bun:amd64 AS amd64");
|
|
133
|
+
expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("defaults both web surfaces on and keeps the image free of web env overrides", () => {
|
|
137
|
+
const config = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
|
|
138
|
+
|
|
139
|
+
expect(config.web).toEqual({ ssr: true, csr: true });
|
|
140
|
+
expect(config.dockerfile).not.toContain("AKAN_SSR");
|
|
141
|
+
expect(config.dockerfile).not.toContain("AKAN_CSR");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("bakes the disabled surface into the image env so the default matches what was built", () => {
|
|
145
|
+
const ssrOnly = new AkanAppConfig(app, [], packageJson, { web: { csr: false } }, baseDevEnv);
|
|
146
|
+
expect(ssrOnly.web).toEqual({ ssr: true, csr: false });
|
|
147
|
+
expect(ssrOnly.dockerfile).toContain("ENV AKAN_CSR=false");
|
|
148
|
+
expect(ssrOnly.dockerfile).not.toContain("ENV AKAN_SSR=false");
|
|
149
|
+
|
|
150
|
+
const apiOnly = new AkanAppConfig(app, [], packageJson, { web: false }, baseDevEnv);
|
|
151
|
+
expect(apiOnly.web).toEqual({ ssr: false, csr: false });
|
|
152
|
+
expect(apiOnly.dockerfile).toContain("ENV AKAN_SSR=false");
|
|
153
|
+
expect(apiOnly.dockerfile).toContain("ENV AKAN_CSR=false");
|
|
154
|
+
|
|
155
|
+
expect(new AkanAppConfig(app, [], packageJson, { web: true }, baseDevEnv).web).toEqual({ ssr: true, csr: true });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("refuses a csr-less build that ships a mobile app", () => {
|
|
159
|
+
expect(
|
|
160
|
+
() => new AkanAppConfig(app, [], packageJson, { web: { csr: false }, mobile: { appName: "portal" } }, baseDevEnv),
|
|
161
|
+
).toThrow("the Capacitor build ships that bundle");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("installs only ca-certificates and tzdata in the default image", () => {
|
|
165
|
+
const config = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
|
|
166
|
+
|
|
167
|
+
expect(config.dockerfile).toContain(
|
|
168
|
+
"RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*",
|
|
169
|
+
);
|
|
170
|
+
// The Chromium/ffmpeg toolchain moved to per-app `preRuns`; keeping it here paid for it in every image.
|
|
171
|
+
for (const dropped of ["libnss3", "ffmpeg", "build-essential", "redis", "xdg-utils"])
|
|
172
|
+
expect(config.dockerfile).not.toContain(dropped);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("keeps a declared Dockerfile string verbatim", () => {
|
|
176
|
+
const dockerfile = 'FROM oven/bun:1-slim\nCOPY . .\nCMD ["bun","main.js"]';
|
|
177
|
+
const config = new AkanAppConfig(app, [], packageJson, { docker: dockerfile }, baseDevEnv);
|
|
178
|
+
|
|
179
|
+
expect(config.docker).toBe(dockerfile);
|
|
180
|
+
expect(config.dockerfile).toBe(dockerfile);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("resolves the image parts, defaulting the base image and the command", () => {
|
|
184
|
+
const config = new AkanAppConfig(app, [], packageJson, { docker: { preRuns: ["echo hi"] } }, baseDevEnv);
|
|
185
|
+
|
|
186
|
+
expect(config.docker).toEqual({
|
|
187
|
+
image: "oven/bun:1-slim",
|
|
188
|
+
preRuns: ["echo hi"],
|
|
189
|
+
postRuns: [],
|
|
190
|
+
command: ["bun", "main.js"],
|
|
191
|
+
});
|
|
134
192
|
});
|
|
135
193
|
|
|
136
194
|
test("creates production package json and reports missing external versions", () => {
|
|
@@ -144,8 +202,6 @@ describe("AkanAppConfig", () => {
|
|
|
144
202
|
react: "19.0.0",
|
|
145
203
|
"react-dom": "19.0.0",
|
|
146
204
|
"react-server-dom-webpack": "19.0.0",
|
|
147
|
-
croner: akanPackageJson.peerDependencies?.croner,
|
|
148
|
-
sharp: "1.0.0",
|
|
149
205
|
"@external/runtime": "2.0.0",
|
|
150
206
|
},
|
|
151
207
|
});
|
|
@@ -184,8 +240,6 @@ describe("AkanAppConfig", () => {
|
|
|
184
240
|
react: runtimeDependencies.react,
|
|
185
241
|
"react-dom": runtimeDependencies["react-dom"],
|
|
186
242
|
"react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
|
|
187
|
-
croner: runtimeDependencies.croner,
|
|
188
|
-
sharp: runtimeDependencies.sharp,
|
|
189
243
|
});
|
|
190
244
|
});
|
|
191
245
|
|
|
@@ -198,9 +252,6 @@ describe("AkanAppConfig", () => {
|
|
|
198
252
|
const multipleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "multiple" }, baseDevEnv);
|
|
199
253
|
const clusterConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "cluster" }, baseDevEnv);
|
|
200
254
|
|
|
201
|
-
expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
202
|
-
croner: runtimeDependencies.croner,
|
|
203
|
-
});
|
|
204
255
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("ioredis");
|
|
205
256
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("bullmq");
|
|
206
257
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
|
|
@@ -210,7 +261,6 @@ describe("AkanAppConfig", () => {
|
|
|
210
261
|
expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
211
262
|
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
212
263
|
bullmq: runtimeDependencies.bullmq,
|
|
213
|
-
croner: runtimeDependencies.croner,
|
|
214
264
|
ioredis: runtimeDependencies.ioredis,
|
|
215
265
|
protobufjs: runtimeDependencies.protobufjs,
|
|
216
266
|
});
|
|
@@ -218,7 +268,6 @@ describe("AkanAppConfig", () => {
|
|
|
218
268
|
|
|
219
269
|
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
220
270
|
bullmq: runtimeDependencies.bullmq,
|
|
221
|
-
croner: runtimeDependencies.croner,
|
|
222
271
|
ioredis: runtimeDependencies.ioredis,
|
|
223
272
|
postgres: runtimeDependencies.postgres,
|
|
224
273
|
protobufjs: runtimeDependencies.protobufjs,
|
|
@@ -392,6 +441,115 @@ describe("deriveDefaultAppId", () => {
|
|
|
392
441
|
});
|
|
393
442
|
});
|
|
394
443
|
|
|
444
|
+
describe("AkanAppConfig lib externalLibs", () => {
|
|
445
|
+
test("merges lib-declared external libs into the app's own, deduped", () => {
|
|
446
|
+
const libAwarePackageJson: PackageJson = {
|
|
447
|
+
...packageJson,
|
|
448
|
+
dependencies: { ...packageJson.dependencies, puppeteer: "24.0.0" },
|
|
449
|
+
};
|
|
450
|
+
const config = new AkanAppConfig(
|
|
451
|
+
app,
|
|
452
|
+
["shared"],
|
|
453
|
+
libAwarePackageJson,
|
|
454
|
+
{ externalLibs: ["@external/runtime"] },
|
|
455
|
+
baseDevEnv,
|
|
456
|
+
[],
|
|
457
|
+
{ externalLibs: ["@external/runtime", "puppeteer"], docker: { preRuns: [], postRuns: [] } },
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
expect(config.externalLibs).toEqual(["@external/runtime", "puppeteer"]);
|
|
461
|
+
expect(config.getProductionPackageJson().dependencies).toMatchObject({
|
|
462
|
+
"@external/runtime": "2.0.0",
|
|
463
|
+
puppeteer: "24.0.0",
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("reads them off every workspace lib config on load", async () => {
|
|
468
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libext-"));
|
|
469
|
+
try {
|
|
470
|
+
fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
|
|
471
|
+
fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
|
|
472
|
+
fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default { externalLibs: ['shiki'] };\n");
|
|
473
|
+
fs.writeFileSync(
|
|
474
|
+
path.join(root, "libs/extlib/akan.config.ts"),
|
|
475
|
+
"export default { externalLibs: ['puppeteer'] };\n",
|
|
476
|
+
);
|
|
477
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
478
|
+
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
479
|
+
|
|
480
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
481
|
+
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
482
|
+
|
|
483
|
+
expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
|
|
484
|
+
} finally {
|
|
485
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("AkanAppConfig lib docker runs", () => {
|
|
491
|
+
const libDocker = (preRuns: string[], postRuns: string[] = []) => ({
|
|
492
|
+
externalLibs: [],
|
|
493
|
+
docker: { preRuns, postRuns },
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("runs lib steps before the app's own, deduped", () => {
|
|
497
|
+
const config = new AkanAppConfig(
|
|
498
|
+
app,
|
|
499
|
+
["shared"],
|
|
500
|
+
packageJson,
|
|
501
|
+
{ docker: { preRuns: ["apt-get install -y ffmpeg", "echo app"], postRuns: ["echo app-post"] } },
|
|
502
|
+
baseDevEnv,
|
|
503
|
+
[],
|
|
504
|
+
libDocker(["apt-get install -y ffmpeg", "echo lib"], ["echo lib-post"]),
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
expect(config.docker).toMatchObject({
|
|
508
|
+
preRuns: ["apt-get install -y ffmpeg", "echo lib", "echo app"],
|
|
509
|
+
postRuns: ["echo lib-post", "echo app-post"],
|
|
510
|
+
});
|
|
511
|
+
expect(config.dockerfile).toContain("RUN echo lib\nRUN echo app\n");
|
|
512
|
+
expect(config.dockerfile.match(/RUN apt-get install -y ffmpeg/g)).toHaveLength(1);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("drops them when the app hands over a whole Dockerfile", () => {
|
|
516
|
+
const config = new AkanAppConfig(
|
|
517
|
+
app,
|
|
518
|
+
["shared"],
|
|
519
|
+
packageJson,
|
|
520
|
+
{ docker: "FROM scratch" },
|
|
521
|
+
baseDevEnv,
|
|
522
|
+
[],
|
|
523
|
+
libDocker(["echo lib"]),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
expect(config.dockerfile).toBe("FROM scratch");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("reads them off every workspace lib config on load", async () => {
|
|
530
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libdocker-"));
|
|
531
|
+
try {
|
|
532
|
+
fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
|
|
533
|
+
fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
|
|
534
|
+
fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default {};\n");
|
|
535
|
+
fs.writeFileSync(
|
|
536
|
+
path.join(root, "libs/extlib/akan.config.ts"),
|
|
537
|
+
"export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
|
|
538
|
+
);
|
|
539
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
540
|
+
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
541
|
+
|
|
542
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
543
|
+
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
544
|
+
|
|
545
|
+
expect(config.dockerfile).toContain("RUN echo from-lib");
|
|
546
|
+
expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
|
|
547
|
+
} finally {
|
|
548
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
395
553
|
describe("AkanLibConfig", () => {
|
|
396
554
|
test("uses empty external libs by default and preserves explicit libs", () => {
|
|
397
555
|
const lib = { name: "shared" } as never;
|
|
@@ -402,6 +560,15 @@ describe("AkanLibConfig", () => {
|
|
|
402
560
|
};
|
|
403
561
|
expect(new AkanLibConfig(lib, config).externalLibs).toEqual(["firebase-admin"]);
|
|
404
562
|
});
|
|
563
|
+
|
|
564
|
+
test("defaults docker runs to empty lists and preserves declared ones", () => {
|
|
565
|
+
const lib = { name: "shared" } as never;
|
|
566
|
+
expect(new AkanLibConfig(lib, {}).docker).toEqual({ preRuns: [], postRuns: [] });
|
|
567
|
+
expect(new AkanLibConfig(lib, { docker: { preRuns: ["echo lib"] } }).docker).toEqual({
|
|
568
|
+
preRuns: ["echo lib"],
|
|
569
|
+
postRuns: [],
|
|
570
|
+
});
|
|
571
|
+
});
|
|
405
572
|
});
|
|
406
573
|
|
|
407
574
|
describe("AkanAppConfig.importConfigModule", () => {
|