@akanjs/devkit 3.0.0-alpha.64 → 3.0.0-alpha.66
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/akanConfig/akanConfig.test.ts +136 -5
- package/akanConfig/akanConfig.ts +92 -38
- package/akanConfig/types.ts +6 -0
- package/applicationBuildRunner.ts +26 -18
- package/executors.ts +11 -2
- package/frontendBuild/ssrBaseArtifactBuilder.ts +1 -0
- package/package.json +2 -2
- package/scanInfo.ts +8 -3
|
@@ -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", () => {
|
|
@@ -396,7 +454,7 @@ describe("AkanAppConfig lib externalLibs", () => {
|
|
|
396
454
|
{ externalLibs: ["@external/runtime"] },
|
|
397
455
|
baseDevEnv,
|
|
398
456
|
[],
|
|
399
|
-
["@external/runtime", "puppeteer"],
|
|
457
|
+
{ externalLibs: ["@external/runtime", "puppeteer"], docker: { preRuns: [], postRuns: [] } },
|
|
400
458
|
);
|
|
401
459
|
|
|
402
460
|
expect(config.externalLibs).toEqual(["@external/runtime", "puppeteer"]);
|
|
@@ -429,6 +487,70 @@ describe("AkanAppConfig lib externalLibs", () => {
|
|
|
429
487
|
});
|
|
430
488
|
});
|
|
431
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
|
+
|
|
432
554
|
describe("AkanLibConfig", () => {
|
|
433
555
|
test("uses empty external libs by default and preserves explicit libs", () => {
|
|
434
556
|
const lib = { name: "shared" } as never;
|
|
@@ -439,6 +561,15 @@ describe("AkanLibConfig", () => {
|
|
|
439
561
|
};
|
|
440
562
|
expect(new AkanLibConfig(lib, config).externalLibs).toEqual(["firebase-admin"]);
|
|
441
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
|
+
});
|
|
442
573
|
});
|
|
443
574
|
|
|
444
575
|
describe("AkanAppConfig.importConfigModule", () => {
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -11,13 +11,18 @@ import {
|
|
|
11
11
|
type AkanMobileConfig,
|
|
12
12
|
type AkanMobileTargetConfig,
|
|
13
13
|
type AkanRouteConfig,
|
|
14
|
+
type AkanWebConfig,
|
|
15
|
+
type AkanWebOption,
|
|
14
16
|
type AppConfigResult,
|
|
15
17
|
type Arch,
|
|
16
18
|
archs,
|
|
17
19
|
type DatabaseMode,
|
|
18
20
|
type DeepPartial,
|
|
19
21
|
type DockerConfig,
|
|
22
|
+
type DockerOption,
|
|
23
|
+
type DockerRun,
|
|
20
24
|
type LibConfigResult,
|
|
25
|
+
type LibDockerConfig,
|
|
21
26
|
} from "./types";
|
|
22
27
|
|
|
23
28
|
const DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
|
|
@@ -49,6 +54,7 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
|
|
|
49
54
|
"react-icons/*",
|
|
50
55
|
];
|
|
51
56
|
const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
|
|
57
|
+
const DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
|
|
52
58
|
const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
|
|
53
59
|
// The firebase client (push tokens) and the Capacitor toolchain — `@capacitor/cli` (`npx cap`),
|
|
54
60
|
// `@capacitor/assets` (`npx @capacitor/assets`) plus the `@capacitor/core`/`ios`/`android` runtime
|
|
@@ -163,11 +169,38 @@ const normalizeDeepLinks = (deepLinks: DeepPartial<AkanMobileTargetConfig["deepL
|
|
|
163
169
|
} satisfies AkanMobileTargetConfig["deepLinks"];
|
|
164
170
|
};
|
|
165
171
|
|
|
172
|
+
/** What `akan.config.ts` may write: the resolved shape made partial, with `docker` and `web` in their unions. */
|
|
173
|
+
type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web"> & {
|
|
174
|
+
docker?: DockerOption;
|
|
175
|
+
web?: AkanWebOption;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** What the workspace's libs add to an app's build, read off each `libs/<lib>/akan.config.ts`. */
|
|
179
|
+
export interface LibContributions {
|
|
180
|
+
externalLibs: string[];
|
|
181
|
+
docker: LibDockerConfig;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const emptyLibContributions = (): LibContributions => ({
|
|
185
|
+
externalLibs: [],
|
|
186
|
+
docker: { preRuns: [], postRuns: [] },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/** First occurrence wins, so a step a lib and its app both declare becomes one layer. */
|
|
190
|
+
const dedupeDockerRuns = (runs: DockerRun[]): DockerRun[] => {
|
|
191
|
+
const byKey = new Map<string, DockerRun>();
|
|
192
|
+
for (const run of runs) byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
|
|
193
|
+
return [...byKey.values()];
|
|
194
|
+
};
|
|
195
|
+
|
|
166
196
|
export class AkanAppConfig implements AppConfigResult {
|
|
167
197
|
app: App;
|
|
168
198
|
rootPackageJson: PackageJson;
|
|
169
199
|
docker: DockerConfig;
|
|
200
|
+
/** The Dockerfile `akan build` writes: the declared string verbatim, or one assembled from the parts. */
|
|
201
|
+
dockerfile: string;
|
|
170
202
|
defaultDatabaseMode: DatabaseMode;
|
|
203
|
+
web: AkanWebConfig;
|
|
171
204
|
externalLibs: string[];
|
|
172
205
|
barrelImports: string[];
|
|
173
206
|
optimizeImports: string[];
|
|
@@ -192,10 +225,10 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
192
225
|
app: App,
|
|
193
226
|
libs: string[],
|
|
194
227
|
rootPackageJson: PackageJson,
|
|
195
|
-
config:
|
|
228
|
+
config: AppConfigDeclaration,
|
|
196
229
|
baseDevEnv: BaseDevEnv,
|
|
197
230
|
plugins: AkanPlugin[] = [],
|
|
198
|
-
|
|
231
|
+
libContributions: LibContributions = emptyLibContributions(),
|
|
199
232
|
) {
|
|
200
233
|
this.app = app;
|
|
201
234
|
this.rootPackageJson = rootPackageJson;
|
|
@@ -204,7 +237,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
204
237
|
this.plugins = plugins;
|
|
205
238
|
this.#applyRoutes(config?.routes);
|
|
206
239
|
this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
|
|
207
|
-
this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...
|
|
240
|
+
this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...libContributions.externalLibs])];
|
|
208
241
|
this.barrelImports = [
|
|
209
242
|
...DEFAULT_BARREL_IMPORTS,
|
|
210
243
|
...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
|
|
@@ -221,7 +254,18 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
221
254
|
this.syncPageLibs = (config?.syncPageLibs as string[] | boolean | undefined) ?? false;
|
|
222
255
|
this.hasMobileConfig = Boolean(config.mobile);
|
|
223
256
|
this.mobile = this.#resolveMobileConfig(config.mobile);
|
|
224
|
-
this.
|
|
257
|
+
this.web = this.#resolveWebConfig(config.web);
|
|
258
|
+
this.docker = AkanAppConfig.#resolveDocker(config.docker, libContributions.docker);
|
|
259
|
+
this.dockerfile = this.#makeDockerfile();
|
|
260
|
+
}
|
|
261
|
+
#resolveWebConfig(web: AkanWebOption | undefined): AkanWebConfig {
|
|
262
|
+
const resolved = typeof web === "object" ? { ssr: true, csr: web.csr } : { ssr: web ?? true, csr: web ?? true };
|
|
263
|
+
// `akan build-ios` / `build-android` copy `dist/apps/<app>/csr/<target>.html` into the native project.
|
|
264
|
+
if (!resolved.csr && this.hasMobileConfig)
|
|
265
|
+
throw new Error(
|
|
266
|
+
`apps/${this.app.name}/akan.config.ts turns the CSR bundle off but declares mobile targets; the Capacitor build ships that bundle. Drop the mobile section or leave CSR on.`,
|
|
267
|
+
);
|
|
268
|
+
return resolved;
|
|
225
269
|
}
|
|
226
270
|
#resolveMobileConfig(mobile: DeepPartial<AkanMobileConfig> | undefined): AkanMobileConfig {
|
|
227
271
|
const {
|
|
@@ -322,7 +366,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
322
366
|
this.branches.forEach((domain) => void domains.add(`${basePath}-${domain}.${serveDomain}`));
|
|
323
367
|
});
|
|
324
368
|
}
|
|
325
|
-
#getDockerRunScripts(runs:
|
|
369
|
+
#getDockerRunScripts(runs: DockerRun[]) {
|
|
326
370
|
return runs.map((run) => {
|
|
327
371
|
if (typeof run === "string") return `RUN ${run}`;
|
|
328
372
|
else
|
|
@@ -339,26 +383,31 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
339
383
|
if (typeof image === "string") return `FROM ${image}`;
|
|
340
384
|
else return archs.map((arch) => `FROM ${image[arch] ?? defaultImage} AS ${arch}`).join("\n");
|
|
341
385
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
386
|
+
/** A declared Dockerfile string is verbatim, so a lib's steps are dropped rather than silently unapplied. */
|
|
387
|
+
static #resolveDocker(docker: DockerOption | undefined, libDocker: LibDockerConfig): DockerConfig {
|
|
388
|
+
if (typeof docker === "string") return docker;
|
|
389
|
+
return {
|
|
390
|
+
image: docker?.image ?? DEFAULT_DOCKER_IMAGE,
|
|
391
|
+
preRuns: dedupeDockerRuns([...libDocker.preRuns, ...(docker?.preRuns ?? [])]),
|
|
392
|
+
postRuns: dedupeDockerRuns([...libDocker.postRuns, ...(docker?.postRuns ?? [])]),
|
|
393
|
+
command: docker?.command ?? ["bun", "main.js"],
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
#makeDockerfile(): string {
|
|
397
|
+
if (typeof this.docker === "string") return this.docker;
|
|
398
|
+
const { image, preRuns, postRuns, command } = this.docker;
|
|
399
|
+
const preRunScripts = this.#getDockerRunScripts(preRuns);
|
|
400
|
+
const postRunScripts = this.#getDockerRunScripts(postRuns);
|
|
401
|
+
const imageScript = this.#getDockerImageScript(image, DEFAULT_DOCKER_IMAGE);
|
|
402
|
+
// The image default matches what the build actually produced; a deployment narrows it further with its
|
|
403
|
+
// own env, and can never widen it past the artifacts that are in the image.
|
|
404
|
+
const webEnvLines = [
|
|
405
|
+
...(this.web.ssr ? [] : ["ENV AKAN_SSR=false"]),
|
|
406
|
+
...(this.web.csr ? [] : ["ENV AKAN_CSR=false"]),
|
|
407
|
+
].join("\n");
|
|
408
|
+
return `${imageScript}
|
|
409
|
+
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*
|
|
359
410
|
RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
|
|
360
|
-
RUN apt-get update && apt-get upgrade -y
|
|
361
|
-
RUN apt-get install -y --no-install-recommends git redis build-essential python3 ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils udev ffmpeg
|
|
362
411
|
ARG TARGETARCH
|
|
363
412
|
${preRunScripts.join("\n")}
|
|
364
413
|
RUN mkdir -p /workspace
|
|
@@ -377,15 +426,8 @@ ${this.basePaths.size ? `ENV AKAN_PUBLIC_BASE_PATHS=${[...this.basePaths].join("
|
|
|
377
426
|
ENV AKAN_PUBLIC_DEFAULT_LOCALE=${this.i18n.defaultLocale}
|
|
378
427
|
ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
|
|
379
428
|
ENV AKAN_PUBLIC_OPERATION_MODE=cloud
|
|
380
|
-
|
|
429
|
+
${webEnvLines}
|
|
381
430
|
CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
382
|
-
return {
|
|
383
|
-
content,
|
|
384
|
-
image: imageScript,
|
|
385
|
-
preRuns: docker.preRuns ?? [],
|
|
386
|
-
postRuns: docker.postRuns ?? [],
|
|
387
|
-
command,
|
|
388
|
-
};
|
|
389
431
|
}
|
|
390
432
|
static #importGeneration = 0;
|
|
391
433
|
/**
|
|
@@ -412,24 +454,34 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
412
454
|
app.workspace.getPackageJson(),
|
|
413
455
|
]);
|
|
414
456
|
const resolved = typeof configImp === "function" ? configImp(app) : configImp;
|
|
415
|
-
const { plugins, ...config } = (resolved ?? {}) as
|
|
416
|
-
const
|
|
417
|
-
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [],
|
|
457
|
+
const { plugins, ...config } = (resolved ?? {}) as AppConfigDeclaration & { plugins?: AkanPlugin[] };
|
|
458
|
+
const libContributions = await AkanAppConfig.#collectLibContributions(app, libs, bustImportCache);
|
|
459
|
+
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libContributions);
|
|
418
460
|
}
|
|
419
461
|
//* Every workspace lib is read, not just this app's lib deps: narrowing the set needs the dependency
|
|
420
462
|
//* scan, and the incremental page rebundle re-reads this config on every file change.
|
|
421
|
-
static async #
|
|
463
|
+
static async #collectLibContributions(
|
|
464
|
+
app: App,
|
|
465
|
+
libs: string[],
|
|
466
|
+
bustImportCache: boolean,
|
|
467
|
+
): Promise<LibContributions> {
|
|
422
468
|
const libConfigs = await Promise.all(
|
|
423
469
|
libs.map(async (libName) =>
|
|
424
470
|
LibExecutor.from(app, libName)
|
|
425
471
|
.getConfig({ refresh: bustImportCache })
|
|
426
472
|
.catch((error: unknown) => {
|
|
427
|
-
app.logger.warn(`Skipped libs/${libName}/akan.config.ts
|
|
473
|
+
app.logger.warn(`Skipped libs/${libName}/akan.config.ts contributions: ${String(error)}`);
|
|
428
474
|
return null;
|
|
429
475
|
}),
|
|
430
476
|
),
|
|
431
477
|
);
|
|
432
|
-
return
|
|
478
|
+
return {
|
|
479
|
+
externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
|
|
480
|
+
docker: {
|
|
481
|
+
preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
|
|
482
|
+
postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? []),
|
|
483
|
+
},
|
|
484
|
+
};
|
|
433
485
|
}
|
|
434
486
|
#resolveProductionDependencyVersion(lib: string) {
|
|
435
487
|
const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
|
|
@@ -539,11 +591,13 @@ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfi
|
|
|
539
591
|
export class AkanLibConfig implements LibConfigResult {
|
|
540
592
|
lib: Lib;
|
|
541
593
|
externalLibs: string[];
|
|
594
|
+
docker: LibDockerConfig;
|
|
542
595
|
/** Live-only: plugins declared in this lib's `akan.config.ts` (never serialized). */
|
|
543
596
|
plugins: AkanPlugin[];
|
|
544
597
|
constructor(lib: Lib, config: DeepPartial<LibConfigResult>, plugins: AkanPlugin[] = []) {
|
|
545
598
|
this.lib = lib;
|
|
546
599
|
this.externalLibs = config?.externalLibs ?? [];
|
|
600
|
+
this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
|
|
547
601
|
this.plugins = plugins;
|
|
548
602
|
}
|
|
549
603
|
static async from(lib: Lib, { bustImportCache = false }: { bustImportCache?: boolean } = {}) {
|
package/akanConfig/types.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type {
|
|
|
9
9
|
AkanRouteConfig,
|
|
10
10
|
AkanScanInfo,
|
|
11
11
|
AkanSyncContext,
|
|
12
|
+
AkanWebConfig,
|
|
13
|
+
AkanWebOption,
|
|
12
14
|
AppConfig,
|
|
13
15
|
AppConfigInput,
|
|
14
16
|
AppConfigResult,
|
|
@@ -17,10 +19,14 @@ export type {
|
|
|
17
19
|
DatabaseMode,
|
|
18
20
|
DeepPartial,
|
|
19
21
|
DockerConfig,
|
|
22
|
+
DockerImageConfig,
|
|
23
|
+
DockerOption,
|
|
24
|
+
DockerRun,
|
|
20
25
|
FileConventionScanResult,
|
|
21
26
|
LibConfig,
|
|
22
27
|
LibConfigInput,
|
|
23
28
|
LibConfigResult,
|
|
29
|
+
LibDockerConfig,
|
|
24
30
|
LibScanResult,
|
|
25
31
|
MobileEnv,
|
|
26
32
|
MobilePermission,
|
|
@@ -77,6 +77,7 @@ export class ApplicationBuildRunner {
|
|
|
77
77
|
async build({ spinner = false }: BuildOptions = {}): Promise<BuildResult> {
|
|
78
78
|
// serial build is needed because of Bun.build is unstable for parallel build
|
|
79
79
|
const phaseOptions = { spinner };
|
|
80
|
+
const { web } = await this.#app.getConfig();
|
|
80
81
|
await this.#runPhase("prepare", "Preparing output directory", () => this.#app.prepareCommand("build"), undefined, {
|
|
81
82
|
spinner,
|
|
82
83
|
});
|
|
@@ -91,18 +92,20 @@ export class ApplicationBuildRunner {
|
|
|
91
92
|
await this.#runPhase(
|
|
92
93
|
"ssr",
|
|
93
94
|
"Building SSR route artifacts",
|
|
94
|
-
() => this.#buildSsr(),
|
|
95
|
+
async () => (web.ssr ? await this.#buildSsr() : null),
|
|
95
96
|
(result) =>
|
|
96
97
|
result
|
|
97
98
|
? `${result.allRoutes.manifest.routeIds.length} routes, ${result.allRoutes.manifest.knownEntries.length} entries`
|
|
98
|
-
:
|
|
99
|
+
: web.ssr
|
|
100
|
+
? "skipped"
|
|
101
|
+
: "disabled by akan.config.ts web.ssr",
|
|
99
102
|
phaseOptions,
|
|
100
103
|
);
|
|
101
104
|
await this.#runPhase(
|
|
102
105
|
"csr",
|
|
103
106
|
"Building CSR assets",
|
|
104
|
-
() => this.#buildCsr(),
|
|
105
|
-
(result) => result?.outputDir ?? "skipped",
|
|
107
|
+
async () => (web.csr ? await this.#buildCsr() : null),
|
|
108
|
+
(result) => result?.outputDir ?? (web.csr ? "skipped" : "disabled by akan.config.ts web.csr"),
|
|
106
109
|
phaseOptions,
|
|
107
110
|
);
|
|
108
111
|
await this.#runPhase(
|
|
@@ -165,7 +168,7 @@ export class ApplicationBuildRunner {
|
|
|
165
168
|
const akanConfig = await this.#app.getConfig();
|
|
166
169
|
await Promise.all([
|
|
167
170
|
this.#app.dist.writeJson("package.json", akanConfig.getProductionPackageJson()),
|
|
168
|
-
this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.
|
|
171
|
+
this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.dockerfile),
|
|
169
172
|
]);
|
|
170
173
|
}
|
|
171
174
|
|
|
@@ -174,6 +177,7 @@ export class ApplicationBuildRunner {
|
|
|
174
177
|
const backendExternals = [
|
|
175
178
|
...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]),
|
|
176
179
|
];
|
|
180
|
+
const { web } = akanConfig;
|
|
177
181
|
const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
178
182
|
for (const entrypoint of backendEntryPoints) {
|
|
179
183
|
if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
|
|
@@ -187,17 +191,20 @@ export class ApplicationBuildRunner {
|
|
|
187
191
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
188
192
|
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
189
193
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
// Nothing spawns the RSC worker without SSR, so an api-only image does not carry it.
|
|
195
|
+
const rscWorkerResult = web.ssr
|
|
196
|
+
? await this.#buildOrThrow("rsc-worker", {
|
|
197
|
+
entrypoints: [this.#resolveRscWorkerBuildEntry()],
|
|
198
|
+
outdir: this.#app.dist.cwdPath,
|
|
199
|
+
target: "bun",
|
|
200
|
+
minify: true,
|
|
201
|
+
naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
|
|
202
|
+
conditions: ["react-server"],
|
|
203
|
+
// `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
|
|
204
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
205
|
+
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
206
|
+
})
|
|
207
|
+
: null;
|
|
201
208
|
const consoleRuntimeResult = await this.#buildOrThrow("console-runtime", {
|
|
202
209
|
entrypoints: [this.#resolveConsoleRuntimeBuildEntry()],
|
|
203
210
|
outdir: this.#app.dist.cwdPath,
|
|
@@ -208,8 +215,9 @@ export class ApplicationBuildRunner {
|
|
|
208
215
|
});
|
|
209
216
|
await this.#writeConsoleShim();
|
|
210
217
|
return {
|
|
211
|
-
entrypoints: backendEntryPoints.length + 2,
|
|
212
|
-
outputs:
|
|
218
|
+
entrypoints: backendEntryPoints.length + (rscWorkerResult ? 2 : 1),
|
|
219
|
+
outputs:
|
|
220
|
+
backendResult.outputs.length + (rscWorkerResult?.outputs.length ?? 0) + consoleRuntimeResult.outputs.length + 1,
|
|
213
221
|
};
|
|
214
222
|
}
|
|
215
223
|
|
package/executors.ts
CHANGED
|
@@ -1371,11 +1371,20 @@ export class AppExecutor extends SysExecutor {
|
|
|
1371
1371
|
await Promise.all([this.dist.mkdir("private"), this.dist.mkdir("public")]);
|
|
1372
1372
|
//* Lib assets are symlinks in the app dir (see syncAssets). dist is the docker build context and the
|
|
1373
1373
|
//* release tarball root, neither of which follows a link out of itself, so materialize them here.
|
|
1374
|
+
//* `public/` has exactly one reader — the web router's catch-all — so an api-only build leaves it out.
|
|
1374
1375
|
await Promise.all([
|
|
1375
1376
|
this.cp("private", `${this.dist.cwdPath}/private`, { dereference: true }),
|
|
1376
|
-
this.cp("public", `${this.dist.cwdPath}/public`, { dereference: true }),
|
|
1377
|
+
...(akanConfig.web.ssr ? [this.cp("public", `${this.dist.cwdPath}/public`, { dereference: true })] : []),
|
|
1377
1378
|
]);
|
|
1378
|
-
} else
|
|
1379
|
+
} else {
|
|
1380
|
+
await this.removeDir(".akan");
|
|
1381
|
+
//? `web` is a build declaration: `akan start` keeps the full dev surface because the incremental
|
|
1382
|
+
//? builder is also the file watcher, so switching it off would take server-code HMR with it.
|
|
1383
|
+
if (!akanConfig.web.ssr || !akanConfig.web.csr)
|
|
1384
|
+
this.logger.warn(
|
|
1385
|
+
`akan.config.ts disables web.${!akanConfig.web.ssr ? "ssr" : "csr"}; \`akan start\` still serves it, \`akan build\` will not`,
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1379
1388
|
//? `akan start` is the dev server, and it must say so rather than inherit an answer. Bun auto-loads the
|
|
1380
1389
|
//? workspace `.env`, so NODE_ENV=production can reach this process without ever being exported in a shell —
|
|
1381
1390
|
//? and a dev server that believes it is production serves from the production route cache, whose every
|
|
@@ -77,6 +77,7 @@ export class SsrBaseArtifactBuilder {
|
|
|
77
77
|
branches: [...akanConfig.branches],
|
|
78
78
|
i18n: akanConfig.i18n,
|
|
79
79
|
imageConfig: akanConfig.images,
|
|
80
|
+
web: akanConfig.web,
|
|
80
81
|
deepLinkAssociations: Object.values(akanConfig.mobile.targets)
|
|
81
82
|
.filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0)
|
|
82
83
|
.map((target) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.66",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@langchain/openai": "^1.4.6",
|
|
46
46
|
"@tailwindcss/node": "^4.3.0",
|
|
47
47
|
"@trapezedev/project": "^7.1.4",
|
|
48
|
-
"akanjs": "3.0.0-alpha.
|
|
48
|
+
"akanjs": "3.0.0-alpha.66",
|
|
49
49
|
"chalk": "^5.6.2",
|
|
50
50
|
"commander": "^14.0.3",
|
|
51
51
|
"dayjs": "^1.11.20",
|
package/scanInfo.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
PkgScanResult,
|
|
10
10
|
ScanResult,
|
|
11
11
|
} from "./akanConfig";
|
|
12
|
+
import { AkanAppConfig } from "./akanConfig";
|
|
12
13
|
import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
13
14
|
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
14
15
|
|
|
@@ -285,12 +286,11 @@ class ScanInfo {
|
|
|
285
286
|
}),
|
|
286
287
|
]);
|
|
287
288
|
const routes = exec.type === "lib" ? [] : await (exec as AppExecutor).getPageKeys();
|
|
288
|
-
const
|
|
289
|
+
const common = {
|
|
289
290
|
name: exec.name,
|
|
290
291
|
type: exec.type,
|
|
291
292
|
repoName: exec.workspace.repoName,
|
|
292
293
|
serveDomain: WorkspaceExecutor.getBaseDevEnv(path.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
|
|
293
|
-
akanConfig,
|
|
294
294
|
files,
|
|
295
295
|
libDeps,
|
|
296
296
|
pkgDeps,
|
|
@@ -298,6 +298,11 @@ class ScanInfo {
|
|
|
298
298
|
devDependencies: npmDevDeps.filter((dep) => !isAkanFrameworkDependency(dep)),
|
|
299
299
|
routes,
|
|
300
300
|
};
|
|
301
|
+
//? `exec.type` and the resolved config class are correlated, which the union alone cannot express.
|
|
302
|
+
const scanResult: AppScanResult | LibScanResult =
|
|
303
|
+
akanConfig instanceof AkanAppConfig
|
|
304
|
+
? ({ ...common, akanConfig } satisfies AppScanResult)
|
|
305
|
+
: ({ ...common, akanConfig } satisfies LibScanResult);
|
|
301
306
|
return scanResult;
|
|
302
307
|
}
|
|
303
308
|
|
|
@@ -462,7 +467,7 @@ export class LibInfo extends ScanInfo {
|
|
|
462
467
|
LibInfo.libInfos.set(libName, await LibInfo.fromExecutor(libExecutor));
|
|
463
468
|
}),
|
|
464
469
|
);
|
|
465
|
-
const libInfo = new LibInfo(exec, scanResult);
|
|
470
|
+
const libInfo = new LibInfo(exec, scanResult as LibScanResult);
|
|
466
471
|
LibInfo.libInfos.set(exec.name, libInfo);
|
|
467
472
|
LibInfo.#sortedLibIndices = null;
|
|
468
473
|
return libInfo;
|