@akanjs/devkit 3.0.0-alpha.9 → 3.0.0-alpha.91
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/DEV_RUNTIME_KNOBS.md +27 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +21 -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 +113 -578
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +442 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +143 -47
- package/akanConfig/types.ts +9 -0
- package/akanContext.ts +30 -14
- package/akanMcpContract.ts +25 -4
- package/appSelectionMemory.ts +28 -0
- 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 +86 -47
- package/biomeBase.ts +6 -5
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/codegenLock.test.ts +137 -0
- package/codegenLock.ts +136 -0
- package/commandDecorators/argMeta.ts +9 -1
- package/commandDecorators/command.ts +94 -7
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/commandDecorators/helpFormatter.ts +4 -0
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +129 -4
- package/executors.ts +105 -24
- package/fileEditor.ts +19 -19
- package/fileSys.ts +19 -1
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +7 -2
- 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/devGeneratedIndexSync.ts +3 -2
- 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/sourceMtimeIndex.ts +1 -1
- 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/frontendBuild/watchRootResolver.test.ts +67 -0
- package/frontendBuild/watchRootResolver.ts +13 -1
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
- package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
- package/index.ts +0 -7
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +112 -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-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 +5 -8
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -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/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +48 -2
- 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/ui/MultiScrollList.tsx +0 -242
- package/ui/ScrollList.tsx +0 -107
- package/ui/index.ts +0 -2
- package/useStdoutDimensions.ts +0 -20
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { AppExecutor, WorkspaceExecutor } from "../executors";
|
|
6
7
|
import type { PackageJson } from "../types";
|
|
7
8
|
import { AkanAppConfig, AkanLibConfig, deriveDefaultAppId } from "./akanConfig";
|
|
8
9
|
import type { DeepPartial, LibConfigResult } from "./types";
|
|
@@ -19,7 +20,6 @@ const packageJson: PackageJson = {
|
|
|
19
20
|
react: "19.0.0",
|
|
20
21
|
"react-dom": "19.0.0",
|
|
21
22
|
"react-server-dom-webpack": "19.0.0",
|
|
22
|
-
sharp: "1.0.0",
|
|
23
23
|
"@external/runtime": "2.0.0",
|
|
24
24
|
},
|
|
25
25
|
};
|
|
@@ -64,7 +64,8 @@ describe("AkanAppConfig", () => {
|
|
|
64
64
|
expect(config.barrelImports).toEqual(
|
|
65
65
|
expect.arrayContaining(["@apps/portal/ui", "@libs/shared/server", "akanjs/common", "akanjs/server"]),
|
|
66
66
|
);
|
|
67
|
-
expect(config.
|
|
67
|
+
expect(config.dockerfile).toContain("ENV AKAN_PUBLIC_APP_NAME=portal");
|
|
68
|
+
expect(config.dockerfile).toContain("ENV AKAN_LOG_TO_FILE=0");
|
|
68
69
|
expect(process.env.AKAN_PUBLIC_DEFAULT_LOCALE).toBe("en");
|
|
69
70
|
});
|
|
70
71
|
|
|
@@ -128,9 +129,67 @@ describe("AkanAppConfig", () => {
|
|
|
128
129
|
});
|
|
129
130
|
expect(config.publicEnv).toEqual(["AKAN_PUBLIC_FEATURE"]);
|
|
130
131
|
expect(config.optimizeImports).toContain("custom-icons");
|
|
131
|
-
expect(config.
|
|
132
|
-
expect(config.
|
|
133
|
-
expect(config.
|
|
132
|
+
expect(config.dockerfile).toContain('CMD ["bun","server.js"]');
|
|
133
|
+
expect(config.dockerfile).toContain("FROM oven/bun:amd64 AS amd64");
|
|
134
|
+
expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("defaults both web surfaces on and keeps the image free of web env overrides", () => {
|
|
138
|
+
const config = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
|
|
139
|
+
|
|
140
|
+
expect(config.web).toEqual({ ssr: true, csr: true });
|
|
141
|
+
expect(config.dockerfile).not.toContain("AKAN_SSR");
|
|
142
|
+
expect(config.dockerfile).not.toContain("AKAN_CSR");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("bakes the disabled surface into the image env so the default matches what was built", () => {
|
|
146
|
+
const ssrOnly = new AkanAppConfig(app, [], packageJson, { web: { csr: false } }, baseDevEnv);
|
|
147
|
+
expect(ssrOnly.web).toEqual({ ssr: true, csr: false });
|
|
148
|
+
expect(ssrOnly.dockerfile).toContain("ENV AKAN_CSR=false");
|
|
149
|
+
expect(ssrOnly.dockerfile).not.toContain("ENV AKAN_SSR=false");
|
|
150
|
+
|
|
151
|
+
const apiOnly = new AkanAppConfig(app, [], packageJson, { web: false }, baseDevEnv);
|
|
152
|
+
expect(apiOnly.web).toEqual({ ssr: false, csr: false });
|
|
153
|
+
expect(apiOnly.dockerfile).toContain("ENV AKAN_SSR=false");
|
|
154
|
+
expect(apiOnly.dockerfile).toContain("ENV AKAN_CSR=false");
|
|
155
|
+
|
|
156
|
+
expect(new AkanAppConfig(app, [], packageJson, { web: true }, baseDevEnv).web).toEqual({ ssr: true, csr: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("refuses a csr-less build that ships a mobile app", () => {
|
|
160
|
+
expect(
|
|
161
|
+
() => new AkanAppConfig(app, [], packageJson, { web: { csr: false }, mobile: { appName: "portal" } }, baseDevEnv),
|
|
162
|
+
).toThrow("the Capacitor build ships that bundle");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("installs only ca-certificates and tzdata in the default image", () => {
|
|
166
|
+
const config = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
|
|
167
|
+
|
|
168
|
+
expect(config.dockerfile).toContain(
|
|
169
|
+
"RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*",
|
|
170
|
+
);
|
|
171
|
+
// The Chromium/ffmpeg toolchain moved to per-app `preRuns`; keeping it here paid for it in every image.
|
|
172
|
+
for (const dropped of ["libnss3", "ffmpeg", "build-essential", "redis", "xdg-utils"])
|
|
173
|
+
expect(config.dockerfile).not.toContain(dropped);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("keeps a declared Dockerfile string verbatim", () => {
|
|
177
|
+
const dockerfile = 'FROM oven/bun:1-slim\nCOPY . .\nCMD ["bun","main.js"]';
|
|
178
|
+
const config = new AkanAppConfig(app, [], packageJson, { docker: dockerfile }, baseDevEnv);
|
|
179
|
+
|
|
180
|
+
expect(config.docker).toBe(dockerfile);
|
|
181
|
+
expect(config.dockerfile).toBe(dockerfile);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("resolves the image parts, defaulting the base image and the command", () => {
|
|
185
|
+
const config = new AkanAppConfig(app, [], packageJson, { docker: { preRuns: ["echo hi"] } }, baseDevEnv);
|
|
186
|
+
|
|
187
|
+
expect(config.docker).toEqual({
|
|
188
|
+
image: "oven/bun:1-slim",
|
|
189
|
+
preRuns: ["echo hi"],
|
|
190
|
+
postRuns: [],
|
|
191
|
+
command: ["bun", "main.js"],
|
|
192
|
+
});
|
|
134
193
|
});
|
|
135
194
|
|
|
136
195
|
test("creates production package json and reports missing external versions", () => {
|
|
@@ -144,8 +203,6 @@ describe("AkanAppConfig", () => {
|
|
|
144
203
|
react: "19.0.0",
|
|
145
204
|
"react-dom": "19.0.0",
|
|
146
205
|
"react-server-dom-webpack": "19.0.0",
|
|
147
|
-
croner: akanPackageJson.peerDependencies?.croner,
|
|
148
|
-
sharp: "1.0.0",
|
|
149
206
|
"@external/runtime": "2.0.0",
|
|
150
207
|
},
|
|
151
208
|
});
|
|
@@ -184,8 +241,6 @@ describe("AkanAppConfig", () => {
|
|
|
184
241
|
react: runtimeDependencies.react,
|
|
185
242
|
"react-dom": runtimeDependencies["react-dom"],
|
|
186
243
|
"react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
|
|
187
|
-
croner: runtimeDependencies.croner,
|
|
188
|
-
sharp: runtimeDependencies.sharp,
|
|
189
244
|
});
|
|
190
245
|
});
|
|
191
246
|
|
|
@@ -198,9 +253,6 @@ describe("AkanAppConfig", () => {
|
|
|
198
253
|
const multipleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "multiple" }, baseDevEnv);
|
|
199
254
|
const clusterConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "cluster" }, baseDevEnv);
|
|
200
255
|
|
|
201
|
-
expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
202
|
-
croner: runtimeDependencies.croner,
|
|
203
|
-
});
|
|
204
256
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("ioredis");
|
|
205
257
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("bullmq");
|
|
206
258
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
|
|
@@ -210,7 +262,6 @@ describe("AkanAppConfig", () => {
|
|
|
210
262
|
expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
211
263
|
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
212
264
|
bullmq: runtimeDependencies.bullmq,
|
|
213
|
-
croner: runtimeDependencies.croner,
|
|
214
265
|
ioredis: runtimeDependencies.ioredis,
|
|
215
266
|
protobufjs: runtimeDependencies.protobufjs,
|
|
216
267
|
});
|
|
@@ -218,7 +269,6 @@ describe("AkanAppConfig", () => {
|
|
|
218
269
|
|
|
219
270
|
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
220
271
|
bullmq: runtimeDependencies.bullmq,
|
|
221
|
-
croner: runtimeDependencies.croner,
|
|
222
272
|
ioredis: runtimeDependencies.ioredis,
|
|
223
273
|
postgres: runtimeDependencies.postgres,
|
|
224
274
|
protobufjs: runtimeDependencies.protobufjs,
|
|
@@ -392,6 +442,115 @@ describe("deriveDefaultAppId", () => {
|
|
|
392
442
|
});
|
|
393
443
|
});
|
|
394
444
|
|
|
445
|
+
describe("AkanAppConfig lib externalLibs", () => {
|
|
446
|
+
test("merges lib-declared external libs into the app's own, deduped", () => {
|
|
447
|
+
const libAwarePackageJson: PackageJson = {
|
|
448
|
+
...packageJson,
|
|
449
|
+
dependencies: { ...packageJson.dependencies, puppeteer: "24.0.0" },
|
|
450
|
+
};
|
|
451
|
+
const config = new AkanAppConfig(
|
|
452
|
+
app,
|
|
453
|
+
["shared"],
|
|
454
|
+
libAwarePackageJson,
|
|
455
|
+
{ externalLibs: ["@external/runtime"] },
|
|
456
|
+
baseDevEnv,
|
|
457
|
+
[],
|
|
458
|
+
{ externalLibs: ["@external/runtime", "puppeteer"], docker: { preRuns: [], postRuns: [] } },
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(config.externalLibs).toEqual(["@external/runtime", "puppeteer"]);
|
|
462
|
+
expect(config.getProductionPackageJson().dependencies).toMatchObject({
|
|
463
|
+
"@external/runtime": "2.0.0",
|
|
464
|
+
puppeteer: "24.0.0",
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("reads them off every workspace lib config on load", async () => {
|
|
469
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libext-"));
|
|
470
|
+
try {
|
|
471
|
+
fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
|
|
472
|
+
fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
|
|
473
|
+
fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default { externalLibs: ['shiki'] };\n");
|
|
474
|
+
fs.writeFileSync(
|
|
475
|
+
path.join(root, "libs/extlib/akan.config.ts"),
|
|
476
|
+
"export default { externalLibs: ['puppeteer'] };\n",
|
|
477
|
+
);
|
|
478
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
479
|
+
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
480
|
+
|
|
481
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
482
|
+
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
483
|
+
|
|
484
|
+
expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
|
|
485
|
+
} finally {
|
|
486
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("AkanAppConfig lib docker runs", () => {
|
|
492
|
+
const libDocker = (preRuns: string[], postRuns: string[] = []) => ({
|
|
493
|
+
externalLibs: [],
|
|
494
|
+
docker: { preRuns, postRuns },
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("runs lib steps before the app's own, deduped", () => {
|
|
498
|
+
const config = new AkanAppConfig(
|
|
499
|
+
app,
|
|
500
|
+
["shared"],
|
|
501
|
+
packageJson,
|
|
502
|
+
{ docker: { preRuns: ["apt-get install -y ffmpeg", "echo app"], postRuns: ["echo app-post"] } },
|
|
503
|
+
baseDevEnv,
|
|
504
|
+
[],
|
|
505
|
+
libDocker(["apt-get install -y ffmpeg", "echo lib"], ["echo lib-post"]),
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
expect(config.docker).toMatchObject({
|
|
509
|
+
preRuns: ["apt-get install -y ffmpeg", "echo lib", "echo app"],
|
|
510
|
+
postRuns: ["echo lib-post", "echo app-post"],
|
|
511
|
+
});
|
|
512
|
+
expect(config.dockerfile).toContain("RUN echo lib\nRUN echo app\n");
|
|
513
|
+
expect(config.dockerfile.match(/RUN apt-get install -y ffmpeg/g)).toHaveLength(1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("drops them when the app hands over a whole Dockerfile", () => {
|
|
517
|
+
const config = new AkanAppConfig(
|
|
518
|
+
app,
|
|
519
|
+
["shared"],
|
|
520
|
+
packageJson,
|
|
521
|
+
{ docker: "FROM scratch" },
|
|
522
|
+
baseDevEnv,
|
|
523
|
+
[],
|
|
524
|
+
libDocker(["echo lib"]),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
expect(config.dockerfile).toBe("FROM scratch");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("reads them off every workspace lib config on load", async () => {
|
|
531
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libdocker-"));
|
|
532
|
+
try {
|
|
533
|
+
fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
|
|
534
|
+
fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
|
|
535
|
+
fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default {};\n");
|
|
536
|
+
fs.writeFileSync(
|
|
537
|
+
path.join(root, "libs/extlib/akan.config.ts"),
|
|
538
|
+
"export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
|
|
539
|
+
);
|
|
540
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
541
|
+
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
542
|
+
|
|
543
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
544
|
+
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
545
|
+
|
|
546
|
+
expect(config.dockerfile).toContain("RUN echo from-lib");
|
|
547
|
+
expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
|
|
548
|
+
} finally {
|
|
549
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
395
554
|
describe("AkanLibConfig", () => {
|
|
396
555
|
test("uses empty external libs by default and preserves explicit libs", () => {
|
|
397
556
|
const lib = { name: "shared" } as never;
|
|
@@ -402,6 +561,15 @@ describe("AkanLibConfig", () => {
|
|
|
402
561
|
};
|
|
403
562
|
expect(new AkanLibConfig(lib, config).externalLibs).toEqual(["firebase-admin"]);
|
|
404
563
|
});
|
|
564
|
+
|
|
565
|
+
test("defaults docker runs to empty lists and preserves declared ones", () => {
|
|
566
|
+
const lib = { name: "shared" } as never;
|
|
567
|
+
expect(new AkanLibConfig(lib, {}).docker).toEqual({ preRuns: [], postRuns: [] });
|
|
568
|
+
expect(new AkanLibConfig(lib, { docker: { preRuns: ["echo lib"] } }).docker).toEqual({
|
|
569
|
+
preRuns: ["echo lib"],
|
|
570
|
+
postRuns: [],
|
|
571
|
+
});
|
|
572
|
+
});
|
|
405
573
|
});
|
|
406
574
|
|
|
407
575
|
describe("AkanAppConfig.importConfigModule", () => {
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -2,22 +2,31 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import type { AkanPlugin } from "akanjs";
|
|
5
|
+
import { normalizeRoutePrefix } from "akanjs/base";
|
|
5
6
|
import { type AkanI18nConfig, resolveAkanI18nConfig } from "akanjs/common";
|
|
6
7
|
import type { AkanImageConfig } from "akanjs/server";
|
|
7
8
|
import type { App, Lib } from "../commandDecorators";
|
|
8
|
-
import { WorkspaceExecutor } from "../executors";
|
|
9
|
+
import { LibExecutor, WorkspaceExecutor } from "../executors";
|
|
9
10
|
import type { BaseDevEnv, PackageJson } from "../types";
|
|
10
11
|
import {
|
|
12
|
+
type AkanApiConfig,
|
|
13
|
+
type AkanAssetsConfig,
|
|
11
14
|
type AkanMobileConfig,
|
|
12
15
|
type AkanMobileTargetConfig,
|
|
13
16
|
type AkanRouteConfig,
|
|
17
|
+
type AkanWebConfig,
|
|
18
|
+
type AkanWebOption,
|
|
14
19
|
type AppConfigResult,
|
|
15
20
|
type Arch,
|
|
16
21
|
archs,
|
|
17
22
|
type DatabaseMode,
|
|
18
23
|
type DeepPartial,
|
|
19
24
|
type DockerConfig,
|
|
25
|
+
type DockerOption,
|
|
26
|
+
type DockerRun,
|
|
27
|
+
type LibAssetsConfig,
|
|
20
28
|
type LibConfigResult,
|
|
29
|
+
type LibDockerConfig,
|
|
21
30
|
} from "./types";
|
|
22
31
|
|
|
23
32
|
const DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
|
|
@@ -49,9 +58,8 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
|
|
|
49
58
|
"react-icons/*",
|
|
50
59
|
];
|
|
51
60
|
const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
|
|
61
|
+
const DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
|
|
52
62
|
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
63
|
// The firebase client (push tokens) and the Capacitor toolchain — `@capacitor/cli` (`npx cap`),
|
|
56
64
|
// `@capacitor/assets` (`npx @capacitor/assets`) plus the `@capacitor/core`/`ios`/`android` runtime
|
|
57
65
|
// and native-platform packages that `npx cap add`/`sync` resolve from the workspace node_modules.
|
|
@@ -92,8 +100,6 @@ const DATABASE_MODE_RUNTIME_PACKAGES = {
|
|
|
92
100
|
} satisfies Record<DatabaseMode, readonly string[]>;
|
|
93
101
|
const AKAN_RUNTIME_PACKAGES = new Set<string>([
|
|
94
102
|
...SSR_RUNTIME_PACKAGES,
|
|
95
|
-
...NATIVE_RUNTIME_PACKAGES,
|
|
96
|
-
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
97
103
|
...MOBILE_RUNTIME_PACKAGES,
|
|
98
104
|
...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
|
|
99
105
|
]);
|
|
@@ -109,6 +115,7 @@ const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
|
|
|
109
115
|
maximumRedirects: 3,
|
|
110
116
|
fetchTimeoutMs: 7000,
|
|
111
117
|
maxRemoteBytes: 25 * 1024 * 1024,
|
|
118
|
+
maxConcurrency: 0,
|
|
112
119
|
};
|
|
113
120
|
|
|
114
121
|
const normalizeIndexPath = (indexPath: string | undefined): string | undefined => {
|
|
@@ -167,21 +174,57 @@ const normalizeDeepLinks = (deepLinks: DeepPartial<AkanMobileTargetConfig["deepL
|
|
|
167
174
|
} satisfies AkanMobileTargetConfig["deepLinks"];
|
|
168
175
|
};
|
|
169
176
|
|
|
177
|
+
/** What `akan.config.ts` may write: the resolved shape made partial, with `docker` and `web` in their unions. */
|
|
178
|
+
type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web"> & {
|
|
179
|
+
docker?: DockerOption;
|
|
180
|
+
web?: AkanWebOption;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** What the workspace's libs add to an app's build, read off each `libs/<lib>/akan.config.ts`. */
|
|
184
|
+
export interface LibContributions {
|
|
185
|
+
externalLibs: string[];
|
|
186
|
+
docker: LibDockerConfig;
|
|
187
|
+
/** Keep globs rewritten to the app's own `public/`, where `akan sync` mounts each lib's assets. */
|
|
188
|
+
keepFonts?: string[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const emptyLibContributions = (): LibContributions => ({
|
|
192
|
+
externalLibs: [],
|
|
193
|
+
docker: { preRuns: [], postRuns: [] },
|
|
194
|
+
keepFonts: [],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const normalizeKeepFonts = (keepFonts: string[] | undefined) => [
|
|
198
|
+
...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean)),
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
/** First occurrence wins, so a step a lib and its app both declare becomes one layer. */
|
|
202
|
+
const dedupeDockerRuns = (runs: DockerRun[]): DockerRun[] => {
|
|
203
|
+
const byKey = new Map<string, DockerRun>();
|
|
204
|
+
for (const run of runs) byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
|
|
205
|
+
return [...byKey.values()];
|
|
206
|
+
};
|
|
207
|
+
|
|
170
208
|
export class AkanAppConfig implements AppConfigResult {
|
|
171
209
|
app: App;
|
|
172
210
|
rootPackageJson: PackageJson;
|
|
173
211
|
docker: DockerConfig;
|
|
212
|
+
/** The Dockerfile `akan build` writes: the declared string verbatim, or one assembled from the parts. */
|
|
213
|
+
dockerfile: string;
|
|
174
214
|
defaultDatabaseMode: DatabaseMode;
|
|
215
|
+
web: AkanWebConfig;
|
|
175
216
|
externalLibs: string[];
|
|
176
217
|
barrelImports: string[];
|
|
177
218
|
optimizeImports: string[];
|
|
178
219
|
images: AkanImageConfig;
|
|
179
220
|
i18n: AkanI18nConfig;
|
|
221
|
+
api: AkanApiConfig;
|
|
180
222
|
publicEnv: string[];
|
|
181
223
|
mobile: AkanMobileConfig;
|
|
182
224
|
/** True only when the app's akan.config.ts explicitly declares a `mobile` section (vs. the synthesized default). */
|
|
183
225
|
hasMobileConfig: boolean;
|
|
184
226
|
secrets: string[];
|
|
227
|
+
assets: AkanAssetsConfig;
|
|
185
228
|
/** Raw setting; resolved against the app's lib deps at sync time (see `AppExecutor.syncPages`). */
|
|
186
229
|
syncPageLibs: string[] | boolean;
|
|
187
230
|
baseDevEnv: BaseDevEnv;
|
|
@@ -196,9 +239,10 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
196
239
|
app: App,
|
|
197
240
|
libs: string[],
|
|
198
241
|
rootPackageJson: PackageJson,
|
|
199
|
-
config:
|
|
242
|
+
config: AppConfigDeclaration,
|
|
200
243
|
baseDevEnv: BaseDevEnv,
|
|
201
244
|
plugins: AkanPlugin[] = [],
|
|
245
|
+
libContributions: LibContributions = emptyLibContributions(),
|
|
202
246
|
) {
|
|
203
247
|
this.app = app;
|
|
204
248
|
this.rootPackageJson = rootPackageJson;
|
|
@@ -207,7 +251,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
207
251
|
this.plugins = plugins;
|
|
208
252
|
this.#applyRoutes(config?.routes);
|
|
209
253
|
this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
|
|
210
|
-
this.externalLibs = config?.externalLibs ?? [];
|
|
254
|
+
this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...libContributions.externalLibs])];
|
|
211
255
|
this.barrelImports = [
|
|
212
256
|
...DEFAULT_BARREL_IMPORTS,
|
|
213
257
|
...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
|
|
@@ -219,12 +263,36 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
219
263
|
this.i18n = resolveAkanI18nConfig(config?.i18n);
|
|
220
264
|
process.env.AKAN_PUBLIC_DEFAULT_LOCALE = this.i18n.defaultLocale;
|
|
221
265
|
process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
|
|
266
|
+
this.api = {
|
|
267
|
+
prefix: normalizeRoutePrefix(config?.api?.prefix) ?? "/api",
|
|
268
|
+
websocketPrefix: normalizeRoutePrefix(config?.api?.websocketPrefix) ?? "/ws",
|
|
269
|
+
};
|
|
270
|
+
process.env.AKAN_PUBLIC_API_PREFIX = this.api.prefix;
|
|
271
|
+
process.env.AKAN_PUBLIC_WS_PREFIX = this.api.websocketPrefix;
|
|
222
272
|
this.publicEnv = (config?.publicEnv as string[] | undefined) ?? ([] as string[]);
|
|
223
273
|
this.secrets = (config?.secrets as string[] | undefined) ?? ([] as string[]);
|
|
274
|
+
this.assets = {
|
|
275
|
+
pruneFonts: config?.assets?.pruneFonts ?? true,
|
|
276
|
+
keepFonts: [
|
|
277
|
+
...normalizeKeepFonts(config?.assets?.keepFonts as string[] | undefined),
|
|
278
|
+
...(libContributions.keepFonts ?? []),
|
|
279
|
+
],
|
|
280
|
+
};
|
|
224
281
|
this.syncPageLibs = (config?.syncPageLibs as string[] | boolean | undefined) ?? false;
|
|
225
282
|
this.hasMobileConfig = Boolean(config.mobile);
|
|
226
283
|
this.mobile = this.#resolveMobileConfig(config.mobile);
|
|
227
|
-
this.
|
|
284
|
+
this.web = this.#resolveWebConfig(config.web);
|
|
285
|
+
this.docker = AkanAppConfig.#resolveDocker(config.docker, libContributions.docker);
|
|
286
|
+
this.dockerfile = this.#makeDockerfile();
|
|
287
|
+
}
|
|
288
|
+
#resolveWebConfig(web: AkanWebOption | undefined): AkanWebConfig {
|
|
289
|
+
const resolved = typeof web === "object" ? { ssr: true, csr: web.csr } : { ssr: web ?? true, csr: web ?? true };
|
|
290
|
+
// `akan build-ios` / `build-android` copy `dist/apps/<app>/csr/<target>.html` into the native project.
|
|
291
|
+
if (!resolved.csr && this.hasMobileConfig)
|
|
292
|
+
throw new Error(
|
|
293
|
+
`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.`,
|
|
294
|
+
);
|
|
295
|
+
return resolved;
|
|
228
296
|
}
|
|
229
297
|
#resolveMobileConfig(mobile: DeepPartial<AkanMobileConfig> | undefined): AkanMobileConfig {
|
|
230
298
|
const {
|
|
@@ -325,7 +393,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
325
393
|
this.branches.forEach((domain) => void domains.add(`${basePath}-${domain}.${serveDomain}`));
|
|
326
394
|
});
|
|
327
395
|
}
|
|
328
|
-
#getDockerRunScripts(runs:
|
|
396
|
+
#getDockerRunScripts(runs: DockerRun[]) {
|
|
329
397
|
return runs.map((run) => {
|
|
330
398
|
if (typeof run === "string") return `RUN ${run}`;
|
|
331
399
|
else
|
|
@@ -342,26 +410,34 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
342
410
|
if (typeof image === "string") return `FROM ${image}`;
|
|
343
411
|
else return archs.map((arch) => `FROM ${image[arch] ?? defaultImage} AS ${arch}`).join("\n");
|
|
344
412
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
413
|
+
/** A declared Dockerfile string is verbatim, so a lib's steps are dropped rather than silently unapplied. */
|
|
414
|
+
static #resolveDocker(docker: DockerOption | undefined, libDocker: LibDockerConfig): DockerConfig {
|
|
415
|
+
if (typeof docker === "string") return docker;
|
|
416
|
+
return {
|
|
417
|
+
image: docker?.image ?? DEFAULT_DOCKER_IMAGE,
|
|
418
|
+
preRuns: dedupeDockerRuns([...libDocker.preRuns, ...(docker?.preRuns ?? [])]),
|
|
419
|
+
postRuns: dedupeDockerRuns([...libDocker.postRuns, ...(docker?.postRuns ?? [])]),
|
|
420
|
+
command: docker?.command ?? ["bun", "main.js"],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
#makeDockerfile(): string {
|
|
424
|
+
if (typeof this.docker === "string") return this.docker;
|
|
425
|
+
const { image, preRuns, postRuns, command } = this.docker;
|
|
426
|
+
const preRunScripts = this.#getDockerRunScripts(preRuns);
|
|
427
|
+
const postRunScripts = this.#getDockerRunScripts(postRuns);
|
|
428
|
+
const imageScript = this.#getDockerImageScript(image, DEFAULT_DOCKER_IMAGE);
|
|
429
|
+
// The image default matches what the build actually produced; a deployment narrows it further with its
|
|
430
|
+
// own env, and can never widen it past the artifacts that are in the image.
|
|
431
|
+
// File logging is off in the image: a container's writable layer is ephemeral and nothing collects a
|
|
432
|
+
// file from it, so the rotating files would only fill the node disk (50MB x 100 at `trace`). stdout is
|
|
433
|
+
// the collection path; a deployment that wants the files back sets `AKAN_LOG_TO_FILE=1`.
|
|
434
|
+
const webEnvLines = [
|
|
435
|
+
...(this.web.ssr ? [] : ["ENV AKAN_SSR=false"]),
|
|
436
|
+
...(this.web.csr ? [] : ["ENV AKAN_CSR=false"]),
|
|
437
|
+
].join("\n");
|
|
438
|
+
return `${imageScript}
|
|
439
|
+
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
440
|
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
441
|
ARG TARGETARCH
|
|
366
442
|
${preRunScripts.join("\n")}
|
|
367
443
|
RUN mkdir -p /workspace
|
|
@@ -379,16 +455,12 @@ ENV AKAN_PUBLIC_ENV=${this.baseDevEnv.env}
|
|
|
379
455
|
${this.basePaths.size ? `ENV AKAN_PUBLIC_BASE_PATHS=${[...this.basePaths].join(",")}` : ""}
|
|
380
456
|
ENV AKAN_PUBLIC_DEFAULT_LOCALE=${this.i18n.defaultLocale}
|
|
381
457
|
ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
|
|
458
|
+
ENV AKAN_PUBLIC_API_PREFIX=${this.api.prefix}
|
|
459
|
+
ENV AKAN_PUBLIC_WS_PREFIX=${this.api.websocketPrefix}
|
|
382
460
|
ENV AKAN_PUBLIC_OPERATION_MODE=cloud
|
|
383
|
-
|
|
461
|
+
ENV AKAN_LOG_TO_FILE=0
|
|
462
|
+
${webEnvLines}
|
|
384
463
|
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
464
|
}
|
|
393
465
|
static #importGeneration = 0;
|
|
394
466
|
/**
|
|
@@ -415,8 +487,34 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
415
487
|
app.workspace.getPackageJson(),
|
|
416
488
|
]);
|
|
417
489
|
const resolved = typeof configImp === "function" ? configImp(app) : configImp;
|
|
418
|
-
const { plugins, ...config } = (resolved ?? {}) as
|
|
419
|
-
|
|
490
|
+
const { plugins, ...config } = (resolved ?? {}) as AppConfigDeclaration & { plugins?: AkanPlugin[] };
|
|
491
|
+
const libContributions = await AkanAppConfig.#collectLibContributions(app, libs, bustImportCache);
|
|
492
|
+
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libContributions);
|
|
493
|
+
}
|
|
494
|
+
//* Every workspace lib is read, not just this app's lib deps: narrowing the set needs the dependency
|
|
495
|
+
//* scan, and the incremental page rebundle re-reads this config on every file change.
|
|
496
|
+
static async #collectLibContributions(app: App, libs: string[], bustImportCache: boolean): Promise<LibContributions> {
|
|
497
|
+
const libConfigs = await Promise.all(
|
|
498
|
+
libs.map(async (libName) =>
|
|
499
|
+
LibExecutor.from(app, libName)
|
|
500
|
+
.getConfig({ refresh: bustImportCache })
|
|
501
|
+
.catch((error: unknown) => {
|
|
502
|
+
app.logger.warn(`Skipped libs/${libName}/akan.config.ts contributions: ${String(error)}`);
|
|
503
|
+
return null;
|
|
504
|
+
}),
|
|
505
|
+
),
|
|
506
|
+
);
|
|
507
|
+
return {
|
|
508
|
+
externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
|
|
509
|
+
docker: {
|
|
510
|
+
preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
|
|
511
|
+
postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? []),
|
|
512
|
+
},
|
|
513
|
+
//* A lib writes the glob against its own `public/`; `akan sync` mounts that at `public/libs/<lib>`.
|
|
514
|
+
keepFonts: libConfigs.flatMap((libConfig) =>
|
|
515
|
+
(libConfig?.assets.keepFonts ?? []).map((glob) => `libs/${libConfig?.lib.name}/${glob}`),
|
|
516
|
+
),
|
|
517
|
+
};
|
|
420
518
|
}
|
|
421
519
|
#resolveProductionDependencyVersion(lib: string) {
|
|
422
520
|
const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
|
|
@@ -428,13 +526,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
428
526
|
return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
429
527
|
}
|
|
430
528
|
#getProductionRuntimePackages() {
|
|
431
|
-
return [
|
|
432
|
-
...this.externalLibs,
|
|
433
|
-
...SSR_RUNTIME_PACKAGES,
|
|
434
|
-
...NATIVE_RUNTIME_PACKAGES,
|
|
435
|
-
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
436
|
-
...this.getDatabaseModeRuntimePackages(),
|
|
437
|
-
];
|
|
529
|
+
return [...this.externalLibs, ...SSR_RUNTIME_PACKAGES, ...this.getDatabaseModeRuntimePackages()];
|
|
438
530
|
}
|
|
439
531
|
getDatabaseModeRuntimePackages(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
|
|
440
532
|
return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
|
|
@@ -532,15 +624,19 @@ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfi
|
|
|
532
624
|
export class AkanLibConfig implements LibConfigResult {
|
|
533
625
|
lib: Lib;
|
|
534
626
|
externalLibs: string[];
|
|
627
|
+
docker: LibDockerConfig;
|
|
628
|
+
assets: LibAssetsConfig;
|
|
535
629
|
/** Live-only: plugins declared in this lib's `akan.config.ts` (never serialized). */
|
|
536
630
|
plugins: AkanPlugin[];
|
|
537
631
|
constructor(lib: Lib, config: DeepPartial<LibConfigResult>, plugins: AkanPlugin[] = []) {
|
|
538
632
|
this.lib = lib;
|
|
539
633
|
this.externalLibs = config?.externalLibs ?? [];
|
|
634
|
+
this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
|
|
635
|
+
this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts as string[] | undefined) };
|
|
540
636
|
this.plugins = plugins;
|
|
541
637
|
}
|
|
542
|
-
static async from(lib: Lib) {
|
|
543
|
-
const
|
|
638
|
+
static async from(lib: Lib, { bustImportCache = false }: { bustImportCache?: boolean } = {}) {
|
|
639
|
+
const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
|
|
544
640
|
const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
|
|
545
641
|
const { plugins, ...config } = (resolved ?? {}) as DeepPartial<LibConfigResult> & { plugins?: AkanPlugin[] };
|
|
546
642
|
return new AkanLibConfig(lib, config, plugins ?? []);
|
package/akanConfig/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type {
|
|
2
|
+
AkanApiConfig,
|
|
3
|
+
AkanAssetsConfig,
|
|
2
4
|
AkanConfigFile,
|
|
3
5
|
AkanExecutor,
|
|
4
6
|
AkanMobileConfig,
|
|
@@ -9,6 +11,8 @@ export type {
|
|
|
9
11
|
AkanRouteConfig,
|
|
10
12
|
AkanScanInfo,
|
|
11
13
|
AkanSyncContext,
|
|
14
|
+
AkanWebConfig,
|
|
15
|
+
AkanWebOption,
|
|
12
16
|
AppConfig,
|
|
13
17
|
AppConfigInput,
|
|
14
18
|
AppConfigResult,
|
|
@@ -17,10 +21,15 @@ export type {
|
|
|
17
21
|
DatabaseMode,
|
|
18
22
|
DeepPartial,
|
|
19
23
|
DockerConfig,
|
|
24
|
+
DockerImageConfig,
|
|
25
|
+
DockerOption,
|
|
26
|
+
DockerRun,
|
|
20
27
|
FileConventionScanResult,
|
|
28
|
+
LibAssetsConfig,
|
|
21
29
|
LibConfig,
|
|
22
30
|
LibConfigInput,
|
|
23
31
|
LibConfigResult,
|
|
32
|
+
LibDockerConfig,
|
|
24
33
|
LibScanResult,
|
|
25
34
|
MobileEnv,
|
|
26
35
|
MobilePermission,
|