diff --git a/README.md b/README.md index 827fece..e966f33 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,37 @@ ## Astronomy/Weather Clock -To test and build this project you can use the following commands: - - "`npm run lint`" to inspect the code with ESLint. - - "`npm run build` [-- [`--dht`] [`--acu`]]" to build (with optional support for wired and/or wireless temperature/humidity sensors). - - "`npm test`" to run unit tests. - - "`npm run start-server`" to start the data server for this project. - - "`npm start`" to serve the web client using webpack-dev-server. - - "`npm run e2e`" to run Protractor for end-to-end tests. +This project is designed to create a desktop clock which provides weather and astronomical information. While primarily designed to run on a Raspberry Pi, the code will create a Node.js server and client web app that can be run on other computers and operating systems, albeit without the Raspberry Pi’s hardware-level support for wired and wireless temperature/humidity sensors. -> Note: As of the time of this writing, the build will not work with Node 11 or later on Linux (including Raspbian) because of a dependency on `node-sass`. Even when using Node 10.x or earlier you might get some errors with `npm install` due to `node-sass`. I found that using `LIBSASS_EXT="no" npm install` helped. (If you have a problem, and this doesn't fix it, search for solutions based on `node-sass` and any specific error messages you receive.) This project currently does not support development on Windows. +The clock displays the time and date in both analog and digital form, in 12- or 24-hour format (with a special display mode for the occasional leap second). The clock also displays current weather conditions, a four-day forecast, sunrise and sunset times, moon phases, and the positions of the Sun, Moon, and major planets along the ecliptic. + +### Getting started + +Cloning the repository and initial set-up: + +```shell script +$ git clone https://github.com/kshetline/aw-clock.git +$ cd aw-clock +$ npm run first-install +``` +While it’s typical to do `npm install` upon first cloning a project, this project is two projects in one — client and server — so `npm run first-install` gets both initial installations done at the same time. + +To build and run this project you can use the following commands: + - “`npm run build` [-- [`--acu`] [`--dht`] [ `--pt`] [`--sd`]]” to build (with optional support for wired and/or wireless temperature/humidity sensors). + - “`npm run start-server`” to start the data server for this project (not on Windows). + - “`npm run start-server-win`” to start the data server for this project (on Windows). + - “`npm start`” to serve the web client using webpack-dev-server. + +> Note: A dependency on `node-sass` sometimes causes build problems. It often helps to delete the top level `node_modules` directory, and then do `npm install` over again. I’ve also found that using `LIBSASS_EXT=”no” npm install` helped. The server requires a Dark Sky API key for weather data. Use the environment variable `AWC_DARK_SKY_API_KEY` to set the key. (See https://darksky.net/ for further details.) -By default the server uses `pool.ntp.org` as an NTP time server. Use the environment variable `AWC_NTP_SERVER` to change the time server. Do not use a Google time server, or any other NTP server that implements "leap second smearing" if you want the Astronomy/Weather Clock to be able to display leap seconds. +By default the server uses `pool.ntp.org` as an NTP time server. Use the environment variable `AWC_NTP_SERVER` to change the time server. Do not use a Google time server, or any other NTP server that implements “leap second smearing” if you want the Astronomy/Weather Clock to be able to display leap seconds. ![Hypothetical leap second](https://shetline.com/misc/moment_of_leap_second.jpg) _This image is hypothetical — the pictured moment in time is not guaranteed to be an actual leap second. Video here: https://shetline.com/video/leap_second_display.mp4_ -To deploy the server along with the web client, use `npm run build` (possibly followed by `--`, then the `--dht` and/or `--acu` options as described above), executed in the project's root directory. The contents of the root-level `dist` directory will then contain the Node.js server code, and the client code in the `dist/public` directory. For example: +To build the server along with the web client, use `npm run build` (possibly followed by `--`, then the `--acu`, `--dht`, and/or other options), executed in the project’s root directory. The contents of the root-level `dist` directory will then contain the Node.js server code, and the client code in the `dist/public` directory. For example: |   |   | | ------------------------------ | -------------------------------------------------------------- | @@ -26,51 +39,53 @@ To deploy the server along with the web client, use `npm run build` (possibly fo | `npm run build -- --dht` |     Server/client with wired indoor sensor support. | | `npm run build -- --dht --acu` |     Server/client with both wired and wireless sensor support. | -_(Note: Don't forget the extra double dashes by themselves before the other options!)_ +This `--pt` option is for “plain text”, meaning the console colors and progress animation are disabled. + +The Raspberry Pi-only option `--sd` deploys the app to the default `~/weather` directory (typically `/home/pi/weather`). + +_(Note: Don’t forget the extra double dashes by themselves that must be present before the other options!)_ If you are running the server on a Raspberry Pi, you have the option to provide wired indoor temperature and humidity data using a DHT22/AM2302 sensor, as seen here: https://www.amazon.com/HiLetgo-Temperature-Humidity-Electronic-Practice/dp/B01N9BA0O4/. The wiring I will describe is specifically for the AM2302 version of the DHT22, with the built-in pull-up resistor. First, you must install the BCM 2835 library as described here: http://www.airspayce.com/mikem/bcm2835/ -Then, with your Raspberry Pi shut down and disconnected from power, connect the DHT22/AM2302 sensor. The code defaults to assuming the signal lead ("out") of the sensor is connected to GPIO 4 (physical pin 7 on the 40-pin GPIO header). You can use the environment variable `AWC_TH_SENSOR_GPIO` to set a different GPIO number. The `+` lead from the sensor needs to be connected to 5V (I chose pin 2 on the 40-pin GPIO header) and the `-` lead needs to be connected to ground (I chose pin 6). +Then, with your Raspberry Pi shut down and disconnected from power, connect the DHT22/AM2302 sensor. The code defaults to assuming the signal lead (“out”) of the sensor is connected to GPIO 4 (physical pin 7 on the 40-pin GPIO header). You can use the environment variable `AWC_TH_SENSOR_GPIO` to set a different GPIO number. The `+` lead from the sensor needs to be connected to 5V (I chose pin 2 on the 40-pin GPIO header) and the `-` lead needs to be connected to ground (I chose pin 6). ![Picture of wiring](https://shetline.com/misc/rpi-dht22-wiring.jpg) The web client only displays the indoor temperature and humidity values when connected to the web server on `localhost:8080`. -Also for the Raspberry Pi you have the option to provide wireless indoor and outdoor weather data using [433 MHz Acu Rite 06002M wireless temperature and humidity sensors](https://www.amazon.com/gp/product/B00T0K8NXC/) paired with a [433 MHz receiver module](https://www.amazon.com/gp/product/B00HEDRHG6/). +Also for the Raspberry Pi you have the option to provide wireless indoor conditions and outdoor weather data using [433 MHz Acu Rite 06002M wireless temperature and humidity sensors](https://www.amazon.com/gp/product/B00T0K8NXC/) paired with a [433 MHz receiver module](https://www.amazon.com/gp/product/B00HEDRHG6/). You can use one wireless sensor in lieu of a wired DHT22/AM2302 for indoor temperature and humidity, and you can use one or two wireless sensors for outdoor temperature and humidity. (When using multiple sensors, each must be set to a different channel — A, B, or C.) -A two-sensor set-up is useful when it's difficult to find a single location for a sensor that isn't overly warmed by the sun for at least part of the day. When you have two sensors, and signal is available from both, values from the cooler of the two sensors will be displayed. +A outdoor two-sensor set-up is useful when it’s difficult to find a single location for a sensor that isn’t overly warmed by the sun for at least part of the day. When you have two sensors, and signal is available from both, values from the cooler of the two sensors will be displayed. -With either one or two outdoor sensors the displayed temperature will be pinned to be with ±2°C (±4°F) of the temperature supplied by the online weather service. The "Feels like" temperature always comes from the weather service, not from your wireless sensors. +With either one or two outdoor sensors the displayed temperature will be pinned to be within ±2°C (±4°F) of the temperature supplied by the online weather service. The “Feels like” temperature always comes from the weather service, not from your wireless sensors. -When connecting the 433 Mhz receiver module, follow the same precautions as given for connecting the DHT22/AM2302. For my own set-up, I've connected the receiver's +5V lead to physical pin 4 of the 40-pin connector, ground to pin 14, and data to pin 13 (GPIO 27). +When connecting the 433 Mhz receiver module, follow the same precautions as given for connecting the DHT22/AM2302. For my own set-up, I’ve connected the receiver’s +5V lead to physical pin 4 of the 40-pin connector, ground to pin 14, and data to pin 13 (GPIO 27). -I can't guarantee that I'm recalling every important step I took to create my own set-up, but hopefully the following is a more-or-less complete guide to setting up a Raspberry Pi to automatically boot up as a full-screen astronomy/weather clock: +I can’t guarantee that I’m recalling every important step I took to create my own set-up, but hopefully the following is a more-or-less complete guide to setting up a Raspberry Pi to automatically boot up as a full-screen astronomy/weather clock: -1. Install Node.js version 10.x. (You can find instructions for this step here: https://www.w3schools.com/nodejs/nodejs_raspberrypi.asp.) Later versions of Node may work if and when node-sass is updated to be compatible. +1. Install Node.js, preferably the latest LTS version. (You can find instructions for this step here: https://www.w3schools.com/nodejs/nodejs_raspberrypi.asp.) 1. Install the BCM 2835 library, as described here: http://www.airspayce.com/mikem/bcm2835/ -1. `pigpio` is probably already installed on your Raspberry Pi, but it may need to be updated, particularly if you're using a Raspberry Pi 4. That's described here: http://abyz.me.uk/rpi/pigpio/download.html. As I write this the `pigpio` website says, "At the moment pigpio on the Pi4B is experimental." This project's software was very flaky until I upgraded `pigpio` from the pre-installed version 71 to version 74. -1. Clone this project and, from the root directory of the project, do `npm install`. -1. Install the Chromium browser if it's not already installed: +1. `pigpio` is probably already installed on your Raspberry Pi, but it may need to be updated, particularly if you’re using a Raspberry Pi 4. That’s described here: http://abyz.me.uk/rpi/pigpio/download.html. As I write this the `pigpio` website says, “At the moment pigpio on the Pi4B is experimental.” This project’s software was a bit flaky until I upgraded `pigpio` from the pre-installed version 71 to version 74. +1. Install the Chromium browser if it’s not already installed: `sudo apt-get install chromium-browser` -1. Install `unclutter` (this will hide your mouse cursor after 30 seconds of inactivity so it doesn't obscure the display): `sudo apt-get install unclutter` -1. Install `xscreensaver` if it's not already installed (`sudo apt-get install xscreensaver`). This is needed not because you want a screen saver for this application — in fact, you want the screen to stay on all of the time without interruption. Installing `xscreensaver` gives you the option of going to your Raspberry Pi's Preferences and _turning off_ the default screen blanking that will otherwise occur. -1. Build the client project as described above (`npm run build`, with or without `--dht` or `--acu` options as described above). -1. Copy the contents of this project's `dist` folder to `/home/pi/weather`. +1. Install `unclutter` (this will hide your mouse cursor after 30 seconds of inactivity so it doesn’t obscure the display): `sudo apt-get install unclutter` +1. Install `xscreensaver` if it’s not already installed (`sudo apt-get install xscreensaver`). This is needed not because you want a screen saver for this application — in fact, you want the screen to stay on all of the time without interruption. Installing `xscreensaver` gives you the option of going to your Raspberry Pi’s Preferences and _turning off_ the default screen blanking that will otherwise occur. +1. Clone this project and, from the root directory of the project, do `npm run first-install`. +1. Build the client project as described above (`npm run build -- --sd`, with or without `--dht` or `--acu` options as described above). 1. If you wish to use an indoor wired temperature/humidity sensor, follow the previously mentioned steps to install the BCM 2835 library and connect the sensor. 1. If you wish to use wireless temperature/humidity sensors, follow those previous instructions. -1. Copy the included file `weatherService` (located in the `raspberry_pi_setup` folder) to `/etc/init.d/`. Make sure the file is owned by -`root` is set to be executable. Follow the instructions listed inside that file to set up the necessary environment variables, which will -be saved in `/etc/defaults/weatherService`. This is where you add your API key, and set `AWC_HAS_INDOOR_SENSOR` to `true` if you're -connecting an indoor temperature/humidity sensor. - * _Don't forget that if you update this project, you may need to manually update `/etc/init.d/weatherService` too._ +1. Copy the included file `weatherService` (located in the `raspberry_pi_setup` folder) to `/etc/init.d/`. Make sure the file is owned by `root` and is set to be executable with `chmod +x`. Follow the instructions listed inside that file to set up the necessary environment variables, which will +be saved in `/etc/defaults/weatherService`. This is where you add your API key, set `AWC_HAS_INDOOR_SENSOR` to `true` if you’re +connecting an indoor temperature/humidity sensor. and set other environment variable options. + * _Don’t forget that if you update this project, you may need to manually update `/etc/init.d/weatherService` too._ 1. Use the command `sudo update-rc.d weatherService defaults` to establish the service that starts up the weather server. 1. Use the command `sudo systemctl enable weatherService` to enable the service. 1. `sudo npm install -g forever` — this installs a utility to monitor and automatically restart the server if necessary. -1. Copy the included files `autostart` and `autostart_extra.sh` to `/home/pi/.config/lxsession/LXDE-pi/` and make sure they're executable. This launches the astronomy/weather clock client in Chromium, using kiosk mode (full screen, no toolbars). It also makes sure Chromium doesn't launch complaining that it was shut down improperly. -1. I'm not sure about the current copyright disposition of these fonts, but for improved appearance I'd recommend finding and installing the fonts "Arial Unicode MS" and "Verdana". These appear to be freely available for download without licensing restrictions. +1. Copy the included files `autostart` and `autostart_extra.sh` to `/home/pi/.config/lxsession/LXDE-pi/` and make sure they’re executable with `chmod +x`. This launches the astronomy/weather clock client in Chromium, using kiosk mode (full screen, no toolbars). It also makes sure Chromium doesn’t launch complaining that it was shut down improperly. +1. I’m not sure about the current copyright disposition of these fonts, but for improved appearance I’d recommend finding and installing the fonts “Arial Unicode MS” and “Verdana”. These appear to be freely available for download without licensing restrictions. 1. Reboot, and if all has gone well, the astronomy/weather clock be up and running. Click on the gear icon in the lower right corner of the app to set your preferences, such as the location to use for weather forecasts and astronomical observations. diff --git a/build.ts b/build.ts index b6bffa0..42b6d72 100644 --- a/build.ts +++ b/build.ts @@ -1,5 +1,5 @@ -import * as chalk from 'chalk'; -import { ChildProcess, spawn } from 'child_process'; +import * as Chalk from 'chalk'; +import { ChildProcess, spawn as nodeSpawn } from 'child_process'; import * as copyfiles from 'copyfiles'; import * as fs from 'fs'; import { processMillis } from './server/src/util'; @@ -13,22 +13,57 @@ const SPIN_DELAY = 100; const MAX_SPIN_DELAY = 100; const NO_OP = () => {}; +const isWindows = (process.platform === 'win32'); + let spinStep = 0; let lastSpin = 0; let npmInitDone = false; let doAcu = false; let doDht = false; +// eslint-disable-next-line @typescript-eslint/no-unused-vars let doGps = false; -let doWwvb = false; let doI2c = false; +let doStdDeploy = false; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let doWwvb = false; +let isRaspberryPi = false; + +const chalk = new Chalk.Instance(); +let canSpin = true; +let backspace = '\x08'; +let trailingSpace = ' '; // Two spaces + +if (process.platform === 'linux') { + try { + if (fs.existsSync('/proc/cpuinfo')) { + const lines = fs.readFileSync('/proc/cpuinfo').toString().split('\n'); + + for (const line of lines) { + if (/\bModel\s*:\s*Raspberry Pi\b/i.test(line)) { + isRaspberryPi = true; + break; + } + } + } + } + catch (err) { + console.error(chalk.red('Raspberry Pi check failed')); + } +} // Remove extraneous command line args, if present. -if (/[/\\]ts-node(?:\.cmd)?$/.test(process.argv[0] ?? '')) +if (/\bts-node\b/.test(process.argv[0] ?? '')) process.argv.splice(0, 1); -if (/[/\\]build\.ts$/.test(process.argv[0] ?? '')) +if (/\bbuild\.ts\b/.test(process.argv[0] ?? '')) process.argv.splice(0, 1); +if (process.argv.length === 0 && isRaspberryPi) { + console.warn(chalk.yellow('Warning: no build options specified.')); + console.warn(chalk.yellow('This could be OK, or this could mean you forgot the leading ') + + chalk.white('--') + chalk.yellow(' before your options.')); +} + process.argv.forEach(arg => { if (arg === '--acu') doAcu = true; @@ -36,39 +71,74 @@ process.argv.forEach(arg => { doDht = true; else if (arg === '--gps') doGps = doI2c = true; + else if (arg === '--pt') { + canSpin = false; + chalk.level = 0; + backspace = ''; + trailingSpace = ' '; + } + else if (arg === '--sd') + doStdDeploy = true; else if (arg === '--wwvb') doWwvb = doI2c = true; else { if (arg !== '--help') console.error('Unrecognized option "' + chalk.red(arg) + '"'); - console.log('Usage: npm run build [-- [--acu] [--dht] [--gps] [--help] [--wwvb]]'); + console.log('Usage: npm run build [-- [--acu] [--dht] [--help] [--pt] [--sd]]'); process.exit(0); } }); -process.stdout.write(('' + doAcu + doGps + doWwvb).substr(0, 0)); +if (doStdDeploy && !isRaspberryPi) { + console.error(chalk.red('--sd option is only valid on Raspberry Pi')); + process.exit(0); +} + +function spawn(command: string, args: string[] = [], options?: any): ChildProcess { + if (isWindows) { + const cmd = process.env.comspec || 'cmd'; + + return nodeSpawn(cmd, ['/c', command, ...args], options); + } + else + return nodeSpawn(command, args, options); +} function spin(): void { const now = processMillis(); if (lastSpin < now - SPIN_DELAY) { lastSpin = now; - process.stdout.write('\x08' + SPIN_CHARS.charAt(spinStep)); + process.stdout.write(backspace + SPIN_CHARS.charAt(spinStep)); spinStep = (spinStep + 1) % 4; } } function monitorProcess(proc: ChildProcess, doSpin = true): Promise { + let errors = ''; let output = ''; + doSpin = doSpin && canSpin; + return new Promise((resolve, reject) => { const slowSpin = setInterval(doSpin ? spin : NO_OP, MAX_SPIN_DELAY); - proc.stderr.on('data', doSpin ? spin : NO_OP); + proc.stderr.on('data', data => { + (doSpin ? spin : NO_OP)(); + data = data.toString(); + // This gets confusing, because a lot of non-error progress messaging goes to stderr, and the + // webpack process doesn't exit with an error for compilation errors unless you make it do so. + if (/\[webpack.Progress]/.test(data)) + return; + + errors += data; + }); proc.stdout.on('data', data => { - output += data.toString(); (doSpin ? spin : NO_OP)(); + data = data.toString(); + output += data; + errors = ''; }); proc.on('error', err => { clearInterval(slowSpin); @@ -76,7 +146,11 @@ function monitorProcess(proc: ChildProcess, doSpin = true): Promise { }); proc.on('close', () => { clearInterval(slowSpin); - resolve(output); + + if (errors && (/\b(error|exception)\b/i.test(errors) || /[_0-9a-z](Error|Exception)\b/.test(errors))) + reject(errors); + else + resolve(output); }); }); } @@ -108,56 +182,67 @@ async function npmInit(): Promise { (async () => { try { console.log(chalk.cyan('Starting build...')); - - process.stdout.write('Updating client '); + process.stdout.write('Updating client' + trailingSpace); await monitorProcess(spawn('npm', ['--dev', 'update'])); - console.log('\x08' + chalk.green(CHECK_MARK)); + console.log(backspace + chalk.green(CHECK_MARK)); - process.stdout.write('Building client '); + process.stdout.write('Building client' + trailingSpace); if (fs.existsSync('dist')) await monitorProcess(spawn('rm', ['-Rf', 'dist'])); let output = await monitorProcess(spawn('webpack')); - console.log('\x08' + chalk.green(CHECK_MARK)); + console.log(backspace + chalk.green(CHECK_MARK)); console.log(chalk.hex('#808080')(getWebpackSummary(output))); - process.stdout.write('Updating server '); + process.stdout.write('Updating server' + trailingSpace); await monitorProcess(spawn('npm', ['--dev', 'update'], { cwd: path.join(__dirname, 'server') })); - console.log('\x08' + chalk.green(CHECK_MARK)); + console.log(backspace + chalk.green(CHECK_MARK)); - process.stdout.write('Building server '); + process.stdout.write('Building server' + trailingSpace); if (fs.existsSync('server/dist')) await monitorProcess(spawn('rm', ['-Rf', 'server/dist'])); - output = await monitorProcess(spawn('npm', ['run', 'build'], { cwd: path.join(__dirname, 'server') })); - console.log('\x08' + chalk.green(CHECK_MARK)); + output = await monitorProcess(spawn('npm', ['run', isWindows ? 'build-win' : 'build'], { cwd: path.join(__dirname, 'server') })); + console.log(backspace + chalk.green(CHECK_MARK)); console.log(chalk.hex('#808080')(getWebpackSummary(output))); if (doAcu) { - process.stdout.write('Adding Acu-Rite wireless temperature/humidity sensor support '); + process.stdout.write('Adding Acu-Rite wireless temperature/humidity sensor support' + trailingSpace); await npmInit(); - await monitorProcess(spawn('npm', ['i', 'rpi-acu-rite-temperature'], { cwd: path.join(__dirname, 'server', 'dist') })); - console.log('\x08' + chalk.green(CHECK_MARK)); + await monitorProcess(spawn('npm', ['i', 'rpi-acu-rite-temperature@2.x'], { cwd: path.join(__dirname, 'server', 'dist') })); + console.log(backspace + chalk.green(CHECK_MARK)); } if (doDht) { - process.stdout.write('Adding DHT wired temperature/humidity sensor support '); + process.stdout.write('Adding DHT wired temperature/humidity sensor support' + trailingSpace); await npmInit(); - await monitorProcess(spawn('npm', ['i', 'node-dht-sensor'], { cwd: path.join(__dirname, 'server', 'dist') })); - console.log('\x08' + chalk.green(CHECK_MARK)); + await monitorProcess(spawn('npm', ['i', 'node-dht-sensor@0.4.x'], { cwd: path.join(__dirname, 'server', 'dist') })); + console.log(backspace + chalk.green(CHECK_MARK)); } if (doI2c) { - process.stdout.write('Adding I²C serial bus support '); + process.stdout.write('Adding I²C serial bus support' + trailingSpace); await npmInit(); await monitorProcess(spawn('npm', ['i', 'i2c-bus'], { cwd: path.join(__dirname, 'server', 'dist') })); - console.log('\x08' + chalk.green(CHECK_MARK)); + console.log(backspace + chalk.green(CHECK_MARK)); } - process.stdout.write('Copying server to top-level dist directory '); + process.stdout.write('Copying server to top-level dist directory' + trailingSpace); await (promisify(copyfiles) as any)(['server/dist/**/*', 'dist/'], { up: 2 }); - console.log('\x08' + chalk.green(CHECK_MARK)); + console.log(backspace + chalk.green(CHECK_MARK)); + + if (doStdDeploy) { + process.stdout.write('Moving server to ~/weather directory' + trailingSpace); + + if (!fs.existsSync(process.env.HOME + '/weather')) + fs.mkdirSync(process.env.HOME + '/weather'); + else + await monitorProcess(spawn('rm', ['-Rf', '~/weather/*'], { shell: true })); + + await monitorProcess(spawn('mv', ['dist/*', '~/weather'], { shell: true })); + console.log(backspace + chalk.green(CHECK_MARK)); + } } catch (err) { - console.log('\x08' + chalk.red(FAIL_MARK)); + console.log(backspace + chalk.red(FAIL_MARK)); console.error(err); } })(); diff --git a/package-lock.json b/package-lock.json index 052cdb7..efd2a31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "aw-clock", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -198,9 +198,9 @@ } }, "@types/jasmine": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.7.tgz", - "integrity": "sha512-HoPp5ZafWFXr36yRUOJNuRbvFNklxvN+I9JXfAaZTHBiEw4ZkN4FBnjbV4YHRNNG433ypHP2K+lOeQyRdyuGxQ==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.8.tgz", + "integrity": "sha512-q/L59BjgE6VJtuIM4iDEEwT7xdSWVHMqC+td9sft5RfgHpDZ4Gdn0vLV59wKZ7PjGuVLCL7aNCdoiw5u6ZKSgA==", "dev": true }, "@types/jasminewd2": { @@ -240,9 +240,9 @@ "dev": true }, "@types/node": { - "version": "13.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.6.tgz", - "integrity": "sha512-eyK7MWD0R1HqVTp+PtwRgFeIsemzuj4gBFSQxfPHY5iMjS7474e5wq+VFgTcdpyHeNxyKSaetYAjdMLJlKoWqA==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.0.tgz", + "integrity": "sha512-0ARSQootUG1RljH2HncpsY2TJBfGQIKOOi7kxzUY6z54ePu/ZD+wJA8zI2Q6v8rol2qpG/rvqsReco8zNMPvhQ==", "dev": true }, "@types/q": { @@ -264,12 +264,12 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.21.0.tgz", - "integrity": "sha512-b5jjjDMxzcjh/Sbjuo7WyhrQmVJg0WipTHQgXh5Xwx10uYm6nPWqN1WGOsaNq4HR3Zh4wUx4IRQdDkCHwyewyw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.22.0.tgz", + "integrity": "sha512-BvxRLaTDVQ3N+Qq8BivLiE9akQLAOUfxNHIEhedOcg8B2+jY8Rc4/D+iVprvuMX1AdezFYautuGDwr9QxqSxBQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.21.0", + "@typescript-eslint/experimental-utils": "2.22.0", "eslint-utils": "^1.4.3", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -277,32 +277,32 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.21.0.tgz", - "integrity": "sha512-olKw9JP/XUkav4lq0I7S1mhGgONJF9rHNhKFn9wJlpfRVjNo3PPjSvybxEldvCXnvD+WAshSzqH5cEjPp9CsBA==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.22.0.tgz", + "integrity": "sha512-sJt1GYBe6yC0dWOQzXlp+tiuGglNhJC9eXZeC8GBVH98Zv9jtatccuhz0OF5kC/DwChqsNfghHx7OlIDQjNYAQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.21.0", + "@typescript-eslint/typescript-estree": "2.22.0", "eslint-scope": "^5.0.0" } }, "@typescript-eslint/parser": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.21.0.tgz", - "integrity": "sha512-VrmbdrrrvvI6cPPOG7uOgGUFXNYTiSbnRq8ZMyuGa4+qmXJXVLEEz78hKuqupvkpwJQNk1Ucz1TenrRP90gmBg==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.22.0.tgz", + "integrity": "sha512-FaZKC1X+nvD7qMPqKFUYHz3H0TAioSVFGvG29f796Nc5tBluoqfHgLbSFKsh7mKjRoeTm8J9WX2Wo9EyZWjG7w==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.21.0", - "@typescript-eslint/typescript-estree": "2.21.0", + "@typescript-eslint/experimental-utils": "2.22.0", + "@typescript-eslint/typescript-estree": "2.22.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.21.0.tgz", - "integrity": "sha512-NC/nogZNb9IK2MEFQqyDBAciOT8Lp8O3KgAfvHx2Skx6WBo+KmDqlU3R9KxHONaijfTIKtojRe3SZQyMjr3wBw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.22.0.tgz", + "integrity": "sha512-2HFZW2FQc4MhIBB8WhDm9lVFaBDy6h9jGrJ4V2Uzxe/ON29HCHBTj3GkgcsgMWfsl2U5as+pTOr30Nibaw7qRQ==", "dev": true, "requires": { "debug": "^4.1.1", @@ -9862,9 +9862,9 @@ "dev": true }, "typescript": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz", - "integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, "uglify-js": { @@ -10942,9 +10942,9 @@ } }, "webpack": { - "version": "4.41.6", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.6.tgz", - "integrity": "sha512-yxXfV0Zv9WMGRD+QexkZzmGIh54bsvEs+9aRWxnN8erLWEOehAKUTeNBoUbA6HPEZPlRo7KDi2ZcNveoZgK9MA==", + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.0.tgz", + "integrity": "sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w==", "dev": true, "requires": { "@webassemblyjs/ast": "1.8.5", diff --git a/package.json b/package.json index 4853462..4c1d6d2 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "name": "aw-clock", - "version": "2.0.2", + "version": "2.0.3", "license": "MIT", "author": "Kerry Shetline ", "scripts": { + "first-install": "npm i && cd server && npm i && cd ..", "start": "webpack-dev-server --port=4200", "start-server": "cd server && npm start", + "start-server-win": "cd server && npm start-win", "build": "ts-node build.ts", "test": "karma start ./karma.conf.js", "lint": "eslint \"**/*.ts\" \"**/*.js\"", @@ -26,13 +28,13 @@ "devDependencies": { "@types/copyfiles": "^2.1.1", "@types/follow-redirects": "^1.8.0", - "@types/jasmine": "^3.5.7", + "@types/jasmine": "^3.5.8", "@types/jasminewd2": "^2.0.8", "@types/jquery": "^3.3.33", "@types/js-cookie": "^2.2.5", - "@types/node": "^13.7.6", - "@typescript-eslint/eslint-plugin": "^2.21.0", - "@typescript-eslint/parser": "^2.21.0", + "@types/node": "^13.9.0", + "@typescript-eslint/eslint-plugin": "^2.22.0", + "@typescript-eslint/parser": "^2.22.0", "autoprefixer": "^9.7.4", "chalk": "^3.0.0", "circular-dependency-plugin": "^5.2.0", @@ -72,9 +74,9 @@ "terser-webpack-plugin": "^2.3.5", "ts-loader": "^6.2.1", "ts-node": "~8.6.2", - "typescript": "^3.8.2", + "typescript": "^3.8.3", "url-loader": "^3.0.0", - "webpack": "^4.41.6", + "webpack": "^4.42.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.1" } diff --git a/server/package-lock.json b/server/package-lock.json index eaa6788..5cbc258 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,6 +1,6 @@ { "name": "aw-clock-server", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -14,15 +14,15 @@ } }, "@types/bluebird": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz", - "integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.30.tgz", + "integrity": "sha512-8LhzvcjIoqoi1TghEkRMkbbmM+jhHnBokPGkJWjclMK+Ks0MxEBow3/p2/iFTZ+OIbJHQDSfpgdZEb+af3gfVw==", "dev": true }, "@types/body-parser": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", - "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", "dev": true, "requires": { "@types/connect": "*", @@ -36,9 +36,9 @@ "dev": true }, "@types/chai": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.9.tgz", - "integrity": "sha512-NeXgZj+MFL4izGqA4sapdYzkzQG+MtGra9vhQ58dnmDY++VgJaRUws+aLVV5zRJCYJl/8s9IjMmhiUw1WsKSmw==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.10.tgz", + "integrity": "sha512-TlWWgb21+0LdkuFqEqfmy7NEgfB/7Jjux15fWQAh3P93gbmXuwTM/vxEdzW89APIcI2BgKR48yjeAkdeH+4qvQ==", "dev": true }, "@types/connect": { @@ -60,9 +60,9 @@ } }, "@types/express": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz", - "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==", + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", "dev": true, "requires": { "@types/body-parser": "*", @@ -71,9 +71,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.1.tgz", - "integrity": "sha512-9e7jj549ZI+RxY21Cl0t8uBnWyb22HzILupyHZjYEVK//5TT/1bZodU+yUbLnPdoYViBBnNWbxp4zYjGV0zUGw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz", + "integrity": "sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg==", "dev": true, "requires": { "@types/node": "*", @@ -81,9 +81,9 @@ } }, "@types/ftp": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@types/ftp/-/ftp-0.3.30.tgz", - "integrity": "sha512-oYX5CLrWa86ooec8RggNt1vnwvRAxCHapaTmDI1Jr6DM55HPXw3UyDFxVpQnz/oTiaN8DEvL+gkgdVxD8peQfg==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@types/ftp/-/ftp-0.3.31.tgz", + "integrity": "sha512-J30a1yD9avBsFahvmGoeumWUZ5YnGRj/7u0aKSa3dGxyaguU+14WkHmVzZYToK7vX0ADfyAXBUQlaQ20PTHhAA==", "dev": true, "requires": { "@types/node": "*" @@ -108,9 +108,9 @@ "dev": true }, "@types/morgan": { - "version": "1.7.37", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.7.37.tgz", - "integrity": "sha512-tIdEA10BcHcOumMmUiiYdw8lhiVVq62r0ghih5Xpp4WETkfsMiTUZL4w9jCI502BBOrKhFrAOGml9IeELvVaBA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.0.tgz", + "integrity": "sha512-warrzirh5dlTMaETytBTKR886pRXwr+SMZD87ZE13gLMR8Pzz69SiYFkvoDaii78qGP1iyBIUYz5GiXyryO//A==", "dev": true, "requires": { "@types/express": "*" @@ -126,9 +126,9 @@ } }, "@types/node": { - "version": "13.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.6.tgz", - "integrity": "sha512-eyK7MWD0R1HqVTp+PtwRgFeIsemzuj4gBFSQxfPHY5iMjS7474e5wq+VFgTcdpyHeNxyKSaetYAjdMLJlKoWqA==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.0.tgz", + "integrity": "sha512-0ARSQootUG1RljH2HncpsY2TJBfGQIKOOi7kxzUY6z54ePu/ZD+wJA8zI2Q6v8rol2qpG/rvqsReco8zNMPvhQ==", "dev": true }, "@types/promise-ftp": { @@ -406,9 +406,9 @@ "dev": true }, "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -475,9 +475,9 @@ "dev": true }, "arg": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz", - "integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, "argparse": { @@ -708,6 +708,7 @@ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "dev": true, + "optional": true, "requires": { "file-uri-to-path": "1.0.0" } @@ -1718,9 +1719,9 @@ "dev": true }, "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, "encodeurl": { @@ -2097,7 +2098,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true + "dev": true, + "optional": true }, "fill-range": { "version": "7.0.1", @@ -3255,13 +3257,13 @@ "dev": true }, "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "dev": true, "requires": { "big.js": "^5.2.2", - "emojis-list": "^2.0.0", + "emojis-list": "^3.0.0", "json5": "^1.0.1" } }, @@ -3315,9 +3317,9 @@ } }, "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, "mamacro": { @@ -3699,12 +3701,6 @@ "propagate": "^2.0.0" } }, - "node-addon-api": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz", - "integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==", - "dev": true - }, "node-environment-flags": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", @@ -4674,16 +4670,6 @@ "inherits": "^2.0.1" } }, - "rpi-acu-rite-temperature": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/rpi-acu-rite-temperature/-/rpi-acu-rite-temperature-2.0.2.tgz", - "integrity": "sha512-kVDF8l6UMjcdTS3k+jXU8sn/IpfUoVElJv0IUZGDbWIpt2TAhVTeHd2d0OEJaBbQ1ROSlR663SlNOy5QjhVuRA==", - "dev": true, - "requires": { - "bindings": "^1.5.0", - "node-addon-api": "^2.0.0" - } - }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -5293,9 +5279,9 @@ } }, "terser": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", - "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.6.tgz", + "integrity": "sha512-4lYPyeNmstjIIESr/ysHg2vUPRGf2tzF9z2yYwnowXVuVzLEamPN1Gfrz7f8I9uEPuHcbFlW4PLIAsJoxXyJ1g==", "dev": true, "requires": { "commander": "^2.20.0", @@ -5496,9 +5482,9 @@ } }, "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", "dev": true }, "tty-browserify": { @@ -5542,15 +5528,15 @@ "dev": true }, "typescript": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz", - "integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", "dev": true, "requires": { "debug": "^2.2.0" @@ -6591,9 +6577,9 @@ } }, "webpack": { - "version": "4.41.6", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.6.tgz", - "integrity": "sha512-yxXfV0Zv9WMGRD+QexkZzmGIh54bsvEs+9aRWxnN8erLWEOehAKUTeNBoUbA6HPEZPlRo7KDi2ZcNveoZgK9MA==", + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.0.tgz", + "integrity": "sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w==", "dev": true, "requires": { "@webassemblyjs/ast": "1.8.5", @@ -6810,6 +6796,12 @@ "which": "^1.2.9" } }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -6827,6 +6819,17 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", diff --git a/server/package.json b/server/package.json index e3c97c6..7f58125 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "aw-clock-server", - "version": "2.0.2", + "version": "2.0.3", "license": "MIT", "author": "Kerry Shetline ", "private": true, @@ -11,7 +11,8 @@ "start": "nodemon --watch ./src/**/*.ts --ignore **/*.spec.ts -e js,mjs,json,ts --exec 'tsc && chmod +x ./tsc-out/app.js && node ./tsc-out/app.js'", "start-win": "nodemon --watch ./src/**/*.ts --ignore **/*.spec.ts -e js,mjs,json,ts --exec \"tsc && node ./tsc-out/app.js\"", "test": "mocha --require ts-node/register src/**/*.spec.ts --exit", - "build": "webpack && chmod +x ./dist/app.js" + "build": "webpack && chmod +x ./dist/app.js", + "build-win": "webpack" }, "dependencies": { "by-request": "^1.1.4", @@ -25,25 +26,24 @@ "request": "^2.88.2" }, "devDependencies": { - "@types/chai": "^4.2.9", + "@types/chai": "^4.2.10", "@types/cookie-parser": "^1.4.2", - "@types/express": "^4.17.2", + "@types/express": "^4.17.3", "@types/http-errors": "^1.6.3", "@types/mocha": "^5.2.7", - "@types/morgan": "^1.7.37", + "@types/morgan": "^1.9.0", "@types/nock": "^11.1.0", - "@types/node": "^13.7.6", + "@types/node": "^13.9.0", "@types/promise-ftp": "^1.3.4", "@types/request": "^2.48.4", "chai": "^4.2.0", "mocha": "^7.1.0", "nock": "^11.9.1", "nodemon": "^2.0.2", - "rpi-acu-rite-temperature": "^2.0.2", "ts-loader": "^6.2.1", "ts-node": "^8.6.2", - "typescript": "^3.8.2", - "webpack": "^4.41.6", + "typescript": "^3.8.3", + "webpack": "^4.42.0", "webpack-cli": "^3.3.11", "webpack-node-externals": "^1.7.2" } diff --git a/server/src/app.ts b/server/src/app.ts index 2698165..e9a64f3 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -17,7 +17,7 @@ const debug = require('debug')('express:server'); let indoorRouter: Router; -if (process.env.AWC_HAS_INDOOR_SENSOR) +if (process.env.AWC_HAS_INDOOR_SENSOR || process.env.AWC_ALT_DEV_SERVER) indoorRouter = require('./indoor-router').router; const allowCors = toBoolean(process.env.AWC_ALLOW_CORS); diff --git a/server/src/indoor-router.ts b/server/src/indoor-router.ts index 39ea3fb..c4ba319 100644 --- a/server/src/indoor-router.ts +++ b/server/src/indoor-router.ts @@ -1,5 +1,6 @@ import { jsonOrJsonp } from './common'; import { Request, Response, Router } from 'express'; +import request from 'request'; import { average, noCache, stdDev, toBoolean } from './util'; export const router = Router(); @@ -98,6 +99,18 @@ router.get('/', (req: Request, res: Response) => { else result = { temperature: lastTemp, humidity: lastHumidity }; } + else if (process.env.AWC_ALT_DEV_SERVER) { + req.pipe(request({ + url: process.env.AWC_ALT_DEV_SERVER + '/indoor', + method: req.method + })) + .on('error', err => { + res.status(500).send('Error connecting to development server: ' + err); + }) + .pipe(res); + + return; + } else { if (warnIndoorNA) { console.warn('Indoor temp/humidity sensor not available.'); diff --git a/server/src/temp-humidity-router.ts b/server/src/temp-humidity-router.ts index af48671..464bab4 100644 --- a/server/src/temp-humidity-router.ts +++ b/server/src/temp-humidity-router.ts @@ -1,5 +1,6 @@ import { Request, Response, Router } from 'express'; import { jsonOrJsonp } from './common'; +import request from 'request'; import { noCache } from './util'; export interface TempHumidityData { @@ -29,7 +30,7 @@ function removeOldData() { }); } -if (process.env.AWC_WIRELESS_TEMP) { +if (process.env.AWC_WIRELESS_TEMP && !process.env.AWC_ALT_DEV_SERVER) { try { ({ addSensorDataListener, removeSensorDataListener } = require('rpi-acu-rite-temperature')); @@ -64,7 +65,19 @@ router.get('/', (req: Request, res: Response) => { let result: any; - if (callbackId >= 0) { + if (process.env.AWC_ALT_DEV_SERVER) { + req.pipe(request({ + url: process.env.AWC_ALT_DEV_SERVER + '/wireless-th', + method: req.method + })) + .on('error', err => { + res.status(500).send('Error connecting to development server: ' + err); + }) + .pipe(res); + + return; + } + else if (callbackId >= 0) { removeOldData(); result = readings; } diff --git a/server/webpack.config.js b/server/webpack.config.js index b903efd..dee905c 100644 --- a/server/webpack.config.js +++ b/server/webpack.config.js @@ -35,5 +35,19 @@ module.exports = { devtool: 'source-map', plugins: [ new webpack.BannerPlugin({ banner: '#!/usr/bin/env node', raw: true }), + function () { + this.plugin('done', stats => { + if (stats.compilation.errors && stats.compilation.errors.length > 0) { + if (stats.compilation.errors.length === 0) + console.error(stats.compilation.errors[0]); + else { + console.error(stats.compilation.errors.map(err => + err && typeof err === 'object' && err.message ? err.message : '').join('\n')); + } + + process.exit(1); + } + }); + } ] }; diff --git a/src/app.service.ts b/src/app.service.ts index 1c6f9fa..909fe23 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -11,6 +11,7 @@ export interface AppService { getTimeInfo(bias?: number): TimeInfo; getWeatherServer(): string; isTimeAccelerated(): boolean; + proxySensorUpdate(): Promise; updateCurrentTemp(cth: CurrentTemperatureHumidity): void; updateTime(hour: number, minute: number, forceRefresh: boolean): void; updateSettings(newSettings: Settings); diff --git a/src/clock.ts b/src/clock.ts index c2305a3..978807a 100644 --- a/src/clock.ts +++ b/src/clock.ts @@ -158,11 +158,11 @@ export class Clock { const y1 = center + radius * Math.sin(Math.PI * (deg - 90) / 180); const tickMark = document.createElementNS(SVG_NAMESPACE, 'circle'); - tickMark.setAttributeNS(null, 'cx', x1.toString()); - tickMark.setAttributeNS(null, 'cy', y1.toString()); - tickMark.setAttributeNS(null, 'r', (i % 5 === 0 && i !== 60 ? 1 : 0.333).toString()); - tickMark.setAttributeNS(null, 'fill', 'white'); - tickMark.setAttributeNS(null, 'fill-opacity', '1'); + tickMark.setAttribute('cx', x1.toString()); + tickMark.setAttribute('cy', y1.toString()); + tickMark.setAttribute('r', (i % 5 === 0 && i !== 60 ? 1 : 0.333).toString()); + tickMark.setAttribute('fill', 'white'); + tickMark.setAttribute('fill-opacity', '1'); if (i > 55) { tickMark.setAttribute('id', 'dot-' + i); @@ -177,9 +177,9 @@ export class Clock { const y2 = center + textRadius * Math.sin(Math.PI * deg / 180); const text2 = document.createElementNS(SVG_NAMESPACE, 'text'); - text2.setAttributeNS(null, 'x', x2.toString()); - text2.setAttributeNS(null, 'y', y2.toString()); - text2.setAttributeNS(null, 'dy', '3.5'); + text2.setAttribute('x', x2.toString()); + text2.setAttribute('y', y2.toString()); + text2.setAttribute('dy', '3.5'); text2.classList.add('clock-face'); text2.textContent = h.toString(); this.clock.insertBefore(text2, this.hands); @@ -188,9 +188,9 @@ export class Clock { const y3 = center + constellationRadius * Math.sin(Math.PI * (-deg - 15) / 180); const text3 = document.createElementNS(SVG_NAMESPACE, 'text'); - text3.setAttributeNS(null, 'x', x3.toString()); - text3.setAttributeNS(null, 'y', y3.toString()); - text3.setAttributeNS(null, 'dy', '1'); + text3.setAttribute('x', x3.toString()); + text3.setAttribute('y', y3.toString()); + text3.setAttribute('dy', '1'); text3.classList.add('constellation'); text3.textContent = String.fromCodePoint(0x2648 + deg / 30); planetTracks.appendChild(text3); @@ -207,22 +207,22 @@ export class Clock { const text = document.createElementNS(SVG_NAMESPACE, 'text'); const path = document.createElementNS(SVG_NAMESPACE, 'path'); - rect.setAttributeNS(null, 'x', (x - 0.9).toString()); - rect.setAttributeNS(null, 'y', (center + dy - 2).toString()); - rect.setAttributeNS(null, 'width', '1.8'); - rect.setAttributeNS(null, 'height', '2.7'); - rect.setAttributeNS(null, 'fill', 'black'); + rect.setAttribute('x', (x - 0.9).toString()); + rect.setAttribute('y', (center + dy - 2).toString()); + rect.setAttribute('width', '1.8'); + rect.setAttribute('height', '2.7'); + rect.setAttribute('fill', 'black'); planetTracks.appendChild(rect); - text.setAttributeNS(null, 'x', x.toString()); - text.setAttributeNS(null, 'y', centerStr); - text.setAttributeNS(null, 'dy', dy.toString()); + text.setAttribute('x', x.toString()); + text.setAttribute('y', centerStr); + text.setAttribute('dy', dy.toString()); text.classList.add('constellation'); text.textContent = String.fromCodePoint(planet); planetTracks.appendChild(text); - path.setAttributeNS(null, 'fill', 'none'); - path.setAttributeNS(null, 'visibility', 'inherited'); + path.setAttribute('fill', 'none'); + path.setAttribute('visibility', 'inherited'); path.classList.add('risen-track'); path.id = `risen-${planetIds[index]}`; risenTracks.appendChild(path); @@ -244,10 +244,10 @@ export class Clock { const labelX = (r1.x + r1.width - r0.x) * scale; const captionX = labelX + Math.max(r2.width, r3.width) * scale; - this.dut1Label.setAttributeNS(null, 'x', labelX.toString()); - this.dtaiLabel.setAttributeNS(null, 'x', labelX.toString()); - this.dut1Caption.setAttributeNS(null, 'x', captionX.toString()); - this.dtaiCaption.setAttributeNS(null, 'x', captionX.toString()); + this.dut1Label.setAttribute('x', labelX.toString()); + this.dtaiLabel.setAttribute('x', labelX.toString()); + this.dut1Caption.setAttribute('x', captionX.toString()); + this.dtaiCaption.setAttribute('x', captionX.toString()); } private tick(): void { diff --git a/src/current-temp-manager.ts b/src/current-temp-manager.ts index 0267075..561cf4f 100644 --- a/src/current-temp-manager.ts +++ b/src/current-temp-manager.ts @@ -29,6 +29,8 @@ export class CurrentTempManager { private readonly cth: CurrentTemperatureHumidity = {}; + private hideIndoor = false; + constructor(private appService: AppService) { this.currentTempBalanceSpace = $('#curr-temp-balance-space'); this.feelsLike = $('#feels-like'); @@ -40,9 +42,19 @@ export class CurrentTempManager { this.temperatureDetail = $('#temperature-detail'); if (!localServer) { + this.hideIndoor = true; this.currentTempBalanceSpace.css('display', 'none'); this.indoorHumidity.text(''); this.indoorTemp.text(''); + + appService.proxySensorUpdate().then(available => { + if (available) { + this.hideIndoor = false; + this.currentTempBalanceSpace.css('display', 'block'); + this.indoorHumidity.text(DD); + this.indoorTemp.text(DD + '°'); + } + }); } } @@ -56,7 +68,7 @@ export class CurrentTempManager { const indoorOption = this.appService.getIndoorOption(); const outdoorOption = this.appService.getOutdoorOption(); - if (indoorOption === 'X' || !localServer) { + if (indoorOption === 'X' || this.hideIndoor) { this.currentTempBalanceSpace.css('display', 'none'); this.indoorHumidity.text(''); this.indoorTemp.text(''); @@ -83,10 +95,10 @@ export class CurrentTempManager { if (temperature != null && this.cth.forecastTemp != null && Math.abs(temperature - this.cth.forecastTemp) > delta && !this.cth.forecastStale) { temperature = Math.min(Math.max(this.cth.forecastTemp - delta, temperature), this.cth.forecastTemp + delta); - this.outdoorTemp.addClass('forecast-substitution'); + this.outdoorTemp.addClass('forecast-limited'); } else - this.outdoorTemp.removeClass('forecast-substitution'); + this.outdoorTemp.removeClass('forecast-limited'); this.outdoorHumidity.text(`${humidity != null ? Math.round(humidity) : DD}%`); this.outdoorTemp.text(`\u00A0${temperature != null ? Math.round(temperature) : DD}°`); diff --git a/src/ephemeris.ts b/src/ephemeris.ts index 047d99c..f015849 100644 --- a/src/ephemeris.ts +++ b/src/ephemeris.ts @@ -130,7 +130,7 @@ export class Ephemeris { rotate(elem, -eclipticLongitude); elem.css('stroke-width', risen ? '0' : '0.25'); - elem[0].setAttributeNS(null, 'r', altitude < targetAltitude ? '0.625' : '0.75'); + elem[0].setAttribute('r', altitude < targetAltitude ? '0.625' : '0.75'); if (risen) { rise = eventFinder.findEvent(planet, RISE_EVENT, time_JDU + 2 / 1400, observer, timezone, null, true); @@ -159,7 +159,7 @@ export class Ephemeris { const arc = describeArc(50, 50, radius, setAngle, riseAngle); - risenTrack[0].setAttributeNS(null, 'd', arc); + risenTrack[0].setAttribute('d', arc); risenTrack.css('visibility', 'inherited'); } else diff --git a/src/index.html b/src/index.html index de5aa8a..e8e60f5 100644 --- a/src/index.html +++ b/src/index.html @@ -78,8 +78,8 @@  --° - - + + @@ -88,8 +88,8 @@ - - + + @@ -98,8 +98,8 @@ - - + + @@ -256,7 +256,7 @@
- 2.0.2 + 2.0.3 diff --git a/src/main.ts b/src/main.ts index 3d84939..483ea75 100644 --- a/src/main.ts +++ b/src/main.ts @@ -75,6 +75,7 @@ class AwClockApp implements AppService { private lastTimezone: KsTimeZone; private lastHour = -1; private frequent = false; + private proxyStatus: boolean | Promise = undefined; private settings = new Settings(); @@ -137,6 +138,28 @@ class AwClockApp implements AppService { return ntpPoller.getTimeInfo(bias); } + proxySensorUpdate(): Promise { + if (this.proxyStatus instanceof Promise) + return this.proxyStatus; + else if (typeof this.proxyStatus === 'boolean') + return Promise.resolve(this.proxyStatus); + + this.proxyStatus = new Promise(resolve => $.ajax({ + url: '/wireless-th', + dataType: 'json', + success: data => { + (this.proxyStatus as Promise).then(() => this.clock.triggerRefresh()); + resolve(this.proxyStatus = typeof data === 'object' && data?.error !== 'n/a'); + }, + error: () => { + (this.proxyStatus as Promise).then(() => this.clock.triggerRefresh()); + resolve(this.proxyStatus = false); + } + })); + + return this.proxyStatus; + } + getWeatherServer(): string { return weatherServer; } diff --git a/src/sensors.ts b/src/sensors.ts index 53f7437..d720094 100644 --- a/src/sensors.ts +++ b/src/sensors.ts @@ -25,7 +25,7 @@ import { CurrentTemperatureHumidity } from './current-temp-manager'; import { updateSvgFlowItems } from './svg-flow'; import { localServer, runningDev } from './settings'; -const DEV_SENSOR_URL = 'http://192.168.42.98:8080'; +const DEV_SENSOR_URL = 'http://localhost:4201'; function errorText(err: any): string { err = err instanceof Error ? err.message : err.error; @@ -36,7 +36,7 @@ function errorText(err: any): string { function getJson(url: string): Promise { return new Promise(resolve => { // `$.ajax()` returns a Promise, but if I try to use that Promise directly, I can't find a way to get - // around "Uncaught (in promise)" errors, when what I was is a Promise resolved with an Error value. + // around "Uncaught (in promise)" errors, when what I want is a Promise resolved with an Error value. // noinspection JSIgnoredPromiseFromCall $.ajax({ url, @@ -65,7 +65,7 @@ export class Sensors { private readonly outdoorMeter: JQuery; private readonly outdoorMeter2: JQuery; - private readonly indoorAvailable: boolean; + private indoorAvailable: boolean; private wiredAvailable = false; private wirelessAvailable = false; @@ -83,12 +83,16 @@ export class Sensors { this.indoorMeter.css('display', 'none'); this.outdoorMeter.css('display', 'none'); this.outdoorMeter2.css('display', 'none'); + + appService.proxySensorUpdate().then(available => + this.indoorAvailable = this.wiredAvailable = this.wirelessAvailable = available); } } get available() { return this.wiredAvailable || this.wirelessAvailable; } public update(celsius: boolean) { + const adjustTemp = (temp: number) => (celsius || temp == null ? temp : temp * 1.8 + 32); const site = (runningDev ? DEV_SENSOR_URL : ''); const wiredUrl = `${site}/indoor`; const wirelessUrl = `${site}/wireless-th`; @@ -100,7 +104,7 @@ export class Sensors { this.indoorMeter.css('display', this.wiredAvailable && /[ABC]{1,2}/.test(indoorOption) ? 'block' : 'none'); this.outdoorMeter.css('display', this.wirelessAvailable && /[ABC]{1,2}/.test(outdoorOption) ? 'block' : 'none'); - if (outdoorOption.length === 2) { + if (outdoorOption.length === 2 && this.wirelessAvailable) { this.outdoorMeter2.css('display', 'block'); newFlowSpec = flowSpec.replace(/(.*\bdx=)[-.\d]+(\b.*)/, '$1-6.3$2'); } @@ -110,7 +114,7 @@ export class Sensors { } if (newFlowSpec !== flowSpec) { - this.outdoorMeter[0].setAttributeNS(null, 'svg-flow', newFlowSpec); + this.outdoorMeter[0].setAttribute('svg-flow', newFlowSpec); updateSvgFlowItems(); } @@ -142,7 +146,7 @@ export class Sensors { this.wiredAvailable = !(/not found/i.test(err)); } else if (wired && indoorOption === 'D') { - cth.indoorTemp = (celsius ? wired.temperature : wired.temperature * 1.8 + 32); + cth.indoorTemp = adjustTemp(wired.temperature); cth.indoorHumidity = wired.humidity; } @@ -164,7 +168,7 @@ export class Sensors { else if (wireless) { if ((thd = wireless[indoorOption])) { if (thd.reliable) { - cth.indoorTemp = (celsius ? thd.temperature : thd.temperature * 1.8 + 32); + cth.indoorTemp = adjustTemp(thd.temperature); cth.indoorHumidity = thd.humidity; } @@ -192,7 +196,7 @@ export class Sensors { signalQs.push(thd.signalQuality); if (thd.temperature !== undefined && thd.reliable) { - const t = Math.round(celsius ? thd.temperature : thd.temperature * 1.8 + 32); + const t = Math.round(adjustTemp(thd.temperature)); sensorDetail.push(`${channel}: ${t}°` + (thd.reliable ? '' : '?')); diff --git a/src/settings-dialog.ts b/src/settings-dialog.ts index fa29549..2a5b52a 100644 --- a/src/settings-dialog.ts +++ b/src/settings-dialog.ts @@ -17,11 +17,11 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import * as $ from 'jquery'; -import { domAlert, htmlEncode, popKeydownListener, pushKeydownListener } from './util'; -import { localServer, Settings } from './settings'; import { AppService } from './app.service'; +import * as $ from 'jquery'; import { isIE, isSafari } from 'ks-util'; +import { localServer, Settings } from './settings'; +import { domAlert, htmlEncode, popKeydownListener, pushKeydownListener } from './util'; const ERROR_BACKGROUND = '#FCC'; const WARNING_BACKGROUND = '#FFC'; @@ -142,8 +142,17 @@ export class SettingsDialog { }); if (!localServer) { + // Hide indoor/outdoor options by default if this isn't a local server, but check if proxied data + // is available, and if so, bring the options back. this.indoorOutdoorOptions.css('display', 'none'); this.searchSection.addClass('no-indoor-outdoor'); + + appService.proxySensorUpdate().then(available => { + if (available) { + this.indoorOutdoorOptions.css('display', 'block'); + this.searchSection.removeClass('no-indoor-outdoor'); + } + }); } if (isIE()) { diff --git a/src/styles.scss b/src/styles.scss index 89b5909..c61501f 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -600,8 +600,8 @@ input, button, select, textarea, optgroup, option { // Ignored by macOS Chrome f font-size: 2px; } -.forecast-substitution { - fill: #AFC; +.forecast-limited { + fill: #FFB; } #temperature-detail { diff --git a/src/svg-flow.ts b/src/svg-flow.ts index 5dbfaf5..c8ca741 100644 --- a/src/svg-flow.ts +++ b/src/svg-flow.ts @@ -30,14 +30,15 @@ export function updateSvgFlowItems(): void { }); } -interface SimpleRect { x: number, y: number, width: number, height: number } +interface GeneralRect { x: number, y: number, width: number, height: number } -function getBBox(elem: SVGGraphicsElement): SimpleRect { +function getBBox(elem: SVGGraphicsElement): GeneralRect { if (elem.localName !== 'tspan') return elem.getBBox(); const text = elem as SVGTextContentElement; - const extent = text.getNumberOfChars() > 0 ? text.getExtentOfChar(0) : { x: 0, y: 0, height: 0 }; + const extent = text.getNumberOfChars() > 0 || text.textContent?.length > 0 ? + text.getExtentOfChar(0) : { x: 0, y: 0, height: 0 }; const width = text.getComputedTextLength(); return { x: extent.x, y: extent.y, width, height: extent.height }; @@ -57,8 +58,8 @@ export function reflow(): void { (item.anchorCorner.charAt(0) === 'b' ? r1.height : 0) - (item.elemCorner.charAt(0) === 'b' ? r2.height : 0) + item.dy; - item.elem.setAttributeNS(null, 'x', labelX.toString()); - item.elem.setAttributeNS(null, 'y', labelY.toString()); + item.elem.setAttribute('x', labelX.toString()); + item.elem.setAttribute('y', labelY.toString()); } }); } diff --git a/webpack.config.js b/webpack.config.js index 3f175d7..7dfa449 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -442,7 +442,21 @@ module.exports = { fallbackModuleFilenameTemplate: '[resource-path]?[hash]', sourceRoot: 'webpack:///' }), - new NamedModulesPlugin({}) + new NamedModulesPlugin({}), + function () { + this.plugin('done', stats => { + if (stats.compilation.errors && stats.compilation.errors.length > 0) { + if (stats.compilation.errors.length === 0) + console.error(stats.compilation.errors[0]); + else { + console.error(stats.compilation.errors.map(err => + err && typeof err === 'object' && err.message ? err.message : '').join('\n')); + } + + process.exit(1); + } + }); + } ], node: { global: true