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