projview 0.5.1 → 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/docs/how-to-set-up.md +2 -1
- package/package.json +1 -1
- package/src/search.js +8 -3
- package/src/server.js +17 -8
- package/src/store.js +39 -2
package/docs/how-to-set-up.md
CHANGED
|
@@ -14,7 +14,8 @@ previewer for markdown + mermaid files.
|
|
|
14
14
|
| Node.js | **18+** |
|
|
15
15
|
| npm | ships with Node |
|
|
16
16
|
|
|
17
|
-
No build step
|
|
17
|
+
No build step. By default indexing is in-memory and nothing is written to disk -
|
|
18
|
+
you can opt into a persistent store (sqlite / JSON / your own DB) below.
|
|
18
19
|
|
|
19
20
|
## pick your path
|
|
20
21
|
|
package/package.json
CHANGED
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
104
|
+
const m = await ensure();
|
|
105
|
+
return m.size ? { entries: [...m.values()] } : null;
|
|
93
106
|
},
|
|
94
107
|
async save(entries) {
|
|
95
|
-
|
|
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
|
};
|