canopycms 0.0.68-int.101 → 0.0.68-int.102
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/client.d.ts +2 -1
- package/dist/api/client.js +2 -1
- package/dist/auth/plugin.d.ts +1 -1
- package/dist/cli/cli.js +5 -0
- package/dist/cli/generate-ai-content.js +3 -0
- package/dist/cli/init.js +1 -0
- package/dist/config/schemas/config.d.ts +3 -0
- package/dist/config/schemas/config.js +1 -0
- package/dist/config/types.d.ts +15 -0
- package/dist/content-reader.d.ts +9 -0
- package/dist/content-reader.js +8 -4
- package/dist/content-store.d.ts +4 -0
- package/dist/content-store.js +2 -0
- package/dist/editor/context/ApiClientContext.d.ts +2 -1
- package/dist/editor/context/ApiClientContext.js +2 -1
- package/dist/editor/fields/MarkdownField.js +6 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
- package/dist/editor/fields/mdx-jsx-support.js +74 -70
- package/dist/http/handler.js +7 -2
- package/dist/url-exclusivity-fixtures.d.ts +7 -0
- package/dist/url-exclusivity-fixtures.js +10 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/api/client.d.ts
CHANGED
|
@@ -32,7 +32,8 @@ export interface ApiClientOptions {
|
|
|
32
32
|
*/
|
|
33
33
|
trailingSlash?: boolean;
|
|
34
34
|
/**
|
|
35
|
-
* Called whenever a response
|
|
35
|
+
* Called whenever a response says the credential is no longer accepted: HTTP 401, or a
|
|
36
|
+
* handler's body with `status: 401` under the HTTP status `unauthenticatedStatus` chose. A
|
|
36
37
|
* notification, not a retry; the editor's auth gate uses it to show sign-in.
|
|
37
38
|
*/
|
|
38
39
|
onUnauthorized?: () => void;
|
package/dist/api/client.js
CHANGED
|
@@ -341,7 +341,8 @@ export class CanopyApiClient {
|
|
|
341
341
|
response = await this.fetchFn(url, init);
|
|
342
342
|
parsed = await response.json().catch(() => undefined);
|
|
343
343
|
}
|
|
344
|
-
|
|
344
|
+
// The body's status is the signal: `unauthenticatedStatus` may send it under HTTP 419.
|
|
345
|
+
if (response.status === 401 || (isApiResponseBody(parsed) && parsed.status === 401)) {
|
|
345
346
|
this.onUnauthorized?.();
|
|
346
347
|
return { ok: false, status: 401, error: errorFromBody(parsed) ?? 'Unauthorized' };
|
|
347
348
|
}
|
package/dist/auth/plugin.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface AuthPlugin {
|
|
|
14
14
|
* groups; core applies bootstrap admins.
|
|
15
15
|
*
|
|
16
16
|
* CREDENTIAL failures (missing, invalid or expired token) RESOLVE to
|
|
17
|
-
* `{ success: false }` and map to
|
|
17
|
+
* `{ success: false }` and map to `unauthenticatedStatus` (default 401). CONFIGURATION errors (an absent
|
|
18
18
|
* CLERK_SECRET_KEY, say) may THROW instead: they are operator mistakes, and
|
|
19
19
|
* belong in a loud 500 rather than a quiet auth denial. A custom adapter
|
|
20
20
|
* calling this directly must be ready for that rejection.
|
package/dist/cli/cli.js
CHANGED
|
@@ -1488,6 +1488,7 @@ var init_init = __esm({
|
|
|
1488
1488
|
contentRoot: contentRootSchema.default("content"),
|
|
1489
1489
|
sourceRoot: sourceRootSchema.optional(),
|
|
1490
1490
|
basePath: basePathSchema,
|
|
1491
|
+
unauthenticatedStatus: z5.union([z5.literal(401), z5.literal(419)]).optional(),
|
|
1491
1492
|
editor: editorConfigSchema.optional(),
|
|
1492
1493
|
authPlugin: z5.custom().optional(),
|
|
1493
1494
|
entryLinkUrl: z5.custom().optional(),
|
|
@@ -6901,6 +6902,7 @@ var init_generate_ai_content = __esm({
|
|
|
6901
6902
|
contentRoot: contentRootSchema2.default("content"),
|
|
6902
6903
|
sourceRoot: sourceRootSchema2.optional(),
|
|
6903
6904
|
basePath: basePathSchema2,
|
|
6905
|
+
unauthenticatedStatus: z62.union([z62.literal(401), z62.literal(419)]).optional(),
|
|
6904
6906
|
editor: editorConfigSchema2.optional(),
|
|
6905
6907
|
authPlugin: z62.custom().optional(),
|
|
6906
6908
|
entryLinkUrl: z62.custom().optional(),
|
|
@@ -8713,6 +8715,8 @@ var init_generate_ai_content = __esm({
|
|
|
8713
8715
|
absolutePath: resolved,
|
|
8714
8716
|
relativePath: path11.relative(this.root, resolved),
|
|
8715
8717
|
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
8718
|
+
collectionPath: schemaItem.logicalPath,
|
|
8719
|
+
slug: safeSlug,
|
|
8716
8720
|
id,
|
|
8717
8721
|
entryTypeName: finalEntryTypeName,
|
|
8718
8722
|
existed
|
|
@@ -13318,6 +13322,7 @@ var init_config4 = __esm({
|
|
|
13318
13322
|
contentRoot: contentRootSchema3.default("content"),
|
|
13319
13323
|
sourceRoot: sourceRootSchema3.optional(),
|
|
13320
13324
|
basePath: basePathSchema3,
|
|
13325
|
+
unauthenticatedStatus: z14.union([z14.literal(401), z14.literal(419)]).optional(),
|
|
13321
13326
|
editor: editorConfigSchema3.optional(),
|
|
13322
13327
|
authPlugin: z14.custom().optional(),
|
|
13323
13328
|
entryLinkUrl: z14.custom().optional(),
|
|
@@ -529,6 +529,7 @@ var init_config = __esm({
|
|
|
529
529
|
contentRoot: contentRootSchema.default("content"),
|
|
530
530
|
sourceRoot: sourceRootSchema.optional(),
|
|
531
531
|
basePath: basePathSchema,
|
|
532
|
+
unauthenticatedStatus: z6.union([z6.literal(401), z6.literal(419)]).optional(),
|
|
532
533
|
editor: editorConfigSchema.optional(),
|
|
533
534
|
authPlugin: z6.custom().optional(),
|
|
534
535
|
entryLinkUrl: z6.custom().optional(),
|
|
@@ -3761,6 +3762,8 @@ var ContentStore = class {
|
|
|
3761
3762
|
absolutePath: resolved,
|
|
3762
3763
|
relativePath: path11.relative(this.root, resolved),
|
|
3763
3764
|
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
3765
|
+
collectionPath: schemaItem.logicalPath,
|
|
3766
|
+
slug: safeSlug,
|
|
3764
3767
|
id,
|
|
3765
3768
|
entryTypeName: finalEntryTypeName,
|
|
3766
3769
|
existed
|
package/dist/cli/init.js
CHANGED
|
@@ -379,6 +379,7 @@ var init_config = __esm({
|
|
|
379
379
|
contentRoot: contentRootSchema.default("content"),
|
|
380
380
|
sourceRoot: sourceRootSchema.optional(),
|
|
381
381
|
basePath: basePathSchema,
|
|
382
|
+
unauthenticatedStatus: z5.union([z5.literal(401), z5.literal(419)]).optional(),
|
|
382
383
|
editor: editorConfigSchema.optional(),
|
|
383
384
|
authPlugin: z5.custom().optional(),
|
|
384
385
|
entryLinkUrl: z5.custom().optional(),
|
|
@@ -71,6 +71,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
71
71
|
contentRoot: z.ZodDefault<z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>>>;
|
|
72
72
|
sourceRoot: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
73
73
|
basePath: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
74
|
+
unauthenticatedStatus: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<401>, z.ZodLiteral<419>]>>;
|
|
74
75
|
editor: z.ZodOptional<z.ZodObject<{
|
|
75
76
|
title: z.ZodOptional<z.ZodString>;
|
|
76
77
|
subtitle: z.ZodOptional<z.ZodString>;
|
|
@@ -151,6 +152,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
151
152
|
deploymentName?: string | undefined;
|
|
152
153
|
sourceRoot?: string | undefined;
|
|
153
154
|
basePath?: string | undefined;
|
|
155
|
+
unauthenticatedStatus?: 401 | 419 | undefined;
|
|
154
156
|
editor?: {
|
|
155
157
|
title?: string | undefined;
|
|
156
158
|
subtitle?: string | undefined;
|
|
@@ -204,6 +206,7 @@ export declare const CanopyConfigSchema: z.ZodObject<{
|
|
|
204
206
|
contentRoot?: string | undefined;
|
|
205
207
|
sourceRoot?: string | undefined;
|
|
206
208
|
basePath?: string | undefined;
|
|
209
|
+
unauthenticatedStatus?: 401 | 419 | undefined;
|
|
207
210
|
editor?: {
|
|
208
211
|
title?: string | undefined;
|
|
209
212
|
subtitle?: string | undefined;
|
|
@@ -103,6 +103,7 @@ export const CanopyConfigSchema = z
|
|
|
103
103
|
contentRoot: contentRootSchema.default('content'),
|
|
104
104
|
sourceRoot: sourceRootSchema.optional(),
|
|
105
105
|
basePath: basePathSchema,
|
|
106
|
+
unauthenticatedStatus: z.union([z.literal(401), z.literal(419)]).optional(),
|
|
106
107
|
editor: editorConfigSchema.optional(),
|
|
107
108
|
authPlugin: z.custom().optional(),
|
|
108
109
|
entryLinkUrl: z.custom().optional(),
|
package/dist/config/types.d.ts
CHANGED
|
@@ -374,6 +374,8 @@ export interface CanopyConfig {
|
|
|
374
374
|
sourceRoot?: SourceRoot;
|
|
375
375
|
/** Deployment prefix the host app is served under — see {@link CanopyConfigInput.basePath}. */
|
|
376
376
|
basePath?: string;
|
|
377
|
+
/** HTTP status of an unauthenticated API response — see {@link CanopyConfigInput.unauthenticatedStatus}. */
|
|
378
|
+
unauthenticatedStatus?: 401 | 419;
|
|
377
379
|
editor?: CanopyEditorConfig;
|
|
378
380
|
authPlugin?: AuthPlugin;
|
|
379
381
|
/** Custom URL resolver for entry links — see {@link CanopyConfigInput.entryLinkUrl}. */
|
|
@@ -439,6 +441,19 @@ export interface CanopyConfigInput {
|
|
|
439
441
|
* silently filter out every entry (zero static params, a build that goes green with no pages).
|
|
440
442
|
*/
|
|
441
443
|
basePath?: string;
|
|
444
|
+
/**
|
|
445
|
+
* HTTP status of an API response to a request with no accepted session. Default 401.
|
|
446
|
+
*
|
|
447
|
+
* Set 419 when the site's own pages sit behind HTTP Basic auth on the same origin as the
|
|
448
|
+
* editor. Chrome, holding a Basic credential entered at the site root, sends it with the
|
|
449
|
+
* editor's API requests and drops it when one answers 401; the next gated request, such as a
|
|
450
|
+
* preview image, then waits on a Basic prompt. 419 rather than 403
|
|
451
|
+
* because a CloudFront custom error response cannot match 419, and one mapping 403 to an error
|
|
452
|
+
* page would replace the body below.
|
|
453
|
+
*
|
|
454
|
+
* The body says `status: 401` either way, and that is what the editor detects sign-out from.
|
|
455
|
+
*/
|
|
456
|
+
unauthenticatedStatus?: 401 | 419;
|
|
442
457
|
editor?: CanopyEditorConfig;
|
|
443
458
|
authPlugin?: AuthPlugin;
|
|
444
459
|
/** Custom URL resolver for entry links. Overrides the default URL computation. */
|
package/dist/content-reader.d.ts
CHANGED
|
@@ -84,10 +84,19 @@ export interface ContentReadMeta {
|
|
|
84
84
|
* `{type}.{slug}.{id}.{ext}`) -- see the `entryType` caveat above for what that implies.
|
|
85
85
|
*/
|
|
86
86
|
entryId?: ContentId;
|
|
87
|
+
/** The entry's slug within its collection, by the rule `listEntries` uses. */
|
|
88
|
+
slug: Slug;
|
|
89
|
+
/** The entry's URL path with no query, by the rule `listEntries` uses. */
|
|
90
|
+
urlPath: string;
|
|
87
91
|
}
|
|
88
92
|
export interface ContentReader {
|
|
89
93
|
read: <T = unknown>(input: ReadContentInput, message?: string) => Promise<{
|
|
90
94
|
data: T;
|
|
95
|
+
/**
|
|
96
|
+
* The requested `entryPath` and slug as a percent-encoded URL path, plus `?branch=<name>`.
|
|
97
|
+
* Through an entry-type `entryPath` it is not the entry's URL. Compare entries by
|
|
98
|
+
* `meta.urlPath`.
|
|
99
|
+
*/
|
|
91
100
|
path: string;
|
|
92
101
|
meta: ContentReadMeta;
|
|
93
102
|
}>;
|
package/dist/content-reader.js
CHANGED
|
@@ -2,7 +2,7 @@ import { loadBranchContext, loadOrCreateBranchContext } from './branch-workspace
|
|
|
2
2
|
import { ContentStore, ContentStoreError } from './content-store.js';
|
|
3
3
|
import { namesNoWorkspace, resolveBranchPaths, } from './paths/index.js';
|
|
4
4
|
import { trimSlashes } from './paths/normalize.js';
|
|
5
|
-
import { isIndexSlug } from './utils/entry-url.js';
|
|
5
|
+
import { computeEntryUrl, isIndexSlug } from './utils/entry-url.js';
|
|
6
6
|
import { isDeployedStatic, isBuildMode, readsFromCheckout } from './build-mode.js';
|
|
7
7
|
import { isNotFoundError } from './utils/error.js';
|
|
8
8
|
import { resolveEntryLinksInData } from './entry-link-resolver.js';
|
|
@@ -141,6 +141,8 @@ export const createContentReader = (options) => {
|
|
|
141
141
|
// key by them without a separate listEntries lookup or filename parse.
|
|
142
142
|
let entryType;
|
|
143
143
|
let entryId;
|
|
144
|
+
let urlPath;
|
|
145
|
+
let resolvedSlug;
|
|
144
146
|
try {
|
|
145
147
|
const resolved = await store.resolveDocumentPath(entryPath, slug ?? '');
|
|
146
148
|
logicalPath = resolved.logicalPath;
|
|
@@ -150,6 +152,8 @@ export const createContentReader = (options) => {
|
|
|
150
152
|
// absent, for legacy entry files without an embedded ID (see ContentReadMeta).
|
|
151
153
|
entryType = resolved.entryTypeName;
|
|
152
154
|
entryId = resolved.id;
|
|
155
|
+
resolvedSlug = resolved.slug;
|
|
156
|
+
urlPath = computeEntryUrl(resolved.collectionPath, resolved.slug, contentRoot);
|
|
153
157
|
}
|
|
154
158
|
catch (err) {
|
|
155
159
|
const message = err instanceof ContentStoreError ? err.message : 'Invalid content request';
|
|
@@ -196,7 +200,7 @@ export const createContentReader = (options) => {
|
|
|
196
200
|
resolveReferences: input.resolveReferences ?? true,
|
|
197
201
|
referenceAccess,
|
|
198
202
|
});
|
|
199
|
-
return { doc, store, physicalPath, entryType, entryId };
|
|
203
|
+
return { doc, store, physicalPath, entryType, entryId, slug: resolvedSlug, urlPath };
|
|
200
204
|
}
|
|
201
205
|
catch (err) {
|
|
202
206
|
if (isNotFoundError(err))
|
|
@@ -211,7 +215,7 @@ export const createContentReader = (options) => {
|
|
|
211
215
|
const defaultMessage = `Content not found for ${entryPath}${slug ? `/${slug}` : ''} on branch ${branchName}`;
|
|
212
216
|
throw new ContentStoreError(message ?? defaultMessage, 'NOT_FOUND');
|
|
213
217
|
}
|
|
214
|
-
const { doc, store, physicalPath, entryType, entryId } = result;
|
|
218
|
+
const { doc, store, physicalPath, entryType, entryId, slug: entrySlug, urlPath } = result;
|
|
215
219
|
// For md/mdx format, merge the body into the data so callers get a complete object.
|
|
216
220
|
// The field name comes from the schema's isBody flag (defaults to 'body').
|
|
217
221
|
const docRecord = doc;
|
|
@@ -228,7 +232,7 @@ export const createContentReader = (options) => {
|
|
|
228
232
|
slug,
|
|
229
233
|
branch: branchName,
|
|
230
234
|
});
|
|
231
|
-
return { data, path, meta: { physicalPath, entryType, entryId } };
|
|
235
|
+
return { data, path, meta: { physicalPath, entryType, entryId, slug: entrySlug, urlPath } };
|
|
232
236
|
};
|
|
233
237
|
return { read };
|
|
234
238
|
};
|
package/dist/content-store.d.ts
CHANGED
|
@@ -365,6 +365,10 @@ export declare class ContentStore {
|
|
|
365
365
|
relativePath: PhysicalPath;
|
|
366
366
|
/** The entry's logical path, the form path-permission rules match (`entryLogicalPath`). */
|
|
367
367
|
logicalPath: LogicalPath;
|
|
368
|
+
/** The collection the entry file lives in: the parent, when an entry-type item delegated. */
|
|
369
|
+
collectionPath: LogicalPath;
|
|
370
|
+
/** The entry's slug within that collection, lowercased as listings report it. */
|
|
371
|
+
slug: Slug;
|
|
368
372
|
id?: string;
|
|
369
373
|
/**
|
|
370
374
|
* Always populated for a valid schema item: the collection branch below resolves a name
|
package/dist/content-store.js
CHANGED
|
@@ -604,6 +604,8 @@ export class ContentStore {
|
|
|
604
604
|
absolutePath: resolved,
|
|
605
605
|
relativePath: path.relative(this.root, resolved),
|
|
606
606
|
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
607
|
+
collectionPath: schemaItem.logicalPath,
|
|
608
|
+
slug: safeSlug,
|
|
607
609
|
id,
|
|
608
610
|
entryTypeName: finalEntryTypeName,
|
|
609
611
|
existed,
|
|
@@ -24,7 +24,8 @@ export interface ApiClientProviderProps {
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function ApiClientProvider({ children, client, basePath }: ApiClientProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
26
26
|
/**
|
|
27
|
-
* Subscribe to every
|
|
27
|
+
* Subscribe to every unauthenticated response (`ApiClientOptions.onUnauthorized`) the provider's
|
|
28
|
+
* client receives, from any request. `listener` should be
|
|
28
29
|
* stable (e.g. from `useCallback`): a new function re-subscribes. No-op outside a provider.
|
|
29
30
|
*/
|
|
30
31
|
export declare function useOnUnauthorized(listener: () => void): void;
|
|
@@ -48,7 +48,8 @@ export function ApiClientProvider({ children, client, basePath }) {
|
|
|
48
48
|
return (_jsx(UnauthorizedSignalContext.Provider, { value: unauthorized, children: _jsx(ApiClientContext.Provider, { value: apiClient, children: children }) }));
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Subscribe to every
|
|
51
|
+
* Subscribe to every unauthenticated response (`ApiClientOptions.onUnauthorized`) the provider's
|
|
52
|
+
* client receives, from any request. `listener` should be
|
|
52
53
|
* stable (e.g. from `useCallback`): a new function re-subscribes. No-op outside a provider.
|
|
53
54
|
*/
|
|
54
55
|
export function useOnUnauthorized(listener) {
|
|
@@ -4,15 +4,19 @@ import React, { Suspense, useId, useRef, useCallback, useEffect, useState } from
|
|
|
4
4
|
import { Alert, Button, Group, Text, Textarea } from '@mantine/core';
|
|
5
5
|
import { InsertEntryLink } from './entry-link/index.js';
|
|
6
6
|
import { MdxImageDialog } from './MdxImageDialog.js';
|
|
7
|
+
import { createMdxJsxPlugins } from './mdx-jsx-support.js';
|
|
7
8
|
import { useApiClient, useAssetContext } from '../context/index.js';
|
|
8
9
|
import { editorImageSrc } from '../media/editor-image-src.js';
|
|
9
10
|
import { uploadAsset } from '../media/upload-asset.js';
|
|
10
11
|
import { FieldDescription, groupDescriptionProps } from './FieldDescription.js';
|
|
11
12
|
const MDXEditorLazy = React.lazy(async () => {
|
|
12
|
-
|
|
13
|
+
// MDXEditor's only code import (theme.tsx loads its CSS); see mdx-jsx-support for why.
|
|
14
|
+
const mdx = await import('@mdxeditor/editor');
|
|
15
|
+
const { MDXEditor, headingsPlugin, listsPlugin, quotePlugin, thematicBreakPlugin, markdownShortcutPlugin, linkPlugin, linkDialogPlugin, imagePlugin, tablePlugin, toolbarPlugin, codeBlockPlugin, codeMirrorPlugin, BoldItalicUnderlineToggles, BlockTypeSelect, ListsToggle, CreateLink, InsertImage, InsertTable, InsertThematicBreak, CodeToggle, InsertCodeBlock, UndoRedo, Separator, insertMarkdown$, usePublisher, useCellValues, saveImage$, closeImageDialog$, imageDialogState$, activeEditor$, $isImageNode,
|
|
13
16
|
// MDXEditor's own lexical instance: lexical keeps the active editor state per module, so
|
|
14
17
|
// a separately resolved copy would throw inside this editor's `read()`.
|
|
15
|
-
lexical: { $getNodeByKey }, }
|
|
18
|
+
lexical: { $getNodeByKey }, } = mdx;
|
|
19
|
+
const mdxJsxPlugins = createMdxJsxPlugins(mdx);
|
|
16
20
|
const EntryLinkToolbarButton = ({ onInsert }) => {
|
|
17
21
|
const insertMarkdown = usePublisher(insertMarkdown$);
|
|
18
22
|
return (_jsx(InsertEntryLink, { onInsert: (markdown) => onInsert(() => insertMarkdown(markdown), markdown) }));
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type * as MdxEditor from '@mdxeditor/editor';
|
|
2
|
+
type MdxEditorModule = typeof MdxEditor;
|
|
3
|
+
/** Creates the JSX plugins from MDXEditor's exports; call once per load of MDXEditor. */
|
|
4
|
+
export declare function createMdxJsxPlugins(mdx: MdxEditorModule): () => MdxEditor.RealmPlugin[];
|
|
5
|
+
export {};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { NestedLexicalEditor, UnrecognizedMarkdownConstructError, addImportVisitor$, importVisitors$, isMdastHTMLNode, isMdastJsxNode, jsxPlugin, realmPlugin, } from '@mdxeditor/editor';
|
|
4
3
|
function childrenOf(node) {
|
|
5
4
|
return 'children' in node && Array.isArray(node.children) ? node.children : [];
|
|
6
5
|
}
|
|
@@ -13,26 +12,6 @@ function describeAttribute(attribute) {
|
|
|
13
12
|
return `${attribute.name}="${attribute.value}"`;
|
|
14
13
|
return `${attribute.name}={…}`;
|
|
15
14
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Shows an element's tag and attributes read-only (they are edited as source)
|
|
18
|
-
* and edits its children, inline or block per the parsed node: one `*`
|
|
19
|
-
* descriptor serves both, and a block editor rejects inline children.
|
|
20
|
-
*/
|
|
21
|
-
const CatchAllJsxEditor = ({ mdastNode }) => {
|
|
22
|
-
const inline = mdastNode.type === 'mdxJsxTextElement';
|
|
23
|
-
const tag = [mdastNode.name ?? '', ...mdastNode.attributes.map(describeAttribute)]
|
|
24
|
-
.join(' ')
|
|
25
|
-
.trim();
|
|
26
|
-
const Wrapper = inline ? 'span' : 'div';
|
|
27
|
-
return (_jsxs(Wrapper, { className: inline ? 'canopy-mdx-jsx canopy-mdx-jsx-inline' : 'canopy-mdx-jsx', children: [_jsx("span", { className: "canopy-mdx-jsx-tag", "data-testid": "mdx-jsx-tag", title: tag, children: `<${tag}>` }), mdastNode.children.length > 0 ? (_jsx(NestedLexicalEditor, { block: !inline, getContent: (node) => node.children, getUpdatedMdastNode: (node, children) => ({ ...node, children }) })) : null] }));
|
|
28
|
-
};
|
|
29
|
-
const catchAllJsxDescriptor = {
|
|
30
|
-
name: '*',
|
|
31
|
-
kind: 'flow',
|
|
32
|
-
props: [],
|
|
33
|
-
hasChildren: true,
|
|
34
|
-
Editor: CatchAllJsxEditor,
|
|
35
|
-
};
|
|
36
15
|
const isStringAttribute = (attribute, name) => attribute.type === 'mdxJsxAttribute' &&
|
|
37
16
|
attribute.name === name &&
|
|
38
17
|
typeof attribute.value === 'string';
|
|
@@ -70,68 +49,93 @@ function roundTripsImage(node) {
|
|
|
70
49
|
}
|
|
71
50
|
// Why MDXEditor cannot round-trip this JSX element, or null if it can. A
|
|
72
51
|
// fragment has no name, which MDXEditor's HTML handling throws on.
|
|
73
|
-
function unsupportedJsx(node) {
|
|
74
|
-
if (!isMdastJsxNode(node))
|
|
52
|
+
function unsupportedJsx(mdx, node) {
|
|
53
|
+
if (!mdx.isMdastJsxNode(node))
|
|
75
54
|
return null;
|
|
76
55
|
if (node.name === null)
|
|
77
56
|
return 'fragments (<>…</>)';
|
|
78
57
|
// `img` is the image plugin's, not in MDXEditor's HTML tag list.
|
|
79
58
|
if (node.name === 'img')
|
|
80
59
|
return roundTripsImage(node) ? null : 'this <img>';
|
|
81
|
-
if (isMdastHTMLNode(node) && breaksSpanCollapse(node)) {
|
|
60
|
+
if (mdx.isMdastHTMLNode(node) && breaksSpanCollapse(node)) {
|
|
82
61
|
return `<${node.name}> wrapping a <span>, with an {expression} class or style`;
|
|
83
62
|
}
|
|
84
63
|
return null;
|
|
85
64
|
}
|
|
86
65
|
/** Typed `string` because the mdast node union this package resolves omits the ESM node. */
|
|
87
66
|
const ESM_NODE_TYPE = 'mdxjsEsm';
|
|
88
|
-
/** Node types
|
|
67
|
+
/** Node types inside a table, imported by its cell editors rather than by visitors. */
|
|
89
68
|
const CONSUMED_BY_PARENT_VISITOR = new Set(['tableRow', 'tableCell']);
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
69
|
+
/** Creates the JSX plugins from MDXEditor's exports; call once per load of MDXEditor. */
|
|
70
|
+
export function createMdxJsxPlugins(mdx) {
|
|
71
|
+
const { NestedLexicalEditor, UnrecognizedMarkdownConstructError, addImportVisitor$, importVisitors$, isMdastJsxNode, jsxPlugin, realmPlugin, } = mdx;
|
|
72
|
+
/**
|
|
73
|
+
* Shows an element's tag and attributes read-only (they are edited as source)
|
|
74
|
+
* and edits its children, inline or block per the parsed node: one `*`
|
|
75
|
+
* descriptor serves both, and a block editor rejects inline children.
|
|
76
|
+
*/
|
|
77
|
+
const CatchAllJsxEditor = ({ mdastNode }) => {
|
|
78
|
+
const inline = mdastNode.type === 'mdxJsxTextElement';
|
|
79
|
+
const tag = [mdastNode.name ?? '', ...mdastNode.attributes.map(describeAttribute)]
|
|
80
|
+
.join(' ')
|
|
81
|
+
.trim();
|
|
82
|
+
const Wrapper = inline ? 'span' : 'div';
|
|
83
|
+
return (_jsxs(Wrapper, { className: inline ? 'canopy-mdx-jsx canopy-mdx-jsx-inline' : 'canopy-mdx-jsx', children: [_jsx("span", { className: "canopy-mdx-jsx-tag", "data-testid": "mdx-jsx-tag", title: tag, children: `<${tag}>` }), mdastNode.children.length > 0 ? (_jsx(NestedLexicalEditor, { block: !inline, getContent: (node) => node.children, getUpdatedMdastNode: (node, children) => ({ ...node, children }) })) : null] }));
|
|
84
|
+
};
|
|
85
|
+
const catchAllJsxDescriptor = {
|
|
86
|
+
name: '*',
|
|
87
|
+
kind: 'flow',
|
|
88
|
+
props: [],
|
|
89
|
+
hasChildren: true,
|
|
90
|
+
Editor: CatchAllJsxEditor,
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Reports through `onError`, at import, what MDXEditor would otherwise lose or
|
|
94
|
+
* break on without reporting it: `import`/`export` lines (its visitor for them
|
|
95
|
+
* is a no-op), elements `unsupportedJsx` rejects, and content with no visitor
|
|
96
|
+
* inside a JSX element or table, whose children nested editors import later,
|
|
97
|
+
* only logging a failure and writing partial children back on edit. It throws
|
|
98
|
+
* an error class MDXEditor's import reports; any other error crashes the editor.
|
|
99
|
+
*/
|
|
100
|
+
const roundTripGuardPlugin = realmPlugin({
|
|
101
|
+
init(realm) {
|
|
102
|
+
const guard = {
|
|
103
|
+
priority: 100,
|
|
104
|
+
testNode: (node) => node.type === ESM_NODE_TYPE || node.type === 'table' || isMdastJsxNode(node),
|
|
105
|
+
visitNode({ mdastNode, descriptors, actions }) {
|
|
106
|
+
if (mdastNode.type === ESM_NODE_TYPE) {
|
|
107
|
+
throw new UnrecognizedMarkdownConstructError('import/export statements cannot be edited in the rich-text editor');
|
|
111
108
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
? visitor.testNode === node.type
|
|
117
|
-
: visitor.testNode(node, descriptors));
|
|
118
|
-
const check = (node) => {
|
|
119
|
-
for (const child of childrenOf(node)) {
|
|
120
|
-
reject(child);
|
|
121
|
-
if (!CONSUMED_BY_PARENT_VISITOR.has(child.type) && !hasVisitor(child)) {
|
|
122
|
-
const where = isMdastJsxNode(mdastNode) ? `<${mdastNode.name ?? ''}>` : 'A table';
|
|
123
|
-
throw new UnrecognizedMarkdownConstructError(`${where} contains ${child.type} content the rich-text editor cannot edit`);
|
|
109
|
+
const reject = (node) => {
|
|
110
|
+
const reason = unsupportedJsx(mdx, node);
|
|
111
|
+
if (reason !== null) {
|
|
112
|
+
throw new UnrecognizedMarkdownConstructError(`${reason} cannot be edited in the rich-text editor`);
|
|
124
113
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
114
|
+
};
|
|
115
|
+
reject(mdastNode);
|
|
116
|
+
const visitors = realm.getValue(importVisitors$).filter((visitor) => visitor !== guard);
|
|
117
|
+
const hasVisitor = (node) => visitors.some((visitor) => typeof visitor.testNode === 'string'
|
|
118
|
+
? visitor.testNode === node.type
|
|
119
|
+
: visitor.testNode(node, descriptors));
|
|
120
|
+
const check = (node) => {
|
|
121
|
+
for (const child of childrenOf(node)) {
|
|
122
|
+
reject(child);
|
|
123
|
+
if (!CONSUMED_BY_PARENT_VISITOR.has(child.type) && !hasVisitor(child)) {
|
|
124
|
+
const where = isMdastJsxNode(mdastNode) ? `<${mdastNode.name ?? ''}>` : 'A table';
|
|
125
|
+
throw new UnrecognizedMarkdownConstructError(`${where} contains ${child.type} content the rich-text editor cannot edit`);
|
|
126
|
+
}
|
|
127
|
+
check(child);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
check(mdastNode);
|
|
131
|
+
actions.nextVisitor();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
realm.pub(addImportVisitor$, guard);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
return () => [
|
|
138
|
+
jsxPlugin({ jsxComponentDescriptors: [catchAllJsxDescriptor] }),
|
|
139
|
+
roundTripGuardPlugin(),
|
|
140
|
+
];
|
|
137
141
|
}
|
package/dist/http/handler.js
CHANGED
|
@@ -86,6 +86,11 @@ const parseQueryParams = (url) => {
|
|
|
86
86
|
return {};
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* The answer to a request with no accepted session. The body's `status: 401` is what the client
|
|
91
|
+
* detects sign-out from, so it stays 401 whatever HTTP status `unauthenticatedStatus` picks.
|
|
92
|
+
*/
|
|
93
|
+
const unauthenticatedResponse = (config, error) => jsonResponse({ ok: false, status: 401, error: error ?? 'Unauthorized' }, config.unauthenticatedStatus ?? 401);
|
|
89
94
|
/**
|
|
90
95
|
* The core handler behind every Canopy API request. A framework adapter
|
|
91
96
|
* (Next.js, Express, Hono) converts its request to a CanopyRequest, extracts
|
|
@@ -129,7 +134,7 @@ export function createCanopyRequestHandler(options) {
|
|
|
129
134
|
// any workspace provisioning below, so they can neither trigger expensive
|
|
130
135
|
// git operations nor read provisioning error details.
|
|
131
136
|
if (!authResult.success || !authResult.user) {
|
|
132
|
-
return
|
|
137
|
+
return unauthenticatedResponse(apiCtx.services.config, authResult.error);
|
|
133
138
|
}
|
|
134
139
|
// Provision the base/active branch workspace on first request, so the many
|
|
135
140
|
// endpoints that assume it exists (registry reads and the like) don't return
|
|
@@ -222,7 +227,7 @@ export function createCanopyRequestHandler(options) {
|
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
229
|
if (user.type === 'anonymous') {
|
|
225
|
-
return
|
|
230
|
+
return unauthenticatedResponse(apiCtx.services.config, authResult.error);
|
|
226
231
|
}
|
|
227
232
|
const queryParams = parseQueryParams(req.url);
|
|
228
233
|
const mergedParams = { ...queryParams, ...match.params };
|
|
@@ -54,6 +54,13 @@ export interface UrlExclusivityReport {
|
|
|
54
54
|
expectedEntryId?: string;
|
|
55
55
|
actualEntryId?: string;
|
|
56
56
|
}>;
|
|
57
|
+
/** Published URLs whose read reports a `meta.slug` or `meta.urlPath` other than the listing's. */
|
|
58
|
+
misreported: Array<{
|
|
59
|
+
urlPath: string;
|
|
60
|
+
slug: string;
|
|
61
|
+
readSlug: string;
|
|
62
|
+
readUrlPath: string;
|
|
63
|
+
}>;
|
|
57
64
|
/** Every adjacent URL probed, sorted. */
|
|
58
65
|
probes: string[];
|
|
59
66
|
/** Probes that resolved despite not being published -- the phantom URLs. Must be empty. */
|
|
@@ -80,12 +80,21 @@ export const collectUrlExclusivityReport = async (ctx, schema, contentRoot = 'co
|
|
|
80
80
|
const items = await ctx.listEntries();
|
|
81
81
|
const unresolved = [];
|
|
82
82
|
const mismatched = [];
|
|
83
|
+
const misreported = [];
|
|
83
84
|
for (const item of items) {
|
|
84
85
|
const result = await ctx.readByUrlPath(item.urlPath);
|
|
85
86
|
if (!result) {
|
|
86
87
|
unresolved.push(item.urlPath);
|
|
87
88
|
continue;
|
|
88
89
|
}
|
|
90
|
+
if (result.meta.slug !== item.slug || result.meta.urlPath !== item.urlPath) {
|
|
91
|
+
misreported.push({
|
|
92
|
+
urlPath: item.urlPath,
|
|
93
|
+
slug: item.slug,
|
|
94
|
+
readSlug: result.meta.slug,
|
|
95
|
+
readUrlPath: result.meta.urlPath,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
89
98
|
if (result.meta.entryId !== item.entryId) {
|
|
90
99
|
mismatched.push({
|
|
91
100
|
urlPath: item.urlPath,
|
|
@@ -108,6 +117,7 @@ export const collectUrlExclusivityReport = async (ctx, schema, contentRoot = 'co
|
|
|
108
117
|
duplicates: findDuplicateUrlPaths(items),
|
|
109
118
|
unresolved: unresolved.sort(),
|
|
110
119
|
mismatched,
|
|
120
|
+
misreported,
|
|
111
121
|
probes: [...probes].sort(),
|
|
112
122
|
phantoms: phantoms.sort(),
|
|
113
123
|
};
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CANOPYCMS_VERSION = "0.0.68-int.
|
|
1
|
+
export declare const CANOPYCMS_VERSION = "0.0.68-int.102";
|
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.0.68-int.
|
|
3
|
+
export const CANOPYCMS_VERSION = '0.0.68-int.102';
|
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.0.68-int.
|
|
4
|
+
"version": "0.0.68-int.102",
|
|
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": {
|