@akanjs/devkit 1.0.19 → 2.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +343 -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/README.md +0 -1
- 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
package/createTunnel.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SshOptions } from "akanjs/base";
|
|
2
|
+
|
|
3
|
+
import { type AppExecutor, WorkspaceExecutor } from "./executors";
|
|
4
|
+
import { createSshTunnel } from "./sshTunnel";
|
|
5
|
+
|
|
6
|
+
const getSshTunnelOptions = (app: AppExecutor, environment: string): SshOptions => {
|
|
7
|
+
const { serveDomain, repoName } = WorkspaceExecutor.getBaseDevEnv();
|
|
8
|
+
return {
|
|
9
|
+
host: `${app.name}-${environment}.${serveDomain}`,
|
|
10
|
+
port: process.env.SSH_TUNNEL_PORT ? parseInt(process.env.SSH_TUNNEL_PORT) : 32767,
|
|
11
|
+
username: process.env.SSH_TUNNEL_USERNAME ?? "root",
|
|
12
|
+
password: process.env.SSH_TUNNEL_PASSWORD ?? repoName,
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
interface TunnelOption {
|
|
17
|
+
app: AppExecutor;
|
|
18
|
+
environment: string;
|
|
19
|
+
port?: number;
|
|
20
|
+
}
|
|
21
|
+
export const createTunnel = async (
|
|
22
|
+
service: "redis" | "postgres",
|
|
23
|
+
{ app, environment, port = service === "postgres" ? 5432 : 6379 }: TunnelOption,
|
|
24
|
+
) => {
|
|
25
|
+
const sshOptions: SshOptions = getSshTunnelOptions(app, environment);
|
|
26
|
+
await createSshTunnel({
|
|
27
|
+
localHost: "0.0.0.0",
|
|
28
|
+
localPort: port,
|
|
29
|
+
srcHost: "0.0.0.0",
|
|
30
|
+
srcPort: port,
|
|
31
|
+
dstHost: `${service}-0.${service}-svc.${app.name}-${environment}.svc.cluster.local`,
|
|
32
|
+
dstPort: service === "postgres" ? 5432 : 6379,
|
|
33
|
+
sshOptions,
|
|
34
|
+
});
|
|
35
|
+
return `localhost:${port}`;
|
|
36
|
+
};
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { builtinModules } from "node:module";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import ignore from "ignore";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import type { App, Lib, Pkg } from "./commandDecorators";
|
|
6
|
+
import { FileSys } from "./fileSys";
|
|
7
|
+
import type { PackageJson, TsConfigJson } from "./types";
|
|
8
|
+
|
|
9
|
+
const testFileRegex = /\.(?:test|spec)\.[cm]?[tj]sx?$/;
|
|
10
|
+
const builtinModuleSet = new Set([...builtinModules, ...builtinModules.map((mod) => `node:${mod}`)]);
|
|
11
|
+
const stripShebang = (source: string) => source.replace(/^#!.*(?:\r?\n|$)/, "");
|
|
12
|
+
|
|
13
|
+
export class TypeScriptDependencyScanner {
|
|
14
|
+
#fileDependencies = new Map<string, string[]>();
|
|
15
|
+
#fileRuntimeDependencies = new Map<string, string[]>();
|
|
16
|
+
#fileTypeDependencies = new Map<string, string[]>();
|
|
17
|
+
#visitedFiles = new Set<string>();
|
|
18
|
+
readonly #tsTranspiler = new Bun.Transpiler({ loader: "ts" });
|
|
19
|
+
readonly #tsxTranspiler = new Bun.Transpiler({ loader: "tsx" });
|
|
20
|
+
private readonly directory: string;
|
|
21
|
+
private readonly rootPackageJson: PackageJson;
|
|
22
|
+
private readonly ig: ReturnType<typeof ignore>;
|
|
23
|
+
private readonly workspaceRoot: string;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
directory: string,
|
|
27
|
+
{
|
|
28
|
+
workspaceRoot,
|
|
29
|
+
rootPackageJson,
|
|
30
|
+
gitignorePatterns = [],
|
|
31
|
+
}: { workspaceRoot: string; tsconfig: TsConfigJson; rootPackageJson: PackageJson; gitignorePatterns?: string[] },
|
|
32
|
+
) {
|
|
33
|
+
this.directory = directory;
|
|
34
|
+
this.rootPackageJson = rootPackageJson;
|
|
35
|
+
this.ig = ignore().add(gitignorePatterns);
|
|
36
|
+
this.workspaceRoot = workspaceRoot;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getMonorepoDependencies(
|
|
40
|
+
projectName: string,
|
|
41
|
+
{ pkgs = [], libs = [] }: { pkgs?: string[]; libs?: string[] } = {},
|
|
42
|
+
): Promise<{ pkgDeps: string[]; libDeps: string[]; npmDeps: string[]; npmDevDeps: string[] }> {
|
|
43
|
+
const npmSet = new Set(
|
|
44
|
+
Object.keys({ ...this.rootPackageJson.dependencies, ...this.rootPackageJson.devDependencies }),
|
|
45
|
+
);
|
|
46
|
+
const pkgPathSet = new Set(pkgs);
|
|
47
|
+
const libPathSet = new Set(libs.map((lib) => `@libs/${lib}`));
|
|
48
|
+
await this.getDependencies();
|
|
49
|
+
const [npmDepSet, pkgPathDepSet, libPathDepSet] = this.#getImportSetsFromDependencies(
|
|
50
|
+
[npmSet, pkgPathSet, libPathSet],
|
|
51
|
+
this.#fileRuntimeDependencies,
|
|
52
|
+
);
|
|
53
|
+
const [npmDevDepSet] = this.#getImportSetsFromDependencies([npmSet], this.#fileTypeDependencies);
|
|
54
|
+
const pkgDeps = [...pkgPathDepSet];
|
|
55
|
+
const libDeps = [...libPathDepSet]
|
|
56
|
+
.map((path) => path.replace("@libs/", ""))
|
|
57
|
+
.filter((libName) => libName !== projectName);
|
|
58
|
+
return {
|
|
59
|
+
pkgDeps,
|
|
60
|
+
libDeps,
|
|
61
|
+
npmDeps: [...npmDepSet],
|
|
62
|
+
npmDevDeps: [...npmDevDepSet].filter((dep) => !npmDepSet.has(dep)),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getPackageBuildDependencies(
|
|
67
|
+
projectName: string,
|
|
68
|
+
): Promise<{ npmDeps: string[]; npmDevDeps: string[]; missingDeps: string[] }> {
|
|
69
|
+
const runtimeDeps = new Set<string>();
|
|
70
|
+
const devDeps = new Set<string>();
|
|
71
|
+
const sourceFiles = await this.#findTypeScriptFiles(this.directory, {
|
|
72
|
+
excludeBuildFiles: true,
|
|
73
|
+
excludeTestFiles: true,
|
|
74
|
+
});
|
|
75
|
+
const cssFiles = await this.#findCssFiles(this.directory);
|
|
76
|
+
|
|
77
|
+
for (const filePath of sourceFiles) {
|
|
78
|
+
const fileContent = await FileSys.readText(filePath);
|
|
79
|
+
const { imports, typeImports } = this.#extractImports(fileContent, filePath);
|
|
80
|
+
this.#addNormalizedImports(runtimeDeps, imports, projectName);
|
|
81
|
+
this.#addNormalizedImports(devDeps, typeImports, projectName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const filePath of cssFiles) {
|
|
85
|
+
const fileContent = await FileSys.readText(filePath);
|
|
86
|
+
this.#addNormalizedImports(runtimeDeps, this.#extractCssPluginImports(fileContent), projectName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const buildFilePath = path.join(this.directory, "build.ts");
|
|
90
|
+
if (await FileSys.fileExists(buildFilePath)) {
|
|
91
|
+
const fileContent = await FileSys.readText(buildFilePath);
|
|
92
|
+
const { imports, typeImports } = this.#extractImports(fileContent, buildFilePath);
|
|
93
|
+
this.#addNormalizedImports(devDeps, [...imports, ...typeImports], projectName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const dep of runtimeDeps) devDeps.delete(dep);
|
|
97
|
+
|
|
98
|
+
const rootDeps = { ...this.rootPackageJson.dependencies, ...this.rootPackageJson.devDependencies };
|
|
99
|
+
const missingDeps: string[] = [];
|
|
100
|
+
for (const dep of [...runtimeDeps, ...devDeps]) {
|
|
101
|
+
if (rootDeps[dep] || (await this.#hasWorkspacePackage(dep))) continue;
|
|
102
|
+
missingDeps.push(dep);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
npmDeps: [...runtimeDeps].sort(),
|
|
106
|
+
npmDevDeps: [...devDeps].sort(),
|
|
107
|
+
missingDeps: missingDeps.sort(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async #hasWorkspacePackage(dep: string) {
|
|
112
|
+
const packageJsonPath = path.join(this.workspaceRoot, "pkgs", dep, "package.json");
|
|
113
|
+
if (!(await Bun.file(packageJsonPath).exists())) return false;
|
|
114
|
+
try {
|
|
115
|
+
const packageJson = await FileSys.readJson<PackageJson>(packageJsonPath);
|
|
116
|
+
return packageJson.name === dep && !!packageJson.version;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getImportSets<DepSets extends Set<string>[]>(depSets: DepSets): Promise<DepSets> {
|
|
123
|
+
const fileDependencies = await this.getDependencies();
|
|
124
|
+
return this.#getImportSetsFromDependencies(depSets, fileDependencies);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#getImportSetsFromDependencies<DepSets extends Set<string>[]>(
|
|
128
|
+
depSets: DepSets,
|
|
129
|
+
fileDependencies: Map<string, string[]>,
|
|
130
|
+
): DepSets {
|
|
131
|
+
const importedDepSets = new Array<Set<string>>(depSets.length);
|
|
132
|
+
for (let i = 0; i < depSets.length; i++) importedDepSets[i] = new Set<string>();
|
|
133
|
+
fileDependencies.forEach((imps) => {
|
|
134
|
+
imps.forEach((imp) => {
|
|
135
|
+
if (imp.startsWith(".")) return;
|
|
136
|
+
const moduleName = imp;
|
|
137
|
+
const moduleNameParts = moduleName.split("/");
|
|
138
|
+
const subModuleLength = moduleNameParts.length;
|
|
139
|
+
for (let i = 0; i < subModuleLength; i++) {
|
|
140
|
+
const importName = moduleNameParts.slice(0, i + 1).join("/");
|
|
141
|
+
for (let j = 0; j < depSets.length; j++) {
|
|
142
|
+
if (depSets[j]?.has(importName)) {
|
|
143
|
+
importedDepSets[j]?.add(importName);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
return importedDepSets as DepSets;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getDependencies(): Promise<Map<string, string[]>> {
|
|
154
|
+
this.#fileDependencies.clear();
|
|
155
|
+
this.#fileRuntimeDependencies.clear();
|
|
156
|
+
this.#fileTypeDependencies.clear();
|
|
157
|
+
this.#visitedFiles.clear();
|
|
158
|
+
|
|
159
|
+
const files = await this.#findTypeScriptFiles(this.directory);
|
|
160
|
+
|
|
161
|
+
for (const file of files) await this.#analyzeFile(file, this.directory);
|
|
162
|
+
|
|
163
|
+
return this.#fileDependencies;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async #findTypeScriptFiles(
|
|
167
|
+
directory: string,
|
|
168
|
+
{
|
|
169
|
+
excludeBuildFiles = false,
|
|
170
|
+
excludeTestFiles = false,
|
|
171
|
+
}: { excludeBuildFiles?: boolean; excludeTestFiles?: boolean } = {},
|
|
172
|
+
): Promise<string[]> {
|
|
173
|
+
const files: string[] = [];
|
|
174
|
+
const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android"];
|
|
175
|
+
|
|
176
|
+
const glob = new Bun.Glob("**/*.{ts,tsx}");
|
|
177
|
+
for await (const filePath of glob.scan({ cwd: directory, onlyFiles: true })) {
|
|
178
|
+
if (skipDirs.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`))) continue;
|
|
179
|
+
if (excludeBuildFiles && filePath === "build.ts") continue;
|
|
180
|
+
if (excludeTestFiles && testFileRegex.test(filePath)) continue;
|
|
181
|
+
|
|
182
|
+
const fullPath = path.join(directory, filePath);
|
|
183
|
+
const relativePath = path.relative(this.workspaceRoot, fullPath);
|
|
184
|
+
if (this.ig.ignores(relativePath)) continue;
|
|
185
|
+
|
|
186
|
+
files.push(fullPath);
|
|
187
|
+
}
|
|
188
|
+
return files;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async #findCssFiles(directory: string): Promise<string[]> {
|
|
192
|
+
const files: string[] = [];
|
|
193
|
+
const skipDirs = ["node_modules", "dist", "build", ".git", ".next", "public", "ios", "android"];
|
|
194
|
+
const glob = new Bun.Glob("**/*.css");
|
|
195
|
+
for await (const filePath of glob.scan({ cwd: directory, onlyFiles: true })) {
|
|
196
|
+
if (skipDirs.some((dir) => filePath.includes(`/${dir}/`) || filePath.startsWith(`${dir}/`))) continue;
|
|
197
|
+
|
|
198
|
+
const fullPath = path.join(directory, filePath);
|
|
199
|
+
const relativePath = path.relative(this.workspaceRoot, fullPath);
|
|
200
|
+
if (this.ig.ignores(relativePath)) continue;
|
|
201
|
+
|
|
202
|
+
files.push(fullPath);
|
|
203
|
+
}
|
|
204
|
+
return files;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async #analyzeFile(filePath: string, baseDir: string): Promise<void> {
|
|
208
|
+
if (this.#visitedFiles.has(filePath)) return;
|
|
209
|
+
|
|
210
|
+
this.#visitedFiles.add(filePath);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const fileContent = await FileSys.readText(filePath);
|
|
214
|
+
const { imports, typeImports } = this.#extractImports(fileContent, filePath);
|
|
215
|
+
|
|
216
|
+
// Convert imports to absolute or relative paths
|
|
217
|
+
const resolvedImports = await this.#resolveImports(imports, filePath, baseDir);
|
|
218
|
+
const resolvedTypeImports = await this.#resolveImports(typeImports, filePath, baseDir);
|
|
219
|
+
|
|
220
|
+
// Store the dependencies
|
|
221
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
222
|
+
this.#fileDependencies.set(relativePath, [...new Set([...resolvedImports, ...resolvedTypeImports])]);
|
|
223
|
+
this.#fileRuntimeDependencies.set(relativePath, resolvedImports);
|
|
224
|
+
this.#fileTypeDependencies.set(relativePath, resolvedTypeImports);
|
|
225
|
+
} catch {
|
|
226
|
+
// Ignore files that cannot be parsed or read during best-effort dependency scanning.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async #resolveImports(imports: string[], filePath: string, baseDir: string): Promise<string[]> {
|
|
231
|
+
return await Promise.all(
|
|
232
|
+
imports.map(async (importPath) => {
|
|
233
|
+
if (importPath.startsWith(".")) {
|
|
234
|
+
// Handle relative imports
|
|
235
|
+
const resolvedPath = `./${path.join(path.relative(baseDir, filePath), importPath)}`;
|
|
236
|
+
return await this.#ensureExtension(resolvedPath);
|
|
237
|
+
}
|
|
238
|
+
return importPath; // Keep package imports as is
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async #ensureExtension(filePath: string): Promise<string> {
|
|
244
|
+
if (await FileSys.fileExists(`${filePath}.ts`)) return `${filePath}.ts`;
|
|
245
|
+
else if (await FileSys.fileExists(`${filePath}.tsx`)) return `${filePath}.tsx`;
|
|
246
|
+
else if (await FileSys.fileExists(filePath)) return filePath;
|
|
247
|
+
return `${filePath}.ts`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#extractImports(source: string, filePath: string) {
|
|
251
|
+
const transpiler = filePath.endsWith(".tsx") ? this.#tsxTranspiler : this.#tsTranspiler;
|
|
252
|
+
const scanSource = stripShebang(source);
|
|
253
|
+
const imports = new Set(
|
|
254
|
+
transpiler
|
|
255
|
+
.scanImports(scanSource)
|
|
256
|
+
.map((imp) => imp.path)
|
|
257
|
+
.filter(Boolean),
|
|
258
|
+
);
|
|
259
|
+
const typeImports = new Set<string>();
|
|
260
|
+
|
|
261
|
+
const sourceFile = ts.createSourceFile(filePath, scanSource, ts.ScriptTarget.Latest, true);
|
|
262
|
+
for (const statement of sourceFile.statements) {
|
|
263
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
264
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
265
|
+
|
|
266
|
+
const importPath = statement.moduleSpecifier.text;
|
|
267
|
+
const namedBindings = statement.importClause?.namedBindings;
|
|
268
|
+
const isNamedTypeOnlyImport =
|
|
269
|
+
namedBindings &&
|
|
270
|
+
ts.isNamedImports(namedBindings) &&
|
|
271
|
+
namedBindings.elements.length > 0 &&
|
|
272
|
+
namedBindings.elements.every((element) => element.isTypeOnly);
|
|
273
|
+
|
|
274
|
+
if ((statement.importClause?.isTypeOnly || isNamedTypeOnlyImport) && !imports.has(importPath)) {
|
|
275
|
+
typeImports.add(importPath);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { imports: [...imports], typeImports: [...typeImports] };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#extractCssPluginImports(source: string) {
|
|
283
|
+
const imports = new Set<string>();
|
|
284
|
+
const pluginRegex = /@plugin\s+(?:url\()?["']([^"')]+)["']\)?/g;
|
|
285
|
+
for (const match of source.matchAll(pluginRegex)) {
|
|
286
|
+
const importPath = match[1];
|
|
287
|
+
if (importPath) imports.add(importPath);
|
|
288
|
+
}
|
|
289
|
+
return [...imports];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#addNormalizedImports(deps: Set<string>, imports: string[], projectName: string) {
|
|
293
|
+
for (const importPath of imports) {
|
|
294
|
+
const dep = this.#normalizePackageImport(importPath, projectName);
|
|
295
|
+
if (dep) deps.add(dep);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#normalizePackageImport(importPath: string, projectName: string): string | null {
|
|
300
|
+
if (
|
|
301
|
+
importPath.startsWith(".") ||
|
|
302
|
+
importPath.startsWith("/") ||
|
|
303
|
+
importPath.startsWith("#") ||
|
|
304
|
+
importPath.startsWith("bun:") ||
|
|
305
|
+
builtinModuleSet.has(importPath)
|
|
306
|
+
)
|
|
307
|
+
return null;
|
|
308
|
+
|
|
309
|
+
const parts = importPath.split("/");
|
|
310
|
+
const packageName = importPath.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
311
|
+
if (!packageName || packageName === projectName || importPath.startsWith(`${projectName}/`)) return null;
|
|
312
|
+
return packageName;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
generateDependencyGraph(): string {
|
|
316
|
+
let graph = "Dependency Graph:\n\n";
|
|
317
|
+
|
|
318
|
+
for (const [file, imports] of this.#fileDependencies.entries()) {
|
|
319
|
+
graph += `${file}:\n`;
|
|
320
|
+
|
|
321
|
+
const projectImports = imports.filter((i) => !i.startsWith("react") && !i.startsWith("@"));
|
|
322
|
+
const externalImports = imports.filter((i) => i.startsWith("react") || i.startsWith("@"));
|
|
323
|
+
|
|
324
|
+
if (projectImports.length > 0) {
|
|
325
|
+
graph += " Project dependencies:\n";
|
|
326
|
+
projectImports.forEach((imp) => {
|
|
327
|
+
graph += ` → ${imp}\n`;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (externalImports.length > 0) {
|
|
332
|
+
graph += " External dependencies:\n";
|
|
333
|
+
externalImports.forEach((imp) => {
|
|
334
|
+
graph += ` → ${imp}\n`;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
graph += "\n";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return graph;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
static async from(exec: App | Lib | Pkg) {
|
|
345
|
+
const [tsconfig, rootPackageJson, gitignorePatterns] = await Promise.all([
|
|
346
|
+
exec.getTsConfig(),
|
|
347
|
+
exec.workspace.getPackageJson(),
|
|
348
|
+
exec.workspace.getGitignorePatterns(),
|
|
349
|
+
]);
|
|
350
|
+
return new TypeScriptDependencyScanner(exec.cwdPath, {
|
|
351
|
+
workspaceRoot: exec.workspace.cwdPath,
|
|
352
|
+
tsconfig,
|
|
353
|
+
rootPackageJson,
|
|
354
|
+
gitignorePatterns,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ApplicationBuildReporter } from "./applicationBuildReporter";
|
|
6
|
+
import { TypeScriptDependencyScanner } from "./dependencyScanner";
|
|
7
|
+
import { extractDependencies } from "./extractDeps";
|
|
8
|
+
import { getModelFileData } from "./getModelFileData";
|
|
9
|
+
import type { PackageJson, TsConfigJson } from "./types";
|
|
10
|
+
|
|
11
|
+
const tempRoots: string[] = [];
|
|
12
|
+
|
|
13
|
+
const makeTempRoot = async () => {
|
|
14
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-utils-"));
|
|
15
|
+
tempRoots.push(root);
|
|
16
|
+
return root;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const write = async (filePath: string, content: string) => {
|
|
20
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
21
|
+
await writeFile(filePath, content);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("extractDependencies", () => {
|
|
29
|
+
const packageJson: PackageJson = {
|
|
30
|
+
name: "fixture",
|
|
31
|
+
version: "1.0.0",
|
|
32
|
+
description: "fixture",
|
|
33
|
+
dependencies: {
|
|
34
|
+
react: "19.0.0",
|
|
35
|
+
"@scope/pkg": "1.0.0",
|
|
36
|
+
lodash: "4.0.0",
|
|
37
|
+
},
|
|
38
|
+
devDependencies: {
|
|
39
|
+
typescript: "6.0.0",
|
|
40
|
+
vite: "5.0.0",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
test("extracts runtime package versions from imports and requires", () => {
|
|
45
|
+
const deps = extractDependencies(
|
|
46
|
+
[
|
|
47
|
+
{
|
|
48
|
+
path: "index.ts",
|
|
49
|
+
text: [
|
|
50
|
+
'import React from "react";',
|
|
51
|
+
'import { value } from "@scope/pkg/subpath";',
|
|
52
|
+
'import type { Type } from "typescript";',
|
|
53
|
+
'const lodash = require("lodash/fp");',
|
|
54
|
+
'const fs = require("node:fs");',
|
|
55
|
+
'const path = require("path");',
|
|
56
|
+
"",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
},
|
|
59
|
+
{ path: "style.css", text: '@import "vite";' },
|
|
60
|
+
],
|
|
61
|
+
packageJson,
|
|
62
|
+
["vite"],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(deps).toEqual({
|
|
66
|
+
"@scope/pkg": "1.0.0",
|
|
67
|
+
lodash: "4.0.0",
|
|
68
|
+
react: "19.0.0",
|
|
69
|
+
typescript: "6.0.0",
|
|
70
|
+
vite: "5.0.0",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("reports missing dependency sections and missing versions", () => {
|
|
75
|
+
expect(() =>
|
|
76
|
+
extractDependencies([{ path: "index.ts", text: 'import React from "react";' }], {
|
|
77
|
+
name: "broken",
|
|
78
|
+
version: "1.0.0",
|
|
79
|
+
description: "broken",
|
|
80
|
+
}),
|
|
81
|
+
).toThrow("No dependencies found");
|
|
82
|
+
|
|
83
|
+
expect(() => extractDependencies([], packageJson, ["missing"])).toThrow("No version found for missing");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("TypeScriptDependencyScanner", () => {
|
|
88
|
+
test("separates monorepo package, lib, runtime, and type-only dependencies", async () => {
|
|
89
|
+
const root = await makeTempRoot();
|
|
90
|
+
const appDir = path.join(root, "apps/demo");
|
|
91
|
+
await write(
|
|
92
|
+
path.join(appDir, "index.ts"),
|
|
93
|
+
[
|
|
94
|
+
'import React from "react";',
|
|
95
|
+
'import { helper } from "@libs/shared/helper";',
|
|
96
|
+
'import { tool } from "akanjs/tool";',
|
|
97
|
+
'import type { Config } from "typescript";',
|
|
98
|
+
'import { local } from "./local";',
|
|
99
|
+
"console.log(React, helper, tool, local);",
|
|
100
|
+
"",
|
|
101
|
+
].join("\n"),
|
|
102
|
+
);
|
|
103
|
+
await write(path.join(appDir, "local.ts"), 'import "lodash";\nexport const local = 1;\n');
|
|
104
|
+
await write(path.join(appDir, "node_modules/ignored.ts"), 'import "ignored";\n');
|
|
105
|
+
await write(path.join(root, ".gitignore"), "ignored-dir\n");
|
|
106
|
+
|
|
107
|
+
const rootPackageJson: PackageJson = {
|
|
108
|
+
name: "repo",
|
|
109
|
+
version: "1.0.0",
|
|
110
|
+
description: "repo",
|
|
111
|
+
dependencies: {
|
|
112
|
+
react: "19.0.0",
|
|
113
|
+
lodash: "4.0.0",
|
|
114
|
+
},
|
|
115
|
+
devDependencies: {
|
|
116
|
+
typescript: "6.0.0",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const tsconfig: TsConfigJson = { compilerOptions: { target: "ESNext" } };
|
|
120
|
+
const scanner = new TypeScriptDependencyScanner(appDir, {
|
|
121
|
+
workspaceRoot: root,
|
|
122
|
+
tsconfig,
|
|
123
|
+
rootPackageJson,
|
|
124
|
+
gitignorePatterns: ["ignored-dir"],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const deps = await scanner.getMonorepoDependencies("demo", {
|
|
128
|
+
pkgs: ["akanjs/tool"],
|
|
129
|
+
libs: ["shared"],
|
|
130
|
+
});
|
|
131
|
+
expect(deps.pkgDeps).toEqual(["akanjs/tool"]);
|
|
132
|
+
expect(deps.libDeps).toEqual(["shared"]);
|
|
133
|
+
expect(deps.npmDeps.sort()).toEqual(["lodash", "react"]);
|
|
134
|
+
expect(deps.npmDevDeps).toEqual(["typescript"]);
|
|
135
|
+
|
|
136
|
+
const graph = scanner.generateDependencyGraph();
|
|
137
|
+
expect(graph).toContain("index.ts");
|
|
138
|
+
expect(graph).toContain("@libs/shared/helper");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("scans package build dependencies with normalized imports and css plugins", async () => {
|
|
142
|
+
const root = await makeTempRoot();
|
|
143
|
+
const pkgDir = path.join(root, "pkgs/akanjs");
|
|
144
|
+
await write(
|
|
145
|
+
path.join(pkgDir, "index.ts"),
|
|
146
|
+
[
|
|
147
|
+
"#!/usr/bin/env bun",
|
|
148
|
+
'import "lodash/fp";',
|
|
149
|
+
'import { AiOutlineApi } from "react-icons/ai";',
|
|
150
|
+
'import type { Config } from "typescript";',
|
|
151
|
+
'import type { DebouncedFunc } from "lodash";',
|
|
152
|
+
'import "node:path";',
|
|
153
|
+
'import "bun:test";',
|
|
154
|
+
'import "akanjs/client";',
|
|
155
|
+
"export const value = AiOutlineApi;",
|
|
156
|
+
"export type ToolConfig = Config & { debounced?: DebouncedFunc<() => void> };",
|
|
157
|
+
"",
|
|
158
|
+
].join("\n"),
|
|
159
|
+
);
|
|
160
|
+
await write(path.join(pkgDir, "styles.css"), '@plugin "tailwind-scrollbar";\n');
|
|
161
|
+
await write(path.join(pkgDir, "index.test.ts"), 'import "commander";\n');
|
|
162
|
+
await write(path.join(pkgDir, "build.ts"), 'import { Command } from "commander";\n');
|
|
163
|
+
await write(path.join(pkgDir, "commented.ts"), '// import type { Linter } from "eslint";\n');
|
|
164
|
+
|
|
165
|
+
const rootPackageJson: PackageJson = {
|
|
166
|
+
name: "repo",
|
|
167
|
+
version: "1.0.0",
|
|
168
|
+
description: "repo",
|
|
169
|
+
dependencies: {
|
|
170
|
+
lodash: "4.0.0",
|
|
171
|
+
"react-icons": "5.0.0",
|
|
172
|
+
"tailwind-scrollbar": "4.0.0",
|
|
173
|
+
},
|
|
174
|
+
devDependencies: {
|
|
175
|
+
commander: "14.0.0",
|
|
176
|
+
typescript: "6.0.0",
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const tsconfig: TsConfigJson = { compilerOptions: { target: "ESNext" } };
|
|
180
|
+
const scanner = new TypeScriptDependencyScanner(pkgDir, {
|
|
181
|
+
workspaceRoot: root,
|
|
182
|
+
tsconfig,
|
|
183
|
+
rootPackageJson,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const deps = await scanner.getPackageBuildDependencies("akanjs");
|
|
187
|
+
|
|
188
|
+
expect(deps.npmDeps).toEqual(["lodash", "react-icons", "tailwind-scrollbar"]);
|
|
189
|
+
expect(deps.npmDevDeps).toEqual(["commander", "typescript"]);
|
|
190
|
+
expect(deps.missingDeps).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("getModelFileData", () => {
|
|
195
|
+
test("reads model files and derives imported local, scalar, and lib models", async () => {
|
|
196
|
+
const root = await makeTempRoot();
|
|
197
|
+
const cwd = process.cwd();
|
|
198
|
+
process.chdir(root);
|
|
199
|
+
try {
|
|
200
|
+
await write(
|
|
201
|
+
path.join(root, "apps/demo/lib/post/post.constant.ts"),
|
|
202
|
+
[
|
|
203
|
+
'import { cnst as shared } from "@libs/shared";',
|
|
204
|
+
'import { User } from "../user/user.constant";',
|
|
205
|
+
'import { Money } from "../_money/money.constant";',
|
|
206
|
+
"export const Post = {};",
|
|
207
|
+
"",
|
|
208
|
+
].join("\n"),
|
|
209
|
+
);
|
|
210
|
+
await write(
|
|
211
|
+
path.join(root, "apps/demo/lib/post/post.Unit.tsx"),
|
|
212
|
+
"export default function Unit() { return null; }\n",
|
|
213
|
+
);
|
|
214
|
+
await write(
|
|
215
|
+
path.join(root, "apps/demo/lib/post/post.View.tsx"),
|
|
216
|
+
"export default function View() { return null; }\n",
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const data = await getModelFileData("apps/demo", "post");
|
|
220
|
+
expect(data).toMatchObject({
|
|
221
|
+
moduleType: "app",
|
|
222
|
+
moduleName: "demo",
|
|
223
|
+
modelName: "post",
|
|
224
|
+
importModelNames: ["user"],
|
|
225
|
+
hasImportScalar: true,
|
|
226
|
+
importLibNames: ["shared"],
|
|
227
|
+
});
|
|
228
|
+
expect(data.constantFileStr).toContain("export const Post");
|
|
229
|
+
expect(data.unitFileStr).toContain("Unit");
|
|
230
|
+
expect(data.viewFileStr).toContain("View");
|
|
231
|
+
} finally {
|
|
232
|
+
process.chdir(cwd);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("ApplicationBuildReporter", () => {
|
|
238
|
+
test("formats duration, phase lines, and nested errors", () => {
|
|
239
|
+
expect(ApplicationBuildReporter.formatDuration(999)).toBe("999ms");
|
|
240
|
+
expect(ApplicationBuildReporter.formatDuration(1234)).toBe("1.2s");
|
|
241
|
+
expect(ApplicationBuildReporter.formatDuration(65_000)).toBe("1m 5s");
|
|
242
|
+
expect(
|
|
243
|
+
ApplicationBuildReporter.formatPhaseLine({
|
|
244
|
+
id: "bundle",
|
|
245
|
+
label: "Bundle",
|
|
246
|
+
durationMs: 1500,
|
|
247
|
+
summary: "3 files",
|
|
248
|
+
}),
|
|
249
|
+
).toBe("✓ Bundle: 3 files (1.5s)");
|
|
250
|
+
|
|
251
|
+
const nested = new Error("outer", { cause: new Error("inner") });
|
|
252
|
+
expect(ApplicationBuildReporter.formatError(nested)).toBe("outer\nCaused by: inner");
|
|
253
|
+
|
|
254
|
+
const aggregate = new AggregateError([new Error("first"), { message: "second" }, "third"], "failed");
|
|
255
|
+
expect(ApplicationBuildReporter.formatError(aggregate)).toBe(
|
|
256
|
+
["failed", " first", " second", " third"].join("\n"),
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
});
|