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
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { E as LayerscopeError, S as validateConfig, T as LayerConfigError, _ as readJson, a as isClonedFrom, d as resolveFile, i as layerFromPath, n as deriveDirectories, o as REGISTRY_FILE, r as layerFromNuxt, t as ROOT_LAYER_NAME, u as packageName, v as realPath, w as ruleSeverity } from "./layer-factory-DVSf2Wj4.mjs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "pathe";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { createJiti } from "jiti";
|
|
6
|
+
import globals from "globals";
|
|
7
|
+
//#region src/config/load.ts
|
|
8
|
+
const CONFIG_NAMES = [
|
|
9
|
+
"layerscope.config.ts",
|
|
10
|
+
"layerscope.config.mts",
|
|
11
|
+
"layerscope.config.js",
|
|
12
|
+
"layerscope.config.mjs"
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Config files import `defineConfig` from this package. Aliasing it to our own module lets
|
|
16
|
+
* them load even when layerscope runs through npx without being a project dependency.
|
|
17
|
+
*/
|
|
18
|
+
function defineConfigModule() {
|
|
19
|
+
const candidates = ["./define.ts", "./define.mjs"].map((path) => fileURLToPath(new URL(path, import.meta.url)));
|
|
20
|
+
return candidates.find((path) => existsSync(path)) ?? candidates[0];
|
|
21
|
+
}
|
|
22
|
+
function findConfigFile(rootDir, configFile) {
|
|
23
|
+
if (configFile !== void 0) {
|
|
24
|
+
const file = resolve(process.cwd(), configFile);
|
|
25
|
+
if (!existsSync(file)) throw new LayerscopeError(`Config file not found: ${file}`);
|
|
26
|
+
return file;
|
|
27
|
+
}
|
|
28
|
+
return CONFIG_NAMES.map((name) => resolve(rootDir, name)).find((path) => existsSync(path)) ?? null;
|
|
29
|
+
}
|
|
30
|
+
function createLoader() {
|
|
31
|
+
const self = defineConfigModule();
|
|
32
|
+
return createJiti(import.meta.url, {
|
|
33
|
+
alias: {
|
|
34
|
+
"nuxt-layerscope": self,
|
|
35
|
+
layerscope: self
|
|
36
|
+
},
|
|
37
|
+
moduleCache: false
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function toConfig(value, file) {
|
|
41
|
+
if (typeof value !== "object" || value === null) throw new LayerscopeError(`${file} must default-export a config object`);
|
|
42
|
+
validateConfig(value, file);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function loadFailed(file, error) {
|
|
46
|
+
return new LayerscopeError(`Failed to load ${file}: ${error.message}`);
|
|
47
|
+
}
|
|
48
|
+
async function loadConfig(rootDir, configFile) {
|
|
49
|
+
const file = findConfigFile(rootDir, configFile);
|
|
50
|
+
if (file === null) return {};
|
|
51
|
+
return toConfig(await createLoader().import(file, { default: true }).catch((error) => {
|
|
52
|
+
throw loadFailed(file, error);
|
|
53
|
+
}), file);
|
|
54
|
+
}
|
|
55
|
+
function requireConfig(file) {
|
|
56
|
+
try {
|
|
57
|
+
return createLoader()(file);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw loadFailed(file, error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** For callers that cannot await, such as lint rules. */
|
|
63
|
+
function loadConfigSync(rootDir) {
|
|
64
|
+
const file = findConfigFile(rootDir);
|
|
65
|
+
if (file === null) return {};
|
|
66
|
+
const module = requireConfig(file);
|
|
67
|
+
return toConfig(module?.default ?? module, file);
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/rules/layer-boundary.ts
|
|
71
|
+
function describeEdge(edge) {
|
|
72
|
+
if (edge.kind === "component") return `Component <${edge.symbol}>`;
|
|
73
|
+
return edge.kind === "import" ? `Import "${edge.symbol}"` : `Auto-import "${edge.symbol}"`;
|
|
74
|
+
}
|
|
75
|
+
/** Edges to packages and within a layer are always allowed. */
|
|
76
|
+
function isAllowed(edge, allow) {
|
|
77
|
+
return edge.toLayer === null || edge.toLayer === edge.fromLayer || allow.includes(edge.toLayer);
|
|
78
|
+
}
|
|
79
|
+
function boundaryFindings(edges, config) {
|
|
80
|
+
const severity = ruleSeverity(config, "layer-boundary");
|
|
81
|
+
if (severity === "off") return [];
|
|
82
|
+
return edges.flatMap((edge) => {
|
|
83
|
+
const allow = config.layers?.[edge.fromLayer]?.allow;
|
|
84
|
+
if (allow === void 0 || isAllowed(edge, allow)) return [];
|
|
85
|
+
return [{
|
|
86
|
+
rule: "layer-boundary",
|
|
87
|
+
severity,
|
|
88
|
+
file: edge.file,
|
|
89
|
+
line: edge.line,
|
|
90
|
+
column: edge.column,
|
|
91
|
+
symbol: edge.symbol,
|
|
92
|
+
fromLayer: edge.fromLayer,
|
|
93
|
+
toLayer: edge.toLayer,
|
|
94
|
+
target: edge.to,
|
|
95
|
+
allowed: allow,
|
|
96
|
+
message: `${describeEdge(edge)} crosses from layer "${edge.fromLayer}" into "${edge.toLayer}"`
|
|
97
|
+
}];
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/nuxt/symbols.ts
|
|
102
|
+
const GLOBAL_CONST = /^ {2}const ([\w$]+): (.*)$/u;
|
|
103
|
+
const EXPORT_CONST = /^export const ([\w$]+): (.*)$/u;
|
|
104
|
+
const INTERFACE_MEMBER = /^ {2}([\w$]+): (.*)$/u;
|
|
105
|
+
const TYPEOF_IMPORT = /typeof import\((['"])(.+?)\1\)/u;
|
|
106
|
+
const INLINE_TYPE = {
|
|
107
|
+
file: null,
|
|
108
|
+
module: "nuxt"
|
|
109
|
+
};
|
|
110
|
+
/** Target of an absolute path or a bare module specifier. */
|
|
111
|
+
function targetOf(source, buildDir) {
|
|
112
|
+
if (!isAbsolute(source)) return {
|
|
113
|
+
file: null,
|
|
114
|
+
module: packageName(source)
|
|
115
|
+
};
|
|
116
|
+
if (source === buildDir || source.startsWith(`${buildDir}/`)) return {
|
|
117
|
+
file: null,
|
|
118
|
+
module: "#build"
|
|
119
|
+
};
|
|
120
|
+
if (source.includes("/node_modules/")) {
|
|
121
|
+
const file = resolveFile(source);
|
|
122
|
+
return {
|
|
123
|
+
file: file === null ? null : realPath(file),
|
|
124
|
+
module: packageName(source)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
file: realPath(resolveFile(source) ?? source),
|
|
129
|
+
module: null
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function toTarget(declaration, fromDir, buildDir) {
|
|
133
|
+
const specifier = TYPEOF_IMPORT.exec(declaration)?.[2];
|
|
134
|
+
if (specifier === void 0) return INLINE_TYPE;
|
|
135
|
+
return targetOf(specifier.startsWith(".") || specifier.startsWith("/") ? resolve(fromDir, specifier) : specifier, buildDir);
|
|
136
|
+
}
|
|
137
|
+
/** Only `declare global { ... }` blocks hold auto-imports; the rest are type augmentations. */
|
|
138
|
+
function parseGlobalDeclarations(file, buildDir) {
|
|
139
|
+
const table = /* @__PURE__ */ new Map();
|
|
140
|
+
if (!existsSync(file)) return table;
|
|
141
|
+
let inGlobal = false;
|
|
142
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
143
|
+
if (line.startsWith("declare global") || line.startsWith("}")) {
|
|
144
|
+
inGlobal = line.startsWith("declare global");
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const match = inGlobal ? GLOBAL_CONST.exec(line) : null;
|
|
148
|
+
if (match) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
|
|
149
|
+
}
|
|
150
|
+
return table;
|
|
151
|
+
}
|
|
152
|
+
/** `.nuxt/components.d.ts`: one `export const` per component. */
|
|
153
|
+
function parseComponentExports(file, buildDir) {
|
|
154
|
+
const table = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
156
|
+
const match = EXPORT_CONST.exec(line);
|
|
157
|
+
if (match && TYPEOF_IMPORT.test(match[2])) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
|
|
158
|
+
}
|
|
159
|
+
return table;
|
|
160
|
+
}
|
|
161
|
+
/** `.nuxt/types/components.d.ts` (Nuxt 4.1+): members of `interface _GlobalComponents`. */
|
|
162
|
+
function parseComponentInterface(file, buildDir) {
|
|
163
|
+
const table = /* @__PURE__ */ new Map();
|
|
164
|
+
let inInterface = false;
|
|
165
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
166
|
+
if (line.startsWith("interface _GlobalComponents") || line.startsWith("}")) {
|
|
167
|
+
inInterface = line.startsWith("interface _GlobalComponents");
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const match = inInterface ? INTERFACE_MEMBER.exec(line) : null;
|
|
171
|
+
if (match && TYPEOF_IMPORT.test(match[2])) table.set(match[1], toTarget(match[2], dirname(file), buildDir));
|
|
172
|
+
}
|
|
173
|
+
return table;
|
|
174
|
+
}
|
|
175
|
+
function loadComponents(buildDir) {
|
|
176
|
+
const root = join(buildDir, "components.d.ts");
|
|
177
|
+
if (existsSync(root)) return parseComponentExports(root, buildDir);
|
|
178
|
+
const types = join(buildDir, "types/components.d.ts");
|
|
179
|
+
return existsSync(types) ? parseComponentInterface(types, buildDir) : null;
|
|
180
|
+
}
|
|
181
|
+
function sameTarget(a, b) {
|
|
182
|
+
return a.file === b.file && a.module === b.module;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Nuxt 3 writes no `shared-imports.d.ts`; Nuxt 4 derives it from the imports both contexts
|
|
186
|
+
* share, so the same rule applies here.
|
|
187
|
+
*/
|
|
188
|
+
function loadSharedImports(buildDir, app, server) {
|
|
189
|
+
const file = join(buildDir, "types/shared-imports.d.ts");
|
|
190
|
+
if (existsSync(file)) return parseGlobalDeclarations(file, buildDir);
|
|
191
|
+
return new Map([...app].filter(([name, target]) => {
|
|
192
|
+
const other = server.get(name);
|
|
193
|
+
return other !== void 0 && sameTarget(target, other);
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
function loadSymbolTable(buildDir) {
|
|
197
|
+
const app = join(buildDir, "types/imports.d.ts");
|
|
198
|
+
const components = existsSync(app) ? loadComponents(buildDir) : null;
|
|
199
|
+
if (components === null) {
|
|
200
|
+
const missing = existsSync(app) ? join(buildDir, "components.d.ts") : app;
|
|
201
|
+
throw new LayerscopeError(`${missing} not found. Run "nuxi prepare" first, or pass --prepare.`);
|
|
202
|
+
}
|
|
203
|
+
const appImports = parseGlobalDeclarations(app, buildDir);
|
|
204
|
+
const serverImports = parseGlobalDeclarations(join(buildDir, "types/nitro-imports.d.ts"), buildDir);
|
|
205
|
+
return {
|
|
206
|
+
imports: {
|
|
207
|
+
app: appImports,
|
|
208
|
+
server: serverImports,
|
|
209
|
+
shared: loadSharedImports(buildDir, appImports, serverImports)
|
|
210
|
+
},
|
|
211
|
+
components
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/registry/read.ts
|
|
216
|
+
function readRegistry(buildDir) {
|
|
217
|
+
const file = join(buildDir, REGISTRY_FILE);
|
|
218
|
+
if (!existsSync(file)) return {
|
|
219
|
+
kind: "missing",
|
|
220
|
+
file
|
|
221
|
+
};
|
|
222
|
+
const registry = readJson(file);
|
|
223
|
+
if (registry.version !== 1) return {
|
|
224
|
+
kind: "incompatible",
|
|
225
|
+
file,
|
|
226
|
+
version: registry.version
|
|
227
|
+
};
|
|
228
|
+
return {
|
|
229
|
+
kind: "ok",
|
|
230
|
+
file,
|
|
231
|
+
registry
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function importMap(imports, buildDir) {
|
|
235
|
+
return new Map(imports.map((entry) => [entry.name, targetOf(entry.from, buildDir)]));
|
|
236
|
+
}
|
|
237
|
+
/** The same table the `.d.ts` parser builds, from Nuxt's own registry. */
|
|
238
|
+
function tableFromRegistry(registry, buildDir) {
|
|
239
|
+
const components = /* @__PURE__ */ new Map();
|
|
240
|
+
const lazy = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const component of registry.components) {
|
|
242
|
+
const target = targetOf(component.file, buildDir);
|
|
243
|
+
components.set(component.name, target);
|
|
244
|
+
lazy.set(`Lazy${component.name}`, target);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
imports: {
|
|
248
|
+
app: importMap(registry.imports.app, buildDir),
|
|
249
|
+
server: importMap(registry.imports.server, buildDir),
|
|
250
|
+
shared: importMap(registry.imports.shared, buildDir)
|
|
251
|
+
},
|
|
252
|
+
components: new Map([...components, ...lazy])
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/nuxt/aliases.ts
|
|
257
|
+
const TSCONFIG_FILES = [
|
|
258
|
+
"tsconfig.app.json",
|
|
259
|
+
"tsconfig.json",
|
|
260
|
+
"tsconfig.server.json",
|
|
261
|
+
"tsconfig.shared.json"
|
|
262
|
+
];
|
|
263
|
+
/** Nuxt's alias map, as written to the generated tsconfig files. */
|
|
264
|
+
function loadAliases(buildDir) {
|
|
265
|
+
const map = /* @__PURE__ */ new Map();
|
|
266
|
+
const files = TSCONFIG_FILES.map((name) => join(buildDir, name)).filter((file) => existsSync(file));
|
|
267
|
+
for (const file of files) {
|
|
268
|
+
const paths = readJson(file).compilerOptions?.paths ?? {};
|
|
269
|
+
for (const [key, targets] of Object.entries(paths)) {
|
|
270
|
+
const alias = key.replace(/\/\*$/u, "");
|
|
271
|
+
const target = targets.at(0)?.replace(/\/\*$/u, "");
|
|
272
|
+
if (target !== void 0 && !map.has(alias)) {
|
|
273
|
+
const isPath = isAbsolute(target) || target.startsWith(".");
|
|
274
|
+
map.set(alias, isPath ? resolve(buildDir, target) : target);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
map.set("#build", buildDir);
|
|
279
|
+
return [...map].toSorted((a, b) => b[0].length - a[0].length);
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/analyze/known-globals.ts
|
|
283
|
+
const SFC_MACROS = [
|
|
284
|
+
"defineProps",
|
|
285
|
+
"defineEmits",
|
|
286
|
+
"defineExpose",
|
|
287
|
+
"defineOptions",
|
|
288
|
+
"defineSlots",
|
|
289
|
+
"defineModel",
|
|
290
|
+
"withDefaults"
|
|
291
|
+
];
|
|
292
|
+
/** Defined on `globalThis` by Nuxt (app) and Nitro (server), so absent from the registry there. */
|
|
293
|
+
const RUNTIME_GLOBALS = ["$fetch"];
|
|
294
|
+
/** Registered globally by Vue and vue-router, so never listed in components.d.ts. */
|
|
295
|
+
const GLOBAL_COMPONENTS = ["RouterLink", "RouterView"];
|
|
296
|
+
function knownIdentifiers(extra) {
|
|
297
|
+
return /* @__PURE__ */ new Set([
|
|
298
|
+
...Object.keys(globals.builtin),
|
|
299
|
+
...Object.keys(globals.browser),
|
|
300
|
+
...Object.keys(globals.node),
|
|
301
|
+
...SFC_MACROS,
|
|
302
|
+
...RUNTIME_GLOBALS,
|
|
303
|
+
"undefined",
|
|
304
|
+
"arguments",
|
|
305
|
+
...extra
|
|
306
|
+
]);
|
|
307
|
+
}
|
|
308
|
+
function knownComponents(extra) {
|
|
309
|
+
return /* @__PURE__ */ new Set([...GLOBAL_COMPONENTS, ...extra]);
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/analyze/analysis-env.ts
|
|
313
|
+
/** What every file analysis needs: symbols, aliases, ownership and known globals. */
|
|
314
|
+
function createAnalysisEnv(table, ownerOf, buildDir, config) {
|
|
315
|
+
return {
|
|
316
|
+
table,
|
|
317
|
+
aliases: loadAliases(buildDir),
|
|
318
|
+
buildDir,
|
|
319
|
+
ownerOf,
|
|
320
|
+
identifiers: knownIdentifiers(config.globals ?? []),
|
|
321
|
+
components: knownComponents(config.globals ?? []),
|
|
322
|
+
unresolvedSeverity: ruleSeverity(config, "unresolved-reference")
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/nuxt/kit.ts
|
|
327
|
+
function findPackageDir(from, name) {
|
|
328
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
329
|
+
const candidate = join(dir, "node_modules", name);
|
|
330
|
+
if (existsSync(join(candidate, "package.json"))) return realpathSync(candidate);
|
|
331
|
+
if (dirname(dir) === dir) return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function packageEntry(dir) {
|
|
335
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
336
|
+
let entry = pkg.exports?.["."];
|
|
337
|
+
while (typeof entry === "object" && entry !== null) {
|
|
338
|
+
const conditions = entry;
|
|
339
|
+
entry = conditions.import ?? conditions.node ?? conditions.default;
|
|
340
|
+
}
|
|
341
|
+
return join(dir, typeof entry === "string" ? entry : pkg.module ?? pkg.main ?? "index.js");
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Loads `@nuxt/kit` from the project's own Nuxt install rather than bundling one, so the
|
|
345
|
+
* config defaults match the project's Nuxt major version.
|
|
346
|
+
*/
|
|
347
|
+
async function loadNuxtKit(rootDir) {
|
|
348
|
+
let kitDir = findPackageDir(rootDir, "@nuxt/kit");
|
|
349
|
+
if (kitDir === null) {
|
|
350
|
+
const nuxtDir = findPackageDir(rootDir, "nuxt");
|
|
351
|
+
kitDir = nuxtDir === null ? null : findPackageDir(nuxtDir, "@nuxt/kit");
|
|
352
|
+
}
|
|
353
|
+
if (kitDir === null) return null;
|
|
354
|
+
return await import(pathToFileURL(packageEntry(kitDir)).href);
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/nuxt/validate-layers.ts
|
|
358
|
+
function assertUniqueNames(layers) {
|
|
359
|
+
const roots = /* @__PURE__ */ new Map();
|
|
360
|
+
for (const layer of layers) {
|
|
361
|
+
const other = roots.get(layer.name);
|
|
362
|
+
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.`);
|
|
363
|
+
roots.set(layer.name, layer.root);
|
|
364
|
+
}
|
|
365
|
+
return new Set(roots.keys());
|
|
366
|
+
}
|
|
367
|
+
function validateLayers(layers, config) {
|
|
368
|
+
const names = assertUniqueNames(layers);
|
|
369
|
+
for (const [name, rule] of Object.entries(config.layers ?? {})) {
|
|
370
|
+
if (!names.has(name)) {
|
|
371
|
+
const known = [...names].toSorted().join(", ");
|
|
372
|
+
throw new LayerConfigError(`Unknown layer "${name}" in config. Known layers: ${known}`);
|
|
373
|
+
}
|
|
374
|
+
const unknown = rule.allow?.find((allowed) => !names.has(allowed));
|
|
375
|
+
if (unknown !== void 0) throw new LayerConfigError(`layers.${name}.allow: unknown layer "${unknown}"`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/nuxt/layers.ts
|
|
380
|
+
function configuredPaths(rootDir, config) {
|
|
381
|
+
return Object.entries(config.layers ?? {}).flatMap(([name, rule]) => rule.path === void 0 ? [] : [[name, realPath(resolve(rootDir, rule.path))]]);
|
|
382
|
+
}
|
|
383
|
+
function configuredSources(config) {
|
|
384
|
+
return Object.entries(config.layers ?? {}).flatMap(([name, rule]) => rule.source === void 0 ? [] : [[name, rule.source]]);
|
|
385
|
+
}
|
|
386
|
+
function layersFromConfig(rootDir, config) {
|
|
387
|
+
const paths = configuredPaths(rootDir, config);
|
|
388
|
+
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.`);
|
|
389
|
+
const layers = paths.map(([name, root]) => layerFromPath(name, root));
|
|
390
|
+
const root = realPath(resolve(rootDir));
|
|
391
|
+
return layers.some((layer) => layer.root === root) ? layers : [layerFromPath(ROOT_LAYER_NAME, root), ...layers];
|
|
392
|
+
}
|
|
393
|
+
async function layersFromNuxt(kit, rootDir) {
|
|
394
|
+
let options = null;
|
|
395
|
+
try {
|
|
396
|
+
options = await kit.loadNuxtConfig({ cwd: rootDir });
|
|
397
|
+
} catch (error) {
|
|
398
|
+
throw new LayerscopeError(`Failed to load Nuxt config: ${error.message}`);
|
|
399
|
+
}
|
|
400
|
+
const dirs = kit.getLayerDirectories?.({ options }) ?? options._layers.map((layer, index) => deriveDirectories(layer, index, options));
|
|
401
|
+
return options._layers.map((layer, index) => layerFromNuxt(layer, index, dirs[index]));
|
|
402
|
+
}
|
|
403
|
+
/** `path` in config names the Nuxt layer at that root. */
|
|
404
|
+
function applyConfiguredNames(layers, rootDir, config) {
|
|
405
|
+
const named = layers.map((layer) => ({ ...layer }));
|
|
406
|
+
for (const [name, root] of configuredPaths(rootDir, config)) {
|
|
407
|
+
const layer = named.find((candidate) => candidate.root === root);
|
|
408
|
+
if (layer === void 0) throw new LayerscopeError(`layers.${name}.path: no Nuxt layer at ${root}`);
|
|
409
|
+
layer.name = name;
|
|
410
|
+
}
|
|
411
|
+
for (const [name, source] of configuredSources(config)) {
|
|
412
|
+
const matches = named.filter((candidate) => isClonedFrom(candidate.root, source));
|
|
413
|
+
if (matches.length !== 1) {
|
|
414
|
+
const found = matches.length === 0 ? "no layer cloned" : "several layers cloned";
|
|
415
|
+
throw new LayerscopeError(`layers.${name}.source: ${found} from "${source}". Set $meta.name in the layer's nuxt.config instead.`);
|
|
416
|
+
}
|
|
417
|
+
matches[0].name = name;
|
|
418
|
+
}
|
|
419
|
+
return named;
|
|
420
|
+
}
|
|
421
|
+
/** Applies `path` names from config to recorded layers and checks config against them. */
|
|
422
|
+
function nameLayers(layers, rootDir, config) {
|
|
423
|
+
const named = applyConfiguredNames(layers, rootDir, config);
|
|
424
|
+
validateLayers(named, config);
|
|
425
|
+
return named;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The ordered layer list, highest priority first. `recorded` are the layers the Nuxt module
|
|
429
|
+
* wrote to the registry; without them the layers come from the project's `@nuxt/kit`.
|
|
430
|
+
*/
|
|
431
|
+
async function resolveLayers(rootDir, config, recorded) {
|
|
432
|
+
let layers = recorded;
|
|
433
|
+
if (layers === void 0) {
|
|
434
|
+
const kit = await loadNuxtKit(rootDir);
|
|
435
|
+
layers = kit === null ? layersFromConfig(rootDir, config) : await layersFromNuxt(kit, rootDir);
|
|
436
|
+
}
|
|
437
|
+
return nameLayers(layers, rootDir, config);
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/nuxt/owner.ts
|
|
441
|
+
/** The layer whose root is the longest prefix of the file; files in node_modules have none. */
|
|
442
|
+
function createOwnerLookup(layers) {
|
|
443
|
+
const deepestFirst = layers.toSorted((a, b) => b.root.length - a.root.length);
|
|
444
|
+
return (file) => {
|
|
445
|
+
const layer = deepestFirst.find((candidate) => file.startsWith(`${candidate.root}/`));
|
|
446
|
+
if (layer === void 0) return null;
|
|
447
|
+
return file.slice(layer.root.length).includes("/node_modules/") ? null : layer;
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
export { readRegistry as a, boundaryFindings as c, loadConfig as d, loadConfigSync as f, createAnalysisEnv as i, isAllowed as l, nameLayers as n, tableFromRegistry as o, resolveLayers as r, loadSymbolTable as s, createOwnerLookup as t, findConfigFile as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as compareStrings } from "./strings-CrOjNZmB.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { l as isAllowed, t as createOwnerLookup } from "./owner-BA00V9VD.mjs";
|
|
3
3
|
import { relative } from "pathe";
|
|
4
4
|
//#region src/rules/edge-status.ts
|
|
5
5
|
/** How the layer rules see one edge. */
|
package/package.json
CHANGED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { glob } from "tinyglobby";
|
|
2
|
-
//#region src/registry/component-files.ts
|
|
3
|
-
/** Files in a component dir, matched the way Nuxt's components module scans it. */
|
|
4
|
-
async function globComponents(dir, pattern, ignore) {
|
|
5
|
-
return (await glob(pattern, {
|
|
6
|
-
cwd: dir,
|
|
7
|
-
absolute: true,
|
|
8
|
-
ignore
|
|
9
|
-
})).toSorted();
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
export { globComponents as t };
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { basename, dirname, join, resolve } from "pathe";
|
|
2
|
-
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
-
//#region src/errors.ts
|
|
4
|
-
/** A config or input problem; the CLI exits with code 2. */
|
|
5
|
-
var LayerscopeError = class extends Error {
|
|
6
|
-
constructor(..._args) {
|
|
7
|
-
super(..._args);
|
|
8
|
-
this.name = "LayerscopeError";
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
//#endregion
|
|
12
|
-
//#region src/layer-config-error.ts
|
|
13
|
-
/** A layer name in the config that matches no layer. */
|
|
14
|
-
var LayerConfigError = class extends LayerscopeError {
|
|
15
|
-
constructor(..._args) {
|
|
16
|
-
super(..._args);
|
|
17
|
-
this.name = "LayerConfigError";
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region src/config/rules.ts
|
|
22
|
-
const DEFAULT_SEVERITY = {
|
|
23
|
-
"layer-boundary": "error",
|
|
24
|
-
"unresolved-reference": "warn",
|
|
25
|
-
"shadowed-component": "warn"
|
|
26
|
-
};
|
|
27
|
-
/** Accepted in config for forward compatibility; not implemented yet. */
|
|
28
|
-
const RESERVED_RULES = /* @__PURE__ */ new Set(["unused-symbol"]);
|
|
29
|
-
const SEVERITIES = /* @__PURE__ */ new Set([
|
|
30
|
-
"off",
|
|
31
|
-
"warn",
|
|
32
|
-
"error"
|
|
33
|
-
]);
|
|
34
|
-
function isRuleName(name) {
|
|
35
|
-
return Object.hasOwn(DEFAULT_SEVERITY, name);
|
|
36
|
-
}
|
|
37
|
-
function ruleSeverity(config, rule) {
|
|
38
|
-
return config.rules?.[rule] ?? DEFAULT_SEVERITY[rule];
|
|
39
|
-
}
|
|
40
|
-
//#endregion
|
|
41
|
-
//#region src/config/validate.ts
|
|
42
|
-
function validateConfig(config, file) {
|
|
43
|
-
for (const [rule, severity] of Object.entries(config.rules ?? {})) {
|
|
44
|
-
if (!isRuleName(rule) && !RESERVED_RULES.has(rule)) throw new LayerscopeError(`${file}: unknown rule "${rule}"`);
|
|
45
|
-
if (!SEVERITIES.has(String(severity))) throw new LayerscopeError(`${file}: rule "${rule}" must be "off", "warn" or "error", got ${JSON.stringify(severity)}`);
|
|
46
|
-
}
|
|
47
|
-
for (const [name, layer] of Object.entries(config.layers ?? {})) {
|
|
48
|
-
if (layer.allow !== void 0 && !Array.isArray(layer.allow)) throw new LayerscopeError(`${file}: layers.${name}.allow must be an array of layer names`);
|
|
49
|
-
if (layer.path !== void 0 && layer.source !== void 0) throw new LayerscopeError(`${file}: layers.${name} sets both "path" and "source"; use one`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
//#endregion
|
|
53
|
-
//#region src/config/effective.ts
|
|
54
|
-
/** Where the module's options live when they come from `nuxt.config`. */
|
|
55
|
-
const NUXT_CONFIG_LABEL = "nuxt.config (layerscope)";
|
|
56
|
-
/** The keys that describe the project; `buildDir` only says where to look. */
|
|
57
|
-
const PROJECT_CONFIG_KEYS = [
|
|
58
|
-
"layers",
|
|
59
|
-
"rules",
|
|
60
|
-
"ignore",
|
|
61
|
-
"globals"
|
|
62
|
-
];
|
|
63
|
-
function hasProjectConfig(config) {
|
|
64
|
-
return PROJECT_CONFIG_KEYS.some((key) => config[key] !== void 0);
|
|
65
|
-
}
|
|
66
|
-
function pickProjectConfig(config) {
|
|
67
|
-
return Object.fromEntries(PROJECT_CONFIG_KEYS.flatMap((key) => config[key] === void 0 ? [] : [[key, config[key]]]));
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* `layerscope.config.*` or the `layerscope` key of `nuxt.config`, which the module records in the
|
|
71
|
-
* registry. Both at once is an error rather than a merge, so it is always clear which one applies.
|
|
72
|
-
*/
|
|
73
|
-
function effectiveConfig(fileConfig, recorded) {
|
|
74
|
-
if (recorded === void 0 || !hasProjectConfig(recorded)) return fileConfig;
|
|
75
|
-
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.");
|
|
76
|
-
validateConfig(recorded, NUXT_CONFIG_LABEL);
|
|
77
|
-
return {
|
|
78
|
-
...fileConfig,
|
|
79
|
-
...recorded
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/** Names the `nuxt.config` key in a layer error, when the settings that caused it came from there. */
|
|
83
|
-
function labelConfigError(error, recorded) {
|
|
84
|
-
if (error instanceof LayerConfigError && recorded !== void 0 && hasProjectConfig(recorded)) return new LayerscopeError(`${NUXT_CONFIG_LABEL}: ${error.message}`);
|
|
85
|
-
return error;
|
|
86
|
-
}
|
|
87
|
-
//#endregion
|
|
88
|
-
//#region src/registry/schema.ts
|
|
89
|
-
/** Bumped on breaking changes to `registry.json`; readers fall back to the `.d.ts` files. */
|
|
90
|
-
const REGISTRY_VERSION = 1;
|
|
91
|
-
/** Relative to the Nuxt build dir. */
|
|
92
|
-
const REGISTRY_FILE = "layerscope/registry.json";
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/utils/fs.ts
|
|
95
|
-
function isFile(path) {
|
|
96
|
-
try {
|
|
97
|
-
return statSync(path).isFile();
|
|
98
|
-
} catch {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
function readJson(path) {
|
|
103
|
-
return JSON.parse(readFileSync(path, "utf8").replaceAll(/^\s*\/\/.*$/gmu, ""));
|
|
104
|
-
}
|
|
105
|
-
/** Symlinks resolved (pnpm links packages), or the path itself when it does not exist. */
|
|
106
|
-
function realPath(path) {
|
|
107
|
-
try {
|
|
108
|
-
return realpathSync(path);
|
|
109
|
-
} catch {
|
|
110
|
-
return path;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
//#endregion
|
|
114
|
-
//#region src/nuxt/remote-layer.ts
|
|
115
|
-
/** c12 clones remote layers into `node_modules/.c12`, or into `~/.cache/c12` without one. */
|
|
116
|
-
const CLONE_DIRS = /* @__PURE__ */ new Set([".c12", "c12"]);
|
|
117
|
-
/**
|
|
118
|
-
* c12 names a clone after the first three word segments of its `extends` source plus a hash of
|
|
119
|
-
* the whole source (`github:org/repo#v1` → `github_org_repo_<hash>`). The hash changes with the
|
|
120
|
-
* ref, so names and config matching use the prefix only.
|
|
121
|
-
*/
|
|
122
|
-
function clonePrefix(source) {
|
|
123
|
-
return source.replaceAll(/\W+/gu, "_").split("_").slice(0, 3).join("_");
|
|
124
|
-
}
|
|
125
|
-
/** The clone name without its hash, so it survives ref changes; `null` for other layers. */
|
|
126
|
-
function remoteLayerName(root) {
|
|
127
|
-
if (!CLONE_DIRS.has(basename(dirname(root)))) return null;
|
|
128
|
-
const name = basename(root);
|
|
129
|
-
const hashStart = name.lastIndexOf("_");
|
|
130
|
-
return hashStart === -1 ? name : name.slice(0, hashStart);
|
|
131
|
-
}
|
|
132
|
-
/** Whether the layer at `root` was cloned from this `extends` source. */
|
|
133
|
-
function isClonedFrom(root, source) {
|
|
134
|
-
return remoteLayerName(root) === clonePrefix(source);
|
|
135
|
-
}
|
|
136
|
-
//#endregion
|
|
137
|
-
//#region src/nuxt/layer-factory.ts
|
|
138
|
-
const ROOT_LAYER_NAME = "root";
|
|
139
|
-
/** Fallback for `@nuxt/kit` releases without `getLayerDirectories`. */
|
|
140
|
-
function deriveDirectories(layer, index, options) {
|
|
141
|
-
const root = resolve(layer.config.rootDir ?? layer.cwd);
|
|
142
|
-
return {
|
|
143
|
-
root,
|
|
144
|
-
app: resolve(root, layer.config.srcDir ?? "."),
|
|
145
|
-
server: index === 0 && options.serverDir !== void 0 ? resolve(options.serverDir) : resolve(root, layer.config.serverDir ?? "server"),
|
|
146
|
-
shared: resolve(root, layer.config.dir?.shared ?? options.dir?.shared ?? "shared")
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
/** Real paths, so they match symlinked paths in generated types and imports. */
|
|
150
|
-
function toDirs(dirs) {
|
|
151
|
-
const dir = (path) => realPath(resolve(path));
|
|
152
|
-
return {
|
|
153
|
-
root: dir(dirs.root),
|
|
154
|
-
srcDir: dir(dirs.app),
|
|
155
|
-
serverDir: dir(dirs.server),
|
|
156
|
-
sharedDir: dir(dirs.shared)
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
function layerFromNuxt(layer, index, dirs) {
|
|
160
|
-
const resolved = toDirs(dirs);
|
|
161
|
-
return {
|
|
162
|
-
name: layer.meta?.name ?? layer.config.$meta?.name ?? (index === 0 ? "root" : remoteLayerName(resolved.root) ?? basename(resolved.root)),
|
|
163
|
-
...resolved,
|
|
164
|
-
defaultComponents: layer.config.components === void 0
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/** Without @nuxt/kit, assume Nuxt defaults: `app/` as srcDir when it exists (Nuxt 4). */
|
|
168
|
-
function layerFromPath(name, root) {
|
|
169
|
-
const appDir = join(root, "app");
|
|
170
|
-
return {
|
|
171
|
-
name,
|
|
172
|
-
...toDirs({
|
|
173
|
-
root,
|
|
174
|
-
app: existsSync(appDir) ? appDir : root,
|
|
175
|
-
server: join(root, "server"),
|
|
176
|
-
shared: join(root, "shared")
|
|
177
|
-
}),
|
|
178
|
-
defaultComponents: true
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
//#endregion
|
|
182
|
-
export { LayerConfigError as _, isClonedFrom as a, realPath as c, effectiveConfig as d, labelConfigError as f, ruleSeverity as g, isRuleName as h, layerFromPath as i, REGISTRY_FILE as l, validateConfig as m, deriveDirectories as n, isFile as o, pickProjectConfig as p, layerFromNuxt as r, readJson as s, ROOT_LAYER_NAME as t, REGISTRY_VERSION as u, LayerscopeError as v };
|
package/dist/report-CKhXT50W.mjs
DELETED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as summarize } from "./summary-DiUYKMPe.mjs";
|
|
2
1
|
import { i as plural } from "./strings-CrOjNZmB.mjs";
|
|
2
|
+
import { t as summarize } from "./summary-DiUYKMPe.mjs";
|
|
3
3
|
import { relative } from "pathe";
|
|
4
4
|
import { styleText } from "node:util";
|
|
5
5
|
//#region src/utils/style.ts
|