From 4d5b645e0e0f76c51a009a9326dfab6f534314a8 Mon Sep 17 00:00:00 2001 From: Georgii Dolzhykov Date: Sat, 6 Aug 2022 19:45:06 +0300 Subject: [PATCH] Support async parser for embedded languages (#13211) Co-authored-by: fisker Cheung --- changelog_unreleased/api/12574.md | 3 +- changelog_unreleased/api/12748.md | 8 +- docs/plugins.md | 74 ++++-- src/language-css/embed.js | 8 +- src/language-html/embed.js | 228 +++++++++--------- src/language-html/syntax-vue.js | 14 +- src/language-js/embed.js | 57 ++--- src/language-js/embed/css.js | 4 +- src/language-js/embed/graphql.js | 4 +- src/language-js/embed/html.js | 9 +- src/language-js/embed/markdown.js | 4 +- src/language-js/printer-estree.js | 1 + src/language-markdown/embed.js | 73 +++--- src/language-markdown/parser-markdown.js | 2 +- src/language-markdown/printer-markdown.js | 1 + src/language-yaml/embed.js | 5 +- src/main/ast-to-doc.js | 42 ++-- src/main/core.js | 6 +- src/main/multiparser.js | 133 ++++++++-- src/main/parser.js | 38 +-- src/utils/front-matter/print.js | 8 +- .../prettier-plugin-async-parser/index.cjs | 34 --- .../prettier-plugin-async-printer/index.cjs | 43 ++++ .../package.json | 2 +- .../plugins/async-parser/async-parser.txt | 1 - .../__snapshots__/jsfmt.spec.js.snap | 8 +- .../plugins/async-printer/async-printer.txt | 1 + .../jsfmt.spec.js | 4 +- .../__snapshots__/jsfmt.spec.js.snap | 40 --- .../with-aync-parser-code-block.md | 7 - .../__snapshots__/jsfmt.spec.js.snap | 30 +++ .../jsfmt.spec.js | 2 +- .../with-aync-printer-code-block.md | 10 + .../__snapshots__/plugin-api-embed.js.snap | 10 + .../integration/__tests__/plugin-api-embed.js | 87 +++++++ 35 files changed, 588 insertions(+), 413 deletions(-) delete mode 100644 tests/config/prettier-plugins/prettier-plugin-async-parser/index.cjs create mode 100644 tests/config/prettier-plugins/prettier-plugin-async-printer/index.cjs rename tests/config/prettier-plugins/{prettier-plugin-async-parser => prettier-plugin-async-printer}/package.json (61%) delete mode 100644 tests/format/misc/plugins/async-parser/async-parser.txt rename tests/format/misc/plugins/{async-parser => async-printer}/__snapshots__/jsfmt.spec.js.snap (72%) create mode 100644 tests/format/misc/plugins/async-printer/async-printer.txt rename tests/format/misc/plugins/{async-parser => async-printer}/jsfmt.spec.js (68%) delete mode 100644 tests/format/misc/plugins/embed-async-parser/__snapshots__/jsfmt.spec.js.snap delete mode 100644 tests/format/misc/plugins/embed-async-parser/with-aync-parser-code-block.md create mode 100644 tests/format/misc/plugins/embed-async-printer/__snapshots__/jsfmt.spec.js.snap rename tests/format/misc/plugins/{embed-async-parser => embed-async-printer}/jsfmt.spec.js (94%) create mode 100644 tests/format/misc/plugins/embed-async-printer/with-aync-printer-code-block.md create mode 100644 tests/integration/__tests__/__snapshots__/plugin-api-embed.js.snap create mode 100644 tests/integration/__tests__/plugin-api-embed.js diff --git a/changelog_unreleased/api/12574.md b/changelog_unreleased/api/12574.md index af75fe135..9353db15d 100644 --- a/changelog_unreleased/api/12574.md +++ b/changelog_unreleased/api/12574.md @@ -1,7 +1,8 @@ -#### [BREAKING] Change public apis to asynchronous (#12574, #12788, #12790 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.formatAST()` returns `Promise` - `prettier.check()` returns `Promise` - `prettier.getSupportInfo()` returns `Promise` - `prettier.resolveConfig.sync` is removed diff --git a/changelog_unreleased/api/12748.md b/changelog_unreleased/api/12748.md index 6f3943463..5252e22a1 100644 --- a/changelog_unreleased/api/12748.md +++ b/changelog_unreleased/api/12748.md @@ -1,7 +1,7 @@ -#### [HIGHLIGHT] Support plugin with async parse function (#12748 by @fisker) +#### [HIGHLIGHT] Support plugins with async parsers (#12748 by @fisker, #13211 by @thorn0 and @fisker) -[`parse` function](https://prettier.io/docs/en/plugins.html#parsers) in plugin can return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) now. +[`parse` function](https://prettier.io/docs/en/plugins.html#parsers) in a plugin can return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) now. - +In order to support async parsers for embedded languages, we had to introduce a breaking change to the plugin API. Namely, the `embed` method of a printer has now to match a completely new signature, incompatible with previous versions. If you're a plugin author and your plugins don't define `embed`, you have nothing to worry about, otherwise see the [docs](/docs/plugins.html#optional-embed) for details. -Note: Since [`embed` functions in plugin](https://prettier.io/docs/en/plugins.html#optional-embed) are still sync, plugins with async parser can't be used to format embed code. +Also, the `preprocess` method of a printer can return a promise now. diff --git a/docs/plugins.md b/docs/plugins.md index 1aadd5f91..61f600f24 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -183,11 +183,14 @@ import { doc } from "prettier"; const { join, line, ifBreak, group } = doc.builders; ``` -The printing process works as follows: +The printing process consists of the following steps: -1. `preprocess(ast: AST, options: object): AST`, if available, is called. It is passed the AST from the _parser_. The AST returned by `preprocess` will be used by Prettier. If `preprocess` is not defined, the AST returned from the _parser_ will be used. -2. Comments are attached to the AST (see _Handling comments in a printer_ for details). -3. A Doc is recursively constructed from the AST. i) `embed(path: AstPath, print, textToDoc, options: object): Doc | null` is called on each AST node. If `embed` returns a Doc, that Doc is used. ii) If `embed` is undefined or returns a falsy value, `print(path: AstPath, options: object, print): Doc` is called on each AST node. +1. **AST preprocessing** (optional). See [`preprocess`](#optional-preprocess). +2. **Comment attachment** (optional). See [Handling comments in a printer](#handling-comments-in-a-printer). +3. **Processing embedded languages** (optional). The [`embed`](#optional-embed) method, if defined, is called for each node, depth-first. While, for performance reasons, the recursion itself is synchronous, `embed` may return asynchronous functions that can call other parsers and printers to compose docs for embedded syntaxes like CSS-in-JS. These returned functions are queued up and sequentially executed before the next step. +4. **Recursive printing**. A doc is recursively constructed from the AST. Starting from the root node: + - If, from the step 3, there is an embedded language doc associated with the current node, this doc is used. + - Otherwise, the `print(path, options, print): Doc` method is called. It composes a doc for the current node, often by printing child nodes using the `print` callback. #### `print` @@ -212,9 +215,9 @@ The `print` function is passed the following parameters: Here’s a simplified example to give an idea of what a typical implementation of `print` looks like: ```js -const { - builders: { group, indent, join, line, softline }, -} = require("prettier").doc; +import { doc } from "prettier"; + +const { group, indent, join, line, softline } = doc.builders; function print(path, options, print) { const node = path.getValue(); @@ -248,42 +251,65 @@ Check out [prettier-python's printer](https://github.com/prettier/prettier-pytho #### (optional) `embed` -The `embed` function is called when the plugin needs to print one language inside another. Examples of this are printing CSS-in-JS or fenced code blocks in Markdown. Its signature is: +A printer can have the `embed` method to print one language inside another. Examples of this are printing CSS-in-JS or fenced code blocks in Markdown. The signature is: ```ts function embed( // Path to the current AST node path: AstPath, - // Print a node with the current printer - print: (selector?: string | number | Array | AstPath) => Doc, - // Parse and print some text using a different parser. - // You should set `options.parser` to specify which parser to use. - textToDoc: (text: string, options: object) => Doc, // Current options - options: object -): Doc | null; + options: Options +): + | (( + // Parses and prints the passed text using a different parser. + // You should set `options.parser` to specify which parser to use. + textToDoc: (text: string, options: Options) => Promise, + // Prints the current node or its descendant node with the current printer + print: ( + selector?: string | number | Array | AstPath + ) => Doc, + // The following two arguments are passed for convenience. + // They're the same `path` and `options` that are passed to `embed`. + path: AstPath, + options: Options + ) => Promise | Doc | undefined) + | Doc + | undefined; ``` -The `embed` function acts like the `print` function, except that it is passed an additional `textToDoc` function, which can be used to render a doc using a different plugin. The `embed` function returns a Doc or a falsy value. If a falsy value is returned, the `print` function is called with the current `path`. If a Doc is returned, that Doc is used in printing and the `print` function is not called. +The `embed` method is similar to the `print` method in that it maps AST nodes to docs, but unlike `print`, it has power to do async work by returning an async function. That function's first parameter, the `textToDoc` async function, can be used to render a doc using a different plugin. -For example, a plugin that had nodes with embedded JavaScript might have the following `embed` function: +If a function returned from `embed` returns a doc or a promise that resolves to a doc, that doc will be used in printing, and the `print` method won’t be called for this node. It's also possible and, in rare situations, might be convenient to return a doc synchronously directly from `embed`, however `textToDoc` and the `print` callback aren’t available at that case. Return a function to get them. + +If `embed` returns `undefined`, or if a function it returned returns `undefined` or a promise that resolves to `undefined`, the node will be printed normally with the `print` method. Same will happen if a returned function throws an error or returns a promise that rejects (e.g., if a parsing error has happened). Set the `PRETTIER_DEBUG` environment variable to a non-empty value if you want Prettier to rethrow these errors. + +For example, a plugin that has nodes with embedded JavaScript might have the following `embed` method: ```js -function embed(path, print, textToDoc, options) { +function embed(path, options) { const node = path.getValue(); if (node.type === "javascript") { - return textToDoc(node.javaScriptText, { ...options, parser: "babel" }); + return async (textToDoc) => { + return [ + "", + ]; + }; } - return false; } ``` +If the [`--embedded-language-formatting`](options.md#embedded-language-formatting) option is set to `off`, the embedding step is entirely skipped, `embed` isn’t called, and all nodes are printed with the `print` method. + #### (optional) `preprocess` -The preprocess function can process the AST from parser before passing into `print` function. +The `preprocess` method can process the AST from the parser before passing it into the `print` method. ```ts -function preprocess(ast: AST, options: object): AST; +function preprocess(ast: AST, options: Options): AST | Promise; ``` #### (optional) `insertPragma` @@ -455,9 +481,9 @@ function isPreviousLineEmpty(text: string, node: N, locStart: (node: N) => nu Since plugins can be resolved using relative paths, when working on one you can do: ```js -const prettier = require("prettier"); +import * as prettier from "prettier"; const code = "(add 1 2)"; -prettier.format(code, { +await prettier.format(code, { parser: "lisp", plugins: ["."], }); diff --git a/src/language-css/embed.js b/src/language-css/embed.js index f4bb7b80b..9501bd2a6 100644 --- a/src/language-css/embed.js +++ b/src/language-css/embed.js @@ -1,12 +1,14 @@ import { hardline } from "../document/builders.js"; import printFrontMatter from "../utils/front-matter/print.js"; -function embed(path, print, textToDoc /*, options */) { +function embed(path) { const node = path.getValue(); if (node.type === "front-matter") { - const doc = printFrontMatter(node, textToDoc); - return doc ? [doc, hardline] : ""; + return async (textToDoc) => { + const doc = await printFrontMatter(node, textToDoc); + return doc ? [doc, hardline] : undefined; + }; } } diff --git a/src/language-html/embed.js b/src/language-html/embed.js index cce5b770c..9a7e76cfc 100644 --- a/src/language-html/embed.js +++ b/src/language-html/embed.js @@ -35,7 +35,7 @@ import { } from "./utils/index.js"; import getNodeContent from "./get-node-content.js"; -function printEmbeddedAttributeValue(node, htmlTextToDoc, options) { +async function printEmbeddedAttributeValue(node, htmlTextToDoc, options) { const isKeyMatched = (patterns) => new RegExp(patterns.join("|")).test(node.fullName); const getValue = () => unescapeQuoteEntities(node.value); @@ -95,7 +95,7 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) { const value = getValue(); if (!value.includes("{{")) { return printExpand( - attributeTextToDoc(value, { + await attributeTextToDoc(value, { parser: "css", __isHTMLStyleAttribute: true, }) @@ -136,22 +136,18 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) { : options.__should_parse_vue_template_with_ts ? "__vue_ts_event_binding" : "__vue_event_binding"; - return printMaybeHug( - attributeTextToDoc(value, { - parser, - }) - ); + return printMaybeHug(await attributeTextToDoc(value, { parser })); } if (isKeyMatched(vueExpressionBindingPatterns)) { return printMaybeHug( - attributeTextToDoc(getValue(), { parser: "__vue_expression" }) + await attributeTextToDoc(getValue(), { parser: "__vue_expression" }) ); } if (isKeyMatched(jsExpressionBindingPatterns)) { return printMaybeHug( - attributeTextToDoc(getValue(), { parser: "__js_expression" }) + await attributeTextToDoc(getValue(), { parser: "__js_expression" }) ); } } @@ -189,11 +185,15 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) { const ngI18nPatterns = ["^i18n(-.+)?$"]; if (isKeyMatched(ngStatementBindingPatterns)) { - return printMaybeHug(ngTextToDoc(getValue(), { parser: "__ng_action" })); + return printMaybeHug( + await ngTextToDoc(getValue(), { parser: "__ng_action" }) + ); } if (isKeyMatched(ngExpressionBindingPatterns)) { - return printMaybeHug(ngTextToDoc(getValue(), { parser: "__ng_binding" })); + return printMaybeHug( + await ngTextToDoc(getValue(), { parser: "__ng_binding" }) + ); } if (isKeyMatched(ngI18nPatterns)) { @@ -206,7 +206,7 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) { if (isKeyMatched(ngDirectiveBindingPatterns)) { return printMaybeHug( - ngTextToDoc(getValue(), { parser: "__ng_directive" }) + await ngTextToDoc(getValue(), { parser: "__ng_directive" }) ); } @@ -224,7 +224,7 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) { "{{", indent([ line, - ngTextToDoc(part, { + await ngTextToDoc(part, { parser: "__ng_interpolation", __isInHtmlInterpolation: true, // to avoid unexpected `}}` }), @@ -245,7 +245,7 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) { return null; } -function embed(path, print, textToDoc, options) { +function embed(path, options) { const node = path.getValue(); switch (node.type) { @@ -261,27 +261,29 @@ function embed(path, print, textToDoc, options) { return; } - const content = getNodeContent(node, options); - let isEmpty = /^\s*$/.test(content); - let doc = ""; - if (!isEmpty) { - doc = textToDoc( - htmlTrimPreserveIndentation(content), - { parser, __embeddedInHtml: true }, - { stripTrailingHardline: true } - ); - isEmpty = doc === ""; - } + return async (textToDoc, print) => { + const content = getNodeContent(node, options); + let isEmpty = /^\s*$/.test(content); + let doc = ""; + if (!isEmpty) { + doc = await textToDoc( + htmlTrimPreserveIndentation(content), + { parser, __embeddedInHtml: true }, + { stripTrailingHardline: true } + ); + isEmpty = doc === ""; + } - return [ - printOpeningTagPrefix(node, options), - group(printOpeningTag(path, options, print)), - isEmpty ? "" : hardline, - doc, - isEmpty ? "" : hardline, - printClosingTag(node, options), - printClosingTagSuffix(node, options), - ]; + return [ + printOpeningTagPrefix(node, options), + group(printOpeningTag(path, options, print)), + isEmpty ? "" : hardline, + doc, + isEmpty ? "" : hardline, + printClosingTag(node, options), + printClosingTagSuffix(node, options), + ]; + }; } break; } @@ -289,60 +291,67 @@ function embed(path, print, textToDoc, options) { if (isScriptLikeTag(node.parent)) { const parser = inferScriptParser(node.parent, options); if (parser) { - const value = - parser === "markdown" - ? dedentString(node.value.replace(/^[^\S\n]*\n/, "")) - : node.value; - const textToDocOptions = { parser, __embeddedInHtml: true }; - if (options.parser === "html" && parser === "babel") { - let sourceType = "script"; - const { attrMap } = node.parent; - if ( - attrMap && - (attrMap.type === "module" || - (attrMap.type === "text/babel" && - attrMap["data-type"] === "module")) - ) { - sourceType = "module"; + return async (textToDoc) => { + const value = + parser === "markdown" + ? dedentString(node.value.replace(/^[^\S\n]*\n/, "")) + : node.value; + const textToDocOptions = { parser, __embeddedInHtml: true }; + if (options.parser === "html" && parser === "babel") { + let sourceType = "script"; + const { attrMap } = node.parent; + if ( + attrMap && + (attrMap.type === "module" || + (attrMap.type === "text/babel" && + attrMap["data-type"] === "module")) + ) { + sourceType = "module"; + } + textToDocOptions.__babelSourceType = sourceType; } - textToDocOptions.__babelSourceType = sourceType; - } - return [ - breakParent, - printOpeningTagPrefix(node, options), - textToDoc(value, textToDocOptions, { - stripTrailingHardline: true, - }), - printClosingTagSuffix(node, options), - ]; + + return [ + breakParent, + printOpeningTagPrefix(node, options), + await textToDoc(value, textToDocOptions, { + stripTrailingHardline: true, + }), + printClosingTagSuffix(node, options), + ]; + }; } } else if (node.parent.type === "interpolation") { - const textToDocOptions = { - __isInHtmlInterpolation: true, // to avoid unexpected `}}` - __embeddedInHtml: true, + return async (textToDoc) => { + const textToDocOptions = { + __isInHtmlInterpolation: true, // to avoid unexpected `}}` + __embeddedInHtml: true, + }; + if (options.parser === "angular") { + textToDocOptions.parser = "__ng_interpolation"; + textToDocOptions.trailingComma = "none"; + } else if (options.parser === "vue") { + textToDocOptions.parser = + options.__should_parse_vue_template_with_ts + ? "__vue_ts_expression" + : "__vue_expression"; + } else { + textToDocOptions.parser = "__js_expression"; + } + + return [ + indent([ + line, + await textToDoc(node.value, textToDocOptions, { + stripTrailingHardline: true, + }), + ]), + node.parent.next && + needsToBorrowPrevClosingTagEndMarker(node.parent.next) + ? " " + : line, + ]; }; - if (options.parser === "angular") { - textToDocOptions.parser = "__ng_interpolation"; - textToDocOptions.trailingComma = "none"; - } else if (options.parser === "vue") { - textToDocOptions.parser = options.__should_parse_vue_template_with_ts - ? "__vue_ts_expression" - : "__vue_expression"; - } else { - textToDocOptions.parser = "__js_expression"; - } - return [ - indent([ - line, - textToDoc(node.value, textToDocOptions, { - stripTrailingHardline: true, - }), - ]), - node.parent.next && - needsToBorrowPrevClosingTagEndMarker(node.parent.next) - ? " " - : line, - ]; } break; } @@ -378,33 +387,34 @@ function embed(path, print, textToDoc, options) { } } - const embeddedAttributeValueDoc = printEmbeddedAttributeValue( - node, - (code, opts) => - // strictly prefer single quote to avoid unnecessary html entity escape - textToDoc( - code, - { __isInHtmlAttribute: true, __embeddedInHtml: true, ...opts }, - { stripTrailingHardline: true } - ), - options - ); - if (embeddedAttributeValueDoc) { - return [ - node.rawName, - '="', - group( - mapDoc(embeddedAttributeValueDoc, (doc) => - typeof doc === "string" ? doc.replace(/"/g, """) : doc - ) - ), - '"', - ]; - } - break; + return async (textToDoc) => { + const embeddedAttributeValueDoc = await printEmbeddedAttributeValue( + node, + (code, opts) => + // strictly prefer single quote to avoid unnecessary html entity escape + textToDoc( + code, + { __isInHtmlAttribute: true, __embeddedInHtml: true, ...opts }, + { stripTrailingHardline: true } + ), + options + ); + if (embeddedAttributeValueDoc) { + return [ + node.rawName, + '="', + group( + mapDoc(embeddedAttributeValueDoc, (doc) => + typeof doc === "string" ? doc.replace(/"/g, """) : doc + ) + ), + '"', + ]; + } + }; } case "front-matter": - return printFrontMatter(node, textToDoc); + return (textToDoc) => printFrontMatter(node, textToDoc); } } diff --git a/src/language-html/syntax-vue.js b/src/language-html/syntax-vue.js index 82395f889..acddc785f 100644 --- a/src/language-html/syntax-vue.js +++ b/src/language-html/syntax-vue.js @@ -6,11 +6,11 @@ import { group } from "../document/builders.js"; * v-for="(..., ...) in ..." * v-for="(..., ...) of ..." */ -function printVueFor(value, textToDoc) { +async function printVueFor(value, attributeTextToDoc) { const { left, operator, right } = parseVueFor(value); return [ group( - textToDoc(`function _(${left}) {}`, { + await attributeTextToDoc(`function _(${left}) {}`, { parser: "babel", __isVueForBindingLeft: true, }) @@ -18,11 +18,7 @@ function printVueFor(value, textToDoc) { " ", operator, " ", - textToDoc( - right, - { parser: "__js_expression" }, - { stripTrailingHardline: true } - ), + await attributeTextToDoc(right, { parser: "__js_expression" }), ]; } @@ -72,8 +68,8 @@ function parseVueFor(value) { }; } -function printVueBindings(value, textToDoc) { - return textToDoc(`function _(${value}) {}`, { +function printVueBindings(value, attributeTextToDoc) { + return attributeTextToDoc(`function _(${value}) {}`, { parser: "babel", __isVueBindings: true, }); diff --git a/src/language-js/embed.js b/src/language-js/embed.js index 1d4381a41..9c064f367 100644 --- a/src/language-js/embed.js +++ b/src/language-js/embed.js @@ -6,36 +6,9 @@ import { import formatMarkdown from "./embed/markdown.js"; import formatCss from "./embed/css.js"; import formatGraphql from "./embed/graphql.js"; -import formatHtml from "./embed/html.js"; +import { formatHtml, formatAngular } from "./embed/html.js"; -function getLanguage(path) { - if ( - isStyledJsx(path) || - isStyledComponents(path) || - isCssProp(path) || - isAngularComponentStyles(path) - ) { - return "css"; - } - - if (isGraphQL(path)) { - return "graphql"; - } - - if (isHtml(path)) { - return "html"; - } - - if (isAngularComponentTemplate(path)) { - return "angular"; - } - - if (isMarkdown(path)) { - return "markdown"; - } -} - -function embed(path, print, textToDoc, options) { +function embed(path) { const node = path.getValue(); if ( @@ -47,25 +20,29 @@ function embed(path, print, textToDoc, options) { return; } - const language = getLanguage(path); - if (!language) { - return; + if ( + isStyledJsx(path) || + isStyledComponents(path) || + isCssProp(path) || + isAngularComponentStyles(path) + ) { + return formatCss; } - if (language === "markdown") { - return formatMarkdown(path, print, textToDoc); + if (isGraphQL(path)) { + return formatGraphql; } - if (language === "css") { - return formatCss(path, print, textToDoc); + if (isHtml(path)) { + return formatHtml; } - if (language === "graphql") { - return formatGraphql(path, print, textToDoc); + if (isAngularComponentTemplate(path)) { + return formatAngular; } - if (language === "html" || language === "angular") { - return formatHtml(path, print, textToDoc, options, { parser: language }); + if (isMarkdown(path)) { + return formatMarkdown; } } diff --git a/src/language-js/embed/css.js b/src/language-js/embed/css.js index 8f1c1a1e5..d43257578 100644 --- a/src/language-js/embed/css.js +++ b/src/language-js/embed/css.js @@ -3,7 +3,7 @@ import { indent, hardline, softline } from "../../document/builders.js"; import { mapDoc, replaceEndOfLine, cleanDoc } from "../../document/utils.js"; import { printTemplateExpressions } from "../print/template-literal.js"; -function format(path, print, textToDoc) { +async function format(textToDoc, print, path /*, options*/) { const node = path.getValue(); // Get full template literal with expressions replaced by placeholders @@ -20,7 +20,7 @@ function format(path, print, textToDoc) { currVal, "" ); - const doc = textToDoc( + const doc = await textToDoc( text, { parser: "scss" }, { stripTrailingHardline: true } diff --git a/src/language-js/embed/graphql.js b/src/language-js/embed/graphql.js index d8a484341..8c78df012 100644 --- a/src/language-js/embed/graphql.js +++ b/src/language-js/embed/graphql.js @@ -4,7 +4,7 @@ import { printTemplateExpressions, } from "../print/template-literal.js"; -function format(path, print, textToDoc) { +async function format(textToDoc, print, path /*, options*/) { const node = path.getValue(); const numQuasis = node.quasis.length; @@ -46,7 +46,7 @@ function format(path, print, textToDoc) { if (commentsAndWhitespaceOnly) { doc = printGraphqlComments(lines); } else { - doc = textToDoc( + doc = await textToDoc( text, { parser: "graphql" }, { stripTrailingHardline: true } diff --git a/src/language-js/embed/html.js b/src/language-js/embed/html.js index e2b9e4028..c9582495f 100644 --- a/src/language-js/embed/html.js +++ b/src/language-js/embed/html.js @@ -7,7 +7,7 @@ import { // The counter is needed to distinguish nested embeds. let htmlTemplateLiteralCounter = 0; -function format(path, print, textToDoc, options, { parser }) { +async function format(textToDoc, print, path, options, parser) { const node = path.getValue(); const counter = htmlTemplateLiteralCounter; htmlTemplateLiteralCounter = (htmlTemplateLiteralCounter + 1) >>> 0; @@ -30,7 +30,7 @@ function format(path, print, textToDoc, options, { parser }) { const placeholderRegex = new RegExp(composePlaceholder("(\\d+)"), "g"); let topLevelCount = 0; - const doc = textToDoc( + const doc = await textToDoc( text, { parser, @@ -93,4 +93,7 @@ function format(path, print, textToDoc, options, { parser }) { ]); } -export default format; +export const formatHtml = (textToDoc, print, path, options) => + format(textToDoc, print, path, options, "html"); +export const formatAngular = (textToDoc, print, path, options) => + format(textToDoc, print, path, options, "angular"); diff --git a/src/language-js/embed/markdown.js b/src/language-js/embed/markdown.js index 636ffda39..7257e00fa 100644 --- a/src/language-js/embed/markdown.js +++ b/src/language-js/embed/markdown.js @@ -6,7 +6,7 @@ import { } from "../../document/builders.js"; import { escapeTemplateCharacters } from "../print/template-literal.js"; -function format(path, print, textToDoc) { +async function format(textToDoc, print, path /*, options*/) { const node = path.getValue(); let text = node.quasis[0].value.raw.replace( /((?:\\\\)*)\\`/g, @@ -18,7 +18,7 @@ function format(path, print, textToDoc) { text = text.replace(new RegExp(`^${indentation}`, "gm"), ""); } const doc = escapeTemplateCharacters( - textToDoc( + await textToDoc( text, { parser: "markdown", __inJsTemplate: true }, { stripTrailingHardline: true } diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index b984d8e9c..f258018fb 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -866,6 +866,7 @@ const printer = { remaining: handleComments.handleRemainingComment, }, getCommentChildNodes: handleComments.getCommentChildNodes, + isNode: (node) => typeof node.type === "string", }; export default printer; diff --git a/src/language-markdown/embed.js b/src/language-markdown/embed.js index 1e19f5121..ad8c295be 100644 --- a/src/language-markdown/embed.js +++ b/src/language-markdown/embed.js @@ -7,45 +7,49 @@ import { replaceEndOfLine } from "../document/utils.js"; import printFrontMatter from "../utils/front-matter/print.js"; import { getFencedCodeBlockValue } from "./utils.js"; -function embed(path, print, textToDoc, options) { +function embed(path, options) { const node = path.getValue(); if (node.type === "code" && node.lang !== null) { const parser = inferParserByLanguage(node.lang, options); if (parser) { - const styleUnit = options.__inJsTemplate ? "~" : "`"; - const style = styleUnit.repeat( - Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1) - ); - const newOptions = { parser }; - if (node.lang === "tsx") { - newOptions.filepath = "dummy.tsx"; - } - const doc = textToDoc( - getFencedCodeBlockValue(node, options.originalText), - newOptions, - { stripTrailingHardline: true } - ); - return markAsRoot([ - style, - node.lang, - node.meta ? " " + node.meta : "", - hardline, - replaceEndOfLine(doc), - hardline, - style, - ]); + return async (textToDoc) => { + const styleUnit = options.__inJsTemplate ? "~" : "`"; + const style = styleUnit.repeat( + Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1) + ); + const newOptions = { parser }; + if (node.lang === "tsx") { + newOptions.filepath = "dummy.tsx"; + } + + const doc = await textToDoc( + getFencedCodeBlockValue(node, options.originalText), + newOptions, + { stripTrailingHardline: true } + ); + + return markAsRoot([ + style, + node.lang, + node.meta ? " " + node.meta : "", + hardline, + replaceEndOfLine(doc), + hardline, + style, + ]); + }; } } switch (node.type) { case "front-matter": - return printFrontMatter(node, textToDoc); + return (textToDoc) => printFrontMatter(node, textToDoc); // MDX case "importExport": - return [ - textToDoc( + return async (textToDoc) => [ + await textToDoc( node.value, { parser: "babel" }, { stripTrailingHardline: true } @@ -53,14 +57,15 @@ function embed(path, print, textToDoc, options) { hardline, ]; case "jsx": - return textToDoc( - `<$>${node.value}`, - { - parser: "__js_expression", - rootMarker: "mdx", - }, - { stripTrailingHardline: true } - ); + return (textToDoc) => + textToDoc( + `<$>${node.value}`, + { + parser: "__js_expression", + rootMarker: "mdx", + }, + { stripTrailingHardline: true } + ); } return null; diff --git a/src/language-markdown/parser-markdown.js b/src/language-markdown/parser-markdown.js index 7e75d5c65..8fab84061 100644 --- a/src/language-markdown/parser-markdown.js +++ b/src/language-markdown/parser-markdown.js @@ -40,7 +40,7 @@ function createParse({ isMDX }) { .use(isMDX ? htmlToJsx : identity) .use(wikiLink) .use(looseItems); - return processor.runSync(processor.parse(text)); + return processor.run(processor.parse(text)); }; } diff --git a/src/language-markdown/printer-markdown.js b/src/language-markdown/printer-markdown.js index adfd7df42..75aea893a 100644 --- a/src/language-markdown/printer-markdown.js +++ b/src/language-markdown/printer-markdown.js @@ -916,6 +916,7 @@ const printer = { massageAstNode: clean, hasPrettierIgnore, insertPragma, + isNode: (obj) => Boolean(obj.type), }; export default printer; diff --git a/src/language-yaml/embed.js b/src/language-yaml/embed.js index babe99d24..56b05884e 100644 --- a/src/language-yaml/embed.js +++ b/src/language-yaml/embed.js @@ -1,4 +1,4 @@ -function embed(path, print, textToDoc, options) { +function embed(path, options) { const node = path.getValue(); // Try to format `.prettierrc`, `.stylelintrc`, and `.lintstagedrc` as `json` first @@ -7,7 +7,8 @@ function embed(path, print, textToDoc, options) { options.filepath && /(?:[/\\]|^)\.(?:prettier|stylelint|lintstaged)rc$/.test(options.filepath) ) { - return textToDoc(options.originalText, { ...options, parser: "json" }); + return (textToDoc) => + textToDoc(options.originalText, { ...options, parser: "json" }); } } diff --git a/src/main/ast-to-doc.js b/src/main/ast-to-doc.js index 589435d10..2940e5084 100644 --- a/src/main/ast-to-doc.js +++ b/src/main/ast-to-doc.js @@ -2,7 +2,7 @@ import AstPath from "../common/ast-path.js"; import { hardline, addAlignmentToDoc } from "../document/builders.js"; import { propagateBreaks } from "../document/utils.js"; import { printComments } from "./comments.js"; -import { printSubtree } from "./multiparser.js"; +import { printEmbeddedLanguages } from "./multiparser.js"; /** * Takes an abstract syntax tree (AST) and recursively converts it to a @@ -25,17 +25,28 @@ import { printSubtree } from "./multiparser.js"; * state of the recursion. It is called "path", because it represents * the path to the current node through the Abstract Syntax Tree. */ -function printAstToDoc(ast, options, alignmentSize = 0) { +async function printAstToDoc(ast, options, alignmentSize = 0) { const { printer } = options; if (printer.preprocess) { - ast = printer.preprocess(ast, options); + ast = await printer.preprocess(ast, options); } const cache = new Map(); const path = new AstPath(ast); - let doc = mainPrint(); + const embeds = new Map(); + await printEmbeddedLanguages(path, mainPrint, options, printAstToDoc, embeds); + + // Only the root call of the print method is awaited. + // This is done to make things simpler for plugins that don't use recursive printing. + let doc = await callPluginPrintFunction( + path, + options, + mainPrint, + undefined, + embeds + ); if (alignmentSize > 0) { // Add a hardline to make the indents take effect @@ -69,7 +80,7 @@ function printAstToDoc(ast, options, alignmentSize = 0) { return cache.get(value); } - const doc = callPluginPrintFunction(path, options, mainPrint, args); + const doc = callPluginPrintFunction(path, options, mainPrint, args, embeds); if (shouldCache) { cache.set(value, doc); @@ -101,7 +112,7 @@ function printPrettierIgnoredNode(node, options) { return { doc: originalText.slice(start, end), printedComments }; } -function callPluginPrintFunction(path, options, printPath, args) { +function callPluginPrintFunction(path, options, printPath, args, embeds) { const node = path.getValue(); const { printer } = options; @@ -111,23 +122,10 @@ function callPluginPrintFunction(path, options, printPath, args) { // Escape hatch if (printer.hasPrettierIgnore && printer.hasPrettierIgnore(path)) { ({ doc, printedComments } = printPrettierIgnoredNode(node, options)); + } else if (embeds.has(node)) { + doc = embeds.get(node); } else { - if (node) { - try { - // Potentially switch to a different parser - doc = printSubtree(path, printPath, options, printAstToDoc); - } catch (error) { - /* istanbul ignore if */ - if (process.env.PRETTIER_DEBUG) { - throw error; - } - // Continue with current parser - } - } - - if (!doc) { - doc = printer.print(path, options, printPath, args); - } + doc = printer.print(path, options, printPath, args); } // We let JSXElement print its comments itself because it adds () around diff --git a/src/main/core.js b/src/main/core.js index a3da2d9c1..e71a23d52 100644 --- a/src/main/core.js +++ b/src/main/core.js @@ -47,7 +47,7 @@ async function coreFormat(originalText, opts, addAlignmentSize = 0) { } const astComments = attachComments(text, ast, opts); - const doc = printAstToDoc(ast, opts, addAlignmentSize); + const doc = await printAstToDoc(ast, opts, addAlignmentSize); const result = printDocToString(doc, opts); @@ -334,9 +334,9 @@ const prettier = { return parsed; }, - formatAST(ast, options) { + async formatAST(ast, options) { options = normalizeOptions(options); - const doc = printAstToDoc(ast, options); + const doc = await printAstToDoc(ast, options); return printDocToString(doc, options); }, diff --git a/src/main/multiparser.js b/src/main/multiparser.js index 58d0e4348..13ef2f3b1 100644 --- a/src/main/multiparser.js +++ b/src/main/multiparser.js @@ -1,27 +1,118 @@ import { stripTrailingHardline } from "../document/utils.js"; import { normalize } from "./options.js"; import { ensureAllCommentsPrinted, attach } from "./comments.js"; -import { parseSync } from "./parser.js"; +import { parse } from "./parser.js"; -function printSubtree(path, print, options, printAstToDoc) { - if (options.printer.embed && options.embeddedLanguageFormatting === "auto") { - return options.printer.embed( - path, - print, - (text, partialNextOptions, textToDocOptions) => - textToDoc( - text, - partialNextOptions, - options, - printAstToDoc, - textToDocOptions - ), - options +async function printEmbeddedLanguages( + /** @type {import("../common/ast-path").default} */ path, + genericPrint, + options, + printAstToDoc, + embeds +) { + const { printer } = options; + + if (!printer.embed || options.embeddedLanguageFormatting !== "auto") { + return; + } + + if (printer.embed.length > 2) { + throw new Error( + "printer.embed has too many parameters. The API changed in Prettier v3. Please update your plugin. See https://prettier.io/docs/en/plugins.html#optional-embed" ); } + + const isNode = + (printer.isNode ?? printer.canAttachComment)?.bind(printer) ?? (() => true); + const hasPrettierIgnore = + printer.hasPrettierIgnore?.bind(printer) ?? (() => false); + const ignoredProperties = printer.massageAstNode?.ignoredProperties; + + const embedCallResults = []; + + recurse(); + + const originalPathStack = path.stack; + + for (const { print, node, pathStack } of embedCallResults) { + try { + path.stack = pathStack; + const doc = await print(textToDocForEmbed, genericPrint, path, options); + + if (doc) { + embeds.set(node, doc); + } + } catch (error) { + /* istanbul ignore if */ + if (process.env.PRETTIER_DEBUG) { + throw error; + } + } + } + + path.stack = originalPathStack; + + function textToDocForEmbed(text, partialNextOptions, textToDocOptions) { + return textToDoc( + text, + partialNextOptions, + options, + printAstToDoc, + textToDocOptions + ); + } + + function recurse() { + const node = path.getValue(); + + if ( + node === null || + typeof node !== "object" || + !isNode(node) || + hasPrettierIgnore(path) + ) { + return; + } + + for (const key in node) { + if ( + Object.prototype.hasOwnProperty.call(node, key) && + !ignoredProperties?.has(key) + ) { + if (Array.isArray(node[key])) { + path.each(recurse, key); + } else { + path.call(recurse, key); + } + } + } + + const result = printer.embed(path, options); + + if (!result) { + return; + } + + if (typeof result === "function") { + embedCallResults.push({ + print: result, + node, + pathStack: [...path.stack], + }); + return; + } + + if (process.env.PRETTIER_DEBUG && typeof result.then === "function") { + throw new Error( + "`embed` should return an async function instead of Promise." + ); + } + + embeds.set(node, result); + } } -function textToDoc( +async function textToDoc( text, partialNextOptions, parentOptions, @@ -39,13 +130,9 @@ function textToDoc( { passThrough: true } ); - const result = parseSync(text, nextOptions); + const result = await parse(text, nextOptions); const { ast } = result; - if (typeof ast?.then === "function") { - throw new TypeError("async parse is not supported in embed"); - } - text = result.text; const astComments = ast.comments; @@ -56,7 +143,7 @@ function textToDoc( // @ts-expect-error -- Casting to `unique symbol` isn't allowed in JSDoc comment nextOptions[Symbol.for("tokens")] = ast.tokens || []; - const doc = printAstToDoc(ast, nextOptions); + const doc = await printAstToDoc(ast, nextOptions); ensureAllCommentsPrinted(astComments); if (shouldStripTrailingHardline) { @@ -72,4 +159,4 @@ function textToDoc( return doc; } -export { printSubtree }; +export { printEmbeddedLanguages }; diff --git a/src/main/parser.js b/src/main/parser.js index f84adfbcf..6e24e4f52 100644 --- a/src/main/parser.js +++ b/src/main/parser.js @@ -102,40 +102,4 @@ function handleParseError(error, text) { throw error; } -// TODO: Remove this when we allow async parser in embed -function parseSync(originalText, opts) { - const parsers = getParsers(opts); - - // Create a new object {parserName: parseFn}. Uses defineProperty() to only call - // the parsers getters when actually calling the parser `parse` function. - const parsersForCustomParserApi = Object.defineProperties( - {}, - Object.fromEntries( - Object.keys(parsers).map((parserName) => [ - parserName, - { - enumerable: true, - get() { - return parsers[parserName].parse; - }, - }, - ]) - ) - ); - - const parser = resolveParser(opts, parsers); - const text = parser.preprocess - ? parser.preprocess(originalText, opts) - : originalText; - - let ast; - try { - ast = parser.parse(text, parsersForCustomParserApi, opts); - } catch (error) { - handleParseError(error, originalText); - } - - return { text, ast }; -} - -export { parse, parseSync, resolveParser }; +export { parse, resolveParser }; diff --git a/src/utils/front-matter/print.js b/src/utils/front-matter/print.js index fad3f94be..c4c3a58de 100644 --- a/src/utils/front-matter/print.js +++ b/src/utils/front-matter/print.js @@ -1,10 +1,14 @@ import { hardline, markAsRoot } from "../../document/builders.js"; -function print(node, textToDoc) { +async function print(node, textToDoc) { if (node.lang === "yaml") { const value = node.value.trim(); const doc = value - ? textToDoc(value, { parser: "yaml" }, { stripTrailingHardline: true }) + ? await textToDoc( + value, + { parser: "yaml" }, + { stripTrailingHardline: true } + ) : ""; return markAsRoot([ node.startDelimiter, diff --git a/tests/config/prettier-plugins/prettier-plugin-async-parser/index.cjs b/tests/config/prettier-plugins/prettier-plugin-async-parser/index.cjs deleted file mode 100644 index d6ce91c51..000000000 --- a/tests/config/prettier-plugins/prettier-plugin-async-parser/index.cjs +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; - -const name = "async-parser"; - -module.exports = { - languages: [ - { - name, - parsers: [name], - }, - ], - parsers: { - [name]: { - parse: (text) => Promise.resolve({ text, isAsyncParserPluginAst: true }), - astFormat: name, - locStart() {}, - locEnd() {}, - }, - }, - printers: { - [name]: { - print(path) { - if (!path.getNode().isAsyncParserPluginAst) { - throw new Error("Unexpected parse result."); - } - - return "Formatted by async-parser plugin"; - }, - massageAstNode() { - return { text: "AST text value placeholder" }; - }, - }, - }, -}; diff --git a/tests/config/prettier-plugins/prettier-plugin-async-printer/index.cjs b/tests/config/prettier-plugins/prettier-plugin-async-printer/index.cjs new file mode 100644 index 000000000..305002491 --- /dev/null +++ b/tests/config/prettier-plugins/prettier-plugin-async-printer/index.cjs @@ -0,0 +1,43 @@ +"use strict"; + +const name = "async-printer"; + +module.exports = { + languages: [ + { + name, + parsers: [name], + }, + ], + parsers: { + [name]: { + parse: (text) => Promise.resolve(JSON.parse(text)), + astFormat: name, + locStart() {}, + locEnd() {}, + }, + }, + printers: { + [name]: { + preprocess(ast) { + return Promise.resolve({ + ...ast, + preprocess: ast.preprocess?.toLowerCase() ?? "", + }); + }, + print(path) { + const ast = path.getValue(); + // print may be async if not used recursively + return Promise.resolve( + JSON.stringify({ + ...ast, + print: ast.print?.replace(/\s+/g, " ") ?? "", + }) + ); + }, + massageAstNode() { + return { text: "AST text value placeholder" }; + }, + }, + }, +}; diff --git a/tests/config/prettier-plugins/prettier-plugin-async-parser/package.json b/tests/config/prettier-plugins/prettier-plugin-async-printer/package.json similarity index 61% rename from tests/config/prettier-plugins/prettier-plugin-async-parser/package.json rename to tests/config/prettier-plugins/prettier-plugin-async-printer/package.json index a9a1ccd2b..495fbaaf3 100644 --- a/tests/config/prettier-plugins/prettier-plugin-async-parser/package.json +++ b/tests/config/prettier-plugins/prettier-plugin-async-printer/package.json @@ -1,6 +1,6 @@ { "private": true, - "name": "prettier-plugin-async-parser", + "name": "prettier-plugin-async-printer", "version": "1.0.0", "main": "./index.cjs" } diff --git a/tests/format/misc/plugins/async-parser/async-parser.txt b/tests/format/misc/plugins/async-parser/async-parser.txt deleted file mode 100644 index 951b30c58..000000000 --- a/tests/format/misc/plugins/async-parser/async-parser.txt +++ /dev/null @@ -1 +0,0 @@ -A text will format as "Formatted by async-parser plugin". diff --git a/tests/format/misc/plugins/async-parser/__snapshots__/jsfmt.spec.js.snap b/tests/format/misc/plugins/async-printer/__snapshots__/jsfmt.spec.js.snap similarity index 72% rename from tests/format/misc/plugins/async-parser/__snapshots__/jsfmt.spec.js.snap rename to tests/format/misc/plugins/async-printer/__snapshots__/jsfmt.spec.js.snap index e2efbc9ed..757f0655e 100644 --- a/tests/format/misc/plugins/async-parser/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/misc/plugins/async-printer/__snapshots__/jsfmt.spec.js.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`async-parser.txt format 1`] = ` +exports[`async-printer.txt format 1`] = ` ====================================options===================================== -parsers: ["async-parser"] +parsers: ["async-printer"] printWidth: 80 | printWidth =====================================input====================================== -A text will format as "Formatted by async-parser plugin". +{ "preprocess": "Lorem Ipsum", "print": "Lorem Ipsum" } =====================================output===================================== -Formatted by async-parser plugin +{"preprocess":"lorem ipsum","print":"Lorem Ipsum"} ================================================================================ `; diff --git a/tests/format/misc/plugins/async-printer/async-printer.txt b/tests/format/misc/plugins/async-printer/async-printer.txt new file mode 100644 index 000000000..493f5ee42 --- /dev/null +++ b/tests/format/misc/plugins/async-printer/async-printer.txt @@ -0,0 +1 @@ +{ "preprocess": "Lorem Ipsum", "print": "Lorem Ipsum" } diff --git a/tests/format/misc/plugins/async-parser/jsfmt.spec.js b/tests/format/misc/plugins/async-printer/jsfmt.spec.js similarity index 68% rename from tests/format/misc/plugins/async-parser/jsfmt.spec.js rename to tests/format/misc/plugins/async-printer/jsfmt.spec.js index a4d6b23b9..ea2572b2d 100644 --- a/tests/format/misc/plugins/async-parser/jsfmt.spec.js +++ b/tests/format/misc/plugins/async-printer/jsfmt.spec.js @@ -3,7 +3,7 @@ import createEsmUtils from "esm-utils"; const { require } = createEsmUtils(import.meta); const plugins = [ - require("../../../../config/prettier-plugins/prettier-plugin-async-parser/index.cjs"), + require("../../../../config/prettier-plugins/prettier-plugin-async-printer/index.cjs"), ]; -run_spec(import.meta, ["async-parser"], { plugins }); +run_spec(import.meta, ["async-printer"], { plugins }); diff --git a/tests/format/misc/plugins/embed-async-parser/__snapshots__/jsfmt.spec.js.snap b/tests/format/misc/plugins/embed-async-parser/__snapshots__/jsfmt.spec.js.snap deleted file mode 100644 index 151c0ea16..000000000 --- a/tests/format/misc/plugins/embed-async-parser/__snapshots__/jsfmt.spec.js.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`async-parser.txt format 1`] = ` -====================================options===================================== -parsers: ["async-parser"] -printWidth: 80 - | printWidth -=====================================input====================================== -A text will format as "Formatted by async-parser plugin". - -=====================================output===================================== -Formatted by async-parser plugin -================================================================================ -`; - -exports[`with-aync-parser-code-block.md format 1`] = ` -====================================options===================================== -parsers: ["markdown"] -printWidth: 80 - | printWidth -=====================================input====================================== -\`\`\`async-parser -Since we don't allow async parser in embed format, so this will not be changed. -\`\`\` - -\`\`\`uppercase-rocks -This text should be uppercased. -\`\`\` - -=====================================output===================================== -\`\`\`async-parser -Since we don't allow async parser in embed format, so this will not be changed. -\`\`\` - -\`\`\`uppercase-rocks -THIS TEXT SHOULD BE UPPERCASED. -\`\`\` - -================================================================================ -`; diff --git a/tests/format/misc/plugins/embed-async-parser/with-aync-parser-code-block.md b/tests/format/misc/plugins/embed-async-parser/with-aync-parser-code-block.md deleted file mode 100644 index 66dd689df..000000000 --- a/tests/format/misc/plugins/embed-async-parser/with-aync-parser-code-block.md +++ /dev/null @@ -1,7 +0,0 @@ -```async-parser -Since we don't allow async parser in embed format, so this will not be changed. -``` - -```uppercase-rocks -This text should be uppercased. -``` diff --git a/tests/format/misc/plugins/embed-async-printer/__snapshots__/jsfmt.spec.js.snap b/tests/format/misc/plugins/embed-async-printer/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000..74a27f62c --- /dev/null +++ b/tests/format/misc/plugins/embed-async-printer/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`with-aync-printer-code-block.md format 1`] = ` +====================================options===================================== +parsers: ["markdown"] +printWidth: 80 + | printWidth +=====================================input====================================== +\`\`\`async-printer +{ + "preprocess": "This text will be lowercased.", + "print": "This text will be whitespace-normalized." +} +\`\`\` + +\`\`\`uppercase-rocks +This text should be uppercased. +\`\`\` + +=====================================output===================================== +\`\`\`async-printer +{"preprocess":"this text will be lowercased.","print":"This text will be whitespace-normalized."} +\`\`\` + +\`\`\`uppercase-rocks +THIS TEXT SHOULD BE UPPERCASED. +\`\`\` + +================================================================================ +`; diff --git a/tests/format/misc/plugins/embed-async-parser/jsfmt.spec.js b/tests/format/misc/plugins/embed-async-printer/jsfmt.spec.js similarity index 94% rename from tests/format/misc/plugins/embed-async-parser/jsfmt.spec.js rename to tests/format/misc/plugins/embed-async-printer/jsfmt.spec.js index 86124ea72..149bb3b47 100644 --- a/tests/format/misc/plugins/embed-async-parser/jsfmt.spec.js +++ b/tests/format/misc/plugins/embed-async-printer/jsfmt.spec.js @@ -1,6 +1,6 @@ const plugins = await Promise.all( [ - "../../../../config/prettier-plugins/prettier-plugin-async-parser/index.cjs", + "../../../../config/prettier-plugins/prettier-plugin-async-printer/index.cjs", "../../../../config/prettier-plugins/prettier-plugin-uppercase-rocks/index.js", ].map(async (plugin) => (await import(plugin)).default) ); diff --git a/tests/format/misc/plugins/embed-async-printer/with-aync-printer-code-block.md b/tests/format/misc/plugins/embed-async-printer/with-aync-printer-code-block.md new file mode 100644 index 000000000..70a26480d --- /dev/null +++ b/tests/format/misc/plugins/embed-async-printer/with-aync-printer-code-block.md @@ -0,0 +1,10 @@ +```async-printer +{ + "preprocess": "This text will be lowercased.", + "print": "This text will be whitespace-normalized." +} +``` + +```uppercase-rocks +This text should be uppercased. +``` diff --git a/tests/integration/__tests__/__snapshots__/plugin-api-embed.js.snap b/tests/integration/__tests__/__snapshots__/plugin-api-embed.js.snap new file mode 100644 index 000000000..5df728ade --- /dev/null +++ b/tests/integration/__tests__/__snapshots__/plugin-api-embed.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`embed API in plugins with new signature should work 1`] = ` +"abcdef +{ "foo": "bar" } +1234 +" +`; + +exports[`embed API in plugins with outdated signature should cause error 1`] = `"printer.embed has too many parameters. The API changed in Prettier v3. Please update your plugin. See https://prettier.io/docs/en/plugins.html#optional-embed"`; diff --git a/tests/integration/__tests__/plugin-api-embed.js b/tests/integration/__tests__/plugin-api-embed.js new file mode 100644 index 000000000..be582658f --- /dev/null +++ b/tests/integration/__tests__/plugin-api-embed.js @@ -0,0 +1,87 @@ +import prettier from "../../config/prettier-entry.js"; + +const { + doc: { + builders: { hardline }, + }, + format, +} = prettier; + +function makePlugin(withOutdatedApi = false) { + return { + parsers: { + // parsers leak across tests, so the name needs to be unique + [withOutdatedApi ? "baz-parser-outdated" : "baz-parser"]: { + parse: (text) => ({ + type: "root", + lines: text + .trim() + .split("\n") + .map((line) => ({ + type: line.startsWith("{") ? "json" : "plain", + text: line, + })), + }), + astFormat: "baz-ast", + }, + }, + printers: { + "baz-ast": { + print: (path, options, print) => { + const { type, text } = path.getValue(); + switch (type) { + case "root": + return path.map(print, "lines"); + case "json": + case "plain": + return [text, hardline]; + } + }, + embed: withOutdatedApi + ? (path, print, textToDoc) => { + const { type, text } = path.getValue(); + if (type === "json") { + return textToDoc(text, { + parser: "json", + printWidth: Number.POSITIVE_INFINITY, + }); + } + } + : (path) => { + const { type, text } = path.getValue(); + if (type === "json") { + return (textToDoc) => + textToDoc(text, { + parser: "json", + printWidth: Number.POSITIVE_INFINITY, + }); + } + }, + }, + }, + }; +} + +const input = `abcdef +{ "foo": "bar"} +1234`; + +describe("embed API in plugins", () => { + test("with new signature should work", async () => { + expect( + await format(input, { + plugins: [makePlugin()], + parser: "baz-parser", + }) + ).toMatchSnapshot(); + }); + + test("with outdated signature should cause error", async () => { + await expect( + format(input, { + plugins: [makePlugin(true)], + parser: "baz-parser-outdated", + }) + ).rejects.toThrowErrorMatchingSnapshot(); + }); +});