@akanjs/devkit 3.0.0-alpha.9 → 3.0.0-alpha.91

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 (183) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/DEV_RUNTIME_KNOBS.md +27 -0
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/agentsIndex.test.ts +10 -0
  6. package/agentsIndex.ts +21 -1
  7. package/aiEditor.ts +1 -1
  8. package/akanApp/BackendImportGraph.test.ts +120 -0
  9. package/akanApp/BackendImportGraph.ts +167 -0
  10. package/akanApp/akanApp.host.test.ts +16 -678
  11. package/akanApp/akanApp.host.ts +113 -578
  12. package/akanApp/devHostPolicy.test.ts +542 -0
  13. package/akanApp/devHostPolicy.ts +442 -0
  14. package/akanApp/index.ts +2 -0
  15. package/akanConfig/akanConfig.test.ts +182 -14
  16. package/akanConfig/akanConfig.ts +143 -47
  17. package/akanConfig/types.ts +9 -0
  18. package/akanContext.ts +30 -14
  19. package/akanMcpContract.ts +25 -4
  20. package/appSelectionMemory.ts +28 -0
  21. package/applicationBuildRunner.test.ts +1 -1
  22. package/applicationBuildRunner.ts +45 -21
  23. package/artifact/implicitRootLayout.test.ts +67 -0
  24. package/artifact/implicitRootLayout.ts +27 -7
  25. package/artifact/routeSeedIndex.test.ts +1 -0
  26. package/biome.base.json +86 -47
  27. package/biomeBase.ts +6 -5
  28. package/capacitorApp.ts +10 -0
  29. package/cloud/globalConfig.ts +9 -2
  30. package/codegenLock.test.ts +137 -0
  31. package/codegenLock.ts +136 -0
  32. package/commandDecorators/argMeta.ts +9 -1
  33. package/commandDecorators/command.ts +94 -7
  34. package/commandDecorators/commandBuilder.ts +20 -5
  35. package/commandDecorators/commandDecorators.test.ts +115 -3
  36. package/commandDecorators/helpFormatter.ts +4 -0
  37. package/dependencyScanner.test.ts +99 -0
  38. package/dependencyScanner.ts +27 -24
  39. package/devkitUtils.test.ts +0 -104
  40. package/executors.test.ts +129 -4
  41. package/executors.ts +105 -24
  42. package/fileEditor.ts +19 -19
  43. package/fileSys.ts +19 -1
  44. package/formSetterScanner.test.ts +80 -0
  45. package/formSetterScanner.ts +92 -0
  46. package/frontendBuild/autoImportSync.test.ts +58 -0
  47. package/frontendBuild/autoImportSync.ts +7 -2
  48. package/frontendBuild/buildRouteClient.test.ts +47 -24
  49. package/frontendBuild/clientBuildTypes.ts +4 -0
  50. package/frontendBuild/clientEntriesBundler.ts +4 -1
  51. package/frontendBuild/clientEntryDiscovery.ts +2 -2
  52. package/frontendBuild/csrArtifactBuilder.ts +116 -84
  53. package/frontendBuild/cssCompiler.ts +123 -12
  54. package/frontendBuild/cssImportResolver.ts +8 -7
  55. package/frontendBuild/devGeneratedIndexSync.ts +3 -2
  56. package/frontendBuild/fontOptimizer.ts +37 -18
  57. package/frontendBuild/fontPruner.test.ts +220 -0
  58. package/frontendBuild/fontPruner.ts +206 -0
  59. package/frontendBuild/frontendBuild.test.ts +178 -10
  60. package/frontendBuild/hmrWatcher.ts +1 -1
  61. package/frontendBuild/index.ts +1 -1
  62. package/frontendBuild/pagesBundleBuilder.ts +3 -3
  63. package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
  64. package/frontendBuild/routeClientBuilder.ts +12 -5
  65. package/frontendBuild/sourceMtimeIndex.ts +1 -1
  66. package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
  67. package/frontendBuild/styleContract.ts +14 -20
  68. package/frontendBuild/themeValidator.ts +22 -17
  69. package/frontendBuild/vendorSpecifiers.ts +1 -0
  70. package/frontendBuild/watchRootResolver.test.ts +67 -0
  71. package/frontendBuild/watchRootResolver.ts +13 -1
  72. package/incrementalBuilder/devWatchBatch.test.ts +18 -20
  73. package/incrementalBuilder/devWatchBatch.ts +6 -2
  74. package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
  75. package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
  76. package/index.ts +0 -7
  77. package/integration/devStabilityHarness.ts +2 -10
  78. package/libSource.test.ts +112 -0
  79. package/libSource.ts +126 -0
  80. package/lint/__fixtures__/README.md +40 -0
  81. package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
  82. package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
  83. package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
  84. package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
  85. package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
  86. package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
  87. package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
  88. package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
  89. package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
  90. package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
  91. package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
  92. package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
  93. package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
  94. package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
  95. package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
  96. package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
  97. package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
  98. package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
  99. package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
  100. package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
  101. package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
  102. package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
  103. package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
  104. package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
  105. package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
  106. package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
  107. package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
  108. package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
  109. package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
  110. package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
  111. package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
  112. package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
  113. package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
  114. package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
  115. package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
  116. package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
  117. package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
  118. package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
  119. package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
  120. package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
  121. package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
  122. package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
  123. package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
  124. package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
  125. package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
  126. package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
  127. package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
  128. package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
  129. package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
  130. package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
  131. package/lint/gritRules.test.ts +178 -0
  132. package/lint/no-arbitrary-color.grit +1 -1
  133. package/lint/no-async-component-in-ui.grit +35 -0
  134. package/lint/no-bang-comment-in-client.grit +23 -10
  135. package/lint/no-daisyui-legacy-class.grit +26 -9
  136. package/lint/no-deprecated-log-level.grit +17 -0
  137. package/lint/no-init-fetch-in-client.grit +47 -0
  138. package/lint/no-inline-color.grit +10 -8
  139. package/lint/no-interpolated-arbitrary-class.grit +3 -3
  140. package/lint/no-model-type-in-util-zone.grit +58 -0
  141. package/lint/no-raw-palette-class.grit +3 -3
  142. package/lint/no-unpublished-form-setter.grit +41 -0
  143. package/lint/non-scalar-props-restricted.grit +16 -7
  144. package/linter.test.ts +80 -0
  145. package/linter.ts +99 -25
  146. package/package.json +5 -8
  147. package/prompter.ts +9 -4
  148. package/qualityScanner.test.ts +116 -0
  149. package/qualityScanner.ts +89 -21
  150. package/recipeScanner.ts +4 -1
  151. package/scanInfo.ts +32 -26
  152. package/semver.test.ts +26 -0
  153. package/semver.ts +31 -0
  154. package/slicePlanner.test.ts +182 -0
  155. package/slicePlanner.ts +235 -0
  156. package/ssrScanner.test.ts +301 -0
  157. package/subspace.test.ts +418 -0
  158. package/subspace.ts +723 -0
  159. package/subspaceConfig.ts +76 -0
  160. package/transforms/asyncDefaultExportDetector.ts +103 -0
  161. package/transforms/barrelImportsPlugin.ts +1 -1
  162. package/transforms/externalizeFrameworkPlugin.ts +0 -1
  163. package/transforms/transforms.test.ts +5 -5
  164. package/transforms/tsconfigPackageResolver.test.ts +230 -0
  165. package/tsconfig.json +6 -2
  166. package/typeChecker.ts +1 -1
  167. package/types.ts +1 -0
  168. package/uploadRelease.ts +2 -2
  169. package/workflow/executor.test.ts +146 -0
  170. package/workspaceLayout.test.ts +56 -4
  171. package/workspaceLayout.ts +48 -2
  172. package/builder.ts +0 -164
  173. package/extractDeps.ts +0 -86
  174. package/frontendBuild/styleGuard.test.ts +0 -165
  175. package/frontendBuild/styleGuard.ts +0 -322
  176. package/getCredentials.ts +0 -19
  177. package/getModelFileData.ts +0 -62
  178. package/src/capacitorApp.ts +0 -282
  179. package/streamAi.ts +0 -45
  180. package/ui/MultiScrollList.tsx +0 -242
  181. package/ui/ScrollList.tsx +0 -107
  182. package/ui/index.ts +0 -2
  183. package/useStdoutDimensions.ts +0 -20
@@ -1,322 +0,0 @@
1
- /**
2
- * styleGuard — 색 어휘 폐쇄를 강제하는 순수 정적 검사기 (의존성 0).
3
- *
4
- * 어휘 폐쇄(styles.css `@theme { --color-*: initial }`)로 raw 팔레트가 CSS를 미생성하게 되면
5
- * 위반은 "조용한 시각 깨짐"이 된다. styleGuard 는 소스 문자열을 스캔해 그 위반을 명시적 진단으로
6
- * 끌어올리고, 에이전트가 읽고 수리할 `suggestion` 을 붙인다.
7
- *
8
- * severity 는 규칙 고유값(판별력)만 담는다. warn/error 를 막을지 여부는 배선(호출자)이 mode 로 결정한다:
9
- * - dev : 절대 막지 않음 (모두 경고 로그)
10
- * - build/CI/lint : severity==="error" 위반이 하나라도 있으면 실패
11
- *
12
- * akanjs 런타임을 import 하지 않는다 — node 표준 라이브러리만 사용.
13
- */
14
-
15
- export type StyleGuardRule =
16
- | "raw-palette"
17
- | "arbitrary-color"
18
- | "inline-color"
19
- | "daisyui-legacy"
20
- | "interpolated-arbitrary";
21
- export type StyleGuardSeverity = "error" | "warn";
22
-
23
- export interface StyleGuardViolation {
24
- rule: StyleGuardRule;
25
- severity: StyleGuardSeverity;
26
- path: string;
27
- line: number;
28
- snippet: string;
29
- suggestion: string;
30
- }
31
-
32
- export interface StyleGuardFile {
33
- path: string;
34
- content: string;
35
- }
36
-
37
- export interface NamedComponentClassMetric {
38
- count: number;
39
- names: string[];
40
- }
41
-
42
- // 색을 받는 유틸 접두사. daisyuiTokenRename.ts 의 PREFIX 와 동일 계보(방향성 border 포함).
43
- const PREFIX =
44
- "(?:bg|text|border(?:-[tblrxy])?(?:-[se])?|ring(?:-offset)?|fill|stroke|shadow|from|to|via|divide|outline|decoration|placeholder|caret|accent)";
45
-
46
- // Tailwind 기본 팔레트 이름. `neutral` 은 시맨틱 토큰이기도 하므로 숫자 suffix 를 요구해 구분한다
47
- // (`bg-neutral` 은 허용, `bg-neutral-500` 은 raw 팔레트).
48
- const PALETTE =
49
- "(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)";
50
-
51
- // 클래스 경계: 앞은 공백/따옴표/백틱/여는 괄호/콜론(변형)/!(important), 뒤는 종결 문자(+ 슬래시=opacity, !).
52
- const LEAD = "(^|[\\s\"'`{(\\[:!])";
53
- const TAIL = "(?=[\\s\"'`})\\]:/!,]|$)";
54
-
55
- const RAW_PALETTE_RE = new RegExp(`${LEAD}((?:${PREFIX})-)${PALETTE}-\\d{2,3}${TAIL}`, "g");
56
-
57
- // 임의값 대괄호 안의 색 리터럴. `[--var]` 같은 변수 참조는 매치되지 않는다(의도).
58
- const ARBITRARY_COLOR_RE = /\[(#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb|color)\([^\]]*\))\]/g;
59
-
60
- // 런타임 값으로 조립한 임의값(`min-h-[${n}px]`, `bg-[${color}]`). Tailwind 는 소스 **텍스트**에서
61
- // 임의값을 추출하므로 CSS 가 아예 생성되지 않는다 — 클래스는 DOM 에 있고 prop 은 연결된 듯 보이지만
62
- // 아무것도 적용되지 않는 무증상 실패다. 같은 모양의 리터럴이 코드베이스 어딘가에 있으면 기본값만
63
- // 우연히 동작하고 override 만 조용히 죽어 더 찾기 어려워진다.
64
- const INTERPOLATED_ARBITRARY_RE = new RegExp(`${LEAD}[a-z][a-z0-9-]*-\\[[^\\]\`]*\\$\\{`, "g");
65
-
66
- // style 객체 / <style> 블록. 색 리터럴이 이 안에 있으면 클래스 스캐너를 우회한 것.
67
- const STYLE_OBJECT_RE = /style=\{\{([\s\S]*?)\}\}/g;
68
- const STYLE_TAG_RE = /<style[^>]*>([\s\S]*?)<\/style>/g;
69
- // var(...) 는 허용이므로 매치 대상에서 자연히 빠진다(hex/색함수 리터럴만 탐지).
70
- const INLINE_COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|hwb)\(/g;
71
-
72
- // daisyUI 잔재. 판별력 높은 compound(변형 suffix 포함) 패턴만 error 로 잡아 오탐을 억제한다.
73
- const DAISYUI_LEGACY_RE = new RegExp(
74
- `${LEAD}(?:` +
75
- // 버튼/배지/알림/입력 등 variant compound
76
- "btn-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|link|outline|square|circle|wide|block|xs|sm|md|lg)|" +
77
- "badge-(?:primary|secondary|accent|neutral|info|success|warning|error|ghost|outline)|" +
78
- "alert-(?:info|success|warning|error)|" +
79
- "input-(?:bordered|primary|secondary|accent|ghost|error)|" +
80
- "select-(?:bordered|primary|ghost)|textarea-(?:bordered|primary|ghost)|" +
81
- "checkbox-(?:primary|secondary|accent)|toggle-(?:primary|secondary|accent)|" +
82
- "loading-(?:spinner|dots|ring|ball|bars|infinity)|" +
83
- // 구조 클래스(daisyUI 전용 조각)
84
- "card-(?:body|title|actions)|modal-(?:box|action|backdrop)|" +
85
- "collapse-(?:title|content|arrow|plus)|dropdown-(?:content|end|start|hover)|" +
86
- "stat-(?:title|value|desc)|tabs-(?:boxed|lifted|bordered)|tab-active|" +
87
- "menu-(?:title|dropdown)|steps-(?:horizontal|vertical)|join-item|" +
88
- "mockup-(?:code|phone|browser|window)|drawer-(?:side|content|toggle)" +
89
- `)${TAIL}`,
90
- "g",
91
- );
92
-
93
- const ALL_RULES: StyleGuardRule[] = [
94
- "raw-palette",
95
- "arbitrary-color",
96
- "inline-color",
97
- "daisyui-legacy",
98
- "interpolated-arbitrary",
99
- ];
100
-
101
- /**
102
- * 위반 억제 지시어(escape hatch). 정당한 팔레트 요청(에디터 신택스 하이라이트, 데이터-viz, 의도적 브랜드
103
- * 데모)은 거절이 아니라 "명시적 옵트아웃"으로 라우팅한다 — 천장은 제약하지 않되 흔적을 남긴다.
104
- * styleguard-disable <rule?> → 파일 전체
105
- * styleguard-disable-next-line <rule?> → 바로 다음 줄
106
- * rule 을 생략하면 모든 규칙을 억제한다.
107
- */
108
- interface Directives {
109
- file: Set<StyleGuardRule> | "all" | null;
110
- nextLine: Map<number, Set<StyleGuardRule> | "all">;
111
- }
112
-
113
- export class StyleGuard {
114
- run(files: StyleGuardFile[]): StyleGuardViolation[] {
115
- const out: StyleGuardViolation[] = [];
116
- for (const file of files) {
117
- const directives = this.#parseDirectives(file.content);
118
- // 주석 내용을 공백으로 치환(offset·줄번호 보존). 주석에 남은 예전 클래스명이 오탐되지 않게.
119
- // directives 는 원본에서 파싱한다 — 지시어 자체가 주석이기 때문.
120
- const scan = this.#stripComments(file.content);
121
- const local: StyleGuardViolation[] = [];
122
- this.#scanClassLiterals(file, scan, local);
123
- this.#scanInlineColors(file, scan, local);
124
- for (const v of local) if (!this.#isSuppressed(v, directives)) out.push(v);
125
- }
126
- return out;
127
- }
128
-
129
- /** 라인/블록 주석을 같은 길이의 공백으로 치환. 문자열/템플릿 리터럴 안의 `//`·`/*` 는 보존. */
130
- #stripComments(src: string): string {
131
- const out = src.split("");
132
- const blank = (i: number) => {
133
- if (src[i] !== "\n") out[i] = " ";
134
- };
135
- let state: "code" | "line" | "block" | "squote" | "dquote" | "template" = "code";
136
- let i = 0;
137
- while (i < src.length) {
138
- const c = src[i];
139
- const d = src[i + 1];
140
- if (state === "code") {
141
- if (c === "/" && d === "/") {
142
- blank(i);
143
- blank(i + 1);
144
- state = "line";
145
- i += 2;
146
- } else if (c === "/" && d === "*") {
147
- blank(i);
148
- blank(i + 1);
149
- state = "block";
150
- i += 2;
151
- } else {
152
- if (c === "'") state = "squote";
153
- else if (c === '"') state = "dquote";
154
- else if (c === "`") state = "template";
155
- i++;
156
- }
157
- } else if (state === "line") {
158
- if (c === "\n") state = "code";
159
- else blank(i);
160
- i++;
161
- } else if (state === "block") {
162
- if (c === "*" && d === "/") {
163
- blank(i);
164
- blank(i + 1);
165
- state = "code";
166
- i += 2;
167
- } else {
168
- blank(i);
169
- i++;
170
- }
171
- } else {
172
- // 문자열/템플릿: 내용 보존, escape 건너뛰고 종결 문자에서 code 로 복귀.
173
- const close = state === "squote" ? "'" : state === "dquote" ? '"' : "`";
174
- if (c === "\\") i += 2;
175
- else {
176
- if (c === close) state = "code";
177
- i++;
178
- }
179
- }
180
- }
181
- return out.join("");
182
- }
183
-
184
- #parseDirectives(content: string): Directives {
185
- const directives: Directives = { file: null, nextLine: new Map() };
186
- const lines = content.split("\n");
187
- for (let i = 0; i < lines.length; i++) {
188
- const line = lines[i];
189
- const nextLineMatch = line.match(/styleguard-disable-next-line\b(.*)$/);
190
- if (nextLineMatch) {
191
- directives.nextLine.set(i + 2, this.#extractRules(nextLineMatch[1]));
192
- continue;
193
- }
194
- const fileMatch = line.match(/styleguard-disable\b(.*)$/);
195
- if (fileMatch) directives.file = this.#mergeFileScope(directives.file, this.#extractRules(fileMatch[1]));
196
- }
197
- return directives;
198
- }
199
-
200
- #extractRules(tail: string): Set<StyleGuardRule> | "all" {
201
- const found = ALL_RULES.filter((rule) => tail.includes(rule));
202
- return found.length === 0 ? "all" : new Set(found);
203
- }
204
-
205
- #mergeFileScope(
206
- prev: Set<StyleGuardRule> | "all" | null,
207
- next: Set<StyleGuardRule> | "all",
208
- ): Set<StyleGuardRule> | "all" {
209
- if (prev === "all" || next === "all") return "all";
210
- return new Set([...(prev ?? []), ...next]);
211
- }
212
-
213
- #isSuppressed(v: StyleGuardViolation, directives: Directives): boolean {
214
- if (directives.file === "all" || directives.file?.has(v.rule)) return true;
215
- const nextLine = directives.nextLine.get(v.line);
216
- return nextLine === "all" || (nextLine?.has(v.rule) ?? false);
217
- }
218
-
219
- #scanClassLiterals(file: StyleGuardFile, scan: string, out: StyleGuardViolation[]): void {
220
- for (const m of scan.matchAll(RAW_PALETTE_RE)) {
221
- out.push(
222
- this.#violation(file, m.index ?? 0, {
223
- rule: "raw-palette",
224
- severity: "error",
225
- suggestion:
226
- "시맨틱 토큰으로 교체하세요 — 예: text-gray-500→text-muted-foreground, bg-red-500→bg-destructive, bg-blue-500→bg-info. 브랜드 색은 page/styles.css 토큰에서 조정합니다.",
227
- }),
228
- );
229
- }
230
- for (const m of scan.matchAll(ARBITRARY_COLOR_RE)) {
231
- out.push(
232
- this.#violation(file, m.index ?? 0, {
233
- rule: "arbitrary-color",
234
- severity: "error",
235
- suggestion:
236
- "임의 색 리터럴 대신 토큰을 쓰세요 — bg-[#3b82f6]→bg-info, text-[rgb(...)]→시맨틱 토큰. 꼭 필요하면 스코프 토큰(.campaign-x { --primary: … })을 정의하세요.",
237
- }),
238
- );
239
- }
240
- for (const m of scan.matchAll(DAISYUI_LEGACY_RE)) {
241
- out.push(
242
- this.#violation(file, m.index ?? 0, {
243
- rule: "daisyui-legacy",
244
- severity: "error",
245
- suggestion:
246
- "daisyUI 클래스는 제거됐습니다. akanjs/ui 프리미티브(Button/Badge 등)나 buttonRecipe()/badgeRecipe() + 시맨틱 토큰으로 교체하세요.",
247
- }),
248
- );
249
- }
250
- for (const m of scan.matchAll(INTERPOLATED_ARBITRARY_RE)) {
251
- out.push(
252
- this.#violation(file, m.index ?? 0, {
253
- rule: "interpolated-arbitrary",
254
- severity: "error",
255
- suggestion:
256
- "런타임 값으로 임의값 클래스를 조립하면 CSS 가 생성되지 않습니다(스캐너는 소스 텍스트를 읽습니다). 크기/위치는 style prop 으로 넘기세요 — style={{ minHeight }}. 값이 enum 이면 리터럴 클래스 맵으로 두세요.",
257
- }),
258
- );
259
- }
260
- }
261
-
262
- #scanInlineColors(file: StyleGuardFile, scan: string, out: StyleGuardViolation[]): void {
263
- const push = (index: number) =>
264
- out.push(
265
- this.#violation(file, index, {
266
- rule: "inline-color",
267
- severity: "error",
268
- suggestion:
269
- "인라인 색 리터럴 대신 CSS 변수/토큰을 참조하세요 — style={{ color: 'var(--primary)' }} 또는 시맨틱 클래스(text-primary). 하드코딩 hex/rgb 는 테마 전환을 깨뜨립니다.",
270
- }),
271
- );
272
- for (const region of [STYLE_OBJECT_RE, STYLE_TAG_RE]) {
273
- for (const m of scan.matchAll(region)) {
274
- const inner = m[1] ?? "";
275
- const innerStart = (m.index ?? 0) + m[0].indexOf(inner);
276
- for (const lit of inner.matchAll(INLINE_COLOR_LITERAL_RE)) {
277
- push(innerStart + (lit.index ?? 0));
278
- }
279
- }
280
- }
281
- }
282
-
283
- #violation(
284
- file: StyleGuardFile,
285
- index: number,
286
- base: Pick<StyleGuardViolation, "rule" | "severity" | "suggestion">,
287
- ): StyleGuardViolation {
288
- const before = file.content.slice(0, index);
289
- const line = before.length === 0 ? 1 : before.split("\n").length;
290
- const lineStart = before.lastIndexOf("\n") + 1;
291
- const lineEnd = file.content.indexOf("\n", index);
292
- const snippet = file.content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
293
- return { ...base, path: file.path, line, snippet };
294
- }
295
-
296
- /**
297
- * `@layer components` 안에서 선언된 명명 클래스 개수 — 그림자 디자인 시스템 증식의 warn 지표(차단 X).
298
- */
299
- static countNamedComponentClasses(css: string): NamedComponentClassMetric {
300
- const names = new Set<string>();
301
- const layerRe = /@layer\s+components\s*\{/g;
302
- for (const layer of css.matchAll(layerRe)) {
303
- const bodyStart = (layer.index ?? 0) + layer[0].length;
304
- const body = StyleGuard.#extractBalanced(css, bodyStart);
305
- for (const sel of body.matchAll(/(^|[\s}])\.(-?[a-zA-Z_][\w-]*)/g)) names.add(sel[2]);
306
- }
307
- return { count: names.size, names: [...names].sort() };
308
- }
309
-
310
- static #extractBalanced(css: string, start: number): string {
311
- let depth = 1;
312
- for (let i = start; i < css.length; i++) {
313
- const ch = css[i];
314
- if (ch === "{") depth++;
315
- else if (ch === "}") {
316
- depth--;
317
- if (depth === 0) return css.slice(start, i);
318
- }
319
- }
320
- return css.slice(start);
321
- }
322
- }
package/getCredentials.ts DELETED
@@ -1,19 +0,0 @@
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
- };
@@ -1,62 +0,0 @@
1
- import { capitalize } from "akanjs/common";
2
-
3
- interface ModelFileData {
4
- moduleType: "lib" | "app";
5
- moduleName: string;
6
- modelName: string;
7
- constantFilePath: string;
8
- importModelNames: string[];
9
- hasImportScalar: boolean;
10
- importLibNames: string[];
11
- constantFileStr: string;
12
- unitFilePath: string;
13
- unitFileStr: string;
14
- viewFilePath: string;
15
- viewFileStr: string;
16
- }
17
-
18
- export const getModelFileData = async (modulePath: string, modelName: string): Promise<ModelFileData> => {
19
- const moduleType = modulePath.startsWith("apps") ? "app" : "lib";
20
- const moduleName = modulePath.split("/")[1] ?? "";
21
- const modelComponentName = capitalize(modelName);
22
- const constantFilePath = `${modulePath}/lib/${modelName}/${modelName}.constant.ts`;
23
- const unitFilePath = `${modulePath}/lib/${modelName}/${modelComponentName}.Unit.tsx`;
24
- const viewFilePath = `${modulePath}/lib/${modelName}/${modelComponentName}.View.tsx`;
25
- const [constantFileStr, unitFileStr, viewFileStr] = await Promise.all([
26
- Bun.file(constantFilePath).text(),
27
- Bun.file(unitFilePath).text(),
28
- Bun.file(viewFilePath).text(),
29
- ]);
30
-
31
- const constantFileLines = constantFileStr.split("\n");
32
- const importLibNames = constantFileLines
33
- .filter((line) => line.startsWith("import { cnst as "))
34
- .map((line) => line.split("cnst as ")[1]?.split(" ")[0] ?? "");
35
-
36
- const importLocalPaths = constantFileLines
37
- .filter((line) => line.startsWith("import { ") && line.includes('from "../'))
38
- .map((line) => line.split("from ")[1]?.split('"')[1] ?? "");
39
-
40
- const importModelNames = importLocalPaths
41
- .map((path) => path.split("/")[1] ?? "")
42
- .filter((name) => !name.startsWith("_"));
43
-
44
- const hasImportScalar = !!importLocalPaths
45
- .map((path) => path.split("/")[1] ?? "")
46
- .filter((name) => name.startsWith("_")).length;
47
-
48
- return {
49
- moduleType,
50
- moduleName,
51
- modelName,
52
- constantFilePath,
53
- unitFilePath,
54
- viewFilePath,
55
- importModelNames,
56
- hasImportScalar,
57
- importLibNames,
58
- constantFileStr,
59
- unitFileStr,
60
- viewFileStr,
61
- };
62
- };
@@ -1,282 +0,0 @@
1
- import type { AppExecutor } from "@akanjs/devkit/executors";
2
- import { FileSys } from "@akanjs/devkit/fileSys";
3
- import type { CapacitorConfig } from "@capacitor/cli";
4
- import { MobileProject } from "@trapezedev/project";
5
- import type { AndroidProject } from "@trapezedev/project/dist/android/project";
6
- import type { IosProject } from "@trapezedev/project/dist/ios/project";
7
- import { capitalize } from "akanjs/common";
8
-
9
- import { FileEditor } from "./fileEditor";
10
-
11
- interface RunConfig extends CapacitorConfig {
12
- operation: "local" | "release";
13
- version: string;
14
- buildNum: number;
15
- appId?: string;
16
- host?: "local" | "debug" | "develop" | "main";
17
- }
18
-
19
- export class CapacitorApp {
20
- project: MobileProject & { ios: IosProject; android: AndroidProject };
21
- iosTargetName = "App";
22
- constructor(private readonly app: AppExecutor) {
23
- this.project = new MobileProject(this.app.cwdPath, {
24
- android: { path: "android" },
25
- ios: { path: "ios/App" },
26
- }) as MobileProject & { ios: IosProject; android: AndroidProject };
27
- }
28
- async init() {
29
- const project = this.project as MobileProject;
30
- await this.project.load();
31
- const hasAndroid = await FileSys.fileExists(`${this.app.cwdPath}/android/app/build.gradle`);
32
- const hasIos = await FileSys.fileExists(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`);
33
- if (!project.android && !hasAndroid) {
34
- await this.app.spawn("npx", ["cap", "add", "android"]);
35
- await this.project.load();
36
- }
37
- if (!project.ios && !hasIos) {
38
- await this.app.spawn("npx", ["cap", "add", "ios"]);
39
- await this.project.load();
40
- }
41
- return this;
42
- }
43
- async save() {
44
- await this.project.commit();
45
- }
46
- async #prepareIos() {
47
- const isAdded = await FileSys.fileExists(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`);
48
- if (!isAdded) {
49
- await this.app.spawn("npx", ["cap", "add", "ios"]);
50
- await this.app.spawn("npx", ["@capacitor/assets", "generate"]);
51
- } else this.app.verbose(`iOS already added, skip adding process`);
52
- this.app.verbose(`syncing iOS`);
53
- await this.app.spawn("npx", ["cap", "sync", "ios"]);
54
- this.app.verbose(`sync completed.`);
55
- }
56
- async buildIos() {
57
- await this.#prepareIos();
58
- this.app.verbose(`build completed iOS.`);
59
- return;
60
- }
61
- async syncIos() {
62
- await this.app.spawn("npx", ["cap", "sync", "ios"]);
63
- }
64
- async openIos() {
65
- await this.app.spawn("npx", ["cap", "open", "ios"]);
66
- }
67
- async runIos({ operation, appId, version = "0.0.1", buildNum = 1, host = "local" }: RunConfig) {
68
- const defaultAppId = `com.${this.app.name}.app`;
69
- await this.#prepareIos();
70
- this.project.ios.setBundleId("App", "Debug", appId ?? defaultAppId);
71
- this.project.ios.setBundleId("App", "Release", appId ?? defaultAppId);
72
- await this.project.ios.setVersion("App", "Debug", version);
73
- await this.project.ios.setVersion("App", "Release", version);
74
- await this.project.ios.setBuild("App", "Debug", buildNum);
75
- await this.project.ios.setBuild("App", "Release", buildNum);
76
- await this.project.commit();
77
- await this.app.spawn(
78
- "npx",
79
- ["cross-env", `APP_OPERATION_MODE=${operation}`, `BUN_PUBLIC_ENV=${host}`, "npx", "cap", "run", "ios"],
80
- {
81
- stdio: "inherit",
82
- },
83
- );
84
-
85
- // this.project.ios.incrementBuild("App", "Debug");
86
- // this.project.ios.incrementBuild("App", "Release");
87
- }
88
-
89
- async #prepareAndroid() {
90
- const isAdded = await Bun.file(`${this.app.cwdPath}/android/app/build.gradle`).exists();
91
- if (!isAdded) {
92
- await this.app.spawn("npx", ["cap", "add", "android"]);
93
- } else this.app.verbose(`Android already added, skip adding process`);
94
- await this.app.spawn("npx", ["@capacitor/assets", "generate"]);
95
- await this.app.spawn("npx", ["cap", "sync", "android"]);
96
- }
97
-
98
- async #updateAndroidBuildTypes() {
99
- //keystore 기본 설정 및 debug, release 설정
100
-
101
- const appGradle = await FileEditor.create(`${this.app.cwdPath}/android/app/build.gradle`);
102
- const buildTypesBlock = `
103
- debug {
104
- applicationIdSuffix ".debug"
105
- versionNameSuffix "-DEBUG"
106
- debuggable true
107
- minifyEnabled false
108
- }
109
- `;
110
- const singinConfigBlock = `
111
- signingConfigs {
112
- debug {
113
- storeFile file('debug.keystore')
114
- storePassword 'android'
115
- keyAlias 'androiddebugkey'
116
- keyPassword 'android'
117
- }
118
- release {
119
- if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
120
- storeFile file(MYAPP_RELEASE_STORE_FILE)
121
- storePassword MYAPP_RELEASE_STORE_PASSWORD
122
- keyAlias MYAPP_RELEASE_KEY_ALIAS
123
- keyPassword MYAPP_RELEASE_KEY_PASSWORD
124
- }
125
- }
126
- }
127
- `;
128
- if (appGradle.find("signingConfigs {") === -1) {
129
- appGradle.insertBefore("buildTypes {", singinConfigBlock);
130
- }
131
- if (appGradle.find(`applicationIdSuffix ".debug"`) === -1) {
132
- appGradle.insertAfter("buildTypes {", buildTypesBlock);
133
- }
134
- await appGradle.save();
135
- }
136
- async buildAndroid(assembleType: "apk" | "aab") {
137
- await this.#prepareAndroid();
138
- await this.#updateAndroidBuildTypes();
139
- //윈도우는 gradlew.bat 사용
140
- const isWindows = process.platform === "win32";
141
- const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
142
-
143
- await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
144
- stdio: "inherit",
145
- cwd: `${this.app.cwdPath}/android`,
146
- });
147
- }
148
- async openAndroid() {
149
- await this.app.spawn("npx", ["cap", "open", "android"]);
150
- }
151
- async syncAndroid() {
152
- await this.#prepareAndroid();
153
- this.app.log(`Sync Android Completed.`);
154
- }
155
- async runAndroid({ operation, appName, appId, version = "0.0.1", buildNum = 1, host = "local" }: RunConfig) {
156
- const defaultAppId = `com.${this.app.name}.app`;
157
- const defaultAppName = this.app.name;
158
- await this.project.android.setVersionName(version);
159
- await this.project.android.setPackageName(appId ?? defaultAppId);
160
- await this.project.android.setVersionCode(buildNum);
161
- const versionName = await this.project.android.getVersionName();
162
- const versionCode = await this.project.android.getVersionCode();
163
- await this.project.android.setAppName(appName ?? defaultAppName);
164
- await this.project.commit();
165
- await this.#prepareAndroid();
166
-
167
- this.app.logger.info(`Running Android in ${operation} mode on ${host} host`);
168
- await this.app.spawn(
169
- "npx",
170
- ["cross-env", `BUN_PUBLIC_ENV=${host}`, `APP_OPERATION_MODE=${operation}`, "npx", "cap", "run", "android"],
171
- {
172
- stdio: "inherit",
173
- },
174
- );
175
- }
176
-
177
- //? 릴리즈시 buildNum +1 version 파라미터 받아서 업데이트
178
- async updateAndroidVersion(version: string, buildNum: number) {
179
- //TODO: 테스트 확인 필요
180
- await this.project.android.setVersionName(version);
181
- await this.project.android.setVersionCode(buildNum);
182
- const versionName = await this.project.android.getVersionName();
183
- const versionCode = await this.project.android.getVersionCode();
184
- await this.project.commit();
185
- }
186
- async releaseIos() {
187
- //TODO: 작업 필요
188
- const isAdded = await Bun.file(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`).exists();
189
- if (!isAdded) {
190
- await this.app.spawn("npx cap add ios");
191
- await this.app.spawn("npx @capacitor/assets generate");
192
- } else this.app.log(`iOS already added, skip adding process`);
193
- await this.app.spawn("cross-env", ["APP_OPERATION_MODE=release", "npx", "cap", "sync", "ios"]);
194
- }
195
- async releaseAndroid() {
196
- //TODO: 작업 필요
197
- const isAdded = await Bun.file(`${this.app.cwdPath}/android/app/build.gradle`).exists();
198
- if (!isAdded) {
199
- await this.app.spawn("npx cap add android");
200
- await this.app.spawn("npx @capacitor/assets generate");
201
- } else this.app.log(`android already added, skip adding process`);
202
- await this.app.spawn("cross-env", ["APP_OPERATION_MODE=release", "npx", "cap", "sync", "android"]);
203
- }
204
- async addCamera() {
205
- await this.#setPermissionInIos({
206
- cameraUsageDescription: "$(PRODUCT_NAME) requires access to the camera to take photos.",
207
- photoAddUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
208
- photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
209
- });
210
- this.#setPermissionsInAndroid(["READ_MEDIA_IMAGES", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"]);
211
- }
212
- async addContact() {
213
- await this.#setPermissionInIos({
214
- contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to add new contacts.",
215
- });
216
- this.#setPermissionsInAndroid(["READ_CONTACTS", "WRITE_CONTACTS"]);
217
- }
218
- async addLocation() {
219
- await this.#setPermissionInIos({
220
- locationAlwaysUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
221
- locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
222
- });
223
- this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
224
- this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
225
- }
226
- async #setPermissionInIos(permissions: { [key: string]: string }) {
227
- const updateNs = Object.fromEntries(
228
- Object.entries(permissions).map(([key, value]) => [`NS${capitalize(key)}`, value]),
229
- );
230
- await Promise.all([
231
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", updateNs),
232
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs),
233
- ]);
234
- }
235
- #setFeaturesInAndroid(features: string[]) {
236
- for (const feature of features) {
237
- if (this.#hasFeatureInAndroid(feature)) {
238
- this.app.logger.info(`${feature} already exists in android`);
239
- return this;
240
- }
241
- this.app.logger.info(`Adding ${feature} to android`);
242
- this.project.android
243
- .getAndroidManifest()
244
- .injectFragment("manifest", `<uses-feature android:name="${feature}" />`);
245
- }
246
- return this;
247
- }
248
- #getFeaturesInAndroid() {
249
- const androidManifest = this.project.android.getAndroidManifest();
250
- const element = androidManifest.getDocumentElement();
251
- if (!element) throw new Error("manifest not found");
252
- const usesFeature = element.getElementsByTagName("uses-feature");
253
- return Array.from(usesFeature).map((feature) => feature.getAttribute("android:name"));
254
- }
255
- #hasFeatureInAndroid(feature: string) {
256
- return this.#getFeaturesInAndroid().includes(feature);
257
- }
258
-
259
- #setPermissionsInAndroid(permissions: string[]) {
260
- for (const permission of permissions) {
261
- if (this.#hasPermissionInAndroid(permission)) {
262
- this.app.logger.info(`${permission} already exists in android`);
263
- return this;
264
- }
265
- this.app.logger.info(`Adding ${permission} to android`);
266
- this.project.android
267
- .getAndroidManifest()
268
- .injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
269
- }
270
- return this;
271
- }
272
- #getPermissionsInAndroid() {
273
- const androidManifest = this.project.android.getAndroidManifest();
274
- const element = androidManifest.getDocumentElement();
275
- if (!element) throw new Error("manifest not found");
276
- const usesPermission = element.getElementsByTagName("uses-permission");
277
- return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
278
- }
279
- #hasPermissionInAndroid(permission: string) {
280
- return this.#getPermissionsInAndroid().includes(permission);
281
- }
282
- }