larrylint 0.1.0 → 0.1.1

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