diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..b4e426a8 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,107 @@ +name: Publish Docker image + +# Tag-triggered. Mirrors publish.yml (npm) on cadence so a single +# release tag fans out to both the npm SignalK webapp bundle and the +# standalone Docker image. Pre-release tags (v*-alpha / v*-beta / +# v*-rc) publish under the version tag but skip the :latest move. + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + # Native runners per arch - free for public repos, much + # faster than QEMU emulation under ubuntu-latest. Each job + # publishes an arch-tagged image; the manifest job stitches + # them into a multi-arch tag. + - { arch: amd64, runner: ubuntu-latest } + - { arch: arm64, runner: ubuntu-24.04-arm } + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute version + image tags + id: meta + run: | + set -euo pipefail + tag="${GITHUB_REF#refs/tags/}" + version="${tag#v}" + repo_lower="$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "image=ghcr.io/${repo_lower}" >> "$GITHUB_OUTPUT" + echo "arch_tag=ghcr.io/${repo_lower}:${version}-${{ matrix.arch }}" >> "$GITHUB_OUTPUT" + + - name: Build and push (${{ matrix.arch }}) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + platforms: linux/${{ matrix.arch }} + tags: ${{ steps.meta.outputs.arch_tag }} + provenance: true + # GitHub Actions cache: arch-scoped so amd64 and arm64 + # don't trample each other. + cache-from: type=gha,scope=docker-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=docker-${{ matrix.arch }} + + manifest: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compose multi-arch manifest + run: | + set -euo pipefail + tag="${GITHUB_REF#refs/tags/}" + version="${tag#v}" + repo_lower="$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" + image="ghcr.io/${repo_lower}" + + # The unversioned :latest tag only moves for stable + # releases. Pre-release suffixes (alpha/beta/rc) still + # get :version published but won't pull as :latest, so a + # `docker pull :latest` stays on the last GA. + latest_args=() + if [[ "$version" != *alpha* && "$version" != *beta* && "$version" != *rc* ]]; then + latest_args=(--tag "${image}:latest") + fi + + docker buildx imagetools create \ + --tag "${image}:${version}" \ + "${latest_args[@]}" \ + "${image}:${version}-amd64" \ + "${image}:${version}-arm64" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9fbd3de2..128251fd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -67,6 +67,12 @@ jobs: node-version: '22.x' registry-url: 'https://registry.npmjs.org' + # esbuild is required by the MinifyPublishedJs target in + # OnaPlotter.csproj. The Release publish below fails with a + # clear MSBuild error if node_modules/esbuild is missing. + - name: Install JS toolchain (esbuild) + run: npm ci + - name: Publish OnaPlotter (Release + AOT) run: dotnet publish OnaPlotter/OnaPlotter.csproj -c Release -o publish diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..ce6497a0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,91 @@ +# syntax=docker/dockerfile:1.7 +# +# Multi-stage build for OnaPlotter as a standalone web server. Two +# AOTs in play - WASM AOT compiles the Blazor client to WebAssembly +# (slow), NativeAOT compiles the Kestrel host to a self-contained +# native binary. Final image lands around 35 to 50 MB depending on +# arch and WASM payload size, on top of the ~10 MB Alpine +# runtime-deps base. +# +# Image layout: /app/OnaPlotter.Server (the native binary) and +# /app/wwwroot (the Blazor static bundle). The host reads SK_SERVER_URL +# and BASE_HREF env vars at startup and templates wwwroot/appsettings.json +# and wwwroot/index.html before the first request - see +# OnaPlotter.Server/Program.cs ApplyRuntimeConfig. +# +# Run: +# docker run -p 8080:8080 \ +# -e SK_SERVER_URL=https://my-sk:3000 \ +# ghcr.io/msallin/ona-plotter: + +ARG DOTNET_VERSION=10.0 + +# -------- Build stage -------- +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-alpine AS build + +# NativeAOT toolchain prerequisites on Alpine. The .NET SDK image +# ships without a native C toolchain so we install it here once. +# build-base pulls gcc + binutils + libc-dev; clang + lld give +# NativeAOT a usable linker; zlib-dev is the one runtime dep the +# linker actually wants. nodejs + npm satisfy the esbuild step +# (the MinifyPublishedJs MSBuild target in OnaPlotter.csproj runs +# `npx esbuild` against the JS interop layer before the bundle is +# copied to /app/publish). +RUN apk add --no-cache build-base clang lld zlib-dev nodejs npm + +WORKDIR /src + +# JS deps first - small layer, rarely changes once esbuild + lint +# tooling pins are stable. Restored before .NET so an esbuild bump +# doesn't bust the dotnet restore cache. +COPY package.json package-lock.json ./ +RUN npm ci --omit=optional + +# .NET restore next, again as its own layer. Copying just the +# project files (not sources) lets a code change reuse the restored +# package cache. +COPY OnaPlotter.slnx global.json ./ +COPY OnaPlotter/OnaPlotter.csproj OnaPlotter/ +COPY OnaPlotter.Server/OnaPlotter.Server.csproj OnaPlotter.Server/ +RUN dotnet workload install wasm-tools \ + && dotnet restore OnaPlotter.Server/OnaPlotter.Server.csproj + +# Source. Order keeps the layer cache: only this and later layers +# rebuild on a source change. +COPY OnaPlotter/ OnaPlotter/ +COPY OnaPlotter.Server/ OnaPlotter.Server/ + +# Publish. Hits both AOTs in one invocation: +# 1. OnaPlotter (the WASM client) - IL to WebAssembly, +# InvariantGlobalization + WasmStripILAfterAOT + Speed +# optimisation from its own csproj. +# 2. OnaPlotter.Server (the Kestrel host) - IL to native code +# for the container arch, single-file self-contained. +# Both projects' AOT settings already gate on Configuration=Release, +# so this is the only switch needed. +RUN dotnet publish OnaPlotter.Server/OnaPlotter.Server.csproj \ + -c Release \ + -o /app/publish + +# -------- Runtime stage -------- +FROM mcr.microsoft.com/dotnet/runtime-deps:${DOTNET_VERSION}-alpine + +WORKDIR /app + +# --chown so the non-root user can write templated wwwroot files +# at container start. ApplyRuntimeConfig mutates appsettings.json +# and index.html from env vars; both live under wwwroot. +COPY --from=build --chown=$APP_UID:$APP_UID /app/publish ./ + +ENV ASPNETCORE_URLS=http://+:8080 \ + DOTNET_RUNNING_IN_CONTAINER=true + +EXPOSE 8080 + +# runtime-deps:alpine sets APP_UID=1654 as a non-root user. Reuse +# it rather than running as root. +USER $APP_UID + +# Native binary; no `dotnet` prefix because PublishAot produces a +# self-contained executable. +ENTRYPOINT ["./OnaPlotter.Server"] diff --git a/OnaPlotter.Server/OnaPlotter.Server.csproj b/OnaPlotter.Server/OnaPlotter.Server.csproj new file mode 100644 index 00000000..08c4b292 --- /dev/null +++ b/OnaPlotter.Server/OnaPlotter.Server.csproj @@ -0,0 +1,42 @@ + + + + net10.0 + enable + enable + + true + + true + + true + + + + + + + + + + diff --git a/OnaPlotter.Server/Program.cs b/OnaPlotter.Server/Program.cs new file mode 100644 index 00000000..988de1c3 --- /dev/null +++ b/OnaPlotter.Server/Program.cs @@ -0,0 +1,164 @@ +// Kestrel host for the OnaPlotter Blazor WebAssembly bundle. Two +// shapes consume this entry point: +// +// 1. Docker: the multi-stage Dockerfile publishes this project +// as a NativeAOT-compiled, self-contained native binary that +// runs on top of mcr.microsoft.com/dotnet/runtime-deps:alpine. +// Container image lands at ~35 to 50 MB total. +// +// 2. Local dev: `dotnet run --project OnaPlotter.Server` is a +// fast iteration path that exercises the Docker-shape +// hosting (env-driven appsettings, base-href templating, +// SPA fallback) without spinning up a container. NativeAOT is +// gated on Release publish, so dev runs use the regular JIT. +// +// Standalone WASM dev (`dotnet run --project OnaPlotter`) still +// works unchanged; that path runs the Blazor app's own dev server, +// which is faster for tight UI iteration. This Server is for +// end-to-end testing of the deployable shape. +// +// Runtime configuration via env vars: +// +// SK_SERVER_URL - written into wwwroot/appsettings.json as +// SignalK:ServerUrl. Default "auto" = the WASM +// client uses the page origin (Settings > +// Standalone mode can still override at runtime). +// +// BASE_HREF - written into wwwroot/index.html's . +// Default "/". Set to "/onaplotter/" if hosting +// behind a reverse proxy at that subpath. +// +// ASPNETCORE_URLS - standard ASP.NET Core; Dockerfile sets it to +// http://+:8080 to bind all interfaces on 8080. + +// CreateBuilder, not CreateSlimBuilder: the slim variant skips the +// configuration sources that wire in the static-web-asset dev +// manifest, which the SPA fallback to index.html depends on under +// `dotnet run`. NativeAOT in net10 is compatible with CreateBuilder; +// CreateSlimBuilder is a startup-time / binary-size optimisation, +// not a hard requirement. +var builder = WebApplication.CreateBuilder(args); + +var app = builder.Build(); + +// Template the static bundle from env vars before the first request. +// Idempotent: re-running with the same env produces byte-identical +// files, so a container restart is safe and a hot-reload of env vars +// is one restart away. Writing on every cold start avoids the +// surprise of stale files when the deployer flips an env var. +ApplyRuntimeConfig(app.Environment.WebRootPath, app.Logger); + +if (!app.Environment.IsDevelopment()) +{ + // Brotli-precompressed .wasm / .js are shipped by the Blazor + // publish; the framework files middleware below picks them up + // automatically when the client advertises Accept-Encoding: br. + // Nothing additional needed here. +} + +app.UseBlazorFrameworkFiles(); +app.UseStaticFiles(); + +// SPA fallback: any GET that doesn't match a static file gets +// index.html so the Blazor client-side router can take over. The +// browser then loads /map, /settings etc. via in-app routing rather +// than a server-side 404 on reload. +app.MapFallbackToFile("index.html"); + +app.Run(); + +static void ApplyRuntimeConfig(string? webRootPath, ILogger logger) +{ + var skServerUrl = Environment.GetEnvironmentVariable("SK_SERVER_URL"); + var baseHref = Environment.GetEnvironmentVariable("BASE_HREF"); + if (string.IsNullOrWhiteSpace(skServerUrl) && string.IsNullOrWhiteSpace(baseHref)) + { + // Neither env var set; nothing to template. Skip silently so + // the `dotnet run` path stays log-quiet for defaults. + return; + } + + if (string.IsNullOrEmpty(webRootPath) || !Directory.Exists(webRootPath)) + { + // Reached in `dotnet run` if either env var is set: dev mode + // serves the Blazor files from the WASM project's wwwroot via + // the static web asset manifest, not from the Server's own + // wwwroot, so there's nothing physical to template here. + // Surface a warning so the operator notices the env vars + // aren't being applied; the deployed (publish) shape is + // where templating actually runs. + logger.LogWarning( + "Env-driven config requested but wwwroot is not materialized at {Path}; " + + "this is normal under `dotnet run`. The published Docker image will template " + + "settings as expected.", + webRootPath); + return; + } + + if (!string.IsNullOrWhiteSpace(skServerUrl)) + { + var appSettingsPath = Path.Combine(webRootPath, "appsettings.json"); + // Hand-rolled JSON so we don't pull JsonSerializer into the + // AOT graph for what is a two-field object. The shape is + // stable: { "SignalK": { "ServerUrl": "..." } } - matches + // what the WASM client reads in SignalKBaseUrl.cs. + var escaped = skServerUrl.Replace("\\", "\\\\").Replace("\"", "\\\""); + var json = $"{{ \"SignalK\": {{ \"ServerUrl\": \"{escaped}\" }} }}\n"; + File.WriteAllText(appSettingsPath, json); + logger.LogInformation( + "Wrote SignalK:ServerUrl = {Url} to appsettings.json", skServerUrl); + } + + if (!string.IsNullOrWhiteSpace(baseHref)) + { + var indexPath = Path.Combine(webRootPath, "index.html"); + if (File.Exists(indexPath)) + { + // Two normalisations on the env var: + // 1. Force a leading slash so "onaplotter/" becomes + // "/onaplotter/" (no relative-base footgun). + // 2. Force a trailing slash so the browser resolves + // sibling resources correctly. + // resolves "_framework/foo.js" against "/" - we + // need "/x/" to land inside the subpath. + var normalized = baseHref; + if (!normalized.StartsWith('/')) normalized = "/" + normalized; + if (!normalized.EndsWith('/')) normalized += "/"; + + var html = File.ReadAllText(indexPath); + // Substitute the href value inside the existing + // tag. Blazor's publish emits this in + // a consistent shape, so the literal-match here is + // robust enough and keeps the AOT graph free of the + // regex engine. Find the tag opening, walk to the next + // double-quote, slice in the new value. + const string startTag = " tag found in index.html"); + } + else + { + var valueStart = startIdx + startTag.Length; + var valueEnd = html.IndexOf('"', valueStart); + if (valueEnd < 0) + { + logger.LogWarning( + "index.html has a malformed tag; skipping patch"); + } + else + { + var patched = string.Concat( + html.AsSpan(0, valueStart), + normalized, + html.AsSpan(valueEnd)); + File.WriteAllText(indexPath, patched); + logger.LogInformation( + "Patched in index.html to {BaseHref}", normalized); + } + } + } + } +} diff --git a/OnaPlotter.Server/Properties/launchSettings.json b/OnaPlotter.Server/Properties/launchSettings.json new file mode 100644 index 00000000..01a9b503 --- /dev/null +++ b/OnaPlotter.Server/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5100", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/OnaPlotter.slnx b/OnaPlotter.slnx index ee65fe17..8168d88c 100644 --- a/OnaPlotter.slnx +++ b/OnaPlotter.slnx @@ -1,5 +1,6 @@ + diff --git a/OnaPlotter/OnaPlotter.csproj b/OnaPlotter/OnaPlotter.csproj index 038db78b..d4a071b6 100644 --- a/OnaPlotter/OnaPlotter.csproj +++ b/OnaPlotter/OnaPlotter.csproj @@ -68,6 +68,20 @@ + + + + + + + + + + + + + + <_JsToMinify Include="$(PublishDir)wwwroot\js\**\*.js" /> + + + + + diff --git a/package-lock.json b/package-lock.json index 4328822e..8f8293b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,436 @@ "name": "ona-plotter-lint", "version": "0.0.0", "devDependencies": { + "esbuild": "^0.24.0", "eslint": "^9.17.0", "globals": "^15.14.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -424,6 +850,47 @@ "dev": true, "license": "MIT" }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", diff --git a/package.json b/package.json index 87f55950..853c43c8 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "lint": "eslint OnaPlotter/wwwroot/js" }, "devDependencies": { + "esbuild": "^0.24.0", "eslint": "^9.17.0", "globals": "^15.14.0" }