-
Notifications
You must be signed in to change notification settings - Fork 83
Open
Labels
Description
Goal
Migrate rclnodejs from CommonJS to ES Modules, enabling modern import { Node } from 'rclnodejs' syntax while maintaining backward compatibility for generated message files.
File Inventory
| Category | Count | Notes |
|---|---|---|
lib/*.js |
48 files | Core library (39 use module.exports) |
rosidl_gen/*.js |
10 files | Message generators + 4 templates |
rosidl_parser/*.js |
2 files | IDL parser utilities |
rostsd_gen/*.js |
1 file | TypeScript declaration generator |
types/*.d.ts |
36 files | TypeScript declarations |
index.js |
1 file | Main entry point |
Dependencies ESM Status
| Package | Status | Strategy |
|---|---|---|
rxjs |
✅ ESM native | Direct import |
debug |
✅ ESM native | Direct import |
bindings |
createRequire() |
|
walk |
createRequire() |
|
json-bigint |
createRequire() |
|
@rclnodejs/ref-struct-di |
createRequire() |
|
@rclnodejs/ref-array-di |
createRequire() |
|
third_party/ref-napi |
Keep as CJS | |
node-addon-api |
Build-time only | No change needed |
Critical Issues Identified
1. Circular Dependencies
lib/rate.js→index.js: Creates Rate instances usingrclnodejs.init()andrclnodejs.createNode()index.js→lib/node.js→lib/lifecycle.js: Deferred imports at bottom ofindex.js- Solution: Convert to dynamic
import()or refactor dependency injection
2. Dynamic Requires in Generated Code
rosidl_gen/templates/message-template.jsgenerates CJS code with:const ref = require('../../third_party/ref-napi'); const StructType = require('@rclnodejs/ref-struct-di')(ref);
- Decision: Keep generated code as CJS (files go to
generated/directory)
3. Native Module Loading
lib/native_loader.jsusesbindings('rclnodejs')- Solution: Wrap with
createRequire()
Essential Migration Steps (5 Phases)
Phase 1: Package Configuration
Files: package.json
{
"type": "module",
"engines": { "node": ">= 18.0.0" },
"exports": {
".": {
"import": "./index.js",
"types": "./types/index.d.ts"
},
"./generated/*": "./generated/*"
}
}- Bump Node.js to 18+ (stable ESM support)
- Add exports map with dual support for generated CJS files
Phase 2: Core Library (lib/)
Files: 48 JS files
Changes per file:
- Remove
'use strict'; const X = require('./x.js')→import X from './x.js'const { A, B } = require('./x.js')→import { A, B } from './x.js'module.exports = X→export default Xmodule.exports = { A, B }→export { A, B }
Special handling (Requires verification):
native_loader.js: UsecreateRequire()forbindingsimport { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const bindings = require('bindings');
rate.js: Dynamic import for circular dependencyconst rclnodejs = await import('../index.js');
Phase 3: Entry Point (index.js)
Files: 1 file
- Convert all requires to imports
- Resolve circular deps with dynamic imports or reorganization
- Export named + default:
export { Node, Clock, Duration, ... }; export default rcl;
Phase 4: Generator/Parser Modules
Files: 13 JS files in rosidl_gen/, rosidl_parser/, rostsd_gen/
- Convert to ESM syntax
- Keep generated output as CJS (templates produce
require()code) - Generated files in
generated/load FFI dependencies that are CJS-only
Phase 5: TypeScript Declarations
Files: 36 .d.ts files in types/
- Update
types/index.d.tsfor ESM:declare module 'rclnodejs' { export const Node: typeof import('./node').Node; // ... named exports export default rcl; }
- Ensure ambient declarations remain compatible
Out of Scope (Deferred)
| Category | Files | Notes |
|---|---|---|
scripts/ |
11 JS files | Build/test scripts, can remain CJS |
test/ |
All test files | Can use --experimental-vm-modules |
example/ |
All examples | Update as documentation |
third_party/ref-napi |
2 files | Must stay CJS (native bindings) |
Risks & Mitigations
| Risk | Likelihood | Mitigation |
|---|---|---|
| FFI packages break | Medium | Wrap with createRequire(), test thoroughly |
| Circular deps cause runtime errors | Medium | Use dynamic import(), refactor if needed |
| Generated code compatibility | Low | Keep as CJS, test message serialization |
| Type definitions break | Low | Run tsd after each phase |
| Native addon loading fails | Low | Test across Node 18/20/22 |
Success Criteria
-
import rclnodejs from 'rclnodejs'works -
import { Node, Clock } from 'rclnodejs'works - All existing tests pass
- Generated message files load correctly
- TypeScript types resolve properly
- Examples run successfully