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 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 you can
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
 
@@ -10,36 +10,31 @@
10
10
  const { spawn } = require('child_process');
11
11
  const path = require('path');
12
12
 
13
- // Name the Electron shell before resolving it, not only at install time.
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 scripts are increasingly blocked -- npm warns about them by
16
- // default now, and plenty of people and companies set ignore-scripts -- and a
17
- // user who installs with them off gets "Electron" in their Dock. Doing it here
18
- // as well means the name is right by the time macOS reads the bundle, whatever
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 (_) { /* cosmetic: never block a launch over the name */ }
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
- let electron;
31
- try {
32
- // Branding resolved the electron module itself, so it is already in Node's
33
- // module cache holding the path from before the rename. Drop it and read it
34
- // again, or the launch spawns a file that no longer exists.
35
- delete require.cache[require.resolve('electron')];
36
- electron = require('electron');
37
- } catch (_) {
38
- console.error(
39
- 'Could not find the Electron runtime.\n' +
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,6 +1,6 @@
1
1
  {
2
2
  "name": "latex-stickies",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "Sticky notes for your desktop that render LaTeX and Markdown.",
5
5
  "main": "src/main.js",
6
6
  "scripts": {
@@ -1,180 +1,198 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Gives the Electron shell this app's name and icon, on macOS.
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 bundle of our own, so macOS
6
- * takes the Dock name and icon from the shell being launched -- which is called
7
- * "Electron" and wears the atom logo. app.setName() cannot change that: the
8
- * Dock reads it from the bundle before any of our code runs.
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
- * Editing Info.plist invalidates the bundle's ad-hoc signature, so it is
11
- * re-signed afterwards. That matters: an unsigned bundle will not launch on
12
- * Apple Silicon at all, which is a far worse outcome than a wrong name. If
13
- * anything here fails, the original Info.plist goes back and the install
14
- * continues quietly -- a cosmetic nicety must never break someone's install.
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 Electron shell ships as com.github.Electron, and LaunchServices keys its
25
- * database by identifier rather than by path. So on a machine that has seen any
26
- * other Electron -- another project's node_modules, an old npx copy, a
27
- * different Electron app entirely -- the Dock can serve that record's name,
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
- * Makes LaunchServices re-read a bundle.
35
- *
36
- * It caches an app's name from the first time it sees the path, and npm
37
- * extracts Electron under a directory macOS indexes -- so it can record
38
- * "Electron" before this script ever runs, and then keep showing that in the
39
- * Dock however right the plist is. Renaming alone is not enough.
40
- */
41
- function refreshLaunchServices(appPath) {
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
- execFileSync(
44
- '/System/Library/Frameworks/CoreServices.framework/Frameworks'
45
- + '/LaunchServices.framework/Support/lsregister',
46
- ['-f', appPath],
47
- { stdio: 'ignore', timeout: 10000 }
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
- * Puts our icon in the bundle, and says whether it had to.
54
- *
55
- * The bundle icon is what macOS shows before the app is running -- in Finder,
56
- * in Cmd-Tab, and in the Dock at launch -- so app.dock.setIcon() alone is not
57
- * enough. Compared byte for byte rather than copied blindly, because a copy
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
- * Re-signs the bundle. Required, not optional: any edit under Contents/
71
- * invalidates the ad-hoc signature, and macOS refuses to launch a bundle whose
72
- * signature does not match its contents.
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 resign(appPath) {
75
- execFileSync('codesign', ['--force', '--deep', '--sign', '-', appPath], {
76
- stdio: 'ignore',
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
- let appPath;
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
- const binary = require(require.resolve('electron', { paths: [path.join(__dirname, '..')] }));
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
- return; // no runtime installed; nothing to brand
115
+ run('cp', ['-R', app, TARGET]);
91
116
  }
92
117
 
93
- const plist = path.join(appPath, 'Contents', 'Info.plist');
94
- if (!fs.existsSync(plist)) return;
95
-
96
- const read = (key) => {
97
- try {
98
- return execFileSync('plutil', ['-extract', key, 'raw', plist]).toString().trim();
99
- } catch (_) {
100
- return '';
101
- }
102
- };
103
-
104
- const binary = path.join(appPath, 'Contents', 'MacOS', NAME);
105
- if (read('CFBundleName') === NAME
106
- && read('CFBundleIdentifier') === APP_ID
107
- && fs.existsSync(binary)) {
108
- // Named already, but the icon may still be an old one: someone who
109
- // installed an earlier version has a bundle branded with that version's
110
- // artwork, and every upgrade since has taken this path and left it there.
111
- if (copyIcon(appPath)) {
112
- resign(appPath);
113
- refreshLaunchServices(appPath);
114
- console.log('updated the icon on the Electron shell');
115
- return;
116
- }
117
- // Otherwise a wrong name in the Dock can only be a stale cache.
118
- refreshLaunchServices(appPath);
119
- // Say so rather than exiting in silence. A user reporting "the Dock still
120
- // says Electron" needs to know whether the rename failed or whether they
121
- // are looking at an older instance still holding the single-instance lock.
122
- if (process.env.LATEX_STICKIES_VERBOSE) {
123
- console.log(`the Electron shell is already named "${NAME}"`);
124
- }
125
- return;
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
- const fields = [
131
- ['CFBundleName', NAME],
132
- ['CFBundleDisplayName', NAME],
133
- ['CFBundleIdentifier', APP_ID],
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
- fs.writeFileSync(plist, backup);
166
- try {
167
- execFileSync('codesign', ['--force', '--deep', '--sign', '-', appPath], {
168
- stdio: 'ignore',
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
- try {
176
- main();
177
- } catch (err) {
178
- // Never fail an install over the name in the Dock.
179
- console.log('skipped naming the Electron shell:', err.message.split('\n')[0]);
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();