-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.js
More file actions
154 lines (130 loc) · 5.08 KB
/
Copy pathbuild.js
File metadata and controls
154 lines (130 loc) · 5.08 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
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const { minify } = require('terser');
const CleanCSS = require('clean-css');
const BUDGET = 14 * 1024;
function isLocalPath(href) {
return !/^https?:\/\//.test(href) && !href.startsWith('//');
}
function reportFile(label, buf) {
const raw = Buffer.byteLength(buf);
const gz = zlib.gzipSync(buf, { level: 9 }).length;
const warn = gz > BUDGET ? ' ⚠ OVER BUDGET' : '';
console.log(` ${label.padEnd(30)} ${(raw / 1024).toFixed(1).padStart(7)} KB raw ${(gz / 1024).toFixed(1).padStart(7)} KB gz${warn}`);
return { raw, gz };
}
async function build() {
// Clean docs
fs.rmSync('docs', { recursive: true, force: true });
fs.mkdirSync('docs/js', { recursive: true });
fs.mkdirSync('docs/vendor', { recursive: true });
// Copy Monaco editor vs/ directory for local serving
if (fs.existsSync('vendor/vs')) {
fs.cpSync('vendor/vs', 'docs/vendor/vs', { recursive: true });
console.log(' Copied vendor/vs/ (Monaco editor)');
}
let html = fs.readFileSync('index.html', 'utf8');
// Strip HTML comments
html = html.replace(/<!--[\s\S]*?-->/g, '');
// Strip JSON-LD structured data
html = html.replace(/<script type="application\/ld\+json">[\s\S]*?<\/script>/g, '');
// Inline all local CSS into <style> (small enough to save an HTTP request)
html = html.replace(
/<link\s+[^>]*rel="stylesheet"[^>]*href="([^"]+)"[^>]*\/?>/g,
(match, href) => {
if (!isLocalPath(href)) return match;
const css = fs.readFileSync(href, 'utf8');
const result = new CleanCSS({ level: 2 }).minify(css);
if (result.errors.length) throw new Error(`clean-css (${href}): ${result.errors}`);
console.log(` Inlined CSS: ${href}`);
return `<style>${result.styles}</style>`;
}
);
// Process local <script src="..."> tags: minify app JS → dist, copy vendor → dist, rewrite paths
const jsRegex = /<script\s+src="([^"]+)"([^>]*)><\/script>/g;
const jsReplacements = [];
let jsMatch;
while ((jsMatch = jsRegex.exec(html)) !== null) {
const [fullMatch, src, attrs] = jsMatch;
if (!isLocalPath(src)) continue;
jsReplacements.push({ fullMatch, src, attrs });
}
for (const { fullMatch, src, attrs } of jsReplacements) {
const js = fs.readFileSync(src, 'utf8');
if (src.startsWith('vendor/')) {
// Copy vendor file (already minified)
const destRel = src; // e.g. vendor/loader.min.js
const destAbs = path.join('docs', destRel);
fs.copyFileSync(src, destAbs);
html = html.replace(fullMatch, `<script src="${destRel}"${attrs}></script>`);
console.log(` Copied vendor: ${src}`);
} else {
// Minify app JS
const basename = path.basename(src, '.js') + '.min.js';
const destRel = `js/${basename}`;
const destAbs = path.join('docs', destRel);
// Collect global functions referenced by onclick/onchange in HTML and JS-generated markup
const htmlHandlers = new Set();
const onPattern = /\bon(?:click|change|input|submit)\s*=\s*(?:"|\\")(\w+)\(/g;
html.replace(onPattern, (_m, fn) => htmlHandlers.add(fn));
js.replace(onPattern, (_m, fn) => htmlHandlers.add(fn));
const result = await minify(js, {
compress: {
passes: 3,
drop_console: false,
unsafe: true,
unsafe_math: true,
unsafe_regexp: true,
pure_getters: true,
},
mangle: {
toplevel: true,
reserved: [...htmlHandlers],
},
format: { comments: false },
});
if (result.error) throw result.error;
fs.writeFileSync(destAbs, result.code);
html = html.replace(fullMatch, `<script src="${destRel}"${attrs}></script>`);
console.log(` Minified JS: ${src} → ${destRel}`);
}
}
// Collapse whitespace between tags (protect <script>/<style> content)
const preserved = [];
html = html.replace(/<(script|style)([\s\S]*?)>([\s\S]*?)<\/\1>/g, (match) => {
const idx = preserved.length;
preserved.push(match);
return `__PRESERVED_${idx}__`;
});
html = html.replace(/\s+/g, ' ');
html = html.replace(/> </g, '><');
preserved.forEach((block, idx) => {
html = html.replace(`__PRESERVED_${idx}__`, block);
});
html = html.trim();
// Write index.html
fs.writeFileSync('docs/index.html', html);
// Report sizes
console.log('\nOutput files:');
let totalRaw = 0, totalGz = 0;
const outFiles = [];
(function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) walk(path.join(dir, entry.name));
else outFiles.push(path.join(dir, entry.name));
}
})('docs');
for (const file of outFiles.sort()) {
const buf = fs.readFileSync(file);
const label = path.relative('docs', file);
const { raw, gz } = reportFile(label, buf);
totalRaw += raw;
totalGz += gz;
}
console.log(` ${'TOTAL'.padEnd(30)} ${(totalRaw / 1024).toFixed(1).padStart(7)} KB raw ${(totalGz / 1024).toFixed(1).padStart(7)} KB gz`);
}
build().catch(err => {
console.error('Build failed:', err);
process.exit(1);
});