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
2 changes: 1 addition & 1 deletion apps/oxfmt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"dependencies": {
"prettier": "3.8.1",
"prettier-plugin-tailwindcss": "0.7.2",
"prettier-plugin-tailwindcss": "0.0.0-insiders.2ac6e70",
"tinypool": "2.1.0"
},
"devDependencies": {
Expand Down
38 changes: 16 additions & 22 deletions apps/oxfmt/src-js/libs/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ export async function formatFile({
// ---

// Import types only to avoid runtime error if plugin is not installed
import type { TransformerEnv } from "prettier-plugin-tailwindcss";

// Shared cache for prettier-plugin-tailwindcss
let tailwindPluginCache: typeof import("prettier-plugin-tailwindcss");
Expand Down Expand Up @@ -153,7 +152,12 @@ async function setupTailwindPlugin(options: Options): Promise<void> {
export interface SortTailwindClassesArgs {
filepath: string;
classes: string[];
options?: Record<string, unknown>;
options?: {
tailwindStylesheet?: string;
tailwindConfig?: string;
tailwindPreserveWhitespace?: boolean;
tailwindPreserveDuplicates?: boolean;
} & Options;
}

/**
Expand All @@ -166,27 +170,17 @@ export async function sortTailwindClasses({
classes,
options = {},
}: SortTailwindClassesArgs): Promise<string[]> {
const tailwindPlugin = await loadTailwindPlugin();

// SAFETY: `options` is created in Rust side, so it's safe to mutate here
options.filepath = filepath;

// Load Tailwind context
const context = await tailwindPlugin.getTailwindConfig(options);
if (!context) return classes;

// Create transformer env with options
const env: TransformerEnv = { context, options };

// Sort all classes
return classes.map((classStr) => {
try {
return tailwindPlugin.sortClasses(classStr, { env });
} catch {
// Failed to sort, return original
return classStr;
}
const { createSorter } = await import("prettier-plugin-tailwindcss/sorter");

const sorter = await createSorter({
filepath,
stylesheetPath: options.tailwindStylesheet,
configPath: options.tailwindConfig,
preserveWhitespace: options.tailwindPreserveWhitespace,
preserveDuplicates: options.tailwindPreserveDuplicates,
});

return sorter.sortClassAttributes(classes);
}

// ---
Expand Down
30 changes: 23 additions & 7 deletions apps/oxfmt/src/core/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::path::{Path, PathBuf};
use std::{
env::current_dir,
path::{Path, PathBuf},
};

use editorconfig_parser::{
EditorConfig, EditorConfigProperties, EditorConfigProperty, EndOfLine, IndentStyle,
Expand Down Expand Up @@ -62,9 +65,15 @@ pub fn resolve_options_from_value(
let oxfmt_options = format_config
.into_oxfmt_options()
.map_err(|err| format!("Failed to parse configuration.\n{err}"))?;
sync_external_options(&mut external_options, &oxfmt_options.format_options);

Ok(ResolvedOptions::from_oxfmt_options(oxfmt_options, external_options, strategy))
sync_external_options(&oxfmt_options.format_options, &mut external_options);

Ok(ResolvedOptions::from_oxfmt_options(
current_dir().ok().as_ref(),
oxfmt_options,
external_options,
strategy,
))
}

// ---
Expand Down Expand Up @@ -99,12 +108,13 @@ impl ResolvedOptions {
///
/// This also applies plugin-specific options (Tailwind, oxfmt plugin flags) based on strategy.
fn from_oxfmt_options(
config_dir: Option<&PathBuf>,
oxfmt_options: OxfmtOptions,
mut external_options: Value,
strategy: &FormatFileStrategy,
) -> Self {
// Apply plugin-specific options based on strategy
finalize_external_options(&mut external_options, strategy);
finalize_external_options(config_dir, &mut external_options, strategy);

#[cfg(feature = "napi")]
let OxfmtOptions { format_options, toml_options, sort_package_json, insert_final_newline } =
Expand Down Expand Up @@ -265,7 +275,7 @@ impl ConfigResolver {
// Apply common Prettier mappings for caching.
// Plugin options will be added later in `resolve()` via `finalize_external_options()`.
// If we finalize here, every per-file options contain plugin options even if not needed.
sync_external_options(&mut external_options, &oxfmt_options.format_options);
sync_external_options(&oxfmt_options.format_options, &mut external_options);

// Save cache for fast path: no per-file overrides
self.cached_options = Some((oxfmt_options, external_options));
Expand All @@ -278,7 +288,12 @@ impl ConfigResolver {
#[instrument(level = "debug", name = "oxfmt::config::resolve", skip_all, fields(path = %strategy.path().display()))]
pub fn resolve(&self, strategy: &FormatFileStrategy) -> ResolvedOptions {
let (oxfmt_options, external_options) = self.resolve_options(strategy.path());
ResolvedOptions::from_oxfmt_options(oxfmt_options, external_options, strategy)
ResolvedOptions::from_oxfmt_options(
self.config_dir.as_ref(),
oxfmt_options,
external_options,
strategy,
)
}

/// Resolve options for a specific file path.
Expand Down Expand Up @@ -323,7 +338,8 @@ impl ConfigResolver {
let oxfmt_options = format_config
.into_oxfmt_options()
.expect("If this fails, there is an issue with override values");
sync_external_options(&mut external_options, &oxfmt_options.format_options);

sync_external_options(&oxfmt_options.format_options, &mut external_options);

(oxfmt_options, external_options)
}
Expand Down
39 changes: 31 additions & 8 deletions apps/oxfmt/src/core/oxfmtrc.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::{Path, PathBuf};

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand Down Expand Up @@ -814,7 +816,7 @@ pub struct OxfmtOptions {
///
/// This function should be called once during config caching.
/// For strategy-specific options (plugin flags), use [`finalize_external_options()`] separately.
pub fn sync_external_options(config: &mut Value, options: &FormatOptions) {
pub fn sync_external_options(options: &FormatOptions, config: &mut Value) {
let Some(obj) = config.as_object_mut() else {
return;
};
Expand Down Expand Up @@ -877,7 +879,11 @@ static OXFMT_PARSERS: phf::Set<&'static str> = phf::phf_set! {
/// - `_oxfmtPluginOptionsJson`: Bundled options for `prettier-plugin-oxfmt`
///
/// Also removes Prettier-unaware options to minimize payload size.
pub fn finalize_external_options(config: &mut Value, strategy: &FormatFileStrategy) {
pub fn finalize_external_options(
config_dir: Option<&PathBuf>,
config: &mut Value,
strategy: &FormatFileStrategy,
) {
let Some(obj) = config.as_object_mut() else {
return;
};
Expand Down Expand Up @@ -907,8 +913,25 @@ pub fn finalize_external_options(config: &mut Value, strategy: &FormatFileStrate
("preserveWhitespace", "tailwindPreserveWhitespace"),
("preserveDuplicates", "tailwindPreserveDuplicates"),
] {
if let Some(v) = tailwind.get(src) {
obj.insert(dst.to_string(), v.clone());
if let Some(value) = tailwind.get(src).cloned() {
if matches!(src, "config" | "stylesheet")
&& let Some(path_str) = value.as_str()
&& let Some(config_dir) = config_dir
{
let path = Path::new(path_str);
if path.is_relative() {
// Resolve relative paths to absolute paths, which can avoid `prettier-plugin-tailwindcss`
// from resolving the Prettier configuration file.
// <https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/125a8bc77639529a5a0c7e4e8a02174d7ed2d70b/src/config.ts#L50-L54>
let config_path = config_dir.join(path);
let canonical_path = config_path.canonicalize().unwrap_or(config_path);
let normalized_path = canonical_path.to_string_lossy().to_string();
obj.insert(dst.to_string(), Value::from(normalized_path));
continue;
}
}

obj.insert(dst.to_string(), value);
}
}
}
Expand Down Expand Up @@ -1178,7 +1201,7 @@ mod tests_sync_external_options {
let config: FormatConfig = serde_json::from_str(json_string).unwrap();
let oxfmt_options = config.into_oxfmt_options().unwrap();

sync_external_options(&mut raw_config, &oxfmt_options.format_options);
sync_external_options(&oxfmt_options.format_options, &mut raw_config);

let obj = raw_config.as_object().unwrap();
assert_eq!(obj.get("printWidth").unwrap(), 100);
Expand All @@ -1195,7 +1218,7 @@ mod tests_sync_external_options {
let config: FormatConfig = serde_json::from_str(json_string).unwrap();
let oxfmt_options = config.into_oxfmt_options().unwrap();

sync_external_options(&mut raw_config, &oxfmt_options.format_options);
sync_external_options(&oxfmt_options.format_options, &mut raw_config);

let obj = raw_config.as_object().unwrap();
// User-specified value is preserved via FormatOptions
Expand Down Expand Up @@ -1252,7 +1275,7 @@ mod tests_sync_external_options {
let oxfmtrc: Oxfmtrc = serde_json::from_str(json_string).unwrap();
let oxfmt_options = oxfmtrc.format_config.into_oxfmt_options().unwrap();

sync_external_options(&mut raw_config, &oxfmt_options.format_options);
sync_external_options(&oxfmt_options.format_options, &mut raw_config);

let obj = raw_config.as_object().unwrap();
// Overrides are preserved (for caching)
Expand Down Expand Up @@ -1280,7 +1303,7 @@ mod tests_sync_external_options {
path: PathBuf::from("test.js"),
source_type: SourceType::mjs(),
};
finalize_external_options(&mut raw_config, &strategy);
finalize_external_options(None, &mut raw_config, &strategy);

let obj = raw_config.as_object().unwrap();
// oxfmt extensions are removed by finalize_external_options
Expand Down
27 changes: 27 additions & 0 deletions apps/oxfmt/test/api/sort_tailwindcss.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, relative } from "node:path";
import { describe, expect, it } from "vitest";
import { format } from "../../dist/index.js";

Expand All @@ -17,6 +20,30 @@ describe("Tailwind CSS Sorting", () => {
expect(result.errors).toStrictEqual([]);
});

it("should resolve relative tailwindConfig paths", async () => {
const cwd = process.cwd();
const dir = await mkdtemp(join(tmpdir(), "oxfmt-tailwind-"));

try {
await writeFile(
join(dir, "tailwind.config.js"),
"module.exports = { content: [], theme: { extend: {} }, plugins: [] };",
);

const input = `const A = <div className="p-4 flex">Hello</div>;`;
const result = await format("src/test.tsx", input, {
experimentalTailwindcss: {
config: join(relative(cwd, dir), "tailwind.config.js"),
},
});

expect(result.code).toContain('className="flex p-4"');
expect(result.errors).toStrictEqual([]);
} finally {
await rm(dir, { recursive: true, force: true });
}
});

it("should NOT sort Tailwind classes when experimentalTailwindcss is disabled (default)", async () => {
const input = `const A = <div className="p-4 flex bg-red-500 text-white">Hello</div>;`;

Expand Down
1 change: 1 addition & 0 deletions apps/oxfmt/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default defineConfig({
// We are using patched version, so we must bundle it
// Also, it internally loads plugins dynamically, so they also must be bundled
"prettier-plugin-tailwindcss",
"prettier-plugin-tailwindcss/sorter",
/^prettier\/plugins\//,

// Cannot bundle: `cli-worker.js` runs in separate thread and can't resolve bundled chunks
Expand Down
64 changes: 0 additions & 64 deletions patches/prettier-plugin-tailwindcss@0.7.2.patch

This file was deleted.

15 changes: 5 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,4 @@ onlyBuiltDependencies:
- "@vscode/vsce-sign@2.0.8"
- esbuild@0.25.11

patchedDependencies:
prettier-plugin-tailwindcss@0.7.2: patches/prettier-plugin-tailwindcss@0.7.2.patch

verifyDepsBeforeRun: install
Loading