Repository: ioBroker/create-adapter
Pull request #716 (2021-04-01 11:30:23)

Refactor to support React based create-adapter application
**PR Checklist:**

-   [x] Provide a meaningful description to this PR or mention which issues this fixes.
-   [x] Add tests for your change. This includes negative tests (i.e. inputs that need to fail) as well as baseline tests (i.e. how should the directory structure look like?).
-   [x] Run the test suite with `npm test`
-   [x] If there are baseline changes, review them and make a separate commit for them with the comment "accept baselines" if they are desired changes _(no baseline changes except for new version and new dependencies)_
-   [x] Ensure the project builds with `npm run build`
-   [ ] ~If you added a required option, also add it to the template creation (`.github/create_templates.ts`)~
-   [ ] ~Add a detailed migration description to `docs/updates` explaining what the user needs to do when manually updating an existing project~
-   [x] Add your changes to `CHANGELOG.md` (referencing the migration description and this PR or the issue you fixed)

**Description:**  
- Restructured the entire source code to split it into "core" and "everything else".
   - "core" can be used in browser projects
   - "everything else" currently requires a file system and some packages that don't work in the browser
   - all type definitions are now properly exported through index.d.ts and core.d.ts for browser
- Added support for icons in formats different than PNG (default is still the same)
- Added a label to each question as this is most of the time required in the Web UI

Note: the web UI and the server generating the adapter code are not part of this project, but will reference the NPM module (once this PR is merged and a new version is published). The WIP can be found here: https://github.com/UncleSamSwiss/iobroker-dev-portal/tree/feature/create-adapter

lgtm-com[bot]: This pull request **introduces 1 alert** when merging d12fb3c69ecee1d446c31204011caab0e6157bb2 into b28d3cd1ec822f0b17a1cac197d8f4566acd54ff - [view on LGTM.com](https://lgtm.com/projects/g/ioBroker/create-adapter/rev/pr-d324980267987279fcd5ea7989db8b4d7d833aae)

**new alerts:**

* 1 for Polynomial regular expression used on uncontrolled data
AlCalzone: Wonder why it's only found now, but can you take a look at the RegExp warning?
UncleSamSwiss: > Some nits, also the linter is not happy:
> 
> ```
> $ npm run lint:wotan
> 
> > @iobroker/create-adapter@1.32.0 lint:wotan C:\Users\domin\Documents\Visual Studio 2017\Repositories\create-adapter
> > wotan
> 
> C:\Users\domin\Documents\Visual Studio 2017\Repositories\create-adapter\src\lib\core\migrationContextBase.ts:8:31
> ERROR 8:31    no-misused-generics           TypeParameter 'T' cannot be inferred from any parameter.
> ERROR 49:4    prefer-for-of                 Prefer a 'for-of' loop over a 'for' loop for this simple iteration.
> ERROR 53:6    try-catch-return-await        Missing 'await' of Promise returned inside try-catch.
> ERROR 57:4    try-catch-return-await        Missing 'await' of Promise returned inside try-catch.
> 
> C:\Users\domin\Documents\Visual Studio 2017\Repositories\create-adapter\src\lib\core\questions.ts:916:39
> ERROR 916:39  no-implicit-tostring          Unexpected implicit string coercion of 'object'.
> ERROR 927:42  no-implicit-tostring          Unexpected implicit string coercion of 'object'.
> 
> C:\Users\domin\Documents\Visual Studio 2017\Repositories\create-adapter\src\lib\tools.ts:116:25
> ERROR 116:25  no-implicit-tostring          Unexpected implicit string coercion of 'object'.
> ERROR 125:25  no-implicit-tostring          Unexpected implicit string coercion of 'object'.
> ERROR 200:20  no-unstable-api-use           CallSignature 'parse(urlStr: string): UrlWithStringQuery' is deprecated: since v11.0.0 - Use the WHATWG URL API.
> ERROR 270:7   no-useless-predicate          Expression is always truthy.
> 
> C:\Users\domin\Documents\Visual Studio 2017\Repositories\create-adapter\src\lib\localMigrationContext.ts:11:28
> ERROR 11:28   no-uninferred-type-parameter  TypeParameter 'T' is inferred as 'unknown'. Consider adding type arguments to the call.
> ERROR 12:30   no-uninferred-type-parameter  TypeParameter 'T' is inferred as 'unknown'. Consider adding type arguments to the call.
> ERROR 25:28   no-misused-generics           TypeParameter 'T' cannot be inferred from any parameter.
> 
> C:\Users\domin\Documents\Visual Studio 2017\Repositories\create-adapter\src\cli.ts:91:59
> ERROR 91:59   no-useless-initializer        Unnecessary initialization with 'undefined'.
> ERROR 213:8   no-useless-predicate          Expression is always truthy.
> 
> C:\Users\domin\Documents\Visual Studio 2017\Repositories\create-adapter\templates\README.md.ts:26:14
> ERROR 26:14   prefer-dot-notation           Prefer 'obj.watch' over 'obj["watch"]'.
> ERROR 29:14   prefer-dot-notation           Prefer 'obj.build' over 'obj["build"]'.
> ERROR 43:13   prefer-dot-notation           Prefer 'obj.test' over 'obj["test"]'.
> ERROR 45:14   prefer-dot-notation           Prefer 'obj.check' over 'obj["check"]'.
> ERROR 48:14   prefer-dot-notation           Prefer 'obj.coverage' over 'obj["coverage"]'.
> ERROR 51:14   prefer-dot-notation           Prefer 'obj.lint' over 'obj["lint"]'.
> 
> C:\Users\domin\Documents\Visual Studio 2017\Repositories\create-adapter\templates\admin\index_m.html.ts:10:50
> ERROR 10:50   no-implicit-tostring          Unexpected implicit string coercion of 'object'.
> 
> ✖ 22 errors
> ```

Do we really want to fix the linter issues now? Many of those errors are ages old (we haven't run the linter in some time and it isn't mentioned in the PR guidlines). I think we should look at those errors in a separate issue.
AlCalzone: Ok, can you raise an issue so we don't forget?
AlCalzone: How do you want to update the web creator? As part of the release action or with dependabot?
UncleSamSwiss: > How do you want to update the web creator? As part of the release action or with dependabot?

Once it is included in the portal project (and on main) after the next release, we should automatically create a PR from here to the portal project during the release. I'll create a PR once we are a bit more advanced in the portal project.

Patch:
diff --git a/.github/create_templates.ts b/.github/create_templates.ts
index bebd057..5c22413 100644
--- a/.github/create_templates.ts
+++ b/.github/create_templates.ts
@@ -3,8 +3,8 @@ import { execSync, ExecSyncOptions } from "child_process";
 import * as fs from "fs-extra";
 import * as path from "path";
 import { createAdapter } from "../src";
+import { Answers } from "../src/lib/core/questions";
 import { writeFiles } from "../src/lib/createAdapter";
-import { Answers } from "../src/lib/questions";
 
 const outDir = path.join(process.cwd(), "ioBroker.template");
 
diff --git a/.prettierignore b/.prettierignore
index 5e2f4bd..a66503c 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,7 +1,7 @@
 package.json
 package-lock.json
 build/
-src/lib/licenses.ts
+src/lib/core/licenses.ts
 templates/
 test/baselines/
 CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c07d00c..cbdb157 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 -->
 ## __WORK IN PROGRESS__
 * (AlCalzone) The generated `io-package.json` files are now validated with the official JSON schema during tests (#711)
+* (UncleSamSwiss) Source code refactoring for web based create-adapter application (#716)
 
 ## 1.32.0 (2021-03-09)
 * (AlCalzone) Update Readme: remove snyk.io badge, use https (#655)
diff --git a/core.ts b/core.ts
new file mode 100644
index 0000000..afe0daa
--- /dev/null
+++ b/core.ts
@@ -0,0 +1,5 @@
+/**
+ * This file contains all core type exports that can be used by a browser application.
+ */
+
+export * from "./src/lib/core";
diff --git a/index.ts b/index.ts
new file mode 100644
index 0000000..3ec7b46
--- /dev/null
+++ b/index.ts
@@ -0,0 +1,5 @@
+/**
+ * This file contains all type exports so create-adapter can be used as a nice library.
+ */
+
+export * from "./src/lib";
diff --git a/maintenance/cacheLicenses.ts b/maintenance/cacheLicenses.ts
index 8f0f7ab..c1a19c5 100644
--- a/maintenance/cacheLicenses.ts
+++ b/maintenance/cacheLicenses.ts
@@ -5,7 +5,7 @@ import axios, { AxiosRequestConfig } from "axios";
 import * as fs from "fs-extra";
 import * as path from "path";
 import * as yargs from "yargs";
-import { License } from "../src/lib/licenses";
+import { License } from "../src/lib/core/licenses";
 import { applyHttpsProxy, getRequestTimeout } from "../src/lib/tools";
 
 // Taken from https://api.github.com/licenses
@@ -21,7 +21,11 @@ const licenseUrls = {
 
 const startMarker = "/** BEGIN LICENSES */";
 const endMarker = "/** END LICENSES */";
-const licenseCacheFile = path.resolve(__dirname, "../src/lib/", "licenses.ts");
+const licenseCacheFile = path.resolve(
+	__dirname,
+	"../src/lib/core/",
+	"licenses.ts",
+);
 
 async function loadLicense(
 	shortName: keyof typeof licenseUrls,
diff --git a/src/cli.ts b/src/cli.ts
index 7b131f5..a9c12d3 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,22 +1,23 @@
-import { blueBright, green, red, underline } from "ansi-colors";
+import { blueBright, gray, green, red, underline } from "ansi-colors";
 import { prompt } from "enquirer";
 import * as fs from "fs-extra";
 import * as path from "path";
 import * as yargs from "yargs";
-import {
-	createFiles,
-	File,
-	testCondition,
-	writeFiles,
-} from "./lib/createAdapter";
-import { MigrationContext } from "./lib/migrationContext";
+import { CheckResult } from "./lib/core/actionsAndTransformers";
+import { MigrationContextBase } from "./lib/core/migrationContextBase";
 import {
 	Answers,
-	isQuestionGroup,
 	Question,
-	questionsAndText,
-} from "./lib/questions";
-import { error, executeCommand, isWindows } from "./lib/tools";
+	QuestionGroup,
+	questionGroups,
+	testCondition,
+} from "./lib/core/questions";
+import { createFiles, File, writeFiles } from "./lib/createAdapter";
+import { LocalMigrationContext } from "./lib/localMigrationContext";
+import { fetchPackageVersion } from "./lib/packageVersions";
+import { error, executeCommand, getOwnVersion, isWindows } from "./lib/tools";
+
+export type ConditionalTitle = (answers: Record<string, any>) => void;
 
 /** Define command line arguments */
 const argv = yargs
@@ -68,15 +69,26 @@ const argv = yargs
 /** Where the output should be written */
 const rootDir = path.resolve(argv.target || process.cwd());
 
+async function checkAdapterExistence(name: string): Promise<CheckResult> {
+	try {
+		await fetchPackageVersion(`iobroker.${name}`);
+		return `The adapter ioBroker.${name} already exists!`;
+	} catch (e) {
+		return true;
+	}
+}
+
 const creatorOptions = {
-	skipAdapterExistenceCheck:
-		!!argv.skipAdapterExistenceCheck || !!argv.migrate,
+	checkAdapterExistence:
+		!argv.skipAdapterExistenceCheck && !argv.migrate
+			? checkAdapterExistence
+			: undefined,
 };
 
 /** Asks a series of questions on the CLI */
 async function ask(): Promise<Answers> {
 	let answers: Record<string, any> = { cli: true };
-	let migrationContext: MigrationContext | undefined = undefined;
+	let migrationContext: MigrationContextBase | undefined = undefined;
 
 	if (!!argv.replay) {
 		const replayFile = path.resolve(argv.replay);
@@ -88,9 +100,10 @@ async function ask(): Promise<Answers> {
 	if (!!argv.migrate) {
 		try {
 			const migrationDir = path.resolve(argv.migrate);
-			migrationContext = new MigrationContext(migrationDir);
+			const ctx = new LocalMigrationContext(migrationDir);
 			console.log(`Migrating from ${migrationDir}`);
-			await migrationContext.load();
+			await ctx.load();
+			migrationContext = ctx;
 		} catch (error) {
 			console.error(error);
 			throw new Error(
@@ -172,6 +185,24 @@ async function ask(): Promise<Answers> {
 		}
 	}
 
+	const questionsAndText: (QuestionGroup | string | ConditionalTitle)[] = [
+		"",
+		green.bold("====================================================="),
+		green.bold(
+			`   Welcome to the ioBroker adapter creator v${getOwnVersion()}!`,
+		),
+		green.bold("====================================================="),
+		"",
+		gray(`You can cancel at any point by pressing Ctrl+C.`),
+		(answers) => (!!answers.replay ? green(`Replaying file`) : undefined),
+		(answers) => (!!answers.replay ? green(answers.replay) : undefined),
+		...questionGroups,
+		"",
+		underline(
+			"That's it. Please wait a minute while I get this working...",
+		),
+	];
+
 	for (const entry of questionsAndText) {
 		if (typeof entry === "string") {
 			// Headlines
@@ -182,7 +213,7 @@ async function ask(): Promise<Answers> {
 			if (text !== undefined) {
 				console.log(text);
 			}
-		} else if (isQuestionGroup(entry)) {
+		} else {
 			// only print the headline if any of the questions are necessary
 			if (
 				entry.questions.find((qq) =>
@@ -195,9 +226,6 @@ async function ask(): Promise<Answers> {
 			for (const qq of entry.questions) {
 				await askQuestion(qq);
 			}
-		} else {
-			// actual questions
-			await askQuestion(entry);
 		}
 	}
 	return answers as Answers;
diff --git a/src/index.ts b/src/index.ts
index 4be489a..63ce1d0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,10 +1,10 @@
-import { createFiles, File } from "./lib/createAdapter";
 import {
 	Answers,
 	checkAnswers,
 	formatAnswers,
 	validateAnswers,
-} from "./lib/questions";
+} from "./lib/core/questions";
+import { createFiles, File } from "./lib/createAdapter";
 
 export async function createAdapter(
 	answers: Answers,
diff --git a/src/lib/actionsAndTransformers.ts b/src/lib/core/actionsAndTransformers.ts
similarity index 88%
rename from src/lib/actionsAndTransformers.ts
rename to src/lib/core/actionsAndTransformers.ts
index 0cccd1b..fdde744 100644
--- a/src/lib/actionsAndTransformers.ts
+++ b/src/lib/core/actionsAndTransformers.ts
@@ -1,5 +1,4 @@
 import { yellow } from "ansi-colors";
-import { fetchPackageVersion } from "./packageVersions";
 import { Answers } from "./questions";
 
 const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -31,23 +30,14 @@ function isAdapterNameValid(name: string): CheckResult {
 	return true;
 }
 
-async function checkAdapterExistence(name: string): Promise<CheckResult> {
-	try {
-		await fetchPackageVersion(`iobroker.${name}`);
-		return `The adapter ioBroker.${name} already exists!`;
-	} catch (e) {
-		return true;
-	}
-}
-
 export async function checkAdapterName<
-	T extends { skipAdapterExistenceCheck: boolean }
+	T extends { checkAdapterExistence?: (name: string) => Promise<CheckResult> }
 >(name: string, options?: T): Promise<CheckResult> {
 	const validCheck = isAdapterNameValid(name);
 	if (typeof validCheck === "string") return validCheck;
 
-	if (!options || !options.skipAdapterExistenceCheck) {
-		const existenceCheck = await checkAdapterExistence(name);
+	if (options && options.checkAdapterExistence) {
+		const existenceCheck = await options.checkAdapterExistence(name);
 		if (typeof existenceCheck === "string") return existenceCheck;
 	}
 
diff --git a/src/lib/core/index.ts b/src/lib/core/index.ts
new file mode 100644
index 0000000..dc323cf
--- /dev/null
+++ b/src/lib/core/index.ts
@@ -0,0 +1,4 @@
+export * from "./actionsAndTransformers";
+export * from "./licenses";
+export * from "./migrationContextBase";
+export * from "./questions";
diff --git a/src/lib/licenses.ts b/src/lib/core/licenses.ts
similarity index 100%
rename from src/lib/licenses.ts
rename to src/lib/core/licenses.ts
diff --git a/src/lib/core/migrationContextBase.ts b/src/lib/core/migrationContextBase.ts
new file mode 100644
index 0000000..60ff57e
--- /dev/null
+++ b/src/lib/core/migrationContextBase.ts
@@ -0,0 +1,71 @@
+export abstract class MigrationContextBase {
+	public packageJson: any;
+	public ioPackageJson: any;
+
+	public abstract joinPath(...parts: string[]): string;
+
+	public abstract readTextFile(fileName: string): Promise<string>;
+	public abstract readJsonFile<T>(fileName: string): Promise<T>;
+
+	public abstract directoryExists(dirName: string): Promise<boolean>;
+	public abstract fileExists(dirName: string): Promise<boolean>;
+
+	public abstract hasFilesWithExtension(
+		dirName: string,
+		extension: string,
+		filter?: (fileName: string) => boolean,
+	): Promise<boolean>;
+
+	public hasDevDependency(packageName: string): boolean {
+		return this.packageJson.devDependencies?.hasOwnProperty(packageName);
+	}
+
+	public async getMainFileContent(): Promise<string> {
+		if (
+			!this.packageJson.main ||
+			!this.packageJson.main.endsWith(".js") ||
+			!(await this.fileExists(this.packageJson.main))
+		) {
+			// we don't have a main JavaScript file, it will be impossible to find code
+			return "";
+		}
+
+		try {
+			const tsMains = [
+				this.joinPath(
+					"src",
+					this.packageJson.main.replace(/\.js$/, ".ts"),
+				),
+				this.packageJson.main
+					.replace(/\.js$/, ".ts")
+					.replace(/^dist([\\/])/, "src$1"),
+				this.packageJson.main
+					.replace(/\.js$/, ".ts")
+					.replace(/^build([\\/])/, "src$1"),
+				this.packageJson.main
+					.replace(/\.js$/, ".ts")
+					.replace(/^(build|dist)[\\/]/, ""),
+			];
+			for (let i = 0; i < tsMains.length; i++) {
+				const tsMain = tsMains[i];
+				if (await this.fileExists(tsMain)) {
+					// most probably TypeScript
+					return this.readTextFile(tsMain);
+				}
+			}
+
+			return this.readTextFile(this.packageJson.main);
+		} catch {
+			// we don't want this to crash, so just return an empty string
+			return "";
+		}
+	}
+
+	public async analyzeCode(either: string, or: string): Promise<boolean> {
+		const content = await this.getMainFileContent();
+		const eitherCount = (content.match(new RegExp(either, "g")) || [])
+			.length;
+		const orCount = (content.match(new RegExp(or, "g")) || []).length;
+		return eitherCount >= orCount;
+	}
+}
diff --git a/src/lib/questions.ts b/src/lib/core/questions.ts
similarity index 88%
rename from src/lib/questions.ts
rename to src/lib/core/questions.ts
index d5e3efb..7911de6 100644
--- a/src/lib/questions.ts
+++ b/src/lib/core/questions.ts
@@ -1,5 +1,5 @@
 import { isArray } from "alcalzone-shared/typeguards";
-import { dim, gray, green, underline } from "ansi-colors";
+import { dim, gray, green } from "ansi-colors";
 import { SpecificPromptOptions } from "enquirer";
 import {
 	checkAdapterName,
@@ -14,10 +14,8 @@ import {
 	transformDescription,
 	transformKeywords,
 } from "./actionsAndTransformers";
-import { testCondition } from "./createAdapter";
 import { licenses } from "./licenses";
-import { MigrationContext } from "./migrationContext";
-import { getOwnVersion } from "./tools";
+import { MigrationContextBase } from "./migrationContextBase";
 
 // This is being used to simulate wrong options for conditions on the type level
 const __misused: unique symbol = Symbol.for("__misused");
@@ -34,25 +32,63 @@ export type Condition = { name: string } & (
 	| { [__misused]: undefined }
 );
 
-interface QuestionMeta {
+export function testCondition(
+	condition: Condition | Condition[] | undefined,
+	answers: Record<string, any>,
+): boolean {
+	if (condition == undefined) return true;
+
+	function testSingleCondition(cond: Condition): boolean {
+		if ("value" in cond) {
+			return answers[cond.name] === cond.value;
+		} else if ("contains" in cond) {
+			return (
+				answers[cond.name] &&
+				(answers[cond.name] as AnswerValue[]).indexOf(cond.contains) >
+					-1
+			);
+		} else if ("doesNotContain" in cond) {
+			return (
+				!answers[cond.name] ||
+				(answers[cond.name] as AnswerValue[]).indexOf(
+					cond.doesNotContain,
+				) === -1
+			);
+		}
+		return false;
+	}
+
+	if (isArray(condition)) {
+		return condition.every((cond) => testSingleCondition(cond));
+	} else {
+		return testSingleCondition(condition);
+	}
+}
+
+export type MigrateFunc = (
+	context: MigrationContextBase,
+	answers: Record<string, any>,
+	question: Question,
+) =>
+	| Promise<AnswerValue | AnswerValue[] | undefined>
+	| AnswerValue
+	| AnswerValue[]
+	| undefined;
+
+export type TransformResult = (
+	val: AnswerValue | AnswerValue[],
+) =>
+	| AnswerValue
+	| AnswerValue[]
+	| undefined
+	| Promise<AnswerValue | AnswerValue[] | undefined>;
+
+export interface QuestionMeta {
+	label: string;
 	/** One or more conditions that need(s) to be fulfilled for this question to be asked */
 	condition?: Condition | Condition[];
-	migrate?: (
-		context: MigrationContext,
-		answers: Record<string, any>,
-		question: Question,
-	) =>
-		| Promise<AnswerValue | AnswerValue[] | undefined>
-		| AnswerValue
-		| AnswerValue[]
-		| undefined;
-	resultTransform?: (
-		val: AnswerValue | AnswerValue[],
-	) =>
-		| AnswerValue
-		| AnswerValue[]
-		| undefined
-		| Promise<AnswerValue | AnswerValue[] | undefined>;
+	migrate?: MigrateFunc;
+	resultTransform?: TransformResult;
 	action?: QuestionAction<undefined | AnswerValue | AnswerValue[]>;
 	/** Whether an answer for this question is optional */
 	optional?: boolean;
@@ -65,17 +101,10 @@ interface QuestionMeta {
 
 export type Question = SpecificPromptOptions & QuestionMeta;
 export interface QuestionGroup {
+	title: string;
 	headline: string;
 	questions: Question[];
 }
-export type ConditionalTitle = (answers: Record<string, any>) => void;
-export function isQuestionGroup(val: any): val is QuestionGroup {
-	if (val == undefined) return false;
-	if (typeof val.headline !== "string") return false;
-	if (!isArray(val.questions)) return false;
-	// For now we don't need any more specific tests
-	return true;
-}
 
 function styledMultiselect<
 	T extends Pick<Question, Exclude<keyof Question, "type">> & {
@@ -95,28 +124,15 @@ function styledMultiselect<
 }
 
 /** All questions and the corresponding text lines */
-export const questionsAndText: (
-	| Question
-	| QuestionGroup
-	| string
-	| ConditionalTitle
-)[] = [
-	"",
-	green.bold("====================================================="),
-	green.bold(
-		`   Welcome to the ioBroker adapter creator v${getOwnVersion()}!`,
-	),
-	green.bold("====================================================="),
-	"",
-	gray(`You can cancel at any point by pressing Ctrl+C.`),
-	(answers) => (!!answers.replay ? green(`Replaying file`) : undefined),
-	(answers) => (!!answers.replay ? green(answers.replay) : undefined),
+export const questionGroups: QuestionGroup[] = [
 	{
+		title: "Basics",
 		headline: "Let's get started with a few questions about your project!",
 		questions: [
 			{
 				type: "input",
 				name: "adapterName",
+				label: "Adapter Name",
 				message: "Please enter the name of your project:",
 				resultTransform: transformAdapterName,
 				action: checkAdapterName,
@@ -125,6 +141,7 @@ export const questionsAndText: (
 			{
 				type: "input",
 				name: "title",
+				label: "Title",
 				message: "Which title should be shown in the admin UI?",
 				action: checkTitle,
 				migrate: (ctx) =>
@@ -134,6 +151,7 @@ export const questionsAndText: (
 			{
 				type: "input",
 				name: "description",
+				label: "Description",
 				message: "Please enter a short description:",
 				hint: "(optional)",
 				optional: true,
@@ -145,6 +163,7 @@ export const questionsAndText: (
 			{
 				type: "input",
 				name: "keywords",
+				label: "Keywords",
 				message:
 					"Enter some keywords (separated by commas) to describe your project:",
 				hint: "(optional)",
@@ -160,6 +179,7 @@ export const questionsAndText: (
 			{
 				type: "input",
 				name: "contributors",
+				label: "Contributors",
 				message:
 					"If you have any contributors, please enter their names (seperated by commas):",
 				hint: "(optional)",
@@ -175,18 +195,21 @@ export const questionsAndText: (
 				condition: { name: "cli", value: false },
 				type: "web_upload" as any,
 				name: "icon",
-				message: "Upload an icon",
+				label: "Adapter Icon",
+				message: "Upload your adapter icon",
 				hint: "(optional)",
 				optional: true,
 			},
 		],
 	},
 	{
+		title: "Technical",
 		headline: "Nice! Let's get technical...",
 		questions: [
 			{
 				type: "select",
 				name: "expert",
+				label: "Expert Mode",
 				message: "How detailed do you want to configure your project?",
 				choices: [
 					{
@@ -200,6 +223,7 @@ export const questionsAndText: (
 			},
 			styledMultiselect({
 				name: "features",
+				label: "Features",
 				message: "Which features should your project contain?",
 				initial: [0],
 				choices: [
@@ -216,6 +240,7 @@ export const questionsAndText: (
 			styledMultiselect({
 				condition: { name: "features", contains: "adapter" },
 				name: "adminFeatures",
+				label: "Admin Features",
 				expert: true,
 				message:
 					"Which additional features should be available in the admin?",
@@ -241,6 +266,7 @@ export const questionsAndText: (
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "type",
+				label: "Adapter Type",
 				message: "Which category does your adapter fall into?",
 				choices: [
 					{
@@ -372,9 +398,10 @@ export const questionsAndText: (
 				migrate: (ctx) => ctx.ioPackageJson.common?.type,
 			},
 			{
-				condition: { name: "features", doesNotContain: "adapter" },
+				condition: { name: "features", contains: "vis" },
 				type: "select",
 				name: "type",
+				label: "VIS Type",
 				message: "Which kind of visualization is this?",
 				choices: [
 					{ message: "Icons for VIS", value: "visualization-icons" },
@@ -386,6 +413,7 @@ export const questionsAndText: (
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "startMode",
+				label: "Start Mode",
 				expert: true,
 				message: "When should the adapter be started?",
 				initial: "daemon",
@@ -412,6 +440,7 @@ export const questionsAndText: (
 				condition: { name: "startMode", value: "schedule" },
 				type: "select",
 				name: "scheduleStartOnChange",
+				label: "Schedule",
 				expert: true,
 				message:
 					"Should the adapter also be started when the configuration is changed?",
@@ -424,6 +453,7 @@ export const questionsAndText: (
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "connectionType",
+				label: "Connection Type",
 				optional: true, // We cannot assume this when creating templates
 				message: `From where will the adapter get its data?`,
 				choices: [
@@ -439,6 +469,7 @@ export const questionsAndText: (
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "dataSource",
+				label: "Data Source",
 				optional: true, // We cannot assume this when creating templates
 				message: `How will the adapter receive its data?`,
 				choices: [
@@ -464,6 +495,7 @@ export const questionsAndText: (
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "connectionIndicator",
+				label: "Show Connection Indicator",
 				expert: true,
 				message: `Do you want to indicate the connection state?`,
 				hint: "(To some device or some service)",
@@ -476,6 +508,12 @@ export const questionsAndText: (
 						? "yes"
 						: "no",
 			},
+		],
+	},
+	{
+		title: "Settings",
+		headline: "Define the settings for the adapter",
+		questions: [
 			{
 				condition: [
 					{ name: "features", contains: "adapter" },
@@ -483,14 +521,22 @@ export const questionsAndText: (
 				],
 				type: "web_unknown" as any, // TODO: give this a good type
 				name: "adapterSettings",
+				label: "Adapter Settings",
 				message: "Define the settings for the adapter",
 				hint: "(optional)",
 				optional: true,
 			},
+		],
+	},
+	{
+		title: "Code",
+		headline: "Some more questions about the source code...",
+		questions: [
 			{
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "language",
+				label: "Programming Language",
 				message:
 					"Which language do you want to use to code the adapter?",
 				choices: ["JavaScript", "TypeScript"],
@@ -507,6 +553,7 @@ export const questionsAndText: (
 				condition: [{ name: "features", contains: "adapter" }],
 				type: "select",
 				name: "adminReact",
+				label: "Admin with React",
 				message: "Use React for the Admin UI?",
 				initial: "no",
 				choices: ["yes", "no"],
@@ -528,6 +575,7 @@ export const questionsAndText: (
 				condition: [{ name: "adminFeatures", contains: "tab" }],
 				type: "select",
 				name: "tabReact",
+				label: "Tab with React",
 				message: "Use React for the tab UI?",
 				initial: "no",
 				choices: ["yes", "no"],
@@ -540,6 +588,7 @@ export const questionsAndText: (
 			styledMultiselect({
 				condition: { name: "language", value: "JavaScript" },
 				name: "tools",
+				label: "Tools",
 				message: "Which of the following tools do you want to use?",
 				initial: [0, 1],
 				choices: [
@@ -565,6 +614,7 @@ export const questionsAndText: (
 			styledMultiselect({
 				condition: { name: "language", value: "TypeScript" },
 				name: "tools",
+				label: "Tools",
 				message: "Which of the following tools do you want to use?",
 				initial: [0],
 				choices: [
@@ -597,6 +647,7 @@ export const questionsAndText: (
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "indentation",
+				label: "Indentation",
 				message: "Do you prefer tab or space indentation?",
 				initial: "Tab",
 				choices: ["Tab", "Space (4)"],
@@ -607,6 +658,7 @@ export const questionsAndText: (
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "quotes",
+				label: "Quotes",
 				message: "Do you prefer double or single quotes?",
 				initial: "double",
 				choices: ["double", "single"],
@@ -617,6 +669,7 @@ export const questionsAndText: (
 				condition: { name: "features", contains: "adapter" },
 				type: "select",
 				name: "es6class",
+				label: "ES6 Class",
 				expert: true,
 				message: "How should the main adapter file be structured?",
 				initial: "yes",
@@ -640,11 +693,13 @@ export const questionsAndText: (
 		],
 	},
 	{
+		title: "Administrative",
 		headline: "Almost done! Just a few administrative details...",
 		questions: [
 			{
 				type: "input",
 				name: "authorName",
+				label: "Author Name",
 				message: "Please enter your name (or nickname):",
 				action: checkAuthorName,
 				migrate: (ctx) => ctx.packageJson.author?.name,
@@ -652,18 +707,20 @@ export const questionsAndText: (
 			{
 				type: "input",
 				name: "authorGithub",
+				label: "GitHub Name",
 				message: "What's your name/org on GitHub?",
 				initial: ((answers: Answers) => answers.authorName) as any,
 				action: checkAuthorName,
 				migrate: (ctx) =>
 					ctx.ioPackageJson.common?.extIcon?.replace(
-						/^.+?\.com\/([^\/]+)\/.+$/,
+						/^\w+:\/\/[^\/]+\.com\/([^\/]+)\/.+$/,
 						"$1",
 					),
 			},
 			{
 				type: "input",
 				name: "authorEmail",
+				label: "Adapter E-Mail",
 				message: "What's your email address?",
 				action: checkEmail,
 				migrate: (ctx) => ctx.packageJson.author?.email,
@@ -671,6 +728,7 @@ export const questionsAndText: (
 			{
 				type: "select",
 				name: "gitRemoteProtocol",
+				label: "GIT Protocol",
 				message: "Which protocol should be used for the repo URL?",
 				expert: true,
 				initial: "HTTPS",
@@ -692,6 +750,7 @@ export const questionsAndText: (
 				condition: { name: "cli", value: true },
 				type: "select",
 				name: "gitCommit",
+				label: "Git Commit",
 				expert: true,
 				message: "Initialize the GitHub repo automatically?",
 				initial: "no",
@@ -701,6 +760,7 @@ export const questionsAndText: (
 			{
 				type: "select",
 				name: "license",
+				label: "License",
 				message: "Which license should be used for your project?",
 				initial: 5,
 				choices: [
@@ -721,6 +781,7 @@ export const questionsAndText: (
 			{
 				type: "select",
 				name: "ci",
+				label: "Continuous Integration",
 				expert: true,
 				message: "Which continuous integration service should be used?",
 				initial: "gh-actions",
@@ -743,6 +804,7 @@ export const questionsAndText: (
 			{
 				type: "select",
 				name: "dependabot",
+				label: "Dependabot",
 				expert: true,
 				message:
 					"Do you want to receive regular dependency updates through Pull Requests?",
@@ -756,15 +818,11 @@ export const questionsAndText: (
 			},
 		],
 	},
-	"",
-	underline("That's it. Please wait a minute while I get this working..."),
 ];
 
 /** Only the questions */
-export const questions = (questionsAndText.filter(
-	(q) => typeof q !== "string" && typeof q !== "function",
-) as (Question | QuestionGroup)[])
-	.map((q) => (isQuestionGroup(q) ? q.questions : [q]))
+export const questions = questionGroups
+	.map((q) => q.questions)
 	.reduce((arr, next) => arr.concat(...next), []);
 
 export interface BaseAdapterSettings<T> {
@@ -781,9 +839,15 @@ export interface NumberAdapterSettings extends BaseAdapterSettings<number> {
 export interface BooleanAdapterSettings extends BaseAdapterSettings<boolean> {
 	inputType: "checkbox";
 }
+
+export interface AdapterSelectOption {
+	value: string;
+	text: string;
+}
+
 export interface SelectAdapterSettings extends BaseAdapterSettings<string> {
 	inputType: "select";
-	options: { value: string; text: string }[];
+	options: AdapterSelectOption[];
 }
 export type AdapterSettings =
 	| StringAdapterSettings
@@ -791,6 +855,16 @@ export type AdapterSettings =
 	| BooleanAdapterSettings
 	| SelectAdapterSettings;
 
+/**
+ * An icon in binary or base64-encoded format.
+ */
+export interface UploadedIcon {
+	/** The data icon in binary or base64-encoded format. */
+	data: string | Buffer;
+	/** The file extension to use with the icon. */
+	extension: string;
+}
+
 export interface Answers {
 	adapterName: string;
 	description?: string;
@@ -828,8 +902,7 @@ export interface Answers {
 	connectionIndicator?: "yes" | "no";
 	connectionType?: "cloud" | "local";
 	dataSource?: "poll" | "push" | "assumption";
-	/** An icon in binary or some string-encoded format */
-	icon?: string | Buffer;
+	icon?: UploadedIcon;
 	/** An array of predefined adapter options */
 	adapterSettings?: AdapterSettings[];
 }
diff --git a/src/lib/createAdapter.ts b/src/lib/createAdapter.ts
index f353a68..f7eb941 100644
--- a/src/lib/createAdapter.ts
+++ b/src/lib/createAdapter.ts
@@ -1,9 +1,8 @@
-import { isArray } from "alcalzone-shared/typeguards";
 import * as fs from "fs-extra";
 import * as os from "os";
 import * as path from "path";
 import * as templateFiles from "../../templates";
-import { Answers, AnswerValue, Condition } from "./questions";
+import { Answers } from "./core/questions";
 import {
 	formatWithPrettier,
 	getOwnVersion,
@@ -31,39 +30,6 @@ export interface File {
 	noReformat: boolean;
 }
 
-export function testCondition(
-	condition: Condition | Condition[] | undefined,
-	answers: Record<string, any>,
-): boolean {
-	if (condition == undefined) return true;
-
-	function testSingleCondition(cond: Condition): boolean {
-		if ("value" in cond) {
-			return answers[cond.name] === cond.value;
-		} else if ("contains" in cond) {
-			return (
-				answers[cond.name] &&
-				(answers[cond.name] as AnswerValue[]).indexOf(cond.contains) >
-					-1
-			);
-		} else if ("doesNotContain" in cond) {
-			return (
-				!answers[cond.name] ||
-				(answers[cond.name] as AnswerValue[]).indexOf(
-					cond.doesNotContain,
-				) === -1
-			);
-		}
-		return false;
-	}
-
-	if (isArray(condition)) {
-		return condition.every((cond) => testSingleCondition(cond));
-	} else {
-		return testSingleCondition(condition);
-	}
-}
-
 export async function createFiles(answers: Answers): Promise<File[]> {
 	const creatorVersion: string = getOwnVersion();
 	const answersWithMeta: Answers & AnswersMeta = {
diff --git a/src/lib/index.ts b/src/lib/index.ts
new file mode 100644
index 0000000..d2035b9
--- /dev/null
+++ b/src/lib/index.ts
@@ -0,0 +1,6 @@
+export * from "./core";
+export * from "./createAdapter";
+export * from "./localMigrationContext";
+export * from "./packageVersions";
+export * from "./tools";
+export * from "./translation";
diff --git a/src/lib/localMigrationContext.ts b/src/lib/localMigrationContext.ts
new file mode 100644
index 0000000..008372e
--- /dev/null
+++ b/src/lib/localMigrationContext.ts
@@ -0,0 +1,53 @@
+import { existsSync, readdir, readFile, readJson, stat } from "fs-extra";
+import { MigrationContextBase } from "./core/migrationContextBase";
+import path = require("path");
+
+export class LocalMigrationContext extends MigrationContextBase {
+	constructor(private readonly baseDir: string) {
+		super();
+	}
+
+	public async load(): Promise<void> {
+		this.packageJson = await this.readJsonFile("package.json");
+		this.ioPackageJson = await this.readJsonFile("io-package.json");
+	}
+
+	public joinPath(...parts: string[]): string {
+		return path.join(...parts);
+	}
+
+	public readTextFile(fileName: string): Promise<string> {
+		return readFile(path.join(this.baseDir, fileName), {
+			encoding: "utf8",
+		});
+	}
+
+	public async readJsonFile<T>(fileName: string): Promise<T> {
+		return (await readJson(path.join(this.baseDir, fileName))) as T;
+	}
+
+	public async directoryExists(dirName: string): Promise<boolean> {
+		const fullPath = path.join(this.baseDir, dirName);
+		return existsSync(fullPath) && (await stat(fullPath)).isDirectory();
+	}
+
+	public async fileExists(dirName: string): Promise<boolean> {
+		const fullPath = path.join(this.baseDir, dirName);
+		return existsSync(fullPath) && (await stat(fullPath)).isFile();
+	}
+
+	public async hasFilesWithExtension(
+		dirName: string,
+		extension: string,
+		filter?: (fileName: string) => boolean,
+	): Promise<boolean> {
+		return (
+			(await this.directoryExists(dirName)) &&
+			(await readdir(path.join(this.baseDir, dirName))).some(
+				(f) =>
+					(!filter || filter(f)) &&
+					f.toLowerCase().endsWith(extension.toLowerCase()),
+			)
+		);
+	}
+}
diff --git a/src/lib/migrationContext.ts b/src/lib/migrationContext.ts
deleted file mode 100644
index a31557e..0000000
--- a/src/lib/migrationContext.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { existsSync, readdir, readFile, readJson, stat } from "fs-extra";
-import path = require("path");
-
-export class MigrationContext {
-	public packageJson: any;
-	public ioPackageJson: any;
-
-	constructor(private readonly baseDir: string) {}
-
-	public async load(): Promise<void> {
-		this.packageJson = await this.readJsonFile("package.json");
-		this.ioPackageJson = await this.readJsonFile("io-package.json");
-	}
-
-	public async readJsonFile<T>(fileName: string): Promise<T> {
-		return (await readJson(path.join(this.baseDir, fileName))) as T;
-	}
-
-	public async directoryExists(dirName: string): Promise<boolean> {
-		const fullPath = path.join(this.baseDir, dirName);
-		return existsSync(fullPath) && (await stat(fullPath)).isDirectory();
-	}
-
-	public async fileExists(dirName: string): Promise<boolean> {
-		const fullPath = path.join(this.baseDir, dirName);
-		return existsSync(fullPath) && (await stat(fullPath)).isFile();
-	}
-
-	public async hasFilesWithExtension(
-		dirName: string,
-		extension: string,
-		filter?: (fileName: string) => boolean,
-	): Promise<boolean> {
-		return (
-			(await this.directoryExists(dirName)) &&
-			(await readdir(path.join(this.baseDir, dirName))).some(
-				(f) =>
-					(!filter || filter(f)) &&
-					f.toLowerCase().endsWith(extension.toLowerCase()),
-			)
-		);
-	}
-
-	public hasDevDependency(packageName: string): boolean {
-		return this.packageJson.devDependencies?.hasOwnProperty(packageName);
-	}
-
-	public async getMainFileContent(): Promise<string> {
-		if (
-			!this.packageJson.main ||
-			!this.packageJson.main.endsWith(".js") ||
-			!(await this.fileExists(this.packageJson.main))
-		) {
-			// we don't have a main JavaScript file, it will be impossible to find code
-			return "";
-		}
-
-		try {
-			const tsMains = [
-				path.join(
-					this.baseDir,
-					"src",
-					this.packageJson.main.replace(/\.js$/, ".ts"),
-				),
-				path.join(
-					this.baseDir,
-					this.packageJson.main
-						.replace(/\.js$/, ".ts")
-						.replace(/^dist([\\/])/, "src$1"),
-				),
-				path.join(
-					this.baseDir,
-					this.packageJson.main
-						.replace(/\.js$/, ".ts")
-						.replace(/^build([\\/])/, "src$1"),
-				),
-				path.join(
-					this.baseDir,
-					this.packageJson.main
-						.replace(/\.js$/, ".ts")
-						.replace(/^(build|dist)[\\/]/, ""),
-				),
-			];
-			for (let i = 0; i < tsMains.length; i++) {
-				const tsMain = tsMains[i];
-				if (existsSync(tsMain)) {
-					// most probably TypeScript
-					return readFile(tsMain, { encoding: "utf8" });
-				}
-			}
-
-			return readFile(path.join(this.baseDir, this.packageJson.main), {
-				encoding: "utf8",
-			});
-		} catch {
-			// we don't want this to crash, so just return an empty string
-			return "";
-		}
-	}
-
-	public async analyzeCode(either: string, or: string): Promise<boolean> {
-		const content = await this.getMainFileContent();
-		const eitherCount = (content.match(new RegExp(either, "g")) || [])
-			.length;
-		const orCount = (content.match(new RegExp(or, "g")) || []).length;
-		return eitherCount >= orCount;
-	}
-}
diff --git a/src/lib/tools.ts b/src/lib/tools.ts
index e189a8d..96a4db8 100644
--- a/src/lib/tools.ts
+++ b/src/lib/tools.ts
@@ -9,8 +9,8 @@ import * as os from "os";
 import * as path from "path";
 import * as prettier from "prettier";
 import * as nodeUrl from "url";
-import { licenses } from "./licenses";
-import { Answers } from "./questions";
+import { licenses } from "./core/licenses";
+import { Answers } from "./core/questions";
 
 export function error(message: string): void {
 	console.error(bold.red(message));
diff --git a/src/lib/translation.ts b/src/lib/translation.ts
index edfb9a4..5af91d1 100644
--- a/src/lib/translation.ts
+++ b/src/lib/translation.ts
@@ -1,5 +1,5 @@
+import { AdapterSettings, Answers, getDefaultAnswer } from "./core/questions";
 import { TemplateFunction } from "./createAdapter";
-import { AdapterSettings, Answers, getDefaultAnswer } from "./questions";
 import { formatJsonString, translateText } from "./tools";
 
 export type Languages =
diff --git a/templates/README.md.ts b/templates/README.md.ts
index e0e3367..385687b 100644
--- a/templates/README.md.ts
+++ b/templates/README.md.ts
@@ -53,7 +53,7 @@ export = (answers => {
 
 	const adapterNameLowerCase = answers.adapterName.toLowerCase();
 	const template = `
-![Logo](admin/${answers.adapterName}.png)
+![Logo](admin/${answers.adapterName}.${answers.icon?.extension || "png"})
 # ioBroker.${answers.adapterName}
 
 [![NPM version](https://img.shields.io/npm/v/iobroker.${adapterNameLowerCase}.svg)](https://www.npmjs.com/package/iobroker.${adapterNameLowerCase})
diff --git a/templates/admin/icon.png.ts b/templates/admin/icon.png.ts
index 6f43bb0..4d83511 100644
--- a/templates/admin/icon.png.ts
+++ b/templates/admin/icon.png.ts
@@ -2,22 +2,22 @@ import { readFileFromRootDir, TemplateFunction } from "../../src/lib/createAdapt
 
 const templateFunction: TemplateFunction = answers => {
 	if (answers.icon) {
-		if (typeof answers.icon === "string") {
+		if (typeof answers.icon.data === "string") {
 			// Try to decode Base64
-			const base64Match = answers.icon.match(/^data:image\/(\w+);base64,(.+)$/);
+			const base64Match = answers.icon.data.match(/^data:image\/([^;]+);base64,(.+)$/);
 			if (base64Match) {
 				return Buffer.from(base64Match[2], "base64");
 			}
 			throw new Error("The icon has an unsupported string encoding!");
 		} else {
 			// Return the raw buffer
-			return answers.icon;
+			return answers.icon.data;
 		}
 	}
 
 	// Fall back to reading the default image
 	return readFileFromRootDir("../../adapter-creator.png", __dirname, true);
 };
-templateFunction.customPath = answers => `admin/${answers.adapterName}.png`;
+templateFunction.customPath = answers => `admin/${answers.adapterName}.${answers.icon?.extension || "png"}`;
 templateFunction.noReformat = true; // Don't format binary files
 export = templateFunction;
diff --git a/templates/admin/index_m.html.ts b/templates/admin/index_m.html.ts
index 2510bb4..72d8efc 100644
--- a/templates/admin/index_m.html.ts
+++ b/templates/admin/index_m.html.ts
@@ -1,5 +1,5 @@
+import { AdapterSettings, getDefaultAnswer } from "../../src/lib/core/questions";
 import { TemplateFunction } from "../../src/lib/createAdapter";
-import { AdapterSettings, getDefaultAnswer } from "../../src/lib/questions";
 
 function generateSettingsDiv(settings: AdapterSettings): string {
 	if (settings.inputType === "select" && settings.options) {
@@ -111,7 +111,7 @@ ${useReact ? (`
 
 		<div class="row">
 			<div class="col s12 m4 l2">
-				<img src="${answers.adapterName}.png" class="logo">
+				<img src="${answers.adapterName}.${answers.icon?.extension || "png"}" class="logo">
 			</div>
 		</div>
 
diff --git a/templates/admin/src/components/settings.tsx_jsx.ts b/templates/admin/src/components/settings.tsx_jsx.ts
index 0a5c997..98b5616 100644
--- a/templates/admin/src/components/settings.tsx_jsx.ts
+++ b/templates/admin/src/components/settings.tsx_jsx.ts
@@ -1,5 +1,5 @@
+import { AdapterSettings, getDefaultAnswer } from "../../../../src/lib/core/questions";
 import { TemplateFunction } from "../../../../src/lib/createAdapter";
-import { AdapterSettings, getDefaultAnswer } from "../../../../src/lib/questions";
 
 function generateSettingsMethod(settings: AdapterSettings): string {
 	if (settings.inputType === "select" && settings.options) {
diff --git a/templates/admin/tab_m.html.ts b/templates/admin/tab_m.html.ts
index b54c3e8..7b87f01 100644
--- a/templates/admin/tab_m.html.ts
+++ b/templates/admin/tab_m.html.ts
@@ -47,7 +47,7 @@ ${useReact ? `
 			 Learn more http://materializecss.com/forms.html-->
 			<div class="row">
 				<div class="input-field col s6">
-					<img src="${answers.adapterName}.png" class="logo">
+					<img src="${answers.adapterName}.${answers.icon?.extension || "png"}" class="logo">
 				</div>
 			</div>
 			<div class="row">
diff --git a/templates/io-package.json.ts b/templates/io-package.json.ts
index 375fc4a..e3735db 100644
--- a/templates/io-package.json.ts
+++ b/templates/io-package.json.ts
@@ -1,8 +1,8 @@
 import { composeObject } from "alcalzone-shared/objects";
 import * as JSON5 from "json5";
+import { licenses } from "../src/lib/core/licenses";
+import { AdapterSettings, getDefaultAnswer } from "../src/lib/core/questions";
 import { TemplateFunction } from "../src/lib/createAdapter";
-import { licenses } from "../src/lib/licenses";
-import { AdapterSettings, getDefaultAnswer } from "../src/lib/questions";
 import { translateText } from "../src/lib/tools";
 
 export = (async answers => {
@@ -61,9 +61,9 @@ export = (async answers => {
 		"keywords": ${JSON.stringify(answers.keywords || getDefaultAnswer("keywords"))},
 		"license": "${licenses[answers.license!].id}",
 		"platform": "Javascript/Node.js",
-		"icon": "${answers.adapterName}.png",
+		"icon": "${answers.adapterName}.${answers.icon?.extension || "png"}",
 		"enabled": true,
-		"extIcon": "https://raw.githubusercontent.com/${answers.authorGithub}/ioBroker.${answers.adapterName}/master/admin/${answers.adapterName}.png",
+		"extIcon": "https://raw.githubusercontent.com/${answers.authorGithub}/ioBroker.${answers.adapterName}/master/admin/${answers.adapterName}.${answers.icon?.extension || "png"}",
 		"readme": "https://github.com/${answers.authorGithub}/ioBroker.${answers.adapterName}/blob/master/README.md",
 		"loglevel": "info",
 		${isWidget ? (`
diff --git a/templates/lib/adapter-config.d.ts.ts b/templates/lib/adapter-config.d.ts.ts
index c7885f4..a094ed1 100644
--- a/templates/lib/adapter-config.d.ts.ts
+++ b/templates/lib/adapter-config.d.ts.ts
@@ -1,5 +1,5 @@
+import { AdapterSettings, getDefaultAnswer } from "../../src/lib/core/questions";
 import { TemplateFunction } from "../../src/lib/createAdapter";
-import { AdapterSettings, getDefaultAnswer } from "../../src/lib/questions";
 
 function generateSettingsProperty(settings: AdapterSettings): string {
 	if (settings.inputType === "select" && settings.options) {
diff --git a/templates/main.es6.js.ts b/templates/main.es6.js.ts
index 83d9c2f..ddfe060 100644
--- a/templates/main.es6.js.ts
+++ b/templates/main.es6.js.ts
@@ -1,5 +1,5 @@
+import { AdapterSettings, getDefaultAnswer } from "../src/lib/core/questions";
 import { TemplateFunction } from "../src/lib/createAdapter";
-import { AdapterSettings, getDefaultAnswer } from "../src/lib/questions";
 import { kebabCaseToUpperCamelCase } from "../src/lib/tools";
 
 const templateFunction: TemplateFunction = async answers => {
diff --git a/templates/main.js.ts b/templates/main.js.ts
index 62f04f8..438860a 100644
--- a/templates/main.js.ts
+++ b/templates/main.js.ts
@@ -1,5 +1,5 @@
+import { AdapterSettings, getDefaultAnswer } from "../src/lib/core/questions";
 import { TemplateFunction } from "../src/lib/createAdapter";
-import { AdapterSettings, getDefaultAnswer } from "../src/lib/questions";
 
 export = (async answers => {
 
diff --git a/templates/package.json.ts b/templates/package.json.ts
index 1ad1969..189913e 100644
--- a/templates/package.json.ts
+++ b/templates/package.json.ts
@@ -1,9 +1,9 @@
 import * as JSON5 from "json5";
 import * as pLimit from "p-limit";
+import { licenses } from "../src/lib/core/licenses";
+import { getDefaultAnswer } from "../src/lib/core/questions";
 import { TemplateFunction } from "../src/lib/createAdapter";
-import { licenses } from "../src/lib/licenses";
 import { fetchPackageVersion, getPackageName } from "../src/lib/packageVersions";
-import { getDefaultAnswer } from "../src/lib/questions";
 
 // Limit package version downloads to 10 simultaneous connections
 const downloadLimiter = pLimit(10);
diff --git a/templates/src/main.es6.ts b/templates/src/main.es6.ts
index 6dc0be4..fea76ec 100644
--- a/templates/src/main.es6.ts
+++ b/templates/src/main.es6.ts
@@ -1,5 +1,5 @@
+import { AdapterSettings, getDefaultAnswer } from "../../src/lib/core/questions";
 import { TemplateFunction } from "../../src/lib/createAdapter";
-import { AdapterSettings, getDefaultAnswer } from "../../src/lib/questions";
 import { kebabCaseToUpperCamelCase } from "../../src/lib/tools";
 
 const templateFunction: TemplateFunction = async answers => {
diff --git a/templates/src/main.ts.ts b/templates/src/main.ts.ts
index 2417e52..4f8b223 100644
--- a/templates/src/main.ts.ts
+++ b/templates/src/main.ts.ts
@@ -1,5 +1,5 @@
+import { AdapterSettings, getDefaultAnswer } from "../../src/lib/core/questions";
 import { TemplateFunction } from "../../src/lib/createAdapter";
-import { AdapterSettings, getDefaultAnswer } from "../../src/lib/questions";
 
 export = (answers => {
 
diff --git a/tsconfig.build.json b/tsconfig.build.json
index e1b09f8..fe484b6 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -3,8 +3,8 @@
 	// Modified config to only compile .ts-files in the src dir
 	"compilerOptions": {
 		"noEmit": false,
-		"declaration": false
+		"declaration": true
 	},
-	"include": ["src/**/*.ts", "templates/**/*.ts"],
+	"include": ["src/**/*.ts", "templates/**/*.ts", "index.ts", "core.ts"],
 	"exclude": ["src/**/*.test.ts", "templates/**/*.raw.*"]
 }


Test patch:
diff --git a/src/lib/core.test.ts b/src/lib/core.test.ts
new file mode 100644
index 0000000..05836dd
--- /dev/null
+++ b/src/lib/core.test.ts
@@ -0,0 +1,68 @@
+import { readdir, readFile } from "fs-extra";
+import { join, resolve } from "path";
+
+const testAllFiles = async (
+	assertion: (content: string, filename: string) => void,
+) => {
+	const baseDir = resolve(__dirname, "./core");
+	const files = await readdir(baseDir);
+	for (const file of files.filter(
+		(f) => f.endsWith(".ts") && !f.endsWith(".test.ts"),
+	)) {
+		const content = await readFile(join(baseDir, file), {
+			encoding: "utf8",
+		});
+		assertion(content, file);
+	}
+};
+
+/**
+ * These tests are used to ensure that everything in the ./core directory
+ * can be included from React. Please be careful when modifying them!
+ */
+describe("./core directory", () => {
+	it("should not contain any imports from base directory (../)", async () => {
+		testAllFiles((content, file) =>
+			content.should.not.match(
+				/import .+ from "\.\./gi,
+				`${file} contains an import from the base directory`,
+			),
+		);
+	});
+
+	it('should not contain any imports from "os"', async () => {
+		await testAllFiles((content, file) =>
+			content.should.not.match(
+				/import .+ from "os";/gi,
+				`${file} contains an import from "os"`,
+			),
+		);
+	});
+
+	it('should not contain any imports from "fs"', async () => {
+		await testAllFiles((content, file) =>
+			content.should.not.match(
+				/import .+ from "fs";/gi,
+				`${file} contains an import from "fs"`,
+			),
+		);
+	});
+
+	it('should not contain any imports from "fs-extra"', async () => {
+		await testAllFiles((content, file) =>
+			content.should.not.match(
+				/import .+ from "fs-extra";/gi,
+				`${file} contains an import from "fs-extra"`,
+			),
+		);
+	});
+
+	it('should not contain any imports from "path"', async () => {
+		await testAllFiles((content, file) =>
+			content.should.not.match(
+				/import .+ from "path";/gi,
+				`${file} contains an import from "path"`,
+			),
+		);
+	});
+});
diff --git a/src/lib/actionsAndTransformers.test.ts b/src/lib/core/actionsAndTransformers.test.ts
similarity index 88%
rename from src/lib/actionsAndTransformers.test.ts
rename to src/lib/core/actionsAndTransformers.test.ts
index 630c9ff..a9561a9 100644
--- a/src/lib/actionsAndTransformers.test.ts
+++ b/src/lib/core/actionsAndTransformers.test.ts
@@ -1,25 +1,17 @@
 import { expect } from "chai";
-import * as proxyquireModule from "proxyquire";
 import { stub } from "sinon";
 import {
+	checkAdapterName,
 	checkAuthorName,
 	checkEmail,
 	checkMinSelections,
+	CheckResult,
 	checkTitle,
 	transformAdapterName,
 	transformDescription,
 } from "./actionsAndTransformers";
 
-const fetchPackageVersion = stub();
-const proxyquire = proxyquireModule.noPreserveCache();
-
-const { checkAdapterName } = proxyquire<
-	typeof import("./actionsAndTransformers")
->("./actionsAndTransformers", {
-	"./packageVersions": {
-		fetchPackageVersion,
-	},
-});
+const checkAdapterExistence = stub<string[], Promise<CheckResult>>();
 
 describe("actionsAndTransformers/checkMinSelections()", () => {
 	it("should return true if the answers array has at least <min> entries", async () => {
@@ -39,7 +31,7 @@ describe("actionsAndTransformers/checkMinSelections()", () => {
 });
 
 describe("actionsAndTransformers/checkAdapterName()", () => {
-	beforeEach(() => fetchPackageVersion.reset());
+	beforeEach(() => checkAdapterExistence.reset());
 
 	it("should not accept empty names", async () => {
 		const forbiddenNames = ["", " ", "\t"];
@@ -74,21 +66,26 @@ describe("actionsAndTransformers/checkAdapterName()", () => {
 	});
 
 	it("should return an error if the adapter already exists", async () => {
-		fetchPackageVersion.resolves("1.2.3");
-		const result = await checkAdapterName("foo");
+		checkAdapterExistence.resolves(
+			"The adapter ioBroker.foo already exists!",
+		);
+		const result = await checkAdapterName("foo", {
+			checkAdapterExistence,
+		});
 		result.should.be.a("string").and.match(/already exists/);
 	});
 
-	it("should not return an error if the adapter exists and the check is skipped", async () => {
-		fetchPackageVersion.resolves("1.2.3");
+	it("should not return an error if the check is skipped", async () => {
 		await checkAdapterName("foo", {
-			skipAdapterExistenceCheck: true,
+			checkAdapterExistence: undefined,
 		}).should.become(true);
 	});
 
 	it("should return true otherwise", async () => {
-		fetchPackageVersion.rejects("404");
-		await checkAdapterName("foo").should.become(true);
+		checkAdapterExistence.resolves(true);
+		await checkAdapterName("foo", {
+			checkAdapterExistence,
+		}).should.become(true);
 	});
 });
 
diff --git a/src/lib/core/questions.test.ts b/src/lib/core/questions.test.ts
new file mode 100644
index 0000000..16140aa
--- /dev/null
+++ b/src/lib/core/questions.test.ts
@@ -0,0 +1,95 @@
+import { expect } from "chai";
+import { questions, testCondition } from "./questions";
+
+describe("questions/questions", () => {
+	it("should be an array of question objects", () => {
+		questions.should.be.an("array");
+		questions.forEach((q) => {
+			q.should.have.property("type");
+			expect(typeof q.type).to.equal("string");
+		});
+	});
+});
+
+describe("questions/testCondition()", () => {
+	it("should return true if a question has no condition", () => {
+		testCondition(undefined, {}).should.be.true;
+		testCondition(null as any, {}).should.be.true;
+	});
+
+	it(`for a "value" condition, it should return whether the answer equals the desired value`, () => {
+		testCondition(
+			{ name: "prop", value: "foo" },
+			{ prop: "foo" },
+		).should.equal(true);
+
+		testCondition(
+			{ name: "prop", value: "bar" },
+			{ prop: "foo" },
+		).should.equal(false);
+	});
+
+	it(`for a "contains" condition, it should return whether the answer array contains the desired value`, () => {
+		testCondition(
+			{ name: "prop", contains: "foo" },
+			{ prop: ["bar", "foo"] },
+		).should.equal(true);
+
+		testCondition(
+			{ name: "prop", contains: "baz" },
+			{ prop: ["bar", "foo"] },
+		).should.equal(false);
+	});
+
+	it(`for a "doesNotContain" condition, it should return whether the answer array contains the desired value`, () => {
+		testCondition(
+			{ name: "prop", doesNotContain: "foo" },
+			{ prop: ["bar", "foo"] },
+		).should.equal(false);
+
+		testCondition(
+			{ name: "prop", doesNotContain: "baz" },
+			{ prop: ["bar", "foo"] },
+		).should.equal(true);
+	});
+
+	it("should return false for invalid conditions", () => {
+		testCondition({ name: "prop" } as any, {
+			prop: ["bar", "foo"],
+		}).should.equal(false);
+	});
+
+	it("should combine a condition array using AND", () => {
+		testCondition(
+			[
+				{ name: "prop1", doesNotContain: "baz" },
+				{ name: "prop2", value: true },
+			],
+			{
+				prop1: ["bar", "foo"],
+				prop2: true,
+			},
+		).should.equal(true);
+
+		testCondition(
+			[
+				{ name: "prop1", doesNotContain: "baz" },
+				{ name: "prop2", value: false },
+			],
+			{
+				prop1: ["bar", "foo"],
+				prop2: true,
+			},
+		).should.equal(false);
+	});
+});
+
+describe("questions/checkAnswers()", () => {
+	it.skip("TODO: This is already tested by test:baselines");
+});
+describe("questions/formatAnswers()", () => {
+	it.skip("TODO: This is already tested by test:baselines");
+});
+describe("questions/validateAnswers()", () => {
+	it.skip("TODO: This is already tested by test:baselines");
+});
diff --git a/src/lib/createAdapter.test.ts b/src/lib/createAdapter.test.ts
index 051d427..1b8aa9b 100644
--- a/src/lib/createAdapter.test.ts
+++ b/src/lib/createAdapter.test.ts
@@ -1,7 +1,6 @@
 import * as path from "path";
 import * as proxyquireModule from "proxyquire";
 import { stub } from "sinon";
-import { testCondition } from "./createAdapter";
 const proxyquire = proxyquireModule.noPreserveCache();
 
 const fsStub = {
@@ -21,79 +20,6 @@ const { readFile, readFileFromRootDir } = proxyquire<
 	"fs-extra": fsStub,
 });
 
-describe("createAdapter/testCondition()", () => {
-	it("should return true if a question has no condition", () => {
-		testCondition(undefined, {}).should.be.true;
-		testCondition(null as any, {}).should.be.true;
-	});
-
-	it(`for a "value" condition, it should return whether the answer equals the desired value`, () => {
-		testCondition(
-			{ name: "prop", value: "foo" },
-			{ prop: "foo" },
-		).should.equal(true);
-
-		testCondition(
-			{ name: "prop", value: "bar" },
-			{ prop: "foo" },
-		).should.equal(false);
-	});
-
-	it(`for a "contains" condition, it should return whether the answer array contains the desired value`, () => {
-		testCondition(
-			{ name: "prop", contains: "foo" },
-			{ prop: ["bar", "foo"] },
-		).should.equal(true);
-
-		testCondition(
-			{ name: "prop", contains: "baz" },
-			{ prop: ["bar", "foo"] },
-		).should.equal(false);
-	});
-
-	it(`for a "doesNotContain" condition, it should return whether the answer array contains the desired value`, () => {
-		testCondition(
-			{ name: "prop", doesNotContain: "foo" },
-			{ prop: ["bar", "foo"] },
-		).should.equal(false);
-
-		testCondition(
-			{ name: "prop", doesNotContain: "baz" },
-			{ prop: ["bar", "foo"] },
-		).should.equal(true);
-	});
-
-	it("should return false for invalid conditions", () => {
-		testCondition({ name: "prop" } as any, {
-			prop: ["bar", "foo"],
-		}).should.equal(false);
-	});
-
-	it("should combine a condition array using AND", () => {
-		testCondition(
-			[
-				{ name: "prop1", doesNotContain: "baz" },
-				{ name: "prop2", value: true },
-			],
-			{
-				prop1: ["bar", "foo"],
-				prop2: true,
-			},
-		).should.equal(true);
-
-		testCondition(
-			[
-				{ name: "prop1", doesNotContain: "baz" },
-				{ name: "prop2", value: false },
-			],
-			{
-				prop1: ["bar", "foo"],
-				prop2: true,
-			},
-		).should.equal(false);
-	});
-});
-
 describe("createAdapter/readFile()", () => {
 	beforeEach(() => fsStub.reset());
 
diff --git a/src/lib/migrationContext.test.ts b/src/lib/localMigrationContext.test.ts
similarity index 78%
rename from src/lib/migrationContext.test.ts
rename to src/lib/localMigrationContext.test.ts
index c7ea34d..d233664 100644
--- a/src/lib/migrationContext.test.ts
+++ b/src/lib/localMigrationContext.test.ts
@@ -1,51 +1,51 @@
 import { expect } from "chai";
-import { MigrationContext } from "./migrationContext";
+import { LocalMigrationContext } from "./localMigrationContext";
 import path = require("path");
 
-describe("directoryExists()", () => {
+describe("LocalMigrationContext.directoryExists()", () => {
 	it("should return true if the directory exists", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.directoryExists(".")).to.be.true;
 		expect(await context.directoryExists("../..")).to.be.true;
 		expect(await context.directoryExists("../../test")).to.be.true;
 	});
 
 	it("should return false if the directory doesn't exists", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.directoryExists("foo")).to.be.false;
 		expect(await context.directoryExists("../bar")).to.be.false;
 	});
 
 	it("should return false if it isn't a directory", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.directoryExists("../../package.json")).to.be.false;
 		expect(await context.directoryExists("../cli.ts")).to.be.false;
 	});
 });
 
-describe("fileExists()", () => {
+describe("LocalMigrationContext.fileExists()", () => {
 	it("should return true if the file exists", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.fileExists("../../package.json")).to.be.true;
 		expect(await context.fileExists("../cli.ts")).to.be.true;
 	});
 
 	it("should return false if the file doesn't exists", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.fileExists("../foo.ts")).to.be.false;
 		expect(await context.fileExists("../../bar.txt")).to.be.false;
 	});
 
 	it("should return false if it isn't a file", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.fileExists("../..")).to.be.false;
 		expect(await context.fileExists("../../test")).to.be.false;
 	});
 });
 
-describe("hasFilesWithExtension()", () => {
+describe("LocalMigrationContext.hasFilesWithExtension()", () => {
 	it("should return true if files exist", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.hasFilesWithExtension("../..", ".json")).to.be
 			.true;
 		expect(await context.hasFilesWithExtension("..", ".ts")).to.be.true;
@@ -59,7 +59,7 @@ describe("hasFilesWithExtension()", () => {
 	});
 
 	it("should return false if no files exist", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.hasFilesWithExtension("..", ".xls")).to.be.false;
 		expect(await context.hasFilesWithExtension("..", ".dts")).to.be.false;
 		expect(
@@ -72,16 +72,16 @@ describe("hasFilesWithExtension()", () => {
 	});
 
 	it("should return false if the directory doesn't exist", async () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		expect(await context.hasFilesWithExtension("foo", ".json")).to.be.false;
 		expect(await context.hasFilesWithExtension("../bar", ".ts")).to.be
 			.false;
 	});
 });
 
-describe("hasDevDependency()", () => {
+describe("LocalMigrationContext.hasDevDependency()", () => {
 	it("should return true if the dependency exists", () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		context.packageJson = {
 			devDependencies: {
 				gulp: "^3.9.1",
@@ -95,7 +95,7 @@ describe("hasDevDependency()", () => {
 	});
 
 	it("should return false if the dependency doesn't exists", () => {
-		const context = new MigrationContext(__dirname);
+		const context = new LocalMigrationContext(__dirname);
 		context.packageJson = {
 			devDependencies: {
 				gulp: "^3.9.1",
@@ -108,12 +108,12 @@ describe("hasDevDependency()", () => {
 	});
 });
 
-describe("getMainFileContent()", () => {
+describe("LocalMigrationContext.getMainFileContent()", () => {
 	if (!process.env.CI) {
 		// not working in GH action as we don't compile the JS code there
 		it("should return the contents of the TS file if a main JS file is found with a corresponding TS file", async () => {
 			const baseDir = path.resolve(__dirname, "../..");
-			const context = new MigrationContext(baseDir);
+			const context = new LocalMigrationContext(baseDir);
 			context.packageJson = {
 				main: "build/src/cli.js",
 			};
@@ -126,7 +126,7 @@ describe("getMainFileContent()", () => {
 
 	it("should return the contents of the main file if a main file is found with no corresponding TS file", async () => {
 		const baseDir = path.resolve(__dirname, "../..");
-		const context = new MigrationContext(baseDir);
+		const context = new LocalMigrationContext(baseDir);
 		context.packageJson = {
 			main: "bin/create-adapter.js",
 		};
@@ -138,7 +138,7 @@ describe("getMainFileContent()", () => {
 
 	it("should return an empty string if no main file is found", async () => {
 		const baseDir = path.resolve(__dirname, "../..");
-		const context = new MigrationContext(baseDir);
+		const context = new LocalMigrationContext(baseDir);
 		context.packageJson = {
 			main: "foo/bar.js",
 		};
@@ -147,12 +147,12 @@ describe("getMainFileContent()", () => {
 });
 
 // not working in GH action - but you can still use this test locally
-describe("analyzeCode()", () => {
+describe("LocalMigrationContext.analyzeCode()", () => {
 	if (!process.env.CI) {
 		// not working in GH action as we don't compile the JS code there
 		it("should return true the first string occurs more than the second", async () => {
 			const baseDir = path.resolve(__dirname, "../..");
-			const context = new MigrationContext(baseDir);
+			const context = new LocalMigrationContext(baseDir);
 			context.packageJson = {
 				main: "build/src/cli.js",
 			};
@@ -161,7 +161,7 @@ describe("analyzeCode()", () => {
 		});
 		it("should return false the first string occurs less often than the second", async () => {
 			const baseDir = path.resolve(__dirname, "../..");
-			const context = new MigrationContext(baseDir);
+			const context = new LocalMigrationContext(baseDir);
 			context.packageJson = {
 				main: "build/src/cli.js",
 			};
diff --git a/src/lib/questions.test.ts b/src/lib/questions.test.ts
deleted file mode 100644
index b8fa701..0000000
--- a/src/lib/questions.test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { expect } from "chai";
-import { isQuestionGroup, questions } from "./questions";
-
-describe("questions/isQuestionGroup()", () => {
-	it("should return false if the argument is null or undefined", () => {
-		isQuestionGroup(undefined).should.be.false;
-		isQuestionGroup(null).should.be.false;
-	});
-
-	it(`should return false if the argument doesn't have a string property "headline"`, () => {
-		for (const arg of [
-			{},
-			[],
-			"whatever",
-			{ foo: "bar" },
-			1,
-			true,
-			{ headline: 1 },
-			{ headline: false },
-		])
-			isQuestionGroup(arg).should.be.false;
-	});
-
-	it(`should return false if the argument doesn't have an array property "questions"`, () => {
-		for (const arg of [
-			{ headline: "headline" },
-			{ headline: "headline", questions: {} },
-			{ headline: "headline", questions: 1 },
-			{ headline: "headline", questions: false },
-		])
-			isQuestionGroup(arg).should.be.false;
-	});
-
-	it("should return true otherwise", () => {
-		isQuestionGroup({ headline: "string", questions: [] }).should.be.true;
-	});
-});
-
-describe("questions/questions", () => {
-	it("should be an array of question objects", () => {
-		questions.should.be.an("array");
-		questions.forEach((q) => {
-			q.should.have.property("type");
-			expect(typeof q.type).to.equal("string");
-		});
-	});
-});
-
-describe("questions/checkAnswers()", () => {
-	it.skip("TODO: This is already tested by test:baselines");
-});
-describe("questions/formatAnswers()", () => {
-	it.skip("TODO: This is already tested by test:baselines");
-});
-describe("questions/validateAnswers()", () => {
-	it.skip("TODO: This is already tested by test:baselines");
-});
diff --git a/test/baselines/TS_Prettier/package.json b/test/baselines/TS_Prettier/package.json
index 21e7ec6..cce37b8 100644
--- a/test/baselines/TS_Prettier/package.json
+++ b/test/baselines/TS_Prettier/package.json
@@ -26,17 +26,17 @@
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
-    "@typescript-eslint/eslint-plugin": "^4.18.0",
-    "@typescript-eslint/parser": "^4.18.0",
+    "@typescript-eslint/eslint-plugin": "^4.20.0",
+    "@typescript-eslint/parser": "^4.20.0",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
+    "eslint": "^7.23.0",
     "eslint-config-prettier": "^8.1.0",
     "eslint-plugin-prettier": "^3.3.1",
     "gulp": "^4.0.2",
@@ -44,8 +44,8 @@
     "prettier": "^2.2.1",
     "proxyquire": "^2.1.3",
     "rimraf": "^3.0.2",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "source-map-support": "^0.5.19",
     "ts-node": "^9.1.1",
     "typescript": "^4.2.3"
diff --git a/test/baselines/TS_SingleQuotes/src/main.ts b/test/baselines/TS_SingleQuotes/src/main.ts
index b2121c9..f985622 100644
--- a/test/baselines/TS_SingleQuotes/src/main.ts
+++ b/test/baselines/TS_SingleQuotes/src/main.ts
@@ -1,5 +1,5 @@
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/.create-adapter.json b/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/.create-adapter.json
index e343d54..9cbb029 100644
--- a/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/.create-adapter.json
+++ b/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/.create-adapter.json
@@ -27,5 +27,5 @@
     "ci": "gh-actions",
     "dependabot": "yes",
     "license": "Apache License 2.0",
-    "creatorVersion": "1.31.0"
+    "creatorVersion": "1.32.0"
 }
\ No newline at end of file
diff --git a/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/main.js b/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/main.js
index 9ff19d7..eee9f93 100644
--- a/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/main.js
+++ b/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/main.js
@@ -1,7 +1,7 @@
 'use strict';
 
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/package.json b/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/package.json
index 5a08ed3..062de68 100644
--- a/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/package.json
+++ b/test/baselines/adapter_JS_ES6Class_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/package.json
@@ -26,20 +26,20 @@
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
+    "eslint": "^7.23.0",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "proxyquire": "^2.1.3",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "typescript": "^4.2.3"
   },
   "main": "main.js",
diff --git a/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/.create-adapter.json b/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/.create-adapter.json
index 84720ac..fd21d75 100644
--- a/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/.create-adapter.json
+++ b/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/.create-adapter.json
@@ -27,5 +27,5 @@
     "ci": "gh-actions",
     "dependabot": "yes",
     "license": "Apache License 2.0",
-    "creatorVersion": "1.31.0"
+    "creatorVersion": "1.32.0"
 }
\ No newline at end of file
diff --git a/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/main.js b/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/main.js
index 1ca96cc..a356275 100644
--- a/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/main.js
+++ b/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/main.js
@@ -1,7 +1,7 @@
 'use strict';
 
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/package.json b/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/package.json
index 5a08ed3..062de68 100644
--- a/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/package.json
+++ b/test/baselines/adapter_JS_ESLint_TypeChecking_Spaces_SingleQuotes_Apache-2.0/package.json
@@ -26,20 +26,20 @@
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
+    "eslint": "^7.23.0",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "proxyquire": "^2.1.3",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "typescript": "^4.2.3"
   },
   "main": "main.js",
diff --git a/test/baselines/adapter_JS_React/.create-adapter.json b/test/baselines/adapter_JS_React/.create-adapter.json
index 947a1de..a593692 100644
--- a/test/baselines/adapter_JS_React/.create-adapter.json
+++ b/test/baselines/adapter_JS_React/.create-adapter.json
@@ -26,5 +26,5 @@
 	"ci": "gh-actions",
 	"dependabot": "yes",
 	"license": "MIT License",
-	"creatorVersion": "1.31.0"
+	"creatorVersion": "1.32.0"
 }
\ No newline at end of file
diff --git a/test/baselines/adapter_JS_React/main.js b/test/baselines/adapter_JS_React/main.js
index d5ef3f4..0efef2b 100644
--- a/test/baselines/adapter_JS_React/main.js
+++ b/test/baselines/adapter_JS_React/main.js
@@ -1,7 +1,7 @@
 "use strict";
 
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/adapter_JS_React/package.json b/test/baselines/adapter_JS_React/package.json
index ac7dac4..14feb5c 100644
--- a/test/baselines/adapter_JS_React/package.json
+++ b/test/baselines/adapter_JS_React/package.json
@@ -22,9 +22,9 @@
     "@iobroker/adapter-core": "^2.4.0"
   },
   "devDependencies": {
-    "@babel/cli": "^7.13.10",
-    "@babel/core": "^7.13.10",
-    "@iobroker/adapter-react": "^1.6.3",
+    "@babel/cli": "^7.13.14",
+    "@babel/core": "^7.13.14",
+    "@iobroker/adapter-react": "^1.6.7",
     "@iobroker/testing": "^2.4.4",
     "@material-ui/core": "^4.11.3",
     "@material-ui/icons": "^4.11.2",
@@ -32,8 +32,8 @@
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
-    "eslint-plugin-react": "^7.22.0",
+    "eslint": "^7.23.0",
+    "eslint-plugin-react": "^7.23.1",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "parcel-bundler": "^1.12.5",
@@ -41,8 +41,8 @@
     "react-dom": "^16.14.0",
     "react-icons": "^4.2.0",
     "react": "^16.14.0",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0"
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0"
   },
   "main": "main.js",
   "scripts": {
diff --git a/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/.create-adapter.json b/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/.create-adapter.json
index 9e6f9e2..fdde158 100644
--- a/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/.create-adapter.json
+++ b/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/.create-adapter.json
@@ -26,5 +26,5 @@
 	"ci": "gh-actions",
 	"dependabot": "yes",
 	"license": "MIT License",
-	"creatorVersion": "1.31.0"
+	"creatorVersion": "1.32.0"
 }
\ No newline at end of file
diff --git a/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/package.json b/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/package.json
index 9a14b17..48573fe 100644
--- a/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/package.json
+++ b/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/package.json
@@ -26,23 +26,23 @@
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
-    "@typescript-eslint/eslint-plugin": "^4.18.0",
-    "@typescript-eslint/parser": "^4.18.0",
+    "@typescript-eslint/eslint-plugin": "^4.20.0",
+    "@typescript-eslint/parser": "^4.20.0",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
+    "eslint": "^7.23.0",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "proxyquire": "^2.1.3",
     "rimraf": "^3.0.2",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "source-map-support": "^0.5.19",
     "ts-node": "^9.1.1",
     "typescript": "^4.2.3"
diff --git a/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/src/main.ts b/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/src/main.ts
index 717a946..08ff94d 100644
--- a/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/src/main.ts
+++ b/test/baselines/adapter_TS_ES6Class_ESLint_Tabs_DoubleQuotes_MIT/src/main.ts
@@ -1,5 +1,5 @@
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/.create-adapter.json b/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/.create-adapter.json
index d1999fb..ca6931a 100644
--- a/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/.create-adapter.json
+++ b/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/.create-adapter.json
@@ -29,5 +29,5 @@
 	"ci": "gh-actions",
 	"dependabot": "yes",
 	"license": "MIT License",
-	"creatorVersion": "1.31.0"
+	"creatorVersion": "1.32.0"
 }
\ No newline at end of file
diff --git a/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/package.json b/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/package.json
index 9a14b17..48573fe 100644
--- a/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/package.json
+++ b/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/package.json
@@ -26,23 +26,23 @@
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
-    "@typescript-eslint/eslint-plugin": "^4.18.0",
-    "@typescript-eslint/parser": "^4.18.0",
+    "@typescript-eslint/eslint-plugin": "^4.20.0",
+    "@typescript-eslint/parser": "^4.20.0",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
+    "eslint": "^7.23.0",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "proxyquire": "^2.1.3",
     "rimraf": "^3.0.2",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "source-map-support": "^0.5.19",
     "ts-node": "^9.1.1",
     "typescript": "^4.2.3"
diff --git a/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/src/main.ts b/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/src/main.ts
index a005262..2689121 100644
--- a/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/src/main.ts
+++ b/test/baselines/adapter_TS_ESLint_Tabs_DoubleQuotes_MIT/src/main.ts
@@ -1,5 +1,5 @@
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/adapter_TS_React/.create-adapter.json b/test/baselines/adapter_TS_React/.create-adapter.json
index 47541b4..b79c5e6 100644
--- a/test/baselines/adapter_TS_React/.create-adapter.json
+++ b/test/baselines/adapter_TS_React/.create-adapter.json
@@ -26,5 +26,5 @@
 	"ci": "gh-actions",
 	"dependabot": "yes",
 	"license": "MIT License",
-	"creatorVersion": "1.31.0"
+	"creatorVersion": "1.32.0"
 }
\ No newline at end of file
diff --git a/test/baselines/adapter_TS_React/package.json b/test/baselines/adapter_TS_React/package.json
index 7154810..a4f06a0 100644
--- a/test/baselines/adapter_TS_React/package.json
+++ b/test/baselines/adapter_TS_React/package.json
@@ -22,32 +22,32 @@
     "@iobroker/adapter-core": "^2.4.0"
   },
   "devDependencies": {
-    "@babel/cli": "^7.13.10",
-    "@babel/core": "^7.13.10",
+    "@babel/cli": "^7.13.14",
+    "@babel/core": "^7.13.14",
     "@babel/plugin-proposal-decorators": "^7.13.5",
-    "@babel/preset-env": "^7.13.10",
+    "@babel/preset-env": "^7.13.12",
     "@babel/preset-typescript": "^7.13.0",
-    "@iobroker/adapter-react": "^1.6.3",
+    "@iobroker/adapter-react": "^1.6.7",
     "@iobroker/testing": "^2.4.4",
     "@material-ui/core": "^4.11.3",
     "@material-ui/icons": "^4.11.2",
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
-    "@types/react-dom": "^16.9.11",
+    "@types/react-dom": "^16.9.12",
     "@types/react": "^16.14.5",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
-    "@typescript-eslint/eslint-plugin": "^4.18.0",
-    "@typescript-eslint/parser": "^4.18.0",
+    "@typescript-eslint/eslint-plugin": "^4.20.0",
+    "@typescript-eslint/parser": "^4.20.0",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
-    "eslint-plugin-react": "^7.22.0",
+    "eslint": "^7.23.0",
+    "eslint-plugin-react": "^7.23.1",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "parcel-bundler": "^1.12.5",
@@ -56,8 +56,8 @@
     "react-icons": "^4.2.0",
     "react": "^16.14.0",
     "rimraf": "^3.0.2",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "source-map-support": "^0.5.19",
     "ts-node": "^9.1.1",
     "typescript": "^4.2.3"
diff --git a/test/baselines/adapter_TS_React/src/main.ts b/test/baselines/adapter_TS_React/src/main.ts
index 717a946..08ff94d 100644
--- a/test/baselines/adapter_TS_React/src/main.ts
+++ b/test/baselines/adapter_TS_React/src/main.ts
@@ -1,5 +1,5 @@
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/connectionIndicator_yes/src/main.ts b/test/baselines/connectionIndicator_yes/src/main.ts
index 59220fc..70f443b 100644
--- a/test/baselines/connectionIndicator_yes/src/main.ts
+++ b/test/baselines/connectionIndicator_yes/src/main.ts
@@ -1,5 +1,5 @@
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/contributors/package.json b/test/baselines/contributors/package.json
index c665c96..1f0ab41 100644
--- a/test/baselines/contributors/package.json
+++ b/test/baselines/contributors/package.json
@@ -34,23 +34,23 @@
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
-    "@typescript-eslint/eslint-plugin": "^4.18.0",
-    "@typescript-eslint/parser": "^4.18.0",
+    "@typescript-eslint/eslint-plugin": "^4.20.0",
+    "@typescript-eslint/parser": "^4.20.0",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
+    "eslint": "^7.23.0",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "proxyquire": "^2.1.3",
     "rimraf": "^3.0.2",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "source-map-support": "^0.5.19",
     "ts-node": "^9.1.1",
     "typescript": "^4.2.3"
diff --git a/test/baselines/customAdapterSettings/src/main.ts b/test/baselines/customAdapterSettings/src/main.ts
index 4f62482..a838ba1 100644
--- a/test/baselines/customAdapterSettings/src/main.ts
+++ b/test/baselines/customAdapterSettings/src/main.ts
@@ -1,5 +1,5 @@
 /*
- * Created with @iobroker/create-adapter v1.31.0
+ * Created with @iobroker/create-adapter v1.32.0
  */
 
 // The adapter-core module gives you access to the core ioBroker functions
diff --git a/test/baselines/git_SSH/package.json b/test/baselines/git_SSH/package.json
index 6f2c6d8..eda4027 100644
--- a/test/baselines/git_SSH/package.json
+++ b/test/baselines/git_SSH/package.json
@@ -26,23 +26,23 @@
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
-    "@typescript-eslint/eslint-plugin": "^4.18.0",
-    "@typescript-eslint/parser": "^4.18.0",
+    "@typescript-eslint/eslint-plugin": "^4.20.0",
+    "@typescript-eslint/parser": "^4.20.0",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
+    "eslint": "^7.23.0",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "proxyquire": "^2.1.3",
     "rimraf": "^3.0.2",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "source-map-support": "^0.5.19",
     "ts-node": "^9.1.1",
     "typescript": "^4.2.3"
diff --git a/test/baselines/keywords/package.json b/test/baselines/keywords/package.json
index d15a3fb..0e072b1 100644
--- a/test/baselines/keywords/package.json
+++ b/test/baselines/keywords/package.json
@@ -27,23 +27,23 @@
     "@types/chai": "^4.2.15",
     "@types/chai-as-promised": "^7.1.3",
     "@types/gulp": "^4.0.8",
-    "@types/mocha": "^8.2.1",
-    "@types/node": "^14.14.35",
+    "@types/mocha": "^8.2.2",
+    "@types/node": "^14.14.37",
     "@types/proxyquire": "^1.3.28",
     "@types/sinon": "^9.0.11",
     "@types/sinon-chai": "^3.2.5",
-    "@typescript-eslint/eslint-plugin": "^4.18.0",
-    "@typescript-eslint/parser": "^4.18.0",
+    "@typescript-eslint/eslint-plugin": "^4.20.0",
+    "@typescript-eslint/parser": "^4.20.0",
     "axios": "^0.21.1",
     "chai": "^4.3.4",
     "chai-as-promised": "^7.1.1",
-    "eslint": "^7.22.0",
+    "eslint": "^7.23.0",
     "gulp": "^4.0.2",
     "mocha": "^8.3.2",
     "proxyquire": "^2.1.3",
     "rimraf": "^3.0.2",
-    "sinon": "^9.2.4",
-    "sinon-chai": "^3.5.0",
+    "sinon": "^10.0.0",
+    "sinon-chai": "^3.6.0",
     "source-map-support": "^0.5.19",
     "ts-node": "^9.1.1",
     "typescript": "^4.2.3"
diff --git a/test/baselines/vis_Widget/.create-adapter.json b/test/baselines/vis_Widget/.create-adapter.json
index bfe4377..81db34d 100644
--- a/test/baselines/vis_Widget/.create-adapter.json
+++ b/test/baselines/vis_Widget/.create-adapter.json
@@ -14,5 +14,5 @@
 	"ci": "gh-actions",
 	"dependabot": "yes",
 	"license": "MIT License",
-	"creatorVersion": "1.31.0"
+	"creatorVersion": "1.32.0"
 }
\ No newline at end of file
diff --git a/test/baselines/vis_Widget_Travis/.create-adapter.json b/test/baselines/vis_Widget_Travis/.create-adapter.json
index 74fc21d..fc2fbd5 100644
--- a/test/baselines/vis_Widget_Travis/.create-adapter.json
+++ b/test/baselines/vis_Widget_Travis/.create-adapter.json
@@ -14,5 +14,5 @@
 	"ci": "travis",
 	"dependabot": "yes",
 	"license": "MIT License",
-	"creatorVersion": "1.31.0"
+	"creatorVersion": "1.32.0"
 }
\ No newline at end of file
diff --git a/test/create-adapter.test.ts b/test/create-adapter.test.ts
index c517bfe..adde8d0 100644
--- a/test/create-adapter.test.ts
+++ b/test/create-adapter.test.ts
@@ -6,8 +6,8 @@ import * as fs from "fs-extra";
 import { validate as validateJSON } from "jsonschema";
 import * as path from "path";
 import { createAdapter } from "../src/index";
+import { Answers } from "../src/lib/core/questions";
 import { File, writeFiles } from "../src/lib/createAdapter";
-import { Answers } from "../src/lib/questions";
 
 const baselineDir = path.join(__dirname, "../test/baselines");
 let ioPackageSchema: unknown;