diff --git a/scripts/project-lint.ts b/scripts/project-lint.ts index 2da8fd0..14d52bb 100644 --- a/scripts/project-lint.ts +++ b/scripts/project-lint.ts @@ -1,6 +1,6 @@ /** * `project` — lint rules this repository holds its own source to, on top of - * the Oxlint categories in `vite.config.ts`. Its rules of its own are `no-disable-comments` and `no-non-ts-imports`. + * the Oxlint categories in `vite.config.ts`. Its rules of its own are `no-disable-comments` and `no-asset-imports`. * * Loaded by `vite.config.ts` as an Oxlint `jsPlugins` entry. * @@ -8,7 +8,7 @@ */ import type { LanguageOptions, RuleDefinition, RuleVisitor, TextSourceCode } from "@eslint/core"; -import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES, 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. */ @@ -70,12 +70,16 @@ const noDisableCommentsRule: ProjectRule = { /** A module specifier read as a path: relative, absolute, or a `#` alias. */ const PATH_SPECIFIER_PATTERN: RegExp = /^[./#]/u; +/** Script module extensions an import may name: our `.ts` files and `.js` package subpaths. */ +const SCRIPT_EXTENSIONS: readonly string[] = [".ts", ".js"]; + /** - * 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. + * Whether `source` names an asset rather than a script module: a query string + * such as `?raw`, or a final path segment with an extension other than `.ts` + * or `.js`. Bare package names are left alone, dots and all, as are + * extensionless paths. */ -function isNonTsSource(source: string): boolean { +function isAssetSource(source: string): boolean { if (source.includes("?")) { return true; } @@ -90,30 +94,31 @@ function isNonTsSource(source: string): boolean { if (lastSegment === "." || lastSegment === ".." || !lastSegment.includes(".")) { return false; } - return !lastSegment.endsWith(".ts"); + return !SCRIPT_EXTENSIONS.some((extension: string): boolean => lastSegment.endsWith(extension)); } /** - * `no-non-ts-imports` — rejects importing anything but a `.ts` module: a + * `no-asset-imports` — rejects importing anything but a script 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. + * an extension other than `.ts` or `.js` (`.css`, `.json`, `.svg`, `.tsx`) or + * carrying a query string (`?raw`, `?url`), and any import or export with + * import attributes. `.js` covers package subpaths such as + * `lit/decorators.js`. Bare package names, `#` aliases, and `node:` builtins + * without an extension pass; package subpaths are checked like paths. */ -const noNonTsImportsRule: ProjectRule = { +const noAssetImportsRule: ProjectRule = { meta: { type: "problem", - docs: { description: "Disallow importing modules other than .ts files" }, + docs: { description: "Disallow importing assets instead of .ts or .js modules" }, schema: [], messages: { - extension: "Only .ts modules may be imported.", + extension: "Only .ts and .js modules may be imported.", attributes: "Import attributes are not allowed.", }, }, create(context) { function checkSource(source: TSESTree.StringLiteral): void { - if (isNonTsSource(source.value)) { + if (isAssetSource(source.value)) { context.report({ node: source, messageId: "extension" }); } } @@ -136,7 +141,7 @@ const noNonTsImportsRule: ProjectRule = { ExportNamedDeclaration: checkDeclaration, ExportAllDeclaration: checkDeclaration, ImportExpression(node: TSESTree.ImportExpression): void { - if (node.source.type === "Literal" && typeof node.source.value === "string") { + if (node.source.type === AST_NODE_TYPES.Literal && typeof node.source.value === "string") { checkSource(node.source); } if (node.options !== null) { @@ -203,7 +208,7 @@ const rules: DummyRuleMap = { "typescript/no-non-null-assertion": "error", // No disable comments at all. "project/no-disable-comments": "error", - "project/no-non-ts-imports": "error", + "project/no-asset-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 @@ -219,7 +224,7 @@ const plugin = { meta: { name: "project" }, rules: { "no-disable-comments": noDisableCommentsRule, - "no-non-ts-imports": noNonTsImportsRule, + "no-asset-imports": noAssetImportsRule, }, configs: { project: { rules } }, };