@lexibase/create-rdx 4.1.0 → 4.2.1

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
@@ -39,6 +39,17 @@ npm create @lexibase/rdx@latest my-docs -- --skip-install
39
39
  `.rdx/`. Otherwise the creator uses the invoking package manager and prepares
40
40
  the runtime. Follow the commands printed by the generator.
41
41
 
42
+ The creator reports file creation, dependency installation, and runtime preparation
43
+ as separate stages, with elapsed time and periodic activity messages during long
44
+ operations. During pnpm installation, RDX shows a rotating activity indicator and
45
+ a bar based on the number of packages linked. Resolution, cache and download
46
+ counts come from pnpm's structured logs; the bar waits for a known package total.
47
+ Warnings, errors and lifecycle output remain visible. Logs without a terminal use
48
+ plain status lines instead of animation. npm and Yarn retain their native output.
49
+ npm and pnpm projects
50
+ prepare using their installed RDX executable directly; Yarn provides its project
51
+ execution environment for Plug'n'Play compatibility.
52
+
42
53
  ## Recovery
43
54
 
44
55
  In a pnpm workspace, installation uses the workspace's existing dependencies and
package/bin/index.js CHANGED
@@ -9,6 +9,9 @@ import prompts from 'prompts'
9
9
 
10
10
  import { parseArguments, validateProjectName } from './options.mjs'
11
11
  import { normalizePnpmRdxApprovals } from './pnpm-policy.mjs'
12
+ import { installedRuntime } from './installed-runtime.mjs'
13
+ import { runStep } from './progress.mjs'
14
+ import { createPnpmProgress } from './pnpm-progress.mjs'
12
15
 
13
16
  function getPackageManager() {
14
17
  const userAgent = process.env.npm_config_user_agent || ''
@@ -19,7 +22,7 @@ function getPackageManager() {
19
22
  return 'npm'
20
23
  }
21
24
 
22
- function run(command, args, cwd) {
25
+ function run(command, args, cwd, label = `${command} ${args.join(' ')}`) {
23
26
  return new Promise((resolve, reject) => {
24
27
  // Reuse the Node entry point when available. Otherwise cross-spawn
25
28
  // resolves Windows command shims and escapes their arguments.
@@ -30,15 +33,29 @@ function run(command, args, cwd) {
30
33
  /\.[cm]?js$/i.test(packageManagerEntry)
31
34
  const executable = useNodeEntry ? process.execPath : command
32
35
  const commandArgs = useNodeEntry ? [packageManagerEntry, ...args] : args
36
+ const progress =
37
+ command === 'pnpm' && args[0] === 'install'
38
+ ? createPnpmProgress()
39
+ : undefined
40
+ if (progress) commandArgs.push('--reporter=ndjson')
33
41
  const child = spawn(executable, commandArgs, {
34
42
  cwd,
35
- stdio: 'inherit',
43
+ stdio: progress ? ['inherit', 'pipe', 'pipe'] : 'inherit',
44
+ windowsHide: true,
36
45
  })
37
46
 
38
- child.on('error', reject)
39
- child.on('exit', (code) => {
47
+ if (progress) {
48
+ child.stdout.setEncoding('utf8').on('data', progress.stdout)
49
+ child.stderr.setEncoding('utf8').on('data', progress.stderr)
50
+ }
51
+ child.on('error', (error) => {
52
+ progress?.finish(false)
53
+ reject(error)
54
+ })
55
+ child.on('close', (code) => {
56
+ progress?.finish(code === 0)
40
57
  if (code === 0) resolve()
41
- else reject(new Error(`${command} ${args.join(' ')} exited with ${code}`))
58
+ else reject(new Error(`${label} exited with ${code}`))
42
59
  })
43
60
  })
44
61
  }
@@ -93,73 +110,93 @@ async function main() {
93
110
  if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
94
111
  throw new Error('Project must be created inside the current directory.')
95
112
  }
96
- try {
97
- await fs.mkdir(targetDir)
98
- } catch (error) {
99
- if (error.code === 'EEXIST')
100
- throw new Error(
101
- `The path "${projectName}" already exists. Choose another name; existing files are never overwritten.`
113
+ await runStep(
114
+ `[1/${skipInstall ? 1 : 3}] Creating project files...`,
115
+ async () => {
116
+ try {
117
+ await fs.mkdir(targetDir)
118
+ } catch (error) {
119
+ if (error.code === 'EEXIST')
120
+ throw new Error(
121
+ `The path "${projectName}" already exists. Choose another name; existing files are never overwritten.`
122
+ )
123
+ throw error
124
+ }
125
+
126
+ const currentFile = fileURLToPath(import.meta.url)
127
+ const templateDir = path.resolve(
128
+ path.dirname(currentFile),
129
+ '../templates/default'
130
+ )
131
+ await fs.copy(templateDir, targetDir, {
132
+ errorOnExist: true,
133
+ overwrite: false,
134
+ })
135
+ await fs.ensureDir(path.join(targetDir, 'public'))
136
+
137
+ const configPath = path.join(targetDir, 'rdx.config.ts')
138
+ const config = await fs.readFile(configPath, 'utf8')
139
+ await fs.writeFile(
140
+ configPath,
141
+ config.replace(
142
+ 'since: new Date().getFullYear(),',
143
+ `since: ${new Date().getFullYear()},`
144
+ )
102
145
  )
103
- throw error
104
- }
105
-
106
- const currentFile = fileURLToPath(import.meta.url)
107
- const templateDir = path.resolve(
108
- path.dirname(currentFile),
109
- '../templates/default'
110
- )
111
- await fs.copy(templateDir, targetDir, {
112
- errorOnExist: true,
113
- overwrite: false,
114
- })
115
- await fs.ensureDir(path.join(targetDir, 'public'))
116
-
117
- const configPath = path.join(targetDir, 'rdx.config.ts')
118
- const config = await fs.readFile(configPath, 'utf8')
119
- await fs.writeFile(
120
- configPath,
121
- config.replace(
122
- 'since: new Date().getFullYear(),',
123
- `since: ${new Date().getFullYear()},`
124
- )
125
- )
126
146
 
127
- const gitignoreTemplatePath = path.join(targetDir, 'gitignore.template')
128
- await fs.move(gitignoreTemplatePath, path.join(targetDir, '.gitignore'), {
129
- overwrite: true,
130
- })
131
- await fs.remove(path.join(targetDir, 'gitignore'))
132
- await fs.remove(path.join(targetDir, '.npmignore'))
133
-
134
- const pkgPath = path.join(targetDir, 'package.json')
135
- const pkg = await fs.readJson(pkgPath)
136
- pkg.name = projectName.toLowerCase()
137
- await fs.writeJson(pkgPath, pkg, { spaces: 2 })
138
-
139
- const readmePath = path.join(targetDir, 'README.md')
140
- const readme = await fs.readFile(readmePath, 'utf8')
141
- await fs.writeFile(
142
- readmePath,
143
- readme
144
- .replaceAll('{{RDX_PROJECT_NAME}}', projectName)
145
- .replaceAll('__RDX_PROJECT_NAME__', projectName)
147
+ const gitignoreTemplatePath = path.join(targetDir, 'gitignore.template')
148
+ await fs.move(gitignoreTemplatePath, path.join(targetDir, '.gitignore'), {
149
+ overwrite: true,
150
+ })
151
+ await fs.remove(path.join(targetDir, 'gitignore'))
152
+ await fs.remove(path.join(targetDir, '.npmignore'))
153
+
154
+ const pkgPath = path.join(targetDir, 'package.json')
155
+ const pkg = await fs.readJson(pkgPath)
156
+ pkg.name = projectName.toLowerCase()
157
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 })
158
+
159
+ const readmePath = path.join(targetDir, 'README.md')
160
+ const readme = await fs.readFile(readmePath, 'utf8')
161
+ await fs.writeFile(
162
+ readmePath,
163
+ readme
164
+ .replaceAll('{{RDX_PROJECT_NAME}}', projectName)
165
+ .replaceAll('__RDX_PROJECT_NAME__', projectName)
166
+ )
167
+ }
146
168
  )
147
169
 
148
170
  const packageManager = getPackageManager()
149
171
 
150
172
  if (!skipInstall) {
151
173
  try {
152
- console.log(`\nInstalling dependencies with ${packageManager}...\n`)
153
- if (packageManager === 'pnpm')
154
- await normalizePnpmRdxApprovals(targetDir)
155
- await run(packageManager, ['install'], targetDir)
156
- if (packageManager === 'pnpm')
157
- await normalizePnpmRdxApprovals(targetDir)
158
- const prepareArgs =
159
- packageManager === 'npm'
160
- ? ['exec', '--', 'rdx', 'prepare']
161
- : ['exec', 'rdx', 'prepare']
162
- await run(packageManager, prepareArgs, targetDir)
174
+ await runStep(
175
+ `[2/3] Installing dependencies with ${packageManager}...`,
176
+ async () => {
177
+ if (packageManager === 'pnpm')
178
+ await normalizePnpmRdxApprovals(targetDir)
179
+ await run(packageManager, ['install'], targetDir)
180
+ if (packageManager === 'pnpm')
181
+ await normalizePnpmRdxApprovals(targetDir)
182
+ },
183
+ { intervalMs: packageManager === 'pnpm' ? 0 : 15_000 }
184
+ )
185
+ await runStep('[3/3] Preparing RDX...', async () => {
186
+ if (packageManager === 'yarn') {
187
+ // Yarn provides the project's Plug'n'Play loader when needed.
188
+ await run(packageManager, ['exec', 'rdx', 'prepare'], targetDir)
189
+ } else {
190
+ // Avoid another package-manager startup and automatic install check.
191
+ const executable = await installedRuntime(targetDir)
192
+ await run(
193
+ process.execPath,
194
+ [executable, 'prepare'],
195
+ targetDir,
196
+ 'rdx prepare'
197
+ )
198
+ }
199
+ })
163
200
  } catch (error) {
164
201
  const runner = packageManager === 'npm' ? 'npm run' : packageManager
165
202
  throw new Error(
@@ -0,0 +1,32 @@
1
+ import { createRequire } from 'node:module'
2
+ import { readFile, stat } from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ /** Resolve the framework installed in the new project, including hoisted workspaces. */
6
+ export async function installedRuntime(directory) {
7
+ const require = createRequire(path.join(directory, 'package.json'))
8
+ let filename
9
+ try {
10
+ filename = require.resolve('@lexibase/rdx/package.json')
11
+ } catch {
12
+ throw new Error(
13
+ 'Could not find @lexibase/rdx in the new project after installation.'
14
+ )
15
+ }
16
+ const manifest = JSON.parse(await readFile(filename, 'utf8'))
17
+ const entry =
18
+ typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.rdx
19
+ if (typeof entry !== 'string' || !entry)
20
+ throw new Error(
21
+ 'The installed @lexibase/rdx package does not provide its rdx executable.'
22
+ )
23
+ const executable = path.resolve(path.dirname(filename), entry)
24
+ try {
25
+ if (!(await stat(executable)).isFile()) throw new Error()
26
+ } catch {
27
+ throw new Error(
28
+ 'The installed RDX executable is missing. Run the package manager install command again.'
29
+ )
30
+ }
31
+ return executable
32
+ }
@@ -0,0 +1,193 @@
1
+ import { performance } from 'node:perf_hooks'
2
+ import process from 'node:process'
3
+
4
+ /** Adapt pnpm's NDJSON logs; the bar measures package linking, not elapsed time. */
5
+ export function createPnpmProgress({
6
+ output = process.stdout,
7
+ errors = process.stderr,
8
+ interactive = Boolean(output.isTTY) && process.env.TERM !== 'dumb',
9
+ intervalMs = 100,
10
+ heartbeatMs = 15_000,
11
+ now = () => performance.now(),
12
+ } = {}) {
13
+ const started = now()
14
+ const counts = { resolved: 0, cached: 0, downloaded: 0, linked: 0 }
15
+ const statuses = {
16
+ resolved: 'resolved',
17
+ found_in_store: 'cached',
18
+ fetched: 'downloaded',
19
+ imported: 'linked',
20
+ }
21
+ const totals = new Map()
22
+ const seen = new Set()
23
+ let phase = 'Resolving dependencies'
24
+ let buffer = ''
25
+ let frame = 0
26
+ let finished = false
27
+ let promptOpen = false
28
+ let previousPhase
29
+ const elapsed = () => `${((now() - started) / 1000).toFixed(1)}s`
30
+ const clear = () => {
31
+ if (interactive) output.write('\r\x1b[2K')
32
+ }
33
+ function render() {
34
+ const total = totals.size
35
+ ? [...totals.values()].reduce((sum, value) => sum + value, 0)
36
+ : undefined
37
+ const width = output.columns || 100
38
+ const cells = width < 100 ? 10 : 16
39
+ const filled =
40
+ total > 0
41
+ ? Math.min(cells, Math.floor((counts.linked / total) * cells))
42
+ : 0
43
+ const bar = `[${'#'.repeat(filled)}${'-'.repeat(cells - filled)}]`
44
+ const packages =
45
+ total === undefined ? `${counts.linked}/?` : `${counts.linked}/${total}`
46
+ const detail = `${width >= 110 ? `resolved ${counts.resolved}, ` : ''}cached ${counts.cached}, downloaded ${counts.downloaded}`
47
+ const shortPhase = phase.split(' ')[0]
48
+ const text =
49
+ width < 70
50
+ ? `${packages} packages | ${elapsed()}`
51
+ : `${shortPhase} ${packages} | ${elapsed()} | ${detail}`
52
+ return `${['|', '/', '-', '\\'][frame++ % 4]} ${bar} ${text}`.slice(
53
+ 0,
54
+ Math.max(1, width - 1)
55
+ )
56
+ }
57
+ function draw(force = false) {
58
+ if (finished || promptOpen) return
59
+ if (interactive) {
60
+ // pnpm emits many debug records per package. Paint on the timer instead
61
+ // of turning each record into terminal writes during large installations.
62
+ if (!force && intervalMs > 0 && previousPhase === phase) return
63
+ clear()
64
+ output.write(render())
65
+ } else if (force || previousPhase !== phase) {
66
+ output.write(
67
+ `[RDX] ${phase}: resolved ${counts.resolved}, cached ${counts.cached}, downloaded ${counts.downloaded}, linked ${counts.linked} (${elapsed()}).\n`
68
+ )
69
+ }
70
+ previousPhase = phase
71
+ }
72
+ function message(text, stream = output) {
73
+ if (!promptOpen) clear()
74
+ stream.write(text)
75
+ // Keep prompts without a newline visible and leave the cursor after them.
76
+ promptOpen = !text.endsWith('\n')
77
+ if (text.endsWith('\n')) draw(true)
78
+ }
79
+ function record(line) {
80
+ if (!line.trim()) {
81
+ if (promptOpen) message('\n')
82
+ return
83
+ }
84
+ let event
85
+ try {
86
+ event = JSON.parse(line)
87
+ } catch {
88
+ message(line + '\n')
89
+ return
90
+ }
91
+ if (
92
+ !event ||
93
+ typeof event !== 'object' ||
94
+ typeof event.name !== 'string' ||
95
+ !(event.name === 'pnpm' || event.name.startsWith('pnpm:'))
96
+ ) {
97
+ message(line + '\n')
98
+ return
99
+ }
100
+ if (promptOpen) {
101
+ output.write('\n')
102
+ promptOpen = false
103
+ }
104
+ if (
105
+ event.name === 'pnpm:progress' &&
106
+ Object.hasOwn(statuses, event.status)
107
+ ) {
108
+ const key = statuses[event.status]
109
+ const id = event.to || event.packageId
110
+ const identity = id && JSON.stringify([event.requester, event.status, id])
111
+ if (!identity || !seen.has(identity)) {
112
+ counts[key]++
113
+ if (identity) seen.add(identity)
114
+ }
115
+ if (key === 'linked') phase = 'Linking packages'
116
+ } else if (
117
+ event.name === 'pnpm:fetching-progress' &&
118
+ event.status === 'started' &&
119
+ event.size >= 1024 * 1024
120
+ ) {
121
+ message(
122
+ `[RDX] Downloading ${event.packageId} (${(event.size / 1024 / 1024).toFixed(1)} MB).\n`
123
+ )
124
+ } else if (event.name === 'pnpm:stats' && Number.isFinite(event.added)) {
125
+ totals.set(event.prefix || '', Math.max(0, event.added))
126
+ } else if (event.name === 'pnpm:stage') {
127
+ if (event.stage === 'importing_started') phase = 'Linking packages'
128
+ if (event.stage === 'importing_done') phase = 'Finalizing installation'
129
+ } else if (event.name === 'pnpm:lifecycle' && event.line) {
130
+ message(`[pnpm:${event.stage || 'script'}] ${event.line}\n`)
131
+ } else if (event.level === 'warn' || event.level === 'error') {
132
+ const error = event.err || event
133
+ const text = [
134
+ error.code,
135
+ error.message || event.message,
136
+ error.hint,
137
+ error.pkgsStack && JSON.stringify(error.pkgsStack),
138
+ ]
139
+ .filter(Boolean)
140
+ .join('\n')
141
+ message(`[pnpm] ${text || JSON.stringify(event)}\n`, errors)
142
+ } else if (event.name === 'pnpm:peer-dependency-issues') {
143
+ message(
144
+ `[pnpm] Peer dependency issues:\n${JSON.stringify(event.issuesByProjects || event, null, 2)}\n`,
145
+ errors
146
+ )
147
+ } else if (event.level === 'info' && event.message) {
148
+ message(`[pnpm] ${event.message}\n`)
149
+ }
150
+ draw()
151
+ }
152
+ draw(true)
153
+ const timer =
154
+ intervalMs > 0
155
+ ? setInterval(() => draw(true), interactive ? intervalMs : heartbeatMs)
156
+ : undefined
157
+ timer?.unref()
158
+ return {
159
+ stdout(chunk) {
160
+ buffer += chunk
161
+ let end
162
+ while ((end = buffer.indexOf('\n')) !== -1) {
163
+ const line = buffer.slice(0, end).replace(/\r$/, '')
164
+ buffer = buffer.slice(end + 1)
165
+ record(line)
166
+ }
167
+ if (buffer && !buffer.trimStart().startsWith('{')) {
168
+ message(buffer)
169
+ buffer = ''
170
+ }
171
+ },
172
+ stderr(chunk) {
173
+ message(chunk, errors)
174
+ },
175
+ finish(success) {
176
+ if (finished) return
177
+ clearInterval(timer)
178
+ if (buffer) {
179
+ record(buffer)
180
+ buffer = ''
181
+ }
182
+ finished = true
183
+ if (promptOpen) {
184
+ output.write('\n')
185
+ promptOpen = false
186
+ }
187
+ clear()
188
+ output.write(
189
+ `[RDX] ${success ? 'Dependencies installed' : 'Installation stopped'}: ${counts.linked} linked, ${counts.cached} cached, ${counts.downloaded} downloaded (${elapsed()}).\n`
190
+ )
191
+ },
192
+ }
193
+ }
@@ -0,0 +1,26 @@
1
+ import { performance } from 'node:perf_hooks'
2
+
3
+ /** Show real stage timings while preserving the installer's own terminal output. */
4
+ export async function runStep(
5
+ label,
6
+ operation,
7
+ { log = console.log, intervalMs = 15_000 } = {}
8
+ ) {
9
+ const started = performance.now()
10
+ const elapsed = () => ((performance.now() - started) / 1000).toFixed(1)
11
+ log(`\n${label}`)
12
+ const timer =
13
+ intervalMs > 0
14
+ ? setInterval(() => {
15
+ log(`[RDX] ${label} (${elapsed()}s elapsed)`)
16
+ }, intervalMs)
17
+ : undefined
18
+ timer?.unref()
19
+ try {
20
+ const result = await operation()
21
+ log(`[RDX] Completed in ${elapsed()}s.`)
22
+ return result
23
+ } finally {
24
+ clearInterval(timer)
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexibase/create-rdx",
3
- "version": "4.1.0",
3
+ "version": "4.2.1",
4
4
  "description": "Create a new RDX documentation project.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -51,7 +51,8 @@ public/images/rdx.svg → /images/rdx.svg
51
51
  public/downloads/example.txt → /downloads/example.txt
52
52
  ```
53
53
 
54
- Use site-root URLs in MDX:
54
+ Write pages as `.md` or `.mdx` inside `docs/`. Both formats support RDX's
55
+ Markdown components. Use site-root URLs for assets:
55
56
 
56
57
  ```md
57
58
  ![RDX logo](/images/rdx.svg)
@@ -6,5 +6,5 @@ link_position: 1
6
6
 
7
7
  # Welcome to your documentation
8
8
 
9
- RDX turns folders and `.mdx` files into a documentation site. Start by editing
9
+ RDX turns folders and `.md` or `.mdx` files into a documentation site. Start by editing
10
10
  this page or adding new pages inside `docs/`.
@@ -9,7 +9,7 @@
9
9
  "start": "rdx start"
10
10
  },
11
11
  "dependencies": {
12
- "@lexibase/rdx": "4.1.0",
12
+ "@lexibase/rdx": "4.2.1",
13
13
  "next": "^16.4.2",
14
14
  "react": "19.3.1",
15
15
  "react-dom": "19.3.1"
@@ -32,7 +32,8 @@ export default defineConfig({
32
32
  // Optional navbar text does not change metadata.applicationName.
33
33
  navbar: {
34
34
  title: '', // Text beside the logo; empty or omitted shows only the logo.
35
- // logo: Logo, // Import a React component that accepts className, not an image path.
35
+ // logo: '/images/logo.png', // File in public/images/; PNG, SVG and other image formats work.
36
+ // Or import your components/logo.tsx and set logo: Logo.
36
37
  actions: {
37
38
  github: {
38
39
  visible: true,