From b65cb6c1f67d95be8e8f7719f2a4174eaa5fe66c Mon Sep 17 00:00:00 2001 From: Georgii Dolzhykov Date: Sat, 23 Jul 2022 22:46:16 +0300 Subject: [PATCH 1/6] Playground: doc explorer (#10183) --- changelog_unreleased/api/10183.md | 3 + docs/technical-details.md | 2 + netlify.toml | 4 + src/main/parser.js | 2 +- website/playground/Playground.js | 87 ++++++++++++++++++---- website/playground/PrettierFormat.js | 6 ++ website/playground/WorkerApi.js | 2 +- website/playground/codeSamples.js | 18 +++++ website/playground/markdown.js | 1 + website/playground/panels.js | 18 +++++ website/playground/util.js | 24 ++++++ website/static/separate-css/playground.css | 25 ++++++- website/static/worker.js | 49 ++++++++++-- 13 files changed, 217 insertions(+), 24 deletions(-) create mode 100644 changelog_unreleased/api/10183.md diff --git a/changelog_unreleased/api/10183.md b/changelog_unreleased/api/10183.md new file mode 100644 index 000000000..9807e453e --- /dev/null +++ b/changelog_unreleased/api/10183.md @@ -0,0 +1,3 @@ +#### "Doc Explorer" mode for the Playground (#10183 by @thorn0) + +Switch the `parser` option to the special `doc-explorer` value to play with Prettier's intermediate representation and see how it's printed with different options. diff --git a/docs/technical-details.md b/docs/technical-details.md index 1a6b44ae5..249b4f39b 100644 --- a/docs/technical-details.md +++ b/docs/technical-details.md @@ -9,4 +9,6 @@ The basic idea is that the printer takes an AST and returns an intermediate repr This means that most of the logic of printing an AST involves generating an abstract representation of the output involving certain commands. For example, `["(", line, arg, line, ")"]` would represent a concatenation of opening parens, an argument, and closing parens. But if that doesn’t fit on one line, the printer can break where `line` is specified. +The [Playground](https://prettier.io/playground) has a special mode for exploring how Prettier’s intermediate representation is printed. To get there, open the sidebar (the "Show options" button) and set the `parser` option to the special value `doc-explorer`. + More (rough) details can be found in [commands.md](https://github.com/prettier/prettier/blob/main/commands.md). diff --git a/netlify.toml b/netlify.toml index ffcd928e0..fb951ce95 100644 --- a/netlify.toml +++ b/netlify.toml @@ -4,3 +4,7 @@ [build.environment] NODE_VERSION = "lts/*" YARN_VERSION = "1.22.15" +[[headers]] + for = "/worker.js" + [headers.values] + Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-eval'" diff --git a/src/main/parser.js b/src/main/parser.js index a5c9606b9..8ae740862 100644 --- a/src/main/parser.js +++ b/src/main/parser.js @@ -96,7 +96,7 @@ function parse(text, opts) { } /* istanbul ignore next */ - throw error.stack; + throw error; } } diff --git a/website/playground/Playground.js b/website/playground/Playground.js index 222896080..c76e724fb 100644 --- a/website/playground/Playground.js +++ b/website/playground/Playground.js @@ -67,10 +67,13 @@ class Playground extends React.Component { options.parser = "babel"; } - const content = original.content || getCodeSample(options.parser); + const codeSample = getCodeSample(options.parser); + const content = original.content || codeSample; + const needsClickForFirstRun = + options.parser === "doc-explorer" && content !== codeSample; const selection = {}; - this.state = { content, options, selection }; + this.state = { content, options, selection, needsClickForFirstRun }; this.handleOptionValueChange = this.handleOptionValueChange.bind(this); @@ -80,14 +83,10 @@ class Playground extends React.Component { this.setSelection = (selection) => this.setState({ selection }); this.setSelectionAsRange = () => { const { selection, content, options } = this.state; - const { head, anchor } = selection; - const range = [head, anchor].map( - ({ ch, line }) => - content.split("\n").slice(0, line).join("\n").length + - ch + - (line ? 1 : 0) + const [rangeStart, rangeEnd] = util.convertSelectionToRange( + selection, + content ); - const [rangeStart, rangeEnd] = range.sort((a, b) => a - b); const updatedOptions = { ...options, rangeStart, rangeEnd }; if (rangeStart === rangeEnd) { delete updatedOptions.rangeStart; @@ -103,6 +102,8 @@ class Playground extends React.Component { this.rangeEndOption = props.availableOptions.find( (opt) => opt.name === "rangeEnd" ); + + this.handleInputPanelFormat = this.handleInputPanelFormat.bind(this); } componentDidUpdate(_, prevState) { @@ -131,6 +132,10 @@ class Playground extends React.Component { ? getCodeSample(options.parser) : state.content; + if (option.name === "parser") { + state.needsClickForFirstRun = false; + } + return { options, content }; }); } @@ -158,6 +163,30 @@ class Playground extends React.Component { }); } + handleInputPanelFormat() { + if (this.state.options.parser !== "doc-explorer") { + return; + } + + const { content, selection } = this.state; + + return this.props.worker + .format(content, { + parser: "__js_expression", + cursorOffset: util.convertSelectionToRange(selection, content)[0], + }) + .then(({ error, formatted, cursorOffset }) => { + if (error) { + return; + } + + return { + value: formatted, + cursor: util.convertOffsetToPosition(cursorOffset, formatted), + }; + }); + } + render() { const { worker, version } = this.props; const { content, options } = this.state; @@ -171,6 +200,7 @@ class Playground extends React.Component { {(editorState) => ( - {editorState.showDoc && debug.doc && ( + {editorState.showDoc && ( this.getMarkdown({ doc: debug.doc })} + disabled={!debug.doc} > Copy doc @@ -284,6 +315,7 @@ class Playground extends React.Component { overlayEnd={options.rangeEnd} onChange={this.setContent} onSelectionChange={this.setSelection} + onFormat={this.handleInputPanelFormat} /> ) : null} {editorState.showAst ? ( @@ -302,11 +334,36 @@ class Playground extends React.Component { /> ) : null} {editorState.showOutput ? ( - + this.state.needsClickForFirstRun ? ( +
+
+ doc-explorer involves running code + provided by users. +
+
+ To stay on the safe side and prevent abuse, an + explicit user action is required when a direct + link to a doc-explorer playground is + opened. +
+
+ Click the button below to start the playground. +
+ +
+ ) : ( + + ) ) : null} {editorState.showSecondFormat ? ( this.setState(result)); diff --git a/website/playground/WorkerApi.js b/website/playground/WorkerApi.js index c251741f2..23928ffeb 100644 --- a/website/playground/WorkerApi.js +++ b/website/playground/WorkerApi.js @@ -32,7 +32,7 @@ export default function WorkerApi(source) { getMetadata() { return postMessage({ type: "meta" }); }, - format(code, options, debug) { + format(code, options, debug = {}) { return postMessage({ type: "format", code, options, debug }); }, postMessage, diff --git a/website/playground/codeSamples.js b/website/playground/codeSamples.js index 096383fdf..3038fbca3 100644 --- a/website/playground/codeSamples.js +++ b/website/playground/codeSamples.js @@ -308,6 +308,24 @@ export default function getCodeSamples(parser) { " ", "", ].join("\n"); + case "doc-explorer": + return [ + "group([", + ' "(*",', + " indent([", + " line,", + ' "Prettier:",', + " line,", + ' "Opinionated,",', + " line,", + ' "Code,",', + " line,", + ' "Formatter",', + " ]),", + " line,", + ' "*)",', + "])", + ].join("\n"); default: return ""; } diff --git a/website/playground/markdown.js b/website/playground/markdown.js index 60e14dd25..ec2c412e3 100644 --- a/website/playground/markdown.js +++ b/website/playground/markdown.js @@ -41,6 +41,7 @@ function getMarkdownSyntax(options) { case "acorn": case "espree": case "meriyah": + case "doc-explorer": return "jsx"; case "babel-ts": case "typescript": diff --git a/website/playground/panels.js b/website/playground/panels.js index aed97d5ca..41b32693f 100644 --- a/website/playground/panels.js +++ b/website/playground/panels.js @@ -19,6 +19,7 @@ class CodeMirrorPanel extends React.Component { delete options.rulerColor; delete options.value; delete options.onChange; + delete options.onFormat; options.rulers = [makeRuler(this.props)]; @@ -26,6 +27,12 @@ class CodeMirrorPanel extends React.Component { options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]; } + if (this.props.onFormat) { + options.extraKeys = { + "Shift-Alt-F": this.handleFormat.bind(this), + }; + } + this._codeMirror = CodeMirror.fromTextArea( this._textareaRef.current, options @@ -112,6 +119,17 @@ class CodeMirrorPanel extends React.Component { } } + handleFormat() { + const result = this.props.onFormat(); + if (!result) { + return; + } + Promise.resolve(result).then(({ value, cursor }) => { + this.props.onChange(value); + this._codeMirror.setCursor(cursor); + }); + } + render() { return (
diff --git a/website/playground/util.js b/website/playground/util.js index 587a54910..592336c28 100644 --- a/website/playground/util.js +++ b/website/playground/util.js @@ -95,6 +95,30 @@ export function getAstAutoFold(parser) { } } +export function convertSelectionToRange({ head, anchor }, content) { + const lines = content.split("\n"); + return [head, anchor] + .map( + ({ ch, line }) => + lines.slice(0, line).join("\n").length + ch + (line ? 1 : 0) + ) + .sort((a, b) => a - b); +} + +export function convertOffsetToPosition(offset, content) { + let line = 0; + let ch = 0; + for (let i = 0; i < offset && i <= content.length; i++) { + if (content[i] === "\n") { + line++; + ch = 0; + } else { + ch++; + } + } + return { line, ch }; +} + /** * Copied from https://github.com/prettier/prettier/blob/6fe21780115cf5f74f83876d64b03a727fbab220/src/cli/utils.js#L6-L27 * @template Obj diff --git a/website/static/separate-css/playground.css b/website/static/separate-css/playground.css index 8507dfb93..030b1bace 100644 --- a/website/static/separate-css/playground.css +++ b/website/static/separate-css/playground.css @@ -112,6 +112,25 @@ header h1 { border-bottom: 1px solid #ddd; } +.disabled-output-panel { + flex-direction: column; + align-items: center; + justify-content: center; + gap: 15px; + text-align: center; +} + +.disabled-output-panel .explanation { + font-size: 16px; + max-width: 400px; + color: darkred; +} + +.disabled-output-panel .btn { + font-size: 22px; + padding: 10px 20px; +} + /* display as 2x2 grid */ @media (min-width: 800px) { .options-container { @@ -258,7 +277,7 @@ input[type="number"] { border-color: #c8e1ff; } -.btn:hover { +.btn:hover:enabled { background-color: #e6ebf1; background-image: linear-gradient(to bottom, #f0f3f6, #dce3ec); border-color: #afb1b2; @@ -271,6 +290,10 @@ input[type="number"] { box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); } +.btn:disabled { + color: #afb1b2; +} + .tooltip { position: absolute; z-index: 6; diff --git a/website/static/worker.js b/website/static/worker.js index 18e423b56..0b115053e 100644 --- a/website/static/worker.js +++ b/website/static/worker.js @@ -30,6 +30,25 @@ for (const file in parsersLocation) { } } +const docExplorerPlugin = { + parsers: { + "doc-explorer": { + parse: (text) => + new Function( + `{ ${Object.keys(prettier.doc.builders)} }`, + `const result = (${text || "''"}\n); return result;` + )(prettier.doc.builders), + astFormat: "doc-explorer", + }, + }, + printers: { + "doc-explorer": { + print: (path) => path.getValue(), + }, + }, + languages: [{ name: "doc-explorer", parsers: ["doc-explorer"] }], +}; + self.onmessage = function (event) { self.postMessage({ uid: event.data.uid, @@ -45,6 +64,8 @@ function serializeAst(ast) { ? { name: value.name, message: value.message, ...value } : typeof value === "bigint" ? `BigInt('${String(value)}')` + : typeof value === "symbol" + ? String(value) : value, 2 ); @@ -58,6 +79,7 @@ function handleMessage(message) { JSON.stringify( prettier.getSupportInfo({ showUnreleased: true, + plugins: [docExplorerPlugin], }) ) ), @@ -72,13 +94,15 @@ function handleMessage(message) { delete options.doc; delete options.output2; - const plugins = [{ parsers }]; + const plugins = [{ parsers }, docExplorerPlugin]; options.plugins = plugins; const formatResult = formatCode(message.code, options); const response = { formatted: formatResult.formatted, + cursorOffset: formatResult.cursorOffset, + error: formatResult.error, debug: { ast: null, doc: null, @@ -111,10 +135,10 @@ function handleMessage(message) { try { response.debug.doc = prettier.__debug.formatDoc( prettier.__debug.printToDoc(message.code, options), - { parser: "babel", plugins } + { plugins } ); - } catch (e) { - response.debug.doc = String(e); + } catch { + response.debug.doc = ""; } } @@ -142,10 +166,23 @@ function formatCode(text, options) { } catch (e) { if (e.constructor && e.constructor.name === "SyntaxError") { // Likely something wrong with the user's code - return { formatted: String(e) }; + return { formatted: String(e), error: true }; } // Likely a bug in Prettier // Provide the whole stack for debugging - return { formatted: e.stack || String(e) }; + return { formatted: stringifyError(e), error: true }; } } + +function stringifyError(e) { + const stringified = String(e); + if (typeof e.stack !== "string") { + return stringified; + } + if (e.stack.includes(stringified)) { + // Chrome + return e.stack; + } + // Firefox + return stringified + "\n" + e.stack; +} From cd9955f1431ca3814ea9b713aa7275ceefa980d9 Mon Sep 17 00:00:00 2001 From: Georgii Dolzhykov Date: Mon, 25 Jul 2022 12:28:24 +0300 Subject: [PATCH 2/6] Fix range format for function bodies (#13173) --- changelog_unreleased/javascript/13173.md | 20 ++++++++++++++++++ src/main/range-util.js | 3 +++ .../js/range/__snapshots__/jsfmt.spec.js.snap | 21 +++++++++++++++++++ tests/format/js/range/function-body.js | 3 +++ 4 files changed, 47 insertions(+) create mode 100644 changelog_unreleased/javascript/13173.md create mode 100644 tests/format/js/range/function-body.js diff --git a/changelog_unreleased/javascript/13173.md b/changelog_unreleased/javascript/13173.md new file mode 100644 index 000000000..505e8b71a --- /dev/null +++ b/changelog_unreleased/javascript/13173.md @@ -0,0 +1,20 @@ +#### Fix range format for function bodies (#13173 by @thorn0) + + +```jsx +// Input +let fn = (() => { + return; // +//^^^^^^^^^^ - range +}); + +// Prettier stable +let fn = (() => { + return; // +};); + +// Prettier main +let fn = (() => { + return; // +}); +``` diff --git a/src/main/range-util.js b/src/main/range-util.js index ecb396ef1..a53e77445 100644 --- a/src/main/range-util.js +++ b/src/main/range-util.js @@ -64,6 +64,9 @@ function findSiblingAncestors( } else { break; } + if (resultStartNode === resultEndNode) { + break; + } } return { diff --git a/tests/format/js/range/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/range/__snapshots__/jsfmt.spec.js.snap index 6daa8552c..cad0645e8 100644 --- a/tests/format/js/range/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/js/range/__snapshots__/jsfmt.spec.js.snap @@ -182,6 +182,27 @@ rangeStart: 0 ================================================================================ `; +exports[`function-body.js format 1`] = ` +====================================options===================================== +parsers: ["babel", "flow", "typescript"] +printWidth: 80 +rangeEnd: 33 +rangeStart: 20 + | | printWidth +=====================================input====================================== + 1 | let fn =a((x ) => { +> 2 | quux (); // + | ^^^^^^^^^^^^^ + 3 | }); + 4 | +=====================================output===================================== +let fn =a((x ) => { + quux(); // +}); + +================================================================================ +`; + exports[`function-declaration.js format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] diff --git a/tests/format/js/range/function-body.js b/tests/format/js/range/function-body.js new file mode 100644 index 000000000..1705cd729 --- /dev/null +++ b/tests/format/js/range/function-body.js @@ -0,0 +1,3 @@ +let fn =a((x ) => { +<<>> quux (); //<<>> +}); From 80ec70f81fa15f7e84d59f41166d8afdadfe47d8 Mon Sep 17 00:00:00 2001 From: jinxdash Date: Thu, 28 Jul 2022 12:06:11 +0200 Subject: [PATCH 3/6] Add rust to community plugins (#13180) Add prettier-plugin-rust --- website/data/languages.yml | 1 + website/versioned_docs/version-stable/plugins.md | 1 + 2 files changed, 2 insertions(+) diff --git a/website/data/languages.yml b/website/data/languages.yml index 8dd807916..0e2d60412 100644 --- a/website/data/languages.yml +++ b/website/data/languages.yml @@ -49,6 +49,7 @@ - "[Java](https://github.com/jhipster/prettier-java)" - "[PHP](https://github.com/prettier/plugin-php)" - "[Ruby](https://github.com/prettier/plugin-ruby)" + - "[Rust](https://github.com/jinxdash/prettier-plugin-rust)" - "[TOML](https://github.com/bd82/toml-tools/tree/master/packages/prettier-plugin-toml)" - "[XML](https://github.com/prettier/plugin-xml)" - "[And more...](./docs/en/plugins.html#community-plugins)" diff --git a/website/versioned_docs/version-stable/plugins.md b/website/versioned_docs/version-stable/plugins.md index 59dad255e..fdd4a1a31 100644 --- a/website/versioned_docs/version-stable/plugins.md +++ b/website/versioned_docs/version-stable/plugins.md @@ -67,6 +67,7 @@ To turn off plugin autoloading, use `--no-plugin-search` when using Prettier CLI - [`prettier-plugin-jsonata`](https://github.com/Stedi/prettier-plugin-jsonata) by [**@Stedi**](https://github.com/Stedi) - [`prettier-plugin-kotlin`](https://github.com/Angry-Potato/prettier-plugin-kotlin) by [**@Angry-Potato**](https://github.com/Angry-Potato) - [`prettier-plugin-properties`](https://github.com/eemeli/prettier-plugin-properties) by [**@eemeli**](https://github.com/eemeli) +- [`prettier-plugin-rust`](https://github.com/jinxdash/prettier-plugin-rust) by [**@jinxdash**](https://github.com/jinxdash) - [`prettier-plugin-sh`](https://github.com/rx-ts/prettier/tree/master/packages/sh) by [**@JounQin**](https://github.com/JounQin) - [`prettier-plugin-solidity`](https://github.com/prettier-solidity/prettier-plugin-solidity) by [**@mattiaerre**](https://github.com/mattiaerre) - [`prettier-plugin-svelte`](https://github.com/UnwrittenFun/prettier-plugin-svelte) by [**@UnwrittenFun**](https://github.com/UnwrittenFun) From 6e1effadda211fe475e3149aa814cb04fb75e17f Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 29 Jul 2022 00:46:35 +0800 Subject: [PATCH 4/6] docs: add `sql` plugin (#13184) --- docs/plugins.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plugins.md b/docs/plugins.md index c42d1702a..59c603ab2 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -66,7 +66,8 @@ To turn off plugin autoloading, use `--no-plugin-search` when using Prettier CLI - [`prettier-plugin-jsonata`](https://github.com/Stedi/prettier-plugin-jsonata) by [**@Stedi**](https://github.com/Stedi) - [`prettier-plugin-kotlin`](https://github.com/Angry-Potato/prettier-plugin-kotlin) by [**@Angry-Potato**](https://github.com/Angry-Potato) - [`prettier-plugin-properties`](https://github.com/eemeli/prettier-plugin-properties) by [**@eemeli**](https://github.com/eemeli) -- [`prettier-plugin-sh`](https://github.com/rx-ts/prettier/tree/master/packages/sh) by [**@JounQin**](https://github.com/JounQin) +- [`prettier-plugin-sh`](https://github.com/un-ts/prettier/tree/master/packages/sh) by [**@JounQin**](https://github.com/JounQin) +- [`prettier-plugin-sql`](https://github.com/un-ts/prettier/tree/master/packages/sql) by [**@JounQin**](https://github.com/JounQin) - [`prettier-plugin-solidity`](https://github.com/prettier-solidity/prettier-plugin-solidity) by [**@mattiaerre**](https://github.com/mattiaerre) - [`prettier-plugin-svelte`](https://github.com/UnwrittenFun/prettier-plugin-svelte) by [**@UnwrittenFun**](https://github.com/UnwrittenFun) - [`prettier-plugin-toml`](https://github.com/bd82/toml-tools/tree/master/packages/prettier-plugin-toml) by [**@bd82**](https://github.com/bd82) From 955553bb3972eae532206ffc3a7d4a2383cb7c62 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sun, 31 Jul 2022 22:58:05 +0900 Subject: [PATCH 5/6] Add `--cache-location` option (#13019) * Implement searching cache file from `--cache-location` option * Add tests * Add docs * Fix * Fix doc * Add changelog * Update snapshots * Update docs * Update docs/cli.md Co-authored-by: Holger Jeromin * Fix changelog * Remove directory support * Update docs * Update tests * Update docs * Add validation for dir * Fix comment * Update option description * Validate JSON * Use `path.resolve` Co-authored-by: Holger Jeromin --- changelog_unreleased/cli/13019.md | 9 ++ docs/cli.md | 10 ++ src/cli/constant.js | 4 + src/cli/find-cache-file.js | 38 +++++++- src/cli/format.js | 10 +- src/cli/utils.js | 15 ++- .../__snapshots__/early-exit.js.snap | 2 + .../__snapshots__/help-options.js.snap | 11 +++ tests/integration/__tests__/cache.js | 97 +++++++++++++++++++ tests/integration/cli/cache/dir/.gitkeep | 0 10 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 changelog_unreleased/cli/13019.md create mode 100644 tests/integration/cli/cache/dir/.gitkeep diff --git a/changelog_unreleased/cli/13019.md b/changelog_unreleased/cli/13019.md new file mode 100644 index 000000000..bb4f81b2b --- /dev/null +++ b/changelog_unreleased/cli/13019.md @@ -0,0 +1,9 @@ +#### Add `--cache-location` option (#13019 by @sosukesuzuki) + +Path to the cache file location used by `--cache` flag. If you don't explicit `--cache-location`, Prettier saves cache file at `./node_modules/.cache/prettier/.prettier-cache`. + +If a file path is passed, that file is used as the cache file. + +```bash +prettier --write --cache --cache-location=my_cache_file src +``` diff --git a/docs/cli.md b/docs/cli.md index af3afef5f..b4d3981e9 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -225,6 +225,16 @@ Also, since the cache file is stored in `./node_modules/.cache/prettier/.prettie > Plugins version and implementation are not used as cache keys. We recommend that you delete the cache when updating plugins. +## `--cache-location` + +Path to the cache file location used by `--cache` flag. If you don't explicit `--cache-location`, Prettier saves cache file at `./node_modules/.cache/prettier/.prettier-cache`. + +If a file path is passed, that file is used as the cache file. + +```bash +prettier --write --cache --cache-location=my_cache_file src +``` + ## `--cache-strategy` Strategy for the cache to use for detecting changed files. Can be either `metadata` or `content`. diff --git a/src/cli/constant.js b/src/cli/constant.js index c7cc8958d..33a64f3d1 100644 --- a/src/cli/constant.js +++ b/src/cli/constant.js @@ -76,6 +76,10 @@ const options = { description: "Only format changed files. Cannot use with --stdin-filepath.", type: "boolean", }, + "cache-location": { + description: "Path to the cache file.", + type: "path", + }, "cache-strategy": { choices: [ { diff --git a/src/cli/find-cache-file.js b/src/cli/find-cache-file.js index c8960ead4..cc6ebee79 100644 --- a/src/cli/find-cache-file.js +++ b/src/cli/find-cache-file.js @@ -1,19 +1,51 @@ "use strict"; +const fs = require("fs").promises; const os = require("os"); const path = require("path"); const findCacheDir = require("find-cache-dir"); +const { statSafe, isJson } = require("./utils.js"); /** * Find default cache file (`./node_modules/.cache/prettier/.prettier-cache`) using https://github.com/avajs/find-cache-dir - * - * @returns {string} */ -function findCacheFile() { +function findDefaultCacheFile() { const cacheDir = findCacheDir({ name: "prettier", create: true }) || os.tmpdir(); const cacheFilePath = path.join(cacheDir, ".prettier-cache"); return cacheFilePath; } +async function findCacheFileFromOption(cacheLocation) { + const cacheFile = path.resolve(cacheLocation); + + const stat = await statSafe(cacheFile); + if (stat) { + if (stat.isDirectory()) { + throw new Error( + `Resolved --cache-location '${cacheFile}' is a directory` + ); + } + + const data = await fs.readFile(cacheFile, "utf8"); + if (!isJson(data)) { + throw new Error(`'${cacheFile}' isn't a valid JSON file`); + } + } + + return cacheFile; +} + +/** + * @param {string | undefined} cacheLocation + * @returns {Promise} + */ +async function findCacheFile(cacheLocation) { + if (!cacheLocation) { + return findDefaultCacheFile(); + } + const cacheFile = await findCacheFileFromOption(cacheLocation); + return cacheFile; +} + module.exports = findCacheFile; diff --git a/src/cli/format.js b/src/cli/format.js index 8f5e4867f..6d91998ac 100644 --- a/src/cli/format.js +++ b/src/cli/format.js @@ -299,7 +299,7 @@ async function formatFiles(context) { } let formatResultsCache; - const cacheFilePath = findCacheFile(); + const cacheFilePath = await findCacheFile(context.argv.cacheLocation); if (context.argv.cache) { formatResultsCache = new FormatResultsCache( cacheFilePath, @@ -312,9 +312,11 @@ async function formatFiles(context) { ); process.exit(2); } - const stat = await statSafe(cacheFilePath); - if (stat) { - await fs.unlink(cacheFilePath); + if (!context.argv.cacheLocation) { + const stat = await statSafe(cacheFilePath); + if (stat) { + await fs.unlink(cacheFilePath); + } } } diff --git a/src/cli/utils.js b/src/cli/utils.js index a846c185c..d13b5efcf 100644 --- a/src/cli/utils.js +++ b/src/cli/utils.js @@ -67,4 +67,17 @@ async function statSafe(filePath) { } } -module.exports = { printToScreen, groupBy, pick, createHash, statSafe }; +/** + * @param {string} value + * @returns {boolean} + */ +function isJson(value) { + try { + JSON.parse(value); + return true; + } catch { + return false; + } +} + +module.exports = { printToScreen, groupBy, pick, createHash, statSafe, isJson }; diff --git a/tests/integration/__tests__/__snapshots__/early-exit.js.snap b/tests/integration/__tests__/__snapshots__/early-exit.js.snap index 63f00b025..d7886195e 100644 --- a/tests/integration/__tests__/__snapshots__/early-exit.js.snap +++ b/tests/integration/__tests__/__snapshots__/early-exit.js.snap @@ -141,6 +141,7 @@ Other options: --cache Only format changed files. Cannot use with --stdin-filepath. Defaults to false. + --cache-location Path to the cache file. --cache-strategy Strategy for the cache to use for detecting changed files. --no-color Do not colorize error messages. @@ -315,6 +316,7 @@ Other options: --cache Only format changed files. Cannot use with --stdin-filepath. Defaults to false. + --cache-location Path to the cache file. --cache-strategy Strategy for the cache to use for detecting changed files. --no-color Do not colorize error messages. diff --git a/tests/integration/__tests__/__snapshots__/help-options.js.snap b/tests/integration/__tests__/__snapshots__/help-options.js.snap index 009f516d3..5574c326f 100644 --- a/tests/integration/__tests__/__snapshots__/help-options.js.snap +++ b/tests/integration/__tests__/__snapshots__/help-options.js.snap @@ -57,6 +57,17 @@ Default: false exports[`show detailed usage with --help cache (write) 1`] = `[]`; +exports[`show detailed usage with --help cache-location (stderr) 1`] = `""`; + +exports[`show detailed usage with --help cache-location (stdout) 1`] = ` +"--cache-location + + Path to the cache file. +" +`; + +exports[`show detailed usage with --help cache-location (write) 1`] = `[]`; + exports[`show detailed usage with --help cache-strategy (stderr) 1`] = `""`; exports[`show detailed usage with --help cache-strategy (stdout) 1`] = ` diff --git a/tests/integration/__tests__/cache.js b/tests/integration/__tests__/cache.js index 863cd2901..a15618395 100644 --- a/tests/integration/__tests__/cache.js +++ b/tests/integration/__tests__/cache.js @@ -18,6 +18,9 @@ describe("--cache option", () => { "node_modules/.cache/prettier/.prettier-cache" ); + const nonDefaultCacheFileName = ".non-default-cache-file"; + const nonDefaultCacheFilePath = path.join(dir, nonDefaultCacheFileName); + let contentA; let contentB; @@ -28,6 +31,7 @@ describe("--cache option", () => { afterEach(async () => { rimraf.sync(path.join(dir, "node_modules")); + rimraf.sync(nonDefaultCacheFilePath); await fs.writeFile(path.join(dir, "a.js"), contentA); await fs.writeFile(path.join(dir, "b.js"), contentB); }); @@ -68,6 +72,20 @@ describe("--cache option", () => { ); }); + it("throws error when `--cache-location` is a directory.", async () => { + const { stderr } = await runPrettier(dir, [ + "foo.js", + "--cache", + "--cache-location", + "dir", + ]); + expect(stripAnsi(stderr.trim())).toEqual( + expect.stringMatching( + /\[error] Resolved --cache-location '.+' is a directory/ + ) + ); + }); + describe("--cache-strategy metadata", () => { it("creates default cache file named `node_modules/.cache/prettier/.prettier-cache`", async () => { await expect(fs.stat(defaultCacheFile)).rejects.toHaveProperty( @@ -370,4 +388,83 @@ describe("--cache option", () => { await expect(fs.stat(defaultCacheFile)).rejects.toThrowError(); }); }); + + describe("--cache-location", () => { + it("doesn't create default cache file when `--cache-location` exists", async () => { + await expect(fs.stat(defaultCacheFile)).rejects.toHaveProperty( + "code", + "ENOENT" + ); + await runPrettier(dir, [ + "--cache", + "--cache-location", + nonDefaultCacheFileName, + ".", + ]); + await expect(fs.stat(defaultCacheFile)).rejects.toHaveProperty( + "code", + "ENOENT" + ); + }); + + it("throws error for invalid JSON file", async () => { + const { stderr } = await runPrettier(dir, [ + "--cache", + "--cache-location", + "a.js", + ".", + ]); + expect(stripAnsi(stderr).trim()).toEqual( + expect.stringMatching(/\[error] '.+' isn't a valid JSON file/) + ); + }); + + describe("file", () => { + it("creates the cache file at location specified by `--cache-location`", async () => { + await expect(fs.stat(nonDefaultCacheFilePath)).rejects.toHaveProperty( + "code", + "ENOENT" + ); + await runPrettier(dir, [ + "--cache", + "--cache-location", + nonDefaultCacheFileName, + ".", + ]); + await expect( + fs.stat(nonDefaultCacheFilePath) + ).resolves.not.toThrowError(); + }); + + it("does'nt format when cache is available", async () => { + const { stdout: firstStdout } = await runPrettier(dir, [ + "--cache", + "--write", + "--cache-location", + nonDefaultCacheFileName, + ".", + ]); + expect(stripAnsi(firstStdout).split("\n").filter(Boolean)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/^a\.js .+ms$/), + expect.stringMatching(/^b\.js .+ms$/), + ]) + ); + + const { stdout: secondStdout } = await runPrettier(dir, [ + "--cache", + "--write", + "--cache-location", + nonDefaultCacheFileName, + ".", + ]); + expect(stripAnsi(secondStdout).split("\n").filter(Boolean)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/^a\.js .+ms \(cached\)$/), + expect.stringMatching(/^b\.js .+ms \(cached\)$/), + ]) + ); + }); + }); + }); }); diff --git a/tests/integration/cli/cache/dir/.gitkeep b/tests/integration/cli/cache/dir/.gitkeep new file mode 100644 index 000000000..e69de29bb From 765cae25522f582b71a79e7fb10417eb4612abe6 Mon Sep 17 00:00:00 2001 From: Sam Apostel Date: Mon, 1 Aug 2022 04:26:23 +0200 Subject: [PATCH 6/6] Docs: Use `npm pkg set` instead of deprecated `npm set-script` (#13186) --- docs/install.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/install.md b/docs/install.md index 567757673..d6563217d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -103,7 +103,7 @@ For example, you can do the following to have Prettier run before each commit: ```bash npm install --save-dev husky lint-staged npx husky install - npm set-script prepare "husky install" + npm pkg set scripts.prepare="husky install" npx husky add .husky/pre-commit "npx lint-staged" ``` @@ -112,7 +112,7 @@ For example, you can do the following to have Prettier run before each commit: ```bash yarn add --dev husky lint-staged npx husky install - npm set-script prepare "husky install" + npm pkg set scripts.prepare="husky install" npx husky add .husky/pre-commit "npx lint-staged" ``` @@ -120,8 +120,6 @@ For example, you can do the following to have Prettier run before each commit: -> Note: npm **set-script** command requires at least **npm v7.x**. See https://docs.npmjs.com/cli/v7/commands/npm-set-script. - 2. Add the following to your `package.json`: ```json