@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 +6 -4
- package/bin/cli.js +53 -10
- package/package.json +1 -1
- package/src/build.js +10 -9
- package/src/config.js +4 -2
- package/src/init.js +204 -48
- package/src/layout.js +1 -1
- package/src/watch.js +8 -3
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
|
@@ -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
|
|
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
|
|
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
|
+
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(
|
|
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(
|
|
47
|
-
console.log(
|
|
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('
|
|
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({
|
|
55
|
-
|
|
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({
|
|
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
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
+
// 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 {
|
|
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,72 @@ 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())
|
|
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": "
|
|
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 {
|
|
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
|
|
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
|
|
88
|
-
const
|
|
89
|
-
|
|
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
|
|
148
|
+
more \`.md\` files alongside it — they show up in the sidebar automatically.
|
|
100
149
|
`
|
|
101
150
|
}
|
|
102
151
|
|
|
103
|
-
/**
|
|
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
|
|
111
|
-
const
|
|
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 (
|
|
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
|
-
|
|
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
|
|
135
|
-
const wranglerPath = path.join(root,
|
|
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(
|
|
229
|
+
skipped.push(wranglerFile)
|
|
138
230
|
} else {
|
|
139
|
-
await fs.writeFile(wranglerPath, WRANGLER(name))
|
|
140
|
-
created.push(
|
|
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
|
|
240
|
+
// package.json — merge only; never replace existing build/dev
|
|
144
241
|
const pkgPath = path.join(root, 'package.json')
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
288
|
+
const ignore = ['node_modules/', `${outDir}/`]
|
|
289
|
+
await ensureGitignore(root, ignore, created)
|
|
163
290
|
|
|
164
|
-
//
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
const rel =
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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 =
|
|
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({
|
|
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)
|