@akanjs/devkit 3.0.0-alpha.8 → 3.0.0-alpha.80
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/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +65 -562
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +412 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +132 -47
- package/akanConfig/types.ts +8 -0
- package/akanContext.ts +70 -14
- package/akanMcpContract.ts +25 -4
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +340 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/commandDecorators/command.ts +30 -6
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +132 -5
- package/executors.ts +89 -14
- package/fileEditor.ts +19 -19
- package/fleetConfig.ts +73 -0
- package/fleetGuard.test.ts +121 -0
- package/fleetGuard.ts +157 -0
- package/fleetSpoke.test.ts +390 -0
- package/fleetSpoke.ts +719 -0
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +4 -0
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +8 -4
- package/index.ts +0 -5
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +109 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- 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-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +6 -6
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/repoIdentity.ts +42 -0
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +151 -0
- package/slicePlanner.ts +157 -0
- package/ssrScanner.test.ts +301 -0
- package/transforms/asyncDefaultExportDetector.ts +103 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/ui/ScrollList.tsx +6 -8
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
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,34 @@ 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
|
+
// File logging is off in the image: a container's writable layer is ephemeral and nothing collects a
|
|
423
|
+
// file from it, so the rotating files would only fill the node disk (50MB x 100 at `trace`). stdout is
|
|
424
|
+
// the collection path; a deployment that wants the files back sets `AKAN_LOG_TO_FILE=1`.
|
|
425
|
+
const webEnvLines = [
|
|
426
|
+
...(this.web.ssr ? [] : ["ENV AKAN_SSR=false"]),
|
|
427
|
+
...(this.web.csr ? [] : ["ENV AKAN_CSR=false"]),
|
|
428
|
+
].join("\n");
|
|
429
|
+
return `${imageScript}
|
|
430
|
+
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
431
|
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
432
|
ARG TARGETARCH
|
|
366
433
|
${preRunScripts.join("\n")}
|
|
367
434
|
RUN mkdir -p /workspace
|
|
@@ -380,15 +447,9 @@ ${this.basePaths.size ? `ENV AKAN_PUBLIC_BASE_PATHS=${[...this.basePaths].join("
|
|
|
380
447
|
ENV AKAN_PUBLIC_DEFAULT_LOCALE=${this.i18n.defaultLocale}
|
|
381
448
|
ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
|
|
382
449
|
ENV AKAN_PUBLIC_OPERATION_MODE=cloud
|
|
383
|
-
|
|
450
|
+
ENV AKAN_LOG_TO_FILE=0
|
|
451
|
+
${webEnvLines}
|
|
384
452
|
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
453
|
}
|
|
393
454
|
static #importGeneration = 0;
|
|
394
455
|
/**
|
|
@@ -415,8 +476,34 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
415
476
|
app.workspace.getPackageJson(),
|
|
416
477
|
]);
|
|
417
478
|
const resolved = typeof configImp === "function" ? configImp(app) : configImp;
|
|
418
|
-
const { plugins, ...config } = (resolved ?? {}) as
|
|
419
|
-
|
|
479
|
+
const { plugins, ...config } = (resolved ?? {}) as AppConfigDeclaration & { plugins?: AkanPlugin[] };
|
|
480
|
+
const libContributions = await AkanAppConfig.#collectLibContributions(app, libs, bustImportCache);
|
|
481
|
+
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libContributions);
|
|
482
|
+
}
|
|
483
|
+
//* Every workspace lib is read, not just this app's lib deps: narrowing the set needs the dependency
|
|
484
|
+
//* scan, and the incremental page rebundle re-reads this config on every file change.
|
|
485
|
+
static async #collectLibContributions(app: App, libs: string[], bustImportCache: boolean): Promise<LibContributions> {
|
|
486
|
+
const libConfigs = await Promise.all(
|
|
487
|
+
libs.map(async (libName) =>
|
|
488
|
+
LibExecutor.from(app, libName)
|
|
489
|
+
.getConfig({ refresh: bustImportCache })
|
|
490
|
+
.catch((error: unknown) => {
|
|
491
|
+
app.logger.warn(`Skipped libs/${libName}/akan.config.ts contributions: ${String(error)}`);
|
|
492
|
+
return null;
|
|
493
|
+
}),
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
return {
|
|
497
|
+
externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
|
|
498
|
+
docker: {
|
|
499
|
+
preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
|
|
500
|
+
postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? []),
|
|
501
|
+
},
|
|
502
|
+
//* A lib writes the glob against its own `public/`; `akan sync` mounts that at `public/libs/<lib>`.
|
|
503
|
+
keepFonts: libConfigs.flatMap((libConfig) =>
|
|
504
|
+
(libConfig?.assets.keepFonts ?? []).map((glob) => `libs/${libConfig?.lib.name}/${glob}`),
|
|
505
|
+
),
|
|
506
|
+
};
|
|
420
507
|
}
|
|
421
508
|
#resolveProductionDependencyVersion(lib: string) {
|
|
422
509
|
const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
|
|
@@ -428,13 +515,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
428
515
|
return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
429
516
|
}
|
|
430
517
|
#getProductionRuntimePackages() {
|
|
431
|
-
return [
|
|
432
|
-
...this.externalLibs,
|
|
433
|
-
...SSR_RUNTIME_PACKAGES,
|
|
434
|
-
...NATIVE_RUNTIME_PACKAGES,
|
|
435
|
-
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
436
|
-
...this.getDatabaseModeRuntimePackages(),
|
|
437
|
-
];
|
|
518
|
+
return [...this.externalLibs, ...SSR_RUNTIME_PACKAGES, ...this.getDatabaseModeRuntimePackages()];
|
|
438
519
|
}
|
|
439
520
|
getDatabaseModeRuntimePackages(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
|
|
440
521
|
return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
|
|
@@ -532,15 +613,19 @@ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfi
|
|
|
532
613
|
export class AkanLibConfig implements LibConfigResult {
|
|
533
614
|
lib: Lib;
|
|
534
615
|
externalLibs: string[];
|
|
616
|
+
docker: LibDockerConfig;
|
|
617
|
+
assets: LibAssetsConfig;
|
|
535
618
|
/** Live-only: plugins declared in this lib's `akan.config.ts` (never serialized). */
|
|
536
619
|
plugins: AkanPlugin[];
|
|
537
620
|
constructor(lib: Lib, config: DeepPartial<LibConfigResult>, plugins: AkanPlugin[] = []) {
|
|
538
621
|
this.lib = lib;
|
|
539
622
|
this.externalLibs = config?.externalLibs ?? [];
|
|
623
|
+
this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
|
|
624
|
+
this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts as string[] | undefined) };
|
|
540
625
|
this.plugins = plugins;
|
|
541
626
|
}
|
|
542
|
-
static async from(lib: Lib) {
|
|
543
|
-
const
|
|
627
|
+
static async from(lib: Lib, { bustImportCache = false }: { bustImportCache?: boolean } = {}) {
|
|
628
|
+
const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
|
|
544
629
|
const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
|
|
545
630
|
const { plugins, ...config } = (resolved ?? {}) as DeepPartial<LibConfigResult> & { plugins?: AkanPlugin[] };
|
|
546
631
|
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";
|
|
@@ -129,15 +130,29 @@ export type CursorMcpConfig = {
|
|
|
129
130
|
mcpServers?: Record<string, unknown>;
|
|
130
131
|
};
|
|
131
132
|
|
|
132
|
-
export const
|
|
133
|
+
export const guidelineResourceUri = (name: string) => `akan://guidelines/${name}`;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resources that exist whatever the workspace holds. The guideline entries are **not** here: they are
|
|
137
|
+
* one per directory under `cli/guidelines/`, and a hardcoded pair listed 2 of 31 while `readResource`
|
|
138
|
+
* threw on the other 29. `buildResourceList` is the only place the two halves meet.
|
|
139
|
+
*/
|
|
140
|
+
const staticResourceList = [
|
|
133
141
|
{ uri: "akan://docs/framework", name: "Akan framework guide", mimeType: "text/markdown" },
|
|
134
|
-
{ uri: "akan://guidelines/framework", name: "Framework guideline", mimeType: "text/markdown" },
|
|
135
|
-
{ uri: "akan://guidelines/modelSignal", name: "Model signal guideline", mimeType: "text/markdown" },
|
|
136
142
|
{ uri: "akan://workspace/summary", name: "Workspace summary", mimeType: "application/json" },
|
|
137
143
|
{ uri: "akan://workspace/apps", name: "Workspace apps", mimeType: "application/json" },
|
|
138
144
|
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" },
|
|
139
145
|
];
|
|
140
146
|
|
|
147
|
+
export const buildResourceList = (guidelineNames: readonly string[]) => [
|
|
148
|
+
...staticResourceList,
|
|
149
|
+
...guidelineNames.map((name) => ({
|
|
150
|
+
uri: guidelineResourceUri(name),
|
|
151
|
+
name: `${name} guideline`,
|
|
152
|
+
mimeType: "text/markdown",
|
|
153
|
+
})),
|
|
154
|
+
];
|
|
155
|
+
|
|
141
156
|
export const cursorMcpConfigPath = ".cursor/mcp.json";
|
|
142
157
|
// Claude Code reads project-scoped MCP servers from `.mcp.json` at the workspace root.
|
|
143
158
|
export const claudeMcpConfigPath = ".mcp.json";
|
|
@@ -276,6 +291,8 @@ const validationCommands = [
|
|
|
276
291
|
"akan test <app-or-lib-or-pkg>",
|
|
277
292
|
"akan build <app-name>",
|
|
278
293
|
"akan doctor --strict --format json",
|
|
294
|
+
"akan quality scan [--format json]",
|
|
295
|
+
"akan quality ssr [--format json]",
|
|
279
296
|
];
|
|
280
297
|
|
|
281
298
|
const unknownGeneratedFilesFreshness: GeneratedFilesFreshness = {
|
|
@@ -618,6 +635,30 @@ export class AkanContextAnalyzer {
|
|
|
618
635
|
};
|
|
619
636
|
}
|
|
620
637
|
|
|
638
|
+
// The conventions in AGENTS.md ship with the package, but nothing re-renders them on `bun update` — a workspace
|
|
639
|
+
// keeps whichever release wrote its block until someone re-runs the install. Comparing the stamp against the
|
|
640
|
+
// running devkit is the only signal that the guide an agent is reading is older than the framework it describes.
|
|
641
|
+
static async #agentGuideDrift(workspace: WorkspaceExecutor) {
|
|
642
|
+
const installed = await readDevkitVersion();
|
|
643
|
+
if (!installed) return null;
|
|
644
|
+
const content = await safeReadText(path.join(workspace.workspaceRoot, "AGENTS.md"));
|
|
645
|
+
if (content === null) return null;
|
|
646
|
+
const stamped = extractBlockVersion(content);
|
|
647
|
+
if (stamped === installed) return null;
|
|
648
|
+
const hint = "Re-render the AGENTS.md managed block from the installed framework release.";
|
|
649
|
+
if (!stamped)
|
|
650
|
+
return {
|
|
651
|
+
code: "agent-guide-unstamped",
|
|
652
|
+
message: `AGENTS.md carries no generated-version stamp, so its conventions may predate @akanjs/devkit ${installed}`,
|
|
653
|
+
hint,
|
|
654
|
+
};
|
|
655
|
+
return {
|
|
656
|
+
code: "agent-guide-stale",
|
|
657
|
+
message: `AGENTS.md was generated by @akanjs/devkit ${stamped}, but ${installed} is installed`,
|
|
658
|
+
hint,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
621
662
|
static async doctor(
|
|
622
663
|
workspace: WorkspaceExecutor,
|
|
623
664
|
{
|
|
@@ -639,23 +680,25 @@ export class AkanContextAnalyzer {
|
|
|
639
680
|
),
|
|
640
681
|
];
|
|
641
682
|
|
|
642
|
-
for (const
|
|
643
|
-
const
|
|
644
|
-
for (const entry of await safeReadDir(
|
|
645
|
-
if (!
|
|
646
|
-
const allowed = entry.isDirectory()
|
|
683
|
+
for (const sys of [...context.apps, ...context.libs]) {
|
|
684
|
+
const sysPath = path.join(workspace.workspaceRoot, sys.path);
|
|
685
|
+
for (const entry of await safeReadDir(sysPath)) {
|
|
686
|
+
if (!isScannedRootEntry(sys.type, entry.name)) continue;
|
|
687
|
+
const allowed = entry.isDirectory()
|
|
688
|
+
? rootAllowedDirs[sys.type].has(entry.name)
|
|
689
|
+
: rootAllowedFiles[sys.type].has(entry.name);
|
|
647
690
|
if (!allowed) {
|
|
648
691
|
const action = repairAction(
|
|
649
692
|
"module-shape",
|
|
650
|
-
`akan repair module-shape --app ${
|
|
651
|
-
|
|
693
|
+
`akan repair module-shape --app ${sys.name}`,
|
|
694
|
+
`Review ${sys.type} root shape and remove or move the unknown entry.`,
|
|
652
695
|
false,
|
|
653
696
|
);
|
|
654
697
|
diagnostics.push({
|
|
655
698
|
severity: "error",
|
|
656
|
-
code:
|
|
657
|
-
path: `${
|
|
658
|
-
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in
|
|
699
|
+
code: `${sys.type}-root-unknown-entry`,
|
|
700
|
+
path: `${sys.path}/${entry.name}`,
|
|
701
|
+
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in ${sys.type} root: ${sys.path}/${entry.name}`,
|
|
659
702
|
repairActions: [action],
|
|
660
703
|
});
|
|
661
704
|
repairActions.push(action);
|
|
@@ -663,6 +706,19 @@ export class AkanContextAnalyzer {
|
|
|
663
706
|
}
|
|
664
707
|
}
|
|
665
708
|
|
|
709
|
+
const agentDrift = await AkanContextAnalyzer.#agentGuideDrift(workspace);
|
|
710
|
+
if (agentDrift) {
|
|
711
|
+
const action = repairAction("generated", "akan agent install agents-md", agentDrift.hint, true);
|
|
712
|
+
diagnostics.push({
|
|
713
|
+
severity: "warning",
|
|
714
|
+
code: agentDrift.code,
|
|
715
|
+
path: "AGENTS.md",
|
|
716
|
+
message: agentDrift.message,
|
|
717
|
+
repairActions: [action],
|
|
718
|
+
});
|
|
719
|
+
repairActions.push(action);
|
|
720
|
+
}
|
|
721
|
+
|
|
666
722
|
for (const sys of [...context.apps, ...context.libs]) {
|
|
667
723
|
for (const module of sys.modules) {
|
|
668
724
|
if (!module.abstract.exists) {
|
package/akanMcpContract.ts
CHANGED
|
@@ -391,10 +391,31 @@ export const applyMcpTools: McpToolDefinition[] = [
|
|
|
391
391
|
},
|
|
392
392
|
];
|
|
393
393
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
394
|
+
/**
|
|
395
|
+
* `guidelineNames` fills the `get_guideline` enum. Without it the argument is a bare string and a model
|
|
396
|
+
* has to guess one of the ~30 directory names, which it cannot see from here — the names are a
|
|
397
|
+
* filesystem listing, so they can only arrive from a caller that already read it.
|
|
398
|
+
*/
|
|
399
|
+
export const listAkanMcpTools = (
|
|
400
|
+
mode: "readonly" | "plan" | "apply" = "readonly",
|
|
401
|
+
{ guidelineNames = [] }: { guidelineNames?: readonly string[] } = {},
|
|
402
|
+
) => {
|
|
403
|
+
const tools =
|
|
404
|
+
mode === "readonly"
|
|
405
|
+
? readonlyMcpTools
|
|
406
|
+
: mode === "plan"
|
|
407
|
+
? [...readonlyMcpTools, ...planMcpTools]
|
|
408
|
+
: [...readonlyMcpTools, ...planMcpTools, ...applyMcpTools];
|
|
409
|
+
if (guidelineNames.length === 0) return tools;
|
|
410
|
+
return tools.map((tool) =>
|
|
411
|
+
tool.name === "get_guideline"
|
|
412
|
+
? {
|
|
413
|
+
...tool,
|
|
414
|
+
description: `${tool.description ?? ""} One of: ${guidelineNames.join(", ")}.`.trim(),
|
|
415
|
+
inputSchema: { ...tool.inputSchema, properties: { name: { type: "string", enum: [...guidelineNames] } } },
|
|
416
|
+
}
|
|
417
|
+
: tool,
|
|
418
|
+
);
|
|
398
419
|
};
|
|
399
420
|
|
|
400
421
|
export const createAkanValidationContract = (listTools = listAkanMcpTools) => ({
|
|
@@ -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
|
});
|