From edd2d0662718b42baecb9c0f63dc8049a553c411 Mon Sep 17 00:00:00 2001 From: cpq Date: Mon, 27 Apr 2026 09:25:29 +0100 Subject: [PATCH] Add inline and watch helper scripts --- resources/inline.js | 157 ++++++++++++++++++ resources/watch.js | 77 +++++++++ tutorials/device-dashboard/Makefile | 4 +- .../device-dashboard/web_root/index.html | 2 +- 4 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 resources/inline.js create mode 100644 resources/watch.js diff --git a/resources/inline.js b/resources/inline.js new file mode 100644 index 00000000..84ed3096 --- /dev/null +++ b/resources/inline.js @@ -0,0 +1,157 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const http = require('http'); + +const args = process.argv.slice(2); +let outputFile = null; +let inputFile = null; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '-o' && i + 1 < args.length) { + outputFile = args[++i]; + } else if (!args[i].startsWith('-')) { + inputFile = args[i]; + } +} + +if (!inputFile) { + console.error('Usage: node inline.js [-o OUTPUT_FILE] HTML_FILE'); + process.exit(1); +} + +const baseDir = path.dirname(path.resolve(inputFile)); +const html = fs.readFileSync(inputFile, 'utf8'); + +function fetchUrl(url) { + return new Promise((resolve, reject) => { + const client = url.startsWith('https') ? https : http; + client.get(url, res => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => resolve(data)); + res.on('error', reject); + }).on('error', reject); + }); +} + +async function inlineCss(match) { + const href = match.match(/href=["']([^"']+)["']/)[1]; + let css = ''; + if (href.startsWith('http')) { + css = await fetchUrl(href); + } else { + const cssPath = path.join(baseDir, href); + css = fs.readFileSync(cssPath, 'utf8'); + } + return ``; +} + +async function inlineJs(match) { + const match2 = match.match(/src=["']([^"']+)["']/); + if (!match2) return match; + const src = match2[1]; + let js = ''; + if (src.startsWith('http')) { + js = await fetchUrl(src); + } else { + const jsPath = path.join(baseDir, src); + js = fs.readFileSync(jsPath, 'utf8'); + } + return ``; +} + +async function inlineImg(match) { + const match2 = match.match(/src=["']([^"']+)["']/); + if (!match2) return match; + const src = match2[1]; + const ext = path.extname(src).toLowerCase(); + let mime = 'image/png'; + if (ext === '.svg') mime = 'image/svg+xml'; + else if (ext === '.jpg' || ext === '.jpeg') mime = 'image/jpeg'; + else if (ext === '.gif') mime = 'image/gif'; + else if (ext === '.webp') mime = 'image/webp'; + else if (ext === '.ico') mime = 'image/x-icon'; + let data; + if (src.startsWith('http')) { + data = await fetchUrl(src); + } else { + const imgPath = path.join(baseDir, src); + data = fs.readFileSync(imgPath); + } + const base64 = Buffer.from(data).toString('base64'); + if (ext === '.svg') { + const svgData = typeof data === 'string' ? data : data.toString('utf8'); + return match.replace(/src="[^"]+"/, `src="data:${mime};utf8,${encodeURIComponent(svgData)}"`); + } + return match.replace(/src="[^"]+"/, `src="data:${mime};base64,${base64}"`); +} + +async function inlineSvg(match) { + const match2 = match.match(/href=["']([^"']+)["']/); + if (!match2) return match; + const href = match2[1]; + if (!href.toLowerCase().endsWith('.svg')) return match; + let data; + if (href.startsWith('http')) { + data = await fetchUrl(href); + } else { + const svgPath = path.join(baseDir, href); + data = fs.readFileSync(svgPath, 'utf8'); + } + const base64 = Buffer.from(data).toString('base64'); + return match.replace(/href="[^"]+"/, `href="data:image/svg+xml;base64,${base64}"`); +} + +async function processHtml(content) { + let result = content; + const cssRegex = /]+rel=["']stylesheet["'][^>]*>/g; + const jsRegex = /]+src=["'][^"']+["'][^>]*><\/script>/g; + const imgRegex = /]+src=["'][^"']+["'][^>]*>/g; + const svgRegex = /]+rel=["']icon["'][^>]*>/g; + + const cssMatches = content.match(cssRegex) || []; + for (const match of cssMatches) { + if (!match.includes('data:')) { + result = result.replace(match, await inlineCss(match)); + } + } + + const jsMatches = content.match(jsRegex) || []; + for (const match of jsMatches) { + if (!match.includes('data:')) { + result = result.replace(match, await inlineJs(match)); + } + } + + const imgMatches = content.match(imgRegex) || []; + for (const match of imgMatches) { + if (!match.includes('data:')) { + result = result.replace(match, await inlineImg(match)); + } + } + + const svgMatches = content.match(svgRegex) || []; + for (const match of svgMatches) { + if (!match.includes('data:')) { + result = result.replace(match, await inlineSvg(match)); + } + } + + return result; +} + +processHtml(html).then(result => { + if (outputFile) { + fs.writeFileSync(outputFile, result); + console.log(`Written to ${outputFile}`); + } else { + console.log(result); + } +}).catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/resources/watch.js b/resources/watch.js new file mode 100644 index 00000000..a7544b37 --- /dev/null +++ b/resources/watch.js @@ -0,0 +1,77 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const args = process.argv.slice(2); +const watchTargets = []; +let command = []; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '-w' && i + 1 < args.length) { + watchTargets.push(args[++i]); + } else if (args[i] === '--') { + command = args.slice(i + 1); + break; + } else if (!args[i].startsWith('-')) { + command = args.slice(i); + break; + } +} + +if (watchTargets.length === 0 || command.length === 0) { + console.error('Usage: node watch.js -w DIR_OR_FILE [-w DIR_OR_FILE] -- COMMAND'); + process.exit(1); +} + +let child = null; +let restartTimeout = null; + +function start() { + if (child) { + child.kill(); + } + console.log(`Starting: ${command.join(' ')}`); + child = spawn(command[0], command.slice(1), { stdio: 'inherit', shell: true }); + child.on('exit', (code) => { + if (code !== null && code !== 0) { + console.log(`Process exited with code ${code}`); + } + }); +} + +function watch(target) { + const stats = fs.statSync(target); + if (stats.isFile()) { + fs.watchFile(target, (curr, prev) => { + if (curr.mtime !== prev.mtime) { + console.log(`File changed: ${target}`); + clearTimeout(restartTimeout); + restartTimeout = setTimeout(start, 100); + } + }); + } else { + const dir = target; + fs.watch(dir, { recursive: true }, (eventType, filename) => { + if (filename) { + console.log(`File changed: ${filename}`); + clearTimeout(restartTimeout); + restartTimeout = setTimeout(start, 100); + } + }); + } +} + +start(); +watchTargets.forEach(target => watch(target)); +console.log(`Watching ${watchTargets.join(', ')} for changes...`); + +process.on('SIGINT', () => { + console.log('\nShutting down...'); + if (child) { + child.kill('SIGTERM'); + } + process.exit(0); +}); diff --git a/tutorials/device-dashboard/Makefile b/tutorials/device-dashboard/Makefile index 2fffd6ce..87634286 100644 --- a/tutorials/device-dashboard/Makefile +++ b/tutorials/device-dashboard/Makefile @@ -16,7 +16,7 @@ $(PROG): $(SOURCES) $(MONGOOSE_DIR)/mongoose.h # Generate a single, self-contained dashboard.html from the UI in web_root/ dashboard.html: $(wildcard web_root/*) - monolith web_root/index.html -o $@ + node $(MONGOOSE_DIR)/resources/inline.js web_root/index.html -o $@ # Create an embedded filesystem containing compressed dashboard.html dashboard.html.c: dashboard.html @@ -28,7 +28,7 @@ purge: # For development: rebuild and restart if any of the sources change watch: - watchexec -r -w main.c -w mongoose_dashboard.c -w $(MONGOOSE_DIR)/mongoose.h -w web_root -e c,h,html,css,js,svg,png -- make + node $(MONGOOSE_DIR)/resources/watch.js -w main.c -w mongoose_dashboard.c -w $(MONGOOSE_DIR)/mongoose.h -w web_root -- make clean: rm -rf $(PROG) *.exe *.obj *.dSYM mongoose dashboard.* diff --git a/tutorials/device-dashboard/web_root/index.html b/tutorials/device-dashboard/web_root/index.html index b05ef373..081950b5 100644 --- a/tutorials/device-dashboard/web_root/index.html +++ b/tutorials/device-dashboard/web_root/index.html @@ -282,7 +282,7 @@
${f.size}