docpensieve 0.5.0-beta.1 → 0.5.0-beta.3
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/docpensieve.js +3 -0
- package/package.json +5 -5
- package/src/commands/build.js +2 -0
- package/src/commands/dev.js +14 -3
- package/src/commands/init.js +209 -18
- package/src/commands/serve.js +5 -3
- package/src/server.js +33 -3
- package/starter/01-guide/01-installation.md +1 -0
- package/starter/01-guide/02-first-site.md +4 -0
- package/starter/01-guide/06-deployment.md +26 -0
- package/starter/01-guide/07-migrate-to-beta.md +17 -2
- package/starter/01-guide/10-languages.md +13 -0
- package/starter/02-components/01-card.mdx +3 -0
- package/starter/02-components/13-snippet.mdx +230 -0
- package/starter/02-components/14-hero.mdx +230 -0
- package/starter/02-components/15-calendar.mdx +130 -0
- package/starter/02-components/index.mdx +9 -3
- package/starter/02-components/snippet-example.js +32 -0
- package/starter/02-components/snippet-example.py +19 -0
- package/starter/03-reference/01-cli.md +14 -7
- package/starter/03-reference/02-configuration.md +19 -0
- package/starter/03-reference/05-api.md +302 -6
- package/starter/05-whats-new.md +66 -3
- package/types/commands/dev.d.ts +2 -1
- package/types/commands/init.d.ts +10 -5
- package/types/commands/serve.d.ts +2 -1
- package/types/server.d.ts +24 -1
package/bin/docpensieve.js
CHANGED
|
@@ -34,6 +34,7 @@ program
|
|
|
34
34
|
.option('-t, --theme <framework>', 'tailwind | custom')
|
|
35
35
|
.option('-u, --site-url <url>', 'public URL of the site')
|
|
36
36
|
.option('--version-name <version>', 'first version, e.g. 1.0')
|
|
37
|
+
.option('--translation <code>', 'code of a second language, e.g. fr')
|
|
37
38
|
.option('-y, --yes', 'accept the defaults without a dialogue')
|
|
38
39
|
.option('-f, --force', 'overwrite an existing configuration')
|
|
39
40
|
.option('--minimal', "leave DocPensieve's documentation out of the new site")
|
|
@@ -62,6 +63,7 @@ program
|
|
|
62
63
|
.command('dev')
|
|
63
64
|
.description('Development server with reload')
|
|
64
65
|
.option('-p, --port <number>', 'listening port', Number, 3000)
|
|
66
|
+
.option('--host <address>', 'address to listen on; 0.0.0.0 opens it to the network', '127.0.0.1')
|
|
65
67
|
.action(async (options) => {
|
|
66
68
|
await dev(options);
|
|
67
69
|
});
|
|
@@ -70,6 +72,7 @@ program
|
|
|
70
72
|
.command('serve')
|
|
71
73
|
.description('Serves the output folder statically')
|
|
72
74
|
.option('-p, --port <number>', 'listening port', Number, 4000)
|
|
75
|
+
.option('--host <address>', 'address to listen on; 0.0.0.0 opens it to the network', '127.0.0.1')
|
|
73
76
|
.option('-d, --dir <dir>', 'folder to serve')
|
|
74
77
|
.action(async (options) => {
|
|
75
78
|
await serve(options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docpensieve",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.3",
|
|
4
4
|
"description": "DocPensieve command-line interface (init, build, check, dev, serve)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"types"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@docpensieve/components": "0.5.0-beta.
|
|
28
|
-
"@docpensieve/core": "0.5.0-beta.
|
|
29
|
-
"@docpensieve/shared": "0.5.0-beta.
|
|
30
|
-
"@docpensieve/theme": "0.5.0-beta.
|
|
27
|
+
"@docpensieve/components": "0.5.0-beta.3",
|
|
28
|
+
"@docpensieve/core": "0.5.0-beta.3",
|
|
29
|
+
"@docpensieve/shared": "0.5.0-beta.3",
|
|
30
|
+
"@docpensieve/theme": "0.5.0-beta.3",
|
|
31
31
|
"chalk": "^6.0.0",
|
|
32
32
|
"chokidar": "^5.0.0",
|
|
33
33
|
"commander": "^15.0.0"
|
package/src/commands/build.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createRegistry,
|
|
12
12
|
setSiteContext,
|
|
13
13
|
setAdmonitionKinds,
|
|
14
|
+
setSnippetIcons,
|
|
14
15
|
} from '@docpensieve/components';
|
|
15
16
|
import { SiteGenerator, loadConfig, resolveVersion } from '@docpensieve/core';
|
|
16
17
|
|
|
@@ -28,6 +29,7 @@ export async function build(versionSlug, options = {}) {
|
|
|
28
29
|
|
|
29
30
|
// The kinds a project declares, beside the ones shipped.
|
|
30
31
|
setAdmonitionKinds(config.admonitions);
|
|
32
|
+
setSnippetIcons(config.snippetIcons);
|
|
31
33
|
|
|
32
34
|
const generator = new SiteGenerator(config, {
|
|
33
35
|
// `globalComponents: false` removes the shipped components: a project that
|
package/src/commands/dev.js
CHANGED
|
@@ -12,12 +12,20 @@ import {
|
|
|
12
12
|
createRegistry,
|
|
13
13
|
setSiteContext,
|
|
14
14
|
setAdmonitionKinds,
|
|
15
|
+
setSnippetIcons,
|
|
15
16
|
} from '@docpensieve/components';
|
|
16
17
|
import { SiteGenerator, loadConfig } from '@docpensieve/core';
|
|
17
18
|
import { CONFIG_FILENAME, DocPensieveError, THEME_FOLDER } from '@docpensieve/shared';
|
|
18
19
|
import chokidar from 'chokidar';
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_HOST,
|
|
23
|
+
RELOAD_PATH,
|
|
24
|
+
createStaticServer,
|
|
25
|
+
exposedNotice,
|
|
26
|
+
isExposed,
|
|
27
|
+
listen,
|
|
28
|
+
} from '../server.js';
|
|
21
29
|
import { createTheme } from '../theme.js';
|
|
22
30
|
|
|
23
31
|
/** Default port of `dev`, distinct from that of `serve`. */
|
|
@@ -40,7 +48,7 @@ const RELOAD_SCRIPT =
|
|
|
40
48
|
`.addEventListener("message",()=>location.reload())</script>`;
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
|
-
* @param {{ port?: number, cwd?: string }} [options]
|
|
51
|
+
* @param {{ port?: number, host?: string, cwd?: string }} [options]
|
|
44
52
|
* @returns {Promise<{
|
|
45
53
|
* server: import('node:http').Server,
|
|
46
54
|
* watcher: import('chokidar').FSWatcher,
|
|
@@ -62,6 +70,7 @@ export async function dev(options = {}) {
|
|
|
62
70
|
// Read again with the rest: a kind added to the configuration must show
|
|
63
71
|
// without restarting the command.
|
|
64
72
|
setAdmonitionKinds(current.admonitions);
|
|
73
|
+
setSnippetIcons(current.snippetIcons);
|
|
65
74
|
const generator = new SiteGenerator(current, {
|
|
66
75
|
// `globalComponents: false` removes the shipped components: a project
|
|
67
76
|
// that defines its own thus avoids a name collision. The option was
|
|
@@ -87,8 +96,10 @@ export async function dev(options = {}) {
|
|
|
87
96
|
},
|
|
88
97
|
});
|
|
89
98
|
|
|
90
|
-
const
|
|
99
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
100
|
+
const port = await listen(server, options.port ?? DEFAULT_PORT, 10, host);
|
|
91
101
|
const url = `http://localhost:${port}${config.baseUrl}`;
|
|
102
|
+
if (isExposed(host)) console.log(exposedNotice(host));
|
|
92
103
|
console.log(`served at ${url}`);
|
|
93
104
|
|
|
94
105
|
// The project's own stylesheets, re-read by every rebuild.
|
package/src/commands/init.js
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
DocPensieveError,
|
|
19
19
|
THEME_FOLDER,
|
|
20
20
|
THEME_FRAMEWORKS,
|
|
21
|
+
isLanguageCode,
|
|
22
|
+
languageName,
|
|
21
23
|
} from '@docpensieve/shared';
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -30,7 +32,16 @@ const FRAMEWORK_LABELS = {
|
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
/** Answers used when there is no dialogue. */
|
|
33
|
-
const DEFAULTS = {
|
|
35
|
+
const DEFAULTS = {
|
|
36
|
+
name: 'My documentation',
|
|
37
|
+
siteUrl: '',
|
|
38
|
+
theme: 'tailwind',
|
|
39
|
+
version: '1.0',
|
|
40
|
+
translation: '',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Second language offered first, being the one whose wording ships too. */
|
|
44
|
+
const DEFAULT_TRANSLATION = 'fr';
|
|
34
45
|
|
|
35
46
|
/**
|
|
36
47
|
* Folder of the installed DocPensieve documentation, inside the version folder.
|
|
@@ -125,15 +136,43 @@ async function askDocumentation(rl) {
|
|
|
125
136
|
}
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Asks whether the site will carry a second language, and which one.
|
|
141
|
+
*
|
|
142
|
+
* Asked rather than left to the configuration: a translation changes the
|
|
143
|
+
* shape of the project — a folder per language beside the pages — and that is
|
|
144
|
+
* cheaper to set up at the start than to retrofit.
|
|
145
|
+
*
|
|
146
|
+
* @param {import('node:readline/promises').Interface} rl
|
|
147
|
+
* @returns {Promise<string>} Language code, or `''` for a single language.
|
|
148
|
+
*/
|
|
149
|
+
async function askTranslation(rl) {
|
|
150
|
+
for (;;) {
|
|
151
|
+
const answer = (await rl.question('Will the site be in several languages? [y/N]: '))
|
|
152
|
+
.trim()
|
|
153
|
+
.toLowerCase();
|
|
154
|
+
if (answer === '' || answer === 'n' || answer === 'no') return '';
|
|
155
|
+
if (answer === 'y' || answer === 'yes') break;
|
|
156
|
+
console.log('Answer not understood. Expected: y or n.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (;;) {
|
|
160
|
+
const code = (await ask(rl, 'Code of the second language', DEFAULT_TRANSLATION)).trim();
|
|
161
|
+
if (isLanguageCode(code)) return code;
|
|
162
|
+
console.log(`"${code}" does not name a language. Expected a code: fr, de, pt-BR, zh-Hans.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
128
166
|
/**
|
|
129
167
|
* Gathers the answers, through a dialogue or from the options.
|
|
130
168
|
*
|
|
131
169
|
* @param {{
|
|
132
170
|
* name?: string, theme?: string, siteUrl?: string, version?: string,
|
|
133
|
-
* yes?: boolean, minimal?: boolean,
|
|
171
|
+
* translation?: string, yes?: boolean, minimal?: boolean,
|
|
134
172
|
* }} options
|
|
135
173
|
* @returns {Promise<{
|
|
136
174
|
* name: string, theme: string, siteUrl: string, version: string, docs: boolean,
|
|
175
|
+
* translation: string,
|
|
137
176
|
* }>}
|
|
138
177
|
*/
|
|
139
178
|
async function collect(options) {
|
|
@@ -142,6 +181,7 @@ async function collect(options) {
|
|
|
142
181
|
siteUrl: options.siteUrl ?? DEFAULTS.siteUrl,
|
|
143
182
|
theme: options.theme ?? DEFAULTS.theme,
|
|
144
183
|
version: options.version ?? DEFAULTS.version,
|
|
184
|
+
translation: options.translation ?? DEFAULTS.translation,
|
|
145
185
|
docs: !options.minimal,
|
|
146
186
|
};
|
|
147
187
|
|
|
@@ -154,7 +194,7 @@ async function collect(options) {
|
|
|
154
194
|
if (!process.stdin.isTTY) {
|
|
155
195
|
console.log('No interactive terminal: no questions asked, the options and defaults apply.');
|
|
156
196
|
console.log(
|
|
157
|
-
'To choose, pass --name, --site-url, --theme, --version-name or --minimal; --yes silences this notice.',
|
|
197
|
+
'To choose, pass --name, --site-url, --theme, --version-name, --translation or --minimal; --yes silences this notice.',
|
|
158
198
|
);
|
|
159
199
|
return fromOptions;
|
|
160
200
|
}
|
|
@@ -164,9 +204,10 @@ async function collect(options) {
|
|
|
164
204
|
const name = await ask(rl, 'Project name', fromOptions.name);
|
|
165
205
|
const siteUrl = await ask(rl, 'Public URL of the site (optional)', fromOptions.siteUrl);
|
|
166
206
|
const version = await ask(rl, 'First version', fromOptions.version);
|
|
207
|
+
const translation = options.translation ?? (await askTranslation(rl));
|
|
167
208
|
const theme = options.theme ?? (await askFramework(rl));
|
|
168
209
|
const docs = options.minimal ? false : await askDocumentation(rl);
|
|
169
|
-
return { name, siteUrl, version, theme, docs };
|
|
210
|
+
return { name, siteUrl, version, theme, docs, translation };
|
|
170
211
|
} finally {
|
|
171
212
|
rl.close();
|
|
172
213
|
}
|
|
@@ -183,6 +224,18 @@ async function collect(options) {
|
|
|
183
224
|
*/
|
|
184
225
|
const quote = (value) => `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
185
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Writes a language code as a property name.
|
|
229
|
+
*
|
|
230
|
+
* `fr` stands on its own; `pt-BR` and `zh-Hans` carry a hyphen, which is a
|
|
231
|
+
* minus sign to JavaScript — unquoted, the generated configuration would not
|
|
232
|
+
* parse.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} code
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
const key = (code) => (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(code) ? code : quote(code));
|
|
238
|
+
|
|
186
239
|
/**
|
|
187
240
|
* Renders `docpensieve.config.mjs`.
|
|
188
241
|
*
|
|
@@ -190,10 +243,13 @@ const quote = (value) => `'${String(value).replace(/\\/g, '\\\\').replace(/'/g,
|
|
|
190
243
|
* or commented out with an example — so that the first file a user opens also
|
|
191
244
|
* tells them everything they can change.
|
|
192
245
|
*
|
|
193
|
-
* @param {{
|
|
246
|
+
* @param {{
|
|
247
|
+
* name: string, theme: string, siteUrl: string, version: string,
|
|
248
|
+
* translation?: string,
|
|
249
|
+
* }} answers
|
|
194
250
|
* @returns {string} Contents of `docpensieve.config.mjs`.
|
|
195
251
|
*/
|
|
196
|
-
function renderConfig({ name, theme, siteUrl, version }) {
|
|
252
|
+
function renderConfig({ name, theme, siteUrl, version, translation = '' }) {
|
|
197
253
|
const slug = versionSlug(version);
|
|
198
254
|
// A type annotation rather than an `import`: `defineConfig` transforms
|
|
199
255
|
// nothing, it is only there for autocompletion. Actually importing it would
|
|
@@ -233,9 +289,14 @@ function renderConfig({ name, theme, siteUrl, version }) {
|
|
|
233
289
|
` name: ${quote(version)}, // label in the version switcher`,
|
|
234
290
|
` folder: ${quote(`docs/${slug}`)},`,
|
|
235
291
|
' current: true,',
|
|
236
|
-
|
|
292
|
+
` // Pages of this version in another language, served under /${translation || 'fr'}/.`,
|
|
237
293
|
' // Your own language stays where it is, and keeps its addresses.',
|
|
238
|
-
|
|
294
|
+
// Written out, not left as an example, once the language is known: the
|
|
295
|
+
// field is the whole of the feature, and the folder beside it already
|
|
296
|
+
// holds a page.
|
|
297
|
+
translation
|
|
298
|
+
? ` translations: { ${key(translation)}: ${quote(`docs/${slug}-${translation}`)} },`
|
|
299
|
+
: ` // translations: { fr: ${quote(`docs/${slug}-fr`)} },`,
|
|
239
300
|
' },',
|
|
240
301
|
' ],',
|
|
241
302
|
'',
|
|
@@ -307,6 +368,16 @@ function renderConfig({ name, theme, siteUrl, version }) {
|
|
|
307
368
|
' // Back-to-top button on every page.',
|
|
308
369
|
' scrollToTop: true,',
|
|
309
370
|
'',
|
|
371
|
+
' // A button copying each block of code. It is the one thing that makes a',
|
|
372
|
+
' // content page load a script — a small file, shared by every page of the',
|
|
373
|
+
' // version — so it stays off until you ask for it.',
|
|
374
|
+
' copyCode: false,',
|
|
375
|
+
'',
|
|
376
|
+
' // Icon collection a code block takes the mark of its language from, for',
|
|
377
|
+
" // instance 'simple-icons' once @iconify-json/simple-icons is installed.",
|
|
378
|
+
' // Left empty, the short name of the language is shown instead.',
|
|
379
|
+
" snippetIcons: '',",
|
|
380
|
+
'',
|
|
310
381
|
' // The header stays at the top of the screen. false lets it scroll away',
|
|
311
382
|
' // with the page, and gives its height back to the text.',
|
|
312
383
|
' stickyHeader: true,',
|
|
@@ -353,11 +424,12 @@ function versionSlug(version) {
|
|
|
353
424
|
}
|
|
354
425
|
|
|
355
426
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
427
|
+
* Home page of the project, in the language of the site.
|
|
428
|
+
*
|
|
429
|
+
* @param {{ name: string, docs: boolean, slug: string, translation: string }} answers
|
|
358
430
|
* @returns {string}
|
|
359
431
|
*/
|
|
360
|
-
const renderIndex = (name, docs) => `---
|
|
432
|
+
const renderIndex = ({ name, docs, slug, translation }) => `---
|
|
361
433
|
title: Introduction
|
|
362
434
|
description: Documentation of ${name}.
|
|
363
435
|
date: ${new Date().toISOString().slice(0, 10)}
|
|
@@ -378,6 +450,20 @@ without appearing in the URL.
|
|
|
378
450
|
|
|
379
451
|
See the [installation guide](/guide/installation/).
|
|
380
452
|
${
|
|
453
|
+
translation
|
|
454
|
+
? `
|
|
455
|
+
## In ${languageName(translation)}
|
|
456
|
+
|
|
457
|
+
The same pages live in \`docs/${slug}-${translation}/\`, and the language switcher
|
|
458
|
+
in the header moves between them.
|
|
459
|
+
|
|
460
|
+
A page with no twin there does not exist in that language: it stays out of the
|
|
461
|
+
menu and out of the sitemap, and the switcher names the language without
|
|
462
|
+
offering it. The installation page is in that case — write
|
|
463
|
+
\`docs/${slug}-${translation}/01-guide/01-installation.md\` and it appears.
|
|
464
|
+
${docs ? '\nThe [languages guide](/docpensieve/guide/languages/) covers the rest.\n' : ''}`
|
|
465
|
+
: ''
|
|
466
|
+
}${
|
|
381
467
|
docs
|
|
382
468
|
? `
|
|
383
469
|
## Learning DocPensieve
|
|
@@ -389,6 +475,70 @@ tool that builds this site, installed along with it. Delete its folder,
|
|
|
389
475
|
: ''
|
|
390
476
|
}`;
|
|
391
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Home page of the second language.
|
|
480
|
+
*
|
|
481
|
+
* French is written out, the tool shipping its wording too. Any other
|
|
482
|
+
* language gets the page in English, saying in its first line that it is
|
|
483
|
+
* there to be translated: a copy passing for a translation is the one failure
|
|
484
|
+
* this feature invites — the reader gets English under an address that
|
|
485
|
+
* promised their language, and nothing reports it.
|
|
486
|
+
*
|
|
487
|
+
* Only the home page is written. Its twin, the installation page, is left
|
|
488
|
+
* untranslated on purpose: it is what shows that an untranslated page does
|
|
489
|
+
* not exist in that language, which no sentence explains as well as the menu
|
|
490
|
+
* that lacks it.
|
|
491
|
+
*
|
|
492
|
+
* @param {{ name: string, slug: string, translation: string }} answers
|
|
493
|
+
* @returns {string}
|
|
494
|
+
*/
|
|
495
|
+
function renderTranslatedIndex({ name, slug, translation }) {
|
|
496
|
+
const french = translation.toLowerCase().split('-')[0] === 'fr';
|
|
497
|
+
const head = `---
|
|
498
|
+
title: Introduction
|
|
499
|
+
description: ${french ? `Documentation de ${name}.` : `Documentation of ${name}.`}
|
|
500
|
+
date: ${new Date().toISOString().slice(0, 10)}
|
|
501
|
+
|
|
502
|
+
jsonld:
|
|
503
|
+
type: TechArticle
|
|
504
|
+
breadcrumbs: true
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
# ${name}
|
|
508
|
+
`;
|
|
509
|
+
|
|
510
|
+
if (french) {
|
|
511
|
+
return `${head}
|
|
512
|
+
Bienvenue dans la documentation.
|
|
513
|
+
|
|
514
|
+
## Une page et sa jumelle
|
|
515
|
+
|
|
516
|
+
Cette page est la version française de \`docs/${slug}/index.md\`. Le sélecteur
|
|
517
|
+
de langue, dans l'en-tête, passe de l'une à l'autre.
|
|
518
|
+
|
|
519
|
+
La page d'installation, elle, n'est pas traduite : elle **n'existe pas** en
|
|
520
|
+
français. Elle ne figure ni dans le menu ni dans le plan du site, et le
|
|
521
|
+
sélecteur la nomme sans la proposer. Écrivez
|
|
522
|
+
\`docs/${slug}-${translation}/01-guide/01-installation.md\` pour la voir apparaître.
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return `${head}
|
|
527
|
+
**Replace this page with your translation.** It is the ${languageName(translation)}
|
|
528
|
+
twin of \`docs/${slug}/index.md\`, written in English so that the site builds:
|
|
529
|
+
left as it is, a reader who picks ${languageName(translation, translation)} gets English.
|
|
530
|
+
|
|
531
|
+
## One page, two languages
|
|
532
|
+
|
|
533
|
+
The language switcher in the header moves between this page and its twin.
|
|
534
|
+
|
|
535
|
+
The installation page has no twin here, so it does not exist in this language:
|
|
536
|
+
it stays out of the menu and out of the sitemap, and the switcher names the
|
|
537
|
+
language without offering it. Write
|
|
538
|
+
\`docs/${slug}-${translation}/01-guide/01-installation.md\` and it appears.
|
|
539
|
+
`;
|
|
540
|
+
}
|
|
541
|
+
|
|
392
542
|
/** @returns {string} Sample page, showing ordering and highlighting. */
|
|
393
543
|
const renderGuide = () => `---
|
|
394
544
|
title: Installation
|
|
@@ -478,11 +628,14 @@ async function installDocumentation(source, target, slug) {
|
|
|
478
628
|
* @param {string} [dir] Target folder, created if needed.
|
|
479
629
|
* @param {{
|
|
480
630
|
* name?: string, theme?: string, siteUrl?: string, version?: string,
|
|
481
|
-
* yes?: boolean, force?: boolean, minimal?: boolean,
|
|
482
|
-
* }} [options] `minimal` leaves DocPensieve's documentation out of the site
|
|
483
|
-
*
|
|
484
|
-
* @
|
|
485
|
-
*
|
|
631
|
+
* translation?: string, yes?: boolean, force?: boolean, minimal?: boolean,
|
|
632
|
+
* }} [options] `minimal` leaves DocPensieve's documentation out of the site;
|
|
633
|
+
* `translation` is the code of a second language, `fr` for instance.
|
|
634
|
+
* @returns {Promise<{
|
|
635
|
+
* dir: string, theme: string, docs: boolean, translation: string,
|
|
636
|
+
* }>}
|
|
637
|
+
* @throws {DocPensieveError} Unknown framework, unknown language, project
|
|
638
|
+
* already initialised, or documentation to install missing.
|
|
486
639
|
*/
|
|
487
640
|
export async function init(dir = '.', options = {}) {
|
|
488
641
|
const target = path.resolve(dir);
|
|
@@ -504,6 +657,14 @@ export async function init(dir = '.', options = {}) {
|
|
|
504
657
|
});
|
|
505
658
|
}
|
|
506
659
|
|
|
660
|
+
// Checked here for the same reason as the framework: a code refused after
|
|
661
|
+
// five questions would be five questions wasted.
|
|
662
|
+
if (options.translation && !isLanguageCode(options.translation)) {
|
|
663
|
+
throw new DocPensieveError(`"${options.translation}" does not name a language.`, {
|
|
664
|
+
hint: 'Write the code, not the name: "fr" for French, "pt-BR", "zh-Hans". It becomes the lang of the document and a segment of the address.',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
507
668
|
const answers = await collect(options);
|
|
508
669
|
|
|
509
670
|
// Located before anything is written: a project left half set up would be
|
|
@@ -535,8 +696,21 @@ export async function init(dir = '.', options = {}) {
|
|
|
535
696
|
|
|
536
697
|
await mkdir(path.join(docsDir, '01-guide'), { recursive: true });
|
|
537
698
|
await writeFile(configPath, renderConfig(answers), 'utf8');
|
|
538
|
-
await writeFile(
|
|
699
|
+
await writeFile(
|
|
700
|
+
path.join(docsDir, 'index.md'),
|
|
701
|
+
renderIndex({ name: answers.name, docs: answers.docs, slug, translation: answers.translation }),
|
|
702
|
+
'utf8',
|
|
703
|
+
);
|
|
539
704
|
await writeFile(path.join(docsDir, '01-guide', '01-installation.md'), renderGuide(), 'utf8');
|
|
705
|
+
if (answers.translation) {
|
|
706
|
+
const folder = path.join(target, 'docs', `${slug}-${answers.translation}`);
|
|
707
|
+
await mkdir(folder, { recursive: true });
|
|
708
|
+
await writeFile(
|
|
709
|
+
path.join(folder, 'index.md'),
|
|
710
|
+
renderTranslatedIndex({ name: answers.name, slug, translation: answers.translation }),
|
|
711
|
+
'utf8',
|
|
712
|
+
);
|
|
713
|
+
}
|
|
540
714
|
if (documentation) {
|
|
541
715
|
await installDocumentation(documentation, path.join(docsDir, DOCS_FOLDER), slug);
|
|
542
716
|
}
|
|
@@ -548,14 +722,31 @@ export async function init(dir = '.', options = {}) {
|
|
|
548
722
|
console.log(` ${CONFIG_FILENAME}`);
|
|
549
723
|
console.log(` docs/${slug}/index.md`);
|
|
550
724
|
console.log(` docs/${slug}/01-guide/01-installation.md`);
|
|
725
|
+
if (answers.translation) {
|
|
726
|
+
const state =
|
|
727
|
+
answers.translation.toLowerCase().split('-')[0] === 'fr'
|
|
728
|
+
? `in ${languageName(answers.translation)}`
|
|
729
|
+
: 'to translate';
|
|
730
|
+
console.log(` docs/${slug}-${answers.translation}/index.md the home page, ${state}`);
|
|
731
|
+
}
|
|
551
732
|
if (documentation) {
|
|
552
733
|
console.log(` docs/${slug}/${DOCS_FOLDER}/ DocPensieve's documentation, to delete when done`);
|
|
553
734
|
}
|
|
554
735
|
for (const line of stylesheets) console.log(` ${line}`);
|
|
555
736
|
console.log(`\nTheme: ${answers.theme} — ${FRAMEWORK_LABELS[answers.theme]}`);
|
|
737
|
+
if (answers.translation) {
|
|
738
|
+
console.log(
|
|
739
|
+
`Second language: ${languageName(answers.translation)} (${answers.translation}) — served under /${answers.translation}/`,
|
|
740
|
+
);
|
|
741
|
+
}
|
|
556
742
|
console.log('\nNext: npx docpensieve dev');
|
|
557
743
|
|
|
558
|
-
return {
|
|
744
|
+
return {
|
|
745
|
+
dir: target,
|
|
746
|
+
theme: answers.theme,
|
|
747
|
+
docs: answers.docs,
|
|
748
|
+
translation: answers.translation,
|
|
749
|
+
};
|
|
559
750
|
}
|
|
560
751
|
|
|
561
752
|
/**
|
package/src/commands/serve.js
CHANGED
|
@@ -10,13 +10,13 @@ import path from 'node:path';
|
|
|
10
10
|
import { loadConfig } from '@docpensieve/core';
|
|
11
11
|
import { DocPensieveError } from '@docpensieve/shared';
|
|
12
12
|
|
|
13
|
-
import { createStaticServer, listen } from '../server.js';
|
|
13
|
+
import { DEFAULT_HOST, createStaticServer, exposedNotice, isExposed, listen } from '../server.js';
|
|
14
14
|
|
|
15
15
|
/** Default port of `serve`, distinct from that of `dev`. */
|
|
16
16
|
const DEFAULT_PORT = 4000;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* @param {{ port?: number, dir?: string, cwd?: string }} [options]
|
|
19
|
+
* @param {{ port?: number, host?: string, dir?: string, cwd?: string }} [options]
|
|
20
20
|
* @returns {Promise<{ server: import('node:http').Server, port: number, url: string }>}
|
|
21
21
|
* @throws {DocPensieveError} When the folder to serve does not exist.
|
|
22
22
|
*/
|
|
@@ -34,11 +34,13 @@ export async function serve(options = {}) {
|
|
|
34
34
|
// The site is mounted under the configuration's baseUrl: otherwise the links
|
|
35
35
|
// of the generated pages would not resolve locally.
|
|
36
36
|
const server = createStaticServer({ root, basePath: config.baseUrl });
|
|
37
|
-
const
|
|
37
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
38
|
+
const port = await listen(server, options.port ?? DEFAULT_PORT, 10, host);
|
|
38
39
|
const url = `http://localhost:${port}${config.baseUrl}`;
|
|
39
40
|
|
|
40
41
|
console.log(`${root}`);
|
|
41
42
|
console.log(`served at ${url}`);
|
|
43
|
+
if (isExposed(host)) console.log(exposedNotice(host));
|
|
42
44
|
|
|
43
45
|
return { server, port, url };
|
|
44
46
|
}
|
package/src/server.js
CHANGED
|
@@ -162,16 +162,35 @@ export function createStaticServer({ root, basePath = '/', inject = null, onRelo
|
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/** The loopback address: the site is served to this machine and to no other. */
|
|
166
|
+
export const DEFAULT_HOST = '127.0.0.1';
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether an address reaches beyond this machine.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} host
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
export function isExposed(host) {
|
|
175
|
+
return !['127.0.0.1', 'localhost', '::1'].includes(host);
|
|
176
|
+
}
|
|
177
|
+
|
|
165
178
|
/**
|
|
166
179
|
* Starts listening, looking for a free port if needed.
|
|
167
180
|
*
|
|
181
|
+
* On this machine only, unless asked otherwise. Called without a host, Node
|
|
182
|
+
* listens on every interface: anyone on the same network — a café, a
|
|
183
|
+
* coworking space — could read the site being written, drafts included.
|
|
184
|
+
*
|
|
168
185
|
* @param {import('node:http').Server} server
|
|
169
186
|
* @param {number} port Desired port.
|
|
170
187
|
* @param {number} [attempts] Number of ports tried from `port` on.
|
|
188
|
+
* @param {string} [host] Address to listen on. `0.0.0.0` opens it to the
|
|
189
|
+
* network, which a phone on the same wifi needs.
|
|
171
190
|
* @returns {Promise<number>} The port actually used.
|
|
172
191
|
* @throws {DocPensieveError} When no port is free in the range.
|
|
173
192
|
*/
|
|
174
|
-
export function listen(server, port, attempts = 10) {
|
|
193
|
+
export function listen(server, port, attempts = 10, host = DEFAULT_HOST) {
|
|
175
194
|
return new Promise((resolve, reject) => {
|
|
176
195
|
let current = port;
|
|
177
196
|
|
|
@@ -181,7 +200,7 @@ export function listen(server, port, attempts = 10) {
|
|
|
181
200
|
// the next one beats forcing --port.
|
|
182
201
|
if (error.code === 'EADDRINUSE' && current < port + attempts - 1) {
|
|
183
202
|
current += 1;
|
|
184
|
-
server.listen(current);
|
|
203
|
+
server.listen(current, host);
|
|
185
204
|
return;
|
|
186
205
|
}
|
|
187
206
|
server.off('error', onError);
|
|
@@ -202,6 +221,17 @@ export function listen(server, port, attempts = 10) {
|
|
|
202
221
|
const address = server.address();
|
|
203
222
|
resolve(typeof address === 'object' && address !== null ? address.port : current);
|
|
204
223
|
});
|
|
205
|
-
server.listen(current);
|
|
224
|
+
server.listen(current, host);
|
|
206
225
|
});
|
|
207
226
|
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Said when the site is opened to the network, so that it is never done
|
|
230
|
+
* without the person knowing.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} host
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
export function exposedNotice(host) {
|
|
236
|
+
return `listening on ${host}: anyone on this network can read the site.`;
|
|
237
|
+
}
|
|
@@ -46,6 +46,7 @@ npx docpensieve init my-site --yes --name "My documentation"
|
|
|
46
46
|
| `-t, --theme <framework>` | `tailwind` or `custom` |
|
|
47
47
|
| `-u, --site-url <url>` | Public URL, from which the deployment prefix is derived |
|
|
48
48
|
| `--version-name <version>` | First version, `1.0` for instance |
|
|
49
|
+
| `--translation <code>` | Code of a second language, `fr` for instance |
|
|
49
50
|
| `-y, --yes` | Accepts the defaults without a dialogue |
|
|
50
51
|
| `-f, --force` | Overwrites an existing configuration |
|
|
51
52
|
| `--minimal` | Leaves DocPensieve's documentation out of the site |
|
|
@@ -98,6 +98,10 @@ online.
|
|
|
98
98
|
→ ignores the deployment prefix "/my-project/"
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
Two commands, and the second reads what the first wrote — so they belong
|
|
102
|
+
together. [Deployment](./deployment/) shows how to say that once, in a
|
|
103
|
+
`package.json`, and have a workflow run the same thing you run.
|
|
104
|
+
|
|
101
105
|
It is the check to run after any change of `baseUrl`, of folder structure or of
|
|
102
106
|
URL. When something does not add up, [When it breaks](./when-it-breaks/) lists
|
|
103
107
|
the failures this tool produces, by the symptom you see.
|
|
@@ -88,6 +88,32 @@ the tool through `npx`, which takes the latest published version. To pin it
|
|
|
88
88
|
from one build to the next, declare `docpensieve` in a `package.json` and add
|
|
89
89
|
`npm ci` before the build.
|
|
90
90
|
|
|
91
|
+
## One command, for you and for the machine
|
|
92
|
+
|
|
93
|
+
Two commands are one too many to remember, and a workflow should run exactly
|
|
94
|
+
what you run. A `package.json` is the place to say it once:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"scripts": {
|
|
99
|
+
"docs": "docpensieve dev",
|
|
100
|
+
"check": "docpensieve build && docpensieve check"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`npm run check` then generates the site and reads it back. A dead link, a
|
|
106
|
+
nesting the markup cannot carry, a page that stops the build: each one fails
|
|
107
|
+
the command, on your machine and in your workflow, in the same way and with
|
|
108
|
+
the same message.
|
|
109
|
+
|
|
110
|
+
The `&&` is the point of it. `check` reads what `build` wrote, so a check run
|
|
111
|
+
on its own tells you about the site as it was the last time it was generated —
|
|
112
|
+
which, the day you forget, is the day it matters.
|
|
113
|
+
|
|
114
|
+
With that in place the recipe above becomes `npm ci` and `npm run check`, and
|
|
115
|
+
the version of the tool stops depending on the day the workflow ran.
|
|
116
|
+
|
|
91
117
|
## Reading back before publishing
|
|
92
118
|
|
|
93
119
|
A generated site can compile without error and contain dead links.
|
|
@@ -31,8 +31,23 @@ npx docpensieve check
|
|
|
31
31
|
|
|
32
32
|
## What changes on its own
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
- **`dev` and `serve` listen on this machine only.** They used to listen on
|
|
35
|
+
every interface, so anyone on the same network could read the site being
|
|
36
|
+
written. To look at it from a phone on the same wifi, say so:
|
|
37
|
+
`npx docpensieve dev --host 0.0.0.0`.
|
|
38
|
+
- **Code blocks carry a background of their own.** A block in a language the
|
|
39
|
+
highlighter does not know used to sit on the bare page; it now looks like
|
|
40
|
+
every other block.
|
|
41
|
+
- **An error keeps its hint.** An error the tool raises while a page compiles
|
|
42
|
+
no longer comes out as a generic syntax complaint.
|
|
43
|
+
|
|
44
|
+
## What is worth turning on
|
|
45
|
+
|
|
46
|
+
- [Snippet](../components/snippet/), to show a real file rather than a copy.
|
|
47
|
+
- [Hero](../components/hero/) and [Calendar](../components/calendar/).
|
|
48
|
+
- `copyCode`, a copy button on every code block — off by default, since it is
|
|
49
|
+
what makes a page load a script. See [the reference](../reference/configuration/).
|
|
50
|
+
- `snippetIcons`, if your project installs an icon collection.
|
|
36
51
|
|
|
37
52
|
## What to check afterwards
|
|
38
53
|
|