canopycms 0.0.55 → 0.0.56
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/api/github-sync.d.ts.map +1 -1
- package/dist/api/github-sync.js +28 -7
- package/dist/api/github-sync.js.map +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +8 -1
- package/dist/auth/caching-auth-plugin.d.ts.map +1 -1
- package/dist/auth/caching-auth-plugin.js +6 -1
- package/dist/auth/caching-auth-plugin.js.map +1 -1
- package/dist/auth/plugin.d.ts +19 -10
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/auth/plugin.js +12 -10
- package/dist/auth/plugin.js.map +1 -1
- package/dist/build/generate-ai-content.d.ts.map +1 -1
- package/dist/build/generate-ai-content.js +7 -0
- package/dist/build/generate-ai-content.js.map +1 -1
- package/dist/cli/generate-ai-content.js +438 -38
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +16 -8
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +6 -3
- package/dist/cli/templates.js.map +1 -1
- package/dist/config/helpers.d.ts.map +1 -1
- package/dist/config/helpers.js +3 -0
- package/dist/config/helpers.js.map +1 -1
- package/dist/config/schemas/config.d.ts +3 -4
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +6 -3
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +6 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config-test.d.ts +5 -3
- package/dist/config-test.d.ts.map +1 -1
- package/dist/config-test.js +9 -1
- package/dist/config-test.js.map +1 -1
- package/dist/content-listing.d.ts +3 -1
- package/dist/content-listing.d.ts.map +1 -1
- package/dist/content-listing.js +1 -0
- package/dist/content-listing.js.map +1 -1
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +11 -1
- package/dist/content-store.js.map +1 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +19 -15
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/hooks/useDraftManager.d.ts +7 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +69 -31
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useSchemaManager.d.ts +34 -10
- package/dist/editor/hooks/useSchemaManager.d.ts.map +1 -1
- package/dist/editor/hooks/useSchemaManager.js +20 -24
- package/dist/editor/hooks/useSchemaManager.js.map +1 -1
- package/dist/editor/schema-editor/CollectionEditor.d.ts +6 -5
- package/dist/editor/schema-editor/CollectionEditor.d.ts.map +1 -1
- package/dist/editor/schema-editor/CollectionEditor.js +9 -4
- package/dist/editor/schema-editor/CollectionEditor.js.map +1 -1
- package/dist/github-service.d.ts +63 -7
- package/dist/github-service.d.ts.map +1 -1
- package/dist/github-service.js +147 -39
- package/dist/github-service.js.map +1 -1
- package/dist/services.js +4 -0
- package/dist/services.js.map +1 -1
- package/dist/static/index.d.ts +32 -0
- package/dist/static/index.d.ts.map +1 -1
- package/dist/static/index.js +90 -0
- package/dist/static/index.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +6 -3
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +21 -38
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +2 -1
|
@@ -9,11 +9,11 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// dist/paths/normalize.js
|
|
12
|
-
function normalizeFilesystemPath(
|
|
13
|
-
return
|
|
12
|
+
function normalizeFilesystemPath(path18) {
|
|
13
|
+
return path18.split(/[\\/]+/).filter(Boolean).join("/");
|
|
14
14
|
}
|
|
15
|
-
function hasTraversalSequence(
|
|
16
|
-
const normalized = normalizeFilesystemPath(
|
|
15
|
+
function hasTraversalSequence(path18) {
|
|
16
|
+
const normalized = normalizeFilesystemPath(path18);
|
|
17
17
|
return normalized.includes("..");
|
|
18
18
|
}
|
|
19
19
|
function createLogicalPath(...segments) {
|
|
@@ -23,14 +23,14 @@ function createLogicalPath(...segments) {
|
|
|
23
23
|
}
|
|
24
24
|
return normalized;
|
|
25
25
|
}
|
|
26
|
-
function trimSlashes(
|
|
26
|
+
function trimSlashes(path18) {
|
|
27
27
|
let start = 0;
|
|
28
|
-
let end =
|
|
29
|
-
while (start < end &&
|
|
28
|
+
let end = path18.length;
|
|
29
|
+
while (start < end && path18[start] === "/")
|
|
30
30
|
start++;
|
|
31
|
-
while (end > start &&
|
|
31
|
+
while (end > start && path18[end - 1] === "/")
|
|
32
32
|
end--;
|
|
33
|
-
return
|
|
33
|
+
return path18.slice(start, end);
|
|
34
34
|
}
|
|
35
35
|
var init_normalize = __esm({
|
|
36
36
|
"dist/paths/normalize.js"() {
|
|
@@ -319,7 +319,7 @@ var init_config = __esm({
|
|
|
319
319
|
gitBotAuthorNameSchema = z4.string().min(1);
|
|
320
320
|
gitBotAuthorEmailSchema = z4.string().email();
|
|
321
321
|
githubTokenEnvVarSchema = z4.string().default("GITHUB_BOT_TOKEN");
|
|
322
|
-
operatingModeSchema = z4.enum(["prod", "dev"])
|
|
322
|
+
operatingModeSchema = z4.enum(["prod", "dev"]);
|
|
323
323
|
deployedAsSchema = z4.enum(["static", "server"]).default("server");
|
|
324
324
|
contentRootSchema = relativePathSchema.default("content");
|
|
325
325
|
sourceRootSchema = z4.string().min(1).optional();
|
|
@@ -357,8 +357,9 @@ var init_config = __esm({
|
|
|
357
357
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
358
358
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
359
359
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
360
|
+
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
361
|
+
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
360
362
|
mode: operatingModeSchema,
|
|
361
|
-
// Has .default(), so not optional in output type
|
|
362
363
|
deployedAs: deployedAsSchema,
|
|
363
364
|
// Has .default('server'), so always present after validation
|
|
364
365
|
settingsBranch: z4.string().optional(),
|
|
@@ -702,12 +703,12 @@ var init_operating_mode = __esm({
|
|
|
702
703
|
});
|
|
703
704
|
|
|
704
705
|
// dist/cli/generate-ai-content.js
|
|
705
|
-
import
|
|
706
|
+
import path17 from "node:path";
|
|
706
707
|
import { createJiti } from "jiti";
|
|
707
708
|
|
|
708
709
|
// dist/build/generate-ai-content.js
|
|
709
|
-
import
|
|
710
|
-
import
|
|
710
|
+
import fs15 from "node:fs/promises";
|
|
711
|
+
import path16 from "node:path";
|
|
711
712
|
|
|
712
713
|
// dist/content-store.js
|
|
713
714
|
import fs6 from "node:fs/promises";
|
|
@@ -736,16 +737,16 @@ async function atomicWriteFile(filePath, content) {
|
|
|
736
737
|
var locks = /* @__PURE__ */ new Map();
|
|
737
738
|
async function withLock(key, fn) {
|
|
738
739
|
const prev = locks.get(key) ?? Promise.resolve();
|
|
739
|
-
let
|
|
740
|
+
let resolve2;
|
|
740
741
|
const next = new Promise((r) => {
|
|
741
|
-
|
|
742
|
+
resolve2 = r;
|
|
742
743
|
});
|
|
743
744
|
locks.set(key, next);
|
|
744
745
|
await prev;
|
|
745
746
|
try {
|
|
746
747
|
return await fn();
|
|
747
748
|
} finally {
|
|
748
|
-
|
|
749
|
+
resolve2();
|
|
749
750
|
if (locks.get(key) === next)
|
|
750
751
|
locks.delete(key);
|
|
751
752
|
}
|
|
@@ -1076,13 +1077,13 @@ function extractIdFromFilename(filename) {
|
|
|
1076
1077
|
return null;
|
|
1077
1078
|
}
|
|
1078
1079
|
async function resolveCollectionPath(root, logicalPath) {
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
1080
|
+
const fs16 = await import("node:fs/promises");
|
|
1081
|
+
const path18 = await import("node:path");
|
|
1081
1082
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
1082
1083
|
let currentPath = root;
|
|
1083
1084
|
for (const segment of segments) {
|
|
1084
1085
|
try {
|
|
1085
|
-
const entries = await
|
|
1086
|
+
const entries = await fs16.readdir(currentPath, { withFileTypes: true });
|
|
1086
1087
|
const matchingDir = entries.find((entry) => {
|
|
1087
1088
|
if (!entry.isDirectory())
|
|
1088
1089
|
return false;
|
|
@@ -1090,7 +1091,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
1090
1091
|
return logicalName === segment.toLowerCase();
|
|
1091
1092
|
});
|
|
1092
1093
|
if (matchingDir) {
|
|
1093
|
-
currentPath =
|
|
1094
|
+
currentPath = path18.join(currentPath, matchingDir.name);
|
|
1094
1095
|
} else {
|
|
1095
1096
|
return null;
|
|
1096
1097
|
}
|
|
@@ -1342,6 +1343,25 @@ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(v
|
|
|
1342
1343
|
// dist/paths/index.js
|
|
1343
1344
|
init_normalize();
|
|
1344
1345
|
|
|
1346
|
+
// dist/paths/normalize-server.js
|
|
1347
|
+
import { sep, resolve, relative } from "node:path";
|
|
1348
|
+
function validateAndNormalizePath(root, target) {
|
|
1349
|
+
const resolvedRoot = resolve(root);
|
|
1350
|
+
const withSep = resolvedRoot.endsWith(sep) ? resolvedRoot : `${resolvedRoot}${sep}`;
|
|
1351
|
+
const resolvedTarget = resolve(target);
|
|
1352
|
+
if (!resolvedTarget.startsWith(withSep) && resolvedTarget !== resolvedRoot) {
|
|
1353
|
+
return {
|
|
1354
|
+
valid: false,
|
|
1355
|
+
error: "Path traversal detected"
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
const relativePath = relative(resolvedRoot, resolvedTarget).split(sep).join("/");
|
|
1359
|
+
return {
|
|
1360
|
+
valid: true,
|
|
1361
|
+
normalizedPath: relativePath
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1345
1365
|
// dist/paths/branch.js
|
|
1346
1366
|
init_operating_mode();
|
|
1347
1367
|
import fs5 from "node:fs/promises";
|
|
@@ -1556,11 +1576,11 @@ var ContentStore = class {
|
|
|
1556
1576
|
getSchemaItems() {
|
|
1557
1577
|
return this.schemaIndex.values();
|
|
1558
1578
|
}
|
|
1559
|
-
assertSchemaItem(
|
|
1560
|
-
const normalized = normalizeFilesystemPath(
|
|
1579
|
+
assertSchemaItem(path18) {
|
|
1580
|
+
const normalized = normalizeFilesystemPath(path18);
|
|
1561
1581
|
const item = this.schemaIndex.get(normalized);
|
|
1562
1582
|
if (!item) {
|
|
1563
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
1583
|
+
throw new ContentStoreError(`Unknown schema item: ${path18}`, "NO_SCHEMA_ITEM");
|
|
1564
1584
|
}
|
|
1565
1585
|
return item;
|
|
1566
1586
|
}
|
|
@@ -1800,7 +1820,7 @@ var ContentStore = class {
|
|
|
1800
1820
|
}
|
|
1801
1821
|
if (existingId && !await filePathExists(absolutePath)) {
|
|
1802
1822
|
const actualRelPath = await this.findEntryPathById(path7.dirname(absolutePath), existingId);
|
|
1803
|
-
const indexedRelPath =
|
|
1823
|
+
const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
|
|
1804
1824
|
if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
|
|
1805
1825
|
throw new ContentConflictError();
|
|
1806
1826
|
}
|
|
@@ -3033,8 +3053,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
3033
3053
|
if (slug && slug !== "index") {
|
|
3034
3054
|
segments.push(slug);
|
|
3035
3055
|
}
|
|
3036
|
-
const
|
|
3037
|
-
return
|
|
3056
|
+
const path18 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
3057
|
+
return path18.toLowerCase();
|
|
3038
3058
|
}
|
|
3039
3059
|
|
|
3040
3060
|
// dist/entry-link-resolver.js
|
|
@@ -3638,7 +3658,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
3638
3658
|
if (err instanceof BranchMetadataConflictError && attempt < maxAttempts) {
|
|
3639
3659
|
const baseDelay = Math.min(10 * Math.pow(2, attempt - 1), 100);
|
|
3640
3660
|
const jitter = Math.random() * baseDelay;
|
|
3641
|
-
await new Promise((
|
|
3661
|
+
await new Promise((resolve2) => setTimeout(resolve2, baseDelay + jitter));
|
|
3642
3662
|
continue;
|
|
3643
3663
|
}
|
|
3644
3664
|
throw err;
|
|
@@ -3861,8 +3881,8 @@ var GitManager = class _GitManager {
|
|
|
3861
3881
|
}
|
|
3862
3882
|
let hasCommits = false;
|
|
3863
3883
|
try {
|
|
3864
|
-
const
|
|
3865
|
-
hasCommits =
|
|
3884
|
+
const log6 = await sourceGit.log(["-1"]);
|
|
3885
|
+
hasCommits = log6.total > 0;
|
|
3866
3886
|
} catch {
|
|
3867
3887
|
hasCommits = false;
|
|
3868
3888
|
}
|
|
@@ -4578,6 +4598,384 @@ async function resolveBranchRoot(config) {
|
|
|
4578
4598
|
return context.branchRoot;
|
|
4579
4599
|
}
|
|
4580
4600
|
|
|
4601
|
+
// dist/content-listing.js
|
|
4602
|
+
import fs14 from "node:fs/promises";
|
|
4603
|
+
import path15 from "node:path";
|
|
4604
|
+
import matter2 from "gray-matter";
|
|
4605
|
+
import { parse as yamlParse2 } from "yaml";
|
|
4606
|
+
var log5 = createDebugLogger({ prefix: "ContentListing" });
|
|
4607
|
+
var normalizePath = (root, target) => {
|
|
4608
|
+
const result = validateAndNormalizePath(root, target);
|
|
4609
|
+
if (!result.valid) {
|
|
4610
|
+
throw new ContentStoreError(result.error || "Path traversal detected", "VALIDATION");
|
|
4611
|
+
}
|
|
4612
|
+
return result.normalizedPath;
|
|
4613
|
+
};
|
|
4614
|
+
var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
4615
|
+
try {
|
|
4616
|
+
const raw = await fs14.readFile(filePath, "utf8");
|
|
4617
|
+
if (format === "json") {
|
|
4618
|
+
return asRecord(JSON.parse(raw));
|
|
4619
|
+
}
|
|
4620
|
+
if (format === "yaml") {
|
|
4621
|
+
return asRecord(yamlParse2(raw));
|
|
4622
|
+
}
|
|
4623
|
+
const parsed = matter2(raw);
|
|
4624
|
+
const data = parsed.data ?? {};
|
|
4625
|
+
if (parsed.content) {
|
|
4626
|
+
data[bodyFieldName] = parsed.content;
|
|
4627
|
+
}
|
|
4628
|
+
return data;
|
|
4629
|
+
} catch (err) {
|
|
4630
|
+
if (isNotFoundError(err))
|
|
4631
|
+
return {};
|
|
4632
|
+
log5.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
|
|
4633
|
+
return {};
|
|
4634
|
+
}
|
|
4635
|
+
};
|
|
4636
|
+
var parseTypedFilename = (filename, entryTypes) => {
|
|
4637
|
+
const lastDot = filename.lastIndexOf(".");
|
|
4638
|
+
if (lastDot === -1)
|
|
4639
|
+
return null;
|
|
4640
|
+
const nameWithoutExt = filename.slice(0, lastDot);
|
|
4641
|
+
const parts = nameWithoutExt.split(".");
|
|
4642
|
+
if (parts.length >= 3) {
|
|
4643
|
+
const potentialType = parts[0];
|
|
4644
|
+
const matchingType = entryTypes.find((e) => e.name === potentialType);
|
|
4645
|
+
if (matchingType) {
|
|
4646
|
+
const id = parts[parts.length - 1];
|
|
4647
|
+
if (!isValidId(id))
|
|
4648
|
+
return null;
|
|
4649
|
+
const slug = parts.slice(1, -1).join(".").toLowerCase();
|
|
4650
|
+
return {
|
|
4651
|
+
type: potentialType,
|
|
4652
|
+
slug,
|
|
4653
|
+
id
|
|
4654
|
+
};
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
return null;
|
|
4658
|
+
};
|
|
4659
|
+
async function listEntries(branchRoot, flatSchema, contentRootName, options) {
|
|
4660
|
+
const rootPath = options?.rootPath ?? contentRootName;
|
|
4661
|
+
const extract = options?.extract;
|
|
4662
|
+
const filter = options?.filter;
|
|
4663
|
+
const customSort = options?.sort;
|
|
4664
|
+
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
4665
|
+
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
4666
|
+
const entries = await listCollectionEntries(branchRoot, collection);
|
|
4667
|
+
return entries.map((entry) => ({ entry, collection }));
|
|
4668
|
+
}));
|
|
4669
|
+
const contentPrefix = contentRootName ? `${contentRootName}/` : "";
|
|
4670
|
+
const items = [];
|
|
4671
|
+
for (const results of collectionResults) {
|
|
4672
|
+
for (const { entry, collection } of results) {
|
|
4673
|
+
const pathWithoutRoot = entry.logicalPath.startsWith(contentPrefix) ? entry.logicalPath.slice(contentPrefix.length) : entry.logicalPath;
|
|
4674
|
+
const pathSegments = pathWithoutRoot.split("/").filter(Boolean);
|
|
4675
|
+
const urlSegments = entry.slug === "index" ? pathSegments.slice(0, -1) : pathSegments;
|
|
4676
|
+
const urlPath = urlSegments.length > 0 ? `/${urlSegments.join("/")}`.toLowerCase() : "/";
|
|
4677
|
+
const raw = entry.data;
|
|
4678
|
+
const meta = {
|
|
4679
|
+
entryPath: entry.logicalPath,
|
|
4680
|
+
entryType: entry.entryType,
|
|
4681
|
+
format: entry.format
|
|
4682
|
+
};
|
|
4683
|
+
const data = extract ? extract(raw, meta) : raw;
|
|
4684
|
+
const item = {
|
|
4685
|
+
pathSegments,
|
|
4686
|
+
urlPath,
|
|
4687
|
+
slug: entry.slug,
|
|
4688
|
+
entryPath: entry.logicalPath,
|
|
4689
|
+
entryId: entry.contentId,
|
|
4690
|
+
collectionId: collection.contentId,
|
|
4691
|
+
collectionPath: entry.collectionPath,
|
|
4692
|
+
entryType: entry.entryType,
|
|
4693
|
+
format: entry.format,
|
|
4694
|
+
data,
|
|
4695
|
+
schema: collection.entries?.find((e) => e.name === entry.entryType)?.schema
|
|
4696
|
+
};
|
|
4697
|
+
if (filter && !filter(item))
|
|
4698
|
+
continue;
|
|
4699
|
+
items.push(item);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
if (customSort) {
|
|
4703
|
+
items.sort(customSort);
|
|
4704
|
+
} else {
|
|
4705
|
+
items.sort((a, b) => a.entryPath.localeCompare(b.entryPath));
|
|
4706
|
+
}
|
|
4707
|
+
return items;
|
|
4708
|
+
}
|
|
4709
|
+
var listCollectionEntries = async (root, collection) => {
|
|
4710
|
+
if (collection.type !== "collection" || !collection.entries) {
|
|
4711
|
+
return [];
|
|
4712
|
+
}
|
|
4713
|
+
const entryTypes = collection.entries;
|
|
4714
|
+
const extToTypes = /* @__PURE__ */ new Map();
|
|
4715
|
+
for (const entryType of entryTypes) {
|
|
4716
|
+
const ext = getFormatExtension(entryType.format);
|
|
4717
|
+
const existing = extToTypes.get(ext) || [];
|
|
4718
|
+
existing.push(entryType);
|
|
4719
|
+
extToTypes.set(ext, existing);
|
|
4720
|
+
}
|
|
4721
|
+
const validExts = Array.from(extToTypes.keys());
|
|
4722
|
+
const collectionRoot = await resolveCollectionPath(root, collection.logicalPath);
|
|
4723
|
+
if (!collectionRoot) {
|
|
4724
|
+
return [];
|
|
4725
|
+
}
|
|
4726
|
+
normalizePath(root, collectionRoot);
|
|
4727
|
+
let dirents;
|
|
4728
|
+
try {
|
|
4729
|
+
dirents = await fs14.readdir(collectionRoot, { withFileTypes: true });
|
|
4730
|
+
} catch (err) {
|
|
4731
|
+
if (isNotFoundError(err))
|
|
4732
|
+
return [];
|
|
4733
|
+
throw err;
|
|
4734
|
+
}
|
|
4735
|
+
const files = dirents.filter((d) => d.isFile() && validExts.some((ext) => d.name.endsWith(ext)) && d.name !== ".collection.json").sort((a, b) => a.name.localeCompare(b.name));
|
|
4736
|
+
const entries = await Promise.all(files.map(async (file) => {
|
|
4737
|
+
const absolutePath = path15.join(collectionRoot, file.name);
|
|
4738
|
+
const relativePath = normalizePath(root, absolutePath);
|
|
4739
|
+
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
4740
|
+
if (!parsed) {
|
|
4741
|
+
log5.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
|
|
4742
|
+
return null;
|
|
4743
|
+
}
|
|
4744
|
+
const { type: entryTypeName, slug, id: contentId } = parsed;
|
|
4745
|
+
const entryType = entryTypes.find((e) => e.name === entryTypeName);
|
|
4746
|
+
const format = entryType?.format || "json";
|
|
4747
|
+
const bodyField = entryType?.schema ? findBodyFieldName(entryType.schema) : "body";
|
|
4748
|
+
const [stats, data] = await Promise.all([
|
|
4749
|
+
fs14.stat(absolutePath),
|
|
4750
|
+
readEntryData(absolutePath, format, bodyField)
|
|
4751
|
+
]);
|
|
4752
|
+
const item = {
|
|
4753
|
+
logicalPath: `${collection.logicalPath}/${slug}`,
|
|
4754
|
+
contentId,
|
|
4755
|
+
slug,
|
|
4756
|
+
collectionPath: collection.logicalPath,
|
|
4757
|
+
collectionName: collection.name,
|
|
4758
|
+
format,
|
|
4759
|
+
entryType: entryTypeName,
|
|
4760
|
+
physicalPath: relativePath,
|
|
4761
|
+
data,
|
|
4762
|
+
updatedAt: stats.mtime.toISOString()
|
|
4763
|
+
};
|
|
4764
|
+
return item;
|
|
4765
|
+
}));
|
|
4766
|
+
return entries.filter((e) => e !== null);
|
|
4767
|
+
};
|
|
4768
|
+
|
|
4769
|
+
// dist/validation/entry-validator.js
|
|
4770
|
+
init_config2();
|
|
4771
|
+
|
|
4772
|
+
// dist/validation/field-traversal.js
|
|
4773
|
+
function isPlainRecord(value) {
|
|
4774
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4775
|
+
}
|
|
4776
|
+
function resolveBlockItem(blockField, item) {
|
|
4777
|
+
const templateName = typeof item.template === "string" ? item.template : typeof item._type === "string" ? item._type : void 0;
|
|
4778
|
+
if (!templateName)
|
|
4779
|
+
return void 0;
|
|
4780
|
+
const template = blockField.templates?.find((t) => t.name === templateName);
|
|
4781
|
+
if (!template?.fields)
|
|
4782
|
+
return void 0;
|
|
4783
|
+
const data = isPlainRecord(item.value) ? item.value : item;
|
|
4784
|
+
return { fields: template.fields, data };
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
// dist/validation/entry-validator.js
|
|
4788
|
+
var KNOWN_FIELD_TYPES = new Set(fieldTypes);
|
|
4789
|
+
var STRING_FIELD_TYPES = /* @__PURE__ */ new Set(["string", "rich-text", "markdown", "mdx", "image", "code"]);
|
|
4790
|
+
function isPlainRecord2(value) {
|
|
4791
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4792
|
+
}
|
|
4793
|
+
function referenceValueId(value) {
|
|
4794
|
+
if (typeof value === "string")
|
|
4795
|
+
return value;
|
|
4796
|
+
if (isPlainRecord2(value) && typeof value.id === "string")
|
|
4797
|
+
return value.id;
|
|
4798
|
+
return void 0;
|
|
4799
|
+
}
|
|
4800
|
+
var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
|
|
4801
|
+
function validateScalar(field, value, path18) {
|
|
4802
|
+
switch (field.type) {
|
|
4803
|
+
case "number":
|
|
4804
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4805
|
+
return [{ fieldPath: path18, message: "Expected a number" }];
|
|
4806
|
+
}
|
|
4807
|
+
return [];
|
|
4808
|
+
case "boolean":
|
|
4809
|
+
if (typeof value !== "boolean") {
|
|
4810
|
+
return [{ fieldPath: path18, message: "Expected true or false" }];
|
|
4811
|
+
}
|
|
4812
|
+
return [];
|
|
4813
|
+
case "datetime":
|
|
4814
|
+
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
4815
|
+
return [{ fieldPath: path18, message: "Expected a valid date/time string" }];
|
|
4816
|
+
}
|
|
4817
|
+
return [];
|
|
4818
|
+
case "select": {
|
|
4819
|
+
if (typeof value !== "string") {
|
|
4820
|
+
return [{ fieldPath: path18, message: "Expected a selection" }];
|
|
4821
|
+
}
|
|
4822
|
+
const options = selectOptionValues(field);
|
|
4823
|
+
if (value !== "" && !options.includes(value)) {
|
|
4824
|
+
return [{ fieldPath: path18, message: `Must be one of: ${options.join(", ")}` }];
|
|
4825
|
+
}
|
|
4826
|
+
return [];
|
|
4827
|
+
}
|
|
4828
|
+
case "reference":
|
|
4829
|
+
if (referenceValueId(value) === void 0) {
|
|
4830
|
+
return [{ fieldPath: path18, message: "Expected a reference id" }];
|
|
4831
|
+
}
|
|
4832
|
+
return [];
|
|
4833
|
+
default:
|
|
4834
|
+
if (STRING_FIELD_TYPES.has(field.type)) {
|
|
4835
|
+
if (typeof value !== "string") {
|
|
4836
|
+
return [{ fieldPath: path18, message: "Expected text" }];
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
return [];
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
function isEmptyForRequired(field, value) {
|
|
4843
|
+
if (value === void 0 || value === null)
|
|
4844
|
+
return true;
|
|
4845
|
+
if (Array.isArray(value))
|
|
4846
|
+
return value.length === 0;
|
|
4847
|
+
if (typeof value === "string") {
|
|
4848
|
+
return value.trim() === "";
|
|
4849
|
+
}
|
|
4850
|
+
return false;
|
|
4851
|
+
}
|
|
4852
|
+
function validateEntryData(fields, data, pathPrefix = "") {
|
|
4853
|
+
const errors = [];
|
|
4854
|
+
for (const field of fields) {
|
|
4855
|
+
if (field.type === "group") {
|
|
4856
|
+
errors.push(...validateEntryData(field.fields, data, pathPrefix));
|
|
4857
|
+
continue;
|
|
4858
|
+
}
|
|
4859
|
+
const path18 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
4860
|
+
const value = data[field.name];
|
|
4861
|
+
const isRequired = "required" in field && field.required === true;
|
|
4862
|
+
if (isRequired && isEmptyForRequired(field, value)) {
|
|
4863
|
+
const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
|
|
4864
|
+
errors.push({ fieldPath: path18, message });
|
|
4865
|
+
continue;
|
|
4866
|
+
}
|
|
4867
|
+
if (value === void 0 || value === null)
|
|
4868
|
+
continue;
|
|
4869
|
+
if (field.type === "block") {
|
|
4870
|
+
const blockField = field;
|
|
4871
|
+
if (!Array.isArray(value)) {
|
|
4872
|
+
errors.push({ fieldPath: path18, message: "Expected a list of blocks" });
|
|
4873
|
+
continue;
|
|
4874
|
+
}
|
|
4875
|
+
value.forEach((item, index) => {
|
|
4876
|
+
const itemPath = `${path18}[${index}]`;
|
|
4877
|
+
if (!isPlainRecord2(item)) {
|
|
4878
|
+
errors.push({ fieldPath: itemPath, message: "Expected a block object" });
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
const resolved = resolveBlockItem(blockField, item);
|
|
4882
|
+
if (!resolved) {
|
|
4883
|
+
const template = typeof item.template === "string" ? item.template : void 0;
|
|
4884
|
+
errors.push({
|
|
4885
|
+
fieldPath: itemPath,
|
|
4886
|
+
message: template ? `Unknown block template "${template}"` : "Missing block template"
|
|
4887
|
+
});
|
|
4888
|
+
return;
|
|
4889
|
+
}
|
|
4890
|
+
errors.push(...validateEntryData(resolved.fields, resolved.data, itemPath));
|
|
4891
|
+
});
|
|
4892
|
+
continue;
|
|
4893
|
+
}
|
|
4894
|
+
if (field.type === "object") {
|
|
4895
|
+
const objectField = field;
|
|
4896
|
+
if (objectField.list) {
|
|
4897
|
+
if (!Array.isArray(value)) {
|
|
4898
|
+
errors.push({ fieldPath: path18, message: "Expected a list of items" });
|
|
4899
|
+
continue;
|
|
4900
|
+
}
|
|
4901
|
+
value.forEach((item, index) => {
|
|
4902
|
+
const itemPath = `${path18}[${index}]`;
|
|
4903
|
+
if (!isPlainRecord2(item)) {
|
|
4904
|
+
errors.push({ fieldPath: itemPath, message: "Expected an object" });
|
|
4905
|
+
return;
|
|
4906
|
+
}
|
|
4907
|
+
if (objectField.fields) {
|
|
4908
|
+
errors.push(...validateEntryData(objectField.fields, item, itemPath));
|
|
4909
|
+
}
|
|
4910
|
+
});
|
|
4911
|
+
} else if (!isPlainRecord2(value)) {
|
|
4912
|
+
errors.push({ fieldPath: path18, message: "Expected an object" });
|
|
4913
|
+
} else if (objectField.fields) {
|
|
4914
|
+
errors.push(...validateEntryData(objectField.fields, value, path18));
|
|
4915
|
+
}
|
|
4916
|
+
continue;
|
|
4917
|
+
}
|
|
4918
|
+
if ("list" in field && field.list) {
|
|
4919
|
+
if (!Array.isArray(value)) {
|
|
4920
|
+
errors.push({ fieldPath: path18, message: "Expected a list of values" });
|
|
4921
|
+
continue;
|
|
4922
|
+
}
|
|
4923
|
+
value.forEach((item, index) => {
|
|
4924
|
+
errors.push(...validateScalar(field, item, `${path18}[${index}]`));
|
|
4925
|
+
});
|
|
4926
|
+
continue;
|
|
4927
|
+
}
|
|
4928
|
+
if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
|
|
4929
|
+
errors.push({ fieldPath: path18, message: "Expected a single value, not a list" });
|
|
4930
|
+
continue;
|
|
4931
|
+
}
|
|
4932
|
+
errors.push(...validateScalar(field, value, path18));
|
|
4933
|
+
}
|
|
4934
|
+
return errors;
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
// dist/static/index.js
|
|
4938
|
+
function normalizeDatesDeep(value) {
|
|
4939
|
+
if (value instanceof Date)
|
|
4940
|
+
return value.toISOString();
|
|
4941
|
+
if (Array.isArray(value))
|
|
4942
|
+
return value.map(normalizeDatesDeep);
|
|
4943
|
+
if (value && typeof value === "object") {
|
|
4944
|
+
const result = {};
|
|
4945
|
+
for (const [key, v] of Object.entries(value)) {
|
|
4946
|
+
result[key] = normalizeDatesDeep(v);
|
|
4947
|
+
}
|
|
4948
|
+
return result;
|
|
4949
|
+
}
|
|
4950
|
+
return value;
|
|
4951
|
+
}
|
|
4952
|
+
function findInvalidEntries(items) {
|
|
4953
|
+
const invalid = [];
|
|
4954
|
+
for (const item of items) {
|
|
4955
|
+
if (!item.schema)
|
|
4956
|
+
continue;
|
|
4957
|
+
const data = normalizeDatesDeep(item.data);
|
|
4958
|
+
const errors = validateEntryData(item.schema, data);
|
|
4959
|
+
if (errors.length > 0) {
|
|
4960
|
+
invalid.push({ entryPath: item.entryPath, errors });
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
return invalid;
|
|
4964
|
+
}
|
|
4965
|
+
function assertBuildEntriesValid(items, phaseLabel) {
|
|
4966
|
+
const invalid = findInvalidEntries(items);
|
|
4967
|
+
if (invalid.length === 0)
|
|
4968
|
+
return;
|
|
4969
|
+
const lines = invalid.map(({ entryPath, errors }) => {
|
|
4970
|
+
const [first, ...rest] = errors;
|
|
4971
|
+
const summary = rest.length > 0 ? `${first.fieldPath}: ${first.message} (+${rest.length} more)` : `${first.fieldPath}: ${first.message}`;
|
|
4972
|
+
return ` - ${entryPath} \u2014 ${summary}`;
|
|
4973
|
+
});
|
|
4974
|
+
throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? "entry" : "entries"} during ${phaseLabel}:
|
|
4975
|
+
${lines.join("\n")}
|
|
4976
|
+
These are likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4581
4979
|
// dist/build/generate-ai-content.js
|
|
4582
4980
|
async function generateAIContentFiles(options) {
|
|
4583
4981
|
const { config, entrySchemaRegistry, outputDir, aiConfig, _testFlatSchema } = options;
|
|
@@ -4591,6 +4989,8 @@ async function generateAIContentFiles(options) {
|
|
|
4591
4989
|
const cached = await schemaCache.getSchema(branchRoot, entrySchemaRegistry, contentRootName);
|
|
4592
4990
|
flatSchema = cached.flatSchema;
|
|
4593
4991
|
}
|
|
4992
|
+
const entriesForValidation = await listEntries(branchRoot, flatSchema, contentRootName);
|
|
4993
|
+
assertBuildEntriesValid(entriesForValidation, "AI content generation");
|
|
4594
4994
|
const store = new ContentStore(branchRoot, flatSchema);
|
|
4595
4995
|
const result = await generateAIContent({
|
|
4596
4996
|
store,
|
|
@@ -4599,15 +4999,15 @@ async function generateAIContentFiles(options) {
|
|
|
4599
4999
|
config: aiConfig,
|
|
4600
5000
|
entryLinkUrl: config.entryLinkUrl
|
|
4601
5001
|
});
|
|
4602
|
-
const absoluteOutputDir =
|
|
5002
|
+
const absoluteOutputDir = path16.resolve(outputDir) + path16.sep;
|
|
4603
5003
|
let fileCount = 0;
|
|
4604
5004
|
for (const [filePath, content] of result.files) {
|
|
4605
|
-
const absolutePath =
|
|
5005
|
+
const absolutePath = path16.resolve(path16.join(absoluteOutputDir, filePath));
|
|
4606
5006
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
4607
5007
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
4608
5008
|
}
|
|
4609
|
-
await
|
|
4610
|
-
await
|
|
5009
|
+
await fs15.mkdir(path16.dirname(absolutePath), { recursive: true });
|
|
5010
|
+
await fs15.writeFile(absolutePath, content, "utf-8");
|
|
4611
5011
|
fileCount++;
|
|
4612
5012
|
}
|
|
4613
5013
|
return { fileCount, outputDir: absoluteOutputDir };
|
|
@@ -4618,7 +5018,7 @@ var jiti = createJiti(import.meta.url);
|
|
|
4618
5018
|
async function generateAIContentCLI(options) {
|
|
4619
5019
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
4620
5020
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
4621
|
-
const canopyConfigPath =
|
|
5021
|
+
const canopyConfigPath = path17.join(projectDir, "canopycms.config.ts");
|
|
4622
5022
|
let canopyConfigModule;
|
|
4623
5023
|
try {
|
|
4624
5024
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -4629,7 +5029,7 @@ async function generateAIContentCLI(options) {
|
|
|
4629
5029
|
}
|
|
4630
5030
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
4631
5031
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
4632
|
-
const schemasPath =
|
|
5032
|
+
const schemasPath = path17.join(projectDir, appDir, "schemas.ts");
|
|
4633
5033
|
let entrySchemaRegistry = {};
|
|
4634
5034
|
try {
|
|
4635
5035
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -4640,7 +5040,7 @@ async function generateAIContentCLI(options) {
|
|
|
4640
5040
|
let aiConfig;
|
|
4641
5041
|
if (configPath) {
|
|
4642
5042
|
try {
|
|
4643
|
-
const aiConfigModule = await jiti.import(
|
|
5043
|
+
const aiConfigModule = await jiti.import(path17.resolve(configPath));
|
|
4644
5044
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
4645
5045
|
} catch (err) {
|
|
4646
5046
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -4657,7 +5057,7 @@ async function generateAIContentCLI(options) {
|
|
|
4657
5057
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
4658
5058
|
process.exit(1);
|
|
4659
5059
|
}
|
|
4660
|
-
const resolvedOutput =
|
|
5060
|
+
const resolvedOutput = path17.resolve(projectDir, outputDir);
|
|
4661
5061
|
console.log(` Output: ${resolvedOutput}`);
|
|
4662
5062
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
4663
5063
|
const result = await generateAIContentFiles({
|
package/dist/cli/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAIhF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,CAAA;AAE1C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;IACvB,EAAE,EAAE,OAAO,CAAA;IACX,4CAA4C;IAC5C,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;CACxB;AA+CD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA8I9D;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2C7E;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkC,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAIhF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,CAAA;AAE1C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;IACvB,EAAE,EAAE,OAAO,CAAA;IACX,4CAA4C;IAC5C,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,UAAU,iBAAiB;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,cAAc,EAAE,OAAO,CAAA;CACxB;AA+CD;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA8I9D;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2C7E;AAwED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFhB"}
|