Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,270 @@
/* eslint sort-keys: ["error", "asc", {"caseSensitive": false}] */
export const defaultReplacements = {
acc: {
accumulator: true,
},
arg: {
argument: true,
},
args: {
arguments: true,
},
arr: {
array: true,
},
attr: {
attribute: true,
},
attrs: {
attributes: true,
},
btn: {
button: true,
},
cb: {
callback: true,
},
conf: {
config: true,
},
ctx: {
context: true,
},
cur: {
current: true,
},
curr: {
current: true,
},
db: {
database: true,
},
def: {
defer: true,
deferred: true,
define: true,
definition: true,
},
dest: {
destination: true,
},
dev: {
development: true,
},
dir: {
direction: true,
directory: true,
},
dirs: {
directories: true,
},
dist: {
distribution: true,
},
doc: {
document: true,
},
docs: {
documentation: true,
documents: true,
},
dst: {
daylightSavingTime: true,
destination: true,
distribution: true,
},
e: {
error: true,
event: true,
},
el: {
element: true,
},
elem: {
element: true,
},
elems: {
elements: true,
},
env: {
environment: true,
},
envs: {
environments: true,
},
err: {
error: true,
},
ev: {
event: true,
},
evt: {
event: true,
},
ext: {
extension: true,
},
exts: {
extensions: true,
},
fn: {
function: true,
},
func: {
function: true,
},
i: {
index: true,
},
idx: {
index: true,
},
j: {
index: true,
},
len: {
length: true,
},
lib: {
library: true,
},
mod: {
module: true,
},
msg: {
message: true,
},
num: {
number: true,
},
obj: {
object: true,
},
opts: {
options: true,
},
param: {
parameter: true,
},
params: {
parameters: true,
},
pkg: {
package: true,
},
prev: {
previous: true,
},
prod: {
production: true,
},
prop: {
property: true,
},
props: {
properties: true,
},
ref: {
reference: true,
},
refs: {
references: true,
},
rel: {
related: true,
relationship: true,
relative: true,
},
req: {
request: true,
},
res: {
resource: true,
response: true,
result: true,
},
ret: {
returnValue: true,
},
retval: {
returnValue: true,
},
sep: {
separator: true,
},
src: {
source: true,
},
stdDev: {
standardDeviation: true,
},
str: {
string: true,
},
tbl: {
table: true,
},
temp: {
temporary: true,
},
tit: {
title: true,
},
tmp: {
temporary: true,
},
util: {
utility: true,
},
utils: {
utilities: true,
},
val: {
value: true,
},
var: {
variable: true,
},
vars: {
variables: true,
},
ver: {
version: true,
},
};
export const defaultAllowList = {
// React.Component Class property
// https://reactjs.org/docs/react-component.html#defaultprops
defaultProps: true,
// `package.json` field
// https://docs.npmjs.com/specifying-dependencies-and-devdependencies-in-a-package-json-file
devDependencies: true,
// Ember class name
// https://api.emberjs.com/ember/3.10/classes/Ember.EmberENV/properties
EmberENV: true,
// React.Component static method
// https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
getDerivedStateFromProps: true,
// Next.js function
// https://nextjs.org/learn/basics/fetching-data-for-pages
getInitialProps: true,
getServerSideProps: true,
getStaticProps: true,
// The name iOS is a standard name for an OS
iOS: true,
// React PropTypes
// https://reactjs.org/docs/typechecking-with-proptypes.html
propTypes: true,
// Jest configuration
// https://jestjs.io/docs/en/configuration#setupfilesafterenv-array
setupFilesAfterEnv: true,
};
export const defaultIgnore = [
// Internationalization and localization
// https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1188
'i18n',
'l10n',
];

View file

@ -0,0 +1,14 @@
const builtinErrors = [
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
'Error',
'EvalError',
'RangeError',
'ReferenceError',
'SyntaxError',
'TypeError',
'URIError',
'AggregateError',
'SuppressedError',
];
export default builtinErrors;

View file

@ -0,0 +1,275 @@
const getVendorPrefixedName = eventName => [
`webkit${eventName}`,
`o${eventName.toLowerCase()}`,
eventName.toLowerCase(),
];
// https://github.com/google/closure-library/blob/8782d8ba16ef2dd4a508d2081a6938f054fc60e8/closure/goog/events/eventtype.js#L44
const domEvents = new Set([
// Mouse events
'click',
'rightclick',
'dblclick',
'auxclick',
'mousedown',
'mouseup',
'mouseover',
'mouseout',
'mousemove',
'mouseenter',
'mouseleave',
// Non-existent event; will never fire. This exists as a mouse counterpart to
// POINTERCANCEL.
'mousecancel',
// Selection events.
// https://www.w3.org/TR/selection-api/
'selectionchange',
'selectstart', // IE, Safari, Chrome
// Wheel events
// http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents
'wheel',
// Key events
'keypress',
'keydown',
'keyup',
// Focus
'blur',
'focus',
'deactivate', // IE only
'focusin',
'focusout',
// Forms
'change',
'reset',
'select',
'submit',
'input',
'propertychange', // IE only
// Drag and drop
'dragstart',
'drag',
'dragenter',
'dragover',
'dragleave',
'drop',
'dragend',
// Touch events
// Note that other touch events exist, but we should follow the W3C list here.
// http://www.w3.org/TR/touch-events/#list-of-touchevent-types
'touchstart',
'touchmove',
'touchend',
'touchcancel',
// Misc
'beforeunload',
'consolemessage',
'contextmenu',
'devicechange',
'devicemotion',
'deviceorientation',
'DOMContentLoaded',
'error',
'help',
'load',
'losecapture',
'orientationchange',
'readystatechange',
'resize',
'scroll',
'unload',
// Media events
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'ended',
'loadeddata',
'loadedmetadata',
'pause',
'play',
'playing',
'progress',
'ratechange',
'seeked',
'seeking',
'stalled',
'suspend',
'timeupdate',
'volumechange',
'waiting',
// Media Source Extensions events
// https://www.w3.org/TR/media-source/#mediasource-events
'sourceopen',
'sourceended',
'sourceclosed',
// https://www.w3.org/TR/media-source/#sourcebuffer-events
'abort',
'update',
'updatestart',
'updateend',
// HTML 5 History events
// See http://www.w3.org/TR/html5/browsers.html#event-definitions-0
'hashchange',
'pagehide',
'pageshow',
'popstate',
// Copy and Paste
// Support is limited. Make sure it works on your favorite browser
// before using.
// http://www.quirksmode.org/dom/events/cutcopypaste.html
'copy',
'paste',
'cut',
'beforecopy',
'beforecut',
'beforepaste',
// HTML5 online/offline events.
// http://www.w3.org/TR/offline-webapps/#related
'online',
'offline',
// HTML 5 worker events
'message',
'connect',
// Service Worker Events - ServiceWorkerGlobalScope context
// See https://w3c.github.io/ServiceWorker/#execution-context-events
// message event defined in worker events section
'install',
'activate',
'fetch',
'foreignfetch',
'messageerror',
// Service Worker Events - Document context
// See https://w3c.github.io/ServiceWorker/#document-context-events
'statechange',
'updatefound',
'controllerchange',
// CSS animation events.
...getVendorPrefixedName('AnimationStart'),
...getVendorPrefixedName('AnimationEnd'),
...getVendorPrefixedName('AnimationIteration'),
// CSS transition events. Based on the browser support described at:
// https://developer.mozilla.org/en/css/css_transitions#Browser_compatibility
...getVendorPrefixedName('TransitionEnd'),
// W3C Pointer Events
// http://www.w3.org/TR/pointerevents/
'pointerdown',
'pointerup',
'pointercancel',
'pointermove',
'pointerover',
'pointerout',
'pointerenter',
'pointerleave',
'gotpointercapture',
'lostpointercapture',
// IE specific events.
// See http://msdn.microsoft.com/en-us/library/ie/hh772103(v=vs.85).aspx
// these events will be supplanted in IE11.
'MSGestureChange',
'MSGestureEnd',
'MSGestureHold',
'MSGestureStart',
'MSGestureTap',
'MSGotPointerCapture',
'MSInertiaStart',
'MSLostPointerCapture',
'MSPointerCancel',
'MSPointerDown',
'MSPointerEnter',
'MSPointerHover',
'MSPointerLeave',
'MSPointerMove',
'MSPointerOut',
'MSPointerOver',
'MSPointerUp',
// Native IMEs/input tools events.
'text',
// The textInput event is supported in IE9+, but only in lower case. All other
// browsers use the camel-case event name.
'textinput',
'textInput',
'compositionstart',
'compositionupdate',
'compositionend',
// The beforeinput event is initially only supported in Safari. See
// https://bugs.chromium.org/p/chromium/issues/detail?id=342670 for Chrome
// implementation tracking.
'beforeinput',
// Webview tag events
// See https://developer.chrome.com/apps/tags/webview
'exit',
'loadabort',
'loadcommit',
'loadredirect',
'loadstart',
'loadstop',
'responsive',
'sizechanged',
'unresponsive',
// HTML5 Page Visibility API. See details at
// `goog.labs.dom.PageVisibilityMonitor`.
'visibilitychange',
// LocalStorage event.
'storage',
// DOM Level 2 mutation events (deprecated).
'DOMSubtreeModified',
'DOMNodeInserted',
'DOMNodeRemoved',
'DOMNodeRemovedFromDocument',
'DOMNodeInsertedIntoDocument',
'DOMAttrModified',
'DOMCharacterDataModified',
// Print events.
'beforeprint',
'afterprint',
// Web app manifest events.
'beforeinstallprompt',
'appinstalled',
// https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/events/DOMEventNames.js#L12
'afterblur',
'beforeblur',
'cancel',
'close',
'dragexit',
'encrypted',
'fullscreenchange',
'invalid',
'toggle',
// https://github.com/sindresorhus/eslint-plugin-unicorn/pull/147
'search',
'open',
'show',
]);
export default domEvents;

View file

@ -0,0 +1,54 @@
/* eslint sort-keys: ["error", "asc", {natural: true}] */
// https://github.com/facebook/react/blob/b87aabd/packages/react-dom/src/events/getEventKey.js#L36
// Only meta characters which can't be deciphered from `String.fromCharCode()`
const eventKeys = {
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
16: 'Shift',
17: 'Control',
18: 'Alt',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: ' ',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
219: '[',
220: '\\',
221: ']',
222: '\'',
224: 'Meta',
};
export default eventKeys;

View file

@ -0,0 +1,52 @@
import isSameReference from '../utils/is-same-reference.js';
import {getParenthesizedRange} from '../utils/parentheses.js';
import {isNumericLiteral} from '../ast/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
const isLengthMemberExpression = node =>
node.type === 'MemberExpression'
&& !node.computed
&& !node.optional
&& node.property.type === 'Identifier'
&& node.property.name === 'length';
const isLiteralPositiveNumber = node =>
isNumericLiteral(node)
&& node.value > 0;
export function getNegativeIndexLengthNode(node, objectNode) {
if (!node) {
return;
}
const {type, operator, left, right} = node;
if (type !== 'BinaryExpression' || operator !== '-' || !isLiteralPositiveNumber(right)) {
return;
}
if (isLengthMemberExpression(left) && isSameReference(left.object, objectNode)) {
return left;
}
// Nested BinaryExpression
return getNegativeIndexLengthNode(left, objectNode);
}
/**
@param {ESTree.Node} node
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
@returns {ESLint.Rule.ReportFixer}
*/
export function removeLengthNode(node, fixer, context) {
const [start, end] = getParenthesizedRange(node, context);
return fixer.removeRange([
start,
end + context.sourceCode.text.slice(end).match(/\S|$/).index,
]);
}

View file

@ -0,0 +1,139 @@
import {isMethodCall} from '../ast/index.js';
import {getParenthesizedText} from '../utils/index.js';
const MESSAGE_ID_ERROR = 'error';
const MESSAGE_ID_SUGGESTION_APPLY_REPLACEMENT = 'suggestion-apply-replacement';
const MESSAGE_ID_SUGGESTION_SPREADING_ARRAY = 'suggestion-spreading-array';
const MESSAGE_ID_SUGGESTION_NOT_SPREADING_ARRAY = 'suggestion-not-spreading-array';
const methods = new Map([
[
'reverse',
{
replacement: 'toReversed',
predicate: callExpression => isMethodCall(callExpression, {
method: 'reverse',
argumentsLength: 0,
optionalCall: false,
}),
},
],
[
'sort',
{
replacement: 'toSorted',
predicate: callExpression => isMethodCall(callExpression, {
method: 'sort',
maximumArguments: 1,
optionalCall: false,
}),
},
],
]);
const schema = [
{
type: 'object',
additionalProperties: false,
properties: {
allowExpressionStatement: {
type: 'boolean',
},
},
},
];
function noArrayMutateRule(methodName) {
const {
replacement,
predicate,
} = methods.get(methodName);
const messages = {
[MESSAGE_ID_ERROR]: `Use \`Array#${replacement}()\` instead of \`Array#${methodName}()\`.`,
[MESSAGE_ID_SUGGESTION_APPLY_REPLACEMENT]: `Switch to \`.${replacement}()\`.`,
[MESSAGE_ID_SUGGESTION_SPREADING_ARRAY]: 'The spreading object is an array.',
[MESSAGE_ID_SUGGESTION_NOT_SPREADING_ARRAY]: 'The spreading object is NOT an array.',
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
const {allowExpressionStatement} = context.options[0];
return {
CallExpression(callExpression) {
if (!predicate(callExpression)) {
return;
}
const array = callExpression.callee.object;
// `[...array].reverse()`
const isSpreadAndMutate = array.type === 'ArrayExpression'
&& array.elements.length === 1
&& array.elements[0].type === 'SpreadElement';
if (allowExpressionStatement && !isSpreadAndMutate) {
const maybeExpressionStatement = callExpression.parent.type === 'ChainExpression'
? callExpression.parent.parent
: callExpression.parent;
if (maybeExpressionStatement.type === 'ExpressionStatement') {
return;
}
}
const methodProperty = callExpression.callee.property;
const suggestions = [];
const fixMethodName = fixer => fixer.replaceText(methodProperty, replacement);
/*
For `[...array].reverse()`, provide two suggestions, let user choose if the object can be unwrapped,
otherwise only change `.reverse()` to `.toReversed()`
*/
if (isSpreadAndMutate) {
suggestions.push({
messageId: MESSAGE_ID_SUGGESTION_SPREADING_ARRAY,
* fix(fixer) {
const text = getParenthesizedText(array.elements[0].argument, context);
yield fixer.replaceText(array, text);
yield fixMethodName(fixer);
},
});
}
suggestions.push({
messageId: isSpreadAndMutate
? MESSAGE_ID_SUGGESTION_NOT_SPREADING_ARRAY
: MESSAGE_ID_SUGGESTION_APPLY_REPLACEMENT,
fix: fixMethodName,
});
return {
node: methodProperty,
messageId: MESSAGE_ID_ERROR,
suggest: suggestions,
};
},
};
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: `Prefer \`Array#${replacement}()\` over \`Array#${methodName}()\`.`,
recommended: 'unopinionated',
},
hasSuggestions: true,
schema,
defaultOptions: [{allowExpressionStatement: true}],
messages,
},
};
return config;
}
export default noArrayMutateRule;

View file

@ -0,0 +1,80 @@
import {isMethodCall, isMemberExpression} from '../ast/index.js';
import {isSameReference} from '../utils/index.js';
import {removeArgument} from '../fix/index.js';
function getObjectLengthOrInfinityDescription(node, object) {
// `Infinity`
if (node.type === 'Identifier' && node.name === 'Infinity') {
return 'Infinity';
}
// `Number.POSITIVE_INFINITY`
if (isMemberExpression(node, {
object: 'Number',
property: 'POSITIVE_INFINITY',
computed: false,
optional: false,
})) {
return 'Number.POSITIVE_INFINITY';
}
// `object?.length`
const isOptional = node.type === 'ChainExpression';
if (isOptional) {
node = node.expression;
}
// `object.length`
if (!(
isMemberExpression(node, {property: 'length', computed: false})
&& isSameReference(object, node.object)
)) {
return;
}
return `${object.type === 'Identifier' ? object.name : '…'}${isOptional ? '?.' : '.'}length`;
}
/** @param {import('eslint').Rule.RuleContext} context */
function listen(context, {methods, messageId}) {
context.on('CallExpression', callExpression => {
if (!isMethodCall(callExpression, {
methods,
argumentsLength: 2,
optionalCall: false,
})) {
return;
}
const secondArgument = callExpression.arguments[1];
const description = getObjectLengthOrInfinityDescription(
secondArgument,
callExpression.callee.object,
);
if (!description) {
return;
}
const methodName = callExpression.callee.property.name;
const messageData = {
description,
};
if (methodName === 'splice') {
messageData.argumentName = 'deleteCount';
} else if (methodName === 'toSpliced') {
messageData.argumentName = 'skipCount';
}
return {
node: secondArgument,
messageId,
data: messageData,
/** @param {import('eslint').Rule.RuleFixer} fixer */
fix: fixer => removeArgument(fixer, secondArgument, context),
};
});
}
export {listen};

View file

@ -0,0 +1,42 @@
import fs from 'node:fs';
import {findUpSync} from 'find-up-simple';
const directoryCache = new Map();
const dataCache = new Map();
/**
Finds the closest package.json file to the given directory and returns its path and contents.
Caches the result for future lookups.
@param dirname {string}
@return {{ path: string, packageJson: Record<string, unknown> } | undefined}
*/
export function readPackageJson(dirname) {
let packageJsonPath;
if (directoryCache.has(dirname)) {
packageJsonPath = directoryCache.get(dirname);
} else {
packageJsonPath = findUpSync('package.json', {cwd: dirname, type: 'file'});
directoryCache.set(dirname, packageJsonPath);
}
if (!packageJsonPath) {
return;
}
let packageJson;
if (dataCache.has(packageJsonPath)) {
packageJson = dataCache.get(packageJsonPath);
} else {
try {
packageJson = JSON.parse(fs.readFileSync(packageJsonPath));
dataCache.set(packageJsonPath, packageJson);
} catch {
// This can happen if package.json files have comments in them etc.
return;
}
}
return {path: packageJsonPath, packageJson};
}

View file

@ -0,0 +1,126 @@
import {hasSideEffect, isParenthesized, findVariable} from '@eslint-community/eslint-utils';
import {isMethodCall} from '../ast/index.js';
import {isSameIdentifier, isFunctionSelfUsedInside} from '../utils/index.js';
const isSimpleCompare = (node, compareNode) =>
node.type === 'BinaryExpression'
&& node.operator === '==='
&& (
isSameIdentifier(node.left, compareNode)
|| isSameIdentifier(node.right, compareNode)
);
const isSimpleCompareCallbackFunction = node =>
// Matches `foo.findIndex(bar => bar === baz)`
(
node.type === 'ArrowFunctionExpression'
&& !node.async
&& node.params.length === 1
&& isSimpleCompare(node.body, node.params[0])
)
// Matches `foo.findIndex(bar => {return bar === baz})`
// Matches `foo.findIndex(function (bar) {return bar === baz})`
|| (
(node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression')
&& !node.async
&& !node.generator
&& node.params.length === 1
&& node.body.type === 'BlockStatement'
&& node.body.body.length === 1
&& node.body.body[0].type === 'ReturnStatement'
&& isSimpleCompare(node.body.body[0].argument, node.params[0])
);
const isIdentifierNamed = ({type, name}, expectName) => type === 'Identifier' && name === expectName;
export default function simpleArraySearchRule({method, replacement}) {
// Add prefix to avoid conflicts in `prefer-includes` rule
const MESSAGE_ID_PREFIX = `prefer-${replacement}-over-${method}/`;
const ERROR = `${MESSAGE_ID_PREFIX}error`;
const SUGGESTION = `${MESSAGE_ID_PREFIX}suggestion`;
const ERROR_MESSAGES = {
findIndex: 'Use `.indexOf()` instead of `.findIndex()` when looking for the index of an item.',
findLastIndex: 'Use `.lastIndexOf()` instead of `findLastIndex() when looking for the index of an item.`',
some: `Use \`.${replacement}()\` instead of \`.${method}()\` when checking value existence.`,
};
const messages = {
[ERROR]: ERROR_MESSAGES[method],
[SUGGESTION]: `Replace \`.${method}()\` with \`.${replacement}()\`.`,
};
function listen(context) {
const {sourceCode} = context;
const {scopeManager} = sourceCode;
context.on('CallExpression', callExpression => {
if (
!isMethodCall(callExpression, {
method,
argumentsLength: 1,
optionalCall: false,
})
|| !isSimpleCompareCallbackFunction(callExpression.arguments[0])
) {
return;
}
const [callback] = callExpression.arguments;
const binaryExpression = callback.body.type === 'BinaryExpression'
? callback.body
: callback.body.body[0].argument;
const [parameter] = callback.params;
const {left, right} = binaryExpression;
const {name} = parameter;
let searchValueNode;
let parameterInBinaryExpression;
if (isIdentifierNamed(left, name)) {
searchValueNode = right;
parameterInBinaryExpression = left;
} else if (isIdentifierNamed(right, name)) {
searchValueNode = left;
parameterInBinaryExpression = right;
} else {
return;
}
const callbackScope = scopeManager.acquire(callback);
if (
// Can't use scopeManager in vue template
// https://github.com/vuejs/vue-eslint-parser/issues/263
!callbackScope
// `parameter` is used somewhere else
|| findVariable(callbackScope, parameter).references.some(({identifier}) => identifier !== parameterInBinaryExpression)
|| isFunctionSelfUsedInside(callback, callbackScope)
) {
return;
}
const methodNode = callExpression.callee.property;
const problem = {
node: methodNode,
messageId: ERROR,
suggest: [],
};
const fix = function * (fixer) {
let text = sourceCode.getText(searchValueNode);
if (isParenthesized(searchValueNode, sourceCode) && !isParenthesized(callback, sourceCode)) {
text = `(${text})`;
}
yield fixer.replaceText(methodNode, replacement);
yield fixer.replaceText(callback, text);
};
if (hasSideEffect(searchValueNode, sourceCode)) {
problem.suggest.push({messageId: SUGGESTION, fix});
} else {
problem.fix = fix;
}
return problem;
});
}
return {messages, listen};
}

View file

@ -0,0 +1,17 @@
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description
const typedArrayTypes = [
'Int8Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'Float16Array',
'Float32Array',
'Float64Array',
'BigInt64Array',
'BigUint64Array',
];
export default typedArrayTypes;