release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+180 -2
View File
@@ -11,6 +11,29 @@ export interface ImageLoaderInput {
export type ImageLoader = (input: ImageLoaderInput) => string;
function safeSvgColor(value: string, name: string): string {
const color = value.trim();
if (color.length > 128) throw new TypeError(`Unsafe ${name} image placeholder color.`);
if (
/^#[0-9a-f]{3,8}$/i.test(color) ||
/^(?:rgb|hsl)a?\([0-9.,%\s/+-]+\)$/i.test(color) ||
/^var\(--[A-Za-z0-9_-]+\)$/.test(color) ||
/^[A-Za-z]+$/.test(color)
) {
return color;
}
throw new TypeError(`Unsafe ${name} image placeholder color.`);
}
function escapeHtmlAttribute(value: string): string {
return value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
export interface ImagePolicy {
remoteHosts?: string[];
allowedProtocols?: string[];
@@ -65,11 +88,14 @@ export interface ImageAuditIssue {
}
export const defaultImageLoader: ImageLoader = ({ src, width, quality, format }) => {
const separator = src.includes("?") ? "&" : "?";
const hashIndex = src.indexOf("#");
const source = hashIndex >= 0 ? src.slice(0, hashIndex) : src;
const hash = hashIndex >= 0 ? src.slice(hashIndex) : "";
const separator = source.includes("?") ? "&" : "?";
const params = new URLSearchParams({ w: String(width) });
if (quality !== undefined) params.set("q", String(quality));
if (format && format !== "original") params.set("format", format);
return `${src}${separator}${params.toString()}`;
return `${source}${separator}${params.toString()}${hash}`;
};
function validateSource(src: string, policy: ImagePolicy): void {
@@ -100,6 +126,10 @@ export function createResponsiveImage(options: ResponsiveImageOptions): Responsi
}
const maxWidth = options.maxWidth ?? 8_192;
const maxQuality = options.maxQuality ?? 100;
if (!Number.isFinite(maxWidth) || maxWidth <= 0)
throw new RangeError("Image maxWidth must be positive.");
if (!Number.isFinite(maxQuality) || maxQuality <= 0)
throw new RangeError("Image maxQuality must be positive.");
const quality = Math.min(maxQuality, Math.max(1, Math.round(options.quality ?? 80)));
const widths = [...new Set(options.widths ?? [options.width])]
.map((width) => Math.round(width))
@@ -186,3 +216,151 @@ export function auditImage(input: ImageAuditInput): ImageAuditIssue[] {
}
return issues;
}
export interface PictureSource {
type: string;
srcset: string;
sizes?: string;
}
export interface PicturePlan {
image: ResponsiveImageAttributes;
sources: PictureSource[];
}
export function normalizeImageWidths(
widths: readonly number[],
options: { min?: number; max?: number } = {},
): number[] {
const requestedMin = options.min ?? 16;
const requestedMax = options.max ?? 8_192;
if (!Number.isFinite(requestedMin) || !Number.isFinite(requestedMax)) {
throw new RangeError("Image width bounds must be finite.");
}
const min = Math.max(1, Math.round(requestedMin));
const max = Math.max(min, Math.round(requestedMax));
return [...new Set(widths.map((width) => Math.round(width)))]
.filter((width) => Number.isFinite(width) && width >= min && width <= max)
.sort((left, right) => left - right);
}
export function createCdnImageLoader(
baseUrl: string,
options: {
sourceParam?: string;
widthParam?: string;
qualityParam?: string;
formatParam?: string;
} = {},
): ImageLoader {
const base = new URL(baseUrl);
validateUrl(base, {
allowRelative: false,
allowedProtocols: ["https:"],
allowCredentials: false,
});
return ({ src, width, quality, format }) => {
const url = new URL(base);
url.searchParams.set(options.sourceParam ?? "src", src);
url.searchParams.set(options.widthParam ?? "w", String(width));
if (quality !== undefined) url.searchParams.set(options.qualityParam ?? "q", String(quality));
if (format && format !== "original")
url.searchParams.set(options.formatParam ?? "format", format);
return url.toString();
};
}
export function createPathImageLoader(prefix = "/__wrnexus/image"): ImageLoader {
validateUrl(prefix, {
base: "https://wrnexus.invalid",
allowRelative: true,
allowedProtocols: ["https:"],
allowCredentials: false,
});
return ({ src, width, quality, format }) => {
const path = `${prefix.replace(/\/$/, "")}/${encodeURIComponent(src)}`;
const query = new URLSearchParams({ w: String(width) });
if (quality !== undefined) query.set("q", String(quality));
if (format && format !== "original") query.set("format", format);
return `${path}?${query}`;
};
}
export function createPicture(
options: ResponsiveImageOptions & { formats?: ImageFormat[] },
): PicturePlan {
validateSource(options.src, options);
const formats: ImageFormat[] = [...new Set<ImageFormat>(options.formats ?? ["avif", "webp"])];
const widths = normalizeImageWidths(options.widths ?? [options.width], { max: options.maxWidth });
const loader = options.loader ?? defaultImageLoader;
const quality = Math.min(
options.maxQuality ?? 100,
Math.max(1, Math.round(options.quality ?? 80)),
);
const sources = formats
.filter((format) => format !== "original")
.map((format) => ({
type: `image/${format === "jpeg" ? "jpeg" : format}`,
srcset: widths
.map((width) => `${loader({ src: options.src, width, quality, format })} ${width}w`)
.join(", "),
...(options.sizes ? { sizes: options.sizes } : {}),
}));
return {
image: createResponsiveImage({ ...options, widths, format: options.format ?? "original" }),
sources,
};
}
export function createBlurPlaceholder(
options: { width?: number; height?: number; color?: string; accent?: string } = {},
): string {
const requestedWidth = options.width ?? 16;
const requestedHeight = options.height ?? 9;
if (!Number.isFinite(requestedWidth) || !Number.isFinite(requestedHeight)) {
throw new RangeError("Image placeholder dimensions must be finite.");
}
const width = Math.max(1, Math.min(512, Math.round(requestedWidth)));
const height = Math.max(1, Math.min(512, Math.round(requestedHeight)));
const color = safeSvgColor(options.color ?? "#e2e8f0", "primary");
const accent = safeSvgColor(options.accent ?? "#cbd5e1", "accent");
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><defs><linearGradient id="g"><stop stop-color="${color}"/><stop offset="1" stop-color="${accent}"/></linearGradient></defs><rect width="100%" height="100%" fill="url(#g)"/></svg>`;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
export function imagePreload(
image: ResponsiveImageAttributes,
options: { as?: string; type?: string; crossOrigin?: "anonymous" | "use-credentials" } = {},
): string {
const attrs = [
'rel="preload"',
`as="${options.as ?? "image"}"`,
`href="${escapeHtmlAttribute(image.src)}"`,
];
if (image.srcset) attrs.push(`imagesrcset="${escapeHtmlAttribute(image.srcset)}"`);
if (image.sizes) attrs.push(`imagesizes="${escapeHtmlAttribute(image.sizes)}"`);
if (options.type) attrs.push(`type="${escapeHtmlAttribute(options.type)}"`);
if (options.crossOrigin) attrs.push(`crossorigin="${options.crossOrigin}"`);
return `<link ${attrs.join(" ")} />`;
}
export function imageCacheKey(input: ImageLoaderInput): string {
const source = `${input.src}|${Math.round(input.width)}|${input.quality ?? ""}|${input.format ?? "original"}`;
let hash = 0x811c9dc5;
for (let index = 0; index < source.length; index++) {
hash ^= source.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return `img-${(hash >>> 0).toString(36)}`;
}
export { imagePlugin, imageComponentsDir } from "./plugin.ts";
export type { ImagePluginOptions } from "./plugin.ts";
export { optimizeImage } from "./optimize.ts";
export type {
ImageProcessor,
ImageProcessorResult,
OptimizeImageOptions,
OptimizedImageManifest,
OptimizedImageVariant,
} from "./optimize.ts";