@amamo/mdx 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jikkai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @amamo/mdx
2
+
3
+ An independent MDX content compiler with a Rust core, official Shiki highlighting, JSON Schema validation, media imports, deterministic manifests, and content-addressed caching. The same compiler state powers the root API, Vite 8, and Next 16.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add @amamo/mdx
9
+ ```
10
+
11
+ Node.js 20.19 or newer is required. A supported native package is installed through `optionalDependencies`; there is no JavaScript or WASI fallback.
12
+
13
+ ## Configure
14
+
15
+ Configuration is plain serializable data. Functions and executable plugins are intentionally rejected.
16
+
17
+ ```ts
18
+ // amamo.config.ts
19
+ import { defineConfig } from '@amamo/mdx'
20
+
21
+ export default defineConfig({
22
+ root: import.meta.dirname,
23
+ collections: {
24
+ posts: {
25
+ directory: 'content/posts',
26
+ schema: {
27
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
28
+ type: 'object',
29
+ properties: {
30
+ title: { type: 'string' },
31
+ password: { type: 'string' },
32
+ },
33
+ required: ['title'],
34
+ },
35
+ sensitive: ['password'],
36
+ },
37
+ },
38
+ highlight: {
39
+ provider: 'shiki',
40
+ engine: 'oniguruma',
41
+ themes: { light: 'vitesse-light', dark: 'vitesse-dark' },
42
+ unknownLanguage: 'plain',
43
+ },
44
+ media: { missing: 'warn' },
45
+ manifests: {
46
+ public: {
47
+ output: '.amamo-mdx/public.json',
48
+ fields: { key: 'key', title: 'title' },
49
+ },
50
+ server: {
51
+ output: '.amamo-mdx/server.json',
52
+ key: 'key',
53
+ fields: {
54
+ key: 'key',
55
+ protected: { from: 'password', transform: 'exists' },
56
+ passwordHash: { from: 'password', transform: 'sha256' },
57
+ },
58
+ },
59
+ },
60
+ })
61
+ ```
62
+
63
+ The default highlighter keeps official Shiki in JavaScript and uses its Oniguruma WASM engine. Set `engine: 'javascript'` when avoiding Oniguruma is more important than grammar compatibility. Relative Markdown media URLs become static imports; authored JSX is left untouched. `unknownLanguage` and `media.missing` accept `error` or their permissive `plain`/`warn` policies.
64
+
65
+ ## Compiler API
66
+
67
+ ```ts
68
+ import { createCompiler } from '@amamo/mdx'
69
+ import config from './amamo.config.js'
70
+
71
+ const compiler = await createCompiler(config)
72
+ await compiler.build()
73
+ await compiler.transform('content/posts/hello.mdx')
74
+ await compiler.remove('content/posts/deleted.mdx')
75
+ await compiler.dispose()
76
+ ```
77
+
78
+ `build()` produces `.amamo-mdx/collections.mjs`, its declaration file, the private loader index, and configured manifests. Unchanged bytes are not rewritten.
79
+
80
+ ## Vite 8
81
+
82
+ ```ts
83
+ import { defineConfig } from 'vite'
84
+ import { amamoMdx } from '@amamo/mdx/vite'
85
+ import amamo from './amamo.config.js'
86
+
87
+ export default defineConfig({ plugins: [amamoMdx(amamo)] })
88
+ ```
89
+
90
+ The adapter joins Vite environments onto one startup build and uses Vite's existing watcher for add, change, and delete events.
91
+
92
+ ## Next 16
93
+
94
+ ```js
95
+ // next.config.mjs
96
+ import { withAmamoMdx } from '@amamo/mdx/next'
97
+ import amamo from './amamo.config.js'
98
+
99
+ export default withAmamoMdx(amamo)({ reactStrictMode: true })
100
+ ```
101
+
102
+ The same private read-only loader is registered for default Turbopack and opt-in Webpack. The config wrapper also accepts and awaits an existing Next config function.
103
+
104
+ ## Security
105
+
106
+ MDX can contain executable JavaScript. Compile only content from trusted authors. Schema validation is not a sandbox. Fields marked `sensitive` are removed before modules, cache records, and public frontmatter are serialized; manifests may only inspect them with `exists` or hash them with `sha256`.
107
+
108
+ Relative media imports are confined to the configured root, including symlink resolution. Cache and generated output writes use same-directory temporary files and atomic rename.
109
+
110
+ ## Native targets
111
+
112
+ - macOS arm64 and x64
113
+ - Linux arm64 and x64, glibc and musl
114
+ - Windows x64 MSVC
115
+
116
+ Unsupported targets fail during native binding load. No WASI or JavaScript fallback is shipped.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,28 @@
1
+ import type { IAmamoMdxConfig } from './config.js';
2
+ import type { IDocumentRecord } from './native.js';
3
+ export interface IBuildResult {
4
+ cached: number;
5
+ compiled: number;
6
+ discovered: number;
7
+ outputsWritten: number;
8
+ }
9
+ export interface ITransformResult {
10
+ cached: boolean;
11
+ code: string;
12
+ map: null;
13
+ outputsWritten: number;
14
+ record: IDocumentRecord;
15
+ }
16
+ export interface ICompiler {
17
+ build(): Promise<IBuildResult>;
18
+ dispose(): Promise<void>;
19
+ remove(file: string): Promise<number>;
20
+ transform(file: string): Promise<ITransformResult>;
21
+ }
22
+ export interface IAdapterCompiler extends ICompiler {
23
+ readonly generatedCollectionModule: string;
24
+ isContentFile(file: string): boolean;
25
+ startWatch(onGeneratedChange?: (paths: string[]) => Promise<void> | void): () => void;
26
+ }
27
+ export declare function createCompiler(config: IAmamoMdxConfig): Promise<ICompiler>;
28
+ export declare function createAdapterCompiler(config: IAmamoMdxConfig): Promise<IAdapterCompiler>;
@@ -0,0 +1,446 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { realpathSync, watch } from 'node:fs';
3
+ import { mkdir, open, readFile, readdir, rename, rm, stat } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { normalizeConfig } from './config.js';
7
+ import { configurationFingerprint, prepareNativeBatch, pruneNativeCache, renderNativeManifests, } from './native.js';
8
+ import { createShikiRenderer } from './shiki.js';
9
+ let temporaryFileId = 0;
10
+ const execFileAsync = promisify(execFile);
11
+ class Compiler {
12
+ config;
13
+ shiki;
14
+ generatedCollectionModule;
15
+ buildInFlight;
16
+ disposed = false;
17
+ generatedListener;
18
+ gitModifiedTimes = new Map();
19
+ records = new Map();
20
+ tail = Promise.resolve();
21
+ watcher;
22
+ watcherError;
23
+ constructor(config, shiki) {
24
+ this.config = config;
25
+ this.shiki = shiki;
26
+ this.generatedCollectionModule = path.join(config.generatedDirectory, 'collections.mjs');
27
+ }
28
+ build() {
29
+ if (this.buildInFlight)
30
+ return this.buildInFlight;
31
+ const operation = this.enqueue(() => this.buildNow());
32
+ const tracked = operation.then((result) => {
33
+ this.buildInFlight = undefined;
34
+ return result;
35
+ }, (error) => {
36
+ this.buildInFlight = undefined;
37
+ throw error;
38
+ });
39
+ this.buildInFlight = tracked;
40
+ return tracked;
41
+ }
42
+ transform(file) {
43
+ return this.enqueue(() => this.transformNow(cleanFileId(file)));
44
+ }
45
+ remove(file) {
46
+ return this.enqueue(async () => {
47
+ const absolute = cleanFileId(file);
48
+ if (!this.records.delete(absolute))
49
+ return 0;
50
+ const changed = await this.writeOutputs();
51
+ if (this.config.cache.enabled) {
52
+ pruneNativeCache(this.config.cache.directory, [...this.records.values()].map((record) => record.cacheKey));
53
+ }
54
+ await this.notifyGeneratedChange(changed);
55
+ return changed.length;
56
+ });
57
+ }
58
+ async dispose() {
59
+ if (this.disposed)
60
+ return;
61
+ await this.enqueue(async () => {
62
+ this.watcher?.close();
63
+ this.watcher = undefined;
64
+ this.shiki?.dispose();
65
+ this.disposed = true;
66
+ }, false);
67
+ }
68
+ isContentFile(file) {
69
+ return this.metadataForFile(cleanFileId(file)) !== undefined;
70
+ }
71
+ startWatch(onGeneratedChange) {
72
+ if (this.watcher)
73
+ return () => this.stopWatch();
74
+ this.generatedListener = onGeneratedChange;
75
+ this.watcher = watch(this.config.root, { recursive: true }, async (_event, filename) => {
76
+ if (!filename)
77
+ return;
78
+ const file = path.resolve(this.config.root, filename.toString());
79
+ if (!this.isContentFile(file) && !this.records.has(file))
80
+ return;
81
+ try {
82
+ await stat(file);
83
+ await this.transform(file);
84
+ }
85
+ catch (error) {
86
+ if (isMissingFile(error)) {
87
+ await this.remove(file);
88
+ }
89
+ else {
90
+ this.watcherError = asError(error);
91
+ }
92
+ }
93
+ });
94
+ this.watcher.on('error', (error) => {
95
+ this.watcherError = error;
96
+ });
97
+ return () => this.stopWatch();
98
+ }
99
+ stopWatch() {
100
+ this.watcher?.close();
101
+ this.watcher = undefined;
102
+ this.generatedListener = undefined;
103
+ }
104
+ enqueue(operation, requireOpen = true) {
105
+ const run = async () => {
106
+ if (requireOpen)
107
+ this.assertOpen();
108
+ return operation();
109
+ };
110
+ const result = this.tail.then(run, run);
111
+ this.tail = result.then(() => undefined, () => undefined);
112
+ return result;
113
+ }
114
+ assertOpen() {
115
+ if (this.disposed)
116
+ throw new Error('AMAMO_COMPILER_DISPOSED: Compiler is disposed');
117
+ if (this.watcherError) {
118
+ const error = this.watcherError;
119
+ this.watcherError = undefined;
120
+ throw error;
121
+ }
122
+ }
123
+ async buildNow() {
124
+ const discovered = await this.discover();
125
+ const records = await this.compile(discovered.map((document) => document.input));
126
+ this.records.clear();
127
+ for (const record of records)
128
+ this.records.set(path.resolve(record.file), record);
129
+ const changed = await this.writeOutputs();
130
+ if (this.config.cache.enabled) {
131
+ pruneNativeCache(this.config.cache.directory, records.map((record) => record.cacheKey));
132
+ }
133
+ await this.notifyGeneratedChange(changed);
134
+ return {
135
+ cached: records.filter((record) => record.cached).length,
136
+ compiled: records.filter((record) => !record.cached).length,
137
+ discovered: discovered.length,
138
+ outputsWritten: changed.length,
139
+ };
140
+ }
141
+ async transformNow(file) {
142
+ const metadata = this.metadataForFile(file);
143
+ if (!metadata) {
144
+ throw new Error(`AMAMO_FILE_OUTSIDE_COLLECTION: ${file}`);
145
+ }
146
+ for (const record of this.records.values()) {
147
+ if (record.file !== file &&
148
+ record.collection === metadata.collection &&
149
+ record.key === metadata.key) {
150
+ throw new Error(`AMAMO_DOCUMENT_DUPLICATE_KEY: ${metadata.collection}/${metadata.key}`);
151
+ }
152
+ }
153
+ const document = await this.readDocument(metadata);
154
+ const records = await this.compile([document.input]);
155
+ const record = records[0];
156
+ if (!record)
157
+ throw new Error(`AMAMO_NATIVE_RESULT_MISSING: ${file}`);
158
+ this.records.set(file, record);
159
+ const changed = await this.writeOutputs();
160
+ if (this.config.cache.enabled) {
161
+ pruneNativeCache(this.config.cache.directory, [...this.records.values()].map((value) => value.cacheKey));
162
+ }
163
+ await this.notifyGeneratedChange(changed);
164
+ return {
165
+ cached: record.cached,
166
+ code: record.module,
167
+ map: null,
168
+ outputsWritten: changed.length,
169
+ record,
170
+ };
171
+ }
172
+ async compile(inputs) {
173
+ const batch = prepareNativeBatch(this.config, inputs);
174
+ const highlights = this.shiki ? await this.shiki.highlight(batch.codeBlocks) : [];
175
+ return batch.finish(highlights);
176
+ }
177
+ async discover() {
178
+ const documentsMetadata = [];
179
+ const keys = new Map();
180
+ for (const [collection, config] of Object.entries(this.config.collections).toSorted(([left], [right]) => left.localeCompare(right))) {
181
+ for (const file of await walkFiles(config.directory)) {
182
+ const metadata = this.metadataForFile(file, collection);
183
+ if (!metadata)
184
+ continue;
185
+ const duplicateKey = `${metadata.collection}\0${metadata.key}`;
186
+ const duplicate = keys.get(duplicateKey);
187
+ if (duplicate) {
188
+ throw new Error(`AMAMO_DOCUMENT_DUPLICATE_KEY: ${metadata.collection}/${metadata.key} is shared by ${duplicate} and ${file}`);
189
+ }
190
+ keys.set(duplicateKey, file);
191
+ documentsMetadata.push(metadata);
192
+ }
193
+ }
194
+ this.gitModifiedTimes.clear();
195
+ if (this.config.derived.lastModified) {
196
+ const times = await gitLastModified(this.config.root, documentsMetadata.map(({ file }) => file), Object.values(this.config.collections).map(({ directory }) => directory));
197
+ for (const [file, modifiedAt] of times)
198
+ this.gitModifiedTimes.set(file, modifiedAt);
199
+ }
200
+ const documents = await Promise.all(documentsMetadata.map((value) => this.readDocument(value)));
201
+ return documents.toSorted((left, right) => left.input.file.localeCompare(right.input.file));
202
+ }
203
+ async readDocument(metadata) {
204
+ const [source, stats] = await Promise.all([
205
+ readFile(metadata.file, 'utf8'),
206
+ stat(metadata.file),
207
+ ]);
208
+ return {
209
+ input: {
210
+ collection: metadata.collection,
211
+ file: metadata.file,
212
+ key: metadata.key,
213
+ locale: metadata.locale,
214
+ modifiedAt: this.config.derived.lastModified
215
+ ? (this.gitModifiedTimes.get(metadata.file) ?? stats.mtime.toISOString())
216
+ : undefined,
217
+ slug: metadata.slug,
218
+ source,
219
+ },
220
+ };
221
+ }
222
+ metadataForFile(file, expectedCollection) {
223
+ const absolute = path.resolve(file);
224
+ for (const [collection, config] of Object.entries(this.config.collections)) {
225
+ if (expectedCollection && collection !== expectedCollection)
226
+ continue;
227
+ const relative = path.relative(config.directory, absolute);
228
+ if (relative.startsWith('..') || path.isAbsolute(relative))
229
+ continue;
230
+ const extension = config.extensions.find((value) => relative.endsWith(value));
231
+ if (!extension)
232
+ continue;
233
+ return deriveMetadata(collection, config, absolute, relative.slice(0, -extension.length));
234
+ }
235
+ return undefined;
236
+ }
237
+ async writeOutputs() {
238
+ const records = [...this.records.values()].toSorted(compareRecords);
239
+ const outputs = renderNativeManifests(this.config, records);
240
+ outputs.push({
241
+ contents: generateCollectionModule(this.config, records),
242
+ path: this.generatedCollectionModule,
243
+ }, {
244
+ contents: generateCollectionTypes(),
245
+ path: path.join(this.config.generatedDirectory, 'collections.d.ts'),
246
+ }, {
247
+ contents: `${JSON.stringify({
248
+ cacheDirectory: this.config.cache.directory,
249
+ configFingerprint: configurationFingerprint(this.config),
250
+ documents: Object.fromEntries(records.map((record) => [record.file, { cacheKey: record.cacheKey }])),
251
+ version: 1,
252
+ }, null, 2)}\n`,
253
+ path: path.join(this.config.generatedDirectory, 'index.json'),
254
+ });
255
+ const changed = [];
256
+ for (const output of outputs.toSorted((left, right) => left.path.localeCompare(right.path))) {
257
+ if (await writeIfChanged(output.path, output.contents))
258
+ changed.push(output.path);
259
+ }
260
+ return changed;
261
+ }
262
+ async notifyGeneratedChange(paths) {
263
+ if (paths.length === 0 || !this.generatedListener)
264
+ return;
265
+ await this.generatedListener(paths);
266
+ }
267
+ }
268
+ export async function createCompiler(config) {
269
+ return createAdapterCompiler(config);
270
+ }
271
+ export async function createAdapterCompiler(config) {
272
+ const normalized = normalizeConfig(config);
273
+ const shiki = normalized.highlight.enabled
274
+ ? await createShikiRenderer(normalized.highlight)
275
+ : undefined;
276
+ return new Compiler(normalized, shiki);
277
+ }
278
+ function deriveMetadata(collection, config, file, extensionless) {
279
+ const segments = extensionless.split(path.sep);
280
+ let name = segments.pop() ?? '';
281
+ let locale;
282
+ if (config.locales) {
283
+ locale = config.locales.default;
284
+ for (const candidate of config.locales.names.toSorted((left, right) => right.length - left.length)) {
285
+ const suffix = `.${candidate}`;
286
+ if (name.endsWith(suffix)) {
287
+ name = name.slice(0, -suffix.length);
288
+ locale = candidate;
289
+ break;
290
+ }
291
+ }
292
+ }
293
+ if (!config.slug.indexNames.includes(name))
294
+ segments.push(name);
295
+ const slug = segments.filter(Boolean).join('/') || '/';
296
+ return {
297
+ collection,
298
+ file,
299
+ key: locale ? `${locale}:${slug}` : slug,
300
+ locale,
301
+ slug,
302
+ };
303
+ }
304
+ async function walkFiles(directory) {
305
+ const files = [];
306
+ const entries = await readdir(directory, { withFileTypes: true });
307
+ for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) {
308
+ const file = path.join(directory, entry.name);
309
+ if (entry.isDirectory())
310
+ files.push(...(await walkFiles(file)));
311
+ else if (entry.isFile())
312
+ files.push(file);
313
+ }
314
+ return files;
315
+ }
316
+ function generateCollectionModule(config, records) {
317
+ const collections = Object.keys(config.collections).toSorted();
318
+ const lines = ['export const collections = {'];
319
+ for (const collection of collections) {
320
+ lines.push(` ${JSON.stringify(collection)}: [`);
321
+ for (const record of records.filter((value) => value.collection === collection)) {
322
+ const specifier = importSpecifier(config.generatedDirectory, record.file);
323
+ const metadata = JSON.stringify({
324
+ derived: record.derived,
325
+ frontmatter: record.frontmatter,
326
+ key: record.key,
327
+ locale: record.locale,
328
+ slug: record.slug,
329
+ });
330
+ lines.push(` { ...${metadata}, load: () => import(${JSON.stringify(specifier)}) },`);
331
+ }
332
+ lines.push(' ],');
333
+ }
334
+ lines.push('};', 'export default collections;', '');
335
+ return lines.join('\n');
336
+ }
337
+ function generateCollectionTypes() {
338
+ return `export interface IGeneratedDocument {
339
+ readonly derived: Readonly<Record<string, unknown>>
340
+ readonly frontmatter: Readonly<Record<string, unknown>>
341
+ readonly key: string
342
+ readonly locale?: string
343
+ readonly slug?: string
344
+ readonly load: () => Promise<{ default: unknown }>
345
+ }
346
+
347
+ export declare const collections: Readonly<Record<string, readonly IGeneratedDocument[]>>
348
+ export default collections
349
+ `;
350
+ }
351
+ function importSpecifier(from, file) {
352
+ const relative = path.relative(from, file).split(path.sep).join('/');
353
+ return relative.startsWith('.') ? relative : `./${relative}`;
354
+ }
355
+ async function writeIfChanged(file, contents) {
356
+ try {
357
+ if ((await readFile(file, 'utf8')) === contents)
358
+ return false;
359
+ }
360
+ catch (error) {
361
+ if (!isMissingFile(error))
362
+ throw error;
363
+ }
364
+ await mkdir(path.dirname(file), { recursive: true });
365
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${temporaryFileId++}.tmp`);
366
+ let handle;
367
+ try {
368
+ handle = await open(temporary, 'wx');
369
+ await handle.writeFile(contents);
370
+ await handle.sync();
371
+ await handle.close();
372
+ handle = undefined;
373
+ await rename(temporary, file);
374
+ }
375
+ catch (error) {
376
+ await handle?.close();
377
+ await rm(temporary, { force: true });
378
+ throw error;
379
+ }
380
+ return true;
381
+ }
382
+ function compareRecords(left, right) {
383
+ return left.collection.localeCompare(right.collection) || left.key.localeCompare(right.key);
384
+ }
385
+ function cleanFileId(file) {
386
+ const absolute = path.resolve(file.split('?')[0] ?? file);
387
+ const missing = [];
388
+ let existing = absolute;
389
+ while (true) {
390
+ try {
391
+ return path.join(realpathSync.native(existing), ...missing);
392
+ }
393
+ catch (error) {
394
+ if (!isMissingFile(error))
395
+ return absolute;
396
+ const parent = path.dirname(existing);
397
+ if (parent === existing)
398
+ return absolute;
399
+ missing.unshift(path.basename(existing));
400
+ existing = parent;
401
+ }
402
+ }
403
+ }
404
+ async function gitLastModified(root, files, pathspecs) {
405
+ const wanted = new Set(files.map(cleanFileId));
406
+ const relativePathspecs = pathspecs
407
+ .map((value) => path.relative(root, value))
408
+ .filter((value) => !value.startsWith('..') && !path.isAbsolute(value));
409
+ if (wanted.size === 0 || relativePathspecs.length === 0)
410
+ return new Map();
411
+ try {
412
+ const { stdout } = await execFileAsync('git', [
413
+ '-c',
414
+ 'core.quotepath=false',
415
+ 'log',
416
+ '--format=AMAMO_COMMIT:%cI',
417
+ '--name-only',
418
+ '--relative',
419
+ '--',
420
+ ...relativePathspecs,
421
+ ], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
422
+ const modified = new Map();
423
+ let commitTime;
424
+ for (const rawLine of stdout.split('\n')) {
425
+ const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
426
+ if (line.startsWith('AMAMO_COMMIT:')) {
427
+ commitTime = line.slice('AMAMO_COMMIT:'.length);
428
+ }
429
+ else if (line && commitTime) {
430
+ const file = cleanFileId(path.resolve(root, line));
431
+ if (wanted.has(file) && !modified.has(file))
432
+ modified.set(file, commitTime);
433
+ }
434
+ }
435
+ return modified;
436
+ }
437
+ catch {
438
+ return new Map();
439
+ }
440
+ }
441
+ function isMissingFile(error) {
442
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT';
443
+ }
444
+ function asError(error) {
445
+ return error instanceof Error ? error : new Error(String(error));
446
+ }