larrylint 0.2.0 → 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 +29 -34
- 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 +301 -308
- package/dist/index.d.mts +0 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
+
| `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
|
-
Type imports are fine
|
|
41
|
+
Type imports are always fine, since they don't end up in the bundle.
|
|
42
42
|
|
|
43
43
|
### Orchestr
|
|
44
44
|
|
|
@@ -47,66 +47,61 @@ 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/
|
|
51
|
-
| `larrylint/initware-throws` | `extendRequest()` doesn't throw, directly or through a function it calls: it runs before every query, so a throw takes down every page. |
|
|
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
|
|
|
54
53
|
### Sections and blocks
|
|
55
54
|
|
|
56
55
|
| Rule | Description |
|
|
57
56
|
| --- | --- |
|
|
58
|
-
| `larrylint/definition-folder` | `defineSection()`
|
|
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
|
-
| `larrylint/single-root` | Sections and blocks render one root element, also through a component they wrap. Otherwise Vue drops the `data-lfc-*`
|
|
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. |
|
|
62
61
|
| `larrylint/slot-children` | Nothing counts slot children with `.length`: frontend-core passes all blocks of a slot as one Fragment. |
|
|
63
62
|
| `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
|
|
63
|
+
| `larrylint/field-name-case` | Top-level schema field names have no `-` and don't start with `$`, which Vue renames or rejects as props. |
|
|
65
64
|
| `larrylint/required-fields` | Schema fields have no `required`, which Studio ignores. Give them a `default` instead. |
|
|
66
65
|
| `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
|
|
66
|
+
| `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
67
|
|
|
69
68
|
### Frontend
|
|
70
69
|
|
|
71
70
|
| Rule | Description |
|
|
72
71
|
| --- | --- |
|
|
73
72
|
| `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
|
|
73
|
+
| `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. |
|
|
74
|
+
| `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
75
|
| `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
76
|
| `larrylint/internal-anchors` | Internal and resolved links use `<NuxtLink>`: a plain `<a>` reloads the page and breaks Studio's navigation sync. |
|
|
78
77
|
|
|
79
78
|
## Layout
|
|
80
79
|
|
|
81
|
-
larrylint
|
|
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
|
|
84
|
+
├── module.ts # registers sections, blocks, orchestr handlers and plugins
|
|
86
85
|
└── runtime/
|
|
87
|
-
├── app/
|
|
88
|
-
│ ├──
|
|
89
|
-
│ ├──
|
|
90
|
-
│
|
|
91
|
-
|
|
92
|
-
│ ├──
|
|
93
|
-
│ ├──
|
|
94
|
-
│ ├──
|
|
95
|
-
│ ├──
|
|
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
|
-
|
|
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.
|
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, 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) {
|
|
@@ -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",
|
|
@@ -107,6 +30,7 @@ const EXTENSIONS = [
|
|
|
107
30
|
"/index.ts",
|
|
108
31
|
"/index.js"
|
|
109
32
|
];
|
|
33
|
+
const SOURCE_EXTENSION = /\.(?:vue|[cm]?[jt]sx?)$/;
|
|
110
34
|
const directories = /* @__PURE__ */ new Map();
|
|
111
35
|
function cachedReader(parse) {
|
|
112
36
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -139,6 +63,9 @@ function isDirectory(path) {
|
|
|
139
63
|
}
|
|
140
64
|
return result;
|
|
141
65
|
}
|
|
66
|
+
function withoutExtension(path) {
|
|
67
|
+
return path.replace(SOURCE_EXTENSION, "");
|
|
68
|
+
}
|
|
142
69
|
function resolveImport(importer, source) {
|
|
143
70
|
if (!source.startsWith("./") && !source.startsWith("../")) return;
|
|
144
71
|
const target = resolve(dirname(importer), source.split("?")[0]);
|
|
@@ -206,6 +133,182 @@ function applyBaseline(cwd, results) {
|
|
|
206
133
|
improved
|
|
207
134
|
};
|
|
208
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
|
+
}
|
|
249
|
+
const HANDLER_FILE = /\.(query|resolver|link|action|templates?|page-index)(?:\.[cm]?[jt]s)?$/;
|
|
250
|
+
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
251
|
+
const APP_FOLDERS = { components: "component" };
|
|
252
|
+
const SERVER_FOLDERS = { client: "client" };
|
|
253
|
+
function classify(file) {
|
|
254
|
+
const absolute = normalize(file);
|
|
255
|
+
const test = TEST_FILE.test(absolute);
|
|
256
|
+
const runtimeIndex = absolute.lastIndexOf("/src/runtime/");
|
|
257
|
+
if (runtimeIndex !== -1) {
|
|
258
|
+
const root = absolute.slice(0, runtimeIndex);
|
|
259
|
+
return {
|
|
260
|
+
root,
|
|
261
|
+
path: absolute.slice(root.length + 1),
|
|
262
|
+
test,
|
|
263
|
+
...classifyRuntime(absolute.slice(runtimeIndex + 13), readRegistration(root))
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const srcIndex = absolute.lastIndexOf("/src/");
|
|
267
|
+
if (srcIndex === -1) return;
|
|
268
|
+
const root = absolute.slice(0, srcIndex);
|
|
269
|
+
const path = absolute.slice(root.length + 1);
|
|
270
|
+
return {
|
|
271
|
+
root,
|
|
272
|
+
path,
|
|
273
|
+
side: path.startsWith("src/types/") ? "other" : "build",
|
|
274
|
+
test
|
|
275
|
+
};
|
|
276
|
+
}
|
|
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 {
|
|
287
|
+
side,
|
|
288
|
+
kind: "block"
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
side,
|
|
292
|
+
kind: registration.plugins.has(withoutExtension(path)) ? "app-plugin" : APP_FOLDERS[folder] ?? "other"
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const orchestr = registration.orchestr.find((dir) => path.startsWith(`${dir}/`));
|
|
296
|
+
if (!orchestr) return {
|
|
297
|
+
side,
|
|
298
|
+
kind: SERVER_FOLDERS[folder] ?? "other"
|
|
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
|
+
};
|
|
311
|
+
}
|
|
209
312
|
function defineRule(rule) {
|
|
210
313
|
return {
|
|
211
314
|
meta: rule.meta,
|
|
@@ -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
|
}
|
|
@@ -396,11 +480,11 @@ function canReturnNullish(fn, value) {
|
|
|
396
480
|
var mutations_default = defineRule({
|
|
397
481
|
meta: {
|
|
398
482
|
type: "problem",
|
|
399
|
-
docs: { description: "Require error handling
|
|
483
|
+
docs: { description: "Require error handling for awaited or dropped `mutateAsync()` calls" },
|
|
400
484
|
schema: [],
|
|
401
485
|
messages: {
|
|
402
|
-
unhandled: "If this mutation fails,
|
|
403
|
-
dropped: "Nothing
|
|
486
|
+
unhandled: "If this mutation fails, an error replaces the whole section or block. Catch it with try/catch or .catch().",
|
|
487
|
+
dropped: "Nothing catches errors of this mutation. Add .catch(), or await it in a try/catch."
|
|
404
488
|
}
|
|
405
489
|
},
|
|
406
490
|
applies: (file) => file.side === "app",
|
|
@@ -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;
|
|
@@ -607,9 +681,9 @@ function rendersTag(element, tag) {
|
|
|
607
681
|
var anchors_default = defineRule({
|
|
608
682
|
meta: {
|
|
609
683
|
type: "problem",
|
|
610
|
-
docs: { description: "Disallow plain `<a>` tags for internal
|
|
684
|
+
docs: { description: "Disallow plain `<a>` tags for internal links" },
|
|
611
685
|
schema: [],
|
|
612
|
-
messages: { anchor: "A plain <a> reloads the whole page
|
|
686
|
+
messages: { anchor: "A plain <a> reloads the whole page. Use <NuxtLink> for internal links." }
|
|
613
687
|
},
|
|
614
688
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
615
689
|
create: ({ report, visitTemplate }) => {
|
|
@@ -646,9 +720,9 @@ const ROUTER_METHODS = /* @__PURE__ */ new Set(["push", "replace"]);
|
|
|
646
720
|
var paths_default = defineRule({
|
|
647
721
|
meta: {
|
|
648
722
|
type: "problem",
|
|
649
|
-
docs: { description: "Disallow
|
|
723
|
+
docs: { description: "Disallow page paths built by hand" },
|
|
650
724
|
schema: [],
|
|
651
|
-
messages: { path: "
|
|
725
|
+
messages: { path: "Hand-built page paths differ per language and market. Use linkResolver instead." }
|
|
652
726
|
},
|
|
653
727
|
applies: (file) => file.side === "app",
|
|
654
728
|
create: ({ report, visitTemplate }) => {
|
|
@@ -681,11 +755,11 @@ var paths_default = defineRule({
|
|
|
681
755
|
var resolve_default = defineRule({
|
|
682
756
|
meta: {
|
|
683
757
|
type: "problem",
|
|
684
|
-
docs: { description: "Disallow
|
|
758
|
+
docs: { description: "Disallow checking the result of `linkResolver.resolve()`" },
|
|
685
759
|
schema: [],
|
|
686
760
|
messages: {
|
|
687
|
-
tested: "linkResolver.resolve() returns
|
|
688
|
-
branch: "
|
|
761
|
+
tested: "linkResolver.resolve() never returns an empty value, so this check misses broken links. Use resolveOrThrow() in a try/catch instead.",
|
|
762
|
+
branch: "linkResolver.resolve() never returns an empty value, so the checks on '{{name}}' miss broken links. Use resolveOrThrow() in a try/catch instead."
|
|
689
763
|
}
|
|
690
764
|
},
|
|
691
765
|
applies: (file) => file.side === "app",
|
|
@@ -765,9 +839,9 @@ var button_default = defineRule({
|
|
|
765
839
|
meta: {
|
|
766
840
|
type: "problem",
|
|
767
841
|
fixable: "code",
|
|
768
|
-
docs: { description: "Disallow `type` on
|
|
842
|
+
docs: { description: "Disallow `type` on ui-kit buttons" },
|
|
769
843
|
schema: [],
|
|
770
|
-
messages: { buttonType: "<{{tag}}> ignores type
|
|
844
|
+
messages: { buttonType: "<{{tag}}> ignores type. Use button-type instead." }
|
|
771
845
|
},
|
|
772
846
|
applies: (file) => file.path.endsWith(".vue"),
|
|
773
847
|
create: ({ report, visitTemplate }) => {
|
|
@@ -861,11 +935,11 @@ const MANAGED = {
|
|
|
861
935
|
var cookies_default = defineRule({
|
|
862
936
|
meta: {
|
|
863
937
|
type: "problem",
|
|
864
|
-
docs: { description: "
|
|
938
|
+
docs: { description: "Require cookies and headers to be set in `extendRequest()` or action handlers, with frontend-core's managed cookie functions" },
|
|
865
939
|
schema: [],
|
|
866
940
|
messages: {
|
|
867
|
-
streamed: "
|
|
868
|
-
managed: "Use {{managed}}() instead
|
|
941
|
+
streamed: "The headers may already be sent when {{name}}() runs here. Call it in extendRequest() or an action handler instead.",
|
|
942
|
+
managed: "Use {{managed}}() instead, so it also works in the Studio preview."
|
|
869
943
|
}
|
|
870
944
|
},
|
|
871
945
|
applies: (file) => file.side === "server",
|
|
@@ -899,12 +973,12 @@ const FILE_START = {
|
|
|
899
973
|
column: 0
|
|
900
974
|
}
|
|
901
975
|
};
|
|
902
|
-
var files_default
|
|
976
|
+
var files_default = defineRule({
|
|
903
977
|
meta: {
|
|
904
978
|
type: "problem",
|
|
905
|
-
docs: { description: "
|
|
979
|
+
docs: { description: "Disallow files other than handlers in `orchestr/`" },
|
|
906
980
|
schema: [],
|
|
907
|
-
messages: { notAHandler: "
|
|
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: () => {
|
|
@@ -917,9 +991,9 @@ var files_default$1 = defineRule({
|
|
|
917
991
|
var exports_default = defineRule({
|
|
918
992
|
meta: {
|
|
919
993
|
type: "problem",
|
|
920
|
-
docs: { description: "Require handler files to export their handler as default
|
|
994
|
+
docs: { description: "Require handler files to export their handler as default" },
|
|
921
995
|
schema: [],
|
|
922
|
-
messages: { missingDefault: "
|
|
996
|
+
messages: { missingDefault: "Export the handler as default, or it will fail the build." }
|
|
923
997
|
},
|
|
924
998
|
applies: (file) => file.kind === "handler",
|
|
925
999
|
create: ({ report }) => {
|
|
@@ -940,47 +1014,14 @@ 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: "Keep server/middleware/ to orchestr and Nitro middleware." },
|
|
954
|
-
schema: [],
|
|
955
|
-
messages: { helper: "server/middleware/ holds orchestr and Nitro middleware. Move helpers like this to server/utils/." }
|
|
956
|
-
},
|
|
957
|
-
applies: (file) => file.kind === "middleware",
|
|
958
|
-
create: ({ report }) => {
|
|
959
|
-
let middleware = false;
|
|
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",
|
|
979
|
-
docs: { description: "Disallow throwing in extendRequest()
|
|
1020
|
+
docs: { description: "Disallow throwing in `extendRequest()`" },
|
|
980
1021
|
schema: [],
|
|
981
1022
|
messages: {
|
|
982
|
-
throws: "extendRequest()
|
|
983
|
-
throwsInCall: "{{name}}() can throw ({{location}})
|
|
1023
|
+
throws: "A throw in extendRequest() breaks every page. Throw later instead, e.g. when the client is used.",
|
|
1024
|
+
throwsInCall: "{{name}}() can throw ({{location}}), and a throw in extendRequest() breaks every page. Catch it here."
|
|
984
1025
|
}
|
|
985
1026
|
},
|
|
986
1027
|
applies: (file) => file.side === "server",
|
|
@@ -1034,9 +1075,9 @@ function canonicalNamespaces(root) {
|
|
|
1034
1075
|
var namespaces_default = defineRule({
|
|
1035
1076
|
meta: {
|
|
1036
1077
|
type: "problem",
|
|
1037
|
-
docs: { description: "
|
|
1078
|
+
docs: { description: "Disallow your own token ids in the namespaces of Laioutr's canonical types" },
|
|
1038
1079
|
schema: [],
|
|
1039
|
-
messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types
|
|
1080
|
+
messages: { canonical: "'{{namespace}}/' belongs to Laioutr's canonical types. Use your app's own namespace." }
|
|
1040
1081
|
},
|
|
1041
1082
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1042
1083
|
create: ({ file, report }) => ({ CallExpression: (node) => {
|
|
@@ -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
|
-
|
|
1097
|
+
option: "sections"
|
|
1071
1098
|
},
|
|
1072
1099
|
defineBlock: {
|
|
1073
1100
|
kind: "block",
|
|
1074
|
-
|
|
1101
|
+
option: "blocks"
|
|
1075
1102
|
}
|
|
1076
1103
|
};
|
|
1077
1104
|
const PICKERS = /* @__PURE__ */ new Set([
|
|
@@ -1079,10 +1106,10 @@ const PICKERS = /* @__PURE__ */ new Set([
|
|
|
1079
1106
|
"radio",
|
|
1080
1107
|
"toggle_button"
|
|
1081
1108
|
]);
|
|
1082
|
-
const TEXTS
|
|
1109
|
+
const TEXTS = /* @__PURE__ */ new Set([
|
|
1083
1110
|
"text",
|
|
1084
1111
|
"textarea",
|
|
1085
|
-
"
|
|
1112
|
+
"richtext"
|
|
1086
1113
|
]);
|
|
1087
1114
|
function readDefinition(call) {
|
|
1088
1115
|
if (call.callee.type !== "Identifier" || !Object.hasOwn(DEFINERS, call.callee.name)) return;
|
|
@@ -1138,7 +1165,7 @@ function expectedDefiner(kind) {
|
|
|
1138
1165
|
}
|
|
1139
1166
|
function fillValue(field, program, filename) {
|
|
1140
1167
|
if (field.type === "checkbox") return { value: field.as === "visibility" };
|
|
1141
|
-
if (TEXTS
|
|
1168
|
+
if (TEXTS.has(field.type ?? "")) return { value: "" };
|
|
1142
1169
|
if (field.type === "content_alignment") {
|
|
1143
1170
|
const axis = findStringProperty(field.node, "axis")?.value;
|
|
1144
1171
|
return { value: !axis || axis === "both" ? "center-center" : "center" };
|
|
@@ -1155,31 +1182,33 @@ function fillValue(field, program, filename) {
|
|
|
1155
1182
|
var description_default = defineRule({
|
|
1156
1183
|
meta: {
|
|
1157
1184
|
type: "suggestion",
|
|
1158
|
-
docs: { description: "Require a studio.description on section and block definitions
|
|
1185
|
+
docs: { description: "Require a `studio.description` on section and block definitions" },
|
|
1159
1186
|
schema: [],
|
|
1160
|
-
messages: { description: "
|
|
1187
|
+
messages: { description: "Add a studio.description, so editors and AI agents know what this {{kind}} is for." }
|
|
1161
1188
|
},
|
|
1162
1189
|
applies: (file) => file.side === "app",
|
|
1163
1190
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
1164
|
-
const
|
|
1165
|
-
if (!options) return;
|
|
1166
|
-
const studio = findProperty(options, "studio");
|
|
1191
|
+
const definition = readDefinition(node);
|
|
1192
|
+
if (!definition?.options) return;
|
|
1193
|
+
const studio = findProperty(definition.options, "studio");
|
|
1167
1194
|
if (studio && studio.type !== "ObjectExpression") return;
|
|
1168
1195
|
const description = studio && findProperty(studio, "description");
|
|
1169
1196
|
if (!description || description.type === "Literal" && String(description.value ?? "").trim() === "") report({
|
|
1170
1197
|
node: node.callee,
|
|
1171
|
-
messageId: "description"
|
|
1198
|
+
messageId: "description",
|
|
1199
|
+
data: { kind: DEFINERS[definition.definer].kind }
|
|
1172
1200
|
});
|
|
1173
1201
|
} })
|
|
1174
1202
|
});
|
|
1175
1203
|
var folder_default = defineRule({
|
|
1176
1204
|
meta: {
|
|
1177
1205
|
type: "problem",
|
|
1178
|
-
docs: { description: "
|
|
1206
|
+
docs: { description: "Require sections and blocks in the folders `module.ts` registers for them, and a definition in every `.vue` registered there" },
|
|
1179
1207
|
schema: [],
|
|
1180
1208
|
messages: {
|
|
1181
|
-
notDefined: "
|
|
1182
|
-
wrongFolder: "{{definer}}() belongs in
|
|
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()."
|
|
1183
1212
|
}
|
|
1184
1213
|
},
|
|
1185
1214
|
applies: (file) => file.side === "app",
|
|
@@ -1189,31 +1218,33 @@ var folder_default = defineRule({
|
|
|
1189
1218
|
"CallExpression": (node) => {
|
|
1190
1219
|
const definition = readDefinition(node);
|
|
1191
1220
|
if (!definition) return;
|
|
1192
|
-
const { kind,
|
|
1221
|
+
const { kind, option } = DEFINERS[definition.definer];
|
|
1193
1222
|
definers.add(definition.definer);
|
|
1194
|
-
if (file.kind
|
|
1223
|
+
if (file.kind === kind || isReferenced(file)) return;
|
|
1224
|
+
const [entry] = readRegistration(file.root)[option];
|
|
1225
|
+
report(entry ? {
|
|
1195
1226
|
node: node.callee,
|
|
1196
1227
|
messageId: "wrongFolder",
|
|
1197
1228
|
data: {
|
|
1198
1229
|
definer: definition.definer,
|
|
1199
|
-
folder
|
|
1230
|
+
folder: entryFolder(entry)
|
|
1200
1231
|
}
|
|
1232
|
+
} : {
|
|
1233
|
+
node: node.callee,
|
|
1234
|
+
messageId: "notRegistered",
|
|
1235
|
+
data: { kind }
|
|
1201
1236
|
});
|
|
1202
1237
|
},
|
|
1203
1238
|
"Program:exit": () => {
|
|
1204
1239
|
const expected = expectedDefiner(file.kind);
|
|
1205
|
-
if (expected && file.path.endsWith(".vue") && !definers.has(expected)) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
definer: expected
|
|
1214
|
-
}
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1240
|
+
if (expected && file.path.endsWith(".vue") && !definers.has(expected)) report({
|
|
1241
|
+
loc: FILE_START,
|
|
1242
|
+
messageId: "notDefined",
|
|
1243
|
+
data: {
|
|
1244
|
+
kind: DEFINERS[expected].kind,
|
|
1245
|
+
definer: expected
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1217
1248
|
}
|
|
1218
1249
|
};
|
|
1219
1250
|
}
|
|
@@ -1221,7 +1252,7 @@ var folder_default = defineRule({
|
|
|
1221
1252
|
var name_default = defineRule({
|
|
1222
1253
|
meta: {
|
|
1223
1254
|
type: "problem",
|
|
1224
|
-
docs: { description: "Require the `component` of a
|
|
1255
|
+
docs: { description: "Require the `component` of a definition to match its file name" },
|
|
1225
1256
|
schema: [],
|
|
1226
1257
|
messages: { componentName: "component: '{{actual}}' must match the file name '{{expected}}'." }
|
|
1227
1258
|
},
|
|
@@ -1239,20 +1270,23 @@ var name_default = defineRule({
|
|
|
1239
1270
|
});
|
|
1240
1271
|
} })
|
|
1241
1272
|
});
|
|
1242
|
-
const
|
|
1243
|
-
"
|
|
1244
|
-
"
|
|
1245
|
-
"
|
|
1246
|
-
|
|
1273
|
+
const MESSAGES = {
|
|
1274
|
+
checkbox: "checkbox",
|
|
1275
|
+
content_alignment: "alignment",
|
|
1276
|
+
text: "text",
|
|
1277
|
+
textarea: "text",
|
|
1278
|
+
richtext: "text"
|
|
1279
|
+
};
|
|
1247
1280
|
var fallbacks_default = defineRule({
|
|
1248
1281
|
meta: {
|
|
1249
1282
|
type: "problem",
|
|
1250
|
-
docs: { description: "Disallow
|
|
1283
|
+
docs: { description: "Disallow `??` fallbacks on fields that frontend-core always fills" },
|
|
1251
1284
|
schema: [],
|
|
1252
1285
|
messages: {
|
|
1253
|
-
picker: "
|
|
1254
|
-
|
|
1255
|
-
|
|
1286
|
+
picker: "This fallback is never used: an unset {{type}} gets its first option, {{fill}}. To use {{fallback}}, make it the first option.",
|
|
1287
|
+
alignment: "This fallback is never used: an unset content_alignment is {{fill}}. Remove the fallback.",
|
|
1288
|
+
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.",
|
|
1289
|
+
text: "An unset {{type}} field is '', so ?? never falls back. Use || instead."
|
|
1256
1290
|
}
|
|
1257
1291
|
},
|
|
1258
1292
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
@@ -1290,7 +1324,7 @@ var fallbacks_default = defineRule({
|
|
|
1290
1324
|
for (const { node, prop } of fallbacks) {
|
|
1291
1325
|
const field = fills.get(prop);
|
|
1292
1326
|
if (!field || node.right.type === "Literal" && node.right.value === field.value) continue;
|
|
1293
|
-
const messageId = field.type
|
|
1327
|
+
const messageId = MESSAGES[field.type] ?? "picker";
|
|
1294
1328
|
const fill = typeof field.value === "string" ? `'${field.value}'` : String(field.value);
|
|
1295
1329
|
report({
|
|
1296
1330
|
node,
|
|
@@ -1357,9 +1391,9 @@ const NOT_READ = /* @__PURE__ */ new Set([
|
|
|
1357
1391
|
var unused_default = defineRule({
|
|
1358
1392
|
meta: {
|
|
1359
1393
|
type: "suggestion",
|
|
1360
|
-
docs: { description: "Disallow schema fields the
|
|
1394
|
+
docs: { description: "Disallow schema fields the component never uses" },
|
|
1361
1395
|
schema: [],
|
|
1362
|
-
messages: { unused: "
|
|
1396
|
+
messages: { unused: "The component never uses '{{name}}'. Use the field or remove it." }
|
|
1363
1397
|
},
|
|
1364
1398
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1365
1399
|
create: ({ context, file, report, visitTemplate }) => {
|
|
@@ -1412,11 +1446,11 @@ var unused_default = defineRule({
|
|
|
1412
1446
|
var casing_default = defineRule({
|
|
1413
1447
|
meta: {
|
|
1414
1448
|
type: "problem",
|
|
1415
|
-
docs: { description: "Disallow
|
|
1449
|
+
docs: { description: "Disallow schema field names that Vue renames or rejects" },
|
|
1416
1450
|
schema: [],
|
|
1417
1451
|
messages: {
|
|
1418
|
-
hyphen: "Vue
|
|
1419
|
-
dollar: "Vue
|
|
1452
|
+
hyphen: "Vue renames '{{name}}' to '{{suggestion}}', so the component never gets this field. Name it '{{suggestion}}'.",
|
|
1453
|
+
dollar: "Vue doesn't allow props that start with $, so the component never gets this field. Rename it."
|
|
1420
1454
|
}
|
|
1421
1455
|
},
|
|
1422
1456
|
applies: (file) => file.side === "app",
|
|
@@ -1440,9 +1474,9 @@ var casing_default = defineRule({
|
|
|
1440
1474
|
var required_default = defineRule({
|
|
1441
1475
|
meta: {
|
|
1442
1476
|
type: "problem",
|
|
1443
|
-
docs: { description: "Disallow `required` on schema fields
|
|
1477
|
+
docs: { description: "Disallow `required` on schema fields" },
|
|
1444
1478
|
schema: [],
|
|
1445
|
-
messages: { required: "
|
|
1479
|
+
messages: { required: "Studio ignores required. Give the field a default instead." }
|
|
1446
1480
|
},
|
|
1447
1481
|
applies: (file) => file.side === "app",
|
|
1448
1482
|
create: ({ report }) => ({ CallExpression: (node) => {
|
|
@@ -1467,11 +1501,11 @@ const RESERVED = /* @__PURE__ */ new Set([
|
|
|
1467
1501
|
var reserved_default = defineRule({
|
|
1468
1502
|
meta: {
|
|
1469
1503
|
type: "problem",
|
|
1470
|
-
docs: { description: "Disallow
|
|
1504
|
+
docs: { description: "Disallow schema field names that Vue or frontend-core already use" },
|
|
1471
1505
|
schema: [],
|
|
1472
1506
|
messages: {
|
|
1473
|
-
reserved: "'{{name}}'
|
|
1474
|
-
slots: "frontend-core passes
|
|
1507
|
+
reserved: "Vue uses '{{name}}' itself, so the component never gets this field. Rename it.",
|
|
1508
|
+
slots: "frontend-core passes the blocks in the slots prop, so it overwrites this field. Rename it."
|
|
1475
1509
|
}
|
|
1476
1510
|
},
|
|
1477
1511
|
applies: (file) => file.side === "app",
|
|
@@ -1506,29 +1540,31 @@ const OWN_ATTRS = /inheritAttrs\s*:\s*false/;
|
|
|
1506
1540
|
var root_default = defineRule({
|
|
1507
1541
|
meta: {
|
|
1508
1542
|
type: "problem",
|
|
1509
|
-
docs: { description: "Require sections and blocks to render one root element
|
|
1543
|
+
docs: { description: "Require sections and blocks to render one root element" },
|
|
1510
1544
|
schema: [],
|
|
1511
1545
|
messages: {
|
|
1512
|
-
roots: "
|
|
1513
|
-
componentRoots: "<{{name}}>
|
|
1546
|
+
roots: "Studio can't select a {{kind}} with more than one root element. Wrap the content in one element.",
|
|
1547
|
+
componentRoots: "<{{name}}> has more than one root element, so Studio can't select this {{kind}}. Give it one root, or wrap it here."
|
|
1514
1548
|
}
|
|
1515
1549
|
},
|
|
1516
1550
|
applies: (file) => file.side === "app" && file.path.endsWith(".vue"),
|
|
1517
1551
|
create: ({ context, report }) => {
|
|
1518
|
-
let
|
|
1552
|
+
let kind;
|
|
1519
1553
|
return {
|
|
1520
1554
|
"CallExpression": (node) => {
|
|
1521
|
-
|
|
1555
|
+
const definition = readDefinition(node);
|
|
1556
|
+
kind ??= definition && DEFINERS[definition.definer].kind;
|
|
1522
1557
|
},
|
|
1523
1558
|
"Program:exit": () => {
|
|
1524
1559
|
const program = context.sourceCode.ast;
|
|
1525
1560
|
const template = program.templateBody;
|
|
1526
|
-
if (!
|
|
1561
|
+
if (!kind || !template || OWN_ATTRS.test(context.sourceCode.text)) return;
|
|
1527
1562
|
const [root, second] = renderedRoots(template);
|
|
1528
1563
|
if (second) {
|
|
1529
1564
|
report({
|
|
1530
1565
|
loc: second.startTag.loc,
|
|
1531
|
-
messageId: "roots"
|
|
1566
|
+
messageId: "roots",
|
|
1567
|
+
data: { kind }
|
|
1532
1568
|
});
|
|
1533
1569
|
return;
|
|
1534
1570
|
}
|
|
@@ -1538,7 +1574,10 @@ var root_default = defineRule({
|
|
|
1538
1574
|
if (root && componentTemplate && renderedRoots(componentTemplate).length > 1 && !OWN_ATTRS.test(component.text)) report({
|
|
1539
1575
|
loc: root.startTag.loc,
|
|
1540
1576
|
messageId: "componentRoots",
|
|
1541
|
-
data: {
|
|
1577
|
+
data: {
|
|
1578
|
+
name: root.rawName,
|
|
1579
|
+
kind
|
|
1580
|
+
}
|
|
1542
1581
|
});
|
|
1543
1582
|
}
|
|
1544
1583
|
};
|
|
@@ -1547,9 +1586,9 @@ var root_default = defineRule({
|
|
|
1547
1586
|
var slots_default = defineRule({
|
|
1548
1587
|
meta: {
|
|
1549
1588
|
type: "problem",
|
|
1550
|
-
docs: { description: "Disallow counting
|
|
1589
|
+
docs: { description: "Disallow counting the blocks of a slot with `.length`" },
|
|
1551
1590
|
schema: [],
|
|
1552
|
-
messages: { length: "frontend-core passes
|
|
1591
|
+
messages: { length: "For blocks, this is always 1: frontend-core passes them as one Fragment. Count the Fragment's children instead." }
|
|
1553
1592
|
},
|
|
1554
1593
|
applies: (file) => file.side === "app",
|
|
1555
1594
|
create: ({ report, visitTemplate }) => {
|
|
@@ -1578,7 +1617,7 @@ var slots_default = defineRule({
|
|
|
1578
1617
|
var bundle_default = defineRule({
|
|
1579
1618
|
meta: {
|
|
1580
1619
|
type: "problem",
|
|
1581
|
-
docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins
|
|
1620
|
+
docs: { description: "Disallow static imports of heavy packages in sections, blocks and plugins" },
|
|
1582
1621
|
schema: [{
|
|
1583
1622
|
type: "object",
|
|
1584
1623
|
properties: { packages: {
|
|
@@ -1587,7 +1626,7 @@ var bundle_default = defineRule({
|
|
|
1587
1626
|
} },
|
|
1588
1627
|
additionalProperties: false
|
|
1589
1628
|
}],
|
|
1590
|
-
messages: { heavy: "
|
|
1629
|
+
messages: { heavy: "This loads '{{name}}' on every page. Load it with import() or defineAsyncComponent() instead." }
|
|
1591
1630
|
},
|
|
1592
1631
|
applies: (file) => file.kind === "section" || file.kind === "block" || file.kind === "app-plugin",
|
|
1593
1632
|
create: ({ context, report }) => {
|
|
@@ -1632,9 +1671,9 @@ function setupOptions(node) {
|
|
|
1632
1671
|
var keys_default = defineRule({
|
|
1633
1672
|
meta: {
|
|
1634
1673
|
type: "problem",
|
|
1635
|
-
docs: { description: "Require runtime config keys
|
|
1674
|
+
docs: { description: "Require runtime config keys to be this package or one of its dependencies" },
|
|
1636
1675
|
schema: [],
|
|
1637
|
-
messages: { key: "'{{key}}' isn't this package or one of its dependencies, so
|
|
1676
|
+
messages: { key: "'{{key}}' isn't this package or one of its dependencies, so this config is undefined." }
|
|
1638
1677
|
},
|
|
1639
1678
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
1640
1679
|
create: ({ file, report }) => {
|
|
@@ -1659,9 +1698,9 @@ var keys_default = defineRule({
|
|
|
1659
1698
|
var public_default = defineRule({
|
|
1660
1699
|
meta: {
|
|
1661
1700
|
type: "problem",
|
|
1662
|
-
docs: { description: "Disallow copying module options into the public runtime config
|
|
1701
|
+
docs: { description: "Disallow copying all module options into the public runtime config" },
|
|
1663
1702
|
schema: [],
|
|
1664
|
-
messages: { options: "
|
|
1703
|
+
messages: { options: "This sends all module options to the browser, secrets included. Copy only the public ones, e.g. options.storefrontUrl." }
|
|
1665
1704
|
},
|
|
1666
1705
|
applies: (file) => file.side === "build",
|
|
1667
1706
|
create: ({ report }) => ({ AssignmentExpression: (node) => {
|
|
@@ -1679,82 +1718,37 @@ var public_default = defineRule({
|
|
|
1679
1718
|
});
|
|
1680
1719
|
} })
|
|
1681
1720
|
});
|
|
1682
|
-
const LABELS = {
|
|
1683
|
-
"section": "section",
|
|
1684
|
-
"block": "block",
|
|
1685
|
-
"component": "component",
|
|
1686
|
-
"composable": "composable",
|
|
1687
|
-
"app-plugin": "plugin",
|
|
1688
|
-
"override": "override",
|
|
1689
|
-
"route": "API route",
|
|
1690
|
-
"nitro-plugin": "Nitro plugin",
|
|
1691
|
-
"orchestr-plugin": "orchestr plugin",
|
|
1692
|
-
"orchestr-file": "orchestr",
|
|
1693
|
-
"media-library": "media library"
|
|
1694
|
-
};
|
|
1695
|
-
const ABOVE_APP_UTILS = /* @__PURE__ */ new Set([
|
|
1696
|
-
"section",
|
|
1697
|
-
"block",
|
|
1698
|
-
"component",
|
|
1699
|
-
"composable",
|
|
1700
|
-
"app-plugin",
|
|
1701
|
-
"override"
|
|
1702
|
-
]);
|
|
1703
|
-
const ABOVE_COMPOSABLES = /* @__PURE__ */ new Set([
|
|
1704
|
-
"section",
|
|
1705
|
-
"block",
|
|
1706
|
-
"component",
|
|
1707
|
-
"override"
|
|
1708
|
-
]);
|
|
1709
|
-
const ABOVE_SERVER_UTILS = /* @__PURE__ */ new Set([
|
|
1710
|
-
"route",
|
|
1711
|
-
"nitro-plugin",
|
|
1712
|
-
"orchestr-plugin",
|
|
1713
|
-
"orchestr-file",
|
|
1714
|
-
"media-library"
|
|
1715
|
-
]);
|
|
1716
1721
|
function findViolation(importer, target, typeOnly) {
|
|
1722
|
+
if (typeOnly) return;
|
|
1717
1723
|
if (importer.side === "app" && target.side === "server") return { messageId: "appImportsServer" };
|
|
1718
1724
|
if (importer.side === "server" && target.side === "app") return { messageId: "serverImportsApp" };
|
|
1719
1725
|
if (importer.side === "shared" && (target.side === "app" || target.side === "server")) return {
|
|
1720
1726
|
messageId: "sharedImportsSide",
|
|
1721
1727
|
data: { side: target.side }
|
|
1722
1728
|
};
|
|
1723
|
-
if (target.side === "build"
|
|
1729
|
+
if (target.side === "build") return { messageId: "runtimeImportsBuild" };
|
|
1724
1730
|
if (target.kind === "handler") return { messageId: "handlerImported" };
|
|
1725
|
-
if (typeOnly) return;
|
|
1726
1731
|
if (target.kind === "client" && importer.kind === "handler") return { messageId: "clientInHandler" };
|
|
1727
|
-
if (importer.kind === "
|
|
1728
|
-
messageId: "
|
|
1729
|
-
data: { kind:
|
|
1730
|
-
};
|
|
1731
|
-
if (importer.kind === "app-util" && ABOVE_APP_UTILS.has(target.kind)) return {
|
|
1732
|
-
messageId: "appUtilImportsUp",
|
|
1733
|
-
data: { kind: LABELS[target.kind] }
|
|
1734
|
-
};
|
|
1735
|
-
if (importer.kind === "composable" && ABOVE_COMPOSABLES.has(target.kind)) return {
|
|
1736
|
-
messageId: "composableImportsUp",
|
|
1737
|
-
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 }
|
|
1738
1735
|
};
|
|
1739
|
-
if (importer.kind === "component" && target.kind === "section") return { messageId: "componentImportsSection" };
|
|
1740
1736
|
}
|
|
1737
|
+
const SERVER_ONLY = /\.server\.[^/]+$/;
|
|
1741
1738
|
var layers_default = defineRule({
|
|
1742
1739
|
meta: {
|
|
1743
1740
|
type: "problem",
|
|
1744
|
-
docs: { description: "Enforce
|
|
1741
|
+
docs: { description: "Enforce which parts of a Laioutr app can import each other" },
|
|
1745
1742
|
schema: [],
|
|
1746
1743
|
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
|
-
|
|
1754
|
-
|
|
1755
|
-
composableImportsUp: "Composables can't import a {{kind}}. Type imports are fine.",
|
|
1756
|
-
componentImportsSection: "Components can't import sections; sections compose components.",
|
|
1757
|
-
nodeBuiltin: "'{{name}}' only exists on the server. Keep it in server/."
|
|
1744
|
+
appImportsServer: "App code can't import server code. Move what both need to src/runtime/shared/.",
|
|
1745
|
+
serverImportsApp: "Server code can't import app code. Move what both need to src/runtime/shared/.",
|
|
1746
|
+
sharedImportsSide: "Shared code can't import from {{side}}/.",
|
|
1747
|
+
runtimeImportsBuild: "Runtime code can't import build code from src/, like module.ts. Type imports are fine.",
|
|
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.",
|
|
1751
|
+
nodeBuiltin: "'{{name}}' only works on the server. Move this code to server/."
|
|
1758
1752
|
}
|
|
1759
1753
|
},
|
|
1760
1754
|
applies: (file) => file.side === "app" || file.side === "server" || file.side === "shared",
|
|
@@ -1762,7 +1756,7 @@ var layers_default = defineRule({
|
|
|
1762
1756
|
const checkImport = (node, source, typeOnly) => {
|
|
1763
1757
|
if (typeof source !== "string") return;
|
|
1764
1758
|
if (source.startsWith("node:")) {
|
|
1765
|
-
if (importer.side !== "server" && !typeOnly) report({
|
|
1759
|
+
if (importer.side !== "server" && !typeOnly && !SERVER_ONLY.test(importer.path)) report({
|
|
1766
1760
|
node,
|
|
1767
1761
|
messageId: "nodeBuiltin",
|
|
1768
1762
|
data: { name: source }
|
|
@@ -1803,10 +1797,9 @@ const plugin = {
|
|
|
1803
1797
|
"heavy-imports": bundle_default,
|
|
1804
1798
|
"config-keys": keys_default,
|
|
1805
1799
|
"public-config": public_default,
|
|
1806
|
-
"orchestr-files": files_default
|
|
1800
|
+
"orchestr-files": files_default,
|
|
1807
1801
|
"orchestr-cookies": cookies_default,
|
|
1808
1802
|
"handler-exports": exports_default,
|
|
1809
|
-
"middleware-files": files_default,
|
|
1810
1803
|
"initware-throws": throws_default,
|
|
1811
1804
|
"token-namespaces": namespaces_default,
|
|
1812
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