diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index cd30c058..6046ccb4 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -69,6 +69,19 @@ jobs: echo "=== arm64 prebuilds ===" ls -la prebuilds/linux-arm64/ + - name: Install dependencies + # --ignore-scripts skips install/postinstall (native build + message + # generation need a sourced ROS env, which this job doesn't have) while + # still installing devDependencies such as tsup needed for build:dist. + run: npm install --ignore-scripts + + - name: Build dist (ESM/CJS bundles) + # npm-pack.sh packs with --ignore-scripts, so the prepack hook that + # normally runs build:dist is skipped. dist/ is git-ignored and absent + # on a fresh checkout, so build it explicitly before packing — the + # published package's main/module/exports all point at ./dist/*. + run: npm run build:dist + - name: Pack run: ./scripts/npm-pack.sh @@ -96,7 +109,7 @@ jobs: fi if [[ "${{ inputs.dry_run }}" == "true" ]]; then echo "=== DRY RUN ===" - npm publish --provenance --access public --tag "$TAG" --dry-run ./dist/rclnodejs-*.tgz + npm publish --provenance --access public --tag "$TAG" --dry-run ./pack/rclnodejs-*.tgz else - npm publish --provenance --access public --tag "$TAG" ./dist/rclnodejs-*.tgz + npm publish --provenance --access public --tag "$TAG" ./pack/rclnodejs-*.tgz fi diff --git a/.gitignore b/.gitignore index bc101a55..4486a56c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ cpplint.py generated types/interfaces.d.ts dist +pack build log .vscode diff --git a/.npmignore b/.npmignore index 70c34eae..bcaeb57e 100644 --- a/.npmignore +++ b/.npmignore @@ -6,6 +6,7 @@ .gitignore .prettierrc.yml eslint.config.mjs +tsup.config.js CONTRIBUTORS.md Dockerfile cpplint.py @@ -15,9 +16,11 @@ build/ __pycache__/ *.pyc coverage/ -dist/ log/ install/ +pack/ +dist/package/ +dist/*.tgz test/ docs/ example/ diff --git a/README.md b/README.md index 09cf572f..68ffea1d 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ npm install 2. Run a publisher example from this checkout. ```bash -node example/topics/publisher/publisher-example.js +node example/topics/publisher/publisher-example.cjs ``` More runnable examples in [example/](https://github.com/RobotWebTools/rclnodejs/tree/develop/example) and step-by-step guides in [tutorials/](./tutorials/). diff --git a/demo/rosocket/README.md b/demo/rosocket/README.md index 4dffa0cd..28709c61 100644 --- a/demo/rosocket/README.md +++ b/demo/rosocket/README.md @@ -11,12 +11,12 @@ library required. - Subscribe to and publish on `/chatter` (`std_msgs/msg/String`). - Call `/add_two_ints` (`example_interfaces/srv/AddTwoInts`) — the service implementation lives in - [`example/services/service/service-example.js`](../../example/services/service/service-example.js) + [`example/services/service/service-example.cjs`](../../example/services/service/service-example.cjs) and is launched in a second terminal. ## Layout -- `server.js` — `rclnodejs` node + `startRosocket` bridge only. +- `server.cjs` — `rclnodejs` node + `startRosocket` bridge only. - `index.html` — single-file browser client using only built-in `WebSocket` and `JSON`. @@ -27,12 +27,12 @@ library required. source /opt/ros/$ROS_DISTRO/setup.bash # 2. Terminal A — start the WebSocket gateway -node demo/rosocket/server.js +node demo/rosocket/server.cjs # [rosocket-demo] listening on ws://localhost:9000 (bind=0.0.0.0) # 3. Terminal B — start the AddTwoInts service so the browser has # something to call -node example/services/service/service-example.js +node example/services/service/service-example.cjs ``` The server binds to `0.0.0.0:9000` so it is reachable from any host diff --git a/demo/rosocket/server.js b/demo/rosocket/server.cjs similarity index 94% rename from demo/rosocket/server.js rename to demo/rosocket/server.cjs index 5f5898d1..9d11cbe4 100644 --- a/demo/rosocket/server.js +++ b/demo/rosocket/server.cjs @@ -17,17 +17,17 @@ // ws://:9000/service/add_two_ints example_interfaces/srv/AddTwoInts // // Run inside WSL (where ROS 2 is sourced): -// node demo/rosocket/server.js +// node demo/rosocket/server.cjs // // To exercise the service, start the existing AddTwoInts example in a // second terminal (it implements `/add_two_ints`): -// node example/services/service/service-example.js +// node example/services/service/service-example.cjs // // Then open demo/rosocket/index.html on the Windows host browser. WSL2 // forwards localhost so `ws://localhost:9000` works as-is. See README.md // for fallback instructions. -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; const { startRosocket } = require('../../rosocket'); const PORT = Number(process.env.PORT) || 9000; diff --git a/demo/web/javascript/README.md b/demo/web/javascript/README.md index 082ed171..46290481 100644 --- a/demo/web/javascript/README.md +++ b/demo/web/javascript/README.md @@ -14,12 +14,12 @@ cd demo/web/javascript ```bash source /opt/ros//setup.bash -node runtime.js +node runtime.cjs # rclnodejs/web : ws://localhost:9000/capability # also http://localhost:9001/capability (call/publish, curl-able) ``` -`runtime.js` exposes a tiny `/add_two_ints` service + 1 Hz +`runtime.cjs` exposes a tiny `/add_two_ints` service + 1 Hz `/web_demo_tick` publisher so every panel has live data. **Shell 2 — static-file server (hosts `index.html` + maps `/sdk/*` to @@ -27,7 +27,7 @@ the in-repo [`web/`](../../../web/) folder so the page can `import` the SDK from a plain URL):** ```bash -node static.js +node static.cjs # Static files : http://localhost:8080/ ``` @@ -67,18 +67,18 @@ curl -sS -X POST http://localhost:9001/capability/call/add_two_ints \ Subscribe stays on WebSocket. -## Without the bundled `runtime.js` +## Without the bundled `runtime.cjs` -`runtime.js` bundles the rclnodejs/web runtime and the demo's sample +`runtime.cjs` bundles the rclnodejs/web runtime and the demo's sample ROS 2 nodes (the `/add_two_ints` service + the `/web_demo_tick` publisher) into one process so the demo runs out of the box. In a real project you already have those ROS 2 nodes running elsewhere, -so you only need the runtime. **Replace shell 1's `node runtime.js` -with the CLI** — shell 2 (`node static.js`) and the browser code are +so you only need the runtime. **Replace shell 1's `node runtime.cjs` +with the CLI** — shell 2 (`node static.cjs`) and the browser code are unchanged: ```bash -# shell 1 (instead of `node runtime.js`); the `-p rclnodejs` tells npx +# shell 1 (instead of `node runtime.cjs`); the `-p rclnodejs` tells npx # the `rclnodejs-web` binary lives inside the `rclnodejs` package: npx -p rclnodejs rclnodejs-web web.json diff --git a/demo/web/javascript/index.html b/demo/web/javascript/index.html index f3865598..bf5c2f37 100644 --- a/demo/web/javascript/index.html +++ b/demo/web/javascript/index.html @@ -1,7 +1,7 @@ diff --git a/demo/web/javascript/runtime.js b/demo/web/javascript/runtime.cjs similarity index 93% rename from demo/web/javascript/runtime.js rename to demo/web/javascript/runtime.cjs index 20608007..1b598407 100644 --- a/demo/web/javascript/runtime.js +++ b/demo/web/javascript/runtime.cjs @@ -7,18 +7,18 @@ // http://www.apache.org/licenses/LICENSE-2.0 // // rclnodejs/web demo — runtime side (rclnodejs/web runtime + the demo's -// ROS 2 nodes; named `runtime.js` to avoid being confused with the -// page-side `static.js`). +// ROS 2 nodes; named `runtime.cjs` to avoid being confused with the +// page-side `static.cjs`). // // 1. Source ROS 2 (`source /opt/ros//setup.bash`) -// 2. From this folder run `node runtime.js` -// 3. In another shell run `node static.js` to host +// 2. From this folder run `node runtime.cjs` +// 3. In another shell run `node static.cjs` to host // `index.html` on http://localhost:8080/ — same split as the // TypeScript demo's `tsx server.ts` + `vite`. 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; // In a downstream project this is the public, supported import: // const { createRuntime, WebSocketTransport, HttpTransport } = // require('rclnodejs/web/server'); @@ -135,7 +135,7 @@ async function main() { console.log(formatCapabilities(caps)); console.log(); console.log( - 'Static page: run `node static.js` in another shell, then open http://localhost:8080/' + 'Static page: run `node static.cjs` in another shell, then open http://localhost:8080/' ); // ---- Graceful shutdown ---------------------------------------------- diff --git a/demo/web/javascript/static.js b/demo/web/javascript/static.cjs similarity index 95% rename from demo/web/javascript/static.js rename to demo/web/javascript/static.cjs index 7bd92c0f..b0380e63 100644 --- a/demo/web/javascript/static.js +++ b/demo/web/javascript/static.cjs @@ -6,15 +6,15 @@ // // http://www.apache.org/licenses/LICENSE-2.0 // -// rclnodejs/web demo — static-file server (page side; named `static.js` -// to avoid being confused with the runtime-side `runtime.js`). +// rclnodejs/web demo — static-file server (page side; named `static.cjs` +// to avoid being confused with the runtime-side `runtime.cjs`). // // Serves index.html on port 8080 and maps `/sdk/*` to the in-repo // `web/` folder so the page can `import { connect } from '/sdk/index.js'` // without bundling. In a downstream project you'd `npm install rclnodejs` // and `import { connect } from 'rclnodejs/web'` instead. // -// Pair with `node runtime.js` (the rclnodejs/web runtime + the demo's +// Pair with `node runtime.cjs` (the rclnodejs/web runtime + the demo's // ROS 2 nodes) in another shell — the same split as the TypeScript // demo's `tsx server.ts` + `vite`. Production deployments use nginx, // a CDN, or any other static host. diff --git a/demo/web/typescript/server.ts b/demo/web/typescript/server.ts index 462aa890..649e1cf4 100644 --- a/demo/web/typescript/server.ts +++ b/demo/web/typescript/server.ts @@ -7,11 +7,11 @@ // http://www.apache.org/licenses/LICENSE-2.0 // // TypeScript demo server. Run with `npm run server` (which uses tsx) or -// `npx tsx server.ts`. Behaviour matches demo/web/javascript/runtime.js +// `npx tsx server.ts`. Behaviour matches demo/web/javascript/runtime.cjs // — same runtime + same demo nodes — except this side is written in // TypeScript so the typed SDK story is visible end to end. The static // page server is Vite (`npm run dev`), parallel to the JS demo's -// separate `node static.js`. +// separate `node static.cjs`. // rclnodejs is a CommonJS module without first-class ESM types; using // require keeps the server independent of how a downstream project @@ -30,7 +30,7 @@ const RUNTIME_PORT = Number(process.env.RUNTIME_PORT || 9000); const HTTP_PORT = Number(process.env.HTTP_PORT || 9001); // Render the registry as a small human-readable table — see the matching -// helper in demo/web/javascript/runtime.js. +// helper in demo/web/javascript/runtime.cjs. function formatCapabilities( caps: Record<'call' | 'publish' | 'subscribe', Record> ): string { diff --git a/eslint.config.mjs b/eslint.config.mjs index dbaa5b67..0605dc20 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -61,7 +61,7 @@ export default [ ecmaVersion: "latest", sourceType: "module", }, - files: ["lib/**/*.js", "test/**/*.js", "bin/**/*.js", "rosocket/**/*.js", "index.js"], + files: ["lib/**/*.js", "test/**/*.js", "bin/**/*.js", "rosocket/**/*.js", "index.js", "example/**/*.mjs"], rules: { ...eslintPluginPrettierRecommended.rules, }, @@ -78,7 +78,7 @@ export default [ sourceType: "commonjs", }, files: ["rosidl_parser/**/*.{js,cjs}", "rosidl_gen/**/*.{js,cjs}", - "rostsd_gen/**/*.{js,cjs}", "example/**/*.js"], + "rostsd_gen/**/*.{js,cjs}", "example/**/*.cjs"], rules: { ...eslintPluginPrettierRecommended.rules, }, diff --git a/example/actions/README.md b/example/actions/README.md index d2a3f583..f257c708 100644 --- a/example/actions/README.md +++ b/example/actions/README.md @@ -24,7 +24,7 @@ Actions are ideal for: The `action_client/` directory contains examples of nodes that send goals to action servers: -#### 1. Basic Action Client (`action-client-example.js`) +#### 1. Basic Action Client (`action-client-example.cjs`) **Purpose**: Demonstrates basic action client functionality with the Fibonacci action. @@ -40,9 +40,9 @@ The `action_client/` directory contains examples of nodes that send goals to act - Goal acceptance verification - Feedback handling during execution - Result processing with status checking -- **Run Command**: `node action_client/action-client-example.js` +- **Run Command**: `node action_client/action-client-example.cjs` -#### 2. Action Client with Cancellation (`action-client-cancel-example.js`) +#### 2. Action Client with Cancellation (`action-client-cancel-example.cjs`) **Purpose**: Demonstrates how to cancel an action goal during execution. @@ -58,9 +58,9 @@ The `action_client/` directory contains examples of nodes that send goals to act - Goal cancellation with `cancelGoal()` - Cancellation response verification - Cleanup and shutdown handling -- **Run Command**: `node action_client/action-client-cancel-example.js` +- **Run Command**: `node action_client/action-client-cancel-example.cjs` -#### 3. Action Client Validation (`action-client-validation-example.js`) +#### 3. Action Client Validation (`action-client-validation-example.cjs`) **Purpose**: Demonstrates goal validation features for action clients. @@ -80,14 +80,14 @@ The `action_client/` directory contains examples of nodes that send goals to act - **Detailed Errors**: Field-level validation issues with expected vs received types - **Strict Mode**: Detect extra fields that don't belong in the goal - **Reusable Validators**: Create validators for repeated goal validation -- **Run Command**: `node action_client/action-client-validation-example.js` +- **Run Command**: `node action_client/action-client-validation-example.cjs` - **Note**: Standalone example - demonstrates validation errors without requiring a running action server ### Action Server Examples The `action_server/` directory contains examples of nodes that provide action services: -#### 1. Basic Action Server (`action-server-example.js`) +#### 1. Basic Action Server (`action-server-example.cjs`) **Purpose**: Demonstrates basic action server implementation for computing Fibonacci sequences. @@ -103,9 +103,9 @@ The `action_server/` directory contains examples of nodes that provide action se - Execution callback with feedback publishing - Cancellation handling (`cancelCallback`) - Progress updates every second -- **Run Command**: `node action_server/action-server-example.js` +- **Run Command**: `node action_server/action-server-example.cjs` -#### 2. Deferred Execution Server (`action-server-defer-example.js`) +#### 2. Deferred Execution Server (`action-server-defer-example.cjs`) **Purpose**: Shows how to defer goal execution using timers and handle accepted callbacks. @@ -121,9 +121,9 @@ The `action_server/` directory contains examples of nodes that provide action se - Handle accepted callback (`handleAcceptedCallback`) - Timer-based execution control - Manual goal execution triggering -- **Run Command**: `node action_server/action-server-defer-example.js` +- **Run Command**: `node action_server/action-server-defer-example.cjs` -#### 3. Single Goal Server (`action-server-single-goal-example.js`) +#### 3. Single Goal Server (`action-server-single-goal-example.cjs`) **Purpose**: Demonstrates a server that only allows one active goal at a time. @@ -139,7 +139,7 @@ The `action_server/` directory contains examples of nodes that provide action se - Automatic abortion of previous goals - Goal state tracking (`isActive`) - Handle accepted callback for goal management -- **Run Command**: `node action_server/action-server-single-goal-example.js` +- **Run Command**: `node action_server/action-server-single-goal-example.cjs` ## How to Run the Examples @@ -156,7 +156,7 @@ The `action_server/` directory contains examples of nodes that provide action se ```bash cd example/actions - node action_server/action-server-example.js + node action_server/action-server-example.cjs ``` You should see: @@ -169,7 +169,7 @@ The `action_server/` directory contains examples of nodes that provide action se ```bash cd example/actions - node action_client/action-client-example.js + node action_client/action-client-example.cjs ``` 3. **Expected Output**: @@ -203,14 +203,14 @@ The `action_server/` directory contains examples of nodes that provide action se 1. **Start Server**: Run any action server example 2. **Start Cancellation Client**: ```bash - node action_client/action-client-cancel-example.js + node action_client/action-client-cancel-example.cjs ``` 3. **Expected Behavior**: Client sends goal, receives feedback for 2 seconds, then cancels #### Specialized Server Examples -- **Deferred Execution**: Use `action-server-defer-example.js` to see 3-second execution delay -- **Single Goal**: Use `action-server-single-goal-example.js` to test goal abortion behavior +- **Deferred Execution**: Use `action-server-defer-example.cjs` to see 3-second execution delay +- **Single Goal**: Use `action-server-single-goal-example.cjs` to test goal abortion behavior ## Action Components Explained diff --git a/example/actions/action_client/action-client-cancel-example.js b/example/actions/action_client/action-client-cancel-example.cjs similarity index 97% rename from example/actions/action_client/action-client-cancel-example.js rename to example/actions/action_client/action-client-cancel-example.cjs index 272d5459..abc70aaf 100644 --- a/example/actions/action_client/action-client-cancel-example.js +++ b/example/actions/action_client/action-client-cancel-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci'); class FibonacciActionClient { diff --git a/example/actions/action_client/action-client-example.js b/example/actions/action_client/action-client-example.cjs similarity index 97% rename from example/actions/action_client/action-client-example.js rename to example/actions/action_client/action-client-example.cjs index 18211379..47e33046 100644 --- a/example/actions/action_client/action-client-example.js +++ b/example/actions/action_client/action-client-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci'); class FibonacciActionClient { diff --git a/example/actions/action_client/action-client-validation-example.js b/example/actions/action_client/action-client-validation-example.cjs similarity index 96% rename from example/actions/action_client/action-client-validation-example.js rename to example/actions/action_client/action-client-validation-example.cjs index aa8a3f4a..880afd59 100644 --- a/example/actions/action_client/action-client-validation-example.js +++ b/example/actions/action_client/action-client-validation-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/actions/action_server/action-server-defer-example.js b/example/actions/action_server/action-server-defer-example.cjs similarity index 98% rename from example/actions/action_server/action-server-defer-example.js rename to example/actions/action_server/action-server-defer-example.cjs index 5a2768f3..99e75ec8 100644 --- a/example/actions/action_server/action-server-defer-example.js +++ b/example/actions/action_server/action-server-defer-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci'); class FibonacciActionServer { diff --git a/example/actions/action_server/action-server-example.js b/example/actions/action_server/action-server-example.cjs similarity index 97% rename from example/actions/action_server/action-server-example.js rename to example/actions/action_server/action-server-example.cjs index e61c9886..8e4da1a8 100644 --- a/example/actions/action_server/action-server-example.js +++ b/example/actions/action_server/action-server-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci'); class FibonacciActionServer { diff --git a/example/actions/action_server/action-server-single-goal-example.js b/example/actions/action_server/action-server-single-goal-example.cjs similarity index 98% rename from example/actions/action_server/action-server-single-goal-example.js rename to example/actions/action_server/action-server-single-goal-example.cjs index ab993baa..6ff88d2e 100644 --- a/example/actions/action_server/action-server-single-goal-example.js +++ b/example/actions/action_server/action-server-single-goal-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci'); class FibonacciActionServer { diff --git a/example/error-handling/README.md b/example/error-handling/README.md index 8990e6ad..5c15ffa4 100644 --- a/example/error-handling/README.md +++ b/example/error-handling/README.md @@ -34,7 +34,7 @@ RclNodeError (base) ## Examples -### 1. Type Validation (`error-handling-example.js` - Example 1) +### 1. Type Validation (`error-handling-example.cjs` - Example 1) **Purpose**: Demonstrates catching `TypeValidationError` when providing wrong argument types. @@ -42,7 +42,7 @@ RclNodeError (base) - **Features**: Shows `argumentName`, `expectedType`, and `providedValue` properties - **Key Properties**: `error.argumentName`, `error.expectedType`, `error.providedValue` -### 2. Range Validation (`error-handling-example.js` - Example 2) +### 2. Range Validation (`error-handling-example.cjs` - Example 2) **Purpose**: Demonstrates catching `RangeValidationError` for out-of-bounds values. @@ -50,7 +50,7 @@ RclNodeError (base) - **Features**: Shows `validationRule` and `providedValue` for constraint violations - **Key Properties**: `error.validationRule`, `error.providedValue`, `error.nodeName` -### 3. Service Errors (`error-handling-example.js` - Example 3) +### 3. Service Errors (`error-handling-example.cjs` - Example 3) **Purpose**: Demonstrates `TimeoutError` and `AbortError` handling for service operations. @@ -58,7 +58,7 @@ RclNodeError (base) - **Features**: Demonstrates timeout with `sendRequestAsync({ timeout })` and `AbortController` - **Key Properties**: `error.timeout`, `error.operationType`, `error.entityName` -### 4. Publisher Errors (`error-handling-example.js` - Example 4) +### 4. Publisher Errors (`error-handling-example.cjs` - Example 4) **Purpose**: Demonstrates catching `PublisherError` during publisher creation. @@ -66,7 +66,7 @@ RclNodeError (base) - **Features**: Shows error handling for topic creation failures - **Key Class**: `PublisherError` -### 5. Subscription Errors (`error-handling-example.js` - Example 5) +### 5. Subscription Errors (`error-handling-example.cjs` - Example 5) **Purpose**: Demonstrates catching `SubscriptionError` during subscription creation. @@ -74,7 +74,7 @@ RclNodeError (base) - **Features**: Shows error handling for subscription creation failures - **Key Class**: `SubscriptionError` -### 6. Parameter Errors (`error-handling-example.js` - Example 6) +### 6. Parameter Errors (`error-handling-example.cjs` - Example 6) **Purpose**: Demonstrates `ParameterTypeError` for parameter type mismatches. @@ -82,7 +82,7 @@ RclNodeError (base) - **Features**: Shows parameter validation and type checking - **Key Class**: `ParameterTypeError` -### 7. Name Validation (`error-handling-example.js` - Example 7) +### 7. Name Validation (`error-handling-example.cjs` - Example 7) **Purpose**: Demonstrates `NameValidationError` for invalid ROS names. @@ -90,7 +90,7 @@ RclNodeError (base) - **Features**: Shows `invalidIndex` property pointing to error location - **Key Properties**: `error.invalidIndex`, `error.message` -### 8. Error Recovery (`error-handling-example.js` - Example 8) +### 8. Error Recovery (`error-handling-example.cjs` - Example 8) **Purpose**: Demonstrates retry logic with structured error handling. @@ -98,7 +98,7 @@ RclNodeError (base) - **Features**: Shows differentiation between recoverable and non-recoverable errors - **Pattern**: Retry on `TimeoutError`, abort on `AbortError` or other errors -### 9. Error Serialization (`error-handling-example.js` - Example 9) +### 9. Error Serialization (`error-handling-example.cjs` - Example 9) **Purpose**: Demonstrates using `toJSON()` for logging and debugging. @@ -106,7 +106,7 @@ RclNodeError (base) - **Features**: Shows `toJSON()` method producing complete error information - **Method**: `error.toJSON()` -### 10. Generic Error Handler (`error-handling-example.js` - Example 10) +### 10. Generic Error Handler (`error-handling-example.cjs` - Example 10) **Purpose**: Demonstrates reusable error handler for all rclnodejs errors. @@ -121,7 +121,7 @@ RclNodeError (base) 2. **Run All Examples**: ```bash - node example/error-handling/error-handling-example.js + node example/error-handling/error-handling-example.cjs ``` 3. **Expected Output**: Demonstrates all 10 error handling patterns with clear success indicators diff --git a/example/error-handling/error-handling-example.js b/example/error-handling/error-handling-example.cjs similarity index 99% rename from example/error-handling/error-handling-example.js rename to example/error-handling/error-handling-example.cjs index a20c1e7d..e2549ad9 100644 --- a/example/error-handling/error-handling-example.js +++ b/example/error-handling/error-handling-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; /** * Example 1: Type Validation Errors diff --git a/example/graph/README.md b/example/graph/README.md index 7ada5bdd..8fca619f 100644 --- a/example/graph/README.md +++ b/example/graph/README.md @@ -22,7 +22,7 @@ Graph introspection allows you to: ## Graph Example -### ROS Graph Discovery (`ros-graph-example.js`) +### ROS Graph Discovery (`ros-graph-example.cjs`) **Purpose**: Demonstrates comprehensive ROS 2 graph introspection capabilities. @@ -61,7 +61,7 @@ This example creates a complete ROS 2 system with multiple nodes and then intros #### Run Command ```bash -node ros-graph-example.js +node ros-graph-example.cjs ``` ## Sample Output diff --git a/example/graph/ros-graph-example.js b/example/graph/ros-graph-example.cjs similarity index 98% rename from example/graph/ros-graph-example.js rename to example/graph/ros-graph-example.cjs index 9039b86a..f179a624 100644 --- a/example/graph/ros-graph-example.js +++ b/example/graph/ros-graph-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; console.log( 'This example creates the following nodes and outputs the corresponding ROS2 graph:' diff --git a/example/lifecycle/README.md b/example/lifecycle/README.md index 628e78e9..2cf36489 100644 --- a/example/lifecycle/README.md +++ b/example/lifecycle/README.md @@ -38,7 +38,7 @@ State transitions are triggered by these transitions: ## Lifecycle Example -### Lifecycle Node with Countdown (`lifecycle-node-example.js`) +### Lifecycle Node with Countdown (`lifecycle-node-example.cjs`) **Purpose**: Demonstrates a complete lifecycle node implementation with automated state management. @@ -81,7 +81,7 @@ This example creates a lifecycle node that: #### Run Command ```bash -node lifecycle-node-example.js +node lifecycle-node-example.cjs ``` ## Sample Output diff --git a/example/lifecycle/lifecycle-node-example.js b/example/lifecycle/lifecycle-node-example.cjs similarity index 98% rename from example/lifecycle/lifecycle-node-example.js rename to example/lifecycle/lifecycle-node-example.cjs index 905895be..5a89fe67 100644 --- a/example/lifecycle/lifecycle-node-example.js +++ b/example/lifecycle/lifecycle-node-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; const NODE_NAME = 'test_node'; const TOPIC = 'test'; diff --git a/example/message-introspector/README.md b/example/message-introspector/README.md index b2319ca8..cd4796ad 100644 --- a/example/message-introspector/README.md +++ b/example/message-introspector/README.md @@ -6,14 +6,14 @@ This directory contains an example demonstrating the `MessageIntrospector` class The `MessageIntrospector` class provides a simple way to understand the structure of ROS 2 messages without directly using `loader.loadInterface`. It's useful for debugging, generating documentation, and building dynamic UIs based on message structure. -## MessageIntrospector Example (`message-introspector-example.js`) +## MessageIntrospector Example (`message-introspector-example.cjs`) **Purpose**: Demonstrates how to inspect message structure, fields, and default values. ### Run Command ```bash -node message-introspector-example.js +node message-introspector-example.cjs ``` ### Expected Output diff --git a/example/message-introspector/message-introspector-example.js b/example/message-introspector/message-introspector-example.cjs similarity index 96% rename from example/message-introspector/message-introspector-example.js rename to example/message-introspector/message-introspector-example.cjs index 45c9d24f..484131dc 100644 --- a/example/message-introspector/message-introspector-example.js +++ b/example/message-introspector/message-introspector-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; /** * This example demonstrates the MessageIntrospector class for diff --git a/example/parameter/README.md b/example/parameter/README.md index 7bb691d5..7c9d548d 100644 --- a/example/parameter/README.md +++ b/example/parameter/README.md @@ -22,7 +22,7 @@ Parameters are ideal for: ### Local Parameters (On Current Node) -#### 1. Parameter Declaration (`parameter-declaration-example.js`) +#### 1. Parameter Declaration (`parameter-declaration-example.cjs`) **Purpose**: Demonstrates how to declare and use parameters in a ROS 2 node. @@ -41,9 +41,9 @@ Parameters are ideal for: - **Name**: `param1` - **Type**: `PARAMETER_STRING` - **Default Value**: `"hello world"` -- **Run Command**: `node parameter-declaration-example.js` +- **Run Command**: `node parameter-declaration-example.cjs` -#### 2. Parameter Override (`parameter-override-example.js`) +#### 2. Parameter Override (`parameter-override-example.cjs`) **Purpose**: Shows how to override parameter values using command-line arguments. @@ -63,11 +63,11 @@ Parameters are ideal for: - **Type**: `PARAMETER_STRING` - **Default Value**: `"hello world"` - **Override Value**: `"hello ros2"` (via command line) -- **Run Command**: `node parameter-override-example.js` +- **Run Command**: `node parameter-override-example.cjs` ### Remote Parameter Access (On Other Nodes) -#### 3. ParameterClient Basic (`parameter-client-basic-example.js`) +#### 3. ParameterClient Basic (`parameter-client-basic-example.cjs`) **Purpose**: Demonstrates accessing and modifying parameters on a remote node using `ParameterClient`. @@ -84,9 +84,9 @@ Parameters are ideal for: - Batch parameter retrieval - Automatic type inference for parameter values - **Target Node**: `turtlesim` (run: `ros2 run turtlesim turtlesim_node`) -- **Run Command**: `node parameter-client-basic-example.js` +- **Run Command**: `node parameter-client-basic-example.cjs` -#### 4. ParameterClient Advanced (`parameter-client-advanced-example.js`) +#### 4. ParameterClient Advanced (`parameter-client-advanced-example.cjs`) **Purpose**: Comprehensive example showing all ParameterClient features and capabilities. @@ -108,9 +108,9 @@ Parameters are ideal for: - Automatic BigInt conversion for integers - Type inference demonstrations - Lifecycle management -- **Run Command**: `node parameter-client-advanced-example.js` +- **Run Command**: `node parameter-client-advanced-example.cjs` -#### 5. ParameterWatcher (`parameter-watcher-example.js`) +#### 5. ParameterWatcher (`parameter-watcher-example.cjs`) **Purpose**: Demonstrates watching parameter changes on a remote node in real-time. @@ -126,7 +126,7 @@ Parameters are ideal for: - Built on top of ParameterClient - Simple EventEmitter API - **Target Node**: `turtlesim` (run: `ros2 run turtlesim turtlesim_node`) -- **Run Command**: `node parameter-watcher-example.js` +- **Run Command**: `node parameter-watcher-example.cjs` - **Test Changes**: In another terminal, run `ros2 param set /turtlesim background_r 200` **ParameterClient Key Features**: @@ -184,7 +184,7 @@ Parameters are ideal for: ```bash cd example/parameter -node parameter-declaration-example.js +node parameter-declaration-example.cjs ``` **Expected Output**: @@ -212,7 +212,7 @@ ParameterDescriptor { ```bash cd example/parameter -node parameter-override-example.js +node parameter-override-example.cjs ``` **Expected Output**: @@ -250,7 +250,7 @@ Then in another terminal: ```bash cd example/parameter -node parameter-client-basic-example.js +node parameter-client-basic-example.cjs ``` **Expected Output**: @@ -264,7 +264,7 @@ Updated background_b: 200n ```bash cd example/parameter -node parameter-client-advanced-example.js +node parameter-client-advanced-example.cjs ``` **Expected Output**: @@ -297,7 +297,7 @@ Then in another terminal: ```bash cd example/parameter -node parameter-watcher-example.js +node parameter-watcher-example.cjs ``` **Expected Output**: diff --git a/example/parameter/parameter-client-advanced-example.js b/example/parameter/parameter-client-advanced-example.cjs similarity index 98% rename from example/parameter/parameter-client-advanced-example.js rename to example/parameter/parameter-client-advanced-example.cjs index 0e03dfbc..ccc00106 100644 --- a/example/parameter/parameter-client-advanced-example.js +++ b/example/parameter/parameter-client-advanced-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; const { ParameterType, Parameter, ParameterDescriptor } = rclnodejs; diff --git a/example/parameter/parameter-client-basic-example.js b/example/parameter/parameter-client-basic-example.cjs similarity index 96% rename from example/parameter/parameter-client-basic-example.js rename to example/parameter/parameter-client-basic-example.cjs index f7424227..0a725dd3 100644 --- a/example/parameter/parameter-client-basic-example.js +++ b/example/parameter/parameter-client-basic-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/parameter/parameter-declaration-example.js b/example/parameter/parameter-declaration-example.cjs similarity index 96% rename from example/parameter/parameter-declaration-example.js rename to example/parameter/parameter-declaration-example.cjs index d2ef5efd..ab85c11e 100644 --- a/example/parameter/parameter-declaration-example.js +++ b/example/parameter/parameter-declaration-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; const ParameterType = rclnodejs.ParameterType; const Parameter = rclnodejs.Parameter; diff --git a/example/parameter/parameter-override-example.js b/example/parameter/parameter-override-example.cjs similarity index 96% rename from example/parameter/parameter-override-example.js rename to example/parameter/parameter-override-example.cjs index e3f76897..1fce85b1 100644 --- a/example/parameter/parameter-override-example.js +++ b/example/parameter/parameter-override-example.cjs @@ -10,7 +10,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; const ParameterType = rclnodejs.ParameterType; const Parameter = rclnodejs.Parameter; diff --git a/example/parameter/parameter-watcher-example.js b/example/parameter/parameter-watcher-example.cjs similarity index 96% rename from example/parameter/parameter-watcher-example.js rename to example/parameter/parameter-watcher-example.cjs index 5edfc775..c178ce27 100644 --- a/example/parameter/parameter-watcher-example.js +++ b/example/parameter/parameter-watcher-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/rate/README.md b/example/rate/README.md index 685e535e..52eb7e7a 100644 --- a/example/rate/README.md +++ b/example/rate/README.md @@ -22,7 +22,7 @@ Rate control is essential for: ## Rate Example -### Rate-Limited Loop with High-Frequency Publisher (`rate-example.js`) +### Rate-Limited Loop with High-Frequency Publisher (`rate-example.cjs`) **Purpose**: Demonstrates rate control in a scenario with mismatched publication and processing frequencies. @@ -62,7 +62,7 @@ This example creates a sophisticated timing demonstration that shows: #### Run Command ```bash -node rate-example.js +node rate-example.cjs ``` ## Sample Output diff --git a/example/rate/rate-example.js b/example/rate/rate-example.cjs similarity index 96% rename from example/rate/rate-example.js rename to example/rate/rate-example.cjs index 527f6ec5..cafce0d8 100644 --- a/example/rate/rate-example.js +++ b/example/rate/rate-example.cjs @@ -12,7 +12,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; /** * This example demonstrates a rate limited loop running at diff --git a/example/rosidl/README.md b/example/rosidl/README.md index 1cefc448..e441acbf 100644 --- a/example/rosidl/README.md +++ b/example/rosidl/README.md @@ -23,7 +23,7 @@ ROSIDL parsing is useful for: ## ROSIDL Examples -### 1. Message Parsing (`rosidl-parse-msg-example.js`) +### 1. Message Parsing (`rosidl-parse-msg-example.cjs`) **Purpose**: Demonstrates parsing ROS 2 message definition files (.msg). @@ -63,7 +63,7 @@ float32 a # Alpha (transparency) component #### Run Command ```bash -node rosidl-parse-msg-example.js +node rosidl-parse-msg-example.cjs ``` #### Expected Output @@ -105,7 +105,7 @@ fields includes: } ``` -### 2. Service Parsing (`rosidl-parse-srv-example.js`) +### 2. Service Parsing (`rosidl-parse-srv-example.cjs`) **Purpose**: Demonstrates parsing ROS 2 service definition files (.srv). @@ -147,7 +147,7 @@ string message # Informational message #### Run Command ```bash -node rosidl-parse-srv-example.js +node rosidl-parse-srv-example.cjs ``` #### Expected Output @@ -183,7 +183,7 @@ srv response fields includes: } ``` -### 3. Action Parsing (`rosidl-parse-action-example.js`) +### 3. Action Parsing (`rosidl-parse-action-example.cjs`) **Purpose**: Demonstrates parsing ROS 2 action definition files (.action). @@ -227,7 +227,7 @@ int32[] partial_sequence # Partial sequence (feedback) #### Run Command ```bash -node rosidl-parse-action-example.js +node rosidl-parse-action-example.cjs ``` #### Expected Output @@ -289,7 +289,7 @@ Each parsed field contains these properties: ### Message Parsing ```javascript -const parser = require('../rosidl_parser/rosidl_parser.js'); +const parser = require('../rosidl_parser/rosidl_parser.cjs'); parser.parseMessageFile(packageName, packagePath).then((spec) => { console.log(`Message: ${spec.msgName}`); diff --git a/example/rosidl/rosidl-parse-action-example.js b/example/rosidl/rosidl-parse-action-example.cjs similarity index 100% rename from example/rosidl/rosidl-parse-action-example.js rename to example/rosidl/rosidl-parse-action-example.cjs diff --git a/example/rosidl/rosidl-parse-msg-example.js b/example/rosidl/rosidl-parse-msg-example.cjs similarity index 100% rename from example/rosidl/rosidl-parse-msg-example.js rename to example/rosidl/rosidl-parse-msg-example.cjs diff --git a/example/rosidl/rosidl-parse-srv-example.js b/example/rosidl/rosidl-parse-srv-example.cjs similarity index 100% rename from example/rosidl/rosidl-parse-srv-example.js rename to example/rosidl/rosidl-parse-srv-example.cjs diff --git a/example/services/README.md b/example/services/README.md index 09027cc1..3f52e5b3 100644 --- a/example/services/README.md +++ b/example/services/README.md @@ -15,7 +15,7 @@ ROS 2 services provide a request-response communication pattern where clients se ### AddTwoInts Service -#### Service Server (`service/service-example.js`) +#### Service Server (`service/service-example.cjs`) **Purpose**: Demonstrates creating a service server that adds two integers. @@ -29,9 +29,9 @@ ROS 2 services provide a request-response communication pattern where clients se - **Features**: - Service introspection (ROS 2 Iron+) for monitoring service calls - Proper response handling using `response.template` and `response.send()` -- **Run Command**: `node example/services/service/service-example.js` +- **Run Command**: `node example/services/service/service-example.cjs` -#### Service Client (`client/client-example.js`) +#### Service Client (`client/client-example.cjs`) **Purpose**: Demonstrates creating a service client that sends requests to the AddTwoInts service. @@ -46,9 +46,9 @@ ROS 2 services provide a request-response communication pattern where clients se - Service availability checking with `waitForService()` - Service introspection configuration (ROS 2 Iron+) - Asynchronous request handling with callbacks -- **Run Command**: `node example/services/client/client-example.js` +- **Run Command**: `node example/services/client/client-example.cjs` -#### Async Service Client (`client/async-client-example.js`) +#### Async Service Client (`client/async-client-example.cjs`) **Purpose**: Demonstrates modern async/await patterns for service communication, solving callback hell and providing cleaner error handling. @@ -68,9 +68,9 @@ ROS 2 services provide a request-response communication pattern where clients se - **Error Types**: Specific error types (`TimeoutError`, `AbortError`) for better error handling (async only) - **Backward Compatible**: Works alongside existing callback-based `sendRequest()` - **TypeScript Ready**: Full type safety with comprehensive TypeScript definitions -- **Run Command**: `node example/services/client/async-client-example.js` +- **Run Command**: `node example/services/client/async-client-example.cjs` -#### Service Client Validation (`client/client-validation-example.js`) +#### Service Client Validation (`client/client-validation-example.cjs`) **Purpose**: Demonstrates request validation features for service clients. @@ -89,7 +89,7 @@ ROS 2 services provide a request-response communication pattern where clients se - **Dynamic Toggle**: Enable/disable validation with `willValidateRequest` property - **Detailed Errors**: Field-level validation issues with expected vs received types - **Strict Mode**: Detect extra fields that don't belong in the request -- **Run Command**: `node example/services/client/client-validation-example.js` +- **Run Command**: `node example/services/client/client-validation-example.cjs` - **Note**: Standalone example - demonstrates validation errors without requiring a running service **Key API Differences**: @@ -129,7 +129,7 @@ try { ### GetMap Service -#### Service Server (`service/getmap-service-example.js`) +#### Service Server (`service/getmap-service-example.cjs`) **Purpose**: Demonstrates creating a service server that provides occupancy grid map data. @@ -145,9 +145,9 @@ try { - Realistic navigation map data for robotics applications - Service introspection support (ROS 2 Iron+) - Detailed logging of map properties -- **Run Command**: `node example/services/service/getmap-service-example.js` +- **Run Command**: `node example/services/service/getmap-service-example.cjs` -#### Service Client (`client/getmap-client-example.js`) +#### Service Client (`client/getmap-client-example.cjs`) **Purpose**: Demonstrates creating a service client that requests map data from the GetMap service. @@ -163,7 +163,7 @@ try { - Map data analysis (cell distribution, metadata extraction) - Educational output showing map properties - Visual representation of map data -- **Run Command**: `node example/services/client/getmap-client-example.js` +- **Run Command**: `node example/services/client/getmap-client-example.cjs` ## How to Run the Examples @@ -175,7 +175,7 @@ try { ```bash cd /path/to/rclnodejs - node example/services/service/service-example.js + node example/services/service/service-example.cjs ``` You should see: @@ -188,7 +188,7 @@ try { ```bash cd /path/to/rclnodejs - node example/services/client/client-example.js + node example/services/client/client-example.cjs ``` 4. **Expected Output**: @@ -216,14 +216,14 @@ try { ```bash cd /path/to/rclnodejs - node example/services/service/service-example.js + node example/services/service/service-example.cjs ``` 3. **Start the Async Client**: In another terminal, run: ```bash cd /path/to/rclnodejs - node example/services/client/async-client-example.js + node example/services/client/async-client-example.cjs ``` 4. **Expected Output**: @@ -251,7 +251,7 @@ try { ```bash cd /path/to/rclnodejs - node example/services/service/getmap-service-example.js + node example/services/service/getmap-service-example.cjs ``` You should see: @@ -266,7 +266,7 @@ try { ```bash cd /path/to/rclnodejs - node example/services/client/getmap-client-example.js + node example/services/client/getmap-client-example.cjs ``` 4. **Expected Output**: diff --git a/example/services/client/async-client-example.js b/example/services/client/async-client-example.cjs similarity index 97% rename from example/services/client/async-client-example.js rename to example/services/client/async-client-example.cjs index 6ded71e2..b1eb5d2e 100644 --- a/example/services/client/async-client-example.js +++ b/example/services/client/async-client-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/services/client/async-client-example.mjs b/example/services/client/async-client-example.mjs new file mode 100644 index 00000000..7508addf --- /dev/null +++ b/example/services/client/async-client-example.mjs @@ -0,0 +1,65 @@ +// Copyright (c) 2026 RobotWebTools Contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ESM variant of services/client/async-client-example.js. +// +// From an installed package you would write `import rclnodejs from 'rclnodejs'`; +// run from this checkout we import the source entry point directly. + +import rclnodejs from '../../../index.js'; + +await rclnodejs.init(); + +const node = rclnodejs.createNode('async_client_example_node'); +const client = node.createClient( + 'example_interfaces/srv/AddTwoInts', + 'add_two_ints' +); + +if ( + rclnodejs.DistroUtils.getDistroId() > + rclnodejs.DistroUtils.getDistroId('humble') +) { + // To view service events use the following command: + // ros2 topic echo "/add_two_ints/_service_event" + client.configureIntrospection( + node.getClock(), + rclnodejs.QoS.profileSystemDefault, + rclnodejs.ServiceIntrospectionStates.METADATA + ); +} + +const request = { + a: BigInt(Math.floor(Math.random() * 100)), + b: BigInt(Math.floor(Math.random() * 100)), +}; + +const available = await client.waitForService(1000); +if (!available) { + console.log('Error: service not available'); + rclnodejs.shutdown(); +} else { + rclnodejs.spin(node); + + console.log(`Sending: ${typeof request}`, request); + + try { + const response = await client.sendRequestAsync(request, { timeout: 5000 }); + console.log(`Result: ${typeof response}`, response); + } catch (error) { + console.log(`Error: ${error.message}`); + } finally { + rclnodejs.shutdown(); + } +} diff --git a/example/services/client/client-example.js b/example/services/client/client-example.cjs similarity index 96% rename from example/services/client/client-example.js rename to example/services/client/client-example.cjs index ccd606e4..89e4eb1f 100644 --- a/example/services/client/client-example.js +++ b/example/services/client/client-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/services/client/client-validation-example.js b/example/services/client/client-validation-example.cjs similarity index 96% rename from example/services/client/client-validation-example.js rename to example/services/client/client-validation-example.cjs index 3b6a45c9..2817a373 100644 --- a/example/services/client/client-validation-example.js +++ b/example/services/client/client-validation-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/services/client/getmap-client-example.js b/example/services/client/getmap-client-example.cjs similarity index 98% rename from example/services/client/getmap-client-example.js rename to example/services/client/getmap-client-example.cjs index 55cfb926..a5381d30 100644 --- a/example/services/client/getmap-client-example.js +++ b/example/services/client/getmap-client-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/services/service/getmap-service-example.js b/example/services/service/getmap-service-example.cjs similarity index 98% rename from example/services/service/getmap-service-example.js rename to example/services/service/getmap-service-example.cjs index 4e78411d..11994c1c 100644 --- a/example/services/service/getmap-service-example.js +++ b/example/services/service/getmap-service-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs .init() diff --git a/example/services/service/service-example.js b/example/services/service/service-example.cjs similarity index 96% rename from example/services/service/service-example.js rename to example/services/service/service-example.cjs index 9ac2c1b8..98b12133 100644 --- a/example/services/service/service-example.js +++ b/example/services/service/service-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs .init() diff --git a/example/services/service/service-example.mjs b/example/services/service/service-example.mjs new file mode 100644 index 00000000..c7650ac7 --- /dev/null +++ b/example/services/service/service-example.mjs @@ -0,0 +1,52 @@ +// Copyright (c) 2026 RobotWebTools Contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ESM variant of services/service/service-example.js. +// +// From an installed package you would write `import rclnodejs from 'rclnodejs'`; +// run from this checkout we import the source entry point directly. + +import rclnodejs from '../../../index.js'; + +await rclnodejs.init(); + +const node = rclnodejs.createNode('service_example_node'); + +const service = node.createService( + 'example_interfaces/srv/AddTwoInts', + 'add_two_ints', + (request, response) => { + console.log(`Incoming request: ${typeof request}`, request); + const result = response.template; + result.sum = request.a + request.b; + console.log(`Sending response: ${typeof result}`, result, '\n--'); + response.send(result); + } +); + +if ( + rclnodejs.DistroUtils.getDistroId() > + rclnodejs.DistroUtils.getDistroId('humble') +) { + console.log('Introspection configured'); + // To view service events use the following command: + // ros2 topic echo "/add_two_ints/_service_event" + service.configureIntrospection( + node.getClock(), + rclnodejs.QoS.profileSystemDefault, + rclnodejs.ServiceIntrospectionStates.CONTENTS + ); +} + +rclnodejs.spin(node); diff --git a/example/timer/timer-example.js b/example/timer/timer-example.cjs similarity index 90% rename from example/timer/timer-example.js rename to example/timer/timer-example.cjs index da43c84d..5fbe27c5 100644 --- a/example/timer/timer-example.js +++ b/example/timer/timer-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../index.js'); +const rclnodejs = require('../../index.js').default; rclnodejs .init() @@ -22,7 +22,7 @@ rclnodejs let node = rclnodejs.createNode('timer_example_node'); let timer = node.createTimer(BigInt(1000000000), () => { - console.log('One second escaped!'); + console.log('One second elapsed!'); console.log('Cancel this timer.'); timer.cancel(); @@ -37,7 +37,7 @@ rclnodejs 'The next call will be ' + timer.timeUntilNextCall() + 'ms later.' ); - console.log('Shuting down...'); + console.log('Shutting down...'); rclnodejs.shutdown(); }); diff --git a/example/timer/timer-example.mjs b/example/timer/timer-example.mjs new file mode 100644 index 00000000..56d15bd8 --- /dev/null +++ b/example/timer/timer-example.mjs @@ -0,0 +1,46 @@ +// Copyright (c) 2026 RobotWebTools Contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ESM variant of timer-example.js. +// +// From an installed package you would write `import rclnodejs from 'rclnodejs'`; +// run from this checkout we import the source entry point directly. + +import rclnodejs from '../../index.js'; + +await rclnodejs.init(); + +const node = rclnodejs.createNode('timer_example_node'); + +const timer = node.createTimer(BigInt(1000000000), () => { + console.log('One second elapsed!'); + + console.log('Cancel this timer.'); + timer.cancel(); + + if (timer.isCanceled()) { + console.log('The timer has been canceled successfully.'); + } + + console.log('Reset the timer.'); + timer.reset(); + console.log( + 'The next call will be ' + timer.timeUntilNextCall() + 'ms later.' + ); + + console.log('Shutting down...'); + rclnodejs.shutdown(); +}); + +rclnodejs.spin(node); diff --git a/example/topics/README.md b/example/topics/README.md index 0b147304..0832f2e1 100644 --- a/example/topics/README.md +++ b/example/topics/README.md @@ -10,66 +10,66 @@ ROS 2 topics are a fundamental communication pattern that allows nodes to exchan The `publisher/` directory contains examples of nodes that publish messages to topics: -### 1. Basic Publisher (`publisher-example.js`) +### 1. Basic Publisher (`publisher-example.cjs`) **Purpose**: Demonstrates basic string message publishing. - **Message Type**: `std_msgs/msg/String` - **Topic**: `topic` - **Functionality**: Publishes "Hello ROS" messages every second -- **Run Command**: `node publisher/publisher-example.js` +- **Run Command**: `node publisher/publisher-example.cjs` -### 2. Content Filter Publisher (`publisher-content-filter-example.js`) +### 2. Content Filter Publisher (`publisher-content-filter-example.cjs`) **Purpose**: Publishes temperature data for content filtering demonstrations. - **Message Type**: `sensor_msgs/msg/Temperature` - **Topic**: `temperature` - **Functionality**: Publishes random temperature values (0-100°C) every 100ms with header information -- **Run Command**: `node publisher/publisher-content-filter-example.js` -- **Pair**: Works with `subscription-content-filter-example.js` +- **Run Command**: `node publisher/publisher-content-filter-example.cjs` +- **Pair**: Works with `subscription-content-filter-example.cjs` -### 3. Message Publisher (`publisher-message-example.js`) +### 3. Message Publisher (`publisher-message-example.cjs`) **Purpose**: Demonstrates publishing complex structured messages. - **Message Type**: `sensor_msgs/msg/JointState` - **Topic**: `JointState` - **Functionality**: Publishes joint state information with header, names, positions, velocities, and efforts -- **Run Command**: `node publisher/publisher-message-example.js` -- **Pair**: Works with `subscription-message-example.js` +- **Run Command**: `node publisher/publisher-message-example.cjs` +- **Pair**: Works with `subscription-message-example.cjs` -### 4. MultiArray Publisher (`publisher-multiarray-example.js`) +### 4. MultiArray Publisher (`publisher-multiarray-example.cjs`) **Purpose**: Shows how to publish multi-dimensional array data. - **Message Type**: `std_msgs/msg/Int32MultiArray` - **Topic**: `Int32MultiArray` - **Functionality**: Publishes 3D array data (2×3×3) with proper layout information -- **Run Command**: `node publisher/publisher-multiarray-example.js` -- **Pair**: Works with `subscription-multiarray-example.js` +- **Run Command**: `node publisher/publisher-multiarray-example.cjs` +- **Pair**: Works with `subscription-multiarray-example.cjs` -### 5. QoS Publisher (`publisher-qos-example.js`) +### 5. QoS Publisher (`publisher-qos-example.cjs`) **Purpose**: Demonstrates Quality of Service (QoS) configuration for publishers. - **Message Type**: `std_msgs/msg/String` - **Topic**: `topic` - **Functionality**: Publishes messages with custom QoS settings (system default policies) -- **Run Command**: `node publisher/publisher-qos-example.js` -- **Pair**: Works with `subscription-qos-example.js` +- **Run Command**: `node publisher/publisher-qos-example.cjs` +- **Pair**: Works with `subscription-qos-example.cjs` -### 6. Raw Message Publisher (`publisher-raw-message.js`) +### 6. Raw Message Publisher (`publisher-raw-message.cjs`) **Purpose**: Shows how to publish raw binary data. - **Message Type**: `test_msgs/msg/BasicTypes` - **Topic**: `chatter` - **Functionality**: Publishes raw Buffer data ("Hello ROS World") -- **Run Command**: `node publisher/publisher-raw-message.js` -- **Pair**: Works with `subscription-raw-message.js` +- **Run Command**: `node publisher/publisher-raw-message.cjs` +- **Pair**: Works with `subscription-raw-message.cjs` -### 7. Publisher Validation (`publisher-validation-example.js`) +### 7. Publisher Validation (`publisher-validation-example.cjs`) **Purpose**: Demonstrates message validation features for publishers. @@ -87,24 +87,24 @@ The `publisher/` directory contains examples of nodes that publish messages to t - Catch invalid messages before publishing - Dynamic validation toggle with `willValidateMessage` property - Detailed error reports with field-level issues -- **Run Command**: `node publisher/publisher-validation-example.js` +- **Run Command**: `node publisher/publisher-validation-example.cjs` - **Note**: Standalone example - no subscriber required ## Subscriber Examples The `subscriber/` directory contains examples of nodes that subscribe to topics: -### 1. Basic Subscriber (`subscription-example.js`) +### 1. Basic Subscriber (`subscription-example.cjs`) **Purpose**: Demonstrates basic message subscription. - **Message Type**: `std_msgs/msg/String` - **Topic**: `topic` - **Functionality**: Receives and logs string messages -- **Run Command**: `node subscriber/subscription-example.js` -- **Pair**: Works with `publisher-example.js` +- **Run Command**: `node subscriber/subscription-example.cjs` +- **Pair**: Works with `publisher-example.cjs` -### 2. Content Filter Subscriber (`subscription-content-filter-example.js`) +### 2. Content Filter Subscriber (`subscription-content-filter-example.cjs`) **Purpose**: Demonstrates content filtering to receive only relevant messages. @@ -112,20 +112,20 @@ The `subscriber/` directory contains examples of nodes that subscribe to topics: - **Topic**: `temperature` - **Functionality**: Only receives temperature messages above 50°C using content filters - **Features**: ROS 2 Humble+ content filtering with expression `temperature > %0` -- **Run Command**: `node subscriber/subscription-content-filter-example.js` -- **Pair**: Works with `publisher-content-filter-example.js` +- **Run Command**: `node subscriber/subscription-content-filter-example.cjs` +- **Pair**: Works with `publisher-content-filter-example.cjs` -### 3. Message Subscriber (`subscription-message-example.js`) +### 3. Message Subscriber (`subscription-message-example.cjs`) **Purpose**: Receives complex structured messages. - **Message Type**: `sensor_msgs/msg/JointState` - **Topic**: `JointState` - **Functionality**: Receives and logs joint state information -- **Run Command**: `node subscriber/subscription-message-example.js` -- **Pair**: Works with `publisher-message-example.js` +- **Run Command**: `node subscriber/subscription-message-example.cjs` +- **Pair**: Works with `publisher-message-example.cjs` -### 4. MultiArray Subscriber (`subscription-multiarray-example.js`) +### 4. MultiArray Subscriber (`subscription-multiarray-example.cjs`) **Purpose**: Demonstrates receiving and parsing multi-dimensional arrays. @@ -133,20 +133,20 @@ The `subscriber/` directory contains examples of nodes that subscribe to topics: - **Topic**: `Int32MultiArray` - **Functionality**: Receives 3D arrays and iterates through all elements with proper indexing - **Features**: Shows how to parse layout information and access array elements -- **Run Command**: `node subscriber/subscription-multiarray-example.js` -- **Pair**: Works with `publisher-multiarray-example.js` +- **Run Command**: `node subscriber/subscription-multiarray-example.cjs` +- **Pair**: Works with `publisher-multiarray-example.cjs` -### 5. QoS Subscriber (`subscription-qos-example.js`) +### 5. QoS Subscriber (`subscription-qos-example.cjs`) **Purpose**: Demonstrates QoS configuration for subscribers. - **Message Type**: `std_msgs/msg/String` - **Topic**: `topic` - **Functionality**: Receives messages with system default QoS profile -- **Run Command**: `node subscriber/subscription-qos-example.js` -- **Pair**: Works with `publisher-qos-example.js` +- **Run Command**: `node subscriber/subscription-qos-example.cjs` +- **Pair**: Works with `publisher-qos-example.cjs` -### 6. Raw Message Subscriber (`subscription-raw-message.js`) +### 6. Raw Message Subscriber (`subscription-raw-message.cjs`) **Purpose**: Shows how to receive raw binary data. @@ -154,10 +154,10 @@ The `subscriber/` directory contains examples of nodes that subscribe to topics: - **Topic**: `chatter` - **Functionality**: Receives raw Buffer data and converts to UTF-8 string - **Features**: Uses `{ isRaw: true }` option -- **Run Command**: `node subscriber/subscription-raw-message.js` -- **Pair**: Works with `publisher-raw-message.js` +- **Run Command**: `node subscriber/subscription-raw-message.cjs` +- **Pair**: Works with `publisher-raw-message.cjs` -### 7. Service Event Subscriber (`subscription-service-event-example.js`) +### 7. Service Event Subscriber (`subscription-service-event-example.cjs`) **Purpose**: Demonstrates subscribing to service events. @@ -165,9 +165,9 @@ The `subscriber/` directory contains examples of nodes that subscribe to topics: - **Topic**: `/add_two_ints/_service_event` - **Functionality**: Monitors service call events for the AddTwoInts service - **Features**: ROS 2 service introspection capabilities -- **Run Command**: `node subscriber/subscription-service-event-example.js` +- **Run Command**: `node subscriber/subscription-service-event-example.cjs` -### 8. Serialization Modes Subscriber (`subscription-serialization-modes-example.js`) +### 8. Serialization Modes Subscriber (`subscription-serialization-modes-example.cjs`) **Purpose**: Demonstrates different serialization modes for message handling. @@ -175,9 +175,9 @@ The `subscriber/` directory contains examples of nodes that subscribe to topics: - **Topic**: `scan` - **Functionality**: Shows how 'default', 'plain', and 'json' modes affect message serialization - **Features**: Message serialization control for web applications and JSON compatibility -- **Run Command**: `node subscriber/subscription-serialization-modes-example.js` +- **Run Command**: `node subscriber/subscription-serialization-modes-example.cjs` -### 9. JSON Utilities Subscriber (`subscription-json-utilities-example.js`) +### 9. JSON Utilities Subscriber (`subscription-json-utilities-example.cjs`) **Purpose**: Demonstrates manual message conversion utilities. @@ -185,9 +185,9 @@ The `subscriber/` directory contains examples of nodes that subscribe to topics: - **Topic**: `scan` - **Functionality**: Shows how to use toJSONSafe and toJSONString utilities for manual conversion - **Features**: Manual conversion of TypedArrays, BigInt, and special values for JSON serialization -- **Run Command**: `node subscriber/subscription-json-utilities-example.js` +- **Run Command**: `node subscriber/subscription-json-utilities-example.cjs` -### 10. Observable Subscriber (`subscription-observable-example.js`) +### 10. Observable Subscriber (`subscription-observable-example.cjs`) **Purpose**: Demonstrates RxJS Observable subscriptions for reactive programming. @@ -199,20 +199,20 @@ The `subscriber/` directory contains examples of nodes that subscribe to topics: - Message transformation with `map()` - Content filtering with `filter()` - Batching with `bufferCount()` -- **Run Command**: `node subscriber/subscription-observable-example.js` -- **Pair**: Works with `publisher-example.js` +- **Run Command**: `node subscriber/subscription-observable-example.cjs` +- **Pair**: Works with `publisher-example.cjs` ## Validator Example The `validator/` directory contains validation utilities: -### Validator (`validator-example.js`) +### Validator (`validator-example.cjs`) **Purpose**: Demonstrates ROS 2 name validation functions. - **Functionality**: Validates topic names, node names, namespaces, and full topic names - **Features**: Uses rclnodejs validator utilities -- **Run Command**: `node validator/validator-example.js` +- **Run Command**: `node validator/validator-example.cjs` ## Paired Examples @@ -220,13 +220,13 @@ Several examples work together to demonstrate complete communication: | Publisher | Subscriber | Description | | ------------------------------------- | ---------------------------------------- | ------------------------------- | -| `publisher-example.js` | `subscription-example.js` | Basic string messaging | -| `publisher-content-filter-example.js` | `subscription-content-filter-example.js` | Temperature data with filtering | -| `publisher-message-example.js` | `subscription-message-example.js` | Complex structured messages | -| `publisher-multiarray-example.js` | `subscription-multiarray-example.js` | Multi-dimensional array data | -| `publisher-qos-example.js` | `subscription-qos-example.js` | QoS configuration | -| `publisher-raw-message.js` | `subscription-raw-message.js` | Raw binary data | -| `publisher-example.js` | `subscription-observable-example.js` | RxJS Observable subscription | +| `publisher-example.cjs` | `subscription-example.cjs` | Basic string messaging | +| `publisher-content-filter-example.cjs` | `subscription-content-filter-example.cjs` | Temperature data with filtering | +| `publisher-message-example.cjs` | `subscription-message-example.cjs` | Complex structured messages | +| `publisher-multiarray-example.cjs` | `subscription-multiarray-example.cjs` | Multi-dimensional array data | +| `publisher-qos-example.cjs` | `subscription-qos-example.cjs` | QoS configuration | +| `publisher-raw-message.cjs` | `subscription-raw-message.cjs` | Raw binary data | +| `publisher-example.cjs` | `subscription-observable-example.cjs` | RxJS Observable subscription | ## How to Run Examples @@ -234,11 +234,11 @@ Several examples work together to demonstrate complete communication: 2. **Navigate**: Change to the example/topics directory 3. **Run Publisher**: Start the publisher in one terminal ```bash - node publisher/publisher-example.js + node publisher/publisher-example.cjs ``` 4. **Run Subscriber**: Start the corresponding subscriber in another terminal ```bash - node subscriber/subscription-example.js + node subscriber/subscription-example.cjs ``` ## Key Concepts Demonstrated diff --git a/example/topics/publisher/publisher-content-filter-example.js b/example/topics/publisher/publisher-content-filter-example.cjs similarity index 95% rename from example/topics/publisher/publisher-content-filter-example.js rename to example/topics/publisher/publisher-content-filter-example.cjs index d759189a..0866aa35 100644 --- a/example/topics/publisher/publisher-content-filter-example.js +++ b/example/topics/publisher/publisher-content-filter-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/topics/publisher/publisher-example.js b/example/topics/publisher/publisher-example.cjs similarity index 94% rename from example/topics/publisher/publisher-example.js rename to example/topics/publisher/publisher-example.cjs index 0d7f3535..e6b1e252 100644 --- a/example/topics/publisher/publisher-example.js +++ b/example/topics/publisher/publisher-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs.init().then(() => { const node = rclnodejs.createNode('publisher_example_node'); diff --git a/example/topics/publisher/publisher-example.mjs b/example/topics/publisher/publisher-example.mjs new file mode 100644 index 00000000..d492cf87 --- /dev/null +++ b/example/topics/publisher/publisher-example.mjs @@ -0,0 +1,34 @@ +// Copyright (c) 2026 RobotWebTools Contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ESM variant of publisher-example.js. +// +// From an installed package you would write `import rclnodejs from 'rclnodejs'`; +// run from this checkout we import the source entry point directly. Note the +// top-level `await` — an ES module feature that removes the `.then()` nesting. + +import rclnodejs from '../../../index.js'; + +await rclnodejs.init(); + +const node = rclnodejs.createNode('publisher_example_node'); +const publisher = node.createPublisher('std_msgs/msg/String', 'topic'); + +let counter = 0; +setInterval(() => { + console.log(`Publishing message: Hello ROS ${counter}`); + publisher.publish(`Hello ROS ${counter++}`); +}, 1000); + +rclnodejs.spin(node); diff --git a/example/topics/publisher/publisher-message-example.js b/example/topics/publisher/publisher-message-example.cjs similarity index 95% rename from example/topics/publisher/publisher-message-example.js rename to example/topics/publisher/publisher-message-example.cjs index 8036b237..5d497c5f 100644 --- a/example/topics/publisher/publisher-message-example.js +++ b/example/topics/publisher/publisher-message-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs .init() diff --git a/example/topics/publisher/publisher-multiarray-example.js b/example/topics/publisher/publisher-multiarray-example.cjs similarity index 96% rename from example/topics/publisher/publisher-multiarray-example.js rename to example/topics/publisher/publisher-multiarray-example.cjs index b1d4f100..cb1abf10 100644 --- a/example/topics/publisher/publisher-multiarray-example.js +++ b/example/topics/publisher/publisher-multiarray-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs .init() diff --git a/example/topics/publisher/publisher-qos-example.js b/example/topics/publisher/publisher-qos-example.cjs similarity index 96% rename from example/topics/publisher/publisher-qos-example.js rename to example/topics/publisher/publisher-qos-example.cjs index 481dd370..72d3a915 100644 --- a/example/topics/publisher/publisher-qos-example.js +++ b/example/topics/publisher/publisher-qos-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const { QoS } = rclnodejs; rclnodejs.init().then(() => { diff --git a/example/topics/publisher/publisher-raw-message.js b/example/topics/publisher/publisher-raw-message.cjs similarity index 95% rename from example/topics/publisher/publisher-raw-message.js rename to example/topics/publisher/publisher-raw-message.cjs index aac8df9d..b4c93126 100644 --- a/example/topics/publisher/publisher-raw-message.js +++ b/example/topics/publisher/publisher-raw-message.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs .init() diff --git a/example/topics/publisher/publisher-validation-example.js b/example/topics/publisher/publisher-validation-example.cjs similarity index 95% rename from example/topics/publisher/publisher-validation-example.js rename to example/topics/publisher/publisher-validation-example.cjs index 41265a6e..c741c73b 100644 --- a/example/topics/publisher/publisher-validation-example.js +++ b/example/topics/publisher/publisher-validation-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs.init().then(() => { const node = rclnodejs.createNode('publisher_validation_example_node'); diff --git a/example/topics/subscriber/subscription-content-filter-example.js b/example/topics/subscriber/subscription-content-filter-example.cjs similarity index 98% rename from example/topics/subscriber/subscription-content-filter-example.js rename to example/topics/subscriber/subscription-content-filter-example.cjs index 24250f85..1802c59c 100644 --- a/example/topics/subscriber/subscription-content-filter-example.js +++ b/example/topics/subscriber/subscription-content-filter-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; /** * This example demonstrates the use of content-filtering diff --git a/example/topics/subscriber/subscription-example.js b/example/topics/subscriber/subscription-example.cjs similarity index 94% rename from example/topics/subscriber/subscription-example.js rename to example/topics/subscriber/subscription-example.cjs index 2785b392..02d55e1e 100644 --- a/example/topics/subscriber/subscription-example.js +++ b/example/topics/subscriber/subscription-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs.init().then(() => { const node = rclnodejs.createNode('subscription_example_node'); diff --git a/example/topics/subscriber/subscription-example.mjs b/example/topics/subscriber/subscription-example.mjs new file mode 100644 index 00000000..ac0783fc --- /dev/null +++ b/example/topics/subscriber/subscription-example.mjs @@ -0,0 +1,30 @@ +// Copyright (c) 2026 RobotWebTools Contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ESM variant of subscription-example.js. +// +// From an installed package you would write `import rclnodejs from 'rclnodejs'`; +// run from this checkout we import the source entry point directly. + +import rclnodejs from '../../../index.js'; + +await rclnodejs.init(); + +const node = rclnodejs.createNode('subscription_example_node'); + +node.createSubscription('std_msgs/msg/String', 'topic', (msg) => { + console.log(`Received message: ${typeof msg}`, msg); +}); + +rclnodejs.spin(node); diff --git a/example/topics/subscriber/subscription-json-utilities-example.js b/example/topics/subscriber/subscription-json-utilities-example.cjs similarity index 96% rename from example/topics/subscriber/subscription-json-utilities-example.js rename to example/topics/subscriber/subscription-json-utilities-example.cjs index 4efad05f..06fc2986 100644 --- a/example/topics/subscriber/subscription-json-utilities-example.js +++ b/example/topics/subscriber/subscription-json-utilities-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; /** * This example demonstrates the JSON utility functions for manual message conversion. diff --git a/example/topics/subscriber/subscription-message-example.js b/example/topics/subscriber/subscription-message-example.cjs similarity index 94% rename from example/topics/subscriber/subscription-message-example.js rename to example/topics/subscriber/subscription-message-example.cjs index aaf62e19..e11096ac 100644 --- a/example/topics/subscriber/subscription-message-example.js +++ b/example/topics/subscriber/subscription-message-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs .init() diff --git a/example/topics/subscriber/subscription-multiarray-example.js b/example/topics/subscriber/subscription-multiarray-example.cjs similarity index 97% rename from example/topics/subscriber/subscription-multiarray-example.js rename to example/topics/subscriber/subscription-multiarray-example.cjs index ceefb7b5..ffe8d350 100644 --- a/example/topics/subscriber/subscription-multiarray-example.js +++ b/example/topics/subscriber/subscription-multiarray-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs .init() diff --git a/example/topics/subscriber/subscription-observable-example.js b/example/topics/subscriber/subscription-observable-example.cjs similarity index 97% rename from example/topics/subscriber/subscription-observable-example.js rename to example/topics/subscriber/subscription-observable-example.cjs index 3dccac3e..a3f8bf56 100644 --- a/example/topics/subscriber/subscription-observable-example.js +++ b/example/topics/subscriber/subscription-observable-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const { throttleTime, map, filter, bufferCount } = require('rxjs'); async function main() { diff --git a/example/topics/subscriber/subscription-qos-example.js b/example/topics/subscriber/subscription-qos-example.cjs similarity index 94% rename from example/topics/subscriber/subscription-qos-example.js rename to example/topics/subscriber/subscription-qos-example.cjs index dee83715..f479be7f 100644 --- a/example/topics/subscriber/subscription-qos-example.js +++ b/example/topics/subscriber/subscription-qos-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const { QoS } = rclnodejs; rclnodejs.init().then(() => { diff --git a/example/topics/subscriber/subscription-raw-message.js b/example/topics/subscriber/subscription-raw-message.cjs similarity index 94% rename from example/topics/subscriber/subscription-raw-message.js rename to example/topics/subscriber/subscription-raw-message.cjs index 711c0de4..7e753649 100644 --- a/example/topics/subscriber/subscription-raw-message.js +++ b/example/topics/subscriber/subscription-raw-message.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; rclnodejs.init().then(() => { const node = rclnodejs.createNode('subscription_message_example_node'); diff --git a/example/topics/subscriber/subscription-serialization-modes-example.js b/example/topics/subscriber/subscription-serialization-modes-example.cjs similarity index 97% rename from example/topics/subscriber/subscription-serialization-modes-example.js rename to example/topics/subscriber/subscription-serialization-modes-example.cjs index 6bfe5dc6..ba622598 100644 --- a/example/topics/subscriber/subscription-serialization-modes-example.js +++ b/example/topics/subscriber/subscription-serialization-modes-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; /** * This example demonstrates the use of serialization modes for subscriptions. diff --git a/example/topics/subscriber/subscription-service-event-example.js b/example/topics/subscriber/subscription-service-event-example.cjs similarity index 94% rename from example/topics/subscriber/subscription-service-event-example.js rename to example/topics/subscriber/subscription-service-event-example.cjs index 91757fb4..b52832fec 100644 --- a/example/topics/subscriber/subscription-service-event-example.js +++ b/example/topics/subscriber/subscription-service-event-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; async function main() { await rclnodejs.init(); diff --git a/example/topics/validator/validator-example.js b/example/topics/validator/validator-example.cjs similarity index 95% rename from example/topics/validator/validator-example.js rename to example/topics/validator/validator-example.cjs index 06b97a45..da25479f 100644 --- a/example/topics/validator/validator-example.js +++ b/example/topics/validator/validator-example.cjs @@ -14,7 +14,7 @@ 'use strict'; -const rclnodejs = require('../../../index.js'); +const rclnodejs = require('../../../index.js').default; const { validator } = rclnodejs; console.log( diff --git a/package.json b/package.json index 6cbaf45a..ad5b8615 100644 --- a/package.json +++ b/package.json @@ -3,22 +3,33 @@ "version": "2.0.0", "description": "ROS2.0 JavaScript client with Node.js", "type": "module", - "main": "index.js", + "main": "./dist/index.cjs", + "module": "./dist/index.js", "types": "types/index.d.ts", "exports": { ".": { "types": "./types/index.d.ts", - "default": "./index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "default": "./dist/index.js" }, "./web": { "types": "./web/index.d.ts", - "default": "./web/index.js" + "import": "./dist/web.js", + "require": "./dist/web.cjs", + "default": "./dist/web.js" }, "./web/server": { "types": "./lib/runtime/index.d.ts", - "default": "./lib/runtime/index.js" + "import": "./dist/server.js", + "require": "./dist/server.cjs", + "default": "./dist/server.js" + }, + "./rosocket": { + "import": "./dist/rosocket.js", + "require": "./dist/rosocket.cjs", + "default": "./dist/rosocket.js" }, - "./rosocket": "./rosocket/index.js", "./lib/*": "./lib/*.js", "./lib/*.js": "./lib/*.js", "./package.json": "./package.json" @@ -34,6 +45,7 @@ "scripts": { "build": "node-gyp -j 16 build", "build:dev": "node-gyp -j 16 build --debug", + "build:dist": "tsup", "rebuild": "npm run clean && node-gyp -j 16 rebuild", "rebuild:dev": "npm run clean && node-gyp -j 16 rebuild --debug", "generate-messages": "node scripts/generate_messages.cjs", @@ -51,7 +63,8 @@ "test-idl": "node --expose-gc ./scripts/run_test.cjs --idl", "lint": "eslint && node ./scripts/cpplint.cjs", "test:asan": "bash scripts/run_asan_test.sh", - "format": "clang-format -i -style=file ./src/*.cpp ./src/*.h && npx --yes prettier --write \"{lib,rosidl_gen,rostsd_gen,rosidl_parser,types,example,test,scripts,benchmark,rostsd_gen}/**/*.{js,cjs,md,ts}\" ./*.{js,md,ts}", + "format": "clang-format -i -style=file ./src/*.cpp ./src/*.h && npx --yes prettier --write \"{lib,rosidl_gen,rostsd_gen,rosidl_parser,types,example,test,scripts,benchmark,rostsd_gen}/**/*.{js,cjs,mjs,md,ts}\" ./*.{js,md,ts}", + "prepack": "npm run build:dist", "prepare": "husky", "coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", "prebuild:node": "prebuildify --napi --strip --name node --target 20.20.2", @@ -99,6 +112,7 @@ "sinon": "^22.0.0", "tree-kill": "^1.2.2", "tsd": "^0.33.0", + "tsup": "^8.5.1", "typescript": "^6.0.2" }, "dependencies": { diff --git a/scripts/npm-pack.sh b/scripts/npm-pack.sh index ae924897..7ce5314b 100755 --- a/scripts/npm-pack.sh +++ b/scripts/npm-pack.sh @@ -31,8 +31,11 @@ FILENAME=`npm pack --ignore-scripts $RAWMODULEDIR | awk '{for(i=1; i<=NF; i++) i TARFILENAME="$WORKDIR/$FILENAME" popd > /dev/null -mkdir -p dist -cp -f $TARFILENAME ./dist/ +# Output the packed tarball to ./pack (NOT ./dist): since the ESM/CJS dual build +# (tsup) now owns ./dist and that directory is published to npm, dropping the +# tarball there would ship pack artifacts inside the package. +mkdir -p pack +cp -f $TARFILENAME ./pack/ rm -rf $WORKDIR -echo "Generated ./dist/$FILENAME" +echo "Generated ./pack/$FILENAME" diff --git a/tsup.config.js b/tsup.config.js new file mode 100644 index 00000000..264c79ee --- /dev/null +++ b/tsup.config.js @@ -0,0 +1,91 @@ +// Copyright (c) 2026 RobotWebTools Contributors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// tsup configuration for the dual ESM + CommonJS build. +// +// The authored source is native ESM (package.json "type": "module"). External +// consumers that use `require('rclnodejs')` still need a CommonJS build, so +// tsup emits both formats into a FLAT dist/ (depth-1) layout: +// +// dist/index.js dist/index.cjs (exports ".") +// dist/web.js dist/web.cjs (exports "./web") +// dist/server.js dist/server.cjs (exports "./web/server") +// dist/rosocket.js dist/rosocket.cjs (exports "./rosocket") +// +// The flat depth-1 layout is deliberate: the native loader resolves the +// prebuilt binaries and the `.node` addon via `path.join(__dirname, '..', ...)` +// and `bindings()` walking up to package.json. With every bundle one level +// under the package root, those `..`-relative paths stay correct. + +import path from 'path'; +import { fileURLToPath } from 'url'; +import { defineConfig } from 'tsup'; + +const ROOT = path.dirname(fileURLToPath(import.meta.url)); +const DIST = path.join(ROOT, 'dist'); + +// The generator/parser trees (rosidl_gen/*, rostsd_gen/*) are CommonJS islands +// that resolve their own template and `generated/` paths from their real +// on-disk location. They must NOT be bundled. esbuild would otherwise inline +// them and break `__dirname`-relative template lookups. Keep them external and +// rewrite each specifier to a path that is correct relative to the flat dist/ +// output (e.g. `./rosidl_gen/index.cjs` -> `../rosidl_gen/index.cjs`). +const externalizeIslands = { + name: 'externalize-cjs-islands', + setup(build) { + build.onResolve({ filter: /[\\/](rosidl_gen|rostsd_gen)[\\/]/ }, (args) => { + const abs = path.resolve(args.resolveDir, args.path); + let rel = path.relative(DIST, abs).split(path.sep).join('/'); + if (!rel.startsWith('.')) { + rel = './' + rel; + } + return { path: rel, external: true }; + }); + }, +}; + +export default defineConfig({ + entry: { + index: 'index.js', + web: 'web/index.js', + server: 'lib/runtime/index.js', + rosocket: 'rosocket/index.js', + }, + outDir: 'dist', + format: ['esm', 'cjs'], + platform: 'node', + target: 'node20', + // One self-contained file per entry/format keeps the dist/ layout flat and + // the externalized island paths consistent. + splitting: false, + sourcemap: true, + clean: true, + // Type declarations are handled separately; do not emit here. + dts: false, + // Provide import.meta.url in the CJS output (and __dirname in ESM) so the + // native loader's path resolution works in both formats. + shims: true, + esbuildPlugins: [externalizeIslands], + esbuildOptions(options, context) { + // Hoist a lone `export default` to `module.exports` in the CJS output so + // that `const rclnodejs = require('rclnodejs')` keeps working with no + // `.default` indirection. Only collapses when `default` is the sole export + // (the root entry); named-only entries such as ./web are left untouched. + if (context.format === 'cjs') { + options.footer = { + js: 'if (module.exports && module.exports.default !== void 0 && Object.keys(module.exports).every(function (k) { return k === "default" || k === "__esModule"; })) { module.exports = module.exports.default; }', + }; + } + }, +}); diff --git a/web/client.js b/web/client.js index c6d756bf..3aac6cb8 100644 --- a/web/client.js +++ b/web/client.js @@ -29,13 +29,23 @@ // - { http, ws } → explicit endpoint pair. let WS = globalThis.WebSocket; -if (!WS) { +let _wsResolved = !!WS; + +// Resolve a Node WebSocket implementation lazily. This avoids a top-level +// `await import('ws')`, which is unavailable in the CommonJS build. In browsers +// `globalThis.WebSocket` is used and the `ws` package is never touched. +async function _ensureWS() { + if (_wsResolved) { + return WS; + } + _wsResolved = true; try { const wsModule = await import('ws'); WS = wsModule.WebSocket || wsModule.default; } catch { // No WebSocket implementation available in this environment. } + return WS; } // Frame ids are UUIDs so they never collide across sessions or modules @@ -71,7 +81,8 @@ class _WsLink { this._closed = false; } - connect() { + async connect() { + await _ensureWS(); return new Promise((resolve, reject) => { if (!WS) { return reject(