@inovus-medical/docs 0.8.1 → 0.10.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
@@ -29,35 +29,75 @@ const flags = parseFlags(cmd === argv[0] ? argv.slice(1) : argv)
29
29
 
30
30
  const root = path.resolve(flags.root || process.cwd())
31
31
  const inDir = flags.in // undefined -> build() auto-detects (docs/ or repo root)
32
- const outDir = flags.out || 'dist'
32
+ const outDirFlag = typeof flags.out === 'string' ? flags.out : undefined
33
33
  const port = Number(flags.port || 8788)
34
34
  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
+ localInstall,
52
+ localDev,
53
+ packageManager,
54
+ coexisting
55
+ } = result
56
+ console.log(
57
+ `Scaffolded "${title}" (worker: ${name}, ${visibility})${repoUrl ? ` — ${repoUrl}` : ''}`
58
+ )
59
+ if (content && content !== '.') console.log(` content: ${content}/`)
60
+ if (coexisting) console.log(' mode: coexisting with existing package.json (docs scripts are docs:*)')
61
+ if (packageManager) console.log(` package manager: ${packageManager}`)
40
62
  if (created.length) console.log(' created: ' + created.join(', '))
41
63
  if (skipped.length) console.log(' kept existing (use --force to overwrite): ' + skipped.join(', '))
64
+ for (const n of notes || []) console.log(' ⚠ ' + n)
42
65
  if (!home) {
43
- console.log(' ⚠ No home page (README.md / index.md) and --no-readme set — the site will have no page at /.')
66
+ console.log(
67
+ ' ⚠ No home page (README.md / index.md) in the content folder — add one or the site will have no page at /.'
68
+ )
44
69
  }
45
70
  console.log('\nNext:')
46
- console.log(' npm install')
47
- console.log(' npm run dev # preview at http://localhost:8788')
71
+ console.log(` ${localInstall}`)
72
+ console.log(` ${localDev} # preview at http://localhost:8788`)
48
73
  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)')
74
+ console.log(' (Commit the lockfile too Cloudflare installs with --frozen-lockfile.)')
75
+ console.log('\nCloudflare Worker (same for every repo — copy exactly):')
76
+ console.log(` Build command: ${buildCommand}`)
77
+ console.log(` Deploy command: ${deployCommand}`)
50
78
  return
51
79
  }
52
80
 
53
81
  if (cmd === 'build') {
54
- const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir, theme })
55
- console.log(`✓ Built ${pageCount} page(s) from ${contentDir === '.' ? 'repo root' : contentDir + '/'} -> ${path.relative(process.cwd(), outPath) || outPath}`)
82
+ const { pageCount, outPath, contentDir, outDir } = await build({
83
+ root,
84
+ inDir,
85
+ outDir: outDirFlag,
86
+ theme
87
+ })
88
+ console.log(
89
+ `✓ Built ${pageCount} page(s) from ${contentDir === '.' ? 'repo root' : contentDir + '/'} -> ${path.relative(process.cwd(), outPath) || outDir}`
90
+ )
56
91
  return
57
92
  }
58
93
 
59
94
  if (cmd === 'dev') {
60
- const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir, theme })
95
+ const { pageCount, outPath, contentDir, outDir } = await build({
96
+ root,
97
+ inDir,
98
+ outDir: outDirFlag,
99
+ theme
100
+ })
61
101
  console.log(`✓ Built ${pageCount} page(s) from ${contentDir === '.' ? 'repo root' : contentDir + '/'}`)
62
102
  const { port: actual } = await serve({ dir: outPath, port })
63
103
  const busy = actual !== port ? ` (port ${port} was busy)` : ''
@@ -67,6 +107,9 @@ async function run() {
67
107
  }
68
108
 
69
109
  if (cmd === 'serve') {
110
+ const { loadConfig } = await import('../src/config.js')
111
+ const config = await loadConfig(root)
112
+ const outDir = outDirFlag || config.outDir || 'dist'
70
113
  const outPath = path.join(root, outDir)
71
114
  const { port: actual } = await serve({ dir: outPath, port })
72
115
  const busy = actual !== port ? ` (port ${port} was busy)` : ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inovus-medical/docs",
3
- "version": "0.8.1",
3
+ "version": "0.10.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
  }
@@ -119,11 +119,12 @@ function routeToFile(outPath, route) {
119
119
  return path.join(outPath, rel, 'index.html')
120
120
  }
121
121
 
122
- export async function build({ root = process.cwd(), inDir, outDir = 'dist', theme: themeFlag } = {}) {
122
+ export async function build({ root = process.cwd(), inDir, outDir, theme: themeFlag } = {}) {
123
123
  const config = await loadConfig(root)
124
124
  const theme = await resolveTheme({ themeFlag, config })
125
125
  config.theme = theme
126
- const outPath = path.join(root, outDir)
126
+ const resolvedOut = outDir || config.outDir || 'dist'
127
+ const outPath = path.join(root, resolvedOut)
127
128
  const ignore = makeIgnore(config, path.basename(outPath))
128
129
 
129
130
  const contentDir = await resolveContentDir({ root, inDir, config, ignore })
@@ -235,5 +236,5 @@ export async function build({ root = process.cwd(), inDir, outDir = 'dist', them
235
236
  await fs.copyFile(file, dest)
236
237
  }
237
238
 
238
- return { pageCount: pages.length, outPath, contentDir }
239
+ return { pageCount: pages.length, outPath, contentDir, outDir: resolvedOut }
239
240
  }
package/src/config.js CHANGED
@@ -5,10 +5,12 @@ 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
+ // Build output directory. null → "dist". App repos usually use "dist-docs".
13
+ outDir: null,
12
14
  repo: null,
13
15
  editBase: null,
14
16
  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,72 @@ 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())
72
+ }
73
+
74
+ /**
75
+ * Detect the repo's package manager from lockfiles / packageManager field.
76
+ * Cloudflare Workers Builds does the same (pnpm/yarn/npm from lockfile).
77
+ */
78
+ async function detectPackageManager(root, pkg) {
79
+ if (await fileExists(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm'
80
+ if (await fileExists(path.join(root, 'yarn.lock'))) return 'yarn'
81
+ if (
82
+ (await fileExists(path.join(root, 'bun.lockb'))) ||
83
+ (await fileExists(path.join(root, 'bun.lock')))
84
+ ) {
85
+ return 'bun'
86
+ }
87
+ const field = pkg && typeof pkg.packageManager === 'string' ? pkg.packageManager : ''
88
+ if (field.startsWith('pnpm@')) return 'pnpm'
89
+ if (field.startsWith('yarn@')) return 'yarn'
90
+ if (field.startsWith('bun@')) return 'bun'
91
+ if (await fileExists(path.join(root, 'package-lock.json'))) return 'npm'
92
+ return 'npm'
93
+ }
94
+
95
+ function runScript(pm, script) {
96
+ if (pm === 'yarn') return `yarn ${script}`
97
+ if (pm === 'bun') return `bun run ${script}`
98
+ if (pm === 'pnpm') return `pnpm run ${script}`
99
+ return `npm run ${script}`
54
100
  }
55
101
 
56
- const WRANGLER = (name) => `{
102
+ const WRANGLER = (name, outDir) => `{
57
103
  "$schema": "node_modules/wrangler/config-schema.json",
58
104
  "name": "${name}",
59
105
  "compatibility_date": "2025-06-01",
60
106
  "assets": {
61
- "directory": "./dist",
107
+ "directory": "./${outDir}",
62
108
  "html_handling": "auto-trailing-slash",
63
109
  "not_found_handling": "404-page"
64
110
  }
65
111
  }
66
112
  `
67
113
 
68
- async function ensureGitignore(root, created) {
114
+ async function ensureGitignore(root, entries, created) {
69
115
  const file = path.join(root, '.gitignore')
70
116
  let body = ''
71
- try { body = await fs.readFile(file, 'utf8') } catch {}
117
+ try {
118
+ body = await fs.readFile(file, 'utf8')
119
+ } catch {}
72
120
  const present = new Set(body.split(/\r?\n/).map((l) => l.trim()))
73
121
  let next = body
74
122
  let changed = false
75
- for (const entry of ['node_modules/', 'dist/']) {
123
+ for (const entry of entries) {
76
124
  if (!present.has(entry)) {
77
125
  next += (next && !next.endsWith('\n') ? '\n' : '') + entry + '\n'
78
126
  changed = true
@@ -84,9 +132,10 @@ async function ensureGitignore(root, created) {
84
132
  }
85
133
  }
86
134
 
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
135
+ async function hasHomePageIn(dir) {
136
+ for (const c of ['README.md', 'readme.md', 'index.md']) {
137
+ if (await fileExists(path.join(dir, c))) return true
138
+ }
90
139
  return false
91
140
  }
92
141
 
@@ -96,82 +145,189 @@ function starterReadme(title) {
96
145
  Welcome to the ${title} documentation.
97
146
 
98
147
  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.
148
+ more \`.md\` files alongside it — they show up in the sidebar automatically.
100
149
  `
101
150
  }
102
151
 
103
- /** Scaffold the per-repo files needed to publish a markdown repo as a docs site. */
152
+ /**
153
+ * Scaffold the per-repo files needed to publish markdown as a docs site.
154
+ * Designed to co-exist with normal app repos: never overwrites app build/dev scripts,
155
+ * avoids clobbering an existing Wrangler config, and uses a separate output dir when needed.
156
+ */
104
157
  export async function init({ root = process.cwd(), flags = {} } = {}) {
105
158
  const enginePkg = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf8'))
106
159
  const engineVersion = `^${enginePkg.version}`
107
160
 
108
161
  const remote = parseRemote(gitRemote(root))
109
162
  const folder = path.basename(root)
110
- const name = sanitizeName(flags.name || remote.repo || folder)
111
- const title = flags.title || titleCase(remote.repo || folder)
163
+ const existingPkg = await readJsonIfExists(path.join(root, 'package.json'))
164
+ const coexisting = Boolean(existingPkg)
165
+ const baseName = remote.repo || folder
166
+ const name = sanitizeName(flags.name || (coexisting ? `${baseName}-docs` : baseName))
167
+ const title = flags.title || titleCase(baseName)
112
168
  const visibility = flags.public ? 'public' : 'private'
113
169
  const repoUrl = flags.repo || remote.httpsUrl || null
114
170
  const force = flags.force === true
171
+
115
172
  // Content root: --content <dir>, else auto-set "docs" when docs/ exists, else omit (auto-detect at build).
116
173
  let content = typeof flags.content === 'string' ? flags.content.trim() : null
117
174
  if (!content && (await fileExists(path.join(root, 'docs')))) content = 'docs'
118
175
 
176
+ // App repos often already use dist/ — keep docs output out of the way via config.outDir.
177
+ const outDir = coexisting ? 'dist-docs' : 'dist'
178
+ // One Cloudflare build command for every repo (npm/pnpm/yarn). Reads docs.config.json.
179
+ const buildCommand = 'npx inovus-docs build'
180
+ const pm = await detectPackageManager(root, existingPkg)
181
+ const localInstall = pm === 'yarn' ? 'yarn' : pm === 'bun' ? 'bun install' : `${pm} install`
182
+ const localDev = runScript(pm, 'docs:dev')
183
+
184
+ const hasAppWrangler =
185
+ (await fileExists(path.join(root, 'wrangler.jsonc'))) ||
186
+ (await fileExists(path.join(root, 'wrangler.toml'))) ||
187
+ (await fileExists(path.join(root, 'wrangler.json')))
188
+ const wranglerFile = coexisting && hasAppWrangler ? 'wrangler.docs.jsonc' : 'wrangler.jsonc'
189
+
119
190
  const created = []
120
191
  const skipped = []
192
+ const notes = []
121
193
 
122
- // docs.config.json (don't clobber existing unless --force)
194
+ // docs.config.json — create or merge additive fields (never wipe an existing file)
123
195
  const cfgPath = path.join(root, 'docs.config.json')
124
196
  if ((await fileExists(cfgPath)) && !force) {
125
- skipped.push('docs.config.json')
197
+ const cfg = (await readJsonIfExists(cfgPath)) || {}
198
+ let changed = false
199
+ if (content && !cfg.content) {
200
+ cfg.content = content
201
+ changed = true
202
+ }
203
+ if (coexisting && !cfg.outDir) {
204
+ cfg.outDir = outDir
205
+ changed = true
206
+ }
207
+ if (!cfg.theme) {
208
+ cfg.theme = 'totum'
209
+ changed = true
210
+ }
211
+ if (changed) {
212
+ await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
213
+ created.push('docs.config.json (updated)')
214
+ } else {
215
+ skipped.push('docs.config.json')
216
+ }
126
217
  } else {
127
- const cfg = { title, description: '', visibility }
218
+ const cfg = { title, description: '', visibility, theme: 'totum' }
128
219
  if (repoUrl) cfg.repo = repoUrl
129
220
  if (content) cfg.content = content
221
+ if (outDir !== 'dist') cfg.outDir = outDir
130
222
  await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
131
223
  created.push('docs.config.json')
132
224
  }
133
225
 
134
- // wrangler.jsonc
135
- const wranglerPath = path.join(root, 'wrangler.jsonc')
226
+ // Wrangler config — never overwrite an app's wrangler unless --force on our docs file
227
+ const wranglerPath = path.join(root, wranglerFile)
136
228
  if ((await fileExists(wranglerPath)) && !force) {
137
- skipped.push('wrangler.jsonc')
229
+ skipped.push(wranglerFile)
138
230
  } else {
139
- await fs.writeFile(wranglerPath, WRANGLER(name))
140
- created.push('wrangler.jsonc')
231
+ await fs.writeFile(wranglerPath, WRANGLER(name, outDir))
232
+ created.push(wranglerFile)
233
+ if (wranglerFile !== 'wrangler.jsonc') {
234
+ notes.push(
235
+ `App Wrangler config left alone; docs deploy uses: npx wrangler deploy -c ${wranglerFile}`
236
+ )
237
+ }
141
238
  }
142
239
 
143
- // package.json (merge if present — never remove existing fields)
240
+ // package.json merge only; never replace existing build/dev
144
241
  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 || {}
242
+ const pkg = existingPkg ? { ...existingPkg } : { name, private: true }
243
+ pkg.scripts = { ...(pkg.scripts || {}) }
244
+ // Local convenience scripts (CF uses `npx inovus-docs build` — not these names).
245
+ pkg.scripts['docs:build'] = 'inovus-docs build'
246
+ pkg.scripts['docs:dev'] = 'inovus-docs dev'
247
+ if (!pkg.scripts.build) {
248
+ pkg.scripts.build = 'inovus-docs build'
249
+ } else {
250
+ notes.push(
251
+ `Left existing "build" script alone (app build). Cloudflare must use: ${buildCommand}`
252
+ )
253
+ }
254
+ if (!pkg.scripts.dev) {
255
+ pkg.scripts.dev = 'inovus-docs dev'
256
+ }
257
+ if (wranglerFile !== 'wrangler.jsonc' && !pkg.scripts['docs:deploy']) {
258
+ pkg.scripts['docs:deploy'] = `wrangler deploy -c ${wranglerFile}`
259
+ }
260
+ pkg.devDependencies = { ...(pkg.devDependencies || {}) }
151
261
  pkg.devDependencies[enginePkg.name] = engineVersion
262
+ if (!pkg.devDependencies.wrangler) {
263
+ pkg.devDependencies.wrangler = '^4'
264
+ }
152
265
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
153
266
  created.push(existingPkg ? 'package.json (updated)' : 'package.json')
154
267
 
155
- // .node-version for Cloudflare's build image (Wrangler needs Node 22)
268
+ // Update the lockfile so Cloudflare --frozen-lockfile can install the engine.
269
+ try {
270
+ console.log(`Updating lockfile (${localInstall})…`)
271
+ execSync(localInstall, { cwd: root, stdio: 'inherit', shell: true })
272
+ created.push('lockfile (updated)')
273
+ } catch {
274
+ notes.push(
275
+ `Could not run "${localInstall}" automatically. Run it yourself and commit the lockfile before deploying.`
276
+ )
277
+ }
278
+
279
+ // .node-version — only for greenfield docs repos (apps often pin their own Node).
156
280
  const nodeVerPath = path.join(root, '.node-version')
157
- if (!(await fileExists(nodeVerPath))) {
281
+ if (!coexisting && !(await fileExists(nodeVerPath))) {
158
282
  await fs.writeFile(nodeVerPath, '22\n')
159
283
  created.push('.node-version')
284
+ } else if (coexisting && !(await fileExists(nodeVerPath))) {
285
+ notes.push('Set Node 22 in the Cloudflare Worker build settings (Wrangler requires it).')
160
286
  }
161
287
 
162
- await ensureGitignore(root, created)
288
+ const ignore = ['node_modules/', `${outDir}/`]
289
+ await ensureGitignore(root, ignore, created)
163
290
 
164
- // Guarantee a landing page: if the repo has no README/index anywhere, drop a starter.
165
- let home = await hasHomePage(root)
291
+ // Home page inside the content root (never invent a root README over an app's README).
292
+ const contentRel = content || '.'
293
+ const contentRoot = path.join(root, contentRel)
294
+ let home = await hasHomePageIn(contentRoot)
166
295
  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)
296
+ await fs.mkdir(contentRoot, { recursive: true })
297
+ const rel = path.join(contentRel === '.' ? '' : contentRel, 'README.md').replace(/^\//, '')
298
+ const readmePath = path.join(root, rel || 'README.md')
170
299
  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
300
+ if (!(await fileExists(readmePath))) {
301
+ await fs.writeFile(readmePath, starterReadme(title))
302
+ created.push((rel || 'README.md').split(path.sep).join('/'))
303
+ home = true
304
+ } else {
305
+ home = true
306
+ }
174
307
  }
175
308
 
176
- return { created, skipped, name, title, visibility, repoUrl, home }
309
+ const deployCommand =
310
+ wranglerFile === 'wrangler.jsonc'
311
+ ? 'npx wrangler deploy'
312
+ : `npx wrangler deploy -c ${wranglerFile}`
313
+
314
+ return {
315
+ created,
316
+ skipped,
317
+ notes,
318
+ name,
319
+ title,
320
+ visibility,
321
+ repoUrl,
322
+ home,
323
+ content: contentRel,
324
+ outDir,
325
+ wranglerFile,
326
+ buildCommand,
327
+ deployCommand,
328
+ localInstall,
329
+ localDev,
330
+ packageManager: pm,
331
+ coexisting
332
+ }
177
333
  }
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 || ''
package/src/watch.js CHANGED
@@ -2,7 +2,7 @@ import { watch } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { build } from './build.js'
4
4
 
5
- const IGNORED = /(^|[\\/])(node_modules|\.git|dist)([\\/]|$)/
5
+ const IGNORED = /(^|[\\/])(node_modules|\.git|dist-docs|dist)([\\/]|$)/
6
6
 
7
7
  /**
8
8
  * Rebuild on any change under the content directory. Debounced so a burst of
@@ -12,7 +12,7 @@ const IGNORED = /(^|[\\/])(node_modules|\.git|dist)([\\/]|$)/
12
12
  */
13
13
  export function watchAndRebuild({ root, inDir, outDir, theme }) {
14
14
  const watchPath = inDir === '.' ? root : path.join(root, inDir)
15
- const outBase = path.basename(outDir)
15
+ const outBase = path.basename(outDir || 'dist')
16
16
  let timer = null
17
17
  let building = false
18
18
 
@@ -23,7 +23,12 @@ export function watchAndRebuild({ root, inDir, outDir, theme }) {
23
23
  if (building) return
24
24
  building = true
25
25
  try {
26
- const { pageCount } = await build({ root, inDir: inDir === '.' ? '.' : inDir, outDir, theme })
26
+ const { pageCount } = await build({
27
+ root,
28
+ inDir: inDir === '.' ? '.' : inDir,
29
+ outDir,
30
+ theme
31
+ })
27
32
  console.log(`↻ Rebuilt ${pageCount} page(s)`)
28
33
  } catch (err) {
29
34
  console.error('Build failed:', err.message)