• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobotWebTools / rclnodejs / 27193208698

09 Jun 2026 08:17AM UTC coverage: 91.22% (+5.7%) from 85.523%
27193208698

push

github

web-flow
Phase 2: convert lib/, index.js and tests to native ES modules (#1530)

**Overview:** 189 files changed, +957 / −1162. Converts `lib/`, `index.js`, `bin/`, `rosocket/` and the full test suite to native ES modules; keeps build/codegen tooling as `.cjs`; updates CI to ROS 2 Lyrical (Ubuntu 26.04); and fixes a native-addon debug-build compile error.

### Core ESM conversion
- **`index.js`** — converted to ESM entrypoint (`import`/`export`), module wiring reworked.
- **`lib/**`** — all library modules converted from `require`/`module.exports` to `import`/`export` (node, client, service, publisher, subscription, action/*, clock*, parameter*, logging*, runtime/*, serialization, time*, timer, qos*, utils, validator, native_loader, etc.).
- **`bin/rclnodejs-web.js`**, **`rosocket/index.js`**, **`rosocket/cli.js`** — converted to ESM.

### ESM/CJS boundary fixes (from Copilot review)
- **`lib/native_loader.js`** — load the CommonJS `bindings` helper via `createRequire`'s `require('bindings')` instead of `import bindings from 'bindings'`, preserving CJS caller context for addon resolution.
- **`lib/type_description_service.js`** — corrected sibling import from `'../lib/parameter.js'` to `'./parameter.js'`.

### CommonJS files kept as `.cjs`
- **`rosidl_gen/*.cjs`** (deallocator, idl_generator, index, primitive_types, templates/message-template), **`rostsd_gen/index.cjs`**, **`scripts/ros_distro.cjs`**, **`scripts/run_test.cjs`** — build/codegen tooling stays CJS (loads ESM helpers via supported `require(esm)` / `.default`).
- **`third_party/ref-napi/`** — `lib/ref.js` and `package.json` tweaks for module resolution.

### Tooling / config
- **`package.json`** — `"type": "module"` and script adjustments.
- **`eslint.config.mjs`** — ESM lint rules.
- **`.c8rc.json`** added, **`.nycrc.yml`** removed — switch coverage from nyc to c8.

### CI
- **`.github/workflows/linux-x64-asan-test.yml`**, **`.github/workflows/linux-x64-build-and-test.yml`** — migrated from ROS 2 ... (continued)

2044 of 2402 branches covered (85.1%)

Branch coverage included in aggregate %.

369 of 370 new or added lines in 65 files covered. (99.73%)

942 existing lines in 47 files now uncovered.

16688 of 18133 relevant lines covered (92.03%)

228.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.25
/lib/interface_loader.js
1
// Copyright (c) 2017 Intel Corporation. All rights reserved.
27✔
2
//
27✔
3
// Licensed under the Apache License, Version 2.0 (the "License");
27✔
4
// you may not use this file except in compliance with the License.
27✔
5
// You may obtain a copy of the License at
27✔
6
//
27✔
7
//     http://www.apache.org/licenses/LICENSE-2.0
27✔
8
//
27✔
9
// Unless required by applicable law or agreed to in writing, software
27✔
10
// distributed under the License is distributed on an "AS IS" BASIS,
27✔
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27✔
12
// See the License for the specific language governing permissions and
27✔
13
// limitations under the License.
27✔
14

27✔
15
import path from 'path';
27✔
16
import fs from 'fs';
27✔
17
import { createRequire } from 'module';
27✔
18
import generator from '../rosidl_gen/index.cjs';
27✔
19
import { TypeValidationError, ValidationError } from './errors.js';
27✔
20

27✔
21
const require = createRequire(import.meta.url);
27✔
22

27✔
23
let interfaceLoader = {
27✔
24
  loadInterfaceByObject(obj) {
27✔
25
    //
758✔
26
    // `obj` param structure
758✔
27
    //
758✔
28
    // {
758✔
29
    //   package: 'std_msgs',
758✔
30
    //   type: 'msg',
758✔
31
    //   name: 'String',
758✔
32
    // }
758✔
33
    //
758✔
34
    if (
758✔
35
      typeof obj !== 'object' ||
758✔
36
      !obj ||
758✔
37
      !obj.package ||
758✔
38
      !obj.type ||
758✔
39
      !obj.name
732✔
40
    ) {
758✔
41
      throw new TypeValidationError(
26✔
42
        'interfaceObject',
26✔
43
        obj,
26✔
44
        'object with package, type, and name properties',
26✔
45
        {
26✔
46
          entityType: 'interface loader',
26✔
47
        }
26✔
48
      );
26✔
49
    }
26✔
50
    return this.loadInterface(obj.package, obj.type, obj.name);
732✔
51
  },
27✔
52

27✔
53
  loadInterfaceByString(name) {
27✔
54
    if (typeof name !== 'string') {
12,655!
55
      throw new TypeValidationError('name', name, 'string', {
×
UNCOV
56
        entityType: 'interface loader',
×
UNCOV
57
      });
×
UNCOV
58
    }
×
59

12,655✔
60
    // TODO(Kenny): more checks of the string argument
12,655✔
61
    if (name.includes('/')) {
12,655✔
62
      let [packageName, type, messageName] = name.split('/');
12,602✔
63
      return this.loadInterface(packageName, type, messageName);
12,602✔
64
    }
12,602✔
65

53✔
66
    // Suppose the name is a package, and traverse the path to collect the IDL files.
53✔
67
    let packagePath = path.join(generator.generatedRoot, name);
53✔
68

53✔
69
    let interfaces = fs.readdirSync(packagePath);
53✔
70
    if (interfaces.length > 0) {
12,405✔
71
      return this.loadInterfaceByPath(packagePath, interfaces);
52✔
72
    }
52✔
UNCOV
73

×
74
    throw new ValidationError(
×
UNCOV
75
      'A string argument in expected in "package/type/message" format',
×
UNCOV
76
      {
×
UNCOV
77
        code: 'INVALID_INTERFACE_FORMAT',
×
UNCOV
78
        argumentName: 'name',
×
UNCOV
79
        providedValue: name,
×
UNCOV
80
        expectedType: 'string in "package/type/message" format',
×
UNCOV
81
        entityType: 'interface loader',
×
UNCOV
82
      }
×
UNCOV
83
    );
×
84
  },
27✔
85

27✔
86
  loadInterfaceByPath(packagePath, interfaces) {
27✔
87
    let interfaceInfos = [];
52✔
88

52✔
89
    interfaces.forEach((file) => {
52✔
90
      let results = file.match(/\w+__(\w+)__(\w+).js$/);
1,504✔
91
      let type = results[1];
1,504✔
92
      let name = results[2];
1,504✔
93
      let filePath = path.join(packagePath, file).normalize();
1,504✔
94
      interfaceInfos.push({ name, type, filePath });
1,504✔
95
    });
52✔
96

52✔
97
    let pkg = { srv: {}, msg: {}, action: {} };
52✔
98
    interfaceInfos.forEach((info) => {
52✔
99
      Object.defineProperty(pkg[info.type], info.name, {
1,504✔
100
        value: require(info.filePath),
1,504✔
101
      });
1,504✔
102
    });
52✔
103

52✔
104
    return pkg;
52✔
105
  },
27✔
106

27✔
107
  _isRos2InstallationPath(pkgPath) {
27✔
108
    // Use "which ros2" to dynamically find the ROS2 installation root
18✔
109
    try {
18✔
110
      const whichResult = require('child_process').spawnSync(
18✔
111
        'which',
18✔
112
        ['ros2'],
18✔
113
        {
18✔
114
          encoding: 'utf8',
18✔
115
          timeout: 5000,
18✔
116
        }
18✔
117
      );
18✔
118

18✔
119
      if (whichResult.status === 0 && whichResult.stdout) {
18✔
120
        const ros2BinPath = whichResult.stdout.trim();
18✔
121
        // Get the ROS2 installation root (typically /opt/ros/<distro> or similar)
18✔
122
        const ros2Root = path.dirname(path.dirname(ros2BinPath));
18✔
123

18✔
124
        return pkgPath.includes(ros2Root);
18✔
125
      }
18✔
126
    } catch (err) {
18!
127
      console.error('Error running which ros2:', err.message);
×
UNCOV
128
      // If "which ros2" fails, fall back to hardcoded check
×
129
      return pkgPath.includes('ros2-linux');
×
UNCOV
130
    }
×
UNCOV
131

×
132
    return false;
×
133
  },
27✔
134

27✔
135
  _searchAndGenerateInterface(packageName, type, messageName, filePath) {
27✔
136
    // Check if it's a valid package
18✔
137
    for (const pkgPath of generator.getInstalledPackagePaths()) {
18✔
138
      // We are going to ignore the path where ROS2 is installed.
18✔
139
      if (this._isRos2InstallationPath(pkgPath)) {
18✔
140
        continue;
17✔
141
      }
17✔
142

1✔
143
      // Recursively search for files named messageName.* under pkgPath/
1✔
144
      if (fs.existsSync(pkgPath)) {
1✔
145
        // Recursive function to search for files
1✔
146
        function searchForFile(dir) {
1✔
147
          try {
5✔
148
            const items = fs.readdirSync(dir, { withFileTypes: true });
5✔
149
            for (const item of items) {
5✔
150
              const fullPath = path.join(dir, item.name);
54✔
151

54✔
152
              if (item.isFile()) {
54✔
153
                const baseName = path.parse(item.name).name;
50✔
154
                // Check if the base filename matches messageName
50✔
155
                if (baseName === messageName) {
50✔
156
                  return fullPath;
1✔
157
                }
1✔
158
              } else if (item.isDirectory()) {
54✔
159
                // Recursively search subdirectories
4✔
160
                const result = searchForFile(fullPath);
4✔
161
                if (result) {
4✔
162
                  return result;
1✔
163
                }
1✔
164
              }
4✔
165
            }
54✔
166
          } catch (err) {
5!
UNCOV
167
            // Skip directories we can't read
×
168
            console.error('Error reading directory:', dir, err.message);
×
UNCOV
169
          }
×
170
          return null;
3✔
171
        }
5✔
172

1✔
173
        const foundFilePath = searchForFile(
1✔
174
          path.join(pkgPath, 'share', packageName)
1✔
175
        );
1✔
176

1✔
177
        if (foundFilePath && foundFilePath.length > 0) {
1✔
178
          // Use worker thread to generate interfaces synchronously
1✔
179
          try {
1✔
180
            generator.generateInPathSyncWorker(pkgPath);
1✔
181
            // Now try to load the interface again from the generated files
1✔
182
            if (fs.existsSync(filePath)) {
1✔
183
              return require(filePath);
1✔
184
            }
1✔
185
          } catch (err) {
1!
186
            console.error('Error in interface generation:', err);
×
UNCOV
187
          }
×
188
        }
1✔
189
      }
1✔
190
    }
18✔
191
    throw new ValidationError(
17✔
192
      `The message required does not exist: ${packageName}, ${type}, ${messageName} at ${generator.generatedRoot}`,
17✔
193
      {
17✔
194
        code: 'MESSAGE_NOT_FOUND',
17✔
195
        entityType: 'interface loader',
17✔
196
        details: {
17✔
197
          packageName: packageName,
17✔
198
          type: type,
17✔
199
          messageName: messageName,
17✔
200
          searchPath: generator.generatedRoot,
17✔
201
        },
17✔
202
      }
17✔
203
    );
17✔
204
  },
27✔
205

27✔
206
  loadInterface(packageName, type, messageName) {
27✔
207
    if (arguments.length === 1) {
26,752✔
208
      const type = arguments[0];
13,418✔
209
      if (typeof type === 'function') {
13,418✔
210
        // Already a loaded interface class/constructor (e.g. the value
5✔
211
        // returned by a previous loadInterface call or rclnodejs.require).
5✔
212
        // Every generated interface class exposes a static type() method, so
5✔
213
        // use it to distinguish a genuine interface from an arbitrary
5✔
214
        // function. Return it as-is so loadInterface is idempotent; otherwise
5✔
215
        // fall through to the MESSAGE_NOT_FOUND error below.
5✔
216
        if (typeof type.type === 'function') {
5✔
217
          return type;
3✔
218
        }
3✔
219
      } else if (typeof type === 'object') {
13,418✔
220
        return this.loadInterfaceByObject(type);
758✔
221
      } else if (typeof type === 'string') {
13,413✔
222
        return this.loadInterfaceByString(type);
12,655✔
223
      }
12,655✔
224
      throw new ValidationError(
2✔
225
        `The message required does not exist: ${type}`,
2✔
226
        {
2✔
227
          code: 'MESSAGE_NOT_FOUND',
2✔
228
          entityType: 'interface loader',
2✔
229
          details: { type: type },
2✔
230
        }
2✔
231
      );
2✔
232
    }
2✔
233
    if (packageName && type && messageName) {
26,752✔
234
      let filePath = path.join(
13,322✔
235
        generator.generatedRoot,
13,322✔
236
        packageName,
13,322✔
237
        packageName + '__' + type + '__' + messageName + '.js'
13,322✔
238
      );
13,322✔
239

13,322✔
240
      if (fs.existsSync(filePath)) {
13,322✔
241
        return require(filePath);
13,304✔
242
      } else {
13,322✔
243
        return this._searchAndGenerateInterface(
18✔
244
          packageName,
18✔
245
          type,
18✔
246
          messageName,
18✔
247
          filePath
18✔
248
        );
18✔
249
      }
18✔
250
    }
13,322✔
251
    // We cannot parse `packageName`, `type` and `messageName` from the string passed.
12✔
252
    throw new ValidationError(
12✔
253
      `The message required does not exist: ${packageName}, ${type}, ${messageName} at ${generator.generatedRoot}`,
12✔
254
      {
12✔
255
        code: 'MESSAGE_NOT_FOUND',
12✔
256
        entityType: 'interface loader',
12✔
257
        details: {
12✔
258
          packageName: packageName,
12✔
259
          type: type,
12✔
260
          messageName: messageName,
12✔
261
          searchPath: generator.generatedRoot,
12✔
262
        },
12✔
263
      }
12✔
264
    );
12✔
265
  },
27✔
266
};
27✔
267

27✔
268
export default interfaceLoader;
27✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc