Skip to content

Commit 994857e

Browse files
authored
fix(*): improve support for inline and .html templates (#47)
1 parent 81d0f02 commit 994857e

File tree

11 files changed

+126
-93
lines changed

11 files changed

+126
-93
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 James Henry
3+
Copyright (c) 2020 James Henry
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

packages/builder/LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 James Henry
3+
Copyright (c) 2020 James Henry
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

packages/builder/src/index.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ function getFilesToLint(
6464
const ignore = options.exclude;
6565
const files = options.files || [];
6666

67+
const componentHTMLFiles = ['**/*.component.html']
68+
.map(file => glob.sync(file, { cwd: root, ignore: ignore, nodir: true }))
69+
.reduce((prev, curr) => prev.concat(curr), [])
70+
.map(file => path.join(root, file));
71+
6772
if (files.length > 0) {
6873
return files
6974
.map((file: any) => glob.sync(file, { cwd: root, ignore, nodir: true }))
@@ -75,7 +80,10 @@ function getFilesToLint(
7580
return [];
7681
}
7782

78-
let programFiles = getFileNamesFromProgram(program);
83+
let programFiles = [
84+
...componentHTMLFiles,
85+
...getFileNamesFromProgram(program),
86+
];
7987

8088
if (ignore && ignore.length > 0) {
8189
// normalize to support ./ paths
@@ -189,14 +197,15 @@ async function _lint(
189197

190198
const cli = new projectESLint.CLIEngine({
191199
configFile: eslintConfigPath,
200+
extensions: ['.ts', '.html'],
192201
useEslintrc: false,
193202
fix: !!options.fix,
194203
cache: !!options.cache,
195204
});
196205

197206
const lintReports: eslint.CLIEngine.LintReport[] = [];
198207
for (const file of files) {
199-
if (program && allPrograms) {
208+
if (file.endsWith('.ts') && program && allPrograms) {
200209
// If it cannot be found in ANY program, then this is an error.
201210
if (allPrograms.every(p => p.getSourceFile(file) === undefined)) {
202211
throw new Error(

packages/eslint-plugin-template/LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 James Henry
3+
Copyright (c) 2020 James Henry
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

packages/eslint-plugin-template/src/processors.ts

+71-70
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ function quickExtractComponentDecorator(text: string) {
1111
function quickStripComponentDecoratorFromMetadata(
1212
componentDecoratorMatch: string,
1313
): string {
14-
// remove @Component()
1514
return componentDecoratorMatch
1615
.slice(0, componentDecoratorMatch.length - 1)
1716
.replace('@Component(', '');
@@ -24,46 +23,27 @@ function quickGetRangeForTemplate(text: string, template: string) {
2423

2524
const rangeMap = new Map();
2625

27-
export function preprocessTSFile(text: string, filename: string) {
26+
export function preprocessComponentFile(text: string, filename: string) {
2827
if (!filename.endsWith('.component.ts')) {
29-
// console.log("preprocess: Ignoring non-component source file", filename);
30-
return [''];
28+
return [text];
3129
}
32-
3330
/**
3431
* Ignore malformed Component files
3532
*/
3633
const componentDecoratorMatch = quickExtractComponentDecorator(text);
3734
if (!componentDecoratorMatch) {
38-
// console.log(
39-
// "preprocess: Ignoring component with no detectable @Component() decorator",
40-
// filename
41-
// );
42-
return [''];
35+
return [text];
4336
}
4437
/**
4538
* Ignore Components which have external template files, they will be linted directly
4639
*/
47-
if (componentDecoratorMatch.includes('templateUrl')) {
48-
// console.log(
49-
// "preprocess: Ignoring component with external template",
50-
// filename
51-
// );
52-
return [''];
53-
}
54-
55-
if (!componentDecoratorMatch.includes('template')) {
56-
// console.log(
57-
// "preprocess: Ignoring component with neither inline nor external template",
58-
// filename
59-
// );
60-
return [''];
40+
if (
41+
componentDecoratorMatch.includes('templateUrl') ||
42+
!componentDecoratorMatch.includes('template')
43+
) {
44+
return [text];
6145
}
6246

63-
// console.log(
64-
// "preprocess: Extracting inline template for Component file",
65-
// filename
66-
// );
6747
try {
6848
const metadataText = quickStripComponentDecoratorFromMetadata(
6949
componentDecoratorMatch,
@@ -78,10 +58,6 @@ export function preprocessTSFile(text: string, filename: string) {
7858

7959
const range = quickGetRangeForTemplate(text, metadata.template);
8060

81-
// console.log(
82-
// "preprocess: creating TS SourceFile for Component file",
83-
// filename
84-
// );
8561
const sourceFile = ts.createSourceFile(
8662
filename,
8763
text,
@@ -96,63 +72,88 @@ export function preprocessTSFile(text: string, filename: string) {
9672
end: sourceFile.getLineAndCharacterOfPosition(range[1]),
9773
},
9874
});
99-
100-
return [metadata.template]; // return an array of strings to lint
75+
/**
76+
* We return an array containing both the original source, and a new fragment
77+
* representing the inline HTML template. It must have an appropriate .html
78+
* extension so that it can be linted using the right rules and plugins.
79+
*
80+
* The postprocessor will handle tying things back to the right position
81+
* in the original file, so this temporary filename will never be visible
82+
* to the end user.
83+
*/
84+
return [
85+
text,
86+
{
87+
text: metadata.template,
88+
filename: 'inline-template.component.html',
89+
},
90+
];
10191
} catch (err) {
10292
console.log(err);
10393
console.error(
10494
'preprocess: ERROR could not parse @Component() metadata',
10595
filename,
10696
);
107-
return [''];
97+
return [text];
10898
}
10999
}
110100

111-
export function postprocessTSFile(
101+
export function postprocessComponentFile(
112102
multiDimensionalMessages: any[][],
113103
filename: string,
114104
) {
115-
const messages = multiDimensionalMessages[0];
116-
if (!messages.length) {
117-
return messages;
105+
const messagesFromComponentSource = multiDimensionalMessages[0];
106+
const messagesFromInlineTemplateHTML = multiDimensionalMessages[1];
107+
/**
108+
* If the Component did not have an inline template the second item
109+
* in the multiDimensionalMessages will not exist
110+
*/
111+
if (
112+
!messagesFromInlineTemplateHTML ||
113+
!messagesFromInlineTemplateHTML.length
114+
) {
115+
return messagesFromComponentSource;
118116
}
119117
const rangeData = rangeMap.get(filename);
120118
if (!rangeData) {
121-
return messages;
119+
return messagesFromComponentSource;
122120
}
123-
// console.log("postprocess: Found original range data", rangeData, messages);
124-
125-
// adjust message location data
126-
return messages.map(
127-
(message: {
128-
line: string | number;
129-
column: any;
130-
endLine: string | number;
131-
endColumn: any;
132-
fix: { range: [number, number]; text: string };
133-
}) => {
134-
// console.log("message before", message);
135-
message.line = message.line + rangeData.lineAndCharacter.start.line;
136-
message.column = message.column;
137-
138-
message.endLine = message.endLine + rangeData.lineAndCharacter.start.line;
139-
message.endColumn = message.endColumn;
140-
141-
const startOffset = rangeData.range[0];
142-
message.fix.range = [
143-
startOffset + message.fix.range[0],
144-
startOffset + message.fix.range[1],
145-
];
146-
// console.log(message);
147-
return message;
148-
},
149-
);
121+
/**
122+
* Adjust message location data to apply it back to the
123+
* original file
124+
*/
125+
return [
126+
...messagesFromComponentSource,
127+
...messagesFromInlineTemplateHTML.map(
128+
(message: {
129+
line: string | number;
130+
column: any;
131+
endLine: string | number;
132+
endColumn: any;
133+
fix: { range: [number, number]; text: string };
134+
}) => {
135+
message.line = message.line + rangeData.lineAndCharacter.start.line;
136+
message.column = message.column;
137+
138+
message.endLine =
139+
message.endLine + rangeData.lineAndCharacter.start.line;
140+
message.endColumn = message.endColumn;
141+
142+
const startOffset = rangeData.range[0];
143+
message.fix.range = [
144+
startOffset + message.fix.range[0],
145+
startOffset + message.fix.range[1],
146+
];
147+
return message;
148+
},
149+
),
150+
];
150151
}
151152

152153
export default {
153-
'.ts': {
154-
preprocess: preprocessTSFile,
155-
postprocess: postprocessTSFile,
154+
'extract-inline-html': {
155+
preprocess: preprocessComponentFile,
156+
postprocess: postprocessComponentFile,
156157
supportsAutofix: true,
157158
},
158159
};

packages/eslint-plugin/LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 James Henry
3+
Copyright (c) 2020 James Henry
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

packages/integration-tests/fixtures/angular-cli-workspace/.eslintrc.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ module.exports = {
4545
// '@angular-eslint/directive-class-suffix': 'error'
4646

4747
// ORIGINAL tslint.json -> "directive-selector": [true, "attribute", "app", "camelCase"],
48-
"@angular-eslint/directive-selector": [
48+
'@angular-eslint/directive-selector': [
4949
'error',
50-
{ type: 'attribute', prefix: 'app', style: 'camelCase' }
50+
{ type: 'attribute', prefix: 'app', style: 'camelCase' },
5151
],
5252

5353
// ORIGINAL tslint.json -> "component-selector": [true, "element", "app", "kebab-case"],
54-
"@angular-eslint/component-selector": [
54+
'@angular-eslint/component-selector': [
5555
'error',
56-
{ type: 'element', prefix: 'app', style: 'kebab-case' }
56+
{ type: 'element', prefix: 'app', style: 'kebab-case' },
5757
],
5858

5959
// ORIGINAL tslint.json -> "import-blacklist": [true, "rxjs/Rx"],
@@ -150,7 +150,7 @@ module.exports = {
150150
'comma-dangle': 'off',
151151

152152
// ORIGINAL tslint.json -> "no-conflicting-lifecycle": true,
153-
"@angular-eslint/no-conflicting-lifecycle": 'error',
153+
'@angular-eslint/no-conflicting-lifecycle': 'error',
154154

155155
// ORIGINAL tslint.json -> "no-host-metadata-property": true,
156156
'@angular-eslint/no-host-metadata-property': 'error',
@@ -198,5 +198,15 @@ module.exports = {
198198
'@angular-eslint/template/no-negated-async': 'error',
199199
},
200200
},
201+
{
202+
files: ['*.component.ts'],
203+
parser: '@typescript-eslint/parser',
204+
parserOptions: {
205+
ecmaVersion: 2020,
206+
sourceType: 'module',
207+
},
208+
plugins: ['@angular-eslint/template'],
209+
processor: '@angular-eslint/template/extract-inline-html',
210+
},
201211
],
202212
};

packages/integration-tests/fixtures/angular-cli-workspace/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
},
2626
"devDependencies": {
2727
"@angular-devkit/build-angular": "~0.900.2",
28-
"@angular-eslint/builder": "file:angular-eslint-builder-0.0.1-alpha.18.tgz",
29-
"@angular-eslint/eslint-plugin": "file:angular-eslint-eslint-plugin-0.0.1-alpha.18.tgz",
30-
"@angular-eslint/eslint-plugin-template": "file:angular-eslint-eslint-plugin-template-0.0.1-alpha.18.tgz",
31-
"@angular-eslint/template-parser": "file:angular-eslint-template-parser-0.0.1-alpha.18.tgz",
28+
"@angular-eslint/builder": "file:angular-eslint-builder-0.0.1-alpha.20.tgz",
29+
"@angular-eslint/eslint-plugin": "file:angular-eslint-eslint-plugin-0.0.1-alpha.20.tgz",
30+
"@angular-eslint/eslint-plugin-template": "file:angular-eslint-eslint-plugin-template-0.0.1-alpha.20.tgz",
31+
"@angular-eslint/template-parser": "file:angular-eslint-template-parser-0.0.1-alpha.20.tgz",
3232
"@angular/cli": "~9.0.2",
3333
"@angular/compiler-cli": "~9.0.1",
3434
"@angular/language-service": "~9.0.1",

packages/integration-tests/fixtures/angular-cli-workspace/src/app/example.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
22

33
@Component({
4-
selector: 'app-example',
4+
selector: "app-example",
55
template: `
66
<input type="text" name="foo" ([ngModel])="foo">
77

packages/integration-tests/tests/__snapshots__/integration.ts.snap

+19-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ exports[`angular-cli-workspace it should produce the expected lint output 1`] =
44
"
55
Linting \\"angular-cli-workspace\\"...
66
7+
__ROOT__/angular-cli-workspace/src/app/app.component.html
8+
1:31 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-a-box
9+
10+
✖ 1 problem (1 error, 0 warnings)
11+
1 error and 0 warnings potentially fixable with the \`--fix\` option.
12+
13+
714
__ROOT__/angular-cli-workspace/src/app/app.component.ts
815
2:1 error 'rxjs/Rx' import is restricted from being used. Please import directly from 'rxjs' instead no-restricted-imports
916
5:1 error Unexpected property on console object was called no-restricted-syntax
@@ -19,12 +26,18 @@ __ROOT__/angular-cli-workspace/src/app/example.pipe.ts
1926
2027
2128
__ROOT__/angular-cli-workspace/src/app/example.component.ts
22-
12:3 error Use @Input rather than the \`inputs\` metadata property (https://angular.io/styleguide#style-05-12) @angular-eslint/no-inputs-metadata-property
23-
13:3 error Use @Output rather than the \`outputs\` metadata property (https://angular.io/styleguide#style-05-12) @angular-eslint/no-outputs-metadata-property
24-
14:3 error Use @Output rather than the \`host\` metadata property (https://angular.io/styleguide#style-05-12) @angular-eslint/no-host-metadata-property
25-
18:3 error In the class \\"ExampleComponent\\", the output property \\"onFoo\\" should not be prefixed with on @angular-eslint/no-output-on-prefix
26-
27-
✖ 4 problems (4 errors, 0 warnings)
29+
4:13 error Strings must use singlequote quotes
30+
12:3 error Use @Input rather than the \`inputs\` metadata property (https://angular.io/styleguide#style-05-12) @angular-eslint/no-inputs-metadata-property
31+
13:3 error Use @Output rather than the \`outputs\` metadata property (https://angular.io/styleguide#style-05-12) @angular-eslint/no-outputs-metadata-property
32+
14:3 error Use @Output rather than the \`host\` metadata property (https://angular.io/styleguide#style-05-12) @angular-eslint/no-host-metadata-property
33+
18:3 error In the class \\"ExampleComponent\\", the output property \\"onFoo\\" should not be prefixed with on @angular-eslint/no-output-on-prefix
34+
6:35 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-a-box
35+
8:15 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-a-box
36+
8:29 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-a-box
37+
9:48 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-a-box
38+
39+
✖ 9 problems (9 errors, 0 warnings)
40+
5 errors and 0 warnings potentially fixable with the \`--fix\` option.
2841
2942
3043
__ROOT__/angular-cli-workspace/src/app/app.component.spec.ts

packages/template-parser/LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 James Henry
3+
Copyright (c) 2020 James Henry
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

0 commit comments

Comments
 (0)