Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"cspell-ban-words": "catalog:",
"heading-case": "catalog:",
"prettier": "catalog:",
"prettier-plugin-packagejson": "catalog:",
"rstack": "workspace:*",
"typescript": "catalog:"
},
Expand Down
51 changes: 51 additions & 0 deletions packages/rstack/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,57 @@ 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.

## sort-package-json

This package includes bundled code from
[sort-package-json](https://github.com/keithamus/sort-package-json).

License: MIT

Copyright (c) 2015 Keith Cirkel

The bundled code also contains MIT-licensed code from:

- detect-indent 7.0.2, copyright Sindre Sorhus
- detect-newline 4.0.1, copyright Sindre Sorhus
- git-hooks-list 4.2.1, copyright fisker Cheung
- is-plain-obj 4.1.0, copyright Sindre Sorhus
- sort-object-keys 2.1.0, copyright Keith Cirkel

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 notices 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.

The bundled code also contains semver 7.8.5, licensed under the ISC License:

Copyright (c) Isaac Z. Schlueter and Contributors

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

## tinyexec

This package includes bundled code from [tinyexec](https://github.com/tinylibs/tinyexec).
Expand Down
1 change: 1 addition & 0 deletions packages/rstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"lint-staged": "catalog:",
"micromatch": "catalog:",
"rslog": "catalog:",
"sort-package-json": "catalog:",
"tiny-readdir": "catalog:",
"typescript": "catalog:",
"worktank": "catalog:"
Expand Down
10 changes: 7 additions & 3 deletions packages/rstack/src/fmt/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { dirname, relative } from 'node:path';
import micromatch from 'micromatch';
import type { Options as PrettierOptions } from 'prettier';
import type { FmtConfig, FmtConfigDefinition, ResolvedFmtConfig } from './types.ts';
import type {
FmtConfig,
FmtConfigDefinition,
ResolvedFmtConfig,
ResolvedFmtOptions,
} from './types.ts';

type ResolveFmtConfigOptions = {
definition: FmtConfigDefinition | undefined;
Expand Down Expand Up @@ -45,7 +49,7 @@ const pathMatchesGlobs = (
};

/** Applies matching overrides to the shared formatter options. */
const resolveFmtOptions = (filePath: string, config: ResolvedFmtConfig): PrettierOptions => {
const resolveFmtOptions = (filePath: string, config: ResolvedFmtConfig): ResolvedFmtOptions => {
if (config.overrides.length === 0) {
return config.baseOptions;
}
Expand Down
5 changes: 3 additions & 2 deletions packages/rstack/src/fmt/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,20 @@ const formatText = async (
...options,
filepath: filePath,
parser,
plugins: getPrettierPlugins(options.plugins),
};
const plugins = await getPrettierPlugins(formatOptions);

if (cursorOffset === undefined) {
return {
status: 'formatted',
formatted: await format(source, formatOptions),
formatted: await format(source, { ...formatOptions, plugins }),
};
}

const result = await formatWithCursor(source, {
...formatOptions,
cursorOffset,
plugins,
});

return {
Expand Down
2 changes: 1 addition & 1 deletion packages/rstack/src/fmt/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const resolveFmtParser = async (
(
await getFileInfo(filePath, {
...fileInfoOptions,
plugins: getPrettierPlugins(options.plugins),
plugins: await getPrettierPlugins(options),
})
).inferredParser;

Expand Down
4 changes: 2 additions & 2 deletions packages/rstack/src/fmt/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { isAbsolute, join, resolve as resolvePath } from 'node:path';
import { pathToFileURL } from 'node:url';
import { moduleResolve } from 'import-meta-resolve';
import type { Options as PrettierOptions } from 'prettier';
import type { FmtPluginSpecifier } from './types.ts';
import type { FmtPluginSpecifier, ResolvedFmtOptions } from './types.ts';

type FmtPlugin = NonNullable<PrettierOptions['plugins']>[number];
type FmtPluginResolver = (options: PrettierOptions) => PrettierOptions;
type FmtPluginResolver = (options: ResolvedFmtOptions) => ResolvedFmtOptions;

const resolveModuleUrl = (specifier: string, parentUrl: URL): string =>
moduleResolve(specifier, parentUrl).href;
Expand Down
28 changes: 23 additions & 5 deletions packages/rstack/src/fmt/prettierPlugins.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import * as yukuPlugin from '@prettier/plugin-yuku';
import type { Options as PrettierOptions } from 'prettier';
import type { Options as PrettierOptions, Plugin } from 'prettier';
import type { ResolvedFmtOptions } from './types.ts';

type PrettierPlugins = NonNullable<PrettierOptions['plugins']>;

const defaultFmtPlugins: PrettierPlugins = [yukuPlugin];
const fmtOptionsPlugin = {
options: {
sortPackageJson: {
category: 'Global',
default: false,
description: 'Sort package.json fields using sort-package-json.',
type: 'boolean',
},
},
} satisfies Plugin;

/** Prepends Yuku so project plugins can override the default parser. */
const getPrettierPlugins = (plugins: PrettierOptions['plugins']): PrettierPlugins =>
plugins?.length ? [...defaultFmtPlugins, ...plugins] : defaultFmtPlugins;
const defaultFmtPlugins: PrettierPlugins = [yukuPlugin, fmtOptionsPlugin];

/** Prepends bundled plugins so project plugins can override their parsers. */
const getPrettierPlugins = async (options: ResolvedFmtOptions): Promise<PrettierPlugins> => {
const plugins =
options.sortPackageJson === true && /(^|[/\\])package\.json$/.test(options.filepath ?? '')
? [...defaultFmtPlugins, (await import('./sortPackageJsonPlugin.ts')).sortPackageJsonPlugin]
: defaultFmtPlugins;

return options.plugins?.length ? [...plugins, ...options.plugins] : plugins;
};

export { getPrettierPlugins };
2 changes: 1 addition & 1 deletion packages/rstack/src/fmt/serial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const formatFileSerial = async (
const source = await readFile(path, 'utf8');
const formatted = await format(source, {
...options,
plugins: getPrettierPlugins(options.plugins),
plugins: await getPrettierPlugins(options),
});

if (source === formatted) {
Expand Down
14 changes: 14 additions & 0 deletions packages/rstack/src/fmt/sortPackageJsonPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Plugin } from 'prettier';
import { parsers } from 'prettier/plugins/babel';
import sortPackageJson from 'sort-package-json';

const sortPackageJsonPlugin: Plugin = {
parsers: {
'json-stringify': {
...parsers['json-stringify'],
preprocess: (source) => sortPackageJson(source),
},
},
};

export { sortPackageJsonPlugin };
24 changes: 18 additions & 6 deletions packages/rstack/src/fmt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@ import type { Config as PrettierConfig, Options as PrettierOptions } from 'prett
/** Plugin objects cannot cross worker boundaries and are not planned for support. */
type FmtPluginSpecifier = string | URL;

type FmtOptions = Omit<PrettierOptions, 'plugins'> & {
plugins?: FmtPluginSpecifier[];
};
interface FmtBuiltinOptions {
/**
* Sort `package.json` fields using `sort-package-json`.
* @default false
*/
sortPackageJson?: boolean;
}

type ResolvedFmtOptions = PrettierOptions & FmtBuiltinOptions;

type FmtOptions = Omit<PrettierOptions, 'plugins'> &
FmtBuiltinOptions & {
plugins?: FmtPluginSpecifier[];
};

type PrettierOverride = NonNullable<PrettierConfig['overrides']>[number];

type FmtOverride = Omit<PrettierOverride, 'options'> & {
options?: FmtOptions;
};

interface FmtConfig extends Omit<PrettierConfig, 'plugins' | 'overrides'> {
interface FmtConfig extends Omit<PrettierConfig, 'plugins' | 'overrides'>, FmtBuiltinOptions {
plugins?: FmtPluginSpecifier[];
overrides?: FmtOverride[];
/** Gitignore-compatible patterns relative to the Rstack config root. */
Expand All @@ -27,7 +38,7 @@ interface ResolvedFmtConfig {
/** Root for relative patterns and plugin paths. */
rootPath: string;
/** Shared Prettier options before per-file overrides. */
baseOptions: PrettierOptions;
baseOptions: ResolvedFmtOptions;
/** Per-file override rules. */
overrides: NonNullable<PrettierConfig['overrides']>;
/** Root-relative ignore patterns. */
Expand Down Expand Up @@ -56,7 +67,7 @@ interface FmtFileRequest {
/** Absolute path to the file. */
path: string;
/** Final Prettier options with the parser and file path resolved. */
options: PrettierOptions & Required<Pick<PrettierOptions, 'filepath' | 'parser'>>;
options: ResolvedFmtOptions & Required<Pick<PrettierOptions, 'filepath' | 'parser'>>;
}

type FmtMode = 'write' | 'check' | 'list-different';
Expand Down Expand Up @@ -123,5 +134,6 @@ export type {
FormatTextOptions,
FormatTextResult,
ResolvedFmtConfig,
ResolvedFmtOptions,
RunFmtFilesOptions,
};
37 changes: 37 additions & 0 deletions packages/rstack/tests/cli/fmt/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { afterEach, beforeEach, expect, test } from 'rstack/test';
import { RSTACK_BIN_PATH } from '#test-helpers';

let projectPath: string;
const packageJsonSource =
'{"dependencies":{"z":"1.0.0","a":"1.0.0"},"type":"module","version":"1.0.0","name":"fixture"}';
const sortedPackageJson =
'{\n "name": "fixture",\n "version": "1.0.0",\n "type": "module",\n "dependencies": {\n "a": "1.0.0",\n "z": "1.0.0"\n }\n}\n';

const writeProjectFile = (filePath: string, content: string): void => {
const absolutePath = path.join(projectPath, filePath);
Expand Down Expand Up @@ -98,6 +102,39 @@ test('formats the current directory with Prettier defaults', () => {
expect(readProjectFile('index.ts')).toBe('const message = "hello";\n');
});

test('does not sort package.json by default', () => {
writeProjectFile('package.json', packageJsonSource);

const result = runFmt(['package.json']);

expect(result.status).toBe(0);
expect(readProjectFile('package.json')).toContain(
'"dependencies": {\n "z": "1.0.0",\n "a": "1.0.0"',
);
});

test.each([
['parallel execution', []],
['serial execution', ['--no-parallel']],
] as const)('sorts package.json with %s', (_, options) => {
writeProjectFile(
'rstack.config.ts',
`import { define } from 'rstack';

define.fmt({ sortPackageJson: true });
`,
);
writeProjectFile('package.json', packageJsonSource);
writeProjectFile('packages/example/package.json', packageJsonSource);

const result = runFmt([...options, 'package.json', 'packages/example/package.json']);

expect(result.status).toBe(0);
expect(result.stderr).toBe('');
expect(readProjectFile('package.json')).toBe(sortedPackageJson);
expect(readProjectFile('packages/example/package.json')).toBe(sortedPackageJson);
});

test.each([
['disabling parallel execution', ['--no-parallel']],
['configuring parallel worker count', ['--parallel-workers', '1']],
Expand Down
31 changes: 31 additions & 0 deletions packages/rstack/tests/fmt/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { formatText } from '../../src/fmt/format.ts';
import { withTempProject, writeProjectFile } from './helpers.ts';

const rootPath = import.meta.dirname;
const packageJsonSource =
'{"dependencies":{"z":"1.0.0","a":"1.0.0"},"version":"1.0.0","name":"fixture"}';

test('applies per-file overrides and maps the cursor', async () => {
const source = 'const value={message:"hello"}';
Expand Down Expand Up @@ -63,6 +65,35 @@ test('uses an explicit parser for unknown file extensions', async () => {
});
});

test('does not sort package.json by default', async () => {
const result = await formatText(packageJsonSource, {
config: normalizeFmtConfig(undefined, rootPath),
filePath: path.join(rootPath, 'package.json'),
});

expect(result).toMatchObject({
formatted:
'{\n "dependencies": {\n "z": "1.0.0",\n "a": "1.0.0"\n },\n "version": "1.0.0",\n "name": "fixture"\n}\n',
});
});

test('sorts package.json when enabled', async () => {
const result = await formatText(packageJsonSource, {
config: normalizeFmtConfig(
{
overrides: [{ files: 'package.json', options: { sortPackageJson: true } }],
},
rootPath,
),
filePath: path.join(rootPath, 'package.json'),
});

expect(result).toMatchObject({
formatted:
'{\n "name": "fixture",\n "version": "1.0.0",\n "dependencies": {\n "a": "1.0.0",\n "z": "1.0.0"\n }\n}\n',
});
});

test('supports a plugin path from matching overrides', async () => {
await withTempProject(async (projectPath) => {
writeProjectFile(
Expand Down
Loading