@inovus-medical/docs 0.8.0 → 0.9.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
@@ -17,22 +17,24 @@ npm install --save-dev @inovus-medical/docs
17
17
  Put markdown in `docs/`, add a `docs.config.json`, then:
18
18
 
19
19
  ```bash
20
- npx inovus-docs build # docs/ -> dist/
20
+ npx inovus-docs build # content -> dist/ (or --out dist-docs)
21
21
  npx inovus-docs dev # build + serve + watch (http://localhost:8788)
22
22
  npx inovus-docs serve # serve an existing dist/
23
+ npx inovus-docs init # scaffold into the current repo (safe in app repos)
23
24
  ```
24
25
 
25
26
  Flags: `--root <dir>` `--in <docs>` `--out <dist>` `--port <n>` `--theme <name>`.
27
+ `init` also accepts `--content <dir>` and merges without overwriting app `build` scripts.
26
28
 
27
29
  ## Themes
28
30
 
29
- Named themes each include light and dark variants. Built-ins: `default`, `totum`.
31
+ Named themes each include light and dark variants. Built-ins: `totum` (default), `default`.
30
32
 
31
33
  Selection (first match wins): `--theme` > `DOCS_THEME` env var > `"theme"` in
32
- `docs.config.json` > `default`. On Cloudflare, set `DOCS_THEME` under Worker →
34
+ `docs.config.json` > `totum`. On Cloudflare, set `DOCS_THEME` under Worker →
33
35
  **Settings → Build → Variables** (build-time).
34
36
 
35
- Local preview: `npx inovus-docs dev --theme totum`.
37
+ Local preview: `npx inovus-docs dev` (Totum) or `--theme default`.
36
38
 
37
39
  ## What it does
38
40
 
package/bin/cli.js CHANGED
@@ -35,18 +35,39 @@ const theme = typeof flags.theme === 'string' ? flags.theme : undefined
35
35
 
36
36
  async function run() {
37
37
  if (cmd === 'init') {
38
- const { created, skipped, name, title, visibility, repoUrl, home } = await init({ root, flags })
39
- console.log(`Scaffolded "${title}" (worker: ${name}, ${visibility})${repoUrl ? ` — ${repoUrl}` : ''}`)
38
+ const result = await init({ root, flags })
39
+ const {
40
+ created,
41
+ skipped,
42
+ notes,
43
+ name,
44
+ title,
45
+ visibility,
46
+ repoUrl,
47
+ home,
48
+ content,
49
+ buildCommand,
50
+ deployCommand,
51
+ coexisting
52
+ } = result
53
+ console.log(
54
+ `Scaffolded "${title}" (worker: ${name}, ${visibility})${repoUrl ? ` — ${repoUrl}` : ''}`
55
+ )
56
+ if (content && content !== '.') console.log(` content: ${content}/`)
57
+ if (coexisting) console.log(' mode: coexisting with existing package.json (docs scripts are docs:*)')
40
58
  if (created.length) console.log(' created: ' + created.join(', '))
41
59
  if (skipped.length) console.log(' kept existing (use --force to overwrite): ' + skipped.join(', '))
60
+ for (const n of notes || []) console.log(' ⚠ ' + n)
42
61
  if (!home) {
43
- console.log(' ⚠ No home page (README.md / index.md) and --no-readme set — the site will have no page at /.')
62
+ console.log(
63
+ ' ⚠ No home page (README.md / index.md) in the content folder — add one or the site will have no page at /.'
64
+ )
44
65
  }
45
66
  console.log('\nNext:')
46
67
  console.log(' npm install')
47
- console.log(' npm run dev # preview at http://localhost:8788')
68
+ console.log(' npm run docs:dev # preview at http://localhost:8788')
48
69
  console.log(' git add -A && git commit -m "Add docs site config" && git push')
49
- console.log(' then connect the repo in Cloudflare (build: npm install && npm run build)')
70
+ console.log(` Cloudflare: build \`${buildCommand}\`, deploy \`${deployCommand}\``)
50
71
  return
51
72
  }
52
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inovus-medical/docs",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Turn a folder of markdown into a themed, Cloudflare-ready docs site.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/build.js CHANGED
@@ -31,18 +31,18 @@ async function listThemes() {
31
31
  }
32
32
 
33
33
  /**
34
- * Resolve theme: CLI --theme > DOCS_THEME env > docs.config.json > "default".
35
- * Unknown names warn and fall back to default.
34
+ * Resolve theme: CLI --theme > DOCS_THEME env > docs.config.json > "totum".
35
+ * Unknown names warn and fall back to totum.
36
36
  */
37
37
  async function resolveTheme({ themeFlag, config }) {
38
- const raw = themeFlag ?? process.env.DOCS_THEME ?? config.theme ?? 'default'
39
- const name = String(raw).trim().toLowerCase() || 'default'
38
+ const raw = themeFlag ?? process.env.DOCS_THEME ?? config.theme ?? 'totum'
39
+ const name = String(raw).trim().toLowerCase() || 'totum'
40
40
  const available = await listThemes()
41
41
  if (!available.has(name)) {
42
42
  console.warn(
43
- `⚠ Unknown theme "${name}" — falling back to default. Available: ${[...available].sort().join(', ')}`
43
+ `⚠ Unknown theme "${name}" — falling back to totum. Available: ${[...available].sort().join(', ')}`
44
44
  )
45
- return 'default'
45
+ return 'totum'
46
46
  }
47
47
  return name
48
48
  }
package/src/config.js CHANGED
@@ -5,10 +5,10 @@ const DEFAULTS = {
5
5
  title: 'Documentation',
6
6
  description: '',
7
7
  logo: null,
8
- // null = use the theme's accent (base styles.css sets --accent for default).
8
+ // null = use the theme's accent (theme CSS / base styles set --accent).
9
9
  accent: null,
10
10
  // Named theme (each has light + dark). Overridden by DOCS_THEME / --theme.
11
- theme: 'default',
11
+ theme: 'totum',
12
12
  repo: null,
13
13
  editBase: null,
14
14
  visibility: 'private',
package/src/init.js CHANGED
@@ -6,11 +6,18 @@ import { execSync } from 'node:child_process'
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
7
 
8
8
  async function fileExists(p) {
9
- try { await fs.access(p); return true } catch { return false }
9
+ try {
10
+ await fs.access(p)
11
+ return true
12
+ } catch {
13
+ return false
14
+ }
10
15
  }
11
16
 
12
17
  async function readJsonIfExists(file) {
13
- try { return JSON.parse(await fs.readFile(file, 'utf8')) } catch (e) {
18
+ try {
19
+ return JSON.parse(await fs.readFile(file, 'utf8'))
20
+ } catch (e) {
14
21
  if (e.code === 'ENOENT') return null
15
22
  throw e
16
23
  }
@@ -18,10 +25,14 @@ async function readJsonIfExists(file) {
18
25
 
19
26
  function gitRemote(root) {
20
27
  try {
21
- return execSync('git config --get remote.origin.url', {
22
- cwd: root,
23
- stdio: ['ignore', 'pipe', 'ignore']
24
- }).toString().trim() || null
28
+ return (
29
+ execSync('git config --get remote.origin.url', {
30
+ cwd: root,
31
+ stdio: ['ignore', 'pipe', 'ignore']
32
+ })
33
+ .toString()
34
+ .trim() || null
35
+ )
25
36
  } catch {
26
37
  return null
27
38
  }
@@ -44,35 +55,44 @@ function parseRemote(url) {
44
55
 
45
56
  function sanitizeName(s) {
46
57
  return (
47
- String(s).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 54) ||
48
- 'docs-site'
58
+ String(s)
59
+ .toLowerCase()
60
+ .replace(/[^a-z0-9-]+/g, '-')
61
+ .replace(/^-+|-+$/g, '')
62
+ .slice(0, 54) || 'docs-site'
49
63
  )
50
64
  }
51
65
 
52
66
  function titleCase(s) {
53
- return String(s).replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim().replace(/\b\w/g, (c) => c.toUpperCase())
67
+ return String(s)
68
+ .replace(/[-_]/g, ' ')
69
+ .replace(/\s+/g, ' ')
70
+ .trim()
71
+ .replace(/\b\w/g, (c) => c.toUpperCase())
54
72
  }
55
73
 
56
- const WRANGLER = (name) => `{
74
+ const WRANGLER = (name, outDir) => `{
57
75
  "$schema": "node_modules/wrangler/config-schema.json",
58
76
  "name": "${name}",
59
77
  "compatibility_date": "2025-06-01",
60
78
  "assets": {
61
- "directory": "./dist",
79
+ "directory": "./${outDir}",
62
80
  "html_handling": "auto-trailing-slash",
63
81
  "not_found_handling": "404-page"
64
82
  }
65
83
  }
66
84
  `
67
85
 
68
- async function ensureGitignore(root, created) {
86
+ async function ensureGitignore(root, entries, created) {
69
87
  const file = path.join(root, '.gitignore')
70
88
  let body = ''
71
- try { body = await fs.readFile(file, 'utf8') } catch {}
89
+ try {
90
+ body = await fs.readFile(file, 'utf8')
91
+ } catch {}
72
92
  const present = new Set(body.split(/\r?\n/).map((l) => l.trim()))
73
93
  let next = body
74
94
  let changed = false
75
- for (const entry of ['node_modules/', 'dist/']) {
95
+ for (const entry of entries) {
76
96
  if (!present.has(entry)) {
77
97
  next += (next && !next.endsWith('\n') ? '\n' : '') + entry + '\n'
78
98
  changed = true
@@ -84,9 +104,10 @@ async function ensureGitignore(root, created) {
84
104
  }
85
105
  }
86
106
 
87
- async function hasHomePage(root) {
88
- const candidates = ['README.md', 'readme.md', 'index.md', 'docs/README.md', 'docs/index.md']
89
- for (const c of candidates) if (await fileExists(path.join(root, c))) return true
107
+ async function hasHomePageIn(dir) {
108
+ for (const c of ['README.md', 'readme.md', 'index.md']) {
109
+ if (await fileExists(path.join(dir, c))) return true
110
+ }
90
111
  return false
91
112
  }
92
113
 
@@ -96,82 +117,154 @@ function starterReadme(title) {
96
117
  Welcome to the ${title} documentation.
97
118
 
98
119
  This is the home page of your docs site. Edit this \`README.md\` to change it, and add
99
- more \`.md\` files anywhere in the repo — they show up in the sidebar automatically.
120
+ more \`.md\` files alongside it — they show up in the sidebar automatically.
100
121
  `
101
122
  }
102
123
 
103
- /** Scaffold the per-repo files needed to publish a markdown repo as a docs site. */
124
+ /**
125
+ * Scaffold the per-repo files needed to publish markdown as a docs site.
126
+ * Designed to co-exist with normal app repos: never overwrites app build/dev scripts,
127
+ * avoids clobbering an existing Wrangler config, and uses a separate output dir when needed.
128
+ */
104
129
  export async function init({ root = process.cwd(), flags = {} } = {}) {
105
130
  const enginePkg = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf8'))
106
131
  const engineVersion = `^${enginePkg.version}`
107
132
 
108
133
  const remote = parseRemote(gitRemote(root))
109
134
  const folder = path.basename(root)
110
- const name = sanitizeName(flags.name || remote.repo || folder)
111
- const title = flags.title || titleCase(remote.repo || folder)
135
+ const existingPkg = await readJsonIfExists(path.join(root, 'package.json'))
136
+ const coexisting = Boolean(existingPkg)
137
+ const baseName = remote.repo || folder
138
+ const name = sanitizeName(flags.name || (coexisting ? `${baseName}-docs` : baseName))
139
+ const title = flags.title || titleCase(baseName)
112
140
  const visibility = flags.public ? 'public' : 'private'
113
141
  const repoUrl = flags.repo || remote.httpsUrl || null
114
142
  const force = flags.force === true
143
+
115
144
  // Content root: --content <dir>, else auto-set "docs" when docs/ exists, else omit (auto-detect at build).
116
145
  let content = typeof flags.content === 'string' ? flags.content.trim() : null
117
146
  if (!content && (await fileExists(path.join(root, 'docs')))) content = 'docs'
118
147
 
148
+ // App repos often already use dist/ and wrangler.jsonc — keep docs out of their way.
149
+ const outDir = coexisting ? 'dist-docs' : 'dist'
150
+ const buildScript = outDir === 'dist' ? 'inovus-docs build' : `inovus-docs build --out ${outDir}`
151
+ const devScript = outDir === 'dist' ? 'inovus-docs dev' : `inovus-docs dev --out ${outDir}`
152
+
153
+ const hasAppWrangler =
154
+ (await fileExists(path.join(root, 'wrangler.jsonc'))) ||
155
+ (await fileExists(path.join(root, 'wrangler.toml'))) ||
156
+ (await fileExists(path.join(root, 'wrangler.json')))
157
+ const wranglerFile = coexisting && hasAppWrangler ? 'wrangler.docs.jsonc' : 'wrangler.jsonc'
158
+
119
159
  const created = []
120
160
  const skipped = []
161
+ const notes = []
121
162
 
122
163
  // docs.config.json (don't clobber existing unless --force)
123
164
  const cfgPath = path.join(root, 'docs.config.json')
124
165
  if ((await fileExists(cfgPath)) && !force) {
125
166
  skipped.push('docs.config.json')
126
167
  } else {
127
- const cfg = { title, description: '', visibility }
168
+ const cfg = { title, description: '', visibility, theme: 'totum' }
128
169
  if (repoUrl) cfg.repo = repoUrl
129
170
  if (content) cfg.content = content
130
171
  await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
131
172
  created.push('docs.config.json')
132
173
  }
133
174
 
134
- // wrangler.jsonc
135
- const wranglerPath = path.join(root, 'wrangler.jsonc')
175
+ // Wrangler config — never overwrite an app's wrangler unless --force on wrangler.jsonc path
176
+ const wranglerPath = path.join(root, wranglerFile)
136
177
  if ((await fileExists(wranglerPath)) && !force) {
137
- skipped.push('wrangler.jsonc')
178
+ skipped.push(wranglerFile)
138
179
  } else {
139
- await fs.writeFile(wranglerPath, WRANGLER(name))
140
- created.push('wrangler.jsonc')
180
+ await fs.writeFile(wranglerPath, WRANGLER(name, outDir))
181
+ created.push(wranglerFile)
182
+ if (wranglerFile !== 'wrangler.jsonc') {
183
+ notes.push(
184
+ `App Wrangler config left alone; docs deploy uses: npx wrangler deploy -c ${wranglerFile}`
185
+ )
186
+ }
141
187
  }
142
188
 
143
- // package.json (merge if present — never remove existing fields)
189
+ // package.json merge only; never replace existing build/dev
144
190
  const pkgPath = path.join(root, 'package.json')
145
- const existingPkg = await readJsonIfExists(pkgPath)
146
- const pkg = existingPkg || { name, private: true }
147
- pkg.scripts = pkg.scripts || {}
148
- if (!pkg.scripts.build) pkg.scripts.build = 'inovus-docs build'
149
- if (!pkg.scripts.dev) pkg.scripts.dev = 'inovus-docs dev'
150
- pkg.devDependencies = pkg.devDependencies || {}
191
+ const pkg = existingPkg ? { ...existingPkg } : { name, private: true }
192
+ pkg.scripts = { ...(pkg.scripts || {}) }
193
+ pkg.scripts['docs:build'] = buildScript
194
+ pkg.scripts['docs:dev'] = devScript
195
+ // Convenience aliases only when the repo doesn't already define them (pure docs repos).
196
+ if (!pkg.scripts.build) {
197
+ pkg.scripts.build = 'npm run docs:build'
198
+ } else {
199
+ notes.push(
200
+ `Left existing "build" script alone. Cloudflare build command should be: npm install && npm run docs:build`
201
+ )
202
+ }
203
+ if (!pkg.scripts.dev) {
204
+ pkg.scripts.dev = 'npm run docs:dev'
205
+ }
206
+ if (wranglerFile !== 'wrangler.jsonc' && !pkg.scripts['docs:deploy']) {
207
+ pkg.scripts['docs:deploy'] = `wrangler deploy -c ${wranglerFile}`
208
+ }
209
+ pkg.devDependencies = { ...(pkg.devDependencies || {}) }
151
210
  pkg.devDependencies[enginePkg.name] = engineVersion
211
+ if (!pkg.devDependencies.wrangler) {
212
+ pkg.devDependencies.wrangler = '^4'
213
+ }
152
214
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
153
215
  created.push(existingPkg ? 'package.json (updated)' : 'package.json')
154
216
 
155
- // .node-version for Cloudflare's build image (Wrangler needs Node 22)
217
+ // .node-version — only for greenfield docs repos (apps often pin their own Node).
156
218
  const nodeVerPath = path.join(root, '.node-version')
157
- if (!(await fileExists(nodeVerPath))) {
219
+ if (!coexisting && !(await fileExists(nodeVerPath))) {
158
220
  await fs.writeFile(nodeVerPath, '22\n')
159
221
  created.push('.node-version')
222
+ } else if (coexisting && !(await fileExists(nodeVerPath))) {
223
+ notes.push('Set Node 22 in the Cloudflare Worker build settings (Wrangler requires it).')
160
224
  }
161
225
 
162
- await ensureGitignore(root, created)
226
+ const ignore = ['node_modules/', `${outDir}/`]
227
+ await ensureGitignore(root, ignore, created)
163
228
 
164
- // Guarantee a landing page: if the repo has no README/index anywhere, drop a starter.
165
- let home = await hasHomePage(root)
229
+ // Home page inside the content root (never invent a root README over an app's README).
230
+ const contentRel = content || '.'
231
+ const contentRoot = path.join(root, contentRel)
232
+ let home = await hasHomePageIn(contentRoot)
166
233
  if (!home && !flags['no-readme']) {
167
- const inDocsMode = await fileExists(path.join(root, 'docs'))
168
- const rel = inDocsMode ? path.join('docs', 'README.md') : 'README.md'
169
- const readmePath = path.join(root, rel)
234
+ await fs.mkdir(contentRoot, { recursive: true })
235
+ const rel = path.join(contentRel === '.' ? '' : contentRel, 'README.md').replace(/^\//, '')
236
+ const readmePath = path.join(root, rel || 'README.md')
170
237
  await fs.mkdir(path.dirname(readmePath), { recursive: true })
171
- await fs.writeFile(readmePath, starterReadme(title))
172
- created.push(rel.split(path.sep).join('/'))
173
- home = true
238
+ // Don't overwrite an existing root README when content is "."
239
+ if (!(await fileExists(readmePath))) {
240
+ await fs.writeFile(readmePath, starterReadme(title))
241
+ created.push((rel || 'README.md').split(path.sep).join('/'))
242
+ home = true
243
+ } else {
244
+ home = true
245
+ }
174
246
  }
175
247
 
176
- return { created, skipped, name, title, visibility, repoUrl, home }
248
+ const buildCommand = 'npm install && npm run docs:build'
249
+ const deployCommand =
250
+ wranglerFile === 'wrangler.jsonc'
251
+ ? 'npx wrangler deploy'
252
+ : `npx wrangler deploy -c ${wranglerFile}`
253
+
254
+ return {
255
+ created,
256
+ skipped,
257
+ notes,
258
+ name,
259
+ title,
260
+ visibility,
261
+ repoUrl,
262
+ home,
263
+ content: contentRel,
264
+ outDir,
265
+ wranglerFile,
266
+ buildCommand,
267
+ deployCommand,
268
+ coexisting
269
+ }
177
270
  }
package/src/layout.js CHANGED
@@ -74,7 +74,7 @@ function header(config) {
74
74
 
75
75
  function shell({ config, pageTitle, description, navHtml, main }) {
76
76
  const accent = config.accent ? `<style>:root{--accent:${esc(config.accent)}}</style>` : ''
77
- const docsTheme = (config.theme && config.theme !== 'default') ? config.theme : 'default'
77
+ const docsTheme = config.theme || 'totum'
78
78
  const themeLink =
79
79
  docsTheme !== 'default' ? '<link rel="stylesheet" href="/_assets/theme.css">\n' : ''
80
80
  const desc = description || config.description || ''