@inovus-medical/docs 0.1.0 → 0.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/bin/cli.js +25 -1
- package/package.json +1 -1
- package/src/build.js +10 -1
- package/src/init.js +173 -0
- package/src/markdown.js +13 -1
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { build } from '../src/build.js'
|
|
4
4
|
import { serve } from '../src/serve.js'
|
|
5
5
|
import { watchAndRebuild } from '../src/watch.js'
|
|
6
|
+
import { init } from '../src/init.js'
|
|
6
7
|
|
|
7
8
|
function parseFlags(args) {
|
|
8
9
|
const opts = {}
|
|
@@ -32,6 +33,22 @@ const outDir = flags.out || 'dist'
|
|
|
32
33
|
const port = Number(flags.port || 8788)
|
|
33
34
|
|
|
34
35
|
async function run() {
|
|
36
|
+
if (cmd === 'init') {
|
|
37
|
+
const { created, skipped, name, title, visibility, repoUrl, home } = await init({ root, flags })
|
|
38
|
+
console.log(`Scaffolded "${title}" (worker: ${name}, ${visibility})${repoUrl ? ` — ${repoUrl}` : ''}`)
|
|
39
|
+
if (created.length) console.log(' created: ' + created.join(', '))
|
|
40
|
+
if (skipped.length) console.log(' kept existing (use --force to overwrite): ' + skipped.join(', '))
|
|
41
|
+
if (!home) {
|
|
42
|
+
console.log(' ⚠ No home page (README.md / index.md) and --no-readme set — the site will have no page at /.')
|
|
43
|
+
}
|
|
44
|
+
console.log('\nNext:')
|
|
45
|
+
console.log(' npm install')
|
|
46
|
+
console.log(' npm run dev # preview at http://localhost:8788')
|
|
47
|
+
console.log(' git add -A && git commit -m "Add docs site config" && git push')
|
|
48
|
+
console.log(' then connect the repo in Cloudflare (build: npm install && npm run build)')
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
if (cmd === 'build') {
|
|
36
53
|
const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir })
|
|
37
54
|
console.log(`✓ Built ${pageCount} page(s) from ${contentDir === '.' ? 'repo root' : contentDir + '/'} -> ${path.relative(process.cwd(), outPath) || outPath}`)
|
|
@@ -54,7 +71,14 @@ async function run() {
|
|
|
54
71
|
return
|
|
55
72
|
}
|
|
56
73
|
|
|
57
|
-
console.error(
|
|
74
|
+
console.error(
|
|
75
|
+
`Unknown command: ${cmd}\n` +
|
|
76
|
+
`Usage:\n` +
|
|
77
|
+
` inovus-docs init [--name <n>] [--title <t>] [--public] [--repo <url>] [--no-readme] [--force]\n` +
|
|
78
|
+
` inovus-docs build [--root .] [--in <dir>] [--out dist]\n` +
|
|
79
|
+
` inovus-docs dev [--port 8788]\n` +
|
|
80
|
+
` inovus-docs serve [--port 8788]`
|
|
81
|
+
)
|
|
58
82
|
process.exit(1)
|
|
59
83
|
}
|
|
60
84
|
|
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -117,7 +117,16 @@ export async function build({ root = process.cwd(), inDir, outDir = 'dist' } = {
|
|
|
117
117
|
const routeSegments = isIndex ? dirSegments : rawSegments
|
|
118
118
|
const route = toRoute(routeSegments)
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
let rendered
|
|
121
|
+
try {
|
|
122
|
+
rendered = renderer.render(content)
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const rel = path.relative(contentRoot, file)
|
|
125
|
+
console.warn(`⚠ Could not fully render ${rel}: ${err.message} — falling back to plain text.`)
|
|
126
|
+
const escaped = content.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]))
|
|
127
|
+
rendered = { html: `<pre>${escaped}</pre>`, headings: [], text: content.slice(0, 2000), title: null }
|
|
128
|
+
}
|
|
129
|
+
const { html, headings, text, title } = rendered
|
|
121
130
|
const pageTitle = data.title || title || (isIndex ? titleCase(dirSegments[dirSegments.length - 1] || config.title) : titleCase(leaf))
|
|
122
131
|
|
|
123
132
|
const page = {
|
package/src/init.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { execSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
async function fileExists(p) {
|
|
9
|
+
try { await fs.access(p); return true } catch { return false }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function readJsonIfExists(file) {
|
|
13
|
+
try { return JSON.parse(await fs.readFile(file, 'utf8')) } catch (e) {
|
|
14
|
+
if (e.code === 'ENOENT') return null
|
|
15
|
+
throw e
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function gitRemote(root) {
|
|
20
|
+
try {
|
|
21
|
+
return execSync('git config --get remote.origin.url', {
|
|
22
|
+
cwd: root,
|
|
23
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
24
|
+
}).toString().trim() || null
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseRemote(url) {
|
|
31
|
+
if (!url) return {}
|
|
32
|
+
const m = url.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/)
|
|
33
|
+
if (!m) return {}
|
|
34
|
+
const [, org, repo] = m
|
|
35
|
+
let httpsUrl
|
|
36
|
+
if (url.startsWith('git@')) {
|
|
37
|
+
const hm = url.match(/git@([^:]+):(.+?)(?:\.git)?$/)
|
|
38
|
+
httpsUrl = hm ? `https://${hm[1]}/${hm[2]}` : null
|
|
39
|
+
} else {
|
|
40
|
+
httpsUrl = url.replace(/\.git$/, '')
|
|
41
|
+
}
|
|
42
|
+
return { org, repo, httpsUrl }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sanitizeName(s) {
|
|
46
|
+
return (
|
|
47
|
+
String(s).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 54) ||
|
|
48
|
+
'docs-site'
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function titleCase(s) {
|
|
53
|
+
return String(s).replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim().replace(/\b\w/g, (c) => c.toUpperCase())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const WRANGLER = (name) => `{
|
|
57
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
58
|
+
"name": "${name}",
|
|
59
|
+
"compatibility_date": "2025-06-01",
|
|
60
|
+
"assets": {
|
|
61
|
+
"directory": "./dist",
|
|
62
|
+
"html_handling": "auto-trailing-slash",
|
|
63
|
+
"not_found_handling": "404-page"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
async function ensureGitignore(root, created) {
|
|
69
|
+
const file = path.join(root, '.gitignore')
|
|
70
|
+
let body = ''
|
|
71
|
+
try { body = await fs.readFile(file, 'utf8') } catch {}
|
|
72
|
+
const present = new Set(body.split(/\r?\n/).map((l) => l.trim()))
|
|
73
|
+
let next = body
|
|
74
|
+
let changed = false
|
|
75
|
+
for (const entry of ['node_modules/', 'dist/']) {
|
|
76
|
+
if (!present.has(entry)) {
|
|
77
|
+
next += (next && !next.endsWith('\n') ? '\n' : '') + entry + '\n'
|
|
78
|
+
changed = true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (changed) {
|
|
82
|
+
await fs.writeFile(file, next)
|
|
83
|
+
created.push(body ? '.gitignore (updated)' : '.gitignore')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
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
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function starterReadme(title) {
|
|
94
|
+
return `# ${title}
|
|
95
|
+
|
|
96
|
+
Welcome to the ${title} documentation.
|
|
97
|
+
|
|
98
|
+
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.
|
|
100
|
+
`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Scaffold the per-repo files needed to publish a markdown repo as a docs site. */
|
|
104
|
+
export async function init({ root = process.cwd(), flags = {} } = {}) {
|
|
105
|
+
const enginePkg = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf8'))
|
|
106
|
+
const engineVersion = `^${enginePkg.version}`
|
|
107
|
+
|
|
108
|
+
const remote = parseRemote(gitRemote(root))
|
|
109
|
+
const folder = path.basename(root)
|
|
110
|
+
const name = sanitizeName(flags.name || remote.repo || folder)
|
|
111
|
+
const title = flags.title || titleCase(remote.repo || folder)
|
|
112
|
+
const visibility = flags.public ? 'public' : 'private'
|
|
113
|
+
const repoUrl = flags.repo || remote.httpsUrl || null
|
|
114
|
+
const force = flags.force === true
|
|
115
|
+
|
|
116
|
+
const created = []
|
|
117
|
+
const skipped = []
|
|
118
|
+
|
|
119
|
+
// docs.config.json (don't clobber existing unless --force)
|
|
120
|
+
const cfgPath = path.join(root, 'docs.config.json')
|
|
121
|
+
if ((await fileExists(cfgPath)) && !force) {
|
|
122
|
+
skipped.push('docs.config.json')
|
|
123
|
+
} else {
|
|
124
|
+
const cfg = { title, description: '', visibility }
|
|
125
|
+
if (repoUrl) cfg.repo = repoUrl
|
|
126
|
+
await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
|
|
127
|
+
created.push('docs.config.json')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// wrangler.jsonc
|
|
131
|
+
const wranglerPath = path.join(root, 'wrangler.jsonc')
|
|
132
|
+
if ((await fileExists(wranglerPath)) && !force) {
|
|
133
|
+
skipped.push('wrangler.jsonc')
|
|
134
|
+
} else {
|
|
135
|
+
await fs.writeFile(wranglerPath, WRANGLER(name))
|
|
136
|
+
created.push('wrangler.jsonc')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// package.json (merge if present — never remove existing fields)
|
|
140
|
+
const pkgPath = path.join(root, 'package.json')
|
|
141
|
+
const existingPkg = await readJsonIfExists(pkgPath)
|
|
142
|
+
const pkg = existingPkg || { name, private: true }
|
|
143
|
+
pkg.scripts = pkg.scripts || {}
|
|
144
|
+
if (!pkg.scripts.build) pkg.scripts.build = 'inovus-docs build'
|
|
145
|
+
if (!pkg.scripts.dev) pkg.scripts.dev = 'inovus-docs dev'
|
|
146
|
+
pkg.devDependencies = pkg.devDependencies || {}
|
|
147
|
+
pkg.devDependencies[enginePkg.name] = engineVersion
|
|
148
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
149
|
+
created.push(existingPkg ? 'package.json (updated)' : 'package.json')
|
|
150
|
+
|
|
151
|
+
// .node-version for Cloudflare's build image (Wrangler needs Node 22)
|
|
152
|
+
const nodeVerPath = path.join(root, '.node-version')
|
|
153
|
+
if (!(await fileExists(nodeVerPath))) {
|
|
154
|
+
await fs.writeFile(nodeVerPath, '22\n')
|
|
155
|
+
created.push('.node-version')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await ensureGitignore(root, created)
|
|
159
|
+
|
|
160
|
+
// Guarantee a landing page: if the repo has no README/index anywhere, drop a starter.
|
|
161
|
+
let home = await hasHomePage(root)
|
|
162
|
+
if (!home && !flags['no-readme']) {
|
|
163
|
+
const inDocsMode = await fileExists(path.join(root, 'docs'))
|
|
164
|
+
const rel = inDocsMode ? path.join('docs', 'README.md') : 'README.md'
|
|
165
|
+
const readmePath = path.join(root, rel)
|
|
166
|
+
await fs.mkdir(path.dirname(readmePath), { recursive: true })
|
|
167
|
+
await fs.writeFile(readmePath, starterReadme(title))
|
|
168
|
+
created.push(rel.split(path.sep).join('/'))
|
|
169
|
+
home = true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { created, skipped, name, title, visibility, repoUrl, home }
|
|
173
|
+
}
|
package/src/markdown.js
CHANGED
|
@@ -42,7 +42,19 @@ export async function createRenderer() {
|
|
|
42
42
|
md.use(
|
|
43
43
|
await Shiki({
|
|
44
44
|
themes: { light: 'github-light', dark: 'github-dark' },
|
|
45
|
-
defaultColor: false
|
|
45
|
+
defaultColor: false,
|
|
46
|
+
// Real repos use fence languages Shiki doesn't ship (env, console, ...).
|
|
47
|
+
// Map the common ones, and fall back to plain text instead of crashing the build.
|
|
48
|
+
fallbackLanguage: 'text',
|
|
49
|
+
langAlias: {
|
|
50
|
+
env: 'ini',
|
|
51
|
+
dotenv: 'ini',
|
|
52
|
+
conf: 'ini',
|
|
53
|
+
console: 'bash',
|
|
54
|
+
shell: 'bash',
|
|
55
|
+
cmd: 'bat',
|
|
56
|
+
vue: 'html'
|
|
57
|
+
}
|
|
46
58
|
})
|
|
47
59
|
)
|
|
48
60
|
|