plexus-peers 0.2.0 → 0.2.2
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/README.md +2 -2
- package/bin/plexus.js +11 -1
- package/lib/fix.js +18 -6
- package/lib/npm.js +66 -9
- package/lib/render-html.js +228 -82
- package/lib/run-analysis.js +20 -5
- package/lib/server.js +18 -0
- package/lib/terminal.js +5 -1
- package/package.json +9 -3
- package/public/index.html +322 -51
- package/public/plexus.css +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Plexus
|
|
2
2
|
|
|
3
|
-
CLI and a small web UI to inspect **direct dependencies** from a `package.json`: peer dependency ranges vs what is installed, optional **
|
|
3
|
+
CLI and a small web UI to inspect **direct dependencies** from a `package.json`: peer dependency ranges vs what is installed, optional **[bundlejs.com](https://bundlejs.com/)** bundled gzip hints (esbuild), and a **`--fix`** mode that queries npm for upgrade and cascade notes.
|
|
4
4
|
|
|
5
5
|
Requires **Node.js 18+** (uses the built-in `fetch` API).
|
|
6
6
|
|
|
@@ -37,7 +37,7 @@ npx plexus-peers -f ./path/to/package.json
|
|
|
37
37
|
| `--out <path>` | HTML output path (with `--html`) |
|
|
38
38
|
| `--conflicts-only` | Only list packages that have peer issues |
|
|
39
39
|
| `--fix` | Call `npm` for latest metadata and print resolution hints (chatty; avoid on CI if logs matter) |
|
|
40
|
-
| `--bundlesize` | With `--html`, query [
|
|
40
|
+
| `--bundlesize` | With `--html`, query [bundlejs.com](https://bundlejs.com/) per direct dependency (slow) |
|
|
41
41
|
| `--pkg <name>` | Focus on one direct dependency and related peer rows |
|
|
42
42
|
|
|
43
43
|
Examples:
|
package/bin/plexus.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { color } = require('../lib/ansi');
|
|
5
6
|
const { runFilesystemAnalysis } = require('../lib/engine');
|
|
6
7
|
const { startServer } = require('../lib/server');
|
|
7
8
|
|
|
@@ -20,7 +21,7 @@ Options:
|
|
|
20
21
|
--out <path> HTML output path (with --html)
|
|
21
22
|
--conflicts-only Terminal: only packages with peer issues
|
|
22
23
|
--fix Query npm for upgrade / cascade hints (noisy on CI)
|
|
23
|
-
--bundlesize With --html: fetch gzip sizes from
|
|
24
|
+
--bundlesize With --html: fetch bundled gzip sizes from bundlejs.com (slow)
|
|
24
25
|
--pkg <name> Focus on one direct dependency and its peers
|
|
25
26
|
|
|
26
27
|
Serve:
|
|
@@ -125,6 +126,15 @@ async function main() {
|
|
|
125
126
|
return;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
if (parsed.flags.bundleSize && !parsed.flags.html) {
|
|
130
|
+
console.error(
|
|
131
|
+
color(
|
|
132
|
+
'Note: --bundlesize only applies with --html (bundlejs.com gzip sizes in the report file). Terminal output is unchanged — use e.g. --html --bundlesize.\n',
|
|
133
|
+
'yellow',
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
128
138
|
await runFilesystemAnalysis({ rootDir: parsed.rootDir, flags: parsed.flags });
|
|
129
139
|
}
|
|
130
140
|
|
package/lib/fix.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { color } = require('./ansi');
|
|
4
4
|
const { satisfies } = require('./graph');
|
|
5
|
-
const {
|
|
5
|
+
const { fetchLatestPackageManifest, registryFetchConcurrency } = require('./npm');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* For each real conflict (version mismatch, not just missing optional):
|
|
@@ -10,7 +10,7 @@ const { queryNpm } = require('./npm');
|
|
|
10
10
|
* 2. Check if latest fixes the conflict (its new peer range accepts the installed version).
|
|
11
11
|
* 3. Check if latest introduces NEW conflicts (cascade detection).
|
|
12
12
|
*/
|
|
13
|
-
function resolveConflicts(graph, directDeps, getInstalledVersion, options = {}) {
|
|
13
|
+
async function resolveConflicts(graph, directDeps, getInstalledVersion, options = {}) {
|
|
14
14
|
const silent = options.silent === true;
|
|
15
15
|
const log = (...args) => {
|
|
16
16
|
if (!silent) console.log(...args);
|
|
@@ -40,21 +40,33 @@ function resolveConflicts(graph, directDeps, getInstalledVersion, options = {})
|
|
|
40
40
|
|
|
41
41
|
const pkgsToCheck = [...new Set(conflicts.map(c => c.pkg))];
|
|
42
42
|
|
|
43
|
-
log(color(`\nQuerying npm for ${pkgsToCheck.length} package(s)…`, 'gray'));
|
|
43
|
+
log(color(`\nQuerying npm registry for ${pkgsToCheck.length} package(s)…`, 'gray'));
|
|
44
|
+
|
|
45
|
+
const limit = registryFetchConcurrency();
|
|
46
|
+
const manifestByPkg = new Map();
|
|
47
|
+
for (let i = 0; i < pkgsToCheck.length; i += limit) {
|
|
48
|
+
const batch = pkgsToCheck.slice(i, i + limit);
|
|
49
|
+
await Promise.all(
|
|
50
|
+
batch.map(async pkg => {
|
|
51
|
+
const m = await fetchLatestPackageManifest(pkg);
|
|
52
|
+
manifestByPkg.set(pkg, m);
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
44
56
|
|
|
45
57
|
const resolutions = {};
|
|
46
58
|
|
|
47
59
|
for (const pkg of pkgsToCheck) {
|
|
48
60
|
write(` ${color(pkg, 'cyan')} … `);
|
|
49
|
-
const info =
|
|
61
|
+
const info = manifestByPkg.get(pkg);
|
|
50
62
|
if (!info) {
|
|
51
63
|
log(color('not found on npm', 'yellow'));
|
|
52
64
|
continue;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
const latest = info
|
|
67
|
+
const latest = info.version;
|
|
56
68
|
if (!latest) {
|
|
57
|
-
log(color('no
|
|
69
|
+
log(color('no version on latest', 'yellow'));
|
|
58
70
|
continue;
|
|
59
71
|
}
|
|
60
72
|
|
package/lib/npm.js
CHANGED
|
@@ -17,21 +17,62 @@ function queryNpm(pkg) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Strip "v"; bundlejs needs a concrete version string. */
|
|
21
|
+
function normalizePkgVersionForBundleQuery(version) {
|
|
22
|
+
if (version == null || version === '') return null;
|
|
23
|
+
const v = String(version).trim().replace(/^v/i, '');
|
|
24
|
+
return v || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const BUNDLE_API_UA = 'plexus-peers (+https://github.com/arnaudmanaranche/plexus; bundle gzip check)';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* [bundlejs](https://bundlejs.com/) — esbuild bundles `export * from 'pkg@ver'`, minifies, reports gzip.
|
|
31
|
+
*/
|
|
20
32
|
async function getBundleSize(pkgName, version) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
const ver = normalizePkgVersionForBundleQuery(version);
|
|
34
|
+
if (!ver) return null;
|
|
35
|
+
const pkgSpec = `${pkgName}@${ver}`;
|
|
36
|
+
const url = `https://deno.bundlejs.com/?q=${encodeURIComponent(pkgSpec)}`;
|
|
37
|
+
const maxAttempts = 3;
|
|
38
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
39
|
+
if (attempt > 0) await new Promise(r => setTimeout(r, 900 * attempt));
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(url, {
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
'User-Agent': BUNDLE_API_UA,
|
|
45
|
+
},
|
|
46
|
+
signal: AbortSignal.timeout(60000),
|
|
47
|
+
});
|
|
48
|
+
if (res.status === 429 || res.status === 503) continue;
|
|
49
|
+
if (!res.ok) return null;
|
|
50
|
+
const ct = res.headers.get('content-type') || '';
|
|
51
|
+
if (!ct.includes('application/json')) return null;
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
const s = data.size;
|
|
54
|
+
if (!s || typeof s !== 'object') return null;
|
|
55
|
+
const rawU = s.rawUncompressedSize;
|
|
56
|
+
const rawC = s.rawCompressedSize;
|
|
57
|
+
if (typeof rawU !== 'number' || typeof rawC !== 'number') return null;
|
|
58
|
+
if (!Number.isFinite(rawU) || !Number.isFinite(rawC)) return null;
|
|
59
|
+
return { size: rawU, gzip: rawC };
|
|
60
|
+
} catch {
|
|
61
|
+
if (attempt === maxAttempts - 1) return null;
|
|
62
|
+
}
|
|
29
63
|
}
|
|
64
|
+
return null;
|
|
30
65
|
}
|
|
31
66
|
|
|
67
|
+
/** Smaller/faster packument for semver resolution (full version list, slim per-version docs). */
|
|
32
68
|
async function fetchResolvedPackageManifest(name, range) {
|
|
33
69
|
const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
|
|
34
|
-
const res = await fetch(url, {
|
|
70
|
+
const res = await fetch(url, {
|
|
71
|
+
headers: {
|
|
72
|
+
Accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8',
|
|
73
|
+
},
|
|
74
|
+
signal: AbortSignal.timeout(20000),
|
|
75
|
+
});
|
|
35
76
|
if (!res.ok) return null;
|
|
36
77
|
const data = await res.json();
|
|
37
78
|
const versions = Object.keys(data.versions || {});
|
|
@@ -40,11 +81,25 @@ async function fetchResolvedPackageManifest(name, range) {
|
|
|
40
81
|
return data.versions[best];
|
|
41
82
|
}
|
|
42
83
|
|
|
84
|
+
/** Latest dist manifest only — used by --fix (avoids full `npm info` + huge JSON per package). */
|
|
85
|
+
async function fetchLatestPackageManifest(name) {
|
|
86
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`;
|
|
87
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
88
|
+
if (!res.ok) return null;
|
|
89
|
+
return res.json();
|
|
90
|
+
}
|
|
91
|
+
|
|
43
92
|
function registryFetchConcurrency() {
|
|
44
93
|
const n = Number(process.env.PLEXUS_REGISTRY_CONCURRENCY || 10);
|
|
45
94
|
return Math.max(1, Math.min(50, n > 0 ? n : 10));
|
|
46
95
|
}
|
|
47
96
|
|
|
97
|
+
/** bundlejs.com builds are slow; keep concurrency low (`PLEXUS_BUNDLE_SIZE_CONCURRENCY`, default 3). */
|
|
98
|
+
function bundleSizeFetchConcurrency() {
|
|
99
|
+
const n = Number(process.env.PLEXUS_BUNDLE_SIZE_CONCURRENCY || 3);
|
|
100
|
+
return Math.max(1, Math.min(8, n > 0 ? n : 3));
|
|
101
|
+
}
|
|
102
|
+
|
|
48
103
|
async function createRegistryContext(directDeps) {
|
|
49
104
|
const pkgs = new Map();
|
|
50
105
|
const names = Object.keys(directDeps);
|
|
@@ -80,6 +135,8 @@ module.exports = {
|
|
|
80
135
|
queryNpm,
|
|
81
136
|
getBundleSize,
|
|
82
137
|
fetchResolvedPackageManifest,
|
|
138
|
+
fetchLatestPackageManifest,
|
|
83
139
|
createRegistryContext,
|
|
84
140
|
registryFetchConcurrency,
|
|
141
|
+
bundleSizeFetchConcurrency,
|
|
85
142
|
};
|
package/lib/render-html.js
CHANGED
|
@@ -8,6 +8,22 @@ function renderHtml(graph, directDeps, rootPkg, resolutions = {}, bundleSizes =
|
|
|
8
8
|
meta.source === 'registry'
|
|
9
9
|
? ' — versions resolved from npm registry (no local node_modules)'
|
|
10
10
|
: '';
|
|
11
|
+
const bundleSizeRequested = meta.bundleSizeRequested === true;
|
|
12
|
+
const directDepCount = Object.keys(directDeps).length;
|
|
13
|
+
const bundleResolvedCount = Object.keys(bundleSizes).length;
|
|
14
|
+
const bundleNote =
|
|
15
|
+
bundleSizeRequested && directDepCount > 0
|
|
16
|
+
? bundleResolvedCount === 0
|
|
17
|
+
? ' — Bundled gzip (bundlejs.com) was requested, but no sizes were returned (timeouts, build failures, or rate limits). Try again or lower PLEXUS_BUNDLE_SIZE_CONCURRENCY.'
|
|
18
|
+
: bundleResolvedCount < directDepCount
|
|
19
|
+
? ` — Bundled gzip: ${bundleResolvedCount} of ${directDepCount} direct dependencies; missing entries show “gzip n/a” on the card.`
|
|
20
|
+
: ' — Bundled gzip estimates (esbuild via bundlejs.com) are shown on each direct dependency card.'
|
|
21
|
+
: '';
|
|
22
|
+
const rootVerTrim =
|
|
23
|
+
rootPkg.version != null && String(rootPkg.version).trim() !== ''
|
|
24
|
+
? String(rootPkg.version).trim()
|
|
25
|
+
: '';
|
|
26
|
+
const heroVerHtml = rootVerTrim !== '' ? ` <span class="hero-ver">v${esc(rootVerTrim)}</span>` : '';
|
|
11
27
|
const directSet = new Set(Object.keys(directDeps));
|
|
12
28
|
|
|
13
29
|
const entries = Object.entries(graph).sort(([a], [b]) => a.localeCompare(b));
|
|
@@ -22,8 +38,9 @@ function renderHtml(graph, directDeps, rootPkg, resolutions = {}, bundleSizes =
|
|
|
22
38
|
return acc + info.peerDeps.filter(p => p.ok).length;
|
|
23
39
|
}, 0);
|
|
24
40
|
|
|
25
|
-
// Size stats
|
|
41
|
+
// Size stats (registry / upload paths have no disk data — omit stat instead of "0 B")
|
|
26
42
|
const totalDiskSize = entries.reduce((acc, [, info]) => acc + (info.diskSize ?? 0), 0);
|
|
43
|
+
const showDiskTotalStat = totalDiskSize > 0;
|
|
27
44
|
|
|
28
45
|
// Abbreviate long semver ranges for compact display
|
|
29
46
|
function shortRange(range) {
|
|
@@ -111,13 +128,28 @@ function renderHtml(graph, directDeps, rootPkg, resolutions = {}, bundleSizes =
|
|
|
111
128
|
? `<div class="required-by">peer of: ${info.requiredBy.map(r => `<a href="#pkg-${slug(r)}">${esc(r)}</a>`).join(', ')}</div>`
|
|
112
129
|
: '';
|
|
113
130
|
|
|
114
|
-
|
|
131
|
+
const versionTrim =
|
|
132
|
+
info.version != null && String(info.version).trim() !== '' ? String(info.version).trim() : '';
|
|
133
|
+
const versionBadge =
|
|
134
|
+
versionTrim !== ''
|
|
135
|
+
? `<span class="pkg-version">v${esc(versionTrim)}</span>`
|
|
136
|
+
: info.missing
|
|
137
|
+
? `<span class="pkg-version" title="Not installed">?</span>`
|
|
138
|
+
: '';
|
|
139
|
+
|
|
140
|
+
return `<div class="card ${statusClass}" id="pkg-${slug(pkgName)}" data-conflicts="${conflictPeers.length}" data-pkg="${esc(pkgName)}" data-size="${info.diskSize ?? 0}" data-bundle-gzip="${bundleInfo ? bundleInfo.gzip : 0}">
|
|
115
141
|
<div class="card-header">
|
|
116
142
|
<a href="${esc(npmPackageUrl(pkgName))}" target="_blank" class="pkg-name">${esc(pkgName)}</a>
|
|
117
143
|
<span class="pkg-meta">
|
|
118
|
-
|
|
144
|
+
${versionBadge}
|
|
119
145
|
${sizeStr ? `<span class="size-badge ${sizeCls}">${esc(sizeStr)}</span>` : ''}
|
|
120
|
-
${
|
|
146
|
+
${
|
|
147
|
+
bundleInfo
|
|
148
|
+
? `<span class="size-badge size-bundle" title="Bundled + minified + gzip (bundlejs.com export * from pkg)">gzip ${esc(formatBytes(bundleInfo.gzip))}</span>`
|
|
149
|
+
: bundleSizeRequested && isDirect
|
|
150
|
+
? `<span class="size-badge size-bundle-missing" title="bundlejs.com could not build this entry (invalid version, native-only package, timeout, or rate limit).">gzip n/a</span>`
|
|
151
|
+
: ''
|
|
152
|
+
}
|
|
121
153
|
</span>
|
|
122
154
|
<div class="badges">
|
|
123
155
|
${isDirect ? '<span class="badge badge-direct">direct</span>' : ''}
|
|
@@ -130,6 +162,30 @@ function renderHtml(graph, directDeps, rootPkg, resolutions = {}, bundleSizes =
|
|
|
130
162
|
})
|
|
131
163
|
.join('\n');
|
|
132
164
|
|
|
165
|
+
const hasConflictCards = entries.some(([, info]) =>
|
|
166
|
+
info.peerDeps.some(p => !p.ok && p.installed),
|
|
167
|
+
);
|
|
168
|
+
/** Show "No issues" only when there is at least one conflict (otherwise redundant with "All"). */
|
|
169
|
+
const showNoIssuesFilter = conflictCount > 0;
|
|
170
|
+
|
|
171
|
+
const toolbarRows = [
|
|
172
|
+
'<input type="text" id="search" placeholder="Search packages…">',
|
|
173
|
+
'<button type="button" class="filter-btn active" data-filter="all">All</button>',
|
|
174
|
+
];
|
|
175
|
+
if (hasConflictCards) {
|
|
176
|
+
toolbarRows.push(
|
|
177
|
+
'<button type="button" class="filter-btn" data-filter="conflicts" id="plexus-filter-conflicts">Conflicts only</button>',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
toolbarRows.push('<button type="button" class="filter-btn" data-filter="direct">Direct only</button>');
|
|
181
|
+
if (showNoIssuesFilter) {
|
|
182
|
+
toolbarRows.push('<button type="button" class="filter-btn" data-filter="ok">No issues</button>');
|
|
183
|
+
}
|
|
184
|
+
toolbarRows.push(
|
|
185
|
+
'<button type="button" class="filter-btn" id="sort-size-btn" style="margin-left:auto">Sort by size ↕</button>',
|
|
186
|
+
);
|
|
187
|
+
const toolbarHtml = `<div class="toolbar">\n ${toolbarRows.join('\n ')}\n</div>`;
|
|
188
|
+
|
|
133
189
|
const conflictSummaryRows = entries
|
|
134
190
|
.flatMap(([pkgName, info]) =>
|
|
135
191
|
info.peerDeps
|
|
@@ -285,10 +341,23 @@ function renderHtml(graph, directDeps, rootPkg, resolutions = {}, bundleSizes =
|
|
|
285
341
|
.copy-json-btn.copied { color: var(--green); border-color: rgba(134,239,172,0.35); }
|
|
286
342
|
.json-block-wrap { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; background: var(--code); }
|
|
287
343
|
.json-block { margin: 0; background: transparent; border: none; border-radius: 0; padding: 20px 22px; font-size: 12px; font-family: ui-monospace, monospace; overflow-x: auto; white-space: pre; color: var(--text); line-height: 1.65; }
|
|
288
|
-
.json-block
|
|
344
|
+
.json-block.json-block-suggested { line-height: 1.42; }
|
|
345
|
+
.json-block-header-text { min-width: 0; flex: 1; }
|
|
346
|
+
.json-block .json-line-changed {
|
|
347
|
+
display: inline;
|
|
348
|
+
padding: 0 6px 0 9px;
|
|
349
|
+
border-left: 3px solid var(--green);
|
|
350
|
+
border-radius: 0 2px 2px 0;
|
|
351
|
+
margin: 0 0 0 -12px;
|
|
352
|
+
background: linear-gradient(90deg, rgba(20,83,45,0.4) 0%, rgba(20,83,45,0.07) 62%, transparent 100%);
|
|
353
|
+
box-decoration-break: clone;
|
|
354
|
+
-webkit-box-decoration-break: clone;
|
|
355
|
+
color: var(--text-bright);
|
|
356
|
+
}
|
|
289
357
|
.version-ok { color: var(--green); }
|
|
290
358
|
.size-badge { font-size: 10px; padding: 2px 7px; border-radius: 6px; font-family: ui-monospace, monospace; }
|
|
291
359
|
.size-bundle { background: rgba(63,63,70,0.45); color: var(--muted); border: 1px solid var(--border); }
|
|
360
|
+
.size-bundle-missing { background: rgba(63,63,70,0.25); color: var(--muted2); border: 1px dashed var(--border-hover); font-style: italic; }
|
|
292
361
|
.size-large { background: rgba(127,29,29,0.35); color: #fca5a5; }
|
|
293
362
|
.size-medium { background: rgba(120,53,15,0.35); color: var(--yellow); }
|
|
294
363
|
.size-small { background: rgba(20,83,45,0.35); color: var(--green); }
|
|
@@ -303,8 +372,8 @@ function renderHtml(graph, directDeps, rootPkg, resolutions = {}, bundleSizes =
|
|
|
303
372
|
<div class="shell">
|
|
304
373
|
<header class="hero">
|
|
305
374
|
<h1>Plexus</h1>
|
|
306
|
-
<p class="hero-meta"><strong>${esc(rootPkg.name ?? '(unnamed)')}</strong
|
|
307
|
-
<p class="hero-sub">Peer dependency analysis${sourceNote}</p>
|
|
375
|
+
<p class="hero-meta"><strong>${esc(rootPkg.name ?? '(unnamed)')}</strong>${heroVerHtml}</p>
|
|
376
|
+
<p class="hero-sub">Peer dependency analysis${sourceNote}${bundleNote}</p>
|
|
308
377
|
<p class="generated">Generated ${esc(new Date().toLocaleString())}</p>
|
|
309
378
|
</header>
|
|
310
379
|
|
|
@@ -314,17 +383,14 @@ function renderHtml(graph, directDeps, rootPkg, resolutions = {}, bundleSizes =
|
|
|
314
383
|
<div class="stat"><div class="stat-num num-ok">${okCount}</div><div class="stat-label">Satisfied peers</div></div>
|
|
315
384
|
<div class="stat"><div class="stat-num num-conflict">${conflictCount}</div><div class="stat-label">Conflicts</div></div>
|
|
316
385
|
<div class="stat"><div class="stat-num num-warning">${missingOptionalCount}</div><div class="stat-label">Missing optional</div></div>
|
|
317
|
-
|
|
386
|
+
${
|
|
387
|
+
showDiskTotalStat
|
|
388
|
+
? ` <div class="stat"><div class="stat-num num-neutral" style="font-size:17px">${esc(formatBytes(totalDiskSize))}</div><div class="stat-label">Total disk (node_modules)</div></div>`
|
|
389
|
+
: ''
|
|
390
|
+
}
|
|
318
391
|
</div>
|
|
319
392
|
|
|
320
|
-
|
|
321
|
-
<input type="text" id="search" placeholder="Search packages…">
|
|
322
|
-
<button class="filter-btn active" data-filter="all">All</button>
|
|
323
|
-
<button class="filter-btn" data-filter="conflicts">Conflicts only</button>
|
|
324
|
-
<button class="filter-btn" data-filter="direct">Direct only</button>
|
|
325
|
-
<button class="filter-btn" data-filter="ok">No issues</button>
|
|
326
|
-
<button class="filter-btn" id="sort-size-btn" style="margin-left:auto">Sort by size ↕</button>
|
|
327
|
-
</div>
|
|
393
|
+
${toolbarHtml}
|
|
328
394
|
|
|
329
395
|
<div class="grid-wrap">
|
|
330
396
|
<h2 class="grid-heading">Packages</h2>
|
|
@@ -368,7 +434,7 @@ ${
|
|
|
368
434
|
return `<tr>
|
|
369
435
|
<td><a href="${esc(npmPackageUrl(pkg))}" target="_blank" class="pkg-link">${esc(pkg)}</a></td>
|
|
370
436
|
<td><code>${esc(r.current)}</code> → <code class="version-ok">^${esc(r.latest)}</code></td>
|
|
371
|
-
<td>${fixesHtml}${stillHtml}</td>
|
|
437
|
+
<td>${fixesHtml}${stillHtml ? ` ${stillHtml}` : ''}</td>
|
|
372
438
|
<td>${cascadeHtml}</td>
|
|
373
439
|
</tr>`;
|
|
374
440
|
})
|
|
@@ -393,33 +459,32 @@ ${
|
|
|
393
459
|
if (suggested.dependencies?.[pkg]) suggested.dependencies[pkg] = `^${r.latest}`;
|
|
394
460
|
if (suggested.devDependencies?.[pkg]) suggested.devDependencies[pkg] = `^${r.latest}`;
|
|
395
461
|
}
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
`"(${changed.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})"`,
|
|
399
|
-
'g',
|
|
400
|
-
);
|
|
401
|
-
const markHolders = [];
|
|
462
|
+
const changedSet = new Set(Object.keys(resolutions));
|
|
463
|
+
const depLineRe = /^(\s*)("([^"]+)":\s*"[^"]*")(,?)$/;
|
|
402
464
|
let depsJson = JSON.stringify(
|
|
403
465
|
{ dependencies: suggested.dependencies, devDependencies: suggested.devDependencies },
|
|
404
466
|
null,
|
|
405
467
|
2,
|
|
406
468
|
);
|
|
407
|
-
depsJson = depsJson
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
469
|
+
depsJson = depsJson
|
|
470
|
+
.split('\n')
|
|
471
|
+
.map(line => {
|
|
472
|
+
const m = line.match(depLineRe);
|
|
473
|
+
if (m && changedSet.has(m[3])) {
|
|
474
|
+
return `<span class="json-line-changed">${esc(m[1] + m[2] + (m[4] || ''))}</span>`;
|
|
475
|
+
}
|
|
476
|
+
return esc(line);
|
|
477
|
+
})
|
|
478
|
+
.join('\n');
|
|
416
479
|
return `<div class="conflict-table-section">
|
|
417
480
|
<div class="json-block-header">
|
|
418
|
-
<
|
|
481
|
+
<div class="json-block-header-text">
|
|
482
|
+
<h2 class="h2-fix">Suggested package.json</h2>
|
|
483
|
+
</div>
|
|
419
484
|
<button type="button" class="copy-json-btn" id="copy-suggested-json" aria-label="Copy JSON to clipboard">Copy JSON</button>
|
|
420
485
|
</div>
|
|
421
486
|
<div class="json-block-wrap">
|
|
422
|
-
<pre class="json-block" id="suggested-json-pre">${depsJson}</pre>
|
|
487
|
+
<pre class="json-block json-block-suggested" id="suggested-json-pre">${depsJson}</pre>
|
|
423
488
|
</div>
|
|
424
489
|
</div>`;
|
|
425
490
|
})()
|
|
@@ -437,43 +502,53 @@ ${
|
|
|
437
502
|
arrow.textContent = rows.classList.contains('hidden') ? '▸' : '▾';
|
|
438
503
|
}
|
|
439
504
|
|
|
440
|
-
|
|
505
|
+
function fallbackCopyJson(text) {
|
|
506
|
+
try {
|
|
507
|
+
var ta = document.createElement('textarea');
|
|
508
|
+
ta.value = text;
|
|
509
|
+
ta.setAttribute('readonly', '');
|
|
510
|
+
ta.style.position = 'fixed';
|
|
511
|
+
ta.style.left = '-9999px';
|
|
512
|
+
document.body.appendChild(ta);
|
|
513
|
+
ta.select();
|
|
514
|
+
var ok = document.execCommand('copy');
|
|
515
|
+
document.body.removeChild(ta);
|
|
516
|
+
return ok;
|
|
517
|
+
} catch (e) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* Run immediately: DOMContentLoaded often does not fire after document.write() / blob report. */
|
|
523
|
+
(function () {
|
|
441
524
|
var currentFilter = 'all';
|
|
442
525
|
var sortedBySize = false;
|
|
443
526
|
var searchInput = document.getElementById('search');
|
|
444
527
|
var grid = document.getElementById('grid');
|
|
528
|
+
if (!grid) return;
|
|
445
529
|
|
|
446
|
-
|
|
530
|
+
var conflictsBtn = document.getElementById('plexus-filter-conflicts');
|
|
531
|
+
if (conflictsBtn && !grid.querySelector('.card-conflict')) {
|
|
532
|
+
conflictsBtn.remove();
|
|
533
|
+
}
|
|
447
534
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
filterCards();
|
|
454
|
-
});
|
|
455
|
-
});
|
|
535
|
+
function effectiveSize(card) {
|
|
536
|
+
var disk = parseInt(card.getAttribute('data-size') || '0', 10) || 0;
|
|
537
|
+
var gzip = parseInt(card.getAttribute('data-bundle-gzip') || '0', 10) || 0;
|
|
538
|
+
return disk > 0 ? disk : gzip;
|
|
539
|
+
}
|
|
456
540
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
sortBtn.textContent = sortedBySize ? 'Sort by name ↕' : 'Sort by size ↕';
|
|
463
|
-
var cards = Array.from(grid.querySelectorAll('.card'));
|
|
464
|
-
cards.sort(function(a, b) {
|
|
465
|
-
if (sortedBySize) {
|
|
466
|
-
return parseInt(b.getAttribute('data-size') || 0) - parseInt(a.getAttribute('data-size') || 0);
|
|
467
|
-
}
|
|
468
|
-
return (a.getAttribute('data-pkg') || '').localeCompare(b.getAttribute('data-pkg') || '');
|
|
469
|
-
});
|
|
470
|
-
cards.forEach(function(card) { grid.appendChild(card); });
|
|
541
|
+
function getGridCards() {
|
|
542
|
+
var g = document.getElementById('grid');
|
|
543
|
+
if (!g) return [];
|
|
544
|
+
return Array.from(g.children).filter(function (el) {
|
|
545
|
+
return el.nodeType === 1 && el.classList.contains('card');
|
|
471
546
|
});
|
|
472
547
|
}
|
|
473
548
|
|
|
474
549
|
function filterCards() {
|
|
475
|
-
var search = searchInput.value.toLowerCase();
|
|
476
|
-
|
|
550
|
+
var search = (searchInput && searchInput.value) ? searchInput.value.toLowerCase() : '';
|
|
551
|
+
getGridCards().forEach(function (card) {
|
|
477
552
|
var pkg = (card.getAttribute('data-pkg') || '').toLowerCase();
|
|
478
553
|
var hasConflict = card.classList.contains('card-conflict');
|
|
479
554
|
var hasWarning = card.classList.contains('card-warning');
|
|
@@ -490,6 +565,89 @@ ${
|
|
|
490
565
|
});
|
|
491
566
|
}
|
|
492
567
|
|
|
568
|
+
if (searchInput) searchInput.addEventListener('input', filterCards);
|
|
569
|
+
|
|
570
|
+
document.querySelectorAll('.filter-btn[data-filter]').forEach(function (btn) {
|
|
571
|
+
btn.addEventListener('click', function () {
|
|
572
|
+
currentFilter = btn.getAttribute('data-filter');
|
|
573
|
+
document.querySelectorAll('.filter-btn[data-filter]').forEach(function (b) {
|
|
574
|
+
b.classList.remove('active');
|
|
575
|
+
});
|
|
576
|
+
btn.classList.add('active');
|
|
577
|
+
filterCards();
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
/* Delegated click: survives edge cases where target is a text node; always use live #grid. */
|
|
582
|
+
document.addEventListener(
|
|
583
|
+
'click',
|
|
584
|
+
function (e) {
|
|
585
|
+
var t = e.target;
|
|
586
|
+
while (t && t.nodeType !== 1) t = t.parentNode;
|
|
587
|
+
var sortBtn = t && t.closest ? t.closest('#sort-size-btn') : null;
|
|
588
|
+
if (!sortBtn) return;
|
|
589
|
+
try {
|
|
590
|
+
var gridEl = document.getElementById('grid');
|
|
591
|
+
if (!gridEl) return;
|
|
592
|
+
|
|
593
|
+
var nextBySize = !sortedBySize;
|
|
594
|
+
var cards = Array.from(gridEl.children).filter(function (el) {
|
|
595
|
+
return el.nodeType === 1 && el.classList.contains('card');
|
|
596
|
+
});
|
|
597
|
+
if (cards.length === 0) return;
|
|
598
|
+
|
|
599
|
+
var pkgKey = function (c) {
|
|
600
|
+
return (c.getAttribute('data-pkg') || '').toLowerCase();
|
|
601
|
+
};
|
|
602
|
+
var eff = cards.map(function (c) {
|
|
603
|
+
return effectiveSize(c);
|
|
604
|
+
});
|
|
605
|
+
var allSameSizes =
|
|
606
|
+
eff.length > 0 && eff.every(function (s) {
|
|
607
|
+
return s === eff[0];
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
cards.sort(function (a, b) {
|
|
611
|
+
if (nextBySize) {
|
|
612
|
+
var sa = effectiveSize(a);
|
|
613
|
+
var sb = effectiveSize(b);
|
|
614
|
+
if (sb !== sa) return sb - sa;
|
|
615
|
+
/* All sizes tied (often 0): reverse name vs initial report order so the grid visibly changes */
|
|
616
|
+
if (allSameSizes) return pkgKey(b).localeCompare(pkgKey(a));
|
|
617
|
+
return pkgKey(a).localeCompare(pkgKey(b));
|
|
618
|
+
}
|
|
619
|
+
return pkgKey(a).localeCompare(pkgKey(b));
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
while (gridEl.firstChild) gridEl.removeChild(gridEl.firstChild);
|
|
623
|
+
var frag = document.createDocumentFragment();
|
|
624
|
+
for (var i = 0; i < cards.length; i++) {
|
|
625
|
+
frag.appendChild(cards[i]);
|
|
626
|
+
}
|
|
627
|
+
gridEl.appendChild(frag);
|
|
628
|
+
void gridEl.offsetWidth;
|
|
629
|
+
|
|
630
|
+
sortedBySize = nextBySize;
|
|
631
|
+
sortBtn.classList.toggle('active', sortedBySize);
|
|
632
|
+
sortBtn.textContent = sortedBySize ? 'Sort by name ↕' : 'Sort by size ↕';
|
|
633
|
+
|
|
634
|
+
console.log('[plexus] sort applied', {
|
|
635
|
+
mode: sortedBySize ? 'by_size' : 'by_name',
|
|
636
|
+
cards: cards.length,
|
|
637
|
+
allSameSizes: nextBySize ? allSameSizes : null,
|
|
638
|
+
firstThree: Array.prototype.slice.call(gridEl.children, 0, 3).map(function (n) {
|
|
639
|
+
return n.getAttribute('data-pkg');
|
|
640
|
+
}),
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
filterCards();
|
|
644
|
+
} catch (err) {
|
|
645
|
+
console.error('[plexus] sort failed', err);
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
false,
|
|
649
|
+
);
|
|
650
|
+
|
|
493
651
|
var copyJsonBtn = document.getElementById('copy-suggested-json');
|
|
494
652
|
var suggestedJsonPre = document.getElementById('suggested-json-pre');
|
|
495
653
|
if (copyJsonBtn && suggestedJsonPre) {
|
|
@@ -505,34 +663,22 @@ ${
|
|
|
505
663
|
}, 2000);
|
|
506
664
|
}
|
|
507
665
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
508
|
-
navigator.clipboard
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
666
|
+
navigator.clipboard
|
|
667
|
+
.writeText(text)
|
|
668
|
+
.then(function () {
|
|
669
|
+
done(true);
|
|
670
|
+
})
|
|
671
|
+
.catch(function () {
|
|
672
|
+
if (fallbackCopyJson(text)) done(true);
|
|
673
|
+
else done(false);
|
|
674
|
+
});
|
|
512
675
|
} else {
|
|
513
676
|
if (fallbackCopyJson(text)) done(true);
|
|
514
677
|
else done(false);
|
|
515
678
|
}
|
|
516
679
|
});
|
|
517
680
|
}
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
function fallbackCopyJson(text) {
|
|
521
|
-
try {
|
|
522
|
-
var ta = document.createElement('textarea');
|
|
523
|
-
ta.value = text;
|
|
524
|
-
ta.setAttribute('readonly', '');
|
|
525
|
-
ta.style.position = 'fixed';
|
|
526
|
-
ta.style.left = '-9999px';
|
|
527
|
-
document.body.appendChild(ta);
|
|
528
|
-
ta.select();
|
|
529
|
-
var ok = document.execCommand('copy');
|
|
530
|
-
document.body.removeChild(ta);
|
|
531
|
-
return ok;
|
|
532
|
-
} catch (e) {
|
|
533
|
-
return false;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
681
|
+
})();
|
|
536
682
|
</script>
|
|
537
683
|
</body>
|
|
538
684
|
</html>`;
|
package/lib/run-analysis.js
CHANGED
|
@@ -8,7 +8,7 @@ const { color } = require('./ansi');
|
|
|
8
8
|
const { formatBytes } = require('./format');
|
|
9
9
|
const { createFsContext, buildGraph } = require('./graph');
|
|
10
10
|
const { resolveConflicts } = require('./fix');
|
|
11
|
-
const { getBundleSize, createRegistryContext,
|
|
11
|
+
const { getBundleSize, createRegistryContext, bundleSizeFetchConcurrency } = require('./npm');
|
|
12
12
|
const { renderHtml } = require('./render-html');
|
|
13
13
|
const { printGraph, printSummary } = require('./terminal');
|
|
14
14
|
|
|
@@ -46,6 +46,12 @@ async function runFilesystemAnalysis(opts) {
|
|
|
46
46
|
...(rootPkg.devDependencies ?? {}),
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
if (Object.keys(directDeps).length === 0) {
|
|
50
|
+
console.log(color('No dependencies or devDependencies in package.json — nothing to analyze.', 'red'));
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
if (focusPkg && !directDeps[focusPkg]) {
|
|
50
56
|
console.log(color(`Package "${focusPkg}" not found in dependencies.`, 'red'));
|
|
51
57
|
process.exitCode = 1;
|
|
@@ -56,7 +62,7 @@ async function runFilesystemAnalysis(opts) {
|
|
|
56
62
|
const graph = buildGraph(directDeps, ctx);
|
|
57
63
|
|
|
58
64
|
const resolutions = fixMode
|
|
59
|
-
? resolveConflicts(graph, directDeps, ctx.getInstalledVersion).resolutions
|
|
65
|
+
? (await resolveConflicts(graph, directDeps, ctx.getInstalledVersion)).resolutions
|
|
60
66
|
: {};
|
|
61
67
|
|
|
62
68
|
if (fixMode && !htmlMode) {
|
|
@@ -94,7 +100,7 @@ async function runFilesystemAnalysis(opts) {
|
|
|
94
100
|
let bundleSizes = {};
|
|
95
101
|
if (bundleSizeMode) {
|
|
96
102
|
const directKeys = Object.keys(directDeps);
|
|
97
|
-
console.log(color(`\nQuerying
|
|
103
|
+
console.log(color(`\nQuerying bundlejs.com sizes for ${directKeys.length} packages…`, 'gray'));
|
|
98
104
|
for (const pkg of directKeys) {
|
|
99
105
|
const ver = ctx.getInstalledVersion(pkg);
|
|
100
106
|
if (!ver) continue;
|
|
@@ -111,6 +117,7 @@ async function runFilesystemAnalysis(opts) {
|
|
|
111
117
|
|
|
112
118
|
const html = renderHtml(graph, directDeps, rootPkg, resolutions, bundleSizes, {
|
|
113
119
|
source: 'filesystem',
|
|
120
|
+
bundleSizeRequested: bundleSizeMode,
|
|
114
121
|
});
|
|
115
122
|
const outPath = outFile ? path.resolve(outFile) : path.join(rootDir, 'dep-graph.html');
|
|
116
123
|
fs.writeFileSync(outPath, html, 'utf8');
|
|
@@ -141,6 +148,13 @@ async function runRegistryAnalysis(rootPkg, flags = {}) {
|
|
|
141
148
|
};
|
|
142
149
|
const maxDirect = Math.max(1, Number(process.env.PLEXUS_MAX_DIRECT_DEPS || 250) || 250);
|
|
143
150
|
const directCount = Object.keys(directDeps).length;
|
|
151
|
+
if (directCount === 0) {
|
|
152
|
+
const err = new Error(
|
|
153
|
+
'This package.json has no dependencies or devDependencies — nothing to analyze.',
|
|
154
|
+
);
|
|
155
|
+
err.statusCode = 400;
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
144
158
|
if (directCount > maxDirect) {
|
|
145
159
|
const err = new Error(`Too many direct dependencies (${directCount}; max ${maxDirect}).`);
|
|
146
160
|
err.statusCode = 400;
|
|
@@ -150,12 +164,12 @@ async function runRegistryAnalysis(rootPkg, flags = {}) {
|
|
|
150
164
|
const graph = buildGraph(directDeps, ctx);
|
|
151
165
|
const fixMode = flags.fix === true;
|
|
152
166
|
const resolutions = fixMode
|
|
153
|
-
? resolveConflicts(graph, directDeps, ctx.getInstalledVersion, { silent: true }).resolutions
|
|
167
|
+
? (await resolveConflicts(graph, directDeps, ctx.getInstalledVersion, { silent: true })).resolutions
|
|
154
168
|
: {};
|
|
155
169
|
let bundleSizes = {};
|
|
156
170
|
if (flags.bundleSize) {
|
|
157
171
|
const names = Object.keys(directDeps);
|
|
158
|
-
const limit =
|
|
172
|
+
const limit = bundleSizeFetchConcurrency();
|
|
159
173
|
for (let i = 0; i < names.length; i += limit) {
|
|
160
174
|
const batch = names.slice(i, i + limit);
|
|
161
175
|
await Promise.all(
|
|
@@ -170,6 +184,7 @@ async function runRegistryAnalysis(rootPkg, flags = {}) {
|
|
|
170
184
|
}
|
|
171
185
|
const html = renderHtml(graph, directDeps, rootPkg, resolutions, bundleSizes, {
|
|
172
186
|
source: 'registry',
|
|
187
|
+
bundleSizeRequested: flags.bundleSize === true,
|
|
173
188
|
});
|
|
174
189
|
return { graph, directDeps, resolutions, bundleSizes, html };
|
|
175
190
|
}
|
package/lib/server.js
CHANGED
|
@@ -27,10 +27,28 @@ function corsForPages(req, res, next) {
|
|
|
27
27
|
next();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** One JSON line per /api request for Render logs and log drains (duration_ms, status). */
|
|
31
|
+
function structuredApiLog(req, res, next) {
|
|
32
|
+
if (!req.path.startsWith('/api')) return next();
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
res.on('finish', () => {
|
|
35
|
+
const line = JSON.stringify({
|
|
36
|
+
event: 'api_call',
|
|
37
|
+
path: req.path,
|
|
38
|
+
method: req.method,
|
|
39
|
+
duration_ms: Date.now() - start,
|
|
40
|
+
status: res.statusCode,
|
|
41
|
+
});
|
|
42
|
+
console.log(line);
|
|
43
|
+
});
|
|
44
|
+
next();
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
function createApp() {
|
|
31
48
|
const app = express();
|
|
32
49
|
app.use(corsForPages);
|
|
33
50
|
app.use(express.json({ limit: '3mb' }));
|
|
51
|
+
app.use(structuredApiLog);
|
|
34
52
|
|
|
35
53
|
const publicDir = path.join(__dirname, '..', 'public');
|
|
36
54
|
app.use(express.static(publicDir));
|
package/lib/terminal.js
CHANGED
|
@@ -18,9 +18,13 @@ function printGraph(graph, directDeps, options = {}) {
|
|
|
18
18
|
if (conflictsOnly && info.peerDeps.every(p => p.ok)) continue;
|
|
19
19
|
|
|
20
20
|
const isDirect = directSet.has(pkgName);
|
|
21
|
+
const vTrim =
|
|
22
|
+
info.version != null && String(info.version).trim() !== '' ? String(info.version).trim() : '';
|
|
21
23
|
const versionStr = info.missing
|
|
22
24
|
? color('NOT INSTALLED', 'red', 'bold')
|
|
23
|
-
:
|
|
25
|
+
: vTrim !== ''
|
|
26
|
+
? color(`v${vTrim}`, 'gray')
|
|
27
|
+
: color('—', 'gray');
|
|
24
28
|
|
|
25
29
|
const tag = isDirect ? color(' [direct]', 'blue') : '';
|
|
26
30
|
console.log(`\n${color(pkgName, 'bold', 'cyan')}${tag} ${versionStr}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plexus-peers",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Plexus — CLI and mini web UI for peer dependency analysis
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Plexus — CLI and mini web UI for peer dependency analysis",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -21,11 +21,17 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"start": "node bin/plexus.js serve",
|
|
24
|
+
"start:dev": "nodemon -q --watch lib --watch bin --watch public --ext js,cjs,mjs,html,css --delay 200ms bin/plexus.js serve",
|
|
24
25
|
"plexus": "node bin/plexus.js",
|
|
25
|
-
"plexus-peers": "node bin/plexus.js"
|
|
26
|
+
"plexus-peers": "node bin/plexus.js",
|
|
27
|
+
"build:css": "tailwindcss -i ./tailwind.source.css -o ./public/plexus.css --minify"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"express": "^4.21.2",
|
|
29
31
|
"semver": "^7.7.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"nodemon": "^3.1.11",
|
|
35
|
+
"tailwindcss": "^3.4.17"
|
|
30
36
|
}
|
|
31
37
|
}
|
package/public/index.html
CHANGED
|
@@ -6,24 +6,7 @@
|
|
|
6
6
|
<!-- Optional: override API host (e.g. custom GitHub Pages domain). Empty = rules in script below. -->
|
|
7
7
|
<meta name="plexus-api-base" content="">
|
|
8
8
|
<title>Plexus — package.json analysis</title>
|
|
9
|
-
<
|
|
10
|
-
<script>
|
|
11
|
-
tailwind.config = {
|
|
12
|
-
theme: {
|
|
13
|
-
extend: {
|
|
14
|
-
colors: {
|
|
15
|
-
plexus: {
|
|
16
|
-
void: '#050505',
|
|
17
|
-
base: '#0a0a0a',
|
|
18
|
-
raised: '#111111',
|
|
19
|
-
subtle: '#171717',
|
|
20
|
-
code: '#1c1c1c',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
</script>
|
|
9
|
+
<link rel="stylesheet" href="plexus.css">
|
|
27
10
|
</head>
|
|
28
11
|
<body class="relative box-border min-h-screen overflow-x-hidden bg-plexus-base text-zinc-300 font-sans antialiased">
|
|
29
12
|
<!-- Ambient background (low contrast, non-interactive) -->
|
|
@@ -42,7 +25,7 @@
|
|
|
42
25
|
<h1 class="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-semibold tracking-tight text-white [text-shadow:0_1px_2px_rgba(0,0,0,0.85),0_0_40px_rgba(10,10,10,0.9)]">
|
|
43
26
|
Plexus
|
|
44
27
|
</h1>
|
|
45
|
-
<span class="inline-flex items-center rounded-lg border border-zinc-700/80 bg-plexus-code px-2.5 py-1 text-xs sm:text-sm font-mono font-medium text-zinc-400 tabular-nums shrink-0" translate="no">v0.2.
|
|
28
|
+
<span class="inline-flex items-center rounded-lg border border-zinc-700/80 bg-plexus-code px-2.5 py-1 text-xs sm:text-sm font-mono font-medium text-zinc-400 tabular-nums shrink-0" translate="no">v0.2.1</span>
|
|
46
29
|
</div>
|
|
47
30
|
<p class="text-sm text-zinc-500 mb-5 md:mb-6">
|
|
48
31
|
<a href="https://github.com/arnaudmanaranche/plexus" target="_blank" rel="noopener noreferrer" class="text-zinc-400 hover:text-white underline-offset-4 hover:underline transition-colors">GitHub</a>
|
|
@@ -54,34 +37,40 @@
|
|
|
54
37
|
<code class="text-base sm:text-lg md:text-xl font-mono text-zinc-400 bg-plexus-code px-2 py-0.5 rounded-md">node_modules</code>.
|
|
55
38
|
</p>
|
|
56
39
|
|
|
57
|
-
<div class="relative z-[1] w-full max-w-2xl mx-auto rounded-2xl border border-zinc-800/90 bg-plexus-raised/92 backdrop-blur-md p-8 sm:p-10 md:p-12 text-left shadow-[0_24px_80px_rgba(0,0,0,0.45),0_0_0_1px_rgba(255,255,255,0.04)]">
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<div class="mt-8 flex flex-col gap-4">
|
|
64
|
-
<label class="flex items-start gap-3 cursor-pointer text-zinc-500 text-base sm:text-lg select-none leading-snug">
|
|
65
|
-
<input type="checkbox" id="optFix" class="mt-1 size-4 shrink-0 rounded border-zinc-700 bg-plexus-subtle text-zinc-200 focus:ring-2 focus:ring-zinc-600 focus:ring-offset-0 focus:ring-offset-plexus-base accent-zinc-300">
|
|
66
|
-
<span>Include <code class="text-sm font-mono text-zinc-400 bg-plexus-code px-1.5 py-0.5 rounded">--fix</code> resolution plan (extra npm requests)</span>
|
|
67
|
-
</label>
|
|
68
|
-
<label class="flex items-start gap-3 cursor-pointer text-zinc-500 text-base sm:text-lg select-none leading-snug">
|
|
69
|
-
<input type="checkbox" id="optBundle" class="mt-1 size-4 shrink-0 rounded border-zinc-700 bg-plexus-subtle text-zinc-200 focus:ring-2 focus:ring-zinc-600 focus:ring-offset-0 focus:ring-offset-plexus-base accent-zinc-300">
|
|
70
|
-
<span>BundlePhobia sizes (slower)</span>
|
|
40
|
+
<div id="zone" class="relative z-[1] w-full max-w-2xl mx-auto rounded-2xl border border-zinc-800/90 bg-plexus-raised/92 backdrop-blur-md p-8 sm:p-10 md:p-12 text-left shadow-[0_24px_80px_rgba(0,0,0,0.45),0_0_0_1px_rgba(255,255,255,0.04)]">
|
|
41
|
+
<div id="zone-panel-upload">
|
|
42
|
+
<label class="block border-2 border-dashed border-zinc-800 rounded-xl py-14 sm:py-16 px-6 text-center cursor-pointer text-zinc-500 text-lg sm:text-xl transition-colors duration-200 hover:border-zinc-600 hover:text-zinc-300 hover:bg-plexus-subtle/50">
|
|
43
|
+
<input type="file" id="file" class="hidden" accept=".json,application/json">
|
|
44
|
+
<span id="fileLabel">Choose a package.json file</span>
|
|
71
45
|
</label>
|
|
72
|
-
</div>
|
|
73
46
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
47
|
+
<div class="mt-8 flex flex-col gap-4">
|
|
48
|
+
<label class="flex items-start gap-3 cursor-pointer text-zinc-500 text-base sm:text-lg select-none leading-snug">
|
|
49
|
+
<input type="checkbox" id="optFix" class="mt-1 size-4 shrink-0 rounded border-zinc-700 bg-plexus-subtle text-zinc-200 focus:ring-2 focus:ring-zinc-600 focus:ring-offset-0 focus:ring-offset-plexus-base accent-zinc-300">
|
|
50
|
+
<span>Include resolution plan (extra npm requests)</span>
|
|
51
|
+
</label>
|
|
52
|
+
<label class="flex items-start gap-3 cursor-pointer text-zinc-500 text-base sm:text-lg select-none leading-snug">
|
|
53
|
+
<input type="checkbox" id="optBundle" class="mt-1 size-4 shrink-0 rounded border-zinc-700 bg-plexus-subtle text-zinc-200 focus:ring-2 focus:ring-zinc-600 focus:ring-offset-0 focus:ring-offset-plexus-base accent-zinc-300">
|
|
54
|
+
<span>Bundle size via bundlejs.com (slower)</span>
|
|
55
|
+
</label>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<button type="button" id="run" disabled
|
|
59
|
+
class="mt-10 w-full py-4 sm:py-5 rounded-xl bg-zinc-200 text-zinc-950 text-lg sm:text-xl font-semibold tracking-tight cursor-pointer transition-colors hover:bg-white disabled:opacity-40 disabled:hover:bg-zinc-200 disabled:cursor-not-allowed">
|
|
60
|
+
Generate report
|
|
61
|
+
</button>
|
|
62
|
+
<p class="mt-3 text-center text-xs sm:text-sm text-zinc-600 leading-snug">
|
|
63
|
+
Large manifests can take 20–90s (longer with <span class="text-zinc-500">bundlejs.com</span> size checks).
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<p class="mt-8 text-center text-sm sm:text-base text-zinc-600 leading-relaxed">
|
|
67
|
+
CLI:
|
|
68
|
+
<code class="font-mono text-zinc-500 bg-plexus-code px-2 py-0.5 rounded-md">npx plexus-peers --html</code>
|
|
69
|
+
from your project root for disk sizes and transitive peers via
|
|
70
|
+
<code class="font-mono text-zinc-500 bg-plexus-code px-2 py-0.5 rounded-md">node_modules</code>.
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="zone-panel-progress" class="hidden" aria-live="polite" aria-busy="true"></div>
|
|
85
74
|
</div>
|
|
86
75
|
</div>
|
|
87
76
|
</main>
|
|
@@ -104,7 +93,267 @@
|
|
|
104
93
|
var toastTimer = null;
|
|
105
94
|
var optFix = document.getElementById('optFix');
|
|
106
95
|
var optBundle = document.getElementById('optBundle');
|
|
96
|
+
var uploadPanel = document.getElementById('zone-panel-upload');
|
|
97
|
+
var progressPanel = document.getElementById('zone-panel-progress');
|
|
98
|
+
var zoneEl = document.getElementById('zone');
|
|
107
99
|
var parsed = null;
|
|
100
|
+
var progressSimTimer = null;
|
|
101
|
+
|
|
102
|
+
function directDepCount(pkg) {
|
|
103
|
+
if (!pkg || typeof pkg !== 'object') return 0;
|
|
104
|
+
var d = pkg.dependencies;
|
|
105
|
+
var dev = pkg.devDependencies;
|
|
106
|
+
var n = 0;
|
|
107
|
+
if (d && typeof d === 'object' && !Array.isArray(d)) n += Object.keys(d).length;
|
|
108
|
+
if (dev && typeof dev === 'object' && !Array.isArray(dev)) n += Object.keys(dev).length;
|
|
109
|
+
return n;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Direct dependency names in deterministic order (matches server merge: deps then devDeps keys, sorted). */
|
|
113
|
+
function getDirectDepNames(pkg) {
|
|
114
|
+
var d =
|
|
115
|
+
pkg.dependencies && typeof pkg.dependencies === 'object' && !Array.isArray(pkg.dependencies)
|
|
116
|
+
? pkg.dependencies
|
|
117
|
+
: {};
|
|
118
|
+
var dev =
|
|
119
|
+
pkg.devDependencies &&
|
|
120
|
+
typeof pkg.devDependencies === 'object' &&
|
|
121
|
+
!Array.isArray(pkg.devDependencies)
|
|
122
|
+
? pkg.devDependencies
|
|
123
|
+
: {};
|
|
124
|
+
var seen = Object.create(null);
|
|
125
|
+
var out = [];
|
|
126
|
+
Object.keys(d).forEach(function (k) {
|
|
127
|
+
if (!seen[k]) {
|
|
128
|
+
seen[k] = true;
|
|
129
|
+
out.push(k);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
Object.keys(dev).forEach(function (k) {
|
|
133
|
+
if (!seen[k]) {
|
|
134
|
+
seen[k] = true;
|
|
135
|
+
out.push(k);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
out.sort(function (a, b) {
|
|
139
|
+
return a.localeCompare(b);
|
|
140
|
+
});
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stopProgressSimulation() {
|
|
145
|
+
if (progressSimTimer) {
|
|
146
|
+
clearInterval(progressSimTimer);
|
|
147
|
+
progressSimTimer = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function showDependencyProgress(depNames) {
|
|
152
|
+
stopProgressSimulation();
|
|
153
|
+
uploadPanel.classList.add('hidden');
|
|
154
|
+
progressPanel.classList.remove('hidden');
|
|
155
|
+
progressPanel.setAttribute('aria-busy', 'true');
|
|
156
|
+
zoneEl.setAttribute('aria-busy', 'true');
|
|
157
|
+
|
|
158
|
+
progressPanel.innerHTML = '';
|
|
159
|
+
var n = depNames.length;
|
|
160
|
+
|
|
161
|
+
var title = document.createElement('p');
|
|
162
|
+
title.className = 'text-zinc-200 text-lg font-medium mb-4 text-center';
|
|
163
|
+
title.textContent = 'Analyzing dependencies…';
|
|
164
|
+
|
|
165
|
+
var pill = document.createElement('div');
|
|
166
|
+
pill.className =
|
|
167
|
+
'mx-auto max-w-md rounded-full border border-zinc-700/75 bg-plexus-void/60 px-6 py-5 sm:px-8 sm:py-6 shadow-[inset_0_1px_0_0_rgba(255,255,255,0.05)]';
|
|
168
|
+
|
|
169
|
+
var stack = document.createElement('div');
|
|
170
|
+
stack.className = 'flex flex-col items-center justify-center gap-1.5 text-center font-mono';
|
|
171
|
+
|
|
172
|
+
var rowTop = document.createElement('div');
|
|
173
|
+
rowTop.className =
|
|
174
|
+
'plexus-pill-row-top flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center gap-2 text-xs sm:text-sm transition-all duration-500 ease-out';
|
|
175
|
+
var rowMid = document.createElement('div');
|
|
176
|
+
rowMid.className =
|
|
177
|
+
'plexus-pill-row-mid flex min-h-[1.6rem] w-full max-w-[20rem] items-center justify-center gap-2 px-2 text-sm sm:text-base font-medium text-zinc-100 transition-all duration-500 ease-out';
|
|
178
|
+
var rowBot = document.createElement('div');
|
|
179
|
+
rowBot.className =
|
|
180
|
+
'plexus-pill-row-bot flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center gap-2 text-xs sm:text-sm transition-all duration-500 ease-out';
|
|
181
|
+
|
|
182
|
+
stack.appendChild(rowTop);
|
|
183
|
+
stack.appendChild(rowMid);
|
|
184
|
+
stack.appendChild(rowBot);
|
|
185
|
+
pill.appendChild(stack);
|
|
186
|
+
|
|
187
|
+
var countEl = document.createElement('p');
|
|
188
|
+
countEl.className = 'mt-4 text-center text-xs text-zinc-600 tabular-nums';
|
|
189
|
+
|
|
190
|
+
progressPanel.appendChild(title);
|
|
191
|
+
progressPanel.appendChild(pill);
|
|
192
|
+
progressPanel.appendChild(countEl);
|
|
193
|
+
|
|
194
|
+
function rowLine(iconText, iconClass, pkgName) {
|
|
195
|
+
var frag = document.createDocumentFragment();
|
|
196
|
+
if (iconText) {
|
|
197
|
+
var ic = document.createElement('span');
|
|
198
|
+
ic.className = iconClass;
|
|
199
|
+
ic.textContent = iconText;
|
|
200
|
+
ic.setAttribute('aria-hidden', 'true');
|
|
201
|
+
frag.appendChild(ic);
|
|
202
|
+
}
|
|
203
|
+
var nm = document.createElement('span');
|
|
204
|
+
nm.className = 'truncate';
|
|
205
|
+
nm.textContent = pkgName || '';
|
|
206
|
+
frag.appendChild(nm);
|
|
207
|
+
return frag;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function clearRow(el) {
|
|
211
|
+
while (el.firstChild) {
|
|
212
|
+
el.removeChild(el.firstChild);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* step = number of packages already finished; middle scans depNames[step] when step < n.
|
|
218
|
+
* step === n: all done (briefly before report opens).
|
|
219
|
+
*/
|
|
220
|
+
function paint(step) {
|
|
221
|
+
clearRow(rowTop);
|
|
222
|
+
|
|
223
|
+
if (step < n) {
|
|
224
|
+
if (step > 0) {
|
|
225
|
+
rowTop.className =
|
|
226
|
+
'plexus-pill-row-top flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center gap-2 text-xs sm:text-sm opacity-45 text-zinc-500 transition-all duration-500 ease-out';
|
|
227
|
+
rowTop.appendChild(
|
|
228
|
+
rowLine('✓', 'shrink-0 text-emerald-400/85', depNames[step - 1]),
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
rowTop.className =
|
|
232
|
+
'plexus-pill-row-top flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center text-xs sm:text-sm opacity-0 pointer-events-none transition-all duration-500 ease-out';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
clearRow(rowMid);
|
|
236
|
+
rowMid.className =
|
|
237
|
+
'plexus-pill-row-mid flex min-h-[1.6rem] w-full max-w-[20rem] items-center justify-center gap-2 px-2 text-sm sm:text-base font-medium text-zinc-100 transition-all duration-500 ease-out';
|
|
238
|
+
var scanIcon = document.createElement('span');
|
|
239
|
+
scanIcon.className =
|
|
240
|
+
'shrink-0 text-amber-400 animate-[pulse_2.2s_ease-in-out_infinite]';
|
|
241
|
+
scanIcon.textContent = '◐';
|
|
242
|
+
scanIcon.setAttribute('aria-hidden', 'true');
|
|
243
|
+
rowMid.appendChild(scanIcon);
|
|
244
|
+
var midName = document.createElement('span');
|
|
245
|
+
midName.className = 'truncate text-zinc-100';
|
|
246
|
+
midName.textContent = depNames[step];
|
|
247
|
+
rowMid.appendChild(midName);
|
|
248
|
+
|
|
249
|
+
clearRow(rowBot);
|
|
250
|
+
if (step < n - 1) {
|
|
251
|
+
rowBot.className =
|
|
252
|
+
'plexus-pill-row-bot flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center gap-2 text-xs sm:text-sm opacity-40 text-zinc-500 transition-all duration-500 ease-out';
|
|
253
|
+
var inIcon = document.createElement('span');
|
|
254
|
+
inIcon.className = 'shrink-0 text-zinc-600';
|
|
255
|
+
inIcon.textContent = '·';
|
|
256
|
+
inIcon.setAttribute('aria-hidden', 'true');
|
|
257
|
+
rowBot.appendChild(inIcon);
|
|
258
|
+
var botName = document.createElement('span');
|
|
259
|
+
botName.className = 'truncate';
|
|
260
|
+
botName.textContent = depNames[step + 1];
|
|
261
|
+
rowBot.appendChild(botName);
|
|
262
|
+
} else {
|
|
263
|
+
rowBot.className =
|
|
264
|
+
'plexus-pill-row-bot flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center text-xs sm:text-sm opacity-0 pointer-events-none transition-all duration-500 ease-out';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
countEl.textContent = Math.min(step + 1, n) + ' / ' + n;
|
|
268
|
+
} else {
|
|
269
|
+
clearRow(rowTop);
|
|
270
|
+
clearRow(rowMid);
|
|
271
|
+
clearRow(rowBot);
|
|
272
|
+
|
|
273
|
+
if (n === 1) {
|
|
274
|
+
rowTop.className =
|
|
275
|
+
'plexus-pill-row-top flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center text-xs sm:text-sm opacity-0';
|
|
276
|
+
rowMid.className =
|
|
277
|
+
'plexus-pill-row-mid flex min-h-[1.6rem] w-full max-w-[20rem] items-center justify-center gap-2 px-2 text-sm sm:text-base font-medium text-zinc-100';
|
|
278
|
+
rowMid.appendChild(rowLine('✓', 'shrink-0 text-emerald-400', depNames[0]));
|
|
279
|
+
rowBot.className =
|
|
280
|
+
'plexus-pill-row-bot flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center opacity-0';
|
|
281
|
+
} else {
|
|
282
|
+
rowTop.className =
|
|
283
|
+
'plexus-pill-row-top flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center gap-2 text-xs sm:text-sm opacity-45 text-zinc-500';
|
|
284
|
+
rowTop.appendChild(
|
|
285
|
+
rowLine('✓', 'shrink-0 text-emerald-400/85', depNames[n - 2]),
|
|
286
|
+
);
|
|
287
|
+
rowMid.className =
|
|
288
|
+
'plexus-pill-row-mid flex min-h-[1.6rem] w-full max-w-[20rem] items-center justify-center gap-2 px-2 text-sm sm:text-base font-medium text-zinc-100';
|
|
289
|
+
rowMid.appendChild(rowLine('✓', 'shrink-0 text-emerald-400', depNames[n - 1]));
|
|
290
|
+
rowBot.className =
|
|
291
|
+
'plexus-pill-row-bot flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center opacity-0';
|
|
292
|
+
}
|
|
293
|
+
countEl.textContent = n + ' / ' + n;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var step = 0;
|
|
298
|
+
var serverWaitShown = false;
|
|
299
|
+
paint(step);
|
|
300
|
+
|
|
301
|
+
function showServerWaitingUi() {
|
|
302
|
+
if (serverWaitShown) return;
|
|
303
|
+
serverWaitShown = true;
|
|
304
|
+
title.textContent = 'Finishing report on server…';
|
|
305
|
+
countEl.className = 'mt-4 text-center text-xs sm:text-sm text-zinc-500 leading-relaxed max-w-lg mx-auto';
|
|
306
|
+
countEl.textContent =
|
|
307
|
+
'The dependency list above is only a visual rhythm — work continues until /api/analyze responds. Bundle size mode can take several minutes.';
|
|
308
|
+
clearRow(rowTop);
|
|
309
|
+
clearRow(rowMid);
|
|
310
|
+
clearRow(rowBot);
|
|
311
|
+
rowTop.className =
|
|
312
|
+
'plexus-pill-row-top flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center opacity-0 pointer-events-none';
|
|
313
|
+
rowBot.className =
|
|
314
|
+
'plexus-pill-row-bot flex min-h-[1.35rem] w-full max-w-[18rem] items-center justify-center opacity-0 pointer-events-none';
|
|
315
|
+
rowMid.className =
|
|
316
|
+
'plexus-pill-row-mid flex min-h-[2.5rem] w-full max-w-[22rem] items-center justify-center gap-2 px-3 text-sm text-zinc-300';
|
|
317
|
+
var dot = document.createElement('span');
|
|
318
|
+
dot.className = 'shrink-0 text-amber-400 animate-pulse';
|
|
319
|
+
dot.textContent = '●';
|
|
320
|
+
dot.setAttribute('aria-hidden', 'true');
|
|
321
|
+
var msg = document.createElement('span');
|
|
322
|
+
msg.textContent = 'Waiting for response…';
|
|
323
|
+
rowMid.appendChild(dot);
|
|
324
|
+
rowMid.appendChild(msg);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function flushRemaining() {
|
|
328
|
+
stopProgressSimulation();
|
|
329
|
+
paint(n);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (n > 0) {
|
|
333
|
+
/* Advance through dependency names only; never imply “all done” until the POST returns. */
|
|
334
|
+
var intervalMs = Math.min(2800, Math.max(950, Math.floor(42000 / n)));
|
|
335
|
+
progressSimTimer = setInterval(function () {
|
|
336
|
+
if (step < n - 1) {
|
|
337
|
+
step++;
|
|
338
|
+
paint(step);
|
|
339
|
+
} else {
|
|
340
|
+
stopProgressSimulation();
|
|
341
|
+
showServerWaitingUi();
|
|
342
|
+
}
|
|
343
|
+
}, intervalMs);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { flushRemaining: flushRemaining };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function hideDependencyProgress() {
|
|
350
|
+
stopProgressSimulation();
|
|
351
|
+
progressPanel.innerHTML = '';
|
|
352
|
+
progressPanel.classList.add('hidden');
|
|
353
|
+
progressPanel.setAttribute('aria-busy', 'false');
|
|
354
|
+
zoneEl.setAttribute('aria-busy', 'false');
|
|
355
|
+
uploadPanel.classList.remove('hidden');
|
|
356
|
+
}
|
|
108
357
|
|
|
109
358
|
function analyzeUrl() {
|
|
110
359
|
var meta = document.querySelector('meta[name="plexus-api-base"]');
|
|
@@ -149,6 +398,14 @@
|
|
|
149
398
|
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
150
399
|
throw new Error('JSON must be a package.json object.');
|
|
151
400
|
}
|
|
401
|
+
if (directDepCount(parsed) === 0) {
|
|
402
|
+
showToast(
|
|
403
|
+
'This package.json has no dependencies or devDependencies — nothing to analyze.',
|
|
404
|
+
);
|
|
405
|
+
parsed = null;
|
|
406
|
+
runBtn.disabled = true;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
152
409
|
runBtn.disabled = false;
|
|
153
410
|
} catch (e) {
|
|
154
411
|
showToast(e.message || 'Invalid JSON.');
|
|
@@ -159,9 +416,16 @@
|
|
|
159
416
|
|
|
160
417
|
runBtn.addEventListener('click', function () {
|
|
161
418
|
if (!parsed) return;
|
|
419
|
+
if (directDepCount(parsed) === 0) {
|
|
420
|
+
showToast(
|
|
421
|
+
'This package.json has no dependencies or devDependencies — nothing to analyze.',
|
|
422
|
+
);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
162
425
|
hideToast();
|
|
426
|
+
var depNames = getDirectDepNames(parsed);
|
|
427
|
+
var progressCtl = showDependencyProgress(depNames);
|
|
163
428
|
runBtn.disabled = true;
|
|
164
|
-
runBtn.textContent = 'Analyzing…';
|
|
165
429
|
var pkgForApi = Object.assign({}, parsed);
|
|
166
430
|
delete pkgForApi.private;
|
|
167
431
|
delete pkgForApi.main;
|
|
@@ -189,13 +453,19 @@
|
|
|
189
453
|
return res.text();
|
|
190
454
|
})
|
|
191
455
|
.then(function (html) {
|
|
192
|
-
|
|
456
|
+
stopProgressSimulation();
|
|
457
|
+
if (progressCtl && progressCtl.flushRemaining) progressCtl.flushRemaining();
|
|
458
|
+
var blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
|
459
|
+
var url = URL.createObjectURL(blob);
|
|
460
|
+
var w = window.open('about:blank', '_blank');
|
|
193
461
|
if (w) {
|
|
194
|
-
w.
|
|
195
|
-
|
|
462
|
+
w.location.href = url;
|
|
463
|
+
setTimeout(function () {
|
|
464
|
+
try {
|
|
465
|
+
URL.revokeObjectURL(url);
|
|
466
|
+
} catch (e) {}
|
|
467
|
+
}, 60000);
|
|
196
468
|
} else {
|
|
197
|
-
var blob = new Blob([html], { type: 'text/html' });
|
|
198
|
-
var url = URL.createObjectURL(blob);
|
|
199
469
|
window.location.href = url;
|
|
200
470
|
}
|
|
201
471
|
})
|
|
@@ -207,6 +477,7 @@
|
|
|
207
477
|
showToast(m);
|
|
208
478
|
})
|
|
209
479
|
.finally(function () {
|
|
480
|
+
hideDependencyProgress();
|
|
210
481
|
runBtn.disabled = false;
|
|
211
482
|
runBtn.textContent = 'Generate report';
|
|
212
483
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.-left-36{left:-9rem}.-right-28{right:-7rem}.bottom-\[18\%\]{bottom:18%}.left-1\/2{left:50%}.right-4{right:1rem}.top-0{top:0}.top-6{top:1.5rem}.top-\[28\%\]{top:28%}.z-0{z-index:0}.z-10{z-index:10}.z-50{z-index:50}.z-\[1\]{z-index:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-14{margin-bottom:3.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.box-border{box-sizing:border-box}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.size-4{width:1rem;height:1rem}.h-\[min\(22rem\2c 45vw\)\]{height:min(22rem,45vw)}.h-\[min\(28rem\2c 50vw\)\]{height:min(28rem,50vw)}.h-\[min\(78vh\2c 600px\)\]{height:min(78vh,600px)}.min-h-\[1\.35rem\]{min-height:1.35rem}.min-h-\[1\.6rem\]{min-height:1.6rem}.min-h-screen{min-height:100vh}.w-\[calc\(100vw_-_2rem\)\]{width:calc(100vw - 2rem)}.w-\[min\(22rem\2c 45vw\)\]{width:min(22rem,45vw)}.w-\[min\(28rem\2c 50vw\)\]{width:min(28rem,50vw)}.w-\[min\(92vw\2c 820px\)\]{width:min(92vw,820px)}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-\[18rem\]{max-width:18rem}.max-w-\[20rem\]{max-width:20rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/3{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/3{--tw-translate-y:-33.333333%}.-translate-y-2{--tw-translate-y:-0.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-\[pulse_2\.2s_ease-in-out_infinite\]{animation:pulse 2.2s ease-in-out infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-r-xl{border-top-right-radius:.75rem;border-bottom-right-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-dashed{border-style:dashed}.border-red-900\/80{border-color:rgba(127,29,29,.8)}.border-zinc-700{--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1))}.border-zinc-700\/75{border-color:rgba(63,63,70,.75)}.border-zinc-700\/80{border-color:rgba(63,63,70,.8)}.border-zinc-800{--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.border-zinc-800\/90{border-color:rgba(39,39,42,.9)}.bg-plexus-base{--tw-bg-opacity:1;background-color:rgb(10 10 10/var(--tw-bg-opacity,1))}.bg-plexus-code{--tw-bg-opacity:1;background-color:rgb(28 28 28/var(--tw-bg-opacity,1))}.bg-plexus-subtle{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-plexus-void\/60{background-color:rgba(5,5,5,.6)}.bg-red-950\/95{background-color:rgba(69,10,10,.95)}.bg-white\/\[0\.028\]{background-color:hsla(0,0%,100%,.028)}.bg-zinc-200{--tw-bg-opacity:1;background-color:rgb(228 228 231/var(--tw-bg-opacity,1))}.bg-zinc-400\/\[0\.04\]{background-color:hsla(240,5%,65%,.04)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-plexus-void\/25{--tw-gradient-from:rgba(5,5,5,.25) var(--tw-gradient-from-position);--tw-gradient-to:rgba(5,5,5,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-white\/\[0\.055\]{--tw-gradient-from:hsla(0,0%,100%,.055) var(--tw-gradient-from-position);--tw-gradient-to:hsla(0,0%,100%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),transparent var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-500\/\[0\.04\]{--tw-gradient-to:hsla(240,4%,46%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),hsla(240,4%,46%,.04) var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-plexus-void\/75{--tw-gradient-to:rgba(5,5,5,.75) var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-light{font-weight:300}.font-medium{font-weight:500}.font-semibold{font-weight:600}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.tracking-tight{letter-spacing:-.025em}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-emerald-400\/85{color:rgba(52,211,153,.85)}.text-red-100{--tw-text-opacity:1;color:rgb(254 226 226/var(--tw-text-opacity,1))}.text-red-300\/90{color:hsla(0,94%,82%,.9)}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-zinc-100{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.text-zinc-200{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.text-zinc-300{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.text-zinc-950{--tw-text-opacity:1;color:rgb(9 9 11/var(--tw-text-opacity,1))}.underline-offset-4{text-underline-offset:4px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-zinc-300{accent-color:#d4d4d8}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-\[0\.4\]{opacity:.4}.shadow-\[0_24px_80px_rgba\(0\2c 0\2c 0\2c 0\.45\)\2c 0_0_0_1px_rgba\(255\2c 255\2c 255\2c 0\.04\)\]{--tw-shadow:0 24px 80px rgba(0,0,0,.45),0 0 0 1px hsla(0,0%,100%,.04);--tw-shadow-colored:0 24px 80px var(--tw-shadow-color),0 0 0 1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_8px_30px_rgba\(0\2c 0\2c 0\2c 0\.45\)\]{--tw-shadow:0 8px 30px rgba(0,0,0,.45);--tw-shadow-colored:0 8px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[inset_0_1px_0_0_rgba\(255\2c 255\2c 255\2c 0\.05\)\]{--tw-shadow:inset 0 1px 0 0 hsla(0,0%,100%,.05);--tw-shadow-colored:inset 0 1px 0 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.blur-3xl{--tw-blur:blur(64px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[background-image\:radial-gradient\(rgba\(255\2c 255\2c 255\2c 0\.042\)_1px\2c transparent_1px\)\]{background-image:radial-gradient(hsla(0,0%,100%,.042) 1px,transparent 0)}.\[background-size\:40px_40px\]{background-size:40px 40px}.\[text-shadow\:0_1px_2px_rgba\(0\2c 0\2c 0\2c 0\.85\)\2c 0_0_40px_rgba\(10\2c 10\2c 10\2c 0\.9\)\]{text-shadow:0 1px 2px rgba(0,0,0,.85),0 0 40px hsla(0,0%,4%,.9)}.hover\:border-zinc-600:hover{--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1))}.hover\:bg-plexus-subtle\/50:hover{background-color:hsla(0,0%,9%,.5)}.hover\:bg-red-900\/60:hover{background-color:rgba(127,29,29,.6)}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:text-zinc-300:hover{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-zinc-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(82 82 91/var(--tw-ring-opacity,1))}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px}.focus\:ring-offset-plexus-base:focus{--tw-ring-offset-color:#0a0a0a}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:hover\:bg-zinc-200:hover:disabled{--tw-bg-opacity:1;background-color:rgb(228 228 231/var(--tw-bg-opacity,1))}@media (min-width:640px){.sm\:-left-24{left:-6rem}.sm\:-right-20{right:-5rem}.sm\:gap-4{gap:1rem}.sm\:p-10{padding:2.5rem}.sm\:px-8{padding-left:2rem;padding-right:2rem}.sm\:py-16{padding-top:4rem;padding-bottom:4rem}.sm\:py-20{padding-top:5rem;padding-bottom:5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:py-6{padding-top:1.5rem;padding-bottom:1.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:mb-16{margin-bottom:4rem}.md\:mb-4{margin-bottom:1rem}.md\:mb-6{margin-bottom:1.5rem}.md\:p-12{padding:3rem}.md\:py-28{padding-top:7rem;padding-bottom:7rem}.md\:text-2xl{font-size:1.5rem;line-height:2rem}.md\:text-6xl{font-size:3.75rem;line-height:1}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:text-7xl{font-size:4.5rem;line-height:1}}
|