@xtrable-ltd/nanoesis 0.1.4 → 0.1.6

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.
Files changed (26) hide show
  1. package/dist/adapter-azure-blob.js +1 -1
  2. package/dist/{chunk-S23S4ZAK.js → chunk-Y7PUOM5I.js} +86 -49
  3. package/dist/editor-api.d.ts +19 -7
  4. package/dist/editor-api.js +9 -43
  5. package/dist/index.d.ts +15 -0
  6. package/dist/index.js +1 -1
  7. package/editor/assets/{TemplatesPane-Cjwu8SOY.js → TemplatesPane-8SToGitk.js} +172 -172
  8. package/editor/assets/{cssMode-CIqtqlOX.js → cssMode-BgMFxIL5.js} +1 -1
  9. package/editor/assets/{freemarker2-03aLbgAQ.js → freemarker2-C1n0dZBT.js} +1 -1
  10. package/editor/assets/{handlebars-CsCT0Kz-.js → handlebars-DHtQKz6I.js} +1 -1
  11. package/editor/assets/{html-BMDaxhoV.js → html-DCKECHJy.js} +1 -1
  12. package/editor/assets/{htmlMode-BmX-GJzB.js → htmlMode-DIxPoVyG.js} +1 -1
  13. package/editor/assets/{index-DSooOnUR.css → index-BRPY7tEn.css} +1 -1
  14. package/editor/assets/{index-xCwd0bS3.js → index-BZ3Bij0z.js} +68 -73
  15. package/editor/assets/{javascript-y8MJYJ58.js → javascript-BQa7DljQ.js} +1 -1
  16. package/editor/assets/{jsonMode-DPpxOVap.js → jsonMode-CUqvw4kw.js} +1 -1
  17. package/editor/assets/{liquid-CGoDffSW.js → liquid-BdmTog1w.js} +1 -1
  18. package/editor/assets/{mdx-CMvr2LP2.js → mdx-CMmi05CR.js} +1 -1
  19. package/editor/assets/{python-Dg93CFNM.js → python-BxYWYFBZ.js} +1 -1
  20. package/editor/assets/{razor-D8HipAex.js → razor-NbjX7n9N.js} +1 -1
  21. package/editor/assets/{tsMode-Drl3yYU0.js → tsMode-DiCcyKwn.js} +1 -1
  22. package/editor/assets/{typescript-Dcqumk7N.js → typescript-CJcxRbel.js} +1 -1
  23. package/editor/assets/{xml-zoMMlERG.js → xml-CdXggTD3.js} +1 -1
  24. package/editor/assets/{yaml-BMaJnJHN.js → yaml-CUKQ9Cpf.js} +1 -1
  25. package/editor/index.html +2 -2
  26. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  contentTypeFor
3
- } from "./chunk-S23S4ZAK.js";
3
+ } from "./chunk-Y7PUOM5I.js";
4
4
 
5
5
  // ../../adapters/azure-blob/src/container.ts
6
6
  var InMemoryBlobContainer = class {
@@ -237,6 +237,20 @@ var IndexedStore = class {
237
237
  store;
238
238
  index;
239
239
  loading;
240
+ /**
241
+ * Per-instance mutex (DESIGN §11d): every mutation (`write`/`delete`/`rename`/`reconcile`)
242
+ * runs through {@link serializeMutation}, which chains onto the previous mutation's
243
+ * completion. Without it, two concurrent mutations both read the cached `this.index`,
244
+ * each compute their "next" key set from that stale snapshot, and the second
245
+ * `saveIndex` clobbers the first — the file writes land in blob, but the index keeps
246
+ * stale references or loses fresh ones (the "I deleted it and it's still there" or
247
+ * "I added it and it didn't appear" bugs surfaced dogfooding the marketing site,
248
+ * 2026-05-28). Reads (`list`/`exists`/`readBytes`) are *not* serialised; an
249
+ * in-flight mutation simply means a reader sees the pre-mutation index, eventually
250
+ * consistent and safe. Crashes inside `work()` release the lock so a single failure
251
+ * does not deadlock subsequent operations.
252
+ */
253
+ mutationLock = Promise.resolve();
240
254
  /** The index, loaded once on first need and cached (mutations replace the cached copy). */
241
255
  async loaded() {
242
256
  if (this.index !== void 0) return this.index;
@@ -244,6 +258,21 @@ var IndexedStore = class {
244
258
  this.index = await this.loading;
245
259
  return this.index;
246
260
  }
261
+ serializeMutation(work) {
262
+ const previous = this.mutationLock;
263
+ let release;
264
+ this.mutationLock = new Promise((resolve) => {
265
+ release = resolve;
266
+ });
267
+ return (async () => {
268
+ try {
269
+ await previous;
270
+ return await work();
271
+ } finally {
272
+ release();
273
+ }
274
+ })();
275
+ }
247
276
  async list(dir) {
248
277
  return childrenOf((await this.loaded()).keys, dir);
249
278
  }
@@ -262,31 +291,35 @@ var IndexedStore = class {
262
291
  * Create or overwrite `key`. The index is rewritten only when `key` is new (an
263
292
  * overwrite leaves the key set unchanged, so editing an item is a single `put`).
264
293
  */
265
- async write(key, bytes) {
266
- const target = guarded(normalize(key));
267
- await this.store.put(target, bytes);
268
- const index = await this.loaded();
269
- if (!index.keys.includes(target)) {
270
- this.index = await saveIndex(this.store, index, [...index.keys, target]);
271
- }
294
+ write(key, bytes) {
295
+ return this.serializeMutation(async () => {
296
+ const target = guarded(normalize(key));
297
+ await this.store.put(target, bytes);
298
+ const index = await this.loaded();
299
+ if (!index.keys.includes(target)) {
300
+ this.index = await saveIndex(this.store, index, [...index.keys, target]);
301
+ }
302
+ });
272
303
  }
273
304
  /**
274
305
  * Delete a file, or a whole directory subtree (every key under `key/`). Idempotent: a
275
306
  * path the index does not know is still deleted from the store (clearing an orphan),
276
307
  * and deleting nothing is a no-op.
277
308
  */
278
- async delete(key) {
279
- const target = guarded(normalize(key));
280
- const index = await this.loaded();
281
- const prefix = `${target}/`;
282
- const removed = index.keys.filter((k) => k === target || k.startsWith(prefix));
283
- if (removed.length === 0) {
284
- await this.store.delete(target);
285
- return;
286
- }
287
- await Promise.all(removed.map((k) => this.store.delete(k)));
288
- const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
289
- this.index = await saveIndex(this.store, index, remaining);
309
+ delete(key) {
310
+ return this.serializeMutation(async () => {
311
+ const target = guarded(normalize(key));
312
+ const index = await this.loaded();
313
+ const prefix = `${target}/`;
314
+ const removed = index.keys.filter((k) => k === target || k.startsWith(prefix));
315
+ if (removed.length === 0) {
316
+ await this.store.delete(target);
317
+ return;
318
+ }
319
+ await Promise.all(removed.map((k) => this.store.delete(k)));
320
+ const remaining = index.keys.filter((k) => k !== target && !k.startsWith(prefix));
321
+ this.index = await saveIndex(this.store, index, remaining);
322
+ });
290
323
  }
291
324
  /**
292
325
  * Move/rename a file, or a whole directory subtree (every key under `from/` is remapped
@@ -295,32 +328,34 @@ var IndexedStore = class {
295
328
  * `/api/rename` enforces. (Mutating the reserved namespace is a programmer error and
296
329
  * still throws.)
297
330
  */
298
- async rename(from, to) {
299
- const source = guarded(normalize(from));
300
- const dest = guarded(normalize(to));
301
- if (source === dest) return { ok: true };
302
- const index = await this.loaded();
303
- const sourcePrefix = `${source}/`;
304
- const affected = index.keys.filter((k) => k === source || k.startsWith(sourcePrefix));
305
- if (affected.length === 0) return { ok: false, reason: "missing" };
306
- const destPrefix = `${dest}/`;
307
- if (index.keys.some((k) => k === dest || k.startsWith(destPrefix))) {
308
- return { ok: false, reason: "exists" };
309
- }
310
- const moves = affected.map((k) => ({
311
- from: k,
312
- to: k === source ? dest : dest + k.slice(source.length)
313
- }));
314
- for (const move of moves) {
315
- const bytes = await this.store.get(move.from);
316
- if (bytes === void 0) continue;
317
- await this.store.put(move.to, bytes);
318
- await this.store.delete(move.from);
319
- }
320
- const movedFrom = new Set(moves.map((move) => move.from));
321
- const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
322
- this.index = await saveIndex(this.store, index, next);
323
- return { ok: true };
331
+ rename(from, to) {
332
+ return this.serializeMutation(async () => {
333
+ const source = guarded(normalize(from));
334
+ const dest = guarded(normalize(to));
335
+ if (source === dest) return { ok: true };
336
+ const index = await this.loaded();
337
+ const sourcePrefix = `${source}/`;
338
+ const affected = index.keys.filter((k) => k === source || k.startsWith(sourcePrefix));
339
+ if (affected.length === 0) return { ok: false, reason: "missing" };
340
+ const destPrefix = `${dest}/`;
341
+ if (index.keys.some((k) => k === dest || k.startsWith(destPrefix))) {
342
+ return { ok: false, reason: "exists" };
343
+ }
344
+ const moves = affected.map((k) => ({
345
+ from: k,
346
+ to: k === source ? dest : dest + k.slice(source.length)
347
+ }));
348
+ for (const move of moves) {
349
+ const bytes = await this.store.get(move.from);
350
+ if (bytes === void 0) continue;
351
+ await this.store.put(move.to, bytes);
352
+ await this.store.delete(move.from);
353
+ }
354
+ const movedFrom = new Set(moves.map((move) => move.from));
355
+ const next = index.keys.filter((k) => !movedFrom.has(k)).concat(moves.map((m) => m.to));
356
+ this.index = await saveIndex(this.store, index, next);
357
+ return { ok: true };
358
+ });
324
359
  }
325
360
  /**
326
361
  * Rebuild this store's index from the *actual* keys the underlying store holds (DESIGN
@@ -330,10 +365,12 @@ var IndexedStore = class {
330
365
  * {@link reconcileIndex} on a fresh store, this also refreshes the cached in-memory
331
366
  * index, so this live instance sees the recovered files immediately.
332
367
  */
333
- async reconcile(actualKeys) {
334
- const result = await reconcileIndex(this.store, actualKeys);
335
- this.index = result.index;
336
- return result;
368
+ reconcile(actualKeys) {
369
+ return this.serializeMutation(async () => {
370
+ const result = await reconcileIndex(this.store, actualKeys);
371
+ this.index = result.index;
372
+ return result;
373
+ });
337
374
  }
338
375
  };
339
376
  function guarded(key) {
@@ -216,16 +216,28 @@ declare function readMcpResource(uri: string): {
216
216
  * §11d) and bundled here as **pure data**, so the engine's purity is untouched and the
217
217
  * scaffold works identically for every host and the MCP server with no per-host wiring.
218
218
  *
219
- * Deliberately minimal (the owner's call, distinct from the fuller `examples/starter`):
220
- * one home page, a page template, the document shell, and a stylesheet. It is gate-clean
221
- * by construction no collection loops (nothing to point at yet), no custom components,
222
- * no required fields left unset so a fresh adopter can publish it immediately, and the
223
- * home page is a **draft** (`isPublished: false`) so going live is a deliberate act.
219
+ * Auto-fires on first sign-in to an empty store (revised 2026-05-28). The earlier behaviour
220
+ * a confirm prompt that the user could dismiss — left a user who said no with no path
221
+ * forward (the empty Content workspace has no signposted route to the Dev workspace, and
222
+ * every alternative to scaffolding needs templates to already exist). Removing the choice
223
+ * removes a concept; the scaffold is idempotent and gate-clean, so doing it always is safe.
224
+ *
225
+ * Deliberately minimal (revised 2026-05-28, owner's call):
226
+ * - `templates/home.html` is a **full HTML document** (DESIGN §6.10: with no
227
+ * `templates/document.html`, a page template is the whole document) — no shared shell.
228
+ * The user can extract one later when they add a second page; for the bootstrap state,
229
+ * three files is simpler than four.
230
+ * - `public/styles.css` for baseline readability — a one-page site without any CSS reads
231
+ * poorly, and the styles.css path is the editor's canonical "your site's CSS" location.
232
+ * - `content/index.json` with `isPublished: true` (revised 2026-05-28). The earlier
233
+ * "draft, so going live is deliberate" framing turned out to be a footgun: a new user
234
+ * would hit Publish and get the "nothing went live" warning without obvious cause. A
235
+ * published placeholder is honest about what a publish will do; the user's first edit
236
+ * is to replace the placeholder copy, not to also remember to toggle a flag.
224
237
  */
225
238
  /**
226
239
  * The starter files, keyed by working-store path. Written through the store's `write`
227
- * (so the index records each), in this order templates and styles before the content
228
- * that depends on them, though the store has no cross-file ordering requirement.
240
+ * (so the index records each). The store has no cross-file ordering requirement.
229
241
  */
230
242
  declare const SCAFFOLD_FILES: Readonly<Record<string, string>>;
231
243
 
@@ -8,45 +8,32 @@ import {
8
8
  loadTemplate,
9
9
  renderReferenceMarkdown,
10
10
  validateSite
11
- } from "./chunk-S23S4ZAK.js";
11
+ } from "./chunk-Y7PUOM5I.js";
12
12
 
13
13
  // ../editor-api/src/scaffold.ts
14
- var DOCUMENT_HTML = `<!doctype html>
14
+ var HOME_HTML = `<!doctype html>
15
15
  <html lang="en">
16
16
  <head>
17
17
  <meta charset="utf-8" />
18
18
  <meta name="viewport" content="width=device-width, initial-scale=1" />
19
19
  <title>{title}</title>
20
- <meta name="description" content="{meta_description}" />
21
20
  <link rel="stylesheet" href="/styles.css" />
22
21
  </head>
23
22
  <body>
24
- <header class="site-header">
25
- <a class="brand" href="/">Your site</a>
26
- </header>
27
23
  <main>
28
- <slot></slot>
24
+ <h1>{title}</h1>
25
+ <div class="prose" data-type="richtext">{body}</div>
29
26
  </main>
30
- <footer class="site-footer">
31
- <p>Built with nanoesis \u2014 static HTML, no runtime.</p>
32
- </footer>
33
27
  </body>
34
28
  </html>
35
29
  `;
36
- var HOME_HTML = `<section class="hero">
37
- <h1>{title}</h1>
38
- <p class="lead">{lead}</p>
39
- </section>
40
- <div class="prose" data-type="richtext">{body}</div>
41
- `;
42
30
  var HOME_JSON = `${JSON.stringify(
43
31
  {
44
32
  template: "home",
45
33
  title: "Welcome",
46
- isPublished: false,
34
+ isPublished: true,
47
35
  fields: {
48
- lead: "Edit this page in the nanoesis editor, then publish when you are ready.",
49
- body: "<p>This is your new site. It compiles to plain static HTML \u2014 there is no server and no client framework, and nothing of the editor survives into production.</p>"
36
+ body: "<p>This is your starter home page. Edit it in the nanoesis editor and click Publish to update your site.</p>"
50
37
  }
51
38
  },
52
39
  null,
@@ -67,30 +54,16 @@ body {
67
54
  margin: 0;
68
55
  }
69
56
 
70
- .site-header,
71
- main,
72
- .site-footer {
57
+ main {
73
58
  max-width: var(--max);
74
59
  margin-inline: auto;
75
60
  padding: 1.5rem 1.25rem;
76
61
  }
77
62
 
78
- .site-header .brand {
79
- font-weight: 700;
80
- text-decoration: none;
81
- color: var(--ink);
82
- }
83
-
84
- .hero h1 {
63
+ h1 {
85
64
  font-size: 2.25rem;
86
65
  letter-spacing: -0.01em;
87
- margin: 0 0 0.5rem;
88
- }
89
-
90
- .lead {
91
- font-size: 1.2rem;
92
- color: var(--muted);
93
- margin: 0 0 2rem;
66
+ margin: 0 0 1rem;
94
67
  }
95
68
 
96
69
  .prose {
@@ -100,15 +73,8 @@ main,
100
73
  a {
101
74
  color: var(--accent);
102
75
  }
103
-
104
- .site-footer {
105
- color: var(--muted);
106
- font-size: 0.9rem;
107
- border-top: 1px solid #eee;
108
- }
109
76
  `;
110
77
  var SCAFFOLD_FILES = {
111
- "templates/document.html": DOCUMENT_HTML,
112
78
  "templates/home.html": HOME_HTML,
113
79
  "public/styles.css": STYLES_CSS,
114
80
  "content/index.json": HOME_JSON
package/dist/index.d.ts CHANGED
@@ -281,9 +281,24 @@ declare class IndexedStore implements WorkingStore {
281
281
  private readonly store;
282
282
  private index;
283
283
  private loading;
284
+ /**
285
+ * Per-instance mutex (DESIGN §11d): every mutation (`write`/`delete`/`rename`/`reconcile`)
286
+ * runs through {@link serializeMutation}, which chains onto the previous mutation's
287
+ * completion. Without it, two concurrent mutations both read the cached `this.index`,
288
+ * each compute their "next" key set from that stale snapshot, and the second
289
+ * `saveIndex` clobbers the first — the file writes land in blob, but the index keeps
290
+ * stale references or loses fresh ones (the "I deleted it and it's still there" or
291
+ * "I added it and it didn't appear" bugs surfaced dogfooding the marketing site,
292
+ * 2026-05-28). Reads (`list`/`exists`/`readBytes`) are *not* serialised; an
293
+ * in-flight mutation simply means a reader sees the pre-mutation index, eventually
294
+ * consistent and safe. Crashes inside `work()` release the lock so a single failure
295
+ * does not deadlock subsequent operations.
296
+ */
297
+ private mutationLock;
284
298
  constructor(store: BlobStore);
285
299
  /** The index, loaded once on first need and cached (mutations replace the cached copy). */
286
300
  private loaded;
301
+ private serializeMutation;
287
302
  list(dir: string): Promise<readonly DirEntry[]>;
288
303
  readText(path: string): Promise<string>;
289
304
  readBytes(path: string): Promise<Uint8Array>;
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ import {
61
61
  validateSite,
62
62
  valueKindOf,
63
63
  wholeValueToken
64
- } from "./chunk-S23S4ZAK.js";
64
+ } from "./chunk-Y7PUOM5I.js";
65
65
  export {
66
66
  ContentParseError,
67
67
  DEFAULT_DIRS,