canopycms 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +79 -47
  2. package/dist/ai/json-to-markdown.js +2 -0
  3. package/dist/ai/resolve-branch.js +2 -1
  4. package/dist/api/branch-entry-changes.js +2 -4
  5. package/dist/authorization/branch.d.ts +10 -4
  6. package/dist/authorization/branch.js +24 -4
  7. package/dist/authorization/types.d.ts +1 -1
  8. package/dist/cli/cli.js +1984 -1630
  9. package/dist/cli/configured-asset-store.js +2 -8
  10. package/dist/cli/generate-ai-content.js +86 -16
  11. package/dist/cli/init.js +1 -1
  12. package/dist/cli/site-config.d.ts +9 -0
  13. package/dist/cli/site-config.js +17 -0
  14. package/dist/cli/sync.js +226 -74
  15. package/dist/config/types.d.ts +4 -4
  16. package/dist/config/types.js +1 -1
  17. package/dist/config/validation.d.ts +9 -3
  18. package/dist/config/validation.js +30 -5
  19. package/dist/content-listing.js +2 -3
  20. package/dist/content-store.js +5 -5
  21. package/dist/editor/FormRenderer.js +5 -0
  22. package/dist/editor/components/EntryChangesDrawer.js +3 -2
  23. package/dist/editor/entry-diff.js +7 -1
  24. package/dist/editor/fields/DateField.d.ts +19 -0
  25. package/dist/editor/fields/DateField.js +53 -0
  26. package/dist/editor/fields/DateTimeField.d.ts +10 -0
  27. package/dist/editor/fields/DateTimeField.js +5 -15
  28. package/dist/editor/fields/list-item-title.js +2 -1
  29. package/dist/editor/theme.js +12 -4
  30. package/dist/entry-schema-registry.js +2 -1
  31. package/dist/services.d.ts +3 -2
  32. package/dist/static/index.js +5 -2
  33. package/dist/system-users.d.ts +2 -0
  34. package/dist/system-users.js +6 -1
  35. package/dist/utils/calendar-day.d.ts +23 -0
  36. package/dist/utils/calendar-day.js +102 -0
  37. package/dist/utils/content-serialize.d.ts +4 -2
  38. package/dist/utils/content-serialize.js +8 -6
  39. package/dist/validation/entry-validator.js +7 -1
  40. package/dist/validation/field-traversal.js +1 -1
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +1 -1
@@ -4,11 +4,11 @@
4
4
  * site's own dependencies, which a release job given `--bucket` does not have.
5
5
  */
6
6
  import path from 'node:path';
7
- import { createJiti } from 'jiti';
8
7
  import { z } from 'zod';
9
8
  import { createAssetStore } from '../assets/factory.js';
10
9
  import { mediaSchema } from '../config/schemas/media.js';
11
10
  import { operatingStrategy } from '../operating-mode/index.js';
11
+ import { importSiteServerConfig } from './site-config.js';
12
12
  const serverConfigSchema = z.object({
13
13
  mode: z.enum(['prod', 'dev']),
14
14
  media: mediaSchema.optional(),
@@ -19,13 +19,7 @@ const serverConfigSchema = z.object({
19
19
  * local store, prod never does.
20
20
  */
21
21
  export async function loadConfiguredAssetStore(projectDir) {
22
- const configPath = path.join(projectDir, 'canopycms.config.ts');
23
- const jiti = createJiti(import.meta.url);
24
- const mod = (await jiti.import(configPath));
25
- const configExport = mod.default ?? mod.config ?? mod;
26
- const server = typeof configExport === 'object' && configExport !== null && 'server' in configExport
27
- ? configExport.server
28
- : configExport;
22
+ const { configPath, server } = await importSiteServerConfig(projectDir);
29
23
  const parsed = serverConfigSchema.safeParse(server);
30
24
  if (!parsed.success) {
31
25
  throw new Error(`Invalid CanopyCMS config at ${configPath}: ${parsed.error.message}`);
@@ -68,7 +68,7 @@ var init_atomic_write = __esm({
68
68
 
69
69
  // dist/validation/field-traversal.js
70
70
  function isPlainRecord(value) {
71
- return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
72
72
  }
73
73
  function resolveBlockItem(blockField, item) {
74
74
  const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
@@ -573,7 +573,7 @@ var scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes;
573
573
  var init_types2 = __esm({
574
574
  "dist/config/types.js"() {
575
575
  "use strict";
576
- scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
576
+ scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
577
577
  markdownFieldTypes = ["markdown", "mdx"];
578
578
  primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
579
579
  fieldTypes = [
@@ -1640,6 +1640,70 @@ function isBlockStructuralKey(key) {
1640
1640
  // dist/utils/content-serialize.js
1641
1641
  init_item_keys();
1642
1642
 
1643
+ // dist/utils/calendar-day.js
1644
+ init_item_keys();
1645
+ init_field_traversal();
1646
+ var DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
1647
+ var MS_PER_DAY = 864e5;
1648
+ var isRealDay = (year, month, day) => {
1649
+ const lastOfMonth = /* @__PURE__ */ new Date(0);
1650
+ lastOfMonth.setUTCFullYear(year, month, 0);
1651
+ return month >= 1 && month <= 12 && day >= 1 && day <= lastOfMonth.getUTCDate();
1652
+ };
1653
+ var dayParts = (value) => {
1654
+ const match = typeof value === "string" ? DAY.exec(value) : null;
1655
+ if (!match)
1656
+ return void 0;
1657
+ const [year, month, day] = match.slice(1).map(Number);
1658
+ return isRealDay(year, month, day) ? [year, month, day] : void 0;
1659
+ };
1660
+ var isCalendarDay = (value) => dayParts(value) !== void 0;
1661
+ var dayOfBareDate = (value) => value.getTime() % MS_PER_DAY === 0 ? value.toISOString().slice(0, 10) : void 0;
1662
+ var mapItems = (items, mapItem) => {
1663
+ const next = items.map((item) => isPlainRecord(item) ? mapItem(item) : item);
1664
+ return next.every((item, i) => item === items[i]) ? items : next;
1665
+ };
1666
+ var blockItemWithDays = (field, item) => {
1667
+ const resolved = resolveBlockItem(field, item);
1668
+ if (!resolved)
1669
+ return item;
1670
+ const data = withCalendarDays(resolved.fields, resolved.data);
1671
+ if (data === resolved.data)
1672
+ return item;
1673
+ return resolved.data === item ? data : { ...item, value: data };
1674
+ };
1675
+ var valueWithDays = (field, value) => {
1676
+ if (field.type === "date") {
1677
+ return value instanceof Date ? dayOfBareDate(value) ?? value : value;
1678
+ }
1679
+ if (field.type === "object") {
1680
+ const { fields, list } = field;
1681
+ if (list) {
1682
+ return Array.isArray(value) ? mapItems(value, (item) => withCalendarDays(fields ?? [], item)) : value;
1683
+ }
1684
+ return isPlainRecord(value) ? withCalendarDays(fields ?? [], value) : value;
1685
+ }
1686
+ if (field.type === "block" && Array.isArray(value)) {
1687
+ return mapItems(value, (item) => blockItemWithDays(field, item));
1688
+ }
1689
+ return value;
1690
+ };
1691
+ function withCalendarDays(fields, data) {
1692
+ let out = data;
1693
+ for (const field of fields) {
1694
+ if (field.type === "group") {
1695
+ out = withCalendarDays(field.fields, out);
1696
+ continue;
1697
+ }
1698
+ const value = out[field.name];
1699
+ const next = valueWithDays(field, value);
1700
+ if (next !== value)
1701
+ out = { ...out, [field.name]: next };
1702
+ }
1703
+ return out;
1704
+ }
1705
+ var entryAsRead = (fields, data) => withItemKeys(fields, withCalendarDays(fields, data), "content");
1706
+
1643
1707
  // dist/utils/debug.js
1644
1708
  var envFlag = (name) => typeof process !== "undefined" && process.env[name] === "true";
1645
1709
  var LOG_LEVELS = {
@@ -3235,9 +3299,9 @@ function asJson(value) {
3235
3299
  const key = identityKey(value);
3236
3300
  return key === void 0 || key === "undefined" ? void 0 : JSON.parse(key);
3237
3301
  }
3238
- function frontmatterAsRead(raw) {
3302
+ function frontmatterAsRead(raw, fields) {
3239
3303
  try {
3240
- return asJson(matter(raw, {}).data);
3304
+ return asJson(withCalendarDays(fields, matter(raw, {}).data));
3241
3305
  } catch {
3242
3306
  return void 0;
3243
3307
  }
@@ -3305,7 +3369,7 @@ ${asKey ? `${text}: v` : `v: ${text}`}
3305
3369
  return true;
3306
3370
  }
3307
3371
  }
3308
- function serializeFrontmatter(editorBody, data, existingRaw, format) {
3372
+ function serializeFrontmatter(editorBody, data, existingRaw, format, fields = []) {
3309
3373
  const file = { content: bodyToWrite(editorBody, data, format, existingRaw) };
3310
3374
  if (existingRaw === void 0)
3311
3375
  return matter.stringify(file, data);
@@ -3313,7 +3377,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
3313
3377
  if (existingFrontmatter === void 0)
3314
3378
  return matter.stringify(file, data);
3315
3379
  const onDisk = parseDocument2(existingFrontmatter);
3316
- const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw), onDisk.toJS()) : data;
3380
+ const toReconcile = onDisk.errors.length === 0 ? withUnchangedAsOnDisk(data, frontmatterAsRead(existingRaw, fields), onDisk.toJS()) : data;
3317
3381
  const reconciled = reconcileYamlSource(existingFrontmatter, isPlainRecord2(toReconcile) ? toReconcile : data, { rootAtColumnZero: true, quote: readsAsAnotherValue });
3318
3382
  if (reconciled === void 0)
3319
3383
  return matter.stringify(file, data);
@@ -3325,7 +3389,7 @@ function serializeFrontmatter(editorBody, data, existingRaw, format) {
3325
3389
  }
3326
3390
  }
3327
3391
  });
3328
- if (isDeepStrictEqual2(frontmatterAsRead(written), asJson(data)))
3392
+ if (isDeepStrictEqual2(frontmatterAsRead(written, fields), asJson(data)))
3329
3393
  return written;
3330
3394
  log2.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
3331
3395
  return matter.stringify(file, data);
@@ -3639,9 +3703,6 @@ function linkOnlyReferenceData(data, format, fields) {
3639
3703
  return rest;
3640
3704
  }
3641
3705
 
3642
- // dist/content-store.js
3643
- init_item_keys();
3644
-
3645
3706
  // dist/static/seo.js
3646
3707
  var DEFAULT_SEO_FIELD_NAMES = {
3647
3708
  title: "metaTitle",
@@ -5834,7 +5895,7 @@ var ContentStore = class {
5834
5895
  absolutePath
5835
5896
  };
5836
5897
  }
5837
- doc.data = withItemKeys(fields, doc.data, "content");
5898
+ doc.data = entryAsRead(fields, doc.data);
5838
5899
  if (options.resolveReferences !== false) {
5839
5900
  doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess, {
5840
5901
  entry: slug ? entryLogicalPath(schemaItem.logicalPath, slug) : schemaItem.logicalPath,
@@ -5938,7 +5999,7 @@ var ContentStore = class {
5938
5999
  } else if (input.format === "yaml") {
5939
6000
  content = serializeYaml(input.data ?? {}, existingRaw);
5940
6001
  } else {
5941
- content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format);
6002
+ content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
5942
6003
  }
5943
6004
  await atomicWriteFile(absolutePath, content);
5944
6005
  const liveIndex = this._idIndex;
@@ -6830,6 +6891,7 @@ _${field.description}_` : "";
6830
6891
  switch (field.type) {
6831
6892
  case "string":
6832
6893
  case "number":
6894
+ case "date":
6833
6895
  case "datetime":
6834
6896
  return `${heading} ${label}${descriptionLine}
6835
6897
 
@@ -6908,6 +6970,7 @@ ${items.map((item) => `- ${item}`).join("\n")}`;
6908
6970
  var FLAT_CELL_TYPES = /* @__PURE__ */ new Set([
6909
6971
  "string",
6910
6972
  "number",
6973
+ "date",
6911
6974
  "datetime",
6912
6975
  "boolean",
6913
6976
  "select",
@@ -10937,6 +11000,9 @@ async function loadOrCreateBranchContext(options) {
10937
11000
  });
10938
11001
  }
10939
11002
 
11003
+ // dist/system-users.js
11004
+ var AI_USER_ID = "canopycms-ai";
11005
+
10940
11006
  // dist/ai/resolve-branch.js
10941
11007
  async function resolveBranchRoot(config) {
10942
11008
  if (readsFromCheckout(config)) {
@@ -10955,7 +11021,7 @@ async function resolveBranchRoot(config) {
10955
11021
  config: resolved,
10956
11022
  branchName: activeBranch,
10957
11023
  mode: config.mode,
10958
- createdBy: "canopycms-ai",
11024
+ createdBy: AI_USER_ID,
10959
11025
  remoteUrl: config.defaultRemoteUrl
10960
11026
  });
10961
11027
  return context.branchRoot;
@@ -10967,7 +11033,6 @@ import path24 from "node:path";
10967
11033
  import matter3 from "gray-matter";
10968
11034
  import { parse as yamlParse2 } from "yaml";
10969
11035
  init_error();
10970
- init_item_keys();
10971
11036
  var log10 = createDebugLogger({ prefix: "ContentListing" });
10972
11037
  var normalizePath = (root, target) => {
10973
11038
  const result = validateAndNormalizePath(root, target);
@@ -11138,7 +11203,7 @@ var listCollectionEntries = async (root, collection, onSkip, readsId) => {
11138
11203
  fs20.stat(absolutePath),
11139
11204
  readEntryData(absolutePath, format, bodyField)
11140
11205
  ]);
11141
- const data = withItemKeys(entryType?.schema ?? [], rawData, "content");
11206
+ const data = entryAsRead(entryType?.schema ?? [], rawData);
11142
11207
  const item = {
11143
11208
  logicalPath: entryLogicalPath(collection.logicalPath, slug),
11144
11209
  contentId,
@@ -11251,6 +11316,11 @@ function validateScalar(field, value, path27) {
11251
11316
  return [{ fieldPath: path27, message: "Expected true or false" }];
11252
11317
  }
11253
11318
  return [];
11319
+ case "date":
11320
+ if (value !== "" && !isCalendarDay(value)) {
11321
+ return [{ fieldPath: path27, message: "Expected a real date, written YYYY-MM-DD" }];
11322
+ }
11323
+ return [];
11254
11324
  case "datetime":
11255
11325
  if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
11256
11326
  return [{ fieldPath: path27, message: "Expected a valid date/time string" }];
@@ -11405,7 +11475,7 @@ function findInvalidEntries(items) {
11405
11475
  for (const item of items) {
11406
11476
  if (!item.schema)
11407
11477
  continue;
11408
- const data = normalizeDatesDeep(item.data);
11478
+ const data = normalizeDatesDeep(withCalendarDays(item.schema, item.data));
11409
11479
  const errors = validateEntryData(item.schema, data);
11410
11480
  if (errors.length > 0) {
11411
11481
  invalid.push({ entryPath: item.entryPath, errors });
package/dist/cli/init.js CHANGED
@@ -26,7 +26,7 @@ var scalarFieldTypes, markdownFieldTypes, primitiveFieldTypes, fieldTypes;
26
26
  var init_types = __esm({
27
27
  "dist/config/types.js"() {
28
28
  "use strict";
29
- scalarFieldTypes = ["string", "number", "boolean", "datetime", "code"];
29
+ scalarFieldTypes = ["string", "number", "boolean", "date", "datetime", "code"];
30
30
  markdownFieldTypes = ["markdown", "mdx"];
31
31
  primitiveFieldTypes = [...scalarFieldTypes, ...markdownFieldTypes];
32
32
  fieldTypes = [
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Imports a site's `canopycms.config.ts` for CLI commands that need it (`sync`, and
3
+ * `configured-asset-store.ts`). Importing runs the site's config and loads its own dependencies.
4
+ */
5
+ /** The site's server config export (`server` of a split config, else the export itself). */
6
+ export declare function importSiteServerConfig(projectDir: string): Promise<{
7
+ configPath: string;
8
+ server: unknown;
9
+ }>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Imports a site's `canopycms.config.ts` for CLI commands that need it (`sync`, and
3
+ * `configured-asset-store.ts`). Importing runs the site's config and loads its own dependencies.
4
+ */
5
+ import path from 'node:path';
6
+ import { createJiti } from 'jiti';
7
+ /** The site's server config export (`server` of a split config, else the export itself). */
8
+ export async function importSiteServerConfig(projectDir) {
9
+ const configPath = path.join(projectDir, 'canopycms.config.ts');
10
+ const jiti = createJiti(import.meta.url);
11
+ const mod = (await jiti.import(configPath));
12
+ const configExport = mod.default ?? mod.config ?? mod;
13
+ const server = typeof configExport === 'object' && configExport !== null && 'server' in configExport
14
+ ? configExport.server
15
+ : configExport;
16
+ return { configPath, server };
17
+ }