364 lines
8.9 KiB
JavaScript
364 lines
8.9 KiB
JavaScript
import { AST_NODE_TYPES } from '@typescript-eslint/types';
|
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
|
|
function createRule(rule) {
|
|
const _createRule = ESLintUtils.RuleCreator(
|
|
(name) => `https://eslint.nuxt.com/packages/plugin#nuxt${name}`
|
|
);
|
|
return _createRule(rule);
|
|
}
|
|
|
|
const processSuffixes = /* @__PURE__ */ new Set([
|
|
"client",
|
|
"browser",
|
|
"server",
|
|
"nitro",
|
|
"dev",
|
|
"test",
|
|
"prerender"
|
|
]);
|
|
const rule$2 = createRule({
|
|
name: "prefer-import-meta",
|
|
meta: {
|
|
type: "suggestion",
|
|
docs: {
|
|
description: "Prefer using `import.meta.*` over `process.*`"
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
default: "Replace `process.{{ suffix }}` with `import.meta.{{ suffix }}`."
|
|
},
|
|
fixable: "code"
|
|
},
|
|
defaultOptions: [],
|
|
create: (context) => ({
|
|
MemberExpression: (node) => {
|
|
if (node.object.type === AST_NODE_TYPES.Identifier && node.object.name === "process" && node.property.type === AST_NODE_TYPES.Identifier && processSuffixes.has(node.property.name)) {
|
|
const suffix = node.property.name;
|
|
context.report({
|
|
node,
|
|
messageId: "default",
|
|
data: {
|
|
suffix
|
|
},
|
|
fix: (fixer) => fixer.replaceText(node, `import.meta.${suffix}`)
|
|
});
|
|
}
|
|
}
|
|
})
|
|
});
|
|
|
|
const OFFICIAL_MODULES = {
|
|
client: [
|
|
"site",
|
|
// SEO module
|
|
"colorMode",
|
|
"content",
|
|
"mdc",
|
|
"ui"
|
|
],
|
|
server: [
|
|
"hub"
|
|
]
|
|
};
|
|
const ORDER_KEYS = [
|
|
// Ids
|
|
"appId",
|
|
"buildId",
|
|
// Extends
|
|
"extends",
|
|
"theme",
|
|
// Extensions
|
|
"modules",
|
|
"plugins",
|
|
// Env ($production, $development, $test)
|
|
/^\$/,
|
|
// Nuxt Core Features
|
|
"ssr",
|
|
"pages",
|
|
"components",
|
|
"imports",
|
|
"devtools",
|
|
// Client-side Integrations
|
|
"app",
|
|
"css",
|
|
"vue",
|
|
"router",
|
|
"unhead",
|
|
...OFFICIAL_MODULES.client,
|
|
"spaLoadingTemplate",
|
|
// Runtime Configs
|
|
"appConfig",
|
|
"runtimeConfig",
|
|
// Dirs
|
|
"dir",
|
|
"rootDir",
|
|
"srcDir",
|
|
"appDir",
|
|
"workspaceDir",
|
|
"serverDir",
|
|
"buildDir",
|
|
"modulesDir",
|
|
"analyzeDir",
|
|
// Resultions
|
|
"alias",
|
|
"extensions",
|
|
"ignore",
|
|
"ignoreOptions",
|
|
"ignorePrefix",
|
|
// Build Pipeline Configs
|
|
"builder",
|
|
"build",
|
|
"generate",
|
|
"routeRules",
|
|
"sourcemap",
|
|
"optimization",
|
|
// Development
|
|
"dev",
|
|
"devServer",
|
|
"watch",
|
|
"watchers",
|
|
// Feature flags
|
|
"future",
|
|
"features",
|
|
"experimental",
|
|
"compatibilityDate",
|
|
// Nitro
|
|
"nitro",
|
|
...OFFICIAL_MODULES.server,
|
|
"serverHandlers",
|
|
"devServerHandlers",
|
|
// Tooling Integrations
|
|
"vite",
|
|
"webpack",
|
|
"typescript",
|
|
"postcss",
|
|
// Other Integrations
|
|
"test",
|
|
"telemetry",
|
|
// Logging
|
|
"debug",
|
|
"logLevel",
|
|
// Hooks
|
|
"hooks"
|
|
];
|
|
|
|
const rule$1 = createRule({
|
|
name: "nuxt-config-keys-order",
|
|
meta: {
|
|
type: "suggestion",
|
|
docs: {
|
|
description: "Prefer recommended order of Nuxt config properties"
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
default: 'Expected config key "{{a}}" to come before "{{b}}"'
|
|
},
|
|
fixable: "code"
|
|
},
|
|
defaultOptions: [],
|
|
create(context) {
|
|
return {
|
|
ExportDefaultDeclaration(node) {
|
|
let object;
|
|
if (node.declaration.type === "ObjectExpression") {
|
|
object = node.declaration;
|
|
} else if (node.declaration.type === "CallExpression" && node.declaration.arguments[0].type === "ObjectExpression") {
|
|
object = node.declaration.arguments[0];
|
|
}
|
|
if (!object) {
|
|
return;
|
|
}
|
|
const hasFixes = sort(context, object);
|
|
if (!hasFixes) {
|
|
const envProps = object.properties.filter((i) => i.type === "Property" && i.key.type === "Identifier" && i.key.name.startsWith("$"));
|
|
for (const prop of envProps) {
|
|
if (prop.value.type === "ObjectExpression")
|
|
sort(context, prop.value);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
function sort(context, node) {
|
|
return sortAst(
|
|
context,
|
|
node,
|
|
node.properties,
|
|
(prop) => {
|
|
if (prop.type === "Property")
|
|
return getString(prop.key);
|
|
return null;
|
|
},
|
|
sortKeys
|
|
);
|
|
}
|
|
function sortKeys(a, b) {
|
|
const indexA = ORDER_KEYS.findIndex((k) => typeof k === "string" ? k === a : k.test(a));
|
|
const indexB = ORDER_KEYS.findIndex((k) => typeof k === "string" ? k === b : k.test(b));
|
|
if (indexA === -1 && indexB !== -1)
|
|
return 1;
|
|
if (indexA !== -1 && indexB === -1)
|
|
return -1;
|
|
if (indexA < indexB)
|
|
return -1;
|
|
if (indexA > indexB)
|
|
return 1;
|
|
return a.localeCompare(b);
|
|
}
|
|
function sortAst(context, node, list, getName, sort2 = (a, b) => a.localeCompare(b), insertComma = true) {
|
|
const firstToken = context.sourceCode.getFirstToken(node);
|
|
const lastToken = context.sourceCode.getLastToken(node);
|
|
if (!firstToken || !lastToken)
|
|
return false;
|
|
if (list.length < 2)
|
|
return false;
|
|
const reordered = list.slice();
|
|
const ranges = /* @__PURE__ */ new Map();
|
|
const names = /* @__PURE__ */ new Map();
|
|
const rangeStart = Math.max(
|
|
firstToken.range[1],
|
|
context.sourceCode.getIndexFromLoc({
|
|
line: list[0].loc.start.line,
|
|
column: 0
|
|
})
|
|
);
|
|
let rangeEnd = rangeStart;
|
|
for (let i = 0; i < list.length; i++) {
|
|
const item = list[i];
|
|
let name = getName(item);
|
|
if (typeof name === "string")
|
|
name = [name];
|
|
names.set(item, name);
|
|
let lastRange = item.range[1];
|
|
const nextToken = context.sourceCode.getTokenAfter(item);
|
|
if (nextToken?.type === "Punctuator" && nextToken.value === ",")
|
|
lastRange = nextToken.range[1];
|
|
const nextChar = context.sourceCode.getText()[lastRange];
|
|
let text = getTextOf(context.sourceCode, [rangeEnd, lastRange]);
|
|
if (nextToken === lastToken && insertComma)
|
|
text += ",";
|
|
if (nextChar === "\n") {
|
|
lastRange++;
|
|
text += "\n";
|
|
}
|
|
ranges.set(item, [rangeEnd, lastRange, text]);
|
|
rangeEnd = lastRange;
|
|
}
|
|
const segments = [];
|
|
let segmentStart = -1;
|
|
for (let i = 0; i < list.length; i++) {
|
|
if (names.get(list[i]) == null) {
|
|
if (segmentStart > -1)
|
|
segments.push([segmentStart, i]);
|
|
segmentStart = -1;
|
|
} else {
|
|
if (segmentStart === -1)
|
|
segmentStart = i;
|
|
}
|
|
}
|
|
if (segmentStart > -1 && segmentStart !== list.length - 1)
|
|
segments.push([segmentStart, list.length]);
|
|
for (const [start, end] of segments) {
|
|
reordered.splice(
|
|
start,
|
|
end - start,
|
|
...reordered.slice(start, end).sort((a, b) => {
|
|
const nameA = names.get(a);
|
|
const nameB = names.get(b);
|
|
const length = Math.max(nameA.length, nameB.length);
|
|
for (let i = 0; i < length; i++) {
|
|
const a2 = nameA[i];
|
|
const b2 = nameB[i];
|
|
if (a2 == null || b2 == null || a2 === b2)
|
|
continue;
|
|
return sort2(a2, b2);
|
|
}
|
|
return 0;
|
|
})
|
|
);
|
|
}
|
|
const changed = reordered.some((prop, i) => prop !== list[i]);
|
|
if (!changed)
|
|
return false;
|
|
const newContent = reordered.map((i) => ranges.get(i)[2]).join("");
|
|
context.report({
|
|
node,
|
|
messageId: "default",
|
|
data: {
|
|
a: names.get(reordered[0])[0],
|
|
b: names.get(reordered[1])[0]
|
|
},
|
|
fix(fixer) {
|
|
return fixer.replaceTextRange([rangeStart, rangeEnd], newContent);
|
|
}
|
|
});
|
|
}
|
|
function getTextOf(sourceCode, node) {
|
|
if (!node)
|
|
return "";
|
|
if (Array.isArray(node))
|
|
return sourceCode.text.slice(node[0], node[1]);
|
|
return sourceCode.getText(node);
|
|
}
|
|
function getString(node) {
|
|
if (node.type === "Identifier")
|
|
return node.name;
|
|
if (node.type === "Literal")
|
|
return String(node.raw);
|
|
return null;
|
|
}
|
|
|
|
const rule = createRule({
|
|
name: "no-nuxt-config-test-key",
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description: "Disallow setting `test` key in Nuxt config"
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
default: "Do not set `test` key in Nuxt config. The test environment is automatically detected."
|
|
}
|
|
},
|
|
defaultOptions: [],
|
|
create(context) {
|
|
return {
|
|
ExportDefaultDeclaration(node) {
|
|
let object;
|
|
if (node.declaration.type === "ObjectExpression") {
|
|
object = node.declaration;
|
|
} else if (node.declaration.type === "CallExpression" && node.declaration.arguments[0]?.type === "ObjectExpression") {
|
|
object = node.declaration.arguments[0];
|
|
}
|
|
if (!object) {
|
|
return;
|
|
}
|
|
for (const prop of object.properties) {
|
|
if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "test" && (prop.value.type === "Literal" && typeof prop.value.value === "boolean" || prop.value.type === "Identifier" && (prop.value.name === "true" || prop.value.name === "false"))) {
|
|
context.report({
|
|
node: prop,
|
|
messageId: "default"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
const rules = {
|
|
"prefer-import-meta": rule$2,
|
|
"nuxt-config-keys-order": rule$1,
|
|
"no-nuxt-config-test-key": rule
|
|
};
|
|
|
|
const index = {
|
|
meta: {
|
|
name: "@nuxt/eslint-plugin"
|
|
},
|
|
rules
|
|
};
|
|
|
|
export { index as default };
|