@lerret/cli 0.1.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/LICENSE +21 -0
- package/dist-studio/.bundle-stamp +34 -0
- package/dist-studio/assets/asset-runtime-MFjDKvQD.js +129 -0
- package/dist-studio/assets/cli-project-source-9dNA_gVa.js +1 -0
- package/dist-studio/assets/dev-harness-BH6a8T7l.js +18 -0
- package/dist-studio/assets/hosted-project-source-dVGq_8c6.js +135 -0
- package/dist-studio/assets/index-BNmJ8c2t.css +1 -0
- package/dist-studio/assets/index-EslqdOhg.js +10 -0
- package/dist-studio/assets/leaf-marker-command.png +0 -0
- package/dist-studio/assets/leaf-marker-comment-box.png +0 -0
- package/dist-studio/assets/leaf-marker-homescreen.png +0 -0
- package/dist-studio/assets/leafmarker-icon-dark-128.png +0 -0
- package/dist-studio/assets/leafmarker-logo-transparent.png +0 -0
- package/dist-studio/assets/leafmarker-logo.png +0 -0
- package/dist-studio/assets/lerret-logo.png +0 -0
- package/dist-studio/assets/lerret-wordmark.svg +3 -0
- package/dist-studio/assets/logo-angular.svg +1 -0
- package/dist-studio/assets/logo-claude.svg +7 -0
- package/dist-studio/assets/logo-codex.svg +1 -0
- package/dist-studio/assets/logo-cursor.svg +1 -0
- package/dist-studio/assets/logo-javascript.svg +1 -0
- package/dist-studio/assets/logo-react.svg +1 -0
- package/dist-studio/assets/logo-svelte.svg +1 -0
- package/dist-studio/assets/logo-vue.svg +1 -0
- package/dist-studio/assets/open-folder-D5OR7eLb.js +8 -0
- package/dist-studio/assets/project-studio-BjNaIuRb.js +795 -0
- package/dist-studio/assets/project-studio-CKuMOMsC.css +1 -0
- package/dist-studio/assets/superwhisper-logo.png +0 -0
- package/dist-studio/index.html +47 -0
- package/dist-studio/module-sw.js +275 -0
- package/package.json +51 -0
- package/src/dev.js +373 -0
- package/src/export.js +1386 -0
- package/src/fs/node-backend.js +631 -0
- package/src/lerret.js +143 -0
- package/src/resolve-project.js +178 -0
- package/src/vite-plugin-lerret-project.js +986 -0
- package/src/watcher.js +214 -0
package/src/watcher.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// watcher.js — the CLI-mode file watcher that turns native `fs` events under
|
|
2
|
+
// a project's `.lerret/` tree into the architecture's normalized
|
|
3
|
+
// {@link WatchEvent}s (AR5).
|
|
4
|
+
//
|
|
5
|
+
// The Node `fs.watch` API on its own is famously coarse and platform-
|
|
6
|
+
// dependent — it does not reliably distinguish `add` / `change` / `remove`,
|
|
7
|
+
// and on macOS its `recursive` mode is half-implemented. So this watcher
|
|
8
|
+
// wraps **chokidar**, the de-facto standard wrapper over `fs.watch` (the same
|
|
9
|
+
// library Vite itself uses). Chokidar gives us per-platform `add` / `change`
|
|
10
|
+
// / `unlink` events and the `addDir` / `unlinkDir` pair for folders, with
|
|
11
|
+
// known/consistent behavior on macOS, Linux, and Windows. We translate that
|
|
12
|
+
// straight into the model's normalized `{ type, path }` shape.
|
|
13
|
+
//
|
|
14
|
+
// THIS file is the only place in `@lerret/cli` allowed to import `chokidar`.
|
|
15
|
+
// `core` stays environment-agnostic (no Node built-ins); the loader's pure
|
|
16
|
+
// `applyWatchEvent` patches the project model off the events this file emits.
|
|
17
|
+
//
|
|
18
|
+
// Path discipline: every emitted `WatchEvent.path` is a {@link LerretPath} —
|
|
19
|
+
// forward-slash, no trailing slash. Chokidar may report native separators on
|
|
20
|
+
// Windows (it normalizes most of the time but not always), so this file does
|
|
21
|
+
// the conversion at the boundary, exactly like the Node fs backend.
|
|
22
|
+
|
|
23
|
+
import { sep as nativeSep } from 'node:path';
|
|
24
|
+
|
|
25
|
+
import chokidar from 'chokidar';
|
|
26
|
+
|
|
27
|
+
import { makeWatchEvent, watchEventType } from '@lerret/core';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {import('@lerret/core').WatchEvent} WatchEvent
|
|
31
|
+
* @typedef {import('@lerret/core').LerretPath} LerretPath
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert a native OS path into a contract-level {@link LerretPath}
|
|
36
|
+
* (forward slashes, no trailing slash).
|
|
37
|
+
*
|
|
38
|
+
* @param {string} nativePath
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function toLerretPath(nativePath) {
|
|
42
|
+
const slashed = nativeSep === '/' ? nativePath : nativePath.replaceAll(nativeSep, '/');
|
|
43
|
+
// Strip a trailing slash so `/a/b/` and `/a/b` are the same path. A bare
|
|
44
|
+
// root `'/'` is left alone — that case never reaches the watcher (the scan
|
|
45
|
+
// root is always a `.lerret/` directory).
|
|
46
|
+
return slashed.length > 1 ? slashed.replace(/\/+$/, '') : slashed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert a contract-level {@link LerretPath} into a path the host OS
|
|
51
|
+
* understands — used when handing the scan root to chokidar.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} lerretPath
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function toNativePath(lerretPath) {
|
|
57
|
+
return nativeSep === '/' ? lerretPath : lerretPath.replaceAll('/', nativeSep);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Chokidar event names → normalized `WatchEventType`. Folder add/remove
|
|
62
|
+
* (`addDir` / `unlinkDir`) and file add/remove map onto the same `add`/
|
|
63
|
+
* `remove` semantics — the loader's patcher uses path classification to
|
|
64
|
+
* decide whether the event is for a page, group, asset, or font.
|
|
65
|
+
*
|
|
66
|
+
* @type {Readonly<Record<string, import('@lerret/core').watchEventType[keyof typeof import('@lerret/core').watchEventType]>>}
|
|
67
|
+
*/
|
|
68
|
+
const CHOKIDAR_TO_TYPE = Object.freeze({
|
|
69
|
+
add: watchEventType.ADD,
|
|
70
|
+
addDir: watchEventType.ADD,
|
|
71
|
+
change: watchEventType.CHANGE,
|
|
72
|
+
unlink: watchEventType.REMOVE,
|
|
73
|
+
unlinkDir: watchEventType.REMOVE,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Optional handler invoked when chokidar's underlying watch errors. Stays
|
|
78
|
+
* non-fatal — the watcher logs and keeps running rather than tearing down
|
|
79
|
+
* the live-edit loop on a transient `fs` error.
|
|
80
|
+
*
|
|
81
|
+
* @callback WatcherErrorHandler
|
|
82
|
+
* @param {Error} err
|
|
83
|
+
* @returns {void}
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The handle returned by {@link startWatcher}. Calling `close()` stops the
|
|
88
|
+
* underlying chokidar watcher and releases the OS resources. Idempotent.
|
|
89
|
+
*
|
|
90
|
+
* @typedef {object} WatcherHandle
|
|
91
|
+
* @property {() => Promise<void>} close
|
|
92
|
+
* Stop watching. Resolves once chokidar has fully closed. Safe to call
|
|
93
|
+
* more than once.
|
|
94
|
+
* @property {Promise<void>} ready
|
|
95
|
+
* Resolves once chokidar has done its initial scan and reported `'ready'`
|
|
96
|
+
* — the watcher is now genuinely live. The `ignoreInitial: true` option
|
|
97
|
+
* means no `add` events fire for files that already existed at start;
|
|
98
|
+
* `await handle.ready` lets a caller (or a test) wait for that quiescent
|
|
99
|
+
* point before triggering edits.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Begin watching a project's `.lerret/` directory for changes, emitting one
|
|
104
|
+
* normalized {@link WatchEvent} per filesystem change.
|
|
105
|
+
*
|
|
106
|
+
* Configuration is deliberately conservative:
|
|
107
|
+
*
|
|
108
|
+
* - `ignoreInitial: true` — chokidar does not emit `add` for files that
|
|
109
|
+
* already existed at start. The initial project state is the loader's job
|
|
110
|
+
* (`scan`), not the watcher's; the watcher only reports CHANGES from that
|
|
111
|
+
* baseline. (Without this we would re-feed every file at boot as `add`,
|
|
112
|
+
* doubling the work of the initial scan.)
|
|
113
|
+
* - `awaitWriteFinish` — debounces a save so the editor's "write the new
|
|
114
|
+
* bytes, then truncate, then close" sequence (which can fire multiple
|
|
115
|
+
* `change` events) yields exactly one. The values are short enough to
|
|
116
|
+
* stay well inside the 1-second NFR2 budget while still coalescing.
|
|
117
|
+
* - No `ignored` patterns by default — the loader's path classification
|
|
118
|
+
* filters out the irrelevant paths (config.json, images, anything under a
|
|
119
|
+
* reserved folder). Watching everything keeps the watcher dumb and the
|
|
120
|
+
* model the single source of mapping rules.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} opts
|
|
123
|
+
* @param {LerretPath} opts.root
|
|
124
|
+
* The project's scan root — the `.lerret/` directory. Same path the
|
|
125
|
+
* loader scanned.
|
|
126
|
+
* @param {(event: WatchEvent) => void} opts.onEvent
|
|
127
|
+
* Called once per normalized event. Receives the validated `WatchEvent`;
|
|
128
|
+
* callers feed it straight to `applyWatchEvent` and re-render off the new
|
|
129
|
+
* model.
|
|
130
|
+
* @param {WatcherErrorHandler} [opts.onError]
|
|
131
|
+
* Optional non-fatal error handler. Defaults to `console.error`.
|
|
132
|
+
* @param {object} [opts.options]
|
|
133
|
+
* Pass-through overrides for chokidar's option bag — exposed for tests
|
|
134
|
+
* that need to tune timing. Merged on top of the defaults.
|
|
135
|
+
* @returns {WatcherHandle}
|
|
136
|
+
*/
|
|
137
|
+
export function startWatcher({ root, onEvent, onError, options = {} }) {
|
|
138
|
+
if (typeof root !== 'string' || root.length === 0) {
|
|
139
|
+
throw new TypeError('startWatcher: root must be a non-empty LerretPath string');
|
|
140
|
+
}
|
|
141
|
+
if (typeof onEvent !== 'function') {
|
|
142
|
+
throw new TypeError('startWatcher: onEvent must be a function');
|
|
143
|
+
}
|
|
144
|
+
const errorHandler =
|
|
145
|
+
typeof onError === 'function' ? onError : (err) => console.error('[watcher]', err);
|
|
146
|
+
|
|
147
|
+
// Chokidar takes a glob-like or path string; we pass the native form so it
|
|
148
|
+
// does its own native-`fs` work without re-translating.
|
|
149
|
+
const watcher = chokidar.watch(toNativePath(root), {
|
|
150
|
+
// Don't fire `add` events for the initial state — the loader already
|
|
151
|
+
// built the model from `scan()`.
|
|
152
|
+
ignoreInitial: true,
|
|
153
|
+
// Coalesce a save's multiple writes into one `change` event.
|
|
154
|
+
awaitWriteFinish: {
|
|
155
|
+
stabilityThreshold: 80,
|
|
156
|
+
pollInterval: 20,
|
|
157
|
+
},
|
|
158
|
+
// Keep symlink quirkiness out of the live-edit loop — match the loader,
|
|
159
|
+
// which never follows symlinks either.
|
|
160
|
+
followSymlinks: false,
|
|
161
|
+
// Always include subdirectories; the watcher is for the whole `.lerret/`.
|
|
162
|
+
depth: undefined,
|
|
163
|
+
// Chokidar normally pre-loads stats. Disabling it shortens initial start
|
|
164
|
+
// on bigger projects; the per-event payload doesn't depend on stat info.
|
|
165
|
+
alwaysStat: false,
|
|
166
|
+
...options,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// `ready` resolves once chokidar has done its initial silent walk. Tests
|
|
170
|
+
// await this before triggering edits so the watcher is genuinely live.
|
|
171
|
+
/** @type {(value: void) => void} */
|
|
172
|
+
let resolveReady;
|
|
173
|
+
/** @type {(reason: unknown) => void} */
|
|
174
|
+
let rejectReady;
|
|
175
|
+
const ready = new Promise((resolve, reject) => {
|
|
176
|
+
resolveReady = resolve;
|
|
177
|
+
rejectReady = reject;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Forward each chokidar event as a normalized WatchEvent.
|
|
181
|
+
watcher.on('all', (chokidarEvent, nativePath) => {
|
|
182
|
+
const type = CHOKIDAR_TO_TYPE[chokidarEvent];
|
|
183
|
+
if (type === undefined) return; // 'ready' / 'error' / 'raw' — not change events
|
|
184
|
+
if (typeof nativePath !== 'string' || nativePath.length === 0) return;
|
|
185
|
+
try {
|
|
186
|
+
// `makeWatchEvent` validates + normalizes — a single place to enforce
|
|
187
|
+
// the contract. A bug in mapping fails loudly here, not silently in a
|
|
188
|
+
// consumer.
|
|
189
|
+
onEvent(makeWatchEvent(type, toLerretPath(nativePath)));
|
|
190
|
+
} catch (err) {
|
|
191
|
+
errorHandler(/** @type {Error} */ (err));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
watcher.on('ready', () => resolveReady());
|
|
196
|
+
watcher.on('error', (err) => {
|
|
197
|
+
errorHandler(/** @type {Error} */ (err));
|
|
198
|
+
// A pre-`ready` error means the watcher never came up — reject so the
|
|
199
|
+
// caller's `await handle.ready` does not hang forever.
|
|
200
|
+
rejectReady(err);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
let closed = false;
|
|
204
|
+
return {
|
|
205
|
+
ready,
|
|
206
|
+
async close() {
|
|
207
|
+
if (closed) return;
|
|
208
|
+
closed = true;
|
|
209
|
+
await watcher.close();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { CHOKIDAR_TO_TYPE };
|