diff --git a/changelog_unreleased/api/12574.md b/changelog_unreleased/api/12574.md index 3b1690da4..af75fe135 100644 --- a/changelog_unreleased/api/12574.md +++ b/changelog_unreleased/api/12574.md @@ -1,8 +1,9 @@ -#### [BREAKING] Change public apis to asynchronous (#12574, #12788 by @fisker) +#### [BREAKING] Change public apis to asynchronous (#12574, #12788, #12790 by @fisker) - `prettier.format()` returns `Promise` - `prettier.formatWithCursor()` returns `Promise<{formatted: string, cursorOffset: number}>` - `prettier.check()` returns `Promise` +- `prettier.getSupportInfo()` returns `Promise` - `prettier.resolveConfig.sync` is removed - `prettier.resolveConfigFile.sync` is removed - `prettier.getFileInfo.sync` is removed diff --git a/docs/api.md b/docs/api.md index 33e48731a..2faf5a753 100644 --- a/docs/api.md +++ b/docs/api.md @@ -102,7 +102,7 @@ When setting `options.resolveConfig` (`boolean`, default `false`), Prettier will ## `prettier.getSupportInfo()` -Returns an object representing the options, parsers, languages and file types Prettier supports. +Returns a promise which resolves to an object representing the options, parsers, languages and file types Prettier supports. The support information looks like this: diff --git a/src/cli/context.js b/src/cli/context.js index 5ae1ff91f..8f7dafc6f 100644 --- a/src/cli/context.js +++ b/src/cli/context.js @@ -26,14 +26,18 @@ class Context { this.rawArguments = rawArguments; this.logger = logger; this.stack = []; + } - const { plugins, pluginSearchDirs } = parseArgvWithoutPlugins( + async init() { + const { rawArguments, logger } = this; + + const { plugins, pluginSearchDirs } = await parseArgvWithoutPlugins( rawArguments, logger, ["plugin", "plugin-search-dir"] ); - this.pushContextPlugins(plugins, pluginSearchDirs); + await this.pushContextPlugins(plugins, pluginSearchDirs); const argv = parseArgv(rawArguments, this.detailedOptions, logger); this.argv = argv; @@ -44,8 +48,8 @@ class Context { * @param {string[]} plugins * @param {string[]=} pluginSearchDirs */ - pushContextPlugins(plugins, pluginSearchDirs) { - const options = getContextOptions(plugins, pluginSearchDirs); + async pushContextPlugins(plugins, pluginSearchDirs) { + const options = await getContextOptions(plugins, pluginSearchDirs); this.stack.push(options); Object.assign(this, options); } diff --git a/src/cli/index.js b/src/cli/index.js index f5157bc74..01e3699be 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -14,15 +14,16 @@ async function run(rawArguments) { let logger = createLogger(); try { - const logLevel = parseArgvWithoutPlugins( + const { loglevel: logLevel } = await parseArgvWithoutPlugins( rawArguments, logger, "loglevel" - ).loglevel; + ); if (logLevel !== logger.logLevel) { logger = createLogger(logLevel); } const context = new Context({ rawArguments, logger }); + await context.init(); if (logger.logLevel !== "debug" && context.performanceTestFlag) { context.logger = createLogger("debug"); } @@ -80,10 +81,9 @@ async function main(context) { } if (context.argv.supportInfo) { + const supportInfo = await prettier.getSupportInfo(); printToScreen( - await prettier.format(stringify(prettier.getSupportInfo()), { - parser: "json", - }) + await prettier.format(stringify(supportInfo), { parser: "json" }) ); return; } diff --git a/src/cli/options/get-context-options.js b/src/cli/options/get-context-options.js index a662000f5..4aeff0a5b 100644 --- a/src/cli/options/get-context-options.js +++ b/src/cli/options/get-context-options.js @@ -7,8 +7,8 @@ import { createDetailedOptionMap, } from "./option-map.js"; -function getContextOptions(plugins, pluginSearchDirs) { - const { options: supportOptions, languages } = getSupportInfo({ +async function getContextOptions(plugins, pluginSearchDirs) { + const { options: supportOptions, languages } = await getSupportInfo({ showDeprecated: true, showUnreleased: true, showInternal: true, diff --git a/src/cli/options/get-options-for-file.js b/src/cli/options/get-options-for-file.js index 10ec1cfd2..1d971ed24 100644 --- a/src/cli/options/get-options-for-file.js +++ b/src/cli/options/get-options-for-file.js @@ -107,7 +107,7 @@ async function getOptionsForFile(context, filepath) { const hasPlugins = options && options.plugins; if (hasPlugins) { - context.pushContextPlugins(options.plugins); + await context.pushContextPlugins(options.plugins); } const appliedOptions = { diff --git a/src/cli/options/parse-cli-arguments.js b/src/cli/options/parse-cli-arguments.js index 1f7e313fa..e40f8549c 100644 --- a/src/cli/options/parse-cli-arguments.js +++ b/src/cli/options/parse-cli-arguments.js @@ -39,11 +39,13 @@ function parseArgv(rawArguments, detailedOptions, logger, keys) { }; } -const detailedOptionsWithoutPlugins = getContextOptions( - [], - false -).detailedOptions; -function parseArgvWithoutPlugins(rawArguments, logger, keys) { +let detailedOptionsWithoutPlugins; +async function parseArgvWithoutPlugins(rawArguments, logger, keys) { + if (!detailedOptionsWithoutPlugins) { + ({ detailedOptions: detailedOptionsWithoutPlugins } = + await getContextOptions([], false)); + } + return parseArgv( rawArguments, detailedOptionsWithoutPlugins, diff --git a/src/common/load-plugins.js b/src/common/load-plugins.js index 0dabc8f11..a93694079 100644 --- a/src/common/load-plugins.js +++ b/src/common/load-plugins.js @@ -1,5 +1,5 @@ import { createRequire } from "node:module"; -import fs from "node:fs"; +import { promises as fsPromises } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import fastGlob from "fast-glob"; @@ -19,7 +19,7 @@ const clearCache = () => { memClear(memoizedSearch); }; -function load(plugins, pluginSearchDirs) { +async function load(plugins, pluginSearchDirs) { if (!plugins) { plugins = []; } @@ -61,36 +61,42 @@ function load(plugins, pluginSearchDirs) { } ); - const externalAutoLoadPluginInfos = pluginSearchDirs.flatMap( - (pluginSearchDir) => { - const resolvedPluginSearchDir = path.resolve( - process.cwd(), - pluginSearchDir - ); - - const nodeModulesDir = path.resolve( - resolvedPluginSearchDir, - "node_modules" - ); - - // In some fringe cases (ex: files "mounted" as virtual directories), the - // isDirectory(resolvedPluginSearchDir) check might be false even though - // the node_modules actually exists. - if ( - !isDirectory(nodeModulesDir) && - !isDirectory(resolvedPluginSearchDir) - ) { - throw new Error( - `${pluginSearchDir} does not exist or is not a directory` + const externalAutoLoadPluginInfos = ( + await Promise.all( + pluginSearchDirs.map(async (pluginSearchDir) => { + const resolvedPluginSearchDir = path.resolve( + process.cwd(), + pluginSearchDir ); - } - return memoizedSearch(nodeModulesDir).map((pluginName) => ({ - name: pluginName, - requirePath: resolve(pluginName, { paths: [resolvedPluginSearchDir] }), - })); - } - ); + const nodeModulesDir = path.resolve( + resolvedPluginSearchDir, + "node_modules" + ); + + // In some fringe cases (ex: files "mounted" as virtual directories), the + // isDirectory(resolvedPluginSearchDir) check might be false even though + // the node_modules actually exists. + if ( + !(await isDirectory(nodeModulesDir)) && + !(await isDirectory(resolvedPluginSearchDir)) + ) { + throw new Error( + `${pluginSearchDir} does not exist or is not a directory` + ); + } + + const pluginNames = await memoizedSearch(nodeModulesDir); + + return pluginNames.map((pluginName) => ({ + name: pluginName, + requirePath: resolve(pluginName, { + paths: [resolvedPluginSearchDir], + }), + })); + }) + ) + ).flat(); const externalPlugins = [ ...uniqByKey( @@ -106,8 +112,8 @@ function load(plugins, pluginSearchDirs) { return externalPlugins; } -function findPluginsInNodeModules(nodeModulesDir) { - const pluginPackageJsonPaths = fastGlob.sync( +async function findPluginsInNodeModules(nodeModulesDir) { + const pluginPackageJsonPaths = await fastGlob( [ "prettier-plugin-*/package.json", "@*/prettier-plugin-*/package.json", @@ -120,12 +126,16 @@ function findPluginsInNodeModules(nodeModulesDir) { return pluginPackageJsonPaths.map(path.dirname); } -function isDirectory(dir) { +async function isDirectory(dir) { + let stat; + try { - return fs.statSync(dir).isDirectory(); + stat = await fsPromises.stat(dir); } catch { return false; } + + return stat.isDirectory(); } export { memoizedLoad as loadPlugins, clearCache }; diff --git a/src/index.js b/src/index.js index 1b1013992..f1f150848 100644 --- a/src/index.js +++ b/src/index.js @@ -39,13 +39,14 @@ function withPlugins( fn, optsArgIdx = 1 // Usually `opts` is the 2nd argument ) { - return (...args) => { + return async (...args) => { const opts = args[optsArgIdx] || {}; + args[optsArgIdx] = { ...opts, plugins: [ ...languages, - ...loadPlugins(opts.plugins, opts.pluginSearchDirs), + ...(await loadPlugins(opts.plugins, opts.pluginSearchDirs)), ], }; return fn(...args); diff --git a/src/standalone.js b/src/standalone.js index 7da087662..f9b6fbe68 100644 --- a/src/standalone.js +++ b/src/standalone.js @@ -10,7 +10,9 @@ function withPlugins( fn, optsArgIdx = 1 // Usually `opts` is the 2nd argument ) { - return (...args) => { + // Returns Promises to consistent with functions in `index.js` + // eslint-disable-next-line require-await + return async (...args) => { const opts = args[optsArgIdx] || {}; const plugins = opts.plugins || []; diff --git a/tests/format/misc/empty/jsfmt.spec.js b/tests/format/misc/empty/jsfmt.spec.js index d70ef0643..85e2c3c31 100644 --- a/tests/format/misc/empty/jsfmt.spec.js +++ b/tests/format/misc/empty/jsfmt.spec.js @@ -1,8 +1,7 @@ import * as prettier from "../../../../index.js"; -const parsers = prettier - .getSupportInfo() - .options.find((option) => option.name === "parser") +const parsers = (await prettier.getSupportInfo()).options + .find((option) => option.name === "parser") .choices.filter((choice) => !choice.deprecated) .map((choice) => choice.value); diff --git a/tests/integration/__tests__/debug-api.js b/tests/integration/__tests__/debug-api.js index bf9c48fb6..2df0467dc 100644 --- a/tests/integration/__tests__/debug-api.js +++ b/tests/integration/__tests__/debug-api.js @@ -27,8 +27,8 @@ describe("API", () => { expect(Array.isArray(ast.program.body)).toBe(true); }); - test("prettier.formatAST", () => { - const { formatted: formatResultFromAST } = formatAST(ast, options); + test("prettier.formatAST", async () => { + const { formatted: formatResultFromAST } = await formatAST(ast, options); expect(formatResultFromAST).toBe(formatted); }); @@ -41,8 +41,8 @@ describe("API", () => { done(); }); - test("prettier.printDocToString", () => { - const { formatted: stringFromDoc } = printDocToString(doc, options); + test("prettier.printDocToString", async () => { + const { formatted: stringFromDoc } = await printDocToString(doc, options); expect(stringFromDoc).toBe(formatted); }); @@ -55,7 +55,7 @@ describe("API", () => { `return ${formatResultFromDoc}` )(builders); - const { formatted: stringFromDoc2 } = printDocToString(doc2, options); + const { formatted: stringFromDoc2 } = await printDocToString(doc2, options); expect(stringFromDoc2).toBe(formatted); const formatResultFromDoc2 = await formatDoc(doc2, options); diff --git a/tests/integration/__tests__/format-ast.js b/tests/integration/__tests__/format-ast.js index 6ca75e4a8..29808d665 100644 --- a/tests/integration/__tests__/format-ast.js +++ b/tests/integration/__tests__/format-ast.js @@ -2,8 +2,8 @@ import prettier from "prettier-local"; const { formatAST } = prettier.__debug; describe("formatAST", () => { - const formatExportSpecifier = (specifier) => { - const { formatted } = formatAST( + const formatExportSpecifier = async (specifier) => { + const { formatted } = await formatAST( { type: "Program", body: [ @@ -19,9 +19,9 @@ describe("formatAST", () => { return formatted; }; - test("Shorthand specifier", () => { + test("Shorthand specifier", async () => { expect( - formatExportSpecifier({ + await formatExportSpecifier({ type: "ExportSpecifier", local: { type: "Identifier", @@ -35,9 +35,9 @@ describe("formatAST", () => { ).toMatchSnapshot(); }); - test("Shorthand specifier 2", () => { + test("Shorthand specifier 2", async () => { expect( - formatExportSpecifier({ + await formatExportSpecifier({ type: "ExportSpecifier", local: { type: "Identifier", @@ -53,9 +53,9 @@ describe("formatAST", () => { ).toMatchSnapshot(); }); - test("Shorthand specifier 3", () => { + test("Shorthand specifier 3", async () => { expect( - formatExportSpecifier({ + await formatExportSpecifier({ type: "ExportSpecifier", local: { type: "Literal", @@ -72,9 +72,9 @@ describe("formatAST", () => { ).toMatchSnapshot(); }); - test("Shorthand specifier 4", () => { + test("Shorthand specifier 4", async () => { expect( - formatExportSpecifier({ + await formatExportSpecifier({ type: "ExportSpecifier", local: { type: "Literal", diff --git a/tests/integration/__tests__/help-options.js b/tests/integration/__tests__/help-options.js index 8dd5f09c9..f6d10222e 100644 --- a/tests/integration/__tests__/help-options.js +++ b/tests/integration/__tests__/help-options.js @@ -7,15 +7,15 @@ import { normalizeDetailedOptionMap, } from "../../../src/cli/options/option-map.js"; +const supportInfo = await prettier.getSupportInfo({ + showDeprecated: true, + showUnreleased: true, + showInternal: true, +}); + for (const option of arrayify( { - ...createDetailedOptionMap( - prettier.getSupportInfo({ - showDeprecated: true, - showUnreleased: true, - showInternal: true, - }).options - ), + ...createDetailedOptionMap(supportInfo.options), ...normalizeDetailedOptionMap(constant.options), }, "name" diff --git a/tests/integration/__tests__/schema.js b/tests/integration/__tests__/schema.js index ab6336f17..d70ee3ac0 100644 --- a/tests/integration/__tests__/schema.js +++ b/tests/integration/__tests__/schema.js @@ -1,6 +1,9 @@ import prettier from "prettier-local"; import generateSchema from "../../../scripts/utils/generate-schema.mjs"; -test("schema", () => { - expect(generateSchema(prettier.getSupportInfo().options)).toMatchSnapshot(); +test("schema", async () => { + const { options } = await prettier.getSupportInfo(); + const schema = generateSchema(options); + + expect(schema).toMatchSnapshot(); }); diff --git a/tests/integration/__tests__/support-info.js b/tests/integration/__tests__/support-info.js index eebf8cc0b..b8b9bc4e7 100644 --- a/tests/integration/__tests__/support-info.js +++ b/tests/integration/__tests__/support-info.js @@ -1,16 +1,16 @@ import prettier from "prettier-local"; import runPrettier from "../run-prettier.js"; -test("API getSupportInfo()", () => { - expect(getCoreInfo()).toMatchSnapshot(); +test("API getSupportInfo()", async () => { + expect(await getCoreInfo()).toMatchSnapshot(); }); describe("CLI --support-info", () => { runPrettier("cli", "--support-info").test({ status: 0 }); }); -function getCoreInfo() { - const supportInfo = prettier.getSupportInfo(); +async function getCoreInfo() { + const supportInfo = await prettier.getSupportInfo(); const languages = Object.fromEntries( supportInfo.languages.map(({ name, parsers }) => [name, parsers]) ); diff --git a/tests/integration/run-prettier.js b/tests/integration/run-prettier.js index a13832e79..26eb84468 100644 --- a/tests/integration/run-prettier.js +++ b/tests/integration/run-prettier.js @@ -59,11 +59,11 @@ async function run(dir, args, options) { - Test file `./__tests__/plugin-virtual-directory.js` - Pull request #5819 */ - const originalStatSync = fs.statSync; + const originalStat = fs.promises.stat; jest - .spyOn(fs, "statSync") + .spyOn(fs.promises, "stat") .mockImplementation((filename) => - originalStatSync( + originalStat( path.basename(filename) === "virtualDirectory" ? __filename : filename ) );