larrylint 0.1.0 → 0.2.0

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