eslint-plugin-svelte3
Advanced tools
Comparing version 3.0.0 to 3.1.0
@@ -0,1 +1,5 @@ | ||
# 3.1.0 | ||
- Add TypeScript support | ||
# 3.0.0 | ||
@@ -2,0 +6,0 @@ |
385
index.js
@@ -103,2 +103,3 @@ 'use strict'; | ||
processor_options.named_blocks = settings['svelte3/named-blocks']; | ||
processor_options.typescript = settings['svelte3/typescript']; | ||
// call original Linter#verify | ||
@@ -118,2 +119,279 @@ return verify.call(this, code, config, options); | ||
var charToInteger = {}; | ||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; | ||
for (var i = 0; i < chars.length; i++) { | ||
charToInteger[chars.charCodeAt(i)] = i; | ||
} | ||
function decode(mappings) { | ||
var decoded = []; | ||
var line = []; | ||
var segment = [ | ||
0, | ||
0, | ||
0, | ||
0, | ||
0, | ||
]; | ||
var j = 0; | ||
for (var i = 0, shift = 0, value = 0; i < mappings.length; i++) { | ||
var c = mappings.charCodeAt(i); | ||
if (c === 44) { // "," | ||
segmentify(line, segment, j); | ||
j = 0; | ||
} | ||
else if (c === 59) { // ";" | ||
segmentify(line, segment, j); | ||
j = 0; | ||
decoded.push(line); | ||
line = []; | ||
segment[0] = 0; | ||
} | ||
else { | ||
var integer = charToInteger[c]; | ||
if (integer === undefined) { | ||
throw new Error('Invalid character (' + String.fromCharCode(c) + ')'); | ||
} | ||
var hasContinuationBit = integer & 32; | ||
integer &= 31; | ||
value += integer << shift; | ||
if (hasContinuationBit) { | ||
shift += 5; | ||
} | ||
else { | ||
var shouldNegate = value & 1; | ||
value >>>= 1; | ||
if (shouldNegate) { | ||
value = value === 0 ? -0x80000000 : -value; | ||
} | ||
segment[j] += value; | ||
j++; | ||
value = shift = 0; // reset | ||
} | ||
} | ||
} | ||
segmentify(line, segment, j); | ||
decoded.push(line); | ||
return decoded; | ||
} | ||
function segmentify(line, segment, j) { | ||
// This looks ugly, but we're creating specialized arrays with a specific | ||
// length. This is much faster than creating a new array (which v8 expands to | ||
// a capacity of 17 after pushing the first item), or slicing out a subarray | ||
// (which is slow). Length 4 is assumed to be the most frequent, followed by | ||
// length 5 (since not everything will have an associated name), followed by | ||
// length 1 (it's probably rare for a source substring to not have an | ||
// associated segment data). | ||
if (j === 4) | ||
line.push([segment[0], segment[1], segment[2], segment[3]]); | ||
else if (j === 5) | ||
line.push([segment[0], segment[1], segment[2], segment[3], segment[4]]); | ||
else if (j === 1) | ||
line.push([segment[0]]); | ||
} | ||
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], | ||
}; | ||
} | ||
} | ||
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$1(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$1(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$1(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)); | ||
} | ||
let default_compiler; | ||
@@ -157,6 +435,7 @@ | ||
} | ||
// get information about the component | ||
let result; | ||
try { | ||
result = compiler.compile(text, { generate: false, ...processor_options.compiler_options }); | ||
result = compile_code(text, compiler, processor_options); | ||
} catch ({ name, message, start, end }) { | ||
@@ -177,3 +456,4 @@ // convert the error to a linting message, store it, and return | ||
} | ||
const { ast, warnings, vars } = result; | ||
const { ast, warnings, vars, mapper } = result; | ||
const references_and_reassignments = `{${vars.filter(v => v.referenced).map(v => v.name)};${vars.filter(v => v.reassigned || v.export_name).map(v => v.name + '=0')}}`; | ||
@@ -183,18 +463,31 @@ state.var_names = new Set(vars.map(v => v.name)); | ||
// convert warnings to linting messages | ||
state.messages = (processor_options.ignore_warnings ? warnings.filter(warning => !processor_options.ignore_warnings(warning)) : warnings).map(({ code, message, start, end }) => ({ | ||
ruleId: code, | ||
severity: 1, | ||
message, | ||
line: start && start.line, | ||
column: start && start.column + 1, | ||
endLine: end && end.line, | ||
endColumn: end && end.column + 1, | ||
})); | ||
const filtered_warnings = processor_options.ignore_warnings ? warnings.filter(warning => !processor_options.ignore_warnings(warning)) : warnings; | ||
state.messages = filtered_warnings.map(({ code, message, start, end }) => { | ||
const start_pos = processor_options.typescript && start ? | ||
mapper.get_original_position(start) : | ||
start && { line: start.line, column: start.column + 1 }; | ||
const end_pos = processor_options.typescript && end ? | ||
mapper.get_original_position(end) : | ||
end && { line: end.line, column: end.column + 1 }; | ||
return { | ||
ruleId: code, | ||
severity: 1, | ||
message, | ||
line: start_pos && start_pos.line, | ||
column: start_pos && start_pos.column, | ||
endLine: end_pos && end_pos.line, | ||
endColumn: end_pos && end_pos.column, | ||
}; | ||
}); | ||
// build strings that we can send along to ESLint to get the remaining messages | ||
// Things to think about: | ||
// - not all Svelte files may be typescript -> do we need a distinction on a file basis by analyzing the attribute + a config option to tell "treat all as TS"? | ||
const with_file_ending = (filename) => `${filename}${processor_options.typescript ? '.ts' : '.js'}`; | ||
if (ast.module) { | ||
// block for <script context='module'> | ||
const block = new_block(); | ||
state.blocks.set('module.js', block); | ||
state.blocks.set(with_file_ending('module'), block); | ||
@@ -213,3 +506,3 @@ get_translation(text, block, ast.module.content); | ||
const block = new_block(); | ||
state.blocks.set('instance.js', block); | ||
state.blocks.set(with_file_ending('instance'), block); | ||
@@ -226,3 +519,3 @@ block.transformed_code = vars.filter(v => v.injected || v.module).map(v => `let ${v.name};`).join(''); | ||
const block = new_block(); | ||
state.blocks.set('template.js', block); | ||
state.blocks.set(with_file_ending('template'), block); | ||
@@ -280,2 +573,66 @@ block.transformed_code = vars.map(v => `let ${v.name};`).join(''); | ||
// How it works for JS: | ||
// 1. compile code | ||
// 2. return ast/vars/warnings | ||
// How it works for TS: | ||
// 1. transpile script contents from TS to JS | ||
// 2. compile result to get Svelte compiler warnings | ||
// 3. provide a mapper to map those warnings back to its original positions | ||
// 4. blank script contents | ||
// 5. compile again to get the AST with original positions in the markdown part | ||
// 6. use AST and warnings of step 5, vars of step 2 | ||
function compile_code(text, compiler, processor_options) { | ||
let ast; | ||
let warnings; | ||
let vars; | ||
let mapper; | ||
let ts_result; | ||
if (processor_options.typescript) { | ||
const diffs = []; | ||
let accumulated_diff = 0; | ||
const transpiled = text.replace(/<script(\s[^]*?)?>([^]*?)<\/script>/gi, (match, attributes = '', content) => { | ||
const output = processor_options.typescript.transpileModule( | ||
content, | ||
{ reportDiagnostics: false, compilerOptions: { target: processor_options.typescript.ScriptTarget.ESNext, sourceMap: true } } | ||
); | ||
const original_start = text.indexOf(content); | ||
const generated_start = accumulated_diff + original_start; | ||
accumulated_diff += output.outputText.length - content.length; | ||
diffs.push({ | ||
original_start: original_start, | ||
generated_start: generated_start, | ||
generated_end: generated_start + output.outputText.length, | ||
diff: output.outputText.length - content.length, | ||
original_content: content, | ||
generated_content: output.outputText, | ||
map: output.sourceMapText | ||
}); | ||
return `<script${attributes}>${output.outputText}</script>`; | ||
}); | ||
mapper = new DocumentMapper(text, transpiled, diffs); | ||
ts_result = compiler.compile(transpiled, { generate: false, ...processor_options.compiler_options }); | ||
text = text.replace(/<script(\s[^]*?)?>([^]*?)<\/script>/gi, (match, attributes = '', content) => { | ||
return `<script${attributes}>${content | ||
// blank out the content | ||
.replace(/[^\n]/g, ' ') | ||
// excess blank space can make the svelte parser very slow (sec->min). break it up with comments (works in style/script) | ||
.replace(/[^\n][^\n][^\n][^\n]\n/g, '/**/\n') | ||
}</script>`; | ||
}); | ||
} | ||
const result = compiler.compile(text, { generate: false, ...processor_options.compiler_options }); | ||
if (!processor_options.typescript) { | ||
({ ast, warnings, vars } = result); | ||
} else { | ||
ast = result.ast; | ||
({ warnings, vars } = ts_result); | ||
} | ||
return { ast, warnings, vars, mapper }; | ||
} | ||
// transform a linting message according to the module/instance script info we've gathered | ||
@@ -282,0 +639,0 @@ const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, range }, message) => { |
{ | ||
"name": "eslint-plugin-svelte3", | ||
"version": "3.0.0", | ||
"version": "3.1.0", | ||
"description": "An ESLint plugin for Svelte v3 components.", | ||
@@ -37,6 +37,11 @@ "keywords": [ | ||
"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" | ||
} | ||
} |
@@ -59,2 +59,48 @@ # eslint-plugin-svelte3 | ||
### 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 | ||
@@ -94,3 +140,3 @@ | ||
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`. | ||
@@ -101,2 +147,8 @@ 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. | ||
### `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` | ||
@@ -103,0 +155,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
38997
679
172
8