Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
27 changes: 12 additions & 15 deletions packages/melonjs/src/loader/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,12 @@ export function load(asset, onload, onerror) {
initParsers();
}

const parser = parsers.get(asset.type);

if (typeof parser === "undefined") {
throw new Error("load : unknown or invalid resource type : " + asset.type);
}

// Resolve the effective src WITHOUT mutating the caller's asset
// descriptor: load() used to write the transformed url back into
// asset.src, so retrying the same object — loader.reload() after a
Expand All @@ -531,33 +537,24 @@ export function load(asset, onload, onerror) {
// caller's original src; only the parser sees the resolved one.
let src = asset.src;

// strip url() wrapper for fontface assets so baseURL can be prepended to the raw path
if (asset.type === "fontface" && typeof src === "string") {
const urlMatch = src.match(/^url\(\s*['"]?(.*?)['"]?\s*\)$/);
if (urlMatch) {
src = urlMatch[1];
}
// Let the parser normalize its source before applying the shared base URL.
if (typeof src === "string") {
src = parser.resolveSrc?.(src) ?? src;
}

// transform the url if necessary (skip for local() font sources and data URIs)
// Data URIs and parser-specific sources do not need a base URL.
if (
typeof baseURL[asset.type] !== "undefined" &&
typeof src === "string" &&
!src.startsWith("local(") &&
!src.startsWith("data:")
!src.startsWith("data:") &&
!parser.skipBaseURL?.(src)
) {
src = baseURL[asset.type] + src;
}

const resource =
src === asset.src ? asset : Object.assign({}, asset, { src });

const parser = parsers.get(asset.type);

if (typeof parser === "undefined") {
throw new Error("load : unknown or invalid resource type : " + asset.type);
}

const settings = {
nocache: nocache,
crossOrigin: crossOrigin,
Expand Down
23 changes: 23 additions & 0 deletions packages/melonjs/src/loader/parsers/fontface.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,26 @@ export function preloadFontFace(data, onload, onerror) {

return 1;
}

/**
* Unwrap a CSS URL before the loader prefixes the font's base URL.
* @param {string} src - font source descriptor
* @returns {string} source path or unchanged descriptor
* @ignore
* @internal
*/
preloadFontFace.resolveSrc = (src) => {
const urlMatch = src.match(/^url\(\s*['"]?(.*?)['"]?\s*\)$/);
return urlMatch ? urlMatch[1] : src;
};

/**
* Installed font names are not paths relative to the asset base URL.
* @param {string} src - font source descriptor
* @returns {boolean} whether the base URL should be skipped
* @ignore
* @internal
*/
preloadFontFace.skipBaseURL = (src) => {
return src.startsWith("local(");
};
47 changes: 47 additions & 0 deletions packages/melonjs/tests/loader-src-resolution.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { boot, loader } from "../src/index.js";
import { preloadFontFace } from "../src/loader/parsers/fontface.js";

describe("parser-owned source resolution", () => {
const type = "font-source-probe";
let received;

beforeAll(() => {
boot();
const parser = (asset, onload) => {
received = asset.src;
onload();
return 1;
};
// Use the font parser's rules under another type, so resolution cannot
// depend on the loader recognizing the name "fontface".
Object.assign(parser, {
resolveSrc: preloadFontFace.resolveSrc,
skipBaseURL: preloadFontFace.skipBaseURL,
});
loader.setParser(type, parser);
loader.setBaseURL(type, "fonts/");
});

afterAll(() => {
loader.setBaseURL(type, "");
});

it.each([
["test.woff2", "fonts/test.woff2"],
["url(test.woff2)", "fonts/test.woff2"],
["url('test.woff2')", "fonts/test.woff2"],
['url( "Test Font.woff2" )', "fonts/Test Font.woff2"],
["local('Test Font')", "local('Test Font')"],
["data:font/woff2;base64,AA==", "data:font/woff2;base64,AA=="],
["url('data:font/woff2;base64,AA==')", "data:font/woff2;base64,AA=="],
])("resolves %s without changing the manifest", async (src, expected) => {
const asset = Object.freeze({ name: "source-probe", type, src });
await loader.load(asset);
expect(received).toBe(expected);
expect(asset.src).toBe(src);
// Retrying the same entry must not prepend the base URL twice.
await loader.load(asset);
expect(received).toBe(expected);
});
});
15 changes: 10 additions & 5 deletions packages/melonjs/tests/loader.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { beforeAll, describe, expect, it } from "vitest";
import { audio, boot, event, loader } from "../src/index.js";
import { fontList, videoList } from "../src/loader/cache.js";
import { preloadFontFace } from "../src/loader/parsers/fontface.js";

describe("loader", () => {
let audioURI;
Expand Down Expand Up @@ -275,14 +276,15 @@ describe("loader", () => {
loader.setBaseURL("fontface", "assets/");
const receivedSrc = [];

// stub fontface parser to capture the resolved src
loader.setParser("fontface", (data, onload) => {
// Capture the resolved src while retaining the font parser's rules.
const parser = (data, onload) => {
receivedSrc.push(data.src);
if (typeof onload === "function") {
onload();
}
return 1;
});
};
loader.setParser("fontface", Object.assign(parser, preloadFontFace));

// plain path
loader.load(
Expand All @@ -307,27 +309,30 @@ describe("loader", () => {

// reset
loader.setBaseURL("fontface", "./");
loader.setParser("fontface", preloadFontFace);
});

it("should not strip local() wrapper from fontface src", () => {
// reset baseURL so it doesn't interfere
loader.setBaseURL("fontface", "./");
const receivedSrc = [];

loader.setParser("fontface", (data, onload) => {
const parser = (data, onload) => {
receivedSrc.push(data.src);
if (typeof onload === "function") {
onload();
}
return 1;
});
};
loader.setParser("fontface", Object.assign(parser, preloadFontFace));

loader.load(
{ name: "font4", type: "fontface", src: "local('My Font')" },
() => {},
);

expect(receivedSrc[0]).toBe("local('My Font')");
loader.setParser("fontface", preloadFontFace);
});

it("should configure loader options", () => {
Expand Down