larrylint 0.0.1 β†’ 0.1.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/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,151 @@
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 structure 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.
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
+ ## Rules
29
+
30
+ | Rule | What it checks |
31
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
+ | `larrylint/layers` | App code doesn't import server code and vice versa; shared code imports neither. Runtime code doesn't import build-time code. Nothing imports orchestr handlers. Handlers and server utils don't import from `server/client/`, and only handlers and media libraries import middleware. Utils are the bottom layer. Domains stay apart. |
33
+ | `larrylint/orchestr-files` | Laioutr loads every file in `orchestr/` as a server plugin, so only handler files belong there, in a domain folder, exporting nothing but their handler. |
34
+ | `larrylint/definitions` | `defineSection()` lives in `app/sections/Section*.vue`, `defineBlock()` in `app/blocks/Block*.vue`, with a `component` name that matches the file and no top-level schema fields named `style`, `class`, `key`, `ref`, `is`, `slot`, `refFor` or `refKey`, which Vue swallows before they reach the component. |
35
+ | `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. |
36
+
37
+ Type imports are fine across most layers, since they don't end up in the bundle.
38
+
39
+ ## Layout
40
+
41
+ larrylint expects the layout of Laioutr's [app starter](https://github.com/laioutr/app-starter):
42
+
43
+ ```
44
+ src/
45
+ β”œβ”€β”€ module.ts # build time
46
+ └── runtime/
47
+ β”œβ”€β”€ app/
48
+ β”‚ β”œβ”€β”€ sections/ # Section*.vue with defineSection()
49
+ β”‚ β”œβ”€β”€ blocks/ # Block*.vue with defineBlock()
50
+ β”‚ β”œβ”€β”€ components/
51
+ β”‚ β”œβ”€β”€ composables/
52
+ β”‚ β”œβ”€β”€ overrides/ # replacements for upstream components
53
+ β”‚ β”œβ”€β”€ plugins/
54
+ β”‚ └── utils/
55
+ β”œβ”€β”€ server/
56
+ β”‚ β”œβ”€β”€ orchestr/
57
+ β”‚ β”‚ β”œβ”€β”€ <domain>/ # *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts, *.page-index.ts
58
+ β”‚ β”‚ └── plugins/
59
+ β”‚ β”œβ”€β”€ middleware/ # orchestr middleware and the builders handlers use
60
+ β”‚ β”œβ”€β”€ client/ # API clients, nothing else
61
+ β”‚ β”œβ”€β”€ api/ # server routes
62
+ β”‚ β”œβ”€β”€ plugins/ # Nitro plugins
63
+ β”‚ β”œβ”€β”€ media-library/
64
+ β”‚ └── utils/
65
+ β”‚ └── <domain>/
66
+ └── shared/ # code for both the app and the server
67
+ ```
68
+
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
+ ## Baseline
72
+
73
+ 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.
74
+
75
+ After fixing old violations, shrink the baseline:
76
+
77
+ ```sh
78
+ npx larrylint --baseline
79
+ ```
80
+
81
+ ## Configuration
82
+
83
+ Most apps need none. To let every domain import a domain, add it to `sharedDomains` in your `package.json`:
84
+
85
+ ```json
86
+ {
87
+ "larrylint": {
88
+ "sharedDomains": ["product"]
89
+ }
90
+ }
91
+ ```
92
+
93
+ Or in a `larrylint.config.ts`:
94
+
95
+ ```ts
96
+ import { defineLarrylintConfig } from 'larrylint'
97
+
98
+ export default defineLarrylintConfig({
99
+ sharedDomains: ['product'],
100
+ })
101
+ ```
102
+
103
+ ## ESLint
104
+
105
+ `larrylint init` adds the rules to your `eslint.config` for you. To do it by hand with `@nuxt/eslint-config` or `@antfu/eslint-config`:
106
+
107
+ ```js
108
+ import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
109
+ import larrylint from 'larrylint'
110
+
111
+ export default createConfigForNuxt().append(larrylint())
112
+ ```
113
+
114
+ With a plain array, like `@laioutr/eslint-config`:
115
+
116
+ ```js
117
+ import config from '@laioutr/eslint-config/nuxt-module'
118
+ import larrylint from 'larrylint'
119
+
120
+ export default [...config, ...(await larrylint())]
121
+ ```
122
+
123
+ The preset only brings rules, no parsers or style rules, so it runs on top of whatever your config already uses.
124
+
125
+ ## CLI
126
+
127
+ ```sh
128
+ larrylint [--cwd <folder>] [--fix] [--baseline]
129
+ larrylint init [--cwd <folder>]
130
+ ```
131
+
132
+ - `--fix` fixes what can be fixed automatically.
133
+ - `--baseline` records the current violations in `larrylint-baseline.json`.
134
+
135
+ The CLI only runs larrylint's rules, independent of your ESLint setup, and exits with code 1 on new violations.
136
+
137
+ ## License
138
+
139
+ Published under the [MIT License](https://github.com/bussmann-io/larrylint/tree/main/LICENSE).
140
+
141
+ [github-actions-src]: https://github.com/bussmann-io/larrylint/actions/workflows/test.yml/badge.svg
142
+ [github-actions-href]: https://github.com/bussmann-io/larrylint/actions
143
+
144
+ [npm-version-src]: https://img.shields.io/npm/v/larrylint/latest.svg?style=flat&colorA=18181B&colorB=31C553
145
+ [npm-version-href]: https://npmjs.com/package/larrylint
146
+
147
+ [npm-last-update-src]: https://img.shields.io/npm/last-update/larrylint.svg?style=flat&colorA=18181B&colorB=31C553
148
+ [npm-last-update-href]: https://npmjs.com/package/larrylint
149
+
150
+ [license-src]: https://img.shields.io/github/license/bussmann-io/larrylint.svg?style=flat&colorA=18181B&colorB=31C553
151
+ [license-href]: https://github.com/bussmann-io/larrylint/tree/main/LICENSE
@@ -0,0 +1,76 @@
1
+ import { larrylint, readBaseline } from "./preset.mjs";
2
+ import { relative } from "pathe";
3
+ import tsParser from "@typescript-eslint/parser";
4
+ import { ESLint } from "eslint";
5
+ import vueParser from "vue-eslint-parser";
6
+ const PARSERS = [
7
+ { ignores: ["**/*.d.ts"] },
8
+ {
9
+ files: ["**/*.{ts,mts,cts}"],
10
+ languageOptions: { parser: tsParser }
11
+ },
12
+ {
13
+ files: ["**/*.vue"],
14
+ languageOptions: {
15
+ parser: vueParser,
16
+ parserOptions: {
17
+ parser: tsParser,
18
+ extraFileExtensions: [".vue"]
19
+ }
20
+ }
21
+ }
22
+ ];
23
+ async function check(cwd, options = {}) {
24
+ const eslint = new ESLint({
25
+ cwd,
26
+ overrideConfigFile: true,
27
+ overrideConfig: [
28
+ ...PARSERS,
29
+ ...await larrylint({ cwd }),
30
+ {
31
+ linterOptions: { reportUnusedDisableDirectives: "off" },
32
+ settings: { larrylint: { baseline: false } }
33
+ }
34
+ ],
35
+ fix: options.fix,
36
+ errorOnUnmatchedPattern: false
37
+ });
38
+ const results = await eslint.lintFiles(["src"]);
39
+ if (options.fix) await ESLint.outputFixes(results);
40
+ return {
41
+ eslint,
42
+ ...applyBaseline(cwd, results)
43
+ };
44
+ }
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
+ 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 { existsSync } from "node:fs";
6
+ import { join, resolve } from "pathe";
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 { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { join, relative, resolve } from "pathe";
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
+ const CONFIG_FILES = [
13
+ "eslint.config.js",
14
+ "eslint.config.mjs",
15
+ "eslint.config.cjs",
16
+ "eslint.config.ts",
17
+ "eslint.config.mts",
18
+ "eslint.config.cts"
19
+ ];
20
+ const COMPOSERS = /^(?:@nuxt\/eslint-config(?:\/flat)?|@antfu\/eslint-config|eslint-flat-config-utils)$|\/\.nuxt\/eslint\.config\.mjs$/;
21
+ const CONFIG_FUNCTIONS = /* @__PURE__ */ new Set(["defineConfig", "extendConfig"]);
22
+ const NEW_ESLINT_CONFIG = `import larrylint from 'larrylint'\n\nexport default larrylint()\n`;
23
+ const MANUAL_SNIPPET = `import larrylint from 'larrylint'
24
+
25
+ export default [
26
+ // ...your config
27
+ ...(await larrylint()),
28
+ ]`;
29
+ function wireEslintConfig(cwd) {
30
+ const file = CONFIG_FILES.map((name) => join(cwd, name)).find((path) => existsSync(path));
31
+ if (!file) {
32
+ const created = join(cwd, "eslint.config.mjs");
33
+ writeFileSync(created, NEW_ESLINT_CONFIG);
34
+ return {
35
+ status: "created",
36
+ file: created
37
+ };
38
+ }
39
+ const result = addLarrylint(readFileSync(file, "utf8"));
40
+ if (typeof result === "string") {
41
+ writeFileSync(file, result);
42
+ return {
43
+ status: "updated",
44
+ file
45
+ };
46
+ }
47
+ return {
48
+ status: result,
49
+ file
50
+ };
51
+ }
52
+ function addLarrylint(code) {
53
+ const mod = parseModule(code);
54
+ const imports = Object.values(mod.imports);
55
+ if (imports.some((item) => item.from === "larrylint")) return "present";
56
+ const body = mod.$ast.body;
57
+ const exported = body.find((node) => node.type === "ExportDefaultDeclaration")?.declaration;
58
+ let edit;
59
+ if (exported?.type === "ArrayExpression") edit = appendItem(code, exported, exported.elements, "...(await larrylint())");
60
+ else if (exported?.type === "CallExpression" && isComposer(exported, imports)) {
61
+ const indent = code.slice(exported.start, exported.end).match(/\n([ \t]*)\.[a-z]/i)?.[1];
62
+ edit = {
63
+ start: exported.end,
64
+ end: exported.end,
65
+ text: `${indent === void 0 ? "" : `\n${indent}`}.append(larrylint())`
66
+ };
67
+ } else if (exported?.type === "CallExpression" && exported.callee.type === "Identifier" && CONFIG_FUNCTIONS.has(exported.callee.name)) edit = appendItem(code, exported, exported.arguments, "await larrylint()");
68
+ else if (exported?.type === "Identifier") edit = extendBinding(body, exported, imports);
69
+ if (!edit) return "unknown";
70
+ const lastImport = body.filter((node) => node.type === "ImportDeclaration").at(-1);
71
+ const importLine = `import larrylint from 'larrylint'${(lastImport ? code.slice(lastImport.start, lastImport.end).endsWith(";") : false) ? ";" : ""}`;
72
+ let result = `${code.slice(0, edit.start)}${edit.text}${code.slice(edit.end)}`;
73
+ result = lastImport ? `${result.slice(0, lastImport.end)}\n${importLine}${result.slice(lastImport.end)}` : `${importLine}\n\n${result}`;
74
+ return result;
75
+ }
76
+ function extendBinding(body, identifier, imports) {
77
+ const imported = imports.find((item) => item.local === identifier.name);
78
+ const declared = body.flatMap((node) => node.type === "VariableDeclaration" ? node.declarations : []).find((declarator) => declarator.id.name === identifier.name);
79
+ return (imported ? COMPOSERS.test(imported.from) : declared?.init?.type === "CallExpression" && isComposer(declared.init, imports)) ? {
80
+ start: identifier.end,
81
+ end: identifier.end,
82
+ text: ".append(larrylint())"
83
+ } : {
84
+ start: identifier.start,
85
+ end: identifier.end,
86
+ text: `[...${identifier.name}, ...(await larrylint())]`
87
+ };
88
+ }
89
+ function isComposer(call, imports) {
90
+ let node = call;
91
+ while (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") node = node.callee.object;
92
+ const factory = node.callee.type === "Identifier" ? imports.find((item) => item.local === node.callee.name) : void 0;
93
+ return factory ? COMPOSERS.test(factory.from) : false;
94
+ }
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
+ 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 };
@@ -0,0 +1,4 @@
1
+ var name = "larrylint";
2
+ var version = "0.1.0";
3
+ var description = "Opinionated structure rules for Laioutr apps, as an ESLint preset and a CLI.";
4
+ export { description, name, version };
@@ -0,0 +1,586 @@
1
+ import { name, version } from "./package.mjs";
2
+ import process from "node:process";
3
+ import { loadConfig } from "c12";
4
+ import { readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { basename, dirname, join, normalize, resolve } from "pathe";
6
+ function defineLarrylintConfig(config) {
7
+ return config;
8
+ }
9
+ async function loadLarrylintConfig(cwd = process.cwd()) {
10
+ const { config } = await loadConfig({
11
+ name: "larrylint",
12
+ cwd,
13
+ packageJson: true,
14
+ rcFile: false,
15
+ globalRc: false,
16
+ dotenv: false
17
+ });
18
+ return { sharedDomains: config.sharedDomains ?? [] };
19
+ }
20
+ const BASELINE_FILE = "larrylint-baseline.json";
21
+ const cache = /* @__PURE__ */ new Map();
22
+ function readBaseline(root) {
23
+ const file = join(root, BASELINE_FILE);
24
+ let mtimeMs;
25
+ try {
26
+ mtimeMs = statSync(file).mtimeMs;
27
+ } catch {
28
+ return {};
29
+ }
30
+ const cached = cache.get(file);
31
+ if (cached?.mtimeMs === mtimeMs) return cached.baseline;
32
+ const baseline = JSON.parse(readFileSync(file, "utf8"));
33
+ cache.set(file, {
34
+ mtimeMs,
35
+ baseline
36
+ });
37
+ return baseline;
38
+ }
39
+ function writeBaseline(root, baseline) {
40
+ const file = join(root, BASELINE_FILE);
41
+ const files = Object.keys(baseline).sort();
42
+ if (files.length === 0) {
43
+ rmSync(file, { force: true });
44
+ return;
45
+ }
46
+ const sorted = {};
47
+ for (const path of files) sorted[path] = Object.fromEntries(Object.entries(baseline[path]).sort(([a], [b]) => a.localeCompare(b)));
48
+ writeFileSync(file, `${JSON.stringify(sorted, null, 2)}\n`);
49
+ }
50
+ function countViolations(baseline) {
51
+ return Object.values(baseline).flatMap((rules) => Object.values(rules)).reduce((sum, count) => sum + count, 0);
52
+ }
53
+ function createReporter(context, file) {
54
+ const reports = [];
55
+ return {
56
+ report: (descriptor) => {
57
+ reports.push(descriptor);
58
+ },
59
+ flush: () => {
60
+ const settings = context.settings.larrylint;
61
+ const allowed = file && settings?.baseline !== false ? readBaseline(file.root)[file.path]?.[context.id] ?? 0 : 0;
62
+ if (reports.length > allowed) for (const descriptor of reports) context.report(descriptor);
63
+ reports.length = 0;
64
+ }
65
+ };
66
+ }
67
+ const directories = /* @__PURE__ */ new Map();
68
+ function isDirectory(path) {
69
+ let result = directories.get(path);
70
+ if (result === void 0) {
71
+ try {
72
+ result = statSync(path).isDirectory();
73
+ } catch {
74
+ result = false;
75
+ }
76
+ directories.set(path, result);
77
+ }
78
+ return result;
79
+ }
80
+ function resolveImport(importer, source) {
81
+ if (!source.startsWith("./") && !source.startsWith("../")) return;
82
+ const target = resolve(dirname(importer), source.split("?")[0]);
83
+ return isDirectory(target) ? `${target}/index` : target;
84
+ }
85
+ const HANDLER_FILE = /\.(?:query|resolver|link|action|template|page-index)(?:\.[cm]?[jt]s)?$/;
86
+ const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
87
+ const APP_FOLDERS = {
88
+ sections: "section",
89
+ blocks: "block",
90
+ components: "component",
91
+ composables: "composable",
92
+ utils: "app-util",
93
+ plugins: "app-plugin",
94
+ overrides: "override"
95
+ };
96
+ const SERVER_FOLDERS = {
97
+ "middleware": "middleware",
98
+ "client": "client",
99
+ "utils": "server-util",
100
+ "api": "route",
101
+ "routes": "route",
102
+ "plugins": "nitro-plugin",
103
+ "media-library": "media-library",
104
+ "media-libraries": "media-library"
105
+ };
106
+ function classify(file) {
107
+ const absolute = normalize(file);
108
+ const test = TEST_FILE.test(absolute);
109
+ const runtimeIndex = absolute.lastIndexOf("/src/runtime/");
110
+ if (runtimeIndex !== -1) {
111
+ const root = absolute.slice(0, runtimeIndex);
112
+ return {
113
+ root,
114
+ path: absolute.slice(root.length + 1),
115
+ test,
116
+ ...classifyRuntime(absolute.slice(runtimeIndex + 13).split("/"))
117
+ };
118
+ }
119
+ const srcIndex = absolute.lastIndexOf("/src/");
120
+ if (srcIndex === -1) return;
121
+ const root = absolute.slice(0, srcIndex);
122
+ const path = absolute.slice(root.length + 1);
123
+ return {
124
+ root,
125
+ path,
126
+ side: path.startsWith("src/types/") ? "other" : "build",
127
+ test
128
+ };
129
+ }
130
+ function domainOf(file) {
131
+ if (!file.domain || file.kind === "handler" || file.kind === "orchestr-file") return file.domain;
132
+ return isDirectory(`${file.root}/src/runtime/server/orchestr/${file.domain}`) ? file.domain : void 0;
133
+ }
134
+ function classifyRuntime(parts) {
135
+ const [side, folder = "", ...rest] = parts;
136
+ if (parts.length < 2 || side !== "app" && side !== "server" && side !== "shared") return { side: "other" };
137
+ if (side === "shared") return {
138
+ side,
139
+ kind: "shared"
140
+ };
141
+ if (side === "app") return {
142
+ side,
143
+ kind: APP_FOLDERS[folder] ?? "other"
144
+ };
145
+ const nested = rest.length > 1;
146
+ if (folder === "orchestr") {
147
+ if (nested && rest[0] === "plugins") return {
148
+ side,
149
+ kind: "orchestr-plugin"
150
+ };
151
+ return {
152
+ side,
153
+ kind: HANDLER_FILE.test(parts.at(-1)) ? "handler" : "orchestr-file",
154
+ domain: nested ? rest[0] : void 0
155
+ };
156
+ }
157
+ const kind = SERVER_FOLDERS[folder] ?? "other";
158
+ return {
159
+ side,
160
+ kind,
161
+ domain: kind === "server-util" && nested ? rest[0] : void 0
162
+ };
163
+ }
164
+ function kebabCase(name) {
165
+ return name.replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
166
+ }
167
+ function prefixName(name, prefix) {
168
+ return `${prefix}${name.endsWith(prefix) ? name.slice(0, -prefix.length) : name}`;
169
+ }
170
+ const UI_KIT_BUTTON = /(?:^#ui-kit|@laioutr-core\/ui-kit)\/.*\/Button\.vue$/;
171
+ const button = {
172
+ meta: {
173
+ type: "problem",
174
+ fixable: "code",
175
+ docs: { description: "Disallow `type` on the ui-kit button, which silently renders its `button-type` prop instead." },
176
+ schema: [],
177
+ messages: { buttonType: "l-button ignores type and always renders its button-type prop, which defaults to \"button\". Use button-type." }
178
+ },
179
+ create(context) {
180
+ const services = context.sourceCode.parserServices;
181
+ if (!services?.defineTemplateBodyVisitor) return {};
182
+ const file = classify(context.filename);
183
+ if (file?.test) return {};
184
+ const reporter = createReporter(context, file);
185
+ const buttons = /* @__PURE__ */ new Set(["LButton", "l-button"]);
186
+ return services.defineTemplateBodyVisitor({
187
+ "VElement": (node) => {
188
+ if (!buttons.has(node.rawName)) return;
189
+ for (const attribute of node.startTag.attributes) {
190
+ if (!attribute.directive && attribute.key.rawName === "type") {
191
+ const key = attribute.key;
192
+ reporter.report({
193
+ loc: key.loc,
194
+ messageId: "buttonType",
195
+ fix: (fixer) => fixer.replaceTextRange(key.range, "button-type")
196
+ });
197
+ }
198
+ const argument = attribute.directive ? attribute.key.argument : null;
199
+ if (attribute.directive && attribute.key.name.name === "bind" && argument?.type === "VIdentifier" && argument.rawName === "type") reporter.report({
200
+ loc: argument.loc,
201
+ messageId: "buttonType",
202
+ fix: (fixer) => fixer.replaceTextRange(argument.range, "button-type")
203
+ });
204
+ }
205
+ },
206
+ "VElement:exit": (node) => {
207
+ if (node.parent.type === "VDocumentFragment") reporter.flush();
208
+ }
209
+ }, { ImportDeclaration: (node) => {
210
+ if (typeof node.source.value !== "string" || !UI_KIT_BUTTON.test(node.source.value)) return;
211
+ for (const specifier of node.specifiers) if (specifier.type === "ImportDefaultSpecifier") {
212
+ const kebab = kebabCase(specifier.local.name);
213
+ buttons.add(specifier.local.name);
214
+ if (kebab.includes("-")) buttons.add(kebab);
215
+ }
216
+ } });
217
+ }
218
+ };
219
+ const FILE_START = {
220
+ start: {
221
+ line: 1,
222
+ column: 0
223
+ },
224
+ end: {
225
+ line: 1,
226
+ column: 0
227
+ }
228
+ };
229
+ function findProperty(object, key) {
230
+ 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;
231
+ }
232
+ function findStringProperty(object, key) {
233
+ const value = findProperty(object, key);
234
+ return value?.type === "Literal" && typeof value.value === "string" ? {
235
+ value: value.value,
236
+ node: value
237
+ } : void 0;
238
+ }
239
+ function objectElements(node) {
240
+ if (node?.type !== "ArrayExpression") return [];
241
+ return node.elements.filter((element) => element?.type === "ObjectExpression");
242
+ }
243
+ const DEFINERS = {
244
+ defineSection: {
245
+ kind: "section",
246
+ folder: "sections",
247
+ prefix: "Section"
248
+ },
249
+ defineBlock: {
250
+ kind: "block",
251
+ folder: "blocks",
252
+ prefix: "Block"
253
+ }
254
+ };
255
+ const FORBIDDEN_FIELD_NAMES = /* @__PURE__ */ new Set([
256
+ "style",
257
+ "class",
258
+ "key",
259
+ "ref",
260
+ "is",
261
+ "slot",
262
+ "refFor",
263
+ "refKey"
264
+ ]);
265
+ const definition = {
266
+ meta: {
267
+ type: "problem",
268
+ docs: { description: "Keep defineSection() in app/sections/Section*.vue and defineBlock() in app/blocks/Block*.vue, with a matching component name and schema fields that reach the component." },
269
+ schema: [],
270
+ messages: {
271
+ notDefined: "Laioutr registers every .vue in {{folder}}/ as a Studio {{kind}}, but this file has no {{definer}}(). Move it to components/.",
272
+ wrongFolder: "{{definer}}() belongs in app/{{folder}}/.",
273
+ prefix: "Name {{kind}}s {{prefix}}*.vue, e.g. {{suggestion}}.vue.",
274
+ componentName: "component: '{{actual}}' must match the file name '{{expected}}'.",
275
+ forbiddenFieldName: "Vue consumes '{{name}}' before it reaches the component, so this field never arrives as a prop. Pick another name, e.g. variant for a style selector."
276
+ }
277
+ },
278
+ create(context) {
279
+ const file = classify(context.filename);
280
+ if (!file || file.test || file.side !== "app") return {};
281
+ const reporter = createReporter(context, file);
282
+ const name = basename(file.path).replace(/\.[^.]+$/, "");
283
+ const calls = [];
284
+ return {
285
+ "CallExpression": (node) => {
286
+ if (node.callee.type === "Identifier" && Object.hasOwn(DEFINERS, node.callee.name)) calls.push({
287
+ definer: node.callee.name,
288
+ node
289
+ });
290
+ },
291
+ "Program:exit": () => {
292
+ const expected = Object.keys(DEFINERS).find((definer) => DEFINERS[definer].kind === file.kind);
293
+ if (expected && file.path.endsWith(".vue") && !calls.some((call) => call.definer === expected)) {
294
+ const { kind, folder } = DEFINERS[expected];
295
+ reporter.report({
296
+ loc: FILE_START,
297
+ messageId: "notDefined",
298
+ data: {
299
+ folder,
300
+ kind,
301
+ definer: expected
302
+ }
303
+ });
304
+ }
305
+ for (const { definer, node } of calls) {
306
+ const { kind, folder, prefix } = DEFINERS[definer];
307
+ if (file.kind !== kind && file.kind !== "override") reporter.report({
308
+ node: node.callee,
309
+ messageId: "wrongFolder",
310
+ data: {
311
+ definer,
312
+ folder
313
+ }
314
+ });
315
+ else if (file.kind === kind && !name.startsWith(prefix)) reporter.report({
316
+ node: node.callee,
317
+ messageId: "prefix",
318
+ data: {
319
+ kind,
320
+ prefix,
321
+ suggestion: prefixName(name, prefix)
322
+ }
323
+ });
324
+ const [options] = node.arguments;
325
+ if (options?.type !== "ObjectExpression") continue;
326
+ const component = findStringProperty(options, "component");
327
+ if (component && component.value !== name) reporter.report({
328
+ node: component.node,
329
+ messageId: "componentName",
330
+ data: {
331
+ actual: component.value,
332
+ expected: name
333
+ }
334
+ });
335
+ for (const group of objectElements(findProperty(options, "schema"))) for (const field of objectElements(findProperty(group, "fields"))) {
336
+ const fieldName = findStringProperty(field, "name");
337
+ if (fieldName && FORBIDDEN_FIELD_NAMES.has(fieldName.value)) reporter.report({
338
+ node: fieldName.node,
339
+ messageId: "forbiddenFieldName",
340
+ data: { name: fieldName.value }
341
+ });
342
+ }
343
+ }
344
+ reporter.flush();
345
+ }
346
+ };
347
+ }
348
+ };
349
+ const files = {
350
+ meta: {
351
+ type: "problem",
352
+ docs: { description: "Keep orchestr/ to handler files in domain folders that only export their handler." },
353
+ schema: [],
354
+ messages: {
355
+ notAHandler: "Laioutr loads every file in orchestr/ as a server plugin. Name handlers *.query.ts, *.resolver.ts, *.link.ts, *.action.ts, *.template.ts or *.page-index.ts, and move everything else to server/utils/.",
356
+ noDomain: "Put handlers in a domain folder, e.g. orchestr/<domain>/{{file}}.",
357
+ namedExport: "Handler files only export their handler. Laioutr registers the default export; move everything else to server/utils/.",
358
+ missingDefault: "Handler files export their handler as default, otherwise laioutr registers nothing."
359
+ }
360
+ },
361
+ create(context) {
362
+ const file = classify(context.filename);
363
+ if (!file || file.test || file.side !== "server" || file.kind !== "handler" && file.kind !== "orchestr-file") return {};
364
+ const reporter = createReporter(context, file);
365
+ if (file.kind === "orchestr-file") return { "Program:exit": () => {
366
+ reporter.report({
367
+ loc: FILE_START,
368
+ messageId: "notAHandler"
369
+ });
370
+ reporter.flush();
371
+ } };
372
+ let hasDefault = false;
373
+ return {
374
+ "ExportDefaultDeclaration": () => {
375
+ hasDefault = true;
376
+ },
377
+ "ExportNamedDeclaration": (node) => {
378
+ const others = node.specifiers.filter((specifier) => {
379
+ const name = specifier.exported.type === "Identifier" ? specifier.exported.name : specifier.exported.value;
380
+ if (name === "default") hasDefault = true;
381
+ return name !== "default";
382
+ });
383
+ if (node.declaration || others.length > 0) reporter.report({
384
+ node,
385
+ messageId: "namedExport"
386
+ });
387
+ },
388
+ "ExportAllDeclaration": (node) => {
389
+ reporter.report({
390
+ node,
391
+ messageId: "namedExport"
392
+ });
393
+ },
394
+ "Program:exit": () => {
395
+ if (!file.domain) reporter.report({
396
+ loc: FILE_START,
397
+ messageId: "noDomain",
398
+ data: { file: basename(file.path) }
399
+ });
400
+ if (!hasDefault) reporter.report({
401
+ loc: FILE_START,
402
+ messageId: "missingDefault"
403
+ });
404
+ reporter.flush();
405
+ }
406
+ };
407
+ }
408
+ };
409
+ function isTypeOnly(node) {
410
+ const { importKind, exportKind } = node;
411
+ if (importKind === "type" || exportKind === "type") return true;
412
+ return node.type === "ImportDeclaration" && node.specifiers.length > 0 && node.specifiers.every((specifier) => specifier.importKind === "type");
413
+ }
414
+ const LABELS = {
415
+ "section": "section",
416
+ "block": "block",
417
+ "component": "component",
418
+ "composable": "composable",
419
+ "app-plugin": "plugin",
420
+ "override": "override",
421
+ "route": "API route",
422
+ "nitro-plugin": "Nitro plugin",
423
+ "orchestr-plugin": "orchestr plugin",
424
+ "orchestr-file": "orchestr",
425
+ "media-library": "media library"
426
+ };
427
+ const ABOVE_APP_UTILS = /* @__PURE__ */ new Set([
428
+ "section",
429
+ "block",
430
+ "component",
431
+ "composable",
432
+ "app-plugin",
433
+ "override"
434
+ ]);
435
+ const ABOVE_COMPOSABLES = /* @__PURE__ */ new Set([
436
+ "section",
437
+ "block",
438
+ "component",
439
+ "override"
440
+ ]);
441
+ const ABOVE_SERVER_UTILS = /* @__PURE__ */ new Set([
442
+ "route",
443
+ "nitro-plugin",
444
+ "orchestr-plugin",
445
+ "orchestr-file",
446
+ "media-library"
447
+ ]);
448
+ const BUILDER_USERS = /* @__PURE__ */ new Set([
449
+ "handler",
450
+ "orchestr-file",
451
+ "middleware",
452
+ "media-library"
453
+ ]);
454
+ function findViolation(importer, target, typeOnly, sharedDomains) {
455
+ if (importer.side === "app" && target.side === "server") return { messageId: "appImportsServer" };
456
+ if (importer.side === "server" && target.side === "app") return { messageId: "serverImportsApp" };
457
+ if (importer.side === "shared" && (target.side === "app" || target.side === "server")) return {
458
+ messageId: "sharedImportsSide",
459
+ data: { side: target.side }
460
+ };
461
+ if (target.side === "build" && !typeOnly) return { messageId: "runtimeImportsBuild" };
462
+ if (target.kind === "handler") return { messageId: "handlerImported" };
463
+ if (typeOnly) return;
464
+ if (target.kind === "client" && importer.kind === "handler") return { messageId: "clientInHandler" };
465
+ if (target.kind === "client" && importer.kind === "server-util") return { messageId: "clientInUtil" };
466
+ if (target.kind === "middleware" && !BUILDER_USERS.has(importer.kind)) return { messageId: "middlewareImported" };
467
+ if (importer.kind === "server-util" && ABOVE_SERVER_UTILS.has(target.kind)) return {
468
+ messageId: "serverUtilImportsUp",
469
+ data: { kind: LABELS[target.kind] }
470
+ };
471
+ const from = domainOf(importer);
472
+ const to = domainOf(target);
473
+ if (from && to && from !== to && !sharedDomains.includes(to)) return {
474
+ messageId: "crossDomain",
475
+ data: {
476
+ from,
477
+ to
478
+ }
479
+ };
480
+ if (importer.kind === "app-util" && ABOVE_APP_UTILS.has(target.kind)) return {
481
+ messageId: "appUtilImportsUp",
482
+ data: { kind: LABELS[target.kind] }
483
+ };
484
+ if (importer.kind === "composable" && ABOVE_COMPOSABLES.has(target.kind)) return {
485
+ messageId: "composableImportsUp",
486
+ data: { kind: LABELS[target.kind] }
487
+ };
488
+ if (importer.kind === "component" && target.kind === "section") return { messageId: "componentImportsSection" };
489
+ }
490
+ const plugin = {
491
+ meta: {
492
+ name,
493
+ version
494
+ },
495
+ rules: {
496
+ "layers": {
497
+ meta: {
498
+ type: "problem",
499
+ docs: { description: "Enforce the layers of a Laioutr app: app, server and shared code, orchestr handlers, middleware, clients, utils and domains." },
500
+ schema: [{
501
+ type: "object",
502
+ properties: { sharedDomains: {
503
+ type: "array",
504
+ items: { type: "string" }
505
+ } },
506
+ additionalProperties: false
507
+ }],
508
+ messages: {
509
+ appImportsServer: "App code can't import server code. Move what both sides need to src/runtime/shared/.",
510
+ serverImportsApp: "Server code can't import app code. Move what both sides need to src/runtime/shared/.",
511
+ sharedImportsSide: "Shared code runs in the app and on the server, so it can't import from {{side}}/.",
512
+ runtimeImportsBuild: "Runtime code can't import build-time code from src/; it isn't part of the runtime bundle. Type imports are fine.",
513
+ handlerImported: "Orchestr handlers are registered by laioutr, never imported. Move the shared code to server/utils/.",
514
+ clientInHandler: "Handlers read API clients from the orchestr context. server/client/ only holds clients; move constants and helpers to server/utils/.",
515
+ clientInUtil: "Server utils get API clients passed in. server/client/ only holds clients; move constants and helpers to server/utils/.",
516
+ middlewareImported: "Only orchestr handlers and media libraries import middleware. Move helpers like this to server/utils/.",
517
+ serverUtilImportsUp: "Server utils are the bottom layer and can't import {{kind}} code.",
518
+ crossDomain: "The {{from}} domain can't import from the {{to}} domain. Move the shared code out of the {{to}} folders into server/utils/, or add '{{to}}' to sharedDomains.",
519
+ appUtilImportsUp: "App utils are the bottom layer and can't import a {{kind}}. Type imports are fine.",
520
+ composableImportsUp: "Composables can't import a {{kind}}. Type imports are fine.",
521
+ componentImportsSection: "Components can't import sections; sections compose components.",
522
+ nodeBuiltin: "'{{name}}' only exists on the server. Keep it in server/."
523
+ }
524
+ },
525
+ create(context) {
526
+ const importer = classify(context.filename);
527
+ if (!importer || importer.test || importer.side !== "app" && importer.side !== "server" && importer.side !== "shared") return {};
528
+ const { sharedDomains = [] } = context.options[0] ?? {};
529
+ const reporter = createReporter(context, importer);
530
+ const checkImport = (node, source, typeOnly) => {
531
+ if (typeof source !== "string") return;
532
+ if (source.startsWith("node:")) {
533
+ if (importer.side !== "server" && !typeOnly) reporter.report({
534
+ node,
535
+ messageId: "nodeBuiltin",
536
+ data: { name: source }
537
+ });
538
+ return;
539
+ }
540
+ const path = resolveImport(context.filename, source);
541
+ const target = path ? classify(path) : void 0;
542
+ const violation = target && findViolation(importer, target, typeOnly, sharedDomains);
543
+ if (violation) reporter.report({
544
+ node,
545
+ ...violation
546
+ });
547
+ };
548
+ return {
549
+ "ImportDeclaration": (node) => {
550
+ checkImport(node, node.source.value, isTypeOnly(node));
551
+ },
552
+ "ExportNamedDeclaration": (node) => {
553
+ if (node.source) checkImport(node, node.source.value, isTypeOnly(node));
554
+ },
555
+ "ExportAllDeclaration": (node) => {
556
+ checkImport(node, node.source.value, isTypeOnly(node));
557
+ },
558
+ "ImportExpression": (node) => {
559
+ if (node.source.type === "Literal") checkImport(node, node.source.value, false);
560
+ },
561
+ "Program:exit": () => {
562
+ reporter.flush();
563
+ }
564
+ };
565
+ }
566
+ },
567
+ "orchestr-files": files,
568
+ "definitions": definition,
569
+ "button-type": button
570
+ }
571
+ };
572
+ async function larrylint(options = {}) {
573
+ const { sharedDomains } = await loadLarrylintConfig(options.cwd);
574
+ return [{
575
+ name: "larrylint",
576
+ files: ["**/src/**/*.{ts,mts,cts,js,mjs,cjs,vue}"],
577
+ plugins: { larrylint: plugin },
578
+ rules: {
579
+ "larrylint/layers": ["error", { sharedDomains }],
580
+ "larrylint/orchestr-files": "error",
581
+ "larrylint/definitions": "error",
582
+ "larrylint/button-type": "error"
583
+ }
584
+ }];
585
+ }
586
+ export { BASELINE_FILE, countViolations, defineLarrylintConfig, larrylint, plugin, readBaseline, writeBaseline };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { description, version } from "../_chunks/package.mjs";
3
+ import { defineCommand, runMain } from "citty";
4
+ const cwdArgs = { cwd: {
5
+ type: "string",
6
+ description: "Folder of the Laioutr app.",
7
+ default: "."
8
+ } };
9
+ const main = defineCommand({
10
+ meta: {
11
+ name: "larrylint",
12
+ version,
13
+ description
14
+ },
15
+ args: cwdArgs,
16
+ subCommands: {
17
+ check: () => import("../_chunks/check2.mjs").then((m) => m.default),
18
+ init: () => import("../_chunks/init.mjs").then((m) => m.default)
19
+ },
20
+ default: "check"
21
+ });
22
+ runMain(main);
23
+ export { cwdArgs };
@@ -0,0 +1,59 @@
1
+ import { Linter } from "eslint";
2
+ interface LarrylintConfig {
3
+ /**
4
+ * Domains every other domain may import.
5
+ */
6
+ sharedDomains?: string[];
7
+ }
8
+ /**
9
+ * Defines the larrylint configuration of a Laioutr app.
10
+ *
11
+ * @param config The configuration.
12
+ *
13
+ * @returns The configuration, typed.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // larrylint.config.ts
18
+ * import { defineLarrylintConfig } from 'larrylint'
19
+ *
20
+ * export default defineLarrylintConfig({
21
+ * sharedDomains: ['product'],
22
+ * })
23
+ * ```
24
+ */
25
+ export declare function defineLarrylintConfig(config: LarrylintConfig): LarrylintConfig;
26
+ export declare const plugin: {
27
+ meta: {
28
+ name: string;
29
+ version: string;
30
+ };
31
+ rules: {
32
+ layers: import("eslint").Rule.RuleModule;
33
+ 'orchestr-files': import("eslint").Rule.RuleModule;
34
+ definitions: import("eslint").Rule.RuleModule;
35
+ 'button-type': import("eslint").Rule.RuleModule;
36
+ };
37
+ };
38
+ interface LarrylintOptions {
39
+ /** Folder of the Laioutr app, defaults to the current working directory. */
40
+ cwd?: string;
41
+ }
42
+ /**
43
+ * Creates the larrylint rules as ESLint flat config. The rules come without parsers,
44
+ * so they run on top of the project's own config, e.g. `@nuxt/eslint-config`.
45
+ *
46
+ * @param options Where to load the larrylint configuration from.
47
+ *
48
+ * @returns The flat config items.
49
+ *
50
+ * @example
51
+ * ```js
52
+ * // eslint.config.mjs
53
+ * import larrylint from 'larrylint'
54
+ *
55
+ * export default createConfigForNuxt().append(larrylint())
56
+ * ```
57
+ */
58
+ declare function larrylint(options?: LarrylintOptions): Promise<Linter.Config[]>;
59
+ export { type LarrylintConfig, type LarrylintOptions, larrylint as default };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { defineLarrylintConfig, larrylint, plugin } from "./_chunks/preset.mjs";
2
+ export { larrylint as default, defineLarrylintConfig, plugin };
package/package.json CHANGED
@@ -1,4 +1,59 @@
1
1
  {
2
2
  "name": "larrylint",
3
- "version": "0.0.1"
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "packageManager": "pnpm@11.5.3",
6
+ "description": "Opinionated structure rules for Laioutr apps, as an ESLint preset and a CLI.",
7
+ "author": "Frederik Bußmann <frederik@bussmann.io>",
8
+ "license": "MIT",
9
+ "repository": "bussmann-io/larrylint",
10
+ "keywords": [
11
+ "laioutr",
12
+ "nuxt",
13
+ "eslint",
14
+ "eslint-plugin",
15
+ "architecture",
16
+ "lint"
17
+ ],
18
+ "exports": {
19
+ ".": "./dist/index.mjs"
20
+ },
21
+ "types": "./dist/index.d.mts",
22
+ "bin": {
23
+ "larrylint": "dist/cli/index.mjs"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "engines": {
29
+ "node": "^20.19.0 || ^22.13.0 || >=24"
30
+ },
31
+ "scripts": {
32
+ "build": "obuild",
33
+ "lint": "eslint .",
34
+ "lint:fix": "eslint . --fix",
35
+ "prepack": "pnpm run build",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "vitest run"
38
+ },
39
+ "dependencies": {
40
+ "@typescript-eslint/parser": "^8.70.1",
41
+ "c12": "^3.3.4",
42
+ "citty": "^0.2.2",
43
+ "consola": "^3.4.2",
44
+ "eslint": "^10.11.0",
45
+ "magicast": "^0.5.5",
46
+ "nypm": "^0.6.10",
47
+ "pathe": "^2.0.3",
48
+ "pkg-types": "^2.3.3",
49
+ "typescript": "^6.0.3",
50
+ "vue-eslint-parser": "^10.4.1"
51
+ },
52
+ "devDependencies": {
53
+ "@antfu/eslint-config": "^9.5.1",
54
+ "@types/estree": "^1.0.9",
55
+ "@types/node": "^24.13.6",
56
+ "obuild": "^0.4.40",
57
+ "vitest": "^5.0.1"
58
+ }
4
59
  }