@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 +6 -4
- package/bin/cli.js +26 -5
- package/package.json +1 -1
- package/src/build.js +6 -6
- package/src/config.js +2 -2
- package/src/init.js +139 -46
- package/src/layout.js +1 -1
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 #
|
|
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
|
|
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` > `
|
|
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
|
|
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
|
|
39
|
-
|
|
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(
|
|
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
|
|
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(
|
|
70
|
+
console.log(` Cloudflare: build \`${buildCommand}\`, deploy \`${deployCommand}\``)
|
|
50
71
|
return
|
|
51
72
|
}
|
|
52
73
|
|
package/package.json
CHANGED
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 > "
|
|
35
|
-
* Unknown names warn and fall back to
|
|
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 ?? '
|
|
39
|
-
const name = String(raw).trim().toLowerCase() || '
|
|
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
|
|
43
|
+
`⚠ Unknown theme "${name}" — falling back to totum. Available: ${[...available].sort().join(', ')}`
|
|
44
44
|
)
|
|
45
|
-
return '
|
|
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
|
|
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: '
|
|
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 {
|
|
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 {
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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)
|
|
48
|
-
|
|
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)
|
|
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": "
|
|
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 {
|
|
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
|
|
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
|
|
88
|
-
const
|
|
89
|
-
|
|
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
|
|
120
|
+
more \`.md\` files alongside it — they show up in the sidebar automatically.
|
|
100
121
|
`
|
|
101
122
|
}
|
|
102
123
|
|
|
103
|
-
/**
|
|
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
|
|
111
|
-
const
|
|
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,
|
|
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(
|
|
178
|
+
skipped.push(wranglerFile)
|
|
138
179
|
} else {
|
|
139
|
-
await fs.writeFile(wranglerPath, WRANGLER(name))
|
|
140
|
-
created.push(
|
|
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
|
|
189
|
+
// package.json — merge only; never replace existing build/dev
|
|
144
190
|
const pkgPath = path.join(root, 'package.json')
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
pkg.scripts =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
-
|
|
226
|
+
const ignore = ['node_modules/', `${outDir}/`]
|
|
227
|
+
await ensureGitignore(root, ignore, created)
|
|
163
228
|
|
|
164
|
-
//
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
const rel =
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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 =
|
|
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 || ''
|