projview 0.5.2 → 0.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projview",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Ephemeral local previewer for markdown + mermaid files in any directory",
5
5
  "type": "module",
6
6
  "bin": {
package/src/search.js CHANGED
@@ -53,10 +53,15 @@ export async function buildIndex(root, store) {
53
53
  return { store, total: entries.length, reindexed, warm: !!cached };
54
54
  }
55
55
 
56
+ // Re-read one file into the in-memory index; return its store entry (or null).
56
57
  export async function updateFile(rel) {
57
- if (!theRoot) return;
58
- try { addToIndex(rel, await fsp.readFile(path.join(theRoot, rel), 'utf8')); }
59
- catch { /* gone between event and read - ignore */ }
58
+ if (!theRoot) return null;
59
+ const abs = path.join(theRoot, rel);
60
+ try {
61
+ const [content, stat] = await Promise.all([fsp.readFile(abs, 'utf8'), fsp.stat(abs)]);
62
+ addToIndex(rel, content);
63
+ return { path: rel, name: rel.split('/').pop(), ext: path.extname(rel).toLowerCase(), mtime: stat.mtimeMs, content };
64
+ } catch { return null; }
60
65
  }
61
66
 
62
67
  export function removeFile(rel) { index.delete(rel); }
package/src/server.js CHANGED
@@ -113,24 +113,33 @@ export async function startServer(root, { port = 4321, host = '127.0.0.1', store
113
113
  });
114
114
 
115
115
  // --- file watcher -> live reload ---
116
+ const store = createStore(storeConfig, root);
117
+ const indexInfo = await buildIndex(root, store); // load-or-build the search index
118
+
119
+ // file watcher -> keep the in-memory index, the persisted store, AND the
120
+ // browser in sync as files change during a session
116
121
  const watcher = chokidar.watch(root, {
117
122
  ignoreInitial: true,
118
123
  ignored: (p) => p.split(path.sep).some(isIgnoredDir)
119
124
  });
120
125
  const rel = (p) => path.relative(root, p).split(path.sep).join('/');
126
+ const touch = async (p, event) => {
127
+ if (!isPreviewable(p)) return;
128
+ const entry = await updateFile(rel(p)); // refresh in-memory index, get the entry
129
+ if (entry) await store.update(entry); // mirror the change into the store
130
+ broadcast(event, { path: rel(p) });
131
+ };
121
132
  watcher
122
- .on('change', (p) => {
133
+ .on('change', (p) => touch(p, 'change'))
134
+ .on('add', (p) => touch(p, 'tree'))
135
+ .on('unlink', async (p) => {
123
136
  if (!isPreviewable(p)) return;
124
- updateFile(rel(p));
125
- broadcast('change', { path: rel(p) });
137
+ removeFile(rel(p));
138
+ await store.remove(rel(p));
139
+ broadcast('tree');
126
140
  })
127
- .on('add', (p) => { if (isPreviewable(p)) { updateFile(rel(p)); broadcast('tree'); } })
128
- .on('unlink', (p) => { if (isPreviewable(p)) { removeFile(rel(p)); broadcast('tree'); } })
129
141
  .on('addDir', () => broadcast('tree'))
130
142
  .on('unlinkDir', () => broadcast('tree'));
131
-
132
- const store = createStore(storeConfig, root);
133
- const indexInfo = await buildIndex(root, store); // load-or-build the search index
134
143
  const listenPort = await listenWithRetry(server, port, host);
135
144
 
136
145
  const close = () => new Promise((resolve) => {
package/src/store.js CHANGED
@@ -78,6 +78,8 @@ function memoryStore() {
78
78
  kind: 'memory',
79
79
  async load() { return null; },
80
80
  async save() {},
81
+ async update() {},
82
+ async remove() {},
81
83
  async close() {},
82
84
  describe() { return 'in-memory (ephemeral - nothing persisted)'; }
83
85
  };
@@ -85,15 +87,29 @@ function memoryStore() {
85
87
 
86
88
  function superliteStore(root, persist) {
87
89
  const file = fileFor(root, persist, 'json');
90
+ let map = null; // path -> entry, lazily loaded
91
+ const ensure = async () => {
92
+ if (map) return map;
93
+ try {
94
+ const d = JSON.parse(await fsp.readFile(file, 'utf8'));
95
+ map = new Map((d.entries || []).map((e) => [e.path, e]));
96
+ } catch { map = new Map(); }
97
+ return map;
98
+ };
99
+ const flush = () => fsp.writeFile(file, JSON.stringify({ builtAt: Date.now(), entries: [...map.values()] }));
88
100
  return {
89
101
  kind: 'superlite',
90
102
  file,
91
103
  async load() {
92
- try { return JSON.parse(await fsp.readFile(file, 'utf8')); } catch { return null; }
104
+ const m = await ensure();
105
+ return m.size ? { entries: [...m.values()] } : null;
93
106
  },
94
107
  async save(entries) {
95
- await fsp.writeFile(file, JSON.stringify({ builtAt: Date.now(), entries }));
108
+ map = new Map(entries.map((e) => [e.path, e]));
109
+ await flush();
96
110
  },
111
+ async update(entry) { (await ensure()).set(entry.path, entry); await flush(); },
112
+ async remove(p) { (await ensure()).delete(p); await flush(); },
97
113
  async close({ keep } = {}) {
98
114
  if (!persist && !keep) { try { await fsp.unlink(file); } catch { /* gone */ } }
99
115
  },
@@ -127,6 +143,15 @@ function sqliteAt(file, { cleanup }) {
127
143
  for (const e of entries) ins.run(e.path, e.name, e.ext, e.mtime, e.content);
128
144
  d.exec('COMMIT');
129
145
  },
146
+ async update(entry) {
147
+ const d = await open();
148
+ d.prepare('INSERT OR REPLACE INTO files (path, name, ext, mtime, content) VALUES (?, ?, ?, ?, ?)')
149
+ .run(entry.path, entry.name, entry.ext, entry.mtime, entry.content);
150
+ },
151
+ async remove(p) {
152
+ const d = await open();
153
+ d.prepare('DELETE FROM files WHERE path = ?').run(p);
154
+ },
130
155
  async close({ keep } = {}) {
131
156
  if (db) { db.close(); db = null; }
132
157
  if (cleanup && !keep) { try { await fsp.unlink(file); } catch { /* gone */ } }
@@ -183,6 +208,18 @@ function postgresStore(url) {
183
208
  }
184
209
  await c.query('COMMIT');
185
210
  },
211
+ async update(entry) {
212
+ const c = await open();
213
+ await c.query(
214
+ 'INSERT INTO projview_files (path, name, ext, mtime, content) VALUES ($1, $2, $3, $4, $5)' +
215
+ ' ON CONFLICT (path) DO UPDATE SET name = $2, ext = $3, mtime = $4, content = $5',
216
+ [entry.path, entry.name, entry.ext, entry.mtime, entry.content]
217
+ );
218
+ },
219
+ async remove(p) {
220
+ const c = await open();
221
+ await c.query('DELETE FROM projview_files WHERE path = $1', [p]);
222
+ },
186
223
  async close() { if (client) { await client.end(); client = null; } }, // never drop the user's table
187
224
  describe() { return `custom/postgres ${redact(url)}`; }
188
225
  };