Skip to main content

alef_backend_napi/gen_bindings/
mod.rs

1//! NAPI-RS (Node.js) backend: orchestration and `Backend` trait implementation.
2
3pub mod enums;
4pub mod errors;
5pub mod functions;
6pub mod methods;
7pub mod types;
8
9use crate::type_map::NapiMapper;
10use ahash::AHashSet;
11use alef_codegen::builder::RustFileBuilder;
12use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
13use alef_codegen::naming::to_node_name;
14use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
15use alef_core::config::{Language, ResolvedCrateConfig, resolve_output_dir};
16use alef_core::ir::{ApiSurface, TypeRef};
17use std::path::PathBuf;
18
19pub struct NapiBackend;
20
21impl NapiBackend {
22    fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
23        RustBindingConfig {
24            struct_attrs: &["napi"],
25            field_attrs: &[],
26            struct_derives: &["Clone"],
27            method_block_attr: Some("napi"),
28            constructor_attr: "#[napi(constructor)]",
29            static_attr: None,
30            function_attr: "#[napi]",
31            enum_attrs: &["napi(string_enum)"],
32            enum_derives: &["Clone"],
33            needs_signature: false,
34            signature_prefix: "",
35            signature_suffix: "",
36            core_import,
37            async_pattern: AsyncPattern::NapiNativeAsync,
38            has_serde,
39            // NAPI napi(object) structs don't derive Serialize — disable serde bridge
40            type_name_prefix: prefix,
41            option_duration_on_defaults: true,
42            opaque_type_names: &[],
43            skip_impl_constructor: false,
44            cast_uints_to_i32: false,
45            cast_large_ints_to_f64: false,
46            named_non_opaque_params_by_ref: false,
47            lossy_skip_types: &[],
48            serializable_opaque_type_names: &[],
49        }
50    }
51}
52
53impl Backend for NapiBackend {
54    fn name(&self) -> &str {
55        "napi"
56    }
57
58    fn language(&self) -> Language {
59        Language::Node
60    }
61
62    fn capabilities(&self) -> Capabilities {
63        Capabilities {
64            supports_async: true,
65            supports_classes: true,
66            supports_enums: true,
67            supports_option: true,
68            supports_result: true,
69            ..Capabilities::default()
70        }
71    }
72
73    fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
74        let prefix = config.node_type_prefix();
75        let trait_type_names: AHashSet<String> = api
76            .types
77            .iter()
78            .filter(|t| t.is_trait)
79            .map(|t| t.name.clone())
80            .collect();
81        let mapper = NapiMapper::with_traits(prefix.clone(), trait_type_names);
82        let core_import = config.core_import_name();
83
84        // Detect serde availability from the output crate's Cargo.toml
85        let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
86        let has_serde = alef_core::config::detect_serde_available(&output_dir);
87        let cfg = Self::binding_config(&core_import, &prefix, has_serde);
88
89        let mut builder = RustFileBuilder::new().with_generated_header();
90        builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
91        builder.add_inner_attribute("allow(unsafe_code)");
92        builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy)");
93        // Cast lints fire heavily on the JS u32/i64/Number bridge — these are
94        // intentional, deliberate at the FFI boundary. Pedantic/nursery noise
95        // (must_use_candidate, use_self, missing_const_for_fn, etc.) is
96        // suppressed for the same reasons documented in the pyo3 backend.
97        builder.add_inner_attribute(
98            "allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::default_trait_access, clippy::useless_conversion, clippy::unsafe_derive_deserialize, clippy::must_use_candidate, clippy::return_self_not_must_use, clippy::use_self, clippy::missing_const_for_fn, clippy::missing_errors_doc, clippy::needless_pass_by_value, clippy::doc_markdown, clippy::derive_partial_eq_without_eq, clippy::uninlined_format_args, clippy::redundant_clone, clippy::implicit_clone, clippy::redundant_closure_for_method_calls, clippy::wildcard_imports, clippy::option_if_let_else, clippy::too_many_lines)",
99        );
100        builder.add_import("napi::*");
101        builder.add_import("napi_derive::napi");
102
103        // Always import serde_json for type conversion in From/Into impls,
104        // even if the binding crate doesn't explicitly list it as a dependency.
105        // serde_json is needed for conversions of types with serde-serializable fields.
106        builder.add_import("serde_json");
107
108        // Import traits needed for trait method dispatch
109        for trait_path in generators::collect_trait_imports(api) {
110            builder.add_import(&trait_path);
111        }
112
113        // Only import HashMap when Map-typed fields or returns are present
114        let has_maps = api
115            .types
116            .iter()
117            .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
118            || api
119                .functions
120                .iter()
121                .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
122        if has_maps {
123            builder.add_import("std::collections::HashMap");
124        }
125
126        // Note: custom_modules for Node are TypeScript-only re-exports
127        // (used in generate_public_api), not Rust module declarations.
128
129        // Check if any function or method is async
130        let has_async =
131            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
132
133        if has_async {
134            builder.add_item(&functions::gen_tokio_runtime());
135        }
136
137        // Check if we have opaque types and trait types (visitors)
138        // Exclude trait types from opaque_types since they use JsVisitorRef instead of Object<'static>
139        let opaque_types: AHashSet<String> = api
140            .types
141            .iter()
142            .filter(|t| t.is_opaque && !t.is_trait)
143            .map(|t| t.name.clone())
144            .collect();
145        let has_traits = api.types.iter().any(|t| t.is_trait);
146        if !opaque_types.is_empty() || has_traits {
147            builder.add_import("std::sync::Arc");
148        }
149
150        let exclude_types: ahash::AHashSet<String> = config
151            .node
152            .as_ref()
153            .map(|c| c.exclude_types.iter().cloned().collect())
154            .unwrap_or_default();
155
156        // Build adapter body map before type iteration so bodies are available for method generation.
157        let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
158
159        // Map "OwnerType.method" -> streaming item type. The napi backend needs to
160        // override the IR-declared `String` return type with `Vec<{prefix}{item}>`
161        // for streaming adapters, since the generated body returns chunks directly
162        // as a JS array instead of a serialized JSON string.
163        let streaming_item_types: ahash::AHashMap<String, String> = config
164            .adapters
165            .iter()
166            .filter(|a| matches!(a.pattern, alef_core::config::AdapterPattern::Streaming))
167            .filter_map(|a| {
168                let owner = a.owner_type.as_deref()?;
169                let item = a.item_type.as_deref()?;
170                Some((format!("{owner}.{}", a.name), item.to_string()))
171            })
172            .collect();
173
174        // JsVisitorRef: a thin wrapper around napi::Object that implements Clone.
175        // This newtype makes Object<'static> work with napi(object) field derivations,
176        // which require Clone. Uses std::sync::Arc to make the handle cheaply cloneable.
177        if has_traits {
178            let js_visitor_ref_def = r#"
179/// Wrapper for trait visitor types (napi::Object<'static>) that implements Clone.
180///
181/// Object is not Clone. This wrapper uses Arc<Object<'static>> internally for cheap cloning.
182/// The .inner field is public for compatibility with generated code that needs to access
183/// the underlying Object for trait dispatch.
184pub struct JsVisitorRef {
185    pub inner: std::sync::Arc<napi::bindgen_prelude::Object<'static>>,
186}
187
188impl Clone for JsVisitorRef {
189    fn clone(&self) -> Self {
190        JsVisitorRef {
191            inner: std::sync::Arc::clone(&self.inner),
192        }
193    }
194}
195
196#[allow(clippy::arc_with_non_send_sync)]
197impl From<napi::bindgen_prelude::Object<'static>> for JsVisitorRef {
198    fn from(visitor: napi::bindgen_prelude::Object<'static>) -> Self {
199        JsVisitorRef {
200            inner: std::sync::Arc::new(visitor),
201        }
202    }
203}
204
205impl From<JsVisitorRef> for napi::bindgen_prelude::Object<'static> {
206    fn from(visitor_ref: JsVisitorRef) -> Self {
207        // Object<'static> is Copy (it just holds an env+handle pair), so deref directly.
208        *visitor_ref.inner
209    }
210}
211"#;
212            builder.add_item(js_visitor_ref_def);
213        }
214
215        // Emit adapter-generated standalone items (streaming iterators, callback bridges).
216        for adapter in &config.adapters {
217            match adapter.pattern {
218                alef_core::config::AdapterPattern::Streaming => {
219                    let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
220                    if let Some(struct_code) = adapter_bodies.get(&key) {
221                        builder.add_item(struct_code);
222                    }
223                }
224                alef_core::config::AdapterPattern::CallbackBridge => {
225                    let struct_key = format!("{}.__bridge_struct__", adapter.name);
226                    let impl_key = format!("{}.__bridge_impl__", adapter.name);
227                    if let Some(struct_code) = adapter_bodies.get(&struct_key) {
228                        builder.add_item(struct_code);
229                    }
230                    if let Some(impl_code) = adapter_bodies.get(&impl_key) {
231                        builder.add_item(impl_code);
232                    }
233                }
234                _ => {}
235            }
236        }
237
238        // NAPI has some unique patterns: Js-prefixed names, Option-wrapped fields,
239        // and custom constructor. Use shared generators for enums and functions,
240        // but keep struct/method generation custom.
241        for typ in api
242            .types
243            .iter()
244            .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
245        {
246            if typ.is_opaque {
247                builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(
248                    typ, &cfg, &prefix,
249                ));
250                builder.add_item(&types::gen_opaque_struct_methods(
251                    typ,
252                    &mapper,
253                    &cfg,
254                    &opaque_types,
255                    &prefix,
256                    &adapter_bodies,
257                    &streaming_item_types,
258                ));
259            } else {
260                // Non-opaque structs use #[napi(object)] — plain JS objects without methods.
261                // napi(object) structs cannot have #[napi] impl blocks.
262                // gen_struct adds Default to derives when typ.has_default is true.
263                builder.add_item(&types::gen_struct(typ, &mapper, &prefix, has_serde, &opaque_types));
264            }
265        }
266
267        // Collect struct names so tagged enum codegen knows which Named types have binding structs
268        let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
269
270        // Collect Named types that have a Default impl. These are eligible to be
271        // promoted to Option<T> in binding signatures so JS callers may pass
272        // `undefined` to fall back to a default-constructed instance.
273        let default_types: ahash::AHashSet<String> = api
274            .types
275            .iter()
276            .filter(|t| t.has_default)
277            .map(|t| t.name.clone())
278            .collect();
279
280        for enum_def in &api.enums {
281            builder.add_item(&enums::gen_enum(enum_def, &prefix, has_serde));
282        }
283
284        let exclude_functions: ahash::AHashSet<String> = config
285            .node
286            .as_ref()
287            .map(|c| c.exclude_functions.iter().cloned().collect())
288            .unwrap_or_default();
289
290        for func in &api.functions {
291            if exclude_functions.contains(&func.name) {
292                continue;
293            }
294            let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
295            let options_field_bridge = crate::trait_bridge::find_options_field_binding(func, &config.trait_bridges);
296            // Skip sanitized functions when there's no trait bridge that can replace the
297            // sanitized parameter — such functions cannot be auto-delegated. Functions
298            // whose only "sanitized" param is a configured trait_bridge param (e.g.
299            // Option<VisitorHandle> in html-to-markdown) are emitted via gen_bridge_function.
300            if func.sanitized && bridge_param.is_none() && options_field_bridge.is_none() {
301                continue;
302            }
303            if let Some((param_idx, bridge_cfg)) = bridge_param {
304                builder.add_item(&crate::trait_bridge::gen_bridge_function(
305                    func,
306                    param_idx,
307                    bridge_cfg,
308                    &mapper,
309                    &cfg,
310                    &Default::default(),
311                    &opaque_types,
312                    &core_import,
313                ));
314            } else if let Some((param_idx, bridge_cfg)) = options_field_bridge {
315                builder.add_item(&crate::trait_bridge::gen_options_field_bridge_function(
316                    func,
317                    param_idx,
318                    bridge_cfg,
319                    &mapper,
320                    &cfg,
321                    &opaque_types,
322                    &core_import,
323                ));
324            } else {
325                builder.add_item(&functions::gen_function(
326                    func,
327                    &mapper,
328                    &cfg,
329                    &opaque_types,
330                    &default_types,
331                    &prefix,
332                ));
333            }
334        }
335
336        // Trait bridge wrappers — generate NAPI bridge structs that delegate to JS objects
337        for bridge_cfg in &config.trait_bridges {
338            if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
339                let bridge = crate::trait_bridge::gen_trait_bridge(
340                    trait_type,
341                    bridge_cfg,
342                    &core_import,
343                    &config.error_type_name(),
344                    &config.error_constructor_expr(),
345                    api,
346                );
347                for imp in &bridge.imports {
348                    builder.add_import(imp);
349                }
350                builder.add_item(&bridge.code);
351            }
352        }
353
354        let binding_to_core = alef_codegen::conversions::convertible_types(api);
355        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
356        let input_types = alef_codegen::conversions::input_type_names(api);
357        let napi_conv_config = alef_codegen::conversions::ConversionConfig {
358            type_name_prefix: &prefix,
359            cast_large_ints_to_i64: true,
360            cast_f32_to_f64: true,
361            // optionalize_defaults: For types with has_default, conversion generators
362            // make all fields Option<T> and apply defaults via FromNapiValue,
363            // enabling JS users to pass partial objects and omit fields they want defaults for.
364            optionalize_defaults: true,
365            option_duration_on_defaults: true,
366            include_cfg_metadata: true,
367            // Pass opaque_types so the conversion generator can emit `Default::default()`
368            // for opaque-type fields (e.g. visitor: Object<'static>) instead of trying to
369            // convert them via Into — these fields are handled separately via bridge code.
370            opaque_types: Some(&opaque_types),
371            // Json fields are stored as serde_json::Value in the binding so JS
372            // callers can pass objects/arrays/scalars directly.
373            json_as_value: true,
374            ..Default::default()
375        };
376        // From/Into conversions using shared parameterized generators
377        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
378            if input_types.contains(&typ.name)
379                && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
380            {
381                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
382                    typ,
383                    &core_import,
384                    &napi_conv_config,
385                ));
386            }
387            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
388                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
389                    typ,
390                    &core_import,
391                    &opaque_types,
392                    &napi_conv_config,
393                ));
394            }
395        }
396        for e in &api.enums {
397            let has_data_variants = e.variants.iter().any(|v| !v.fields.is_empty());
398            let is_tagged_data_enum = e.serde_tag.is_some() && has_data_variants;
399            let is_untagged_data_enum = e.serde_untagged && has_data_variants;
400            if is_tagged_data_enum {
401                // Tagged data enums use flattened struct — generate custom conversions
402                builder.add_item(&methods::gen_tagged_enum_binding_to_core(
403                    e,
404                    &core_import,
405                    &prefix,
406                    &struct_names,
407                ));
408                builder.add_item(&methods::gen_tagged_enum_core_to_binding(
409                    e,
410                    &core_import,
411                    &prefix,
412                    &struct_names,
413                ));
414            } else if is_untagged_data_enum {
415                // Untagged data enums are wrapped around serde_json::Value — bridge via serde.
416                let binding_name = format!("{prefix}{}", e.name);
417                let core_path = alef_codegen::conversions::core_enum_path_remapped(
418                    e,
419                    &core_import,
420                    napi_conv_config.source_crate_remaps,
421                );
422                builder.add_item(&format!(
423                    "impl From<{binding_name}> for {core_path} {{\n    \
424                         fn from(val: {binding_name}) -> Self {{\n        \
425                             serde_json::from_value(val.0).unwrap_or_default()\n    \
426                         }}\n\
427                     }}\n"
428                ));
429                builder.add_item(&format!(
430                    "impl From<{core_path}> for {binding_name} {{\n    \
431                         fn from(val: {core_path}) -> Self {{\n        \
432                             Self(serde_json::to_value(val).unwrap_or_default())\n    \
433                         }}\n\
434                     }}\n"
435                ));
436            } else {
437                if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
438                    builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
439                        e,
440                        &core_import,
441                        &napi_conv_config,
442                    ));
443                }
444                if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
445                    builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
446                        e,
447                        &core_import,
448                        &napi_conv_config,
449                    ));
450                }
451            }
452        }
453
454        // Error types (variant name constants + converter functions)
455        for error in &api.errors {
456            builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
457            builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
458        }
459
460        let content = builder.build();
461
462        let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
463
464        Ok(vec![GeneratedFile {
465            path: PathBuf::from(&output_dir).join("lib.rs"),
466            content,
467            generated_header: false,
468        }])
469    }
470
471    fn generate_public_api(
472        &self,
473        api: &ApiSurface,
474        config: &ResolvedCrateConfig,
475    ) -> anyhow::Result<Vec<GeneratedFile>> {
476        let prefix = config.node_type_prefix();
477
478        // Separate exports into functions (plain export) and types (export type)
479        let mut type_exports = vec![];
480        let mut function_exports = vec![];
481
482        // Collect all types (exported with prefix from native module) - export type.
483        // Skip trait definitions (e.g. HtmlVisitor): the NAPI binding exposes opaque
484        // *Handle classes for trait bridges, not the trait types themselves, so
485        // re-exporting `JsHtmlVisitor` produces a TS2305 'has no exported member'
486        // error against the generated index.d.ts.
487        for typ in api.types.iter() {
488            if typ.is_trait {
489                continue;
490            }
491            type_exports.push(format!("{prefix}{}", typ.name));
492        }
493
494        // Collect all enums as type exports.
495        // With verbatimModuleSyntax enabled, re-exporting const enums as values causes
496        // TS2748/TS1205; using `export type` avoids both errors.
497        for enum_def in &api.enums {
498            type_exports.push(format!("{prefix}{}", enum_def.name));
499        }
500
501        // NAPI errors are thrown as native JS Error objects, not exported as TS types.
502        // Skip error types in the public API re-exports.
503
504        // Collect all functions (exported from native module) - plain export
505        for func in &api.functions {
506            // Convert snake_case to camelCase for JavaScript naming
507            let js_name = to_node_name(&func.name);
508            function_exports.push(js_name);
509        }
510
511        // Sort for consistent output
512        type_exports.sort();
513        function_exports.sort();
514
515        // Generate the index.ts re-export file using a single export block
516        // with inline `type` annotations for verbatimModuleSyntax compatibility.
517        let mut lines = vec![
518            "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
519            "".to_string(),
520        ];
521
522        // Separate value and type exports for verbatimModuleSyntax compatibility.
523        // Value exports (functions) in one block, type exports (structs + enums) in another.
524        if !function_exports.is_empty() {
525            lines.push("export {".to_string());
526            for name in &function_exports {
527                lines.push(format!("  {name},"));
528            }
529            lines.push(format!("}} from '{}';", config.node_package_name()));
530            lines.push("".to_string());
531        }
532        if !type_exports.is_empty() {
533            lines.push("export type {".to_string());
534            for name in &type_exports {
535                lines.push(format!("  {name},"));
536            }
537            lines.push(format!("}} from '{}';", config.node_package_name()));
538        }
539
540        // Append re-exports for custom modules (from [custom_modules] node = [...])
541        let custom_mods = config.custom_modules.for_language(Language::Node);
542        for module_name in custom_mods {
543            lines.push(format!("export * from './{module_name}';"));
544        }
545
546        let content = lines.join("\n");
547
548        // Output path: packages/typescript/src/index.ts
549        let output_path = PathBuf::from("packages/typescript/src/index.ts");
550
551        Ok(vec![GeneratedFile {
552            path: output_path,
553            content,
554            generated_header: false,
555        }])
556    }
557
558    fn generate_type_stubs(
559        &self,
560        api: &ApiSurface,
561        config: &ResolvedCrateConfig,
562    ) -> anyhow::Result<Vec<GeneratedFile>> {
563        let prefix = config.node_type_prefix();
564        let exclude_functions: ahash::AHashSet<String> = config
565            .node
566            .as_ref()
567            .map(|c| c.exclude_functions.iter().cloned().collect())
568            .unwrap_or_default();
569        let content = errors::gen_dts(api, &prefix, &exclude_functions, &config.trait_bridges);
570
571        // `output_for("node")` points to the `src/` directory (e.g., `crates/{name}-node/src/`).
572        // `index.d.ts` belongs at the crate root, one level up from `src/`.
573        // When the configured path ends in `src/` or `src`, strip that suffix to get the crate root.
574        // Falls back to `crates/{name}-node/` if no node output is configured.
575        let src_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
576        let crate_root = {
577            let p = PathBuf::from(&src_dir);
578            match p.file_name().and_then(|n| n.to_str()) {
579                Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
580                _ => p,
581            }
582        };
583
584        Ok(vec![GeneratedFile {
585            path: crate_root.join("index.d.ts"),
586            content,
587            generated_header: false,
588        }])
589    }
590
591    fn build_config(&self) -> Option<BuildConfig> {
592        Some(BuildConfig {
593            tool: "napi",
594            crate_suffix: "-node",
595            build_dep: BuildDependency::None,
596            post_build: vec![PostBuildStep::PatchFile {
597                path: "index.d.ts",
598                find: "export declare const enum",
599                replace: "export declare enum",
600            }],
601        })
602    }
603}
604
605/// Generate a NAPI struct with Js-prefixed name and fields wrapped in Option only if optional.
606#[cfg(test)]
607mod tests {
608    use super::NapiBackend;
609    use alef_core::backend::Backend;
610    use alef_core::config::Language;
611
612    /// NapiBackend::name returns "napi".
613    #[test]
614    fn napi_backend_name_is_napi() {
615        let b = NapiBackend;
616        assert_eq!(b.name(), "napi");
617    }
618
619    /// NapiBackend::language returns Language::Node.
620    #[test]
621    fn napi_backend_language_is_node() {
622        let b = NapiBackend;
623        assert_eq!(b.language(), Language::Node);
624    }
625}