Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/languages/ts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,13 @@ export default (options = {}) => {
context.write(' ');

if (node.specifiers.length === 0) {
if (node.importKind === 'type') context.write('type {} from ');
if (node.importKind === 'type') {
// one token per write, so that each maps to its own source token
context.write('type ');
context.write('{');
context.write('}');
context.write(' from ');
}
context.visit(node.source);
write_import_attributes(context, node);
context.write(';');
Expand Down
53 changes: 53 additions & 0 deletions test/sourcemap-keywords.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ test.each([

test.each([
['named import', `import { /* before */ value /* after */ } from 'module';`],
['empty type-only import', `import type /* before */ { /* inside */ } from 'module';`],
['named export', `const value = 1;\nexport { /* before */ value /* after */ };`],
['empty named export', `export { /* inside */ };`],
['import attributes', `import value from 'module' with { type: 'json' };`],
Expand Down Expand Up @@ -474,6 +475,7 @@ test.each([
],
['for in', `for (const k /* c */ in o) {}`, ['in']],
['import type', `import /* c */ type { T } from 'm';`, ['type']],
['empty import type', `import /* c */ type {} from 'm';`, ['type']],
['import type specifier', `import { type /* c */ T, a as /* c */ b } from 'm';`, ['type', 'as']],
['export type', `type T = 1;\nexport /* c */ type { T };`, ['type']],
['export star as', `export * as /* c */ ns from 'm';`, ['as']],
Expand Down Expand Up @@ -531,3 +533,54 @@ test.each([
])('tokens map the keywords of a decorated declaration: %s', (_name, source, words) => {
expectExactKeywordMappings(source, words);
});

/**
* The source position of every bracket in `code` that is not inside a comment
* @param {string} source
* @param {string} code
* @param {[number, number, number, number][][]} mappings
*/
function bracketMappings(source, code, mappings) {
/** @type {{ char: string, generated: number, source: number[] }[]} */
const result = [];
const stripped = code.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
for (let i = 0; i < stripped.length; i += 1) {
if (!'()[]{}'.includes(stripped[i])) continue;
result.push({
char: code[i],
generated: i,
source: mappingsAtIndex(code, i, mappings).map((s) => s[2] * 1e6 + s[3])
});
}
return result;
}

test.each([
// a comment is flushed once the next node's tokens are in view, so a comment
// whose text is that node's opening bracket is the case that can go wrong
['a comment before an object', `const o = /* { */ { a: 1 };`],
['a comment before an array', `const arr = /* [ */ [1];`],
['a comment before an arrow function', `const f = /* ( */ (a) => a;`],
['a comment line before an arrow function', `const f = /*\n(\n*/ (a) => a;`],
['a comment line before a block', `if (x) /*\n{\n*/ { y(); }`],
['a line comment', `fn(value); // )\nfn(other);`],
['commented-out code', `function f() {\n\t/*\n\tif (x) {\n\t\ty();\n\t}\n\t*/\n\tg(1);\n}`]
])('brackets in %s are not mistaken for source tokens', (_name, source) => {
const { code, mappings } = mappedWithTokens(source, { tokens: true });
const stripped = source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
/** @type {number[]} */
const expected = [];
for (let i = 0; i < stripped.length; i += 1) {
if ('()[]{}'.includes(stripped[i])) {
const { gen_line, gen_col } = generatedLineColumn(source, i);
expected.push(gen_line * 1e6 + gen_col);
}
}
const actual = bracketMappings(source, code, mappings);
expect(actual.map((b) => b.char).join('')).toBe(
[...stripped].filter((c) => '()[]{}'.includes(c)).join('')
);
for (let i = 0; i < actual.length; i += 1) {
expect(actual[i].source, `${actual[i].char} at ${actual[i].generated}`).toContain(expected[i]);
}
});
Loading