@doc-kit/core 0.0.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 (75) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +36 -0
  4. package/package.json +88 -0
  5. package/shiki.config.mjs +50 -0
  6. package/src/caching.mjs +183 -0
  7. package/src/generators/ast/README.md +8 -0
  8. package/src/generators/ast/constants.mjs +1 -0
  9. package/src/generators/ast/generate.mjs +125 -0
  10. package/src/generators/ast/index.mjs +20 -0
  11. package/src/generators/ast/types.d.ts +7 -0
  12. package/src/generators/ast-js/README.md +8 -0
  13. package/src/generators/ast-js/generate.mjs +56 -0
  14. package/src/generators/ast-js/index.mjs +24 -0
  15. package/src/generators/ast-js/types.d.ts +5 -0
  16. package/src/generators/index.mjs +48 -0
  17. package/src/generators/json-simple/README.md +9 -0
  18. package/src/generators/json-simple/generate.mjs +33 -0
  19. package/src/generators/json-simple/index.mjs +23 -0
  20. package/src/generators/json-simple/types.d.ts +6 -0
  21. package/src/generators/loader.mjs +96 -0
  22. package/src/generators/metadata/README.md +8 -0
  23. package/src/generators/metadata/constants.mjs +68 -0
  24. package/src/generators/metadata/generate.mjs +40 -0
  25. package/src/generators/metadata/index.mjs +21 -0
  26. package/src/generators/metadata/maps/builtin.json +16 -0
  27. package/src/generators/metadata/maps/mdn.json +1114 -0
  28. package/src/generators/metadata/types.d.ts +145 -0
  29. package/src/generators/metadata/utils/parse.mjs +160 -0
  30. package/src/generators/metadata/utils/resolveTypes.mjs +204 -0
  31. package/src/generators/metadata/utils/slugger.mjs +39 -0
  32. package/src/generators/metadata/utils/transformers.mjs +109 -0
  33. package/src/generators/metadata/utils/visitors.mjs +134 -0
  34. package/src/generators/metadata/utils/yaml.mjs +58 -0
  35. package/src/generators/types.d.ts +129 -0
  36. package/src/generators.mjs +145 -0
  37. package/src/logger/constants.mjs +34 -0
  38. package/src/logger/index.mjs +27 -0
  39. package/src/logger/logger.mjs +177 -0
  40. package/src/logger/transports/console.mjs +54 -0
  41. package/src/logger/transports/github.mjs +42 -0
  42. package/src/logger/transports/index.mjs +11 -0
  43. package/src/logger/types.d.ts +29 -0
  44. package/src/logger/utils/colors.mjs +18 -0
  45. package/src/logger/utils/time.mjs +21 -0
  46. package/src/parsers/markdown.mjs +49 -0
  47. package/src/parsers/types.d.ts +7 -0
  48. package/src/threading/chunk-worker.mjs +23 -0
  49. package/src/threading/index.mjs +27 -0
  50. package/src/threading/parallel.mjs +140 -0
  51. package/src/utils/array.mjs +7 -0
  52. package/src/utils/configuration/index.mjs +263 -0
  53. package/src/utils/configuration/templates.mjs +21 -0
  54. package/src/utils/configuration/types.d.ts +64 -0
  55. package/src/utils/file.mjs +20 -0
  56. package/src/utils/generators.mjs +71 -0
  57. package/src/utils/highlighter.mjs +198 -0
  58. package/src/utils/html-minifier.mjs +12 -0
  59. package/src/utils/loaders.mjs +48 -0
  60. package/src/utils/misc.mjs +72 -0
  61. package/src/utils/queries/constants.mjs +3 -0
  62. package/src/utils/queries/index.mjs +93 -0
  63. package/src/utils/queries/utils.mjs +45 -0
  64. package/src/utils/remark.mjs +97 -0
  65. package/src/utils/signature/constants.mjs +12 -0
  66. package/src/utils/signature/parseList.mjs +133 -0
  67. package/src/utils/signature/parseSignature.mjs +208 -0
  68. package/src/utils/signature/types.d.ts +66 -0
  69. package/src/utils/swc.mjs +34 -0
  70. package/src/utils/type-annotations/hast.mjs +115 -0
  71. package/src/utils/type-annotations/mdast.mjs +75 -0
  72. package/src/utils/type-annotations/remark.mjs +24 -0
  73. package/src/utils/type-annotations/syntax.mjs +188 -0
  74. package/src/utils/unist.mjs +75 -0
  75. package/src/utils/url.mjs +27 -0
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ import { SKIP } from 'unist-util-visit';
4
+
5
+ import { transformUnixManualToLink } from './transformers.mjs';
6
+ import { extractYamlContent, parseYAMLIntoMetadata } from './yaml.mjs';
7
+ import { QUERIES } from '../../../utils/queries/index.mjs';
8
+ import { getRemark as remark } from '../../../utils/remark.mjs';
9
+ import { transformNodesToString } from '../../../utils/unist.mjs';
10
+
11
+ /**
12
+ * Updates a Markdown link into a HTML link for API docs
13
+ * @param {import('@types/mdast').Link} node A Markdown link node
14
+ */
15
+ export const visitMarkdownLink = node => {
16
+ node.url = node.url.replace(
17
+ QUERIES.markdownUrl,
18
+ (_, filename, hash = '') => `${filename}.html${hash}`
19
+ );
20
+
21
+ return [SKIP];
22
+ };
23
+
24
+ /**
25
+ * Updates a reference
26
+ * @param {import('@types/mdast').Text} node The current node
27
+ * @param {import('@types/mdast').Parent} parent The parent node
28
+ * @param {string|RegExp} query The search query
29
+ * @param {Function} transformer The function to transform the reference
30
+ */
31
+ const updateReferences = (query, transformer, node, parent) => {
32
+ const replacedTypes = node.value
33
+ .replace(query, transformer)
34
+ // Remark doesn't handle leading / trailing spaces, so replace them with
35
+ // HTML entities.
36
+ .replace(/^\s/, ' ')
37
+ .replace(/\s$/, ' ');
38
+
39
+ // This changes the type into a link by splitting it up into several nodes,
40
+ // and adding those nodes to the parent.
41
+ const {
42
+ children: [newNode],
43
+ } = remark().parse(replacedTypes);
44
+
45
+ // Find the index of the original node in the parent
46
+ const index = parent.children.indexOf(node);
47
+
48
+ // Replace the original node with the new node(s)
49
+ parent.children.splice(index, 1, ...newNode.children);
50
+
51
+ return [SKIP];
52
+ };
53
+
54
+ /**
55
+ * Updates unix manual references to be markdown links
56
+ * @param {import('@types/mdast').Text} node The text node
57
+ * @param {import('@types/mdast').Parent} parent The parent node
58
+ */
59
+ export const visitTextWithUnixManualNode = (node, parent) =>
60
+ updateReferences(
61
+ QUERIES.unixManualPage,
62
+ transformUnixManualToLink,
63
+ node,
64
+ parent
65
+ );
66
+
67
+ /**
68
+ * Updates a Markdown Link Reference into an actual Link to the Definition
69
+ * @param {import('@types/mdast').LinkReference} node A link reference node
70
+ * @param {Array<import('@types/mdast').Definition>} definitions The Definitions of the API Doc
71
+ */
72
+ export const visitLinkReference = (node, definitions) => {
73
+ const definition = definitions.find(
74
+ ({ identifier }) => identifier === node.identifier
75
+ );
76
+
77
+ node.type = 'link';
78
+ node.url = definition.url;
79
+
80
+ return [SKIP];
81
+ };
82
+
83
+ /**
84
+ * Parses a Stability Index Entry and updates the current Metadata
85
+ * @param {import('@types/mdast').Blockquote} node The blockquote node
86
+ * @param {import('../types').MetadataEntry} metadata The metadata object to update
87
+ */
88
+ export const visitStability = (node, metadata) => {
89
+ // `node` is a `blockquote` node, and the first child will always be
90
+ // a `paragraph` node, so we can safely access the children of the first child
91
+ // which we use as the prefix and description of the Stability Index
92
+ const stabilityPrefix = transformNodesToString(node.children[0].children);
93
+
94
+ // Attempts to grab the Stability index and description from the prefix
95
+ const matches = QUERIES.stabilityIndex.exec(stabilityPrefix);
96
+
97
+ // Ensures that the matches are valid and that we have at least 3 entries
98
+ if (matches && matches.length === 3) {
99
+ // Updates the `data` property of the Stability Index node
100
+ // so that the original node data can also be inferred
101
+ node.data = {
102
+ // The 2nd match should be the group that matches the Stability Index
103
+ index: matches[1],
104
+ // The 3rd match should be the group containing all the remaining text
105
+ // which is used as a description (we trim it to an one liner)
106
+ description: matches[2].replace(/\n/g, ' ').trim(),
107
+ };
108
+
109
+ // Adds the Stability Index metadata to the current Metadata entry
110
+ if (metadata) {
111
+ metadata.stability = node;
112
+ }
113
+ }
114
+
115
+ return [SKIP];
116
+ };
117
+
118
+ /**
119
+ * Helper function to parse YAML from a node
120
+ * @param {import('@types/mdast').Blockquote} node The blockquote node
121
+ * @param {Object} metadata The metadata object to update
122
+ */
123
+ export const visitYAML = (node, metadata) => {
124
+ const yaml = extractYamlContent(node);
125
+ const meta = parseYAMLIntoMetadata(yaml);
126
+
127
+ Object.entries(meta).forEach(([key, value]) => {
128
+ if (Array.isArray(metadata[key])) {
129
+ return metadata[key].push(...value);
130
+ }
131
+
132
+ metadata[key] = value;
133
+ });
134
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ import yaml from 'yaml';
4
+
5
+ import { QUERIES } from '../../../utils/queries/index.mjs';
6
+
7
+ /**
8
+ * Extracts raw YAML content from a node
9
+ *
10
+ * @param {import('mdast').Node} node A HTML node containing the YAML content
11
+ * @returns {string} The extracted raw YAML content
12
+ */
13
+ export const extractYamlContent = node => {
14
+ return node.value.replace(
15
+ QUERIES.yamlInnerContent,
16
+ // Either capture a YAML multinline block, or a simple single-line YAML block
17
+ (_, simple, yaml) => simple || yaml
18
+ );
19
+ };
20
+
21
+ /**
22
+ * Normalizes YAML syntax by fixing some non-cool formatted properties of the
23
+ * docs schema
24
+ *
25
+ * @param {string} yamlContent The raw YAML content to normalize
26
+ * @returns {string} The normalized YAML content
27
+ */
28
+ export const normalizeYamlSyntax = yamlContent => {
29
+ return yamlContent
30
+ .replace('introduced_in=', 'introduced_in: ')
31
+ .replace('source_link=', 'source_link: ')
32
+ .replace('type=', 'type: ')
33
+ .replace('name=', 'name: ')
34
+ .replace('llm_description=', 'llm_description: ')
35
+ .replace(/^[\r\n]+|[\r\n]+$/g, ''); // Remove initial and final line breaks
36
+ };
37
+
38
+ /**
39
+ * Parses Markdown YAML source into a JavaScript object containing all the metadata
40
+ * (this is forwarded to the parser so it knows what to do with said metadata)
41
+ *
42
+ * @param {string} yamlString The YAML string to be parsed
43
+ * @returns {import('../types').YAMLProperties} The parsed YAML metadata
44
+ */
45
+ export const parseYAMLIntoMetadata = yamlString => {
46
+ const normalizedYaml = normalizeYamlSyntax(yamlString);
47
+
48
+ // Ensures that the parsed YAML is an object, because even if it is not
49
+ // i.e. a plain string or an array, it will simply not result into anything
50
+ let parsedYaml = yaml.parse(normalizedYaml);
51
+ // Ensure that only Objects get parsed on Object.keys(), since some `<!--`
52
+ // comments, might be just plain strings and not even a valid YAML metadata
53
+ if (typeof parsedYaml === 'string') {
54
+ return { tags: [parsedYaml] };
55
+ }
56
+
57
+ return parsedYaml;
58
+ };
@@ -0,0 +1,129 @@
1
+ import type { publicGenerators, allGenerators } from './index.mjs';
2
+
3
+ declare global {
4
+ // Public generators exposed to the CLI
5
+ export type AvailableGenerators = typeof publicGenerators;
6
+
7
+ // All generators including internal ones (metadata, jsx-ast, ast-js)
8
+ export type AllGenerators = typeof allGenerators;
9
+
10
+ /**
11
+ * ParallelWorker interface for distributing work across Node.js worker threads.
12
+ * Streams results as chunks complete, enabling pipeline parallelism.
13
+ */
14
+ export interface ParallelWorker {
15
+ /**
16
+ * Processes items in parallel across worker threads and yields results
17
+ * as each chunk completes. Enables downstream processing to begin
18
+ * while upstream chunks are still being processed.
19
+ *
20
+ * @param items - Items to process (determines chunk distribution)
21
+ * @param opts - Additional options to pass to workers
22
+ * @yields Each chunk's results as they complete
23
+ */
24
+ stream<T, R>(
25
+ items: T[],
26
+ opts?: Record<string, unknown>
27
+ ): AsyncGenerator<R[], void, unknown>;
28
+ }
29
+
30
+ export interface ParallelTaskOptions {
31
+ /** Resolved import specifier the worker loads the generator from */
32
+ generatorSpecifier: string;
33
+ input: unknown[];
34
+ itemIndices: number[];
35
+ }
36
+
37
+ /**
38
+ * Type for the generate function of a generator
39
+ * @template I - Input type
40
+ * @template O - Output type (can be AsyncGenerator or Promise)
41
+ */
42
+ export type Generate<I, O> = (
43
+ input: I,
44
+ worker: ParallelWorker
45
+ ) => O extends AsyncGenerator ? O : Promise<O>;
46
+
47
+ /**
48
+ * Type for the optional processChunk function of a generator
49
+ * @template I - Input type
50
+ * @template O - Output type
51
+ * @template D - Dependencies type
52
+ */
53
+ export type ProcessChunk<I, O, D = {}> = (
54
+ slicedInput: I[],
55
+ itemIndices: number[],
56
+ dependencies: D
57
+ ) => Promise<O>;
58
+
59
+ export type GeneratorMetadata<
60
+ C extends any,
61
+ G extends Generate<any, any>,
62
+ P extends ProcessChunk<any, any, any> | undefined = undefined,
63
+ > = {
64
+ readonly defaultConfiguration: C;
65
+
66
+ // The name of the Generator. Must match the Key in AllGenerators
67
+ name: keyof AllGenerators;
68
+
69
+ description: string;
70
+
71
+ hasParallelProcessor?: boolean;
72
+
73
+ /**
74
+ * The immediate generator that this generator depends on.
75
+ * For example, the `html` generator depends on the `react` generator.
76
+ *
77
+ * If a given generator has no "before" generator, it will be considered a top-level
78
+ * generator, and run in parallel.
79
+ *
80
+ * Assume you pass to the `createGenerator`: ['json', 'html'] as the generators,
81
+ * this means both the 'json' and the 'html' generators will be executed and generate their
82
+ * own outputs in parallel. If the 'html' generator depends on the 'react' generator, then
83
+ * the 'react' generator will be executed first, then the 'html' generator.
84
+ *
85
+ * But both 'json' and 'html' generators will be executed in parallel.
86
+ *
87
+ * If you pass `createGenerator` with ['react', 'html'], the 'react' generator will be executed first,
88
+ * as it is a top level generator and then the 'html' generator would be executed after the 'react' generator.
89
+ *
90
+ * The 'ast' generator is the top-level parser for markdown files. It has no dependencies.
91
+ *
92
+ * The `ast-js` generator is the top-level parser for JavaScript files. It
93
+ * passes the ASTs for any JavaScript files given in the input.
94
+ *
95
+ * Declared as an import specifier (e.g. `@doc-kit/core/metadata`),
96
+ * so a dependency may live in any installed package.
97
+ */
98
+ dependsOn: string | undefined;
99
+
100
+ /**
101
+ * Generators are abstract and the different generators have different sort of inputs and outputs.
102
+ * For example, a MDX generator would take the raw AST and output MDX with React Components;
103
+ * Whereas a JSON generator would take the raw AST and output JSON;
104
+ * Then a React generator could receive either the raw AST or the MDX output and output React Components.
105
+ * (depending if they support such I/O)
106
+ *
107
+ * Hence you can combine different generators to achieve different outputs.
108
+ */
109
+ generate: G;
110
+
111
+ /**
112
+ * Optional method for chunk-level parallelization using real worker threads.
113
+ * Called by chunk-worker.mjs when processing items in parallel.
114
+ *
115
+ * Generators that implement this method can have their work distributed
116
+ * across multiple worker threads for true parallel processing.
117
+ *
118
+ * Input is automatically sliced to only include items at the specified indices,
119
+ * reducing serialization overhead. The itemIndices are remapped to 0-based
120
+ * indices into the sliced array.
121
+ *
122
+ * @param slicedInput - Sliced input containing only items for this chunk
123
+ * @param itemIndices - Array of 0-based indices into slicedInput
124
+ * @param dependencies - Generator options (without worker, which isn't serializable)
125
+ * @returns Array of results for the processed items
126
+ */
127
+ processChunk?: P;
128
+ };
129
+ }
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ import { createCache } from './caching.mjs';
4
+ import {
5
+ loadGenerators,
6
+ resolveGeneratorSpecifier,
7
+ } from './generators/loader.mjs';
8
+ import logger from './logger/index.mjs';
9
+ import createWorkerPool from './threading/index.mjs';
10
+ import createParallelWorker from './threading/parallel.mjs';
11
+ import { isAsyncIterable } from './utils/misc.mjs';
12
+
13
+ const generatorsLogger = logger.child('generators');
14
+
15
+ /**
16
+ * Creates a generator orchestration system that manages the execution of
17
+ * documentation generators in dependency order, with support for parallel
18
+ * processing and streaming results.
19
+ */
20
+ const createGenerator = () => {
21
+ // Owns all caching: generator results, stream collection, and the
22
+ // consumer-count bookkeeping that drives eviction.
23
+ const cache = createCache();
24
+
25
+ /** @type {import('piscina').Piscina} */
26
+ let pool;
27
+
28
+ /**
29
+ * Gets the collected input from a dependency generator.
30
+ *
31
+ * @param {string | undefined} dependsOn - Resolved dependency specifier
32
+ * @returns {Promise<unknown>}
33
+ */
34
+ const getDependencyInput = async dependsOn => {
35
+ if (!dependsOn) {
36
+ return undefined;
37
+ }
38
+
39
+ return cache.consume(dependsOn);
40
+ };
41
+
42
+ /**
43
+ * Schedules a generator and its dependencies for execution.
44
+ *
45
+ * @param {string} specifier - Resolved generator specifier to schedule
46
+ * @param {Map<string, GeneratorMetadata>} generators - Loaded generators
47
+ * @param {import('./utils/configuration/types').Configuration} configuration - Runtime options
48
+ */
49
+ const scheduleGenerator = (specifier, generators, configuration) => {
50
+ if (cache.has(specifier)) {
51
+ return;
52
+ }
53
+
54
+ const generator = generators.get(specifier);
55
+ const { name, generate, hasParallelProcessor } = generator;
56
+
57
+ const dependsOn =
58
+ generator.dependsOn && resolveGeneratorSpecifier(generator.dependsOn);
59
+
60
+ // Schedule dependency first
61
+ if (dependsOn && !cache.has(dependsOn)) {
62
+ scheduleGenerator(dependsOn, generators, configuration);
63
+ }
64
+
65
+ generatorsLogger.debug(`Scheduling "${name}"`, {
66
+ dependsOn: dependsOn || 'none',
67
+ streaming: hasParallelProcessor,
68
+ });
69
+
70
+ // Schedule the generator
71
+ cache.store(
72
+ specifier,
73
+ (async () => {
74
+ const dependencyInput = await getDependencyInput(dependsOn);
75
+
76
+ generatorsLogger.debug(`Starting "${name}"`);
77
+
78
+ // Create parallel worker for streaming generators
79
+ const worker = hasParallelProcessor
80
+ ? createParallelWorker(specifier, generator, pool, configuration)
81
+ : Promise.resolve(null);
82
+
83
+ const result = await generate(dependencyInput, await worker);
84
+
85
+ // For streaming generators, "Completed" is logged when the cache
86
+ // finishes collecting, not here when the generator returns
87
+ if (!isAsyncIterable(result)) {
88
+ generatorsLogger.debug(`Completed "${name}"`);
89
+ }
90
+
91
+ return result;
92
+ })()
93
+ );
94
+ };
95
+
96
+ /**
97
+ * Runs all requested generators with their dependencies.
98
+ *
99
+ * @param {import('./utils/configuration/types').Configuration} configuration - Runtime options
100
+ * @returns {Promise<unknown[]>} Results of all requested generators
101
+ */
102
+ const runGenerators = async configuration => {
103
+ const { target, threads } = configuration;
104
+
105
+ // Resolve shorthand names and load the full dependency closure up front,
106
+ // so scheduling below is fully synchronous.
107
+ const targets = target.map(resolveGeneratorSpecifier);
108
+ const generators = await loadGenerators(targets);
109
+
110
+ generatorsLogger.debug(`Starting pipeline`, {
111
+ generators: targets.join(', '),
112
+ threads,
113
+ });
114
+
115
+ // Compute consumer counts up front so dependencies can be evicted as soon
116
+ // as their last consumer runs (must be ready before any generator starts).
117
+ cache.populateConsumerCounts(targets, specifier => {
118
+ const { dependsOn } = generators.get(specifier);
119
+
120
+ return dependsOn && resolveGeneratorSpecifier(dependsOn);
121
+ });
122
+
123
+ // Create worker pool
124
+ pool = createWorkerPool(threads);
125
+
126
+ // Schedule all generators
127
+ for (const specifier of targets) {
128
+ scheduleGenerator(specifier, generators, configuration);
129
+ }
130
+
131
+ // Start all collections in parallel (don't await sequentially). Consuming
132
+ // through the shared path lets the final read also trigger eviction.
133
+ const results = await Promise.all(
134
+ targets.map(specifier => cache.consume(specifier))
135
+ );
136
+
137
+ await pool.destroy();
138
+
139
+ return results;
140
+ };
141
+
142
+ return { runGenerators };
143
+ };
144
+
145
+ export default createGenerator;
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Numeric log level definitions.
5
+ */
6
+ export const LogLevel = {
7
+ debug: 10,
8
+ info: 20,
9
+ warn: 30,
10
+ error: 40,
11
+ fatal: 50,
12
+ };
13
+
14
+ /**
15
+ * Maps log level numbers to their string tags.
16
+ */
17
+ export const levelTags = {
18
+ [LogLevel.debug]: 'DEBUG',
19
+ [LogLevel.info]: 'INFO',
20
+ [LogLevel.warn]: 'WARN',
21
+ [LogLevel.error]: 'ERROR',
22
+ [LogLevel.fatal]: 'FATAL',
23
+ };
24
+
25
+ /**
26
+ * Maps log level numbers to CLI color names.
27
+ */
28
+ export const levelToColorMap = {
29
+ [LogLevel.debug]: 'blue',
30
+ [LogLevel.info]: 'green',
31
+ [LogLevel.warn]: 'yellow',
32
+ [LogLevel.error]: 'magenta',
33
+ [LogLevel.fatal]: 'red',
34
+ };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ import { createLogger } from './logger.mjs';
4
+ import { transports } from './transports/index.mjs';
5
+
6
+ /**
7
+ * @typedef {ReturnType<typeof createLogger>} LoggerInstance
8
+ */
9
+
10
+ /**
11
+ * Creates a new logger instance with the specified transport.
12
+ *
13
+ * @param {string} [transportName='console'] - Name of the transport to use.
14
+ * @returns {LoggerInstance}
15
+ */
16
+ export const Logger = (transportName = 'console') => {
17
+ const transport = transports[transportName];
18
+
19
+ if (!transport) {
20
+ throw new Error(`Transport '${transportName}' not found.`);
21
+ }
22
+
23
+ return createLogger(transport);
24
+ };
25
+
26
+ // Default logger instance using console transport
27
+ export default Logger();