Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: replace cosmicconfig with lilconfig #157

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
101512a
refactor: replace cosmicconfig with lilconfig
tylerbutler Nov 30, 2024
e4b4df2
lockfile
tylerbutler Nov 30, 2024
32c7cd5
build
tylerbutler Nov 30, 2024
512d752
Revert "refactor: replace cosmicconfig with lilconfig"
tylerbutler Nov 30, 2024
24db30a
lockfile
tylerbutler Nov 30, 2024
2040f8e
Merge branch 'main' into lilconfig
tylerbutler Nov 30, 2024
bcece3b
Merge branch 'main' into lilconfig
tylerbutler Jan 16, 2025
8efbdd8
refactor: replace cosmicconfig with lilconfig
tylerbutler Nov 30, 2024
7d4428c
lockfile
tylerbutler Jan 16, 2025
d877cc7
wip
tylerbutler Jan 23, 2025
c276e61
Merge branch 'main' into lilconfig
tylerbutler Jan 23, 2025
de0f1ce
tests
tylerbutler Jan 25, 2025
a15307f
missing dep
tylerbutler Jan 25, 2025
bb36548
updates
tylerbutler Jan 26, 2025
e660da1
deps
tylerbutler Jan 26, 2025
27a1aa5
updates
tylerbutler Jan 26, 2025
01d70a4
tests
tylerbutler Jan 27, 2025
cb88bab
updates
tylerbutler Jan 27, 2025
e659d1a
tests
tylerbutler Jan 27, 2025
e03ac50
loader
tylerbutler Jan 27, 2025
b0120e8
builds
tylerbutler Jan 27, 2025
e56f43e
add scope
tylerbutler Jan 27, 2025
1d60d19
build
tylerbutler Jan 27, 2025
64357ba
changeset
tylerbutler Jan 27, 2025
8e90f72
Merge branch 'lilconfig-loader-ts' into lilconfig
tylerbutler Jan 27, 2025
6131fe2
wip
tylerbutler Jan 27, 2025
e2161ab
fix name
tylerbutler Jan 27, 2025
9e5f77b
Merge branch 'lilconfig-loader-ts' into lilconfig
tylerbutler Jan 27, 2025
5fac67d
lockfile
tylerbutler Jan 27, 2025
7b09ca8
updates
tylerbutler Jan 27, 2025
68b53fc
syncpack
tylerbutler Jan 27, 2025
19bff2c
Merge branch 'lilconfig-loader-ts' into lilconfig
tylerbutler Jan 27, 2025
39ed7c7
lockfile
tylerbutler Jan 27, 2025
3de33e9
Merge branch 'lilconfig-loader-ts' into lilconfig
tylerbutler Jan 27, 2025
dd3c0f6
Merge branch 'main' into lilconfig
tylerbutler Jan 28, 2025
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
4 changes: 4 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

// oclif manifest
"oclif.manifest.json",

// test json
"test/data/json/**/*.json",
],
"ignoreUnknown": true,
},
Expand Down Expand Up @@ -93,6 +96,7 @@
"include": ["**/*.mochatest.*", "**/*.vitest.*", "**/*.test.ts"],
"linter": {
"rules": {
"performance": { "useTopLevelRegex": "off" },
"suspicious": {
// Console logging is ok in tests.
"noConsole": "off",
Expand Down
13 changes: 6 additions & 7 deletions packages/cli-api/api-docs/cli-api.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,8 @@ export abstract class CommandWithConfig<T extends typeof Command & {
flags: typeof CommandWithConfig.flags;
}, C> extends BaseCommand<T> {
// (undocumented)
protected get commandConfig(): C;
// (undocumented)
protected get configPath(): string | undefined;
protected get commandConfig(): C | undefined;
protected get configLocation(): string | "DEFAULT" | undefined;
protected defaultConfig: C | undefined;
// (undocumented)
static readonly flags: {
Expand Down Expand Up @@ -134,11 +133,11 @@ export interface Logger {
export type LoggingFunction = (message?: string, ...args: unknown[]) => void;

// @beta
export type PackageTransformer<T extends PackageJson = PackageJson> = (json: T) => T;
export type PackageTransformer<J extends PackageJson = PackageJson> = (json: J) => J | Promise<J>;

// @beta
export function readJsonWithIndent(filePath: PathLike): Promise<{
json: unknown;
export function readJsonWithIndent<J = unknown>(filePath: PathLike): Promise<{
json: J;
indent: Indent;
}>;

Expand Down Expand Up @@ -167,7 +166,7 @@ export function revList(git: SimpleGit, baseCommit: string, headCommit?: string)
export function shortCommit(commit: string): string;

// @beta
export function updatePackageJsonFile<T extends PackageJson = PackageJson>(packagePath: string, packageTransformer: PackageTransformer, options?: JsonWriteOptions): Promise<void>;
export function updatePackageJsonFile<J extends PackageJson = PackageJson>(packagePath: string, packageTransformer: PackageTransformer, options?: JsonWriteOptions): Promise<void>;

// (No @packageDocumentation comment for this package)

Expand Down
22 changes: 20 additions & 2 deletions packages/cli-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
}
},
"types": "./esm/index.d.ts",
"files": ["/CHANGELOG.md", "/esm", "/THIRD-PARTY-LICENSES.txt"],
"files": [
"/CHANGELOG.md",
"/esm",
"!/esm/commands",
"!/esm/testConfig.*",
"/THIRD-PARTY-LICENSES.txt"
],
"scripts": {
"api": "api-extractor run --local",
"api:markdown": "api-documenter markdown -i _temp/api-extractor -o _temp/docs",
Expand All @@ -30,14 +36,24 @@
"full": "fluid-build . --task full",
"lint": "biome lint .",
"lint:fix": "biome lint . --write",
"sort-package-json": "sort-package-json"
"sort-package-json": "sort-package-json",
"test": "npm run test:vitest",
"test:coverage": "vitest run test --coverage",
"test:vitest": "vitest run test"
},
"oclif": {
"bin": "tylerbu-cli-api",
"commands": "./esm/commands",
"dirname": "tylerbu-cli-api"
},
"dependencies": {
"@oclif/core": "^4.2.3",
"@tylerbu/lilconfig-loader-ts": "workspace:^",
"cosmiconfig": "^9.0.0",
"debug": "^4.3.4",
"detect-indent": "^7.0.1",
"jsonfile": "^6.1.0",
"lilconfig": "^3.1.2",
"pathe": "^2.0.2",
"picocolors": "^1.1.0",
"simple-git": "^3.24.0",
Expand All @@ -55,6 +71,7 @@
"@types/mocha": "^10.0.10",
"@types/node": "^20.12.7",
"@types/semver": "^7.5.8",
"@vitest/coverage-v8": "^3.0.2",
"chai": "^5.1.2",
"concurrently": "^9.1.2",
"mocha": "^11.1.0",
Expand All @@ -63,6 +80,7 @@
"tmp-promise": "^3.0.3",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"tsx": "^4.19.2",
"type-fest": "^4.33.0",
"typescript": "~5.5.4",
"vitest": "^3.0.2"
Expand Down
4 changes: 4 additions & 0 deletions packages/cli-api/packlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ esm/json.d.ts
esm/json.d.ts.map
esm/json.js
esm/json.js.map
esm/loadConfig.d.ts
esm/loadConfig.d.ts.map
esm/loadConfig.js
esm/loadConfig.js.map
esm/logger.d.ts
esm/logger.d.ts.map
esm/logger.js
Expand Down
27 changes: 27 additions & 0 deletions packages/cli-api/src/commands/configTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CommandWithConfig } from "@tylerbu/cli-api";

export interface TestConfig {
stringProperty: string;
}

/**
* An implementation of CommandWithConfig used for testing.
*/
export default class ConfigTestCommand extends CommandWithConfig<
typeof ConfigTestCommand,
TestConfig
> {
protected override defaultConfig: TestConfig | undefined = {
stringProperty: "default",
};

// biome-ignore lint/suspicious/useAwait: inherited method
override async run(): Promise<TestConfig | undefined> {
// if (this.commandConfig === undefined) {
// this.error(`Couldn't find a config file.`);
// }

this.log(`Loaded config from: ${this.configLocation}`);
return this.commandConfig;
}
}
8 changes: 8 additions & 0 deletions packages/cli-api/src/commands/configTestNoDefault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ConfigTestCommand from "./configTest.js";

/**
* Tests CommandWithConfig loads default configs.
*/
export default class ConfigTestNoDefaultCommand extends ConfigTestCommand {
protected override defaultConfig = undefined;
}
68 changes: 21 additions & 47 deletions packages/cli-api/src/configCommand.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
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 { BaseCommand } from "./baseCommand.js";
import { ConfigFileFlagHidden } from "./flags.js";
import { findGitRoot } from "./git.js";
import { loadConfig } from "./loadConfig.js";

Check warning on line 4 in packages/cli-api/src/configCommand.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli-api/src/configCommand.ts#L4

Added line #L4 was not covered by tests

/**
* A base command that loads typed configuration values from a config file.
Expand All @@ -30,64 +27,41 @@
} as const;

/**
* A default config value to use if none is found. If this returns undefined, no default value will be used.
* A default config value to use if none is found. If this returns `undefined`, no default value will be used.
*/
protected defaultConfig: C | undefined;

public override async init(): Promise<void> {
await super.init();
const { config: configFlag } = this.flags;
const loaded = await this.loadConfig(configFlag);
if (loaded === undefined) {
this.error(`Failure to load config: ${configFlag}`, { exit: 1 });
const searchPath = configFlag ?? process.cwd();
const loaded = await loadConfig<C>(this.config.bin, searchPath, undefined);

Check warning on line 38 in packages/cli-api/src/configCommand.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli-api/src/configCommand.ts#L37-L38

Added lines #L37 - L38 were not covered by tests

if (loaded === undefined && this.defaultConfig === undefined) {
this.error(`Failure to load config: ${searchPath}`, { exit: 1 });

Check warning on line 41 in packages/cli-api/src/configCommand.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli-api/src/configCommand.ts#L40-L41

Added lines #L40 - L41 were not covered by tests
}
const { config, path } = loaded;
const { config, location } = loaded ?? {
config: this.defaultConfig,
location: "DEFAULT",
};

Check warning on line 46 in packages/cli-api/src/configCommand.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli-api/src/configCommand.ts#L43-L46

Added lines #L43 - L46 were not covered by tests
this._commandConfig = config;
this._configPath = path;
this._configPath = location;

Check warning on line 48 in packages/cli-api/src/configCommand.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli-api/src/configCommand.ts#L48

Added line #L48 was not covered by tests
}

private async loadConfig(
searchPath = process.cwd(),
reload?: boolean,
): Promise<{ config: C; path: string } | undefined> {
if (this._commandConfig === undefined || reload === true) {
const moduleName = this.config.bin;
const repoRoot = await findGitRoot();
const explorer = cosmiconfig(moduleName, {
searchStrategy: "global",
stopDir: repoRoot,
});
const pathStats = await stat(searchPath);
this.verbose(
`Looking for '${this.config.bin}' config at '${searchPath}'`,
);
const config: CosmiconfigResult = pathStats.isDirectory()
? await explorer.search(searchPath)
: await explorer.load(searchPath);

if (config?.config !== undefined) {
this.verbose(`Found config at ${config.filepath}`);
} else {
this.verbose(`No config found; started searching at ${searchPath}`);
if (this.defaultConfig === undefined) {
return undefined;
}
return { config: this.defaultConfig, path: "" };
}
return { config: config.config as C, path: config.filepath };
protected get commandConfig(): C | undefined {
if (this._commandConfig === undefined && this.defaultConfig !== undefined) {
this._commandConfig = this.defaultConfig;

Check warning on line 53 in packages/cli-api/src/configCommand.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli-api/src/configCommand.ts#L51-L53

Added lines #L51 - L53 were not covered by tests
}
}

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;
}

protected get configPath(): string | undefined {
/**
* The location of the config. If the config was loaded from a file, this will be the path to the file. If no config
* was loaded, and no default config is defined, this will return `undefined`. If the default config was loaded, this
* will return the string "DEFAULT";
*/
protected get configLocation(): string | "DEFAULT" | undefined {

Check warning on line 64 in packages/cli-api/src/configCommand.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli-api/src/configCommand.ts#L64

Added line #L64 was not covered by tests
return this._configPath;
}
}
Expand Down
40 changes: 22 additions & 18 deletions packages/cli-api/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const defaultJsonWriteOptions = {
indent: "\t",
sort: true,
};

/**
* Reads a JSON file and its indentation.
*
Expand All @@ -38,8 +39,10 @@ const defaultJsonWriteOptions = {
*
* @beta
*/
export async function readJsonWithIndent(filePath: PathLike): Promise<{
json: unknown;
export async function readJsonWithIndent<J = unknown>(
filePath: PathLike,
): Promise<{
json: J;
indent: Indent;
}> {
const contents: string = await readFile(filePath, {
Expand All @@ -55,9 +58,9 @@ export async function readJsonWithIndent(filePath: PathLike): Promise<{
*
* @beta
*/
function writePackageJson(
function writePackageJson<J extends PackageJson = PackageJson>(
packagePath: string,
pkgJson: PackageJson,
pkgJson: J,
{ indent, sort }: JsonWriteOptions,
) {
const spaces =
Expand All @@ -70,27 +73,28 @@ function writePackageJson(
}

/**
* A function that transforms a PackaageJson and returns the transformed object.
* A function that transforms a PackageJson and returns the transformed object.
*
* @beta
*/
export type PackageTransformer<T extends PackageJson = PackageJson> = (
json: T,
) => T;
export type PackageTransformer<J extends PackageJson = PackageJson> = (
json: J,
) => J | Promise<J>;

/**
* Reads the contents of package.json, applies a transform function to it, then writes the results back to the source
* file.
* Reads the contents of package.json, applies a transform function to it, then writes
* the results back to the source file.
*
* @param packagePath - A path to a package.json file or a folder containing one. If the path is a directory, the
* package.json from that directory will be used.
* @param packageTransformer - A function that will be executed on the package.json contents before writing it
* back to the file.
* @param packagePath - A path to a package.json file or a folder containing one. If the
* path is a directory, the package.json from that directory will be used.
* @param packageTransformer - A function that will be executed on the package.json
* contents before writing it back to the file.
* @param options - Options that control the output JSON format.
*
* @beta
*/
export async function updatePackageJsonFile<
T extends PackageJson = PackageJson,
J extends PackageJson = PackageJson,
>(
packagePath: string,
packageTransformer: PackageTransformer,
Expand All @@ -99,11 +103,11 @@ export async function updatePackageJsonFile<
const resolvedPath = packagePath.endsWith("package.json")
? packagePath
: path.join(packagePath, "package.json");
const { json, indent } = await readJsonWithIndent(resolvedPath);
const pkgJson = json as T;
const { json, indent } = await readJsonWithIndent<J>(resolvedPath);
const pkgJson = json;

// Transform the package.json
const transformed = packageTransformer(pkgJson);
const transformed = await Promise.resolve(packageTransformer(pkgJson));

writePackageJson(resolvedPath, transformed, { sort: options?.sort, indent });
}
52 changes: 52 additions & 0 deletions packages/cli-api/src/loadConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { stat } from "node:fs/promises";
import { TypeScriptLoader } from "@tylerbu/lilconfig-loader-ts";
import { type LilconfigResult, type Options, lilconfig } from "lilconfig";

/**
* Loads a config of the given type from the file system.
*
* Config file names to be of the form ${moduleName}.config.(cjs/mjs/ts).
*
* @typeParam C - The type of the loaded config.
* @param moduleName - A string for the module/app whose config is being loaded.
* @param searchPath - The path to start searching for a config. If this is a path to a file that matches a config file,
* it will be loaded.
* @param stopDir - An optional directory to stop recursing up to find a config.
* @param defaultConfig - An optional default config that will be used if no config is loaded.
* @returns An object containing the `config` and its location (file path), if any is loaded. Returns `undefined` if no
* config file is found.
*/
export async function loadConfig<C>(
moduleName: string,
searchPath: string,
stopDir?: string,
): Promise<{ config: C; location: string } | undefined> {
const options: Options = {
searchPlaces: [
`${moduleName}.config.ts`,
`${moduleName}.config.mjs`,
`${moduleName}.config.cjs`,
],
loaders: {
".ts": TypeScriptLoader,
},
};

if (stopDir !== undefined) {
options.stopDir = stopDir;
}

const configLoader = lilconfig(moduleName, options);

const pathStats = await stat(searchPath);
const maybeConfig: LilconfigResult = pathStats.isDirectory()
? await configLoader.search(searchPath)
: await configLoader.load(searchPath);

Check warning on line 44 in packages/cli-api/src/loadConfig.ts

View check run for this annotation

Codecov / codecov/patch

packages/cli-api/src/loadConfig.ts#L44

Added line #L44 was not covered by tests

return maybeConfig === null
? undefined
: {
config: maybeConfig.config,
location: maybeConfig.filepath,
};
}
Loading
Loading