@amamo/mdx 0.3.0 → 0.5.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.
@@ -22,7 +22,11 @@ export interface ICompiler {
22
22
  export interface IAdapterCompiler extends ICompiler {
23
23
  readonly generatedCollectionModule: string;
24
24
  isContentFile(file: string): boolean;
25
+ scan(): Promise<IBuildResult>;
25
26
  startWatch(onGeneratedChange?: (paths: string[]) => Promise<void> | void): () => void;
27
+ transformSource(file: string, source: string): Promise<ITransformResult>;
26
28
  }
27
29
  export declare function createCompiler(config: IAmamoMdxConfig): Promise<ICompiler>;
28
- export declare function createAdapterCompiler(config: IAmamoMdxConfig): Promise<IAdapterCompiler>;
30
+ export declare function createAdapterCompiler(config: IAmamoMdxConfig, options?: {
31
+ retainModules?: boolean;
32
+ }): Promise<IAdapterCompiler>;
package/dist/compiler.js CHANGED
@@ -10,19 +10,22 @@ let temporaryFileId = 0;
10
10
  const execFileAsync = promisify(execFile);
11
11
  class Compiler {
12
12
  config;
13
- shiki;
13
+ retainModules;
14
14
  generatedCollectionModule;
15
15
  buildInFlight;
16
16
  disposed = false;
17
17
  generatedListener;
18
18
  gitModifiedTimes = new Map();
19
+ modules = new Map();
19
20
  records = new Map();
21
+ sources = new Map();
22
+ shikiPromise;
20
23
  tail = Promise.resolve();
21
24
  watcher;
22
25
  watcherError;
23
- constructor(config, shiki) {
26
+ constructor(config, retainModules) {
24
27
  this.config = config;
25
- this.shiki = shiki;
28
+ this.retainModules = retainModules;
26
29
  this.generatedCollectionModule = path.join(config.generatedDirectory, 'collections.mjs');
27
30
  }
28
31
  build() {
@@ -42,11 +45,19 @@ class Compiler {
42
45
  transform(file) {
43
46
  return this.enqueue(() => this.transformNow(cleanFileId(file)));
44
47
  }
48
+ transformSource(file, source) {
49
+ return this.enqueue(() => this.transformNow(cleanFileId(file), source));
50
+ }
51
+ scan() {
52
+ return this.enqueue(() => this.scanNow());
53
+ }
45
54
  remove(file) {
46
55
  return this.enqueue(async () => {
47
56
  const absolute = cleanFileId(file);
48
57
  if (!this.records.delete(absolute))
49
58
  return 0;
59
+ this.modules.delete(absolute);
60
+ this.sources.delete(absolute);
50
61
  const changed = await this.writeOutputs();
51
62
  if (this.config.cache.enabled) {
52
63
  pruneNativeCache(this.config.cache.directory, [...this.records.values()].map((record) => record.cacheKey));
@@ -61,7 +72,8 @@ class Compiler {
61
72
  await this.enqueue(async () => {
62
73
  this.watcher?.close();
63
74
  this.watcher = undefined;
64
- this.shiki?.dispose();
75
+ const shiki = this.shikiPromise ? await this.shikiPromise : undefined;
76
+ shiki?.dispose();
65
77
  this.disposed = true;
66
78
  }, false);
67
79
  }
@@ -123,9 +135,12 @@ class Compiler {
123
135
  async buildNow() {
124
136
  const discovered = await this.discover();
125
137
  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);
138
+ this.replaceRecords(records.map(documentMetadata), discovered.map((document) => document.input));
139
+ this.modules.clear();
140
+ if (this.retainModules) {
141
+ for (const record of records)
142
+ this.modules.set(path.resolve(record.file), record.module);
143
+ }
129
144
  const changed = await this.writeOutputs();
130
145
  if (this.config.cache.enabled) {
131
146
  pruneNativeCache(this.config.cache.directory, records.map((record) => record.cacheKey));
@@ -138,7 +153,39 @@ class Compiler {
138
153
  outputsWritten: changed.length,
139
154
  };
140
155
  }
141
- async transformNow(file) {
156
+ async scanNow() {
157
+ const discovered = await this.discover();
158
+ const inputs = discovered.map((document) => document.input);
159
+ const records = prepareNativeBatch(this.config, inputs).takeMetadata();
160
+ this.replaceRecords(records, inputs);
161
+ this.modules.clear();
162
+ const changed = await this.writeOutputs();
163
+ if (this.config.cache.enabled) {
164
+ pruneNativeCache(this.config.cache.directory, records.map((record) => record.cacheKey));
165
+ }
166
+ await this.notifyGeneratedChange(changed);
167
+ return {
168
+ cached: records.filter((record) => record.cached).length,
169
+ compiled: 0,
170
+ discovered: discovered.length,
171
+ outputsWritten: changed.length,
172
+ };
173
+ }
174
+ async transformNow(file, source) {
175
+ const current = this.records.get(file);
176
+ const currentModule = this.modules.get(file);
177
+ if (source !== undefined &&
178
+ source === this.sources.get(file) &&
179
+ current &&
180
+ currentModule !== undefined) {
181
+ return {
182
+ cached: true,
183
+ code: currentModule,
184
+ map: null,
185
+ outputsWritten: 0,
186
+ record: { ...current, cached: true, module: currentModule },
187
+ };
188
+ }
142
189
  const metadata = this.metadataForFile(file);
143
190
  if (!metadata) {
144
191
  throw new Error(`AMAMO_FILE_OUTSIDE_COLLECTION: ${file}`);
@@ -150,12 +197,16 @@ class Compiler {
150
197
  throw new Error(`AMAMO_DOCUMENT_DUPLICATE_KEY: ${metadata.collection}/${metadata.key}`);
151
198
  }
152
199
  }
153
- const document = await this.readDocument(metadata);
200
+ const document = await this.readDocument(metadata, source);
154
201
  const records = await this.compile([document.input]);
155
202
  const record = records[0];
156
203
  if (!record)
157
204
  throw new Error(`AMAMO_NATIVE_RESULT_MISSING: ${file}`);
158
- this.records.set(file, record);
205
+ this.records.set(file, documentMetadata(record));
206
+ if (this.retainModules) {
207
+ this.modules.set(file, record.module);
208
+ this.sources.set(file, document.input.source);
209
+ }
159
210
  const changed = await this.writeOutputs();
160
211
  if (this.config.cache.enabled) {
161
212
  pruneNativeCache(this.config.cache.directory, [...this.records.values()].map((value) => value.cacheKey));
@@ -171,7 +222,11 @@ class Compiler {
171
222
  }
172
223
  async compile(inputs) {
173
224
  const batch = prepareNativeBatch(this.config, inputs);
174
- const highlights = this.shiki ? await this.shiki.highlight(batch.codeBlocks) : [];
225
+ let highlights = [];
226
+ if (this.config.highlight.enabled && batch.codeBlocks.length > 0) {
227
+ this.shikiPromise ??= createShikiRenderer(this.config.highlight);
228
+ highlights = await (await this.shikiPromise).highlight(batch.codeBlocks);
229
+ }
175
230
  return batch.finish(highlights);
176
231
  }
177
232
  async discover() {
@@ -200,10 +255,10 @@ class Compiler {
200
255
  const documents = await Promise.all(documentsMetadata.map((value) => this.readDocument(value)));
201
256
  return documents.toSorted((left, right) => left.input.file.localeCompare(right.input.file));
202
257
  }
203
- async readDocument(metadata) {
258
+ async readDocument(metadata, providedSource) {
204
259
  const [source, stats] = await Promise.all([
205
- readFile(metadata.file, 'utf8'),
206
- stat(metadata.file),
260
+ providedSource ?? readFile(metadata.file, 'utf8'),
261
+ this.config.derived.lastModified ? stat(metadata.file) : undefined,
207
262
  ]);
208
263
  return {
209
264
  input: {
@@ -212,7 +267,7 @@ class Compiler {
212
267
  key: metadata.key,
213
268
  locale: metadata.locale,
214
269
  modifiedAt: this.config.derived.lastModified
215
- ? (this.gitModifiedTimes.get(metadata.file) ?? stats.mtime.toISOString())
270
+ ? (this.gitModifiedTimes.get(metadata.file) ?? stats?.mtime.toISOString())
216
271
  : undefined,
217
272
  slug: metadata.slug,
218
273
  source,
@@ -246,9 +301,20 @@ class Compiler {
246
301
  }, {
247
302
  contents: `${JSON.stringify({
248
303
  cacheDirectory: this.config.cache.directory,
304
+ config: this.config,
249
305
  configFingerprint: configurationFingerprint(this.config),
250
- documents: Object.fromEntries(records.map((record) => [record.file, { cacheKey: record.cacheKey }])),
251
- version: 1,
306
+ documents: Object.fromEntries(records.map((record) => [
307
+ record.file,
308
+ {
309
+ cacheKey: record.cacheKey,
310
+ collection: record.collection,
311
+ key: record.key,
312
+ locale: record.locale,
313
+ modifiedAt: record.modifiedAt,
314
+ slug: record.slug,
315
+ },
316
+ ])),
317
+ version: 2,
252
318
  }, null, 2)}\n`,
253
319
  path: path.join(this.config.generatedDirectory, 'index.json'),
254
320
  });
@@ -259,6 +325,16 @@ class Compiler {
259
325
  }
260
326
  return changed;
261
327
  }
328
+ replaceRecords(records, inputs) {
329
+ this.records.clear();
330
+ for (const record of records)
331
+ this.records.set(path.resolve(record.file), record);
332
+ this.sources.clear();
333
+ if (this.retainModules) {
334
+ for (const input of inputs)
335
+ this.sources.set(path.resolve(input.file), input.source);
336
+ }
337
+ }
262
338
  async notifyGeneratedChange(paths) {
263
339
  if (paths.length === 0 || !this.generatedListener)
264
340
  return;
@@ -268,12 +344,9 @@ class Compiler {
268
344
  export async function createCompiler(config) {
269
345
  return createAdapterCompiler(config);
270
346
  }
271
- export async function createAdapterCompiler(config) {
347
+ export async function createAdapterCompiler(config, options = {}) {
272
348
  const normalized = normalizeConfig(config);
273
- const shiki = normalized.highlight.enabled
274
- ? await createShikiRenderer(normalized.highlight)
275
- : undefined;
276
- return new Compiler(normalized, shiki);
349
+ return new Compiler(normalized, options.retainModules ?? true);
277
350
  }
278
351
  function deriveMetadata(collection, config, file, extensionless) {
279
352
  const segments = extensionless.split(path.sep);
@@ -335,13 +408,20 @@ function generateCollectionModule(config, records) {
335
408
  return lines.join('\n');
336
409
  }
337
410
  function generateCollectionTypes() {
338
- return `export interface IGeneratedDocument {
411
+ return `import type { IStructuredData, ITocItem } from '@amamo/mdx'
412
+
413
+ export interface IGeneratedDocument {
339
414
  readonly derived: Readonly<Record<string, unknown>>
340
415
  readonly frontmatter: Readonly<Record<string, unknown>>
341
416
  readonly key: string
342
417
  readonly locale?: string
343
418
  readonly slug?: string
344
- readonly load: () => Promise<{ default: unknown }>
419
+ readonly load: () => Promise<{
420
+ readonly default: unknown
421
+ readonly frontmatter: Readonly<Record<string, unknown>>
422
+ readonly structuredData: IStructuredData
423
+ readonly toc: readonly ITocItem[]
424
+ }>
345
425
  }
346
426
 
347
427
  export declare const collections: Readonly<Record<string, readonly IGeneratedDocument[]>>
@@ -382,6 +462,24 @@ async function writeIfChanged(file, contents) {
382
462
  function compareRecords(left, right) {
383
463
  return left.collection.localeCompare(right.collection) || left.key.localeCompare(right.key);
384
464
  }
465
+ function documentMetadata(record) {
466
+ return {
467
+ cacheKey: record.cacheKey,
468
+ cached: record.cached,
469
+ collection: record.collection,
470
+ dependencies: record.dependencies,
471
+ derived: record.derived,
472
+ diagnostics: record.diagnostics,
473
+ file: record.file,
474
+ frontmatter: record.frontmatter,
475
+ hash: record.hash,
476
+ key: record.key,
477
+ locale: record.locale,
478
+ modifiedAt: record.modifiedAt,
479
+ projections: record.projections,
480
+ slug: record.slug,
481
+ };
482
+ }
385
483
  function cleanFileId(file) {
386
484
  const absolute = path.resolve(file.split('?')[0] ?? file);
387
485
  const missing = [];
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface ITocItem {
3
+ depth: number;
4
+ title: ReactNode;
5
+ url: string;
6
+ }
7
+ export interface IStructuredDataHeading {
8
+ content: string;
9
+ id: string;
10
+ }
11
+ export interface IStructuredDataContent {
12
+ content: string;
13
+ heading?: string;
14
+ }
15
+ export interface IStructuredData {
16
+ contents: IStructuredDataContent[];
17
+ headings: IStructuredDataHeading[];
18
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { IAmamoMDXConfig, IAmamoMdxConfig, ICacheConfig, ICollectionConfig, IDerivedConfig, IFrontmatterSchema, IHighlightConfig, ILocaleConfig, IManifestConfig, IMathConfig, IMdxConfig, IMdxExtensionsConfig, IMediaConfig, JsonValue, ManifestField, } from './config.js';
2
2
  export { defineConfig, normalizeConfig, z } from './config.js';
3
+ export type { IStructuredData, IStructuredDataContent, IStructuredDataHeading, ITocItem, } from './content.js';
3
4
  export type { IBuildResult, ICompiler, ITransformResult } from './compiler.js';
4
5
  export { createCompiler } from './compiler.js';
5
6
  export type { IDiagnostic } from './native.js';
package/dist/native.d.ts CHANGED
@@ -37,7 +37,7 @@ export interface IHighlightedCodeBlock {
37
37
  documentId: string;
38
38
  hast: JsonValue;
39
39
  }
40
- export interface IDocumentRecord {
40
+ export interface IDocumentMetadata {
41
41
  cacheKey: string;
42
42
  cached: boolean;
43
43
  collection: string;
@@ -49,10 +49,13 @@ export interface IDocumentRecord {
49
49
  hash: string;
50
50
  key: string;
51
51
  locale?: string;
52
- module: string;
52
+ modifiedAt?: string;
53
53
  projections: Record<string, JsonValue>;
54
54
  slug?: string;
55
55
  }
56
+ export interface IDocumentRecord extends IDocumentMetadata {
57
+ module: string;
58
+ }
56
59
  export interface IRenderedManifest {
57
60
  contents: string;
58
61
  path: string;
@@ -60,6 +63,7 @@ export interface IRenderedManifest {
60
63
  export interface IPreparedNativeBatch {
61
64
  codeBlocks: ICodeBlock[];
62
65
  finish(highlights: IHighlightedCodeBlock[]): IDocumentRecord[];
66
+ takeMetadata(): IDocumentMetadata[];
63
67
  }
64
68
  export declare class AmamoMdxError extends Error {
65
69
  readonly diagnostics: IDiagnostic[];
@@ -68,4 +72,4 @@ export declare class AmamoMdxError extends Error {
68
72
  export declare function configurationFingerprint(config: IAmamoMDXConfig): string;
69
73
  export declare function prepareNativeBatch(config: IAmamoMDXConfig, inputs: INativeDocumentInput[]): IPreparedNativeBatch;
70
74
  export declare function pruneNativeCache(cacheDirectory: string, keepKeys: string[]): number;
71
- export declare function renderNativeManifests(config: IAmamoMDXConfig, records: IDocumentRecord[]): IRenderedManifest[];
75
+ export declare function renderNativeManifests(config: IAmamoMDXConfig, records: IDocumentMetadata[]): IRenderedManifest[];
package/dist/native.js CHANGED
@@ -14,7 +14,7 @@ const shikiVersion = require('shiki/package.json').version;
14
14
  function nativeConfigJson(config) {
15
15
  return JSON.stringify({
16
16
  ...config,
17
- _runtime: { packageVersion, shikiVersion },
17
+ _runtime: { packageVersion, shikiTransformers: true, shikiVersion },
18
18
  });
19
19
  }
20
20
  export function configurationFingerprint(config) {
@@ -44,6 +44,14 @@ export function prepareNativeBatch(config, inputs) {
44
44
  return mapNativeError(error);
45
45
  }
46
46
  },
47
+ takeMetadata() {
48
+ try {
49
+ return JSON.parse(batch.takeMetadataJson());
50
+ }
51
+ catch (error) {
52
+ return mapNativeError(error);
53
+ }
54
+ },
47
55
  };
48
56
  }
49
57
  catch (error) {
@@ -60,7 +68,11 @@ export function pruneNativeCache(cacheDirectory, keepKeys) {
60
68
  }
61
69
  export function renderNativeManifests(config, records) {
62
70
  try {
63
- return JSON.parse(loadBinding().renderManifests(nativeConfigJson(config), JSON.stringify(records)));
71
+ const manifestRecords = records.map(({ key, projections }) => ({
72
+ key,
73
+ projections,
74
+ }));
75
+ return JSON.parse(loadBinding().renderManifests(nativeConfigJson(config), JSON.stringify(manifestRecords)));
64
76
  }
65
77
  catch (error) {
66
78
  return mapNativeError(error);
@@ -4,30 +4,48 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  const promises_1 = require("node:fs/promises");
6
6
  const node_path_1 = __importDefault(require("node:path"));
7
- function nextLoader(_source) {
8
- return readCompiledModule(this);
7
+ function nextLoader(source) {
8
+ return compileModule(this, source);
9
9
  }
10
- async function readCompiledModule(context) {
10
+ async function compileModule(context, source) {
11
11
  const options = context.getOptions();
12
+ context.addDependency(options.indexFile);
13
+ let config;
14
+ let input;
12
15
  try {
13
16
  const index = JSON.parse(await (0, promises_1.readFile)(options.indexFile, 'utf8'));
14
17
  const resource = await (0, promises_1.realpath)(node_path_1.default.resolve(context.resourcePath));
15
- const cacheKey = index.documents?.[resource]?.cacheKey;
16
- if (index.configFingerprint !== options.configFingerprint ||
18
+ const document = index.documents?.[resource];
19
+ if (index.version !== 2 ||
20
+ index.configFingerprint !== options.configFingerprint ||
17
21
  typeof index.cacheDirectory !== 'string' ||
18
- typeof cacheKey !== 'string' ||
19
- !/^[a-f0-9]{64}$/.test(cacheKey)) {
22
+ typeof index.config !== 'object' ||
23
+ index.config === null ||
24
+ index.config.cache.directory !== index.cacheDirectory ||
25
+ typeof document?.cacheKey !== 'string' ||
26
+ !/^[a-f0-9]{64}$/.test(document.cacheKey) ||
27
+ typeof document.collection !== 'string' ||
28
+ typeof document.key !== 'string') {
20
29
  throw new Error('index entry does not match this build');
21
30
  }
22
- const cacheFile = node_path_1.default.join(index.cacheDirectory, cacheKey.slice(0, 2), `${cacheKey}.json`);
23
- const record = JSON.parse(await (0, promises_1.readFile)(cacheFile, 'utf8'));
24
- if (record.cacheKey !== cacheKey || typeof record.module !== 'string') {
25
- throw new Error('cache record does not match its index entry');
26
- }
27
- return record.module;
31
+ config = index.config;
32
+ input = {
33
+ collection: document.collection,
34
+ file: resource,
35
+ key: document.key,
36
+ locale: document.locale,
37
+ modifiedAt: document.modifiedAt,
38
+ slug: document.slug,
39
+ source,
40
+ };
28
41
  }
29
42
  catch (error) {
30
43
  throw new Error(`AMAMO_NEXT_CACHE_MISS: ${context.resourcePath}; rerun the coordinated amamo-mdx build`, { cause: error });
31
44
  }
45
+ const { compileNextDocument } = await import('./next-runtime.js');
46
+ const record = await compileNextDocument(config, input);
47
+ for (const dependency of record.dependencies)
48
+ context.addDependency(dependency);
49
+ return record.module;
32
50
  }
33
51
  module.exports = nextLoader;
@@ -0,0 +1,3 @@
1
+ import type { IAmamoMDXConfig } from './config.js';
2
+ import type { IDocumentRecord, INativeDocumentInput } from './native.js';
3
+ export declare function compileNextDocument(config: IAmamoMDXConfig, input: INativeDocumentInput): Promise<IDocumentRecord>;
@@ -0,0 +1,20 @@
1
+ import { configurationFingerprint, prepareNativeBatch } from './native.js';
2
+ import { createShikiRenderer } from './shiki.js';
3
+ const renderers = new Map();
4
+ export async function compileNextDocument(config, input) {
5
+ const batch = prepareNativeBatch(config, [input]);
6
+ let highlights = [];
7
+ if (config.highlight.enabled && batch.codeBlocks.length > 0) {
8
+ const fingerprint = configurationFingerprint(config);
9
+ let renderer = renderers.get(fingerprint);
10
+ if (!renderer) {
11
+ renderer = createShikiRenderer(config.highlight);
12
+ renderers.set(fingerprint, renderer);
13
+ }
14
+ highlights = await (await renderer).highlight(batch.codeBlocks);
15
+ }
16
+ const record = batch.finish(highlights)[0];
17
+ if (!record)
18
+ throw new Error(`AMAMO_NATIVE_RESULT_MISSING: ${input.file}`);
19
+ return record;
20
+ }
package/dist/next.js CHANGED
@@ -13,17 +13,23 @@ export function withAmamoMdx(config) {
13
13
  };
14
14
  let compilerPromise;
15
15
  let buildPromise;
16
+ let scanPromise;
16
17
  let watching = false;
17
18
  function compiler() {
18
- compilerPromise ??= createAdapterCompiler(config);
19
+ compilerPromise ??= createAdapterCompiler(config, { retainModules: false });
19
20
  return compilerPromise;
20
21
  }
21
22
  async function prepare(phase) {
22
23
  if (phase !== PHASE_DEVELOPMENT_SERVER && phase !== PHASE_PRODUCTION_BUILD)
23
24
  return;
24
- buildPromise ??= compiler().then((instance) => instance.build());
25
- await buildPromise;
26
- if (phase === PHASE_DEVELOPMENT_SERVER && !watching) {
25
+ if (phase === PHASE_PRODUCTION_BUILD) {
26
+ buildPromise ??= compiler().then((instance) => instance.build());
27
+ await buildPromise;
28
+ return;
29
+ }
30
+ scanPromise ??= compiler().then((instance) => instance.scan());
31
+ await scanPromise;
32
+ if (!watching) {
27
33
  const instance = await compiler();
28
34
  instance.startWatch();
29
35
  watching = true;
package/dist/shiki.js CHANGED
@@ -1,8 +1,18 @@
1
+ import { transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight, transformerRemoveLineBreak, transformerRemoveNotationEscape, } from '@shikijs/transformers';
1
2
  import { createHighlighterCore } from 'shiki/core';
2
3
  import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
3
4
  import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
4
5
  import { bundledLanguages, bundledLanguagesAlias } from 'shiki/langs';
5
6
  import { bundledThemes } from 'shiki/themes';
7
+ function languageClassTransformer(language) {
8
+ return {
9
+ pre(node) {
10
+ const classes = String(node.properties.class ?? '').split(' ');
11
+ classes.push(`language-${language}`);
12
+ node.properties.class = classes.filter(Boolean).join(' ');
13
+ },
14
+ };
15
+ }
6
16
  function themeLoader(name) {
7
17
  if (!Object.hasOwn(bundledThemes, name)) {
8
18
  throw new Error(`AMAMO_SHIKI_UNKNOWN_THEME: Unknown bundled Shiki theme \`${name}\``);
@@ -59,8 +69,17 @@ export async function createShikiRenderer(config) {
59
69
  async highlight(blocks) {
60
70
  if (disposed)
61
71
  throw new Error('AMAMO_SHIKI_DISPOSED: Shiki renderer is disposed');
62
- const languages = await Promise.all(blocks.map((block) => loadLanguage(block.lang)));
63
- return blocks.map((block, index) => {
72
+ const keys = blocks.map((block) => JSON.stringify([block.lang, block.meta, block.code]));
73
+ const uniqueBlocks = new Map();
74
+ for (const [index, block] of blocks.entries()) {
75
+ const key = keys[index];
76
+ if (key !== undefined && !uniqueBlocks.has(key))
77
+ uniqueBlocks.set(key, block);
78
+ }
79
+ const entries = [...uniqueBlocks.entries()];
80
+ const languages = await Promise.all(entries.map(([, block]) => loadLanguage(block.lang)));
81
+ const highlighted = new Map();
82
+ for (const [index, [key, block]] of entries.entries()) {
64
83
  const language = block.lang;
65
84
  const hast = highlighter.codeToHast(block.code, {
66
85
  colorReplacements: config.colorReplacements,
@@ -70,22 +89,45 @@ export async function createShikiRenderer(config) {
70
89
  light: config.themes.light,
71
90
  dark: config.themes.dark,
72
91
  },
73
- transformers: language && !['text', 'plain', 'plaintext'].includes(language)
74
- ? [
75
- {
76
- pre(node) {
77
- const classes = String(node.properties.class ?? '').split(' ');
78
- classes.push(`language-${language}`);
79
- node.properties.class = classes.filter(Boolean).join(' ');
80
- },
92
+ transformers: [
93
+ ...(language && !['text', 'plain', 'plaintext'].includes(language)
94
+ ? [languageClassTransformer(language)]
95
+ : []),
96
+ transformerMetaHighlight({ className: 'line-highlighted' }),
97
+ transformerMetaWordHighlight({ className: 'word-highlighted' }),
98
+ transformerNotationHighlight({ classActiveLine: 'line-highlighted' }),
99
+ transformerNotationDiff({
100
+ classLineAdd: 'line-diff-add',
101
+ classLineRemove: 'line-diff-remove',
102
+ }),
103
+ transformerNotationFocus({ classActiveLine: 'line-focused' }),
104
+ transformerNotationErrorLevel({
105
+ classMap: {
106
+ error: ['line-highlighted', 'line-error'],
107
+ warning: ['line-highlighted', 'line-warning'],
108
+ info: ['line-highlighted', 'line-info'],
81
109
  },
82
- ]
83
- : undefined,
110
+ }),
111
+ transformerNotationWordHighlight({
112
+ classActiveWord: 'word-highlighted',
113
+ classActivePre: 'has-word-highlighted',
114
+ }),
115
+ transformerRemoveLineBreak(),
116
+ transformerRemoveNotationEscape(),
117
+ ],
84
118
  });
119
+ highlighted.set(key, JSON.parse(JSON.stringify(hast)));
120
+ }
121
+ return blocks.map((block, index) => {
122
+ const key = keys[index];
123
+ const hast = key === undefined ? undefined : highlighted.get(key);
124
+ if (hast === undefined) {
125
+ throw new Error('AMAMO_SHIKI_RESULT_MISSING: Highlighted code block is missing');
126
+ }
85
127
  return {
86
128
  blockId: block.blockId,
87
129
  documentId: block.documentId,
88
- hast: JSON.parse(JSON.stringify(hast)),
130
+ hast,
89
131
  };
90
132
  });
91
133
  },
package/dist/vite.js CHANGED
@@ -25,11 +25,11 @@ export function amamoMdx(config) {
25
25
  async buildStart() {
26
26
  await ensureBuild();
27
27
  },
28
- async transform(_source, id) {
28
+ async transform(source, id) {
29
29
  const instance = await compiler();
30
30
  if (!instance.isContentFile(id))
31
31
  return null;
32
- const result = await instance.transform(id);
32
+ const result = await instance.transformSource(id, source);
33
33
  return { code: result.code, map: result.map };
34
34
  },
35
35
  async configureServer(server) {
package/native.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  /* eslint-disable */
3
3
  export declare class PreparedBatch {
4
4
  get codeBlocksJson(): string
5
+ takeMetadataJson(): string
5
6
  finish(highlightsJson: string): string
6
7
  }
7
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amamo/mdx",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A native MDX content compiler with Vite and Next adapters",
5
5
  "homepage": "https://jikkai.github.io/mdx/",
6
6
  "license": "MIT",
@@ -55,26 +55,27 @@
55
55
  "release": "verso"
56
56
  },
57
57
  "dependencies": {
58
+ "@shikijs/transformers": "4.4.3",
58
59
  "shiki": "4.4.3",
59
- "zod": "4.4.3"
60
+ "zod": "4.5.4"
60
61
  },
61
62
  "devDependencies": {
62
- "@amamo/oxlint-config": "1.1.0",
63
- "@amamo/verso": "1.1.0",
63
+ "@amamo/oxlint-config": "1.1.1",
64
+ "@amamo/verso": "1.2.0",
64
65
  "@napi-rs/cli": "3.8.6",
65
- "@types/node": "26.2.0",
66
+ "@types/node": "26.4.0",
66
67
  "@types/react": "19.2.18",
67
- "@types/react-dom": "19.2.4",
68
- "lint-staged": "17.3.0",
69
- "next": "16.3.1",
70
- "oxfmt": "0.63.0",
71
- "oxlint": "1.78.0",
68
+ "@types/react-dom": "19.2.5",
69
+ "lint-staged": "17.4.1",
70
+ "next": "16.3.3",
71
+ "oxfmt": "0.65.0",
72
+ "oxlint": "1.80.0",
72
73
  "react": "19.2.8",
73
74
  "react-dom": "19.2.8",
74
- "simple-git-hooks": "2.13.1",
75
+ "simple-git-hooks": "2.14.0",
75
76
  "typescript": "7.0.2",
76
- "vite": "8.2.1",
77
- "vitest": "4.1.10"
77
+ "vite": "8.2.2",
78
+ "vitest": "4.1.11"
78
79
  },
79
80
  "peerDependencies": {
80
81
  "next": "^16.0.0",
@@ -117,14 +118,14 @@
117
118
  "engines": {
118
119
  "node": ">=20.19"
119
120
  },
120
- "packageManager": "pnpm@11.20.0",
121
+ "packageManager": "pnpm@12.1.0",
121
122
  "optionalDependencies": {
122
- "@amamo/mdx-darwin-arm64": "0.3.0",
123
- "@amamo/mdx-darwin-x64": "0.3.0",
124
- "@amamo/mdx-linux-arm64-gnu": "0.3.0",
125
- "@amamo/mdx-linux-x64-gnu": "0.3.0",
126
- "@amamo/mdx-linux-arm64-musl": "0.3.0",
127
- "@amamo/mdx-linux-x64-musl": "0.3.0",
128
- "@amamo/mdx-win32-x64-msvc": "0.3.0"
123
+ "@amamo/mdx-darwin-arm64": "0.5.0",
124
+ "@amamo/mdx-darwin-x64": "0.5.0",
125
+ "@amamo/mdx-linux-arm64-gnu": "0.5.0",
126
+ "@amamo/mdx-linux-x64-gnu": "0.5.0",
127
+ "@amamo/mdx-linux-arm64-musl": "0.5.0",
128
+ "@amamo/mdx-linux-x64-musl": "0.5.0",
129
+ "@amamo/mdx-win32-x64-msvc": "0.5.0"
129
130
  }
130
131
  }