@inditextech/docouture-cli 0.1.0-SNAPSHOT.40.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/build/bin.js +321 -0
- package/build/commands/build.js +22 -0
- package/build/commands/completion.js +69 -0
- package/build/commands/dev.js +67 -0
- package/build/commands/doctor.js +156 -0
- package/build/commands/eject.js +71 -0
- package/build/commands/new.js +512 -0
- package/build/commands/publish.js +111 -0
- package/build/commands/teardown.js +66 -0
- package/build/commands/upgrade.js +156 -0
- package/build/commands/version.js +60 -0
- package/build/lib/agents-md.js +67 -0
- package/build/lib/antora-log.js +36 -0
- package/build/lib/antora-yml.js +47 -0
- package/build/lib/args.js +26 -0
- package/build/lib/cli-context.js +17 -0
- package/build/lib/cli-info.js +20 -0
- package/build/lib/config-resolver.js +23 -0
- package/build/lib/copy-template.js +162 -0
- package/build/lib/debug-log.js +21 -0
- package/build/lib/detect-package-manager.js +79 -0
- package/build/lib/dev-server.js +337 -0
- package/build/lib/doctor-checks.js +240 -0
- package/build/lib/global-flags.js +31 -0
- package/build/lib/kroki-compose.js +54 -0
- package/build/lib/playbook-yml.js +90 -0
- package/build/lib/repo-root.js +21 -0
- package/build/lib/run-script.js +50 -0
- package/build/lib/theme.js +51 -0
- package/build/templates/agent-support/.claude/skills/docs-internals/SKILL.md +53 -0
- package/build/templates/agent-support/.claude/skills/docs-internals/reference/antora-extensions.md +54 -0
- package/build/templates/agent-support/.claude/skills/docs-internals/reference/naming.md +41 -0
- package/build/templates/agent-support/.claude/skills/docs-internals/reference/page-patterns.md +238 -0
- package/build/templates/agent-support/.claude/skills/docs-versioning.versioned/SKILL.md +37 -0
- package/build/templates/agent-support/.claude/skills/docs-versioning.versioned/reference/releasing.md +106 -0
- package/build/templates/agent-support/.claude/skills/documenting-your-repo/SKILL.md +55 -0
- package/build/templates/agent-support/.claude/skills/documenting-your-repo/reference/content-sourcing.md +55 -0
- package/build/templates/agent-support/.claude/skills/documenting-your-repo/reference/maintenance-loop.md +68 -0
- package/build/templates/agent-support/.claude/skills/documenting-your-repo/reference/structure-planning.md +102 -0
- package/build/templates/agent-support/.claude/skills/writing-docs-pages/SKILL.md +112 -0
- package/build/templates/agent-support/.claude/skills/writing-docs-pages/reference/docouture-blocks.md +170 -0
- package/build/templates/agent-support/.claude/skills/writing-docs-pages/reference/language-basics.md +402 -0
- package/build/templates/agent-support/.opencode/skills/docs-internals/SKILL.md +53 -0
- package/build/templates/agent-support/.opencode/skills/docs-internals/reference/antora-extensions.md +54 -0
- package/build/templates/agent-support/.opencode/skills/docs-internals/reference/naming.md +41 -0
- package/build/templates/agent-support/.opencode/skills/docs-internals/reference/page-patterns.md +238 -0
- package/build/templates/agent-support/.opencode/skills/docs-versioning.versioned/SKILL.md +37 -0
- package/build/templates/agent-support/.opencode/skills/docs-versioning.versioned/reference/releasing.md +106 -0
- package/build/templates/agent-support/.opencode/skills/documenting-your-repo/SKILL.md +55 -0
- package/build/templates/agent-support/.opencode/skills/documenting-your-repo/reference/content-sourcing.md +55 -0
- package/build/templates/agent-support/.opencode/skills/documenting-your-repo/reference/maintenance-loop.md +68 -0
- package/build/templates/agent-support/.opencode/skills/documenting-your-repo/reference/structure-planning.md +102 -0
- package/build/templates/agent-support/.opencode/skills/writing-docs-pages/SKILL.md +112 -0
- package/build/templates/agent-support/.opencode/skills/writing-docs-pages/reference/docouture-blocks.md +170 -0
- package/build/templates/agent-support/.opencode/skills/writing-docs-pages/reference/language-basics.md +402 -0
- package/build/templates/agent-support/AGENTS.md +72 -0
- package/build/templates/starter/antora-playbook.local.yml +99 -0
- package/build/templates/starter/antora-playbook.versioned.yml +167 -0
- package/build/templates/starter/antora-playbook.yml +189 -0
- package/build/templates/starter/gitignore +3 -0
- package/build/templates/starter/package.json +37 -0
- package/build/templates/starter/scripts/check-links.mjs +178 -0
- package/build/templates/starter/src/antora.yml +85 -0
- package/build/templates/starter/src/modules/ROOT/images/card-placeholder.png +0 -0
- package/build/templates/starter/src/modules/ROOT/images/feature-placeholder-dark.png +0 -0
- package/build/templates/starter/src/modules/ROOT/images/feature-placeholder.png +0 -0
- package/build/templates/starter/src/modules/ROOT/images/hero-placeholder.png +0 -0
- package/build/templates/starter/src/modules/ROOT/pages/index.adoc +124 -0
- package/build/templates/starter/src/modules/main/nav.adoc +19 -0
- package/build/templates/starter/src/modules/main/pages/app.adoc +5 -0
- package/build/templates/starter/src/modules/main/pages/architecture.adoc +6 -0
- package/build/templates/starter/src/modules/main/pages/changelog/index.adoc +9 -0
- package/build/templates/starter/src/modules/main/pages/components.adoc +701 -0
- package/build/templates/starter/src/modules/main/pages/contributing.adoc +6 -0
- package/build/templates/starter/src/modules/main/pages/getting-started.adoc +169 -0
- package/build/templates/starter/src/modules/main/pages/glossary.adoc +8 -0
- package/build/templates/starter/src/modules/main/pages/index.adoc +9 -0
- package/build/templates/starter/src/modules/main/pages/prerequisites.adoc +55 -0
- package/build/templates/starter/src/modules/main/pages/quickstart.adoc +21 -0
- package/build/templates/starter/src/modules/main/pages/release-notes/index.adoc +9 -0
- package/build/templates/starter/src/modules/main/pages/requirements.adoc +8 -0
- package/build/templates/starter/src/modules/main/pages/roadmap.adoc +4 -0
- package/build/templates/starter/src/modules/main/pages/structure.adoc +5 -0
- package/build/templates/starter/src/release-version.versioned +1 -0
- package/build/templates/starter/supplemental-ui/favicon.ico +0 -0
- package/build/templates/starter/supplemental-ui/product-logo-dark.png +0 -0
- package/build/templates/starter/supplemental-ui/product-logo.png +0 -0
- package/build/templates/workflows/docouture-kroki-cache-warm.yml +101 -0
- package/build/templates/workflows/docouture-pr-verify.yml +120 -0
- package/build/templates/workflows/docouture-publish-prerelease.yml +122 -0
- package/build/templates/workflows/docouture-publish.yml +138 -0
- package/build/templates/workflows/docouture-release-preview.yml +198 -0
- package/build/templates/workflows/docouture-release.yml +414 -0
- package/package.json +38 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const PLACEHOLDERS = {
|
|
5
|
+
__DOCOUTURE_NAME__: 'name',
|
|
6
|
+
__DOCOUTURE_TITLE__: 'title',
|
|
7
|
+
__DOCOUTURE_COMPONENT_NAME__: 'componentName',
|
|
8
|
+
__DOCOUTURE_CLI_VERSION__: 'cliVersion',
|
|
9
|
+
__DOCOUTURE_PM__: 'pmName',
|
|
10
|
+
__DOCOUTURE_PM_CACHE__: 'pmCacheName',
|
|
11
|
+
__DOCOUTURE_LOCKFILE__: 'pmLockfile',
|
|
12
|
+
__DOCOUTURE_INSTALL_CI__: 'pmCiCmd',
|
|
13
|
+
// The whole comment line (leading spaces, `#`, trailing newline) is the
|
|
14
|
+
// token here, not just the bare placeholder name — a bare
|
|
15
|
+
// `__DOCOUTURE_PM_SETUP_STEP__` sitting at the start of a YAML line with a
|
|
16
|
+
// step's own `- name: …` right after it on the same line is not valid YAML
|
|
17
|
+
// on its own (before substitution), which is exactly the form these
|
|
18
|
+
// template files are in until `docouture new` runs — and `just fmt`/prettier
|
|
19
|
+
// parses them as real YAML. A `#`-prefixed placeholder is a comment,
|
|
20
|
+
// valid on any line, so the *template* stays parseable; substituting the
|
|
21
|
+
// whole line (not just the token inside it) is what lets the pnpm-only
|
|
22
|
+
// value (which ends in its own `\n` — see packageManagerPlan) or the
|
|
23
|
+
// empty npm value drop cleanly in its place.
|
|
24
|
+
' # __DOCOUTURE_PM_SETUP_STEP__\n': 'pmSetupStepYaml',
|
|
25
|
+
// A bare token this time, unlike pmSetupStepYaml above — see
|
|
26
|
+
// TemplateValues.repoIgnoreGlob's own comment for why the whole-segment
|
|
27
|
+
// trick doesn't survive here.
|
|
28
|
+
__DOCOUTURE_REPO_IGNORE_GLOB__: 'repoIgnoreGlob',
|
|
29
|
+
};
|
|
30
|
+
// `<!-- prettier-ignore -->` directives exist only to stop prettier mangling a
|
|
31
|
+
// placeholder token in the template source (e.g. `__DOCOUTURE_TITLE__` inside a
|
|
32
|
+
// markdown heading, which double-underscore emphasis would otherwise rewrite
|
|
33
|
+
// to `**DOCOUTURE_TITLE**`) — they are not meant to survive into scaffolded
|
|
34
|
+
// output, where the placeholder has already been substituted away.
|
|
35
|
+
const PRETTIER_IGNORE_LINE = /^<!-- prettier-ignore -->\n/m;
|
|
36
|
+
function substitute(text, values) {
|
|
37
|
+
let out = text.replace(PRETTIER_IGNORE_LINE, '');
|
|
38
|
+
for (const [token, key] of Object.entries(PLACEHOLDERS)) {
|
|
39
|
+
out = out.split(token).join(values[key]);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
// A `.versioned` marker in a template filename (e.g.
|
|
44
|
+
// `antora-playbook.versioned.yml` beside `antora-playbook.yml`, or
|
|
45
|
+
// `release-version.versioned` for a file with no natural extension to hang a
|
|
46
|
+
// mid-name marker off) is a versioning-mode override — see
|
|
47
|
+
// `writeTemplateFile`, which reads one explicitly to lay a versioned-mode
|
|
48
|
+
// file down under its real name when `docouture new --mode versioned` is used.
|
|
49
|
+
// It must never be copied under its own literal name by the generic walk
|
|
50
|
+
// below, standalone or not.
|
|
51
|
+
const VERSIONED_MARKER = '.versioned';
|
|
52
|
+
// AGENTS.md needs its own merge logic (see lib/agents-md.ts) rather than a
|
|
53
|
+
// blind overwrite — a repository may already have its own AGENTS.md, or a
|
|
54
|
+
// human may have added notes around docouture' own section since the last
|
|
55
|
+
// `docouture new`/`docouture upgrade`. `new.ts`/`upgrade.ts` read, merge and write
|
|
56
|
+
// it themselves; the generic walk below must skip over it entirely rather
|
|
57
|
+
// than clobbering it the same way it does every other template file.
|
|
58
|
+
const SKIP_FILENAMES = new Set(['AGENTS.md']);
|
|
59
|
+
// npm's publish step unconditionally strips any file matching `.git*`
|
|
60
|
+
// (.gitignore, .gitattributes, .gitmodules, ...) from a package's tarball,
|
|
61
|
+
// regardless of the `files` field or an .npmignore — confirmed here with
|
|
62
|
+
// `npm pack --dry-run` on this very package, which silently drops
|
|
63
|
+
// templates/starter/.gitignore while every sibling file in that directory
|
|
64
|
+
// survives. That means a `.gitignore` template only ever worked when
|
|
65
|
+
// `docouture new` ran from a repo checkout, never from an npm-installed copy —
|
|
66
|
+
// the real-world case. The fix (the same one create-react-app's own
|
|
67
|
+
// template uses): the template source file has no leading dot at all
|
|
68
|
+
// (`gitignore`), and this map renames it back to its real name only at
|
|
69
|
+
// write time, after npm has already packed it safely.
|
|
70
|
+
const DOTFILE_RENAMES = {
|
|
71
|
+
gitignore: '.gitignore',
|
|
72
|
+
};
|
|
73
|
+
// Extensions copied as raw bytes, no placeholder substitution — logo and
|
|
74
|
+
// favicon assets under `supplemental-ui/` (GH-114). Substituting text tokens
|
|
75
|
+
// into a binary file via `readFile(from, 'utf8')` corrupts it (the bytes get
|
|
76
|
+
// decoded as UTF-8 and re-encoded on write), so these must take a separate,
|
|
77
|
+
// non-text path instead of the generic one below.
|
|
78
|
+
const BINARY_EXTENSIONS = new Set(['.png', '.ico', '.jpg', '.jpeg', '.gif', '.webp']);
|
|
79
|
+
function isBinaryFile(name) {
|
|
80
|
+
const dot = name.lastIndexOf('.');
|
|
81
|
+
if (dot === -1)
|
|
82
|
+
return false;
|
|
83
|
+
return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
|
|
84
|
+
}
|
|
85
|
+
// Copies every file under `srcDir` into `destDir`, substituting placeholder
|
|
86
|
+
// tokens in each text file. Most template files are plain text (YAML,
|
|
87
|
+
// AsciiDoc, JSON) and go through that substitution; image assets
|
|
88
|
+
// (BINARY_EXTENSIONS) are copied as raw bytes instead — see its own comment.
|
|
89
|
+
// Returns every destination file path written (or, under `dryRun`, that
|
|
90
|
+
// would have been written) — `new.ts` ignores this, `upgrade.ts` uses it
|
|
91
|
+
// both to report progress and to preview a dry run.
|
|
92
|
+
export async function copyTemplate(srcDir, destDir, values, opts = {}) {
|
|
93
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
94
|
+
const written = [];
|
|
95
|
+
if (!opts.dryRun) {
|
|
96
|
+
await mkdir(destDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.name.includes(VERSIONED_MARKER))
|
|
100
|
+
continue;
|
|
101
|
+
if (SKIP_FILENAMES.has(entry.name))
|
|
102
|
+
continue;
|
|
103
|
+
const from = join(srcDir, entry.name);
|
|
104
|
+
const to = join(destDir, DOTFILE_RENAMES[entry.name] ?? entry.name);
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
written.push(...(await copyTemplate(from, to, values, opts)));
|
|
107
|
+
}
|
|
108
|
+
else if (opts.dryRun) {
|
|
109
|
+
written.push(to);
|
|
110
|
+
}
|
|
111
|
+
else if (isBinaryFile(entry.name)) {
|
|
112
|
+
const content = await readFile(from);
|
|
113
|
+
await writeFile(to, content);
|
|
114
|
+
written.push(to);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const content = await readFile(from, 'utf8');
|
|
118
|
+
await writeFile(to, substitute(content, values), 'utf8');
|
|
119
|
+
written.push(to);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return written;
|
|
123
|
+
}
|
|
124
|
+
// Overwrites a single already-copied file with a versioning-mode override —
|
|
125
|
+
// used for `antora-playbook.yml` and `docs/.release-version`, whose
|
|
126
|
+
// versioned-mode shape (see reference/versioning-modes.md) differs from the
|
|
127
|
+
// standalone default copyTemplate above lays down (or, for
|
|
128
|
+
// `.release-version`, does not lay down at all). `docs/antora.yml` is NOT
|
|
129
|
+
// overridden this way — it is identical for both modes (see
|
|
130
|
+
// templates/starter/docs/antora.yml's own comment). Same placeholder
|
|
131
|
+
// substitution, just for one file instead of a whole tree.
|
|
132
|
+
export async function writeTemplateFile(srcFile, destFile, values) {
|
|
133
|
+
const content = await readFile(srcFile, 'utf8');
|
|
134
|
+
await writeFile(destFile, substitute(content, values), 'utf8');
|
|
135
|
+
}
|
|
136
|
+
// Renders a single template file's placeholder-substituted content without
|
|
137
|
+
// writing it anywhere — used for AGENTS.md (see lib/agents-md.ts), which
|
|
138
|
+
// `new.ts`/`upgrade.ts` need as a string to merge against whatever's already
|
|
139
|
+
// on disk, rather than a file copyTemplate can just write directly.
|
|
140
|
+
export async function renderTemplateFile(srcFile, values) {
|
|
141
|
+
const content = await readFile(srcFile, 'utf8');
|
|
142
|
+
return substitute(content, values);
|
|
143
|
+
}
|
|
144
|
+
export async function isEmptyOrMissing(dir) {
|
|
145
|
+
try {
|
|
146
|
+
const entries = await readdir(dir);
|
|
147
|
+
return entries.length === 0;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function exists(path) {
|
|
154
|
+
try {
|
|
155
|
+
await stat(path);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=copy-template.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { getContext } from './cli-context.js';
|
|
3
|
+
// Node.js CLI convention (see the debug package, and most CLIs that follow
|
|
4
|
+
// it): DEBUG=docouture or DEBUG=* turns this on from the environment, the same
|
|
5
|
+
// as --verbose does from a flag — either is enough. Always written to
|
|
6
|
+
// stderr, never stdout, so it never contaminates a command's actual output
|
|
7
|
+
// (in particular `docouture doctor --json`'s piped-and-parsed JSON).
|
|
8
|
+
function enabled() {
|
|
9
|
+
if (getContext().verbose)
|
|
10
|
+
return true;
|
|
11
|
+
const debug = process.env.DEBUG;
|
|
12
|
+
if (!debug)
|
|
13
|
+
return false;
|
|
14
|
+
return debug.split(',').some((token) => token.trim() === 'docouture' || token.trim() === '*');
|
|
15
|
+
}
|
|
16
|
+
export function debugLog(message) {
|
|
17
|
+
if (!enabled())
|
|
18
|
+
return;
|
|
19
|
+
console.error(`[docouture debug] ${message}`);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=debug-log.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const PNPM_ACTION_SETUP_STEP = ' - name: Setup pnpm\n' +
|
|
5
|
+
' uses: pnpm/action-setup@a3252b78c470c02f7abfb6128aa3c1852013d47c # v4\n' +
|
|
6
|
+
'\n';
|
|
7
|
+
// Reads the invoking package manager off npm's own user-agent env var — set
|
|
8
|
+
// by npm, pnpm and yarn alike on every script/exec they run, e.g.
|
|
9
|
+
// 'pnpm/9.1.0 npm/? node/v20.11.0 darwin x64'. This is how `docouture new` was
|
|
10
|
+
// actually invoked (`npx @inditextech/docouture-cli` vs `pnpm dlx
|
|
11
|
+
// @inditextech/docouture-cli`) — the best signal available for a brand-new
|
|
12
|
+
// repository with no lockfile of its own yet.
|
|
13
|
+
function fromUserAgent() {
|
|
14
|
+
const ua = process.env.npm_config_user_agent;
|
|
15
|
+
if (!ua)
|
|
16
|
+
return null;
|
|
17
|
+
if (ua.startsWith('pnpm/'))
|
|
18
|
+
return 'pnpm';
|
|
19
|
+
if (ua.startsWith('yarn/'))
|
|
20
|
+
return 'npm'; // yarn isn't a supported output here — fall through to npm.
|
|
21
|
+
return 'npm';
|
|
22
|
+
}
|
|
23
|
+
// Best-effort: an existing repository's own package.json#packageManager
|
|
24
|
+
// field (the corepack convention, e.g. "pnpm@9.1.0") wins over anything
|
|
25
|
+
// inferred, since it's an explicit declaration rather than a guess.
|
|
26
|
+
function fromPackageManagerField(targetDir) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = readFileSync(join(targetDir, 'package.json'), 'utf8');
|
|
29
|
+
const pkg = JSON.parse(raw);
|
|
30
|
+
if (typeof pkg.packageManager === 'string' && pkg.packageManager.startsWith('pnpm@'))
|
|
31
|
+
return 'pnpm';
|
|
32
|
+
if (typeof pkg.packageManager === 'string' && pkg.packageManager.startsWith('npm@'))
|
|
33
|
+
return 'npm';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// No package.json, unreadable, or no such field — fall through.
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function fromLockfile(targetDir) {
|
|
41
|
+
if (existsSync(join(targetDir, 'pnpm-lock.yaml')) || existsSync(join(targetDir, 'pnpm-workspace.yaml'))) {
|
|
42
|
+
return 'pnpm';
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Which package manager `docouture new`'s printed next-steps (and the
|
|
48
|
+
* package-manager-aware bits of the scaffolded workflow templates) should
|
|
49
|
+
* assume for this repository. Checked in order of how much it's worth
|
|
50
|
+
* trusting: an explicit `packageManager` field first, then a lockfile
|
|
51
|
+
* already committed at the repo root, then how `docouture new` itself was
|
|
52
|
+
* invoked, defaulting to npm when none of those say otherwise.
|
|
53
|
+
*/
|
|
54
|
+
export function detectPackageManager(targetDir) {
|
|
55
|
+
return fromPackageManagerField(targetDir) ?? fromLockfile(targetDir) ?? fromUserAgent() ?? 'npm';
|
|
56
|
+
}
|
|
57
|
+
export function packageManagerPlan(pm) {
|
|
58
|
+
if (pm === 'pnpm') {
|
|
59
|
+
return {
|
|
60
|
+
pm,
|
|
61
|
+
installCmd: 'pnpm install',
|
|
62
|
+
devCmd: 'pnpm run dev',
|
|
63
|
+
ciCmd: 'pnpm install --frozen-lockfile',
|
|
64
|
+
lockfile: 'pnpm-lock.yaml',
|
|
65
|
+
cacheName: 'pnpm',
|
|
66
|
+
setupStepYaml: PNPM_ACTION_SETUP_STEP,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
pm,
|
|
71
|
+
installCmd: 'npm install',
|
|
72
|
+
devCmd: 'npm run dev',
|
|
73
|
+
ciCmd: 'npm ci',
|
|
74
|
+
lockfile: 'package-lock.json',
|
|
75
|
+
cacheName: 'npm',
|
|
76
|
+
setupStepYaml: '',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=detect-package-manager.js.map
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Development server for a scaffolded Antora site — ported from the
|
|
3
|
+
// monorepo's code/scripts/dev.mjs, trimmed to what a standalone site (no
|
|
4
|
+
// sibling ui-bundle package, ever) actually needs: this package builds and
|
|
5
|
+
// ships the server so a bugfix reaches already-scaffolded sites on their
|
|
6
|
+
// next `npm update @inditextech/docouture-cli`, rather than being frozen at
|
|
7
|
+
// whatever `docouture new` copied at scaffold time.
|
|
8
|
+
//
|
|
9
|
+
// Antora is a batch generator with no incremental mode, so "live reload"
|
|
10
|
+
// here means re-running the whole build on every change and reloading the
|
|
11
|
+
// browser once it lands. A failed rebuild leaves the previous output in
|
|
12
|
+
// place — the browser keeps showing the last version that worked rather
|
|
13
|
+
// than a half-written site.
|
|
14
|
+
import { createServer } from 'node:http';
|
|
15
|
+
import { execFile } from 'node:child_process';
|
|
16
|
+
import { readFile, stat, watch as watchDir } from 'node:fs/promises';
|
|
17
|
+
import { createReadStream, existsSync } from 'node:fs';
|
|
18
|
+
import { extname, isAbsolute, join, normalize, relative, resolve } from 'node:path';
|
|
19
|
+
import { readSiteUrl } from './playbook-yml.js';
|
|
20
|
+
import { ANTORA_LOG_LEVEL_ARGS, filterObservableAntoraLog } from './antora-log.js';
|
|
21
|
+
import { debugLog } from './debug-log.js';
|
|
22
|
+
const RELOAD_PATH = '/__dev/reload';
|
|
23
|
+
const CLIENT_PATH = '/__dev/client.js';
|
|
24
|
+
const DEBOUNCE_MS = 150;
|
|
25
|
+
const BUILD_TIMEOUT_MS = 120_000;
|
|
26
|
+
const CLIENT_SCRIPT = `// Injected by docouture dev. Not part of the built site.
|
|
27
|
+
const es = new EventSource(${JSON.stringify(RELOAD_PATH)})
|
|
28
|
+
es.addEventListener('message', () => location.reload())
|
|
29
|
+
// Chrome (and other browsers) can freeze a navigated-away-from page in the
|
|
30
|
+
// back/forward cache instead of tearing it down — keeping its EventSource
|
|
31
|
+
// open indefinitely. Without this, every navigation leaks one open SSE
|
|
32
|
+
// connection; after enough of them the browser's per-origin connection
|
|
33
|
+
// limit is exhausted and the current page's own requests stop completing.
|
|
34
|
+
// 'pagehide' fires on both a normal unload and a bfcache freeze, so this
|
|
35
|
+
// closes the connection either way.
|
|
36
|
+
window.addEventListener('pagehide', () => es.close())
|
|
37
|
+
`;
|
|
38
|
+
const CONTENT_TYPES = {
|
|
39
|
+
'.css': 'text/css; charset=utf-8',
|
|
40
|
+
'.html': 'text/html; charset=utf-8',
|
|
41
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
42
|
+
'.json': 'application/json; charset=utf-8',
|
|
43
|
+
'.svg': 'image/svg+xml',
|
|
44
|
+
'.png': 'image/png',
|
|
45
|
+
'.jpg': 'image/jpeg',
|
|
46
|
+
'.gif': 'image/gif',
|
|
47
|
+
'.ico': 'image/x-icon',
|
|
48
|
+
'.woff': 'font/woff',
|
|
49
|
+
'.woff2': 'font/woff2',
|
|
50
|
+
'.ttf': 'font/ttf',
|
|
51
|
+
'.map': 'application/json; charset=utf-8',
|
|
52
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
53
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
54
|
+
};
|
|
55
|
+
const IGNORED = /(?:^|[\\/])(?:\.|.*~$)|\.swp$/;
|
|
56
|
+
function defaultRunBuild(siteRoot, onChildStart) {
|
|
57
|
+
// On Windows, a locally-installed bin is shimmed as `<name>.cmd`, not the
|
|
58
|
+
// bare POSIX shell script `execFile` would otherwise try (and fail) to
|
|
59
|
+
// run directly — see run-script.ts's own comment on the equivalent `npm`
|
|
60
|
+
// vs `npm.cmd` gotcha.
|
|
61
|
+
const antoraBinName = process.platform === 'win32' ? 'antora.cmd' : 'antora';
|
|
62
|
+
const antoraBin = resolve(siteRoot, 'node_modules', '.bin', antoraBinName);
|
|
63
|
+
return () => new Promise((resolvePromise) => {
|
|
64
|
+
// No --fetch here: that belongs to the one-off build only — on a
|
|
65
|
+
// watch loop it would re-fetch the content source on every keystroke.
|
|
66
|
+
// A timeout guards against a wedged/hung antora child process: with
|
|
67
|
+
// none, a single stuck build would permanently block every future
|
|
68
|
+
// rebuild for the rest of the dev session (drain()'s `running` flag
|
|
69
|
+
// never clears because runBuild() never resolves).
|
|
70
|
+
debugLog(`spawning: ${antoraBin} antora-playbook.local.yml ${ANTORA_LOG_LEVEL_ARGS.join(' ')}`);
|
|
71
|
+
const child = execFile(antoraBin, ['antora-playbook.local.yml', ...ANTORA_LOG_LEVEL_ARGS], { cwd: siteRoot, timeout: BUILD_TIMEOUT_MS }, (err, stdout, stderr) => {
|
|
72
|
+
if (err) {
|
|
73
|
+
// A failed rebuild gets its raw, unfiltered output — the whole
|
|
74
|
+
// story, not a filtered excerpt that might cut short exactly
|
|
75
|
+
// the line that explains the failure.
|
|
76
|
+
if (stdout)
|
|
77
|
+
process.stdout.write(stdout);
|
|
78
|
+
if (stderr)
|
|
79
|
+
process.stderr.write(stderr);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// A successful rebuild would otherwise be completely silent —
|
|
83
|
+
// every docouture-* extension's own observability logs (Kroki's
|
|
84
|
+
// auto-start/render lifecycle, search-index's summary, ...)
|
|
85
|
+
// discarded along with Antora's own routine noise. See
|
|
86
|
+
// antora-log.ts's own header for why `--log-level=info` above
|
|
87
|
+
// is what makes those lines exist to filter in the first place.
|
|
88
|
+
const observable = filterObservableAntoraLog(`${stdout}\n${stderr}`);
|
|
89
|
+
if (observable)
|
|
90
|
+
process.stdout.write(observable + '\n');
|
|
91
|
+
}
|
|
92
|
+
resolvePromise(!err);
|
|
93
|
+
});
|
|
94
|
+
onChildStart?.(child);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function readBasePath(siteRoot) {
|
|
98
|
+
let source;
|
|
99
|
+
try {
|
|
100
|
+
source = await readFile(join(siteRoot, 'antora-playbook.local.yml'), 'utf8');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
const url = readSiteUrl(source);
|
|
106
|
+
if (!url || url === '/')
|
|
107
|
+
return '';
|
|
108
|
+
const path = url.startsWith('/') ? url : URL.parse(url)?.pathname;
|
|
109
|
+
if (!path || path === '/')
|
|
110
|
+
return '';
|
|
111
|
+
return path.replace(/\/+$/, '');
|
|
112
|
+
}
|
|
113
|
+
export async function startDevServer(options) {
|
|
114
|
+
const { siteRoot } = options;
|
|
115
|
+
const log = options.log ?? ((msg) => console.log(`dev ${msg}`));
|
|
116
|
+
const logError = options.logError ?? ((msg) => console.error(`dev ${msg}`));
|
|
117
|
+
// Only the default build path's child is trackable for a forced kill on
|
|
118
|
+
// shutdown (below) — a custom options.runBuild (tests; a future
|
|
119
|
+
// non-Antora build) owns its own process lifecycle, if it has one at all.
|
|
120
|
+
let activeBuildChild = null;
|
|
121
|
+
const runBuild = options.runBuild ?? defaultRunBuild(siteRoot, (child) => (activeBuildChild = child));
|
|
122
|
+
const root = resolve(siteRoot, 'build', 'site');
|
|
123
|
+
const basePath = await readBasePath(siteRoot);
|
|
124
|
+
async function resolveTarget(urlPath) {
|
|
125
|
+
const decoded = decodeURIComponent(urlPath.split('?')[0] ?? '');
|
|
126
|
+
const candidate = resolve(join(root, normalize(decoded)));
|
|
127
|
+
// Containment check against `root`, expressed as a relative-path test —
|
|
128
|
+
// the form static analysis (CodeQL's js/path-injection) recognises as a
|
|
129
|
+
// real sanitizer, not just `startsWith` on two strings. A `candidate`
|
|
130
|
+
// outside `root` resolves to a relative path that either escapes
|
|
131
|
+
// upwards (`..`) or is itself absolute (no common prefix at all).
|
|
132
|
+
const rel = relative(root, candidate);
|
|
133
|
+
if (rel.startsWith('..') || isAbsolute(rel))
|
|
134
|
+
return null;
|
|
135
|
+
try {
|
|
136
|
+
const info = await stat(candidate);
|
|
137
|
+
if (!info.isDirectory())
|
|
138
|
+
return candidate;
|
|
139
|
+
const index = join(candidate, 'index.html');
|
|
140
|
+
await stat(index);
|
|
141
|
+
return index;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const clients = new Set();
|
|
148
|
+
function notifyReload() {
|
|
149
|
+
for (const res of clients) {
|
|
150
|
+
try {
|
|
151
|
+
res.write('data: reload\n\n');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Client's socket already gone (e.g. torn down mid-navigation) —
|
|
155
|
+
// drop it rather than let an unhandled write error crash the
|
|
156
|
+
// server; the 'error'/'close' listeners below also reap it.
|
|
157
|
+
clients.delete(res);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A path that isn't in the site. Antora only writes `404.html` at the
|
|
163
|
+
* site root when `site.url` is set (@antora/page-composer
|
|
164
|
+
* create404Page) — when it does, serve it (rewritten for reload, like
|
|
165
|
+
* any other HTML response) so the dev server's 404 matches the deployed
|
|
166
|
+
* one instead of a bare plain-text stand-in.
|
|
167
|
+
*/
|
|
168
|
+
async function respondNotFound(res) {
|
|
169
|
+
try {
|
|
170
|
+
const html = await readFile(join(root, '404.html'), 'utf8');
|
|
171
|
+
const snippet = `<script src="${CLIENT_PATH}"></script>`;
|
|
172
|
+
const body = html.includes('</body>') ? html.replace('</body>', `${snippet}</body>`) : html + snippet;
|
|
173
|
+
res.writeHead(404, { 'content-type': CONTENT_TYPES['.html'], 'cache-control': 'no-store' });
|
|
174
|
+
res.end(body);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
178
|
+
res.end('404 Not Found');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const server = createServer((req, res) => {
|
|
182
|
+
void (async () => {
|
|
183
|
+
const url = req.url ?? '/';
|
|
184
|
+
if (url === RELOAD_PATH) {
|
|
185
|
+
res.writeHead(200, {
|
|
186
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
187
|
+
'cache-control': 'no-cache',
|
|
188
|
+
connection: 'keep-alive',
|
|
189
|
+
});
|
|
190
|
+
res.write(': connected\n\n');
|
|
191
|
+
clients.add(res);
|
|
192
|
+
req.on('close', () => clients.delete(res));
|
|
193
|
+
// A page navigation can drop the underlying socket before the
|
|
194
|
+
// 'close' event above fires; without an 'error' listener here
|
|
195
|
+
// Node's default behaviour for an unhandled stream error is to
|
|
196
|
+
// throw, taking the whole dev server down mid-navigation.
|
|
197
|
+
res.on('error', () => clients.delete(res));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (url === CLIENT_PATH) {
|
|
201
|
+
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' });
|
|
202
|
+
res.end(CLIENT_SCRIPT);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
let sitePath = url;
|
|
206
|
+
if (basePath) {
|
|
207
|
+
if (url === '/' || url === basePath) {
|
|
208
|
+
res.writeHead(302, { location: `${basePath}/` });
|
|
209
|
+
res.end();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!url.startsWith(`${basePath}/`)) {
|
|
213
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
214
|
+
res.end(`404 Not Found — this site is served under ${basePath}/`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
sitePath = url.slice(basePath.length);
|
|
218
|
+
}
|
|
219
|
+
const target = await resolveTarget(sitePath);
|
|
220
|
+
if (!target) {
|
|
221
|
+
await respondNotFound(res);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const contentType = CONTENT_TYPES[extname(target)] ?? 'application/octet-stream';
|
|
225
|
+
if (contentType === CONTENT_TYPES['.html']) {
|
|
226
|
+
const html = await readFile(target, 'utf8');
|
|
227
|
+
const snippet = `<script src="${CLIENT_PATH}"></script>`;
|
|
228
|
+
const body = html.includes('</body>') ? html.replace('</body>', `${snippet}</body>`) : html + snippet;
|
|
229
|
+
res.writeHead(200, { 'content-type': contentType, 'cache-control': 'no-store' });
|
|
230
|
+
res.end(body);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
res.writeHead(200, { 'content-type': contentType, 'cache-control': 'no-store' });
|
|
234
|
+
createReadStream(target).pipe(res);
|
|
235
|
+
})();
|
|
236
|
+
});
|
|
237
|
+
let running = false;
|
|
238
|
+
let queued = false;
|
|
239
|
+
let timer = null;
|
|
240
|
+
function schedule() {
|
|
241
|
+
queued = true;
|
|
242
|
+
if (timer)
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
timer = setTimeout(() => void drain(), DEBOUNCE_MS);
|
|
245
|
+
}
|
|
246
|
+
async function drain() {
|
|
247
|
+
timer = null;
|
|
248
|
+
if (running || !queued)
|
|
249
|
+
return;
|
|
250
|
+
running = true;
|
|
251
|
+
try {
|
|
252
|
+
while (queued) {
|
|
253
|
+
queued = false;
|
|
254
|
+
const started = Date.now();
|
|
255
|
+
log('content changed, rebuilding site');
|
|
256
|
+
const ok = await runBuild();
|
|
257
|
+
if (ok) {
|
|
258
|
+
log(`rebuilt in ${((Date.now() - started) / 1000).toFixed(1)}s`);
|
|
259
|
+
notifyReload();
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
logError('rebuild failed, serving the previous build');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
// A build that throws instead of resolving false must not leave
|
|
268
|
+
// `running` stuck true forever — that would silently disable every
|
|
269
|
+
// future rebuild for the rest of the dev session.
|
|
270
|
+
logError(`rebuild crashed, serving the previous build: ${err.message}`);
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
running = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const watchAbort = new AbortController();
|
|
277
|
+
async function watchPath(path, watchOptions = {}) {
|
|
278
|
+
try {
|
|
279
|
+
for await (const event of watchDir(path, {
|
|
280
|
+
recursive: watchOptions.recursive ?? true,
|
|
281
|
+
signal: watchAbort.signal,
|
|
282
|
+
})) {
|
|
283
|
+
if (event.filename && IGNORED.test(event.filename))
|
|
284
|
+
continue;
|
|
285
|
+
schedule();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (err.name !== 'AbortError') {
|
|
290
|
+
logError(`stopped watching ${path}: ${err.message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!existsSync(root)) {
|
|
295
|
+
log('no build output yet, building the site');
|
|
296
|
+
if (!(await runBuild())) {
|
|
297
|
+
throw new Error('initial build failed');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
await new Promise((resolvePromise, reject) => {
|
|
301
|
+
server.once('error', reject);
|
|
302
|
+
server.listen(options.port ?? 5000, () => {
|
|
303
|
+
server.off('error', reject);
|
|
304
|
+
resolvePromise();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
const address = server.address();
|
|
308
|
+
const port = typeof address === 'object' && address ? address.port : (options.port ?? 5000);
|
|
309
|
+
log(`serving ${root}`);
|
|
310
|
+
log(` http://localhost:${port}${basePath}/`);
|
|
311
|
+
const watchers = [
|
|
312
|
+
watchPath(join(siteRoot, 'docs')),
|
|
313
|
+
watchPath(join(siteRoot, 'antora-playbook.local.yml'), { recursive: false }),
|
|
314
|
+
];
|
|
315
|
+
void Promise.all(watchers);
|
|
316
|
+
return {
|
|
317
|
+
port,
|
|
318
|
+
basePath,
|
|
319
|
+
url: `http://localhost:${port}${basePath}/`,
|
|
320
|
+
async close() {
|
|
321
|
+
watchAbort.abort();
|
|
322
|
+
if (timer)
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
// A rebuild that's still in flight when SIGINT/SIGTERM arrives would
|
|
325
|
+
// otherwise be left running as an orphan after the dev server itself
|
|
326
|
+
// has already torn down — kill it too, best-effort (it may already
|
|
327
|
+
// have exited on its own by the time close() runs).
|
|
328
|
+
if (activeBuildChild && activeBuildChild.exitCode === null && !activeBuildChild.killed) {
|
|
329
|
+
activeBuildChild.kill('SIGTERM');
|
|
330
|
+
}
|
|
331
|
+
for (const res of clients)
|
|
332
|
+
res.end();
|
|
333
|
+
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=dev-server.js.map
|