Skip to content

Commit 192bf82

Browse files
add TypeScript support (#83)
Co-authored-by: Conduitry <git@chor.date>
1 parent 5efeb5a commit 192bf82

File tree

17 files changed

+732
-16
lines changed

17 files changed

+732
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 3.1.0 (Unreleased)
2+
3+
- Add TypeScript support
4+
15
# 3.0.0
26

37
- Breaking change: Node 10+ is now required

README.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,52 @@ module.exports = {
5656

5757
By default, this plugin needs to be able to `require('svelte/compiler')`. If ESLint, this plugin, and Svelte are all installed locally in your project, this should not be a problem.
5858

59+
### Installation with TypeScript
60+
61+
If you want to use TypeScript, you'll need a different ESLint configuration. In addition to the Svelte plugin, you also need the ESLint TypeScript parser and plugin. Install `typescript`, `@typescript-eslint/parser` and `@typescript-eslint/eslint-plugin` from npm and then adjust your config like this:
62+
63+
```javascript
64+
module.exports = {
65+
parser: '@typescript-eslint/parser', // add the TypeScript parser
66+
plugins: [
67+
'svelte3',
68+
'@typescript-eslint' // add the TypeScript plugin
69+
],
70+
overrides: [ // this stays the same
71+
{
72+
files: ['*.svelte'],
73+
processor: 'svelte3/svelte3'
74+
}
75+
],
76+
rules: {
77+
// ...
78+
},
79+
settings: {
80+
'svelte3/typescript': require('typescript'), // pass the TypeScript package to the Svelte plugin
81+
// ...
82+
}
83+
};
84+
```
85+
86+
If you also want to be able to use type-aware linting rules (which will result in slower linting, because the whole program needs to be compiled and type-checked), then you also need to add some `parserOptions` configuration. The values below assume that your ESLint config is at the root of your project next to your `tsconfig.json`. For more information, see [here](https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/TYPED_LINTING.md).
87+
88+
```javascript
89+
module.exports = {
90+
// ...
91+
parserOptions: { // add these parser options
92+
tsconfigRootDir: __dirname,
93+
project: ['./tsconfig.json'],
94+
extraFileExtensions: ['.svelte'],
95+
},
96+
extends: [ // then, enable whichever type-aware rules you want to use
97+
'eslint:recommended',
98+
'plugin:@typescript-eslint/recommended',
99+
'plugin:@typescript-eslint/recommended-requiring-type-checking'
100+
],
101+
// ...
102+
};
103+
```
104+
59105
## Interactions with other plugins
60106

61107
Care needs to be taken when using this plugin alongside others. Take a look at [this list of things you need to watch out for](OTHER_PLUGINS.md).
@@ -90,12 +136,18 @@ The default is to not ignore any styles.
90136

91137
### `svelte3/named-blocks`
92138

93-
When an [ESLint processor](https://eslint.org/docs/user-guide/configuring#specifying-processor) processes a file, it is able to output named code blocks, which can each have their own linting configuration. When this setting is enabled, the code extracted from `<script context='module'>` tag, the `<script>` tag, and the template are respectively given the block names `module.js`, `instance.js`, and `template.js`.
139+
When an [ESLint processor](https://eslint.org/docs/user-guide/configuring/plugins#specifying-processor) processes a file, it is able to output named code blocks, which can each have their own linting configuration. When this setting is enabled, the code extracted from `<script context='module'>` tag, the `<script>` tag, and the template are respectively given the block names `module.js`, `instance.js`, and `template.js`.
94140

95141
This means that to override linting rules in Svelte components, you'd instead have to target `**/*.svelte/*.js`. But it also means that you can define an override targeting `**/*.svelte/*_template.js` for example, and that configuration will only apply to linting done on the templates in Svelte components.
96142

97143
The default is to not use named code blocks.
98144

145+
### `svelte3/typescript`
146+
147+
If you use TypeScript inside your Svelte components and want ESLint support, you need to set this option. It expects an instance of the TypeScript package. This probably means doing `'svelte3/typescript': require('typescript')`.
148+
149+
The default is to not enable TypeScript support.
150+
99151
### `svelte3/compiler`
100152

101153
In some esoteric setups, this plugin might not be able to find the correct instance of the Svelte compiler to use.

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@
3434
"test": "npm run build && node test"
3535
},
3636
"devDependencies": {
37+
"@rollup/plugin-node-resolve": "^11.2.0",
38+
"@typescript-eslint/eslint-plugin": "^4.14.2",
39+
"@typescript-eslint/parser": "^4.14.2",
3740
"eslint": ">=6.0.0",
3841
"rollup": "^2",
39-
"svelte": "^3.2.0"
42+
"sourcemap-codec": "1.4.8",
43+
"svelte": "^3.2.0",
44+
"typescript": "^4.0.0"
4045
}
4146
}

rollup.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import node_resolve from '@rollup/plugin-node-resolve';
2+
13
export default {
24
input: 'src/index.js',
35
output: { file: 'index.js', format: 'cjs' },
6+
plugins: [ node_resolve() ],
47
};

src/mapping.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { decode } from 'sourcemap-codec';
2+
3+
class GeneratedFragmentMapper {
4+
constructor(generated_code, diff) {
5+
this.generated_code = generated_code;
6+
this.diff = diff;
7+
}
8+
9+
get_position_relative_to_fragment(position_relative_to_file) {
10+
const fragment_offset = this.offset_in_fragment(offset_at(position_relative_to_file, this.generated_code));
11+
return position_at(fragment_offset, this.diff.generated_content);
12+
}
13+
14+
offset_in_fragment(offset) {
15+
return offset - this.diff.generated_start
16+
}
17+
}
18+
19+
class OriginalFragmentMapper {
20+
constructor(original_code, diff) {
21+
this.original_code = original_code;
22+
this.diff = diff;
23+
}
24+
25+
get_position_relative_to_file(position_relative_to_fragment) {
26+
const parent_offset = this.offset_in_parent(offset_at(position_relative_to_fragment, this.diff.original_content));
27+
return position_at(parent_offset, this.original_code);
28+
}
29+
30+
offset_in_parent(offset) {
31+
return this.diff.original_start + offset;
32+
}
33+
}
34+
35+
class SourceMapper {
36+
constructor(raw_source_map) {
37+
this.raw_source_map = raw_source_map;
38+
}
39+
40+
get_original_position(generated_position) {
41+
if (generated_position.line < 0) {
42+
return { line: -1, column: -1 };
43+
}
44+
45+
// Lazy-load
46+
if (!this.decoded) {
47+
this.decoded = decode(JSON.parse(this.raw_source_map).mappings);
48+
}
49+
50+
let line = generated_position.line;
51+
let column = generated_position.column;
52+
53+
let line_match = this.decoded[line];
54+
while (line >= 0 && (!line_match || !line_match.length)) {
55+
line -= 1;
56+
line_match = this.decoded[line];
57+
if (line_match && line_match.length) {
58+
return {
59+
line: line_match[line_match.length - 1][2],
60+
column: line_match[line_match.length - 1][3]
61+
};
62+
}
63+
}
64+
65+
if (line < 0) {
66+
return { line: -1, column: -1 };
67+
}
68+
69+
const column_match = line_match.find((col, idx) =>
70+
idx + 1 === line_match.length ||
71+
(col[0] <= column && line_match[idx + 1][0] > column)
72+
);
73+
74+
return {
75+
line: column_match[2],
76+
column: column_match[3],
77+
};
78+
}
79+
}
80+
81+
export class DocumentMapper {
82+
constructor(original_code, generated_code, diffs) {
83+
this.original_code = original_code;
84+
this.generated_code = generated_code;
85+
this.diffs = diffs;
86+
this.mappers = diffs.map(diff => {
87+
return {
88+
start: diff.generated_start,
89+
end: diff.generated_end,
90+
diff: diff.diff,
91+
generated_fragment_mapper: new GeneratedFragmentMapper(generated_code, diff),
92+
source_mapper: new SourceMapper(diff.map),
93+
original_fragment_mapper: new OriginalFragmentMapper(original_code, diff)
94+
}
95+
});
96+
}
97+
98+
get_original_position(generated_position) {
99+
generated_position = { line: generated_position.line - 1, column: generated_position.column };
100+
const offset = offset_at(generated_position, this.generated_code);
101+
let original_offset = offset;
102+
for (const mapper of this.mappers) {
103+
if (offset >= mapper.start && offset <= mapper.end) {
104+
return this.map(mapper, generated_position);
105+
}
106+
if (offset > mapper.end) {
107+
original_offset -= mapper.diff;
108+
}
109+
}
110+
const original_position = position_at(original_offset, this.original_code);
111+
return this.to_ESLint_position(original_position);
112+
}
113+
114+
map(mapper, generated_position) {
115+
// Map the position to be relative to the transpiled fragment
116+
const position_in_transpiled_fragment = mapper.generated_fragment_mapper.get_position_relative_to_fragment(
117+
generated_position
118+
);
119+
// Map the position, using the sourcemap, to the original position in the source fragment
120+
const position_in_original_fragment = mapper.source_mapper.get_original_position(
121+
position_in_transpiled_fragment
122+
);
123+
// Map the position to be in the original fragment's parent
124+
const original_position = mapper.original_fragment_mapper.get_position_relative_to_file(position_in_original_fragment);
125+
return this.to_ESLint_position(original_position);
126+
}
127+
128+
to_ESLint_position(position) {
129+
// ESLint line/column is 1-based
130+
return { line: position.line + 1, column: position.column + 1 };
131+
}
132+
133+
}
134+
135+
/**
136+
* Get the offset of the line and character position
137+
* @param position Line and character position
138+
* @param text The text for which the offset should be retrieved
139+
*/
140+
function offset_at(position, text) {
141+
const line_offsets = get_line_offsets(text);
142+
143+
if (position.line >= line_offsets.length) {
144+
return text.length;
145+
} else if (position.line < 0) {
146+
return 0;
147+
}
148+
149+
const line_offset = line_offsets[position.line];
150+
const next_line_offset =
151+
position.line + 1 < line_offsets.length ? line_offsets[position.line + 1] : text.length;
152+
153+
return clamp(next_line_offset, line_offset, line_offset + position.column);
154+
}
155+
156+
function position_at(offset, text) {
157+
offset = clamp(offset, 0, text.length);
158+
159+
const line_offsets = get_line_offsets(text);
160+
let low = 0;
161+
let high = line_offsets.length;
162+
if (high === 0) {
163+
return { line: 0, column: offset };
164+
}
165+
166+
while (low < high) {
167+
const mid = Math.floor((low + high) / 2);
168+
if (line_offsets[mid] > offset) {
169+
high = mid;
170+
} else {
171+
low = mid + 1;
172+
}
173+
}
174+
175+
// low is the least x for which the line offset is larger than the current offset
176+
// or array.length if no line offset is larger than the current offset
177+
const line = low - 1;
178+
return { line, column: offset - line_offsets[line] };
179+
}
180+
181+
function get_line_offsets(text) {
182+
const line_offsets = [];
183+
let is_line_start = true;
184+
185+
for (let i = 0; i < text.length; i++) {
186+
if (is_line_start) {
187+
line_offsets.push(i);
188+
is_line_start = false;
189+
}
190+
const ch = text.charAt(i);
191+
is_line_start = ch === '\r' || ch === '\n';
192+
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
193+
i++;
194+
}
195+
}
196+
197+
if (is_line_start && text.length > 0) {
198+
line_offsets.push(text.length);
199+
}
200+
201+
return line_offsets;
202+
}
203+
204+
function clamp(num, min, max) {
205+
return Math.max(min, Math.min(max, num));
206+
}

0 commit comments

Comments
 (0)