mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 12:00:26 +00:00
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:
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
export function studioProxyEnv(
|
||||
autoProxy: boolean,
|
||||
baseEnv: NodeJS.ProcessEnv = process.env,
|
||||
): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...baseEnv,
|
||||
HYPERFRAMES_AUTO_PROXY: autoProxy ? "true" : "false",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user