canopycms 0.0.68-int.102 → 0.0.68-int.104

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 (54) hide show
  1. package/README.md +3 -1
  2. package/dist/assets/asset-url.d.ts +6 -0
  3. package/dist/assets/asset-url.js +9 -0
  4. package/dist/build/asset-refs.d.ts +2 -1
  5. package/dist/build/asset-refs.js +25 -9
  6. package/dist/build/inline-flight.d.ts +37 -0
  7. package/dist/build/inline-flight.js +142 -0
  8. package/dist/cli/cli.js +829 -662
  9. package/dist/cli/generate-ai-content.js +403 -96
  10. package/dist/cli/init.js +0 -80
  11. package/dist/content-store.js +1 -1
  12. package/dist/editor/Editor.js +3 -1
  13. package/dist/editor/FormRenderer.js +78 -9
  14. package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
  15. package/dist/editor/components/EditorCrashScreen.js +22 -0
  16. package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
  17. package/dist/editor/components/EditorErrorBoundary.js +40 -0
  18. package/dist/editor/context/AssetContext.d.ts +2 -7
  19. package/dist/editor/context/AssetContext.js +2 -12
  20. package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
  21. package/dist/editor/fields/FieldCrashFallback.js +30 -0
  22. package/dist/editor/fields/ImageField.js +8 -5
  23. package/dist/editor/fields/MarkdownField.d.ts +21 -0
  24. package/dist/editor/fields/MarkdownField.js +45 -17
  25. package/dist/editor/fields/markdown-export-options.d.ts +2 -0
  26. package/dist/editor/fields/markdown-export-options.js +8 -0
  27. package/dist/editor/fields/mdx-jsx-support.js +52 -6
  28. package/dist/editor/fields/rich-text-failures.d.ts +5 -0
  29. package/dist/editor/fields/rich-text-failures.js +26 -0
  30. package/dist/editor/hooks/useEntryManager.js +19 -5
  31. package/dist/editor/media/AssetCard.d.ts +2 -5
  32. package/dist/editor/media/AssetCard.js +4 -7
  33. package/dist/editor/media/editor-image-src.d.ts +6 -5
  34. package/dist/editor/media/editor-image-src.js +7 -9
  35. package/dist/editor/preview-asset-base.d.ts +5 -4
  36. package/dist/editor/preview-asset-base.js +9 -5
  37. package/dist/editor/preview-bridge.d.ts +8 -0
  38. package/dist/editor/preview-bridge.js +22 -6
  39. package/dist/editor/raw-asset-base.d.ts +5 -0
  40. package/dist/editor/raw-asset-base.js +14 -0
  41. package/dist/editor/utils/editor-errors.d.ts +19 -0
  42. package/dist/editor/utils/editor-errors.js +32 -0
  43. package/dist/preview.d.ts +1 -1
  44. package/dist/preview.js +1 -1
  45. package/dist/server.d.ts +9 -0
  46. package/dist/server.js +9 -0
  47. package/dist/utils/content-serialize.d.ts +7 -5
  48. package/dist/utils/content-serialize.js +58 -9
  49. package/dist/utils/error.js +6 -2
  50. package/dist/utils/markdown-body-splice.d.ts +39 -0
  51. package/dist/utils/markdown-body-splice.js +421 -0
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/package.json +15 -1
package/README.md CHANGED
@@ -637,7 +637,9 @@ media: { adapter: 's3', bucket: 'my-site-assets', region: 'us-east-1' }
637
637
 
638
638
  Passing an `image` field value applies its stored crop (`opts.crop` overrides it). Its `width`
639
639
  and `height` are the uncropped original's, so scale them by `crop.w` and `crop.h` for a cropped
640
- value. SVGs and PDFs are served statically, with no transform.
640
+ value. SVGs and PDFs are served statically, with no transform. The package declares
641
+ `sideEffects`, so this import adds under 7 KB minified to a client bundle and none of the
642
+ config schemas.
641
643
 
642
644
  - **Where `/assets` is mounted** — the URLs above are root-relative, because that is what gets
643
645
  stored in content and content moves between branches and environments. If your renderer sees
@@ -41,6 +41,12 @@ export interface AssetUrlOptions {
41
41
  */
42
42
  baseUrl?: string;
43
43
  }
44
+ /**
45
+ * Whether `src` is in canopycms's own `/assets` URL space - a transform or static asset src the
46
+ * asset store serves. Every other src (a site's own static file, an off-site URL, a `data:` URI)
47
+ * is the host's, and the asset store never serves it.
48
+ */
49
+ export declare function isAssetStoreSrc(src: string): boolean;
44
50
  /**
45
51
  * Build a transform URL, merging `opts` and `ref.crop` over the directives already present
46
52
  * in `ref.src` (see `mergeDirectives` for precedence). For static srcs (svg/pdf under
@@ -9,6 +9,15 @@ import { getPreviewAssetBase } from '../editor/preview-asset-base.js';
9
9
  import { ASSET_PREFIXES } from './asset-prefixes.js';
10
10
  import { formatDirectives, isAllowedTransformWidth, parseTransformPath, } from './transform-directives.js';
11
11
  const TRANSFORM_URL_PREFIX = `/${ASSET_PREFIXES.transform}/`;
12
+ const ASSET_SPACE_PREFIX = `/${ASSET_PREFIXES.public}/`;
13
+ /**
14
+ * Whether `src` is in canopycms's own `/assets` URL space - a transform or static asset src the
15
+ * asset store serves. Every other src (a site's own static file, an off-site URL, a `data:` URI)
16
+ * is the host's, and the asset store never serves it.
17
+ */
18
+ export function isAssetStoreSrc(src) {
19
+ return src.startsWith(ASSET_SPACE_PREFIX);
20
+ }
12
21
  /**
13
22
  * Test-only. A function stored on `globalThis` under this symbol is called with every URL
14
23
  * `assetUrl` returns; the dual-build fixture installs one during a static export to prove
@@ -63,9 +63,10 @@ declare const assetRefsFileSchema: z.ZodObject<{
63
63
  }[];
64
64
  }>;
65
65
  export type AssetRefsFile = z.infer<typeof assetRefsFileSchema>;
66
- /** A URL in the output that no stored object can answer. */
66
+ /** A URL in the output that no stored object can answer, or an inline flight script that cannot be read. */
67
67
  export interface AssetRefProblem {
68
68
  file: string;
69
+ /** The URL, or the script's position when the problem is a script. */
69
70
  url: string;
70
71
  error: string;
71
72
  }
@@ -12,6 +12,7 @@ import { z } from 'zod';
12
12
  import { ASSET_PREFIXES } from '../assets/asset-prefixes.js';
13
13
  import { canonicalizeTransformPath } from '../assets/transform-directives.js';
14
14
  import { atomicWriteFile } from '../utils/atomic-write.js';
15
+ import { extractInlineFlight } from './inline-flight.js';
15
16
  /** @internal Written into the scanned directory, so a manifest the adopter builds afterwards covers it. */
16
17
  export const ASSET_REFS_FILENAME = 'canopy-asset-refs.json';
17
18
  /**
@@ -84,11 +85,12 @@ async function listScannedFiles(root) {
84
85
  .filter((rel) => rel !== ASSET_REFS_FILENAME)
85
86
  .sort();
86
87
  }
88
+ const HTML_FILE_RE = /\.html?$/;
87
89
  /** The route a static-export HTML file serves (`about/index.html` and `about.html` are `/about`). */
88
90
  function routeForFile(file) {
89
- if (!/\.html?$/.test(file))
91
+ if (!HTML_FILE_RE.test(file))
90
92
  return undefined;
91
- const withoutExt = file.replace(/\.html?$/, '');
93
+ const withoutExt = file.replace(HTML_FILE_RE, '');
92
94
  const route = withoutExt === 'index' ? '' : withoutExt.replace(/\/index$/, '');
93
95
  return `/${route}`;
94
96
  }
@@ -191,13 +193,8 @@ export async function collectAssetRefs(outDir) {
191
193
  const transforms = new RefCollector();
192
194
  const statics = new RefCollector();
193
195
  const problems = [];
194
- let scannedFiles = 0;
195
- for (const file of files) {
196
- const filePath = path.join(root, file);
197
- if (path.extname(file) === '' && (await startsBinary(filePath)))
198
- continue;
199
- scannedFiles++;
200
- const text = decodeUrlEscapes(await fs.readFile(filePath, 'utf-8'));
196
+ const scan = (file, raw) => {
197
+ const text = decodeUrlEscapes(raw);
201
198
  for (const match of text.matchAll(TRANSFORM_URL_RE)) {
202
199
  const [url, filename] = trimTrailingPunctuation(match[0], match[3]);
203
200
  const [, encodedDirectives, hash32] = match;
@@ -228,6 +225,25 @@ export async function collectAssetRefs(outDir) {
228
225
  statics.add(`${ASSET_PREFIXES.public}/${hash32}/${filename}`, file);
229
226
  }
230
227
  }
228
+ };
229
+ let scannedFiles = 0;
230
+ for (const file of files) {
231
+ const filePath = path.join(root, file);
232
+ if (path.extname(file) === '' && (await startsBinary(filePath)))
233
+ continue;
234
+ scannedFiles++;
235
+ const text = await fs.readFile(filePath, 'utf-8');
236
+ if (!HTML_FILE_RE.test(file)) {
237
+ scan(file, text);
238
+ continue;
239
+ }
240
+ // A page's inline RSC payload is split across scripts at arbitrary bytes (inline-flight.ts).
241
+ const page = extractInlineFlight(text);
242
+ scan(file, page.markup);
243
+ for (const flight of page.texts)
244
+ scan(file, flight);
245
+ for (const { script, error } of page.problems)
246
+ problems.push({ file, url: script, error });
231
247
  }
232
248
  if (problems.length > 0)
233
249
  throw new AssetRefsError(problems);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Reassemble the RSC (flight) stream Next's App Router inlines into a page, so a URL the stream
3
+ * splits across two scripts is scanned whole.
4
+ *
5
+ * Next writes each chunk of the stream as its own `<script>`, and React Flight cuts chunks at
6
+ * arbitrary bytes (it fills fixed-size views and enqueues the partial one at every flush), so a URL
7
+ * can straddle two scripts and the page text holds only its fragments. The format is the same in Next 15.5.21 and 16.1.7
8
+ * (`createInlinedDataReadableStream` in `dist/server/app-render/use-flight-response.js`, read back
9
+ * by `nextServerDataCallback` in `dist/client/app-index.js`):
10
+ *
11
+ * - `(self.__next_f=self.__next_f||[]).push([0])` starts the stream, followed in the same script by
12
+ * `;self.__next_f.push([2,formState])` when there is form state;
13
+ * - each chunk is then `self.__next_f.push([1,text])`, or `[3,base64]` for a chunk that is not
14
+ * valid UTF-8, whose bytes join the same stream;
15
+ * - every argument is `JSON.stringify` output passed through `htmlEscapeJsonString`, which only
16
+ * rewrites `& < > U+2028 U+2029` as `\uXXXX` escapes, so it parses as JSON and never holds
17
+ * `</script`.
18
+ *
19
+ * Every push on a page joins one stream: Next inlines one flight stream per page.
20
+ */
21
+ export interface InlineFlight {
22
+ /** The page with each parsed flight script replaced by a space, so no fragment is scanned. */
23
+ markup: string;
24
+ /** The stream's text, joined in document order, then each form state as JSON. */
25
+ texts: string[];
26
+ /** Scripts that name `__next_f` but are not flight pushes; their data cannot be scanned. */
27
+ problems: {
28
+ script: string;
29
+ error: string;
30
+ }[];
31
+ }
32
+ /**
33
+ * Split `html` into its markup and its inline flight stream. Only a script whose body names
34
+ * `__next_f` is flight; one that does but does not parse stays in the markup and is reported,
35
+ * because skipping it could drop a reference.
36
+ */
37
+ export declare function extractInlineFlight(html: string): InlineFlight;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Reassemble the RSC (flight) stream Next's App Router inlines into a page, so a URL the stream
3
+ * splits across two scripts is scanned whole.
4
+ *
5
+ * Next writes each chunk of the stream as its own `<script>`, and React Flight cuts chunks at
6
+ * arbitrary bytes (it fills fixed-size views and enqueues the partial one at every flush), so a URL
7
+ * can straddle two scripts and the page text holds only its fragments. The format is the same in Next 15.5.21 and 16.1.7
8
+ * (`createInlinedDataReadableStream` in `dist/server/app-render/use-flight-response.js`, read back
9
+ * by `nextServerDataCallback` in `dist/client/app-index.js`):
10
+ *
11
+ * - `(self.__next_f=self.__next_f||[]).push([0])` starts the stream, followed in the same script by
12
+ * `;self.__next_f.push([2,formState])` when there is form state;
13
+ * - each chunk is then `self.__next_f.push([1,text])`, or `[3,base64]` for a chunk that is not
14
+ * valid UTF-8, whose bytes join the same stream;
15
+ * - every argument is `JSON.stringify` output passed through `htmlEscapeJsonString`, which only
16
+ * rewrites `& < > U+2028 U+2029` as `\uXXXX` escapes, so it parses as JSON and never holds
17
+ * `</script`.
18
+ *
19
+ * Every push on a page joins one stream: Next inlines one flight stream per page.
20
+ */
21
+ import { getErrorMessage } from '../utils/error.js';
22
+ const PUSH_CALL_RE = /(?:\(self\.__next_f=self\.__next_f\|\|\[\]\)|self\.__next_f)\.push\(/y;
23
+ const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
24
+ /** The index just past the JSON array that opens at `start`; `JSON.parse` checks the rest. */
25
+ function jsonArrayEnd(body, start) {
26
+ if (body[start] !== '[')
27
+ throw new Error(`expected a JSON array at ${start}`);
28
+ let depth = 0;
29
+ let inString = false;
30
+ for (let i = start; i < body.length; i++) {
31
+ const c = body[i];
32
+ if (inString) {
33
+ if (c === '\\')
34
+ i++;
35
+ else if (c === '"')
36
+ inString = false;
37
+ }
38
+ else if (c === '"') {
39
+ inString = true;
40
+ }
41
+ else if (c === '[' || c === '{') {
42
+ depth++;
43
+ }
44
+ else if (c === ']' || c === '}') {
45
+ depth--;
46
+ if (depth === 0)
47
+ return i + 1;
48
+ }
49
+ }
50
+ throw new Error('unterminated JSON array');
51
+ }
52
+ /** The arguments of a script body that is nothing but `;`-joined `self.__next_f.push(...)` calls. */
53
+ function parsePushArguments(body) {
54
+ const args = [];
55
+ let pos = 0;
56
+ for (;;) {
57
+ PUSH_CALL_RE.lastIndex = pos;
58
+ if (!PUSH_CALL_RE.test(body))
59
+ throw new Error(`expected self.__next_f.push( at ${pos}`);
60
+ const end = jsonArrayEnd(body, PUSH_CALL_RE.lastIndex);
61
+ args.push(JSON.parse(body.slice(PUSH_CALL_RE.lastIndex, end)));
62
+ if (body[end] !== ')')
63
+ throw new Error(`expected ) at ${end}`);
64
+ pos = end + 1;
65
+ if (pos === body.length)
66
+ return args;
67
+ if (body[pos] !== ';')
68
+ throw new Error(`expected ; or the end of the script at ${pos}`);
69
+ pos++;
70
+ }
71
+ }
72
+ /** A push's contribution: bytes of the stream, form state as JSON, or nothing (the bootstrap). */
73
+ function readSegment(arg) {
74
+ if (!Array.isArray(arg))
75
+ throw new Error('argument is not an array');
76
+ const [kind, payload] = arg;
77
+ if (kind === 0 && arg.length === 1)
78
+ return {};
79
+ if (kind === 1 && arg.length === 2 && typeof payload === 'string') {
80
+ return { bytes: Buffer.from(payload, 'utf8') };
81
+ }
82
+ if (kind === 2 && arg.length === 2)
83
+ return { formState: JSON.stringify(payload) };
84
+ if (kind === 3 && arg.length === 2 && typeof payload === 'string' && BASE64_RE.test(payload)) {
85
+ return { bytes: Buffer.from(payload, 'base64') };
86
+ }
87
+ throw new Error(`unknown segment ${JSON.stringify(arg).slice(0, 40)}`);
88
+ }
89
+ /** Each script element's span and body, in document order; one pass, however the page ends. */
90
+ function* scriptElements(html) {
91
+ const openRe = /<script\b[^<>]*>/gi;
92
+ const closeRe = /<\/script\s*>/gi;
93
+ for (let open = openRe.exec(html); open; open = openRe.exec(html)) {
94
+ closeRe.lastIndex = openRe.lastIndex;
95
+ const close = closeRe.exec(html);
96
+ if (!close)
97
+ return;
98
+ yield {
99
+ start: open.index,
100
+ end: closeRe.lastIndex,
101
+ body: html.slice(openRe.lastIndex, close.index),
102
+ };
103
+ openRe.lastIndex = closeRe.lastIndex;
104
+ }
105
+ }
106
+ /**
107
+ * Split `html` into its markup and its inline flight stream. Only a script whose body names
108
+ * `__next_f` is flight; one that does but does not parse stays in the markup and is reported,
109
+ * because skipping it could drop a reference.
110
+ */
111
+ export function extractInlineFlight(html) {
112
+ const stream = [];
113
+ const formStates = [];
114
+ const problems = [];
115
+ let markup = '';
116
+ let last = 0;
117
+ for (const { start, end, body } of scriptElements(html)) {
118
+ if (!body.includes('__next_f'))
119
+ continue;
120
+ let segments;
121
+ try {
122
+ segments = parsePushArguments(body).map(readSegment);
123
+ }
124
+ catch (err) {
125
+ problems.push({
126
+ script: `<script> at offset ${start}`,
127
+ error: `Not a Next inline flight script (${getErrorMessage(err)}); its RSC payload cannot be scanned`,
128
+ });
129
+ continue;
130
+ }
131
+ for (const { bytes, formState } of segments) {
132
+ if (bytes)
133
+ stream.push(bytes);
134
+ if (formState !== undefined)
135
+ formStates.push(formState);
136
+ }
137
+ markup += `${html.slice(last, start)} `;
138
+ last = end;
139
+ }
140
+ const texts = stream.length > 0 ? [Buffer.concat(stream).toString('utf8'), ...formStates] : formStates;
141
+ return { markup: markup + html.slice(last), texts, problems };
142
+ }