larrylint 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,12 +33,12 @@ After that, `eslint .` and your editor report larrylint's rules next to your own
33
33
 
34
34
  | Rule | Description |
35
35
  | --- | --- |
36
- | `larrylint/layers` | App, server and shared code stay apart, and runtime code doesn't import build-time code, or `node:` modules outside the server. Nothing imports orchestr handlers, and handlers get API clients from the orchestr context. Utils are the bottom layer, followed by composables, components and sections. |
36
+ | `larrylint/layers` | App, server and shared code don't import each other, runtime code doesn't import build-time code, and `node:` modules stay on the server. As Laioutr's coding standards ask, handlers get API clients from the orchestr context, nothing imports handler files, and components don't import sections or blocks. |
37
37
  | `larrylint/heavy-imports` | Sections, blocks and plugins load the packages in `heavyPackages` with `import()`: frontend-core imports every section and block up front, and plugins run on every page, so a static import loads the package on every page. |
38
38
  | `larrylint/config-keys` | Runtime config keys like `'@laioutr-app/shopware'` are the app itself or one of its dependencies, otherwise the config is missing at runtime. |
39
39
  | `larrylint/public-config` | The module doesn't copy its whole options object into `runtimeConfig.public`, which would send every secret in it to the browser. |
40
40
 
41
- Type imports are fine across most layers, since they don't end up in the bundle.
41
+ Type imports are always fine, since they don't end up in the bundle.
42
42
 
43
43
  ### Orchestr
44
44
 
@@ -47,7 +47,6 @@ Type imports are fine across most layers, since they don't end up in the bundle.
47
47
  | `larrylint/orchestr-files` | Laioutr loads every file in `orchestr/` as a Nitro plugin, so only handler files belong there. |
48
48
  | `larrylint/orchestr-cookies` | Cookies and headers are only written in `extendRequest()` and action handlers, since query, link and resolver handlers and `use()` middleware can run after the headers are sent. Cookies go through frontend-core's `setManagedCookie()` and `deleteManagedCookie()`, which the Studio preview needs. |
49
49
  | `larrylint/handler-exports` | Handler files export their handler as default. Without one, the build fails. |
50
- | `larrylint/middleware-files` | `server/middleware/` only holds orchestr and Nitro middleware; helpers go to `server/utils/`. |
51
50
  | `larrylint/initware-throws` | `extendRequest()` doesn't throw, directly or through a function it calls: it runs before every query, so a throw breaks every page. |
52
51
  | `larrylint/token-namespaces` | Your own tokens stay out of the namespaces of Laioutr's canonical types, like `ecommerce/`, where a later canonical-types release or another app can take the same id. |
53
52
 
@@ -55,7 +54,7 @@ Type imports are fine across most layers, since they don't end up in the bundle.
55
54
 
56
55
  | Rule | Description |
57
56
  | --- | --- |
58
- | `larrylint/definition-folder` | `defineSection()` lives in `app/sections/` and `defineBlock()` in `app/blocks/` (or `section/` and `block/`), and every `.vue` there has one. |
57
+ | `larrylint/definition-folder` | `defineSection()` and `defineBlock()` live where `module.ts` registers sections and blocks, and every `.vue` registered there has one. Files that `module.ts` wires up by hand, like replacements for upstream sections, can live anywhere. |
59
58
  | `larrylint/component-name` | The `component` of a definition matches its file name. |
60
59
  | `larrylint/definition-description` | Definitions have a `studio.description`, which Studio shows in its section picker and AI agents read through Laioutr's MCP server. |
61
60
  | `larrylint/single-root` | Sections and blocks render one root element, also through a component they wrap. Otherwise Vue drops the `data-lfc-*` attributes Studio needs to select them in the preview. |
@@ -78,35 +77,31 @@ Type imports are fine across most layers, since they don't end up in the bundle.
78
77
 
79
78
  ## Layout
80
79
 
81
- larrylint expects the layout of Laioutr's [app starter](https://github.com/laioutr/app-starter):
80
+ larrylint follows the runtime layout from Laioutr's [coding standards](https://docs.laioutr.com/apps/app-development/coding-standards):
82
81
 
83
82
  ```
84
83
  src/
85
- ├── module.ts # build time
84
+ ├── module.ts # registers sections, blocks, orchestr handlers and plugins
86
85
  └── runtime/
87
- ├── app/
88
- │ ├── sections/ # defineSection() components, or section/
89
- │ ├── blocks/ # defineBlock() components, or block/
90
- │ ├── components/
91
- │ ├── composables/
92
- │ ├── overrides/ # replacements for upstream components
93
- │ ├── plugins/
94
- │ ├── shared-fields/ # schema fields sections and blocks share
95
- │ ├── theme/
86
+ ├── app/ # client code
87
+ │ ├── components/ # UI components
88
+ │ ├── sections/ # defineSection() components
89
+ │ └── blocks/ # defineBlock() components
90
+ ├── server/ # server code
91
+ │ ├── client/ # API client factories
92
+ │ ├── const/
93
+ │ ├── mappers/
94
+ │ ├── middleware/ # defineOrchestr() with extendRequest()
95
+ │ ├── orchestr/ # *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts, *.page-index.ts
96
+ │ ├── orchestr-helper/ # helper logic for handlers
96
97
  │ └── utils/
97
- ├── server/
98
- │ ├── orchestr/
99
- │ │ ├── <entity>/ # *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts, *.page-index.ts
100
- │ │ └── plugins/
101
- │ ├── middleware/ # orchestr middleware and the builders handlers use
102
- │ ├── client/ # API clients, nothing else
103
- │ ├── api/ # server routes, or routes/
104
- │ ├── plugins/ # Nitro plugins
105
- │ ├── media-library/ # or media-libraries/
106
- │ └── utils/
107
- └── shared/ # code for both the app and the server
98
+ └── shared/ # code for both sides, like orchestr tokens
108
99
  ```
109
100
 
101
+ Where your sections, blocks and handlers live, larrylint reads from your `module.ts`: the folders and globs you pass to `registerLaioutrApp()`, and the plugins you pass to `addPlugin()`. So other folders work too, like `section/` and `block/` in Laioutr's own ui-app. Without a `module.ts` it can read, larrylint assumes the folders of Laioutr's [app starter](https://github.com/laioutr/app-starter): `app/sections/`, `app/blocks/` and `server/orchestr/`.
102
+
103
+ Besides that, larrylint only relies on the names `app/components/` and `server/client/`, and on the split into `app/`, `server/` and `shared/`.
104
+
110
105
  ## Baseline
111
106
 
112
107
  On an existing codebase, `larrylint init` records all current violations in `larrylint-baseline.json`, counted per file and rule. A file stays quiet as long as it has no more violations of a rule than recorded. Once it gets more, all of them show again, like ESLint's bulk suppressions. The baseline applies in your editor, in `eslint .` and in the CLI.
@@ -1,4 +1,4 @@
1
1
  var name = "larrylint";
2
- var version = "0.2.1";
2
+ var version = "0.2.2";
3
3
  var description = "Opinionated rules for Laioutr apps, as an ESLint preset and a CLI.";
4
4
  export { description, name, version };
@@ -2,7 +2,7 @@ import { name, version } from "./package.mjs";
2
2
  import process from "node:process";
3
3
  import { loadConfig } from "c12";
4
4
  import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
5
- import { dirname, join, normalize, parse, relative, resolve } from "pathe";
5
+ import { dirname, join, matchesGlob, 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) {
@@ -30,6 +30,7 @@ const EXTENSIONS = [
30
30
  "/index.ts",
31
31
  "/index.js"
32
32
  ];
33
+ const SOURCE_EXTENSION = /\.(?:vue|[cm]?[jt]sx?)$/;
33
34
  const directories = /* @__PURE__ */ new Map();
34
35
  function cachedReader(parse) {
35
36
  const cache = /* @__PURE__ */ new Map();
@@ -62,6 +63,9 @@ function isDirectory(path) {
62
63
  }
63
64
  return result;
64
65
  }
66
+ function withoutExtension(path) {
67
+ return path.replace(SOURCE_EXTENSION, "");
68
+ }
65
69
  function resolveImport(importer, source) {
66
70
  if (!source.startsWith("./") && !source.startsWith("../")) return;
67
71
  const target = resolve(dirname(importer), source.split("?")[0]);
@@ -129,31 +133,123 @@ function applyBaseline(cwd, results) {
129
133
  improved
130
134
  };
131
135
  }
136
+ function findProperty(object, key) {
137
+ 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;
138
+ }
139
+ function findStringProperty(object, key) {
140
+ const value = findProperty(object, key);
141
+ return value?.type === "Literal" && typeof value.value === "string" ? {
142
+ value: value.value,
143
+ node: value
144
+ } : void 0;
145
+ }
146
+ function objectElements(node) {
147
+ if (node?.type !== "ArrayExpression") return [];
148
+ return node.elements.filter((element) => element?.type === "ObjectExpression");
149
+ }
150
+ const parseFile = cachedReader((text) => {
151
+ try {
152
+ return parse$1(text, {
153
+ range: true,
154
+ loc: true
155
+ });
156
+ } catch {
157
+ return;
158
+ }
159
+ });
160
+ const SKIPPED = /* @__PURE__ */ new Set([
161
+ "parent",
162
+ "loc",
163
+ "range",
164
+ "tokens",
165
+ "comments"
166
+ ]);
167
+ function children(node) {
168
+ const result = [];
169
+ for (const [key, value] of Object.entries(node)) {
170
+ if (SKIPPED.has(key)) continue;
171
+ for (const child of Array.isArray(value) ? value : [value]) if (typeof child === "object" && child !== null && typeof child.type === "string") result.push(child);
172
+ }
173
+ return result;
174
+ }
175
+ function walk(node, enter) {
176
+ if (enter(node) === false) return;
177
+ for (const child of children(node)) walk(child, enter);
178
+ }
179
+ const STARTER = {
180
+ sections: ["app/sections"],
181
+ blocks: ["app/blocks"],
182
+ orchestr: ["server/orchestr"],
183
+ plugins: /* @__PURE__ */ new Set(),
184
+ references: /* @__PURE__ */ new Set()
185
+ };
186
+ const registrations = /* @__PURE__ */ new WeakMap();
187
+ function readRegistration(root) {
188
+ const program = parseFile(join(root, "src/module.ts"));
189
+ if (!program) return STARTER;
190
+ let registration = registrations.get(program);
191
+ if (!registration) {
192
+ registration = readModule(program);
193
+ registrations.set(program, registration);
194
+ }
195
+ return registration;
196
+ }
197
+ function readModule(program) {
198
+ const apps = [];
199
+ const plugins = /* @__PURE__ */ new Set();
200
+ const references = /* @__PURE__ */ new Set();
201
+ walk(program, (node) => {
202
+ const reference = node.type === "Literal" || node.type === "TemplateLiteral" ? runtimePath(node) : void 0;
203
+ if (reference) references.add(withoutExtension(reference));
204
+ if (node.type !== "CallExpression" || node.callee.type !== "Identifier") return;
205
+ const [argument] = node.arguments;
206
+ if (node.callee.name === "registerLaioutrApp" && argument?.type === "ObjectExpression") apps.push(argument);
207
+ if (node.callee.name === "addPlugin" && argument && argument.type !== "SpreadElement") {
208
+ const src = argument.type === "ObjectExpression" ? findProperty(argument, "src") : argument;
209
+ const plugin = src && runtimePath(src);
210
+ if (plugin) plugins.add(withoutExtension(plugin));
211
+ }
212
+ });
213
+ const [app] = apps;
214
+ return {
215
+ sections: app ? readEntries(app, "sections", STARTER.sections) : STARTER.sections,
216
+ blocks: app ? readEntries(app, "blocks", STARTER.blocks) : STARTER.blocks,
217
+ orchestr: app ? readEntries(app, "orchestrDirs", STARTER.orchestr) : STARTER.orchestr,
218
+ plugins,
219
+ references
220
+ };
221
+ }
222
+ function readEntries(app, option, fallback) {
223
+ const value = findProperty(app, option);
224
+ if (!value) return [];
225
+ const paths = value.type === "ArrayExpression" ? value.elements.map((element) => element && element.type !== "SpreadElement" ? runtimePath(element) : void 0) : [void 0];
226
+ const known = paths.filter((path) => path !== void 0);
227
+ return known.length > 0 || paths.length === 0 ? known : fallback;
228
+ }
229
+ function runtimePath(node) {
230
+ const parts = [];
231
+ walk(node, (child) => {
232
+ if (child.type === "Literal" && typeof child.value === "string") parts.push(child.value);
233
+ else if (child.type === "TemplateElement" && child.value.cooked) parts.push(child.value.cooked);
234
+ });
235
+ const path = normalize(parts.join("/")).replace(/^(?:.*\/)?runtime\//, "").replace(/^\/|\/$/g, "");
236
+ return /^(?:app|server|shared)\//.test(path) ? path : void 0;
237
+ }
238
+ function isRegistered(path, entries) {
239
+ return entries.some((entry) => matchesGlob(path, entry.includes(".vue") ? entry : `${entry}/**/*.vue`));
240
+ }
241
+ function entryFolder(entry) {
242
+ const parts = entry.split("/");
243
+ const end = parts.findIndex((part) => /[*?{[]/.test(part) || part.endsWith(".vue"));
244
+ return (end === -1 ? parts : parts.slice(0, end)).join("/");
245
+ }
246
+ function isReferenced(file) {
247
+ return readRegistration(file.root).references.has(withoutExtension(file.path.replace(/^src\/runtime\//, "")));
248
+ }
132
249
  const HANDLER_FILE = /\.(query|resolver|link|action|templates?|page-index)(?:\.[cm]?[jt]s)?$/;
133
250
  const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
134
- const APP_FOLDERS = {
135
- "sections": "section",
136
- "section": "section",
137
- "blocks": "block",
138
- "block": "block",
139
- "components": "component",
140
- "composables": "composable",
141
- "utils": "app-util",
142
- "plugins": "app-plugin",
143
- "overrides": "override",
144
- "theme": "theme",
145
- "shared-fields": "shared-field"
146
- };
147
- const SERVER_FOLDERS = {
148
- "middleware": "middleware",
149
- "client": "client",
150
- "utils": "server-util",
151
- "api": "route",
152
- "routes": "route",
153
- "plugins": "nitro-plugin",
154
- "media-library": "media-library",
155
- "media-libraries": "media-library"
156
- };
251
+ const APP_FOLDERS = { components: "component" };
252
+ const SERVER_FOLDERS = { client: "client" };
157
253
  function classify(file) {
158
254
  const absolute = normalize(file);
159
255
  const test = TEST_FILE.test(absolute);
@@ -164,7 +260,7 @@ function classify(file) {
164
260
  root,
165
261
  path: absolute.slice(root.length + 1),
166
262
  test,
167
- ...classifyRuntime(absolute.slice(runtimeIndex + 13).split("/"))
263
+ ...classifyRuntime(absolute.slice(runtimeIndex + 13), readRegistration(root))
168
264
  };
169
265
  }
170
266
  const srcIndex = absolute.lastIndexOf("/src/");
@@ -178,33 +274,40 @@ function classify(file) {
178
274
  test
179
275
  };
180
276
  }
181
- function classifyRuntime(parts) {
182
- const [side, folder = "", ...rest] = parts;
183
- if (parts.length < 2 || side !== "app" && side !== "server" && side !== "shared") return { side: "other" };
184
- if (side === "shared") return {
185
- side,
186
- kind: "shared"
187
- };
188
- if (side === "app") return {
189
- side,
190
- kind: APP_FOLDERS[folder] ?? "other"
191
- };
192
- if (folder === "orchestr") {
193
- if (rest.length > 1 && rest[0] === "plugins") return {
277
+ function classifyRuntime(path, registration) {
278
+ const [side, folder] = path.split("/");
279
+ if (!folder || side !== "app" && side !== "server" && side !== "shared") return { side: "other" };
280
+ if (side === "shared") return { side };
281
+ if (side === "app") {
282
+ if (isRegistered(path, registration.sections)) return {
283
+ side,
284
+ kind: "section"
285
+ };
286
+ if (isRegistered(path, registration.blocks)) return {
194
287
  side,
195
- kind: "orchestr-plugin"
288
+ kind: "block"
196
289
  };
197
- const handler = HANDLER_FILE.exec(parts.at(-1))?.[1]?.replace("templates", "template");
198
290
  return {
199
291
  side,
200
- kind: handler ? "handler" : "orchestr-file",
201
- handler
292
+ kind: registration.plugins.has(withoutExtension(path)) ? "app-plugin" : APP_FOLDERS[folder] ?? "other"
202
293
  };
203
294
  }
204
- return {
295
+ const orchestr = registration.orchestr.find((dir) => path.startsWith(`${dir}/`));
296
+ if (!orchestr) return {
205
297
  side,
206
298
  kind: SERVER_FOLDERS[folder] ?? "other"
207
299
  };
300
+ const [first, ...rest] = path.slice(orchestr.length + 1).split("/");
301
+ if (first === "plugins" && rest.length > 0) return {
302
+ side,
303
+ kind: "orchestr-plugin"
304
+ };
305
+ const handler = HANDLER_FILE.exec(path)?.[1]?.replace("templates", "template");
306
+ return {
307
+ side,
308
+ kind: handler ? "handler" : "orchestr-file",
309
+ handler
310
+ };
208
311
  }
209
312
  function defineRule(rule) {
210
313
  return {
@@ -331,25 +434,6 @@ function isTested(node) {
331
434
  default: return false;
332
435
  }
333
436
  }
334
- const SKIPPED = /* @__PURE__ */ new Set([
335
- "parent",
336
- "loc",
337
- "range",
338
- "tokens",
339
- "comments"
340
- ]);
341
- function children(node) {
342
- const result = [];
343
- for (const [key, value] of Object.entries(node)) {
344
- if (SKIPPED.has(key)) continue;
345
- for (const child of Array.isArray(value) ? value : [value]) if (typeof child === "object" && child !== null && typeof child.type === "string") result.push(child);
346
- }
347
- return result;
348
- }
349
- function walk(node, enter) {
350
- if (enter(node) === false) return;
351
- for (const child of children(node)) walk(child, enter);
352
- }
353
437
  function isFunction(node) {
354
438
  return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
355
439
  }
@@ -450,16 +534,6 @@ function isRouter(node) {
450
534
  const name = nameOf(node.type === "CallExpression" ? node.callee : node);
451
535
  return node.type === "CallExpression" ? name === "useRouter" : name === "router" || name === "$router";
452
536
  }
453
- const parseFile = cachedReader((text) => {
454
- try {
455
- return parse$1(text, {
456
- range: true,
457
- loc: true
458
- });
459
- } catch {
460
- return;
461
- }
462
- });
463
537
  function isTypeOnly(node) {
464
538
  const { importKind, exportKind } = node;
465
539
  if (importKind === "type" || exportKind === "type") return true;
@@ -899,12 +973,12 @@ const FILE_START = {
899
973
  column: 0
900
974
  }
901
975
  };
902
- var files_default$1 = defineRule({
976
+ var files_default = defineRule({
903
977
  meta: {
904
978
  type: "problem",
905
979
  docs: { description: "Disallow files other than handlers in `orchestr/`" },
906
980
  schema: [],
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." }
981
+ messages: { notAHandler: "Only handlers belong in orchestr/. Move this file to server/orchestr-helper/, or give it a handler suffix like .query.ts or .action.ts." }
908
982
  },
909
983
  applies: (file) => file.kind === "orchestr-file",
910
984
  create: ({ report }) => ({ Program: () => {
@@ -940,39 +1014,6 @@ var exports_default = defineRule({
940
1014
  };
941
1015
  }
942
1016
  });
943
- const MIDDLEWARE = /* @__PURE__ */ new Set([
944
- "defineOrchestr",
945
- "defineEventHandler",
946
- "eventHandler",
947
- "defineRequestMiddleware",
948
- "fromNodeMiddleware"
949
- ]);
950
- var files_default = defineRule({
951
- meta: {
952
- type: "suggestion",
953
- docs: { description: "Disallow files other than middleware in `server/middleware/`" },
954
- schema: [],
955
- messages: { helper: "Only middleware belongs in server/middleware/. Move this file to server/utils/." }
956
- },
957
- applies: (file) => file.kind === "middleware",
958
- create: ({ report }) => {
959
- let middleware = false;
960
- return {
961
- "Identifier": (node) => {
962
- middleware ||= MIDDLEWARE.has(node.name);
963
- },
964
- "CallExpression": (node) => {
965
- middleware ||= readMiddleware(node) !== void 0;
966
- },
967
- "Program:exit": () => {
968
- if (!middleware) report({
969
- loc: FILE_START,
970
- messageId: "helper"
971
- });
972
- }
973
- };
974
- }
975
- });
976
1017
  var throws_default = defineRule({
977
1018
  meta: {
978
1019
  type: "problem",
@@ -1050,28 +1091,14 @@ var namespaces_default = defineRule({
1050
1091
  });
1051
1092
  } })
1052
1093
  });
1053
- function findProperty(object, key) {
1054
- for (const entry of object.properties) if (entry.type === "Property" && !entry.computed && (entry.key.type === "Identifier" && entry.key.name === key || entry.key.type === "Literal" && entry.key.value === key)) return entry.value;
1055
- }
1056
- function findStringProperty(object, key) {
1057
- const value = findProperty(object, key);
1058
- return value?.type === "Literal" && typeof value.value === "string" ? {
1059
- value: value.value,
1060
- node: value
1061
- } : void 0;
1062
- }
1063
- function objectElements(node) {
1064
- if (node?.type !== "ArrayExpression") return [];
1065
- return node.elements.filter((element) => element?.type === "ObjectExpression");
1066
- }
1067
1094
  const DEFINERS = {
1068
1095
  defineSection: {
1069
1096
  kind: "section",
1070
- folder: "sections"
1097
+ option: "sections"
1071
1098
  },
1072
1099
  defineBlock: {
1073
1100
  kind: "block",
1074
- folder: "blocks"
1101
+ option: "blocks"
1075
1102
  }
1076
1103
  };
1077
1104
  const PICKERS = /* @__PURE__ */ new Set([
@@ -1082,7 +1109,7 @@ const PICKERS = /* @__PURE__ */ new Set([
1082
1109
  const TEXTS = /* @__PURE__ */ new Set([
1083
1110
  "text",
1084
1111
  "textarea",
1085
- "secret"
1112
+ "richtext"
1086
1113
  ]);
1087
1114
  function readDefinition(call) {
1088
1115
  if (call.callee.type !== "Identifier" || !Object.hasOwn(DEFINERS, call.callee.name)) return;
@@ -1176,11 +1203,12 @@ var description_default = defineRule({
1176
1203
  var folder_default = defineRule({
1177
1204
  meta: {
1178
1205
  type: "problem",
1179
- docs: { description: "Require sections in `app/sections/` and blocks in `app/blocks/`, and a definition in every `.vue` there" },
1206
+ docs: { description: "Require sections and blocks in the folders `module.ts` registers for them, and a definition in every `.vue` registered there" },
1180
1207
  schema: [],
1181
1208
  messages: {
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."
1209
+ notDefined: "This file is registered as a {{kind}}, so it needs {{definer}}(). Add it, or move the file to components/.",
1210
+ wrongFolder: "{{definer}}() belongs in {{folder}}/. Move the file there.",
1211
+ notRegistered: "module.ts registers no {{kind}}s, so this {{kind}} never shows up in Studio. Add its folder to registerLaioutrApp()."
1184
1212
  }
1185
1213
  },
1186
1214
  applies: (file) => file.side === "app",
@@ -1190,15 +1218,21 @@ var folder_default = defineRule({
1190
1218
  "CallExpression": (node) => {
1191
1219
  const definition = readDefinition(node);
1192
1220
  if (!definition) return;
1193
- const { kind, folder } = DEFINERS[definition.definer];
1221
+ const { kind, option } = DEFINERS[definition.definer];
1194
1222
  definers.add(definition.definer);
1195
- if (file.kind !== kind && file.kind !== "override") report({
1223
+ if (file.kind === kind || isReferenced(file)) return;
1224
+ const [entry] = readRegistration(file.root)[option];
1225
+ report(entry ? {
1196
1226
  node: node.callee,
1197
1227
  messageId: "wrongFolder",
1198
1228
  data: {
1199
1229
  definer: definition.definer,
1200
- folder
1230
+ folder: entryFolder(entry)
1201
1231
  }
1232
+ } : {
1233
+ node: node.callee,
1234
+ messageId: "notRegistered",
1235
+ data: { kind }
1202
1236
  });
1203
1237
  },
1204
1238
  "Program:exit": () => {
@@ -1206,7 +1240,10 @@ var folder_default = defineRule({
1206
1240
  if (expected && file.path.endsWith(".vue") && !definers.has(expected)) report({
1207
1241
  loc: FILE_START,
1208
1242
  messageId: "notDefined",
1209
- data: { definer: expected }
1243
+ data: {
1244
+ kind: DEFINERS[expected].kind,
1245
+ definer: expected
1246
+ }
1210
1247
  });
1211
1248
  }
1212
1249
  };
@@ -1238,7 +1275,7 @@ const MESSAGES = {
1238
1275
  content_alignment: "alignment",
1239
1276
  text: "text",
1240
1277
  textarea: "text",
1241
- secret: "text"
1278
+ richtext: "text"
1242
1279
  };
1243
1280
  var fallbacks_default = defineRule({
1244
1281
  meta: {
@@ -1681,65 +1718,23 @@ var public_default = defineRule({
1681
1718
  });
1682
1719
  } })
1683
1720
  });
1684
- const LABELS = {
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"
1696
- };
1697
- const ABOVE_APP_UTILS = /* @__PURE__ */ new Set([
1698
- "section",
1699
- "block",
1700
- "component",
1701
- "composable",
1702
- "app-plugin",
1703
- "override"
1704
- ]);
1705
- const ABOVE_COMPOSABLES = /* @__PURE__ */ new Set([
1706
- "section",
1707
- "block",
1708
- "component",
1709
- "override"
1710
- ]);
1711
- const ABOVE_SERVER_UTILS = /* @__PURE__ */ new Set([
1712
- "route",
1713
- "nitro-plugin",
1714
- "orchestr-plugin",
1715
- "orchestr-file",
1716
- "media-library"
1717
- ]);
1718
1721
  function findViolation(importer, target, typeOnly) {
1722
+ if (typeOnly) return;
1719
1723
  if (importer.side === "app" && target.side === "server") return { messageId: "appImportsServer" };
1720
1724
  if (importer.side === "server" && target.side === "app") return { messageId: "serverImportsApp" };
1721
1725
  if (importer.side === "shared" && (target.side === "app" || target.side === "server")) return {
1722
1726
  messageId: "sharedImportsSide",
1723
1727
  data: { side: target.side }
1724
1728
  };
1725
- if (target.side === "build" && !typeOnly) return { messageId: "runtimeImportsBuild" };
1729
+ if (target.side === "build") return { messageId: "runtimeImportsBuild" };
1726
1730
  if (target.kind === "handler") return { messageId: "handlerImported" };
1727
- if (typeOnly) return;
1728
1731
  if (target.kind === "client" && importer.kind === "handler") return { messageId: "clientInHandler" };
1729
- if (importer.kind === "server-util" && ABOVE_SERVER_UTILS.has(target.kind)) return {
1730
- messageId: "serverUtilImportsUp",
1731
- data: { kind: LABELS[target.kind] }
1732
- };
1733
- if (importer.kind === "app-util" && ABOVE_APP_UTILS.has(target.kind)) return {
1734
- messageId: "appUtilImportsUp",
1735
- data: { kind: LABELS[target.kind] }
1736
- };
1737
- if (importer.kind === "composable" && ABOVE_COMPOSABLES.has(target.kind)) return {
1738
- messageId: "composableImportsUp",
1739
- data: { kind: LABELS[target.kind] }
1732
+ if (importer.kind === "component" && (target.kind === "section" || target.kind === "block")) return {
1733
+ messageId: "schemaInComponent",
1734
+ data: { kind: target.kind }
1740
1735
  };
1741
- if (importer.kind === "component" && target.kind === "section") return { messageId: "componentImportsSection" };
1742
1736
  }
1737
+ const SERVER_ONLY = /\.server\.[^/]+$/;
1743
1738
  var layers_default = defineRule({
1744
1739
  meta: {
1745
1740
  type: "problem",
@@ -1750,12 +1745,9 @@ var layers_default = defineRule({
1750
1745
  serverImportsApp: "Server code can't import app code. Move what both need to src/runtime/shared/.",
1751
1746
  sharedImportsSide: "Shared code can't import from {{side}}/.",
1752
1747
  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.",
1748
+ handlerImported: "Don't import orchestr handlers. Move the code you need to server/orchestr-helper/.",
1749
+ clientInHandler: "Handlers get API clients from the orchestr context, not by import. Move constants to server/const/ and helpers to server/orchestr-helper/.",
1750
+ schemaInComponent: "Components can't import {{kind}}s.",
1759
1751
  nodeBuiltin: "'{{name}}' only works on the server. Move this code to server/."
1760
1752
  }
1761
1753
  },
@@ -1764,7 +1756,7 @@ var layers_default = defineRule({
1764
1756
  const checkImport = (node, source, typeOnly) => {
1765
1757
  if (typeof source !== "string") return;
1766
1758
  if (source.startsWith("node:")) {
1767
- if (importer.side !== "server" && !typeOnly) report({
1759
+ if (importer.side !== "server" && !typeOnly && !SERVER_ONLY.test(importer.path)) report({
1768
1760
  node,
1769
1761
  messageId: "nodeBuiltin",
1770
1762
  data: { name: source }
@@ -1805,10 +1797,9 @@ const plugin = {
1805
1797
  "heavy-imports": bundle_default,
1806
1798
  "config-keys": keys_default,
1807
1799
  "public-config": public_default,
1808
- "orchestr-files": files_default$1,
1800
+ "orchestr-files": files_default,
1809
1801
  "orchestr-cookies": cookies_default,
1810
1802
  "handler-exports": exports_default,
1811
- "middleware-files": files_default,
1812
1803
  "initware-throws": throws_default,
1813
1804
  "token-namespaces": namespaces_default,
1814
1805
  "definition-folder": folder_default,
package/dist/index.d.mts CHANGED
@@ -34,7 +34,6 @@ export declare const plugin: {
34
34
  'orchestr-files': import("eslint").Rule.RuleModule;
35
35
  'orchestr-cookies': import("eslint").Rule.RuleModule;
36
36
  'handler-exports': import("eslint").Rule.RuleModule;
37
- 'middleware-files': import("eslint").Rule.RuleModule;
38
37
  'initware-throws': import("eslint").Rule.RuleModule;
39
38
  'token-namespaces': import("eslint").Rule.RuleModule;
40
39
  'definition-folder': import("eslint").Rule.RuleModule;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "larrylint",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "description": "Opinionated rules for Laioutr apps, as an ESLint preset and a CLI.",
6
6
  "author": "Frederik Bußmann <frederik@bussmann.io>",
7
7
  "license": "MIT",