From 567690831c4ce54c7eb3fd49b9667bdba0f401d6 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 5 Aug 2026 22:15:00 -0700 Subject: [PATCH] feat: Show the build date on the loading banner of unreleased builds (#2659) --- scripts/build-date-plugin.js | 40 ++++++++++++++++++++++++++++++++++ scripts/build-defines.js | 11 ++++++++-- src/components/loading.ts | 9 ++++++-- src/scss/loading.scss | 5 +++++ src/utils/build-info.ts | 11 ++++++++++ tests/utils/build-info.test.ts | 12 +++++++++- vite.config.ts | 2 ++ 7 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 scripts/build-date-plugin.js diff --git a/scripts/build-date-plugin.js b/scripts/build-date-plugin.js new file mode 100644 index 00000000..b516871b --- /dev/null +++ b/scripts/build-date-plugin.js @@ -0,0 +1,40 @@ +/** + * Vite plugin: writes the date of the build into the card. + */ + +// Strict length requirement: See below. +export const BUILD_DATE_PLACEHOLDER = '__BUILD_DATE_SENTINEL___'; + +/** + * The date cannot be handed to the bundler as a substituted name, because the + * build configuration is evaluated once and `vite build --watch` reuses it. + * As such, every rebuild would report the time the watcher started rather than + * the actual build time. Output is generated afresh for each build, so the date + * is written here instead. + * + * @type {() => import('vite').Plugin} + */ +export const buildDate = () => ({ + name: 'build-date', + + renderChunk(code) { + if (!code.includes(BUILD_DATE_PLACEHOLDER)) { + return null; + } + + const date = new Date().toISOString(); + if (date.length !== BUILD_DATE_PLACEHOLDER.length) { + this.error( + 'The build date must be exactly as long as the placeholder it replaces.', + ); + } + + return { + code: code.replaceAll(BUILD_DATE_PLACEHOLDER, date), + + // The replacement is exactly as long as what it replaces, so the existing + // sourcemap still describes the code. + map: null, + }; + }, +}); diff --git a/scripts/build-defines.js b/scripts/build-defines.js index 71091d55..dd01b06e 100644 --- a/scripts/build-defines.js +++ b/scripts/build-defines.js @@ -1,6 +1,8 @@ import { execFileSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; +import { BUILD_DATE_PLACEHOLDER } from './build-date-plugin.js'; + /** * Asks git something or gives back nothing when it cannot be asked. */ @@ -23,7 +25,11 @@ const getPackageVersion = () => * * `releaseVersion` is the version being released, which only the release * workflow knows; a build without one reports the version in `package.json`, or - * a development build the commit it was made from. + * a development build the commit it was made from. Its presence is what makes a + * build a released one. + * + * The build date is a placeholder here, written in for by the `buildDate` + * plugin as each build's output is generated. * * The values are JSON so that a bundler can drop them in as written. */ @@ -35,10 +41,11 @@ export const getBuildDefines = ({ dev, releaseVersion }) => { __ADVANCED_CAMERA_CARD_RELEASE_VERSION__: JSON.stringify( releaseVersion ?? (dev ? developmentVersion : getPackageVersion()), ), + __ADVANCED_CAMERA_CARD_IS_RELEASE_BUILD__: JSON.stringify(!!releaseVersion), __ADVANCED_CAMERA_CARD_GIT_HASH__: JSON.stringify(gitHash), __ADVANCED_CAMERA_CARD_GIT_DATE__: JSON.stringify( askGit('log', '-1', '--format=%cI'), ), - __ADVANCED_CAMERA_CARD_BUILD_DATE__: JSON.stringify(new Date().toISOString()), + __ADVANCED_CAMERA_CARD_BUILD_DATE__: JSON.stringify(BUILD_DATE_PLACEHOLDER), }; }; diff --git a/src/components/loading.ts b/src/components/loading.ts index f9d76681..30aa811d 100644 --- a/src/components/loading.ts +++ b/src/components/loading.ts @@ -9,7 +9,8 @@ import { customElement, property } from 'lit/decorators.js'; import loadingStyle from '../scss/loading.scss?inline'; import type { EffectName, EffectsManagerInterface } from '../types'; -import { getReleaseVersion } from '../utils/build-info.js'; +import { formatDateAndTime } from '../utils/basic.js'; +import { getReleaseVersion, getUnreleasedBuildDate } from '../utils/build-info.js'; import './icon'; @@ -80,10 +81,14 @@ export class AdvancedCameraCardLoading extends LitElement { } protected render(): TemplateResult { + const buildDate = getUnreleasedBuildDate(); + return html`${getReleaseVersion()}`; + >${getReleaseVersion()} ${buildDate + ? html`${formatDateAndTime(buildDate, true)}` + : ''}`; } static get styles(): CSSResultGroup { diff --git a/src/scss/loading.scss b/src/scss/loading.scss index d1077f50..5ff3e7be 100644 --- a/src/scss/loading.scss +++ b/src/scss/loading.scss @@ -53,3 +53,8 @@ advanced-camera-card-icon { span { font-size: x-large; } + +.build-date { + font-size: medium; + opacity: 0.7; +} diff --git a/src/utils/build-info.ts b/src/utils/build-info.ts index 92eb6067..5794721a 100644 --- a/src/utils/build-info.ts +++ b/src/utils/build-info.ts @@ -1,4 +1,5 @@ declare const __ADVANCED_CAMERA_CARD_RELEASE_VERSION__: string | undefined; +declare const __ADVANCED_CAMERA_CARD_IS_RELEASE_BUILD__: boolean | undefined; declare const __ADVANCED_CAMERA_CARD_GIT_HASH__: string | undefined; declare const __ADVANCED_CAMERA_CARD_GIT_DATE__: string | undefined; declare const __ADVANCED_CAMERA_CARD_BUILD_DATE__: string | undefined; @@ -25,6 +26,14 @@ const BUILD_DATE = typeof __ADVANCED_CAMERA_CARD_BUILD_DATE__ === 'undefined' ? undefined : __ADVANCED_CAMERA_CARD_BUILD_DATE__; + +const IS_RELEASE_BUILD = + typeof __ADVANCED_CAMERA_CARD_IS_RELEASE_BUILD__ === 'undefined' + ? false + : __ADVANCED_CAMERA_CARD_IS_RELEASE_BUILD__; + +const UNRELEASED_BUILD_DATE = + !IS_RELEASE_BUILD && BUILD_DATE ? new Date(BUILD_DATE) : null; /* v8 ignore stop -- @preserve */ export interface GitInfo { @@ -34,6 +43,8 @@ export interface GitInfo { } export const getReleaseVersion = (): string => RELEASE_VERSION; +export const getUnreleasedBuildDate = (): Date | null => UNRELEASED_BUILD_DATE; + export const getGitInfo = (): GitInfo => ({ hash: GIT_HASH, commitDate: GIT_DATE, diff --git a/tests/utils/build-info.test.ts b/tests/utils/build-info.test.ts index 3d258fc9..13fae50d 100644 --- a/tests/utils/build-info.test.ts +++ b/tests/utils/build-info.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { getGitInfo, getReleaseVersion } from '../../src/utils/build-info'; +import { + getGitInfo, + getReleaseVersion, + getUnreleasedBuildDate, +} from '../../src/utils/build-info'; // As these are running as tests, the won't be build substitutes so this only // tests default/fallback values. @@ -16,3 +20,9 @@ describe('getGitInfo', () => { expect(getGitInfo()).toEqual({}); }); }); + +describe('getUnreleasedBuildDate', () => { + it('should report no build date for an unbuilt card', () => { + expect(getUnreleasedBuildDate()).toBeNull(); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index a6757582..1afbf6a0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ import { visualizer } from 'rollup-plugin-visualizer'; import { defineConfig } from 'vite'; +import { buildDate } from './scripts/build-date-plugin.js'; import { getBuildDefines } from './scripts/build-defines.js'; import { cleanDist } from './scripts/clean-dist-plugin.js'; import { facadeEntry } from './scripts/facade-entry-plugin.js'; @@ -24,6 +25,7 @@ export default defineConfig(({ mode }) => { plugins: [ cleanDist(), svgPath(), + buildDate(), facadeEntry({ publicFileNames: PUBLIC_ENTRIES }), visualizer({ filename: 'visualizations/treemap.html', template: 'treemap' }), ],