mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
fix: insert new line after inserting title, abstract or keywords (#29882)
GitOrigin-RevId: d8d79e95d9eb544adaff8850630df996461bacb9
This commit is contained in:
11
libraries/eslint-plugin/index.js
Normal file
11
libraries/eslint-plugin/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
rules: {
|
||||||
|
'no-unnecessary-trans': require('./no-unnecessary-trans'),
|
||||||
|
'prefer-kebab-url': require('./prefer-kebab-url'),
|
||||||
|
'should-unescape-trans': require('./should-unescape-trans'),
|
||||||
|
'no-generated-editor-themes': require('./no-generated-editor-themes'),
|
||||||
|
'require-script-runner': require('./require-script-runner'),
|
||||||
|
'require-vi-doMock-valid-path': require('./require-vi-doMock-valid-path'),
|
||||||
|
'require-loading-label': require('./require-loading-label'),
|
||||||
|
},
|
||||||
|
}
|
||||||
21
libraries/eslint-plugin/no-generated-editor-themes.js
Normal file
21
libraries/eslint-plugin/no-generated-editor-themes.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'error',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Prohibit CodeMirror themes that are generated in a function',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
':matches(ArrowFunctionExpression, FunctionDeclaration, FunctionExpression) CallExpression > MemberExpression[object.name="EditorView"]:matches([property.name="theme"],[property.name="baseTheme"])'(
|
||||||
|
node
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: `EditorView.theme and EditorView.baseTheme each add CSS to the page for every instance of the theme. Store the theme in a variable and reuse it instead.`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
43
libraries/eslint-plugin/no-unnecessary-trans.js
Normal file
43
libraries/eslint-plugin/no-unnecessary-trans.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
fixable: 'code',
|
||||||
|
docs: {
|
||||||
|
description: 'Prohibit Trans with no components or values',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
'JSXOpeningElement[name.name="Trans"]'(node) {
|
||||||
|
const attributes = new Map(
|
||||||
|
node.attributes.map(attr => [attr.name.name, attr])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!attributes.has('components')) {
|
||||||
|
if (node.parent.children.length > 0) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: `Trans components must not have child elements`,
|
||||||
|
})
|
||||||
|
} else if (attributes.has('values')) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: `Use t('…') when there are no components`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: `Use t('…') when there are no components`,
|
||||||
|
fix(fixer) {
|
||||||
|
const i18nKey = attributes.get('i18nKey').value.value
|
||||||
|
|
||||||
|
// Note: Prettier can fix indentation
|
||||||
|
return fixer.replaceText(node.parent, `{t('${i18nKey}')}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
17
libraries/eslint-plugin/package.json
Normal file
17
libraries/eslint-plugin/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@overleaf/eslint-plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"author": "Overleaf (https://www.overleaf.com)",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"eslint": "^8.51.0",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/parser": "^8.30.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "node rules.test.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
83
libraries/eslint-plugin/prefer-kebab-url-ignore.js
Normal file
83
libraries/eslint-plugin/prefer-kebab-url-ignore.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// URL parts should be kebab-case, but we didn't have this rule in the past.
|
||||||
|
// The ESLint rule `prefer-kebab-url` will ignore these "legacy" URL parts.
|
||||||
|
|
||||||
|
const ignoreWords = {
|
||||||
|
snake: new Set([
|
||||||
|
'clear_saml_data',
|
||||||
|
'confirm_link',
|
||||||
|
'confirm_university_domain',
|
||||||
|
'create_recurly_account',
|
||||||
|
'current_history_content',
|
||||||
|
'current_user',
|
||||||
|
'default_email',
|
||||||
|
'disable_managed_users',
|
||||||
|
'doc_snapshot',
|
||||||
|
'enable_history_ranges_support',
|
||||||
|
'features_override',
|
||||||
|
'generate_password_reset_url',
|
||||||
|
'get_assignment',
|
||||||
|
'get_clone',
|
||||||
|
'health_check',
|
||||||
|
'institutional_emails',
|
||||||
|
'latest_template',
|
||||||
|
'link_after_saml_response',
|
||||||
|
'linked_file',
|
||||||
|
'metrics_segmentation',
|
||||||
|
'new_users',
|
||||||
|
'no_autostart_post_gateway',
|
||||||
|
'personal_info',
|
||||||
|
'planned_maintenance',
|
||||||
|
'refresh_features',
|
||||||
|
'register_admin',
|
||||||
|
'register_ldap_admin',
|
||||||
|
'register_saml_admin',
|
||||||
|
'restore_file',
|
||||||
|
'revert_file',
|
||||||
|
'saved_vers',
|
||||||
|
'send_test_email',
|
||||||
|
'session_maintenance',
|
||||||
|
'set_in_session',
|
||||||
|
'sign_in_to_link',
|
||||||
|
'split_test',
|
||||||
|
'sso_configuration_test',
|
||||||
|
'sso_email',
|
||||||
|
'sso_enrollment',
|
||||||
|
'track_changes',
|
||||||
|
'update_admin',
|
||||||
|
'user_details',
|
||||||
|
]),
|
||||||
|
camel: new Set([
|
||||||
|
'addWorkflowScope',
|
||||||
|
'aiErrorAssistant',
|
||||||
|
'beginAuth',
|
||||||
|
'brandVariationId',
|
||||||
|
'closeEditor',
|
||||||
|
'completeRegistration',
|
||||||
|
'deactivateOldProjects',
|
||||||
|
'deletedSubscription',
|
||||||
|
'disconnectAllUsers',
|
||||||
|
'editingSession',
|
||||||
|
'emailSubscription',
|
||||||
|
'enableManagedUsers',
|
||||||
|
'externalCollaboration',
|
||||||
|
'flushProjectToTpds',
|
||||||
|
'indexAll',
|
||||||
|
'offboardManagedUser',
|
||||||
|
'openEditor',
|
||||||
|
'perfTest',
|
||||||
|
'pollDropboxForUser',
|
||||||
|
'resendInvite',
|
||||||
|
'resendManagedUserInvite',
|
||||||
|
'salesContactForm',
|
||||||
|
'showSupport',
|
||||||
|
]),
|
||||||
|
other: new Set([
|
||||||
|
'Project',
|
||||||
|
'disableSSO',
|
||||||
|
'enableSSO',
|
||||||
|
'resendSSOLinkInvite',
|
||||||
|
'usersCSV',
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { ignoreWords }
|
||||||
91
libraries/eslint-plugin/prefer-kebab-url.js
Normal file
91
libraries/eslint-plugin/prefer-kebab-url.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const _ = require('lodash')
|
||||||
|
const { ignoreWords } = require('./prefer-kebab-url-ignore')
|
||||||
|
|
||||||
|
const removeTextBetweenBrackets = text => {
|
||||||
|
while (text.includes('[') || text.includes('(')) {
|
||||||
|
text = text.replaceAll(/\[[^[\]]*]/g, '')
|
||||||
|
text = text.replaceAll(/\([^()]*\)/g, '')
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldIgnoreWord = str =>
|
||||||
|
str.includes(':') ||
|
||||||
|
str.includes('(') ||
|
||||||
|
str === '*' ||
|
||||||
|
str.match(/^[a-z0-9.]+$/) ||
|
||||||
|
ignoreWords.snake.has(str) ||
|
||||||
|
ignoreWords.camel.has(str) ||
|
||||||
|
ignoreWords.other.has(str)
|
||||||
|
|
||||||
|
const getSuggestion = routePath => {
|
||||||
|
if (typeof routePath === 'string') {
|
||||||
|
const kebabed = routePath
|
||||||
|
.split('/')
|
||||||
|
.map(word => (shouldIgnoreWord(word) ? word : _.kebabCase(word)))
|
||||||
|
.join('/')
|
||||||
|
return kebabed === routePath ? null : `'${kebabed}'`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (routePath instanceof RegExp) {
|
||||||
|
const words = removeTextBetweenBrackets(routePath.source).match(/[\w-]+/g)
|
||||||
|
if (!words) return routePath
|
||||||
|
|
||||||
|
let newSource = routePath.source
|
||||||
|
for (const word of words) {
|
||||||
|
if (!shouldIgnoreWord(word)) {
|
||||||
|
newSource = newSource.replaceAll(
|
||||||
|
new RegExp(`\\b${word}\\b`, 'g'),
|
||||||
|
_.kebabCase(word)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kebabed = new RegExp(newSource, routePath.flags)
|
||||||
|
return kebabed.source.toString() === routePath.source.toString()
|
||||||
|
? null
|
||||||
|
: kebabed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
fixable: 'code',
|
||||||
|
hasSuggestions: true,
|
||||||
|
docs: {
|
||||||
|
description: 'Enforce using kebab-case for URL paths',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: context => ({
|
||||||
|
CallExpression(node) {
|
||||||
|
if (
|
||||||
|
node.callee.type === 'MemberExpression' &&
|
||||||
|
node.arguments[0]?.type === 'Literal' &&
|
||||||
|
[/app/i, /router/i].some(callee =>
|
||||||
|
typeof callee === 'string'
|
||||||
|
? node.callee.object.name === callee
|
||||||
|
: callee.test(node.callee.object.name)
|
||||||
|
) &&
|
||||||
|
['get', 'post', 'put', 'delete'].includes(node.callee.property.name)
|
||||||
|
) {
|
||||||
|
const routePath = node.arguments[0].value
|
||||||
|
|
||||||
|
const suggestion = getSuggestion(routePath)
|
||||||
|
|
||||||
|
if (suggestion) {
|
||||||
|
context.report({
|
||||||
|
node: node.arguments[0],
|
||||||
|
message: 'Route path should be in kebab-case.',
|
||||||
|
suggest: [
|
||||||
|
{
|
||||||
|
desc: `Change to kebab-case: ${suggestion}`,
|
||||||
|
fix: fixer => fixer.replaceText(node.arguments[0], suggestion),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
49
libraries/eslint-plugin/require-loading-label.js
Normal file
49
libraries/eslint-plugin/require-loading-label.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
fixable: null,
|
||||||
|
docs: {
|
||||||
|
description: 'Require loadingLabel prop when isLoading is specified on OLButton',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
'JSXOpeningElement[name.name="OLButton"]'(node) {
|
||||||
|
const attributes = new Map(
|
||||||
|
node.attributes.map(attr => [
|
||||||
|
attr.name?.name,
|
||||||
|
attr
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const isLoadingAttr = attributes.get('isLoading')
|
||||||
|
const loadingLabelAttr = attributes.get('loadingLabel')
|
||||||
|
|
||||||
|
if (isLoadingAttr && !loadingLabelAttr) {
|
||||||
|
const isLoadingValue = isLoadingAttr.value
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isLoadingValue ||
|
||||||
|
(isLoadingValue.type === 'JSXExpressionContainer' &&
|
||||||
|
isLoadingValue.expression.type === 'Literal' &&
|
||||||
|
isLoadingValue.expression.value === true)
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node: isLoadingAttr,
|
||||||
|
message: 'Button with isLoading prop must also specify loadingLabel',
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
isLoadingValue.type === 'JSXExpressionContainer' &&
|
||||||
|
isLoadingValue.expression.type !== 'Literal'
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node: isLoadingAttr,
|
||||||
|
message: 'Button with isLoading prop must also specify loadingLabel',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
28
libraries/eslint-plugin/require-script-runner.js
Normal file
28
libraries/eslint-plugin/require-script-runner.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description: 'Require Script Runner for scripts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
let hasImport = false
|
||||||
|
|
||||||
|
return {
|
||||||
|
ImportDeclaration(node) {
|
||||||
|
if (node.source.value.endsWith('lib/ScriptRunner.mjs')) {
|
||||||
|
hasImport = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Program:exit'() {
|
||||||
|
if (!hasImport) {
|
||||||
|
context.report({
|
||||||
|
loc: { line: 1, column: 0 },
|
||||||
|
message:
|
||||||
|
'Please use Script Runner for scripts. Refer to the developer manual (https://manual.dev-overleaf.com/development/code/web_scripts/#monitor-script-execution-and-usage-with-script-runner) for more information.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
121
libraries/eslint-plugin/require-vi-doMock-valid-path.js
Normal file
121
libraries/eslint-plugin/require-vi-doMock-valid-path.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
docs: {
|
||||||
|
description: 'Ensure vi.doMock first argument is a resolvable path.',
|
||||||
|
category: 'Best Practices',
|
||||||
|
recommended: false,
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
fixable: 'code',
|
||||||
|
hasSuggestions: true,
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
unresolvablePath: 'The path "{{pathValue}}" in vi.doMock() cannot be resolved relative to the current file.',
|
||||||
|
notAStringLiteral: 'The first argument of vi.doMock() must be (or resolve to) a string literal representing a path.',
|
||||||
|
noArguments: 'vi.doMock() called with no arguments.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const currentFilePath = context.getFilename();
|
||||||
|
// ESLint can sometimes pass <text> or <input> for snippets not in a file
|
||||||
|
if (currentFilePath === '<text>' || currentFilePath === '<input>') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const currentDirectory = path.dirname(currentFilePath);
|
||||||
|
|
||||||
|
function canResolve(modulePath) {
|
||||||
|
try {
|
||||||
|
require.resolve(path.resolve(currentDirectory, modulePath));
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
const absolutePath = path.resolve(currentDirectory, modulePath);
|
||||||
|
const extensions = ['', '.js', '.mjs', '.ts', '.jsx', '.tsx', '.json', '.node', '/index.js', '/index.ts']; // Add common extensions
|
||||||
|
for (const ext of extensions) {
|
||||||
|
if (fs.existsSync(absolutePath + ext)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
CallExpression(node) {
|
||||||
|
if (
|
||||||
|
node.callee.type === 'MemberExpression' &&
|
||||||
|
node.callee.object.type === 'Identifier' &&
|
||||||
|
node.callee.object.name === 'vi' &&
|
||||||
|
node.callee.property.type === 'Identifier' &&
|
||||||
|
node.callee.property.name === 'doMock'
|
||||||
|
) {
|
||||||
|
if (node.arguments.length === 0) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noArguments',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstArg = node.arguments[0]
|
||||||
|
let pathValue = firstArg.value
|
||||||
|
|
||||||
|
if (firstArg.type !== 'Literal' || typeof firstArg.value !== 'string') {
|
||||||
|
if (firstArg.type === 'Identifier') {
|
||||||
|
const variable = context.getScope().variables.find(v => v.name === firstArg.name);
|
||||||
|
if (
|
||||||
|
variable &&
|
||||||
|
variable.defs.length > 0 &&
|
||||||
|
variable.defs[0].node.init &&
|
||||||
|
variable.defs[0].node.init.type === 'Literal' &&
|
||||||
|
typeof variable.defs[0].node.init.value === 'string'
|
||||||
|
) {
|
||||||
|
pathValue = variable.defs[0].node.init.value
|
||||||
|
if (canResolve(pathValue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If the first argument was a variable that didn't resolve then we can't auto-fix it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.report({
|
||||||
|
node: firstArg,
|
||||||
|
messageId: 'notAStringLiteral',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!pathValue.startsWith('.')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canResolve(pathValue)) {
|
||||||
|
const mjsPath = pathValue.replace('.js', '.mjs')
|
||||||
|
const additionalReportOptions = {}
|
||||||
|
if (canResolve(mjsPath)) {
|
||||||
|
additionalReportOptions.fix = (fixer) => fixer.replaceText(firstArg, `'${mjsPath}'`)
|
||||||
|
additionalReportOptions.suggest = [
|
||||||
|
{
|
||||||
|
desc: `Replace with "${pathValue.replace('.js', '.mjs')}"`,
|
||||||
|
fix: (fixer) => fixer.replaceText(firstArg, `'${mjsPath}'`),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
context.report({
|
||||||
|
node: firstArg,
|
||||||
|
messageId: 'unresolvablePath',
|
||||||
|
data: {
|
||||||
|
pathValue,
|
||||||
|
},
|
||||||
|
...additionalReportOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
161
libraries/eslint-plugin/rules.test.js
Normal file
161
libraries/eslint-plugin/rules.test.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
const { RuleTester } = require('eslint')
|
||||||
|
const preferKebabUrl = require('./prefer-kebab-url')
|
||||||
|
const noUnnecessaryTrans = require('./no-unnecessary-trans')
|
||||||
|
const shouldUnescapeTrans = require('./should-unescape-trans')
|
||||||
|
const noGeneratedEditorThemes = require('./no-generated-editor-themes')
|
||||||
|
const viDoMockValidPath = require('./require-vi-doMock-valid-path')
|
||||||
|
|
||||||
|
const ruleTester = new RuleTester({
|
||||||
|
parser: require.resolve('@typescript-eslint/parser'),
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ruleTester.run('prefer-kebab-url', preferKebabUrl, {
|
||||||
|
valid: [
|
||||||
|
{ code: `app.get('/foo-bar')` },
|
||||||
|
{ code: `app.get('/foo-bar/:id')` },
|
||||||
|
{ code: `router.post('/foo-bar')` },
|
||||||
|
{ code: `router.get('/foo-bar/:id/:name/:age')` },
|
||||||
|
{ code: `webRouter.get('/foo-bar/:user_id/(ProjectName)/get-info')` },
|
||||||
|
{ code: `webApp.post('/foo-bar/:user_id/(ProjectName)/get-info')` },
|
||||||
|
{
|
||||||
|
code: `router.get(/^\\/download\\/project\\/([^/]*)\\/output\\/output\\.pdf$/)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `webRouter.get(/^\\/project\\/([^/]*)\\/user\\/([0-9a-f]+)\\/build\\/([0-9a-f-]+)\\/output\\/(.*)$/)`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `app.get('/fooBar')`,
|
||||||
|
errors: [{ message: 'Route path should be in kebab-case.' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `app.get('/fooBar/:id')`,
|
||||||
|
errors: [{ message: 'Route path should be in kebab-case.' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `webRouter.get('/foo_bar/:id/FooBar/:name/fooBar')`,
|
||||||
|
errors: [{ message: 'Route path should be in kebab-case.' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `router.get(/^\\/downLoad\\/pro-ject\\/([^/]*)\\/OutPut\\/out-put\\.pdf$/)`,
|
||||||
|
errors: [{ message: 'Route path should be in kebab-case.' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
ruleTester.run('no-unnecessary-trans', noUnnecessaryTrans, {
|
||||||
|
valid: [
|
||||||
|
{ code: `<Trans i18nKey="test" components={{ strong: <strong/> }}/>` },
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `<Trans i18nKey="test" values={{ test: 'foo '}}/>`,
|
||||||
|
errors: [{ message: `Use t('…') when there are no components` }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `<Trans i18nKey="test" />`,
|
||||||
|
errors: [{ message: `Use t('…') when there are no components` }],
|
||||||
|
output: `{t('test')}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
ruleTester.run('should-unescape-trans', shouldUnescapeTrans, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: `<Trans i18nKey="test" components={{ strong: <strong/> }}/>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape tOptions={{ interpolation: { escapeValue: true } }}/>`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} />`,
|
||||||
|
errors: [{ message: 'Trans with values must have shouldUnescape' }],
|
||||||
|
output: `<Trans i18nKey="test" values={{ foo: 'bar' }}\nshouldUnescape components={{ strong: <strong/> }} />`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape />`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Trans with shouldUnescape must have tOptions.interpolation.escapeValue',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
output: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape\ntOptions={{ interpolation: { escapeValue: true } }} />`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const noGeneratedEditorThemesError =
|
||||||
|
'EditorView.theme and EditorView.baseTheme each add CSS to the page for every instance of the theme. Store the theme in a variable and reuse it instead.'
|
||||||
|
ruleTester.run('no-generated-editor-themes', noGeneratedEditorThemes, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: `EditorView.theme({ '.cm-editor': { color: 'black' } })`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `const theme = EditorView.theme({ '.cm-editor': { color: 'black' } })`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: `function createTheme() { return EditorView.theme({ '.cm-editor': { color: 'black' } }) }`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: noGeneratedEditorThemesError,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `() => EditorView.theme({ '.cm-editor': { color: 'black' } })`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: noGeneratedEditorThemesError,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: `class Foo { createTheme() { return EditorView.theme({ '.cm-editor': { color: 'black' } }) } }`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: noGeneratedEditorThemesError,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
ruleTester.run('domock-require-valid-path', viDoMockValidPath, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: 'vi.doMock("./require-vi-doMock-valid-path.js")',
|
||||||
|
filename: __filename
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'const filename = "./require-vi-doMock-valid-path.js"; vi.doMock(filename);',
|
||||||
|
filename: __filename
|
||||||
|
}
|
||||||
|
],
|
||||||
|
invalid: [{
|
||||||
|
code: "vi.doMock('./require-vi-doMock-valid-path2')",
|
||||||
|
filename: __filename,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'The path "./require-vi-doMock-valid-path2" in vi.doMock() cannot be resolved relative to the current file.'}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
code: 'const filename = "./require-vi-doMock-valid-path2.js"; vi.doMock(filename);',
|
||||||
|
filename: __filename,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'The first argument of vi.doMock() must be (or resolve to) a string literal representing a path.'}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
})
|
||||||
60
libraries/eslint-plugin/should-unescape-trans.js
Normal file
60
libraries/eslint-plugin/should-unescape-trans.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
fixable: 'code',
|
||||||
|
docs: {
|
||||||
|
description: 'Ensure that Trans with values has shouldUnescape',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
return {
|
||||||
|
'JSXOpeningElement[name.name="Trans"]'(node) {
|
||||||
|
const attributes = new Map(
|
||||||
|
node.attributes.map(attr => [attr.name.name, attr])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (attributes.has('values') && !attributes.has('shouldUnescape')) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Trans with values must have shouldUnescape',
|
||||||
|
fix(fixer) {
|
||||||
|
return fixer.insertTextAfter(
|
||||||
|
attributes.get('values'),
|
||||||
|
'\nshouldUnescape' // Note: Prettier can fix indentation
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.has('values') && attributes.has('shouldUnescape')) {
|
||||||
|
const tOptions = attributes.get('tOptions')
|
||||||
|
if (!tOptions) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message:
|
||||||
|
'Trans with shouldUnescape must have tOptions.interpolation.escapeValue',
|
||||||
|
fix(fixer) {
|
||||||
|
return fixer.insertTextAfter(
|
||||||
|
attributes.get('shouldUnescape'),
|
||||||
|
'\ntOptions={{ interpolation: { escapeValue: true } }}' // Note: Prettier can fix indentation
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const property = tOptions.value.expression.properties
|
||||||
|
.find(p => p.key.name === 'interpolation')
|
||||||
|
?.value.properties.find(p => p.key.name === 'escapeValue')
|
||||||
|
|
||||||
|
if (property?.value.value !== true) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message:
|
||||||
|
'Trans with shouldUnescape must have tOptions.interpolation.escapeValue set to true',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
6
libraries/eslint-plugin/tsconfig.json
Normal file
6
libraries/eslint-plugin/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.backend.json",
|
||||||
|
"include": [
|
||||||
|
"**/*.js",
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user