larrylint 0.1.0 → 0.2.0
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 +58 -19
- package/dist/_chunks/check.mjs +2 -34
- package/dist/_chunks/check2.mjs +1 -1
- package/dist/_chunks/init.mjs +15 -15
- package/dist/_chunks/package.mjs +2 -2
- package/dist/_chunks/preset.mjs +1619 -364
- package/dist/index.d.mts +26 -8
- package/package.json +14 -13
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
[![NPM last update][npm-last-update-src]][npm-last-update-href]
|
|
6
6
|
[![License][license-src]][license-href]
|
|
7
7
|
|
|
8
|
-
Opinionated
|
|
8
|
+
Opinionated rules for [Laioutr](https://laioutr.com) apps: an ESLint preset for your editor and a CLI for CI.
|
|
9
9
|
|
|
10
|
-
Laioutr apps are Nuxt modules with a lot of moving parts: sections and blocks, orchestr handlers, middleware, API clients, server routes. larrylint keeps them where they belong and stops the imports that make a codebase drift, like app code pulling in server code
|
|
10
|
+
Laioutr apps are Nuxt modules with a lot of moving parts: sections and blocks, orchestr handlers, middleware, API clients, server routes. larrylint keeps them where they belong and stops the imports that make a codebase drift, like app code pulling in server code or handlers importing each other. It also catches the mistakes that only show up in Studio or in production, like schema fields that never reach the component, fallbacks that never apply, or cookies written after orchestr has sent the headers.
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
@@ -25,17 +25,57 @@ npx larrylint init
|
|
|
25
25
|
|
|
26
26
|
After that, `eslint .` and your editor report larrylint's rules next to your own.
|
|
27
27
|
|
|
28
|
+
`token-namespaces` reads the app's installed `@laioutr-core/canonical-types`, so install the app's dependencies first. larrylint stops with an error when it's missing.
|
|
29
|
+
|
|
28
30
|
## Rules
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
| `larrylint/
|
|
35
|
-
| `larrylint/
|
|
32
|
+
### Structure
|
|
33
|
+
|
|
34
|
+
| Rule | Description |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `larrylint/layers` | App, server and shared code stay apart, and runtime code doesn't import build-time code, or `node:` modules outside the server. Nothing imports orchestr handlers, and handlers get API clients from the orchestr context. Utils are the bottom layer, followed by composables, components and sections. |
|
|
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 lands in the chunk every page loads. |
|
|
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 reaches the browser with every token among them. |
|
|
36
40
|
|
|
37
41
|
Type imports are fine across most layers, since they don't end up in the bundle.
|
|
38
42
|
|
|
43
|
+
### Orchestr
|
|
44
|
+
|
|
45
|
+
| Rule | Description |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `larrylint/orchestr-files` | Laioutr loads every file in `orchestr/` as a Nitro plugin, so only handler files belong there. |
|
|
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
|
+
| `larrylint/handler-exports` | Handler files export their handler as default. Without one, the build fails. |
|
|
50
|
+
| `larrylint/middleware-files` | `server/middleware/` only holds orchestr and Nitro middleware; helpers go to `server/utils/`. |
|
|
51
|
+
| `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. |
|
|
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
|
+
|
|
54
|
+
### Sections and blocks
|
|
55
|
+
|
|
56
|
+
| Rule | Description |
|
|
57
|
+
| --- | --- |
|
|
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
|
+
| `larrylint/component-name` | The `component` of a definition matches its file name. |
|
|
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-*` markers frontend-core adds. |
|
|
62
|
+
| `larrylint/slot-children` | Nothing counts slot children with `.length`: frontend-core passes all blocks of a slot as one Fragment. |
|
|
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 `$`: Vue camelizes the one and rejects the other. |
|
|
65
|
+
| `larrylint/required-fields` | Schema fields have no `required`, which Studio ignores. Give them a `default` instead. |
|
|
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 apply: frontend-core fills unset pickers with their first option, checkboxes with `false` and text fields with `''`. |
|
|
68
|
+
|
|
69
|
+
### Frontend
|
|
70
|
+
|
|
71
|
+
| Rule | Description |
|
|
72
|
+
| --- | --- |
|
|
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 frontend-core's "Retry" state, or ends up as an unhandled rejection. |
|
|
75
|
+
| `larrylint/resolve-result` | Nothing tests the result of `linkResolver.resolve()`: a link it can't resolve comes back as a `#…` fallback, not as an empty value. |
|
|
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
|
+
| `larrylint/internal-anchors` | Internal and resolved links use `<NuxtLink>`: a plain `<a>` reloads the page and breaks Studio's navigation sync. |
|
|
78
|
+
|
|
39
79
|
## Layout
|
|
40
80
|
|
|
41
81
|
larrylint expects the layout of Laioutr's [app starter](https://github.com/laioutr/app-starter):
|
|
@@ -45,29 +85,28 @@ src/
|
|
|
45
85
|
├── module.ts # build time
|
|
46
86
|
└── runtime/
|
|
47
87
|
├── app/
|
|
48
|
-
│ ├── sections/ #
|
|
49
|
-
│ ├── blocks/ #
|
|
88
|
+
│ ├── sections/ # defineSection() components, or section/
|
|
89
|
+
│ ├── blocks/ # defineBlock() components, or block/
|
|
50
90
|
│ ├── components/
|
|
51
91
|
│ ├── composables/
|
|
52
92
|
│ ├── overrides/ # replacements for upstream components
|
|
53
93
|
│ ├── plugins/
|
|
94
|
+
│ ├── shared-fields/ # schema fields sections and blocks share
|
|
95
|
+
│ ├── theme/
|
|
54
96
|
│ └── utils/
|
|
55
97
|
├── server/
|
|
56
98
|
│ ├── orchestr/
|
|
57
|
-
│ │ ├── <
|
|
99
|
+
│ │ ├── <entity>/ # *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts, *.page-index.ts
|
|
58
100
|
│ │ └── plugins/
|
|
59
101
|
│ ├── middleware/ # orchestr middleware and the builders handlers use
|
|
60
102
|
│ ├── client/ # API clients, nothing else
|
|
61
|
-
│ ├── api/ # server routes
|
|
103
|
+
│ ├── api/ # server routes, or routes/
|
|
62
104
|
│ ├── plugins/ # Nitro plugins
|
|
63
|
-
│ ├── media-library/
|
|
105
|
+
│ ├── media-library/ # or media-libraries/
|
|
64
106
|
│ └── utils/
|
|
65
|
-
│ └── <domain>/
|
|
66
107
|
└── shared/ # code for both the app and the server
|
|
67
108
|
```
|
|
68
109
|
|
|
69
|
-
A domain is a folder in `server/orchestr/`, together with the `server/utils/` folder of the same name. Other folders in `server/utils/`, like `tracking/`, hold shared helpers that every domain may use.
|
|
70
|
-
|
|
71
110
|
## Baseline
|
|
72
111
|
|
|
73
112
|
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.
|
|
@@ -80,12 +119,12 @@ npx larrylint --baseline
|
|
|
80
119
|
|
|
81
120
|
## Configuration
|
|
82
121
|
|
|
83
|
-
Most apps need none.
|
|
122
|
+
Most apps need none. `heavyPackages` lists the packages that sections, blocks and plugins must load with `import()`, for `heavy-imports`. Set it in your `package.json`:
|
|
84
123
|
|
|
85
124
|
```json
|
|
86
125
|
{
|
|
87
126
|
"larrylint": {
|
|
88
|
-
"
|
|
127
|
+
"heavyPackages": ["leaflet"]
|
|
89
128
|
}
|
|
90
129
|
}
|
|
91
130
|
```
|
|
@@ -96,7 +135,7 @@ Or in a `larrylint.config.ts`:
|
|
|
96
135
|
import { defineLarrylintConfig } from 'larrylint'
|
|
97
136
|
|
|
98
137
|
export default defineLarrylintConfig({
|
|
99
|
-
|
|
138
|
+
heavyPackages: ['leaflet'],
|
|
100
139
|
})
|
|
101
140
|
```
|
|
102
141
|
|
package/dist/_chunks/check.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { relative } from "pathe";
|
|
1
|
+
import { applyBaseline, larrylint } from "./preset.mjs";
|
|
3
2
|
import tsParser from "@typescript-eslint/parser";
|
|
4
|
-
import { ESLint } from "eslint";
|
|
5
3
|
import vueParser from "vue-eslint-parser";
|
|
4
|
+
import { ESLint } from "eslint";
|
|
6
5
|
const PARSERS = [
|
|
7
6
|
{ ignores: ["**/*.d.ts"] },
|
|
8
7
|
{
|
|
@@ -42,35 +41,4 @@ async function check(cwd, options = {}) {
|
|
|
42
41
|
...applyBaseline(cwd, results)
|
|
43
42
|
};
|
|
44
43
|
}
|
|
45
|
-
function applyBaseline(cwd, results) {
|
|
46
|
-
const baseline = readBaseline(cwd);
|
|
47
|
-
const violations = {};
|
|
48
|
-
let baselined = 0;
|
|
49
|
-
let improved = 0;
|
|
50
|
-
const filtered = results.map((result) => {
|
|
51
|
-
const path = relative(cwd, result.filePath);
|
|
52
|
-
const perRule = {};
|
|
53
|
-
const relevant = result.messages.filter((message) => message.fatal || message.ruleId?.startsWith("larrylint/"));
|
|
54
|
-
for (const { ruleId } of relevant) if (ruleId) perRule[ruleId] = (perRule[ruleId] ?? 0) + 1;
|
|
55
|
-
if (Object.keys(perRule).length > 0) violations[path] = perRule;
|
|
56
|
-
const messages = relevant.filter(({ ruleId }) => !ruleId || perRule[ruleId] > (baseline[path]?.[ruleId] ?? 0));
|
|
57
|
-
baselined += relevant.length - messages.length;
|
|
58
|
-
return {
|
|
59
|
-
...result,
|
|
60
|
-
messages,
|
|
61
|
-
errorCount: messages.filter((message) => message.severity === 2).length,
|
|
62
|
-
fatalErrorCount: messages.filter((message) => message.fatal).length,
|
|
63
|
-
warningCount: messages.filter((message) => message.severity === 1).length,
|
|
64
|
-
fixableErrorCount: messages.filter((message) => message.severity === 2 && message.fix).length,
|
|
65
|
-
fixableWarningCount: messages.filter((message) => message.severity === 1 && message.fix).length
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
for (const [path, rules] of Object.entries(baseline)) for (const [rule, allowed] of Object.entries(rules)) if ((violations[path]?.[rule] ?? 0) < allowed) improved++;
|
|
69
|
-
return {
|
|
70
|
-
results: filtered,
|
|
71
|
-
violations,
|
|
72
|
-
baselined,
|
|
73
|
-
improved
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
44
|
export { check };
|
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 { existsSync } from "node:fs";
|
|
6
5
|
import { join, resolve } from "pathe";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
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,13 +2,27 @@ 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 { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
5
|
import { join, relative, resolve } from "pathe";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
8
|
import { consola } from "consola";
|
|
9
9
|
import { addDevDependency, detectPackageManager } from "nypm";
|
|
10
10
|
import { readPackageJSON } from "pkg-types";
|
|
11
11
|
import { parseModule } from "magicast";
|
|
12
|
+
function appendItem(code, container, items, text) {
|
|
13
|
+
const closing = container.end - 1;
|
|
14
|
+
const last = items.at(-1);
|
|
15
|
+
const insert = (at, insertion) => ({
|
|
16
|
+
start: at,
|
|
17
|
+
end: at,
|
|
18
|
+
text: insertion
|
|
19
|
+
});
|
|
20
|
+
if (!last) return insert(closing, text);
|
|
21
|
+
if (!code.slice(container.start, container.end).includes("\n")) return insert(last.end, `, ${text}`);
|
|
22
|
+
const indent = code.slice(code.lastIndexOf("\n", last.start) + 1, last.start).match(/^[ \t]*/)[0];
|
|
23
|
+
const comma = code.indexOf(",", last.end);
|
|
24
|
+
return comma !== -1 && comma < closing ? insert(comma + 1, `\n${indent}${text},`) : insert(last.end, `,\n${indent}${text}`);
|
|
25
|
+
}
|
|
12
26
|
const CONFIG_FILES = [
|
|
13
27
|
"eslint.config.js",
|
|
14
28
|
"eslint.config.mjs",
|
|
@@ -92,20 +106,6 @@ function isComposer(call, imports) {
|
|
|
92
106
|
const factory = node.callee.type === "Identifier" ? imports.find((item) => item.local === node.callee.name) : void 0;
|
|
93
107
|
return factory ? COMPOSERS.test(factory.from) : false;
|
|
94
108
|
}
|
|
95
|
-
function appendItem(code, container, items, text) {
|
|
96
|
-
const closing = container.end - 1;
|
|
97
|
-
const last = items.at(-1);
|
|
98
|
-
const insert = (at, insertion) => ({
|
|
99
|
-
start: at,
|
|
100
|
-
end: at,
|
|
101
|
-
text: insertion
|
|
102
|
-
});
|
|
103
|
-
if (!last) return insert(closing, text);
|
|
104
|
-
if (!code.slice(container.start, container.end).includes("\n")) return insert(last.end, `, ${text}`);
|
|
105
|
-
const indent = code.slice(code.lastIndexOf("\n", last.start) + 1, last.start).match(/^[ \t]*/)[0];
|
|
106
|
-
const comma = code.indexOf(",", last.end);
|
|
107
|
-
return comma !== -1 && comma < closing ? insert(comma + 1, `\n${indent}${text},`) : insert(last.end, `,\n${indent}${text}`);
|
|
108
|
-
}
|
|
109
109
|
var init_default = defineCommand({
|
|
110
110
|
meta: {
|
|
111
111
|
name: "init",
|
package/dist/_chunks/package.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
var name = "larrylint";
|
|
2
|
-
var version = "0.
|
|
3
|
-
var description = "Opinionated
|
|
2
|
+
var version = "0.2.0";
|
|
3
|
+
var description = "Opinionated rules for Laioutr apps, as an ESLint preset and a CLI.";
|
|
4
4
|
export { description, name, version };
|