@lexibase/rdx-next 1.0.0-beta.8 → 1.0.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/README.md CHANGED
@@ -2,11 +2,33 @@
2
2
 
3
3
  The official Next.js runtime for RDX documentation projects.
4
4
 
5
+ Requires Node.js 22.18+ (22.x) or 24.11+ (24.x). Use the latest patch of either line.
6
+ The MDX renderer keeps JavaScript expression blocking enabled. Markdown, fenced
7
+ code and registered JSX components with literal props remain supported, but
8
+ expressions such as `{value}` or `style={{ ... }}` are not evaluated. Documentation
9
+ must still be trusted project-owned content, not untrusted uploads.
10
+
5
11
  It exposes the `rdx` command used by generated projects to create their
6
12
  internal `.rdx` application and run Next.js.
7
13
 
14
+ Run `pnpm rdx add versioning` (or `npm exec -- rdx add versioning`) from a
15
+ documentation project to install and activate the official optional plugin.
16
+ The command detects the package manager, preserves config and plugin options,
17
+ validates the result before saving it without creating backups, and prepares
18
+ the runtime. Repeating it does not duplicate or upgrade an installed plugin.
19
+ Use `--package-manager npm|pnpm|yarn` to resolve ambiguous lockfiles. Dynamic
20
+ configurations receive manual instructions and are not automatically edited.
21
+ Installing a package with npm/pnpm/yarn alone never edits `rdx.config.ts`.
22
+
23
+ First plugin installation follows the installed runtime: stable releases use
24
+ `latest`, betas use `beta`, and release candidates use `rc`. Unknown prerelease
25
+ channels require manual installation; missing tags do not fall back to another
26
+ channel. A missing but declared plugin is restored with the manager's `install`
27
+ command, without replacing its dependency range or section. Package-manager
28
+ lockfile rules and security policies still apply normally.
29
+
8
30
  Generated sites use the same theme and components as the development website,
9
- through `@lexibase/rdx-ui`: logo, navbar, Radix version dropdown, mobile/desktop
31
+ through `@lexibase/rdx-ui`: logo, navbar, Base UI version dropdown, mobile/desktop
10
32
  sidebar, landing page, MDX styles and theme provider. The adapter owns content
11
33
  and route data; it does not implement a parallel UI theme.
12
34
 
@@ -18,9 +40,76 @@ explicitly configured plugins enable the shared navbar dropdown.
18
40
  `.rdx/` is generated. Configure the site or edit a project-owned `home.tsx`
19
41
  instead of modifying files inside `.rdx/`.
20
42
 
43
+ The generated project loader supports both the documentation project root and
44
+ the internal `.rdx/` application as the server working directory. This matters
45
+ for serverless Next workers, which can start inside `.rdx/`. Content and version
46
+ manifests stay in the project root; do not copy them into the generated folder
47
+ or initialize versioning again to work around a runtime path error.
48
+
21
49
  Place images, favicon and downloads in the project-root `public/` directory.
22
50
  The CLI links `.rdx/public` to it using a junction on Windows or a directory
23
51
  symlink elsewhere. `/images/logo.svg` serves `public/images/logo.svg` in dev and
24
52
  production. No asset copies are cached and original files are never removed.
25
53
  Restart/redeploy production after adding/removing assets. Documentation versions
26
54
  share this directory; snapshots do not copy its contents.
55
+
56
+ Production builds trace only configured documentation (`.mdx` and
57
+ `_category.json`) and runtime files declared by configured plugins. The versioning
58
+ plugin declares its manifest, including a custom `versionsFile`; current, active
59
+ and archived content sources are included. Live documentation always uses `docs/`;
60
+ custom plugin snapshot paths remain supported without copying the whole project
61
+ into the server deployment.
62
+ Rebuild/redeploy after changing production content or version manifests.
63
+
64
+ Native Next.js applications using the RDX core (such as `apps/web`) must spread
65
+ `await getRdxTracingConfig({ rootDir })` from `@lexibase/rdx-next/config` into
66
+ their Next configuration. If Next runs in a subdirectory, also pass `appDir` and
67
+ set `outputFileTracingRoot` to include the documentation project root. Generated
68
+ `.rdx/next.config.mjs` already does this. Do not remove the explicit tracing
69
+ configuration: the core intentionally does not ask Turbopack to trace arbitrary
70
+ project-relative filesystem paths.
71
+
72
+ ## Site metadata
73
+
74
+ Configure SEO in `rdx.config.ts`; the generated layout uses Next.js's native
75
+ Metadata API. `metadata.applicationName` is the single site name, while
76
+ `description` sets the default site description at the top level.
77
+
78
+ ```ts
79
+ export default defineConfig({
80
+ description: 'Learn how to use our project.',
81
+ metadata: {
82
+ applicationName: 'My documentation',
83
+ metadataBase: 'https://docs.example.com',
84
+ authors: [{ name: 'My team', url: 'https://example.com' }],
85
+ keywords: ['documentation', 'my-project'],
86
+ icons: { icon: '/favicon.ico' },
87
+ robots: { index: true, follow: true },
88
+ openGraph: {
89
+ type: 'website',
90
+ images: [{ url: '/og.png', width: 1200, height: 630, alt: 'My project' }],
91
+ },
92
+ twitter: { card: 'summary_large_image', images: ['/og.png'] },
93
+ },
94
+ })
95
+ ```
96
+
97
+ The starter provides `metadata.applicationName: 'My documentation'`. This name
98
+ is required and supplies browser titles (including the `%s | Name` page title
99
+ template), social preview defaults, accessible logo labels, the footer and the
100
+ built-in homepage fallback. `navbar.title` is independent and optional. Custom
101
+ homepage components own their contents; RDX does not inject this name into them.
102
+ When upgrading, move the former root `title` into `metadata.applicationName`;
103
+ there is no separate `metadata.title` to repeat. Set
104
+ `metadataBase` to your real public URL when deploying; RDX does not invent a
105
+ production domain or force localhost. Image, favicon and manifest paths refer
106
+ to files you create in `public/`. Nothing creates those assets automatically.
107
+
108
+ Supported fields are `metadataBase` (an absolute http(s) string),
109
+ `applicationName`, `authors`, `keywords`, `icons`, `manifest`, `robots`,
110
+ `openGraph` and `twitter`. TypeScript describes the supported nested options,
111
+ and invalid fields fail during configuration validation. This is a portable
112
+ subset, not an unrestricted pass-through of all Next.js metadata options.
113
+ Open Graph and Twitter inherit `metadata.applicationName` and the site description
114
+ unless explicitly overridden within their own options.
115
+ No site-wide canonical or fixed Open Graph URL is emitted for every document.
@@ -0,0 +1,277 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { createRequire } from 'node:module'
3
+ import { lstat, readFile, writeFile, rename, unlink } from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import process from 'node:process'
6
+
7
+ import { createRdxProject, resolveConfig } from '@lexibase/rdx-core/config'
8
+ import spawn from 'cross-spawn'
9
+ import { createJiti } from 'jiti'
10
+
11
+ import { activateVersioning, versioningPackage } from './plugin-config.mjs'
12
+
13
+ const managers = new Set(['npm', 'pnpm', 'yarn'])
14
+ const runtimeVersion = createRequire(import.meta.url)('../package.json').version
15
+
16
+ // Internal installer policy; channels belong to the installed runtime release,
17
+ // not the user's docs version, invoking command or dependency range.
18
+ export function versioningInstallRequest(version, declared) {
19
+ if (declared !== undefined) {
20
+ if (typeof declared !== 'string' || !declared.trim())
21
+ throw new Error(
22
+ 'The declared @lexibase/rdx-versioning dependency must be a non-empty string.'
23
+ )
24
+ return { kind: 'restore' }
25
+ }
26
+ const match =
27
+ typeof version === 'string' &&
28
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/.exec(
29
+ version
30
+ )
31
+ if (
32
+ !match ||
33
+ match[4]
34
+ ?.split('.')
35
+ .some(
36
+ (part) => /^\d+$/.test(part) && part.length > 1 && part.startsWith('0')
37
+ )
38
+ )
39
+ throw new Error(
40
+ 'Cannot select a plugin channel: the RDX runtime version is invalid.'
41
+ )
42
+ const prerelease = match[4]?.split('.')[0]
43
+ if (prerelease && !['beta', 'rc'].includes(prerelease))
44
+ throw new Error(
45
+ `Unsupported RDX runtime prerelease channel "${prerelease}". Install and configure the official plugin manually; no channel fallback was attempted.`
46
+ )
47
+ return {
48
+ kind: 'add',
49
+ specifier: `${versioningPackage}@${prerelease ?? 'latest'}`,
50
+ }
51
+ }
52
+
53
+ export function parseAddArguments(args) {
54
+ const [plugin, ...rest] = args
55
+ if (plugin !== 'versioning')
56
+ throw new Error(
57
+ 'Usage: rdx add versioning [--package-manager npm|pnpm|yarn]. Only the official versioning plugin is currently supported.'
58
+ )
59
+ if (!rest.length) return {}
60
+ if (
61
+ rest.length === 2 &&
62
+ rest[0] === '--package-manager' &&
63
+ managers.has(rest[1])
64
+ )
65
+ return { packageManager: rest[1] }
66
+ throw new Error(
67
+ 'Usage: rdx add versioning [--package-manager npm|pnpm|yarn].'
68
+ )
69
+ }
70
+
71
+ async function exists(filename) {
72
+ try {
73
+ await lstat(filename)
74
+ return true
75
+ } catch (error) {
76
+ if (error.code === 'ENOENT') return false
77
+ throw error
78
+ }
79
+ }
80
+
81
+ export async function detectPackageManager(
82
+ rootDir,
83
+ pkg,
84
+ env = process.env,
85
+ explicit
86
+ ) {
87
+ if (explicit) {
88
+ if (!managers.has(explicit)) throw new Error('Unsupported package manager.')
89
+ return explicit
90
+ }
91
+ if (
92
+ pkg.packageManager !== undefined &&
93
+ typeof pkg.packageManager !== 'string'
94
+ )
95
+ throw new Error('packageManager in package.json must be a string.')
96
+ const declared = pkg.packageManager?.split('@')[0]
97
+ if (declared && !managers.has(declared))
98
+ throw new Error(
99
+ 'Unsupported packageManager in package.json. Choose --package-manager npm, pnpm or yarn explicitly.'
100
+ )
101
+ const locks = []
102
+ for (const [manager, filenames] of [
103
+ ['npm', ['package-lock.json', 'npm-shrinkwrap.json']],
104
+ ['pnpm', ['pnpm-lock.yaml']],
105
+ ['yarn', ['yarn.lock']],
106
+ ]) {
107
+ if (
108
+ (
109
+ await Promise.all(
110
+ filenames.map((filename) => exists(path.join(rootDir, filename)))
111
+ )
112
+ ).some(Boolean)
113
+ )
114
+ locks.push(manager)
115
+ }
116
+ if (locks.length > 1 || (declared && locks.length && locks[0] !== declared))
117
+ throw new Error(
118
+ 'Conflicting package managers. Keep the intended lockfile or choose --package-manager npm, pnpm or yarn explicitly.'
119
+ )
120
+ const agent = env.npm_config_user_agent?.split('/')[0]
121
+ return declared ?? locks[0] ?? (managers.has(agent) ? agent : 'npm')
122
+ }
123
+
124
+ export function installPackage(
125
+ manager,
126
+ rootDir,
127
+ env = process.env,
128
+ request = versioningInstallRequest(runtimeVersion)
129
+ ) {
130
+ return new Promise((resolve, reject) => {
131
+ const args =
132
+ request.kind === 'restore'
133
+ ? ['install']
134
+ : [manager === 'npm' ? 'install' : 'add', request.specifier]
135
+ const nodeEntry =
136
+ env.npm_config_user_agent?.startsWith(`${manager}/`) &&
137
+ env.npm_execpath &&
138
+ /\.[cm]?js$/i.test(env.npm_execpath)
139
+ const child = spawn(
140
+ nodeEntry ? process.execPath : manager,
141
+ nodeEntry ? [env.npm_execpath, ...args] : args,
142
+ { cwd: rootDir, env, stdio: 'inherit' }
143
+ )
144
+ child.once('error', (error) =>
145
+ reject(
146
+ new Error(
147
+ `Could not run ${manager}: ${error.message}. Install it or choose --package-manager explicitly.`
148
+ )
149
+ )
150
+ )
151
+ child.once('exit', (code, signal) => {
152
+ if (code === 0) resolve()
153
+ else
154
+ reject(
155
+ new Error(
156
+ `${manager} installation failed (${signal ?? code}). rdx.config.ts was not changed. Package/lock files may have changed; inspect them before retrying. If pnpm reports a release-age policy error, approve only the trusted RDX release in your project policy.`
157
+ )
158
+ )
159
+ })
160
+ })
161
+ }
162
+
163
+ async function validateCandidate(filename) {
164
+ const jiti = createJiti(import.meta.url, {
165
+ moduleCache: false,
166
+ fsCache: false,
167
+ jsx: { runtime: 'automatic' },
168
+ })
169
+ const input = await jiti.import(filename, { default: true })
170
+ const config = resolveConfig(input)
171
+ // Validate paths and command registrations without running plugin setup hooks.
172
+ await createRdxProject(
173
+ {
174
+ ...config,
175
+ plugins: config.plugins.map((plugin) => ({
176
+ ...plugin,
177
+ setup: undefined,
178
+ })),
179
+ },
180
+ { rootDir: path.dirname(filename), command: 'prepare' }
181
+ )
182
+ if (
183
+ config.plugins.filter((plugin) => plugin.name === versioningPackage)
184
+ .length !== 1
185
+ )
186
+ throw new Error(
187
+ 'Expected exactly one official versioning plugin in the resulting configuration.'
188
+ )
189
+ }
190
+
191
+ export async function addVersioning(rootDir, options = {}) {
192
+ const {
193
+ install = installPackage,
194
+ log = console.log,
195
+ env = process.env,
196
+ } = options
197
+ const filename = path.join(rootDir, 'rdx.config.ts')
198
+ let info
199
+ try {
200
+ info = await lstat(filename)
201
+ } catch (error) {
202
+ if (error.code === 'ENOENT')
203
+ throw new Error(
204
+ 'Missing rdx.config.ts. Run rdx add from the documentation project root.'
205
+ )
206
+ throw error
207
+ }
208
+ if (!info.isFile() || info.isSymbolicLink())
209
+ throw new Error('rdx.config.ts must be a regular project file, not a link.')
210
+ const original = await readFile(filename, 'utf8')
211
+ // Refuse unsupported configurations before running the package manager.
212
+ const updated = activateVersioning(original)
213
+ const pkg = JSON.parse(
214
+ await readFile(path.join(rootDir, 'package.json'), 'utf8')
215
+ )
216
+ const manager = await detectPackageManager(
217
+ rootDir,
218
+ pkg,
219
+ env,
220
+ options.packageManager
221
+ )
222
+ const declared =
223
+ pkg.dependencies?.[versioningPackage] ??
224
+ pkg.devDependencies?.[versioningPackage]
225
+ let installed = false
226
+ try {
227
+ createRequire(path.join(rootDir, 'package.json')).resolve(versioningPackage)
228
+ installed = true
229
+ } catch (error) {
230
+ // The official package exposes ESM imports only. require.resolve can find
231
+ // its manifest but reject the require condition; validation below uses Jiti.
232
+ if (error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') installed = true
233
+ else if (!['MODULE_NOT_FOUND', 'ERR_MODULE_NOT_FOUND'].includes(error.code))
234
+ throw error
235
+ }
236
+ if (!declared || !installed) {
237
+ const request = versioningInstallRequest(runtimeVersion, declared)
238
+ log(
239
+ request.kind === 'restore'
240
+ ? `Restoring the declared ${versioningPackage} dependency with ${manager}...`
241
+ : `Installing ${request.specifier} with ${manager}...`
242
+ )
243
+ await install(manager, rootDir, env, request)
244
+ } else
245
+ log(`${versioningPackage} is already installed; preserving its version.`)
246
+
247
+ const temporary = path.join(rootDir, `rdx.config.add-${randomUUID()}.ts`)
248
+ try {
249
+ await writeFile(temporary, updated, { flag: 'wx', mode: info.mode })
250
+ try {
251
+ await validateCandidate(temporary)
252
+ } catch (error) {
253
+ throw new Error(
254
+ `Plugin configuration validation failed: ${error.message}. rdx.config.ts was not changed. Installed dependencies were kept; fix the error and retry.`
255
+ )
256
+ }
257
+ if ((await readFile(filename, 'utf8')) !== original)
258
+ throw new Error(
259
+ 'rdx.config.ts changed during installation. No configuration was overwritten; retry after reviewing your edits.'
260
+ )
261
+ const currentInfo = await lstat(filename)
262
+ if (!currentInfo.isFile() || currentInfo.isSymbolicLink())
263
+ throw new Error(
264
+ 'rdx.config.ts was replaced during installation. No configuration was overwritten.'
265
+ )
266
+ if (updated !== original) {
267
+ await rename(temporary, filename)
268
+ log('Versioning activated in rdx.config.ts.')
269
+ } else
270
+ log('Versioning is already configured; no configuration changes needed.')
271
+ } finally {
272
+ await unlink(temporary).catch((error) => {
273
+ if (error.code !== 'ENOENT') throw error
274
+ })
275
+ }
276
+ return { changed: updated !== original }
277
+ }
@@ -0,0 +1,155 @@
1
+ import ts from 'typescript'
2
+
3
+ export const versioningPackage = '@lexibase/rdx-versioning'
4
+ const manual = `Cannot safely edit rdx.config.ts. Add "import { versioning } from '${versioningPackage}'" and versioning() to plugins manually, then run rdx prepare.`
5
+
6
+ function unwrap(node) {
7
+ while (
8
+ ts.isParenthesizedExpression(node) ||
9
+ ts.isAsExpression(node) ||
10
+ ts.isSatisfiesExpression(node)
11
+ )
12
+ node = node.expression
13
+ return node
14
+ }
15
+
16
+ // Edit only known literal shapes; never print/reconstruct the user's entire file.
17
+ export function activateVersioning(source) {
18
+ const file = ts.createSourceFile(
19
+ 'rdx.config.ts',
20
+ source,
21
+ ts.ScriptTarget.Latest,
22
+ true,
23
+ ts.ScriptKind.TS
24
+ )
25
+ if (file.parseDiagnostics.length) throw new Error(manual)
26
+ const imports = file.statements.filter(ts.isImportDeclaration)
27
+ const definitions = new Set()
28
+ let factory
29
+ for (const entry of imports) {
30
+ const name = entry.moduleSpecifier.text
31
+ const clause = entry.importClause
32
+ if (!clause || clause.isTypeOnly) continue
33
+ const bindings = clause.namedBindings
34
+ if (bindings && ts.isNamedImports(bindings)) {
35
+ for (const specifier of bindings.elements) {
36
+ if (specifier.isTypeOnly) continue
37
+ const imported = (specifier.propertyName ?? specifier.name).text
38
+ if (
39
+ ['@lexibase/rdx-next', '@lexibase/rdx-core/config'].includes(name) &&
40
+ imported === 'defineConfig'
41
+ )
42
+ definitions.add(specifier.name.text)
43
+ if (name === versioningPackage && imported === 'versioning') {
44
+ if (factory) throw new Error(manual)
45
+ factory = specifier.name.text
46
+ }
47
+ }
48
+ } else if (
49
+ bindings &&
50
+ ts.isNamespaceImport(bindings) &&
51
+ name === versioningPackage
52
+ ) {
53
+ if (factory) throw new Error(manual)
54
+ factory = `${bindings.name.text}.versioning`
55
+ }
56
+ }
57
+ const exports = file.statements.filter(ts.isExportAssignment)
58
+ if (exports.length !== 1 || exports[0].isExportEquals) throw new Error(manual)
59
+ let config = unwrap(exports[0].expression)
60
+ if (ts.isCallExpression(config)) {
61
+ if (
62
+ !ts.isIdentifier(config.expression) ||
63
+ !definitions.has(config.expression.text) ||
64
+ config.arguments.length !== 1
65
+ )
66
+ throw new Error(manual)
67
+ config = unwrap(config.arguments[0])
68
+ }
69
+ if (!ts.isObjectLiteralExpression(config)) throw new Error(manual)
70
+ const properties = config.properties
71
+ if (
72
+ properties.some(
73
+ (entry) =>
74
+ !ts.isPropertyAssignment(entry) || ts.isComputedPropertyName(entry.name)
75
+ )
76
+ )
77
+ throw new Error(manual)
78
+ const pluginsProperties = properties.filter(
79
+ (entry) => entry.name.text === 'plugins'
80
+ )
81
+ if (pluginsProperties.length > 1) throw new Error(manual)
82
+ const plugins = pluginsProperties.length
83
+ ? unwrap(pluginsProperties[0].initializer)
84
+ : undefined
85
+ if (
86
+ plugins &&
87
+ (!ts.isArrayLiteralExpression(plugins) ||
88
+ plugins.elements.some(ts.isSpreadElement))
89
+ )
90
+ throw new Error(manual)
91
+ if (plugins && factory) {
92
+ const matches = plugins.elements.filter(
93
+ (entry) =>
94
+ ts.isCallExpression(unwrap(entry)) &&
95
+ unwrap(entry).expression.getText(file) === factory
96
+ )
97
+ if (matches.length > 1)
98
+ throw new Error(
99
+ 'Versioning is configured more than once. Remove the duplicate plugin before retrying.'
100
+ )
101
+ if (matches.length === 1) return source
102
+ }
103
+ const newline = source.includes('\r\n') ? '\r\n' : '\n'
104
+ const edits = []
105
+ if (!factory) {
106
+ const identifiers = new Set()
107
+ function visit(node) {
108
+ if (ts.isIdentifier(node)) identifiers.add(node.text)
109
+ ts.forEachChild(node, visit)
110
+ }
111
+ visit(file)
112
+ factory = 'versioning'
113
+ let suffix = 1
114
+ while (identifiers.has(factory)) factory = `rdxVersioning${suffix++}`
115
+ const specifier =
116
+ factory === 'versioning' ? 'versioning' : `versioning as ${factory}`
117
+ let position = imports.length
118
+ ? imports.at(-1).end
119
+ : file.statements[0].getStart(file)
120
+ if (!imports.length) {
121
+ for (const statement of file.statements) {
122
+ if (
123
+ !ts.isExpressionStatement(statement) ||
124
+ !ts.isStringLiteral(statement.expression)
125
+ )
126
+ break
127
+ position = statement.end
128
+ }
129
+ }
130
+ edits.push({
131
+ position,
132
+ text: `${newline}import { ${specifier} } from '${versioningPackage}'${newline}`,
133
+ })
134
+ }
135
+ if (plugins) {
136
+ const position = plugins.elements.length
137
+ ? plugins.elements.at(-1).end
138
+ : plugins.getStart(file) + 1
139
+ edits.push({
140
+ position,
141
+ text: `${plugins.elements.length ? ',' : ''} ${factory}()`,
142
+ })
143
+ } else {
144
+ if (properties.length && !properties.hasTrailingComma)
145
+ edits.push({ position: properties.at(-1).end, text: ',' })
146
+ edits.push({
147
+ position: config.end - 1,
148
+ text: `${newline} plugins: [${factory}()],${newline}`,
149
+ })
150
+ }
151
+ for (const edit of edits.sort((a, b) => b.position - a.position))
152
+ source =
153
+ source.slice(0, edit.position) + edit.text + source.slice(edit.position)
154
+ return source
155
+ }
package/bin/rdx.mjs CHANGED
@@ -13,7 +13,7 @@ import { createJiti } from 'jiti'
13
13
  import { preparePublicAssets } from './public-assets.mjs'
14
14
 
15
15
  const supportedCommands = new Set(['dev', 'build', 'start'])
16
- const command = process.argv[2]
16
+ let command = process.argv[2]
17
17
  const commandArgs = process.argv.slice(3)
18
18
  const projectRoot = process.cwd()
19
19
  const packageRoot = path.resolve(
@@ -31,13 +31,22 @@ async function prepareRuntime() {
31
31
 
32
32
  async function main() {
33
33
  if (!command || command === 'help' || command === '--help') {
34
- console.log('Usage: rdx <dev|build|start|prepare|plugin-command>')
34
+ console.log('Usage: rdx <dev|build|start|prepare|add|plugin-command>')
35
+ console.log(' rdx add versioning [--package-manager npm|pnpm|yarn]')
35
36
  console.log(
36
37
  'Plugin commands are registered through plugins in rdx.config.ts.'
37
38
  )
38
39
  return
39
40
  }
40
41
 
42
+ if (command === 'add') {
43
+ const { addVersioning, parseAddArguments } =
44
+ await import('./add-plugin.mjs')
45
+ const options = parseAddArguments(commandArgs)
46
+ await addVersioning(projectRoot, options)
47
+ command = 'prepare'
48
+ }
49
+
41
50
  const configPath = path.join(projectRoot, 'rdx.config.ts')
42
51
  try {
43
52
  await access(configPath)
@@ -78,7 +87,7 @@ async function main() {
78
87
  )
79
88
  if (!pluginCommand)
80
89
  throw new Error(
81
- `Unknown command "${command}". Available: dev, build, start, prepare${project.commands.map((entry) => `, ${entry.name}`).join('')}. Install and configure the plugin that provides this command.`
90
+ `Unknown command "${command}". Available: dev, build, start, prepare, add${project.commands.map((entry) => `, ${entry.name}`).join('')}. Install and configure the plugin that provides this command.`
82
91
  )
83
92
  await pluginCommand.run({
84
93
  rootDir: projectRoot,
package/config.d.mts ADDED
@@ -0,0 +1,5 @@
1
+ /** Explicit deployment files for a native Next.js application using RDX. */
2
+ export function getRdxTracingConfig(options: {
3
+ rootDir: string
4
+ appDir?: string
5
+ }): Promise<{ outputFileTracingIncludes: Record<string, string[]> }>
package/config.mjs ADDED
@@ -0,0 +1,80 @@
1
+ import { readdir, stat } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import { createRdxProject, resolveProjectPath } from '@lexibase/rdx-core/config'
5
+ import { createJiti } from 'jiti'
6
+
7
+ // These files are enumerated by Next's configuration loader, not by a server
8
+ // bundle. The core's dynamic readers deliberately leave tracing to this adapter.
9
+ async function contentFiles(directory) {
10
+ let entries
11
+ try {
12
+ entries = await readdir(directory, { withFileTypes: true })
13
+ } catch (error) {
14
+ if (error.code === 'ENOENT') return []
15
+ throw error
16
+ }
17
+ const files = await Promise.all(
18
+ entries.map(async (entry) => {
19
+ const filename = path.join(directory, entry.name)
20
+ if (entry.isDirectory()) return contentFiles(filename)
21
+ return entry.isFile() &&
22
+ (entry.name.endsWith('.mdx') || entry.name === '_category.json')
23
+ ? [filename]
24
+ : []
25
+ })
26
+ )
27
+ return files.flat()
28
+ }
29
+
30
+ // outputFileTracingIncludes takes globs. Escape configured names so a literal
31
+ // bracket, wildcard or parenthesis cannot silently widen the deployment trace.
32
+ function literalPattern(filename) {
33
+ return filename
34
+ .split(path.sep)
35
+ .join('/')
36
+ .replace(/([*?[\]{}()!+@])/g, '\\$1')
37
+ }
38
+
39
+ /** Next.js tracing for configured RDX content, independent of plugin names. */
40
+ export async function getRdxTracingConfig({ rootDir, appDir = rootDir }) {
41
+ rootDir = path.resolve(rootDir)
42
+ appDir = path.resolve(appDir)
43
+ const jiti = createJiti(import.meta.url, {
44
+ moduleCache: false,
45
+ fsCache: false,
46
+ jsx: { runtime: 'automatic' },
47
+ })
48
+ const input = await jiti.import(path.join(rootDir, 'rdx.config.ts'), {
49
+ default: true,
50
+ })
51
+ const project = await createRdxProject(input, { rootDir })
52
+ const files = (
53
+ await Promise.all(
54
+ project.sources.map((source) =>
55
+ contentFiles(
56
+ resolveProjectPath(rootDir, source.docsDir, 'content source docsDir')
57
+ )
58
+ )
59
+ )
60
+ ).flat()
61
+ for (const filename of project.runtimeFiles) {
62
+ const resolved = resolveProjectPath(
63
+ rootDir,
64
+ filename,
65
+ 'plugin runtimeFiles'
66
+ )
67
+ if (!(await stat(resolved)).isFile())
68
+ throw new Error(`Plugin runtimeFiles must name a file: ${filename}`)
69
+ files.push(resolved)
70
+ }
71
+ return {
72
+ outputFileTracingIncludes: {
73
+ '/*': [
74
+ ...new Set(
75
+ files.map((file) => literalPattern(path.relative(appDir, file)))
76
+ ),
77
+ ].sort(),
78
+ },
79
+ }
80
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { defineConfig } from '@lexibase/rdx-core/config';
2
- export { RdxConfig, RdxHomepage, RdxPlugin, RdxPluginContext, RdxRuntimeConfig } from '@lexibase/rdx-types';
2
+ export { RDXConfig, RdxHomepage, RdxMetadata, RdxPlugin, RdxPluginContext, RdxRuntimeConfig } from '@lexibase/rdx-types';
@@ -1,19 +1,28 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { RdxProject, RdxRuntimeConfig } from '@lexibase/rdx-types';
3
+ import { RdxProject, RDXConfig } from '@lexibase/rdx-types';
4
4
  export { createRdxProject, getProjectDocsPaths } from '@lexibase/rdx-core';
5
+ import { Metadata } from 'next';
6
+
7
+ declare function getSearchSources(project: RdxProject): {
8
+ source: string;
9
+ }[];
10
+ declare function getSearchResponse(project: RdxProject, key: string): Response;
11
+
12
+ /** Translate the shared configuration through Next's native Metadata API. */
13
+ declare function getRdxMetadata(config: RDXConfig): Metadata;
5
14
 
6
15
  declare function RdxLayout({ children, config, bodyClassName, }: {
7
16
  children: ReactNode;
8
- config: RdxRuntimeConfig;
17
+ config: RDXConfig;
9
18
  bodyClassName?: string;
10
- }): react_jsx_runtime.JSX.Element;
19
+ }): react.JSX.Element;
11
20
  declare function HomePage({ project }: {
12
21
  project: RdxProject;
13
- }): react_jsx_runtime.JSX.Element;
22
+ }): react.JSX.Element;
14
23
  declare function DocsPage({ slug, project, }: {
15
24
  slug?: string[];
16
25
  project: RdxProject;
17
- }): Promise<react_jsx_runtime.JSX.Element>;
26
+ }): Promise<react.JSX.Element>;
18
27
 
19
- export { DocsPage, HomePage, RdxLayout };
28
+ export { DocsPage, HomePage, RdxLayout, getRdxMetadata, getSearchResponse, getSearchSources };
@@ -1,12 +1,15 @@
1
1
  // src/runtime/index.tsx
2
2
  import {
3
- getDocument,
3
+ getDocument as getDocument2,
4
4
  getDocsPaths,
5
- getSourceOptions,
5
+ getSourceOptions as getSourceOptions2,
6
6
  resolveDocsRoute
7
7
  } from "@lexibase/rdx-core";
8
8
  import { Button } from "@lexibase/rdx-ui/components/button";
9
+ import { ButtonLink } from "@lexibase/rdx-ui/components/button-link";
9
10
  import { DocsSidebar } from "@lexibase/rdx-ui/components/docs-sidebar";
11
+ import { DocsToc } from "@lexibase/rdx-ui/components/docs-toc";
12
+ import { DocsSearch } from "@lexibase/rdx-ui/components/docs-search";
10
13
  import { Home } from "@lexibase/rdx-ui/components/home";
11
14
  import { Logo } from "@lexibase/rdx-ui/components/logo";
12
15
  import { mdxComponents } from "@lexibase/rdx-ui/components/mdx-components";
@@ -15,10 +18,90 @@ import {
15
18
  VersionDropdown
16
19
  } from "@lexibase/rdx-ui/components/version-dropdown";
17
20
  import { ThemeProvider } from "@lexibase/rdx-ui/providers/theme-provider";
18
- import { compileMDX } from "next-mdx-remote/rsc";
19
- import Link from "next/link";
20
21
  import { notFound, redirect } from "next/navigation";
22
+
23
+ // src/runtime/mdx.ts
24
+ import { compileMDX } from "next-mdx-remote/rsc";
25
+ import remarkGfm from "remark-gfm";
26
+ import {
27
+ remarkDocumentLinks,
28
+ rehypeDocumentationHeadings,
29
+ rehypeDocumentationCode
30
+ } from "@lexibase/rdx-core/mdx";
31
+ function compileDocumentation(source, components, links) {
32
+ return compileMDX({
33
+ source,
34
+ components,
35
+ options: {
36
+ blockJS: true,
37
+ blockDangerousJS: true,
38
+ mdxOptions: {
39
+ remarkPlugins: [remarkGfm, [remarkDocumentLinks, links]],
40
+ rehypePlugins: [rehypeDocumentationHeadings, rehypeDocumentationCode]
41
+ }
42
+ }
43
+ });
44
+ }
45
+
46
+ // src/runtime/index.tsx
21
47
  import { createRdxProject, getProjectDocsPaths } from "@lexibase/rdx-core";
48
+
49
+ // src/runtime/search.ts
50
+ import { getDocsIndex, getDocument, getSourceOptions } from "@lexibase/rdx-core";
51
+ import { buildSearchIndex } from "@lexibase/rdx-core/search";
52
+ function getSearchSources(project) {
53
+ return project.config.search ? project.sources.map((source) => ({ source: source.routePrefix || "_" })) : [];
54
+ }
55
+ function getSearchResponse(project, key) {
56
+ if (!project.config.search) return new Response(null, { status: 404 });
57
+ const source = project.sources.find(
58
+ (entry) => (entry.routePrefix || "_") === key
59
+ );
60
+ if (!source) return new Response(null, { status: 404 });
61
+ const options = getSourceOptions(project, source);
62
+ const documents = getDocsIndex(options).flatMap(
63
+ (category) => category.pages.map((page) => ({
64
+ title: page.title,
65
+ description: page.description,
66
+ category: category.title,
67
+ href: [
68
+ "/docs",
69
+ source.routePrefix,
70
+ page.slug.split("/").map(encodeURIComponent).join("/")
71
+ ].filter(Boolean).join("/"),
72
+ content: getDocument(page.slug, options)?.content ?? ""
73
+ }))
74
+ );
75
+ return Response.json(buildSearchIndex(documents));
76
+ }
77
+
78
+ // src/runtime/metadata.ts
79
+ function getRdxMetadata(config) {
80
+ const { metadataBase, ...metadata } = config.metadata;
81
+ const name = metadata.applicationName;
82
+ return {
83
+ title: { default: name, template: `%s | ${name}` },
84
+ description: config.description,
85
+ ...metadata,
86
+ ...metadataBase ? { metadataBase: new URL(metadataBase) } : {},
87
+ ...metadata.openGraph ? {
88
+ openGraph: {
89
+ title: name,
90
+ description: config.description,
91
+ ...metadata.openGraph
92
+ }
93
+ } : {},
94
+ ...metadata.twitter ? {
95
+ twitter: {
96
+ title: name,
97
+ description: config.description,
98
+ ...metadata.twitter
99
+ }
100
+ } : {}
101
+ };
102
+ }
103
+
104
+ // src/runtime/index.tsx
22
105
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
23
106
  function RdxLayout({
24
107
  children,
@@ -46,8 +129,8 @@ function RdxHeader({
46
129
  const { config } = project;
47
130
  const { source, slug: currentSlug } = resolveDocsRoute(project, slug);
48
131
  const options = project.sources.map((edition) => {
49
- const docs = getSourceOptions(project, edition);
50
- const destination = currentSlug && getDocument(currentSlug, docs) ? currentSlug : getDocsPaths(docs)[0];
132
+ const docs = getSourceOptions2(project, edition);
133
+ const destination = currentSlug && getDocument2(currentSlug, docs) ? currentSlug : getDocsPaths(docs)[0];
51
134
  return {
52
135
  id: edition.id,
53
136
  label: edition.id === "current" ? "Current" : `Version ${edition.label}`,
@@ -60,7 +143,14 @@ function RdxHeader({
60
143
  Navbar,
61
144
  {
62
145
  title: config.navbar?.title ?? "",
63
- siteTitle: config.title,
146
+ search: config.search ? /* @__PURE__ */ jsx(
147
+ DocsSearch,
148
+ {
149
+ indexUrl: `/rdx-search/${encodeURIComponent(source.routePrefix || "_")}`,
150
+ version: source.label
151
+ }
152
+ ) : void 0,
153
+ siteTitle: config.metadata.applicationName,
64
154
  logo: /* @__PURE__ */ jsx(BrandLogo, { className: "size-5" }),
65
155
  homeHref: config.homepage?.enabled ? "/" : "/docs",
66
156
  docsHref: ["/docs", source.routePrefix].filter(Boolean).join("/"),
@@ -87,7 +177,7 @@ function HomePage({ project }) {
87
177
  children: CustomHome ? /* @__PURE__ */ jsx(CustomHome, {}) : /* @__PURE__ */ jsx(
88
178
  Home,
89
179
  {
90
- title: homepage.title ?? config.title,
180
+ title: homepage.title ?? config.metadata.applicationName,
91
181
  logo: /* @__PURE__ */ jsx(BrandLogo, { className: "size-52 hover:scale-105 hover:rotate-1 transition-[scale,rotation] duration-1000" }),
92
182
  description: homepage.description,
93
183
  buttons: homepage.buttons
@@ -116,10 +206,16 @@ async function DocsPage({
116
206
  ] })
117
207
  ] });
118
208
  if (!document) notFound();
119
- const { content } = await compileMDX({
120
- source: document.content,
121
- components: mdxComponents
122
- });
209
+ const { content } = await compileDocumentation(
210
+ document.content,
211
+ mdxComponents,
212
+ {
213
+ sourceRoot: getSourceOptions2(project, source).docsDir,
214
+ sourcePath: document.sourcePath,
215
+ routePrefix: source.routePrefix,
216
+ documents: categories.flatMap((category2) => category2.pages)
217
+ }
218
+ );
123
219
  const title = typeof document.frontmatter.title === "string" ? document.frontmatter.title : currentSlug;
124
220
  const description = typeof document.frontmatter.description === "string" ? document.frontmatter.description : null;
125
221
  const hrefFor = (pageSlug) => ["/docs", source.routePrefix, pageSlug].filter(Boolean).join("/");
@@ -147,37 +243,69 @@ async function DocsPage({
147
243
  pathname: hrefFor(currentSlug)
148
244
  }
149
245
  ),
150
- /* @__PURE__ */ jsx("div", { className: "flex-1 h-full overflow-y-auto pt-12 md:pt-0 outline-none", children: /* @__PURE__ */ jsxs("section", { className: "w-full min-h-full px-7 flex flex-col gap-5 pb-8 md:py-8", children: [
151
- /* @__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: [
152
- /* @__PURE__ */ jsxs("header", { className: "mb-6", children: [
153
- source.badge && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: source.badge }),
154
- /* @__PURE__ */ jsx("span", { className: "text-xs text-zinc-600 font-semibold uppercase block -mb-0.5", children: category?.title }),
155
- /* @__PURE__ */ jsx("h1", { className: "text-4xl uppercase font-bold mb-2", children: title }),
156
- description && /* @__PURE__ */ jsx("p", { className: "text-sm text-ring max-w-[726px]", children: description })
157
- ] }),
158
- /* @__PURE__ */ jsx(
159
- "main",
160
- {
161
- id: "rdx-main",
162
- tabIndex: -1,
163
- className: "h-full w-full flex-1 max-w-[960px]",
164
- children: content
165
- }
166
- )
167
- ] }),
168
- /* @__PURE__ */ jsxs("footer", { className: "flex flex-col", children: [
169
- /* @__PURE__ */ jsxs("section", { className: "flex items-center justify-between", children: [
170
- previous ? /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", asChild: true, children: /* @__PURE__ */ jsx(Link, { href: hrefFor(previous.slug), children: previous.title }) }) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: true, children: "Previous" }),
171
- next ? /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", asChild: true, children: /* @__PURE__ */ jsx(Link, { href: hrefFor(next.slug), children: next.title }) }) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: true, children: "Next" })
172
- ] }),
173
- /* @__PURE__ */ jsx("section", { className: "flex items-center justify-center w-full py-5", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
174
- "\xA9 ",
175
- (/* @__PURE__ */ new Date()).getFullYear(),
176
- " ",
177
- /* @__PURE__ */ jsx("strong", { children: project.config.title })
178
- ] }) })
179
- ] })
180
- ] }) })
246
+ /* @__PURE__ */ jsxs(
247
+ "div",
248
+ {
249
+ "data-rdx-docs-scroll": true,
250
+ className: "relative flex-1 min-w-0 h-full overflow-y-auto pt-12 md:pt-0 outline-none",
251
+ children: [
252
+ project.config.TOC && /* @__PURE__ */ jsx(DocsToc, {}, hrefFor(currentSlug)),
253
+ /* @__PURE__ */ jsxs(
254
+ "section",
255
+ {
256
+ className: `w-full min-h-full px-7 flex flex-col gap-5 pb-8 md:py-8 ${project.config.TOC ? "xl:pr-20" : ""}`,
257
+ children: [
258
+ /* @__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: [
259
+ /* @__PURE__ */ jsxs("header", { className: "mb-6", children: [
260
+ source.badge && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: source.badge }),
261
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-zinc-600 font-semibold uppercase block -mb-0.5", children: category?.title }),
262
+ /* @__PURE__ */ jsx("h1", { className: "text-4xl uppercase font-bold mb-2", children: title }),
263
+ description && /* @__PURE__ */ jsx("p", { className: "text-sm text-ring max-w-[726px]", children: description })
264
+ ] }),
265
+ /* @__PURE__ */ jsx(
266
+ "main",
267
+ {
268
+ id: "rdx-main",
269
+ tabIndex: -1,
270
+ className: "h-full w-full flex-1 max-w-[960px]",
271
+ children: content
272
+ }
273
+ )
274
+ ] }),
275
+ /* @__PURE__ */ jsxs("footer", { className: "flex flex-col", children: [
276
+ /* @__PURE__ */ jsxs("section", { className: "flex items-center justify-between", children: [
277
+ previous ? /* @__PURE__ */ jsx(
278
+ ButtonLink,
279
+ {
280
+ variant: "outline",
281
+ size: "sm",
282
+ href: hrefFor(previous.slug),
283
+ children: previous.title
284
+ }
285
+ ) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: true, children: "Previous" }),
286
+ next ? /* @__PURE__ */ jsx(
287
+ ButtonLink,
288
+ {
289
+ variant: "outline",
290
+ size: "sm",
291
+ href: hrefFor(next.slug),
292
+ children: next.title
293
+ }
294
+ ) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", disabled: true, children: "Next" })
295
+ ] }),
296
+ /* @__PURE__ */ jsx("section", { className: "flex items-center justify-center w-full py-5", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
297
+ "\xA9 ",
298
+ (/* @__PURE__ */ new Date()).getFullYear(),
299
+ " ",
300
+ /* @__PURE__ */ jsx("strong", { children: project.config.metadata.applicationName })
301
+ ] }) })
302
+ ] })
303
+ ]
304
+ }
305
+ )
306
+ ]
307
+ }
308
+ )
181
309
  ] })
182
310
  ] });
183
311
  }
@@ -186,5 +314,8 @@ export {
186
314
  HomePage,
187
315
  RdxLayout,
188
316
  createRdxProject,
189
- getProjectDocsPaths
317
+ getProjectDocsPaths,
318
+ getRdxMetadata,
319
+ getSearchResponse,
320
+ getSearchSources
190
321
  };
package/package.json CHANGED
@@ -1,15 +1,25 @@
1
1
  {
2
2
  "name": "@lexibase/rdx-next",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0",
4
4
  "description": "The Next.js runtime for RDX documentation projects.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "engines": {
8
+ "node": "^22.18.0 || ^24.11.0"
9
+ },
7
10
  "bin": {
8
11
  "rdx": "./bin/rdx.mjs"
9
12
  },
10
13
  "exports": {
14
+ "./config": {
15
+ "types": "./config.d.mts",
16
+ "import": "./config.mjs"
17
+ },
11
18
  "./postcss.config": "./postcss.config.mjs",
12
- "./styles.css": "./src/runtime/styles.css",
19
+ "./styles.css": {
20
+ "types": "./src/runtime/styles.d.css.ts",
21
+ "default": "./src/runtime/styles.css"
22
+ },
13
23
  ".": {
14
24
  "types": "./dist/index.d.ts",
15
25
  "import": "./dist/index.js"
@@ -20,24 +30,30 @@
20
30
  }
21
31
  },
22
32
  "files": [
33
+ "config.mjs",
34
+ "config.d.mts",
23
35
  "bin",
24
36
  "dist",
25
37
  "templates",
26
38
  "postcss.config.mjs",
27
39
  "src/runtime/styles.css",
40
+ "src/runtime/styles.d.css.ts",
28
41
  "README.md"
29
42
  ],
30
43
  "dependencies": {
44
+ "cross-spawn": "^7.0.6",
31
45
  "jiti": "^2.6.1",
32
- "next-mdx-remote": "^5.0.0",
33
- "@lexibase/rdx-ui": "1.0.0-beta.1",
34
- "@lexibase/rdx-types": "1.0.0-beta.4",
35
- "@lexibase/rdx-core": "1.0.0-beta.5"
46
+ "next-mdx-remote": "^6.0.0",
47
+ "remark-gfm": "^4",
48
+ "typescript": "6.0.3",
49
+ "@lexibase/rdx-core": "1.0.0",
50
+ "@lexibase/rdx-ui": "1.0.0",
51
+ "@lexibase/rdx-types": "1.0.0"
36
52
  },
37
53
  "peerDependencies": {
38
- "next": "^15.5.0",
39
- "react": "^19.1.0",
40
- "react-dom": "^19.1.0"
54
+ "next": "^16.4.2",
55
+ "react": "^19.3.1",
56
+ "react-dom": "^19.3.1"
41
57
  },
42
58
  "publishConfig": {
43
59
  "access": "public"
@@ -53,17 +69,17 @@
53
69
  "homepage": "https://github.com/lexibase/rdx#readme",
54
70
  "devDependencies": {
55
71
  "@types/node": "^22.15.3",
56
- "@types/react": "19.1.0",
57
- "@types/react-dom": "19.1.1",
58
- "eslint": "^9.34.0",
59
- "next": "^15.5.0",
60
- "react": "19.1.0",
61
- "react-dom": "19.1.0",
62
- "typescript": "5.9.2",
72
+ "@types/react": "19.3.0",
73
+ "@types/react-dom": "19.3.0",
74
+ "eslint": "^9.39.5",
75
+ "next": "^16.4.2",
76
+ "react": "19.3.1",
77
+ "react-dom": "19.3.1",
63
78
  "@rdx/typescript-config": "1.0.0",
64
79
  "@rdx/eslint-config": "1.0.0"
65
80
  },
66
81
  "scripts": {
82
+ "dev": "tsup src/index.ts src/runtime/index.tsx --format esm --dts --watch --external @lexibase/rdx-core --external @lexibase/rdx-types --external next --external react --external react-dom --external next-mdx-remote",
67
83
  "build": "tsup src/index.ts src/runtime/index.tsx --format esm --dts --clean --external @lexibase/rdx-core --external @lexibase/rdx-types --external next --external react --external react-dom --external next-mdx-remote",
68
84
  "lint": "eslint . --max-warnings 0",
69
85
  "check-types": "tsc --noEmit",
@@ -0,0 +1,2 @@
1
+ // Global runtime styles are loaded for their side effects only.
2
+ export {}
@@ -0,0 +1,2 @@
1
+ // Next.js loads this global stylesheet for its side effects only.
2
+ export {}
@@ -1,6 +1,6 @@
1
1
  import type { Metadata } from 'next'
2
2
 
3
- import { RdxLayout } from '@lexibase/rdx-next/runtime'
3
+ import { getRdxMetadata, RdxLayout } from '@lexibase/rdx-next/runtime'
4
4
 
5
5
  import { getProject } from '../rdx-project'
6
6
 
@@ -8,10 +8,7 @@ import './globals.css'
8
8
 
9
9
  export async function generateMetadata(): Promise<Metadata> {
10
10
  const { config } = await getProject()
11
- return {
12
- title: { default: config.title, template: `%s | ${config.title}` },
13
- description: config.description,
14
- }
11
+ return getRdxMetadata(config)
15
12
  }
16
13
 
17
14
  export default async function RootLayout({
@@ -0,0 +1,16 @@
1
+ import { getSearchResponse, getSearchSources } from '@lexibase/rdx-next/runtime'
2
+ import { getProject } from '../../../rdx-project'
3
+
4
+ export const dynamic = 'force-static'
5
+ export const dynamicParams = false
6
+
7
+ export async function generateStaticParams() {
8
+ return getSearchSources(await getProject())
9
+ }
10
+
11
+ export async function GET(
12
+ _request: Request,
13
+ { params }: { params: Promise<{ source: string }> }
14
+ ) {
15
+ return getSearchResponse(await getProject(), (await params).source)
16
+ }
@@ -1,7 +1,17 @@
1
+ import { fileURLToPath } from 'node:url'
2
+
3
+ import { getRdxTracingConfig } from '@lexibase/rdx-next/config'
4
+
5
+ const rootDir = fileURLToPath(new URL('..', import.meta.url))
6
+ const appDir = fileURLToPath(new URL('.', import.meta.url))
7
+
1
8
  /** @type {import('next').NextConfig} */
2
9
  const nextConfig = {
3
- experimental: {
4
- externalDir: true,
10
+ ...(await getRdxTracingConfig({ rootDir, appDir })),
11
+ outputFileTracingRoot: rootDir,
12
+ agentRules: false,
13
+ turbopack: {
14
+ root: fileURLToPath(new URL('..', import.meta.url)),
5
15
  },
6
16
  transpilePackages: [
7
17
  '@lexibase/rdx-core',
@@ -1,7 +1,13 @@
1
+ import path from 'node:path'
2
+
1
3
  import { createRdxProject } from '@lexibase/rdx-next/runtime'
2
4
 
3
5
  import config from '../rdx.config'
4
6
 
5
7
  export function getProject() {
6
- return createRdxProject(config)
8
+ // The CLI starts at the project root; serverless Next workers start in .rdx.
9
+ // Keep project-owned content outside the generated application in both cases.
10
+ const cwd = process.cwd()
11
+ const rootDir = path.basename(cwd) === '.rdx' ? path.dirname(cwd) : cwd
12
+ return createRdxProject(config, { rootDir })
7
13
  }
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "rootDir": "..",
4
+ "types": ["node", "react"],
5
+ "allowArbitraryExtensions": true,
6
+ "noUncheckedSideEffectImports": true,
3
7
  "allowJs": true,
4
8
  "esModuleInterop": true,
5
9
  "incremental": true,
6
10
  "isolatedModules": true,
7
- "jsx": "preserve",
11
+ "jsx": "react-jsx",
8
12
  "lib": ["dom", "dom.iterable", "esnext"],
9
13
  "module": "esnext",
10
14
  "moduleResolution": "bundler",
@@ -15,6 +19,12 @@
15
19
  "strict": true,
16
20
  "target": "esnext"
17
21
  },
18
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
22
+ "include": [
23
+ "next-env.d.ts",
24
+ "**/*.ts",
25
+ "**/*.tsx",
26
+ ".next/types/**/*.ts",
27
+ ".next/dev/types/**/*.ts"
28
+ ],
19
29
  "exclude": ["node_modules", "public"]
20
30
  }