canopycms 0.4.0 → 0.6.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.
- package/CHANGELOG.md +78 -42
- package/dist/ai/generate.d.ts +3 -0
- package/dist/ai/generate.js +12 -29
- package/dist/ai/handler.js +1 -0
- package/dist/ai/types.d.ts +3 -9
- package/dist/api/reference-options.d.ts +2 -5
- package/dist/api/reference-options.js +21 -2
- package/dist/api/schema.d.ts +4 -4
- package/dist/build/generate-ai-content.js +1 -0
- package/dist/cli/cli.js +136 -39
- package/dist/cli/generate-ai-content.js +75 -32
- package/dist/cli/init.js +34 -5
- package/dist/cli/template-files/cms-stack.ts.template +3 -0
- package/dist/cli/templates.js +5 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +74 -0
- package/dist/config/schemas/config.js +21 -0
- package/dist/config/schemas/field.d.ts +30 -0
- package/dist/config/schemas/field.js +22 -0
- package/dist/config/types.d.ts +39 -0
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +33 -0
- package/dist/editor/FormRenderer.js +15 -13
- package/dist/editor/copy.d.ts +5 -0
- package/dist/editor/copy.js +5 -0
- package/dist/editor/fields/BlockField.d.ts +2 -0
- package/dist/editor/fields/BlockField.js +15 -8
- package/dist/editor/fields/ItemList.d.ts +21 -3
- package/dist/editor/fields/ItemList.js +16 -8
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +6 -2
- package/dist/editor/fields/ObjectListField.js +9 -3
- package/dist/editor/fields/ReferenceField.d.ts +6 -7
- package/dist/editor/fields/ReferenceField.js +28 -122
- package/dist/editor/fields/ReferenceListField.d.ts +41 -0
- package/dist/editor/fields/ReferenceListField.js +139 -0
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +6 -3
- package/dist/editor/fields/StringListField.d.ts +10 -1
- package/dist/editor/fields/StringListField.js +18 -8
- package/dist/editor/fields/item-expansion.d.ts +7 -3
- package/dist/editor/fields/item-expansion.js +6 -4
- package/dist/editor/fields/list-item-keys.d.ts +6 -5
- package/dist/editor/fields/list-item-keys.js +45 -13
- package/dist/editor/fields/reference-noindex.d.ts +37 -0
- package/dist/editor/fields/reference-noindex.js +48 -0
- package/dist/editor/fields/use-reference-options.d.ts +36 -0
- package/dist/editor/fields/use-reference-options.js +121 -0
- package/dist/editor/form-outline.d.ts +2 -2
- package/dist/editor/form-outline.js +4 -2
- package/dist/editor/theme.js +5 -0
- package/dist/entry-schema-registry.d.ts +1 -1
- package/dist/entry-schema-registry.js +3 -2
- package/dist/entry-schema.d.ts +5 -3
- package/dist/entry-schema.js +2 -1
- package/dist/reference-resolver.d.ts +18 -1
- package/dist/reference-resolver.js +175 -8
- package/dist/schema/schema-store.d.ts +2 -2
- package/dist/static/index.d.ts +8 -4
- package/dist/static/index.js +38 -13
- package/dist/static/seo.d.ts +7 -5
- package/dist/static/seo.js +13 -6
- package/dist/validation/entry-validator.d.ts +5 -0
- package/dist/validation/entry-validator.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/github-gateway-http.js +2 -2
- package/dist/worker/github-gateway.js +2 -2
- package/package.json +1 -1
package/dist/static/index.js
CHANGED
|
@@ -68,11 +68,13 @@ async function enumerateRoutableEntries(ctx, rootPath, phaseLabel, resolveRefere
|
|
|
68
68
|
* when you actually mean to — omission should be a decision, not an oversight.
|
|
69
69
|
*
|
|
70
70
|
* `noindex` is NOT applied here: an entry that must not be advertised must still be built. Apply
|
|
71
|
-
* `isNoindexEntry` at the advertising surface (as `generateContentSitemap` does)
|
|
71
|
+
* `isNoindexEntry` at the advertising surface (as `generateContentSitemap` does), at the SEO
|
|
72
|
+
* location in `CanopyConfig.seo`.
|
|
72
73
|
*
|
|
73
74
|
* @example
|
|
74
|
-
* const
|
|
75
|
-
* const
|
|
75
|
+
* const ctx = await getCanopyForBuild()
|
|
76
|
+
* const entries = await collectRoutableEntries(ctx)
|
|
77
|
+
* const published = entries.filter((e) => !isNoindexEntry(e.data, ctx.services.config.seo))
|
|
76
78
|
*/
|
|
77
79
|
export async function collectRoutableEntries(ctx, options = {}) {
|
|
78
80
|
const entries = await enumerateRoutableEntries(ctx, options.rootPath, 'routable entry enumeration', options.resolveReferences);
|
|
@@ -283,22 +285,45 @@ export function warnUnsafeMarkdown(items, phaseLabel, siteAllow) {
|
|
|
283
285
|
*
|
|
284
286
|
* Fails the build rather than silently skipping the offending entry: a page that silently
|
|
285
287
|
* disappears from a static build is a worse failure mode than a red build. Lists every
|
|
286
|
-
* offending entry so one build catches every abandoned scaffold, not just the first.
|
|
288
|
+
* offending entry so one build catches every abandoned scaffold, not just the first. An
|
|
289
|
+
* over-`maxItems` list gets its own line per list, with the fix; the abandoned-scaffold advice
|
|
290
|
+
* appears only when some entry has another kind of error.
|
|
287
291
|
*/
|
|
288
292
|
export function assertBuildEntriesValid(items, phaseLabel) {
|
|
289
293
|
const invalid = findInvalidEntries(items);
|
|
290
294
|
if (invalid.length === 0)
|
|
291
295
|
return;
|
|
292
|
-
const lines =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
const lines = [];
|
|
297
|
+
let hasOverCap = false;
|
|
298
|
+
let hasOrdinary = false;
|
|
299
|
+
for (const { entryPath, errors } of invalid) {
|
|
300
|
+
const ordinary = errors.filter((e) => e.maxItems === undefined);
|
|
301
|
+
if (ordinary.length > 0) {
|
|
302
|
+
hasOrdinary = true;
|
|
303
|
+
const [first, ...rest] = ordinary;
|
|
304
|
+
const summary = rest.length > 0
|
|
305
|
+
? `${first.fieldPath}: ${first.message} (+${rest.length} more)`
|
|
306
|
+
: `${first.fieldPath}: ${first.message}`;
|
|
307
|
+
lines.push(` - ${entryPath} — ${summary}`);
|
|
308
|
+
}
|
|
309
|
+
for (const { fieldPath, maxItems } of errors) {
|
|
310
|
+
if (maxItems === undefined)
|
|
311
|
+
continue;
|
|
312
|
+
hasOverCap = true;
|
|
313
|
+
lines.push(` - ${entryPath} — ${fieldPath}: ${maxItems.count} items, but maxItems is ${maxItems.cap}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const advice = [];
|
|
317
|
+
if (hasOverCap) {
|
|
318
|
+
advice.push(`A list over its maxItems: open the entry in the editor and remove a pick from that list (or remove items) until it holds at most maxItems, then rebuild. ` +
|
|
319
|
+
`This usually happens when two branches each add one item to the same list and are merged one after the other.`);
|
|
320
|
+
}
|
|
321
|
+
if (hasOrdinary) {
|
|
322
|
+
advice.push(`${hasOverCap ? 'The other errors are' : 'These are'} likely abandoned create-scaffolds (empty entries left behind when a create was started but never finished). ` +
|
|
323
|
+
`Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
|
|
324
|
+
}
|
|
299
325
|
throw new Error(`CanopyCMS static build: found ${invalid.length} schema-invalid ${invalid.length === 1 ? 'entry' : 'entries'} during ${phaseLabel}:\n${lines.join('\n')}\n` +
|
|
300
|
-
|
|
301
|
-
`Finish editing the entry (fill in its required fields) or delete the abandoned draft, then rebuild.`);
|
|
326
|
+
advice.join('\n'));
|
|
302
327
|
}
|
|
303
328
|
/**
|
|
304
329
|
* Find every `urlPath` claimed by two or more entries.
|
package/dist/static/seo.d.ts
CHANGED
|
@@ -72,14 +72,16 @@ export type SeoFieldLocation = Pick<ExtractSeoFieldsOptions, 'fields' | 'group'>
|
|
|
72
72
|
*/
|
|
73
73
|
export declare function extractSeoFields(entryData: unknown, opts?: ExtractSeoFieldsOptions): SeoFields;
|
|
74
74
|
/**
|
|
75
|
-
* THE `noindex` predicate. Every surface that
|
|
76
|
-
*
|
|
75
|
+
* THE `noindex` predicate. Every surface that reads the flag routes through this one function,
|
|
76
|
+
* with `opts` taken from `CanopyConfig.seo` (canopycms-next's free helpers take it as an
|
|
77
|
+
* argument instead), rather than re-deriving it:
|
|
77
78
|
*
|
|
78
79
|
* - the page's own `robots: { index: false }` (canopycms-next's `entryToMetadata`),
|
|
79
|
-
* - exclusion from the sitemap (canopycms-next's `generateContentSitemap`),
|
|
80
|
-
* - exclusion from AI content (`ai/generate.ts`, unless `exclude.noindex: false`)
|
|
80
|
+
* - exclusion from the sitemap (canopycms-next's `generateContentSitemap`),
|
|
81
|
+
* - exclusion from AI content (`ai/generate.ts`, unless `exclude.noindex: false`), and
|
|
82
|
+
* - the marker on a reference picker's option (`ReferenceResolver.loadReferenceOptions`).
|
|
81
83
|
*
|
|
82
|
-
*
|
|
84
|
+
* One predicate at one configured location is the point. Derived separately, a `noindex` entry
|
|
83
85
|
* stays advertised in one surface while correctly absent from another, and nothing warns.
|
|
84
86
|
*
|
|
85
87
|
* NOT applied to enumeration: `noindex` means "don't advertise this", not "don't build this".
|
package/dist/static/seo.js
CHANGED
|
@@ -75,7 +75,12 @@ const asTwitterCard = (value) => {
|
|
|
75
75
|
* rather than throwing.
|
|
76
76
|
*/
|
|
77
77
|
export function extractSeoFields(entryData, opts = {}) {
|
|
78
|
-
const names = { ...DEFAULT_SEO_FIELD_NAMES
|
|
78
|
+
const names = { ...DEFAULT_SEO_FIELD_NAMES };
|
|
79
|
+
// Only a real name overrides: `{ noindex: undefined }` (an unset env var, say) keeps the default.
|
|
80
|
+
for (const [key, name] of Object.entries(opts.fields ?? {})) {
|
|
81
|
+
if (typeof name === 'string' && name !== '')
|
|
82
|
+
names[key] = name;
|
|
83
|
+
}
|
|
79
84
|
const entry = asRecord(entryData);
|
|
80
85
|
const source = opts.group ? asRecord(entry?.[opts.group]) : entry;
|
|
81
86
|
return {
|
|
@@ -89,14 +94,16 @@ export function extractSeoFields(entryData, opts = {}) {
|
|
|
89
94
|
};
|
|
90
95
|
}
|
|
91
96
|
/**
|
|
92
|
-
* THE `noindex` predicate. Every surface that
|
|
93
|
-
*
|
|
97
|
+
* THE `noindex` predicate. Every surface that reads the flag routes through this one function,
|
|
98
|
+
* with `opts` taken from `CanopyConfig.seo` (canopycms-next's free helpers take it as an
|
|
99
|
+
* argument instead), rather than re-deriving it:
|
|
94
100
|
*
|
|
95
101
|
* - the page's own `robots: { index: false }` (canopycms-next's `entryToMetadata`),
|
|
96
|
-
* - exclusion from the sitemap (canopycms-next's `generateContentSitemap`),
|
|
97
|
-
* - exclusion from AI content (`ai/generate.ts`, unless `exclude.noindex: false`)
|
|
102
|
+
* - exclusion from the sitemap (canopycms-next's `generateContentSitemap`),
|
|
103
|
+
* - exclusion from AI content (`ai/generate.ts`, unless `exclude.noindex: false`), and
|
|
104
|
+
* - the marker on a reference picker's option (`ReferenceResolver.loadReferenceOptions`).
|
|
98
105
|
*
|
|
99
|
-
*
|
|
106
|
+
* One predicate at one configured location is the point. Derived separately, a `noindex` entry
|
|
100
107
|
* stays advertised in one surface while correctly absent from another, and nothing warns.
|
|
101
108
|
*
|
|
102
109
|
* NOT applied to enumeration: `noindex` means "don't advertise this", not "don't build this".
|
|
@@ -20,6 +20,11 @@ import type { ContentFormat, EntrySchema } from '../config/index.js';
|
|
|
20
20
|
export interface EntryFieldError {
|
|
21
21
|
fieldPath: string;
|
|
22
22
|
message: string;
|
|
23
|
+
/** Set on an over-cap list's error, so a caller can say how to fix it. */
|
|
24
|
+
maxItems?: {
|
|
25
|
+
count: number;
|
|
26
|
+
cap: number;
|
|
27
|
+
};
|
|
23
28
|
}
|
|
24
29
|
/**
|
|
25
30
|
* Merge an md/mdx body string into the data record under the schema's body
|
|
@@ -173,6 +173,18 @@ export function validateEntryData(fields, data, pathPrefix = '', options = {}) {
|
|
|
173
173
|
}
|
|
174
174
|
if (value === undefined || value === null)
|
|
175
175
|
continue;
|
|
176
|
+
// No `continue`: the items inside an over-cap list are still checked and reported.
|
|
177
|
+
if (Array.isArray(value) &&
|
|
178
|
+
'maxItems' in field &&
|
|
179
|
+
typeof field.maxItems === 'number' &&
|
|
180
|
+
value.length > field.maxItems) {
|
|
181
|
+
const cap = field.maxItems;
|
|
182
|
+
errors.push({
|
|
183
|
+
fieldPath: path,
|
|
184
|
+
message: `At most ${cap} ${cap === 1 ? 'item' : 'items'}; this list has ${value.length}`,
|
|
185
|
+
maxItems: { count: value.length, cap },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
176
188
|
if (field.type === 'block') {
|
|
177
189
|
const blockField = field;
|
|
178
190
|
if (!Array.isArray(value)) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CANOPYCMS_VERSION = "0.
|
|
1
|
+
export declare const CANOPYCMS_VERSION = "0.6.0";
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by scripts/bump-version.mjs, its only writer. The package builds with plain
|
|
2
2
|
// tsc (rootDir: src), so src/ cannot import ../package.json; version.test.ts keeps the two equal.
|
|
3
|
-
export const CANOPYCMS_VERSION = '0.
|
|
3
|
+
export const CANOPYCMS_VERSION = '0.6.0';
|
|
@@ -631,8 +631,8 @@ const POLL_WAIT_MS = MAX_POLL_WAIT_MS;
|
|
|
631
631
|
/** How long `health` waits for the gateway: `getSecret` alone has taken 87 s. */
|
|
632
632
|
const HEALTH_WAIT_MS = 300_000;
|
|
633
633
|
/**
|
|
634
|
-
* How long `authSnapshot` waits: a
|
|
635
|
-
*
|
|
634
|
+
* How long `authSnapshot` waits: a generous bound on a fetch that pages the provider's whole user,
|
|
635
|
+
* organization and membership lists one page at a time. A later call joins a fetch an earlier one stopped waiting for.
|
|
636
636
|
*/
|
|
637
637
|
const AUTH_SNAPSHOT_WAIT_MS = 30 * 60_000;
|
|
638
638
|
/** How long `termination` and `completeLifecycle` wait: a poll's wait is well inside it. */
|
|
@@ -283,8 +283,8 @@ class LocalGitHubGateway {
|
|
|
283
283
|
}
|
|
284
284
|
/**
|
|
285
285
|
* One fetch serves every caller: concurrent callers join it, and its result is answered again
|
|
286
|
-
* for {@link AUTH_SNAPSHOT_REUSE_MS}. A fetch
|
|
287
|
-
* would otherwise hold the provider's API at its rate limit. The fetch runs apart from every
|
|
286
|
+
* for {@link AUTH_SNAPSHOT_REUSE_MS}. A fetch pages the provider's whole user and membership lists,
|
|
287
|
+
* so a worker asking in a loop would otherwise hold the provider's API at its rate limit. The fetch runs apart from every
|
|
288
288
|
* caller's signal, which ends only that caller's wait, so a retry after a timed-out wait joins it.
|
|
289
289
|
*/
|
|
290
290
|
async authSnapshot(signal) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@codemirror/language, @lezer/highlight: workaround — @mdxeditor/editor uses cm6-theme-basic-light which peer-requires these but mdxeditor doesn't declare them as dependencies",
|
|
3
3
|
"name": "canopycms",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "CanopyCMS core package: schema-driven content, branch-aware editing, and editor UI for Next.js.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|