fumadocs-core 15.5.2 → 15.5.4

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.
@@ -8,18 +8,49 @@ var defaultThemes = {
8
8
  };
9
9
  var highlighters = /* @__PURE__ */ new Map();
10
10
  async function _highlight(code, options) {
11
- const { lang, components: _, engine, ...rest } = options;
12
- let themes = { themes: defaultThemes };
11
+ const {
12
+ lang: initialLang,
13
+ fallbackLanguage,
14
+ components: _,
15
+ engine = "oniguruma",
16
+ ...rest
17
+ } = options;
18
+ let lang = initialLang;
19
+ let themes;
20
+ let themesToLoad;
13
21
  if ("theme" in options && options.theme) {
14
22
  themes = { theme: options.theme };
15
- } else if ("themes" in options && options.themes) {
16
- themes = { themes: options.themes };
23
+ themesToLoad = [themes.theme];
24
+ } else {
25
+ themes = {
26
+ themes: "themes" in options && options.themes ? options.themes : defaultThemes
27
+ };
28
+ themesToLoad = Object.values(themes.themes).filter((v) => v !== void 0);
29
+ }
30
+ let highlighter;
31
+ if (typeof engine === "string") {
32
+ highlighter = await getHighlighter(engine, {
33
+ langs: [],
34
+ themes: themesToLoad
35
+ });
36
+ } else {
37
+ highlighter = await getHighlighter("custom", {
38
+ engine,
39
+ langs: [],
40
+ themes: themesToLoad
41
+ });
42
+ if (process.env.NODE_ENV === "development") {
43
+ console.warn(
44
+ "[Fumadocs `highlight()`] Avoid passing `engine` directly. For custom engines, use `shiki` directly instead."
45
+ );
46
+ }
47
+ }
48
+ try {
49
+ await highlighter.loadLanguage(lang);
50
+ } catch {
51
+ lang = fallbackLanguage ?? "text";
52
+ await highlighter.loadLanguage(lang);
17
53
  }
18
- const highlighter = await getHighlighter("custom", {
19
- engine,
20
- langs: [lang],
21
- themes: "theme" in themes ? [themes.theme] : Object.values(themes.themes).filter((v) => v !== void 0)
22
- });
23
54
  return highlighter.codeToHast(code, {
24
55
  lang,
25
56
  ...rest,
@@ -3,29 +3,18 @@ import {
3
3
  _highlight,
4
4
  _renderHighlight,
5
5
  highlight
6
- } from "../chunk-KNWSJ4IF.js";
6
+ } from "../chunk-3NX26V7I.js";
7
7
 
8
8
  // src/highlight/client.tsx
9
9
  import {
10
10
  use,
11
+ useEffect,
11
12
  useId,
12
- useLayoutEffect,
13
13
  useMemo,
14
14
  useRef,
15
15
  useState
16
16
  } from "react";
17
17
  import { jsx } from "react/jsx-runtime";
18
- var jsEngine;
19
- function getHighlightOptions(from) {
20
- if (from.engine) return from;
21
- jsEngine ??= import("shiki/engine/javascript").then(
22
- (res) => res.createJavaScriptRegexEngine()
23
- );
24
- return {
25
- ...from,
26
- engine: jsEngine
27
- };
28
- }
29
18
  function useShiki(code, {
30
19
  withPrerenderScript = false,
31
20
  loading,
@@ -36,7 +25,10 @@ function useShiki(code, {
36
25
  () => deps ? JSON.stringify(deps) : `${options.lang}:${code}`,
37
26
  [code, deps, options.lang]
38
27
  );
39
- const shikiOptions = getHighlightOptions(options);
28
+ const shikiOptions = {
29
+ ...options,
30
+ engine: options.engine ?? "js"
31
+ };
40
32
  const currentTask = useRef({
41
33
  key,
42
34
  aborted: false
@@ -51,7 +43,7 @@ function useShiki(code, {
51
43
  currentTask.current = void 0;
52
44
  return loading;
53
45
  });
54
- useLayoutEffect(() => {
46
+ useEffect(() => {
55
47
  if (currentTask.current?.key === key) return;
56
48
  if (currentTask.current) {
57
49
  currentTask.current.aborted = true;
@@ -4,8 +4,9 @@ import { Components } from 'hast-util-to-jsx-runtime';
4
4
  import { ReactNode } from 'react';
5
5
 
6
6
  type HighlightOptionsCommon = CodeToHastOptionsCommon<BundledLanguage> & CodeOptionsMeta & {
7
- engine?: Awaitable<RegexEngine>;
7
+ engine?: 'js' | 'oniguruma' | Awaitable<RegexEngine>;
8
8
  components?: Partial<Components>;
9
+ fallbackLanguage?: BundledLanguage;
9
10
  };
10
11
  type HighlightOptionsThemes = CodeOptionsThemes<BundledTheme>;
11
12
  type HighlightOptions = HighlightOptionsCommon & (HighlightOptionsThemes | Record<never, never>);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getHighlighter,
3
3
  highlight
4
- } from "../chunk-KNWSJ4IF.js";
4
+ } from "../chunk-3NX26V7I.js";
5
5
  export {
6
6
  getHighlighter,
7
7
  highlight
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  defaultThemes,
11
11
  getHighlighter
12
- } from "../chunk-KNWSJ4IF.js";
12
+ } from "../chunk-3NX26V7I.js";
13
13
 
14
14
  // src/mdx-plugins/index.ts
15
15
  import {
@@ -22,6 +22,7 @@ declare class FileSystem<File> {
22
22
 
23
23
  interface LoadOptions {
24
24
  transformers?: Transformer[];
25
+ buildFiles: (files: VirtualFile[]) => (MetaFile | PageFile)[];
25
26
  }
26
27
  type ContentStorage = FileSystem<MetaFile | PageFile>;
27
28
  interface MetaFile<Data extends MetaData = MetaData> {
@@ -41,7 +42,7 @@ type Transformer = (context: {
41
42
  storage: ContentStorage;
42
43
  options: LoadOptions;
43
44
  }) => void;
44
- declare function loadFiles(files: VirtualFile[], buildFile: (file: VirtualFile) => MetaFile | PageFile, options: LoadOptions): ContentStorage;
45
+ declare function loadFiles(files: VirtualFile[], options: LoadOptions): ContentStorage;
45
46
 
46
47
  interface FileInfo {
47
48
  /**
@@ -201,14 +202,14 @@ interface LoaderOutput<Config extends LoaderConfig> {
201
202
  generateParams: <TSlug extends string = 'slug', TLang extends string = 'lang'>(slug?: TSlug, lang?: TLang) => (Record<TSlug, string[]> & Record<TLang, string>)[];
202
203
  }
203
204
  declare function createGetUrl(baseUrl: string, i18n?: I18nConfig): UrlFn;
204
- /**
205
- * Convert file path into slugs, also encode non-ASCII characters, so they can work in pathname
206
- */
207
- declare function getSlugs(info: FileInfo): string[];
208
205
  declare function loader<Config extends SourceConfig, I18n extends I18nConfig | undefined = undefined>(options: LoaderOptions<Config, I18n>): LoaderOutput<{
209
206
  source: Config;
210
207
  i18n: I18n extends I18nConfig ? true : false;
211
208
  }>;
209
+ /**
210
+ * Convert file path into slugs, also encode non-ASCII characters, so they can work in pathname
211
+ */
212
+ declare function getSlugs(file: string | FileInfo): string[];
212
213
 
213
214
  interface MetaData {
214
215
  icon?: string | undefined;
@@ -265,12 +265,15 @@ var FileSystem = class {
265
265
  };
266
266
 
267
267
  // src/source/load-files.ts
268
- function loadFiles(files, buildFile, options) {
268
+ function loadFiles(files, options) {
269
269
  const { transformers = [] } = options;
270
270
  const storage = new FileSystem();
271
- for (const file of files) {
272
- const parsedPath = normalizePath(file.path);
273
- storage.write(parsedPath, buildFile(file));
271
+ const normalized = files.map((file) => ({
272
+ ...file,
273
+ path: normalizePath(file.path)
274
+ }));
275
+ for (const item of options.buildFiles(normalized)) {
276
+ storage.write(item.path, item);
274
277
  }
275
278
  for (const transformer of transformers) {
276
279
  transformer({
@@ -280,14 +283,14 @@ function loadFiles(files, buildFile, options) {
280
283
  }
281
284
  return storage;
282
285
  }
283
- function loadFilesI18n(files, buildFile, options) {
284
- const parser = options.i18n.parser === "dir" ? dirParser : dotParser;
286
+ function loadFilesI18n(files, options, i18n) {
287
+ const parser = i18n.parser === "dir" ? dirParser : dotParser;
285
288
  const storages = {};
286
- for (const lang of options.i18n.languages) {
289
+ for (const lang of i18n.languages) {
287
290
  storages[lang] = loadFiles(
288
291
  files.flatMap((file) => {
289
292
  const [path, locale] = parser(normalizePath(file.path));
290
- if ((locale ?? options.i18n.defaultLanguage) === lang) {
293
+ if ((locale ?? i18n.defaultLanguage) === lang) {
291
294
  return {
292
295
  ...file,
293
296
  path
@@ -295,7 +298,6 @@ function loadFilesI18n(files, buildFile, options) {
295
298
  }
296
299
  return [];
297
300
  }),
298
- buildFile,
299
301
  options
300
302
  );
301
303
  }
@@ -385,16 +387,6 @@ function createGetUrl(baseUrl, i18n) {
385
387
  return `/${paths.filter((v) => v.length > 0).join("/")}`;
386
388
  };
387
389
  }
388
- function getSlugs(info) {
389
- const slugs = [];
390
- for (const seg of info.dirname.split("/")) {
391
- if (seg.length > 0 && !/^\(.+\)$/.test(seg)) slugs.push(encodeURI(seg));
392
- }
393
- if (info.name !== "index") {
394
- slugs.push(encodeURI(info.name));
395
- }
396
- return slugs;
397
- }
398
390
  function loader(options) {
399
391
  return createOutput(options);
400
392
  }
@@ -402,35 +394,67 @@ function createOutput(options) {
402
394
  if (!options.url && !options.baseUrl) {
403
395
  console.warn("`loader()` now requires a `baseUrl` option to be defined.");
404
396
  }
405
- const { source, slugs: slugsFn = getSlugs, i18n } = options;
397
+ const {
398
+ source,
399
+ baseUrl = "/",
400
+ i18n,
401
+ slugs: slugsFn,
402
+ url: getUrl = createGetUrl(baseUrl ?? "/", i18n),
403
+ transformers
404
+ } = options;
406
405
  const defaultLanguage = i18n?.defaultLanguage ?? "";
407
- const getUrl = options.url ?? createGetUrl(options.baseUrl ?? "/", options.i18n);
408
406
  const files = typeof source.files === "function" ? source.files() : source.files;
409
- function buildFile(file) {
410
- if (file.type === "page") {
407
+ function buildFiles(files2) {
408
+ const indexFiles = [];
409
+ const taken = /* @__PURE__ */ new Set();
410
+ for (const file of files2) {
411
+ if (file.type !== "page" || file.slugs) continue;
412
+ if (isIndex(file.path) && !slugsFn) {
413
+ indexFiles.push(file);
414
+ continue;
415
+ }
416
+ file.slugs = slugsFn ? slugsFn(parseFilePath(file.path)) : getSlugs(file.path);
417
+ const key = file.slugs.join("/");
418
+ if (taken.has(key)) throw new Error("Duplicated slugs");
419
+ taken.add(key);
420
+ }
421
+ for (const file of indexFiles) {
422
+ file.slugs = getSlugs(file.path);
423
+ if (taken.has(file.slugs.join("/"))) file.slugs.push("index");
424
+ }
425
+ return files2.map((file) => {
426
+ if (file.type === "page") {
427
+ return {
428
+ format: "page",
429
+ path: file.path,
430
+ slugs: file.slugs,
431
+ data: file.data,
432
+ absolutePath: file.absolutePath ?? ""
433
+ };
434
+ }
411
435
  return {
412
- format: "page",
436
+ format: "meta",
413
437
  path: file.path,
414
- slugs: file.slugs ?? slugsFn(parseFilePath(file.path)),
415
- data: file.data,
416
- absolutePath: file.absolutePath ?? ""
438
+ absolutePath: file.absolutePath ?? "",
439
+ data: file.data
417
440
  };
418
- }
419
- return {
420
- format: "meta",
421
- path: file.path,
422
- absolutePath: file.absolutePath ?? "",
423
- data: file.data
424
- };
441
+ });
425
442
  }
426
- const storages = i18n ? loadFilesI18n(files, buildFile, {
427
- ...options,
428
- i18n: {
443
+ const storages = i18n ? loadFilesI18n(
444
+ files,
445
+ {
446
+ buildFiles,
447
+ transformers
448
+ },
449
+ {
429
450
  ...i18n,
430
451
  parser: i18n.parser ?? "dot"
431
452
  }
432
- }) : {
433
- "": loadFiles(files, buildFile, options)
453
+ ) : {
454
+ "": loadFiles(files, {
455
+ transformers,
456
+ buildFiles
457
+ })
434
458
  };
435
459
  const walker = indexPages(storages, getUrl, i18n);
436
460
  const builder = createPageTreeBuilder(getUrl);
@@ -458,20 +482,19 @@ function createOutput(options) {
458
482
  pageTree = v;
459
483
  },
460
484
  getPageByHref(href, { dir = "", language } = {}) {
461
- const pages = this.getPages(language);
462
485
  const [value, hash] = href.split("#", 2);
463
486
  let target;
464
487
  if (value.startsWith(".") && (value.endsWith(".md") || value.endsWith(".mdx"))) {
465
- const hrefPath = joinPath(dir, value);
466
- target = pages.find((item) => item.file.path === hrefPath);
488
+ const path = joinPath(dir, value);
489
+ target = walker.pathToPage.get(`${language}.${path}`);
467
490
  } else {
468
- target = pages.find((item) => item.url === value);
491
+ target = this.getPages(language).find((item) => item.url === value);
469
492
  }
470
- if (!target) return;
471
- return {
472
- page: target,
473
- hash
474
- };
493
+ if (target)
494
+ return {
495
+ page: target,
496
+ hash
497
+ };
475
498
  },
476
499
  getPages(language = defaultLanguage) {
477
500
  const pages = [];
@@ -549,6 +572,25 @@ function fileToPage(file, getUrl, locale) {
549
572
  locale
550
573
  };
551
574
  }
575
+ var GroupRegex = /^\(.+\)$/;
576
+ function isIndex(file) {
577
+ return basename(file, extname(file)) === "index";
578
+ }
579
+ function getSlugs(file) {
580
+ if (typeof file !== "string") return getSlugs(file.path);
581
+ const dir = dirname(file);
582
+ const name = basename(file, extname(file));
583
+ const slugs = [];
584
+ for (const seg of dir.split("/")) {
585
+ if (seg.length > 0 && !GroupRegex.test(seg)) slugs.push(encodeURI(seg));
586
+ }
587
+ if (GroupRegex.test(name))
588
+ throw new Error(`Cannot use folder group in file names: ${file}`);
589
+ if (name !== "index") {
590
+ slugs.push(encodeURI(name));
591
+ }
592
+ return slugs;
593
+ }
552
594
  export {
553
595
  FileSystem,
554
596
  createGetUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-core",
3
- "version": "15.5.2",
3
+ "version": "15.5.4",
4
4
  "description": "The library for building a documentation website in Next.js",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -87,8 +87,8 @@
87
87
  "dependencies": {
88
88
  "@formatjs/intl-localematcher": "^0.6.1",
89
89
  "@orama/orama": "^3.1.6",
90
- "@shikijs/rehype": "^3.6.0",
91
- "@shikijs/transformers": "^3.6.0",
90
+ "@shikijs/rehype": "^3.7.0",
91
+ "@shikijs/transformers": "^3.7.0",
92
92
  "github-slugger": "^2.0.0",
93
93
  "hast-util-to-estree": "^3.1.3",
94
94
  "hast-util-to-jsx-runtime": "^2.3.6",
@@ -99,24 +99,24 @@
99
99
  "remark-gfm": "^4.0.1",
100
100
  "remark-rehype": "^11.1.2",
101
101
  "scroll-into-view-if-needed": "^3.1.0",
102
- "shiki": "^3.6.0",
102
+ "shiki": "^3.7.0",
103
103
  "unist-util-visit": "^5.0.0"
104
104
  },
105
105
  "devDependencies": {
106
106
  "@mdx-js/mdx": "^3.1.0",
107
107
  "@oramacloud/client": "^2.1.4",
108
- "@tanstack/react-router": "^1.121.2",
108
+ "@tanstack/react-router": "^1.121.27",
109
109
  "@types/estree-jsx": "^1.0.5",
110
110
  "@types/hast": "^3.0.4",
111
111
  "@types/mdast": "^4.0.3",
112
112
  "@types/negotiator": "^0.6.4",
113
- "@types/node": "24.0.1",
113
+ "@types/node": "24.0.3",
114
114
  "@types/react": "^19.1.8",
115
115
  "@types/react-dom": "^19.1.6",
116
- "algoliasearch": "5.27.0",
116
+ "algoliasearch": "5.29.0",
117
117
  "mdast-util-mdx-jsx": "^3.2.0",
118
118
  "mdast-util-mdxjs-esm": "^2.0.1",
119
- "next": "^15.3.3",
119
+ "next": "^15.3.4",
120
120
  "react-router": "^7.6.2",
121
121
  "remark-mdx": "^3.1.0",
122
122
  "typescript": "^5.8.3",
@@ -127,11 +127,11 @@
127
127
  },
128
128
  "peerDependencies": {
129
129
  "@oramacloud/client": "1.x.x || 2.x.x",
130
+ "@types/react": "*",
130
131
  "algoliasearch": "5.x.x",
131
132
  "next": "14.x.x || 15.x.x",
132
133
  "react": "18.x.x || 19.x.x",
133
- "react-dom": "18.x.x || 19.x.x",
134
- "@types/react": "*"
134
+ "react-dom": "18.x.x || 19.x.x"
135
135
  },
136
136
  "peerDependenciesMeta": {
137
137
  "@types/react": {