Skip to content

Commit 432d553

Browse files
committed
refactor(resources): improve embedded resources handling and caching
- Move from TS stub to JSON format for embedded resources metadata - Add better metadata handling with fallback defaults - Support cache-only and json-only generation modes - Include generatedAt timestamp in metadata
1 parent 20246ff commit 432d553

File tree

6 files changed

+241
-64
lines changed

6 files changed

+241
-64
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ package-lock.json
2525

2626
# MkDocs build output
2727
site/
28+
src/shared/resources/embedded-resources.json

scripts/build-binaries.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ console.log(`\n${bold}${cyan}╭────────────────
4343
console.log(`${bold}${cyan}${reset} Building ${bold}CodeMachine${reset} v${mainVersion} ${bold}${cyan}${reset}`);
4444
console.log(`${bold}${cyan}╰────────────────────────────────────────╯${reset}\n`);
4545

46-
await generateEmbeddedResources({ quiet: true, writeStub: false });
46+
await generateEmbeddedResources({ quiet: true, writeJson: true, writeCache: true });
4747
console.log(`${green}${reset} ${dim}Embedded resources refreshed${reset}`);
4848

4949
// Auto-sync platform package versions before building

scripts/generate-embedded-resources.mjs

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bun
22
import { mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from 'node:fs';
3-
import { join, dirname, relative } from 'node:path';
3+
import { join, dirname } from 'node:path';
44
import { fileURLToPath } from 'node:url';
55
import { brotliCompressSync } from 'node:zlib';
66
import { createHash } from 'node:crypto';
@@ -9,7 +9,7 @@ import { homedir, tmpdir } from 'node:os';
99
const __dirname = dirname(fileURLToPath(import.meta.url));
1010
const repoRoot = join(__dirname, '..');
1111
const resourcesDir = join(repoRoot, 'src', 'shared', 'resources');
12-
const targetPath = join(resourcesDir, 'embedded-resources.ts');
12+
const targetJsonPath = join(resourcesDir, 'embedded-resources.json');
1313
const packageJsonPath = join(repoRoot, 'package.json');
1414
const cacheDirName = 'embedded-resources';
1515

@@ -51,7 +51,15 @@ function getCacheBase() {
5151

5252
export async function generateEmbeddedResources(options = {}) {
5353
const include = options.include ?? DEFAULT_INCLUDE;
54-
const shouldWriteStub = options.writeStub ?? true;
54+
let shouldWriteJson = options.writeJson;
55+
if (shouldWriteJson === undefined && options.writeStub === true) {
56+
// Backwards compatibility for callers that previously passed writeStub
57+
shouldWriteJson = true;
58+
}
59+
if (typeof shouldWriteJson !== 'boolean') {
60+
shouldWriteJson = true;
61+
}
62+
const shouldWriteCache = options.writeCache ?? true;
5563
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
5664
const files = [];
5765

@@ -84,20 +92,13 @@ export async function generateEmbeddedResources(options = {}) {
8492
const base64 = compressed.toString('base64');
8593
const hash = createHash('sha256').update(compressed).digest('hex');
8694

87-
const fileContents = `// AUTO-GENERATED FILE. DO NOT EDIT.
88-
// Run scripts/generate-embedded-resources.mjs to regenerate.
89-
import { Buffer } from 'node:buffer';
90-
91-
export const EMBEDDED_RESOURCES_VERSION = '${packageJson.version}';
92-
export const EMBEDDED_RESOURCES_HASH = '${hash}';
93-
export const EMBEDDED_RESOURCES_SIZE = ${compressed.length};
94-
95-
const EMBEDDED_BASE64 = ${JSON.stringify(base64)};
96-
97-
export function getEmbeddedResourceArchive(): Buffer {
98-
return Buffer.from(EMBEDDED_BASE64, 'base64');
99-
}
100-
`;
95+
const archiveRecord = {
96+
version: packageJson.version,
97+
generatedAt: payload.generatedAt,
98+
hash,
99+
size: compressed.length,
100+
base64,
101+
};
101102

102103
const writeCache = () => {
103104
let cacheBase = getCacheBase();
@@ -106,12 +107,7 @@ export function getEmbeddedResourceArchive(): Buffer {
106107

107108
try {
108109
mkdirSync(cacheDir, { recursive: true });
109-
writeFileSync(cacheFile, JSON.stringify({
110-
version: packageJson.version,
111-
hash,
112-
size: compressed.length,
113-
base64,
114-
}, null, 2));
110+
writeFileSync(cacheFile, JSON.stringify(archiveRecord, null, 2));
115111
return { cacheFile };
116112
} catch (error) {
117113
// Fall back to a temp cache if the primary location is not writable
@@ -120,42 +116,48 @@ export function getEmbeddedResourceArchive(): Buffer {
120116
cacheDir = join(cacheBase, cacheDirName);
121117
cacheFile = join(cacheDir, 'embedded-resources.json');
122118
mkdirSync(cacheDir, { recursive: true });
123-
writeFileSync(cacheFile, JSON.stringify({
124-
version: packageJson.version,
125-
hash,
126-
size: compressed.length,
127-
base64,
128-
}, null, 2));
119+
writeFileSync(cacheFile, JSON.stringify(archiveRecord, null, 2));
129120
return { cacheFile };
130121
}
131122
throw error;
132123
}
133124
};
134125

135-
const { cacheFile } = writeCache();
126+
const cacheResult = shouldWriteCache ? writeCache() : {};
136127

137-
if (shouldWriteStub) {
128+
if (shouldWriteJson) {
138129
mkdirSync(resourcesDir, { recursive: true });
139-
writeFileSync(targetPath, fileContents);
130+
writeFileSync(targetJsonPath, JSON.stringify(archiveRecord, null, 2));
140131
}
141132

142133
if (!options.quiet) {
143134
console.log(`[embed] Embedded ${files.length} files (${payloadBuffer.length} bytes, compressed to ${compressed.length} bytes)`);
144135
console.log(`[embed] Hash: ${hash}`);
145-
console.log(`[embed] Cache file written to ${cacheFile}`);
146-
if (!shouldWriteStub) {
147-
console.log('[embed] Skipped updating src/shared/resources/embedded-resources.ts (cache-only)');
136+
if (cacheResult.cacheFile) {
137+
console.log(`[embed] Cache file written to ${cacheResult.cacheFile}`);
138+
}
139+
if (shouldWriteJson) {
140+
console.log(`[embed] JSON archive written to ${targetJsonPath}`);
141+
} else {
142+
console.log('[embed] Skipped writing src/shared/resources/embedded-resources.json');
148143
}
149144
}
150145

151-
return { hash, size: compressed.length, fileCount: files.length, cacheFile };
146+
return {
147+
hash,
148+
size: compressed.length,
149+
fileCount: files.length,
150+
cacheFile: cacheResult.cacheFile,
151+
jsonFile: shouldWriteJson ? targetJsonPath : undefined,
152+
};
152153
}
153154

154155
if (import.meta.main) {
155156
const args = new Set(process.argv.slice(2));
156157
const cacheOnly = args.has('--cache-only');
157-
const writeStub = args.has('--write-stub') ? true : !cacheOnly;
158-
generateEmbeddedResources({ writeStub }).catch((error) => {
158+
const writeJson = args.has('--no-json') ? false : args.has('--write-json') ? true : !cacheOnly;
159+
const writeCache = args.has('--no-cache') ? false : true;
160+
generateEmbeddedResources({ writeJson, writeCache }).catch((error) => {
159161
console.error('[embed] Failed to generate embedded resources:', error);
160162
process.exitCode = 1;
161163
});

src/shared/resources/embedded-loader.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ import { dirname, join } from 'node:path';
44
import { homedir, tmpdir } from 'node:os';
55
import { brotliDecompressSync } from 'node:zlib';
66

7-
import {
8-
EMBEDDED_RESOURCES_HASH,
9-
EMBEDDED_RESOURCES_SIZE,
10-
EMBEDDED_RESOURCES_VERSION,
11-
getEmbeddedResourceArchive
12-
} from './embedded-resources.js';
7+
import { getEmbeddedResourceArchive, getEmbeddedResourceMetadata } from './embedded-resources.js';
138

149
type EmbeddedFile = {
1510
path: string;
@@ -27,15 +22,32 @@ type ArchiveMetadata = {
2722
version: string;
2823
hash: string;
2924
size: number;
25+
generatedAt?: string;
3026
};
3127

32-
const DEFAULT_METADATA: ArchiveMetadata = {
33-
version: EMBEDDED_RESOURCES_VERSION,
34-
hash: EMBEDDED_RESOURCES_HASH,
35-
size: EMBEDDED_RESOURCES_SIZE,
28+
const FALLBACK_METADATA: ArchiveMetadata = {
29+
version: 'unknown',
30+
hash: 'unknown',
31+
size: 0,
3632
};
3733

38-
let activeMetadata: ArchiveMetadata = { ...DEFAULT_METADATA };
34+
let defaultMetadataCache: ArchiveMetadata | null = null;
35+
36+
function getDefaultMetadata(): ArchiveMetadata {
37+
if (defaultMetadataCache) {
38+
return defaultMetadataCache;
39+
}
40+
41+
try {
42+
defaultMetadataCache = getEmbeddedResourceMetadata();
43+
} catch {
44+
defaultMetadataCache = FALLBACK_METADATA;
45+
}
46+
47+
return defaultMetadataCache;
48+
}
49+
50+
let activeMetadata: ArchiveMetadata = { ...getDefaultMetadata() };
3951
let cachedPayload: EmbeddedPayload | null = null;
4052
let cachedArchiveBuffer: Buffer | null = null;
4153

@@ -59,16 +71,20 @@ function tryLoadArchiveFromCache(): Buffer | null {
5971
hash?: string;
6072
size?: number;
6173
base64?: string;
74+
generatedAt?: string;
6275
};
6376
if (!data?.base64) {
6477
return null;
6578
}
79+
const buffer = Buffer.from(data.base64, 'base64');
80+
const defaults = getDefaultMetadata();
6681
activeMetadata = {
67-
version: data.version ?? DEFAULT_METADATA.version,
68-
hash: data.hash ?? DEFAULT_METADATA.hash,
69-
size: data.size ?? DEFAULT_METADATA.size,
82+
version: data.version ?? defaults.version,
83+
hash: data.hash ?? defaults.hash,
84+
size: data.size ?? buffer.length,
85+
generatedAt: data.generatedAt ?? defaults.generatedAt,
7086
};
71-
return Buffer.from(data.base64, 'base64');
87+
return buffer;
7288
} catch {
7389
return null;
7490
}
@@ -90,6 +106,12 @@ function getArchiveBuffer(): Buffer {
90106
throw new Error('Embedded resource archive is empty');
91107
}
92108

109+
// Ensure metadata reflects the currently loaded archive
110+
try {
111+
activeMetadata = getEmbeddedResourceMetadata();
112+
} catch {
113+
activeMetadata = { ...getDefaultMetadata(), size: fallback.length };
114+
}
93115
cachedArchiveBuffer = fallback;
94116
return cachedArchiveBuffer;
95117
}
@@ -102,6 +124,12 @@ function decodeEmbeddedPayload(): EmbeddedPayload {
102124
const archive = getArchiveBuffer();
103125
const decompressed = brotliDecompressSync(archive);
104126
cachedPayload = JSON.parse(decompressed.toString('utf8')) as EmbeddedPayload;
127+
if (cachedPayload.version && cachedPayload.version !== activeMetadata.version) {
128+
activeMetadata = { ...activeMetadata, version: cachedPayload.version };
129+
}
130+
if (cachedPayload.generatedAt && !activeMetadata.generatedAt) {
131+
activeMetadata = { ...activeMetadata, generatedAt: cachedPayload.generatedAt };
132+
}
105133
return cachedPayload;
106134
}
107135

src/shared/resources/embedded-resources.ts

Lines changed: 155 additions & 7 deletions
Large diffs are not rendered by default.

tests/e2e/start-cli.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { spawn, type ChildProcess } from 'node:child_process';
2-
import { existsSync } from 'node:fs';
32
import { mkdtemp, readFile, rm } from 'node:fs/promises';
43
import path from 'node:path';
54
import { fileURLToPath } from 'node:url';
65
import os from 'node:os';
7-
import { afterEach, beforeAll, describe, expect, it } from 'bun:test';
6+
import { afterEach, describe, expect, it } from 'bun:test';
87

98
const __filename = fileURLToPath(import.meta.url);
109
const __dirname = path.dirname(__filename);
1110
const projectRoot = path.resolve(__dirname, '..', '..');
12-
const distEntry = path.join(projectRoot, 'dist', 'index.js');
1311
const fixturesRoot = path.join(projectRoot, 'tests', 'fixtures');
1412
const engineFixturesDir = path.join(fixturesRoot, 'codex');
1513
const activeProcesses = new Set<ChildProcess>();

0 commit comments

Comments
 (0)