@xtrable-ltd/nanoesis 0.1.26 → 0.1.27

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 (31) hide show
  1. package/dist/adapter-azure-blob.js +1 -1
  2. package/dist/{chunk-TEIQFPNO.js → chunk-CH4DCPNN.js} +50 -3
  3. package/dist/{chunk-H6ZHZWDJ.js → chunk-EGQLAHLP.js} +210 -97
  4. package/dist/editor-api.d.ts +17 -3
  5. package/dist/editor-api.js +2 -2
  6. package/dist/index.d.ts +107 -2
  7. package/dist/index.js +3 -1
  8. package/dist/mcp.js +3 -3
  9. package/editor/assets/MigrationsPane-eijyEoIw.js +4 -0
  10. package/editor/assets/{TemplatesPane-Dt8Z7trK.js → TemplatesPane-DjFFkccd.js} +116 -116
  11. package/editor/assets/{cssMode-D9n2swjt.js → cssMode-BNSyZV7c.js} +1 -1
  12. package/editor/assets/{freemarker2-DPD1J29I.js → freemarker2-B8rz05rs.js} +1 -1
  13. package/editor/assets/{handlebars-D3A5tmfF.js → handlebars-B3M9DD2d.js} +1 -1
  14. package/editor/assets/{html-ChVj5GP3.js → html-BajOMTpc.js} +1 -1
  15. package/editor/assets/{htmlMode-BLNw3RHS.js → htmlMode-Cqrnk3KF.js} +1 -1
  16. package/editor/assets/index-BtwjABtU.js +145 -0
  17. package/editor/assets/{index-D4IOSCAV.css → index-CUG-24-D.css} +1 -1
  18. package/editor/assets/{javascript-DNcoKqyy.js → javascript-CgKgaKRj.js} +1 -1
  19. package/editor/assets/{jsonMode-DMU6Vb_m.js → jsonMode-B4i8iNyO.js} +1 -1
  20. package/editor/assets/{liquid-BXHGTS2v.js → liquid-DJaUoyuM.js} +1 -1
  21. package/editor/assets/{mdx-BV8b_dZV.js → mdx-DBfFAeUj.js} +1 -1
  22. package/editor/assets/{python-C4cGrrAN.js → python-ClvSkGDd.js} +1 -1
  23. package/editor/assets/{razor-DW1FX8pl.js → razor-BJcU60fL.js} +1 -1
  24. package/editor/assets/{tsMode-C141_BK1.js → tsMode-8Lhfeq5V.js} +1 -1
  25. package/editor/assets/{typescript-CakAGQIi.js → typescript-D3i2tBiq.js} +1 -1
  26. package/editor/assets/{xml-BbYDGoTr.js → xml-CXwucYqL.js} +1 -1
  27. package/editor/assets/{yaml-CSwnT3wl.js → yaml-D48SS_IL.js} +1 -1
  28. package/editor/index.html +2 -2
  29. package/package.json +1 -1
  30. package/editor/assets/MigrationsPane-BEY3O03s.js +0 -4
  31. package/editor/assets/index-DbxwBEBR.js +0 -145
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  contentTypeFor
3
- } from "./chunk-H6ZHZWDJ.js";
3
+ } from "./chunk-EGQLAHLP.js";
4
4
 
5
5
  // ../../adapters/azure-blob/src/container.ts
6
6
  var InMemoryBlobContainer = class {
@@ -13,11 +13,12 @@ import {
13
13
  loadComponents,
14
14
  loadTemplate,
15
15
  pendingMigrations,
16
+ planPublish,
16
17
  publishSite,
17
18
  renderReferenceMarkdown,
18
19
  validateSite,
19
20
  workingStoreRoundTripDiagnostic
20
- } from "./chunk-H6ZHZWDJ.js";
21
+ } from "./chunk-EGQLAHLP.js";
21
22
 
22
23
  // ../editor-api/src/scaffold.ts
23
24
  var HOME_HTML = `<!doctype html>
@@ -111,6 +112,46 @@ function json(status, data) {
111
112
  function methodNotAllowed() {
112
113
  return json(405, { ok: false, error: "POST only" });
113
114
  }
115
+ var NDJSON = "application/x-ndjson";
116
+ function streamingPublish(deps) {
117
+ const stream = async function* () {
118
+ const encoder = new TextEncoder();
119
+ const line = (event) => encoder.encode(`${JSON.stringify(event)}
120
+ `);
121
+ const queue = [];
122
+ let wake;
123
+ let settled = false;
124
+ let thrown;
125
+ const onProgress = (event) => {
126
+ queue.push(event);
127
+ wake?.();
128
+ };
129
+ const running = deps.publish({ onProgress }).then(
130
+ () => void 0,
131
+ (error) => {
132
+ thrown = error;
133
+ }
134
+ ).finally(() => {
135
+ settled = true;
136
+ wake?.();
137
+ });
138
+ for (; ; ) {
139
+ if (queue.length === 0 && !settled) {
140
+ await new Promise((resolve2) => {
141
+ wake = resolve2;
142
+ });
143
+ wake = void 0;
144
+ }
145
+ while (queue.length > 0) yield line(queue.shift());
146
+ if (settled && queue.length === 0) break;
147
+ }
148
+ await running;
149
+ if (thrown !== void 0) {
150
+ yield line({ phase: "error", message: thrown instanceof Error ? thrown.message : String(thrown) });
151
+ }
152
+ };
153
+ return { status: 200, headers: { "content-type": NDJSON, "cache-control": "no-store" }, stream };
154
+ }
114
155
  function withDefaultHeaders(response) {
115
156
  const headers = { ...response.headers ?? {} };
116
157
  if (headers["cache-control"] === void 0) {
@@ -531,8 +572,13 @@ async function dispatchApi(deps, req) {
531
572
  const status = result.reason === "exists" ? 409 : 404;
532
573
  return json(status, { ok: false, error: result.reason });
533
574
  }
575
+ case "/api/publish/plan": {
576
+ if (req.method !== "GET") return json(405, { ok: false, error: "GET only" });
577
+ return json(200, await planPublish(deps.store));
578
+ }
534
579
  case "/api/publish": {
535
580
  if (req.method !== "POST") return methodNotAllowed();
581
+ if ((req.getHeader("accept") ?? "").includes(NDJSON)) return streamingPublish(deps);
536
582
  const result = await deps.publish();
537
583
  if (result.ok) {
538
584
  return json(200, {
@@ -923,7 +969,7 @@ function createEditor(config) {
923
969
  const sink = asSink(config.website);
924
970
  const wipeBeforePublish = config.wipeBeforePublish ?? true;
925
971
  const reconcile = config.enumerate === void 0 ? void 0 : async () => working.reconcile([...await config.enumerate()]);
926
- const publish = async () => {
972
+ const publish = async (options) => {
927
973
  const validation = await validateSite(working);
928
974
  if (validation.ok && wipeBeforePublish && config.website.wipe !== void 0) {
929
975
  await config.website.wipe();
@@ -935,7 +981,8 @@ function createEditor(config) {
935
981
  ...config.purge !== void 0 && { purge: config.purge },
936
982
  ...config.baseUrl !== void 0 && { baseUrl: config.baseUrl },
937
983
  ...dir !== void 0 && { authorDirectory: dir },
938
- ...prebuild !== void 0 && { prebuild }
984
+ ...prebuild !== void 0 && { prebuild },
985
+ ...options?.onProgress !== void 0 && { onProgress: options.onProgress }
939
986
  });
940
987
  };
941
988
  const users = config.users;
@@ -2051,27 +2051,8 @@ function urlForItem(contentPath) {
2051
2051
  }
2052
2052
 
2053
2053
  // ../engine/src/compile/site.ts
2054
- var DEFAULT_IMAGE_CONCURRENCY = 4;
2055
- async function compileSite(source, options = {}) {
2056
- const [
2057
- tree,
2058
- components,
2059
- componentStyles,
2060
- componentScripts,
2061
- documentShell,
2062
- redirects,
2063
- siteConfig
2064
- ] = await Promise.all([
2065
- loadContentTree(source),
2066
- loadComponents(source),
2067
- loadComponentStyles(source),
2068
- loadComponentScripts(source),
2069
- loadDocumentShell(source),
2070
- loadRedirects(source),
2071
- loadSiteConfig(source)
2072
- ]);
2073
- const context = buildResolveContext(tree);
2074
- const baseUrl = options.baseUrl ?? siteConfig.baseUrl;
2054
+ var DEFAULT_IMAGE_CONCURRENCY = 16;
2055
+ async function planPages(tree, source, components) {
2075
2056
  const templateCache = /* @__PURE__ */ new Map();
2076
2057
  const getTemplate = async (name) => {
2077
2058
  const cached = templateCache.get(name);
@@ -2080,16 +2061,6 @@ async function compileSite(source, options = {}) {
2080
2061
  templateCache.set(name, loaded);
2081
2062
  return loaded;
2082
2063
  };
2083
- const styleCache = /* @__PURE__ */ new Map();
2084
- const scriptCache = /* @__PURE__ */ new Map();
2085
- const getStyle = async (name) => {
2086
- if (!styleCache.has(name)) styleCache.set(name, await loadTemplateStyle(source, name));
2087
- return styleCache.get(name);
2088
- };
2089
- const getScript = async (name) => {
2090
- if (!scriptCache.has(name)) scriptCache.set(name, await loadTemplateScript(source, name));
2091
- return scriptCache.get(name);
2092
- };
2093
2064
  const analysisCache = /* @__PURE__ */ new Map();
2094
2065
  const getAnalysis = (name, templateSource) => {
2095
2066
  const cached = analysisCache.get(name);
@@ -2098,8 +2069,7 @@ async function compileSite(source, options = {}) {
2098
2069
  analysisCache.set(name, analysis);
2099
2070
  return analysis;
2100
2071
  };
2101
- const artifacts = [];
2102
- const entries = [];
2072
+ const plans = [];
2103
2073
  const walk = async (dir, inheritedTemplate) => {
2104
2074
  const dirTemplate = dir.defaultTemplate ?? inheritedTemplate;
2105
2075
  for (const child of dir.children) {
@@ -2116,34 +2086,126 @@ async function compileSite(source, options = {}) {
2116
2086
  }
2117
2087
  const template = await getTemplate(templateName);
2118
2088
  const analysis = getAnalysis(templateName, template);
2119
- const { media, mediaArtifacts } = await collectMedia(
2120
- source,
2121
- child,
2122
- analysis,
2123
- options.imageEncoder,
2124
- options.imageConcurrency ?? DEFAULT_IMAGE_CONCURRENCY
2125
- );
2126
- artifacts.push(...mediaArtifacts);
2127
- const templateStyle = await getStyle(templateName);
2128
- const templateScript = await getScript(templateName);
2129
- const contents = compileTemplate({
2130
- template,
2131
- scope: buildScope(child.item),
2132
- components,
2133
- context,
2134
- media,
2135
- componentStyles,
2136
- componentScripts,
2137
- ...options.authorDirectory !== void 0 && { authorDirectory: options.authorDirectory },
2138
- ...documentShell !== void 0 && { document: documentShell },
2139
- ...templateStyle !== void 0 && { templateStyle },
2140
- ...templateScript !== void 0 && { templateScript }
2141
- });
2142
- artifacts.push({ path: outputPathForItem(child.path), contents });
2143
- entries.push(toEntry(child));
2089
+ plans.push({ node: child, templateName, template, refs: itemMediaRefs(child, analysis) });
2144
2090
  }
2145
2091
  };
2146
2092
  await walk(tree, void 0);
2093
+ return plans;
2094
+ }
2095
+ function assetLabel(published) {
2096
+ const slash = published.lastIndexOf("/");
2097
+ return slash === -1 ? published : published.slice(slash + 1);
2098
+ }
2099
+ function buildPublishPlan(plans) {
2100
+ const images = [];
2101
+ const files = [];
2102
+ const imageSeen = /* @__PURE__ */ new Set();
2103
+ const fileSeen = /* @__PURE__ */ new Set();
2104
+ for (const plan of plans) {
2105
+ for (const { published } of plan.refs.images) {
2106
+ if (imageSeen.has(published)) continue;
2107
+ imageSeen.add(published);
2108
+ images.push({ kind: "image", id: published, label: assetLabel(published) });
2109
+ }
2110
+ for (const { published } of plan.refs.files) {
2111
+ if (fileSeen.has(published)) continue;
2112
+ fileSeen.add(published);
2113
+ files.push({ kind: "file", id: published, label: assetLabel(published) });
2114
+ }
2115
+ }
2116
+ const pages = plans.map((plan) => ({
2117
+ kind: "page",
2118
+ id: outputPathForItem(plan.node.path),
2119
+ label: plan.node.item.title
2120
+ }));
2121
+ return {
2122
+ resources: [...images, ...files, ...pages],
2123
+ images: images.length,
2124
+ files: files.length,
2125
+ pages: pages.length
2126
+ };
2127
+ }
2128
+ async function planPublish(source) {
2129
+ const [tree, components] = await Promise.all([loadContentTree(source), loadComponents(source)]);
2130
+ return buildPublishPlan(await planPages(tree, source, components));
2131
+ }
2132
+ async function compileSite(source, options = {}) {
2133
+ const [
2134
+ tree,
2135
+ components,
2136
+ componentStyles,
2137
+ componentScripts,
2138
+ documentShell,
2139
+ redirects,
2140
+ siteConfig
2141
+ ] = await Promise.all([
2142
+ loadContentTree(source),
2143
+ loadComponents(source),
2144
+ loadComponentStyles(source),
2145
+ loadComponentScripts(source),
2146
+ loadDocumentShell(source),
2147
+ loadRedirects(source),
2148
+ loadSiteConfig(source)
2149
+ ]);
2150
+ const context = buildResolveContext(tree);
2151
+ const baseUrl = options.baseUrl ?? siteConfig.baseUrl;
2152
+ const styleCache = /* @__PURE__ */ new Map();
2153
+ const scriptCache = /* @__PURE__ */ new Map();
2154
+ const getStyle = async (name) => {
2155
+ if (!styleCache.has(name)) styleCache.set(name, await loadTemplateStyle(source, name));
2156
+ return styleCache.get(name);
2157
+ };
2158
+ const getScript = async (name) => {
2159
+ if (!scriptCache.has(name)) scriptCache.set(name, await loadTemplateScript(source, name));
2160
+ return scriptCache.get(name);
2161
+ };
2162
+ const artifacts = [];
2163
+ const entries = [];
2164
+ const plans = await planPages(tree, source, components);
2165
+ const onProgress = options.onProgress;
2166
+ let done = 0;
2167
+ let resourceById;
2168
+ if (onProgress !== void 0) {
2169
+ const publishPlan = buildPublishPlan(plans);
2170
+ resourceById = new Map(publishPlan.resources.map((resource) => [resource.id, resource]));
2171
+ onProgress({ phase: "plan", plan: publishPlan });
2172
+ }
2173
+ const report = (id) => {
2174
+ if (onProgress === void 0 || resourceById === void 0) return;
2175
+ const resource = resourceById.get(id);
2176
+ if (resource === void 0) return;
2177
+ done += 1;
2178
+ onProgress({ phase: "resource", resource, done, total: resourceById.size });
2179
+ };
2180
+ const { mediaArtifacts, imageInfoByPublished, fileUrlByPublished } = await collectSiteMedia(
2181
+ source,
2182
+ plans.map((p) => p.refs),
2183
+ options.imageEncoder,
2184
+ options.imageConcurrency ?? DEFAULT_IMAGE_CONCURRENCY,
2185
+ onProgress === void 0 ? void 0 : (published) => report(published)
2186
+ );
2187
+ artifacts.push(...mediaArtifacts);
2188
+ for (const { node, templateName, template, refs } of plans) {
2189
+ const templateStyle = await getStyle(templateName);
2190
+ const templateScript = await getScript(templateName);
2191
+ const contents = compileTemplate({
2192
+ template,
2193
+ scope: buildScope(node.item),
2194
+ components,
2195
+ context,
2196
+ media: resolverFor(refs, imageInfoByPublished, fileUrlByPublished),
2197
+ componentStyles,
2198
+ componentScripts,
2199
+ ...options.authorDirectory !== void 0 && { authorDirectory: options.authorDirectory },
2200
+ ...documentShell !== void 0 && { document: documentShell },
2201
+ ...templateStyle !== void 0 && { templateStyle },
2202
+ ...templateScript !== void 0 && { templateScript }
2203
+ });
2204
+ const outputPath = outputPathForItem(node.path);
2205
+ artifacts.push({ path: outputPath, contents });
2206
+ entries.push(toEntry(node));
2207
+ report(outputPath);
2208
+ }
2147
2209
  const redirectArtifact = buildRedirects(redirects, new Set(entries.map((entry) => entry.url)));
2148
2210
  if (redirectArtifact !== void 0) artifacts.push(redirectArtifact);
2149
2211
  if (options.contentIndex !== false) artifacts.push(buildContentIndex(entries));
@@ -2189,14 +2251,14 @@ async function compilePage(source, itemPath, options = {}) {
2189
2251
  if (options.media !== void 0) {
2190
2252
  media = options.media;
2191
2253
  } else {
2192
- const collected = await collectMedia(
2254
+ const refs = itemMediaRefs(node, analysis);
2255
+ const collected = await collectSiteMedia(
2193
2256
  source,
2194
- node,
2195
- analysis,
2257
+ [refs],
2196
2258
  options.imageEncoder,
2197
2259
  DEFAULT_IMAGE_CONCURRENCY
2198
2260
  );
2199
- media = collected.media;
2261
+ media = resolverFor(refs, collected.imageInfoByPublished, collected.fileUrlByPublished);
2200
2262
  mediaArtifacts = collected.mediaArtifacts;
2201
2263
  }
2202
2264
  const html = compileTemplate({
@@ -2244,48 +2306,91 @@ function inCollection(entry, collectionPath) {
2244
2306
  if (collection === "") return true;
2245
2307
  return entry.path === collection || entry.path.startsWith(`${collection}/`);
2246
2308
  }
2247
- async function collectMedia(source, node, analysis, encoder, imageConcurrency) {
2309
+ function itemMediaRefs(node, analysis) {
2248
2310
  const itemDir = parentPath(node.path);
2249
2311
  const fields = node.item.fields;
2250
- const images = /* @__PURE__ */ new Map();
2251
- const files = /* @__PURE__ */ new Map();
2252
- const mediaArtifacts = [];
2312
+ const files = [];
2253
2313
  for (const field of analysis.fileFields) {
2254
2314
  for (const ref of assetRefs(fields[field])) {
2255
- const published = joinAsset(itemDir, ref);
2256
- const bytes = await tryReadBytes(source, `content/${published}`);
2257
- if (bytes === void 0) continue;
2258
- mediaArtifacts.push({ path: published, contents: bytes });
2259
- files.set(ref, `/${published}`);
2315
+ files.push({ ref, published: joinAsset(itemDir, ref) });
2260
2316
  }
2261
2317
  }
2262
- if (encoder !== void 0) {
2263
- const imageRefs = /* @__PURE__ */ new Set();
2264
- for (const field of analysis.imageFields) {
2265
- for (const ref of assetRefs(fields[field])) imageRefs.add(ref);
2266
- }
2267
- for (const field of analysis.richTextFields) {
2268
- const value = fields[field];
2269
- if (typeof value === "string") {
2270
- for (const ref of inlineImageRefs(value)) imageRefs.add(ref);
2318
+ const images = [];
2319
+ const seen = /* @__PURE__ */ new Set();
2320
+ const addImage = (ref) => {
2321
+ if (seen.has(ref)) return;
2322
+ seen.add(ref);
2323
+ images.push({ ref, published: joinAsset(itemDir, ref) });
2324
+ };
2325
+ for (const field of analysis.imageFields) {
2326
+ for (const ref of assetRefs(fields[field])) addImage(ref);
2327
+ }
2328
+ for (const field of analysis.richTextFields) {
2329
+ const value = fields[field];
2330
+ if (typeof value === "string") {
2331
+ for (const ref of inlineImageRefs(value)) addImage(ref);
2332
+ }
2333
+ }
2334
+ return { images, files };
2335
+ }
2336
+ async function collectSiteMedia(source, itemRefs, encoder, imageConcurrency, onAsset) {
2337
+ const uniqueOrdered = (pick) => {
2338
+ const seen = /* @__PURE__ */ new Set();
2339
+ const order = [];
2340
+ for (const refs of itemRefs) {
2341
+ for (const { published } of pick(refs)) {
2342
+ if (!seen.has(published)) {
2343
+ seen.add(published);
2344
+ order.push(published);
2345
+ }
2271
2346
  }
2272
2347
  }
2273
- const encoded = await mapWithConcurrency([...imageRefs], imageConcurrency, async (ref) => {
2274
- const published = joinAsset(itemDir, ref);
2348
+ return order;
2349
+ };
2350
+ const mediaArtifacts = [];
2351
+ const filePaths = uniqueOrdered((r) => r.files);
2352
+ const fileBytes = await mapWithConcurrency(filePaths, imageConcurrency, async (published) => {
2353
+ const bytes = await tryReadBytes(source, `content/${published}`);
2354
+ onAsset?.(published);
2355
+ return bytes;
2356
+ });
2357
+ const fileUrlByPublished = /* @__PURE__ */ new Map();
2358
+ filePaths.forEach((published, index) => {
2359
+ const bytes = fileBytes[index];
2360
+ if (bytes === void 0) return;
2361
+ mediaArtifacts.push({ path: published, contents: bytes });
2362
+ fileUrlByPublished.set(published, `/${published}`);
2363
+ });
2364
+ const imageInfoByPublished = /* @__PURE__ */ new Map();
2365
+ if (encoder !== void 0) {
2366
+ const imagePaths = uniqueOrdered((r) => r.images);
2367
+ const encoded = await mapWithConcurrency(imagePaths, imageConcurrency, async (published) => {
2275
2368
  const bytes = await tryReadBytes(source, `content/${published}`);
2276
- if (bytes === void 0) return void 0;
2277
- return { ref, ...await processImage(bytes, published, encoder) };
2369
+ const result = bytes === void 0 ? void 0 : await processImage(bytes, published, encoder);
2370
+ onAsset?.(published);
2371
+ return result;
2278
2372
  });
2279
- for (const result of encoded) {
2280
- if (result === void 0) continue;
2373
+ imagePaths.forEach((published, index) => {
2374
+ const result = encoded[index];
2375
+ if (result === void 0) return;
2281
2376
  mediaArtifacts.push(...result.artifacts);
2282
- images.set(result.ref, result.info);
2283
- }
2377
+ imageInfoByPublished.set(published, result.info);
2378
+ });
2284
2379
  }
2285
- return {
2286
- media: { image: (ref) => images.get(ref), file: (ref) => files.get(ref) },
2287
- mediaArtifacts
2288
- };
2380
+ return { mediaArtifacts, imageInfoByPublished, fileUrlByPublished };
2381
+ }
2382
+ function resolverFor(refs, imageInfoByPublished, fileUrlByPublished) {
2383
+ const imageByRef = /* @__PURE__ */ new Map();
2384
+ for (const { ref, published } of refs.images) {
2385
+ const info = imageInfoByPublished.get(published);
2386
+ if (info !== void 0) imageByRef.set(ref, info);
2387
+ }
2388
+ const fileByRef = /* @__PURE__ */ new Map();
2389
+ for (const { ref, published } of refs.files) {
2390
+ const url = fileUrlByPublished.get(published);
2391
+ if (url !== void 0) fileByRef.set(ref, url);
2392
+ }
2393
+ return { image: (ref) => imageByRef.get(ref), file: (ref) => fileByRef.get(ref) };
2289
2394
  }
2290
2395
  async function tryReadBytes(source, path) {
2291
2396
  try {
@@ -3078,9 +3183,12 @@ var noopPurgeService = {
3078
3183
  // ../engine/src/publish/publish.ts
3079
3184
  var DEFAULT_WRITE_CONCURRENCY = 8;
3080
3185
  async function publishSite(source, sink, options = {}) {
3186
+ const onProgress = options.onProgress;
3187
+ onProgress?.({ phase: "validate" });
3081
3188
  const validation = await validateSite(source);
3082
3189
  const summary = summarizeTree(await loadContentTree(source));
3083
3190
  if (!validation.ok) {
3191
+ onProgress?.({ phase: "blocked", errors: validation.errors.map((error) => error.message) });
3084
3192
  return { ok: false, validation, written: [], summary };
3085
3193
  }
3086
3194
  const {
@@ -3104,12 +3212,16 @@ async function publishSite(source, sink, options = {}) {
3104
3212
  );
3105
3213
  const byPath = /* @__PURE__ */ new Map();
3106
3214
  for (const artifact of [...stamped, ...passthrough]) byPath.set(artifact.path, artifact);
3107
- await mapWithConcurrency(
3108
- [...byPath.values()],
3109
- writeConcurrency,
3110
- (artifact) => sink.write(artifact.path, artifact.contents, cacheControlFor(artifact))
3111
- );
3215
+ const uploadTotal = byPath.size;
3216
+ let uploaded = 0;
3217
+ await mapWithConcurrency([...byPath.values()], writeConcurrency, async (artifact) => {
3218
+ await sink.write(artifact.path, artifact.contents, cacheControlFor(artifact));
3219
+ uploaded += 1;
3220
+ onProgress?.({ phase: "upload", written: uploaded, total: uploadTotal });
3221
+ });
3222
+ if (purge !== noopPurgeService) onProgress?.({ phase: "purge" });
3112
3223
  await purge.purgeAll();
3224
+ onProgress?.({ phase: "done", written: uploadTotal, summary });
3113
3225
  return { ok: true, validation, written: [...byPath.keys()].sort(), summary };
3114
3226
  }
3115
3227
  function asBytes(contents) {
@@ -3420,6 +3532,7 @@ export {
3420
3532
  buildContentIndex,
3421
3533
  outputPathForItem,
3422
3534
  urlForItem,
3535
+ planPublish,
3423
3536
  compileSite,
3424
3537
  compilePage,
3425
3538
  buildResolveContext,
@@ -1,4 +1,4 @@
1
- import { WorkingStore, IdentityProvider, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, Storage, ImageEncoder, PurgeService, UserSummary, PreBuildHook, AuthorDirectory, Repair, DiagnosticCheck } from '@nanoesis/engine';
1
+ import { WorkingStore, IdentityProvider, ProgressReporter, PublishResult, ReconcileResult, AuthEndpoints, UserAdminEndpoints, AuthorOption, DiagnosticRegistry, Storage, ImageEncoder, PurgeService, UserSummary, PreBuildHook, AuthorDirectory, Repair, DiagnosticCheck } from '@nanoesis/engine';
2
2
  export { Storage } from '@nanoesis/engine';
3
3
 
4
4
  /**
@@ -93,14 +93,28 @@ interface ApiResponse {
93
93
  readonly status: number;
94
94
  readonly headers?: Record<string, string>;
95
95
  readonly body?: string | Uint8Array;
96
+ /**
97
+ * A streaming body (DESIGN §11): when present, the host writes these chunks as they are
98
+ * produced instead of `body`, for the publish-progress NDJSON stream. A host that cannot
99
+ * stream may buffer the chunks and send them as one body, the client decodes the same
100
+ * NDJSON either way. `body` is ignored when `stream` is set.
101
+ */
102
+ readonly stream?: () => AsyncIterable<Uint8Array>;
96
103
  }
97
104
  interface ApiDeps {
98
105
  /** The editor working store (read + write/delete/rename), any {@link WorkingStore}. */
99
106
  readonly store: WorkingStore;
100
107
  /** Who is calling, gates every editing route (DESIGN §11). */
101
108
  readonly identity: IdentityProvider;
102
- /** Run the publish pipeline (host binds source/sink/purge). */
103
- readonly publish: () => Promise<PublishResult>;
109
+ /**
110
+ * Run the publish pipeline (host binds source/sink/purge). The optional `onProgress`
111
+ * callback receives {@link PublishProgress} events as the publish runs, so the streaming
112
+ * `/api/publish` route can relay a live timeline; a caller that ignores it (the MCP
113
+ * publish tool, tests) gets the same buffered {@link PublishResult}.
114
+ */
115
+ readonly publish: (options?: {
116
+ onProgress?: ProgressReporter;
117
+ }) => Promise<PublishResult>;
104
118
  /**
105
119
  * Optional index-reconcile (DESIGN §11d): rebuild the working store's content index
106
120
  * from the keys actually present, recovering files that arrived by a path that
@@ -21,8 +21,8 @@ import {
21
21
  serveEditorAsset,
22
22
  templateSnapshotIntegrityDiagnostic,
23
23
  templateSuffixConflictDiagnostic
24
- } from "./chunk-TEIQFPNO.js";
25
- import "./chunk-H6ZHZWDJ.js";
24
+ } from "./chunk-CH4DCPNN.js";
25
+ import "./chunk-EGQLAHLP.js";
26
26
  export {
27
27
  FileBrandingStore,
28
28
  InMemoryBrandingStore,