@akanjs/devkit 1.0.20 → 2.1.0-rc.1
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.ko.md +65 -0
- package/README.md +62 -6
- package/aiEditor.ts +304 -0
- package/akanApp/akanApp.host.ts +393 -0
- package/akanApp/index.ts +1 -0
- package/akanConfig/akanConfig.test.ts +236 -0
- package/akanConfig/akanConfig.ts +384 -0
- package/akanConfig/index.ts +2 -0
- package/akanConfig/types.ts +23 -0
- package/applicationBuildReporter.ts +69 -0
- package/applicationBuildRunner.ts +302 -0
- package/applicationReleasePackager.ts +206 -0
- package/artifact/implicitRootLayout.ts +155 -0
- package/artifact/index.ts +1 -0
- package/artifact/routeSeedIndex.test.ts +98 -0
- package/artifact/routeSeedIndex.ts +130 -0
- package/auth.ts +41 -0
- package/builder.ts +164 -0
- package/capacitor.base.config.ts +88 -0
- package/capacitorApp.ts +440 -0
- package/commandDecorators/argMeta.ts +102 -0
- package/commandDecorators/command.ts +351 -0
- package/commandDecorators/commandBuilder.ts +224 -0
- package/commandDecorators/commandDecorators.test.ts +212 -0
- package/commandDecorators/commandMeta.ts +7 -0
- package/commandDecorators/dependencyBuilder.ts +100 -0
- package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
- package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
- package/commandDecorators/targetMeta.ts +31 -0
- package/commandDecorators/types.ts +10 -0
- package/constants.ts +25 -0
- package/createTunnel.ts +36 -0
- package/dependencyScanner.ts +357 -0
- package/devkitUtils.test.ts +259 -0
- package/executors.test.ts +315 -0
- package/executors.ts +1390 -0
- package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
- package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
- package/fileSys.ts +39 -0
- package/frontendBuild/allRoutesBuilder.ts +103 -0
- package/frontendBuild/buildRouteClient.test.ts +190 -0
- package/frontendBuild/clientBuildTypes.ts +114 -0
- package/frontendBuild/clientEntriesBundler.ts +303 -0
- package/frontendBuild/clientEntryDiscovery.ts +199 -0
- package/frontendBuild/csrArtifactBuilder.ts +237 -0
- package/frontendBuild/cssCompiler.ts +286 -0
- package/frontendBuild/cssImportResolver.ts +116 -0
- package/frontendBuild/fontOptimizer.ts +427 -0
- package/frontendBuild/frontendBuild.test.ts +204 -0
- package/frontendBuild/hmrChangeClassifier.ts +28 -0
- package/frontendBuild/hmrWatcher.ts +102 -0
- package/frontendBuild/index.ts +18 -0
- package/frontendBuild/pagesBundleBuilder.ts +137 -0
- package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
- package/frontendBuild/precompressArtifacts.ts +59 -0
- package/frontendBuild/routeClientBuilder.ts +290 -0
- package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
- package/frontendBuild/vendorSpecifiers.ts +16 -0
- package/frontendBuild/watchRootResolver.ts +28 -0
- package/getCredentials.ts +19 -0
- package/getDirname.ts +3 -0
- package/getModelFileData.ts +59 -0
- package/getRelatedCnsts.ts +313 -0
- package/guideline.ts +19 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
- package/incrementalBuilder/index.ts +1 -0
- package/{esm/src/index.js → index.ts} +28 -15
- package/lint/no-deep-internal-import.grit +25 -0
- package/lint/no-import-client-functions.grit +32 -0
- package/lint/no-import-external-library.grit +21 -0
- package/lint/no-js-private-class-method.grit +42 -0
- package/lint/no-use-client-in-server.grit +7 -0
- package/lint/non-scalar-props-restricted.grit +13 -0
- package/linter.ts +271 -0
- package/mobile/index.ts +1 -0
- package/mobile/mobileTarget.test.ts +53 -0
- package/mobile/mobileTarget.ts +88 -0
- package/package.json +48 -31
- package/prompter.ts +72 -0
- package/scanInfo.ts +606 -0
- package/selectModel.ts +11 -0
- package/{esm/src/spinner.js → spinner.ts} +22 -28
- package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
- package/sshTunnel.ts +152 -0
- package/{esm/src/streamAi.js → streamAi.ts} +18 -12
- package/transforms/barrelAnalyzer.ts +278 -0
- package/transforms/barrelImportsPlugin.ts +504 -0
- package/transforms/externalizeFrameworkPlugin.ts +185 -0
- package/transforms/index.ts +5 -0
- package/transforms/rscUseClientTransform.ts +59 -0
- package/transforms/transforms.test.ts +208 -0
- package/transforms/useClientBundlePlugin.ts +47 -0
- package/tsconfig.json +37 -0
- package/typeChecker.ts +264 -0
- package/types.ts +44 -0
- package/ui/MultiScrollList.tsx +242 -0
- package/ui/ScrollList.tsx +107 -0
- package/ui/index.ts +2 -0
- package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
- package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
- package/cjs/index.js +0 -21
- package/cjs/src/aiEditor.js +0 -311
- package/cjs/src/auth.js +0 -72
- package/cjs/src/builder.js +0 -114
- package/cjs/src/capacitorApp.js +0 -313
- package/cjs/src/commandDecorators/argMeta.js +0 -88
- package/cjs/src/commandDecorators/command.js +0 -324
- package/cjs/src/commandDecorators/commandMeta.js +0 -30
- package/cjs/src/commandDecorators/helpFormatter.js +0 -211
- package/cjs/src/commandDecorators/index.js +0 -31
- package/cjs/src/commandDecorators/targetMeta.js +0 -57
- package/cjs/src/commandDecorators/types.js +0 -15
- package/cjs/src/constants.js +0 -46
- package/cjs/src/createTunnel.js +0 -49
- package/cjs/src/dependencyScanner.js +0 -220
- package/cjs/src/executors.js +0 -964
- package/cjs/src/extractDeps.js +0 -103
- package/cjs/src/fileEditor.js +0 -120
- package/cjs/src/getCredentials.js +0 -44
- package/cjs/src/getDirname.js +0 -38
- package/cjs/src/getModelFileData.js +0 -66
- package/cjs/src/getRelatedCnsts.js +0 -260
- package/cjs/src/guideline.js +0 -15
- package/cjs/src/index.js +0 -65
- package/cjs/src/linter.js +0 -238
- package/cjs/src/prompter.js +0 -85
- package/cjs/src/scanInfo.js +0 -491
- package/cjs/src/selectModel.js +0 -46
- package/cjs/src/spinner.js +0 -93
- package/cjs/src/streamAi.js +0 -62
- package/cjs/src/typeChecker.js +0 -207
- package/cjs/src/types.js +0 -15
- package/cjs/src/uploadRelease.js +0 -112
- package/cjs/src/useStdoutDimensions.js +0 -43
- package/esm/index.js +0 -1
- package/esm/src/aiEditor.js +0 -282
- package/esm/src/auth.js +0 -42
- package/esm/src/builder.js +0 -81
- package/esm/src/commandDecorators/argMeta.js +0 -54
- package/esm/src/commandDecorators/command.js +0 -290
- package/esm/src/commandDecorators/commandMeta.js +0 -7
- package/esm/src/commandDecorators/targetMeta.js +0 -33
- package/esm/src/commandDecorators/types.js +0 -0
- package/esm/src/constants.js +0 -17
- package/esm/src/createTunnel.js +0 -26
- package/esm/src/dependencyScanner.js +0 -187
- package/esm/src/executors.js +0 -928
- package/esm/src/getCredentials.js +0 -11
- package/esm/src/getDirname.js +0 -5
- package/esm/src/getModelFileData.js +0 -33
- package/esm/src/getRelatedCnsts.js +0 -221
- package/esm/src/guideline.js +0 -0
- package/esm/src/linter.js +0 -205
- package/esm/src/prompter.js +0 -51
- package/esm/src/scanInfo.js +0 -455
- package/esm/src/selectModel.js +0 -13
- package/esm/src/typeChecker.js +0 -174
- package/esm/src/types.js +0 -0
- package/index.d.ts +0 -1
- package/src/aiEditor.d.ts +0 -50
- package/src/auth.d.ts +0 -9
- package/src/builder.d.ts +0 -18
- package/src/capacitorApp.d.ts +0 -39
- package/src/commandDecorators/argMeta.d.ts +0 -67
- package/src/commandDecorators/command.d.ts +0 -2
- package/src/commandDecorators/commandMeta.d.ts +0 -2
- package/src/commandDecorators/helpFormatter.d.ts +0 -3
- package/src/commandDecorators/index.d.ts +0 -6
- package/src/commandDecorators/targetMeta.d.ts +0 -19
- package/src/commandDecorators/types.d.ts +0 -1
- package/src/constants.d.ts +0 -26
- package/src/createTunnel.d.ts +0 -8
- package/src/dependencyScanner.d.ts +0 -23
- package/src/executors.d.ts +0 -296
- package/src/extractDeps.d.ts +0 -7
- package/src/fileEditor.d.ts +0 -16
- package/src/getCredentials.d.ts +0 -12
- package/src/getDirname.d.ts +0 -1
- package/src/getModelFileData.d.ts +0 -16
- package/src/getRelatedCnsts.d.ts +0 -53
- package/src/guideline.d.ts +0 -19
- package/src/index.d.ts +0 -23
- package/src/linter.d.ts +0 -109
- package/src/prompter.d.ts +0 -14
- package/src/scanInfo.d.ts +0 -82
- package/src/selectModel.d.ts +0 -1
- package/src/spinner.d.ts +0 -20
- package/src/streamAi.d.ts +0 -6
- package/src/typeChecker.d.ts +0 -52
- package/src/types.d.ts +0 -31
- package/src/uploadRelease.d.ts +0 -10
- package/src/useStdoutDimensions.d.ts +0 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
or {
|
|
5
|
+
JsMethodClassMember() as $method where {
|
|
6
|
+
$method <: contains JsPrivateClassMemberName() as $name,
|
|
7
|
+
$method <: r"async\s+#",
|
|
8
|
+
$method <: contains `async` as $asyncToken,
|
|
9
|
+
$name <: r"#(.*)"($bareName),
|
|
10
|
+
$privateName = join(list = ["_", $bareName], separator = ""),
|
|
11
|
+
register_diagnostic(
|
|
12
|
+
span = $method,
|
|
13
|
+
message = "JavaScript private class methods (#) should not be used in mixin-enabled modules. Use a TypeScript private method with an underscore prefix instead.",
|
|
14
|
+
severity = "error"
|
|
15
|
+
),
|
|
16
|
+
$asyncToken => `private async`,
|
|
17
|
+
$name => $privateName
|
|
18
|
+
},
|
|
19
|
+
JsMethodClassMember() as $method where {
|
|
20
|
+
$method <: contains JsPrivateClassMemberName() as $name,
|
|
21
|
+
not $method <: r"async\s+#",
|
|
22
|
+
$name <: r"#(.*)"($bareName),
|
|
23
|
+
$privateName = join(list = ["private _", $bareName], separator = ""),
|
|
24
|
+
register_diagnostic(
|
|
25
|
+
span = $method,
|
|
26
|
+
message = "JavaScript private class methods (#) should not be used in mixin-enabled modules. Use a TypeScript private method with an underscore prefix instead.",
|
|
27
|
+
severity = "error"
|
|
28
|
+
),
|
|
29
|
+
$name => $privateName
|
|
30
|
+
},
|
|
31
|
+
JsCallExpression() as $call where {
|
|
32
|
+
$call <: contains JsPrivateName() as $name,
|
|
33
|
+
$name <: r"#(.*)"($bareName),
|
|
34
|
+
$privateName = join(list = ["_", $bareName], separator = ""),
|
|
35
|
+
register_diagnostic(
|
|
36
|
+
span = $call,
|
|
37
|
+
message = "Calls to JavaScript private class methods (#) should use the underscore-prefixed TypeScript private method.",
|
|
38
|
+
severity = "error"
|
|
39
|
+
),
|
|
40
|
+
$name => $privateName
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
`$name={$value}` where {
|
|
2
|
+
or {
|
|
3
|
+
$value <: `($$$_) => $_`,
|
|
4
|
+
$value <: `$_ => $_`,
|
|
5
|
+
$value <: `function($$$_) { $$$_ }`,
|
|
6
|
+
$value <: `function $fnName($$$_) { $$$_ }`
|
|
7
|
+
},
|
|
8
|
+
not $name <: or { `loader`, `render`, `of` },
|
|
9
|
+
register_diagnostic(
|
|
10
|
+
span = $name,
|
|
11
|
+
message = "Non-scalar props (function expressions) should not be used in server components. Allowed: loader, render, of."
|
|
12
|
+
)
|
|
13
|
+
}
|
package/linter.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { Logger } from "akanjs/common";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
// import { ESLint, type Linter as ESLintLinter } from "eslint";
|
|
6
|
+
|
|
7
|
+
export class Linter {
|
|
8
|
+
#logger = new Logger("Linter");
|
|
9
|
+
// #eslint: any;
|
|
10
|
+
lintRoot: string;
|
|
11
|
+
|
|
12
|
+
constructor(cwdPath: string) {
|
|
13
|
+
this.lintRoot = this.#findEslintRootPath(cwdPath);
|
|
14
|
+
// this.#eslint = new ESLint({ cwd: this.lintRoot, errorOnUnmatchedPattern: false });
|
|
15
|
+
}
|
|
16
|
+
#findEslintRootPath(dir: string): string {
|
|
17
|
+
const configPath = path.join(dir, "eslint.config.ts");
|
|
18
|
+
if (existsSync(configPath)) return dir;
|
|
19
|
+
const parentDir = path.dirname(dir);
|
|
20
|
+
return this.#findEslintRootPath(parentDir);
|
|
21
|
+
}
|
|
22
|
+
async lint(filePath: string, { fix = false, dryRun = false }: { fix?: boolean; dryRun?: boolean } = {}) {
|
|
23
|
+
if (fix) return await this.fixFile(filePath, dryRun);
|
|
24
|
+
return await this.lintFile(filePath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lint a single file using ESLint
|
|
29
|
+
* @param filePath - Path to the file to lint
|
|
30
|
+
* @returns Array of ESLint results
|
|
31
|
+
*/
|
|
32
|
+
async lintFile(filePath: string): Promise<{
|
|
33
|
+
fixed: boolean;
|
|
34
|
+
output?: string;
|
|
35
|
+
results: any[]; // ESLint.LintResult[];
|
|
36
|
+
errors: any[]; // ESLintLinter.LintMessage[];
|
|
37
|
+
warnings: any[]; // ESLintLinter.LintMessage[];
|
|
38
|
+
}> {
|
|
39
|
+
if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
40
|
+
// const isIgnored = await this.#eslint.isPathIgnored(filePath);
|
|
41
|
+
// if (isIgnored) {
|
|
42
|
+
// this.#logger.warn(`File ${filePath} is ignored by ESLint configuration`);
|
|
43
|
+
// return { fixed: false, results: [], errors: [], warnings: [] };
|
|
44
|
+
// }
|
|
45
|
+
// const results = await this.#eslint.lintFiles([filePath]);
|
|
46
|
+
// const errors = results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
|
|
47
|
+
// const warnings = results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
|
|
48
|
+
return { fixed: false, results: [], errors: [], warnings: [] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format lint results for console output
|
|
53
|
+
* @param results - Array of ESLint results
|
|
54
|
+
* @returns Formatted string
|
|
55
|
+
*/
|
|
56
|
+
formatLintResults(results: any[]): string {
|
|
57
|
+
// ESLint.LintResult[]
|
|
58
|
+
if (results.length === 0) return "No files to lint";
|
|
59
|
+
|
|
60
|
+
const output: string[] = [];
|
|
61
|
+
let totalErrors = 0;
|
|
62
|
+
let totalWarnings = 0;
|
|
63
|
+
|
|
64
|
+
results.forEach((result) => {
|
|
65
|
+
totalErrors += result.errorCount;
|
|
66
|
+
totalWarnings += result.warningCount;
|
|
67
|
+
|
|
68
|
+
if (result.messages.length > 0) {
|
|
69
|
+
output.push(`\n${chalk.cyan(result.filePath)}`);
|
|
70
|
+
|
|
71
|
+
// Read source file once
|
|
72
|
+
let sourceLines: string[] = [];
|
|
73
|
+
if (existsSync(result.filePath)) {
|
|
74
|
+
try {
|
|
75
|
+
const sourceContent = readFileSync(result.filePath, "utf8");
|
|
76
|
+
sourceLines = sourceContent.split("\n");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Ignore read errors
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result.messages.forEach((message: any) => {
|
|
83
|
+
const type = message.severity === 2 ? "error" : "warning";
|
|
84
|
+
const typeColor = message.severity === 2 ? chalk.red : chalk.yellow;
|
|
85
|
+
const icon = message.severity === 2 ? "❌" : "⚠️";
|
|
86
|
+
const ruleInfo = message.ruleId ? chalk.dim(` (${message.ruleId})`) : "";
|
|
87
|
+
|
|
88
|
+
output.push(`\n ${icon} ${typeColor(type)}: ${message.message}${ruleInfo}`);
|
|
89
|
+
output.push(` ${chalk.gray("at")} ${result.filePath}:${chalk.bold(`${message.line}:${message.column}`)}`);
|
|
90
|
+
|
|
91
|
+
// Show source line with underline
|
|
92
|
+
if (sourceLines.length > 0 && message.line <= sourceLines.length) {
|
|
93
|
+
const sourceLine = sourceLines[message.line - 1];
|
|
94
|
+
const lineNumber = message.line.toString().padStart(5, " ");
|
|
95
|
+
|
|
96
|
+
output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
|
|
97
|
+
|
|
98
|
+
// Create underline
|
|
99
|
+
const underlinePrefix = " ".repeat(message.column - 1);
|
|
100
|
+
const underlineLength = message.endColumn ? message.endColumn - message.column : 1;
|
|
101
|
+
const underline = "^".repeat(Math.max(1, underlineLength));
|
|
102
|
+
|
|
103
|
+
output.push(`${chalk.dim(`${" ".repeat(lineNumber.length)} |`)} ${underlinePrefix}${typeColor(underline)}`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (totalErrors === 0 && totalWarnings === 0) return chalk.bold("✅ No ESLint errors or warnings found");
|
|
110
|
+
|
|
111
|
+
const errorText = totalErrors > 0 ? chalk.red(`${totalErrors} error(s)`) : "0 errors";
|
|
112
|
+
const warningText = totalWarnings > 0 ? chalk.yellow(`${totalWarnings} warning(s)`) : "0 warnings";
|
|
113
|
+
const summary = [`\n${errorText}, ${warningText} found`];
|
|
114
|
+
|
|
115
|
+
return summary.concat(output).join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get detailed lint information
|
|
120
|
+
* @param filePath - Path to the file to lint
|
|
121
|
+
* @returns Object containing detailed lint information
|
|
122
|
+
*/
|
|
123
|
+
async getDetailedLintInfo(filePath: string): Promise<{
|
|
124
|
+
results: any[]; // ESLint.LintResult[];
|
|
125
|
+
details: {
|
|
126
|
+
line: number;
|
|
127
|
+
column: number;
|
|
128
|
+
message: string;
|
|
129
|
+
ruleId: string | null;
|
|
130
|
+
severity: "error" | "warning";
|
|
131
|
+
fix?: {
|
|
132
|
+
range: [number, number];
|
|
133
|
+
text: string;
|
|
134
|
+
};
|
|
135
|
+
suggestions?: {
|
|
136
|
+
desc: string;
|
|
137
|
+
fix: {
|
|
138
|
+
range: [number, number];
|
|
139
|
+
text: string;
|
|
140
|
+
};
|
|
141
|
+
}[];
|
|
142
|
+
}[];
|
|
143
|
+
stats: {
|
|
144
|
+
errorCount: number;
|
|
145
|
+
warningCount: number;
|
|
146
|
+
fixableErrorCount: number;
|
|
147
|
+
fixableWarningCount: number;
|
|
148
|
+
};
|
|
149
|
+
}> {
|
|
150
|
+
const { results } = await this.lintFile(filePath);
|
|
151
|
+
|
|
152
|
+
const details = results.flatMap((result) =>
|
|
153
|
+
result.messages.map((message: any) => ({
|
|
154
|
+
line: message.line,
|
|
155
|
+
column: message.column,
|
|
156
|
+
message: message.message,
|
|
157
|
+
ruleId: message.ruleId,
|
|
158
|
+
severity: message.severity === 2 ? ("error" as const) : ("warning" as const),
|
|
159
|
+
fix: message.fix,
|
|
160
|
+
suggestions: message.suggestions,
|
|
161
|
+
})),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const stats = results.reduce(
|
|
165
|
+
(acc, result) => ({
|
|
166
|
+
errorCount: acc.errorCount + result.errorCount,
|
|
167
|
+
warningCount: acc.warningCount + result.warningCount,
|
|
168
|
+
fixableErrorCount: acc.fixableErrorCount + result.fixableErrorCount,
|
|
169
|
+
fixableWarningCount: acc.fixableWarningCount + result.fixableWarningCount,
|
|
170
|
+
}),
|
|
171
|
+
{ errorCount: 0, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0 },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return { results, details, stats };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if a file has lint errors
|
|
179
|
+
* @param filePath - Path to the file to check
|
|
180
|
+
* @returns true if there are no errors, false otherwise
|
|
181
|
+
*/
|
|
182
|
+
async hasNoLintErrors(filePath: string): Promise<boolean> {
|
|
183
|
+
try {
|
|
184
|
+
const { results } = await this.lintFile(filePath);
|
|
185
|
+
return results.every((result) => result.errorCount === 0);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get only error messages (excluding warnings)
|
|
193
|
+
* @param filePath - Path to the file to lint
|
|
194
|
+
* @returns Array of error messages
|
|
195
|
+
*/
|
|
196
|
+
async getErrors(filePath: string): Promise<any[]> {
|
|
197
|
+
// ESLintLinter.LintMessage[]
|
|
198
|
+
const { results } = await this.lintFile(filePath);
|
|
199
|
+
return results.flatMap((result) => result.messages.filter((message: any) => message.severity === 2));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get only warning messages
|
|
204
|
+
* @param filePath - Path to the file to lint
|
|
205
|
+
* @returns Array of warning messages
|
|
206
|
+
*/
|
|
207
|
+
async getWarnings(filePath: string): Promise<any[]> {
|
|
208
|
+
// ESLintLinter.LintMessage[]
|
|
209
|
+
const { results } = await this.lintFile(filePath);
|
|
210
|
+
return results.flatMap((result) => result.messages.filter((message: any) => message.severity === 1));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Fix lint errors automatically
|
|
215
|
+
* @param filePath - Path to the file to fix
|
|
216
|
+
* @param dryRun - If true, returns the fixed content without writing to file
|
|
217
|
+
* @returns Fixed content and remaining issues
|
|
218
|
+
*/
|
|
219
|
+
async fixFile(
|
|
220
|
+
filePath: string,
|
|
221
|
+
dryRun = false,
|
|
222
|
+
): Promise<{
|
|
223
|
+
fixed: boolean;
|
|
224
|
+
output?: string;
|
|
225
|
+
results: any[]; // ESLint.LintResult[];
|
|
226
|
+
errors: any[]; // ESLintLinter.LintMessage[];
|
|
227
|
+
warnings: any[]; // ESLintLinter.LintMessage[];
|
|
228
|
+
}> {
|
|
229
|
+
if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
230
|
+
|
|
231
|
+
// const eslint = new ESLint({ cwd: this.lintRoot, fix: true });
|
|
232
|
+
// const results = await eslint.lintFiles([filePath]);
|
|
233
|
+
// const errors = results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
|
|
234
|
+
// const warnings = results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
|
|
235
|
+
// if (!dryRun) await ESLint.outputFixes(results);
|
|
236
|
+
|
|
237
|
+
// const fixedResult = results[0];
|
|
238
|
+
// return { fixed: fixedResult.output !== undefined, output: fixedResult.output, results, errors, warnings };
|
|
239
|
+
return { fixed: false, output: undefined, results: [], errors: [], warnings: [] };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get ESLint configuration for a file
|
|
244
|
+
* @param filePath - Path to the file
|
|
245
|
+
* @returns ESLint configuration object
|
|
246
|
+
*/
|
|
247
|
+
async getConfigForFile(filePath: string): Promise<unknown> {
|
|
248
|
+
// const eslint = new ESLint();
|
|
249
|
+
// const config = (await eslint.calculateConfigForFile(filePath)) as unknown;
|
|
250
|
+
// return config;
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get rules that are causing errors in a file
|
|
256
|
+
* @param filePath - Path to the file to check
|
|
257
|
+
* @returns Object mapping rule IDs to their error counts
|
|
258
|
+
*/
|
|
259
|
+
async getProblematicRules(filePath: string): Promise<Record<string, number>> {
|
|
260
|
+
const { results } = await this.lintFile(filePath);
|
|
261
|
+
const ruleCounts: Record<string, number> = {};
|
|
262
|
+
|
|
263
|
+
results.forEach((result) => {
|
|
264
|
+
result.messages.forEach((message: any) => {
|
|
265
|
+
if (message.ruleId) ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return ruleCounts;
|
|
270
|
+
}
|
|
271
|
+
}
|
package/mobile/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mobileTarget";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { AkanMobileTargetConfig } from "../akanConfig";
|
|
3
|
+
import type { App } from "../commandDecorators";
|
|
4
|
+
import { getMobileTargetChoices, resolveMobilePath, resolveMobileTargets, targetHtmlFilename } from "./mobileTarget";
|
|
5
|
+
|
|
6
|
+
const target = {
|
|
7
|
+
name: "akanjs",
|
|
8
|
+
basePath: "akanjs",
|
|
9
|
+
appName: "Akanjs",
|
|
10
|
+
appId: "com.akanjs.app",
|
|
11
|
+
version: "1.0.0",
|
|
12
|
+
buildNum: 1,
|
|
13
|
+
} as AkanMobileTargetConfig;
|
|
14
|
+
|
|
15
|
+
describe("mobile target helpers", () => {
|
|
16
|
+
test("maps deep links into target base paths without duplicating prefixes", () => {
|
|
17
|
+
expect(resolveMobilePath(target, "/order/123")).toBe("/akanjs/order/123");
|
|
18
|
+
expect(resolveMobilePath(target, "/akanjs/order/123")).toBe("/akanjs/order/123");
|
|
19
|
+
expect(resolveMobilePath({ ...target, basePath: undefined }, "/order/123")).toBe("/order/123");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("selects the self-contained html file for a target", () => {
|
|
23
|
+
expect(targetHtmlFilename(target)).toBe("akanjs.html");
|
|
24
|
+
expect(targetHtmlFilename({ ...target, basePath: undefined })).toBe("index.html");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("lists route base paths when only one mobile target is configured", async () => {
|
|
28
|
+
const app = {
|
|
29
|
+
getConfig: async () => ({
|
|
30
|
+
basePaths: new Set(["akanjs", "soft", "office"]),
|
|
31
|
+
mobile: { targets: { akanjs: target } },
|
|
32
|
+
}),
|
|
33
|
+
} as unknown as App;
|
|
34
|
+
|
|
35
|
+
await expect(getMobileTargetChoices(app)).resolves.toEqual(["akanjs", "soft", "office"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("resolves a route base path onto the default mobile target config", async () => {
|
|
39
|
+
const app = {
|
|
40
|
+
getConfig: async () => ({
|
|
41
|
+
basePaths: new Set(["akanjs", "soft", "office"]),
|
|
42
|
+
mobile: { targets: { akanjs: target } },
|
|
43
|
+
}),
|
|
44
|
+
} as unknown as App;
|
|
45
|
+
|
|
46
|
+
await expect(resolveMobileTargets(app, "soft")).resolves.toEqual([
|
|
47
|
+
{
|
|
48
|
+
name: "soft",
|
|
49
|
+
config: { ...target, name: "soft", basePath: "soft" },
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { AkanMobileTargetConfig, MobileEnv } from "../akanConfig";
|
|
2
|
+
import type { App } from "../commandDecorators";
|
|
3
|
+
|
|
4
|
+
export type MobilePlatform = "ios" | "android";
|
|
5
|
+
export type MobileTargetSelection = string | "all" | undefined;
|
|
6
|
+
|
|
7
|
+
export interface ResolvedMobileTarget {
|
|
8
|
+
name: string;
|
|
9
|
+
config: AkanMobileTargetConfig;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly MobileEnv[];
|
|
13
|
+
|
|
14
|
+
export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]> => {
|
|
15
|
+
const config = await app.getConfig();
|
|
16
|
+
return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
|
|
20
|
+
const config = await app.getConfig();
|
|
21
|
+
const targetNames = Object.keys(config.mobile.targets);
|
|
22
|
+
if (targetNames.length > 1) return targetNames;
|
|
23
|
+
const basePaths = [...config.basePaths];
|
|
24
|
+
if (basePaths.length > 1) return basePaths;
|
|
25
|
+
if (targetNames.length > 0) return targetNames;
|
|
26
|
+
return basePaths;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const resolveMobileTargetByBasePath = (
|
|
30
|
+
targets: ResolvedMobileTarget[],
|
|
31
|
+
basePath: string,
|
|
32
|
+
): ResolvedMobileTarget | undefined => {
|
|
33
|
+
const normalizedBasePath = basePath.replace(/^\/+|\/+$/g, "");
|
|
34
|
+
const byBasePath = targets.find((target) => target.config.basePath?.replace(/^\/+|\/+$/g, "") === normalizedBasePath);
|
|
35
|
+
if (byBasePath) return byBasePath;
|
|
36
|
+
const [template] = targets;
|
|
37
|
+
if (!template) return undefined;
|
|
38
|
+
return {
|
|
39
|
+
name: normalizedBasePath,
|
|
40
|
+
config: {
|
|
41
|
+
...template.config,
|
|
42
|
+
name: normalizedBasePath,
|
|
43
|
+
basePath: normalizedBasePath,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const resolveMobileTargets = async (
|
|
49
|
+
app: App,
|
|
50
|
+
selection: MobileTargetSelection,
|
|
51
|
+
): Promise<ResolvedMobileTarget[]> => {
|
|
52
|
+
const config = await app.getConfig();
|
|
53
|
+
const targets = await getMobileTargets(app);
|
|
54
|
+
if (targets.length === 0) throw new Error(`No mobile targets configured for ${app.name}`);
|
|
55
|
+
if (!selection) {
|
|
56
|
+
const choices = await getMobileTargetChoices(app);
|
|
57
|
+
if (choices.length === 1) return resolveMobileTargets(app, choices[0]);
|
|
58
|
+
throw new Error(`Multiple mobile targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
|
|
59
|
+
}
|
|
60
|
+
if (selection === "all") {
|
|
61
|
+
if (Object.keys(config.mobile.targets).length > 1) return targets;
|
|
62
|
+
const basePaths = [...config.basePaths];
|
|
63
|
+
if (basePaths.length > 1) {
|
|
64
|
+
return basePaths.flatMap((basePath) => {
|
|
65
|
+
const resolved = resolveMobileTargetByBasePath(targets, basePath);
|
|
66
|
+
return resolved ? [resolved] : [];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return targets;
|
|
70
|
+
}
|
|
71
|
+
const target = targets.find((candidate) => candidate.name === selection);
|
|
72
|
+
if (target) return [target];
|
|
73
|
+
const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
|
|
74
|
+
if (basePathTarget && config.basePaths.has(selection.replace(/^\/+|\/+$/g, ""))) return [basePathTarget];
|
|
75
|
+
const choices = await getMobileTargetChoices(app);
|
|
76
|
+
throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: string) => {
|
|
80
|
+
const basePath = target.basePath?.replace(/^\/+|\/+$/g, "");
|
|
81
|
+
const normalizedPath = `/${pathname.replace(/^\/+/, "")}`;
|
|
82
|
+
if (!basePath) return normalizedPath;
|
|
83
|
+
if (normalizedPath === `/${basePath}` || normalizedPath.startsWith(`/${basePath}/`)) return normalizedPath;
|
|
84
|
+
return `/${basePath}${normalizedPath === "/" ? "" : normalizedPath}`;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const targetHtmlFilename = (target: AkanMobileTargetConfig) =>
|
|
88
|
+
target.basePath?.replace(/^\/+|\/+$/g, "") ? `${target.basePath.replace(/^\/+|\/+$/g, "")}.html` : "index.html";
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "2.1.0-rc.1",
|
|
4
4
|
"sourceType": "module",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"publishConfig": {
|
|
6
7
|
"access": "public"
|
|
7
8
|
},
|
|
@@ -12,41 +13,57 @@
|
|
|
12
13
|
"url": "https://github.com/akan-team/akanjs.git",
|
|
13
14
|
"directory": "pkgs/@akanjs/devkit"
|
|
14
15
|
},
|
|
15
|
-
"main": "./index.js",
|
|
16
16
|
"engines": {
|
|
17
|
-
"
|
|
17
|
+
"bun": ">=1.3.13"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./index.ts",
|
|
22
|
+
"types": "./index.ts",
|
|
23
|
+
"default": "./index.ts"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
"./capacitor.base.config": {
|
|
27
|
+
"import": "./capacitor.base.config.ts",
|
|
28
|
+
"types": "./capacitor.base.config.ts",
|
|
29
|
+
"default": "./capacitor.base.config.ts"
|
|
30
|
+
},
|
|
31
|
+
"./*": "./*"
|
|
18
32
|
},
|
|
19
33
|
"dependencies": {
|
|
20
|
-
"@inquirer/prompts": "^
|
|
21
|
-
"@langchain/core": "^
|
|
22
|
-
"@langchain/deepseek": "^
|
|
23
|
-
"@langchain/openai": "^
|
|
24
|
-
"@trapezedev/project": "^7.1.
|
|
25
|
-
"
|
|
26
|
-
"chalk": "^5.
|
|
27
|
-
"commander": "^14.0.
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"esbuild-plugin-d.ts": "^1.3.1",
|
|
31
|
-
"eslint": "^9.0.0",
|
|
32
|
-
"form-data": "^4.0.1",
|
|
34
|
+
"@inquirer/prompts": "^8.4.3",
|
|
35
|
+
"@langchain/core": "^1.1.47",
|
|
36
|
+
"@langchain/deepseek": "^1.0.26",
|
|
37
|
+
"@langchain/openai": "^1.4.6",
|
|
38
|
+
"@trapezedev/project": "^7.1.4",
|
|
39
|
+
"akanjs": "2.1.0-rc.0",
|
|
40
|
+
"chalk": "^5.6.2",
|
|
41
|
+
"commander": "^14.0.3",
|
|
42
|
+
"fontaine": "^0.8.0",
|
|
43
|
+
"fonteditor-core": "^2.6.3",
|
|
33
44
|
"ignore": "^7.0.5",
|
|
34
|
-
"ink": "^6.
|
|
35
|
-
"js-yaml": "^4.1.
|
|
36
|
-
"ora": "^
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"typescript": "
|
|
45
|
+
"ink": "^6.8.0",
|
|
46
|
+
"js-yaml": "^4.1.1",
|
|
47
|
+
"ora": "^9.4.0",
|
|
48
|
+
"ssh2": "^1.17.0",
|
|
49
|
+
"subset-font": "^2.5.0",
|
|
50
|
+
"tailwindcss": "^4.3.0",
|
|
51
|
+
"typescript": "^6.0.3"
|
|
41
52
|
},
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@capacitor/cli": "^8.3.4",
|
|
55
|
+
"react": "19.2.6"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@capacitor/cli": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"react": {
|
|
62
|
+
"optional": true
|
|
47
63
|
}
|
|
48
64
|
},
|
|
49
|
-
"
|
|
50
|
-
|
|
65
|
+
"devDependencies": {},
|
|
66
|
+
"bun": {
|
|
67
|
+
"platform": "bun"
|
|
51
68
|
}
|
|
52
|
-
}
|
|
69
|
+
}
|
package/prompter.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fsPromise from "node:fs/promises";
|
|
2
|
+
import { input, select } from "@inquirer/prompts";
|
|
3
|
+
|
|
4
|
+
import { getDirname } from "./getDirname";
|
|
5
|
+
import type { GuideGenerateJson } from "./guideline";
|
|
6
|
+
|
|
7
|
+
interface FileUpdateRequestProps {
|
|
8
|
+
context: string;
|
|
9
|
+
request: string;
|
|
10
|
+
}
|
|
11
|
+
export class Prompter {
|
|
12
|
+
static async #getGuidelineRoot() {
|
|
13
|
+
const dirname = getDirname(import.meta.url);
|
|
14
|
+
const candidates = [`${dirname}/guidelines`, `${dirname}/../cli/guidelines`];
|
|
15
|
+
for (const candidate of candidates) {
|
|
16
|
+
try {
|
|
17
|
+
await fsPromise.access(candidate);
|
|
18
|
+
return candidate;
|
|
19
|
+
} catch {
|
|
20
|
+
// Try the next layout; source and bundled CLI resolve from different dirs.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return candidates[0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static async selectGuideline() {
|
|
27
|
+
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
28
|
+
const guideNames = (await fsPromise.readdir(guidelineRoot)).filter((name) => !name.startsWith("_"));
|
|
29
|
+
return await select({ message: "Select a guideline", choices: guideNames.map((name) => ({ name, value: name })) });
|
|
30
|
+
}
|
|
31
|
+
static async getGuideJson(guideName: string): Promise<GuideGenerateJson> {
|
|
32
|
+
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
33
|
+
const filePath = `${guidelineRoot}/${guideName}/${guideName}.generate.json`;
|
|
34
|
+
const guideJson = await fsPromise.readFile(filePath, "utf-8");
|
|
35
|
+
return JSON.parse(guideJson) as GuideGenerateJson;
|
|
36
|
+
}
|
|
37
|
+
static async getInstruction(guideName: string): Promise<string> {
|
|
38
|
+
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
39
|
+
const filePath = `${guidelineRoot}/${guideName}/${guideName}.instruction.md`;
|
|
40
|
+
const content = await fsPromise.readFile(filePath, "utf-8");
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
static async getUpdateRequest(guideName: string) {
|
|
44
|
+
return await input({ message: `What do you want to update in ${guideName}?` });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async makeTsFileUpdatePrompt({ context, request }: FileUpdateRequestProps) {
|
|
48
|
+
return `You are a senior developer writing TypeScript-based programs using Akan.js, an in-house framework. Here's an overview of the Akan.js framework:
|
|
49
|
+
${await this.getDocumentation("framework")}
|
|
50
|
+
Please understand the following background information, write code that meets the requirements, verify that it satisfies the validation conditions, and return the result.
|
|
51
|
+
|
|
52
|
+
# Background Information
|
|
53
|
+
\`\`\`markdown
|
|
54
|
+
${context}
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
# Requirements
|
|
58
|
+
\`\`\`markdown
|
|
59
|
+
${request}
|
|
60
|
+
\`\`\`
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
async getDocumentation(guideName: string) {
|
|
64
|
+
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
65
|
+
const filePath = `${guidelineRoot}/${guideName}/${guideName}.instruction.md`;
|
|
66
|
+
const document = await fsPromise.readFile(filePath, "utf-8");
|
|
67
|
+
return `\`\`\`markdown
|
|
68
|
+
${document}
|
|
69
|
+
\`\`\`
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
}
|