merge: 최신 main을 토너먼트 조별 순위 카드에 통합한다
This commit is contained in:
@@ -553,6 +553,10 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
specialDomestic: resolveTraitDisplayName(general.specialCode, domesticNames),
|
||||
specialWar: resolveTraitDisplayName(general.special2Code, warNames),
|
||||
},
|
||||
traitAges: {
|
||||
specialDomestic: readNumber(metaRecord.specage, 0),
|
||||
specialWar: readNumber(metaRecord.specage2, 0),
|
||||
},
|
||||
traitInfo: {
|
||||
personal: personalityNames.get(general.personalCode)?.info ?? '',
|
||||
specialDomestic: domesticNames.get(general.specialCode)?.info ?? '',
|
||||
|
||||
@@ -510,6 +510,7 @@ describe('in-game my information ownership', () => {
|
||||
personalCode: 'che_안전',
|
||||
specialCode: 'che_상재',
|
||||
special2Code: 'che_신산',
|
||||
meta: { specage: 31, specage2: 35 },
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -520,6 +521,10 @@ describe('in-game my information ownership', () => {
|
||||
specialDomestic: '상재',
|
||||
specialWar: '신산',
|
||||
},
|
||||
traitAges: {
|
||||
specialDomestic: 31,
|
||||
specialWar: 35,
|
||||
},
|
||||
traitInfo: {
|
||||
personal: '사기 -5, 징·모병 비용 -20%',
|
||||
specialDomestic: '[내정] 상업 투자 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
|
||||
@@ -543,6 +548,31 @@ describe('in-game my information ownership', () => {
|
||||
expect(fixture.db.nation.findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns the scheduled acquisition ages when the owned general has no domestic or war trait', async () => {
|
||||
const fixture = createContext({
|
||||
me: buildGeneral({
|
||||
age: 30,
|
||||
specialCode: 'None',
|
||||
special2Code: 'None',
|
||||
meta: { specage: 35, specage2: 29 },
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
|
||||
general: {
|
||||
age: 30,
|
||||
traits: {
|
||||
specialDomestic: '-',
|
||||
specialWar: '-',
|
||||
},
|
||||
traitAges: {
|
||||
specialDomestic: 35,
|
||||
specialWar: 29,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('reads legacy top-level settings and dispatches only the session-owned general', async () => {
|
||||
const requestCommand = vi.fn(async () => ({ type: 'setMySetting', ok: true, generalId: 7 }));
|
||||
const fixture = createContext({ requestCommand });
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { gamePath } from './gameTestPaths.js';
|
||||
|
||||
const currentCommitSha = process.env.PLAYWRIGHT_BUILD_COMMIT_SHA ?? '0123456789abcdef0123456789abcdef01234567';
|
||||
const nextCommitSha = '89abcdef0123456789abcdef0123456789abcdef';
|
||||
const noticeMessage = '새 버전이 준비되었습니다. 새로고침하면 변경사항이 반영됩니다.';
|
||||
|
||||
const installVersionFixture = async (page: Page) => {
|
||||
let availableCommitSha = currentCommitSha;
|
||||
let requests = 0;
|
||||
await page.route('**/deployment-version.json*', async (route) => {
|
||||
requests += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ commitSha: availableCommitSha }),
|
||||
});
|
||||
});
|
||||
return {
|
||||
deployNextVersion: () => {
|
||||
availableCommitSha = nextCommitSha;
|
||||
},
|
||||
requestCount: () => requests,
|
||||
};
|
||||
};
|
||||
|
||||
for (const viewport of [
|
||||
{ name: 'desktop', width: 1280, height: 800 },
|
||||
{ name: 'mobile', width: 390, height: 844 },
|
||||
]) {
|
||||
test(`shows one quiet update toast without forcing reload on ${viewport.name}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||||
const fixture = await installVersionFixture(page);
|
||||
await page.goto(gamePath('/version-notice-fixture'));
|
||||
await expect(page.getByRole('heading', { name: 'Not Found' })).toBeVisible();
|
||||
await expect.poll(fixture.requestCount).toBeGreaterThan(0);
|
||||
await page.evaluate(() => {
|
||||
Object.assign(window, { __versionNoticePageMarker: 'kept' });
|
||||
});
|
||||
|
||||
fixture.deployNextVersion();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('online')));
|
||||
return fixture.requestCount();
|
||||
})
|
||||
.toBeGreaterThan(1);
|
||||
const toast = page.getByTestId('game-toast').filter({ hasText: noticeMessage });
|
||||
await expect(toast).toBeVisible();
|
||||
await expect(toast).toHaveAttribute('data-feedback-kind', 'info');
|
||||
await expect(toast).toHaveCSS('transform', 'none');
|
||||
const box = await toast.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
expect(box!.x).toBeGreaterThanOrEqual(0);
|
||||
expect(box!.y).toBeGreaterThanOrEqual(0);
|
||||
expect(box!.x + box!.width).toBeLessThanOrEqual(viewport.width);
|
||||
expect(box!.y + box!.height).toBeLessThanOrEqual(viewport.height);
|
||||
expect(await page.evaluate(() => Reflect.get(window, '__versionNoticePageMarker'))).toBe('kept');
|
||||
|
||||
await page.getByRole('button', { name: '알림 닫기' }).click();
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('online')));
|
||||
await expect(toast).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
@@ -84,6 +84,7 @@ type FixtureState = {
|
||||
joinConfig?: Record<string, unknown>;
|
||||
createGeneralInputs?: Array<Record<string, unknown>>;
|
||||
mainTraits?: { personal: string; specialDomestic: string; specialWar: string };
|
||||
mainTraitAges?: { specialDomestic: number; specialWar: number };
|
||||
richMyInfo?: boolean;
|
||||
hiddenSeedLogText?: string;
|
||||
recentRecords?: {
|
||||
@@ -137,6 +138,7 @@ const myGeneral = (state: FixtureState) => ({
|
||||
}
|
||||
: null,
|
||||
traits: state.mainTraits ?? { personal: '-', specialDomestic: '-', specialWar: '-' },
|
||||
traitAges: state.mainTraitAges ?? { specialDomestic: 31, specialWar: 31 },
|
||||
traitInfo: state.richMyInfo
|
||||
? {
|
||||
personal: '부상당할 확률이 감소합니다.',
|
||||
@@ -732,6 +734,47 @@ test('재야 메인은 국가 틀과 성격·특기 표기명을 Chromium에 표
|
||||
await persistParityArtifact(page, 'main-neutral-trait-display', geometry);
|
||||
});
|
||||
|
||||
test('메인 장수 정보는 없는 내정·전투 특기의 Ref 획득 나이를 Chromium에 표시한다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
permission: 'member',
|
||||
myset: 0,
|
||||
mainTraits: { personal: '안전', specialDomestic: '-', specialWar: '-' },
|
||||
mainTraitAges: { specialDomestic: 35, specialWar: 29 },
|
||||
settingMutations: [],
|
||||
accessPages: [],
|
||||
};
|
||||
await install(page, state);
|
||||
|
||||
for (const viewport of [
|
||||
{ width: 1000, height: 900 },
|
||||
{ width: 390, height: 844 },
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
await page.goto('');
|
||||
|
||||
const specialValue = page.locator('.general-card .special-value');
|
||||
await expect(specialValue).toHaveText(/35세\s*\/\s*31세/u);
|
||||
await expect(specialValue).toHaveAttribute('aria-label', '35세 / 31세');
|
||||
const geometry = await specialValue.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const documentWidth = document.documentElement.scrollWidth;
|
||||
return {
|
||||
text: element.textContent?.replace(/\s+/gu, ' ').trim(),
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
width: rect.width,
|
||||
documentWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
};
|
||||
});
|
||||
expect(geometry.width).toBeGreaterThan(0);
|
||||
expect(geometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(geometry.right).toBeLessThanOrEqual(geometry.documentWidth);
|
||||
expect(geometry.documentWidth).toBe(Math.max(viewport.width, 500));
|
||||
await persistParityArtifact(page, `main-speciality-age-${viewport.width}`, geometry);
|
||||
}
|
||||
});
|
||||
|
||||
test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명으로 표시된다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
permission: 'head',
|
||||
|
||||
@@ -42,6 +42,7 @@ export default defineConfig({
|
||||
'session-auth.spec.ts',
|
||||
'npcPossession.spec.ts',
|
||||
'joinLayout.spec.ts',
|
||||
'deploymentVersionNotice.spec.ts',
|
||||
],
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import GameFeedbackLayer from './components/ui/GameFeedbackLayer.vue';
|
||||
import { useDeploymentVersionNotice } from './composables/useDeploymentVersionNotice';
|
||||
|
||||
useDeploymentVersionNotice();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -99,6 +99,7 @@ export interface GeneralBasicCardData {
|
||||
crewTypeName?: string;
|
||||
crewTypeInfo?: CrewTypeDisplayInfo | null;
|
||||
traits?: { personal: string; specialWar: string; specialDomestic: string };
|
||||
traitAges?: { specialWar: number; specialDomestic: number };
|
||||
traitInfo?: { personal: string; specialWar: string; specialDomestic: string };
|
||||
progression?: GeneralProgression;
|
||||
itemNames?: ItemDisplayNames;
|
||||
@@ -226,9 +227,19 @@ const displayDefence = computed(() => {
|
||||
});
|
||||
const displayKillTurn = computed(() => props.general?.killTurn ?? props.killTurn);
|
||||
const displayRemainingMinutes = computed(() => props.general?.remainingMinutes ?? props.remainingMinutes);
|
||||
const resolveSpecialDisplayName = (kind: 'specialDomestic' | 'specialWar') => {
|
||||
const general = props.general;
|
||||
if (!general) return '-';
|
||||
const traitName = general.traits?.[kind];
|
||||
if (traitName && traitName !== '-') return traitName;
|
||||
const scheduledAge = general.traitAges?.[kind];
|
||||
if (general.age === undefined || scheduledAge === undefined) return '-';
|
||||
return `${Math.max(general.age + 1, scheduledAge)}세`;
|
||||
};
|
||||
const specialDomesticText = computed(() => resolveSpecialDisplayName('specialDomestic'));
|
||||
const specialWarText = computed(() => resolveSpecialDisplayName('specialWar'));
|
||||
const specialText = computed(() => {
|
||||
const traits = props.general?.traits;
|
||||
return traits ? `${traits.specialDomestic || '-'} / ${traits.specialWar || '-'}` : '-';
|
||||
return `${specialDomesticText.value} / ${specialWarText.value}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -379,19 +390,19 @@ const specialText = computed(() => {
|
||||
<span class="cell-label">특기</span>
|
||||
<strong class="special-value" :aria-label="specialText">
|
||||
<RichTooltip
|
||||
:title="`내정특기 · ${props.general.traits?.specialDomestic ?? '-'}`"
|
||||
:title="`내정특기 · ${specialDomesticText}`"
|
||||
:description="props.general.traitInfo?.specialDomestic"
|
||||
test-id="special-domestic"
|
||||
>
|
||||
{{ props.general.traits?.specialDomestic ?? '-' }}
|
||||
{{ specialDomesticText }}
|
||||
</RichTooltip>
|
||||
/
|
||||
<RichTooltip
|
||||
:title="`전투특기 · ${props.general.traits?.specialWar ?? '-'}`"
|
||||
:title="`전투특기 · ${specialWarText}`"
|
||||
:description="props.general.traitInfo?.specialWar"
|
||||
test-id="special-war"
|
||||
>
|
||||
{{ props.general.traits?.specialWar ?? '-' }}
|
||||
{{ specialWarText }}
|
||||
</RichTooltip>
|
||||
</strong>
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import { createDeploymentVersionChecker } from '../config/deploymentVersion';
|
||||
import { useGameFeedback } from './useGameFeedback';
|
||||
|
||||
const pollIntervalMs = 60_000;
|
||||
const noticeMessage = '새 버전이 준비되었습니다. 새로고침하면 변경사항이 반영됩니다.';
|
||||
|
||||
const resolveSessionStorage = (): Pick<Storage, 'getItem' | 'setItem'> | undefined => {
|
||||
try {
|
||||
return window.sessionStorage;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const useDeploymentVersionNotice = (): void => {
|
||||
const currentCommitSha = import.meta.env.VITE_BUILD_COMMIT_SHA?.trim() ?? '';
|
||||
const versionUrl = `${import.meta.env.BASE_URL}deployment-version.json`;
|
||||
const { info: showInfoToast } = useGameFeedback();
|
||||
const checker = createDeploymentVersionChecker({
|
||||
currentCommitSha,
|
||||
versionUrl,
|
||||
storage: resolveSessionStorage(),
|
||||
onVersionChanged: () => showInfoToast(noticeMessage),
|
||||
});
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const checkWhenVisible = (): void => {
|
||||
if (document.visibilityState === 'visible') void checker.check();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
void checker.check();
|
||||
pollTimer = setInterval(checkWhenVisible, pollIntervalMs);
|
||||
document.addEventListener('visibilitychange', checkWhenVisible);
|
||||
window.addEventListener('online', checkWhenVisible);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
document.removeEventListener('visibilitychange', checkWhenVisible);
|
||||
window.removeEventListener('online', checkWhenVisible);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
const fullCommitShaPattern = /^[0-9a-f]{40,64}$/iu;
|
||||
|
||||
type VersionStorage = Pick<Storage, 'getItem' | 'setItem'>;
|
||||
|
||||
export type DeploymentVersionCheckerOptions = {
|
||||
currentCommitSha: string;
|
||||
versionUrl: string;
|
||||
fetchVersion?: typeof fetch;
|
||||
storage?: VersionStorage;
|
||||
now?: () => number;
|
||||
onVersionChanged: (availableCommitSha: string) => void;
|
||||
};
|
||||
|
||||
export const deploymentVersionAssetSource = (buildCommitSha: string): string =>
|
||||
`${JSON.stringify({ commitSha: buildCommitSha })}\n`;
|
||||
|
||||
export const parseDeploymentCommitSha = (payload: unknown): string | null => {
|
||||
if (!payload || typeof payload !== 'object' || !('commitSha' in payload)) return null;
|
||||
const commitSha = String(payload.commitSha).trim().toLowerCase();
|
||||
return fullCommitShaPattern.test(commitSha) ? commitSha : null;
|
||||
};
|
||||
|
||||
const notificationStorageKey = (versionUrl: string, availableCommitSha: string): string =>
|
||||
`sammo:deployment-version-notice:${versionUrl}:${availableCommitSha}`;
|
||||
|
||||
export const createDeploymentVersionChecker = (options: DeploymentVersionCheckerOptions) => {
|
||||
const currentCommitSha = options.currentCommitSha.trim().toLowerCase();
|
||||
const fetchVersion = options.fetchVersion ?? fetch;
|
||||
const now = options.now ?? Date.now;
|
||||
let inFlight: Promise<void> | null = null;
|
||||
let lastNotifiedCommitSha: string | null = null;
|
||||
|
||||
const wasNotified = (key: string): boolean => {
|
||||
try {
|
||||
return options.storage?.getItem(key) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const rememberNotification = (key: string): void => {
|
||||
try {
|
||||
options.storage?.setItem(key, '1');
|
||||
} catch {
|
||||
// Session storage can be unavailable under restrictive browser policies.
|
||||
}
|
||||
};
|
||||
|
||||
const run = async (): Promise<void> => {
|
||||
if (!fullCommitShaPattern.test(currentCommitSha)) return;
|
||||
const separator = options.versionUrl.includes('?') ? '&' : '?';
|
||||
const response = await fetchVersion(`${options.versionUrl}${separator}t=${now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const availableCommitSha = parseDeploymentCommitSha(await response.json());
|
||||
if (!availableCommitSha || availableCommitSha === currentCommitSha) return;
|
||||
|
||||
const storageKey = notificationStorageKey(options.versionUrl, availableCommitSha);
|
||||
if (lastNotifiedCommitSha === availableCommitSha || wasNotified(storageKey)) return;
|
||||
lastNotifiedCommitSha = availableCommitSha;
|
||||
rememberNotification(storageKey);
|
||||
options.onVersionChanged(availableCommitSha);
|
||||
};
|
||||
|
||||
return {
|
||||
check: (): Promise<void> => {
|
||||
if (inFlight) return inFlight;
|
||||
inFlight = run()
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
inFlight = null;
|
||||
});
|
||||
return inFlight;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
createDeploymentVersionChecker,
|
||||
deploymentVersionAssetSource,
|
||||
parseDeploymentCommitSha,
|
||||
} from '../src/config/deploymentVersion.ts';
|
||||
|
||||
const currentCommitSha = '0123456789abcdef0123456789abcdef01234567';
|
||||
const nextCommitSha = '89abcdef0123456789abcdef0123456789abcdef';
|
||||
const laterCommitSha = 'fedcba9876543210fedcba9876543210fedcba98';
|
||||
|
||||
const jsonResponse = (commitSha: string): Response =>
|
||||
new Response(JSON.stringify({ commitSha }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
void test('accepts only full hexadecimal deployment commit values', () => {
|
||||
assert.equal(parseDeploymentCommitSha({ commitSha: nextCommitSha.toUpperCase() }), nextCommitSha);
|
||||
assert.equal(parseDeploymentCommitSha({ commitSha: 'main' }), null);
|
||||
assert.equal(parseDeploymentCommitSha({ version: nextCommitSha }), null);
|
||||
assert.equal(parseDeploymentCommitSha(null), null);
|
||||
});
|
||||
|
||||
void test('exposes only the read-only build commit in the deployment version asset', () => {
|
||||
assert.deepEqual(JSON.parse(deploymentVersionAssetSource(currentCommitSha)), { commitSha: currentCommitSha });
|
||||
});
|
||||
|
||||
void test('notifies once per available version and bypasses browser caches', async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
||||
const stored = new Map<string, string>();
|
||||
const notifications: string[] = [];
|
||||
let availableCommitSha = currentCommitSha;
|
||||
const checker = createDeploymentVersionChecker({
|
||||
currentCommitSha,
|
||||
versionUrl: '/che/deployment-version.json',
|
||||
now: () => 1234,
|
||||
storage: {
|
||||
getItem: (key) => stored.get(key) ?? null,
|
||||
setItem: (key, value) => void stored.set(key, value),
|
||||
},
|
||||
fetchVersion: async (url, init) => {
|
||||
requests.push({ url: String(url), init });
|
||||
return jsonResponse(availableCommitSha);
|
||||
},
|
||||
onVersionChanged: (commitSha) => notifications.push(commitSha),
|
||||
});
|
||||
|
||||
await checker.check();
|
||||
availableCommitSha = nextCommitSha;
|
||||
await checker.check();
|
||||
await checker.check();
|
||||
availableCommitSha = laterCommitSha;
|
||||
await checker.check();
|
||||
|
||||
assert.deepEqual(notifications, [nextCommitSha, laterCommitSha]);
|
||||
assert.equal(requests.every(({ url }) => url === '/che/deployment-version.json?t=1234'), true);
|
||||
assert.equal(requests.every(({ init }) => init?.cache === 'no-store'), true);
|
||||
assert.equal(requests.every(({ init }) => new Headers(init?.headers).get('Cache-Control') === 'no-cache'), true);
|
||||
});
|
||||
|
||||
void test('shares the once-only notice within the current tab session', async () => {
|
||||
const stored = new Map<string, string>();
|
||||
const storage = {
|
||||
getItem: (key: string) => stored.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void stored.set(key, value),
|
||||
};
|
||||
let notifications = 0;
|
||||
const options = {
|
||||
currentCommitSha,
|
||||
versionUrl: '/hwe/deployment-version.json',
|
||||
storage,
|
||||
fetchVersion: async () => jsonResponse(nextCommitSha),
|
||||
onVersionChanged: () => notifications++,
|
||||
};
|
||||
|
||||
await createDeploymentVersionChecker(options).check();
|
||||
await createDeploymentVersionChecker(options).check();
|
||||
|
||||
assert.equal(notifications, 1);
|
||||
});
|
||||
|
||||
void test('ignores unavailable or malformed version documents without disrupting the page', async () => {
|
||||
let notifications = 0;
|
||||
const unavailable = createDeploymentVersionChecker({
|
||||
currentCommitSha,
|
||||
versionUrl: '/che/deployment-version.json',
|
||||
fetchVersion: async () => {
|
||||
throw new Error('offline');
|
||||
},
|
||||
onVersionChanged: () => notifications++,
|
||||
});
|
||||
const malformed = createDeploymentVersionChecker({
|
||||
currentCommitSha,
|
||||
versionUrl: '/che/deployment-version.json',
|
||||
fetchVersion: async () => new Response('{', { status: 200 }),
|
||||
onVersionChanged: () => notifications++,
|
||||
});
|
||||
|
||||
await unavailable.check();
|
||||
await malformed.check();
|
||||
assert.equal(notifications, 0);
|
||||
});
|
||||
@@ -48,6 +48,17 @@ void describe('game frontend Vite config', () => {
|
||||
loaded?.config.define?.['import.meta.env.VITE_BUILD_COMMIT_SHA'],
|
||||
JSON.stringify(commitSha.toLowerCase())
|
||||
);
|
||||
assert.equal(
|
||||
loaded?.config.plugins?.some(
|
||||
(plugin) =>
|
||||
plugin !== null &&
|
||||
typeof plugin === 'object' &&
|
||||
!Array.isArray(plugin) &&
|
||||
'name' in plugin &&
|
||||
plugin.name === 'sammo-deployment-version'
|
||||
),
|
||||
true
|
||||
);
|
||||
} finally {
|
||||
if (previousCommitSha === undefined) delete process.env.VITE_BUILD_COMMIT_SHA;
|
||||
else process.env.VITE_BUILD_COMMIT_SHA = previousCommitSha;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import { defineConfig, loadEnv, type Plugin } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import path from 'path';
|
||||
import { mergeViteEnv } from './src/config/viteEnv';
|
||||
import { deploymentVersionAssetSource } from './src/config/deploymentVersion.ts';
|
||||
import { mergeViteEnv } from './src/config/viteEnv.ts';
|
||||
|
||||
const fullCommitShaPattern = /^[0-9a-f]{40,64}$/iu;
|
||||
|
||||
@@ -24,6 +25,17 @@ export const resolveBuildCommitSha = (explicitSha: string | undefined, repositor
|
||||
}
|
||||
};
|
||||
|
||||
export const createDeploymentVersionPlugin = (buildCommitSha: string): Plugin => ({
|
||||
name: 'sammo-deployment-version',
|
||||
generateBundle() {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: 'deployment-version.json',
|
||||
source: deploymentVersionAssetSource(buildCommitSha),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const normalizeBasePath = (value: string | undefined): string => {
|
||||
const pathValue = (value ?? '/').trim();
|
||||
if (!pathValue || pathValue === '/') {
|
||||
@@ -50,7 +62,7 @@ export default defineConfig(({ mode }) => {
|
||||
const buildCommitSha = resolveBuildCommitSha(env.VITE_BUILD_COMMIT_SHA, path.resolve(import.meta.dirname, '../..'));
|
||||
return {
|
||||
base: normalizeBasePath(env.VITE_APP_BASE_PATH),
|
||||
plugins: [vue(), tailwindcss()],
|
||||
plugins: [vue(), tailwindcss(), createDeploymentVersionPlugin(buildCommitSha)],
|
||||
define: {
|
||||
'import.meta.env.VITE_BUILD_COMMIT_SHA': JSON.stringify(buildCommitSha),
|
||||
},
|
||||
|
||||
@@ -23,11 +23,15 @@ describe('profile frontend materializer', () => {
|
||||
await mkdir(source, { recursive: true });
|
||||
await mkdir(target, { recursive: true });
|
||||
await writeFile(path.join(source, 'index.html'), 'new release');
|
||||
await writeFile(path.join(source, 'deployment-version.json'), '{"commitSha":"new"}\n');
|
||||
await writeFile(path.join(target, 'index.html'), 'old release');
|
||||
|
||||
await execFileAsync(process.execPath, [materializer, 'che:2'], { cwd: workspaceRoot });
|
||||
|
||||
await expect(readFile(path.join(target, 'index.html'), 'utf8')).resolves.toBe('new release');
|
||||
await expect(readFile(path.join(target, 'deployment-version.json'), 'utf8')).resolves.toBe(
|
||||
'{"commitSha":"new"}\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps an existing artifact untouched when the cached build is missing', async () => {
|
||||
|
||||
Reference in New Issue
Block a user