@jsenv/core 41.4.10 → 41.4.11
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/dist/build/build.js +22 -1
- package/dist/js/dependency_status.js +233 -2
- package/dist/start_dev_server/start_dev_server.js +29 -2
- package/package.json +1 -1
- package/src/dev/start_dev_server.js +1 -0
- package/src/helpers/watch_dependencies.js +5 -0
- package/src/kitchen/package_dependencies.js +18 -0
- package/src/plugins/dependency_status/client/dependency_status.js +233 -2
- package/src/plugins/dependency_status/jsenv_plugin_dependency_status.js +5 -1
package/dist/build/build.js
CHANGED
|
@@ -196,12 +196,21 @@ const createStatus = (
|
|
|
196
196
|
declaredVersion,
|
|
197
197
|
declaredBy,
|
|
198
198
|
installedVersion: null,
|
|
199
|
+
// the file telling this dependency apart; it is what an install rewrites and
|
|
200
|
+
// what the dev server looks at to know the dependency became the declared one
|
|
201
|
+
watchedPath: null,
|
|
199
202
|
state: "missing",
|
|
200
203
|
};
|
|
201
204
|
const installedDirectoryUrl = findInstalledDirectoryUrl(
|
|
202
205
|
declaringDirectoryUrl,
|
|
203
206
|
packageName,
|
|
204
207
|
);
|
|
208
|
+
status.watchedPath = watchedPathFromDirectoryUrl(
|
|
209
|
+
packageDirectory,
|
|
210
|
+
// not installed yet: name the place where it is expected to appear
|
|
211
|
+
installedDirectoryUrl ||
|
|
212
|
+
`${declaringDirectoryUrl}node_modules/${packageName}/`,
|
|
213
|
+
);
|
|
205
214
|
if (!installedDirectoryUrl) {
|
|
206
215
|
return status;
|
|
207
216
|
}
|
|
@@ -220,6 +229,14 @@ const createStatus = (
|
|
|
220
229
|
return status;
|
|
221
230
|
};
|
|
222
231
|
|
|
232
|
+
const watchedPathFromDirectoryUrl = (packageDirectory, directoryUrl) => {
|
|
233
|
+
const packageJsonUrl = `${directoryUrl}package.json`;
|
|
234
|
+
if (!packageDirectory.url) {
|
|
235
|
+
return packageJsonUrl;
|
|
236
|
+
}
|
|
237
|
+
return urlToRelativeUrl(packageJsonUrl, packageDirectory.url);
|
|
238
|
+
};
|
|
239
|
+
|
|
223
240
|
const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
|
|
224
241
|
let directoryUrl = declaringDirectoryUrl;
|
|
225
242
|
while (directoryUrl) {
|
|
@@ -9298,6 +9315,7 @@ const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
|
|
|
9298
9315
|
const jsenvPluginDependencyStatus = ({
|
|
9299
9316
|
dependencyProblemEventEmitter,
|
|
9300
9317
|
getDependencyProblems,
|
|
9318
|
+
getDependencyWatchInfo = () => ({}),
|
|
9301
9319
|
}) => {
|
|
9302
9320
|
return {
|
|
9303
9321
|
name: "jsenv:dependency_status",
|
|
@@ -9334,7 +9352,10 @@ const jsenvPluginDependencyStatus = ({
|
|
|
9334
9352
|
src: clientReference.generatedSpecifier,
|
|
9335
9353
|
initCall: {
|
|
9336
9354
|
callee: "initDependencyStatus",
|
|
9337
|
-
params: {
|
|
9355
|
+
params: {
|
|
9356
|
+
problems: getDependencyProblems(),
|
|
9357
|
+
watchInfo: getDependencyWatchInfo(),
|
|
9358
|
+
},
|
|
9338
9359
|
},
|
|
9339
9360
|
pluginName: "jsenv:dependency_status",
|
|
9340
9361
|
});
|
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A missing dependency is not handled here: it makes the import fail, so the
|
|
5
5
|
* error overlay already says it, with more precision (which import, where).
|
|
6
|
+
*
|
|
7
|
+
* The overlay does not only describe the problem, it shows the dev server
|
|
8
|
+
* watching for the fix: the paths being looked at are named and kept alive on
|
|
9
|
+
* screen, so waiting for "npm install" feels like waiting for something that is
|
|
10
|
+
* actually going to happen.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
let removeOverlay = () => {};
|
|
14
|
+
let watchInfoFromServer = {};
|
|
15
|
+
// packages listed as outdated at some point, and the path watched for each;
|
|
16
|
+
// what leaves this map has been installed while the overlay was open, which is
|
|
17
|
+
// worth showing as such
|
|
18
|
+
const outdatedPathMap = new Map();
|
|
9
19
|
|
|
10
|
-
const initDependencyStatus = ({ problems }) => {
|
|
20
|
+
const initDependencyStatus = ({ problems, watchInfo }) => {
|
|
21
|
+
watchInfoFromServer = watchInfo || {};
|
|
11
22
|
render(problems);
|
|
12
23
|
// without the server events channel the page only knows the state it was
|
|
13
24
|
// served with, which is still better than nothing
|
|
@@ -24,9 +35,19 @@ const render = (problems) => {
|
|
|
24
35
|
const outdatedList = problems.filter(({ state }) => state === "outdated");
|
|
25
36
|
removeOverlay();
|
|
26
37
|
removeOverlay = () => {};
|
|
38
|
+
const installedList = [];
|
|
39
|
+
for (const [packageName, watchedPath] of outdatedPathMap) {
|
|
40
|
+
if (!outdatedList.some((problem) => problem.packageName === packageName)) {
|
|
41
|
+
installedList.push({ packageName, watchedPath });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
27
44
|
if (outdatedList.length === 0) {
|
|
45
|
+
outdatedPathMap.clear();
|
|
28
46
|
return;
|
|
29
47
|
}
|
|
48
|
+
for (const { packageName, watchedPath } of outdatedList) {
|
|
49
|
+
outdatedPathMap.set(packageName, watchedPath);
|
|
50
|
+
}
|
|
30
51
|
const supervisor = window.__supervisor__;
|
|
31
52
|
if (!supervisor || !supervisor.reportWarning) {
|
|
32
53
|
console.warn(summarize(outdatedList));
|
|
@@ -41,8 +62,8 @@ const render = (problems) => {
|
|
|
41
62
|
details: [
|
|
42
63
|
"The page is running with what is installed in node_modules, which is not what package.json asks for. It may work, but it is not the code the project expects.",
|
|
43
64
|
"Run npm install to fix it. If an install is already running, there is nothing to do but wait.",
|
|
44
|
-
"This page reloads on its own as soon as node_modules matches package.json.",
|
|
45
65
|
],
|
|
66
|
+
node: createWatchNode(outdatedList, installedList),
|
|
46
67
|
});
|
|
47
68
|
};
|
|
48
69
|
|
|
@@ -58,4 +79,214 @@ const summarize = (outdatedList) => {
|
|
|
58
79
|
.join(", ")}`;
|
|
59
80
|
};
|
|
60
81
|
|
|
82
|
+
const createWatchNode = (outdatedList, installedList) => {
|
|
83
|
+
const { packageJsonPath = "package.json", pollInterval } =
|
|
84
|
+
watchInfoFromServer;
|
|
85
|
+
const node = document.createElement("div");
|
|
86
|
+
node.className = "dependency_watch";
|
|
87
|
+
const rows = [];
|
|
88
|
+
rows.push(
|
|
89
|
+
createRow({
|
|
90
|
+
state: "watching",
|
|
91
|
+
path: packageJsonPath,
|
|
92
|
+
// a file watcher, not a poll: an edit here is what puts a dependency out
|
|
93
|
+
// of date in the first place
|
|
94
|
+
note: "on every change",
|
|
95
|
+
pollInterval,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
for (const { packageName, declaredVersion, watchedPath } of outdatedList) {
|
|
99
|
+
rows.push(
|
|
100
|
+
createRow({
|
|
101
|
+
state: "watching",
|
|
102
|
+
path: watchedPath || `node_modules/${packageName}/package.json`,
|
|
103
|
+
note: pollInterval
|
|
104
|
+
? `waiting for ${declaredVersion}, read every ${pollInterval}ms`
|
|
105
|
+
: `waiting for ${declaredVersion}`,
|
|
106
|
+
pollInterval,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
for (const { packageName, watchedPath } of installedList) {
|
|
111
|
+
rows.push(
|
|
112
|
+
createRow({
|
|
113
|
+
state: "done",
|
|
114
|
+
path: watchedPath || `node_modules/${packageName}/package.json`,
|
|
115
|
+
note: "installed",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
node.innerHTML = `
|
|
120
|
+
<style>
|
|
121
|
+
${watchCSS}
|
|
122
|
+
</style>
|
|
123
|
+
<div class="dependency_watch_head">
|
|
124
|
+
<span class="dependency_watch_live">
|
|
125
|
+
<span class="dependency_watch_live_dot"></span>watching
|
|
126
|
+
</span>
|
|
127
|
+
<span class="dependency_watch_head_text">
|
|
128
|
+
the files that will tell the install is over
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="dependency_watch_rows">
|
|
132
|
+
${rows.join("\n ")}
|
|
133
|
+
</div>
|
|
134
|
+
<div class="dependency_watch_effect" data-copy-line>
|
|
135
|
+
As soon as node_modules matches package.json, this page reloads by itself.
|
|
136
|
+
</div>`;
|
|
137
|
+
return node;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const createRow = ({ state, path, note, pollInterval }) => {
|
|
141
|
+
const sweepStyle = pollInterval
|
|
142
|
+
? ` style="animation-duration: ${pollInterval}ms"`
|
|
143
|
+
: "";
|
|
144
|
+
return `<div class="dependency_watch_row" data-state="${state}">
|
|
145
|
+
<span class="dependency_watch_row_icon"></span>
|
|
146
|
+
<code class="dependency_watch_row_path">${escapeHtml(path)}</code>
|
|
147
|
+
<span class="dependency_watch_row_note">${escapeHtml(note)}</span>
|
|
148
|
+
${
|
|
149
|
+
state === "watching"
|
|
150
|
+
? `<span class="dependency_watch_row_sweep"${sweepStyle}></span>`
|
|
151
|
+
: ""
|
|
152
|
+
}
|
|
153
|
+
</div>`;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const escapeHtml = (string) => {
|
|
157
|
+
return String(string)
|
|
158
|
+
.replace(/&/g, "&")
|
|
159
|
+
.replace(/</g, "<")
|
|
160
|
+
.replace(/>/g, ">")
|
|
161
|
+
.replace(/"/g, """)
|
|
162
|
+
.replace(/'/g, "'");
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const watchCSS = /* css */ `
|
|
166
|
+
.dependency_watch_head {
|
|
167
|
+
display: flex;
|
|
168
|
+
margin-bottom: 8px;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
}
|
|
172
|
+
.dependency_watch_live {
|
|
173
|
+
display: flex;
|
|
174
|
+
padding: 2px 8px;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 6px;
|
|
177
|
+
color: #ffab40;
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
text-transform: uppercase;
|
|
180
|
+
letter-spacing: 0.08em;
|
|
181
|
+
border: 1px solid currentColor;
|
|
182
|
+
border-radius: 999px;
|
|
183
|
+
}
|
|
184
|
+
.dependency_watch_live_dot {
|
|
185
|
+
width: 7px;
|
|
186
|
+
height: 7px;
|
|
187
|
+
background: currentColor;
|
|
188
|
+
border-radius: 50%;
|
|
189
|
+
animation: dependency_watch_pulse 1.4s ease-in-out infinite;
|
|
190
|
+
}
|
|
191
|
+
@keyframes dependency_watch_pulse {
|
|
192
|
+
0%,
|
|
193
|
+
100% {
|
|
194
|
+
opacity: 0.25;
|
|
195
|
+
transform: scale(0.8);
|
|
196
|
+
}
|
|
197
|
+
50% {
|
|
198
|
+
opacity: 1;
|
|
199
|
+
transform: scale(1.15);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
.dependency_watch_head_text {
|
|
203
|
+
font-size: 13px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.dependency_watch_rows {
|
|
207
|
+
display: flex;
|
|
208
|
+
flex-direction: column;
|
|
209
|
+
gap: 4px;
|
|
210
|
+
}
|
|
211
|
+
.dependency_watch_row {
|
|
212
|
+
position: relative;
|
|
213
|
+
display: flex;
|
|
214
|
+
padding: 6px 10px;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 8px;
|
|
217
|
+
font-size: 13px;
|
|
218
|
+
background: rgba(255, 255, 255, 0.04);
|
|
219
|
+
border: 1px solid #333;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
overflow: hidden;
|
|
222
|
+
}
|
|
223
|
+
.dependency_watch_row_icon {
|
|
224
|
+
width: 8px;
|
|
225
|
+
height: 8px;
|
|
226
|
+
flex: none;
|
|
227
|
+
border: 1px solid #ffab40;
|
|
228
|
+
border-radius: 50%;
|
|
229
|
+
}
|
|
230
|
+
.dependency_watch_row[data-state="done"] .dependency_watch_row_icon {
|
|
231
|
+
background: #7fd67f;
|
|
232
|
+
border-color: #7fd67f;
|
|
233
|
+
}
|
|
234
|
+
.dependency_watch_row_path {
|
|
235
|
+
color: #eee;
|
|
236
|
+
text-overflow: ellipsis;
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
}
|
|
240
|
+
.dependency_watch_row_note {
|
|
241
|
+
margin-left: auto;
|
|
242
|
+
padding-left: 8px;
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
white-space: nowrap;
|
|
245
|
+
opacity: 0.8;
|
|
246
|
+
}
|
|
247
|
+
.dependency_watch_row[data-state="done"] .dependency_watch_row_note {
|
|
248
|
+
color: #7fd67f;
|
|
249
|
+
opacity: 1;
|
|
250
|
+
}
|
|
251
|
+
/* the sweep is the watching made visible: one pass per read */
|
|
252
|
+
.dependency_watch_row_sweep {
|
|
253
|
+
position: absolute;
|
|
254
|
+
top: 0;
|
|
255
|
+
left: 0;
|
|
256
|
+
width: 35%;
|
|
257
|
+
height: 100%;
|
|
258
|
+
background: linear-gradient(
|
|
259
|
+
90deg,
|
|
260
|
+
transparent,
|
|
261
|
+
rgba(255, 171, 64, 0.16),
|
|
262
|
+
transparent
|
|
263
|
+
);
|
|
264
|
+
animation: dependency_watch_sweep 2s linear infinite;
|
|
265
|
+
pointer-events: none;
|
|
266
|
+
}
|
|
267
|
+
@keyframes dependency_watch_sweep {
|
|
268
|
+
from {
|
|
269
|
+
transform: translateX(-100%);
|
|
270
|
+
}
|
|
271
|
+
to {
|
|
272
|
+
transform: translateX(340%);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.dependency_watch_effect {
|
|
277
|
+
margin-top: 8px;
|
|
278
|
+
color: #ffab40;
|
|
279
|
+
font-size: 13px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (prefers-reduced-motion: reduce) {
|
|
283
|
+
.dependency_watch_live_dot {
|
|
284
|
+
animation: none;
|
|
285
|
+
}
|
|
286
|
+
.dependency_watch_row_sweep {
|
|
287
|
+
display: none;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
|
|
61
292
|
export { initDependencyStatus };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebSocketResponse, pickContentType, ServerEvents, serverPluginErrorHandler, composeTwoResponses, fetchDirectory, serverPluginCORS, jsenvAccessControlAllowedHeaders, startServer } from "@jsenv/server";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
3
|
-
import { registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame,
|
|
3
|
+
import { urlToRelativeUrl, registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, errorToHTML, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, URL_META, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, collectFiles, registerDirectoryLifecycle, asUrlWithoutSearch, readEntryStatSync, ensurePathnameTrailingSlash, compareFileUrls, urlToFilename, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, createDetailedMessage, stringifyUrlSite, injectQueryParamsIntoSpecifier, isSpecifierForNodeBuiltin, injectQueryParams, urlToFileSystemPath, writeFileSync, moveUrl, ensureWindowsDriveLetter, validateResponseIntegrity, setUrlFilename, getCallerPosition, asSpecifierWithoutSearch, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, createLogger, normalizeUrl, ANSI, RUNTIME_COMPAT, formatError, assertAndNormalizeDirectoryUrl, createTaskLog } from "./jsenv_core_packages.js";
|
|
4
4
|
import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
|
|
5
5
|
import { parseHtml, injectJsenvScript, stringifyHtmlAst, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, visitHtmlNodes, parseJsUrls, getUrlForContentInsideJs, applyBabelPlugins, analyzeLinkNode, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, generateUrlForInlineContent, parseJsWithAcorn } from "@jsenv/ast";
|
|
6
6
|
import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
|
|
@@ -144,12 +144,21 @@ const createStatus = (
|
|
|
144
144
|
declaredVersion,
|
|
145
145
|
declaredBy,
|
|
146
146
|
installedVersion: null,
|
|
147
|
+
// the file telling this dependency apart; it is what an install rewrites and
|
|
148
|
+
// what the dev server looks at to know the dependency became the declared one
|
|
149
|
+
watchedPath: null,
|
|
147
150
|
state: "missing",
|
|
148
151
|
};
|
|
149
152
|
const installedDirectoryUrl = findInstalledDirectoryUrl(
|
|
150
153
|
declaringDirectoryUrl,
|
|
151
154
|
packageName,
|
|
152
155
|
);
|
|
156
|
+
status.watchedPath = watchedPathFromDirectoryUrl(
|
|
157
|
+
packageDirectory,
|
|
158
|
+
// not installed yet: name the place where it is expected to appear
|
|
159
|
+
installedDirectoryUrl ||
|
|
160
|
+
`${declaringDirectoryUrl}node_modules/${packageName}/`,
|
|
161
|
+
);
|
|
153
162
|
if (!installedDirectoryUrl) {
|
|
154
163
|
return status;
|
|
155
164
|
}
|
|
@@ -168,6 +177,14 @@ const createStatus = (
|
|
|
168
177
|
return status;
|
|
169
178
|
};
|
|
170
179
|
|
|
180
|
+
const watchedPathFromDirectoryUrl = (packageDirectory, directoryUrl) => {
|
|
181
|
+
const packageJsonUrl = `${directoryUrl}package.json`;
|
|
182
|
+
if (!packageDirectory.url) {
|
|
183
|
+
return packageJsonUrl;
|
|
184
|
+
}
|
|
185
|
+
return urlToRelativeUrl(packageJsonUrl, packageDirectory.url);
|
|
186
|
+
};
|
|
187
|
+
|
|
171
188
|
const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
|
|
172
189
|
let directoryUrl = declaringDirectoryUrl;
|
|
173
190
|
while (directoryUrl) {
|
|
@@ -232,8 +249,13 @@ const watchDependencies = (
|
|
|
232
249
|
{ onProblem, onInstalled, onChange, pollInterval = POLL_INTERVAL },
|
|
233
250
|
) => {
|
|
234
251
|
let problemMap = new Map();
|
|
252
|
+
// every path given to the browser is relative to the package directory, the
|
|
253
|
+
// one holding the package.json being watched
|
|
254
|
+
const packageJsonPath = "package.json";
|
|
235
255
|
const watcher = {
|
|
236
256
|
getProblems: () => Array.from(problemMap.values()),
|
|
257
|
+
// what the browser needs to display the watching in progress
|
|
258
|
+
getWatchInfo: () => ({ packageJsonPath, pollInterval }),
|
|
237
259
|
stop: () => {},
|
|
238
260
|
};
|
|
239
261
|
if (!packageDirectory.url) {
|
|
@@ -7122,6 +7144,7 @@ const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
|
|
|
7122
7144
|
const jsenvPluginDependencyStatus = ({
|
|
7123
7145
|
dependencyProblemEventEmitter,
|
|
7124
7146
|
getDependencyProblems,
|
|
7147
|
+
getDependencyWatchInfo = () => ({}),
|
|
7125
7148
|
}) => {
|
|
7126
7149
|
return {
|
|
7127
7150
|
name: "jsenv:dependency_status",
|
|
@@ -7158,7 +7181,10 @@ const jsenvPluginDependencyStatus = ({
|
|
|
7158
7181
|
src: clientReference.generatedSpecifier,
|
|
7159
7182
|
initCall: {
|
|
7160
7183
|
callee: "initDependencyStatus",
|
|
7161
|
-
params: {
|
|
7184
|
+
params: {
|
|
7185
|
+
problems: getDependencyProblems(),
|
|
7186
|
+
watchInfo: getDependencyWatchInfo(),
|
|
7187
|
+
},
|
|
7162
7188
|
},
|
|
7163
7189
|
pluginName: "jsenv:dependency_status",
|
|
7164
7190
|
});
|
|
@@ -11882,6 +11908,7 @@ const startDevServer = async ({
|
|
|
11882
11908
|
dependencyStatus: {
|
|
11883
11909
|
dependencyProblemEventEmitter,
|
|
11884
11910
|
getDependencyProblems: dependencyWatcher.getProblems,
|
|
11911
|
+
getDependencyWatchInfo: dependencyWatcher.getWatchInfo,
|
|
11885
11912
|
},
|
|
11886
11913
|
cacheControl,
|
|
11887
11914
|
ribbon,
|
package/package.json
CHANGED
|
@@ -23,8 +23,13 @@ export const watchDependencies = (
|
|
|
23
23
|
{ onProblem, onInstalled, onChange, pollInterval = POLL_INTERVAL },
|
|
24
24
|
) => {
|
|
25
25
|
let problemMap = new Map();
|
|
26
|
+
// every path given to the browser is relative to the package directory, the
|
|
27
|
+
// one holding the package.json being watched
|
|
28
|
+
const packageJsonPath = "package.json";
|
|
26
29
|
const watcher = {
|
|
27
30
|
getProblems: () => Array.from(problemMap.values()),
|
|
31
|
+
// what the browser needs to display the watching in progress
|
|
32
|
+
getWatchInfo: () => ({ packageJsonPath, pollInterval }),
|
|
28
33
|
stop: () => {},
|
|
29
34
|
};
|
|
30
35
|
if (!packageDirectory.url) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* would pick, which is way beyond what is needed here.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { urlToRelativeUrl } from "@jsenv/urls";
|
|
15
16
|
import { existsSync } from "node:fs";
|
|
16
17
|
|
|
17
18
|
const DEPENDENCY_FIELDS = [
|
|
@@ -93,12 +94,21 @@ const createStatus = (
|
|
|
93
94
|
declaredVersion,
|
|
94
95
|
declaredBy,
|
|
95
96
|
installedVersion: null,
|
|
97
|
+
// the file telling this dependency apart; it is what an install rewrites and
|
|
98
|
+
// what the dev server looks at to know the dependency became the declared one
|
|
99
|
+
watchedPath: null,
|
|
96
100
|
state: "missing",
|
|
97
101
|
};
|
|
98
102
|
const installedDirectoryUrl = findInstalledDirectoryUrl(
|
|
99
103
|
declaringDirectoryUrl,
|
|
100
104
|
packageName,
|
|
101
105
|
);
|
|
106
|
+
status.watchedPath = watchedPathFromDirectoryUrl(
|
|
107
|
+
packageDirectory,
|
|
108
|
+
// not installed yet: name the place where it is expected to appear
|
|
109
|
+
installedDirectoryUrl ||
|
|
110
|
+
`${declaringDirectoryUrl}node_modules/${packageName}/`,
|
|
111
|
+
);
|
|
102
112
|
if (!installedDirectoryUrl) {
|
|
103
113
|
return status;
|
|
104
114
|
}
|
|
@@ -117,6 +127,14 @@ const createStatus = (
|
|
|
117
127
|
return status;
|
|
118
128
|
};
|
|
119
129
|
|
|
130
|
+
const watchedPathFromDirectoryUrl = (packageDirectory, directoryUrl) => {
|
|
131
|
+
const packageJsonUrl = `${directoryUrl}package.json`;
|
|
132
|
+
if (!packageDirectory.url) {
|
|
133
|
+
return packageJsonUrl;
|
|
134
|
+
}
|
|
135
|
+
return urlToRelativeUrl(packageJsonUrl, packageDirectory.url);
|
|
136
|
+
};
|
|
137
|
+
|
|
120
138
|
const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
|
|
121
139
|
let directoryUrl = declaringDirectoryUrl;
|
|
122
140
|
while (directoryUrl) {
|
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A missing dependency is not handled here: it makes the import fail, so the
|
|
5
5
|
* error overlay already says it, with more precision (which import, where).
|
|
6
|
+
*
|
|
7
|
+
* The overlay does not only describe the problem, it shows the dev server
|
|
8
|
+
* watching for the fix: the paths being looked at are named and kept alive on
|
|
9
|
+
* screen, so waiting for "npm install" feels like waiting for something that is
|
|
10
|
+
* actually going to happen.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
let removeOverlay = () => {};
|
|
14
|
+
let watchInfoFromServer = {};
|
|
15
|
+
// packages listed as outdated at some point, and the path watched for each;
|
|
16
|
+
// what leaves this map has been installed while the overlay was open, which is
|
|
17
|
+
// worth showing as such
|
|
18
|
+
const outdatedPathMap = new Map();
|
|
9
19
|
|
|
10
|
-
export const initDependencyStatus = ({ problems }) => {
|
|
20
|
+
export const initDependencyStatus = ({ problems, watchInfo }) => {
|
|
21
|
+
watchInfoFromServer = watchInfo || {};
|
|
11
22
|
render(problems);
|
|
12
23
|
// without the server events channel the page only knows the state it was
|
|
13
24
|
// served with, which is still better than nothing
|
|
@@ -24,9 +35,19 @@ const render = (problems) => {
|
|
|
24
35
|
const outdatedList = problems.filter(({ state }) => state === "outdated");
|
|
25
36
|
removeOverlay();
|
|
26
37
|
removeOverlay = () => {};
|
|
38
|
+
const installedList = [];
|
|
39
|
+
for (const [packageName, watchedPath] of outdatedPathMap) {
|
|
40
|
+
if (!outdatedList.some((problem) => problem.packageName === packageName)) {
|
|
41
|
+
installedList.push({ packageName, watchedPath });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
27
44
|
if (outdatedList.length === 0) {
|
|
45
|
+
outdatedPathMap.clear();
|
|
28
46
|
return;
|
|
29
47
|
}
|
|
48
|
+
for (const { packageName, watchedPath } of outdatedList) {
|
|
49
|
+
outdatedPathMap.set(packageName, watchedPath);
|
|
50
|
+
}
|
|
30
51
|
const supervisor = window.__supervisor__;
|
|
31
52
|
if (!supervisor || !supervisor.reportWarning) {
|
|
32
53
|
console.warn(summarize(outdatedList));
|
|
@@ -41,8 +62,8 @@ const render = (problems) => {
|
|
|
41
62
|
details: [
|
|
42
63
|
"The page is running with what is installed in node_modules, which is not what package.json asks for. It may work, but it is not the code the project expects.",
|
|
43
64
|
"Run npm install to fix it. If an install is already running, there is nothing to do but wait.",
|
|
44
|
-
"This page reloads on its own as soon as node_modules matches package.json.",
|
|
45
65
|
],
|
|
66
|
+
node: createWatchNode(outdatedList, installedList),
|
|
46
67
|
});
|
|
47
68
|
};
|
|
48
69
|
|
|
@@ -57,3 +78,213 @@ const summarize = (outdatedList) => {
|
|
|
57
78
|
.map(({ packageName }) => packageName)
|
|
58
79
|
.join(", ")}`;
|
|
59
80
|
};
|
|
81
|
+
|
|
82
|
+
const createWatchNode = (outdatedList, installedList) => {
|
|
83
|
+
const { packageJsonPath = "package.json", pollInterval } =
|
|
84
|
+
watchInfoFromServer;
|
|
85
|
+
const node = document.createElement("div");
|
|
86
|
+
node.className = "dependency_watch";
|
|
87
|
+
const rows = [];
|
|
88
|
+
rows.push(
|
|
89
|
+
createRow({
|
|
90
|
+
state: "watching",
|
|
91
|
+
path: packageJsonPath,
|
|
92
|
+
// a file watcher, not a poll: an edit here is what puts a dependency out
|
|
93
|
+
// of date in the first place
|
|
94
|
+
note: "on every change",
|
|
95
|
+
pollInterval,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
for (const { packageName, declaredVersion, watchedPath } of outdatedList) {
|
|
99
|
+
rows.push(
|
|
100
|
+
createRow({
|
|
101
|
+
state: "watching",
|
|
102
|
+
path: watchedPath || `node_modules/${packageName}/package.json`,
|
|
103
|
+
note: pollInterval
|
|
104
|
+
? `waiting for ${declaredVersion}, read every ${pollInterval}ms`
|
|
105
|
+
: `waiting for ${declaredVersion}`,
|
|
106
|
+
pollInterval,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
for (const { packageName, watchedPath } of installedList) {
|
|
111
|
+
rows.push(
|
|
112
|
+
createRow({
|
|
113
|
+
state: "done",
|
|
114
|
+
path: watchedPath || `node_modules/${packageName}/package.json`,
|
|
115
|
+
note: "installed",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
node.innerHTML = `
|
|
120
|
+
<style>
|
|
121
|
+
${watchCSS}
|
|
122
|
+
</style>
|
|
123
|
+
<div class="dependency_watch_head">
|
|
124
|
+
<span class="dependency_watch_live">
|
|
125
|
+
<span class="dependency_watch_live_dot"></span>watching
|
|
126
|
+
</span>
|
|
127
|
+
<span class="dependency_watch_head_text">
|
|
128
|
+
the files that will tell the install is over
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="dependency_watch_rows">
|
|
132
|
+
${rows.join("\n ")}
|
|
133
|
+
</div>
|
|
134
|
+
<div class="dependency_watch_effect" data-copy-line>
|
|
135
|
+
As soon as node_modules matches package.json, this page reloads by itself.
|
|
136
|
+
</div>`;
|
|
137
|
+
return node;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const createRow = ({ state, path, note, pollInterval }) => {
|
|
141
|
+
const sweepStyle = pollInterval
|
|
142
|
+
? ` style="animation-duration: ${pollInterval}ms"`
|
|
143
|
+
: "";
|
|
144
|
+
return `<div class="dependency_watch_row" data-state="${state}">
|
|
145
|
+
<span class="dependency_watch_row_icon"></span>
|
|
146
|
+
<code class="dependency_watch_row_path">${escapeHtml(path)}</code>
|
|
147
|
+
<span class="dependency_watch_row_note">${escapeHtml(note)}</span>
|
|
148
|
+
${
|
|
149
|
+
state === "watching"
|
|
150
|
+
? `<span class="dependency_watch_row_sweep"${sweepStyle}></span>`
|
|
151
|
+
: ""
|
|
152
|
+
}
|
|
153
|
+
</div>`;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const escapeHtml = (string) => {
|
|
157
|
+
return String(string)
|
|
158
|
+
.replace(/&/g, "&")
|
|
159
|
+
.replace(/</g, "<")
|
|
160
|
+
.replace(/>/g, ">")
|
|
161
|
+
.replace(/"/g, """)
|
|
162
|
+
.replace(/'/g, "'");
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const watchCSS = /* css */ `
|
|
166
|
+
.dependency_watch_head {
|
|
167
|
+
display: flex;
|
|
168
|
+
margin-bottom: 8px;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
}
|
|
172
|
+
.dependency_watch_live {
|
|
173
|
+
display: flex;
|
|
174
|
+
padding: 2px 8px;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 6px;
|
|
177
|
+
color: #ffab40;
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
text-transform: uppercase;
|
|
180
|
+
letter-spacing: 0.08em;
|
|
181
|
+
border: 1px solid currentColor;
|
|
182
|
+
border-radius: 999px;
|
|
183
|
+
}
|
|
184
|
+
.dependency_watch_live_dot {
|
|
185
|
+
width: 7px;
|
|
186
|
+
height: 7px;
|
|
187
|
+
background: currentColor;
|
|
188
|
+
border-radius: 50%;
|
|
189
|
+
animation: dependency_watch_pulse 1.4s ease-in-out infinite;
|
|
190
|
+
}
|
|
191
|
+
@keyframes dependency_watch_pulse {
|
|
192
|
+
0%,
|
|
193
|
+
100% {
|
|
194
|
+
opacity: 0.25;
|
|
195
|
+
transform: scale(0.8);
|
|
196
|
+
}
|
|
197
|
+
50% {
|
|
198
|
+
opacity: 1;
|
|
199
|
+
transform: scale(1.15);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
.dependency_watch_head_text {
|
|
203
|
+
font-size: 13px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.dependency_watch_rows {
|
|
207
|
+
display: flex;
|
|
208
|
+
flex-direction: column;
|
|
209
|
+
gap: 4px;
|
|
210
|
+
}
|
|
211
|
+
.dependency_watch_row {
|
|
212
|
+
position: relative;
|
|
213
|
+
display: flex;
|
|
214
|
+
padding: 6px 10px;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 8px;
|
|
217
|
+
font-size: 13px;
|
|
218
|
+
background: rgba(255, 255, 255, 0.04);
|
|
219
|
+
border: 1px solid #333;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
overflow: hidden;
|
|
222
|
+
}
|
|
223
|
+
.dependency_watch_row_icon {
|
|
224
|
+
width: 8px;
|
|
225
|
+
height: 8px;
|
|
226
|
+
flex: none;
|
|
227
|
+
border: 1px solid #ffab40;
|
|
228
|
+
border-radius: 50%;
|
|
229
|
+
}
|
|
230
|
+
.dependency_watch_row[data-state="done"] .dependency_watch_row_icon {
|
|
231
|
+
background: #7fd67f;
|
|
232
|
+
border-color: #7fd67f;
|
|
233
|
+
}
|
|
234
|
+
.dependency_watch_row_path {
|
|
235
|
+
color: #eee;
|
|
236
|
+
text-overflow: ellipsis;
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
}
|
|
240
|
+
.dependency_watch_row_note {
|
|
241
|
+
margin-left: auto;
|
|
242
|
+
padding-left: 8px;
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
white-space: nowrap;
|
|
245
|
+
opacity: 0.8;
|
|
246
|
+
}
|
|
247
|
+
.dependency_watch_row[data-state="done"] .dependency_watch_row_note {
|
|
248
|
+
color: #7fd67f;
|
|
249
|
+
opacity: 1;
|
|
250
|
+
}
|
|
251
|
+
/* the sweep is the watching made visible: one pass per read */
|
|
252
|
+
.dependency_watch_row_sweep {
|
|
253
|
+
position: absolute;
|
|
254
|
+
top: 0;
|
|
255
|
+
left: 0;
|
|
256
|
+
width: 35%;
|
|
257
|
+
height: 100%;
|
|
258
|
+
background: linear-gradient(
|
|
259
|
+
90deg,
|
|
260
|
+
transparent,
|
|
261
|
+
rgba(255, 171, 64, 0.16),
|
|
262
|
+
transparent
|
|
263
|
+
);
|
|
264
|
+
animation: dependency_watch_sweep 2s linear infinite;
|
|
265
|
+
pointer-events: none;
|
|
266
|
+
}
|
|
267
|
+
@keyframes dependency_watch_sweep {
|
|
268
|
+
from {
|
|
269
|
+
transform: translateX(-100%);
|
|
270
|
+
}
|
|
271
|
+
to {
|
|
272
|
+
transform: translateX(340%);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.dependency_watch_effect {
|
|
277
|
+
margin-top: 8px;
|
|
278
|
+
color: #ffab40;
|
|
279
|
+
font-size: 13px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (prefers-reduced-motion: reduce) {
|
|
283
|
+
.dependency_watch_live_dot {
|
|
284
|
+
animation: none;
|
|
285
|
+
}
|
|
286
|
+
.dependency_watch_row_sweep {
|
|
287
|
+
display: none;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
@@ -15,6 +15,7 @@ const clientFileUrl = import.meta.resolve("./client/dependency_status.js");
|
|
|
15
15
|
export const jsenvPluginDependencyStatus = ({
|
|
16
16
|
dependencyProblemEventEmitter,
|
|
17
17
|
getDependencyProblems,
|
|
18
|
+
getDependencyWatchInfo = () => ({}),
|
|
18
19
|
}) => {
|
|
19
20
|
return {
|
|
20
21
|
name: "jsenv:dependency_status",
|
|
@@ -51,7 +52,10 @@ export const jsenvPluginDependencyStatus = ({
|
|
|
51
52
|
src: clientReference.generatedSpecifier,
|
|
52
53
|
initCall: {
|
|
53
54
|
callee: "initDependencyStatus",
|
|
54
|
-
params: {
|
|
55
|
+
params: {
|
|
56
|
+
problems: getDependencyProblems(),
|
|
57
|
+
watchInfo: getDependencyWatchInfo(),
|
|
58
|
+
},
|
|
55
59
|
},
|
|
56
60
|
pluginName: "jsenv:dependency_status",
|
|
57
61
|
});
|