diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 290dcdd3..b795a530 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -66,6 +66,10 @@ jobs: pnpm test # 7. Next.js Build + # Skip env refine during compile/page-data; runtime still validates via env.ts. + # Drop stale generated types that can diverge from source after incremental builds. + rm -rf .next/types .next/dev + export SKIP_ENV_VALIDATION=1 pnpm build # 8. Fix ownership: Build first, THEN set permissions for the web server diff --git a/src/app/admin/import/furni/nitro-editor-dialog.tsx b/src/app/admin/import/furni/nitro-editor-dialog.tsx index 74860c70..352b0fd7 100644 --- a/src/app/admin/import/furni/nitro-editor-dialog.tsx +++ b/src/app/admin/import/furni/nitro-editor-dialog.tsx @@ -39,30 +39,51 @@ interface NitroEditorDialogProps { onOpenChange: (open: boolean) => void; } -// biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code -function updateNested(obj: any, path: string, value: unknown): any { - const clone = JSON.parse(JSON.stringify(obj)); +function updateNested( + obj: Record, + path: string, + value: unknown, +): Record { + const clone = JSON.parse(JSON.stringify(obj)) as Record; const keys = path.split("."); - let current = clone; + let current: Record = clone; for (let i = 0; i < keys.length - 1; i++) { - if (!current[keys[i]]) current[keys[i]] = {}; - current = current[keys[i]]; + const key = keys[i]; + const next = current[key]; + if (next == null || typeof next !== "object" || Array.isArray(next)) { + current[key] = {}; + } + current = current[key] as Record; } current[keys[keys.length - 1]] = value; return clone; } -// biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code -function getNested(obj: any, path: string, fallback?: unknown): any { +function getNested( + obj: unknown, + path: string, + fallback?: unknown, +): unknown { const keys = path.split("."); - let current = obj; + let current: unknown = obj; for (const key of keys) { if (current == null || typeof current !== "object") return fallback; - current = current[key]; + current = (current as Record)[key]; } return current ?? fallback; } +function patchMetadata( + prev: unknown, + path: string, + value: unknown, +): unknown { + if (prev == null || typeof prev !== "object" || Array.isArray(prev)) { + return prev; + } + return updateNested(prev as Record, path, value); +} + const ALL_DIRECTIONS = [0, 1, 2, 3, 4, 5, 6, 7]; const DIR_LABELS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; @@ -220,33 +241,33 @@ export function NitroEditorDialog({ // ── Visual tab helpers ── - const dims = metadata - ? getNested(metadata, "logic.model.dimensions", { x: 1, y: 1, z: 0 }) - : { x: 1, y: 1, z: 0 }; - const directions: number[] = metadata - ? getNested(metadata, "logic.model.directions", [0]) - : [0]; + const dims = ( + metadata + ? getNested(metadata, "logic.model.dimensions", { x: 1, y: 1, z: 0 }) + : { x: 1, y: 1, z: 0 } + ) as { x: number; y: number; z: number }; + const directions = ( + metadata ? getNested(metadata, "logic.model.directions", [0]) : [0] + ) as number[]; const setDim = (axis: "x" | "y" | "z", value: string) => { const num = axis === "z" ? parseFloat(value) || 0 : parseInt(value, 10) || 0; setMetadata((prev: unknown) => - updateNested(prev, `logic.model.dimensions.${axis}`, num), + patchMetadata(prev, `logic.model.dimensions.${axis}`, num), ); }; const toggleDirection = (dir: number) => { - const current: number[] = getNested( - metadata, - "logic.model.directions", - [0], - ); + const current = getNested(metadata, "logic.model.directions", [ + 0, + ]) as number[]; const next = current.includes(dir) ? current.filter((d: number) => d !== dir) : [...current, dir].sort((a: number, b: number) => a - b); if (next.length === 0) return; setMetadata((prev: unknown) => - updateNested(prev, "logic.model.directions", next), + patchMetadata(prev, "logic.model.directions", next), ); }; @@ -549,7 +570,7 @@ export function NitroEditorDialog({ value={metadata.visualizationType || ""} onChange={(e) => setMetadata((prev: unknown) => - updateNested( + patchMetadata( prev, "visualizationType", e.target.value, @@ -571,7 +592,7 @@ export function NitroEditorDialog({ value={metadata.logicType || ""} onChange={(e) => setMetadata((prev: unknown) => - updateNested(prev, "logicType", e.target.value), + patchMetadata(prev, "logicType", e.target.value), ) } className="mt-1 font-mono text-sm" @@ -622,7 +643,7 @@ export function NitroEditorDialog({ value={(layer.z as number) ?? 0} onChange={(e) => { setMetadata((prev: unknown) => - updateNested( + patchMetadata( prev, `visualizations.${viz64Index}.layers.${layerId}.z`, parseInt(e.target.value, 10) || 0, @@ -638,7 +659,7 @@ export function NitroEditorDialog({ value={(layer.alpha as number) ?? 255} onChange={(e) => { setMetadata((prev: unknown) => - updateNested( + patchMetadata( prev, `visualizations.${viz64Index}.layers.${layerId}.alpha`, parseInt(e.target.value, 10) || 0, @@ -652,7 +673,7 @@ export function NitroEditorDialog({ placeholder="—" onChange={(e) => { setMetadata((prev: unknown) => - updateNested( + patchMetadata( prev, `visualizations.${viz64Index}.layers.${layerId}.ink`, e.target.value || undefined, @@ -737,7 +758,7 @@ export function NitroEditorDialog({ .slice(0, 6); const num = parseInt(hex, 16) || 0; setMetadata((prev: unknown) => - updateNested( + patchMetadata( prev, `visualizations.${viz64Index}.colors.${colorId}.${layerId}.color`, num, diff --git a/src/lib/deploy-workflow-contract.test.ts b/src/lib/deploy-workflow-contract.test.ts index fda6036b..65c72c51 100644 --- a/src/lib/deploy-workflow-contract.test.ts +++ b/src/lib/deploy-workflow-contract.test.ts @@ -9,8 +9,10 @@ describe("production deploy workflow", () => { ); it("preserves the Next.js incremental build cache", () => { - expect(workflow).not.toMatch(/rm\s+-rf[^\n]*\.next/); + // May clear .next/types or .next/dev, but must not wipe the whole .next tree. + expect(workflow).not.toMatch(/rm\s+-rf\s+\.next(?:\s|$)/); expect(workflow).toContain("pnpm install --frozen-lockfile"); + expect(workflow).toContain("SKIP_ENV_VALIDATION=1"); }); it("does not override onlyBuiltDependencies (uses pnpm-workspace.yaml)", () => {