diff --git a/.changeset/swap-prettier-for-tsv.md b/.changeset/swap-prettier-for-tsv.md new file mode 100644 index 0000000000..55d31596e8 --- /dev/null +++ b/.changeset/swap-prettier-for-tsv.md @@ -0,0 +1,6 @@ +--- +'@fuzdev/gro': minor +--- + +feat: replace Prettier with [`tsv`](https://github.com/fuzdev/tsv) + diff --git a/CLAUDE.md b/CLAUDE.md index 605ee2b05d..49ec1e15c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Gro (`@fuzdev/gro`) is a dev tool for building TypeScript projects with SvelteKit, providing: convention-based task runner, Node loader for TypeScript/Svelte/SvelteKit modules, code generation system, plugin -architecture, and integrations with Vite, esbuild, Vitest, Prettier, ESLint, and +architecture, and integrations with Vite, esbuild, Vitest, tsv, ESLint, and Changesets. It's designed around conventions and the filesystem, not configuration files. @@ -168,7 +168,7 @@ Naming convention: Return values: -- String: default filename, auto-formatted with Prettier +- String: default filename, auto-formatted with tsv - Object: `{content, filename?, format?}` for control - Array: multiple files from one genfile - `null`: no-op diff --git a/README.md b/README.md index 95cea780d2..5dc67e1708 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ It includes: - codegen by convention with [`gen`](/src/docs/gen.md) - linting with [ESLint](https://github.com/eslint/eslint) (I also maintain [`@feltjs/eslint-config`](https://github.com/feltjs/eslint-config)) -- formatting with [Prettier](https://github.com/prettier/prettier) +- formatting with [`tsv`](https://github.com/fuzdev/tsv) ## docs @@ -207,10 +207,10 @@ gro lint # eslint For a usage example see [the `check.yml` CI config](.github/workflows/check.yml). -Formatting with [`prettier`](https://github.com/prettier/prettier): +Formatting with [`tsv`](https://github.com/fuzdev/tsv): ```bash -gro format # format all of the source files using Prettier +gro format # format all of the source files using tsv gro format --check # check that all source files are formatted ``` @@ -274,7 +274,7 @@ Gro builds on [zod](https://github.com/colinhacks/zod) ∙ [@fuzdev/fuz_util](https://github.com/fuzdev/fuz_util) ∙ [ESLint](https://github.com/eslint/eslint) ∙ -[Prettier](https://github.com/prettier/prettier) ∙ +[tsv](https://github.com/fuzdev/tsv) ∙ [svelte-check](https://github.com/sveltejs/language-tools/tree/master/packages/svelte-check) & [more](package.json) diff --git a/package-lock.json b/package-lock.json index 3a576982b3..7895b268bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,11 @@ "version": "0.205.1", "license": "MIT", "dependencies": { + "@fuzdev/tsv_wasm": "^0.1.0", "chokidar": "^5.0.0", "dotenv": "^17.2.3", "esm-env": "^1.2.2", "oxc-parser": "^0.99.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.5.1", "ts-blank-space": "^0.6.2", "tslib": "^2.8.1" }, @@ -942,6 +941,21 @@ } } }, + "node_modules/@fuzdev/tsv_wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@fuzdev/tsv_wasm/-/tsv_wasm-0.1.0.tgz", + "integrity": "sha512-hlZxdL1c5jiMyLnMv3NWONxNR64JChNekxlsKEJxXvgr/bmcRsgF1XuyabOTtppW9C+9HUvCpklJgJeU4aZbKw==", + "license": "MIT", + "bin": { + "tsv": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1012,6 +1026,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1022,6 +1037,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1032,6 +1048,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1041,12 +1058,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1623,6 +1642,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1772,6 +1792,7 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1795,6 +1816,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -1947,7 +1969,7 @@ "version": "8.60.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2206,6 +2228,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2268,6 +2291,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2287,6 +2311,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2366,6 +2391,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2524,6 +2550,7 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "dev": true, "license": "MIT" }, "node_modules/dotenv": { @@ -2765,6 +2792,7 @@ "version": "2.2.11", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -3036,6 +3064,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -3408,6 +3437,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -3437,6 +3467,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -3819,31 +3850,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-svelte": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz", - "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==", - "license": "MIT", - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4060,6 +4066,7 @@ "version": "5.56.1", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.1.tgz", "integrity": "sha512-eArsJmvl3xZVuTYD852PzIEdg2wgDdIZ1NEsIPbzAukHwi284B18No4nK2rCO9AwsWUDza4Cjvmoa4HaojTl5g==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -4614,6 +4621,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, "license": "MIT" }, "node_modules/zod": { diff --git a/package.json b/package.json index 1ba4645210..a9450942ca 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "bugs": "https://github.com/fuzdev/gro/issues", "funding": "https://www.ryanatkn.com/funding", "scripts": { - "bootstrap": "rm -rf .gro dist && svelte-kit sync && svelte-package && chmod +x ./dist/gro.js && npm link -f", + "bootstrap": "rm -rf .gro dist && svelte-kit sync && svelte-package && node -e \"import('./src/lib/dist_rewrite_imports.ts').then((m) => m.rewrite_dist_imports())\" && chmod +x ./dist/gro.js && npm link -f", "start": "gro dev", "dev": "gro dev", "build": "gro build", @@ -48,12 +48,11 @@ "typescript" ], "dependencies": { + "@fuzdev/tsv_wasm": "^0.1.0", "chokidar": "^5.0.0", "dotenv": "^17.2.3", "esm-env": "^1.2.2", "oxc-parser": "^0.99.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.5.1", "ts-blank-space": "^0.6.2", "tslib": "^2.8.1" }, @@ -115,23 +114,6 @@ "zimmerframe": "^1.1.4", "zod": "^4.3.6" }, - "prettier": { - "plugins": [ - "prettier-plugin-svelte" - ], - "useTabs": true, - "printWidth": 100, - "singleQuote": true, - "bracketSpacing": false, - "overrides": [ - { - "files": "package.json", - "options": { - "useTabs": false - } - } - ] - }, "sideEffects": [ "**/*.css" ], diff --git a/src/docs/README.md b/src/docs/README.md index 41123c235b..ce4d907490 100644 --- a/src/docs/README.md +++ b/src/docs/README.md @@ -1,5 +1,6 @@ # docs + > [gro](/../..) / docs / README.md - [build](build.md) diff --git a/src/docs/task.md b/src/docs/task.md index 6aa2536768..499683ec6e 100644 --- a/src/docs/task.md +++ b/src/docs/task.md @@ -291,8 +291,7 @@ Some builtin Gro tasks call external commands like [`svelte-kit`](https://github.com/sveltejs/kit), [`vite`](https://github.com/vitejs/vite), [`vitest`](https://github.com/vitest-dev/vitest), -[`tsc`](https://github.com/microsoft/typescript), -and [`prettier`](https://github.com/prettier/prettier). +and [`tsc`](https://github.com/microsoft/typescript). Gro supports generic agnostic args forwarding to these tasks via the `--` pattern: for example, to forward args to `svelte-kit` and `vitest`, no matter which task invokes them, use `gro taskname --taskname-arg -- vitest --arg1 neat --arg2 22 -- svelte-kit --arg3`. diff --git a/src/lib/constants.ts b/src/lib/constants.ts index fe28c66b30..7221e208e2 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -19,7 +19,6 @@ export const GRO_DIR = GRO_DIRNAME + '/'; /** @trailing_slash */ export const GRO_DEV_DIR = GRO_DEV_DIRNAME + '/'; export const GRO_CONFIG_FILENAME = 'gro.config.ts'; -export const README_FILENAME = 'README.md'; export const SVELTE_CONFIG_FILENAME = 'svelte.config.js'; export const VITE_CONFIG_FILENAME = 'vite.config.ts'; export const NODE_MODULES_DIRNAME = 'node_modules'; @@ -29,7 +28,6 @@ export const SVELTEKIT_DEV_DIRNAME = '.svelte-kit'; // TODO use Svelte config va export const SVELTEKIT_BUILD_DIRNAME = 'build'; export const SVELTEKIT_DIST_DIRNAME = 'dist'; export const SVELTEKIT_VITE_CACHE_PATH = NODE_MODULES_DIRNAME + '/.vite'; -export const GITHUB_DIRNAME = '.github'; export const GIT_DIRNAME = '.git'; export const TSCONFIG_FILENAME = 'tsconfig.json'; @@ -47,7 +45,6 @@ export const EVERYTHING_MATCHER = /.*/; export const JS_CLI_DEFAULT = 'node'; export const PM_CLI_DEFAULT = 'npm'; -export const PRETTIER_CLI_DEFAULT = 'prettier'; export const SVELTEKIT_CLI = 'svelte-kit'; export const SVELTE_CHECK_CLI = 'svelte-check'; export const SVELTE_PACKAGE_CLI = 'svelte-package'; diff --git a/src/lib/filer.ts b/src/lib/filer.ts index c99f1885bb..e8bf69f3f5 100644 --- a/src/lib/filer.ts +++ b/src/lib/filer.ts @@ -449,7 +449,9 @@ export const filter_dependents = ( const dependent_disknode = get_by_id(dependent_id); if (!dependent_disknode) { log?.warn( - `[filer.filter_dependents] dependent source file ${dependent_id} not found for ${current.id}`, + `[filer.filter_dependents] dependent source file ${dependent_id} not found for ${ + current.id + }`, ); continue; } diff --git a/src/lib/format.task.ts b/src/lib/format.task.ts index 9331f62d89..17db74072d 100644 --- a/src/lib/format.task.ts +++ b/src/lib/format.task.ts @@ -1,15 +1,12 @@ -import {print_spawn_result} from '@fuzdev/fuz_util/process.ts'; import {z} from 'zod'; -import {to_implicit_forwarded_args} from './args.ts'; -import {PRETTIER_CLI_DEFAULT} from './constants.ts'; import {format_directory} from './format_directory.ts'; import {paths} from './paths.ts'; import {TaskError, type Task} from './task.ts'; /** @nodocs */ export const Args = z.strictObject({ - _: z.array(z.string()).meta({description: 'files or directories to format'}).optional(), + _: z.array(z.string()).meta({description: 'files or globs to format'}).optional(), check: z .boolean() .meta({description: 'exit with a nonzero code if any files are unformatted'}) @@ -24,21 +21,15 @@ export const task: Task = { run: async ({args, log, config}) => { const {_: patterns, check} = args; - const format_result = await format_directory( + const result = await format_directory( log, paths.source, check, - undefined, - undefined, - undefined, - config.pm_cli, - to_implicit_forwarded_args(PRETTIER_CLI_DEFAULT), + config.search_filters, patterns, ); - if (!format_result.ok) { - throw new TaskError( - `Failed ${check ? 'formatting check' : 'to format'}. ${print_spawn_result(format_result)}`, - ); + if (!result.ok) { + throw new TaskError(`Failed ${check ? 'formatting check' : 'to format'}.`); } }, }; diff --git a/src/lib/format_directory.ts b/src/lib/format_directory.ts index 09cefdb1bd..8a6ac42827 100644 --- a/src/lib/format_directory.ts +++ b/src/lib/format_directory.ts @@ -1,65 +1,135 @@ -import {args_serialize, type Args} from '@fuzdev/fuz_util/args.ts'; +import {map_concurrent} from '@fuzdev/fuz_util/async.ts'; +import {fs_search} from '@fuzdev/fuz_util/fs.ts'; import type {Logger} from '@fuzdev/fuz_util/log.ts'; -import type {SpawnResult} from '@fuzdev/fuz_util/process.ts'; +import type {PathFilter} from '@fuzdev/fuz_util/path.ts'; +import {globSync} from 'node:fs'; +import {readFile, writeFile} from 'node:fs/promises'; +import {isAbsolute, join, resolve} from 'node:path'; -import {spawn_cli, to_cli_name, type Cli} from './cli.ts'; import { - GITHUB_DIRNAME, - README_FILENAME, + GRO_CONFIG_FILENAME, SVELTE_CONFIG_FILENAME, - VITE_CONFIG_FILENAME, TSCONFIG_FILENAME, - GRO_CONFIG_FILENAME, - PM_CLI_DEFAULT, - PRETTIER_CLI_DEFAULT, + VITE_CONFIG_FILENAME, } from './constants.ts'; +import {format_file} from './format_file.ts'; import {paths} from './paths.ts'; -const EXTENSIONS_DEFAULT = 'ts,js,json,svelte,html,css,md,yml'; -const ROOT_PATHS_DEFAULT = `${[ - README_FILENAME, +/** Matches the file extensions `format_file` knows how to format. */ +const FORMATTABLE_MATCHER = /\.(ts|mts|cts|js|mjs|cjs|svelte|css|json)$/; + +/** + * Root-level files formatted alongside `paths.source`. + * `package.json` is intentionally omitted — `gro sync` owns its serialization + * via `package_json_serialize` (2-space, matching the npm convention). + */ +const ROOT_FILES_DEFAULT = [ GRO_CONFIG_FILENAME, SVELTE_CONFIG_FILENAME, VITE_CONFIG_FILENAME, TSCONFIG_FILENAME, - GITHUB_DIRNAME, -].join(',')}/**/*`; +]; + +const FORMAT_CONCURRENCY = 16; + +export interface FormatDirectoryResult { + ok: boolean; + /** The files that were formatted (or, in `check` mode, that need formatting). */ + formatted: Array; + /** Files the formatter rejected (e.g. a syntax error), left untouched. */ + errored: Array<{id: string; error: unknown}>; +} /** - * Formats files on the filesystem. - * When `patterns` is provided, formats those specific files/patterns. - * Otherwise formats `dir` with default extensions, plus root files if `dir` is `paths.source`. - * This is separated from `./format_file` to avoid importing all of the `prettier` code - * inside modules that import this one. (which has a nontrivial cost) + * Formats files on the filesystem in-process via `format_file`. + * When `patterns` is provided, formats those specific files/globs. + * Otherwise formats `dir` (recursively, respecting `filter`) plus the root + * files when `dir` is `paths.source`. + * + * Files the formatter rejects (e.g. a syntax error) are reported and fail the + * run in both modes rather than being silently skipped. + * + * This is separated from `./format_file` so modules that only need directory + * traversal don't pull in the formatter (which loads the `tsv` WASM module). + * + * @param check - when `true`, reports unformatted files instead of writing them + * @param filter - directory filters (e.g. `config.search_filters`) to skip; + * also prunes `patterns` results so an explicit glob can't reach `node_modules` */ export const format_directory = async ( log: Logger, dir: string, check = false, - extensions = EXTENSIONS_DEFAULT, - root_paths = ROOT_PATHS_DEFAULT, - prettier_cli: string | Cli = PRETTIER_CLI_DEFAULT, - pm_cli: string = PM_CLI_DEFAULT, - additional_args?: Args, + filter?: PathFilter | Array, patterns?: Array, -): Promise => { - const forwarded_args = {...additional_args}; - if (forwarded_args.check === undefined && forwarded_args.write === undefined) { - forwarded_args[check ? 'check' : 'write'] = true; +): Promise => { + const file_ids = patterns?.length + ? globSync(patterns, to_glob_options(filter)) + .filter((p) => FORMATTABLE_MATCHER.test(p)) + .map((p) => (isAbsolute(p) ? p : resolve(p))) + : await collect_source_files(dir, filter); + + const formatted: Array = []; + const errored: Array<{id: string; error: unknown}> = []; + await map_concurrent(file_ids, FORMAT_CONCURRENCY, async (id) => { + let content: string; + try { + content = await readFile(id, 'utf8'); + } catch (_err) { + return; // a default root file that doesn't exist in this project + } + let next: string; + try { + next = format_file(content, {filepath: id}); + } catch (error) { + errored.push({id, error}); + return; + } + if (next === content) return; + formatted.push(id); + if (!check) await writeFile(id, next); + }); + + // `map_concurrent` completes in nondeterministic order; sort for stable output. + formatted.sort(); + errored.sort((a, b) => a.id.localeCompare(b.id)); + + if (check && formatted.length) { + log.error(`${formatted.length} file(s) need formatting:`); + for (const id of formatted) log.error(` ${id}`); } - const serialized_args = args_serialize(forwarded_args); - if (patterns?.length) { - serialized_args.push(...patterns); - } else { - serialized_args.push(`${dir}**/*.{${extensions}}`); - if (dir === paths.source) { - serialized_args.push(`${paths.root}{${root_paths}}`); + if (errored.length) { + log.error(`${errored.length} file(s) could not be formatted:`); + for (const {id, error} of errored) { + log.error(` ${id}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + return {ok: errored.length === 0 && (!check || formatted.length === 0), formatted, errored}; +}; + +/** Builds `globSync` options that prune the same paths `filter` would exclude. */ +const to_glob_options = ( + filter?: PathFilter | Array, +): {exclude: (path: string) => boolean} => { + const filters = filter && (Array.isArray(filter) ? filter : [filter]); + if (!filters?.length) return {exclude: () => false}; + return {exclude: (path) => filters.some((f) => !f(path, false))}; +}; + +const collect_source_files = async ( + dir: string, + filter?: PathFilter | Array, +): Promise> => { + const found = await fs_search(dir, { + filter, + file_filter: (path) => FORMATTABLE_MATCHER.test(path), + }); + const file_ids = found.map((f) => f.id); + if (dir === paths.source) { + for (const name of ROOT_FILES_DEFAULT) { + file_ids.push(join(paths.root, name)); } } - const spawned = await spawn_cli(prettier_cli, serialized_args, log); - if (!spawned) - throw Error( - `failed to find \`${to_cli_name(prettier_cli)}\` CLI locally or globally, do you need to run \`${pm_cli} install\`?`, - ); - return spawned; + return file_ids; }; diff --git a/src/lib/format_file.ts b/src/lib/format_file.ts index be164166b2..4d602c9c4d 100644 --- a/src/lib/format_file.ts +++ b/src/lib/format_file.ts @@ -1,46 +1,84 @@ -import prettier from 'prettier'; +import {format_css, format_svelte, format_typescript} from '@fuzdev/tsv_wasm'; import {extname} from 'node:path'; -import {package_json_load} from './package_json.ts'; +/** + * The source languages Gro can format in-process, backed by + * `@fuzdev/tsv_wasm` (`typescript`/`svelte`/`css`) plus a builtin `json` + * formatter. Anything else passes through unchanged. + */ +export type FormatLang = 'typescript' | 'svelte' | 'css' | 'json'; -let cached_base_options: prettier.Options | undefined; +export interface FormatFileOptions { + /** The file path, used to infer the language from its extension. */ + filepath?: string; + /** The language to format as, overriding any inference from `filepath`. */ + lang?: FormatLang; +} /** - * Formats a file with Prettier. - * @param content - * @param options - * @param base_options - defaults to the cwd's `package.json` `prettier` value + * Formats a string of source code in-process. + * Passes the input through unchanged when the language is unsupported (an + * expected no-op), but throws when the formatter rejects the source — e.g. a + * syntax error — so callers can decide whether to log, skip, or fail rather + * than silently treating broken input as already-formatted. + * @param content - the source to format + * @param options - a `filepath` to infer the language, or an explicit `lang` */ -export const format_file = async ( - content: string, - options: prettier.Options, - base_options: prettier.Options | null | undefined = cached_base_options, -): Promise => { - const final_base_options = - base_options !== undefined - ? base_options - : (cached_base_options = (await package_json_load()).prettier as any); - let final_options = options; - if (options.filepath && !options.parser) { - const {filepath, ...rest} = options; - const parser = infer_parser(filepath); - if (parser) final_options = {...rest, parser}; +export const format_file = (content: string, options: FormatFileOptions = {}): string => { + const lang = options.lang ?? (options.filepath ? infer_lang(options.filepath) : null); + switch (lang) { + case 'typescript': + return format_typescript(content); + case 'svelte': + return format_svelte(content); + case 'css': + return format_css(content); + case 'json': + return format_json(content); + default: + return content; } +}; + +/** + * Formats JSON with tabs. + * Passes non-strict JSON (e.g. JSONC like `tsconfig.json` with comments) + * through unchanged rather than throwing — unparseable JSON is usually + * intentional, not a formatting error. + * Forward-compatible with a future `tsv` JSON formatter. + */ +const format_json = (content: string): string => { + let parsed; try { - return await prettier.format(content, {...final_base_options, ...final_options}); + parsed = JSON.parse(content); } catch (_err) { return content; } + return JSON.stringify(parsed, null, '\t') + '\n'; }; -// This is just a simple convenience for callers so they can pass a file path. -// They can provide the Prettier `options.parser` for custom extensions. -const infer_parser = (path: string): string | null => { - const extension = extname(path).substring(1); - switch (extension) { - case 'svelte': - case 'xml': { - return extension; +/** + * Infers the format language from a file path's extension. + * Returns `null` for extensions Gro doesn't format. + */ +const infer_lang = (path: string): FormatLang | null => { + switch (extname(path).substring(1)) { + case 'ts': + case 'mts': + case 'cts': + case 'js': + case 'mjs': + case 'cjs': { + return 'typescript'; + } + case 'svelte': { + return 'svelte'; + } + case 'css': { + return 'css'; + } + case 'json': { + return 'json'; } default: { return null; diff --git a/src/lib/gen.task.ts b/src/lib/gen.task.ts index 24c14a4e37..2a0b2c1c5a 100644 --- a/src/lib/gen.task.ts +++ b/src/lib/gen.task.ts @@ -125,7 +125,12 @@ export const task: Task = { log.info( format_gen_output(output_lines) + - `\n\n\t${new_count} ${st(new_count > 0 ? 'green' : 'gray', 'new')}, ${changed_count} ${st(changed_count > 0 ? 'cyan' : 'gray', 'changed')}, ${unchanged_count} ${st('gray', 'unchanged')}${error_count ? `, ${error_count} ${st('red', 'error' + plural(error_count))}` : ''} from ${gen_results.input_count} input file${plural(gen_results.input_count)}`, + `\n\n\t${new_count} ${st(new_count > 0 ? 'green' : 'gray', 'new')}, ${changed_count} ${st( + changed_count > 0 ? 'cyan' : 'gray', + 'changed', + )}, ${unchanged_count} ${st('gray', 'unchanged')}${ + error_count ? `, ${error_count} ${st('red', 'error' + plural(error_count))}` : '' + } from ${gen_results.input_count} input file${plural(gen_results.input_count)}`, ); if (fail_count) { @@ -200,7 +205,9 @@ const format_gen_output = (output_lines: Array): string => { for (const line of output_lines) { const elapsed_text = line.elapsed.padStart(max_elapsed_length); const source_text = line.source.padEnd(max_source_length); - log_result += `\n\t${st(line.status.color, line.status.symbol)} ${elapsed_text} ${source_text} → ${line.target}`; + log_result += `\n\t${st(line.status.color, line.status.symbol)} ${elapsed_text} ${ + source_text + } → ${line.target}`; } return log_result; }; diff --git a/src/lib/gen.ts b/src/lib/gen.ts index db257eefd2..e5a258ac99 100644 --- a/src/lib/gen.ts +++ b/src/lib/gen.ts @@ -165,14 +165,18 @@ export const to_output_file_name = (filename: string): string => { } if (gen_pattern_index !== parts.lastIndexOf(GEN_FILE_PATTERN_TEXT)) { throw Error( - `Invalid gen file name - multiple instances of '${GEN_FILE_PATTERN_TEXT}' found in '${filename}'`, + `Invalid gen file name - multiple instances of '${GEN_FILE_PATTERN_TEXT}' found in '${ + filename + }'`, ); } if (gen_pattern_index < parts.length - 3) { // This check is technically unneccessary, // but ensures a consistent file naming convention. throw Error( - `Invalid gen file name - only one additional extension is allowed to follow '${GEN_FILE_PATTERN}' in '${filename}'`, + `Invalid gen file name - only one additional extension is allowed to follow '${ + GEN_FILE_PATTERN + }' in '${filename}'`, ); } const final_parts: Array = []; @@ -202,13 +206,13 @@ export type AnalyzedGenResult = existing_content: string; is_new: false; has_changed: boolean; - } + } | { file: GenFile; existing_content: null; is_new: true; has_changed: true; - }; + }; export const analyze_gen_results = async ( gen_results: GenResults, @@ -276,7 +280,7 @@ export type FindGenfilesFailure = unmapped_input_paths: Array; resolved_input_paths: Array; reasons: Array; - } + } | { type: 'input_directories_with_no_files'; input_directories_with_no_files: Array; @@ -284,7 +288,7 @@ export type FindGenfilesFailure = resolved_input_files_by_root_dir: Map>; resolved_input_paths: Array; reasons: Array; - }; + }; /** * Finds modules from input paths. (see `input_path.ts` for more) diff --git a/src/lib/gro_config.ts b/src/lib/gro_config.ts index 0beb33071b..c93e3e6825 100644 --- a/src/lib/gro_config.ts +++ b/src/lib/gro_config.ts @@ -146,9 +146,13 @@ export const SEARCH_EXCLUDER_DEFAULT = new RegExp( `(${ '(^|/)\\.[^/]+' + // exclude all `.`-prefixed directories // TODO probably change to `pkg.name` instead of this catch-all (also `gro` below) - `|(^|/)${NODE_MODULES_DIRNAME}(?!/(@[^/]+/)?gro/${SVELTEKIT_DIST_DIRNAME})` + // exclude `node_modules` unless it's to the Gro directory + `|(^|/)${NODE_MODULES_DIRNAME}(?!/(@[^/]+/)?gro/${ + SVELTEKIT_DIST_DIRNAME + })` + // exclude `node_modules` unless it's to the Gro directory `|(^|/)${SVELTEKIT_BUILD_DIRNAME}` + // exclude the SvelteKit build directory - `|(^|/)(? 0) { log.info( - `[gen] re-running for ${queued_files.size} more queued file${queued_files.size === 1 ? '' : 's'}`, + `[gen] re-running for ${queued_files.size} more queued file${ + queued_files.size === 1 ? '' : 's' + }`, ); setTimeout(flush_gen_queue); // setTimeout is needed bc of throttle behavior } else { diff --git a/src/lib/gro_plugin_sveltekit_app.ts b/src/lib/gro_plugin_sveltekit_app.ts index 0de6eeaaaf..c7a540846a 100644 --- a/src/lib/gro_plugin_sveltekit_app.ts +++ b/src/lib/gro_plugin_sveltekit_app.ts @@ -43,7 +43,9 @@ export const gro_plugin_sveltekit_app = ({ const spawned = await spawn_cli(found_vite_cli, serialized_args, log); if (!spawned?.ok) { throw new TaskError( - `${vite_cli} build failed: ${spawned ? spawn_result_to_message(spawned) : 'unknown error'}`, + `${vite_cli} build failed: ${ + spawned ? spawn_result_to_message(spawned) : 'unknown error' + }`, ); } } diff --git a/src/lib/input_path.ts b/src/lib/input_path.ts index 39dd3571a4..99befa7e13 100644 --- a/src/lib/input_path.ts +++ b/src/lib/input_path.ts @@ -195,7 +195,12 @@ export const resolve_input_files = async ( let remaining = resolved_input_paths.slice(); const handle_found = (input_path: InputPath, id: PathId) => { remaining = remaining.filter( - (r) => !(r.id === id || r.input_path === input_path || r.input_path === id), // `r.input_path === id` may be unnecessary + (r) => + !( + r.id === id || + r.input_path === input_path || + r.input_path === id + ), // `r.input_path === id` may be unnecessary ); }; diff --git a/src/lib/invoke_task.ts b/src/lib/invoke_task.ts index e46ef79684..7474ba0207 100644 --- a/src/lib/invoke_task.ts +++ b/src/lib/invoke_task.ts @@ -114,7 +114,9 @@ export const invoke_task = async ( if (loaded_tasks.modules.length !== 1) throw Error('expected one loaded task'); // run only one task at a time const task = loaded_tasks.modules[0]!; log.info( - `→ ${st('cyan', task.name)} ${(task.mod.task.summary && st('gray', task.mod.task.summary)) ?? ''}`, + `→ ${st('cyan', task.name)} ${ + (task.mod.task.summary && st('gray', task.mod.task.summary)) ?? '' + }`, ); const timing_to_run_task = timings.start('run task ' + task_name); diff --git a/src/lib/modules.ts b/src/lib/modules.ts index ae92d5ddb3..a7a78a5a46 100644 --- a/src/lib/modules.ts +++ b/src/lib/modules.ts @@ -22,7 +22,7 @@ export type LoadModuleFailure = id: PathId; mod: Record; validation: string; - }; + }; export const load_module = async >( id: PathId, diff --git a/src/lib/publish.task.ts b/src/lib/publish.task.ts index 3020fc82b0..bcd01e818e 100644 --- a/src/lib/publish.task.ts +++ b/src/lib/publish.task.ts @@ -214,7 +214,9 @@ export const task: Task = { const changeset_publish_result = await spawn_cli(found_changeset_cli, ['publish'], log); if (!changeset_publish_result?.ok) { throw new TaskError( - `\`${changeset_cli} publish\` failed - continue manually or try again after running \`git reset --hard\``, + `\`${ + changeset_cli + } publish\` failed - continue manually or try again after running \`git reset --hard\``, ); } diff --git a/src/lib/reinstall.task.ts b/src/lib/reinstall.task.ts index 7b91f57c2a..026045ff6f 100644 --- a/src/lib/reinstall.task.ts +++ b/src/lib/reinstall.task.ts @@ -20,7 +20,9 @@ export const task: Task = { // Deleting both the lockfile and node_modules upgrades to the latest minor/patch versions. await Promise.all([rm(LOCKFILE_FILENAME), rm(NODE_MODULES_DIRNAME, {recursive: true})]); log.info( - `running \`${config.pm_cli} install\` after deleting ${LOCKFILE_FILENAME} and ${NODE_MODULES_DIRNAME}, this can take a while...`, + `running \`${config.pm_cli} install\` after deleting ${LOCKFILE_FILENAME} and ${ + NODE_MODULES_DIRNAME + }, this can take a while...`, ); await install_with_cache_healing_or_throw(config.pm_cli, { log, diff --git a/src/lib/run_gen.ts b/src/lib/run_gen.ts index 79ef877d63..ec83794b50 100644 --- a/src/lib/run_gen.ts +++ b/src/lib/run_gen.ts @@ -70,12 +70,13 @@ export const run_gen = async ( // Convert the module's return value to a normalized form. const gen_result = to_gen_result(id, raw_gen_result); - // Format the files if needed. + // Format the files if needed. `format_file` is synchronous and throws + // on a real formatting error, so log and fall back to the unformatted file. const files = format_file - ? await map_concurrent(gen_result.files, 10, async (file) => { + ? gen_result.files.map((file) => { if (!file.format) return file; try { - return {...file, content: await format_file(file.content, {filepath: file.id})}; + return {...file, content: format_file(file.content, {filepath: file.id})}; } catch (error) { log.error( st('red', `Error formatting ${print_path(file.id)} via ${print_path(id)}`), diff --git a/src/lib/run_task.ts b/src/lib/run_task.ts index 83674905da..304e816536 100644 --- a/src/lib/run_task.ts +++ b/src/lib/run_task.ts @@ -15,12 +15,12 @@ export type RunTaskResult = | { ok: true; output: unknown; - } + } | { ok: false; reason: string; error: Error; - }; + }; export const run_task = async ( task_meta: TaskModuleMeta, @@ -44,7 +44,9 @@ export const run_task = async ( const parsed = args_parse(unparsed_args, task.Args); if (!parsed.success) { throw new TaskError( - `Failed task args validation for task '${task_meta.name}':\n${z.prettifyError(parsed.error)}`, + `Failed task args validation for task '${task_meta.name}':\n${z.prettifyError( + parsed.error, + )}`, ); } args = parsed.data; diff --git a/src/lib/sveltekit_helpers.ts b/src/lib/sveltekit_helpers.ts index 724c89cf3e..1394406981 100644 --- a/src/lib/sveltekit_helpers.ts +++ b/src/lib/sveltekit_helpers.ts @@ -59,7 +59,9 @@ export const sveltekit_sync = async ( const result = await spawn_cli(sveltekit_cli, ['sync']); if (!result) { throw new TaskError( - `Failed to find SvelteKit CLI \`${to_cli_name(sveltekit_cli)}\`, do you need to run \`${pm_cli} install\`?`, + `Failed to find SvelteKit CLI \`${to_cli_name(sveltekit_cli)}\`, do you need to run \`${ + pm_cli + } install\`?`, ); } else if (!result.ok) { throw new TaskError(`Failed ${to_cli_name(sveltekit_cli)} sync`); @@ -164,7 +166,9 @@ export const run_svelte_package = async ( const found_svelte_package_cli = cli === cli_name ? await find_cli(cli) : (cli as Cli); if (found_svelte_package_cli?.kind !== 'local') { throw new TaskError( - `Failed to find SvelteKit packaging CLI \`${cli_name}\`, do you need to run \`${pm_cli} install\`?`, + `Failed to find SvelteKit packaging CLI \`${cli_name}\`, do you need to run \`${ + pm_cli + } install\`?`, ); } const serialized_args = args_serialize({ diff --git a/src/lib/task.ts b/src/lib/task.ts index 71300ee4fc..9e68ec5120 100644 --- a/src/lib/task.ts +++ b/src/lib/task.ts @@ -23,7 +23,10 @@ import type {Filer} from './filer.ts'; export interface Task< TArgs = Args, - TArgsSchema extends z.ZodType = z.ZodType, // TODO improve type? separate input/output? + TArgsSchema extends z.ZodType = z.ZodType< + Args, + Args + >, // TODO improve type? separate input/output? TReturn = unknown, > { run: (ctx: TaskContext) => TReturn | Promise; // TODO unused return value @@ -110,7 +113,7 @@ export type FindModulesFailure = input_paths: Array; task_root_dirs: Array; reasons: Array; - } + } | { type: 'input_directories_with_no_files'; input_directories_with_no_files: Array; @@ -120,7 +123,7 @@ export type FindModulesFailure = input_paths: Array; task_root_dirs: Array; reasons: Array; - }; + }; /** * Finds modules from input paths. (see `input_path.ts` for more) diff --git a/src/lib/typecheck.task.ts b/src/lib/typecheck.task.ts index 55198608b7..20fde1fdd8 100644 --- a/src/lib/typecheck.task.ts +++ b/src/lib/typecheck.task.ts @@ -75,7 +75,9 @@ export const task: Task = { } throw new TaskError( - `Failed to typecheck because neither \`${svelte_check_cli}\` nor \`${typescript_cli}\` was found`, + `Failed to typecheck because neither \`${svelte_check_cli}\` nor \`${ + typescript_cli + }\` was found`, ); }, }; diff --git a/src/lib/upgrade.task.ts b/src/lib/upgrade.task.ts index 810ec13bc2..4b9b459f2b 100644 --- a/src/lib/upgrade.task.ts +++ b/src/lib/upgrade.task.ts @@ -98,7 +98,9 @@ export const task: Task = { if (only.length && only.length !== deps.length) { throw new TaskError( - `Some deps to upgrade were not found: ${only.filter((o) => !deps.find((d) => d.name === o)).join(', ')}`, + `Some deps to upgrade were not found: ${only + .filter((o) => !deps.find((d) => d.name === o)) + .join(', ')}`, ); } diff --git a/src/test/fixtures/changelog_cache.json b/src/test/fixtures/changelog_cache.json index 0182e3788f..c47ba05d6c 100644 --- a/src/test/fixtures/changelog_cache.json +++ b/src/test/fixtures/changelog_cache.json @@ -10,7 +10,9 @@ "id": 1573508138, "html_url": "https://github.com/fuzdev/gro/pull/429", "number": 429, - "user": {"login": "ryanatkn"} + "user": { + "login": "ryanatkn" + } } ], "etag": "W/\"4912dcf67e17b0ea3e68ca2727dcfbedfd92983b2ff46b8ae58ce2b062a38c45\"", @@ -28,7 +30,9 @@ "id": 1639829810, "html_url": "https://github.com/fuzdev/gro/pull/437", "number": 437, - "user": {"login": "ryanatkn"} + "user": { + "login": "ryanatkn" + } } ], "etag": "W/\"1fdf4fc5a79e58799d9ee4876aa55fa0534950f9cb2a56ced4b95df74879ad9d\"", @@ -126,7 +130,9 @@ "id": 1593503608, "html_url": "https://github.com/fuzdev/gro/pull/434", "number": 434, - "user": {"login": "ryanatkn"} + "user": { + "login": "ryanatkn" + } } ], "etag": "W/\"b26ed2c9b5eb0e59fdb65c82dd7aa053651769f0438e3a1ab107524ace8b706f\"", diff --git a/src/test/fixtures/test_sveltekit_env_subprocess.ts b/src/test/fixtures/test_sveltekit_env_subprocess.ts index 1b5b9eadfc..e23c66382e 100644 --- a/src/test/fixtures/test_sveltekit_env_subprocess.ts +++ b/src/test/fixtures/test_sveltekit_env_subprocess.ts @@ -22,7 +22,9 @@ async function runTest() { if (fixture.exported_env_static_public !== VALUE) { console.error( - `✗ Static import test failed: expected "${VALUE}", got "${fixture.exported_env_static_public}"`, + `✗ Static import test failed: expected "${VALUE}", got "${ + fixture.exported_env_static_public + }"`, ); process.exit(1); } @@ -34,7 +36,9 @@ async function runTest() { if (!env_mod.PUBLIC_SOME_PUBLIC_ENV_VAR || env_mod.PUBLIC_SOME_PUBLIC_ENV_VAR !== VALUE) { console.error( - `✗ Dynamic import test failed: expected "${VALUE}", got "${env_mod.PUBLIC_SOME_PUBLIC_ENV_VAR}"`, + `✗ Dynamic import test failed: expected "${VALUE}", got "${ + env_mod.PUBLIC_SOME_PUBLIC_ENV_VAR + }"`, ); process.exit(1); } diff --git a/src/test/format_file.test.ts b/src/test/format_file.test.ts index 7e73eb7e21..2d4fe1fccb 100644 --- a/src/test/format_file.test.ts +++ b/src/test/format_file.test.ts @@ -2,16 +2,43 @@ import {test, expect} from 'vitest'; import {format_file} from '$lib/format_file.ts'; -test('format ts', async () => { +test('format ts', () => { const ts_unformatted = 'hey (1)'; const ts_formatted = 'hey(1);\n'; - expect(await format_file(ts_unformatted, {filepath: 'foo.ts'})).toBe(ts_formatted); - expect(await format_file(ts_unformatted, {parser: 'typescript'})).toBe(ts_formatted); + expect(format_file(ts_unformatted, {filepath: 'foo.ts'})).toBe(ts_formatted); + expect(format_file(ts_unformatted, {lang: 'typescript'})).toBe(ts_formatted); }); -test('format svelte', async () => { +test('format js', () => { + expect(format_file('const x=1', {filepath: 'foo.js'})).toBe('const x = 1;\n'); +}); + +test('format svelte', () => { const svelte_unformatted = ''; const svelte_formatted = '\n'; - expect(await format_file(svelte_unformatted, {filepath: 'foo.svelte'})).toBe(svelte_formatted); - expect(await format_file(svelte_unformatted, {parser: 'svelte'})).toBe(svelte_formatted); + expect(format_file(svelte_unformatted, {filepath: 'foo.svelte'})).toBe(svelte_formatted); + expect(format_file(svelte_unformatted, {lang: 'svelte'})).toBe(svelte_formatted); +}); + +test('format css', () => { + expect(format_file('a{color:red}', {filepath: 'foo.css'})).toBe('a {\n\tcolor: red;\n}\n'); +}); + +test('format json with tabs', () => { + expect(format_file('{"b":1,"a":2}', {filepath: 'foo.json'})).toBe('{\n\t"b": 1,\n\t"a": 2\n}\n'); +}); + +test('unsupported extension passes through unchanged', () => { + const md = '# hi\n\n\nextra'; + expect(format_file(md, {filepath: 'foo.md'})).toBe(md); + expect(format_file(md)).toBe(md); +}); + +test('non-strict json (jsonc) passes through unchanged', () => { + const jsonc = '{\n\t// a comment\n\t"a": 1\n}'; + expect(format_file(jsonc, {filepath: 'tsconfig.json'})).toBe(jsonc); +}); + +test('invalid source throws', () => { + expect(() => format_file('const = =', {filepath: 'foo.ts'})).toThrow(); }); diff --git a/tsconfig.json b/tsconfig.json index 0300902f5a..6e255c09cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { - "types": ["@sveltejs/kit"], + "types": [ + "@sveltejs/kit" + ], "module": "nodenext", "moduleResolution": "nodenext", "strict": true,