@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/akanConfig/akanConfig.ts
CHANGED
|
@@ -5,19 +5,26 @@ import type { AkanPlugin } from "akanjs";
|
|
|
5
5
|
import { type AkanI18nConfig, resolveAkanI18nConfig } from "akanjs/common";
|
|
6
6
|
import type { AkanImageConfig } from "akanjs/server";
|
|
7
7
|
import type { App, Lib } from "../commandDecorators";
|
|
8
|
-
import { WorkspaceExecutor } from "../executors";
|
|
8
|
+
import { LibExecutor, WorkspaceExecutor } from "../executors";
|
|
9
9
|
import type { BaseDevEnv, PackageJson } from "../types";
|
|
10
10
|
import {
|
|
11
|
+
type AkanAssetsConfig,
|
|
11
12
|
type AkanMobileConfig,
|
|
12
13
|
type AkanMobileTargetConfig,
|
|
13
14
|
type AkanRouteConfig,
|
|
15
|
+
type AkanWebConfig,
|
|
16
|
+
type AkanWebOption,
|
|
14
17
|
type AppConfigResult,
|
|
15
18
|
type Arch,
|
|
16
19
|
archs,
|
|
17
20
|
type DatabaseMode,
|
|
18
21
|
type DeepPartial,
|
|
19
22
|
type DockerConfig,
|
|
23
|
+
type DockerOption,
|
|
24
|
+
type DockerRun,
|
|
25
|
+
type LibAssetsConfig,
|
|
20
26
|
type LibConfigResult,
|
|
27
|
+
type LibDockerConfig,
|
|
21
28
|
} from "./types";
|
|
22
29
|
|
|
23
30
|
const DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
|
|
@@ -49,9 +56,8 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
|
|
|
49
56
|
"react-icons/*",
|
|
50
57
|
];
|
|
51
58
|
const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
|
|
59
|
+
const DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
|
|
52
60
|
const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
|
|
53
|
-
const NATIVE_RUNTIME_PACKAGES = ["sharp"] as const;
|
|
54
|
-
const DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"] as const;
|
|
55
61
|
// The firebase client (push tokens) and the Capacitor toolchain — `@capacitor/cli` (`npx cap`),
|
|
56
62
|
// `@capacitor/assets` (`npx @capacitor/assets`) plus the `@capacitor/core`/`ios`/`android` runtime
|
|
57
63
|
// and native-platform packages that `npx cap add`/`sync` resolve from the workspace node_modules.
|
|
@@ -92,8 +98,6 @@ const DATABASE_MODE_RUNTIME_PACKAGES = {
|
|
|
92
98
|
} satisfies Record<DatabaseMode, readonly string[]>;
|
|
93
99
|
const AKAN_RUNTIME_PACKAGES = new Set<string>([
|
|
94
100
|
...SSR_RUNTIME_PACKAGES,
|
|
95
|
-
...NATIVE_RUNTIME_PACKAGES,
|
|
96
|
-
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
97
101
|
...MOBILE_RUNTIME_PACKAGES,
|
|
98
102
|
...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
|
|
99
103
|
]);
|
|
@@ -109,6 +113,7 @@ const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
|
|
|
109
113
|
maximumRedirects: 3,
|
|
110
114
|
fetchTimeoutMs: 7000,
|
|
111
115
|
maxRemoteBytes: 25 * 1024 * 1024,
|
|
116
|
+
maxConcurrency: 0,
|
|
112
117
|
};
|
|
113
118
|
|
|
114
119
|
const normalizeIndexPath = (indexPath: string | undefined): string | undefined => {
|
|
@@ -167,11 +172,45 @@ const normalizeDeepLinks = (deepLinks: DeepPartial<AkanMobileTargetConfig["deepL
|
|
|
167
172
|
} satisfies AkanMobileTargetConfig["deepLinks"];
|
|
168
173
|
};
|
|
169
174
|
|
|
175
|
+
/** What `akan.config.ts` may write: the resolved shape made partial, with `docker` and `web` in their unions. */
|
|
176
|
+
type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web"> & {
|
|
177
|
+
docker?: DockerOption;
|
|
178
|
+
web?: AkanWebOption;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** What the workspace's libs add to an app's build, read off each `libs/<lib>/akan.config.ts`. */
|
|
182
|
+
export interface LibContributions {
|
|
183
|
+
externalLibs: string[];
|
|
184
|
+
docker: LibDockerConfig;
|
|
185
|
+
/** Keep globs rewritten to the app's own `public/`, where `akan sync` mounts each lib's assets. */
|
|
186
|
+
keepFonts?: string[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const emptyLibContributions = (): LibContributions => ({
|
|
190
|
+
externalLibs: [],
|
|
191
|
+
docker: { preRuns: [], postRuns: [] },
|
|
192
|
+
keepFonts: [],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const normalizeKeepFonts = (keepFonts: string[] | undefined) => [
|
|
196
|
+
...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean)),
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
/** First occurrence wins, so a step a lib and its app both declare becomes one layer. */
|
|
200
|
+
const dedupeDockerRuns = (runs: DockerRun[]): DockerRun[] => {
|
|
201
|
+
const byKey = new Map<string, DockerRun>();
|
|
202
|
+
for (const run of runs) byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
|
|
203
|
+
return [...byKey.values()];
|
|
204
|
+
};
|
|
205
|
+
|
|
170
206
|
export class AkanAppConfig implements AppConfigResult {
|
|
171
207
|
app: App;
|
|
172
208
|
rootPackageJson: PackageJson;
|
|
173
209
|
docker: DockerConfig;
|
|
210
|
+
/** The Dockerfile `akan build` writes: the declared string verbatim, or one assembled from the parts. */
|
|
211
|
+
dockerfile: string;
|
|
174
212
|
defaultDatabaseMode: DatabaseMode;
|
|
213
|
+
web: AkanWebConfig;
|
|
175
214
|
externalLibs: string[];
|
|
176
215
|
barrelImports: string[];
|
|
177
216
|
optimizeImports: string[];
|
|
@@ -182,6 +221,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
182
221
|
/** True only when the app's akan.config.ts explicitly declares a `mobile` section (vs. the synthesized default). */
|
|
183
222
|
hasMobileConfig: boolean;
|
|
184
223
|
secrets: string[];
|
|
224
|
+
assets: AkanAssetsConfig;
|
|
185
225
|
/** Raw setting; resolved against the app's lib deps at sync time (see `AppExecutor.syncPages`). */
|
|
186
226
|
syncPageLibs: string[] | boolean;
|
|
187
227
|
baseDevEnv: BaseDevEnv;
|
|
@@ -196,9 +236,10 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
196
236
|
app: App,
|
|
197
237
|
libs: string[],
|
|
198
238
|
rootPackageJson: PackageJson,
|
|
199
|
-
config:
|
|
239
|
+
config: AppConfigDeclaration,
|
|
200
240
|
baseDevEnv: BaseDevEnv,
|
|
201
241
|
plugins: AkanPlugin[] = [],
|
|
242
|
+
libContributions: LibContributions = emptyLibContributions(),
|
|
202
243
|
) {
|
|
203
244
|
this.app = app;
|
|
204
245
|
this.rootPackageJson = rootPackageJson;
|
|
@@ -207,7 +248,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
207
248
|
this.plugins = plugins;
|
|
208
249
|
this.#applyRoutes(config?.routes);
|
|
209
250
|
this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
|
|
210
|
-
this.externalLibs = config?.externalLibs ?? [];
|
|
251
|
+
this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...libContributions.externalLibs])];
|
|
211
252
|
this.barrelImports = [
|
|
212
253
|
...DEFAULT_BARREL_IMPORTS,
|
|
213
254
|
...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
|
|
@@ -221,10 +262,28 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
221
262
|
process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
|
|
222
263
|
this.publicEnv = (config?.publicEnv as string[] | undefined) ?? ([] as string[]);
|
|
223
264
|
this.secrets = (config?.secrets as string[] | undefined) ?? ([] as string[]);
|
|
265
|
+
this.assets = {
|
|
266
|
+
pruneFonts: config?.assets?.pruneFonts ?? true,
|
|
267
|
+
keepFonts: [
|
|
268
|
+
...normalizeKeepFonts(config?.assets?.keepFonts as string[] | undefined),
|
|
269
|
+
...(libContributions.keepFonts ?? []),
|
|
270
|
+
],
|
|
271
|
+
};
|
|
224
272
|
this.syncPageLibs = (config?.syncPageLibs as string[] | boolean | undefined) ?? false;
|
|
225
273
|
this.hasMobileConfig = Boolean(config.mobile);
|
|
226
274
|
this.mobile = this.#resolveMobileConfig(config.mobile);
|
|
227
|
-
this.
|
|
275
|
+
this.web = this.#resolveWebConfig(config.web);
|
|
276
|
+
this.docker = AkanAppConfig.#resolveDocker(config.docker, libContributions.docker);
|
|
277
|
+
this.dockerfile = this.#makeDockerfile();
|
|
278
|
+
}
|
|
279
|
+
#resolveWebConfig(web: AkanWebOption | undefined): AkanWebConfig {
|
|
280
|
+
const resolved = typeof web === "object" ? { ssr: true, csr: web.csr } : { ssr: web ?? true, csr: web ?? true };
|
|
281
|
+
// `akan build-ios` / `build-android` copy `dist/apps/<app>/csr/<target>.html` into the native project.
|
|
282
|
+
if (!resolved.csr && this.hasMobileConfig)
|
|
283
|
+
throw new Error(
|
|
284
|
+
`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.`,
|
|
285
|
+
);
|
|
286
|
+
return resolved;
|
|
228
287
|
}
|
|
229
288
|
#resolveMobileConfig(mobile: DeepPartial<AkanMobileConfig> | undefined): AkanMobileConfig {
|
|
230
289
|
const {
|
|
@@ -325,7 +384,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
325
384
|
this.branches.forEach((domain) => void domains.add(`${basePath}-${domain}.${serveDomain}`));
|
|
326
385
|
});
|
|
327
386
|
}
|
|
328
|
-
#getDockerRunScripts(runs:
|
|
387
|
+
#getDockerRunScripts(runs: DockerRun[]) {
|
|
329
388
|
return runs.map((run) => {
|
|
330
389
|
if (typeof run === "string") return `RUN ${run}`;
|
|
331
390
|
else
|
|
@@ -342,26 +401,31 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
342
401
|
if (typeof image === "string") return `FROM ${image}`;
|
|
343
402
|
else return archs.map((arch) => `FROM ${image[arch] ?? defaultImage} AS ${arch}`).join("\n");
|
|
344
403
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
404
|
+
/** A declared Dockerfile string is verbatim, so a lib's steps are dropped rather than silently unapplied. */
|
|
405
|
+
static #resolveDocker(docker: DockerOption | undefined, libDocker: LibDockerConfig): DockerConfig {
|
|
406
|
+
if (typeof docker === "string") return docker;
|
|
407
|
+
return {
|
|
408
|
+
image: docker?.image ?? DEFAULT_DOCKER_IMAGE,
|
|
409
|
+
preRuns: dedupeDockerRuns([...libDocker.preRuns, ...(docker?.preRuns ?? [])]),
|
|
410
|
+
postRuns: dedupeDockerRuns([...libDocker.postRuns, ...(docker?.postRuns ?? [])]),
|
|
411
|
+
command: docker?.command ?? ["bun", "main.js"],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
#makeDockerfile(): string {
|
|
415
|
+
if (typeof this.docker === "string") return this.docker;
|
|
416
|
+
const { image, preRuns, postRuns, command } = this.docker;
|
|
417
|
+
const preRunScripts = this.#getDockerRunScripts(preRuns);
|
|
418
|
+
const postRunScripts = this.#getDockerRunScripts(postRuns);
|
|
419
|
+
const imageScript = this.#getDockerImageScript(image, DEFAULT_DOCKER_IMAGE);
|
|
420
|
+
// The image default matches what the build actually produced; a deployment narrows it further with its
|
|
421
|
+
// own env, and can never widen it past the artifacts that are in the image.
|
|
422
|
+
const webEnvLines = [
|
|
423
|
+
...(this.web.ssr ? [] : ["ENV AKAN_SSR=false"]),
|
|
424
|
+
...(this.web.csr ? [] : ["ENV AKAN_CSR=false"]),
|
|
425
|
+
].join("\n");
|
|
426
|
+
return `${imageScript}
|
|
427
|
+
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*
|
|
362
428
|
RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
|
|
363
|
-
RUN apt-get update && apt-get upgrade -y
|
|
364
|
-
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
|
|
365
429
|
ARG TARGETARCH
|
|
366
430
|
${preRunScripts.join("\n")}
|
|
367
431
|
RUN mkdir -p /workspace
|
|
@@ -380,15 +444,8 @@ ${this.basePaths.size ? `ENV AKAN_PUBLIC_BASE_PATHS=${[...this.basePaths].join("
|
|
|
380
444
|
ENV AKAN_PUBLIC_DEFAULT_LOCALE=${this.i18n.defaultLocale}
|
|
381
445
|
ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
|
|
382
446
|
ENV AKAN_PUBLIC_OPERATION_MODE=cloud
|
|
383
|
-
|
|
447
|
+
${webEnvLines}
|
|
384
448
|
CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
385
|
-
return {
|
|
386
|
-
content,
|
|
387
|
-
image: imageScript,
|
|
388
|
-
preRuns: docker.preRuns ?? [],
|
|
389
|
-
postRuns: docker.postRuns ?? [],
|
|
390
|
-
command,
|
|
391
|
-
};
|
|
392
449
|
}
|
|
393
450
|
static #importGeneration = 0;
|
|
394
451
|
/**
|
|
@@ -415,8 +472,34 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
415
472
|
app.workspace.getPackageJson(),
|
|
416
473
|
]);
|
|
417
474
|
const resolved = typeof configImp === "function" ? configImp(app) : configImp;
|
|
418
|
-
const { plugins, ...config } = (resolved ?? {}) as
|
|
419
|
-
|
|
475
|
+
const { plugins, ...config } = (resolved ?? {}) as AppConfigDeclaration & { plugins?: AkanPlugin[] };
|
|
476
|
+
const libContributions = await AkanAppConfig.#collectLibContributions(app, libs, bustImportCache);
|
|
477
|
+
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libContributions);
|
|
478
|
+
}
|
|
479
|
+
//* Every workspace lib is read, not just this app's lib deps: narrowing the set needs the dependency
|
|
480
|
+
//* scan, and the incremental page rebundle re-reads this config on every file change.
|
|
481
|
+
static async #collectLibContributions(app: App, libs: string[], bustImportCache: boolean): Promise<LibContributions> {
|
|
482
|
+
const libConfigs = await Promise.all(
|
|
483
|
+
libs.map(async (libName) =>
|
|
484
|
+
LibExecutor.from(app, libName)
|
|
485
|
+
.getConfig({ refresh: bustImportCache })
|
|
486
|
+
.catch((error: unknown) => {
|
|
487
|
+
app.logger.warn(`Skipped libs/${libName}/akan.config.ts contributions: ${String(error)}`);
|
|
488
|
+
return null;
|
|
489
|
+
}),
|
|
490
|
+
),
|
|
491
|
+
);
|
|
492
|
+
return {
|
|
493
|
+
externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
|
|
494
|
+
docker: {
|
|
495
|
+
preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
|
|
496
|
+
postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? []),
|
|
497
|
+
},
|
|
498
|
+
//* A lib writes the glob against its own `public/`; `akan sync` mounts that at `public/libs/<lib>`.
|
|
499
|
+
keepFonts: libConfigs.flatMap((libConfig) =>
|
|
500
|
+
(libConfig?.assets.keepFonts ?? []).map((glob) => `libs/${libConfig?.lib.name}/${glob}`),
|
|
501
|
+
),
|
|
502
|
+
};
|
|
420
503
|
}
|
|
421
504
|
#resolveProductionDependencyVersion(lib: string) {
|
|
422
505
|
const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
|
|
@@ -428,13 +511,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
428
511
|
return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
429
512
|
}
|
|
430
513
|
#getProductionRuntimePackages() {
|
|
431
|
-
return [
|
|
432
|
-
...this.externalLibs,
|
|
433
|
-
...SSR_RUNTIME_PACKAGES,
|
|
434
|
-
...NATIVE_RUNTIME_PACKAGES,
|
|
435
|
-
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
436
|
-
...this.getDatabaseModeRuntimePackages(),
|
|
437
|
-
];
|
|
514
|
+
return [...this.externalLibs, ...SSR_RUNTIME_PACKAGES, ...this.getDatabaseModeRuntimePackages()];
|
|
438
515
|
}
|
|
439
516
|
getDatabaseModeRuntimePackages(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
|
|
440
517
|
return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
|
|
@@ -532,15 +609,19 @@ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfi
|
|
|
532
609
|
export class AkanLibConfig implements LibConfigResult {
|
|
533
610
|
lib: Lib;
|
|
534
611
|
externalLibs: string[];
|
|
612
|
+
docker: LibDockerConfig;
|
|
613
|
+
assets: LibAssetsConfig;
|
|
535
614
|
/** Live-only: plugins declared in this lib's `akan.config.ts` (never serialized). */
|
|
536
615
|
plugins: AkanPlugin[];
|
|
537
616
|
constructor(lib: Lib, config: DeepPartial<LibConfigResult>, plugins: AkanPlugin[] = []) {
|
|
538
617
|
this.lib = lib;
|
|
539
618
|
this.externalLibs = config?.externalLibs ?? [];
|
|
619
|
+
this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
|
|
620
|
+
this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts as string[] | undefined) };
|
|
540
621
|
this.plugins = plugins;
|
|
541
622
|
}
|
|
542
|
-
static async from(lib: Lib) {
|
|
543
|
-
const
|
|
623
|
+
static async from(lib: Lib, { bustImportCache = false }: { bustImportCache?: boolean } = {}) {
|
|
624
|
+
const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
|
|
544
625
|
const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
|
|
545
626
|
const { plugins, ...config } = (resolved ?? {}) as DeepPartial<LibConfigResult> & { plugins?: AkanPlugin[] };
|
|
546
627
|
return new AkanLibConfig(lib, config, plugins ?? []);
|
package/akanConfig/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type {
|
|
2
|
+
AkanAssetsConfig,
|
|
2
3
|
AkanConfigFile,
|
|
3
4
|
AkanExecutor,
|
|
4
5
|
AkanMobileConfig,
|
|
@@ -9,6 +10,8 @@ export type {
|
|
|
9
10
|
AkanRouteConfig,
|
|
10
11
|
AkanScanInfo,
|
|
11
12
|
AkanSyncContext,
|
|
13
|
+
AkanWebConfig,
|
|
14
|
+
AkanWebOption,
|
|
12
15
|
AppConfig,
|
|
13
16
|
AppConfigInput,
|
|
14
17
|
AppConfigResult,
|
|
@@ -17,10 +20,15 @@ export type {
|
|
|
17
20
|
DatabaseMode,
|
|
18
21
|
DeepPartial,
|
|
19
22
|
DockerConfig,
|
|
23
|
+
DockerImageConfig,
|
|
24
|
+
DockerOption,
|
|
25
|
+
DockerRun,
|
|
20
26
|
FileConventionScanResult,
|
|
27
|
+
LibAssetsConfig,
|
|
21
28
|
LibConfig,
|
|
22
29
|
LibConfigInput,
|
|
23
30
|
LibConfigResult,
|
|
31
|
+
LibDockerConfig,
|
|
24
32
|
LibScanResult,
|
|
25
33
|
MobileEnv,
|
|
26
34
|
MobilePermission,
|
package/akanContext.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { capitalize } from "akanjs/common";
|
|
4
|
+
import { extractBlockVersion, readDevkitVersion } from "./agentsIndex";
|
|
4
5
|
import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
|
|
5
6
|
import { FileSys } from "./fileSys";
|
|
6
7
|
import { collectRecipeSources, findInlineRecipeDuplicates, scanRecipes } from "./recipeScanner";
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
workflowRunArtifactPath,
|
|
15
16
|
workflowSyncDir,
|
|
16
17
|
} from "./workflow";
|
|
17
|
-
import {
|
|
18
|
+
import { isScannedRootEntry, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
18
19
|
|
|
19
20
|
export type AkanContextFormat = "json" | "markdown";
|
|
20
21
|
export type AkanModuleKind = "domain" | "service" | "scalar";
|
|
@@ -276,6 +277,8 @@ const validationCommands = [
|
|
|
276
277
|
"akan test <app-or-lib-or-pkg>",
|
|
277
278
|
"akan build <app-name>",
|
|
278
279
|
"akan doctor --strict --format json",
|
|
280
|
+
"akan quality scan [--format json]",
|
|
281
|
+
"akan quality ssr [--format json]",
|
|
279
282
|
];
|
|
280
283
|
|
|
281
284
|
const unknownGeneratedFilesFreshness: GeneratedFilesFreshness = {
|
|
@@ -618,6 +621,30 @@ export class AkanContextAnalyzer {
|
|
|
618
621
|
};
|
|
619
622
|
}
|
|
620
623
|
|
|
624
|
+
// The conventions in AGENTS.md ship with the package, but nothing re-renders them on `bun update` — a workspace
|
|
625
|
+
// keeps whichever release wrote its block until someone re-runs the install. Comparing the stamp against the
|
|
626
|
+
// running devkit is the only signal that the guide an agent is reading is older than the framework it describes.
|
|
627
|
+
static async #agentGuideDrift(workspace: WorkspaceExecutor) {
|
|
628
|
+
const installed = await readDevkitVersion();
|
|
629
|
+
if (!installed) return null;
|
|
630
|
+
const content = await safeReadText(path.join(workspace.workspaceRoot, "AGENTS.md"));
|
|
631
|
+
if (content === null) return null;
|
|
632
|
+
const stamped = extractBlockVersion(content);
|
|
633
|
+
if (stamped === installed) return null;
|
|
634
|
+
const hint = "Re-render the AGENTS.md managed block from the installed framework release.";
|
|
635
|
+
if (!stamped)
|
|
636
|
+
return {
|
|
637
|
+
code: "agent-guide-unstamped",
|
|
638
|
+
message: `AGENTS.md carries no generated-version stamp, so its conventions may predate @akanjs/devkit ${installed}`,
|
|
639
|
+
hint,
|
|
640
|
+
};
|
|
641
|
+
return {
|
|
642
|
+
code: "agent-guide-stale",
|
|
643
|
+
message: `AGENTS.md was generated by @akanjs/devkit ${stamped}, but ${installed} is installed`,
|
|
644
|
+
hint,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
621
648
|
static async doctor(
|
|
622
649
|
workspace: WorkspaceExecutor,
|
|
623
650
|
{
|
|
@@ -639,23 +666,25 @@ export class AkanContextAnalyzer {
|
|
|
639
666
|
),
|
|
640
667
|
];
|
|
641
668
|
|
|
642
|
-
for (const
|
|
643
|
-
const
|
|
644
|
-
for (const entry of await safeReadDir(
|
|
645
|
-
if (!
|
|
646
|
-
const allowed = entry.isDirectory()
|
|
669
|
+
for (const sys of [...context.apps, ...context.libs]) {
|
|
670
|
+
const sysPath = path.join(workspace.workspaceRoot, sys.path);
|
|
671
|
+
for (const entry of await safeReadDir(sysPath)) {
|
|
672
|
+
if (!isScannedRootEntry(sys.type, entry.name)) continue;
|
|
673
|
+
const allowed = entry.isDirectory()
|
|
674
|
+
? rootAllowedDirs[sys.type].has(entry.name)
|
|
675
|
+
: rootAllowedFiles[sys.type].has(entry.name);
|
|
647
676
|
if (!allowed) {
|
|
648
677
|
const action = repairAction(
|
|
649
678
|
"module-shape",
|
|
650
|
-
`akan repair module-shape --app ${
|
|
651
|
-
|
|
679
|
+
`akan repair module-shape --app ${sys.name}`,
|
|
680
|
+
`Review ${sys.type} root shape and remove or move the unknown entry.`,
|
|
652
681
|
false,
|
|
653
682
|
);
|
|
654
683
|
diagnostics.push({
|
|
655
684
|
severity: "error",
|
|
656
|
-
code:
|
|
657
|
-
path: `${
|
|
658
|
-
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in
|
|
685
|
+
code: `${sys.type}-root-unknown-entry`,
|
|
686
|
+
path: `${sys.path}/${entry.name}`,
|
|
687
|
+
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in ${sys.type} root: ${sys.path}/${entry.name}`,
|
|
659
688
|
repairActions: [action],
|
|
660
689
|
});
|
|
661
690
|
repairActions.push(action);
|
|
@@ -663,6 +692,19 @@ export class AkanContextAnalyzer {
|
|
|
663
692
|
}
|
|
664
693
|
}
|
|
665
694
|
|
|
695
|
+
const agentDrift = await AkanContextAnalyzer.#agentGuideDrift(workspace);
|
|
696
|
+
if (agentDrift) {
|
|
697
|
+
const action = repairAction("generated", "akan agent install agents-md", agentDrift.hint, true);
|
|
698
|
+
diagnostics.push({
|
|
699
|
+
severity: "warning",
|
|
700
|
+
code: agentDrift.code,
|
|
701
|
+
path: "AGENTS.md",
|
|
702
|
+
message: agentDrift.message,
|
|
703
|
+
repairActions: [action],
|
|
704
|
+
});
|
|
705
|
+
repairActions.push(action);
|
|
706
|
+
}
|
|
707
|
+
|
|
666
708
|
for (const sys of [...context.apps, ...context.libs]) {
|
|
667
709
|
for (const module of sys.modules) {
|
|
668
710
|
if (!module.abstract.exists) {
|
|
@@ -4,7 +4,7 @@ import { AKAN_OPTIONAL_BACKEND_EXTERNALS } from "./applicationBuildRunner";
|
|
|
4
4
|
describe("ApplicationBuildRunner", () => {
|
|
5
5
|
test("externalizes Akan optional backend dependencies", () => {
|
|
6
6
|
expect(AKAN_OPTIONAL_BACKEND_EXTERNALS).toEqual(
|
|
7
|
-
expect.arrayContaining(["@libsql/client", "bullmq", "
|
|
7
|
+
expect.arrayContaining(["@libsql/client", "bullmq", "ioredis", "postgres", "protobufjs"]),
|
|
8
8
|
);
|
|
9
9
|
});
|
|
10
10
|
});
|
|
@@ -7,7 +7,13 @@ import type {
|
|
|
7
7
|
ApplicationBuildResult,
|
|
8
8
|
} from "./applicationBuildReporter";
|
|
9
9
|
import type { App } from "./commandDecorators";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
AllRoutesBuilder,
|
|
12
|
+
CsrArtifactBuilder,
|
|
13
|
+
FontPruner,
|
|
14
|
+
precompressArtifacts,
|
|
15
|
+
SsrBaseArtifactBuilder,
|
|
16
|
+
} from "./frontendBuild";
|
|
11
17
|
import { Spinner } from "./spinner";
|
|
12
18
|
|
|
13
19
|
export interface TypecheckOptions {
|
|
@@ -15,7 +21,7 @@ export interface TypecheckOptions {
|
|
|
15
21
|
incremental?: boolean;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
export type BuildPhaseId = "prepare" | "typecheck" | "backend" | "ssr" | "csr" | "compress" | "metadata";
|
|
24
|
+
export type BuildPhaseId = "prepare" | "typecheck" | "backend" | "ssr" | "csr" | "assets" | "compress" | "metadata";
|
|
19
25
|
|
|
20
26
|
export type BuildPhaseResult = ApplicationBuildPhaseResult & { id: BuildPhaseId };
|
|
21
27
|
export type BuildResult = ApplicationBuildResult;
|
|
@@ -37,6 +43,7 @@ const BUILD_PHASE_EMOJIS: Record<BuildPhaseId, string> = {
|
|
|
37
43
|
backend: "📦",
|
|
38
44
|
ssr: "🧭",
|
|
39
45
|
csr: "🎨",
|
|
46
|
+
assets: "✂️",
|
|
40
47
|
compress: "🗜️",
|
|
41
48
|
metadata: "📝",
|
|
42
49
|
};
|
|
@@ -56,7 +63,6 @@ const SSR_RENDER_EXTERNALS = [
|
|
|
56
63
|
export const AKAN_OPTIONAL_BACKEND_EXTERNALS = [
|
|
57
64
|
"@libsql/client",
|
|
58
65
|
"bullmq",
|
|
59
|
-
"croner",
|
|
60
66
|
"ioredis",
|
|
61
67
|
"postgres",
|
|
62
68
|
"protobufjs",
|
|
@@ -78,6 +84,7 @@ export class ApplicationBuildRunner {
|
|
|
78
84
|
async build({ spinner = false }: BuildOptions = {}): Promise<BuildResult> {
|
|
79
85
|
// serial build is needed because of Bun.build is unstable for parallel build
|
|
80
86
|
const phaseOptions = { spinner };
|
|
87
|
+
const { web, assets } = await this.#app.getConfig();
|
|
81
88
|
await this.#runPhase("prepare", "Preparing output directory", () => this.#app.prepareCommand("build"), undefined, {
|
|
82
89
|
spinner,
|
|
83
90
|
});
|
|
@@ -92,18 +99,30 @@ export class ApplicationBuildRunner {
|
|
|
92
99
|
await this.#runPhase(
|
|
93
100
|
"ssr",
|
|
94
101
|
"Building SSR route artifacts",
|
|
95
|
-
() => this.#buildSsr(),
|
|
102
|
+
async () => (web.ssr ? await this.#buildSsr() : null),
|
|
96
103
|
(result) =>
|
|
97
104
|
result
|
|
98
105
|
? `${result.allRoutes.manifest.routeIds.length} routes, ${result.allRoutes.manifest.knownEntries.length} entries`
|
|
99
|
-
:
|
|
106
|
+
: web.ssr
|
|
107
|
+
? "skipped"
|
|
108
|
+
: "disabled by akan.config.ts web.ssr",
|
|
100
109
|
phaseOptions,
|
|
101
110
|
);
|
|
102
111
|
await this.#runPhase(
|
|
103
112
|
"csr",
|
|
104
113
|
"Building CSR assets",
|
|
105
|
-
() => this.#buildCsr(),
|
|
106
|
-
(result) => result?.outputDir ?? "skipped",
|
|
114
|
+
async () => (web.csr ? await this.#buildCsr() : null),
|
|
115
|
+
(result) => result?.outputDir ?? (web.csr ? "skipped" : "disabled by akan.config.ts web.csr"),
|
|
116
|
+
phaseOptions,
|
|
117
|
+
);
|
|
118
|
+
await this.#runPhase(
|
|
119
|
+
"assets",
|
|
120
|
+
"Trimming unread static assets",
|
|
121
|
+
async () => (assets.pruneFonts ? await new FontPruner(this.#app, assets).prune() : null),
|
|
122
|
+
(result) =>
|
|
123
|
+
result
|
|
124
|
+
? `${result.removed.length} font file(s) dropped, ${ApplicationBuildRunner.formatBytes(result.freedBytes)} freed; ${result.kept.length} kept`
|
|
125
|
+
: "disabled by akan.config.ts assets.pruneFonts",
|
|
107
126
|
phaseOptions,
|
|
108
127
|
);
|
|
109
128
|
await this.#runPhase(
|
|
@@ -166,7 +185,7 @@ export class ApplicationBuildRunner {
|
|
|
166
185
|
const akanConfig = await this.#app.getConfig();
|
|
167
186
|
await Promise.all([
|
|
168
187
|
this.#app.dist.writeJson("package.json", akanConfig.getProductionPackageJson()),
|
|
169
|
-
this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.
|
|
188
|
+
this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.dockerfile),
|
|
170
189
|
]);
|
|
171
190
|
}
|
|
172
191
|
|
|
@@ -175,6 +194,7 @@ export class ApplicationBuildRunner {
|
|
|
175
194
|
const backendExternals = [
|
|
176
195
|
...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]),
|
|
177
196
|
];
|
|
197
|
+
const { web } = akanConfig;
|
|
178
198
|
const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
|
|
179
199
|
for (const entrypoint of backendEntryPoints) {
|
|
180
200
|
if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
|
|
@@ -188,17 +208,20 @@ export class ApplicationBuildRunner {
|
|
|
188
208
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
189
209
|
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
190
210
|
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
// Nothing spawns the RSC worker without SSR, so an api-only image does not carry it.
|
|
212
|
+
const rscWorkerResult = web.ssr
|
|
213
|
+
? await this.#buildOrThrow("rsc-worker", {
|
|
214
|
+
entrypoints: [this.#resolveRscWorkerBuildEntry()],
|
|
215
|
+
outdir: this.#app.dist.cwdPath,
|
|
216
|
+
target: "bun",
|
|
217
|
+
minify: true,
|
|
218
|
+
naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
|
|
219
|
+
conditions: ["react-server"],
|
|
220
|
+
// `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
|
|
221
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
222
|
+
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
223
|
+
})
|
|
224
|
+
: null;
|
|
202
225
|
const consoleRuntimeResult = await this.#buildOrThrow("console-runtime", {
|
|
203
226
|
entrypoints: [this.#resolveConsoleRuntimeBuildEntry()],
|
|
204
227
|
outdir: this.#app.dist.cwdPath,
|
|
@@ -209,8 +232,9 @@ export class ApplicationBuildRunner {
|
|
|
209
232
|
});
|
|
210
233
|
await this.#writeConsoleShim();
|
|
211
234
|
return {
|
|
212
|
-
entrypoints: backendEntryPoints.length + 2,
|
|
213
|
-
outputs:
|
|
235
|
+
entrypoints: backendEntryPoints.length + (rscWorkerResult ? 2 : 1),
|
|
236
|
+
outputs:
|
|
237
|
+
backendResult.outputs.length + (rscWorkerResult?.outputs.length ?? 0) + consoleRuntimeResult.outputs.length + 1,
|
|
214
238
|
};
|
|
215
239
|
}
|
|
216
240
|
|
|
@@ -55,7 +55,7 @@ interface RootBoundary {
|
|
|
55
55
|
segments: string[];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function getRootBoundarySegments(key: string): string[] | null {
|
|
58
|
+
export function getRootBoundarySegments(key: string): string[] | null {
|
|
59
59
|
const match = LAYOUT_KEY_RE.exec(key);
|
|
60
60
|
if (!match) return null;
|
|
61
61
|
const prefix = match[1]?.replace(/\/$/, "");
|
|
@@ -76,7 +76,7 @@ function implicitDictionaryMacroAbsPath(appCwdPath: string): string {
|
|
|
76
76
|
return path.join(path.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function isRootBoundarySegments(segments: string[], basePaths: Iterable<string>): boolean {
|
|
79
|
+
export function isRootBoundarySegments(segments: string[], basePaths: Iterable<string>): boolean {
|
|
80
80
|
const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
|
|
81
81
|
if (firstVisibleIndex === -1) return segments.length <= 1;
|
|
82
82
|
if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment))) return false;
|