mirror of
https://github.com/denoland/deno.git
synced 2024-12-28 18:19:08 -05:00
608 lines
18 KiB
TypeScript
Executable file
608 lines
18 KiB
TypeScript
Executable file
#!/usr/bin/env -S deno --allow-run --allow-write
|
|
/**
|
|
* Copyright © James Long and contributors
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
|
// This script formats the given source files. If the files are omitted, it
|
|
// formats the all files in the repository.
|
|
import { parse } from "../flags/mod.ts";
|
|
import * as path from "../path/mod.ts";
|
|
import * as toml from "../encoding/toml.ts";
|
|
import { ExpandGlobOptions, WalkInfo, expandGlob } from "../fs/mod.ts";
|
|
import { prettier, prettierPlugins } from "./prettier.ts";
|
|
const { args, cwd, exit, readAll, readFile, stdin, stdout, writeFile } = Deno;
|
|
|
|
const HELP_MESSAGE = `
|
|
Formats the given files. If no arg is passed, then formats the all files.
|
|
|
|
Usage: deno prettier/main.ts [options] [files...]
|
|
|
|
Options:
|
|
-H, --help Show this help message and exit.
|
|
--check Check if the source files are formatted.
|
|
--write Whether to write to the file, otherwise
|
|
it will output to stdout, Defaults to
|
|
false.
|
|
--ignore <path> Ignore the given path(s).
|
|
--ignore-path <auto|disable|path> Path to a file containing patterns that
|
|
describe files to ignore. Optional
|
|
value: auto/disable/filepath. Defaults
|
|
to null.
|
|
--stdin Specifies to read the code from stdin.
|
|
If run the command in a pipe, you do not
|
|
need to specify this flag.
|
|
Defaults to false.
|
|
--stdin-parser <typescript|babel|markdown|json>
|
|
If set --stdin flag, then need specify a
|
|
parser for stdin. available parser:
|
|
typescript/babel/markdown/json. Defaults
|
|
to typescript.
|
|
--config <auto|disable|path> Specify the configuration file of the
|
|
prettier.
|
|
Optional value: auto/disable/filepath.
|
|
Defaults to null.
|
|
|
|
JS/TS Styling Options:
|
|
--print-width <int> The line length where Prettier will try
|
|
wrap. Defaults to 80.
|
|
--tab-width <int> Number of spaces per indentation level.
|
|
Defaults to 2.
|
|
--use-tabs Indent with tabs instead of spaces.
|
|
Defaults to false.
|
|
--no-semi Do not print semicolons, except at the
|
|
beginning of lines which may need them.
|
|
--single-quote Use single quotes instead of double
|
|
quotes. Defaults to false.
|
|
--quote-props <as-needed|consistent|preserve>
|
|
Change when properties in objects are
|
|
quoted. Defaults to as-needed.
|
|
--jsx-single-quote Use single quotes instead of double
|
|
quotes in JSX.
|
|
--jsx-bracket-same-line Put the > of a multi-line JSX element at
|
|
the end of the last line instead of
|
|
being alone on the next line (does not
|
|
apply to self closing elements).
|
|
--trailing-comma <none|es5|all> Print trailing commas wherever possible
|
|
when multi-line. Defaults to none.
|
|
--no-bracket-spacing Do not print spaces between brackets.
|
|
--arrow-parens <avoid|always> Include parentheses around a sole arrow
|
|
function parameter. Defaults to avoid.
|
|
--end-of-line <auto|lf|crlf|cr> Which end of line characters to apply.
|
|
Defaults to auto.
|
|
|
|
Markdown Styling Options:
|
|
--prose-wrap <always|never|preserve> How to wrap prose. Defaults to preserve.
|
|
|
|
Example:
|
|
deno run prettier/main.ts --write script1.ts script2.js
|
|
Formats the files
|
|
|
|
deno run prettier/main.ts --check script1.ts script2.js
|
|
Checks if the files are formatted
|
|
|
|
deno run prettier/main.ts --write
|
|
Formats the all files in the repository
|
|
|
|
deno run prettier/main.ts script1.ts
|
|
Print the formatted code to stdout
|
|
|
|
cat script1.ts | deno run prettier/main.ts
|
|
Read the typescript code from stdin and
|
|
output formatted code to stdout.
|
|
|
|
cat config.json | deno run prettier/main.ts --stdin-parser=json
|
|
Read the JSON string from stdin and
|
|
output formatted code to stdout.
|
|
`;
|
|
|
|
// Available parsers
|
|
type ParserLabel = "typescript" | "babel" | "markdown" | "json";
|
|
|
|
interface PrettierBuildInOptions {
|
|
printWidth: number;
|
|
tabWidth: number;
|
|
useTabs: boolean;
|
|
semi: boolean;
|
|
singleQuote: boolean;
|
|
quoteProps: string;
|
|
jsxSingleQuote: boolean;
|
|
jsxBracketSameLine: boolean;
|
|
trailingComma: string;
|
|
bracketSpacing: boolean;
|
|
arrowParens: string;
|
|
proseWrap: string;
|
|
endOfLine: string;
|
|
}
|
|
|
|
interface PrettierOptions extends PrettierBuildInOptions {
|
|
write: boolean;
|
|
}
|
|
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
|
|
async function readFileIfExists(filename: string): Promise<string | null> {
|
|
let data;
|
|
try {
|
|
data = await readFile(filename);
|
|
} catch (e) {
|
|
// The file is deleted. Returns null.
|
|
return null;
|
|
}
|
|
|
|
return decoder.decode(data);
|
|
}
|
|
|
|
/**
|
|
* Checks if the file has been formatted with prettier.
|
|
*/
|
|
async function checkFile(
|
|
filename: string,
|
|
parser: ParserLabel,
|
|
prettierOpts: PrettierOptions
|
|
): Promise<boolean> {
|
|
const text = await readFileIfExists(filename);
|
|
|
|
if (!text) {
|
|
// The file is empty. Skip.
|
|
return true;
|
|
}
|
|
|
|
const formatted = prettier.check(text, {
|
|
...prettierOpts,
|
|
parser,
|
|
plugins: prettierPlugins
|
|
});
|
|
|
|
if (!formatted) {
|
|
// TODO: print some diff info here to show why this failed
|
|
console.error(`${filename} ... Not formatted`);
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
|
|
/**
|
|
* Formats the given file.
|
|
*/
|
|
async function formatFile(
|
|
filename: string,
|
|
parser: ParserLabel,
|
|
prettierOpts: PrettierOptions
|
|
): Promise<void> {
|
|
const text = await readFileIfExists(filename);
|
|
|
|
if (!text) {
|
|
// The file is deleted. Skip.
|
|
return;
|
|
}
|
|
|
|
const formatted: string = prettier.format(text, {
|
|
...prettierOpts,
|
|
parser,
|
|
plugins: prettierPlugins
|
|
});
|
|
|
|
const fileUnit8 = encoder.encode(formatted);
|
|
if (prettierOpts.write) {
|
|
if (text !== formatted) {
|
|
console.log(`Formatting ${filename}`);
|
|
await writeFile(filename, fileUnit8);
|
|
}
|
|
} else {
|
|
await stdout.write(fileUnit8);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selects the right prettier parser for the given path.
|
|
*/
|
|
function selectParser(path: string): ParserLabel | null {
|
|
if (/\.tsx?$/.test(path)) {
|
|
return "typescript";
|
|
} else if (/\.jsx?$/.test(path)) {
|
|
return "babel";
|
|
} else if (/\.json$/.test(path)) {
|
|
return "json";
|
|
} else if (/\.md$/.test(path)) {
|
|
return "markdown";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks if the files of the given paths have been formatted with prettier.
|
|
* If paths are empty, then checks all the files.
|
|
*/
|
|
async function checkSourceFiles(
|
|
files: AsyncIterableIterator<WalkInfo>,
|
|
prettierOpts: PrettierOptions
|
|
): Promise<void> {
|
|
const checks: Array<Promise<boolean>> = [];
|
|
|
|
for await (const { filename } of files) {
|
|
const parser = selectParser(filename);
|
|
if (parser) {
|
|
checks.push(checkFile(filename, parser, prettierOpts));
|
|
}
|
|
}
|
|
|
|
const results = await Promise.all(checks);
|
|
|
|
if (results.every((result): boolean => result)) {
|
|
console.log("Every file is formatted");
|
|
exit(0);
|
|
} else {
|
|
console.log("Some files are not formatted");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats the files of the given paths with prettier.
|
|
* If paths are empty, then formats all the files.
|
|
*/
|
|
async function formatSourceFiles(
|
|
files: AsyncIterableIterator<WalkInfo>,
|
|
prettierOpts: PrettierOptions
|
|
): Promise<void> {
|
|
const formats: Array<Promise<void>> = [];
|
|
|
|
for await (const { filename } of files) {
|
|
const parser = selectParser(filename);
|
|
if (parser) {
|
|
formats.push(formatFile(filename, parser, prettierOpts));
|
|
}
|
|
}
|
|
|
|
await Promise.all(formats);
|
|
exit(0);
|
|
}
|
|
|
|
/**
|
|
* Format source code
|
|
*/
|
|
function format(
|
|
text: string,
|
|
parser: ParserLabel,
|
|
prettierOpts: PrettierOptions
|
|
): string {
|
|
const formatted: string = prettier.format(text, {
|
|
...prettierOpts,
|
|
parser: parser,
|
|
plugins: prettierPlugins
|
|
});
|
|
|
|
return formatted;
|
|
}
|
|
|
|
/**
|
|
* Format code from stdin and output to stdout
|
|
*/
|
|
async function formatFromStdin(
|
|
parser: ParserLabel,
|
|
prettierOpts: PrettierOptions
|
|
): Promise<void> {
|
|
const byte = await readAll(stdin);
|
|
const formattedCode = format(
|
|
new TextDecoder().decode(byte),
|
|
parser,
|
|
prettierOpts
|
|
);
|
|
await stdout.write(new TextEncoder().encode(formattedCode));
|
|
}
|
|
|
|
/**
|
|
* Get the files to format.
|
|
* @param include The glob patterns to select the files.
|
|
* eg `"cmd/*.ts"` to select all the typescript files in cmd
|
|
* directory.
|
|
* eg `"cmd/run.ts"` to select `cmd/run.ts` file as only.
|
|
* @param exclude The glob patterns to ignore files.
|
|
* eg `"*_test.ts"` to ignore all the test file.
|
|
* @param root The directory from which to apply default globs.
|
|
* @returns returns an async iterable object
|
|
*/
|
|
async function* getTargetFiles(
|
|
include: string[],
|
|
exclude: string[],
|
|
root: string = cwd()
|
|
): AsyncIterableIterator<WalkInfo> {
|
|
const expandGlobOpts: ExpandGlobOptions = {
|
|
root,
|
|
exclude,
|
|
includeDirs: true,
|
|
extended: true,
|
|
globstar: true
|
|
};
|
|
|
|
async function* expandDirectory(d: string): AsyncIterableIterator<WalkInfo> {
|
|
for await (const walkInfo of expandGlob("**/*", {
|
|
...expandGlobOpts,
|
|
root: d,
|
|
includeDirs: false
|
|
})) {
|
|
yield walkInfo;
|
|
}
|
|
}
|
|
|
|
for (const globString of include) {
|
|
for await (const walkInfo of expandGlob(globString, expandGlobOpts)) {
|
|
if (walkInfo.info.isDirectory()) {
|
|
yield* expandDirectory(walkInfo.filename);
|
|
} else {
|
|
yield walkInfo;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* auto detect prettier configuration file and return config if file exist.
|
|
*/
|
|
async function autoResolveConfig(): Promise<PrettierBuildInOptions> {
|
|
const configFileNamesMap = {
|
|
".prettierrc.json": 1,
|
|
".prettierrc.yaml": 1,
|
|
".prettierrc.yml": 1,
|
|
".prettierrc.js": 1,
|
|
".prettierrc.ts": 1,
|
|
"prettier.config.js": 1,
|
|
"prettier.config.ts": 1,
|
|
".prettierrc.toml": 1
|
|
};
|
|
|
|
const files = await Deno.readDir(".");
|
|
|
|
for (const f of files) {
|
|
if (f.isFile() && configFileNamesMap[f.name]) {
|
|
const c = await resolveConfig(f.name);
|
|
if (c) {
|
|
return c;
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* parse prettier configuration file.
|
|
* @param filepath the configuration file path.
|
|
* support extension name with .json/.toml/.js
|
|
*/
|
|
async function resolveConfig(
|
|
filepath: string
|
|
): Promise<PrettierBuildInOptions> {
|
|
let config: PrettierOptions = undefined;
|
|
|
|
function generateError(msg: string): Error {
|
|
return new Error(`Invalid prettier configuration file: ${msg}.`);
|
|
}
|
|
|
|
const raw = new TextDecoder().decode(await Deno.readFile(filepath));
|
|
|
|
switch (path.extname(filepath)) {
|
|
case ".json":
|
|
try {
|
|
config = JSON.parse(raw) as PrettierOptions;
|
|
} catch (err) {
|
|
throw generateError(err.message);
|
|
}
|
|
break;
|
|
case ".yml":
|
|
case ".yaml":
|
|
// TODO: Unimplemented loading yaml / yml configuration file yet.
|
|
break;
|
|
case ".toml":
|
|
try {
|
|
config = toml.parse(raw) as PrettierOptions;
|
|
} catch (err) {
|
|
throw generateError(err.message);
|
|
}
|
|
break;
|
|
case ".js":
|
|
case ".ts":
|
|
const absPath = path.isAbsolute(filepath)
|
|
? filepath
|
|
: path.join(cwd(), filepath);
|
|
|
|
try {
|
|
const output = await import(
|
|
// TODO: Remove platform condition
|
|
// after https://github.com/denoland/deno/issues/3355 fixed
|
|
Deno.build.os === "win" ? "file://" + absPath : absPath
|
|
);
|
|
|
|
if (output && output.default) {
|
|
config = output.default;
|
|
} else {
|
|
throw new Error(
|
|
"Prettier of JS version should have default exports."
|
|
);
|
|
}
|
|
} catch (err) {
|
|
throw generateError(err.message);
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* auto detect .prettierignore and return pattern if file exist.
|
|
*/
|
|
async function autoResolveIgnoreFile(): Promise<string[]> {
|
|
const files = await Deno.readDir(".");
|
|
|
|
for (const f of files) {
|
|
if (f.isFile() && f.name === ".prettierignore") {
|
|
return await resolveIgnoreFile(f.name);
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* parse prettier ignore file.
|
|
* @param filepath the ignore file path.
|
|
*/
|
|
async function resolveIgnoreFile(filepath: string): Promise<string[]> {
|
|
const raw = new TextDecoder().decode(await Deno.readFile(filepath));
|
|
|
|
return raw
|
|
.split("\n")
|
|
.filter((v: string) => !!v.trim() && v.trim().indexOf("#") !== 0)
|
|
.map(v => v);
|
|
}
|
|
|
|
async function main(opts): Promise<void> {
|
|
const { help, check, _: args } = opts;
|
|
|
|
let prettierOpts: PrettierOptions = {
|
|
printWidth: Number(opts["print-width"]),
|
|
tabWidth: Number(opts["tab-width"]),
|
|
useTabs: Boolean(opts["use-tabs"]),
|
|
semi: Boolean(opts["semi"]),
|
|
singleQuote: Boolean(opts["single-quote"]),
|
|
quoteProps: opts["quote-props"],
|
|
jsxSingleQuote: Boolean(opts["jsx-single-quote"]),
|
|
jsxBracketSameLine: Boolean(opts["jsx-bracket-same-line "]),
|
|
trailingComma: opts["trailing-comma"],
|
|
bracketSpacing: Boolean(opts["bracket-spacing"]),
|
|
arrowParens: opts["arrow-parens"],
|
|
proseWrap: opts["prose-wrap"],
|
|
endOfLine: opts["end-of-line"],
|
|
write: opts["write"]
|
|
};
|
|
|
|
if (help) {
|
|
console.log(HELP_MESSAGE);
|
|
exit(0);
|
|
}
|
|
|
|
const configFilepath = opts["config"];
|
|
|
|
if (configFilepath && configFilepath !== "disable") {
|
|
const config =
|
|
configFilepath === "auto"
|
|
? await autoResolveConfig()
|
|
: await resolveConfig(configFilepath);
|
|
|
|
if (config) {
|
|
prettierOpts = { ...prettierOpts, ...config };
|
|
}
|
|
}
|
|
|
|
let ignore = opts.ignore as string[];
|
|
|
|
if (!Array.isArray(ignore)) {
|
|
ignore = [ignore];
|
|
}
|
|
|
|
const ignoreFilepath = opts["ignore-path"];
|
|
|
|
if (ignoreFilepath && ignoreFilepath !== "disable") {
|
|
const ignorePatterns: string[] =
|
|
ignoreFilepath === "auto"
|
|
? await autoResolveIgnoreFile()
|
|
: await resolveIgnoreFile(ignoreFilepath);
|
|
|
|
ignore = ignore.concat(ignorePatterns);
|
|
}
|
|
|
|
const files = getTargetFiles(args.length ? args : ["."], ignore);
|
|
|
|
const tty = Deno.isTTY();
|
|
|
|
const shouldReadFromStdin =
|
|
(!tty.stdin && (tty.stdout || tty.stderr)) || !!opts["stdin"];
|
|
|
|
try {
|
|
if (shouldReadFromStdin) {
|
|
await formatFromStdin(opts["stdin-parser"], prettierOpts);
|
|
} else if (check) {
|
|
await checkSourceFiles(files, prettierOpts);
|
|
} else {
|
|
await formatSourceFiles(files, prettierOpts);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
main(
|
|
parse(args.slice(1), {
|
|
string: [
|
|
"ignore",
|
|
"ignore-path",
|
|
"printWidth",
|
|
"tab-width",
|
|
"trailing-comma",
|
|
"arrow-parens",
|
|
"prose-wrap",
|
|
"end-of-line",
|
|
"stdin-parser",
|
|
"quote-props"
|
|
],
|
|
boolean: [
|
|
"check",
|
|
"help",
|
|
"semi",
|
|
"use-tabs",
|
|
"single-quote",
|
|
"bracket-spacing",
|
|
"write",
|
|
"stdin",
|
|
"jsx-single-quote",
|
|
"jsx-bracket-same-line"
|
|
],
|
|
default: {
|
|
ignore: [],
|
|
"print-width": "80",
|
|
"tab-width": "2",
|
|
"use-tabs": false,
|
|
semi: true,
|
|
"single-quote": false,
|
|
"trailing-comma": "none",
|
|
"bracket-spacing": true,
|
|
"arrow-parens": "avoid",
|
|
"prose-wrap": "preserve",
|
|
"end-of-line": "auto",
|
|
write: false,
|
|
stdin: false,
|
|
"stdin-parser": "typescript",
|
|
"quote-props": "as-needed",
|
|
"jsx-single-quote": false,
|
|
"jsx-bracket-same-line": false
|
|
},
|
|
alias: {
|
|
H: "help"
|
|
}
|
|
})
|
|
);
|