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
22 changes: 15 additions & 7 deletions apps/oxfmt/src-js/cli.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { format } from "./bindings.js";
import { setupConfig, formatEmbeddedCode, formatFile } from "./prettier-proxy.js";
import { runInit } from "./migration/init.js";

const args = process.argv.slice(2);
void (async () => {
const args = process.argv.slice(2);

// Call the Rust formatter with our JS callback
const success = await format(args, setupConfig, formatEmbeddedCode, formatFile);
// Handle `--init` command in JS
if (args.includes("--init")) {
return await runInit();
}

// NOTE: It's recommended to set `process.exitCode` instead of calling `process.exit()`.
// `process.exit()` kills the process immediately and `stdout` may not be flushed before process dies.
// https://nodejs.org/api/process.html#processexitcode
if (!success) process.exitCode = 1;
// Call the Rust formatter with our JS callback
const success = await format(args, setupConfig, formatEmbeddedCode, formatFile);

// NOTE: It's recommended to set `process.exitCode` instead of calling `process.exit()`.
// `process.exit()` kills the process immediately and `stdout` may not be flushed before process dies.
// https://nodejs.org/api/process.html#processexitcode
if (!success) process.exitCode = 1;
})();
52 changes: 52 additions & 0 deletions apps/oxfmt/src-js/migration/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* oxlint-disable no-console */

import { stat, writeFile } from "node:fs/promises";

async function isFile(path: string) {
try {
const stats = await stat(path);
return stats.isFile();
} catch {
return false;
}
}

/**
* Run the `--init` command to scaffold a default `.oxfmtrc.json` file.
*/
export async function runInit() {
// Check if config file already exists
if ((await isFile(".oxfmtrc.json")) || (await isFile(".oxfmtrc.jsonc"))) {
console.error("Configuration file already exists.");
process.exitCode = 1;
return;
}

// Build config object
const schemaPath = "./node_modules/oxfmt/configuration_schema.json";

const config: Record<string, unknown> = {
// Add `$schema` field at the top if schema file exists in `node_modules`
$schema: schemaPath,
// `ignorePatterns` is included to make visible and preferred over `.prettierignore`
ignorePatterns: [],
};

// Remove if this command is run with e.g. `npx`
// NOTE: To keep `$schema` field at the top, we delete it here instead of defining conditionally above
if (!(await isFile(schemaPath))) {
delete config.$schema;
}

try {
const jsonStr = JSON.stringify(config, null, 2);

// TODO: Call napi `validateConfig()` to ensure validity

await writeFile(".oxfmtrc.json", jsonStr + "\n");
console.log("Created `.oxfmtrc.json`.");
} catch {
console.error("Failed to write `.oxfmtrc.json`.");
process.exitCode = 1;
}
}
10 changes: 6 additions & 4 deletions apps/oxfmt/src/cli/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ pub enum Mode {
/// Default CLI mode run against files and directories
Cli(OutputMode),
#[cfg(feature = "napi")]
/// Initialize `.oxfmtrc.jsonc` with default values
Init,
#[cfg(feature = "napi")]
/// Start language server protocol (LSP) server
Lsp,
#[cfg(feature = "napi")]
/// Initialize `.oxfmtrc.json` with default values
// NOTE: This is handled by JS side before reaching Rust.
// Just to display help message correctly.
Init,
}

fn mode() -> impl bpaf::Parser<Mode> {
Expand All @@ -57,7 +59,7 @@ fn mode() -> impl bpaf::Parser<Mode> {
#[cfg(feature = "napi")]
{
let init = bpaf::long("init")
.help("Initialize `.oxfmtrc.jsonc` with default values")
.help("Initialize `.oxfmtrc.json` with default values")
.req_flag(Mode::Init)
.hide_usage();
let lsp = bpaf::long("lsp")
Expand Down
11 changes: 3 additions & 8 deletions apps/oxfmt/src/cli/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,20 @@ pub enum CliRunResult {
// Success
None,
FormatSucceeded,
InitSucceeded,
// Warning error
InvalidOptionConfig,
FormatMismatch,
InitAborted,
// Fatal error
NoFilesFound,
FormatFailed,
InitFailed,
}

impl Termination for CliRunResult {
fn report(self) -> ExitCode {
match self {
Self::None | Self::FormatSucceeded | Self::InitSucceeded => ExitCode::from(0),
Self::InvalidOptionConfig | Self::FormatMismatch | Self::InitAborted => {
ExitCode::from(1)
}
Self::NoFilesFound | Self::FormatFailed | Self::InitFailed => ExitCode::from(2),
Self::None | Self::FormatSucceeded => ExitCode::from(0),
Self::InvalidOptionConfig | Self::FormatMismatch => ExitCode::from(1),
Self::NoFilesFound | Self::FormatFailed => ExitCode::from(2),
}
}
}
59 changes: 0 additions & 59 deletions apps/oxfmt/src/init/mod.rs

This file was deleted.

1 change: 0 additions & 1 deletion apps/oxfmt/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
pub mod cli;
mod core;
pub mod init;
pub mod lsp;

// Only include code to run formatter when the `napi` feature is enabled.
Expand Down
3 changes: 1 addition & 2 deletions apps/oxfmt/src/main_napi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use crate::{
CliRunResult, FormatRunner, Mode, format_command, init_miette, init_rayon, init_tracing,
},
core::{ExternalFormatter, JsFormatEmbeddedCb, JsFormatFileCb, JsSetupConfigCb},
init::run_init,
lsp::run_lsp,
};

Expand Down Expand Up @@ -68,7 +67,7 @@ async fn format_impl(
};

match command.mode {
Mode::Init => run_init(),
Mode::Init => unreachable!("`--init` should be handled by JS side"),
Mode::Lsp => {
run_lsp().await;
CliRunResult::None
Expand Down
82 changes: 82 additions & 0 deletions apps/oxfmt/test/init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { join } from "node:path";
import { tmpdir } from "node:os";
import fs from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { runCli } from "./utils";

describe("init", () => {
it("should create .oxfmtrc.json", async () => {
const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-init-test-"));

try {
const result = await runCli(tempDir, ["--init"]);

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Created `.oxfmtrc.json`.");

const configPath = join(tempDir, ".oxfmtrc.json");
const content = await fs.readFile(configPath, "utf8");
const config = JSON.parse(content);

expect(config.ignorePatterns).toEqual([]);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});

it("should add $schema when node_modules/oxfmt exists", async () => {
const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-init-test-"));

try {
// Create fake node_modules/oxfmt/configuration_schema.json
const schemaDir = join(tempDir, "node_modules", "oxfmt");
await fs.mkdir(schemaDir, { recursive: true });
await fs.writeFile(join(schemaDir, "configuration_schema.json"), "{}");

const result = await runCli(tempDir, ["--init"]);

expect(result.exitCode).toBe(0);

const configPath = join(tempDir, ".oxfmtrc.json");
const content = await fs.readFile(configPath, "utf8");
const config = JSON.parse(content);

expect(config.$schema).toBe("./node_modules/oxfmt/configuration_schema.json");
expect(Object.keys(config)[0]).toBe("$schema"); // $schema should be first
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});

it("should abort if .oxfmtrc.json already exists", async () => {
const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-init-test-"));

try {
// Create existing config file
await fs.writeFile(join(tempDir, ".oxfmtrc.json"), "{}");

const result = await runCli(tempDir, ["--init"]);

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("Configuration file already exists.");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});

it("should abort if .oxfmtrc.jsonc already exists", async () => {
const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-init-test-"));

try {
// Create existing config file
await fs.writeFile(join(tempDir, ".oxfmtrc.jsonc"), "{}");

const result = await runCli(tempDir, ["--init"]);

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("Configuration file already exists.");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
});
4 changes: 2 additions & 2 deletions apps/oxfmt/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ ${afterContent}

// ---

type RunResult = {
export type RunResult = {
stdout: string;
stderr: string;
exitCode: number;
};

async function runCli(cwd: string, args: string[]): Promise<RunResult> {
export async function runCli(cwd: string, args: string[]): Promise<RunResult> {
const cliPath = join(import.meta.dirname, "..", "dist", "cli.js");

const result = await execa("node", [cliPath, ...args], {
Expand Down
2 changes: 1 addition & 1 deletion tasks/website_formatter/src/snapshots/cli.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ search: false

## Mode Options:
- **` --init`** &mdash;
Initialize `.oxfmtrc.jsonc` with default values
Initialize `.oxfmtrc.json` with default values
- **` --lsp`** &mdash;
Start language server protocol (LSP) server

Expand Down
2 changes: 1 addition & 1 deletion tasks/website_formatter/src/snapshots/cli_terminal.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ expression: snapshot
Usage: [-c=PATH] [PATH]...

Mode Options:
--init Initialize `.oxfmtrc.jsonc` with default values
--init Initialize `.oxfmtrc.json` with default values
--lsp Start language server protocol (LSP) server

Output Options:
Expand Down
Loading