@discord/eslint-plugin-discord-intl 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +3 -1
- package/lib/is-typescript.js +11 -0
- package/lib/traverse.js +108 -32
- package/package.json +4 -2
- package/rules/no-opaque-messages-objects.js +48 -0
- package/rules/no-opaque-messages-objects.test.js +92 -0
- package/rules/{trimmable-whitespace.js → trimmed-whitespace.js} +0 -3
- package/rules/{trimmable-whitespace.test.js → trimmed-whitespace.test.js} +9 -9
package/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
rules: {
|
|
3
|
-
'trimmed-whitespace': require('./rules/
|
|
3
|
+
'trimmed-whitespace': require('./rules/trimmed-whitespace'),
|
|
4
4
|
'use-static-access': require('./rules/use-static-access'),
|
|
5
|
+
'no-opaque-messages-objects': require('./rules/no-opaque-messages-objects'),
|
|
5
6
|
},
|
|
6
7
|
configs: {
|
|
7
8
|
recommended: {
|
|
@@ -9,6 +10,7 @@ module.exports = {
|
|
|
9
10
|
rules: {
|
|
10
11
|
'@discord/discord-intl/trimmed-whitespace': 'error',
|
|
11
12
|
'@discord/discord-intl/use-static-access': 'error',
|
|
13
|
+
'@discord/discord-intl/no-opaque-messages-objects': 'error',
|
|
12
14
|
},
|
|
13
15
|
},
|
|
14
16
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the current execution is using a TypeScript parser, using a best-effort guess
|
|
3
|
+
* based on the given context.
|
|
4
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
5
|
+
* @returns {boolean}
|
|
6
|
+
*/
|
|
7
|
+
function isTypeScript(context) {
|
|
8
|
+
return context.parserPath?.includes('@typescript-eslint') ?? false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { isTypeScript };
|
package/lib/traverse.js
CHANGED
|
@@ -1,18 +1,80 @@
|
|
|
1
1
|
const { isMessageDefinitionsFile } = require('@discord/intl-loader-core');
|
|
2
2
|
|
|
3
|
+
/** @typedef {import('eslint').Rule.NodeParentExtension} NodeParentExtension */
|
|
4
|
+
/** @typedef {import('eslint').SourceCode} SourceCode */
|
|
5
|
+
/** @typedef {import('eslint').Scope.ScopeManager} ScopeManager */
|
|
6
|
+
/** @typedef {import('eslint').Scope.Reference} Reference */
|
|
3
7
|
/** @typedef {import('eslint').Rule.RuleListener} RuleListener */
|
|
4
8
|
/** @typedef {import('eslint').Rule.RuleContext} RuleContext */
|
|
9
|
+
/** @typedef {import('estree').Node} Node */
|
|
5
10
|
/** @typedef {import('estree').MemberExpression} MemberExpression */
|
|
11
|
+
/** @typedef {import('estree').ImportDeclaration} ImportDeclaration */
|
|
12
|
+
/** @typedef {import('estree').ImportSpecifier} ImportSpecifier */
|
|
13
|
+
/** @typedef {import('estree').ImportDefaultSpecifier} ImportDefaultSpecifier */
|
|
14
|
+
/** @typedef {import('estree').ImportNamespaceSpecifier} ImportNamespaceSpecifier */
|
|
6
15
|
/** @typedef {import('estree').BaseModuleSpecifier} BaseModuleSpecifier */
|
|
7
16
|
/** @typedef {import('estree').ObjectExpression} ObjectExpression */
|
|
8
17
|
/** @typedef {import('estree').SimpleLiteral} SimpleLiteral */
|
|
9
18
|
/** @typedef {import('estree').TemplateLiteral} TemplateLiteral */
|
|
10
19
|
/** @typedef {import('estree').Property} Property */
|
|
20
|
+
/** @typedef {import('estree').Identifier} Identifier */
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
|
-
* @typedef
|
|
23
|
+
* @typedef DiscordIntlPluginConfig
|
|
24
|
+
* @property {Record<string, string[]>} [extraImports]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {DiscordIntlPluginConfig | undefined} config
|
|
29
|
+
* @param {ImportDeclaration} path
|
|
30
|
+
* @returns {Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>}
|
|
31
|
+
*/
|
|
32
|
+
function getImportedMessagesObjectSpecifiers(config, path) {
|
|
33
|
+
const importSource = /** @type {string} */ (path.source.value);
|
|
34
|
+
// TODO: Make `isMessageDefinitionsFile` understand this properly.
|
|
35
|
+
const isDefinition = isMessageDefinitionsFile(importSource) || importSource.endsWith('.messages');
|
|
36
|
+
const extraImportSpecifiers = config?.extraImports?.[importSource] ?? [];
|
|
37
|
+
// This transformer only handles usages of intl messages, so only
|
|
38
|
+
// imports of definitions files and configured extra specifiers need to
|
|
39
|
+
// be handled.
|
|
40
|
+
if (!isDefinition && extraImportSpecifiers.length === 0) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return path.specifiers.filter(
|
|
45
|
+
(specifier) =>
|
|
46
|
+
(isDefinition && specifier.type === 'ImportDefaultSpecifier') ||
|
|
47
|
+
extraImportSpecifiers.includes(specifier.local.name),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
*
|
|
53
|
+
* @param {RuleContext} context
|
|
54
|
+
* @param {Node} node
|
|
55
|
+
* @returns {Reference[]}
|
|
56
|
+
*/
|
|
57
|
+
function getBindingReferences(context, node) {
|
|
58
|
+
return (
|
|
59
|
+
context.sourceCode
|
|
60
|
+
.getDeclaredVariables(node)
|
|
61
|
+
// This should only ever yield 1 variable, so flatMap is unnecessary, but it is safer to
|
|
62
|
+
// use in case this changes in the future.
|
|
63
|
+
.flatMap((variable) => /** @type {Reference[]} */ (variable.references))
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @typedef {(
|
|
69
|
+
* node: MemberExpression & NodeParentExtension,
|
|
70
|
+
* importer: BaseModuleSpecifier & NodeParentExtension
|
|
71
|
+
* ) => void} MessageAccessCallback
|
|
72
|
+
* @typedef {(
|
|
73
|
+
* reference: Identifier & NodeParentExtension,
|
|
74
|
+
* importer: BaseModuleSpecifier & NodeParentExtension
|
|
75
|
+
* ) => void} MessagesReferenceCallback
|
|
14
76
|
* @typedef {(
|
|
15
|
-
* property: Property,
|
|
77
|
+
* property: Property & NodeParentExtension,
|
|
16
78
|
* value: SimpleLiteral | TemplateLiteral,
|
|
17
79
|
* definition: ObjectExpression | undefined,
|
|
18
80
|
* ) => void} MessageDefinitionCallback
|
|
@@ -30,44 +92,26 @@ const { isMessageDefinitionsFile } = require('@discord/intl-loader-core');
|
|
|
30
92
|
*/
|
|
31
93
|
function traverseMessageAccesses(context, callback) {
|
|
32
94
|
const config = context.settings['@discord/discord-intl'];
|
|
33
|
-
const source = context.sourceCode;
|
|
34
95
|
|
|
35
96
|
return /** @type {RuleListener} */ ({
|
|
36
97
|
ImportDeclaration(path) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const isDefinition =
|
|
40
|
-
isMessageDefinitionsFile(importSource) || importSource.endsWith('.messages');
|
|
41
|
-
const extraImportSpecifiers = config.extraImports?.[importSource] ?? [];
|
|
42
|
-
// This transformer only handles usages of intl messages, so only
|
|
43
|
-
// imports of definitions files and configured extra specifiers need to
|
|
44
|
-
// be handled.
|
|
45
|
-
if (!isDefinition && extraImportSpecifiers.length === 0) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const specifiers = path.specifiers.filter(
|
|
50
|
-
(specifier) =>
|
|
51
|
-
(isDefinition && specifier.type === 'ImportDefaultSpecifier') ||
|
|
52
|
-
extraImportSpecifiers.includes(specifier.local.name),
|
|
53
|
-
);
|
|
98
|
+
const specifiers = getImportedMessagesObjectSpecifiers(config, path);
|
|
99
|
+
if (specifiers.length === 0) return;
|
|
54
100
|
|
|
55
101
|
for (const specifier of specifiers) {
|
|
56
|
-
const bindingReferences =
|
|
57
|
-
.getDeclaredVariables(specifier)
|
|
58
|
-
// This should only ever yield 1 variable, so flatMap is unnecessary, but it is safer to
|
|
59
|
-
// use in case this changes in the future.
|
|
60
|
-
.flatMap((variable) => variable.references);
|
|
102
|
+
const bindingReferences = getBindingReferences(context, specifier);
|
|
61
103
|
|
|
62
104
|
for (const reference of bindingReferences) {
|
|
63
|
-
|
|
64
|
-
|
|
105
|
+
const parent = /** @type {MemberExpression & NodeParentExtension} */ (
|
|
106
|
+
// @ts-expect-error `identifier` is actually `Identifier & NodeParentExtension`.
|
|
107
|
+
reference.identifier.parent
|
|
108
|
+
);
|
|
65
109
|
|
|
66
110
|
// We only care about member expressions, since a direct reference to the
|
|
67
111
|
// message source doesn't necessarily make it a message access.
|
|
68
112
|
if (parent.type !== 'MemberExpression') continue;
|
|
69
113
|
|
|
70
|
-
callback(/** @type {
|
|
114
|
+
callback(parent, /** @type {BaseModuleSpecifier & NodeParentExtension} */ (specifier));
|
|
71
115
|
}
|
|
72
116
|
}
|
|
73
117
|
},
|
|
@@ -113,13 +157,12 @@ function traverseMessageDefinitions(context, callback) {
|
|
|
113
157
|
case 'Literal':
|
|
114
158
|
case 'TemplateLiteral':
|
|
115
159
|
callback(
|
|
116
|
-
property,
|
|
160
|
+
/** @type {Property & NodeParentExtension} */ (property),
|
|
117
161
|
/** @type {SimpleLiteral | TemplateLiteral} */ (property.value),
|
|
118
162
|
undefined,
|
|
119
163
|
);
|
|
120
164
|
break;
|
|
121
165
|
case 'ObjectExpression': {
|
|
122
|
-
console.log('got expression');
|
|
123
166
|
const messageProperty = property.value.properties.find((prop) => {
|
|
124
167
|
return prop.type === 'Property' && 'name' in prop.key && prop.key.name === 'message';
|
|
125
168
|
});
|
|
@@ -132,7 +175,7 @@ function traverseMessageDefinitions(context, callback) {
|
|
|
132
175
|
}
|
|
133
176
|
|
|
134
177
|
callback(
|
|
135
|
-
property,
|
|
178
|
+
/** @type {Property & NodeParentExtension} */ (property),
|
|
136
179
|
/** @type {SimpleLiteral} */ (messageProperty.value),
|
|
137
180
|
property.value,
|
|
138
181
|
);
|
|
@@ -143,4 +186,37 @@ function traverseMessageDefinitions(context, callback) {
|
|
|
143
186
|
});
|
|
144
187
|
}
|
|
145
188
|
|
|
146
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Visit all `Identifier`s that act as accesses to a complete intl messages object. This includes
|
|
191
|
+
* all references, including message accesses, object passing, and includes the original import
|
|
192
|
+
* for completeness.
|
|
193
|
+
*
|
|
194
|
+
* @param {RuleContext} context The rule context from ESLint
|
|
195
|
+
* @param {MessagesReferenceCallback} callback Function to call for each instance of a message access
|
|
196
|
+
* @returns {RuleListener} A visitor object for the Babel transform.
|
|
197
|
+
*/
|
|
198
|
+
function traverseMessageObjectReferences(context, callback) {
|
|
199
|
+
const config = context.settings['@discord/discord-intl'];
|
|
200
|
+
|
|
201
|
+
return /** @type {RuleListener} */ ({
|
|
202
|
+
ImportDeclaration(path) {
|
|
203
|
+
const specifiers = getImportedMessagesObjectSpecifiers(config, path);
|
|
204
|
+
for (const specifier of specifiers) {
|
|
205
|
+
const bindingReferences = getBindingReferences(context, specifier);
|
|
206
|
+
|
|
207
|
+
for (const reference of bindingReferences) {
|
|
208
|
+
callback(
|
|
209
|
+
/** @type {Identifier & NodeParentExtension} */ (reference.identifier),
|
|
210
|
+
/** @type {BaseModuleSpecifier & NodeParentExtension} */ (specifier),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
traverseMessageAccesses,
|
|
220
|
+
traverseMessageDefinitions,
|
|
221
|
+
traverseMessageObjectReferences,
|
|
222
|
+
};
|
package/package.json
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discord/eslint-plugin-discord-intl",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"description": "ESLint plugin for validating and linting messages and translations using @discord/intl",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@
|
|
9
|
+
"@typescript-eslint/scope-manager": "^8.8.1",
|
|
10
|
+
"@discord/intl-loader-core": "0.6.2"
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
12
13
|
"@types/eslint": "^8.56.12",
|
|
13
14
|
"@types/estree": "^1.0.6",
|
|
15
|
+
"@typescript-eslint/parser": "^8.8.1",
|
|
14
16
|
"eslint": "8.57.1"
|
|
15
17
|
}
|
|
16
18
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { traverseMessageObjectReferences } = require('../lib/traverse');
|
|
2
|
+
const { isTypeScript } = require('../lib/is-typescript');
|
|
3
|
+
|
|
4
|
+
module.exports = /** @type {import('eslint').Rule.RuleModule} */ ({
|
|
5
|
+
meta: {
|
|
6
|
+
docs: {
|
|
7
|
+
description:
|
|
8
|
+
'Disallow using whole messages objects as singular values, through passing as arguments to functions, taking the type of the object, and more.',
|
|
9
|
+
category: 'Best Practices',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noObjectArgument:
|
|
13
|
+
'Avoid passing message objects around as parameters. Use messages individually',
|
|
14
|
+
noTypeof:
|
|
15
|
+
'Avoid requesting the type of an entire messages object. Use messages individually.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
return traverseMessageObjectReferences(context, (reference) => {
|
|
20
|
+
const parent = reference.parent;
|
|
21
|
+
if (parent.type === 'CallExpression') {
|
|
22
|
+
context.report({
|
|
23
|
+
node: reference,
|
|
24
|
+
messageId: 'noObjectArgument',
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (parent.type === 'UnaryExpression' && parent.operator === 'typeof') {
|
|
30
|
+
context.report({
|
|
31
|
+
node: parent,
|
|
32
|
+
messageId: 'noTypeof',
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isTypeScript(context)) {
|
|
38
|
+
// @ts-expect-error TSNodes
|
|
39
|
+
if (parent.type === 'TSTypeQuery') {
|
|
40
|
+
context.report({
|
|
41
|
+
node: parent,
|
|
42
|
+
messageId: 'noTypeof',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// enforce-foo-bar.test.js
|
|
2
|
+
const { RuleTester } = require('eslint');
|
|
3
|
+
const noOpaqueMessagesObjects = require('./no-opaque-messages-objects');
|
|
4
|
+
|
|
5
|
+
const typescriptParser = require.resolve('@typescript-eslint/parser');
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
// Must use at least ecmaVersion 2015 because
|
|
8
|
+
// that's when `const` variables were introduced.
|
|
9
|
+
parserOptions: { ecmaVersion: 2015, sourceType: 'module' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
ruleTester.run('no-opaque-messages-objects', noOpaqueMessagesObjects, {
|
|
13
|
+
valid: [
|
|
14
|
+
{
|
|
15
|
+
name: 'normal message formatting',
|
|
16
|
+
code: `
|
|
17
|
+
import messages from 'Feature.messages';
|
|
18
|
+
intl.format(messages.SOME_MESSAGE);
|
|
19
|
+
otherFunction(messages.OTHER_MESSAGE);
|
|
20
|
+
`,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'intermediate object passing',
|
|
24
|
+
code: `
|
|
25
|
+
import messages from 'Feature.messages';
|
|
26
|
+
otherFunction({
|
|
27
|
+
FIRST: messages.FIRST,
|
|
28
|
+
OTHER_MESSAGE: messages.OTHER_MESSAGE,
|
|
29
|
+
});
|
|
30
|
+
`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'typeof individual message',
|
|
34
|
+
parser: typescriptParser,
|
|
35
|
+
code: `
|
|
36
|
+
import messages from 'Feature.messages';
|
|
37
|
+
typeof messages.FOO;
|
|
38
|
+
function foo(message: typeof messages.FOO) {}
|
|
39
|
+
`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
invalid: [
|
|
43
|
+
{
|
|
44
|
+
name: 'passing whole object',
|
|
45
|
+
code: `
|
|
46
|
+
import messages from 'Feature.messages';
|
|
47
|
+
otherFunction(messages);
|
|
48
|
+
`,
|
|
49
|
+
errors: 1,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'passing whole object',
|
|
53
|
+
settings: {
|
|
54
|
+
'@discord/discord-intl': {
|
|
55
|
+
extraImports: { '@app/intl': ['t'] },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
code: `
|
|
59
|
+
import {t} from '@app/intl';
|
|
60
|
+
otherFunction(t);
|
|
61
|
+
`,
|
|
62
|
+
errors: 1,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'passing whole object',
|
|
66
|
+
code: `
|
|
67
|
+
import messages from 'Feature.messages';
|
|
68
|
+
otherFunction(messages);
|
|
69
|
+
`,
|
|
70
|
+
errors: 1,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'typeof messages value',
|
|
74
|
+
code: `
|
|
75
|
+
import messages from 'Feature.messages';
|
|
76
|
+
typeof messages;
|
|
77
|
+
`,
|
|
78
|
+
errors: 1,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'typeof messages as parameter',
|
|
82
|
+
parser: typescriptParser,
|
|
83
|
+
code: `
|
|
84
|
+
import messages from 'Feature.messages';
|
|
85
|
+
function foo(strings: typeof messages) {}
|
|
86
|
+
`,
|
|
87
|
+
errors: 1,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log('All tests passed!');
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const util = require('node:util');
|
|
2
1
|
const { traverseMessageDefinitions } = require('../lib/traverse');
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -35,8 +34,6 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ({
|
|
|
35
34
|
const rawNoQuotes = sourceText.slice(1, -1);
|
|
36
35
|
|
|
37
36
|
const trimmed = trimLeadingWhitespace(trimTrailingWhitespace(rawNoQuotes));
|
|
38
|
-
util.inspect(rawNoQuotes);
|
|
39
|
-
util.inspect(trimmed);
|
|
40
37
|
if (trimmed === rawNoQuotes) return;
|
|
41
38
|
|
|
42
39
|
context.report({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// enforce-foo-bar.test.js
|
|
2
2
|
const { RuleTester } = require('eslint');
|
|
3
|
-
const
|
|
3
|
+
const trimmedWhitespace = require('./trimmed-whitespace');
|
|
4
4
|
|
|
5
5
|
const ruleTester = new RuleTester({
|
|
6
6
|
// Must use at least ecmaVersion 2015 because
|
|
@@ -19,22 +19,22 @@ function defineMessages(messages) {
|
|
|
19
19
|
`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
ruleTester.run('
|
|
22
|
+
ruleTester.run('trimmed-whitespace', trimmedWhitespace, {
|
|
23
23
|
valid: [
|
|
24
24
|
{
|
|
25
25
|
name: 'normal strings',
|
|
26
|
-
code: defineMessages("{ A: 'no
|
|
26
|
+
code: defineMessages("{ A: 'no trimmed whitespace' }"),
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
name: 'templates',
|
|
30
30
|
code: defineMessages(
|
|
31
|
-
'{ A: `no
|
|
31
|
+
'{ A: `no trimmed whitespace`, QUASI: `${ space }`, MULTILINE: `hi\n yes` }',
|
|
32
32
|
),
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
name: 'multi-line',
|
|
36
36
|
code: defineMessages(
|
|
37
|
-
`{ A: \`no
|
|
37
|
+
`{ A: \`no trimmed
|
|
38
38
|
whitespace\`}`,
|
|
39
39
|
),
|
|
40
40
|
},
|
|
@@ -107,14 +107,14 @@ ruleTester.run('trimmable-whitespace', trimmableWhitespace, {
|
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
name: 'template quasis',
|
|
110
|
-
code: defineMessages('{ A: `no
|
|
111
|
-
output: defineMessages('{ A: `no
|
|
110
|
+
code: defineMessages('{ A: `no trimmed whitespace`, QUASI: ` ${ space } ` }'),
|
|
111
|
+
output: defineMessages('{ A: `no trimmed whitespace`, QUASI: `${ space }` }'),
|
|
112
112
|
errors: 1,
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
name: 'multiline templates',
|
|
116
|
-
code: defineMessages('{ A: `no
|
|
117
|
-
output: defineMessages('{ A: `no
|
|
116
|
+
code: defineMessages('{ A: `no trimmed whitespace`, QUASI: `\n\t${ space }\n ` }'),
|
|
117
|
+
output: defineMessages('{ A: `no trimmed whitespace`, QUASI: `${ space }` }'),
|
|
118
118
|
errors: 1,
|
|
119
119
|
},
|
|
120
120
|
{
|