Skip to content

Commit 41b45fb

Browse files
committed
Update PWA icons and manifest
1 parent a8ef717 commit 41b45fb

9 files changed

Lines changed: 199 additions & 6 deletions

File tree

.github/workflows/release.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Manual Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tag_name:
7+
description: 'Tag name for the release (e.g., v1.0.0)'
8+
required: true
9+
default: 'v1.0.0'
10+
release_body:
11+
description: 'Release description'
12+
required: true
13+
default: 'Manual release via GitHub Actions'
14+
15+
jobs:
16+
build-and-release:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
22+
- name: Setup Node.js
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: '20'
26+
27+
- name: Install dependencies
28+
run: npm install
29+
30+
- name: Build project
31+
run: bash bin/build.sh
32+
33+
- name: Create Single-File HTML
34+
run: node bin/bundle_single_html.js
35+
36+
- name: Create ZIP archive
37+
run: |
38+
cd dist
39+
zip -r ../document-editor.zip .
40+
cd ..
41+
42+
- name: Create Release
43+
id: create_release
44+
uses: softprops/action-gh-release@v2
45+
with:
46+
tag_name: ${{ github.event.inputs.tag_name }}
47+
name: Release ${{ github.event.inputs.tag_name }}
48+
body: ${{ github.event.inputs.release_body }}
49+
files: |
50+
document-editor.zip
51+
dist/single-file.html
52+
draft: false
53+
prerelease: false
54+
env:
55+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

bin/bundle_single_html.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = path.dirname(__filename);
7+
const DIST_DIR = path.resolve(__dirname, '../dist');
8+
const OUTPUT_FILE = path.resolve(DIST_DIR, 'single-file.html');
9+
10+
function getBase64(file) {
11+
const bitmap = fs.readFileSync(file);
12+
return Buffer.from(bitmap).toString('base64');
13+
}
14+
15+
function getMimeType(filePath) {
16+
const ext = path.extname(filePath).toLowerCase();
17+
const mimes = {
18+
'.js': 'application/javascript',
19+
'.css': 'text/css',
20+
'.wasm': 'application/wasm',
21+
'.png': 'image/png',
22+
'.jpg': 'image/jpeg',
23+
'.gif': 'image/gif',
24+
'.woff': 'font/woff',
25+
'.woff2': 'font/woff2',
26+
'.ttf': 'font/ttf',
27+
'.html': 'text/html',
28+
};
29+
return mimes[ext] || 'application/octet-stream';
30+
}
31+
32+
function walkDir(dir, callback) {
33+
fs.readdirSync(dir).forEach((f) => {
34+
const dirPath = path.join(dir, f);
35+
const isDirectory = fs.statSync(dirPath).isDirectory();
36+
isDirectory ? walkDir(dirPath, callback) : callback(path.join(dir, f));
37+
});
38+
}
39+
40+
function bundle() {
41+
console.log('Starting Single-File HTML bundling...');
42+
let html = fs.readFileSync(path.join(DIST_DIR, 'index.html'), 'utf8');
43+
44+
// 1. Collect all files into VFS
45+
const vfs = {};
46+
walkDir(DIST_DIR, (filePath) => {
47+
const relativePath = path.relative(DIST_DIR, filePath);
48+
if (relativePath === 'index.html' || relativePath === 'single-file.html' || relativePath === 'sw.js') return;
49+
50+
console.log(`Bunding: ${relativePath}`);
51+
const content = getBase64(filePath);
52+
vfs[`/${relativePath}`] = {
53+
content,
54+
mime: getMimeType(filePath)
55+
};
56+
});
57+
58+
// 2. Inject VFS and Request Interceptor
59+
const interceptorScript = `
60+
<script>
61+
(function() {
62+
const VFS = ${JSON.stringify(vfs)};
63+
64+
function base64ToUint8Array(base64) {
65+
var binary_string = window.atob(base64);
66+
var len = binary_string.length;
67+
var bytes = new Uint8Array(len);
68+
for (var i = 0; i < len; i++) {
69+
bytes[i] = binary_string.charCodeAt(i);
70+
}
71+
return bytes;
72+
}
73+
74+
// Intercept Fetch
75+
const originalFetch = window.fetch;
76+
window.fetch = function(input, init) {
77+
const url = typeof input === 'string' ? input : input.url;
78+
const path = new URL(url, window.location.origin).pathname;
79+
const normalizedPath = path.startsWith('./') ? path.substring(1) : path;
80+
81+
if (VFS[normalizedPath]) {
82+
const file = VFS[normalizedPath];
83+
const data = base64ToUint8Array(file.content);
84+
const response = new Response(data, {
85+
status: 200,
86+
headers: { 'Content-Type': file.mime }
87+
});
88+
return Promise.resolve(response);
89+
}
90+
return originalFetch.apply(this, arguments);
91+
};
92+
93+
// Intercept XHR
94+
const originalOpen = XMLHttpRequest.prototype.open;
95+
XMLHttpRequest.prototype.open = function(method, url) {
96+
this._url = url;
97+
return originalOpen.apply(this, arguments);
98+
};
99+
100+
const originalSend = XMLHttpRequest.prototype.send;
101+
XMLHttpRequest.prototype.send = function() {
102+
const path = new URL(this._url, window.location.origin).pathname;
103+
if (VFS[path]) {
104+
const file = VFS[path];
105+
const data = base64ToUint8Array(file.content);
106+
107+
Object.defineProperty(this, 'status', { writable: true, value: 200 });
108+
Object.defineProperty(this, 'readyState', { writable: true, value: 4 });
109+
Object.defineProperty(this, 'response', { writable: true, value: data.buffer });
110+
Object.defineProperty(this, 'responseText', { writable: true, value: new TextDecoder().decode(data) });
111+
112+
if (this.onreadystatechange) this.onreadystatechange();
113+
if (this.onload) this.onload();
114+
return;
115+
}
116+
return originalSend.apply(this, arguments);
117+
};
118+
119+
console.log('VFS Interceptor active');
120+
})();
121+
</script>
122+
`;
123+
124+
// Insert interceptor at the top of head
125+
html = html.replace('<head>', '<head>' + interceptorScript);
126+
127+
// 3. Fix relative paths in HTML to be absolute for the interceptor
128+
html = html.replace(/(src|href)="\.?\//g, '$1="/');
129+
130+
fs.writeFileSync(OUTPUT_FILE, html);
131+
console.log(`Successfully generated: ${OUTPUT_FILE} (${(fs.statSync(OUTPUT_FILE).size / 1024 / 1024).toFixed(2)} MB)`);
132+
}
133+
134+
bundle();

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
77
<link href="/img/64.png" rel="shortcut icon" />
88
<link rel="icon" type="image/png" href="/img/64.png" />
9-
<link rel="manifest" href="./manifest.webmanifest" />
9+
<link rel="manifest" href="./manifest.json" />
1010
<meta name="theme-color" content="#0052cc" />
1111
<meta name="mobile-web-app-capable" content="yes" />
1212
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
@@ -18,7 +18,7 @@
1818
<div id="app" class="w-full h-full">
1919
<div id="iframe"></div>
2020
</div>
21-
<pwa-install id="pwa-install" manifestpath="./manifest.webmanifest" iconpath="./img/pwa-512.png"></pwa-install>
21+
<pwa-install id="pwa-install" manifestpath="./manifest.json" iconpath="./img/pwa-512.png"></pwa-install>
2222
</body>
2323
<script type="module" src="./index.ts"></script>
2424

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"scripts": {
77
"dev": "vite --host --force",
88
"build": "sh ./bin/build.sh",
9+
"build:single": "node bin/bundle_single_html.js",
910
"preview": "vite preview",
1011
"tsc": "tsc --noEmit",
1112
"format": "prettier --write .",

public/img/64.png

2.12 KB
Loading

public/img/pwa-192.png

13.8 KB
Loading

public/img/pwa-512.png

-66.6 KB
Loading
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@
1010
{
1111
"src": "img/64.png",
1212
"sizes": "64x64",
13-
"type": "image/png"
13+
"type": "image/png",
14+
"purpose": "any maskable"
1415
},
1516
{
1617
"src": "img/pwa-192.png",
1718
"sizes": "192x192",
18-
"type": "image/png"
19+
"type": "image/png",
20+
"purpose": "any maskable"
1921
},
2022
{
2123
"src": "img/pwa-512.png",
2224
"sizes": "512x512",
23-
"type": "image/png"
25+
"type": "image/png",
26+
"purpose": "any maskable"
2427
}
2528
]
2629
}

public/sw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const CACHE_VERSION = 'SW_VERSION_PLACEHOLDER'.includes('PLACEHOLDER') ? 'dev-' + Date.now() : 'SW_VERSION_PLACEHOLDER';
22
const CACHE_NAME = `document-editor-${CACHE_VERSION}`;
3-
const ASSETS_TO_CACHE = ['./', './index.html', './img/64.png'];
3+
const ASSETS_TO_CACHE = ['./', './index.html', './manifest.json', './img/64.png'];
44

55
// Cache limits and clean-up configuration
66
const MAX_CACHE_ITEMS = 100;

0 commit comments

Comments
 (0)