eslint-plugin-solid
Version:
Solid-specific linting rules for ESLint.
201 lines (190 loc) • 5.6 kB
text/typescript
import { TSESTree as T, TSESLint, ESLintUtils } from "@typescript-eslint/utils";
import { appendImports, insertImports, removeSpecifier } from "../utils";
import { getSourceCode } from "../compat";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
// Below: create maps of imports and types to designated import source.
// We could mess with `Object.keys(require("solid-js"))` to generate this, but requiring it from
// node activates the "node" export condition, which doesn't necessarily match what users will
// receive i.e. through bundlers. Instead, we're manually listing all of the public exports that
// should be imported from "solid-js", etc.
// ==============
type Source = "solid-js" | "solid-js/web" | "solid-js/store";
// Set up map of imports to module
const primitiveMap = new Map<string, Source>();
for (const primitive of [
"createSignal",
"createEffect",
"createMemo",
"createResource",
"onMount",
"onCleanup",
"onError",
"untrack",
"batch",
"on",
"createRoot",
"getOwner",
"runWithOwner",
"mergeProps",
"splitProps",
"useTransition",
"observable",
"from",
"mapArray",
"indexArray",
"createContext",
"useContext",
"children",
"lazy",
"createUniqueId",
"createDeferred",
"createRenderEffect",
"createComputed",
"createReaction",
"createSelector",
"DEV",
"For",
"Show",
"Switch",
"Match",
"Index",
"ErrorBoundary",
"Suspense",
"SuspenseList",
]) {
primitiveMap.set(primitive, "solid-js");
}
for (const primitive of [
"Portal",
"render",
"hydrate",
"renderToString",
"renderToStream",
"isServer",
"renderToStringAsync",
"generateHydrationScript",
"HydrationScript",
"Dynamic",
]) {
primitiveMap.set(primitive, "solid-js/web");
}
for (const primitive of [
"createStore",
"produce",
"reconcile",
"unwrap",
"createMutable",
"modifyMutable",
]) {
primitiveMap.set(primitive, "solid-js/store");
}
// Set up map of type imports to module
const typeMap = new Map<string, Source>();
for (const type of [
"Signal",
"Accessor",
"Setter",
"Resource",
"ResourceActions",
"ResourceOptions",
"ResourceReturn",
"ResourceFetcher",
"InitializedResourceReturn",
"Component",
"VoidProps",
"VoidComponent",
"ParentProps",
"ParentComponent",
"FlowProps",
"FlowComponent",
"ValidComponent",
"ComponentProps",
"Ref",
"MergeProps",
"SplitPrips",
"Context",
"JSX",
"ResolvedChildren",
"MatchProps",
]) {
typeMap.set(type, "solid-js");
}
for (const type of [/* "JSX", */ "MountableElement"]) {
typeMap.set(type, "solid-js/web");
}
for (const type of ["StoreNode", "Store", "SetStoreFunction"]) {
typeMap.set(type, "solid-js/store");
}
const sourceRegex = /^solid-js(?:\/web|\/store)?$/;
const isSource = (source: string): source is Source => sourceRegex.test(source);
export default createRule({
meta: {
type: "suggestion",
docs: {
description:
'Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store".',
url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/imports.md",
},
fixable: "code",
schema: [],
messages: {
"prefer-source": 'Prefer importing {{name}} from "{{source}}".',
},
},
defaultOptions: [],
create(context) {
return {
ImportDeclaration(node) {
const source = node.source.value;
if (!isSource(source)) return;
for (const specifier of node.specifiers) {
if (specifier.type === "ImportSpecifier") {
const isType = specifier.importKind === "type" || node.importKind === "type";
const map = isType ? typeMap : primitiveMap;
const correctSource = map.get(specifier.imported.name);
if (correctSource != null && correctSource !== source) {
context.report({
node: specifier,
messageId: "prefer-source",
data: {
name: specifier.imported.name,
source: correctSource,
},
fix(fixer) {
const sourceCode = getSourceCode(context);
const program: T.Program = sourceCode.ast;
const correctDeclaration = program.body.find(
(node) =>
node.type === "ImportDeclaration" && node.source.value === correctSource
) as T.ImportDeclaration | undefined;
if (correctDeclaration) {
return [
removeSpecifier(fixer, sourceCode, specifier),
appendImports(fixer, sourceCode, correctDeclaration, [
sourceCode.getText(specifier),
]),
].filter(Boolean) as Array<TSESLint.RuleFix>;
}
const firstSolidDeclaration = program.body.find(
(node) => node.type === "ImportDeclaration" && isSource(node.source.value)
) as T.ImportDeclaration | undefined;
return [
removeSpecifier(fixer, sourceCode, specifier),
insertImports(
fixer,
sourceCode,
correctSource,
[sourceCode.getText(specifier)],
firstSolidDeclaration,
isType
),
];
},
});
}
}
}
},
};
},
});