@nuasite/cms-sidecar 0.43.0-beta.3 → 0.43.0-beta.8

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/dist/cli.js CHANGED
@@ -21791,6 +21791,7 @@ var import_parser = __toESM(require_lib(), 1);
21791
21791
  var FIELD_TYPES = [
21792
21792
  "text",
21793
21793
  "textarea",
21794
+ "markdown",
21794
21795
  "date",
21795
21796
  "datetime",
21796
21797
  "time",
@@ -21828,7 +21829,8 @@ var FIELD_HELPER_TYPES = new Set([
21828
21829
  "time",
21829
21830
  "year",
21830
21831
  "month",
21831
- "textarea"
21832
+ "textarea",
21833
+ "markdown"
21832
21834
  ]);
21833
21835
  var VALID_HINT_KEYS = new Set([
21834
21836
  "min",
@@ -21943,6 +21945,8 @@ function parseConfigSource(source, _sourcePath) {
21943
21945
  const loaderOptions = loaderProperty?.type === "ObjectProperty" ? extractGlobLoaderOptions(loaderProperty.value, bindings) : {};
21944
21946
  const loaderPattern = loaderOptions.pattern;
21945
21947
  const loaderBase = loaderOptions.base;
21948
+ const cmsProperty = decl.properties.find((p) => p.type === "ObjectProperty" && propertyKeyName(p.key) === "cms");
21949
+ const layout = cmsProperty?.type === "ObjectProperty" ? parseCmsLayout(cmsProperty.value, bindings) : undefined;
21946
21950
  const schemaProperty = decl.properties.find((p) => p.type === "ObjectProperty" && propertyKeyName(p.key) === "schema");
21947
21951
  if (!schemaProperty || schemaProperty.type !== "ObjectProperty") {
21948
21952
  if (!loaderPattern)
@@ -21951,7 +21955,8 @@ function parseConfigSource(source, _sourcePath) {
21951
21955
  name: collectionName,
21952
21956
  fields: [],
21953
21957
  loaderPattern,
21954
- loaderBase
21958
+ loaderBase,
21959
+ layout
21955
21960
  });
21956
21961
  continue;
21957
21962
  }
@@ -21963,7 +21968,8 @@ function parseConfigSource(source, _sourcePath) {
21963
21968
  name: collectionName,
21964
21969
  fields: [],
21965
21970
  loaderPattern,
21966
- loaderBase
21971
+ loaderBase,
21972
+ layout
21967
21973
  });
21968
21974
  continue;
21969
21975
  }
@@ -21971,13 +21977,69 @@ function parseConfigSource(source, _sourcePath) {
21971
21977
  name: collectionName,
21972
21978
  fields: parseSchemaFields(schemaObject, bindings),
21973
21979
  loaderPattern,
21974
- loaderBase
21980
+ loaderBase,
21981
+ layout
21975
21982
  });
21976
21983
  }
21977
21984
  return result;
21978
21985
  }
21986
+ function parseCmsLayout(node, bindings) {
21987
+ const resolved = resolveExpression(node, bindings);
21988
+ if (resolved.type !== "ObjectExpression")
21989
+ return;
21990
+ const layout = {};
21991
+ for (const prop of resolved.properties) {
21992
+ if (prop.type !== "ObjectProperty")
21993
+ continue;
21994
+ const key = propertyKeyName(prop.key);
21995
+ const value = resolveExpression(prop.value, bindings);
21996
+ if (key === "display") {
21997
+ if (value.type === "StringLiteral" && (value.value === "tabs" || value.value === "sections"))
21998
+ layout.display = value.value;
21999
+ } else if (key === "sidebar") {
22000
+ if (value.type === "ArrayExpression")
22001
+ layout.sidebar = stringArray(value);
22002
+ } else if (key === "sections") {
22003
+ if (value.type === "ArrayExpression") {
22004
+ const sections = value.elements.map((el) => el && el.type !== "SpreadElement" ? parseLayoutSection(resolveExpression(el, bindings)) : null).filter((s) => s !== null);
22005
+ if (sections.length > 0)
22006
+ layout.sections = sections;
22007
+ }
22008
+ }
22009
+ }
22010
+ return Object.keys(layout).length > 0 ? layout : undefined;
22011
+ }
22012
+ function parseLayoutSection(node) {
22013
+ if (node.type !== "ObjectExpression")
22014
+ return null;
22015
+ let title;
22016
+ let fields = [];
22017
+ let collapsed = false;
22018
+ for (const prop of node.properties) {
22019
+ if (prop.type !== "ObjectProperty")
22020
+ continue;
22021
+ const key = propertyKeyName(prop.key);
22022
+ if (key === "title" && prop.value.type === "StringLiteral")
22023
+ title = prop.value.value;
22024
+ else if (key === "fields" && prop.value.type === "ArrayExpression")
22025
+ fields = stringArray(prop.value);
22026
+ else if (key === "collapsed" && prop.value.type === "BooleanLiteral")
22027
+ collapsed = prop.value.value;
22028
+ }
22029
+ if (title === undefined || fields.length === 0)
22030
+ return null;
22031
+ return collapsed ? { title, fields, collapsed } : { title, fields };
22032
+ }
22033
+ function stringArray(node) {
22034
+ const out = [];
22035
+ for (const el of node.elements) {
22036
+ if (el?.type === "StringLiteral")
22037
+ out.push(el.value);
22038
+ }
22039
+ return out;
22040
+ }
21979
22041
  function isDefineCollectionCallee(callee) {
21980
- return callee.type === "Identifier" && callee.name === "defineCollection";
22042
+ return callee.type === "Identifier" && (callee.name === "defineCollection" || callee.name === "defineCmsCollection");
21981
22043
  }
21982
22044
  function propertyKeyName(key) {
21983
22045
  if (key.type === "Identifier")
@@ -22127,6 +22189,9 @@ function analyzeBaseCall(node, field, bindings) {
22127
22189
  const hints = parseHintsFromObject(firstArg);
22128
22190
  if (hints)
22129
22191
  field.hints = hints;
22192
+ const layout = parseFieldLayoutFromObject(firstArg);
22193
+ if (layout)
22194
+ field.layout = layout;
22130
22195
  }
22131
22196
  return;
22132
22197
  }
@@ -22219,6 +22284,49 @@ function assignHint(hints, key, value) {
22219
22284
  return;
22220
22285
  }
22221
22286
  }
22287
+ function parseFieldLayoutFromObject(obj) {
22288
+ const layout = {};
22289
+ for (const prop of obj.properties) {
22290
+ if (prop.type !== "ObjectProperty")
22291
+ continue;
22292
+ const key = propertyKeyName(prop.key);
22293
+ const value = prop.value;
22294
+ switch (key) {
22295
+ case "label":
22296
+ if (value.type === "StringLiteral")
22297
+ layout.label = value.value;
22298
+ break;
22299
+ case "help":
22300
+ if (value.type === "StringLiteral")
22301
+ layout.help = value.value;
22302
+ break;
22303
+ case "group":
22304
+ if (value.type === "StringLiteral")
22305
+ layout.group = value.value;
22306
+ break;
22307
+ case "width":
22308
+ if (value.type === "StringLiteral" && (value.value === "full" || value.value === "half"))
22309
+ layout.width = value.value;
22310
+ break;
22311
+ case "order":
22312
+ if (value.type === "NumericLiteral") {
22313
+ layout.order = value.value;
22314
+ } else if (value.type === "UnaryExpression" && value.operator === "-" && value.argument.type === "NumericLiteral") {
22315
+ layout.order = -value.argument.value;
22316
+ }
22317
+ break;
22318
+ case "sidebar":
22319
+ if (value.type === "BooleanLiteral")
22320
+ layout.sidebar = value.value;
22321
+ break;
22322
+ case "hidden":
22323
+ if (value.type === "BooleanLiteral")
22324
+ layout.hidden = value.value;
22325
+ break;
22326
+ }
22327
+ }
22328
+ return Object.keys(layout).length > 0 ? layout : undefined;
22329
+ }
22222
22330
 
22223
22331
  // ../cms-core/src/shared.ts
22224
22332
  function slugifyHref(text) {
@@ -22548,6 +22656,41 @@ async function buildCollectionDefinition(fs, basePath, sources, collectionName,
22548
22656
  assignFieldMetadata(def.fields, allDirectives);
22549
22657
  return def;
22550
22658
  }
22659
+ async function buildDataCollectionDefinition(fs, basePath, sources, collectionName, contentDir) {
22660
+ if (sources.length === 0)
22661
+ return null;
22662
+ const sourceBasePath = getCollectionSourceBasePath(basePath, collectionName, contentDir);
22663
+ const fieldMap = new Map;
22664
+ const entryInfos = [];
22665
+ const ext = sources.some((s) => s.relPath.endsWith(".json")) ? "json" : sources.some((s) => s.relPath.endsWith(".yaml")) ? "yaml" : "yml";
22666
+ const fileContents = await Promise.all(sources.map((s) => fs.readFile(path.join(basePath, s.relPath)).catch(() => null)));
22667
+ for (let i = 0;i < sources.length; i++) {
22668
+ const source = sources[i];
22669
+ const raw = fileContents[i];
22670
+ if (raw === null)
22671
+ continue;
22672
+ let data = null;
22673
+ try {
22674
+ data = source.relPath.endsWith(".json") ? JSON.parse(raw) : import_yaml.parse(raw);
22675
+ } catch {
22676
+ continue;
22677
+ }
22678
+ if (!data || typeof data !== "object")
22679
+ continue;
22680
+ const title = typeof data.name === "string" ? data.name : typeof data.title === "string" ? data.title : undefined;
22681
+ entryInfos.push({
22682
+ slug: source.slug,
22683
+ title,
22684
+ sourcePath: path.join(sourceBasePath, source.relPath),
22685
+ data
22686
+ });
22687
+ collectFieldObservations(fieldMap, data, sources.length);
22688
+ }
22689
+ return assembleCollectionDefinition(collectionName, contentDir, fieldMap, entryInfos, sources.length, {
22690
+ type: "data",
22691
+ fileExtension: ext
22692
+ });
22693
+ }
22551
22694
  async function scanCollection(fs, collectionPath, collectionName, contentDir) {
22552
22695
  const dirEntries = await fs.list(collectionPath);
22553
22696
  if (dirEntries.length === 0)
@@ -22585,10 +22728,16 @@ async function scanCollection(fs, collectionPath, collectionName, contentDir) {
22585
22728
  }
22586
22729
  async function scanGlobCollection(fs, collectionName, baseRel, pattern, contentDir) {
22587
22730
  const matches = await fs.glob(path.join(baseRel, pattern));
22588
- const sources = matches.filter((rel) => rel.endsWith(".md") || rel.endsWith(".mdx")).map((rel) => path.relative(baseRel, rel)).filter((relToBase) => !relToBase.split("/").some((seg) => seg.startsWith("_") || seg.startsWith("."))).map((relToBase) => ({ slug: relToBase.replace(/\.(md|mdx)$/, ""), relPath: relToBase }));
22589
- if (sources.length === 0)
22590
- return null;
22591
- return await buildCollectionDefinition(fs, baseRel, sources, collectionName, contentDir);
22731
+ const rels = matches.map((rel) => path.relative(baseRel, rel)).filter((relToBase) => !relToBase.split("/").some((seg) => seg.startsWith("_") || seg.startsWith(".")));
22732
+ const mdSources = rels.filter((relToBase) => relToBase.endsWith(".md") || relToBase.endsWith(".mdx")).map((relToBase) => ({ slug: relToBase.replace(/\.(md|mdx)$/, ""), relPath: relToBase }));
22733
+ if (mdSources.length > 0) {
22734
+ return await buildCollectionDefinition(fs, baseRel, mdSources, collectionName, contentDir);
22735
+ }
22736
+ const dataSources = rels.filter((relToBase) => /\.(ya?ml|json)$/.test(relToBase)).map((relToBase) => ({ slug: relToBase.replace(/\.(ya?ml|json)$/, ""), relPath: relToBase }));
22737
+ if (dataSources.length > 0) {
22738
+ return await buildDataCollectionDefinition(fs, baseRel, dataSources, collectionName, contentDir);
22739
+ }
22740
+ return null;
22592
22741
  }
22593
22742
  function applyParsedConfig(collections, parsed) {
22594
22743
  for (const [collectionName, parsedColl] of parsed) {
@@ -22606,8 +22755,29 @@ function applyParsedConfig(collections, parsed) {
22606
22755
  continue;
22607
22756
  applyParsedFieldOverrides(field, pf);
22608
22757
  }
22758
+ if (parsedColl.layout)
22759
+ def.layout = parsedColl.layout;
22609
22760
  }
22610
22761
  }
22762
+ function applyParsedFieldLayout(field, pf) {
22763
+ const layout = pf.layout;
22764
+ if (!layout)
22765
+ return;
22766
+ if (layout.label !== undefined)
22767
+ field.label = layout.label;
22768
+ if (layout.help !== undefined)
22769
+ field.help = layout.help;
22770
+ if (layout.group !== undefined)
22771
+ field.group = layout.group;
22772
+ if (layout.width !== undefined)
22773
+ field.width = layout.width;
22774
+ if (layout.order !== undefined)
22775
+ field.order = layout.order;
22776
+ if (layout.sidebar)
22777
+ field.position = "sidebar";
22778
+ if (layout.hidden)
22779
+ field.hidden = true;
22780
+ }
22611
22781
  function applyParsedFieldOverrides(field, pf) {
22612
22782
  if (pf.type) {
22613
22783
  field.type = pf.type;
@@ -22621,6 +22791,7 @@ function applyParsedFieldOverrides(field, pf) {
22621
22791
  if (pf.astroImage)
22622
22792
  field.astroImage = true;
22623
22793
  field.required = pf.required;
22794
+ applyParsedFieldLayout(field, pf);
22624
22795
  if (pf.fields) {
22625
22796
  const existingByName = new Map((field.fields ?? []).map((f) => [f.name, f]));
22626
22797
  field.fields = pf.fields.map((subPf) => {
@@ -22649,6 +22820,7 @@ function parsedFieldToFieldDefinition(pf) {
22649
22820
  fd.astroImage = true;
22650
22821
  if (pf.fields)
22651
22822
  fd.fields = pf.fields.map(parsedFieldToFieldDefinition);
22823
+ applyParsedFieldLayout(fd, pf);
22652
22824
  return fd;
22653
22825
  }
22654
22826
  function applyCollectionOrderBy(collections, parsed) {
@@ -22874,38 +23046,7 @@ async function scanDataCollection(fs, collectionPath, collectionName, contentDir
22874
23046
  if (entry)
22875
23047
  sources.push(entry);
22876
23048
  }
22877
- if (sources.length === 0)
22878
- return null;
22879
- const fieldMap = new Map;
22880
- const entryInfos = [];
22881
- const ext = sources.some((s) => s.relPath.endsWith(".json")) ? "json" : sources.some((s) => s.relPath.endsWith(".yaml")) ? "yaml" : "yml";
22882
- const fileContents = await Promise.all(sources.map((s) => fs.readFile(path.join(collectionPath, s.relPath)).catch(() => null)));
22883
- for (let i = 0;i < sources.length; i++) {
22884
- const source = sources[i];
22885
- const raw = fileContents[i];
22886
- if (raw === null)
22887
- continue;
22888
- let data = null;
22889
- try {
22890
- data = source.relPath.endsWith(".json") ? JSON.parse(raw) : import_yaml.parse(raw);
22891
- } catch {
22892
- continue;
22893
- }
22894
- if (!data || typeof data !== "object")
22895
- continue;
22896
- const title = typeof data.name === "string" ? data.name : typeof data.title === "string" ? data.title : undefined;
22897
- entryInfos.push({
22898
- slug: source.slug,
22899
- title,
22900
- sourcePath: path.join(contentDir, collectionName, source.relPath),
22901
- data
22902
- });
22903
- collectFieldObservations(fieldMap, data, sources.length);
22904
- }
22905
- return assembleCollectionDefinition(collectionName, contentDir, fieldMap, entryInfos, sources.length, {
22906
- type: "data",
22907
- fileExtension: ext
22908
- });
23049
+ return await buildDataCollectionDefinition(fs, collectionPath, sources, collectionName, contentDir);
22909
23050
  }
22910
23051
  async function scanCollections(fs, contentDir = "src/content", parseCache = new Map) {
22911
23052
  const collections = {};
@@ -23190,6 +23331,123 @@ function extractPreviewWidth(content) {
23190
23331
  }
23191
23332
  // ../cms-core/src/handlers/entry-ops.ts
23192
23333
  var import_yaml2 = __toESM(require_dist(), 1);
23334
+
23335
+ // ../cms-core/src/media/local.ts
23336
+ import { randomUUID } from "crypto";
23337
+ import fs from "fs/promises";
23338
+ import path3 from "path";
23339
+ function createLocalStorageAdapter(options = {}) {
23340
+ const dir = path3.resolve(options.dir ?? "public/uploads");
23341
+ const urlPrefix = (options.urlPrefix ?? "/uploads").replace(/\/$/, "");
23342
+ return {
23343
+ staticFiles: { urlPrefix, dir },
23344
+ async list(opts) {
23345
+ const limit = opts?.limit ?? 50;
23346
+ const offset = opts?.cursor ? parseInt(opts.cursor, 10) : 0;
23347
+ const folder = opts?.folder ?? "";
23348
+ const targetDir = folder ? path3.join(dir, folder) : dir;
23349
+ await fs.mkdir(targetDir, { recursive: true });
23350
+ const entries = await fs.readdir(targetDir, { withFileTypes: true });
23351
+ const folders = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
23352
+ name: e.name,
23353
+ path: folder ? `${folder}/${e.name}` : e.name
23354
+ })).sort((a, b) => a.name.localeCompare(b.name));
23355
+ const files = entries.filter((e) => e.isFile() && !e.name.startsWith("."));
23356
+ const withStats = await Promise.all(files.map(async (f) => {
23357
+ const filePath = path3.join(targetDir, f.name);
23358
+ const stat = await fs.stat(filePath);
23359
+ return { name: f.name, stat };
23360
+ }));
23361
+ withStats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
23362
+ const slice = withStats.slice(offset, offset + limit);
23363
+ const hasMore = offset + limit < withStats.length;
23364
+ const urlFolder = folder ? `/${folder}` : "";
23365
+ const items = slice.map((f) => {
23366
+ const ext = path3.extname(f.name).toLowerCase();
23367
+ const contentType = mimeFromExt(ext);
23368
+ return {
23369
+ id: folder ? `${folder}/${f.name}` : f.name,
23370
+ url: `${urlPrefix}${urlFolder}/${f.name}`,
23371
+ filename: f.name,
23372
+ contentType,
23373
+ uploadedAt: f.stat.mtime.toISOString(),
23374
+ folder: folder || undefined
23375
+ };
23376
+ });
23377
+ return {
23378
+ items,
23379
+ folders,
23380
+ hasMore,
23381
+ cursor: hasMore ? String(offset + limit) : undefined
23382
+ };
23383
+ },
23384
+ async upload(file, filename, contentType, uploadOpts) {
23385
+ const folder = uploadOpts?.folder ?? "";
23386
+ const targetDir = folder ? path3.join(dir, folder) : dir;
23387
+ await fs.mkdir(targetDir, { recursive: true });
23388
+ const ext = getFileExtension(filename);
23389
+ const uuid = randomUUID();
23390
+ const newFilename = `${uuid}${ext ? `.${ext}` : ""}`;
23391
+ const filePath = path3.join(targetDir, newFilename);
23392
+ await fs.writeFile(filePath, file);
23393
+ const urlFolder = folder ? `/${folder}` : "";
23394
+ const id = folder ? `${folder}/${newFilename}` : newFilename;
23395
+ return {
23396
+ success: true,
23397
+ url: `${urlPrefix}${urlFolder}/${newFilename}`,
23398
+ filename: newFilename,
23399
+ id
23400
+ };
23401
+ },
23402
+ async delete(id) {
23403
+ const safePath = id.split("/").map((s) => path3.basename(s)).join("/");
23404
+ const filePath = path3.join(dir, safePath);
23405
+ try {
23406
+ await fs.unlink(filePath);
23407
+ return { success: true };
23408
+ } catch (error) {
23409
+ const message = error instanceof Error ? error.message : String(error);
23410
+ return { success: false, error: message };
23411
+ }
23412
+ },
23413
+ async createFolder(folder) {
23414
+ const segments = folder.split("/").filter(Boolean);
23415
+ if (segments.some((s) => s === ".." || s === ".")) {
23416
+ return { success: false, error: "Invalid folder name" };
23417
+ }
23418
+ try {
23419
+ await fs.mkdir(path3.join(dir, ...segments), { recursive: true });
23420
+ return { success: true };
23421
+ } catch (error) {
23422
+ const message = error instanceof Error ? error.message : String(error);
23423
+ return { success: false, error: message };
23424
+ }
23425
+ }
23426
+ };
23427
+ }
23428
+ function getFileExtension(filename) {
23429
+ const parts = filename.split(".");
23430
+ const ext = parts.length > 1 ? parts.pop()?.toLowerCase() ?? "" : "";
23431
+ return /^[a-z0-9]+$/.test(ext) ? ext : "";
23432
+ }
23433
+ var MIME_BY_EXT = {
23434
+ ".jpg": "image/jpeg",
23435
+ ".jpeg": "image/jpeg",
23436
+ ".png": "image/png",
23437
+ ".gif": "image/gif",
23438
+ ".webp": "image/webp",
23439
+ ".avif": "image/avif",
23440
+ ".svg": "image/svg+xml",
23441
+ ".ico": "image/x-icon",
23442
+ ".mp4": "video/mp4",
23443
+ ".webm": "video/webm",
23444
+ ".pdf": "application/pdf"
23445
+ };
23446
+ function mimeFromExt(ext) {
23447
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
23448
+ }
23449
+
23450
+ // ../cms-core/src/handlers/entry-ops.ts
23193
23451
  var MARKDOWN_EXTENSIONS = ["md", "mdx"];
23194
23452
  function fileExtension(filePath) {
23195
23453
  const idx = filePath.lastIndexOf(".");
@@ -23199,8 +23457,18 @@ function isDataFile(filePath) {
23199
23457
  const ext = fileExtension(filePath);
23200
23458
  return ext === "json" || ext === "yaml" || ext === "yml";
23201
23459
  }
23460
+ async function resolveCollectionDir(deps, collection) {
23461
+ const parsed = await parseContentConfig(deps.fs, deps.parseCache);
23462
+ const loaderBase = parsed.get(collection)?.loaderBase;
23463
+ if (loaderBase) {
23464
+ const normalized = loaderBase.replace(/^\.\/+/, "").replace(/[/\\]+$/, "");
23465
+ if (normalized)
23466
+ return normalized;
23467
+ }
23468
+ return `${deps.contentDir}/${collection}`;
23469
+ }
23202
23470
  async function resolveEntryPath(deps, collection, slug) {
23203
- const base = `${deps.contentDir}/${collection}`;
23471
+ const base = await resolveCollectionDir(deps, collection);
23204
23472
  const flatExts = ["md", "mdx", "json", "yaml", "yml"];
23205
23473
  for (const ext of flatExts) {
23206
23474
  const candidate = `${base}/${slug}.${ext}`;
@@ -23214,6 +23482,36 @@ async function resolveEntryPath(deps, collection, slug) {
23214
23482
  }
23215
23483
  return null;
23216
23484
  }
23485
+ function resolveRelativePath(baseDir, rel) {
23486
+ const out = rel.startsWith("/") ? [] : baseDir.split("/").filter(Boolean);
23487
+ for (const seg of rel.replace(/^\/+/, "").split("/")) {
23488
+ if (seg === "" || seg === ".")
23489
+ continue;
23490
+ if (seg === "..") {
23491
+ if (out.length === 0)
23492
+ return null;
23493
+ out.pop();
23494
+ continue;
23495
+ }
23496
+ out.push(seg);
23497
+ }
23498
+ return out.join("/");
23499
+ }
23500
+ function extOf(filePath) {
23501
+ const idx = filePath.lastIndexOf(".");
23502
+ return idx >= 0 ? filePath.slice(idx).toLowerCase() : "";
23503
+ }
23504
+ async function getEntryAsset(deps, collection, slug, assetPath) {
23505
+ const sourcePath = await resolveEntryPath(deps, collection, slug);
23506
+ if (!sourcePath)
23507
+ return null;
23508
+ const lastSlash = sourcePath.lastIndexOf("/");
23509
+ const baseDir = lastSlash >= 0 ? sourcePath.slice(0, lastSlash) : "";
23510
+ const resolved = resolveRelativePath(baseDir, assetPath);
23511
+ if (!resolved || !await deps.fs.exists(resolved))
23512
+ return null;
23513
+ return { bytes: await deps.fs.readBytes(resolved), contentType: mimeFromExt(extOf(resolved)) };
23514
+ }
23217
23515
  function parseFrontmatter2(raw) {
23218
23516
  const trimmed = raw.trimStart();
23219
23517
  if (!trimmed.startsWith("---")) {
@@ -23331,7 +23629,7 @@ async function detectCollectionMarkdownLayout(deps, collection) {
23331
23629
  return "flat";
23332
23630
  }
23333
23631
  async function inferLayoutFromExistingEntries(deps, collection) {
23334
- const collectionPath = `${deps.contentDir}/${collection}`;
23632
+ const collectionPath = await resolveCollectionDir(deps, collection);
23335
23633
  const dirEntries = await deps.fs.list(collectionPath);
23336
23634
  if (dirEntries.length === 0)
23337
23635
  return null;
@@ -23405,7 +23703,8 @@ async function createEntry(deps, input) {
23405
23703
  }
23406
23704
  const isData = ext === "json" || ext === "yaml" || ext === "yml";
23407
23705
  const layout = isData ? "flat" : await detectCollectionMarkdownLayout(deps, collection);
23408
- const sourcePath = layout === "index" ? `${deps.contentDir}/${collection}/${normalizedSlug}/index.${ext}` : `${deps.contentDir}/${collection}/${normalizedSlug}.${ext}`;
23706
+ const collectionDir = await resolveCollectionDir(deps, collection);
23707
+ const sourcePath = layout === "index" ? `${collectionDir}/${normalizedSlug}/index.${ext}` : `${collectionDir}/${normalizedSlug}.${ext}`;
23409
23708
  let fileContent;
23410
23709
  if (isData) {
23411
23710
  fileContent = ext === "json" ? JSON.stringify({ ...frontmatter }, null, 2) + `
@@ -23816,27 +24115,30 @@ function parseRedirectLines(lines) {
23816
24115
  }
23817
24116
 
23818
24117
  // ../cms-core/src/core.ts
23819
- function createCmsCore(fs, opts = {}) {
24118
+ function createCmsCore(fs2, opts = {}) {
23820
24119
  const contentDir = opts.contentDir ?? "src/content";
23821
24120
  const componentDirs = opts.componentDirs ?? ["src/components"];
23822
24121
  const parseCache = new Map;
23823
24122
  const entryDeps = {
23824
- fs,
24123
+ fs: fs2,
23825
24124
  contentDir,
23826
24125
  parseCache,
23827
24126
  componentDirs,
23828
- resolveComponentDefinitions: () => scanComponentDefinitions(fs, componentDirs)
24127
+ resolveComponentDefinitions: () => scanComponentDefinitions(fs2, componentDirs)
23829
24128
  };
23830
24129
  return {
23831
24130
  scanCollections() {
23832
- return scanCollections(fs, contentDir, parseCache);
24131
+ return scanCollections(fs2, contentDir, parseCache);
23833
24132
  },
23834
24133
  scanComponents() {
23835
- return scanComponentDefinitions(fs, componentDirs);
24134
+ return scanComponentDefinitions(fs2, componentDirs);
23836
24135
  },
23837
24136
  getEntry(collection, slug) {
23838
24137
  return getEntry(entryDeps, collection, slug);
23839
24138
  },
24139
+ getEntryAsset(collection, slug, assetPath) {
24140
+ return getEntryAsset(entryDeps, collection, slug, assetPath);
24141
+ },
23840
24142
  createEntry(input) {
23841
24143
  return createEntry(entryDeps, input);
23842
24144
  },
@@ -23856,28 +24158,28 @@ function createCmsCore(fs, opts = {}) {
23856
24158
  return removeArrayItem(entryDeps, input);
23857
24159
  },
23858
24160
  createPage(input) {
23859
- return createPage({ fs }, input);
24161
+ return createPage({ fs: fs2 }, input);
23860
24162
  },
23861
24163
  duplicatePage(input) {
23862
- return duplicatePage({ fs }, input);
24164
+ return duplicatePage({ fs: fs2 }, input);
23863
24165
  },
23864
24166
  deletePage(input) {
23865
- return deletePage({ fs }, input);
24167
+ return deletePage({ fs: fs2 }, input);
23866
24168
  },
23867
24169
  getLayouts() {
23868
- return getLayouts({ fs });
24170
+ return getLayouts({ fs: fs2 });
23869
24171
  },
23870
24172
  listRedirects() {
23871
- return listRedirects({ fs });
24173
+ return listRedirects({ fs: fs2 });
23872
24174
  },
23873
24175
  addRedirect(input) {
23874
- return addRedirect({ fs }, input);
24176
+ return addRedirect({ fs: fs2 }, input);
23875
24177
  },
23876
24178
  updateRedirect(input) {
23877
- return updateRedirect({ fs }, input);
24179
+ return updateRedirect({ fs: fs2 }, input);
23878
24180
  },
23879
24181
  deleteRedirect(input) {
23880
- return deleteRedirect({ fs }, input);
24182
+ return deleteRedirect({ fs: fs2 }, input);
23881
24183
  },
23882
24184
  media: opts.media
23883
24185
  };
@@ -23916,14 +24218,14 @@ function globToRegExp(glob) {
23916
24218
  return new RegExp(`^${re}$`);
23917
24219
  }
23918
24220
  // ../cms-core/src/fs/node-fs.ts
23919
- import fs from "fs/promises";
23920
- import path3 from "path";
24221
+ import fs2 from "fs/promises";
24222
+ import path4 from "path";
23921
24223
  function resolveWithinRoot(root, filePath) {
23922
- const resolvedRoot = path3.resolve(root);
24224
+ const resolvedRoot = path4.resolve(root);
23923
24225
  const isAbsoluteFs = filePath.startsWith(resolvedRoot);
23924
24226
  const normalizedPath = !isAbsoluteFs && filePath.startsWith("/") ? filePath.slice(1) : filePath;
23925
- const fullPath = path3.isAbsolute(normalizedPath) ? path3.resolve(normalizedPath) : path3.resolve(resolvedRoot, normalizedPath);
23926
- if (!fullPath.startsWith(resolvedRoot + path3.sep) && fullPath !== resolvedRoot) {
24227
+ const fullPath = path4.isAbsolute(normalizedPath) ? path4.resolve(normalizedPath) : path4.resolve(resolvedRoot, normalizedPath);
24228
+ if (!fullPath.startsWith(resolvedRoot + path4.sep) && fullPath !== resolvedRoot) {
23927
24229
  throw new Error(`Path traversal detected: ${filePath}`);
23928
24230
  }
23929
24231
  return fullPath;
@@ -23934,17 +24236,17 @@ function isNodeError(error) {
23934
24236
  async function walkFiles(absRoot, absDir) {
23935
24237
  let dirEntries;
23936
24238
  try {
23937
- dirEntries = await fs.readdir(absDir, { withFileTypes: true });
24239
+ dirEntries = await fs2.readdir(absDir, { withFileTypes: true });
23938
24240
  } catch {
23939
24241
  return [];
23940
24242
  }
23941
24243
  const out = [];
23942
24244
  for (const entry of dirEntries) {
23943
- const abs = path3.join(absDir, entry.name);
24245
+ const abs = path4.join(absDir, entry.name);
23944
24246
  if (entry.isDirectory()) {
23945
24247
  out.push(...await walkFiles(absRoot, abs));
23946
24248
  } else if (entry.isFile()) {
23947
- out.push(path3.relative(absRoot, abs).split(path3.sep).join("/"));
24249
+ out.push(path4.relative(absRoot, abs).split(path4.sep).join("/"));
23948
24250
  }
23949
24251
  }
23950
24252
  return out;
@@ -23963,35 +24265,38 @@ function staticPrefixDir(pattern) {
23963
24265
  return staticSegments.join("/");
23964
24266
  }
23965
24267
  function createNodeFs(root) {
23966
- const resolvedRoot = path3.resolve(root);
24268
+ const resolvedRoot = path4.resolve(root);
23967
24269
  const resolve = (p) => resolveWithinRoot(resolvedRoot, p);
23968
24270
  return {
23969
24271
  async readFile(filePath) {
23970
- return fs.readFile(resolve(filePath), "utf-8");
24272
+ return fs2.readFile(resolve(filePath), "utf-8");
24273
+ },
24274
+ async readBytes(filePath) {
24275
+ return fs2.readFile(resolve(filePath));
23971
24276
  },
23972
24277
  async writeFile(filePath, content) {
23973
24278
  const fullPath = resolve(filePath);
23974
- await fs.mkdir(path3.dirname(fullPath), { recursive: true });
24279
+ await fs2.mkdir(path4.dirname(fullPath), { recursive: true });
23975
24280
  const tempPath = `${fullPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
23976
24281
  try {
23977
- await fs.writeFile(tempPath, content, "utf-8");
23978
- await fs.rename(tempPath, fullPath);
24282
+ await fs2.writeFile(tempPath, content, "utf-8");
24283
+ await fs2.rename(tempPath, fullPath);
23979
24284
  } catch (error) {
23980
- await fs.rm(tempPath, { force: true });
24285
+ await fs2.rm(tempPath, { force: true });
23981
24286
  throw error;
23982
24287
  }
23983
24288
  },
23984
24289
  async rename(from, to) {
23985
24290
  const fullTo = resolve(to);
23986
- await fs.mkdir(path3.dirname(fullTo), { recursive: true });
23987
- await fs.rename(resolve(from), fullTo);
24291
+ await fs2.mkdir(path4.dirname(fullTo), { recursive: true });
24292
+ await fs2.rename(resolve(from), fullTo);
23988
24293
  },
23989
24294
  async remove(filePath) {
23990
- await fs.rm(resolve(filePath), { force: true });
24295
+ await fs2.rm(resolve(filePath), { force: true });
23991
24296
  },
23992
24297
  async exists(filePath) {
23993
24298
  try {
23994
- await fs.access(resolve(filePath));
24299
+ await fs2.access(resolve(filePath));
23995
24300
  return true;
23996
24301
  } catch {
23997
24302
  return false;
@@ -23999,7 +24304,7 @@ function createNodeFs(root) {
23999
24304
  },
24000
24305
  async list(dir) {
24001
24306
  try {
24002
- const entries = await fs.readdir(resolve(dir), { withFileTypes: true });
24307
+ const entries = await fs2.readdir(resolve(dir), { withFileTypes: true });
24003
24308
  return entries.map((entry) => ({ name: entry.name, isDirectory: entry.isDirectory() }));
24004
24309
  } catch (error) {
24005
24310
  if (isNodeError(error) && error.code === "ENOENT")
@@ -24015,7 +24320,7 @@ function createNodeFs(root) {
24015
24320
  return files.filter((rel) => matcher.test(rel));
24016
24321
  },
24017
24322
  async stat(filePath) {
24018
- const s = await fs.stat(resolve(filePath));
24323
+ const s = await fs2.stat(resolve(filePath));
24019
24324
  return { mtimeMs: s.mtimeMs, size: s.size };
24020
24325
  }
24021
24326
  };
@@ -24083,120 +24388,6 @@ function createContemberStorageAdapter(options) {
24083
24388
  }
24084
24389
  };
24085
24390
  }
24086
- // ../cms-core/src/media/local.ts
24087
- import { randomUUID } from "crypto";
24088
- import fs2 from "fs/promises";
24089
- import path4 from "path";
24090
- function createLocalStorageAdapter(options = {}) {
24091
- const dir = path4.resolve(options.dir ?? "public/uploads");
24092
- const urlPrefix = (options.urlPrefix ?? "/uploads").replace(/\/$/, "");
24093
- return {
24094
- staticFiles: { urlPrefix, dir },
24095
- async list(opts) {
24096
- const limit = opts?.limit ?? 50;
24097
- const offset = opts?.cursor ? parseInt(opts.cursor, 10) : 0;
24098
- const folder = opts?.folder ?? "";
24099
- const targetDir = folder ? path4.join(dir, folder) : dir;
24100
- await fs2.mkdir(targetDir, { recursive: true });
24101
- const entries = await fs2.readdir(targetDir, { withFileTypes: true });
24102
- const folders = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({
24103
- name: e.name,
24104
- path: folder ? `${folder}/${e.name}` : e.name
24105
- })).sort((a, b) => a.name.localeCompare(b.name));
24106
- const files = entries.filter((e) => e.isFile() && !e.name.startsWith("."));
24107
- const withStats = await Promise.all(files.map(async (f) => {
24108
- const filePath = path4.join(targetDir, f.name);
24109
- const stat = await fs2.stat(filePath);
24110
- return { name: f.name, stat };
24111
- }));
24112
- withStats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
24113
- const slice = withStats.slice(offset, offset + limit);
24114
- const hasMore = offset + limit < withStats.length;
24115
- const urlFolder = folder ? `/${folder}` : "";
24116
- const items = slice.map((f) => {
24117
- const ext = path4.extname(f.name).toLowerCase();
24118
- const contentType = mimeFromExt(ext);
24119
- return {
24120
- id: folder ? `${folder}/${f.name}` : f.name,
24121
- url: `${urlPrefix}${urlFolder}/${f.name}`,
24122
- filename: f.name,
24123
- contentType,
24124
- uploadedAt: f.stat.mtime.toISOString(),
24125
- folder: folder || undefined
24126
- };
24127
- });
24128
- return {
24129
- items,
24130
- folders,
24131
- hasMore,
24132
- cursor: hasMore ? String(offset + limit) : undefined
24133
- };
24134
- },
24135
- async upload(file, filename, contentType, uploadOpts) {
24136
- const folder = uploadOpts?.folder ?? "";
24137
- const targetDir = folder ? path4.join(dir, folder) : dir;
24138
- await fs2.mkdir(targetDir, { recursive: true });
24139
- const ext = getFileExtension(filename);
24140
- const uuid = randomUUID();
24141
- const newFilename = `${uuid}${ext ? `.${ext}` : ""}`;
24142
- const filePath = path4.join(targetDir, newFilename);
24143
- await fs2.writeFile(filePath, file);
24144
- const urlFolder = folder ? `/${folder}` : "";
24145
- const id = folder ? `${folder}/${newFilename}` : newFilename;
24146
- return {
24147
- success: true,
24148
- url: `${urlPrefix}${urlFolder}/${newFilename}`,
24149
- filename: newFilename,
24150
- id
24151
- };
24152
- },
24153
- async delete(id) {
24154
- const safePath = id.split("/").map((s) => path4.basename(s)).join("/");
24155
- const filePath = path4.join(dir, safePath);
24156
- try {
24157
- await fs2.unlink(filePath);
24158
- return { success: true };
24159
- } catch (error) {
24160
- const message = error instanceof Error ? error.message : String(error);
24161
- return { success: false, error: message };
24162
- }
24163
- },
24164
- async createFolder(folder) {
24165
- const segments = folder.split("/").filter(Boolean);
24166
- if (segments.some((s) => s === ".." || s === ".")) {
24167
- return { success: false, error: "Invalid folder name" };
24168
- }
24169
- try {
24170
- await fs2.mkdir(path4.join(dir, ...segments), { recursive: true });
24171
- return { success: true };
24172
- } catch (error) {
24173
- const message = error instanceof Error ? error.message : String(error);
24174
- return { success: false, error: message };
24175
- }
24176
- }
24177
- };
24178
- }
24179
- function getFileExtension(filename) {
24180
- const parts = filename.split(".");
24181
- const ext = parts.length > 1 ? parts.pop()?.toLowerCase() ?? "" : "";
24182
- return /^[a-z0-9]+$/.test(ext) ? ext : "";
24183
- }
24184
- var MIME_BY_EXT = {
24185
- ".jpg": "image/jpeg",
24186
- ".jpeg": "image/jpeg",
24187
- ".png": "image/png",
24188
- ".gif": "image/gif",
24189
- ".webp": "image/webp",
24190
- ".avif": "image/avif",
24191
- ".svg": "image/svg+xml",
24192
- ".ico": "image/x-icon",
24193
- ".mp4": "video/mp4",
24194
- ".webm": "video/webm",
24195
- ".pdf": "application/pdf"
24196
- };
24197
- function mimeFromExt(ext) {
24198
- return MIME_BY_EXT[ext] ?? "application/octet-stream";
24199
- }
24200
24391
  // ../cms-core/src/media/project-images.ts
24201
24392
  var IMAGE_EXTENSIONS2 = new Set(Object.entries(MIME_BY_EXT).filter(([, mime]) => mime.startsWith("image/")).map(([ext]) => ext));
24202
24393
  // ../cms-core/src/media/s3.ts
@@ -24424,12 +24615,14 @@ var SIDECAR_FEATURES = [
24424
24615
  "entry.crud",
24425
24616
  "entry.rename",
24426
24617
  "entry.array",
24618
+ "entry.asset",
24427
24619
  "entry.optimistic-concurrency",
24428
24620
  "pages.crud",
24429
24621
  "pages.list",
24430
24622
  "pages.layouts",
24431
24623
  "redirects.crud",
24432
- "media"
24624
+ "media",
24625
+ "components"
24433
24626
  ];
24434
24627
  var API_PREFIX = "/cms/v1";
24435
24628
  var DEFAULT_LIMIT = 50;
@@ -24591,6 +24784,10 @@ function createServer(opts) {
24591
24784
  const map = await core.scanCollections();
24592
24785
  return map[name] ?? null;
24593
24786
  }
24787
+ async function scanComponentsList() {
24788
+ const map = await core.scanComponents();
24789
+ return Object.values(map).sort((a, b) => a.name.localeCompare(b.name));
24790
+ }
24594
24791
  async function entryDetail(collection, slug) {
24595
24792
  const result = await core.getEntry(collection, slug);
24596
24793
  if (!result)
@@ -24610,6 +24807,15 @@ function createServer(opts) {
24610
24807
  };
24611
24808
  return json(entry);
24612
24809
  }
24810
+ async function assetResponse(collection, slug, url) {
24811
+ const assetPath = url.searchParams.get("path");
24812
+ if (assetPath === null || assetPath === "")
24813
+ return error("validation", 'A "path" query parameter is required');
24814
+ const asset = await core.getEntryAsset(collection, slug, assetPath);
24815
+ if (!asset)
24816
+ return error("not_found", `Asset not found for ${collection}/${slug}: ${assetPath}`);
24817
+ return new Response(asset.bytes, { headers: { "content-type": asset.contentType, "cache-control": "no-cache" } });
24818
+ }
24613
24819
  async function patchEntry(collection, slug, body) {
24614
24820
  const existing = await core.getEntry(collection, slug);
24615
24821
  if (!existing)
@@ -24674,6 +24880,10 @@ function createServer(opts) {
24674
24880
  return json(model);
24675
24881
  }
24676
24882
  break;
24883
+ case "components":
24884
+ if (method === "GET")
24885
+ return json(await scanComponentsList());
24886
+ break;
24677
24887
  case "collections":
24678
24888
  return routeCollections(method, tail, req, url);
24679
24889
  case "pages":
@@ -24715,6 +24925,8 @@ function createServer(opts) {
24715
24925
  return addArrayRoute(collection, slug, req);
24716
24926
  if (action === "array" && method === "DELETE")
24717
24927
  return removeArrayRoute(collection, slug, req);
24928
+ if (action === "asset" && method === "GET")
24929
+ return assetResponse(collection, slug, url);
24718
24930
  }
24719
24931
  return error("not_found", `No route: ${method} /cms/v1/collections/${tail.join("/")}`);
24720
24932
  }