larrylint 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/_chunks/check2.mjs +1 -1
- package/dist/_chunks/init.mjs +1 -1
- package/dist/_chunks/package.mjs +1 -1
- package/dist/_chunks/preset.mjs +189 -187
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,9 +34,9 @@ After that, `eslint .` and your editor report larrylint's rules next to your own
|
|
|
34
34
|
| Rule | Description |
|
|
35
35
|
| --- | --- |
|
|
36
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. |
|
|
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
|
|
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
|
-
| `larrylint/public-config` | The module doesn't copy its whole options object into `runtimeConfig.public`, which
|
|
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
41
|
Type imports are fine across most layers, since they don't end up in the bundle.
|
|
42
42
|
|
|
@@ -48,7 +48,7 @@ Type imports are fine across most layers, since they don't end up in the bundle.
|
|
|
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
50
|
| `larrylint/middleware-files` | `server/middleware/` only holds orchestr and Nitro middleware; helpers go to `server/utils/`. |
|
|
51
|
-
| `larrylint/initware-throws` | `extendRequest()` doesn't throw, directly or through a function it calls: it runs before every query, so a throw
|
|
51
|
+
| `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
52
|
| `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
53
|
|
|
54
54
|
### Sections and blocks
|
|
@@ -58,21 +58,21 @@ Type imports are fine across most layers, since they don't end up in the bundle.
|
|
|
58
58
|
| `larrylint/definition-folder` | `defineSection()` lives in `app/sections/` and `defineBlock()` in `app/blocks/` (or `section/` and `block/`), and every `.vue` there has one. |
|
|
59
59
|
| `larrylint/component-name` | The `component` of a definition matches its file name. |
|
|
60
60
|
| `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
|
-
| `larrylint/single-root` | Sections and blocks render one root element, also through a component they wrap. Otherwise Vue drops the `data-lfc-*`
|
|
61
|
+
| `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. |
|
|
62
62
|
| `larrylint/slot-children` | Nothing counts slot children with `.length`: frontend-core passes all blocks of a slot as one Fragment. |
|
|
63
63
|
| `larrylint/reserved-field-names` | No top-level schema fields named `key`, `ref`, `ref_for`, `ref_key`, `class` or `style`, which Vue handles itself, and no `slots` on sections, which frontend-core overwrites. |
|
|
64
|
-
| `larrylint/field-name-case` | Top-level schema field names have no `-` and don't start with
|
|
64
|
+
| `larrylint/field-name-case` | Top-level schema field names have no `-` and don't start with `$`, which Vue renames or rejects as props. |
|
|
65
65
|
| `larrylint/required-fields` | Schema fields have no `required`, which Studio ignores. Give them a `default` instead. |
|
|
66
66
|
| `larrylint/unused-fields` | The component reads every schema field it defines, directly, through the section it's a block of, or in another field's `if`, so editors don't get fields that do nothing. |
|
|
67
|
-
| `larrylint/dead-fallbacks` | No `??` fallbacks that never
|
|
67
|
+
| `larrylint/dead-fallbacks` | No `??` fallbacks that are never used: frontend-core fills unset pickers with their first option, content alignments with the center, checkboxes with `false` and text fields with `''`. |
|
|
68
68
|
|
|
69
69
|
### Frontend
|
|
70
70
|
|
|
71
71
|
| Rule | Description |
|
|
72
72
|
| --- | --- |
|
|
73
73
|
| `larrylint/button-type` | No `type` on the ui-kit buttons: they always render their `button-type` prop, so `type="submit"` silently renders a dead button. Autofixable. |
|
|
74
|
-
| `larrylint/mutation-errors` | An awaited or dropped `mutateAsync()` handles its error. Otherwise a failed mutation replaces the whole section or block with
|
|
75
|
-
| `larrylint/resolve-result` | Nothing
|
|
74
|
+
| `larrylint/mutation-errors` | An awaited or dropped `mutateAsync()` handles its error. Otherwise a failed mutation replaces the whole section or block with an error, or nothing handles it. |
|
|
75
|
+
| `larrylint/resolve-result` | Nothing checks the result of `linkResolver.resolve()`: a link it can't resolve comes back as a `#…` fallback, not as an empty value. |
|
|
76
76
|
| `larrylint/hand-built-links` | Links to pages come from `linkResolver`, not from paths like `` `/hotels/${slug}` ``: page paths are set per page and language in Studio, and each market adds its own prefix, like `/en`. |
|
|
77
77
|
| `larrylint/internal-anchors` | Internal and resolved links use `<NuxtLink>`: a plain `<a>` reloads the page and breaks Studio's navigation sync. |
|
|
78
78
|
|
package/dist/_chunks/check2.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { BASELINE_FILE, countViolations, writeBaseline } from "./preset.mjs";
|
|
|
2
2
|
import { cwdArgs } from "../cli/index.mjs";
|
|
3
3
|
import { check } from "./check.mjs";
|
|
4
4
|
import process from "node:process";
|
|
5
|
-
import { join, resolve } from "pathe";
|
|
6
5
|
import { existsSync } from "node:fs";
|
|
6
|
+
import { join, resolve } from "pathe";
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
8
|
import { consola } from "consola";
|
|
9
9
|
var check_default = defineCommand({
|
package/dist/_chunks/init.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { BASELINE_FILE, countViolations, writeBaseline } from "./preset.mjs";
|
|
|
2
2
|
import { cwdArgs } from "../cli/index.mjs";
|
|
3
3
|
import { check } from "./check.mjs";
|
|
4
4
|
import process from "node:process";
|
|
5
|
-
import { join, relative, resolve } from "pathe";
|
|
6
5
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join, relative, resolve } from "pathe";
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
8
|
import { consola } from "consola";
|
|
9
9
|
import { addDevDependency, detectPackageManager } from "nypm";
|
package/dist/_chunks/package.mjs
CHANGED
package/dist/_chunks/preset.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { name, version } from "./package.mjs";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { loadConfig } from "c12";
|
|
4
|
-
import { 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) {
|
|
@@ -19,83 +19,6 @@ async function loadLarrylintConfig(cwd = process.cwd()) {
|
|
|
19
19
|
});
|
|
20
20
|
return { heavyPackages: config.heavyPackages ?? [] };
|
|
21
21
|
}
|
|
22
|
-
const HANDLER_FILE = /\.(query|resolver|link|action|templates?|page-index)(?:\.[cm]?[jt]s)?$/;
|
|
23
|
-
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
24
|
-
const APP_FOLDERS = {
|
|
25
|
-
"sections": "section",
|
|
26
|
-
"section": "section",
|
|
27
|
-
"blocks": "block",
|
|
28
|
-
"block": "block",
|
|
29
|
-
"components": "component",
|
|
30
|
-
"composables": "composable",
|
|
31
|
-
"utils": "app-util",
|
|
32
|
-
"plugins": "app-plugin",
|
|
33
|
-
"overrides": "override",
|
|
34
|
-
"theme": "theme",
|
|
35
|
-
"shared-fields": "shared-field"
|
|
36
|
-
};
|
|
37
|
-
const SERVER_FOLDERS = {
|
|
38
|
-
"middleware": "middleware",
|
|
39
|
-
"client": "client",
|
|
40
|
-
"utils": "server-util",
|
|
41
|
-
"api": "route",
|
|
42
|
-
"routes": "route",
|
|
43
|
-
"plugins": "nitro-plugin",
|
|
44
|
-
"media-library": "media-library",
|
|
45
|
-
"media-libraries": "media-library"
|
|
46
|
-
};
|
|
47
|
-
function classify(file) {
|
|
48
|
-
const absolute = normalize(file);
|
|
49
|
-
const test = TEST_FILE.test(absolute);
|
|
50
|
-
const runtimeIndex = absolute.lastIndexOf("/src/runtime/");
|
|
51
|
-
if (runtimeIndex !== -1) {
|
|
52
|
-
const root = absolute.slice(0, runtimeIndex);
|
|
53
|
-
return {
|
|
54
|
-
root,
|
|
55
|
-
path: absolute.slice(root.length + 1),
|
|
56
|
-
test,
|
|
57
|
-
...classifyRuntime(absolute.slice(runtimeIndex + 13).split("/"))
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
const srcIndex = absolute.lastIndexOf("/src/");
|
|
61
|
-
if (srcIndex === -1) return;
|
|
62
|
-
const root = absolute.slice(0, srcIndex);
|
|
63
|
-
const path = absolute.slice(root.length + 1);
|
|
64
|
-
return {
|
|
65
|
-
root,
|
|
66
|
-
path,
|
|
67
|
-
side: path.startsWith("src/types/") ? "other" : "build",
|
|
68
|
-
test
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function classifyRuntime(parts) {
|
|
72
|
-
const [side, folder = "", ...rest] = parts;
|
|
73
|
-
if (parts.length < 2 || side !== "app" && side !== "server" && side !== "shared") return { side: "other" };
|
|
74
|
-
if (side === "shared") return {
|
|
75
|
-
side,
|
|
76
|
-
kind: "shared"
|
|
77
|
-
};
|
|
78
|
-
if (side === "app") return {
|
|
79
|
-
side,
|
|
80
|
-
kind: APP_FOLDERS[folder] ?? "other"
|
|
81
|
-
};
|
|
82
|
-
if (folder === "orchestr") {
|
|
83
|
-
if (rest.length > 1 && rest[0] === "plugins") return {
|
|
84
|
-
side,
|
|
85
|
-
kind: "orchestr-plugin"
|
|
86
|
-
};
|
|
87
|
-
const handler = HANDLER_FILE.exec(parts.at(-1))?.[1]?.replace("templates", "template");
|
|
88
|
-
return {
|
|
89
|
-
side,
|
|
90
|
-
kind: handler ? "handler" : "orchestr-file",
|
|
91
|
-
handler
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
return {
|
|
95
|
-
side,
|
|
96
|
-
kind: SERVER_FOLDERS[folder] ?? "other"
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
22
|
const EXTENSIONS = [
|
|
100
23
|
"",
|
|
101
24
|
".ts",
|
|
@@ -206,6 +129,83 @@ function applyBaseline(cwd, results) {
|
|
|
206
129
|
improved
|
|
207
130
|
};
|
|
208
131
|
}
|
|
132
|
+
const HANDLER_FILE = /\.(query|resolver|link|action|templates?|page-index)(?:\.[cm]?[jt]s)?$/;
|
|
133
|
+
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
|
+
};
|
|
157
|
+
function classify(file) {
|
|
158
|
+
const absolute = normalize(file);
|
|
159
|
+
const test = TEST_FILE.test(absolute);
|
|
160
|
+
const runtimeIndex = absolute.lastIndexOf("/src/runtime/");
|
|
161
|
+
if (runtimeIndex !== -1) {
|
|
162
|
+
const root = absolute.slice(0, runtimeIndex);
|
|
163
|
+
return {
|
|
164
|
+
root,
|
|
165
|
+
path: absolute.slice(root.length + 1),
|
|
166
|
+
test,
|
|
167
|
+
...classifyRuntime(absolute.slice(runtimeIndex + 13).split("/"))
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const srcIndex = absolute.lastIndexOf("/src/");
|
|
171
|
+
if (srcIndex === -1) return;
|
|
172
|
+
const root = absolute.slice(0, srcIndex);
|
|
173
|
+
const path = absolute.slice(root.length + 1);
|
|
174
|
+
return {
|
|
175
|
+
root,
|
|
176
|
+
path,
|
|
177
|
+
side: path.startsWith("src/types/") ? "other" : "build",
|
|
178
|
+
test
|
|
179
|
+
};
|
|
180
|
+
}
|
|
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 {
|
|
194
|
+
side,
|
|
195
|
+
kind: "orchestr-plugin"
|
|
196
|
+
};
|
|
197
|
+
const handler = HANDLER_FILE.exec(parts.at(-1))?.[1]?.replace("templates", "template");
|
|
198
|
+
return {
|
|
199
|
+
side,
|
|
200
|
+
kind: handler ? "handler" : "orchestr-file",
|
|
201
|
+
handler
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
side,
|
|
206
|
+
kind: SERVER_FOLDERS[folder] ?? "other"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
209
|
function defineRule(rule) {
|
|
210
210
|
return {
|
|
211
211
|
meta: rule.meta,
|
|
@@ -396,11 +396,11 @@ function canReturnNullish(fn, value) {
|
|
|
396
396
|
var mutations_default = defineRule({
|
|
397
397
|
meta: {
|
|
398
398
|
type: "problem",
|
|
399
|
-
docs: { description: "Require error handling
|
|
399
|
+
docs: { description: "Require error handling for awaited or dropped `mutateAsync()` calls" },
|
|
400
400
|
schema: [],
|
|
401
401
|
messages: {
|
|
402
|
-
unhandled: "If this mutation fails,
|
|
403
|
-
dropped: "Nothing
|
|
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
404
|
}
|
|
405
405
|
},
|
|
406
406
|
applies: (file) => file.side === "app",
|
|
@@ -607,9 +607,9 @@ function rendersTag(element, tag) {
|
|
|
607
607
|
var anchors_default = defineRule({
|
|
608
608
|
meta: {
|
|
609
609
|
type: "problem",
|
|
610
|
-
docs: { description: "Disallow plain `<a>` tags for internal
|
|
610
|
+
docs: { description: "Disallow plain `<a>` tags for internal links" },
|
|
611
611
|
schema: [],
|
|
612
|
-
messages: { anchor: "A plain <a> reloads the whole page
|
|
612
|
+
messages: { anchor: "A plain <a> reloads the whole page. Use <NuxtLink> for internal links." }
|
|
613
613
|
},
|
|
614
614
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
615
615
|
create: ({ report, visitTemplate }) => {
|
|
@@ -646,9 +646,9 @@ const ROUTER_METHODS = /* @__PURE__ */ new Set(["push", "replace"]);
|
|
|
646
646
|
var paths_default = defineRule({
|
|
647
647
|
meta: {
|
|
648
648
|
type: "problem",
|
|
649
|
-
docs: { description: "Disallow
|
|
649
|
+
docs: { description: "Disallow page paths built by hand" },
|
|
650
650
|
schema: [],
|
|
651
|
-
messages: { path: "
|
|
651
|
+
messages: { path: "Hand-built page paths differ per language and market. Use linkResolver instead." }
|
|
652
652
|
},
|
|
653
653
|
applies: (file) => file.side === "app",
|
|
654
654
|
create: ({ report, visitTemplate }) => {
|
|
@@ -681,11 +681,11 @@ var paths_default = defineRule({
|
|
|
681
681
|
var resolve_default = defineRule({
|
|
682
682
|
meta: {
|
|
683
683
|
type: "problem",
|
|
684
|
-
docs: { description: "Disallow
|
|
684
|
+
docs: { description: "Disallow checking the result of `linkResolver.resolve()`" },
|
|
685
685
|
schema: [],
|
|
686
686
|
messages: {
|
|
687
|
-
tested: "linkResolver.resolve() returns
|
|
688
|
-
branch: "
|
|
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
689
|
}
|
|
690
690
|
},
|
|
691
691
|
applies: (file) => file.side === "app",
|
|
@@ -765,9 +765,9 @@ var button_default = defineRule({
|
|
|
765
765
|
meta: {
|
|
766
766
|
type: "problem",
|
|
767
767
|
fixable: "code",
|
|
768
|
-
docs: { description: "Disallow `type` on
|
|
768
|
+
docs: { description: "Disallow `type` on ui-kit buttons" },
|
|
769
769
|
schema: [],
|
|
770
|
-
messages: { buttonType: "<{{tag}}> ignores type
|
|
770
|
+
messages: { buttonType: "<{{tag}}> ignores type. Use button-type instead." }
|
|
771
771
|
},
|
|
772
772
|
applies: (file) => file.path.endsWith(".vue"),
|
|
773
773
|
create: ({ report, visitTemplate }) => {
|
|
@@ -861,11 +861,11 @@ const MANAGED = {
|
|
|
861
861
|
var cookies_default = defineRule({
|
|
862
862
|
meta: {
|
|
863
863
|
type: "problem",
|
|
864
|
-
docs: { description: "
|
|
864
|
+
docs: { description: "Require cookies and headers to be set in `extendRequest()` or action handlers, with frontend-core's managed cookie functions" },
|
|
865
865
|
schema: [],
|
|
866
866
|
messages: {
|
|
867
|
-
streamed: "
|
|
868
|
-
managed: "Use {{managed}}() 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."
|
|
869
869
|
}
|
|
870
870
|
},
|
|
871
871
|
applies: (file) => file.side === "server",
|
|
@@ -902,9 +902,9 @@ const FILE_START = {
|
|
|
902
902
|
var files_default$1 = defineRule({
|
|
903
903
|
meta: {
|
|
904
904
|
type: "problem",
|
|
905
|
-
docs: { description: "
|
|
905
|
+
docs: { description: "Disallow files other than handlers in `orchestr/`" },
|
|
906
906
|
schema: [],
|
|
907
|
-
messages: { notAHandler: "
|
|
907
|
+
messages: { notAHandler: "Only handlers belong in orchestr/. Move this file to server/utils/, or give it a handler suffix like .query.ts or .action.ts." }
|
|
908
908
|
},
|
|
909
909
|
applies: (file) => file.kind === "orchestr-file",
|
|
910
910
|
create: ({ report }) => ({ Program: () => {
|
|
@@ -917,9 +917,9 @@ var files_default$1 = defineRule({
|
|
|
917
917
|
var exports_default = defineRule({
|
|
918
918
|
meta: {
|
|
919
919
|
type: "problem",
|
|
920
|
-
docs: { description: "Require handler files to export their handler as default
|
|
920
|
+
docs: { description: "Require handler files to export their handler as default" },
|
|
921
921
|
schema: [],
|
|
922
|
-
messages: { missingDefault: "
|
|
922
|
+
messages: { missingDefault: "Export the handler as default, or it will fail the build." }
|
|
923
923
|
},
|
|
924
924
|
applies: (file) => file.kind === "handler",
|
|
925
925
|
create: ({ report }) => {
|
|
@@ -950,9 +950,9 @@ const MIDDLEWARE = /* @__PURE__ */ new Set([
|
|
|
950
950
|
var files_default = defineRule({
|
|
951
951
|
meta: {
|
|
952
952
|
type: "suggestion",
|
|
953
|
-
docs: { description: "
|
|
953
|
+
docs: { description: "Disallow files other than middleware in `server/middleware/`" },
|
|
954
954
|
schema: [],
|
|
955
|
-
messages: { helper: "
|
|
955
|
+
messages: { helper: "Only middleware belongs in server/middleware/. Move this file to server/utils/." }
|
|
956
956
|
},
|
|
957
957
|
applies: (file) => file.kind === "middleware",
|
|
958
958
|
create: ({ report }) => {
|
|
@@ -976,11 +976,11 @@ var files_default = defineRule({
|
|
|
976
976
|
var throws_default = defineRule({
|
|
977
977
|
meta: {
|
|
978
978
|
type: "problem",
|
|
979
|
-
docs: { description: "Disallow throwing in extendRequest()
|
|
979
|
+
docs: { description: "Disallow throwing in `extendRequest()`" },
|
|
980
980
|
schema: [],
|
|
981
981
|
messages: {
|
|
982
|
-
throws: "extendRequest()
|
|
983
|
-
throwsInCall: "{{name}}() can throw ({{location}})
|
|
982
|
+
throws: "A throw in extendRequest() breaks every page. Throw later instead, e.g. when the client is used.",
|
|
983
|
+
throwsInCall: "{{name}}() can throw ({{location}}), and a throw in extendRequest() breaks every page. Catch it here."
|
|
984
984
|
}
|
|
985
985
|
},
|
|
986
986
|
applies: (file) => file.side === "server",
|
|
@@ -1034,9 +1034,9 @@ function canonicalNamespaces(root) {
|
|
|
1034
1034
|
var namespaces_default = defineRule({
|
|
1035
1035
|
meta: {
|
|
1036
1036
|
type: "problem",
|
|
1037
|
-
docs: { description: "
|
|
1037
|
+
docs: { description: "Disallow your own token ids in the namespaces of Laioutr's canonical types" },
|
|
1038
1038
|
schema: [],
|
|
1039
|
-
messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types
|
|
1039
|
+
messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types. Use your app's own namespace." }
|
|
1040
1040
|
},
|
|
1041
1041
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1042
1042
|
create: ({ file, report }) => ({ CallExpression: (node) => {
|
|
@@ -1079,7 +1079,7 @@ const PICKERS = /* @__PURE__ */ new Set([
|
|
|
1079
1079
|
"radio",
|
|
1080
1080
|
"toggle_button"
|
|
1081
1081
|
]);
|
|
1082
|
-
const TEXTS
|
|
1082
|
+
const TEXTS = /* @__PURE__ */ new Set([
|
|
1083
1083
|
"text",
|
|
1084
1084
|
"textarea",
|
|
1085
1085
|
"secret"
|
|
@@ -1138,7 +1138,7 @@ function expectedDefiner(kind) {
|
|
|
1138
1138
|
}
|
|
1139
1139
|
function fillValue(field, program, filename) {
|
|
1140
1140
|
if (field.type === "checkbox") return { value: field.as === "visibility" };
|
|
1141
|
-
if (TEXTS
|
|
1141
|
+
if (TEXTS.has(field.type ?? "")) return { value: "" };
|
|
1142
1142
|
if (field.type === "content_alignment") {
|
|
1143
1143
|
const axis = findStringProperty(field.node, "axis")?.value;
|
|
1144
1144
|
return { value: !axis || axis === "both" ? "center-center" : "center" };
|
|
@@ -1155,31 +1155,32 @@ function fillValue(field, program, filename) {
|
|
|
1155
1155
|
var description_default = defineRule({
|
|
1156
1156
|
meta: {
|
|
1157
1157
|
type: "suggestion",
|
|
1158
|
-
docs: { description: "Require a studio.description on section and block definitions
|
|
1158
|
+
docs: { description: "Require a `studio.description` on section and block definitions" },
|
|
1159
1159
|
schema: [],
|
|
1160
|
-
messages: { description: "
|
|
1160
|
+
messages: { description: "Add a studio.description, so editors and AI agents know what this {{kind}} is for." }
|
|
1161
1161
|
},
|
|
1162
1162
|
applies: (file) => file.side === "app",
|
|
1163
1163
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1164
|
-
const
|
|
1165
|
-
if (!options) return;
|
|
1166
|
-
const studio = findProperty(options, "studio");
|
|
1164
|
+
const definition = readDefinition(node);
|
|
1165
|
+
if (!definition?.options) return;
|
|
1166
|
+
const studio = findProperty(definition.options, "studio");
|
|
1167
1167
|
if (studio && studio.type !== "ObjectExpression") return;
|
|
1168
1168
|
const description = studio && findProperty(studio, "description");
|
|
1169
1169
|
if (!description || description.type === "Literal" && String(description.value ?? "").trim() === "") report({
|
|
1170
1170
|
node: node.callee,
|
|
1171
|
-
messageId: "description"
|
|
1171
|
+
messageId: "description",
|
|
1172
|
+
data: { kind: DEFINERS[definition.definer].kind }
|
|
1172
1173
|
});
|
|
1173
1174
|
} })
|
|
1174
1175
|
});
|
|
1175
1176
|
var folder_default = defineRule({
|
|
1176
1177
|
meta: {
|
|
1177
1178
|
type: "problem",
|
|
1178
|
-
docs: { description: "
|
|
1179
|
+
docs: { description: "Require sections in `app/sections/` and blocks in `app/blocks/`, and a definition in every `.vue` there" },
|
|
1179
1180
|
schema: [],
|
|
1180
1181
|
messages: {
|
|
1181
|
-
notDefined: "
|
|
1182
|
-
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."
|
|
1183
1184
|
}
|
|
1184
1185
|
},
|
|
1185
1186
|
applies: (file) => file.side === "app",
|
|
@@ -1202,18 +1203,11 @@ var folder_default = defineRule({
|
|
|
1202
1203
|
},
|
|
1203
1204
|
"Program:exit": () => {
|
|
1204
1205
|
const expected = expectedDefiner(file.kind);
|
|
1205
|
-
if (expected && file.path.endsWith(".vue") && !definers.has(expected)) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
data: {
|
|
1211
|
-
folder,
|
|
1212
|
-
kind,
|
|
1213
|
-
definer: expected
|
|
1214
|
-
}
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1206
|
+
if (expected && file.path.endsWith(".vue") && !definers.has(expected)) report({
|
|
1207
|
+
loc: FILE_START,
|
|
1208
|
+
messageId: "notDefined",
|
|
1209
|
+
data: { definer: expected }
|
|
1210
|
+
});
|
|
1217
1211
|
}
|
|
1218
1212
|
};
|
|
1219
1213
|
}
|
|
@@ -1221,7 +1215,7 @@ var folder_default = defineRule({
|
|
|
1221
1215
|
var name_default = defineRule({
|
|
1222
1216
|
meta: {
|
|
1223
1217
|
type: "problem",
|
|
1224
|
-
docs: { description: "Require the `component` of a
|
|
1218
|
+
docs: { description: "Require the `component` of a definition to match its file name" },
|
|
1225
1219
|
schema: [],
|
|
1226
1220
|
messages: { componentName: "component: '{{actual}}' must match the file name '{{expected}}'." }
|
|
1227
1221
|
},
|
|
@@ -1239,20 +1233,23 @@ var name_default = defineRule({
|
|
|
1239
1233
|
});
|
|
1240
1234
|
} })
|
|
1241
1235
|
});
|
|
1242
|
-
const
|
|
1243
|
-
"
|
|
1244
|
-
"
|
|
1245
|
-
"
|
|
1246
|
-
|
|
1236
|
+
const MESSAGES = {
|
|
1237
|
+
checkbox: "checkbox",
|
|
1238
|
+
content_alignment: "alignment",
|
|
1239
|
+
text: "text",
|
|
1240
|
+
textarea: "text",
|
|
1241
|
+
secret: "text"
|
|
1242
|
+
};
|
|
1247
1243
|
var fallbacks_default = defineRule({
|
|
1248
1244
|
meta: {
|
|
1249
1245
|
type: "problem",
|
|
1250
|
-
docs: { description: "Disallow
|
|
1246
|
+
docs: { description: "Disallow `??` fallbacks on fields that frontend-core always fills" },
|
|
1251
1247
|
schema: [],
|
|
1252
1248
|
messages: {
|
|
1253
|
-
picker: "
|
|
1254
|
-
|
|
1255
|
-
|
|
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."
|
|
1256
1253
|
}
|
|
1257
1254
|
},
|
|
1258
1255
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
@@ -1290,7 +1287,7 @@ var fallbacks_default = defineRule({
|
|
|
1290
1287
|
for (const { node, prop } of fallbacks) {
|
|
1291
1288
|
const field = fills.get(prop);
|
|
1292
1289
|
if (!field || node.right.type === "Literal" && node.right.value === field.value) continue;
|
|
1293
|
-
const messageId = field.type
|
|
1290
|
+
const messageId = MESSAGES[field.type] ?? "picker";
|
|
1294
1291
|
const fill = typeof field.value === "string" ? `'${field.value}'` : String(field.value);
|
|
1295
1292
|
report({
|
|
1296
1293
|
node,
|
|
@@ -1357,9 +1354,9 @@ const NOT_READ = /* @__PURE__ */ new Set([
|
|
|
1357
1354
|
var unused_default = defineRule({
|
|
1358
1355
|
meta: {
|
|
1359
1356
|
type: "suggestion",
|
|
1360
|
-
docs: { description: "Disallow schema fields the
|
|
1357
|
+
docs: { description: "Disallow schema fields the component never uses" },
|
|
1361
1358
|
schema: [],
|
|
1362
|
-
messages: { unused: "
|
|
1359
|
+
messages: { unused: "The component never uses '{{name}}'. Use the field or remove it." }
|
|
1363
1360
|
},
|
|
1364
1361
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1365
1362
|
create: ({ context, file, report, visitTemplate }) => {
|
|
@@ -1412,11 +1409,11 @@ var unused_default = defineRule({
|
|
|
1412
1409
|
var casing_default = defineRule({
|
|
1413
1410
|
meta: {
|
|
1414
1411
|
type: "problem",
|
|
1415
|
-
docs: { description: "Disallow
|
|
1412
|
+
docs: { description: "Disallow schema field names that Vue renames or rejects" },
|
|
1416
1413
|
schema: [],
|
|
1417
1414
|
messages: {
|
|
1418
|
-
hyphen: "Vue
|
|
1419
|
-
dollar: "Vue
|
|
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."
|
|
1420
1417
|
}
|
|
1421
1418
|
},
|
|
1422
1419
|
applies: (file) => file.side === "app",
|
|
@@ -1440,9 +1437,9 @@ var casing_default = defineRule({
|
|
|
1440
1437
|
var required_default = defineRule({
|
|
1441
1438
|
meta: {
|
|
1442
1439
|
type: "problem",
|
|
1443
|
-
docs: { description: "Disallow `required` on schema fields
|
|
1440
|
+
docs: { description: "Disallow `required` on schema fields" },
|
|
1444
1441
|
schema: [],
|
|
1445
|
-
messages: { required: "
|
|
1442
|
+
messages: { required: "Studio ignores required. Give the field a default instead." }
|
|
1446
1443
|
},
|
|
1447
1444
|
applies: (file) => file.side === "app",
|
|
1448
1445
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
@@ -1467,11 +1464,11 @@ const RESERVED = /* @__PURE__ */ new Set([
|
|
|
1467
1464
|
var reserved_default = defineRule({
|
|
1468
1465
|
meta: {
|
|
1469
1466
|
type: "problem",
|
|
1470
|
-
docs: { description: "Disallow
|
|
1467
|
+
docs: { description: "Disallow schema field names that Vue or frontend-core already use" },
|
|
1471
1468
|
schema: [],
|
|
1472
1469
|
messages: {
|
|
1473
|
-
reserved: "'{{name}}'
|
|
1474
|
-
slots: "frontend-core passes
|
|
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."
|
|
1475
1472
|
}
|
|
1476
1473
|
},
|
|
1477
1474
|
applies: (file) => file.side === "app",
|
|
@@ -1506,29 +1503,31 @@ const OWN_ATTRS = /inheritAttrs\s*:\s*false/;
|
|
|
1506
1503
|
var root_default = defineRule({
|
|
1507
1504
|
meta: {
|
|
1508
1505
|
type: "problem",
|
|
1509
|
-
docs: { description: "Require sections and blocks to render one root element
|
|
1506
|
+
docs: { description: "Require sections and blocks to render one root element" },
|
|
1510
1507
|
schema: [],
|
|
1511
1508
|
messages: {
|
|
1512
|
-
roots: "
|
|
1513
|
-
componentRoots: "<{{name}}>
|
|
1509
|
+
roots: "Studio can't select a {{kind}} with more than one root element. Wrap the content in one element.",
|
|
1510
|
+
componentRoots: "<{{name}}> has more than one root element, so Studio can't select this {{kind}}. Give it one root, or wrap it here."
|
|
1514
1511
|
}
|
|
1515
1512
|
},
|
|
1516
1513
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1517
1514
|
create: ({ context, report }) => {
|
|
1518
|
-
let
|
|
1515
|
+
let kind;
|
|
1519
1516
|
return {
|
|
1520
1517
|
"CallExpression": (node) => {
|
|
1521
|
-
|
|
1518
|
+
const definition = readDefinition(node);
|
|
1519
|
+
kind ??= definition && DEFINERS[definition.definer].kind;
|
|
1522
1520
|
},
|
|
1523
1521
|
"Program:exit": () => {
|
|
1524
1522
|
const program = context.sourceCode.ast;
|
|
1525
1523
|
const template = program.templateBody;
|
|
1526
|
-
if (!
|
|
1524
|
+
if (!kind || !template || OWN_ATTRS.test(context.sourceCode.text)) return;
|
|
1527
1525
|
const [root, second] = renderedRoots(template);
|
|
1528
1526
|
if (second) {
|
|
1529
1527
|
report({
|
|
1530
1528
|
loc: second.startTag.loc,
|
|
1531
|
-
messageId: "roots"
|
|
1529
|
+
messageId: "roots",
|
|
1530
|
+
data: { kind }
|
|
1532
1531
|
});
|
|
1533
1532
|
return;
|
|
1534
1533
|
}
|
|
@@ -1538,7 +1537,10 @@ var root_default = defineRule({
|
|
|
1538
1537
|
if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 && !OWN_ATTRS.test(component.text)) report({
|
|
1539
1538
|
loc: root.startTag.loc,
|
|
1540
1539
|
messageId: "componentRoots",
|
|
1541
|
-
data: {
|
|
1540
|
+
data: {
|
|
1541
|
+
name: root.rawName,
|
|
1542
|
+
kind
|
|
1543
|
+
}
|
|
1542
1544
|
});
|
|
1543
1545
|
}
|
|
1544
1546
|
};
|
|
@@ -1547,9 +1549,9 @@ var root_default = defineRule({
|
|
|
1547
1549
|
var slots_default = defineRule({
|
|
1548
1550
|
meta: {
|
|
1549
1551
|
type: "problem",
|
|
1550
|
-
docs: { description: "Disallow counting
|
|
1552
|
+
docs: { description: "Disallow counting the blocks of a slot with `.length`" },
|
|
1551
1553
|
schema: [],
|
|
1552
|
-
messages: { length: "frontend-core passes
|
|
1554
|
+
messages: { length: "For blocks, this is always 1: frontend-core passes them as one Fragment. Count the Fragment's children instead." }
|
|
1553
1555
|
},
|
|
1554
1556
|
applies: (file) => file.side === "app",
|
|
1555
1557
|
create: ({ report, visitTemplate }) => {
|
|
@@ -1578,7 +1580,7 @@ var slots_default = defineRule({
|
|
|
1578
1580
|
var bundle_default = defineRule({
|
|
1579
1581
|
meta: {
|
|
1580
1582
|
type: "problem",
|
|
1581
|
-
docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins
|
|
1583
|
+
docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins" },
|
|
1582
1584
|
schema: [{
|
|
1583
1585
|
type: "object",
|
|
1584
1586
|
properties: { packages: {
|
|
@@ -1587,7 +1589,7 @@ var bundle_default = defineRule({
|
|
|
1587
1589
|
} },
|
|
1588
1590
|
additionalProperties: false
|
|
1589
1591
|
}],
|
|
1590
|
-
messages: { heavy: "
|
|
1592
|
+
messages: { heavy: "This loads '{{name}}' on every page. Load it with import() or defineAsyncComponent() instead." }
|
|
1591
1593
|
},
|
|
1592
1594
|
applies: (file) => file.kind === "section" || file.kind === "block" || file.kind === "app-plugin",
|
|
1593
1595
|
create: ({ context, report }) => {
|
|
@@ -1632,9 +1634,9 @@ function setupOptions(node) {
|
|
|
1632
1634
|
var keys_default = defineRule({
|
|
1633
1635
|
meta: {
|
|
1634
1636
|
type: "problem",
|
|
1635
|
-
docs: { description: "Require runtime config keys
|
|
1637
|
+
docs: { description: "Require runtime config keys to be this package or one of its dependencies" },
|
|
1636
1638
|
schema: [],
|
|
1637
|
-
messages: { key: "'{{key}}' isn't this package or one of its dependencies, so
|
|
1639
|
+
messages: { key: "'{{key}}' isn't this package or one of its dependencies, so this config is undefined." }
|
|
1638
1640
|
},
|
|
1639
1641
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1640
1642
|
create: ({ file, report }) => {
|
|
@@ -1659,9 +1661,9 @@ var keys_default = defineRule({
|
|
|
1659
1661
|
var public_default = defineRule({
|
|
1660
1662
|
meta: {
|
|
1661
1663
|
type: "problem",
|
|
1662
|
-
docs: { description: "Disallow copying module options into the public runtime config
|
|
1664
|
+
docs: { description: "Disallow copying all module options into the public runtime config" },
|
|
1663
1665
|
schema: [],
|
|
1664
|
-
messages: { options: "
|
|
1666
|
+
messages: { options: "This sends all module options to the browser, secrets included. Copy only the public ones, e.g. options.storefrontUrl." }
|
|
1665
1667
|
},
|
|
1666
1668
|
applies: (file) => file.side === "build",
|
|
1667
1669
|
create: ({ report }) => ({ AssignmentExpression: (node) => {
|
|
@@ -1680,17 +1682,17 @@ var public_default = defineRule({
|
|
|
1680
1682
|
} })
|
|
1681
1683
|
});
|
|
1682
1684
|
const LABELS = {
|
|
1683
|
-
"section": "
|
|
1684
|
-
"block": "
|
|
1685
|
-
"component": "
|
|
1686
|
-
"composable": "
|
|
1687
|
-
"app-plugin": "
|
|
1688
|
-
"override": "
|
|
1689
|
-
"route": "API
|
|
1690
|
-
"nitro-plugin": "Nitro
|
|
1691
|
-
"orchestr-plugin": "orchestr
|
|
1692
|
-
"orchestr-file": "orchestr",
|
|
1693
|
-
"media-library": "media
|
|
1685
|
+
"section": "sections",
|
|
1686
|
+
"block": "blocks",
|
|
1687
|
+
"component": "components",
|
|
1688
|
+
"composable": "composables",
|
|
1689
|
+
"app-plugin": "plugins",
|
|
1690
|
+
"override": "overrides",
|
|
1691
|
+
"route": "API routes",
|
|
1692
|
+
"nitro-plugin": "Nitro plugins",
|
|
1693
|
+
"orchestr-plugin": "orchestr plugins",
|
|
1694
|
+
"orchestr-file": "orchestr files",
|
|
1695
|
+
"media-library": "media libraries"
|
|
1694
1696
|
};
|
|
1695
1697
|
const ABOVE_APP_UTILS = /* @__PURE__ */ new Set([
|
|
1696
1698
|
"section",
|
|
@@ -1741,20 +1743,20 @@ function findViolation(importer, target, typeOnly) {
|
|
|
1741
1743
|
var layers_default = defineRule({
|
|
1742
1744
|
meta: {
|
|
1743
1745
|
type: "problem",
|
|
1744
|
-
docs: { description: "Enforce
|
|
1746
|
+
docs: { description: "Enforce which parts of a Laioutr app can import each other" },
|
|
1745
1747
|
schema: [],
|
|
1746
1748
|
messages: {
|
|
1747
|
-
appImportsServer: "App code can't import server code. Move what both
|
|
1748
|
-
serverImportsApp: "Server code can't import app code. Move what both
|
|
1749
|
-
sharedImportsSide: "Shared code
|
|
1750
|
-
runtimeImportsBuild: "Runtime code can't import build
|
|
1751
|
-
handlerImported: "
|
|
1752
|
-
clientInHandler: "Handlers
|
|
1753
|
-
serverUtilImportsUp: "Server utils
|
|
1754
|
-
appUtilImportsUp: "App utils
|
|
1755
|
-
composableImportsUp: "Composables can't import
|
|
1756
|
-
componentImportsSection: "Components can't import sections
|
|
1757
|
-
nodeBuiltin: "'{{name}}' only
|
|
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/."
|
|
1758
1760
|
}
|
|
1759
1761
|
},
|
|
1760
1762
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
package/package.json
CHANGED