canopycms 0.0.68-int.103 → 0.0.68-int.105
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/build/asset-refs.d.ts +2 -1
- package/dist/build/asset-refs.js +25 -9
- package/dist/build/inline-flight.d.ts +37 -0
- package/dist/build/inline-flight.js +142 -0
- package/dist/cli/cli.js +142 -10
- package/dist/editor/fields/mdx-jsx-support.js +91 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
}
|
package/dist/build/asset-refs.js
CHANGED
|
@@ -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 (
|
|
91
|
+
if (!HTML_FILE_RE.test(file))
|
|
90
92
|
return undefined;
|
|
91
|
-
const withoutExt = file.replace(
|
|
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
|
-
|
|
195
|
-
|
|
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
|
+
}
|
package/dist/cli/cli.js
CHANGED
|
@@ -12653,6 +12653,123 @@ var init_atomic_write2 = __esm({
|
|
|
12653
12653
|
}
|
|
12654
12654
|
});
|
|
12655
12655
|
|
|
12656
|
+
// dist/build/inline-flight.js
|
|
12657
|
+
function jsonArrayEnd(body, start) {
|
|
12658
|
+
if (body[start] !== "[")
|
|
12659
|
+
throw new Error(`expected a JSON array at ${start}`);
|
|
12660
|
+
let depth = 0;
|
|
12661
|
+
let inString = false;
|
|
12662
|
+
for (let i = start; i < body.length; i++) {
|
|
12663
|
+
const c = body[i];
|
|
12664
|
+
if (inString) {
|
|
12665
|
+
if (c === "\\")
|
|
12666
|
+
i++;
|
|
12667
|
+
else if (c === '"')
|
|
12668
|
+
inString = false;
|
|
12669
|
+
} else if (c === '"') {
|
|
12670
|
+
inString = true;
|
|
12671
|
+
} else if (c === "[" || c === "{") {
|
|
12672
|
+
depth++;
|
|
12673
|
+
} else if (c === "]" || c === "}") {
|
|
12674
|
+
depth--;
|
|
12675
|
+
if (depth === 0)
|
|
12676
|
+
return i + 1;
|
|
12677
|
+
}
|
|
12678
|
+
}
|
|
12679
|
+
throw new Error("unterminated JSON array");
|
|
12680
|
+
}
|
|
12681
|
+
function parsePushArguments(body) {
|
|
12682
|
+
const args = [];
|
|
12683
|
+
let pos = 0;
|
|
12684
|
+
for (; ; ) {
|
|
12685
|
+
PUSH_CALL_RE.lastIndex = pos;
|
|
12686
|
+
if (!PUSH_CALL_RE.test(body))
|
|
12687
|
+
throw new Error(`expected self.__next_f.push( at ${pos}`);
|
|
12688
|
+
const end = jsonArrayEnd(body, PUSH_CALL_RE.lastIndex);
|
|
12689
|
+
args.push(JSON.parse(body.slice(PUSH_CALL_RE.lastIndex, end)));
|
|
12690
|
+
if (body[end] !== ")")
|
|
12691
|
+
throw new Error(`expected ) at ${end}`);
|
|
12692
|
+
pos = end + 1;
|
|
12693
|
+
if (pos === body.length)
|
|
12694
|
+
return args;
|
|
12695
|
+
if (body[pos] !== ";")
|
|
12696
|
+
throw new Error(`expected ; or the end of the script at ${pos}`);
|
|
12697
|
+
pos++;
|
|
12698
|
+
}
|
|
12699
|
+
}
|
|
12700
|
+
function readSegment(arg) {
|
|
12701
|
+
if (!Array.isArray(arg))
|
|
12702
|
+
throw new Error("argument is not an array");
|
|
12703
|
+
const [kind, payload] = arg;
|
|
12704
|
+
if (kind === 0 && arg.length === 1)
|
|
12705
|
+
return {};
|
|
12706
|
+
if (kind === 1 && arg.length === 2 && typeof payload === "string") {
|
|
12707
|
+
return { bytes: Buffer.from(payload, "utf8") };
|
|
12708
|
+
}
|
|
12709
|
+
if (kind === 2 && arg.length === 2)
|
|
12710
|
+
return { formState: JSON.stringify(payload) };
|
|
12711
|
+
if (kind === 3 && arg.length === 2 && typeof payload === "string" && BASE64_RE.test(payload)) {
|
|
12712
|
+
return { bytes: Buffer.from(payload, "base64") };
|
|
12713
|
+
}
|
|
12714
|
+
throw new Error(`unknown segment ${JSON.stringify(arg).slice(0, 40)}`);
|
|
12715
|
+
}
|
|
12716
|
+
function* scriptElements(html) {
|
|
12717
|
+
const openRe = /<script\b[^<>]*>/gi;
|
|
12718
|
+
const closeRe = /<\/script\s*>/gi;
|
|
12719
|
+
for (let open = openRe.exec(html); open; open = openRe.exec(html)) {
|
|
12720
|
+
closeRe.lastIndex = openRe.lastIndex;
|
|
12721
|
+
const close = closeRe.exec(html);
|
|
12722
|
+
if (!close)
|
|
12723
|
+
return;
|
|
12724
|
+
yield {
|
|
12725
|
+
start: open.index,
|
|
12726
|
+
end: closeRe.lastIndex,
|
|
12727
|
+
body: html.slice(openRe.lastIndex, close.index)
|
|
12728
|
+
};
|
|
12729
|
+
openRe.lastIndex = closeRe.lastIndex;
|
|
12730
|
+
}
|
|
12731
|
+
}
|
|
12732
|
+
function extractInlineFlight(html) {
|
|
12733
|
+
const stream = [];
|
|
12734
|
+
const formStates = [];
|
|
12735
|
+
const problems = [];
|
|
12736
|
+
let markup = "";
|
|
12737
|
+
let last = 0;
|
|
12738
|
+
for (const { start, end, body } of scriptElements(html)) {
|
|
12739
|
+
if (!body.includes("__next_f"))
|
|
12740
|
+
continue;
|
|
12741
|
+
let segments;
|
|
12742
|
+
try {
|
|
12743
|
+
segments = parsePushArguments(body).map(readSegment);
|
|
12744
|
+
} catch (err2) {
|
|
12745
|
+
problems.push({
|
|
12746
|
+
script: `<script> at offset ${start}`,
|
|
12747
|
+
error: `Not a Next inline flight script (${getErrorMessage(err2)}); its RSC payload cannot be scanned`
|
|
12748
|
+
});
|
|
12749
|
+
continue;
|
|
12750
|
+
}
|
|
12751
|
+
for (const { bytes, formState } of segments) {
|
|
12752
|
+
if (bytes)
|
|
12753
|
+
stream.push(bytes);
|
|
12754
|
+
if (formState !== void 0)
|
|
12755
|
+
formStates.push(formState);
|
|
12756
|
+
}
|
|
12757
|
+
markup += `${html.slice(last, start)} `;
|
|
12758
|
+
last = end;
|
|
12759
|
+
}
|
|
12760
|
+
const texts = stream.length > 0 ? [Buffer.concat(stream).toString("utf8"), ...formStates] : formStates;
|
|
12761
|
+
return { markup: markup + html.slice(last), texts, problems };
|
|
12762
|
+
}
|
|
12763
|
+
var PUSH_CALL_RE, BASE64_RE;
|
|
12764
|
+
var init_inline_flight = __esm({
|
|
12765
|
+
"dist/build/inline-flight.js"() {
|
|
12766
|
+
"use strict";
|
|
12767
|
+
init_error();
|
|
12768
|
+
PUSH_CALL_RE = /(?:\(self\.__next_f=self\.__next_f\|\|\[\]\)|self\.__next_f)\.push\(/y;
|
|
12769
|
+
BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
12770
|
+
}
|
|
12771
|
+
});
|
|
12772
|
+
|
|
12656
12773
|
// dist/build/asset-refs.js
|
|
12657
12774
|
import fs24 from "node:fs/promises";
|
|
12658
12775
|
import path26 from "node:path";
|
|
@@ -12662,9 +12779,9 @@ async function listScannedFiles(root) {
|
|
|
12662
12779
|
return entries.filter((entry) => entry.isFile() && SCANNED_EXTENSIONS.has(path26.extname(entry.name))).map((entry) => path26.relative(root, path26.join(entry.parentPath, entry.name))).map((rel) => rel.split(path26.sep).join("/")).filter((rel) => rel !== ASSET_REFS_FILENAME).sort();
|
|
12663
12780
|
}
|
|
12664
12781
|
function routeForFile(file) {
|
|
12665
|
-
if (
|
|
12782
|
+
if (!HTML_FILE_RE.test(file))
|
|
12666
12783
|
return void 0;
|
|
12667
|
-
const withoutExt = file.replace(
|
|
12784
|
+
const withoutExt = file.replace(HTML_FILE_RE, "");
|
|
12668
12785
|
const route = withoutExt === "index" ? "" : withoutExt.replace(/\/index$/, "");
|
|
12669
12786
|
return `/${route}`;
|
|
12670
12787
|
}
|
|
@@ -12718,13 +12835,8 @@ async function collectAssetRefs(outDir) {
|
|
|
12718
12835
|
const transforms = new RefCollector();
|
|
12719
12836
|
const statics = new RefCollector();
|
|
12720
12837
|
const problems = [];
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
const filePath2 = path26.join(root, file);
|
|
12724
|
-
if (path26.extname(file) === "" && await startsBinary(filePath2))
|
|
12725
|
-
continue;
|
|
12726
|
-
scannedFiles++;
|
|
12727
|
-
const text3 = decodeUrlEscapes(await fs24.readFile(filePath2, "utf-8"));
|
|
12838
|
+
const scan = (file, raw) => {
|
|
12839
|
+
const text3 = decodeUrlEscapes(raw);
|
|
12728
12840
|
for (const match of text3.matchAll(TRANSFORM_URL_RE)) {
|
|
12729
12841
|
const [url, filename] = trimTrailingPunctuation(match[0], match[3]);
|
|
12730
12842
|
const [, encodedDirectives, hash32] = match;
|
|
@@ -12751,6 +12863,24 @@ async function collectAssetRefs(outDir) {
|
|
|
12751
12863
|
statics.add(`${ASSET_PREFIXES.public}/${hash32}/${filename}`, file);
|
|
12752
12864
|
}
|
|
12753
12865
|
}
|
|
12866
|
+
};
|
|
12867
|
+
let scannedFiles = 0;
|
|
12868
|
+
for (const file of files) {
|
|
12869
|
+
const filePath2 = path26.join(root, file);
|
|
12870
|
+
if (path26.extname(file) === "" && await startsBinary(filePath2))
|
|
12871
|
+
continue;
|
|
12872
|
+
scannedFiles++;
|
|
12873
|
+
const text3 = await fs24.readFile(filePath2, "utf-8");
|
|
12874
|
+
if (!HTML_FILE_RE.test(file)) {
|
|
12875
|
+
scan(file, text3);
|
|
12876
|
+
continue;
|
|
12877
|
+
}
|
|
12878
|
+
const page = extractInlineFlight(text3);
|
|
12879
|
+
scan(file, page.markup);
|
|
12880
|
+
for (const flight of page.texts)
|
|
12881
|
+
scan(file, flight);
|
|
12882
|
+
for (const { script, error } of page.problems)
|
|
12883
|
+
problems.push({ file, url: script, error });
|
|
12754
12884
|
}
|
|
12755
12885
|
if (problems.length > 0)
|
|
12756
12886
|
throw new AssetRefsError(problems);
|
|
@@ -12772,13 +12902,14 @@ async function readAssetRefsFile(filePath) {
|
|
|
12772
12902
|
}
|
|
12773
12903
|
return parsed.data;
|
|
12774
12904
|
}
|
|
12775
|
-
var ASSET_REFS_FILENAME, SCANNED_EXTENSIONS, URL_STOP, TRANSFORM_URL_RE, STATIC_URL_RE, STATIC_FILENAME_RE, HASH32_RE2, assetRefEntrySchema, assetRefsFileSchema, AssetRefsError, ENCODED_ASSETS, RUN_STOP, RefCollector;
|
|
12905
|
+
var ASSET_REFS_FILENAME, SCANNED_EXTENSIONS, URL_STOP, TRANSFORM_URL_RE, STATIC_URL_RE, STATIC_FILENAME_RE, HASH32_RE2, assetRefEntrySchema, assetRefsFileSchema, AssetRefsError, HTML_FILE_RE, ENCODED_ASSETS, RUN_STOP, RefCollector;
|
|
12776
12906
|
var init_asset_refs = __esm({
|
|
12777
12907
|
"dist/build/asset-refs.js"() {
|
|
12778
12908
|
"use strict";
|
|
12779
12909
|
init_asset_prefixes();
|
|
12780
12910
|
init_transform_directives();
|
|
12781
12911
|
init_atomic_write2();
|
|
12912
|
+
init_inline_flight();
|
|
12782
12913
|
ASSET_REFS_FILENAME = "canopy-asset-refs.json";
|
|
12783
12914
|
SCANNED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
12784
12915
|
"",
|
|
@@ -12820,6 +12951,7 @@ var init_asset_refs = __esm({
|
|
|
12820
12951
|
this.name = "AssetRefsError";
|
|
12821
12952
|
}
|
|
12822
12953
|
};
|
|
12954
|
+
HTML_FILE_RE = /\.html?$/;
|
|
12823
12955
|
ENCODED_ASSETS = /%2[fF]assets%2[fF]/g;
|
|
12824
12956
|
RUN_STOP = /[\s"'`<>&]/;
|
|
12825
12957
|
RefCollector = class {
|
|
@@ -62,13 +62,83 @@ function unsupportedJsx(mdx, node) {
|
|
|
62
62
|
}
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
|
+
// Blocks that run into the block before them in a list item, by that block's type.
|
|
66
|
+
const RUNS_INTO = {
|
|
67
|
+
paragraph: ['thematicBreak'],
|
|
68
|
+
blockquote: ['paragraph', 'blockquote', 'table'],
|
|
69
|
+
table: ['paragraph', 'table'],
|
|
70
|
+
};
|
|
71
|
+
const BLOCK_NAMES = {
|
|
72
|
+
paragraph: 'paragraph',
|
|
73
|
+
blockquote: 'quote',
|
|
74
|
+
table: 'table',
|
|
75
|
+
thematicBreak: 'horizontal rule',
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Why MDXEditor would change what this list item says, or null. Its import moves a nested list into
|
|
79
|
+
* a new item after its own, so what follows the list comes out before it; its export writes an item
|
|
80
|
+
* tight, so `---` underlines a paragraph and a line continues a quote or table (`RUNS_INTO`).
|
|
81
|
+
*/
|
|
82
|
+
function rearrangedBlocks(node) {
|
|
83
|
+
if (node.type !== 'listItem')
|
|
84
|
+
return null;
|
|
85
|
+
for (let i = 1; i < node.children.length; i++) {
|
|
86
|
+
const before = node.children[i - 1].type;
|
|
87
|
+
const after = node.children[i].type;
|
|
88
|
+
if (before === 'list')
|
|
89
|
+
return 'a list item with content after its nested list';
|
|
90
|
+
if (RUNS_INTO[before]?.includes(after)) {
|
|
91
|
+
return `a list item with a ${BLOCK_NAMES[before] ?? before} followed by a ${BLOCK_NAMES[after] ?? after}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const isLexicalParent = (node) => 'getChildren' in node && typeof node.getChildren === 'function';
|
|
97
|
+
const hasBlockChild = (node) => isLexicalParent(node) && node.getChildren().some((child) => !child.isInline());
|
|
98
|
+
// Writes `node`'s children into `target`: a run of inline children as one paragraph, a block as itself.
|
|
99
|
+
function writeBlocks(node, target, actions) {
|
|
100
|
+
let paragraph = null;
|
|
101
|
+
for (const child of isLexicalParent(node) ? node.getChildren() : []) {
|
|
102
|
+
if (!child.isInline()) {
|
|
103
|
+
paragraph = null;
|
|
104
|
+
actions.visit(child, target);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (paragraph === null) {
|
|
108
|
+
const created = { type: 'paragraph', children: [] };
|
|
109
|
+
actions.appendToParent(target, created);
|
|
110
|
+
paragraph = created;
|
|
111
|
+
}
|
|
112
|
+
actions.visit(child, paragraph);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// MDXEditor 4 exports each child of a quote as a block, so inline children the toolbar's Quote,
|
|
116
|
+
// the `> ` shortcut or a merge put in a quote would come out one paragraph each.
|
|
117
|
+
const quoteExportVisitor = {
|
|
118
|
+
priority: 100,
|
|
119
|
+
testLexicalNode: (node) => node.getType() === 'quote',
|
|
120
|
+
visitLexicalNode({ lexicalNode, mdastParent, actions }) {
|
|
121
|
+
const quote = { type: 'blockquote', children: [] };
|
|
122
|
+
actions.appendToParent(mdastParent, quote);
|
|
123
|
+
writeBlocks(lexicalNode, quote, actions);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
// Backspace at the start of a quote moves its paragraphs and lists into one paragraph, which
|
|
127
|
+
// MDXEditor would export as one run of text; they are written as the blocks they show.
|
|
128
|
+
const paragraphOfBlocksExportVisitor = {
|
|
129
|
+
priority: 100,
|
|
130
|
+
testLexicalNode: (node) => node.getType() === 'paragraph' && hasBlockChild(node),
|
|
131
|
+
visitLexicalNode({ lexicalNode, mdastParent, actions }) {
|
|
132
|
+
writeBlocks(lexicalNode, mdastParent, actions);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
65
135
|
/** Typed `string` because the mdast node union this package resolves omits the ESM node. */
|
|
66
136
|
const ESM_NODE_TYPE = 'mdxjsEsm';
|
|
67
137
|
/** Node types inside a table, imported by its cell editors rather than by visitors. */
|
|
68
138
|
const CONSUMED_BY_PARENT_VISITOR = new Set(['tableRow', 'tableCell']);
|
|
69
139
|
/** Creates the JSX plugins from MDXEditor's exports; call once per load of MDXEditor. */
|
|
70
140
|
export function createMdxJsxPlugins(mdx) {
|
|
71
|
-
const { NestedLexicalEditor, UnrecognizedMarkdownConstructError, addImportVisitor$, importVisitors$, isMdastJsxNode, jsxPlugin, realmPlugin, } = mdx;
|
|
141
|
+
const { NestedLexicalEditor, UnrecognizedMarkdownConstructError, addExportVisitor$, addImportVisitor$, importVisitors$, isMdastJsxNode, jsxPlugin, realmPlugin, } = mdx;
|
|
72
142
|
/**
|
|
73
143
|
* Shows an element's tag and attributes read-only (they are edited as source)
|
|
74
144
|
* and edits its children, inline or block per the parsed node: one `*`
|
|
@@ -92,27 +162,35 @@ export function createMdxJsxPlugins(mdx) {
|
|
|
92
162
|
/**
|
|
93
163
|
* Reports through `onError`, at import, what MDXEditor would otherwise lose or
|
|
94
164
|
* break on without reporting it: `import`/`export` lines (its visitor for them
|
|
95
|
-
* is a no-op),
|
|
96
|
-
* inside a JSX element or table, whose children nested editors
|
|
97
|
-
* only logging a failure and writing partial children back on
|
|
98
|
-
* an error class MDXEditor's import reports; any other error
|
|
165
|
+
* is a no-op), what `unsupportedJsx` and `rearrangedBlocks` reject, and content
|
|
166
|
+
* with no visitor inside a JSX element or table, whose children nested editors
|
|
167
|
+
* import later, only logging a failure and writing partial children back on
|
|
168
|
+
* edit. It throws an error class MDXEditor's import reports; any other error
|
|
169
|
+
* crashes the editor.
|
|
99
170
|
*/
|
|
100
171
|
const roundTripGuardPlugin = realmPlugin({
|
|
101
172
|
init(realm) {
|
|
102
173
|
const guard = {
|
|
103
174
|
priority: 100,
|
|
104
|
-
testNode: (node) => node.type === ESM_NODE_TYPE ||
|
|
175
|
+
testNode: (node) => node.type === ESM_NODE_TYPE ||
|
|
176
|
+
node.type === 'table' ||
|
|
177
|
+
node.type === 'listItem' ||
|
|
178
|
+
isMdastJsxNode(node),
|
|
105
179
|
visitNode({ mdastNode, descriptors, actions }) {
|
|
106
180
|
if (mdastNode.type === ESM_NODE_TYPE) {
|
|
107
181
|
throw new UnrecognizedMarkdownConstructError('import/export statements cannot be edited in the rich-text editor');
|
|
108
182
|
}
|
|
109
183
|
const reject = (node) => {
|
|
110
|
-
const reason = unsupportedJsx(mdx, node);
|
|
184
|
+
const reason = unsupportedJsx(mdx, node) ?? rearrangedBlocks(node);
|
|
111
185
|
if (reason !== null) {
|
|
112
186
|
throw new UnrecognizedMarkdownConstructError(`${reason} cannot be edited in the rich-text editor`);
|
|
113
187
|
}
|
|
114
188
|
};
|
|
115
189
|
reject(mdastNode);
|
|
190
|
+
if (mdastNode.type === 'listItem') {
|
|
191
|
+
actions.nextVisitor();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
116
194
|
const visitors = realm.getValue(importVisitors$).filter((visitor) => visitor !== guard);
|
|
117
195
|
const hasVisitor = (node) => visitors.some((visitor) => typeof visitor.testNode === 'string'
|
|
118
196
|
? visitor.testNode === node.type
|
|
@@ -134,8 +212,14 @@ export function createMdxJsxPlugins(mdx) {
|
|
|
134
212
|
realm.pub(addImportVisitor$, guard);
|
|
135
213
|
},
|
|
136
214
|
});
|
|
215
|
+
const blockExportPlugin = realmPlugin({
|
|
216
|
+
init(realm) {
|
|
217
|
+
realm.pub(addExportVisitor$, [quoteExportVisitor, paragraphOfBlocksExportVisitor]);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
137
220
|
return () => [
|
|
138
221
|
jsxPlugin({ jsxComponentDescriptors: [catchAllJsxDescriptor] }),
|
|
139
222
|
roundTripGuardPlugin(),
|
|
223
|
+
blockExportPlugin(),
|
|
140
224
|
];
|
|
141
225
|
}
|
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.105";
|
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.105';
|
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.105",
|
|
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": {
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"@mantine/hooks": "^7.14.2",
|
|
115
115
|
"@mantine/modals": "^7.14.2",
|
|
116
116
|
"@mantine/notifications": "^7.14.2",
|
|
117
|
-
"@mdxeditor/editor": "^3.
|
|
117
|
+
"@mdxeditor/editor": "^4.3.2",
|
|
118
118
|
"@octokit/auth-token": "^4.0.0",
|
|
119
119
|
"@octokit/plugin-throttling": "^8.2.0",
|
|
120
120
|
"@octokit/rest": "^20.0.2",
|