use super::*; #[test] fn erases_types_without_removing_value_imports_or_leaking_private_modules() { let module = "avril:quoted\"name"; let compiled = compile( Path::new("src/api.ts"), r#" import type { Internal } from "ext:private/types.js"; import { unused } from "ext:private/value.js"; const internal: Internal | null = null; export function identity(value: unknown): unknown { return value; } "#, Some(module), ) .unwrap(); let allocator = Allocator::default(); let javascript = Parser::new(&allocator, &compiled.javascript, SourceType::mjs()).parse(); assert!( javascript.diagnostics.is_empty(), "{:?}", javascript.diagnostics ); let imports: Vec<_> = javascript .program .body .iter() .filter_map(|statement| match statement { Statement::ImportDeclaration(import) => Some(import.source.value.as_str()), _ => None, }) .collect(); assert_eq!(imports, ["ext:private/value.js"]); let declaration = compiled.declaration.unwrap(); let declarations = Parser::new(&allocator, &declaration, SourceType::d_ts()).parse(); assert!( declarations.diagnostics.is_empty(), "{:?}", declarations.diagnostics ); assert_eq!(declarations.program.body.len(), 1); let Statement::TSExternalModuleDeclaration(public) = &declarations.program.body[0] else { panic!("Expected an ambient public module"); }; assert!(public.declare); assert_eq!(public.id.value.as_str(), module); let body = &public.body.as_ref().unwrap().body; assert_eq!(body.len(), 1); let Statement::ExportDeclaration(export) = &body[0] else { panic!("Expected the public function"); }; let Declaration::FunctionDeclaration(function) = &export.declaration else { panic!("Expected a function declaration"); }; assert_eq!(function.id.as_ref().unwrap().name.as_str(), "identity"); assert!(function.body.is_none()); assert!(!function.declare); } #[test] fn internal_modules_do_not_require_isolated_declarations_or_function_only_exports() { let compiled = compile( Path::new("src/internal.ts"), "import 'ext:private/setup.js'; export class State { #value: number = 42; value() { return this.#value; } }", None, ) .unwrap(); assert!(compiled.declaration.is_none()); let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &compiled.javascript, SourceType::mjs()).parse(); assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics); let Statement::ImportDeclaration(import) = &parsed.program.body[0] else { panic!("Expected the side-effect import"); }; assert_eq!(import.source.value.as_str(), "ext:private/setup.js"); let Statement::ExportDeclaration(export) = &parsed.program.body[1] else { panic!("Expected the native class export"); }; assert!(matches!( export.declaration, Declaration::ClassDeclaration(_) )); } #[test] fn isolated_declaration_diagnostics_are_propagated() { assert!( compile( Path::new("src/api.ts"), "export function value() { return 42; }", Some("avril:fixture"), ) .is_ok() ); let error = compile( Path::new("src/api.ts"), "export function value() { return Math.random(); }", Some("avril:fixture"), ) .err() .expect("Nontrivial return expressions require an annotation"); assert!(error.to_string().contains("return type"), "{error}"); } #[test] fn unsupported_public_declarations_fail_instead_of_being_dropped() { for source in [ "export enum Value { One }", "export namespace Value { export type Inner = number; }", "export class Value {}", "export { value } from 'ext:private/main.js';", "export default function value(): number { return 42; }", "import type { Value } from 'ext:private/types.js'; export function value(v: Value): Value { return v; }", ] { let error = compile(Path::new("src/api.ts"), source, Some("avril:fixture")) .err() .expect("Unsupported public declarations must fail"); assert!( error.to_string().contains("Unsupported public declaration"), "{source}: {error}" ); } } #[test] fn packages_callable_intersections_and_their_exported_types() { let compiled = compile( Path::new("src/public.ts"), r#" import { registration } from "ext:private/main.js"; export interface Options { name: string; } export type Register = ((name: string) => void) & ((options: Options) => void); export const register: Register & { only: Register } = registration; "#, Some("avril:fixture"), ) .unwrap(); let declaration = compiled.declaration.unwrap(); assert!(!declaration.contains("ext:private")); let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &declaration, SourceType::d_ts()).parse(); assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics); let Statement::TSExternalModuleDeclaration(public) = &parsed.program.body[0] else { panic!("Expected an ambient public module"); }; let exports: Vec<_> = public .body .as_ref() .unwrap() .body .iter() .map(|statement| { let Statement::ExportDeclaration(export) = statement else { panic!("Expected a public export"); }; match &export.declaration { Declaration::TSInterfaceDeclaration(interface) => interface.id.name.as_str(), Declaration::TSTypeAliasDeclaration(alias) => alias.id.name.as_str(), Declaration::VariableDeclaration(variable) => { assert!(!variable.declare); assert_eq!(variable.declarations.len(), 1); assert!(variable.declarations[0].init.is_none()); let oxc::ast::ast::BindingPattern::BindingIdentifier(id) = &variable.declarations[0].id else { panic!("Expected the exported binding"); }; id.name.as_str() } _ => panic!("Unexpected public declaration"), } }) .collect(); assert_eq!(exports, ["Options", "Register", "register"]); } #[test] fn private_types_are_kept_without_being_exported() { let compiled = compile( Path::new("src/public.ts"), r#" type Hidden = { value: number }; interface AlsoHidden { text: string; } import { convert as implementation } from "./internal.js"; export const convert: (input: Hidden) => AlsoHidden = implementation; "#, Some("avril:fixture"), ) .unwrap(); let declaration = compiled.declaration.unwrap(); assert!(!declaration.contains("./internal.js")); let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &declaration, SourceType::d_ts()).parse(); assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics); let Statement::TSExternalModuleDeclaration(public) = &parsed.program.body[0] else { panic!("Expected an ambient public module"); }; let mut exported = 0; let mut markers = 0; let mut private = Vec::new(); for statement in public.body.as_ref().unwrap().body.iter() { match statement { Statement::ExportDeclaration(_) => exported += 1, Statement::ExportNamedDeclaration(export) => { assert!(export.specifiers.is_empty()); markers += 1; } Statement::TSInterfaceDeclaration(interface) => { private.push(interface.id.name.as_str()); } Statement::TSTypeAliasDeclaration(alias) => private.push(alias.id.name.as_str()), statement => panic!("Unexpected public statement: {statement:?}"), } } assert_eq!(exported, 1); // The marker is required even though the body has exports: without it // the private declarations would be importable. assert_eq!(markers, 1); assert_eq!(private, ["Hidden", "AlsoHidden"]); } #[test] fn public_type_imports_and_reexports_are_preserved_verbatim() { let compiled = compile( Path::new("src/public/sync.ts"), r#" import type { CopyOptions } from "avril:fs"; export type { CopyOptions } from "avril:fs"; import { copy as implementation } from "../fs_sync.js"; export const copy: (source: string, options?: CopyOptions) => void = implementation; "#, Some("avril:fixture/sync"), ) .unwrap(); let declaration = compiled.declaration.unwrap(); assert!(declaration.contains("from \"avril:fs\"")); assert!(!declaration.contains("../fs_sync.js")); let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &declaration, SourceType::d_ts()).parse(); assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics); let Statement::TSExternalModuleDeclaration(public) = &parsed.program.body[0] else { panic!("Expected an ambient public module"); }; let mut imports = 0; let mut reexports = 0; let mut exports = 0; for statement in public.body.as_ref().unwrap().body.iter() { match statement { Statement::ImportDeclaration(import) => { assert_eq!(import.source.value.as_str(), "avril:fs"); imports += 1; } Statement::ExportFromDeclaration(export) => { assert_eq!(export.source.value.as_str(), "avril:fs"); reexports += 1; } Statement::ExportDeclaration(_) => exports += 1, statement => panic!("Unexpected public statement: {statement:?}"), } } assert_eq!((imports, reexports, exports), (1, 1, 1)); } #[test] fn private_module_references_fail_instead_of_being_bundled() { for source in [ "import type { Value } from './types.js'; export function value(v: Value): Value { return v; }", "export type { Value } from './types.js';", "import { Value } from 'avril:fixture'; import { value as implementation } from './internal.js'; export const value: (v: Value) => Value = implementation;", "export { Value } from 'avril:fixture';", ] { let error = compile(Path::new("src/api.ts"), source, Some("avril:fixture")) .err() .expect("Private module references must fail"); assert!( error.to_string().contains("Unsupported public declaration"), "{source}: {error}" ); } } #[test] fn outputs_mirror_source_paths_under_src() { use std::path::PathBuf; let out = Path::new("target"); assert_eq!( output_path(out, Path::new("src/main.ts")).unwrap(), PathBuf::from("target/main.ts") ); assert_eq!( output_path(out, Path::new("src/public.ts")).unwrap(), PathBuf::from("target/public.ts") ); assert_eq!( output_path(out, Path::new("src/public/posix.ts")).unwrap(), PathBuf::from("target/public/posix.ts") ); output_path(out, Path::new("other.ts")).expect_err("Sources outside src/ must fail"); } #[test] fn invalid_syntax_is_rejected_with_source_diagnostics() { let error = compile(Path::new("src/broken.ts"), "export function broken(", None) .err() .expect("Invalid syntax must fail"); assert!( error.to_string().contains("export function broken("), "{error}" ); } #[test] fn implementation_only_declarations_are_pruned_from_the_public_declaration() { let compiled = compile( Path::new("src/public.ts"), r#" import { factory } from "ext:core/ops"; import type { Unused } from "./internal.js"; interface State { entries: Map; } const state = new Map(); class Impl { run(): void { factory(); } } export interface Surface { readonly value: number; } export function read(): Surface { return null as unknown as Surface; } "#, Some("avril:fixture"), ) .unwrap(); let declaration = compiled.declaration.unwrap(); assert!(!declaration.contains("ext:core/ops"), "{declaration}"); assert!(!declaration.contains("./internal.js"), "{declaration}"); assert!(!declaration.contains("State"), "{declaration}"); assert!(!declaration.contains("Impl"), "{declaration}"); assert!( declaration.contains("export interface Surface"), "{declaration}" ); } #[test] fn side_effect_imports_do_not_reach_the_public_declaration() { let compiled = compile( Path::new("src/public.ts"), "import 'ext:private/setup.js'; export function value(): number { return 42; }", Some("avril:fixture"), ) .unwrap(); let declaration = compiled.declaration.unwrap(); assert!(!declaration.contains("ext:private"), "{declaration}"); assert!( declaration.contains("export function value"), "{declaration}" ); }