fix: type-safe nitro editor helpers and stabilize deploy build
Local Build and Deploy / deploy (push) Failing after 29s

Rewrite getNested/updateNested without fragile any/Json inference, clear stale .next types before build, and skip env refine during compile.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-18 19:57:33 +02:00
1 parent 1273f9aa46
commit b22725d3a9
3 files changed
+57 -30

No files matched your search

+4
View File
@@ -66,6 +66,10 @@ jobs:
pnpm test pnpm test
# 7. Next.js Build # 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 pnpm build
# 8. Fix ownership: Build first, THEN set permissions for the web server # 8. Fix ownership: Build first, THEN set permissions for the web server
@@ -39,30 +39,51 @@ interface NitroEditorDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
// biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code function updateNested(
function updateNested(obj: any, path: string, value: unknown): any { obj: Record<string, unknown>,
const clone = JSON.parse(JSON.stringify(obj)); path: string,
value: unknown,
): Record<string, unknown> {
const clone = JSON.parse(JSON.stringify(obj)) as Record<string, unknown>;
const keys = path.split("."); const keys = path.split(".");
let current = clone; let current: Record<string, unknown> = clone;
for (let i = 0; i < keys.length - 1; i++) { for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) current[keys[i]] = {}; const key = keys[i];
current = current[keys[i]]; const next = current[key];
if (next == null || typeof next !== "object" || Array.isArray(next)) {
current[key] = {};
}
current = current[key] as Record<string, unknown>;
} }
current[keys[keys.length - 1]] = value; current[keys[keys.length - 1]] = value;
return clone; return clone;
} }
// biome-ignore lint/suspicious/noExplicitAny: explicitly chosen here, see surrounding code function getNested(
function getNested(obj: any, path: string, fallback?: unknown): any { obj: unknown,
path: string,
fallback?: unknown,
): unknown {
const keys = path.split("."); const keys = path.split(".");
let current = obj; let current: unknown = obj;
for (const key of keys) { for (const key of keys) {
if (current == null || typeof current !== "object") return fallback; if (current == null || typeof current !== "object") return fallback;
current = current[key]; current = (current as Record<string, unknown>)[key];
} }
return current ?? fallback; 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<string, unknown>, path, value);
}
const ALL_DIRECTIONS = [0, 1, 2, 3, 4, 5, 6, 7]; const ALL_DIRECTIONS = [0, 1, 2, 3, 4, 5, 6, 7];
const DIR_LABELS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; const DIR_LABELS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
@@ -220,33 +241,33 @@ export function NitroEditorDialog({
// ── Visual tab helpers ── // ── Visual tab helpers ──
const dims = metadata const dims = (
? getNested(metadata, "logic.model.dimensions", { x: 1, y: 1, z: 0 }) metadata
: { x: 1, y: 1, z: 0 }; ? getNested(metadata, "logic.model.dimensions", { x: 1, y: 1, z: 0 })
const directions: number[] = metadata : { x: 1, y: 1, z: 0 }
? getNested(metadata, "logic.model.directions", [0]) ) as { x: number; y: number; z: number };
: [0]; const directions = (
metadata ? getNested(metadata, "logic.model.directions", [0]) : [0]
) as number[];
const setDim = (axis: "x" | "y" | "z", value: string) => { const setDim = (axis: "x" | "y" | "z", value: string) => {
const num = const num =
axis === "z" ? parseFloat(value) || 0 : parseInt(value, 10) || 0; axis === "z" ? parseFloat(value) || 0 : parseInt(value, 10) || 0;
setMetadata((prev: unknown) => setMetadata((prev: unknown) =>
updateNested(prev, `logic.model.dimensions.${axis}`, num), patchMetadata(prev, `logic.model.dimensions.${axis}`, num),
); );
}; };
const toggleDirection = (dir: number) => { const toggleDirection = (dir: number) => {
const current: number[] = getNested( const current = getNested(metadata, "logic.model.directions", [
metadata, 0,
"logic.model.directions", ]) as number[];
[0],
);
const next = current.includes(dir) const next = current.includes(dir)
? current.filter((d: number) => d !== dir) ? current.filter((d: number) => d !== dir)
: [...current, dir].sort((a: number, b: number) => a - b); : [...current, dir].sort((a: number, b: number) => a - b);
if (next.length === 0) return; if (next.length === 0) return;
setMetadata((prev: unknown) => 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 || ""} value={metadata.visualizationType || ""}
onChange={(e) => onChange={(e) =>
setMetadata((prev: unknown) => setMetadata((prev: unknown) =>
updateNested( patchMetadata(
prev, prev,
"visualizationType", "visualizationType",
e.target.value, e.target.value,
@@ -571,7 +592,7 @@ export function NitroEditorDialog({
value={metadata.logicType || ""} value={metadata.logicType || ""}
onChange={(e) => onChange={(e) =>
setMetadata((prev: unknown) => setMetadata((prev: unknown) =>
updateNested(prev, "logicType", e.target.value), patchMetadata(prev, "logicType", e.target.value),
) )
} }
className="mt-1 font-mono text-sm" className="mt-1 font-mono text-sm"
@@ -622,7 +643,7 @@ export function NitroEditorDialog({
value={(layer.z as number) ?? 0} value={(layer.z as number) ?? 0}
onChange={(e) => { onChange={(e) => {
setMetadata((prev: unknown) => setMetadata((prev: unknown) =>
updateNested( patchMetadata(
prev, prev,
`visualizations.${viz64Index}.layers.${layerId}.z`, `visualizations.${viz64Index}.layers.${layerId}.z`,
parseInt(e.target.value, 10) || 0, parseInt(e.target.value, 10) || 0,
@@ -638,7 +659,7 @@ export function NitroEditorDialog({
value={(layer.alpha as number) ?? 255} value={(layer.alpha as number) ?? 255}
onChange={(e) => { onChange={(e) => {
setMetadata((prev: unknown) => setMetadata((prev: unknown) =>
updateNested( patchMetadata(
prev, prev,
`visualizations.${viz64Index}.layers.${layerId}.alpha`, `visualizations.${viz64Index}.layers.${layerId}.alpha`,
parseInt(e.target.value, 10) || 0, parseInt(e.target.value, 10) || 0,
@@ -652,7 +673,7 @@ export function NitroEditorDialog({
placeholder="—" placeholder="—"
onChange={(e) => { onChange={(e) => {
setMetadata((prev: unknown) => setMetadata((prev: unknown) =>
updateNested( patchMetadata(
prev, prev,
`visualizations.${viz64Index}.layers.${layerId}.ink`, `visualizations.${viz64Index}.layers.${layerId}.ink`,
e.target.value || undefined, e.target.value || undefined,
@@ -737,7 +758,7 @@ export function NitroEditorDialog({
.slice(0, 6); .slice(0, 6);
const num = parseInt(hex, 16) || 0; const num = parseInt(hex, 16) || 0;
setMetadata((prev: unknown) => setMetadata((prev: unknown) =>
updateNested( patchMetadata(
prev, prev,
`visualizations.${viz64Index}.colors.${colorId}.${layerId}.color`, `visualizations.${viz64Index}.colors.${colorId}.${layerId}.color`,
num, num,
+3 -1
View File
@@ -9,8 +9,10 @@ describe("production deploy workflow", () => {
); );
it("preserves the Next.js incremental build cache", () => { 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("pnpm install --frozen-lockfile");
expect(workflow).toContain("SKIP_ENV_VALIDATION=1");
}); });
it("does not override onlyBuiltDependencies (uses pnpm-workspace.yaml)", () => { it("does not override onlyBuiltDependencies (uses pnpm-workspace.yaml)", () => {