@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/qualityScanner.ts
CHANGED
|
@@ -6,7 +6,7 @@ import ignore from "ignore";
|
|
|
6
6
|
import ts from "typescript";
|
|
7
7
|
import { AbstractDoc } from "./abstractDoc";
|
|
8
8
|
import { FormSetterScanner } from "./formSetterScanner";
|
|
9
|
-
import { formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
|
|
9
|
+
import { fileWarning, formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
|
|
10
10
|
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
11
11
|
|
|
12
12
|
type QualitySeverity = "warning";
|
|
@@ -103,8 +103,6 @@ const CONVENTION_SUFFIXES = [
|
|
|
103
103
|
".store.ts",
|
|
104
104
|
] as const;
|
|
105
105
|
|
|
106
|
-
// How to remediate each rule, keyed by rule id. Surfaced as a `fix:` line per warning (text + JSON output)
|
|
107
|
-
// so the scan result tells the reader what to do, not just what is wrong.
|
|
108
106
|
const RULE_FIXES: Record<string, string> = {
|
|
109
107
|
"akan.global.duplicate-exported-function-name":
|
|
110
108
|
"Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
|
|
@@ -114,7 +112,7 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
114
112
|
"Split the file by responsibility — move Zones, Utils, or subcomponents into sibling files.",
|
|
115
113
|
"akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
|
|
116
114
|
"akan.file.abstract-max-lines":
|
|
117
|
-
"
|
|
115
|
+
"Trim the abstract by hand to a title, one sentence naming what the module owns, a `## Rules` list of the invariants the source cannot show, and an optional workflow chain.",
|
|
118
116
|
"akan.file.placeholder-export":
|
|
119
117
|
"Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
120
118
|
"akan.file.bang-comment-in-client":
|
|
@@ -297,24 +295,24 @@ export class AkanQualityScanner {
|
|
|
297
295
|
const warnings: QualityWarning[] = [];
|
|
298
296
|
const lineCount = sourceFile.content.split(/\r?\n/).length;
|
|
299
297
|
const recommendedLineLimit = getRecommendedLineLimit(sourceFile.file);
|
|
300
|
-
if (recommendedLineLimit && lineCount > recommendedLineLimit)
|
|
301
|
-
warnings.push(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (lineCount > MAX_FILE_LINES)
|
|
310
|
-
warnings.push(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
298
|
+
if (recommendedLineLimit && lineCount > recommendedLineLimit)
|
|
299
|
+
warnings.push(
|
|
300
|
+
fileWarning(
|
|
301
|
+
"akan.file.recommended-max-lines",
|
|
302
|
+
"file",
|
|
303
|
+
sourceFile.file,
|
|
304
|
+
`File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
if (lineCount > MAX_FILE_LINES)
|
|
308
|
+
warnings.push(
|
|
309
|
+
fileWarning(
|
|
310
|
+
"akan.file.max-lines",
|
|
311
|
+
"file",
|
|
312
|
+
sourceFile.file,
|
|
313
|
+
`File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`,
|
|
314
|
+
),
|
|
315
|
+
);
|
|
318
316
|
|
|
319
317
|
warnings.push(...getBangCommentWarnings(sourceFile));
|
|
320
318
|
warnings.push(...getPlaceholderExportWarnings(sourceFile));
|
|
@@ -327,14 +325,15 @@ export class AkanQualityScanner {
|
|
|
327
325
|
for (const declaration of getTopLevelDeclarations(sourceFile)) {
|
|
328
326
|
if (declaration.kind === "class" && exportedClassNames.includes(declaration.name)) continue;
|
|
329
327
|
if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name)) continue;
|
|
330
|
-
warnings.push(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
328
|
+
warnings.push(
|
|
329
|
+
fileWarning(
|
|
330
|
+
"akan.file.class-export-global-declaration",
|
|
331
|
+
"file",
|
|
332
|
+
sourceFile.file,
|
|
333
|
+
`Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`,
|
|
334
|
+
declaration.line,
|
|
335
|
+
),
|
|
336
|
+
);
|
|
338
337
|
}
|
|
339
338
|
return warnings;
|
|
340
339
|
}
|
|
@@ -343,9 +342,7 @@ export class AkanQualityScanner {
|
|
|
343
342
|
if (!isComponentDeclarationFile(sourceFile.file)) return [];
|
|
344
343
|
const isPage = isPageRouteFile(sourceFile.file);
|
|
345
344
|
const declarations = getComponentFileDeclarations(sourceFile.sourceFile);
|
|
346
|
-
// Compound/namespaced components (e.g. `Like.WithDislike = WithDislike`) are valid without being exported.
|
|
347
345
|
const compoundComponentNames = getCompoundComponentNames(sourceFile.sourceFile);
|
|
348
|
-
// Components are the exported (or compound) PascalCase values; their "<Component>Props" interface may live here.
|
|
349
346
|
const componentNames = declarations
|
|
350
347
|
.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind))
|
|
351
348
|
.filter((declaration) => isPascalCaseName(declaration.name))
|
|
@@ -358,28 +355,28 @@ export class AkanQualityScanner {
|
|
|
358
355
|
if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name)) continue;
|
|
359
356
|
if (declaration.exported) {
|
|
360
357
|
if (isAllowedComponentExport(declaration, isPage)) continue;
|
|
361
|
-
warnings.push(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
358
|
+
warnings.push(
|
|
359
|
+
fileWarning(
|
|
360
|
+
"akan.file.component-export",
|
|
361
|
+
"file",
|
|
362
|
+
sourceFile.file,
|
|
363
|
+
`Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`,
|
|
364
|
+
declaration.line,
|
|
365
|
+
),
|
|
366
|
+
);
|
|
369
367
|
continue;
|
|
370
368
|
}
|
|
371
|
-
// A non-exported PascalCase component attached as a compound member is an accepted pattern.
|
|
372
369
|
if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name)) continue;
|
|
373
|
-
if (isRestrictedInternalKind(declaration.kind))
|
|
374
|
-
warnings.push(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
370
|
+
if (isRestrictedInternalKind(declaration.kind))
|
|
371
|
+
warnings.push(
|
|
372
|
+
fileWarning(
|
|
373
|
+
"akan.file.component-internal-declaration",
|
|
374
|
+
"file",
|
|
375
|
+
sourceFile.file,
|
|
376
|
+
`Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`,
|
|
377
|
+
declaration.line,
|
|
378
|
+
),
|
|
379
|
+
);
|
|
383
380
|
}
|
|
384
381
|
return warnings;
|
|
385
382
|
}
|
|
@@ -389,35 +386,29 @@ export class AkanQualityScanner {
|
|
|
389
386
|
if (!suffix) return [];
|
|
390
387
|
|
|
391
388
|
const modelName = toPascalCase(path.basename(sourceFile.file, suffix));
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
modelName,
|
|
404
|
-
)}.`,
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
return warnings;
|
|
389
|
+
return getTopLevelDeclarations(sourceFile)
|
|
390
|
+
.filter((declaration) => !isAllowedConventionDeclaration(suffix, modelName, declaration))
|
|
391
|
+
.map((declaration) =>
|
|
392
|
+
fileWarning(
|
|
393
|
+
`akan.convention${suffix.replace(".ts", "")}`,
|
|
394
|
+
"convention",
|
|
395
|
+
sourceFile.file,
|
|
396
|
+
`${path.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`,
|
|
397
|
+
declaration.line,
|
|
398
|
+
),
|
|
399
|
+
);
|
|
408
400
|
}
|
|
409
401
|
|
|
410
402
|
#scanAbstractQuality({ file, content }: TextFileInfo): QualityWarning[] {
|
|
411
403
|
const lineCount = AbstractDoc.lineCountOf(content);
|
|
412
404
|
if (lineCount <= AbstractDoc.maxLines) return [];
|
|
413
405
|
return [
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
severity: "warning",
|
|
406
|
+
fileWarning(
|
|
407
|
+
"akan.file.abstract-max-lines",
|
|
408
|
+
"file",
|
|
418
409
|
file,
|
|
419
|
-
|
|
420
|
-
|
|
410
|
+
`Abstract has ${lineCount} lines. Keep abstracts under ${AbstractDoc.maxLines} lines and compact them periodically.`,
|
|
411
|
+
),
|
|
421
412
|
];
|
|
422
413
|
}
|
|
423
414
|
|
|
@@ -428,27 +419,27 @@ export class AkanQualityScanner {
|
|
|
428
419
|
const { type, name, isDir } = rootEntry;
|
|
429
420
|
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
430
421
|
const kind = isDir ? "folder" : "file";
|
|
431
|
-
if (!allowed)
|
|
432
|
-
warnings.push(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
422
|
+
if (!allowed)
|
|
423
|
+
warnings.push(
|
|
424
|
+
fileWarning(
|
|
425
|
+
`akan.layout.${type}-root-${kind}`,
|
|
426
|
+
"layout",
|
|
427
|
+
sourceFile.file,
|
|
428
|
+
`Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`,
|
|
429
|
+
),
|
|
430
|
+
);
|
|
440
431
|
}
|
|
441
432
|
|
|
442
433
|
const libFacetFile = getLibFacetRootFile(sourceFile.file);
|
|
443
|
-
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
|
|
444
|
-
warnings.push(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
434
|
+
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile))
|
|
435
|
+
warnings.push(
|
|
436
|
+
fileWarning(
|
|
437
|
+
"akan.layout.lib-facet-file",
|
|
438
|
+
"layout",
|
|
439
|
+
sourceFile.file,
|
|
440
|
+
`Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`,
|
|
441
|
+
),
|
|
442
|
+
);
|
|
452
443
|
|
|
453
444
|
const moduleUiWarning = getModuleUiWarning(sourceFile.file);
|
|
454
445
|
if (moduleUiWarning) warnings.push(moduleUiWarning);
|
|
@@ -517,79 +508,59 @@ function formatQualityLocation(file: string | undefined, line: number | undefine
|
|
|
517
508
|
|
|
518
509
|
function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionLike[] {
|
|
519
510
|
const declarations: ExportedFunctionLike[] = [];
|
|
520
|
-
// UI component names (
|
|
521
|
-
// duplicate-exported-name check. This only relaxes the name check; the shared-body check still applies.
|
|
511
|
+
// UI component names (Card, Button) repeat across apps and libs by design; only the name check is relaxed.
|
|
522
512
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
513
|
+
const add = (
|
|
514
|
+
name: string,
|
|
515
|
+
kind: ExportedFunctionLike["kind"],
|
|
516
|
+
node: ts.Node,
|
|
517
|
+
body: ts.Node | undefined,
|
|
518
|
+
isEnumClass = false,
|
|
519
|
+
) =>
|
|
520
|
+
declarations.push({
|
|
521
|
+
name,
|
|
522
|
+
kind,
|
|
523
|
+
file: sourceFile.file,
|
|
524
|
+
line: getLine(sourceFile.sourceFile, node),
|
|
525
|
+
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, body),
|
|
526
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClass),
|
|
527
|
+
});
|
|
523
528
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
524
|
-
if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement))
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
file: sourceFile.file,
|
|
529
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
530
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement.body),
|
|
531
|
-
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
if (ts.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
535
|
-
declarations.push({
|
|
536
|
-
name: statement.name.text,
|
|
537
|
-
kind: "class",
|
|
538
|
-
file: sourceFile.file,
|
|
539
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
540
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement),
|
|
541
|
-
duplicateNameExempt:
|
|
542
|
-
nameExempt ||
|
|
543
|
-
isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement)),
|
|
544
|
-
});
|
|
545
|
-
}
|
|
529
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement))
|
|
530
|
+
add(statement.name.text, "function", statement, statement.body);
|
|
531
|
+
if (ts.isClassDeclaration(statement) && statement.name && isExported(statement))
|
|
532
|
+
add(statement.name.text, "class", statement, statement, isEnumClassStatement(sourceFile.sourceFile, statement));
|
|
546
533
|
if (ts.isVariableStatement(statement) && isExported(statement)) {
|
|
547
534
|
for (const declaration of statement.declarationList.declarations) {
|
|
548
535
|
if (!ts.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer)) continue;
|
|
549
|
-
|
|
550
|
-
name: declaration.name.text,
|
|
551
|
-
kind: "function-variable",
|
|
552
|
-
file: sourceFile.file,
|
|
553
|
-
line: getLine(sourceFile.sourceFile, declaration),
|
|
554
|
-
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, declaration.initializer),
|
|
555
|
-
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
|
|
556
|
-
});
|
|
536
|
+
add(declaration.name.text, "function-variable", declaration, declaration.initializer);
|
|
557
537
|
}
|
|
558
538
|
}
|
|
559
539
|
}
|
|
560
540
|
return declarations;
|
|
561
541
|
}
|
|
562
542
|
|
|
563
|
-
function
|
|
543
|
+
function sysSegments(file: string) {
|
|
564
544
|
const segments = file.split("/");
|
|
565
|
-
return
|
|
545
|
+
return segments[0] === "apps" || segments[0] === "libs" ? segments : null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function isPageRouteFile(file: string) {
|
|
549
|
+
return sysSegments(file)?.[2] === "page";
|
|
566
550
|
}
|
|
567
551
|
|
|
568
552
|
function isUiComponentFile(file: string) {
|
|
569
|
-
|
|
570
|
-
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
|
|
553
|
+
return sysSegments(file)?.[2] === "ui";
|
|
571
554
|
}
|
|
572
555
|
|
|
573
556
|
function isConventionDuplicateNameExempt(file: string, isEnumClass: boolean) {
|
|
574
|
-
if (!
|
|
575
|
-
if (file.endsWith(".tsx")) return true;
|
|
576
|
-
if (
|
|
577
|
-
file.endsWith(".document.ts") ||
|
|
578
|
-
file.endsWith(".service.ts") ||
|
|
579
|
-
file.endsWith(".signal.ts") ||
|
|
580
|
-
file.endsWith(".store.ts")
|
|
581
|
-
)
|
|
582
|
-
return true;
|
|
557
|
+
if (!sysSegments(file)?.includes("lib")) return false;
|
|
558
|
+
if (file.endsWith(".tsx") || /\.(document|service|signal|store)\.ts$/.test(file)) return true;
|
|
583
559
|
// Model view classes may repeat across modules; enum classes must stay uniquely named.
|
|
584
560
|
if (file.endsWith(".constant.ts")) return !isEnumClass;
|
|
585
561
|
return false;
|
|
586
562
|
}
|
|
587
563
|
|
|
588
|
-
function isInLibModule(file: string) {
|
|
589
|
-
const segments = file.split("/");
|
|
590
|
-
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
591
|
-
}
|
|
592
|
-
|
|
593
564
|
function isEnumClassStatement(sourceFile: ts.SourceFile, statement: ts.Statement) {
|
|
594
565
|
if (!ts.isClassDeclaration(statement)) return false;
|
|
595
566
|
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
|
@@ -646,9 +617,7 @@ function getComponentFileDeclarations(sourceFile: ts.SourceFile): ComponentFileD
|
|
|
646
617
|
return declarations;
|
|
647
618
|
}
|
|
648
619
|
|
|
649
|
-
//
|
|
650
|
-
// member is the public component name; when the right side is a PascalCase identifier it is the local
|
|
651
|
-
// definition. Both are treated as valid components so the definition and its "<Component>Props" may stay local.
|
|
620
|
+
// `Like.WithDislike = WithDislike` makes both names components, so the local definition and its Props may stay unexported.
|
|
652
621
|
function getCompoundComponentNames(sourceFile: ts.SourceFile): Set<string> {
|
|
653
622
|
const names = new Set<string>();
|
|
654
623
|
for (const statement of sourceFile.statements) {
|
|
@@ -676,7 +645,6 @@ function isAllowedComponentExport(declaration: ComponentFileDeclaration, isPage:
|
|
|
676
645
|
}
|
|
677
646
|
|
|
678
647
|
function isPascalCaseName(name: string) {
|
|
679
|
-
// PascalCase component names start uppercase and are not SCREAMING_SNAKE_CASE constants.
|
|
680
648
|
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
681
649
|
}
|
|
682
650
|
|
|
@@ -686,31 +654,23 @@ function isDefaultExportStatement(statement: ts.Statement) {
|
|
|
686
654
|
return !!ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
687
655
|
}
|
|
688
656
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
* bang marker in browser-reachable code ships verbatim to every visitor. `no-bang-comment-in-client.grit`
|
|
692
|
-
* is the build gate; this exists because that rule cannot see the whole file.
|
|
693
|
-
*
|
|
694
|
-
* A comment is trivia, which Biome's GritQL exposes to no node pattern, so the rule matches through
|
|
695
|
-
* `file($name, $body)` — and `$body` is the module's *token* span. A marker above the first statement or
|
|
696
|
-
* below the last one sits in leading or trailing trivia and is unreachable from GritQL at all. Reading the
|
|
697
|
-
* raw text here covers both, and re-reporting the markers grit already catches costs nothing: the two run
|
|
698
|
-
* from different commands, and a file with one is failing lint either way.
|
|
699
|
-
*/
|
|
657
|
+
// Bun keeps `//!` and `/*!` through minification. GritQL's `file($name, $body)` spans tokens only, so the grit rule
|
|
658
|
+
// misses a marker in the file's leading or trailing trivia; the raw text is read here.
|
|
700
659
|
function getBangCommentWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
701
660
|
if (!isClientReachableFile(sourceFile.file)) return [];
|
|
702
661
|
// Anchored to line start or to whitespace after code, so a literal like `"https://host//!path"` is not a hit.
|
|
703
|
-
return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) =>
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
662
|
+
return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) =>
|
|
663
|
+
fileWarning(
|
|
664
|
+
"akan.file.bang-comment-in-client",
|
|
665
|
+
"file",
|
|
666
|
+
sourceFile.file,
|
|
667
|
+
"A `//!` or `/*!` marker survives minification and ships to the browser.",
|
|
668
|
+
line,
|
|
669
|
+
),
|
|
670
|
+
);
|
|
711
671
|
}
|
|
712
672
|
|
|
713
|
-
|
|
673
|
+
// Mirrors the path scope of `no-bang-comment-in-client.grit` in `biome.base.json`.
|
|
714
674
|
function isClientReachableFile(file: string) {
|
|
715
675
|
if (/\.(test|spec)\.tsx?$/.test(file)) return false;
|
|
716
676
|
if (/\.(constant|store)\.ts$/.test(file)) return true;
|
|
@@ -725,67 +685,53 @@ function getPlaceholderExportWarnings(sourceFile: SourceFileInfo): QualityWarnin
|
|
|
725
685
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx")) return [];
|
|
726
686
|
return getTopLevelDeclarations(sourceFile)
|
|
727
687
|
.filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name))
|
|
728
|
-
.map((declaration) =>
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
688
|
+
.map((declaration) =>
|
|
689
|
+
fileWarning(
|
|
690
|
+
"akan.file.placeholder-export",
|
|
691
|
+
"file",
|
|
692
|
+
sourceFile.file,
|
|
693
|
+
`Generated or barrel index file should not export placeholder "${declaration.name}".`,
|
|
694
|
+
declaration.line,
|
|
695
|
+
),
|
|
696
|
+
);
|
|
736
697
|
}
|
|
737
698
|
|
|
738
699
|
function getDictionaryTextWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
739
700
|
if (!sourceFile.file.endsWith(".dictionary.ts")) return [];
|
|
740
701
|
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
741
702
|
return stalePatterns.flatMap(({ pattern, label }) =>
|
|
742
|
-
findPatternLines(sourceFile.content, pattern).map((line) =>
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
703
|
+
findPatternLines(sourceFile.content, pattern).map((line) =>
|
|
704
|
+
fileWarning(
|
|
705
|
+
"akan.file.dictionary-stale-text",
|
|
706
|
+
"file",
|
|
707
|
+
sourceFile.file,
|
|
708
|
+
`Dictionary text appears to contain ${label}.`,
|
|
709
|
+
line,
|
|
710
|
+
),
|
|
711
|
+
),
|
|
750
712
|
);
|
|
751
713
|
}
|
|
752
714
|
|
|
753
715
|
function getGlobalMutationWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
754
716
|
const warnings: QualityWarning[] = [];
|
|
755
717
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
756
|
-
|
|
757
|
-
warnings.push(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
767
|
-
warnings.push({
|
|
768
|
-
rule: "akan.file.window-augmentation",
|
|
769
|
-
scope: "file",
|
|
770
|
-
severity: "warning",
|
|
771
|
-
file: sourceFile.file,
|
|
772
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
773
|
-
message: "Window augmentation should be isolated to approved browser integration files.",
|
|
774
|
-
});
|
|
775
|
-
}
|
|
718
|
+
const warn = (rule: string, message: string) =>
|
|
719
|
+
warnings.push(fileWarning(rule, "file", sourceFile.file, message, getLine(sourceFile.sourceFile, statement)));
|
|
720
|
+
if (ts.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global")
|
|
721
|
+
warn("akan.file.global-declaration", "Global declarations require an explicit low-level integration allowlist.");
|
|
722
|
+
if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window")
|
|
723
|
+
warn(
|
|
724
|
+
"akan.file.window-augmentation",
|
|
725
|
+
"Window augmentation should be isolated to approved browser integration files.",
|
|
726
|
+
);
|
|
776
727
|
if (
|
|
777
728
|
ts.isExpressionStatement(statement) &&
|
|
778
729
|
statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")
|
|
779
|
-
)
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
file: sourceFile.file,
|
|
785
|
-
line: getLine(sourceFile.sourceFile, statement),
|
|
786
|
-
message: "Prototype mutation should be avoided or isolated to approved low-level integration files.",
|
|
787
|
-
});
|
|
788
|
-
}
|
|
730
|
+
)
|
|
731
|
+
warn(
|
|
732
|
+
"akan.file.prototype-mutation",
|
|
733
|
+
"Prototype mutation should be avoided or isolated to approved low-level integration files.",
|
|
734
|
+
);
|
|
789
735
|
}
|
|
790
736
|
return warnings;
|
|
791
737
|
}
|
|
@@ -798,35 +744,35 @@ function getTopLevelDeclarations(sourceFile: SourceFileInfo): TopLevelDeclaratio
|
|
|
798
744
|
|
|
799
745
|
function getTopLevelDeclaration(sourceFile: ts.SourceFile, statement: ts.Statement): TopLevelDeclaration[] {
|
|
800
746
|
const line = getLine(sourceFile, statement);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
if (ts.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (ts.
|
|
814
|
-
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
815
|
-
}
|
|
816
|
-
if (ts.isVariableStatement(statement)) {
|
|
747
|
+
const declared = (name: string, kind: string, at = line) => ({
|
|
748
|
+
name,
|
|
749
|
+
kind,
|
|
750
|
+
line: at,
|
|
751
|
+
exported: isExported(statement),
|
|
752
|
+
node: statement,
|
|
753
|
+
});
|
|
754
|
+
if (ts.isClassDeclaration(statement) && statement.name) return [declared(statement.name.text, "class")];
|
|
755
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) return [declared(statement.name.text, "function")];
|
|
756
|
+
if (ts.isInterfaceDeclaration(statement)) return [declared(statement.name.text, "interface")];
|
|
757
|
+
if (ts.isTypeAliasDeclaration(statement)) return [declared(statement.name.text, "type")];
|
|
758
|
+
if (ts.isEnumDeclaration(statement)) return [declared(statement.name.text, "enum")];
|
|
759
|
+
if (ts.isVariableStatement(statement))
|
|
817
760
|
return statement.declarationList.declarations
|
|
818
761
|
.filter((declaration) => ts.isIdentifier(declaration.name))
|
|
819
|
-
.map((declaration) =>
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
exported: isExported(statement),
|
|
824
|
-
node: statement,
|
|
825
|
-
}));
|
|
826
|
-
}
|
|
827
|
-
if (ts.isExportDeclaration(statement)) {
|
|
762
|
+
.map((declaration) =>
|
|
763
|
+
declared((declaration.name as ts.Identifier).text, "variable", getLine(sourceFile, declaration)),
|
|
764
|
+
);
|
|
765
|
+
if (ts.isExportDeclaration(statement))
|
|
828
766
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
829
|
-
|
|
767
|
+
return [];
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function getConventionClassNames(suffix: (typeof CONVENTION_SUFFIXES)[number], model: string) {
|
|
771
|
+
if (suffix === ".constant.ts") return [`${model}Input`, `${model}Object`, model, `Light${model}`, `${model}Insight`];
|
|
772
|
+
if (suffix === ".document.ts") return [`${model}Filter`, model, `${model}Model`];
|
|
773
|
+
if (suffix === ".service.ts") return [`${model}Service`];
|
|
774
|
+
if (suffix === ".signal.ts") return [`${model}Internal`, `${model}Slice`, `${model}Endpoint`];
|
|
775
|
+
if (suffix === ".store.ts") return [`${model}Store`];
|
|
830
776
|
return [];
|
|
831
777
|
}
|
|
832
778
|
|
|
@@ -836,45 +782,18 @@ function isAllowedConventionDeclaration(
|
|
|
836
782
|
declaration: TopLevelDeclaration,
|
|
837
783
|
) {
|
|
838
784
|
if (suffix === ".dictionary.ts") return isExportedConst(declaration) && declaration.name === "dictionary";
|
|
839
|
-
if (suffix === ".constant.ts") return isAllowedConstantDeclaration(modelName, declaration);
|
|
840
|
-
if (suffix === ".document.ts")
|
|
841
|
-
return (
|
|
842
|
-
declaration.kind === "class" && [`${modelName}Filter`, modelName, `${modelName}Model`].includes(declaration.name)
|
|
843
|
-
);
|
|
844
|
-
if (suffix === ".service.ts") return declaration.kind === "class" && declaration.name === `${modelName}Service`;
|
|
845
|
-
if (suffix === ".signal.ts")
|
|
846
|
-
return (
|
|
847
|
-
declaration.kind === "class" &&
|
|
848
|
-
[`${modelName}Internal`, `${modelName}Slice`, `${modelName}Endpoint`].includes(declaration.name)
|
|
849
|
-
);
|
|
850
|
-
if (suffix === ".store.ts") return declaration.kind === "class" && declaration.name === `${modelName}Store`;
|
|
851
|
-
return false;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function isAllowedConstantDeclaration(modelName: string, declaration: TopLevelDeclaration) {
|
|
855
785
|
if (declaration.kind !== "class") return false;
|
|
856
|
-
if (
|
|
857
|
-
|
|
858
|
-
declaration.name,
|
|
859
|
-
)
|
|
860
|
-
)
|
|
861
|
-
return true;
|
|
862
|
-
if (!ts.isClassDeclaration(declaration.node)) return false;
|
|
863
|
-
const heritageClause = declaration.node.heritageClauses?.find(
|
|
864
|
-
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
|
865
|
-
);
|
|
866
|
-
const expression = heritageClause?.types[0]?.expression;
|
|
867
|
-
return !!expression && expression.getText().startsWith("enumOf(");
|
|
786
|
+
if (getConventionClassNames(suffix, modelName).includes(declaration.name)) return true;
|
|
787
|
+
return suffix === ".constant.ts" && isEnumClassStatement(declaration.node.getSourceFile(), declaration.node);
|
|
868
788
|
}
|
|
869
789
|
|
|
870
790
|
function getConventionDescription(suffix: (typeof CONVENTION_SUFFIXES)[number], modelName: string) {
|
|
871
791
|
if (suffix === ".dictionary.ts") return "export const dictionary";
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
return `${modelName}Store`;
|
|
792
|
+
const names = [
|
|
793
|
+
...getConventionClassNames(suffix, modelName),
|
|
794
|
+
...(suffix === ".constant.ts" ? ["enumOf classes"] : []),
|
|
795
|
+
];
|
|
796
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}` : names[0];
|
|
878
797
|
}
|
|
879
798
|
|
|
880
799
|
const sysRootTypes = { apps: "app", libs: "lib" } as const;
|
|
@@ -910,13 +829,12 @@ function getModuleUiWarning(file: string): QualityWarning | null {
|
|
|
910
829
|
: ["Template", "Unit"];
|
|
911
830
|
const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
|
|
912
831
|
if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx")) return null;
|
|
913
|
-
return
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
severity: "warning",
|
|
832
|
+
return fileWarning(
|
|
833
|
+
"akan.layout.module-ui-file",
|
|
834
|
+
"layout",
|
|
917
835
|
file,
|
|
918
|
-
|
|
919
|
-
|
|
836
|
+
`Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`,
|
|
837
|
+
);
|
|
920
838
|
}
|
|
921
839
|
|
|
922
840
|
function getModuleInfo(file: string) {
|
|
@@ -930,10 +848,7 @@ function getModuleInfo(file: string) {
|
|
|
930
848
|
}
|
|
931
849
|
if (segments.length !== libIndex + 3) return null;
|
|
932
850
|
const fileName = segments[libIndex + 2];
|
|
933
|
-
if (moduleName.startsWith("__")) {
|
|
934
|
-
const scalarName = moduleName === "__scalar" ? segments[libIndex + 2] : moduleName;
|
|
935
|
-
return { moduleName: scalarName, fileName, kind: "scalar" as const };
|
|
936
|
-
}
|
|
851
|
+
if (moduleName.startsWith("__")) return { moduleName, fileName, kind: "scalar" as const };
|
|
937
852
|
if (moduleName.startsWith("_")) return { moduleName, fileName, kind: "service" as const };
|
|
938
853
|
return { moduleName, fileName, kind: "database" as const };
|
|
939
854
|
}
|
|
@@ -947,10 +862,7 @@ function isExportedConst(declaration: TopLevelDeclaration) {
|
|
|
947
862
|
}
|
|
948
863
|
|
|
949
864
|
function isExported(node: ts.Node) {
|
|
950
|
-
return (
|
|
951
|
-
!!ts.getCombinedModifierFlags(node as ts.Declaration) &&
|
|
952
|
-
!!(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export)
|
|
953
|
-
);
|
|
865
|
+
return !!(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export);
|
|
954
866
|
}
|
|
955
867
|
|
|
956
868
|
function isFunctionLikeInitializer(node: ts.Expression | undefined) {
|