fix(studio): guard import.meta.env reads so the player survives non-Vite ESM hosts (#3948)

In non-Vite ESM hosts, import.meta.env is undefined and direct reads crash at module load.

Guard both player reads with optional chaining. Existing Vite defaults are unchanged.

Reject future unguarded import.meta.env property access in either module.

— tai

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
terencecho
2026-09-14 23:40:48 +00:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 14ad0c08a3
commit f98b8ead37
4 changed files with 33 additions and 2 deletions
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { afterEach, describe, expect, it, vi } from "vitest";
afterEach(() => {
@@ -20,4 +21,20 @@ describe("timeline row virtualization flag", () => {
expect(STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED).toBe(false);
});
it("guards the import.meta.env read so it evaluates outside Vite", () => {
// `import.meta.env` is a Vite-only extension and is undefined in a non-Vite
// ESM host (e.g. @hyperframes/studio embedded in Turbopack/Next.js), where
// an unguarded `import.meta.env.VITE_*` read crashes module evaluation. The
// vitest runtime always provides a real `import.meta.env` object, so this
// hazard can't be reproduced behaviorally in-environment; assert on the
// source that the optional-chaining guard is present, matching the repo's
// existing source-inspection tests (see timelineMotionStyles.test.ts).
const source = readFileSync(
new URL("./timelineRowVirtualizationFlag.ts", import.meta.url),
"utf8",
);
expect(source).toContain("import.meta.env?.VITE_STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED");
expect(source).not.toMatch(/import\.meta\.env\.\w/);
});
});
@@ -7,4 +7,4 @@
* type back, which would close an import cycle.
*/
export const STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED =
import.meta.env.VITE_STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED !== "0";
import.meta.env?.VITE_STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED !== "0";
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { defaultThumbnailMode, effectiveThumbnailMode } from "./thumbnailPolicy";
@@ -11,4 +12,17 @@ describe("thumbnail runtime policy", () => {
expect(effectiveThumbnailMode("adaptive", "force-hidden")).toBe("hidden");
expect(effectiveThumbnailMode("adaptive", "follow-preference")).toBe("adaptive");
});
it("guards the import.meta.env read so it evaluates outside Vite", () => {
// `import.meta.env` is a Vite-only extension and is undefined in a non-Vite
// ESM host (e.g. @hyperframes/studio embedded in Turbopack/Next.js), where
// an unguarded `import.meta.env.VITE_*` read crashes module evaluation. The
// vitest runtime always provides a real `import.meta.env` object, so this
// hazard can't be reproduced behaviorally in-environment; assert on the
// source that the optional-chaining guard is present, matching the repo's
// existing source-inspection tests (see timelineMotionStyles.test.ts).
const source = readFileSync(new URL("./thumbnailPolicy.ts", import.meta.url), "utf8");
expect(source).toContain("import.meta.env?.VITE_STUDIO_TIMELINE_THUMBNAIL_POLICY");
expect(source).not.toMatch(/import\.meta\.env\.\w/);
});
});
@@ -4,7 +4,7 @@ export type ThumbnailRuntimePolicy = "follow-preference" | "force-hidden" | "leg
// "Adaptive" currently means the scheduler pauses rich work while scrolling.
// The mode name leaves room for finer-grained runtime heuristics later.
const rawPolicy = import.meta.env.VITE_STUDIO_TIMELINE_THUMBNAIL_POLICY;
const rawPolicy = import.meta.env?.VITE_STUDIO_TIMELINE_THUMBNAIL_POLICY;
const studioThumbnailRuntimePolicy: ThumbnailRuntimePolicy =
rawPolicy === "force-hidden" || rawPolicy === "legacy-default" ? rawPolicy : "follow-preference";