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
37 changes: 37 additions & 0 deletions packages/rstack/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,40 @@ 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.

## vscode-languageserver

The language server started by `rs fmt --lsp` includes bundled code from
[vscode-languageserver](https://github.com/microsoft/vscode-languageserver-node).

License: MIT

The bundled code also contains MIT-licensed code from:

- vscode-jsonrpc 9.0.1, copyright Microsoft Corporation
- vscode-languageserver-protocol 3.18.2, copyright Microsoft Corporation
- vscode-languageserver-types 3.18.0, copyright Microsoft Corporation

Copyright (c) Microsoft Corporation

All rights reserved.

MIT License

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.
4 changes: 3 additions & 1 deletion packages/rstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@
"sort-package-json": "catalog:",
"svelte": "catalog:",
"tiny-readdir": "catalog:",
"typescript": "catalog:"
"typescript": "catalog:",
"vscode-languageserver": "catalog:",
"vscode-languageserver-textdocument": "catalog:"
},
"peerDependencies": {
"@rspress/core": "^2.0.17"
Expand Down
2 changes: 1 addition & 1 deletion packages/rstack/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig } from '@rslib/core';
import prettierPkgJson from 'prettier/package.json' with { type: 'json' };
import pkgJson from './package.json' with { type: 'json' };

const fullyMinifiedChunks = /(?:fmt(?:Plugins)?|sortPackageJsonPlugin|staged)\.js$/;
const fullyMinifiedChunks = /(?:fmt(?:Lsp|Plugins)?|sortPackageJsonPlugin|staged)\.js$/;

export default defineConfig({
dts: true,
Expand Down
9 changes: 7 additions & 2 deletions packages/rstack/src/cli/commands.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { join } from 'node:path';
import { join, resolve } from 'node:path';
import { getConfigState } from '../config.ts';
import { insertConfigArg, parseArgs, parseCliArgs } from './args.ts';
import { hasHelpFlag, renderHelp } from './help.ts';
Expand Down Expand Up @@ -606,7 +606,12 @@ export async function setupCommands(): Promise<void> {
const { args, configPath } = parseCliArgs(process.argv.slice(2));
const command = args[0];

getConfigState().configPath = configPath;
// Resolved for every command so that a relative `--config` path always means
// the same file: it is anchored to the directory the CLI was invoked in, even
// when the config is later loaded from another directory. The motivating case
// is `rs fmt --lsp`, which loads the config from the LSP workspace root the
// client reports, and that root need not be the process working directory.
getConfigState().configPath = configPath === undefined ? undefined : resolve(configPath);

if (!command || command === '-h' || command === '--help') {
console.log(renderRootHelp());
Expand Down
15 changes: 14 additions & 1 deletion packages/rstack/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ export type LoadedRstackConfig = {
export type LoadRstackConfigOptions = {
/**
* The path to the Rstack config file, can be a relative or absolute path.
* A relative path is resolved from `cwd`.
* If `configFilePath` is not provided, the config path set by the CLI is used.
* If neither path is provided, the function will search for the config file in the current working directory.
* If neither path is provided, the function will search for the config file in `cwd`.
*/
configFilePath?: string;
/**
* The directory the config file is searched in and relative config paths are resolved from.
* Defaults to the current working directory.
*/
cwd?: string;
};

type ConfigSession = {
Expand All @@ -42,6 +48,11 @@ type ConfigSession = {
};

type ConfigState = {
/**
* Config file path from the global `--config` flag. Always absolute: the CLI
* resolves it at parse time so it stays independent of later cwd choices
* (`loadRstackConfig` may be called with an LSP workspace root as `cwd`).
*/
configPath?: string;
};

Expand Down Expand Up @@ -161,6 +172,7 @@ export const define: Define = {

export const loadRstackConfig = async ({
configFilePath,
cwd,
}: LoadRstackConfigOptions = {}): Promise<LoadedRstackConfig> => {
const state = getConfigState();
const configPath = configFilePath ?? state.configPath;
Expand All @@ -175,6 +187,7 @@ export const loadRstackConfig = async ({
loader: 'native',
exportName: false,
fresh: true,
cwd,
...(configPath !== undefined
? { path: configPath }
: {
Expand Down
55 changes: 46 additions & 9 deletions packages/rstack/src/fmt/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ interface ParsedFmtCLIArgs {
help: boolean;
/** Path the stdin content is formatted as; it need not exist on disk. */
stdinFilepath?: string;
/** Serve formatting over the Language Server Protocol instead of exiting. */
lsp: boolean;
}

const renderFmtHelp = (): string =>
Expand All @@ -46,6 +48,7 @@ const renderFmtHelp = (): string =>
['--with-node-modules', 'Process files inside node_modules'],
['--parallel-workers <count>', 'Number of parallel workers'],
['--stdin-filepath <path>', 'Format stdin as if it were saved at <path>'],
['--lsp', 'Run a language server on stdio'],
['-c, --config <path>', 'Specify Rstack config file path'],
['-h, --help', 'Display this help message'],
],
Expand All @@ -66,6 +69,19 @@ const parseMaxWorkers = (value: string | undefined): number | undefined => {
return maxWorkers;
};

/** Rejects the mode flags and file arguments that a server-like option replaces. */
const assertExclusiveMode = (option: string, hasMode: boolean, positionals: string[]): void => {
if (hasMode) {
throw new Error(
`The ${option} option cannot be used with --write, --check, or --list-different.`,
);
}

if (positionals.length > 0) {
throw new Error(`The ${option} option cannot be used with file arguments.`);
}
};

const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
const { values, positionals } = parseArgs({
args,
Expand All @@ -81,6 +97,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
'with-node-modules': { type: 'boolean' },
'parallel-workers': { type: 'string' },
'stdin-filepath': { type: 'string' },
lsp: { type: 'boolean' },
help: { type: 'boolean', short: 'h' },
},
allowPositionals: true,
Expand Down Expand Up @@ -110,19 +127,20 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
const maxWorkers = parseMaxWorkers(parallelWorkers);
const help = values.help ?? false;
const stdinFilepath = values.stdinFilepath;
const lsp = values.lsp ?? false;

if (stdinFilepath !== undefined) {
if (modes.length > 0) {
throw new Error(
'The --stdin-filepath option cannot be used with --write, --check, or --list-different.',
);
}
if (lsp) {
assertExclusiveMode('--lsp', modes.length > 0, positionals);

if (positionals.length > 0) {
throw new Error('The --stdin-filepath option cannot be used with file arguments.');
if (stdinFilepath !== undefined) {
throw new Error('The --lsp option cannot be used with --stdin-filepath.');
}
}

if (stdinFilepath !== undefined) {
assertExclusiveMode('--stdin-filepath', modes.length > 0, positionals);
}

return {
cache,
cacheLocation,
Expand All @@ -135,6 +153,7 @@ const parseFmtCLIArgs = (args: string[]): ParsedFmtCLIArgs => {
maxWorkers,
help,
stdinFilepath,
lsp,
};
};

Expand Down Expand Up @@ -244,7 +263,7 @@ const logFmtResult = (
};

const loadFmtConfig = async (cwd: string): Promise<ResolvedFmtConfig> => {
const { configs, filePath } = await loadRstackConfig();
const { configs, filePath } = await loadRstackConfig({ cwd });

return resolveFmtConfig({
definition: configs.fmt,
Expand All @@ -266,6 +285,7 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
help,
ignorePaths,
ignoreUnknown,
lsp,
maxWorkers,
mode,
noErrorOnUnmatchedPattern,
Expand All @@ -278,6 +298,23 @@ const runFmtCLI = async (args: string[]): Promise<void> => {
return;
}

if (lsp) {
const { runFmtLsp } = await import(
/* rspackChunkName: 'fmtLsp' */
'./lsp/server.ts'
);
await runFmtLsp({
// The client's workspace root is not necessarily the directory the
// editor spawned the server in; the server resolves relative
// `--ignore-path` values from this cwd so they stay based on the same
// directory as a relative `--config`.
cwd,
ignorePaths,
loadConfig: loadFmtConfig,
});
return;
}

if (stdinFilepath !== undefined) {
const { runFmtStdin } = await import(
/* rspackChunkName: 'fmtStdin' */
Expand Down
41 changes: 27 additions & 14 deletions packages/rstack/src/fmt/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'node:path';
import { createFmtOptionsResolver, type FmtOptionsResolver } from './config.ts';
import { discoverFmtPaths } from './discoverPaths.ts';
import { createIgnoreMatcher } from './ignore.ts';
import type { FmtPluginResolver } from './plugins.ts';
import type { DiscoverFmtFilesOptions, FmtFileRequest } from './types.ts';

const createFileRequest = (
Expand All @@ -12,6 +13,26 @@ const createFileRequest = (
options: resolveOptions(filePath),
});

/** Imports the plugin chunk on first use and shares the resolver across calls. */
const createLazyPluginResolver = (rootPath: string): (() => Promise<FmtPluginResolver>) => {
let resolver: Promise<FmtPluginResolver> | undefined;

return () =>
(resolver ??= import(
/* rspackChunkName: 'fmtPlugins' */
'./plugins.ts'
).then(({ createFmtPluginResolver }) => createFmtPluginResolver(rootPath)));
};

/** Resolves the plugin specifiers of a request whose options configure plugins. */
const resolveFileRequestPlugins = async (
file: FmtFileRequest,
getPluginResolver: () => Promise<FmtPluginResolver>,
): Promise<FmtFileRequest> =>
file.options.plugins?.length
? { ...file, options: (await getPluginResolver())(file.options) }
: file;

const createDirMatcher = (dirPath: string): ((filePath: string) => boolean) => {
const prefix = dirPath.endsWith(path.sep) ? dirPath : `${dirPath}${path.sep}`;
return (filePath) => filePath === dirPath || filePath.startsWith(prefix);
Expand Down Expand Up @@ -43,21 +64,13 @@ const discoverFmtFiles = async ({
}

const resolveOptions = createFmtOptionsResolver(config);
const files = filePaths.map((filePath) => createFileRequest(filePath, resolveOptions));
if (!files.some((file) => file.options.plugins?.length)) {
return files;
}
const getPluginResolver = createLazyPluginResolver(config.rootPath);

const { createFmtPluginResolver } = await import(
/* rspackChunkName: 'fmtPlugins' */
'./plugins.ts'
return Promise.all(
filePaths.map((filePath) =>
resolveFileRequestPlugins(createFileRequest(filePath, resolveOptions), getPluginResolver),
),
);
const resolvePlugins = createFmtPluginResolver(config.rootPath);

return files.map((file) => ({
...file,
options: resolvePlugins(file.options),
}));
};

export { createFileRequest, discoverFmtFiles };
export { createFileRequest, createLazyPluginResolver, discoverFmtFiles, resolveFileRequestPlugins };
1 change: 1 addition & 0 deletions packages/rstack/src/fmt/ignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ const createIgnoreMatcher = async ({
};

export { createIgnoreMatcher };
export type { IgnorePredicate };
Loading