@backmatter/next 0.0.17 → 0.0.19
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/config.cjs +299 -7
- package/package.json +1 -1
- package/src/cached-doc.ts +34 -0
- package/src/config.ts +28 -4
- package/src/mdx-compile.ts +20 -1
- package/src/routes.ts +25 -22
package/dist/config.cjs
CHANGED
|
@@ -73,8 +73,8 @@ __export(exports_config, {
|
|
|
73
73
|
withBackmatter: () => withBackmatter
|
|
74
74
|
});
|
|
75
75
|
module.exports = __toCommonJS(exports_config);
|
|
76
|
-
var
|
|
77
|
-
var
|
|
76
|
+
var import_node_fs4 = require("node:fs");
|
|
77
|
+
var import_node_path4 = __toESM(require("node:path"));
|
|
78
78
|
|
|
79
79
|
// ../core/src/node/generate.ts
|
|
80
80
|
var import_node_fs2 = require("node:fs");
|
|
@@ -398,16 +398,295 @@ function generate(options) {
|
|
|
398
398
|
};
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
// ../core/src/node/registry-import.ts
|
|
402
|
+
var import_node_fs3 = require("node:fs");
|
|
403
|
+
var import_node_path3 = __toESM(require("node:path"));
|
|
404
|
+
var REGISTRY_SPECIFIER = "backmatter";
|
|
405
|
+
function registryAliases() {
|
|
406
|
+
return REGISTRY_FILES.map((file) => {
|
|
407
|
+
const stem = file.slice(0, -".ts".length);
|
|
408
|
+
return {
|
|
409
|
+
specifier: stem === "index" ? REGISTRY_SPECIFIER : `${REGISTRY_SPECIFIER}/${stem}`,
|
|
410
|
+
file
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function registryTurbopackAlias(appRoot, outDir = DEFAULT_OUT_DIR) {
|
|
415
|
+
const alias = {};
|
|
416
|
+
const base = import_node_path3.default.resolve(appRoot);
|
|
417
|
+
for (const { specifier, file } of registryAliases()) {
|
|
418
|
+
const abs = import_node_path3.default.resolve(appRoot, outDir, file);
|
|
419
|
+
alias[specifier] = asRelative(import_node_path3.default.relative(base, abs).split(import_node_path3.default.sep).join("/"));
|
|
420
|
+
}
|
|
421
|
+
return alias;
|
|
422
|
+
}
|
|
423
|
+
function registryWebpackAlias(root, outDir = DEFAULT_OUT_DIR) {
|
|
424
|
+
const alias = {};
|
|
425
|
+
for (const { specifier, file } of registryAliases()) {
|
|
426
|
+
alias[`${specifier}$`] = import_node_path3.default.resolve(root, outDir, file);
|
|
427
|
+
}
|
|
428
|
+
return alias;
|
|
429
|
+
}
|
|
430
|
+
function registryImportPaths(outDir = DEFAULT_OUT_DIR, baseUrl) {
|
|
431
|
+
const base = !baseUrl || baseUrl === "." ? "." : baseUrl;
|
|
432
|
+
const from = import_node_path3.default.resolve("/registry-root", base);
|
|
433
|
+
const to = import_node_path3.default.resolve("/registry-root", outDir);
|
|
434
|
+
const folder = asRelative(import_node_path3.default.relative(from, to).split(import_node_path3.default.sep).join("/"));
|
|
435
|
+
const paths = {};
|
|
436
|
+
for (const { specifier, file } of registryAliases()) {
|
|
437
|
+
paths[specifier] = [`${folder}/${file}`];
|
|
438
|
+
}
|
|
439
|
+
return paths;
|
|
440
|
+
}
|
|
441
|
+
function addRegistryPaths(source, outDir = DEFAULT_OUT_DIR) {
|
|
442
|
+
let parsed;
|
|
443
|
+
try {
|
|
444
|
+
parsed = looseParse(source);
|
|
445
|
+
} catch {
|
|
446
|
+
return source;
|
|
447
|
+
}
|
|
448
|
+
if (!isRecord(parsed))
|
|
449
|
+
return source;
|
|
450
|
+
const options = parsed.compilerOptions;
|
|
451
|
+
if (!isRecord(options))
|
|
452
|
+
return source;
|
|
453
|
+
const baseUrl = typeof options.baseUrl === "string" ? options.baseUrl : undefined;
|
|
454
|
+
const mappings = registryImportPaths(outDir, baseUrl);
|
|
455
|
+
const existing = isRecord(options.paths) ? options.paths : undefined;
|
|
456
|
+
if (options.paths !== undefined && !existing)
|
|
457
|
+
return source;
|
|
458
|
+
const missing = Object.fromEntries(Object.entries(mappings).filter(([key]) => !existing || existing[key] === undefined));
|
|
459
|
+
if (Object.keys(missing).length === 0)
|
|
460
|
+
return source;
|
|
461
|
+
const compiler = findKeyBrace(source, "compilerOptions", 0, source.length);
|
|
462
|
+
if (!compiler)
|
|
463
|
+
return source;
|
|
464
|
+
const compilerEnd = matchBrace(source, compiler.brace);
|
|
465
|
+
if (compilerEnd < 0)
|
|
466
|
+
return source;
|
|
467
|
+
const nl = source.includes(`\r
|
|
468
|
+
`) ? `\r
|
|
469
|
+
` : `
|
|
470
|
+
`;
|
|
471
|
+
if (existing) {
|
|
472
|
+
const pathsBrace = findKeyBrace(source, "paths", compiler.brace + 1, compilerEnd);
|
|
473
|
+
if (!pathsBrace || pathsBrace.brace > compilerEnd)
|
|
474
|
+
return source;
|
|
475
|
+
const indent2 = innerIndent(source, pathsBrace.brace);
|
|
476
|
+
const entries2 = formatEntries(missing, indent2, nl);
|
|
477
|
+
return insertEntries(source, pathsBrace.brace, entries2, indent2, nl);
|
|
478
|
+
}
|
|
479
|
+
const indent = innerIndent(source, compiler.brace);
|
|
480
|
+
const child = `${indent} `;
|
|
481
|
+
const entries = formatEntries(missing, child, nl);
|
|
482
|
+
const block = `${indent}"paths": {${nl}${entries},${nl}${indent}}`;
|
|
483
|
+
return `${source.slice(0, compiler.brace + 1)}${nl}${block},${source.slice(compiler.brace + 1)}`;
|
|
484
|
+
}
|
|
485
|
+
function ensureRegistryTsconfig(root, outDir = DEFAULT_OUT_DIR) {
|
|
486
|
+
const tsconfig = import_node_path3.default.join(root, "tsconfig.json");
|
|
487
|
+
const jsconfig = import_node_path3.default.join(root, "jsconfig.json");
|
|
488
|
+
const file = import_node_fs3.existsSync(tsconfig) ? tsconfig : import_node_fs3.existsSync(jsconfig) ? jsconfig : null;
|
|
489
|
+
if (!file)
|
|
490
|
+
return null;
|
|
491
|
+
const before = import_node_fs3.readFileSync(file, "utf-8");
|
|
492
|
+
const after = addRegistryPaths(before, outDir);
|
|
493
|
+
if (after === before)
|
|
494
|
+
return null;
|
|
495
|
+
import_node_fs3.writeFileSync(file, after);
|
|
496
|
+
return `updated ${import_node_path3.default.basename(file)}`;
|
|
497
|
+
}
|
|
498
|
+
function asRelative(rel) {
|
|
499
|
+
if (rel.startsWith("./") || rel.startsWith("../"))
|
|
500
|
+
return rel;
|
|
501
|
+
return `./${rel}`;
|
|
502
|
+
}
|
|
503
|
+
function insertEntries(source, open, entries, indent, nl) {
|
|
504
|
+
let rest = source.slice(open + 1);
|
|
505
|
+
if (!/^\r?\n/.test(rest))
|
|
506
|
+
rest = `${nl}${indent}${rest.trimStart()}`;
|
|
507
|
+
return `${source.slice(0, open + 1)}${nl}${entries},${rest}`;
|
|
508
|
+
}
|
|
509
|
+
function formatEntries(mappings, indent, nl) {
|
|
510
|
+
return Object.entries(mappings).map(([key, value]) => `${indent}"${key}": [${value.map((entry) => JSON.stringify(entry)).join(", ")}]`).join(`,${nl}`);
|
|
511
|
+
}
|
|
512
|
+
function innerIndent(source, open) {
|
|
513
|
+
const rest = source.slice(open + 1);
|
|
514
|
+
const nl = rest.search(/\r?\n/);
|
|
515
|
+
if (nl !== -1) {
|
|
516
|
+
const after = rest.slice(nl).replace(/^\r?\n/, "");
|
|
517
|
+
const indent = after.match(/^[ \t]*/)?.[0] ?? "";
|
|
518
|
+
const next = after[indent.length];
|
|
519
|
+
if (indent.length > 0 && next && next !== "}")
|
|
520
|
+
return indent;
|
|
521
|
+
}
|
|
522
|
+
const lineStart = source.lastIndexOf(`
|
|
523
|
+
`, open) + 1;
|
|
524
|
+
const base = source.slice(lineStart, open).match(/^[ \t]*/)?.[0] ?? "";
|
|
525
|
+
return `${base} `;
|
|
526
|
+
}
|
|
527
|
+
function looseParse(source) {
|
|
528
|
+
const stripped = stripJsonComments(source).replace(/,\s*([}\]])/g, "$1");
|
|
529
|
+
return JSON.parse(stripped);
|
|
530
|
+
}
|
|
531
|
+
function stripJsonComments(input) {
|
|
532
|
+
let out = "";
|
|
533
|
+
let i = 0;
|
|
534
|
+
let inString = false;
|
|
535
|
+
let escaped = false;
|
|
536
|
+
while (i < input.length) {
|
|
537
|
+
const c = input[i];
|
|
538
|
+
const next = input[i + 1];
|
|
539
|
+
if (inString) {
|
|
540
|
+
out += c;
|
|
541
|
+
if (escaped)
|
|
542
|
+
escaped = false;
|
|
543
|
+
else if (c === "\\")
|
|
544
|
+
escaped = true;
|
|
545
|
+
else if (c === '"')
|
|
546
|
+
inString = false;
|
|
547
|
+
i++;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (c === '"') {
|
|
551
|
+
inString = true;
|
|
552
|
+
out += c;
|
|
553
|
+
i++;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (c === "/" && next === "/") {
|
|
557
|
+
i += 2;
|
|
558
|
+
while (i < input.length && input[i] !== `
|
|
559
|
+
`)
|
|
560
|
+
i++;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (c === "/" && next === "*") {
|
|
564
|
+
i += 2;
|
|
565
|
+
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/"))
|
|
566
|
+
i++;
|
|
567
|
+
i += 2;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
out += c;
|
|
571
|
+
i++;
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
function isRecord(value) {
|
|
576
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
577
|
+
}
|
|
578
|
+
function findKeyBrace(source, key, start, end) {
|
|
579
|
+
const needle = `"${key}"`;
|
|
580
|
+
let i = start;
|
|
581
|
+
let inString = false;
|
|
582
|
+
let escaped = false;
|
|
583
|
+
while (i < end) {
|
|
584
|
+
const c = source[i];
|
|
585
|
+
const next = source[i + 1];
|
|
586
|
+
if (inString) {
|
|
587
|
+
if (escaped)
|
|
588
|
+
escaped = false;
|
|
589
|
+
else if (c === "\\")
|
|
590
|
+
escaped = true;
|
|
591
|
+
else if (c === '"')
|
|
592
|
+
inString = false;
|
|
593
|
+
i++;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (c === '"') {
|
|
597
|
+
if (source.startsWith(needle, i)) {
|
|
598
|
+
let j = i + needle.length;
|
|
599
|
+
while (j < end && /\s/.test(source[j]))
|
|
600
|
+
j++;
|
|
601
|
+
if (source[j] === ":") {
|
|
602
|
+
j++;
|
|
603
|
+
while (j < end && /\s/.test(source[j]))
|
|
604
|
+
j++;
|
|
605
|
+
if (source[j] === "{")
|
|
606
|
+
return { brace: j };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
inString = true;
|
|
610
|
+
i++;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (c === "/" && next === "/") {
|
|
614
|
+
i += 2;
|
|
615
|
+
while (i < end && source[i] !== `
|
|
616
|
+
`)
|
|
617
|
+
i++;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (c === "/" && next === "*") {
|
|
621
|
+
i += 2;
|
|
622
|
+
while (i < end && !(source[i] === "*" && source[i + 1] === "/"))
|
|
623
|
+
i++;
|
|
624
|
+
i += 2;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
i++;
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
function matchBrace(source, open) {
|
|
632
|
+
let depth = 0;
|
|
633
|
+
let i = open;
|
|
634
|
+
let inString = false;
|
|
635
|
+
let escaped = false;
|
|
636
|
+
while (i < source.length) {
|
|
637
|
+
const c = source[i];
|
|
638
|
+
const next = source[i + 1];
|
|
639
|
+
if (inString) {
|
|
640
|
+
if (escaped)
|
|
641
|
+
escaped = false;
|
|
642
|
+
else if (c === "\\")
|
|
643
|
+
escaped = true;
|
|
644
|
+
else if (c === '"')
|
|
645
|
+
inString = false;
|
|
646
|
+
i++;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (c === '"') {
|
|
650
|
+
inString = true;
|
|
651
|
+
i++;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (c === "/" && next === "/") {
|
|
655
|
+
i += 2;
|
|
656
|
+
while (i < source.length && source[i] !== `
|
|
657
|
+
`)
|
|
658
|
+
i++;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (c === "/" && next === "*") {
|
|
662
|
+
i += 2;
|
|
663
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/"))
|
|
664
|
+
i++;
|
|
665
|
+
i += 2;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (c === "{")
|
|
669
|
+
depth++;
|
|
670
|
+
else if (c === "}") {
|
|
671
|
+
depth--;
|
|
672
|
+
if (depth === 0)
|
|
673
|
+
return i;
|
|
674
|
+
}
|
|
675
|
+
i++;
|
|
676
|
+
}
|
|
677
|
+
return -1;
|
|
678
|
+
}
|
|
679
|
+
|
|
401
680
|
// src/config.ts
|
|
402
681
|
var LOADER = "@backmatter/next/loader";
|
|
403
682
|
var SDK = "@backmatter/next";
|
|
404
683
|
function warnMissingInstrumentation(root) {
|
|
405
684
|
if (detectSdk(root) !== "@backmatter/next")
|
|
406
685
|
return;
|
|
407
|
-
const hasRoot =
|
|
408
|
-
const hasSrc =
|
|
686
|
+
const hasRoot = import_node_fs4.existsSync(import_node_path4.default.join(root, "instrumentation.ts"));
|
|
687
|
+
const hasSrc = import_node_fs4.existsSync(import_node_path4.default.join(root, "src", "instrumentation.ts"));
|
|
409
688
|
if (!hasRoot && !hasSrc) {
|
|
410
|
-
console.warn("backmatter: add instrumentation.ts that imports
|
|
689
|
+
console.warn("backmatter: add instrumentation.ts that imports backmatter/sync — block schema sync will not run without it");
|
|
411
690
|
}
|
|
412
691
|
}
|
|
413
692
|
function defaultLog(result, root) {
|
|
@@ -423,7 +702,7 @@ function defaultLog(result, root) {
|
|
|
423
702
|
}
|
|
424
703
|
function resolveLoaderOptions(options = {}) {
|
|
425
704
|
return {
|
|
426
|
-
root:
|
|
705
|
+
root: import_node_path4.default.resolve(options.root ?? process.cwd()),
|
|
427
706
|
outDir: options.outDir ?? DEFAULT_OUT_DIR,
|
|
428
707
|
modelsDir: options.modelsDir ?? DEFAULT_DIRS.models,
|
|
429
708
|
blocksDir: options.blocksDir ?? DEFAULT_DIRS.blocks,
|
|
@@ -442,7 +721,7 @@ function generateRegistry(options = {}) {
|
|
|
442
721
|
return result;
|
|
443
722
|
}
|
|
444
723
|
function registryMatchers(loaderOptions) {
|
|
445
|
-
const rel =
|
|
724
|
+
const rel = import_node_path4.default.relative(loaderOptions.root, import_node_path4.default.resolve(loaderOptions.root, loaderOptions.outDir)).split(import_node_path4.default.sep).join("/");
|
|
446
725
|
const escaped = rel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
447
726
|
return {
|
|
448
727
|
glob: `**/${rel}/*.ts`,
|
|
@@ -457,17 +736,30 @@ function withBackmatter(nextConfig = {}, options = {}) {
|
|
|
457
736
|
const cfg = nextConfig;
|
|
458
737
|
const existingTurbo = cfg.turbopack ?? {};
|
|
459
738
|
const existingRules = existingTurbo.rules ?? {};
|
|
739
|
+
const existingAlias = existingTurbo.resolveAlias ?? {};
|
|
460
740
|
const turbopackRule = { loaders: [use] };
|
|
461
741
|
const turbopack = {
|
|
462
742
|
...existingTurbo,
|
|
743
|
+
resolveAlias: {
|
|
744
|
+
...registryTurbopackAlias(loaderOptions.root, loaderOptions.outDir),
|
|
745
|
+
...existingAlias
|
|
746
|
+
},
|
|
463
747
|
rules: { ...existingRules, [glob]: turbopackRule }
|
|
464
748
|
};
|
|
465
749
|
const webpack = (config, context) => {
|
|
466
750
|
config.module ??= {};
|
|
467
751
|
config.module.rules ??= [];
|
|
468
752
|
config.module.rules.push({ test, enforce: "pre", use: [use] });
|
|
753
|
+
config.resolve ??= {};
|
|
754
|
+
const prevAlias = config.resolve.alias;
|
|
755
|
+
const prev = prevAlias && typeof prevAlias === "object" && !Array.isArray(prevAlias) ? prevAlias : {};
|
|
756
|
+
config.resolve.alias = {
|
|
757
|
+
...registryWebpackAlias(loaderOptions.root, loaderOptions.outDir),
|
|
758
|
+
...prev
|
|
759
|
+
};
|
|
469
760
|
return cfg.webpack ? cfg.webpack(config, context) : config;
|
|
470
761
|
};
|
|
762
|
+
ensureRegistryTsconfig(loaderOptions.root, loaderOptions.outDir);
|
|
471
763
|
const cacheComponents = cfg.cacheComponents ?? true;
|
|
472
764
|
const transpilePackages = [
|
|
473
765
|
...new Set([...cfg.transpilePackages ?? [], "@backmatter/next", "@backmatter/core"])
|
package/package.json
CHANGED
package/src/cached-doc.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// packages/next/src/cached-doc.ts
|
|
2
|
+
import type { ReactNode } from "react";
|
|
2
3
|
import { contentCacheTags, listCacheTags, type BackmatterDoc, type BackmatterDocSummary } from "@backmatter/core";
|
|
3
4
|
|
|
4
5
|
export type CacheFns = {
|
|
@@ -17,8 +18,15 @@ export type ListDocsFn = (filter?: {
|
|
|
17
18
|
locale?: string;
|
|
18
19
|
}) => Promise<BackmatterDocSummary[]>;
|
|
19
20
|
|
|
21
|
+
export type PublishedRender = (
|
|
22
|
+
locale: string,
|
|
23
|
+
type: string,
|
|
24
|
+
slug: string,
|
|
25
|
+
) => Promise<ReactNode>;
|
|
26
|
+
|
|
20
27
|
let registeredGetDoc: GetDocFn | null = null;
|
|
21
28
|
let registeredListDocs: ListDocsFn | null = null;
|
|
29
|
+
let registeredPublishedRender: PublishedRender | null = null;
|
|
22
30
|
|
|
23
31
|
/** Called from `createBackmatter` so `"use cache"` only closes over serializable args. */
|
|
24
32
|
export function registerCachedDocGet(getDoc: GetDocFn): void {
|
|
@@ -29,6 +37,11 @@ export function registerCachedListDocs(listDocs: ListDocsFn): void {
|
|
|
29
37
|
registeredListDocs = listDocs;
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
/** The catch-all registers the MDX/block render. `"use cache"` only receives ids. */
|
|
41
|
+
export function registerPublishedRender(render: PublishedRender): void {
|
|
42
|
+
registeredPublishedRender = render;
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
async function loadCacheFns(): Promise<CacheFns> {
|
|
33
46
|
try {
|
|
34
47
|
const mod = (await import("next/cache" as string)) as CacheFns;
|
|
@@ -80,6 +93,27 @@ export async function getCachedDocs(
|
|
|
80
93
|
return listDocs({ type, locale });
|
|
81
94
|
}
|
|
82
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Finished published page (MDX body or composer blocks, plus layout).
|
|
98
|
+
* The cache entry is what a static prerender and a normal link prefetch store.
|
|
99
|
+
*/
|
|
100
|
+
export async function getCachedPublished(
|
|
101
|
+
locale: string,
|
|
102
|
+
type: string,
|
|
103
|
+
slug: string,
|
|
104
|
+
cache?: CacheFns,
|
|
105
|
+
): Promise<ReactNode> {
|
|
106
|
+
"use cache";
|
|
107
|
+
const fns = cache ?? (await loadCacheFns());
|
|
108
|
+
fns.cacheLife("hours");
|
|
109
|
+
fns.cacheTag(...contentCacheTags(locale, type, slug));
|
|
110
|
+
const render = registeredPublishedRender;
|
|
111
|
+
if (!render) {
|
|
112
|
+
throw new Error("[backmatter] getCachedPublished called before a published render was registered");
|
|
113
|
+
}
|
|
114
|
+
return render(locale, type, slug);
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
/** Unwrap Next `params` inside `"use cache"` so Cache Components can prerender. */
|
|
84
118
|
export async function cachedRouteParams<T extends object>(
|
|
85
119
|
params: Promise<T> | T,
|
package/src/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// packages/next/src/config.ts
|
|
2
2
|
// `withBackmatter(nextConfig)` for next.config.{js,mjs,ts}.
|
|
3
3
|
//
|
|
4
|
-
// 1. Generates `.backmatter/` once when the config loads,
|
|
5
|
-
//
|
|
4
|
+
// 1. Generates `.backmatter/` once when the config loads, and aliases it to the
|
|
5
|
+
// `backmatter` import (`import { cms } from "backmatter"`).
|
|
6
6
|
// 2. Registers `@backmatter/next/loader` for the registry files with Turbopack
|
|
7
7
|
// (Next 16 default) and webpack (`next build --webpack`). The loader declares
|
|
8
8
|
// `models/`, `blocks/`, `richtext/`, and `mdx/` as context dependencies, so the bundler's
|
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
generate,
|
|
17
17
|
type GenerateResult,
|
|
18
18
|
} from "@backmatter/core/generate";
|
|
19
|
+
import {
|
|
20
|
+
ensureRegistryTsconfig,
|
|
21
|
+
registryTurbopackAlias,
|
|
22
|
+
registryWebpackAlias,
|
|
23
|
+
} from "@backmatter/core/node/registry-import";
|
|
19
24
|
import type { BackmatterLoaderOptions } from "./loader";
|
|
20
25
|
|
|
21
26
|
export type WithBackmatterOptions = {
|
|
@@ -52,6 +57,7 @@ export type NextConfigLike = {
|
|
|
52
57
|
|
|
53
58
|
export type WebpackConfigLike = {
|
|
54
59
|
module?: { rules?: unknown[] } & Record<string, unknown>;
|
|
60
|
+
resolve?: { alias?: Record<string, unknown> } & Record<string, unknown>;
|
|
55
61
|
} & Record<string, unknown>;
|
|
56
62
|
|
|
57
63
|
export function warnMissingInstrumentation(root: string): void {
|
|
@@ -60,7 +66,7 @@ export function warnMissingInstrumentation(root: string): void {
|
|
|
60
66
|
const hasSrc = existsSync(path.join(root, "src", "instrumentation.ts"));
|
|
61
67
|
if (!hasRoot && !hasSrc) {
|
|
62
68
|
console.warn(
|
|
63
|
-
"backmatter: add instrumentation.ts that imports
|
|
69
|
+
"backmatter: add instrumentation.ts that imports backmatter/sync — block schema sync will not run without it",
|
|
64
70
|
);
|
|
65
71
|
}
|
|
66
72
|
}
|
|
@@ -141,9 +147,14 @@ export function withBackmatter<T extends object>(
|
|
|
141
147
|
const cfg = nextConfig as T & NextConfigLike;
|
|
142
148
|
const existingTurbo = (cfg.turbopack ?? {}) as Record<string, unknown>;
|
|
143
149
|
const existingRules = (existingTurbo.rules ?? {}) as Record<string, unknown>;
|
|
150
|
+
const existingAlias = (existingTurbo.resolveAlias ?? {}) as Record<string, unknown>;
|
|
144
151
|
const turbopackRule: TurbopackRule = { loaders: [use] };
|
|
145
152
|
const turbopack = {
|
|
146
153
|
...existingTurbo,
|
|
154
|
+
resolveAlias: {
|
|
155
|
+
...registryTurbopackAlias(loaderOptions.root, loaderOptions.outDir),
|
|
156
|
+
...existingAlias,
|
|
157
|
+
},
|
|
147
158
|
rules: { ...existingRules, [glob]: turbopackRule },
|
|
148
159
|
};
|
|
149
160
|
|
|
@@ -152,17 +163,30 @@ export function withBackmatter<T extends object>(
|
|
|
152
163
|
config.module.rules ??= [];
|
|
153
164
|
// `enforce: "pre"` runs before Next's SWC loader; we return TypeScript, SWC compiles it.
|
|
154
165
|
config.module.rules.push({ test, enforce: "pre", use: [use] });
|
|
166
|
+
config.resolve ??= {};
|
|
167
|
+
const prevAlias = config.resolve.alias;
|
|
168
|
+
const prev =
|
|
169
|
+
prevAlias && typeof prevAlias === "object" && !Array.isArray(prevAlias) ? prevAlias : {};
|
|
170
|
+
config.resolve.alias = {
|
|
171
|
+
...registryWebpackAlias(loaderOptions.root, loaderOptions.outDir),
|
|
172
|
+
...prev,
|
|
173
|
+
};
|
|
155
174
|
return cfg.webpack ? cfg.webpack(config, context) : config;
|
|
156
175
|
};
|
|
157
176
|
|
|
177
|
+
ensureRegistryTsconfig(loaderOptions.root, loaderOptions.outDir);
|
|
178
|
+
|
|
158
179
|
const cacheComponents = cfg.cacheComponents ?? true;
|
|
180
|
+
// Published pages are static files. Partial prefetching would replace those
|
|
181
|
+
// files with one shared shell for every CMS URL, so a normal link would
|
|
182
|
+
// prefetch the shell instead of the finished page. Hosts can still opt in.
|
|
159
183
|
const transpilePackages = [
|
|
160
184
|
...new Set([...(cfg.transpilePackages ?? []), "@backmatter/next", "@backmatter/core"]),
|
|
161
185
|
];
|
|
162
186
|
return {
|
|
163
187
|
...nextConfig,
|
|
164
188
|
cacheComponents,
|
|
165
|
-
partialPrefetching: cfg.partialPrefetching ??
|
|
189
|
+
partialPrefetching: cfg.partialPrefetching ?? false,
|
|
166
190
|
transpilePackages,
|
|
167
191
|
turbopack,
|
|
168
192
|
webpack,
|
package/src/mdx-compile.ts
CHANGED
|
@@ -10,6 +10,25 @@ import remarkGfm from "remark-gfm";
|
|
|
10
10
|
|
|
11
11
|
export type MdxComponents = Record<string, ComponentType<any>>;
|
|
12
12
|
|
|
13
|
+
function UnknownMdx(props: { children?: ReactNode }) {
|
|
14
|
+
return props.children ?? null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MDX copies `components` with object spread, which skips a Proxy.
|
|
19
|
+
* Fill each capitalized tag the host did not provide so one unknown tag
|
|
20
|
+
* does not fail the page render.
|
|
21
|
+
*/
|
|
22
|
+
function mdxComponentsWithFallback(source: string, components?: MdxComponents): MdxComponents {
|
|
23
|
+
const provided: MdxComponents = { ...components };
|
|
24
|
+
for (const match of source.matchAll(/<([A-Z][A-Za-z0-9]*)\b/g)) {
|
|
25
|
+
const name = match[1];
|
|
26
|
+
if (!name || provided[name]) continue;
|
|
27
|
+
provided[name] = UnknownMdx;
|
|
28
|
+
}
|
|
29
|
+
return provided;
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
export async function compileMdx(
|
|
14
33
|
source: string,
|
|
15
34
|
components?: MdxComponents,
|
|
@@ -22,7 +41,7 @@ export async function compileMdx(
|
|
|
22
41
|
remarkPlugins: [remarkGfm],
|
|
23
42
|
baseUrl: import.meta.url,
|
|
24
43
|
});
|
|
25
|
-
return createElement(Content, { components });
|
|
44
|
+
return createElement(Content, { components: mdxComponentsWithFallback(source, components) });
|
|
26
45
|
} catch (err) {
|
|
27
46
|
const message = err instanceof Error ? err.message : "MDX compile failed";
|
|
28
47
|
return createElement("pre", { role: "alert" }, message);
|
package/src/routes.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { isRoutable, matchRoute, modelFormat, PREVIEW_CHECKOUT_QUERY, PREVIEW_CO
|
|
|
8
8
|
import type { BackmatterClient } from "@backmatter/core/node/create-backmatter";
|
|
9
9
|
import { fetchPreviewDoc } from "@backmatter/core/preview";
|
|
10
10
|
import { renderDoc, type MdxComponents } from "./mdx-compile";
|
|
11
|
-
import { cachedRouteParams } from "./cached-doc";
|
|
11
|
+
import { cachedRouteParams, getCachedPublished, registerPublishedRender } from "./cached-doc";
|
|
12
12
|
import { renderPage, type BlockComponents, type BlockFallbacks } from "./page-render";
|
|
13
13
|
import { previewUnavailable } from "./preview-ui";
|
|
14
14
|
|
|
@@ -177,11 +177,9 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
177
177
|
? await renderPage(doc, options.blocks, options.fallbacks)
|
|
178
178
|
: await renderDoc(doc, { components: options.components });
|
|
179
179
|
let body = rendered;
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
if (previewEnabled && isPage) {
|
|
180
|
+
if (previewEnabled) {
|
|
181
|
+
// Refresh on the server. Passing the host component map into the client
|
|
182
|
+
// preview wrapper is not serializable and crashes the page.
|
|
185
183
|
const { PreviewPageReloader } = await import("./preview-refresh");
|
|
186
184
|
body = createElement(
|
|
187
185
|
"div",
|
|
@@ -191,7 +189,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
191
189
|
type: doc.type,
|
|
192
190
|
slug: doc.slug,
|
|
193
191
|
}),
|
|
194
|
-
|
|
192
|
+
rendered,
|
|
195
193
|
);
|
|
196
194
|
}
|
|
197
195
|
const layout = options.layouts?.[doc.type];
|
|
@@ -231,15 +229,30 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
231
229
|
}
|
|
232
230
|
}
|
|
233
231
|
|
|
232
|
+
async function renderPublished(locale: string, type: string, slug: string): Promise<ReactNode> {
|
|
233
|
+
const doc = await cms.getDoc(locale, type, slug);
|
|
234
|
+
if (!doc) return null;
|
|
235
|
+
return createElement(
|
|
236
|
+
"div",
|
|
237
|
+
{ "data-backmatter-published": "", style: { display: "contents" } },
|
|
238
|
+
await layoutDoc(doc, false),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function publishedBody(locale: string, type: string, slug: string): Promise<ReactNode> {
|
|
243
|
+
registerPublishedRender(renderPublished);
|
|
244
|
+
const body = await getCachedPublished(locale, type, slug);
|
|
245
|
+
if (body == null) return failFor();
|
|
246
|
+
return body;
|
|
247
|
+
}
|
|
248
|
+
|
|
234
249
|
const CmsRoute: CatchAllRoute["CmsRoute"] = async ({ params, searchParams }) => {
|
|
235
250
|
const resolved = await cachedRouteParams(params);
|
|
236
251
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
237
252
|
if (!hit) return failFor();
|
|
238
253
|
const preview = await resolvePreviewState(cms, options.preview, searchParams);
|
|
239
254
|
if (previewReplacesPublished(preview)) return loadPreviewDoc(hit, preview);
|
|
240
|
-
|
|
241
|
-
if (!doc) return failFor();
|
|
242
|
-
return layoutDoc(doc, false);
|
|
255
|
+
return publishedBody(hit.locale, hit.type, hit.slug);
|
|
243
256
|
};
|
|
244
257
|
|
|
245
258
|
const PublishedRoute = async ({
|
|
@@ -251,13 +264,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
251
264
|
const resolved = await params;
|
|
252
265
|
const hit = resolve(resolved[CATCH_ALL_PARAM]);
|
|
253
266
|
if (!hit) return failFor();
|
|
254
|
-
|
|
255
|
-
if (!doc) return failFor();
|
|
256
|
-
return createElement(
|
|
257
|
-
"div",
|
|
258
|
-
{ "data-backmatter-published": "", style: { display: "contents" } },
|
|
259
|
-
await layoutDoc(doc, false),
|
|
260
|
-
);
|
|
267
|
+
return publishedBody(hit.locale, hit.type, hit.slug);
|
|
261
268
|
};
|
|
262
269
|
|
|
263
270
|
const PreviewBranch = async ({
|
|
@@ -288,11 +295,7 @@ export function createCatchAll<Models extends readonly ModelDef[]>(
|
|
|
288
295
|
createElement(
|
|
289
296
|
Fragment,
|
|
290
297
|
null,
|
|
291
|
-
createElement(
|
|
292
|
-
Suspense,
|
|
293
|
-
{ fallback: createElement("div", { "aria-busy": "true", style: { minHeight: "100vh" } }) },
|
|
294
|
-
createElement(PublishedRoute, props),
|
|
295
|
-
),
|
|
298
|
+
createElement(PublishedRoute, props),
|
|
296
299
|
createElement(Suspense, { fallback: null }, createElement(PreviewBranch, props)),
|
|
297
300
|
);
|
|
298
301
|
|