larrylint 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import { name, version } from "./package.mjs";
2
2
  import process from "node:process";
3
3
  import { loadConfig } from "c12";
4
- import { basename, dirname, join, normalize, parse, relative, resolve } from "pathe";
5
4
  import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { dirname, join, normalize, parse, relative, resolve } from "pathe";
6
6
  import tsParser, { parse as parse$1 } from "@typescript-eslint/parser";
7
7
  import { parse as parse$2 } from "vue-eslint-parser";
8
8
  function defineLarrylintConfig(config) {
@@ -17,10 +17,7 @@ async function loadLarrylintConfig(cwd = process.cwd()) {
17
17
  globalRc: false,
18
18
  dotenv: false
19
19
  });
20
- return {
21
- sharedDomains: config.sharedDomains ?? [],
22
- heavyPackages: config.heavyPackages ?? []
23
- };
20
+ return { heavyPackages: config.heavyPackages ?? [] };
24
21
  }
25
22
  const EXTENSIONS = [
26
23
  "",
@@ -82,11 +79,63 @@ function findPackage(from, name) {
82
79
  if (dirname(folder) === folder) return;
83
80
  }
84
81
  }
85
- const HANDLER_FILE = /\.(query|resolver|link|action|template|page-index)(?:\.[cm]?[jt]s)?$/;
82
+ const BASELINE_FILE = "larrylint-baseline.json";
83
+ const readBaselineFile = cachedReader((text) => JSON.parse(text));
84
+ function readBaseline(root) {
85
+ return readBaselineFile(join(root, "larrylint-baseline.json")) ?? {};
86
+ }
87
+ function writeBaseline(root, baseline) {
88
+ const file = join(root, BASELINE_FILE);
89
+ const files = Object.keys(baseline).sort();
90
+ if (files.length === 0) {
91
+ rmSync(file, { force: true });
92
+ return;
93
+ }
94
+ const sorted = {};
95
+ for (const path of files) sorted[path] = Object.fromEntries(Object.entries(baseline[path]).sort(([a], [b]) => a.localeCompare(b)));
96
+ writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`);
97
+ }
98
+ function countViolations(baseline) {
99
+ return Object.values(baseline).flatMap((rules) => Object.values(rules)).reduce((sum, count) => sum + count, 0);
100
+ }
101
+ function applyBaseline(cwd, results) {
102
+ const baseline = readBaseline(cwd);
103
+ const violations = {};
104
+ let baselined = 0;
105
+ let improved = 0;
106
+ const filtered = results.map((result) => {
107
+ const path = relative(cwd, result.filePath);
108
+ const perRule = {};
109
+ const relevant = result.messages.filter((message) => message.fatal || message.ruleId?.startsWith("larrylint/"));
110
+ for (const { ruleId } of relevant) if (ruleId) perRule[ruleId] = (perRule[ruleId] ?? 0) + 1;
111
+ if (Object.keys(perRule).length > 0) violations[path] = perRule;
112
+ const messages = relevant.filter(({ ruleId }) => !ruleId || perRule[ruleId] > (baseline[path]?.[ruleId] ?? 0));
113
+ baselined += relevant.length - messages.length;
114
+ return {
115
+ ...result,
116
+ messages,
117
+ errorCount: messages.filter((message) => message.severity === 2).length,
118
+ fatalErrorCount: messages.filter((message) => message.fatal).length,
119
+ warningCount: messages.filter((message) => message.severity === 1).length,
120
+ fixableErrorCount: messages.filter((message) => message.severity === 2 && message.fix).length,
121
+ fixableWarningCount: messages.filter((message) => message.severity === 1 && message.fix).length
122
+ };
123
+ });
124
+ for (const [path, rules] of Object.entries(baseline)) for (const [rule, allowed] of Object.entries(rules)) if ((violations[path]?.[rule] ?? 0) < allowed) improved++;
125
+ return {
126
+ results: filtered,
127
+ violations,
128
+ baselined,
129
+ improved
130
+ };
131
+ }
132
+ const HANDLER_FILE = /\.(query|resolver|link|action|templates?|page-index)(?:\.[cm]?[jt]s)?$/;
86
133
  const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
87
134
  const APP_FOLDERS = {
88
135
  "sections": "section",
136
+ "section": "section",
89
137
  "blocks": "block",
138
+ "block": "block",
90
139
  "components": "component",
91
140
  "composables": "composable",
92
141
  "utils": "app-util",
@@ -129,10 +178,6 @@ function classify(file) {
129
178
  test
130
179
  };
131
180
  }
132
- function domainOf(file) {
133
- if (!file.domain || file.kind === "handler" || file.kind === "orchestr-file") return file.domain;
134
- return isDirectory(`${file.root}/src/runtime/server/orchestr/${file.domain}`) ? file.domain : void 0;
135
- }
136
181
  function classifyRuntime(parts) {
137
182
  const [side, folder = "", ...rest] = parts;
138
183
  if (parts.length < 2 || side !== "app" && side !== "server" && side !== "shared") return { side: "other" };
@@ -144,75 +189,21 @@ function classifyRuntime(parts) {
144
189
  side,
145
190
  kind: APP_FOLDERS[folder] ?? "other"
146
191
  };
147
- const nested = rest.length > 1;
148
192
  if (folder === "orchestr") {
149
- if (nested && rest[0] === "plugins") return {
193
+ if (rest.length > 1 && rest[0] === "plugins") return {
150
194
  side,
151
195
  kind: "orchestr-plugin"
152
196
  };
153
- const handler = HANDLER_FILE.exec(parts.at(-1))?.[1];
197
+ const handler = HANDLER_FILE.exec(parts.at(-1))?.[1]?.replace("templates", "template");
154
198
  return {
155
199
  side,
156
200
  kind: handler ? "handler" : "orchestr-file",
157
- handler,
158
- domain: nested ? rest[0] : void 0
201
+ handler
159
202
  };
160
203
  }
161
- const kind = SERVER_FOLDERS[folder] ?? "other";
162
204
  return {
163
205
  side,
164
- kind,
165
- domain: kind === "server-util" && nested ? rest[0] : void 0
166
- };
167
- }
168
- const BASELINE_FILE = "larrylint-baseline.json";
169
- const readBaselineFile = cachedReader((text) => JSON.parse(text));
170
- function readBaseline(root) {
171
- return readBaselineFile(join(root, "larrylint-baseline.json")) ?? {};
172
- }
173
- function writeBaseline(root, baseline) {
174
- const file = join(root, BASELINE_FILE);
175
- const files = Object.keys(baseline).sort();
176
- if (files.length === 0) {
177
- rmSync(file, { force: true });
178
- return;
179
- }
180
- const sorted = {};
181
- for (const path of files) sorted[path] = Object.fromEntries(Object.entries(baseline[path]).sort(([a], [b]) => a.localeCompare(b)));
182
- writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`);
183
- }
184
- function countViolations(baseline) {
185
- return Object.values(baseline).flatMap((rules) => Object.values(rules)).reduce((sum, count) => sum + count, 0);
186
- }
187
- function applyBaseline(cwd, results) {
188
- const baseline = readBaseline(cwd);
189
- const violations = {};
190
- let baselined = 0;
191
- let improved = 0;
192
- const filtered = results.map((result) => {
193
- const path = relative(cwd, result.filePath);
194
- const perRule = {};
195
- const relevant = result.messages.filter((message) => message.fatal || message.ruleId?.startsWith("larrylint/"));
196
- for (const { ruleId } of relevant) if (ruleId) perRule[ruleId] = (perRule[ruleId] ?? 0) + 1;
197
- if (Object.keys(perRule).length > 0) violations[path] = perRule;
198
- const messages = relevant.filter(({ ruleId }) => !ruleId || perRule[ruleId] > (baseline[path]?.[ruleId] ?? 0));
199
- baselined += relevant.length - messages.length;
200
- return {
201
- ...result,
202
- messages,
203
- errorCount: messages.filter((message) => message.severity === 2).length,
204
- fatalErrorCount: messages.filter((message) => message.fatal).length,
205
- warningCount: messages.filter((message) => message.severity === 1).length,
206
- fixableErrorCount: messages.filter((message) => message.severity === 2 && message.fix).length,
207
- fixableWarningCount: messages.filter((message) => message.severity === 1 && message.fix).length
208
- };
209
- });
210
- for (const [path, rules] of Object.entries(baseline)) for (const [rule, allowed] of Object.entries(rules)) if ((violations[path]?.[rule] ?? 0) < allowed) improved++;
211
- return {
212
- results: filtered,
213
- violations,
214
- baselined,
215
- improved
206
+ kind: SERVER_FOLDERS[folder] ?? "other"
216
207
  };
217
208
  }
218
209
  function defineRule(rule) {
@@ -257,47 +248,6 @@ function defineRule(rule) {
257
248
  }
258
249
  };
259
250
  }
260
- function findProperty(object, key) {
261
- for (const entry of object.properties) if (entry.type === "Property" && !entry.computed && (entry.key.type === "Identifier" && entry.key.name === key || entry.key.type === "Literal" && entry.key.value === key)) return entry.value;
262
- }
263
- function findStringProperty(object, key) {
264
- const value = findProperty(object, key);
265
- return value?.type === "Literal" && typeof value.value === "string" ? {
266
- value: value.value,
267
- node: value
268
- } : void 0;
269
- }
270
- function objectElements(node) {
271
- if (node?.type !== "ArrayExpression") return [];
272
- return node.elements.filter((element) => element?.type === "ObjectExpression");
273
- }
274
- const ISO_4217 = /^[A-Z]{3}$/;
275
- var money_default = defineRule({
276
- meta: {
277
- type: "problem",
278
- docs: { description: "Require money amounts in minor units and ISO 4217 currency codes." },
279
- schema: [],
280
- messages: {
281
- minorUnits: "Money amounts are in minor units, e.g. 1999 for 19.99, so this price is off by a factor of 100.",
282
- currency: "Money currencies are ISO 4217 codes like 'EUR', and formatters show '{{currency}}' as it is."
283
- }
284
- },
285
- applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
286
- create: ({ report }) => ({ ObjectExpression: (node) => {
287
- const amount = findProperty(node, "amount");
288
- const currency = findProperty(node, "currency");
289
- if (!amount || !currency) return;
290
- if (amount.type === "Literal" && typeof amount.value === "number" && !Number.isInteger(amount.value)) report({
291
- node: amount,
292
- messageId: "minorUnits"
293
- });
294
- if (currency.type === "Literal" && typeof currency.value === "string" && !ISO_4217.test(currency.value)) report({
295
- node: currency,
296
- messageId: "currency",
297
- data: { currency: currency.value }
298
- });
299
- } })
300
- });
301
251
  const WRAPPERS = /* @__PURE__ */ new Set([
302
252
  "ChainExpression",
303
253
  "TSNonNullExpression",
@@ -314,15 +264,6 @@ function chainRoot(node) {
314
264
  else if (current.type === "MemberExpression") current = current.object;
315
265
  else return current.type === "Identifier" ? current : void 0;
316
266
  }
317
- function chainMethods(node) {
318
- const methods = [];
319
- let current = node;
320
- while (current?.type === "CallExpression" && current.callee.type === "MemberExpression" && current.callee.property.type === "Identifier") {
321
- methods.push(current.callee.property.name);
322
- current = current.callee.object;
323
- }
324
- return methods;
325
- }
326
267
  function memberPath(node) {
327
268
  if (node.type === "Identifier") return [node.name];
328
269
  if (node.type !== "MemberExpression") return [];
@@ -359,6 +300,37 @@ function followPromise(promise) {
359
300
  end = call;
360
301
  }
361
302
  }
303
+ const TEST_DIRECTIVES = /* @__PURE__ */ new Set([
304
+ "if",
305
+ "else-if",
306
+ "show"
307
+ ]);
308
+ function isReference(node) {
309
+ const { parent } = node;
310
+ if (parent?.type === "MemberExpression") return parent.property !== node || parent.computed;
311
+ return parent?.type !== "Property" || parent.key !== node || parent.computed || parent.shorthand;
312
+ }
313
+ function isNullish(node) {
314
+ return node.type === "Identifier" && node.name === "undefined" || node.type === "Literal" && node.value === null && !("regex" in node);
315
+ }
316
+ function canBeNullish(value) {
317
+ if (isNullish(value)) return true;
318
+ if (value.type === "LogicalExpression" && value.operator === "??") return canBeNullish(value.right);
319
+ if (value.type === "ConditionalExpression") return canBeNullish(value.consequent) || canBeNullish(value.alternate);
320
+ return false;
321
+ }
322
+ function isTested(node) {
323
+ const parent = node.parent;
324
+ switch (parent?.type) {
325
+ case "IfStatement":
326
+ case "WhileStatement":
327
+ case "ConditionalExpression": return parent.test === node;
328
+ case "LogicalExpression": return parent.left === node;
329
+ case "UnaryExpression": return parent.operator === "!";
330
+ case "VExpressionContainer": return parent.parent?.type === "VAttribute" && TEST_DIRECTIVES.has(parent.parent.key?.name?.name ?? "");
331
+ default: return false;
332
+ }
333
+ }
362
334
  const SKIPPED = /* @__PURE__ */ new Set([
363
335
  "parent",
364
336
  "loc",
@@ -412,16 +384,28 @@ function isCaught(node) {
412
384
  }
413
385
  return false;
414
386
  }
387
+ function canReturnNullish(fn, value) {
388
+ if (fn.body.type !== "BlockStatement") return fn.body !== value && canBeNullish(fn.body);
389
+ let nullish = false;
390
+ walkBody(fn, (node) => {
391
+ nullish ||= node.type === "ReturnStatement" && node.argument !== value && (!node.argument || canBeNullish(node.argument));
392
+ });
393
+ const last = fn.body.body.at(-1);
394
+ return nullish || last?.type !== "ReturnStatement" && last?.type !== "ThrowStatement";
395
+ }
415
396
  var mutations_default = defineRule({
416
397
  meta: {
417
398
  type: "problem",
418
- docs: { description: "Require error handling where a mutateAsync() promise is awaited or dropped, since a rejected mutation takes down its whole section or block." },
399
+ docs: { description: "Require error handling for awaited or dropped `mutateAsync()` calls" },
419
400
  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()." }
401
+ messages: {
402
+ unhandled: "If this mutation fails, an error replaces the whole section or block. Catch it with try/catch or .catch().",
403
+ dropped: "Nothing catches errors of this mutation. Add .catch(), or await it in a try/catch."
404
+ }
421
405
  },
422
406
  applies: (file) => file.side === "app",
423
407
  create: ({ report, visitTemplate }) => {
424
- const check = (call) => {
408
+ const check = (call, template) => {
425
409
  if (nameOf(call.callee) !== "mutateAsync") return;
426
410
  const { end, handled } = followPromise(call);
427
411
  const { parent } = end;
@@ -431,146 +415,40 @@ var mutations_default = defineRule({
431
415
  if (!awaited && !dropped || awaited && isCaught(parent)) return;
432
416
  report({
433
417
  node: call.callee.type === "MemberExpression" ? call.callee.property : call.callee,
434
- messageId: "unhandled"
418
+ messageId: dropped && !template ? "dropped" : "unhandled"
435
419
  });
436
420
  };
437
- visitTemplate({ CallExpression: check });
438
- return { CallExpression: check };
421
+ visitTemplate({ CallExpression: (node) => check(node, true) });
422
+ return { CallExpression: (node) => check(node, false) };
439
423
  }
440
424
  });
441
- const 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
- });
425
+ const METHODS = /* @__PURE__ */ new Set(["resolve", "resolveOrThrow"]);
426
+ function destructuredResolvers(declarator) {
427
+ const methods = /* @__PURE__ */ new Map();
428
+ if (declarator.id.type !== "ObjectPattern" || !declarator.init || nameOf(declarator.init) !== "linkResolver") return methods;
429
+ for (const property of declarator.id.properties) {
430
+ const method = property.type === "Property" ? nameOf(property.key) : void 0;
431
+ if (method && METHODS.has(method) && property.type === "Property" && property.value.type === "Identifier") methods.set(property.value.name, method);
432
+ }
433
+ return methods;
434
+ }
435
+ function linkResolverMethod(call, destructured) {
436
+ const { callee } = call;
437
+ if (callee.type === "Identifier") return destructured.get(callee.name);
438
+ const method = callee.type === "MemberExpression" ? nameOf(callee) : void 0;
439
+ return method && METHODS.has(method) && callee.type === "MemberExpression" && nameOf(callee.object) === "linkResolver" ? method : void 0;
440
+ }
441
+ const NOT_PAGES = /^\/(?:\/|api\/|_|app-|\.well-known\/)/;
462
442
  function isInternalPath(path) {
463
- return path?.startsWith("/") === true && !path.startsWith("//") && !path.startsWith("/api/");
443
+ return path?.startsWith("/") === true && !NOT_PAGES.test(path);
464
444
  }
465
445
  function isHandBuiltPath(node) {
466
446
  if (node?.type === "TemplateLiteral") return node.expressions.length > 0 && isInternalPath(node.quasis[0]?.value.cooked);
467
447
  return node?.type === "BinaryExpression" && node.operator === "+" && node.left.type === "Literal" && typeof node.left.value === "string" && isInternalPath(node.left.value);
468
448
  }
469
- function 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
- }
449
+ function isRouter(node) {
450
+ const name = nameOf(node.type === "CallExpression" ? node.callee : node);
451
+ return node.type === "CallExpression" ? name === "useRouter" : name === "router" || name === "$router";
574
452
  }
575
453
  const parseFile = cachedReader((text) => {
576
454
  try {
@@ -611,6 +489,22 @@ function findFunction(statements, name) {
611
489
  }
612
490
  }
613
491
  }
492
+ function findConstant(statements, name) {
493
+ for (const statement of statements) {
494
+ const declaration = statement.type === "ExportNamedDeclaration" && statement.declaration ? statement.declaration : statement;
495
+ if (declaration.type !== "VariableDeclaration" || declaration.kind !== "const") continue;
496
+ const declarator = declaration.declarations.find((item) => item.id.type === "Identifier" && item.id.name === name);
497
+ if (declarator?.init) return declarator.init;
498
+ }
499
+ }
500
+ function resolveConstant(name, program, filename) {
501
+ const local = findConstant(program.body, name);
502
+ if (local) return local;
503
+ const imported = findImport(program, name);
504
+ const file = imported && imported.name !== "*" ? resolveModule(filename, imported.source) : void 0;
505
+ const module = file ? parseFile(file) : void 0;
506
+ return module && imported ? findConstant(module.body, imported.name) : void 0;
507
+ }
614
508
  function findExport(program, name) {
615
509
  for (const statement of program.body) {
616
510
  if (statement.type === "ExportDefaultDeclaration" && name === "default") {
@@ -665,33 +559,138 @@ function usedProps(node) {
665
559
  }
666
560
  function findHolder(node) {
667
561
  let value = node;
562
+ let fn;
668
563
  const { parent } = node;
669
564
  if (parent?.type === "ReturnStatement") {
670
- let 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;
565
+ let current = parent.parent;
566
+ while (current && !isFunction(current)) current = current.parent;
567
+ fn = current ?? void 0;
568
+ value = current ?? value;
569
+ } else if (parent && isFunction(parent) && parent.body === node) {
570
+ fn = parent;
571
+ value = parent;
572
+ }
674
573
  const computed = value.parent?.type === "CallExpression" && value.parent.callee.type === "Identifier" && value.parent.callee.name === "computed" ? value.parent : void 0;
675
574
  const declarator = (computed ?? value).parent;
676
575
  return declarator?.type === "VariableDeclarator" && declarator.id.type === "Identifier" ? {
677
576
  name: declarator.id.name,
678
- computed: computed !== void 0
577
+ computed: computed !== void 0,
578
+ fn
679
579
  } : void 0;
680
580
  }
681
581
  function importedComponent(program, filename, name) {
682
582
  const imported = findImport(program, name);
683
583
  return imported?.name === "default" && imported.source.endsWith(".vue") ? resolveModule(filename, imported.source) : void 0;
684
584
  }
585
+ function renderedRoots(template) {
586
+ return template.children.filter((child) => child.type === "VElement" && !child.startTag.attributes.some((attribute) => attribute.directive && (attribute.key.name.name === "else" || attribute.key.name.name === "else-if")));
587
+ }
588
+ function findAttribute(element, name) {
589
+ return element.startTag.attributes.find((attribute) => attribute.directive ? attribute.key.name.name === "bind" && attribute.key.argument?.type === "VIdentifier" && attribute.key.argument.name === name : attribute.key.name === name);
590
+ }
591
+ function valueStart(attribute) {
592
+ if (!attribute.directive) return attribute.value?.value;
593
+ const expression = attribute.value?.expression;
594
+ if (expression?.type === "Literal" && typeof expression.value === "string") return expression.value;
595
+ return expression?.type === "TemplateLiteral" ? expression.quasis[0]?.value.cooked ?? void 0 : void 0;
596
+ }
597
+ function rendersTag(element, tag) {
598
+ const is = element.rawName === "component" ? findAttribute(element, "is") : void 0;
599
+ if (element.rawName === tag || is && !is.directive && is.value?.value === tag) return true;
600
+ const expression = is?.directive ? is.value?.expression : void 0;
601
+ let found = false;
602
+ if (expression) walk(expression, (node) => {
603
+ found ||= node.type === "Literal" && node.value === tag;
604
+ });
605
+ return found;
606
+ }
607
+ var anchors_default = defineRule({
608
+ meta: {
609
+ type: "problem",
610
+ docs: { description: "Disallow plain `<a>` tags for internal links" },
611
+ schema: [],
612
+ messages: { anchor: "A plain <a> reloads the whole page. Use <NuxtLink> for internal links." }
613
+ },
614
+ applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
615
+ create: ({ report, visitTemplate }) => {
616
+ const destructured = /* @__PURE__ */ new Map();
617
+ const resolved = /* @__PURE__ */ new Set();
618
+ const resolves = (node) => node.type === "CallExpression" && (linkResolverMethod(node, destructured) !== void 0 || nameOf(node.callee) === "useResolvedLink");
619
+ visitTemplate({ VElement: (node) => {
620
+ const href = rendersTag(node, "a") ? findAttribute(node, "href") : void 0;
621
+ const target = findAttribute(node, "target");
622
+ if (!href || findAttribute(node, "download") || target && !target.directive && target.value?.value === "_blank") return;
623
+ const expression = href.directive ? href.value?.expression : void 0;
624
+ if (expression && (resolves(expression) || expression.type === "Identifier" && resolved.has(expression.name)) || isInternalPath(valueStart(href))) report({
625
+ loc: href.loc,
626
+ messageId: "anchor"
627
+ });
628
+ } });
629
+ return {
630
+ VariableDeclarator: (node) => {
631
+ destructuredResolvers(node).forEach((method, local) => destructured.set(local, method));
632
+ },
633
+ CallExpression: (node) => {
634
+ const holder = resolves(node) ? findHolder(node) : void 0;
635
+ if (holder) resolved.add(holder.name);
636
+ }
637
+ };
638
+ }
639
+ });
640
+ const LINK_NAMES = /* @__PURE__ */ new Set([
641
+ "to",
642
+ "href",
643
+ "link"
644
+ ]);
645
+ const ROUTER_METHODS = /* @__PURE__ */ new Set(["push", "replace"]);
646
+ var paths_default = defineRule({
647
+ meta: {
648
+ type: "problem",
649
+ docs: { description: "Disallow page paths built by hand" },
650
+ schema: [],
651
+ messages: { path: "Hand-built page paths differ per language and market. Use linkResolver instead." }
652
+ },
653
+ applies: (file) => file.side === "app",
654
+ create: ({ report, visitTemplate }) => {
655
+ visitTemplate({ VAttribute: (node) => {
656
+ const argument = node.directive && node.key.name.name === "bind" ? node.key.argument : null;
657
+ const expression = node.directive ? node.value?.expression : void 0;
658
+ if (argument?.type === "VIdentifier" && LINK_NAMES.has(argument.name) && isHandBuiltPath(expression)) report({
659
+ node: expression,
660
+ messageId: "path"
661
+ });
662
+ } });
663
+ return {
664
+ CallExpression: (node) => {
665
+ const { callee } = node;
666
+ const [target] = node.arguments;
667
+ if ((callee.type === "Identifier" ? callee.name === "navigateTo" : callee.type === "MemberExpression" && ROUTER_METHODS.has(nameOf(callee) ?? "") && isRouter(callee.object)) && isHandBuiltPath(target)) report({
668
+ node: target,
669
+ messageId: "path"
670
+ });
671
+ },
672
+ Property: (node) => {
673
+ if (node.key.type === "Identifier" && LINK_NAMES.has(node.key.name) && isHandBuiltPath(node.value)) report({
674
+ node: node.value,
675
+ messageId: "path"
676
+ });
677
+ }
678
+ };
679
+ }
680
+ });
685
681
  var resolve_default = defineRule({
686
682
  meta: {
687
683
  type: "problem",
688
- docs: { description: "Disallow testing the result of linkResolver.resolve(), which is never empty." },
684
+ docs: { description: "Disallow checking the result of `linkResolver.resolve()`" },
689
685
  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." }
686
+ messages: {
687
+ tested: "linkResolver.resolve() never returns an empty value, so this check misses broken links. Use resolveOrThrow() in a try/catch instead.",
688
+ branch: "linkResolver.resolve() never returns an empty value, so the checks on '{{name}}' miss broken links. Use resolveOrThrow() in a try/catch instead."
689
+ }
691
690
  },
692
691
  applies: (file) => file.side === "app",
693
692
  create: ({ report, visitTemplate }) => {
694
- const resolveNames = /* @__PURE__ */ new Set();
693
+ const destructured = /* @__PURE__ */ new Map();
695
694
  const calls = [];
696
695
  const scriptIdentifiers = [];
697
696
  const templateIdentifiers = [];
@@ -700,8 +699,7 @@ var resolve_default = defineRule({
700
699
  } });
701
700
  return {
702
701
  "VariableDeclarator": (node) => {
703
- 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);
702
+ destructuredResolvers(node).forEach((method, local) => destructured.set(local, method));
705
703
  },
706
704
  "CallExpression": (node) => {
707
705
  calls.push(node);
@@ -711,26 +709,38 @@ var resolve_default = defineRule({
711
709
  },
712
710
  "Program:exit": () => {
713
711
  const holders = /* @__PURE__ */ new Map();
712
+ const reported = /* @__PURE__ */ new Set();
714
713
  for (const call of calls) {
715
- const { callee } = call;
716
- if (!(callee.type === "MemberExpression" && nameOf(callee) === "resolve" && nameOf(callee.object) === "linkResolver") && !(callee.type === "Identifier" && resolveNames.has(callee.name))) continue;
714
+ if (linkResolverMethod(call, destructured) !== "resolve") continue;
717
715
  if (isTested(call)) report({
718
716
  node: call,
719
717
  messageId: "tested"
720
718
  });
721
719
  const holder = findHolder(call);
722
- if (holder) holders.set(holder.name, holder.computed);
720
+ if (holder) holders.set(holder.name, {
721
+ computed: holder.computed,
722
+ branch: holder.fn && canReturnNullish(holder.fn, call) ? call : void 0
723
+ });
723
724
  }
724
725
  const check = (identifier, template) => {
725
- const computed = holders.get(identifier.name);
726
+ const holder = holders.get(identifier.name);
726
727
  const { parent } = identifier;
727
- if (computed === void 0 || !isReference(identifier)) return;
728
+ if (!holder || !isReference(identifier)) return;
728
729
  const readsValue = parent.type === "MemberExpression" && parent.object === identifier && nameOf(parent) === "value";
729
- const value = computed && !template ? readsValue ? parent : void 0 : identifier;
730
- if (value && isTested(value)) report({
730
+ const value = holder.computed && !template ? readsValue ? parent : void 0 : identifier;
731
+ if (!value || !isTested(value)) return;
732
+ if (!holder.branch) report({
731
733
  node: value,
732
734
  messageId: "tested"
733
735
  });
736
+ else if (!reported.has(holder.branch)) {
737
+ reported.add(holder.branch);
738
+ report({
739
+ node: holder.branch,
740
+ messageId: "branch",
741
+ data: { name: identifier.name }
742
+ });
743
+ }
734
744
  };
735
745
  scriptIdentifiers.forEach((identifier) => check(identifier, false));
736
746
  templateIdentifiers.forEach((identifier) => check(identifier, true));
@@ -741,24 +751,27 @@ var resolve_default = defineRule({
741
751
  function kebabCase(name) {
742
752
  return name.replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
743
753
  }
744
- function prefixName(name, prefix) {
745
- return `${prefix}${name.endsWith(prefix) ? name.slice(0, -prefix.length) : name}`;
746
- }
747
754
  function camelCase(name) {
748
755
  return name.replace(/[-_\s]+(.)?/g, (_, char) => char?.toUpperCase() ?? "").replace(/^[A-Z]/, (char) => char.toLowerCase());
749
756
  }
750
- const UI_KIT_BUTTON = /(?:^#ui-kit|@laioutr-core\/ui-kit)\/.*\/Button\.vue$/;
757
+ const UI_KIT_BUTTON = /(?:^#ui-kit|@laioutr-core\/ui-kit)\/.*\/(?:Icon|BackgroundAware|Animated)?Button\.vue$/;
758
+ const BUTTONS = [
759
+ "LButton",
760
+ "LIconButton",
761
+ "LBackgroundAwareButton",
762
+ "LAnimatedButton"
763
+ ];
751
764
  var button_default = defineRule({
752
765
  meta: {
753
766
  type: "problem",
754
767
  fixable: "code",
755
- docs: { description: "Disallow `type` on the ui-kit button, which silently renders its `button-type` prop instead." },
768
+ docs: { description: "Disallow `type` on ui-kit buttons" },
756
769
  schema: [],
757
- messages: { buttonType: "l-button ignores type and always renders its button-type prop, which defaults to \"button\". Use button-type." }
770
+ messages: { buttonType: "<{{tag}}> ignores type. Use button-type instead." }
758
771
  },
759
772
  applies: (file) => file.path.endsWith(".vue"),
760
773
  create: ({ report, visitTemplate }) => {
761
- const buttons = /* @__PURE__ */ new Set(["LButton", "l-button"]);
774
+ const buttons = new Set(BUTTONS.flatMap((name) => [name, kebabCase(name)]));
762
775
  visitTemplate({ VElement: (node) => {
763
776
  if (!buttons.has(node.rawName)) return;
764
777
  for (const attribute of node.startTag.attributes) {
@@ -767,6 +780,7 @@ var button_default = defineRule({
767
780
  report({
768
781
  loc: key.loc,
769
782
  messageId: "buttonType",
783
+ data: { tag: node.rawName },
770
784
  fix: (fixer) => fixer.replaceTextRange(key.range, "button-type")
771
785
  });
772
786
  }
@@ -774,6 +788,7 @@ var button_default = defineRule({
774
788
  if (attribute.directive && attribute.key.name.name === "bind" && argument?.type === "VIdentifier" && argument.rawName === "type") report({
775
789
  loc: argument.loc,
776
790
  messageId: "buttonType",
791
+ data: { tag: node.rawName },
777
792
  fix: (fixer) => fixer.replaceTextRange(argument.range, "button-type")
778
793
  });
779
794
  }
@@ -788,101 +803,6 @@ var button_default = defineRule({
788
803
  } };
789
804
  }
790
805
  });
791
- function nuxtName(folders, stem) {
792
- const parts = [...folders];
793
- while (parts.length > 0 && kebabCase(stem).startsWith(kebabCase(parts.at(-1)))) parts.pop();
794
- return [...parts, stem].join("");
795
- }
796
- const PACKAGES = ["@laioutr-core/ui-kit", "@laioutr-core/ui"];
797
- const cache$1 = /* @__PURE__ */ new Map();
798
- function uiKitTags(root) {
799
- let tags = cache$1.get(root);
800
- if (!tags) {
801
- tags = /* @__PURE__ */ new Set();
802
- for (const name of PACKAGES) {
803
- const folder = findPackage(root, name);
804
- if (!folder) throw new Error(`larrylint can't find ${name} from ${root}, which it reads the <l-*> components from. Install the app's dependencies first.`);
805
- for (const file of readdirSync(join(folder, "dist/runtime"), {
806
- recursive: true,
807
- encoding: "utf8"
808
- })) {
809
- if (!file.endsWith(".vue")) continue;
810
- const { dir, name: stem } = parse(file);
811
- const segments = dir.split("/");
812
- const folders = segments.slice(segments.lastIndexOf("components") + 1);
813
- tags.add(`l-${kebabCase(stem)}`);
814
- tags.add(`l-${kebabCase(nuxtName(folders, stem))}`);
815
- }
816
- }
817
- cache$1.set(root, tags);
818
- }
819
- return tags;
820
- }
821
- var tags_default = defineRule({
822
- meta: {
823
- type: "problem",
824
- docs: { description: "Require `<l-*>` tags to be ui-kit or ui components." },
825
- schema: [],
826
- messages: { unknown: "<{{tag}}> isn't a ui-kit or ui component, so it renders nothing. Only their components have the l- prefix; use your own components by their own name." }
827
- },
828
- applies: (file) => file.path.endsWith(".vue"),
829
- create: ({ file, report, visitTemplate }) => {
830
- const imported = /* @__PURE__ */ new Set();
831
- visitTemplate({ VElement: (node) => {
832
- const tag = kebabCase(node.rawName);
833
- if (tag.startsWith("l-") && !imported.has(tag) && !uiKitTags(file.root).has(tag)) report({
834
- loc: node.startTag.loc,
835
- messageId: "unknown",
836
- data: { tag: node.rawName }
837
- });
838
- } });
839
- return { ImportDeclaration: (node) => {
840
- for (const specifier of node.specifiers) imported.add(kebabCase(specifier.local.name));
841
- } };
842
- }
843
- });
844
- const COOKIE_SETTERS = /* @__PURE__ */ new Set(["setCookie", "deleteCookie"]);
845
- const REDIRECTS = /* @__PURE__ */ new Set(["sendRedirect", "navigateTo"]);
846
- const HEADER_SETTERS = /* @__PURE__ */ new Set([
847
- "setHeader",
848
- "setHeaders",
849
- "appendHeader",
850
- "setResponseHeader",
851
- "setResponseHeaders",
852
- "appendResponseHeader"
853
- ]);
854
- function responseEffect(call) {
855
- const name = nameOf(call.callee) ?? "";
856
- if (COOKIE_SETTERS.has(name)) return "cookie";
857
- if (REDIRECTS.has(name)) return "redirect";
858
- const header = HEADER_SETTERS.has(name) ? call.arguments.find((argument) => argument.type === "Literal" && typeof argument.value === "string") : void 0;
859
- const value = header?.type === "Literal" ? String(header.value).toLowerCase() : void 0;
860
- return value === "set-cookie" ? "cookie" : value === "location" ? "redirect" : void 0;
861
- }
862
- function thrownStatus(error) {
863
- if (error.type !== "CallExpression" && error.type !== "NewExpression") return;
864
- for (const argument of error.arguments) {
865
- const status = argument.type === "ObjectExpression" ? findProperty(argument, "status") ?? findProperty(argument, "statusCode") : void 0;
866
- if (status?.type === "Literal" && typeof status.value === "number") return status.value;
867
- }
868
- }
869
- var errors_default = defineRule({
870
- meta: {
871
- type: "problem",
872
- docs: { description: "Require actions to return client errors as a status instead of throwing them." },
873
- schema: [],
874
- messages: { status: "The client never sees this {{status}}: orchestr's action transport drops status codes, so it arrives as a failed action. Return it as a status in the action's output instead." }
875
- },
876
- applies: (file) => file.handler === "action",
877
- create: ({ report }) => ({ ThrowStatement: (node) => {
878
- const status = thrownStatus(node.argument);
879
- if (status !== void 0 && status >= 400 && status < 500) report({
880
- node,
881
- messageId: "status",
882
- data: { status: String(status) }
883
- });
884
- } })
885
- });
886
806
  function readMiddleware(call) {
887
807
  const { callee } = call;
888
808
  if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") return;
@@ -894,42 +814,78 @@ function readMiddleware(call) {
894
814
  callback
895
815
  } : void 0;
896
816
  }
897
- function 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;
817
+ function enclosingMiddleware(node) {
818
+ for (let parent = node.parent; parent; parent = parent.parent) {
819
+ const middleware = isFunction(parent) && parent.parent?.type === "CallExpression" ? readMiddleware(parent.parent) : void 0;
820
+ if (middleware?.callback === parent) return middleware;
821
+ }
906
822
  }
907
- 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;
823
+ const COOKIE_WRITES = /* @__PURE__ */ new Set([
824
+ "setCookie",
825
+ "deleteCookie",
826
+ "setManagedCookie",
827
+ "deleteManagedCookie"
828
+ ]);
829
+ const HEADER_WRITES = /* @__PURE__ */ new Set([
830
+ "setHeader",
831
+ "setHeaders",
832
+ "appendHeader",
833
+ "appendHeaders",
834
+ "setResponseHeader",
835
+ "setResponseHeaders",
836
+ "appendResponseHeader",
837
+ "appendResponseHeaders",
838
+ "setResponseStatus",
839
+ "sendRedirect"
840
+ ]);
841
+ function responseWrite(call) {
842
+ const name = call.callee.type === "Identifier" ? call.callee.name : "";
843
+ if (COOKIE_WRITES.has(name)) return {
844
+ name,
845
+ kind: "cookie"
846
+ };
847
+ return HEADER_WRITES.has(name) ? {
848
+ name,
849
+ kind: "header"
850
+ } : void 0;
916
851
  }
852
+ const STREAMED = /* @__PURE__ */ new Set([
853
+ "query",
854
+ "link",
855
+ "resolver"
856
+ ]);
857
+ const MANAGED = {
858
+ setCookie: "setManagedCookie",
859
+ deleteCookie: "deleteManagedCookie"
860
+ };
917
861
  var cookies_default = defineRule({
918
862
  meta: {
919
863
  type: "problem",
920
- docs: { description: "Disallow Set-Cookie and redirects from orchestr handlers and middleware, which cached pages replay to every visitor." },
864
+ docs: { description: "Require cookies and headers to be set in `extendRequest()` or action handlers, with frontend-core's managed cookie functions" },
921
865
  schema: [],
922
866
  messages: {
923
- 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."
867
+ streamed: "The headers may already be sent when {{name}}() runs here. Call it in extendRequest() or an action handler instead.",
868
+ managed: "Use {{managed}}() instead, so it also works in the Studio preview."
925
869
  }
926
870
  },
927
871
  applies: (file) => file.side === "server",
928
872
  create: ({ file, report }) => ({ CallExpression: (node) => {
929
- const effect = responseEffect(node);
930
- if (effect && (file.kind === "handler" || inMiddleware(node))) report({
873
+ const write = responseWrite(node);
874
+ if (!write) return;
875
+ const method = enclosingMiddleware(node)?.method;
876
+ const managed = MANAGED[write.name];
877
+ if (method === "use" || !method && STREAMED.has(file.handler)) report({
878
+ node,
879
+ messageId: "streamed",
880
+ data: { name: write.name }
881
+ });
882
+ else if (managed && (method === "extendRequest" || !method && file.handler === "action")) report({
931
883
  node,
932
- messageId: effect
884
+ messageId: "managed",
885
+ data: {
886
+ name: write.name,
887
+ managed
888
+ }
933
889
  });
934
890
  } })
935
891
  });
@@ -946,9 +902,9 @@ const FILE_START = {
946
902
  var files_default$1 = defineRule({
947
903
  meta: {
948
904
  type: "problem",
949
- docs: { description: "Keep orchestr/ to handler files, since Laioutr loads every file in it as a server plugin." },
905
+ docs: { description: "Disallow files other than handlers in `orchestr/`" },
950
906
  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/." }
907
+ messages: { notAHandler: "Only handlers belong in orchestr/. Move this file to server/utils/, or give it a handler suffix like .query.ts or .action.ts." }
952
908
  },
953
909
  applies: (file) => file.kind === "orchestr-file",
954
910
  create: ({ report }) => ({ Program: () => {
@@ -958,54 +914,12 @@ var files_default$1 = defineRule({
958
914
  });
959
915
  } })
960
916
  });
961
- var context_default = defineRule({
962
- meta: {
963
- type: "suggestion",
964
- docs: { description: "Require handlers to take config, clients and the request from their arguments and middleware context." },
965
- schema: [],
966
- messages: {
967
- runtimeConfig: "Handlers get their config and clients from the middleware context. Read the config once in the builder's middleware.",
968
- event: "Handlers get the request as `event` in their arguments, not from useEvent()."
969
- }
970
- },
971
- applies: (file) => file.kind === "handler",
972
- create: ({ report }) => ({ CallExpression: (node) => {
973
- if (node.callee.type !== "Identifier") return;
974
- if (node.callee.name === "useRuntimeConfig") report({
975
- node,
976
- messageId: "runtimeConfig"
977
- });
978
- else if (node.callee.name === "useEvent") report({
979
- node,
980
- messageId: "event"
981
- });
982
- } })
983
- });
984
- var domains_default = defineRule({
985
- meta: {
986
- type: "problem",
987
- docs: { description: "Keep orchestr handlers in domain folders." },
988
- schema: [],
989
- messages: { noDomain: "Put handlers in a domain folder, e.g. orchestr/<domain>/{{file}}." }
990
- },
991
- applies: (file) => file.kind === "handler" && !file.domain,
992
- create: ({ file, report }) => ({ Program: () => {
993
- report({
994
- loc: FILE_START,
995
- messageId: "noDomain",
996
- data: { file: basename(file.path) }
997
- });
998
- } })
999
- });
1000
917
  var exports_default = defineRule({
1001
918
  meta: {
1002
919
  type: "problem",
1003
- docs: { description: "Require handler files to export their handler as default, and nothing else." },
920
+ docs: { description: "Require handler files to export their handler as default" },
1004
921
  schema: [],
1005
- messages: {
1006
- namedExport: "Handler files only export their handler. Laioutr registers the default export; move everything else to server/utils/.",
1007
- missingDefault: "Handler files export their handler as default, otherwise laioutr registers nothing."
1008
- }
922
+ messages: { missingDefault: "Export the handler as default, or it will fail the build." }
1009
923
  },
1010
924
  applies: (file) => file.kind === "handler",
1011
925
  create: ({ report }) => {
@@ -1014,22 +928,8 @@ var exports_default = defineRule({
1014
928
  "ExportDefaultDeclaration": () => {
1015
929
  hasDefault = true;
1016
930
  },
1017
- "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
- });
931
+ "ExportSpecifier": (node) => {
932
+ hasDefault ||= (node.exported.type === "Identifier" ? node.exported.name : node.exported.value) === "default";
1033
933
  },
1034
934
  "Program:exit": () => {
1035
935
  if (!hasDefault) report({
@@ -1050,9 +950,9 @@ const MIDDLEWARE = /* @__PURE__ */ new Set([
1050
950
  var files_default = defineRule({
1051
951
  meta: {
1052
952
  type: "suggestion",
1053
- docs: { description: "Keep server/middleware/ to orchestr and Nitro middleware." },
953
+ docs: { description: "Disallow files other than middleware in `server/middleware/`" },
1054
954
  schema: [],
1055
- messages: { helper: "server/middleware/ holds orchestr and Nitro middleware. Move helpers like this to server/utils/." }
955
+ messages: { helper: "Only middleware belongs in server/middleware/. Move this file to server/utils/." }
1056
956
  },
1057
957
  applies: (file) => file.kind === "middleware",
1058
958
  create: ({ report }) => {
@@ -1073,50 +973,14 @@ var files_default = defineRule({
1073
973
  };
1074
974
  }
1075
975
  });
1076
- const readPackageFile = cachedReader((text) => {
1077
- const json = JSON.parse(text);
1078
- return {
1079
- name: json.name,
1080
- dependencies: new Set([
1081
- json.dependencies,
1082
- json.devDependencies,
1083
- json.peerDependencies,
1084
- json.optionalDependencies
1085
- ].flatMap((deps) => Object.keys(deps ?? {})))
1086
- };
1087
- });
1088
- function readPackage(root) {
1089
- return readPackageFile(join(root, "package.json"));
1090
- }
1091
- var meta_default = defineRule({
1092
- meta: {
1093
- type: "problem",
1094
- docs: { description: "Require `meta({ app })` of an orchestr builder to be the package name." },
1095
- schema: [],
1096
- messages: { app: "meta({ app }) is '{{actual}}', but the package is '{{expected}}'. Traces use this name to tell which app's middleware ran." }
1097
- },
1098
- applies: (file) => file.side === "server",
1099
- create: ({ file, report }) => ({ CallExpression: (node) => {
1100
- const app = readMetaApp(node);
1101
- const expected = app && readPackage(file.root)?.name;
1102
- if (app && expected && app.value !== expected) report({
1103
- node: app.node,
1104
- messageId: "app",
1105
- data: {
1106
- actual: app.value,
1107
- expected
1108
- }
1109
- });
1110
- } })
1111
- });
1112
976
  var throws_default = defineRule({
1113
977
  meta: {
1114
978
  type: "problem",
1115
- docs: { description: "Disallow throwing in extendRequest(), which runs before every query, so a throw takes down every page." },
979
+ docs: { description: "Disallow throwing in `extendRequest()`" },
1116
980
  schema: [],
1117
981
  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."
982
+ throws: "A throw in extendRequest() breaks every page. Throw later instead, e.g. when the client is used.",
983
+ throwsInCall: "{{name}}() can throw ({{location}}), and a throw in extendRequest() breaks every page. Catch it here."
1120
984
  }
1121
985
  },
1122
986
  applies: (file) => file.side === "server",
@@ -1149,42 +1013,6 @@ var throws_default = defineRule({
1149
1013
  });
1150
1014
  } })
1151
1015
  });
1152
- var passthrough_default = defineRule({
1153
- meta: {
1154
- type: "problem",
1155
- docs: { description: "Disallow passthrough.require() in component resolvers." },
1156
- schema: [],
1157
- messages: { require: "Other queries for this entity may not set this passthrough, and require() then fails the whole resolver. Use passthrough.get() and handle a missing value." }
1158
- },
1159
- applies: (file) => file.handler === "resolver",
1160
- create: ({ report }) => ({ CallExpression: (node) => {
1161
- const { callee } = node;
1162
- if (callee.type === "MemberExpression" && nameOf(callee) === "require" && nameOf(callee.object) === "passthrough") report({
1163
- node: callee.property,
1164
- messageId: "require"
1165
- });
1166
- } })
1167
- });
1168
- var returns_default = defineRule({
1169
- meta: {
1170
- type: "problem",
1171
- docs: { description: "Require component resolvers to resolve fields to empty values rather than undefined or null." },
1172
- schema: [],
1173
- messages: { empty: "Studio shows a literal \"...\" for a field that resolves to undefined or null. Resolve to an empty value instead, e.g. ''." }
1174
- },
1175
- applies: (file) => file.handler === "resolver",
1176
- create: ({ report }) => ({ CallExpression: (node) => {
1177
- const [entity] = node.arguments;
1178
- if (node.callee.type !== "Identifier" || node.callee.name !== "$entity" || entity?.type !== "ObjectExpression") return;
1179
- for (const component of entity.properties) {
1180
- if (component.type !== "Property" || component.key.type === "Identifier" && component.key.name === "id") continue;
1181
- for (const object of componentObjects(component.value)) for (const field of object.properties) if (field.type === "Property" && canBeNullish(field.value)) report({
1182
- node: field.value,
1183
- messageId: "empty"
1184
- });
1185
- }
1186
- } })
1187
- });
1188
1016
  const PACKAGE = "@laioutr-core/canonical-types";
1189
1017
  const TOKEN_ID = /define\w*Token\(\s*["'`]([a-z0-9-]+)\//g;
1190
1018
  const cache = /* @__PURE__ */ new Map();
@@ -1206,9 +1034,9 @@ function canonicalNamespaces(root) {
1206
1034
  var namespaces_default = defineRule({
1207
1035
  meta: {
1208
1036
  type: "problem",
1209
- docs: { description: "Keep your own token ids out of the namespaces of Laioutr's canonical types." },
1037
+ docs: { description: "Disallow your own token ids in the namespaces of Laioutr's canonical types" },
1210
1038
  schema: [],
1211
- messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types. Put your own tokens under your app's namespace; otherwise registration order decides which handler wins." }
1039
+ messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types. Use your app's own namespace." }
1212
1040
  },
1213
1041
  applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
1214
1042
  create: ({ file, report }) => ({ CallExpression: (node) => {
@@ -1222,46 +1050,40 @@ var namespaces_default = defineRule({
1222
1050
  });
1223
1051
  } })
1224
1052
  });
1225
- function 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
- }
1053
+ function findProperty(object, key) {
1054
+ for (const entry of object.properties) if (entry.type === "Property" && !entry.computed && (entry.key.type === "Identifier" && entry.key.name === key || entry.key.type === "Literal" && entry.key.value === key)) return entry.value;
1232
1055
  }
1233
- function isNullable(schema) {
1234
- return chainMethods(schema).some((method) => method === "nullable" || method === "nullish");
1056
+ function findStringProperty(object, key) {
1057
+ const value = findProperty(object, key);
1058
+ return value?.type === "Literal" && typeof value.value === "string" ? {
1059
+ value: value.value,
1060
+ node: value
1061
+ } : void 0;
1062
+ }
1063
+ function objectElements(node) {
1064
+ if (node?.type !== "ArrayExpression") return [];
1065
+ return node.elements.filter((element) => element?.type === "ObjectExpression");
1235
1066
  }
1236
- var nullable_default = defineRule({
1237
- meta: {
1238
- type: "problem",
1239
- docs: { description: "Disallow nullable top-level fields in entity component tokens, which Studio can't bind." },
1240
- schema: [],
1241
- messages: { nullable: "Studio can't bind a nullable field. Resolve it to an empty value instead, e.g. '' or []." }
1242
- },
1243
- applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
1244
- create: ({ report }) => ({ CallExpression: (node) => {
1245
- const options = node.arguments[1];
1246
- if (node.callee.type !== "Identifier" || node.callee.name !== "defineEntityComponentToken" || options?.type !== "ObjectExpression") return;
1247
- for (const field of objectShape(findProperty(options, "schema"))?.properties ?? []) if (field.type === "Property" && isNullable(field.value)) report({
1248
- node: field,
1249
- messageId: "nullable"
1250
- });
1251
- } })
1252
- });
1253
1067
  const DEFINERS = {
1254
1068
  defineSection: {
1255
1069
  kind: "section",
1256
- folder: "sections",
1257
- prefix: "Section"
1070
+ folder: "sections"
1258
1071
  },
1259
1072
  defineBlock: {
1260
1073
  kind: "block",
1261
- folder: "blocks",
1262
- prefix: "Block"
1074
+ folder: "blocks"
1263
1075
  }
1264
1076
  };
1077
+ const PICKERS = /* @__PURE__ */ new Set([
1078
+ "select",
1079
+ "radio",
1080
+ "toggle_button"
1081
+ ]);
1082
+ const TEXTS = /* @__PURE__ */ new Set([
1083
+ "text",
1084
+ "textarea",
1085
+ "secret"
1086
+ ]);
1265
1087
  function readDefinition(call) {
1266
1088
  if (call.callee.type !== "Identifier" || !Object.hasOwn(DEFINERS, call.callee.name)) return;
1267
1089
  const [options] = call.arguments;
@@ -1277,7 +1099,6 @@ function readGroups(owner) {
1277
1099
  const node = "call" in owner ? owner.options : owner.node;
1278
1100
  return objectElements(node && findProperty(node, "schema")).map((group) => ({
1279
1101
  node: group,
1280
- label: findStringProperty(group, "label"),
1281
1102
  fields: objectElements(findProperty(group, "fields")).map(readField)
1282
1103
  }));
1283
1104
  }
@@ -1301,37 +1122,65 @@ function readAllFields(definition) {
1301
1122
  readFields(definition).forEach(visit);
1302
1123
  return fields;
1303
1124
  }
1125
+ function conditionFields(definition) {
1126
+ const names = /* @__PURE__ */ new Set();
1127
+ for (const field of readAllFields(definition)) {
1128
+ const condition = findProperty(field.node, "if");
1129
+ if (condition) walk(condition, (node) => {
1130
+ const [operator, name] = node.type === "ArrayExpression" ? node.elements : [];
1131
+ if (operator?.type === "Literal" && operator.value === "get" && name?.type === "Literal" && typeof name.value === "string") names.add(name.value.split(".")[0]);
1132
+ });
1133
+ }
1134
+ return names;
1135
+ }
1304
1136
  function expectedDefiner(kind) {
1305
1137
  return Object.keys(DEFINERS).find((definer) => DEFINERS[definer].kind === kind);
1306
1138
  }
1139
+ function fillValue(field, program, filename) {
1140
+ if (field.type === "checkbox") return { value: field.as === "visibility" };
1141
+ if (TEXTS.has(field.type ?? "")) return { value: "" };
1142
+ if (field.type === "content_alignment") {
1143
+ const axis = findStringProperty(field.node, "axis")?.value;
1144
+ return { value: !axis || axis === "both" ? "center-center" : "center" };
1145
+ }
1146
+ if (!PICKERS.has(field.type ?? "")) return;
1147
+ const property = findProperty(field.node, "options");
1148
+ const declared = property?.type === "Identifier" ? resolveConstant(property.name, program, filename) : property;
1149
+ let options = declared && unwrap(declared);
1150
+ if (options?.type === "CallExpression") options = options.arguments[0];
1151
+ const first = options?.type === "ArrayExpression" ? options.elements[0] : void 0;
1152
+ const value = first?.type === "ObjectExpression" ? findProperty(first, "value") : first;
1153
+ return value?.type === "Literal" ? { value: value.value } : void 0;
1154
+ }
1307
1155
  var description_default = defineRule({
1308
1156
  meta: {
1309
1157
  type: "suggestion",
1310
- docs: { description: "Require a studio.description on section and block definitions." },
1158
+ docs: { description: "Require a `studio.description` on section and block definitions" },
1311
1159
  schema: [],
1312
- messages: { description: "Studio shows studio.description in its section and block picker. Add one." }
1160
+ messages: { description: "Add a studio.description, so editors and AI agents know what this {{kind}} is for." }
1313
1161
  },
1314
1162
  applies: (file) => file.side === "app",
1315
1163
  create: ({ report }) => ({ CallExpression: (node) => {
1316
- const options = readDefinition(node)?.options;
1317
- if (!options) return;
1318
- const studio = findProperty(options, "studio");
1164
+ const definition = readDefinition(node);
1165
+ if (!definition?.options) return;
1166
+ const studio = findProperty(definition.options, "studio");
1319
1167
  if (studio && studio.type !== "ObjectExpression") return;
1320
1168
  const description = studio && findProperty(studio, "description");
1321
1169
  if (!description || description.type === "Literal" && String(description.value ?? "").trim() === "") report({
1322
1170
  node: node.callee,
1323
- messageId: "description"
1171
+ messageId: "description",
1172
+ data: { kind: DEFINERS[definition.definer].kind }
1324
1173
  });
1325
1174
  } })
1326
1175
  });
1327
1176
  var folder_default = defineRule({
1328
1177
  meta: {
1329
1178
  type: "problem",
1330
- docs: { description: "Keep defineSection() in app/sections/ and defineBlock() in app/blocks/, and every .vue there defined." },
1179
+ docs: { description: "Require sections in `app/sections/` and blocks in `app/blocks/`, and a definition in every `.vue` there" },
1331
1180
  schema: [],
1332
1181
  messages: {
1333
- notDefined: "Laioutr registers every .vue in {{folder}}/ as a Studio {{kind}}, but this file has no {{definer}}(). Move it to components/.",
1334
- wrongFolder: "{{definer}}() belongs in app/{{folder}}/."
1182
+ notDefined: "Every .vue file in this folder needs {{definer}}(). Add it, or move the file to components/.",
1183
+ wrongFolder: "{{definer}}() belongs in app/{{folder}}/. Move the file there."
1335
1184
  }
1336
1185
  },
1337
1186
  applies: (file) => file.side === "app",
@@ -1354,18 +1203,11 @@ var folder_default = defineRule({
1354
1203
  },
1355
1204
  "Program:exit": () => {
1356
1205
  const expected = expectedDefiner(file.kind);
1357
- if (expected && file.path.endsWith(".vue") && !definers.has(expected)) {
1358
- const { kind, folder } = DEFINERS[expected];
1359
- report({
1360
- loc: FILE_START,
1361
- messageId: "notDefined",
1362
- data: {
1363
- folder,
1364
- kind,
1365
- definer: expected
1366
- }
1367
- });
1368
- }
1206
+ if (expected && file.path.endsWith(".vue") && !definers.has(expected)) report({
1207
+ loc: FILE_START,
1208
+ messageId: "notDefined",
1209
+ data: { definer: expected }
1210
+ });
1369
1211
  }
1370
1212
  };
1371
1213
  }
@@ -1373,7 +1215,7 @@ var folder_default = defineRule({
1373
1215
  var name_default = defineRule({
1374
1216
  meta: {
1375
1217
  type: "problem",
1376
- docs: { description: "Require the `component` of a section or block definition to match its file name." },
1218
+ docs: { description: "Require the `component` of a definition to match its file name" },
1377
1219
  schema: [],
1378
1220
  messages: { componentName: "component: '{{actual}}' must match the file name '{{expected}}'." }
1379
1221
  },
@@ -1391,118 +1233,103 @@ var name_default = defineRule({
1391
1233
  });
1392
1234
  } })
1393
1235
  });
1394
- 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"]);
1236
+ const MESSAGES = {
1237
+ checkbox: "checkbox",
1238
+ content_alignment: "alignment",
1239
+ text: "text",
1240
+ textarea: "text",
1241
+ secret: "text"
1242
+ };
1425
1243
  var fallbacks_default = defineRule({
1426
1244
  meta: {
1427
1245
  type: "problem",
1428
- docs: { description: "Disallow fallbacks for props that never arrive as undefined: unset picker fields arrive as false, unset text fields as ''." },
1246
+ docs: { description: "Disallow `??` fallbacks on fields that frontend-core always fills" },
1429
1247
  schema: [],
1430
1248
  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 ||."
1249
+ picker: "This fallback is never used: an unset {{type}} gets its first option, {{fill}}. To use {{fallback}}, make it the first option.",
1250
+ alignment: "This fallback is never used: an unset content_alignment is {{fill}}. Remove the fallback.",
1251
+ checkbox: "This fallback is never used: an unset checkbox is false. To make true the default, invert the field, e.g. hideIcon instead of showIcon.",
1252
+ text: "An unset {{type}} field is '', so ?? never falls back. Use || instead."
1434
1253
  }
1435
1254
  },
1436
1255
  applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
1437
1256
  create: ({ context, report, visitTemplate }) => {
1438
- const types = /* @__PURE__ */ new Map();
1439
- const propsVariable = findPropsVariable(context.sourceCode.ast);
1440
- const reads = [];
1257
+ const program = context.sourceCode.ast;
1258
+ const propsVariable = findPropsVariable(program);
1259
+ const fills = /* @__PURE__ */ new Map();
1260
+ const fallbacks = [];
1441
1261
  const propOf = (node, template) => {
1442
1262
  if (template && node.type === "Identifier") return node.name;
1443
1263
  const props = node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.object.type === "Identifier" ? node.object.name : void 0;
1444
1264
  return props && (props === propsVariable || template && props === "$props") && node.type === "MemberExpression" && node.property.type === "Identifier" ? node.property.name : void 0;
1445
1265
  };
1446
- const collect = (template) => ({
1447
- LogicalExpression: (node) => {
1448
- const prop = node.operator === "??" ? propOf(node.left, template) : void 0;
1449
- if (prop) reads.push({
1450
- node,
1451
- prop,
1452
- check: "??"
1453
- });
1454
- },
1455
- BinaryExpression: (node) => {
1456
- if (node.operator !== "!==" && node.operator !== "===" && node.operator !== "!=" && node.operator !== "==") return;
1457
- for (const [side, other] of [[node.left, node.right], [node.right, node.left]]) {
1458
- const prop = propOf(side, template);
1459
- const value = other.type === "Identifier" && other.name === "undefined" ? "undefined" : other.type === "Literal" && other.value === null && !("regex" in other) ? "null" : other.type === "Literal" && other.value === false && node.operator === "!==" ? "false" : void 0;
1460
- if (prop && value) reads.push({
1461
- node,
1462
- prop,
1463
- check: `${node.operator} ${value}`
1464
- });
1465
- }
1466
- }
1467
- });
1266
+ const collect = (template) => ({ LogicalExpression: (node) => {
1267
+ const prop = node.operator === "??" ? propOf(node.left, template) : void 0;
1268
+ if (prop && !isNullish(node.right)) fallbacks.push({
1269
+ node,
1270
+ prop
1271
+ });
1272
+ } });
1468
1273
  visitTemplate(collect(true));
1469
1274
  return {
1470
1275
  ...collect(false),
1471
1276
  "CallExpression": (node) => {
1472
1277
  const definition = readDefinition(node);
1473
- for (const { name, type, as } of definition ? readFields(definition) : []) if (name && type && !as) types.set(name.value, type);
1278
+ for (const field of definition ? readFields(definition) : []) {
1279
+ const fill = field.name && field.type && !field.as ? fillValue(field, program, context.filename) : void 0;
1280
+ if (fill) fills.set(field.name.value, {
1281
+ type: field.type,
1282
+ value: fill.value
1283
+ });
1284
+ }
1474
1285
  },
1475
1286
  "Program:exit": () => {
1476
- for (const { node, prop, check } of reads) {
1477
- const type = types.get(prop);
1478
- if (type === "checkbox") report({
1479
- node,
1480
- messageId: "checkbox",
1481
- data: { check }
1482
- });
1483
- else if (type && FALSE_WHEN_UNSET.has(type)) report({
1287
+ for (const { node, prop } of fallbacks) {
1288
+ const field = fills.get(prop);
1289
+ if (!field || node.right.type === "Literal" && node.right.value === field.value) continue;
1290
+ const messageId = MESSAGES[field.type] ?? "picker";
1291
+ const fill = typeof field.value === "string" ? `'${field.value}'` : String(field.value);
1292
+ report({
1484
1293
  node,
1485
- messageId: "picker",
1294
+ messageId,
1486
1295
  data: {
1487
- type,
1488
- check
1296
+ type: field.type,
1297
+ fill,
1298
+ fallback: context.sourceCode.getText(node.right)
1489
1299
  }
1490
1300
  });
1491
- else if (type && EMPTY_WHEN_UNSET.has(type) && check === "??") report({
1492
- node,
1493
- messageId: "text",
1494
- data: { type }
1495
- });
1496
1301
  }
1497
1302
  }
1498
1303
  };
1499
1304
  }
1500
1305
  });
1501
1306
  const PROP_READ = /\.props\??\.([A-Z_$][\w$]*)|\.props\??\.?\[\s*['"]([^'"]+)['"]\s*\]|['"]([^'"]+)['"]\s+in\s+[\w$.?]+\.props\b/gi;
1502
- const readPropNames = cachedReader((text) => [...text.matchAll(PROP_READ)].map((match) => match[1] ?? match[2] ?? match[3]));
1307
+ const PROPS_ALIAS = /\b(?:const|let|var)\s+([\w$]+)\s*=\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/g;
1308
+ const PROPS_DESTRUCTURING = /\{([^{}]*)\}\s*=\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/g;
1309
+ const PROPS_SPREAD = /\.\.\.\s*[\w$.?]+\.props\b(?!\s*(?:\?\.|[.[]))/;
1310
+ const readSlotProps = cachedReader((text) => {
1311
+ const names = [...text.matchAll(PROP_READ)].map((match) => match[1] ?? match[2] ?? match[3]);
1312
+ let all = PROPS_SPREAD.test(text);
1313
+ for (const [, alias] of text.matchAll(PROPS_ALIAS)) {
1314
+ const name = alias.replaceAll("$", "\\$");
1315
+ const reads = new RegExp(`\\b${name}\\??\\.([A-Z_$][\\w$]*)|\\b${name}\\??\\.?\\[\\s*['"]([^'"]+)['"]\\s*\\]`, "gi");
1316
+ for (const match of text.matchAll(reads)) names.push(match[1] ?? match[2]);
1317
+ }
1318
+ for (const [, list] of text.matchAll(PROPS_DESTRUCTURING)) for (const entry of list.split(",").map((item) => item.trim()).filter(Boolean)) {
1319
+ all ||= entry.startsWith("...");
1320
+ names.push(entry.split(/[:=]/)[0].trim());
1321
+ }
1322
+ return {
1323
+ names,
1324
+ all
1325
+ };
1326
+ });
1503
1327
  function slotProps(root) {
1504
1328
  const folder = join(root, "src/runtime/app");
1505
- const names = /* @__PURE__ */ new Set();
1329
+ const result = {
1330
+ names: /* @__PURE__ */ new Set(),
1331
+ all: false
1332
+ };
1506
1333
  let files;
1507
1334
  try {
1508
1335
  files = readdirSync(folder, {
@@ -1510,10 +1337,14 @@ function slotProps(root) {
1510
1337
  encoding: "utf8"
1511
1338
  }).filter((file) => /\.(?:vue|[cm]?[jt]s)$/.test(file));
1512
1339
  } catch {
1513
- return names;
1340
+ return result;
1514
1341
  }
1515
- for (const file of files) readPropNames(join(folder, file))?.forEach((name) => names.add(name));
1516
- return names;
1342
+ for (const file of files) {
1343
+ const read = readSlotProps(join(folder, file));
1344
+ read?.names.forEach((name) => result.names.add(name));
1345
+ result.all ||= read?.all ?? false;
1346
+ }
1347
+ return result;
1517
1348
  }
1518
1349
  const NOT_READ = /* @__PURE__ */ new Set([
1519
1350
  "info",
@@ -1523,9 +1354,9 @@ const NOT_READ = /* @__PURE__ */ new Set([
1523
1354
  var unused_default = defineRule({
1524
1355
  meta: {
1525
1356
  type: "suggestion",
1526
- docs: { description: "Disallow schema fields the section or block never reads." },
1357
+ docs: { description: "Disallow schema fields the component never uses" },
1527
1358
  schema: [],
1528
- messages: { unused: "Studio shows '{{name}}' to editors, but the component never reads it. Use the field or remove it." }
1359
+ messages: { unused: "The component never uses '{{name}}'. Use the field or remove it." }
1529
1360
  },
1530
1361
  applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
1531
1362
  create: ({ context, file, report, visitTemplate }) => {
@@ -1533,6 +1364,7 @@ var unused_default = defineRule({
1533
1364
  const used = /* @__PURE__ */ new Set();
1534
1365
  const propsVariable = findPropsVariable(context.sourceCode.ast);
1535
1366
  let hasProps = false;
1367
+ let block = false;
1536
1368
  let escapes = false;
1537
1369
  const track = (node) => {
1538
1370
  const names = usedProps(node);
@@ -1551,7 +1383,10 @@ var unused_default = defineRule({
1551
1383
  "CallExpression": (node) => {
1552
1384
  hasProps ||= isDefineProps(node);
1553
1385
  const definition = readDefinition(node);
1554
- for (const { name, type, as } of definition ? readFields(definition) : []) if (name && !as && !NOT_READ.has(type ?? "")) fields.set(name.value, name.node);
1386
+ if (!definition) return;
1387
+ block ||= definition.definer === "defineBlock";
1388
+ conditionFields(definition).forEach((name) => used.add(name));
1389
+ for (const { name, type, as } of readFields(definition)) if (name && !as && !NOT_READ.has(type ?? "")) fields.set(name.value, name.node);
1555
1390
  },
1556
1391
  "Identifier": (node) => {
1557
1392
  const declaration = node.parent.type === "VariableDeclarator" && node.parent.id === node;
@@ -1560,8 +1395,9 @@ var unused_default = defineRule({
1560
1395
  "Program:exit": () => {
1561
1396
  if (!hasProps || escapes) return;
1562
1397
  const unused = [...fields].filter(([name]) => !used.has(name));
1563
- const readBySection = unused.length > 0 && file.kind === "block" ? slotProps(file.root) : /* @__PURE__ */ new Set();
1564
- for (const [name, node] of unused) if (!readBySection.has(name)) report({
1398
+ const readBySection = unused.length > 0 && block ? slotProps(file.root) : void 0;
1399
+ if (readBySection?.all) return;
1400
+ for (const [name, node] of unused) if (!readBySection?.names.has(name)) report({
1565
1401
  node,
1566
1402
  messageId: "unused",
1567
1403
  data: { name }
@@ -1570,67 +1406,40 @@ var unused_default = defineRule({
1570
1406
  };
1571
1407
  }
1572
1408
  });
1573
- const CAMEL_CASE = /^[a-z][a-zA-Z0-9]*$/;
1574
1409
  var casing_default = defineRule({
1575
1410
  meta: {
1576
1411
  type: "problem",
1577
- docs: { description: "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." },
1412
+ docs: { description: "Disallow schema field names that Vue renames or rejects" },
1600
1413
  schema: [],
1601
1414
  messages: {
1602
- panel: "Studio's schema has the panels Content, Design and Rules. Move these fields into one of them.",
1603
- order: "'{{label}}' goes before '{{previous}}': the panels are Content, Design, Rules."
1415
+ hyphen: "Vue renames '{{name}}' to '{{suggestion}}', so the component never gets this field. Name it '{{suggestion}}'.",
1416
+ dollar: "Vue doesn't allow props that start with $, so the component never gets this field. Rename it."
1604
1417
  }
1605
1418
  },
1606
1419
  applies: (file) => file.side === "app",
1607
1420
  create: ({ report }) => ({ CallExpression: (node) => {
1608
1421
  const definition = readDefinition(node);
1609
- let previous;
1610
- for (const { label } of definition ? readGroups(definition) : []) {
1611
- if (!label) continue;
1612
- if (!PANELS.includes(label.value)) report({
1613
- node: label.node,
1614
- messageId: "panel"
1615
- });
1616
- else if (previous && PANELS.indexOf(label.value) < PANELS.indexOf(previous)) report({
1617
- node: label.node,
1618
- messageId: "order",
1619
- data: {
1620
- label: label.value,
1621
- previous
1622
- }
1623
- });
1624
- else previous = label.value;
1625
- }
1422
+ for (const { name } of definition ? readFields(definition) : []) if (name?.value.startsWith("$")) report({
1423
+ node: name.node,
1424
+ messageId: "dollar",
1425
+ data: { name: name.value }
1426
+ });
1427
+ else if (name?.value.includes("-")) report({
1428
+ node: name.node,
1429
+ messageId: "hyphen",
1430
+ data: {
1431
+ name: name.value,
1432
+ suggestion: camelCase(name.value)
1433
+ }
1434
+ });
1626
1435
  } })
1627
1436
  });
1628
1437
  var required_default = defineRule({
1629
1438
  meta: {
1630
1439
  type: "problem",
1631
- docs: { description: "Disallow `required` on schema fields, which Studio doesn't support." },
1440
+ docs: { description: "Disallow `required` on schema fields" },
1632
1441
  schema: [],
1633
- messages: { required: "Schema fields have no required option, so Studio ignores it. Give the field a default instead." }
1442
+ messages: { required: "Studio ignores required. Give the field a default instead." }
1634
1443
  },
1635
1444
  applies: (file) => file.side === "app",
1636
1445
  create: ({ report }) => ({ CallExpression: (node) => {
@@ -1645,21 +1454,22 @@ var required_default = defineRule({
1645
1454
  } })
1646
1455
  });
1647
1456
  const RESERVED = /* @__PURE__ */ new Set([
1648
- "style",
1649
- "class",
1650
1457
  "key",
1651
1458
  "ref",
1652
- "is",
1653
- "slot",
1654
- "refFor",
1655
- "refKey"
1459
+ "ref_for",
1460
+ "ref_key",
1461
+ "class",
1462
+ "style"
1656
1463
  ]);
1657
1464
  var reserved_default = defineRule({
1658
1465
  meta: {
1659
1466
  type: "problem",
1660
- docs: { description: "Disallow top-level schema field names that Vue swallows before they reach the component's props." },
1467
+ docs: { description: "Disallow schema field names that Vue or frontend-core already use" },
1661
1468
  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." }
1469
+ messages: {
1470
+ reserved: "Vue uses '{{name}}' itself, so the component never gets this field. Rename it.",
1471
+ slots: "frontend-core passes the blocks in the slots prop, so it overwrites this field. Rename it."
1472
+ }
1663
1473
  },
1664
1474
  applies: (file) => file.side === "app",
1665
1475
  create: ({ report }) => ({ CallExpression: (node) => {
@@ -1669,36 +1479,12 @@ var reserved_default = defineRule({
1669
1479
  messageId: "reserved",
1670
1480
  data: { name: name.value }
1671
1481
  });
1482
+ else if (name?.value === "slots" && definition?.definer === "defineSection") report({
1483
+ node: name.node,
1484
+ messageId: "slots"
1485
+ });
1672
1486
  } })
1673
1487
  });
1674
- var id_default = defineRule({
1675
- meta: {
1676
- type: "problem",
1677
- docs: { description: "Disallow an id on the root element of a section or block, which frontend-core replaces with its own." },
1678
- schema: [],
1679
- messages: { rootId: "frontend-core gives the root element of every section and block its own id, so this one never reaches the page. Put it on an inner element." }
1680
- },
1681
- applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
1682
- create: ({ context, report }) => {
1683
- let defined = false;
1684
- return {
1685
- "CallExpression": (node) => {
1686
- defined ||= readDefinition(node) !== void 0;
1687
- },
1688
- "Program:exit": () => {
1689
- const template = context.sourceCode.ast.templateBody;
1690
- if (!defined || !template) return;
1691
- for (const root of template.children) {
1692
- const id = root.type === "VElement" ? findAttribute(root, "id") : void 0;
1693
- if (id) report({
1694
- loc: id.loc,
1695
- messageId: "rootId"
1696
- });
1697
- }
1698
- }
1699
- };
1700
- }
1701
- });
1702
1488
  const parseVueFile = cachedReader((text) => {
1703
1489
  try {
1704
1490
  return {
@@ -1713,42 +1499,48 @@ const parseVueFile = cachedReader((text) => {
1713
1499
  return;
1714
1500
  }
1715
1501
  });
1502
+ const OWN_ATTRS = /inheritAttrs\s*:\s*false/;
1716
1503
  var root_default = defineRule({
1717
1504
  meta: {
1718
1505
  type: "problem",
1719
- docs: { description: "Require sections and blocks to render one root element, which carries frontend-core's data-lfc-* markers." },
1506
+ docs: { description: "Require sections and blocks to render one root element" },
1720
1507
  schema: [],
1721
1508
  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."
1509
+ roots: "Studio can't select a {{kind}} with more than one root element. Wrap the content in one element.",
1510
+ componentRoots: "<{{name}}> has more than one root element, so Studio can't select this {{kind}}. Give it one root, or wrap it here."
1724
1511
  }
1725
1512
  },
1726
1513
  applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
1727
1514
  create: ({ context, report }) => {
1728
- let defined = false;
1515
+ let kind;
1729
1516
  return {
1730
1517
  "CallExpression": (node) => {
1731
- defined ||= readDefinition(node) !== void 0;
1518
+ const definition = readDefinition(node);
1519
+ kind ??= definition && DEFINERS[definition.definer].kind;
1732
1520
  },
1733
1521
  "Program:exit": () => {
1734
1522
  const program = context.sourceCode.ast;
1735
1523
  const template = program.templateBody;
1736
- if (!defined || !template) return;
1524
+ if (!kind || !template || OWN_ATTRS.test(context.sourceCode.text)) return;
1737
1525
  const [root, second] = renderedRoots(template);
1738
1526
  if (second) {
1739
1527
  report({
1740
1528
  loc: second.startTag.loc,
1741
- messageId: "roots"
1529
+ messageId: "roots",
1530
+ data: { kind }
1742
1531
  });
1743
1532
  return;
1744
1533
  }
1745
1534
  const file = root && importedComponent(program, context.filename, root.rawName);
1746
1535
  const component = file ? parseVueFile(file) : void 0;
1747
1536
  const componentTemplate = component?.ast.templateBody;
1748
- if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 && !/inheritAttrs\s*:\s*false/.test(component.text)) report({
1537
+ if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 && !OWN_ATTRS.test(component.text)) report({
1749
1538
  loc: root.startTag.loc,
1750
1539
  messageId: "componentRoots",
1751
- data: { name: root.rawName }
1540
+ data: {
1541
+ name: root.rawName,
1542
+ kind
1543
+ }
1752
1544
  });
1753
1545
  }
1754
1546
  };
@@ -1757,9 +1549,9 @@ var root_default = defineRule({
1757
1549
  var slots_default = defineRule({
1758
1550
  meta: {
1759
1551
  type: "problem",
1760
- docs: { description: "Disallow counting slot children, since Studio passes all blocks of a slot as one Fragment." },
1552
+ docs: { description: "Disallow counting the blocks of a slot with `.length`" },
1761
1553
  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." }
1554
+ messages: { length: "For blocks, this is always 1: frontend-core passes them as one Fragment. Count the Fragment's children instead." }
1763
1555
  },
1764
1556
  applies: (file) => file.side === "app",
1765
1557
  create: ({ report, visitTemplate }) => {
@@ -1788,7 +1580,7 @@ var slots_default = defineRule({
1788
1580
  var bundle_default = defineRule({
1789
1581
  meta: {
1790
1582
  type: "problem",
1791
- docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins, which every page loads." },
1583
+ docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins" },
1792
1584
  schema: [{
1793
1585
  type: "object",
1794
1586
  properties: { packages: {
@@ -1797,7 +1589,7 @@ var bundle_default = defineRule({
1797
1589
  } },
1798
1590
  additionalProperties: false
1799
1591
  }],
1800
- messages: { heavy: "Laioutr registers sections, blocks and plugins globally, so '{{name}}' lands in the chunk every page loads. Load it with import() or defineAsyncComponent() instead." }
1592
+ messages: { heavy: "This loads '{{name}}' on every page. Load it with import() or defineAsyncComponent() instead." }
1801
1593
  },
1802
1594
  applies: (file) => file.kind === "section" || file.kind === "block" || file.kind === "app-plugin",
1803
1595
  create: ({ context, report }) => {
@@ -1812,6 +1604,21 @@ var bundle_default = defineRule({
1812
1604
  } };
1813
1605
  }
1814
1606
  });
1607
+ const readPackageFile = cachedReader((text) => {
1608
+ const json = JSON.parse(text);
1609
+ return {
1610
+ name: json.name,
1611
+ dependencies: new Set([
1612
+ json.dependencies,
1613
+ json.devDependencies,
1614
+ json.peerDependencies,
1615
+ json.optionalDependencies
1616
+ ].flatMap((deps) => Object.keys(deps ?? {})))
1617
+ };
1618
+ });
1619
+ function readPackage(root) {
1620
+ return readPackageFile(join(root, "package.json"));
1621
+ }
1815
1622
  function isRuntimeConfig(node, variables) {
1816
1623
  if (node.type === "CallExpression") return node.callee.type === "Identifier" && node.callee.name === "useRuntimeConfig";
1817
1624
  if (node.type === "Identifier") return variables.has(node.name);
@@ -1827,9 +1634,9 @@ function setupOptions(node) {
1827
1634
  var keys_default = defineRule({
1828
1635
  meta: {
1829
1636
  type: "problem",
1830
- docs: { description: "Require runtime config keys of apps to be the package itself or one of its dependencies." },
1637
+ docs: { description: "Require runtime config keys to be this package or one of its dependencies" },
1831
1638
  schema: [],
1832
- messages: { key: "'{{key}}' isn't this package or one of its dependencies, so its config is missing at runtime. Use the key of an installed app." }
1639
+ messages: { key: "'{{key}}' isn't this package or one of its dependencies, so this config is undefined." }
1833
1640
  },
1834
1641
  applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
1835
1642
  create: ({ file, report }) => {
@@ -1854,9 +1661,9 @@ var keys_default = defineRule({
1854
1661
  var public_default = defineRule({
1855
1662
  meta: {
1856
1663
  type: "problem",
1857
- docs: { description: "Disallow copying module options into the public runtime config, which reaches the browser." },
1664
+ docs: { description: "Disallow copying all module options into the public runtime config" },
1858
1665
  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." }
1666
+ messages: { options: "This sends all module options to the browser, secrets included. Copy only the public ones, e.g. options.storefrontUrl." }
1860
1667
  },
1861
1668
  applies: (file) => file.side === "build",
1862
1669
  create: ({ report }) => ({ AssignmentExpression: (node) => {
@@ -1867,42 +1674,25 @@ var public_default = defineRule({
1867
1674
  walk(node.right, (child) => {
1868
1675
  if (child.type !== "Identifier" || child.name !== options || !isReference(child)) return;
1869
1676
  const { parent } = child;
1870
- if (!(parent.type === "MemberExpression" && parent.object === child && nameOf(parent) === "public")) report({
1677
+ if (parent.type !== "MemberExpression" || parent.object !== child) report({
1871
1678
  node: child,
1872
1679
  messageId: "options"
1873
1680
  });
1874
1681
  });
1875
1682
  } })
1876
1683
  });
1877
- var folders_default = defineRule({
1878
- meta: {
1879
- type: "suggestion",
1880
- docs: { description: "Require runtime files to live in the folders of the Laioutr app layout." },
1881
- schema: [],
1882
- messages: { unknown: "larrylint doesn't know {{folder}}, so no rule checks what's in it. Move the file into one of the folders of the Laioutr app layout." }
1883
- },
1884
- applies: (file) => (file.side === "app" || file.side === "server") && file.kind === "other",
1885
- create: ({ file, report }) => ({ Program: () => {
1886
- const [side, folder] = file.path.slice(file.path.indexOf("/runtime/") + 9).split("/");
1887
- report({
1888
- loc: FILE_START,
1889
- messageId: "unknown",
1890
- data: { folder: `${side}/${folder}/` }
1891
- });
1892
- } })
1893
- });
1894
1684
  const LABELS = {
1895
- "section": "section",
1896
- "block": "block",
1897
- "component": "component",
1898
- "composable": "composable",
1899
- "app-plugin": "plugin",
1900
- "override": "override",
1901
- "route": "API route",
1902
- "nitro-plugin": "Nitro plugin",
1903
- "orchestr-plugin": "orchestr plugin",
1904
- "orchestr-file": "orchestr",
1905
- "media-library": "media library"
1685
+ "section": "sections",
1686
+ "block": "blocks",
1687
+ "component": "components",
1688
+ "composable": "composables",
1689
+ "app-plugin": "plugins",
1690
+ "override": "overrides",
1691
+ "route": "API routes",
1692
+ "nitro-plugin": "Nitro plugins",
1693
+ "orchestr-plugin": "orchestr plugins",
1694
+ "orchestr-file": "orchestr files",
1695
+ "media-library": "media libraries"
1906
1696
  };
1907
1697
  const ABOVE_APP_UTILS = /* @__PURE__ */ new Set([
1908
1698
  "section",
@@ -1925,13 +1715,7 @@ const ABOVE_SERVER_UTILS = /* @__PURE__ */ new Set([
1925
1715
  "orchestr-file",
1926
1716
  "media-library"
1927
1717
  ]);
1928
- const BUILDER_USERS = /* @__PURE__ */ new Set([
1929
- "handler",
1930
- "orchestr-file",
1931
- "middleware",
1932
- "media-library"
1933
- ]);
1934
- function findViolation(importer, target, typeOnly, sharedDomains) {
1718
+ function findViolation(importer, target, typeOnly) {
1935
1719
  if (importer.side === "app" && target.side === "server") return { messageId: "appImportsServer" };
1936
1720
  if (importer.side === "server" && target.side === "app") return { messageId: "serverImportsApp" };
1937
1721
  if (importer.side === "shared" && (target.side === "app" || target.side === "server")) return {
@@ -1942,25 +1726,10 @@ function findViolation(importer, target, typeOnly, sharedDomains) {
1942
1726
  if (target.kind === "handler") return { messageId: "handlerImported" };
1943
1727
  if (typeOnly) return;
1944
1728
  if (target.kind === "client" && importer.kind === "handler") return { messageId: "clientInHandler" };
1945
- if (target.kind === "client" && importer.kind === "server-util") return { messageId: "clientInUtil" };
1946
- if (target.kind === "middleware" && !BUILDER_USERS.has(importer.kind)) return { messageId: "middlewareImported" };
1947
1729
  if (importer.kind === "server-util" && ABOVE_SERVER_UTILS.has(target.kind)) return {
1948
1730
  messageId: "serverUtilImportsUp",
1949
1731
  data: { kind: LABELS[target.kind] }
1950
1732
  };
1951
- const from = domainOf(importer);
1952
- const to = domainOf(target);
1953
- if (from && to && from !== to && !sharedDomains.includes(to)) return {
1954
- messageId: "crossDomain",
1955
- data: {
1956
- from,
1957
- to
1958
- }
1959
- };
1960
- if (importer.kind === "server-util" && !from && to && !sharedDomains.includes(to)) return {
1961
- messageId: "sharedImportsDomain",
1962
- data: { to }
1963
- };
1964
1733
  if (importer.kind === "app-util" && ABOVE_APP_UTILS.has(target.kind)) return {
1965
1734
  messageId: "appUtilImportsUp",
1966
1735
  data: { kind: LABELS[target.kind] }
@@ -1974,36 +1743,24 @@ function findViolation(importer, target, typeOnly, sharedDomains) {
1974
1743
  var layers_default = defineRule({
1975
1744
  meta: {
1976
1745
  type: "problem",
1977
- docs: { description: "Enforce 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
- }],
1746
+ docs: { description: "Enforce which parts of a Laioutr app can import each other" },
1747
+ schema: [],
1986
1748
  messages: {
1987
- appImportsServer: "App code can't import server code. Move what both 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/."
1749
+ appImportsServer: "App code can't import server code. Move what both need to src/runtime/shared/.",
1750
+ serverImportsApp: "Server code can't import app code. Move what both need to src/runtime/shared/.",
1751
+ sharedImportsSide: "Shared code can't import from {{side}}/.",
1752
+ runtimeImportsBuild: "Runtime code can't import build code from src/, like module.ts. Type imports are fine.",
1753
+ handlerImported: "Don't import orchestr handlers. Move the code you need to server/utils/.",
1754
+ clientInHandler: "Handlers get API clients from the orchestr context, not by import. Move other code to server/utils/.",
1755
+ serverUtilImportsUp: "Server utils can't import {{kind}}. Type imports are fine.",
1756
+ appUtilImportsUp: "App utils can't import {{kind}}. Type imports are fine.",
1757
+ composableImportsUp: "Composables can't import {{kind}}. Type imports are fine.",
1758
+ componentImportsSection: "Components can't import sections.",
1759
+ nodeBuiltin: "'{{name}}' only works on the server. Move this code to server/."
2002
1760
  }
2003
1761
  },
2004
1762
  applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
2005
1763
  create: ({ context, file: importer, report }) => {
2006
- const { sharedDomains = [] } = context.options[0] ?? {};
2007
1764
  const checkImport = (node, source, typeOnly) => {
2008
1765
  if (typeof source !== "string") return;
2009
1766
  if (source.startsWith("node:")) {
@@ -2016,7 +1773,7 @@ var layers_default = defineRule({
2016
1773
  }
2017
1774
  const path = resolveImport(context.filename, source);
2018
1775
  const target = path ? classify(path) : void 0;
2019
- const violation = target && findViolation(importer, target, typeOnly, sharedDomains);
1776
+ const violation = target && findViolation(importer, target, typeOnly);
2020
1777
  if (violation) report({
2021
1778
  node,
2022
1779
  ...violation
@@ -2045,55 +1802,40 @@ const plugin = {
2045
1802
  },
2046
1803
  rules: {
2047
1804
  "layers": layers_default,
2048
- "known-folders": folders_default,
2049
1805
  "heavy-imports": bundle_default,
2050
1806
  "config-keys": keys_default,
2051
1807
  "public-config": public_default,
2052
1808
  "orchestr-files": files_default$1,
2053
1809
  "orchestr-cookies": cookies_default,
2054
- "handler-domains": domains_default,
2055
1810
  "handler-exports": exports_default,
2056
- "handler-context": context_default,
2057
- "resolver-undefined": returns_default,
2058
- "resolver-passthrough": passthrough_default,
2059
- "action-errors": errors_default,
2060
1811
  "middleware-files": files_default,
2061
1812
  "initware-throws": throws_default,
2062
- "orchestr-meta": meta_default,
2063
1813
  "token-namespaces": namespaces_default,
2064
- "token-nullable": nullable_default,
2065
1814
  "definition-folder": folder_default,
2066
- "definition-prefix": prefix_default,
2067
1815
  "component-name": name_default,
2068
1816
  "definition-description": description_default,
2069
- "root-id": id_default,
2070
1817
  "single-root": root_default,
2071
1818
  "slot-children": slots_default,
2072
1819
  "reserved-field-names": reserved_default,
2073
1820
  "field-name-case": casing_default,
2074
1821
  "required-fields": required_default,
2075
- "schema-groups": groups_default,
2076
1822
  "unused-fields": unused_default,
2077
1823
  "dead-fallbacks": fallbacks_default,
2078
1824
  "button-type": button_default,
2079
- "ui-kit-tags": tags_default,
2080
1825
  "mutation-errors": mutations_default,
2081
- "orchestr-store": store_default,
2082
1826
  "resolve-result": resolve_default,
2083
1827
  "hand-built-links": paths_default,
2084
- "internal-anchors": anchors_default,
2085
- "money": money_default
1828
+ "internal-anchors": anchors_default
2086
1829
  }
2087
1830
  };
2088
1831
  async function larrylint(options = {}) {
2089
- const { sharedDomains, heavyPackages } = await loadLarrylintConfig(options.cwd);
1832
+ const { heavyPackages } = await loadLarrylintConfig(options.cwd);
2090
1833
  return [{
2091
1834
  name: "larrylint",
2092
1835
  files: ["**/src/**/*.{ts,mts,cts,js,mjs,cjs,vue}"],
2093
1836
  plugins: { larrylint: plugin },
2094
1837
  rules: {
2095
1838
  ...Object.fromEntries(Object.keys(plugin.rules).map((id) => [`larrylint/${id}`, "error"])),
2096
- "larrylint/layers": ["error", { sharedDomains }],
2097
1839
  "larrylint/heavy-imports": ["error", { packages: heavyPackages }]
2098
1840
  }
2099
1841
  }];