@lexibase/rdx-next 4.0.0 → 4.1.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 +9 -0
- package/bin/add-plugin.mjs +111 -35
- package/bin/cli.mjs +34 -7
- package/bin/i18n-setup.mjs +115 -0
- package/bin/locale-routes.mjs +138 -0
- package/bin/plugin-config.mjs +47 -13
- package/config.mjs +27 -6
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +193 -31
- package/package.json +8 -7
- package/templates/runtime/app/globals.css +1 -0
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ internal `.rdx` application and run Next.js.
|
|
|
19
19
|
Run `pnpm rdx check` to validate static documentation links, heading fragments
|
|
20
20
|
and local files in public/ before deployment. It reports source locations and
|
|
21
21
|
exits nonzero for problems, without preparing the runtime or rendering documents.
|
|
22
|
+
`rdx build` runs the same check automatically before runtime preparation and
|
|
23
|
+
Next.js compilation, and stops if problems are found.
|
|
22
24
|
|
|
23
25
|
Run `pnpm rdx add versioning` (or `npm exec -- rdx add versioning`) from a
|
|
24
26
|
documentation project to install and activate the official optional plugin.
|
|
@@ -29,6 +31,13 @@ Use `--package-manager npm|pnpm|yarn` to resolve ambiguous lockfiles. Dynamic
|
|
|
29
31
|
configurations receive manual instructions and are not automatically edited.
|
|
30
32
|
Installing a package with npm/pnpm/yarn alone never edits `rdx.config.ts`.
|
|
31
33
|
|
|
34
|
+
Run `pnpm rdx add i18n` to activate multilingual documentation. It reuses the
|
|
35
|
+
primary `lang`, asks for additional language tags and creates translation
|
|
36
|
+
directories with `.gitkeep` files for empty trees. `--locales en,es-ES` supplies
|
|
37
|
+
additional languages noninteractively. Existing configurations and authored
|
|
38
|
+
translations are preserved; add later languages directly to `locales` and
|
|
39
|
+
`locale/<folder>/docs/`. The main project creator asks only for a project name.
|
|
40
|
+
|
|
32
41
|
First plugin installation follows the installed runtime: stable releases use
|
|
33
42
|
`latest`, betas use `beta`, and release candidates use `rc`. Unknown prerelease
|
|
34
43
|
channels require manual installation; missing tags do not fall back to another
|
package/bin/add-plugin.mjs
CHANGED
|
@@ -8,7 +8,18 @@ import { createRdxProject, resolveConfig } from '@lexibase/rdx-core/config'
|
|
|
8
8
|
import spawn from 'cross-spawn'
|
|
9
9
|
import { createJiti } from 'jiti'
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
activateVersioning,
|
|
13
|
+
versioningPackage,
|
|
14
|
+
activateI18n,
|
|
15
|
+
inspectI18n,
|
|
16
|
+
i18nPackage,
|
|
17
|
+
} from './plugin-config.mjs'
|
|
18
|
+
import {
|
|
19
|
+
askI18nLanguages,
|
|
20
|
+
initialLocales,
|
|
21
|
+
prepareLanguageFolders,
|
|
22
|
+
} from './i18n-setup.mjs'
|
|
12
23
|
|
|
13
24
|
const managers = new Set(['npm', 'pnpm', 'yarn'])
|
|
14
25
|
const runtimeVersion = createRequire(import.meta.url)('../package.json').version
|
|
@@ -16,10 +27,18 @@ const runtimeVersion = createRequire(import.meta.url)('../package.json').version
|
|
|
16
27
|
// Internal installer policy; channels belong to the installed runtime release,
|
|
17
28
|
// not the user's docs version, invoking command or dependency range.
|
|
18
29
|
export function versioningInstallRequest(version, declared) {
|
|
30
|
+
return pluginInstallRequest(version, declared, versioningPackage)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function i18nInstallRequest(version, declared) {
|
|
34
|
+
return pluginInstallRequest(version, declared, i18nPackage)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pluginInstallRequest(version, declared, packageName) {
|
|
19
38
|
if (declared !== undefined) {
|
|
20
39
|
if (typeof declared !== 'string' || !declared.trim())
|
|
21
40
|
throw new Error(
|
|
22
|
-
|
|
41
|
+
`The declared ${packageName} dependency must be a non-empty string.`
|
|
23
42
|
)
|
|
24
43
|
return { kind: 'restore' }
|
|
25
44
|
}
|
|
@@ -46,26 +65,36 @@ export function versioningInstallRequest(version, declared) {
|
|
|
46
65
|
)
|
|
47
66
|
return {
|
|
48
67
|
kind: 'add',
|
|
49
|
-
specifier: `${
|
|
68
|
+
specifier: `${packageName}@${prerelease ?? 'latest'}`,
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
export function parseAddArguments(args) {
|
|
54
73
|
const [plugin, ...rest] = args
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
const usage =
|
|
75
|
+
'Usage: rdx add versioning|i18n [--package-manager npm|pnpm|yarn]. i18n also accepts --locales en,es-ES.'
|
|
76
|
+
if (!['versioning', 'i18n'].includes(plugin)) throw new Error(usage)
|
|
77
|
+
const options = {}
|
|
78
|
+
for (let index = 0; index < rest.length; index += 2) {
|
|
79
|
+
const flag = rest[index]
|
|
80
|
+
const value = rest[index + 1]
|
|
81
|
+
if (
|
|
82
|
+
flag === '--package-manager' &&
|
|
83
|
+
managers.has(value) &&
|
|
84
|
+
!options.packageManager
|
|
58
85
|
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
options.packageManager = value
|
|
87
|
+
else if (
|
|
88
|
+
plugin === 'i18n' &&
|
|
89
|
+
flag === '--locales' &&
|
|
90
|
+
value?.trim() &&
|
|
91
|
+
!value.startsWith('-') &&
|
|
92
|
+
options.locales === undefined
|
|
93
|
+
)
|
|
94
|
+
options.locales = value
|
|
95
|
+
else throw new Error(usage)
|
|
96
|
+
}
|
|
97
|
+
return options
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
async function exists(filename) {
|
|
@@ -160,7 +189,7 @@ export function installPackage(
|
|
|
160
189
|
})
|
|
161
190
|
}
|
|
162
191
|
|
|
163
|
-
async function validateCandidate(filename) {
|
|
192
|
+
async function validateCandidate(filename, packageName) {
|
|
164
193
|
const jiti = createJiti(import.meta.url, {
|
|
165
194
|
moduleCache: false,
|
|
166
195
|
fsCache: false,
|
|
@@ -168,27 +197,62 @@ async function validateCandidate(filename) {
|
|
|
168
197
|
})
|
|
169
198
|
const input = await jiti.import(filename, { default: true })
|
|
170
199
|
const config = resolveConfig(input)
|
|
171
|
-
//
|
|
172
|
-
await createRdxProject(
|
|
200
|
+
// Read the official language declaration; other setup hooks run during prepare.
|
|
201
|
+
const project = await createRdxProject(
|
|
173
202
|
{
|
|
174
203
|
...config,
|
|
175
204
|
plugins: config.plugins.map((plugin) => ({
|
|
176
205
|
...plugin,
|
|
177
|
-
setup:
|
|
206
|
+
setup:
|
|
207
|
+
packageName === i18nPackage && plugin.name === i18nPackage
|
|
208
|
+
? plugin.setup
|
|
209
|
+
: undefined,
|
|
178
210
|
})),
|
|
179
211
|
},
|
|
180
|
-
{ rootDir: path.dirname(filename), command: '
|
|
212
|
+
{ rootDir: path.dirname(filename), command: 'runtime' }
|
|
181
213
|
)
|
|
182
214
|
if (
|
|
183
|
-
config.plugins.filter((plugin) => plugin.name ===
|
|
184
|
-
.length !== 1
|
|
215
|
+
config.plugins.filter((plugin) => plugin.name === packageName).length !== 1
|
|
185
216
|
)
|
|
186
217
|
throw new Error(
|
|
187
|
-
|
|
218
|
+
`Expected exactly one ${packageName} plugin in the resulting configuration.`
|
|
188
219
|
)
|
|
220
|
+
return project
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
export async function addVersioning(rootDir, options = {}) {
|
|
224
|
+
return addPlugin(rootDir, options, {
|
|
225
|
+
packageName: versioningPackage,
|
|
226
|
+
label: 'Versioning',
|
|
227
|
+
activate: activateVersioning,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function addI18n(rootDir, options = {}) {
|
|
232
|
+
return addPlugin(rootDir, options, {
|
|
233
|
+
packageName: i18nPackage,
|
|
234
|
+
label: 'i18n',
|
|
235
|
+
async activate(source) {
|
|
236
|
+
const state = inspectI18n(source)
|
|
237
|
+
if (state.configured) {
|
|
238
|
+
if (options.locales !== undefined)
|
|
239
|
+
throw new Error(
|
|
240
|
+
'i18n is already configured. Add further languages to locales in rdx.config.ts and create their locale/<folder>/docs/ directories.'
|
|
241
|
+
)
|
|
242
|
+
return source
|
|
243
|
+
}
|
|
244
|
+
const primary = Intl.getCanonicalLocales(state.lang)[0]
|
|
245
|
+
if (!primary) throw new Error('lang must be a valid language tag.')
|
|
246
|
+
const additional =
|
|
247
|
+
options.locales ?? (await (options.ask ?? askI18nLanguages)(primary))
|
|
248
|
+
if (additional === undefined) return
|
|
249
|
+
return activateI18n(source, initialLocales(primary, additional))
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function addPlugin(rootDir, options, definition) {
|
|
255
|
+
const { packageName, label } = definition
|
|
192
256
|
const {
|
|
193
257
|
install = installPackage,
|
|
194
258
|
log = console.log,
|
|
@@ -209,7 +273,11 @@ export async function addVersioning(rootDir, options = {}) {
|
|
|
209
273
|
throw new Error('rdx.config.ts must be a regular project file, not a link.')
|
|
210
274
|
const original = await readFile(filename, 'utf8')
|
|
211
275
|
// Refuse unsupported configurations before running the package manager.
|
|
212
|
-
const updated =
|
|
276
|
+
const updated = await definition.activate(original)
|
|
277
|
+
if (updated === undefined) {
|
|
278
|
+
log(`${label} setup cancelled.`)
|
|
279
|
+
return { changed: false, cancelled: true }
|
|
280
|
+
}
|
|
213
281
|
const pkg = JSON.parse(
|
|
214
282
|
await readFile(path.join(rootDir, 'package.json'), 'utf8')
|
|
215
283
|
)
|
|
@@ -220,11 +288,10 @@ export async function addVersioning(rootDir, options = {}) {
|
|
|
220
288
|
options.packageManager
|
|
221
289
|
)
|
|
222
290
|
const declared =
|
|
223
|
-
pkg.dependencies?.[
|
|
224
|
-
pkg.devDependencies?.[versioningPackage]
|
|
291
|
+
pkg.dependencies?.[packageName] ?? pkg.devDependencies?.[packageName]
|
|
225
292
|
let installed = false
|
|
226
293
|
try {
|
|
227
|
-
createRequire(path.join(rootDir, 'package.json')).resolve(
|
|
294
|
+
createRequire(path.join(rootDir, 'package.json')).resolve(packageName)
|
|
228
295
|
installed = true
|
|
229
296
|
} catch (error) {
|
|
230
297
|
// The official package exposes ESM imports only. require.resolve can find
|
|
@@ -234,21 +301,27 @@ export async function addVersioning(rootDir, options = {}) {
|
|
|
234
301
|
throw error
|
|
235
302
|
}
|
|
236
303
|
if (!declared || !installed) {
|
|
237
|
-
const request =
|
|
304
|
+
const request = pluginInstallRequest(runtimeVersion, declared, packageName)
|
|
238
305
|
log(
|
|
239
306
|
request.kind === 'restore'
|
|
240
|
-
? `Restoring the declared ${
|
|
307
|
+
? `Restoring the declared ${packageName} dependency with ${manager}...`
|
|
241
308
|
: `Installing ${request.specifier} with ${manager}...`
|
|
242
309
|
)
|
|
243
310
|
await install(manager, rootDir, env, request)
|
|
244
|
-
} else
|
|
245
|
-
log(`${versioningPackage} is already installed; preserving its version.`)
|
|
311
|
+
} else log(`${packageName} is already installed; preserving its version.`)
|
|
246
312
|
|
|
247
313
|
const temporary = path.join(rootDir, `rdx.config.add-${randomUUID()}.ts`)
|
|
314
|
+
let rollbackFolders
|
|
315
|
+
let committed = false
|
|
248
316
|
try {
|
|
249
317
|
await writeFile(temporary, updated, { flag: 'wx', mode: info.mode })
|
|
250
318
|
try {
|
|
251
|
-
await validateCandidate(temporary)
|
|
319
|
+
const project = await validateCandidate(temporary, packageName)
|
|
320
|
+
if (project.localization)
|
|
321
|
+
rollbackFolders = await prepareLanguageFolders(
|
|
322
|
+
rootDir,
|
|
323
|
+
project.localization
|
|
324
|
+
)
|
|
252
325
|
} catch (error) {
|
|
253
326
|
throw new Error(
|
|
254
327
|
`Plugin configuration validation failed: ${error.message}. rdx.config.ts was not changed. Installed dependencies were kept; fix the error and retry.`
|
|
@@ -265,10 +338,13 @@ export async function addVersioning(rootDir, options = {}) {
|
|
|
265
338
|
)
|
|
266
339
|
if (updated !== original) {
|
|
267
340
|
await rename(temporary, filename)
|
|
268
|
-
|
|
341
|
+
committed = true
|
|
342
|
+
log(`${label} activated in rdx.config.ts.`)
|
|
269
343
|
} else
|
|
270
|
-
log(
|
|
344
|
+
log(`${label} is already configured; no configuration changes needed.`)
|
|
345
|
+
committed = true
|
|
271
346
|
} finally {
|
|
347
|
+
if (!committed) await rollbackFolders?.()
|
|
272
348
|
await unlink(temporary).catch((error) => {
|
|
273
349
|
if (error.code !== 'ENOENT') throw error
|
|
274
350
|
})
|
package/bin/cli.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { createJiti } from 'jiti'
|
|
|
11
11
|
import postcssConfig from '../postcss.config.mjs'
|
|
12
12
|
import { preparePublicAssets } from './public-assets.mjs'
|
|
13
13
|
import { prepareProjectStyles } from './project-styles.mjs'
|
|
14
|
+
import { prepareLocaleRoutes } from './locale-routes.mjs'
|
|
14
15
|
|
|
15
16
|
export async function runRdxCli({ templateDir } = {}) {
|
|
16
17
|
const supportedCommands = new Set(['dev', 'build', 'start'])
|
|
@@ -25,7 +26,7 @@ export async function runRdxCli({ templateDir } = {}) {
|
|
|
25
26
|
templateDir ?? path.join(packageRoot, 'templates', 'runtime')
|
|
26
27
|
const runtimeDirectory = path.join(projectRoot, '.rdx')
|
|
27
28
|
|
|
28
|
-
async function prepareRuntime() {
|
|
29
|
+
async function prepareRuntime(project) {
|
|
29
30
|
await mkdir(runtimeDirectory, { recursive: true })
|
|
30
31
|
await preparePublicAssets(projectRoot, runtimeDirectory)
|
|
31
32
|
await cp(runtimeTemplate, runtimeDirectory, {
|
|
@@ -33,6 +34,7 @@ export async function runRdxCli({ templateDir } = {}) {
|
|
|
33
34
|
recursive: true,
|
|
34
35
|
})
|
|
35
36
|
await prepareProjectStyles(projectRoot, runtimeDirectory)
|
|
37
|
+
await prepareLocaleRoutes(project, runtimeDirectory, runtimeTemplate)
|
|
36
38
|
// Resolve in Node before Next processes this file: UI may be transitive,
|
|
37
39
|
// and native Tailwind plugins must stay outside the Turbopack module graph.
|
|
38
40
|
const resolvePlugin = createRequire(import.meta.url)
|
|
@@ -57,6 +59,9 @@ export async function runRdxCli({ templateDir } = {}) {
|
|
|
57
59
|
' rdx check: validate documentation links, sections and local assets'
|
|
58
60
|
)
|
|
59
61
|
console.log(' rdx add versioning [--package-manager npm|pnpm|yarn]')
|
|
62
|
+
console.log(
|
|
63
|
+
' rdx add i18n [--locales en,es-ES] [--package-manager npm|pnpm|yarn]'
|
|
64
|
+
)
|
|
60
65
|
console.log(
|
|
61
66
|
'Plugin commands are registered through plugins in rdx.config.ts.'
|
|
62
67
|
)
|
|
@@ -77,10 +82,31 @@ export async function runRdxCli({ templateDir } = {}) {
|
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
if (command === 'add') {
|
|
80
|
-
|
|
85
|
+
if (
|
|
86
|
+
(commandArgs.length === 1 &&
|
|
87
|
+
['--help', '-h'].includes(commandArgs[0])) ||
|
|
88
|
+
(commandArgs.length === 2 &&
|
|
89
|
+
['versioning', 'i18n'].includes(commandArgs[0]) &&
|
|
90
|
+
['--help', '-h'].includes(commandArgs[1]))
|
|
91
|
+
) {
|
|
92
|
+
console.log(
|
|
93
|
+
'Usage: rdx add versioning|i18n [--package-manager npm|pnpm|yarn]'
|
|
94
|
+
)
|
|
95
|
+
console.log(
|
|
96
|
+
' i18n: reuse lang and ask for additional languages, or pass --locales en,es-ES.'
|
|
97
|
+
)
|
|
98
|
+
console.log(
|
|
99
|
+
' Existing i18n languages are edited in rdx.config.ts and locale/<folder>/docs/.'
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
const { addVersioning, addI18n, parseAddArguments } =
|
|
81
104
|
await import('./add-plugin.mjs')
|
|
82
105
|
const options = parseAddArguments(commandArgs)
|
|
83
|
-
await
|
|
106
|
+
const result = await (
|
|
107
|
+
commandArgs[0] === 'i18n' ? addI18n : addVersioning
|
|
108
|
+
)(projectRoot, options)
|
|
109
|
+
if (result.cancelled) return
|
|
84
110
|
command = 'prepare'
|
|
85
111
|
}
|
|
86
112
|
|
|
@@ -114,7 +140,7 @@ export async function runRdxCli({ templateDir } = {}) {
|
|
|
114
140
|
rootDir: projectRoot,
|
|
115
141
|
command,
|
|
116
142
|
})
|
|
117
|
-
if (command === 'check') {
|
|
143
|
+
if (command === 'check' || command === 'build') {
|
|
118
144
|
const { checkDocumentation, formatDocumentationCheck } =
|
|
119
145
|
await import('@lexibase/rdx-core/check')
|
|
120
146
|
const report = checkDocumentation(project)
|
|
@@ -122,11 +148,12 @@ export async function runRdxCli({ templateDir } = {}) {
|
|
|
122
148
|
if (report.diagnostics.length) {
|
|
123
149
|
console.error(output)
|
|
124
150
|
process.exitCode = 1
|
|
151
|
+
return
|
|
125
152
|
} else console.log(output)
|
|
126
|
-
return
|
|
153
|
+
if (command === 'check') return
|
|
127
154
|
}
|
|
128
155
|
if (command === 'prepare') {
|
|
129
|
-
await prepareRuntime()
|
|
156
|
+
await prepareRuntime(project)
|
|
130
157
|
console.log('RDX runtime prepared in .rdx/')
|
|
131
158
|
return
|
|
132
159
|
}
|
|
@@ -147,7 +174,7 @@ export async function runRdxCli({ templateDir } = {}) {
|
|
|
147
174
|
return
|
|
148
175
|
}
|
|
149
176
|
|
|
150
|
-
await prepareRuntime()
|
|
177
|
+
await prepareRuntime(project)
|
|
151
178
|
const projectRequire = createRequire(path.join(projectRoot, 'package.json'))
|
|
152
179
|
const nextCli = projectRequire.resolve('next/dist/bin/next')
|
|
153
180
|
const child = spawn(
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lstat,
|
|
3
|
+
mkdir,
|
|
4
|
+
realpath,
|
|
5
|
+
rmdir,
|
|
6
|
+
writeFile,
|
|
7
|
+
readFile,
|
|
8
|
+
unlink,
|
|
9
|
+
} from 'node:fs/promises'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import process from 'node:process'
|
|
12
|
+
|
|
13
|
+
import { normalizeLocalization } from '@lexibase/rdx-core/config'
|
|
14
|
+
|
|
15
|
+
function languageLabel(code) {
|
|
16
|
+
try {
|
|
17
|
+
const label = new Intl.DisplayNames([code], { type: 'language' }).of(code)
|
|
18
|
+
return label[0].toLocaleUpperCase(code) + label.slice(1)
|
|
19
|
+
} catch {
|
|
20
|
+
// Valid tags with extensions need not have a display name in ICU.
|
|
21
|
+
return code
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Additional language tags; the existing lang always remains primary. */
|
|
26
|
+
export function initialLocales(primary, additional) {
|
|
27
|
+
const lang = Intl.getCanonicalLocales(primary)[0]
|
|
28
|
+
if (!lang) throw new Error('lang must be a valid language tag.')
|
|
29
|
+
if (typeof additional !== 'string' || !additional.trim())
|
|
30
|
+
throw new Error('Enter at least one additional language, e.g. en, es-ES.')
|
|
31
|
+
const codes = additional.split(',').map((value) => {
|
|
32
|
+
const code = Intl.getCanonicalLocales(value.trim())[0]
|
|
33
|
+
if (!code) throw new Error('Enter language tags separated by commas.')
|
|
34
|
+
return code
|
|
35
|
+
})
|
|
36
|
+
const locales = [lang, ...codes.filter((code) => code !== lang)].map(
|
|
37
|
+
(code) => ({
|
|
38
|
+
code,
|
|
39
|
+
label: languageLabel(code),
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
// Use the same validation as manual configuration, including duplicates.
|
|
43
|
+
normalizeLocalization({ locales }, lang)
|
|
44
|
+
return locales
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function askI18nLanguages(primary) {
|
|
48
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Interactive language setup needs a terminal. Use rdx add i18n --locales en,es-ES (additional languages; lang stays primary).'
|
|
51
|
+
)
|
|
52
|
+
const { default: prompts } = await import('prompts')
|
|
53
|
+
const response = await prompts({
|
|
54
|
+
type: 'text',
|
|
55
|
+
name: 'languages',
|
|
56
|
+
message: `Additional languages (primary: ${primary}; e.g. en, es-ES)`,
|
|
57
|
+
validate(value) {
|
|
58
|
+
try {
|
|
59
|
+
initialLocales(primary, value)
|
|
60
|
+
return true
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return error.message
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
return response.languages
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Create only empty translation directories; never copy or replace documents. */
|
|
70
|
+
export async function prepareLanguageFolders(rootDir, localization) {
|
|
71
|
+
const root = await realpath(rootDir)
|
|
72
|
+
const created = []
|
|
73
|
+
const markers = []
|
|
74
|
+
async function rollback() {
|
|
75
|
+
for (const marker of markers)
|
|
76
|
+
if ((await readFile(marker, 'utf8').catch(() => undefined)) === '')
|
|
77
|
+
await unlink(marker).catch((error) => {
|
|
78
|
+
if (error.code !== 'ENOENT') throw error
|
|
79
|
+
})
|
|
80
|
+
for (const directory of [...created].reverse())
|
|
81
|
+
await rmdir(directory).catch((error) => {
|
|
82
|
+
// A user may have started writing while installation was running.
|
|
83
|
+
if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) throw error
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
for (const locale of localization.locales) {
|
|
88
|
+
if (!locale.folder) continue
|
|
89
|
+
let directory = root
|
|
90
|
+
for (const part of ['locale', locale.folder, 'docs']) {
|
|
91
|
+
directory = path.join(directory, part)
|
|
92
|
+
try {
|
|
93
|
+
await mkdir(directory)
|
|
94
|
+
created.push(directory)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.code !== 'EEXIST') throw error
|
|
97
|
+
}
|
|
98
|
+
const info = await lstat(directory)
|
|
99
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Translation folders must be regular project directories: ${directory}`
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
if (created.includes(directory)) {
|
|
105
|
+
const marker = path.join(directory, '.gitkeep')
|
|
106
|
+
await writeFile(marker, '', { flag: 'wx' })
|
|
107
|
+
markers.push(marker)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await rollback()
|
|
112
|
+
throw error
|
|
113
|
+
}
|
|
114
|
+
return rollback
|
|
115
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const defaultFiles = [
|
|
5
|
+
'layout.tsx',
|
|
6
|
+
'page.tsx',
|
|
7
|
+
'not-found.tsx',
|
|
8
|
+
'docs/[[...slug]]/page.tsx',
|
|
9
|
+
]
|
|
10
|
+
const generated = [
|
|
11
|
+
...defaultFiles.map((file) => `app/(rdx-default)/${file}`),
|
|
12
|
+
'app/(rdx-default)/[...notFound]/page.tsx',
|
|
13
|
+
'app/(rdx-locales)/[locale]/layout.tsx',
|
|
14
|
+
'app/(rdx-locales)/[locale]/not-found.tsx',
|
|
15
|
+
'app/(rdx-locales)/[locale]/docs/[[...slug]]/page.tsx',
|
|
16
|
+
'app/(rdx-locales)/[locale]/rdx-search/[source]/route.ts',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
/** Root layouts keep HTML language correct on the server, without middleware. */
|
|
20
|
+
export async function prepareLocaleRoutes(project, runtime, template) {
|
|
21
|
+
async function safe(file) {
|
|
22
|
+
const parts = file.split('/')
|
|
23
|
+
let current = runtime
|
|
24
|
+
for (const segment of parts) {
|
|
25
|
+
current = path.join(current, segment)
|
|
26
|
+
try {
|
|
27
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Generated locale routes must not contain links: ${current}`
|
|
30
|
+
)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT') throw error
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return current
|
|
36
|
+
}
|
|
37
|
+
const marker = await safe('.rdx-localization.json')
|
|
38
|
+
let owned = false
|
|
39
|
+
try {
|
|
40
|
+
owned = JSON.parse(await readFile(marker, 'utf8')).version === 1
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code !== 'ENOENT') throw error
|
|
43
|
+
}
|
|
44
|
+
if (!project.localization) {
|
|
45
|
+
if (owned) {
|
|
46
|
+
for (const file of generated) await rm(await safe(file), { force: true })
|
|
47
|
+
await rm(marker)
|
|
48
|
+
}
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
async function emit(file, content) {
|
|
52
|
+
const target = await safe(file)
|
|
53
|
+
if (!owned) {
|
|
54
|
+
try {
|
|
55
|
+
await lstat(target)
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Conflicting generated locale route: ${target}. Remove the old .rdx/ runtime and prepare again.`
|
|
58
|
+
)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code !== 'ENOENT') throw error
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
64
|
+
await writeFile(target, content)
|
|
65
|
+
}
|
|
66
|
+
const layout = await readFile(path.join(template, 'app/layout.tsx'), 'utf8')
|
|
67
|
+
const provider = layout.match(/from '([^']+\/runtime)'/)[1]
|
|
68
|
+
for (const file of defaultFiles) {
|
|
69
|
+
const text = await readFile(path.join(template, 'app', file), 'utf8')
|
|
70
|
+
await emit(
|
|
71
|
+
`app/(rdx-default)/${file}`,
|
|
72
|
+
text
|
|
73
|
+
.replace(/from '(\.\.\/[^']*rdx-project)'/g, "from '../$1'")
|
|
74
|
+
.replace("'./globals.css'", "'../globals.css'")
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
await emit(
|
|
78
|
+
'app/(rdx-default)/[...notFound]/page.tsx',
|
|
79
|
+
`import { notFound } from 'next/navigation'\nexport default function Missing() { notFound() }\n`
|
|
80
|
+
)
|
|
81
|
+
await emit(
|
|
82
|
+
'app/(rdx-locales)/[locale]/layout.tsx',
|
|
83
|
+
`import { RdxLayout, getRdxMetadata, getLocalizedProject } from '${provider}'
|
|
84
|
+
import { getProject } from '../../../rdx-project'
|
|
85
|
+
import '../../globals.css'
|
|
86
|
+
export async function generateStaticParams() { return (await getProject()).localization!.locales.map(locale => ({ locale: locale.path })) }
|
|
87
|
+
export const dynamicParams = false
|
|
88
|
+
async function configuration(params: Promise<{ locale: string }>) {
|
|
89
|
+
const project = await getProject()
|
|
90
|
+
const { locale } = await params
|
|
91
|
+
return project.localization!.locales.some(entry => entry.path === locale) ? getLocalizedProject(project, locale).config : project.config
|
|
92
|
+
}
|
|
93
|
+
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { return getRdxMetadata(await configuration(params)) }
|
|
94
|
+
export default async function Layout({ children, params }: { children: React.ReactNode, params: Promise<{ locale: string }> }) {
|
|
95
|
+
return <RdxLayout config={await configuration(params)} bodyClassName="antialiased">{children}</RdxLayout>
|
|
96
|
+
}
|
|
97
|
+
`
|
|
98
|
+
)
|
|
99
|
+
await emit(
|
|
100
|
+
'app/(rdx-locales)/[locale]/not-found.tsx',
|
|
101
|
+
`import { NotFoundPage } from '${provider}'
|
|
102
|
+
import { getProject } from '../../../rdx-project'
|
|
103
|
+
export default async function Missing() { return <NotFoundPage project={await getProject()} /> }
|
|
104
|
+
`
|
|
105
|
+
)
|
|
106
|
+
await emit(
|
|
107
|
+
'app/(rdx-locales)/[locale]/docs/[[...slug]]/page.tsx',
|
|
108
|
+
`import { DocsPage, getDocumentMetadata, getProjectDocsPaths, getLocalizedProject } from '${provider}'
|
|
109
|
+
import { notFound } from 'next/navigation'
|
|
110
|
+
import { getProject } from '../../../../../rdx-project'
|
|
111
|
+
type Params = { locale: string, slug?: string[] }
|
|
112
|
+
async function selected(locale: string) {
|
|
113
|
+
const project = await getProject()
|
|
114
|
+
if (!project.localization!.locales.some(entry => entry.path === locale)) notFound()
|
|
115
|
+
return getLocalizedProject(project, locale)
|
|
116
|
+
}
|
|
117
|
+
export async function generateStaticParams({ params }: { params: { locale: string } }) { return getProjectDocsPaths(await selected(params.locale)).map(slug => ({ slug: slug.split('/') })) }
|
|
118
|
+
export async function generateMetadata({ params }: { params: Promise<Params> }) { const { locale, slug } = await params; return getDocumentMetadata({ project: await selected(locale), slug }) }
|
|
119
|
+
export default async function Page({ params }: { params: Promise<Params> }) { const { locale, slug } = await params; return <DocsPage project={await selected(locale)} slug={slug} /> }
|
|
120
|
+
`
|
|
121
|
+
)
|
|
122
|
+
await emit(
|
|
123
|
+
'app/(rdx-locales)/[locale]/rdx-search/[source]/route.ts',
|
|
124
|
+
`import { getSearchResponse, getSearchSources, getLocalizedProject } from '${provider}'
|
|
125
|
+
import { getProject } from '../../../../../rdx-project'
|
|
126
|
+
export const dynamic = 'force-static'
|
|
127
|
+
export const dynamicParams = false
|
|
128
|
+
export async function generateStaticParams() {
|
|
129
|
+
const project = await getProject()
|
|
130
|
+
return project.localization!.locales.flatMap(locale => getSearchSources(getLocalizedProject(project, locale.path)).map(source => ({ locale: locale.path, ...source })))
|
|
131
|
+
}
|
|
132
|
+
export async function GET(_request: Request, { params }: { params: Promise<{ locale: string, source: string }> }) { const { locale, source } = await params; return getSearchResponse(getLocalizedProject(await getProject(), locale), source) }
|
|
133
|
+
`
|
|
134
|
+
)
|
|
135
|
+
await writeFile(marker, JSON.stringify({ version: 1 }) + '\n')
|
|
136
|
+
for (const file of defaultFiles)
|
|
137
|
+
await rm(await safe(`app/${file}`), { force: true })
|
|
138
|
+
}
|
package/bin/plugin-config.mjs
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
|
|
3
3
|
export const versioningPackage = '@lexibase/rdx-versioning'
|
|
4
|
-
const
|
|
4
|
+
export const i18nPackage = '@lexibase/rdx-i18n'
|
|
5
|
+
const versioning = {
|
|
6
|
+
packageName: versioningPackage,
|
|
7
|
+
name: 'versioning',
|
|
8
|
+
label: 'Versioning',
|
|
9
|
+
}
|
|
10
|
+
const localization = { packageName: i18nPackage, name: 'i18n', label: 'i18n' }
|
|
11
|
+
|
|
12
|
+
export function activateVersioning(source) {
|
|
13
|
+
return editPluginConfig(source, versioning)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function inspectI18n(source) {
|
|
17
|
+
return editPluginConfig(source, localization, '', true)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function activateI18n(source, locales) {
|
|
21
|
+
return editPluginConfig(
|
|
22
|
+
source,
|
|
23
|
+
localization,
|
|
24
|
+
JSON.stringify({ locales }, null, 2)
|
|
25
|
+
)
|
|
26
|
+
}
|
|
5
27
|
|
|
6
28
|
function unwrap(node) {
|
|
7
29
|
while (
|
|
@@ -14,7 +36,9 @@ function unwrap(node) {
|
|
|
14
36
|
}
|
|
15
37
|
|
|
16
38
|
// Edit only known literal shapes; never print/reconstruct the user's entire file.
|
|
17
|
-
|
|
39
|
+
function editPluginConfig(source, plugin, argument = '', inspect = false) {
|
|
40
|
+
const { packageName, name: pluginName, label } = plugin
|
|
41
|
+
const manual = `Cannot safely edit rdx.config.ts. Add "import { ${pluginName} } from '${packageName}'" and ${pluginName}(${pluginName === 'i18n' ? '{ locales: [...] }' : ''}) to plugins manually, then run rdx prepare.`
|
|
18
42
|
const file = ts.createSourceFile(
|
|
19
43
|
'rdx.config.ts',
|
|
20
44
|
source,
|
|
@@ -44,7 +68,7 @@ export function activateVersioning(source) {
|
|
|
44
68
|
imported === 'defineConfig'
|
|
45
69
|
)
|
|
46
70
|
definitions.add(specifier.name.text)
|
|
47
|
-
if (name ===
|
|
71
|
+
if (name === packageName && imported === pluginName) {
|
|
48
72
|
if (factory) throw new Error(manual)
|
|
49
73
|
factory = specifier.name.text
|
|
50
74
|
}
|
|
@@ -52,10 +76,10 @@ export function activateVersioning(source) {
|
|
|
52
76
|
} else if (
|
|
53
77
|
bindings &&
|
|
54
78
|
ts.isNamespaceImport(bindings) &&
|
|
55
|
-
name ===
|
|
79
|
+
name === packageName
|
|
56
80
|
) {
|
|
57
81
|
if (factory) throw new Error(manual)
|
|
58
|
-
factory = `${bindings.name.text}
|
|
82
|
+
factory = `${bindings.name.text}.${pluginName}`
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
const exports = file.statements.filter(ts.isExportAssignment)
|
|
@@ -100,9 +124,18 @@ export function activateVersioning(source) {
|
|
|
100
124
|
)
|
|
101
125
|
if (matches.length > 1)
|
|
102
126
|
throw new Error(
|
|
103
|
-
|
|
127
|
+
`${label} is configured more than once. Remove the duplicate plugin before retrying.`
|
|
104
128
|
)
|
|
105
|
-
if (matches.length === 1) return source
|
|
129
|
+
if (matches.length === 1) return inspect ? { configured: true } : source
|
|
130
|
+
}
|
|
131
|
+
if (inspect) {
|
|
132
|
+
const languages = properties.filter((entry) => entry.name.text === 'lang')
|
|
133
|
+
if (languages.length > 1) throw new Error(manual)
|
|
134
|
+
const language = languages.length
|
|
135
|
+
? unwrap(languages[0].initializer)
|
|
136
|
+
: undefined
|
|
137
|
+
if (language && !ts.isStringLiteral(language)) throw new Error(manual)
|
|
138
|
+
return { configured: false, lang: language?.text ?? 'en' }
|
|
106
139
|
}
|
|
107
140
|
const newline = source.includes('\r\n') ? '\r\n' : '\n'
|
|
108
141
|
const edits = []
|
|
@@ -113,11 +146,12 @@ export function activateVersioning(source) {
|
|
|
113
146
|
ts.forEachChild(node, visit)
|
|
114
147
|
}
|
|
115
148
|
visit(file)
|
|
116
|
-
factory =
|
|
149
|
+
factory = pluginName
|
|
117
150
|
let suffix = 1
|
|
118
|
-
|
|
151
|
+
const alias = `rdx${pluginName[0].toUpperCase()}${pluginName.slice(1)}`
|
|
152
|
+
while (identifiers.has(factory)) factory = `${alias}${suffix++}`
|
|
119
153
|
const specifier =
|
|
120
|
-
factory ===
|
|
154
|
+
factory === pluginName ? pluginName : `${pluginName} as ${factory}`
|
|
121
155
|
let position = imports.length
|
|
122
156
|
? imports.at(-1).end
|
|
123
157
|
: file.statements[0].getStart(file)
|
|
@@ -133,7 +167,7 @@ export function activateVersioning(source) {
|
|
|
133
167
|
}
|
|
134
168
|
edits.push({
|
|
135
169
|
position,
|
|
136
|
-
text: `${newline}import { ${specifier} } from '${
|
|
170
|
+
text: `${newline}import { ${specifier} } from '${packageName}'${newline}`,
|
|
137
171
|
})
|
|
138
172
|
}
|
|
139
173
|
if (plugins) {
|
|
@@ -142,14 +176,14 @@ export function activateVersioning(source) {
|
|
|
142
176
|
: plugins.getStart(file) + 1
|
|
143
177
|
edits.push({
|
|
144
178
|
position,
|
|
145
|
-
text: `${plugins.elements.length ? ',' : ''} ${factory}()`,
|
|
179
|
+
text: `${plugins.elements.length ? ',' : ''} ${factory}(${argument.replaceAll('\n', newline)})`,
|
|
146
180
|
})
|
|
147
181
|
} else {
|
|
148
182
|
if (properties.length && !properties.hasTrailingComma)
|
|
149
183
|
edits.push({ position: properties.at(-1).end, text: ',' })
|
|
150
184
|
edits.push({
|
|
151
185
|
position: config.end - 1,
|
|
152
|
-
text: `${newline} plugins: [${factory}()],${newline}`,
|
|
186
|
+
text: `${newline} plugins: [${factory}(${argument.replaceAll('\n', newline)})],${newline}`,
|
|
153
187
|
})
|
|
154
188
|
}
|
|
155
189
|
for (const edit of edits.sort((a, b) => b.position - a.position))
|
package/config.mjs
CHANGED
|
@@ -2,7 +2,11 @@ import { readdir, realpath, stat } from 'node:fs/promises'
|
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createRdxProject,
|
|
7
|
+
resolveProjectPath,
|
|
8
|
+
getContentSources,
|
|
9
|
+
} from '@lexibase/rdx-core/config'
|
|
6
10
|
import { createJiti } from 'jiti'
|
|
7
11
|
|
|
8
12
|
/** Include the project and the real Next.js installation in Turbopack's root. */
|
|
@@ -77,7 +81,7 @@ export async function getRdxTracingConfig({ rootDir, appDir = rootDir }) {
|
|
|
77
81
|
const project = await createRdxProject(input, { rootDir })
|
|
78
82
|
const files = (
|
|
79
83
|
await Promise.all(
|
|
80
|
-
project
|
|
84
|
+
getContentSources(project)
|
|
81
85
|
.filter((source) => source.redirectTo === undefined)
|
|
82
86
|
.map((source) =>
|
|
83
87
|
contentFiles(
|
|
@@ -102,11 +106,28 @@ export async function getRdxTracingConfig({ rootDir, appDir = rootDir }) {
|
|
|
102
106
|
}
|
|
103
107
|
const redirects = project.sources
|
|
104
108
|
.filter((source) => source.redirectTo !== undefined)
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
.flatMap((source) =>
|
|
110
|
+
[
|
|
111
|
+
'/docs',
|
|
112
|
+
...(project.localization?.locales.map(
|
|
113
|
+
(locale) => `/${locale.path}/docs`
|
|
114
|
+
) ?? []),
|
|
115
|
+
].map((base) => ({
|
|
116
|
+
source: `${base}/${source.routePrefix}/:path*`,
|
|
117
|
+
destination: `${base}/${source.redirectTo}/:path*`,
|
|
118
|
+
permanent: true,
|
|
119
|
+
}))
|
|
120
|
+
)
|
|
121
|
+
if (project.localization) {
|
|
122
|
+
const primary = project.localization.locales.find(
|
|
123
|
+
(locale) => locale.code === project.localization.defaultLocale
|
|
124
|
+
)
|
|
125
|
+
redirects.push({
|
|
126
|
+
source: '/docs/:path*',
|
|
127
|
+
destination: `/${primary.path}/docs/:path*`,
|
|
108
128
|
permanent: true,
|
|
109
|
-
})
|
|
129
|
+
})
|
|
130
|
+
}
|
|
110
131
|
return {
|
|
111
132
|
...(redirects.length ? { redirects: async () => redirects } : {}),
|
|
112
133
|
outputFileTracingIncludes: {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { RdxProject, RDXConfig, RdxContentSource, RdxDocPage } from '@lexibase/rdx-types';
|
|
4
|
-
export { createRdxProject, getProjectDocsPaths } from '@lexibase/rdx-core';
|
|
4
|
+
export { createRdxProject, getLocalizedProject, getProjectDocsPaths } from '@lexibase/rdx-core';
|
|
5
5
|
import { MetadataRoute, Metadata } from 'next';
|
|
6
6
|
|
|
7
7
|
declare function getSearchSources(project: RdxProject): {
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// src/runtime/index.tsx
|
|
2
2
|
import {
|
|
3
|
-
getDocument as
|
|
3
|
+
getDocument as getDocument3,
|
|
4
4
|
getDocsPaths,
|
|
5
|
-
getSourceOptions as
|
|
6
|
-
resolveDocsRoute as resolveDocsRoute2
|
|
5
|
+
getSourceOptions as getSourceOptions4,
|
|
6
|
+
resolveDocsRoute as resolveDocsRoute2,
|
|
7
|
+
getLocalizedProject as getLocalizedProject2,
|
|
8
|
+
getDocsHref as getDocsHref3,
|
|
9
|
+
getLanguageLinks,
|
|
10
|
+
isFallbackPage as isFallbackPage3
|
|
7
11
|
} from "@lexibase/rdx-core";
|
|
8
12
|
import { Button } from "@lexibase/rdx-ui/components/button";
|
|
13
|
+
import { Callout } from "@lexibase/rdx-ui/components/callout";
|
|
14
|
+
import { LanguageDropdown } from "@lexibase/rdx-ui/components/language-dropdown";
|
|
9
15
|
import { DocsSidebar } from "@lexibase/rdx-ui/components/docs-sidebar";
|
|
10
16
|
import { DocsToc } from "@lexibase/rdx-ui/components/docs-toc";
|
|
11
17
|
import { DocsPageTransition } from "@lexibase/rdx-ui/components/docs-page-transition";
|
|
@@ -19,6 +25,7 @@ import { getMessages, formatMessage } from "@lexibase/rdx-ui/lib/i18n";
|
|
|
19
25
|
import { LocaleProvider } from "@lexibase/rdx-ui/providers/locale-provider";
|
|
20
26
|
import { Navbar } from "@lexibase/rdx-ui/components/navbar";
|
|
21
27
|
import { NotFound } from "@lexibase/rdx-ui/components/not-found";
|
|
28
|
+
import { LocalizedNotFound } from "@lexibase/rdx-ui/components/localized-not-found";
|
|
22
29
|
import {
|
|
23
30
|
VersionDropdown
|
|
24
31
|
} from "@lexibase/rdx-ui/components/version-dropdown";
|
|
@@ -54,10 +61,19 @@ function compileDocumentation(source, components, links) {
|
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// src/runtime/index.tsx
|
|
57
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
createRdxProject,
|
|
66
|
+
getProjectDocsPaths,
|
|
67
|
+
getLocalizedProject as getLocalizedProject3
|
|
68
|
+
} from "@lexibase/rdx-core";
|
|
58
69
|
|
|
59
70
|
// src/runtime/search.ts
|
|
60
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
getDocsIndex,
|
|
73
|
+
getDocument,
|
|
74
|
+
getSourceOptions,
|
|
75
|
+
getDocsHref
|
|
76
|
+
} from "@lexibase/rdx-core";
|
|
61
77
|
import { buildSearchIndex } from "@lexibase/rdx-core/search";
|
|
62
78
|
function getSearchSources(project) {
|
|
63
79
|
return project.config.search ? project.sources.filter((source) => source.redirectTo === void 0).map((source) => ({ source: source.routePrefix || "_" })) : [];
|
|
@@ -70,17 +86,16 @@ function getSearchResponse(project, key) {
|
|
|
70
86
|
(entry) => entry.redirectTo === void 0 && (entry.routePrefix || "_") === key
|
|
71
87
|
);
|
|
72
88
|
if (!source) return new Response(null, { status: 404, headers });
|
|
73
|
-
|
|
89
|
+
if (source.fallbackOnly)
|
|
90
|
+
return Response.json(buildSearchIndex([]), { headers });
|
|
91
|
+
const { docsDir } = getSourceOptions(project, source);
|
|
92
|
+
const options = { docsDir };
|
|
74
93
|
const documents = getDocsIndex(options).flatMap(
|
|
75
94
|
(category) => category.pages.map((page) => ({
|
|
76
95
|
title: page.title,
|
|
77
96
|
description: page.description,
|
|
78
97
|
category: category.title,
|
|
79
|
-
href:
|
|
80
|
-
"/docs",
|
|
81
|
-
source.routePrefix,
|
|
82
|
-
page.slug.split("/").map(encodeURIComponent).join("/")
|
|
83
|
-
].filter(Boolean).join("/"),
|
|
98
|
+
href: getDocsHref(project, source, page.slug),
|
|
84
99
|
content: getDocument(page.slug, options)?.content ?? ""
|
|
85
100
|
}))
|
|
86
101
|
);
|
|
@@ -88,7 +103,12 @@ function getSearchResponse(project, key) {
|
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
// src/runtime/indexing.ts
|
|
91
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
getDocsIndex as getDocsIndex2,
|
|
108
|
+
getSourceOptions as getSourceOptions2,
|
|
109
|
+
getContentSources,
|
|
110
|
+
isFallbackPage
|
|
111
|
+
} from "@lexibase/rdx-core";
|
|
92
112
|
function getSourceRobots(project, source) {
|
|
93
113
|
const published = project.sources.some(
|
|
94
114
|
(entry) => entry.id !== "current" && entry.redirectTo === void 0 && entry.badge !== "Archived"
|
|
@@ -107,7 +127,7 @@ function getRdxRobots(config) {
|
|
|
107
127
|
}
|
|
108
128
|
function getRdxSitemap(project, {
|
|
109
129
|
documentPath = (source, page) => [
|
|
110
|
-
"/docs",
|
|
130
|
+
source.basePath ?? "/docs",
|
|
111
131
|
source.routePrefix,
|
|
112
132
|
page.slug.split("/").map(encodeURIComponent).join("/")
|
|
113
133
|
].filter(Boolean).join("/")
|
|
@@ -116,10 +136,11 @@ function getRdxSitemap(project, {
|
|
|
116
136
|
if (!base) return [];
|
|
117
137
|
const urls = /* @__PURE__ */ new Set();
|
|
118
138
|
if (project.config.homepage?.enabled) urls.add(new URL("/", base).href);
|
|
119
|
-
for (const source of project
|
|
139
|
+
for (const source of getContentSources(project)) {
|
|
120
140
|
if (!getSourceRobots(project, source).index) continue;
|
|
121
141
|
for (const category of getDocsIndex2(getSourceOptions2(project, source))) {
|
|
122
142
|
for (const page of category.pages) {
|
|
143
|
+
if (isFallbackPage(project, source, page.sourcePath)) continue;
|
|
123
144
|
urls.add(new URL(documentPath(source, page), base).href);
|
|
124
145
|
}
|
|
125
146
|
}
|
|
@@ -128,21 +149,67 @@ function getRdxSitemap(project, {
|
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
// src/runtime/metadata.ts
|
|
131
|
-
import {
|
|
152
|
+
import {
|
|
153
|
+
resolveDocsRoute,
|
|
154
|
+
getLocalizedProject,
|
|
155
|
+
getDocsHref as getDocsHref2,
|
|
156
|
+
isFallbackPage as isFallbackPage2,
|
|
157
|
+
getDocument as getDocument2,
|
|
158
|
+
getSourceOptions as getSourceOptions3
|
|
159
|
+
} from "@lexibase/rdx-core";
|
|
132
160
|
import { notFound } from "next/navigation";
|
|
133
161
|
function getDocumentMetadata({
|
|
134
162
|
project,
|
|
135
163
|
slug
|
|
136
164
|
}) {
|
|
165
|
+
if (project.localization && !project.locale)
|
|
166
|
+
return { robots: { index: false, follow: true } };
|
|
137
167
|
const route = resolveDocsRoute(project, slug);
|
|
138
168
|
if (route.source.redirectTo !== void 0)
|
|
139
169
|
return { robots: getSourceRobots(project, route.source) };
|
|
140
170
|
if (!route.slug) return { robots: { index: false, follow: true } };
|
|
141
171
|
if (!route.document) notFound();
|
|
142
172
|
const page = route.categories.flatMap((category) => category.pages).find((page2) => page2.slug === route.slug);
|
|
173
|
+
const fallback = isFallbackPage2(project, route.source, page.sourcePath);
|
|
174
|
+
const primary = project.localization?.locales.find(
|
|
175
|
+
(locale) => locale.code === project.localization.defaultLocale
|
|
176
|
+
);
|
|
177
|
+
const canonicalProject = fallback ? getLocalizedProject(project, primary.path) : project;
|
|
178
|
+
const base = project.config.metadata.metadataBase;
|
|
179
|
+
const languages = project.localization && base ? Object.fromEntries(
|
|
180
|
+
project.localization.locales.flatMap((locale) => {
|
|
181
|
+
const view = getLocalizedProject(project, locale.path);
|
|
182
|
+
const source = view.sources.find(
|
|
183
|
+
(entry) => entry.id === route.source.id
|
|
184
|
+
);
|
|
185
|
+
return !source.fallbackOnly && getDocument2(page.slug, {
|
|
186
|
+
docsDir: getSourceOptions3(view, source).docsDir
|
|
187
|
+
}) ? [
|
|
188
|
+
[
|
|
189
|
+
locale.code,
|
|
190
|
+
new URL(getDocsHref2(view, source, page.slug), base).href
|
|
191
|
+
]
|
|
192
|
+
] : [];
|
|
193
|
+
})
|
|
194
|
+
) : void 0;
|
|
143
195
|
return {
|
|
144
196
|
...getPageMetadata(project.config, page),
|
|
145
|
-
robots: getSourceRobots(project, route.source)
|
|
197
|
+
robots: fallback ? { index: false, follow: true } : getSourceRobots(project, route.source),
|
|
198
|
+
...project.localization && base ? {
|
|
199
|
+
alternates: {
|
|
200
|
+
canonical: new URL(
|
|
201
|
+
getDocsHref2(
|
|
202
|
+
canonicalProject,
|
|
203
|
+
canonicalProject.sources.find(
|
|
204
|
+
(entry) => entry.id === route.source.id
|
|
205
|
+
),
|
|
206
|
+
page.slug
|
|
207
|
+
),
|
|
208
|
+
base
|
|
209
|
+
).href,
|
|
210
|
+
...fallback ? {} : { languages }
|
|
211
|
+
}
|
|
212
|
+
} : {}
|
|
146
213
|
};
|
|
147
214
|
}
|
|
148
215
|
function getPageMetadata(config, page) {
|
|
@@ -211,13 +278,13 @@ function RdxHeader({
|
|
|
211
278
|
const messages = getMessages(config.lang);
|
|
212
279
|
const { source, slug: currentSlug } = resolveDocsRoute2(project, slug);
|
|
213
280
|
const options = project.sources.filter((edition) => edition.redirectTo === void 0).map((edition) => {
|
|
214
|
-
const docs =
|
|
215
|
-
const destination = currentSlug &&
|
|
281
|
+
const docs = getSourceOptions4(project, edition);
|
|
282
|
+
const destination = currentSlug && getDocument3(currentSlug, docs) ? currentSlug : getDocsPaths(docs)[0];
|
|
216
283
|
return {
|
|
217
284
|
id: edition.id,
|
|
218
285
|
label: edition.id === "current" ? messages.current : formatMessage(messages.version, { version: edition.label }),
|
|
219
286
|
kind: edition.id === "current" ? "live" : edition.badge === "Archived" ? "archived" : "version",
|
|
220
|
-
href:
|
|
287
|
+
href: getDocsHref3(project, edition, destination)
|
|
221
288
|
};
|
|
222
289
|
});
|
|
223
290
|
const BrandLogo = config.navbar?.logo ?? Logo;
|
|
@@ -229,14 +296,23 @@ function RdxHeader({
|
|
|
229
296
|
search: config.search ? /* @__PURE__ */ jsx(
|
|
230
297
|
DocsSearch,
|
|
231
298
|
{
|
|
232
|
-
indexUrl:
|
|
299
|
+
indexUrl: `${project.locale ? `/${project.locale.path}` : ""}/rdx-search/${encodeURIComponent(source.routePrefix || "_")}`,
|
|
233
300
|
version: source.id === "current" ? messages.current : formatMessage(messages.version, { version: source.label })
|
|
234
301
|
}
|
|
235
302
|
) : void 0,
|
|
236
303
|
siteTitle: config.metadata.applicationName,
|
|
237
304
|
logo: /* @__PURE__ */ jsx(BrandLogo, { className: "size-5" }),
|
|
238
|
-
homeHref: config.homepage?.enabled ? "/" :
|
|
239
|
-
docsHref:
|
|
305
|
+
homeHref: config.homepage?.enabled ? "/" : getDocsHref3(project),
|
|
306
|
+
docsHref: getDocsHref3(project, source),
|
|
307
|
+
languageMenu: project.localization ? /* @__PURE__ */ jsx(
|
|
308
|
+
LanguageDropdown,
|
|
309
|
+
{
|
|
310
|
+
lang: config.lang,
|
|
311
|
+
current: project.locale?.code ?? project.localization.defaultLocale,
|
|
312
|
+
options: getLanguageLinks(project, source, currentSlug),
|
|
313
|
+
contributionUrl: project.localization.contributionUrl
|
|
314
|
+
}
|
|
315
|
+
) : void 0,
|
|
240
316
|
versionMenu: project.sources.length > 1 ? /* @__PURE__ */ jsx(VersionDropdown, { options, currentId: source.id }) : void 0,
|
|
241
317
|
github: config.navbar?.actions?.github,
|
|
242
318
|
showToggleMode: config.navbar?.actions?.showToggleMode
|
|
@@ -245,7 +321,7 @@ function RdxHeader({
|
|
|
245
321
|
}
|
|
246
322
|
function HomePage({ project }) {
|
|
247
323
|
const { config } = project;
|
|
248
|
-
if (!config.homepage?.enabled) redirect(
|
|
324
|
+
if (!config.homepage?.enabled) redirect(getDocsHref3(project));
|
|
249
325
|
const homepage = config.homepage;
|
|
250
326
|
const CustomHome = homepage.component;
|
|
251
327
|
const BrandLogo = config.navbar?.logo ?? Logo;
|
|
@@ -271,6 +347,25 @@ function HomePage({ project }) {
|
|
|
271
347
|
] });
|
|
272
348
|
}
|
|
273
349
|
function NotFoundPage({ project }) {
|
|
350
|
+
if (project.localization) {
|
|
351
|
+
const { config, localization } = project;
|
|
352
|
+
const BrandLogo = config.navbar?.logo ?? Logo;
|
|
353
|
+
return /* @__PURE__ */ jsx(
|
|
354
|
+
LocalizedNotFound,
|
|
355
|
+
{
|
|
356
|
+
locales: localization.locales,
|
|
357
|
+
defaultLocale: localization.defaultLocale,
|
|
358
|
+
homepage: !!config.homepage?.enabled,
|
|
359
|
+
brand: {
|
|
360
|
+
title: config.navbar?.title,
|
|
361
|
+
siteTitle: config.metadata.applicationName,
|
|
362
|
+
github: config.navbar?.actions?.github,
|
|
363
|
+
showToggleMode: config.navbar?.actions?.showToggleMode,
|
|
364
|
+
logo: /* @__PURE__ */ jsx(BrandLogo, { className: "size-5" })
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
}
|
|
274
369
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
275
370
|
/* @__PURE__ */ jsx(RdxHeader, { project }),
|
|
276
371
|
/* @__PURE__ */ jsx(NotFound, { lang: project.config.lang })
|
|
@@ -280,6 +375,8 @@ async function DocsPage({
|
|
|
280
375
|
slug,
|
|
281
376
|
project
|
|
282
377
|
}) {
|
|
378
|
+
if (project.localization && !project.locale)
|
|
379
|
+
permanentRedirect(getDocsHref3(project, project.sources[0], slug?.join("/")));
|
|
283
380
|
const messages = getMessages(project.config.lang);
|
|
284
381
|
const {
|
|
285
382
|
source,
|
|
@@ -289,11 +386,13 @@ async function DocsPage({
|
|
|
289
386
|
} = resolveDocsRoute2(project, slug);
|
|
290
387
|
if (source.redirectTo !== void 0)
|
|
291
388
|
permanentRedirect(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
389
|
+
getDocsHref3(
|
|
390
|
+
project,
|
|
391
|
+
project.sources.find(
|
|
392
|
+
(entry) => entry.routePrefix === source.redirectTo
|
|
393
|
+
),
|
|
394
|
+
slug?.slice(1).join("/")
|
|
395
|
+
)
|
|
297
396
|
);
|
|
298
397
|
if (!currentSlug)
|
|
299
398
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -304,19 +403,32 @@ async function DocsPage({
|
|
|
304
403
|
] })
|
|
305
404
|
] });
|
|
306
405
|
if (!document) notFound2();
|
|
406
|
+
const fallback = isFallbackPage3(project, source, document.sourcePath);
|
|
407
|
+
const primaryLocale = (source.localization ?? project.localization)?.locales.find(
|
|
408
|
+
(locale) => locale.code === project.localization.defaultLocale
|
|
409
|
+
);
|
|
410
|
+
const originalProject = fallback && primaryLocale ? getLocalizedProject2(project, primaryLocale.path) : project;
|
|
411
|
+
const contentSource = originalProject.sources.find(
|
|
412
|
+
(entry) => entry.id === source.id
|
|
413
|
+
);
|
|
414
|
+
const contentOptions = getSourceOptions4(originalProject, contentSource);
|
|
415
|
+
const contentCategories = fallback ? resolveDocsRoute2(originalProject, slug).categories : categories;
|
|
416
|
+
const contentLanguage = fallback ? primaryLocale.code : project.config.lang;
|
|
307
417
|
const { content } = await compileDocumentation(
|
|
308
418
|
document.content,
|
|
309
419
|
getMdxComponents(project.config.lang),
|
|
310
420
|
{
|
|
311
|
-
sourceRoot:
|
|
421
|
+
sourceRoot: contentOptions.docsDir,
|
|
312
422
|
sourcePath: document.sourcePath,
|
|
313
423
|
routePrefix: source.routePrefix,
|
|
314
|
-
|
|
424
|
+
basePath: contentSource.basePath,
|
|
425
|
+
virtualPaths: !!contentOptions.fallbackDocsDir,
|
|
426
|
+
documents: contentCategories.flatMap((category2) => category2.pages)
|
|
315
427
|
}
|
|
316
428
|
);
|
|
317
429
|
const title = typeof document.frontmatter.title === "string" ? document.frontmatter.title : currentSlug;
|
|
318
430
|
const description = typeof document.frontmatter.description === "string" ? document.frontmatter.description : null;
|
|
319
|
-
const hrefFor = (pageSlug) =>
|
|
431
|
+
const hrefFor = (pageSlug) => getDocsHref3(project, source, pageSlug);
|
|
320
432
|
const sidebarCategories = categories.map((category2) => ({
|
|
321
433
|
title: category2.title,
|
|
322
434
|
links: category2.pages.map((page) => ({
|
|
@@ -362,9 +474,58 @@ async function DocsPage({
|
|
|
362
474
|
"main",
|
|
363
475
|
{
|
|
364
476
|
id: "rdx-main",
|
|
477
|
+
lang: contentLanguage,
|
|
365
478
|
tabIndex: -1,
|
|
366
479
|
className: "w-full flex flex-col gap-5 md:mx-auto md:max-w-[860px] pt-14 md:pt-0 flex-1 outline-none",
|
|
367
480
|
children: [
|
|
481
|
+
fallback && /* @__PURE__ */ jsxs(
|
|
482
|
+
Callout,
|
|
483
|
+
{
|
|
484
|
+
lang: project.config.lang,
|
|
485
|
+
variant: "note",
|
|
486
|
+
title: messages.translationUnavailable,
|
|
487
|
+
"data-rdx-translation-fallback": true,
|
|
488
|
+
children: [
|
|
489
|
+
/* @__PURE__ */ jsx("p", { children: formatMessage(messages.translationFallback, {
|
|
490
|
+
requested: project.locale.label,
|
|
491
|
+
original: primaryLocale.label
|
|
492
|
+
}) }),
|
|
493
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2", children: [
|
|
494
|
+
/* @__PURE__ */ jsx(
|
|
495
|
+
Button,
|
|
496
|
+
{
|
|
497
|
+
variant: "link",
|
|
498
|
+
href: getDocsHref3(
|
|
499
|
+
originalProject,
|
|
500
|
+
contentSource,
|
|
501
|
+
currentSlug
|
|
502
|
+
),
|
|
503
|
+
children: messages.viewOriginal
|
|
504
|
+
}
|
|
505
|
+
),
|
|
506
|
+
project.localization?.contributionUrl && /* @__PURE__ */ jsxs(
|
|
507
|
+
Button,
|
|
508
|
+
{
|
|
509
|
+
variant: "link",
|
|
510
|
+
href: project.localization.contributionUrl,
|
|
511
|
+
target: "_blank",
|
|
512
|
+
rel: "noopener noreferrer",
|
|
513
|
+
"data-rdx-translate-page": true,
|
|
514
|
+
children: [
|
|
515
|
+
messages.translateThisPage,
|
|
516
|
+
/* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
517
|
+
" ",
|
|
518
|
+
"(",
|
|
519
|
+
messages.opensInNewTab,
|
|
520
|
+
")"
|
|
521
|
+
] })
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
)
|
|
525
|
+
] })
|
|
526
|
+
]
|
|
527
|
+
}
|
|
528
|
+
),
|
|
368
529
|
/* @__PURE__ */ jsxs("header", { className: "mb-6", children: [
|
|
369
530
|
source.badge && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: source.badge === "Archived" ? messages.archivedVersion : source.badge }),
|
|
370
531
|
/* @__PURE__ */ jsx("span", { className: "text-xs text-zinc-600 font-semibold uppercase block -mb-0.5", children: category?.title }),
|
|
@@ -425,6 +586,7 @@ export {
|
|
|
425
586
|
RdxLayout,
|
|
426
587
|
createRdxProject,
|
|
427
588
|
getDocumentMetadata,
|
|
589
|
+
getLocalizedProject3 as getLocalizedProject,
|
|
428
590
|
getPageMetadata,
|
|
429
591
|
getProjectDocsPaths,
|
|
430
592
|
getRdxMetadata,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexibase/rdx-next",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "The Next.js runtime for RDX documentation projects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,14 +48,15 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"cross-spawn": "^7.0.6",
|
|
50
50
|
"jiti": "^2.6.1",
|
|
51
|
+
"prompts": "^2.4.2",
|
|
51
52
|
"next-mdx-remote": "^6.0.0",
|
|
52
53
|
"remark-gfm": "^4",
|
|
53
54
|
"typescript": "6.0.3",
|
|
54
|
-
"@lexibase/rdx-
|
|
55
|
-
"@lexibase/rdx-
|
|
55
|
+
"@lexibase/rdx-core": "2.1.0",
|
|
56
|
+
"@lexibase/rdx-types": "2.1.0"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
|
-
"@lexibase/rdx-ui": "^3.
|
|
59
|
+
"@lexibase/rdx-ui": "^3.3.0",
|
|
59
60
|
"next": "^16.4.2",
|
|
60
61
|
"react": "^19.3.1",
|
|
61
62
|
"react-dom": "^19.3.1"
|
|
@@ -80,9 +81,9 @@
|
|
|
80
81
|
"next": "^16.4.2",
|
|
81
82
|
"react": "19.3.1",
|
|
82
83
|
"react-dom": "19.3.1",
|
|
83
|
-
"@rdx
|
|
84
|
-
"@
|
|
85
|
-
"@rdx/
|
|
84
|
+
"@lexibase/rdx-ui": "3.3.0",
|
|
85
|
+
"@rdx/typescript-config": "1.0.0",
|
|
86
|
+
"@rdx/eslint-config": "1.0.0"
|
|
86
87
|
},
|
|
87
88
|
"scripts": {
|
|
88
89
|
"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",
|