From 9009323ec137b53b0d8bb07dacdcd333220d833a Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 20 Apr 2026 09:44:34 -0400 Subject: [PATCH 1/9] add a frontend version tag to Footer.tsx --- dev-env/shib-dev-env/vite.config.ts | 144 +++++++++++++++++- dev-env/vite.config.ts | 144 +++++++++++++++++- public/locales/en/footer.json | 3 +- public/locales/es/footer.json | 3 +- src/sections/layout/footer/Footer.module.scss | 17 ++- src/sections/layout/footer/Footer.tsx | 35 +++-- src/version/resolveSpaVersionDisplay.ts | 35 +++++ src/version/spaVersion.ts | 11 ++ src/vite-env.d.ts | 13 ++ .../sections/layout/footer/Footer.spec.tsx | 2 + .../version/resolveSpaVersionDisplay.spec.ts | 26 ++++ vite.config.ts | 108 ++++++++++++- 12 files changed, 518 insertions(+), 23 deletions(-) create mode 100644 src/version/resolveSpaVersionDisplay.ts create mode 100644 src/version/spaVersion.ts create mode 100644 tests/component/version/resolveSpaVersionDisplay.spec.ts diff --git a/dev-env/shib-dev-env/vite.config.ts b/dev-env/shib-dev-env/vite.config.ts index f4034adeb..7b5008150 100644 --- a/dev-env/shib-dev-env/vite.config.ts +++ b/dev-env/shib-dev-env/vite.config.ts @@ -1,9 +1,149 @@ +import { readFileSync } from 'node:fs' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import istanbul from 'vite-plugin-istanbul' import * as path from 'path' +function readTextFile(filePath: string): string | undefined { + try { + return readFileSync(filePath, 'utf8').trim() || undefined + } catch { + return undefined + } +} + +function resolveSpaVersionDisplay({ + packageVersion, + commitSha, + exactTag, + refName, + refType +}: { + packageVersion?: string + commitSha?: string + exactTag?: string + refName?: string + refType?: string +}): string { + const releaseTag = refType === 'tag' ? refName : exactTag + const normalizedReleaseTag = releaseTag?.replace(/^refs\/tags\//, '').replace(/^v/, '') + + if (packageVersion && normalizedReleaseTag === packageVersion) { + return packageVersion + } + + if (commitSha) { + return commitSha + } + + return packageVersion ?? 'unknown' +} + +function resolveProjectRoot(configDir: string): string { + const candidates = [configDir, path.resolve(configDir, '..'), path.resolve(configDir, '../..')] + + const projectRoot = candidates.find((candidate) => { + return Boolean(readTextFile(path.join(candidate, 'package.json'))) && Boolean(readTextFile(path.join(candidate, '.git'))) + }) + + return projectRoot ?? configDir +} + +function readPackageVersion(projectRoot: string): string | undefined { + try { + const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8')) as { + version?: string + } + + return packageJson.version + } catch { + return undefined + } +} + +function resolveGitDir(projectRoot: string): string | undefined { + const dotGitPath = path.join(projectRoot, '.git') + const dotGitContent = readTextFile(dotGitPath) + + if (dotGitContent?.startsWith('gitdir: ')) { + return path.resolve(projectRoot, dotGitContent.slice('gitdir: '.length)) + } + + return dotGitContent ? undefined : dotGitPath +} + +function readPackedRef(gitDir: string, ref: string): string | undefined { + const packedRefs = readTextFile(path.join(gitDir, 'packed-refs')) + + if (!packedRefs) { + return undefined + } + + for (const line of packedRefs.split('\n')) { + if (!line || line.startsWith('#') || line.startsWith('^')) { + continue + } + + const [sha, packedRef] = line.split(' ') + if (packedRef === ref) { + return sha + } + } + + return undefined +} + +function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: string } { + const gitDir = resolveGitDir(projectRoot) + + if (!gitDir) { + return {} + } + + const head = readTextFile(path.join(gitDir, 'HEAD')) + + if (!head) { + return {} + } + + if (!head.startsWith('ref: ')) { + return { commitSha: head.slice(0, 9) } + } + + const ref = head.slice('ref: '.length) + const commitSha = readTextFile(path.join(gitDir, ref)) ?? readPackedRef(gitDir, ref) + const exactTag = ref.startsWith('refs/tags/') ? ref.replace('refs/tags/', '') : undefined + + return { + commitSha: commitSha?.slice(0, 9), + exactTag + } +} + +const projectRoot = resolveProjectRoot(__dirname) +const packageVersion = readPackageVersion(projectRoot) +const gitHeadInfo = readGitHeadInfo(projectRoot) +const shortCommitSha = process.env.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha +const exactTag = + (process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : undefined) ?? + gitHeadInfo.exactTag +const spaDisplayVersion = resolveSpaVersionDisplay({ + packageVersion, + commitSha: shortCommitSha, + exactTag, + refName: process.env.GITHUB_REF_NAME, + refType: process.env.GITHUB_REF_TYPE +}) export default defineConfig({ + root: projectRoot, + define: { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), + 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), + 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), + 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(process.env.GITHUB_REF_NAME), + 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(process.env.GITHUB_REF_TYPE), + 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) + }, plugins: [ react(), istanbul({ @@ -24,8 +164,8 @@ export default defineConfig({ }, resolve: { alias: { - '@': path.resolve(__dirname, 'src'), - '@tests': path.resolve(__dirname, 'tests') + '@': path.resolve(projectRoot, 'src'), + '@tests': path.resolve(projectRoot, 'tests') } } }) diff --git a/dev-env/vite.config.ts b/dev-env/vite.config.ts index 3036ac992..87cdbdcff 100644 --- a/dev-env/vite.config.ts +++ b/dev-env/vite.config.ts @@ -1,10 +1,150 @@ +import { readFileSync } from 'node:fs' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import istanbul from 'vite-plugin-istanbul' import * as path from 'path' +function readTextFile(filePath: string): string | undefined { + try { + return readFileSync(filePath, 'utf8').trim() || undefined + } catch { + return undefined + } +} + +function resolveSpaVersionDisplay({ + packageVersion, + commitSha, + exactTag, + refName, + refType +}: { + packageVersion?: string + commitSha?: string + exactTag?: string + refName?: string + refType?: string +}): string { + const releaseTag = refType === 'tag' ? refName : exactTag + const normalizedReleaseTag = releaseTag?.replace(/^refs\/tags\//, '').replace(/^v/, '') + + if (packageVersion && normalizedReleaseTag === packageVersion) { + return packageVersion + } + + if (commitSha) { + return commitSha + } + + return packageVersion ?? 'unknown' +} + +function resolveProjectRoot(configDir: string): string { + const candidates = [configDir, path.resolve(configDir, '..'), path.resolve(configDir, '../..')] + + const projectRoot = candidates.find((candidate) => { + return Boolean(readTextFile(path.join(candidate, 'package.json'))) && Boolean(readTextFile(path.join(candidate, '.git'))) + }) + + return projectRoot ?? configDir +} + +function readPackageVersion(projectRoot: string): string | undefined { + try { + const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8')) as { + version?: string + } + + return packageJson.version + } catch { + return undefined + } +} + +function resolveGitDir(projectRoot: string): string | undefined { + const dotGitPath = path.join(projectRoot, '.git') + const dotGitContent = readTextFile(dotGitPath) + + if (dotGitContent?.startsWith('gitdir: ')) { + return path.resolve(projectRoot, dotGitContent.slice('gitdir: '.length)) + } + + return dotGitContent ? undefined : dotGitPath +} + +function readPackedRef(gitDir: string, ref: string): string | undefined { + const packedRefs = readTextFile(path.join(gitDir, 'packed-refs')) + + if (!packedRefs) { + return undefined + } + + for (const line of packedRefs.split('\n')) { + if (!line || line.startsWith('#') || line.startsWith('^')) { + continue + } + + const [sha, packedRef] = line.split(' ') + if (packedRef === ref) { + return sha + } + } + + return undefined +} + +function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: string } { + const gitDir = resolveGitDir(projectRoot) + + if (!gitDir) { + return {} + } + + const head = readTextFile(path.join(gitDir, 'HEAD')) + + if (!head) { + return {} + } + + if (!head.startsWith('ref: ')) { + return { commitSha: head.slice(0, 9) } + } + + const ref = head.slice('ref: '.length) + const commitSha = readTextFile(path.join(gitDir, ref)) ?? readPackedRef(gitDir, ref) + const exactTag = ref.startsWith('refs/tags/') ? ref.replace('refs/tags/', '') : undefined + + return { + commitSha: commitSha?.slice(0, 9), + exactTag + } +} + +const projectRoot = resolveProjectRoot(__dirname) +const packageVersion = readPackageVersion(projectRoot) +const gitHeadInfo = readGitHeadInfo(projectRoot) +const shortCommitSha = process.env.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha +const exactTag = + (process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : undefined) ?? + gitHeadInfo.exactTag +const spaDisplayVersion = resolveSpaVersionDisplay({ + packageVersion, + commitSha: shortCommitSha, + exactTag, + refName: process.env.GITHUB_REF_NAME, + refType: process.env.GITHUB_REF_TYPE +}) export default defineConfig({ + root: projectRoot, base: '/modern', + define: { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), + 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), + 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), + 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(process.env.GITHUB_REF_NAME), + 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(process.env.GITHUB_REF_TYPE), + 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) + }, plugins: [ react(), istanbul({ @@ -26,8 +166,8 @@ export default defineConfig({ }, resolve: { alias: { - '@': path.resolve(__dirname, 'src'), - '@tests': path.resolve(__dirname, 'tests') + '@': path.resolve(projectRoot, 'src'), + '@tests': path.resolve(projectRoot, 'tests') } } }) diff --git a/public/locales/en/footer.json b/public/locales/en/footer.json index cdf13602d..91ed0c627 100644 --- a/public/locales/en/footer.json +++ b/public/locales/en/footer.json @@ -1,5 +1,6 @@ { "copyright": "Copyright © {{year}}, {{copyrightHolder}}", "privacyPolicy": "Privacy Policy", - "poweredBy": "Powered by" + "poweredBy": "Powered by", + "spaVersion": "frontend version: {{version}}" } diff --git a/public/locales/es/footer.json b/public/locales/es/footer.json index ac3cb7e89..cb6d63fa0 100644 --- a/public/locales/es/footer.json +++ b/public/locales/es/footer.json @@ -1,5 +1,6 @@ { "copyright": "Copyright © {{year}}, {{copyrightHolder}}", "privacyPolicy": "Política de privacidad", - "poweredBy": "Desarrollado por" + "poweredBy": "Desarrollado por", + "spaVersion": "frontend version: {{version}}" } diff --git a/src/sections/layout/footer/Footer.module.scss b/src/sections/layout/footer/Footer.module.scss index 5536d07cc..42a2eca04 100644 --- a/src/sections/layout/footer/Footer.module.scss +++ b/src/sections/layout/footer/Footer.module.scss @@ -24,6 +24,18 @@ .powered-by-container { font-size: $dv-font-size-sm; text-align: right; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.25rem; +} + +.branding-row, +.versions-row { + display: flex; + justify-content: flex-end; + align-items: center; + flex-wrap: wrap; } .powered-by-text { @@ -31,8 +43,11 @@ } .version { - margin-right: 0.3em; white-space: nowrap; text-align: right; vertical-align: bottom; } + +.separator { + margin: 0 0.3em; +} diff --git a/src/sections/layout/footer/Footer.tsx b/src/sections/layout/footer/Footer.tsx index 780ef3d2d..b1d6bf28b 100644 --- a/src/sections/layout/footer/Footer.tsx +++ b/src/sections/layout/footer/Footer.tsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next' import { Container, Row, Col } from '@iqss/dataverse-design-system' import { requireAppConfig } from '@/config' +import { spaVersion } from '@/version/spaVersion' import { DataverseInfoRepository } from '../../../info/domain/repositories/DataverseInfoRepository' import { useDataverseVersion } from './useDataverseVersion' import dataverseProjectLogo from '@/assets/dataverse-project-logo.svg' @@ -41,20 +42,26 @@ export function Footer({ dataverseInfoRepository }: FooterProps) {
- {t('poweredBy')} - - The Dataverse Project logo - - {dataverseVersion} +
+ {t('poweredBy')} + + The Dataverse Project logo + +
+
+ {dataverseVersion && {dataverseVersion}} + {dataverseVersion && |} + {t('spaVersion', { version: spaVersion })} +
diff --git a/src/version/resolveSpaVersionDisplay.ts b/src/version/resolveSpaVersionDisplay.ts new file mode 100644 index 000000000..8fa1b106c --- /dev/null +++ b/src/version/resolveSpaVersionDisplay.ts @@ -0,0 +1,35 @@ +export interface SpaVersionBuildInfo { + packageVersion?: string + commitSha?: string + exactTag?: string + refName?: string + refType?: string +} + +function normalizeTag(tag?: string): string | undefined { + if (!tag) { + return undefined + } + + return tag.replace(/^refs\/tags\//, '').replace(/^v/, '') +} + +export function resolveSpaVersionDisplay({ + packageVersion, + commitSha, + exactTag, + refName, + refType +}: SpaVersionBuildInfo): string { + const releaseTag = refType === 'tag' ? refName : exactTag + + if (packageVersion && normalizeTag(releaseTag) === packageVersion) { + return packageVersion + } + + if (commitSha) { + return commitSha + } + + return packageVersion ?? 'unknown' +} diff --git a/src/version/spaVersion.ts b/src/version/spaVersion.ts new file mode 100644 index 000000000..08a9bba3b --- /dev/null +++ b/src/version/spaVersion.ts @@ -0,0 +1,11 @@ +import { resolveSpaVersionDisplay } from './resolveSpaVersionDisplay' + +export const spaVersion = + import.meta.env.VITE_SPA_DISPLAY_VERSION ?? + resolveSpaVersionDisplay({ + packageVersion: import.meta.env.VITE_APP_VERSION, + commitSha: import.meta.env.VITE_COMMIT_SHA_SHORT, + exactTag: import.meta.env.VITE_GIT_EXACT_TAG, + refName: import.meta.env.VITE_GITHUB_REF_NAME, + refType: import.meta.env.VITE_GITHUB_REF_TYPE + }) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe2a..09914070c 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,14 @@ /// + +interface ImportMetaEnv { + readonly VITE_APP_VERSION?: string + readonly VITE_COMMIT_SHA_SHORT?: string + readonly VITE_GIT_EXACT_TAG?: string + readonly VITE_GITHUB_REF_NAME?: string + readonly VITE_GITHUB_REF_TYPE?: string + readonly VITE_SPA_DISPLAY_VERSION?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/tests/component/sections/layout/footer/Footer.spec.tsx b/tests/component/sections/layout/footer/Footer.spec.tsx index 5b79f7114..fe486e2cf 100644 --- a/tests/component/sections/layout/footer/Footer.spec.tsx +++ b/tests/component/sections/layout/footer/Footer.spec.tsx @@ -5,6 +5,7 @@ import { Footer } from '../../../../../src/sections/layout/footer/Footer' import { applyTestAppConfig } from '../../../../support/bootstrapAppConfig' import type { AppConfig } from '@/config' import { DataverseInfoMockRepository } from '@/stories/shared-mock-repositories/info/DataverseInfoMockRepository' +import { spaVersion } from '@/version/spaVersion' describe('Footer component', () => { const sandbox: SinonSandbox = createSandbox() @@ -26,6 +27,7 @@ describe('Footer component', () => { cy.findByText('Privacy Policy').should('exist') cy.findByAltText('The Dataverse Project logo').should('exist') cy.findByText(testVersion).should('exist') + cy.findByText(`SPA: ${spaVersion}`).should('exist') }) it('should call dataverseInfoRepository.getVersion on mount', () => { diff --git a/tests/component/version/resolveSpaVersionDisplay.spec.ts b/tests/component/version/resolveSpaVersionDisplay.spec.ts new file mode 100644 index 000000000..cd0383227 --- /dev/null +++ b/tests/component/version/resolveSpaVersionDisplay.spec.ts @@ -0,0 +1,26 @@ +import { resolveSpaVersionDisplay } from '@/version/resolveSpaVersionDisplay' + +describe('resolveSpaVersionDisplay', () => { + it('returns the package version when the current ref is an official release tag', () => { + expect( + resolveSpaVersionDisplay({ + packageVersion: '3.5.0', + commitSha: 'abc123def', + refName: 'v3.5.0', + refType: 'tag' + }) + ).to.equal('3.5.0') + }) + + it('returns the commit sha when the current build is not an official release', () => { + expect( + resolveSpaVersionDisplay({ + packageVersion: '3.5.0', + commitSha: 'abc123def', + exactTag: 'v3.4.9', + refName: 'develop', + refType: 'branch' + }) + ).to.equal('abc123def') + }) +}) diff --git a/vite.config.ts b/vite.config.ts index 55ed74726..e96aca33a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,115 @@ +import { readFileSync } from 'node:fs' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import istanbul from 'vite-plugin-istanbul' -import { keycloakify } from "keycloakify/vite-plugin"; +import { keycloakify } from 'keycloakify/vite-plugin' import * as path from 'path' +import { resolveSpaVersionDisplay } from './src/version/resolveSpaVersionDisplay' + +function readTextFile(filePath: string): string | undefined { + try { + return readFileSync(filePath, 'utf8').trim() || undefined + } catch { + return undefined + } +} + +function readPackageVersion(projectRoot: string): string | undefined { + try { + const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8')) as { + version?: string + } + + return packageJson.version + } catch { + return undefined + } +} + +function resolveGitDir(projectRoot: string): string | undefined { + const dotGitPath = path.join(projectRoot, '.git') + const dotGitContent = readTextFile(dotGitPath) + + if (dotGitContent?.startsWith('gitdir: ')) { + return path.resolve(projectRoot, dotGitContent.slice('gitdir: '.length)) + } + + return dotGitContent ? undefined : dotGitPath +} + +function readPackedRef(gitDir: string, ref: string): string | undefined { + const packedRefs = readTextFile(path.join(gitDir, 'packed-refs')) + + if (!packedRefs) { + return undefined + } + + for (const line of packedRefs.split('\n')) { + if (!line || line.startsWith('#') || line.startsWith('^')) { + continue + } + + const [sha, packedRef] = line.split(' ') + if (packedRef === ref) { + return sha + } + } + + return undefined +} + +function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: string } { + const gitDir = resolveGitDir(projectRoot) + + if (!gitDir) { + return {} + } + + const head = readTextFile(path.join(gitDir, 'HEAD')) + + if (!head) { + return {} + } + + if (!head.startsWith('ref: ')) { + return { commitSha: head.slice(0, 9) } + } + + const ref = head.slice('ref: '.length) + const commitSha = readTextFile(path.join(gitDir, ref)) ?? readPackedRef(gitDir, ref) + const exactTag = ref.startsWith('refs/tags/') ? ref.replace('refs/tags/', '') : undefined + + return { + commitSha: commitSha?.slice(0, 9), + exactTag + } +} + +const projectRoot = path.resolve(__dirname) +const packageVersion = readPackageVersion(projectRoot) +const gitHeadInfo = readGitHeadInfo(projectRoot) +const shortCommitSha = process.env.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha +const exactTag = + (process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : undefined) ?? + gitHeadInfo.exactTag +const spaDisplayVersion = resolveSpaVersionDisplay({ + packageVersion, + commitSha: shortCommitSha, + exactTag, + refName: process.env.GITHUB_REF_NAME, + refType: process.env.GITHUB_REF_TYPE +}) export default defineConfig({ base: '/modern', + define: { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), + 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), + 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), + 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(process.env.GITHUB_REF_NAME), + 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(process.env.GITHUB_REF_TYPE), + 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) + }, plugins: [ react(), istanbul({ @@ -14,7 +118,7 @@ export default defineConfig({ }), keycloakify({ themeName: 'dataverse-spa', - keycloakifyBuildDirPath: "./dist_keycloak", + keycloakifyBuildDirPath: './dist_keycloak', accountThemeImplementation: 'none', keycloakVersionTargets: { '22-to-25': false, From 89291ac29cbbb2c5b3b01e708a9c021e82eeba8e Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 20 Apr 2026 09:58:09 -0400 Subject: [PATCH 2/9] Update public/locales/es/footer.json add spanish translation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- public/locales/es/footer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/es/footer.json b/public/locales/es/footer.json index cb6d63fa0..8779a5ce4 100644 --- a/public/locales/es/footer.json +++ b/public/locales/es/footer.json @@ -2,5 +2,5 @@ "copyright": "Copyright © {{year}}, {{copyrightHolder}}", "privacyPolicy": "Política de privacidad", "poweredBy": "Desarrollado por", - "spaVersion": "frontend version: {{version}}" + "spaVersion": "versión del frontend: {{version}}" } From c35fb6d1c77758cf5e73f49a463713c06171b3a9 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 20 Apr 2026 09:59:57 -0400 Subject: [PATCH 3/9] fix unit test --- tests/component/sections/layout/footer/Footer.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/component/sections/layout/footer/Footer.spec.tsx b/tests/component/sections/layout/footer/Footer.spec.tsx index fe486e2cf..c6bc0d0d8 100644 --- a/tests/component/sections/layout/footer/Footer.spec.tsx +++ b/tests/component/sections/layout/footer/Footer.spec.tsx @@ -27,7 +27,7 @@ describe('Footer component', () => { cy.findByText('Privacy Policy').should('exist') cy.findByAltText('The Dataverse Project logo').should('exist') cy.findByText(testVersion).should('exist') - cy.findByText(`SPA: ${spaVersion}`).should('exist') + cy.findByText(`frontend version: ${spaVersion}`).should('exist') }) it('should call dataverseInfoRepository.getVersion on mount', () => { From 7608a934258c991cf38761eec1ece992e83295e4 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Mon, 20 Apr 2026 15:31:07 -0400 Subject: [PATCH 4/9] fix the Cypress/dev Vite config --- dev-env/shib-dev-env/vite.config.ts | 17 ++++++++++------- dev-env/vite.config.ts | 17 ++++++++++------- tests/support/component.ts | 2 ++ tests/support/e2e.ts | 2 ++ tests/support/processShim.ts | 29 +++++++++++++++++++++++++++++ vite.config.ts | 16 +++++++++------- 6 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 tests/support/processShim.ts diff --git a/dev-env/shib-dev-env/vite.config.ts b/dev-env/shib-dev-env/vite.config.ts index 7b5008150..871f10e0f 100644 --- a/dev-env/shib-dev-env/vite.config.ts +++ b/dev-env/shib-dev-env/vite.config.ts @@ -3,6 +3,10 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import istanbul from 'vite-plugin-istanbul' import * as path from 'path' + +const nodeEnv = (globalThis as { process?: { env?: Record } }).process + ?.env + function readTextFile(filePath: string): string | undefined { try { return readFileSync(filePath, 'utf8').trim() || undefined @@ -122,16 +126,15 @@ function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: const projectRoot = resolveProjectRoot(__dirname) const packageVersion = readPackageVersion(projectRoot) const gitHeadInfo = readGitHeadInfo(projectRoot) -const shortCommitSha = process.env.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha +const shortCommitSha = nodeEnv?.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha const exactTag = - (process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : undefined) ?? - gitHeadInfo.exactTag + (nodeEnv?.GITHUB_REF_TYPE === 'tag' ? nodeEnv.GITHUB_REF_NAME : undefined) ?? gitHeadInfo.exactTag const spaDisplayVersion = resolveSpaVersionDisplay({ packageVersion, commitSha: shortCommitSha, exactTag, - refName: process.env.GITHUB_REF_NAME, - refType: process.env.GITHUB_REF_TYPE + refName: nodeEnv?.GITHUB_REF_NAME, + refType: nodeEnv?.GITHUB_REF_TYPE }) export default defineConfig({ @@ -140,8 +143,8 @@ export default defineConfig({ 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), - 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(process.env.GITHUB_REF_NAME), - 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(process.env.GITHUB_REF_TYPE), + 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(nodeEnv?.GITHUB_REF_NAME), + 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(nodeEnv?.GITHUB_REF_TYPE), 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) }, plugins: [ diff --git a/dev-env/vite.config.ts b/dev-env/vite.config.ts index 87cdbdcff..64d28544e 100644 --- a/dev-env/vite.config.ts +++ b/dev-env/vite.config.ts @@ -3,6 +3,10 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import istanbul from 'vite-plugin-istanbul' import * as path from 'path' + +const nodeEnv = (globalThis as { process?: { env?: Record } }).process + ?.env + function readTextFile(filePath: string): string | undefined { try { return readFileSync(filePath, 'utf8').trim() || undefined @@ -122,16 +126,15 @@ function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: const projectRoot = resolveProjectRoot(__dirname) const packageVersion = readPackageVersion(projectRoot) const gitHeadInfo = readGitHeadInfo(projectRoot) -const shortCommitSha = process.env.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha +const shortCommitSha = nodeEnv?.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha const exactTag = - (process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : undefined) ?? - gitHeadInfo.exactTag + (nodeEnv?.GITHUB_REF_TYPE === 'tag' ? nodeEnv.GITHUB_REF_NAME : undefined) ?? gitHeadInfo.exactTag const spaDisplayVersion = resolveSpaVersionDisplay({ packageVersion, commitSha: shortCommitSha, exactTag, - refName: process.env.GITHUB_REF_NAME, - refType: process.env.GITHUB_REF_TYPE + refName: nodeEnv?.GITHUB_REF_NAME, + refType: nodeEnv?.GITHUB_REF_TYPE }) export default defineConfig({ @@ -141,8 +144,8 @@ export default defineConfig({ 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), - 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(process.env.GITHUB_REF_NAME), - 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(process.env.GITHUB_REF_TYPE), + 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(nodeEnv?.GITHUB_REF_NAME), + 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(nodeEnv?.GITHUB_REF_TYPE), 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) }, plugins: [ diff --git a/tests/support/component.ts b/tests/support/component.ts index 2981be7ad..943b8911a 100644 --- a/tests/support/component.ts +++ b/tests/support/component.ts @@ -13,6 +13,8 @@ // https://on.cypress.io/configuration // *********************************************************** +import './processShim' + import './bootstrapAppConfig' // Initialize test runtime config before any commands/modules import './commands' import '@cypress/code-coverage/support' diff --git a/tests/support/e2e.ts b/tests/support/e2e.ts index 67ac8273b..d6b5e18b5 100644 --- a/tests/support/e2e.ts +++ b/tests/support/e2e.ts @@ -13,6 +13,8 @@ // https://on.cypress.io/configuration // *********************************************************** +import './processShim' + // Initialize test runtime config before any commands/modules use requireAppConfig import './bootstrapAppConfig' diff --git a/tests/support/processShim.ts b/tests/support/processShim.ts new file mode 100644 index 000000000..d4b5fc1ee --- /dev/null +++ b/tests/support/processShim.ts @@ -0,0 +1,29 @@ +type ProcessShim = { + env: Record + nextTick: (callback: (...args: unknown[]) => void, ...args: unknown[]) => void + binding: () => Record +} + +const globalWithProcess = globalThis as unknown as { + process?: Partial +} + +if (!globalWithProcess.process) { + globalWithProcess.process = {} +} + +if (!globalWithProcess.process.env) { + globalWithProcess.process.env = {} +} + +if (!globalWithProcess.process.nextTick) { + globalWithProcess.process.nextTick = (callback, ...args) => { + queueMicrotask(() => { + callback(...args) + }) + } +} + +if (!globalWithProcess.process.binding) { + globalWithProcess.process.binding = () => ({}) +} diff --git a/vite.config.ts b/vite.config.ts index e96aca33a..fb87682de 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,9 @@ import { keycloakify } from 'keycloakify/vite-plugin' import * as path from 'path' import { resolveSpaVersionDisplay } from './src/version/resolveSpaVersionDisplay' +const nodeEnv = (globalThis as { process?: { env?: Record } }).process + ?.env + function readTextFile(filePath: string): string | undefined { try { return readFileSync(filePath, 'utf8').trim() || undefined @@ -88,16 +91,15 @@ function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: const projectRoot = path.resolve(__dirname) const packageVersion = readPackageVersion(projectRoot) const gitHeadInfo = readGitHeadInfo(projectRoot) -const shortCommitSha = process.env.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha +const shortCommitSha = nodeEnv?.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha const exactTag = - (process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : undefined) ?? - gitHeadInfo.exactTag + (nodeEnv?.GITHUB_REF_TYPE === 'tag' ? nodeEnv.GITHUB_REF_NAME : undefined) ?? gitHeadInfo.exactTag const spaDisplayVersion = resolveSpaVersionDisplay({ packageVersion, commitSha: shortCommitSha, exactTag, - refName: process.env.GITHUB_REF_NAME, - refType: process.env.GITHUB_REF_TYPE + refName: nodeEnv?.GITHUB_REF_NAME, + refType: nodeEnv?.GITHUB_REF_TYPE }) export default defineConfig({ @@ -106,8 +108,8 @@ export default defineConfig({ 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), - 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(process.env.GITHUB_REF_NAME), - 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(process.env.GITHUB_REF_TYPE), + 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(nodeEnv?.GITHUB_REF_NAME), + 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(nodeEnv?.GITHUB_REF_TYPE), 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) }, plugins: [ From e7b1c336b17b7759b407c2572ddd53b317bb9247 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 21 Apr 2026 13:22:47 -0400 Subject: [PATCH 5/9] centralize the logic for determining the display version --- build/spaVersionMetadata.mjs | 147 +++++++++++++++++++++++++++ dev-env/shib-dev-env/vite.config.ts | 148 +++------------------------- dev-env/vite.config.ts | 148 +++------------------------- vite.config.ts | 114 +++------------------ 4 files changed, 187 insertions(+), 370 deletions(-) create mode 100644 build/spaVersionMetadata.mjs diff --git a/build/spaVersionMetadata.mjs b/build/spaVersionMetadata.mjs new file mode 100644 index 000000000..b969ab475 --- /dev/null +++ b/build/spaVersionMetadata.mjs @@ -0,0 +1,147 @@ +import { existsSync, readFileSync } from 'node:fs' +import * as path from 'node:path' + +export function getNodeEnv() { + return globalThis.process?.env +} + +function normalizeTag(tag) { + if (!tag) { + return undefined + } + + return tag.replace(/^refs\/tags\//, '').replace(/^v/, '') +} + +function resolveSpaVersionDisplay({ packageVersion, commitSha, exactTag, refName, refType }) { + const releaseTag = refType === 'tag' ? refName : exactTag + + if (packageVersion && normalizeTag(releaseTag) === packageVersion) { + return packageVersion + } + + if (commitSha) { + return commitSha + } + + return packageVersion ?? 'unknown' +} + +function readTextFile(filePath) { + try { + return readFileSync(filePath, 'utf8').trim() || undefined + } catch { + return undefined + } +} + +export function resolveProjectRoot(configDir) { + const candidates = [configDir, path.resolve(configDir, '..'), path.resolve(configDir, '../..')] + + const projectRoot = candidates.find((candidate) => { + return ( + existsSync(path.join(candidate, 'package.json')) && existsSync(path.join(candidate, '.git')) + ) + }) + + return projectRoot ?? configDir +} + +function readPackageVersion(projectRoot) { + try { + const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8')) + + return packageJson.version + } catch { + return undefined + } +} + +function resolveGitDir(projectRoot) { + const dotGitPath = path.join(projectRoot, '.git') + + if (!existsSync(dotGitPath)) { + return undefined + } + + const dotGitContent = readTextFile(dotGitPath) + + if (dotGitContent?.startsWith('gitdir: ')) { + return path.resolve(projectRoot, dotGitContent.slice('gitdir: '.length)) + } + + return dotGitPath +} + +function readPackedRef(gitDir, ref) { + const packedRefs = readTextFile(path.join(gitDir, 'packed-refs')) + + if (!packedRefs) { + return undefined + } + + for (const line of packedRefs.split('\n')) { + if (!line || line.startsWith('#') || line.startsWith('^')) { + continue + } + + const [sha, packedRef] = line.split(' ') + if (packedRef === ref) { + return sha + } + } + + return undefined +} + +function readGitHeadInfo(projectRoot) { + const gitDir = resolveGitDir(projectRoot) + + if (!gitDir) { + return {} + } + + const head = readTextFile(path.join(gitDir, 'HEAD')) + + if (!head) { + return {} + } + + if (!head.startsWith('ref: ')) { + return { commitSha: head.slice(0, 9) } + } + + const ref = head.slice('ref: '.length) + const commitSha = readTextFile(path.join(gitDir, ref)) ?? readPackedRef(gitDir, ref) + const exactTag = ref.startsWith('refs/tags/') ? ref.replace('refs/tags/', '') : undefined + + return { + commitSha: commitSha?.slice(0, 9), + exactTag + } +} + +export function createSpaVersionDefines(projectRoot, nodeEnv = getNodeEnv()) { + const packageVersion = readPackageVersion(projectRoot) + const gitHeadInfo = readGitHeadInfo(projectRoot) + const shortCommitSha = nodeEnv?.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha + const exactTag = + (nodeEnv?.GITHUB_REF_TYPE === 'tag' ? nodeEnv.GITHUB_REF_NAME : undefined) ?? + gitHeadInfo.exactTag + const spaDisplayVersion = resolveSpaVersionDisplay({ + packageVersion, + commitSha: shortCommitSha, + exactTag, + refName: nodeEnv?.GITHUB_REF_NAME, + refType: nodeEnv?.GITHUB_REF_TYPE + }) + + return { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), + 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), + 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), + 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(nodeEnv?.GITHUB_REF_NAME), + 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(nodeEnv?.GITHUB_REF_TYPE), + 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) + } +} diff --git a/dev-env/shib-dev-env/vite.config.ts b/dev-env/shib-dev-env/vite.config.ts index 871f10e0f..aa8bdb038 100644 --- a/dev-env/shib-dev-env/vite.config.ts +++ b/dev-env/shib-dev-env/vite.config.ts @@ -1,152 +1,30 @@ -import { readFileSync } from 'node:fs' +import { existsSync } from 'node:fs' +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import istanbul from 'vite-plugin-istanbul' import * as path from 'path' -const nodeEnv = (globalThis as { process?: { env?: Record } }).process - ?.env +const sharedBuildModuleUrl = [ + new URL('./build/spaVersionMetadata.mjs', import.meta.url), + new URL('../build/spaVersionMetadata.mjs', import.meta.url), + new URL('../../build/spaVersionMetadata.mjs', import.meta.url) +].find((candidate) => existsSync(fileURLToPath(candidate))) -function readTextFile(filePath: string): string | undefined { - try { - return readFileSync(filePath, 'utf8').trim() || undefined - } catch { - return undefined - } -} - -function resolveSpaVersionDisplay({ - packageVersion, - commitSha, - exactTag, - refName, - refType -}: { - packageVersion?: string - commitSha?: string - exactTag?: string - refName?: string - refType?: string -}): string { - const releaseTag = refType === 'tag' ? refName : exactTag - const normalizedReleaseTag = releaseTag?.replace(/^refs\/tags\//, '').replace(/^v/, '') - - if (packageVersion && normalizedReleaseTag === packageVersion) { - return packageVersion - } - - if (commitSha) { - return commitSha - } - - return packageVersion ?? 'unknown' -} - -function resolveProjectRoot(configDir: string): string { - const candidates = [configDir, path.resolve(configDir, '..'), path.resolve(configDir, '../..')] - - const projectRoot = candidates.find((candidate) => { - return Boolean(readTextFile(path.join(candidate, 'package.json'))) && Boolean(readTextFile(path.join(candidate, '.git'))) - }) - - return projectRoot ?? configDir -} - -function readPackageVersion(projectRoot: string): string | undefined { - try { - const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8')) as { - version?: string - } - - return packageJson.version - } catch { - return undefined - } -} - -function resolveGitDir(projectRoot: string): string | undefined { - const dotGitPath = path.join(projectRoot, '.git') - const dotGitContent = readTextFile(dotGitPath) - - if (dotGitContent?.startsWith('gitdir: ')) { - return path.resolve(projectRoot, dotGitContent.slice('gitdir: '.length)) - } - - return dotGitContent ? undefined : dotGitPath +if (!sharedBuildModuleUrl) { + throw new Error('Could not locate build/spaVersionMetadata.mjs') } -function readPackedRef(gitDir: string, ref: string): string | undefined { - const packedRefs = readTextFile(path.join(gitDir, 'packed-refs')) - - if (!packedRefs) { - return undefined - } - - for (const line of packedRefs.split('\n')) { - if (!line || line.startsWith('#') || line.startsWith('^')) { - continue - } - - const [sha, packedRef] = line.split(' ') - if (packedRef === ref) { - return sha - } - } - - return undefined -} - -function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: string } { - const gitDir = resolveGitDir(projectRoot) - - if (!gitDir) { - return {} - } - - const head = readTextFile(path.join(gitDir, 'HEAD')) - - if (!head) { - return {} - } - - if (!head.startsWith('ref: ')) { - return { commitSha: head.slice(0, 9) } - } - - const ref = head.slice('ref: '.length) - const commitSha = readTextFile(path.join(gitDir, ref)) ?? readPackedRef(gitDir, ref) - const exactTag = ref.startsWith('refs/tags/') ? ref.replace('refs/tags/', '') : undefined - - return { - commitSha: commitSha?.slice(0, 9), - exactTag - } +const { createSpaVersionDefines, resolveProjectRoot } = (await import(sharedBuildModuleUrl.href)) as { + createSpaVersionDefines: (projectRoot: string) => Record + resolveProjectRoot: (configDir: string) => string } const projectRoot = resolveProjectRoot(__dirname) -const packageVersion = readPackageVersion(projectRoot) -const gitHeadInfo = readGitHeadInfo(projectRoot) -const shortCommitSha = nodeEnv?.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha -const exactTag = - (nodeEnv?.GITHUB_REF_TYPE === 'tag' ? nodeEnv.GITHUB_REF_NAME : undefined) ?? gitHeadInfo.exactTag -const spaDisplayVersion = resolveSpaVersionDisplay({ - packageVersion, - commitSha: shortCommitSha, - exactTag, - refName: nodeEnv?.GITHUB_REF_NAME, - refType: nodeEnv?.GITHUB_REF_TYPE -}) export default defineConfig({ root: projectRoot, - define: { - 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), - 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), - 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), - 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(nodeEnv?.GITHUB_REF_NAME), - 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(nodeEnv?.GITHUB_REF_TYPE), - 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) - }, + define: createSpaVersionDefines(projectRoot), plugins: [ react(), istanbul({ diff --git a/dev-env/vite.config.ts b/dev-env/vite.config.ts index 64d28544e..57c38dec3 100644 --- a/dev-env/vite.config.ts +++ b/dev-env/vite.config.ts @@ -1,153 +1,31 @@ -import { readFileSync } from 'node:fs' +import { existsSync } from 'node:fs' +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import istanbul from 'vite-plugin-istanbul' import * as path from 'path' -const nodeEnv = (globalThis as { process?: { env?: Record } }).process - ?.env +const sharedBuildModuleUrl = [ + new URL('./build/spaVersionMetadata.mjs', import.meta.url), + new URL('../build/spaVersionMetadata.mjs', import.meta.url), + new URL('../../build/spaVersionMetadata.mjs', import.meta.url) +].find((candidate) => existsSync(fileURLToPath(candidate))) -function readTextFile(filePath: string): string | undefined { - try { - return readFileSync(filePath, 'utf8').trim() || undefined - } catch { - return undefined - } -} - -function resolveSpaVersionDisplay({ - packageVersion, - commitSha, - exactTag, - refName, - refType -}: { - packageVersion?: string - commitSha?: string - exactTag?: string - refName?: string - refType?: string -}): string { - const releaseTag = refType === 'tag' ? refName : exactTag - const normalizedReleaseTag = releaseTag?.replace(/^refs\/tags\//, '').replace(/^v/, '') - - if (packageVersion && normalizedReleaseTag === packageVersion) { - return packageVersion - } - - if (commitSha) { - return commitSha - } - - return packageVersion ?? 'unknown' -} - -function resolveProjectRoot(configDir: string): string { - const candidates = [configDir, path.resolve(configDir, '..'), path.resolve(configDir, '../..')] - - const projectRoot = candidates.find((candidate) => { - return Boolean(readTextFile(path.join(candidate, 'package.json'))) && Boolean(readTextFile(path.join(candidate, '.git'))) - }) - - return projectRoot ?? configDir -} - -function readPackageVersion(projectRoot: string): string | undefined { - try { - const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8')) as { - version?: string - } - - return packageJson.version - } catch { - return undefined - } -} - -function resolveGitDir(projectRoot: string): string | undefined { - const dotGitPath = path.join(projectRoot, '.git') - const dotGitContent = readTextFile(dotGitPath) - - if (dotGitContent?.startsWith('gitdir: ')) { - return path.resolve(projectRoot, dotGitContent.slice('gitdir: '.length)) - } - - return dotGitContent ? undefined : dotGitPath +if (!sharedBuildModuleUrl) { + throw new Error('Could not locate build/spaVersionMetadata.mjs') } -function readPackedRef(gitDir: string, ref: string): string | undefined { - const packedRefs = readTextFile(path.join(gitDir, 'packed-refs')) - - if (!packedRefs) { - return undefined - } - - for (const line of packedRefs.split('\n')) { - if (!line || line.startsWith('#') || line.startsWith('^')) { - continue - } - - const [sha, packedRef] = line.split(' ') - if (packedRef === ref) { - return sha - } - } - - return undefined -} - -function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: string } { - const gitDir = resolveGitDir(projectRoot) - - if (!gitDir) { - return {} - } - - const head = readTextFile(path.join(gitDir, 'HEAD')) - - if (!head) { - return {} - } - - if (!head.startsWith('ref: ')) { - return { commitSha: head.slice(0, 9) } - } - - const ref = head.slice('ref: '.length) - const commitSha = readTextFile(path.join(gitDir, ref)) ?? readPackedRef(gitDir, ref) - const exactTag = ref.startsWith('refs/tags/') ? ref.replace('refs/tags/', '') : undefined - - return { - commitSha: commitSha?.slice(0, 9), - exactTag - } +const { createSpaVersionDefines, resolveProjectRoot } = (await import(sharedBuildModuleUrl.href)) as { + createSpaVersionDefines: (projectRoot: string) => Record + resolveProjectRoot: (configDir: string) => string } const projectRoot = resolveProjectRoot(__dirname) -const packageVersion = readPackageVersion(projectRoot) -const gitHeadInfo = readGitHeadInfo(projectRoot) -const shortCommitSha = nodeEnv?.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha -const exactTag = - (nodeEnv?.GITHUB_REF_TYPE === 'tag' ? nodeEnv.GITHUB_REF_NAME : undefined) ?? gitHeadInfo.exactTag -const spaDisplayVersion = resolveSpaVersionDisplay({ - packageVersion, - commitSha: shortCommitSha, - exactTag, - refName: nodeEnv?.GITHUB_REF_NAME, - refType: nodeEnv?.GITHUB_REF_TYPE -}) export default defineConfig({ root: projectRoot, base: '/modern', - define: { - 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), - 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), - 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), - 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(nodeEnv?.GITHUB_REF_NAME), - 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(nodeEnv?.GITHUB_REF_TYPE), - 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) - }, + define: createSpaVersionDefines(projectRoot), plugins: [ react(), istanbul({ diff --git a/vite.config.ts b/vite.config.ts index fb87682de..472516589 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,117 +1,31 @@ -import { readFileSync } from 'node:fs' +import { existsSync } from 'node:fs' +import { fileURLToPath } from 'node:url' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import istanbul from 'vite-plugin-istanbul' import { keycloakify } from 'keycloakify/vite-plugin' import * as path from 'path' -import { resolveSpaVersionDisplay } from './src/version/resolveSpaVersionDisplay' -const nodeEnv = (globalThis as { process?: { env?: Record } }).process - ?.env +const sharedBuildModuleUrl = [ + new URL('./build/spaVersionMetadata.mjs', import.meta.url), + new URL('../build/spaVersionMetadata.mjs', import.meta.url), + new URL('../../build/spaVersionMetadata.mjs', import.meta.url) +].find((candidate) => existsSync(fileURLToPath(candidate))) -function readTextFile(filePath: string): string | undefined { - try { - return readFileSync(filePath, 'utf8').trim() || undefined - } catch { - return undefined - } +if (!sharedBuildModuleUrl) { + throw new Error('Could not locate build/spaVersionMetadata.mjs') } -function readPackageVersion(projectRoot: string): string | undefined { - try { - const packageJson = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8')) as { - version?: string - } - - return packageJson.version - } catch { - return undefined - } -} - -function resolveGitDir(projectRoot: string): string | undefined { - const dotGitPath = path.join(projectRoot, '.git') - const dotGitContent = readTextFile(dotGitPath) - - if (dotGitContent?.startsWith('gitdir: ')) { - return path.resolve(projectRoot, dotGitContent.slice('gitdir: '.length)) - } - - return dotGitContent ? undefined : dotGitPath +const { createSpaVersionDefines, resolveProjectRoot } = (await import(sharedBuildModuleUrl.href)) as { + createSpaVersionDefines: (projectRoot: string) => Record + resolveProjectRoot: (configDir: string) => string } -function readPackedRef(gitDir: string, ref: string): string | undefined { - const packedRefs = readTextFile(path.join(gitDir, 'packed-refs')) - - if (!packedRefs) { - return undefined - } - - for (const line of packedRefs.split('\n')) { - if (!line || line.startsWith('#') || line.startsWith('^')) { - continue - } - - const [sha, packedRef] = line.split(' ') - if (packedRef === ref) { - return sha - } - } - - return undefined -} - -function readGitHeadInfo(projectRoot: string): { commitSha?: string; exactTag?: string } { - const gitDir = resolveGitDir(projectRoot) - - if (!gitDir) { - return {} - } - - const head = readTextFile(path.join(gitDir, 'HEAD')) - - if (!head) { - return {} - } - - if (!head.startsWith('ref: ')) { - return { commitSha: head.slice(0, 9) } - } - - const ref = head.slice('ref: '.length) - const commitSha = readTextFile(path.join(gitDir, ref)) ?? readPackedRef(gitDir, ref) - const exactTag = ref.startsWith('refs/tags/') ? ref.replace('refs/tags/', '') : undefined - - return { - commitSha: commitSha?.slice(0, 9), - exactTag - } -} - -const projectRoot = path.resolve(__dirname) -const packageVersion = readPackageVersion(projectRoot) -const gitHeadInfo = readGitHeadInfo(projectRoot) -const shortCommitSha = nodeEnv?.GITHUB_SHA?.slice(0, 9) ?? gitHeadInfo.commitSha -const exactTag = - (nodeEnv?.GITHUB_REF_TYPE === 'tag' ? nodeEnv.GITHUB_REF_NAME : undefined) ?? gitHeadInfo.exactTag -const spaDisplayVersion = resolveSpaVersionDisplay({ - packageVersion, - commitSha: shortCommitSha, - exactTag, - refName: nodeEnv?.GITHUB_REF_NAME, - refType: nodeEnv?.GITHUB_REF_TYPE -}) +const projectRoot = resolveProjectRoot(__dirname) export default defineConfig({ base: '/modern', - define: { - 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageVersion), - 'import.meta.env.VITE_COMMIT_SHA_SHORT': JSON.stringify(shortCommitSha), - 'import.meta.env.VITE_GIT_EXACT_TAG': JSON.stringify(exactTag), - 'import.meta.env.VITE_GITHUB_REF_NAME': JSON.stringify(nodeEnv?.GITHUB_REF_NAME), - 'import.meta.env.VITE_GITHUB_REF_TYPE': JSON.stringify(nodeEnv?.GITHUB_REF_TYPE), - 'import.meta.env.VITE_SPA_DISPLAY_VERSION': JSON.stringify(spaDisplayVersion) - }, + define: createSpaVersionDefines(projectRoot), plugins: [ react(), istanbul({ From 80b42dfee87cb9bedbb9231a02febc4b32776ded Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Tue, 21 Apr 2026 13:34:08 -0400 Subject: [PATCH 6/9] add item to CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db245d58..bdb6dcd4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - Added disclaimer text and custom popup text to Publish Dataset modal for better communication of the implications of publishing a dataset. The text can be configured through the runtime configuration. - Dataset page Terms tab title now depends on permissions: users with dataset update permission see `Terms and Guestbook`, and read-only users see `Terms`. - Avoided prop-drilling for collection repository, so used context to share epository instances. +- Added frontend version to the footer of the application, which is retrieved from the `version` field in `package.json` at build time. ### Fixed From 32f63fcc8c32a42c133633e9b022efdae98f0cb3 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 23 Apr 2026 05:44:01 -0400 Subject: [PATCH 7/9] add configurable banner message --- public/config.js | 3 +++ src/config.ts | 1 + src/sections/layout/Layout.tsx | 16 +++++++++++----- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/public/config.js b/public/config.js index bd63f3f93..a1fa7944a 100644 --- a/public/config.js +++ b/public/config.js @@ -5,6 +5,9 @@ window.__APP_CONFIG__ = { // Base URL of your Dataverse backend backendUrl: 'http://localhost:8000', + // Optional banner shown at the top of the app when set. Basic HTML markup is supported. + bannerMessage: + 'You are using the new Dataverse Modern version. This is an early release and some features from the original site are not yet available.', // OIDC provider settings oidc: { clientId: 'test', diff --git a/src/config.ts b/src/config.ts index 2f6d423f2..fb4530cef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,7 @@ let CONFIG: AppConfig | undefined const AppConfigSchema = z.object({ backendUrl: z.url(), + bannerMessage: z.string().optional(), oidc: z.object({ clientId: z.string(), authorizationEndpoint: z.url(), diff --git a/src/sections/layout/Layout.tsx b/src/sections/layout/Layout.tsx index a0924c2cd..e27b39c76 100644 --- a/src/sections/layout/Layout.tsx +++ b/src/sections/layout/Layout.tsx @@ -1,3 +1,4 @@ +import DOMPurify from 'dompurify' import { Outlet } from 'react-router-dom' import { Container } from '@iqss/dataverse-design-system' import styles from './Layout.module.scss' @@ -5,18 +6,23 @@ import { FooterFactory } from './footer/FooterFactory' import TopBarProgressIndicator from './topbar-progress-indicator/TopbarProgressIndicator' import { HeaderFactory } from './header/HeaderFactory' import { HistoryTrackerProvider } from '@/router/HistoryTrackerProvider' +import { requireAppConfig } from '@/config' export function Layout() { + const { bannerMessage } = requireAppConfig() + const sanitizedBannerMessage = bannerMessage + ? DOMPurify.sanitize(bannerMessage, { USE_PROFILES: { html: true } }) + : null + return ( {HeaderFactory.create()} - {/*
-
- You are using the new Dataverse Modern version. This is an early release and - some features from the original site are not yet available. + {sanitizedBannerMessage ? ( +
+
-
*/} + ) : null}
From 7b4a63c0eb6578105c8300c8c21d690f812089b2 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 23 Apr 2026 05:54:37 -0400 Subject: [PATCH 8/9] add test for banner message --- .../component/sections/layout/Layout.spec.tsx | 44 +++++++++++++++---- tests/support/bootstrapAppConfig.ts | 2 + 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/tests/component/sections/layout/Layout.spec.tsx b/tests/component/sections/layout/Layout.spec.tsx index 63a18eb65..591349ef5 100644 --- a/tests/component/sections/layout/Layout.spec.tsx +++ b/tests/component/sections/layout/Layout.spec.tsx @@ -2,13 +2,21 @@ import { createSandbox, SinonSandbox } from 'sinon' import { FooterFactory } from '../../../../src/sections/layout/footer/FooterFactory' import { FooterMother } from './footer/FooterMother' import { Layout } from '../../../../src/sections/layout/Layout' +import { applyTestAppConfig } from '../../../support/bootstrapAppConfig' +import type { AppConfig } from '@/config' describe('Layout', () => { const sandbox: SinonSandbox = createSandbox() + const defaultBannerMessageEnv = Cypress.env('bannerMessage') as AppConfig['bannerMessage'] + + beforeEach(() => { + sandbox.stub(FooterFactory, 'create').returns(FooterMother.withDataverseVersion(sandbox)) + }) afterEach(() => { sandbox.restore() - sandbox.stub(FooterFactory, 'create').returns(FooterMother.withDataverseVersion(sandbox)) + Cypress.env('bannerMessage', defaultBannerMessageEnv) + applyTestAppConfig() }) it('renders the header', () => { @@ -22,16 +30,34 @@ describe('Layout', () => { }) it('renders the Footer', () => { - cy.customMount() + cy.customMount() - it('displays the Powered By link', () => { - cy.customMount() - cy.findByRole('link', { name: 'The Dataverse Project logo' }).should('exist') - }) + cy.findByRole('link', { name: 'The Dataverse Project logo' }).should('exist') + cy.findByText('Privacy Policy').should('exist') + }) + + it('does not render a banner when bannerMessage is not configured', () => { + Cypress.env('bannerMessage', undefined) + applyTestAppConfig() + + cy.customMount() + + cy.findByRole('alert').should('not.exist') + }) + + it('renders banner markup from config after sanitizing it', () => { + Cypress.env( + 'bannerMessage', + 'You are using the new Dataverse Modern version. ' + ) + applyTestAppConfig() + + cy.customMount() - it('displays the Privacy Policy', () => { - cy.customMount() - cy.findByText('privacyPolicy').should('exist') + cy.findByRole('alert').within(() => { + cy.findByText('You are using the new Dataverse', { exact: false }).should('exist') + cy.get('strong').should('contain.text', 'Modern version') + cy.get('script').should('not.exist') }) }) }) diff --git a/tests/support/bootstrapAppConfig.ts b/tests/support/bootstrapAppConfig.ts index ba144e5fc..9676ad08a 100644 --- a/tests/support/bootstrapAppConfig.ts +++ b/tests/support/bootstrapAppConfig.ts @@ -10,6 +10,7 @@ declare global { } function buildTestConfig(): AppConfig { + const bannerMessage = Cypress.env('bannerMessage') as AppConfig['bannerMessage'] const branding = (Cypress.env('branding') as AppConfig['branding']) ?? { dataverseName: 'Dataverse' } @@ -23,6 +24,7 @@ function buildTestConfig(): AppConfig { return { backendUrl: Cypress.env('backendUrl') as string, + bannerMessage, oidc: { clientId: Cypress.env('oidcClientId') as string, authorizationEndpoint: Cypress.env('oidcAuthorizationEndpoint') as string, From d4f7ab74788ef12bef9c9b6bd44bd07d73a690fc Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 23 Apr 2026 09:47:25 -0400 Subject: [PATCH 9/9] add changelog item for banner message --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb6dcd4f..27d44d435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - Added runtime configuration options for homepage branding and support link. - Added an environment variable to docker-compose-dev.yml to hide the OIDC client used in the SPA from the JSF frontend: DATAVERSE_AUTH_OIDC_HIDDEN_JSF: 1 - Download with terms of use and guestbook. +- Layout: added a configurable homepage banner for announcements and important messages. (#787) ### Changed