nuxt-layerscope 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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/dist/analyze-DS51xy6X.mjs +337 -0
- package/dist/analyze-YePAJEVS.mjs +2 -0
- package/dist/api.d.mts +148 -0
- package/dist/api.mjs +6 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +374 -0
- package/dist/component-files-DgOXNE6l.mjs +12 -0
- package/dist/define-DNRGLVG_.d.mts +126 -0
- package/dist/define-l62B9fVk.mjs +6 -0
- package/dist/define.d.mts +2 -0
- package/dist/define.mjs +2 -0
- package/dist/effective-Dqg4EwH_.d.mts +7 -0
- package/dist/eslint.d.mts +7 -0
- package/dist/eslint.mjs +213 -0
- package/dist/file-analysis-Bkq_69x4.mjs +1085 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.mjs +364 -0
- package/dist/layer-factory-xZmIAPrA.mjs +168 -0
- package/dist/report-BNW0GCl9.mjs +131 -0
- package/dist/report-DtAi0vPb.mjs +2 -0
- package/dist/strings-CrOjNZmB.mjs +17 -0
- package/dist/summary-DiUYKMPe.mjs +10 -0
- package/dist/unused-Dxep1p6u.mjs +164 -0
- package/dist/version-D6whhsHH.mjs +9 -0
- package/package.json +93 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { u as LayerscopeConfig } from "./define-DNRGLVG_.mjs";
|
|
2
|
+
//#region src/config/effective.d.ts
|
|
3
|
+
/** The keys that describe the project; `buildDir` only says where to look. */
|
|
4
|
+
declare const PROJECT_CONFIG_KEYS: readonly ['layers', 'rules', 'ignore', 'globals'];
|
|
5
|
+
type ProjectConfig = Pick<LayerscopeConfig, (typeof PROJECT_CONFIG_KEYS)[number]>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { ProjectConfig as t };
|
package/dist/eslint.mjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { d as effectiveConfig, l as REGISTRY_FILE } from "./layer-factory-xZmIAPrA.mjs";
|
|
2
|
+
import { t as packageVersion } from "./version-D6whhsHH.mjs";
|
|
3
|
+
import { _ as findConfigFile, a as lineColumn, c as createOwnerLookup, d as createAnalysisEnv, f as readRegistry, h as boundaryFindings, i as isCheckedFile, l as nameLayers, o as offsetAt, p as tableFromRegistry, s as scanFile, t as FileAnalysis, y as loadConfigSync } from "./file-analysis-Bkq_69x4.mjs";
|
|
4
|
+
import { dirname, join, relative, resolve } from "pathe";
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
//#region src/eslint/lint-file.ts
|
|
7
|
+
/** Both rules lint the same text, so the last result per file is reused. */
|
|
8
|
+
const lastLint = /* @__PURE__ */ new Map();
|
|
9
|
+
/**
|
|
10
|
+
* oxlint passes only the `<script>` of a `.vue` file to JS plugins, while the template matters
|
|
11
|
+
* too; the saved file is read instead.
|
|
12
|
+
*/
|
|
13
|
+
function fullSource(file, text) {
|
|
14
|
+
return file.endsWith(".vue") && !/<(?:script|template)[\s>]/u.test(text) ? readFileSync(file, "utf8") : text;
|
|
15
|
+
}
|
|
16
|
+
/** Lints the text the editor holds, which may differ from the file on disk. */
|
|
17
|
+
function lintFile(file, text, project) {
|
|
18
|
+
const source = fullSource(file, text);
|
|
19
|
+
const cached = lastLint.get(file);
|
|
20
|
+
if (cached?.source === source) return cached.lint;
|
|
21
|
+
const layer = project.env.ownerOf(file);
|
|
22
|
+
let lint = {
|
|
23
|
+
source,
|
|
24
|
+
boundary: [],
|
|
25
|
+
unresolved: []
|
|
26
|
+
};
|
|
27
|
+
if (layer !== null && isCheckedFile(file, layer, project.config.ignore ?? [])) {
|
|
28
|
+
const analysis = new FileAnalysis(file, source, layer, project.env).run(scanFile(source, file));
|
|
29
|
+
lint = {
|
|
30
|
+
source,
|
|
31
|
+
boundary: boundaryFindings(analysis.edges, project.config),
|
|
32
|
+
unresolved: analysis.unresolved
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
lastLint.set(file, {
|
|
36
|
+
source,
|
|
37
|
+
lint
|
|
38
|
+
});
|
|
39
|
+
return lint;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/eslint/location.ts
|
|
43
|
+
/**
|
|
44
|
+
* Maps a finding from the full file into the text the linter passed. ESLint passes the full
|
|
45
|
+
* file; oxlint passes only a `.vue` file's `<script>`, where template findings have no position.
|
|
46
|
+
*/
|
|
47
|
+
function reportLocation(finding, source, text) {
|
|
48
|
+
if (source === text) return {
|
|
49
|
+
line: finding.line,
|
|
50
|
+
column: finding.column,
|
|
51
|
+
outside: false
|
|
52
|
+
};
|
|
53
|
+
const base = source.indexOf(text);
|
|
54
|
+
const offset = offsetAt(source, finding) - base;
|
|
55
|
+
if (base === -1 || offset < 0 || offset > text.length) return {
|
|
56
|
+
line: 1,
|
|
57
|
+
column: 1,
|
|
58
|
+
outside: true
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
...lineColumn(text, offset),
|
|
62
|
+
outside: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/eslint/load-project.ts
|
|
67
|
+
/** Config, layers and symbols of a project, from its registry; Nuxt itself is never loaded. */
|
|
68
|
+
function loadProject(rootDir) {
|
|
69
|
+
const fileConfig = loadConfigSync(rootDir);
|
|
70
|
+
const buildDir = resolve(rootDir, fileConfig.buildDir ?? ".nuxt");
|
|
71
|
+
const read = readRegistry(buildDir);
|
|
72
|
+
if (read.kind !== "ok") return {
|
|
73
|
+
ok: false,
|
|
74
|
+
message: `${read.file} is missing or from another nuxt-layerscope release. Add "nuxt-layerscope" to "modules" in nuxt.config and run "nuxi prepare".`
|
|
75
|
+
};
|
|
76
|
+
const config = effectiveConfig(fileConfig, read.registry.config);
|
|
77
|
+
const layers = nameLayers(read.registry.layers, rootDir, config);
|
|
78
|
+
const table = tableFromRegistry(read.registry, buildDir);
|
|
79
|
+
const lintConfig = {
|
|
80
|
+
...config,
|
|
81
|
+
rules: {
|
|
82
|
+
...config.rules,
|
|
83
|
+
"layer-boundary": "error",
|
|
84
|
+
"unresolved-reference": "warn"
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
project: {
|
|
90
|
+
rootDir,
|
|
91
|
+
config: lintConfig,
|
|
92
|
+
env: createAnalysisEnv(table, createOwnerLookup(layers), buildDir, lintConfig)
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/eslint/project.ts
|
|
98
|
+
const DEFAULT_REGISTRY = join(".nuxt", REGISTRY_FILE);
|
|
99
|
+
/** Editors keep the linter running, so a project is reloaded when its inputs change. */
|
|
100
|
+
const cache = /* @__PURE__ */ new Map();
|
|
101
|
+
function findRoot(file) {
|
|
102
|
+
for (let dir = dirname(file);; dir = dirname(dir)) {
|
|
103
|
+
if (existsSync(join(dir, DEFAULT_REGISTRY))) return dir;
|
|
104
|
+
if (dirname(dir) === dir) return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function mtime(file) {
|
|
108
|
+
return file !== null && existsSync(file) ? statSync(file).mtimeMs : 0;
|
|
109
|
+
}
|
|
110
|
+
function tryLoadProject(rootDir) {
|
|
111
|
+
try {
|
|
112
|
+
return loadProject(rootDir);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
message: error.message
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** The Nuxt project a linted file belongs to, from its registry; Nuxt itself is never loaded. */
|
|
121
|
+
function projectFor(file, root) {
|
|
122
|
+
const rootDir = root === void 0 ? findRoot(file) : resolve(root);
|
|
123
|
+
if (rootDir === null) return {
|
|
124
|
+
ok: false,
|
|
125
|
+
message: `No ${DEFAULT_REGISTRY} above this file. Run "nuxi prepare", or set the rule's "root" option to the Nuxt project.`
|
|
126
|
+
};
|
|
127
|
+
const configFile = findConfigFile(rootDir);
|
|
128
|
+
const registry = join(rootDir, DEFAULT_REGISTRY);
|
|
129
|
+
const stamp = `${mtime(configFile)}:${mtime(registry)}`;
|
|
130
|
+
const cached = cache.get(rootDir);
|
|
131
|
+
if (cached?.stamp === stamp) return cached.load;
|
|
132
|
+
const load = tryLoadProject(rootDir);
|
|
133
|
+
cache.set(rootDir, {
|
|
134
|
+
stamp,
|
|
135
|
+
load
|
|
136
|
+
});
|
|
137
|
+
return load;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/eslint/rules.ts
|
|
141
|
+
const OPTIONS_SCHEMA = [{
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: { root: { type: "string" } },
|
|
144
|
+
additionalProperties: false
|
|
145
|
+
}];
|
|
146
|
+
function describeFinding(finding, rootDir) {
|
|
147
|
+
if (finding.rule !== "layer-boundary" || finding.target === null) return finding.message;
|
|
148
|
+
const allowed = finding.allowed === void 0 || finding.allowed.length === 0 ? "no other layers" : finding.allowed.join(", ");
|
|
149
|
+
return `${finding.message}: ${finding.symbol} → ${relative(rootDir, finding.target)} (allowed for "${finding.fromLayer}": ${allowed})`;
|
|
150
|
+
}
|
|
151
|
+
function createRule(description, pick, reportsSetup) {
|
|
152
|
+
return {
|
|
153
|
+
meta: {
|
|
154
|
+
type: "problem",
|
|
155
|
+
docs: { description },
|
|
156
|
+
schema: OPTIONS_SCHEMA
|
|
157
|
+
},
|
|
158
|
+
create(context) {
|
|
159
|
+
const [options] = context.options;
|
|
160
|
+
return { "Program:exit"() {
|
|
161
|
+
const load = projectFor(context.filename, options?.root);
|
|
162
|
+
if (!load.ok) {
|
|
163
|
+
if (reportsSetup) context.report({
|
|
164
|
+
loc: {
|
|
165
|
+
line: 1,
|
|
166
|
+
column: 0
|
|
167
|
+
},
|
|
168
|
+
message: `layerscope: ${load.message}`
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const { project } = load;
|
|
173
|
+
const { text } = context.sourceCode;
|
|
174
|
+
const lint = lintFile(context.filename, text, project);
|
|
175
|
+
for (const finding of pick(lint)) {
|
|
176
|
+
const { line, column, outside } = reportLocation(finding, lint.source, text);
|
|
177
|
+
const message = describeFinding(finding, project.rootDir);
|
|
178
|
+
context.report({
|
|
179
|
+
loc: {
|
|
180
|
+
line,
|
|
181
|
+
column: column - 1
|
|
182
|
+
},
|
|
183
|
+
message: outside ? `${message} (template line ${finding.line})` : message
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
} };
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const rules = {
|
|
191
|
+
"layer-boundary": createRule("Disallow dependencies, including auto-imports, into layers this layer may not use", (lint) => lint.boundary, true),
|
|
192
|
+
"unresolved-reference": createRule("Report identifiers, components and imports that cannot be resolved", (lint) => lint.unresolved, false)
|
|
193
|
+
};
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/eslint/index.ts
|
|
196
|
+
const plugin = {
|
|
197
|
+
meta: {
|
|
198
|
+
name: "nuxt-layerscope",
|
|
199
|
+
version: packageVersion()
|
|
200
|
+
},
|
|
201
|
+
rules,
|
|
202
|
+
configs: {}
|
|
203
|
+
};
|
|
204
|
+
plugin.configs.recommended = {
|
|
205
|
+
name: "nuxt-layerscope/recommended",
|
|
206
|
+
plugins: { layerscope: plugin },
|
|
207
|
+
rules: {
|
|
208
|
+
"layerscope/layer-boundary": "error",
|
|
209
|
+
"layerscope/unresolved-reference": "warn"
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
export { plugin as default };
|