larrylint 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -56
- package/dist/_chunks/check2.mjs +1 -1
- package/dist/_chunks/init.mjs +1 -1
- package/dist/_chunks/package.mjs +1 -1
- package/dist/_chunks/preset.mjs +609 -867
- package/dist/index.d.mts +1 -17
- package/package.json +1 -1
package/dist/_chunks/preset.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { name, version } from "./package.mjs";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { loadConfig } from "c12";
|
|
4
|
-
import { basename, dirname, join, normalize, parse, relative, resolve } from "pathe";
|
|
5
4
|
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, normalize, parse, relative, resolve } from "pathe";
|
|
6
6
|
import tsParser, { parse as parse$1 } from "@typescript-eslint/parser";
|
|
7
7
|
import { parse as parse$2 } from "vue-eslint-parser";
|
|
8
8
|
function defineLarrylintConfig(config) {
|
|
@@ -17,10 +17,7 @@ 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
|
-
};
|
|
20
|
+
return { heavyPackages: config.heavyPackages ?? [] };
|
|
24
21
|
}
|
|
25
22
|
const EXTENSIONS = [
|
|
26
23
|
"",
|
|
@@ -82,11 +79,63 @@ function findPackage(from, name) {
|
|
|
82
79
|
if (dirname(folder) === folder) return;
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
|
-
const
|
|
82
|
+
const BASELINE_FILE = "larrylint-baseline.json";
|
|
83
|
+
const readBaselineFile = cachedReader((text) => JSON.parse(text));
|
|
84
|
+
function readBaseline(root) {
|
|
85
|
+
return readBaselineFile(join(root, "larrylint-baseline.json")) ?? {};
|
|
86
|
+
}
|
|
87
|
+
function writeBaseline(root, baseline) {
|
|
88
|
+
const file = join(root, BASELINE_FILE);
|
|
89
|
+
const files = Object.keys(baseline).sort();
|
|
90
|
+
if (files.length === 0) {
|
|
91
|
+
rmSync(file, { force: true });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const sorted = {};
|
|
95
|
+
for (const path of files) sorted[path] = Object.fromEntries(Object.entries(baseline[path]).sort(([a], [b]) => a.localeCompare(b)));
|
|
96
|
+
writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
97
|
+
}
|
|
98
|
+
function countViolations(baseline) {
|
|
99
|
+
return Object.values(baseline).flatMap((rules) => Object.values(rules)).reduce((sum, count) => sum + count, 0);
|
|
100
|
+
}
|
|
101
|
+
function applyBaseline(cwd, results) {
|
|
102
|
+
const baseline = readBaseline(cwd);
|
|
103
|
+
const violations = {};
|
|
104
|
+
let baselined = 0;
|
|
105
|
+
let improved = 0;
|
|
106
|
+
const filtered = results.map((result) => {
|
|
107
|
+
const path = relative(cwd, result.filePath);
|
|
108
|
+
const perRule = {};
|
|
109
|
+
const relevant = result.messages.filter((message) => message.fatal || message.ruleId?.startsWith("larrylint/"));
|
|
110
|
+
for (const { ruleId } of relevant) if (ruleId) perRule[ruleId] = (perRule[ruleId] ?? 0) + 1;
|
|
111
|
+
if (Object.keys(perRule).length > 0) violations[path] = perRule;
|
|
112
|
+
const messages = relevant.filter(({ ruleId }) => !ruleId || perRule[ruleId] > (baseline[path]?.[ruleId] ?? 0));
|
|
113
|
+
baselined += relevant.length - messages.length;
|
|
114
|
+
return {
|
|
115
|
+
...result,
|
|
116
|
+
messages,
|
|
117
|
+
errorCount: messages.filter((message) => message.severity === 2).length,
|
|
118
|
+
fatalErrorCount: messages.filter((message) => message.fatal).length,
|
|
119
|
+
warningCount: messages.filter((message) => message.severity === 1).length,
|
|
120
|
+
fixableErrorCount: messages.filter((message) => message.severity === 2 && message.fix).length,
|
|
121
|
+
fixableWarningCount: messages.filter((message) => message.severity === 1 && message.fix).length
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
for (const [path, rules] of Object.entries(baseline)) for (const [rule, allowed] of Object.entries(rules)) if ((violations[path]?.[rule] ?? 0) < allowed) improved++;
|
|
125
|
+
return {
|
|
126
|
+
results: filtered,
|
|
127
|
+
violations,
|
|
128
|
+
baselined,
|
|
129
|
+
improved
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const HANDLER_FILE = /\.(query|resolver|link|action|templates?|page-index)(?:\.[cm]?[jt]s)?$/;
|
|
86
133
|
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
87
134
|
const APP_FOLDERS = {
|
|
88
135
|
"sections": "section",
|
|
136
|
+
"section": "section",
|
|
89
137
|
"blocks": "block",
|
|
138
|
+
"block": "block",
|
|
90
139
|
"components": "component",
|
|
91
140
|
"composables": "composable",
|
|
92
141
|
"utils": "app-util",
|
|
@@ -129,10 +178,6 @@ function classify(file) {
|
|
|
129
178
|
test
|
|
130
179
|
};
|
|
131
180
|
}
|
|
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
181
|
function classifyRuntime(parts) {
|
|
137
182
|
const [side, folder = "", ...rest] = parts;
|
|
138
183
|
if (parts.length < 2 || side !== "app" && side !== "server" && side !== "shared") return { side: "other" };
|
|
@@ -144,75 +189,21 @@ function classifyRuntime(parts) {
|
|
|
144
189
|
side,
|
|
145
190
|
kind: APP_FOLDERS[folder] ?? "other"
|
|
146
191
|
};
|
|
147
|
-
const nested = rest.length > 1;
|
|
148
192
|
if (folder === "orchestr") {
|
|
149
|
-
if (
|
|
193
|
+
if (rest.length > 1 && rest[0] === "plugins") return {
|
|
150
194
|
side,
|
|
151
195
|
kind: "orchestr-plugin"
|
|
152
196
|
};
|
|
153
|
-
const handler = HANDLER_FILE.exec(parts.at(-1))?.[1];
|
|
197
|
+
const handler = HANDLER_FILE.exec(parts.at(-1))?.[1]?.replace("templates", "template");
|
|
154
198
|
return {
|
|
155
199
|
side,
|
|
156
200
|
kind: handler ? "handler" : "orchestr-file",
|
|
157
|
-
handler
|
|
158
|
-
domain: nested ? rest[0] : void 0
|
|
201
|
+
handler
|
|
159
202
|
};
|
|
160
203
|
}
|
|
161
|
-
const kind = SERVER_FOLDERS[folder] ?? "other";
|
|
162
204
|
return {
|
|
163
205
|
side,
|
|
164
|
-
kind
|
|
165
|
-
domain: kind === "server-util" && nested ? rest[0] : void 0
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
const BASELINE_FILE = "larrylint-baseline.json";
|
|
169
|
-
const readBaselineFile = cachedReader((text) => JSON.parse(text));
|
|
170
|
-
function readBaseline(root) {
|
|
171
|
-
return readBaselineFile(join(root, "larrylint-baseline.json")) ?? {};
|
|
172
|
-
}
|
|
173
|
-
function writeBaseline(root, baseline) {
|
|
174
|
-
const file = join(root, BASELINE_FILE);
|
|
175
|
-
const files = Object.keys(baseline).sort();
|
|
176
|
-
if (files.length === 0) {
|
|
177
|
-
rmSync(file, { force: true });
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const sorted = {};
|
|
181
|
-
for (const path of files) sorted[path] = Object.fromEntries(Object.entries(baseline[path]).sort(([a], [b]) => a.localeCompare(b)));
|
|
182
|
-
writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
183
|
-
}
|
|
184
|
-
function countViolations(baseline) {
|
|
185
|
-
return Object.values(baseline).flatMap((rules) => Object.values(rules)).reduce((sum, count) => sum + count, 0);
|
|
186
|
-
}
|
|
187
|
-
function applyBaseline(cwd, results) {
|
|
188
|
-
const baseline = readBaseline(cwd);
|
|
189
|
-
const violations = {};
|
|
190
|
-
let baselined = 0;
|
|
191
|
-
let improved = 0;
|
|
192
|
-
const filtered = results.map((result) => {
|
|
193
|
-
const path = relative(cwd, result.filePath);
|
|
194
|
-
const perRule = {};
|
|
195
|
-
const relevant = result.messages.filter((message) => message.fatal || message.ruleId?.startsWith("larrylint/"));
|
|
196
|
-
for (const { ruleId } of relevant) if (ruleId) perRule[ruleId] = (perRule[ruleId] ?? 0) + 1;
|
|
197
|
-
if (Object.keys(perRule).length > 0) violations[path] = perRule;
|
|
198
|
-
const messages = relevant.filter(({ ruleId }) => !ruleId || perRule[ruleId] > (baseline[path]?.[ruleId] ?? 0));
|
|
199
|
-
baselined += relevant.length - messages.length;
|
|
200
|
-
return {
|
|
201
|
-
...result,
|
|
202
|
-
messages,
|
|
203
|
-
errorCount: messages.filter((message) => message.severity === 2).length,
|
|
204
|
-
fatalErrorCount: messages.filter((message) => message.fatal).length,
|
|
205
|
-
warningCount: messages.filter((message) => message.severity === 1).length,
|
|
206
|
-
fixableErrorCount: messages.filter((message) => message.severity === 2 && message.fix).length,
|
|
207
|
-
fixableWarningCount: messages.filter((message) => message.severity === 1 && message.fix).length
|
|
208
|
-
};
|
|
209
|
-
});
|
|
210
|
-
for (const [path, rules] of Object.entries(baseline)) for (const [rule, allowed] of Object.entries(rules)) if ((violations[path]?.[rule] ?? 0) < allowed) improved++;
|
|
211
|
-
return {
|
|
212
|
-
results: filtered,
|
|
213
|
-
violations,
|
|
214
|
-
baselined,
|
|
215
|
-
improved
|
|
206
|
+
kind: SERVER_FOLDERS[folder] ?? "other"
|
|
216
207
|
};
|
|
217
208
|
}
|
|
218
209
|
function defineRule(rule) {
|
|
@@ -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
|
-
docs: { description: "Require error handling
|
|
399
|
+
docs: { description: "Require error handling for awaited or dropped `mutateAsync()` calls" },
|
|
419
400
|
schema: [],
|
|
420
|
-
messages: {
|
|
401
|
+
messages: {
|
|
402
|
+
unhandled: "If this mutation fails, an error replaces the whole section or block. Catch it with try/catch or .catch().",
|
|
403
|
+
dropped: "Nothing catches errors of this mutation. Add .catch(), or await it in a try/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 links" },
|
|
611
|
+
schema: [],
|
|
612
|
+
messages: { anchor: "A plain <a> reloads the whole page. 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 page paths built by hand" },
|
|
650
|
+
schema: [],
|
|
651
|
+
messages: { path: "Hand-built page paths differ per language and market. Use 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
|
|
684
|
+
docs: { description: "Disallow checking the result of `linkResolver.resolve()`" },
|
|
689
685
|
schema: [],
|
|
690
|
-
messages: {
|
|
686
|
+
messages: {
|
|
687
|
+
tested: "linkResolver.resolve() never returns an empty value, so this check misses broken links. Use resolveOrThrow() in a try/catch instead.",
|
|
688
|
+
branch: "linkResolver.resolve() never returns an empty value, so the checks on '{{name}}' miss broken links. Use resolveOrThrow() in a try/catch 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
|
|
768
|
+
docs: { description: "Disallow `type` on ui-kit buttons" },
|
|
756
769
|
schema: [],
|
|
757
|
-
messages: { buttonType: "
|
|
770
|
+
messages: { buttonType: "<{{tag}}> ignores type. Use button-type instead." }
|
|
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
|
-
function inMiddleware(node) {
|
|
904
|
-
for (let parent = node.parent; parent; parent = parent.parent) if (isFunction(parent) && parent.parent?.type === "CallExpression" && readMiddleware(parent.parent)?.callback === parent) return true;
|
|
905
|
-
return false;
|
|
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
|
+
}
|
|
906
822
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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;
|
|
916
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: "Require cookies and headers to be set in `extendRequest()` or action handlers, with frontend-core's managed cookie functions" },
|
|
921
865
|
schema: [],
|
|
922
866
|
messages: {
|
|
923
|
-
|
|
924
|
-
|
|
867
|
+
streamed: "The headers may already be sent when {{name}}() runs here. Call it in extendRequest() or an action handler instead.",
|
|
868
|
+
managed: "Use {{managed}}() instead, so it also works in the Studio preview."
|
|
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({
|
|
878
|
+
node,
|
|
879
|
+
messageId: "streamed",
|
|
880
|
+
data: { name: write.name }
|
|
881
|
+
});
|
|
882
|
+
else if (managed && (method === "extendRequest" || !method && file.handler === "action")) report({
|
|
931
883
|
node,
|
|
932
|
-
messageId:
|
|
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: "
|
|
905
|
+
docs: { description: "Disallow files other than handlers in `orchestr/`" },
|
|
950
906
|
schema: [],
|
|
951
|
-
messages: { notAHandler: "
|
|
907
|
+
messages: { notAHandler: "Only handlers belong in orchestr/. Move this file to server/utils/, or give it a handler suffix like .query.ts or .action.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: "Export the handler as default, or it will fail the build." }
|
|
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({
|
|
@@ -1050,9 +950,9 @@ const MIDDLEWARE = /* @__PURE__ */ new Set([
|
|
|
1050
950
|
var files_default = defineRule({
|
|
1051
951
|
meta: {
|
|
1052
952
|
type: "suggestion",
|
|
1053
|
-
docs: { description: "
|
|
953
|
+
docs: { description: "Disallow files other than middleware in `server/middleware/`" },
|
|
1054
954
|
schema: [],
|
|
1055
|
-
messages: { helper: "
|
|
955
|
+
messages: { helper: "Only middleware belongs in server/middleware/. Move this file to server/utils/." }
|
|
1056
956
|
},
|
|
1057
957
|
applies: (file) => file.kind === "middleware",
|
|
1058
958
|
create: ({ report }) => {
|
|
@@ -1073,50 +973,14 @@ 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",
|
|
1115
|
-
docs: { description: "Disallow throwing in extendRequest()
|
|
979
|
+
docs: { description: "Disallow throwing in `extendRequest()`" },
|
|
1116
980
|
schema: [],
|
|
1117
981
|
messages: {
|
|
1118
|
-
throws: "extendRequest()
|
|
1119
|
-
throwsInCall: "{{name}}() can throw ({{location}})
|
|
982
|
+
throws: "A throw in extendRequest() breaks every page. Throw later instead, e.g. when the client is used.",
|
|
983
|
+
throwsInCall: "{{name}}() can throw ({{location}}), and a throw in extendRequest() breaks every page. Catch it here."
|
|
1120
984
|
}
|
|
1121
985
|
},
|
|
1122
986
|
applies: (file) => file.side === "server",
|
|
@@ -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();
|
|
@@ -1206,9 +1034,9 @@ function canonicalNamespaces(root) {
|
|
|
1206
1034
|
var namespaces_default = defineRule({
|
|
1207
1035
|
meta: {
|
|
1208
1036
|
type: "problem",
|
|
1209
|
-
docs: { description: "
|
|
1037
|
+
docs: { description: "Disallow your own token ids in 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. Use your app's own 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
|
-
while (current?.type === "CallExpression" && current.callee.type === "MemberExpression" && current.callee.property.type === "Identifier") {
|
|
1228
|
-
const [shape] = current.arguments;
|
|
1229
|
-
if (current.callee.property.name === "object" && shape?.type === "ObjectExpression") return shape;
|
|
1230
|
-
current = current.callee.object;
|
|
1231
|
-
}
|
|
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;
|
|
1232
1055
|
}
|
|
1233
|
-
function
|
|
1234
|
-
|
|
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;
|
|
1062
|
+
}
|
|
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 = /* @__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,37 +1122,65 @@ 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.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
|
-
docs: { description: "Require a studio.description on section and block definitions
|
|
1158
|
+
docs: { description: "Require a `studio.description` on section and block definitions" },
|
|
1311
1159
|
schema: [],
|
|
1312
|
-
messages: { description: "
|
|
1160
|
+
messages: { description: "Add a studio.description, so editors and AI agents know what this {{kind}} is for." }
|
|
1313
1161
|
},
|
|
1314
1162
|
applies: (file) => file.side === "app",
|
|
1315
1163
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1316
|
-
const
|
|
1317
|
-
if (!options) return;
|
|
1318
|
-
const studio = findProperty(options, "studio");
|
|
1164
|
+
const definition = readDefinition(node);
|
|
1165
|
+
if (!definition?.options) return;
|
|
1166
|
+
const studio = findProperty(definition.options, "studio");
|
|
1319
1167
|
if (studio && studio.type !== "ObjectExpression") return;
|
|
1320
1168
|
const description = studio && findProperty(studio, "description");
|
|
1321
1169
|
if (!description || description.type === "Literal" && String(description.value ?? "").trim() === "") report({
|
|
1322
1170
|
node: node.callee,
|
|
1323
|
-
messageId: "description"
|
|
1171
|
+
messageId: "description",
|
|
1172
|
+
data: { kind: DEFINERS[definition.definer].kind }
|
|
1324
1173
|
});
|
|
1325
1174
|
} })
|
|
1326
1175
|
});
|
|
1327
1176
|
var folder_default = defineRule({
|
|
1328
1177
|
meta: {
|
|
1329
1178
|
type: "problem",
|
|
1330
|
-
docs: { description: "
|
|
1179
|
+
docs: { description: "Require sections in `app/sections/` and blocks in `app/blocks/`, and a definition in every `.vue` there" },
|
|
1331
1180
|
schema: [],
|
|
1332
1181
|
messages: {
|
|
1333
|
-
notDefined: "
|
|
1334
|
-
wrongFolder: "{{definer}}() belongs in app/{{folder}}/."
|
|
1182
|
+
notDefined: "Every .vue file in this folder needs {{definer}}(). Add it, or move the file to components/.",
|
|
1183
|
+
wrongFolder: "{{definer}}() belongs in app/{{folder}}/. Move the file there."
|
|
1335
1184
|
}
|
|
1336
1185
|
},
|
|
1337
1186
|
applies: (file) => file.side === "app",
|
|
@@ -1354,18 +1203,11 @@ var folder_default = defineRule({
|
|
|
1354
1203
|
},
|
|
1355
1204
|
"Program:exit": () => {
|
|
1356
1205
|
const expected = expectedDefiner(file.kind);
|
|
1357
|
-
if (expected && file.path.endsWith(".vue") && !definers.has(expected)) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
data: {
|
|
1363
|
-
folder,
|
|
1364
|
-
kind,
|
|
1365
|
-
definer: expected
|
|
1366
|
-
}
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1206
|
+
if (expected && file.path.endsWith(".vue") && !definers.has(expected)) report({
|
|
1207
|
+
loc: FILE_START,
|
|
1208
|
+
messageId: "notDefined",
|
|
1209
|
+
data: { definer: expected }
|
|
1210
|
+
});
|
|
1369
1211
|
}
|
|
1370
1212
|
};
|
|
1371
1213
|
}
|
|
@@ -1373,7 +1215,7 @@ var folder_default = defineRule({
|
|
|
1373
1215
|
var name_default = defineRule({
|
|
1374
1216
|
meta: {
|
|
1375
1217
|
type: "problem",
|
|
1376
|
-
docs: { description: "Require the `component` of a
|
|
1218
|
+
docs: { description: "Require the `component` of a definition to match its file name" },
|
|
1377
1219
|
schema: [],
|
|
1378
1220
|
messages: { componentName: "component: '{{actual}}' must match the file name '{{expected}}'." }
|
|
1379
1221
|
},
|
|
@@ -1391,118 +1233,103 @@ var name_default = defineRule({
|
|
|
1391
1233
|
});
|
|
1392
1234
|
} })
|
|
1393
1235
|
});
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
applies: (file) => file.kind === "section" || file.kind === "block",
|
|
1402
|
-
create: ({ file, report }) => ({ CallExpression: (node) => {
|
|
1403
|
-
const definition = readDefinition(node);
|
|
1404
|
-
if (!definition) return;
|
|
1405
|
-
const { kind, prefix } = DEFINERS[definition.definer];
|
|
1406
|
-
const { name } = parse(file.path);
|
|
1407
|
-
if (file.kind === kind && !name.startsWith(prefix)) report({
|
|
1408
|
-
node: node.callee,
|
|
1409
|
-
messageId: "prefix",
|
|
1410
|
-
data: {
|
|
1411
|
-
kind,
|
|
1412
|
-
prefix,
|
|
1413
|
-
suggestion: prefixName(name, prefix)
|
|
1414
|
-
}
|
|
1415
|
-
});
|
|
1416
|
-
} })
|
|
1417
|
-
});
|
|
1418
|
-
const FALSE_WHEN_UNSET = /* @__PURE__ */ new Set([
|
|
1419
|
-
"checkbox",
|
|
1420
|
-
"select",
|
|
1421
|
-
"radio",
|
|
1422
|
-
"toggle_button"
|
|
1423
|
-
]);
|
|
1424
|
-
const EMPTY_WHEN_UNSET = /* @__PURE__ */ new Set(["text", "textarea"]);
|
|
1236
|
+
const MESSAGES = {
|
|
1237
|
+
checkbox: "checkbox",
|
|
1238
|
+
content_alignment: "alignment",
|
|
1239
|
+
text: "text",
|
|
1240
|
+
textarea: "text",
|
|
1241
|
+
secret: "text"
|
|
1242
|
+
};
|
|
1425
1243
|
var fallbacks_default = defineRule({
|
|
1426
1244
|
meta: {
|
|
1427
1245
|
type: "problem",
|
|
1428
|
-
docs: { description: "Disallow fallbacks
|
|
1246
|
+
docs: { description: "Disallow `??` fallbacks on fields that frontend-core always fills" },
|
|
1429
1247
|
schema: [],
|
|
1430
1248
|
messages: {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1249
|
+
picker: "This fallback is never used: an unset {{type}} gets its first option, {{fill}}. To use {{fallback}}, make it the first option.",
|
|
1250
|
+
alignment: "This fallback is never used: an unset content_alignment is {{fill}}. Remove the fallback.",
|
|
1251
|
+
checkbox: "This fallback is never used: an unset checkbox is false. To make true the default, invert the field, e.g. hideIcon instead of showIcon.",
|
|
1252
|
+
text: "An unset {{type}} field is '', so ?? never falls back. Use || instead."
|
|
1434
1253
|
}
|
|
1435
1254
|
},
|
|
1436
1255
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1437
1256
|
create: ({ context, report, visitTemplate }) => {
|
|
1438
|
-
const
|
|
1439
|
-
const propsVariable = findPropsVariable(
|
|
1440
|
-
const
|
|
1257
|
+
const program = context.sourceCode.ast;
|
|
1258
|
+
const propsVariable = findPropsVariable(program);
|
|
1259
|
+
const fills = /* @__PURE__ */ new Map();
|
|
1260
|
+
const fallbacks = [];
|
|
1441
1261
|
const propOf = (node, template) => {
|
|
1442
1262
|
if (template && node.type === "Identifier") return node.name;
|
|
1443
1263
|
const props = node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.object.type === "Identifier" ? node.object.name : void 0;
|
|
1444
1264
|
return props && (props === propsVariable || template && props === "$props") && node.type === "MemberExpression" && node.property.type === "Identifier" ? node.property.name : void 0;
|
|
1445
1265
|
};
|
|
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
|
-
});
|
|
1266
|
+
const collect = (template) => ({ LogicalExpression: (node) => {
|
|
1267
|
+
const prop = node.operator === "??" ? propOf(node.left, template) : void 0;
|
|
1268
|
+
if (prop && !isNullish(node.right)) fallbacks.push({
|
|
1269
|
+
node,
|
|
1270
|
+
prop
|
|
1271
|
+
});
|
|
1272
|
+
} });
|
|
1468
1273
|
visitTemplate(collect(true));
|
|
1469
1274
|
return {
|
|
1470
1275
|
...collect(false),
|
|
1471
1276
|
"CallExpression": (node) => {
|
|
1472
1277
|
const definition = readDefinition(node);
|
|
1473
|
-
for (const
|
|
1278
|
+
for (const field of definition ? readFields(definition) : []) {
|
|
1279
|
+
const fill = field.name && field.type && !field.as ? fillValue(field, program, context.filename) : void 0;
|
|
1280
|
+
if (fill) fills.set(field.name.value, {
|
|
1281
|
+
type: field.type,
|
|
1282
|
+
value: fill.value
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1474
1285
|
},
|
|
1475
1286
|
"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({
|
|
1287
|
+
for (const { node, prop } of fallbacks) {
|
|
1288
|
+
const field = fills.get(prop);
|
|
1289
|
+
if (!field || node.right.type === "Literal" && node.right.value === field.value) continue;
|
|
1290
|
+
const messageId = MESSAGES[field.type] ?? "picker";
|
|
1291
|
+
const fill = typeof field.value === "string" ? `'${field.value}'` : String(field.value);
|
|
1292
|
+
report({
|
|
1484
1293
|
node,
|
|
1485
|
-
messageId
|
|
1294
|
+
messageId,
|
|
1486
1295
|
data: {
|
|
1487
|
-
type,
|
|
1488
|
-
|
|
1296
|
+
type: field.type,
|
|
1297
|
+
fill,
|
|
1298
|
+
fallback: context.sourceCode.getText(node.right)
|
|
1489
1299
|
}
|
|
1490
1300
|
});
|
|
1491
|
-
else if (type && EMPTY_WHEN_UNSET.has(type) && check === "??") report({
|
|
1492
|
-
node,
|
|
1493
|
-
messageId: "text",
|
|
1494
|
-
data: { type }
|
|
1495
|
-
});
|
|
1496
1301
|
}
|
|
1497
1302
|
}
|
|
1498
1303
|
};
|
|
1499
1304
|
}
|
|
1500
1305
|
});
|
|
1501
1306
|
const PROP_READ = /\.props\??\.([A-Z_$][\w$]*)|\.props\??\.?\[\s*['"]([^'"]+)['"]\s*\]|['"]([^'"]+)['"]\s+in\s+[\w$.?]+\.props\b/gi;
|
|
1502
|
-
const
|
|
1307
|
+
const PROPS_ALIAS = /\b(?:const|let|var)\s+([\w$]+)\s*=\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/g;
|
|
1308
|
+
const PROPS_DESTRUCTURING = /\{([^{}]*)\}\s*=\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/g;
|
|
1309
|
+
const PROPS_SPREAD = /\.\.\.\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/;
|
|
1310
|
+
const readSlotProps = cachedReader((text) => {
|
|
1311
|
+
const names = [...text.matchAll(PROP_READ)].map((match) => match[1] ?? match[2] ?? match[3]);
|
|
1312
|
+
let all = PROPS_SPREAD.test(text);
|
|
1313
|
+
for (const [, alias] of text.matchAll(PROPS_ALIAS)) {
|
|
1314
|
+
const name = alias.replaceAll("$", "\\$");
|
|
1315
|
+
const reads = new RegExp(`\\b${name}\\??\\.([A-Z_$][\\w$]*)|\\b${name}\\??\\.?\\[\\s*['"]([^'"]+)['"]\\s*\\]`, "gi");
|
|
1316
|
+
for (const match of text.matchAll(reads)) names.push(match[1] ?? match[2]);
|
|
1317
|
+
}
|
|
1318
|
+
for (const [, list] of text.matchAll(PROPS_DESTRUCTURING)) for (const entry of list.split(",").map((item) => item.trim()).filter(Boolean)) {
|
|
1319
|
+
all ||= entry.startsWith("...");
|
|
1320
|
+
names.push(entry.split(/[:=]/)[0].trim());
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
names,
|
|
1324
|
+
all
|
|
1325
|
+
};
|
|
1326
|
+
});
|
|
1503
1327
|
function slotProps(root) {
|
|
1504
1328
|
const folder = join(root, "src/runtime/app");
|
|
1505
|
-
const
|
|
1329
|
+
const result = {
|
|
1330
|
+
names: /* @__PURE__ */ new Set(),
|
|
1331
|
+
all: false
|
|
1332
|
+
};
|
|
1506
1333
|
let files;
|
|
1507
1334
|
try {
|
|
1508
1335
|
files = readdirSync(folder, {
|
|
@@ -1510,10 +1337,14 @@ function slotProps(root) {
|
|
|
1510
1337
|
encoding: "utf8"
|
|
1511
1338
|
}).filter((file) => /\.(?:vue|[cm]?[jt]s)$/.test(file));
|
|
1512
1339
|
} catch {
|
|
1513
|
-
return
|
|
1340
|
+
return result;
|
|
1514
1341
|
}
|
|
1515
|
-
for (const file of files)
|
|
1516
|
-
|
|
1342
|
+
for (const file of files) {
|
|
1343
|
+
const read = readSlotProps(join(folder, file));
|
|
1344
|
+
read?.names.forEach((name) => result.names.add(name));
|
|
1345
|
+
result.all ||= read?.all ?? false;
|
|
1346
|
+
}
|
|
1347
|
+
return result;
|
|
1517
1348
|
}
|
|
1518
1349
|
const NOT_READ = /* @__PURE__ */ new Set([
|
|
1519
1350
|
"info",
|
|
@@ -1523,9 +1354,9 @@ const NOT_READ = /* @__PURE__ */ new Set([
|
|
|
1523
1354
|
var unused_default = defineRule({
|
|
1524
1355
|
meta: {
|
|
1525
1356
|
type: "suggestion",
|
|
1526
|
-
docs: { description: "Disallow schema fields the
|
|
1357
|
+
docs: { description: "Disallow schema fields the component never uses" },
|
|
1527
1358
|
schema: [],
|
|
1528
|
-
messages: { unused: "
|
|
1359
|
+
messages: { unused: "The component never uses '{{name}}'. Use the field or remove it." }
|
|
1529
1360
|
},
|
|
1530
1361
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1531
1362
|
create: ({ context, file, report, visitTemplate }) => {
|
|
@@ -1533,6 +1364,7 @@ var unused_default = defineRule({
|
|
|
1533
1364
|
const used = /* @__PURE__ */ new Set();
|
|
1534
1365
|
const propsVariable = findPropsVariable(context.sourceCode.ast);
|
|
1535
1366
|
let hasProps = false;
|
|
1367
|
+
let block = false;
|
|
1536
1368
|
let escapes = false;
|
|
1537
1369
|
const track = (node) => {
|
|
1538
1370
|
const names = usedProps(node);
|
|
@@ -1551,7 +1383,10 @@ var unused_default = defineRule({
|
|
|
1551
1383
|
"CallExpression": (node) => {
|
|
1552
1384
|
hasProps ||= isDefineProps(node);
|
|
1553
1385
|
const definition = readDefinition(node);
|
|
1554
|
-
|
|
1386
|
+
if (!definition) return;
|
|
1387
|
+
block ||= definition.definer === "defineBlock";
|
|
1388
|
+
conditionFields(definition).forEach((name) => used.add(name));
|
|
1389
|
+
for (const { name, type, as } of readFields(definition)) if (name && !as && !NOT_READ.has(type ?? "")) fields.set(name.value, name.node);
|
|
1555
1390
|
},
|
|
1556
1391
|
"Identifier": (node) => {
|
|
1557
1392
|
const declaration = node.parent.type === "VariableDeclarator" && node.parent.id === node;
|
|
@@ -1560,8 +1395,9 @@ var unused_default = defineRule({
|
|
|
1560
1395
|
"Program:exit": () => {
|
|
1561
1396
|
if (!hasProps || escapes) return;
|
|
1562
1397
|
const unused = [...fields].filter(([name]) => !used.has(name));
|
|
1563
|
-
const readBySection = unused.length > 0 &&
|
|
1564
|
-
|
|
1398
|
+
const readBySection = unused.length > 0 && block ? slotProps(file.root) : void 0;
|
|
1399
|
+
if (readBySection?.all) return;
|
|
1400
|
+
for (const [name, node] of unused) if (!readBySection?.names.has(name)) report({
|
|
1565
1401
|
node,
|
|
1566
1402
|
messageId: "unused",
|
|
1567
1403
|
data: { name }
|
|
@@ -1570,67 +1406,40 @@ var unused_default = defineRule({
|
|
|
1570
1406
|
};
|
|
1571
1407
|
}
|
|
1572
1408
|
});
|
|
1573
|
-
const CAMEL_CASE = /^[a-z][a-zA-Z0-9]*$/;
|
|
1574
1409
|
var casing_default = defineRule({
|
|
1575
1410
|
meta: {
|
|
1576
1411
|
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." },
|
|
1412
|
+
docs: { description: "Disallow schema field names that Vue renames or rejects" },
|
|
1600
1413
|
schema: [],
|
|
1601
1414
|
messages: {
|
|
1602
|
-
|
|
1603
|
-
|
|
1415
|
+
hyphen: "Vue renames '{{name}}' to '{{suggestion}}', so the component never gets this field. Name it '{{suggestion}}'.",
|
|
1416
|
+
dollar: "Vue doesn't allow props that start with $, so the component never gets this field. Rename it."
|
|
1604
1417
|
}
|
|
1605
1418
|
},
|
|
1606
1419
|
applies: (file) => file.side === "app",
|
|
1607
1420
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1608
1421
|
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
|
-
}
|
|
1422
|
+
for (const { name } of definition ? readFields(definition) : []) if (name?.value.startsWith("$")) report({
|
|
1423
|
+
node: name.node,
|
|
1424
|
+
messageId: "dollar",
|
|
1425
|
+
data: { name: name.value }
|
|
1426
|
+
});
|
|
1427
|
+
else if (name?.value.includes("-")) report({
|
|
1428
|
+
node: name.node,
|
|
1429
|
+
messageId: "hyphen",
|
|
1430
|
+
data: {
|
|
1431
|
+
name: name.value,
|
|
1432
|
+
suggestion: camelCase(name.value)
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1626
1435
|
} })
|
|
1627
1436
|
});
|
|
1628
1437
|
var required_default = defineRule({
|
|
1629
1438
|
meta: {
|
|
1630
1439
|
type: "problem",
|
|
1631
|
-
docs: { description: "Disallow `required` on schema fields
|
|
1440
|
+
docs: { description: "Disallow `required` on schema fields" },
|
|
1632
1441
|
schema: [],
|
|
1633
|
-
messages: { required: "
|
|
1442
|
+
messages: { required: "Studio ignores required. Give the field a default instead." }
|
|
1634
1443
|
},
|
|
1635
1444
|
applies: (file) => file.side === "app",
|
|
1636
1445
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
@@ -1645,21 +1454,22 @@ var required_default = defineRule({
|
|
|
1645
1454
|
} })
|
|
1646
1455
|
});
|
|
1647
1456
|
const RESERVED = /* @__PURE__ */ new Set([
|
|
1648
|
-
"style",
|
|
1649
|
-
"class",
|
|
1650
1457
|
"key",
|
|
1651
1458
|
"ref",
|
|
1652
|
-
"
|
|
1653
|
-
"
|
|
1654
|
-
"
|
|
1655
|
-
"
|
|
1459
|
+
"ref_for",
|
|
1460
|
+
"ref_key",
|
|
1461
|
+
"class",
|
|
1462
|
+
"style"
|
|
1656
1463
|
]);
|
|
1657
1464
|
var reserved_default = defineRule({
|
|
1658
1465
|
meta: {
|
|
1659
1466
|
type: "problem",
|
|
1660
|
-
docs: { description: "Disallow
|
|
1467
|
+
docs: { description: "Disallow schema field names that Vue or frontend-core already use" },
|
|
1661
1468
|
schema: [],
|
|
1662
|
-
messages: {
|
|
1469
|
+
messages: {
|
|
1470
|
+
reserved: "Vue uses '{{name}}' itself, so the component never gets this field. Rename it.",
|
|
1471
|
+
slots: "frontend-core passes the blocks in the slots prop, so it overwrites this field. Rename it."
|
|
1472
|
+
}
|
|
1663
1473
|
},
|
|
1664
1474
|
applies: (file) => file.side === "app",
|
|
1665
1475
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
@@ -1669,36 +1479,12 @@ var reserved_default = defineRule({
|
|
|
1669
1479
|
messageId: "reserved",
|
|
1670
1480
|
data: { name: name.value }
|
|
1671
1481
|
});
|
|
1482
|
+
else if (name?.value === "slots" && definition?.definer === "defineSection") report({
|
|
1483
|
+
node: name.node,
|
|
1484
|
+
messageId: "slots"
|
|
1485
|
+
});
|
|
1672
1486
|
} })
|
|
1673
1487
|
});
|
|
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
1488
|
const parseVueFile = cachedReader((text) => {
|
|
1703
1489
|
try {
|
|
1704
1490
|
return {
|
|
@@ -1713,42 +1499,48 @@ const parseVueFile = cachedReader((text) => {
|
|
|
1713
1499
|
return;
|
|
1714
1500
|
}
|
|
1715
1501
|
});
|
|
1502
|
+
const OWN_ATTRS = /inheritAttrs\s*:\s*false/;
|
|
1716
1503
|
var root_default = defineRule({
|
|
1717
1504
|
meta: {
|
|
1718
1505
|
type: "problem",
|
|
1719
|
-
docs: { description: "Require sections and blocks to render one root element
|
|
1506
|
+
docs: { description: "Require sections and blocks to render one root element" },
|
|
1720
1507
|
schema: [],
|
|
1721
1508
|
messages: {
|
|
1722
|
-
roots: "
|
|
1723
|
-
componentRoots: "<{{name}}>
|
|
1509
|
+
roots: "Studio can't select a {{kind}} with more than one root element. Wrap the content in one element.",
|
|
1510
|
+
componentRoots: "<{{name}}> has more than one root element, so Studio can't select this {{kind}}. Give it one root, or wrap it here."
|
|
1724
1511
|
}
|
|
1725
1512
|
},
|
|
1726
1513
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1727
1514
|
create: ({ context, report }) => {
|
|
1728
|
-
let
|
|
1515
|
+
let kind;
|
|
1729
1516
|
return {
|
|
1730
1517
|
"CallExpression": (node) => {
|
|
1731
|
-
|
|
1518
|
+
const definition = readDefinition(node);
|
|
1519
|
+
kind ??= definition && DEFINERS[definition.definer].kind;
|
|
1732
1520
|
},
|
|
1733
1521
|
"Program:exit": () => {
|
|
1734
1522
|
const program = context.sourceCode.ast;
|
|
1735
1523
|
const template = program.templateBody;
|
|
1736
|
-
if (!
|
|
1524
|
+
if (!kind || !template || OWN_ATTRS.test(context.sourceCode.text)) return;
|
|
1737
1525
|
const [root, second] = renderedRoots(template);
|
|
1738
1526
|
if (second) {
|
|
1739
1527
|
report({
|
|
1740
1528
|
loc: second.startTag.loc,
|
|
1741
|
-
messageId: "roots"
|
|
1529
|
+
messageId: "roots",
|
|
1530
|
+
data: { kind }
|
|
1742
1531
|
});
|
|
1743
1532
|
return;
|
|
1744
1533
|
}
|
|
1745
1534
|
const file = root && importedComponent(program, context.filename, root.rawName);
|
|
1746
1535
|
const component = file ? parseVueFile(file) : void 0;
|
|
1747
1536
|
const componentTemplate = component?.ast.templateBody;
|
|
1748
|
-
if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 &&
|
|
1537
|
+
if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 && !OWN_ATTRS.test(component.text)) report({
|
|
1749
1538
|
loc: root.startTag.loc,
|
|
1750
1539
|
messageId: "componentRoots",
|
|
1751
|
-
data: {
|
|
1540
|
+
data: {
|
|
1541
|
+
name: root.rawName,
|
|
1542
|
+
kind
|
|
1543
|
+
}
|
|
1752
1544
|
});
|
|
1753
1545
|
}
|
|
1754
1546
|
};
|
|
@@ -1757,9 +1549,9 @@ var root_default = defineRule({
|
|
|
1757
1549
|
var slots_default = defineRule({
|
|
1758
1550
|
meta: {
|
|
1759
1551
|
type: "problem",
|
|
1760
|
-
docs: { description: "Disallow counting
|
|
1552
|
+
docs: { description: "Disallow counting the blocks of a slot with `.length`" },
|
|
1761
1553
|
schema: [],
|
|
1762
|
-
messages: { length: "
|
|
1554
|
+
messages: { length: "For blocks, this is always 1: frontend-core passes them as one Fragment. Count the Fragment's children instead." }
|
|
1763
1555
|
},
|
|
1764
1556
|
applies: (file) => file.side === "app",
|
|
1765
1557
|
create: ({ report, visitTemplate }) => {
|
|
@@ -1788,7 +1580,7 @@ var slots_default = defineRule({
|
|
|
1788
1580
|
var bundle_default = defineRule({
|
|
1789
1581
|
meta: {
|
|
1790
1582
|
type: "problem",
|
|
1791
|
-
docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins
|
|
1583
|
+
docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins" },
|
|
1792
1584
|
schema: [{
|
|
1793
1585
|
type: "object",
|
|
1794
1586
|
properties: { packages: {
|
|
@@ -1797,7 +1589,7 @@ var bundle_default = defineRule({
|
|
|
1797
1589
|
} },
|
|
1798
1590
|
additionalProperties: false
|
|
1799
1591
|
}],
|
|
1800
|
-
messages: { heavy: "
|
|
1592
|
+
messages: { heavy: "This loads '{{name}}' on every page. Load it with import() or defineAsyncComponent() instead." }
|
|
1801
1593
|
},
|
|
1802
1594
|
applies: (file) => file.kind === "section" || file.kind === "block" || file.kind === "app-plugin",
|
|
1803
1595
|
create: ({ context, report }) => {
|
|
@@ -1812,6 +1604,21 @@ var bundle_default = defineRule({
|
|
|
1812
1604
|
} };
|
|
1813
1605
|
}
|
|
1814
1606
|
});
|
|
1607
|
+
const readPackageFile = cachedReader((text) => {
|
|
1608
|
+
const json = JSON.parse(text);
|
|
1609
|
+
return {
|
|
1610
|
+
name: json.name,
|
|
1611
|
+
dependencies: new Set([
|
|
1612
|
+
json.dependencies,
|
|
1613
|
+
json.devDependencies,
|
|
1614
|
+
json.peerDependencies,
|
|
1615
|
+
json.optionalDependencies
|
|
1616
|
+
].flatMap((deps) => Object.keys(deps ?? {})))
|
|
1617
|
+
};
|
|
1618
|
+
});
|
|
1619
|
+
function readPackage(root) {
|
|
1620
|
+
return readPackageFile(join(root, "package.json"));
|
|
1621
|
+
}
|
|
1815
1622
|
function isRuntimeConfig(node, variables) {
|
|
1816
1623
|
if (node.type === "CallExpression") return node.callee.type === "Identifier" && node.callee.name === "useRuntimeConfig";
|
|
1817
1624
|
if (node.type === "Identifier") return variables.has(node.name);
|
|
@@ -1827,9 +1634,9 @@ function setupOptions(node) {
|
|
|
1827
1634
|
var keys_default = defineRule({
|
|
1828
1635
|
meta: {
|
|
1829
1636
|
type: "problem",
|
|
1830
|
-
docs: { description: "Require runtime config keys
|
|
1637
|
+
docs: { description: "Require runtime config keys to be this package or one of its dependencies" },
|
|
1831
1638
|
schema: [],
|
|
1832
|
-
messages: { key: "'{{key}}' isn't this package or one of its dependencies, so
|
|
1639
|
+
messages: { key: "'{{key}}' isn't this package or one of its dependencies, so this config is undefined." }
|
|
1833
1640
|
},
|
|
1834
1641
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1835
1642
|
create: ({ file, report }) => {
|
|
@@ -1854,9 +1661,9 @@ var keys_default = defineRule({
|
|
|
1854
1661
|
var public_default = defineRule({
|
|
1855
1662
|
meta: {
|
|
1856
1663
|
type: "problem",
|
|
1857
|
-
docs: { description: "Disallow copying module options into the public runtime config
|
|
1664
|
+
docs: { description: "Disallow copying all module options into the public runtime config" },
|
|
1858
1665
|
schema: [],
|
|
1859
|
-
messages: { options: "
|
|
1666
|
+
messages: { options: "This sends all module options to the browser, secrets included. Copy only the public ones, e.g. options.storefrontUrl." }
|
|
1860
1667
|
},
|
|
1861
1668
|
applies: (file) => file.side === "build",
|
|
1862
1669
|
create: ({ report }) => ({ AssignmentExpression: (node) => {
|
|
@@ -1867,42 +1674,25 @@ var public_default = defineRule({
|
|
|
1867
1674
|
walk(node.right, (child) => {
|
|
1868
1675
|
if (child.type !== "Identifier" || child.name !== options || !isReference(child)) return;
|
|
1869
1676
|
const { parent } = child;
|
|
1870
|
-
if (
|
|
1677
|
+
if (parent.type !== "MemberExpression" || parent.object !== child) report({
|
|
1871
1678
|
node: child,
|
|
1872
1679
|
messageId: "options"
|
|
1873
1680
|
});
|
|
1874
1681
|
});
|
|
1875
1682
|
} })
|
|
1876
1683
|
});
|
|
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
1684
|
const LABELS = {
|
|
1895
|
-
"section": "
|
|
1896
|
-
"block": "
|
|
1897
|
-
"component": "
|
|
1898
|
-
"composable": "
|
|
1899
|
-
"app-plugin": "
|
|
1900
|
-
"override": "
|
|
1901
|
-
"route": "API
|
|
1902
|
-
"nitro-plugin": "Nitro
|
|
1903
|
-
"orchestr-plugin": "orchestr
|
|
1904
|
-
"orchestr-file": "orchestr",
|
|
1905
|
-
"media-library": "media
|
|
1685
|
+
"section": "sections",
|
|
1686
|
+
"block": "blocks",
|
|
1687
|
+
"component": "components",
|
|
1688
|
+
"composable": "composables",
|
|
1689
|
+
"app-plugin": "plugins",
|
|
1690
|
+
"override": "overrides",
|
|
1691
|
+
"route": "API routes",
|
|
1692
|
+
"nitro-plugin": "Nitro plugins",
|
|
1693
|
+
"orchestr-plugin": "orchestr plugins",
|
|
1694
|
+
"orchestr-file": "orchestr files",
|
|
1695
|
+
"media-library": "media libraries"
|
|
1906
1696
|
};
|
|
1907
1697
|
const ABOVE_APP_UTILS = /* @__PURE__ */ new Set([
|
|
1908
1698
|
"section",
|
|
@@ -1925,13 +1715,7 @@ const ABOVE_SERVER_UTILS = /* @__PURE__ */ new Set([
|
|
|
1925
1715
|
"orchestr-file",
|
|
1926
1716
|
"media-library"
|
|
1927
1717
|
]);
|
|
1928
|
-
|
|
1929
|
-
"handler",
|
|
1930
|
-
"orchestr-file",
|
|
1931
|
-
"middleware",
|
|
1932
|
-
"media-library"
|
|
1933
|
-
]);
|
|
1934
|
-
function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
1718
|
+
function findViolation(importer, target, typeOnly) {
|
|
1935
1719
|
if (importer.side === "app" && target.side === "server") return { messageId: "appImportsServer" };
|
|
1936
1720
|
if (importer.side === "server" && target.side === "app") return { messageId: "serverImportsApp" };
|
|
1937
1721
|
if (importer.side === "shared" && (target.side === "app" || target.side === "server")) return {
|
|
@@ -1942,25 +1726,10 @@ function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
|
1942
1726
|
if (target.kind === "handler") return { messageId: "handlerImported" };
|
|
1943
1727
|
if (typeOnly) return;
|
|
1944
1728
|
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
1729
|
if (importer.kind === "server-util" && ABOVE_SERVER_UTILS.has(target.kind)) return {
|
|
1948
1730
|
messageId: "serverUtilImportsUp",
|
|
1949
1731
|
data: { kind: LABELS[target.kind] }
|
|
1950
1732
|
};
|
|
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
1733
|
if (importer.kind === "app-util" && ABOVE_APP_UTILS.has(target.kind)) return {
|
|
1965
1734
|
messageId: "appUtilImportsUp",
|
|
1966
1735
|
data: { kind: LABELS[target.kind] }
|
|
@@ -1974,36 +1743,24 @@ function findViolation(importer, target, typeOnly, sharedDomains) {
|
|
|
1974
1743
|
var layers_default = defineRule({
|
|
1975
1744
|
meta: {
|
|
1976
1745
|
type: "problem",
|
|
1977
|
-
docs: { description: "Enforce
|
|
1978
|
-
schema: [
|
|
1979
|
-
type: "object",
|
|
1980
|
-
properties: { sharedDomains: {
|
|
1981
|
-
type: "array",
|
|
1982
|
-
items: { type: "string" }
|
|
1983
|
-
} },
|
|
1984
|
-
additionalProperties: false
|
|
1985
|
-
}],
|
|
1746
|
+
docs: { description: "Enforce which parts of a Laioutr app can import each other" },
|
|
1747
|
+
schema: [],
|
|
1986
1748
|
messages: {
|
|
1987
|
-
appImportsServer: "App code can't import server code. Move what both
|
|
1988
|
-
serverImportsApp: "Server code can't import app code. Move what both
|
|
1989
|
-
sharedImportsSide: "Shared code
|
|
1990
|
-
runtimeImportsBuild: "Runtime code can't import build
|
|
1991
|
-
handlerImported: "
|
|
1992
|
-
clientInHandler: "Handlers
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
appUtilImportsUp: "App utils are the bottom layer and can't import a {{kind}}. Type imports are fine.",
|
|
1999
|
-
composableImportsUp: "Composables can't import a {{kind}}. Type imports are fine.",
|
|
2000
|
-
componentImportsSection: "Components can't import sections; sections compose components.",
|
|
2001
|
-
nodeBuiltin: "'{{name}}' only exists on the server. Keep it in server/."
|
|
1749
|
+
appImportsServer: "App code can't import server code. Move what both need to src/runtime/shared/.",
|
|
1750
|
+
serverImportsApp: "Server code can't import app code. Move what both need to src/runtime/shared/.",
|
|
1751
|
+
sharedImportsSide: "Shared code can't import from {{side}}/.",
|
|
1752
|
+
runtimeImportsBuild: "Runtime code can't import build code from src/, like module.ts. Type imports are fine.",
|
|
1753
|
+
handlerImported: "Don't import orchestr handlers. Move the code you need to server/utils/.",
|
|
1754
|
+
clientInHandler: "Handlers get API clients from the orchestr context, not by import. Move other code to server/utils/.",
|
|
1755
|
+
serverUtilImportsUp: "Server utils can't import {{kind}}. Type imports are fine.",
|
|
1756
|
+
appUtilImportsUp: "App utils can't import {{kind}}. Type imports are fine.",
|
|
1757
|
+
composableImportsUp: "Composables can't import {{kind}}. Type imports are fine.",
|
|
1758
|
+
componentImportsSection: "Components can't import sections.",
|
|
1759
|
+
nodeBuiltin: "'{{name}}' only works on the server. Move this code to server/."
|
|
2002
1760
|
}
|
|
2003
1761
|
},
|
|
2004
1762
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
2005
1763
|
create: ({ context, file: importer, report }) => {
|
|
2006
|
-
const { sharedDomains = [] } = context.options[0] ?? {};
|
|
2007
1764
|
const checkImport = (node, source, typeOnly) => {
|
|
2008
1765
|
if (typeof source !== "string") return;
|
|
2009
1766
|
if (source.startsWith("node:")) {
|
|
@@ -2016,7 +1773,7 @@ var layers_default = defineRule({
|
|
|
2016
1773
|
}
|
|
2017
1774
|
const path = resolveImport(context.filename, source);
|
|
2018
1775
|
const target = path ? classify(path) : void 0;
|
|
2019
|
-
const violation = target && findViolation(importer, target, typeOnly
|
|
1776
|
+
const violation = target && findViolation(importer, target, typeOnly);
|
|
2020
1777
|
if (violation) report({
|
|
2021
1778
|
node,
|
|
2022
1779
|
...violation
|
|
@@ -2045,55 +1802,40 @@ const plugin = {
|
|
|
2045
1802
|
},
|
|
2046
1803
|
rules: {
|
|
2047
1804
|
"layers": layers_default,
|
|
2048
|
-
"known-folders": folders_default,
|
|
2049
1805
|
"heavy-imports": bundle_default,
|
|
2050
1806
|
"config-keys": keys_default,
|
|
2051
1807
|
"public-config": public_default,
|
|
2052
1808
|
"orchestr-files": files_default$1,
|
|
2053
1809
|
"orchestr-cookies": cookies_default,
|
|
2054
|
-
"handler-domains": domains_default,
|
|
2055
1810
|
"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
1811
|
"middleware-files": files_default,
|
|
2061
1812
|
"initware-throws": throws_default,
|
|
2062
|
-
"orchestr-meta": meta_default,
|
|
2063
1813
|
"token-namespaces": namespaces_default,
|
|
2064
|
-
"token-nullable": nullable_default,
|
|
2065
1814
|
"definition-folder": folder_default,
|
|
2066
|
-
"definition-prefix": prefix_default,
|
|
2067
1815
|
"component-name": name_default,
|
|
2068
1816
|
"definition-description": description_default,
|
|
2069
|
-
"root-id": id_default,
|
|
2070
1817
|
"single-root": root_default,
|
|
2071
1818
|
"slot-children": slots_default,
|
|
2072
1819
|
"reserved-field-names": reserved_default,
|
|
2073
1820
|
"field-name-case": casing_default,
|
|
2074
1821
|
"required-fields": required_default,
|
|
2075
|
-
"schema-groups": groups_default,
|
|
2076
1822
|
"unused-fields": unused_default,
|
|
2077
1823
|
"dead-fallbacks": fallbacks_default,
|
|
2078
1824
|
"button-type": button_default,
|
|
2079
|
-
"ui-kit-tags": tags_default,
|
|
2080
1825
|
"mutation-errors": mutations_default,
|
|
2081
|
-
"orchestr-store": store_default,
|
|
2082
1826
|
"resolve-result": resolve_default,
|
|
2083
1827
|
"hand-built-links": paths_default,
|
|
2084
|
-
"internal-anchors": anchors_default
|
|
2085
|
-
"money": money_default
|
|
1828
|
+
"internal-anchors": anchors_default
|
|
2086
1829
|
}
|
|
2087
1830
|
};
|
|
2088
1831
|
async function larrylint(options = {}) {
|
|
2089
|
-
const {
|
|
1832
|
+
const { heavyPackages } = await loadLarrylintConfig(options.cwd);
|
|
2090
1833
|
return [{
|
|
2091
1834
|
name: "larrylint",
|
|
2092
1835
|
files: ["**/src/**/*.{ts,mts,cts,js,mjs,cjs,vue}"],
|
|
2093
1836
|
plugins: { larrylint: plugin },
|
|
2094
1837
|
rules: {
|
|
2095
1838
|
...Object.fromEntries(Object.keys(plugin.rules).map((id) => [`larrylint/${id}`, "error"])),
|
|
2096
|
-
"larrylint/layers": ["error", { sharedDomains }],
|
|
2097
1839
|
"larrylint/heavy-imports": ["error", { packages: heavyPackages }]
|
|
2098
1840
|
}
|
|
2099
1841
|
}];
|