@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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/package.json +88 -0
- package/shiki.config.mjs +50 -0
- package/src/caching.mjs +183 -0
- package/src/generators/ast/README.md +8 -0
- package/src/generators/ast/constants.mjs +1 -0
- package/src/generators/ast/generate.mjs +125 -0
- package/src/generators/ast/index.mjs +20 -0
- package/src/generators/ast/types.d.ts +7 -0
- package/src/generators/ast-js/README.md +8 -0
- package/src/generators/ast-js/generate.mjs +56 -0
- package/src/generators/ast-js/index.mjs +24 -0
- package/src/generators/ast-js/types.d.ts +5 -0
- package/src/generators/index.mjs +48 -0
- package/src/generators/json-simple/README.md +9 -0
- package/src/generators/json-simple/generate.mjs +33 -0
- package/src/generators/json-simple/index.mjs +23 -0
- package/src/generators/json-simple/types.d.ts +6 -0
- package/src/generators/loader.mjs +96 -0
- package/src/generators/metadata/README.md +8 -0
- package/src/generators/metadata/constants.mjs +68 -0
- package/src/generators/metadata/generate.mjs +40 -0
- package/src/generators/metadata/index.mjs +21 -0
- package/src/generators/metadata/maps/builtin.json +16 -0
- package/src/generators/metadata/maps/mdn.json +1114 -0
- package/src/generators/metadata/types.d.ts +145 -0
- package/src/generators/metadata/utils/parse.mjs +160 -0
- package/src/generators/metadata/utils/resolveTypes.mjs +204 -0
- package/src/generators/metadata/utils/slugger.mjs +39 -0
- package/src/generators/metadata/utils/transformers.mjs +109 -0
- package/src/generators/metadata/utils/visitors.mjs +134 -0
- package/src/generators/metadata/utils/yaml.mjs +58 -0
- package/src/generators/types.d.ts +129 -0
- package/src/generators.mjs +145 -0
- package/src/logger/constants.mjs +34 -0
- package/src/logger/index.mjs +27 -0
- package/src/logger/logger.mjs +177 -0
- package/src/logger/transports/console.mjs +54 -0
- package/src/logger/transports/github.mjs +42 -0
- package/src/logger/transports/index.mjs +11 -0
- package/src/logger/types.d.ts +29 -0
- package/src/logger/utils/colors.mjs +18 -0
- package/src/logger/utils/time.mjs +21 -0
- package/src/parsers/markdown.mjs +49 -0
- package/src/parsers/types.d.ts +7 -0
- package/src/threading/chunk-worker.mjs +23 -0
- package/src/threading/index.mjs +27 -0
- package/src/threading/parallel.mjs +140 -0
- package/src/utils/array.mjs +7 -0
- package/src/utils/configuration/index.mjs +263 -0
- package/src/utils/configuration/templates.mjs +21 -0
- package/src/utils/configuration/types.d.ts +64 -0
- package/src/utils/file.mjs +20 -0
- package/src/utils/generators.mjs +71 -0
- package/src/utils/highlighter.mjs +198 -0
- package/src/utils/html-minifier.mjs +12 -0
- package/src/utils/loaders.mjs +48 -0
- package/src/utils/misc.mjs +72 -0
- package/src/utils/queries/constants.mjs +3 -0
- package/src/utils/queries/index.mjs +93 -0
- package/src/utils/queries/utils.mjs +45 -0
- package/src/utils/remark.mjs +97 -0
- package/src/utils/signature/constants.mjs +12 -0
- package/src/utils/signature/parseList.mjs +133 -0
- package/src/utils/signature/parseSignature.mjs +208 -0
- package/src/utils/signature/types.d.ts +66 -0
- package/src/utils/swc.mjs +34 -0
- package/src/utils/type-annotations/hast.mjs +115 -0
- package/src/utils/type-annotations/mdast.mjs +75 -0
- package/src/utils/type-annotations/remark.mjs +24 -0
- package/src/utils/type-annotations/syntax.mjs +188 -0
- package/src/utils/unist.mjs +75 -0
- package/src/utils/url.mjs +27 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { cpus } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { isMainThread } from 'node:worker_threads';
|
|
7
|
+
|
|
8
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
9
|
+
import { coerce } from 'semver';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
loadGenerators,
|
|
13
|
+
resolveGeneratorSpecifier,
|
|
14
|
+
} from '../../generators/loader.mjs';
|
|
15
|
+
import logger from '../../logger/index.mjs';
|
|
16
|
+
import { parseChangelog, parseIndex } from '../../parsers/markdown.mjs';
|
|
17
|
+
import { enforceArray } from '../array.mjs';
|
|
18
|
+
import { leftHandAssign } from '../generators.mjs';
|
|
19
|
+
import { deepMerge } from '../misc.mjs';
|
|
20
|
+
|
|
21
|
+
const configExplorer = cosmiconfig('doc-kit');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The name of the project being documented, from the manifest in the working
|
|
25
|
+
* directory. Generators use it for titles, logos, and templated text.
|
|
26
|
+
*
|
|
27
|
+
* @returns {string | undefined}
|
|
28
|
+
*/
|
|
29
|
+
const detectProject = () => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync('package.json', 'utf-8')).name;
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get's the default configuration for the loaded generators
|
|
39
|
+
*
|
|
40
|
+
* @param {Map<string, GeneratorMetadata>} generators - Loaded generators
|
|
41
|
+
* @param {Partial<import('./types').Configuration>} config - The user configuration
|
|
42
|
+
*/
|
|
43
|
+
export const getDefaultConfig = (generators, config) =>
|
|
44
|
+
[...generators.values()].reduce(
|
|
45
|
+
(acc, generator) => {
|
|
46
|
+
acc[generator.name] =
|
|
47
|
+
'defaultConfiguration' in generator
|
|
48
|
+
? typeof generator.defaultConfiguration === 'function'
|
|
49
|
+
? generator.defaultConfiguration(config)
|
|
50
|
+
: generator.defaultConfiguration
|
|
51
|
+
: {};
|
|
52
|
+
|
|
53
|
+
return acc;
|
|
54
|
+
},
|
|
55
|
+
/** @type {import('./types').Configuration} */ ({
|
|
56
|
+
global: {
|
|
57
|
+
project: detectProject() ?? 'API Docs',
|
|
58
|
+
version: process.version,
|
|
59
|
+
minify: true,
|
|
60
|
+
ref: 'HEAD',
|
|
61
|
+
// Without release history there is nothing to build a version picker
|
|
62
|
+
// from, so generators render single-version output.
|
|
63
|
+
changelog: [],
|
|
64
|
+
pathsToCopy: ['assets', 'public', 'static'],
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// The number of wasm memory instances is severely limited on
|
|
68
|
+
// riscv64 with sv39. Running multiple generators that use wasm in
|
|
69
|
+
// parallel could cause failures to allocate new wasm instance.
|
|
70
|
+
// See also https://github.com/nodejs/node/pull/60591
|
|
71
|
+
threads: process.arch === 'riscv64' ? 1 : cpus().length,
|
|
72
|
+
chunkSize: 10,
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolves an `extends` entry of a configuration file into an importable
|
|
78
|
+
* URL: relative paths resolve against the configuration file, anything else
|
|
79
|
+
* resolves as a package import specifier (e.g. `@node-core/doc-kit/config`).
|
|
80
|
+
*
|
|
81
|
+
* @param {string} specifier - The `extends` entry
|
|
82
|
+
* @param {string} configFilePath - The configuration file it appears in
|
|
83
|
+
* @returns {string} A `file:` URL to import
|
|
84
|
+
*/
|
|
85
|
+
const resolveConfigExtends = (specifier, configFilePath) => {
|
|
86
|
+
if (specifier.startsWith('.') || isAbsolute(specifier)) {
|
|
87
|
+
return pathToFileURL(resolve(dirname(configFilePath), specifier)).href;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return pathToFileURL(createRequire(configFilePath).resolve(specifier)).href;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Loads an explicit configuration file or searches for one using cosmiconfig.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} [filePath] - The path to an explicit configuration file
|
|
97
|
+
* @returns {Promise<Partial<import('./types').Configuration>>} The loaded configuration object, or an empty object if none is found
|
|
98
|
+
*/
|
|
99
|
+
export const loadConfigFile = async filePath => {
|
|
100
|
+
const result = filePath
|
|
101
|
+
? await configExplorer.load(filePath)
|
|
102
|
+
: await configExplorer.search();
|
|
103
|
+
|
|
104
|
+
if (!result) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let { extends: presets, ...config } = result.config ?? {};
|
|
109
|
+
|
|
110
|
+
for (const preset of enforceArray(presets ?? []).toReversed()) {
|
|
111
|
+
const module = await import(resolveConfigExtends(preset, result.filepath));
|
|
112
|
+
|
|
113
|
+
config = deepMerge(module.default ?? module, config);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return config;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Transforms configuration values that need async processing or coercion.
|
|
121
|
+
* Only processes values that haven't been transformed yet (strings for changelog/index, non-coerced versions).
|
|
122
|
+
*
|
|
123
|
+
* @param {import('./types').GlobalConfiguration} value
|
|
124
|
+
* @returns {Promise<import('./types').GlobalConfiguration>}
|
|
125
|
+
*/
|
|
126
|
+
const transformConfig = async value => {
|
|
127
|
+
// Only coerce if it's a string (not already coerced)
|
|
128
|
+
if (value.version && typeof value.version === 'string') {
|
|
129
|
+
value.version = coerce(value.version);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Only parse if it's not already parsed
|
|
133
|
+
if (value.changelog && !Array.isArray(value.changelog)) {
|
|
134
|
+
value.changelog = await parseChangelog(value.changelog);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Only parse if it's not already parsed
|
|
138
|
+
if (value.index && !Array.isArray(value.index)) {
|
|
139
|
+
value.index = await parseIndex(value.index);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return value;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Converts CLI options into a config
|
|
147
|
+
* @param {import('../../../bin/commands/generate.mjs').CLIOptions} options
|
|
148
|
+
* @returns {import('./types').Configuration}
|
|
149
|
+
*/
|
|
150
|
+
export const createConfigFromCLIOptions = options => ({
|
|
151
|
+
global: {
|
|
152
|
+
input: options.input,
|
|
153
|
+
ignore: options.ignore,
|
|
154
|
+
output: options.output,
|
|
155
|
+
minify: options.minify,
|
|
156
|
+
ref: options.gitRef,
|
|
157
|
+
version: options.version,
|
|
158
|
+
changelog: options.changelog,
|
|
159
|
+
index: options.index,
|
|
160
|
+
},
|
|
161
|
+
metadata: {
|
|
162
|
+
typeMap: options.typeMap,
|
|
163
|
+
},
|
|
164
|
+
target: options.target,
|
|
165
|
+
threads: options.threads,
|
|
166
|
+
chunkSize: options.chunkSize,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Asserts that the resolved configuration has everything needed to run:
|
|
171
|
+
* at least one generator `target` and an `input` to read source files from.
|
|
172
|
+
* These may come from CLI flags or a config file; by this point both sources
|
|
173
|
+
* have been merged, so we validate the result rather than the raw options.
|
|
174
|
+
*
|
|
175
|
+
* @param {import('./types').Configuration} config - The merged configuration
|
|
176
|
+
*/
|
|
177
|
+
export const assertRunnableOptions = config => {
|
|
178
|
+
if (!config.target || !config.global?.input) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Both a `target` and an `input` must be provided, either via ' +
|
|
181
|
+
'`--target`/`--input` or a configuration file. ' +
|
|
182
|
+
'Run `doc-kit generate --help` for usage.'
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Creates a complete run configuration by merging config file, user options, and defaults.
|
|
189
|
+
* Processes and validates configuration values including version coercion, changelog parsing,
|
|
190
|
+
* and constraint enforcement for threads and chunk size.
|
|
191
|
+
*
|
|
192
|
+
* @param {import('../../../bin/commands/generate.mjs').CLIOptions} options - User-provided configuration options
|
|
193
|
+
* @returns {Promise<import('./types').Configuration>} The configuration
|
|
194
|
+
*/
|
|
195
|
+
export const createRunConfiguration = async options => {
|
|
196
|
+
const config = await loadConfigFile(options.configFile);
|
|
197
|
+
config.target &&= enforceArray(config.target);
|
|
198
|
+
|
|
199
|
+
// Resolve user configuration first so dynamic defaults can use it
|
|
200
|
+
const cliConfig = createConfigFromCLIOptions(options);
|
|
201
|
+
const intermediate = deepMerge(config, cliConfig);
|
|
202
|
+
|
|
203
|
+
// Resolve shorthand targets into import specifiers, then load the requested
|
|
204
|
+
// generators (and their dependency closure) so their defaults can be applied
|
|
205
|
+
intermediate.target &&= intermediate.target.map(resolveGeneratorSpecifier);
|
|
206
|
+
const generators = await loadGenerators(intermediate.target ?? []);
|
|
207
|
+
|
|
208
|
+
const merged = deepMerge(
|
|
209
|
+
getDefaultConfig(generators, intermediate),
|
|
210
|
+
intermediate
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// These need to be coerced
|
|
214
|
+
merged.threads = Math.max(merged.threads, 1);
|
|
215
|
+
merged.chunkSize = Math.max(merged.chunkSize, 1);
|
|
216
|
+
|
|
217
|
+
if (process.arch === 'riscv64' && merged.threads > 1) {
|
|
218
|
+
logger.warn(
|
|
219
|
+
`Using ${merged.threads} threads might cause failures when ` +
|
|
220
|
+
'allocating wasm memory due to insufficient virtual address space ' +
|
|
221
|
+
'on riscv64 with sv39. Please consider using only a single thread.'
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Transform global config if it wasn't already done
|
|
226
|
+
await transformConfig(merged.global);
|
|
227
|
+
|
|
228
|
+
// Now assign to each generator config (they inherit from global)
|
|
229
|
+
await Promise.all(
|
|
230
|
+
[...generators.values()].map(async ({ name }) => {
|
|
231
|
+
const value = merged[name];
|
|
232
|
+
|
|
233
|
+
// Transform generator-specific overrides
|
|
234
|
+
await transformConfig(value);
|
|
235
|
+
|
|
236
|
+
// Assign from global (this populates missing values from global)
|
|
237
|
+
leftHandAssign(value, merged.global);
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return merged;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/** @type {import('./types').Configuration} */
|
|
245
|
+
let config;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Configuration setter
|
|
249
|
+
* @param {import('./types').Configuration | import('../../../bin/commands/generate.mjs').CLIOptions} options
|
|
250
|
+
* @returns {Promise<import('./types').Configuration>}
|
|
251
|
+
*/
|
|
252
|
+
export const setConfig = async options =>
|
|
253
|
+
(config = isMainThread ? await createRunConfiguration(options) : options);
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Configuration getter
|
|
257
|
+
* @template {keyof import('./types').Configuration} T
|
|
258
|
+
* @param {T} generator
|
|
259
|
+
* @returns {T extends keyof import('./types').Configuration ? import('./types').Configuration[T] : import('./types').Configuration}
|
|
260
|
+
*/
|
|
261
|
+
const getConfig = generator => (generator ? config[generator] : config);
|
|
262
|
+
|
|
263
|
+
export default getConfig;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// This is the CHANGELOG to be consumed to generate a list of all major versions
|
|
2
|
+
export const CHANGELOG_URL =
|
|
3
|
+
'https://raw.githubusercontent.com/{repository}/{ref}/CHANGELOG.md';
|
|
4
|
+
|
|
5
|
+
// This is the Base URL for viewing a file within GitHub UI
|
|
6
|
+
export const GITHUB_BLOB_URL = 'https://github.com/{repository}/blob/{ref}/';
|
|
7
|
+
|
|
8
|
+
// This is the API docs base URL for editing a file on GitHub UI
|
|
9
|
+
export const GITHUB_EDIT_URL = 'https://github.com/{repository}/edit/{ref}';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Populate a template string based on a configuration
|
|
13
|
+
* @param {string} template The template string
|
|
14
|
+
* @param {import("./types").GlobalConfiguration} config The configuration
|
|
15
|
+
* @returns {string} the populated string
|
|
16
|
+
*/
|
|
17
|
+
export const populate = (template, config) =>
|
|
18
|
+
template.replace(
|
|
19
|
+
/\{(\w+)(?:\|(\w+))?\}/g,
|
|
20
|
+
(match, path, fallback) => config[path] ?? fallback ?? match
|
|
21
|
+
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { SemVer } from 'semver';
|
|
2
|
+
import type { ReleaseEntry } from '../../parsers/types';
|
|
3
|
+
|
|
4
|
+
export type Configuration = {
|
|
5
|
+
global: GlobalConfiguration;
|
|
6
|
+
|
|
7
|
+
// A list of generators to be used in the API doc generation process;
|
|
8
|
+
// This is considered a "sorted" list of generators, in the sense that
|
|
9
|
+
// if the last entry of this list contains a generated value, we will return
|
|
10
|
+
// the value of the last generator in the list, if any.
|
|
11
|
+
target: Array<keyof AvailableGenerators>;
|
|
12
|
+
|
|
13
|
+
// The number of threads the process is allowed to use
|
|
14
|
+
threads: number;
|
|
15
|
+
|
|
16
|
+
// Number of items to process per worker thread
|
|
17
|
+
chunkSize: number;
|
|
18
|
+
} & {
|
|
19
|
+
[K in keyof AllGenerators]: GlobalConfiguration &
|
|
20
|
+
AllGenerators[K]['defaultConfiguration'];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type GlobalConfiguration = {
|
|
24
|
+
// The name of the project being documented, used for titles, logos, and
|
|
25
|
+
// templated text (defaults to the `name` in the working directory's
|
|
26
|
+
// `package.json`)
|
|
27
|
+
project: string;
|
|
28
|
+
|
|
29
|
+
// The repository (`owner/name`), used for source and edit links; when
|
|
30
|
+
// omitted, repository-specific UI (e.g. the GitHub link) is omitted
|
|
31
|
+
repository?: string;
|
|
32
|
+
|
|
33
|
+
// The path to the input source files. This parameter accepts globs and can
|
|
34
|
+
// be a glob when passed to a generator.
|
|
35
|
+
input: string | string[];
|
|
36
|
+
|
|
37
|
+
// The path or glob patterns used to ignore files from the input source files.
|
|
38
|
+
ignore: string | string[];
|
|
39
|
+
|
|
40
|
+
// The path used to output generated files, this is to be considered
|
|
41
|
+
// the base path that any generator will use for generating files
|
|
42
|
+
// This parameter accepts globs but when passed to generators will contain
|
|
43
|
+
// the already resolved absolute path to the output folder
|
|
44
|
+
output: string;
|
|
45
|
+
|
|
46
|
+
// Whether or not the minify the output, in whatever form it may be
|
|
47
|
+
minify: boolean;
|
|
48
|
+
|
|
49
|
+
// Target Node.js version for the generation of the API docs
|
|
50
|
+
version: SemVer;
|
|
51
|
+
|
|
52
|
+
// A list of all major versions and their respective release information
|
|
53
|
+
changelog: Array<ReleaseEntry>;
|
|
54
|
+
|
|
55
|
+
// A list of all the titles of all the documentation files
|
|
56
|
+
index: Array<{ section: string; api: string }>;
|
|
57
|
+
|
|
58
|
+
// The base URL of the published site; templates referencing `{baseURL}`
|
|
59
|
+
// (e.g. sitemap and llms-txt page URLs) need it to produce absolute URLs
|
|
60
|
+
baseURL?: string | URL;
|
|
61
|
+
|
|
62
|
+
// Git ref (i.e. HEAD)
|
|
63
|
+
ref: string;
|
|
64
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the input string with the `ext` extension, replacing any pre-existing extension
|
|
6
|
+
* @param {string} str
|
|
7
|
+
* @param {string} ext
|
|
8
|
+
*/
|
|
9
|
+
export const withExt = (str, ext) =>
|
|
10
|
+
`${str.replace(/\.[0-9a-z]+$/i, '')}${ext ? `.${ext}` : ''}`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Writes a file, recursively
|
|
14
|
+
*
|
|
15
|
+
* @type {typeof fs.writeFile}
|
|
16
|
+
*/
|
|
17
|
+
export const writeFile = (file, ...args) =>
|
|
18
|
+
fs
|
|
19
|
+
.mkdir(dirname(file), { recursive: true })
|
|
20
|
+
.then(() => fs.writeFile(file, ...args));
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { coerce, major } from 'semver';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Groups all the API metadata nodes by module (`api` property) so that we can process each different file
|
|
7
|
+
* based on the module it belongs to.
|
|
8
|
+
*
|
|
9
|
+
* @param {Array<import('../generators/metadata/types').MetadataEntry>} nodes The API metadata Nodes to be grouped
|
|
10
|
+
*/
|
|
11
|
+
export const groupNodesByModule = nodes => {
|
|
12
|
+
return Map.groupBy(nodes, node => node.api);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses the SemVer string into a Node.js-alike version
|
|
17
|
+
*
|
|
18
|
+
* @param {import('semver').SemVer} version The version to be parsed
|
|
19
|
+
*/
|
|
20
|
+
export const getVersionFromSemVer = version =>
|
|
21
|
+
version.minor === 0
|
|
22
|
+
? `${version.major}.x`
|
|
23
|
+
: `${version.major}.${version.minor}.x`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @TODO: This should not be necessary, and indicates errors within the API docs
|
|
27
|
+
* @TODO: Hookup into a future Validation/Linting API
|
|
28
|
+
*
|
|
29
|
+
* This is a safe fallback to ensure that we always have a SemVer compatible version
|
|
30
|
+
* even if the input is not a valid SemVer string
|
|
31
|
+
*
|
|
32
|
+
* @param {string|import('semver').SemVer} version SemVer compatible version (maybe)
|
|
33
|
+
* @returns {import('semver').SemVer} SemVer compatible version
|
|
34
|
+
*/
|
|
35
|
+
export const coerceSemVer = version => {
|
|
36
|
+
const coercedVersion = coerce(version);
|
|
37
|
+
|
|
38
|
+
if (coercedVersion === null) {
|
|
39
|
+
// @TODO: Linter to complain about invalid SemVer strings
|
|
40
|
+
return coerce('0.0.0-REPLACEME');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return coercedVersion;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets compatible versions for an entry
|
|
48
|
+
*
|
|
49
|
+
* @param {string | import('semver').SemVer} introduced
|
|
50
|
+
* @param {Array<import('../parsers/types').ReleaseEntry>} releases
|
|
51
|
+
* @returns {Array<import('../parsers/types').ReleaseEntry>}
|
|
52
|
+
*/
|
|
53
|
+
export const getCompatibleVersions = (introduced, releases) => {
|
|
54
|
+
const coercedMajor = major(coerceSemVer(introduced));
|
|
55
|
+
// All Node.js versions that support the current API; If there's no "introduced_at" field,
|
|
56
|
+
// we simply show all versions, as we cannot pinpoint the exact version
|
|
57
|
+
return releases.filter(release => release.version.major >= coercedMajor);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Assigns properties from one or more source objects to the target object
|
|
62
|
+
* **without overwriting existing keys** in the target.
|
|
63
|
+
*
|
|
64
|
+
* Similar to `Object.assign`, but preserves the target's existing keys.
|
|
65
|
+
* The target object is mutated in place.
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} target - The object to assign properties to.
|
|
68
|
+
* @param {Object} source - The source object
|
|
69
|
+
*/
|
|
70
|
+
export const leftHandAssign = (target, source) =>
|
|
71
|
+
Object.keys(source).forEach(k => k in target || (target[k] = source[k]));
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { endianness } from 'node:os';
|
|
4
|
+
|
|
5
|
+
import createHighlighter from '@node-core/rehype-shiki';
|
|
6
|
+
import { h as createElement } from 'hastscript';
|
|
7
|
+
import { SKIP, visit } from 'unist-util-visit';
|
|
8
|
+
|
|
9
|
+
import shikiConfig from '../../shiki.config.mjs';
|
|
10
|
+
|
|
11
|
+
// This is what Remark will use as prefix within a <pre> className
|
|
12
|
+
// to attribute the current language of the <pre> element
|
|
13
|
+
const languagePrefix = 'language-';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a toolbar element with a language label and copy button.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} languageId - The language identifier for the code block.
|
|
19
|
+
* @returns {import('hast').Element} The toolbar element.
|
|
20
|
+
*/
|
|
21
|
+
function createToolbarElement(languageId) {
|
|
22
|
+
return createElement('div', { class: 'code-toolbar' }, [
|
|
23
|
+
createElement('span', { class: 'code-language' }, languageId),
|
|
24
|
+
createElement('button', { class: 'copy-button' }, 'copy'),
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Checks if the given node is a valid code element.
|
|
30
|
+
*
|
|
31
|
+
* @param {import('unist').Node} node - The node to be verified.
|
|
32
|
+
*
|
|
33
|
+
* @return {boolean} - True when it is a valid code element, false otherwise.
|
|
34
|
+
*/
|
|
35
|
+
function isCodeBlock(node) {
|
|
36
|
+
return Boolean(
|
|
37
|
+
node?.tagName === 'pre' && node?.children[0].tagName === 'code'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const highlighter = await createHighlighter({
|
|
42
|
+
// riscv64 with sv39 has limited virtual memory space, where creating
|
|
43
|
+
// too many (>20) wasm memory instances fails.
|
|
44
|
+
// https://github.com/nodejs/node/pull/60591
|
|
45
|
+
//
|
|
46
|
+
// The wasm highlighter is currently not compatible with big endian.
|
|
47
|
+
// https://github.com/nodejs/node/pull/62512#issuecomment-4243469950
|
|
48
|
+
wasm: process.arch !== 'riscv64' && endianness() === 'LE',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a HAST transformer for Shiki which is used for transforming our codeboxes
|
|
53
|
+
*
|
|
54
|
+
* @deprecated This is used only for the legacy-html generator, please use `@node-core/rehype-shiki` directly instead.
|
|
55
|
+
*
|
|
56
|
+
* @type {import('unified').Plugin}
|
|
57
|
+
*/
|
|
58
|
+
export default function rehypeShikiji() {
|
|
59
|
+
/**
|
|
60
|
+
* @param {import('hast').Root} tree - The HAST tree to be transformed.
|
|
61
|
+
*/
|
|
62
|
+
return function (tree) {
|
|
63
|
+
visit(tree, 'element', (node, index, parent) => {
|
|
64
|
+
// We only want to process <pre>...</pre> elements
|
|
65
|
+
if (!parent || index == null || node.tagName !== 'pre') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// We want the contents of the <pre> element, hence we attempt to get the first child
|
|
70
|
+
const preElement = node.children[0];
|
|
71
|
+
|
|
72
|
+
// If thereÄs nothing inside the <pre> element... What are we doing here?
|
|
73
|
+
if (!preElement || !preElement.properties) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Ensure that we're not visiting a <code> element but it's inner contents
|
|
78
|
+
// (keep iterating further down until we reach where we want)
|
|
79
|
+
if (preElement.type !== 'element' || preElement.tagName !== 'code') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get the <pre> element class names
|
|
84
|
+
const preClassNames = preElement.properties.className;
|
|
85
|
+
|
|
86
|
+
// The current classnames should be an array and it should have a length
|
|
87
|
+
if (typeof preClassNames !== 'object' || preClassNames.length === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// We want to retrieve the language class name from the class names
|
|
92
|
+
const codeLanguage = preClassNames.find(
|
|
93
|
+
c => typeof c === 'string' && c.startsWith(languagePrefix)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// If we didn't find any `language-` classname then we shouldn't highlight
|
|
97
|
+
if (typeof codeLanguage !== 'string') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Grabs the relevant alias/name of the language
|
|
102
|
+
const languageId = codeLanguage.slice(languagePrefix.length);
|
|
103
|
+
|
|
104
|
+
// Parses the <pre> contents and returns a HAST tree with the highlighted code
|
|
105
|
+
const { children } = highlighter.shiki.codeToHast(
|
|
106
|
+
preElement.children[0].value,
|
|
107
|
+
{
|
|
108
|
+
lang: languageId,
|
|
109
|
+
// Allows support for dual themes (light, dark) for Shiki
|
|
110
|
+
themes: { light: shikiConfig.themes[0], dark: shikiConfig.themes[1] },
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Adds the original language back to the <pre> element
|
|
115
|
+
children[0].properties.class = `${children[0].properties.class} ${codeLanguage}`;
|
|
116
|
+
|
|
117
|
+
// Adds the toolbar (language label + copy button) to the <pre> element
|
|
118
|
+
children[0].children.push(createToolbarElement(languageId));
|
|
119
|
+
|
|
120
|
+
// Replaces the <pre> element with the updated one
|
|
121
|
+
parent.children.splice(index, 1, ...children);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
visit(tree, 'element', (_, index, parent) => {
|
|
125
|
+
const codeElements = [];
|
|
126
|
+
|
|
127
|
+
let currentIndex = index;
|
|
128
|
+
|
|
129
|
+
while (isCodeBlock(parent?.children[currentIndex])) {
|
|
130
|
+
const preElement = parent?.children[currentIndex];
|
|
131
|
+
const codeElement = preElement.children[0];
|
|
132
|
+
|
|
133
|
+
// We should get the language name from the class name
|
|
134
|
+
if (preElement.properties.class?.length) {
|
|
135
|
+
const className = preElement.properties.class;
|
|
136
|
+
const matches = className.match(/language-(?<language>.*)/);
|
|
137
|
+
const language = matches?.groups.language ?? 'text';
|
|
138
|
+
|
|
139
|
+
// For this iteration of our code, we only support multi-code tab for
|
|
140
|
+
// JavaScript languages for Node.js (CJS / MJS)
|
|
141
|
+
if (language === 'cjs' || language === 'mjs') {
|
|
142
|
+
// We patch up Shiki's `pre` element properties into the `code` element
|
|
143
|
+
// since we want to keep the original properties
|
|
144
|
+
codeElement.properties.class = `${className} ${language}`;
|
|
145
|
+
codeElement.properties.style = preElement.properties.style;
|
|
146
|
+
codeElement.properties.language = language;
|
|
147
|
+
|
|
148
|
+
// Add the code element to the pre children
|
|
149
|
+
codeElements.push(codeElement);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
currentIndex += 1;
|
|
154
|
+
|
|
155
|
+
// Since we only support CJS/MJS switch, we should have exactly 2 elements
|
|
156
|
+
// with different languages in order to create a switchable code tab
|
|
157
|
+
if (
|
|
158
|
+
codeElements.length === 2 &&
|
|
159
|
+
codeElements[0].properties?.language !==
|
|
160
|
+
codeElements[1].properties?.language
|
|
161
|
+
) {
|
|
162
|
+
const switchablePreElement = createElement(
|
|
163
|
+
'pre',
|
|
164
|
+
{
|
|
165
|
+
// We grab Shiki's styling from the code tag
|
|
166
|
+
// back to the pre element tag to ensure consistency
|
|
167
|
+
style: codeElements[0].properties.style,
|
|
168
|
+
class: 'shiki',
|
|
169
|
+
},
|
|
170
|
+
[
|
|
171
|
+
createElement('input', {
|
|
172
|
+
class: 'js-flavor-toggle',
|
|
173
|
+
type: 'checkbox',
|
|
174
|
+
// If the CJS code block is the first one, then we should keep
|
|
175
|
+
// the checkbox checked so that it highglit the CJS by default
|
|
176
|
+
checked: codeElements[0].properties.language === 'cjs',
|
|
177
|
+
}),
|
|
178
|
+
...codeElements,
|
|
179
|
+
createToolbarElement('javascript'),
|
|
180
|
+
]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// This removes all the original code Elements and adds a new CodeTab Element
|
|
184
|
+
// at the original start of the first Code Element
|
|
185
|
+
parent.children.splice(
|
|
186
|
+
index,
|
|
187
|
+
currentIndex - index,
|
|
188
|
+
switchablePreElement
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Prevent visiting the code block children and for the next N Elements
|
|
192
|
+
// since all of them belong to this CodeTabs Element
|
|
193
|
+
return [SKIP];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { minify } from '@swc/html-wasm';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minifies HTML with project defaults and optional overrides. At the moment,
|
|
5
|
+
* swc's defaults are suitable for our needs, but in the event that this changes,
|
|
6
|
+
* allowing project defaults is beneficial.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} html
|
|
9
|
+
* @param {import('@swc/html-wasm').Options} [options]
|
|
10
|
+
*/
|
|
11
|
+
export const minifyHTML = async (html, options = {}) =>
|
|
12
|
+
minify(html, options).then(({ code }) => code);
|