@akanjs/devkit 3.0.0-beta.3 → 3.0.0-beta.31
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/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +2 -2
- package/README.md +3 -2
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +427 -141
- package/akanConfig/akanConfig.ts +327 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +21 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +269 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +177 -0
- package/mobile/desktopBin.ts +143 -0
- package/mobile/desktopServerStage.test.ts +204 -0
- package/mobile/desktopServerStage.ts +138 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +177 -0
- package/mobile/nativeApp.ts +416 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +414 -0
- package/mobile/nativeConfig.ts +293 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +30 -25
- package/subspace.ts +81 -115
- package/subspaceConfig.ts +11 -9
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AkanAppConfig } from "../akanConfig";
|
|
5
|
+
import type { App } from "../commandDecorators";
|
|
6
|
+
import { tempDirs, writeText as write } from "../testHelpers";
|
|
7
|
+
import type { PackageJson } from "../types";
|
|
8
|
+
import { DesktopServerStage } from "./desktopServerStage";
|
|
9
|
+
import { NativeApi } from "./nativeApi";
|
|
10
|
+
import { NativeApp } from "./nativeApp";
|
|
11
|
+
import { NativeConfig } from "./nativeConfig";
|
|
12
|
+
|
|
13
|
+
const makeTempRoot = tempDirs("akan-desktop-server-");
|
|
14
|
+
const repoApp = path.resolve(import.meta.dir, "../../../../apps/minimal");
|
|
15
|
+
const baseDevEnv = {
|
|
16
|
+
repoName: "akanjs",
|
|
17
|
+
serveDomain: "akanjs.com",
|
|
18
|
+
env: "debug" as const,
|
|
19
|
+
portOffset: 0,
|
|
20
|
+
workspaceRoot: "/w",
|
|
21
|
+
};
|
|
22
|
+
const rootPackageJson: PackageJson = {
|
|
23
|
+
name: "repo",
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
description: "repo",
|
|
26
|
+
dependencies: { "@external/runtime": "2.0.0", pg: "8.0.0" },
|
|
27
|
+
};
|
|
28
|
+
const appConfig = (config: object = {}) =>
|
|
29
|
+
new AkanAppConfig({ name: "portal" } as never, [], rootPackageJson, config, baseDevEnv);
|
|
30
|
+
|
|
31
|
+
describe("DesktopServerStage", () => {
|
|
32
|
+
test("starts from the image's env, as an API-only edge server on SQLite that the launcher gives a port", () => {
|
|
33
|
+
const env = DesktopServerStage.env(appConfig({ database: { modes: ["single", "cluster"] } }), "main");
|
|
34
|
+
|
|
35
|
+
expect(env).toMatchObject({
|
|
36
|
+
NODE_ENV: "production",
|
|
37
|
+
AKAN_PUBLIC_APP_NAME: "portal",
|
|
38
|
+
AKAN_PUBLIC_ENV: "main",
|
|
39
|
+
AKAN_PUBLIC_OPERATION_MODE: "edge",
|
|
40
|
+
AKAN_DATABASE_MODE: "single",
|
|
41
|
+
AKAN_DATABASE_MODES: "single",
|
|
42
|
+
AKAN_SSR: "false",
|
|
43
|
+
AKAN_CSR: "false",
|
|
44
|
+
AKAN_MCP: "false",
|
|
45
|
+
AKAN_SHUTDOWN_TIMEOUT_MS: "1000",
|
|
46
|
+
});
|
|
47
|
+
expect(env.PORT).toBeUndefined();
|
|
48
|
+
expect(env.AKAN_LOG_TO_FILE).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("installs what the image does, minus the RSC renderer and the other modes' drivers", () => {
|
|
52
|
+
const config = appConfig({ database: { modes: ["single", "cluster"] }, externalLibs: ["@external/runtime"] });
|
|
53
|
+
const built = config.getProductionPackageJson();
|
|
54
|
+
const drivers = config.getDatabaseModeRuntimePackages("cluster");
|
|
55
|
+
|
|
56
|
+
const { dependencies } = DesktopServerStage.packageJson(config, built);
|
|
57
|
+
|
|
58
|
+
expect(Object.keys(dependencies ?? {}).sort()).toEqual(["@external/runtime", "react", "react-dom"]);
|
|
59
|
+
for (const driver of drivers) expect(Object.keys(built.dependencies ?? {})).toContain(driver);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("keeps the image's trusted packages, so their install scripts run in the app's server too", () => {
|
|
63
|
+
const config = appConfig({ trustedDependencies: ["@external/runtime"], externalLibs: ["@external/runtime"] });
|
|
64
|
+
expect(DesktopServerStage.packageJson(config, config.getProductionPackageJson()).trustedDependencies).toEqual([
|
|
65
|
+
"@external/runtime",
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("says what the image installs that the desktop app's server goes without", () => {
|
|
70
|
+
const notice = (docker: unknown, carried: string[] = []) =>
|
|
71
|
+
DesktopServerStage.imageStepsNotice({ app: { name: "portal" }, docker } as never, carried);
|
|
72
|
+
const steps = { image: "oven/bun", preRuns: ["apt-get install -y ffmpeg"], postRuns: [], command: [] };
|
|
73
|
+
expect(notice({ ...steps, preRuns: [] })).toBeNull();
|
|
74
|
+
expect(notice(steps)).toEqual({
|
|
75
|
+
level: "warn",
|
|
76
|
+
message: expect.stringContaining(
|
|
77
|
+
"The image runs 1 docker step from the app and its libs, and a desktop app's server runs none of them",
|
|
78
|
+
),
|
|
79
|
+
});
|
|
80
|
+
expect(notice("FROM ros:humble-ros-core")?.message).toStartWith(
|
|
81
|
+
"apps/portal/akan.config.ts writes its own Dockerfile",
|
|
82
|
+
);
|
|
83
|
+
expect(notice(steps, ["ffmpeg"])).toEqual({
|
|
84
|
+
level: "info",
|
|
85
|
+
message: expect.stringContaining("carries only bin: ffmpeg"),
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const stageApp = async (spawn: (...args: unknown[]) => Promise<string> = async () => "") => {
|
|
90
|
+
const root = await makeTempRoot();
|
|
91
|
+
const dist = path.join(root, "dist/apps/portal");
|
|
92
|
+
const config = appConfig();
|
|
93
|
+
const app = {
|
|
94
|
+
cwdPath: path.join(root, "apps/portal"),
|
|
95
|
+
dist: { cwdPath: dist },
|
|
96
|
+
getConfig: async () => config,
|
|
97
|
+
getScanInfo: () => ({ getLibs: () => [] }),
|
|
98
|
+
logger: { info: () => undefined, warn: () => undefined },
|
|
99
|
+
spawn,
|
|
100
|
+
} as unknown as App;
|
|
101
|
+
return { dist, app, stage: new DesktopServerStage(app) };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
test("carries everything the backend build wrote beside main.js but what only the image reads", async () => {
|
|
105
|
+
const { dist, stage } = await stageApp();
|
|
106
|
+
const carried = [
|
|
107
|
+
"main.js",
|
|
108
|
+
"server.js",
|
|
109
|
+
"chunk-5eap2n9b.js",
|
|
110
|
+
"akan.build.json",
|
|
111
|
+
"better-sqlite3-a1b2c3d4.node",
|
|
112
|
+
"engine-e5f6a7b8.wasm",
|
|
113
|
+
"model-c9d0e1f2.bin",
|
|
114
|
+
"private/service-account.json",
|
|
115
|
+
];
|
|
116
|
+
const imageOnly = [
|
|
117
|
+
"Dockerfile",
|
|
118
|
+
"package.json",
|
|
119
|
+
"rscWorker.js",
|
|
120
|
+
"console.js",
|
|
121
|
+
"console-runtime.js",
|
|
122
|
+
"csr/index.html",
|
|
123
|
+
"public/favicon.ico",
|
|
124
|
+
".akan/artifact/base-artifact.json",
|
|
125
|
+
];
|
|
126
|
+
for (const file of [...carried, ...imageOnly]) await write(path.join(dist, file), file);
|
|
127
|
+
|
|
128
|
+
await stage.prepare("main");
|
|
129
|
+
|
|
130
|
+
const staged = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: stage.dir, dot: true }));
|
|
131
|
+
expect(staged.map((file) => file.split(path.sep).join("/")).sort()).toEqual(carried.sort());
|
|
132
|
+
expect(await Bun.file(path.join(stage.dir, "better-sqlite3-a1b2c3d4.node")).text()).toBe(
|
|
133
|
+
"better-sqlite3-a1b2c3d4.node",
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("installs from the config alone, offline when Bun's cache holds the packages", async () => {
|
|
138
|
+
const spawned: unknown[][] = [];
|
|
139
|
+
const { stage } = await stageApp(async (...args) => {
|
|
140
|
+
spawned.push(args);
|
|
141
|
+
return "";
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await stage.install();
|
|
145
|
+
|
|
146
|
+
expect(spawned).toEqual([[process.execPath, ["install", "--production", "--prefer-offline"], { cwd: stage.dir }]]);
|
|
147
|
+
const installed = (await Bun.file(path.join(stage.dir, "package.json")).json()) as PackageJson;
|
|
148
|
+
expect(Object.keys(installed.dependencies ?? {}).sort()).toEqual(["react", "react-dom"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("an install that fails says it needs the registry or a Bun cache that holds the packages", async () => {
|
|
152
|
+
const { stage } = await stageApp(async () => {
|
|
153
|
+
throw new Error("error: GET https://registry.npmjs.org/scheduler - ConnectionRefused");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const failed = stage.install();
|
|
157
|
+
|
|
158
|
+
await expect(failed).rejects.toThrow(
|
|
159
|
+
"it needs the npm registry, or a Bun cache that already holds every one of them.\nerror: GET https://registry.npmjs.org/scheduler",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("refuses an app whose database modes leave out single", () => {
|
|
164
|
+
expect(() => DesktopServerStage.assertCarriable(appConfig({ database: { modes: ["cluster"] } }))).toThrow(
|
|
165
|
+
"only database mode single runs",
|
|
166
|
+
);
|
|
167
|
+
expect(() =>
|
|
168
|
+
DesktopServerStage.assertCarriable(appConfig({ database: { modes: ["single", "cluster"] } })),
|
|
169
|
+
).not.toThrow();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("the runtime accepts a desktop config that carries the server, with single-instance on", async () => {
|
|
173
|
+
const root = await makeTempRoot();
|
|
174
|
+
await mkdir(path.join(root, "web"), { recursive: true });
|
|
175
|
+
await writeFile(path.join(root, "web/index.html"), "<html><head></head><body></body></html>");
|
|
176
|
+
await mkdir(path.join(root, "server"), { recursive: true });
|
|
177
|
+
await writeFile(path.join(root, "server/main.js"), "");
|
|
178
|
+
const server = { dir: path.join(root, "server"), entry: "main.js", env: { AKAN_PUBLIC_ENV: "main" } };
|
|
179
|
+
|
|
180
|
+
const { config } = NativeConfig.build({
|
|
181
|
+
appPath: root,
|
|
182
|
+
target: { name: "default", appName: "Portal", appId: "com.portal.app", version: "1.0.0", buildNum: 1 },
|
|
183
|
+
webDir: path.join(root, "web"),
|
|
184
|
+
contributions: [],
|
|
185
|
+
locales: ["en"],
|
|
186
|
+
platform: "macos",
|
|
187
|
+
desktopServer: server,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(config.plugins).toContain("single-instance");
|
|
191
|
+
expect(config.desktop).toEqual({ server });
|
|
192
|
+
expect((await NativeApi.load(repoApp)).validateConfig(config, { appDir: root })).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("a phone build cannot carry the server", async () => {
|
|
196
|
+
const app = new NativeApp({ cwdPath: "/repo/apps/portal" } as never, {
|
|
197
|
+
name: "default",
|
|
198
|
+
config: { name: "default", appName: "Portal", appId: "com.portal.app", version: "1.0.0", buildNum: 1 },
|
|
199
|
+
});
|
|
200
|
+
const server = { dir: "/tmp/server", entry: "main.js", env: {} };
|
|
201
|
+
await expect(app.build("ios", { server })).rejects.toThrow("Only a desktop app carries its server");
|
|
202
|
+
await expect(app.run("android", { server })).rejects.toThrow("Only a desktop app carries its server");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { AkanAppConfig, NativeEnv } from "../akanConfig";
|
|
4
|
+
import type { App } from "../commandDecorators";
|
|
5
|
+
import type { PackageJson } from "../types";
|
|
6
|
+
import { DesktopBin } from "./desktopBin";
|
|
7
|
+
|
|
8
|
+
/** What a desktop build hands the native runtime as `desktop.server`. */
|
|
9
|
+
export interface DesktopServerBundle {
|
|
10
|
+
dir: string;
|
|
11
|
+
entry: string;
|
|
12
|
+
env: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//* A desktop app's carried server is the backend `akan build` wrote into dist, installed on its own as the image's
|
|
16
|
+
//* `bun install --production` does. It is staged outside dist: installing there would put host packages into the
|
|
17
|
+
//* image's `COPY . .`.
|
|
18
|
+
export class DesktopServerStage {
|
|
19
|
+
//? An API-only server starts neither the RSC worker nor the akan console and serves no CSR, public/ or SSR artifact,
|
|
20
|
+
//? and `install` wrote its own package.json. The rest of dist is the server's: its chunks, a `.node`, a `.wasm`.
|
|
21
|
+
static readonly uncarried = new Set([
|
|
22
|
+
"Dockerfile",
|
|
23
|
+
"package.json",
|
|
24
|
+
"rscWorker.js",
|
|
25
|
+
"console.js",
|
|
26
|
+
"console-runtime.js",
|
|
27
|
+
"csr",
|
|
28
|
+
"public",
|
|
29
|
+
".akan",
|
|
30
|
+
]);
|
|
31
|
+
//? Only the RSC worker loads it, and its peer webpack makes 35 of the 43 MB an install would add.
|
|
32
|
+
static readonly rscRenderer = "react-server-dom-webpack";
|
|
33
|
+
|
|
34
|
+
readonly dir: string;
|
|
35
|
+
|
|
36
|
+
constructor(readonly app: App) {
|
|
37
|
+
this.dir = path.join(app.cwdPath, ".akan", "desktop", "server");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static assertCarriable(config: AkanAppConfig) {
|
|
41
|
+
if (!config.database.modes.includes("single"))
|
|
42
|
+
throw new Error(
|
|
43
|
+
`desktop.server in native puts the server in the app, where only database mode single runs (no Redis or Postgres); apps/${config.app.name}/akan.config.ts declares ${config.database.modes.join(", ")}.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static env(config: AkanAppConfig, environment: NativeEnv): Record<string, string> {
|
|
48
|
+
//? The launcher picks the port, and the log file stays on: a user's computer has no log collector.
|
|
49
|
+
const { PORT: _port, AKAN_LOG_TO_FILE: _fileLog, ...image } = config.getProductionEnv(environment);
|
|
50
|
+
return {
|
|
51
|
+
...image,
|
|
52
|
+
//? Neither the developer's machine (local, which derives a forgeable JWT secret) nor a deployment (cloud).
|
|
53
|
+
AKAN_PUBLIC_OPERATION_MODE: "edge",
|
|
54
|
+
AKAN_DATABASE_MODE: "single",
|
|
55
|
+
AKAN_DATABASE_MODES: "single",
|
|
56
|
+
//? API only: the shell serves the CSR bundle, and an RSC worker would need a bun on the user's PATH.
|
|
57
|
+
AKAN_SSR: "false",
|
|
58
|
+
AKAN_CSR: "false",
|
|
59
|
+
AKAN_MCP: "false",
|
|
60
|
+
//? Ahead of the launcher's 1.5 s grace before it signals, which sits inside the shell's 2 s quit budget.
|
|
61
|
+
AKAN_SHUTDOWN_TIMEOUT_MS: "1000",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//? The image's steps install into a Linux image the desktop app never runs in, so its server has none of it.
|
|
66
|
+
static imageStepsNotice(
|
|
67
|
+
config: Pick<AkanAppConfig, "app" | "docker">,
|
|
68
|
+
carried: string[],
|
|
69
|
+
): { level: "warn" | "info"; message: string } | null {
|
|
70
|
+
const steps =
|
|
71
|
+
typeof config.docker === "string" ? null : config.docker.preRuns.length + config.docker.postRuns.length;
|
|
72
|
+
if (steps === 0) return null;
|
|
73
|
+
const image =
|
|
74
|
+
steps === null
|
|
75
|
+
? `apps/${config.app.name}/akan.config.ts writes its own Dockerfile`
|
|
76
|
+
: `The image runs ${steps} docker step${steps === 1 ? "" : "s"} from the app and its libs`;
|
|
77
|
+
return carried.length
|
|
78
|
+
? {
|
|
79
|
+
level: "info",
|
|
80
|
+
message: `${image}; the desktop app's server runs none of them and carries only bin: ${carried.join(", ")}.`,
|
|
81
|
+
}
|
|
82
|
+
: {
|
|
83
|
+
level: "warn",
|
|
84
|
+
message: `${image}, and a desktop app's server runs none of them: declare in bin the executables its code spawns, or those calls fail on the user's computer.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static packageJson(config: AkanAppConfig, built: PackageJson): PackageJson {
|
|
89
|
+
const otherDrivers = new Set(
|
|
90
|
+
config.database.modes
|
|
91
|
+
.filter((mode) => mode !== "single")
|
|
92
|
+
.flatMap((mode) => config.getDatabaseModeRuntimePackages(mode)),
|
|
93
|
+
);
|
|
94
|
+
const dependencies = Object.entries(built.dependencies ?? {}).filter(
|
|
95
|
+
([name]) =>
|
|
96
|
+
config.externalLibs.includes(name) || (name !== DesktopServerStage.rscRenderer && !otherDrivers.has(name)),
|
|
97
|
+
);
|
|
98
|
+
return { ...built, dependencies: Object.fromEntries(dependencies) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//* The packages come from the config alone, so they install before `akan build` runs: a machine that cannot install
|
|
102
|
+
//* them stops at once instead of after the whole build.
|
|
103
|
+
async install() {
|
|
104
|
+
const config = await this.app.getConfig();
|
|
105
|
+
DesktopServerStage.assertCarriable(config);
|
|
106
|
+
await rm(this.dir, { recursive: true, force: true });
|
|
107
|
+
await mkdir(this.dir, { recursive: true });
|
|
108
|
+
await writeFile(
|
|
109
|
+
path.join(this.dir, "package.json"),
|
|
110
|
+
JSON.stringify(DesktopServerStage.packageJson(config, config.getProductionPackageJson()), null, 2),
|
|
111
|
+
);
|
|
112
|
+
try {
|
|
113
|
+
await this.app.spawn(process.execPath, ["install", "--production", "--prefer-offline"], { cwd: this.dir });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`The desktop app's server could not install its packages in ${this.dir}: it needs the npm registry, or a Bun cache that already holds every one of them.\n${error instanceof Error ? error.message : String(error)}`,
|
|
117
|
+
{ cause: error },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
//? Launcher links only: a link out of the app bundle breaks its signature, and the server runs none of them.
|
|
121
|
+
await rm(path.join(this.dir, "node_modules", ".bin"), { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//? Copies the build beside what `install` put in the stage, so it runs after both.
|
|
125
|
+
async prepare(environment: NativeEnv): Promise<DesktopServerBundle> {
|
|
126
|
+
const config = await this.app.getConfig();
|
|
127
|
+
const dist = this.app.dist.cwdPath;
|
|
128
|
+
await mkdir(this.dir, { recursive: true });
|
|
129
|
+
for (const name of await readdir(dist))
|
|
130
|
+
if (!DesktopServerStage.uncarried.has(name))
|
|
131
|
+
await cp(path.join(dist, name), path.join(this.dir, name), { recursive: true });
|
|
132
|
+
const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? (await this.app.scan({ write: false }));
|
|
133
|
+
const carried = [...DesktopBin.select(config, scanInfo.getLibs()).keys()];
|
|
134
|
+
const notice = DesktopServerStage.imageStepsNotice(config, carried);
|
|
135
|
+
if (notice) this.app.logger[notice.level](notice.message);
|
|
136
|
+
return { dir: this.dir, entry: "main.js", env: DesktopServerStage.env(config, environment) };
|
|
137
|
+
}
|
|
138
|
+
}
|
package/mobile/index.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
+
export * from "./desktopBin";
|
|
2
|
+
export * from "./desktopServerStage";
|
|
1
3
|
export * from "./mobileTarget";
|
|
4
|
+
export * from "./nativeApi";
|
|
5
|
+
export * from "./nativeApp";
|
|
6
|
+
export * from "./nativeConfig";
|
|
7
|
+
export * from "./nativePluginFolders";
|
|
8
|
+
export * from "./nativeWebDir";
|
|
9
|
+
export * from "./placeholderAppId";
|
|
10
|
+
export * from "./usageDescriptions";
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AkanNativeTarget } from "../akanConfig";
|
|
3
3
|
import type { App } from "../commandDecorators";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
appIdsOf,
|
|
6
|
+
getMobileTargetChoices,
|
|
7
|
+
resolveAppId,
|
|
8
|
+
resolveMobilePath,
|
|
9
|
+
resolveMobileTargets,
|
|
10
|
+
targetHtmlFilename,
|
|
11
|
+
} from "./mobileTarget";
|
|
5
12
|
|
|
6
13
|
const target = {
|
|
7
14
|
name: "akanjs",
|
|
@@ -10,25 +17,36 @@ const target = {
|
|
|
10
17
|
appId: "com.akanjs.app",
|
|
11
18
|
version: "1.0.0",
|
|
12
19
|
buildNum: 1,
|
|
13
|
-
} as
|
|
20
|
+
} as AkanNativeTarget;
|
|
14
21
|
|
|
15
|
-
describe("
|
|
22
|
+
describe("native target helpers", () => {
|
|
16
23
|
test("maps deep links into target base paths without duplicating prefixes", () => {
|
|
17
24
|
expect(resolveMobilePath(target, "/order/123")).toBe("/akanjs/order/123");
|
|
18
25
|
expect(resolveMobilePath(target, "/akanjs/order/123")).toBe("/akanjs/order/123");
|
|
19
26
|
expect(resolveMobilePath({ ...target, basePath: undefined }, "/order/123")).toBe("/order/123");
|
|
20
27
|
});
|
|
21
28
|
|
|
29
|
+
test("an app id per platform falls back to default, and a platform with neither is refused", () => {
|
|
30
|
+
const appId = { default: "com.yeollege", ios: "com.puffinplanet.yeollege" };
|
|
31
|
+
expect(resolveAppId(appId, "ios")).toBe("com.puffinplanet.yeollege");
|
|
32
|
+
expect(resolveAppId(appId, "android")).toBe("com.yeollege");
|
|
33
|
+
expect(resolveAppId("com.one.app", "macos")).toBe("com.one.app");
|
|
34
|
+
expect(() => resolveAppId({ ios: "com.puffinplanet.yeollege" }, "android")).toThrow(
|
|
35
|
+
"native.appId names no id for android",
|
|
36
|
+
);
|
|
37
|
+
expect(appIdsOf(appId)).toEqual(["com.yeollege", "com.puffinplanet.yeollege"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
22
40
|
test("selects the self-contained html file for a target", () => {
|
|
23
41
|
expect(targetHtmlFilename(target)).toBe("akanjs.html");
|
|
24
42
|
expect(targetHtmlFilename({ ...target, basePath: undefined })).toBe("index.html");
|
|
25
43
|
});
|
|
26
44
|
|
|
27
|
-
test("uses the configured
|
|
45
|
+
test("uses the configured native target as the only choice when one target is configured", async () => {
|
|
28
46
|
const app = {
|
|
29
47
|
getConfig: async () => ({
|
|
30
48
|
basePaths: new Set(["akanjs", "soft", "office"]),
|
|
31
|
-
|
|
49
|
+
native: { targets: { akanjs: target } },
|
|
32
50
|
}),
|
|
33
51
|
} as unknown as App;
|
|
34
52
|
|
|
@@ -36,11 +54,42 @@ describe("mobile target helpers", () => {
|
|
|
36
54
|
await expect(resolveMobileTargets(app, undefined)).resolves.toEqual([{ name: "akanjs", config: target }]);
|
|
37
55
|
});
|
|
38
56
|
|
|
39
|
-
test("
|
|
57
|
+
test("a target naming no basePath is a template in an app with basePaths, never a page of its own", async () => {
|
|
58
|
+
const template = { ...target, name: "default", basePath: undefined };
|
|
59
|
+
const appWith = (basePaths: string[]) =>
|
|
60
|
+
({
|
|
61
|
+
name: "angelo",
|
|
62
|
+
getConfig: async () => ({ basePaths: new Set(basePaths), native: { targets: { default: template } } }),
|
|
63
|
+
}) as unknown as App;
|
|
64
|
+
|
|
65
|
+
const two = appWith(["soft", "office"]);
|
|
66
|
+
await expect(getMobileTargetChoices(two)).resolves.toEqual(["soft", "office"]);
|
|
67
|
+
await expect(resolveMobileTargets(two, undefined)).rejects.toThrow(
|
|
68
|
+
"Multiple native targets found for angelo. Pass --target <soft|office|all>.",
|
|
69
|
+
);
|
|
70
|
+
await expect(resolveMobileTargets(two, "office")).resolves.toEqual([
|
|
71
|
+
{ name: "office", config: { ...template, name: "office", basePath: "office" } },
|
|
72
|
+
]);
|
|
73
|
+
await expect(resolveMobileTargets(two, "all")).resolves.toEqual([
|
|
74
|
+
{ name: "soft", config: { ...template, name: "soft", basePath: "soft" } },
|
|
75
|
+
{ name: "office", config: { ...template, name: "office", basePath: "office" } },
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const one = appWith(["soft"]);
|
|
79
|
+
const soft = [{ name: "soft", config: { ...template, name: "soft", basePath: "soft" } }];
|
|
80
|
+
await expect(resolveMobileTargets(one, undefined)).resolves.toEqual(soft);
|
|
81
|
+
await expect(resolveMobileTargets(one, "all")).resolves.toEqual(soft);
|
|
82
|
+
|
|
83
|
+
await expect(resolveMobileTargets(appWith([]), undefined)).resolves.toEqual([
|
|
84
|
+
{ name: "default", config: template },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("resolves a route base path onto the default native target config", async () => {
|
|
40
89
|
const app = {
|
|
41
90
|
getConfig: async () => ({
|
|
42
91
|
basePaths: new Set(["akanjs", "soft", "office"]),
|
|
43
|
-
|
|
92
|
+
native: { targets: { akanjs: target } },
|
|
44
93
|
}),
|
|
45
94
|
} as unknown as App;
|
|
46
95
|
|
package/mobile/mobileTarget.ts
CHANGED
|
@@ -1,45 +1,52 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AkanNativeAppId } from "akanjs";
|
|
2
|
+
import type { AkanAppConfig, AkanNativeTarget, NativeEnv } from "../akanConfig";
|
|
2
3
|
import type { App } from "../commandDecorators";
|
|
3
4
|
|
|
4
5
|
export type MobilePlatform = "ios" | "android";
|
|
6
|
+
export type DesktopPlatform = "macos" | "windows" | "linux";
|
|
7
|
+
export type NativePlatform = MobilePlatform | DesktopPlatform;
|
|
5
8
|
export type MobileTargetSelection = string | "all" | undefined;
|
|
6
9
|
|
|
7
10
|
export interface ResolvedMobileTarget {
|
|
8
11
|
name: string;
|
|
9
|
-
config:
|
|
12
|
+
config: AkanNativeTarget;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly
|
|
15
|
+
export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly NativeEnv[];
|
|
16
|
+
|
|
17
|
+
const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, "");
|
|
13
18
|
|
|
14
19
|
export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]> => {
|
|
15
20
|
const config = await app.getConfig();
|
|
16
|
-
return Object.entries(config.
|
|
21
|
+
return Object.entries(config.native.targets).map(([name, target]) => ({ name, config: target }));
|
|
17
22
|
};
|
|
18
23
|
|
|
24
|
+
//* An app with basePaths builds a CSR page per basePath and none at its root, so a target naming no basePath opens
|
|
25
|
+
//* nothing on its own there; it is the template each basePath's target is made from.
|
|
26
|
+
const isTemplateOnly = ({ basePaths, native }: Pick<AkanAppConfig, "basePaths" | "native">) =>
|
|
27
|
+
basePaths.size > 0 && Object.values(native.targets).every((target) => !target.basePath);
|
|
28
|
+
|
|
19
29
|
export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
|
|
20
30
|
const config = await app.getConfig();
|
|
21
|
-
const targetNames = Object.keys(config.
|
|
22
|
-
if (targetNames.length > 0) return targetNames;
|
|
23
|
-
|
|
24
|
-
return basePaths;
|
|
31
|
+
const targetNames = Object.keys(config.native.targets);
|
|
32
|
+
if (targetNames.length > 0 && !isTemplateOnly(config)) return targetNames;
|
|
33
|
+
return [...config.basePaths];
|
|
25
34
|
};
|
|
26
35
|
|
|
27
36
|
const resolveMobileTargetByBasePath = (
|
|
28
37
|
targets: ResolvedMobileTarget[],
|
|
29
38
|
basePath: string,
|
|
30
39
|
): ResolvedMobileTarget | undefined => {
|
|
31
|
-
const normalizedBasePath = basePath
|
|
32
|
-
const byBasePath = targets.find(
|
|
40
|
+
const normalizedBasePath = trimSlashes(basePath);
|
|
41
|
+
const byBasePath = targets.find(
|
|
42
|
+
({ config }) => config.basePath !== undefined && trimSlashes(config.basePath) === normalizedBasePath,
|
|
43
|
+
);
|
|
33
44
|
if (byBasePath) return byBasePath;
|
|
34
45
|
const [template] = targets;
|
|
35
46
|
if (!template) return undefined;
|
|
36
47
|
return {
|
|
37
48
|
name: normalizedBasePath,
|
|
38
|
-
config: {
|
|
39
|
-
...template.config,
|
|
40
|
-
name: normalizedBasePath,
|
|
41
|
-
basePath: normalizedBasePath,
|
|
42
|
-
},
|
|
49
|
+
config: { ...template.config, name: normalizedBasePath, basePath: normalizedBasePath },
|
|
43
50
|
};
|
|
44
51
|
};
|
|
45
52
|
|
|
@@ -49,17 +56,17 @@ export const resolveMobileTargets = async (
|
|
|
49
56
|
): Promise<ResolvedMobileTarget[]> => {
|
|
50
57
|
const config = await app.getConfig();
|
|
51
58
|
const targets = await getMobileTargets(app);
|
|
52
|
-
if (targets.length === 0) throw new Error(`No
|
|
53
|
-
if (!selection && targets.length === 1) return targets;
|
|
59
|
+
if (targets.length === 0) throw new Error(`No native targets configured for ${app.name}`);
|
|
60
|
+
if (!selection && targets.length === 1 && !isTemplateOnly(config)) return targets;
|
|
54
61
|
if (!selection) {
|
|
55
62
|
const choices = await getMobileTargetChoices(app);
|
|
56
63
|
if (choices.length === 1) return resolveMobileTargets(app, choices[0]);
|
|
57
|
-
throw new Error(`Multiple
|
|
64
|
+
throw new Error(`Multiple native targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
|
|
58
65
|
}
|
|
59
66
|
if (selection === "all") {
|
|
60
|
-
if (Object.keys(config.
|
|
67
|
+
if (Object.keys(config.native.targets).length > 1) return targets;
|
|
61
68
|
const basePaths = [...config.basePaths];
|
|
62
|
-
if (basePaths.length > 1) {
|
|
69
|
+
if (basePaths.length > 1 || isTemplateOnly(config)) {
|
|
63
70
|
return basePaths.flatMap((basePath) => {
|
|
64
71
|
const resolved = resolveMobileTargetByBasePath(targets, basePath);
|
|
65
72
|
return resolved ? [resolved] : [];
|
|
@@ -70,18 +77,30 @@ export const resolveMobileTargets = async (
|
|
|
70
77
|
const target = targets.find((candidate) => candidate.name === selection);
|
|
71
78
|
if (target) return [target];
|
|
72
79
|
const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
|
|
73
|
-
if (basePathTarget && config.basePaths.has(selection
|
|
80
|
+
if (basePathTarget && config.basePaths.has(trimSlashes(selection))) return [basePathTarget];
|
|
74
81
|
const choices = await getMobileTargetChoices(app);
|
|
75
|
-
throw new Error(`
|
|
82
|
+
throw new Error(`Native target '${selection}' was not found. Available: ${choices.join(", ")}`);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const resolveAppId = (appId: AkanNativeAppId, platform: NativePlatform): string => {
|
|
86
|
+
if (typeof appId === "string") return appId;
|
|
87
|
+
const id = appId[platform] ?? appId.default;
|
|
88
|
+
if (!id) throw new Error(`native.appId names no id for ${platform}; give it ${platform} or default.`);
|
|
89
|
+
return id;
|
|
76
90
|
};
|
|
77
91
|
|
|
78
|
-
export const
|
|
79
|
-
|
|
92
|
+
export const appIdsOf = (appId: AkanNativeAppId): string[] =>
|
|
93
|
+
typeof appId === "string" ? [appId] : Object.values(appId).filter((id): id is string => !!id);
|
|
94
|
+
|
|
95
|
+
export const resolveMobilePath = (target: AkanNativeTarget, pathname: string) => {
|
|
96
|
+
const basePath = trimSlashes(target.basePath ?? "");
|
|
80
97
|
const normalizedPath = `/${pathname.replace(/^\/+/, "")}`;
|
|
81
98
|
if (!basePath) return normalizedPath;
|
|
82
99
|
if (normalizedPath === `/${basePath}` || normalizedPath.startsWith(`/${basePath}/`)) return normalizedPath;
|
|
83
100
|
return `/${basePath}${normalizedPath === "/" ? "" : normalizedPath}`;
|
|
84
101
|
};
|
|
85
102
|
|
|
86
|
-
export const targetHtmlFilename = (target:
|
|
87
|
-
|
|
103
|
+
export const targetHtmlFilename = (target: AkanNativeTarget) => {
|
|
104
|
+
const basePath = trimSlashes(target.basePath ?? "");
|
|
105
|
+
return basePath ? `${basePath}.html` : "index.html";
|
|
106
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, realpath, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tempDirs } from "../testHelpers";
|
|
5
|
+
import { NativeApi } from "./nativeApi";
|
|
6
|
+
|
|
7
|
+
const makeTempRoot = tempDirs("akan-native-api-");
|
|
8
|
+
const repoApp = path.resolve(import.meta.dir, "../../../../apps/minimal");
|
|
9
|
+
|
|
10
|
+
describe("NativeApi", () => {
|
|
11
|
+
test("inside this repo, loads the workspace package and checks its major", async () => {
|
|
12
|
+
expect(NativeApi.entryOf(repoApp)).toBe(path.resolve(import.meta.dir, "../../native/packages/cli/src/api.ts"));
|
|
13
|
+
const api = await NativeApi.load(repoApp);
|
|
14
|
+
expect(api.API_VERSION.split(".")[0]).toBe(String(NativeApi.supportedMajor));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("elsewhere, looks for the copy akanjs vendors, and says so when it is missing", async () => {
|
|
18
|
+
const root = await realpath(await makeTempRoot());
|
|
19
|
+
await mkdir(path.join(root, "node_modules/akanjs"), { recursive: true });
|
|
20
|
+
await writeFile(
|
|
21
|
+
path.join(root, "node_modules/akanjs/package.json"),
|
|
22
|
+
JSON.stringify({ name: "akanjs", exports: { "./package.json": "./package.json" } }),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(NativeApi.entryOf(root)).toBe(
|
|
26
|
+
path.join(root, "node_modules/akanjs/vendor/@akanjs/native/packages/cli/src/api.ts"),
|
|
27
|
+
);
|
|
28
|
+
await expect(NativeApi.load(root)).rejects.toThrow("The installed akanjs has no native build");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type * as NativeBuildApi from "@akanjs/native/api";
|
|
3
|
+
|
|
4
|
+
export type NativeBuildApiModule = typeof NativeBuildApi;
|
|
5
|
+
|
|
6
|
+
export class NativeApi {
|
|
7
|
+
//* The build API versions itself; a major this devkit was not written against is refused before anything builds.
|
|
8
|
+
static readonly supportedMajor = 0;
|
|
9
|
+
|
|
10
|
+
/** The API of the akanjs the app installed: the workspace package inside this repo, akanjs's vendored copy elsewhere. */
|
|
11
|
+
static entryOf(appDir: string): string {
|
|
12
|
+
const workspaceEntry = NativeApi.#resolve("@akanjs/native/api", appDir);
|
|
13
|
+
if (workspaceEntry) return workspaceEntry;
|
|
14
|
+
const akanjsPackage = NativeApi.#resolve("akanjs/package.json", appDir);
|
|
15
|
+
if (!akanjsPackage) throw new Error(`akanjs is not installed for ${appDir}; the native build ships inside it.`);
|
|
16
|
+
return path.join(path.dirname(akanjsPackage), "vendor", "@akanjs", "native", "packages", "cli", "src", "api.ts");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static async load(appDir: string): Promise<NativeBuildApiModule> {
|
|
20
|
+
const entry = NativeApi.entryOf(appDir);
|
|
21
|
+
if (!(await Bun.file(entry).exists()))
|
|
22
|
+
throw new Error(`The installed akanjs has no native build at ${entry}; update akanjs.`);
|
|
23
|
+
const api = (await import(entry)) as NativeBuildApiModule;
|
|
24
|
+
const major = Number(api.API_VERSION.split(".")[0]);
|
|
25
|
+
if (major !== NativeApi.supportedMajor)
|
|
26
|
+
throw new Error(
|
|
27
|
+
`The native build API is ${api.API_VERSION}, and this akan CLI speaks ${NativeApi.supportedMajor}.x; install matching versions of akanjs and @akanjs/cli.`,
|
|
28
|
+
);
|
|
29
|
+
return api;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static #resolve(specifier: string, from: string) {
|
|
33
|
+
try {
|
|
34
|
+
return Bun.resolveSync(specifier, from);
|
|
35
|
+
} catch {
|
|
36
|
+
// Not installed where the app can reach it, which the caller answers with the next place to look.
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|