Skip to content

Commit

Permalink
Support First Class Component Templates -- <template>
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli committed Jan 30, 2022
1 parent 139dcc8 commit 84f716e
Show file tree
Hide file tree
Showing 7 changed files with 831 additions and 17 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
| [no-ember-super-in-es-classes](./docs/rules/no-ember-super-in-es-classes.md) | disallow use of `this._super` in ES class methods || 🔧 | |
| [no-empty-glimmer-component-classes](./docs/rules/no-empty-glimmer-component-classes.md) | disallow empty backing classes for Glimmer components || | |

### Ember Polaris

| Name | Description || 🔧 | 💡 |
|:--------|:------------|:---------------|:-----------|:---------------|
| [template-vars](./docs/rules/template-vars.md) | require that variables used in <template> tags do not trigger no-unused-vars failures, and should trigger no-undef if they are not defined properly | | | |

### jQuery

| Name | Description || 🔧 | 💡 |
Expand Down
12 changes: 12 additions & 0 deletions lib/config/base.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const util = require('ember-template-imports/src/util');

module.exports = {
root: true,

Expand All @@ -12,4 +14,14 @@ module.exports = {
},

plugins: ['ember'],

overrides: [
{
files: ['**/*.gjs', '**/*.gts'],
processor: 'ember/<template>',
globals: {
[util.TEMPLATE_TAG_PLACEHOLDER]: 'readonly',
},
},
],
};
8 changes: 8 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

const requireIndex = require('requireindex');

const gjs = require('./preprocessors/glimmer');

module.exports = {
rules: requireIndex(`${__dirname}/rules`),
configs: requireIndex(`${__dirname}/config`),
utils: {
ember: require('./utils/ember'),
utils: require('./utils/utils'),
},
processors: {
// https://eslint.org/docs/developer-guide/working-with-plugins#file-extension-named-processor
'.gjs': gjs,
'.gts': gjs,
'<template>': gjs,
},
};
42 changes: 42 additions & 0 deletions lib/preprocessors/glimmer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const { getTemplateLocals } = require('@glimmer/syntax');
const {
preprocessEmbeddedTemplates,
} = require('ember-template-imports/lib/preprocess-embedded-templates');
const util = require('ember-template-imports/src/util');

function gjs(text, filename) {
const isGlimmerFile = filename.endsWith('.gjs') || filename.endsWith('.gts');

if (!isGlimmerFile) {
return [
{
filename,
text,
},
];
}

const preprocessed = preprocessEmbeddedTemplates(text, {
getTemplateLocals,
relativePath: filename,

templateTag: util.TEMPLATE_TAG_NAME,
templateTagReplacement: util.TEMPLATE_TAG_PLACEHOLDER,

includeSourceMaps: false,
includeTemplateTokens: true,
});

return [
{
filename,
text: preprocessed.output,
},
];
}

module.exports = {
preprocess: gjs,
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,14 @@
},
"dependencies": {
"@ember-data/rfc395-data": "^0.0.4",
"@glimmer/syntax": "^0.83.1",
"css-tree": "^2.0.4",
"ember-rfc176-data": "^0.3.15",
"ember-template-imports": "^2.0.0",
"eslint-utils": "^3.0.0",
"estraverse": "^5.2.0",
"lodash.kebabcase": "^4.1.1",
"magic-string": "^0.25.7",
"requireindex": "^1.2.0",
"snake-case": "^3.0.3"
},
Expand Down
176 changes: 176 additions & 0 deletions tests/lib/rules-preprocessor/gjs-gts-processor-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
'use strict';

/**
* Because this test needs the preprocessor, we can't use the normal
* RuleTester api doesn't support preprocessors.
*
* @typedef {import('eslint/lib/cli-engine/cli-engine').CLIEngineOptions} CLIEngineOptions
*/

const { ESLint } = require('eslint');
const plugin = require('../../../lib');

/**
* Helper function which creates ESLint instance with enabled/disabled autofix feature.
*
* @param {CLIEngineOptions} [options={}] Whether to enable autofix feature.
* @returns {ESLint} ESLint instance to execute in tests.
*/
function initESLint(options) {
// tests must be run with ESLint 7+
return new ESLint({
ignore: false,
useEslintrc: false,
plugins: { ember: plugin },
overrideConfig: {
root: true,
env: {
browser: true,
},
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
plugins: ['ember'],
extends: ['plugin:ember/base'],
rules: {
'no-undef': 'error',
},
},
...options,
});
}

const valid = [
{
filename: 'my-component.js',
code: `
import Component from '@glimmer/component';
export default class MyComponent extends Component {
}
`,
},
{
filename: 'my-component.gjs',
code: `
import { on } from '@ember/modifier';
const noop = () => {};
<template>
<div {{on 'click' noop}} />
</template>
`,
},
{
filename: 'my-component.gjs',
code: `
import { on } from '@ember/modifier';
const noop = () => {};
export default <template>
<div {{on 'click' noop}} />
</template>
`,
},
{
filename: 'my-component.gjs',
code: `
const Foo = <template>hi</template>
<template>
<Foo />
</template>
`,
},
];

const invalid = [
{
filename: 'my-component.gjs',
code: `
const noop = () => {};
<template>
{{on 'click' noop}}
</template>
`,
errors: [
{
message: "'on' is not defined.",
},
],
},
{
filename: 'my-component.gjs',
code: `
<template>
{{noop}}
</template>
`,
errors: [
{
message: "'noop' is not defined.",
},
],
},
{
filename: 'my-component.gjs',
code: `
<template>
<Foo />
</template>
`,
errors: [
{
message: "'Foo' is not defined.",
},
],
},
];

describe('template-vars', () => {
describe('valid', () => {
for (const scenario of valid) {
const { code, filename } = scenario;

// eslint-disable-next-line jest/valid-title
it(code, async () => {
const eslint = initESLint();
const results = await eslint.lintText(code, { filePath: filename });
const resultErrors = results.map((result) => result.messages).flat();

// This gives more meaningful information than
// checking if results is empty / length === 0
expect(results?.[0]?.messages?.[0]?.message || '').toStrictEqual('');
expect(resultErrors).toHaveLength(0);
});
}
});

describe('invalid', () => {
for (const scenario of invalid) {
const { code, filename, errors } = scenario;

// eslint-disable-next-line jest/valid-title
it(code, async () => {
const eslint = initESLint();
const results = await eslint.lintText(code, { filePath: filename });

const resultErrors = results.map((result) => result.messages).flat();
expect(resultErrors).toHaveLength(errors.length);

for (const [index, error] of resultErrors.entries()) {
const expected = errors[index];

for (const key of Object.keys(expected)) {
expect(error[key]).toStrictEqual(expected[key]);
}
}
});
}
});
});
Loading

0 comments on commit 84f716e

Please sign in to comment.