@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
@@ -1,8 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { createRequire } from 'node:module';
3
2
  import { cpus } from 'node:os';
4
- import { dirname, isAbsolute, resolve } from 'node:path';
5
- import { pathToFileURL } from 'node:url';
3
+ import { fileURLToPath } from 'node:url';
6
4
  import { isMainThread } from 'node:worker_threads';
7
5
 
8
6
  import { cosmiconfig } from 'cosmiconfig';
@@ -16,6 +14,12 @@ import logger from '#logger/index.mjs';
16
14
  import { parseChangelog, parseIndex } from '#parsers/markdown.mjs';
17
15
  import { enforceArray } from '#utils/array.mjs';
18
16
  import { leftHandAssign } from '#utils/generators.mjs';
17
+ import { resolveSpecifier } from '#utils/loaders.mjs';
18
+ import {
19
+ CONFIGURED_PLUGINS,
20
+ PLUGIN_LISTS,
21
+ } from '#utils/markdown/constants.mjs';
22
+ import { resolveMarkdown } from '#utils/markdown/plugins.mjs';
19
23
  import { deepMerge } from '#utils/misc.mjs';
20
24
 
21
25
  import { DEFAULT_CHUNK_SIZE, DEFAULT_MAX_THREADS } from './constants.mjs';
@@ -58,12 +62,13 @@ export const getDefaultConfig = (generators, config) =>
58
62
  global: {
59
63
  project: detectProject() ?? 'API Docs',
60
64
  version: process.version,
61
- minify: true,
65
+ minify: false,
62
66
  ref: 'HEAD',
63
67
  // Without release history there is nothing to build a version picker
64
68
  // from, so generators render single-version output.
65
69
  changelog: [],
66
70
  pathsToCopy: ['assets', 'public', 'static'],
71
+ markdown: { remarkPlugins: [], rehypePlugins: [], recmaPlugins: [] },
67
72
  },
68
73
 
69
74
  // The number of wasm memory instances is severely limited on
@@ -79,21 +84,26 @@ export const getDefaultConfig = (generators, config) =>
79
84
  );
80
85
 
81
86
  /**
82
- * Resolves an `extends` entry of a configuration file into an importable
83
- * URL: relative paths resolve against the configuration file, anything else
84
- * resolves as a package import specifier (e.g. `@node-core/doc-kit/config`).
87
+ * Resolves the Markdown plugins of a configuration (in `global`, and each
88
+ * generator's section) from the file declaring them.
85
89
  *
86
- * @param {string} specifier - The `extends` entry
87
- * @param {string} configFilePath - The configuration file it appears in
88
- * @returns {string} A `file:` URL to import
90
+ * @param {Partial<import('./types').Configuration>} config - The configuration
91
+ * @param {string} filePath - The file declaring it
92
+ * @returns {Partial<import('./types').Configuration>}
89
93
  */
90
- const resolveConfigExtends = (specifier, configFilePath) => {
91
- if (specifier.startsWith('.') || isAbsolute(specifier)) {
92
- return pathToFileURL(resolve(dirname(configFilePath), specifier)).href;
93
- }
94
+ const resolveMarkdownPlugins = (config, filePath) =>
95
+ Object.fromEntries(
96
+ Object.entries(config).map(([name, value]) => {
97
+ if (value?.markdown) {
98
+ const label = `${name}.markdown`;
99
+ const markdown = resolveMarkdown(value.markdown, label, filePath);
94
100
 
95
- return pathToFileURL(createRequire(configFilePath).resolve(specifier)).href;
96
- };
101
+ return [name, { ...value, markdown }];
102
+ }
103
+
104
+ return [name, value];
105
+ })
106
+ );
97
107
 
98
108
  /**
99
109
  * Loads an explicit configuration file or searches for one using cosmiconfig.
@@ -112,15 +122,63 @@ export const loadConfigFile = async filePath => {
112
122
 
113
123
  let { extends: presets, ...config } = result.config ?? {};
114
124
 
125
+ config = resolveMarkdownPlugins(config, result.filepath);
126
+
115
127
  for (const preset of enforceArray(presets ?? []).toReversed()) {
116
- const module = await import(resolveConfigExtends(preset, result.filepath));
128
+ const url = resolveSpecifier(preset, result.filepath);
129
+ const module = await import(url);
117
130
 
118
- config = deepMerge(module.default ?? module, config);
131
+ // A preset's Markdown plugins resolve from the preset
132
+ config = deepMerge(
133
+ resolveMarkdownPlugins(module.default ?? module, fileURLToPath(url)),
134
+ config
135
+ );
119
136
  }
120
137
 
121
138
  return config;
122
139
  };
123
140
 
141
+ /**
142
+ * Returns the Markdown plugins a generator takes: for each list of its
143
+ * pipeline with a `'...'`, the global ones, then its own. Generators rendering
144
+ * Markdown (with rehype or recma plugins) skip the global remark plugins,
145
+ * which already ran in `ast`. Its own plugins it doesn't take are ignored with
146
+ * a warning.
147
+ *
148
+ * @param {GeneratorMetadata} generator - The generator
149
+ * @param {Partial<import('./types').MarkdownConfiguration>} [markdown] - Its own plugins
150
+ * @param {import('./types').GlobalConfiguration} global - The global configuration
151
+ * @returns {Partial<import('./types').MarkdownConfiguration> | undefined}
152
+ */
153
+ const configureMarkdown = (generator, markdown = {}, global) => {
154
+ const pipeline = generator.markdown ?? {};
155
+ const renders = Boolean(pipeline.rehypePlugins || pipeline.recmaPlugins);
156
+ const configured = {};
157
+
158
+ for (const list of PLUGIN_LISTS) {
159
+ if (!pipeline[list]?.includes(CONFIGURED_PLUGINS)) {
160
+ if (markdown[list]?.length > 0) {
161
+ logger.warn(
162
+ `Ignoring \`${generator.name}.markdown.${list}\`: ` +
163
+ `\`${generator.name}\` does not take them.`
164
+ );
165
+ }
166
+
167
+ continue;
168
+ }
169
+
170
+ const plugins = markdown[list] ?? [];
171
+
172
+ if (list === 'remarkPlugins' && renders) {
173
+ configured[list] = plugins;
174
+ } else {
175
+ configured[list] = [...global.markdown[list], ...plugins];
176
+ }
177
+ }
178
+
179
+ return generator.markdown && configured;
180
+ };
181
+
124
182
  /**
125
183
  * Transforms configuration values that need async processing or coercion.
126
184
  * Only processes values that haven't been transformed yet (strings for changelog/index, non-coerced versions).
@@ -232,12 +290,20 @@ export const createRunConfiguration = async options => {
232
290
 
233
291
  // Now assign to each generator config (they inherit from global)
234
292
  await Promise.all(
235
- [...generators.values()].map(async ({ name }) => {
236
- const value = merged[name];
293
+ [...generators.values()].map(async generator => {
294
+ const value = merged[generator.name];
237
295
 
238
296
  // Transform generator-specific overrides
239
297
  await transformConfig(value);
240
298
 
299
+ // A generator's own Markdown plugins add to the global ones. Set even
300
+ // when undefined, so the assignment below doesn't copy the global ones
301
+ value.markdown = configureMarkdown(
302
+ generator,
303
+ value.markdown,
304
+ merged.global
305
+ );
306
+
241
307
  // Assign from global (this populates missing values from global)
242
308
  leftHandAssign(value, merged.global);
243
309
  })
@@ -1,4 +1,5 @@
1
1
  import type { SemVer } from 'semver';
2
+
2
3
  import type { ReleaseEntry } from '#parsers/types';
3
4
 
4
5
  export type Configuration = {
@@ -61,4 +62,30 @@ export type GlobalConfiguration = {
61
62
 
62
63
  // Git ref (i.e. HEAD)
63
64
  ref: string;
65
+
66
+ // Markdown plugins added to the pipeline of each generator processing
67
+ // Markdown. A generator's own `markdown` adds to them, rather than
68
+ // replacing them.
69
+ markdown: MarkdownConfiguration;
70
+ };
71
+
72
+ // A plugin's module specifier (a package name, or a path relative to the file
73
+ // declaring it), alone or with its options
74
+ export type PluginEntry = string | [specifier: string, options?: unknown];
75
+
76
+ export type MarkdownConfiguration = {
77
+ // remark plugins, run on Markdown syntax trees
78
+ remarkPlugins: Array<PluginEntry>;
79
+
80
+ // rehype plugins, run on HTML syntax trees
81
+ rehypePlugins: Array<PluginEntry>;
82
+
83
+ // recma plugins, run on JavaScript syntax trees
84
+ recmaPlugins: Array<PluginEntry>;
85
+ };
86
+
87
+ // A generator's Markdown pipeline, where each list takes the configured
88
+ // plugins in place of its `'...'`
89
+ export type MarkdownPipeline = {
90
+ [List in keyof MarkdownConfiguration]?: Array<PluginEntry | '...'>;
64
91
  };
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { extname, isAbsolute, join } from 'node:path';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
 
5
6
  /**
@@ -28,6 +29,27 @@ export const loadFromURL = async url => {
28
29
  }
29
30
  };
30
31
 
32
+ /**
33
+ * Resolves an import specifier into a `file:` URL: relative paths resolve
34
+ * against the file it appears in, anything else as a package import
35
+ * specifier (e.g. `@node-core/doc-kit/config`).
36
+ *
37
+ * @param {string} specifier - A path, a package specifier, or a `file:` URL
38
+ * @param {string} filePath - The file it appears in
39
+ * @returns {string} A `file:` URL to import
40
+ */
41
+ export const resolveSpecifier = (specifier, filePath) => {
42
+ if (specifier.startsWith('file:')) {
43
+ return specifier;
44
+ }
45
+
46
+ if (specifier.startsWith('.') || isAbsolute(specifier)) {
47
+ return pathToFileURL(resolve(dirname(filePath), specifier)).href;
48
+ }
49
+
50
+ return pathToFileURL(createRequire(filePath).resolve(specifier)).href;
51
+ };
52
+
31
53
  /**
32
54
  * Dynamically imports a module from a URL, using JSON import assertion if applicable.
33
55
  *
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ // The plugin lists of a Markdown pipeline, one for each syntax tree
4
+ export const PLUGIN_LISTS = ['remarkPlugins', 'rehypePlugins', 'recmaPlugins'];
5
+
6
+ // Marks where a list of a generator's pipeline takes the configured plugins
7
+ export const CONFIGURED_PLUGINS = '...';
@@ -0,0 +1,232 @@
1
+ 'use strict';
2
+
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { getGeneratorModule } from '#generators/loader.mjs';
6
+ import { enforceArray } from '#utils/array.mjs';
7
+ import { resolveSpecifier } from '#utils/loaders.mjs';
8
+ import { isPlainObject } from '#utils/misc.mjs';
9
+
10
+ import { CONFIGURED_PLUGINS, PLUGIN_LISTS } from './constants.mjs';
11
+
12
+ /**
13
+ * The plugins of a Markdown pipeline, as `processor.use()` takes them.
14
+ *
15
+ * @typedef {Record<'remarkPlugins' | 'rehypePlugins' | 'recmaPlugins', import('unified').PluggableList>} MarkdownPlugins
16
+ */
17
+
18
+ // The pipeline each generator runs on this thread, by its name
19
+ const loadedPipelines = new Map();
20
+
21
+ /**
22
+ * Resolves the module specifiers of Markdown plugins (in the `markdown`
23
+ * option, or a generator's pipeline) from the file declaring them into
24
+ * `file:` URLs, which any thread can import.
25
+ *
26
+ * @param {import('../configuration/types').MarkdownPipeline} markdown - The plugins
27
+ * @param {string} label - Where they are declared, for errors (e.g. `global.markdown`)
28
+ * @param {string} filePath - The file declaring them
29
+ * @returns {import('../configuration/types').MarkdownPipeline}
30
+ */
31
+ export const resolveMarkdown = (markdown, label, filePath) => {
32
+ const resolved = { ...markdown };
33
+
34
+ for (const list of PLUGIN_LISTS) {
35
+ resolved[list] &&= enforceArray(markdown[list]).map((entry, index) => {
36
+ if (entry === CONFIGURED_PLUGINS) {
37
+ return entry;
38
+ }
39
+
40
+ const [specifier, ...options] = enforceArray(entry);
41
+
42
+ if (typeof specifier !== 'string') {
43
+ throw new TypeError(
44
+ `${label}.${list}[${index}] in ${filePath} must be a module ` +
45
+ 'specifier (a package name or a path), as Markdown is processed ' +
46
+ 'in worker threads, which import the plugins themselves.'
47
+ );
48
+ }
49
+
50
+ const url = resolveSpecifier(specifier, filePath);
51
+
52
+ return options.length === 0 ? url : [url, ...options];
53
+ });
54
+ }
55
+
56
+ return resolved;
57
+ };
58
+
59
+ /**
60
+ * Resolves the module specifiers of a generator's Markdown pipeline from the
61
+ * module it was loaded from.
62
+ *
63
+ * @param {GeneratorMetadata} generator - The generator
64
+ * @returns {import('../configuration/types').MarkdownPipeline}
65
+ */
66
+ const resolveMarkdownPipeline = generator => {
67
+ const module = getGeneratorModule(generator);
68
+
69
+ return resolveMarkdown(
70
+ generator.markdown,
71
+ `${generator.name}.markdown`,
72
+ module && fileURLToPath(module)
73
+ );
74
+ };
75
+
76
+ /**
77
+ * Merges options into others: arrays add up, plain objects merge, and other
78
+ * values replace the ones they are merged into.
79
+ *
80
+ * @param {unknown} options - The options merged into
81
+ * @param {unknown} added - The options merged
82
+ * @returns {unknown}
83
+ */
84
+ const mergeOptions = (options, added) => {
85
+ if (Array.isArray(options) && Array.isArray(added)) {
86
+ return [...options, ...added];
87
+ }
88
+
89
+ if (!isPlainObject(options) || !isPlainObject(added)) {
90
+ return added;
91
+ }
92
+
93
+ const merged = { ...options };
94
+
95
+ for (const [key, value] of Object.entries(added)) {
96
+ merged[key] = mergeOptions(options[key], value);
97
+ }
98
+
99
+ return merged;
100
+ };
101
+
102
+ /**
103
+ * Builds a list of a pipeline, with the configured plugins in place of its
104
+ * `'...'`. A configured plugin the list already has, or added earlier, isn't
105
+ * added again: its options merge into the ones it has.
106
+ *
107
+ * @param {Array<import('../configuration/types').PluginEntry | '...'>} [own] - The list
108
+ * @param {Array<import('../configuration/types').PluginEntry>} [configured] - The configured plugins
109
+ * @returns {Array<{ entry: import('../configuration/types').PluginEntry, added: boolean }>}
110
+ */
111
+ const configureList = (own = [], configured = []) => {
112
+ const plugins = own.map(entry => ({ entry, added: false }));
113
+ const added = [];
114
+
115
+ // Each plugin by its specifier: the first listing of it takes the options
116
+ const listed = new Map();
117
+
118
+ for (const plugin of plugins) {
119
+ const [specifier] = enforceArray(plugin.entry);
120
+
121
+ if (!listed.has(specifier)) {
122
+ listed.set(specifier, plugin);
123
+ }
124
+ }
125
+
126
+ for (const entry of configured) {
127
+ const [specifier, options] = enforceArray(entry);
128
+ const plugin = listed.get(specifier);
129
+
130
+ if (!plugin) {
131
+ const addedPlugin = { entry, added: true };
132
+
133
+ added.push(addedPlugin);
134
+ listed.set(specifier, addedPlugin);
135
+ } else if (options !== undefined) {
136
+ const [, listedOptions] = enforceArray(plugin.entry);
137
+
138
+ plugin.entry = [specifier, mergeOptions(listedOptions, options)];
139
+ }
140
+ }
141
+
142
+ return plugins.flatMap(plugin =>
143
+ plugin.entry === CONFIGURED_PLUGINS ? added : [plugin]
144
+ );
145
+ };
146
+
147
+ /**
148
+ * Imports a plugin as `processor.use()` takes it. Its module default-exports
149
+ * the plugin, a list of plugins, or a preset, or exports an async `load`,
150
+ * taking the plugin's options and returning it, for setup unified plugins
151
+ * can't do themselves.
152
+ *
153
+ * @param {import('../configuration/types').PluginEntry} entry - The plugin
154
+ * @returns {Promise<import('unified').Pluggable>}
155
+ */
156
+ const importPlugin = async entry => {
157
+ const [specifier, ...options] = enforceArray(entry);
158
+ const { default: plugin, load } = await import(specifier);
159
+
160
+ if (typeof load === 'function') {
161
+ return [await load(...options)];
162
+ }
163
+
164
+ if (typeof plugin === 'function') {
165
+ return [plugin, ...options];
166
+ }
167
+
168
+ // A list is used as a preset, not to be taken for a [plugin, options] pair
169
+ return Array.isArray(plugin) ? { plugins: plugin } : plugin;
170
+ };
171
+
172
+ /**
173
+ * Loads a generator's Markdown pipeline on the current thread, with the
174
+ * plugins configured for it. Loading the same configuration again does
175
+ * nothing.
176
+ *
177
+ * @param {GeneratorMetadata} generator - The generator
178
+ * @param {Partial<import('../configuration/types').MarkdownConfiguration>} [markdown] - The plugins configured for it
179
+ * @returns {Promise<void>}
180
+ */
181
+ export const loadMarkdownPlugins = async (generator, markdown = {}) => {
182
+ if (!generator.markdown) {
183
+ return;
184
+ }
185
+
186
+ const key = JSON.stringify(markdown);
187
+ const loaded = loadedPipelines.get(generator.name);
188
+
189
+ if (loaded?.generator === generator && loaded.key === key) {
190
+ return;
191
+ }
192
+
193
+ const pipeline = resolveMarkdownPipeline(generator);
194
+ const configured = {};
195
+ const own = {};
196
+
197
+ // The lists import at once, so a slow plugin (Shiki) doesn't hold the rest
198
+ const loading = PLUGIN_LISTS.map(async list => {
199
+ const plugins = configureList(pipeline[list], markdown[list]);
200
+ const imports = plugins.map(({ entry }) => importPlugin(entry));
201
+
202
+ const imported = await Promise.all(imports);
203
+
204
+ configured[list] = imported;
205
+ own[list] = imported.filter((_, index) => !plugins[index].added);
206
+ });
207
+
208
+ await Promise.all(loading);
209
+
210
+ loadedPipelines.set(generator.name, { generator, key, configured, own });
211
+ };
212
+
213
+ /**
214
+ * Gets the plugins of a generator's Markdown pipeline, as loaded on the
215
+ * current thread: with the configured plugins, or only its own (with their
216
+ * configured options).
217
+ *
218
+ * @param {string} generator - The name of the generator
219
+ * @param {boolean} [configured] - Whether with the configured plugins
220
+ * @returns {MarkdownPlugins}
221
+ */
222
+ export const getMarkdownPlugins = (generator, configured = true) => {
223
+ const loaded = loadedPipelines.get(generator);
224
+
225
+ if (!loaded) {
226
+ throw new Error(
227
+ `The Markdown pipeline of "${generator}" is not loaded on this thread`
228
+ );
229
+ }
230
+
231
+ return configured ? loaded.configured : loaded.own;
232
+ };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ import remarkMdx from 'remark-mdx';
4
+ import { unified } from 'unified';
5
+
6
+ import { PLUGIN_LISTS } from './constants.mjs';
7
+ import { getMarkdownPlugins } from './plugins.mjs';
8
+
9
+ // The processors of each loaded pipeline, of Markdown and of MDX
10
+ const processors = new WeakMap();
11
+
12
+ /**
13
+ * Creates the processor of a pipeline: its remark plugins, then its rehype
14
+ * ones, then its recma ones. A processor of MDX knows its syntax too, and
15
+ * tells plugins it processes MDX through `this.data('mdx')`.
16
+ *
17
+ * @param {import('./plugins.mjs').MarkdownPlugins} plugins - The pipeline
18
+ * @param {boolean} mdx - Whether it processes MDX
19
+ * @returns {import('unified').Processor}
20
+ */
21
+ const createProcessor = (plugins, mdx) => {
22
+ const processor = unified().data('mdx', mdx);
23
+
24
+ if (mdx) {
25
+ processor.use(remarkMdx);
26
+ }
27
+
28
+ for (const list of PLUGIN_LISTS) {
29
+ processor.use(plugins[list]);
30
+ }
31
+
32
+ return processor;
33
+ };
34
+
35
+ /**
36
+ * Gets the processor of a generator's Markdown pipeline, as loaded on the
37
+ * current thread (see `loadMarkdownPlugins`).
38
+ *
39
+ * @param {string} generator - The name of the generator
40
+ * @param {Object} [options]
41
+ * @param {boolean} [options.mdx] - Whether it processes MDX
42
+ * @param {boolean} [options.configured] - Whether it runs the configured
43
+ * plugins, rather than only the generator's own
44
+ * @returns {import('unified').Processor}
45
+ */
46
+ export const getProcessor = (
47
+ generator,
48
+ { mdx = false, configured = true } = {}
49
+ ) => {
50
+ const plugins = getMarkdownPlugins(generator, configured);
51
+
52
+ if (!processors.has(plugins)) {
53
+ processors.set(plugins, {});
54
+ }
55
+
56
+ const created = processors.get(plugins);
57
+ const syntax = mdx ? 'mdx' : 'markdown';
58
+
59
+ return (created[syntax] ??= createProcessor(plugins, mdx));
60
+ };