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

Showing the changes to apps (2 of 3 files). Show all

Unified · Split

2 files
  1. apps/server/src/projectRegistry.ts +3 −34
  2. apps/web/src/store.ts +1 −32
apps/server/src/projectRegistry.ts+3−34
@@ -11,6 +11,7 @@ import { · 10 unmodified lines
11 type ProjectScript,11 type ProjectScript,
12 type ProjectUpdateScriptsInput,12 type ProjectUpdateScriptsInput,
13 type ProjectUpdateScriptsResult,13 type ProjectUpdateScriptsResult,
14 normalizeProjectScripts,
14 projectAddInputSchema,15 projectAddInputSchema,
15 projectRecordSchema,16 projectRecordSchema,
16 projectRemoveInputSchema,17 projectRemoveInputSchema,
@@ -130,7 +131,7 @@ export class ProjectRegistry { · 113 unmodified lines
130 throw new Error(`Project not found: ${input.id}`);131 throw new Error(`Project not found: ${input.id}`);
131 }132 }
132 133
133 const nextScripts = dedupeScriptsById(projectScriptsSchema.parse(input.scripts));134 const nextScripts = normalizeProjectScripts(projectScriptsSchema.parse(input.scripts));
134 const nextUpdatedAt = new Date().toISOString();135 const nextUpdatedAt = new Date().toISOString();
135 const nextProject: ProjectRecord = {136 const nextProject: ProjectRecord = {
136 ...this.projects[index]!,137 ...this.projects[index]!,
@@ -167,7 +168,7 @@ export class ProjectRegistry { · 30 unmodified lines
167 cwd: normalizedCwd,168 cwd: normalizedCwd,
168 name:169 name:
169 project.name.trim().length > 0 ? project.name.trim() : inferProjectName(normalizedCwd),170 project.name.trim().length > 0 ? project.name.trim() : inferProjectName(normalizedCwd),
170 scripts: dedupeScriptsById(project.scripts),171 scripts: normalizeProjectScripts(project.scripts),
171 };172 };
172 deduped.set(normalizedCwd, normalizedProject);173 deduped.set(normalizedCwd, normalizedProject);
173 normalizedProjects.push(normalizedProject);174 normalizedProjects.push(normalizedProject);
@@ -232,35 +233,3 @@ export class ProjectRegistry { · 58 unmodified lines
232 fs.renameSync(tempFile, this.filePath);233 fs.renameSync(tempFile, this.filePath);
233 }234 }
234}235}
235
236function 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
8 useReducer,8 useReducer,
9} from "react";9} from "react";
10 10
11import type { ProviderEvent, ProviderSession, TerminalEvent } from "@t3tools/contracts";11import { type ProviderEvent, type ProviderSession, type TerminalEvent, normalizeProjectScripts } from "@t3tools/contracts";
12import { resolveModelSlug } from "./model-logic";12import { resolveModelSlug } from "./model-logic";
13import { hydratePersistedState, toPersistedState } from "./persistenceSchema";13import { hydratePersistedState, toPersistedState } from "./persistenceSchema";
14import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic";14import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic";
@@ -165,37 +165,6 @@ function normalizeRunningTerminalIds( · 150 unmodified lines
165 .filter((id) => id.length > 0 && validTerminalIdSet.has(id));165 .filter((id) => id.length > 0 && validTerminalIdSet.has(id));
166}166}
167 167
168function 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}
199 168
200function normalizeTerminalGroupIds(terminalIds: string[]): string[] {169function normalizeTerminalGroupIds(terminalIds: string[]): string[] {
201 return [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))];170 return [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))];