Skip to content

Commit

Permalink
refactor: replace cosmicconfig with lilconfig
Browse files Browse the repository at this point in the history
  • Loading branch information
tylerbutler committed Nov 30, 2024
1 parent 10f25a1 commit 101512a
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 29 deletions.
3 changes: 2 additions & 1 deletion packages/cli-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@
"dependencies": {
"@oclif/core": "^4.0.19",
"chalk": "^5.3.0",
"cosmiconfig": "^9.0.0",
"debug": "^4.3.4",
"detect-indent": "^7.0.1",
"fs-extra": "^11.2.0",
"jiti": "^2.4.0",
"lilconfig": "^3.1.2",
"simple-git": "^3.24.0",
"strip-ansi": "^7.1.0"
},
Expand Down
86 changes: 58 additions & 28 deletions packages/cli-api/src/configCommand.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import assert from "node:assert/strict";
import { stat } from "node:fs/promises";
import type { Command } from "@oclif/core";
import { type CosmiconfigResult, cosmiconfig } from "cosmiconfig";
import { createJiti } from "jiti";
import {
type LilconfigResult,
type Loader,
type LoaderResult,
lilconfig,
} from "lilconfig";
import { BaseCommand } from "./baseCommand.js";
import { ConfigFileFlagHidden } from "./flags.js";
import { findGitRoot } from "./git.js";

// barebones ts-loader
const jiti = createJiti(import.meta.url);
const tsLoader: Loader = async (
filepath: string,
_content: string,
): Promise<LoaderResult> => {
const modDefault = await jiti.import(filepath, { default: true });
return modDefault;
};

/**
* A base command that loads typed configuration values from a config file.
*
Expand All @@ -28,14 +45,10 @@ export abstract class CommandWithConfig<
...BaseCommand.flags,
} as const;

public override async init(): Promise<void> {
await super.init();
const { config } = this.flags;
const loaded = await this.loadConfig(config);
if (loaded === undefined) {
this.error(`Failure to load config: ${config}`, { exit: 1 });
}
}
/**
* A default config value to use if none is found. If this returns undefined, no default value will be used.
*/
protected defaultConfig: C | undefined;

protected async loadConfig(
filePath?: string,
Expand All @@ -45,41 +58,47 @@ export abstract class CommandWithConfig<
const configPath = filePath ?? process.cwd();
const moduleName = this.config.bin;
const repoRoot = await findGitRoot();
const explorer = cosmiconfig(moduleName, {
searchStrategy: "global",
const configLoader = lilconfig(moduleName, {
stopDir: repoRoot,
searchPlaces: [
`${moduleName}.config.ts`,
`${moduleName}.config.mjs`,
`${moduleName}.config.cjs`,
`${moduleName}.config.js`,
"package.json",
],
loaders: {
".ts": tsLoader,
},
});
const pathStats = await stat(configPath);
this.verbose(
`Looking for '${this.config.bin}' config at '${configPath}'`,
);
let config: CosmiconfigResult;
let maybeConfig: LilconfigResult;
if (pathStats.isDirectory()) {
config = await explorer.search(configPath);
maybeConfig = await configLoader.search(configPath);
} else {
config = await explorer.load(configPath);
maybeConfig = await configLoader.load(configPath);
}
if (config?.config !== undefined) {
this.verbose(`Found config at ${config.filepath}`);
if (maybeConfig?.config !== undefined) {
this.verbose(`Found config at ${maybeConfig?.filepath}`);
} else {
this.verbose(`No config found; started searching at ${configPath}`);
}
this._commandConfig = config?.config as C;
this._commandConfig = maybeConfig?.config ?? this.defaultConfig;
}
return this._commandConfig;
}

// protected abstract get defaultConfig(): C | undefined;

// protected get commandConfig(): C {
// // TODO: There has to be a better pattern for this.
// assert(
// this._commandConfig !== undefined,
// "commandConfig is undefined; this may happen if loadConfig is not called prior to accessing commandConfig. loadConfig is called from init() - check that code path is called.",
// );
// // this._commandConfig ??= this.loadConfig();
// return this._commandConfig;
// }
protected get commandConfig(): C {
// TODO: There has to be a better pattern for this.
assert(
this._commandConfig !== undefined,
"commandConfig is undefined; this may happen if loadConfig is not called prior to accessing commandConfig. loadConfig is called from init() - check that code path is called.",
);
return this._commandConfig;
}
}

/**
Expand All @@ -89,10 +108,21 @@ export abstract class CommandWithConfig<
*
* @privateRemarks
* This class may be an unneeded wrapper around BaseCommand. There's no clear beenfit to using this vs. BaseCommand directly.
*
* @deprecated Use the BaseCommand directly.
*/
export abstract class CommandWithoutConfig<
T extends typeof Command & {
args: typeof CommandWithoutConfig.args;
flags: typeof CommandWithoutConfig.flags;
},
> extends BaseCommand<T> {}

/**
* An interface implemented by commands that use a context object.
*
* @beta
*/
export interface CommandWithContext<CONTEXT> {
getContext(): Promise<CONTEXT>;
}

0 comments on commit 101512a

Please sign in to comment.