@mintlify/common 1.0.1132 → 1.0.1134

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.
@@ -21,13 +21,20 @@ export const getMDXOptions = ({ data, remarkPlugins = [], rehypePlugins = [], md
21
21
  // `knownComponents`: components the page defines (`export const X = …`, including snippet
22
22
  // imports resolved at prebuild). A fragment tree has no exports of its own, so without this the
23
23
  // unknown-JSX filter would strip `<X />` inside the fragment.
24
- const buildPlugins = (extracts, { fragment = false, knownComponents = [], depth = 0, budget = undefined, } = {}) => ({
24
+ const buildPlugins = (extracts, { fragment = false, knownComponents = [], knownConstants = {}, depth = 0, budget = undefined, } = {}) => ({
25
25
  remarkPlugins: [
26
26
  // first, so `<Snippet file>` is injected on the page and inside every `<MDX>` fragment
27
27
  [remarkMdxInjectSnippets, data.snippetTreeMap],
28
28
  [
29
29
  remarkMdxExpandExpressions,
30
- { buildPlugins, extraPlugins: fragmentPlugins, knownComponents, depth, budget },
30
+ {
31
+ buildPlugins,
32
+ extraPlugins: fragmentPlugins,
33
+ knownComponents,
34
+ knownConstants,
35
+ depth,
36
+ budget,
37
+ },
31
38
  ],
32
39
  remarkMdxStyleStringToObject,
33
40
  [remarkResolveRelativeLinks, { path: data.path }],
@@ -10,6 +10,7 @@ export type RemarkMdxExpandExpressionsOptions = {
10
10
  buildPlugins: (mdxExtracts: MdxExtracts, options: {
11
11
  fragment: true;
12
12
  knownComponents: string[];
13
+ knownConstants: KnownConstants;
13
14
  depth: number;
14
15
  budget: FragmentBudget;
15
16
  }) => {
@@ -22,6 +23,8 @@ export type RemarkMdxExpandExpressionsOptions = {
22
23
  * fragments still know them; the page-level instance derives them from the tree.
23
24
  */
24
25
  knownComponents?: string[];
26
+ /** Literal `export const` values of the page (how prebuild writes snippet props); passed down to fragments. */
27
+ knownConstants?: KnownConstants;
25
28
  /** Nesting depth of the document being compiled: 0 for the page, +1 per enclosing fragment. */
26
29
  depth?: number;
27
30
  /** Expansion budget shared by a page compile and every fragment compiled for it. */
@@ -47,6 +50,14 @@ export type FragmentBudget = {
47
50
  */
48
51
  export declare const MAX_MDX_FRAGMENT_DEPTH = 8;
49
52
  export declare const MAX_MDX_FRAGMENTS_PER_PAGE = 500;
53
+ /**
54
+ * A value a page exports and the evaluator can fold; `undefined` is a real value (a snippet prop
55
+ * that was not passed). Objects are only read through member access, never compared.
56
+ */
57
+ export type KnownConstant = string | number | boolean | null | undefined | {
58
+ readonly [key: string]: KnownConstant;
59
+ };
60
+ export type KnownConstants = Record<string, KnownConstant>;
50
61
  /**
51
62
  * Compile-time expansion of `<MDX>` inside `{…}` expressions.
52
63
  *
@@ -92,9 +103,22 @@ export declare const MAX_MDX_FRAGMENTS_PER_PAGE = 500;
92
103
  * Table of contents: the fragment pipeline runs `remarkExtractTableOfContents` (it also rewrites
93
104
  * headings to `<Heading>`, so it cannot be skipped) into a fragment-scoped extracts object; the
94
105
  * headings are stored on the expression node as `data.tableOfContents` and merged into the page
95
- * TOC in document order by the page-level run. Headings of a branch that never renders are still
96
- * listed, and a fragment heading that repeats a page heading's title keeps its own id — a
97
- * render-time reconcile is the planned follow-up.
106
+ * TOC in document order by the page-level run. A branch whose condition folds to a page constant
107
+ * (`export const variant = "a"`, how prebuild writes a snippet prop) contributes no headings when
108
+ * it cannot render (`markReachability`); an unknown condition keeps every branch. The branch is
109
+ * still compiled, so a wrong verdict can only affect the TOC. A fragment heading that repeats a
110
+ * page heading's title keeps its own id — a render-time reconcile is the planned follow-up.
111
+ *
112
+ * The folding is a deliberately small evaluator, not a JS engine. It knows: identifiers bound by
113
+ * a page-level `export const` to a literal (string, number, boolean, null, `undefined`, a template
114
+ * without expressions) or to an object literal of such values; member access on those objects;
115
+ * `! - +`; `=== !== == != < <= > >=`; `&& || ??`; ternaries. Anything else — a function call, a
116
+ * `let`, a computed key, a comparison between objects — is unknown, and unknown keeps headings.
98
117
  */
99
118
  export declare const remarkMdxExpandExpressions: (options: RemarkMdxExpandExpressionsOptions) => (tree: Root) => Promise<void>;
119
+ /**
120
+ * Literal `export const` values of the page. `let`/`var` may be reassigned and a non-literal
121
+ * initializer is not known, so both are left out, which keeps every branch's headings.
122
+ */
123
+ export declare const getExportedConstants: (tree: Root) => KnownConstants;
100
124
  export declare const dedent: (text: string) => string;
@@ -38,6 +38,7 @@ const JSXParser = Parser.extend(jsx());
38
38
  */
39
39
  export const MAX_MDX_FRAGMENT_DEPTH = 8;
40
40
  export const MAX_MDX_FRAGMENTS_PER_PAGE = 500;
41
+ const UNKNOWN = Symbol('unknown');
41
42
  /**
42
43
  * Compile-time expansion of `<MDX>` inside `{…}` expressions.
43
44
  *
@@ -83,12 +84,20 @@ export const MAX_MDX_FRAGMENTS_PER_PAGE = 500;
83
84
  * Table of contents: the fragment pipeline runs `remarkExtractTableOfContents` (it also rewrites
84
85
  * headings to `<Heading>`, so it cannot be skipped) into a fragment-scoped extracts object; the
85
86
  * headings are stored on the expression node as `data.tableOfContents` and merged into the page
86
- * TOC in document order by the page-level run. Headings of a branch that never renders are still
87
- * listed, and a fragment heading that repeats a page heading's title keeps its own id — a
88
- * render-time reconcile is the planned follow-up.
87
+ * TOC in document order by the page-level run. A branch whose condition folds to a page constant
88
+ * (`export const variant = "a"`, how prebuild writes a snippet prop) contributes no headings when
89
+ * it cannot render (`markReachability`); an unknown condition keeps every branch. The branch is
90
+ * still compiled, so a wrong verdict can only affect the TOC. A fragment heading that repeats a
91
+ * page heading's title keeps its own id — a render-time reconcile is the planned follow-up.
92
+ *
93
+ * The folding is a deliberately small evaluator, not a JS engine. It knows: identifiers bound by
94
+ * a page-level `export const` to a literal (string, number, boolean, null, `undefined`, a template
95
+ * without expressions) or to an object literal of such values; member access on those objects;
96
+ * `! - +`; `=== !== == != < <= > >=`; `&& || ??`; ternaries. Anything else — a function call, a
97
+ * `let`, a computed key, a comparison between objects — is unknown, and unknown keeps headings.
89
98
  */
90
99
  export const remarkMdxExpandExpressions = (options) => (tree) => __awaiter(void 0, void 0, void 0, function* () {
91
- var _a, _b, _c;
100
+ var _a, _b, _c, _d, _e;
92
101
  const depth = (_a = options.depth) !== null && _a !== void 0 ? _a : 0;
93
102
  decodeFragmentCode(tree, depth > 0);
94
103
  const expressions = [];
@@ -110,12 +119,15 @@ export const remarkMdxExpandExpressions = (options) => (tree) => __awaiter(void
110
119
  const knownComponents = [
111
120
  ...new Set([...((_b = options.knownComponents) !== null && _b !== void 0 ? _b : []), ...getExportedFunctionNames(tree)]),
112
121
  ];
113
- const budget = (_c = options.budget) !== null && _c !== void 0 ? _c : { remaining: MAX_MDX_FRAGMENTS_PER_PAGE };
122
+ // ESM inside a fragment is dropped when it is spliced back, so only the page's exports count
123
+ const knownConstants = depth === 0
124
+ ? Object.assign(Object.assign({}, ((_c = options.knownConstants) !== null && _c !== void 0 ? _c : {})), getExportedConstants(tree)) : Object.assign({}, ((_d = options.knownConstants) !== null && _d !== void 0 ? _d : {}));
125
+ const budget = (_e = options.budget) !== null && _e !== void 0 ? _e : { remaining: MAX_MDX_FRAGMENTS_PER_PAGE };
114
126
  for (const node of expressions) {
115
- yield expandExpression(node, options, { knownComponents, depth, budget });
127
+ yield expandExpression(node, options, { knownComponents, knownConstants, depth, budget });
116
128
  }
117
129
  });
118
- const expandExpression = (node_1, options_1, _a) => __awaiter(void 0, [node_1, options_1, _a], void 0, function* (node, options, { knownComponents, depth, budget }) {
130
+ const expandExpression = (node_1, options_1, _a) => __awaiter(void 0, [node_1, options_1, _a], void 0, function* (node, options, { knownComponents, knownConstants, depth, budget }) {
119
131
  var _b, _c, _d, _e;
120
132
  if (!node.value.includes('<MDX'))
121
133
  return;
@@ -132,6 +144,7 @@ const expandExpression = (node_1, options_1, _a) => __awaiter(void 0, [node_1, o
132
144
  const elements = collectMdxElements(expression);
133
145
  if (elements.length === 0)
134
146
  return;
147
+ const reachability = markReachability(expression, knownConstants);
135
148
  const headings = [];
136
149
  let changed = false;
137
150
  for (const element of elements) {
@@ -153,6 +166,7 @@ const expandExpression = (node_1, options_1, _a) => __awaiter(void 0, [node_1, o
153
166
  const { remarkPlugins, rehypePlugins } = options.buildPlugins(fragmentExtracts, {
154
167
  fragment: true,
155
168
  knownComponents,
169
+ knownConstants,
156
170
  depth: depth + 1,
157
171
  budget,
158
172
  });
@@ -181,7 +195,7 @@ const expandExpression = (node_1, options_1, _a) => __awaiter(void 0, [node_1, o
181
195
  ? compiled.children
182
196
  : [compiled];
183
197
  changed = true;
184
- if (fragmentExtracts.tableOfContents) {
198
+ if (fragmentExtracts.tableOfContents && reachability.get(element) !== false) {
185
199
  headings.push(...fragmentExtracts.tableOfContents);
186
200
  }
187
201
  }
@@ -234,6 +248,247 @@ const collectMdxElements = (expression) => {
234
248
  const isMdxElement = (node) => node.type === 'JSXElement' &&
235
249
  node.openingElement.name.type === 'JSXIdentifier' &&
236
250
  node.openingElement.name.name === 'MDX';
251
+ /**
252
+ * Literal `export const` values of the page. `let`/`var` may be reassigned and a non-literal
253
+ * initializer is not known, so both are left out, which keeps every branch's headings.
254
+ */
255
+ export const getExportedConstants = (tree) => {
256
+ const constants = {};
257
+ visit(tree, 'mdxjsEsm', (node) => {
258
+ var _a, _b, _c, _d;
259
+ for (const statement of (_c = (_b = (_a = node.data) === null || _a === void 0 ? void 0 : _a.estree) === null || _b === void 0 ? void 0 : _b.body) !== null && _c !== void 0 ? _c : []) {
260
+ if (statement.type !== 'ExportNamedDeclaration')
261
+ continue;
262
+ if (((_d = statement.declaration) === null || _d === void 0 ? void 0 : _d.type) !== 'VariableDeclaration')
263
+ continue;
264
+ if (statement.declaration.kind !== 'const')
265
+ continue;
266
+ for (const { id, init } of statement.declaration.declarations) {
267
+ if (id.type !== 'Identifier' || !init)
268
+ continue;
269
+ const value = evaluate(init, {});
270
+ if (value !== UNKNOWN)
271
+ constants[id.name] = value;
272
+ }
273
+ }
274
+ });
275
+ return constants;
276
+ };
277
+ const isObject = (value) => typeof value === 'object' && value !== null;
278
+ const compare = (operator, left, right) => {
279
+ // two object literals are distinct references at runtime; never fold their comparison
280
+ if (isObject(left) || isObject(right))
281
+ return UNKNOWN;
282
+ switch (operator) {
283
+ case '===':
284
+ return left === right;
285
+ case '!==':
286
+ return left !== right;
287
+ case '==':
288
+ // eslint-disable-next-line eqeqeq -- mirrors the author's loose comparison
289
+ return left == right;
290
+ case '!=':
291
+ // eslint-disable-next-line eqeqeq -- mirrors the author's loose comparison
292
+ return left != right;
293
+ default:
294
+ break;
295
+ }
296
+ const ordered = (typeof left === 'number' && typeof right === 'number') ||
297
+ (typeof left === 'string' && typeof right === 'string');
298
+ if (!ordered)
299
+ return UNKNOWN;
300
+ const l = left;
301
+ const r = right;
302
+ switch (operator) {
303
+ case '<':
304
+ return l < r;
305
+ case '<=':
306
+ return l <= r;
307
+ case '>':
308
+ return l > r;
309
+ case '>=':
310
+ return l >= r;
311
+ default:
312
+ return UNKNOWN;
313
+ }
314
+ };
315
+ /** Static value of an expression given the page's constants, or UNKNOWN. */
316
+ const evaluate = (node, constants) => {
317
+ // acorn emits this node only with `preserveParens`; the estree types do not list it
318
+ if (node.type === 'ParenthesizedExpression') {
319
+ return evaluate(node.expression, constants);
320
+ }
321
+ switch (node.type) {
322
+ case 'Literal':
323
+ return 'regex' in node || 'bigint' in node ? UNKNOWN : node.value;
324
+ case 'TemplateLiteral':
325
+ return node.expressions.length === 0
326
+ ? node.quasis.map((quasi) => { var _a; return (_a = quasi.value.cooked) !== null && _a !== void 0 ? _a : quasi.value.raw; }).join('')
327
+ : UNKNOWN;
328
+ case 'Identifier':
329
+ if (node.name === 'undefined')
330
+ return undefined;
331
+ return Object.hasOwn(constants, node.name) ? constants[node.name] : UNKNOWN;
332
+ case 'ObjectExpression': {
333
+ const object = {};
334
+ for (const property of node.properties) {
335
+ if (property.type !== 'Property' || property.kind !== 'init' || property.computed) {
336
+ return UNKNOWN;
337
+ }
338
+ const key = property.key.type === 'Identifier'
339
+ ? property.key.name
340
+ : property.key.type === 'Literal'
341
+ ? String(property.key.value)
342
+ : undefined;
343
+ if (key === undefined)
344
+ return UNKNOWN;
345
+ const value = evaluate(property.value, constants);
346
+ if (value === UNKNOWN)
347
+ return UNKNOWN;
348
+ object[key] = value;
349
+ }
350
+ return object;
351
+ }
352
+ case 'MemberExpression': {
353
+ const object = evaluate(node.object, constants);
354
+ if (object === UNKNOWN || !isObject(object))
355
+ return UNKNOWN;
356
+ const key = node.computed
357
+ ? evaluate(node.property, constants)
358
+ : node.property.type === 'Identifier'
359
+ ? node.property.name
360
+ : UNKNOWN;
361
+ if (typeof key !== 'string' && typeof key !== 'number')
362
+ return UNKNOWN;
363
+ return Object.hasOwn(object, String(key)) ? object[String(key)] : undefined;
364
+ }
365
+ case 'UnaryExpression': {
366
+ const argument = evaluate(node.argument, constants);
367
+ if (argument === UNKNOWN)
368
+ return UNKNOWN;
369
+ if (node.operator === '!')
370
+ return !argument;
371
+ if (typeof argument === 'number' && node.operator === '-')
372
+ return -argument;
373
+ if (typeof argument === 'number' && node.operator === '+')
374
+ return argument;
375
+ return UNKNOWN;
376
+ }
377
+ case 'BinaryExpression': {
378
+ const left = evaluate(node.left, constants);
379
+ const right = evaluate(node.right, constants);
380
+ return left === UNKNOWN || right === UNKNOWN ? UNKNOWN : compare(node.operator, left, right);
381
+ }
382
+ case 'LogicalExpression': {
383
+ const left = evaluate(node.left, constants);
384
+ if (left === UNKNOWN)
385
+ return UNKNOWN;
386
+ if (node.operator === '&&')
387
+ return left ? evaluate(node.right, constants) : left;
388
+ if (node.operator === '||')
389
+ return left ? left : evaluate(node.right, constants);
390
+ return left != null ? left : evaluate(node.right, constants);
391
+ }
392
+ default:
393
+ return UNKNOWN;
394
+ }
395
+ };
396
+ /**
397
+ * Names a function binds: parameters and every declaration in its body. Over-approximate on
398
+ * purpose (a destructuring key counts too): a shadowed name becomes unknown, which keeps headings.
399
+ */
400
+ const collectFunctionBindings = (fn) => {
401
+ const names = new Set();
402
+ const addIdentifiers = (pattern) => {
403
+ if (pattern) {
404
+ walk(pattern, {
405
+ enter(jsNode) {
406
+ if (jsNode.type === 'Identifier')
407
+ names.add(jsNode.name);
408
+ },
409
+ });
410
+ }
411
+ };
412
+ walk(fn, {
413
+ enter(jsNode) {
414
+ if (isFunction(jsNode)) {
415
+ for (const param of jsNode.params)
416
+ addIdentifiers(param);
417
+ if (jsNode.type === 'FunctionDeclaration')
418
+ addIdentifiers(jsNode.id);
419
+ }
420
+ else if (jsNode.type === 'VariableDeclarator')
421
+ addIdentifiers(jsNode.id);
422
+ else if (jsNode.type === 'ClassDeclaration')
423
+ addIdentifiers(jsNode.id);
424
+ else if (jsNode.type === 'CatchClause')
425
+ addIdentifiers(jsNode.param);
426
+ },
427
+ });
428
+ return names;
429
+ };
430
+ const isFunction = (node) => node.type === 'ArrowFunctionExpression' ||
431
+ node.type === 'FunctionExpression' ||
432
+ node.type === 'FunctionDeclaration';
433
+ const NON_CHILD_KEYS = new Set(['type', 'start', 'end', 'loc', 'range', 'comments']);
434
+ /**
435
+ * Whether each `<MDX>` element in `expression` can render: `false` only when a guarding
436
+ * conditional or logical operator folds to the other branch, `true` on a taken branch or when
437
+ * any guard is unknown. Names a function binds shadow constants inside it.
438
+ */
439
+ const markReachability = (expression, constants) => {
440
+ const reachability = new Map();
441
+ const visitNode = (value, reachable, scope) => {
442
+ if (!value || typeof value !== 'object')
443
+ return;
444
+ if (Array.isArray(value)) {
445
+ for (const child of value)
446
+ visitNode(child, reachable, scope);
447
+ return;
448
+ }
449
+ const node = value;
450
+ if (typeof node.type !== 'string')
451
+ return;
452
+ if (isMdxElement(node)) {
453
+ reachability.set(node, reachable);
454
+ return;
455
+ }
456
+ if (node.type === 'ConditionalExpression') {
457
+ const test = evaluate(node.test, scope);
458
+ visitNode(node.consequent, reachable && (test === UNKNOWN || Boolean(test)), scope);
459
+ visitNode(node.alternate, reachable && (test === UNKNOWN || !test), scope);
460
+ return;
461
+ }
462
+ if (node.type === 'LogicalExpression') {
463
+ const left = evaluate(node.left, scope);
464
+ visitNode(node.left, reachable, scope);
465
+ let rightReachable = reachable;
466
+ if (left !== UNKNOWN) {
467
+ if (node.operator === '&&')
468
+ rightReachable = reachable && Boolean(left);
469
+ else if (node.operator === '||')
470
+ rightReachable = reachable && !left;
471
+ else
472
+ rightReachable = reachable && left == null;
473
+ }
474
+ visitNode(node.right, rightReachable, scope);
475
+ return;
476
+ }
477
+ let childScope = scope;
478
+ if (isFunction(node)) {
479
+ childScope = Object.assign({}, scope);
480
+ for (const name of collectFunctionBindings(node))
481
+ delete childScope[name];
482
+ }
483
+ for (const key of Object.keys(node)) {
484
+ if (NON_CHILD_KEYS.has(key))
485
+ continue;
486
+ visitNode(node[key], reachable, childScope);
487
+ }
488
+ };
489
+ visitNode(expression, true, constants);
490
+ return reachability;
491
+ };
237
492
  export const dedent = (text) => {
238
493
  var _a, _b;
239
494
  const lines = text.split('\n');
@@ -1,7 +1,7 @@
1
1
  import type { Root } from 'mdast';
2
2
  import type { FileType, FileWithImports } from '../../types/mdx/index.js';
3
3
  export type ResolveImportsWarning = {
4
- type: 'invalid-import-path' | 'missing-file' | 'import-resolution-error' | 'default-import-no-default-export';
4
+ type: 'invalid-import-path' | 'missing-file' | 'import-resolution-error' | 'default-import-no-default-export' | 'export-collision';
5
5
  message: string;
6
6
  };
7
7
  export declare const resolveAllImports: (params: {
@@ -56,7 +56,7 @@ export const resolveAllImports = (params) => __awaiter(void 0, void 0, void 0, f
56
56
  }
57
57
  }
58
58
  try {
59
- const contentWithResolvedImport = yield resolveImport(specifier, ast, importedSnippet.tree, exportMap);
59
+ const contentWithResolvedImport = yield resolveImport(specifier, ast, importedSnippet.tree, exportMap, onWarning !== null && onWarning !== void 0 ? onWarning : ((warning) => console.log(warning.message)));
60
60
  if (contentWithResolvedImport == undefined) {
61
61
  throw new Error('Import failed to resolve');
62
62
  }
@@ -0,0 +1,7 @@
1
+ export type ExportCollisionWarning = {
2
+ type: 'export-collision';
3
+ message: string;
4
+ };
5
+ export type OnExportCollision = (warning: ExportCollisionWarning) => void;
6
+ /** Same name, different code: the first declaration wins, so say so instead of failing silently. */
7
+ export declare const warnOnExportCollision: (name: string, existing: string, incoming: string, onWarning?: OnExportCollision) => void;
@@ -0,0 +1,10 @@
1
+ const normalizeStatement = (statement) => statement.replace(/\s+/g, ' ').trim();
2
+ /** Same name, different code: the first declaration wins, so say so instead of failing silently. */
3
+ export const warnOnExportCollision = (name, existing, incoming, onWarning) => {
4
+ if (normalizeStatement(existing) === normalizeStatement(incoming))
5
+ return;
6
+ onWarning === null || onWarning === void 0 ? void 0 : onWarning({
7
+ type: 'export-collision',
8
+ message: `"${name}" is declared twice with different definitions; the first one is kept. Import one of them under another name: import { ${name} as Other${name} } from '…'`,
9
+ });
10
+ };
@@ -1,5 +1,7 @@
1
1
  import type { Root } from 'mdast';
2
2
  import type { ImportSpecifier } from '../../../types/mdx/snippets/import.js';
3
+ import { type OnExportCollision } from './exportCollision.js';
4
+ export { type ExportCollisionWarning, type OnExportCollision } from './exportCollision.js';
3
5
  export { findExport } from './findExport.js';
4
6
  export { injectToTopOfFileOfTree } from './injectToTopOfFile.js';
5
7
  /**
@@ -9,4 +11,4 @@ export { injectToTopOfFileOfTree } from './injectToTopOfFile.js';
9
11
  * @param importedFileContent The content of the file we are importing from
10
12
  * @returns
11
13
  */
12
- export declare const resolveImport: (importSpecifier: ImportSpecifier, destinationPageContent: Root, importedFileContent: Root, exportMap: Record<string, string>) => Promise<Root | undefined>;
14
+ export declare const resolveImport: (importSpecifier: ImportSpecifier, destinationPageContent: Root, importedFileContent: Root, exportMap: Record<string, string>, onWarning?: OnExportCollision) => Promise<Root | undefined>;
@@ -9,6 +9,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { MdxImportSpecifier } from '../../../types/mdx/snippets/import.js';
11
11
  import { getAST } from '../../remark.js';
12
+ import { getExportMapFromTree } from '../getExportMap.js';
13
+ import { warnOnExportCollision } from './exportCollision.js';
12
14
  import { findExport } from './findExport.js';
13
15
  import { injectToTopOfFileOfTree } from './injectToTopOfFile.js';
14
16
  import { resolveComponentWithContent } from './resolveComponentWithContent.js';
@@ -29,25 +31,33 @@ const OTHER_SPECIFIERS = [
29
31
  * @param importedFileContent The content of the file we are importing from
30
32
  * @returns
31
33
  */
32
- export const resolveImport = (importSpecifier, destinationPageContent, importedFileContent, exportMap) => __awaiter(void 0, void 0, void 0, function* () {
34
+ export const resolveImport = (importSpecifier, destinationPageContent, importedFileContent, exportMap, onWarning) => __awaiter(void 0, void 0, void 0, function* () {
35
+ var _a, _b;
33
36
  if (VALID_SPECIFIERS.includes(importSpecifier.type)) {
34
37
  // normal import: import { Component } from '/snippets/Component.mdx' or jsx file
35
- if ((importSpecifier.renamedName && exportMap[importSpecifier.renamedName]) ||
36
- exportMap[importSpecifier.name]) {
37
- // TODO: Handle collisions
38
- return destinationPageContent;
39
- }
40
38
  // find "Component" in "/snippets/Component.mdx" or jsx files
41
39
  const exportContent = findExport(importSpecifier.name, importedFileContent, importSpecifier.renamedName);
42
40
  if (exportContent == undefined)
43
41
  throw new Error(`Could not find export ${importSpecifier.name} in snippet`);
44
- // Inject "export const Component = `...`" into the top of the destination page
45
- injectToTopOfFileOfTree(destinationPageContent, getAST(exportContent).children);
42
+ // `import { X as Y }` binds Y; an existing declaration of that name wins
43
+ const localName = (_a = importSpecifier.renamedName) !== null && _a !== void 0 ? _a : importSpecifier.name;
44
+ const existing = exportMap[localName];
45
+ if (existing !== undefined) {
46
+ warnOnExportCollision(localName, existing, exportContent, onWarning);
47
+ return destinationPageContent;
48
+ }
49
+ // Inject "export const Component = `...`" into the top of the destination page and record
50
+ // every name it declares so a later import or inlined snippet dedupes against it
51
+ const injected = getAST(exportContent);
52
+ injectToTopOfFileOfTree(destinationPageContent, injected.children);
53
+ for (const [declared, statement] of Object.entries(getExportMapFromTree(injected))) {
54
+ (_b = exportMap[declared]) !== null && _b !== void 0 ? _b : (exportMap[declared] = statement);
55
+ }
46
56
  return destinationPageContent;
47
57
  }
48
58
  else if (OTHER_SPECIFIERS.includes(importSpecifier.type)) {
49
59
  // default export: import DefaultComponent from '/snippets/DefaultComponent.mdx'
50
60
  // replace every instance of DefaultComponent with the content of the imported file
51
- return yield resolveComponentWithContent(destinationPageContent, importSpecifier.name, importedFileContent, exportMap);
61
+ return yield resolveComponentWithContent(destinationPageContent, importSpecifier.name, importedFileContent, exportMap, onWarning);
52
62
  }
53
63
  });
@@ -1,4 +1,5 @@
1
1
  import type { Root } from 'mdast';
2
+ import { type OnExportCollision } from './exportCollision.js';
2
3
  /**
3
4
  *
4
5
  * @param tree The tree to inject into
@@ -6,4 +7,4 @@ import type { Root } from 'mdast';
6
7
  * @param snippet The snippet to inject in place of the component
7
8
  * @param exportMap The export map of the snippet
8
9
  */
9
- export declare const resolveComponentWithContent: (tree: Root, componentName: string, snippet: Root, exportMap: Record<string, string>) => Promise<Root>;
10
+ export declare const resolveComponentWithContent: (tree: Root, componentName: string, snippet: Root, exportMap: Record<string, string>, onWarning?: OnExportCollision) => Promise<Root>;
@@ -16,6 +16,7 @@ import { removeFrontmatterFromAST } from '../../astUtils.js';
16
16
  import { getAST } from '../../remark.js';
17
17
  import { createUniqueVariableName, isMdxJsxFlowElement } from '../../utils.js';
18
18
  import { findAndRemoveExports } from '../findAndRemoveExports.js';
19
+ import { warnOnExportCollision } from './exportCollision.js';
19
20
  import { injectToTopOfFileOfTree } from './injectToTopOfFile.js';
20
21
  /**
21
22
  *
@@ -24,7 +25,7 @@ import { injectToTopOfFileOfTree } from './injectToTopOfFile.js';
24
25
  * @param snippet The snippet to inject in place of the component
25
26
  * @param exportMap The export map of the snippet
26
27
  */
27
- export const resolveComponentWithContent = (tree, componentName, snippet, exportMap) => __awaiter(void 0, void 0, void 0, function* () {
28
+ export const resolveComponentWithContent = (tree, componentName, snippet, exportMap, onWarning) => __awaiter(void 0, void 0, void 0, function* () {
28
29
  const clonedSnippet = structuredClone(snippet);
29
30
  removeFrontmatterFromAST(clonedSnippet);
30
31
  const snippetExportMap = findAndRemoveExports(clonedSnippet);
@@ -51,7 +52,7 @@ export const resolveComponentWithContent = (tree, componentName, snippet, export
51
52
  treeToInject = treeToInjectClone;
52
53
  }
53
54
  });
54
- reinsertExports(tree, exportMap, snippetExportMap);
55
+ reinsertExports(tree, exportMap, snippetExportMap, onWarning);
55
56
  return tree;
56
57
  });
57
58
  /**
@@ -272,15 +273,18 @@ const replaceCodePlaceholdersWithProps = (tree, propValues) => {
272
273
  });
273
274
  });
274
275
  };
275
- const reinsertExports = (content, exportMap, snippetExportMap) => {
276
+ const reinsertExports = (content, exportMap, snippetExportMap, onWarning) => {
276
277
  const nodesToInject = [];
277
278
  for (const [key, value] of Object.entries(snippetExportMap)) {
278
- // TODO: handle duplicate exports
279
- if (exportMap[key] == undefined) {
279
+ const existing = exportMap[key];
280
+ if (existing == undefined) {
280
281
  const exportAST = getAST(value);
281
282
  nodesToInject.push(...exportAST.children);
282
283
  exportMap[key] = value;
283
284
  }
285
+ else {
286
+ warnOnExportCollision(key, existing, value, onWarning);
287
+ }
284
288
  }
285
289
  if (nodesToInject.length > 0) {
286
290
  injectToTopOfFileOfTree(content, nodesToInject);