latex-stickies 1.3.7 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/latex-stickies.js +22 -27
- package/package.json +1 -1
- package/scripts/brand-electron.js +162 -144
package/README.md
CHANGED
|
@@ -24,7 +24,10 @@ latex-stickies
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Requires Node 22.12 or newer. The first run downloads the Electron runtime
|
|
27
|
-
(~230 MB), so give it a minute; later launches are instant. On a Mac
|
|
27
|
+
(~230 MB), so give it a minute; later launches are instant. On a Mac it also
|
|
28
|
+
keeps a copy of the runtime carrying this app's name and icon in
|
|
29
|
+
`~/Library/Application Support/latex-stickies/` -- on APFS that is a
|
|
30
|
+
copy-on-write clone, so it costs almost no disk. On a Mac you can
|
|
28
31
|
download a `.dmg` from [Releases](https://github.com/kev-nj/latex-stickies/releases)
|
|
29
32
|
instead and skip Node entirely.
|
|
30
33
|
|
package/bin/latex-stickies.js
CHANGED
|
@@ -10,36 +10,31 @@
|
|
|
10
10
|
const { spawn } = require('child_process');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// Brand before resolving anything: this returns the path to a copy of the
|
|
14
|
+
// Electron shell carrying our name and icon, kept outside node_modules so it
|
|
15
|
+
// survives npx cache churn and npm ci. Falls back to the plain shell, which
|
|
16
|
+
// runs identically and only looks wrong in the Dock.
|
|
14
17
|
//
|
|
15
|
-
// postinstall
|
|
16
|
-
// default
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
// happened during the install. It returns immediately once done.
|
|
20
|
-
//
|
|
21
|
-
// Strictly before require('electron'): branding renames the executable and
|
|
22
|
-
// rewrites the path.txt that require reads, so a path captured first points at
|
|
23
|
-
// a file that no longer exists and the launch fails with ENOENT. That cost a
|
|
24
|
-
// release -- every first launch after upgrading failed, and only the second
|
|
25
|
-
// worked.
|
|
18
|
+
// postinstall cannot be relied on for this -- npm 11 warns about install
|
|
19
|
+
// scripts by default and blocks them outright for global installs, and plenty
|
|
20
|
+
// of people and companies turn them off.
|
|
21
|
+
let electron = null;
|
|
26
22
|
try {
|
|
27
|
-
require('../scripts/brand-electron.js');
|
|
28
|
-
} catch (
|
|
23
|
+
electron = require('../scripts/brand-electron.js').ensure();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error(`could not brand the app: ${err.message}`);
|
|
26
|
+
}
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'Reinstall with: npm install -g latex-stickies'
|
|
41
|
-
);
|
|
42
|
-
process.exit(1);
|
|
28
|
+
if (!electron) {
|
|
29
|
+
try {
|
|
30
|
+
electron = require('electron');
|
|
31
|
+
} catch (_) {
|
|
32
|
+
console.error(
|
|
33
|
+
'Could not find the Electron runtime.\n' +
|
|
34
|
+
'Reinstall with: npm install -g latex-stickies'
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
const appDir = path.join(__dirname, '..');
|
package/package.json
CHANGED
|
@@ -1,180 +1,198 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Gives the
|
|
3
|
+
* Gives this app its own macOS bundle, so the Dock shows our name and icon.
|
|
4
4
|
*
|
|
5
|
-
* Run from source or from npm there is no .app
|
|
6
|
-
*
|
|
7
|
-
* "Electron" and wears the atom logo. app.setName() cannot
|
|
8
|
-
*
|
|
5
|
+
* Run from source or from npm there is no .app of our own: macOS takes the
|
|
6
|
+
* Dock name and icon from the Electron shell being launched, which is called
|
|
7
|
+
* "Electron" and wears the atom logo. app.setName() cannot help -- the Dock
|
|
8
|
+
* reads the bundle before any of our code runs.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* So the shell is cloned to a stable path of our own and branded there:
|
|
11
|
+
*
|
|
12
|
+
* ~/Library/Application Support/latex-stickies/LaTeX Stickies.app
|
|
13
|
+
*
|
|
14
|
+
* Cloning rather than editing node_modules in place, for three reasons. npx
|
|
15
|
+
* installs under a fresh cache hash every time, so in-place work is redone
|
|
16
|
+
* constantly and thrown away by any cache prune or npm ci. A path macOS has
|
|
17
|
+
* already registered as "Electron" tends to keep serving that name back from
|
|
18
|
+
* the LaunchServices database however correct the plist becomes -- a path it
|
|
19
|
+
* has never seen has no such record. And `npm update electron` would silently
|
|
20
|
+
* revert every rename we made.
|
|
21
|
+
*
|
|
22
|
+
* On APFS the clone is copy-on-write: near-instant, and it costs almost no
|
|
23
|
+
* disk until the files diverge.
|
|
24
|
+
*
|
|
25
|
+
* Everything here is best-effort. A wrong name in the Dock is a blemish; a
|
|
26
|
+
* failed launch is not, so any error falls back to the unbranded shell.
|
|
15
27
|
*/
|
|
16
28
|
const { execFileSync } = require('child_process');
|
|
29
|
+
const crypto = require('crypto');
|
|
17
30
|
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
18
32
|
const path = require('path');
|
|
19
33
|
|
|
20
34
|
const NAME = 'LaTeX Stickies';
|
|
21
35
|
/**
|
|
22
36
|
* Our own bundle identifier, matching the packaged builds.
|
|
23
37
|
*
|
|
24
|
-
* The
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* "Electron", however carefully this script renames our copy. Taking our own
|
|
29
|
-
* identifier is what stops the collision; renaming alone cannot.
|
|
38
|
+
* The shell ships as com.github.Electron, and LaunchServices keys its database
|
|
39
|
+
* by identifier as well as by path -- so on a machine that has seen any other
|
|
40
|
+
* Electron, that shared identifier can resolve to someone else's record and
|
|
41
|
+
* serve its name, "Electron", however carefully this bundle is renamed.
|
|
30
42
|
*/
|
|
31
43
|
const APP_ID = 'com.kevinjusak.latexsticky';
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const ROOT = path.join(__dirname, '..');
|
|
46
|
+
const ICON = path.join(ROOT, 'build', 'icon.icns');
|
|
47
|
+
|
|
48
|
+
/** Overridable so a test can brand somewhere disposable. */
|
|
49
|
+
const HOME = process.env.LATEX_STICKIES_APP_DIR
|
|
50
|
+
|| path.join(os.homedir(), 'Library', 'Application Support', 'latex-stickies');
|
|
51
|
+
const TARGET = path.join(HOME, `${NAME}.app`);
|
|
52
|
+
const STAMP = path.join(HOME, '.branded.json');
|
|
53
|
+
|
|
54
|
+
const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework/Frameworks'
|
|
55
|
+
+ '/LaunchServices.framework/Support/lsregister';
|
|
56
|
+
|
|
57
|
+
const run = (cmd, args, opts = {}) =>
|
|
58
|
+
execFileSync(cmd, args, { stdio: 'ignore', timeout: 120000, ...opts });
|
|
59
|
+
|
|
60
|
+
/** The Electron shell npm installed, and the version we would be cloning. */
|
|
61
|
+
function source() {
|
|
62
|
+
const dir = path.dirname(require.resolve('electron/package.json', { paths: [ROOT] }));
|
|
63
|
+
const binary = require(path.join(dir, 'index.js'));
|
|
64
|
+
// .../Electron.app/Contents/MacOS/Electron -> .../Electron.app
|
|
65
|
+
const app = path.resolve(path.dirname(binary), '..', '..');
|
|
66
|
+
const { version } = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
67
|
+
return { app, version };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sha = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
71
|
+
|
|
72
|
+
/** What the branded copy was built from, so it can be rebuilt when that moves. */
|
|
73
|
+
function stamp(version) {
|
|
74
|
+
return JSON.stringify({
|
|
75
|
+
electron: version,
|
|
76
|
+
icon: fs.existsSync(ICON) ? sha(ICON) : '',
|
|
77
|
+
name: NAME,
|
|
78
|
+
id: APP_ID,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isCurrent(version) {
|
|
42
83
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
49
|
-
} catch (_) { /* the name is still right; only the cache is stale */ }
|
|
84
|
+
return fs.existsSync(path.join(TARGET, 'Contents', 'MacOS', NAME))
|
|
85
|
+
&& fs.readFileSync(STAMP, 'utf8') === stamp(version);
|
|
86
|
+
} catch (_) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
50
89
|
}
|
|
51
90
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* invalidates the signature and costs a re-sign every launch.
|
|
59
|
-
*/
|
|
60
|
-
function copyIcon(appPath) {
|
|
61
|
-
const icon = path.join(__dirname, '..', 'build', 'icon.icns');
|
|
62
|
-
const target = path.join(appPath, 'Contents', 'Resources', 'electron.icns');
|
|
63
|
-
if (!fs.existsSync(icon) || !fs.existsSync(target)) return false;
|
|
64
|
-
if (fs.readFileSync(icon).equals(fs.readFileSync(target))) return false;
|
|
65
|
-
fs.copyFileSync(icon, target);
|
|
66
|
-
return true;
|
|
91
|
+
function setPlist(plist, key, value) {
|
|
92
|
+
try {
|
|
93
|
+
run('plutil', ['-replace', key, '-string', value, plist]);
|
|
94
|
+
} catch (_) {
|
|
95
|
+
run('plutil', ['-insert', key, '-string', value, plist]);
|
|
96
|
+
}
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
100
|
+
* Builds the branded copy.
|
|
101
|
+
*
|
|
102
|
+
* The executable is renamed as well as the plist keys: macOS takes the Dock
|
|
103
|
+
* label from the running executable, which is why packaged builds have always
|
|
104
|
+
* been right -- electron-builder renames it -- and every npm install was not.
|
|
73
105
|
*/
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
execFileSync('codesign', ['--verify', appPath], { stdio: 'ignore' });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function main() {
|
|
82
|
-
if (process.platform !== 'darwin') return; // only macOS names apps this way
|
|
106
|
+
function build(app, version) {
|
|
107
|
+
fs.mkdirSync(HOME, { recursive: true });
|
|
108
|
+
fs.rmSync(TARGET, { recursive: true, force: true });
|
|
83
109
|
|
|
84
|
-
|
|
110
|
+
// -c asks APFS for a copy-on-write clone; plain copy on any filesystem
|
|
111
|
+
// that cannot, which is slower but correct.
|
|
85
112
|
try {
|
|
86
|
-
|
|
87
|
-
// .../Electron.app/Contents/MacOS/Electron -> .../Electron.app
|
|
88
|
-
appPath = path.resolve(path.dirname(binary), '..', '..');
|
|
113
|
+
run('cp', ['-cR', app, TARGET]);
|
|
89
114
|
} catch (_) {
|
|
90
|
-
|
|
115
|
+
run('cp', ['-R', app, TARGET]);
|
|
91
116
|
}
|
|
92
117
|
|
|
93
|
-
const plist = path.join(
|
|
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
|
-
|
|
118
|
+
const plist = path.join(TARGET, 'Contents', 'Info.plist');
|
|
119
|
+
setPlist(plist, 'CFBundleName', NAME);
|
|
120
|
+
setPlist(plist, 'CFBundleDisplayName', NAME);
|
|
121
|
+
setPlist(plist, 'CFBundleIdentifier', APP_ID);
|
|
122
|
+
setPlist(plist, 'CFBundleExecutable', NAME);
|
|
123
|
+
|
|
124
|
+
const from = path.join(TARGET, 'Contents', 'MacOS', 'Electron');
|
|
125
|
+
const to = path.join(TARGET, 'Contents', 'MacOS', NAME);
|
|
126
|
+
if (fs.existsSync(from)) fs.renameSync(from, to);
|
|
127
|
+
|
|
128
|
+
// The bundle icon is what Finder, Cmd-Tab and the Dock show before the app
|
|
129
|
+
// is running; app.dock.setIcon() cannot stand in for it.
|
|
130
|
+
const icon = path.join(TARGET, 'Contents', 'Resources', 'electron.icns');
|
|
131
|
+
if (fs.existsSync(ICON) && fs.existsSync(icon)) fs.copyFileSync(ICON, icon);
|
|
132
|
+
|
|
133
|
+
// Re-signing is required: any edit under Contents/ invalidates the ad-hoc
|
|
134
|
+
// signature, and macOS will not launch a bundle whose signature does not
|
|
135
|
+
// match its contents. Only the outer bundle was touched, so the helpers keep
|
|
136
|
+
// their own signatures -- and --preserve-metadata keeps whatever
|
|
137
|
+
// entitlements the stock build shipped with, which --deep would discard.
|
|
138
|
+
run('codesign', [
|
|
139
|
+
'--force',
|
|
140
|
+
'--preserve-metadata=entitlements,requirements,flags',
|
|
141
|
+
'--sign', '-', TARGET,
|
|
142
|
+
]);
|
|
143
|
+
run('codesign', ['--verify', TARGET]);
|
|
144
|
+
|
|
145
|
+
// Evict any record for this path before registering it, since lsregister -f
|
|
146
|
+
// refreshes an entry but does not replace a stale one.
|
|
147
|
+
try { run(LSREGISTER, ['-u', TARGET]); } catch (_) { /* nothing registered */ }
|
|
148
|
+
try { run(LSREGISTER, ['-f', TARGET]); } catch (_) { /* only the cache */ }
|
|
149
|
+
|
|
150
|
+
fs.writeFileSync(STAMP, stamp(version));
|
|
151
|
+
return to;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The binary to launch: the branded copy, or null to fall back to the shell.
|
|
156
|
+
*/
|
|
157
|
+
function ensure() {
|
|
158
|
+
if (process.platform !== 'darwin') return null;
|
|
159
|
+
|
|
160
|
+
let app;
|
|
161
|
+
let version;
|
|
162
|
+
try {
|
|
163
|
+
({ app, version } = source());
|
|
164
|
+
} catch (err) {
|
|
165
|
+
report(`could not find the Electron runtime to brand (${first(err)})`);
|
|
166
|
+
return null;
|
|
126
167
|
}
|
|
127
168
|
|
|
128
|
-
const backup = fs.readFileSync(plist);
|
|
129
169
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
['CFBundleExecutable', NAME],
|
|
135
|
-
];
|
|
136
|
-
for (const [key, value] of fields) {
|
|
137
|
-
try {
|
|
138
|
-
execFileSync('plutil', ['-replace', key, '-string', value, plist]);
|
|
139
|
-
} catch (_) {
|
|
140
|
-
execFileSync('plutil', ['-insert', key, '-string', value, plist]);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Rename the executable too. macOS takes the Dock label from the running
|
|
145
|
-
// executable, not only from the plist -- which is why the packaged builds
|
|
146
|
-
// have always shown the right name (electron-builder renames it) while
|
|
147
|
-
// every npm install showed "Electron" however the plist was rewritten.
|
|
148
|
-
const oldBinary = path.join(appPath, 'Contents', 'MacOS', 'Electron');
|
|
149
|
-
if (fs.existsSync(oldBinary) && !fs.existsSync(binary)) {
|
|
150
|
-
fs.renameSync(oldBinary, binary);
|
|
151
|
-
}
|
|
152
|
-
// require('electron') reads this file for the path it returns, so it has
|
|
153
|
-
// to follow the rename or every launcher breaks.
|
|
154
|
-
const pathTxt = path.join(path.dirname(appPath), '..', 'path.txt');
|
|
155
|
-
if (fs.existsSync(pathTxt)) {
|
|
156
|
-
fs.writeFileSync(pathTxt, `Electron.app/Contents/MacOS/${NAME}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
copyIcon(appPath);
|
|
160
|
-
resign(appPath);
|
|
161
|
-
refreshLaunchServices(appPath);
|
|
162
|
-
|
|
163
|
-
console.log(`named the Electron shell "${NAME}"`);
|
|
170
|
+
if (isCurrent(version)) return path.join(TARGET, 'Contents', 'MacOS', NAME);
|
|
171
|
+
const binary = build(app, version);
|
|
172
|
+
report(`branded ${TARGET}`, true);
|
|
173
|
+
return binary;
|
|
164
174
|
} catch (err) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
} catch (_) { /* leave it as found */ }
|
|
171
|
-
console.log(`could not rename the Electron shell (${err.message.split('\n')[0]})`);
|
|
175
|
+
// Leave nothing half-built: a bundle with a broken signature will not
|
|
176
|
+
// launch at all, which is far worse than the wrong name.
|
|
177
|
+
fs.rmSync(TARGET, { recursive: true, force: true });
|
|
178
|
+
report(`could not brand the app (${first(err)})`);
|
|
179
|
+
return null;
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
const first = (err) => String(err && err.message).split('\n')[0];
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Failures are reported, successes only when asked.
|
|
187
|
+
*
|
|
188
|
+
* A silent failure is what made this so hard to diagnose from a user's
|
|
189
|
+
* terminal: "the Dock still says Electron" looked identical whether branding
|
|
190
|
+
* had failed, had never run, or had worked on a bundle nobody was launching.
|
|
191
|
+
*/
|
|
192
|
+
function report(message, quiet = false) {
|
|
193
|
+
if (!quiet || process.env.LATEX_STICKIES_VERBOSE) console.log(message);
|
|
180
194
|
}
|
|
195
|
+
|
|
196
|
+
module.exports = { ensure, TARGET, NAME, APP_ID };
|
|
197
|
+
|
|
198
|
+
if (require.main === module) ensure();
|