@bobfrankston/rmfmail 1.0.632 → 1.0.640
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/TODO.md +5 -0
- package/client/app.js +22 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +15 -0
- package/client/components/alarms.js +17 -10
- package/client/components/alarms.js.map +1 -1
- package/client/components/alarms.ts +17 -9
- package/client/compose/compose.js +13 -15
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +16 -8
- package/client/compose/editor.js +36 -3
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +40 -3
- package/client/index.html +19 -15
- package/client/styles/components.css +14 -0
- package/docs/prod-android.md +55 -69
- package/docs/prod-wrapper.md.merged +215 -0
- package/docs/prod.md +178 -59
- package/docs/rmf-tiny.md +156 -0
- package/package.json +4 -1
- package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +16 -1
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +15 -1
- package/client/android.html.deleted +0 -226
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# prod / dev release workflow — wrapper-package variant
|
|
2
|
+
|
|
3
|
+
Status: design only. Alternative to `prod.md` (dist-tag approach). Pick one.
|
|
4
|
+
|
|
5
|
+
## Shape
|
|
6
|
+
|
|
7
|
+
Today: one package `@bobfrankston/rmfmail` ships everything.
|
|
8
|
+
|
|
9
|
+
Proposed: split into two packages, three install names.
|
|
10
|
+
|
|
11
|
+
| Name | Role | Versioning |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `@bobfrankston/rmfmailapp` | The actual code (today's rmfmail content). | Bumps freely on every build. |
|
|
14
|
+
| `@bobfrankston/rmfmail` | Thin wrapper. `dependencies: { rmfmailapp: "1.0.NNN" }` pinned. | Bumps only on promotion. |
|
|
15
|
+
| `@bobfrankston/rmfmaildev` | Thin wrapper. `dependencies: { rmfmailapp: "*" }` (or repinned at every publish to current `@latest`). | Tracks rmfmailapp tip. |
|
|
16
|
+
|
|
17
|
+
Casual user: `npm install -g @bobfrankston/rmfmail` — pulls the wrapper, npm resolves the pinned rmfmailapp, installs both. The wrapper's `bin` shim invokes rmfmailapp's actual entry point.
|
|
18
|
+
|
|
19
|
+
Dev user: `npm install -g @bobfrankston/rmfmaildev` — wrapper that pulls `rmfmailapp@latest` (or its current `*`-resolved match).
|
|
20
|
+
|
|
21
|
+
Promotion = single git commit on the wrapper repo bumping its `dependencies.rmfmailapp` from one frozen version to a newer frozen version, plus `npm publish` of the wrapper.
|
|
22
|
+
|
|
23
|
+
## Why this rather than dist-tags
|
|
24
|
+
|
|
25
|
+
- **Audit trail in git.** The history of "what version was current prod when" is `git log app/wrapper/package.json`, not registry-only state.
|
|
26
|
+
- **Promotion is reversible by `git revert`.** Same as any code change.
|
|
27
|
+
- **Multi-channel scales.** `rmfmail-beta`, `rmfmail-rc`, etc. are just more wrappers, each pinned independently. dist-tags work too but become a bigger pile.
|
|
28
|
+
- **Pre-flight gating possible.** A CI check on the wrapper's package.json (lint, smoke test) can refuse promotions that name a version that hasn't been validated.
|
|
29
|
+
|
|
30
|
+
## Why NOT, vs dist-tags
|
|
31
|
+
|
|
32
|
+
- **Two packages to maintain** instead of one. Two `package.json`s, two `npm publish` calls per promotion.
|
|
33
|
+
- **One more rename.** Current `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp` (every workspace dep that references it has to flip too — not many in mailx, but it's a non-trivial rename).
|
|
34
|
+
- **npm cache pressure.** Each version of rmfmailapp lingers in users' npm caches; the wrapper resolves at install time, so old transitive resolutions can sit around. dist-tags don't multiply package versions.
|
|
35
|
+
|
|
36
|
+
## Layout
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
mailx/
|
|
40
|
+
rmfmailapp/ # was app/, the actual code
|
|
41
|
+
package.json → "name": "@bobfrankston/rmfmailapp"
|
|
42
|
+
bin/mailx.ts → unchanged
|
|
43
|
+
packages/ → unchanged
|
|
44
|
+
client/ → unchanged
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
rmfmail/ # NEW — wrapper for prod
|
|
48
|
+
package.json → see below
|
|
49
|
+
bin/rmfmail.js → 3-line shim
|
|
50
|
+
|
|
51
|
+
rmfmaildev/ # NEW — wrapper for dev
|
|
52
|
+
package.json → see below
|
|
53
|
+
bin/rmfmaildev.js → 3-line shim
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `rmfmail/package.json` (prod wrapper)
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"name": "@bobfrankston/rmfmail",
|
|
61
|
+
"version": "1.0.0",
|
|
62
|
+
"description": "Local-first email client (current stable)",
|
|
63
|
+
"type": "module",
|
|
64
|
+
"bin": { "rmfmail": "bin/rmfmail.js" },
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@bobfrankston/rmfmailapp": "1.0.609"
|
|
67
|
+
},
|
|
68
|
+
"license": "MIT"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Wrapper's own version (`1.0.0`) only bumps on shape changes (e.g., bin renamed, scope changed). Day-to-day promotions just re-pin the dependency line.
|
|
73
|
+
|
|
74
|
+
### `rmfmail/bin/rmfmail.js` (shim)
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
#!/usr/bin/env node
|
|
78
|
+
// Wrapper bin — defer to the pinned rmfmailapp entry point. Keeps argv
|
|
79
|
+
// and exit code identical so the wrapper is invisible at the CLI.
|
|
80
|
+
import { fileURLToPath } from "node:url";
|
|
81
|
+
import { createRequire } from "node:module";
|
|
82
|
+
const require = createRequire(import.meta.url);
|
|
83
|
+
const appBin = require.resolve("@bobfrankston/rmfmailapp/bin/mailx.js");
|
|
84
|
+
await import(`file://${appBin}`);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
(Or even just `require("@bobfrankston/rmfmailapp/bin/mailx.js")` if the rmfmailapp bin runs on `import`.)
|
|
88
|
+
|
|
89
|
+
### `rmfmaildev/package.json` (dev wrapper)
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"name": "@bobfrankston/rmfmaildev",
|
|
94
|
+
"version": "1.0.0",
|
|
95
|
+
"description": "Local-first email client (development tip)",
|
|
96
|
+
"type": "module",
|
|
97
|
+
"bin": { "rmfmail": "bin/rmfmaildev.js" },
|
|
98
|
+
"dependencies": {
|
|
99
|
+
"@bobfrankston/rmfmailapp": "*"
|
|
100
|
+
},
|
|
101
|
+
"license": "MIT"
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`*` means "any version" — npm picks the latest published. New `npm install -g @bobfrankston/rmfmaildev` always installs whatever rmfmailapp tip is at install time. Existing dev users run `npm update -g @bobfrankston/rmfmaildev` to refresh.
|
|
106
|
+
|
|
107
|
+
Alternative: `rmfmaildev` re-pins on every rmfmailapp publish (`prepublish` hook in rmfmailapp's CI bumps rmfmaildev's pin and publishes). Adds steps but gives dev installs the same lockstep behavior as prod.
|
|
108
|
+
|
|
109
|
+
### Workflow
|
|
110
|
+
|
|
111
|
+
**rebuild.cmd** (publishes rmfmailapp):
|
|
112
|
+
```cmd
|
|
113
|
+
cd rmfmailapp
|
|
114
|
+
npmglobalize
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
No tag flag needed (rmfmailapp's `@latest` is just "newest dev build"; rmfmail prod doesn't read from it directly).
|
|
118
|
+
|
|
119
|
+
**prod.cmd** (promote):
|
|
120
|
+
```cmd
|
|
121
|
+
@echo off
|
|
122
|
+
setlocal
|
|
123
|
+
set TARGET=%1
|
|
124
|
+
if "%TARGET%"=="" (
|
|
125
|
+
for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0rmfmailapp\package.json"') do set TARGET=%%~v
|
|
126
|
+
set TARGET=%TARGET: =%
|
|
127
|
+
set TARGET=%TARGET:"=%
|
|
128
|
+
)
|
|
129
|
+
echo Promoting rmfmail wrapper to pin rmfmailapp@%TARGET%...
|
|
130
|
+
|
|
131
|
+
rem Edit rmfmail/package.json's dependency line. 5-line node one-liner:
|
|
132
|
+
node -e "const p=require('./rmfmail/package.json'); p.dependencies['@bobfrankston/rmfmailapp']='%TARGET%'; require('fs').writeFileSync('./rmfmail/package.json', JSON.stringify(p,null,2)+'\n');"
|
|
133
|
+
|
|
134
|
+
rem Bump wrapper patch version (so npm sees a new wrapper to install).
|
|
135
|
+
cd rmfmail && npm version patch && cd ..
|
|
136
|
+
git add rmfmail/package.json
|
|
137
|
+
git commit -m "rmfmail: pin rmfmailapp@%TARGET%"
|
|
138
|
+
|
|
139
|
+
cd rmfmail
|
|
140
|
+
npm publish --access public
|
|
141
|
+
cd ..
|
|
142
|
+
echo Done. Casual users running 'npm install -g @bobfrankston/rmfmail' now get rmfmail@x.y.z → rmfmailapp@%TARGET%.
|
|
143
|
+
endlocal
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Casual install** (unchanged from today's user-facing command):
|
|
147
|
+
```
|
|
148
|
+
npm install -g @bobfrankston/rmfmail
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Dev install**:
|
|
152
|
+
```
|
|
153
|
+
npm install -g @bobfrankston/rmfmaildev
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Required code changes
|
|
157
|
+
|
|
158
|
+
### 1. Rename `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp`
|
|
159
|
+
|
|
160
|
+
- `app/package.json` `"name"` field.
|
|
161
|
+
- Every `dependencies` reference in workspace `packages/*/package.json`. Today they use `file:` paths so this might already be transparent — check.
|
|
162
|
+
- `bin/postinstall.js` — any hard-coded package name strings.
|
|
163
|
+
- `npmglobalize` config — no changes if it reads from package.json.
|
|
164
|
+
|
|
165
|
+
The rename is a one-time event. After it lands, no package.json edits in normal day-to-day work.
|
|
166
|
+
|
|
167
|
+
### 2. Create `rmfmail/` and `rmfmaildev/` directories
|
|
168
|
+
|
|
169
|
+
Top-level (not under `app/`). Each gets a 5-line `package.json` and a 3-line bin shim. ~30 lines total.
|
|
170
|
+
|
|
171
|
+
### 3. CI / build wiring
|
|
172
|
+
|
|
173
|
+
Today's `rebuild.cmd` builds and publishes app. Add:
|
|
174
|
+
- `prod.cmd` — promotes by bumping rmfmail's pin.
|
|
175
|
+
- Optionally automate rmfmaildev re-pin on every app publish (postpublish hook).
|
|
176
|
+
|
|
177
|
+
### 4. npmglobalize: no changes needed
|
|
178
|
+
|
|
179
|
+
The wrapper packages publish like any other. dist-tag flag work in `prod.md` is **not required** for this approach.
|
|
180
|
+
|
|
181
|
+
## What this does NOT do
|
|
182
|
+
|
|
183
|
+
- Does not enable side-by-side coexistence on a single machine — same as dist-tag plan, that's the orthogonal `RMFMAIL_PROFILE` env-var design.
|
|
184
|
+
- Does not eliminate user upgrades — users still need to re-run `npm install -g` (or rely on mailx's in-app updater) to pick up a new prod pin.
|
|
185
|
+
- Does not split testing or CI per channel — same code, same tests; only the promotion step gates the user-facing pin.
|
|
186
|
+
|
|
187
|
+
## Comparison to `prod.md`
|
|
188
|
+
|
|
189
|
+
| | `prod.md` (dist-tag) | `prod-wrapper.md` (this) |
|
|
190
|
+
|--|--|--|
|
|
191
|
+
| Number of npm packages | 1 (rmfmail) | 3 (rmfmailapp + rmfmail + rmfmaildev) |
|
|
192
|
+
| What "promotion" is | `npm dist-tag add` (registry call) | git commit + `npm publish` of wrapper |
|
|
193
|
+
| Promotion auditable in git | no | yes |
|
|
194
|
+
| Roll-back mechanism | retag earlier version | `git revert` then republish wrapper |
|
|
195
|
+
| Required npmglobalize change | yes (`--tag` flag) | no |
|
|
196
|
+
| Casual install command | `npm install -g @bobfrankston/rmfmail` | (same) |
|
|
197
|
+
| Dev install command | `... rmfmail@dev` | `... rmfmaildev` |
|
|
198
|
+
| One-time rename of code package | no | yes (rmfmail → rmfmailapp) |
|
|
199
|
+
| Multi-channel future (beta, rc, …) | more dist-tags | more wrappers |
|
|
200
|
+
|
|
201
|
+
## Recommendation
|
|
202
|
+
|
|
203
|
+
If you want:
|
|
204
|
+
- **Minimum mechanical change**: dist-tag (`prod.md`).
|
|
205
|
+
- **Maximum traceability**: wrapper (`prod-wrapper.md`).
|
|
206
|
+
- **Both eventually**: nothing stops doing dist-tag now and migrating to wrapper later. The dist-tag is a cheap throwaway; the wrapper is a structural commitment.
|
|
207
|
+
|
|
208
|
+
## Effort estimate
|
|
209
|
+
|
|
210
|
+
- One-time rename `rmfmail` → `rmfmailapp`: ~30 minutes (search-replace + verify, plus npm deprecate of old name to point users at new install path).
|
|
211
|
+
- `rmfmail/` + `rmfmaildev/` skeletons: ~15 minutes.
|
|
212
|
+
- `prod.cmd`: ~15 minutes.
|
|
213
|
+
- Test cycle: build app, publish, run prod.cmd, install in clean dir, verify it runs. ~30 minutes.
|
|
214
|
+
|
|
215
|
+
About 1.5 hours wall-clock. Bigger one-time investment than dist-tag (~15 minutes); smaller per-promotion friction (one git commit vs `npm dist-tag add ... prod && add ... latest`).
|
package/docs/prod.md
CHANGED
|
@@ -1,105 +1,224 @@
|
|
|
1
1
|
# prod / dev release workflow
|
|
2
2
|
|
|
3
|
-
Status: design
|
|
3
|
+
Status: design. Two viable approaches; pick one. Both deliver the same shape:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- `rebuild.cmd` publishes builds without affecting what casual users get.
|
|
6
|
+
- A separate `prod.cmd` step promotes a known-good build to be what casual users install.
|
|
7
|
+
- Casual users keep typing `npm install -g @bobfrankston/rmfmail` (no flags).
|
|
8
|
+
- Dev users explicitly opt in.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
## Key invariant (answers the obvious confusion)
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- **`@prod`** (and **`@latest`**) — what casual `npm install -g @bobfrankston/rmfmail` gets. Promoted manually from the current `@dev` tip when something has proved stable.
|
|
12
|
+
`@latest` is **what casual `npm install` gets**. It only moves when prod.cmd promotes. So between promotions:
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
```
|
|
15
|
+
@latest = @prod = 1.0.609 ← last promoted, what users get
|
|
16
|
+
@dev = 1.0.620 ← every rebuild bumps this
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Casual users keep installing 1.0.609 until you say so. Dev tip never accidentally becomes the casual install.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Approach A — npm dist-tags (one package)
|
|
24
|
+
|
|
25
|
+
### Mechanics
|
|
26
|
+
|
|
27
|
+
npm's built-in [dist-tag](https://docs.npmjs.com/cli/v10/commands/npm-dist-tag) feature: one package, multiple movable pointers.
|
|
13
28
|
|
|
14
|
-
|
|
29
|
+
- `npm publish --tag dev` sets `@dev` to the new version. `@latest` untouched.
|
|
30
|
+
- `npm dist-tag add @bobfrankston/rmfmail@<v> prod` moves the `prod` pointer; `npm dist-tag add ... latest` moves `latest` too. Two HTTP calls, no republish.
|
|
15
31
|
|
|
16
|
-
|
|
32
|
+
### Required changes
|
|
17
33
|
|
|
34
|
+
**1. npmglobalize** — accepts a `--dist-tag` flag and forwards to `npm publish`. ~5 lines. See `docs/npmglobalize-disttag.md` for the patch.
|
|
35
|
+
|
|
36
|
+
**2. rebuild.cmd** — invoke `npmglobalize --dist-tag dev`. One-line edit.
|
|
37
|
+
|
|
38
|
+
**3. prod.cmd** — new file. Explicit arg wins; otherwise query npm for the
|
|
39
|
+
current `@dev` and promote that. (`@dev` is the right default because you've
|
|
40
|
+
just published it via `rebuild.cmd` and want it to become user-facing.)
|
|
41
|
+
|
|
42
|
+
```cmd
|
|
43
|
+
@echo off
|
|
44
|
+
setlocal
|
|
45
|
+
set V=%1
|
|
46
|
+
if "%V%"=="" (
|
|
47
|
+
for /f "delims=" %%v in ('npm view @bobfrankston/rmfmail dist-tags.dev') do set V=%%v
|
|
48
|
+
)
|
|
49
|
+
if "%V%"=="" (
|
|
50
|
+
echo No version supplied and 'npm view ... dist-tags.dev' returned empty.
|
|
51
|
+
echo Usage: prod.cmd [version] e.g. prod.cmd 1.0.611
|
|
52
|
+
exit /b 1
|
|
53
|
+
)
|
|
54
|
+
echo Promoting @bobfrankston/rmfmail@%V% to prod and latest...
|
|
55
|
+
call npm dist-tag add @bobfrankston/rmfmail@%V% prod
|
|
56
|
+
call npm dist-tag add @bobfrankston/rmfmail@%V% latest
|
|
57
|
+
echo Done.
|
|
58
|
+
endlocal
|
|
18
59
|
```
|
|
19
|
-
# what rebuild.cmd should run (via npmglobalize):
|
|
20
|
-
npm publish <tarball> --tag dev # version exists, dev → it, latest unchanged
|
|
21
60
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
61
|
+
Roll back to an older known-good: `prod.cmd 1.0.598` — explicit arg path.
|
|
62
|
+
|
|
63
|
+
The "default = local package.json" alternative (if you never want to query
|
|
64
|
+
the registry):
|
|
65
|
+
|
|
66
|
+
```cmd
|
|
67
|
+
for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0app\package.json"') do set V=%%~v
|
|
68
|
+
set V=%V: =%
|
|
69
|
+
set V=%V:"=%
|
|
25
70
|
```
|
|
26
71
|
|
|
27
|
-
|
|
72
|
+
Use whichever default matches your workflow — the explicit-arg path works
|
|
73
|
+
either way.
|
|
28
74
|
|
|
29
|
-
|
|
75
|
+
### Workflow
|
|
30
76
|
|
|
31
|
-
|
|
77
|
+
1. Edit code, run `rebuild.cmd` — version bumps, publishes as `@dev`. `@latest`/`@prod` unaffected.
|
|
78
|
+
2. On the dev machine: `npm install -g @bobfrankston/rmfmail@dev` to grab the freshest build.
|
|
79
|
+
3. When something feels stable: run `prod.cmd`. Promotes the current `package.json` version to `@prod` and `@latest`.
|
|
80
|
+
4. On the prod machine (and any default install): `npm install -g @bobfrankston/rmfmail` (no tag) gets the newly-promoted version.
|
|
32
81
|
|
|
33
|
-
|
|
82
|
+
Roll back: `npm dist-tag add @bobfrankston/rmfmail@<earlier> prod` + `... latest`. No republish, no version bump.
|
|
83
|
+
|
|
84
|
+
### Effort
|
|
85
|
+
|
|
86
|
+
- npmglobalize change: ~5 lines, ~5 minutes.
|
|
87
|
+
- `rebuild.cmd`: one-line edit.
|
|
88
|
+
- `prod.cmd`: 15 lines, fresh file.
|
|
89
|
+
- Test cycle: ~10 minutes.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Approach B — wrapper packages
|
|
94
|
+
|
|
95
|
+
### Mechanics
|
|
96
|
+
|
|
97
|
+
Split the current single package into three:
|
|
98
|
+
|
|
99
|
+
| Name | Role | Versioning |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `@bobfrankston/rmfmailapp` | Today's actual code. | Bumps freely on every build. |
|
|
102
|
+
| `@bobfrankston/rmfmail` | Wrapper. `dependencies: { rmfmailapp: "1.0.609" }` pinned. | Bumps only on promotion. |
|
|
103
|
+
| `@bobfrankston/rmfmaildev` | Wrapper. `dependencies: { rmfmailapp: "*" }`. | Tracks rmfmailapp tip. |
|
|
104
|
+
|
|
105
|
+
Casual user: `npm install -g @bobfrankston/rmfmail` — installs the wrapper, which pulls in the pinned rmfmailapp via npm's normal dependency resolution. Wrapper's `bin` shim invokes rmfmailapp's actual entry point, so the CLI is identical.
|
|
106
|
+
|
|
107
|
+
Promotion = git commit on the wrapper bumping `dependencies.rmfmailapp` from one frozen version to another, plus `npm publish` of the wrapper.
|
|
108
|
+
|
|
109
|
+
### Layout
|
|
34
110
|
|
|
35
|
-
```
|
|
36
|
-
|
|
111
|
+
```
|
|
112
|
+
mailx/
|
|
113
|
+
rmfmailapp/ # was app/
|
|
114
|
+
package.json "name": "@bobfrankston/rmfmailapp"
|
|
115
|
+
bin/, packages/, client/ unchanged
|
|
116
|
+
rmfmail/ # NEW
|
|
117
|
+
package.json pins rmfmailapp version
|
|
118
|
+
bin/rmfmail.js shim that imports rmfmailapp's entry
|
|
119
|
+
rmfmaildev/ # NEW
|
|
120
|
+
package.json "*" range or always-repinned
|
|
121
|
+
bin/rmfmaildev.js shim
|
|
37
122
|
```
|
|
38
123
|
|
|
39
|
-
|
|
124
|
+
### `rmfmail/package.json`
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"name": "@bobfrankston/rmfmail",
|
|
129
|
+
"version": "1.0.0",
|
|
130
|
+
"type": "module",
|
|
131
|
+
"bin": { "rmfmail": "bin/rmfmail.js" },
|
|
132
|
+
"dependencies": { "@bobfrankston/rmfmailapp": "1.0.609" }
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `rmfmail/bin/rmfmail.js`
|
|
40
137
|
|
|
41
138
|
```js
|
|
42
|
-
|
|
43
|
-
|
|
139
|
+
#!/usr/bin/env node
|
|
140
|
+
import { createRequire } from "node:module";
|
|
141
|
+
const require = createRequire(import.meta.url);
|
|
142
|
+
await import(`file://${require.resolve("@bobfrankston/rmfmailapp/bin/mailx.js")}`);
|
|
44
143
|
```
|
|
45
144
|
|
|
46
|
-
|
|
145
|
+
### `rmfmaildev/package.json`
|
|
47
146
|
|
|
48
|
-
|
|
147
|
+
Same shape but `"@bobfrankston/rmfmailapp": "*"` so npm always resolves to the freshest published rmfmailapp.
|
|
49
148
|
|
|
50
|
-
|
|
149
|
+
### Required changes
|
|
51
150
|
|
|
52
|
-
|
|
53
|
-
npmglobalize --dist-tag dev
|
|
54
|
-
```
|
|
151
|
+
**1. Rename** `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp` in `app/package.json` and any workspace deps that reference it. One-time.
|
|
55
152
|
|
|
56
|
-
|
|
153
|
+
**2. Create** `rmfmail/` and `rmfmaildev/` directories with the package.json + shim listed above. ~30 lines total.
|
|
57
154
|
|
|
58
|
-
|
|
155
|
+
**3. prod.cmd** — bumps the pin and publishes the wrapper:
|
|
59
156
|
|
|
60
157
|
```cmd
|
|
61
158
|
@echo off
|
|
62
159
|
setlocal
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
160
|
+
set TARGET=%1
|
|
161
|
+
if "%TARGET%"=="" (
|
|
162
|
+
for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0rmfmailapp\package.json"') do set TARGET=%%~v
|
|
163
|
+
set TARGET=%TARGET: =%
|
|
164
|
+
set TARGET=%TARGET:"=%
|
|
165
|
+
)
|
|
166
|
+
node -e "const p=require('./rmfmail/package.json'); p.dependencies['@bobfrankston/rmfmailapp']='%TARGET%'; require('fs').writeFileSync('./rmfmail/package.json', JSON.stringify(p,null,2)+'\n');"
|
|
167
|
+
cd rmfmail
|
|
168
|
+
call npm version patch
|
|
169
|
+
call npm publish --access public
|
|
170
|
+
cd ..
|
|
171
|
+
git add rmfmail/package.json
|
|
172
|
+
git commit -m "rmfmail: pin rmfmailapp@%TARGET%"
|
|
70
173
|
endlocal
|
|
71
174
|
```
|
|
72
175
|
|
|
73
|
-
|
|
176
|
+
**4. npmglobalize**: no changes required.
|
|
74
177
|
|
|
75
|
-
###
|
|
178
|
+
### Workflow
|
|
76
179
|
|
|
77
|
-
|
|
180
|
+
1. Build, run `rebuild.cmd` — publishes rmfmailapp.
|
|
181
|
+
2. Dev install: `npm install -g @bobfrankston/rmfmaildev`. Pulls latest rmfmailapp through the `*` range.
|
|
182
|
+
3. Promotion: `prod.cmd 1.0.611`. Edits rmfmail's pin, bumps wrapper patch version, publishes wrapper, commits.
|
|
183
|
+
4. Casual install: `npm install -g @bobfrankston/rmfmail` resolves the new wrapper, npm fetches the pinned rmfmailapp, both land under the wrapper.
|
|
78
184
|
|
|
79
|
-
|
|
80
|
-
- Dev: `npm install -g @bobfrankston/rmfmail@dev` (current dev tip).
|
|
81
|
-
- Pin to a specific version: `npm install -g @bobfrankston/rmfmail@1.0.NNN`.
|
|
185
|
+
Roll back: edit rmfmail/package.json to a prior pin, `git revert`, republish wrapper.
|
|
82
186
|
|
|
83
|
-
|
|
187
|
+
### Effort
|
|
84
188
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
189
|
+
- One-time rename: ~30 minutes.
|
|
190
|
+
- Wrapper skeletons: ~15 minutes.
|
|
191
|
+
- prod.cmd: ~15 minutes.
|
|
192
|
+
- Test: ~30 minutes.
|
|
193
|
+
- Total: ~1.5 hours up-front, then near-zero per promotion.
|
|
89
194
|
|
|
90
|
-
|
|
195
|
+
---
|
|
91
196
|
|
|
92
|
-
##
|
|
197
|
+
## Comparison
|
|
93
198
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
199
|
+
| | A: dist-tags | B: wrappers |
|
|
200
|
+
|--|--|--|
|
|
201
|
+
| Number of npm packages | 1 | 3 (app + 2 wrappers) |
|
|
202
|
+
| What "promotion" is | `npm dist-tag add` (HTTP call) | git commit + `npm publish` of wrapper |
|
|
203
|
+
| Promotion in git history | no (registry-only) | yes |
|
|
204
|
+
| Roll-back | retag prior version | `git revert` then republish wrapper |
|
|
205
|
+
| npmglobalize change needed | yes (`--tag` flag, ~5 lines) | no |
|
|
206
|
+
| Casual install command | `npm install -g @bobfrankston/rmfmail` | (same) |
|
|
207
|
+
| Dev install command | `... rmfmail@dev` | `... rmfmaildev` |
|
|
208
|
+
| One-time setup work | ~15 minutes | ~1.5 hours |
|
|
209
|
+
| Per-promotion work | seconds (registry call) | minutes (commit + publish) |
|
|
210
|
+
| Multi-channel future (beta, rc) | more dist-tags | more wrappers |
|
|
97
211
|
|
|
98
|
-
##
|
|
212
|
+
## Recommendation
|
|
99
213
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
214
|
+
If you want it working today with minimum disturbance: **A (dist-tags)**.
|
|
215
|
+
|
|
216
|
+
If auditability of "what version was prod when" matters and you're willing to spend the one-time rename: **B (wrappers)**.
|
|
217
|
+
|
|
218
|
+
A → B migration later is straightforward (the rename and wrapper creation is the same work whenever you do it; dist-tags become a non-issue once wrappers exist).
|
|
219
|
+
|
|
220
|
+
## What neither approach does
|
|
104
221
|
|
|
105
|
-
|
|
222
|
+
- Does not enable side-by-side coexistence on a single machine — that's the orthogonal `RMFMAIL_PROFILE` env-var design (separate config dir, AUMID, mailto handler, etc.).
|
|
223
|
+
- Does not change the in-app updater. mailx's existing version-check flow keeps working with either.
|
|
224
|
+
- Does not affect Android. APKs aren't on npm — `prod-android.md` is the parallel document with the equivalent channel-manifest design.
|
package/docs/rmf-tiny.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# `rmf-tiny` — optional TinyMCE editor adapter for rmfmail
|
|
2
|
+
|
|
3
|
+
Status: design only. Not implemented.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Let users who want Thunderbird-class paste fidelity opt into TinyMCE without baking it into rmfmail itself. Bob 2026-05-09 verified: pasted a Word document into Thunderbird, tiptap, Quill, and TinyMCE — only TinyMCE preserved the formatting (boxed monospace block with borders, paragraph spacing, font preservation). Nothing else came close.
|
|
8
|
+
|
|
9
|
+
The license catch — TinyMCE is GPLv2 — means rmfmail (MIT) can't bundle it. The arms-length pattern is the resolution: rmfmail ships **no** TinyMCE bytes; a separate `rmf-tiny` package the user installs themselves provides the adapter + pulls in TinyMCE via npm.
|
|
10
|
+
|
|
11
|
+
## Shape
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
@bobfrankston/rmfmail ← MIT, ships no TinyMCE
|
|
15
|
+
client/compose/editor.ts ← MailxEditor interface + factory
|
|
16
|
+
gains a "tinymce" branch that
|
|
17
|
+
dynamically imports rmf-tiny
|
|
18
|
+
|
|
19
|
+
@bobfrankston/rmf-tiny ← MIT (the adapter glue is original code)
|
|
20
|
+
package.json ← peerDependency: tinymce
|
|
21
|
+
src/adapter.ts ← implements MailxEditor against TinyMCE
|
|
22
|
+
README.md ← install instructions
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
User opts in:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm install -g @bobfrankston/rmf-tiny tinymce
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
(Two packages explicit — keeps the licensing posture clean. rmf-tiny is just the adapter; tinymce comes from Tiny's own npm publication, not bundled with anything rmfmail-related.)
|
|
32
|
+
|
|
33
|
+
In rmfmail Settings → Compose → Editor: the dropdown grows a "TinyMCE (requires rmf-tiny)" option. If selected and the import fails, surface a status-bar error pointing at the install command.
|
|
34
|
+
|
|
35
|
+
## Why a separate package, not a flag inside rmfmail
|
|
36
|
+
|
|
37
|
+
- rmfmail's git history and npm tarball never contain TinyMCE → distribution layer is clean.
|
|
38
|
+
- rmf-tiny is small (~200 lines of adapter glue + a thin install README) and can be MIT — no GPL infection because it imports TinyMCE at runtime via the same kind of dynamic boundary npm packages always use. Combination at user's machine is fully GPL-permitted.
|
|
39
|
+
- Other editors can follow the same pattern (`rmf-ckeditor`, `rmf-prosemirror-pro`) without polluting rmfmail.
|
|
40
|
+
|
|
41
|
+
## Why call it `rmf-tiny`
|
|
42
|
+
|
|
43
|
+
Short, distinguishable from the upstream package, namespaces under your scope (`@bobfrankston/rmf-tiny`), and "tiny" is the upstream's own brand so the connection is obvious. Other naming options:
|
|
44
|
+
- `@bobfrankston/mailx-tinymce-adapter` (verbose, descriptive)
|
|
45
|
+
- `@bobfrankston/rmftiny` (no hyphen, matches rmfmail style)
|
|
46
|
+
|
|
47
|
+
`rmf-tiny` is fine; pick the form that scans cleanest in the install line.
|
|
48
|
+
|
|
49
|
+
## Files
|
|
50
|
+
|
|
51
|
+
### `rmf-tiny/package.json`
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "@bobfrankston/rmf-tiny",
|
|
56
|
+
"version": "0.1.0",
|
|
57
|
+
"description": "TinyMCE editor adapter for rmfmail. Bring-your-own TinyMCE.",
|
|
58
|
+
"type": "module",
|
|
59
|
+
"main": "src/adapter.js",
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"tinymce": ">=6"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"tinymce": { "optional": false }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`peerDependencies` puts the install responsibility on the user — `npm install @bobfrankston/rmf-tiny` warns that `tinymce` must also be installed; rmf-tiny does not vendor or bundle TinyMCE.
|
|
71
|
+
|
|
72
|
+
### `rmf-tiny/src/adapter.ts`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// MIT-licensed adapter glue. Imports the user-installed TinyMCE at
|
|
76
|
+
// runtime; doesn't redistribute any TinyMCE bytes. Implements the same
|
|
77
|
+
// `MailxEditor` interface rmfmail's createEditor factory expects, so
|
|
78
|
+
// the host code path is identical.
|
|
79
|
+
import type { Editor as TinyEditor } from "tinymce";
|
|
80
|
+
|
|
81
|
+
export interface MailxEditor {
|
|
82
|
+
setHtml(html: string): void;
|
|
83
|
+
getHtml(): string;
|
|
84
|
+
getText(): string;
|
|
85
|
+
focus(): void;
|
|
86
|
+
setCursor(pos: number): void;
|
|
87
|
+
root: HTMLElement;
|
|
88
|
+
// …other methods rmfmail's interface requires
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function createTinyMceEditor(container: HTMLElement): Promise<MailxEditor> {
|
|
92
|
+
// Dynamic import — fails clearly if the user hasn't installed tinymce.
|
|
93
|
+
const tinymce = (await import("tinymce")).default;
|
|
94
|
+
// Plus the plugins TinyMCE's paste-from-Word handler needs:
|
|
95
|
+
await Promise.all([
|
|
96
|
+
import("tinymce/themes/silver"),
|
|
97
|
+
import("tinymce/icons/default"),
|
|
98
|
+
import("tinymce/plugins/paste"),
|
|
99
|
+
import("tinymce/plugins/lists"),
|
|
100
|
+
import("tinymce/plugins/link"),
|
|
101
|
+
import("tinymce/plugins/table"),
|
|
102
|
+
import("tinymce/plugins/code"),
|
|
103
|
+
]);
|
|
104
|
+
// ... initialize tinymce against `container`, return MailxEditor shim.
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The actual init/wire-up is the work — wrapping TinyMCE's API in our `MailxEditor` shape, hooking paste events, setHtml/getHtml round-tripping, etc. Maybe ~200 lines.
|
|
109
|
+
|
|
110
|
+
### rmfmail-side change to `editor.ts`
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
export async function createEditor(
|
|
114
|
+
container: HTMLElement,
|
|
115
|
+
type: "quill" | "tiptap" | "tinymce"
|
|
116
|
+
): Promise<MailxEditor> {
|
|
117
|
+
if (type === "tinymce") {
|
|
118
|
+
try {
|
|
119
|
+
const m = await import("@bobfrankston/rmf-tiny");
|
|
120
|
+
return m.createTinyMceEditor(container);
|
|
121
|
+
} catch (e: any) {
|
|
122
|
+
const status = document.getElementById("status-sync");
|
|
123
|
+
if (status) status.textContent = `TinyMCE editor not available — install: npm install -g @bobfrankston/rmf-tiny tinymce`;
|
|
124
|
+
return createQuillEditor(container); // fall back so compose still works
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (type === "tiptap") return createTiptapEditor(container);
|
|
128
|
+
return createQuillEditor(container);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Settings panel adds the "TinyMCE" option in the editor dropdown.
|
|
133
|
+
|
|
134
|
+
## Effort
|
|
135
|
+
|
|
136
|
+
- `rmf-tiny` package skeleton (package.json, README, basic adapter shell): ~30 minutes.
|
|
137
|
+
- Adapter wiring (init, setHtml/getHtml, paste handling, toolbar config): ~3 hours including testing the Word-paste case.
|
|
138
|
+
- rmfmail-side factory branch + Settings dropdown option: ~30 minutes.
|
|
139
|
+
- Documentation for users (README in rmf-tiny + Settings tooltip): ~30 minutes.
|
|
140
|
+
|
|
141
|
+
About half a day total.
|
|
142
|
+
|
|
143
|
+
## Order
|
|
144
|
+
|
|
145
|
+
1. Build `rmf-tiny` against your local TinyMCE install. Verify the Word-paste case actually works through your adapter.
|
|
146
|
+
2. Add the factory branch and Settings option to rmfmail.
|
|
147
|
+
3. Publish `rmf-tiny` to npm.
|
|
148
|
+
4. Document the opt-in install command in rmfmail's Settings tooltip and `README.md`.
|
|
149
|
+
|
|
150
|
+
Don't publish the adapter until the Word-paste case actually works end-to-end through the adapter — TinyMCE direct is one thing, our adapter possibly mangles it differently.
|
|
151
|
+
|
|
152
|
+
## Caveats
|
|
153
|
+
|
|
154
|
+
- The adapter must not re-implement parts of TinyMCE — it's a thin wrapper. Re-implementing parts of GPL code in MIT is the legal trap; staying purely in adapter / interface territory is safe.
|
|
155
|
+
- TinyMCE's bundle is large (~1MB minified). Users opting in pay that cost on the first compose-window load. Fine for opt-in; would be unacceptable as a default.
|
|
156
|
+
- `rmf-tiny` major version should track TinyMCE's major version (rmf-tiny@7.x for tinymce@7.x) so peerDependency mismatches are obvious.
|