Repository navigation
Expand file tree
/
Copy pathbuild-cli.mjs
More file actions
142 lines (131 loc) · 5.66 KB
/
Copy pathbuild-cli.mjs
File metadata and controls
142 lines (131 loc) · 5.66 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
#!/usr/bin/env node
/**
* Builds the `forwardemail` terminal client into cli/dist/forwardemail.cjs:
* one CommonJS file with no runtime dependencies, used both by the npm
* package and as the entry point of the single executable (scripts/build-sea.mjs),
* plus cli/dist/notifier/, the programs that show desktop notifications.
*
* node scripts/build-cli.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { build } from 'vite';
import {
insertAfterRule,
pruneCustomProperties,
pruneStylesheet,
referencedCustomProperties,
} from '../src/cli/prune-css.js';
import { convertStylesheet } from '../src/cli/px-to-cells.js';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
process.chdir(root);
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const outDir = path.join(root, 'cli', 'dist');
const configFile = path.join(root, 'vite.cli.config.js');
fs.rmSync(outDir, { recursive: true, force: true });
for (const mode of ['app', 'launcher', 'thread']) {
const started = Date.now();
await build({ configFile, mode });
console.log(`built ${mode} in ${((Date.now() - started) / 1000).toFixed(1)}s`);
}
const read = (name) => fs.readFileSync(path.join(outDir, name), 'utf8');
const app = read('app.cjs');
const launcher = read('launcher.cjs');
const thread = read('thread.cjs');
// Custom properties that scripts, markup or the terminal theme mention by
// name are kept even when no rule reads them.
function sourceText(dir) {
let text = '';
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const file = path.join(dir, entry.name);
if (entry.isDirectory()) text += sourceText(file);
else if (/\.(ts|js|svelte|css)$/.test(entry.name)) {
const content = fs.readFileSync(file, 'utf8');
// A component's own <style> is in the stylesheet already.
text += entry.name.endsWith('.svelte')
? content.replace(/<style[\s\S]*?<\/style>/g, '')
: entry.name.endsWith('.css') && !file.includes(`${path.sep}cli${path.sep}`)
? ''
: content;
}
}
return text;
}
// The stylesheet is trimmed to what a terminal can draw and its lengths are
// converted to cells once, here, rather than on every launch.
const keep = referencedCustomProperties(sourceText(path.join(root, 'src')));
const css = insertAfterRule(
convertStylesheet(pruneCustomProperties(pruneStylesheet(read('app.css')), keep)),
'.inline-flex',
fs.readFileSync(path.join(root, 'src', 'cli', 'terminal-utilities.css'), 'utf8'),
);
// The app runs only when the launcher calls __forwardemailLoadApp(), after the
// terminal document, storage and workers are installed as globals.
const output = [
'#!/usr/bin/env node',
`/*! forwardemail v${pkg.version} | https://github.com/forwardemail/mail.forwardemail.net | BUSL-1.1 */`,
`var __forwardemailAppCss = ${JSON.stringify(css)};`,
`var __forwardemailWorkerBootstrap = ${JSON.stringify(thread)};`,
'function __forwardemailLoadApp() {',
' var module = { exports: {} };',
' (function (module, exports, require, __filename, __dirname) {',
app,
' })(module, module.exports, require, __filename, __dirname);',
' return module.exports;',
'}',
// The launcher gets its own scope too: its top-level names (TermDOM declares
// Comment, Node and the like for its own use) would otherwise shadow the
// DOM globals the app reads by the same names.
'(function () {',
launcher,
'})();',
'',
].join('\n');
const target = path.join(outDir, 'forwardemail.cjs');
fs.writeFileSync(target, output, { mode: 0o755 });
for (const name of ['app.cjs', 'launcher.cjs', 'thread.cjs', 'app.css']) {
fs.rmSync(path.join(outDir, name));
}
// Desktop notifications (src/cli/notifications.ts) run the system's notifier
// through toasted-notifier, which needs its programs as files: macOS's
// terminal-notifier and Windows' ntfytoast, with their licenses. Linux uses
// notify-send from the system. build-sea.mjs embeds the ones for its platform.
const toasted = path.dirname(
createRequire(import.meta.url).resolve('toasted-notifier/package.json'),
);
const notifierDir = path.join(outDir, 'notifier');
const notifierFiles = [
['vendor/mac.noindex/terminal-notifier.app', 'mac.noindex/terminal-notifier.app'],
['vendor/terminal-notifier-LICENSE', 'LICENSE-terminal-notifier'],
['vendor/ntfyToast/ntfytoast.exe', 'ntfytoast.exe'],
['vendor/ntfyToast/LICENSE.txt', 'LICENSE-ntfytoast.txt'],
['LICENSE.md', 'LICENSE-toasted-notifier.md'],
];
for (const [from, to] of notifierFiles) {
fs.cpSync(path.join(toasted, from), path.join(notifierDir, to), { recursive: true });
}
fs.copyFileSync(
path.join(root, 'public', 'icons', 'icon-256.png'),
path.join(notifierDir, 'icon.png'),
);
// toasted-notifier's package has them without the executable bit.
for (const program of [
'mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier',
'ntfytoast.exe',
]) {
fs.chmodSync(path.join(notifierDir, program), 0o755);
}
const size = (fs.statSync(target).size / 1024 / 1024).toFixed(1);
console.log(`wrote ${path.relative(root, target)} (${size} MB)`);
// The npm package (cli/) ships the bundle with the CLI guide as its README.
const cliPkg = JSON.parse(fs.readFileSync(path.join(root, 'cli', 'package.json'), 'utf8'));
if (cliPkg.version !== pkg.version) {
console.error(
`cli/package.json is at ${cliPkg.version} but package.json is at ${pkg.version}; run node scripts/sync-version.cjs`,
);
process.exit(1);
}
fs.copyFileSync(path.join(root, 'docs', 'CLI.md'), path.join(root, 'cli', 'README.md'));
fs.copyFileSync(path.join(root, 'LICENSE.md'), path.join(root, 'cli', 'LICENSE.md'));