@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.
Files changed (195) hide show
  1. package/README.ko.md +65 -0
  2. package/README.md +62 -6
  3. package/aiEditor.ts +304 -0
  4. package/akanApp/akanApp.host.ts +393 -0
  5. package/akanApp/index.ts +1 -0
  6. package/akanConfig/akanConfig.test.ts +236 -0
  7. package/akanConfig/akanConfig.ts +384 -0
  8. package/akanConfig/index.ts +2 -0
  9. package/akanConfig/types.ts +23 -0
  10. package/applicationBuildReporter.ts +69 -0
  11. package/applicationBuildRunner.ts +302 -0
  12. package/applicationReleasePackager.ts +206 -0
  13. package/artifact/implicitRootLayout.ts +155 -0
  14. package/artifact/index.ts +1 -0
  15. package/artifact/routeSeedIndex.test.ts +98 -0
  16. package/artifact/routeSeedIndex.ts +130 -0
  17. package/auth.ts +41 -0
  18. package/builder.ts +164 -0
  19. package/capacitor.base.config.ts +88 -0
  20. package/capacitorApp.ts +440 -0
  21. package/commandDecorators/argMeta.ts +102 -0
  22. package/commandDecorators/command.ts +351 -0
  23. package/commandDecorators/commandBuilder.ts +224 -0
  24. package/commandDecorators/commandDecorators.test.ts +212 -0
  25. package/commandDecorators/commandMeta.ts +7 -0
  26. package/commandDecorators/dependencyBuilder.ts +100 -0
  27. package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
  28. package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
  29. package/commandDecorators/targetMeta.ts +31 -0
  30. package/commandDecorators/types.ts +10 -0
  31. package/constants.ts +25 -0
  32. package/createTunnel.ts +36 -0
  33. package/dependencyScanner.ts +357 -0
  34. package/devkitUtils.test.ts +259 -0
  35. package/executors.test.ts +315 -0
  36. package/executors.ts +1390 -0
  37. package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
  38. package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
  39. package/fileSys.ts +39 -0
  40. package/frontendBuild/allRoutesBuilder.ts +103 -0
  41. package/frontendBuild/buildRouteClient.test.ts +190 -0
  42. package/frontendBuild/clientBuildTypes.ts +114 -0
  43. package/frontendBuild/clientEntriesBundler.ts +303 -0
  44. package/frontendBuild/clientEntryDiscovery.ts +199 -0
  45. package/frontendBuild/csrArtifactBuilder.ts +237 -0
  46. package/frontendBuild/cssCompiler.ts +286 -0
  47. package/frontendBuild/cssImportResolver.ts +116 -0
  48. package/frontendBuild/fontOptimizer.ts +427 -0
  49. package/frontendBuild/frontendBuild.test.ts +204 -0
  50. package/frontendBuild/hmrChangeClassifier.ts +28 -0
  51. package/frontendBuild/hmrWatcher.ts +102 -0
  52. package/frontendBuild/index.ts +18 -0
  53. package/frontendBuild/pagesBundleBuilder.ts +137 -0
  54. package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
  55. package/frontendBuild/precompressArtifacts.ts +59 -0
  56. package/frontendBuild/routeClientBuilder.ts +290 -0
  57. package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
  58. package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
  59. package/frontendBuild/vendorSpecifiers.ts +16 -0
  60. package/frontendBuild/watchRootResolver.ts +28 -0
  61. package/getCredentials.ts +19 -0
  62. package/getDirname.ts +3 -0
  63. package/getModelFileData.ts +59 -0
  64. package/getRelatedCnsts.ts +313 -0
  65. package/guideline.ts +19 -0
  66. package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
  67. package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
  68. package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
  69. package/incrementalBuilder/index.ts +1 -0
  70. package/{esm/src/index.js → index.ts} +28 -15
  71. package/lint/no-deep-internal-import.grit +25 -0
  72. package/lint/no-import-client-functions.grit +32 -0
  73. package/lint/no-import-external-library.grit +21 -0
  74. package/lint/no-js-private-class-method.grit +42 -0
  75. package/lint/no-use-client-in-server.grit +7 -0
  76. package/lint/non-scalar-props-restricted.grit +13 -0
  77. package/linter.ts +271 -0
  78. package/mobile/index.ts +1 -0
  79. package/mobile/mobileTarget.test.ts +53 -0
  80. package/mobile/mobileTarget.ts +88 -0
  81. package/package.json +48 -31
  82. package/prompter.ts +72 -0
  83. package/scanInfo.ts +606 -0
  84. package/selectModel.ts +11 -0
  85. package/{esm/src/spinner.js → spinner.ts} +22 -28
  86. package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
  87. package/sshTunnel.ts +152 -0
  88. package/{esm/src/streamAi.js → streamAi.ts} +18 -12
  89. package/transforms/barrelAnalyzer.ts +278 -0
  90. package/transforms/barrelImportsPlugin.ts +504 -0
  91. package/transforms/externalizeFrameworkPlugin.ts +185 -0
  92. package/transforms/index.ts +5 -0
  93. package/transforms/rscUseClientTransform.ts +59 -0
  94. package/transforms/transforms.test.ts +208 -0
  95. package/transforms/useClientBundlePlugin.ts +47 -0
  96. package/tsconfig.json +37 -0
  97. package/typeChecker.ts +264 -0
  98. package/types.ts +44 -0
  99. package/ui/MultiScrollList.tsx +242 -0
  100. package/ui/ScrollList.tsx +107 -0
  101. package/ui/index.ts +2 -0
  102. package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
  103. package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
  104. package/cjs/index.js +0 -21
  105. package/cjs/src/aiEditor.js +0 -311
  106. package/cjs/src/auth.js +0 -72
  107. package/cjs/src/builder.js +0 -114
  108. package/cjs/src/capacitorApp.js +0 -313
  109. package/cjs/src/commandDecorators/argMeta.js +0 -88
  110. package/cjs/src/commandDecorators/command.js +0 -324
  111. package/cjs/src/commandDecorators/commandMeta.js +0 -30
  112. package/cjs/src/commandDecorators/helpFormatter.js +0 -211
  113. package/cjs/src/commandDecorators/index.js +0 -31
  114. package/cjs/src/commandDecorators/targetMeta.js +0 -57
  115. package/cjs/src/commandDecorators/types.js +0 -15
  116. package/cjs/src/constants.js +0 -46
  117. package/cjs/src/createTunnel.js +0 -49
  118. package/cjs/src/dependencyScanner.js +0 -220
  119. package/cjs/src/executors.js +0 -964
  120. package/cjs/src/extractDeps.js +0 -103
  121. package/cjs/src/fileEditor.js +0 -120
  122. package/cjs/src/getCredentials.js +0 -44
  123. package/cjs/src/getDirname.js +0 -38
  124. package/cjs/src/getModelFileData.js +0 -66
  125. package/cjs/src/getRelatedCnsts.js +0 -260
  126. package/cjs/src/guideline.js +0 -15
  127. package/cjs/src/index.js +0 -65
  128. package/cjs/src/linter.js +0 -238
  129. package/cjs/src/prompter.js +0 -85
  130. package/cjs/src/scanInfo.js +0 -491
  131. package/cjs/src/selectModel.js +0 -46
  132. package/cjs/src/spinner.js +0 -93
  133. package/cjs/src/streamAi.js +0 -62
  134. package/cjs/src/typeChecker.js +0 -207
  135. package/cjs/src/types.js +0 -15
  136. package/cjs/src/uploadRelease.js +0 -112
  137. package/cjs/src/useStdoutDimensions.js +0 -43
  138. package/esm/index.js +0 -1
  139. package/esm/src/aiEditor.js +0 -282
  140. package/esm/src/auth.js +0 -42
  141. package/esm/src/builder.js +0 -81
  142. package/esm/src/commandDecorators/argMeta.js +0 -54
  143. package/esm/src/commandDecorators/command.js +0 -290
  144. package/esm/src/commandDecorators/commandMeta.js +0 -7
  145. package/esm/src/commandDecorators/targetMeta.js +0 -33
  146. package/esm/src/commandDecorators/types.js +0 -0
  147. package/esm/src/constants.js +0 -17
  148. package/esm/src/createTunnel.js +0 -26
  149. package/esm/src/dependencyScanner.js +0 -187
  150. package/esm/src/executors.js +0 -928
  151. package/esm/src/getCredentials.js +0 -11
  152. package/esm/src/getDirname.js +0 -5
  153. package/esm/src/getModelFileData.js +0 -33
  154. package/esm/src/getRelatedCnsts.js +0 -221
  155. package/esm/src/guideline.js +0 -0
  156. package/esm/src/linter.js +0 -205
  157. package/esm/src/prompter.js +0 -51
  158. package/esm/src/scanInfo.js +0 -455
  159. package/esm/src/selectModel.js +0 -13
  160. package/esm/src/typeChecker.js +0 -174
  161. package/esm/src/types.js +0 -0
  162. package/index.d.ts +0 -1
  163. package/src/aiEditor.d.ts +0 -50
  164. package/src/auth.d.ts +0 -9
  165. package/src/builder.d.ts +0 -18
  166. package/src/capacitorApp.d.ts +0 -39
  167. package/src/commandDecorators/argMeta.d.ts +0 -67
  168. package/src/commandDecorators/command.d.ts +0 -2
  169. package/src/commandDecorators/commandMeta.d.ts +0 -2
  170. package/src/commandDecorators/helpFormatter.d.ts +0 -3
  171. package/src/commandDecorators/index.d.ts +0 -6
  172. package/src/commandDecorators/targetMeta.d.ts +0 -19
  173. package/src/commandDecorators/types.d.ts +0 -1
  174. package/src/constants.d.ts +0 -26
  175. package/src/createTunnel.d.ts +0 -8
  176. package/src/dependencyScanner.d.ts +0 -23
  177. package/src/executors.d.ts +0 -296
  178. package/src/extractDeps.d.ts +0 -7
  179. package/src/fileEditor.d.ts +0 -16
  180. package/src/getCredentials.d.ts +0 -12
  181. package/src/getDirname.d.ts +0 -1
  182. package/src/getModelFileData.d.ts +0 -16
  183. package/src/getRelatedCnsts.d.ts +0 -53
  184. package/src/guideline.d.ts +0 -19
  185. package/src/index.d.ts +0 -23
  186. package/src/linter.d.ts +0 -109
  187. package/src/prompter.d.ts +0 -14
  188. package/src/scanInfo.d.ts +0 -82
  189. package/src/selectModel.d.ts +0 -1
  190. package/src/spinner.d.ts +0 -20
  191. package/src/streamAi.d.ts +0 -6
  192. package/src/typeChecker.d.ts +0 -52
  193. package/src/types.d.ts +0 -31
  194. package/src/uploadRelease.d.ts +0 -10
  195. package/src/useStdoutDimensions.d.ts +0 -1
@@ -0,0 +1,62 @@
1
+ import path from "node:path";
2
+ import type { RoutesManifest, SsrManifestEntry } from "akanjs/server";
3
+
4
+ export type SerializedRoutesManifest = Omit<RoutesManifest, "knownEntries"> &
5
+ Partial<Pick<RoutesManifest, "knownEntries">>;
6
+
7
+ export class RoutesManifestArtifactSerializer {
8
+ #manifest: RoutesManifest;
9
+ #artifactDir: string;
10
+ #production: boolean;
11
+
12
+ constructor(manifest: RoutesManifest, artifactDir: string, options: { production?: boolean } = {}) {
13
+ this.#manifest = manifest;
14
+ this.#artifactDir = path.resolve(artifactDir);
15
+ this.#production = options.production ?? false;
16
+ }
17
+
18
+ static serialize(
19
+ manifest: RoutesManifest,
20
+ artifactDir: string,
21
+ options: { production?: boolean } = {},
22
+ ): SerializedRoutesManifest {
23
+ return new RoutesManifestArtifactSerializer(manifest, artifactDir, options).serialize();
24
+ }
25
+
26
+ serialize(): SerializedRoutesManifest {
27
+ const serialized: SerializedRoutesManifest = {
28
+ ...this.#manifest,
29
+ knownEntries: this.#manifest.knownEntries.map((entry) => this.#serializeArtifactPath(entry)),
30
+ };
31
+ if (this.#production) delete serialized.knownEntries;
32
+ return {
33
+ ...serialized,
34
+ clientManifest: Object.fromEntries(
35
+ Object.entries(this.#manifest.clientManifest).map(([key, row]) => [this.#serializeArtifactPath(key), row]),
36
+ ),
37
+ ssrManifest: {
38
+ ...this.#manifest.ssrManifest,
39
+ moduleMap: Object.fromEntries(
40
+ Object.entries(this.#manifest.ssrManifest.moduleMap).map(([entryUrl, byName]) => [
41
+ entryUrl,
42
+ Object.fromEntries(
43
+ Object.entries(byName).map(([name, entry]) => [
44
+ name,
45
+ {
46
+ ...entry,
47
+ id: this.#serializeArtifactPath(entry.id),
48
+ chunks: entry.chunks.map((chunk) => this.#serializeArtifactPath(chunk)),
49
+ } satisfies SsrManifestEntry,
50
+ ]),
51
+ ),
52
+ ]),
53
+ ),
54
+ },
55
+ };
56
+ }
57
+
58
+ #serializeArtifactPath(artifactPath: string): string {
59
+ if (!path.isAbsolute(artifactPath)) return artifactPath;
60
+ return path.relative(this.#artifactDir, artifactPath).split(path.sep).join("/");
61
+ }
62
+ }
@@ -0,0 +1,139 @@
1
+ import path from "node:path";
2
+ import type { BaseBuildArtifact } from "akanjs/server";
3
+ import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
4
+ import { computeRouteSeedIndex, type RouteSeedIndex, saveRouteSeedIndex } from "../artifact/routeSeedIndex";
5
+ import type { App } from "../commandDecorators";
6
+ import { ClientEntriesBundler } from "./clientEntriesBundler";
7
+ import { CssCompiler } from "./cssCompiler";
8
+ import { FontOptimizer } from "./fontOptimizer";
9
+ import { PagesBundleBuilder } from "./pagesBundleBuilder";
10
+ import { VENDOR_SPECIFIERS, type VendorSpecifier } from "./vendorSpecifiers";
11
+
12
+ export interface BuildSsrBaseArtifactResult {
13
+ artifact: BaseBuildArtifact;
14
+ seedIndex: RouteSeedIndex;
15
+ cssCompiler: CssCompiler;
16
+ optimizedFonts: Awaited<ReturnType<FontOptimizer["optimize"]>>;
17
+ }
18
+
19
+ export class SsrBaseArtifactBuilder {
20
+ #app: App;
21
+ #command: "build" | "start";
22
+ #artifactDir: string;
23
+ #absArtifactDir: string;
24
+ #started = Date.now();
25
+
26
+ constructor(app: App, command: "build" | "start" = "start") {
27
+ this.#app = app;
28
+ this.#command = command;
29
+ this.#artifactDir = `${command === "build" ? app.dist.cwdPath : app.cwdPath}/.akan/artifact`;
30
+ this.#absArtifactDir = path.resolve(this.#artifactDir);
31
+ }
32
+
33
+ async build(): Promise<BuildSsrBaseArtifactResult> {
34
+ const akanConfig = await this.#app.getConfig();
35
+ const { rscClientUrl, vendorMap } = await this.#buildRuntimeClientEntries();
36
+ const pageKeys = await this.#app.getPageKeys();
37
+ this.#app.verbose(`[base-artifact] discovered ${pageKeys.length} route files under ${this.#app.cwdPath}/page`);
38
+
39
+ const pageEntries = await resolveSsrPageEntriesForApp(this.#app, pageKeys);
40
+ const { cssCompiler, optimizedFonts, cssAssets } = await this.#buildStyleAssets();
41
+ const pagesBundle = await new PagesBundleBuilder(this.#app, this.#command, pageEntries).build();
42
+ this.#app.verbose(
43
+ `[base-artifact] pages bundle -> ${pagesBundle.bundlePath} (buildId=${pagesBundle.buildId}, splitting=${pagesBundle.splitting}, entry=${pagesBundle.entryBytes} bytes, outputs=${pagesBundle.outputCount}, chunks=${pagesBundle.chunkCount}, total=${pagesBundle.outputBytes} bytes)`,
44
+ );
45
+
46
+ const seedIndex = computeRouteSeedIndex(pageEntries);
47
+ const seedIndexPath = await saveRouteSeedIndex(this.#absArtifactDir, seedIndex, {
48
+ production: this.#command === "build",
49
+ });
50
+ this.#app.verbose(
51
+ `[base-artifact] route seed index -> ${seedIndexPath} entries=${seedIndex.entries.length} globalLayouts=${seedIndex.globalLayoutFiles.length}`,
52
+ );
53
+
54
+ const artifact: BaseBuildArtifact = {
55
+ rscClientUrl,
56
+ vendorMap,
57
+ cssAssets,
58
+ pagesBundlePath:
59
+ this.#command === "build"
60
+ ? path.relative(this.#absArtifactDir, pagesBundle.bundlePath)
61
+ : pagesBundle.bundlePath,
62
+ pagesBundleBuildId: pagesBundle.buildId,
63
+ domains: [...akanConfig.domains],
64
+ subRoutes: Object.fromEntries(
65
+ Array.from(akanConfig.subRoutes.entries()).map(([basePath, domains]) => [basePath, [...domains]]),
66
+ ),
67
+ basePaths: [...akanConfig.basePaths],
68
+ branches: [...akanConfig.branches],
69
+ i18n: akanConfig.i18n,
70
+ imageConfig: akanConfig.images,
71
+ };
72
+ await Bun.write(path.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}\n`);
73
+ this.#app.verbose(`[base-artifact] complete in ${Date.now() - this.#started}ms`);
74
+
75
+ return { artifact, seedIndex, cssCompiler, optimizedFonts };
76
+ }
77
+
78
+ async #buildRuntimeClientEntries(): Promise<{ rscClientUrl: string; vendorMap: Record<VendorSpecifier, string> }> {
79
+ const akanServerPath = await this.#resolveAkanServerPath();
80
+ const rscClientEntry = `${akanServerPath}/rscClient.tsx`;
81
+ const vendorEntries = VENDOR_SPECIFIERS.map((specifier) => ({
82
+ specifier,
83
+ absPath: `${akanServerPath}/vendor/${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`,
84
+ }));
85
+ const entries = [rscClientEntry, ...vendorEntries.map((v) => v.absPath)];
86
+ const clientBundle = await new ClientEntriesBundler({ app: this.#app, entries, command: this.#command }).bundle();
87
+ const rscClientUrl = clientBundle.entryUrlsByAbsPath.get(rscClientEntry) ?? "";
88
+ const vendorMap = Object.fromEntries(
89
+ vendorEntries.map(({ specifier, absPath }) => [specifier, clientBundle.entryUrlsByAbsPath.get(absPath) ?? ""]),
90
+ ) as Record<VendorSpecifier, string>;
91
+ this.#app.verbose(`[base-artifact] rscClientUrl=${rscClientUrl} vendors=${Object.keys(vendorMap).length}`);
92
+ return { rscClientUrl, vendorMap };
93
+ }
94
+ async #resolveAkanServerPath() {
95
+ const candidates = [
96
+ path.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server"),
97
+ path.resolve(import.meta.dir, "../../server"),
98
+ path.resolve(import.meta.dir, "../server"),
99
+ ];
100
+ for (const candidate of candidates) {
101
+ if (await Bun.file(path.join(candidate, "rscClient.tsx")).exists()) return candidate;
102
+ }
103
+ return candidates[0];
104
+ }
105
+
106
+ async #buildStyleAssets(): Promise<{
107
+ cssCompiler: CssCompiler;
108
+ optimizedFonts: Awaited<ReturnType<FontOptimizer["optimize"]>>;
109
+ cssAssets: BaseBuildArtifact["cssAssets"];
110
+ }> {
111
+ const cssCompiler = new CssCompiler(this.#app);
112
+ const cssByBasePath = await cssCompiler.getCssByBasePath();
113
+ const optimizedFonts = await new FontOptimizer(this.#app, this.#command).optimize();
114
+ const cssAssets = Object.fromEntries(
115
+ await Promise.all(
116
+ Object.entries(cssByBasePath).flatMap(([basePath, baseCssText]) => {
117
+ const cssText = [baseCssText, optimizedFonts.css].filter(Boolean).join("\n");
118
+ if (!cssText) return [];
119
+ return [this.#writeCssAsset(basePath, cssText)];
120
+ }),
121
+ ),
122
+ );
123
+ if (optimizedFonts.files.length > 0)
124
+ this.#app.verbose(`[base-artifact] optimized ${optimizedFonts.files.length} font files`);
125
+ return { cssCompiler, optimizedFonts, cssAssets };
126
+ }
127
+
128
+ async #writeCssAsset(basePath: string, cssText: string) {
129
+ const cssAssetName = basePath || "root";
130
+ const cssHash = Bun.hash(`${basePath}\n${cssText}`).toString(36);
131
+ const [cssRelPath, cssUrl] = [
132
+ `styles/${cssAssetName}-${cssHash}.css`,
133
+ `/_akan/styles/${cssAssetName}-${cssHash}.css`,
134
+ ];
135
+ await Bun.write(path.join(this.#absArtifactDir, cssRelPath), cssText);
136
+ this.#app.verbose(`[base-artifact] wrote ${cssText.length} bytes of CSS for ${basePath} -> ${cssRelPath}`);
137
+ return [basePath, { cssUrl, cssRelPath }] as const;
138
+ }
139
+ }
@@ -0,0 +1,16 @@
1
+ export const VENDOR_SPECIFIERS = [
2
+ "react",
3
+ "react-dom",
4
+ "react-dom/client",
5
+ "react/jsx-runtime",
6
+ "react/jsx-dev-runtime",
7
+ "react-refresh/runtime",
8
+ "scheduler",
9
+ "react-server-dom-webpack/client.browser",
10
+ "akanjs/store",
11
+ "akanjs/base",
12
+ "akanjs/common",
13
+ "akanjs/constant",
14
+ ] as const;
15
+
16
+ export type VendorSpecifier = (typeof VENDOR_SPECIFIERS)[number];
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { App } from "../commandDecorators";
4
+
5
+ export class WatchRootResolver {
6
+ #app: App;
7
+
8
+ constructor(app: App) {
9
+ this.#app = app;
10
+ }
11
+
12
+ async resolve(): Promise<string[]> {
13
+ const tsconfig = await this.#app.getTsConfig();
14
+ const set = new Set<string>();
15
+ set.add(path.resolve(`${this.#app.cwdPath}/page`));
16
+ for (const targets of Object.values(tsconfig.compilerOptions.paths ?? {})) {
17
+ for (const target of targets) {
18
+ if (!target) continue;
19
+ if (path.isAbsolute(target)) continue;
20
+ // Strip the trailing filename and glob so we watch the package root dir.
21
+ const cleaned = target.replace(/\/?\*+.*$/, "").replace(/\/[^/]+\.[^/]+$/, "");
22
+ const resolved = path.resolve(this.#app.workspace.workspaceRoot, cleaned);
23
+ if (fs.existsSync(resolved)) set.add(resolved);
24
+ }
25
+ }
26
+ return [...set];
27
+ }
28
+ }
@@ -0,0 +1,19 @@
1
+ import yaml from "js-yaml";
2
+
3
+ import type { AppExecutor } from "./executors";
4
+ import { FileSys } from "./fileSys";
5
+
6
+ export interface AppSecret {
7
+ postgres?: { account?: { user?: { username: string; password: string } } };
8
+ }
9
+ interface Secret {
10
+ [key: string]: AppSecret;
11
+ }
12
+
13
+ export const getCredentials = async (app: AppExecutor, environment: string): Promise<AppSecret> => {
14
+ const content = await FileSys.readText(`${app.workspace.workspaceRoot}/infra/app/values/${app.name}-secret.yaml`);
15
+ const secret = yaml.load(content) as Secret;
16
+ const appSecret = secret[environment];
17
+ if (!appSecret) throw new Error(`No secret found for ${app.name} in ${environment}`);
18
+ return appSecret;
19
+ };
package/getDirname.ts ADDED
@@ -0,0 +1,3 @@
1
+ import path from "node:path";
2
+
3
+ export const getDirname = (url: string) => path.dirname(new URL(url).pathname);
@@ -0,0 +1,59 @@
1
+ interface ModelFileData {
2
+ moduleType: "lib" | "app";
3
+ moduleName: string;
4
+ modelName: string;
5
+ constantFilePath: string;
6
+ importModelNames: string[];
7
+ hasImportScalar: boolean;
8
+ importLibNames: string[];
9
+ constantFileStr: string;
10
+ unitFilePath: string;
11
+ unitFileStr: string;
12
+ viewFilePath: string;
13
+ viewFileStr: string;
14
+ }
15
+
16
+ export const getModelFileData = async (modulePath: string, modelName: string): Promise<ModelFileData> => {
17
+ const moduleType = modulePath.startsWith("apps") ? "app" : "lib";
18
+ const moduleName = modulePath.split("/")[1] ?? "";
19
+ const constantFilePath = `${modulePath}/lib/${modelName}/${modelName}.constant.ts`;
20
+ const unitFilePath = `${modulePath}/lib/${modelName}/${modelName}.Unit.tsx`;
21
+ const viewFilePath = `${modulePath}/lib/${modelName}/${modelName}.View.tsx`;
22
+ const [constantFileStr, unitFileStr, viewFileStr] = await Promise.all([
23
+ Bun.file(constantFilePath).text(),
24
+ Bun.file(unitFilePath).text(),
25
+ Bun.file(viewFilePath).text(),
26
+ ]);
27
+
28
+ const constantFileLines = constantFileStr.split("\n");
29
+ const importLibNames = constantFileLines
30
+ .filter((line) => line.startsWith("import { cnst as "))
31
+ .map((line) => line.split("cnst as ")[1]?.split(" ")[0] ?? "");
32
+
33
+ const importLocalPaths = constantFileLines
34
+ .filter((line) => line.startsWith("import { ") && line.includes('from "../'))
35
+ .map((line) => line.split("from ")[1]?.split('"')[1] ?? "");
36
+
37
+ const importModelNames = importLocalPaths
38
+ .map((path) => path.split("/")[1] ?? "")
39
+ .filter((name) => !name.startsWith("_"));
40
+
41
+ const hasImportScalar = !!importLocalPaths
42
+ .map((path) => path.split("/")[1] ?? "")
43
+ .filter((name) => name.startsWith("_")).length;
44
+
45
+ return {
46
+ moduleType,
47
+ moduleName,
48
+ modelName,
49
+ constantFilePath,
50
+ unitFilePath,
51
+ viewFilePath,
52
+ importModelNames,
53
+ hasImportScalar,
54
+ importLibNames,
55
+ constantFileStr,
56
+ unitFileStr,
57
+ viewFileStr,
58
+ };
59
+ };
@@ -0,0 +1,313 @@
1
+ import { readFileSync, realpathSync } from "node:fs";
2
+ import ora from "ora";
3
+ import * as ts from "typescript";
4
+
5
+ const tsTranspiler = new Bun.Transpiler({ loader: "ts" });
6
+ const tsxTranspiler = new Bun.Transpiler({ loader: "tsx" });
7
+
8
+ const getTranspiler = (filePath: string) => (filePath.endsWith(".tsx") ? tsxTranspiler : tsTranspiler);
9
+
10
+ const scanModuleSpecifiers = (source: string, filePath: string, includeExports: boolean) => {
11
+ const scannedImports = getTranspiler(filePath)
12
+ .scanImports(source)
13
+ .map((imp) => imp.path)
14
+ .filter(Boolean);
15
+
16
+ if (includeExports) {
17
+ const specifiers = new Set(scannedImports);
18
+ const typeOnlyModuleRegex = /\b(?:import|export)\s+type\s+[\s\S]*?\s+from\s*["']([^"']+)["']/g;
19
+ for (const match of source.matchAll(typeOnlyModuleRegex)) {
20
+ const importPath = match[1];
21
+ if (importPath) specifiers.add(importPath);
22
+ }
23
+ const exportFromRegex = /\bexport\s+(?:type\s+)?(?:\*|{[\s\S]*?})\s+from\s*["']([^"']+)["']/g;
24
+ for (const match of source.matchAll(exportFromRegex)) {
25
+ const importPath = match[1];
26
+ if (importPath) specifiers.add(importPath);
27
+ }
28
+ return specifiers;
29
+ }
30
+
31
+ const importSpecifiers = new Set<string>();
32
+ const importDeclarationRegex = /\bimport\s+(?:type\s+)?(?:["']([^"']+)["']|[\s\S]*?\s+from\s*["']([^"']+)["'])/g;
33
+ for (const match of source.matchAll(importDeclarationRegex)) {
34
+ const importPath = match[1] ?? match[2];
35
+ if (importPath && (scannedImports.includes(importPath) || match[0].startsWith("import type"))) {
36
+ importSpecifiers.add(importPath);
37
+ }
38
+ }
39
+
40
+ return importSpecifiers;
41
+ };
42
+
43
+ /**
44
+ * TypeScript 설정 파일을 읽고 파싱하는 함수
45
+ */
46
+ export const parseTsConfig = (tsConfigPath: string = "./tsconfig.json") => {
47
+ const configFile = ts.readConfigFile(tsConfigPath, (path) => {
48
+ return ts.sys.readFile(path);
49
+ });
50
+
51
+ return ts.parseJsonConfigFileContent(configFile.config, ts.sys, realpathSync(tsConfigPath).replace(/[^/\\]+$/, ""));
52
+ };
53
+
54
+ /**
55
+ * 파일 내의 임포트를 기반으로 관련 파일을 수집하는 함수
56
+ */
57
+ export const collectImportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) => {
58
+ const allFilesToAnalyze = new Set<string>([constantFilePath]);
59
+ const analyzedFiles = new Set<string>();
60
+ const spinner = ora("Collecting related files...");
61
+ spinner.start();
62
+
63
+ function collectImported(filePath: string) {
64
+ if (analyzedFiles.has(filePath)) return;
65
+ analyzedFiles.add(filePath);
66
+
67
+ const source = readFileSync(filePath, "utf-8");
68
+ for (const importPath of scanModuleSpecifiers(source, filePath, false)) {
69
+ if (!importPath.startsWith(".")) continue;
70
+
71
+ const resolved = ts.resolveModuleName(importPath, filePath, parsedConfig.options, ts.sys).resolvedModule
72
+ ?.resolvedFileName;
73
+
74
+ if (resolved && !allFilesToAnalyze.has(resolved)) {
75
+ allFilesToAnalyze.add(resolved);
76
+ // 재귀적으로 임포트된 파일의 임포트도 수집
77
+ collectImported(resolved);
78
+ }
79
+ }
80
+ }
81
+
82
+ // 임포트 기반 관련 파일 수집 실행
83
+ collectImported(constantFilePath);
84
+ spinner.succeed(`Found ${allFilesToAnalyze.size} related files.`);
85
+
86
+ return {
87
+ allFilesToAnalyze,
88
+ analyzedFiles,
89
+ };
90
+ };
91
+
92
+ /**
93
+ * export 선언을 기반으로 관련 파일을 수집하는 함수
94
+ */
95
+ export const collectExportedFiles = (constantFilePath: string, parsedConfig: ts.ParsedCommandLine) => {
96
+ const allFilesToAnalyze = new Set<string>([constantFilePath]);
97
+ const analyzedFiles = new Set<string>();
98
+ const spinner = ora("Collecting files from exports...");
99
+ spinner.start();
100
+
101
+ function collectExported(filePath: string) {
102
+ if (analyzedFiles.has(filePath)) return;
103
+ analyzedFiles.add(filePath);
104
+
105
+ const source = readFileSync(filePath, "utf-8");
106
+ for (const importPath of scanModuleSpecifiers(source, filePath, true)) {
107
+ if (!importPath.startsWith(".")) continue;
108
+
109
+ const resolved = ts.resolveModuleName(importPath, filePath, parsedConfig.options, ts.sys).resolvedModule
110
+ ?.resolvedFileName;
111
+
112
+ if (resolved && !allFilesToAnalyze.has(resolved)) {
113
+ allFilesToAnalyze.add(resolved);
114
+ // 재귀적으로 export/import 연결 파일도 확인
115
+ collectExported(resolved);
116
+ }
117
+ }
118
+ }
119
+
120
+ // export 기반 관련 파일 수집 실행
121
+ collectExported(constantFilePath);
122
+ spinner.succeed(`Found ${allFilesToAnalyze.size} related files from exports.`);
123
+
124
+ return {
125
+ allFilesToAnalyze,
126
+ analyzedFiles,
127
+ };
128
+ };
129
+
130
+ /**
131
+ * TypeScript 프로그램 생성 함수
132
+ */
133
+ export const createTsProgram = (filePaths: Set<string>, options: ts.CompilerOptions) => {
134
+ const spinner = ora("Creating TypeScript program for all files...");
135
+ spinner.start();
136
+
137
+ const program = ts.createProgram(Array.from(filePaths), options);
138
+ const checker = program.getTypeChecker();
139
+
140
+ spinner.succeed("TypeScript program created.");
141
+
142
+ return {
143
+ program,
144
+ checker,
145
+ };
146
+ };
147
+
148
+ /**
149
+ * 심볼 캐싱 및 조회 함수 생성기
150
+ */
151
+ export const createSymbolCache = (checker: ts.TypeChecker) => {
152
+ const symbolCache = new Map<string, ts.Symbol | undefined>();
153
+
154
+ return (node: ts.Node): ts.Symbol | undefined => {
155
+ const cacheKey = `${node.getSourceFile().fileName}:${node.pos}:${node.end}`;
156
+
157
+ if (!symbolCache.has(cacheKey)) {
158
+ symbolCache.set(cacheKey, checker.getSymbolAtLocation(node));
159
+ }
160
+
161
+ return symbolCache.get(cacheKey);
162
+ };
163
+ };
164
+
165
+ /**
166
+ * 파일 속성 분석 함수
167
+ */
168
+ export const analyzeProperties = (filesToAnalyze: Set<string>, program: ts.Program, checker: ts.TypeChecker) => {
169
+ const propertyMap = new Map<
170
+ string,
171
+ {
172
+ filePath: string;
173
+ isLibModule: boolean;
174
+ isImport: boolean;
175
+ isScalar: boolean;
176
+ source: string;
177
+ libName?: string;
178
+ }
179
+ >();
180
+
181
+ const analyzedFiles = new Set<string>();
182
+ const sourceLineCache = new Map<string, string[]>();
183
+ const getCachedSymbol = createSymbolCache(checker);
184
+
185
+ const spinner = ora("Analyzing property relationships...");
186
+ spinner.start();
187
+
188
+ // 파일 내 속성 분석 함수
189
+ function analyzeFileProperties(filePath: string) {
190
+ if (analyzedFiles.has(filePath)) return;
191
+ analyzedFiles.add(filePath);
192
+
193
+ const source = program.getSourceFile(filePath);
194
+ if (!source) return;
195
+
196
+ // 소스 라인 캐싱 (한 번만 분할)
197
+ if (!sourceLineCache.has(filePath)) {
198
+ sourceLineCache.set(filePath, source.getFullText().split("\n"));
199
+ }
200
+ const sourceLines = sourceLineCache.get(filePath);
201
+
202
+ // 속성 분석 함수
203
+ function visit(node: ts.Node) {
204
+ if (!source) return;
205
+
206
+ // 프로퍼티 접근 표현식 처리
207
+ if (ts.isPropertyAccessExpression(node)) {
208
+ const left = node.expression;
209
+ const right = node.name;
210
+ const { line } = ts.getLineAndCharacterOfPosition(source, node.getStart());
211
+
212
+ // 패턴 검색 로직 - 기존 문자열 기반 검색 유지
213
+ if (
214
+ ts.isIdentifier(left) &&
215
+ sourceLines &&
216
+ sourceLines.length > line &&
217
+ sourceLines[line] &&
218
+ (sourceLines[line]?.includes(`@Field.Prop(() => ${left.text}.${right.text}`) ||
219
+ sourceLines[line].includes(`base.Filter(${left.text}.${right.text},`))
220
+ ) {
221
+ // 캐싱된 심볼 사용
222
+ const symbol = getCachedSymbol(right);
223
+
224
+ if (symbol?.declarations && symbol.declarations.length > 0) {
225
+ const key = symbol.declarations[0]?.getSourceFile().fileName.split("/").pop()?.split(".")[0] ?? "";
226
+ const property = propertyMap.get(key);
227
+ const isScalar = symbol.declarations[0]?.getSourceFile().fileName.includes("_") ?? false;
228
+ // 현재 작업 중인 디렉토리 이전은 제거
229
+ const symbolFilePath = symbol.declarations[0]
230
+ ?.getSourceFile()
231
+ .fileName.replace(`${ts.sys.getCurrentDirectory()}/`, "");
232
+ if (!symbolFilePath) throw new Error(`No symbol file path found for ${left.text}.${right.text}`);
233
+
234
+ if (property) {
235
+ propertyMap.set(`${left.text}.${right.text}`, {
236
+ filePath: symbolFilePath,
237
+ isLibModule: true,
238
+ isImport: false,
239
+ libName: left.text,
240
+ source: readFileSync(symbolFilePath, "utf-8"),
241
+ isScalar,
242
+ });
243
+ } else {
244
+ propertyMap.set(key, {
245
+ filePath: symbolFilePath,
246
+ isLibModule: true,
247
+ isImport: false,
248
+ libName: left.text,
249
+ isScalar,
250
+ source: readFileSync(symbolFilePath, "utf-8"),
251
+ });
252
+ }
253
+ }
254
+ }
255
+ } else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
256
+ // 모듈 선언 표현식 처리
257
+ const importPath = node.moduleSpecifier.text;
258
+
259
+ if (importPath.startsWith(".")) {
260
+ const resolved = ts.resolveModuleName(importPath, filePath, program.getCompilerOptions(), ts.sys)
261
+ .resolvedModule?.resolvedFileName;
262
+ const moduleName = importPath.split("/").pop()?.split(".")[0] ?? "";
263
+ const property = propertyMap.get(moduleName);
264
+ const isScalar = importPath.includes("_");
265
+
266
+ if (moduleName && resolved && (!property || property.filePath !== resolved)) {
267
+ propertyMap.set(moduleName, {
268
+ filePath: resolved,
269
+ isLibModule: false,
270
+ isImport: true,
271
+ isScalar,
272
+ source: readFileSync(resolved, "utf-8"),
273
+ });
274
+ }
275
+ }
276
+ }
277
+
278
+ // 모든 자식 노드 처리
279
+ ts.forEachChild(node, visit);
280
+ }
281
+
282
+ // 파일 분석 시작
283
+ visit(source);
284
+ }
285
+
286
+ // 모든 파일의 속성 관계 분석
287
+ for (const filePath of filesToAnalyze) {
288
+ analyzeFileProperties(filePath);
289
+ }
290
+
291
+ spinner.succeed(`Analysis complete. Found ${propertyMap.size} properties.`);
292
+
293
+ return propertyMap;
294
+ };
295
+
296
+ /**
297
+ * 메인 함수: 상수 파일 관련 요소 분석
298
+ */
299
+ export const getRelatedCnsts = (constantFilePath: string) => {
300
+ // 1. TypeScript 설정 파일 읽기 및 파싱
301
+ const parsedConfig = parseTsConfig();
302
+
303
+ // 2. 상수 파일 관련 파일 수집 (임포트 기반)
304
+ const { allFilesToAnalyze } = collectImportedFiles(constantFilePath, parsedConfig);
305
+ // 3. TypeScript 프로그램 생성 및 타입 체커 설정
306
+ const { program, checker } = createTsProgram(allFilesToAnalyze, parsedConfig.options);
307
+
308
+ // 4. 파일 속성 분석
309
+ const propertyMap = analyzeProperties(allFilesToAnalyze, program, checker);
310
+
311
+ // 5. 결과 반환
312
+ return Array.from(propertyMap.entries()).map(([key, value]) => ({ key, ...value }));
313
+ };
package/guideline.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface GuideScan {
2
+ type: "example" | "source" | "usage" | (string & {});
3
+ description: string;
4
+ path: string;
5
+ filterText?: string;
6
+ sample?: number;
7
+ }
8
+ export interface GuideUpdate {
9
+ filePath: string;
10
+ contents: string[];
11
+ rules: string[];
12
+ }
13
+ export interface GuideGenerateJson {
14
+ title: string;
15
+ description: string;
16
+ scans: GuideScan[];
17
+ update: GuideUpdate;
18
+ page?: string;
19
+ }