nuxt-layerscope 0.1.2 → 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 +23 -0
- package/dist/{analyze-QaYmBXyK.mjs → analyze-BwmKhroS.mjs} +6 -99
- package/dist/analyze-vZUz30sN.mjs +2 -0
- package/dist/api.d.mts +100 -4
- package/dist/api.mjs +5 -4
- package/dist/bin.mjs +23 -14
- 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/{report-BNW0GCl9.mjs → report-jEMtNXS8.mjs} +27 -13
- 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-DtAi0vPb.mjs +0 -2
package/dist/eslint.mjs
CHANGED
|
@@ -1,68 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as packageVersion } from "./version-
|
|
3
|
-
import {
|
|
1
|
+
import { c as FileAnalysis, g as scanFile, h as offsetAt, m as lineColumn, o as REGISTRY_FILE, p as isCheckedFile, y as effectiveConfig } from "./layer-factory-DVSf2Wj4.mjs";
|
|
2
|
+
import { t as packageVersion } from "./version-D5JrgoO_.mjs";
|
|
3
|
+
import { a as readRegistry, c as boundaryFindings, f as loadConfigSync, i as createAnalysisEnv, n as nameLayers, o as tableFromRegistry, t as createOwnerLookup, u as findConfigFile } from "./owner-BA00V9VD.mjs";
|
|
4
4
|
import { dirname, join, relative, resolve } from "pathe";
|
|
5
5
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
-
|
|
7
|
-
/** Both rules lint the same text, so the last result per file is reused. */
|
|
8
|
-
const lastLint = /* @__PURE__ */ new Map();
|
|
9
|
-
/**
|
|
10
|
-
* oxlint passes only the `<script>` of a `.vue` file to JS plugins, while the template matters
|
|
11
|
-
* too; the saved file is read instead.
|
|
12
|
-
*/
|
|
13
|
-
function fullSource(file, text) {
|
|
14
|
-
return file.endsWith(".vue") && !/<(?:script|template)[\s>]/u.test(text) ? readFileSync(file, "utf8") : text;
|
|
15
|
-
}
|
|
16
|
-
/** Lints the text the editor holds, which may differ from the file on disk. */
|
|
17
|
-
function lintFile(file, text, project) {
|
|
18
|
-
const source = fullSource(file, text);
|
|
19
|
-
const cached = lastLint.get(file);
|
|
20
|
-
if (cached?.source === source) return cached.lint;
|
|
21
|
-
const layer = project.env.ownerOf(file);
|
|
22
|
-
let lint = {
|
|
23
|
-
source,
|
|
24
|
-
boundary: [],
|
|
25
|
-
unresolved: []
|
|
26
|
-
};
|
|
27
|
-
if (layer !== null && isCheckedFile(file, layer, project.config.ignore ?? [])) {
|
|
28
|
-
const analysis = new FileAnalysis(file, source, layer, project.env).run(scanFile(source, file));
|
|
29
|
-
lint = {
|
|
30
|
-
source,
|
|
31
|
-
boundary: boundaryFindings(analysis.edges, project.config),
|
|
32
|
-
unresolved: analysis.unresolved
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
lastLint.set(file, {
|
|
36
|
-
source,
|
|
37
|
-
lint
|
|
38
|
-
});
|
|
39
|
-
return lint;
|
|
40
|
-
}
|
|
41
|
-
//#endregion
|
|
42
|
-
//#region src/eslint/location.ts
|
|
43
|
-
/**
|
|
44
|
-
* Maps a finding from the full file into the text the linter passed. ESLint passes the full
|
|
45
|
-
* file; oxlint passes only a `.vue` file's `<script>`, where template findings have no position.
|
|
46
|
-
*/
|
|
47
|
-
function reportLocation(finding, source, text) {
|
|
48
|
-
if (source === text) return {
|
|
49
|
-
line: finding.line,
|
|
50
|
-
column: finding.column,
|
|
51
|
-
outside: false
|
|
52
|
-
};
|
|
53
|
-
const base = source.indexOf(text);
|
|
54
|
-
const offset = offsetAt(source, finding) - base;
|
|
55
|
-
if (base === -1 || offset < 0 || offset > text.length) return {
|
|
56
|
-
line: 1,
|
|
57
|
-
column: 1,
|
|
58
|
-
outside: true
|
|
59
|
-
};
|
|
60
|
-
return {
|
|
61
|
-
...lineColumn(text, offset),
|
|
62
|
-
outside: false
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
//#endregion
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
66
7
|
//#region src/eslint/load-project.ts
|
|
67
8
|
/** Config, layers and symbols of a project, from its registry; Nuxt itself is never loaded. */
|
|
68
9
|
function loadProject(rootDir) {
|
|
@@ -98,8 +39,9 @@ function loadProject(rootDir) {
|
|
|
98
39
|
const DEFAULT_REGISTRY = join(".nuxt", REGISTRY_FILE);
|
|
99
40
|
/** Editors keep the linter running, so a project is reloaded when its inputs change. */
|
|
100
41
|
const cache = /* @__PURE__ */ new Map();
|
|
101
|
-
|
|
102
|
-
|
|
42
|
+
/** The nearest directory from `startDir` up that holds a registry. */
|
|
43
|
+
function findProjectRoot(startDir) {
|
|
44
|
+
for (let dir = startDir;; dir = dirname(dir)) {
|
|
103
45
|
if (existsSync(join(dir, DEFAULT_REGISTRY))) return dir;
|
|
104
46
|
if (dirname(dir) === dir) return null;
|
|
105
47
|
}
|
|
@@ -119,7 +61,7 @@ function tryLoadProject(rootDir) {
|
|
|
119
61
|
}
|
|
120
62
|
/** The Nuxt project a linted file belongs to, from its registry; Nuxt itself is never loaded. */
|
|
121
63
|
function projectFor(file, root) {
|
|
122
|
-
const rootDir = root === void 0 ?
|
|
64
|
+
const rootDir = root === void 0 ? findProjectRoot(dirname(file)) : resolve(root);
|
|
123
65
|
if (rootDir === null) return {
|
|
124
66
|
ok: false,
|
|
125
67
|
message: `No ${DEFAULT_REGISTRY} above this file. Run "nuxi prepare", or set the rule's "root" option to the Nuxt project.`
|
|
@@ -137,6 +79,101 @@ function projectFor(file, root) {
|
|
|
137
79
|
return load;
|
|
138
80
|
}
|
|
139
81
|
//#endregion
|
|
82
|
+
//#region src/eslint/stamp.ts
|
|
83
|
+
function digest(file) {
|
|
84
|
+
return file !== null && existsSync(file) ? createHash("sha1").update(readFileSync(file)).digest("hex") : "";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* What layerscope's results depend on besides the linted file: the registry and the layer config.
|
|
88
|
+
* ESLint's cache keys a result on the file and the ESLint config only, so this goes into the
|
|
89
|
+
* config, which drops cached results when it changes. Content, not mtime: `nuxi prepare` rewrites
|
|
90
|
+
* an unchanged registry.
|
|
91
|
+
*/
|
|
92
|
+
function projectStamp(cwd = process.cwd()) {
|
|
93
|
+
const root = findProjectRoot(cwd);
|
|
94
|
+
if (root === null) return "";
|
|
95
|
+
const parts = [join(root, DEFAULT_REGISTRY), findConfigFile(root)].map((file) => digest(file));
|
|
96
|
+
return createHash("sha1").update(parts.join(":")).digest("hex");
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/eslint/recommended.ts
|
|
100
|
+
/** Built when `eslint.config.*` loads, so `--cache` sees the stamp of this run. */
|
|
101
|
+
function createRecommended(plugin, cwd) {
|
|
102
|
+
return {
|
|
103
|
+
name: "nuxt-layerscope/recommended",
|
|
104
|
+
plugins: { layerscope: plugin },
|
|
105
|
+
settings: { layerscope: { stamp: projectStamp(cwd) } },
|
|
106
|
+
rules: {
|
|
107
|
+
"layerscope/layer-boundary": "error",
|
|
108
|
+
"layerscope/unresolved-reference": "warn"
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/eslint/lint-file.ts
|
|
114
|
+
/**
|
|
115
|
+
* Both rules lint the same text, so the last result per file is reused. A reloaded project
|
|
116
|
+
* (registry or config changed) is another object, so results from before it are not reused.
|
|
117
|
+
*/
|
|
118
|
+
const lastLint = /* @__PURE__ */ new Map();
|
|
119
|
+
/**
|
|
120
|
+
* oxlint passes only the `<script>` of a `.vue` file to JS plugins, while the template matters
|
|
121
|
+
* too; the saved file is read instead.
|
|
122
|
+
*/
|
|
123
|
+
function fullSource(file, text) {
|
|
124
|
+
return file.endsWith(".vue") && !/<(?:script|template)[\s>]/u.test(text) ? readFileSync(file, "utf8") : text;
|
|
125
|
+
}
|
|
126
|
+
/** Lints the text the editor holds, which may differ from the file on disk. */
|
|
127
|
+
function lintFile(file, text, project) {
|
|
128
|
+
const source = fullSource(file, text);
|
|
129
|
+
const cached = lastLint.get(file);
|
|
130
|
+
if (cached?.project === project && cached.source === source) return cached.lint;
|
|
131
|
+
const layer = project.env.ownerOf(file);
|
|
132
|
+
let lint = {
|
|
133
|
+
source,
|
|
134
|
+
boundary: [],
|
|
135
|
+
unresolved: []
|
|
136
|
+
};
|
|
137
|
+
if (layer !== null && isCheckedFile(file, layer, project.config.ignore ?? [])) {
|
|
138
|
+
const analysis = new FileAnalysis(file, source, layer, project.env).run(scanFile(source, file));
|
|
139
|
+
lint = {
|
|
140
|
+
source,
|
|
141
|
+
boundary: boundaryFindings(analysis.edges, project.config),
|
|
142
|
+
unresolved: analysis.unresolved
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
lastLint.set(file, {
|
|
146
|
+
project,
|
|
147
|
+
source,
|
|
148
|
+
lint
|
|
149
|
+
});
|
|
150
|
+
return lint;
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/eslint/location.ts
|
|
154
|
+
/**
|
|
155
|
+
* Maps a finding from the full file into the text the linter passed. ESLint passes the full
|
|
156
|
+
* file; oxlint passes only a `.vue` file's `<script>`, where template findings have no position.
|
|
157
|
+
*/
|
|
158
|
+
function reportLocation(finding, source, text) {
|
|
159
|
+
if (source === text) return {
|
|
160
|
+
line: finding.line,
|
|
161
|
+
column: finding.column,
|
|
162
|
+
outside: false
|
|
163
|
+
};
|
|
164
|
+
const base = source.indexOf(text);
|
|
165
|
+
const offset = offsetAt(source, finding) - base;
|
|
166
|
+
if (base === -1 || offset < 0 || offset > text.length) return {
|
|
167
|
+
line: 1,
|
|
168
|
+
column: 1,
|
|
169
|
+
outside: true
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
...lineColumn(text, offset),
|
|
173
|
+
outside: false
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
140
177
|
//#region src/eslint/rules.ts
|
|
141
178
|
const OPTIONS_SCHEMA = [{
|
|
142
179
|
type: "object",
|
|
@@ -201,13 +238,6 @@ const plugin = {
|
|
|
201
238
|
rules,
|
|
202
239
|
configs: {}
|
|
203
240
|
};
|
|
204
|
-
plugin.configs.recommended =
|
|
205
|
-
name: "nuxt-layerscope/recommended",
|
|
206
|
-
plugins: { layerscope: plugin },
|
|
207
|
-
rules: {
|
|
208
|
-
"layerscope/layer-boundary": "error",
|
|
209
|
-
"layerscope/unresolved-reference": "warn"
|
|
210
|
-
}
|
|
211
|
-
};
|
|
241
|
+
plugin.configs.recommended = createRecommended(plugin);
|
|
212
242
|
//#endregion
|
|
213
243
|
export { plugin as default };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as Context, c as Layer, d as Reference, f as ReferenceKind, g as SourceOption, h as Severity, i as BaselineResult, l as LayerRule, m as RuleName, n as AnalyzeResult, o as Edge, p as ResolutionSource, r as BaselineEntry, s as Finding, t as defineConfig, u as LayerscopeConfig } from "./define-
|
|
2
|
-
import { t as ProjectConfig } from "./effective-
|
|
1
|
+
import { a as Context, c as Layer, d as Reference, f as ReferenceKind, g as SourceOption, h as Severity, i as BaselineResult, l as LayerRule, m as RuleName, n as AnalyzeResult, o as Edge, p as ResolutionSource, r as BaselineEntry, s as Finding, t as defineConfig, u as LayerscopeConfig } from "./define-DDg4HQRH.mjs";
|
|
2
|
+
import { t as ProjectConfig } from "./effective-BPMNHe9A.mjs";
|
|
3
3
|
//#region src/module/index.d.ts
|
|
4
4
|
interface ModuleOptions extends ProjectConfig {
|
|
5
5
|
/** Write `.nuxt/layerscope/registry.json`. Defaults to `true`. */
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as summarize } from "./summary-DiUYKMPe.mjs";
|
|
1
|
+
import { o as REGISTRY_FILE, r as layerFromNuxt, x as pickProjectConfig } from "./layer-factory-DVSf2Wj4.mjs";
|
|
3
2
|
import { i as plural, t as compareStrings } from "./strings-CrOjNZmB.mjs";
|
|
4
|
-
import { t as
|
|
5
|
-
import { t as
|
|
3
|
+
import { i as BASELINE_FILE, n as AnalysisCache, t as globComponents } from "./component-files-B_qvkKwv.mjs";
|
|
4
|
+
import { t as summarize } from "./summary-DiUYKMPe.mjs";
|
|
5
|
+
import { t as packageVersion } from "./version-D5JrgoO_.mjs";
|
|
6
6
|
import { t as defineConfig } from "./define-l62B9fVk.mjs";
|
|
7
7
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
8
8
|
import { addDevServerHandler, defineNuxtModule, getLayerDirectories } from "@nuxt/kit";
|
|
9
9
|
import { dirname, isAbsolute, join, relative, resolve } from "pathe";
|
|
10
|
-
import {
|
|
10
|
+
import { readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { defineEventHandler, getHeader, getQuery, setResponseHeader, setResponseStatus } from "h3";
|
|
11
13
|
//#region src/module/nuxt.ts
|
|
12
14
|
/** `nitro:init` is typed by Nuxt's Nitro integration, not by `@nuxt/schema`. */
|
|
13
15
|
function onNitroInit(nuxt, handler) {
|
|
@@ -18,6 +20,111 @@ function onDevtoolsCustomTabs(nuxt, handler) {
|
|
|
18
20
|
nuxt.hook("devtools:customTabs", handler);
|
|
19
21
|
}
|
|
20
22
|
//#endregion
|
|
23
|
+
//#region src/devtools/analyzer.ts
|
|
24
|
+
/** The output the revision tracks; the symbol table and edges are large and derived from it. */
|
|
25
|
+
function fingerprint(result) {
|
|
26
|
+
return JSON.stringify([
|
|
27
|
+
result.findings,
|
|
28
|
+
result.baseline?.suppressed,
|
|
29
|
+
result.layers.map((layer) => layer.name),
|
|
30
|
+
result.files.length,
|
|
31
|
+
result.notes
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
/** Analyzes lazily, shares one run between concurrent callers, and reuses per-file work. */
|
|
35
|
+
var Analyzer = class {
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.id = randomUUID();
|
|
38
|
+
this.cache = new AnalysisCache();
|
|
39
|
+
this.lastFingerprint = "";
|
|
40
|
+
this.dirty = true;
|
|
41
|
+
this.options = options;
|
|
42
|
+
}
|
|
43
|
+
get current() {
|
|
44
|
+
return this.snapshot;
|
|
45
|
+
}
|
|
46
|
+
/** Marks the snapshot stale; the next `get` re-analyzes. Does no work itself. */
|
|
47
|
+
invalidate() {
|
|
48
|
+
this.dirty = true;
|
|
49
|
+
}
|
|
50
|
+
async get() {
|
|
51
|
+
if (this.snapshot !== void 0 && !this.dirty) return this.snapshot;
|
|
52
|
+
return await (this.inflight ?? this.start());
|
|
53
|
+
}
|
|
54
|
+
/** Always analyzes after the call: a run already in flight may predate the request. */
|
|
55
|
+
async refresh() {
|
|
56
|
+
if (this.inflight === void 0) return await this.start();
|
|
57
|
+
this.queued ??= this.afterInflight();
|
|
58
|
+
return await this.queued;
|
|
59
|
+
}
|
|
60
|
+
async afterInflight() {
|
|
61
|
+
await Promise.allSettled([this.inflight]);
|
|
62
|
+
this.queued = void 0;
|
|
63
|
+
return await this.refresh();
|
|
64
|
+
}
|
|
65
|
+
async start() {
|
|
66
|
+
this.inflight = this.analyze().finally(() => {
|
|
67
|
+
this.inflight = void 0;
|
|
68
|
+
});
|
|
69
|
+
return await this.inflight;
|
|
70
|
+
}
|
|
71
|
+
async analyze() {
|
|
72
|
+
this.dirty = false;
|
|
73
|
+
const started = Date.now();
|
|
74
|
+
try {
|
|
75
|
+
const result = await (this.options.run ?? (await import("./analyze-vZUz30sN.mjs")).analyze)({
|
|
76
|
+
rootDir: this.options.rootDir,
|
|
77
|
+
baseline: this.options.baseline,
|
|
78
|
+
cache: this.cache,
|
|
79
|
+
envKey: await this.options.envKey()
|
|
80
|
+
});
|
|
81
|
+
const next = fingerprint(result);
|
|
82
|
+
const changed = this.snapshot !== void 0 && next !== this.lastFingerprint;
|
|
83
|
+
this.lastFingerprint = next;
|
|
84
|
+
this.snapshot = {
|
|
85
|
+
id: this.id,
|
|
86
|
+
rev: (this.snapshot?.rev ?? 0) + (changed ? 1 : 0),
|
|
87
|
+
analyzedAt: Date.now(),
|
|
88
|
+
durationMs: Date.now() - started,
|
|
89
|
+
result
|
|
90
|
+
};
|
|
91
|
+
return this.snapshot;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.dirty = true;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/devtools/env-key.ts
|
|
100
|
+
const WATCHED = [
|
|
101
|
+
"layerscope/registry.json",
|
|
102
|
+
"imports.d.ts",
|
|
103
|
+
"components.d.ts",
|
|
104
|
+
"tsconfig.json",
|
|
105
|
+
"tsconfig.app.json",
|
|
106
|
+
"tsconfig.server.json",
|
|
107
|
+
"tsconfig.shared.json",
|
|
108
|
+
"types/imports.d.ts",
|
|
109
|
+
"types/nitro-imports.d.ts",
|
|
110
|
+
"types/shared-imports.d.ts",
|
|
111
|
+
"types/components.d.ts"
|
|
112
|
+
];
|
|
113
|
+
const CONFIG_PATTERN = /^(nuxt|layerscope)\.config\./u;
|
|
114
|
+
function stamp(file) {
|
|
115
|
+
try {
|
|
116
|
+
const { mtimeMs, size } = statSync(file);
|
|
117
|
+
return `${file}:${mtimeMs}:${size}`;
|
|
118
|
+
} catch {
|
|
119
|
+
return `${file}:missing`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Changes when the registry, generated types or config change, which invalidates cached analyses. */
|
|
123
|
+
function computeEnvKey(rootDir, buildDir) {
|
|
124
|
+
const configs = readdirSync(rootDir).filter((name) => CONFIG_PATTERN.test(name));
|
|
125
|
+
return [...WATCHED.map((name) => stamp(join(buildDir, name))), ...configs.map((name) => stamp(join(rootDir, name)))].join("|");
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
21
128
|
//#region src/devtools/html.ts
|
|
22
129
|
const ENTITIES = {
|
|
23
130
|
"&": "&",
|
|
@@ -89,7 +196,9 @@ function findingsList(input) {
|
|
|
89
196
|
function summaryLine(result) {
|
|
90
197
|
const { errors, warnings } = summarize(result.findings);
|
|
91
198
|
const source = result.source === "registry" ? "module registry" : "generated .d.ts files";
|
|
92
|
-
|
|
199
|
+
const suppressed = result.baseline?.suppressed.length ?? 0;
|
|
200
|
+
const baseline = suppressed === 0 ? "" : ` · ${suppressed} in baseline`;
|
|
201
|
+
return `${plural(result.files.length, "file")} in ${plural(result.layers.length, "layer")} · ${plural(errors, "error")}, ${plural(warnings, "warning")}${baseline} · symbols from the ${source}`;
|
|
93
202
|
}
|
|
94
203
|
function document(body) {
|
|
95
204
|
return `<!doctype html><html><head><meta charset="utf-8"><title>layerscope</title><style>${PAGE_STYLE}</style></head><body>${body}<script>${PAGE_SCRIPT}<\/script></body></html>`;
|
|
@@ -103,30 +212,101 @@ function renderError(message) {
|
|
|
103
212
|
return document(`<header><h1>layerscope</h1><button data-reload>Re-run</button></header><p class="error-message">${escapeHtml(message)}</p>`);
|
|
104
213
|
}
|
|
105
214
|
//#endregion
|
|
106
|
-
//#region src/devtools/
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
215
|
+
//#region src/devtools/handler.ts
|
|
216
|
+
const JSON_TYPE = "application/json";
|
|
217
|
+
const HTML_TYPE = "text/html; charset=utf-8";
|
|
218
|
+
function meta({ rev, analyzedAt, durationMs }) {
|
|
219
|
+
return {
|
|
220
|
+
rev,
|
|
221
|
+
analyzedAt,
|
|
222
|
+
durationMs
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async function reportOf(snapshot) {
|
|
226
|
+
const { formatResult } = await import("./report-DG-XzEq4.mjs");
|
|
227
|
+
const { rootDir } = snapshot.result;
|
|
228
|
+
return JSON.parse(formatResult(snapshot.result, "json", rootDir));
|
|
229
|
+
}
|
|
230
|
+
function json(event, status, body) {
|
|
231
|
+
setResponseStatus(event, status);
|
|
232
|
+
setResponseHeader(event, "content-type", JSON_TYPE);
|
|
233
|
+
return JSON.stringify(body);
|
|
234
|
+
}
|
|
235
|
+
async function page(event, input) {
|
|
236
|
+
const snapshot = await input.analyzer.refresh();
|
|
237
|
+
if (getQuery(event).format === "json") {
|
|
238
|
+
setResponseHeader(event, "content-type", JSON_TYPE);
|
|
239
|
+
return JSON.stringify(await reportOf(snapshot));
|
|
240
|
+
}
|
|
241
|
+
setResponseHeader(event, "content-type", HTML_TYPE);
|
|
113
242
|
return renderPage({
|
|
114
|
-
result,
|
|
115
|
-
openInEditor:
|
|
243
|
+
result: snapshot.result,
|
|
244
|
+
openInEditor: input.openInEditor
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function withReport(snapshot) {
|
|
248
|
+
return {
|
|
249
|
+
...meta(snapshot),
|
|
250
|
+
report: await reportOf(snapshot)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function api(event, path, input) {
|
|
254
|
+
if (path === "/api/rerun") return event.method === "POST" ? json(event, 200, await withReport(await input.analyzer.refresh())) : json(event, 405, { error: "Use POST" });
|
|
255
|
+
if (path !== "/api/state" && path !== "/api/report") return json(event, 404, { error: `Not found: ${path}` });
|
|
256
|
+
if (event.method !== "GET") return json(event, 405, { error: "Use GET" });
|
|
257
|
+
const snapshot = await input.analyzer.get();
|
|
258
|
+
const etag = `"${snapshot.id}-${snapshot.rev}"`;
|
|
259
|
+
setResponseHeader(event, "etag", etag);
|
|
260
|
+
setResponseHeader(event, "cache-control", "no-cache");
|
|
261
|
+
if (getHeader(event, "if-none-match") === etag) {
|
|
262
|
+
setResponseStatus(event, 304);
|
|
263
|
+
return "";
|
|
264
|
+
}
|
|
265
|
+
return json(event, 200, path === "/api/state" ? {
|
|
266
|
+
...meta(snapshot),
|
|
267
|
+
status: "ready"
|
|
268
|
+
} : await withReport(snapshot));
|
|
269
|
+
}
|
|
270
|
+
/** The DevTools tab, mounted at `/__layerscope`: the page, its JSON report and `/api/*`. */
|
|
271
|
+
function createDevtoolsHandler(input) {
|
|
272
|
+
return defineEventHandler(async (event) => {
|
|
273
|
+
const path = event.path.split("?")[0] ?? "/";
|
|
274
|
+
const isPage = path === "/" || path === "";
|
|
275
|
+
try {
|
|
276
|
+
return isPage ? await page(event, input) : await api(event, path, input);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
const message = error.message;
|
|
279
|
+
if (isPage) {
|
|
280
|
+
setResponseHeader(event, "content-type", HTML_TYPE);
|
|
281
|
+
return renderError(message);
|
|
282
|
+
}
|
|
283
|
+
return json(event, 500, { error: message });
|
|
284
|
+
}
|
|
116
285
|
});
|
|
117
286
|
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/devtools/index.ts
|
|
289
|
+
const DEVTOOLS_ROUTE = "/__layerscope";
|
|
118
290
|
/** A Nuxt DevTools tab with the layer report, served while `nuxi dev` runs. */
|
|
119
291
|
function setupDevtools(nuxt) {
|
|
292
|
+
const { rootDir, buildDir, app } = nuxt.options;
|
|
293
|
+
let analyzer;
|
|
294
|
+
const invalidate = () => analyzer?.invalidate();
|
|
295
|
+
nuxt.hook("app:templatesGenerated", invalidate);
|
|
296
|
+
nuxt.hook("builder:watch", invalidate);
|
|
297
|
+
const openInEditor = join("/", app.baseURL, app.buildAssetsDir, "__open-in-editor");
|
|
120
298
|
addDevServerHandler({
|
|
121
299
|
route: DEVTOOLS_ROUTE,
|
|
122
|
-
handler:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
300
|
+
handler: createDevtoolsHandler({
|
|
301
|
+
get analyzer() {
|
|
302
|
+
analyzer ??= new Analyzer({
|
|
303
|
+
rootDir,
|
|
304
|
+
baseline: BASELINE_FILE,
|
|
305
|
+
envKey: () => computeEnvKey(rootDir, buildDir)
|
|
306
|
+
});
|
|
307
|
+
return analyzer;
|
|
308
|
+
},
|
|
309
|
+
openInEditor
|
|
130
310
|
})
|
|
131
311
|
});
|
|
132
312
|
onDevtoolsCustomTabs(nuxt, (tabs) => {
|