@rbac/rbac
Version:
Blazing Fast, Zero dependency, Hierarchical Role-Based Access Control for Node.js
213 lines (147 loc) • 10.2 kB
Markdown
# Design Document: ES Module Import Fix
## Overview
This design addresses the ES module import bug in the @rbac/rbac package by implementing proper dual-format package configuration. The solution uses the "exports" field in package.json to provide correct entry points for both CommonJS and ES module consumers, ensuring that default imports work correctly without requiring `.default` accessor while maintaining full backward compatibility.
The core issue stems from the mismatch between how TypeScript compiles default exports to CommonJS and how ES module consumers expect to import them. When TypeScript compiles `export default RBAC` with `module: "commonjs"`, it creates a CommonJS module with `exports.default = RBAC`. ES module consumers using `import RBAC from '@rbac/rbac'` expect the default export to be directly accessible, but Node.js's interop layer doesn't automatically unwrap this for packages without proper configuration.
## Architecture
The solution follows a **dual-format package** architecture pattern:
```
@rbac/rbac
├── package.json (with "exports" field)
├── src/
│ └── index.ts (TypeScript source)
└── lib/
├── index.js (CommonJS output)
└── index.d.ts (Type definitions)
```
**Key architectural decisions:**
1. **Single compilation target**: Continue compiling to CommonJS only (no ESM build)
2. **Exports field configuration**: Use package.json "exports" to handle module resolution
3. **Conditional exports**: Leverage Node.js conditional exports for proper interop
4. **Backward compatibility**: Maintain "main" field for older tooling
This approach avoids the complexity of dual builds while providing correct behavior for both module systems through Node.js's built-in interop mechanisms.
## Components and Interfaces
### 1. Package.json Exports Configuration
The "exports" field will define how different module systems resolve the package:
```json
{
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"exports": {
".": {
"types": "./lib/index.d.ts",
"import": "./lib/index.js",
"require": "./lib/index.js",
"default": "./lib/index.js"
}
}
}
```
**Component responsibilities:**
- `main`: Fallback for older tools that don't support "exports"
- `types`: Root-level type definitions for backward compatibility
- `exports["."].types`: Type definitions for TypeScript 4.7+
- `exports["."].import`: Entry point for ES module consumers
- `exports["."].require`: Entry point for CommonJS consumers
- `exports["."].default`: Fallback for any other resolution
### 2. Source Code Export Structure
The src/index.ts file maintains its current structure:
```typescript
import RBAC from './rbac';
import type { RBACConfig, RBACInstance } from './types';
import type { RoleAdapter } from './adapters/adapter';
export async function createTenantRBAC<P>(
adapter: RoleAdapter<P>,
tenantId: string,
config: RBACConfig = {}
): Promise<RBACInstance<P>> {
const roles = await adapter.getRoles(tenantId);
return RBAC<P>(config)(roles);
}
export * from './middlewares';
export * from './roles.schema';
export default RBAC;
```
**No changes required** - the existing export structure is correct. The issue is purely in the package configuration.
### 3. TypeScript Compilation Configuration
The tsconfig.json maintains CommonJS compilation:
```json
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"declaration": true,
"esModuleInterop": true,
// ... other options
}
}
```
**Key settings:**
- `module: "commonjs"`: Compile to CommonJS format
- `esModuleInterop: true`: Enable proper interop helpers
- `declaration: true`: Generate .d.ts files
The `esModuleInterop` flag is crucial - it ensures TypeScript generates proper interop code that works with Node.js's ES module loader.
## Data Models
No new data models are required. The existing type definitions remain unchanged:
- `RBACConfig`: Configuration options for RBAC
- `RBACInstance<P>`: The RBAC instance type
- `RoleAdapter<P>`: Interface for role adapters
- All middleware and schema types
The type definitions in lib/index.d.ts will continue to be generated from the TypeScript source without modification.
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Default Import Provides Function Directly
*For any* module system (CommonJS or ES modules) and any valid import statement, importing the default export from '@rbac/rbac' should provide the RBAC function directly without requiring `.default` accessor, and calling this function with valid configuration should execute successfully.
**Validates: Requirements 1.1, 1.3, 2.1**
### Property 2: TypeScript Type Resolution for Default Import
*For any* TypeScript project using either CommonJS or ES modules, importing the default export from '@rbac/rbac' should be recognized by the TypeScript compiler as a function with correct type signatures, and using it should not produce type errors or require `.default` accessor for correct types.
**Validates: Requirements 1.2, 6.1, 6.3**
### Property 3: Named Exports Accessibility
*For any* module system (CommonJS or ES modules) and any named export from the package (such as `createTenantRBAC`), importing that named export should provide the correct function with accurate TypeScript types, and calling it with valid arguments should execute successfully.
**Validates: Requirements 4.1, 6.2**
### Property 4: Backward Compatibility for .default Accessor
*For any* CommonJS consumer, accessing the package via `require('@rbac/rbac').default` should provide the same RBAC function as `require('@rbac/rbac')`, ensuring backward compatibility with existing code patterns.
**Validates: Requirements 2.2**
### Property 5: TypeScript Module Resolution
*For any* TypeScript project configuration (CommonJS or ES modules), the TypeScript compiler should successfully resolve type definitions for both default and named imports without errors, using the paths specified in the package.json exports field.
**Validates: Requirements 3.4**
## Error Handling
The fix primarily addresses module resolution and type checking, which are build-time and load-time concerns rather than runtime errors. However, we must ensure:
1. **Missing exports field**: Older Node.js versions (< 12.20) that don't support "exports" field will fall back to the "main" field, ensuring graceful degradation.
2. **Type resolution failures**: If TypeScript cannot resolve types, it should provide clear error messages pointing to the package configuration issue rather than cryptic module resolution errors.
3. **Invalid import syntax**: When consumers use incorrect import syntax, both Node.js and TypeScript should provide helpful error messages. The package configuration should not obscure these errors.
4. **Build failures**: If the TypeScript compilation fails, the build process should exit with a clear error code and message, preventing publication of broken packages.
**Error handling strategy:**
- Maintain "main" field as fallback for older tools
- Use "default" condition in exports as final fallback
- Ensure esModuleInterop is enabled for proper interop code generation
- Test with multiple Node.js and TypeScript versions to verify compatibility
## Testing Strategy
The testing approach combines unit tests for specific scenarios with property-based tests for comprehensive coverage across different module systems and usage patterns.
### Unit Testing
Unit tests will verify specific examples and edge cases:
1. **Package.json structure validation**: Verify the exports field contains all required keys (import, require, types, default) and points to correct files
2. **Build output verification**: Confirm that lib/ directory contains expected .js and .d.ts files after compilation
3. **Named exports preservation**: Verify all existing named exports (createTenantRBAC, middlewares, schemas) are still accessible
4. **Backward compatibility**: Test that existing CommonJS usage patterns continue to work
### Property-Based Testing
Property tests will verify universal correctness properties across many generated scenarios:
1. **Import correctness across module systems**: Generate test projects with different module configurations (CommonJS, ES modules) and verify default imports provide the function directly
2. **Type checking across configurations**: Generate TypeScript projects with different tsconfig.json settings and verify type resolution works correctly
3. **Named export accessibility**: Generate import statements for all named exports and verify they work in both module systems
4. **Backward compatibility**: Generate CommonJS require statements with and without .default accessor and verify both work
**Property test configuration:**
- Minimum 100 iterations per property test
- Use fast-check or similar library for JavaScript/TypeScript property testing
- Each test tagged with: **Feature: esm-import-fix, Property {number}: {property_text}**
- Tests should cover Node.js versions 14, 16, 18, 20 (LTS versions)
- Tests should cover TypeScript versions 4.5+ (versions with exports field support)
**Test execution approach:**
- Create temporary test projects with different configurations
- Use npm link or file: protocol to test the local package
- Execute imports and verify behavior programmatically
- Clean up test projects after execution
**Integration with existing tests:**
- Run existing test suite with the new package configuration
- Verify all existing tests pass without modification
- Add new tests specifically for ES module imports
- Ensure test coverage includes both module systems