mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
refactor: make @hyperframes/lint depend only on parsers (#1773)
* refactor: make @hyperframes/lint depend only on parsers, not core Relocates the leaf utilities lint pulled from core — URL/asset-path helpers, font aliases, and the slideshow manifest parser — into the standalone @hyperframes/parsers base, and drops @hyperframes/core from lint's dependencies. Core keeps back-compat re-export stubs at the old paths, so producer/studio/cli are unchanged. Why: lint was the lightweight validator from #1749, but depending on core transitively pulled studio-server (hono) and bpm-detective — irrelevant to linting. Now installing @hyperframes/lint pulls only parsers + postcss, and the core<->lint dependency cycle is gone. - parsers main entry stays browser-safe (pure utils only); the node:path asset helpers live behind the new @hyperframes/parsers/asset-paths subpath - slideshow parser exposed via @hyperframes/parsers/slideshow * feat(lint): add browser entry; harden CSS url() regex (ReDoS) @hyperframes/lint/browser — a fully client-side rule engine (lintHyperframeHtml, lintMediaUrls, shouldBlockRender) with zero node: builtins, so browser-only editors can validate compositions with no Node.js and no server round-trip. Closes the browser-validation ask on #1749. - shouldBlockRender extracted from the fs-bound project.ts into its own pure module so the browser entry stays node-free - pure composition primitives (data types, font aliases, URL helper) exposed via a new recast-free @hyperframes/parsers/composition subpath, so the browser bundle tree-shakes out the GSAP/recast machinery (verified: esbuild platform=browser bundles with 0 node builtins) - lint built with a platform:browser tsup pass — compile-time guarantee the browser entry never pulls a node builtin - harden CSS_URL_RE against polynomial ReDoS (CodeQL js/polynomial-redos); behavior-preserving, verified against existing tests + an old/new parity check - parsers/lint marked sideEffects:false
This commit is contained in:
@@ -333,6 +333,20 @@
|
||||
// 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",
|
||||
// assetPaths.ts / rewriteSubCompPaths.ts: pure URL/asset-path helpers moved
|
||||
// verbatim from packages/core/src/compiler/ into parsers. The move makes
|
||||
// fallow score them as fresh (high CRAP = inherited complexity with no
|
||||
// coverage mapping yet); the logic is unchanged.
|
||||
"packages/parsers/src/assetPaths.ts",
|
||||
"packages/parsers/src/rewriteSubCompPaths.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",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"packages/aws-lambda": {
|
||||
"name": "@hyperframes/aws-lambda",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.700.0",
|
||||
"@aws-sdk/client-sfn": "^3.700.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@hyperframes/cli",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"bin": {
|
||||
"hyperframes": "./dist/cli.js",
|
||||
},
|
||||
@@ -103,7 +103,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@hyperframes/core",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@chenglou/pretext": "^0.0.5",
|
||||
"@hyperframes/lint": "workspace:*",
|
||||
@@ -128,7 +128,7 @@
|
||||
},
|
||||
"packages/engine": {
|
||||
"name": "@hyperframes/engine",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.13.0",
|
||||
"@hyperframes/core": "workspace:^",
|
||||
@@ -146,7 +146,7 @@
|
||||
},
|
||||
"packages/gcp-cloud-run": {
|
||||
"name": "@hyperframes/gcp-cloud-run",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
"@google-cloud/workflows": "^4.2.0",
|
||||
@@ -166,9 +166,8 @@
|
||||
},
|
||||
"packages/lint": {
|
||||
"name": "@hyperframes/lint",
|
||||
"version": "0.7.11",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
"postcss": "^8.5.8",
|
||||
},
|
||||
@@ -182,7 +181,7 @@
|
||||
},
|
||||
"packages/parsers": {
|
||||
"name": "@hyperframes/parsers",
|
||||
"version": "0.7.11",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"acorn": "^8.17.0",
|
||||
@@ -202,7 +201,7 @@
|
||||
},
|
||||
"packages/player": {
|
||||
"name": "@hyperframes/player",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
},
|
||||
@@ -217,7 +216,7 @@
|
||||
},
|
||||
"packages/producer": {
|
||||
"name": "@hyperframes/producer",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@fontsource/archivo-black": "^5.2.8",
|
||||
"@fontsource/eb-garamond": "^5.2.7",
|
||||
@@ -260,7 +259,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@hyperframes/sdk",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
@@ -286,7 +285,7 @@
|
||||
},
|
||||
"packages/shader-transitions": {
|
||||
"name": "@hyperframes/shader-transitions",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.4.1",
|
||||
},
|
||||
@@ -298,7 +297,7 @@
|
||||
},
|
||||
"packages/studio": {
|
||||
"name": "@hyperframes/studio",
|
||||
"version": "0.7.14",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.1",
|
||||
"@codemirror/commands": "^6.10.3",
|
||||
@@ -345,7 +344,7 @@
|
||||
},
|
||||
"packages/studio-server": {
|
||||
"name": "@hyperframes/studio-server",
|
||||
"version": "0.7.11",
|
||||
"version": "0.7.16",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
|
||||
@@ -79,6 +79,25 @@ if (shouldBlockRender(result)) {
|
||||
}
|
||||
```
|
||||
|
||||
## Browser usage
|
||||
|
||||
The rule engine runs **fully client-side** — no Node.js, no filesystem, no server round-trip. Import from the `@hyperframes/lint/browser` entry to validate composition HTML directly in a browser-only editor or tool:
|
||||
|
||||
```typescript
|
||||
import { lintHyperframeHtml, shouldBlockRender } from '@hyperframes/lint/browser';
|
||||
|
||||
const result = await lintHyperframeHtml(htmlString, { filePath: 'index.html' });
|
||||
if (!result.ok) {
|
||||
for (const f of result.findings) console.warn(f.code, f.message);
|
||||
}
|
||||
```
|
||||
|
||||
The browser entry exposes `lintHyperframeHtml`, `lintMediaUrls`, and `shouldBlockRender` — everything that operates on an HTML string. It is built with a browser target and contains **zero `node:` builtins**, so it bundles cleanly for the client (verified at build time).
|
||||
|
||||
<Info>
|
||||
`lintProject` (which walks a project **directory**) is filesystem-based and is **not** part of the browser entry — import it from the main `@hyperframes/lint` entry in Node.
|
||||
</Info>
|
||||
|
||||
## What the Linter Catches
|
||||
|
||||
Detected issues include:
|
||||
|
||||
@@ -32,6 +32,9 @@ npm install @hyperframes/parsers
|
||||
| `@hyperframes/parsers/gsap-constants` | `SUPPORTED_PROPS`, `SUPPORTED_EASES`, property groups |
|
||||
| `@hyperframes/parsers/spring-ease` | Spring-ease curve generation |
|
||||
| `@hyperframes/parsers/hf-ids` | Deterministic element id stamping |
|
||||
| `@hyperframes/parsers/slideshow` | Slideshow manifest parser (`parseSlideshowManifest`, `resolveSlideshow`) |
|
||||
| `@hyperframes/parsers/composition` | Pure, browser-safe composition primitives (data types, font aliases, URL helper) |
|
||||
| `@hyperframes/parsers/asset-paths` | Node-only asset-path rewriting helpers (`rewriteAssetPath`, …) |
|
||||
|
||||
<Info>
|
||||
The package ships subpath entries so consumers tree-shake to what they use — importing `@hyperframes/parsers/hf-ids` (a couple KB) does **not** pull in the GSAP AST machinery (recast/babel/acorn).
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,129 +1,7 @@
|
||||
/**
|
||||
* Single source of truth for the deterministic font alias map. Both the
|
||||
* producer's @font-face injector and the core lint rules import from here,
|
||||
* eliminating manual drift between the two.
|
||||
*
|
||||
* Keys are lowercase font family names. Values are canonical font slugs
|
||||
* matching CANONICAL_FONTS keys in the producer's deterministicFonts module.
|
||||
*/
|
||||
export const FONT_ALIAS_MAP = {
|
||||
// ── Canonical bundled fonts (self-referencing) ────────────────────────
|
||||
inter: "inter",
|
||||
montserrat: "montserrat",
|
||||
outfit: "outfit",
|
||||
nunito: "nunito",
|
||||
oswald: "oswald",
|
||||
"league gothic": "league-gothic",
|
||||
"archivo black": "archivo-black",
|
||||
"space mono": "space-mono",
|
||||
"ibm plex mono": "ibm-plex-mono",
|
||||
"jetbrains mono": "jetbrains-mono",
|
||||
"eb garamond": "eb-garamond",
|
||||
"playfair display": "playfair-display",
|
||||
"source code pro": "source-code-pro",
|
||||
"noto sans jp": "noto-sans-jp",
|
||||
roboto: "roboto",
|
||||
"open sans": "open-sans",
|
||||
lato: "lato",
|
||||
poppins: "poppins",
|
||||
|
||||
// ── Common aliases → nearest canonical ────────────────────────────────
|
||||
"helvetica neue": "inter",
|
||||
helvetica: "inter",
|
||||
arial: "inter",
|
||||
"helvetica bold": "inter",
|
||||
futura: "montserrat",
|
||||
"din alternate": "montserrat",
|
||||
"arial black": "montserrat",
|
||||
"bebas neue": "league-gothic",
|
||||
"courier new": "jetbrains-mono",
|
||||
courier: "jetbrains-mono",
|
||||
garamond: "eb-garamond",
|
||||
"noto sans japanese": "noto-sans-jp",
|
||||
"segoe ui": "roboto",
|
||||
|
||||
// ── macOS sans-serif system fonts → inter ─────────────────────────────
|
||||
"sf pro": "inter",
|
||||
"sf pro display": "inter",
|
||||
"sf pro text": "inter",
|
||||
"sf pro rounded": "inter",
|
||||
avenir: "inter",
|
||||
"avenir next": "inter",
|
||||
"lucida grande": "inter",
|
||||
geneva: "inter",
|
||||
optima: "inter",
|
||||
|
||||
// ── Windows sans-serif system fonts → inter ───────────────────────────
|
||||
verdana: "inter",
|
||||
tahoma: "inter",
|
||||
"trebuchet ms": "inter",
|
||||
calibri: "inter",
|
||||
candara: "inter",
|
||||
corbel: "inter",
|
||||
"lucida sans": "inter",
|
||||
"lucida sans unicode": "inter",
|
||||
|
||||
// ── Linux sans-serif system fonts → inter ─────────────────────────────
|
||||
"noto sans": "inter",
|
||||
"dejavu sans": "inter",
|
||||
"liberation sans": "inter",
|
||||
|
||||
// ── Monospace system fonts → jetbrains-mono ───────────────────────────
|
||||
"sf mono": "jetbrains-mono",
|
||||
menlo: "jetbrains-mono",
|
||||
monaco: "jetbrains-mono",
|
||||
consolas: "jetbrains-mono",
|
||||
"lucida console": "jetbrains-mono",
|
||||
"lucida sans typewriter": "jetbrains-mono",
|
||||
"andale mono": "jetbrains-mono",
|
||||
"dejavu sans mono": "jetbrains-mono",
|
||||
"liberation mono": "jetbrains-mono",
|
||||
|
||||
// ── Serif system fonts → eb-garamond ──────────────────────────────────
|
||||
georgia: "eb-garamond",
|
||||
palatino: "eb-garamond",
|
||||
"palatino linotype": "eb-garamond",
|
||||
"book antiqua": "eb-garamond",
|
||||
cambria: "eb-garamond",
|
||||
times: "eb-garamond",
|
||||
"times new roman": "eb-garamond",
|
||||
"dejavu serif": "eb-garamond",
|
||||
"liberation serif": "eb-garamond",
|
||||
} satisfies Readonly<Record<string, string>>;
|
||||
|
||||
export const FONT_ALIAS_KEYS: ReadonlySet<string> = new Set(Object.keys(FONT_ALIAS_MAP));
|
||||
|
||||
/**
|
||||
* Human-readable display names for canonical font slugs. Used by the lint
|
||||
* rule to tell authors what their aliased font will render as.
|
||||
*/
|
||||
export const CANONICAL_FONT_DISPLAY_NAMES: Readonly<Record<string, string>> = {
|
||||
inter: "Inter",
|
||||
montserrat: "Montserrat",
|
||||
outfit: "Outfit",
|
||||
nunito: "Nunito",
|
||||
oswald: "Oswald",
|
||||
"league-gothic": "League Gothic",
|
||||
"archivo-black": "Archivo Black",
|
||||
"space-mono": "Space Mono",
|
||||
"ibm-plex-mono": "IBM Plex Mono",
|
||||
"jetbrains-mono": "JetBrains Mono",
|
||||
"eb-garamond": "EB Garamond",
|
||||
"playfair-display": "Playfair Display",
|
||||
"source-code-pro": "Source Code Pro",
|
||||
"noto-sans-jp": "Noto Sans JP",
|
||||
roboto: "Roboto",
|
||||
"open-sans": "Open Sans",
|
||||
lato: "Lato",
|
||||
poppins: "Poppins",
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a font alias to its canonical display name, or undefined if the
|
||||
* alias is not in the map.
|
||||
*/
|
||||
export function resolveAliasDisplayName(alias: string): string | undefined {
|
||||
const slug = (FONT_ALIAS_MAP as Record<string, string>)[alias.toLowerCase()];
|
||||
if (!slug) return undefined;
|
||||
return CANONICAL_FONT_DISPLAY_NAMES[slug];
|
||||
}
|
||||
// Moved to @hyperframes/parsers. Re-exported here for back-compat.
|
||||
export {
|
||||
FONT_ALIAS_MAP,
|
||||
FONT_ALIAS_KEYS,
|
||||
CANONICAL_FONT_DISPLAY_NAMES,
|
||||
resolveAliasDisplayName,
|
||||
} from "@hyperframes/parsers";
|
||||
|
||||
@@ -45,9 +45,9 @@ export type {
|
||||
ResolvedSlide,
|
||||
ResolvedSlideSequence,
|
||||
ResolvedSlideshow,
|
||||
} from "./slideshow/slideshow.types";
|
||||
} from "./slideshow/index.js";
|
||||
|
||||
export { parseSlideshowManifest, resolveSlideshow } from "./slideshow/parseSlideshow";
|
||||
export { parseSlideshowManifest, resolveSlideshow } from "./slideshow/index.js";
|
||||
|
||||
export {
|
||||
CANVAS_DIMENSIONS,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { stableClipId } from "./clipTree";
|
||||
import { swallow } from "./diagnostics";
|
||||
import { readElementPlaybackRate } from "./media";
|
||||
import { createRuntimeStartTimeResolver } from "./startResolver";
|
||||
import { isSceneLikeCompositionId } from "../slideshow/sceneId";
|
||||
import { isSceneLikeCompositionId } from "../slideshow/index.js";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./slideshow.types";
|
||||
export * from "./parseSlideshow";
|
||||
export { isSceneLikeCompositionId } from "./sceneId";
|
||||
// Moved to @hyperframes/parsers/slideshow. Re-exported here for back-compat.
|
||||
export * from "@hyperframes/parsers/slideshow";
|
||||
|
||||
@@ -1,11 +1,2 @@
|
||||
export function decodeUrlPathVariants(path: string): string[] {
|
||||
const variants = [path];
|
||||
try {
|
||||
const decoded = decodeURIComponent(path);
|
||||
if (decoded !== path) variants.unshift(decoded);
|
||||
} catch {
|
||||
// Malformed percent sequences may be literal filesystem names.
|
||||
}
|
||||
|
||||
return variants;
|
||||
}
|
||||
// Moved to @hyperframes/parsers. Re-exported here for back-compat.
|
||||
export { decodeUrlPathVariants } from "@hyperframes/parsers";
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"README.md"
|
||||
],
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
@@ -20,6 +21,12 @@
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
},
|
||||
"./browser": {
|
||||
"browser": "./src/browser.ts",
|
||||
"bun": "./src/browser.ts",
|
||||
"import": "./src/browser.ts",
|
||||
"types": "./src/browser.ts"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"publishConfig": {
|
||||
@@ -29,6 +36,10 @@
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./browser": {
|
||||
"import": "./dist/browser.js",
|
||||
"types": "./dist/browser.d.ts"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
@@ -42,7 +53,6 @@
|
||||
"prepublishOnly": "echo skip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
"postcss": "^8.5.8"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { lintHyperframeHtml, shouldBlockRender } from "./browser.js";
|
||||
|
||||
// Guards that @hyperframes/lint/browser exposes a working, node-free rule engine.
|
||||
// (The platform:"browser" tsup build is the compile-time node-free guarantee;
|
||||
// this verifies the API actually runs.)
|
||||
describe("@hyperframes/lint/browser", () => {
|
||||
it("lints an HTML string with no filesystem access", async () => {
|
||||
const html = `<html><body>
|
||||
<div data-composition-id="main" data-width="1920" data-height="1080"></div>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html, { filePath: "index.html" });
|
||||
expect(typeof result.ok).toBe("boolean");
|
||||
expect(Array.isArray(result.findings)).toBe(true);
|
||||
});
|
||||
|
||||
it("exposes the pure shouldBlockRender gate", () => {
|
||||
expect(shouldBlockRender(true, false, 1, 0)).toBe(true);
|
||||
expect(shouldBlockRender(true, false, 0, 3)).toBe(false);
|
||||
expect(shouldBlockRender(false, true, 0, 1)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Browser-safe entry for @hyperframes/lint.
|
||||
*
|
||||
* Exposes the composition rule engine — HTML-string in, findings out — with
|
||||
* **zero Node.js dependencies**: no `node:fs`, no filesystem, no server. This
|
||||
* lets browser-only editors and tools validate compositions entirely
|
||||
* client-side, before any network call.
|
||||
*
|
||||
* The Node-only project layer (`lintProject`, which walks a directory) is NOT
|
||||
* exported here — import it from the main `@hyperframes/lint` entry in Node.
|
||||
*/
|
||||
export type {
|
||||
HyperframeLintSeverity,
|
||||
HyperframeLintFinding,
|
||||
HyperframeLintResult,
|
||||
HyperframeLinterOptions,
|
||||
} from "./types.js";
|
||||
export { lintHyperframeHtml, lintMediaUrls } from "./hyperframeLinter.js";
|
||||
export { shouldBlockRender } from "./shouldBlockRender.js";
|
||||
@@ -1,6 +1,8 @@
|
||||
export { shouldBlockRender } from "./shouldBlockRender.js";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, extname, isAbsolute, join, posix, relative, resolve } from "node:path";
|
||||
import { decodeUrlPathVariants, rewriteAssetPath } from "@hyperframes/core";
|
||||
import { decodeUrlPathVariants } from "@hyperframes/parsers/composition";
|
||||
import { rewriteAssetPath } from "@hyperframes/parsers/asset-paths";
|
||||
import { lintHyperframeHtml } from "./hyperframeLinter.js";
|
||||
import type { HyperframeLintFinding, HyperframeLintResult } from "./types.js";
|
||||
|
||||
@@ -240,15 +242,6 @@ export async function lintProject(projectDir: string): Promise<ProjectLintResult
|
||||
return { results, totalErrors, totalWarnings, totalInfos };
|
||||
}
|
||||
|
||||
export function shouldBlockRender(
|
||||
strictErrors: boolean,
|
||||
strictAll: boolean,
|
||||
totalErrors: number,
|
||||
totalWarnings: number,
|
||||
): boolean {
|
||||
return (strictErrors && totalErrors > 0) || (strictAll && (totalErrors > 0 || totalWarnings > 0));
|
||||
}
|
||||
|
||||
function lintProjectAudioFiles(
|
||||
projectDir: string,
|
||||
htmlSources: HtmlSource[],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LintContext, HyperframeLintFinding, ExtractedBlock } from "../context";
|
||||
import { findHtmlTag, readAttr, readJsonAttr, stripJsComments, truncateSnippet } from "../utils";
|
||||
import { COMPOSITION_VARIABLE_TYPES } from "@hyperframes/parsers";
|
||||
import { COMPOSITION_VARIABLE_TYPES } from "@hyperframes/parsers/composition";
|
||||
|
||||
// Agent guidance thresholds: warning-only nudges for files/tracks that become hard
|
||||
// to inspect and revise reliably in a single composition.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FONT_ALIAS_KEYS, resolveAliasDisplayName } from "@hyperframes/core/fonts/aliases";
|
||||
import { FONT_ALIAS_KEYS, resolveAliasDisplayName } from "@hyperframes/parsers/composition";
|
||||
import type { LintContext, HyperframeLintFinding } from "../context";
|
||||
import { isRegistrySourceFile, isRegistryInstalledFile } from "./composition";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
parseSlideshowManifest,
|
||||
resolveSlideshow,
|
||||
isSceneLikeCompositionId,
|
||||
} from "@hyperframes/core/slideshow";
|
||||
} from "@hyperframes/parsers/slideshow";
|
||||
|
||||
type Scene = { id: string; start: number; duration: number };
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Pure render-gate decision — no Node.js dependencies, so it is safe to import
|
||||
* from the browser entry alongside the rule engine.
|
||||
*/
|
||||
export function shouldBlockRender(
|
||||
strictErrors: boolean,
|
||||
strictAll: boolean,
|
||||
totalErrors: number,
|
||||
totalWarnings: number,
|
||||
): boolean {
|
||||
return (strictErrors && totalErrors > 0) || (strictAll && (totalErrors > 0 || totalWarnings > 0));
|
||||
}
|
||||
@@ -1,14 +1,31 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: { index: "src/index.ts" },
|
||||
format: ["esm"],
|
||||
outDir: "dist",
|
||||
target: "node22",
|
||||
platform: "node",
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
dts: true,
|
||||
});
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: { index: "src/index.ts" },
|
||||
format: ["esm"],
|
||||
outDir: "dist",
|
||||
target: "node22",
|
||||
platform: "node",
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
dts: true,
|
||||
},
|
||||
{
|
||||
// Browser-safe subset. platform: "browser" makes the build FAIL if any
|
||||
// node:* builtin sneaks into the rule engine — a compile-time guarantee
|
||||
// that @hyperframes/lint/browser stays client-side runnable.
|
||||
entry: { browser: "src/browser.ts" },
|
||||
format: ["esm"],
|
||||
outDir: "dist",
|
||||
target: "es2022",
|
||||
platform: "browser",
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
clean: false,
|
||||
dts: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"README.md"
|
||||
],
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
@@ -62,6 +63,24 @@
|
||||
"node": "./dist/gsapParser.js",
|
||||
"import": "./src/gsapParser.ts",
|
||||
"types": "./src/gsapParser.ts"
|
||||
},
|
||||
"./slideshow": {
|
||||
"bun": "./src/slideshow/index.ts",
|
||||
"node": "./dist/slideshow.js",
|
||||
"import": "./src/slideshow/index.ts",
|
||||
"types": "./src/slideshow/index.ts"
|
||||
},
|
||||
"./asset-paths": {
|
||||
"bun": "./src/assets.ts",
|
||||
"node": "./dist/assets.js",
|
||||
"import": "./src/assets.ts",
|
||||
"types": "./src/assets.ts"
|
||||
},
|
||||
"./composition": {
|
||||
"bun": "./src/composition.ts",
|
||||
"node": "./dist/composition.js",
|
||||
"import": "./src/composition.ts",
|
||||
"types": "./src/composition.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
@@ -99,6 +118,18 @@
|
||||
"./gsap-parser-recast": {
|
||||
"import": "./dist/gsapParser.js",
|
||||
"types": "./dist/gsapParser.d.ts"
|
||||
},
|
||||
"./slideshow": {
|
||||
"import": "./dist/slideshow.js",
|
||||
"types": "./dist/slideshow.d.ts"
|
||||
},
|
||||
"./asset-paths": {
|
||||
"import": "./dist/assets.js",
|
||||
"types": "./dist/assets.d.ts"
|
||||
},
|
||||
"./composition": {
|
||||
"import": "./dist/composition.js",
|
||||
"types": "./dist/composition.d.ts"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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. The URL group is anchored to non-whitespace at both ends so the
|
||||
* surrounding `\s*` can never overlap it (avoids polynomial-ReDoS backtracking);
|
||||
* the captured value is whitespace-bounded already, matching the old behavior
|
||||
* after callers `.trim()` it.
|
||||
*/
|
||||
export const CSS_URL_RE = /\burl\(\s*(["']?)([^)"'\s](?:[^)"']*[^)"'\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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Node-only asset-path utilities (use node:path). Behind the
|
||||
// @hyperframes/parsers/asset-paths subpath so the main entry stays browser-safe.
|
||||
export * from "./assetPaths.js";
|
||||
export * from "./rewriteSubCompPaths.js";
|
||||
@@ -0,0 +1,12 @@
|
||||
// Pure, browser-safe composition primitives (data types, font aliases, URL
|
||||
// helper). Recast/linkedom-free, so browser consumers (e.g. the lint rule
|
||||
// engine via @hyperframes/lint/browser) can import these without pulling the
|
||||
// GSAP/HTML parser machinery from the main entry.
|
||||
export * from "./types.js";
|
||||
export {
|
||||
FONT_ALIAS_MAP,
|
||||
FONT_ALIAS_KEYS,
|
||||
CANONICAL_FONT_DISPLAY_NAMES,
|
||||
resolveAliasDisplayName,
|
||||
} from "./fontAliases.js";
|
||||
export { decodeUrlPathVariants } from "./utils/urlPath.js";
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Single source of truth for the deterministic font alias map. Both the
|
||||
* producer's @font-face injector and the core lint rules import from here,
|
||||
* eliminating manual drift between the two.
|
||||
*
|
||||
* Keys are lowercase font family names. Values are canonical font slugs
|
||||
* matching CANONICAL_FONTS keys in the producer's deterministicFonts module.
|
||||
*/
|
||||
export const FONT_ALIAS_MAP = {
|
||||
// ── Canonical bundled fonts (self-referencing) ────────────────────────
|
||||
inter: "inter",
|
||||
montserrat: "montserrat",
|
||||
outfit: "outfit",
|
||||
nunito: "nunito",
|
||||
oswald: "oswald",
|
||||
"league gothic": "league-gothic",
|
||||
"archivo black": "archivo-black",
|
||||
"space mono": "space-mono",
|
||||
"ibm plex mono": "ibm-plex-mono",
|
||||
"jetbrains mono": "jetbrains-mono",
|
||||
"eb garamond": "eb-garamond",
|
||||
"playfair display": "playfair-display",
|
||||
"source code pro": "source-code-pro",
|
||||
"noto sans jp": "noto-sans-jp",
|
||||
roboto: "roboto",
|
||||
"open sans": "open-sans",
|
||||
lato: "lato",
|
||||
poppins: "poppins",
|
||||
|
||||
// ── Common aliases → nearest canonical ────────────────────────────────
|
||||
"helvetica neue": "inter",
|
||||
helvetica: "inter",
|
||||
arial: "inter",
|
||||
"helvetica bold": "inter",
|
||||
futura: "montserrat",
|
||||
"din alternate": "montserrat",
|
||||
"arial black": "montserrat",
|
||||
"bebas neue": "league-gothic",
|
||||
"courier new": "jetbrains-mono",
|
||||
courier: "jetbrains-mono",
|
||||
garamond: "eb-garamond",
|
||||
"noto sans japanese": "noto-sans-jp",
|
||||
"segoe ui": "roboto",
|
||||
|
||||
// ── macOS sans-serif system fonts → inter ─────────────────────────────
|
||||
"sf pro": "inter",
|
||||
"sf pro display": "inter",
|
||||
"sf pro text": "inter",
|
||||
"sf pro rounded": "inter",
|
||||
avenir: "inter",
|
||||
"avenir next": "inter",
|
||||
"lucida grande": "inter",
|
||||
geneva: "inter",
|
||||
optima: "inter",
|
||||
|
||||
// ── Windows sans-serif system fonts → inter ───────────────────────────
|
||||
verdana: "inter",
|
||||
tahoma: "inter",
|
||||
"trebuchet ms": "inter",
|
||||
calibri: "inter",
|
||||
candara: "inter",
|
||||
corbel: "inter",
|
||||
"lucida sans": "inter",
|
||||
"lucida sans unicode": "inter",
|
||||
|
||||
// ── Linux sans-serif system fonts → inter ─────────────────────────────
|
||||
"noto sans": "inter",
|
||||
"dejavu sans": "inter",
|
||||
"liberation sans": "inter",
|
||||
|
||||
// ── Monospace system fonts → jetbrains-mono ───────────────────────────
|
||||
"sf mono": "jetbrains-mono",
|
||||
menlo: "jetbrains-mono",
|
||||
monaco: "jetbrains-mono",
|
||||
consolas: "jetbrains-mono",
|
||||
"lucida console": "jetbrains-mono",
|
||||
"lucida sans typewriter": "jetbrains-mono",
|
||||
"andale mono": "jetbrains-mono",
|
||||
"dejavu sans mono": "jetbrains-mono",
|
||||
"liberation mono": "jetbrains-mono",
|
||||
|
||||
// ── Serif system fonts → eb-garamond ──────────────────────────────────
|
||||
georgia: "eb-garamond",
|
||||
palatino: "eb-garamond",
|
||||
"palatino linotype": "eb-garamond",
|
||||
"book antiqua": "eb-garamond",
|
||||
cambria: "eb-garamond",
|
||||
times: "eb-garamond",
|
||||
"times new roman": "eb-garamond",
|
||||
"dejavu serif": "eb-garamond",
|
||||
"liberation serif": "eb-garamond",
|
||||
} satisfies Readonly<Record<string, string>>;
|
||||
|
||||
export const FONT_ALIAS_KEYS: ReadonlySet<string> = new Set(Object.keys(FONT_ALIAS_MAP));
|
||||
|
||||
/**
|
||||
* Human-readable display names for canonical font slugs. Used by the lint
|
||||
* rule to tell authors what their aliased font will render as.
|
||||
*/
|
||||
export const CANONICAL_FONT_DISPLAY_NAMES: Readonly<Record<string, string>> = {
|
||||
inter: "Inter",
|
||||
montserrat: "Montserrat",
|
||||
outfit: "Outfit",
|
||||
nunito: "Nunito",
|
||||
oswald: "Oswald",
|
||||
"league-gothic": "League Gothic",
|
||||
"archivo-black": "Archivo Black",
|
||||
"space-mono": "Space Mono",
|
||||
"ibm-plex-mono": "IBM Plex Mono",
|
||||
"jetbrains-mono": "JetBrains Mono",
|
||||
"eb-garamond": "EB Garamond",
|
||||
"playfair-display": "Playfair Display",
|
||||
"source-code-pro": "Source Code Pro",
|
||||
"noto-sans-jp": "Noto Sans JP",
|
||||
roboto: "Roboto",
|
||||
"open-sans": "Open Sans",
|
||||
lato: "Lato",
|
||||
poppins: "Poppins",
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a font alias to its canonical display name, or undefined if the
|
||||
* alias is not in the map.
|
||||
*/
|
||||
export function resolveAliasDisplayName(alias: string): string | undefined {
|
||||
const slug = (FONT_ALIAS_MAP as Record<string, string>)[alias.toLowerCase()];
|
||||
if (!slug) return undefined;
|
||||
return CANONICAL_FONT_DISPLAY_NAMES[slug];
|
||||
}
|
||||
@@ -4,3 +4,15 @@ export * from "./htmlParser.js";
|
||||
export * from "./hfIds.js";
|
||||
export { unrollComputedTimeline } from "./gsapUnroll.js";
|
||||
export { queryByAttr } from "./utils/cssSelector.js";
|
||||
|
||||
// Pure, browser-safe composition primitives shared by the linter (so it can
|
||||
// consume them without depending on @hyperframes/core). The Node-only asset
|
||||
// path helpers live behind the ./asset-paths subpath to keep this entry
|
||||
// browser-safe.
|
||||
export { decodeUrlPathVariants } from "./utils/urlPath.js";
|
||||
export {
|
||||
FONT_ALIAS_MAP,
|
||||
FONT_ALIAS_KEYS,
|
||||
CANONICAL_FONT_DISPLAY_NAMES,
|
||||
resolveAliasDisplayName,
|
||||
} from "./fontAliases.js";
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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 || ""})`;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./slideshow.types.js";
|
||||
export * from "./parseSlideshow.js";
|
||||
export { isSceneLikeCompositionId } from "./sceneId.js";
|
||||
@@ -0,0 +1,11 @@
|
||||
export function decodeUrlPathVariants(path: string): string[] {
|
||||
const variants = [path];
|
||||
try {
|
||||
const decoded = decodeURIComponent(path);
|
||||
if (decoded !== path) variants.unshift(decoded);
|
||||
} catch {
|
||||
// Malformed percent sequences may be literal filesystem names.
|
||||
}
|
||||
|
||||
return variants;
|
||||
}
|
||||
@@ -10,6 +10,9 @@ export default defineConfig({
|
||||
springEase: "src/springEase.ts",
|
||||
hfIds: "src/hfIds.ts",
|
||||
gsapParser: "src/gsapParser.ts",
|
||||
slideshow: "src/slideshow/index.ts",
|
||||
assets: "src/assets.ts",
|
||||
composition: "src/composition.ts",
|
||||
},
|
||||
format: ["esm"],
|
||||
outDir: "dist",
|
||||
|
||||
Reference in New Issue
Block a user