larrylint 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/_chunks/check.mjs +44 -0
- package/dist/_chunks/check2.mjs +50 -0
- package/dist/_chunks/init.mjs +149 -0
- package/dist/_chunks/package.mjs +4 -0
- package/dist/_chunks/preset.mjs +2101 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +23 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.mjs +2 -0
- package/package.json +58 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Frederik Bußmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# 🦙 larrylint
|
|
2
|
+
|
|
3
|
+
[![Github Actions][github-actions-src]][github-actions-href]
|
|
4
|
+
[![NPM version][npm-version-src]][npm-version-href]
|
|
5
|
+
[![NPM last update][npm-last-update-src]][npm-last-update-href]
|
|
6
|
+
[![License][license-src]][license-href]
|
|
7
|
+
|
|
8
|
+
Opinionated rules for [Laioutr](https://laioutr.com) apps: an ESLint preset for your editor and a CLI for CI.
|
|
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, handlers importing each other, or one business domain reaching into another. It also catches the mistakes that only show up in Studio or in production, like schema fields that never reach the component, links Studio can't follow, or cookies the page cache hands to every visitor.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
Check the Laioutr app in the current folder. No install and no config needed:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx larrylint
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Set it up for good. This installs larrylint, adds its rules to your `eslint.config`, and records existing violations in a baseline so only new code has to follow the rules:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx larrylint init
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
After that, `eslint .` and your editor report larrylint's rules next to your own.
|
|
27
|
+
|
|
28
|
+
`ui-kit-tags` and `token-namespaces` read the app's installed `@laioutr-core/ui-kit`, `@laioutr-core/ui` and `@laioutr-core/canonical-types`, so install the app's dependencies first. larrylint stops with an error when one of them is missing.
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
|
|
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, only handlers and media libraries import middleware, and API clients stay out of handlers and server utils. Utils are the bottom layer, followed by composables, components and sections. Domains stay apart. |
|
|
37
|
+
| `larrylint/known-folders` | Runtime files live in the folders of the [layout](#layout), where the other rules know what to check. |
|
|
38
|
+
| `larrylint/heavy-imports` | Sections, blocks and plugins load the packages in `heavyPackages` with `import()`. Laioutr registers them globally, so a static import lands in the chunk every page loads. |
|
|
39
|
+
| `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. |
|
|
40
|
+
| `larrylint/public-config` | The module doesn't copy its options into `runtimeConfig.public`, which reaches the browser with every token among them. |
|
|
41
|
+
|
|
42
|
+
Type imports are fine across most layers, since they don't end up in the bundle.
|
|
43
|
+
|
|
44
|
+
### Orchestr
|
|
45
|
+
|
|
46
|
+
| Rule | Description |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `larrylint/orchestr-files` | Laioutr loads every file in `orchestr/` as a server plugin, so only handler files belong there. |
|
|
49
|
+
| `larrylint/orchestr-cookies` | Handlers and middleware don't set cookies or redirect, since the page cache replays the response to every visitor. |
|
|
50
|
+
| `larrylint/handler-domains` | Handlers live in a domain folder, e.g. `orchestr/<domain>/`. |
|
|
51
|
+
| `larrylint/handler-exports` | Handler files export their handler as default and nothing else. |
|
|
52
|
+
| `larrylint/handler-context` | Handlers take config and clients from the middleware context and the request from their arguments, not from `useRuntimeConfig()` or `useEvent()`. |
|
|
53
|
+
| `larrylint/resolver-undefined` | Component resolvers resolve fields to empty values like `''`, not `undefined` or `null`, which Studio shows as "...". |
|
|
54
|
+
| `larrylint/resolver-passthrough` | Component resolvers use `passthrough.get()`: `require()` fails the whole resolver when another query didn't set the value. |
|
|
55
|
+
| `larrylint/action-errors` | Actions return client errors as a status in their output. A thrown 4xx arrives as a failed action, since the action transport drops status codes. |
|
|
56
|
+
| `larrylint/middleware-files` | `server/middleware/` only holds orchestr and Nitro middleware; helpers go to `server/utils/`. |
|
|
57
|
+
| `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. |
|
|
58
|
+
| `larrylint/orchestr-meta` | `defineOrchestr.meta({ app })` is the package name, which traces use to tell which app's middleware ran. |
|
|
59
|
+
| `larrylint/token-namespaces` | Your own tokens stay out of the namespaces of Laioutr's canonical types, like `ecommerce/`, where registration order decides which handler wins. |
|
|
60
|
+
| `larrylint/token-nullable` | Entity component tokens have no nullable top-level fields, which Studio can't bind. |
|
|
61
|
+
|
|
62
|
+
### Sections and blocks
|
|
63
|
+
|
|
64
|
+
| Rule | Description |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `larrylint/definition-folder` | `defineSection()` lives in `app/sections/` and `defineBlock()` in `app/blocks/`, and every `.vue` there has one. |
|
|
67
|
+
| `larrylint/definition-prefix` | Sections are named `Section*.vue`, blocks `Block*.vue`. |
|
|
68
|
+
| `larrylint/component-name` | The `component` of a definition matches its file name. |
|
|
69
|
+
| `larrylint/definition-description` | Definitions have a `studio.description`, which Studio shows in its section and block picker. |
|
|
70
|
+
| `larrylint/root-id` | The root element has no `id`: frontend-core replaces it with its own. |
|
|
71
|
+
| `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. |
|
|
72
|
+
| `larrylint/slot-children` | Nothing counts slot children with `.length`: Studio passes all blocks of a slot as one Fragment. |
|
|
73
|
+
| `larrylint/reserved-field-names` | No top-level schema fields named `style`, `class`, `key`, `ref`, `is`, `slot`, `refFor` or `refKey`, which Vue swallows before they reach the component. |
|
|
74
|
+
| `larrylint/field-name-case` | Top-level schema fields are camelCase, since they become props. |
|
|
75
|
+
| `larrylint/required-fields` | Schema fields have no `required`, which Studio ignores. Give them a `default` instead. |
|
|
76
|
+
| `larrylint/schema-groups` | Schema groups are Studio's panels, in the order Content, Design, Rules. |
|
|
77
|
+
| `larrylint/unused-fields` | The component reads every schema field it defines, so editors don't get fields that do nothing. |
|
|
78
|
+
| `larrylint/dead-fallbacks` | No `??` fallbacks or `undefined` checks on props that are never `undefined`: unset checkbox, select, radio and toggle fields arrive as `false`, unset text fields as `''`. |
|
|
79
|
+
|
|
80
|
+
### Frontend
|
|
81
|
+
|
|
82
|
+
| Rule | Description |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `larrylint/button-type` | No `type` on the ui-kit button: it always renders its `button-type` prop, so `type="submit"` silently renders a dead button. Autofixable. |
|
|
85
|
+
| `larrylint/ui-kit-tags` | `<l-*>` tags are ui-kit or ui components. Anything else renders nothing. |
|
|
86
|
+
| `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. |
|
|
87
|
+
| `larrylint/orchestr-store` | Components read orchestr data through composables, not `useOrchestrStore()`. |
|
|
88
|
+
| `larrylint/resolve-result` | Nothing tests the result of `linkResolver.resolve()`, which is never empty: a link it can't resolve comes back as a `#missing-required-params…` string. |
|
|
89
|
+
| `larrylint/hand-built-links` | Internal links come from `linkResolver`, not from paths like `` `/hotels/${slug}` ``, which break when a page type's route changes. |
|
|
90
|
+
| `larrylint/internal-anchors` | Internal links use `<NuxtLink>`: a plain `<a>` reloads the page and breaks Studio's navigation sync. |
|
|
91
|
+
|
|
92
|
+
### Canonical types
|
|
93
|
+
|
|
94
|
+
| Rule | What it checks |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `larrylint/money` | Money amounts are integers in minor units, e.g. `1999` for 19.99, and currencies are ISO 4217 codes like `EUR`. |
|
|
97
|
+
|
|
98
|
+
## Layout
|
|
99
|
+
|
|
100
|
+
larrylint expects the layout of Laioutr's [app starter](https://github.com/laioutr/app-starter):
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
src/
|
|
104
|
+
├── module.ts # build time
|
|
105
|
+
└── runtime/
|
|
106
|
+
├── app/
|
|
107
|
+
│ ├── sections/ # Section*.vue with defineSection()
|
|
108
|
+
│ ├── blocks/ # Block*.vue with defineBlock()
|
|
109
|
+
│ ├── components/
|
|
110
|
+
│ ├── composables/
|
|
111
|
+
│ ├── overrides/ # replacements for upstream components
|
|
112
|
+
│ ├── plugins/
|
|
113
|
+
│ ├── shared-fields/ # schema fields sections and blocks share
|
|
114
|
+
│ ├── theme/
|
|
115
|
+
│ └── utils/
|
|
116
|
+
├── server/
|
|
117
|
+
│ ├── orchestr/
|
|
118
|
+
│ │ ├── <domain>/ # *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts, *.page-index.ts
|
|
119
|
+
│ │ └── plugins/
|
|
120
|
+
│ ├── middleware/ # orchestr middleware and the builders handlers use
|
|
121
|
+
│ ├── client/ # API clients, nothing else
|
|
122
|
+
│ ├── api/ # server routes, or routes/
|
|
123
|
+
│ ├── plugins/ # Nitro plugins
|
|
124
|
+
│ ├── media-library/ # or media-libraries/
|
|
125
|
+
│ └── utils/
|
|
126
|
+
│ └── <domain>/
|
|
127
|
+
└── shared/ # code for both the app and the server
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
## Baseline
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
After fixing old violations, shrink the baseline:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
npx larrylint --baseline
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
Most apps need none. Two options tune the rules:
|
|
145
|
+
|
|
146
|
+
- `sharedDomains`: domains every other domain may import, for `layers`.
|
|
147
|
+
- `heavyPackages`: packages that sections, blocks and plugins must load with `import()`, for `heavy-imports`.
|
|
148
|
+
|
|
149
|
+
Set them in your `package.json`:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"larrylint": {
|
|
154
|
+
"sharedDomains": ["product"],
|
|
155
|
+
"heavyPackages": ["leaflet"]
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Or in a `larrylint.config.ts`:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { defineLarrylintConfig } from 'larrylint'
|
|
164
|
+
|
|
165
|
+
export default defineLarrylintConfig({
|
|
166
|
+
sharedDomains: ['product'],
|
|
167
|
+
heavyPackages: ['leaflet'],
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## ESLint
|
|
172
|
+
|
|
173
|
+
`larrylint init` adds the rules to your `eslint.config` for you. To do it by hand with `@nuxt/eslint-config` or `@antfu/eslint-config`:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
|
|
177
|
+
import larrylint from 'larrylint'
|
|
178
|
+
|
|
179
|
+
export default createConfigForNuxt().append(larrylint())
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With a plain array, like `@laioutr/eslint-config`:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
import config from '@laioutr/eslint-config/nuxt-module'
|
|
186
|
+
import larrylint from 'larrylint'
|
|
187
|
+
|
|
188
|
+
export default [...config, ...(await larrylint())]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The preset only brings rules, no parsers or style rules, so it runs on top of whatever your config already uses.
|
|
192
|
+
|
|
193
|
+
## CLI
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
larrylint [--cwd <folder>] [--fix] [--baseline]
|
|
197
|
+
larrylint init [--cwd <folder>]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `--fix` fixes what can be fixed automatically.
|
|
201
|
+
- `--baseline` records the current violations in `larrylint-baseline.json`.
|
|
202
|
+
|
|
203
|
+
The CLI only runs larrylint's rules, independent of your ESLint setup, and exits with code 1 on new violations.
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
Published under the [MIT License](https://github.com/bussmann-io/larrylint/tree/main/LICENSE).
|
|
208
|
+
|
|
209
|
+
[github-actions-src]: https://github.com/bussmann-io/larrylint/actions/workflows/test.yml/badge.svg
|
|
210
|
+
[github-actions-href]: https://github.com/bussmann-io/larrylint/actions
|
|
211
|
+
|
|
212
|
+
[npm-version-src]: https://img.shields.io/npm/v/larrylint/latest.svg?style=flat&colorA=18181B&colorB=31C553
|
|
213
|
+
[npm-version-href]: https://npmjs.com/package/larrylint
|
|
214
|
+
|
|
215
|
+
[npm-last-update-src]: https://img.shields.io/npm/last-update/larrylint.svg?style=flat&colorA=18181B&colorB=31C553
|
|
216
|
+
[npm-last-update-href]: https://npmjs.com/package/larrylint
|
|
217
|
+
|
|
218
|
+
[license-src]: https://img.shields.io/github/license/bussmann-io/larrylint.svg?style=flat&colorA=18181B&colorB=31C553
|
|
219
|
+
[license-href]: https://github.com/bussmann-io/larrylint/tree/main/LICENSE
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { applyBaseline, larrylint } from "./preset.mjs";
|
|
2
|
+
import tsParser from "@typescript-eslint/parser";
|
|
3
|
+
import vueParser from "vue-eslint-parser";
|
|
4
|
+
import { ESLint } from "eslint";
|
|
5
|
+
const PARSERS = [
|
|
6
|
+
{ ignores: ["**/*.d.ts"] },
|
|
7
|
+
{
|
|
8
|
+
files: ["**/*.{ts,mts,cts}"],
|
|
9
|
+
languageOptions: { parser: tsParser }
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
files: ["**/*.vue"],
|
|
13
|
+
languageOptions: {
|
|
14
|
+
parser: vueParser,
|
|
15
|
+
parserOptions: {
|
|
16
|
+
parser: tsParser,
|
|
17
|
+
extraFileExtensions: [".vue"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
async function check(cwd, options = {}) {
|
|
23
|
+
const eslint = new ESLint({
|
|
24
|
+
cwd,
|
|
25
|
+
overrideConfigFile: true,
|
|
26
|
+
overrideConfig: [
|
|
27
|
+
...PARSERS,
|
|
28
|
+
...await larrylint({ cwd }),
|
|
29
|
+
{
|
|
30
|
+
linterOptions: { reportUnusedDisableDirectives: "off" },
|
|
31
|
+
settings: { larrylint: { baseline: false } }
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
fix: options.fix,
|
|
35
|
+
errorOnUnmatchedPattern: false
|
|
36
|
+
});
|
|
37
|
+
const results = await eslint.lintFiles(["src"]);
|
|
38
|
+
if (options.fix) await ESLint.outputFixes(results);
|
|
39
|
+
return {
|
|
40
|
+
eslint,
|
|
41
|
+
...applyBaseline(cwd, results)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export { check };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { BASELINE_FILE, countViolations, writeBaseline } from "./preset.mjs";
|
|
2
|
+
import { cwdArgs } from "../cli/index.mjs";
|
|
3
|
+
import { check } from "./check.mjs";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { join, resolve } from "pathe";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import { consola } from "consola";
|
|
9
|
+
var check_default = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: "check",
|
|
12
|
+
description: "Check the Laioutr app in the current folder."
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
...cwdArgs,
|
|
16
|
+
fix: {
|
|
17
|
+
type: "boolean",
|
|
18
|
+
description: "Fix what can be fixed automatically."
|
|
19
|
+
},
|
|
20
|
+
baseline: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
description: `Record all current violations in ${BASELINE_FILE}, so only new ones fail.`
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
run: async ({ args }) => {
|
|
26
|
+
const cwd = resolve(args.cwd);
|
|
27
|
+
if (!existsSync(join(cwd, "src/runtime"))) {
|
|
28
|
+
consola.warn(`No src/runtime/ in ${cwd}. larrylint checks Laioutr apps, run it in the app's folder.`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const { eslint, results, violations, baselined, improved } = await check(cwd, { fix: args.fix });
|
|
33
|
+
if (args.baseline) {
|
|
34
|
+
writeBaseline(cwd, violations);
|
|
35
|
+
const total = countViolations(violations);
|
|
36
|
+
consola.success(total > 0 ? `Recorded ${total} violations in ${Object.keys(violations).length} files in ${BASELINE_FILE}.` : "No violations, nothing to record.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const output = await (await eslint.loadFormatter("stylish")).format(results);
|
|
40
|
+
if (output) process.stdout.write(`${output}\n`);
|
|
41
|
+
if (baselined > 0) consola.info(`${baselined} known violations are recorded in ${BASELINE_FILE}.`);
|
|
42
|
+
if (improved > 0) consola.info(`${improved} baseline entries have fewer violations now. Run larrylint --baseline to lock that in.`);
|
|
43
|
+
if (results.some((result) => result.errorCount > 0)) {
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
consola.success(baselined > 0 ? "No new violations." : "No violations.");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
export { check_default as default };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { BASELINE_FILE, countViolations, writeBaseline } from "./preset.mjs";
|
|
2
|
+
import { cwdArgs } from "../cli/index.mjs";
|
|
3
|
+
import { check } from "./check.mjs";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { join, relative, resolve } from "pathe";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import { consola } from "consola";
|
|
9
|
+
import { addDevDependency, detectPackageManager } from "nypm";
|
|
10
|
+
import { readPackageJSON } from "pkg-types";
|
|
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
|
+
}
|
|
26
|
+
const CONFIG_FILES = [
|
|
27
|
+
"eslint.config.js",
|
|
28
|
+
"eslint.config.mjs",
|
|
29
|
+
"eslint.config.cjs",
|
|
30
|
+
"eslint.config.ts",
|
|
31
|
+
"eslint.config.mts",
|
|
32
|
+
"eslint.config.cts"
|
|
33
|
+
];
|
|
34
|
+
const COMPOSERS = /^(?:@nuxt\/eslint-config(?:\/flat)?|@antfu\/eslint-config|eslint-flat-config-utils)$|\/\.nuxt\/eslint\.config\.mjs$/;
|
|
35
|
+
const CONFIG_FUNCTIONS = /* @__PURE__ */ new Set(["defineConfig", "extendConfig"]);
|
|
36
|
+
const NEW_ESLINT_CONFIG = `import larrylint from 'larrylint'\n\nexport default larrylint()\n`;
|
|
37
|
+
const MANUAL_SNIPPET = `import larrylint from 'larrylint'
|
|
38
|
+
|
|
39
|
+
export default [
|
|
40
|
+
// ...your config
|
|
41
|
+
...(await larrylint()),
|
|
42
|
+
]`;
|
|
43
|
+
function wireEslintConfig(cwd) {
|
|
44
|
+
const file = CONFIG_FILES.map((name) => join(cwd, name)).find((path) => existsSync(path));
|
|
45
|
+
if (!file) {
|
|
46
|
+
const created = join(cwd, "eslint.config.mjs");
|
|
47
|
+
writeFileSync(created, NEW_ESLINT_CONFIG);
|
|
48
|
+
return {
|
|
49
|
+
status: "created",
|
|
50
|
+
file: created
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const result = addLarrylint(readFileSync(file, "utf8"));
|
|
54
|
+
if (typeof result === "string") {
|
|
55
|
+
writeFileSync(file, result);
|
|
56
|
+
return {
|
|
57
|
+
status: "updated",
|
|
58
|
+
file
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
status: result,
|
|
63
|
+
file
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function addLarrylint(code) {
|
|
67
|
+
const mod = parseModule(code);
|
|
68
|
+
const imports = Object.values(mod.imports);
|
|
69
|
+
if (imports.some((item) => item.from === "larrylint")) return "present";
|
|
70
|
+
const body = mod.$ast.body;
|
|
71
|
+
const exported = body.find((node) => node.type === "ExportDefaultDeclaration")?.declaration;
|
|
72
|
+
let edit;
|
|
73
|
+
if (exported?.type === "ArrayExpression") edit = appendItem(code, exported, exported.elements, "...(await larrylint())");
|
|
74
|
+
else if (exported?.type === "CallExpression" && isComposer(exported, imports)) {
|
|
75
|
+
const indent = code.slice(exported.start, exported.end).match(/\n([ \t]*)\.[a-z]/i)?.[1];
|
|
76
|
+
edit = {
|
|
77
|
+
start: exported.end,
|
|
78
|
+
end: exported.end,
|
|
79
|
+
text: `${indent === void 0 ? "" : `\n${indent}`}.append(larrylint())`
|
|
80
|
+
};
|
|
81
|
+
} else if (exported?.type === "CallExpression" && exported.callee.type === "Identifier" && CONFIG_FUNCTIONS.has(exported.callee.name)) edit = appendItem(code, exported, exported.arguments, "await larrylint()");
|
|
82
|
+
else if (exported?.type === "Identifier") edit = extendBinding(body, exported, imports);
|
|
83
|
+
if (!edit) return "unknown";
|
|
84
|
+
const lastImport = body.filter((node) => node.type === "ImportDeclaration").at(-1);
|
|
85
|
+
const importLine = `import larrylint from 'larrylint'${(lastImport ? code.slice(lastImport.start, lastImport.end).endsWith(";") : false) ? ";" : ""}`;
|
|
86
|
+
let result = `${code.slice(0, edit.start)}${edit.text}${code.slice(edit.end)}`;
|
|
87
|
+
result = lastImport ? `${result.slice(0, lastImport.end)}\n${importLine}${result.slice(lastImport.end)}` : `${importLine}\n\n${result}`;
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
function extendBinding(body, identifier, imports) {
|
|
91
|
+
const imported = imports.find((item) => item.local === identifier.name);
|
|
92
|
+
const declared = body.flatMap((node) => node.type === "VariableDeclaration" ? node.declarations : []).find((declarator) => declarator.id.name === identifier.name);
|
|
93
|
+
return (imported ? COMPOSERS.test(imported.from) : declared?.init?.type === "CallExpression" && isComposer(declared.init, imports)) ? {
|
|
94
|
+
start: identifier.end,
|
|
95
|
+
end: identifier.end,
|
|
96
|
+
text: ".append(larrylint())"
|
|
97
|
+
} : {
|
|
98
|
+
start: identifier.start,
|
|
99
|
+
end: identifier.end,
|
|
100
|
+
text: `[...${identifier.name}, ...(await larrylint())]`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function isComposer(call, imports) {
|
|
104
|
+
let node = call;
|
|
105
|
+
while (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") node = node.callee.object;
|
|
106
|
+
const factory = node.callee.type === "Identifier" ? imports.find((item) => item.local === node.callee.name) : void 0;
|
|
107
|
+
return factory ? COMPOSERS.test(factory.from) : false;
|
|
108
|
+
}
|
|
109
|
+
var init_default = defineCommand({
|
|
110
|
+
meta: {
|
|
111
|
+
name: "init",
|
|
112
|
+
description: "Set up larrylint in the Laioutr app in the current folder."
|
|
113
|
+
},
|
|
114
|
+
args: cwdArgs,
|
|
115
|
+
run: async ({ args }) => {
|
|
116
|
+
const cwd = resolve(args.cwd);
|
|
117
|
+
if (!existsSync(join(cwd, "package.json")) || !existsSync(join(cwd, "src/runtime"))) {
|
|
118
|
+
consola.error(`${cwd} doesn't look like a Laioutr app: it needs a package.json and a src/runtime/ folder.`);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const pkg = await readPackageJSON(cwd);
|
|
123
|
+
if (pkg.dependencies?.larrylint || pkg.devDependencies?.larrylint) consola.info("larrylint is already installed.");
|
|
124
|
+
else {
|
|
125
|
+
const packageManager = await detectPackageManager(cwd);
|
|
126
|
+
const workspace = packageManager?.name === "pnpm" ? existsSync(join(cwd, "pnpm-workspace.yaml")) : packageManager?.name === "yarn" && Boolean(pkg.workspaces);
|
|
127
|
+
consola.start("Installing larrylint...");
|
|
128
|
+
await addDevDependency("larrylint", {
|
|
129
|
+
cwd,
|
|
130
|
+
packageManager,
|
|
131
|
+
workspace,
|
|
132
|
+
silent: true
|
|
133
|
+
});
|
|
134
|
+
consola.success("Installed larrylint.");
|
|
135
|
+
}
|
|
136
|
+
const wired = wireEslintConfig(cwd);
|
|
137
|
+
const file = relative(cwd, wired.file);
|
|
138
|
+
if (wired.status === "created") consola.success(`Created ${file} with the larrylint rules.`);
|
|
139
|
+
else if (wired.status === "updated") consola.success(`Added the larrylint rules to ${file}.`);
|
|
140
|
+
else if (wired.status === "present") consola.info(`${file} already uses larrylint.`);
|
|
141
|
+
else consola.warn(`Couldn't add larrylint to ${file} automatically. Add it like this:\n\n${MANUAL_SNIPPET}\n`);
|
|
142
|
+
const { violations } = await check(cwd);
|
|
143
|
+
writeBaseline(cwd, violations);
|
|
144
|
+
const total = countViolations(violations);
|
|
145
|
+
if (total > 0) consola.info(`Recorded ${total} existing violations in ${Object.keys(violations).length} files in ${BASELINE_FILE}. New code has to follow the rules; run larrylint --baseline after fixing old violations.`);
|
|
146
|
+
else consola.success("No violations found.");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
export { init_default as default };
|