diff --git a/packages/cli/src/utils/compositionServer.ts b/packages/cli/src/utils/compositionServer.ts
index 13b6ae4309..24c46a5036 100644
--- a/packages/cli/src/utils/compositionServer.ts
+++ b/packages/cli/src/utils/compositionServer.ts
@@ -3,6 +3,7 @@
// composition asset files, and binding to a free port.
import { existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
+import { fileURLToPath } from "node:url";
/** Minimal surface of a listening server (satisfied by @hono/node-server's ServerType). */
interface PortBindable {
@@ -15,7 +16,9 @@ interface PortBindable {
}
function helperDir(): string {
- return dirname(new URL(import.meta.url).pathname);
+ // fileURLToPath (not URL.pathname) so the Windows "/D:/..." leading-slash form
+ // doesn't break the bundle-path resolution below.
+ return dirname(fileURLToPath(import.meta.url));
}
export function resolveRuntimePath(): string | null {
diff --git a/packages/core/src/lint/rules/slideshow.ts b/packages/core/src/lint/rules/slideshow.ts
index 2731a47512..80b5d8f449 100644
--- a/packages/core/src/lint/rules/slideshow.ts
+++ b/packages/core/src/lint/rules/slideshow.ts
@@ -1,20 +1,11 @@
-// fallow-ignore-file code-duplication
import type { LintContext, HyperframeLintFinding } from "../context";
import type { LintRule } from "../types";
import { readAttr } from "../utils";
import { parseSlideshowManifest, resolveSlideshow } from "../../slideshow/parseSlideshow";
+import { isSceneLikeCompositionId } from "../../slideshow/sceneId";
type Scene = { id: string; start: number; duration: number };
-/** Mirrors isSceneLikeCompositionId in packages/core/src/runtime/timeline.ts */
-function isSceneLikeCompositionId(compositionId: string): boolean {
- const normalized = compositionId.trim().toLowerCase();
- if (!normalized || normalized === "main") return false;
- if (normalized.includes("caption")) return false;
- if (normalized.includes("ambient")) return false;
- return true;
-}
-
function parseTiming(raw: string): { start: number; duration: number } | null {
const startStr = readAttr(raw, "data-start");
if (startStr === null) return null;
diff --git a/packages/core/src/runtime/timeline.ts b/packages/core/src/runtime/timeline.ts
index e6328d73a8..6f8e583818 100644
--- a/packages/core/src/runtime/timeline.ts
+++ b/packages/core/src/runtime/timeline.ts
@@ -7,6 +7,7 @@ import type {
import { swallow } from "./diagnostics";
import { readElementPlaybackRate } from "./media";
import { createRuntimeStartTimeResolver } from "./startResolver";
+import { isSceneLikeCompositionId } from "../slideshow/sceneId";
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
const AUTHORED_END_ATTR = "data-hf-authored-end";
@@ -230,13 +231,6 @@ export function collectRuntimeTimelinePayload(params: {
}
return maxWindowEndSeconds > 0 ? maxWindowEndSeconds : null;
};
- const isSceneLikeCompositionId = (compositionId: string): boolean => {
- const normalized = compositionId.trim().toLowerCase();
- if (!normalized || normalized === "main") return false;
- if (normalized.includes("caption")) return false;
- if (normalized.includes("ambient")) return false;
- return true;
- };
const resolveNearestCompositionContext = (
node: Element,
root: Element | null,
diff --git a/packages/core/src/slideshow/parseSlideshow.test.ts b/packages/core/src/slideshow/parseSlideshow.test.ts
index d9207243fb..1633d58818 100644
--- a/packages/core/src/slideshow/parseSlideshow.test.ts
+++ b/packages/core/src/slideshow/parseSlideshow.test.ts
@@ -37,6 +37,11 @@ describe("parseSlideshowManifest", () => {
expect(() => parseSlideshowManifest(html)).toThrow();
});
+ it("rejects a non-object manifest (e.g. a JSON array)", () => {
+ const html = ``;
+ expect(() => parseSlideshowManifest(html)).toThrow();
+ });
+
it("throws when a slide entry is malformed (sceneId not a string)", () => {
const html = `