Skip to content
Merged
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
15 changes: 14 additions & 1 deletion js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,20 @@ const MM = (function () {
const loadConfig = async function () {
try {
const res = await fetch(new URL("config/", `${location.origin}${config.basePath}`));
config = JSON.parse(await res.text());

// The server tags functions as { __mmFunction: "<source>" } because
// JSON.stringify can't serialise live functions. This reviver turns
// those tagged objects back into callable functions.
config = JSON.parse(await res.text(), (key, value) => {
if (value && typeof value === "object" && typeof value.__mmFunction === "string") {
try {
return new Function(`return (${value.__mmFunction})`)();
} catch {
Log.warn(`Failed to revive function for config key "${key}".`);
}
}
return value;
});
} catch (error) {
Log.error("Unable to retrieve config", error);
}
Expand Down
19 changes: 14 additions & 5 deletions js/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,21 @@ function Server (configObj) {
const getStartup = (req, res) => res.send(startUp);

const getConfig = (req, res) => {
if (config.hideConfigSecrets) {
res.send(configObj.redactedConf);
} else {
res.send(configObj.fullConf);
}
const obj = config.hideConfigSecrets ? configObj.redactedConf : configObj.fullConf;
// Functions can't survive JSON.stringify, so we wrap them in a
// tagged object { __mmFunction: "<source>" }. The client-side
// JSON reviver in main.js recognises this tag and reconstructs
// the live function from the source string.
const jsonString = JSON.stringify(obj, (key, value) => {
if (typeof value === "function") {
return { __mmFunction: value.toString() };
}
return value;
});
res.set("Content-Type", "application/json");
res.send(jsonString);
};

app.get("/config", (req, res) => getConfig(req, res));

app.get("/cors", async (req, res) => await cors(req, res));
Expand Down
35 changes: 35 additions & 0 deletions tests/configs/config_functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*eslint object-shorthand: ["error", "always", { "methodsIgnorePattern": "^roundToInt2$" }]*/

let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
modules: [
{
module: "clock",
position: "middle_center",
config: {
moduleFunctions: {
roundToInt1: (value) => {
try {
return Math.round(parseFloat(value));
} catch {
return value;
}
},
roundToInt2: function (value) {
try {
return Math.round(parseFloat(value));
} catch {
return value;
}
}
},
stringWithArrow: "a => b is not a function",
stringWithFunction: "this function keyword is just text"
}
}
]
});

/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}
21 changes: 21 additions & 0 deletions tests/e2e/config_functions_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const helpers = require("./helpers/global-setup");

describe("config with module function", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/config_functions.js");
});

afterAll(async () => {
await helpers.stopApplication();
});

it("config should resolve module functions", () => {
expect(config.modules[0].config.moduleFunctions.roundToInt1(13.3)).toBe(13);
expect(config.modules[0].config.moduleFunctions.roundToInt2(13.3)).toBe(13);
});

it("config should not revive plain strings containing arrow or function keywords", () => {
expect(config.modules[0].config.stringWithArrow).toBe("a => b is not a function");
expect(config.modules[0].config.stringWithFunction).toBe("this function keyword is just text");
});
});
Loading