@lexibase/rdx-next 3.0.0 → 3.2.0

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/bin/cli.mjs CHANGED
@@ -10,6 +10,7 @@ import { createJiti } from 'jiti'
10
10
 
11
11
  import postcssConfig from '../postcss.config.mjs'
12
12
  import { preparePublicAssets } from './public-assets.mjs'
13
+ import { prepareProjectStyles } from './project-styles.mjs'
13
14
 
14
15
  export async function runRdxCli({ templateDir } = {}) {
15
16
  const supportedCommands = new Set(['dev', 'build', 'start'])
@@ -31,6 +32,7 @@ export async function runRdxCli({ templateDir } = {}) {
31
32
  force: true,
32
33
  recursive: true,
33
34
  })
35
+ await prepareProjectStyles(projectRoot, runtimeDirectory)
34
36
  // Resolve in Node before Next processes this file: UI may be transitive,
35
37
  // and native Tailwind plugins must stay outside the Turbopack module graph.
36
38
  const resolvePlugin = createRequire(import.meta.url)
@@ -0,0 +1,27 @@
1
+ import { readFile, stat, writeFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ /** Import project-owned CSS after the theme without copying or editing it. */
5
+ export async function prepareProjectStyles(projectRoot, runtimeDirectory) {
6
+ const source = path.join(projectRoot, 'rdx.css')
7
+ const destination = path.join(runtimeDirectory, 'app', 'globals.css')
8
+ const current = await readFile(destination, 'utf8')
9
+ const original = current.replace(
10
+ /^@import "\.\.\/\.\.\/rdx\.css";\r?\n/gm,
11
+ ''
12
+ )
13
+ let exists = false
14
+ try {
15
+ const info = await stat(source)
16
+ if (!info.isFile()) throw new Error('rdx.css must be a CSS file.')
17
+ exists = true
18
+ } catch (error) {
19
+ if (error.code !== 'ENOENT') throw error
20
+ }
21
+ const next = exists
22
+ ? original.replace(/(@import[^;]+;)/, '$1\n@import "../../rdx.css";')
23
+ : original
24
+ if (next !== current) {
25
+ await writeFile(destination, next)
26
+ }
27
+ }
package/config.d.mts CHANGED
@@ -1,8 +1,13 @@
1
1
  /** Common build root for the documentation project and its installed Next.js. */
2
2
  export function getRdxBuildRoot(rootDir: string): Promise<string>
3
3
 
4
- /** Explicit deployment files for a native Next.js application using RDX. */
4
+ /** Explicit deployment files and historical route redirects for a native Next.js application. */
5
5
  export function getRdxTracingConfig(options: {
6
6
  rootDir: string
7
7
  appDir?: string
8
- }): Promise<{ outputFileTracingIncludes: Record<string, string[]> }>
8
+ }): Promise<{
9
+ outputFileTracingIncludes: Record<string, string[]>
10
+ redirects?: () => Promise<
11
+ { source: string; destination: string; permanent: boolean }[]
12
+ >
13
+ }>
package/config.mjs CHANGED
@@ -62,7 +62,7 @@ function literalPattern(filename) {
62
62
  .replace(/([*?[\]{}()!+@])/g, '\\$1')
63
63
  }
64
64
 
65
- /** Next.js tracing for configured RDX content, independent of plugin names. */
65
+ /** Next.js tracing and historical route redirects, independent of plugin names. */
66
66
  export async function getRdxTracingConfig({ rootDir, appDir = rootDir }) {
67
67
  rootDir = path.resolve(rootDir)
68
68
  appDir = path.resolve(appDir)
@@ -77,11 +77,17 @@ export async function getRdxTracingConfig({ rootDir, appDir = rootDir }) {
77
77
  const project = await createRdxProject(input, { rootDir })
78
78
  const files = (
79
79
  await Promise.all(
80
- project.sources.map((source) =>
81
- contentFiles(
82
- resolveProjectPath(rootDir, source.docsDir, 'content source docsDir')
80
+ project.sources
81
+ .filter((source) => source.redirectTo === undefined)
82
+ .map((source) =>
83
+ contentFiles(
84
+ resolveProjectPath(
85
+ rootDir,
86
+ source.docsDir,
87
+ 'content source docsDir'
88
+ )
89
+ )
83
90
  )
84
- )
85
91
  )
86
92
  ).flat()
87
93
  for (const filename of project.runtimeFiles) {
@@ -94,7 +100,15 @@ export async function getRdxTracingConfig({ rootDir, appDir = rootDir }) {
94
100
  throw new Error(`Plugin runtimeFiles must name a file: ${filename}`)
95
101
  files.push(resolved)
96
102
  }
103
+ const redirects = project.sources
104
+ .filter((source) => source.redirectTo !== undefined)
105
+ .map((source) => ({
106
+ source: `/docs/${source.routePrefix}/:path*`,
107
+ destination: `/docs/${source.redirectTo}/:path*`,
108
+ permanent: true,
109
+ }))
97
110
  return {
111
+ ...(redirects.length ? { redirects: async () => redirects } : {}),
98
112
  outputFileTracingIncludes: {
99
113
  '/*': [
100
114
  ...new Set(
@@ -8,8 +8,10 @@ import {
8
8
  import { Button } from "@lexibase/rdx-ui/components/button";
9
9
  import { DocsSidebar } from "@lexibase/rdx-ui/components/docs-sidebar";
10
10
  import { DocsToc } from "@lexibase/rdx-ui/components/docs-toc";
11
+ import { DocsPageTransition } from "@lexibase/rdx-ui/components/docs-page-transition";
11
12
  import { DocsSearch } from "@lexibase/rdx-ui/components/docs-search";
12
13
  import { Home } from "@lexibase/rdx-ui/components/home";
14
+ import { SkipLink } from "@lexibase/rdx-ui/components/skip-link";
13
15
  import { FooterCredit } from "@lexibase/rdx-ui/components/footer-credit";
14
16
  import { Logo } from "@lexibase/rdx-ui/components/logo";
15
17
  import { getMdxComponents } from "@lexibase/rdx-ui/components/mdx-components";
@@ -21,13 +23,14 @@ import {
21
23
  VersionDropdown
22
24
  } from "@lexibase/rdx-ui/components/version-dropdown";
23
25
  import { ThemeProvider } from "@lexibase/rdx-ui/providers/theme-provider";
24
- import { notFound as notFound2, redirect } from "next/navigation";
26
+ import { notFound as notFound2, redirect, permanentRedirect } from "next/navigation";
25
27
 
26
28
  // src/runtime/mdx.ts
27
29
  import { compileMDX } from "next-mdx-remote/rsc";
28
30
  import remarkGfm from "remark-gfm";
29
31
  import {
30
32
  remarkDocumentLinks,
33
+ remarkDocumentationComponents,
31
34
  rehypeDocumentationHeadings,
32
35
  rehypeDocumentationCode
33
36
  } from "@lexibase/rdx-core/mdx";
@@ -39,7 +42,11 @@ function compileDocumentation(source, components, links) {
39
42
  blockJS: true,
40
43
  blockDangerousJS: true,
41
44
  mdxOptions: {
42
- remarkPlugins: [remarkGfm, [remarkDocumentLinks, links]],
45
+ remarkPlugins: [
46
+ remarkGfm,
47
+ remarkDocumentationComponents,
48
+ [remarkDocumentLinks, links]
49
+ ],
43
50
  rehypePlugins: [rehypeDocumentationHeadings, rehypeDocumentationCode]
44
51
  }
45
52
  }
@@ -53,12 +60,12 @@ import { createRdxProject, getProjectDocsPaths } from "@lexibase/rdx-core";
53
60
  import { getDocsIndex, getDocument, getSourceOptions } from "@lexibase/rdx-core";
54
61
  import { buildSearchIndex } from "@lexibase/rdx-core/search";
55
62
  function getSearchSources(project) {
56
- return project.config.search ? project.sources.map((source) => ({ source: source.routePrefix || "_" })) : [];
63
+ return project.config.search ? project.sources.filter((source) => source.redirectTo === void 0).map((source) => ({ source: source.routePrefix || "_" })) : [];
57
64
  }
58
65
  function getSearchResponse(project, key) {
59
66
  if (!project.config.search) return new Response(null, { status: 404 });
60
67
  const source = project.sources.find(
61
- (entry) => (entry.routePrefix || "_") === key
68
+ (entry) => entry.redirectTo === void 0 && (entry.routePrefix || "_") === key
62
69
  );
63
70
  if (!source) return new Response(null, { status: 404 });
64
71
  const options = getSourceOptions(project, source);
@@ -86,6 +93,7 @@ function getDocumentMetadata({
86
93
  slug
87
94
  }) {
88
95
  const route = resolveDocsRoute(project, slug);
96
+ if (route.source.redirectTo !== void 0) return {};
89
97
  if (!route.slug) return {};
90
98
  if (!route.document) notFound();
91
99
  const page = route.categories.flatMap((category) => category.pages).find((page2) => page2.slug === route.slug);
@@ -134,7 +142,6 @@ function RdxLayout({
134
142
  config,
135
143
  bodyClassName
136
144
  }) {
137
- const messages = getMessages(config.lang);
138
145
  return /* @__PURE__ */ jsx("html", { suppressHydrationWarning: true, lang: config.lang ?? "en", children: /* @__PURE__ */ jsx("body", { className: bodyClassName, children: /* @__PURE__ */ jsx(
139
146
  ThemeProvider,
140
147
  {
@@ -143,7 +150,7 @@ function RdxLayout({
143
150
  attribute: "class",
144
151
  defaultTheme: config.theme?.defaultTheme ?? "system",
145
152
  children: /* @__PURE__ */ jsxs(LocaleProvider, { lang: config.lang, children: [
146
- /* @__PURE__ */ jsx("a", { className: "rdx-skip-link", href: "#rdx-main", children: messages.skipContent }),
153
+ /* @__PURE__ */ jsx(SkipLink, { lang: config.lang }),
147
154
  /* @__PURE__ */ jsx("div", { className: "flex flex-col h-dvh", children })
148
155
  ] })
149
156
  }
@@ -156,7 +163,7 @@ function RdxHeader({
156
163
  const { config } = project;
157
164
  const messages = getMessages(config.lang);
158
165
  const { source, slug: currentSlug } = resolveDocsRoute2(project, slug);
159
- const options = project.sources.map((edition) => {
166
+ const options = project.sources.filter((edition) => edition.redirectTo === void 0).map((edition) => {
160
167
  const docs = getSourceOptions2(project, edition);
161
168
  const destination = currentSlug && getDocument2(currentSlug, docs) ? currentSlug : getDocsPaths(docs)[0];
162
169
  return {
@@ -233,6 +240,14 @@ async function DocsPage({
233
240
  document,
234
241
  categories
235
242
  } = resolveDocsRoute2(project, slug);
243
+ if (source.redirectTo !== void 0)
244
+ permanentRedirect(
245
+ [
246
+ "/docs",
247
+ source.redirectTo,
248
+ ...slug?.slice(1).map(encodeURIComponent) ?? []
249
+ ].join("/")
250
+ );
236
251
  if (!currentSlug)
237
252
  return /* @__PURE__ */ jsxs(Fragment, { children: [
238
253
  /* @__PURE__ */ jsx(RdxHeader, { project, slug }),
@@ -286,51 +301,68 @@ async function DocsPage({
286
301
  className: "relative flex-1 min-w-0 h-full overflow-y-auto pt-12 md:pt-0 outline-none",
287
302
  children: [
288
303
  project.config.TOC && /* @__PURE__ */ jsx(DocsToc, {}, hrefFor(currentSlug)),
289
- /* @__PURE__ */ jsxs(
290
- "section",
304
+ /* @__PURE__ */ jsx(
305
+ DocsPageTransition,
291
306
  {
292
- className: `w-full min-h-full px-7 flex flex-col gap-5 pb-8 md:py-8 ${project.config.TOC ? "xl:pr-20" : ""}`,
293
- children: [
294
- /* @__PURE__ */ jsxs("div", { className: "w-full flex flex-col gap-5 md:mx-auto md:max-w-[860px] pt-14 md:pt-0 flex-1", children: [
295
- /* @__PURE__ */ jsxs("header", { className: "mb-6", children: [
296
- source.badge && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: source.badge === "Archived" ? messages.archivedVersion : source.badge }),
297
- /* @__PURE__ */ jsx("span", { className: "text-xs text-zinc-600 font-semibold uppercase block -mb-0.5", children: category?.title }),
298
- /* @__PURE__ */ jsx("h1", { className: "text-4xl uppercase font-bold mb-2", children: title }),
299
- description && /* @__PURE__ */ jsx("p", { className: "text-sm text-ring max-w-[726px]", children: description })
300
- ] }),
301
- /* @__PURE__ */ jsx(
302
- "main",
303
- {
304
- id: "rdx-main",
305
- tabIndex: -1,
306
- className: "h-full w-full flex-1 max-w-[960px]",
307
- children: content
308
- }
309
- )
310
- ] }),
311
- /* @__PURE__ */ jsxs("footer", { className: "flex flex-col", children: [
312
- /* @__PURE__ */ jsxs("section", { className: "flex items-center justify-between", children: [
313
- previous ? /* @__PURE__ */ jsx(
314
- Button,
307
+ previousHref: previous ? hrefFor(previous.slug) : void 0,
308
+ nextHref: next ? hrefFor(next.slug) : void 0,
309
+ children: /* @__PURE__ */ jsxs(
310
+ "section",
311
+ {
312
+ className: `w-full min-h-full bg-background px-7 flex flex-col gap-5 pb-8 md:py-8 ${project.config.TOC ? "xl:pr-20" : ""}`,
313
+ children: [
314
+ /* @__PURE__ */ jsxs(
315
+ "main",
315
316
  {
316
- variant: "outline",
317
- size: "sm",
318
- href: hrefFor(previous.slug),
319
- children: previous.title
317
+ id: "rdx-main",
318
+ tabIndex: -1,
319
+ className: "w-full flex flex-col gap-5 md:mx-auto md:max-w-[860px] pt-14 md:pt-0 flex-1 outline-none",
320
+ children: [
321
+ /* @__PURE__ */ jsxs("header", { className: "mb-6", children: [
322
+ source.badge && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: source.badge === "Archived" ? messages.archivedVersion : source.badge }),
323
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-zinc-600 font-semibold uppercase block -mb-0.5", children: category?.title }),
324
+ /* @__PURE__ */ jsx("h1", { className: "text-4xl uppercase font-bold mb-2", children: title }),
325
+ description && /* @__PURE__ */ jsx("p", { className: "text-sm text-ring max-w-[726px]", children: description })
326
+ ] }),
327
+ /* @__PURE__ */ jsx("div", { className: "h-full w-full flex-1 max-w-[960px]", children: content })
328
+ ]
320
329
  }
321
- ) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: true, children: messages.previous }),
322
- next ? /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", href: hrefFor(next.slug), children: next.title }) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: true, children: messages.next })
323
- ] }),
324
- /* @__PURE__ */ jsx(
325
- FooterCredit,
326
- {
327
- lang: project.config.lang,
328
- name: project.config.metadata.applicationName,
329
- since: project.config.metadata.since
330
- }
331
- )
332
- ] })
333
- ]
330
+ ),
331
+ /* @__PURE__ */ jsxs("footer", { className: "flex flex-col", children: [
332
+ /* @__PURE__ */ jsxs("section", { className: "flex items-center justify-between", children: [
333
+ previous ? /* @__PURE__ */ jsx(
334
+ Button,
335
+ {
336
+ variant: "outline",
337
+ size: "sm",
338
+ href: hrefFor(previous.slug),
339
+ transitionTypes: ["rdx-doc-back"],
340
+ children: previous.title
341
+ }
342
+ ) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: true, children: messages.previous }),
343
+ next ? /* @__PURE__ */ jsx(
344
+ Button,
345
+ {
346
+ variant: "outline",
347
+ size: "sm",
348
+ href: hrefFor(next.slug),
349
+ transitionTypes: ["rdx-doc-forward"],
350
+ children: next.title
351
+ }
352
+ ) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: true, children: messages.next })
353
+ ] }),
354
+ /* @__PURE__ */ jsx(
355
+ FooterCredit,
356
+ {
357
+ lang: project.config.lang,
358
+ name: project.config.metadata.applicationName,
359
+ since: project.config.metadata.since
360
+ }
361
+ )
362
+ ] })
363
+ ]
364
+ }
365
+ )
334
366
  }
335
367
  )
336
368
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexibase/rdx-next",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "The Next.js runtime for RDX documentation projects.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,11 +51,11 @@
51
51
  "next-mdx-remote": "^6.0.0",
52
52
  "remark-gfm": "^4",
53
53
  "typescript": "6.0.3",
54
- "@lexibase/rdx-core": "1.2.0",
55
- "@lexibase/rdx-types": "1.1.1"
54
+ "@lexibase/rdx-core": "1.4.0",
55
+ "@lexibase/rdx-types": "1.2.0"
56
56
  },
57
57
  "peerDependencies": {
58
- "@lexibase/rdx-ui": "^3.0.0",
58
+ "@lexibase/rdx-ui": "^3.2.0",
59
59
  "next": "^16.4.2",
60
60
  "react": "^19.3.1",
61
61
  "react-dom": "^19.3.1"
@@ -80,9 +80,9 @@
80
80
  "next": "^16.4.2",
81
81
  "react": "19.3.1",
82
82
  "react-dom": "19.3.1",
83
- "@rdx/eslint-config": "1.0.0",
83
+ "@lexibase/rdx-ui": "3.2.0",
84
84
  "@rdx/typescript-config": "1.0.0",
85
- "@lexibase/rdx-ui": "3.0.0"
85
+ "@rdx/eslint-config": "1.0.0"
86
86
  },
87
87
  "scripts": {
88
88
  "dev": "tsup src/index.ts src/runtime/index.tsx --format esm --dts --watch --external @lexibase/rdx-core --external @lexibase/rdx-types --external @lexibase/rdx-ui --external next --external react --external react-dom --external next-mdx-remote",
@@ -2,16 +2,4 @@
2
2
  /* Register the entry file directly: consumer ignore rules can hide nested dist folders. */
3
3
  @source '../../dist/runtime/index.js';
4
4
 
5
- /* Colors, buttons, dropdowns and typography come from the shared UI theme. */
6
- .rdx-skip-link {
7
- position: fixed;
8
- top: -100px;
9
- left: 16px;
10
- z-index: 100;
11
- padding: 12px;
12
- background: var(--background);
13
- color: var(--foreground);
14
- }
15
- .rdx-skip-link:focus {
16
- top: 12px;
17
- }
5
+ /* Colors, focus navigation and typography come from the shared UI theme. */