feat(cli): let projects opt out of automatic proxying (#2591)

* feat(studio-server): serve H.264 proxies from the preview route

Wires the codec manifest and the transcoder into the preview surface: the route
negotiates a proxy via a query param and serves it through the existing range
and ETag machinery, composition HTML carries a codec map for the runtime, and
hostile assets pre-warm so a first play does not wait on a cold transcode.
Exposes the three subpath exports the CLI surfaces consume upstack.

Drops the TEMP fallow entry added with the transcoder: it has real importers now.

* fix(studio-server): publish media proxy exports

* fix(parsers): scan HTML comments linearly

* feat(cli): let projects opt out of automatic proxying

Adds media.autoProxy to hyperframes.json plus --proxy/--no-proxy flags, and
forwards the resolved value into the studio and preview servers and the vite
adapter. Lands before the runtime slice that turns auto-proxying on, so the
switch exists before there is any behavior to switch off.

* fix(cli): align media config schema
This commit is contained in:
Miguel Ángel
2026-07-16 23:01:14 -04:00
committed by GitHub
parent 67eab59f44
commit 6458807066
10 changed files with 292 additions and 5 deletions
@@ -8,6 +8,7 @@ import {
normalizeConfig,
projectConfigPath,
readProjectConfig,
resolveAutoProxy,
writeProjectConfig,
PROJECT_CONFIG_FILENAME,
} from "./projectConfig.js";
@@ -36,6 +37,7 @@ describe("projectConfig", () => {
$schema: DEFAULT_PROJECT_CONFIG.$schema,
registry: "https://example.com/my-registry",
paths: { blocks: "src/blocks", components: "src/fx", assets: "media" },
media: { autoProxy: true },
};
writeProjectConfig(dir, custom);
const read = readProjectConfig(dir);
@@ -60,6 +62,28 @@ describe("projectConfig", () => {
expect(result.paths.components).toBe(DEFAULT_PROJECT_CONFIG.paths.components);
expect(result.paths.assets).toBe(DEFAULT_PROJECT_CONFIG.paths.assets);
});
it("defaults media.autoProxy to true when media is absent", () => {
const result = normalizeConfig({ registry: "https://alt.example.com" });
expect(result.media).toEqual({ autoProxy: true });
});
it("preserves an explicit media.autoProxy: false", () => {
const result = normalizeConfig({ media: { autoProxy: false } });
expect(result.media).toEqual({ autoProxy: false });
});
it("falls back to the default when media.autoProxy is malformed", () => {
const result = normalizeConfig({
media: { autoProxy: "nope" } as unknown as never,
});
expect(result.media).toEqual({ autoProxy: true });
});
it("falls back to the default when media itself is malformed", () => {
const result = normalizeConfig({ media: "nope" as unknown as never });
expect(result.media).toEqual({ autoProxy: true });
});
});
describe("readProjectConfig", () => {
@@ -123,4 +147,85 @@ describe("projectConfig", () => {
}
});
});
describe("resolveAutoProxy", () => {
it("defaults to true when no config file exists and no flag is passed", () => {
const dir = tmp();
try {
expect(resolveAutoProxy(dir, undefined)).toBe(true);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("returns false when the config sets media.autoProxy: false", () => {
const dir = tmp();
try {
writeFileSync(
projectConfigPath(dir),
JSON.stringify({ media: { autoProxy: false } }),
"utf-8",
);
expect(resolveAutoProxy(dir, undefined)).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("defaults to true when the config file is partial and omits media", () => {
const dir = tmp();
try {
writeFileSync(
projectConfigPath(dir),
JSON.stringify({ registry: "https://only-this.example.com" }),
"utf-8",
);
expect(resolveAutoProxy(dir, undefined)).toBe(true);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("an explicit false flag wins over a config that enables it", () => {
const dir = tmp();
try {
writeFileSync(
projectConfigPath(dir),
JSON.stringify({ media: { autoProxy: true } }),
"utf-8",
);
expect(resolveAutoProxy(dir, false)).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("an explicit true flag wins over a config that disables it", () => {
const dir = tmp();
try {
writeFileSync(
projectConfigPath(dir),
JSON.stringify({ media: { autoProxy: false } }),
"utf-8",
);
expect(resolveAutoProxy(dir, true)).toBe(true);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("falls back to the default when the on-disk media value is malformed", () => {
const dir = tmp();
try {
writeFileSync(
projectConfigPath(dir),
JSON.stringify({ media: { autoProxy: "nope" } }),
"utf-8",
);
expect(resolveAutoProxy(dir, undefined)).toBe(true);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
});
+35
View File
@@ -23,12 +23,23 @@ export interface ProjectConfigPaths {
assets: string;
}
export interface ProjectConfigMedia {
/**
* Auto-transcode browser-hostile video codecs (e.g. HEVC) to a cached
* H.264 proxy for supported preview surfaces. Render always uses the
* original file regardless of this setting. Default true.
*/
autoProxy?: boolean;
}
export interface ProjectConfig {
$schema?: string;
/** Base URL of the registry to pull items from. */
registry: string;
/** Target paths for each item type. */
paths: ProjectConfigPaths;
/** Media handling options (e.g. auto-proxying of browser-hostile codecs). */
media?: ProjectConfigMedia;
}
export const DEFAULT_PROJECT_CONFIG: ProjectConfig = {
@@ -39,6 +50,9 @@ export const DEFAULT_PROJECT_CONFIG: ProjectConfig = {
components: "compositions/components",
assets: "assets",
},
media: {
autoProxy: true,
},
};
/** Path to the config file for a project rooted at `projectDir`. */
@@ -72,6 +86,12 @@ export function normalizeConfig(partial: Partial<ProjectConfig>): ProjectConfig
components: partial.paths?.components ?? DEFAULT_PROJECT_CONFIG.paths.components,
assets: partial.paths?.assets ?? DEFAULT_PROJECT_CONFIG.paths.assets,
},
media: {
autoProxy:
typeof partial.media?.autoProxy === "boolean"
? partial.media.autoProxy
: DEFAULT_PROJECT_CONFIG.media?.autoProxy,
},
};
}
@@ -92,3 +112,18 @@ export function writeProjectConfig(
export function loadProjectConfig(projectDir: string): ProjectConfig {
return readProjectConfig(projectDir) ?? DEFAULT_PROJECT_CONFIG;
}
/**
* Resolve whether auto-proxying of browser-hostile video codecs (HEVC, etc.)
* is enabled for a project's live-preview surfaces. A caller's explicit
* `--proxy`/`--no-proxy` flag always wins over the project config, in either
* direction. Falls back to the committed `hyperframes.json`
* `media.autoProxy` setting, and finally to `true` when neither is set.
* Render is never affected by this setting: it always uses the original file.
*/
export function resolveAutoProxy(projectDir: string, flagValue: boolean | undefined): boolean {
if (typeof flagValue === "boolean") {
return flagValue;
}
return loadProjectConfig(projectDir).media?.autoProxy ?? true;
}
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { studioProxyEnv } from "./studioProxyEnv.js";
describe("studioProxyEnv", () => {
it("forwards an explicit --proxy decision to a Studio child process", () => {
expect(studioProxyEnv(true, { KEEP: "yes" })).toEqual({
KEEP: "yes",
HYPERFRAMES_AUTO_PROXY: "true",
});
expect(studioProxyEnv(false, { KEEP: "yes" })).toEqual({
KEEP: "yes",
HYPERFRAMES_AUTO_PROXY: "false",
});
});
});
+9
View File
@@ -0,0 +1,9 @@
export function studioProxyEnv(
autoProxy: boolean,
baseEnv: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
return {
...baseEnv,
HYPERFRAMES_AUTO_PROXY: autoProxy ? "true" : "false",
};
}