larrylint 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -19
- package/dist/_chunks/check.mjs +2 -34
- package/dist/_chunks/check2.mjs +1 -1
- package/dist/_chunks/init.mjs +15 -15
- package/dist/_chunks/package.mjs +2 -2
- package/dist/_chunks/preset.mjs +1619 -364
- package/dist/index.d.mts +26 -8
- package/package.json +14 -13
package/dist/_chunks/preset.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { name, version } from "./package.mjs";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { loadConfig } from "c12";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { dirname, join, normalize, parse, relative, resolve } from "pathe";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import tsParser, { parse as parse$1 } from "@typescript-eslint/parser";
|
|
7
|
+
import { parse as parse$2 } from "vue-eslint-parser";
|
|
6
8
|
function defineLarrylintConfig(config) {
|
|
7
9
|
return config;
|
|
8
10
|
}
|
|
@@ -15,83 +17,22 @@ async function loadLarrylintConfig(cwd = process.cwd()) {
|
|
|
15
17
|
globalRc: false,
|
|
16
18
|
dotenv: false
|
|
17
19
|
});
|
|
18
|
-
return {
|
|
20
|
+
return { heavyPackages: config.heavyPackages ?? [] };
|
|
19
21
|
}
|
|
20
|
-
const
|
|
21
|
-
const cache = /* @__PURE__ */ new Map();
|
|
22
|
-
function readBaseline(root) {
|
|
23
|
-
const file = join(root, BASELINE_FILE);
|
|
24
|
-
let mtimeMs;
|
|
25
|
-
try {
|
|
26
|
-
mtimeMs = statSync(file).mtimeMs;
|
|
27
|
-
} catch {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
const cached = cache.get(file);
|
|
31
|
-
if (cached?.mtimeMs === mtimeMs) return cached.baseline;
|
|
32
|
-
const baseline = JSON.parse(readFileSync(file, "utf8"));
|
|
33
|
-
cache.set(file, {
|
|
34
|
-
mtimeMs,
|
|
35
|
-
baseline
|
|
36
|
-
});
|
|
37
|
-
return baseline;
|
|
38
|
-
}
|
|
39
|
-
function writeBaseline(root, baseline) {
|
|
40
|
-
const file = join(root, BASELINE_FILE);
|
|
41
|
-
const files = Object.keys(baseline).sort();
|
|
42
|
-
if (files.length === 0) {
|
|
43
|
-
rmSync(file, { force: true });
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const sorted = {};
|
|
47
|
-
for (const path of files) sorted[path] = Object.fromEntries(Object.entries(baseline[path]).sort(([a], [b]) => a.localeCompare(b)));
|
|
48
|
-
writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
49
|
-
}
|
|
50
|
-
function countViolations(baseline) {
|
|
51
|
-
return Object.values(baseline).flatMap((rules) => Object.values(rules)).reduce((sum, count) => sum + count, 0);
|
|
52
|
-
}
|
|
53
|
-
function createReporter(context, file) {
|
|
54
|
-
const reports = [];
|
|
55
|
-
return {
|
|
56
|
-
report: (descriptor) => {
|
|
57
|
-
reports.push(descriptor);
|
|
58
|
-
},
|
|
59
|
-
flush: () => {
|
|
60
|
-
const settings = context.settings.larrylint;
|
|
61
|
-
const allowed = file && settings?.baseline !== false ? readBaseline(file.root)[file.path]?.[context.id] ?? 0 : 0;
|
|
62
|
-
if (reports.length > allowed) for (const descriptor of reports) context.report(descriptor);
|
|
63
|
-
reports.length = 0;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
const directories = /* @__PURE__ */ new Map();
|
|
68
|
-
function isDirectory(path) {
|
|
69
|
-
let result = directories.get(path);
|
|
70
|
-
if (result === void 0) {
|
|
71
|
-
try {
|
|
72
|
-
result = statSync(path).isDirectory();
|
|
73
|
-
} catch {
|
|
74
|
-
result = false;
|
|
75
|
-
}
|
|
76
|
-
directories.set(path, result);
|
|
77
|
-
}
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
function resolveImport(importer, source) {
|
|
81
|
-
if (!source.startsWith("./") && !source.startsWith("../")) return;
|
|
82
|
-
const target = resolve(dirname(importer), source.split("?")[0]);
|
|
83
|
-
return isDirectory(target) ? `${target}/index` : target;
|
|
84
|
-
}
|
|
85
|
-
const HANDLER_FILE = /\.(?:query|resolver|link|action|template|page-index)(?:\.[cm]?[jt]s)?$/;
|
|
22
|
+
const HANDLER_FILE = /\.(query|resolver|link|action|templates?|page-index)(?:\.[cm]?[jt]s)?$/;
|
|
86
23
|
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
87
24
|
const APP_FOLDERS = {
|
|
88
|
-
sections: "section",
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
25
|
+
"sections": "section",
|
|
26
|
+
"section": "section",
|
|
27
|
+
"blocks": "block",
|
|
28
|
+
"block": "block",
|
|
29
|
+
"components": "component",
|
|
30
|
+
"composables": "composable",
|
|
31
|
+
"utils": "app-util",
|
|
32
|
+
"plugins": "app-plugin",
|
|
33
|
+
"overrides": "override",
|
|
34
|
+
"theme": "theme",
|
|
35
|
+
"shared-fields": "shared-field"
|
|
95
36
|
};
|
|
96
37
|
const SERVER_FOLDERS = {
|
|
97
38
|
"middleware": "middleware",
|
|
@@ -127,10 +68,6 @@ function classify(file) {
|
|
|
127
68
|
test
|
|
128
69
|
};
|
|
129
70
|
}
|
|
130
|
-
function domainOf(file) {
|
|
131
|
-
if (!file.domain || file.kind === "handler" || file.kind === "orchestr-file") return file.domain;
|
|
132
|
-
return isDirectory(`${file.root}/src/runtime/server/orchestr/${file.domain}`) ? file.domain : void 0;
|
|
133
|
-
}
|
|
134
71
|
function classifyRuntime(parts) {
|
|
135
72
|
const [side, folder = "", ...rest] = parts;
|
|
136
73
|
if (parts.length < 2 || side !== "app" && side !== "server" && side !== "shared") return { side: "other" };
|
|
@@ -142,90 +79,977 @@ function classifyRuntime(parts) {
|
|
|
142
79
|
side,
|
|
143
80
|
kind: APP_FOLDERS[folder] ?? "other"
|
|
144
81
|
};
|
|
145
|
-
const nested = rest.length > 1;
|
|
146
82
|
if (folder === "orchestr") {
|
|
147
|
-
if (
|
|
83
|
+
if (rest.length > 1 && rest[0] === "plugins") return {
|
|
148
84
|
side,
|
|
149
85
|
kind: "orchestr-plugin"
|
|
150
86
|
};
|
|
87
|
+
const handler = HANDLER_FILE.exec(parts.at(-1))?.[1]?.replace("templates", "template");
|
|
88
|
+
return {
|
|
89
|
+
side,
|
|
90
|
+
kind: handler ? "handler" : "orchestr-file",
|
|
91
|
+
handler
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
side,
|
|
96
|
+
kind: SERVER_FOLDERS[folder] ?? "other"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const EXTENSIONS = [
|
|
100
|
+
"",
|
|
101
|
+
".ts",
|
|
102
|
+
".mts",
|
|
103
|
+
".cts",
|
|
104
|
+
".js",
|
|
105
|
+
".mjs",
|
|
106
|
+
".cjs",
|
|
107
|
+
"/index.ts",
|
|
108
|
+
"/index.js"
|
|
109
|
+
];
|
|
110
|
+
const directories = /* @__PURE__ */ new Map();
|
|
111
|
+
function cachedReader(parse) {
|
|
112
|
+
const cache = /* @__PURE__ */ new Map();
|
|
113
|
+
return (path) => {
|
|
114
|
+
let mtimeMs;
|
|
115
|
+
try {
|
|
116
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
117
|
+
} catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const cached = cache.get(path);
|
|
121
|
+
if (cached?.mtimeMs === mtimeMs) return cached.value;
|
|
122
|
+
const value = parse(readFileSync(path, "utf8"));
|
|
123
|
+
cache.set(path, {
|
|
124
|
+
mtimeMs,
|
|
125
|
+
value
|
|
126
|
+
});
|
|
127
|
+
return value;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function isDirectory(path) {
|
|
131
|
+
let result = directories.get(path);
|
|
132
|
+
if (result === void 0) {
|
|
133
|
+
try {
|
|
134
|
+
result = statSync(path).isDirectory();
|
|
135
|
+
} catch {
|
|
136
|
+
result = false;
|
|
137
|
+
}
|
|
138
|
+
directories.set(path, result);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
function resolveImport(importer, source) {
|
|
143
|
+
if (!source.startsWith("./") && !source.startsWith("../")) return;
|
|
144
|
+
const target = resolve(dirname(importer), source.split("?")[0]);
|
|
145
|
+
return isDirectory(target) ? `${target}/index` : target;
|
|
146
|
+
}
|
|
147
|
+
function resolveModule(importer, source) {
|
|
148
|
+
if (!source.startsWith("./") && !source.startsWith("../")) return;
|
|
149
|
+
const target = resolve(dirname(importer), source.split("?")[0]);
|
|
150
|
+
return EXTENSIONS.map((extension) => `${target}${extension}`).find((path) => existsSync(path) && !isDirectory(path));
|
|
151
|
+
}
|
|
152
|
+
function findPackage(from, name) {
|
|
153
|
+
for (let folder = from;; folder = dirname(folder)) {
|
|
154
|
+
const candidate = join(folder, "node_modules", name);
|
|
155
|
+
if (existsSync(join(candidate, "package.json"))) return realpathSync(candidate);
|
|
156
|
+
if (dirname(folder) === folder) return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const BASELINE_FILE = "larrylint-baseline.json";
|
|
160
|
+
const readBaselineFile = cachedReader((text) => JSON.parse(text));
|
|
161
|
+
function readBaseline(root) {
|
|
162
|
+
return readBaselineFile(join(root, "larrylint-baseline.json")) ?? {};
|
|
163
|
+
}
|
|
164
|
+
function writeBaseline(root, baseline) {
|
|
165
|
+
const file = join(root, BASELINE_FILE);
|
|
166
|
+
const files = Object.keys(baseline).sort();
|
|
167
|
+
if (files.length === 0) {
|
|
168
|
+
rmSync(file, { force: true });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const sorted = {};
|
|
172
|
+
for (const path of files) sorted[path] = Object.fromEntries(Object.entries(baseline[path]).sort(([a], [b]) => a.localeCompare(b)));
|
|
173
|
+
writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
174
|
+
}
|
|
175
|
+
function countViolations(baseline) {
|
|
176
|
+
return Object.values(baseline).flatMap((rules) => Object.values(rules)).reduce((sum, count) => sum + count, 0);
|
|
177
|
+
}
|
|
178
|
+
function applyBaseline(cwd, results) {
|
|
179
|
+
const baseline = readBaseline(cwd);
|
|
180
|
+
const violations = {};
|
|
181
|
+
let baselined = 0;
|
|
182
|
+
let improved = 0;
|
|
183
|
+
const filtered = results.map((result) => {
|
|
184
|
+
const path = relative(cwd, result.filePath);
|
|
185
|
+
const perRule = {};
|
|
186
|
+
const relevant = result.messages.filter((message) => message.fatal || message.ruleId?.startsWith("larrylint/"));
|
|
187
|
+
for (const { ruleId } of relevant) if (ruleId) perRule[ruleId] = (perRule[ruleId] ?? 0) + 1;
|
|
188
|
+
if (Object.keys(perRule).length > 0) violations[path] = perRule;
|
|
189
|
+
const messages = relevant.filter(({ ruleId }) => !ruleId || perRule[ruleId] > (baseline[path]?.[ruleId] ?? 0));
|
|
190
|
+
baselined += relevant.length - messages.length;
|
|
191
|
+
return {
|
|
192
|
+
...result,
|
|
193
|
+
messages,
|
|
194
|
+
errorCount: messages.filter((message) => message.severity === 2).length,
|
|
195
|
+
fatalErrorCount: messages.filter((message) => message.fatal).length,
|
|
196
|
+
warningCount: messages.filter((message) => message.severity === 1).length,
|
|
197
|
+
fixableErrorCount: messages.filter((message) => message.severity === 2 && message.fix).length,
|
|
198
|
+
fixableWarningCount: messages.filter((message) => message.severity === 1 && message.fix).length
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
for (const [path, rules] of Object.entries(baseline)) for (const [rule, allowed] of Object.entries(rules)) if ((violations[path]?.[rule] ?? 0) < allowed) improved++;
|
|
202
|
+
return {
|
|
203
|
+
results: filtered,
|
|
204
|
+
violations,
|
|
205
|
+
baselined,
|
|
206
|
+
improved
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function defineRule(rule) {
|
|
210
|
+
return {
|
|
211
|
+
meta: rule.meta,
|
|
212
|
+
create(context) {
|
|
213
|
+
const file = classify(context.filename);
|
|
214
|
+
if (!file || file.test || !rule.applies(file)) return {};
|
|
215
|
+
const reports = [];
|
|
216
|
+
let template;
|
|
217
|
+
const { "Program:exit": programExit, ...script } = rule.create({
|
|
218
|
+
context,
|
|
219
|
+
file,
|
|
220
|
+
report: (descriptor) => {
|
|
221
|
+
reports.push(descriptor);
|
|
222
|
+
},
|
|
223
|
+
visitTemplate: (listener) => {
|
|
224
|
+
template = listener;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
const program = context.sourceCode.ast;
|
|
228
|
+
const finish = () => {
|
|
229
|
+
programExit?.(program);
|
|
230
|
+
const allowed = context.settings.larrylint?.baseline === false ? 0 : readBaseline(file.root)[file.path]?.[context.id] ?? 0;
|
|
231
|
+
if (reports.length > allowed) for (const descriptor of reports) context.report(descriptor);
|
|
232
|
+
};
|
|
233
|
+
const services = context.sourceCode.parserServices;
|
|
234
|
+
if (template && program.templateBody && services?.defineTemplateBodyVisitor) {
|
|
235
|
+
const { "VElement:exit": elementExit, ...listener } = template;
|
|
236
|
+
return services.defineTemplateBodyVisitor({
|
|
237
|
+
...listener,
|
|
238
|
+
"VElement:exit": (node) => {
|
|
239
|
+
elementExit?.(node);
|
|
240
|
+
if (node === program.templateBody) finish();
|
|
241
|
+
}
|
|
242
|
+
}, script);
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
...script,
|
|
246
|
+
"Program:exit": finish
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const WRAPPERS = /* @__PURE__ */ new Set([
|
|
252
|
+
"ChainExpression",
|
|
253
|
+
"TSNonNullExpression",
|
|
254
|
+
"TSAsExpression",
|
|
255
|
+
"TSSatisfiesExpression"
|
|
256
|
+
]);
|
|
257
|
+
function nameOf(node) {
|
|
258
|
+
if (node.type === "Identifier") return node.name;
|
|
259
|
+
return node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" ? node.property.name : void 0;
|
|
260
|
+
}
|
|
261
|
+
function chainRoot(node) {
|
|
262
|
+
let current = node;
|
|
263
|
+
for (;;) if (current.type === "CallExpression") current = current.callee;
|
|
264
|
+
else if (current.type === "MemberExpression") current = current.object;
|
|
265
|
+
else return current.type === "Identifier" ? current : void 0;
|
|
266
|
+
}
|
|
267
|
+
function memberPath(node) {
|
|
268
|
+
if (node.type === "Identifier") return [node.name];
|
|
269
|
+
if (node.type !== "MemberExpression") return [];
|
|
270
|
+
const property = !node.computed && node.property.type === "Identifier" ? node.property.name : "*";
|
|
271
|
+
return [...memberPath(node.object), property];
|
|
272
|
+
}
|
|
273
|
+
function unwrap(node) {
|
|
274
|
+
let current = node;
|
|
275
|
+
while (WRAPPERS.has(current.type)) current = current.expression;
|
|
276
|
+
return current;
|
|
277
|
+
}
|
|
278
|
+
function followPromise(promise) {
|
|
279
|
+
let end = promise;
|
|
280
|
+
for (;;) {
|
|
281
|
+
let member = end.parent;
|
|
282
|
+
while (member && WRAPPERS.has(member.type)) {
|
|
283
|
+
end = member;
|
|
284
|
+
member = end.parent;
|
|
285
|
+
}
|
|
286
|
+
const call = member?.parent;
|
|
287
|
+
if (member?.type !== "MemberExpression" || member.object !== end || member.property.type !== "Identifier" || call?.type !== "CallExpression" || call.callee !== member) return {
|
|
288
|
+
end,
|
|
289
|
+
handled: false
|
|
290
|
+
};
|
|
291
|
+
const method = member.property.name;
|
|
292
|
+
if (method === "catch" || method === "then" && call.arguments.length > 1) return {
|
|
293
|
+
end: call,
|
|
294
|
+
handled: true
|
|
295
|
+
};
|
|
296
|
+
if (method !== "then" && method !== "finally") return {
|
|
297
|
+
end,
|
|
298
|
+
handled: false
|
|
299
|
+
};
|
|
300
|
+
end = call;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const TEST_DIRECTIVES = /* @__PURE__ */ new Set([
|
|
304
|
+
"if",
|
|
305
|
+
"else-if",
|
|
306
|
+
"show"
|
|
307
|
+
]);
|
|
308
|
+
function isReference(node) {
|
|
309
|
+
const { parent } = node;
|
|
310
|
+
if (parent?.type === "MemberExpression") return parent.property !== node || parent.computed;
|
|
311
|
+
return parent?.type !== "Property" || parent.key !== node || parent.computed || parent.shorthand;
|
|
312
|
+
}
|
|
313
|
+
function isNullish(node) {
|
|
314
|
+
return node.type === "Identifier" && node.name === "undefined" || node.type === "Literal" && node.value === null && !("regex" in node);
|
|
315
|
+
}
|
|
316
|
+
function canBeNullish(value) {
|
|
317
|
+
if (isNullish(value)) return true;
|
|
318
|
+
if (value.type === "LogicalExpression" && value.operator === "??") return canBeNullish(value.right);
|
|
319
|
+
if (value.type === "ConditionalExpression") return canBeNullish(value.consequent) || canBeNullish(value.alternate);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
function isTested(node) {
|
|
323
|
+
const parent = node.parent;
|
|
324
|
+
switch (parent?.type) {
|
|
325
|
+
case "IfStatement":
|
|
326
|
+
case "WhileStatement":
|
|
327
|
+
case "ConditionalExpression": return parent.test === node;
|
|
328
|
+
case "LogicalExpression": return parent.left === node;
|
|
329
|
+
case "UnaryExpression": return parent.operator === "!";
|
|
330
|
+
case "VExpressionContainer": return parent.parent?.type === "VAttribute" && TEST_DIRECTIVES.has(parent.parent.key?.name?.name ?? "");
|
|
331
|
+
default: return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const SKIPPED = /* @__PURE__ */ new Set([
|
|
335
|
+
"parent",
|
|
336
|
+
"loc",
|
|
337
|
+
"range",
|
|
338
|
+
"tokens",
|
|
339
|
+
"comments"
|
|
340
|
+
]);
|
|
341
|
+
function children(node) {
|
|
342
|
+
const result = [];
|
|
343
|
+
for (const [key, value] of Object.entries(node)) {
|
|
344
|
+
if (SKIPPED.has(key)) continue;
|
|
345
|
+
for (const child of Array.isArray(value) ? value : [value]) if (typeof child === "object" && child !== null && typeof child.type === "string") result.push(child);
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
function walk(node, enter) {
|
|
350
|
+
if (enter(node) === false) return;
|
|
351
|
+
for (const child of children(node)) walk(child, enter);
|
|
352
|
+
}
|
|
353
|
+
function isFunction(node) {
|
|
354
|
+
return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
|
355
|
+
}
|
|
356
|
+
function walkBody(fn, enter) {
|
|
357
|
+
const visit = (node, caught) => {
|
|
358
|
+
enter(node, caught);
|
|
359
|
+
if (isFunction(node)) return;
|
|
360
|
+
if (node.type === "TryStatement") {
|
|
361
|
+
visit(node.block, caught || Boolean(node.handler));
|
|
362
|
+
if (node.handler) visit(node.handler, caught);
|
|
363
|
+
if (node.finalizer) visit(node.finalizer, caught);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
for (const child of children(node)) visit(child, caught);
|
|
367
|
+
};
|
|
368
|
+
if (fn.body.type === "BlockStatement") for (const child of children(fn.body)) visit(child, false);
|
|
369
|
+
else visit(fn.body, false);
|
|
370
|
+
}
|
|
371
|
+
function uncaughtThrow(fn) {
|
|
372
|
+
let found;
|
|
373
|
+
walkBody(fn, (node, caught) => {
|
|
374
|
+
if (!found && !caught && node.type === "ThrowStatement") found = node;
|
|
375
|
+
});
|
|
376
|
+
return found;
|
|
377
|
+
}
|
|
378
|
+
function isCaught(node) {
|
|
379
|
+
let child = node;
|
|
380
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
381
|
+
if (isFunction(parent)) return false;
|
|
382
|
+
if (parent.type === "TryStatement" && parent.block === child && parent.handler) return true;
|
|
383
|
+
child = parent;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
function canReturnNullish(fn, value) {
|
|
388
|
+
if (fn.body.type !== "BlockStatement") return fn.body !== value && canBeNullish(fn.body);
|
|
389
|
+
let nullish = false;
|
|
390
|
+
walkBody(fn, (node) => {
|
|
391
|
+
nullish ||= node.type === "ReturnStatement" && node.argument !== value && (!node.argument || canBeNullish(node.argument));
|
|
392
|
+
});
|
|
393
|
+
const last = fn.body.body.at(-1);
|
|
394
|
+
return nullish || last?.type !== "ReturnStatement" && last?.type !== "ThrowStatement";
|
|
395
|
+
}
|
|
396
|
+
var mutations_default = defineRule({
|
|
397
|
+
meta: {
|
|
398
|
+
type: "problem",
|
|
399
|
+
docs: { description: "Require error handling where a mutateAsync() promise is awaited or dropped, since a rejected mutation takes down its whole section or block." },
|
|
400
|
+
schema: [],
|
|
401
|
+
messages: {
|
|
402
|
+
unhandled: "If this mutation fails, frontend-core replaces the whole section or block with its \"Retry\" state, unless every caller catches the error. Catch it here with try/catch or .catch().",
|
|
403
|
+
dropped: "Nothing handles this promise, so a failed mutation ends up as an unhandled rejection without any feedback. Catch it with try/catch or .catch()."
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
applies: (file) => file.side === "app",
|
|
407
|
+
create: ({ report, visitTemplate }) => {
|
|
408
|
+
const check = (call, template) => {
|
|
409
|
+
if (nameOf(call.callee) !== "mutateAsync") return;
|
|
410
|
+
const { end, handled } = followPromise(call);
|
|
411
|
+
const { parent } = end;
|
|
412
|
+
if (handled || !parent) return;
|
|
413
|
+
const awaited = parent.type === "AwaitExpression";
|
|
414
|
+
const dropped = parent.type === "ExpressionStatement" || parent.type === "UnaryExpression" && parent.operator === "void";
|
|
415
|
+
if (!awaited && !dropped || awaited && isCaught(parent)) return;
|
|
416
|
+
report({
|
|
417
|
+
node: call.callee.type === "MemberExpression" ? call.callee.property : call.callee,
|
|
418
|
+
messageId: dropped && !template ? "dropped" : "unhandled"
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
visitTemplate({ CallExpression: (node) => check(node, true) });
|
|
422
|
+
return { CallExpression: (node) => check(node, false) };
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
const METHODS = /* @__PURE__ */ new Set(["resolve", "resolveOrThrow"]);
|
|
426
|
+
function destructuredResolvers(declarator) {
|
|
427
|
+
const methods = /* @__PURE__ */ new Map();
|
|
428
|
+
if (declarator.id.type !== "ObjectPattern" || !declarator.init || nameOf(declarator.init) !== "linkResolver") return methods;
|
|
429
|
+
for (const property of declarator.id.properties) {
|
|
430
|
+
const method = property.type === "Property" ? nameOf(property.key) : void 0;
|
|
431
|
+
if (method && METHODS.has(method) && property.type === "Property" && property.value.type === "Identifier") methods.set(property.value.name, method);
|
|
432
|
+
}
|
|
433
|
+
return methods;
|
|
434
|
+
}
|
|
435
|
+
function linkResolverMethod(call, destructured) {
|
|
436
|
+
const { callee } = call;
|
|
437
|
+
if (callee.type === "Identifier") return destructured.get(callee.name);
|
|
438
|
+
const method = callee.type === "MemberExpression" ? nameOf(callee) : void 0;
|
|
439
|
+
return method && METHODS.has(method) && callee.type === "MemberExpression" && nameOf(callee.object) === "linkResolver" ? method : void 0;
|
|
440
|
+
}
|
|
441
|
+
const NOT_PAGES = /^\/(?:\/|api\/|_|app-|\.well-known\/)/;
|
|
442
|
+
function isInternalPath(path) {
|
|
443
|
+
return path?.startsWith("/") === true && !NOT_PAGES.test(path);
|
|
444
|
+
}
|
|
445
|
+
function isHandBuiltPath(node) {
|
|
446
|
+
if (node?.type === "TemplateLiteral") return node.expressions.length > 0 && isInternalPath(node.quasis[0]?.value.cooked);
|
|
447
|
+
return node?.type === "BinaryExpression" && node.operator === "+" && node.left.type === "Literal" && typeof node.left.value === "string" && isInternalPath(node.left.value);
|
|
448
|
+
}
|
|
449
|
+
function isRouter(node) {
|
|
450
|
+
const name = nameOf(node.type === "CallExpression" ? node.callee : node);
|
|
451
|
+
return node.type === "CallExpression" ? name === "useRouter" : name === "router" || name === "$router";
|
|
452
|
+
}
|
|
453
|
+
const parseFile = cachedReader((text) => {
|
|
454
|
+
try {
|
|
455
|
+
return parse$1(text, {
|
|
456
|
+
range: true,
|
|
457
|
+
loc: true
|
|
458
|
+
});
|
|
459
|
+
} catch {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
function isTypeOnly(node) {
|
|
464
|
+
const { importKind, exportKind } = node;
|
|
465
|
+
if (importKind === "type" || exportKind === "type") return true;
|
|
466
|
+
return node.type === "ImportDeclaration" && node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.importKind === "type");
|
|
467
|
+
}
|
|
468
|
+
function findImport(program, local) {
|
|
469
|
+
for (const statement of program.body) {
|
|
470
|
+
const specifier = statement.type === "ImportDeclaration" ? statement.specifiers.find((item) => item.local.name === local) : void 0;
|
|
471
|
+
if (!specifier || statement.type !== "ImportDeclaration" || typeof statement.source.value !== "string") continue;
|
|
472
|
+
const source = statement.source.value;
|
|
473
|
+
if (specifier.type === "ImportSpecifier") return {
|
|
474
|
+
source,
|
|
475
|
+
name: specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value)
|
|
476
|
+
};
|
|
477
|
+
return {
|
|
478
|
+
source,
|
|
479
|
+
name: specifier.type === "ImportDefaultSpecifier" ? "default" : "*"
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function findFunction(statements, name) {
|
|
484
|
+
for (const statement of statements) {
|
|
485
|
+
const declaration = statement.type === "ExportNamedDeclaration" && statement.declaration ? statement.declaration : statement;
|
|
486
|
+
if (declaration.type === "FunctionDeclaration" && declaration.id?.name === name) return declaration;
|
|
487
|
+
if (declaration.type === "VariableDeclaration") {
|
|
488
|
+
for (const declarator of declaration.declarations) if (declarator.id.type === "Identifier" && declarator.id.name === name && declarator.init && isFunction(declarator.init)) return declarator.init;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function findConstant(statements, name) {
|
|
493
|
+
for (const statement of statements) {
|
|
494
|
+
const declaration = statement.type === "ExportNamedDeclaration" && statement.declaration ? statement.declaration : statement;
|
|
495
|
+
if (declaration.type !== "VariableDeclaration" || declaration.kind !== "const") continue;
|
|
496
|
+
const declarator = declaration.declarations.find((item) => item.id.type === "Identifier" && item.id.name === name);
|
|
497
|
+
if (declarator?.init) return declarator.init;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function resolveConstant(name, program, filename) {
|
|
501
|
+
const local = findConstant(program.body, name);
|
|
502
|
+
if (local) return local;
|
|
503
|
+
const imported = findImport(program, name);
|
|
504
|
+
const file = imported && imported.name !== "*" ? resolveModule(filename, imported.source) : void 0;
|
|
505
|
+
const module = file ? parseFile(file) : void 0;
|
|
506
|
+
return module && imported ? findConstant(module.body, imported.name) : void 0;
|
|
507
|
+
}
|
|
508
|
+
function findExport(program, name) {
|
|
509
|
+
for (const statement of program.body) {
|
|
510
|
+
if (statement.type === "ExportDefaultDeclaration" && name === "default") {
|
|
511
|
+
const { declaration } = statement;
|
|
512
|
+
return isFunction(declaration) ? declaration : declaration.type === "Identifier" ? findFunction(program.body, declaration.name) : void 0;
|
|
513
|
+
}
|
|
514
|
+
if (statement.type !== "ExportNamedDeclaration") continue;
|
|
515
|
+
const declared = statement.declaration && findFunction([statement.declaration], name);
|
|
516
|
+
if (declared) return declared;
|
|
517
|
+
const specifier = statement.source ? void 0 : statement.specifiers.find((item) => (item.exported.type === "Identifier" ? item.exported.name : item.exported.value) === name);
|
|
518
|
+
if (specifier?.local.type === "Identifier") return findFunction(program.body, specifier.local.name);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function resolveCallee(call, program, filename) {
|
|
522
|
+
if (call.callee.type !== "Identifier") return;
|
|
523
|
+
const { name } = call.callee;
|
|
524
|
+
const local = findFunction(program.body, name);
|
|
525
|
+
if (local) return {
|
|
526
|
+
node: local,
|
|
527
|
+
file: filename
|
|
528
|
+
};
|
|
529
|
+
const imported = findImport(program, name);
|
|
530
|
+
const file = imported && imported.name !== "*" ? resolveModule(filename, imported.source) : void 0;
|
|
531
|
+
const module = file ? parseFile(file) : void 0;
|
|
532
|
+
const node = module && imported && findExport(module, imported.name);
|
|
533
|
+
return file && node ? {
|
|
534
|
+
node,
|
|
535
|
+
file
|
|
536
|
+
} : void 0;
|
|
537
|
+
}
|
|
538
|
+
function findPropsVariable(program) {
|
|
539
|
+
for (const statement of program.body) {
|
|
540
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
541
|
+
for (const declarator of statement.declarations) if (declarator.id.type === "Identifier" && declarator.init && isDefineProps(declarator.init)) return declarator.id.name;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function isDefineProps(node) {
|
|
545
|
+
if (node.type !== "CallExpression" || node.callee.type !== "Identifier") return false;
|
|
546
|
+
const [first] = node.arguments;
|
|
547
|
+
return node.callee.name === "defineProps" || node.callee.name === "withDefaults" && first !== void 0 && isDefineProps(first);
|
|
548
|
+
}
|
|
549
|
+
function usedProps(node) {
|
|
550
|
+
const { parent } = node;
|
|
551
|
+
if (parent.type === "MemberExpression" && parent.object === node) {
|
|
552
|
+
if (!parent.computed && parent.property.type === "Identifier") return [parent.property.name];
|
|
553
|
+
return parent.property.type === "Literal" && typeof parent.property.value === "string" ? [parent.property.value] : void 0;
|
|
554
|
+
}
|
|
555
|
+
if (parent.type === "VariableDeclarator" && parent.init === node && parent.id.type === "ObjectPattern") {
|
|
556
|
+
const names = parent.id.properties.map((property) => property.type === "Property" && property.key.type === "Identifier" && !property.computed ? property.key.name : void 0);
|
|
557
|
+
return names.every((name) => name !== void 0) ? names : void 0;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function findHolder(node) {
|
|
561
|
+
let value = node;
|
|
562
|
+
let fn;
|
|
563
|
+
const { parent } = node;
|
|
564
|
+
if (parent?.type === "ReturnStatement") {
|
|
565
|
+
let current = parent.parent;
|
|
566
|
+
while (current && !isFunction(current)) current = current.parent;
|
|
567
|
+
fn = current ?? void 0;
|
|
568
|
+
value = current ?? value;
|
|
569
|
+
} else if (parent && isFunction(parent) && parent.body === node) {
|
|
570
|
+
fn = parent;
|
|
571
|
+
value = parent;
|
|
572
|
+
}
|
|
573
|
+
const computed = value.parent?.type === "CallExpression" && value.parent.callee.type === "Identifier" && value.parent.callee.name === "computed" ? value.parent : void 0;
|
|
574
|
+
const declarator = (computed ?? value).parent;
|
|
575
|
+
return declarator?.type === "VariableDeclarator" && declarator.id.type === "Identifier" ? {
|
|
576
|
+
name: declarator.id.name,
|
|
577
|
+
computed: computed !== void 0,
|
|
578
|
+
fn
|
|
579
|
+
} : void 0;
|
|
580
|
+
}
|
|
581
|
+
function importedComponent(program, filename, name) {
|
|
582
|
+
const imported = findImport(program, name);
|
|
583
|
+
return imported?.name === "default" && imported.source.endsWith(".vue") ? resolveModule(filename, imported.source) : void 0;
|
|
584
|
+
}
|
|
585
|
+
function renderedRoots(template) {
|
|
586
|
+
return template.children.filter((child) => child.type === "VElement" && !child.startTag.attributes.some((attribute) => attribute.directive && (attribute.key.name.name === "else" || attribute.key.name.name === "else-if")));
|
|
587
|
+
}
|
|
588
|
+
function findAttribute(element, name) {
|
|
589
|
+
return element.startTag.attributes.find((attribute) => attribute.directive ? attribute.key.name.name === "bind" && attribute.key.argument?.type === "VIdentifier" && attribute.key.argument.name === name : attribute.key.name === name);
|
|
590
|
+
}
|
|
591
|
+
function valueStart(attribute) {
|
|
592
|
+
if (!attribute.directive) return attribute.value?.value;
|
|
593
|
+
const expression = attribute.value?.expression;
|
|
594
|
+
if (expression?.type === "Literal" && typeof expression.value === "string") return expression.value;
|
|
595
|
+
return expression?.type === "TemplateLiteral" ? expression.quasis[0]?.value.cooked ?? void 0 : void 0;
|
|
596
|
+
}
|
|
597
|
+
function rendersTag(element, tag) {
|
|
598
|
+
const is = element.rawName === "component" ? findAttribute(element, "is") : void 0;
|
|
599
|
+
if (element.rawName === tag || is && !is.directive && is.value?.value === tag) return true;
|
|
600
|
+
const expression = is?.directive ? is.value?.expression : void 0;
|
|
601
|
+
let found = false;
|
|
602
|
+
if (expression) walk(expression, (node) => {
|
|
603
|
+
found ||= node.type === "Literal" && node.value === tag;
|
|
604
|
+
});
|
|
605
|
+
return found;
|
|
606
|
+
}
|
|
607
|
+
var anchors_default = defineRule({
|
|
608
|
+
meta: {
|
|
609
|
+
type: "problem",
|
|
610
|
+
docs: { description: "Disallow plain `<a>` tags for internal and resolved links, which reload the page and break Studio's navigation sync." },
|
|
611
|
+
schema: [],
|
|
612
|
+
messages: { anchor: "A plain <a> reloads the whole page and breaks Studio's navigation sync. Use <NuxtLink> for internal links." }
|
|
613
|
+
},
|
|
614
|
+
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
615
|
+
create: ({ report, visitTemplate }) => {
|
|
616
|
+
const destructured = /* @__PURE__ */ new Map();
|
|
617
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
618
|
+
const resolves = (node) => node.type === "CallExpression" && (linkResolverMethod(node, destructured) !== void 0 || nameOf(node.callee) === "useResolvedLink");
|
|
619
|
+
visitTemplate({ VElement: (node) => {
|
|
620
|
+
const href = rendersTag(node, "a") ? findAttribute(node, "href") : void 0;
|
|
621
|
+
const target = findAttribute(node, "target");
|
|
622
|
+
if (!href || findAttribute(node, "download") || target && !target.directive && target.value?.value === "_blank") return;
|
|
623
|
+
const expression = href.directive ? href.value?.expression : void 0;
|
|
624
|
+
if (expression && (resolves(expression) || expression.type === "Identifier" && resolved.has(expression.name)) || isInternalPath(valueStart(href))) report({
|
|
625
|
+
loc: href.loc,
|
|
626
|
+
messageId: "anchor"
|
|
627
|
+
});
|
|
628
|
+
} });
|
|
629
|
+
return {
|
|
630
|
+
VariableDeclarator: (node) => {
|
|
631
|
+
destructuredResolvers(node).forEach((method, local) => destructured.set(local, method));
|
|
632
|
+
},
|
|
633
|
+
CallExpression: (node) => {
|
|
634
|
+
const holder = resolves(node) ? findHolder(node) : void 0;
|
|
635
|
+
if (holder) resolved.add(holder.name);
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
const LINK_NAMES = /* @__PURE__ */ new Set([
|
|
641
|
+
"to",
|
|
642
|
+
"href",
|
|
643
|
+
"link"
|
|
644
|
+
]);
|
|
645
|
+
const ROUTER_METHODS = /* @__PURE__ */ new Set(["push", "replace"]);
|
|
646
|
+
var paths_default = defineRule({
|
|
647
|
+
meta: {
|
|
648
|
+
type: "problem",
|
|
649
|
+
docs: { description: "Disallow hand-built paths to pages, which miss the per-language paths and market prefixes Studio sets." },
|
|
650
|
+
schema: [],
|
|
651
|
+
messages: { path: "Page paths are set per page and language in Studio, and each market adds its own prefix, e.g. /en, so this path can lead to the wrong page. Resolve the link with linkResolver instead." }
|
|
652
|
+
},
|
|
653
|
+
applies: (file) => file.side === "app",
|
|
654
|
+
create: ({ report, visitTemplate }) => {
|
|
655
|
+
visitTemplate({ VAttribute: (node) => {
|
|
656
|
+
const argument = node.directive && node.key.name.name === "bind" ? node.key.argument : null;
|
|
657
|
+
const expression = node.directive ? node.value?.expression : void 0;
|
|
658
|
+
if (argument?.type === "VIdentifier" && LINK_NAMES.has(argument.name) && isHandBuiltPath(expression)) report({
|
|
659
|
+
node: expression,
|
|
660
|
+
messageId: "path"
|
|
661
|
+
});
|
|
662
|
+
} });
|
|
663
|
+
return {
|
|
664
|
+
CallExpression: (node) => {
|
|
665
|
+
const { callee } = node;
|
|
666
|
+
const [target] = node.arguments;
|
|
667
|
+
if ((callee.type === "Identifier" ? callee.name === "navigateTo" : callee.type === "MemberExpression" && ROUTER_METHODS.has(nameOf(callee) ?? "") && isRouter(callee.object)) && isHandBuiltPath(target)) report({
|
|
668
|
+
node: target,
|
|
669
|
+
messageId: "path"
|
|
670
|
+
});
|
|
671
|
+
},
|
|
672
|
+
Property: (node) => {
|
|
673
|
+
if (node.key.type === "Identifier" && LINK_NAMES.has(node.key.name) && isHandBuiltPath(node.value)) report({
|
|
674
|
+
node: node.value,
|
|
675
|
+
messageId: "path"
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
var resolve_default = defineRule({
|
|
682
|
+
meta: {
|
|
683
|
+
type: "problem",
|
|
684
|
+
docs: { description: "Disallow testing the result of linkResolver.resolve(), which falls back to a '#…' string instead of an empty value." },
|
|
685
|
+
schema: [],
|
|
686
|
+
messages: {
|
|
687
|
+
tested: "linkResolver.resolve() returns a '#…' fallback instead of an empty value when it can't resolve a link, so this check doesn't catch broken links. Use resolveOrThrow() (frontend-core 0.42+) in a try/catch to tell them apart.",
|
|
688
|
+
branch: "When it can't resolve the link, linkResolver.resolve() returns a '#…' fallback here, so the checks on {{name}} treat a broken link as a working one. Use resolveOrThrow() (frontend-core 0.42+) in a try/catch and return undefined instead."
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
applies: (file) => file.side === "app",
|
|
692
|
+
create: ({ report, visitTemplate }) => {
|
|
693
|
+
const destructured = /* @__PURE__ */ new Map();
|
|
694
|
+
const calls = [];
|
|
695
|
+
const scriptIdentifiers = [];
|
|
696
|
+
const templateIdentifiers = [];
|
|
697
|
+
visitTemplate({ Identifier: (node) => {
|
|
698
|
+
templateIdentifiers.push(node);
|
|
699
|
+
} });
|
|
700
|
+
return {
|
|
701
|
+
"VariableDeclarator": (node) => {
|
|
702
|
+
destructuredResolvers(node).forEach((method, local) => destructured.set(local, method));
|
|
703
|
+
},
|
|
704
|
+
"CallExpression": (node) => {
|
|
705
|
+
calls.push(node);
|
|
706
|
+
},
|
|
707
|
+
"Identifier": (node) => {
|
|
708
|
+
scriptIdentifiers.push(node);
|
|
709
|
+
},
|
|
710
|
+
"Program:exit": () => {
|
|
711
|
+
const holders = /* @__PURE__ */ new Map();
|
|
712
|
+
const reported = /* @__PURE__ */ new Set();
|
|
713
|
+
for (const call of calls) {
|
|
714
|
+
if (linkResolverMethod(call, destructured) !== "resolve") continue;
|
|
715
|
+
if (isTested(call)) report({
|
|
716
|
+
node: call,
|
|
717
|
+
messageId: "tested"
|
|
718
|
+
});
|
|
719
|
+
const holder = findHolder(call);
|
|
720
|
+
if (holder) holders.set(holder.name, {
|
|
721
|
+
computed: holder.computed,
|
|
722
|
+
branch: holder.fn && canReturnNullish(holder.fn, call) ? call : void 0
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
const check = (identifier, template) => {
|
|
726
|
+
const holder = holders.get(identifier.name);
|
|
727
|
+
const { parent } = identifier;
|
|
728
|
+
if (!holder || !isReference(identifier)) return;
|
|
729
|
+
const readsValue = parent.type === "MemberExpression" && parent.object === identifier && nameOf(parent) === "value";
|
|
730
|
+
const value = holder.computed && !template ? readsValue ? parent : void 0 : identifier;
|
|
731
|
+
if (!value || !isTested(value)) return;
|
|
732
|
+
if (!holder.branch) report({
|
|
733
|
+
node: value,
|
|
734
|
+
messageId: "tested"
|
|
735
|
+
});
|
|
736
|
+
else if (!reported.has(holder.branch)) {
|
|
737
|
+
reported.add(holder.branch);
|
|
738
|
+
report({
|
|
739
|
+
node: holder.branch,
|
|
740
|
+
messageId: "branch",
|
|
741
|
+
data: { name: identifier.name }
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
scriptIdentifiers.forEach((identifier) => check(identifier, false));
|
|
746
|
+
templateIdentifiers.forEach((identifier) => check(identifier, true));
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
function kebabCase(name) {
|
|
752
|
+
return name.replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
753
|
+
}
|
|
754
|
+
function camelCase(name) {
|
|
755
|
+
return name.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() ?? "").replace(/^[A-Z]/, (char) => char.toLowerCase());
|
|
756
|
+
}
|
|
757
|
+
const UI_KIT_BUTTON = /(?:^#ui-kit|@laioutr-core\/ui-kit)\/.*\/(?:Icon|BackgroundAware|Animated)?Button\.vue$/;
|
|
758
|
+
const BUTTONS = [
|
|
759
|
+
"LButton",
|
|
760
|
+
"LIconButton",
|
|
761
|
+
"LBackgroundAwareButton",
|
|
762
|
+
"LAnimatedButton"
|
|
763
|
+
];
|
|
764
|
+
var button_default = defineRule({
|
|
765
|
+
meta: {
|
|
766
|
+
type: "problem",
|
|
767
|
+
fixable: "code",
|
|
768
|
+
docs: { description: "Disallow `type` on the ui-kit buttons, which silently render their `button-type` prop instead." },
|
|
769
|
+
schema: [],
|
|
770
|
+
messages: { buttonType: "<{{tag}}> ignores type and always renders its button-type prop, which defaults to \"button\". Use button-type." }
|
|
771
|
+
},
|
|
772
|
+
applies: (file) => file.path.endsWith(".vue"),
|
|
773
|
+
create: ({ report, visitTemplate }) => {
|
|
774
|
+
const buttons = new Set(BUTTONS.flatMap((name) => [name, kebabCase(name)]));
|
|
775
|
+
visitTemplate({ VElement: (node) => {
|
|
776
|
+
if (!buttons.has(node.rawName)) return;
|
|
777
|
+
for (const attribute of node.startTag.attributes) {
|
|
778
|
+
if (!attribute.directive && attribute.key.rawName === "type") {
|
|
779
|
+
const key = attribute.key;
|
|
780
|
+
report({
|
|
781
|
+
loc: key.loc,
|
|
782
|
+
messageId: "buttonType",
|
|
783
|
+
data: { tag: node.rawName },
|
|
784
|
+
fix: (fixer) => fixer.replaceTextRange(key.range, "button-type")
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
const argument = attribute.directive ? attribute.key.argument : null;
|
|
788
|
+
if (attribute.directive && attribute.key.name.name === "bind" && argument?.type === "VIdentifier" && argument.rawName === "type") report({
|
|
789
|
+
loc: argument.loc,
|
|
790
|
+
messageId: "buttonType",
|
|
791
|
+
data: { tag: node.rawName },
|
|
792
|
+
fix: (fixer) => fixer.replaceTextRange(argument.range, "button-type")
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
} });
|
|
796
|
+
return { ImportDeclaration: (node) => {
|
|
797
|
+
if (typeof node.source.value !== "string" || !UI_KIT_BUTTON.test(node.source.value)) return;
|
|
798
|
+
for (const specifier of node.specifiers) if (specifier.type === "ImportDefaultSpecifier") {
|
|
799
|
+
const kebab = kebabCase(specifier.local.name);
|
|
800
|
+
buttons.add(specifier.local.name);
|
|
801
|
+
if (kebab.includes("-")) buttons.add(kebab);
|
|
802
|
+
}
|
|
803
|
+
} };
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
function readMiddleware(call) {
|
|
807
|
+
const { callee } = call;
|
|
808
|
+
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") return;
|
|
809
|
+
const method = callee.property.name;
|
|
810
|
+
if (method !== "extendRequest" && (method !== "use" || !/^define[A-Z]/.test(chainRoot(callee.object)?.name ?? ""))) return;
|
|
811
|
+
const [callback] = call.arguments;
|
|
812
|
+
return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? {
|
|
813
|
+
method,
|
|
814
|
+
callback
|
|
815
|
+
} : void 0;
|
|
816
|
+
}
|
|
817
|
+
function enclosingMiddleware(node) {
|
|
818
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
819
|
+
const middleware = isFunction(parent) && parent.parent?.type === "CallExpression" ? readMiddleware(parent.parent) : void 0;
|
|
820
|
+
if (middleware?.callback === parent) return middleware;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const COOKIE_WRITES = /* @__PURE__ */ new Set([
|
|
824
|
+
"setCookie",
|
|
825
|
+
"deleteCookie",
|
|
826
|
+
"setManagedCookie",
|
|
827
|
+
"deleteManagedCookie"
|
|
828
|
+
]);
|
|
829
|
+
const HEADER_WRITES = /* @__PURE__ */ new Set([
|
|
830
|
+
"setHeader",
|
|
831
|
+
"setHeaders",
|
|
832
|
+
"appendHeader",
|
|
833
|
+
"appendHeaders",
|
|
834
|
+
"setResponseHeader",
|
|
835
|
+
"setResponseHeaders",
|
|
836
|
+
"appendResponseHeader",
|
|
837
|
+
"appendResponseHeaders",
|
|
838
|
+
"setResponseStatus",
|
|
839
|
+
"sendRedirect"
|
|
840
|
+
]);
|
|
841
|
+
function responseWrite(call) {
|
|
842
|
+
const name = call.callee.type === "Identifier" ? call.callee.name : "";
|
|
843
|
+
if (COOKIE_WRITES.has(name)) return {
|
|
844
|
+
name,
|
|
845
|
+
kind: "cookie"
|
|
846
|
+
};
|
|
847
|
+
return HEADER_WRITES.has(name) ? {
|
|
848
|
+
name,
|
|
849
|
+
kind: "header"
|
|
850
|
+
} : void 0;
|
|
851
|
+
}
|
|
852
|
+
const STREAMED = /* @__PURE__ */ new Set([
|
|
853
|
+
"query",
|
|
854
|
+
"link",
|
|
855
|
+
"resolver"
|
|
856
|
+
]);
|
|
857
|
+
const MANAGED = {
|
|
858
|
+
setCookie: "setManagedCookie",
|
|
859
|
+
deleteCookie: "deleteManagedCookie"
|
|
860
|
+
};
|
|
861
|
+
var cookies_default = defineRule({
|
|
862
|
+
meta: {
|
|
863
|
+
type: "problem",
|
|
864
|
+
docs: { description: "Write cookies and headers only where orchestr allows it: in extendRequest() and action handlers, with frontend-core's managed cookie functions." },
|
|
865
|
+
schema: [],
|
|
866
|
+
messages: {
|
|
867
|
+
streamed: "Orchestr may have sent the response headers by the time {{name}}() runs here, so the browser never gets it. Write cookies and headers in extendRequest() or an action handler.",
|
|
868
|
+
managed: "Use {{managed}}() instead. It applies Laioutr's cookie policy, which the Studio preview needs (SameSite=None and Partitioned), and {{name}}() skips it."
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
applies: (file) => file.side === "server",
|
|
872
|
+
create: ({ file, report }) => ({ CallExpression: (node) => {
|
|
873
|
+
const write = responseWrite(node);
|
|
874
|
+
if (!write) return;
|
|
875
|
+
const method = enclosingMiddleware(node)?.method;
|
|
876
|
+
const managed = MANAGED[write.name];
|
|
877
|
+
if (method === "use" || !method && STREAMED.has(file.handler)) report({
|
|
878
|
+
node,
|
|
879
|
+
messageId: "streamed",
|
|
880
|
+
data: { name: write.name }
|
|
881
|
+
});
|
|
882
|
+
else if (managed && (method === "extendRequest" || !method && file.handler === "action")) report({
|
|
883
|
+
node,
|
|
884
|
+
messageId: "managed",
|
|
885
|
+
data: {
|
|
886
|
+
name: write.name,
|
|
887
|
+
managed
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
} })
|
|
891
|
+
});
|
|
892
|
+
const FILE_START = {
|
|
893
|
+
start: {
|
|
894
|
+
line: 1,
|
|
895
|
+
column: 0
|
|
896
|
+
},
|
|
897
|
+
end: {
|
|
898
|
+
line: 1,
|
|
899
|
+
column: 0
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
var files_default$1 = defineRule({
|
|
903
|
+
meta: {
|
|
904
|
+
type: "problem",
|
|
905
|
+
docs: { description: "Keep orchestr/ to handler files, since Laioutr loads every file in it as a Nitro plugin." },
|
|
906
|
+
schema: [],
|
|
907
|
+
messages: { notAHandler: "Laioutr loads every file in orchestr/ as a Nitro plugin, so a helper here runs at startup or breaks the build. Move it to server/utils/, and name handlers *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts or *.page-index.ts." }
|
|
908
|
+
},
|
|
909
|
+
applies: (file) => file.kind === "orchestr-file",
|
|
910
|
+
create: ({ report }) => ({ Program: () => {
|
|
911
|
+
report({
|
|
912
|
+
loc: FILE_START,
|
|
913
|
+
messageId: "notAHandler"
|
|
914
|
+
});
|
|
915
|
+
} })
|
|
916
|
+
});
|
|
917
|
+
var exports_default = defineRule({
|
|
918
|
+
meta: {
|
|
919
|
+
type: "problem",
|
|
920
|
+
docs: { description: "Require handler files to export their handler as default." },
|
|
921
|
+
schema: [],
|
|
922
|
+
messages: { missingDefault: "Laioutr loads every file in orchestr/ as a Nitro plugin from its default export, so without one the build fails. Export the handler as default." }
|
|
923
|
+
},
|
|
924
|
+
applies: (file) => file.kind === "handler",
|
|
925
|
+
create: ({ report }) => {
|
|
926
|
+
let hasDefault = false;
|
|
927
|
+
return {
|
|
928
|
+
"ExportDefaultDeclaration": () => {
|
|
929
|
+
hasDefault = true;
|
|
930
|
+
},
|
|
931
|
+
"ExportSpecifier": (node) => {
|
|
932
|
+
hasDefault ||= (node.exported.type === "Identifier" ? node.exported.name : node.exported.value) === "default";
|
|
933
|
+
},
|
|
934
|
+
"Program:exit": () => {
|
|
935
|
+
if (!hasDefault) report({
|
|
936
|
+
loc: FILE_START,
|
|
937
|
+
messageId: "missingDefault"
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
const MIDDLEWARE = /* @__PURE__ */ new Set([
|
|
944
|
+
"defineOrchestr",
|
|
945
|
+
"defineEventHandler",
|
|
946
|
+
"eventHandler",
|
|
947
|
+
"defineRequestMiddleware",
|
|
948
|
+
"fromNodeMiddleware"
|
|
949
|
+
]);
|
|
950
|
+
var files_default = defineRule({
|
|
951
|
+
meta: {
|
|
952
|
+
type: "suggestion",
|
|
953
|
+
docs: { description: "Keep server/middleware/ to orchestr and Nitro middleware." },
|
|
954
|
+
schema: [],
|
|
955
|
+
messages: { helper: "server/middleware/ holds orchestr and Nitro middleware. Move helpers like this to server/utils/." }
|
|
956
|
+
},
|
|
957
|
+
applies: (file) => file.kind === "middleware",
|
|
958
|
+
create: ({ report }) => {
|
|
959
|
+
let middleware = false;
|
|
151
960
|
return {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
961
|
+
"Identifier": (node) => {
|
|
962
|
+
middleware ||= MIDDLEWARE.has(node.name);
|
|
963
|
+
},
|
|
964
|
+
"CallExpression": (node) => {
|
|
965
|
+
middleware ||= readMiddleware(node) !== void 0;
|
|
966
|
+
},
|
|
967
|
+
"Program:exit": () => {
|
|
968
|
+
if (!middleware) report({
|
|
969
|
+
loc: FILE_START,
|
|
970
|
+
messageId: "helper"
|
|
971
|
+
});
|
|
972
|
+
}
|
|
155
973
|
};
|
|
156
974
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
side,
|
|
160
|
-
kind,
|
|
161
|
-
domain: kind === "server-util" && nested ? rest[0] : void 0
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function kebabCase(name) {
|
|
165
|
-
return name.replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
166
|
-
}
|
|
167
|
-
function prefixName(name, prefix) {
|
|
168
|
-
return `${prefix}${name.endsWith(prefix) ? name.slice(0, -prefix.length) : name}`;
|
|
169
|
-
}
|
|
170
|
-
const UI_KIT_BUTTON = /(?:^#ui-kit|@laioutr-core\/ui-kit)\/.*\/Button\.vue$/;
|
|
171
|
-
const button = {
|
|
975
|
+
});
|
|
976
|
+
var throws_default = defineRule({
|
|
172
977
|
meta: {
|
|
173
978
|
type: "problem",
|
|
174
|
-
|
|
175
|
-
docs: { description: "Disallow `type` on the ui-kit button, which silently renders its `button-type` prop instead." },
|
|
979
|
+
docs: { description: "Disallow throwing in extendRequest(), which runs before every query, so a throw takes down every page." },
|
|
176
980
|
schema: [],
|
|
177
|
-
messages: {
|
|
981
|
+
messages: {
|
|
982
|
+
throws: "extendRequest() runs before every query, so this throw takes down every page. Return a stand-in instead, e.g. a client that fails only when it's used.",
|
|
983
|
+
throwsInCall: "{{name}}() can throw ({{location}}). extendRequest() runs before every query, so that takes down every page. Catch it here or return a stand-in."
|
|
984
|
+
}
|
|
178
985
|
},
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const key = attribute.key;
|
|
192
|
-
reporter.report({
|
|
193
|
-
loc: key.loc,
|
|
194
|
-
messageId: "buttonType",
|
|
195
|
-
fix: (fixer) => fixer.replaceTextRange(key.range, "button-type")
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
const argument = attribute.directive ? attribute.key.argument : null;
|
|
199
|
-
if (attribute.directive && attribute.key.name.name === "bind" && argument?.type === "VIdentifier" && argument.rawName === "type") reporter.report({
|
|
200
|
-
loc: argument.loc,
|
|
201
|
-
messageId: "buttonType",
|
|
202
|
-
fix: (fixer) => fixer.replaceTextRange(argument.range, "button-type")
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
"VElement:exit": (node) => {
|
|
207
|
-
if (node.parent.type === "VDocumentFragment") reporter.flush();
|
|
986
|
+
applies: (file) => file.side === "server",
|
|
987
|
+
create: ({ context, report }) => ({ CallExpression: (node) => {
|
|
988
|
+
const middleware = readMiddleware(node);
|
|
989
|
+
if (middleware?.method !== "extendRequest") return;
|
|
990
|
+
walkBody(middleware.callback, (child, caught) => {
|
|
991
|
+
if (caught) return;
|
|
992
|
+
if (child.type === "ThrowStatement") {
|
|
993
|
+
report({
|
|
994
|
+
node: child,
|
|
995
|
+
messageId: "throws"
|
|
996
|
+
});
|
|
997
|
+
return;
|
|
208
998
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
999
|
+
const callee = child.type === "CallExpression" ? resolveCallee(child, context.sourceCode.ast, context.filename) : void 0;
|
|
1000
|
+
const thrown = callee && uncaughtThrow(callee.node);
|
|
1001
|
+
if (callee && thrown && child.type === "CallExpression") {
|
|
1002
|
+
const line = thrown.loc?.start.line;
|
|
1003
|
+
const location = callee.file === context.filename ? `line ${line}` : `${relative(dirname(context.filename), callee.file)}:${line}`;
|
|
1004
|
+
report({
|
|
1005
|
+
node: child.callee,
|
|
1006
|
+
messageId: "throwsInCall",
|
|
1007
|
+
data: {
|
|
1008
|
+
name: child.callee.name,
|
|
1009
|
+
location
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
215
1012
|
}
|
|
216
|
-
}
|
|
1013
|
+
});
|
|
1014
|
+
} })
|
|
1015
|
+
});
|
|
1016
|
+
const PACKAGE = "@laioutr-core/canonical-types";
|
|
1017
|
+
const TOKEN_ID = /define\w*Token\(\s*["'`]([a-z0-9-]+)\//g;
|
|
1018
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1019
|
+
function canonicalNamespaces(root) {
|
|
1020
|
+
let namespaces = cache.get(root);
|
|
1021
|
+
if (!namespaces) {
|
|
1022
|
+
const frontendCore = findPackage(root, "@laioutr-core/frontend-core");
|
|
1023
|
+
const folder = findPackage(root, PACKAGE) ?? (frontendCore && findPackage(frontendCore, PACKAGE));
|
|
1024
|
+
if (!folder) throw new Error(`larrylint can't find ${PACKAGE} from ${root}, which it reads the canonical token namespaces from. Install the app's dependencies first.`);
|
|
1025
|
+
namespaces = /* @__PURE__ */ new Set();
|
|
1026
|
+
for (const file of readdirSync(join(folder, "dist"), {
|
|
1027
|
+
recursive: true,
|
|
1028
|
+
encoding: "utf8"
|
|
1029
|
+
}).filter((file) => /\.[cm]?js$/.test(file))) for (const [, namespace] of readFileSync(join(folder, "dist", file), "utf8").matchAll(TOKEN_ID)) namespaces.add(namespace);
|
|
1030
|
+
cache.set(root, namespaces);
|
|
217
1031
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
1032
|
+
return namespaces;
|
|
1033
|
+
}
|
|
1034
|
+
var namespaces_default = defineRule({
|
|
1035
|
+
meta: {
|
|
1036
|
+
type: "problem",
|
|
1037
|
+
docs: { description: "Keep your own token ids out of the namespaces of Laioutr's canonical types." },
|
|
1038
|
+
schema: [],
|
|
1039
|
+
messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types, and a later canonical-types release or another app can take this id. Token metadata is last-write-wins, so put your own tokens under your app's namespace." }
|
|
223
1040
|
},
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
1041
|
+
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1042
|
+
create: ({ file, report }) => ({ CallExpression: (node) => {
|
|
1043
|
+
const [id] = node.arguments;
|
|
1044
|
+
if (node.callee.type !== "Identifier" || !/^define\w*Token$/.test(node.callee.name) || id?.type !== "Literal" || typeof id.value !== "string" || !id.value.includes("/")) return;
|
|
1045
|
+
const namespace = id.value.slice(0, id.value.indexOf("/"));
|
|
1046
|
+
if (canonicalNamespaces(file.root).has(namespace)) report({
|
|
1047
|
+
node: id,
|
|
1048
|
+
messageId: "canonical",
|
|
1049
|
+
data: { namespace }
|
|
1050
|
+
});
|
|
1051
|
+
} })
|
|
1052
|
+
});
|
|
229
1053
|
function findProperty(object, key) {
|
|
230
1054
|
for (const entry of object.properties) if (entry.type === "Property" && !entry.computed && (entry.key.type === "Identifier" && entry.key.name === key || entry.key.type === "Literal" && entry.key.value === key)) return entry.value;
|
|
231
1055
|
}
|
|
@@ -243,56 +1067,144 @@ function objectElements(node) {
|
|
|
243
1067
|
const DEFINERS = {
|
|
244
1068
|
defineSection: {
|
|
245
1069
|
kind: "section",
|
|
246
|
-
folder: "sections"
|
|
247
|
-
prefix: "Section"
|
|
1070
|
+
folder: "sections"
|
|
248
1071
|
},
|
|
249
1072
|
defineBlock: {
|
|
250
1073
|
kind: "block",
|
|
251
|
-
folder: "blocks"
|
|
252
|
-
prefix: "Block"
|
|
1074
|
+
folder: "blocks"
|
|
253
1075
|
}
|
|
254
1076
|
};
|
|
255
|
-
const
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
"
|
|
1077
|
+
const PICKERS = /* @__PURE__ */ new Set([
|
|
1078
|
+
"select",
|
|
1079
|
+
"radio",
|
|
1080
|
+
"toggle_button"
|
|
1081
|
+
]);
|
|
1082
|
+
const TEXTS$1 = /* @__PURE__ */ new Set([
|
|
1083
|
+
"text",
|
|
1084
|
+
"textarea",
|
|
1085
|
+
"secret"
|
|
264
1086
|
]);
|
|
265
|
-
|
|
1087
|
+
function readDefinition(call) {
|
|
1088
|
+
if (call.callee.type !== "Identifier" || !Object.hasOwn(DEFINERS, call.callee.name)) return;
|
|
1089
|
+
const [options] = call.arguments;
|
|
1090
|
+
const inline = options?.type === "ObjectExpression" ? options : void 0;
|
|
1091
|
+
return {
|
|
1092
|
+
definer: call.callee.name,
|
|
1093
|
+
call,
|
|
1094
|
+
options: inline,
|
|
1095
|
+
component: inline && findStringProperty(inline, "component")
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function readGroups(owner) {
|
|
1099
|
+
const node = "call" in owner ? owner.options : owner.node;
|
|
1100
|
+
return objectElements(node && findProperty(node, "schema")).map((group) => ({
|
|
1101
|
+
node: group,
|
|
1102
|
+
fields: objectElements(findProperty(group, "fields")).map(readField)
|
|
1103
|
+
}));
|
|
1104
|
+
}
|
|
1105
|
+
function readField(node) {
|
|
1106
|
+
return {
|
|
1107
|
+
node,
|
|
1108
|
+
name: findStringProperty(node, "name"),
|
|
1109
|
+
type: findStringProperty(node, "type")?.value,
|
|
1110
|
+
as: findStringProperty(node, "as")?.value
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
function readFields(definition) {
|
|
1114
|
+
return readGroups(definition).flatMap((group) => group.fields);
|
|
1115
|
+
}
|
|
1116
|
+
function readAllFields(definition) {
|
|
1117
|
+
const fields = [];
|
|
1118
|
+
const visit = (field) => {
|
|
1119
|
+
fields.push(field);
|
|
1120
|
+
readGroups(field).flatMap((group) => group.fields).forEach(visit);
|
|
1121
|
+
};
|
|
1122
|
+
readFields(definition).forEach(visit);
|
|
1123
|
+
return fields;
|
|
1124
|
+
}
|
|
1125
|
+
function conditionFields(definition) {
|
|
1126
|
+
const names = /* @__PURE__ */ new Set();
|
|
1127
|
+
for (const field of readAllFields(definition)) {
|
|
1128
|
+
const condition = findProperty(field.node, "if");
|
|
1129
|
+
if (condition) walk(condition, (node) => {
|
|
1130
|
+
const [operator, name] = node.type === "ArrayExpression" ? node.elements : [];
|
|
1131
|
+
if (operator?.type === "Literal" && operator.value === "get" && name?.type === "Literal" && typeof name.value === "string") names.add(name.value.split(".")[0]);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
return names;
|
|
1135
|
+
}
|
|
1136
|
+
function expectedDefiner(kind) {
|
|
1137
|
+
return Object.keys(DEFINERS).find((definer) => DEFINERS[definer].kind === kind);
|
|
1138
|
+
}
|
|
1139
|
+
function fillValue(field, program, filename) {
|
|
1140
|
+
if (field.type === "checkbox") return { value: field.as === "visibility" };
|
|
1141
|
+
if (TEXTS$1.has(field.type ?? "")) return { value: "" };
|
|
1142
|
+
if (field.type === "content_alignment") {
|
|
1143
|
+
const axis = findStringProperty(field.node, "axis")?.value;
|
|
1144
|
+
return { value: !axis || axis === "both" ? "center-center" : "center" };
|
|
1145
|
+
}
|
|
1146
|
+
if (!PICKERS.has(field.type ?? "")) return;
|
|
1147
|
+
const property = findProperty(field.node, "options");
|
|
1148
|
+
const declared = property?.type === "Identifier" ? resolveConstant(property.name, program, filename) : property;
|
|
1149
|
+
let options = declared && unwrap(declared);
|
|
1150
|
+
if (options?.type === "CallExpression") options = options.arguments[0];
|
|
1151
|
+
const first = options?.type === "ArrayExpression" ? options.elements[0] : void 0;
|
|
1152
|
+
const value = first?.type === "ObjectExpression" ? findProperty(first, "value") : first;
|
|
1153
|
+
return value?.type === "Literal" ? { value: value.value } : void 0;
|
|
1154
|
+
}
|
|
1155
|
+
var description_default = defineRule({
|
|
1156
|
+
meta: {
|
|
1157
|
+
type: "suggestion",
|
|
1158
|
+
docs: { description: "Require a studio.description on section and block definitions." },
|
|
1159
|
+
schema: [],
|
|
1160
|
+
messages: { description: "Studio shows studio.description in its section picker, and AI agents read it through Laioutr's MCP server. Add one." }
|
|
1161
|
+
},
|
|
1162
|
+
applies: (file) => file.side === "app",
|
|
1163
|
+
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1164
|
+
const options = readDefinition(node)?.options;
|
|
1165
|
+
if (!options) return;
|
|
1166
|
+
const studio = findProperty(options, "studio");
|
|
1167
|
+
if (studio && studio.type !== "ObjectExpression") return;
|
|
1168
|
+
const description = studio && findProperty(studio, "description");
|
|
1169
|
+
if (!description || description.type === "Literal" && String(description.value ?? "").trim() === "") report({
|
|
1170
|
+
node: node.callee,
|
|
1171
|
+
messageId: "description"
|
|
1172
|
+
});
|
|
1173
|
+
} })
|
|
1174
|
+
});
|
|
1175
|
+
var folder_default = defineRule({
|
|
266
1176
|
meta: {
|
|
267
1177
|
type: "problem",
|
|
268
|
-
docs: { description: "Keep defineSection() in app/sections/
|
|
1178
|
+
docs: { description: "Keep defineSection() in app/sections/ and defineBlock() in app/blocks/, and every .vue there defined." },
|
|
269
1179
|
schema: [],
|
|
270
1180
|
messages: {
|
|
271
|
-
notDefined: "Laioutr
|
|
272
|
-
wrongFolder: "{{definer}}() belongs in app/{{folder}}/."
|
|
273
|
-
prefix: "Name {{kind}}s {{prefix}}*.vue, e.g. {{suggestion}}.vue.",
|
|
274
|
-
componentName: "component: '{{actual}}' must match the file name '{{expected}}'.",
|
|
275
|
-
forbiddenFieldName: "Vue consumes '{{name}}' before it reaches the component, so this field never arrives as a prop. Pick another name, e.g. variant for a style selector."
|
|
1181
|
+
notDefined: "Laioutr loads every .vue in {{folder}}/ as a {{kind}}, but this file has no {{definer}}(), so frontend-core warns about it. Move it to components/.",
|
|
1182
|
+
wrongFolder: "{{definer}}() belongs in app/{{folder}}/."
|
|
276
1183
|
}
|
|
277
1184
|
},
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const reporter = createReporter(context, file);
|
|
282
|
-
const name = basename(file.path).replace(/\.[^.]+$/, "");
|
|
283
|
-
const calls = [];
|
|
1185
|
+
applies: (file) => file.side === "app",
|
|
1186
|
+
create: ({ file, report }) => {
|
|
1187
|
+
const definers = /* @__PURE__ */ new Set();
|
|
284
1188
|
return {
|
|
285
1189
|
"CallExpression": (node) => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1190
|
+
const definition = readDefinition(node);
|
|
1191
|
+
if (!definition) return;
|
|
1192
|
+
const { kind, folder } = DEFINERS[definition.definer];
|
|
1193
|
+
definers.add(definition.definer);
|
|
1194
|
+
if (file.kind !== kind && file.kind !== "override") report({
|
|
1195
|
+
node: node.callee,
|
|
1196
|
+
messageId: "wrongFolder",
|
|
1197
|
+
data: {
|
|
1198
|
+
definer: definition.definer,
|
|
1199
|
+
folder
|
|
1200
|
+
}
|
|
289
1201
|
});
|
|
290
1202
|
},
|
|
291
1203
|
"Program:exit": () => {
|
|
292
|
-
const expected =
|
|
293
|
-
if (expected && file.path.endsWith(".vue") && !
|
|
1204
|
+
const expected = expectedDefiner(file.kind);
|
|
1205
|
+
if (expected && file.path.endsWith(".vue") && !definers.has(expected)) {
|
|
294
1206
|
const { kind, folder } = DEFINERS[expected];
|
|
295
|
-
|
|
1207
|
+
report({
|
|
296
1208
|
loc: FILE_START,
|
|
297
1209
|
messageId: "notDefined",
|
|
298
1210
|
data: {
|
|
@@ -302,115 +1214,471 @@ const definition = {
|
|
|
302
1214
|
}
|
|
303
1215
|
});
|
|
304
1216
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
var name_default = defineRule({
|
|
1222
|
+
meta: {
|
|
1223
|
+
type: "problem",
|
|
1224
|
+
docs: { description: "Require the `component` of a section or block definition to match its file name." },
|
|
1225
|
+
schema: [],
|
|
1226
|
+
messages: { componentName: "component: '{{actual}}' must match the file name '{{expected}}'." }
|
|
1227
|
+
},
|
|
1228
|
+
applies: (file) => file.side === "app",
|
|
1229
|
+
create: ({ file, report }) => ({ CallExpression: (node) => {
|
|
1230
|
+
const component = readDefinition(node)?.component;
|
|
1231
|
+
const { name } = parse(file.path);
|
|
1232
|
+
if (component && component.value !== name) report({
|
|
1233
|
+
node: component.node,
|
|
1234
|
+
messageId: "componentName",
|
|
1235
|
+
data: {
|
|
1236
|
+
actual: component.value,
|
|
1237
|
+
expected: name
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
} })
|
|
1241
|
+
});
|
|
1242
|
+
const TEXTS = /* @__PURE__ */ new Set([
|
|
1243
|
+
"text",
|
|
1244
|
+
"textarea",
|
|
1245
|
+
"secret"
|
|
1246
|
+
]);
|
|
1247
|
+
var fallbacks_default = defineRule({
|
|
1248
|
+
meta: {
|
|
1249
|
+
type: "problem",
|
|
1250
|
+
docs: { description: "Disallow ?? fallbacks that never apply, because frontend-core fills unset fields: pickers with their first option, checkboxes with false and text with ''." },
|
|
1251
|
+
schema: [],
|
|
1252
|
+
messages: {
|
|
1253
|
+
picker: "frontend-core fills an unset {{type}} field with its first option, {{fill}}, so this fallback never applies. Make {{fallback}} the first option if it should be the default.",
|
|
1254
|
+
checkbox: "frontend-core fills an unset checkbox with false, so this fallback never applies. Name the field so that unchecked is the default.",
|
|
1255
|
+
text: "frontend-core fills an unset {{type}} field with '', so ?? never falls back. Use || if an empty field should fall back."
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1259
|
+
create: ({ context, report, visitTemplate }) => {
|
|
1260
|
+
const program = context.sourceCode.ast;
|
|
1261
|
+
const propsVariable = findPropsVariable(program);
|
|
1262
|
+
const fills = /* @__PURE__ */ new Map();
|
|
1263
|
+
const fallbacks = [];
|
|
1264
|
+
const propOf = (node, template) => {
|
|
1265
|
+
if (template && node.type === "Identifier") return node.name;
|
|
1266
|
+
const props = node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.object.type === "Identifier" ? node.object.name : void 0;
|
|
1267
|
+
return props && (props === propsVariable || template && props === "$props") && node.type === "MemberExpression" && node.property.type === "Identifier" ? node.property.name : void 0;
|
|
1268
|
+
};
|
|
1269
|
+
const collect = (template) => ({ LogicalExpression: (node) => {
|
|
1270
|
+
const prop = node.operator === "??" ? propOf(node.left, template) : void 0;
|
|
1271
|
+
if (prop && !isNullish(node.right)) fallbacks.push({
|
|
1272
|
+
node,
|
|
1273
|
+
prop
|
|
1274
|
+
});
|
|
1275
|
+
} });
|
|
1276
|
+
visitTemplate(collect(true));
|
|
1277
|
+
return {
|
|
1278
|
+
...collect(false),
|
|
1279
|
+
"CallExpression": (node) => {
|
|
1280
|
+
const definition = readDefinition(node);
|
|
1281
|
+
for (const field of definition ? readFields(definition) : []) {
|
|
1282
|
+
const fill = field.name && field.type && !field.as ? fillValue(field, program, context.filename) : void 0;
|
|
1283
|
+
if (fill) fills.set(field.name.value, {
|
|
1284
|
+
type: field.type,
|
|
1285
|
+
value: fill.value
|
|
323
1286
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
"Program:exit": () => {
|
|
1290
|
+
for (const { node, prop } of fallbacks) {
|
|
1291
|
+
const field = fills.get(prop);
|
|
1292
|
+
if (!field || node.right.type === "Literal" && node.right.value === field.value) continue;
|
|
1293
|
+
const messageId = field.type === "checkbox" ? "checkbox" : TEXTS.has(field.type) ? "text" : "picker";
|
|
1294
|
+
const fill = typeof field.value === "string" ? `'${field.value}'` : String(field.value);
|
|
1295
|
+
report({
|
|
1296
|
+
node,
|
|
1297
|
+
messageId,
|
|
330
1298
|
data: {
|
|
331
|
-
|
|
332
|
-
|
|
1299
|
+
type: field.type,
|
|
1300
|
+
fill,
|
|
1301
|
+
fallback: context.sourceCode.getText(node.right)
|
|
333
1302
|
}
|
|
334
1303
|
});
|
|
335
|
-
for (const group of objectElements(findProperty(options, "schema"))) for (const field of objectElements(findProperty(group, "fields"))) {
|
|
336
|
-
const fieldName = findStringProperty(field, "name");
|
|
337
|
-
if (fieldName && FORBIDDEN_FIELD_NAMES.has(fieldName.value)) reporter.report({
|
|
338
|
-
node: fieldName.node,
|
|
339
|
-
messageId: "forbiddenFieldName",
|
|
340
|
-
data: { name: fieldName.value }
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
1304
|
}
|
|
344
|
-
reporter.flush();
|
|
345
1305
|
}
|
|
346
1306
|
};
|
|
347
1307
|
}
|
|
348
|
-
};
|
|
349
|
-
const
|
|
1308
|
+
});
|
|
1309
|
+
const PROP_READ = /\.props\??\.([A-Z_$][\w$]*)|\.props\??\.?\[\s*['"]([^'"]+)['"]\s*\]|['"]([^'"]+)['"]\s+in\s+[\w$.?]+\.props\b/gi;
|
|
1310
|
+
const PROPS_ALIAS = /\b(?:const|let|var)\s+([\w$]+)\s*=\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/g;
|
|
1311
|
+
const PROPS_DESTRUCTURING = /\{([^{}]*)\}\s*=\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/g;
|
|
1312
|
+
const PROPS_SPREAD = /\.\.\.\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/;
|
|
1313
|
+
const readSlotProps = cachedReader((text) => {
|
|
1314
|
+
const names = [...text.matchAll(PROP_READ)].map((match) => match[1] ?? match[2] ?? match[3]);
|
|
1315
|
+
let all = PROPS_SPREAD.test(text);
|
|
1316
|
+
for (const [, alias] of text.matchAll(PROPS_ALIAS)) {
|
|
1317
|
+
const name = alias.replaceAll("$", "\\$");
|
|
1318
|
+
const reads = new RegExp(`\\b${name}\\??\\.([A-Z_$][\\w$]*)|\\b${name}\\??\\.?\\[\\s*['"]([^'"]+)['"]\\s*\\]`, "gi");
|
|
1319
|
+
for (const match of text.matchAll(reads)) names.push(match[1] ?? match[2]);
|
|
1320
|
+
}
|
|
1321
|
+
for (const [, list] of text.matchAll(PROPS_DESTRUCTURING)) for (const entry of list.split(",").map((item) => item.trim()).filter(Boolean)) {
|
|
1322
|
+
all ||= entry.startsWith("...");
|
|
1323
|
+
names.push(entry.split(/[:=]/)[0].trim());
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
names,
|
|
1327
|
+
all
|
|
1328
|
+
};
|
|
1329
|
+
});
|
|
1330
|
+
function slotProps(root) {
|
|
1331
|
+
const folder = join(root, "src/runtime/app");
|
|
1332
|
+
const result = {
|
|
1333
|
+
names: /* @__PURE__ */ new Set(),
|
|
1334
|
+
all: false
|
|
1335
|
+
};
|
|
1336
|
+
let files;
|
|
1337
|
+
try {
|
|
1338
|
+
files = readdirSync(folder, {
|
|
1339
|
+
recursive: true,
|
|
1340
|
+
encoding: "utf8"
|
|
1341
|
+
}).filter((file) => /\.(?:vue|[cm]?[jt]s)$/.test(file));
|
|
1342
|
+
} catch {
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
for (const file of files) {
|
|
1346
|
+
const read = readSlotProps(join(folder, file));
|
|
1347
|
+
read?.names.forEach((name) => result.names.add(name));
|
|
1348
|
+
result.all ||= read?.all ?? false;
|
|
1349
|
+
}
|
|
1350
|
+
return result;
|
|
1351
|
+
}
|
|
1352
|
+
const NOT_READ = /* @__PURE__ */ new Set([
|
|
1353
|
+
"info",
|
|
1354
|
+
"separator",
|
|
1355
|
+
"query"
|
|
1356
|
+
]);
|
|
1357
|
+
var unused_default = defineRule({
|
|
1358
|
+
meta: {
|
|
1359
|
+
type: "suggestion",
|
|
1360
|
+
docs: { description: "Disallow schema fields the section or block never reads." },
|
|
1361
|
+
schema: [],
|
|
1362
|
+
messages: { unused: "Studio shows '{{name}}' to editors, but the component never reads it. Use the field or remove it." }
|
|
1363
|
+
},
|
|
1364
|
+
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1365
|
+
create: ({ context, file, report, visitTemplate }) => {
|
|
1366
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1367
|
+
const used = /* @__PURE__ */ new Set();
|
|
1368
|
+
const propsVariable = findPropsVariable(context.sourceCode.ast);
|
|
1369
|
+
let hasProps = false;
|
|
1370
|
+
let block = false;
|
|
1371
|
+
let escapes = false;
|
|
1372
|
+
const track = (node) => {
|
|
1373
|
+
const names = usedProps(node);
|
|
1374
|
+
if (names) names.forEach((name) => used.add(name));
|
|
1375
|
+
else escapes = true;
|
|
1376
|
+
};
|
|
1377
|
+
visitTemplate({
|
|
1378
|
+
VExpressionContainer: (node) => {
|
|
1379
|
+
for (const { id } of node.references) used.add(id.name);
|
|
1380
|
+
},
|
|
1381
|
+
Identifier: (node) => {
|
|
1382
|
+
if ((node.name === "$props" || node.name === propsVariable) && isReference(node)) track(node);
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
return {
|
|
1386
|
+
"CallExpression": (node) => {
|
|
1387
|
+
hasProps ||= isDefineProps(node);
|
|
1388
|
+
const definition = readDefinition(node);
|
|
1389
|
+
if (!definition) return;
|
|
1390
|
+
block ||= definition.definer === "defineBlock";
|
|
1391
|
+
conditionFields(definition).forEach((name) => used.add(name));
|
|
1392
|
+
for (const { name, type, as } of readFields(definition)) if (name && !as && !NOT_READ.has(type ?? "")) fields.set(name.value, name.node);
|
|
1393
|
+
},
|
|
1394
|
+
"Identifier": (node) => {
|
|
1395
|
+
const declaration = node.parent.type === "VariableDeclarator" && node.parent.id === node;
|
|
1396
|
+
if (node.name === propsVariable && !declaration && isReference(node)) track(node);
|
|
1397
|
+
},
|
|
1398
|
+
"Program:exit": () => {
|
|
1399
|
+
if (!hasProps || escapes) return;
|
|
1400
|
+
const unused = [...fields].filter(([name]) => !used.has(name));
|
|
1401
|
+
const readBySection = unused.length > 0 && block ? slotProps(file.root) : void 0;
|
|
1402
|
+
if (readBySection?.all) return;
|
|
1403
|
+
for (const [name, node] of unused) if (!readBySection?.names.has(name)) report({
|
|
1404
|
+
node,
|
|
1405
|
+
messageId: "unused",
|
|
1406
|
+
data: { name }
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
var casing_default = defineRule({
|
|
350
1413
|
meta: {
|
|
351
1414
|
type: "problem",
|
|
352
|
-
docs: { description: "
|
|
1415
|
+
docs: { description: "Disallow top-level schema field names that Vue renames or rejects as props." },
|
|
353
1416
|
schema: [],
|
|
354
1417
|
messages: {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
namedExport: "Handler files only export their handler. Laioutr registers the default export; move everything else to server/utils/.",
|
|
358
|
-
missingDefault: "Handler files export their handler as default, otherwise laioutr registers nothing."
|
|
1418
|
+
hyphen: "Vue camelizes '{{name}}' to {{suggestion}}, so the prop never arrives under the field's name. Name the field {{suggestion}}.",
|
|
1419
|
+
dollar: "Vue rejects prop names that start with $, so '{{name}}' never arrives as a prop. Pick another name."
|
|
359
1420
|
}
|
|
360
1421
|
},
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
1422
|
+
applies: (file) => file.side === "app",
|
|
1423
|
+
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1424
|
+
const definition = readDefinition(node);
|
|
1425
|
+
for (const { name } of definition ? readFields(definition) : []) if (name?.value.startsWith("$")) report({
|
|
1426
|
+
node: name.node,
|
|
1427
|
+
messageId: "dollar",
|
|
1428
|
+
data: { name: name.value }
|
|
1429
|
+
});
|
|
1430
|
+
else if (name?.value.includes("-")) report({
|
|
1431
|
+
node: name.node,
|
|
1432
|
+
messageId: "hyphen",
|
|
1433
|
+
data: {
|
|
1434
|
+
name: name.value,
|
|
1435
|
+
suggestion: camelCase(name.value)
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
} })
|
|
1439
|
+
});
|
|
1440
|
+
var required_default = defineRule({
|
|
1441
|
+
meta: {
|
|
1442
|
+
type: "problem",
|
|
1443
|
+
docs: { description: "Disallow `required` on schema fields, which Studio doesn't support." },
|
|
1444
|
+
schema: [],
|
|
1445
|
+
messages: { required: "Schema fields have no required option, so Studio ignores it. Give the field a default instead." }
|
|
1446
|
+
},
|
|
1447
|
+
applies: (file) => file.side === "app",
|
|
1448
|
+
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1449
|
+
const definition = readDefinition(node);
|
|
1450
|
+
for (const field of definition ? readAllFields(definition) : []) {
|
|
1451
|
+
const required = findProperty(field.node, "required");
|
|
1452
|
+
if (required) report({
|
|
1453
|
+
node: required,
|
|
1454
|
+
messageId: "required"
|
|
369
1455
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1456
|
+
}
|
|
1457
|
+
} })
|
|
1458
|
+
});
|
|
1459
|
+
const RESERVED = /* @__PURE__ */ new Set([
|
|
1460
|
+
"key",
|
|
1461
|
+
"ref",
|
|
1462
|
+
"ref_for",
|
|
1463
|
+
"ref_key",
|
|
1464
|
+
"class",
|
|
1465
|
+
"style"
|
|
1466
|
+
]);
|
|
1467
|
+
var reserved_default = defineRule({
|
|
1468
|
+
meta: {
|
|
1469
|
+
type: "problem",
|
|
1470
|
+
docs: { description: "Disallow top-level schema field names that never reach the component's props." },
|
|
1471
|
+
schema: [],
|
|
1472
|
+
messages: {
|
|
1473
|
+
reserved: "'{{name}}' never arrives as a prop: Vue handles key, ref, ref_for and ref_key itself, and merges class and style into the root element. Pick another name, e.g. variant for a style selector.",
|
|
1474
|
+
slots: "frontend-core passes a section's blocks in its slots prop, so a field named slots is overwritten. Pick another name."
|
|
1475
|
+
}
|
|
1476
|
+
},
|
|
1477
|
+
applies: (file) => file.side === "app",
|
|
1478
|
+
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1479
|
+
const definition = readDefinition(node);
|
|
1480
|
+
for (const { name } of definition ? readFields(definition) : []) if (name && RESERVED.has(name.value)) report({
|
|
1481
|
+
node: name.node,
|
|
1482
|
+
messageId: "reserved",
|
|
1483
|
+
data: { name: name.value }
|
|
1484
|
+
});
|
|
1485
|
+
else if (name?.value === "slots" && definition?.definer === "defineSection") report({
|
|
1486
|
+
node: name.node,
|
|
1487
|
+
messageId: "slots"
|
|
1488
|
+
});
|
|
1489
|
+
} })
|
|
1490
|
+
});
|
|
1491
|
+
const parseVueFile = cachedReader((text) => {
|
|
1492
|
+
try {
|
|
373
1493
|
return {
|
|
374
|
-
|
|
375
|
-
|
|
1494
|
+
ast: parse$2(text, {
|
|
1495
|
+
parser: tsParser,
|
|
1496
|
+
sourceType: "module",
|
|
1497
|
+
ecmaVersion: "latest"
|
|
1498
|
+
}),
|
|
1499
|
+
text
|
|
1500
|
+
};
|
|
1501
|
+
} catch {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
const OWN_ATTRS = /inheritAttrs\s*:\s*false/;
|
|
1506
|
+
var root_default = defineRule({
|
|
1507
|
+
meta: {
|
|
1508
|
+
type: "problem",
|
|
1509
|
+
docs: { description: "Require sections and blocks to render one root element, which carries frontend-core's data-lfc-* markers." },
|
|
1510
|
+
schema: [],
|
|
1511
|
+
messages: {
|
|
1512
|
+
roots: "frontend-core adds its data-lfc-* markers to the root element of a section or block, and Vue drops them when there's more than one root. Wrap the content in one element.",
|
|
1513
|
+
componentRoots: "<{{name}}> renders more than one root element, so Vue drops the data-lfc-* markers frontend-core adds to this section or block. Give {{name}} one root, or wrap it here."
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1517
|
+
create: ({ context, report }) => {
|
|
1518
|
+
let defined = false;
|
|
1519
|
+
return {
|
|
1520
|
+
"CallExpression": (node) => {
|
|
1521
|
+
defined ||= readDefinition(node) !== void 0;
|
|
376
1522
|
},
|
|
377
|
-
"
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
1523
|
+
"Program:exit": () => {
|
|
1524
|
+
const program = context.sourceCode.ast;
|
|
1525
|
+
const template = program.templateBody;
|
|
1526
|
+
if (!defined || !template || OWN_ATTRS.test(context.sourceCode.text)) return;
|
|
1527
|
+
const [root, second] = renderedRoots(template);
|
|
1528
|
+
if (second) {
|
|
1529
|
+
report({
|
|
1530
|
+
loc: second.startTag.loc,
|
|
1531
|
+
messageId: "roots"
|
|
1532
|
+
});
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const file = root && importedComponent(program, context.filename, root.rawName);
|
|
1536
|
+
const component = file ? parseVueFile(file) : void 0;
|
|
1537
|
+
const componentTemplate = component?.ast.templateBody;
|
|
1538
|
+
if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 && !OWN_ATTRS.test(component.text)) report({
|
|
1539
|
+
loc: root.startTag.loc,
|
|
1540
|
+
messageId: "componentRoots",
|
|
1541
|
+
data: { name: root.rawName }
|
|
386
1542
|
});
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
var slots_default = defineRule({
|
|
1548
|
+
meta: {
|
|
1549
|
+
type: "problem",
|
|
1550
|
+
docs: { description: "Disallow counting slot children, since frontend-core passes all blocks of a slot as one Fragment." },
|
|
1551
|
+
schema: [],
|
|
1552
|
+
messages: { length: "frontend-core passes all blocks of a slot as one Fragment, so this length is 1. Count the rendered blocks with a helper that flattens Fragments." }
|
|
1553
|
+
},
|
|
1554
|
+
applies: (file) => file.side === "app",
|
|
1555
|
+
create: ({ report, visitTemplate }) => {
|
|
1556
|
+
const slotVariables = /* @__PURE__ */ new Set(["slots", "$slots"]);
|
|
1557
|
+
const isSlots = (node) => node.type === "Identifier" && slotVariables.has(node.name) || node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "useSlots";
|
|
1558
|
+
const check = (node) => {
|
|
1559
|
+
if (node.computed || node.property.type !== "Identifier" || node.property.name !== "length") return;
|
|
1560
|
+
const call = unwrap(node.object);
|
|
1561
|
+
const slot = call.type === "CallExpression" ? unwrap(call.callee) : void 0;
|
|
1562
|
+
if (slot?.type === "MemberExpression" && isSlots(slot.object)) report({
|
|
1563
|
+
node: node.property,
|
|
1564
|
+
messageId: "length"
|
|
1565
|
+
});
|
|
1566
|
+
};
|
|
1567
|
+
visitTemplate({ MemberExpression: check });
|
|
1568
|
+
return {
|
|
1569
|
+
VariableDeclarator: (node) => {
|
|
1570
|
+
if (node.id.type === "Identifier" && node.init && isSlots(node.init)) slotVariables.add(node.id.name);
|
|
387
1571
|
},
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1572
|
+
MemberExpression: (node) => {
|
|
1573
|
+
check(node);
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
var bundle_default = defineRule({
|
|
1579
|
+
meta: {
|
|
1580
|
+
type: "problem",
|
|
1581
|
+
docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins, which every page loads." },
|
|
1582
|
+
schema: [{
|
|
1583
|
+
type: "object",
|
|
1584
|
+
properties: { packages: {
|
|
1585
|
+
type: "array",
|
|
1586
|
+
items: { type: "string" }
|
|
1587
|
+
} },
|
|
1588
|
+
additionalProperties: false
|
|
1589
|
+
}],
|
|
1590
|
+
messages: { heavy: "frontend-core imports every section and block up front, and plugins run on every page, so '{{name}}' lands in the chunk every page loads. Load it with import() or defineAsyncComponent() instead." }
|
|
1591
|
+
},
|
|
1592
|
+
applies: (file) => file.kind === "section" || file.kind === "block" || file.kind === "app-plugin",
|
|
1593
|
+
create: ({ context, report }) => {
|
|
1594
|
+
const { packages = [] } = context.options[0] ?? {};
|
|
1595
|
+
return { ImportDeclaration: (node) => {
|
|
1596
|
+
const source = String(node.source.value);
|
|
1597
|
+
if (!isTypeOnly(node) && packages.some((name) => source === name || source.startsWith(`${name}/`))) report({
|
|
1598
|
+
node,
|
|
1599
|
+
messageId: "heavy",
|
|
1600
|
+
data: { name: source }
|
|
1601
|
+
});
|
|
1602
|
+
} };
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
const readPackageFile = cachedReader((text) => {
|
|
1606
|
+
const json = JSON.parse(text);
|
|
1607
|
+
return {
|
|
1608
|
+
name: json.name,
|
|
1609
|
+
dependencies: new Set([
|
|
1610
|
+
json.dependencies,
|
|
1611
|
+
json.devDependencies,
|
|
1612
|
+
json.peerDependencies,
|
|
1613
|
+
json.optionalDependencies
|
|
1614
|
+
].flatMap((deps) => Object.keys(deps ?? {})))
|
|
1615
|
+
};
|
|
1616
|
+
});
|
|
1617
|
+
function readPackage(root) {
|
|
1618
|
+
return readPackageFile(join(root, "package.json"));
|
|
1619
|
+
}
|
|
1620
|
+
function isRuntimeConfig(node, variables) {
|
|
1621
|
+
if (node.type === "CallExpression") return node.callee.type === "Identifier" && node.callee.name === "useRuntimeConfig";
|
|
1622
|
+
if (node.type === "Identifier") return variables.has(node.name);
|
|
1623
|
+
return node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.property.name === "public" && isRuntimeConfig(node.object, variables);
|
|
1624
|
+
}
|
|
1625
|
+
function setupOptions(node) {
|
|
1626
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
1627
|
+
const [options] = isFunction(parent) ? parent.params : [];
|
|
1628
|
+
const owner = parent.parent;
|
|
1629
|
+
if (options?.type === "Identifier" && owner?.type === "Property" && owner.key.type === "Identifier" && owner.key.name === "setup") return options.name;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
var keys_default = defineRule({
|
|
1633
|
+
meta: {
|
|
1634
|
+
type: "problem",
|
|
1635
|
+
docs: { description: "Require runtime config keys of apps to be the package itself or one of its dependencies." },
|
|
1636
|
+
schema: [],
|
|
1637
|
+
messages: { key: "'{{key}}' isn't this package or one of its dependencies, so its config is missing at runtime. Use the key of an installed app." }
|
|
1638
|
+
},
|
|
1639
|
+
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1640
|
+
create: ({ file, report }) => {
|
|
1641
|
+
const variables = /* @__PURE__ */ new Set();
|
|
1642
|
+
return {
|
|
1643
|
+
VariableDeclarator: (node) => {
|
|
1644
|
+
if (node.id.type === "Identifier" && node.init && isRuntimeConfig(node.init, variables)) variables.add(node.id.name);
|
|
393
1645
|
},
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
messageId: "missingDefault"
|
|
1646
|
+
MemberExpression: (node) => {
|
|
1647
|
+
const key = node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : void 0;
|
|
1648
|
+
if (!key?.startsWith("@") || !isRuntimeConfig(node.object, variables)) return;
|
|
1649
|
+
const pkg = readPackage(file.root);
|
|
1650
|
+
if (pkg && key !== pkg.name && !pkg.dependencies.has(key)) report({
|
|
1651
|
+
node: node.property,
|
|
1652
|
+
messageId: "key",
|
|
1653
|
+
data: { key }
|
|
403
1654
|
});
|
|
404
|
-
reporter.flush();
|
|
405
1655
|
}
|
|
406
1656
|
};
|
|
407
1657
|
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
1658
|
+
});
|
|
1659
|
+
var public_default = defineRule({
|
|
1660
|
+
meta: {
|
|
1661
|
+
type: "problem",
|
|
1662
|
+
docs: { description: "Disallow copying module options into the public runtime config, which reaches the browser." },
|
|
1663
|
+
schema: [],
|
|
1664
|
+
messages: { options: "runtimeConfig.public reaches the browser, and with it every token or secret among the module options. Copy only the options meant to be public, e.g. options.storefrontUrl." }
|
|
1665
|
+
},
|
|
1666
|
+
applies: (file) => file.side === "build",
|
|
1667
|
+
create: ({ report }) => ({ AssignmentExpression: (node) => {
|
|
1668
|
+
const path = memberPath(node.left);
|
|
1669
|
+
const index = path.indexOf("runtimeConfig");
|
|
1670
|
+
const options = index !== -1 && path[index + 1] === "public" ? setupOptions(node) : void 0;
|
|
1671
|
+
if (!options) return;
|
|
1672
|
+
walk(node.right, (child) => {
|
|
1673
|
+
if (child.type !== "Identifier" || child.name !== options || !isReference(child)) return;
|
|
1674
|
+
const { parent } = child;
|
|
1675
|
+
if (parent.type !== "MemberExpression" || parent.object !== child) report({
|
|
1676
|
+
node: child,
|
|
1677
|
+
messageId: "options"
|
|
1678
|
+
});
|
|
1679
|
+
});
|
|
1680
|
+
} })
|
|
1681
|
+
});
|
|
414
1682
|
const LABELS = {
|
|
415
1683
|
"section": "section",
|
|
416
1684
|
"block": "block",
|
|
@@ -445,13 +1713,7 @@ const ABOVE_SERVER_UTILS = /* @__PURE__ */ new Set([
|
|
|
445
1713
|
"orchestr-file",
|
|
446
1714
|
"media-library"
|
|
447
1715
|
]);
|
|
448
|
-
|
|
449
|
-
"handler",
|
|
450
|
-
"orchestr-file",
|
|
451
|
-
"middleware",
|
|
452
|
-
"media-library"
|
|
453
|
-
]);
|
|
454
|
-
function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
1716
|
+
function findViolation(importer, target, typeOnly) {
|
|
455
1717
|
if (importer.side === "app" && target.side === "server") return { messageId: "appImportsServer" };
|
|
456
1718
|
if (importer.side === "server" && target.side === "app") return { messageId: "serverImportsApp" };
|
|
457
1719
|
if (importer.side === "shared" && (target.side === "app" || target.side === "server")) return {
|
|
@@ -462,21 +1724,10 @@ function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
|
462
1724
|
if (target.kind === "handler") return { messageId: "handlerImported" };
|
|
463
1725
|
if (typeOnly) return;
|
|
464
1726
|
if (target.kind === "client" && importer.kind === "handler") return { messageId: "clientInHandler" };
|
|
465
|
-
if (target.kind === "client" && importer.kind === "server-util") return { messageId: "clientInUtil" };
|
|
466
|
-
if (target.kind === "middleware" && !BUILDER_USERS.has(importer.kind)) return { messageId: "middlewareImported" };
|
|
467
1727
|
if (importer.kind === "server-util" && ABOVE_SERVER_UTILS.has(target.kind)) return {
|
|
468
1728
|
messageId: "serverUtilImportsUp",
|
|
469
1729
|
data: { kind: LABELS[target.kind] }
|
|
470
1730
|
};
|
|
471
|
-
const from = domainOf(importer);
|
|
472
|
-
const to = domainOf(target);
|
|
473
|
-
if (from && to && from !== to && !sharedDomains.includes(to)) return {
|
|
474
|
-
messageId: "crossDomain",
|
|
475
|
-
data: {
|
|
476
|
-
from,
|
|
477
|
-
to
|
|
478
|
-
}
|
|
479
|
-
};
|
|
480
1731
|
if (importer.kind === "app-util" && ABOVE_APP_UTILS.has(target.kind)) return {
|
|
481
1732
|
messageId: "appUtilImportsUp",
|
|
482
1733
|
data: { kind: LABELS[target.kind] }
|
|
@@ -487,100 +1738,104 @@ function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
|
487
1738
|
};
|
|
488
1739
|
if (importer.kind === "component" && target.kind === "section") return { messageId: "componentImportsSection" };
|
|
489
1740
|
}
|
|
1741
|
+
var layers_default = defineRule({
|
|
1742
|
+
meta: {
|
|
1743
|
+
type: "problem",
|
|
1744
|
+
docs: { description: "Enforce the layers of a Laioutr app: app, server and shared code, build-time code, orchestr handlers, API clients and utils." },
|
|
1745
|
+
schema: [],
|
|
1746
|
+
messages: {
|
|
1747
|
+
appImportsServer: "App code can't import server code. Move what both sides need to src/runtime/shared/.",
|
|
1748
|
+
serverImportsApp: "Server code can't import app code. Move what both sides need to src/runtime/shared/.",
|
|
1749
|
+
sharedImportsSide: "Shared code runs in the app and on the server, so it can't import from {{side}}/.",
|
|
1750
|
+
runtimeImportsBuild: "Runtime code can't import build-time code from src/; it isn't part of the runtime bundle. Type imports are fine.",
|
|
1751
|
+
handlerImported: "Orchestr handlers are registered by laioutr, never imported. Move the shared code to server/utils/.",
|
|
1752
|
+
clientInHandler: "Handlers read API clients from the orchestr context. server/client/ only holds clients; move constants and helpers to server/utils/.",
|
|
1753
|
+
serverUtilImportsUp: "Server utils are the bottom layer and can't import {{kind}} code.",
|
|
1754
|
+
appUtilImportsUp: "App utils are the bottom layer and can't import a {{kind}}. Type imports are fine.",
|
|
1755
|
+
composableImportsUp: "Composables can't import a {{kind}}. Type imports are fine.",
|
|
1756
|
+
componentImportsSection: "Components can't import sections; sections compose components.",
|
|
1757
|
+
nodeBuiltin: "'{{name}}' only exists on the server. Keep it in server/."
|
|
1758
|
+
}
|
|
1759
|
+
},
|
|
1760
|
+
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1761
|
+
create: ({ context, file: importer, report }) => {
|
|
1762
|
+
const checkImport = (node, source, typeOnly) => {
|
|
1763
|
+
if (typeof source !== "string") return;
|
|
1764
|
+
if (source.startsWith("node:")) {
|
|
1765
|
+
if (importer.side !== "server" && !typeOnly) report({
|
|
1766
|
+
node,
|
|
1767
|
+
messageId: "nodeBuiltin",
|
|
1768
|
+
data: { name: source }
|
|
1769
|
+
});
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const path = resolveImport(context.filename, source);
|
|
1773
|
+
const target = path ? classify(path) : void 0;
|
|
1774
|
+
const violation = target && findViolation(importer, target, typeOnly);
|
|
1775
|
+
if (violation) report({
|
|
1776
|
+
node,
|
|
1777
|
+
...violation
|
|
1778
|
+
});
|
|
1779
|
+
};
|
|
1780
|
+
return {
|
|
1781
|
+
ImportDeclaration: (node) => {
|
|
1782
|
+
checkImport(node, node.source.value, isTypeOnly(node));
|
|
1783
|
+
},
|
|
1784
|
+
ExportNamedDeclaration: (node) => {
|
|
1785
|
+
if (node.source) checkImport(node, node.source.value, isTypeOnly(node));
|
|
1786
|
+
},
|
|
1787
|
+
ExportAllDeclaration: (node) => {
|
|
1788
|
+
checkImport(node, node.source.value, isTypeOnly(node));
|
|
1789
|
+
},
|
|
1790
|
+
ImportExpression: (node) => {
|
|
1791
|
+
if (node.source.type === "Literal") checkImport(node, node.source.value, false);
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
490
1796
|
const plugin = {
|
|
491
1797
|
meta: {
|
|
492
1798
|
name,
|
|
493
1799
|
version
|
|
494
1800
|
},
|
|
495
1801
|
rules: {
|
|
496
|
-
"layers":
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
componentImportsSection: "Components can't import sections; sections compose components.",
|
|
522
|
-
nodeBuiltin: "'{{name}}' only exists on the server. Keep it in server/."
|
|
523
|
-
}
|
|
524
|
-
},
|
|
525
|
-
create(context) {
|
|
526
|
-
const importer = classify(context.filename);
|
|
527
|
-
if (!importer || importer.test || importer.side !== "app" && importer.side !== "server" && importer.side !== "shared") return {};
|
|
528
|
-
const { sharedDomains = [] } = context.options[0] ?? {};
|
|
529
|
-
const reporter = createReporter(context, importer);
|
|
530
|
-
const checkImport = (node, source, typeOnly) => {
|
|
531
|
-
if (typeof source !== "string") return;
|
|
532
|
-
if (source.startsWith("node:")) {
|
|
533
|
-
if (importer.side !== "server" && !typeOnly) reporter.report({
|
|
534
|
-
node,
|
|
535
|
-
messageId: "nodeBuiltin",
|
|
536
|
-
data: { name: source }
|
|
537
|
-
});
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
const path = resolveImport(context.filename, source);
|
|
541
|
-
const target = path ? classify(path) : void 0;
|
|
542
|
-
const violation = target && findViolation(importer, target, typeOnly, sharedDomains);
|
|
543
|
-
if (violation) reporter.report({
|
|
544
|
-
node,
|
|
545
|
-
...violation
|
|
546
|
-
});
|
|
547
|
-
};
|
|
548
|
-
return {
|
|
549
|
-
"ImportDeclaration": (node) => {
|
|
550
|
-
checkImport(node, node.source.value, isTypeOnly(node));
|
|
551
|
-
},
|
|
552
|
-
"ExportNamedDeclaration": (node) => {
|
|
553
|
-
if (node.source) checkImport(node, node.source.value, isTypeOnly(node));
|
|
554
|
-
},
|
|
555
|
-
"ExportAllDeclaration": (node) => {
|
|
556
|
-
checkImport(node, node.source.value, isTypeOnly(node));
|
|
557
|
-
},
|
|
558
|
-
"ImportExpression": (node) => {
|
|
559
|
-
if (node.source.type === "Literal") checkImport(node, node.source.value, false);
|
|
560
|
-
},
|
|
561
|
-
"Program:exit": () => {
|
|
562
|
-
reporter.flush();
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
},
|
|
567
|
-
"orchestr-files": files,
|
|
568
|
-
"definitions": definition,
|
|
569
|
-
"button-type": button
|
|
1802
|
+
"layers": layers_default,
|
|
1803
|
+
"heavy-imports": bundle_default,
|
|
1804
|
+
"config-keys": keys_default,
|
|
1805
|
+
"public-config": public_default,
|
|
1806
|
+
"orchestr-files": files_default$1,
|
|
1807
|
+
"orchestr-cookies": cookies_default,
|
|
1808
|
+
"handler-exports": exports_default,
|
|
1809
|
+
"middleware-files": files_default,
|
|
1810
|
+
"initware-throws": throws_default,
|
|
1811
|
+
"token-namespaces": namespaces_default,
|
|
1812
|
+
"definition-folder": folder_default,
|
|
1813
|
+
"component-name": name_default,
|
|
1814
|
+
"definition-description": description_default,
|
|
1815
|
+
"single-root": root_default,
|
|
1816
|
+
"slot-children": slots_default,
|
|
1817
|
+
"reserved-field-names": reserved_default,
|
|
1818
|
+
"field-name-case": casing_default,
|
|
1819
|
+
"required-fields": required_default,
|
|
1820
|
+
"unused-fields": unused_default,
|
|
1821
|
+
"dead-fallbacks": fallbacks_default,
|
|
1822
|
+
"button-type": button_default,
|
|
1823
|
+
"mutation-errors": mutations_default,
|
|
1824
|
+
"resolve-result": resolve_default,
|
|
1825
|
+
"hand-built-links": paths_default,
|
|
1826
|
+
"internal-anchors": anchors_default
|
|
570
1827
|
}
|
|
571
1828
|
};
|
|
572
1829
|
async function larrylint(options = {}) {
|
|
573
|
-
const {
|
|
1830
|
+
const { heavyPackages } = await loadLarrylintConfig(options.cwd);
|
|
574
1831
|
return [{
|
|
575
1832
|
name: "larrylint",
|
|
576
1833
|
files: ["**/src/**/*.{ts,mts,cts,js,mjs,cjs,vue}"],
|
|
577
1834
|
plugins: { larrylint: plugin },
|
|
578
1835
|
rules: {
|
|
579
|
-
|
|
580
|
-
"larrylint/
|
|
581
|
-
"larrylint/definitions": "error",
|
|
582
|
-
"larrylint/button-type": "error"
|
|
1836
|
+
...Object.fromEntries(Object.keys(plugin.rules).map((id) => [`larrylint/${id}`, "error"])),
|
|
1837
|
+
"larrylint/heavy-imports": ["error", { packages: heavyPackages }]
|
|
583
1838
|
}
|
|
584
1839
|
}];
|
|
585
1840
|
}
|
|
586
|
-
export { BASELINE_FILE, countViolations, defineLarrylintConfig, larrylint, plugin,
|
|
1841
|
+
export { BASELINE_FILE, applyBaseline, countViolations, defineLarrylintConfig, larrylint, plugin, writeBaseline };
|