-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.js
More file actions
183 lines (167 loc) · 7.51 KB
/
Copy pathbuild.js
File metadata and controls
183 lines (167 loc) · 7.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/**
* MBolka Player v3.0.0 — 生产构建脚本
* 用法: npm install && node build.js
* 依赖: npm i terser clean-css
*/
const fs = require('fs');
const path = require('path');
const SRC = __dirname;
const DIST = path.join(SRC, 'dist');
const JS_FILES = [
'globals.js', 'utils.js', 'storage.js', 'loader.js',
'audio-core.js', 'pip.js', 'visualizer.js', 'ui-core.js',
'cover-lib.js', 'gamepad.js', 'app.js', 'vibration.js',
'theme-color.js', 'wco.js'
];
const CSS_FILES = [
'style.css', 'variables.css', 'base-layout.css',
'immersive.css', 'modals.css', 'components.css', 'cover-lib.css',
'wco.css'
];
// 🚀 v3.5.0: 元数据解析 Worker 内联模板 —— 作为源码 js/meta-worker.js 缺失时的兜底
// 模板内容与 js/meta-worker.js 完全一致;CDN 链接 (jsmediatags@3.9.8) 也保持一致
const META_WORKER_TEMPLATE =
"/**\n" +
" * MBolka Player - 元数据解析 Worker (P1-3, v3.6.6)\n" +
" * 后台线程中用 jsmediatags 解析音乐文件标签,主线程只收结果。\n" +
" * 通信协议:\n" +
" * -> postMessage({ key: String, file: File })\n" +
" * <- postMessage({ key, title?, artist?, album?, art?, lrcText?, error: Boolean })\n" +
" */\n" +
"importScripts('https://cdn.jsdelivr.net/npm/jsmediatags@3.9.8/build/jsmediatags.min.js');\n" +
"\n" +
"self.onmessage = (e) => {\n" +
" const { key, file } = e.data;\n" +
" if (!file) { self.postMessage({ key, error: true }); return; }\n" +
" try {\n" +
" jsmediatags.read(file, {\n" +
" onSuccess: (tag) => {\n" +
" const result = { key, error: false };\n" +
" const t = tag.tags;\n" +
" if (t.title) result.title = _decodeText(t.title);\n" +
" if (t.artist) result.artist = _decodeText(t.artist);\n" +
" if (t.album) result.album = _decodeText(t.album);\n" +
" if (t.lyrics) result.lrcText = _decodeText(t.lyrics.lyrics || t.lyrics);\n" +
" if (t.picture) {\n" +
" let b64 = '';\n" +
" const d = t.picture.data;\n" +
" for (let i = 0; i < d.length; i++) b64 += String.fromCharCode(d[i]);\n" +
" result.art = 'data:' + t.picture.format + ';base64,' + self.btoa(b64);\n" +
" }\n" +
" self.postMessage(result);\n" +
" },\n" +
" onError: () => self.postMessage({ key, error: true })\n" +
" });\n" +
" } catch (_e) { self.postMessage({ key, error: true }); }\n" +
"};\n" +
"\n" +
"function _decodeText(str) {\n" +
" if (!str) return '';\n" +
" let s = str.replace(/\\\\u([0-9a-fA-F]{4})/g, (m, g) => String.fromCharCode(parseInt(g, 16)));\n" +
" s = s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '\"');\n" +
" return s;\n" +
"}\n";
async function build() {
console.log('🔨 Building MBolka Player v3.6.6p2...');
// Create dist directory
if (!fs.existsSync(DIST)) fs.mkdirSync(DIST, { recursive: true });
// Minify JS
const { minify } = require('terser');
let jsBundle = '';
for (const f of JS_FILES) {
const code = fs.readFileSync(path.join(SRC, 'js', f), 'utf-8');
jsBundle += `/* ${f} */\n${code}\n`;
}
const minified = await minify(jsBundle, { compress: true, mangle: true });
fs.writeFileSync(path.join(DIST, 'bundle.min.js'), minified.code);
console.log(` ✅ bundle.min.js (${(minified.code.length / 1024).toFixed(1)} KB)`);
// Minify CSS
const CleanCSS = require('clean-css');
let cssBundle = '';
for (const f of CSS_FILES) {
cssBundle += fs.readFileSync(path.join(SRC, 'css', f), 'utf-8') + '\n';
}
const cssResult = new CleanCSS({ level: 2 }).minify(cssBundle);
fs.writeFileSync(path.join(DIST, 'style.min.css'), cssResult.styles);
console.log(` ✅ style.min.css (${(cssResult.styles.length / 1024).toFixed(1)} KB)`);
// Copy HTML
let html = fs.readFileSync(path.join(SRC, 'index.html'), 'utf-8');
html = html.replace(/<script src="js\/.*?"><\/script>\n?/g, '')
.replace(/<link rel="stylesheet" href="css\/.*?">\n?/g, '')
.replace('</head>', ' <link rel="stylesheet" href="style.min.css">\n</head>')
.replace('</body>', ' <script src="bundle.min.js"></script>\n</body>');
fs.writeFileSync(path.join(DIST, 'index.html'), html);
console.log(` ✅ index.html`);
// Copy static assets (favicon + PWA icons) — 分支部署时由根目录提供,dist 必须自带
fs.copyFileSync(path.join(SRC, 'favicon.ico'), path.join(DIST, 'favicon.ico'));
fs.cpSync(path.join(SRC, 'icons'), path.join(DIST, 'icons'), { recursive: true });
fs.copyFileSync(path.join(SRC, 'manifest.json'), path.join(DIST, 'manifest.json'));
// 🚀 v3.5.0: 元数据解析 Worker(不参与 bundle,原样复制)
// 优先从源码 js/meta-worker.js 复制;缺失时回退到 build.js 内联模板,保证 CI 必过
const _mwSrc = path.join(SRC, 'js/meta-worker.js');
const _mwDst = path.join(DIST, 'js/meta-worker.js');
fs.mkdirSync(path.dirname(_mwDst), { recursive: true });
if (fs.existsSync(_mwSrc)) {
fs.copyFileSync(_mwSrc, _mwDst);
} else {
console.log(` ⚠️ ${_mwSrc} 不存在,使用 build.js 内联模板生成`);
fs.writeFileSync(_mwDst, META_WORKER_TEMPLATE);
}
console.log(` ✅ favicon.ico + icons/ + manifest.json + js/meta-worker.js`);
// Generate dist-specific Service Worker (相对路径,子路径 /muse/ 安全)
const iconFiles = fs.readdirSync(path.join(SRC, 'icons'))
.filter(f => /\.png$/i.test(f))
.map(f => './icons/' + f);
fs.writeFileSync(path.join(DIST, 'sw.js'), genSW([
'./', './index.html',
'./bundle.min.js', './style.min.css',
'./manifest.json', './favicon.ico',
...iconFiles
]));
console.log(` ✅ sw.js (dist, 相对路径)`);
console.log('\n✨ Build complete! Output in dist/');
}
/**
* 生成 dist 专用 Service Worker:预缓存列表用相对路径,子路径 (/muse/) 安全。
*/
function genSW(urls) {
const list = JSON.stringify(urls, null, 12);
return `/* MBolka Player v3.6.3 — dist Service Worker (相对路径, 子路径安全) */
const CACHE_NAME = 'mbolka-v3.6.3';
const RUNTIME_CACHE = 'mbolka-runtime-v3.6.3';
const CACHE_URLS = ${list};
self.addEventListener('install', e => {
self.skipWaiting();
e.waitUntil(caches.open(CACHE_NAME).then(cache =>
Promise.all(CACHE_URLS.map(url =>
cache.add(url).catch(err => console.warn('[SW] 预缓存失败:', url, err))
))
));
});
self.addEventListener('activate', e => {
e.waitUntil(caches.keys().then(keys => Promise.all(
keys.filter(k => k !== CACHE_NAME && k !== RUNTIME_CACHE).map(k => caches.delete(k))
)).then(() => clients.claim()));
});
self.addEventListener('fetch', e => {
const req = e.request;
if (req.method !== 'GET') return;
// 🔧 v3.6.3: Network-First —— 在线优先回源(源码改动刷新即生效),离线/失败回退缓存
e.respondWith(
fetch(req).then(res => {
if (res && res.ok && isSameOrigin(req.url)) {
const copy = res.clone();
caches.open(RUNTIME_CACHE).then(c => c.put(req, copy)).catch(() => {});
}
return res;
}).catch(() => caches.match(req).then(cached =>
cached || new Response('', { status: 503, statusText: 'Offline' })
))
);
});
function isSameOrigin(url) {
try { return new URL(url).origin === self.location.origin; } catch (_) { return false; }
}
`;
}
build().catch(e => { console.error('Build failed:', e); process.exit(1); });