From ab252951f9dd54a57cb4f6be9adf49a9c8e7bc46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:17:36 +0000 Subject: [PATCH 1/3] Initial plan From f2996a5f03d1be4c4e2ef1962f0135256c488484 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:27:10 +0000 Subject: [PATCH 2/3] Implement support for jsonConfig and React tabs simultaneously Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- dist/index.js | 133 ++++++++++++++++++++++++++++++++++++++-- src/index.ts | 165 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 287 insertions(+), 11 deletions(-) diff --git a/dist/index.js b/dist/index.js index 59c315cc..f849fe1b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -282,6 +282,35 @@ class DevServer { readPackageJson() { return (0, fs_extra_1.readJson)(path.join(this.rootDir, 'package.json')); } + async readIoPackageJson() { + return (0, fs_extra_1.readJson)(path.join(this.rootDir, 'io-package.json')); + } + async getAdapterUiCapabilities() { + const hasJsonConfig = !!this.getJsonConfigPath(); + // Check if adapter has React tab or HTML config by examining: + // 1. package.json scripts for React builds + // 2. Admin files existence + let hasReactTab = false; + let hasHtmlConfig = false; + if (!this.isJSController()) { + const pkg = await this.readPackageJson(); + const scripts = pkg.scripts; + if (scripts && (scripts['watch:react'] || scripts['watch:parcel'])) { + hasReactTab = true; + } + // Check for HTML config files + const htmlConfigPath = path.resolve(this.rootDir, 'admin/index.html'); + if ((0, fs_extra_1.existsSync)(htmlConfigPath)) { + hasHtmlConfig = true; + } + } + this.log.debug(`UI capabilities: jsonConfig=${hasJsonConfig}, reactTab=${hasReactTab}, htmlConfig=${hasHtmlConfig}`); + return { + hasJsonConfig, + hasReactTab, + hasHtmlConfig, + }; + } getPort(adminPort, offset) { let port = adminPort + offset; if (port > 65000) { @@ -545,13 +574,21 @@ class DevServer { ws: true, })); } - else if (this.getJsonConfigPath()) { - // JSON config - await this.createJsonConfigProxy(app, this.config); - } else { - // HTML or React config - await this.createHtmlConfigProxy(app, this.config); + // Determine what UI capabilities this adapter needs + const uiCapabilities = await this.getAdapterUiCapabilities(); + if (uiCapabilities.hasJsonConfig && (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig)) { + // Adapter uses both jsonConfig AND React/HTML - support both simultaneously + await this.createCombinedConfigProxy(app, this.config, uiCapabilities); + } + else if (uiCapabilities.hasJsonConfig) { + // JSON config only + await this.createJsonConfigProxy(app, this.config); + } + else { + // HTML or React config only + await this.createHtmlConfigProxy(app, this.config); + } } // start express this.log.notice(`Starting web server on port ${this.config.adminPort}`); @@ -693,6 +730,90 @@ class DevServer { ws: true, })); } + async createCombinedConfigProxy(app, config, uiCapabilities) { + // This method combines the functionality of createJsonConfigProxy and createHtmlConfigProxy + // to support adapters that use both jsonConfig and React/HTML tabs + const pathRewrite = {}; + const browserSyncPort = this.getPort(config.adminPort, HIDDEN_BROWSER_SYNC_PORT_OFFSET); + const adminUrl = `http://127.0.0.1:${this.getPort(config.adminPort, HIDDEN_ADMIN_PORT_OFFSET)}`; + // Handle React build watching if needed (from createHtmlConfigProxy) + let hasReact = false; + if (uiCapabilities.hasReactTab && !this.isJSController()) { + const pkg = await this.readPackageJson(); + const scripts = pkg.scripts; + if (scripts) { + if (scripts['watch:react']) { + await this.startReact('watch:react'); + hasReact = true; + if ((0, fs_extra_1.existsSync)(path.resolve(this.rootDir, 'admin/.watch'))) { + // rewrite the build directory to the .watch directory, + // because "watch:react" no longer updates the build directory automatically + pathRewrite[`^/adapter/${this.adapterName}/build/`] = '/.watch/'; + } + } + else if (scripts['watch:parcel']) { + // use React with legacy script name + await this.startReact('watch:parcel'); + hasReact = true; + } + } + } + // Start browser-sync (from both methods) + const bs = this.startBrowserSync(browserSyncPort, hasReact); + // Handle jsonConfig file watching if present (from createJsonConfigProxy) + if (uiCapabilities.hasJsonConfig) { + const jsonConfigFile = this.getJsonConfigPath(); + bs.watch(jsonConfigFile, undefined, async (e) => { + var _a; + if (e === 'change') { + const content = await (0, fs_extra_1.readFile)(jsonConfigFile); + (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify([ + 3, + 46, + 'writeFile', + [ + `${this.adapterName}.admin`, + path.basename(jsonConfigFile), + Buffer.from(content).toString('base64'), + ], + ])); + } + }); + // "proxy" for the main page which injects our script (from createJsonConfigProxy) + app.get('/', async (_req, res) => { + const { data } = await axios_1.default.get(adminUrl); + res.send((0, jsonConfig_1.injectCode)(data, this.adapterName, path.basename(jsonConfigFile))); + }); + } + // Setup proxies similar to both methods + if (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig) { + // browser-sync proxy for adapter files (from createHtmlConfigProxy) + const adminPattern = `/adapter/${this.adapterName}/**`; + pathRewrite[`^/adapter/${this.adapterName}/`] = '/'; + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)([adminPattern, '/browser-sync/**'], { + target: `http://127.0.0.1:${browserSyncPort}`, + //ws: true, // can't have two web-socket connections proxying to different locations + pathRewrite, + })); + // admin proxy + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)([`!${adminPattern}`, '!/browser-sync/**'], { + target: adminUrl, + ws: true, + })); + } + else { + // browser-sync proxy (from createJsonConfigProxy) + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)(['/browser-sync/**'], { + target: `http://127.0.0.1:${browserSyncPort}`, + // ws: true, // can't have two web-socket connections proxying to different locations + })); + // admin proxy + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)({ + target: adminUrl, + ws: true, + })); + } + } async copySourcemaps() { const outDir = path.join(this.profileDir, 'node_modules', `iobroker.${this.adapterName}`); this.log.notice(`Creating or patching sourcemaps in ${outDir}`); diff --git a/src/index.ts b/src/index.ts index b53f63e1..1a85ac8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -349,6 +349,48 @@ class DevServer { return readJson(path.join(this.rootDir, 'package.json')); } + private async readIoPackageJson(): Promise { + return readJson(path.join(this.rootDir, 'io-package.json')); + } + + private async getAdapterUiCapabilities(): Promise<{ + hasJsonConfig: boolean; + hasReactTab: boolean; + hasHtmlConfig: boolean; + }> { + const hasJsonConfig = !!this.getJsonConfigPath(); + + // Check if adapter has React tab or HTML config by examining: + // 1. package.json scripts for React builds + // 2. Admin files existence + let hasReactTab = false; + let hasHtmlConfig = false; + + if (!this.isJSController()) { + const pkg = await this.readPackageJson(); + const scripts = pkg.scripts; + if (scripts && (scripts['watch:react'] || scripts['watch:parcel'])) { + hasReactTab = true; + } + + // Check for HTML config files + const htmlConfigPath = path.resolve(this.rootDir, 'admin/index.html'); + if (existsSync(htmlConfigPath)) { + hasHtmlConfig = true; + } + } + + this.log.debug( + `UI capabilities: jsonConfig=${hasJsonConfig}, reactTab=${hasReactTab}, htmlConfig=${hasHtmlConfig}`, + ); + + return { + hasJsonConfig, + hasReactTab, + hasHtmlConfig, + }; + } + private getPort(adminPort: number, offset: number): number { let port = adminPort + offset; if (port > 65000) { @@ -666,12 +708,20 @@ class DevServer { ws: true, }), ); - } else if (this.getJsonConfigPath()) { - // JSON config - await this.createJsonConfigProxy(app, this.config); } else { - // HTML or React config - await this.createHtmlConfigProxy(app, this.config); + // Determine what UI capabilities this adapter needs + const uiCapabilities = await this.getAdapterUiCapabilities(); + + if (uiCapabilities.hasJsonConfig && (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig)) { + // Adapter uses both jsonConfig AND React/HTML - support both simultaneously + await this.createCombinedConfigProxy(app, this.config, uiCapabilities); + } else if (uiCapabilities.hasJsonConfig) { + // JSON config only + await this.createJsonConfigProxy(app, this.config); + } else { + // HTML or React config only + await this.createHtmlConfigProxy(app, this.config); + } } // start express @@ -838,6 +888,111 @@ class DevServer { ); } + private async createCombinedConfigProxy( + app: Application, + config: DevServerConfig, + uiCapabilities: { hasJsonConfig: boolean; hasReactTab: boolean; hasHtmlConfig: boolean }, + ): Promise { + // This method combines the functionality of createJsonConfigProxy and createHtmlConfigProxy + // to support adapters that use both jsonConfig and React/HTML tabs + + const pathRewrite: Record = {}; + const browserSyncPort = this.getPort(config.adminPort, HIDDEN_BROWSER_SYNC_PORT_OFFSET); + const adminUrl = `http://127.0.0.1:${this.getPort(config.adminPort, HIDDEN_ADMIN_PORT_OFFSET)}`; + + // Handle React build watching if needed (from createHtmlConfigProxy) + let hasReact = false; + if (uiCapabilities.hasReactTab && !this.isJSController()) { + const pkg = await this.readPackageJson(); + const scripts = pkg.scripts; + if (scripts) { + if (scripts['watch:react']) { + await this.startReact('watch:react'); + hasReact = true; + + if (existsSync(path.resolve(this.rootDir, 'admin/.watch'))) { + // rewrite the build directory to the .watch directory, + // because "watch:react" no longer updates the build directory automatically + pathRewrite[`^/adapter/${this.adapterName}/build/`] = '/.watch/'; + } + } else if (scripts['watch:parcel']) { + // use React with legacy script name + await this.startReact('watch:parcel'); + hasReact = true; + } + } + } + + // Start browser-sync (from both methods) + const bs = this.startBrowserSync(browserSyncPort, hasReact); + + // Handle jsonConfig file watching if present (from createJsonConfigProxy) + if (uiCapabilities.hasJsonConfig) { + const jsonConfigFile = this.getJsonConfigPath(); + bs.watch(jsonConfigFile, undefined, async e => { + if (e === 'change') { + const content = await readFile(jsonConfigFile); + this.websocket?.send( + JSON.stringify([ + 3, + 46, + 'writeFile', + [ + `${this.adapterName}.admin`, + path.basename(jsonConfigFile), + Buffer.from(content).toString('base64'), + ], + ]), + ); + } + }); + + // "proxy" for the main page which injects our script (from createJsonConfigProxy) + app.get('/', async (_req, res) => { + const { data } = await axios.get(adminUrl); + res.send(injectCode(data, this.adapterName, path.basename(jsonConfigFile))); + }); + } + + // Setup proxies similar to both methods + if (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig) { + // browser-sync proxy for adapter files (from createHtmlConfigProxy) + const adminPattern = `/adapter/${this.adapterName}/**`; + pathRewrite[`^/adapter/${this.adapterName}/`] = '/'; + app.use( + createProxyMiddleware([adminPattern, '/browser-sync/**'], { + target: `http://127.0.0.1:${browserSyncPort}`, + //ws: true, // can't have two web-socket connections proxying to different locations + pathRewrite, + }), + ); + + // admin proxy + app.use( + createProxyMiddleware([`!${adminPattern}`, '!/browser-sync/**'], { + target: adminUrl, + ws: true, + }), + ); + } else { + // browser-sync proxy (from createJsonConfigProxy) + app.use( + createProxyMiddleware(['/browser-sync/**'], { + target: `http://127.0.0.1:${browserSyncPort}`, + // ws: true, // can't have two web-socket connections proxying to different locations + }), + ); + + // admin proxy + app.use( + createProxyMiddleware({ + target: adminUrl, + ws: true, + }), + ); + } + } + private async copySourcemaps(): Promise { const outDir = path.join(this.profileDir, 'node_modules', `iobroker.${this.adapterName}`); this.log.notice(`Creating or patching sourcemaps in ${outDir}`); From 2b99f6e2f61236915456faf0a7c99244ffff5aa5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:35:08 +0000 Subject: [PATCH 3/3] Enhance UI detection with .create-adapter.json, io-package.json adminUi field, and tab file support Co-authored-by: Apollon77 <11976694+Apollon77@users.noreply.github.com> --- dist/index.js | 278 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/index.ts | 172 +++++++++++++++++++++++++++++-- 2 files changed, 433 insertions(+), 17 deletions(-) mode change 100755 => 100644 dist/index.js diff --git a/dist/index.js b/dist/index.js old mode 100755 new mode 100644 index a990bba4..4af802da --- a/dist/index.js +++ b/dist/index.js @@ -293,6 +293,109 @@ class DevServer { readPackageJson() { return (0, fs_extra_1.readJson)(path.join(this.rootDir, 'package.json')); } + async readIoPackageJson() { + return (0, fs_extra_1.readJson)(path.join(this.rootDir, 'io-package.json')); + } + async getAdapterUiCapabilities() { + var _a, _b, _c; + const hasJsonConfig = !!this.getJsonConfigPath(); + // Check if adapter has React tab or HTML config by examining: + // 1. package.json scripts for React builds + // 2. Admin files existence + // 3. .create-adapter.json configuration + // 4. io-package.json adminUi field + // 5. Tab files (tab.html vs jsonTab.json) + let hasReactTab = false; + let hasHtmlConfig = false; + let hasTab = false; + let hasJsonTab = false; + let tabType = 'none'; + if (!this.isJSController()) { + // Check .create-adapter.json if it exists + const createAdapterJsonPath = path.resolve(this.rootDir, '.create-adapter.json'); + if ((0, fs_extra_1.existsSync)(createAdapterJsonPath)) { + try { + const createAdapterConfig = await (0, fs_extra_1.readJson)(createAdapterJsonPath); + this.log.debug(`Found .create-adapter.json: ${JSON.stringify(createAdapterConfig)}`); + // Extract UI hints from create-adapter configuration + if (((_a = createAdapterConfig.adminUi) === null || _a === void 0 ? void 0 : _a.type) === 'react') { + hasReactTab = true; + } + else if (((_b = createAdapterConfig.adminUi) === null || _b === void 0 ? void 0 : _b.type) === 'html') { + hasHtmlConfig = true; + } + } + catch (error) { + this.log.debug(`Failed to read .create-adapter.json: ${error}`); + } + } + // Check io-package.json adminUi field + try { + const ioPackage = await this.readIoPackageJson(); + if ((_c = ioPackage === null || ioPackage === void 0 ? void 0 : ioPackage.common) === null || _c === void 0 ? void 0 : _c.adminUi) { + const adminUi = ioPackage.common.adminUi; + this.log.debug(`Found adminUi configuration: ${JSON.stringify(adminUi)}`); + if (adminUi.config === 'json') { + // Has JSON config (already detected above, but this confirms it) + } + else if (adminUi.config === 'html' || adminUi.config === 'materialize') { + hasHtmlConfig = true; + } + // Check if there are tabs defined + if (adminUi.tab) { + hasTab = true; + if (adminUi.tab === 'json') { + hasJsonTab = true; + tabType = 'json'; + } + else if (adminUi.tab === 'html' || adminUi.tab === 'materialize') { + tabType = 'html'; + } + } + } + } + catch (error) { + this.log.debug(`Failed to read io-package.json: ${error}`); + } + // Check package.json scripts for React builds + const pkg = await this.readPackageJson(); + const scripts = pkg.scripts; + if (scripts && (scripts['watch:react'] || scripts['watch:parcel'])) { + hasReactTab = true; + } + // Check for HTML config files + const htmlConfigPath = path.resolve(this.rootDir, 'admin/index.html'); + if ((0, fs_extra_1.existsSync)(htmlConfigPath)) { + hasHtmlConfig = true; + } + // Check for tab files + const tabHtmlPath = path.resolve(this.rootDir, 'admin/tab.html'); + const jsonTabPath = path.resolve(this.rootDir, 'admin/jsonTab.json'); + const jsonTab5Path = path.resolve(this.rootDir, 'admin/jsonTab.json5'); + if ((0, fs_extra_1.existsSync)(tabHtmlPath)) { + hasTab = true; + if (tabType === 'none') { + tabType = 'html'; + } + } + if ((0, fs_extra_1.existsSync)(jsonTabPath) || (0, fs_extra_1.existsSync)(jsonTab5Path)) { + hasTab = true; + hasJsonTab = true; + if (tabType === 'none') { + tabType = 'json'; + } + } + } + this.log.debug(`UI capabilities: jsonConfig=${hasJsonConfig}, reactTab=${hasReactTab}, htmlConfig=${hasHtmlConfig}, tab=${hasTab}, jsonTab=${hasJsonTab}, tabType=${tabType}`); + return { + hasJsonConfig, + hasReactTab, + hasHtmlConfig, + hasTab, + hasJsonTab, + tabType, + }; + } isTypeScriptMain(mainFile) { return !!(mainFile && mainFile.endsWith('.ts')); } @@ -559,13 +662,22 @@ class DevServer { ws: true, })); } - else if (this.getJsonConfigPath()) { - // JSON config - await this.createJsonConfigProxy(app, this.config, useBrowserSync); - } else { - // HTML or React config - await this.createHtmlConfigProxy(app, this.config, useBrowserSync); + // Determine what UI capabilities this adapter needs + const uiCapabilities = await this.getAdapterUiCapabilities(); + if (uiCapabilities.hasJsonConfig && + (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig || uiCapabilities.hasTab)) { + // Adapter uses both jsonConfig AND React/HTML/tabs - support both simultaneously + await this.createCombinedConfigProxy(app, this.config, uiCapabilities, useBrowserSync); + } + else if (uiCapabilities.hasJsonConfig) { + // JSON config only + await this.createJsonConfigProxy(app, this.config, useBrowserSync); + } + else { + // HTML or React config only + await this.createHtmlConfigProxy(app, this.config, useBrowserSync); + } } // start express this.log.notice(`Starting web server on port ${this.config.adminPort}`); @@ -731,6 +843,160 @@ class DevServer { })); } } + async createCombinedConfigProxy(app, config, uiCapabilities, useBrowserSync = true) { + // This method combines the functionality of createJsonConfigProxy and createHtmlConfigProxy + // to support adapters that use both jsonConfig and React/HTML tabs + const pathRewrite = {}; + const browserSyncPort = this.getPort(config.adminPort, HIDDEN_BROWSER_SYNC_PORT_OFFSET); + const adminUrl = `http://127.0.0.1:${this.getPort(config.adminPort, HIDDEN_ADMIN_PORT_OFFSET)}`; + // Handle React build watching if needed (from createHtmlConfigProxy) + let hasReact = false; + let bs = null; + if (useBrowserSync) { + if (uiCapabilities.hasReactTab && !this.isJSController()) { + const pkg = await this.readPackageJson(); + const scripts = pkg.scripts; + if (scripts) { + if (scripts['watch:react']) { + await this.startReact('watch:react'); + hasReact = true; + if ((0, fs_extra_1.existsSync)(path.resolve(this.rootDir, 'admin/.watch'))) { + // rewrite the build directory to the .watch directory, + // because "watch:react" no longer updates the build directory automatically + pathRewrite[`^/adapter/${this.adapterName}/build/`] = '/.watch/'; + } + } + else if (scripts['watch:parcel']) { + // use React with legacy script name + await this.startReact('watch:parcel'); + hasReact = true; + } + } + } + // Start browser-sync (from both methods) + bs = this.startBrowserSync(browserSyncPort, hasReact); + } + // Handle jsonConfig file watching if present (from createJsonConfigProxy) + if (uiCapabilities.hasJsonConfig) { + const jsonConfigFile = this.getJsonConfigPath(); + if (useBrowserSync && bs) { + bs.watch(jsonConfigFile, undefined, async (e) => { + var _a; + if (e === 'change') { + const content = await (0, fs_extra_1.readFile)(jsonConfigFile); + (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify([ + 3, + 46, + 'writeFile', + [ + `${this.adapterName}.admin`, + path.basename(jsonConfigFile), + Buffer.from(content).toString('base64'), + ], + ])); + } + }); + // "proxy" for the main page which injects our script (from createJsonConfigProxy) + app.get('/', async (_req, res) => { + const { data } = await axios_1.default.get(adminUrl); + res.send((0, jsonConfig_1.injectCode)(data, this.adapterName, path.basename(jsonConfigFile))); + }); + } + } + // Handle tab file watching if present + if (uiCapabilities.hasTab && useBrowserSync && bs) { + if (uiCapabilities.hasJsonTab) { + // Watch JSON tab files + const jsonTabPath = path.resolve(this.rootDir, 'admin/jsonTab.json'); + const jsonTab5Path = path.resolve(this.rootDir, 'admin/jsonTab.json5'); + if ((0, fs_extra_1.existsSync)(jsonTabPath)) { + bs.watch(jsonTabPath, undefined, async (e) => { + var _a; + if (e === 'change') { + const content = await (0, fs_extra_1.readFile)(jsonTabPath); + (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify([ + 3, + 46, + 'writeFile', + [ + `${this.adapterName}.admin`, + 'jsonTab.json', + Buffer.from(content).toString('base64'), + ], + ])); + } + }); + } + if ((0, fs_extra_1.existsSync)(jsonTab5Path)) { + bs.watch(jsonTab5Path, undefined, async (e) => { + var _a; + if (e === 'change') { + const content = await (0, fs_extra_1.readFile)(jsonTab5Path); + (_a = this.websocket) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify([ + 3, + 46, + 'writeFile', + [ + `${this.adapterName}.admin`, + 'jsonTab.json5', + Buffer.from(content).toString('base64'), + ], + ])); + } + }); + } + } + if (uiCapabilities.tabType === 'html') { + // Watch HTML tab files + const tabHtmlPath = path.resolve(this.rootDir, 'admin/tab.html'); + if ((0, fs_extra_1.existsSync)(tabHtmlPath)) { + bs.watch(tabHtmlPath, undefined, (e) => { + if (e === 'change') { + this.log.debug('Tab HTML file changed, reloading browser...'); + // For HTML tabs, we rely on BrowserSync's automatic reload + } + }); + } + } + } + // Setup proxies similar to both methods + if (useBrowserSync) { + if (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig || uiCapabilities.hasTab) { + // browser-sync proxy for adapter files (from createHtmlConfigProxy) + const adminPattern = `/adapter/${this.adapterName}/**`; + pathRewrite[`^/adapter/${this.adapterName}/`] = '/'; + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)([adminPattern, '/browser-sync/**'], { + target: `http://127.0.0.1:${browserSyncPort}`, + //ws: true, // can't have two web-socket connections proxying to different locations + pathRewrite, + })); + // admin proxy + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)([`!${adminPattern}`, '!/browser-sync/**'], { + target: adminUrl, + ws: true, + })); + } + else { + // browser-sync proxy (from createJsonConfigProxy) + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)(['/browser-sync/**'], { + target: `http://127.0.0.1:${browserSyncPort}`, + // ws: true, // can't have two web-socket connections proxying to different locations + })); + // admin proxy + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)({ + target: adminUrl, + ws: true, + })); + } + } + else { + // Direct admin proxy without browser-sync + app.use((0, http_proxy_middleware_1.legacyCreateProxyMiddleware)({ + target: adminUrl, + ws: true, + })); + } + } async copySourcemaps() { const outDir = path.join(this.profileDir, 'node_modules', `iobroker.${this.adapterName}`); this.log.notice(`Creating or patching sourcemaps in ${outDir}`); diff --git a/src/index.ts b/src/index.ts index fe461891..1f97cd06 100644 --- a/src/index.ts +++ b/src/index.ts @@ -362,7 +362,6 @@ class DevServer { return readJson(path.join(this.rootDir, 'package.json')); } -<<<<<<< HEAD private async readIoPackageJson(): Promise { return readJson(path.join(this.rootDir, 'io-package.json')); } @@ -371,16 +370,72 @@ class DevServer { hasJsonConfig: boolean; hasReactTab: boolean; hasHtmlConfig: boolean; + hasTab: boolean; + hasJsonTab: boolean; + tabType: 'html' | 'json' | 'none'; }> { const hasJsonConfig = !!this.getJsonConfigPath(); // Check if adapter has React tab or HTML config by examining: // 1. package.json scripts for React builds // 2. Admin files existence + // 3. .create-adapter.json configuration + // 4. io-package.json adminUi field + // 5. Tab files (tab.html vs jsonTab.json) let hasReactTab = false; let hasHtmlConfig = false; + let hasTab = false; + let hasJsonTab = false; + let tabType: 'html' | 'json' | 'none' = 'none'; if (!this.isJSController()) { + // Check .create-adapter.json if it exists + const createAdapterJsonPath = path.resolve(this.rootDir, '.create-adapter.json'); + if (existsSync(createAdapterJsonPath)) { + try { + const createAdapterConfig = await readJson(createAdapterJsonPath); + this.log.debug(`Found .create-adapter.json: ${JSON.stringify(createAdapterConfig)}`); + + // Extract UI hints from create-adapter configuration + if (createAdapterConfig.adminUi?.type === 'react') { + hasReactTab = true; + } else if (createAdapterConfig.adminUi?.type === 'html') { + hasHtmlConfig = true; + } + } catch (error) { + this.log.debug(`Failed to read .create-adapter.json: ${error as Error}`); + } + } + + // Check io-package.json adminUi field + try { + const ioPackage = await this.readIoPackageJson(); + if (ioPackage?.common?.adminUi) { + const adminUi = ioPackage.common.adminUi; + this.log.debug(`Found adminUi configuration: ${JSON.stringify(adminUi)}`); + + if (adminUi.config === 'json') { + // Has JSON config (already detected above, but this confirms it) + } else if (adminUi.config === 'html' || adminUi.config === 'materialize') { + hasHtmlConfig = true; + } + + // Check if there are tabs defined + if (adminUi.tab) { + hasTab = true; + if (adminUi.tab === 'json') { + hasJsonTab = true; + tabType = 'json'; + } else if (adminUi.tab === 'html' || adminUi.tab === 'materialize') { + tabType = 'html'; + } + } + } + } catch (error) { + this.log.debug(`Failed to read io-package.json: ${error as Error}`); + } + + // Check package.json scripts for React builds const pkg = await this.readPackageJson(); const scripts = pkg.scripts; if (scripts && (scripts['watch:react'] || scripts['watch:parcel'])) { @@ -392,21 +447,44 @@ class DevServer { if (existsSync(htmlConfigPath)) { hasHtmlConfig = true; } + + // Check for tab files + const tabHtmlPath = path.resolve(this.rootDir, 'admin/tab.html'); + const jsonTabPath = path.resolve(this.rootDir, 'admin/jsonTab.json'); + const jsonTab5Path = path.resolve(this.rootDir, 'admin/jsonTab.json5'); + + if (existsSync(tabHtmlPath)) { + hasTab = true; + if (tabType === 'none') { + tabType = 'html'; + } + } + + if (existsSync(jsonTabPath) || existsSync(jsonTab5Path)) { + hasTab = true; + hasJsonTab = true; + if (tabType === 'none') { + tabType = 'json'; + } + } } this.log.debug( - `UI capabilities: jsonConfig=${hasJsonConfig}, reactTab=${hasReactTab}, htmlConfig=${hasHtmlConfig}`, + `UI capabilities: jsonConfig=${hasJsonConfig}, reactTab=${hasReactTab}, htmlConfig=${hasHtmlConfig}, tab=${hasTab}, jsonTab=${hasJsonTab}, tabType=${tabType}`, ); return { hasJsonConfig, hasReactTab, hasHtmlConfig, + hasTab, + hasJsonTab, + tabType, }; -======= + } + private isTypeScriptMain(mainFile: string): boolean { return !!(mainFile && mainFile.endsWith('.ts')); ->>>>>>> main } private getPort(adminPort: number, offset: number): number { @@ -735,8 +813,11 @@ class DevServer { // Determine what UI capabilities this adapter needs const uiCapabilities = await this.getAdapterUiCapabilities(); - if (uiCapabilities.hasJsonConfig && (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig)) { - // Adapter uses both jsonConfig AND React/HTML - support both simultaneously + if ( + uiCapabilities.hasJsonConfig && + (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig || uiCapabilities.hasTab) + ) { + // Adapter uses both jsonConfig AND React/HTML/tabs - support both simultaneously await this.createCombinedConfigProxy(app, this.config, uiCapabilities, useBrowserSync); } else if (uiCapabilities.hasJsonConfig) { // JSON config only @@ -947,7 +1028,14 @@ class DevServer { private async createCombinedConfigProxy( app: Application, config: DevServerConfig, - uiCapabilities: { hasJsonConfig: boolean; hasReactTab: boolean; hasHtmlConfig: boolean }, + uiCapabilities: { + hasJsonConfig: boolean; + hasReactTab: boolean; + hasHtmlConfig: boolean; + hasTab: boolean; + hasJsonTab: boolean; + tabType: 'html' | 'json' | 'none'; + }, useBrowserSync = true, ): Promise { // This method combines the functionality of createJsonConfigProxy and createHtmlConfigProxy @@ -960,7 +1048,7 @@ class DevServer { // Handle React build watching if needed (from createHtmlConfigProxy) let hasReact = false; let bs: any = null; - + if (useBrowserSync) { if (uiCapabilities.hasReactTab && !this.isJSController()) { const pkg = await this.readPackageJson(); @@ -990,9 +1078,9 @@ class DevServer { // Handle jsonConfig file watching if present (from createJsonConfigProxy) if (uiCapabilities.hasJsonConfig) { const jsonConfigFile = this.getJsonConfigPath(); - + if (useBrowserSync && bs) { - bs.watch(jsonConfigFile, undefined, async e => { + bs.watch(jsonConfigFile, undefined, async (e: any) => { if (e === 'change') { const content = await readFile(jsonConfigFile); this.websocket?.send( @@ -1018,9 +1106,71 @@ class DevServer { } } + // Handle tab file watching if present + if (uiCapabilities.hasTab && useBrowserSync && bs) { + if (uiCapabilities.hasJsonTab) { + // Watch JSON tab files + const jsonTabPath = path.resolve(this.rootDir, 'admin/jsonTab.json'); + const jsonTab5Path = path.resolve(this.rootDir, 'admin/jsonTab.json5'); + + if (existsSync(jsonTabPath)) { + bs.watch(jsonTabPath, undefined, async (e: any) => { + if (e === 'change') { + const content = await readFile(jsonTabPath); + this.websocket?.send( + JSON.stringify([ + 3, + 46, + 'writeFile', + [ + `${this.adapterName}.admin`, + 'jsonTab.json', + Buffer.from(content).toString('base64'), + ], + ]), + ); + } + }); + } + + if (existsSync(jsonTab5Path)) { + bs.watch(jsonTab5Path, undefined, async (e: any) => { + if (e === 'change') { + const content = await readFile(jsonTab5Path); + this.websocket?.send( + JSON.stringify([ + 3, + 46, + 'writeFile', + [ + `${this.adapterName}.admin`, + 'jsonTab.json5', + Buffer.from(content).toString('base64'), + ], + ]), + ); + } + }); + } + } + + if (uiCapabilities.tabType === 'html') { + // Watch HTML tab files + const tabHtmlPath = path.resolve(this.rootDir, 'admin/tab.html'); + if (existsSync(tabHtmlPath)) { + bs.watch(tabHtmlPath, undefined, (e: any) => { + if (e === 'change') { + this.log.debug('Tab HTML file changed, reloading browser...'); + // For HTML tabs, we rely on BrowserSync's automatic reload + } + }); + } + } + } + // Setup proxies similar to both methods if (useBrowserSync) { - if (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig) { + if (uiCapabilities.hasReactTab || uiCapabilities.hasHtmlConfig || uiCapabilities.hasTab) { // browser-sync proxy for adapter files (from createHtmlConfigProxy) const adminPattern = `/adapter/${this.adapterName}/**`; pathRewrite[`^/adapter/${this.adapterName}/`] = '/';