@akanjs/devkit 3.0.0-beta.9 → 3.0.0
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 +21 -7
- package/README.md +17 -8
- 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 +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -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 +270 -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 +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -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 +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -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 +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- 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
package/executors.test.ts
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, readFile, readlink, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { AkanAppConfig } from "./akanConfig";
|
|
6
6
|
import { AppExecutor, CommandExecutionError, Executor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
7
7
|
import { AppInfo } from "./scanInfo";
|
|
8
|
+
import { isolateEnv, tempDirs, writeJson, writeText } from "./testHelpers";
|
|
8
9
|
import type { PackageJson } from "./types";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const makeTempRoot = async () => {
|
|
14
|
-
const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-"));
|
|
15
|
-
tempRoots.push(root);
|
|
16
|
-
return root;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const writeJson = async (filePath: string, value: object) => {
|
|
20
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
21
|
-
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
22
|
-
};
|
|
11
|
+
isolateEnv();
|
|
12
|
+
const makeTempRoot = tempDirs("akan-devkit-");
|
|
23
13
|
|
|
24
14
|
const PAGE_SOURCE = "export default function Page() {\n return null;\n}\n";
|
|
25
15
|
|
|
@@ -39,49 +29,34 @@ const rootPackageJson = (extra: Partial<PackageJson> = {}): PackageJson => ({
|
|
|
39
29
|
...extra,
|
|
40
30
|
});
|
|
41
31
|
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
process.env = { ...originalEnv };
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
afterEach(async () => {
|
|
47
|
-
process.env = { ...originalEnv };
|
|
48
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
49
|
-
});
|
|
50
|
-
|
|
51
32
|
describe("Executor filesystem helpers", () => {
|
|
52
33
|
test("reports command failures with command context and captured output", async () => {
|
|
53
34
|
const root = await makeTempRoot();
|
|
54
35
|
const exec = new Executor("fixture", root);
|
|
55
36
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
} catch (caught) {
|
|
60
|
-
error = caught;
|
|
61
|
-
}
|
|
62
|
-
|
|
37
|
+
const error = await exec
|
|
38
|
+
.spawn(process.execPath, ["--eval", "console.error('spawn failed'); process.exit(7)"])
|
|
39
|
+
.catch((caught: unknown) => caught);
|
|
63
40
|
expect(error).toBeInstanceOf(CommandExecutionError);
|
|
64
|
-
|
|
65
|
-
expect(
|
|
66
|
-
expect(
|
|
67
|
-
expect(
|
|
41
|
+
const { message } = error as CommandExecutionError;
|
|
42
|
+
expect(message).toContain(`Command failed: ${process.execPath}`);
|
|
43
|
+
expect(message).toContain(`cwd: ${root}`);
|
|
44
|
+
expect(message).toContain("exit code: 7");
|
|
45
|
+
expect(message).toContain("spawn failed");
|
|
68
46
|
});
|
|
69
47
|
|
|
70
48
|
test("reports inherited stdio command failures with a fallback message", async () => {
|
|
71
49
|
const root = await makeTempRoot();
|
|
72
50
|
const exec = new Executor("fixture", root);
|
|
73
51
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} catch (caught) {
|
|
78
|
-
error = caught;
|
|
79
|
-
}
|
|
80
|
-
|
|
52
|
+
const error = await exec
|
|
53
|
+
.spawn(process.execPath, ["--eval", "process.exit(3)"], { stdio: "inherit" })
|
|
54
|
+
.catch((caught: unknown) => caught);
|
|
81
55
|
expect(error).toBeInstanceOf(CommandExecutionError);
|
|
82
|
-
|
|
83
|
-
expect(
|
|
84
|
-
expect(
|
|
56
|
+
const { message } = error as CommandExecutionError;
|
|
57
|
+
expect(message).toContain(`Command failed: ${process.execPath}`);
|
|
58
|
+
expect(message).toContain(`cwd: ${root}`);
|
|
59
|
+
expect(message).toContain("exit code: 3");
|
|
85
60
|
});
|
|
86
61
|
|
|
87
62
|
test("resolves paths and reads/writes files relative to cwd", async () => {
|
|
@@ -105,6 +80,31 @@ describe("Executor filesystem helpers", () => {
|
|
|
105
80
|
expect(entries.dirs).toContain("nested");
|
|
106
81
|
});
|
|
107
82
|
|
|
83
|
+
test("a refreshed tsconfig re-reads the config it extends", async () => {
|
|
84
|
+
const root = await makeTempRoot();
|
|
85
|
+
const appDir = path.join(root, "apps/demo");
|
|
86
|
+
await writeJson(path.join(root, "tsconfig.json"), {
|
|
87
|
+
compilerOptions: { target: "es2022", paths: { "@libs/*": ["libs/*"] } },
|
|
88
|
+
});
|
|
89
|
+
await writeJson(path.join(appDir, "tsconfig.json"), {
|
|
90
|
+
extends: "../../tsconfig.json",
|
|
91
|
+
compilerOptions: { target: "esnext" },
|
|
92
|
+
references: [{ path: "../shared" }],
|
|
93
|
+
});
|
|
94
|
+
const exec = new Executor("fixture", appDir);
|
|
95
|
+
expect(await exec.getTsConfig()).toEqual({
|
|
96
|
+
extends: "../../tsconfig.json",
|
|
97
|
+
compilerOptions: { target: "esnext", paths: { "@libs/*": ["libs/*"] } },
|
|
98
|
+
references: [{ path: "../shared" }],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await writeJson(path.join(root, "tsconfig.json"), { compilerOptions: { target: "es2020" } });
|
|
102
|
+
await writeJson(path.join(appDir, "tsconfig.json"), { extends: "../../tsconfig.json", compilerOptions: {} });
|
|
103
|
+
const refreshed = { extends: "../../tsconfig.json", compilerOptions: { target: "es2020" } };
|
|
104
|
+
expect(await exec.getTsConfig("tsconfig.json", { refresh: true })).toEqual(refreshed);
|
|
105
|
+
expect(await new Executor("fixture", appDir).getTsConfig()).toEqual(refreshed);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
108
|
test("applies CLI template files with dictionary replacement and overwrite control", async () => {
|
|
109
109
|
const root = await makeTempRoot();
|
|
110
110
|
const exec = new Executor("fixture", root);
|
|
@@ -128,9 +128,7 @@ describe("Executor filesystem helpers", () => {
|
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
test("hands every scaffolded TypeScript file to the formatter, and nothing else", async () => {
|
|
131
|
-
// A template
|
|
132
|
-
// named Task and wrong for Zoo — and unsorted imports fail `biome check`. So the scaffold is formatted
|
|
133
|
-
// on the way out, or it is red for most model names whatever the template says.
|
|
131
|
+
// A template cannot sort imports that depend on the model name, and unsorted imports fail `biome check`.
|
|
134
132
|
const root = await makeTempRoot();
|
|
135
133
|
const exec = new Executor("fixture", root);
|
|
136
134
|
const formatted: string[] = [];
|
|
@@ -152,8 +150,7 @@ describe("Executor filesystem helpers", () => {
|
|
|
152
150
|
});
|
|
153
151
|
|
|
154
152
|
test("a formatter that cannot run does not fail the scaffold", async () => {
|
|
155
|
-
//
|
|
156
|
-
// no config above the target. An unformatted file is a lint fix; a failed scaffold is not.
|
|
153
|
+
// create-akan-workspace scaffolds before `bun install`, so Biome may be absent; a lint fix beats a failed scaffold.
|
|
157
154
|
const root = await makeTempRoot();
|
|
158
155
|
const exec = new Executor("fixture", root);
|
|
159
156
|
exec.getLinter = () => {
|
|
@@ -191,8 +188,7 @@ describe("Executor filesystem helpers", () => {
|
|
|
191
188
|
"AI Development Guide",
|
|
192
189
|
);
|
|
193
190
|
expect(await readFile(path.join(root, "workspace/docs/GENERATED.md"), "utf8")).toContain("Generated Akan Files");
|
|
194
|
-
// Rules
|
|
195
|
-
// existing workspace on `bun update` — the workspace file only extends it and scopes its own files.
|
|
191
|
+
// Rules live in the package's base config, so a framework release reaches an existing workspace on `bun update`.
|
|
196
192
|
expect(await readFile(path.join(root, "workspace/biome.json"), "utf8")).toContain(
|
|
197
193
|
'"extends": ["@akanjs/devkit/biome.base.json"]',
|
|
198
194
|
);
|
|
@@ -312,11 +308,18 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
312
308
|
expect(env.AKAN_PUBLIC_SERVER_PORT).toBe("8285");
|
|
313
309
|
expect(env.EXTRA).toBe("ok");
|
|
314
310
|
|
|
311
|
+
const shellOperationMode = process.env.AKAN_PUBLIC_OPERATION_MODE;
|
|
312
|
+
process.env.AKAN_PUBLIC_OPERATION_MODE = "local";
|
|
315
313
|
const prepared = await app.prepareCommand("build");
|
|
314
|
+
const bakedOperationMode = process.env.AKAN_PUBLIC_OPERATION_MODE;
|
|
315
|
+
if (shellOperationMode === undefined) delete process.env.AKAN_PUBLIC_OPERATION_MODE;
|
|
316
|
+
else process.env.AKAN_PUBLIC_OPERATION_MODE = shellOperationMode;
|
|
317
|
+
expect(bakedOperationMode).toBeUndefined();
|
|
316
318
|
expect(prepared.env.AKAN_COMMAND_TYPE).toBe("build");
|
|
317
319
|
expect(prepared.env.AKAN_PUBLIC_BASE_PATHS).toBe("admin");
|
|
318
|
-
|
|
319
|
-
|
|
320
|
+
expect(prepared.env.AKAN_DATABASE_MODE).toBe("single");
|
|
321
|
+
expect(prepared.env.AKAN_DATABASE_MODES).toBe("single");
|
|
322
|
+
// Bundling bakes each AKAN_PUBLIC_* into a literal, so a dev port published here would outrank the container PORT.
|
|
320
323
|
expect(process.env.AKAN_PUBLIC_APP_NAME).toBe("demo");
|
|
321
324
|
expect(process.env.AKAN_PUBLIC_CLIENT_PORT).toBeUndefined();
|
|
322
325
|
expect(process.env.AKAN_PUBLIC_SERVER_PORT).toBeUndefined();
|
|
@@ -324,6 +327,38 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
324
327
|
expect((await stat(path.join(root, "dist/apps/demo/public"))).isDirectory()).toBe(true);
|
|
325
328
|
});
|
|
326
329
|
|
|
330
|
+
test("akan start clears the dev output and keeps native builds, update releases and the bin downloads", async () => {
|
|
331
|
+
const root = await makeTempRoot();
|
|
332
|
+
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
333
|
+
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
334
|
+
process.env.AKAN_PUBLIC_ENV = "local";
|
|
335
|
+
process.env.PORT_OFFSET = "0";
|
|
336
|
+
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
337
|
+
await mkdir(path.join(root, "apps/startclean/page"), { recursive: true });
|
|
338
|
+
await writeFile(path.join(root, "apps/startclean/akan.config.ts"), "export default {};\n");
|
|
339
|
+
const akan = path.join(root, "apps/startclean/.akan");
|
|
340
|
+
const kept = [
|
|
341
|
+
"native/desktop/updates/macos-arm64/main.json",
|
|
342
|
+
"native/desktop/build/macos/App.app",
|
|
343
|
+
"cache/bin/ffmpeg",
|
|
344
|
+
];
|
|
345
|
+
const removed = [
|
|
346
|
+
"artifact/client/app.js",
|
|
347
|
+
"generated/dict/index.ts",
|
|
348
|
+
"cache/cssCandidates.json",
|
|
349
|
+
"desktop/server/main.js",
|
|
350
|
+
"mobile/desktop/native/macos/App.app",
|
|
351
|
+
];
|
|
352
|
+
for (const file of [...kept, ...removed]) await writeText(path.join(akan, file), "x");
|
|
353
|
+
|
|
354
|
+
const app = AppExecutor.from(new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" }), "startclean");
|
|
355
|
+
await app.prepareCommand("start");
|
|
356
|
+
|
|
357
|
+
for (const file of kept) expect(existsSync(path.join(akan, file))).toBe(true);
|
|
358
|
+
for (const file of [...removed, "artifact", "generated", "desktop", "mobile"])
|
|
359
|
+
expect(existsSync(path.join(akan, file))).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
327
362
|
describe("syncPages", () => {
|
|
328
363
|
// `AppExecutor.from` memoises by name, so each test needs a name no other test has used.
|
|
329
364
|
const makeAppWithLibPages = async (
|
|
@@ -452,7 +487,6 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
452
487
|
});
|
|
453
488
|
|
|
454
489
|
describe("syncAssets", () => {
|
|
455
|
-
// `AppExecutor.from` memoises by name, so each test needs a name no other test has used.
|
|
456
490
|
const makeAppWithLibAssets = async (appName: string) => {
|
|
457
491
|
const root = await makeTempRoot();
|
|
458
492
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
@@ -527,7 +561,6 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
527
561
|
});
|
|
528
562
|
|
|
529
563
|
describe("devOnly routes", () => {
|
|
530
|
-
// `AppExecutor.from` memoises by name, so each test needs a name no other test has used.
|
|
531
564
|
const makeAppWithRoutes = async (appName: string, routes: Record<string, string>) => {
|
|
532
565
|
const root = await makeTempRoot();
|
|
533
566
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
@@ -621,7 +654,6 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
621
654
|
await mkdir(path.join(root, "apps", name), { recursive: true });
|
|
622
655
|
await writeFile(path.join(root, "apps", name, "akan.config.ts"), "export default {};\n");
|
|
623
656
|
}
|
|
624
|
-
// `AppExecutor.from` memoises by name, so each test needs names no other test has used.
|
|
625
657
|
return new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
626
658
|
};
|
|
627
659
|
|
|
@@ -637,8 +669,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
637
669
|
const app = AppExecutor.from(workspace, "drift-b");
|
|
638
670
|
expect(await app.getDevPort()).toBe(8282);
|
|
639
671
|
|
|
640
|
-
// Sorts ahead of `drift-b`, so the same app
|
|
641
|
-
// recomputes this on every restart.
|
|
672
|
+
// Sorts ahead of `drift-b`, so the same app gets another port — and a dev host recomputes it on each restart.
|
|
642
673
|
await mkdir(path.join(workspace.workspaceRoot, "apps/drift-a"), { recursive: true });
|
|
643
674
|
await writeFile(path.join(workspace.workspaceRoot, "apps/drift-a/akan.config.ts"), "export default {};\n");
|
|
644
675
|
|
|
@@ -727,7 +758,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
727
758
|
});
|
|
728
759
|
});
|
|
729
760
|
|
|
730
|
-
test("accepts
|
|
761
|
+
test("accepts head route exports during page key discovery", async () => {
|
|
731
762
|
const root = await makeTempRoot();
|
|
732
763
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
733
764
|
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
@@ -737,24 +768,19 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
737
768
|
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
738
769
|
await writeFile(
|
|
739
770
|
path.join(root, "apps/demo/page/_layout.tsx"),
|
|
740
|
-
[
|
|
741
|
-
"export const head = null;",
|
|
742
|
-
"export const metadata = { title: 'Root' };",
|
|
743
|
-
"export default function Layout({ children }) { return children; }",
|
|
744
|
-
"",
|
|
745
|
-
].join("\n"),
|
|
771
|
+
["export const head = null;", "export default function Layout({ children }) { return children; }", ""].join("\n"),
|
|
746
772
|
);
|
|
747
773
|
await writeFile(
|
|
748
774
|
path.join(root, "apps/demo/page/docs/_layout.tsx"),
|
|
749
775
|
[
|
|
750
|
-
"export async function
|
|
776
|
+
"export async function generateHead() { return null; }",
|
|
751
777
|
"export default function Layout({ children }) { return children; }",
|
|
752
778
|
"",
|
|
753
779
|
].join("\n"),
|
|
754
780
|
);
|
|
755
781
|
await writeFile(
|
|
756
782
|
path.join(root, "apps/demo/page/docs/intro.tsx"),
|
|
757
|
-
["export const
|
|
783
|
+
["export const head = null;", "export default function Page() { return null; }", ""].join("\n"),
|
|
758
784
|
);
|
|
759
785
|
|
|
760
786
|
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
@@ -767,7 +793,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
767
793
|
]);
|
|
768
794
|
});
|
|
769
795
|
|
|
770
|
-
test("rejects
|
|
796
|
+
test("rejects a metadata route export during page key discovery", async () => {
|
|
771
797
|
const root = await makeTempRoot();
|
|
772
798
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
773
799
|
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
@@ -776,47 +802,16 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
776
802
|
await mkdir(path.join(root, "apps/demo/page"), { recursive: true });
|
|
777
803
|
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
778
804
|
await writeFile(
|
|
779
|
-
path.join(root, "apps/demo/page/
|
|
780
|
-
[
|
|
781
|
-
"
|
|
782
|
-
|
|
783
|
-
"export default function Page() { return null; }",
|
|
784
|
-
"",
|
|
785
|
-
].join("\n"),
|
|
805
|
+
path.join(root, "apps/demo/page/legacy.tsx"),
|
|
806
|
+
["export const metadata = { title: 'Legacy' };", "export default function Page() { return null; }", ""].join(
|
|
807
|
+
"\n",
|
|
808
|
+
),
|
|
786
809
|
);
|
|
787
810
|
|
|
788
811
|
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
789
812
|
const app = AppExecutor.from(workspace, "demo");
|
|
790
813
|
|
|
791
|
-
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
|
|
792
|
-
"metadata and generateMetadata cannot both be exported",
|
|
793
|
-
);
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
test("rejects mixed head and metadata route export channels during page key discovery", async () => {
|
|
797
|
-
const root = await makeTempRoot();
|
|
798
|
-
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
799
|
-
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
800
|
-
process.env.AKAN_PUBLIC_ENV = "local";
|
|
801
|
-
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
802
|
-
await mkdir(path.join(root, "apps/demo/page"), { recursive: true });
|
|
803
|
-
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
804
|
-
await writeFile(
|
|
805
|
-
path.join(root, "apps/demo/page/mixed.tsx"),
|
|
806
|
-
[
|
|
807
|
-
"export const head = null;",
|
|
808
|
-
"export const metadata = { title: 'Mixed' };",
|
|
809
|
-
"export default function Page() { return null; }",
|
|
810
|
-
"",
|
|
811
|
-
].join("\n"),
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
815
|
-
const app = AppExecutor.from(workspace, "demo");
|
|
816
|
-
|
|
817
|
-
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
|
|
818
|
-
"head/generateHead and metadata/generateMetadata cannot both be exported",
|
|
819
|
-
);
|
|
814
|
+
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow('unsupported export "metadata"');
|
|
820
815
|
});
|
|
821
816
|
|
|
822
817
|
test("assigns start command ports from sorted app order", async () => {
|
|
@@ -985,3 +980,78 @@ describe("scan info construction", () => {
|
|
|
985
980
|
expect(info.file.document.scalars.has("money")).toBe(true);
|
|
986
981
|
});
|
|
987
982
|
});
|
|
983
|
+
|
|
984
|
+
describe("WorkspaceExecutor listing", () => {
|
|
985
|
+
test("lists apps, libs and pkgs in name order", async () => {
|
|
986
|
+
const root = await makeTempRoot();
|
|
987
|
+
for (const app of ["zeta", "alpha", "mid"]) await writeText(path.join(root, "apps", app, "akan.config.ts"), "");
|
|
988
|
+
for (const lib of ["util", "shared"]) await writeText(path.join(root, "libs", lib, "akan.config.ts"), "");
|
|
989
|
+
for (const pkg of ["zeta-tool", "@sample/tool", "akanjs"])
|
|
990
|
+
await writeJson(path.join(root, "pkgs", pkg, "package.json"), { name: pkg });
|
|
991
|
+
|
|
992
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
993
|
+
expect(await workspace.getExecs()).toEqual([
|
|
994
|
+
["alpha", "mid", "zeta"],
|
|
995
|
+
["shared", "util"],
|
|
996
|
+
["@sample/tool", "akanjs", "zeta-tool"],
|
|
997
|
+
]);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("never takes a scratch workspace under a local/ folder for a package, app or lib", async () => {
|
|
1001
|
+
const root = await makeTempRoot();
|
|
1002
|
+
await writeJson(path.join(root, "pkgs/@akanjs/devkit/package.json"), { name: "@akanjs/devkit" });
|
|
1003
|
+
await writeJson(path.join(root, "pkgs/@akanjs/devkit/local/akan-cli-x/package.json"), { name: "repo" });
|
|
1004
|
+
await writeText(path.join(root, "apps/portal/akan.config.ts"), "");
|
|
1005
|
+
await writeText(path.join(root, "libs/util/local/akan-cli-y/akan.config.ts"), "");
|
|
1006
|
+
|
|
1007
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
1008
|
+
expect(await workspace.getExecs()).toEqual([["portal"], [], ["@akanjs/devkit"]]);
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
describe("SysExecutor module listing", () => {
|
|
1013
|
+
test("lists only the module folders that hold the module's own file", async () => {
|
|
1014
|
+
const root = await makeTempRoot();
|
|
1015
|
+
const lib = path.join(root, "apps/modlist/lib");
|
|
1016
|
+
for (const file of [
|
|
1017
|
+
"cnst.ts",
|
|
1018
|
+
"post/post.constant.ts",
|
|
1019
|
+
"post/Post.View.tsx",
|
|
1020
|
+
"post/Post.Unit.tsx",
|
|
1021
|
+
"draft/draft.document.ts",
|
|
1022
|
+
"_auth/auth.service.ts",
|
|
1023
|
+
"_notes/notes.md",
|
|
1024
|
+
"__scalar/money/money.constant.ts",
|
|
1025
|
+
"__scalar/stale/stale.dictionary.ts",
|
|
1026
|
+
])
|
|
1027
|
+
await writeText(path.join(lib, file), "export {};\n");
|
|
1028
|
+
|
|
1029
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
1030
|
+
const app = AppExecutor.from(workspace, "modlist");
|
|
1031
|
+
expect(await app.getDatabaseModules()).toEqual(["post"]);
|
|
1032
|
+
expect(await app.getServiceModules()).toEqual(["_auth"]);
|
|
1033
|
+
expect(await app.getScalarModules()).toEqual(["money"]);
|
|
1034
|
+
expect(await app.getViewComponents()).toEqual(["post"]);
|
|
1035
|
+
expect(await app.getUnitComponents()).toEqual(["post"]);
|
|
1036
|
+
expect(await app.getTemplateComponents()).toEqual([]);
|
|
1037
|
+
expect((await app.getViewsSourceCode()).map(({ filePath }) => filePath)).toEqual([
|
|
1038
|
+
"apps/modlist/lib/post/Post.View.tsx",
|
|
1039
|
+
]);
|
|
1040
|
+
expect((await app.getScalarConstantFiles()).map(({ filePath }) => filePath)).toEqual([
|
|
1041
|
+
"apps/modlist/lib/__scalar/money/money.constant.ts",
|
|
1042
|
+
]);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
test("reads scalar dictionaries from the scalar folder", async () => {
|
|
1046
|
+
const root = await makeTempRoot();
|
|
1047
|
+
const scalarDir = path.join(root, "apps/scalardict/lib/__scalar/money");
|
|
1048
|
+
await writeText(path.join(scalarDir, "money.constant.ts"), "export {};\n");
|
|
1049
|
+
await writeText(path.join(scalarDir, "money.dictionary.ts"), "export const money = {};\n");
|
|
1050
|
+
|
|
1051
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
1052
|
+
const app = AppExecutor.from(workspace, "scalardict");
|
|
1053
|
+
expect(await app.getScalarDictionaryFiles()).toEqual([
|
|
1054
|
+
{ filePath: "apps/scalardict/lib/__scalar/money/money.dictionary.ts", content: "export const money = {};\n" },
|
|
1055
|
+
]);
|
|
1056
|
+
});
|
|
1057
|
+
});
|