feat(napi/minify): expose legalComments option and result (#20370)

## Summary

- Add `legalComments` and `legalCommentsPath` fields to the NAPI `CodegenOptions` struct, accepting `"none"`, `"inline"`, `"eof"`, `"linked"`, `"external"`
- Add `legalComments: Vec<String>` to `MinifyResult`, populated from `CodegenReturn::legal_comments` when using `"linked"` or `"external"` mode
- Replace the `From<&CodegenOptions>` impl with a `to_codegen_options()` method that validates and maps string values to `oxc_codegen::LegalComment` variants

This enables downstream consumers (e.g. `oxc-webpack-plugin`) to use oxc's built-in legal comment extraction instead of regex-based post-processing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-05-26 04:31:38 +00:00
1 parent ced8c1c533
commit e857b0cfe5
6 files changed
+119 -6

No files matched your search

+36
View File
@@ -7,6 +7,18 @@ export interface CodegenOptions {
* @default true
*/
removeWhitespace?: boolean
/**
* How to handle legal comments (comments containing `@license`, `@preserve`, or starting with `//!`/`/*!`).
*
* * `"none"` - Do not preserve any legal comments.
* * `"inline"` - Preserve all legal comments inline.
* * `"eof"` - Move all legal comments to the end of the file.
* * `"external"` - Extract legal comments without linking.
* * `{ linked: "path/to/legal.txt" }` - Extract legal comments and add a link comment to the given path.
*
* @default "none" (when minifying)
*/
legalComments?: 'none' | 'inline' | 'eof' | 'external' | { linked: string }
}
export interface CompressOptions {
@@ -93,6 +105,25 @@ export interface CompressOptionsKeepNames {
class: boolean
}
export interface LegalCommentsLinked {
/**
* Extract legal comments and write them to the given path, with a link
* comment appended to the generated code.
*/
linked: string
}
export declare const enum LegalCommentsMode {
/** Do not preserve any legal comments. */
None = 'none',
/** Preserve all legal comments inline. */
Inline = 'inline',
/** Move all legal comments to the end of the file. */
Eof = 'eof',
/** Extract legal comments without linking. */
External = 'external'
}
export interface MangleOptions {
/**
* Pass `true` to mangle names declared in the top level scope.
@@ -145,6 +176,11 @@ export interface MinifyResult {
code: string
map?: SourceMap
errors: Array<OxcError>
/**
* Legal comments extracted from the source code.
* Only populated when `codegen.legalComments` is `"linked"` or `"external"`.
*/
legalComments: Array<string>
}
/** Minify synchronously. */
+2 -1
View File
@@ -587,7 +587,8 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { minify, minifySync, Severity } = nativeBinding
const { LegalCommentsMode, minify, minifySync, Severity } = nativeBinding
export { LegalCommentsMode }
export { minify }
export { minifySync }
export { Severity }
+1
View File
@@ -57,6 +57,7 @@ const {
},
})
export default __napiModule.exports
export const LegalCommentsMode = __napiModule.exports.LegalCommentsMode
export const minify = __napiModule.exports.minify
export const minifySync = __napiModule.exports.minifySync
export const Severity = __napiModule.exports.Severity
+1
View File
@@ -108,6 +108,7 @@ const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule
},
})
module.exports = __napiModule.exports
module.exports.LegalCommentsMode = __napiModule.exports.LegalCommentsMode
module.exports.minify = __napiModule.exports.minify
module.exports.minifySync = __napiModule.exports.minifySync
module.exports.Severity = __napiModule.exports.Severity
+20 -1
View File
@@ -37,6 +37,9 @@ pub struct MinifyResult {
pub code: String,
pub map: Option<SourceMap>,
pub errors: Vec<OxcError>,
/// Legal comments extracted from the source code.
/// Only populated when `codegen.legalComments` is `"linked"` or `"external"`.
pub legal_comments: Vec<String>,
}
fn minify_impl(filename: &str, source_text: &str, options: Option<MinifyOptions>) -> MinifyResult {
@@ -74,7 +77,19 @@ fn minify_impl(filename: &str, source_text: &str, options: Option<MinifyOptions>
// Need to remove all comments.
Some(Either::A(false)) => CodegenOptions { minify: false, ..CodegenOptions::minify() },
None | Some(Either::A(true)) => CodegenOptions::minify(),
Some(Either::B(o)) => CodegenOptions::from(o),
Some(Either::B(o)) => match o.to_codegen_options() {
Ok(opts) => opts,
Err(error) => {
return MinifyResult {
errors: OxcError::from_diagnostics(
filename,
source_text,
vec![OxcDiagnostic::error(error)],
),
..MinifyResult::default()
};
}
},
};
if options.sourcemap == Some(true) {
@@ -83,10 +98,14 @@ fn minify_impl(filename: &str, source_text: &str, options: Option<MinifyOptions>
let ret = Codegen::new().with_options(codegen_options).with_scoping(scoping).build(&program);
let legal_comments =
ret.legal_comments.iter().map(|c| c.span.source_text(source_text).to_string()).collect();
MinifyResult {
code: ret.code,
map: ret.map.map(oxc_sourcemap::napi::SourceMap::from),
errors: OxcError::from_diagnostics(filename, source_text, parser_ret.errors),
legal_comments,
}
}
+59 -4
View File
@@ -260,28 +260,83 @@ impl From<&MangleOptionsKeepNames> for oxc_minifier::MangleOptionsKeepNames {
}
}
#[napi(string_enum = "lowercase")]
pub enum LegalCommentsMode {
/// Do not preserve any legal comments.
None,
/// Preserve all legal comments inline.
Inline,
/// Move all legal comments to the end of the file.
Eof,
/// Extract legal comments without linking.
External,
}
#[napi(object)]
pub struct LegalCommentsLinked {
/// Extract legal comments and write them to the given path, with a link
/// comment appended to the generated code.
pub linked: String,
}
#[napi(object)]
pub struct CodegenOptions {
/// Remove whitespace.
///
/// @default true
pub remove_whitespace: Option<bool>,
/// How to handle legal comments (comments containing `@license`, `@preserve`, or starting with `//!`/`/*!`).
///
/// * `"none"` - Do not preserve any legal comments.
/// * `"inline"` - Preserve all legal comments inline.
/// * `"eof"` - Move all legal comments to the end of the file.
/// * `"external"` - Extract legal comments without linking.
/// * `{ linked: "path/to/legal.txt" }` - Extract legal comments and add a link comment to the given path.
///
/// @default "none" (when minifying)
#[napi(ts_type = "'none' | 'inline' | 'eof' | 'external' | { linked: string }")]
pub legal_comments: Option<Either<LegalCommentsMode, LegalCommentsLinked>>,
}
impl Default for CodegenOptions {
fn default() -> Self {
Self { remove_whitespace: Some(true) }
Self { remove_whitespace: Some(true), legal_comments: None }
}
}
impl From<&CodegenOptions> for oxc_codegen::CodegenOptions {
fn from(o: &CodegenOptions) -> Self {
if o.remove_whitespace.is_some_and(|b| b) {
impl CodegenOptions {
/// Convert N-API codegen options into codegen options.
///
/// # Errors
///
/// Returns an error if the `linked` variant is given an empty path.
pub fn to_codegen_options(&self) -> Result<oxc_codegen::CodegenOptions, String> {
let mut opts = if self.remove_whitespace.unwrap_or(true) {
oxc_codegen::CodegenOptions::minify()
} else {
// Need to remove all comments.
oxc_codegen::CodegenOptions { minify: false, ..oxc_codegen::CodegenOptions::minify() }
};
if let Some(legal) = &self.legal_comments {
opts.comments.legal = match legal {
Either::A(mode) => match mode {
LegalCommentsMode::None => oxc_codegen::LegalComment::None,
LegalCommentsMode::Inline => oxc_codegen::LegalComment::Inline,
LegalCommentsMode::Eof => oxc_codegen::LegalComment::Eof,
LegalCommentsMode::External => oxc_codegen::LegalComment::External,
},
Either::B(linked) => {
if linked.linked.is_empty() {
return Err("legalComments.linked must be a non-empty path.".into());
}
oxc_codegen::LegalComment::Linked(linked.linked.clone())
}
};
}
Ok(opts)
}
}