Skip to content

Commit

Permalink
add TypeScript support (#83)
Browse files Browse the repository at this point in the history
Co-authored-by: Conduitry <git@chor.date>
  • Loading branch information
dummdidumm and Conduitry committed Feb 15, 2021
1 parent 5efeb5a commit 192bf82
Show file tree
Hide file tree
Showing 17 changed files with 732 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -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
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
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
@@ -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
@@ -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));
}

0 comments on commit 192bf82

Please sign in to comment.