Skip to content

Commit

Permalink
Reworked configuration resolution to take the support file names in a…
Browse files Browse the repository at this point in the history
…s an input parameter
  • Loading branch information
fabiospampinato committed Dec 2, 2023
1 parent f128175 commit e1f5a72
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 116 deletions.
35 changes: 18 additions & 17 deletions src/config_editorconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,38 @@ import { fastJoinedPath, findLastIndex, memoize, zipObject } from "./utils.js";
import type { Config, ConfigWithOverrides } from "tiny-editorconfig";
import type { FormatOptions } from "./types.js";

const getEditorConfig = memoize(async (folderPath: string): Promise<ConfigWithOverrides | undefined> => {
if (!Known.hasFileName(".editorconfig")) return;
const filePath = fastJoinedPath(folderPath, ".editorconfig");
if (!Known.hasFilePath(filePath)) return;
try {
const fileContent = await fs.readFile(filePath, "utf8");
const config = EditorConfig.parse(fileContent);
return config;
} catch {
return;
const getEditorConfig = memoize(async (folderPath: string, filesNames: string[]): Promise<ConfigWithOverrides | undefined> => {
for (let i = 0, l = filesNames.length; i < l; i++) {
const fileName = filesNames[i];
if (!Known.hasFileName(fileName)) return;
const filePath = fastJoinedPath(folderPath, fileName);
if (!Known.hasFilePath(filePath)) return;
try {
const fileContent = await fs.readFile(filePath, "utf8");
const config = EditorConfig.parse(fileContent);
return config;
} catch {}
}
});

const getEditorConfigsMap = async (foldersPaths: string[]): Promise<Partial<Record<string, ConfigWithOverrides>>> => {
const configs = await Promise.all(foldersPaths.map(getEditorConfig));
const getEditorConfigsMap = async (foldersPaths: string[], filesNames: string[]): Promise<Partial<Record<string, ConfigWithOverrides>>> => {
const configs = await Promise.all(foldersPaths.map((folderPath) => getEditorConfig(folderPath, filesNames)));
const map = zipObject(foldersPaths, configs);
return map;
};

const getEditorConfigsUp = memoize(async (folderPath: string): Promise<ConfigWithOverrides[]> => {
const config = await getEditorConfig(folderPath);
const getEditorConfigsUp = memoize(async (folderPath: string, filesNames: string[]): Promise<ConfigWithOverrides[]> => {
const config = await getEditorConfig(folderPath, filesNames);
const folderPathUp = path.dirname(folderPath);
const configsUp = folderPath !== folderPathUp ? await getEditorConfigsUp(folderPathUp) : [];
const configsUp = folderPath !== folderPathUp ? await getEditorConfigsUp(folderPathUp, filesNames) : [];
const configs = config ? [...configsUp, config] : configsUp;
const lastRootIndex = findLastIndex(configs, (config) => config.root);
return lastRootIndex > 0 ? configs.slice(lastRootIndex) : configs;
});

const getEditorConfigResolved = async (filePath: string): Promise<Config> => {
const getEditorConfigResolved = async (filePath: string, filesNames: string[]): Promise<Config> => {
const folderPath = path.dirname(filePath);
const configs = await getEditorConfigsUp(folderPath);
const configs = await getEditorConfigsUp(folderPath, filesNames);
const config = EditorConfig.resolve(configs, filePath);
return config;
};
Expand Down
48 changes: 21 additions & 27 deletions src/config_ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ignore from "ignore";
import fs from "node:fs/promises";
import path from "node:path";
import Known from "./known.js";
import { fastJoinedPath, fastRelativeChildPath, memoize, someOf, zipObject } from "./utils.js";
import { fastJoinedPath, fastRelativeChildPath, isString, memoize, someOf, zipObject } from "./utils.js";
import type { Ignore } from "./types.js";

const getIgnoreContentBy = async (folderPath: string, fileName: string): Promise<string | undefined> => {
Expand All @@ -11,26 +11,23 @@ const getIgnoreContentBy = async (folderPath: string, fileName: string): Promise
if (!Known.hasFilePath(filePath)) return;
try {
return await fs.readFile(filePath, "utf8");
} catch {
return;
}
} catch {}
};

const getIgnoresContent = memoize(async (folderPath: string): Promise<[string?, string?] | undefined> => {
const git = await getIgnoreContentBy(folderPath, ".gitignore");
const prettier = await getIgnoreContentBy(folderPath, ".prettierignore");
const contents = git ? (prettier ? [git, prettier] : [git]) : prettier ? [prettier] : [];
if (!contents.length) return;
return [git, prettier];
const getIgnoresContent = memoize(async (folderPath: string, filesNames: string[]): Promise<string[]> => {
const contentsRaw = await Promise.all(filesNames.map((fileName) => getIgnoreContentBy(folderPath, fileName)));
const contents = contentsRaw.filter(isString);
return contents;
});

const getIgnoresContentMap = async (foldersPaths: string[]): Promise<Partial<Record<string, [string?, string?]>>> => {
const ignoresContent = await Promise.all(foldersPaths.map(getIgnoresContent));
const map = zipObject(foldersPaths, ignoresContent);
const getIgnoresContentMap = async (foldersPaths: string[], filesNames: string[]): Promise<Partial<Record<string, string[]>>> => {
const contents = await Promise.all(foldersPaths.map((folderPath) => getIgnoresContent(folderPath, filesNames)));
const map = zipObject(foldersPaths, contents);
return map;
};

const getIgnoreBy = (folderPath: string, fileContent: string): Ignore => {
//TODO: Optimize this massively, as it pops up while profiling a lot
const instance = ignore().add(fileContent);
const ignores = instance.ignores.bind(instance);
return (filePath: string): boolean => {
Expand All @@ -39,32 +36,29 @@ const getIgnoreBy = (folderPath: string, fileContent: string): Ignore => {
};
};

const getIgnores = memoize(async (folderPath: string): Promise<Ignore | undefined> => {
const contents = await getIgnoresContent(folderPath);
if (!contents) return;
const [gitContent, prettierContent] = contents;
const git = gitContent ? getIgnoreBy(folderPath, gitContent) : undefined;
const prettier = prettierContent ? getIgnoreBy(folderPath, prettierContent) : undefined;
const ignores = git ? (prettier ? [git, prettier] : [git]) : prettier ? [prettier] : [];
if (!ignores.length) return;
const getIgnores = memoize(async (folderPath: string, filesNames: string[]): Promise<Ignore | undefined> => {
const contents = await getIgnoresContent(folderPath, filesNames);
if (!contents.length) return;
const ignores = contents.map((content) => getIgnoreBy(folderPath, content));
const ignore = someOf(ignores);
return ignore;
});

const getIgnoresUp = memoize(async (folderPath: string): Promise<Ignore | undefined> => {
const ignore = await getIgnores(folderPath);
const getIgnoresUp = memoize(async (folderPath: string, filesNames: string[]): Promise<Ignore | undefined> => {
const ignore = await getIgnores(folderPath, filesNames);
const folderPathUp = path.dirname(folderPath);
const ignoreUp = folderPath !== folderPathUp ? await getIgnoresUp(folderPathUp) : undefined;
const ignoreUp = folderPath !== folderPathUp ? await getIgnoresUp(folderPathUp, filesNames) : undefined;
const ignores = ignore ? (ignoreUp ? [ignore, ignoreUp] : [ignore]) : ignoreUp ? [ignoreUp] : [];
if (!ignores.length) return;
const ignoreAll = someOf(ignores);
return ignoreAll;
});

const getIgnoreResolved = async (filePath: string): Promise<boolean> => {
const getIgnoreResolved = async (filePath: string, filesNames: string[]): Promise<boolean> => {
const folderPath = path.dirname(filePath);
const ignore = await getIgnoresUp(folderPath);
return !!ignore?.(filePath);
const ignore = await getIgnoresUp(folderPath, filesNames);
const ignored = !!ignore?.(filePath);
return ignored;
};

export { getIgnoreBy, getIgnores, getIgnoresContent, getIgnoresContentMap, getIgnoresUp, getIgnoreResolved };
120 changes: 63 additions & 57 deletions src/config_prettier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,81 +13,87 @@ import type { PrettierConfig, PrettierConfigWithOverrides } from "./types.js";
//TODO: Maybe add support for TOML
//TODO: Check which of these file names have been found in the repo just once

const loaders = [
{
files: ["package.json"],
loader: async (filePath: string): Promise<unknown> => {
const fileContent = await fs.readFile(filePath, "utf8");
const pkg = JSON.parse(fileContent);
const config = isObject(pkg) && "prettier" in pkg ? pkg.prettier : undefined;
return config;
},
const Loaders = {
js: async (filePath: string): Promise<unknown> => {
const exists = sfs.existsSync(filePath);
if (!exists) return;
const module = await import(filePath);
return module.default || module.exports || module.config || module.prettier; //TODO: Streamline this
},
{
files: [".prettierrc", ".prettierrc.yml", ".prettierrc.yaml"],
loader: async (filePath: string): Promise<unknown> => {
const fileContent = await fs.readFile(filePath, "utf8");
return yaml.load(fileContent, {
schema: yaml.JSON_SCHEMA,
});
},
json: async (filePath: string): Promise<unknown> => {
const fileContent = await fs.readFile(filePath, "utf8");
const config = JSON.parse(fileContent);
return config;
},
{
files: [".prettierrc.json", ".prettierrc.jsonc", ".prettierrc.json5"],
loader: async (filePath: string): Promise<unknown> => {
const fileContent = await fs.readFile(filePath, "utf8");
const config = JSONC.parse(fileContent);
return config;
},
jsonc: async (filePath: string): Promise<unknown> => {
const fileContent = await fs.readFile(filePath, "utf8");
const config = JSONC.parse(fileContent);
return config;
},
{
files: [".prettierrc.js", "prettier.config.js", ".prettierrc.cjs", "prettier.config.cjs", ".prettierrc.mjs", "prettier.config.mjs"],
loader: async (filePath: string): Promise<unknown> => {
const exists = sfs.existsSync(filePath);
if (!exists) return;
const module = await import(filePath);
return module.default || module.exports || module.config || module.prettier; //TODO: Streamline this
},
package: async (filePath: string): Promise<unknown> => {
const fileContent = await fs.readFile(filePath, "utf8");
const pkg = JSON.parse(fileContent);
const config = isObject(pkg) && "prettier" in pkg ? pkg.prettier : undefined;
return config;
},
];
yaml: async (filePath: string): Promise<unknown> => {
const fileContent = await fs.readFile(filePath, "utf8");
return yaml.load(fileContent, {
schema: yaml.JSON_SCHEMA,
});
},
};

const getPrettierConfig = memoize(async (folderPath: string): Promise<PrettierConfigWithOverrides | undefined> => {
for (let li = 0, ll = loaders.length; li < ll; li++) {
const { files, loader } = loaders[li];
for (let fi = 0, fl = files.length; fi < fl; fi++) {
const fileName = files[fi];
if (!Known.hasFileName(fileName)) continue;
const filePath = fastJoinedPath(folderPath, fileName);
if (!Known.hasFilePath(filePath)) continue;
try {
const config = await loader(filePath);
if (!config) continue;
if (!isObject(config)) continue;
return normalizePrettierOptions(config, folderPath);
} catch {
continue;
}
}
const File2Loader: Record<string, (filePath: string) => Promise<unknown>> = {
"package.json": Loaders.package,
".prettierrc": Loaders.yaml,
".prettierrc.yml": Loaders.yaml,
".prettierrc.yaml": Loaders.yaml,
".prettierrc.json": Loaders.json,
".prettierrc.jsonc": Loaders.jsonc,
".prettierrc.json5": Loaders.jsonc,
".prettierrc.js": Loaders.js,
".prettierrc.cjs": Loaders.js,
".prettierrc.mjs": Loaders.js,
"prettier.config.js": Loaders.js,
"prettier.config.cjs": Loaders.js,
"prettier.config.mjs": Loaders.js,
};

const getPrettierConfig = memoize(async (folderPath: string, filesNames: string[]): Promise<PrettierConfigWithOverrides | undefined> => {
for (let i = 0, l = filesNames.length; i < l; i++) {
const fileName = filesNames[i];
if (!Known.hasFileName(fileName)) continue;
const filePath = fastJoinedPath(folderPath, fileName);
if (!Known.hasFilePath(filePath)) continue;
const loader = File2Loader[fileName];
if (!loader) continue;
try {
const config = await loader(filePath);
if (!config) continue;
if (!isObject(config)) continue;
return normalizePrettierOptions(config, folderPath);
} catch {}
}
});

const getPrettierConfigsMap = async (foldersPaths: string[]): Promise<Partial<Record<string, PrettierConfig>>> => {
const configs = await Promise.all(foldersPaths.map(getPrettierConfig));
const getPrettierConfigsMap = async (foldersPaths: string[], filesNames: string[]): Promise<Partial<Record<string, PrettierConfig>>> => {
const configs = await Promise.all(foldersPaths.map((folderPath) => getPrettierConfig(folderPath, filesNames)));
const map = zipObject(foldersPaths, configs);
return map;
};

const getPrettierConfigsUp = memoize(async (folderPath: string): Promise<PrettierConfigWithOverrides[]> => {
const config = await getPrettierConfig(folderPath);
const getPrettierConfigsUp = memoize(async (folderPath: string, filesNames: string[]): Promise<PrettierConfigWithOverrides[]> => {
const config = await getPrettierConfig(folderPath, filesNames);
const folderPathUp = path.dirname(folderPath);
const configsUp = folderPath !== folderPathUp ? await getPrettierConfigsUp(folderPathUp) : [];
const configsUp = folderPath !== folderPathUp ? await getPrettierConfigsUp(folderPathUp, filesNames) : [];
const configs = config ? [...configsUp, config] : configsUp;
return configs;
});

const getPrettierConfigResolved = async (filePath: string): Promise<PrettierConfig> => {
const getPrettierConfigResolved = async (filePath: string, filesNames: string[]): Promise<PrettierConfig> => {
const folderPath = path.dirname(filePath);
const configs = await getPrettierConfigsUp(folderPath);
const configs = await getPrettierConfigsUp(folderPath, filesNames);
let resolved: PrettierConfig = {};

for (let ci = 0, cl = configs.length; ci < cl; ci++) {
Expand Down
22 changes: 13 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ async function run(options: Options): Promise<void> {
const prettierVersion = PRETTIER_VERSION;
const cliVersion = CLI_VERSION;
const pluginsVersions = ""; //TODO
const editorConfigs = options.editorConfig ? await getEditorConfigsMap(foldersPathsTargets) : {};
const ignoreContents = await getIgnoresContentMap(foldersPathsTargets);
const prettierConfigs = options.config ? await getPrettierConfigsMap(foldersPathsTargets) : {};

const editorConfigNames = [".editorconfig"];
const ignoreNames = [".gitignore", ".prettierignore"];
const prettierConfigNames = ["package.json", ".prettierrc", ".prettierrc.yml", ".prettierrc.yaml", ".prettierrc.json", ".prettierrc.jsonc", ".prettierrc.json5", ".prettierrc.js", "prettier.config.js", ".prettierrc.cjs", "prettier.config.cjs", ".prettierrc.mjs", "prettier.config.mjs"]; // prettier-ignore

const editorConfigs = options.editorConfig ? await getEditorConfigsMap(foldersPathsTargets, editorConfigNames) : {};
const ignoreContents = await getIgnoresContentMap(foldersPathsTargets, ignoreNames);
const prettierConfigs = options.config ? await getPrettierConfigsMap(foldersPathsTargets, prettierConfigNames) : {};

const cliConfig = options.formatOptions;
const cacheVersion = stringify({ prettierVersion, cliVersion, pluginsVersions, editorConfigs, ignoreContents, prettierConfigs, cliConfig });

Expand All @@ -52,11 +58,11 @@ async function run(options: Options): Promise<void> {
//TODO: Maybe do work in chunks here, as keeping too many formatted files in memory can be a problem
const filesResults = await Promise.allSettled(
filesPathsTargets.map(async (filePath) => {
const ignored = await getIgnoreResolved(filePath);
const ignored = await getIgnoreResolved(filePath, ignoreNames);
if (ignored) return;
const getFormatOptions = async (): Promise<FormatOptions> => {
const editorConfig = options.editorConfig ? getEditorConfigFormatOptions(await getEditorConfigResolved(filePath)) : {};
const prettierConfig = options.config ? await getPrettierConfigResolved(filePath) : {};
const editorConfig = options.editorConfig ? getEditorConfigFormatOptions(await getEditorConfigResolved(filePath, editorConfigNames)) : {};
const prettierConfig = options.config ? await getPrettierConfigResolved(filePath, prettierConfigNames) : {};
const formatOptions = { ...editorConfig, ...prettierConfig, ...options.formatOptions };
return formatOptions;
};
Expand Down Expand Up @@ -133,9 +139,7 @@ async function run(options: Options): Promise<void> {

// if (totalErrored) {
// if (options.check) {
// logger.prefixed.error(
// `Parsing issues found in ${totalErrored} ${pluralize("file", totalErrored)}. Check files to fix.`,
// );
// logger.prefixed.error(`Parsing issues found in ${totalErrored} ${pluralize("file", totalErrored)}. Check files to fix.`);
// }
// }

Expand Down
14 changes: 8 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,15 @@ function isUndefined(value: unknown): value is undefined {
return typeof value === "undefined";
}

function memoize<T, Return>(fn: (arg: T) => Return): ((arg: T) => Return) & { cache: Map<T, Return> } {
const memoized = (arg: T): Return => {
//FIXME: Ensure all arguments are actually taken into account
function memoize<Args extends unknown[], Return>(fn: (...args: Args) => Return): ((...args: Args) => Return) & { cache: Map<Args[0], Return> } {
const memoized = (...args: Args): Return => {
const { cache } = memoized;
const cached = cache.get(arg);
if (!isUndefined(cached) || cache.has(arg)) return cached;
const result = fn(arg);
cache.set(arg, result);
const id = args[0];
const cached = cache.get(id);
if (!isUndefined(cached) || cache.has(id)) return cached;
const result = fn(...args);
cache.set(id, result);
return result;
};
memoized.cache = new Map();
Expand Down

0 comments on commit e1f5a72

Please sign in to comment.