Skip to content

Commit ce8973a

Browse files
committed
Add build script and GitHub Actions workflow for automated releases
1 parent 27c4e33 commit ce8973a

5 files changed

Lines changed: 752 additions & 5 deletions

File tree

.github/workflows/release.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Build and Release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
build-and-release:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
cache: 'npm'
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Build project
26+
run: npm run build
27+
28+
- name: Create tarball
29+
run: |
30+
tar -czf linear-algebra-playground.tar.gz dist/
31+
32+
- name: Create or update release
33+
uses: ncipollo/release-action@v1
34+
with:
35+
tag: latest
36+
name: Latest Release
37+
body: |
38+
Latest build from main branch.
39+
40+
## Installation
41+
42+
```bash
43+
wget https://github.com/${{ github.repository }}/releases/download/latest/linear-algebra-playground.tar.gz
44+
tar -xzf linear-algebra-playground.tar.gz
45+
cd dist && npm start
46+
```
47+
artifacts: linear-algebra-playground.tar.gz
48+
draft: false
49+
prerelease: false
50+
makeLatest: true
51+
allowUpdates: true

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
logs
3-
.claude
3+
.claude
4+
dist

build.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
const esbuild = require('esbuild');
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
const DIST_DIR = path.join(__dirname, 'dist');
6+
const DIST_CLIENT_DIR = path.join(DIST_DIR, 'client');
7+
8+
// Ensure dist directories exist
9+
function ensureDirs() {
10+
[DIST_DIR, DIST_CLIENT_DIR].forEach(dir => {
11+
if (!fs.existsSync(dir)) {
12+
fs.mkdirSync(dir, { recursive: true });
13+
}
14+
});
15+
}
16+
17+
// JavaScript files in load order (from index.html)
18+
const jsFiles = [
19+
'client/help-modal.js',
20+
'client/logger.js',
21+
'client/core/status.js',
22+
'client/core/config.js',
23+
'client/core/operation-schemas.js',
24+
'client/core/help.js',
25+
'client/core/mode-manager.js',
26+
'client/core/color-utils.js',
27+
'client/core/theme-service.js',
28+
'client/core/results-panel.js',
29+
'client/core/format-utils.js',
30+
'client/core/vector.js',
31+
'client/core/animator.js',
32+
'client/core/coordinate-system.js',
33+
'client/app.js',
34+
'client/entities/matrix.js',
35+
'client/modes/vector-sidebar.js',
36+
'client/modes/vector-canvas.js',
37+
'client/modes/vector-operations.js',
38+
'client/modes/vector-mode.js',
39+
'client/modes/matrix-operations.js',
40+
'client/modes/matrix-mode.js',
41+
'client/linear-algebra.js'
42+
];
43+
44+
// CSS files
45+
const cssFiles = [
46+
'client/bespoke.css',
47+
'client/layout.css',
48+
'client/vector-mode.css',
49+
'client/matrix-mode.css'
50+
];
51+
52+
// Minify content using esbuild transform API (no temp files needed)
53+
async function minify(content, loader = 'js') {
54+
const result = await esbuild.transform(content, {
55+
loader,
56+
minify: true,
57+
target: 'es2020',
58+
});
59+
return result.code;
60+
}
61+
62+
// Bundle and minify files
63+
async function bundleFiles(files, outputPath, loader = 'js') {
64+
const content = files
65+
.map(file => fs.readFileSync(path.join(__dirname, file), 'utf8'))
66+
.join('\n\n');
67+
68+
const minified = await minify(content, loader);
69+
fs.writeFileSync(outputPath, minified);
70+
}
71+
72+
// Copy directory recursively (simplified)
73+
function copyDir(src, dest) {
74+
if (!fs.existsSync(src)) return false;
75+
76+
if (!fs.existsSync(dest)) {
77+
fs.mkdirSync(dest, { recursive: true });
78+
}
79+
80+
const entries = fs.readdirSync(src, { withFileTypes: true });
81+
for (const entry of entries) {
82+
const srcPath = path.join(src, entry.name);
83+
const destPath = path.join(dest, entry.name);
84+
85+
if (entry.isDirectory()) {
86+
copyDir(srcPath, destPath);
87+
} else {
88+
fs.copyFileSync(srcPath, destPath);
89+
}
90+
}
91+
return true;
92+
}
93+
94+
// Create production HTML
95+
function createProductionHtml() {
96+
let html = fs.readFileSync(path.join(__dirname, 'client', 'index.html'), 'utf8');
97+
98+
// Remove all script tags
99+
html = html.replace(/<script[^>]*src="[^"]+"[^>]*><\/script>\s*/g, '');
100+
101+
// Remove all CSS link tags
102+
html = html.replace(/<link rel="stylesheet" href="[^"]+\.css"[^>]*\/>\s*/g, '');
103+
104+
// Insert bundled CSS before </head>
105+
html = html.replace('</head>', ' <link rel="stylesheet" href="./styles.bundle.css" />\n</head>');
106+
107+
// Insert bundled script before </body>
108+
html = html.replace('</body>', ' <script src="./app.bundle.js"></script>\n</body>');
109+
110+
fs.writeFileSync(path.join(DIST_CLIENT_DIR, 'index.html'), html);
111+
}
112+
113+
async function build() {
114+
console.log('Building production bundle...');
115+
116+
try {
117+
ensureDirs();
118+
119+
// Bundle JavaScript
120+
console.log('Bundling JavaScript files...');
121+
await bundleFiles(jsFiles, path.join(DIST_CLIENT_DIR, 'app.bundle.js'), 'js');
122+
console.log('✓ JavaScript bundled');
123+
124+
// Bundle CSS
125+
console.log('Bundling CSS files...');
126+
await bundleFiles(cssFiles, path.join(DIST_CLIENT_DIR, 'styles.bundle.css'), 'css');
127+
console.log('✓ CSS bundled');
128+
129+
// Minify server.js
130+
console.log('Minifying server.js...');
131+
const serverContent = fs.readFileSync(path.join(__dirname, 'server.js'), 'utf8');
132+
const minifiedServer = await minify(serverContent, 'js');
133+
fs.writeFileSync(path.join(DIST_DIR, 'server.js'), minifiedServer);
134+
console.log('✓ server.js minified');
135+
136+
// Copy static files
137+
console.log('Copying static files...');
138+
fs.writeFileSync(
139+
path.join(DIST_DIR, 'package.json'),
140+
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')
141+
);
142+
fs.writeFileSync(
143+
path.join(DIST_CLIENT_DIR, 'config.json'),
144+
fs.readFileSync(path.join(__dirname, 'client', 'config.json'), 'utf8')
145+
);
146+
console.log('✓ Static files copied');
147+
148+
// Copy ws dependency
149+
console.log('Copying ws dependency...');
150+
const wsCopied = copyDir(
151+
path.join(__dirname, 'node_modules', 'ws'),
152+
path.join(DIST_DIR, 'node_modules', 'ws')
153+
);
154+
console.log(wsCopied ? '✓ ws dependency copied' : '⚠ ws dependency not found, skipping...');
155+
156+
// Create production HTML
157+
console.log('Creating production index.html...');
158+
createProductionHtml();
159+
console.log('✓ Production index.html created');
160+
161+
console.log('\n✓ Build complete! Output in dist/');
162+
} catch (error) {
163+
console.error('Build failed:', error);
164+
process.exit(1);
165+
}
166+
}
167+
168+
build();

0 commit comments

Comments
 (0)