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.
@@ -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 { fileURLToPath, pathToFileURL } from "node:url";
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
- //#region src/config/load.ts
13
- const CONFIG_NAMES = [
14
- "layerscope.config.ts",
15
- "layerscope.config.mts",
16
- "layerscope.config.js",
17
- "layerscope.config.mjs"
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/resolve/package-name.ts
131
- const NODE_MODULES = "/node_modules/";
132
- /** Package name of a bare specifier or of a path inside node_modules. */
133
- function packageName(specifier) {
134
- const index = specifier.lastIndexOf(NODE_MODULES);
135
- const bare = index === -1 ? specifier : specifier.slice(index + 14);
136
- const parts = bare.split("/");
137
- return bare.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
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/nuxt/symbols.ts
141
- const GLOBAL_CONST = /^ {2}const ([\w$]+): (.*)$/u;
142
- const EXPORT_CONST = /^export const ([\w$]+): (.*)$/u;
143
- const INTERFACE_MEMBER = /^ {2}([\w$]+): (.*)$/u;
144
- const TYPEOF_IMPORT = /typeof import\((['"])(.+?)\1\)/u;
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
- /** Target of an absolute path or a bare module specifier. */
150
- function targetOf(source, buildDir) {
151
- if (!isAbsolute(source)) return {
152
- file: null,
153
- module: packageName(source)
154
- };
155
- if (source === buildDir || source.startsWith(`${buildDir}/`)) return {
156
- file: null,
157
- module: "#build"
158
- };
159
- if (source.includes("/node_modules/")) {
160
- const file = resolveFile(source);
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/registry/read.ts
255
- function readRegistry(buildDir) {
256
- const file = join(buildDir, REGISTRY_FILE);
257
- if (!existsSync(file)) return {
258
- kind: "missing",
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
- return {
286
- imports: {
287
- app: importMap(registry.imports.app, buildDir),
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/analyze/known-globals.ts
322
- const SFC_MACROS = [
323
- "defineProps",
324
- "defineEmits",
325
- "defineExpose",
326
- "defineOptions",
327
- "defineSlots",
328
- "defineModel",
329
- "withDefaults"
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
- /** Defined on `globalThis` by Nuxt (app) and Nitro (server), so absent from the registry there. */
332
- const RUNTIME_GLOBALS = ["$fetch"];
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 packageEntry(dir) {
374
- const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
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
- * Loads `@nuxt/kit` from the project's own Nuxt install rather than bundling one, so the
384
- * config defaults match the project's Nuxt major version.
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
- async function loadNuxtKit(rootDir) {
387
- let kitDir = findPackageDir(rootDir, "@nuxt/kit");
388
- if (kitDir === null) {
389
- const nuxtDir = findPackageDir(rootDir, "nuxt");
390
- kitDir = nuxtDir === null ? null : findPackageDir(nuxtDir, "@nuxt/kit");
391
- }
392
- if (kitDir === null) return null;
393
- return await import(pathToFileURL(packageEntry(kitDir)).href);
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
- function validateLayers(layers, config) {
407
- const names = assertUniqueNames(layers);
408
- for (const [name, rule] of Object.entries(config.layers ?? {})) {
409
- if (!names.has(name)) {
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/nuxt/layers.ts
419
- function configuredPaths(rootDir, config) {
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
- options = await kit.loadNuxtConfig({ cwd: rootDir });
436
- } catch (error) {
437
- throw new LayerscopeError(`Failed to load Nuxt config: ${error.message}`);
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
- /** Applies `path` names from config to recorded layers and checks config against them. */
461
- function nameLayers(layers, rootDir, config) {
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
- * The ordered layer list, highest priority first. `recorded` are the layers the Nuxt module
468
- * wrote to the registry; without them the layers come from the project's `@nuxt/kit`.
469
- */
470
- async function resolveLayers(rootDir, config, recorded) {
471
- let layers = recorded;
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
- return isAbsolute(target) ? resolvePath(path, buildDir) : {
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
- export { findConfigFile as _, lineColumn as a, createOwnerLookup as c, createAnalysisEnv as d, readRegistry as f, isAllowed as g, boundaryFindings as h, isCheckedFile as i, nameLayers as l, loadSymbolTable as m, analyzeFile as n, offsetAt as o, tableFromRegistry as p, collectFiles as r, scanFile as s, FileAnalysis as t, resolveLayers as u, loadConfig as v, loadConfigSync as y };
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 };