Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@
// complexity pre-dates the computed-timeline work. Exempted at file level
// rather than refactored as scope creep.
"ignore": [
// timeline.ts: collectRuntimeTimelinePayload (CRITICAL) pre-dates this PR;
// only an import line changed here (slideshow/sceneId → slideshow/index),
// but the line-shift fingerprint makes fallow re-flag inherited complexity.
"packages/core/src/runtime/timeline.ts",
// sceneId.ts: trivial 5-cyclomatic guard, moved verbatim from
// packages/core/src/slideshow/ into parsers; flagged only because the move
// makes fallow treat it as a fresh finding.
"packages/parsers/src/slideshow/sceneId.ts",
// gsapParser.ts: the recast/babel GSAP writer is a 2500-line legacy parser;
// moved from packages/core/src/parsers/ — same complexity rationale.
"packages/parsers/src/gsapParser.ts",
Expand Down
27 changes: 13 additions & 14 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 7 additions & 39 deletions packages/core/src/compiler/assetPaths.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,7 @@
/**
* Shared primitives for scanning and rewriting asset paths in HTML/CSS.
*
* Used by: rewriteSubCompPaths (core), collectExternalAssets (producer),
* localizeExternalAssets (CLI publish).
*/

import { isAbsolute, relative, resolve } from "node:path";

/** Regex matching CSS `url(...)` references — captures the quote style and the raw URL. */
export const CSS_URL_RE = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;

/** Attributes that may contain relative asset paths. */
export const PATH_ATTRS = ["src", "href"] as const;

/** Returns true for URLs/prefixes that should never be rewritten. */
export function isNonRelativeUrl(val: string): boolean {
return (
!val ||
val.startsWith("http://") ||
val.startsWith("https://") ||
val.startsWith("//") ||
val.startsWith("data:") ||
val.startsWith("#") ||
val.startsWith("/")
);
}

/**
* Cross-platform containment check: is `childPath` inside `parentPath`?
* Equality counts as "inside".
*/
export function isPathInside(childPath: string, parentPath: string): boolean {
const absChild = resolve(childPath);
const absParent = resolve(parentPath);
if (absChild === absParent) return true;
const rel = relative(absParent, absChild);
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
}
// Moved to @hyperframes/parsers. Re-exported here for back-compat.
export {
CSS_URL_RE,
PATH_ATTRS,
isNonRelativeUrl,
isPathInside,
} from "@hyperframes/parsers/asset-paths";
122 changes: 7 additions & 115 deletions packages/core/src/compiler/rewriteSubCompPaths.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,7 @@
/**
* Rewrite relative asset paths in sub-composition content so they resolve
* correctly after the content is inlined into the root document.
*
* A sub-composition at "compositions/scene.html" referencing "../icon.svg"
* means the project root — but after inlining into root index.html, the
* "../" escapes the project directory and causes 404s. This function
* resolves each relative path against the sub-composition's directory,
* then normalizes it to be relative to the project root.
*
* Used by both the core bundler (preview) and the producer compiler (render)
* to ensure consistent behavior.
*/

// URL paths in HTML output are POSIX regardless of host OS — use the `posix`
// submodule so Windows builds don't emit backslash-separated paths (or worse,
// drive-letter-prefixed artifacts from `resolve("/", ...)`).
import { posix } from "path";
const { join, resolve, dirname } = posix;

import { CSS_URL_RE, PATH_ATTRS, isNonRelativeUrl } from "./assetPaths.js";

const isAbsoluteOrSpecial = isNonRelativeUrl;

/**
* Returns true only for paths that traverse up with `../`.
* Plain relative paths like `assets/foo.svg` are already correct from the
* root perspective — the browser resolves them against the served root, which
* is the project root, so they don't need rewriting.
*/
function needsRewrite(val: string): boolean {
return val.startsWith("../") || val === "..";
}

/**
* Rewrite a single relative path from a sub-composition's context to the
* project root context.
*
* @param compSrcPath - The `data-composition-src` value (e.g. "compositions/scene.html")
* @param relativePath - The asset path to rewrite (e.g. "../icon.svg")
* @returns The rewritten path relative to project root (e.g. "icon.svg"), or
* the original path if no rewriting is needed.
*/
export function rewriteAssetPath(compSrcPath: string, relativePath: string): string {
if (isAbsoluteOrSpecial(relativePath)) return relativePath;
if (!needsRewrite(relativePath)) return relativePath;
const compDir = dirname(compSrcPath);
if (!compDir || compDir === ".") return relativePath;
const resolved = join(compDir, relativePath);
const normalized = resolve("/", resolved).slice(1);
return normalized;
}

/**
* Rewrite all relative `src` and `href` attributes on elements within a
* DOM tree, adjusting paths from the sub-composition's directory context
* to the project root.
*
* @param elements - Iterable of DOM elements to scan (e.g. from querySelectorAll)
* @param compSrcPath - The `data-composition-src` value
* @param getAttr - Function to read an attribute from an element
* @param setAttr - Function to set an attribute on an element
*/
export function rewriteAssetPaths<T>(
elements: Iterable<T>,
compSrcPath: string,
getAttr: (el: T, attr: string) => string | null | undefined,
setAttr: (el: T, attr: string, value: string) => void,
): void {
for (const el of elements) {
for (const attr of PATH_ATTRS) {
const val = (getAttr(el, attr) || "").trim();
const rewritten = rewriteAssetPath(compSrcPath, val);
if (rewritten !== val) {
setAttr(el, attr, rewritten);
}
}
}
}

/**
* Rewrite CSS url(...) references inside inline style attributes.
*/
export function rewriteInlineStyleAssetUrls<T>(
elements: Iterable<T>,
compSrcPath: string,
getStyle: (el: T) => string | null | undefined,
setStyle: (el: T, value: string) => void,
): void {
const compDir = dirname(compSrcPath);
if (!compDir || compDir === ".") return;

for (const el of elements) {
const style = getStyle(el);
if (!style) continue;
const rewritten = rewriteCssAssetUrls(style, compSrcPath);
if (rewritten !== style) {
setStyle(el, rewritten);
}
}
}

/**
* Rewrite CSS url(...) references in a sub-composition's inline styles so
* ../foo.woff2 remains valid after the CSS is hoisted into the root document.
*/
export function rewriteCssAssetUrls(cssText: string, compSrcPath: string): string {
if (!cssText) return cssText;
return cssText.replace(CSS_URL_RE, (full, quote: string, rawUrl: string) => {
const urlValue = (rawUrl || "").trim();
const rewritten = rewriteAssetPath(compSrcPath, urlValue);
if (rewritten === urlValue) return full;
return `url(${quote || ""}${rewritten}${quote || ""})`;
});
}
// Moved to @hyperframes/parsers. Re-exported here for back-compat.
export {
rewriteAssetPath,
rewriteAssetPaths,
rewriteInlineStyleAssetUrls,
rewriteCssAssetUrls,
} from "@hyperframes/parsers/asset-paths";
Loading
Loading