docpensieve 0.1.2 → 0.1.4

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 CHANGED
@@ -6,16 +6,35 @@ Part of [DocPensieve](https://github.com/Juniors017/docpensieve), a static docum
6
6
  Markdown and MDX in, static HTML out, one version per orphan branch, JSON-LD
7
7
  structured data from the frontmatter.
8
8
 
9
- ## Installation
9
+ ## Getting started
10
+
11
+ Nothing to install first: `npx` fetches the package.
12
+
13
+ ```bash
14
+ npx docpensieve init my-site # asks a few questions, then sets up the project
15
+ cd my-site
16
+ npx docpensieve dev # builds, serves, watches and reloads
17
+ ```
18
+
19
+ ## Installing it in a project
20
+
21
+ To pin the version a project uses — what continuous integration needs:
10
22
 
11
23
  ```bash
24
+ npm init -y # only when the folder has no package.json yet
12
25
  npm install docpensieve
26
+ npx docpensieve init .
13
27
  ```
14
28
 
15
- ## Usage
29
+ `npm install` only installs: it creates no site and asks nothing — `init`
30
+ does. And npm installs in the nearest folder that has a `package.json`, going
31
+ up from the current one: in a folder without one, the package lands in a parent
32
+ folder and nothing appears where you are. Hence `npm init -y` first.
33
+
34
+ ## Commands
16
35
 
17
36
  ```bash
18
- npx docpensieve init # sets up a project and picks the theme
37
+ npx docpensieve init [dir] # sets up a project and picks the theme
19
38
  npx docpensieve dev # builds, serves, watches and reloads
20
39
  npx docpensieve build [ver] # builds every version, or a single one
21
40
  npx docpensieve check # reads the produced site back: links, markup
@@ -32,6 +51,18 @@ The development server reloads the browser after every rebuild, through a
32
51
  script injected **at serving time**: the output of `build` stays free of
33
52
  JavaScript.
34
53
 
54
+ ## Updating
55
+
56
+ ```bash
57
+ npm install docpensieve@latest
58
+ ```
59
+
60
+ Run through `npx` alone, it needs nothing: `npx docpensieve` fetches the
61
+ latest version by itself. A project set up with 0.1.0 can rename
62
+ `docpensieve.config.js` to `docpensieve.config.mjs`, which Node reads as a
63
+ module whatever the `package.json` says: the warning printed on every build
64
+ goes away.
65
+
35
66
  ## Documentation
36
67
 
37
68
  See the [repository](https://github.com/Juniors017/docpensieve#readme).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docpensieve",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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.1.2",
28
- "@docpensieve/core": "0.1.2",
29
- "@docpensieve/shared": "0.1.2",
30
- "@docpensieve/theme": "0.1.2",
27
+ "@docpensieve/components": "0.1.4",
28
+ "@docpensieve/core": "0.1.4",
29
+ "@docpensieve/shared": "0.1.4",
30
+ "@docpensieve/theme": "0.1.4",
31
31
  "chalk": "^6.0.0",
32
32
  "chokidar": "^5.0.0",
33
33
  "commander": "^15.0.0"
@@ -4,11 +4,12 @@
4
4
  * @module docpensieve/commands/dev
5
5
  */
6
6
 
7
+ import { existsSync } from 'node:fs';
7
8
  import path from 'node:path';
8
9
 
9
10
  import { componentsCss, createRegistry, setSiteContext } from '@docpensieve/components';
10
11
  import { SiteGenerator, loadConfig } from '@docpensieve/core';
11
- import { CONFIG_FILENAME, DocPensieveError } from '@docpensieve/shared';
12
+ import { CONFIG_FILENAME, DocPensieveError, THEME_FOLDER } from '@docpensieve/shared';
12
13
  import chokidar from 'chokidar';
13
14
 
14
15
  import { RELOAD_PATH, createStaticServer, listen } from '../server.js';
@@ -20,6 +21,9 @@ const DEFAULT_PORT = 3000;
20
21
  /** Delay for grouping file events, in milliseconds. */
21
22
  const DEBOUNCE = 120;
22
23
 
24
+ /** How often a missing theme folder is looked for, in ms. */
25
+ const THEME_POLL = 1000;
26
+
23
27
  /**
24
28
  * Reload script injected on the fly, never written to disk.
25
29
  *
@@ -79,9 +83,12 @@ export async function dev(options = {}) {
79
83
  const url = `http://localhost:${port}${config.baseUrl}`;
80
84
  console.log(`served at ${url}`);
81
85
 
86
+ // The project's own stylesheets, re-read by every rebuild.
87
+ const themeFolder = path.resolve(cwd, THEME_FOLDER);
82
88
  const watched = [
83
89
  config.configFile ?? path.resolve(cwd, CONFIG_FILENAME),
84
90
  ...config.versions.map((version) => path.resolve(cwd, version.folder)),
91
+ ...(existsSync(themeFolder) ? [themeFolder] : []),
85
92
  ];
86
93
  const watcher = chokidar.watch(watched, { ignoreInitial: true });
87
94
 
@@ -92,7 +99,8 @@ export async function dev(options = {}) {
92
99
 
93
100
  /** @type {NodeJS.Timeout | undefined} */
94
101
  let pending;
95
- watcher.on('all', (_event, changed) => {
102
+ /** @param {string} changed */
103
+ const schedule = (changed) => {
96
104
  // An editor emits several events per save: group them.
97
105
  clearTimeout(pending);
98
106
  pending = setTimeout(async () => {
@@ -111,7 +119,22 @@ export async function dev(options = {}) {
111
119
  }
112
120
  }
113
121
  }, DEBOUNCE);
114
- });
122
+ };
123
+ watcher.on('all', (_event, changed) => schedule(changed));
124
+
125
+ // Handed a path that does not exist, chokidar loses the events of all the
126
+ // others. A missing theme folder is therefore looked for every second, and
127
+ // watched from the moment it appears — its creation itself triggers no
128
+ // event, hence the rebuild asked for here.
129
+ const lookForTheme = existsSync(themeFolder)
130
+ ? undefined
131
+ : setInterval(() => {
132
+ if (!existsSync(themeFolder)) return;
133
+ clearInterval(lookForTheme);
134
+ watcher.add(themeFolder);
135
+ schedule(themeFolder);
136
+ }, THEME_POLL);
137
+ lookForTheme?.unref();
115
138
 
116
139
  console.log('watching — Ctrl+C to stop');
117
140
 
@@ -122,6 +145,7 @@ export async function dev(options = {}) {
122
145
  url,
123
146
  close: async () => {
124
147
  clearTimeout(pending);
148
+ clearInterval(lookForTheme);
125
149
  await watcher.close();
126
150
  // Without this, close() waits for keep-alive connections to expire —
127
151
  // the reload stream keeps one open permanently.
@@ -15,6 +15,7 @@ import {
15
15
  CONFIG_FILENAME,
16
16
  CONFIG_FILENAMES,
17
17
  DocPensieveError,
18
+ THEME_FOLDER,
18
19
  THEME_FRAMEWORKS,
19
20
  } from '@docpensieve/shared';
20
21
 
@@ -24,7 +25,7 @@ import {
24
25
  */
25
26
  const FRAMEWORK_LABELS = {
26
27
  tailwind: 'Tailwind CSS — ships with the tool, nothing to install',
27
- custom: 'custom theme, light stylesheet, no utilities',
28
+ custom: 'custom theme, light stylesheet, your own classes in theme/',
28
29
  };
29
30
 
30
31
  /** Answers used when there is no dialogue. */
@@ -39,10 +40,20 @@ const DEFAULTS = { name: 'My documentation', siteUrl: '', theme: 'tailwind', ver
39
40
  const DOCS_FOLDER = '99-docpensieve';
40
41
 
41
42
  /**
42
- * Entries of DocPensieve's documentation that are not installed: its home page
43
- * and the icons only that page uses belong to DocPensieve's own site.
43
+ * Stylesheet of the documentation's examples, shipped next to its pages.
44
+ * Under the custom theme, it goes to the theme folder, not among the pages.
44
45
  */
45
- const NOT_INSTALLED = new Set(['index.md', 'index.mdx', 'icons']);
46
+ const EXAMPLES_CSS = 'examples.css';
47
+
48
+ /**
49
+ * Entries of DocPensieve's documentation that are not installed with the
50
+ * pages: its home page and the icons only that page uses belong to
51
+ * DocPensieve's own site, and the examples' stylesheet has a place of its own.
52
+ */
53
+ const NOT_INSTALLED = new Set(['index.md', 'index.mdx', 'icons', EXAMPLES_CSS]);
54
+
55
+ /** Starting point of the project's own stylesheet, under the custom theme. */
56
+ const CUSTOM_CSS = fileURLToPath(new URL('../templates/custom.css', import.meta.url));
46
57
 
47
58
  /** Where the generated configuration sends readers for every field. */
48
59
  const DOCUMENTATION_URL = 'https://juniors017.github.io/docpensieve/';
@@ -127,9 +138,19 @@ async function collect(options) {
127
138
  docs: !options.minimal,
128
139
  };
129
140
 
141
+ if (options.yes) return fromOptions;
142
+
130
143
  // Without a terminal — script, CI, pipe — the dialogue would never complete:
131
- // stick to the options and the defaults.
132
- if (options.yes || !process.stdin.isTTY) return fromOptions;
144
+ // stick to the options and the defaults. But say so: some terminals run
145
+ // programs without handing them one, and the questions used to vanish
146
+ // without a word, the defaults going unnoticed.
147
+ if (!process.stdin.isTTY) {
148
+ console.log('No interactive terminal: no questions asked, the options and defaults apply.');
149
+ console.log(
150
+ 'To choose, pass --name, --site-url, --theme, --version-name or --minimal; --yes silences this notice.',
151
+ );
152
+ return fromOptions;
153
+ }
133
154
 
134
155
  const rl = createInterface({ input: process.stdin, output: process.stdout });
135
156
  try {
@@ -215,6 +236,9 @@ function renderConfig({ name, theme, siteUrl, version }) {
215
236
  ' // CSS appended to the stylesheet, outside any layer: it wins over the',
216
237
  ' // default rules.',
217
238
  " // css: '.dp-article h2 { letter-spacing: -0.01em; }',",
239
+ '',
240
+ ' // Longer rules go in the theme/ folder: every .css file in it is',
241
+ ' // appended after the theme, and "docpensieve dev" picks up changes.',
218
242
  ...(theme === 'tailwind'
219
243
  ? [
220
244
  '',
@@ -423,6 +447,17 @@ export async function init(dir = '.', options = {}) {
423
447
  hint: 'Reinstall docpensieve, or run init with --minimal to go without it.',
424
448
  });
425
449
  }
450
+ // Under the custom theme, its examples also need their stylesheet: without
451
+ // it, every one of them would render unstyled, and nothing would say why.
452
+ if (
453
+ documentation &&
454
+ answers.theme === 'custom' &&
455
+ !existsSync(path.join(documentation, EXAMPLES_CSS))
456
+ ) {
457
+ throw new DocPensieveError("The stylesheet of the documentation's examples is missing.", {
458
+ hint: 'Reinstall docpensieve, or run init with --minimal to go without the documentation.',
459
+ });
460
+ }
426
461
 
427
462
  const slug = versionSlug(answers.version);
428
463
  const docsDir = path.join(target, 'docs', slug);
@@ -438,6 +473,8 @@ export async function init(dir = '.', options = {}) {
438
473
  if (documentation) {
439
474
  await installDocumentation(documentation, path.join(docsDir, DOCS_FOLDER), slug);
440
475
  }
476
+ const stylesheets =
477
+ answers.theme === 'custom' ? await writeStylesheets(target, documentation) : [];
441
478
  await ignoreOutput(target);
442
479
 
443
480
  console.log(`\nProject initialised in ${target}`);
@@ -447,12 +484,41 @@ export async function init(dir = '.', options = {}) {
447
484
  if (documentation) {
448
485
  console.log(` docs/${slug}/${DOCS_FOLDER}/ DocPensieve's documentation, to delete when done`);
449
486
  }
487
+ for (const line of stylesheets) console.log(` ${line}`);
450
488
  console.log(`\nTheme: ${answers.theme} — ${FRAMEWORK_LABELS[answers.theme]}`);
451
489
  console.log('\nNext: npx docpensieve dev');
452
490
 
453
491
  return { dir: target, theme: answers.theme, docs: answers.docs };
454
492
  }
455
493
 
494
+ /**
495
+ * Gives the custom theme its stylesheets, in the project's theme folder.
496
+ *
497
+ * The custom theme loads no utility framework: the classes a page uses are
498
+ * defined by the project. `custom.css` is where they go — never overwritten,
499
+ * even with `--force`, since it holds the project's own rules. The installed
500
+ * documentation brings the classes of its examples in a file of its own, to
501
+ * delete along with it.
502
+ *
503
+ * @param {string} target Project folder.
504
+ * @param {string | null} documentation Source of the installed documentation.
505
+ * @returns {Promise<string[]>} One line per file, for the summary.
506
+ */
507
+ async function writeStylesheets(target, documentation) {
508
+ const folder = path.join(target, THEME_FOLDER);
509
+ await mkdir(folder, { recursive: true });
510
+
511
+ const own = path.join(folder, 'custom.css');
512
+ if (!existsSync(own)) cpSync(CUSTOM_CSS, own);
513
+ const lines = [`${THEME_FOLDER}/custom.css your own styles`];
514
+
515
+ if (documentation) {
516
+ cpSync(path.join(documentation, EXAMPLES_CSS), path.join(folder, `${DOCS_FOLDER}.css`));
517
+ lines.push(`${THEME_FOLDER}/${DOCS_FOLDER}.css classes of its examples, to delete with it`);
518
+ }
519
+ return lines;
520
+ }
521
+
456
522
  /**
457
523
  * Adds the output folder to .gitignore, without overwriting what is there.
458
524
  *
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Your own styles.
3
+ *
4
+ * Every .css file of this theme/ folder is appended to the site's stylesheet,
5
+ * after the theme's, in name order, and "docpensieve dev" picks up every
6
+ * change. The rules sit outside any layer: they win over the default look of
7
+ * the components.
8
+ *
9
+ * The custom theme loads no utility framework: a className written in a page
10
+ * names a class defined here. Write the colours on the --dp-* tokens
11
+ * (--dp-accent, --dp-accent-soft, --dp-bg, --dp-text, --dp-text-soft,
12
+ * --dp-border…) so that they follow the light and dark palettes.
13
+ *
14
+ * For instance, for <Card className="accent-card">:
15
+ *
16
+ * .accent-card {
17
+ * border-color: var(--dp-accent);
18
+ * background: var(--dp-accent-soft);
19
+ * }
20
+ */
package/src/theme.js CHANGED
@@ -4,18 +4,39 @@
4
4
  * @module docpensieve/theme
5
5
  */
6
6
 
7
- import { setThemeClasses } from '@docpensieve/components';
8
- import { ConfigError, THEME_FRAMEWORKS } from '@docpensieve/shared';
7
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ import { setThemeClasses, setThemeFramework } from '@docpensieve/components';
11
+ import { ConfigError, THEME_FOLDER, THEME_FRAMEWORKS } from '@docpensieve/shared';
9
12
  import { CustomProvider, TailwindProvider, ThemeEngine } from '@docpensieve/theme';
10
13
 
14
+ /**
15
+ * Reads the project's own stylesheets: every `.css` file of its `theme/`
16
+ * folder, in name order. None when the folder does not exist.
17
+ *
18
+ * @param {string} [rootDir] Project root.
19
+ * @returns {string[]} The contents of each file.
20
+ */
21
+ export function projectCss(rootDir) {
22
+ const folder = rootDir ? path.join(rootDir, THEME_FOLDER) : '';
23
+ if (!folder || !existsSync(folder)) return [];
24
+ return readdirSync(folder)
25
+ .filter((name) => name.toLowerCase().endsWith('.css'))
26
+ .sort()
27
+ .map((name) => readFileSync(path.join(folder, name), 'utf8').trim());
28
+ }
29
+
11
30
  /**
12
31
  * Mounts the ThemeEngine matching the declared framework.
13
32
  *
14
33
  * The CLI does this wiring: `core` deliberately ignores the `theme` package
15
34
  * and receives the engine by injection (ADR-002).
16
35
  *
17
- * Along the way, the class table is handed to the components: that is what
18
- * lets them ask for their class instead of hard-coding it (ADR-007).
36
+ * Along the way, the class table and the framework are handed to the
37
+ * components: that is what lets them ask for their class instead of
38
+ * hard-coding it (ADR-007), and lets `ForTheme` keep the variant of the active
39
+ * theme.
19
40
  *
20
41
  * @param {Record<string, any>} config Normalised configuration.
21
42
  * @param {string} [extraCss] CSS appended after the theme's — the look of the
@@ -27,16 +48,18 @@ export function createTheme(config, extraCss = '') {
27
48
  const theme = config.theme ?? {};
28
49
  const framework = theme.framework ?? 'tailwind';
29
50
 
30
- // Components first, project CSS second: the latter must be able to correct
31
- // them.
51
+ // Components first, project CSS after: the latter must be able to correct
52
+ // them. The theme folder comes last — the project's final word, and the
53
+ // only place the custom theme finds the classes a page uses.
32
54
  const options = {
33
55
  tokens: theme.tokens,
34
- css: [extraCss, theme.css].filter(Boolean).join('\n\n'),
56
+ css: [extraCss, theme.css, ...projectCss(config.rootDir)].filter(Boolean).join('\n\n'),
35
57
  };
36
58
 
37
59
  /** @param {ThemeEngine} engine */
38
60
  const mount = (engine) => {
39
61
  setThemeClasses(engine.classes);
62
+ setThemeFramework(framework);
40
63
  return engine;
41
64
  };
42
65
 
@@ -26,7 +26,9 @@ cd my-site
26
26
 
27
27
  The command creates the folder and writes a configuration, a first
28
28
  documentation folder and a home page into it. It asks a few questions; `--yes`
29
- skips them and accepts the defaults.
29
+ skips them and accepts the defaults. Where it cannot ask — a script, continuous
30
+ integration, or a terminal that gives the programs it runs no interactive
31
+ input — it says so and sticks to the options and the defaults.
30
32
 
31
33
  It also installs this very documentation, in a **DocPensieve** section at the
32
34
  end of the new site's menu. It matches the version you installed, and its
@@ -48,27 +50,55 @@ npx docpensieve init my-site --yes --name "My documentation"
48
50
  | `-f, --force` | Overwrites an existing configuration |
49
51
  | `--minimal` | Leaves DocPensieve's documentation out of the site |
50
52
 
51
- ## In an existing project
53
+ ## Installing it in a project
54
+
55
+ Through `npx` alone, the version used is the latest. To pin the one a project
56
+ uses — what continuous integration needs — install it in the project:
52
57
 
53
58
  ```bash
59
+ npm init -y # only when the folder has no package.json yet
54
60
  npm install docpensieve
55
- npx docpensieve init . --force
61
+ npx docpensieve init .
56
62
  ```
57
63
 
58
- `init` on an occupied folder refuses to overwrite an existing configuration:
59
- you have to ask for it with `--force`. The refusal is deliberate a
64
+ `npm install` only installs: it creates no site and asks nothing — `init` does.
65
+ It also installs in the nearest folder that has a `package.json`, going up from
66
+ the current one: in a folder without one, the package lands in a parent folder
67
+ and nothing appears where you are. Hence `npm init -y` first. `npx docpensieve`
68
+ then runs the installed copy.
69
+
70
+ `init` refuses to overwrite an existing configuration: you have to ask for it
71
+ with `--force`. The refusal is deliberate — a
60
72
  configuration overwritten by mistake is only noticed at the next deployment.
61
73
 
62
74
  ## Choosing the theme
63
75
 
64
76
  `tailwind` is the default and installs Tailwind as a dependency. `custom` does
65
77
  without it entirely: the site is then styled by a stylesheet written in the
66
- package, with no styling dependency.
78
+ package, with no styling dependency. Your own classes then go in the `theme/`
79
+ folder, which `init` starts with `theme/custom.css`.
67
80
 
68
81
  Both are equivalent in use — the templates are the same, only the styling
69
82
  changes. The choice is not final: it fits in one field of the configuration,
70
83
  described in [Themes](./themes/).
71
84
 
85
+ ## Updating
86
+
87
+ ```bash
88
+ npm install docpensieve@latest
89
+ ```
90
+
91
+ Run through `npx` alone, DocPensieve needs nothing: `npx docpensieve` fetches
92
+ the latest version by itself.
93
+
94
+ A project set up with 0.1.0 has a `docpensieve.config.js`. It still works, but
95
+ Node reads it as a module only when the `package.json` says so, and warns on
96
+ every build otherwise: rename it `docpensieve.config.mjs`.
97
+
98
+ The documentation `init` installed in `99-docpensieve` stays at the version it
99
+ came with. To refresh it, run `init` in a scratch folder and copy that folder
100
+ over.
101
+
72
102
  ## Checking
73
103
 
74
104
  ```bash
@@ -126,8 +126,8 @@ Three ways to guard against it:
126
126
 
127
127
  - prefer `<div>` to `<p>` as a wrapper — a paragraph is valid inside it;
128
128
  - write short content on the same line as its tags;
129
- - for repeated cases, put a class in `theme.css` rather than a long string of
130
- utilities, so that the line stays short.
129
+ - for repeated cases, put a class in the `theme/` folder rather than a long
130
+ string of utilities, so that the line stays short.
131
131
 
132
132
  `docpensieve check` reports these nestings on the produced site.
133
133
 
@@ -100,9 +100,11 @@ Without this layer, a component rule written after a utility would beat it at
100
100
  equal specificity, and the author's `className` would be ignored without a
101
101
  word.
102
102
 
103
- The `custom` theme has no utilities: a `className` there designates your own
104
- classes. Declare them in `theme.css` outside any layer, they come before the
105
- component rules.
103
+ The `custom` theme has no utilities: a `className` there names classes of your
104
+ own. Write them in the `theme/` folder, at the root of the project: every `.css`
105
+ file in it is appended to the stylesheet, outside any layer, so they come
106
+ before the component rules. Under this theme, `init` starts the folder with
107
+ `theme/custom.css`.
106
108
 
107
109
  ## The stylesheet is compiled last
108
110