feat(changelog): add release ledger data
Lint and Build Checks / Lint, Test, and Build (push) Failing after 560h18m4s
Lint and Build Checks / Lint, Test, and Build (push) Failing after 560h18m4s
This commit is contained in:
@@ -1 +1,2 @@
|
||||
npm run version:bump-build
|
||||
npm run validate:commit
|
||||
|
||||
@@ -61,13 +61,15 @@ __tests__/ # Jest tests
|
||||
|
||||
## Current App Behavior
|
||||
|
||||
- Home menu shows New Game, Saves, Settings, Policies, Credits, and Toolkit.
|
||||
- Home menu shows New Game, Saves, Settings, Policies, Change Log, Credits, and Toolkit.
|
||||
- Toolkit is shown only when the resolution setting is `browser`.
|
||||
- Toolkit is clickable only when the socket status is connected.
|
||||
- Route state persists through refreshes.
|
||||
- Settings persist resolution, autosave, mute, and master/music/ambience/speech/SFX volume.
|
||||
- Browser background music is implemented in `useBackgroundMusic.web.ts`.
|
||||
- Change Log content is sourced from `src/data/changelog.json`.
|
||||
- Credits Toolkit data is sourced from `src/data/credits.json` and synced through the socket server.
|
||||
- Toolkit placeholder datasets live under `src/data/toolkit/` as empty arrays until schemas stabilize.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
@@ -96,6 +98,13 @@ __tests__/ # Jest tests
|
||||
4. Extend `src/socket/toolkitSocketClient.ts` and a store under `src/state/` for client state.
|
||||
5. Keep Toolkit UI read-only unless editable forms are explicitly requested.
|
||||
|
||||
### Add or change data content
|
||||
|
||||
1. Read `src/data/AGENTS.md` first.
|
||||
2. Keep app-imported JSON covered by a typed export and runtime validation.
|
||||
3. Keep Toolkit placeholder resources as arrays in `src/data/toolkit/` until concrete schemas are requested.
|
||||
4. Add tests for new data shape, ordering, or placeholder expectations.
|
||||
|
||||
### Work with assets
|
||||
|
||||
- Images: `src/assets/images/`
|
||||
@@ -136,7 +145,7 @@ npm run start
|
||||
- Socket changes: run tests plus `npm run dev:toolkit` and check `/health`
|
||||
- Commit validation: `npm run validate:commit`
|
||||
|
||||
Husky runs `npm run validate:commit` on pre-commit.
|
||||
Husky runs `npm run version:bump-build` and then `npm run validate:commit` on pre-commit. The version bump updates package metadata and public `.env` build fields for `version-yyyymmdd-last7commit` display.
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Change Log
|
||||
|
||||
All notable project deliverables and build bumps are tracked here.
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- Established the browser and Electron game shell.
|
||||
- Added persisted settings, route state, Toolkit gating, and socket status.
|
||||
- Added Toolkit planning surfaces and socket-backed Credits data.
|
||||
- Added the first-pass gameplay phase scaffold with local saves.
|
||||
|
||||
@@ -70,14 +70,14 @@ npm run web:build
|
||||
npm run electron:build
|
||||
```
|
||||
|
||||
Husky runs `npm run validate:commit` on commit. That command runs lint, Jest, and the Electron build path.
|
||||
Husky increments the patch build version with `npm run version:bump-build`, writes public build metadata to `.env`, then runs `npm run validate:commit` on commit. Validation runs lint, Jest, and the Electron build path. The menu build label uses `version-yyyymmdd-last7commit`.
|
||||
|
||||
## App Structure
|
||||
|
||||
```text
|
||||
App.tsx # Safe area, status bar, viewport, navigator root
|
||||
src/navigation/ # Route metadata and lightweight app-state navigator
|
||||
src/screens/ # Home, Settings, Policies, Toolkit, generic pages
|
||||
src/screens/ # Home, Gameplay, Saves, Settings, Policies, Toolkit, generic pages
|
||||
src/components/ # Shared React Native UI primitives
|
||||
src/state/ # Zustand stores and persistence adapters
|
||||
src/settings/ # Resolution and audio setting definitions
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
changelogEntries,
|
||||
getChangelogEntriesNewestFirst,
|
||||
isChangelogEntries,
|
||||
} from '../src/data';
|
||||
|
||||
describe('changelog data', () => {
|
||||
it('provides valid changelog entries', () => {
|
||||
expect(isChangelogEntries(changelogEntries)).toBe(true);
|
||||
expect(changelogEntries.length).toBeGreaterThan(0);
|
||||
expect(changelogEntries[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'build-metadata-changelog-poc',
|
||||
title: 'Build Metadata and Changelog Scaffold',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('sorts changelog entries newest first', () => {
|
||||
const sortedEntries = getChangelogEntriesNewestFirst([
|
||||
{
|
||||
changes: ['Older change'],
|
||||
createdAt: '2026-06-01',
|
||||
id: 'older-entry',
|
||||
summary: 'Older summary',
|
||||
title: 'Older Entry',
|
||||
version: '0.0.1',
|
||||
},
|
||||
{
|
||||
changes: ['Newer change'],
|
||||
createdAt: '2026-06-16',
|
||||
id: 'newer-entry',
|
||||
summary: 'Newer summary',
|
||||
title: 'Newer Entry',
|
||||
version: '0.0.2',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(sortedEntries.map(entry => entry.id)).toEqual([
|
||||
'newer-entry',
|
||||
'older-entry',
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects malformed changelog data', () => {
|
||||
expect(
|
||||
isChangelogEntries([
|
||||
{
|
||||
changes: 'not-an-array',
|
||||
createdAt: '2026-06-16',
|
||||
id: 'bad',
|
||||
summary: 'Bad entry',
|
||||
title: 'Bad',
|
||||
version: '0.0.1',
|
||||
},
|
||||
]),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ describe('routeDefinitions', () => {
|
||||
'newGame',
|
||||
'saves',
|
||||
'settings',
|
||||
'changelog',
|
||||
'policiesLegal',
|
||||
'policiesPrivacy',
|
||||
'policiesTerms',
|
||||
@@ -49,6 +50,7 @@ describe('routeDefinitions', () => {
|
||||
'saves',
|
||||
'settings',
|
||||
'policies',
|
||||
'changelog',
|
||||
'credits',
|
||||
'toolkit',
|
||||
]);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ScrollView, Text } from 'react-native';
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
||||
|
||||
import { AppNavigator } from '../src/navigation/AppNavigator';
|
||||
import { buildVersionLabel } from '../src/config/buildInfo';
|
||||
import {
|
||||
policyRoutes,
|
||||
primaryMenuRoutes,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
toolkitRoutes,
|
||||
} from '../src/navigation/routes';
|
||||
import { defaultVolumeSettings } from '../src/settings/gameSettings';
|
||||
import { ChangelogScreen } from '../src/screens/ChangelogScreen';
|
||||
import { GameplayScreen } from '../src/screens/GameplayScreen';
|
||||
import { HomeScreen } from '../src/screens/HomeScreen';
|
||||
import { PageScreen } from '../src/screens/PageScreen';
|
||||
@@ -168,8 +170,9 @@ describe('screen scaffold', () => {
|
||||
expect(text).toContain('Saves');
|
||||
expect(text).toContain('Settings');
|
||||
expect(text).toContain('Policies');
|
||||
expect(text).toContain('Change Log');
|
||||
expect(text).toContain('Credits');
|
||||
expect(text).toContain('Build v0.0.1');
|
||||
expect(text).toContain(buildVersionLabel);
|
||||
expect(text).not.toContain('Toolkit');
|
||||
expect(renderer.root.findByProps({ testID: 'home-build-version' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'home-background' })).toBeTruthy();
|
||||
@@ -178,6 +181,7 @@ describe('screen scaffold', () => {
|
||||
expect(renderer.root.findByProps({ testID: 'menu-saves-icon' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'menu-settings-icon' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'menu-policies-icon' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'menu-changelog-icon' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'menu-credits-icon' })).toBeTruthy();
|
||||
expect(renderer.root.findAllByProps({ testID: 'menu-toolkit' })).toHaveLength(0);
|
||||
});
|
||||
@@ -258,6 +262,17 @@ describe('screen scaffold', () => {
|
||||
expect(renderer.root.findByProps({ testID: 'back-home-button' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders changelog entries newest first', () => {
|
||||
const renderer = render(<ChangelogScreen goHome={jest.fn()} />);
|
||||
const text = getRenderedText(renderer);
|
||||
|
||||
expect(text).toContain('Build Metadata and Changelog Scaffold');
|
||||
expect(text).toContain('0.0.1-20260616-13aa686');
|
||||
expect(text).toContain('Seeded data-backed changelog content');
|
||||
expect(renderer.root.findByProps({ testID: 'changelog-screen' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'back-home-button' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders gameplay phases and moves through the turn scaffold', () => {
|
||||
act(() => {
|
||||
useGameplayStore.getState().startNewGame();
|
||||
@@ -501,7 +516,7 @@ describe('screen scaffold', () => {
|
||||
expect(text).toContain('Jacob Mathison');
|
||||
expect(text).toContain('React Native');
|
||||
expect(text).toContain('Font Library contributors');
|
||||
expect(text).toContain('Build v0.0.1');
|
||||
expect(text).toContain(buildVersionLabel);
|
||||
expect(text).toContain('Bundled fallback credits');
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-credits-card' })).toBeTruthy();
|
||||
expect(
|
||||
@@ -619,6 +634,7 @@ describe('screen scaffold', () => {
|
||||
it('does not use scroll containers for fixed landscape screens', () => {
|
||||
const screens = [
|
||||
<HomeScreen key="home" navigate={jest.fn()} />,
|
||||
<ChangelogScreen key="changelog" goHome={jest.fn()} />,
|
||||
<GameplayScreen key="gameplay" navigate={jest.fn()} />,
|
||||
<PageScreen key="page" goHome={jest.fn()} routeName="newGame" />,
|
||||
<PoliciesScreen key="policies" goHome={jest.fn()} navigate={jest.fn()} />,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const { readFileSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const toolkitPlaceholderFiles = [
|
||||
'audio.json',
|
||||
'credits.json',
|
||||
'endings.json',
|
||||
'factions.json',
|
||||
'flags.json',
|
||||
'lore.json',
|
||||
'map.json',
|
||||
'opening.json',
|
||||
'petitioners.json',
|
||||
'references.json',
|
||||
'resources.json',
|
||||
'rulings.json',
|
||||
'seed.json',
|
||||
'story.json',
|
||||
];
|
||||
|
||||
describe('toolkit data placeholders', () => {
|
||||
it('provides an empty array JSON file for every toolkit section', () => {
|
||||
for (const fileName of toolkitPlaceholderFiles) {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
'src',
|
||||
'data',
|
||||
'toolkit',
|
||||
fileName,
|
||||
);
|
||||
const content = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
|
||||
expect(content).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
**Name:** Bone Lord Bob — Throne of the Bone King
|
||||
**Repo:** `jacob-mathison/blb-throne-of-the-bone-king` (Gitea: `http://100.79.253.19:3000/jacob-mathison/blb-throne-of-the-bone-king`)
|
||||
**Type:** Cross-platform game shell — React Native UI shared across mobile, web, and desktop
|
||||
**Status:** Early development / scaffolding (menu shell, settings, navigation)
|
||||
**Status:** Early development / scaffolding (menu shell, settings, navigation, gameplay phase flow, saves, toolkit data placeholders)
|
||||
**Machine manifest:** `manifest.llm.json` — targets, task routes, commands, architecture index, known gaps
|
||||
|
||||
## Part of
|
||||
@@ -27,7 +27,7 @@ Primary day-to-day development currently runs through **web** (`npm run web`) an
|
||||
- **Zustand** ^5.0.14 — app state + persistence
|
||||
- **Vite** ^8 — web/Electron renderer bundler
|
||||
- **Electron** ^42 — desktop shell
|
||||
- **socket.io** / **socket.io-client** ^4.8 — declared; `socket/` not yet implemented
|
||||
- **socket.io** / **socket.io-client** ^4.8 — Toolkit socket server and client sync
|
||||
- **Jest** — unit/integration tests
|
||||
- **ESLint + Prettier** — lint/format
|
||||
- **Node** >= 22.12.0
|
||||
@@ -47,13 +47,21 @@ App.tsx
|
||||
**Navigation** is store-driven, not React Navigation:
|
||||
- Route catalog: `src/navigation/routes.ts` (`RouteName`, menu groupings)
|
||||
- Navigator: `src/navigation/AppNavigator.tsx` — reads/writes `useNavigationStore`, animates opacity on route change
|
||||
- Screens: `src/screens/` (`HomeScreen`, `SettingsScreen`, `PoliciesScreen`, `ToolkitScreen`, `PageScreen`)
|
||||
- Screens: `src/screens/` (`HomeScreen`, `GameplayScreen`, `SavesScreen`, `ChangelogScreen`, `SettingsScreen`, `PoliciesScreen`, `ToolkitScreen`, `PageScreen`)
|
||||
|
||||
**State** lives in Zustand stores under `src/state/`:
|
||||
- `navigationStore` — current/pending route (persisted as `blb-navigation`)
|
||||
- `gameSettingsStore` — resolution, volumes, autosave (persisted as `blb-game-settings`)
|
||||
- `gameplayStore` — active run, day counter, and current gameplay phase (persisted as `blb-gameplay`)
|
||||
- `saveGameStore` — local save slots for gameplay runs (persisted as `blb-save-games`)
|
||||
- `createPersistOptions` — shared persist middleware wiring
|
||||
|
||||
**Data content** lives under `src/data/`:
|
||||
- `changelog.json` + `changelog.ts` — app Change Log entries, sorted newest-first in the UI
|
||||
- `credits.json` + `credits.ts` — Toolkit Credits fallback/source content
|
||||
- `legal.json` — policy content
|
||||
- `toolkit/*.json` — empty array placeholders for future Toolkit resources until schemas stabilize
|
||||
|
||||
**Settings** constants/helpers: `src/settings/gameSettings.ts`
|
||||
Resolution presets include phone sizes plus a `browser` (unbounded) mode. Toolkit menu items are only shown when resolution is `browser`.
|
||||
|
||||
@@ -97,6 +105,7 @@ blb-throne-of-the-bone-king/
|
||||
│ └── preload.ts # Preload bridge
|
||||
├── src/
|
||||
│ ├── components/ # Reusable UI
|
||||
│ ├── data/ # Editable JSON content + typed exports
|
||||
│ ├── navigation/ # Routes + AppNavigator
|
||||
│ ├── screens/ # Screen-level views
|
||||
│ ├── settings/ # Game settings constants/helpers
|
||||
@@ -105,7 +114,7 @@ blb-throne-of-the-bone-king/
|
||||
├── __tests__/ # Jest test suite
|
||||
├── socket/ # Planned real-time layer (stub)
|
||||
├── docs/ # Project docs (stub)
|
||||
├── wiki/ # Wiki content (stub)
|
||||
├── wiki/ # Wiki/reference images and loose design assets
|
||||
├── vite.config.ts # Vite + react-native-web aliases
|
||||
├── tsconfig.electron.json # Electron main-process TS config
|
||||
├── metro.config.js # Metro bundler (mobile)
|
||||
@@ -163,9 +172,9 @@ npm run test # Jest
|
||||
|
||||
## Known Gaps
|
||||
Canonical list lives in `manifest.llm.json` → `knownGaps`. Current highlights:
|
||||
- `socket/` is a stub (socket.io deps declared, not wired)
|
||||
- `docs/` and `wiki/` are placeholder stubs
|
||||
- `AGENTS.md` and `README.md` are stale relative to web/Electron targets
|
||||
- `docs/` is still a placeholder stub
|
||||
- `wiki/` currently stores reference images and loose wiki/design assets
|
||||
- Toolkit placeholder JSON files are empty arrays until schemas are requested
|
||||
|
||||
## Notes for LLMs
|
||||
- **Read `manifest.llm.json` for task routing** — use `taskRoutes` to pick the narrowest file set
|
||||
@@ -175,6 +184,6 @@ Canonical list lives in `manifest.llm.json` → `knownGaps`. Current highlights:
|
||||
- **Metro** bundles mobile; **Vite** bundles web/Electron renderer — not interchangeable
|
||||
- **Navigation is Zustand-based** — do not add React Navigation unless explicitly requested
|
||||
- **Persistence is platform-split** — extend `persistStorage.ts` / `persistStorage.web.ts`, not stores directly
|
||||
- `socket/` is a stub — socket.io is installed but no server/client wiring yet
|
||||
- `src/data/AGENTS.md` contains extra rules for JSON content and Toolkit placeholders
|
||||
- iOS Pods and `node_modules/` are not committed; run `pod install` after pulling native dep changes
|
||||
- `dist/` build artifacts are gitignored
|
||||
|
||||
+54
-4
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"universe": "Bone Lord Bob (BLB) - Mathison Projects Inc.",
|
||||
"status": "early-development",
|
||||
"summary": "Menu shell with persisted route navigation, settings, policies, credits, background music, browser/Electron rendering, and a browser-only socket-gated Toolkit for volatile gameplay/content systems."
|
||||
"summary": "Menu shell with persisted route navigation, settings, policies, changelog, credits, gameplay phase scaffolding, local saves, build metadata display, background music, browser/Electron rendering, and a browser-only socket-gated Toolkit for volatile gameplay/content systems."
|
||||
},
|
||||
"principles": [
|
||||
"Start with llm.txt for human-readable orientation.",
|
||||
@@ -84,6 +84,11 @@
|
||||
"path": "socket/README.md",
|
||||
"type": "toolkit-socket-notes",
|
||||
"purpose": "Socket folder notes."
|
||||
},
|
||||
{
|
||||
"path": "src/data/AGENTS.md",
|
||||
"type": "data-instructions",
|
||||
"purpose": "Rules for editable JSON content and Toolkit placeholder resources."
|
||||
}
|
||||
],
|
||||
"targets": [
|
||||
@@ -149,7 +154,7 @@
|
||||
"Sass ^1.101.0",
|
||||
"Jest",
|
||||
"ESLint + Prettier",
|
||||
"Husky pre-commit validation",
|
||||
"Husky pre-commit build version bump and validation",
|
||||
"Node >= 22.12.0"
|
||||
],
|
||||
"scripts": {
|
||||
@@ -166,7 +171,8 @@
|
||||
"electron:package": "npm run electron:build && electron-builder",
|
||||
"lint": "eslint .",
|
||||
"test": "jest",
|
||||
"validate:commit": "npm run lint && npm run test -- --runInBand && npm run electron:build"
|
||||
"validate:commit": "npm run lint && npm run test -- --runInBand && npm run electron:build",
|
||||
"version:bump-build": "node tools/bump-build-version.mjs"
|
||||
},
|
||||
"architecture": {
|
||||
"rootComponent": "App.tsx",
|
||||
@@ -195,6 +201,16 @@
|
||||
"persistKey": "blb-game-settings",
|
||||
"purpose": "Resolution, autosave, mute, and audio volume settings."
|
||||
},
|
||||
{
|
||||
"file": "src/state/gameplayStore.ts",
|
||||
"persistKey": "blb-gameplay",
|
||||
"purpose": "Active gameplay run, day counter, phase state, and phase navigation."
|
||||
},
|
||||
{
|
||||
"file": "src/state/saveGameStore.ts",
|
||||
"persistKey": "blb-save-games",
|
||||
"purpose": "Local save slots for gameplay runs."
|
||||
},
|
||||
{
|
||||
"file": "src/state/toolkitSocketStore.ts",
|
||||
"purpose": "Toolkit socket connection status."
|
||||
@@ -229,6 +245,9 @@
|
||||
},
|
||||
"screens": [
|
||||
"src/screens/HomeScreen.tsx",
|
||||
"src/screens/GameplayScreen.tsx",
|
||||
"src/screens/SavesScreen.tsx",
|
||||
"src/screens/ChangelogScreen.tsx",
|
||||
"src/screens/SettingsScreen.tsx",
|
||||
"src/screens/PoliciesScreen.tsx",
|
||||
"src/screens/ToolkitScreen.tsx",
|
||||
@@ -248,12 +267,37 @@
|
||||
"favicon": "src/assets/images/favicon.svg"
|
||||
},
|
||||
"data": {
|
||||
"changelog": {
|
||||
"json": "src/data/changelog.json",
|
||||
"typedExport": "src/data/changelog.ts",
|
||||
"sortOrder": "newest-created-first"
|
||||
},
|
||||
"credits": {
|
||||
"json": "src/data/credits.json",
|
||||
"typedExport": "src/data/credits.ts"
|
||||
},
|
||||
"legal": {
|
||||
"json": "src/data/legal.json"
|
||||
},
|
||||
"toolkitPlaceholders": {
|
||||
"directory": "src/data/toolkit/",
|
||||
"shape": "empty arrays until schemas stabilize",
|
||||
"files": [
|
||||
"audio.json",
|
||||
"credits.json",
|
||||
"endings.json",
|
||||
"factions.json",
|
||||
"flags.json",
|
||||
"lore.json",
|
||||
"map.json",
|
||||
"opening.json",
|
||||
"petitioners.json",
|
||||
"references.json",
|
||||
"resources.json",
|
||||
"rulings.json",
|
||||
"seed.json",
|
||||
"story.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
@@ -399,6 +443,7 @@
|
||||
"read": [
|
||||
"llm.txt",
|
||||
"manifest.llm.json",
|
||||
"src/data/AGENTS.md",
|
||||
"src/data/",
|
||||
"socket/contentStore.cjs"
|
||||
],
|
||||
@@ -555,12 +600,17 @@
|
||||
{
|
||||
"command": "npm run validate:commit",
|
||||
"purpose": "Pre-commit safety check: lint, Jest, and Electron build."
|
||||
},
|
||||
{
|
||||
"command": "npm run version:bump-build",
|
||||
"purpose": "Increment the package patch version, update public .env build metadata, and stage package/env metadata before a commit."
|
||||
}
|
||||
],
|
||||
"knownGaps": [
|
||||
"docs/ and wiki/ are placeholder README stubs.",
|
||||
"docs/ is still a placeholder README stub.",
|
||||
"No JSON Schema validator exists yet for manifest.llm.json.",
|
||||
"Toolkit content panels are read-only scaffolds except for socket/store save plumbing.",
|
||||
"Toolkit placeholder JSON files are intentionally empty arrays until content schemas are requested.",
|
||||
"Native android/ and ios/ folders are not currently present in this checkout.",
|
||||
"No database exists yet; editable content is intentionally JSON-backed until schemas stabilize."
|
||||
],
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "BlbThroneOfTheBoneKing",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "BlbThroneOfTheBoneKing",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^3.1.1",
|
||||
"@react-native/new-app-screen": "0.85.3",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "BlbThroneOfTheBoneKing",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"main": "dist/electron/main.js",
|
||||
"scripts": {
|
||||
@@ -19,6 +19,7 @@
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"validate:commit": "npm run lint && npm run test -- --runInBand && npm run electron:build",
|
||||
"version:bump-build": "node tools/bump-build-version.mjs",
|
||||
"web": "vite --host 127.0.0.1",
|
||||
"web:build": "vite build",
|
||||
"web:preview": "vite preview --host 127.0.0.1"
|
||||
|
||||
@@ -71,10 +71,10 @@ const styles = StyleSheet.create({
|
||||
borderColor: '#4d4432',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
minHeight: 42,
|
||||
minHeight: 38,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 9,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 7,
|
||||
width: '100%',
|
||||
},
|
||||
buttonPressed: {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export const appVersion = '0.0.1';
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
export const buildVersionLabel = `Build v${appVersion}`;
|
||||
export const appVersion = packageJson.version;
|
||||
|
||||
export const buildIdentifier = appVersion;
|
||||
|
||||
export const buildVersionLabel = `Build ${buildIdentifier}`;
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
const viteAppVersion = import.meta.env.VITE_APP_VERSION;
|
||||
const viteLastBuildDate = import.meta.env.VITE_LAST_BUILD_DATE;
|
||||
const viteLastSuccessfulCommit = import.meta.env.VITE_LAST_SUCCESSFUL_COMMIT;
|
||||
|
||||
export const appVersion =
|
||||
typeof viteAppVersion === 'string' && viteAppVersion.length > 0
|
||||
? viteAppVersion
|
||||
: '0.0.1';
|
||||
: packageJson.version;
|
||||
|
||||
export const buildVersionLabel = `Build v${appVersion}`;
|
||||
export const buildDate =
|
||||
typeof viteLastBuildDate === 'string' && viteLastBuildDate.length > 0
|
||||
? viteLastBuildDate
|
||||
: null;
|
||||
|
||||
export const lastSuccessfulCommit =
|
||||
typeof viteLastSuccessfulCommit === 'string' &&
|
||||
viteLastSuccessfulCommit.length > 0
|
||||
? viteLastSuccessfulCommit
|
||||
: null;
|
||||
|
||||
export const buildIdentifier =
|
||||
buildDate == null || lastSuccessfulCommit == null
|
||||
? appVersion
|
||||
: `${appVersion}-${buildDate}-${lastSuccessfulCommit}`;
|
||||
|
||||
export const buildVersionLabel = `Build ${buildIdentifier}`;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# AGENTS.md - Data Folder
|
||||
|
||||
This folder contains source-of-truth JSON content used by the app and Toolkit.
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep JSON valid, stable, and reviewable.
|
||||
- Add TypeScript exports and runtime validation when app code imports JSON directly.
|
||||
- Keep Toolkit placeholder files in `toolkit/` as arrays until schemas stabilize.
|
||||
- Do not put secrets, credentials, local machine paths, or private notes in this folder.
|
||||
- Prefer additive schema changes. If a shape changes, update tests and any socket/content-store validation.
|
||||
|
||||
## Current Content
|
||||
|
||||
- `changelog.json`: app-facing changelog entries, newest records shown first in the Change Log screen.
|
||||
- `credits.json`: Toolkit Credits source content.
|
||||
- `legal.json`: policy/legal content.
|
||||
- `toolkit/*.json`: empty array placeholders for future Toolkit authoring resources.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": "build-metadata-changelog-poc",
|
||||
"version": "0.0.1-20260616-13aa686",
|
||||
"createdAt": "2026-06-16",
|
||||
"title": "Build Metadata and Changelog Scaffold",
|
||||
"summary": "Proof-of-concept changelog record for tracking deliverables alongside the visible build identifier.",
|
||||
"changes": [
|
||||
"Added pre-commit build metadata for version, build date, and last successful commit.",
|
||||
"Added a Change Log route to the main menu.",
|
||||
"Seeded data-backed changelog content for future release notes."
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
import changelogJson from './changelog.json';
|
||||
|
||||
export type ChangelogEntry = Readonly<{
|
||||
changes: string[];
|
||||
createdAt: string;
|
||||
id: string;
|
||||
summary: string;
|
||||
title: string;
|
||||
version: string;
|
||||
}>;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value != null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isChangelogEntry(value: unknown): value is ChangelogEntry {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.id === 'string' &&
|
||||
typeof value.version === 'string' &&
|
||||
typeof value.createdAt === 'string' &&
|
||||
typeof value.title === 'string' &&
|
||||
typeof value.summary === 'string' &&
|
||||
Array.isArray(value.changes) &&
|
||||
value.changes.every(change => typeof change === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
export function isChangelogEntries(value: unknown): value is ChangelogEntry[] {
|
||||
return Array.isArray(value) && value.every(isChangelogEntry);
|
||||
}
|
||||
|
||||
if (!isChangelogEntries(changelogJson)) {
|
||||
throw new Error('src/data/changelog.json does not match ChangelogEntry[].');
|
||||
}
|
||||
|
||||
export const changelogEntries: ChangelogEntry[] = changelogJson;
|
||||
|
||||
export function getChangelogEntriesNewestFirst(
|
||||
entries: readonly ChangelogEntry[] = changelogEntries,
|
||||
) {
|
||||
return [...entries].sort((firstEntry, secondEntry) =>
|
||||
secondEntry.createdAt.localeCompare(firstEntry.createdAt),
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
export {
|
||||
changelogEntries,
|
||||
getChangelogEntriesNewestFirst,
|
||||
isChangelogEntries,
|
||||
type ChangelogEntry,
|
||||
} from './changelog';
|
||||
export {
|
||||
fallbackCreditsContent,
|
||||
isCreditsContent,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Animated, StyleSheet } from 'react-native';
|
||||
|
||||
import { useToolkitSocketConnection } from '../hooks/useToolkitSocketConnection';
|
||||
import { ChangelogScreen } from '../screens/ChangelogScreen';
|
||||
import { GameplayScreen } from '../screens/GameplayScreen';
|
||||
import { HomeScreen } from '../screens/HomeScreen';
|
||||
import { PageScreen } from '../screens/PageScreen';
|
||||
@@ -112,6 +113,9 @@ function renderRoute(
|
||||
if (routeName === 'home') {
|
||||
return <HomeScreen navigate={navigate} />;
|
||||
}
|
||||
if (routeName === 'changelog') {
|
||||
return <ChangelogScreen goHome={goHome} />;
|
||||
}
|
||||
if (routeName === 'gameplay') {
|
||||
return <GameplayScreen navigate={navigate} />;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ export const routeDefinitions = {
|
||||
label: 'Policies',
|
||||
title: 'Policies',
|
||||
},
|
||||
changelog: {
|
||||
label: 'Change Log',
|
||||
title: 'Change Log',
|
||||
},
|
||||
policiesLegal: {
|
||||
label: 'Legal',
|
||||
title: 'Legal',
|
||||
@@ -112,6 +116,7 @@ export const primaryMenuRoutes = [
|
||||
'saves',
|
||||
'settings',
|
||||
'policies',
|
||||
'changelog',
|
||||
'credits',
|
||||
'toolkit',
|
||||
] as const satisfies readonly RouteName[];
|
||||
@@ -167,6 +172,7 @@ export const requiredPageRoutes = [
|
||||
'newGame',
|
||||
'saves',
|
||||
'settings',
|
||||
'changelog',
|
||||
'policiesLegal',
|
||||
'policiesPrivacy',
|
||||
'policiesTerms',
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { MenuButton } from '../components/MenuButton';
|
||||
import {
|
||||
getChangelogEntriesNewestFirst,
|
||||
type ChangelogEntry,
|
||||
} from '../data';
|
||||
import { defaultTextStyle, displayTextStyle } from '../styles/typography';
|
||||
|
||||
type ChangelogScreenProps = Readonly<{
|
||||
goHome: () => void;
|
||||
}>;
|
||||
|
||||
const visibleChangeLimit = 4;
|
||||
|
||||
export function ChangelogScreen({ goHome }: ChangelogScreenProps) {
|
||||
const safeAreaInsets = useSafeAreaInsets();
|
||||
const entries = getChangelogEntriesNewestFirst();
|
||||
const latestEntry = entries[0];
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
paddingBottom: safeAreaInsets.bottom + 18,
|
||||
paddingLeft: safeAreaInsets.left + 22,
|
||||
paddingRight: safeAreaInsets.right + 22,
|
||||
paddingTop: safeAreaInsets.top + 18,
|
||||
},
|
||||
]}
|
||||
testID="changelog-screen">
|
||||
<View style={styles.leftRail}>
|
||||
<Text style={styles.eyebrow}>Bone Lord Bob</Text>
|
||||
<Text style={styles.title}>Change Log</Text>
|
||||
<Text style={styles.description}>
|
||||
Newest release notes are pinned at the top, with older deliverables
|
||||
settling toward the bottom.
|
||||
</Text>
|
||||
{latestEntry == null ? null : (
|
||||
<View style={styles.latestPanel} testID="changelog-latest-panel">
|
||||
<Text style={styles.panelLabel}>Latest Build</Text>
|
||||
<Text style={styles.latestVersion}>{latestEntry.version}</Text>
|
||||
<Text style={styles.latestDate}>{latestEntry.createdAt}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.homeAction}>
|
||||
<MenuButton label="Home" onPress={goHome} testID="back-home-button" />
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.releaseBoard}>
|
||||
<View style={styles.boardHeader}>
|
||||
<View>
|
||||
<Text style={styles.boardKicker}>Release Ledger</Text>
|
||||
<Text style={styles.boardTitle}>Deliverable History</Text>
|
||||
</View>
|
||||
<Text style={styles.entryCount}>
|
||||
{entries.length} {entries.length === 1 ? 'record' : 'records'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.timeline} testID="changelog-entry-list">
|
||||
{entries.map((entry, index) => (
|
||||
<ChangelogCard
|
||||
entry={entry}
|
||||
index={index}
|
||||
key={entry.id}
|
||||
latest={index === 0}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
type ChangelogCardProps = Readonly<{
|
||||
entry: ChangelogEntry;
|
||||
index: number;
|
||||
latest: boolean;
|
||||
}>;
|
||||
|
||||
function ChangelogCard({ entry, index, latest }: ChangelogCardProps) {
|
||||
return (
|
||||
<View style={styles.timelineRow} testID={`changelog-entry-${entry.id}`}>
|
||||
<View style={styles.timelineMarkerColumn}>
|
||||
<View style={[styles.marker, latest ? styles.markerLatest : null]}>
|
||||
<Text style={styles.markerText}>{index + 1}</Text>
|
||||
</View>
|
||||
<View style={styles.markerLine} />
|
||||
</View>
|
||||
<View style={[styles.entryCard, latest ? styles.entryCardLatest : null]}>
|
||||
<View style={styles.entryHeader}>
|
||||
<View style={styles.versionPill}>
|
||||
<Text style={styles.version}>{entry.version}</Text>
|
||||
</View>
|
||||
<Text style={styles.date}>{entry.createdAt}</Text>
|
||||
</View>
|
||||
<Text style={styles.entryTitle}>{entry.title}</Text>
|
||||
<Text style={styles.summary}>{entry.summary}</Text>
|
||||
<View style={styles.changeList}>
|
||||
{entry.changes.slice(0, visibleChangeLimit).map(change => (
|
||||
<View key={change} style={styles.changeRow}>
|
||||
<Text style={styles.changeBullet}>◆</Text>
|
||||
<Text style={styles.changeItem}>{change}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
boardHeader: {
|
||||
alignItems: 'center',
|
||||
borderBottomColor: '#37342d',
|
||||
borderBottomWidth: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingBottom: 10,
|
||||
},
|
||||
boardKicker: {
|
||||
...defaultTextStyle,
|
||||
color: '#d6b25e',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
boardTitle: {
|
||||
...displayTextStyle,
|
||||
color: '#f6f1e7',
|
||||
fontSize: 18,
|
||||
letterSpacing: 0,
|
||||
lineHeight: 22,
|
||||
},
|
||||
changeBullet: {
|
||||
color: '#d6b25e',
|
||||
fontSize: 9,
|
||||
lineHeight: 15,
|
||||
},
|
||||
changeItem: {
|
||||
...defaultTextStyle,
|
||||
color: '#c9c1b3',
|
||||
flex: 1,
|
||||
fontSize: 11,
|
||||
lineHeight: 15,
|
||||
},
|
||||
changeList: {
|
||||
gap: 5,
|
||||
},
|
||||
changeRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 7,
|
||||
},
|
||||
container: {
|
||||
backgroundColor: '#111315',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
gap: 18,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
date: {
|
||||
...defaultTextStyle,
|
||||
color: '#80796c',
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0,
|
||||
},
|
||||
description: {
|
||||
...defaultTextStyle,
|
||||
color: '#c9c1b3',
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
entryCard: {
|
||||
backgroundColor: '#191b1f',
|
||||
borderColor: '#37342d',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
flex: 1,
|
||||
gap: 8,
|
||||
minWidth: 0,
|
||||
padding: 12,
|
||||
},
|
||||
entryCardLatest: {
|
||||
backgroundColor: '#1e1d1a',
|
||||
borderColor: '#d6b25e',
|
||||
},
|
||||
entryCount: {
|
||||
...defaultTextStyle,
|
||||
backgroundColor: '#24272b',
|
||||
borderColor: '#4d4432',
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
color: '#f5d98d',
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
entryHeader: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
entryTitle: {
|
||||
...displayTextStyle,
|
||||
color: '#f6f1e7',
|
||||
fontSize: 17,
|
||||
letterSpacing: 0,
|
||||
lineHeight: 21,
|
||||
},
|
||||
eyebrow: {
|
||||
...defaultTextStyle,
|
||||
color: '#d6b25e',
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
homeAction: {
|
||||
marginTop: 'auto',
|
||||
width: 164,
|
||||
},
|
||||
latestDate: {
|
||||
...defaultTextStyle,
|
||||
color: '#80796c',
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0,
|
||||
},
|
||||
latestPanel: {
|
||||
backgroundColor: '#191b1f',
|
||||
borderColor: '#4d4432',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
gap: 4,
|
||||
padding: 12,
|
||||
},
|
||||
latestVersion: {
|
||||
...defaultTextStyle,
|
||||
color: '#f5d98d',
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0,
|
||||
lineHeight: 15,
|
||||
},
|
||||
leftRail: {
|
||||
gap: 12,
|
||||
justifyContent: 'flex-start',
|
||||
width: 232,
|
||||
zIndex: 1,
|
||||
},
|
||||
marker: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#24272b',
|
||||
borderColor: '#4d4432',
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
height: 28,
|
||||
justifyContent: 'center',
|
||||
width: 28,
|
||||
},
|
||||
markerLatest: {
|
||||
backgroundColor: '#3a2918',
|
||||
borderColor: '#d6b25e',
|
||||
},
|
||||
markerLine: {
|
||||
backgroundColor: '#37342d',
|
||||
flex: 1,
|
||||
marginVertical: 5,
|
||||
width: 1,
|
||||
},
|
||||
markerText: {
|
||||
...defaultTextStyle,
|
||||
color: '#f5d98d',
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0,
|
||||
},
|
||||
panelLabel: {
|
||||
...defaultTextStyle,
|
||||
color: '#d6b25e',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
releaseBoard: {
|
||||
backgroundColor: '#15171a',
|
||||
borderColor: '#37342d',
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
flex: 1,
|
||||
gap: 12,
|
||||
padding: 14,
|
||||
zIndex: 1,
|
||||
},
|
||||
summary: {
|
||||
...defaultTextStyle,
|
||||
color: '#d6b25e',
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
},
|
||||
timeline: {
|
||||
flex: 1,
|
||||
gap: 10,
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
timelineMarkerColumn: {
|
||||
alignItems: 'center',
|
||||
width: 28,
|
||||
},
|
||||
timelineRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
height: 170,
|
||||
},
|
||||
title: {
|
||||
...displayTextStyle,
|
||||
color: '#f6f1e7',
|
||||
fontSize: 34,
|
||||
letterSpacing: 0,
|
||||
lineHeight: 40,
|
||||
},
|
||||
version: {
|
||||
...displayTextStyle,
|
||||
color: '#f5d98d',
|
||||
fontSize: 12,
|
||||
letterSpacing: 0,
|
||||
},
|
||||
versionPill: {
|
||||
backgroundColor: '#3a2918',
|
||||
borderColor: '#d6b25e',
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
});
|
||||
@@ -24,6 +24,7 @@ type HomeScreenProps = Readonly<{
|
||||
}>;
|
||||
|
||||
const menuIcons: Record<PrimaryMenuRouteName, string> = {
|
||||
changelog: '✎',
|
||||
credits: '★',
|
||||
newGame: '▶',
|
||||
policies: '§',
|
||||
@@ -155,7 +156,7 @@ const styles = StyleSheet.create({
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
menu: {
|
||||
gap: 8,
|
||||
gap: 6,
|
||||
width: 156,
|
||||
},
|
||||
menuBackgroundImage: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { defaultTextStyle } from '../styles/typography';
|
||||
type PageRouteName = Exclude<
|
||||
RouteName,
|
||||
| 'gameplay'
|
||||
| 'changelog'
|
||||
| 'home'
|
||||
| 'policies'
|
||||
| 'saves'
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const envPath = new URL('../.env', import.meta.url);
|
||||
const packagePath = new URL('../package.json', import.meta.url);
|
||||
const packageLockPath = new URL('../package-lock.json', import.meta.url);
|
||||
|
||||
function getNextPatchVersion(version) {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
||||
|
||||
if (match == null) {
|
||||
throw new Error(`Expected package version to use #.#.# format, got "${version}".`);
|
||||
}
|
||||
|
||||
const [, major, minor, patch] = match;
|
||||
|
||||
return `${major}.${minor}.${Number(patch) + 1}`;
|
||||
}
|
||||
|
||||
async function readJson(filePath) {
|
||||
return JSON.parse(await readFile(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
async function writeJson(filePath, json) {
|
||||
await writeFile(filePath, `${JSON.stringify(json, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function getLastSuccessfulCommit() {
|
||||
const { stdout } = await execFileAsync('git', ['rev-parse', '--short=7', 'HEAD']);
|
||||
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
function getBuildDate() {
|
||||
const now = new Date();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
|
||||
return `${now.getFullYear()}${month}${day}`;
|
||||
}
|
||||
|
||||
async function readEnvLines() {
|
||||
try {
|
||||
return (await readFile(envPath, 'utf8')).split(/\r?\n/);
|
||||
} catch (error) {
|
||||
if (error != null && typeof error === 'object' && 'code' in error) {
|
||||
const nodeError = error;
|
||||
|
||||
if (nodeError.code === 'ENOENT') {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeEnvValues(values) {
|
||||
const lines = await readEnvLines();
|
||||
const remainingKeys = new Set(Object.keys(values));
|
||||
const nextLines = lines
|
||||
.filter((line, index) => line.length > 0 || index < lines.length - 1)
|
||||
.map(line => {
|
||||
const [key] = line.split('=', 1);
|
||||
|
||||
if (!remainingKeys.has(key)) {
|
||||
return line;
|
||||
}
|
||||
|
||||
remainingKeys.delete(key);
|
||||
|
||||
return `${key}=${values[key]}`;
|
||||
});
|
||||
|
||||
for (const key of remainingKeys) {
|
||||
nextLines.push(`${key}=${values[key]}`);
|
||||
}
|
||||
|
||||
await writeFile(envPath, `${nextLines.join('\n')}\n`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const packageJson = await readJson(packagePath);
|
||||
const nextVersion = getNextPatchVersion(packageJson.version);
|
||||
const buildDate = getBuildDate();
|
||||
const lastSuccessfulCommit = await getLastSuccessfulCommit();
|
||||
|
||||
packageJson.version = nextVersion;
|
||||
await writeJson(packagePath, packageJson);
|
||||
|
||||
const packageLockJson = await readJson(packageLockPath);
|
||||
packageLockJson.version = nextVersion;
|
||||
|
||||
if (
|
||||
packageLockJson.packages != null &&
|
||||
packageLockJson.packages[''] != null
|
||||
) {
|
||||
packageLockJson.packages[''].version = nextVersion;
|
||||
}
|
||||
|
||||
await writeJson(packageLockPath, packageLockJson);
|
||||
await writeEnvValues({
|
||||
VITE_LAST_BUILD_DATE: buildDate,
|
||||
VITE_LAST_SUCCESSFUL_COMMIT: lastSuccessfulCommit,
|
||||
});
|
||||
await execFileAsync('git', ['add', '.env', 'package.json', 'package-lock.json']);
|
||||
|
||||
console.log(
|
||||
`Updated build version to ${nextVersion}-${buildDate}-${lastSuccessfulCommit}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
Before Width: | Height: | Size: 5.1 MiB After Width: | Height: | Size: 5.1 MiB |
Reference in New Issue
Block a user