larrylint 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -54
- package/dist/_chunks/package.mjs +1 -1
- package/dist/_chunks/preset.mjs +554 -814
- package/dist/index.d.mts +1 -17
- package/package.json +1 -1
package/dist/_chunks/preset.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { name, version } from "./package.mjs";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { loadConfig } from "c12";
|
|
4
|
-
import {
|
|
4
|
+
import { dirname, join, normalize, parse, relative, resolve } from "pathe";
|
|
5
5
|
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import tsParser, { parse as parse$1 } from "@typescript-eslint/parser";
|
|
7
7
|
import { parse as parse$2 } from "vue-eslint-parser";
|
|
@@ -17,76 +17,15 @@ async function loadLarrylintConfig(cwd = process.cwd()) {
|
|
|
17
17
|
globalRc: false,
|
|
18
18
|
dotenv: false
|
|
19
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
|
-
};
|
|
20
|
+
return { heavyPackages: config.heavyPackages ?? [] };
|
|
55
21
|
}
|
|
56
|
-
|
|
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)?$/;
|
|
22
|
+
const HANDLER_FILE = /\.(query|resolver|link|action|templates?|page-index)(?:\.[cm]?[jt]s)?$/;
|
|
86
23
|
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
87
24
|
const APP_FOLDERS = {
|
|
88
25
|
"sections": "section",
|
|
26
|
+
"section": "section",
|
|
89
27
|
"blocks": "block",
|
|
28
|
+
"block": "block",
|
|
90
29
|
"components": "component",
|
|
91
30
|
"composables": "composable",
|
|
92
31
|
"utils": "app-util",
|
|
@@ -129,10 +68,6 @@ function classify(file) {
|
|
|
129
68
|
test
|
|
130
69
|
};
|
|
131
70
|
}
|
|
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
71
|
function classifyRuntime(parts) {
|
|
137
72
|
const [side, folder = "", ...rest] = parts;
|
|
138
73
|
if (parts.length < 2 || side !== "app" && side !== "server" && side !== "shared") return { side: "other" };
|
|
@@ -144,27 +79,83 @@ function classifyRuntime(parts) {
|
|
|
144
79
|
side,
|
|
145
80
|
kind: APP_FOLDERS[folder] ?? "other"
|
|
146
81
|
};
|
|
147
|
-
const nested = rest.length > 1;
|
|
148
82
|
if (folder === "orchestr") {
|
|
149
|
-
if (
|
|
83
|
+
if (rest.length > 1 && rest[0] === "plugins") return {
|
|
150
84
|
side,
|
|
151
85
|
kind: "orchestr-plugin"
|
|
152
86
|
};
|
|
153
|
-
const handler = HANDLER_FILE.exec(parts.at(-1))?.[1];
|
|
87
|
+
const handler = HANDLER_FILE.exec(parts.at(-1))?.[1]?.replace("templates", "template");
|
|
154
88
|
return {
|
|
155
89
|
side,
|
|
156
90
|
kind: handler ? "handler" : "orchestr-file",
|
|
157
|
-
handler
|
|
158
|
-
domain: nested ? rest[0] : void 0
|
|
91
|
+
handler
|
|
159
92
|
};
|
|
160
93
|
}
|
|
161
|
-
const kind = SERVER_FOLDERS[folder] ?? "other";
|
|
162
94
|
return {
|
|
163
95
|
side,
|
|
164
|
-
kind
|
|
165
|
-
domain: kind === "server-util" && nested ? rest[0] : void 0
|
|
96
|
+
kind: SERVER_FOLDERS[folder] ?? "other"
|
|
166
97
|
};
|
|
167
98
|
}
|
|
99
|
+
const EXTENSIONS = [
|
|
100
|
+
"",
|
|
101
|
+
".ts",
|
|
102
|
+
".mts",
|
|
103
|
+
".cts",
|
|
104
|
+
".js",
|
|
105
|
+
".mjs",
|
|
106
|
+
".cjs",
|
|
107
|
+
"/index.ts",
|
|
108
|
+
"/index.js"
|
|
109
|
+
];
|
|
110
|
+
const directories = /* @__PURE__ */ new Map();
|
|
111
|
+
function cachedReader(parse) {
|
|
112
|
+
const cache = /* @__PURE__ */ new Map();
|
|
113
|
+
return (path) => {
|
|
114
|
+
let mtimeMs;
|
|
115
|
+
try {
|
|
116
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
117
|
+
} catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const cached = cache.get(path);
|
|
121
|
+
if (cached?.mtimeMs === mtimeMs) return cached.value;
|
|
122
|
+
const value = parse(readFileSync(path, "utf8"));
|
|
123
|
+
cache.set(path, {
|
|
124
|
+
mtimeMs,
|
|
125
|
+
value
|
|
126
|
+
});
|
|
127
|
+
return value;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function isDirectory(path) {
|
|
131
|
+
let result = directories.get(path);
|
|
132
|
+
if (result === void 0) {
|
|
133
|
+
try {
|
|
134
|
+
result = statSync(path).isDirectory();
|
|
135
|
+
} catch {
|
|
136
|
+
result = false;
|
|
137
|
+
}
|
|
138
|
+
directories.set(path, result);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
function resolveImport(importer, source) {
|
|
143
|
+
if (!source.startsWith("./") && !source.startsWith("../")) return;
|
|
144
|
+
const target = resolve(dirname(importer), source.split("?")[0]);
|
|
145
|
+
return isDirectory(target) ? `${target}/index` : target;
|
|
146
|
+
}
|
|
147
|
+
function resolveModule(importer, source) {
|
|
148
|
+
if (!source.startsWith("./") && !source.startsWith("../")) return;
|
|
149
|
+
const target = resolve(dirname(importer), source.split("?")[0]);
|
|
150
|
+
return EXTENSIONS.map((extension) => `${target}${extension}`).find((path) => existsSync(path) && !isDirectory(path));
|
|
151
|
+
}
|
|
152
|
+
function findPackage(from, name) {
|
|
153
|
+
for (let folder = from;; folder = dirname(folder)) {
|
|
154
|
+
const candidate = join(folder, "node_modules", name);
|
|
155
|
+
if (existsSync(join(candidate, "package.json"))) return realpathSync(candidate);
|
|
156
|
+
if (dirname(folder) === folder) return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
168
159
|
const BASELINE_FILE = "larrylint-baseline.json";
|
|
169
160
|
const readBaselineFile = cachedReader((text) => JSON.parse(text));
|
|
170
161
|
function readBaseline(root) {
|
|
@@ -257,47 +248,6 @@ function defineRule(rule) {
|
|
|
257
248
|
}
|
|
258
249
|
};
|
|
259
250
|
}
|
|
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
251
|
const WRAPPERS = /* @__PURE__ */ new Set([
|
|
302
252
|
"ChainExpression",
|
|
303
253
|
"TSNonNullExpression",
|
|
@@ -314,15 +264,6 @@ function chainRoot(node) {
|
|
|
314
264
|
else if (current.type === "MemberExpression") current = current.object;
|
|
315
265
|
else return current.type === "Identifier" ? current : void 0;
|
|
316
266
|
}
|
|
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
267
|
function memberPath(node) {
|
|
327
268
|
if (node.type === "Identifier") return [node.name];
|
|
328
269
|
if (node.type !== "MemberExpression") return [];
|
|
@@ -359,6 +300,37 @@ function followPromise(promise) {
|
|
|
359
300
|
end = call;
|
|
360
301
|
}
|
|
361
302
|
}
|
|
303
|
+
const TEST_DIRECTIVES = /* @__PURE__ */ new Set([
|
|
304
|
+
"if",
|
|
305
|
+
"else-if",
|
|
306
|
+
"show"
|
|
307
|
+
]);
|
|
308
|
+
function isReference(node) {
|
|
309
|
+
const { parent } = node;
|
|
310
|
+
if (parent?.type === "MemberExpression") return parent.property !== node || parent.computed;
|
|
311
|
+
return parent?.type !== "Property" || parent.key !== node || parent.computed || parent.shorthand;
|
|
312
|
+
}
|
|
313
|
+
function isNullish(node) {
|
|
314
|
+
return node.type === "Identifier" && node.name === "undefined" || node.type === "Literal" && node.value === null && !("regex" in node);
|
|
315
|
+
}
|
|
316
|
+
function canBeNullish(value) {
|
|
317
|
+
if (isNullish(value)) return true;
|
|
318
|
+
if (value.type === "LogicalExpression" && value.operator === "??") return canBeNullish(value.right);
|
|
319
|
+
if (value.type === "ConditionalExpression") return canBeNullish(value.consequent) || canBeNullish(value.alternate);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
function isTested(node) {
|
|
323
|
+
const parent = node.parent;
|
|
324
|
+
switch (parent?.type) {
|
|
325
|
+
case "IfStatement":
|
|
326
|
+
case "WhileStatement":
|
|
327
|
+
case "ConditionalExpression": return parent.test === node;
|
|
328
|
+
case "LogicalExpression": return parent.left === node;
|
|
329
|
+
case "UnaryExpression": return parent.operator === "!";
|
|
330
|
+
case "VExpressionContainer": return parent.parent?.type === "VAttribute" && TEST_DIRECTIVES.has(parent.parent.key?.name?.name ?? "");
|
|
331
|
+
default: return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
362
334
|
const SKIPPED = /* @__PURE__ */ new Set([
|
|
363
335
|
"parent",
|
|
364
336
|
"loc",
|
|
@@ -412,16 +384,28 @@ function isCaught(node) {
|
|
|
412
384
|
}
|
|
413
385
|
return false;
|
|
414
386
|
}
|
|
387
|
+
function canReturnNullish(fn, value) {
|
|
388
|
+
if (fn.body.type !== "BlockStatement") return fn.body !== value && canBeNullish(fn.body);
|
|
389
|
+
let nullish = false;
|
|
390
|
+
walkBody(fn, (node) => {
|
|
391
|
+
nullish ||= node.type === "ReturnStatement" && node.argument !== value && (!node.argument || canBeNullish(node.argument));
|
|
392
|
+
});
|
|
393
|
+
const last = fn.body.body.at(-1);
|
|
394
|
+
return nullish || last?.type !== "ReturnStatement" && last?.type !== "ThrowStatement";
|
|
395
|
+
}
|
|
415
396
|
var mutations_default = defineRule({
|
|
416
397
|
meta: {
|
|
417
398
|
type: "problem",
|
|
418
399
|
docs: { description: "Require error handling where a mutateAsync() promise is awaited or dropped, since a rejected mutation takes down its whole section or block." },
|
|
419
400
|
schema: [],
|
|
420
|
-
messages: {
|
|
401
|
+
messages: {
|
|
402
|
+
unhandled: "If this mutation fails, frontend-core replaces the whole section or block with its \"Retry\" state, unless every caller catches the error. Catch it here with try/catch or .catch().",
|
|
403
|
+
dropped: "Nothing handles this promise, so a failed mutation ends up as an unhandled rejection without any feedback. Catch it with try/catch or .catch()."
|
|
404
|
+
}
|
|
421
405
|
},
|
|
422
406
|
applies: (file) => file.side === "app",
|
|
423
407
|
create: ({ report, visitTemplate }) => {
|
|
424
|
-
const check = (call) => {
|
|
408
|
+
const check = (call, template) => {
|
|
425
409
|
if (nameOf(call.callee) !== "mutateAsync") return;
|
|
426
410
|
const { end, handled } = followPromise(call);
|
|
427
411
|
const { parent } = end;
|
|
@@ -431,146 +415,40 @@ var mutations_default = defineRule({
|
|
|
431
415
|
if (!awaited && !dropped || awaited && isCaught(parent)) return;
|
|
432
416
|
report({
|
|
433
417
|
node: call.callee.type === "MemberExpression" ? call.callee.property : call.callee,
|
|
434
|
-
messageId: "unhandled"
|
|
418
|
+
messageId: dropped && !template ? "dropped" : "unhandled"
|
|
435
419
|
});
|
|
436
420
|
};
|
|
437
|
-
visitTemplate({ CallExpression: check });
|
|
438
|
-
return { CallExpression: check };
|
|
421
|
+
visitTemplate({ CallExpression: (node) => check(node, true) });
|
|
422
|
+
return { CallExpression: (node) => check(node, false) };
|
|
439
423
|
}
|
|
440
424
|
});
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
"
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
messageId: "store"
|
|
459
|
-
});
|
|
460
|
-
} })
|
|
461
|
-
});
|
|
425
|
+
const METHODS = /* @__PURE__ */ new Set(["resolve", "resolveOrThrow"]);
|
|
426
|
+
function destructuredResolvers(declarator) {
|
|
427
|
+
const methods = /* @__PURE__ */ new Map();
|
|
428
|
+
if (declarator.id.type !== "ObjectPattern" || !declarator.init || nameOf(declarator.init) !== "linkResolver") return methods;
|
|
429
|
+
for (const property of declarator.id.properties) {
|
|
430
|
+
const method = property.type === "Property" ? nameOf(property.key) : void 0;
|
|
431
|
+
if (method && METHODS.has(method) && property.type === "Property" && property.value.type === "Identifier") methods.set(property.value.name, method);
|
|
432
|
+
}
|
|
433
|
+
return methods;
|
|
434
|
+
}
|
|
435
|
+
function linkResolverMethod(call, destructured) {
|
|
436
|
+
const { callee } = call;
|
|
437
|
+
if (callee.type === "Identifier") return destructured.get(callee.name);
|
|
438
|
+
const method = callee.type === "MemberExpression" ? nameOf(callee) : void 0;
|
|
439
|
+
return method && METHODS.has(method) && callee.type === "MemberExpression" && nameOf(callee.object) === "linkResolver" ? method : void 0;
|
|
440
|
+
}
|
|
441
|
+
const NOT_PAGES = /^\/(?:\/|api\/|_|app-|\.well-known\/)/;
|
|
462
442
|
function isInternalPath(path) {
|
|
463
|
-
return path?.startsWith("/") === true && !
|
|
443
|
+
return path?.startsWith("/") === true && !NOT_PAGES.test(path);
|
|
464
444
|
}
|
|
465
445
|
function isHandBuiltPath(node) {
|
|
466
446
|
if (node?.type === "TemplateLiteral") return node.expressions.length > 0 && isInternalPath(node.quasis[0]?.value.cooked);
|
|
467
447
|
return node?.type === "BinaryExpression" && node.operator === "+" && node.left.type === "Literal" && typeof node.left.value === "string" && isInternalPath(node.left.value);
|
|
468
448
|
}
|
|
469
|
-
function
|
|
470
|
-
|
|
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
|
-
}
|
|
449
|
+
function isRouter(node) {
|
|
450
|
+
const name = nameOf(node.type === "CallExpression" ? node.callee : node);
|
|
451
|
+
return node.type === "CallExpression" ? name === "useRouter" : name === "router" || name === "$router";
|
|
574
452
|
}
|
|
575
453
|
const parseFile = cachedReader((text) => {
|
|
576
454
|
try {
|
|
@@ -611,6 +489,22 @@ function findFunction(statements, name) {
|
|
|
611
489
|
}
|
|
612
490
|
}
|
|
613
491
|
}
|
|
492
|
+
function findConstant(statements, name) {
|
|
493
|
+
for (const statement of statements) {
|
|
494
|
+
const declaration = statement.type === "ExportNamedDeclaration" && statement.declaration ? statement.declaration : statement;
|
|
495
|
+
if (declaration.type !== "VariableDeclaration" || declaration.kind !== "const") continue;
|
|
496
|
+
const declarator = declaration.declarations.find((item) => item.id.type === "Identifier" && item.id.name === name);
|
|
497
|
+
if (declarator?.init) return declarator.init;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function resolveConstant(name, program, filename) {
|
|
501
|
+
const local = findConstant(program.body, name);
|
|
502
|
+
if (local) return local;
|
|
503
|
+
const imported = findImport(program, name);
|
|
504
|
+
const file = imported && imported.name !== "*" ? resolveModule(filename, imported.source) : void 0;
|
|
505
|
+
const module = file ? parseFile(file) : void 0;
|
|
506
|
+
return module && imported ? findConstant(module.body, imported.name) : void 0;
|
|
507
|
+
}
|
|
614
508
|
function findExport(program, name) {
|
|
615
509
|
for (const statement of program.body) {
|
|
616
510
|
if (statement.type === "ExportDefaultDeclaration" && name === "default") {
|
|
@@ -665,33 +559,138 @@ function usedProps(node) {
|
|
|
665
559
|
}
|
|
666
560
|
function findHolder(node) {
|
|
667
561
|
let value = node;
|
|
562
|
+
let fn;
|
|
668
563
|
const { parent } = node;
|
|
669
564
|
if (parent?.type === "ReturnStatement") {
|
|
670
|
-
let
|
|
671
|
-
while (
|
|
672
|
-
|
|
673
|
-
|
|
565
|
+
let current = parent.parent;
|
|
566
|
+
while (current && !isFunction(current)) current = current.parent;
|
|
567
|
+
fn = current ?? void 0;
|
|
568
|
+
value = current ?? value;
|
|
569
|
+
} else if (parent && isFunction(parent) && parent.body === node) {
|
|
570
|
+
fn = parent;
|
|
571
|
+
value = parent;
|
|
572
|
+
}
|
|
674
573
|
const computed = value.parent?.type === "CallExpression" && value.parent.callee.type === "Identifier" && value.parent.callee.name === "computed" ? value.parent : void 0;
|
|
675
574
|
const declarator = (computed ?? value).parent;
|
|
676
575
|
return declarator?.type === "VariableDeclarator" && declarator.id.type === "Identifier" ? {
|
|
677
576
|
name: declarator.id.name,
|
|
678
|
-
computed: computed !== void 0
|
|
577
|
+
computed: computed !== void 0,
|
|
578
|
+
fn
|
|
679
579
|
} : void 0;
|
|
680
580
|
}
|
|
681
581
|
function importedComponent(program, filename, name) {
|
|
682
582
|
const imported = findImport(program, name);
|
|
683
583
|
return imported?.name === "default" && imported.source.endsWith(".vue") ? resolveModule(filename, imported.source) : void 0;
|
|
684
584
|
}
|
|
585
|
+
function renderedRoots(template) {
|
|
586
|
+
return template.children.filter((child) => child.type === "VElement" && !child.startTag.attributes.some((attribute) => attribute.directive && (attribute.key.name.name === "else" || attribute.key.name.name === "else-if")));
|
|
587
|
+
}
|
|
588
|
+
function findAttribute(element, name) {
|
|
589
|
+
return element.startTag.attributes.find((attribute) => attribute.directive ? attribute.key.name.name === "bind" && attribute.key.argument?.type === "VIdentifier" && attribute.key.argument.name === name : attribute.key.name === name);
|
|
590
|
+
}
|
|
591
|
+
function valueStart(attribute) {
|
|
592
|
+
if (!attribute.directive) return attribute.value?.value;
|
|
593
|
+
const expression = attribute.value?.expression;
|
|
594
|
+
if (expression?.type === "Literal" && typeof expression.value === "string") return expression.value;
|
|
595
|
+
return expression?.type === "TemplateLiteral" ? expression.quasis[0]?.value.cooked ?? void 0 : void 0;
|
|
596
|
+
}
|
|
597
|
+
function rendersTag(element, tag) {
|
|
598
|
+
const is = element.rawName === "component" ? findAttribute(element, "is") : void 0;
|
|
599
|
+
if (element.rawName === tag || is && !is.directive && is.value?.value === tag) return true;
|
|
600
|
+
const expression = is?.directive ? is.value?.expression : void 0;
|
|
601
|
+
let found = false;
|
|
602
|
+
if (expression) walk(expression, (node) => {
|
|
603
|
+
found ||= node.type === "Literal" && node.value === tag;
|
|
604
|
+
});
|
|
605
|
+
return found;
|
|
606
|
+
}
|
|
607
|
+
var anchors_default = defineRule({
|
|
608
|
+
meta: {
|
|
609
|
+
type: "problem",
|
|
610
|
+
docs: { description: "Disallow plain `<a>` tags for internal and resolved links, which reload the page and break Studio's navigation sync." },
|
|
611
|
+
schema: [],
|
|
612
|
+
messages: { anchor: "A plain <a> reloads the whole page and breaks Studio's navigation sync. Use <NuxtLink> for internal links." }
|
|
613
|
+
},
|
|
614
|
+
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
615
|
+
create: ({ report, visitTemplate }) => {
|
|
616
|
+
const destructured = /* @__PURE__ */ new Map();
|
|
617
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
618
|
+
const resolves = (node) => node.type === "CallExpression" && (linkResolverMethod(node, destructured) !== void 0 || nameOf(node.callee) === "useResolvedLink");
|
|
619
|
+
visitTemplate({ VElement: (node) => {
|
|
620
|
+
const href = rendersTag(node, "a") ? findAttribute(node, "href") : void 0;
|
|
621
|
+
const target = findAttribute(node, "target");
|
|
622
|
+
if (!href || findAttribute(node, "download") || target && !target.directive && target.value?.value === "_blank") return;
|
|
623
|
+
const expression = href.directive ? href.value?.expression : void 0;
|
|
624
|
+
if (expression && (resolves(expression) || expression.type === "Identifier" && resolved.has(expression.name)) || isInternalPath(valueStart(href))) report({
|
|
625
|
+
loc: href.loc,
|
|
626
|
+
messageId: "anchor"
|
|
627
|
+
});
|
|
628
|
+
} });
|
|
629
|
+
return {
|
|
630
|
+
VariableDeclarator: (node) => {
|
|
631
|
+
destructuredResolvers(node).forEach((method, local) => destructured.set(local, method));
|
|
632
|
+
},
|
|
633
|
+
CallExpression: (node) => {
|
|
634
|
+
const holder = resolves(node) ? findHolder(node) : void 0;
|
|
635
|
+
if (holder) resolved.add(holder.name);
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
const LINK_NAMES = /* @__PURE__ */ new Set([
|
|
641
|
+
"to",
|
|
642
|
+
"href",
|
|
643
|
+
"link"
|
|
644
|
+
]);
|
|
645
|
+
const ROUTER_METHODS = /* @__PURE__ */ new Set(["push", "replace"]);
|
|
646
|
+
var paths_default = defineRule({
|
|
647
|
+
meta: {
|
|
648
|
+
type: "problem",
|
|
649
|
+
docs: { description: "Disallow hand-built paths to pages, which miss the per-language paths and market prefixes Studio sets." },
|
|
650
|
+
schema: [],
|
|
651
|
+
messages: { path: "Page paths are set per page and language in Studio, and each market adds its own prefix, e.g. /en, so this path can lead to the wrong page. Resolve the link with linkResolver instead." }
|
|
652
|
+
},
|
|
653
|
+
applies: (file) => file.side === "app",
|
|
654
|
+
create: ({ report, visitTemplate }) => {
|
|
655
|
+
visitTemplate({ VAttribute: (node) => {
|
|
656
|
+
const argument = node.directive && node.key.name.name === "bind" ? node.key.argument : null;
|
|
657
|
+
const expression = node.directive ? node.value?.expression : void 0;
|
|
658
|
+
if (argument?.type === "VIdentifier" && LINK_NAMES.has(argument.name) && isHandBuiltPath(expression)) report({
|
|
659
|
+
node: expression,
|
|
660
|
+
messageId: "path"
|
|
661
|
+
});
|
|
662
|
+
} });
|
|
663
|
+
return {
|
|
664
|
+
CallExpression: (node) => {
|
|
665
|
+
const { callee } = node;
|
|
666
|
+
const [target] = node.arguments;
|
|
667
|
+
if ((callee.type === "Identifier" ? callee.name === "navigateTo" : callee.type === "MemberExpression" && ROUTER_METHODS.has(nameOf(callee) ?? "") && isRouter(callee.object)) && isHandBuiltPath(target)) report({
|
|
668
|
+
node: target,
|
|
669
|
+
messageId: "path"
|
|
670
|
+
});
|
|
671
|
+
},
|
|
672
|
+
Property: (node) => {
|
|
673
|
+
if (node.key.type === "Identifier" && LINK_NAMES.has(node.key.name) && isHandBuiltPath(node.value)) report({
|
|
674
|
+
node: node.value,
|
|
675
|
+
messageId: "path"
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
});
|
|
685
681
|
var resolve_default = defineRule({
|
|
686
682
|
meta: {
|
|
687
683
|
type: "problem",
|
|
688
|
-
docs: { description: "Disallow testing the result of linkResolver.resolve(), which
|
|
684
|
+
docs: { description: "Disallow testing the result of linkResolver.resolve(), which falls back to a '#…' string instead of an empty value." },
|
|
689
685
|
schema: [],
|
|
690
|
-
messages: {
|
|
686
|
+
messages: {
|
|
687
|
+
tested: "linkResolver.resolve() returns a '#…' fallback instead of an empty value when it can't resolve a link, so this check doesn't catch broken links. Use resolveOrThrow() (frontend-core 0.42+) in a try/catch to tell them apart.",
|
|
688
|
+
branch: "When it can't resolve the link, linkResolver.resolve() returns a '#…' fallback here, so the checks on {{name}} treat a broken link as a working one. Use resolveOrThrow() (frontend-core 0.42+) in a try/catch and return undefined instead."
|
|
689
|
+
}
|
|
691
690
|
},
|
|
692
691
|
applies: (file) => file.side === "app",
|
|
693
692
|
create: ({ report, visitTemplate }) => {
|
|
694
|
-
const
|
|
693
|
+
const destructured = /* @__PURE__ */ new Map();
|
|
695
694
|
const calls = [];
|
|
696
695
|
const scriptIdentifiers = [];
|
|
697
696
|
const templateIdentifiers = [];
|
|
@@ -700,8 +699,7 @@ var resolve_default = defineRule({
|
|
|
700
699
|
} });
|
|
701
700
|
return {
|
|
702
701
|
"VariableDeclarator": (node) => {
|
|
703
|
-
|
|
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);
|
|
702
|
+
destructuredResolvers(node).forEach((method, local) => destructured.set(local, method));
|
|
705
703
|
},
|
|
706
704
|
"CallExpression": (node) => {
|
|
707
705
|
calls.push(node);
|
|
@@ -711,26 +709,38 @@ var resolve_default = defineRule({
|
|
|
711
709
|
},
|
|
712
710
|
"Program:exit": () => {
|
|
713
711
|
const holders = /* @__PURE__ */ new Map();
|
|
712
|
+
const reported = /* @__PURE__ */ new Set();
|
|
714
713
|
for (const call of calls) {
|
|
715
|
-
|
|
716
|
-
if (!(callee.type === "MemberExpression" && nameOf(callee) === "resolve" && nameOf(callee.object) === "linkResolver") && !(callee.type === "Identifier" && resolveNames.has(callee.name))) continue;
|
|
714
|
+
if (linkResolverMethod(call, destructured) !== "resolve") continue;
|
|
717
715
|
if (isTested(call)) report({
|
|
718
716
|
node: call,
|
|
719
717
|
messageId: "tested"
|
|
720
718
|
});
|
|
721
719
|
const holder = findHolder(call);
|
|
722
|
-
if (holder) holders.set(holder.name,
|
|
720
|
+
if (holder) holders.set(holder.name, {
|
|
721
|
+
computed: holder.computed,
|
|
722
|
+
branch: holder.fn && canReturnNullish(holder.fn, call) ? call : void 0
|
|
723
|
+
});
|
|
723
724
|
}
|
|
724
725
|
const check = (identifier, template) => {
|
|
725
|
-
const
|
|
726
|
+
const holder = holders.get(identifier.name);
|
|
726
727
|
const { parent } = identifier;
|
|
727
|
-
if (
|
|
728
|
+
if (!holder || !isReference(identifier)) return;
|
|
728
729
|
const readsValue = parent.type === "MemberExpression" && parent.object === identifier && nameOf(parent) === "value";
|
|
729
|
-
const value = computed && !template ? readsValue ? parent : void 0 : identifier;
|
|
730
|
-
if (value
|
|
730
|
+
const value = holder.computed && !template ? readsValue ? parent : void 0 : identifier;
|
|
731
|
+
if (!value || !isTested(value)) return;
|
|
732
|
+
if (!holder.branch) report({
|
|
731
733
|
node: value,
|
|
732
734
|
messageId: "tested"
|
|
733
735
|
});
|
|
736
|
+
else if (!reported.has(holder.branch)) {
|
|
737
|
+
reported.add(holder.branch);
|
|
738
|
+
report({
|
|
739
|
+
node: holder.branch,
|
|
740
|
+
messageId: "branch",
|
|
741
|
+
data: { name: identifier.name }
|
|
742
|
+
});
|
|
743
|
+
}
|
|
734
744
|
};
|
|
735
745
|
scriptIdentifiers.forEach((identifier) => check(identifier, false));
|
|
736
746
|
templateIdentifiers.forEach((identifier) => check(identifier, true));
|
|
@@ -741,24 +751,27 @@ var resolve_default = defineRule({
|
|
|
741
751
|
function kebabCase(name) {
|
|
742
752
|
return name.replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
743
753
|
}
|
|
744
|
-
function prefixName(name, prefix) {
|
|
745
|
-
return `${prefix}${name.endsWith(prefix) ? name.slice(0, -prefix.length) : name}`;
|
|
746
|
-
}
|
|
747
754
|
function camelCase(name) {
|
|
748
755
|
return name.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() ?? "").replace(/^[A-Z]/, (char) => char.toLowerCase());
|
|
749
756
|
}
|
|
750
|
-
const UI_KIT_BUTTON = /(?:^#ui-kit|@laioutr-core\/ui-kit)\/.*\/Button\.vue$/;
|
|
757
|
+
const UI_KIT_BUTTON = /(?:^#ui-kit|@laioutr-core\/ui-kit)\/.*\/(?:Icon|BackgroundAware|Animated)?Button\.vue$/;
|
|
758
|
+
const BUTTONS = [
|
|
759
|
+
"LButton",
|
|
760
|
+
"LIconButton",
|
|
761
|
+
"LBackgroundAwareButton",
|
|
762
|
+
"LAnimatedButton"
|
|
763
|
+
];
|
|
751
764
|
var button_default = defineRule({
|
|
752
765
|
meta: {
|
|
753
766
|
type: "problem",
|
|
754
767
|
fixable: "code",
|
|
755
|
-
docs: { description: "Disallow `type` on the ui-kit
|
|
768
|
+
docs: { description: "Disallow `type` on the ui-kit buttons, which silently render their `button-type` prop instead." },
|
|
756
769
|
schema: [],
|
|
757
|
-
messages: { buttonType: "
|
|
770
|
+
messages: { buttonType: "<{{tag}}> ignores type and always renders its button-type prop, which defaults to \"button\". Use button-type." }
|
|
758
771
|
},
|
|
759
772
|
applies: (file) => file.path.endsWith(".vue"),
|
|
760
773
|
create: ({ report, visitTemplate }) => {
|
|
761
|
-
const buttons =
|
|
774
|
+
const buttons = new Set(BUTTONS.flatMap((name) => [name, kebabCase(name)]));
|
|
762
775
|
visitTemplate({ VElement: (node) => {
|
|
763
776
|
if (!buttons.has(node.rawName)) return;
|
|
764
777
|
for (const attribute of node.startTag.attributes) {
|
|
@@ -767,6 +780,7 @@ var button_default = defineRule({
|
|
|
767
780
|
report({
|
|
768
781
|
loc: key.loc,
|
|
769
782
|
messageId: "buttonType",
|
|
783
|
+
data: { tag: node.rawName },
|
|
770
784
|
fix: (fixer) => fixer.replaceTextRange(key.range, "button-type")
|
|
771
785
|
});
|
|
772
786
|
}
|
|
@@ -774,6 +788,7 @@ var button_default = defineRule({
|
|
|
774
788
|
if (attribute.directive && attribute.key.name.name === "bind" && argument?.type === "VIdentifier" && argument.rawName === "type") report({
|
|
775
789
|
loc: argument.loc,
|
|
776
790
|
messageId: "buttonType",
|
|
791
|
+
data: { tag: node.rawName },
|
|
777
792
|
fix: (fixer) => fixer.replaceTextRange(argument.range, "button-type")
|
|
778
793
|
});
|
|
779
794
|
}
|
|
@@ -788,101 +803,6 @@ var button_default = defineRule({
|
|
|
788
803
|
} };
|
|
789
804
|
}
|
|
790
805
|
});
|
|
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
806
|
function readMiddleware(call) {
|
|
887
807
|
const { callee } = call;
|
|
888
808
|
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") return;
|
|
@@ -894,42 +814,78 @@ function readMiddleware(call) {
|
|
|
894
814
|
callback
|
|
895
815
|
} : void 0;
|
|
896
816
|
}
|
|
897
|
-
function
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
817
|
+
function enclosingMiddleware(node) {
|
|
818
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
819
|
+
const middleware = isFunction(parent) && parent.parent?.type === "CallExpression" ? readMiddleware(parent.parent) : void 0;
|
|
820
|
+
if (middleware?.callback === parent) return middleware;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const COOKIE_WRITES = /* @__PURE__ */ new Set([
|
|
824
|
+
"setCookie",
|
|
825
|
+
"deleteCookie",
|
|
826
|
+
"setManagedCookie",
|
|
827
|
+
"deleteManagedCookie"
|
|
828
|
+
]);
|
|
829
|
+
const HEADER_WRITES = /* @__PURE__ */ new Set([
|
|
830
|
+
"setHeader",
|
|
831
|
+
"setHeaders",
|
|
832
|
+
"appendHeader",
|
|
833
|
+
"appendHeaders",
|
|
834
|
+
"setResponseHeader",
|
|
835
|
+
"setResponseHeaders",
|
|
836
|
+
"appendResponseHeader",
|
|
837
|
+
"appendResponseHeaders",
|
|
838
|
+
"setResponseStatus",
|
|
839
|
+
"sendRedirect"
|
|
840
|
+
]);
|
|
841
|
+
function responseWrite(call) {
|
|
842
|
+
const name = call.callee.type === "Identifier" ? call.callee.name : "";
|
|
843
|
+
if (COOKIE_WRITES.has(name)) return {
|
|
844
|
+
name,
|
|
845
|
+
kind: "cookie"
|
|
846
|
+
};
|
|
847
|
+
return HEADER_WRITES.has(name) ? {
|
|
848
|
+
name,
|
|
849
|
+
kind: "header"
|
|
850
|
+
} : void 0;
|
|
851
|
+
}
|
|
852
|
+
const STREAMED = /* @__PURE__ */ new Set([
|
|
853
|
+
"query",
|
|
854
|
+
"link",
|
|
855
|
+
"resolver"
|
|
856
|
+
]);
|
|
857
|
+
const MANAGED = {
|
|
858
|
+
setCookie: "setManagedCookie",
|
|
859
|
+
deleteCookie: "deleteManagedCookie"
|
|
860
|
+
};
|
|
917
861
|
var cookies_default = defineRule({
|
|
918
862
|
meta: {
|
|
919
863
|
type: "problem",
|
|
920
|
-
docs: { description: "
|
|
864
|
+
docs: { description: "Write cookies and headers only where orchestr allows it: in extendRequest() and action handlers, with frontend-core's managed cookie functions." },
|
|
921
865
|
schema: [],
|
|
922
866
|
messages: {
|
|
923
|
-
|
|
924
|
-
|
|
867
|
+
streamed: "Orchestr may have sent the response headers by the time {{name}}() runs here, so the browser never gets it. Write cookies and headers in extendRequest() or an action handler.",
|
|
868
|
+
managed: "Use {{managed}}() instead. It applies Laioutr's cookie policy, which the Studio preview needs (SameSite=None and Partitioned), and {{name}}() skips it."
|
|
925
869
|
}
|
|
926
870
|
},
|
|
927
871
|
applies: (file) => file.side === "server",
|
|
928
872
|
create: ({ file, report }) => ({ CallExpression: (node) => {
|
|
929
|
-
const
|
|
930
|
-
if (
|
|
873
|
+
const write = responseWrite(node);
|
|
874
|
+
if (!write) return;
|
|
875
|
+
const method = enclosingMiddleware(node)?.method;
|
|
876
|
+
const managed = MANAGED[write.name];
|
|
877
|
+
if (method === "use" || !method && STREAMED.has(file.handler)) report({
|
|
931
878
|
node,
|
|
932
|
-
messageId:
|
|
879
|
+
messageId: "streamed",
|
|
880
|
+
data: { name: write.name }
|
|
881
|
+
});
|
|
882
|
+
else if (managed && (method === "extendRequest" || !method && file.handler === "action")) report({
|
|
883
|
+
node,
|
|
884
|
+
messageId: "managed",
|
|
885
|
+
data: {
|
|
886
|
+
name: write.name,
|
|
887
|
+
managed
|
|
888
|
+
}
|
|
933
889
|
});
|
|
934
890
|
} })
|
|
935
891
|
});
|
|
@@ -946,9 +902,9 @@ const FILE_START = {
|
|
|
946
902
|
var files_default$1 = defineRule({
|
|
947
903
|
meta: {
|
|
948
904
|
type: "problem",
|
|
949
|
-
docs: { description: "Keep orchestr/ to handler files, since Laioutr loads every file in it as a
|
|
905
|
+
docs: { description: "Keep orchestr/ to handler files, since Laioutr loads every file in it as a Nitro plugin." },
|
|
950
906
|
schema: [],
|
|
951
|
-
messages: { notAHandler: "Laioutr loads every file in orchestr/ as a
|
|
907
|
+
messages: { notAHandler: "Laioutr loads every file in orchestr/ as a Nitro plugin, so a helper here runs at startup or breaks the build. Move it to server/utils/, and name handlers *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts or *.page-index.ts." }
|
|
952
908
|
},
|
|
953
909
|
applies: (file) => file.kind === "orchestr-file",
|
|
954
910
|
create: ({ report }) => ({ Program: () => {
|
|
@@ -958,54 +914,12 @@ var files_default$1 = defineRule({
|
|
|
958
914
|
});
|
|
959
915
|
} })
|
|
960
916
|
});
|
|
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
917
|
var exports_default = defineRule({
|
|
1001
918
|
meta: {
|
|
1002
919
|
type: "problem",
|
|
1003
|
-
docs: { description: "Require handler files to export their handler as default
|
|
920
|
+
docs: { description: "Require handler files to export their handler as default." },
|
|
1004
921
|
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
|
-
}
|
|
922
|
+
messages: { missingDefault: "Laioutr loads every file in orchestr/ as a Nitro plugin from its default export, so without one the build fails. Export the handler as default." }
|
|
1009
923
|
},
|
|
1010
924
|
applies: (file) => file.kind === "handler",
|
|
1011
925
|
create: ({ report }) => {
|
|
@@ -1014,22 +928,8 @@ var exports_default = defineRule({
|
|
|
1014
928
|
"ExportDefaultDeclaration": () => {
|
|
1015
929
|
hasDefault = true;
|
|
1016
930
|
},
|
|
1017
|
-
"
|
|
1018
|
-
|
|
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
|
-
});
|
|
931
|
+
"ExportSpecifier": (node) => {
|
|
932
|
+
hasDefault ||= (node.exported.type === "Identifier" ? node.exported.name : node.exported.value) === "default";
|
|
1033
933
|
},
|
|
1034
934
|
"Program:exit": () => {
|
|
1035
935
|
if (!hasDefault) report({
|
|
@@ -1073,42 +973,6 @@ var files_default = defineRule({
|
|
|
1073
973
|
};
|
|
1074
974
|
}
|
|
1075
975
|
});
|
|
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
976
|
var throws_default = defineRule({
|
|
1113
977
|
meta: {
|
|
1114
978
|
type: "problem",
|
|
@@ -1149,42 +1013,6 @@ var throws_default = defineRule({
|
|
|
1149
1013
|
});
|
|
1150
1014
|
} })
|
|
1151
1015
|
});
|
|
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
1016
|
const PACKAGE = "@laioutr-core/canonical-types";
|
|
1189
1017
|
const TOKEN_ID = /define\w*Token\(\s*["'`]([a-z0-9-]+)\//g;
|
|
1190
1018
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -1208,7 +1036,7 @@ var namespaces_default = defineRule({
|
|
|
1208
1036
|
type: "problem",
|
|
1209
1037
|
docs: { description: "Keep your own token ids out of the namespaces of Laioutr's canonical types." },
|
|
1210
1038
|
schema: [],
|
|
1211
|
-
messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types.
|
|
1039
|
+
messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types, and a later canonical-types release or another app can take this id. Token metadata is last-write-wins, so put your own tokens under your app's namespace." }
|
|
1212
1040
|
},
|
|
1213
1041
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1214
1042
|
create: ({ file, report }) => ({ CallExpression: (node) => {
|
|
@@ -1222,46 +1050,40 @@ var namespaces_default = defineRule({
|
|
|
1222
1050
|
});
|
|
1223
1051
|
} })
|
|
1224
1052
|
});
|
|
1225
|
-
function
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1053
|
+
function findProperty(object, key) {
|
|
1054
|
+
for (const entry of object.properties) if (entry.type === "Property" && !entry.computed && (entry.key.type === "Identifier" && entry.key.name === key || entry.key.type === "Literal" && entry.key.value === key)) return entry.value;
|
|
1055
|
+
}
|
|
1056
|
+
function findStringProperty(object, key) {
|
|
1057
|
+
const value = findProperty(object, key);
|
|
1058
|
+
return value?.type === "Literal" && typeof value.value === "string" ? {
|
|
1059
|
+
value: value.value,
|
|
1060
|
+
node: value
|
|
1061
|
+
} : void 0;
|
|
1232
1062
|
}
|
|
1233
|
-
function
|
|
1234
|
-
|
|
1063
|
+
function objectElements(node) {
|
|
1064
|
+
if (node?.type !== "ArrayExpression") return [];
|
|
1065
|
+
return node.elements.filter((element) => element?.type === "ObjectExpression");
|
|
1235
1066
|
}
|
|
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
1067
|
const DEFINERS = {
|
|
1254
1068
|
defineSection: {
|
|
1255
1069
|
kind: "section",
|
|
1256
|
-
folder: "sections"
|
|
1257
|
-
prefix: "Section"
|
|
1070
|
+
folder: "sections"
|
|
1258
1071
|
},
|
|
1259
1072
|
defineBlock: {
|
|
1260
1073
|
kind: "block",
|
|
1261
|
-
folder: "blocks"
|
|
1262
|
-
prefix: "Block"
|
|
1074
|
+
folder: "blocks"
|
|
1263
1075
|
}
|
|
1264
1076
|
};
|
|
1077
|
+
const PICKERS = /* @__PURE__ */ new Set([
|
|
1078
|
+
"select",
|
|
1079
|
+
"radio",
|
|
1080
|
+
"toggle_button"
|
|
1081
|
+
]);
|
|
1082
|
+
const TEXTS$1 = /* @__PURE__ */ new Set([
|
|
1083
|
+
"text",
|
|
1084
|
+
"textarea",
|
|
1085
|
+
"secret"
|
|
1086
|
+
]);
|
|
1265
1087
|
function readDefinition(call) {
|
|
1266
1088
|
if (call.callee.type !== "Identifier" || !Object.hasOwn(DEFINERS, call.callee.name)) return;
|
|
1267
1089
|
const [options] = call.arguments;
|
|
@@ -1277,7 +1099,6 @@ function readGroups(owner) {
|
|
|
1277
1099
|
const node = "call" in owner ? owner.options : owner.node;
|
|
1278
1100
|
return objectElements(node && findProperty(node, "schema")).map((group) => ({
|
|
1279
1101
|
node: group,
|
|
1280
|
-
label: findStringProperty(group, "label"),
|
|
1281
1102
|
fields: objectElements(findProperty(group, "fields")).map(readField)
|
|
1282
1103
|
}));
|
|
1283
1104
|
}
|
|
@@ -1301,15 +1122,42 @@ function readAllFields(definition) {
|
|
|
1301
1122
|
readFields(definition).forEach(visit);
|
|
1302
1123
|
return fields;
|
|
1303
1124
|
}
|
|
1125
|
+
function conditionFields(definition) {
|
|
1126
|
+
const names = /* @__PURE__ */ new Set();
|
|
1127
|
+
for (const field of readAllFields(definition)) {
|
|
1128
|
+
const condition = findProperty(field.node, "if");
|
|
1129
|
+
if (condition) walk(condition, (node) => {
|
|
1130
|
+
const [operator, name] = node.type === "ArrayExpression" ? node.elements : [];
|
|
1131
|
+
if (operator?.type === "Literal" && operator.value === "get" && name?.type === "Literal" && typeof name.value === "string") names.add(name.value.split(".")[0]);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
return names;
|
|
1135
|
+
}
|
|
1304
1136
|
function expectedDefiner(kind) {
|
|
1305
1137
|
return Object.keys(DEFINERS).find((definer) => DEFINERS[definer].kind === kind);
|
|
1306
1138
|
}
|
|
1139
|
+
function fillValue(field, program, filename) {
|
|
1140
|
+
if (field.type === "checkbox") return { value: field.as === "visibility" };
|
|
1141
|
+
if (TEXTS$1.has(field.type ?? "")) return { value: "" };
|
|
1142
|
+
if (field.type === "content_alignment") {
|
|
1143
|
+
const axis = findStringProperty(field.node, "axis")?.value;
|
|
1144
|
+
return { value: !axis || axis === "both" ? "center-center" : "center" };
|
|
1145
|
+
}
|
|
1146
|
+
if (!PICKERS.has(field.type ?? "")) return;
|
|
1147
|
+
const property = findProperty(field.node, "options");
|
|
1148
|
+
const declared = property?.type === "Identifier" ? resolveConstant(property.name, program, filename) : property;
|
|
1149
|
+
let options = declared && unwrap(declared);
|
|
1150
|
+
if (options?.type === "CallExpression") options = options.arguments[0];
|
|
1151
|
+
const first = options?.type === "ArrayExpression" ? options.elements[0] : void 0;
|
|
1152
|
+
const value = first?.type === "ObjectExpression" ? findProperty(first, "value") : first;
|
|
1153
|
+
return value?.type === "Literal" ? { value: value.value } : void 0;
|
|
1154
|
+
}
|
|
1307
1155
|
var description_default = defineRule({
|
|
1308
1156
|
meta: {
|
|
1309
1157
|
type: "suggestion",
|
|
1310
1158
|
docs: { description: "Require a studio.description on section and block definitions." },
|
|
1311
1159
|
schema: [],
|
|
1312
|
-
messages: { description: "Studio shows studio.description in its section and
|
|
1160
|
+
messages: { description: "Studio shows studio.description in its section picker, and AI agents read it through Laioutr's MCP server. Add one." }
|
|
1313
1161
|
},
|
|
1314
1162
|
applies: (file) => file.side === "app",
|
|
1315
1163
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
@@ -1330,7 +1178,7 @@ var folder_default = defineRule({
|
|
|
1330
1178
|
docs: { description: "Keep defineSection() in app/sections/ and defineBlock() in app/blocks/, and every .vue there defined." },
|
|
1331
1179
|
schema: [],
|
|
1332
1180
|
messages: {
|
|
1333
|
-
notDefined: "Laioutr
|
|
1181
|
+
notDefined: "Laioutr loads every .vue in {{folder}}/ as a {{kind}}, but this file has no {{definer}}(), so frontend-core warns about it. Move it to components/.",
|
|
1334
1182
|
wrongFolder: "{{definer}}() belongs in app/{{folder}}/."
|
|
1335
1183
|
}
|
|
1336
1184
|
},
|
|
@@ -1391,118 +1239,100 @@ var name_default = defineRule({
|
|
|
1391
1239
|
});
|
|
1392
1240
|
} })
|
|
1393
1241
|
});
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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"
|
|
1242
|
+
const TEXTS = /* @__PURE__ */ new Set([
|
|
1243
|
+
"text",
|
|
1244
|
+
"textarea",
|
|
1245
|
+
"secret"
|
|
1423
1246
|
]);
|
|
1424
|
-
const EMPTY_WHEN_UNSET = /* @__PURE__ */ new Set(["text", "textarea"]);
|
|
1425
1247
|
var fallbacks_default = defineRule({
|
|
1426
1248
|
meta: {
|
|
1427
1249
|
type: "problem",
|
|
1428
|
-
docs: { description: "Disallow fallbacks
|
|
1250
|
+
docs: { description: "Disallow ?? fallbacks that never apply, because frontend-core fills unset fields: pickers with their first option, checkboxes with false and text with ''." },
|
|
1429
1251
|
schema: [],
|
|
1430
1252
|
messages: {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
text: "
|
|
1253
|
+
picker: "frontend-core fills an unset {{type}} field with its first option, {{fill}}, so this fallback never applies. Make {{fallback}} the first option if it should be the default.",
|
|
1254
|
+
checkbox: "frontend-core fills an unset checkbox with false, so this fallback never applies. Name the field so that unchecked is the default.",
|
|
1255
|
+
text: "frontend-core fills an unset {{type}} field with '', so ?? never falls back. Use || if an empty field should fall back."
|
|
1434
1256
|
}
|
|
1435
1257
|
},
|
|
1436
1258
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1437
1259
|
create: ({ context, report, visitTemplate }) => {
|
|
1438
|
-
const
|
|
1439
|
-
const propsVariable = findPropsVariable(
|
|
1440
|
-
const
|
|
1260
|
+
const program = context.sourceCode.ast;
|
|
1261
|
+
const propsVariable = findPropsVariable(program);
|
|
1262
|
+
const fills = /* @__PURE__ */ new Map();
|
|
1263
|
+
const fallbacks = [];
|
|
1441
1264
|
const propOf = (node, template) => {
|
|
1442
1265
|
if (template && node.type === "Identifier") return node.name;
|
|
1443
1266
|
const props = node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.object.type === "Identifier" ? node.object.name : void 0;
|
|
1444
1267
|
return props && (props === propsVariable || template && props === "$props") && node.type === "MemberExpression" && node.property.type === "Identifier" ? node.property.name : void 0;
|
|
1445
1268
|
};
|
|
1446
|
-
const collect = (template) => ({
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
});
|
|
1269
|
+
const collect = (template) => ({ LogicalExpression: (node) => {
|
|
1270
|
+
const prop = node.operator === "??" ? propOf(node.left, template) : void 0;
|
|
1271
|
+
if (prop && !isNullish(node.right)) fallbacks.push({
|
|
1272
|
+
node,
|
|
1273
|
+
prop
|
|
1274
|
+
});
|
|
1275
|
+
} });
|
|
1468
1276
|
visitTemplate(collect(true));
|
|
1469
1277
|
return {
|
|
1470
1278
|
...collect(false),
|
|
1471
1279
|
"CallExpression": (node) => {
|
|
1472
1280
|
const definition = readDefinition(node);
|
|
1473
|
-
for (const
|
|
1281
|
+
for (const field of definition ? readFields(definition) : []) {
|
|
1282
|
+
const fill = field.name && field.type && !field.as ? fillValue(field, program, context.filename) : void 0;
|
|
1283
|
+
if (fill) fills.set(field.name.value, {
|
|
1284
|
+
type: field.type,
|
|
1285
|
+
value: fill.value
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1474
1288
|
},
|
|
1475
1289
|
"Program:exit": () => {
|
|
1476
|
-
for (const { node, prop
|
|
1477
|
-
const
|
|
1478
|
-
if (type === "
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
});
|
|
1483
|
-
else if (type && FALSE_WHEN_UNSET.has(type)) report({
|
|
1290
|
+
for (const { node, prop } of fallbacks) {
|
|
1291
|
+
const field = fills.get(prop);
|
|
1292
|
+
if (!field || node.right.type === "Literal" && node.right.value === field.value) continue;
|
|
1293
|
+
const messageId = field.type === "checkbox" ? "checkbox" : TEXTS.has(field.type) ? "text" : "picker";
|
|
1294
|
+
const fill = typeof field.value === "string" ? `'${field.value}'` : String(field.value);
|
|
1295
|
+
report({
|
|
1484
1296
|
node,
|
|
1485
|
-
messageId
|
|
1297
|
+
messageId,
|
|
1486
1298
|
data: {
|
|
1487
|
-
type,
|
|
1488
|
-
|
|
1299
|
+
type: field.type,
|
|
1300
|
+
fill,
|
|
1301
|
+
fallback: context.sourceCode.getText(node.right)
|
|
1489
1302
|
}
|
|
1490
1303
|
});
|
|
1491
|
-
else if (type && EMPTY_WHEN_UNSET.has(type) && check === "??") report({
|
|
1492
|
-
node,
|
|
1493
|
-
messageId: "text",
|
|
1494
|
-
data: { type }
|
|
1495
|
-
});
|
|
1496
1304
|
}
|
|
1497
1305
|
}
|
|
1498
1306
|
};
|
|
1499
1307
|
}
|
|
1500
1308
|
});
|
|
1501
1309
|
const PROP_READ = /\.props\??\.([A-Z_$][\w$]*)|\.props\??\.?\[\s*['"]([^'"]+)['"]\s*\]|['"]([^'"]+)['"]\s+in\s+[\w$.?]+\.props\b/gi;
|
|
1502
|
-
const
|
|
1310
|
+
const PROPS_ALIAS = /\b(?:const|let|var)\s+([\w$]+)\s*=\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/g;
|
|
1311
|
+
const PROPS_DESTRUCTURING = /\{([^{}]*)\}\s*=\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/g;
|
|
1312
|
+
const PROPS_SPREAD = /\.\.\.\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/;
|
|
1313
|
+
const readSlotProps = cachedReader((text) => {
|
|
1314
|
+
const names = [...text.matchAll(PROP_READ)].map((match) => match[1] ?? match[2] ?? match[3]);
|
|
1315
|
+
let all = PROPS_SPREAD.test(text);
|
|
1316
|
+
for (const [, alias] of text.matchAll(PROPS_ALIAS)) {
|
|
1317
|
+
const name = alias.replaceAll("$", "\\$");
|
|
1318
|
+
const reads = new RegExp(`\\b${name}\\??\\.([A-Z_$][\\w$]*)|\\b${name}\\??\\.?\\[\\s*['"]([^'"]+)['"]\\s*\\]`, "gi");
|
|
1319
|
+
for (const match of text.matchAll(reads)) names.push(match[1] ?? match[2]);
|
|
1320
|
+
}
|
|
1321
|
+
for (const [, list] of text.matchAll(PROPS_DESTRUCTURING)) for (const entry of list.split(",").map((item) => item.trim()).filter(Boolean)) {
|
|
1322
|
+
all ||= entry.startsWith("...");
|
|
1323
|
+
names.push(entry.split(/[:=]/)[0].trim());
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
names,
|
|
1327
|
+
all
|
|
1328
|
+
};
|
|
1329
|
+
});
|
|
1503
1330
|
function slotProps(root) {
|
|
1504
1331
|
const folder = join(root, "src/runtime/app");
|
|
1505
|
-
const
|
|
1332
|
+
const result = {
|
|
1333
|
+
names: /* @__PURE__ */ new Set(),
|
|
1334
|
+
all: false
|
|
1335
|
+
};
|
|
1506
1336
|
let files;
|
|
1507
1337
|
try {
|
|
1508
1338
|
files = readdirSync(folder, {
|
|
@@ -1510,10 +1340,14 @@ function slotProps(root) {
|
|
|
1510
1340
|
encoding: "utf8"
|
|
1511
1341
|
}).filter((file) => /\.(?:vue|[cm]?[jt]s)$/.test(file));
|
|
1512
1342
|
} catch {
|
|
1513
|
-
return
|
|
1343
|
+
return result;
|
|
1514
1344
|
}
|
|
1515
|
-
for (const file of files)
|
|
1516
|
-
|
|
1345
|
+
for (const file of files) {
|
|
1346
|
+
const read = readSlotProps(join(folder, file));
|
|
1347
|
+
read?.names.forEach((name) => result.names.add(name));
|
|
1348
|
+
result.all ||= read?.all ?? false;
|
|
1349
|
+
}
|
|
1350
|
+
return result;
|
|
1517
1351
|
}
|
|
1518
1352
|
const NOT_READ = /* @__PURE__ */ new Set([
|
|
1519
1353
|
"info",
|
|
@@ -1533,6 +1367,7 @@ var unused_default = defineRule({
|
|
|
1533
1367
|
const used = /* @__PURE__ */ new Set();
|
|
1534
1368
|
const propsVariable = findPropsVariable(context.sourceCode.ast);
|
|
1535
1369
|
let hasProps = false;
|
|
1370
|
+
let block = false;
|
|
1536
1371
|
let escapes = false;
|
|
1537
1372
|
const track = (node) => {
|
|
1538
1373
|
const names = usedProps(node);
|
|
@@ -1551,7 +1386,10 @@ var unused_default = defineRule({
|
|
|
1551
1386
|
"CallExpression": (node) => {
|
|
1552
1387
|
hasProps ||= isDefineProps(node);
|
|
1553
1388
|
const definition = readDefinition(node);
|
|
1554
|
-
|
|
1389
|
+
if (!definition) return;
|
|
1390
|
+
block ||= definition.definer === "defineBlock";
|
|
1391
|
+
conditionFields(definition).forEach((name) => used.add(name));
|
|
1392
|
+
for (const { name, type, as } of readFields(definition)) if (name && !as && !NOT_READ.has(type ?? "")) fields.set(name.value, name.node);
|
|
1555
1393
|
},
|
|
1556
1394
|
"Identifier": (node) => {
|
|
1557
1395
|
const declaration = node.parent.type === "VariableDeclarator" && node.parent.id === node;
|
|
@@ -1560,8 +1398,9 @@ var unused_default = defineRule({
|
|
|
1560
1398
|
"Program:exit": () => {
|
|
1561
1399
|
if (!hasProps || escapes) return;
|
|
1562
1400
|
const unused = [...fields].filter(([name]) => !used.has(name));
|
|
1563
|
-
const readBySection = unused.length > 0 &&
|
|
1564
|
-
|
|
1401
|
+
const readBySection = unused.length > 0 && block ? slotProps(file.root) : void 0;
|
|
1402
|
+
if (readBySection?.all) return;
|
|
1403
|
+
for (const [name, node] of unused) if (!readBySection?.names.has(name)) report({
|
|
1565
1404
|
node,
|
|
1566
1405
|
messageId: "unused",
|
|
1567
1406
|
data: { name }
|
|
@@ -1570,59 +1409,32 @@ var unused_default = defineRule({
|
|
|
1570
1409
|
};
|
|
1571
1410
|
}
|
|
1572
1411
|
});
|
|
1573
|
-
const CAMEL_CASE = /^[a-z][a-zA-Z0-9]*$/;
|
|
1574
1412
|
var casing_default = defineRule({
|
|
1575
1413
|
meta: {
|
|
1576
1414
|
type: "problem",
|
|
1577
|
-
docs: { description: "
|
|
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." },
|
|
1415
|
+
docs: { description: "Disallow top-level schema field names that Vue renames or rejects as props." },
|
|
1600
1416
|
schema: [],
|
|
1601
1417
|
messages: {
|
|
1602
|
-
|
|
1603
|
-
|
|
1418
|
+
hyphen: "Vue camelizes '{{name}}' to {{suggestion}}, so the prop never arrives under the field's name. Name the field {{suggestion}}.",
|
|
1419
|
+
dollar: "Vue rejects prop names that start with $, so '{{name}}' never arrives as a prop. Pick another name."
|
|
1604
1420
|
}
|
|
1605
1421
|
},
|
|
1606
1422
|
applies: (file) => file.side === "app",
|
|
1607
1423
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1608
1424
|
const definition = readDefinition(node);
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
}
|
|
1623
|
-
});
|
|
1624
|
-
else previous = label.value;
|
|
1625
|
-
}
|
|
1425
|
+
for (const { name } of definition ? readFields(definition) : []) if (name?.value.startsWith("$")) report({
|
|
1426
|
+
node: name.node,
|
|
1427
|
+
messageId: "dollar",
|
|
1428
|
+
data: { name: name.value }
|
|
1429
|
+
});
|
|
1430
|
+
else if (name?.value.includes("-")) report({
|
|
1431
|
+
node: name.node,
|
|
1432
|
+
messageId: "hyphen",
|
|
1433
|
+
data: {
|
|
1434
|
+
name: name.value,
|
|
1435
|
+
suggestion: camelCase(name.value)
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1626
1438
|
} })
|
|
1627
1439
|
});
|
|
1628
1440
|
var required_default = defineRule({
|
|
@@ -1645,21 +1457,22 @@ var required_default = defineRule({
|
|
|
1645
1457
|
} })
|
|
1646
1458
|
});
|
|
1647
1459
|
const RESERVED = /* @__PURE__ */ new Set([
|
|
1648
|
-
"style",
|
|
1649
|
-
"class",
|
|
1650
1460
|
"key",
|
|
1651
1461
|
"ref",
|
|
1652
|
-
"
|
|
1653
|
-
"
|
|
1654
|
-
"
|
|
1655
|
-
"
|
|
1462
|
+
"ref_for",
|
|
1463
|
+
"ref_key",
|
|
1464
|
+
"class",
|
|
1465
|
+
"style"
|
|
1656
1466
|
]);
|
|
1657
1467
|
var reserved_default = defineRule({
|
|
1658
1468
|
meta: {
|
|
1659
1469
|
type: "problem",
|
|
1660
|
-
docs: { description: "Disallow top-level schema field names that
|
|
1470
|
+
docs: { description: "Disallow top-level schema field names that never reach the component's props." },
|
|
1661
1471
|
schema: [],
|
|
1662
|
-
messages: {
|
|
1472
|
+
messages: {
|
|
1473
|
+
reserved: "'{{name}}' never arrives as a prop: Vue handles key, ref, ref_for and ref_key itself, and merges class and style into the root element. Pick another name, e.g. variant for a style selector.",
|
|
1474
|
+
slots: "frontend-core passes a section's blocks in its slots prop, so a field named slots is overwritten. Pick another name."
|
|
1475
|
+
}
|
|
1663
1476
|
},
|
|
1664
1477
|
applies: (file) => file.side === "app",
|
|
1665
1478
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
@@ -1669,36 +1482,12 @@ var reserved_default = defineRule({
|
|
|
1669
1482
|
messageId: "reserved",
|
|
1670
1483
|
data: { name: name.value }
|
|
1671
1484
|
});
|
|
1485
|
+
else if (name?.value === "slots" && definition?.definer === "defineSection") report({
|
|
1486
|
+
node: name.node,
|
|
1487
|
+
messageId: "slots"
|
|
1488
|
+
});
|
|
1672
1489
|
} })
|
|
1673
1490
|
});
|
|
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
1491
|
const parseVueFile = cachedReader((text) => {
|
|
1703
1492
|
try {
|
|
1704
1493
|
return {
|
|
@@ -1713,6 +1502,7 @@ const parseVueFile = cachedReader((text) => {
|
|
|
1713
1502
|
return;
|
|
1714
1503
|
}
|
|
1715
1504
|
});
|
|
1505
|
+
const OWN_ATTRS = /inheritAttrs\s*:\s*false/;
|
|
1716
1506
|
var root_default = defineRule({
|
|
1717
1507
|
meta: {
|
|
1718
1508
|
type: "problem",
|
|
@@ -1733,7 +1523,7 @@ var root_default = defineRule({
|
|
|
1733
1523
|
"Program:exit": () => {
|
|
1734
1524
|
const program = context.sourceCode.ast;
|
|
1735
1525
|
const template = program.templateBody;
|
|
1736
|
-
if (!defined || !template) return;
|
|
1526
|
+
if (!defined || !template || OWN_ATTRS.test(context.sourceCode.text)) return;
|
|
1737
1527
|
const [root, second] = renderedRoots(template);
|
|
1738
1528
|
if (second) {
|
|
1739
1529
|
report({
|
|
@@ -1745,7 +1535,7 @@ var root_default = defineRule({
|
|
|
1745
1535
|
const file = root && importedComponent(program, context.filename, root.rawName);
|
|
1746
1536
|
const component = file ? parseVueFile(file) : void 0;
|
|
1747
1537
|
const componentTemplate = component?.ast.templateBody;
|
|
1748
|
-
if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 &&
|
|
1538
|
+
if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 && !OWN_ATTRS.test(component.text)) report({
|
|
1749
1539
|
loc: root.startTag.loc,
|
|
1750
1540
|
messageId: "componentRoots",
|
|
1751
1541
|
data: { name: root.rawName }
|
|
@@ -1757,9 +1547,9 @@ var root_default = defineRule({
|
|
|
1757
1547
|
var slots_default = defineRule({
|
|
1758
1548
|
meta: {
|
|
1759
1549
|
type: "problem",
|
|
1760
|
-
docs: { description: "Disallow counting slot children, since
|
|
1550
|
+
docs: { description: "Disallow counting slot children, since frontend-core passes all blocks of a slot as one Fragment." },
|
|
1761
1551
|
schema: [],
|
|
1762
|
-
messages: { length: "
|
|
1552
|
+
messages: { length: "frontend-core passes all blocks of a slot as one Fragment, so this length is 1. Count the rendered blocks with a helper that flattens Fragments." }
|
|
1763
1553
|
},
|
|
1764
1554
|
applies: (file) => file.side === "app",
|
|
1765
1555
|
create: ({ report, visitTemplate }) => {
|
|
@@ -1797,7 +1587,7 @@ var bundle_default = defineRule({
|
|
|
1797
1587
|
} },
|
|
1798
1588
|
additionalProperties: false
|
|
1799
1589
|
}],
|
|
1800
|
-
messages: { heavy: "
|
|
1590
|
+
messages: { heavy: "frontend-core imports every section and block up front, and plugins run on every page, so '{{name}}' lands in the chunk every page loads. Load it with import() or defineAsyncComponent() instead." }
|
|
1801
1591
|
},
|
|
1802
1592
|
applies: (file) => file.kind === "section" || file.kind === "block" || file.kind === "app-plugin",
|
|
1803
1593
|
create: ({ context, report }) => {
|
|
@@ -1812,6 +1602,21 @@ var bundle_default = defineRule({
|
|
|
1812
1602
|
} };
|
|
1813
1603
|
}
|
|
1814
1604
|
});
|
|
1605
|
+
const readPackageFile = cachedReader((text) => {
|
|
1606
|
+
const json = JSON.parse(text);
|
|
1607
|
+
return {
|
|
1608
|
+
name: json.name,
|
|
1609
|
+
dependencies: new Set([
|
|
1610
|
+
json.dependencies,
|
|
1611
|
+
json.devDependencies,
|
|
1612
|
+
json.peerDependencies,
|
|
1613
|
+
json.optionalDependencies
|
|
1614
|
+
].flatMap((deps) => Object.keys(deps ?? {})))
|
|
1615
|
+
};
|
|
1616
|
+
});
|
|
1617
|
+
function readPackage(root) {
|
|
1618
|
+
return readPackageFile(join(root, "package.json"));
|
|
1619
|
+
}
|
|
1815
1620
|
function isRuntimeConfig(node, variables) {
|
|
1816
1621
|
if (node.type === "CallExpression") return node.callee.type === "Identifier" && node.callee.name === "useRuntimeConfig";
|
|
1817
1622
|
if (node.type === "Identifier") return variables.has(node.name);
|
|
@@ -1856,7 +1661,7 @@ var public_default = defineRule({
|
|
|
1856
1661
|
type: "problem",
|
|
1857
1662
|
docs: { description: "Disallow copying module options into the public runtime config, which reaches the browser." },
|
|
1858
1663
|
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." }
|
|
1664
|
+
messages: { options: "runtimeConfig.public reaches the browser, and with it every token or secret among the module options. Copy only the options meant to be public, e.g. options.storefrontUrl." }
|
|
1860
1665
|
},
|
|
1861
1666
|
applies: (file) => file.side === "build",
|
|
1862
1667
|
create: ({ report }) => ({ AssignmentExpression: (node) => {
|
|
@@ -1867,30 +1672,13 @@ var public_default = defineRule({
|
|
|
1867
1672
|
walk(node.right, (child) => {
|
|
1868
1673
|
if (child.type !== "Identifier" || child.name !== options || !isReference(child)) return;
|
|
1869
1674
|
const { parent } = child;
|
|
1870
|
-
if (
|
|
1675
|
+
if (parent.type !== "MemberExpression" || parent.object !== child) report({
|
|
1871
1676
|
node: child,
|
|
1872
1677
|
messageId: "options"
|
|
1873
1678
|
});
|
|
1874
1679
|
});
|
|
1875
1680
|
} })
|
|
1876
1681
|
});
|
|
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
1682
|
const LABELS = {
|
|
1895
1683
|
"section": "section",
|
|
1896
1684
|
"block": "block",
|
|
@@ -1925,13 +1713,7 @@ const ABOVE_SERVER_UTILS = /* @__PURE__ */ new Set([
|
|
|
1925
1713
|
"orchestr-file",
|
|
1926
1714
|
"media-library"
|
|
1927
1715
|
]);
|
|
1928
|
-
|
|
1929
|
-
"handler",
|
|
1930
|
-
"orchestr-file",
|
|
1931
|
-
"middleware",
|
|
1932
|
-
"media-library"
|
|
1933
|
-
]);
|
|
1934
|
-
function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
1716
|
+
function findViolation(importer, target, typeOnly) {
|
|
1935
1717
|
if (importer.side === "app" && target.side === "server") return { messageId: "appImportsServer" };
|
|
1936
1718
|
if (importer.side === "server" && target.side === "app") return { messageId: "serverImportsApp" };
|
|
1937
1719
|
if (importer.side === "shared" && (target.side === "app" || target.side === "server")) return {
|
|
@@ -1942,25 +1724,10 @@ function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
|
1942
1724
|
if (target.kind === "handler") return { messageId: "handlerImported" };
|
|
1943
1725
|
if (typeOnly) return;
|
|
1944
1726
|
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
1727
|
if (importer.kind === "server-util" && ABOVE_SERVER_UTILS.has(target.kind)) return {
|
|
1948
1728
|
messageId: "serverUtilImportsUp",
|
|
1949
1729
|
data: { kind: LABELS[target.kind] }
|
|
1950
1730
|
};
|
|
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
1731
|
if (importer.kind === "app-util" && ABOVE_APP_UTILS.has(target.kind)) return {
|
|
1965
1732
|
messageId: "appUtilImportsUp",
|
|
1966
1733
|
data: { kind: LABELS[target.kind] }
|
|
@@ -1974,15 +1741,8 @@ function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
|
1974
1741
|
var layers_default = defineRule({
|
|
1975
1742
|
meta: {
|
|
1976
1743
|
type: "problem",
|
|
1977
|
-
docs: { description: "Enforce the layers of a Laioutr app: app, server and shared code, orchestr handlers,
|
|
1978
|
-
schema: [
|
|
1979
|
-
type: "object",
|
|
1980
|
-
properties: { sharedDomains: {
|
|
1981
|
-
type: "array",
|
|
1982
|
-
items: { type: "string" }
|
|
1983
|
-
} },
|
|
1984
|
-
additionalProperties: false
|
|
1985
|
-
}],
|
|
1744
|
+
docs: { description: "Enforce the layers of a Laioutr app: app, server and shared code, build-time code, orchestr handlers, API clients and utils." },
|
|
1745
|
+
schema: [],
|
|
1986
1746
|
messages: {
|
|
1987
1747
|
appImportsServer: "App code can't import server code. Move what both sides need to src/runtime/shared/.",
|
|
1988
1748
|
serverImportsApp: "Server code can't import app code. Move what both sides need to src/runtime/shared/.",
|
|
@@ -1990,11 +1750,7 @@ var layers_default = defineRule({
|
|
|
1990
1750
|
runtimeImportsBuild: "Runtime code can't import build-time code from src/; it isn't part of the runtime bundle. Type imports are fine.",
|
|
1991
1751
|
handlerImported: "Orchestr handlers are registered by laioutr, never imported. Move the shared code to server/utils/.",
|
|
1992
1752
|
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
1753
|
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
1754
|
appUtilImportsUp: "App utils are the bottom layer and can't import a {{kind}}. Type imports are fine.",
|
|
1999
1755
|
composableImportsUp: "Composables can't import a {{kind}}. Type imports are fine.",
|
|
2000
1756
|
componentImportsSection: "Components can't import sections; sections compose components.",
|
|
@@ -2003,7 +1759,6 @@ var layers_default = defineRule({
|
|
|
2003
1759
|
},
|
|
2004
1760
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
2005
1761
|
create: ({ context, file: importer, report }) => {
|
|
2006
|
-
const { sharedDomains = [] } = context.options[0] ?? {};
|
|
2007
1762
|
const checkImport = (node, source, typeOnly) => {
|
|
2008
1763
|
if (typeof source !== "string") return;
|
|
2009
1764
|
if (source.startsWith("node:")) {
|
|
@@ -2016,7 +1771,7 @@ var layers_default = defineRule({
|
|
|
2016
1771
|
}
|
|
2017
1772
|
const path = resolveImport(context.filename, source);
|
|
2018
1773
|
const target = path ? classify(path) : void 0;
|
|
2019
|
-
const violation = target && findViolation(importer, target, typeOnly
|
|
1774
|
+
const violation = target && findViolation(importer, target, typeOnly);
|
|
2020
1775
|
if (violation) report({
|
|
2021
1776
|
node,
|
|
2022
1777
|
...violation
|
|
@@ -2045,55 +1800,40 @@ const plugin = {
|
|
|
2045
1800
|
},
|
|
2046
1801
|
rules: {
|
|
2047
1802
|
"layers": layers_default,
|
|
2048
|
-
"known-folders": folders_default,
|
|
2049
1803
|
"heavy-imports": bundle_default,
|
|
2050
1804
|
"config-keys": keys_default,
|
|
2051
1805
|
"public-config": public_default,
|
|
2052
1806
|
"orchestr-files": files_default$1,
|
|
2053
1807
|
"orchestr-cookies": cookies_default,
|
|
2054
|
-
"handler-domains": domains_default,
|
|
2055
1808
|
"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
1809
|
"middleware-files": files_default,
|
|
2061
1810
|
"initware-throws": throws_default,
|
|
2062
|
-
"orchestr-meta": meta_default,
|
|
2063
1811
|
"token-namespaces": namespaces_default,
|
|
2064
|
-
"token-nullable": nullable_default,
|
|
2065
1812
|
"definition-folder": folder_default,
|
|
2066
|
-
"definition-prefix": prefix_default,
|
|
2067
1813
|
"component-name": name_default,
|
|
2068
1814
|
"definition-description": description_default,
|
|
2069
|
-
"root-id": id_default,
|
|
2070
1815
|
"single-root": root_default,
|
|
2071
1816
|
"slot-children": slots_default,
|
|
2072
1817
|
"reserved-field-names": reserved_default,
|
|
2073
1818
|
"field-name-case": casing_default,
|
|
2074
1819
|
"required-fields": required_default,
|
|
2075
|
-
"schema-groups": groups_default,
|
|
2076
1820
|
"unused-fields": unused_default,
|
|
2077
1821
|
"dead-fallbacks": fallbacks_default,
|
|
2078
1822
|
"button-type": button_default,
|
|
2079
|
-
"ui-kit-tags": tags_default,
|
|
2080
1823
|
"mutation-errors": mutations_default,
|
|
2081
|
-
"orchestr-store": store_default,
|
|
2082
1824
|
"resolve-result": resolve_default,
|
|
2083
1825
|
"hand-built-links": paths_default,
|
|
2084
|
-
"internal-anchors": anchors_default
|
|
2085
|
-
"money": money_default
|
|
1826
|
+
"internal-anchors": anchors_default
|
|
2086
1827
|
}
|
|
2087
1828
|
};
|
|
2088
1829
|
async function larrylint(options = {}) {
|
|
2089
|
-
const {
|
|
1830
|
+
const { heavyPackages } = await loadLarrylintConfig(options.cwd);
|
|
2090
1831
|
return [{
|
|
2091
1832
|
name: "larrylint",
|
|
2092
1833
|
files: ["**/src/**/*.{ts,mts,cts,js,mjs,cjs,vue}"],
|
|
2093
1834
|
plugins: { larrylint: plugin },
|
|
2094
1835
|
rules: {
|
|
2095
1836
|
...Object.fromEntries(Object.keys(plugin.rules).map((id) => [`larrylint/${id}`, "error"])),
|
|
2096
|
-
"larrylint/layers": ["error", { sharedDomains }],
|
|
2097
1837
|
"larrylint/heavy-imports": ["error", { packages: heavyPackages }]
|
|
2098
1838
|
}
|
|
2099
1839
|
}];
|