History

refactor: extract shared normalizeProjectScripts into @t3tools/contracts

Move the duplicated script normalization logic (dedup-by-id + single runOnWorktreeCreate enforcement) from apps/web/src/store.ts and apps/server/src/projectRegistry.ts into packages/contracts/src/project.ts so both consumers share one canonical implementation. Applied via @cursor push command

Cursor Agent committed 8 months ago

Commit b236dcc1 parent ac05ddc3 files · +41 −66Browse files

Unified · Split

3 files
  1. apps/server/src/projectRegistry.ts +3 −34
  2. apps/web/src/store.ts +1 −32
  3. packages/contracts/src/project.ts +37 −0
apps/server/src/projectRegistry.ts+3−34
@@ -11,6 +11,7 @@ import { · 10 unmodified lines
1111 type ProjectScript,
1212 type ProjectUpdateScriptsInput,
1313 type ProjectUpdateScriptsResult,
14+ normalizeProjectScripts,
1415 projectAddInputSchema,
1516 projectRecordSchema,
1617 projectRemoveInputSchema,
@@ -130,7 +131,7 @@ export class ProjectRegistry { · 113 unmodified lines
130131 throw new Error(`Project not found: ${input.id}`);
131132 }
132133
133− const nextScripts = dedupeScriptsById(projectScriptsSchema.parse(input.scripts));
134+ const nextScripts = normalizeProjectScripts(projectScriptsSchema.parse(input.scripts));
134135 const nextUpdatedAt = new Date().toISOString();
135136 const nextProject: ProjectRecord = {
136137 ...this.projects[index]!,
@@ -167,7 +168,7 @@ export class ProjectRegistry { · 30 unmodified lines
167168 cwd: normalizedCwd,
168169 name:
169170 project.name.trim().length > 0 ? project.name.trim() : inferProjectName(normalizedCwd),
170− scripts: dedupeScriptsById(project.scripts),
171+ scripts: normalizeProjectScripts(project.scripts),
171172 };
172173 deduped.set(normalizedCwd, normalizedProject);
173174 normalizedProjects.push(normalizedProject);
@@ -232,35 +233,3 @@ export class ProjectRegistry { · 58 unmodified lines
232233 fs.renameSync(tempFile, this.filePath);
233234 }
234235 }
235−
236−function dedupeScriptsById(scripts: readonly ProjectScript[]): ProjectScript[] {
237− const seenIds = new Set<string>();
238− const deduped: ProjectScript[] = [];
239− for (const script of scripts) {
240− if (seenIds.has(script.id)) continue;
241− seenIds.add(script.id);
242− deduped.push({
243− id: script.id,
244− name: script.name,
245− command: script.command,
246− icon: script.icon,
247− runOnWorktreeCreate: script.runOnWorktreeCreate,
248− });
249− }
250−
251− let setupAssigned = false;
252− const normalized: ProjectScript[] = [];
253− for (const script of deduped) {
254− if (!script.runOnWorktreeCreate) {
255− normalized.push(script);
256− continue;
257− }
258− if (!setupAssigned) {
259− setupAssigned = true;
260− normalized.push(script);
261− continue;
262− }
263− normalized.push({ ...script, runOnWorktreeCreate: false });
264− }
265− return normalized;
266−}
apps/web/src/store.ts+1−32
@@ -8,7 +8,7 @@ import { · 7 unmodified lines
88 useReducer,
99 } from "react";
1010
11−import type { ProviderEvent, ProviderSession, TerminalEvent } from "@t3tools/contracts";
11+import { type ProviderEvent, type ProviderSession, type TerminalEvent, normalizeProjectScripts } from "@t3tools/contracts";
1212 import { resolveModelSlug } from "./model-logic";
1313 import { hydratePersistedState, toPersistedState } from "./persistenceSchema";
1414 import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic";
@@ -165,37 +165,6 @@ function normalizeRunningTerminalIds( · 150 unmodified lines
165165 .filter((id) => id.length > 0 && validTerminalIdSet.has(id));
166166 }
167167
168−function normalizeProjectScripts(scripts: ProjectScript[]): ProjectScript[] {
169− const deduped: ProjectScript[] = [];
170− const seenIds = new Set<string>();
171− for (const script of scripts) {
172− const id = script.id.trim();
173− if (id.length === 0 || seenIds.has(id)) continue;
174− seenIds.add(id);
175− deduped.push({
176− ...script,
177− id,
178− name: script.name.trim(),
179− command: script.command.trim(),
180− });
181− }
182−
183− let setupAssigned = false;
184− const normalized: ProjectScript[] = [];
185− for (const script of deduped) {
186− if (!script.runOnWorktreeCreate) {
187− normalized.push(script);
188− continue;
189− }
190− if (!setupAssigned) {
191− setupAssigned = true;
192− normalized.push(script);
193− continue;
194− }
195− normalized.push({ ...script, runOnWorktreeCreate: false });
196− }
197− return normalized;
198−}
199168
200169 function normalizeTerminalGroupIds(terminalIds: string[]): string[] {
201170 return [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))];
packages/contracts/src/project.ts+37−0
@@ -84,6 +84,43 @@ export const projectUpdateScriptsResultSchema = z.object({ · 83 unmodified lines
8484
8585 export type ProjectScriptIcon = z.infer<typeof projectScriptIconSchema>;
8686 export type ProjectScript = z.infer<typeof projectScriptSchema>;
87+
88+/**
89+ * Deduplicate scripts by ID and enforce at most one `runOnWorktreeCreate: true`
90+ * flag (the first one wins). Fields are trimmed defensively so this is safe to
91+ * call on data that hasn't been through Zod parsing.
92+ */
93+export function normalizeProjectScripts(scripts: readonly ProjectScript[]): ProjectScript[] {
94+ const seenIds = new Set<string>();
95+ const deduped: ProjectScript[] = [];
96+ for (const script of scripts) {
97+ const id = script.id.trim();
98+ if (id.length === 0 || seenIds.has(id)) continue;
99+ seenIds.add(id);
100+ deduped.push({
101+ ...script,
102+ id,
103+ name: script.name.trim(),
104+ command: script.command.trim(),
105+ });
106+ }
107+
108+ let setupAssigned = false;
109+ const normalized: ProjectScript[] = [];
110+ for (const script of deduped) {
111+ if (!script.runOnWorktreeCreate) {
112+ normalized.push(script);
113+ continue;
114+ }
115+ if (!setupAssigned) {
116+ setupAssigned = true;
117+ normalized.push(script);
118+ continue;
119+ }
120+ normalized.push({ ...script, runOnWorktreeCreate: false });
121+ }
122+ return normalized;
123+}
87124 export type ProjectRecord = z.infer<typeof projectRecordSchema>;
88125 export type ProjectListResult = z.infer<typeof projectListResultSchema>;
89126 export type ProjectAddInput = z.input<typeof projectAddInputSchema>;