Skip to content

Commit 87276ef

Browse files
Rich-HarrisGauBen
andauthored
fix export { foo as default } (#99)
* feat: support `export { ... as default} from ...` * Create soft-ties-agree.md * omg it works * typo * code style:tm: * alternative approach * simplify * tidy up * upgrade changeset * changeset * lint --------- Co-authored-by: Gautier Ben Aïm <[email protected]> Co-authored-by: Gautier Ben Aïm <[email protected]>
1 parent a6e923d commit 87276ef

File tree

18 files changed

+240
-156
lines changed

18 files changed

+240
-156
lines changed

.changeset/slimy-waves-tease.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'dts-buddy': patch
3+
---
4+
5+
fix: improve sourcemaps

.changeset/soft-ties-agree.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'dts-buddy': minor
3+
---
4+
5+
feat: support `export { ... as default } from ...`

src/create-module-declaration.js

Lines changed: 123 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function create_module_declaration(id, entry, created, resolve, options)
5656
const exports = new Set();
5757

5858
/** @type {Set<string>} */
59-
const globals = new Set();
59+
const reserved = new Set(['default']);
6060

6161
/** @type {string[]} */
6262
const export_specifiers = [];
@@ -69,7 +69,7 @@ export function create_module_declaration(id, entry, created, resolve, options)
6969
const module = get_dts(file, created, resolve, options);
7070

7171
for (const name of module.globals) {
72-
globals.add(name);
72+
reserved.add(name);
7373
}
7474

7575
for (const dep of module.dependencies) {
@@ -139,7 +139,7 @@ export function create_module_declaration(id, entry, created, resolve, options)
139139
// step 2 - treeshaking
140140
{
141141
/** @type {Set<string>} */
142-
const names = new Set(globals);
142+
const names = new Set(reserved);
143143

144144
/** @type {Set<Declaration>} */
145145
const declarations = new Set();
@@ -173,13 +173,15 @@ export function create_module_declaration(id, entry, created, resolve, options)
173173
for (const name of exports) {
174174
const declaration = trace_export(entry, name);
175175
if (declaration) {
176-
declaration.alias = get_name(globals.has(name) ? declaration.name : name);
176+
declaration.alias = get_name(reserved.has(name) ? declaration.name : name);
177177
mark(declaration);
178178

179-
if (declaration.alias !== name) {
179+
if (name === 'default') {
180+
declaration.default = true;
181+
} else if (declaration.alias !== name) {
180182
export_specifiers.push(`${declaration.alias} as ${name}`);
181183
} else {
182-
declaration.exported = true;
184+
declaration.export = true;
183185
}
184186
} else {
185187
throw new Error('Something strange happened');
@@ -258,7 +260,11 @@ export function create_module_declaration(id, entry, created, resolve, options)
258260
}
259261

260262
ts.forEachChild(module.ast, (node) => {
261-
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
263+
if (
264+
ts.isImportDeclaration(node) ||
265+
ts.isExportDeclaration(node) ||
266+
ts.isExportAssignment(node)
267+
) {
262268
result.remove(node.pos, node.end);
263269
return;
264270
}
@@ -273,157 +279,134 @@ export function create_module_declaration(id, entry, created, resolve, options)
273279
return;
274280
}
275281

276-
if (is_declaration(node)) {
277-
if (is_internal(node) && options.stripInternal) {
278-
result.remove(node.pos, node.end);
279-
}
282+
if (!is_declaration(node)) {
283+
return;
284+
}
285+
286+
if (is_internal(node) && options.stripInternal) {
287+
result.remove(node.pos, node.end);
288+
}
280289

281-
const identifier = ts.isVariableStatement(node)
290+
const identifier = /** @type {ts.DeclarationName} */ (
291+
ts.isVariableStatement(node)
282292
? ts.getNameOfDeclaration(node.declarationList.declarations[0])
283-
: ts.getNameOfDeclaration(node);
293+
: ts.getNameOfDeclaration(node)
294+
);
284295

285-
const name = identifier?.getText(module.ast);
286-
if (!name) {
287-
throw new Error('TODO');
288-
}
296+
const name = identifier.getText(module.ast);
289297

290-
const declaration = /** @type {Declaration} */ (module.declarations.get(name));
298+
const declaration = /** @type {Declaration} */ (module.declarations.get(name));
291299

292-
if (!declaration.included) {
293-
result.remove(node.pos, node.end);
294-
return;
295-
}
300+
if (!declaration.included) {
301+
result.remove(node.pos, node.end);
302+
return;
303+
}
296304

297-
if (declaration.alias !== 'default') {
298-
const export_modifier = node.modifiers?.find((node) => tsu.isExportKeyword(node));
305+
// special case — TS turns `export default 42` into `const _default: 42; export default _default` —
306+
// the `export default` assignment is already taken care of, we just need to remove the `const _default:`
307+
if (declaration.default && ts.isVariableStatement(node)) {
308+
result.remove(
309+
node.getStart(),
310+
/** @type {ts.TypeNode} */ (node.declarationList.declarations[0].type).getStart()
311+
);
312+
}
299313

300-
if (export_modifier) {
301-
// remove `default` keyword
302-
const default_modifier = node.modifiers?.find((node) => tsu.isDefaultKeyword(node));
314+
const modifiers = declaration.default
315+
? 'export default '
316+
: declaration.export
317+
? 'export '
318+
: '';
319+
320+
if (node.modifiers) {
321+
let end = node.modifiers[node.modifiers.length - 1].end;
322+
while (/\s/.test(result.original[end])) end += 1;
323+
result.overwrite(node.getStart(), end, modifiers);
324+
} else if (modifiers) {
325+
result.prependRight(node.getStart(), modifiers);
326+
}
303327

304-
if (default_modifier) {
305-
let b = default_modifier.end;
306-
const a = b - 7;
307-
while (/\s/.test(module.dts[b])) b += 1;
308-
result.remove(a, b);
309-
}
328+
const loc = module.locator(identifier.getStart(module.ast));
310329

311-
if (identifier && name) {
312-
const pos = identifier.getStart(module.ast);
313-
const loc = module.locator(pos);
314-
315-
if (loc) {
316-
// the sourcemaps generated by TypeScript are very inaccurate, borderline useless.
317-
// we need to fix them up here. TODO is it only inaccurate in the JSDoc case?
318-
const segments = module.source?.mappings?.[loc.line - 1];
319-
320-
if (module.source && segments) {
321-
// find the segments immediately before and after the generated column
322-
const index = segments.findIndex((segment) => segment[0] >= loc.column);
323-
324-
const a = segments[index - 1] ?? segments[0];
325-
if (a) {
326-
let l = /** @type {number} */ (a[2]);
327-
328-
const source_line = module.source.code.split('\n')[l];
329-
const regex = new RegExp(`\\b${name}\\b`);
330-
const match = regex.exec(source_line);
331-
332-
if (match) {
333-
const mapping = {
334-
source: path.resolve(
335-
path.dirname(module.file),
336-
module.source.map.sources[0]
337-
),
338-
line: l + 1,
339-
column: match.index
340-
};
341-
mappings.set(name, /** @type {Mapping} */ (mapping));
342-
} else {
343-
// TODO figure out how to repair sourcemaps in this case
344-
}
345-
} else {
346-
// TODO how does this happen?
347-
}
348-
} else {
349-
const mapping = {
350-
source: module.file,
351-
line: loc.line,
352-
column: loc.column
353-
};
354-
mappings.set(name, /** @type {Mapping} */ (mapping));
355-
}
356-
}
357-
}
330+
if (loc) {
331+
// the sourcemaps generated by TypeScript are very inaccurate, borderline useless.
332+
// we need to fix them up here. TODO is it only inaccurate in the JSDoc case?
333+
const segments = module.source?.mappings?.[loc.line - 1];
334+
335+
if (module.source && segments) {
336+
// find the segments immediately before and after the generated column
337+
const index = segments.findIndex((segment) => segment[0] >= loc.column);
338+
339+
const a = segments[index - 1] ?? segments[0];
340+
if (a) {
341+
let l = /** @type {number} */ (a[2]);
342+
343+
const source_line = module.source.code.split('\n')[l];
344+
const regex = new RegExp(`\\b${name}\\b`);
345+
const match = regex.exec(source_line);
358346

359-
if (!exports.has(declaration.alias)) {
360-
// remove all export keywords in the initial pass; reinstate as necessary later
361-
let b = export_modifier.end;
362-
const a = b - 6;
363-
while (/\s/.test(module.dts[b])) b += 1;
364-
result.remove(a, b);
347+
if (match) {
348+
const mapping = {
349+
source: path.resolve(path.dirname(module.file), module.source.map.sources[0]),
350+
line: l + 1,
351+
column: match.index
352+
};
353+
mappings.set(name, /** @type {Mapping} */ (mapping));
354+
} else {
355+
// TODO figure out how to repair sourcemaps in this case
365356
}
366-
} else if (declaration.exported) {
367-
export_specifiers.push(declaration.alias);
357+
} else {
358+
// TODO how does this happen?
368359
}
360+
} else {
361+
mappings.set(name, {
362+
source: module.file,
363+
line: loc.line,
364+
column: loc.column
365+
});
369366
}
367+
}
370368

371-
const declare_modifier = node.modifiers?.find((node) => tsu.isDeclareKeyword(node));
372-
if (declare_modifier) {
373-
// i'm not sure why typescript turns `export function` in a .ts file to `export declare function`,
374-
// but it's weird and we don't want it
375-
let b = declare_modifier.end;
376-
const a = b - 7;
377-
while (/\s/.test(module.dts[b])) b += 1;
378-
result.remove(a, b);
369+
walk(node, (node) => {
370+
if (ts.isPropertySignature(node) && is_internal(node) && options.stripInternal) {
371+
result.remove(node.pos, node.end);
372+
return false;
379373
}
380374

381-
walk(node, (node) => {
382-
if (ts.isPropertySignature(node) && is_internal(node) && options.stripInternal) {
383-
result.remove(node.pos, node.end);
384-
return false;
385-
}
375+
// We need to include the declarations because if references to them have changed, we need to update the declarations, too
376+
if (is_reference(node, true)) {
377+
const name = node.getText(module.ast);
386378

387-
// We need to include the declarations because if references to them have changed, we need to update the declarations, too
388-
if (is_reference(node, true)) {
389-
const name = node.getText(module.ast);
379+
const { alias } = trace(module.file, name);
390380

391-
const declaration = trace(module.file, name);
392-
393-
if (
394-
declaration.alias !== name &&
395-
declaration.alias &&
396-
declaration.alias !== 'default'
397-
) {
398-
result.overwrite(node.getStart(module.ast), node.getEnd(), declaration.alias);
399-
}
381+
if (alias !== name) {
382+
result.overwrite(node.getStart(module.ast), node.getEnd(), alias);
400383
}
384+
}
401385

402-
// `import('./foo').Foo` -> `Foo`
403-
if (
404-
ts.isImportTypeNode(node) &&
405-
ts.isLiteralTypeNode(node.argument) &&
406-
ts.isStringLiteral(node.argument.literal) &&
407-
node.argument.literal.text.startsWith('.')
408-
) {
409-
// follow import
410-
const resolved = resolve_dts(path.dirname(module.file), node.argument.literal.text);
411-
412-
// included.add(resolved);
413-
// remove the `import(...)`
414-
if (node.qualifier) {
415-
const name = node.qualifier.getText(module.ast);
416-
const declaration = trace(resolved, name);
417-
418-
result.overwrite(node.getStart(module.ast), node.qualifier.end, declaration.alias);
419-
} else {
420-
throw new Error('TODO');
421-
}
386+
// `import('./foo').Foo` -> `Foo`
387+
if (
388+
ts.isImportTypeNode(node) &&
389+
ts.isLiteralTypeNode(node.argument) &&
390+
ts.isStringLiteral(node.argument.literal) &&
391+
node.argument.literal.text.startsWith('.')
392+
) {
393+
// follow import
394+
const resolved = resolve_dts(path.dirname(module.file), node.argument.literal.text);
395+
396+
// included.add(resolved);
397+
// remove the `import(...)`
398+
if (node.qualifier) {
399+
const name = node.qualifier.getText(module.ast);
400+
const declaration = trace(resolved, name);
401+
402+
result.overwrite(node.getStart(module.ast), node.qualifier.end, declaration.alias);
403+
} else {
404+
throw new Error('TODO');
422405
}
406+
}
423407

424-
clean_jsdoc(node, result);
425-
});
426-
}
408+
clean_jsdoc(node, result);
409+
});
427410
});
428411

429412
const mod = result
@@ -542,7 +525,8 @@ export function create_module_declaration(id, entry, created, resolve, options)
542525
module: '<builtin>',
543526
external: false,
544527
included: true,
545-
exported: false,
528+
export: false,
529+
default: false,
546530
name,
547531
alias: name,
548532
dependencies: [],
@@ -570,7 +554,8 @@ function create_external_declaration(binding, alias) {
570554
module: binding.id,
571555
name: binding.name,
572556
alias: '',
573-
exported: false,
557+
export: false,
558+
default: false,
574559
external: true,
575560
included: false,
576561
dependencies: [],

src/types.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ interface Declaration {
1111
module: string;
1212
name: string;
1313
alias: string;
14-
exported: boolean;
14+
/**
15+
* Whether this declaration should have an `export` modifier in the output
16+
*/
17+
export: boolean;
18+
/**
19+
* Whether this declaration should have a `default` modifier in the output
20+
*/
21+
default: boolean;
1522
external: boolean;
1623
included: boolean;
1724
dependencies: Reference[];

src/utils.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,8 @@ export function get_dts(file, created, resolve, options) {
352352
module: file,
353353
name,
354354
alias: '',
355-
exported: false,
355+
export: false,
356+
default: false,
356357
included: false,
357358
external: false,
358359
dependencies: [],
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Add two numbers
3+
* @param {number} a
4+
* @param {number} b
5+
* @returns {number}
6+
*/
7+
export function add(a, b) {
8+
return a + b;
9+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { add as default } from './add.js';

0 commit comments

Comments
 (0)