nuxt-layerscope 0.1.3 → 0.1.4
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 +14 -0
- package/dist/{analyze-QaYmBXyK.mjs → analyze-BwmKhroS.mjs} +6 -99
- package/dist/analyze-vZUz30sN.mjs +2 -0
- package/dist/api.d.mts +93 -2
- package/dist/api.mjs +5 -4
- package/dist/bin.mjs +5 -4
- package/dist/component-files-B_qvkKwv.mjs +153 -0
- package/dist/{define-DNRGLVG_.d.mts → define-DDg4HQRH.d.mts} +1 -1
- package/dist/define.d.mts +1 -1
- package/dist/{effective-Dqg4EwH_.d.mts → effective-BPMNHe9A.d.mts} +1 -1
- package/dist/eslint.mjs +104 -74
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +203 -23
- package/dist/{file-analysis-Bh7qcWUK.mjs → layer-factory-DVSf2Wj4.mjs} +201 -462
- package/dist/owner-BA00V9VD.mjs +451 -0
- package/dist/report-DG-XzEq4.mjs +2 -0
- package/dist/{unused-BtsPSfxb.mjs → unused-DbvZ4gRV.mjs} +1 -1
- package/dist/{version-DFWOUgi5.mjs → version-D5JrgoO_.mjs} +1 -1
- package/package.json +1 -1
- package/dist/analyze-DVzyPH7N.mjs +0 -2
- package/dist/component-files-DgOXNE6l.mjs +0 -12
- package/dist/layer-factory-DFh7nkFH.mjs +0 -182
- package/dist/report-CKhXT50W.mjs +0 -2
- package/dist/{report-DShTCB59.mjs → report-jEMtNXS8.mjs} +1 -1
|
@@ -1,490 +1,113 @@
|
|
|
1
|
-
import { _ as LayerConfigError, a as isClonedFrom, c as realPath, g as ruleSeverity, i as layerFromPath, l as REGISTRY_FILE, m as validateConfig, n as deriveDirectories, o as isFile, r as layerFromNuxt, s as readJson, t as ROOT_LAYER_NAME, v as LayerscopeError } from "./layer-factory-DFh7nkFH.mjs";
|
|
2
1
|
import { n as escapeRegExp, r as pascalCase, t as compareStrings } from "./strings-CrOjNZmB.mjs";
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve } from "pathe";
|
|
4
|
-
import {
|
|
5
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
6
|
-
import { glob } from "tinyglobby";
|
|
7
|
-
import { createJiti } from "jiti";
|
|
8
|
-
import globals from "globals";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "pathe";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
9
4
|
import { ScopeTracker, isReferenceIdentifier, parseAndWalk, walk } from "oxc-walker";
|
|
10
5
|
import { compileScript, compileTemplate, parse } from "@vue/compiler-sfc";
|
|
11
6
|
import path from "node:path";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Config files import `defineConfig` from this package. Aliasing it to our own module lets
|
|
21
|
-
* them load even when layerscope runs through npx without being a project dependency.
|
|
22
|
-
*/
|
|
23
|
-
function defineConfigModule() {
|
|
24
|
-
const candidates = ["./define.ts", "./define.mjs"].map((path) => fileURLToPath(new URL(path, import.meta.url)));
|
|
25
|
-
return candidates.find((path) => existsSync(path)) ?? candidates[0];
|
|
26
|
-
}
|
|
27
|
-
function findConfigFile(rootDir, configFile) {
|
|
28
|
-
if (configFile !== void 0) {
|
|
29
|
-
const file = resolve(process.cwd(), configFile);
|
|
30
|
-
if (!existsSync(file)) throw new LayerscopeError(`Config file not found: ${file}`);
|
|
31
|
-
return file;
|
|
32
|
-
}
|
|
33
|
-
return CONFIG_NAMES.map((name) => resolve(rootDir, name)).find((path) => existsSync(path)) ?? null;
|
|
34
|
-
}
|
|
35
|
-
function createLoader() {
|
|
36
|
-
const self = defineConfigModule();
|
|
37
|
-
return createJiti(import.meta.url, {
|
|
38
|
-
alias: {
|
|
39
|
-
"nuxt-layerscope": self,
|
|
40
|
-
layerscope: self
|
|
41
|
-
},
|
|
42
|
-
moduleCache: false
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
function toConfig(value, file) {
|
|
46
|
-
if (typeof value !== "object" || value === null) throw new LayerscopeError(`${file} must default-export a config object`);
|
|
47
|
-
validateConfig(value, file);
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
|
-
function loadFailed(file, error) {
|
|
51
|
-
return new LayerscopeError(`Failed to load ${file}: ${error.message}`);
|
|
52
|
-
}
|
|
53
|
-
async function loadConfig(rootDir, configFile) {
|
|
54
|
-
const file = findConfigFile(rootDir, configFile);
|
|
55
|
-
if (file === null) return {};
|
|
56
|
-
return toConfig(await createLoader().import(file, { default: true }).catch((error) => {
|
|
57
|
-
throw loadFailed(file, error);
|
|
58
|
-
}), file);
|
|
59
|
-
}
|
|
60
|
-
function requireConfig(file) {
|
|
61
|
-
try {
|
|
62
|
-
return createLoader()(file);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
throw loadFailed(file, error);
|
|
7
|
+
import { glob } from "tinyglobby";
|
|
8
|
+
//#region src/errors.ts
|
|
9
|
+
/** A config or input problem; the CLI exits with code 2. */
|
|
10
|
+
var LayerscopeError = class extends Error {
|
|
11
|
+
constructor(..._args) {
|
|
12
|
+
super(..._args);
|
|
13
|
+
this.name = "LayerscopeError";
|
|
65
14
|
}
|
|
66
|
-
}
|
|
67
|
-
/** For callers that cannot await, such as lint rules. */
|
|
68
|
-
function loadConfigSync(rootDir) {
|
|
69
|
-
const file = findConfigFile(rootDir);
|
|
70
|
-
if (file === null) return {};
|
|
71
|
-
const module = requireConfig(file);
|
|
72
|
-
return toConfig(module?.default ?? module, file);
|
|
73
|
-
}
|
|
74
|
-
//#endregion
|
|
75
|
-
//#region src/rules/layer-boundary.ts
|
|
76
|
-
function describeEdge(edge) {
|
|
77
|
-
if (edge.kind === "component") return `Component <${edge.symbol}>`;
|
|
78
|
-
return edge.kind === "import" ? `Import "${edge.symbol}"` : `Auto-import "${edge.symbol}"`;
|
|
79
|
-
}
|
|
80
|
-
/** Edges to packages and within a layer are always allowed. */
|
|
81
|
-
function isAllowed(edge, allow) {
|
|
82
|
-
return edge.toLayer === null || edge.toLayer === edge.fromLayer || allow.includes(edge.toLayer);
|
|
83
|
-
}
|
|
84
|
-
function boundaryFindings(edges, config) {
|
|
85
|
-
const severity = ruleSeverity(config, "layer-boundary");
|
|
86
|
-
if (severity === "off") return [];
|
|
87
|
-
return edges.flatMap((edge) => {
|
|
88
|
-
const allow = config.layers?.[edge.fromLayer]?.allow;
|
|
89
|
-
if (allow === void 0 || isAllowed(edge, allow)) return [];
|
|
90
|
-
return [{
|
|
91
|
-
rule: "layer-boundary",
|
|
92
|
-
severity,
|
|
93
|
-
file: edge.file,
|
|
94
|
-
line: edge.line,
|
|
95
|
-
column: edge.column,
|
|
96
|
-
symbol: edge.symbol,
|
|
97
|
-
fromLayer: edge.fromLayer,
|
|
98
|
-
toLayer: edge.toLayer,
|
|
99
|
-
target: edge.to,
|
|
100
|
-
allowed: allow,
|
|
101
|
-
message: `${describeEdge(edge)} crosses from layer "${edge.fromLayer}" into "${edge.toLayer}"`
|
|
102
|
-
}];
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
//#endregion
|
|
106
|
-
//#region src/resolve/file.ts
|
|
107
|
-
const EXTENSIONS = [
|
|
108
|
-
".ts",
|
|
109
|
-
".tsx",
|
|
110
|
-
".mts",
|
|
111
|
-
".js",
|
|
112
|
-
".mjs",
|
|
113
|
-
".jsx",
|
|
114
|
-
".vue",
|
|
115
|
-
".json"
|
|
116
|
-
];
|
|
117
|
-
function withTsExtension(path) {
|
|
118
|
-
const jsExt = /\.(m?)js$/u.exec(path);
|
|
119
|
-
if (!jsExt) return null;
|
|
120
|
-
const base = path.slice(0, -jsExt[0].length);
|
|
121
|
-
return [`${base}.${jsExt[1]}ts`, `${base}.tsx`].find((candidate) => isFile(candidate)) ?? null;
|
|
122
|
-
}
|
|
123
|
-
/** Bundler-style lookup: exact, added extension, `.js` → `.ts`, then `index.*`. */
|
|
124
|
-
function resolveFile(path) {
|
|
125
|
-
const direct = [path, ...EXTENSIONS.map((ext) => path + ext)].find((candidate) => isFile(candidate));
|
|
126
|
-
if (direct !== void 0) return direct;
|
|
127
|
-
return withTsExtension(path) ?? EXTENSIONS.map((ext) => join(path, `index${ext}`)).find((candidate) => isFile(candidate)) ?? null;
|
|
128
|
-
}
|
|
15
|
+
};
|
|
129
16
|
//#endregion
|
|
130
|
-
//#region src/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
17
|
+
//#region src/layer-config-error.ts
|
|
18
|
+
/** A layer name in the config that matches no layer. */
|
|
19
|
+
var LayerConfigError = class extends LayerscopeError {
|
|
20
|
+
constructor(..._args) {
|
|
21
|
+
super(..._args);
|
|
22
|
+
this.name = "LayerConfigError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
139
25
|
//#endregion
|
|
140
|
-
//#region src/
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const INLINE_TYPE = {
|
|
146
|
-
file: null,
|
|
147
|
-
module: "nuxt"
|
|
26
|
+
//#region src/config/rules.ts
|
|
27
|
+
const DEFAULT_SEVERITY = {
|
|
28
|
+
"layer-boundary": "error",
|
|
29
|
+
"unresolved-reference": "warn",
|
|
30
|
+
"shadowed-component": "warn"
|
|
148
31
|
};
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
file: file === null ? null : realPath(file),
|
|
163
|
-
module: packageName(source)
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
file: realPath(resolveFile(source) ?? source),
|
|
168
|
-
module: null
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function toTarget(declaration, fromDir, buildDir) {
|
|
172
|
-
const specifier = TYPEOF_IMPORT.exec(declaration)?.[2];
|
|
173
|
-
if (specifier === void 0) return INLINE_TYPE;
|
|
174
|
-
return targetOf(specifier.startsWith(".") || specifier.startsWith("/") ? resolve(fromDir, specifier) : specifier, buildDir);
|
|
175
|
-
}
|
|
176
|
-
/** Only `declare global { ... }` blocks hold auto-imports; the rest are type augmentations. */
|
|
177
|
-
function parseGlobalDeclarations(file, buildDir) {
|
|
178
|
-
const table = /* @__PURE__ */ new Map();
|
|
179
|
-
if (!existsSync(file)) return table;
|
|
180
|
-
let inGlobal = false;
|
|
181
|
-
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
182
|
-
if (line.startsWith("declare global") || line.startsWith("}")) {
|
|
183
|
-
inGlobal = line.startsWith("declare global");
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
const match = inGlobal ? GLOBAL_CONST.exec(line) : null;
|
|
187
|
-
if (match) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
|
|
188
|
-
}
|
|
189
|
-
return table;
|
|
190
|
-
}
|
|
191
|
-
/** `.nuxt/components.d.ts`: one `export const` per component. */
|
|
192
|
-
function parseComponentExports(file, buildDir) {
|
|
193
|
-
const table = /* @__PURE__ */ new Map();
|
|
194
|
-
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
195
|
-
const match = EXPORT_CONST.exec(line);
|
|
196
|
-
if (match && TYPEOF_IMPORT.test(match[2])) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
|
|
197
|
-
}
|
|
198
|
-
return table;
|
|
199
|
-
}
|
|
200
|
-
/** `.nuxt/types/components.d.ts` (Nuxt 4.1+): members of `interface _GlobalComponents`. */
|
|
201
|
-
function parseComponentInterface(file, buildDir) {
|
|
202
|
-
const table = /* @__PURE__ */ new Map();
|
|
203
|
-
let inInterface = false;
|
|
204
|
-
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
205
|
-
if (line.startsWith("interface _GlobalComponents") || line.startsWith("}")) {
|
|
206
|
-
inInterface = line.startsWith("interface _GlobalComponents");
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
const match = inInterface ? INTERFACE_MEMBER.exec(line) : null;
|
|
210
|
-
if (match && TYPEOF_IMPORT.test(match[2])) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
|
|
211
|
-
}
|
|
212
|
-
return table;
|
|
213
|
-
}
|
|
214
|
-
function loadComponents(buildDir) {
|
|
215
|
-
const root = join(buildDir, "components.d.ts");
|
|
216
|
-
if (existsSync(root)) return parseComponentExports(root, buildDir);
|
|
217
|
-
const types = join(buildDir, "types/components.d.ts");
|
|
218
|
-
return existsSync(types) ? parseComponentInterface(types, buildDir) : null;
|
|
219
|
-
}
|
|
220
|
-
function sameTarget(a, b) {
|
|
221
|
-
return a.file === b.file && a.module === b.module;
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Nuxt 3 writes no `shared-imports.d.ts`; Nuxt 4 derives it from the imports both contexts
|
|
225
|
-
* share, so the same rule applies here.
|
|
226
|
-
*/
|
|
227
|
-
function loadSharedImports(buildDir, app, server) {
|
|
228
|
-
const file = join(buildDir, "types/shared-imports.d.ts");
|
|
229
|
-
if (existsSync(file)) return parseGlobalDeclarations(file, buildDir);
|
|
230
|
-
return new Map([...app].filter(([name, target]) => {
|
|
231
|
-
const other = server.get(name);
|
|
232
|
-
return other !== void 0 && sameTarget(target, other);
|
|
233
|
-
}));
|
|
234
|
-
}
|
|
235
|
-
function loadSymbolTable(buildDir) {
|
|
236
|
-
const app = join(buildDir, "types/imports.d.ts");
|
|
237
|
-
const components = existsSync(app) ? loadComponents(buildDir) : null;
|
|
238
|
-
if (components === null) {
|
|
239
|
-
const missing = existsSync(app) ? join(buildDir, "components.d.ts") : app;
|
|
240
|
-
throw new LayerscopeError(`${missing} not found. Run "nuxi prepare" first, or pass --prepare.`);
|
|
241
|
-
}
|
|
242
|
-
const appImports = parseGlobalDeclarations(app, buildDir);
|
|
243
|
-
const serverImports = parseGlobalDeclarations(join(buildDir, "types/nitro-imports.d.ts"), buildDir);
|
|
244
|
-
return {
|
|
245
|
-
imports: {
|
|
246
|
-
app: appImports,
|
|
247
|
-
server: serverImports,
|
|
248
|
-
shared: loadSharedImports(buildDir, appImports, serverImports)
|
|
249
|
-
},
|
|
250
|
-
components
|
|
251
|
-
};
|
|
32
|
+
/** Accepted in config for forward compatibility; not implemented yet. */
|
|
33
|
+
const RESERVED_RULES = /* @__PURE__ */ new Set(["unused-symbol"]);
|
|
34
|
+
const SEVERITIES = /* @__PURE__ */ new Set([
|
|
35
|
+
"off",
|
|
36
|
+
"warn",
|
|
37
|
+
"error"
|
|
38
|
+
]);
|
|
39
|
+
function isRuleName(name) {
|
|
40
|
+
return Object.hasOwn(DEFAULT_SEVERITY, name);
|
|
41
|
+
}
|
|
42
|
+
function ruleSeverity(config, rule) {
|
|
43
|
+
return config.rules?.[rule] ?? DEFAULT_SEVERITY[rule];
|
|
252
44
|
}
|
|
253
45
|
//#endregion
|
|
254
|
-
//#region src/
|
|
255
|
-
function
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
file
|
|
260
|
-
};
|
|
261
|
-
const registry = readJson(file);
|
|
262
|
-
if (registry.version !== 1) return {
|
|
263
|
-
kind: "incompatible",
|
|
264
|
-
file,
|
|
265
|
-
version: registry.version
|
|
266
|
-
};
|
|
267
|
-
return {
|
|
268
|
-
kind: "ok",
|
|
269
|
-
file,
|
|
270
|
-
registry
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
function importMap(imports, buildDir) {
|
|
274
|
-
return new Map(imports.map((entry) => [entry.name, targetOf(entry.from, buildDir)]));
|
|
275
|
-
}
|
|
276
|
-
/** The same table the `.d.ts` parser builds, from Nuxt's own registry. */
|
|
277
|
-
function tableFromRegistry(registry, buildDir) {
|
|
278
|
-
const components = /* @__PURE__ */ new Map();
|
|
279
|
-
const lazy = /* @__PURE__ */ new Map();
|
|
280
|
-
for (const component of registry.components) {
|
|
281
|
-
const target = targetOf(component.file, buildDir);
|
|
282
|
-
components.set(component.name, target);
|
|
283
|
-
lazy.set(`Lazy${component.name}`, target);
|
|
46
|
+
//#region src/config/validate.ts
|
|
47
|
+
function validateConfig(config, file) {
|
|
48
|
+
for (const [rule, severity] of Object.entries(config.rules ?? {})) {
|
|
49
|
+
if (!isRuleName(rule) && !RESERVED_RULES.has(rule)) throw new LayerscopeError(`${file}: unknown rule "${rule}"`);
|
|
50
|
+
if (!SEVERITIES.has(String(severity))) throw new LayerscopeError(`${file}: rule "${rule}" must be "off", "warn" or "error", got ${JSON.stringify(severity)}`);
|
|
284
51
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
server: importMap(registry.imports.server, buildDir),
|
|
289
|
-
shared: importMap(registry.imports.shared, buildDir)
|
|
290
|
-
},
|
|
291
|
-
components: new Map([...components, ...lazy])
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
//#endregion
|
|
295
|
-
//#region src/nuxt/aliases.ts
|
|
296
|
-
const TSCONFIG_FILES = [
|
|
297
|
-
"tsconfig.app.json",
|
|
298
|
-
"tsconfig.json",
|
|
299
|
-
"tsconfig.server.json",
|
|
300
|
-
"tsconfig.shared.json"
|
|
301
|
-
];
|
|
302
|
-
/** Nuxt's alias map, as written to the generated tsconfig files. */
|
|
303
|
-
function loadAliases(buildDir) {
|
|
304
|
-
const map = /* @__PURE__ */ new Map();
|
|
305
|
-
const files = TSCONFIG_FILES.map((name) => join(buildDir, name)).filter((file) => existsSync(file));
|
|
306
|
-
for (const file of files) {
|
|
307
|
-
const paths = readJson(file).compilerOptions?.paths ?? {};
|
|
308
|
-
for (const [key, targets] of Object.entries(paths)) {
|
|
309
|
-
const alias = key.replace(/\/\*$/u, "");
|
|
310
|
-
const target = targets.at(0)?.replace(/\/\*$/u, "");
|
|
311
|
-
if (target !== void 0 && !map.has(alias)) {
|
|
312
|
-
const isPath = isAbsolute(target) || target.startsWith(".");
|
|
313
|
-
map.set(alias, isPath ? resolve(buildDir, target) : target);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
52
|
+
for (const [name, layer] of Object.entries(config.layers ?? {})) {
|
|
53
|
+
if (layer.allow !== void 0 && !Array.isArray(layer.allow)) throw new LayerscopeError(`${file}: layers.${name}.allow must be an array of layer names`);
|
|
54
|
+
if (layer.path !== void 0 && layer.source !== void 0) throw new LayerscopeError(`${file}: layers.${name} sets both "path" and "source"; use one`);
|
|
316
55
|
}
|
|
317
|
-
map.set("#build", buildDir);
|
|
318
|
-
return [...map].toSorted((a, b) => b[0].length - a[0].length);
|
|
319
56
|
}
|
|
320
57
|
//#endregion
|
|
321
|
-
//#region src/
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
"
|
|
58
|
+
//#region src/config/effective.ts
|
|
59
|
+
/** Where the module's options live when they come from `nuxt.config`. */
|
|
60
|
+
const NUXT_CONFIG_LABEL = "nuxt.config (layerscope)";
|
|
61
|
+
/** The keys that describe the project; `buildDir` only says where to look. */
|
|
62
|
+
const PROJECT_CONFIG_KEYS = [
|
|
63
|
+
"layers",
|
|
64
|
+
"rules",
|
|
65
|
+
"ignore",
|
|
66
|
+
"globals"
|
|
330
67
|
];
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
/** Registered globally by Vue and vue-router, so never listed in components.d.ts. */
|
|
334
|
-
const GLOBAL_COMPONENTS = ["RouterLink", "RouterView"];
|
|
335
|
-
function knownIdentifiers(extra) {
|
|
336
|
-
return /* @__PURE__ */ new Set([
|
|
337
|
-
...Object.keys(globals.builtin),
|
|
338
|
-
...Object.keys(globals.browser),
|
|
339
|
-
...Object.keys(globals.node),
|
|
340
|
-
...SFC_MACROS,
|
|
341
|
-
...RUNTIME_GLOBALS,
|
|
342
|
-
"undefined",
|
|
343
|
-
"arguments",
|
|
344
|
-
...extra
|
|
345
|
-
]);
|
|
346
|
-
}
|
|
347
|
-
function knownComponents(extra) {
|
|
348
|
-
return /* @__PURE__ */ new Set([...GLOBAL_COMPONENTS, ...extra]);
|
|
349
|
-
}
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region src/analyze/analysis-env.ts
|
|
352
|
-
/** What every file analysis needs: symbols, aliases, ownership and known globals. */
|
|
353
|
-
function createAnalysisEnv(table, ownerOf, buildDir, config) {
|
|
354
|
-
return {
|
|
355
|
-
table,
|
|
356
|
-
aliases: loadAliases(buildDir),
|
|
357
|
-
buildDir,
|
|
358
|
-
ownerOf,
|
|
359
|
-
identifiers: knownIdentifiers(config.globals ?? []),
|
|
360
|
-
components: knownComponents(config.globals ?? []),
|
|
361
|
-
unresolvedSeverity: ruleSeverity(config, "unresolved-reference")
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
//#endregion
|
|
365
|
-
//#region src/nuxt/kit.ts
|
|
366
|
-
function findPackageDir(from, name) {
|
|
367
|
-
for (let dir = from;; dir = dirname(dir)) {
|
|
368
|
-
const candidate = join(dir, "node_modules", name);
|
|
369
|
-
if (existsSync(join(candidate, "package.json"))) return realpathSync(candidate);
|
|
370
|
-
if (dirname(dir) === dir) return null;
|
|
371
|
-
}
|
|
68
|
+
function hasProjectConfig(config) {
|
|
69
|
+
return PROJECT_CONFIG_KEYS.some((key) => config[key] !== void 0);
|
|
372
70
|
}
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
let entry = pkg.exports?.["."];
|
|
376
|
-
while (typeof entry === "object" && entry !== null) {
|
|
377
|
-
const conditions = entry;
|
|
378
|
-
entry = conditions.import ?? conditions.node ?? conditions.default;
|
|
379
|
-
}
|
|
380
|
-
return join(dir, typeof entry === "string" ? entry : pkg.module ?? pkg.main ?? "index.js");
|
|
71
|
+
function pickProjectConfig(config) {
|
|
72
|
+
return Object.fromEntries(PROJECT_CONFIG_KEYS.flatMap((key) => config[key] === void 0 ? [] : [[key, config[key]]]));
|
|
381
73
|
}
|
|
382
74
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
75
|
+
* `layerscope.config.*` or the `layerscope` key of `nuxt.config`, which the module records in the
|
|
76
|
+
* registry. Both at once is an error rather than a merge, so it is always clear which one applies.
|
|
385
77
|
*/
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
//#endregion
|
|
396
|
-
//#region src/nuxt/validate-layers.ts
|
|
397
|
-
function assertUniqueNames(layers) {
|
|
398
|
-
const roots = /* @__PURE__ */ new Map();
|
|
399
|
-
for (const layer of layers) {
|
|
400
|
-
const other = roots.get(layer.name);
|
|
401
|
-
if (other !== void 0) throw new LayerscopeError(`Two layers are named "${layer.name}" (${other} and ${layer.root}). Set "path" in layerscope.config.ts to tell them apart.`);
|
|
402
|
-
roots.set(layer.name, layer.root);
|
|
403
|
-
}
|
|
404
|
-
return new Set(roots.keys());
|
|
78
|
+
function effectiveConfig(fileConfig, recorded) {
|
|
79
|
+
if (recorded === void 0 || !hasProjectConfig(recorded)) return fileConfig;
|
|
80
|
+
if (hasProjectConfig(fileConfig)) throw new LayerscopeError("Layers or rules are set both in layerscope.config and in the \"layerscope\" key of nuxt.config. Keep one.");
|
|
81
|
+
validateConfig(recorded, NUXT_CONFIG_LABEL);
|
|
82
|
+
return {
|
|
83
|
+
...fileConfig,
|
|
84
|
+
...recorded
|
|
85
|
+
};
|
|
405
86
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const known = [...names].toSorted().join(", ");
|
|
411
|
-
throw new LayerConfigError(`Unknown layer "${name}" in config. Known layers: ${known}`);
|
|
412
|
-
}
|
|
413
|
-
const unknown = rule.allow?.find((allowed) => !names.has(allowed));
|
|
414
|
-
if (unknown !== void 0) throw new LayerConfigError(`layers.${name}.allow: unknown layer "${unknown}"`);
|
|
415
|
-
}
|
|
87
|
+
/** Names the `nuxt.config` key in a layer error, when the settings that caused it came from there. */
|
|
88
|
+
function labelConfigError(error, recorded) {
|
|
89
|
+
if (error instanceof LayerConfigError && recorded !== void 0 && hasProjectConfig(recorded)) return new LayerscopeError(`${NUXT_CONFIG_LABEL}: ${error.message}`);
|
|
90
|
+
return error;
|
|
416
91
|
}
|
|
417
92
|
//#endregion
|
|
418
|
-
//#region src/
|
|
419
|
-
function
|
|
420
|
-
return Object.entries(config.layers ?? {}).flatMap(([name, rule]) => rule.path === void 0 ? [] : [[name, realPath(resolve(rootDir, rule.path))]]);
|
|
421
|
-
}
|
|
422
|
-
function configuredSources(config) {
|
|
423
|
-
return Object.entries(config.layers ?? {}).flatMap(([name, rule]) => rule.source === void 0 ? [] : [[name, rule.source]]);
|
|
424
|
-
}
|
|
425
|
-
function layersFromConfig(rootDir, config) {
|
|
426
|
-
const paths = configuredPaths(rootDir, config);
|
|
427
|
-
if (paths.length === 0) throw new LayerscopeError(`Could not load @nuxt/kit from ${rootDir}. Install dependencies, or give every layer a "path" in layerscope.config.ts.`);
|
|
428
|
-
const layers = paths.map(([name, root]) => layerFromPath(name, root));
|
|
429
|
-
const root = realPath(resolve(rootDir));
|
|
430
|
-
return layers.some((layer) => layer.root === root) ? layers : [layerFromPath(ROOT_LAYER_NAME, root), ...layers];
|
|
431
|
-
}
|
|
432
|
-
async function layersFromNuxt(kit, rootDir) {
|
|
433
|
-
let options = null;
|
|
93
|
+
//#region src/utils/fs.ts
|
|
94
|
+
function isFile(path) {
|
|
434
95
|
try {
|
|
435
|
-
|
|
436
|
-
} catch
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
const dirs = kit.getLayerDirectories?.({ options }) ?? options._layers.map((layer, index) => deriveDirectories(layer, index, options));
|
|
440
|
-
return options._layers.map((layer, index) => layerFromNuxt(layer, index, dirs[index]));
|
|
441
|
-
}
|
|
442
|
-
/** `path` in config names the Nuxt layer at that root. */
|
|
443
|
-
function applyConfiguredNames(layers, rootDir, config) {
|
|
444
|
-
const named = layers.map((layer) => ({ ...layer }));
|
|
445
|
-
for (const [name, root] of configuredPaths(rootDir, config)) {
|
|
446
|
-
const layer = named.find((candidate) => candidate.root === root);
|
|
447
|
-
if (layer === void 0) throw new LayerscopeError(`layers.${name}.path: no Nuxt layer at ${root}`);
|
|
448
|
-
layer.name = name;
|
|
449
|
-
}
|
|
450
|
-
for (const [name, source] of configuredSources(config)) {
|
|
451
|
-
const matches = named.filter((candidate) => isClonedFrom(candidate.root, source));
|
|
452
|
-
if (matches.length !== 1) {
|
|
453
|
-
const found = matches.length === 0 ? "no layer cloned" : "several layers cloned";
|
|
454
|
-
throw new LayerscopeError(`layers.${name}.source: ${found} from "${source}". Set $meta.name in the layer's nuxt.config instead.`);
|
|
455
|
-
}
|
|
456
|
-
matches[0].name = name;
|
|
96
|
+
return statSync(path).isFile();
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
457
99
|
}
|
|
458
|
-
return named;
|
|
459
100
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const named = applyConfiguredNames(layers, rootDir, config);
|
|
463
|
-
validateLayers(named, config);
|
|
464
|
-
return named;
|
|
101
|
+
function readJson(path) {
|
|
102
|
+
return JSON.parse(readFileSync(path, "utf8").replaceAll(/^\s*\/\/.*$/gmu, ""));
|
|
465
103
|
}
|
|
466
|
-
/**
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (layers === void 0) {
|
|
473
|
-
const kit = await loadNuxtKit(rootDir);
|
|
474
|
-
layers = kit === null ? layersFromConfig(rootDir, config) : await layersFromNuxt(kit, rootDir);
|
|
104
|
+
/** Symlinks resolved (pnpm links packages), or the path itself when it does not exist. */
|
|
105
|
+
function realPath(path) {
|
|
106
|
+
try {
|
|
107
|
+
return realpathSync(path);
|
|
108
|
+
} catch {
|
|
109
|
+
return path;
|
|
475
110
|
}
|
|
476
|
-
return nameLayers(layers, rootDir, config);
|
|
477
|
-
}
|
|
478
|
-
//#endregion
|
|
479
|
-
//#region src/nuxt/owner.ts
|
|
480
|
-
/** The layer whose root is the longest prefix of the file; files in node_modules have none. */
|
|
481
|
-
function createOwnerLookup(layers) {
|
|
482
|
-
const deepestFirst = layers.toSorted((a, b) => b.root.length - a.root.length);
|
|
483
|
-
return (file) => {
|
|
484
|
-
const layer = deepestFirst.find((candidate) => file.startsWith(`${candidate.root}/`));
|
|
485
|
-
if (layer === void 0) return null;
|
|
486
|
-
return file.slice(layer.root.length).includes("/node_modules/") ? null : layer;
|
|
487
|
-
};
|
|
488
111
|
}
|
|
489
112
|
//#endregion
|
|
490
113
|
//#region src/scan/ast.ts
|
|
@@ -900,6 +523,40 @@ function contextOf(file, layer) {
|
|
|
900
523
|
return "app";
|
|
901
524
|
}
|
|
902
525
|
//#endregion
|
|
526
|
+
//#region src/resolve/file.ts
|
|
527
|
+
const EXTENSIONS = [
|
|
528
|
+
".ts",
|
|
529
|
+
".tsx",
|
|
530
|
+
".mts",
|
|
531
|
+
".js",
|
|
532
|
+
".mjs",
|
|
533
|
+
".jsx",
|
|
534
|
+
".vue",
|
|
535
|
+
".json"
|
|
536
|
+
];
|
|
537
|
+
function withTsExtension(path) {
|
|
538
|
+
const jsExt = /\.(m?)js$/u.exec(path);
|
|
539
|
+
if (!jsExt) return null;
|
|
540
|
+
const base = path.slice(0, -jsExt[0].length);
|
|
541
|
+
return [`${base}.${jsExt[1]}ts`, `${base}.tsx`].find((candidate) => isFile(candidate)) ?? null;
|
|
542
|
+
}
|
|
543
|
+
/** Bundler-style lookup: exact, added extension, `.js` → `.ts`, then `index.*`. */
|
|
544
|
+
function resolveFile(path) {
|
|
545
|
+
const direct = [path, ...EXTENSIONS.map((ext) => path + ext)].find((candidate) => isFile(candidate));
|
|
546
|
+
if (direct !== void 0) return direct;
|
|
547
|
+
return withTsExtension(path) ?? EXTENSIONS.map((ext) => join(path, `index${ext}`)).find((candidate) => isFile(candidate)) ?? null;
|
|
548
|
+
}
|
|
549
|
+
//#endregion
|
|
550
|
+
//#region src/resolve/package-name.ts
|
|
551
|
+
const NODE_MODULES = "/node_modules/";
|
|
552
|
+
/** Package name of a bare specifier or of a path inside node_modules. */
|
|
553
|
+
function packageName(specifier) {
|
|
554
|
+
const index = specifier.lastIndexOf(NODE_MODULES);
|
|
555
|
+
const bare = index === -1 ? specifier : specifier.slice(index + 14);
|
|
556
|
+
const parts = bare.split("/");
|
|
557
|
+
return bare.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
903
560
|
//#region src/resolve/specifier.ts
|
|
904
561
|
function isRelative(specifier) {
|
|
905
562
|
return /^\.\.?(?:\/|$)/u.test(specifier);
|
|
@@ -923,10 +580,18 @@ function resolveAlias(specifier, aliases, buildDir) {
|
|
|
923
580
|
if (match === void 0) return null;
|
|
924
581
|
const [alias, target] = match;
|
|
925
582
|
const path = target + specifier.slice(alias.length);
|
|
926
|
-
|
|
583
|
+
if (!isAbsolute(target)) return {
|
|
927
584
|
kind: "external",
|
|
928
585
|
module: packageName(path)
|
|
929
586
|
};
|
|
587
|
+
const resolution = resolvePath(path, buildDir);
|
|
588
|
+
const manifest = join(target, "package.json");
|
|
589
|
+
if (resolution.kind !== "missing" || !isFile(manifest)) return resolution;
|
|
590
|
+
const { name } = readJson(manifest);
|
|
591
|
+
return {
|
|
592
|
+
kind: "external",
|
|
593
|
+
module: name ?? packageName(target)
|
|
594
|
+
};
|
|
930
595
|
}
|
|
931
596
|
function resolveSpecifier(specifier, fromFile, aliases, buildDir) {
|
|
932
597
|
if (isRelative(specifier)) return resolvePath(resolve(dirname(fromFile), specifier), buildDir);
|
|
@@ -1092,4 +757,78 @@ function analyzeFile(file, layer, env) {
|
|
|
1092
757
|
return new FileAnalysis(file, source, layer, env).run(scanFile(source, file));
|
|
1093
758
|
}
|
|
1094
759
|
//#endregion
|
|
1095
|
-
|
|
760
|
+
//#region src/registry/schema.ts
|
|
761
|
+
/** Bumped on breaking changes to `registry.json`; readers fall back to the `.d.ts` files. */
|
|
762
|
+
const REGISTRY_VERSION = 1;
|
|
763
|
+
/** Relative to the Nuxt build dir. */
|
|
764
|
+
const REGISTRY_FILE = "layerscope/registry.json";
|
|
765
|
+
//#endregion
|
|
766
|
+
//#region src/nuxt/remote-layer.ts
|
|
767
|
+
/** c12 clones remote layers into `node_modules/.c12`, or into `~/.cache/c12` without one. */
|
|
768
|
+
const CLONE_DIRS = /* @__PURE__ */ new Set([".c12", "c12"]);
|
|
769
|
+
/**
|
|
770
|
+
* c12 names a clone after the first three word segments of its `extends` source plus a hash of
|
|
771
|
+
* the whole source (`github:org/repo#v1` → `github_org_repo_<hash>`). The hash changes with the
|
|
772
|
+
* ref, so names and config matching use the prefix only.
|
|
773
|
+
*/
|
|
774
|
+
function clonePrefix(source) {
|
|
775
|
+
return source.replaceAll(/\W+/gu, "_").split("_").slice(0, 3).join("_");
|
|
776
|
+
}
|
|
777
|
+
/** The clone name without its hash, so it survives ref changes; `null` for other layers. */
|
|
778
|
+
function remoteLayerName(root) {
|
|
779
|
+
if (!CLONE_DIRS.has(basename(dirname(root)))) return null;
|
|
780
|
+
const name = basename(root);
|
|
781
|
+
const hashStart = name.lastIndexOf("_");
|
|
782
|
+
return hashStart === -1 ? name : name.slice(0, hashStart);
|
|
783
|
+
}
|
|
784
|
+
/** Whether the layer at `root` was cloned from this `extends` source. */
|
|
785
|
+
function isClonedFrom(root, source) {
|
|
786
|
+
return remoteLayerName(root) === clonePrefix(source);
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/nuxt/layer-factory.ts
|
|
790
|
+
const ROOT_LAYER_NAME = "root";
|
|
791
|
+
/** Fallback for `@nuxt/kit` releases without `getLayerDirectories`. */
|
|
792
|
+
function deriveDirectories(layer, index, options) {
|
|
793
|
+
const root = resolve(layer.config.rootDir ?? layer.cwd);
|
|
794
|
+
return {
|
|
795
|
+
root,
|
|
796
|
+
app: resolve(root, layer.config.srcDir ?? "."),
|
|
797
|
+
server: index === 0 && options.serverDir !== void 0 ? resolve(options.serverDir) : resolve(root, layer.config.serverDir ?? "server"),
|
|
798
|
+
shared: resolve(root, layer.config.dir?.shared ?? options.dir?.shared ?? "shared")
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
/** Real paths, so they match symlinked paths in generated types and imports. */
|
|
802
|
+
function toDirs(dirs) {
|
|
803
|
+
const dir = (path) => realPath(resolve(path));
|
|
804
|
+
return {
|
|
805
|
+
root: dir(dirs.root),
|
|
806
|
+
srcDir: dir(dirs.app),
|
|
807
|
+
serverDir: dir(dirs.server),
|
|
808
|
+
sharedDir: dir(dirs.shared)
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function layerFromNuxt(layer, index, dirs) {
|
|
812
|
+
const resolved = toDirs(dirs);
|
|
813
|
+
return {
|
|
814
|
+
name: layer.meta?.name ?? layer.config.$meta?.name ?? (index === 0 ? "root" : remoteLayerName(resolved.root) ?? basename(resolved.root)),
|
|
815
|
+
...resolved,
|
|
816
|
+
defaultComponents: layer.config.components === void 0
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
/** Without @nuxt/kit, assume Nuxt defaults: `app/` as srcDir when it exists (Nuxt 4). */
|
|
820
|
+
function layerFromPath(name, root) {
|
|
821
|
+
const appDir = join(root, "app");
|
|
822
|
+
return {
|
|
823
|
+
name,
|
|
824
|
+
...toDirs({
|
|
825
|
+
root,
|
|
826
|
+
app: existsSync(appDir) ? appDir : root,
|
|
827
|
+
server: join(root, "server"),
|
|
828
|
+
shared: join(root, "shared")
|
|
829
|
+
}),
|
|
830
|
+
defaultComponents: true
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
//#endregion
|
|
834
|
+
export { isRuleName as C, LayerscopeError as E, validateConfig as S, LayerConfigError as T, readJson as _, isClonedFrom as a, labelConfigError as b, FileAnalysis as c, resolveFile as d, collectFiles as f, scanFile as g, offsetAt as h, layerFromPath as i, analyzeFile as l, lineColumn as m, deriveDirectories as n, REGISTRY_FILE as o, isCheckedFile as p, layerFromNuxt as r, REGISTRY_VERSION as s, ROOT_LAYER_NAME as t, packageName as u, realPath as v, ruleSeverity as w, pickProjectConfig as x, effectiveConfig as y };
|