Skip to content

Commit 2805e91

Browse files
CopilotApollon77
andcommitted
Add JSON validation for package.json and io-package.json in base directory
Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com>
1 parent 298377d commit 2805e91

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3512
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
-->
77
## **WORK IN PROGRESS**
88
* (@Apollon77/@copilot) Exclude admin/tsconfig.json from strict JSON validation as it may contain JSON5 syntax
9+
* (@Apollon77/@copilot) Add JSON validation for package.json and io-package.json in base directory
910

1011
## 5.2.1 (2025-11-08)
1112
* (@Apollon77/@copilot) Add validation for JSON files in admin/ and admin/i18n/ directories

build/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './tests';

build/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"use strict";
2+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+
if (k2 === undefined) k2 = k;
4+
var desc = Object.getOwnPropertyDescriptor(m, k);
5+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+
desc = { enumerable: true, get: function() { return m[k]; } };
7+
}
8+
Object.defineProperty(o, k2, desc);
9+
}) : (function(o, m, k, k2) {
10+
if (k2 === undefined) k2 = k;
11+
o[k2] = m[k];
12+
}));
13+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
14+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15+
};
16+
Object.defineProperty(exports, "__esModule", { value: true });
17+
__exportStar(require("./tests"), exports);

build/lib/adapterTools.d.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Loads an adapter's package.json
3+
*
4+
* @param adapterDir The directory the adapter resides in
5+
*/
6+
export declare function loadNpmPackage(adapterDir: string): Record<string, any>;
7+
/**
8+
* Loads an adapter's io-package.json
9+
*
10+
* @param adapterDir The directory the adapter resides in
11+
*/
12+
export declare function loadIoPackage(adapterDir: string): Record<string, any>;
13+
export declare function getAdapterExecutionMode(adapterDir: string): ioBroker.AdapterCommon['mode'];
14+
/**
15+
* Locates an adapter's main file
16+
*
17+
* @param adapterDir The directory the adapter resides in
18+
*/
19+
export declare function locateAdapterMainFile(adapterDir: string): Promise<string>;
20+
/**
21+
* Locates an adapter's config to populate the `adapter.config` object with
22+
*
23+
* @param adapterDir The directory the adapter resides in
24+
*/
25+
export declare function loadAdapterConfig(adapterDir: string): Record<string, any>;
26+
/**
27+
* Loads the adapter's common configuration from `io-package.json`
28+
*
29+
* @param adapterDir The directory the adapter resides in
30+
*/
31+
export declare function loadAdapterCommon(adapterDir: string): Record<string, any>;
32+
/**
33+
* Loads the instanceObjects for an adapter from its `io-package.json`
34+
*
35+
* @param adapterDir The directory the adapter resides in
36+
*/
37+
export declare function loadInstanceObjects(adapterDir: string): ioBroker.Object[];
38+
/** Returns the branded name of "iobroker" */
39+
export declare function getAppName(adapterDir: string): string;
40+
/** Returns the name of an adapter without the prefix */
41+
export declare function getAdapterName(adapterDir: string): string;
42+
/** Returns the full name of an adapter, including the prefix */
43+
export declare function getAdapterFullName(adapterDir: string): string;
44+
/** Reads other ioBroker modules this adapter depends on from io-package.json */
45+
export declare function getAdapterDependencies(adapterDir: string): Record<string, string>;

build/lib/adapterTools.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"use strict";
2+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+
if (k2 === undefined) k2 = k;
4+
var desc = Object.getOwnPropertyDescriptor(m, k);
5+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+
desc = { enumerable: true, get: function() { return m[k]; } };
7+
}
8+
Object.defineProperty(o, k2, desc);
9+
}) : (function(o, m, k, k2) {
10+
if (k2 === undefined) k2 = k;
11+
o[k2] = m[k];
12+
}));
13+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+
Object.defineProperty(o, "default", { enumerable: true, value: v });
15+
}) : function(o, v) {
16+
o["default"] = v;
17+
});
18+
var __importStar = (this && this.__importStar) || (function () {
19+
var ownKeys = function(o) {
20+
ownKeys = Object.getOwnPropertyNames || function (o) {
21+
var ar = [];
22+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23+
return ar;
24+
};
25+
return ownKeys(o);
26+
};
27+
return function (mod) {
28+
if (mod && mod.__esModule) return mod;
29+
var result = {};
30+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31+
__setModuleDefault(result, mod);
32+
return result;
33+
};
34+
})();
35+
var __importDefault = (this && this.__importDefault) || function (mod) {
36+
return (mod && mod.__esModule) ? mod : { "default": mod };
37+
};
38+
Object.defineProperty(exports, "__esModule", { value: true });
39+
exports.loadNpmPackage = loadNpmPackage;
40+
exports.loadIoPackage = loadIoPackage;
41+
exports.getAdapterExecutionMode = getAdapterExecutionMode;
42+
exports.locateAdapterMainFile = locateAdapterMainFile;
43+
exports.loadAdapterConfig = loadAdapterConfig;
44+
exports.loadAdapterCommon = loadAdapterCommon;
45+
exports.loadInstanceObjects = loadInstanceObjects;
46+
exports.getAppName = getAppName;
47+
exports.getAdapterName = getAdapterName;
48+
exports.getAdapterFullName = getAdapterFullName;
49+
exports.getAdapterDependencies = getAdapterDependencies;
50+
// Add debug logging for tests
51+
const typeguards_1 = require("alcalzone-shared/typeguards");
52+
const debug_1 = __importDefault(require("debug"));
53+
const fs_extra_1 = require("fs-extra");
54+
const path = __importStar(require("path"));
55+
const debug = (0, debug_1.default)('testing:unit:adapterTools');
56+
/**
57+
* Loads an adapter's package.json
58+
*
59+
* @param adapterDir The directory the adapter resides in
60+
*/
61+
function loadNpmPackage(adapterDir) {
62+
return require(path.join(adapterDir, 'package.json'));
63+
}
64+
/**
65+
* Loads an adapter's io-package.json
66+
*
67+
* @param adapterDir The directory the adapter resides in
68+
*/
69+
function loadIoPackage(adapterDir) {
70+
return require(path.join(adapterDir, 'io-package.json'));
71+
}
72+
function getAdapterExecutionMode(adapterDir) {
73+
const ioPackage = loadIoPackage(adapterDir);
74+
return ioPackage.common.mode;
75+
}
76+
/**
77+
* Locates an adapter's main file
78+
*
79+
* @param adapterDir The directory the adapter resides in
80+
*/
81+
async function locateAdapterMainFile(adapterDir) {
82+
debug(`locating adapter main file in ${adapterDir}...`);
83+
const ioPackage = loadIoPackage(adapterDir);
84+
const npmPackage = loadNpmPackage(adapterDir);
85+
// First look for the file defined in io-package.json or package.json or use "main.js" as a fallback
86+
const mainFile = typeof ioPackage.common.main === 'string'
87+
? ioPackage.common.main
88+
: typeof npmPackage.main === 'string'
89+
? npmPackage.main
90+
: 'main.js';
91+
let ret = path.join(adapterDir, mainFile);
92+
debug(` => trying ${ret}`);
93+
if (await (0, fs_extra_1.pathExists)(ret)) {
94+
debug(` => found ${mainFile}`);
95+
return ret;
96+
}
97+
// If the specified file doesn't exist and ends with .js, try the .ts equivalent
98+
if (mainFile.endsWith('.js')) {
99+
const tsFile = mainFile.replace(/\.js$/, '.ts');
100+
ret = path.join(adapterDir, tsFile);
101+
debug(` => trying ${ret}`);
102+
if (await (0, fs_extra_1.pathExists)(ret)) {
103+
debug(` => found ${tsFile}`);
104+
return ret;
105+
}
106+
}
107+
// If both don't exist, JS-Controller uses <adapter name>.js as another fallback
108+
ret = path.join(adapterDir, `${ioPackage.common.name}.js`);
109+
debug(` => trying ${ret}`);
110+
if (await (0, fs_extra_1.pathExists)(ret)) {
111+
debug(` => found ${ioPackage.common.name}.js`);
112+
return ret;
113+
}
114+
// Also try <adapter name>.ts as a fallback
115+
ret = path.join(adapterDir, `${ioPackage.common.name}.ts`);
116+
debug(` => trying ${ret}`);
117+
if (await (0, fs_extra_1.pathExists)(ret)) {
118+
debug(` => found ${ioPackage.common.name}.ts`);
119+
return ret;
120+
}
121+
throw new Error(`The adapter main file was not found in ${adapterDir}`);
122+
}
123+
/**
124+
* Locates an adapter's config to populate the `adapter.config` object with
125+
*
126+
* @param adapterDir The directory the adapter resides in
127+
*/
128+
function loadAdapterConfig(adapterDir) {
129+
const ioPackage = loadIoPackage(adapterDir);
130+
return ioPackage.native || {};
131+
}
132+
/**
133+
* Loads the adapter's common configuration from `io-package.json`
134+
*
135+
* @param adapterDir The directory the adapter resides in
136+
*/
137+
function loadAdapterCommon(adapterDir) {
138+
const ioPackage = loadIoPackage(adapterDir);
139+
return ioPackage.common || {};
140+
}
141+
/**
142+
* Loads the instanceObjects for an adapter from its `io-package.json`
143+
*
144+
* @param adapterDir The directory the adapter resides in
145+
*/
146+
function loadInstanceObjects(adapterDir) {
147+
const ioPackage = loadIoPackage(adapterDir);
148+
return ioPackage.instanceObjects || [];
149+
}
150+
/** Returns the branded name of "iobroker" */
151+
function getAppName(adapterDir) {
152+
const npmPackage = loadNpmPackage(adapterDir);
153+
return npmPackage.name.split('.')[0] || 'iobroker';
154+
}
155+
/** Returns the name of an adapter without the prefix */
156+
function getAdapterName(adapterDir) {
157+
const ioPackage = loadIoPackage(adapterDir);
158+
return ioPackage.common.name;
159+
}
160+
/** Returns the full name of an adapter, including the prefix */
161+
function getAdapterFullName(adapterDir) {
162+
const npmPackage = loadNpmPackage(adapterDir);
163+
return npmPackage.name;
164+
}
165+
/** Reads other ioBroker modules this adapter depends on from io-package.json */
166+
function getAdapterDependencies(adapterDir) {
167+
const ioPackage = loadIoPackage(adapterDir);
168+
const ret = {};
169+
if ((0, typeguards_1.isArray)(ioPackage.common.dependencies)) {
170+
for (const dep of ioPackage.common.dependencies) {
171+
if (typeof dep === 'string') {
172+
ret[dep] = 'latest';
173+
}
174+
else if ((0, typeguards_1.isObject)(dep)) {
175+
const key = Object.keys(dep)[0];
176+
if (key) {
177+
ret[key] = dep[key] || 'latest';
178+
}
179+
}
180+
}
181+
}
182+
return ret;
183+
}

build/lib/executeCommand.d.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export interface ExecuteCommandOptions {
2+
/** Whether the executed command should be logged to the stdout. Default: false */
3+
logCommandExecution: boolean;
4+
/** The directory to execute the command in */
5+
cwd: string;
6+
/** Where to redirect the stdin. Default: process.stdin */
7+
stdin: NodeJS.ReadStream;
8+
/** A write stream to redirect the stdout, "ignore" to ignore it or "pipe" to return it as a string. Default: process.stdout */
9+
stdout: NodeJS.WriteStream | 'pipe' | 'ignore';
10+
/** A write stream to redirect the stderr, "ignore" to ignore it or "pipe" to return it as a string. Default: process.stderr */
11+
stderr: NodeJS.WriteStream | 'pipe' | 'ignore';
12+
}
13+
export interface ExecuteCommandResult {
14+
/** The exit code of the spawned process */
15+
exitCode?: number;
16+
/** The signal the process received before termination */
17+
signal?: string;
18+
/** If options.stdout was set to "buffer", this contains the stdout of the spawned process */
19+
stdout?: string;
20+
/** If options.stderr was set to "buffer", this contains the stderr of the spawned process */
21+
stderr?: string;
22+
}
23+
export declare function executeCommand(command: string, options?: Partial<ExecuteCommandOptions>): Promise<ExecuteCommandResult>;
24+
/**
25+
* Executes a command and returns the exit code and (if requested) the stdout
26+
*
27+
* @param command The command to execute
28+
* @param args The command line arguments for the command
29+
* @param options (optional) Some options for the command execution
30+
*/
31+
export declare function executeCommand(command: string, args: string[], options?: Partial<ExecuteCommandOptions>): Promise<ExecuteCommandResult>;

build/lib/executeCommand.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.executeCommand = executeCommand;
4+
const child_process_1 = require("child_process");
5+
const isWindows = /^win/.test(process.platform);
6+
/**
7+
* Executes a command and returns the exit code and (if requested) the stdout
8+
*
9+
* @param command The command to execute
10+
* @param argsOrOptions The command line arguments for the command
11+
* @param options (optional) Some options for the command execution
12+
*/
13+
function executeCommand(command, argsOrOptions, options) {
14+
return new Promise(resolve => {
15+
let args;
16+
if (Array.isArray(argsOrOptions)) {
17+
args = argsOrOptions;
18+
}
19+
else if (argsOrOptions && typeof argsOrOptions === 'object') {
20+
// no args were given
21+
options = argsOrOptions;
22+
}
23+
if (options == null) {
24+
options = {};
25+
}
26+
if (args == null) {
27+
args = [];
28+
}
29+
const spawnOptions = {
30+
stdio: [options.stdin || process.stdin, options.stdout || process.stdout, options.stderr || process.stderr],
31+
windowsHide: true,
32+
};
33+
if (options.cwd != null) {
34+
spawnOptions.cwd = options.cwd;
35+
}
36+
// Fix npm / node executable paths on Windows
37+
if (isWindows) {
38+
if (command === 'npm') {
39+
command += '.cmd';
40+
// Needed since Node.js v18.20.2 and v20.12.2
41+
// https://github.com/nodejs/node/releases/tag/v18.20.2
42+
spawnOptions.shell = true;
43+
}
44+
else if (command === 'node') {
45+
command += '.exe';
46+
}
47+
}
48+
if (options.logCommandExecution == null) {
49+
options.logCommandExecution = false;
50+
}
51+
if (options.logCommandExecution) {
52+
console.log(`executing: ${command} ${args.join(' ')}`);
53+
}
54+
// Now execute the npm process and avoid throwing errors
55+
try {
56+
let bufferedStdout;
57+
let bufferedStderr;
58+
const cmd = (0, child_process_1.spawn)(command, args, spawnOptions).on('close', (code, signal) => {
59+
resolve({
60+
exitCode: code ?? undefined,
61+
signal: signal ?? undefined,
62+
stdout: bufferedStdout,
63+
stderr: bufferedStderr,
64+
});
65+
});
66+
// Capture stdout/stderr if requested
67+
if (options.stdout === 'pipe') {
68+
bufferedStdout = '';
69+
cmd.stdout.on('data', (chunk) => {
70+
if (Buffer.isBuffer(chunk)) {
71+
chunk = chunk.toString('utf8');
72+
}
73+
bufferedStdout += chunk;
74+
});
75+
}
76+
if (options.stderr === 'pipe') {
77+
bufferedStderr = '';
78+
cmd.stderr.on('data', (chunk) => {
79+
if (Buffer.isBuffer(chunk)) {
80+
chunk = chunk.toString('utf8');
81+
}
82+
bufferedStderr += chunk;
83+
});
84+
}
85+
}
86+
catch {
87+
// doesn't matter, we return the exit code in the "close" handler
88+
}
89+
});
90+
}

build/lib/str2regex.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export declare function str2regex(pattern: string): RegExp;

build/lib/str2regex.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.str2regex = str2regex;
4+
function str2regex(pattern) {
5+
return new RegExp(pattern
6+
.replace(/\\/g, '\\\\') // Backslashes escapen
7+
.replace(/\./g, '\\.') // Punkte als solche matchen
8+
.replace(/\*/g, '.*') // Wildcard in Regex umsetzen
9+
.replace(/!/g, '?!'));
10+
}

build/lib/testAdapter.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};

0 commit comments

Comments
 (0)