@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.20
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/README.md +1 -0
- 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 +1 -3
- package/akanApp/akanApp.host.ts +82 -254
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +85 -69
- package/akanConfig/akanConfig.ts +69 -153
- package/akanConfig/types.ts +1 -0
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +5 -3
- package/applicationBuildRunner.ts +54 -61
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +9 -30
- package/artifact/implicitRootLayout.ts +34 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +6 -1
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +32 -16
- package/capacitorApp.ts +110 -238
- 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 +70 -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/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 +132 -106
- package/executors.ts +194 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +6 -8
- 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 +62 -37
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- 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/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 +46 -69
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +8 -32
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +23 -46
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- 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 +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- 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/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
- package/index.ts +5 -17
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- 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/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +10 -5
- 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 +197 -285
- 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 +87 -216
- 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 +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/types.ts +1 -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.ts +4 -19
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- 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/scanInfo.ts
CHANGED
|
@@ -27,20 +27,13 @@ const serviceFileTypes = [
|
|
|
27
27
|
"zone",
|
|
28
28
|
] as const;
|
|
29
29
|
type ServiceFileType = (typeof serviceFileTypes)[number];
|
|
30
|
-
const databaseFileTypes = [
|
|
31
|
-
"constant",
|
|
32
|
-
"dictionary",
|
|
33
|
-
"document",
|
|
34
|
-
"service",
|
|
35
|
-
"signal",
|
|
36
|
-
"store",
|
|
37
|
-
"template",
|
|
38
|
-
"unit",
|
|
39
|
-
"util",
|
|
40
|
-
"view",
|
|
41
|
-
"zone",
|
|
42
|
-
] as const;
|
|
30
|
+
const databaseFileTypes = ["constant", "dictionary", "document", ...serviceFileTypes.slice(1)] as const;
|
|
43
31
|
type DatabaseFileType = (typeof databaseFileTypes)[number];
|
|
32
|
+
const uiFileTypes = new Set<DatabaseFileType>(["template", "unit", "util", "view", "zone"]);
|
|
33
|
+
const fileTypeOf = (filename: string) =>
|
|
34
|
+
databaseFileTypes.find((type) =>
|
|
35
|
+
filename.endsWith(uiFileTypes.has(type) ? `.${type[0].toUpperCase()}${type.slice(1)}.tsx` : `.${type}.ts`),
|
|
36
|
+
);
|
|
44
37
|
|
|
45
38
|
type ModuleKind = "database" | "service" | "scalar";
|
|
46
39
|
|
|
@@ -56,14 +49,11 @@ const moduleUiFileTypes = {
|
|
|
56
49
|
service: new Set(["Util", "Zone"]),
|
|
57
50
|
scalar: new Set(["Template", "Unit"]),
|
|
58
51
|
} satisfies Record<ModuleKind, Set<string>>;
|
|
59
|
-
const testFilePattern = /\.(test|spec)\.(ts|tsx)$/;
|
|
60
52
|
|
|
61
|
-
//
|
|
62
|
-
// load it on demand rather than through the module graph of every process that imports scan info.
|
|
53
|
+
// Lazy: the dependency scanner pulls in `typescript` (~65MB resident), which only scan and sync need.
|
|
63
54
|
const createDependencyScanner = async (exec: AppExecutor | LibExecutor | PkgExecutor) =>
|
|
64
55
|
(await import("./dependencyScanner")).TypeScriptDependencyScanner.from(exec);
|
|
65
56
|
|
|
66
|
-
const isAllowedTestFile = (filename: string) => testFilePattern.test(filename);
|
|
67
57
|
const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
|
|
68
58
|
path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
|
|
69
59
|
async function clearGeneratedRootCapacitorConfigs(exec: AppExecutor | LibExecutor) {
|
|
@@ -85,33 +75,21 @@ async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: {
|
|
|
85
75
|
const allowedRootFiles: ReadonlySet<string> = rootAllowedFiles[exec.type];
|
|
86
76
|
const allowedRootDirs: ReadonlySet<string> = rootAllowedDirs[exec.type];
|
|
87
77
|
const { files, dirs } = await exec.getFilesAndDirs(".");
|
|
88
|
-
files
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
dirs
|
|
94
|
-
.filter((dirname) => !allowedRootDirs.has(dirname))
|
|
95
|
-
.forEach((dirname) => {
|
|
96
|
-
addViolation(dirname, `unsupported ${exec.type} root folder`);
|
|
97
|
-
});
|
|
78
|
+
for (const filename of files)
|
|
79
|
+
if (!allowedRootFiles.has(filename)) addViolation(filename, `unsupported ${exec.type} root file`);
|
|
80
|
+
for (const dirname of dirs)
|
|
81
|
+
if (!allowedRootDirs.has(dirname)) addViolation(dirname, `unsupported ${exec.type} root folder`);
|
|
98
82
|
|
|
99
|
-
//*
|
|
100
|
-
//* routes went unchecked until whichever app opted into them with `syncPageLibs` blew up on the sync.
|
|
83
|
+
//* A lib has no `getPageKeys`, so its own route files are validated here.
|
|
101
84
|
if (exec.type === "lib")
|
|
102
85
|
for (const { relativePath, reason } of await exec.getPageConventionViolations()) addViolation(relativePath, reason);
|
|
103
86
|
|
|
104
|
-
libRoot.files
|
|
105
|
-
|
|
106
|
-
.forEach((filename) => {
|
|
87
|
+
for (const filename of libRoot.files)
|
|
88
|
+
if (!isAllowedLibFacetRootFile(filename))
|
|
107
89
|
addViolation(path.join("lib", filename), "unsupported lib facet root file");
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
libRoot.dirs
|
|
111
|
-
.filter((dirname) => dirname.startsWith("__") && !internalLibDirs.has(dirname))
|
|
112
|
-
.forEach((dirname) => {
|
|
90
|
+
for (const dirname of libRoot.dirs)
|
|
91
|
+
if (dirname.startsWith("__") && !internalLibDirs.has(dirname))
|
|
113
92
|
addViolation(path.join("lib", dirname), "unsupported internal lib folder");
|
|
114
|
-
});
|
|
115
93
|
|
|
116
94
|
const databaseDirs = libRoot.dirs.filter((dirname) => !dirname.startsWith("_"));
|
|
117
95
|
const serviceDirs = libRoot.dirs.filter((dirname) => dirname.startsWith("_") && !dirname.startsWith("__"));
|
|
@@ -146,43 +124,23 @@ async function validateModuleFiles(
|
|
|
146
124
|
|
|
147
125
|
const uiModuleName = moduleName[0].toUpperCase() + moduleName.slice(1);
|
|
148
126
|
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
if (filename === "index.ts" || filename === "index.tsx" ||
|
|
152
|
-
if (filename === `${moduleName}.abstract.md`)
|
|
153
|
-
|
|
127
|
+
for (const filename of files) {
|
|
128
|
+
const scanPath = getScanPath(exec, path.join(modulePath, filename));
|
|
129
|
+
if (filename === "index.ts" || filename === "index.tsx" || /\.(test|spec)\.(ts|tsx)$/.test(filename)) continue;
|
|
130
|
+
if (filename === `${moduleName}.abstract.md`) continue;
|
|
154
131
|
const uiMatch = filename.match(/^([A-Z][A-Za-z0-9]+)\.([A-Z][A-Za-z0-9]*)\.tsx$/);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
violations.push(
|
|
160
|
-
`${getScanPath(exec, filePath)}: module name mismatch: expected '${uiModuleName}', got '${fileModuleName}'`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
if (!moduleUiFileTypes[kind].has(fileType)) {
|
|
164
|
-
violations.push(`${getScanPath(exec, filePath)}: unsupported ${kind} UI file`);
|
|
165
|
-
}
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const nonUiMatch = filename.match(/^([a-z][a-zA-Z0-9]*)\.([a-z][a-z0-9]*)\.ts$/);
|
|
170
|
-
if (nonUiMatch) {
|
|
171
|
-
const fileModuleName = nonUiMatch[1];
|
|
172
|
-
const fileType = nonUiMatch[2];
|
|
173
|
-
if (fileModuleName !== moduleName) {
|
|
174
|
-
violations.push(
|
|
175
|
-
`${getScanPath(exec, filePath)}: module name mismatch: expected '${moduleName}', got '${fileModuleName}'`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
if (!moduleNonUiFileTypes[kind].has(fileType)) {
|
|
179
|
-
violations.push(`${getScanPath(exec, filePath)}: unsupported ${kind} file`);
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
132
|
+
const match = uiMatch ?? filename.match(/^([a-z][a-zA-Z0-9]*)\.([a-z][a-z0-9]*)\.ts$/);
|
|
133
|
+
if (!match) {
|
|
134
|
+
violations.push(`${scanPath}: unsupported module file`);
|
|
135
|
+
continue;
|
|
182
136
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
137
|
+
const [, fileModuleName, fileType] = match;
|
|
138
|
+
const expectedName = uiMatch ? uiModuleName : moduleName;
|
|
139
|
+
if (fileModuleName !== expectedName)
|
|
140
|
+
violations.push(`${scanPath}: module name mismatch: expected '${expectedName}', got '${fileModuleName}'`);
|
|
141
|
+
if (!(uiMatch ? moduleUiFileTypes : moduleNonUiFileTypes)[kind].has(fileType))
|
|
142
|
+
violations.push(`${scanPath}: unsupported ${kind}${uiMatch ? " UI" : ""} file`);
|
|
143
|
+
}
|
|
186
144
|
}
|
|
187
145
|
|
|
188
146
|
class ScanInfo {
|
|
@@ -239,51 +197,16 @@ class ScanInfo {
|
|
|
239
197
|
} else databaseDirs.push(name);
|
|
240
198
|
});
|
|
241
199
|
|
|
200
|
+
const collect = async (dir: string, name: string, kind: "databases" | "services" | "scalars") => {
|
|
201
|
+
for (const filename of await exec.readdir(dir)) {
|
|
202
|
+
const type = fileTypeOf(filename);
|
|
203
|
+
if (type) (files[type] as { [key in typeof kind]?: string[] })[kind]?.push(name);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
242
206
|
await Promise.all([
|
|
243
|
-
...databaseDirs.map(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (filename.endsWith(".constant.ts")) files.constant.databases.push(name);
|
|
247
|
-
else if (filename.endsWith(".dictionary.ts")) files.dictionary.databases.push(name);
|
|
248
|
-
else if (filename.endsWith(".document.ts")) files.document.databases.push(name);
|
|
249
|
-
else if (filename.endsWith(".service.ts")) files.service.databases.push(name);
|
|
250
|
-
else if (filename.endsWith(".signal.ts")) files.signal.databases.push(name);
|
|
251
|
-
else if (filename.endsWith(".store.ts")) files.store.databases.push(name);
|
|
252
|
-
else if (filename.endsWith(".Template.tsx")) files.template.databases.push(name);
|
|
253
|
-
else if (filename.endsWith(".Unit.tsx")) files.unit.databases.push(name);
|
|
254
|
-
else if (filename.endsWith(".Util.tsx")) files.util.databases.push(name);
|
|
255
|
-
else if (filename.endsWith(".View.tsx")) files.view.databases.push(name);
|
|
256
|
-
else if (filename.endsWith(".Zone.tsx")) files.zone.databases.push(name);
|
|
257
|
-
});
|
|
258
|
-
}),
|
|
259
|
-
...serviceDirs.map(async (dirname) => {
|
|
260
|
-
const name = dirname.slice(1);
|
|
261
|
-
const filenames = await exec.readdir(path.join("lib", dirname));
|
|
262
|
-
filenames.forEach((filename) => {
|
|
263
|
-
if (filename.endsWith(".dictionary.ts")) files.dictionary.services.push(name);
|
|
264
|
-
else if (filename.endsWith(".service.ts")) files.service.services.push(name);
|
|
265
|
-
else if (filename.endsWith(".signal.ts")) files.signal.services.push(name);
|
|
266
|
-
else if (filename.endsWith(".store.ts")) files.store.services.push(name);
|
|
267
|
-
else if (filename.endsWith(".Template.tsx")) files.template.services.push(name);
|
|
268
|
-
else if (filename.endsWith(".Unit.tsx")) files.unit.services.push(name);
|
|
269
|
-
else if (filename.endsWith(".Util.tsx")) files.util.services.push(name);
|
|
270
|
-
else if (filename.endsWith(".View.tsx")) files.view.services.push(name);
|
|
271
|
-
else if (filename.endsWith(".Zone.tsx")) files.zone.services.push(name);
|
|
272
|
-
});
|
|
273
|
-
}),
|
|
274
|
-
...scalarDirs.map(async (name) => {
|
|
275
|
-
const filenames = await exec.readdir(path.join("lib/__scalar", name));
|
|
276
|
-
filenames.forEach((filename) => {
|
|
277
|
-
if (filename.endsWith(".constant.ts")) files.constant.scalars.push(name);
|
|
278
|
-
else if (filename.endsWith(".dictionary.ts")) files.dictionary.scalars.push(name);
|
|
279
|
-
else if (filename.endsWith(".document.ts")) files.document.scalars.push(name);
|
|
280
|
-
else if (filename.endsWith(".Template.tsx")) files.template.scalars.push(name);
|
|
281
|
-
else if (filename.endsWith(".Unit.tsx")) files.unit.scalars.push(name);
|
|
282
|
-
else if (filename.endsWith(".Util.tsx")) files.util.scalars.push(name);
|
|
283
|
-
else if (filename.endsWith(".View.tsx")) files.view.scalars.push(name);
|
|
284
|
-
else if (filename.endsWith(".Zone.tsx")) files.zone.scalars.push(name);
|
|
285
|
-
});
|
|
286
|
-
}),
|
|
207
|
+
...databaseDirs.map((name) => collect(path.join("lib", name), name, "databases")),
|
|
208
|
+
...serviceDirs.map((dirname) => collect(path.join("lib", dirname), dirname.slice(1), "services")),
|
|
209
|
+
...scalarDirs.map((name) => collect(path.join("lib/__scalar", name), name, "scalars")),
|
|
287
210
|
]);
|
|
288
211
|
const routes = exec.type === "lib" ? [] : await (exec as AppExecutor).getPageKeys();
|
|
289
212
|
const common = {
|
|
@@ -309,35 +232,16 @@ class ScanInfo {
|
|
|
309
232
|
constructor(scanResult: ScanResult) {
|
|
310
233
|
this.name = scanResult.name;
|
|
311
234
|
this.scanResult = scanResult;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.database.set(modelName, model);
|
|
323
|
-
this.file[key].all.add(modelName);
|
|
324
|
-
this.file[key].databases.add(modelName);
|
|
325
|
-
});
|
|
326
|
-
services?.forEach((serviceName) => {
|
|
327
|
-
const service = this.service.get(serviceName) ?? new Set<ServiceFileType>();
|
|
328
|
-
service.add(key as ServiceFileType);
|
|
329
|
-
this.service.set(serviceName, service);
|
|
330
|
-
this.file[key].all.add(serviceName);
|
|
331
|
-
this.file[key].services.add(serviceName);
|
|
332
|
-
});
|
|
333
|
-
scalars?.forEach((scalarName) => {
|
|
334
|
-
const scalar = this.scalar.get(scalarName) ?? new Set<ScalarFileType>();
|
|
335
|
-
scalar.add(key as ScalarFileType);
|
|
336
|
-
this.scalar.set(scalarName, scalar);
|
|
337
|
-
this.file[key].all.add(scalarName);
|
|
338
|
-
this.file[key].scalars.add(scalarName);
|
|
339
|
-
});
|
|
340
|
-
});
|
|
235
|
+
const modulesOf = { databases: this.database, services: this.service, scalars: this.scalar } as {
|
|
236
|
+
[key in "databases" | "services" | "scalars"]: Map<string, Set<DatabaseFileType>>;
|
|
237
|
+
};
|
|
238
|
+
for (const [key, groups] of Object.entries(scanResult.files) as [DatabaseFileType, { [kind: string]: string[] }][])
|
|
239
|
+
for (const kind of ["databases", "services", "scalars"] as const)
|
|
240
|
+
for (const name of groups[kind] ?? []) {
|
|
241
|
+
modulesOf[kind].set(name, (modulesOf[kind].get(name) ?? new Set()).add(key));
|
|
242
|
+
this.file[key].all.add(name);
|
|
243
|
+
this.file[key][kind].add(name);
|
|
244
|
+
}
|
|
341
245
|
}
|
|
342
246
|
getScanResult() {
|
|
343
247
|
return this.scanResult;
|
|
@@ -351,6 +255,29 @@ class ScanInfo {
|
|
|
351
255
|
getScalarModules() {
|
|
352
256
|
return [...this.scalar.keys()];
|
|
353
257
|
}
|
|
258
|
+
|
|
259
|
+
#sortedLibs: string[] | null = null;
|
|
260
|
+
protected sortedLibs(libDeps: string[]) {
|
|
261
|
+
if (this.#sortedLibs) return this.#sortedLibs;
|
|
262
|
+
const libIndices = LibInfo.getSortedLibIndices();
|
|
263
|
+
this.#sortedLibs = libDeps.sort((libNameA, libNameB) => {
|
|
264
|
+
const indexA = libIndices.get(libNameA);
|
|
265
|
+
const indexB = libIndices.get(libNameB);
|
|
266
|
+
if (indexA === undefined || indexB === undefined)
|
|
267
|
+
throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
|
|
268
|
+
return indexA - indexB;
|
|
269
|
+
});
|
|
270
|
+
return this.#sortedLibs;
|
|
271
|
+
}
|
|
272
|
+
protected sortedLibInfos(libDeps: string[]) {
|
|
273
|
+
return new Map(
|
|
274
|
+
this.sortedLibs(libDeps).map((libName) => {
|
|
275
|
+
const libInfo = LibInfo.libInfos.get(libName);
|
|
276
|
+
if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
|
|
277
|
+
return [libName, libInfo];
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
354
281
|
}
|
|
355
282
|
|
|
356
283
|
const isAkanFrameworkDependency = (dep: string) => dep === "akanjs" || dep.startsWith("akanjs/");
|
|
@@ -362,7 +289,6 @@ export class AppInfo extends ScanInfo {
|
|
|
362
289
|
|
|
363
290
|
static appInfos = new Map<string, AppInfo>();
|
|
364
291
|
static async fromExecutor(exec: AppExecutor, options: { refresh?: boolean } = {}) {
|
|
365
|
-
// cache check
|
|
366
292
|
const existingAppInfo = AppInfo.appInfos.get(exec.name);
|
|
367
293
|
if (existingAppInfo && !options.refresh) return existingAppInfo;
|
|
368
294
|
const scanResult = await ScanInfo.getScanResult(exec);
|
|
@@ -408,30 +334,11 @@ export class AppInfo extends ScanInfo {
|
|
|
408
334
|
return [...libSet];
|
|
409
335
|
}
|
|
410
336
|
|
|
411
|
-
#sortedLibs: string[] | null = null;
|
|
412
|
-
#getSortedLibs() {
|
|
413
|
-
if (this.#sortedLibs) return this.#sortedLibs;
|
|
414
|
-
const libIndices = LibInfo.getSortedLibIndices();
|
|
415
|
-
this.#sortedLibs = this.libDeps.sort((libNameA, libNameB) => {
|
|
416
|
-
const indexA = libIndices.get(libNameA);
|
|
417
|
-
const indexB = libIndices.get(libNameB);
|
|
418
|
-
if (indexA === undefined || indexB === undefined)
|
|
419
|
-
throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
|
|
420
|
-
return indexA - indexB;
|
|
421
|
-
});
|
|
422
|
-
return this.#sortedLibs;
|
|
423
|
-
}
|
|
424
337
|
getLibs() {
|
|
425
|
-
return this
|
|
338
|
+
return this.sortedLibs(this.libDeps);
|
|
426
339
|
}
|
|
427
340
|
getLibInfos() {
|
|
428
|
-
return
|
|
429
|
-
this.#getSortedLibs().map((libName) => {
|
|
430
|
-
const libInfo = LibInfo.libInfos.get(libName);
|
|
431
|
-
if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
|
|
432
|
-
return [libName, libInfo];
|
|
433
|
-
}),
|
|
434
|
-
);
|
|
341
|
+
return this.sortedLibInfos(this.libDeps);
|
|
435
342
|
}
|
|
436
343
|
}
|
|
437
344
|
export class LibInfo extends ScanInfo {
|
|
@@ -482,36 +389,16 @@ export class LibInfo extends ScanInfo {
|
|
|
482
389
|
return this.scanResult as LibScanResult;
|
|
483
390
|
}
|
|
484
391
|
|
|
485
|
-
#sortedLibs: string[] | null = null;
|
|
486
|
-
#getSortedLibs() {
|
|
487
|
-
if (this.#sortedLibs) return this.#sortedLibs;
|
|
488
|
-
const libs = LibInfo.getSortedLibIndices();
|
|
489
|
-
this.#sortedLibs = this.scanResult.libDeps.sort((libNameA, libNameB) => {
|
|
490
|
-
const indexA = libs.get(libNameA);
|
|
491
|
-
const indexB = libs.get(libNameB);
|
|
492
|
-
if (indexA === undefined || indexB === undefined)
|
|
493
|
-
throw new Error(`LibInfo not found: ${libNameA} or ${libNameB}`);
|
|
494
|
-
return indexA - indexB;
|
|
495
|
-
});
|
|
496
|
-
return this.#sortedLibs;
|
|
497
|
-
}
|
|
498
392
|
getLibs() {
|
|
499
|
-
return this
|
|
393
|
+
return this.sortedLibs(this.scanResult.libDeps);
|
|
500
394
|
}
|
|
501
395
|
getLibInfo(libName: string) {
|
|
502
396
|
if (!this.getScanResult().libDeps.includes(libName)) return undefined;
|
|
503
|
-
|
|
504
|
-
if (!libSet.has(libName)) throw new Error(`LibInfo is invalid: ${libName}`);
|
|
397
|
+
if (!this.getLibs().includes(libName)) throw new Error(`LibInfo is invalid: ${libName}`);
|
|
505
398
|
return LibInfo.libInfos.get(libName);
|
|
506
399
|
}
|
|
507
400
|
getLibInfos() {
|
|
508
|
-
return
|
|
509
|
-
this.#getSortedLibs().map((libName) => {
|
|
510
|
-
const libInfo = LibInfo.libInfos.get(libName);
|
|
511
|
-
if (!libInfo) throw new Error(`LibInfo not found: ${libName}`);
|
|
512
|
-
return [libName, libInfo];
|
|
513
|
-
}),
|
|
514
|
-
);
|
|
401
|
+
return this.sortedLibInfos(this.scanResult.libDeps);
|
|
515
402
|
}
|
|
516
403
|
}
|
|
517
404
|
|
|
@@ -524,27 +411,25 @@ export class PkgInfo {
|
|
|
524
411
|
const [tsconfig, rootPackageJson] = await Promise.all([exec.getTsConfig(), exec.workspace.getPackageJson()]);
|
|
525
412
|
const scanner = await createDependencyScanner(exec);
|
|
526
413
|
const npmSet = new Set(Object.keys({ ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies }));
|
|
414
|
+
const workspacePathOf = (resolve: string) => resolve.replace(/^\.\//, "");
|
|
527
415
|
const pkgPathSet = new Set(
|
|
528
416
|
Object.keys(tsconfig.compilerOptions.paths ?? {})
|
|
529
|
-
.filter((path) =>
|
|
417
|
+
.filter((path) =>
|
|
418
|
+
tsconfig.compilerOptions.paths?.[path]?.some((resolve) => workspacePathOf(resolve).startsWith("pkgs/")),
|
|
419
|
+
)
|
|
530
420
|
.map((path) => path.replace("/*", "")),
|
|
531
421
|
);
|
|
532
422
|
const [npmDepSet, pkgPathDepSet] = await scanner.getImportSets([npmSet, pkgPathSet]);
|
|
533
423
|
const pkgDeps = [...pkgPathDepSet]
|
|
534
424
|
.map((path) => {
|
|
535
425
|
const pathSplitLength = path.split("/").length;
|
|
536
|
-
return (tsconfig.compilerOptions.paths?.[path]?.[0] ?? "*")
|
|
426
|
+
return workspacePathOf(tsconfig.compilerOptions.paths?.[path]?.[0] ?? "*")
|
|
537
427
|
.split("/")
|
|
538
428
|
.slice(1, 1 + pathSplitLength)
|
|
539
429
|
.join("/");
|
|
540
430
|
})
|
|
541
431
|
.filter((pkg) => pkg !== exec.name);
|
|
542
|
-
|
|
543
|
-
name: exec.name,
|
|
544
|
-
pkgDeps,
|
|
545
|
-
dependencies: [...npmDepSet],
|
|
546
|
-
};
|
|
547
|
-
return pkgScanResult;
|
|
432
|
+
return { name: exec.name, pkgDeps, dependencies: [...npmDepSet] };
|
|
548
433
|
}
|
|
549
434
|
|
|
550
435
|
static #pkgInfos = new Map<string, PkgInfo>();
|
|
@@ -582,23 +467,9 @@ export class WorkspaceInfo {
|
|
|
582
467
|
const [appNames, libNames, pkgNames] = await Promise.all([exec.getApps(), exec.getLibs(), exec.getPkgs()]);
|
|
583
468
|
// TODO: prevent duplicate scan by resolving the dependency graph
|
|
584
469
|
const [appInfos, libInfos, pkgInfos] = await Promise.all([
|
|
585
|
-
Promise.all(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
return await app.scan();
|
|
589
|
-
}),
|
|
590
|
-
),
|
|
591
|
-
Promise.all(
|
|
592
|
-
libNames.map(async (libName) => {
|
|
593
|
-
const lib = LibExecutor.from(exec, libName);
|
|
594
|
-
return await lib.scan();
|
|
595
|
-
}),
|
|
596
|
-
),
|
|
597
|
-
Promise.all(
|
|
598
|
-
pkgNames.map(async (pkgName) => {
|
|
599
|
-
return await PkgExecutor.from(exec, pkgName).scan();
|
|
600
|
-
}),
|
|
601
|
-
),
|
|
470
|
+
Promise.all(appNames.map(async (appName) => await AppExecutor.from(exec, appName).scan())),
|
|
471
|
+
Promise.all(libNames.map(async (libName) => await LibExecutor.from(exec, libName).scan())),
|
|
472
|
+
Promise.all(pkgNames.map(async (pkgName) => await PkgExecutor.from(exec, pkgName).scan())),
|
|
602
473
|
]);
|
|
603
474
|
const workspaceInfo = new WorkspaceInfo(
|
|
604
475
|
new Map(appInfos.map((app) => [app.exec.name, app as AppInfo])),
|
package/selectModel.ts
CHANGED
|
@@ -3,9 +3,8 @@ import { select } from "@inquirer/prompts";
|
|
|
3
3
|
|
|
4
4
|
export const selectModel = async (modulePath: string) => {
|
|
5
5
|
const modelNames = readdirSync(`${modulePath}/lib`).filter((dir) => !dir.includes(".") && !dir.startsWith("_"));
|
|
6
|
-
|
|
6
|
+
return await select({
|
|
7
7
|
message: "Select the model to create the unit for",
|
|
8
8
|
choices: modelNames.map((name) => ({ name, value: name })),
|
|
9
9
|
});
|
|
10
|
-
return modelName;
|
|
11
10
|
};
|
package/semver.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
// `Bun.semver.order` compares
|
|
2
|
-
// operator prefix has to come off before it sees the version.
|
|
1
|
+
// `Bun.semver.order` compares ranges loosely (`^3.1049.0` vs `^3.721.0` answers 0), so the operator comes off first.
|
|
3
2
|
const stripRangeOperator = (version: string) => version.replace(/^[\s^~>=<v]+/, "").trim();
|
|
4
3
|
|
|
5
|
-
const parseVersion = (version: string): number[] =>
|
|
6
|
-
|
|
4
|
+
const parseVersion = (version: string): number[] =>
|
|
5
|
+
version
|
|
7
6
|
.replace(/^[^\d]*/, "")
|
|
8
7
|
.split(/[.-]/)
|
|
9
8
|
.map((part) => Number.parseInt(part, 10))
|
|
10
9
|
.map((part) => (Number.isFinite(part) ? part : 0));
|
|
11
|
-
};
|
|
12
10
|
|
|
13
11
|
const compareNumeric = (a: string, b: string): number => {
|
|
14
12
|
const left = parseVersion(a);
|
package/slicePlanner.test.ts
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir,
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { AppExecutor, Executor, WorkspaceExecutor } from "./executors";
|
|
6
5
|
import { formatSlicePlan, SlicePlanner } from "./slicePlanner";
|
|
6
|
+
import { isolateEnv, tempDirs, writeText as write } from "./testHelpers";
|
|
7
7
|
import type { PackageJson } from "./types";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
process.env = { ...originalEnv };
|
|
14
|
-
process.env.AKAN_PUBLIC_REPO_NAME = "workspace";
|
|
15
|
-
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
16
|
-
process.env.AKAN_PUBLIC_ENV = "local";
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
process.env = { ...originalEnv };
|
|
21
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const write = async (filePath: string, content: string) => {
|
|
25
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
26
|
-
await writeFile(filePath, content);
|
|
27
|
-
};
|
|
9
|
+
isolateEnv({ AKAN_PUBLIC_REPO_NAME: "workspace", AKAN_PUBLIC_SERVE_DOMAIN: "example.com", AKAN_PUBLIC_ENV: "local" });
|
|
10
|
+
const makeTempRoot = tempDirs("akan-slice-");
|
|
28
11
|
|
|
29
12
|
const gitignore = [
|
|
30
13
|
"node_modules",
|
|
@@ -36,8 +19,7 @@ const gitignore = [
|
|
|
36
19
|
|
|
37
20
|
// `AppExecutor.from` and `AppInfo.fromExecutor` both memoise by name, so each fixture needs a fresh one.
|
|
38
21
|
const makeWorkspace = async (appName: string, libName: string) => {
|
|
39
|
-
const root = await
|
|
40
|
-
tempRoots.push(root);
|
|
22
|
+
const root = await makeTempRoot();
|
|
41
23
|
const rootPackageJson: PackageJson = {
|
|
42
24
|
name: "workspace",
|
|
43
25
|
version: "0.0.1",
|
package/slicePlanner.ts
CHANGED
|
@@ -18,12 +18,6 @@ export interface SlicePlan {
|
|
|
18
18
|
warnings: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* The exact file set a single app needs to live in a workspace of its own: the app, its transitive lib
|
|
23
|
-
* closure, the workspace shell around them, and a root manifest for the result.
|
|
24
|
-
*
|
|
25
|
-
* Consumed by `akan plan-slice`, and by anything that moves an app or a lib between workspaces.
|
|
26
|
-
*/
|
|
27
21
|
export class SlicePlanner {
|
|
28
22
|
/** Root entries owned by an app, a lib or an in-tree package rather than by the workspace shell. */
|
|
29
23
|
static readonly memberDirs = ["apps", "libs", "pkgs"];
|
|
@@ -39,13 +33,8 @@ export class SlicePlanner {
|
|
|
39
33
|
];
|
|
40
34
|
static readonly akanPackages = ["akanjs", "@akanjs/devkit", "@akanjs/cli"];
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* `scheduler` and the RSC runtime, but also `ioredis`, `postgres` and `bullmq` the moment an app uses
|
|
45
|
-
* a database or a queue. No source file in the workspace names any of them, so no scan can find them
|
|
46
|
-
* — they are the akan packages' own `peerDependencies`, read wherever those are installed. Anything
|
|
47
|
-
* they declare as a plain dependency is installed with them and needs no root entry.
|
|
48
|
-
*/
|
|
36
|
+
// The akan packages' peerDependencies (react, the RSC runtime, ioredis, postgres, bullmq): the framework imports
|
|
37
|
+
// them but no workspace source names them, so no scan can find them.
|
|
49
38
|
static async #frameworkPeers(workspace: WorkspaceExecutor) {
|
|
50
39
|
const peers = new Set<string>();
|
|
51
40
|
let found = false;
|
|
@@ -67,14 +56,8 @@ export class SlicePlanner {
|
|
|
67
56
|
return dependency.startsWith("@") ? `@types/${dependency.slice(1).replace("/", "__")}` : `@types/${dependency}`;
|
|
68
57
|
}
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* version specs kept — that is what makes one branch resolve to one dependency tree everywhere.
|
|
73
|
-
*
|
|
74
|
-
* A `@types/x` package is kept alongside `x`: nothing imports it by name, so no scan can see it, and
|
|
75
|
-
* dropping it breaks the typecheck of every file that imports `x`. `workspaces` is dropped because it
|
|
76
|
-
* names in-tree packages, which a slice never carries — it consumes akanjs from the registry.
|
|
77
|
-
*/
|
|
59
|
+
// `@types/x` is kept alongside `x` since nothing imports it by name; `workspaces` is dropped because a slice
|
|
60
|
+
// consumes akanjs from the registry.
|
|
78
61
|
static async pruneDependencies(
|
|
79
62
|
workspace: WorkspaceExecutor,
|
|
80
63
|
rootPackageJson: PackageJson,
|
|
@@ -82,9 +65,8 @@ export class SlicePlanner {
|
|
|
82
65
|
) {
|
|
83
66
|
const peers = await SlicePlanner.#frameworkPeers(workspace);
|
|
84
67
|
const { workspaces: _workspaces, ...rest } = rootPackageJson;
|
|
85
|
-
//* Without
|
|
86
|
-
//*
|
|
87
|
-
//* whole instead and say why.
|
|
68
|
+
//* Without the peer list every peer looks unused, and pruning it breaks the server at its first redis or
|
|
69
|
+
//* postgres call.
|
|
88
70
|
if (!peers)
|
|
89
71
|
return {
|
|
90
72
|
packageJson: rest as PackageJson,
|
|
@@ -121,10 +103,6 @@ export class SlicePlanner {
|
|
|
121
103
|
this.#app = app;
|
|
122
104
|
}
|
|
123
105
|
|
|
124
|
-
async #trackedFiles(paths: string[]) {
|
|
125
|
-
return await this.#app.workspace.listGitFiles(paths);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
106
|
async #untrackedFiles(paths: string[]) {
|
|
129
107
|
const [tracked, all] = await Promise.all([
|
|
130
108
|
this.#app.workspace.listGitFiles(paths),
|
|
@@ -170,14 +148,16 @@ export class SlicePlanner {
|
|
|
170
148
|
const slicePaths = [`apps/${this.#app.name}`, ...libs.map((lib) => `libs/${lib}`)];
|
|
171
149
|
|
|
172
150
|
const [appFiles, allTracked, untracked, rootPackageJson] = await Promise.all([
|
|
173
|
-
this.#
|
|
174
|
-
this.#
|
|
151
|
+
this.#app.workspace.listGitFiles([`apps/${this.#app.name}`]),
|
|
152
|
+
this.#app.workspace.listGitFiles(["."]),
|
|
175
153
|
this.#untrackedFiles(slicePaths),
|
|
176
154
|
this.#app.workspace.getPackageJson(),
|
|
177
155
|
]);
|
|
178
156
|
|
|
179
157
|
const libFiles = Object.fromEntries(
|
|
180
|
-
await Promise.all(
|
|
158
|
+
await Promise.all(
|
|
159
|
+
libs.map(async (lib) => [lib, await this.#app.workspace.listGitFiles([`libs/${lib}`])] as const),
|
|
160
|
+
),
|
|
181
161
|
);
|
|
182
162
|
const rootFiles = allTracked.filter((file) => !this.#isMemberFile(file));
|
|
183
163
|
|
|
@@ -211,7 +191,6 @@ export class SlicePlanner {
|
|
|
211
191
|
}
|
|
212
192
|
}
|
|
213
193
|
|
|
214
|
-
/** Top-level entry each path sits under, so a long tree (`infra/**`) reads as one line. */
|
|
215
194
|
const rootEntriesOf = (rootFiles: string[]) => [...new Set(rootFiles.map((file) => file.split("/")[0] ?? file))].sort();
|
|
216
195
|
|
|
217
196
|
export function formatSlicePlan(plan: SlicePlan) {
|
package/spinner.test.ts
CHANGED
|
@@ -6,10 +6,7 @@ interface TtyStub {
|
|
|
6
6
|
rawModeCalls: boolean[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* ora only reaches for stdin when both streams look interactive, so the stub has to fake a tty on
|
|
11
|
-
* stderr (which decides `isEnabled`) as well as on stdin.
|
|
12
|
-
*/
|
|
9
|
+
// ora only reaches for stdin when both streams look interactive, so stderr (which decides `isEnabled`) is faked too.
|
|
13
10
|
const stubTty = (): TtyStub => {
|
|
14
11
|
const rawModeCalls: boolean[] = [];
|
|
15
12
|
const stdin = process.stdin as NodeJS.ReadStream & { setRawMode?: (mode: boolean) => unknown };
|
|
@@ -41,8 +38,7 @@ const stubTty = (): TtyStub => {
|
|
|
41
38
|
};
|
|
42
39
|
|
|
43
40
|
describe("Spinner", () => {
|
|
44
|
-
// A raw terminal at spawn
|
|
45
|
-
// how `akan start` used to leave a terminal that no longer turns Ctrl+C into SIGINT.
|
|
41
|
+
// A Bun child snapshots a raw terminal at spawn and writes it back on exit, leaving Ctrl+C no longer a SIGINT.
|
|
46
42
|
test("never puts the terminal into raw mode while it spins", () => {
|
|
47
43
|
const tty = stubTty();
|
|
48
44
|
try {
|
|
@@ -59,8 +55,7 @@ describe("Spinner", () => {
|
|
|
59
55
|
expect(Spinner.oraOptions.discardStdin).toBe(false);
|
|
60
56
|
});
|
|
61
57
|
|
|
62
|
-
// An unsized pty reports `isTTY: true` with `columns: 0`, which turns ora's clear loop into an
|
|
63
|
-
// infinite one — the process then writes cursor moves until it is SIGKILLed.
|
|
58
|
+
// An unsized pty reports `isTTY: true` with `columns: 0`, which turns ora's clear loop into an infinite one.
|
|
64
59
|
test("refuses to animate against a tty that reports no width", () => {
|
|
65
60
|
expect(Spinner.canAnimate({ isTTY: true, columns: 0 } as NodeJS.WriteStream)).toBe(false);
|
|
66
61
|
expect(Spinner.canAnimate({ isTTY: true, columns: 120 } as NodeJS.WriteStream)).toBe(true);
|
package/spinner.ts
CHANGED
|
@@ -2,23 +2,11 @@ import ora, { type Ora } from "ora";
|
|
|
2
2
|
|
|
3
3
|
export class Spinner {
|
|
4
4
|
static padding = 12;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* spinner runs, and a Bun child spawned in that window snapshots the raw termios and writes it back
|
|
8
|
-
* when it exits — long after the spinner restored the terminal. `akan start` spawns the builder and
|
|
9
|
-
* the backend under the "Preparing backend..." spinner, so the first builder recycle (a config or
|
|
10
|
-
* runtime-metadata change) SIGTERMs that child and silently turns the developer's terminal raw:
|
|
11
|
-
* `isig` goes off, Ctrl+C stops producing SIGINT at all, and the dev server looks unkillable.
|
|
12
|
-
*/
|
|
5
|
+
// XXX: `discardStdin` must stay off: ora raw-modes the terminal while spinning, a Bun child spawned then writes that
|
|
6
|
+
// raw termios back when it exits, and Ctrl+C stops producing SIGINT.
|
|
13
7
|
static oraOptions = { discardStdin: false } as const;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* makes it `Infinity` and `clear()` never returns. An unsized pty does exactly that (`isTTY: true`,
|
|
17
|
-
* `columns: 0`) — CI runners, `expect`/`script` harnesses, some detached panes. Measured: 750MB of
|
|
18
|
-
* cursor moves and 8.7GB RSS inside a minute, in a loop that no longer reaches the point where SIGINT
|
|
19
|
-
* or SIGTERM could be handled, so only SIGKILL ends it. A terminal with no width has nothing to
|
|
20
|
-
* animate anyway; fall back to plain lines.
|
|
21
|
-
*/
|
|
8
|
+
// ora's clear loop is `ceil(lineWidth / columns)`, so an unsized pty (`isTTY` with 0 columns: CI, `script`) never
|
|
9
|
+
// returns from `clear()`; such a terminal gets plain lines.
|
|
22
10
|
static canAnimate(stream: NodeJS.WriteStream = process.stderr): boolean {
|
|
23
11
|
return !stream.isTTY || stream.columns > 0;
|
|
24
12
|
}
|