diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index 58a9a238..3954c634 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -88,6 +88,7 @@ pub struct NapiStruct { pub implement_iterator: bool, pub use_custom_finalize: bool, pub register_name: Ident, + pub use_nullable: bool, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/backend/src/codegen/struct.rs b/crates/backend/src/codegen/struct.rs index 93348776..a5bcb209 100644 --- a/crates/backend/src/codegen/struct.rs +++ b/crates/backend/src/codegen/struct.rs @@ -493,15 +493,24 @@ impl NapiStruct { let alias_ident = format_ident!("{}_", ident); field_destructions.push(quote! { #ident: #alias_ident }); if is_optional_field { - obj_field_setters.push(quote! { - if #alias_ident.is_some() { - obj.set(#field_js_name, #alias_ident)?; - } + obj_field_setters.push(match self.use_nullable { + false => quote! { + if #alias_ident.is_some() { + obj.set(#field_js_name, #alias_ident)?; + } + }, + true => quote! { + if let Some(#alias_ident) = #alias_ident { + obj.set(#field_js_name, #alias_ident)?; + } else { + obj.set(#field_js_name, napi::bindgen_prelude::Null)?; + } + }, }); } else { obj_field_setters.push(quote! { obj.set(#field_js_name, #alias_ident)?; }); } - if is_optional_field { + if is_optional_field && !self.use_nullable { obj_field_getters.push(quote! { let #alias_ident: #ty = obj.get(#field_js_name)?; }); } else { obj_field_getters.push(quote! { @@ -515,15 +524,24 @@ impl NapiStruct { syn::Member::Unnamed(i) => { field_destructions.push(quote! { arg #i }); if is_optional_field { - obj_field_setters.push(quote! { - if arg #1.is_some() { - obj.set(#field_js_name, arg #i)?; - } + obj_field_setters.push(match self.use_nullable { + false => quote! { + if arg #1.is_some() { + obj.set(#field_js_name, arg #i)?; + } + }, + true => quote! { + if let Some(arg #i) = arg #i { + obj.set(#field_js_name, arg #i)?; + } else { + obj.set(#field_js_name, napi::bindgen_prelude::Null)?; + } + }, }); } else { obj_field_setters.push(quote! { obj.set(#field_js_name, arg #1)?; }); } - if is_optional_field { + if is_optional_field && !self.use_nullable { obj_field_getters.push(quote! { let arg #i: #ty = obj.get(#field_js_name)?; }); } else { obj_field_getters.push(quote! { diff --git a/crates/backend/src/typegen/struct.rs b/crates/backend/src/typegen/struct.rs index b18b6ab2..b32f1e86 100644 --- a/crates/backend/src/typegen/struct.rs +++ b/crates/backend/src/typegen/struct.rs @@ -128,8 +128,13 @@ impl NapiStruct { let (arg, is_optional) = ty_to_ts_type(&f.ty, false, true, false); let arg = f.ts_type.as_ref().map(|ty| ty.to_string()).unwrap_or(arg); - let sep = if is_optional { "?" } else { "" }; - let arg = format!("{}{}: {}", &f.js_name, sep, arg); + let arg = match is_optional { + false => format!("{}: {}", &f.js_name, arg), + true => match self.use_nullable { + false => format!("{}?: {}", &f.js_name, arg), + true => format!("{}: {} | null", &f.js_name, arg), + }, + }; if self.kind == NapiStructKind::Constructor { ctor_args.push(arg.clone()); } diff --git a/crates/macro/src/parser/attrs.rs b/crates/macro/src/parser/attrs.rs index f1e3e11d..f150597f 100644 --- a/crates/macro/src/parser/attrs.rs +++ b/crates/macro/src/parser/attrs.rs @@ -55,15 +55,15 @@ macro_rules! attrgen { (getter, Getter(Span, Option)), (setter, Setter(Span, Option)), (readonly, Readonly(Span)), - (enumerable, Enumerable(Span, Option)), - (writable, Writable(Span, Option)), - (configurable, Configurable(Span, Option)), + (enumerable, Enumerable(Span, Option), true), + (writable, Writable(Span, Option), true), + (configurable, Configurable(Span, Option), true), (skip, Skip(Span)), (strict, Strict(Span)), (return_if_invalid, ReturnIfInvalid(Span)), (object, Object(Span)), - (object_from_js, ObjectFromJs(Span, Option)), - (object_to_js, ObjectToJs(Span, Option)), + (object_from_js, ObjectFromJs(Span, Option), true), + (object_to_js, ObjectToJs(Span, Option), true), (custom_finalize, CustomFinalize(Span)), (namespace, Namespace(Span, String, Span)), (iterator, Iterator(Span)), @@ -72,6 +72,7 @@ macro_rules! attrgen { (ts_type, TsType(Span, String, Span)), (ts_generic_types, TsGenericTypes(Span, String, Span)), (string_enum, StringEnum(Span)), + (use_nullable, UseNullable(Span, Option), false), // impl later // (inspectable, Inspectable(Span)), @@ -86,8 +87,8 @@ macro_rules! attrgen { } macro_rules! methods { - ($(($name:ident, $variant:ident($($contents:tt)*)),)*) => { - $(methods!(@method $name, $variant($($contents)*));)* + ($(($name:ident, $variant:ident($($contents:tt)*) $($extra_tokens:tt)*),)*) => { + $(methods!(@method $name, $variant($($contents)*) $($extra_tokens)*);)* #[cfg(feature = "strict")] #[allow(unused)] @@ -131,7 +132,7 @@ macro_rules! methods { } }; - (@method $name:ident, $variant:ident(Span, Option)) => { + (@method $name:ident, $variant:ident(Span, Option), $default_value:literal) => { pub fn $name(&self) -> bool { self.attrs .iter() @@ -143,7 +144,7 @@ macro_rules! methods { _ => None, }) .next() - .unwrap_or(true) + .unwrap_or($default_value) } }; @@ -265,11 +266,11 @@ impl Default for BindgenAttrs { } macro_rules! gen_bindgen_attr { - ($( ($method:ident, $($variants:tt)*) ,)*) => { + ($( ($method:ident, $variant:ident($($associated_data:tt)*) $($extra_tokens:tt)*) ,)*) => { /// The possible attributes in the `#[napi]`. #[derive(Debug)] pub enum BindgenAttr { - $($($variants)*,)* + $($variant($($associated_data)*)),* } } } @@ -395,7 +396,7 @@ impl Parse for BindgenAttr { return Ok(BindgenAttr::$variant(attr_span, val, span)) }); - (@parser $variant:ident(Span, Option)) => ({ + (@parser $variant:ident(Span, Option), $default_value:literal) => ({ if let Ok(_) = input.parse::() { let (val, _) = match input.parse::() { Ok(str) => (str.value(), str.span()), @@ -406,7 +407,7 @@ impl Parse for BindgenAttr { }; return Ok::(BindgenAttr::$variant(attr_span, Some(val))) } else { - return Ok(BindgenAttr::$variant(attr_span, Some(true))) + return Ok(BindgenAttr::$variant(attr_span, Some($default_value))) } }); diff --git a/crates/macro/src/parser/mod.rs b/crates/macro/src/parser/mod.rs index 5de28f29..ff01a070 100644 --- a/crates/macro/src/parser/mod.rs +++ b/crates/macro/src/parser/mod.rs @@ -862,6 +862,7 @@ impl ConvertToAST for syn::ItemStruct { } else { NapiStructKind::None }; + let use_nullable = opts.use_nullable(); for (i, field) in self.fields.iter_mut().enumerate() { match field.vis { @@ -943,6 +944,7 @@ impl ConvertToAST for syn::ItemStruct { implement_iterator, use_custom_finalize: opts.custom_finalize().is_some(), register_name: get_register_ident(format!("{struct_name}_struct").as_str()), + use_nullable, }), }) } diff --git a/examples/napi/__tests__/__snapshots__/typegen.spec.ts.md b/examples/napi/__tests__/__snapshots__/typegen.spec.ts.md index e1a4e7ac..93e4c626 100644 --- a/examples/napi/__tests__/__snapshots__/typegen.spec.ts.md +++ b/examples/napi/__tests__/__snapshots__/typegen.spec.ts.md @@ -143,6 +143,14 @@ Generated by [AVA](https://avajs.dev). constructor(width: number, height: number)␊ }␊ ␊ + export class DefaultUseNullableClass {␊ + requiredNumberField: number␊ + requiredStringField: string␊ + optionalNumberField?: number␊ + optionalStringField?: string␊ + constructor(requiredNumberField: number, requiredStringField: string, optionalNumberField?: number, optionalStringField?: string)␊ + }␊ + ␊ export class Dog {␊ name: string␊ constructor(name: string)␊ @@ -193,6 +201,14 @@ Generated by [AVA](https://avajs.dev). returnThis(this: this): this␊ }␊ ␊ + export class NotUseNullableClass {␊ + requiredNumberField: number␊ + requiredStringField: string␊ + optionalNumberField?: number␊ + optionalStringField?: string␊ + constructor(requiredNumberField: number, requiredStringField: string, optionalNumberField?: number, optionalStringField?: string)␊ + }␊ + ␊ export class NotWritableClass {␊ name: string␊ constructor(name: string)␊ @@ -214,6 +230,14 @@ Generated by [AVA](https://avajs.dev). constructor(orderBy: Array, select: Array, struct: string, where?: string)␊ }␊ ␊ + export class UseNullableClass {␊ + requiredNumberField: number␊ + requiredStringField: string␊ + nullableNumberField: number | null␊ + nullableStringField: string | null␊ + constructor(requiredNumberField: number, requiredStringField: string, nullableNumberField: number | null, nullableStringField: string | null)␊ + }␊ + ␊ export class Width {␊ value: number␊ constructor(value: number)␊ @@ -372,6 +396,13 @@ Generated by [AVA](https://avajs.dev). /** This is a const */␊ export const DEFAULT_COST: number␊ ␊ + export interface DefaultUseNullableStruct {␊ + requiredNumberField: number␊ + requiredStringField: string␊ + optionalNumberField?: number␊ + optionalStringField?: string␊ + }␊ + ␊ export function derefUint8Array(a: Uint8Array, b: Uint8ClampedArray): number␊ ␊ export function either3(input: string | number | boolean): number␊ @@ -465,6 +496,13 @@ Generated by [AVA](https://avajs.dev). ␊ export function mutateTypedArray(input: Float32Array): void␊ ␊ + export interface NotUseNullableStruct {␊ + requiredNumberField: number␊ + requiredStringField: string␊ + optionalNumberField?: number␊ + optionalStringField?: string␊ + }␊ + ␊ export interface Obj {␊ v: string | number␊ }␊ @@ -619,6 +657,13 @@ Generated by [AVA](https://avajs.dev). ␊ export function u8ArrayToArray(input: any): Array␊ ␊ + export interface UseNullableStruct {␊ + requiredNumberField: number␊ + requiredStringField: string␊ + nullableNumberField: number | null␊ + nullableStringField: string | null␊ + }␊ + ␊ export function validateArray(arr: Array): number␊ ␊ export function validateBigint(input: bigint): bigint␊ diff --git a/examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap b/examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap index 65024907..d8e2ce56 100644 Binary files a/examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap and b/examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap differ diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index 7874fe39..814003eb 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -133,6 +133,14 @@ export class CustomFinalize { constructor(width: number, height: number) } +export class DefaultUseNullableClass { + requiredNumberField: number + requiredStringField: string + optionalNumberField?: number + optionalStringField?: string + constructor(requiredNumberField: number, requiredStringField: string, optionalNumberField?: number, optionalStringField?: string) +} + export class Dog { name: string constructor(name: string) @@ -183,6 +191,14 @@ export class NinjaTurtle { returnThis(this: this): this } +export class NotUseNullableClass { + requiredNumberField: number + requiredStringField: string + optionalNumberField?: number + optionalStringField?: string + constructor(requiredNumberField: number, requiredStringField: string, optionalNumberField?: number, optionalStringField?: string) +} + export class NotWritableClass { name: string constructor(name: string) @@ -204,6 +220,14 @@ export class Selector { constructor(orderBy: Array, select: Array, struct: string, where?: string) } +export class UseNullableClass { + requiredNumberField: number + requiredStringField: string + nullableNumberField: number | null + nullableStringField: string | null + constructor(requiredNumberField: number, requiredStringField: string, nullableNumberField: number | null, nullableStringField: string | null) +} + export class Width { value: number constructor(value: number) @@ -362,6 +386,13 @@ export function dateToNumber(input: Date): number /** This is a const */ export const DEFAULT_COST: number +export interface DefaultUseNullableStruct { + requiredNumberField: number + requiredStringField: string + optionalNumberField?: number + optionalStringField?: string +} + export function derefUint8Array(a: Uint8Array, b: Uint8ClampedArray): number export function either3(input: string | number | boolean): number @@ -455,6 +486,13 @@ export function mutateExternal(external: ExternalObject, newVal: number) export function mutateTypedArray(input: Float32Array): void +export interface NotUseNullableStruct { + requiredNumberField: number + requiredStringField: string + optionalNumberField?: number + optionalStringField?: string +} + export interface Obj { v: string | number } @@ -609,6 +647,13 @@ export function u64ArrayToArray(input: any): Array export function u8ArrayToArray(input: any): Array +export interface UseNullableStruct { + requiredNumberField: number + requiredStringField: string + nullableNumberField: number | null + nullableStringField: string | null +} + export function validateArray(arr: Array): number export function validateBigint(input: bigint): bigint diff --git a/examples/napi/src/nullable.rs b/examples/napi/src/nullable.rs index 18a532c8..b6f1fd38 100644 --- a/examples/napi/src/nullable.rs +++ b/examples/napi/src/nullable.rs @@ -12,3 +12,51 @@ fn return_null() -> Null { #[napi] fn return_undefined() -> Undefined {} + +#[napi(object, use_nullable = true)] +struct UseNullableStruct { + pub required_number_field: u32, + pub required_string_field: String, + pub nullable_number_field: Option, + pub nullable_string_field: Option, +} + +#[napi(object, use_nullable = false)] +struct NotUseNullableStruct { + pub required_number_field: u32, + pub required_string_field: String, + pub optional_number_field: Option, + pub optional_string_field: Option, +} + +#[napi(object)] +struct DefaultUseNullableStruct { + pub required_number_field: u32, + pub required_string_field: String, + pub optional_number_field: Option, + pub optional_string_field: Option, +} + +#[napi(constructor, use_nullable = true)] +struct UseNullableClass { + pub required_number_field: u32, + pub required_string_field: String, + pub nullable_number_field: Option, + pub nullable_string_field: Option, +} + +#[napi(constructor, use_nullable = false)] +struct NotUseNullableClass { + pub required_number_field: u32, + pub required_string_field: String, + pub optional_number_field: Option, + pub optional_string_field: Option, +} + +#[napi(constructor)] +struct DefaultUseNullableClass { + pub required_number_field: u32, + pub required_string_field: String, + pub optional_number_field: Option, + pub optional_string_field: Option, +}