mechanica 2.1.0-beta.5 → 2.1.0-beta.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.
@@ -1,4 +1,4 @@
1
- import { W as isAssetName, Y as updateImageManifest, q as readImageManifest } from "./rich-text-codec-BXOKV94u.js";
1
+ import { G as isAssetName, J as readImageManifest, X as updateImageManifest } from "./rich-text-codec-BSD35k2U.js";
2
2
  import { join } from "node:path";
3
3
  import { chmod, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
@@ -1,7 +1,7 @@
1
- import { n as configureSite, r as indexGeneratedPages } from "./site-Co04D3rB.js";
2
- import { A as pageVersion, C as harvestPageImages, D as movePage, E as listPages, I as setPageDraft, J as storeUpload, K as listUploads, L as translationsOf, M as renamePage, N as savePage, R as contentFilesOf, T as listFolders, U as fsAssetStore, W as isAssetName, _ as deletePage, a as folderOf, b as fillContentDefaults, c as mergeLocaleSiteData, f as readSiteData, g as createTranslation, h as createPage, j as readPage, l as mergeSiteData, m as PageExistsError, o as mergeFolderData, s as mergeLocaleFolderData, u as readFolderData, v as deleteTranslation, w as imageNamesOf, x as fillImageMeta, y as duplicatePage } from "./rich-text-codec-BXOKV94u.js";
1
+ import { n as configureSite, r as indexGeneratedPages } from "./site-DfazSr3_.js";
2
+ import { A as pageVersion, C as harvestPageImages, D as movePage, E as listPages, I as setPageDraft, L as translationsOf, M as renamePage, N as savePage, Q as uploadResponse, R as contentFilesOf, T as listFolders, U as contentTypeOf, W as fsAssetStore, Y as storeUpload, _ as deletePage, a as folderOf, b as fillContentDefaults, c as mergeLocaleSiteData, f as readSiteData, g as createTranslation, h as createPage, j as readPage, l as mergeSiteData, m as PageExistsError, o as mergeFolderData, q as listUploads, s as mergeLocaleFolderData, u as readFolderData, v as deleteTranslation, w as imageNamesOf, x as fillImageMeta, y as duplicatePage } from "./rich-text-codec-BSD35k2U.js";
3
3
  import fs, { readFileSync, readdirSync } from "node:fs";
4
- import { basename, extname, join, resolve, sep } from "node:path";
4
+ import { basename, join, resolve, sep } from "node:path";
5
5
  import { EDITOR_DIST_DIR, mergeTranslation, parseLocalePath, passDataToHTML, resolveQueryKey, serializeState } from "mechanica-shared";
6
6
  import { parseComposedBlock, serializeComposedBlock } from "mechanica-shared/block-format";
7
7
  import { createHash } from "node:crypto";
@@ -362,25 +362,6 @@ var json = (data, status = 200) => new Response(JSON.stringify(data), {
362
362
  status,
363
363
  headers: { "content-type": "application/json" }
364
364
  });
365
- /** Content types of the files a site stores — browsers don't sniff SVG or fonts. */
366
- var MIME = {
367
- ".png": "image/png",
368
- ".jpg": "image/jpeg",
369
- ".jpeg": "image/jpeg",
370
- ".gif": "image/gif",
371
- ".webp": "image/webp",
372
- ".avif": "image/avif",
373
- ".svg": "image/svg+xml",
374
- ".ico": "image/x-icon",
375
- ".mp4": "video/mp4",
376
- ".webm": "video/webm",
377
- ".mp3": "audio/mpeg",
378
- ".pdf": "application/pdf",
379
- ".json": "application/json",
380
- ".txt": "text/plain; charset=utf-8",
381
- ".woff": "font/woff",
382
- ".woff2": "font/woff2"
383
- };
384
365
  /**
385
366
  * Stream a file from `dir`, containing the resolved path so an encoded `..`
386
367
  * (or an absolute path) cannot escape it.
@@ -395,7 +376,7 @@ function serveFile(dir, relative, headers = {}) {
395
376
  const file = resolve(dir, decoded);
396
377
  if (file !== dir && !file.startsWith(dir + sep)) return new Response("Forbidden", { status: 403 });
397
378
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return new Response("Not found", { status: 404 });
398
- const type = MIME[extname(file).toLowerCase()];
379
+ const type = contentTypeOf(file);
399
380
  return new Response(Readable.toWeb(fs.createReadStream(file)), { headers: {
400
381
  ...type ? { "content-type": type } : {},
401
382
  ...headers
@@ -422,19 +403,7 @@ function validateComposed(def) {
422
403
  function createEditorService(mech, options = {}) {
423
404
  const mechDir = typeof mech === "string" ? mech : null;
424
405
  const assets = options.assets ?? (mechDir ? fsAssetStore(mechDir) : null);
425
- const asset = async (encoded) => {
426
- let name;
427
- try {
428
- name = decodeURIComponent(encoded);
429
- } catch {
430
- return new Response("Bad request", { status: 400 });
431
- }
432
- if (!assets || !isAssetName(name)) return null;
433
- const body = await assets.open(name);
434
- if (!body) return null;
435
- const type = MIME[extname(name).toLowerCase()];
436
- return new Response(body, { headers: type ? { "content-type": type } : {} });
437
- };
406
+ const asset = (encoded) => uploadResponse(assets, encoded);
438
407
  let configured = null;
439
408
  let generatedIndex = null;
440
409
  const currentSite = async () => {
@@ -273,6 +273,74 @@ async function listUploads(assets) {
273
273
  src: assetUrl(name)
274
274
  }));
275
275
  }
276
+ /** Content types of the files a site stores — browsers don't sniff SVG or fonts. */
277
+ var MIME = {
278
+ ".png": "image/png",
279
+ ".jpg": "image/jpeg",
280
+ ".jpeg": "image/jpeg",
281
+ ".gif": "image/gif",
282
+ ".webp": "image/webp",
283
+ ".avif": "image/avif",
284
+ ".svg": "image/svg+xml",
285
+ ".ico": "image/x-icon",
286
+ ".mp4": "video/mp4",
287
+ ".webm": "video/webm",
288
+ ".mp3": "audio/mpeg",
289
+ ".pdf": "application/pdf",
290
+ ".json": "application/json",
291
+ ".txt": "text/plain; charset=utf-8",
292
+ ".woff": "font/woff",
293
+ ".woff2": "font/woff2"
294
+ };
295
+ /** The content type a stored file is served with, when its extension says. */
296
+ var contentTypeOf = (fileName) => MIME[parse(fileName).ext.toLowerCase()];
297
+ /**
298
+ * The HTTP answer for an upload asked for by its address — the part of
299
+ * `/media/<name>` after the prefix, still percent-encoded. null when it is not
300
+ * an upload of this store, so the caller can fall through; a 400 for a name
301
+ * that is not valid encoding. One function for every host that serves uploads:
302
+ * the dev server, an editing host, a site's live host.
303
+ */
304
+ async function uploadResponse(assets, encodedName) {
305
+ let name;
306
+ try {
307
+ name = decodeURIComponent(encodedName);
308
+ } catch {
309
+ return new Response("Bad request", { status: 400 });
310
+ }
311
+ if (!assets || !isAssetName(name)) return null;
312
+ const body = await assets.open(name);
313
+ if (!body) return null;
314
+ const type = contentTypeOf(name);
315
+ return new Response(body, { headers: type ? { "content-type": type } : {} });
316
+ }
317
+ /**
318
+ * A store for rendering a site whose uploads the host keeps and serves itself.
319
+ * `hosted` is what the host knows about them — a name with its image info. The
320
+ * result knows those names (`list`, `info`) but cannot open them: an export
321
+ * over it fills in their dimensions and previews, copies no bytes, and reports
322
+ * them as the host's to serve (see `ExportResult.uploads`). Whatever the local
323
+ * store has is used as before.
324
+ */
325
+ function withHostedAssets(local, hosted) {
326
+ const names = Object.keys(hosted).filter(isAssetName);
327
+ return {
328
+ write: (name, data) => local.write(name, data),
329
+ open: (name) => local.open(name),
330
+ saveInfo: (entries) => local.saveInfo(entries),
331
+ async list() {
332
+ return [.../* @__PURE__ */ new Set([...await local.list(), ...names])];
333
+ },
334
+ async info(wanted) {
335
+ const known = await local.info(wanted);
336
+ for (const name of wanted) {
337
+ const entry = hosted[name];
338
+ if (entry && !known[name]) known[name] = entry;
339
+ }
340
+ return known;
341
+ }
342
+ };
343
+ }
276
344
  var MISS_TTL_MS = 3e4;
277
345
  var MEMORY_LIMIT = 67108864;
278
346
  /**
@@ -1203,4 +1271,4 @@ function findRichTextProps(props) {
1203
1271
  return found;
1204
1272
  }
1205
1273
  //#endregion
1206
- export { analyzeImageBuffer as $, pageVersion as A, memoryContentFiles as B, harvestPageImages as C, movePage as D, listPages as E, setPageCodec as F, isDerivedAsset as G, assetFileOf as H, setPageDraft as I, storeUpload as J, listUploads as K, translationsOf as L, renamePage as M, savePage as N, pageOfFile as O, setPageBlocks as P, wasRecentlyMutated as Q, contentFilesOf as R, getPagePath as S, listFolders as T, fsAssetStore as U, UPLOADS_PREFIX$1 as V, isAssetName as W, uploadName as X, updateImageManifest as Y, withRemoteAssets as Z, deletePage as _, folderOf as a, fillContentDefaults as b, mergeLocaleSiteData as c, readFoldersData as d, hasSharp as et, readSiteData as f, createTranslation as g, createPage as h, toBlockMeta as i, readPage as j, pageUrlOf as k, mergeSiteData as l, PageExistsError as m, richTextCodecFromBlocks as n, mergeFolderData as o, readSiteLocaleOverride as p, readImageManifest as q, composedBlockMeta as r, mergeLocaleFolderData as s, buildRichTextCodec as t, isRasterImage as tt, readFolderData as u, deleteTranslation as v, imageNamesOf as w, fillImageMeta as x, duplicatePage as y, fsContentFiles as z };
1274
+ export { withHostedAssets as $, pageVersion as A, memoryContentFiles as B, harvestPageImages as C, movePage as D, listPages as E, setPageCodec as F, isAssetName as G, assetFileOf as H, setPageDraft as I, readImageManifest as J, isDerivedAsset as K, translationsOf as L, renamePage as M, savePage as N, pageOfFile as O, setPageBlocks as P, uploadResponse as Q, contentFilesOf as R, getPagePath as S, listFolders as T, contentTypeOf as U, UPLOADS_PREFIX$1 as V, fsAssetStore as W, updateImageManifest as X, storeUpload as Y, uploadName as Z, deletePage as _, folderOf as a, fillContentDefaults as b, mergeLocaleSiteData as c, readFoldersData as d, withRemoteAssets as et, readSiteData as f, createTranslation as g, createPage as h, toBlockMeta as i, isRasterImage as it, readPage as j, pageUrlOf as k, mergeSiteData as l, PageExistsError as m, richTextCodecFromBlocks as n, analyzeImageBuffer as nt, mergeFolderData as o, readSiteLocaleOverride as p, listUploads as q, composedBlockMeta as r, hasSharp as rt, mergeLocaleFolderData as s, buildRichTextCodec as t, wasRecentlyMutated as tt, readFolderData as u, deleteTranslation as v, imageNamesOf as w, fillImageMeta as x, duplicatePage as y, fsContentFiles as z };
@@ -1,4 +1,4 @@
1
- import { F as setPageCodec, P as setPageBlocks, i as toBlockMeta, n as richTextCodecFromBlocks, r as composedBlockMeta } from "./rich-text-codec-BXOKV94u.js";
1
+ import { F as setPageCodec, P as setPageBlocks, i as toBlockMeta, n as richTextCodecFromBlocks, r as composedBlockMeta } from "./rich-text-codec-BSD35k2U.js";
2
2
  import fs from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { SITE_MANIFEST_FILE, areFieldSchemasRegistered, localePath, registerFieldSchemas } from "mechanica-shared";
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
- import { t as buildSiteManifest } from "./chunks/site-Co04D3rB.js";
2
- import { $ as analyzeImageBuffer, G as isDerivedAsset, Y as updateImageManifest, et as hasSharp, q as readImageManifest, tt as isRasterImage } from "./chunks/rich-text-codec-BXOKV94u.js";
3
- import { a as git, c as PlatformError, d as request, f as requireToken, l as REMOTE_NAME, m as saveToken, o as gitAuthEnv, p as hostOrigin, r as runAssetsPull, s as gitOk, t as pushUploads, u as parseRemoteUrl } from "./chunks/assets-C7yHGJBU.js";
1
+ import { t as buildSiteManifest } from "./chunks/site-DfazSr3_.js";
2
+ import { J as readImageManifest, K as isDerivedAsset, W as fsAssetStore, X as updateImageManifest, et as withRemoteAssets, it as isRasterImage, nt as analyzeImageBuffer, rt as hasSharp } from "./chunks/rich-text-codec-BSD35k2U.js";
3
+ import { a as git, c as PlatformError, d as request, f as requireToken, i as uploadsIgnored, l as REMOTE_NAME, m as saveToken, n as remoteAssetsFor, o as gitAuthEnv, p as hostOrigin, r as runAssetsPull, s as gitOk, t as pushUploads, u as parseRemoteUrl } from "./chunks/assets-nujjbCx8.js";
4
4
  import { exportBuilt } from "./export.js";
5
5
  import fs, { readFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
@@ -128,13 +128,31 @@ async function writeSiteManifest(distDir) {
128
128
  //#endregion
129
129
  //#region src/cli/run-export.ts
130
130
  /**
131
+ * The uploads a local export reads: the project's `.mech/assets`, and — for a
132
+ * project linked to a hosted platform — the uploads kept there, fetched as the
133
+ * pages turn out to need them (and kept in `.mech/assets` when git ignores it).
134
+ * Only local files are *listed*: the uploads the platform holds that no page
135
+ * here refers to are not this export's to warn about.
136
+ */
137
+ function exportAssets(cwd) {
138
+ const local = fsAssetStore(join(cwd, ".mech"));
139
+ return {
140
+ ...withRemoteAssets(local, remoteAssetsFor(cwd), { cache: () => uploadsIgnored(cwd) }),
141
+ list: () => local.list()
142
+ };
143
+ }
144
+ /**
131
145
  * `mechanica export`: build the project, then statically render every page
132
146
  * into `export/`. Kept apart from `export.ts` so that module never imports
133
147
  * Vite — `mechanica/export` runs where only a built bundle exists.
134
148
  */
135
149
  async function runExport(options = {}) {
136
150
  await runBuild();
137
- const { pages } = await exportBuilt(process.cwd(), options);
151
+ const cwd = process.cwd();
152
+ const { pages } = await exportBuilt(cwd, {
153
+ assets: exportAssets(cwd),
154
+ ...options
155
+ });
138
156
  for (const path of pages) console.info("Generated", path);
139
157
  console.info(`Exported ${pages.length} page(s) → ${options.outDir ?? "export"}/`);
140
158
  }
package/dist/export.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as analyzeImageBuffer, E as listPages, F as setPageCodec, G as isDerivedAsset, H as assetFileOf, S as getPagePath, V as UPLOADS_PREFIX$1, Y as updateImageManifest, d as readFoldersData, et as hasSharp, f as readSiteData, i as toBlockMeta, p as readSiteLocaleOverride, q as readImageManifest, r as composedBlockMeta, t as buildRichTextCodec, u as readFolderData } from "./chunks/rich-text-codec-BXOKV94u.js";
1
+ import { $ as withHostedAssets, E as listPages, F as setPageCodec, H as assetFileOf, K as isDerivedAsset, S as getPagePath, V as UPLOADS_PREFIX$1, W as fsAssetStore, d as readFoldersData, f as readSiteData, i as toBlockMeta, nt as analyzeImageBuffer, p as readSiteLocaleOverride, r as composedBlockMeta, rt as hasSharp, t as buildRichTextCodec, u as readFolderData } from "./chunks/rich-text-codec-BSD35k2U.js";
2
2
  import { n as blockAssetLinks, t as BLOCKS_MANIFEST_FILE } from "./chunks/page-assets-DsJXmoDs.js";
3
3
  import { dirname, join, parse, relative, resolve } from "node:path";
4
4
  import { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, applySeoTags, auditPageHtml, buildRobotsTxt, buildSitemap, findUnknownBlocks, generateProject, isPaginatedQuery, localePath, mergeTranslation, migrateContent, paginationVariantPath, registerFieldSchemas, resolveQueryKey, templateBlockIds, validateLinks, walkSchema, walkTree } from "mechanica-shared";
@@ -218,45 +218,52 @@ function fillImageValue(value, entry) {
218
218
  if (entry.width && !value.width) value.width = entry.width;
219
219
  if (entry.height && !value.height) value.height = entry.height;
220
220
  }
221
+ /** Everything an upload's bytes come as, in one buffer. */
222
+ var bytesOf = async (body) => body instanceof Uint8Array ? Buffer.from(body) : Buffer.from(await new Response(body).arrayBuffer());
221
223
  /**
222
224
  * Fill missing image metadata — LQIP `previewSrc` + intrinsic dimensions —
223
225
  * into the state being exported (page files are never touched). Sources, in
224
- * order: the `.mech/images.json` manifest (written by uploads, editor saves
225
- * and `mechanica images`), then the optional `sharp` dependency for anything
226
- * still missing — those computed entries are cached back into the manifest.
227
- * Without sharp, leftover gaps get a one-time hint.
226
+ * order: what the asset store knows (written by uploads, editor saves and
227
+ * `mechanica images`), then the optional `sharp` dependency for anything still
228
+ * missing that the store can open — those computed entries are saved back to
229
+ * it. Without sharp, leftover gaps get a one-time hint.
228
230
  */
229
- async function backfillImageMeta(pages, blocksMap, mechDir, warn) {
230
- const manifest = readImageManifest(mechDir);
231
- const incomplete = /* @__PURE__ */ new Map();
231
+ async function backfillImageMeta(pages, blocksMap, assets, warn) {
232
+ const values = /* @__PURE__ */ new Map();
232
233
  for (const page of pages) walkTree(page.content, (block) => {
233
234
  const meta = blocksMap.get(block.blockId);
234
235
  if (!meta) return;
235
236
  walkSchema(block.data, meta.props, (value, schema) => {
236
237
  if (schema.format !== "image") return;
237
238
  const file = assetFileOf(value?.src);
238
- if (!file) return;
239
- const entry = manifest[file];
240
- if (entry) fillImageValue(value, entry);
241
- if (!value.previewSrc || value.previewSrc === value.src || !value.width || !value.height) incomplete.set(file, [...incomplete.get(file) ?? [], value]);
239
+ if (file) values.set(file, [...values.get(file) ?? [], value]);
242
240
  });
243
241
  });
242
+ if (!values.size) return;
243
+ const known = await assets.info([...values.keys()]);
244
+ const incomplete = /* @__PURE__ */ new Map();
245
+ for (const [file, list] of values) {
246
+ const entry = known[file];
247
+ const lacking = list.filter((value) => {
248
+ if (entry) fillImageValue(value, entry);
249
+ return !value.previewSrc || value.previewSrc === value.src || !value.width || !value.height;
250
+ });
251
+ if (lacking.length) incomplete.set(file, lacking);
252
+ }
244
253
  if (!incomplete.size) return;
245
254
  if (!await hasSharp()) {
246
255
  warn(`[mechanica] ${incomplete.size} image(s) lack a preview or dimensions — install the optional "sharp" dependency or run \`mechanica images\` to generate them`);
247
256
  return;
248
257
  }
249
258
  const computed = {};
250
- for (const [file, values] of incomplete) {
251
- const buffer = await readFile(join(mechDir, "assets", file)).catch(() => null);
252
- const info = buffer ? await analyzeImageBuffer(buffer) : null;
259
+ for (const [file, list] of incomplete) {
260
+ const body = await assets.open(file).catch(() => null);
261
+ const info = body ? await analyzeImageBuffer(await bytesOf(body)) : null;
253
262
  if (!info) continue;
254
263
  computed[file] = info;
255
- for (const value of values) fillImageValue(value, info);
264
+ for (const value of list) fillImageValue(value, info);
256
265
  }
257
- try {
258
- updateImageManifest(mechDir, computed);
259
- } catch {}
266
+ await assets.saveInfo(computed).catch(() => {});
260
267
  }
261
268
  /**
262
269
  * Statically render every page of an already-built project into `export/`,
@@ -277,6 +284,7 @@ async function backfillImageMeta(pages, blocksMap, mechDir, warn) {
277
284
  */
278
285
  async function exportProject(cwd, ssr, options = {}) {
279
286
  const warn = options.onWarn ?? ((message) => console.warn(message));
287
+ const assets = options.assets ?? fsAssetStore(join(cwd, ".mech"));
280
288
  const index = await readFile(join(cwd, "dist/index.html"), "utf-8");
281
289
  const siteUrl = options.siteUrl ?? ssr.site?.url;
282
290
  const siteName = options.siteName ?? ssr.site?.name;
@@ -374,7 +382,7 @@ async function exportProject(cwd, ssr, options = {}) {
374
382
  }
375
383
  if (!pages.some((page) => page.logicalPath === "/404")) warn("[mechanica] No /404 page — the static host will show its own error page. Create a page at /404 (it exports as 404.html and is noindexed automatically).");
376
384
  for (const issue of validateLinks(pages, blocksMap)) warn(`[mechanica] Broken link on ${issue.page}: ${issue.url} has no matching page`);
377
- await backfillImageMeta(pages, blocksMap, join(cwd, ".mech"), warn);
385
+ await backfillImageMeta(pages, blocksMap, assets, warn);
378
386
  const exportDir = resolve(cwd, options.outDir ?? "export");
379
387
  await mkdir(exportDir, { recursive: true });
380
388
  for (const entry of await readdir(exportDir)) await rm(join(exportDir, entry), {
@@ -435,8 +443,9 @@ async function exportProject(cwd, ssr, options = {}) {
435
443
  onFile,
436
444
  pageLinks
437
445
  };
446
+ const stored = await assets.list();
438
447
  const unclaimed = /* @__PURE__ */ new Map();
439
- for (const name of await readdir(join(cwd, ".mech/assets")).catch(() => [])) {
448
+ for (const name of stored) {
440
449
  const spellings = [
441
450
  name,
442
451
  encodeURI(name),
@@ -550,26 +559,41 @@ async function exportProject(cwd, ssr, options = {}) {
550
559
  }));
551
560
  recordSitemap(path, source, source.page.pagination.page);
552
561
  }
553
- await copyUploads(join(cwd, ".mech/assets"), join(exportDir, MEDIA_DIR), referenced, warn);
562
+ const uploads = await copyUploads(assets, new Set(stored), join(exportDir, MEDIA_DIR), referenced, warn);
554
563
  if (written.includes("/404")) await copyFile(join(exportDir, "404/index.html"), join(exportDir, "404.html"));
555
564
  if (siteUrl) {
556
565
  await writeFile(join(exportDir, "sitemap.xml"), buildSitemap(siteUrl, sitemapEntries));
557
566
  const robotsPath = join(exportDir, "robots.txt");
558
567
  if (!await access(robotsPath).then(() => true, () => false)) await writeFile(robotsPath, buildRobotsTxt(siteUrl));
559
568
  }
560
- return written;
569
+ return {
570
+ pages: written,
571
+ uploads
572
+ };
561
573
  }
562
- /** Copy referenced uploads into the export; warn about missing + orphaned files. */
563
- async function copyUploads(assetsDir, outDir, referenced, warn) {
564
- for (const relative of referenced) {
565
- const target = join(outDir, relative);
566
- await mkdir(dirname(target), { recursive: true });
567
- await copyFile(join(assetsDir, relative), target).catch(() => {
568
- warn(`[mechanica] Missing upload: ${relative} is referenced by a page but not in .mech/assets`);
569
- });
574
+ /**
575
+ * Put the referenced uploads into the export: copy the ones the store can open,
576
+ * leave to the host the ones it only knows about, warn about the ones it does
577
+ * not know — and about uploads no page refers to.
578
+ */
579
+ async function copyUploads(assets, stored, outDir, referenced, warn) {
580
+ const uploads = {
581
+ copied: [],
582
+ hosted: []
583
+ };
584
+ for (const name of referenced) {
585
+ const body = await assets.open(name).catch(() => null);
586
+ if (body) {
587
+ const target = join(outDir, name);
588
+ await mkdir(dirname(target), { recursive: true });
589
+ await writeFile(target, await bytesOf(body));
590
+ uploads.copied.push(name);
591
+ } else if (stored.has(name)) uploads.hosted.push(name);
592
+ else warn(`[mechanica] Missing upload: ${name} is referenced by a page but is not among the site's uploads`);
570
593
  }
571
- const orphans = (await readdir(assetsDir, { recursive: true }).catch(() => [])).map((entry) => entry.replace(/\\/g, "/")).filter((entry) => /\.\w+$/.test(entry) && !referenced.has(entry) && !isDerivedAsset(entry));
572
- if (orphans.length) warn(`[mechanica] ${orphans.length} unused upload(s) in .mech/assets (not exported): ${orphans.join(", ")}`);
594
+ const orphans = [...stored].filter((name) => /\.\w+$/.test(name) && !referenced.has(name) && !isDerivedAsset(name));
595
+ if (orphans.length) warn(`[mechanica] ${orphans.length} unused upload(s) (not exported): ${orphans.join(", ")}`);
596
+ return uploads;
573
597
  }
574
598
  /**
575
599
  * Statically render an already-built project: load `<cwd>/dist/ssr.js` and run
@@ -585,13 +609,15 @@ async function exportBuilt(cwd, options = {}) {
585
609
  warnings.push(message);
586
610
  (options.onWarn ?? console.warn)(message);
587
611
  };
612
+ const { pages, uploads } = await exportProject(cwd, await import(pathToFileURL(join(cwd, "dist/ssr.js")).href), {
613
+ ...options,
614
+ onWarn
615
+ });
588
616
  return {
589
- pages: await exportProject(cwd, await import(pathToFileURL(join(cwd, "dist/ssr.js")).href), {
590
- ...options,
591
- onWarn
592
- }),
593
- warnings
617
+ pages,
618
+ warnings,
619
+ uploads
594
620
  };
595
621
  }
596
622
  //#endregion
597
- export { exportBuilt, exportProject };
623
+ export { exportBuilt, exportProject, fsAssetStore, withHostedAssets };
package/dist/plugin.js CHANGED
@@ -1,7 +1,7 @@
1
- import { a as collectGeneratedPages, n as configureSite, r as indexGeneratedPages, t as buildSiteManifest } from "./chunks/site-Co04D3rB.js";
2
- import { a as renderEditablePage, c as fillStateImages, d as loadComposedDefinitions, n as toNodeMiddleware, o as buildGeneratedState, r as createEditorService, s as buildPageState, t as toAssetMiddleware, u as collectComposed } from "./chunks/node-adapter-GZZrfGSz.js";
3
- import { Q as wasRecentlyMutated, U as fsAssetStore, Z as withRemoteAssets, k as pageUrlOf } from "./chunks/rich-text-codec-BXOKV94u.js";
4
- import { i as uploadsIgnored, n as remoteAssetsFor } from "./chunks/assets-C7yHGJBU.js";
1
+ import { a as collectGeneratedPages, n as configureSite, r as indexGeneratedPages, t as buildSiteManifest } from "./chunks/site-DfazSr3_.js";
2
+ import { a as renderEditablePage, c as fillStateImages, d as loadComposedDefinitions, n as toNodeMiddleware, o as buildGeneratedState, r as createEditorService, s as buildPageState, t as toAssetMiddleware, u as collectComposed } from "./chunks/node-adapter-CSkDXYGO.js";
3
+ import { W as fsAssetStore, et as withRemoteAssets, k as pageUrlOf, tt as wasRecentlyMutated } from "./chunks/rich-text-codec-BSD35k2U.js";
4
+ import { i as uploadsIgnored, n as remoteAssetsFor } from "./chunks/assets-nujjbCx8.js";
5
5
  import { t as BLOCKS_MANIFEST_FILE } from "./chunks/page-assets-DsJXmoDs.js";
6
6
  import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { join, relative } from "node:path";
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
- import { i as readSiteManifest, n as configureSite, t as buildSiteManifest } from "./chunks/site-Co04D3rB.js";
2
- import { a as renderEditablePage, i as readEditorTemplate, n as toNodeMiddleware, r as createEditorService, t as toAssetMiddleware } from "./chunks/node-adapter-GZZrfGSz.js";
3
- import { B as memoryContentFiles, G as isDerivedAsset, O as pageOfFile, U as fsAssetStore, X as uploadName, Z as withRemoteAssets, z as fsContentFiles } from "./chunks/rich-text-codec-BXOKV94u.js";
1
+ import { i as readSiteManifest, n as configureSite, t as buildSiteManifest } from "./chunks/site-DfazSr3_.js";
2
+ import { a as renderEditablePage, i as readEditorTemplate, n as toNodeMiddleware, r as createEditorService, t as toAssetMiddleware } from "./chunks/node-adapter-CSkDXYGO.js";
3
+ import { $ as withHostedAssets, B as memoryContentFiles, G as isAssetName, K as isDerivedAsset, O as pageOfFile, Q as uploadResponse, U as contentTypeOf, W as fsAssetStore, Z as uploadName, et as withRemoteAssets, z as fsContentFiles } from "./chunks/rich-text-codec-BSD35k2U.js";
4
4
  import { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, UPLOADS_PREFIX, uploadNameOf } from "mechanica-shared";
5
- export { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, UPLOADS_PREFIX, buildSiteManifest, configureSite, createEditorService, fsAssetStore, fsContentFiles, isDerivedAsset, memoryContentFiles, pageOfFile, readEditorTemplate, readSiteManifest, renderEditablePage, toAssetMiddleware, toNodeMiddleware, uploadName, uploadNameOf, withRemoteAssets };
5
+ export { EDITOR_DIST_DIR, SITE_MANIFEST_FILE, UPLOADS_PREFIX, buildSiteManifest, configureSite, contentTypeOf, createEditorService, fsAssetStore, fsContentFiles, isAssetName, isDerivedAsset, memoryContentFiles, pageOfFile, readEditorTemplate, readSiteManifest, renderEditablePage, toAssetMiddleware, toNodeMiddleware, uploadName, uploadNameOf, uploadResponse, withHostedAssets, withRemoteAssets };
@@ -1,5 +1,6 @@
1
1
  import { type ComposedBlockDefinition, type LocalesConfig, type RenderResult, type VirtualPage } from 'mechanica-shared';
2
2
  import { type BlockComponent } from '../editor/lib/block-meta';
3
+ import { fsAssetStore, withHostedAssets, type AssetStore, type ImageManifest } from '../server/assets-store';
3
4
  /** An already-built SSR bundle (`dist/ssr.js`) exposes these. */
4
5
  export interface SsrBundle {
5
6
  /**
@@ -69,6 +70,24 @@ export interface ExportOptions {
69
70
  fetchJson?: (options: {
70
71
  url: string;
71
72
  } & Record<string, unknown>) => Promise<unknown>;
73
+ /**
74
+ * Where the site's uploads are: their bytes and their image info. Defaults to
75
+ * the project's `.mech/assets` + `.mech/images.json`. The export reads uploads
76
+ * only through this — a linked project passes a store that also fetches the
77
+ * ones kept by its platform, and a hosted render one that knows uploads it
78
+ * cannot open (`withHostedAssets`): those are not copied, the host serves them.
79
+ */
80
+ assets?: AssetStore;
81
+ }
82
+ /** What became of the uploads the pages refer to. */
83
+ export interface ExportedUploads {
84
+ /** Copied into the export's `media/` directory. */
85
+ copied: string[];
86
+ /**
87
+ * Known to the store but not readable through it — left for the host to
88
+ * serve at `/media/<name>`. Empty for an export over a plain directory.
89
+ */
90
+ hosted: string[];
72
91
  }
73
92
  /**
74
93
  * Statically render every page of an already-built project into `export/`,
@@ -87,13 +106,18 @@ export interface ExportOptions {
87
106
  * absolute social-image URLs, pagination prev/next, JSON-LD) and emits
88
107
  * `sitemap.xml` + `robots.txt`.
89
108
  */
90
- export declare function exportProject(cwd: string, ssr: SsrBundle, options?: ExportOptions): Promise<string[]>;
109
+ export declare function exportProject(cwd: string, ssr: SsrBundle, options?: ExportOptions): Promise<{
110
+ pages: string[];
111
+ uploads: ExportedUploads;
112
+ }>;
91
113
  /** What {@link exportBuilt} produced. */
92
114
  export interface ExportResult {
93
115
  /** The generated page paths, in render order. */
94
116
  pages: string[];
95
117
  /** Everything the export warned about (broken links, missing uploads, SEO lint). */
96
118
  warnings: string[];
119
+ /** The uploads the pages refer to: which were copied, which the host is to serve. */
120
+ uploads: ExportedUploads;
97
121
  }
98
122
  /**
99
123
  * Statically render an already-built project: load `<cwd>/dist/ssr.js` and run
@@ -103,3 +127,5 @@ export interface ExportResult {
103
127
  * Warnings are collected into the result and still forwarded to `onWarn`.
104
128
  */
105
129
  export declare function exportBuilt(cwd: string, options?: ExportOptions): Promise<ExportResult>;
130
+ export { fsAssetStore, withHostedAssets };
131
+ export type { AssetStore, ImageManifest };
@@ -1,4 +1,13 @@
1
+ import { type AssetStore } from '../server/assets-store';
1
2
  import { type ExportOptions } from './export';
3
+ /**
4
+ * The uploads a local export reads: the project's `.mech/assets`, and — for a
5
+ * project linked to a hosted platform — the uploads kept there, fetched as the
6
+ * pages turn out to need them (and kept in `.mech/assets` when git ignores it).
7
+ * Only local files are *listed*: the uploads the platform holds that no page
8
+ * here refers to are not this export's to warn about.
9
+ */
10
+ export declare function exportAssets(cwd: string): AssetStore;
2
11
  /**
3
12
  * `mechanica export`: build the project, then statically render every page
4
13
  * into `export/`. Kept apart from `export.ts` so that module never imports
@@ -84,6 +84,25 @@ export declare function listUploads(assets: AssetStore): Promise<{
84
84
  name: string;
85
85
  src: string;
86
86
  }[]>;
87
+ /** The content type a stored file is served with, when its extension says. */
88
+ export declare const contentTypeOf: (fileName: string) => string | undefined;
89
+ /**
90
+ * The HTTP answer for an upload asked for by its address — the part of
91
+ * `/media/<name>` after the prefix, still percent-encoded. null when it is not
92
+ * an upload of this store, so the caller can fall through; a 400 for a name
93
+ * that is not valid encoding. One function for every host that serves uploads:
94
+ * the dev server, an editing host, a site's live host.
95
+ */
96
+ export declare function uploadResponse(assets: AssetStore | null | undefined, encodedName: string): Promise<Response | null>;
97
+ /**
98
+ * A store for rendering a site whose uploads the host keeps and serves itself.
99
+ * `hosted` is what the host knows about them — a name with its image info. The
100
+ * result knows those names (`list`, `info`) but cannot open them: an export
101
+ * over it fills in their dimensions and previews, copies no bytes, and reports
102
+ * them as the host's to serve (see `ExportResult.uploads`). Whatever the local
103
+ * store has is used as before.
104
+ */
105
+ export declare function withHostedAssets(local: AssetStore, hosted: ImageManifest): AssetStore;
87
106
  /**
88
107
  * Where a project's uploads also live — a hosted platform the project is linked
89
108
  * to. Uploads made in its online editor are not in the project's directory
@@ -8,7 +8,7 @@ export { createEditorService, type EditorService, type EditorServiceOptions, typ
8
8
  export { toNodeMiddleware, toAssetMiddleware, type NodeMiddleware } from './node-adapter';
9
9
  export { fsContentFiles, memoryContentFiles, type ContentFiles, type ContentChanges, type MemoryContentFiles, type Mech, } from './content-files';
10
10
  export { pageOfFile } from './pages-store';
11
- export { fsAssetStore, withRemoteAssets, uploadName, isDerivedAsset, type AssetStore, type RemoteAssets, type RemoteAssetsOptions, type ImageManifest, type ImageManifestEntry, } from './assets-store';
11
+ export { fsAssetStore, withRemoteAssets, withHostedAssets, uploadResponse, contentTypeOf, isAssetName, uploadName, isDerivedAsset, type AssetStore, type RemoteAssets, type RemoteAssetsOptions, type ImageManifest, type ImageManifestEntry, } from './assets-store';
12
12
  export { buildSiteManifest, readSiteManifest, configureSite, type SiteManifestInput } from './site';
13
13
  export { renderEditablePage, readEditorTemplate, type EditablePageOptions } from './editor-page';
14
14
  export { SITE_MANIFEST_FILE, EDITOR_DIST_DIR, UPLOADS_PREFIX, uploadNameOf, type SiteManifest, type EditorHostConfig, } from 'mechanica-shared';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mechanica",
3
- "version": "2.1.0-beta.5",
3
+ "version": "2.1.0-beta.6",
4
4
  "description": "Build Vue 3 websites with a visual block editor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -66,7 +66,7 @@
66
66
  "prepublishOnly": "bun ../../scripts/ensure-bun-publish.js && bun run build"
67
67
  },
68
68
  "dependencies": {
69
- "mechanica-shared": "2.1.0-beta.5",
69
+ "mechanica-shared": "2.1.0-beta.6",
70
70
  "@vue/compiler-sfc": "^3.5.38",
71
71
  "compact-json-schema": "^0.1.5",
72
72
  "estree-walker": "^3.0.3",