Move the action from Deno to Node.js and Vite+
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* `project` — lint rules this repository holds its own source to. Deno lint
|
||||
* ran here with its `recommended` rules and no plugins; the Oxlint categories
|
||||
* in `vite.config.ts` plus the rule list below keep at least that coverage.
|
||||
* Its rules of its own are `no-disable-comments` and `no-non-ts-imports`.
|
||||
*
|
||||
* Loaded by `vite.config.ts` as an Oxlint `jsPlugins` entry.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { LanguageOptions, RuleDefinition, RuleVisitor, TextSourceCode } from "@eslint/core";
|
||||
import type { TSESTree } from "@typescript-eslint/types";
|
||||
import type { DummyRuleMap } from "vite-plus/lint";
|
||||
|
||||
/** The source code both hosts hand a rule, with the comment list. */
|
||||
interface ProjectSourceCode extends TextSourceCode {
|
||||
getAllComments(): TSESTree.Comment[];
|
||||
}
|
||||
|
||||
/** ESLint's default rule types, reporting TypeScript-ESTree nodes. */
|
||||
interface ProjectRuleTypes {
|
||||
LangOptions: LanguageOptions;
|
||||
Code: ProjectSourceCode;
|
||||
RuleOptions: [];
|
||||
Visitor: RuleVisitor;
|
||||
Node: TSESTree.Node;
|
||||
MessageIds: string;
|
||||
ExtRuleDocs: unknown;
|
||||
}
|
||||
|
||||
/** A repo-local rule, in ESLint's standard `create` form. */
|
||||
type ProjectRule = RuleDefinition<ProjectRuleTypes>;
|
||||
|
||||
/** A comment that turns lint rules off or back on, in ESLint or Oxlint form. */
|
||||
const DIRECTIVE_PATTERN: RegExp = /^\s*(?:eslint|oxlint)-(?:disable|enable)/u;
|
||||
|
||||
/** This rule's own name, which a directive may not use to silence it. */
|
||||
const RULE_NAME: string = "no-disable-comments";
|
||||
|
||||
/**
|
||||
* `no-disable-comments` — rejects every `eslint-disable*`, `eslint-enable*`,
|
||||
* `oxlint-disable*`, and `oxlint-enable*` comment, line or block. Oxlint has
|
||||
* no `noInlineConfig` and no `eslint-comments` rules, so this repo bans them
|
||||
* itself: when a rule fires, fix the source.
|
||||
*
|
||||
* A directive can silence the report on itself, so two built-in checks close
|
||||
* the gaps: `unicorn/no-abusive-eslint-disable` fails one that names no rule,
|
||||
* and this rule skips one that names it, which then silences nothing and fails
|
||||
* `reportUnusedDisableDirectives` in `vite.config.ts`.
|
||||
*/
|
||||
const noDisableCommentsRule: ProjectRule = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: { description: "Disallow lint disable and enable comments" },
|
||||
schema: [],
|
||||
messages: { directive: "Lint disable comments are not allowed; fix the source." },
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Program(_node: TSESTree.Program): void {
|
||||
for (const comment of context.sourceCode.getAllComments()) {
|
||||
if (DIRECTIVE_PATTERN.test(comment.value) && !comment.value.includes(RULE_NAME)) {
|
||||
context.report({ loc: comment.loc, messageId: "directive" });
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/** A module specifier read as a path: relative, absolute, or a `#` alias. */
|
||||
const PATH_SPECIFIER_PATTERN: RegExp = /^[./#]/u;
|
||||
|
||||
/**
|
||||
* Whether `source` names something other than a `.ts` module: a query string
|
||||
* such as `?raw`, or a final path segment with any other extension. Bare
|
||||
* package names are left alone, dots and all, as are extensionless paths.
|
||||
*/
|
||||
function isNonTsSource(source: string): boolean {
|
||||
if (source.includes("?")) {
|
||||
return true;
|
||||
}
|
||||
const segments: string[] = source.split("/");
|
||||
if (!PATH_SPECIFIER_PATTERN.test(source)) {
|
||||
const nameLength: number = source.startsWith("@") ? 2 : 1;
|
||||
if (segments.length <= nameLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const lastSegment: string = segments.at(-1) ?? "";
|
||||
if (lastSegment === "." || lastSegment === ".." || !lastSegment.includes(".")) {
|
||||
return false;
|
||||
}
|
||||
return !lastSegment.endsWith(".ts");
|
||||
}
|
||||
|
||||
/**
|
||||
* `no-non-ts-imports` — rejects importing anything but a `.ts` module: a
|
||||
* source path, static or a dynamic `import()` with a string literal, ending in
|
||||
* another extension (`.css`, `.json`, `.svg`, `.js`, `.tsx`) or carrying a
|
||||
* query string (`?raw`, `?url`), and any import or export with import
|
||||
* attributes. Bare package names, `#` aliases, and `node:` builtins without an
|
||||
* extension pass; package subpaths are checked like paths.
|
||||
*/
|
||||
const noNonTsImportsRule: ProjectRule = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: { description: "Disallow importing modules other than .ts files" },
|
||||
schema: [],
|
||||
messages: {
|
||||
extension: "Only .ts modules may be imported.",
|
||||
attributes: "Import attributes are not allowed.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
function checkSource(source: TSESTree.StringLiteral): void {
|
||||
if (isNonTsSource(source.value)) {
|
||||
context.report({ node: source, messageId: "extension" });
|
||||
}
|
||||
}
|
||||
function checkDeclaration(
|
||||
node:
|
||||
| TSESTree.ImportDeclaration
|
||||
| TSESTree.ExportNamedDeclaration
|
||||
| TSESTree.ExportAllDeclaration,
|
||||
): void {
|
||||
if (node.source === null) {
|
||||
return;
|
||||
}
|
||||
checkSource(node.source);
|
||||
if (node.attributes.length > 0) {
|
||||
context.report({ node, messageId: "attributes" });
|
||||
}
|
||||
}
|
||||
return {
|
||||
ImportDeclaration: checkDeclaration,
|
||||
ExportNamedDeclaration: checkDeclaration,
|
||||
ExportAllDeclaration: checkDeclaration,
|
||||
ImportExpression(node: TSESTree.ImportExpression): void {
|
||||
if (node.source.type === "Literal" && typeof node.source.value === "string") {
|
||||
checkSource(node.source);
|
||||
}
|
||||
if (node.options !== null) {
|
||||
context.report({ node, messageId: "attributes" });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This repository's own rule list, spread by `vite.config.ts`: Deno's
|
||||
* `recommended` rules that sit outside the Oxlint categories, plus the
|
||||
* template's extras. Built-in Oxlint rule ids sit here too, so
|
||||
* `vite.config.ts` lists no rule by name.
|
||||
*/
|
||||
const rules: DummyRuleMap = {
|
||||
// The explicit-types template's `camelcase` stand-in: no `_` inside a name
|
||||
// once leading and trailing ones are trimmed, unless it has no lowercase
|
||||
// ASCII letter (UPPER_SNAKE).
|
||||
"id-match": [
|
||||
"error",
|
||||
"^_*[^_]*_*$|^[^a-z]*$",
|
||||
{ properties: false, classFields: false, onlyDeclarations: false, ignoreDestructuring: true },
|
||||
],
|
||||
// Deno's `recommended` rules outside the categories.
|
||||
"no-array-constructor": "error",
|
||||
"no-case-declarations": "error",
|
||||
"no-empty": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-inner-declarations": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"require-await": "error",
|
||||
"require-yield": "error",
|
||||
"typescript/adjacent-overload-signatures": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/no-empty-interface": "error",
|
||||
"typescript/no-empty-object-type": "error",
|
||||
"typescript/no-explicit-any": "error",
|
||||
"typescript/no-namespace": "error",
|
||||
"typescript/no-non-null-asserted-optional-chain": "error",
|
||||
"typescript/no-unsafe-function-type": "error",
|
||||
"typescript/no-wrapper-object-types": "error",
|
||||
"typescript/prefer-as-const": "error",
|
||||
"typescript/prefer-namespace-keyword": "error",
|
||||
"typescript/triple-slash-reference": "error",
|
||||
// Deno's `no-window`, `no-process-global`, and `no-node-globals`: on Node,
|
||||
// these come from an explicit `node:` import instead of the global scope.
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
"window",
|
||||
"process",
|
||||
"Buffer",
|
||||
"global",
|
||||
"setImmediate",
|
||||
"clearImmediate",
|
||||
],
|
||||
// The explicit-types template's extras.
|
||||
"import/no-cycle": "error",
|
||||
"typescript/consistent-type-imports": "error",
|
||||
"typescript/no-import-type-side-effects": "error",
|
||||
"typescript/no-non-null-assertion": "error",
|
||||
// Deno's `ban-untagged-ignore` and `ban-unused-ignore`, made stricter: no
|
||||
// disable comments at all.
|
||||
"project/no-disable-comments": "error",
|
||||
"project/no-non-ts-imports": "error",
|
||||
"unicorn/no-abusive-eslint-disable": "error",
|
||||
// Pedantic, but Node's and the web's own types (`Response`, `Stats`,
|
||||
// `RequestInit`) are mutable, so no parameter of theirs can pass; the
|
||||
// explicit-types template and the app repos turn it off too.
|
||||
"typescript/prefer-readonly-parameter-types": "off",
|
||||
};
|
||||
|
||||
/**
|
||||
* The repo-local plugin, namespace `project`: its own rules, enabled as
|
||||
* `project/<rule>`, and `configs.project`, the rule list above.
|
||||
*/
|
||||
const plugin = {
|
||||
meta: { name: "project" },
|
||||
rules: {
|
||||
"no-disable-comments": noDisableCommentsRule,
|
||||
"no-non-ts-imports": noNonTsImportsRule,
|
||||
},
|
||||
configs: { project: { rules } },
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
Reference in New Issue
Block a user