@doc-kit/core 1.2.0 → 1.3.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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/package.json +6 -7
  4. package/src/generators/ast/generate.mjs +7 -3
  5. package/src/generators/ast/index.mjs +9 -0
  6. package/src/generators/index.mjs +1 -0
  7. package/src/generators/json/README.md +1 -1
  8. package/src/generators/json/generated/schema.d.ts +1 -1
  9. package/src/generators/json/index.mjs +9 -0
  10. package/src/generators/json/utils/markdown.mjs +2 -2
  11. package/src/generators/json-all/README.md +1 -1
  12. package/src/generators/json-simple/README.md +1 -1
  13. package/src/generators/loader.mjs +14 -0
  14. package/src/generators/metadata/generate.mjs +4 -2
  15. package/src/generators/metadata/index.mjs +9 -0
  16. package/src/generators/metadata/maps/mdn.json +3 -0
  17. package/src/generators/metadata/types.d.ts +1 -1
  18. package/src/generators/metadata/utils/parse.mjs +2 -4
  19. package/src/generators/metadata/utils/transformers.mjs +1 -1
  20. package/src/generators/metadata/utils/visitors.mjs +2 -2
  21. package/src/generators/types.d.ts +8 -0
  22. package/src/generators.mjs +4 -0
  23. package/src/logger/constants.mjs +35 -0
  24. package/src/logger/transports/github.mjs +48 -15
  25. package/src/plugins/shiki/highlighter.mjs +373 -0
  26. package/src/plugins/shiki/rehype.mjs +38 -0
  27. package/src/{utils/type-annotations/highlighted.mjs → plugins/type-annotations/highlighter.mjs} +14 -19
  28. package/src/{utils → plugins}/type-annotations/remark.mjs +6 -2
  29. package/src/threading/chunk-worker.mjs +4 -0
  30. package/src/threading/constants.mjs +10 -0
  31. package/src/threading/index.mjs +6 -1
  32. package/src/utils/configuration/constants.mjs +3 -3
  33. package/src/utils/configuration/index.mjs +86 -20
  34. package/src/utils/configuration/types.d.ts +27 -0
  35. package/src/utils/loaders.mjs +23 -1
  36. package/src/utils/markdown/constants.mjs +7 -0
  37. package/src/utils/markdown/plugins.mjs +232 -0
  38. package/src/utils/markdown/processor.mjs +60 -0
  39. package/src/utils/highlighter.mjs +0 -198
  40. package/src/utils/remark-shiki.mjs +0 -29
  41. package/src/utils/remark.mjs +0 -90
  42. /package/src/{utils → plugins}/type-annotations/hast.mjs +0 -0
  43. /package/src/{utils → plugins}/type-annotations/mdast.mjs +0 -0
  44. /package/src/{utils → plugins}/type-annotations/syntax.mjs +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @doc-kit/core
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1156](https://github.com/nodejs/doc-kit/pull/1156) [`6d5f9d1`](https://github.com/nodejs/doc-kit/commit/6d5f9d17d9f265c62db0f878c4e03903881b11c9) Thanks [@ovflowd](https://github.com/ovflowd)! - Builds take about half the memory and a third of the time (measured on the Node.js API docs): Shiki registers each language once code in it is highlighted, highlighted code reaches pages as static markup rather than a tree per token, `all.html` is rendered by the worker pool, output is no longer minified by default (serve it compressed instead), idle workers end after 250ms, and each worker's heap is limited to 512MB, which Node.js's `--max-old-space-size` raises.
8
+
9
+ - [#1127](https://github.com/nodejs/doc-kit/pull/1127) [`c50e75f`](https://github.com/nodejs/doc-kit/commit/c50e75f8f50c84b6ff3b16b84059bd0f53ca5d1c) Thanks [@ovflowd](https://github.com/ovflowd)! - Add the `llms-txt-full` generator, writing a `llms-full.txt` file holding the Markdown of every page, each preceded by its URL.
10
+
11
+ - [#1157](https://github.com/nodejs/doc-kit/pull/1157) [`a4c9684`](https://github.com/nodejs/doc-kit/commit/a4c9684fd46f14b3bcbaf2e8f01a246a47ee27cd) Thanks [@ovflowd](https://github.com/ovflowd)! - Add a `markdown` option to add remark, rehype, and recma plugins to the generators processing Markdown, or configure the ones they use, such as Shiki. The `@doc-kit/core` modules the generators' pipelines replace are removed (`utils/remark.mjs`, `utils/remark-shiki.mjs`, and `utils/highlighter.mjs`), and `utils/type-annotations` moves to `plugins/type-annotations`
12
+
13
+ ## 1.2.1
14
+
15
+ ### Patch Changes
16
+
17
+ - [#1105](https://github.com/nodejs/doc-kit/pull/1105) [`615aabe`](https://github.com/nodejs/doc-kit/commit/615aabeb1955a8028146e26fd4589c1172f58367) Thanks [@ryzrr](https://github.com/ryzrr)! - fix(metadata): link class types whose name starts with an acronym
18
+
3
19
  ## 1.2.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -15,7 +15,7 @@ debugging-only `json-simple`); the rest come from companion packages:
15
15
 
16
16
  | Package | Generators |
17
17
  | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
18
- | [`@doc-kit/generator-react`](https://www.npmjs.com/package/@doc-kit/generator-react) | `html` (the modern site), `orama-db`, `llms-txt`, `sitemap` |
18
+ | [`@doc-kit/generator-react`](https://www.npmjs.com/package/@doc-kit/generator-react) | `html` (the modern site), `orama-db`, `llms-txt`, `llms-txt-full`, `sitemap` |
19
19
  | [`@node-core/doc-kit-legacy`](https://www.npmjs.com/package/@node-core/doc-kit-legacy) | `legacy-html`, `legacy-html-all`, `legacy-json`, `legacy-json-all` (Node.js-specific) |
20
20
  | [`@node-core/doc-kit`](https://www.npmjs.com/package/@node-core/doc-kit) | `man-page`, `api-links`, `addon-verify` (Node.js-specific) |
21
21
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@doc-kit/core",
3
3
  "type": "module",
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "description": "The doc-kit engine: the pipeline and shared APIs that power the doc-kit generators and CLI",
6
6
  "repository": {
7
7
  "type": "git",
@@ -33,6 +33,7 @@
33
33
  "./src/parsers/*",
34
34
  "./src/parsers/*.d.ts"
35
35
  ],
36
+ "#plugins/*": "./src/plugins/*",
36
37
  "#utils/*": "./src/utils/*"
37
38
  },
38
39
  "files": [
@@ -45,19 +46,17 @@
45
46
  "README.md"
46
47
  ],
47
48
  "dependencies": {
48
- "@actions/core": "^3.0.0",
49
- "@node-core/rehype-shiki": "^1.5.0",
50
- "@swc/html-wasm": "^1.15.46",
49
+ "@node-core/rehype-shiki": "^1.5.1",
50
+ "@swc/html-wasm": "^1.16.2",
51
51
  "@swc/wasm": "^1.15.46",
52
52
  "acorn": "^8.17.0",
53
53
  "cosmiconfig": "^9.0.2",
54
54
  "dedent": "^1.7.2",
55
55
  "github-slugger": "^2.0.0",
56
56
  "glob-parent": "^6.0.2",
57
- "hastscript": "^9.0.1",
58
57
  "mdast-util-slice-markdown": "^2.0.1",
59
58
  "mdast-util-to-string": "^4.0.0",
60
- "piscina": "^5.3.1",
59
+ "piscina": "^5.3.2",
61
60
  "rehype-stringify": "^10.0.1",
62
61
  "remark-gfm": "^4.0.1",
63
62
  "remark-mdx": "^3.1.1",
@@ -74,7 +73,7 @@
74
73
  "unist-util-remove": "^4.0.0",
75
74
  "unist-util-select": "^5.1.0",
76
75
  "unist-util-visit": "^5.1.0",
77
- "yaml": "^2.9.0"
76
+ "yaml": "^2.9.1"
78
77
  },
79
78
  "devDependencies": {
80
79
  "ajv": "^8.20.0",
@@ -9,8 +9,8 @@ import { parse as parseYaml } from 'yaml';
9
9
 
10
10
  import getConfig from '#utils/configuration/index.mjs';
11
11
  import { withExt } from '#utils/file.mjs';
12
+ import { getProcessor } from '#utils/markdown/processor.mjs';
12
13
  import { QUERIES } from '#utils/queries/index.mjs';
13
- import { getRemark as remark, getRemarkMdx } from '#utils/remark.mjs';
14
14
 
15
15
  import { STABILITY_INDEX_URL } from './constants.mjs';
16
16
 
@@ -67,6 +67,8 @@ export async function processChunk(inputSlice, itemIndices) {
67
67
  // The path is the relative path minus the extension
68
68
  const relativePath = sep + withExt(relative(parent, path));
69
69
 
70
+ const processor = getProcessor('ast', { mdx });
71
+
70
72
  let tree;
71
73
 
72
74
  if (mdx) {
@@ -79,7 +81,7 @@ export async function processChunk(inputSlice, itemIndices) {
79
81
  ''
80
82
  );
81
83
 
82
- tree = getRemarkMdx().parse(source);
84
+ tree = processor.parse(source);
83
85
 
84
86
  if (frontmatter) {
85
87
  tree.children.unshift({
@@ -93,9 +95,11 @@ export async function processChunk(inputSlice, itemIndices) {
93
95
  (_, yaml) => `<!-- YAML\n${yaml}\n-->`
94
96
  );
95
97
 
96
- tree = remark().parse(value);
98
+ tree = processor.parse(value);
97
99
  }
98
100
 
101
+ tree = await processor.run(tree, { path });
102
+
99
103
  results.push({ tree, path: relativePath, mdx });
100
104
  }
101
105
 
@@ -15,6 +15,15 @@ export default {
15
15
 
16
16
  hasParallelProcessor: true,
17
17
 
18
+ markdown: {
19
+ remarkPlugins: [
20
+ 'remark-parse',
21
+ '#plugins/type-annotations/remark.mjs',
22
+ 'remark-gfm',
23
+ '...',
24
+ ],
25
+ },
26
+
18
27
  generate,
19
28
  processChunk,
20
29
  };
@@ -22,6 +22,7 @@ export const publicGenerators = {
22
22
  'api-links': '@node-core/doc-kit/api-links',
23
23
  'orama-db': '@doc-kit/generator-react/orama-db',
24
24
  'llms-txt': '@doc-kit/generator-react/llms-txt',
25
+ 'llms-txt-full': '@doc-kit/generator-react/llms-txt-full',
25
26
  sitemap: '@doc-kit/generator-react/sitemap',
26
27
  html: '@doc-kit/generator-react/html',
27
28
  'section-pages': '@doc-kit/generator-react/section-pages',
@@ -18,7 +18,7 @@ Files keep the input's directory layout: `doc/api/some/fs.md` becomes `out/some/
18
18
 
19
19
  - `output` {string} The directory to write the documents to.
20
20
  - `minify` {boolean} Whether to minify the output. Inherited from `global`.
21
- **Default:** `true`.
21
+ **Default:** `false`.
22
22
  - `repository` {string} The `owner/name` repository source links resolve
23
23
  against. Inherited from `global`; without one, `sourceLink.url` is `null`.
24
24
  - `schemaURL` {string} Where the schema is published. `{schemaVersion}` is
@@ -1,4 +1,4 @@
1
- /* eslint-disable */
1
+ /* oxlint-disable */
2
2
  /**
3
3
  * Generated from `schema.json` by `scripts/generate-json-types.mjs`.
4
4
  * Do not edit: change the schema and regenerate instead.
@@ -22,6 +22,15 @@ export default {
22
22
 
23
23
  hasParallelProcessor: true,
24
24
 
25
+ markdown: {
26
+ remarkPlugins: [
27
+ '#plugins/type-annotations/remark.mjs',
28
+ 'remark-gfm',
29
+ 'remark-stringify',
30
+ '...',
31
+ ],
32
+ },
33
+
25
34
  generate,
26
35
  processChunk,
27
36
  };
@@ -3,7 +3,7 @@
3
3
  import { u as createTree } from 'unist-builder';
4
4
  import { visit } from 'unist-util-visit';
5
5
 
6
- import { getRemark, getRemarkMdx } from '#utils/remark.mjs';
6
+ import { getProcessor } from '#utils/markdown/processor.mjs';
7
7
 
8
8
  import { DISPLAY_NAME } from '../constants.mjs';
9
9
 
@@ -19,7 +19,7 @@ export const blocksToMarkdown = (nodes, mdx = false) => {
19
19
  return '';
20
20
  }
21
21
 
22
- const processor = mdx ? getRemarkMdx() : getRemark();
22
+ const processor = getProcessor('json', { mdx });
23
23
 
24
24
  return processor.stringify(createTree('root', nodes)).trim();
25
25
  };
@@ -23,7 +23,7 @@ generator's schema for the documents.
23
23
 
24
24
  - `output` {string} The directory where `all.json` will be written.
25
25
  - `minify` {boolean} Whether to minify the output. Inherited from `global`.
26
- **Default:** `true`.
26
+ **Default:** `false`.
27
27
  - `index` {Array} The `{ api }` objects defining the document order. Inherited
28
28
  from `global`.
29
29
  - `schemaURL` {string} Where the bundle's schema is published.
@@ -6,4 +6,4 @@ The `json-simple` generator creates a simplified JSON version of the API documen
6
6
 
7
7
  - `output` {string} The directory where `api-docs.json` will be written.
8
8
  - `minify` {boolean} Whether to minify the output JSON. Inherited from
9
- `global`. **Default:** `true`.
9
+ `global`. **Default:** `false`.
@@ -7,6 +7,9 @@ import { enforceArray } from '#utils/array.mjs';
7
7
 
8
8
  import { allGenerators } from './index.mjs';
9
9
 
10
+ // The module each loaded generator comes from
11
+ const generatorModules = new WeakMap();
12
+
10
13
  /**
11
14
  * Resolves a CLI/configuration target into an import specifier. Shorthand
12
15
  * names map through the alias table; filesystem paths become file URLs;
@@ -64,9 +67,20 @@ export const loadGenerator = async specifier => {
64
67
  );
65
68
  }
66
69
 
70
+ generatorModules.set(generator, import.meta.resolve(resolved));
71
+
67
72
  return generator;
68
73
  };
69
74
 
75
+ /**
76
+ * The URL of the module a generator was loaded from, which the specifiers of
77
+ * its Markdown pipeline resolve against.
78
+ *
79
+ * @param {GeneratorMetadata} generator - A generator loaded by `loadGenerator`
80
+ * @returns {string | undefined}
81
+ */
82
+ export const getGeneratorModule = generator => generatorModules.get(generator);
83
+
70
84
  /**
71
85
  * Loads the given generators plus the transitive closure of their
72
86
  * dependencies (via `dependsOn`) and dependents (via `dependent`).
@@ -3,8 +3,6 @@
3
3
  import getConfig from '#utils/configuration/index.mjs';
4
4
  import { loadFromURL } from '#utils/loaders.mjs';
5
5
 
6
- import { parseApiDoc } from './utils/parse.mjs';
7
-
8
6
  /**
9
7
  * Process a chunk of API doc files in a worker thread.
10
8
  * Called by chunk-worker.mjs for parallel processing.
@@ -12,6 +10,10 @@ import { parseApiDoc } from './utils/parse.mjs';
12
10
  * @type {import('./types').Generator['processChunk']}
13
11
  */
14
12
  export async function processChunk(fullInput, itemIndices, typeMap) {
13
+ // Loaded on first use rather than with the generator, so the main thread,
14
+ // which never parses, does not load the TypeScript parser (~40MB of WASM)
15
+ const { parseApiDoc } = await import('./utils/parse.mjs');
16
+
15
17
  const results = [];
16
18
 
17
19
  for (const idx of itemIndices) {
@@ -16,6 +16,15 @@ export default {
16
16
 
17
17
  hasParallelProcessor: true,
18
18
 
19
+ markdown: {
20
+ remarkPlugins: [
21
+ 'remark-parse',
22
+ '#plugins/type-annotations/remark.mjs',
23
+ 'remark-gfm',
24
+ '...',
25
+ ],
26
+ },
27
+
19
28
  generate,
20
29
  processChunk,
21
30
  };
@@ -475,6 +475,7 @@
475
475
  "magnetometer": "https://developer.mozilla.org/docs/Web/API/Magnetometer",
476
476
  "managedmediasource": "https://developer.mozilla.org/docs/Web/API/ManagedMediaSource",
477
477
  "managedsourcebuffer": "https://developer.mozilla.org/docs/Web/API/ManagedSourceBuffer",
478
+ "mathmlanchorelement": "https://developer.mozilla.org/docs/Web/API/MathMLAnchorElement",
478
479
  "mathmlelement": "https://developer.mozilla.org/docs/Web/API/MathMLElement",
479
480
  "mediacapabilities": "https://developer.mozilla.org/docs/Web/API/MediaCapabilities",
480
481
  "mediadeviceinfo": "https://developer.mozilla.org/docs/Web/API/MediaDeviceInfo",
@@ -553,6 +554,7 @@
553
554
  "oes_vertex_array_object": "https://developer.mozilla.org/docs/Web/API/OES_vertex_array_object",
554
555
  "otpcredential": "https://developer.mozilla.org/docs/Web/API/OTPCredential",
555
556
  "ovr_multiview2": "https://developer.mozilla.org/docs/Web/API/OVR_multiview2",
557
+ "observable": "https://developer.mozilla.org/docs/Web/API/Observable",
556
558
  "offlineaudiocompletionevent": "https://developer.mozilla.org/docs/Web/API/OfflineAudioCompletionEvent",
557
559
  "offlineaudiocontext": "https://developer.mozilla.org/docs/Web/API/OfflineAudioContext",
558
560
  "offscreencanvas": "https://developer.mozilla.org/docs/Web/API/OffscreenCanvas",
@@ -826,6 +828,7 @@
826
828
  "stylesheet": "https://developer.mozilla.org/docs/Web/API/StyleSheet",
827
829
  "stylesheetlist": "https://developer.mozilla.org/docs/Web/API/StyleSheetList",
828
830
  "submitevent": "https://developer.mozilla.org/docs/Web/API/SubmitEvent",
831
+ "subscriber": "https://developer.mozilla.org/docs/Web/API/Subscriber",
829
832
  "subtlecrypto": "https://developer.mozilla.org/docs/Web/API/SubtleCrypto",
830
833
  "summarizer": "https://developer.mozilla.org/docs/Web/API/Summarizer",
831
834
  "syncevent": "https://developer.mozilla.org/docs/Web/API/SyncEvent",
@@ -1,5 +1,5 @@
1
- import type { Position } from 'unist';
2
1
  import type { Root, Node, Data, Blockquote, Heading } from 'mdast';
2
+ import type { Position } from 'unist';
3
3
 
4
4
  export type Generator = GeneratorMetadata<
5
5
  {},
@@ -14,7 +14,6 @@ import {
14
14
  IGNORE_STABILITY_STEMS,
15
15
  } from '#generators/metadata/constants.mjs';
16
16
  import { UNIST } from '#utils/queries/index.mjs';
17
- import { getRemark as remark } from '#utils/remark.mjs';
18
17
  import { relative } from '#utils/url.mjs';
19
18
 
20
19
  import { resolveTypeAnnotations } from './resolveTypes.mjs';
@@ -147,9 +146,8 @@ export const parseApiDoc = ({ path, tree, mdx = false }, typeMap) => {
147
146
  // Remove processed YAML nodes from the content
148
147
  remove(subTree, [UNIST.isYamlNode]);
149
148
 
150
- // Apply AST transformations
151
- const parsedSubTree = remark().runSync(subTree);
152
- metadata.content = parsedSubTree;
149
+ // The remark plugins already ran on the whole document, in `ast`
150
+ metadata.content = subTree;
153
151
 
154
152
  // Add to collection
155
153
  metadataCollection.push(metadata);
@@ -72,7 +72,7 @@ export const resolveTypeReference = (name, typeMap) => {
72
72
  // Transform Node.js types like 'vm.Something'.
73
73
  if (MODULE_QUALIFIED_NAME.test(name)) {
74
74
  const [mod, ...pieces] = name.split('.');
75
- const isClass = pieces.at(-1).match(/^[A-Z][a-z]/);
75
+ const isClass = pieces.at(-1).match(/^[A-Z](?=.*[a-z])/);
76
76
 
77
77
  return `${mod}.html#${isClass ? 'class-' : ''}${slug(name)}`;
78
78
  }
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { SKIP } from 'unist-util-visit';
4
4
 
5
+ import { getProcessor } from '#utils/markdown/processor.mjs';
5
6
  import { QUERIES } from '#utils/queries/index.mjs';
6
- import { getRemark as remark } from '#utils/remark.mjs';
7
7
  import { transformNodesToString } from '#utils/unist.mjs';
8
8
 
9
9
  import { transformUnixManualToLink } from './transformers.mjs';
@@ -41,7 +41,7 @@ const updateReferences = (query, transformer, node, parent) => {
41
41
  // and adding those nodes to the parent.
42
42
  const {
43
43
  children: [newNode],
44
- } = remark().parse(replacedTypes);
44
+ } = getProcessor('metadata').parse(replacedTypes);
45
45
 
46
46
  // Find the index of the original node in the parent
47
47
  const index = parent.children.indexOf(node);
@@ -70,6 +70,14 @@ declare global {
70
70
 
71
71
  hasParallelProcessor?: boolean;
72
72
 
73
+ /**
74
+ * The unified plugins this generator processes Markdown with, listed as
75
+ * in the `markdown` option, with paths relative to its module. Each list
76
+ * takes the configured plugins in place of its `'...'`.
77
+ * `getProcessor(name)` gives its processor.
78
+ */
79
+ markdown?: import('../utils/configuration/types').MarkdownPipeline;
80
+
73
81
  /**
74
82
  * The immediate generator that this generator depends on.
75
83
  * For example, the `html` generator depends on the `react` generator.
@@ -9,6 +9,7 @@ import { resolvePipeline } from './generators/pipeline.mjs';
9
9
  import logger from './logger/index.mjs';
10
10
  import createWorkerPool from './threading/index.mjs';
11
11
  import createParallelWorker from './threading/parallel.mjs';
12
+ import { loadMarkdownPlugins } from './utils/markdown/plugins.mjs';
12
13
  import { isAsyncIterable } from './utils/misc.mjs';
13
14
 
14
15
  const generatorsLogger = logger.child('generators');
@@ -78,6 +79,9 @@ const createGenerator = () => {
78
79
 
79
80
  generatorsLogger.debug(`Starting "${name}"`);
80
81
 
82
+ // Load its Markdown pipeline, for what it processes on this thread
83
+ await loadMarkdownPlugins(generator, configuration[name]?.markdown);
84
+
81
85
  // Create parallel worker for streaming generators
82
86
  const worker = hasParallelProcessor
83
87
  ? createParallelWorker(specifier, generator, pool, configuration)
@@ -32,3 +32,38 @@ export const levelToColorMap = {
32
32
  [LogLevel.error]: 'magenta',
33
33
  [LogLevel.fatal]: 'red',
34
34
  };
35
+
36
+ /**
37
+ * Maps log level numbers to the GitHub Actions workflow commands reporting
38
+ * them.
39
+ */
40
+ export const levelToGitHubCommand = {
41
+ [LogLevel.debug]: 'debug',
42
+ [LogLevel.info]: 'notice',
43
+ [LogLevel.warn]: 'warning',
44
+ [LogLevel.error]: 'error',
45
+ [LogLevel.fatal]: 'error',
46
+ };
47
+
48
+ /**
49
+ * Maps the characters a workflow command's message can't hold to their
50
+ * escapes. A command is a line, `::command key=value,key=value::message`,
51
+ * so its message, which runs to the end of the line, can't break it, nor hold
52
+ * the `%` an escape starts with.
53
+ */
54
+ export const commandDataEscapes = {
55
+ '%': '%25',
56
+ '\r': '%0D',
57
+ '\n': '%0A',
58
+ };
59
+
60
+ /**
61
+ * Maps the characters a workflow command's property can't hold to their
62
+ * escapes: those its message can't, and the `,` separating properties and
63
+ * the `:` of the `::` ending them.
64
+ */
65
+ export const commandPropertyEscapes = {
66
+ ...commandDataEscapes,
67
+ ':': '%3A',
68
+ ',': '%2C',
69
+ };
@@ -1,23 +1,35 @@
1
1
  'use strict';
2
2
 
3
- import { debug, notice, warning, error } from '@actions/core';
3
+ import { EOL } from 'node:os';
4
4
 
5
- import { LogLevel } from '#logger/constants.mjs';
5
+ import {
6
+ commandDataEscapes,
7
+ commandPropertyEscapes,
8
+ levelToGitHubCommand,
9
+ } from '#logger/constants.mjs';
6
10
  import { prettifyLevel } from '#logger/utils/colors.mjs';
7
11
  import { prettifyTimestamp } from '#logger/utils/time.mjs';
8
12
 
9
- const actions = {
10
- [LogLevel.debug]: debug,
11
- [LogLevel.info]: notice,
12
- [LogLevel.warn]: warning,
13
- [LogLevel.error]: error,
14
- [LogLevel.fatal]: error,
15
- };
13
+ /**
14
+ * Escapes a value of a workflow command: its message, with
15
+ * `commandDataEscapes`, or one of its properties, with
16
+ * `commandPropertyEscapes`.
17
+ *
18
+ * @param {string | number} value - The value
19
+ * @param {Record<string, string>} escapes - The escapes of the characters it can't hold
20
+ * @returns {string}
21
+ */
22
+ const escapeCommandValue = (value, escapes) =>
23
+ String(value).replace(
24
+ /[%\r\n:,]/g,
25
+ character => escapes[character] ?? character
26
+ );
16
27
 
17
28
  /**
18
- * Logs messages to GitHub Actions with formatted output and file info with
19
- * appropriate gh actions based on level.
29
+ * Logs messages to GitHub Actions as workflow commands, with the file and
30
+ * lines they're about, for them to annotate.
20
31
  *
32
+ * @see https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands
21
33
  * @param {import('../types').TransportContext} context
22
34
  * @returns {void}
23
35
  */
@@ -30,13 +42,34 @@ const github = ({ level, message, timestamp, metadata = {}, module }) => {
30
42
 
31
43
  const logMessage = `[${time}] ${prettyLevel}${module ? ` (${module})` : ''}: ${message}`;
32
44
 
33
- const logFn = actions[level] ?? notice;
45
+ const command = levelToGitHubCommand[level] ?? 'notice';
46
+
47
+ const data = escapeCommandValue(logMessage, commandDataEscapes);
34
48
 
35
- logFn(logMessage, {
49
+ // Debug messages take no properties
50
+ if (command === 'debug') {
51
+ process.stdout.write(`::debug::${data}${EOL}`);
52
+
53
+ return;
54
+ }
55
+
56
+ const properties = {
36
57
  file: file?.path,
37
- startLine: file?.position?.start.line,
58
+ line: file?.position?.start.line,
38
59
  endLine: file?.position?.end.line,
39
- });
60
+ };
61
+
62
+ const annotations = [];
63
+
64
+ for (const [key, value] of Object.entries(properties)) {
65
+ if (value) {
66
+ annotations.push(
67
+ `${key}=${escapeCommandValue(value, commandPropertyEscapes)}`
68
+ );
69
+ }
70
+ }
71
+
72
+ process.stdout.write(`::${command} ${annotations.join(',')}::${data}${EOL}`);
40
73
  };
41
74
 
42
75
  export default github;