@companion-module/tools 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,453 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ /**
4
+ * The platforms that every native addon in a module package must have a binary for.
5
+ * Note: darwin-x64 is expected to be dropped in the future
6
+ */
7
+ export const REQUIRED_PREBUILD_TARGETS = [
8
+ 'linux-x64',
9
+ 'linux-arm64',
10
+ 'win32-x64',
11
+ 'win32-arm64',
12
+ 'darwin-x64',
13
+ 'darwin-arm64',
14
+ ];
15
+ const KNOWN_PLATFORMS = new Set(['linux', 'win32', 'darwin', 'freebsd', 'openbsd', 'android', 'sunos', 'aix', 'netbsd']);
16
+ // Tokens which appear at the end of the names of per-platform packages, eg `@napi-rs/canvas-linux-x64-gnu`
17
+ const PLATFORM_PACKAGE_SUFFIX_TOKENS = new Set([
18
+ ...KNOWN_PLATFORMS,
19
+ 'windows',
20
+ 'macos',
21
+ 'x64',
22
+ 'x86_64',
23
+ 'arm64',
24
+ 'aarch64',
25
+ 'ia32',
26
+ 'x86',
27
+ 'arm',
28
+ 'armv7',
29
+ 'gnu',
30
+ 'musl',
31
+ 'msvc',
32
+ 'gnueabihf',
33
+ 'musleabihf',
34
+ 'eabi',
35
+ 'gnueabi',
36
+ 'musleabi',
37
+ 'gnux32',
38
+ 'mingw',
39
+ 'riscv64',
40
+ 'ppc64',
41
+ 'ppc64le',
42
+ 's390x',
43
+ 'loong64',
44
+ 'mips64el',
45
+ 'wasm32',
46
+ 'wasi',
47
+ 'universal',
48
+ ]);
49
+ const HEADER_READ_SIZE = 4096;
50
+ export class PrebuildCoverageError extends Error {
51
+ constructor(message) {
52
+ super(message);
53
+ this.name = 'PrebuildCoverageError';
54
+ }
55
+ }
56
+ function elfArch(machine) {
57
+ switch (machine) {
58
+ case 0x3e:
59
+ return 'x64';
60
+ case 0xb7:
61
+ return 'arm64';
62
+ case 0x03:
63
+ return 'ia32';
64
+ case 0x28:
65
+ return 'arm';
66
+ default:
67
+ return `unknown(0x${machine.toString(16)})`;
68
+ }
69
+ }
70
+ function peArch(machine) {
71
+ switch (machine) {
72
+ case 0x8664:
73
+ return 'x64';
74
+ case 0xaa64:
75
+ return 'arm64';
76
+ case 0x014c:
77
+ return 'ia32';
78
+ case 0x01c4:
79
+ return 'arm';
80
+ default:
81
+ return `unknown(0x${machine.toString(16)})`;
82
+ }
83
+ }
84
+ function machoArch(cpuType) {
85
+ switch (cpuType) {
86
+ case 0x01000007:
87
+ return 'x64';
88
+ case 0x0100000c:
89
+ return 'arm64';
90
+ case 0x00000007:
91
+ return 'ia32';
92
+ case 0x0000000c:
93
+ return 'arm';
94
+ default:
95
+ return `unknown(0x${cpuType.toString(16)})`;
96
+ }
97
+ }
98
+ /**
99
+ * Determine the platform(s) and arch(s) a native binary was compiled for, by inspecting its header.
100
+ * Returns targets in the form `platform-arch`, or null if the file is not a recognised binary format
101
+ */
102
+ export async function detectBinaryTargets(filePath) {
103
+ const handle = await fs.open(filePath, 'r');
104
+ try {
105
+ const readAt = async (position, length) => {
106
+ const buffer = Buffer.alloc(length);
107
+ const { bytesRead } = await handle.read(buffer, 0, length, position);
108
+ return buffer.subarray(0, bytesRead);
109
+ };
110
+ const header = await readAt(0, HEADER_READ_SIZE);
111
+ if (header.length < 8)
112
+ return null;
113
+ // ELF (linux)
114
+ if (header[0] === 0x7f && header[1] === 0x45 && header[2] === 0x4c && header[3] === 0x46) {
115
+ if (header.length < 20)
116
+ return null;
117
+ const littleEndian = header[5] !== 2;
118
+ const machine = littleEndian ? header.readUInt16LE(18) : header.readUInt16BE(18);
119
+ const osAbi = header[7];
120
+ const platform = osAbi === 9 ? 'freebsd' : osAbi === 12 ? 'openbsd' : osAbi === 2 ? 'netbsd' : 'linux';
121
+ return [`${platform}-${elfArch(machine)}`];
122
+ }
123
+ // PE (windows)
124
+ if (header[0] === 0x4d && header[1] === 0x5a) {
125
+ if (header.length < 0x40)
126
+ return null;
127
+ const peOffset = header.readUInt32LE(0x3c);
128
+ const peHeader = peOffset + 6 <= header.length ? header.subarray(peOffset) : await readAt(peOffset, 6);
129
+ if (peHeader.length < 6 || peHeader.readUInt32BE(0) !== 0x50450000)
130
+ return null;
131
+ return [`win32-${peArch(peHeader.readUInt16LE(4))}`];
132
+ }
133
+ // Mach-O (macos), single architecture
134
+ const magicLE = header.readUInt32LE(0);
135
+ if (magicLE === 0xfeedfacf || magicLE === 0xfeedface) {
136
+ return [`darwin-${machoArch(header.readUInt32LE(4))}`];
137
+ }
138
+ // Mach-O (macos), universal/fat binary containing multiple architectures
139
+ const magicBE = header.readUInt32BE(0);
140
+ if (magicBE === 0xcafebabe || magicBE === 0xcafebabf) {
141
+ const entrySize = magicBE === 0xcafebabf ? 32 : 20;
142
+ const count = header.readUInt32BE(4);
143
+ const entries = 8 + count * entrySize <= header.length ? header : await readAt(0, 8 + count * entrySize);
144
+ const targets = [];
145
+ for (let i = 0; i < count; i++) {
146
+ const offset = 8 + i * entrySize;
147
+ if (offset + 4 > entries.length)
148
+ return null;
149
+ targets.push(`darwin-${machoArch(entries.readUInt32BE(offset))}`);
150
+ }
151
+ return targets.length ? targets : null;
152
+ }
153
+ return null;
154
+ }
155
+ finally {
156
+ await handle.close();
157
+ }
158
+ }
159
+ async function findNativeFiles(rootDir) {
160
+ const binaries = [];
161
+ const packageJsons = [];
162
+ const walk = async (dir) => {
163
+ const entries = await fs.readdir(dir, { withFileTypes: true });
164
+ for (const entry of entries) {
165
+ const fullPath = path.join(dir, entry.name);
166
+ if (entry.isDirectory()) {
167
+ await walk(fullPath);
168
+ }
169
+ else if (entry.isFile() && entry.name.endsWith('.node')) {
170
+ binaries.push(fullPath);
171
+ }
172
+ else if (entry.isFile() && entry.name === 'package.json') {
173
+ packageJsons.push(fullPath);
174
+ }
175
+ }
176
+ };
177
+ await walk(rootDir);
178
+ return { binaries: binaries.sort(), packageJsons: packageJsons.sort() };
179
+ }
180
+ async function readPackageJson(dir, cache) {
181
+ let json = cache.get(dir);
182
+ if (json === undefined) {
183
+ try {
184
+ json = JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf8'));
185
+ }
186
+ catch {
187
+ json = null;
188
+ }
189
+ cache.set(dir, json);
190
+ }
191
+ return json;
192
+ }
193
+ /**
194
+ * Find the nearest package.json for a file, without leaving the root dir
195
+ */
196
+ async function findOwningPackageJson(rootDir, filePath, cache) {
197
+ let dir = path.dirname(filePath);
198
+ while (true) {
199
+ const json = await readPackageJson(dir, cache);
200
+ if (json)
201
+ return json;
202
+ if (path.resolve(dir) === path.resolve(rootDir))
203
+ return null;
204
+ const parent = path.dirname(dir);
205
+ if (parent === dir)
206
+ return null;
207
+ dir = parent;
208
+ }
209
+ }
210
+ const PLATFORM_IN_NAME_REGEX = new RegExp(`(^|[-.])(${Array.from(KNOWN_PLATFORMS).join('|')})([-.+]|$)`);
211
+ /**
212
+ * Strip the platform portion from a prebuild folder or file name.
213
+ * eg `freetype2-linux-x64` -> `freetype2`, `image-rs.linux-x64-gnu` -> `image-rs`, `darwin-x64+arm64` -> ``
214
+ */
215
+ function prebuildNamePrefix(name) {
216
+ const match = PLATFORM_IN_NAME_REGEX.exec(name);
217
+ if (!match)
218
+ return name;
219
+ return name.slice(0, match.index);
220
+ }
221
+ /**
222
+ * Strip the platform portion from the name of a per-platform package.
223
+ * eg `@napi-rs/canvas-linux-x64-gnu` -> `@napi-rs/canvas`, `@esbuild/linux-x64` -> `@esbuild`
224
+ * Returns null if the name does not look like a per-platform package
225
+ */
226
+ function platformPackageBaseName(name) {
227
+ const slash = name.indexOf('/');
228
+ const scope = name.startsWith('@') && slash !== -1 ? name.slice(0, slash + 1) : '';
229
+ const tokens = name.slice(scope.length).split('-');
230
+ let foundPlatform = false;
231
+ while (tokens.length && PLATFORM_PACKAGE_SUFFIX_TOKENS.has(tokens[tokens.length - 1].toLowerCase())) {
232
+ const token = tokens.pop().toLowerCase();
233
+ if (KNOWN_PLATFORMS.has(token) || token === 'windows' || token === 'macos')
234
+ foundPlatform = true;
235
+ }
236
+ if (!foundPlatform)
237
+ return null;
238
+ const rest = tokens.join('-');
239
+ if (rest)
240
+ return scope + rest;
241
+ return scope ? scope.slice(0, -1) : null;
242
+ }
243
+ /**
244
+ * Check a value against a package.json `os` or `cpu` style constraint, which can contain negations like `!win32`
245
+ */
246
+ function matchesConstraint(value, constraint) {
247
+ if (!Array.isArray(constraint) || constraint.length === 0)
248
+ return true;
249
+ const entries = constraint.filter((entry) => typeof entry === 'string');
250
+ if (entries.includes(`!${value}`))
251
+ return false;
252
+ const positives = entries.filter((entry) => !entry.startsWith('!'));
253
+ return positives.length === 0 || positives.includes(value);
254
+ }
255
+ function hasPlatformConstraint(pkgJson) {
256
+ return Array.isArray(pkgJson.os) || Array.isArray(pkgJson.cpu);
257
+ }
258
+ /**
259
+ * Determine the targets a package says it supports, through its `os`, `cpu` and `libc` fields
260
+ */
261
+ function packageJsonTargetFilter(pkgJson) {
262
+ return (target) => {
263
+ const [platform, arch] = target.split('-');
264
+ if (!matchesConstraint(platform, pkgJson.os) || !matchesConstraint(arch, pkgJson.cpu))
265
+ return false;
266
+ // We only support glibc on linux
267
+ if (platform === 'linux' && !matchesConstraint('glibc', pkgJson.libc))
268
+ return false;
269
+ return true;
270
+ };
271
+ }
272
+ const PLATFORM_PACKAGE_HINT = 'This library ships its binaries as separate packages per platform, and only the one(s) for the build machine got installed.';
273
+ const LOOSE_BINARY_HINT = 'This binary is not in a prebuilds folder, so was most likely compiled or downloaded for the build machine only (eg by node-gyp, prebuild-install or node-pre-gyp).';
274
+ /**
275
+ * Find every native addon in a package and check that it has binaries for every required target
276
+ * @param packageDir The root of the package to check
277
+ * @param requiredTargets Override the targets which must be covered
278
+ */
279
+ export async function checkPrebuildCoverage(packageDir, requiredTargets = REQUIRED_PREBUILD_TARGETS) {
280
+ const { binaries, packageJsons } = await findNativeFiles(packageDir);
281
+ const packageJsonCache = new Map();
282
+ const groups = new Map();
283
+ const getGroup = (key, kind) => {
284
+ let group = groups.get(key);
285
+ if (!group) {
286
+ group = { kind, files: [], targets: new Set(), unrecognised: [] };
287
+ groups.set(key, group);
288
+ }
289
+ return group;
290
+ };
291
+ // Libraries which ship per-platform packages (eg napi-rs) list them as optionalDependencies, and the package manager installs only the matching ones
292
+ for (const packageJsonPath of packageJsons) {
293
+ const pkgJson = await readPackageJson(path.dirname(packageJsonPath), packageJsonCache);
294
+ if (!pkgJson)
295
+ continue;
296
+ // Packages it expects to have installed for each platform
297
+ if (pkgJson.optionalDependencies && typeof pkgJson.optionalDependencies === 'object') {
298
+ for (const depName of Object.keys(pkgJson.optionalDependencies)) {
299
+ const baseName = platformPackageBaseName(depName);
300
+ if (baseName)
301
+ getGroup(baseName, 'platform-package');
302
+ }
303
+ }
304
+ // An installed per-platform package
305
+ if (typeof pkgJson.name === 'string' && hasPlatformConstraint(pkgJson)) {
306
+ const baseName = platformPackageBaseName(pkgJson.name);
307
+ if (baseName) {
308
+ const group = getGroup(baseName, 'platform-package');
309
+ const filter = packageJsonTargetFilter(pkgJson);
310
+ for (const target of requiredTargets) {
311
+ if (filter(target))
312
+ group.targets.add(target);
313
+ }
314
+ }
315
+ }
316
+ }
317
+ for (const filePath of binaries) {
318
+ const relativePath = path.relative(packageDir, filePath).split(path.sep).join('/');
319
+ const segments = relativePath.split('/');
320
+ const owningPackage = await findOwningPackageJson(packageDir, filePath, packageJsonCache);
321
+ const platformBaseName = owningPackage && typeof owningPackage.name === 'string' && hasPlatformConstraint(owningPackage)
322
+ ? platformPackageBaseName(owningPackage.name)
323
+ : null;
324
+ let group;
325
+ const prebuildsIndex = segments.lastIndexOf('prebuilds');
326
+ if (platformBaseName) {
327
+ group = getGroup(platformBaseName, 'platform-package');
328
+ }
329
+ else if (prebuildsIndex !== -1 && prebuildsIndex < segments.length - 1) {
330
+ // Some form of `prebuilds/<platform-ish>/<name>.node` or `prebuilds/<platform-ish>.node`
331
+ const prebuildsDir = segments.slice(0, prebuildsIndex + 1).join('/');
332
+ const prefix = prebuildNamePrefix(segments[prebuildsIndex + 1].replace(/\.node$/, ''));
333
+ group = getGroup(prefix ? `${prebuildsDir}/${prefix}` : prebuildsDir, 'prebuilds');
334
+ group.prebuildsDir = prebuildsDir;
335
+ }
336
+ else {
337
+ group = getGroup(relativePath, 'loose');
338
+ }
339
+ // A package which declares it only works on some platforms (eg fsevents) only needs binaries for those
340
+ if (owningPackage && !platformBaseName && hasPlatformConstraint(owningPackage) && !group.filter) {
341
+ group.filter = packageJsonTargetFilter(owningPackage);
342
+ group.note = `${String(owningPackage.name)} declares itself as only supporting some platforms`;
343
+ }
344
+ group.files.push(relativePath);
345
+ const targets = await detectBinaryTargets(filePath);
346
+ if (targets) {
347
+ // The binary header can't easily tell glibc/musl/android apart, so use the naming to exclude the unsupported ones
348
+ const nameTokens = new Set([
349
+ ...segments.slice(prebuildsIndex !== -1 ? prebuildsIndex + 1 : segments.length - 1),
350
+ platformBaseName ? String(owningPackage?.name) : '',
351
+ ]
352
+ .join('/')
353
+ .toLowerCase()
354
+ .split(/[^a-z0-9]+/));
355
+ const isOtherLinux = ['musl', 'android', 'musleabihf', 'musleabi', 'ohos', 'openharmony'].some((token) => nameTokens.has(token));
356
+ for (const target of targets) {
357
+ if (isOtherLinux && target.startsWith('linux-'))
358
+ continue;
359
+ group.targets.add(target);
360
+ }
361
+ }
362
+ else {
363
+ group.unrecognised.push(relativePath);
364
+ }
365
+ }
366
+ const missingFor = (group) => requiredTargets.filter((target) => (!group.filter || group.filter(target)) && !group.targets.has(target));
367
+ // Some libraries have extra addons for specific platforms alongside a complete one (eg node-hid has HID_hidraw for linux)
368
+ // When a sibling in the same prebuilds folder is complete, assume any others are intentionally limited to the platforms they have binaries for
369
+ for (const [key, group] of groups) {
370
+ if (group.kind !== 'prebuilds' || group.filter)
371
+ continue;
372
+ const completeSibling = Array.from(groups).find(([, other]) => other !== group &&
373
+ other.prebuildsDir === group.prebuildsDir &&
374
+ missingFor(other).length === 0 &&
375
+ other.unrecognised.length === 0);
376
+ if (!completeSibling || missingFor(group).length === 0)
377
+ continue;
378
+ const platforms = new Set(Array.from(group.targets).map((target) => target.split('-')[0]));
379
+ group.filter = (target) => platforms.has(target.split('-')[0]);
380
+ group.note = `${key} is assumed to be specific to ${Array.from(platforms).sort().join(', ')}, as ${completeSibling[0]} covers every platform`;
381
+ }
382
+ const addons = [];
383
+ for (const [key, group] of groups) {
384
+ const missing = missingFor(group);
385
+ let hint;
386
+ if (group.kind === 'platform-package')
387
+ hint = PLATFORM_PACKAGE_HINT;
388
+ else if (group.kind === 'loose')
389
+ hint = LOOSE_BINARY_HINT;
390
+ addons.push({
391
+ key,
392
+ kind: group.kind,
393
+ files: group.files,
394
+ targets: Array.from(group.targets).sort(),
395
+ missing,
396
+ unrecognised: group.unrecognised,
397
+ hint: missing.length || group.unrecognised.length ? hint : undefined,
398
+ note: group.note,
399
+ });
400
+ }
401
+ addons.sort((a, b) => a.key.localeCompare(b.key));
402
+ return {
403
+ addons,
404
+ ok: addons.every((addon) => addon.missing.length === 0 && addon.unrecognised.length === 0),
405
+ };
406
+ }
407
+ /**
408
+ * Format a report into human readable lines
409
+ */
410
+ export function formatPrebuildReport(report) {
411
+ const lines = [];
412
+ for (const addon of report.addons) {
413
+ const ok = addon.missing.length === 0 && addon.unrecognised.length === 0;
414
+ lines.push(`${ok ? '✔' : '✘'} ${addon.key}: found ${addon.targets.join(', ') || 'nothing'}`);
415
+ if (addon.note)
416
+ lines.push(` note: ${addon.note}`);
417
+ if (addon.missing.length)
418
+ lines.push(` missing: ${addon.missing.join(', ')}`);
419
+ for (const file of addon.unrecognised)
420
+ lines.push(` unrecognised binary: ${file}`);
421
+ if (!ok) {
422
+ for (const file of addon.files)
423
+ lines.push(` file: ${file}`);
424
+ if (addon.hint)
425
+ lines.push(` ${addon.hint}`);
426
+ }
427
+ }
428
+ return lines;
429
+ }
430
+ /**
431
+ * Print the result of a prebuild check, and throw if any required targets are missing
432
+ */
433
+ export function enforcePrebuildCoverage(report, options = {}) {
434
+ const stdout = options.stdout ?? process.stdout;
435
+ const stderr = options.stderr ?? process.stderr;
436
+ if (report.addons.length === 0) {
437
+ stdout.write('Native prebuilds: none found\n');
438
+ return;
439
+ }
440
+ const lines = formatPrebuildReport(report);
441
+ if (report.ok) {
442
+ stdout.write(`Native prebuilds:\n${lines.map((line) => ` ${line}\n`).join('')}`);
443
+ return;
444
+ }
445
+ const failed = report.addons.filter((addon) => addon.missing.length || addon.unrecognised.length).length;
446
+ const label = options.ignorePrebuildRules ? 'PREBUILD WARNING' : 'PREBUILD ERROR';
447
+ stderr.write(`${label}: ${failed} native ${failed === 1 ? 'library is' : 'libraries are'} missing binaries for required platforms (${REQUIRED_PREBUILD_TARGETS.join(', ')})\n`);
448
+ stderr.write(lines.map((line) => ` ${line}\n`).join(''));
449
+ if (!options.ignorePrebuildRules) {
450
+ stderr.write('Pass --ignore-prebuild-rules to build anyway.\n');
451
+ throw new PrebuildCoverageError(`Prebuild validation failed for ${failed} native ${failed === 1 ? 'library' : 'libraries'}`);
452
+ }
453
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-module/tools",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "exports": {
24
24
  ".": "./dist/index.js",
25
+ "./license": "./dist/license/index.js",
25
26
  "./eslint/config.mjs": "./dist/eslint/config.mjs",
26
27
  "./scripts/*.js": "./dist/scripts/*.js",
27
28
  "./tsconfig/node22/recommended.json": "./tsconfig/node22/recommended.json",
@@ -38,14 +39,14 @@
38
39
  ],
39
40
  "dependencies": {
40
41
  "@eslint/js": "^10.0.1",
41
- "esbuild": "^0.28.1",
42
+ "esbuild": "^0.28.2",
42
43
  "eslint-config-prettier": "^10.1.8",
43
- "eslint-plugin-n": "^18.1.0",
44
+ "eslint-plugin-n": "^18.4.0",
44
45
  "eslint-plugin-prettier": "^5.5.6",
45
46
  "find-up": "^8.0.0",
46
- "semver": "^7.8.4",
47
+ "semver": "^7.8.5",
47
48
  "spdx-expression-parse": "^5.0.0",
48
- "tar": "^7.5.21",
49
+ "tar": "^7.5.22",
49
50
  "zx": "^8.8.5"
50
51
  },
51
52
  "peerDependencies": {
@@ -73,18 +74,18 @@
73
74
  }
74
75
  },
75
76
  "devDependencies": {
76
- "@companion-module/base": "^2.0.4",
77
- "@companion-surface/base": "^1.2.1",
77
+ "@companion-module/base": "^2.1.3",
78
+ "@companion-surface/base": "^1.4.0",
78
79
  "@types/eslint": "^9.6.1",
79
80
  "@types/fs-extra": "^11.0.4",
80
- "@types/node": "^22.19.21",
81
- "@types/semver": "^7.7.1",
81
+ "@types/node": "^22.20.4",
82
+ "@types/semver": "^7.8.0",
82
83
  "@types/spdx-expression-parse": "^4.0.0",
83
- "eslint": "^10.5.0",
84
- "prettier": "^3.8.4",
84
+ "eslint": "^10.11.0",
85
+ "prettier": "^3.9.9",
85
86
  "rimraf": "^6.1.3",
86
87
  "typescript": "^6.0.3",
87
- "typescript-eslint": "^8.61.1"
88
+ "typescript-eslint": "^8.71.0"
88
89
  },
89
- "packageManager": "yarn@4.17.0"
90
+ "packageManager": "yarn@4.18.1"
90
91
  }
@@ -1,26 +1,11 @@
1
1
  {
2
+ "extends": "../shared.json",
2
3
  "compilerOptions": {
3
4
  "module": "node20",
4
5
  "target": "es2025",
5
- "noImplicitAny": true,
6
6
  "moduleResolution": "node16",
7
- "sourceMap": true,
8
- "declaration": false,
9
- "importHelpers": false,
10
- "listFiles": false,
11
- "traceResolution": false,
12
- "pretty": true,
13
- "lib": ["es2025"],
14
- "types": ["node"],
15
- "strict": true,
16
- "forceConsistentCasingInFileNames": true,
17
- "noFallthroughCasesInSwitch": true,
18
- "noImplicitReturns": true,
19
- "noUnusedLocals": true,
20
- "noUnusedParameters": true,
21
- "skipLibCheck": true,
22
- "allowSyntheticDefaultImports": true,
23
- "esModuleInterop": true
24
- },
25
- "compileOnSave": false
7
+ "lib": [
8
+ "es2025"
9
+ ]
10
+ }
26
11
  }