canopycms 0.0.68-int.103 → 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.
- 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 +52 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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,6 +62,43 @@ 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: ['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 or quote says, or null. Its import joins a list
|
|
79
|
+
* item's adjacent paragraphs and moves a nested list into a new item after its own, so what follows
|
|
80
|
+
* the list comes out before it; its export writes an item tight, so `---` underlines a paragraph and
|
|
81
|
+
* a line continues a quote or table (`RUNS_INTO`); and 3.53 exports a quote as one paragraph.
|
|
82
|
+
*/
|
|
83
|
+
function rearrangedBlocks(node) {
|
|
84
|
+
if (node.type === 'blockquote') {
|
|
85
|
+
return node.children.length > 1 ? 'a quote with more than one paragraph or block' : null;
|
|
86
|
+
}
|
|
87
|
+
if (node.type !== 'listItem')
|
|
88
|
+
return null;
|
|
89
|
+
for (let i = 1; i < node.children.length; i++) {
|
|
90
|
+
const before = node.children[i - 1].type;
|
|
91
|
+
const after = node.children[i].type;
|
|
92
|
+
if (before === 'list')
|
|
93
|
+
return 'a list item with content after its nested list';
|
|
94
|
+
if (RUNS_INTO[before]?.includes(after)) {
|
|
95
|
+
return before === 'paragraph' && after === 'paragraph'
|
|
96
|
+
? 'a list item with more than one paragraph'
|
|
97
|
+
: `a list item with a ${BLOCK_NAMES[before] ?? before} followed by a ${BLOCK_NAMES[after] ?? after}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
65
102
|
/** Typed `string` because the mdast node union this package resolves omits the ESM node. */
|
|
66
103
|
const ESM_NODE_TYPE = 'mdxjsEsm';
|
|
67
104
|
/** Node types inside a table, imported by its cell editors rather than by visitors. */
|
|
@@ -92,27 +129,36 @@ export function createMdxJsxPlugins(mdx) {
|
|
|
92
129
|
/**
|
|
93
130
|
* Reports through `onError`, at import, what MDXEditor would otherwise lose or
|
|
94
131
|
* 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
|
|
132
|
+
* is a no-op), what `unsupportedJsx` and `rearrangedBlocks` reject, and content
|
|
133
|
+
* with no visitor inside a JSX element or table, whose children nested editors
|
|
134
|
+
* import later, only logging a failure and writing partial children back on
|
|
135
|
+
* edit. It throws an error class MDXEditor's import reports; any other error
|
|
136
|
+
* crashes the editor.
|
|
99
137
|
*/
|
|
100
138
|
const roundTripGuardPlugin = realmPlugin({
|
|
101
139
|
init(realm) {
|
|
102
140
|
const guard = {
|
|
103
141
|
priority: 100,
|
|
104
|
-
testNode: (node) => node.type === ESM_NODE_TYPE ||
|
|
142
|
+
testNode: (node) => node.type === ESM_NODE_TYPE ||
|
|
143
|
+
node.type === 'table' ||
|
|
144
|
+
node.type === 'listItem' ||
|
|
145
|
+
node.type === 'blockquote' ||
|
|
146
|
+
isMdastJsxNode(node),
|
|
105
147
|
visitNode({ mdastNode, descriptors, actions }) {
|
|
106
148
|
if (mdastNode.type === ESM_NODE_TYPE) {
|
|
107
149
|
throw new UnrecognizedMarkdownConstructError('import/export statements cannot be edited in the rich-text editor');
|
|
108
150
|
}
|
|
109
151
|
const reject = (node) => {
|
|
110
|
-
const reason = unsupportedJsx(mdx, node);
|
|
152
|
+
const reason = unsupportedJsx(mdx, node) ?? rearrangedBlocks(node);
|
|
111
153
|
if (reason !== null) {
|
|
112
154
|
throw new UnrecognizedMarkdownConstructError(`${reason} cannot be edited in the rich-text editor`);
|
|
113
155
|
}
|
|
114
156
|
};
|
|
115
157
|
reject(mdastNode);
|
|
158
|
+
if (mdastNode.type === 'listItem' || mdastNode.type === 'blockquote') {
|
|
159
|
+
actions.nextVisitor();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
116
162
|
const visitors = realm.getValue(importVisitors$).filter((visitor) => visitor !== guard);
|
|
117
163
|
const hasVisitor = (node) => visitors.some((visitor) => typeof visitor.testNode === 'string'
|
|
118
164
|
? visitor.testNode === node.type
|
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.104";
|
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.104';
|
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.104",
|
|
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": {
|