describe-me 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +58 -44
  2. package/dist-cli/build-live-preview.d.ts +6 -0
  3. package/dist-cli/build-live-preview.js +62 -0
  4. package/dist-cli/code-of.d.ts +2 -0
  5. package/dist-cli/code-of.js +4 -0
  6. package/dist-cli/import-build-live.d.ts +27 -0
  7. package/dist-cli/import-build-live.js +20 -0
  8. package/dist-cli/import-create-live-server.d.ts +20 -0
  9. package/dist-cli/import-create-live-server.js +20 -0
  10. package/dist-cli/live-consequences.d.ts +8 -0
  11. package/dist-cli/live-consequences.js +8 -0
  12. package/dist-cli/live-predates.d.ts +2 -0
  13. package/dist-cli/live-predates.js +4 -0
  14. package/dist-cli/live-preview-endpoint.d.ts +8 -0
  15. package/dist-cli/live-preview-endpoint.js +26 -0
  16. package/dist-cli/live-preview-info.d.ts +12 -0
  17. package/dist-cli/live-preview-info.js +1 -0
  18. package/dist-cli/live-project.d.ts +14 -0
  19. package/dist-cli/live-project.js +31 -0
  20. package/dist-cli/main.js +8 -4
  21. package/dist-cli/parse-args.d.ts +13 -2
  22. package/dist-cli/parse-args.js +9 -3
  23. package/dist-cli/reason-of.d.ts +5 -0
  24. package/dist-cli/reason-of.js +13 -0
  25. package/dist-cli/resolve-live-entry.d.ts +5 -0
  26. package/dist-cli/resolve-live-entry.js +42 -0
  27. package/dist-cli/run-build.d.ts +7 -1
  28. package/dist-cli/run-build.js +8 -1
  29. package/dist-cli/run-dev.d.ts +13 -2
  30. package/dist-cli/run-dev.js +26 -5
  31. package/dist-cli/start-live-preview.d.ts +7 -0
  32. package/dist-cli/start-live-preview.js +34 -0
  33. package/package.json +7 -4
  34. package/src/apply-viewport.ts +21 -3
  35. package/src/inspector.ts +35 -22
  36. package/src/keyboard.ts +42 -9
  37. package/src/listen-to-live.ts +128 -0
  38. package/src/live-iframe.ts +6 -0
  39. package/src/live-section.ts +84 -0
  40. package/src/live-session.ts +35 -0
  41. package/src/live-shown.ts +10 -0
  42. package/src/live-src.ts +32 -0
  43. package/src/live-status-words.ts +15 -0
  44. package/src/load-live-preview.ts +80 -0
  45. package/src/main-panel.ts +5 -2
  46. package/src/main.ts +6 -0
  47. package/src/paint-live.ts +63 -0
  48. package/src/render-frame.ts +8 -5
  49. package/src/reset-live-session.ts +12 -0
  50. package/src/stage.ts +24 -4
  51. package/src/state-hash.ts +5 -1
  52. package/src/state.ts +8 -0
  53. package/src/states-gallery.ts +6 -2
  54. package/src/style.css +107 -0
  55. package/src/toggle-live.ts +16 -0
  56. package/src/viewport-controls.ts +37 -4
package/README.md CHANGED
@@ -1,59 +1,73 @@
1
1
  # describe-me
2
2
 
3
- The viewer and CLI of [describe-me](https://github.com/grzehub/describe-me):
4
- living component documentation generated from the Vitest tests you already have.
5
- `describe` blocks become the sidebar, each `it` is a story, each step is a frame
6
- you can scrub through.
3
+ **Your tests are your stories.** The viewer and the `describe-me` CLI of
4
+ describe-me: living component documentation generated from the Vitest tests
5
+ you already have.
7
6
 
8
- It reads the `.describe-me/` directory that `@describe-me/vitest`'s reporter
9
- writes, and replays the snapshots in a sandboxed iframe.
7
+ **[Read the docs](https://grzehub.github.io/describe-me/)** · open the viewer
8
+ of the [browser-mode example](https://grzehub.github.io/describe-me/examples/react-browser/)
9
+ or the [jsdom example](https://grzehub.github.io/describe-me/examples/react-jsdom/)
10
+ · [Limitations](https://grzehub.github.io/describe-me/limitations.html)
10
11
 
11
- ## Install
12
+ describe-me captures the DOM after each step of a test and shows the result as
13
+ a browsable catalog. `describe` blocks become the sidebar, each `it` is a
14
+ story, and each step is a frame you can scrub through. Stories cannot rot,
15
+ because they are tests. If one drifts from the component, CI goes red.
16
+
17
+ Status: prototype. React 18 and 19, Vitest 4 and 5 in browser mode or jsdom,
18
+ recorded frames and Live in the viewer.
19
+
20
+ ## Which package do I need
21
+
22
+ | Package | What it is | Install it |
23
+ | --------------------- | ------------------------------------------- | ----------------------- |
24
+ | `describe-me` | the viewer and the `describe-me` CLI (this) | yes |
25
+ | `@describe-me/vitest` | the Vitest plugin, setup files and reporter | yes |
26
+ | `@describe-me/react` | `render` functions that record frames | yes |
27
+ | `@describe-me/core` | the recorder and the manifest types | no, the others bring it |
28
+
29
+ ## Quick start
12
30
 
13
31
  ```sh
14
32
  pnpm add -D describe-me @describe-me/vitest @describe-me/react
15
33
  ```
16
34
 
17
- `vite` is a peer dependency (Vitest requires it anyway): pnpm and npm install
18
- it automatically; with yarn, add `vite` to your `devDependencies`.
35
+ ```ts
36
+ // vitest.config.ts
37
+ import { describeMe } from '@describe-me/vitest/plugin'
38
+
39
+ export default defineConfig({
40
+ plugins: [react(), describeMe()],
41
+ test: { environment: 'jsdom' }, // or your browser-mode settings
42
+ })
43
+ ```
19
44
 
20
- All describe-me packages must be on the same version
21
- ([One version for all packages](https://grzehub.github.io/describe-me/installation.html#lockstep)).
45
+ Run your tests, then `describe-me dev` opens the viewer.
46
+ [Getting started](https://grzehub.github.io/describe-me/getting-started.html)
47
+ walks through both environments. Keep all describe-me packages on one version
48
+ ([why](https://grzehub.github.io/describe-me/installation.html#lockstep)).
22
49
 
23
- ## Usage
50
+ ## This package
24
51
 
25
52
  ```sh
26
- describe-me dev # viewer on http://localhost:6006, live while vitest --watch runs
27
- describe-me build --out docs # self-contained static site: viewer + __data/
53
+ describe-me dev # the viewer, repainted while vitest --watch runs
54
+ describe-me build --out docs # a static site: the viewer and its data
28
55
  ```
29
56
 
30
- | Flag | Default | Meaning |
31
- | ------------------- | -------------------- | ------------------------------------- |
32
- | `--data` | `.describe-me` | The directory the reporter wrote. |
33
- | `--out` | `describe-me-dist` | Output directory for `build`. |
34
- | `--port` | `6006` | Port for `dev`. |
35
- | `--no-vendor-fonts` | fonts are downloaded | `build`: keep web fonts on their CDN. |
36
-
37
- The viewer has search, collapsible suites, keyboard navigation, custom
38
- viewport sizes and shareable links
39
- ([Viewer and CLI](https://grzehub.github.io/describe-me/viewer.html)).
40
-
41
- `build` downloads the web fonts that the preview head and the snapshots load
42
- from Google Fonts, Bunny Fonts and Fontsource on jsDelivr into the site, so
43
- the docs show them offline. Adobe Fonts stay remote, because their license
44
- does not allow self-hosting, and so do fonts on any other host. The build
45
- lists both.
46
-
47
- This needs the network. By default `build` connects to Google Fonts, Bunny
48
- Fonts and jsDelivr when the data loads fonts from them and the cache has no
49
- fresh copy. Downloads are cached in `node_modules/.cache/describe-me/fonts`,
50
- so after one online build later builds run offline. An unreachable host costs
51
- up to about 10 s once, then it is skipped. A font that does not download keeps
52
- loading from the network, and the build never fails because of it.
53
- `--no-vendor-fonts` keeps the build off the network, for CI without network
54
- access or with privacy rules.
55
-
56
- The static site uses relative URLs, so it works from a sub-path such as
57
- GitHub Pages.
58
-
59
- See the [docs](https://grzehub.github.io/describe-me/) for the full picture.
57
+ `describe-me build --live` adds Live to the site. Read
58
+ [what such a site contains](https://grzehub.github.io/describe-me/viewer.html#flag-live)
59
+ before you publish it.
60
+
61
+ `build` writes a site with relative URLs, so it works under a sub-path such as
62
+ GitHub Pages. It needs a web server and does not open from disk. It also
63
+ downloads web fonts from Google Fonts, Bunny Fonts and Fontsource into the
64
+ site, so it shows them offline.
65
+ [Viewer and CLI](https://grzehub.github.io/describe-me/viewer.html) lists every
66
+ flag and what the viewer can do.
67
+
68
+ `vite` is a peer dependency, which pnpm and npm install on their own. With
69
+ yarn, add it to your `devDependencies`.
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Builds the live preview of the manifest's tests into `<out>/__live/` and
3
+ * writes `__live.json` next to the viewer, so the static site shows Live.
4
+ * On failure it leaves neither behind and throws the reason.
5
+ */
6
+ export declare function buildLivePreview(data: string, out: string, root: string | undefined): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { LIVE_PROTOCOL } from '@describe-me/core/types';
4
+ import { codeOf } from './code-of.js';
5
+ import { importBuildLive } from './import-build-live.js';
6
+ import { LIVE_CONSEQUENCES } from './live-consequences.js';
7
+ import { liveProject } from './live-project.js';
8
+ import { reasonOf } from './reason-of.js';
9
+ const STOPPED = LIVE_CONSEQUENCES.build;
10
+ // The code `buildLive()` rejects with when the project's config sets `live: false`.
11
+ const LIVE_OFF = 'DESCRIBE_ME_LIVE_OFF';
12
+ const READY = { status: 'ready', base: '__live/' };
13
+ /** The module ids of the manifest. Even with `--root` the build needs them, as its list of tests. */
14
+ function manifestTestFiles(data) {
15
+ const file = join(resolve(data), 'manifest.json');
16
+ if (!existsSync(file)) {
17
+ throw new Error(`${file} is missing, ${STOPPED}. Run the tests first, because the manifest lists the tests to build.`);
18
+ }
19
+ try {
20
+ const manifest = JSON.parse(readFileSync(file, 'utf8'));
21
+ return manifest.modules.map((module) => module.id);
22
+ }
23
+ catch (error) {
24
+ throw new Error(`cannot read ${file} (${reasonOf(error)}), ${STOPPED}.`);
25
+ }
26
+ }
27
+ /** The reason for an error of `buildLive()`, which knows nothing of the CLI. */
28
+ function buildFailure(error) {
29
+ if (codeOf(error) === LIVE_OFF) {
30
+ return new Error(`${reasonOf(error)}, but --live asks for it, ${STOPPED}. Remove one of the two.`);
31
+ }
32
+ return new Error(`the live build failed: ${reasonOf(error)}`);
33
+ }
34
+ async function buildProject(project, outDir, testFiles) {
35
+ const buildLive = await importBuildLive(project.root);
36
+ const built = await buildLive({ ...project, outDir, testFiles }).catch((error) => {
37
+ throw buildFailure(error);
38
+ });
39
+ if (built.protocol !== LIVE_PROTOCOL) {
40
+ throw new Error(`all describe-me packages must be on the same version. The live build in ${project.root} speaks protocol ${built.protocol}, this viewer ${LIVE_PROTOCOL}, ${STOPPED}.`);
41
+ }
42
+ }
43
+ /**
44
+ * Builds the live preview of the manifest's tests into `<out>/__live/` and
45
+ * writes `__live.json` next to the viewer, so the static site shows Live.
46
+ * On failure it leaves neither behind and throws the reason.
47
+ */
48
+ export async function buildLivePreview(data, out, root) {
49
+ const outDir = join(resolve(out), '__live');
50
+ let testFiles;
51
+ try {
52
+ testFiles = manifestTestFiles(data);
53
+ await buildProject(liveProject(data, root, STOPPED), outDir, testFiles);
54
+ }
55
+ catch (error) {
56
+ rmSync(outDir, { recursive: true, force: true });
57
+ throw new Error(reasonOf(error), { cause: error });
58
+ }
59
+ const files = testFiles.length === 1 ? '1 test file' : `${testFiles.length} test files`;
60
+ writeFileSync(join(resolve(out), '__live.json'), JSON.stringify(READY));
61
+ process.stdout.write(` ➜ Live preview: ${join(out, '__live')}/ (${files})\n`);
62
+ }
@@ -0,0 +1,2 @@
1
+ /** The `code` of an error, such as `ERR_PACKAGE_PATH_NOT_EXPORTED`, or undefined. */
2
+ export declare function codeOf(error: unknown): unknown;
@@ -0,0 +1,4 @@
1
+ /** The `code` of an error, such as `ERR_PACKAGE_PATH_NOT_EXPORTED`, or undefined. */
2
+ export function codeOf(error) {
3
+ return typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
4
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * What the CLI passes to `buildLive()` from `@describe-me/vitest/live`.
3
+ * Declared here, because `describe-me` does not depend on `@describe-me/vitest`.
4
+ */
5
+ export interface LivePreviewBuildOptions {
6
+ /** The project root. */
7
+ root: string;
8
+ /** The Vitest config file, relative to `root`. Absent: the one Vitest picks there. */
9
+ configFile?: string;
10
+ /** Where the live page goes. */
11
+ outDir: string;
12
+ /** The module ids of the manifest. */
13
+ testFiles: string[];
14
+ }
15
+ /** The part of a finished live build that the CLI uses. */
16
+ export interface LivePreviewBuild {
17
+ /** The live protocol of the project's describeMe() plugin. */
18
+ protocol: number;
19
+ }
20
+ /** `buildLive()` as the CLI calls it. */
21
+ export type BuildLive = (options: LivePreviewBuildOptions) => Promise<LivePreviewBuild>;
22
+ /**
23
+ * `buildLive()` from the project's own `@describe-me/vitest/live`, so the
24
+ * build runs the plugin the tests ran with. Throws the reason when the
25
+ * project has none.
26
+ */
27
+ export declare function importBuildLive(root: string): Promise<BuildLive>;
@@ -0,0 +1,20 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { LIVE_CONSEQUENCES } from './live-consequences.js';
3
+ import { livePredates } from './live-predates.js';
4
+ import { resolveLiveEntry } from './resolve-live-entry.js';
5
+ /**
6
+ * `buildLive()` from the project's own `@describe-me/vitest/live`, so the
7
+ * build runs the plugin the tests ran with. Throws the reason when the
8
+ * project has none.
9
+ */
10
+ export async function importBuildLive(root) {
11
+ const entry = resolveLiveEntry(root, LIVE_CONSEQUENCES.build);
12
+ const exported = await import(pathToFileURL(entry).href);
13
+ const build = typeof exported === 'object' && exported !== null && 'buildLive' in exported
14
+ ? exported.buildLive
15
+ : undefined;
16
+ if (typeof build !== 'function') {
17
+ throw livePredates(root, 'buildLive()', LIVE_CONSEQUENCES.build);
18
+ }
19
+ return build;
20
+ }
@@ -0,0 +1,20 @@
1
+ import type { LiveProject } from './live-project.js';
2
+ /**
3
+ * The part of the server from `@describe-me/vitest/live` that the CLI uses.
4
+ * Declared here, because `describe-me` does not depend on `@describe-me/vitest`.
5
+ */
6
+ export interface LivePreviewServer {
7
+ /** The URL the live page is under. */
8
+ base: string;
9
+ /** The live protocol of the project's describeMe() plugin. */
10
+ protocol: number;
11
+ close(): Promise<void>;
12
+ }
13
+ /** `createLiveServer()` as the CLI calls it. */
14
+ export type CreateLiveServer = (project: LiveProject) => Promise<LivePreviewServer>;
15
+ /**
16
+ * `createLiveServer()` from the project's own `@describe-me/vitest/live`, so
17
+ * the preview runs the plugin the tests ran with. Throws the reason when the
18
+ * project has none.
19
+ */
20
+ export declare function importCreateLiveServer(root: string): Promise<CreateLiveServer>;
@@ -0,0 +1,20 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { LIVE_CONSEQUENCES } from './live-consequences.js';
3
+ import { livePredates } from './live-predates.js';
4
+ import { resolveLiveEntry } from './resolve-live-entry.js';
5
+ /**
6
+ * `createLiveServer()` from the project's own `@describe-me/vitest/live`, so
7
+ * the preview runs the plugin the tests ran with. Throws the reason when the
8
+ * project has none.
9
+ */
10
+ export async function importCreateLiveServer(root) {
11
+ const entry = resolveLiveEntry(root, LIVE_CONSEQUENCES.dev);
12
+ const exported = await import(pathToFileURL(entry).href);
13
+ const create = typeof exported === 'object' && exported !== null && 'createLiveServer' in exported
14
+ ? exported.createLiveServer
15
+ : undefined;
16
+ if (typeof create !== 'function') {
17
+ throw livePredates(root, 'the live preview', LIVE_CONSEQUENCES.dev);
18
+ }
19
+ return create;
20
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * How a reason goes on when the live preview cannot start or build: what
3
+ * that means for the command, so the same reason fits `dev` and `build --live`.
4
+ */
5
+ export declare const LIVE_CONSEQUENCES: {
6
+ readonly dev: "so the viewer runs without Live";
7
+ readonly build: "so describe-me build --live stopped";
8
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * How a reason goes on when the live preview cannot start or build: what
3
+ * that means for the command, so the same reason fits `dev` and `build --live`.
4
+ */
5
+ export const LIVE_CONSEQUENCES = {
6
+ dev: 'so the viewer runs without Live',
7
+ build: 'so describe-me build --live stopped',
8
+ };
@@ -0,0 +1,2 @@
1
+ /** The error for a project whose `@describe-me/vitest` is older than `feature`. */
2
+ export declare function livePredates(root: string, feature: string, consequence: string): Error;
@@ -0,0 +1,4 @@
1
+ /** The error for a project whose `@describe-me/vitest` is older than `feature`. */
2
+ export function livePredates(root, feature, consequence) {
3
+ return new Error(`the @describe-me/vitest in ${root} predates ${feature}, ${consequence}.`);
4
+ }
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { LivePreviewInfo } from './live-preview-info.js';
3
+ /**
4
+ * Answers `GET` and `HEAD /__live.json` on the viewer's dev server with what
5
+ * `info()` says at that moment. It is an inline plugin of the CLI, because
6
+ * the viewer's `vite.config.ts` cannot see the CLI's state.
7
+ */
8
+ export declare function livePreviewEndpoint(info: () => LivePreviewInfo): Plugin;
@@ -0,0 +1,26 @@
1
+ const PATH = '/__live.json';
2
+ /**
3
+ * Answers `GET` and `HEAD /__live.json` on the viewer's dev server with what
4
+ * `info()` says at that moment. It is an inline plugin of the CLI, because
5
+ * the viewer's `vite.config.ts` cannot see the CLI's state.
6
+ */
7
+ export function livePreviewEndpoint(info) {
8
+ return {
9
+ name: 'describe-me:live-preview',
10
+ configureServer(server) {
11
+ server.middlewares.use((request, response, next) => {
12
+ const path = (request.url ?? '').split('?')[0];
13
+ const readable = request.method === 'GET' || request.method === 'HEAD';
14
+ if (path !== PATH || !readable) {
15
+ next();
16
+ return;
17
+ }
18
+ const body = JSON.stringify(info());
19
+ response.setHeader('Content-Type', 'application/json');
20
+ response.setHeader('Cache-Control', 'no-store');
21
+ response.setHeader('Content-Length', Buffer.byteLength(body));
22
+ response.end(request.method === 'HEAD' ? undefined : body);
23
+ });
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,12 @@
1
+ /** What `__live.json` next to the viewer says about the live preview. */
2
+ export interface LivePreviewInfo {
3
+ status: 'starting' | 'ready' | 'off' | 'error';
4
+ /**
5
+ * The URL `live.html` is under, relative to `__live.json` or absolute:
6
+ * `__live/` on a static site, `http://localhost:5173/__describe-me/` under
7
+ * `describe-me dev`. Only with `ready`.
8
+ */
9
+ base?: string;
10
+ /** Why the live preview did not start. Only with `error`. */
11
+ error?: string;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ /** The project the live preview runs, as `createLiveServer()` and `buildLive()` take it. */
2
+ export interface LiveProject {
3
+ /** The project root. */
4
+ root: string;
5
+ /** The Vitest config file, relative to `root`. Absent: the one Vitest picks there. */
6
+ configFile?: string;
7
+ }
8
+ /**
9
+ * The project of the live preview. `--root` replaces both the root and the
10
+ * config file of the manifest, so the preview uses the config Vitest picks in
11
+ * that directory. Throws the reason, followed by `consequence`, when there is
12
+ * neither.
13
+ */
14
+ export declare function liveProject(data: string, root: string | undefined, consequence: string): LiveProject;
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ /**
4
+ * The project of the live preview. `--root` replaces both the root and the
5
+ * config file of the manifest, so the preview uses the config Vitest picks in
6
+ * that directory. Throws the reason, followed by `consequence`, when there is
7
+ * neither.
8
+ */
9
+ export function liveProject(data, root, consequence) {
10
+ if (root !== undefined) {
11
+ return { root: resolve(root) };
12
+ }
13
+ const dir = resolve(data);
14
+ const file = join(dir, 'manifest.json');
15
+ if (!existsSync(file)) {
16
+ throw new Error(`no manifest in ${dir} to find the project by, ${consequence}. Run the tests and start again, or pass --root.`);
17
+ }
18
+ let manifest;
19
+ try {
20
+ manifest = JSON.parse(readFileSync(file, 'utf8'));
21
+ }
22
+ catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ throw new Error(`cannot read ${file} (${message}), ${consequence}.`);
25
+ }
26
+ const { root: projectRoot, configFile } = manifest;
27
+ if (configFile !== undefined && !existsSync(resolve(projectRoot, configFile))) {
28
+ throw new Error(`the manifest names the config file ${configFile}, which is not in ${projectRoot}, ${consequence}. Run the tests again, or pass --root.`);
29
+ }
30
+ return { root: projectRoot, configFile };
31
+ }
package/dist-cli/main.js CHANGED
@@ -3,8 +3,8 @@ import { runBuild } from './run-build.js';
3
3
  import { runDev } from './run-dev.js';
4
4
  const USAGE = `describe-me — living component docs from your tests
5
5
 
6
- describe-me dev [--data .describe-me] [--port 6006] viewer with live updates
7
- describe-me build [--data .describe-me] [--out describe-me-dist] [--no-vendor-fonts] static site
6
+ describe-me dev [--data .describe-me] [--port 6006] [--no-live] [--root <dir>] viewer that follows the test runs, and the live preview
7
+ describe-me build [--data .describe-me] [--out describe-me-dist] [--no-vendor-fonts] [--live] [--root <dir>] static site
8
8
  `;
9
9
  /** Entry point of the `describe-me` binary. */
10
10
  async function main() {
@@ -14,10 +14,14 @@ async function main() {
14
14
  return;
15
15
  }
16
16
  if (options.command === 'build') {
17
- await runBuild(options.data, options.out, { vendorFonts: options.vendorFonts });
17
+ await runBuild(options.data, options.out, {
18
+ vendorFonts: options.vendorFonts,
19
+ live: options.live,
20
+ root: options.root,
21
+ });
18
22
  return;
19
23
  }
20
- await runDev(options.data, options.port);
24
+ await runDev(options.data, options.port, { live: options.live, root: options.root });
21
25
  }
22
26
  main().catch((error) => {
23
27
  process.stderr.write(`describe-me: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -9,9 +9,20 @@ export interface CliOptions {
9
9
  port: number;
10
10
  /** `build`: download web fonts from known font hosts into the site. */
11
11
  vendorFonts: boolean;
12
+ /**
13
+ * `dev`: start the live preview next to the viewer. `build`: build it into
14
+ * the site. On for `dev` and off for `build`, unless `--live` or `--no-live` says otherwise.
15
+ */
16
+ live: boolean;
17
+ /**
18
+ * `dev` and `build --live`: the project root of the live preview, instead of
19
+ * the manifest's root and config file.
20
+ */
21
+ root: string | undefined;
12
22
  }
13
23
  /**
14
- * Turn `describe-me <command> [--data dir] [--out dir] [--port n] [--no-vendor-fonts]`
15
- * into options with defaults.
24
+ * Turn `describe-me <command> [--data dir] [--out dir] [--port n] [--no-vendor-fonts]
25
+ * [--live | --no-live] [--root dir]` into options with defaults. `build` ignores
26
+ * `--port`, and `--root` without `--live`.
16
27
  */
17
28
  export declare function parseArgs(argv: string[]): CliOptions;
@@ -9,8 +9,9 @@ function commandFrom(positional, help) {
9
9
  return 'dev';
10
10
  }
11
11
  /**
12
- * Turn `describe-me <command> [--data dir] [--out dir] [--port n] [--no-vendor-fonts]`
13
- * into options with defaults.
12
+ * Turn `describe-me <command> [--data dir] [--out dir] [--port n] [--no-vendor-fonts]
13
+ * [--live | --no-live] [--root dir]` into options with defaults. `build` ignores
14
+ * `--port`, and `--root` without `--live`.
14
15
  */
15
16
  export function parseArgs(argv) {
16
17
  const { values, positionals } = parseNodeArgs({
@@ -23,13 +24,18 @@ export function parseArgs(argv) {
23
24
  port: { type: 'string', default: '6006' },
24
25
  help: { type: 'boolean', short: 'h', default: false },
25
26
  'vendor-fonts': { type: 'boolean', default: true },
27
+ live: { type: 'boolean' },
28
+ root: { type: 'string' },
26
29
  },
27
30
  });
31
+ const command = commandFrom(positionals[0], values.help);
28
32
  return {
29
- command: commandFrom(positionals[0], values.help),
33
+ command,
30
34
  data: values.data,
31
35
  out: values.out,
32
36
  port: Number(values.port),
33
37
  vendorFonts: values['vendor-fonts'],
38
+ live: values.live ?? command === 'dev',
39
+ root: values.root,
34
40
  };
35
41
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The message of an error as the CLI prints it after its own
3
+ * `describe-me: `: without colours, and without a prefix of its own.
4
+ */
5
+ export declare function reasonOf(error: unknown): string;
@@ -0,0 +1,13 @@
1
+ const PREFIX = 'describe-me: ';
2
+ // Vite and its bundler colour their errors for the terminal.
3
+ const COLOURS = new RegExp(String.raw `\u001b\[[\d;]*m`, 'g');
4
+ /**
5
+ * The message of an error as the CLI prints it after its own
6
+ * `describe-me: `: without colours, and without a prefix of its own.
7
+ */
8
+ export function reasonOf(error) {
9
+ const message = (error instanceof Error ? error.message : String(error))
10
+ .replace(COLOURS, '')
11
+ .trim();
12
+ return message.startsWith(PREFIX) ? message.slice(PREFIX.length) : message;
13
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The file `@describe-me/vitest/live` resolves to from the project root.
3
+ * Throws the reason, followed by `consequence`, when the project has none.
4
+ */
5
+ export declare function resolveLiveEntry(root: string, consequence: string): string;
@@ -0,0 +1,42 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join } from 'node:path';
4
+ import { codeOf } from './code-of.js';
5
+ import { livePredates } from './live-predates.js';
6
+ function notFound(root, consequence) {
7
+ return new Error(`cannot find @describe-me/vitest from ${root}, ${consequence}. Pass --root with the project directory.`);
8
+ }
9
+ /** Whether `node_modules/@describe-me/vitest` is in `root` or a directory above it. */
10
+ function isInstalledFrom(root) {
11
+ for (let dir = root;; dir = dirname(dir)) {
12
+ if (existsSync(join(dir, 'node_modules', '@describe-me', 'vitest', 'package.json'))) {
13
+ return true;
14
+ }
15
+ if (dirname(dir) === dir) {
16
+ return false;
17
+ }
18
+ }
19
+ }
20
+ /**
21
+ * The file `@describe-me/vitest/live` resolves to from the project root.
22
+ * Throws the reason, followed by `consequence`, when the project has none.
23
+ */
24
+ export function resolveLiveEntry(root, consequence) {
25
+ // The bin shims of package managers put their own store on NODE_PATH, which
26
+ // `resolve()` searches too. Only the project's own node_modules count.
27
+ if (!isInstalledFrom(root)) {
28
+ throw notFound(root, consequence);
29
+ }
30
+ try {
31
+ return createRequire(join(root, 'package.json')).resolve('@describe-me/vitest/live');
32
+ }
33
+ catch (error) {
34
+ if (codeOf(error) === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
35
+ throw livePredates(root, 'the live preview', consequence);
36
+ }
37
+ if (codeOf(error) === 'MODULE_NOT_FOUND') {
38
+ throw notFound(root, consequence);
39
+ }
40
+ throw error;
41
+ }
42
+ }
@@ -2,9 +2,15 @@
2
2
  export interface RunBuildOptions {
3
3
  /** Download web fonts from known font hosts into the site. */
4
4
  vendorFonts: boolean;
5
+ /** Build the live preview into the site as `__live/`. */
6
+ live: boolean;
7
+ /** The project root of the live preview. Default: the manifest's `root` and `configFile`. */
8
+ root?: string;
5
9
  }
6
10
  /**
7
11
  * Produce a self-contained static site: the viewer plus the data directory
8
- * under `__data/`. Font vendoring works on that copy and never fails the build.
12
+ * under `__data/`, and with `live` the live preview under `__live/`. A live
13
+ * build that fails stops the command. Font vendoring works on the copy of the
14
+ * data and never fails the build.
9
15
  */
10
16
  export declare function runBuild(data: string, out: string, options: RunBuildOptions): Promise<void>;
@@ -1,13 +1,16 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { build } from 'vite';
4
+ import { buildLivePreview } from './build-live-preview.js';
4
5
  import { fontCacheDir } from './font-cache-dir.js';
5
6
  import { printVendorReport } from './print-vendor-report.js';
6
7
  import { vendorFonts } from './vendor-fonts.js';
7
8
  import { viewerRoot } from './viewer-root.js';
8
9
  /**
9
10
  * Produce a self-contained static site: the viewer plus the data directory
10
- * under `__data/`. Font vendoring works on that copy and never fails the build.
11
+ * under `__data/`, and with `live` the live preview under `__live/`. A live
12
+ * build that fails stops the command. Font vendoring works on the copy of the
13
+ * data and never fails the build.
11
14
  */
12
15
  export async function runBuild(data, out, options) {
13
16
  process.env.DESCRIBE_ME_DIR = resolve(data);
@@ -17,6 +20,10 @@ export async function runBuild(data, out, options) {
17
20
  configFile: resolve(root, 'vite.config.ts'),
18
21
  build: { outDir: resolve(out), emptyOutDir: true },
19
22
  });
23
+ // After the viewer, whose build empties `out` and so removes an old `__live/` and `__live.json`.
24
+ if (options.live) {
25
+ await buildLivePreview(data, out, options.root);
26
+ }
20
27
  if (!options.vendorFonts) {
21
28
  return;
22
29
  }