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
46 changes: 44 additions & 2 deletions packages/config-api/src/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,49 @@
import { describe, it, expect, vi } from "vitest";
import { openUrl } from "./index";
import { describe, it, expect, vi, afterEach } from "vitest";
import { getConfiguration, openUrl } from "./index";
import { Config, ProcessInfo } from "./configSchema";

describe("getConfiguration", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

const loadConfig = (config: object) => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
vi.stubGlobal("self", { finickyConfig: { default: config } });
getConfiguration("finickyConfig");
return warn.mock.calls.map(([message]) => message);
};

it("warns about options set at the top level", () => {
const warnings = loadConfig({ defaultBrowser: "Safari", hideIcon: true });
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('Ignoring "hideIcon" at the top level');
});

it("warns about unknown properties and options", () => {
const warnings = loadConfig({
defaultBrowser: "Safari",
handler: [],
options: { checkForUpdate: false },
});
expect(warnings).toEqual([
'Ignoring unknown configuration property "handler".',
'Ignoring unknown option "options.checkForUpdate".',
]);
});

it("doesn't warn about a valid configuration", () => {
const warnings = loadConfig({
defaultBrowser: "Safari",
options: { hideIcon: true, keepRunning: true },
rewrite: [],
handlers: [],
});
expect(warnings).toEqual([]);
});
});

describe("openUrl", () => {
const mockProcessInfo: ProcessInfo = {
name: "TestApp",
Expand Down
2 changes: 1 addition & 1 deletion packages/config-api/src/configSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const HandlerRuleSchema = z
.identifier("BrowserHandler");

// ===== Configuration Schemas =====
const ConfigOptionsSchema = z
export const ConfigOptionsSchema = z
.object({
urlShorteners: z.array(z.string()).optional(),
logRequests: z.boolean().optional().describe("Log to file on disk"),
Expand Down
35 changes: 35 additions & 0 deletions packages/config-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "core-js/features/url";
import "core-js/features/url-search-params";
import {
ConfigSchema,
ConfigOptionsSchema,
Config,
OpenUrlOptions,
ProcessInfo,
Expand Down Expand Up @@ -44,11 +45,13 @@ export function getConfiguration(namespace: string): Config {
const namespaceObj = (self as any)[namespace];
if (namespaceObj) {
if (namespaceObj.default) {
warnAboutUnknownKeys(namespaceObj.default);
return namespaceObj.default;
} else {
console.warn(
"No default export found for configuration namespace, using legacy configuration. Please update your configuration to use `export default { ... }` instead of `module.exports = { ... }` syntax. https://github.com/johnste/finicky/wiki/Use-Modern-ECMAScript-Module-Syntax"
);
warnAboutUnknownKeys(namespaceObj);
return namespaceObj;
}
}
Expand All @@ -58,6 +61,38 @@ export function getConfiguration(namespace: string): Config {
);
}

// Zod strips unknown keys silently, so a misplaced option (like a top-level
// `hideIcon`) would otherwise just be ignored.
function warnAboutUnknownKeys(config: unknown) {
if (!config || typeof config !== "object") {
return;
}

const configKeys = new Set(Object.keys(ConfigSchema.shape));
// `.identifier()` types the options schema as a plain ZodType, so read its
// keys from the definition instead of `.shape`
const optionKeys = new Set(Object.keys(ConfigOptionsSchema._def.shape()));

for (const key of Object.keys(config)) {
if (optionKeys.has(key)) {
console.warn(
`Ignoring "${key}" at the top level of the configuration, it needs to be inside \`options\`. https://github.com/johnste/finicky/wiki/Configuration-(v4)#general-options`
);
} else if (!configKeys.has(key)) {
console.warn(`Ignoring unknown configuration property "${key}".`);
}
}

const options = "options" in config ? config.options : undefined;
if (options && typeof options === "object") {
for (const key of Object.keys(options)) {
if (!optionKeys.has(key)) {
console.warn(`Ignoring unknown option "options.${key}".`);
}
}
}
}

export function getOption<T, K extends keyof Config["options"]>(
option: keyof Config["options"],
config: Config,
Expand Down