Skip to content

Add TypeScript support #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 3.1.0 (Unreleased)

- Add TypeScript support

# 3.0.0

- Breaking change: Node 10+ is now required
Expand Down
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,37 @@ module.exports = {

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.

If you want to use TypeScript, you need to adjust your ESLint configuration. In addition to the Svelte plugin, you also need the ESLint-TypeScript plugin. You need to install `typescript`, `@typescript-eslint/parser` and `@typescript-eslint/eslint-plugin` from npm and then adjust your config like this:

```javascript
module.exports = {
parser: '@typescript-eslint/parser', // add the TypeScript parser
extends: [
// optional - a standard rule set
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
// optional - if you want type-aware rules (also see `parserOptions` below).
// Note that this results in slower checks
// because the whole program needs to be compiled and type checked
'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
plugins: ['svelte3', '@typescript-eslint'], // add the TypeScript plugin
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], // this stays the same
settings: {
'svelte3/typescript': require('typescript'), // pass the TypeScript package to the Svelte plugin
// ...
},
// The following is only needed if you want to use type-aware rules
// It assumes that your eslint config is at the root next to your tsconfig.json
// More info: https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/TYPED_LINTING.md
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
extraFileExtensions: ['.svelte'],
},
}
```

## Interactions with other plugins

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).
Expand Down Expand Up @@ -90,12 +121,18 @@ The default is to not ignore any styles.

### `svelte3/named-blocks`

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`.
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`.

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.

The default is to not use named code blocks.

### `svelte3/typescript`

If you use TypeScript inside your Svelte components and want ESLint support, you need to set this option. It expects the TypeScript package.

Example: `"svelte3/typescript": require("typescript")`

### `svelte3/compiler`

In some esoteric setups, this plugin might not be able to find the correct instance of the Svelte compiler to use.
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@
"test": "npm run build && node test"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"eslint": ">=6.0.0",
"rollup": "^2",
"svelte": "^3.2.0"
"svelte": "^3.2.0",
"typescript": "^4.0.0"
},
"dependencies": {
"sourcemap-codec": "^1.4.8"
}
}
212 changes: 212 additions & 0 deletions src/mapping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { decode } from 'sourcemap-codec';

class GeneratedFragmentMapper {
constructor(
generated_code,
tag_info,
) {
this.generated_code = generated_code;
this.tag_info = tag_info;
}

get_position_relative_to_fragment(positionRelativeToFile) {
const fragment_offset = this.offset_in_fragment(offset_at(positionRelativeToFile, this.generated_code));
return position_at(fragment_offset, this.tag_info.generated_content);
}

offset_in_fragment(offset) {
return offset - this.tag_info.generated_start
}
}

class OriginalFragmentMapper {
constructor(
original_code,
tag_info,
) {
this.original_code = original_code;
this.tag_info = tag_info;
}

get_position_relative_to_file(positionRelativeToFragment) {
const parent_offset = this.offset_in_parent(offset_at(positionRelativeToFragment, this.tag_info.original_content));
return position_at(parent_offset, this.original_code);
}

offset_in_parent(offset) {
return this.tag_info.original_start + offset;
}
}

class SourceMapper {
constructor(raw_source_map) {
this.raw_source_map = raw_source_map;
}

getOriginalPosition(generated_position) {
if (generated_position.line < 0) {
return { line: -1, column: -1 };
}

// Lazy-load
if (!this.decoded) {
this.decoded = decode(JSON.parse(this.raw_source_map).mappings);
}

let line = generated_position.line;
let column = generated_position.column;

let line_match = this.decoded[generated_position.line];
while (line >= 0 && (!line_match || !line_match.length)) {
line -= 1;
line_match = this.decoded[generated_position];
if (line_match && line_match.length) {
return {
line: line_match[line_match.length - 1][2],
column: line_match[line_match.length - 1][3]
};
}
}

if (line < 0) {
return { line: -1, column: -1 };
}

const column_match = line_match.find((col, idx) =>
idx + 1 === line_match.length ||
(col[0] <= column && line_match[idx + 1][0] > column)
);

return {
line: column_match[2],
column: column_match[3],
};
}
}

export class DocumentMapper {
constructor(original_code, generated_code, diffs) {
this.original_code = original_code;
this.generated_code = generated_code;
this.diffs = diffs;
this.mappers = diffs.map(diff => {
return {
start: diff.generated_start,
end: diff.generated_end,
diff: diff.diff,
generated_fragment_mapper: new GeneratedFragmentMapper(generated_code, diff),
source_mapper: new SourceMapper(diff.map),
original_fragment_mapper: new OriginalFragmentMapper(original_code, diff)
}
});
}

get_original_position(generated_position) {
generated_position = { line: generated_position.line - 1, column: generated_position.column };
const offset = offset_at(generated_position, this.generated_code);
let original_offset = offset;
for (const mapper of this.mappers) {
if (offset >= mapper.start && offset <= mapper.end) {
return this.map(mapper, generated_position);
}
if (offset > mapper.end) {
original_offset -= mapper.diff;
}
}
const original_position = position_at(original_offset, this.original_code);
return this.to_ESLint_position(original_position);
}

map(mapper, generatedPosition) {
// Map the position to be relative to the transpiled fragment
const position_in_transpiled_fragment = mapper.generated_fragment_mapper.get_position_relative_to_fragment(
generatedPosition
);
// Map the position, using the sourcemap, to the original position in the source fragment
const position_in_original_fragment = mapper.source_mapper.getOriginalPosition(
position_in_transpiled_fragment
);
// Map the position to be in the original fragment's parent
const original_position = mapper.original_fragment_mapper.get_position_relative_to_file(position_in_original_fragment);
return this.to_ESLint_position(original_position);
}

to_ESLint_position(position) {
// ESLint line/column is 1-based
return { line: position.line + 1, column: position.column + 1 };
}

}

/**
* Get the offset of the line and character position
* @param position Line and character position
* @param text The text for which the offset should be retrived
*/
function offset_at(position, text) {
const line_offsets = get_line_offsets(text);

if (position.line >= line_offsets.length) {
return text.length;
} else if (position.line < 0) {
return 0;
}

const line_offset = line_offsets[position.line];
const next_line_offset =
position.line + 1 < line_offsets.length ? line_offsets[position.line + 1] : text.length;

return clamp(next_line_offset, line_offset, line_offset + position.column);
}

function position_at(offset, text) {
offset = clamp(offset, 0, text.length);

const line_offsets = get_line_offsets(text);
let low = 0;
let high = line_offsets.length;
if (high === 0) {
return { line: 0, column: offset };
}

while (low < high) {
const mid = Math.floor((low + high) / 2);
if (line_offsets[mid] > offset) {
high = mid;
} else {
low = mid + 1;
}
}

// low is the least x for which the line offset is larger than the current offset
// or array.length if no line offset is larger than the current offset
const line = low - 1;
return { line, column: offset - line_offsets[line] };
}

function get_line_offsets(text) {
const line_offsets = [];
let is_line_start = true;

for (let i = 0; i < text.length; i++) {
if (is_line_start) {
line_offsets.push(i);
is_line_start = false;
}
const ch = text.charAt(i);
is_line_start = ch === '\r' || ch === '\n';
if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
i++;
}
}

if (is_line_start && text.length > 0) {
line_offsets.push(text.length);
}

return line_offsets;
}

function clamp(num, min, max) {
return Math.max(min, Math.min(max, num));
}
Loading