refactor(plugins): rename document-mode to doc-mode

Renames the bundled plugin's id (`document-mode` → `doc-mode`), display
name (`Document Mode (Alpha)` → `Doc Mode (Alpha)`), package name,
directory, and asset path. No migration: the plugin has never been
published, so there is no on-disk user data to preserve.

Touched everywhere the id was hardcoded:
- Plugin manifest, package.json, build/deploy/test scripts, log prefixes
- Host BUNDLED_PLUGIN_PATHS entry (src/app/plugins/plugin.service.ts)
- Host comments and test fixtures (plugin-hooks, plugin-persistence-key,
  plugin-persistence.model, conflict-resolution.service.spec)
- E2E spec filenames + PLUGIN_ID constant + label assertions
- build-all.js plugin orchestrator
- build/release-notes.md user-facing entry

Test pass: 88/88 plugin specs, 8/8 plugin-hooks, 132/132
conflict-resolution, 15/15 plugin-persistence-key.util.
Bundle redeployed to src/assets/bundled-plugins/doc-mode (gitignored).

The old `src/assets/bundled-plugins/document-mode/` dir is gitignored and
left on disk after this commit; the host loader no longer references it,
so it's inert. Remove with `rm -rf src/assets/bundled-plugins/document-mode`.
This commit is contained in:
Johannes Millan 2026-05-27 16:48:59 +02:00
parent 0df783a669
commit 3443bcacd0
32 changed files with 56 additions and 59 deletions

View file

@ -33,6 +33,6 @@ For all current downloads, package links, and platform-specific notes: [check th
### Plugins & Advanced
- Added plugin support for work-context header buttons, an embed slot, the WORK_CONTEXT_CHANGE hook, and iframe-only installs.
- Added a TipTap-based document-mode plugin and reduced redundant synced chip data.
- Added a TipTap-based doc-mode plugin and reduced redundant synced chip data.
- Added distribution-target suffixes to Electron version strings.
- Fixed SuperSync Caddy healthchecks, PostgreSQL connection headroom, Docker sync package inclusion, and the F-Droid build dependency issue.

View file

@ -5,9 +5,9 @@ import {
} from '../../helpers/plugin-test.helpers';
// Smoke test for the BUNDLED_PLUGIN_PATHS entry added in commit 199e816479.
// A typo or accidental removal of 'assets/bundled-plugins/document-mode'
// A typo or accidental removal of 'assets/bundled-plugins/doc-mode'
// would silently regress this; the test fails loudly instead.
test.describe('Document Mode bundled plugin', () => {
test.describe('Doc Mode bundled plugin', () => {
test('appears in plugin management list', async ({ page, workViewPage }) => {
test.setTimeout(60000);
@ -26,7 +26,7 @@ test.describe('Document Mode bundled plugin', () => {
const pluginReady = await waitForPluginManagementInit(page);
expect(pluginReady).toBeTruthy();
// Manifest declares the user-visible name as "Document Mode (Alpha)".
// Manifest declares the user-visible name as "Doc Mode (Alpha)".
// Match on the prefix so a future Alpha→Beta label change doesn't break.
const titles = await page.evaluate(() => {
const cards = Array.from(document.querySelectorAll('plugin-management mat-card'));
@ -35,7 +35,7 @@ test.describe('Document Mode bundled plugin', () => {
.filter(Boolean);
});
const hasDocumentMode = titles.some((t) => t.startsWith('Document Mode'));
expect(hasDocumentMode).toBeTruthy();
const hasDocMode = titles.some((t) => t.startsWith('Doc Mode'));
expect(hasDocMode).toBeTruthy();
});
});

View file

@ -7,8 +7,8 @@ import {
/**
* End-to-end coverage of the Stage A keyed persistence migration in the
* bundled document-mode plugin. The migration logic is unit-tested against
* a mock PluginAPI (`packages/plugin-dev/document-mode/src/persistence.spec.ts`),
* bundled doc-mode plugin. The migration logic is unit-tested against
* a mock PluginAPI (`packages/plugin-dev/doc-mode/src/persistence.spec.ts`),
* but those tests can't catch real-iframe quirks: postMessage handling of
* `undefined` second args, commit-chain timing across the host's rate
* limiter, hydration ordering against the op-log.
@ -21,7 +21,7 @@ import {
* `doc:${ctxId}` keyed entries and tombstones the legacy id.
*/
const PLUGIN_ID = 'document-mode';
const PLUGIN_ID = 'doc-mode';
// Underscore-cased so the @typescript-eslint/naming-convention rule on
// object literal keys is happy. The keys are opaque context ids; their
// shape doesn't matter to the host.
@ -79,7 +79,7 @@ const findEntry = (
id: string,
): PluginUserDataEntry | undefined => entries?.find((e) => e.id === id);
test.describe('Document Mode Stage A migration', () => {
test.describe('Doc Mode Stage A migration', () => {
test('stamps migration success on a fresh install', async ({ page, workViewPage }) => {
test.setTimeout(60000);
@ -96,7 +96,7 @@ test.describe('Document Mode Stage A migration', () => {
const pluginReady = await waitForPluginManagementInit(page);
expect(pluginReady).toBeTruthy();
const enabled = await enablePluginWithVerification(page, 'Document Mode');
const enabled = await enablePluginWithVerification(page, 'Doc Mode');
expect(enabled).toBeTruthy();
// The migration runs once the plugin's background.js executes init().
@ -169,7 +169,7 @@ test.describe('Document Mode Stage A migration', () => {
const pluginReady = await waitForPluginManagementInit(page);
expect(pluginReady).toBeTruthy();
const enabled = await enablePluginWithVerification(page, 'Document Mode');
const enabled = await enablePluginWithVerification(page, 'Doc Mode');
expect(enabled).toBeTruthy();
// Wait for the migration to stamp success. The full sequence is:

View file

@ -1,11 +1,11 @@
{
"name": "document-mode",
"name": "doc-mode",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "document-mode",
"name": "doc-mode",
"version": "0.1.0",
"dependencies": {
"@super-productivity/plugin-api": "file:../../plugin-api",

View file

@ -1,5 +1,5 @@
{
"name": "document-mode",
"name": "doc-mode",
"version": "0.1.0",
"description": "Document-style editor for projects and Today using TipTap",
"private": true,

View file

@ -9,7 +9,7 @@ const SRC_DIR = path.join(ROOT_DIR, 'src');
const DIST_DIR = path.join(ROOT_DIR, 'dist');
async function buildPlugin() {
console.log('Building document-mode plugin with esbuild...');
console.log('Building doc-mode plugin with esbuild...');
if (fs.existsSync(DIST_DIR)) {
fs.rmSync(DIST_DIR, { recursive: true });

View file

@ -13,7 +13,7 @@ const TARGET_DIR = path.join(
'src',
'assets',
'bundled-plugins',
'document-mode',
'doc-mode',
);
// editor.js is inlined into index.html, so it doesn't need to be deployed.

View file

@ -1,7 +1,7 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-require-imports */
/**
* Unit-test runner for the document-mode plugin.
* Unit-test runner for the doc-mode plugin.
*
* The plugin has no browser-based test setup its testable logic is the pure
* `doc-transform` module. This script transpiles every `*.spec.ts` under

View file

@ -54,7 +54,7 @@ const init = async (): Promise<void> => {
try {
await migrateToKeyedPersistence(PluginAPI);
} catch (err) {
PluginAPI.log.err('document-mode: migration failed', err);
PluginAPI.log.err('doc-mode: migration failed', err);
// Continue anyway: a partial migration leaves the data in place
// (corruption guard in persistence.ts) and the next session will retry.
}
@ -94,12 +94,12 @@ const onButtonClick = async (ctx: ActiveWorkContext): Promise<void> => {
return [...set];
});
} catch (err) {
PluginAPI.log.err('document-mode: failed to persist toggle', err);
PluginAPI.log.err('doc-mode: failed to persist toggle', err);
}
};
PluginAPI.registerWorkContextHeaderButton({
label: 'Document Mode',
label: 'Doc Mode',
icon: 'description',
showFor: ['PROJECT', 'TODAY'],
onClick: (ctx) => {

View file

@ -1,5 +1,5 @@
/**
* Pure document-transform helpers for Document Mode.
* Pure document-transform helpers for Doc Mode.
*
* Everything here operates on plain ProseMirror-JSON (`PMNode`) and a task
* lookup function no TipTap editor, no DOM, no module-global cache. That

View file

Before

Width:  |  Height:  |  Size: 359 B

After

Width:  |  Height:  |  Size: 359 B

Before After
Before After

View file

@ -1,7 +1,7 @@
{
"id": "document-mode",
"id": "doc-mode",
"manifestVersion": 1,
"name": "Document Mode (Alpha)",
"name": "Doc Mode (Alpha)",
"version": "0.1.0",
"description": "Document-style editor for projects and Today, powered by TipTap.",
"author": "SuperProductivity",

View file

@ -48,7 +48,7 @@ export const reconcileEnabledIds = async (
try {
next = new Set(await loadEnabledCtxIds(api));
} catch (err) {
api.log.err('document-mode: enabled-ids reload failed', err);
api.log.err('doc-mode: enabled-ids reload failed', err);
return { next: new Set(prev), action: 'noop' };
}
// Skip the visibility reconcile when membership is identical — most fires
@ -69,7 +69,7 @@ export const reconcileEnabledIds = async (
const ctx = await api.getActiveWorkContext();
activeId = ctx?.id ?? null;
} catch (err) {
api.log.err('document-mode: active-ctx read failed', err);
api.log.err('doc-mode: active-ctx read failed', err);
return { next, action: 'noop' };
}
if (activeId === null) return { next, action: 'noop' };

View file

@ -1,5 +1,5 @@
/**
* Pure top-level-navigation helpers for the Document Mode editor.
* Pure top-level-navigation helpers for the Doc Mode editor.
*
* Every function here operates on a `DocLike` the minimal structural
* slice of a ProseMirror document node that the drag / move / keyboard

View file

@ -113,7 +113,7 @@ const logErr = (msg: string, err?: unknown): void => {
// ignore — fall through to console
}
// eslint-disable-next-line no-console
console.error('[document-mode]', msg, err);
console.error('[doc-mode]', msg, err);
};
/**
@ -1910,7 +1910,7 @@ const mount = async (): Promise<void> => {
try {
await migrateToKeyedPersistence(PluginAPI);
} catch (err) {
logErr('document-mode: migration failed', err);
logErr('doc-mode: migration failed', err);
}
updateDocStatusBanner();
const initialCtx = await PluginAPI.getActiveWorkContext();
@ -2149,7 +2149,7 @@ const waitForPluginAPI = (): Promise<void> =>
if (attempts >= MAX_ATTEMPTS) {
// eslint-disable-next-line no-console
console.error(
'[document-mode] PluginAPI not injected after',
'[doc-mode] PluginAPI not injected after',
MAX_ATTEMPTS * INTERVAL_MS,
'ms — giving up',
);
@ -2173,7 +2173,7 @@ void waitForPluginAPI()
const msg = document.createElement('div');
msg.className = 'doc-error-state';
msg.textContent =
'Document Mode could not connect to Super Productivity. ' +
'Doc Mode could not connect to Super Productivity. ' +
'Try closing and reopening this panel.';
root.appendChild(msg);
}

View file

@ -1,5 +1,5 @@
/**
* Inline SVG icons for Document Mode. Kept as a static path map so the
* Inline SVG icons for Doc Mode. Kept as a static path map so the
* iframe renders icons offline no Material Icons web font, no Google
* Fonts request. Paths are the standard Material Symbols 24×24 outlines.
*/

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Document Mode</title>
<title>Doc Mode</title>
<style>
html,
body {

View file

@ -347,15 +347,15 @@ const plugins = [
},
},
{
name: 'document-mode',
path: 'document-mode',
name: 'doc-mode',
path: 'doc-mode',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
await execAsync(`cd ${pluginPath} && npm run build`);
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/document-mode',
'../../../src/assets/bundled-plugins/doc-mode',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });

View file

@ -1151,7 +1151,7 @@ describe('ConflictResolutionService', () => {
const conflicts: EntityConflict[] = [
{
entityType: 'PLUGIN_USER_DATA',
entityId: 'document-mode',
entityId: 'doc-mode',
localOps: [
{
id: 'local-plugin-data',
@ -1159,8 +1159,8 @@ describe('ConflictResolutionService', () => {
actionType: '[Plugin] Upsert User Data' as ActionType,
opType: OpType.Update,
entityType: 'PLUGIN_USER_DATA',
entityId: 'document-mode',
payload: { id: 'document-mode', data: 'local-blob' },
entityId: 'doc-mode',
payload: { id: 'doc-mode', data: 'local-blob' },
vectorClock: { clientA: 1 },
timestamp: now - 500,
schemaVersion: 1,
@ -1173,8 +1173,8 @@ describe('ConflictResolutionService', () => {
actionType: '[Plugin] Upsert User Data' as ActionType,
opType: OpType.Update,
entityType: 'PLUGIN_USER_DATA',
entityId: 'document-mode',
payload: { id: 'document-mode', data: 'remote-blob' },
entityId: 'doc-mode',
payload: { id: 'doc-mode', data: 'remote-blob' },
vectorClock: { clientB: 1 },
timestamp: now,
schemaVersion: 1,

View file

@ -110,27 +110,24 @@ describe('PluginHooksService.dispatchHookToPlugin', () => {
});
it('fires every handler when a plugin registers more than once for the same hook', async () => {
// document-mode registers from both its background script and its iframe
// doc-mode registers from both its background script and its iframe
// editor under the same pluginId — pre-fix the second registration
// silently overwrote the first, so iframe registration permanently
// shadowed background's enabledIds-reconcile handler.
const bgHandler = jasmine.createSpy('bgHandler');
const iframeHandler = jasmine.createSpy('iframeHandler');
service.registerHookHandler(
'document-mode',
'doc-mode',
PluginHooks.PERSISTED_DATA_CHANGED,
bgHandler,
);
service.registerHookHandler(
'document-mode',
'doc-mode',
PluginHooks.PERSISTED_DATA_CHANGED,
iframeHandler,
);
await service.dispatchHookToPlugin(
'document-mode',
PluginHooks.PERSISTED_DATA_CHANGED,
);
await service.dispatchHookToPlugin('doc-mode', PluginHooks.PERSISTED_DATA_CHANGED);
expect(bgHandler).toHaveBeenCalledTimes(1);
expect(iframeHandler).toHaveBeenCalledTimes(1);
@ -143,23 +140,23 @@ describe('PluginHooksService.dispatchHookToPlugin', () => {
const otherPlugin = jasmine.createSpy('otherPlugin');
service.registerHookHandler(
'document-mode',
'doc-mode',
PluginHooks.PERSISTED_DATA_CHANGED,
dataChanged,
);
service.registerHookHandler(
'document-mode',
'doc-mode',
PluginHooks.PERSISTED_DATA_CHANGED,
dataChanged2,
);
service.registerHookHandler('document-mode', PluginHooks.TASK_COMPLETE, taskComplete);
// Sibling plugin's handler must survive a teardown targeting document-mode.
service.registerHookHandler('doc-mode', PluginHooks.TASK_COMPLETE, taskComplete);
// Sibling plugin's handler must survive a teardown targeting doc-mode.
service.registerHookHandler('other', PluginHooks.TASK_COMPLETE, otherPlugin);
service.unregisterPluginHooks('document-mode');
service.unregisterPluginHooks('doc-mode');
return Promise.all([
service.dispatchHookToPlugin('document-mode', PluginHooks.PERSISTED_DATA_CHANGED),
service.dispatchHookToPlugin('doc-mode', PluginHooks.PERSISTED_DATA_CHANGED),
service.dispatchHook(PluginHooks.TASK_COMPLETE),
]).then(() => {
expect(dataChanged).not.toHaveBeenCalled();

View file

@ -7,7 +7,7 @@ import { PluginLog } from '../core/log';
* Each handler has a 5s timeout to prevent hung handlers from blocking others.
*
* A plugin may register multiple handlers for the same hook under one
* pluginId for example, document-mode registers from both its background
* pluginId for example, doc-mode registers from both its background
* script (host page) and its iframe editor. All registered handlers fire on
* dispatch.
*/

View file

@ -2,7 +2,7 @@
* Maximum size of a single plugin-persistence write, in bytes (256 KB).
*
* Sized for the realistic upper bound of plugin payloads a heavy
* document-mode TipTap doc is ~30100 KB, plugin configs are KB-scale,
* doc-mode TipTap doc is ~30100 KB, plugin configs are KB-scale,
* automation rules / AI prompts are KB-scale. 256 KB gives several×
* headroom while keeping the per-plugin storage growth bounded:
* a misbehaving plugin with N keyed entries can still hold N × 256 KB,
@ -11,7 +11,7 @@
* The pre-Stage-A cap was 1 MB because one blob held every context's
* data; with the keyed split each context gets its own entry, so the
* per-write budget can shrink. Legacy migrations skip individual docs
* that exceed this (see `document-mode/src/persistence.ts`).
* that exceed this (see `doc-mode/src/persistence.ts`).
*/
export const MAX_PLUGIN_DATA_SIZE = 256 * 1024; // 256 KB

View file

@ -57,7 +57,7 @@ const BUNDLED_PLUGIN_PATHS = [
'assets/bundled-plugins/voice-reminder',
'assets/bundled-plugins/google-calendar-provider',
'assets/bundled-plugins/caldav-calendar-provider',
'assets/bundled-plugins/document-mode',
'assets/bundled-plugins/doc-mode',
] as const;
@Injectable({

View file

@ -45,7 +45,7 @@ export const extractOwnerPluginId = (entityId: string): string => {
/**
* Bound on a single plugin's persistence key length. Generous for any
* realistic per-plugin keyspace (e.g. document-mode uses `doc:<uuid>`,
* realistic per-plugin keyspace (e.g. doc-mode uses `doc:<uuid>`,
* well under 100 chars). Prevents a compromised iframe from passing a
* multi-megabyte `key` that would be stored verbatim in NgRx state,
* IndexedDB, the op-log, and on the sync wire bypassing the