@spectrabrain/eds 0.0.4 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI.md +13 -40
- package/AI_CONTEXT.json +90 -0
- package/CHANGELOG.md +120 -0
- package/CONSUMER_AGENT.md +152 -0
- package/FRONTEND_CONVENTIONS.md +252 -0
- package/QUICKSTART.md +42 -0
- package/README.md +38 -69
- package/TAILWIND_V4.md +57 -243
- package/TOKEN_CONTEXT.md +98 -0
- package/consumer/align/templates/components.json +21 -0
- package/consumer/align/templates/env.mock +1 -0
- package/consumer/align/templates/eslint.config.js +35 -0
- package/consumer/align/templates/package.scripts.json +10 -0
- package/consumer/align/templates/prettierignore +2 -0
- package/consumer/align/templates/prettierrc.json +7 -0
- package/consumer/align/templates/src/app/App.tsx +16 -0
- package/consumer/align/templates/src/app/main.tsx +27 -0
- package/consumer/align/templates/src/app/providers/ErrorBoundary.tsx +58 -0
- package/consumer/align/templates/src/app/providers/ErrorHandler.tsx +29 -0
- package/consumer/align/templates/src/app/router/index.tsx +23 -0
- package/consumer/align/templates/src/app/styles/index.css +2 -0
- package/consumer/align/templates/src/mocks/browser.ts +5 -0
- package/consumer/align/templates/src/mocks/handlers.ts +4 -0
- package/consumer/align/templates/src/shared/api/index.ts +23 -0
- package/consumer/align/templates/src/shared/api/queryClient.ts +24 -0
- package/consumer/align/templates/src/shared/lib/eventBus.ts +44 -0
- package/consumer/align/templates/src/shared/lib/utils/index.ts +6 -0
- package/consumer/align/templates/src/vite-env.d.ts +2 -0
- package/consumer/align/templates/tsconfig.json +22 -0
- package/consumer/align/templates/vite.config.babel.ts +36 -0
- package/consumer/align/templates/vite.config.rolldown.ts +35 -0
- package/consumer/align-analysis.mjs +1159 -0
- package/consumer/align-plan.mjs +377 -0
- package/consumer/align-rules.mjs +473 -0
- package/consumer/align.mjs +401 -0
- package/consumer/cli.mjs +157 -0
- package/consumer/css-root.mjs +259 -0
- package/consumer/doctor.mjs +204 -0
- package/consumer/generate-tailwind.mjs +83 -0
- package/consumer/init.mjs +186 -0
- package/consumer/managed-block.mjs +126 -0
- package/consumer/paths.mjs +266 -0
- package/consumer/register-scripts.mjs +129 -0
- package/consumer/scan-classes.mjs +71 -0
- package/consumer/sync.mjs +155 -0
- package/consumer/update-agent-context.mjs +143 -0
- package/consumer-postinstall.mjs +60 -0
- package/dist/base.css +4 -0
- package/dist/components/Button/README.md +70 -80
- package/dist/components/Button/spec.json +71 -133
- package/dist/components.css +102 -368
- package/dist/index.d.mts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +34 -6250
- package/dist/index.mjs +36 -6252
- package/dist/styles.css +1 -0
- package/dist/tokens.d.mts +5 -2
- package/dist/tokens.d.ts +5 -2
- package/dist/tokens.js +6 -7
- package/dist/tokens.mjs +6 -7
- package/llms.txt +22 -9
- package/package.json +33 -2
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { conventionsFileName, edsLucideVersion, installRanges, mockDevDependencies, templateDevDependencies } from "./align-rules.mjs";
|
|
2
|
+
import { cliCommand, runnerCommand } from "./paths.mjs";
|
|
3
|
+
|
|
4
|
+
const installPrefixes = {
|
|
5
|
+
npm: { add: "npm install", dev: "npm install -D", remove: "npm uninstall", dlx: "npx", exec: "npx" },
|
|
6
|
+
pnpm: { add: "pnpm add", dev: "pnpm add -D", remove: "pnpm remove", dlx: "pnpm dlx", exec: "pnpm exec" },
|
|
7
|
+
yarn: { add: "yarn add", dev: "yarn add -D", remove: "yarn remove", dlx: "yarn dlx", exec: "yarn" },
|
|
8
|
+
bun: { add: "bun add", dev: "bun add -d", remove: "bun remove", dlx: "bunx", exec: "bunx" },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// @vitejs/plugin-react 6 requires Vite 8; 4 still covers Vite 4–7 on Node 18.
|
|
12
|
+
export function pluginReactRange(viteMajor) {
|
|
13
|
+
return viteMajor !== null && viteMajor >= 8 ? "^6" : "^4";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function packageManagerCommands(packageManager, { viteMajor = null } = {}) {
|
|
17
|
+
const prefixes = installPrefixes[packageManager?.name] ?? installPrefixes.npm;
|
|
18
|
+
const dlx = packageManager?.name === "yarn" && !packageManager.berry ? "npx" : prefixes.dlx;
|
|
19
|
+
const ranges = { ...installRanges, "@vitejs/plugin-react": pluginReactRange(viteMajor), ...(edsLucideVersion ? { "lucide-react": edsLucideVersion } : {}) };
|
|
20
|
+
const withRange = (name) => (ranges[name] ? name + "@" + ranges[name] : name);
|
|
21
|
+
return {
|
|
22
|
+
run: (script) => runnerCommand(packageManager, script),
|
|
23
|
+
add: (names) => prefixes.add + " " + names.map(withRange).join(" "),
|
|
24
|
+
addDev: (names) => prefixes.dev + " " + names.map(withRange).join(" "),
|
|
25
|
+
remove: (names) => prefixes.remove + " " + names.join(" "),
|
|
26
|
+
eds: (args) => cliCommand(packageManager, args),
|
|
27
|
+
dlx: (command) => dlx + " " + command,
|
|
28
|
+
// A binary of an installed package (for example msw init).
|
|
29
|
+
exec: (command) => prefixes.exec + " " + command,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const kindOrder = ["protected", "outside", "backend-script", "backend-dependency", "structure", "language", "import", "dependency", "config", "script", "pattern", "agents"];
|
|
34
|
+
|
|
35
|
+
export function countFindings(findings) {
|
|
36
|
+
const errors = new Map();
|
|
37
|
+
const warnings = new Map();
|
|
38
|
+
for (const finding of findings) {
|
|
39
|
+
const counts = finding.level === "error" ? errors : warnings;
|
|
40
|
+
counts.set(finding.kind, (counts.get(finding.kind) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
const format = (counts) => kindOrder.filter((kind) => counts.has(kind)).map((kind) => kind + " " + counts.get(kind)).join(", ");
|
|
43
|
+
const total = (counts) => [...counts.values()].reduce((sum, count) => sum + count, 0);
|
|
44
|
+
return { errors: total(errors), warnings: total(warnings), errorSummary: format(errors), warningSummary: format(warnings) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function code(text) {
|
|
48
|
+
return "`" + text + "`";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function table(header, rows) {
|
|
52
|
+
if (rows.length === 0) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return [
|
|
56
|
+
"| " + header.join(" | ") + " |",
|
|
57
|
+
"| " + header.map(() => "---").join(" | ") + " |",
|
|
58
|
+
...rows.map((row) => "| " + row.join(" | ") + " |"),
|
|
59
|
+
"",
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function list(items, empty) {
|
|
64
|
+
return items.length === 0 ? [empty] : items;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// PLAN.md has this many steps; NOTES.md counts the finished ones against it.
|
|
68
|
+
const stepCount = 8;
|
|
69
|
+
|
|
70
|
+
export function renderNotes() {
|
|
71
|
+
return [
|
|
72
|
+
"# EDS align 작업 기록",
|
|
73
|
+
"",
|
|
74
|
+
"`eds align`은 이 파일을 다시 쓰지 않습니다. 새 세션을 시작했거나 이전 대화가 요약되었으면 PLAN.md와 이 파일을 읽고 「진행」의 다음 단계부터 이어갑니다.",
|
|
75
|
+
"",
|
|
76
|
+
"## 진행",
|
|
77
|
+
"",
|
|
78
|
+
"- commit: (1단계에서 `허용` 또는 `안 함`으로 적습니다)",
|
|
79
|
+
"- 완료한 단계: 0 / " + stepCount + " (단계의 완료 기준을 모두 만족한 뒤에만 번호를 올립니다)",
|
|
80
|
+
"",
|
|
81
|
+
"## 기준선",
|
|
82
|
+
"",
|
|
83
|
+
"## domain 목록",
|
|
84
|
+
"",
|
|
85
|
+
"## 보호 경로 확인",
|
|
86
|
+
"",
|
|
87
|
+
"## 단계별 기록",
|
|
88
|
+
"",
|
|
89
|
+
"## 보류 사항",
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function numbered(items) {
|
|
95
|
+
return items.filter(Boolean).map((item, index) => index + 1 + ". " + item);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// PLAN.md: the facts of this repository and the ordered steps. The rules themselves live in FRONTEND_CONVENTIONS.md.
|
|
99
|
+
export function renderPlan({ version, createdAt, base, packageManager, frontend, analysis, templates, alignLabel, conventionsPath = null }) {
|
|
100
|
+
const { app, dependencies, protectedInfo, majors } = analysis;
|
|
101
|
+
const commands = packageManagerCommands(packageManager, { viteMajor: majors.vite });
|
|
102
|
+
const alignCheck = commands.eds("align --check");
|
|
103
|
+
const counts = countFindings(analysis.findings);
|
|
104
|
+
const packageLabel = frontend.packageRelative === "" ? "repository root" : code(frontend.packageRelative);
|
|
105
|
+
const conventions = conventionsPath === null ? "EDS package root의 " + code(conventionsFileName) : code(conventionsPath);
|
|
106
|
+
const findingsOf = (kinds) => analysis.findings.filter((finding) => kinds.includes(finding.kind));
|
|
107
|
+
const scripts = analysis.packageJson?.scripts ?? {};
|
|
108
|
+
|
|
109
|
+
const protectedRows = protectedInfo.inside.map((entry) => [
|
|
110
|
+
code(entry),
|
|
111
|
+
entry === "shared" ? "backend와 프론트엔드가 함께 쓰는 코드(import 허용, 수정 금지)" : "backend·배포 코드 또는 설정",
|
|
112
|
+
]);
|
|
113
|
+
if (frontend.packageRelative !== "") {
|
|
114
|
+
protectedRows.push(["프론트엔드 package 밖 전체", "다른 package와 backend" + (protectedInfo.repositoryOnly.length > 0 ? "(" + protectedInfo.repositoryOnly.map(code).join(", ") + " 포함)" : "")]);
|
|
115
|
+
}
|
|
116
|
+
const backendScriptEntries = Object.entries(protectedInfo.backendScripts ?? {});
|
|
117
|
+
const backendScripts = backendScriptEntries.map(([name, command]) => code(name) + ": " + code(command));
|
|
118
|
+
|
|
119
|
+
const structureRows = analysis.structure.map((entry) => [
|
|
120
|
+
code(entry.name + (entry.type === "directory" ? "/" : "")),
|
|
121
|
+
String(entry.files),
|
|
122
|
+
entry.hint ?? (entry.fsd ? "FSD 위치입니다. 하위 규칙은 check 경고를 따릅니다." : "역할을 보고 app·pages·widgets·features·entities·shared 중 한 곳으로 옮깁니다."),
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const replacementRows = dependencies.replacements.filter((entry) => !entry.keptForBackend).map((entry) => [
|
|
126
|
+
entry.category,
|
|
127
|
+
code(entry.name),
|
|
128
|
+
entry.target === "lucide-react" ? entry.label + "(" + code(commands.add(["lucide-react"])) + ")" : entry.label,
|
|
129
|
+
]);
|
|
130
|
+
const keptForBackend = dependencies.replacements.filter((entry) => entry.keptForBackend).map((entry) => code(entry.name));
|
|
131
|
+
const replacedCategories = new Set(replacementRows.map(([category]) => category));
|
|
132
|
+
const reactOutdated = dependencies.outdated.find((entry) => entry.name === "react") ?? null;
|
|
133
|
+
const laterUpgrades = dependencies.outdated.filter((entry) => entry.name !== "react" && entry.name !== "tailwindcss");
|
|
134
|
+
const upgrades = dependencies.outdated.map((entry) => {
|
|
135
|
+
const step = entry.name === "react" || entry.name === "tailwindcss" ? "2단계" : "4단계";
|
|
136
|
+
return "- " + code(entry.name) + " " + entry.major + " → " + entry.minimum + " (" + step + ")";
|
|
137
|
+
});
|
|
138
|
+
const missingRuntime = dependencies.missing.filter((entry) => !entry.dev && entry.name !== "@spectrabrain/eds").map((entry) => entry.name);
|
|
139
|
+
// Step 2 already installs Tailwind and, when React is upgraded, the React 19 types.
|
|
140
|
+
const installedInStep2 = new Set(["tailwindcss", "@tailwindcss/vite", ...(dependencies.outdated.some((entry) => entry.name === "react") ? ["@types/react", "@types/react-dom"] : [])]);
|
|
141
|
+
const missingDev = [
|
|
142
|
+
...dependencies.missing.filter((entry) => entry.dev && !installedInStep2.has(entry.name)).map((entry) => entry.name),
|
|
143
|
+
...templateDevDependencies.filter((name) => !analysis.declared.has(name) && !(dependencies.sdkOnly && name === "msw")),
|
|
144
|
+
...(dependencies.outdatedTools ?? []).map((entry) => entry.name),
|
|
145
|
+
];
|
|
146
|
+
const edsDeclared = analysis.declared.has("@spectrabrain/eds");
|
|
147
|
+
const tailwindMajor = majors.tailwindcss;
|
|
148
|
+
const viteLabel = majors.vite === null ? "Vite" : "Vite " + majors.vite;
|
|
149
|
+
const reactLabel = majors.react === null ? "React" : "React " + majors.react;
|
|
150
|
+
const configFindings = findingsOf(["config", "script"]).map((finding) => "- " + finding.message);
|
|
151
|
+
const importedFrontend = protectedInfo.importedFrontendFiles.map(code);
|
|
152
|
+
const backendImportsViteConfig = protectedInfo.importedFrontendFiles.some((file) => /^vite\.config\.[cm]?[jt]s$/.test(file));
|
|
153
|
+
const backendStartScript = backendScriptEntries.find(([name]) => name === "start")?.[0] ?? backendScriptEntries[0]?.[0] ?? null;
|
|
154
|
+
const appPrefix = app.appDir === "." ? "" : app.appDir + "/";
|
|
155
|
+
const entryLabel = app.entry === null ? appPrefix + "index.html" : appPrefix + app.entry.replace(/^\//, "");
|
|
156
|
+
const templatesOf = (relativePath) => code(alignLabel + "/templates/" + relativePath);
|
|
157
|
+
// Config templates are the files outside the source and public trees; package.scripts.json is merged, not copied.
|
|
158
|
+
const configTemplates = templates.filter((target) => !(dependencies.sdkOnly && target.endsWith(".env.mock")) &&
|
|
159
|
+
target !== "package.scripts.json" && !target.startsWith(app.srcDir + "/") && !target.startsWith(appPrefix + "public/") &&
|
|
160
|
+
!target.startsWith("public/")).map(code);
|
|
161
|
+
const srcPath = (relativePath) => app.srcDir + "/" + relativePath;
|
|
162
|
+
const sdkClients = dependencies.dataClients ?? [];
|
|
163
|
+
const uiDependents = dependencies.uiDependents ?? [];
|
|
164
|
+
const inProgressAgentsLine = "- " + alignLabel + "/PLAN.md가 있으면 작업 전에 PLAN.md와 NOTES.md를 읽고 「진행」의 다음 단계부터 이어갑니다.";
|
|
165
|
+
|
|
166
|
+
return [
|
|
167
|
+
"# EDS align 계획",
|
|
168
|
+
"",
|
|
169
|
+
"<!-- @spectrabrain/eds@" + version + " eds align이 생성했습니다. 다시 실행하면 이 파일과 templates/를 새로 씁니다. -->",
|
|
170
|
+
"",
|
|
171
|
+
"이 문서는 `eds align`이 이 저장소를 분석해 만든 일회성 작업 계획입니다. AI 에이전트는 이 계획의 단계를 순서대로 진행해 프론트엔드를 EVA 프론트엔드 컨벤션에 맞춥니다. 규칙의 원천은 " + conventions + "이며, 이 문서는 이 저장소에 해당하는 사실과 순서만 담습니다.",
|
|
172
|
+
"",
|
|
173
|
+
"- 기준 commit: " + code(base.slice(0, 12)) + " (" + createdAt.slice(0, 10) + " 생성)",
|
|
174
|
+
"- 프론트엔드 package: " + packageLabel + " · " + viteLabel + " · " + reactLabel + " · 패키지 매니저 " + packageManager.name,
|
|
175
|
+
"- 작업 디렉터리: " + code(frontend.packageRelative || ".") + "(git root 기준). 이 문서의 모든 명령은 이 디렉터리에서 실행합니다.",
|
|
176
|
+
"- 소스 디렉터리: " + code(app.srcDir) + " · Vite root(`index.html`): " + code(app.appDir),
|
|
177
|
+
"- 검증: " + code(alignCheck) + "(이하 check)는 기준 commit 이후의 보호 경로 변경과 구조·import·의존성·설정·script를 점검합니다. 1~5단계에서는 각 단계 완료 기준에 적힌 종류의 줄만 사라지면 되고, exit 0은 6단계부터 요구합니다.",
|
|
178
|
+
"- 작업 기록: " + code(alignLabel + "/NOTES.md") + "에 진행, 기준선, domain 목록, 결정, 보류 사항을 적습니다.",
|
|
179
|
+
"- 템플릿: " + code(alignLabel + "/templates/") + "에 이 저장소의 경로로 채운 설정·소스 파일 " + templates.length + "개가 대상 경로 그대로 있습니다.",
|
|
180
|
+
"- 테스트: 테스트 파일·테스트 디렉터리·테스트 도구·설정은 제자리에 둡니다. 소스를 옮겨 깨진 테스트의 import 경로와 `jest.mock`·`vi.mock` 경로 문자열은 고치고, 그 밖의 원인으로 테스트 파일에서 나는 lint·typecheck 오류는 고치지 않고 NOTES.md에 적은 뒤 사용자에게 확인합니다. 옛 디렉터리에 테스트만 남아도 되며 check는 테스트 파일을 점검하지 않습니다.",
|
|
181
|
+
"",
|
|
182
|
+
"## 보호 경로",
|
|
183
|
+
"",
|
|
184
|
+
"아래 경로는 읽기만 합니다. check가 기준 commit 이후의 변경을 `protected:`·`outside:`로 보고합니다.",
|
|
185
|
+
"",
|
|
186
|
+
...(protectedRows.length === 0
|
|
187
|
+
? ["- 감지된 backend 경로가 없습니다. backend가 다른 저장소에 있거나 이 package가 프론트엔드 전용인지 1단계에서 확인합니다.", ""]
|
|
188
|
+
: table(["경로", "이유"], protectedRows)),
|
|
189
|
+
"- API 계약을 유지합니다. 요청 URL·method·header·payload, 응답 해석, 인증 흐름, backend와 배포가 읽는 환경 변수 이름은 그대로 두고, 코드를 옮겨도 같은 요청을 보냅니다.",
|
|
190
|
+
...(backendScripts.length > 0 ? ["- backend를 실행하는 script는 그대로 둡니다: " + backendScripts.join(", ")] : []),
|
|
191
|
+
...(protectedInfo.backendUsed.length > 0 ? ["- backend 코드가 쓰는 의존성은 package.json에서 지우지 않습니다: " + protectedInfo.backendUsed.map(code).join(", ")] : []),
|
|
192
|
+
...(importedFrontend.length > 0 ? ["- backend 코드가 import하는 프론트엔드 파일은 export 형태를 유지합니다: " + importedFrontend.join(", ")] : []),
|
|
193
|
+
"",
|
|
194
|
+
"## 현재 상태",
|
|
195
|
+
"",
|
|
196
|
+
"check 기준 오류 " + counts.errors + "개" + (counts.errorSummary ? "(" + counts.errorSummary + ")" : "") +
|
|
197
|
+
", 경고 " + counts.warnings + "개" + (counts.warningSummary ? "(" + counts.warningSummary + ")" : "") + "입니다.",
|
|
198
|
+
"",
|
|
199
|
+
"### 구조 (" + code(app.srcDir) + ")",
|
|
200
|
+
"",
|
|
201
|
+
...table(["현재", "파일", "옮길 위치"], structureRows),
|
|
202
|
+
"### 의존성",
|
|
203
|
+
"",
|
|
204
|
+
...(replacementRows.length > 0
|
|
205
|
+
? ["교체할 라이브러리입니다(4단계).", "", ...table(["범주", "패키지", "대체"], replacementRows)]
|
|
206
|
+
: keptForBackend.length > 0 ? [] : ["교체할 라이브러리가 없습니다.", ""]),
|
|
207
|
+
...(keptForBackend.length > 0
|
|
208
|
+
? ["backend 코드도 쓰는 교체 대상입니다. 패키지는 package.json에 남기고 프론트엔드 코드의 사용만 대체 라이브러리로 옮기며, backend가 import하는 파일은 그대로 둡니다(4단계): " + keptForBackend.join(", "), ""]
|
|
209
|
+
: []),
|
|
210
|
+
...(upgrades.length > 0 ? ["업그레이드할 라이브러리입니다.", "", ...upgrades, ""] : []),
|
|
211
|
+
...(dependencies.backendClients.length > 0
|
|
212
|
+
? ["backend client SDK는 교체하지 않습니다(6단계 service): " + dependencies.backendClients.map(code).join(", "), ""]
|
|
213
|
+
: []),
|
|
214
|
+
...(dependencies.others.length > 0 ? ["표준 스택 밖의 의존성입니다(4단계): " + dependencies.others.map(code).join(", "), ""] : []),
|
|
215
|
+
"### 설정과 script",
|
|
216
|
+
"",
|
|
217
|
+
...list(configFindings, "- 설정과 script 오류가 없습니다."),
|
|
218
|
+
"",
|
|
219
|
+
"## 멈추고 사용자에게 확인하는 경우",
|
|
220
|
+
"",
|
|
221
|
+
"아래에 해당하면 그 항목의 변경을 멈추고, NOTES.md 「보류 사항」에 파일·이유·선택지를 적은 뒤 사용자에게 묻습니다. 답을 받을 때까지 그 항목과 그 항목에 의존하는 작업은 두고 무관한 작업은 계속합니다. 단계의 완료 기준이 보류 항목에 막히면 그 단계는 끝내지 않습니다.",
|
|
222
|
+
"",
|
|
223
|
+
"- 보호 경로, API 계약, backend script·의존성을 바꿔야만 진행할 수 있을 때",
|
|
224
|
+
"- 교체할 라이브러리의 기능을 표준 스택으로 표현할 수 없을 때(예: 표준에 없는 UI 컴포넌트), 또는 교체하면 검증 규칙·오류 메시지·제출 동작 중 하나가 달라질 때",
|
|
225
|
+
...(uiDependents.length > 0
|
|
226
|
+
? ["- 교체할 패키지를 shadcn ui 파일이 import할 때: " + uiDependents.map((entry) => code(entry.file) + "(" + code(entry.name) + ")").join(", ")]
|
|
227
|
+
: []),
|
|
228
|
+
"- 업그레이드가 다른 의존성 때문에 막힐 때",
|
|
229
|
+
"- 파일이 프론트엔드인지 backend인지 판단할 수 없을 때",
|
|
230
|
+
"- 검증 실패의 원인이 프론트엔드 밖(backend·배포 설정)에 있을 때",
|
|
231
|
+
"- lint 규칙을 끄거나 우회하지 않고는 lint·typecheck를 통과할 수 없을 때",
|
|
232
|
+
"",
|
|
233
|
+
"## 단계",
|
|
234
|
+
"",
|
|
235
|
+
"새 세션을 시작했거나 이전 대화가 요약되었으면 NOTES.md의 「진행」을 읽고 그다음 단계부터 이어갑니다. 각 단계는 완료 기준을 모두 만족해야 끝납니다. 단계를 마치면 check를 실행하고, 「진행」의 완료한 단계 번호를 올린 뒤 결과와 결정을 「단계별 기록」에 적습니다. 「진행」이 `commit: 허용`이면 단계를 마칠 때마다 commit하고, `commit: 안 함`이면 `git status --short` 출력을 「단계별 기록」에 붙입니다.",
|
|
236
|
+
"",
|
|
237
|
+
"`protected:`·`outside:`·`backend-script:`·`backend-dependency:` 오류는 어느 단계에서 나오든 그 단계를 끝내지 못하게 하므로 해당 변경을 되돌립니다. 검증을 통과시키려고 lint 규칙을 끄거나 `eslint-disable`·`@ts-ignore`·`@ts-expect-error`·`any` 캐스팅·ignore 경로를 새로 넣지 않습니다. 명령은 이 저장소의 패키지 매니저(" + packageManager.name + ") 기준입니다.",
|
|
238
|
+
"",
|
|
239
|
+
"### 1. 준비",
|
|
240
|
+
"",
|
|
241
|
+
...numbered([
|
|
242
|
+
conventions + "를 끝까지 읽습니다.",
|
|
243
|
+
"「보호 경로」가 실제 backend·배포 코드와 맞는지 확인합니다. 프론트엔드가 보호 경로(예: backend와 함께 쓰는 package root `shared/`의 schema·type)를 import하는 것은 정상이므로 import와 alias를 그대로 둡니다. 보호 경로 안에 프론트엔드에서만 쓰는 파일이 있거나 backend·배포 경로가 목록에 없으면 멈추고 사용자에게 확인합니다. 답은 NOTES.md 「보호 경로 확인」에 적고, 목록에 없던 backend 경로는 끝까지 수정하지 않습니다(check는 그 경로를 점검하지 않습니다). 보호 경로 안의 프론트엔드 전용 파일은 사용자가 옮기라고 답해도 옮기지 않고 「보류 사항」에 남깁니다(check가 보호 경로의 변경을 오류로 보고합니다).",
|
|
244
|
+
"작업 branch를 만들고(예: `git switch -c eds-align`), 단계마다 commit해도 되는지 사용자에게 한 번 묻고 답을 NOTES.md 「진행」의 `commit:` 줄에 `허용` 또는 `안 함`으로 적습니다.",
|
|
245
|
+
"`AGENTS.md`의 EDS managed block 밖에 다음 줄을 추가합니다: `" + inProgressAgentsLine + "`",
|
|
246
|
+
"프론트엔드의 API 호출을 모두 찾아 domain 목록을 정하고 NOTES.md 「domain 목록」에 적습니다. domain 이름은 backend resource를 소문자 복수형으로 씁니다(예: `/api/users` → `users`, `supabase.from(\"profiles\")` → `profiles`).",
|
|
247
|
+
scripts.build === undefined ? null : code(commands.run("build")) + "의 exit code와 오류 수를 NOTES.md 「기준선」에 적습니다.",
|
|
248
|
+
]),
|
|
249
|
+
"",
|
|
250
|
+
"완료 기준: 「진행」의 `commit:` 줄에 답이 있고, `AGENTS.md`에 4의 줄이 있고, 「domain 목록」·「보호 경로 확인」" + (scripts.build === undefined ? "" : "·「기준선」") + "이 채워져 있습니다.",
|
|
251
|
+
"",
|
|
252
|
+
"### 2. 스타일 기반과 EDS",
|
|
253
|
+
"",
|
|
254
|
+
...numbered([
|
|
255
|
+
reactOutdated === null
|
|
256
|
+
? null
|
|
257
|
+
: "`react` " + reactOutdated.major + " → 19: " + code(commands.add(["react", "react-dom"])) + "와 " + code(commands.addDev(["@types/react", "@types/react-dom"])) +
|
|
258
|
+
"를 실행한 뒤 " + code(commands.dlx("codemod@latest react/19/migration-recipe")) + "와 " + code(commands.dlx("types-react-codemod@latest preset-19 ./" + app.srcDir)) + "로 코드를 옮깁니다.",
|
|
259
|
+
(tailwindMajor === null
|
|
260
|
+
? "Tailwind CSS v4를 설치하고(" + code(commands.addDev(["tailwindcss", "@tailwindcss/vite"])) + ") `vite.config.ts`의 plugins에 `tailwindcss()`를 추가합니다(템플릿 참고)."
|
|
261
|
+
: (tailwindMajor < 4
|
|
262
|
+
? "Tailwind CSS를 v4로 올리고(" + code(commands.dlx("@tailwindcss/upgrade")) + "), Tailwind를 `@tailwindcss/vite` plugin으로 실행합니다(" + code(commands.addDev(["@tailwindcss/vite"])) + ")."
|
|
263
|
+
: "Tailwind CSS v4를 `@tailwindcss/vite` plugin으로 실행합니다" + (analysis.declared.has("@tailwindcss/vite") ? "." : "(" + code(commands.addDev(["@tailwindcss/vite"])) + ").")) +
|
|
264
|
+
" upgrade 도구가 추가했거나 기존에 쓰던 `@tailwindcss/postcss`와 `postcss.config`의 Tailwind 항목은 지웁니다(PostCSS 경로는 EDS font 파일을 번들에 넣지 않습니다)."),
|
|
265
|
+
"전역 CSS를 " + code(srcPath("app/styles/index.css")) + " 하나로 모으고(템플릿 같은 경로), 현재 entry " + code(entryLabel) + "가 이 파일을 import하게 합니다. 기존 `@import`와 EDS가 넣은 줄은 유지합니다. 컴포넌트별 CSS 규칙은 6단계에서 className으로 옮깁니다.",
|
|
266
|
+
(edsDeclared ? "" : code(commands.add(["@spectrabrain/eds"])) + "로 EDS를 dependencies에 선언하고, ") + code(commands.eds("init")) + "로 EDS script 등록과 첫 sync를 실행합니다. Tailwind root 후보가 여러 개라서 exit 2이면, 나머지 후보가 3번에서 합친 옛 전역 CSS일 때만 " + code("EDS_TAILWIND_ROOT=" + srcPath("app/styles/index.css") + " " + commands.run("sync:eds")) + "를 실행하고, 그렇지 않으면 사용자에게 root를 확인합니다.",
|
|
267
|
+
]),
|
|
268
|
+
"",
|
|
269
|
+
"완료 기준: " + code(commands.run("eds:doctor")) + "가 `[EDS] synced with` 줄을 출력하고 " + code(commands.run("build")) + "가 exit 0이며, check 출력에 `@tailwindcss/vite`·`@tailwindcss/postcss` 줄이 없습니다." +
|
|
270
|
+
(reactOutdated === null ? "" : " package.json의 `react` major가 19입니다."),
|
|
271
|
+
"",
|
|
272
|
+
"### 3. 개발 도구와 설정",
|
|
273
|
+
"",
|
|
274
|
+
...numbered([
|
|
275
|
+
missingDev.length > 0
|
|
276
|
+
? "개발 도구를 설치합니다(템플릿보다 오래된 도구는 다시 설치합니다): " + code(commands.addDev(missingDev))
|
|
277
|
+
: "필요한 개발 도구가 모두 선언되어 있습니다.",
|
|
278
|
+
"`templates/`의 설정 파일(" + configTemplates.join(", ") + ")을 같은 경로로 복사합니다. 설정 값의 기준은 " + conventions + " 「개발 규칙」입니다. 이미 있는 파일은 다음처럼 합칩니다.",
|
|
279
|
+
]),
|
|
280
|
+
" - `vite.config.ts`: 기존 설정의 키와 값은 모두 유지합니다(`root`·`envDir`·`base`·`server`·`build.outDir`·기존 alias·`test` 등). 템플릿에서 없는 것만 추가합니다: `@tailwindcss/vite`·svgr plugin" + (majors.vite !== null && majors.vite >= 8 ? "·`@rolldown/plugin-babel`의 React Compiler plugin" : "") + ", `resolve.alias[\"@\"]`, `resolve.dedupe`(기존 배열이 있으면 합침), `build.minify`·`terserOptions`. " +
|
|
281
|
+
(majors.vite !== null && majors.vite >= 8 ? "" : "기존 `react()` 호출에는 React Compiler의 `babel` 옵션을 넣습니다. ") + "같은 키에 다른 값이 이미 있으면 기존 값을 두고 NOTES.md에 적습니다." +
|
|
282
|
+
(replacedCategories.has("build-plugin") ? " `@vitejs/plugin-react-swc`·`@vitejs/plugin-react-oxc`를 쓰고 있으면 React Compiler를 위해 `@vitejs/plugin-react`로 바꿉니다(패키지 제거는 4단계)." : ""),
|
|
283
|
+
" - `tsconfig.json`: `include`에 「보호 경로」의 경로가 있으면 compilerOptions는 그대로 두고 `paths[\"@/*\"]`만 템플릿처럼 맞춥니다. 보호 경로가 없으면 템플릿 기준으로 바꾸고, 테스트 도구가 쓰는 설정(`types`의 `vitest/globals` 등)만 유지합니다. `tsconfig.app.json`·`tsconfig.node.json`의 프론트엔드 설정은 합친 뒤 지웁니다.",
|
|
284
|
+
" - `components.json`: `aliases`와 `tailwind.css`만 템플릿 값으로 바꾸고 `style`·`baseColor`는 유지합니다.",
|
|
285
|
+
" - `package.json` scripts: `package.scripts.json`에서 없는 script만 추가하고, 추가한 뒤 " + code(commands.eds("init --no-sync")) + "로 `pretypecheck`·`prebuild` hook을 등록합니다. backend를 실행하는 script는 바꾸지 않습니다.",
|
|
286
|
+
" - 이전 lint·format 설정(`.eslintrc*`, `biome.json`, 중복 Prettier 설정)은 규칙을 옮긴 뒤 지웁니다" + (replacedCategories.has("lint-format") ? "(패키지 제거는 4단계)." : "."),
|
|
287
|
+
// Items 1 and 2 above always exist, so the numbering continues at 3.
|
|
288
|
+
"3. " + code(commands.run("format")) + "로 전체를 한 번 포맷합니다. 이 변경은 다른 변경과 섞지 않고 따로 commit하거나(`commit: 허용`) 따로 기록합니다(`commit: 안 함`).",
|
|
289
|
+
"4. " + code(commands.run("typecheck")) + "·" + code(commands.run("lint")) + "를 처음 실행한 결과(오류 수)를 NOTES.md 「기준선」에 적습니다. lint 오류는 4~6단계에서 파일을 옮기거나 코드를 고치면서 함께 줄입니다.",
|
|
290
|
+
"",
|
|
291
|
+
"완료 기준: check에 `config:`·`script:` 오류가 없고, " + [commands.run("format:check"), commands.run("typecheck"), commands.run("build")].map(code).join("·") + "가 exit 0입니다. 기존 `build.outDir`·`base`·`server.proxy`가 병합 전과 같고, " + (packageManager.berry ? "`typecheck` script가 `eds doctor && `로 시작하며" : "`package.json`의 `pretypecheck`가 `eds doctor`를 실행하며") + ", 「기준선」에 typecheck·lint 결과가 있습니다.",
|
|
292
|
+
"",
|
|
293
|
+
"### 4. 라이브러리 정렬",
|
|
294
|
+
"",
|
|
295
|
+
...numbered([
|
|
296
|
+
missingRuntime.length > 0 ? "표준 라이브러리를 추가합니다: " + code(commands.add(missingRuntime)) + (dependencies.sdkOnly ? ". backend는 SDK로만 호출하므로 axios는 설치하지 않습니다." : "") : "표준 라이브러리가 모두 선언되어 있습니다.",
|
|
297
|
+
replacementRows.length > 0 || keptForBackend.length > 0
|
|
298
|
+
? "「의존성」의 교체 대상을 범주 하나씩 처리합니다. 코드를 대체 라이브러리로 옮기고, 표에 있는 원래 패키지를 제거하고(" + code(commands.remove(["<패키지>"])) + "), typecheck·build를 통과시킨 뒤 서두의 commit 규칙을 적용합니다. 범주별 방법은 " + conventions + " 「라이브러리 스택」을 따릅니다. routing·provider처럼 `app/main.tsx`·`app/router/`·`app/providers/`에 둘 코드는 현재 entry에서 동작만 옮기고, 파일 위치와 템플릿 적용은 5단계에서 합니다."
|
|
299
|
+
: null,
|
|
300
|
+
laterUpgrades.length > 0 ? "「의존성」에서 4단계로 표시한 업그레이드를 하나씩 올리고 검증합니다." : null,
|
|
301
|
+
dependencies.others.length > 0 ? "「의존성」의 표준 스택 밖 의존성마다 쓰임을 확인해, 쓰이지 않으면 제거하고 유지·제거 결정을 NOTES.md에 적습니다." : null,
|
|
302
|
+
]),
|
|
303
|
+
"",
|
|
304
|
+
"완료 기준: check에 `dependency:` 오류가 없고, " + [commands.run("typecheck"), commands.run("build")].map(code).join("·") + "가 exit 0입니다." +
|
|
305
|
+
(dependencies.others.length > 0 ? " 표준 스택 밖 의존성마다 NOTES.md에 결정이 있습니다." : ""),
|
|
306
|
+
"",
|
|
307
|
+
"### 5. FSD 구조",
|
|
308
|
+
"",
|
|
309
|
+
...numbered([
|
|
310
|
+
"「구조」 표를 따라 파일을 `git mv`로 옮겨 이력을 보존합니다. 컴포넌트를 역할별로 나누는 기준은 " + conventions + " 「FSD 계층」입니다.",
|
|
311
|
+
"기존 route 경로와 provider 목록을 NOTES.md에 적습니다. entry를 " + code(srcPath("app/main.tsx")) + "로 옮기고 " + code(appPrefix + "index.html") + "의 module script를 `/src/app/main.tsx`로 바꿉니다.",
|
|
312
|
+
"기존 entry의 내용을 템플릿 `main.tsx`·`App.tsx` 구조로 합칩니다. 기존 provider는 모두 유지하고, 템플릿과 같은 역할(Router, QueryClient, 오류 처리)의 provider만 템플릿 것으로 바꿉니다. 기존 route는 모두 `router/index.tsx`로 옮기고 예시 `HomePage` route는 지웁니다. 기존 오류 처리 provider가 i18n 호출로 문구를 표시하고 있으면 그 호출은 유지합니다." +
|
|
313
|
+
(dependencies.sdkOnly ? " 이 저장소는 MSW를 쓰지 않으므로 템플릿 `main.tsx`의 `VITE_API_MOCKING` 블록은 옮기지 않습니다." : ""),
|
|
314
|
+
[srcPath("app/"), srcPath("shared/"), srcPath("vite-env.d.ts"), ...(dependencies.sdkOnly ? [] : [srcPath("mocks/")])].map(templatesOf).join("·") + "의 파일은 같은 경로에 같은 이름의 파일이 없을 때만 복사하고, 있으면 합칩니다. " +
|
|
315
|
+
(dependencies.sdkOnly
|
|
316
|
+
? code(srcPath("shared/api/index.ts")) + "는 템플릿 대신 기존 SDK client(" + sdkClients.map(code).join(", ") + ")를 한 번 만들어 export하고, `queryClient.ts`·`ErrorHandler.tsx`의 `isAxiosError`·`error.response?.status` 판별은 SDK의 오류 타입 판별로 바꿉니다."
|
|
317
|
+
: code(srcPath("shared/api/index.ts")) + "에는 기존 요청의 base URL·header·credentials를 옮겨 채웁니다."),
|
|
318
|
+
"import 경로와 barrel(`index.ts`)은 " + conventions + " 「FSD 계층」의 import·이름 규칙을 따릅니다.",
|
|
319
|
+
code(app.srcDir) + " 안의 JavaScript 파일(`.js`·`.jsx`)은 TypeScript로 바꿉니다. 테스트 파일은 그대로 둡니다.",
|
|
320
|
+
]),
|
|
321
|
+
"",
|
|
322
|
+
"완료 기준: check에 `structure:`·`import:`·`language:` 오류가 없고, " + [commands.run("typecheck"), commands.run("build")].map(code).join("·") + "가 exit 0이며, NOTES.md에 적은 route와 provider가 모두 옮겨져 있습니다.",
|
|
323
|
+
"",
|
|
324
|
+
"### 6. 코드 패턴",
|
|
325
|
+
"",
|
|
326
|
+
"방법은 " + conventions + " 「코드 패턴」의 해당 절을 따릅니다.",
|
|
327
|
+
"",
|
|
328
|
+
...numbered([
|
|
329
|
+
"앱 전역을 먼저 한 번 설정합니다: " + code(srcPath("shared/api")) + (dependencies.sdkOnly ? "의 SDK client" : " instance") + ", 오류 처리(ErrorBoundary·ErrorHandler·sonner·eventBus), " + code(srcPath("app/router/index.tsx")) + "의 lazy page" +
|
|
330
|
+
(dependencies.sdkOnly
|
|
331
|
+
? "."
|
|
332
|
+
: ". 「domain 목록」에 REST API domain이 있으면 " + code(commands.addDev(mockDevDependencies)) + "를 설치하고 `dev:mock` script를 확인한 뒤 " +
|
|
333
|
+
code(commands.exec("msw init " + appPrefix + "public --save")) + "로 worker를 Vite root의 `public/`에 만듭니다."),
|
|
334
|
+
"NOTES.md 「domain 목록」의 domain마다 `types/api.ts`·`types/enums.ts` → `<domain>Service.ts` → query·mutation hook → (필요하면) store" + (dependencies.sdkOnly ? "" : " → (REST API면) " + code(srcPath("mocks/<domain>")) + " handler") + " → form·component·page 순으로 옮깁니다. UI 문구는 " + conventions + " 「UI 문구」를 따릅니다." +
|
|
335
|
+
(dependencies.backendClients.length > 0 ? " backend client SDK(" + dependencies.backendClients.map(code).join(", ") + ")는 교체하지 않으며, SDK client는 " + code(srcPath("shared/api")) + "에서 한 번만 만들고 entity service만 그 client를 import합니다." : "") +
|
|
336
|
+
" domain 하나를 마칠 때마다 typecheck·build를 실행하고 「단계별 기록」에 `<domain>: service·hook·store·mock 경로, 완료`를 적습니다.",
|
|
337
|
+
"남은 " + code(commands.run("lint")) + " 오류를 고칩니다.",
|
|
338
|
+
]),
|
|
339
|
+
"",
|
|
340
|
+
"완료 기준: 「domain 목록」의 모든 domain에 service·hook 경로와 완료 표시가 있고, 이 저장소 코드의 fetch()·axios 호출은 " + code(srcPath("shared/api")) + "와 entity service에만 있습니다. check가 exit 0이고, 8단계에서 해소하는 `warning agents:`를 뺀 나머지 경고는 모두 고치거나 이유를 NOTES.md에 적었습니다. " +
|
|
341
|
+
[commands.run("lint"), commands.run("typecheck"), commands.run("build")].map(code).join("·") + "가 exit 0입니다.",
|
|
342
|
+
"",
|
|
343
|
+
"### 7. EDS 컴포넌트와 token",
|
|
344
|
+
"",
|
|
345
|
+
...numbered([
|
|
346
|
+
"`QUICKSTART.md` 규칙대로 버튼을 EDS `Button`으로, 색상·typography를 EDS token class로 바꿉니다. shadcn `button.tsx`는 다른 shadcn 컴포넌트가 쓸 때만 남깁니다. 기존 로컬 Button wrapper를 남길지는 `dist/components/Button/README.md` 「문서와 검증」을 따릅니다.",
|
|
347
|
+
code(commands.run("eds:doctor") + " -- --scan") + "을 실행해 보고된 raw 색상 class를 공개 token class로 바꿉니다. 바꿀 수 없는 항목은 파일·class·이유를 NOTES.md에 적습니다.",
|
|
348
|
+
]),
|
|
349
|
+
"",
|
|
350
|
+
"완료 기준: " + code(commands.run("eds:doctor")) + "가 `[EDS] synced with` 줄을 출력하고, `-- --scan` 출력이 `[EDS] scan: no raw color classes found`이거나 남은 class마다 NOTES.md에 이유가 있습니다. " +
|
|
351
|
+
code("grep -rn \"<button\" " + app.srcDir) + "의 결과가 " + code(srcPath("shared/ui/")) + " 안의 shadcn 컴포넌트뿐입니다.",
|
|
352
|
+
"",
|
|
353
|
+
"### 8. 최종 검증과 마무리",
|
|
354
|
+
"",
|
|
355
|
+
...numbered([
|
|
356
|
+
"`AGENTS.md`에서 1단계에 넣은 줄을 다음 줄로 바꿉니다: `- 파일을 만들거나 옮길 때, 의존성을 추가하거나 바꿀 때, service·hook·store·component·page와 화면 문구를 쓸 때 EDS package root의 " + conventionsFileName +
|
|
357
|
+
" 해당 절을 읽고, 완료 전에 " + alignCheck + "를 실행합니다.`",
|
|
358
|
+
[commands.run("format:check"), commands.run("lint"), commands.run("typecheck"), commands.run("build"), commands.run("eds:doctor"), alignCheck].map(code).join(", ") + "을 실행해 모두 exit 0이고 " + code(commands.run("eds:doctor")) + "가 `[EDS] synced with` 줄을 출력하는지 확인합니다.",
|
|
359
|
+
backendImportsViteConfig && backendStartScript !== null
|
|
360
|
+
? "backend가 `vite.config.ts`를 import하므로 build 뒤 " + code(commands.run(backendStartScript)) + "를 실행해 설정을 불러오는 단계에서 오류 없이 기동되는지 확인합니다."
|
|
361
|
+
: null,
|
|
362
|
+
"아래 「완료 보고」를 사용자에게 보내는 최종 응답으로 작성합니다. NOTES.md의 결정·보류 사항과 기록한 수치를 보고에 옮긴 뒤 " + code(alignLabel + "/") + "를 지웁니다.",
|
|
363
|
+
]),
|
|
364
|
+
"",
|
|
365
|
+
"완료 기준: 2의 명령이 모두 exit 0이고 doctor가 `[EDS] synced with` 줄을 출력하고" + (backendImportsViteConfig && backendStartScript !== null ? ", backend가 기동되고" : "") + ", `AGENTS.md`에 1의 줄이 있고 1단계에 넣은 줄은 없으며, " + code(alignLabel + "/") + "가 없습니다.",
|
|
366
|
+
"",
|
|
367
|
+
"## 완료 보고",
|
|
368
|
+
"",
|
|
369
|
+
"- 바뀐 구조(옮긴 디렉터리와 새 layer·slice)",
|
|
370
|
+
"- 교체·추가·제거한 라이브러리와 유지한 라이브러리, 유지한 이유",
|
|
371
|
+
"- 보호 경로를 바꾸지 않았다는 근거(check 출력)",
|
|
372
|
+
"- 검증 명령과 결과, 「기준선」과 비교한 결과, 7단계에서 남긴 raw 색상 class 수",
|
|
373
|
+
"- 결정 사항",
|
|
374
|
+
"- 보류 사항",
|
|
375
|
+
"",
|
|
376
|
+
].join("\n");
|
|
377
|
+
}
|