fix(cli): localize external assets in publish archive (#1160)

Compositions referencing assets outside the project directory (via ../
paths) produced broken published projects — those files were never
included in the ZIP archive.

localizeExternalAssets() now scans all HTML and CSS files in the archive
for src, href, and url() references that resolve outside the project
dir. For each, it copies the file into the archive under _ext/ and
rewrites the reference to point there.

Handles: src/href attributes, <style> url(), inline style url(),
standalone CSS url(), sub-composition HTML files, deduplication of
the same asset referenced from multiple files.

Shared primitives (CSS_URL_RE, isNonRelativeUrl, isPathInside) extracted
into core/compiler/assetPaths.ts — single source of truth across core,
producer, and CLI.
This commit is contained in:
Miguel Ángel
2026-06-01 21:06:35 -04:00
committed by GitHub
parent 598e3e957a
commit a4706da513
7 changed files with 486 additions and 33 deletions
+39
View File
@@ -0,0 +1,39 @@
/**
* 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);
}
+3
View File
@@ -46,3 +46,6 @@ export {
type InlineSubCompositionsOptions,
type InlineSubCompositionsResult,
} from "./inlineSubCompositions";
// Asset-path primitives (shared across core, producer, CLI)
export { CSS_URL_RE, PATH_ATTRS, isNonRelativeUrl, isPathInside } from "./assetPaths";
@@ -18,21 +18,9 @@
import { posix } from "path";
const { join, resolve, dirname } = posix;
/** Attributes that may contain relative asset paths. */
const PATH_ATTRS = ["src", "href"] as const;
const CSS_URL_RE = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
import { CSS_URL_RE, PATH_ATTRS, isNonRelativeUrl } from "./assetPaths.js";
/** Protocols and prefixes that should never be rewritten. */
function isAbsoluteOrSpecial(val: string): boolean {
return (
!val ||
val.startsWith("http://") ||
val.startsWith("https://") ||
val.startsWith("//") ||
val.startsWith("data:") ||
val.startsWith("#")
);
}
const isAbsoluteOrSpecial = isNonRelativeUrl;
/**
* Returns true only for paths that traverse up with `../`.
+1
View File
@@ -134,6 +134,7 @@ export {
rewriteAssetPath,
rewriteCssAssetUrls,
} from "./compiler/rewriteSubCompPaths";
export { CSS_URL_RE, isNonRelativeUrl, isPathInside } from "./compiler/assetPaths";
export { decodeUrlPathVariants } from "./utils/urlPath";
// Inline scripts