Skip to content
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

Add TypeScript support #83

Merged
merged 13 commits into from
Feb 15, 2021
Merged
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
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
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,52 @@ 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.

### Installation with TypeScript

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:

```javascript
module.exports = {
parser: '@typescript-eslint/parser', // add the TypeScript parser
plugins: [
'svelte3',
'@typescript-eslint' // add the TypeScript plugin
],
overrides: [ // this stays the same
{
files: ['*.svelte'],
processor: 'svelte3/svelte3'
}
],
rules: {
// ...
},
settings: {
'svelte3/typescript': require('typescript'), // pass the TypeScript package to the Svelte plugin
// ...
}
};
```

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

```javascript
module.exports = {
// ...
parserOptions: { // add these parser options
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
extraFileExtensions: ['.svelte'],
},
extends: [ // then, enable whichever type-aware rules you want to use
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
// ...
};
```

## 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 +136,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 an instance of the TypeScript package. This probably means doing `'svelte3/typescript': require('typescript')`.

The default is to not enable TypeScript support.

### `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
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@
"test": "npm run build && node test"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^11.2.0",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"eslint": ">=6.0.0",
"rollup": "^2",
"svelte": "^3.2.0"
"sourcemap-codec": "1.4.8",
"svelte": "^3.2.0",
"typescript": "^4.0.0"
}
}
3 changes: 3 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import node_resolve from '@rollup/plugin-node-resolve';

export default {
input: 'src/index.js',
output: { file: 'index.js', format: 'cjs' },
plugins: [ node_resolve() ],
};
206 changes: 206 additions & 0 deletions src/mapping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { decode } from 'sourcemap-codec';

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

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

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

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

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

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

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

get_original_position(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[line];
while (line >= 0 && (!line_match || !line_match.length)) {
line -= 1;
line_match = this.decoded[line];
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, generated_position) {
// Map the position to be relative to the transpiled fragment
const position_in_transpiled_fragment = mapper.generated_fragment_mapper.get_position_relative_to_fragment(
generated_position
);
// Map the position, using the sourcemap, to the original position in the source fragment
const position_in_original_fragment = mapper.source_mapper.get_original_position(
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 retrieved
*/
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