diff --git a/CHANGELOG.md b/CHANGELOG.md index ea30b500d..9daffd103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - Added a message note to the login page - Download with terms of use and guestbook. - Show terms modal before download when dataset has custom terms, a non-default license (not CC0 1.0), or a guestbook. Draft datasets and dataset editors bypass the modal. +- Layout: added a configurable homepage banner for announcements and important messages. (#787) ### Changed @@ -33,6 +34,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 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 f4034adeb..aa8bdb038 100644 --- a/dev-env/shib-dev-env/vite.config.ts +++ b/dev-env/shib-dev-env/vite.config.ts @@ -1,9 +1,30 @@ +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 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))) + +if (!sharedBuildModuleUrl) { + throw new Error('Could not locate build/spaVersionMetadata.mjs') +} + +const { createSpaVersionDefines, resolveProjectRoot } = (await import(sharedBuildModuleUrl.href)) as { + createSpaVersionDefines: (projectRoot: string) => Record + resolveProjectRoot: (configDir: string) => string +} + +const projectRoot = resolveProjectRoot(__dirname) + export default defineConfig({ + root: projectRoot, + define: createSpaVersionDefines(projectRoot), plugins: [ react(), istanbul({ @@ -24,8 +45,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..57c38dec3 100644 --- a/dev-env/vite.config.ts +++ b/dev-env/vite.config.ts @@ -1,10 +1,31 @@ +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 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))) + +if (!sharedBuildModuleUrl) { + throw new Error('Could not locate build/spaVersionMetadata.mjs') +} + +const { createSpaVersionDefines, resolveProjectRoot } = (await import(sharedBuildModuleUrl.href)) as { + createSpaVersionDefines: (projectRoot: string) => Record + resolveProjectRoot: (configDir: string) => string +} + +const projectRoot = resolveProjectRoot(__dirname) + export default defineConfig({ + root: projectRoot, base: '/modern', + define: createSpaVersionDefines(projectRoot), plugins: [ react(), istanbul({ @@ -26,8 +47,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/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/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..8779a5ce4 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": "versión del frontend: {{version}}" } 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}
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 5c289fe00..736062a8b 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -2,6 +2,12 @@ interface ImportMetaEnv { readonly VITE_DATAVERSE_BASE_URL?: string + 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 { 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/component/sections/layout/footer/Footer.spec.tsx b/tests/component/sections/layout/footer/Footer.spec.tsx index 5b79f7114..c6bc0d0d8 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(`frontend version: ${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/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, 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 55ed74726..472516589 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,31 @@ +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 { keycloakify } from 'keycloakify/vite-plugin' import * as path from 'path' +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))) + +if (!sharedBuildModuleUrl) { + throw new Error('Could not locate build/spaVersionMetadata.mjs') +} + +const { createSpaVersionDefines, resolveProjectRoot } = (await import(sharedBuildModuleUrl.href)) as { + createSpaVersionDefines: (projectRoot: string) => Record + resolveProjectRoot: (configDir: string) => string +} + +const projectRoot = resolveProjectRoot(__dirname) + export default defineConfig({ base: '/modern', + define: createSpaVersionDefines(projectRoot), plugins: [ react(), istanbul({ @@ -14,7 +34,7 @@ export default defineConfig({ }), keycloakify({ themeName: 'dataverse-spa', - keycloakifyBuildDirPath: "./dist_keycloak", + keycloakifyBuildDirPath: './dist_keycloak', accountThemeImplementation: 'none', keycloakVersionTargets: { '22-to-25': false,