student life social platform
1import { execa } from 'execa';
2import { readFile, writeFile } from 'node:fs/promises';
3import path from 'node:path';
4
5const GENERATED_START_MARKER = '// @generated buildinfo';
6const GENERATED_END_MARKER = '// end generated buildinfo';
7
8const stub = process.argv.length >= 3 && process.argv[2] === '--stub';
9
10type Package = 'api' | 'app' | 'sync' | 'db';
11
12/** files to inject in */
13let targets: Array<[string, Package]> = [
14 ['packages/app/src/lib/buildinfo.ts', 'app'],
15 ['packages/api/src/lib/buildinfo.ts', 'api'],
16 ['packages/app/svelte.config.js', 'app'],
17];
18
19// TODO use command line arguments (this) to not inject in e.g. API package from app package's script
20if (stub && process.argv.length >= 4)
21 targets = process.argv.slice(3).map((arg) => arg.split(':') as [string, Package]);
22if (!stub && process.argv.length >= 3)
23 targets = process.argv.slice(2).map((arg) => arg.split(':') as [string, Package]);
24
25async function git(args: string): Promise<string> {
26 const additionalConfig = {
27 // see https://stackoverflow.com/a/22634649/9943464
28 'versionsort.suffix': '-alpha -beta -pre -rc',
29 };
30 const { stdout } = await execa('git', [
31 '-c',
32 ...Object.entries(additionalConfig).map(([k, v]) => `${k}='${v}'`),
33 ...args.split(' '),
34 ]);
35 return stdout;
36}
37
38const hash = stub ? 'dev' : await git('rev-parse HEAD').then((hash) => hash.trim());
39const toplevel = await git('rev-parse --show-toplevel');
40const variables = {
41 CURRENT_COMMIT: hash,
42 CURRENT_VERSIONS: {
43 api: process.env.TAG || 'dev',
44 app: process.env.TAG || 'dev',
45 sync: process.env.TAG || 'dev',
46 db: process.env.TAG || 'dev',
47 },
48};
49
50console.info(`Build info: ${JSON.stringify(variables)}`);
51console.info(`Injecting into ${targets.join(', ')}`);
52
53// Inject in graphinx config
54const graphinxConfigPath = path.join(toplevel.trim(), 'packages/api/.graphinx.yaml');
55
56await writeFile(
57 graphinxConfigPath,
58 (await readFile(graphinxConfigPath, 'utf-8'))
59 .replace(
60 /^(\s*)CURRENT_COMMIT: .+$/m,
61 `$1CURRENT_COMMIT: ${JSON.stringify(variables.CURRENT_COMMIT)}`,
62 )
63 .replace(
64 /^(\s*)CURRENT_COMMIT_SHORT: .+$/m,
65 `$1CURRENT_COMMIT_SHORT: ${JSON.stringify(variables.CURRENT_COMMIT.slice(0, 7))}`,
66 )
67 .replace(
68 /^(\s*)CURRENT_VERSION: .+$/m,
69 `$1CURRENT_VERSION: ${JSON.stringify(variables.CURRENT_VERSIONS.api)}`,
70 ),
71);
72
73function singlequotes(literal: string): string {
74 if ((literal.match(/"/g) || []).length !== 2) return literal;
75 return `'${literal.replace(/"/g, '')}'`;
76}
77
78function constDeclaration(
79 name: string,
80 value: unknown,
81 { typescript = true, exported = true } = {},
82) {
83 return `${exported ? 'export ' : ''}const ${name} = ${singlequotes(JSON.stringify(value))}${typescript ? ' as string' : ''};`;
84}
85
86function replaceBetweenLines(start: string, end: string, replacement: string, contents: string) {
87 const lines = contents.split('\n');
88 const startIndex = lines.findIndex((line) => line.trim().includes(start.trim()));
89 const endIndex = lines.findIndex((line) => line.trim().includes(end.trim()));
90 return [...lines.slice(0, startIndex + 1), replacement, ...lines.slice(endIndex)].join('\n');
91}
92
93await Promise.all(
94 targets.map(async ([relativePath, pkg]) => {
95 const filepath = path.join(toplevel.trim(), relativePath);
96 const typescript = path.extname(filepath) === '.ts';
97 const oldContents = await readFile(filepath, 'utf-8').catch(() => '');
98
99 /** isolated means the file contains nothing other than these two variables */
100 const isolated =
101 !oldContents.includes(GENERATED_START_MARKER) && !oldContents.includes(GENERATED_END_MARKER);
102
103 const declarations = [
104 constDeclaration('CURRENT_COMMIT', variables.CURRENT_COMMIT, {
105 typescript,
106 exported: isolated,
107 }),
108 constDeclaration('CURRENT_VERSION', variables.CURRENT_VERSIONS[pkg], {
109 typescript,
110 exported: isolated,
111 }),
112 ];
113 await writeFile(
114 filepath,
115 isolated
116 ? declarations.join('\n') + '\n'
117 : replaceBetweenLines(
118 GENERATED_START_MARKER,
119 GENERATED_END_MARKER,
120 declarations.join('\n'),
121 oldContents,
122 ),
123 );
124 }),
125);