@jsenv/core 41.4.9 → 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/browserslist_index/browserslist_index.js +60 -31
- package/dist/build/build.js +66 -13
- package/dist/js/dependency_status.js +233 -2
- package/dist/start_dev_server/start_dev_server.js +85 -15
- package/package.json +1 -1
- package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +12 -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/src/plugins/directory_reference_effect/jsenv_plugin_directory_reference_effect.js +3 -1
- package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +41 -11
|
@@ -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
|
});
|
|
@@ -54,7 +54,9 @@ export const jsenvPluginDirectoryReferenceEffect = (
|
|
|
54
54
|
} else {
|
|
55
55
|
const directoryRelativeUrl = urlToRelativeUrl(
|
|
56
56
|
reference.url,
|
|
57
|
-
reference
|
|
57
|
+
// the root url info has no originalUrl; it owns the reference
|
|
58
|
+
// created for an incoming request ("http_request")
|
|
59
|
+
reference.ownerUrlInfo.originalUrl || reference.ownerUrlInfo.url,
|
|
58
60
|
);
|
|
59
61
|
reference.filenameHint = directoryRelativeUrl;
|
|
60
62
|
}
|
|
@@ -67,6 +67,9 @@ export const jsenvPluginFsRedirection = ({
|
|
|
67
67
|
const { search, hash } = urlObject;
|
|
68
68
|
urlObject.search = "";
|
|
69
69
|
urlObject.hash = "";
|
|
70
|
+
// must be read before applyFsStatEffectsOnUrlObject which forces the
|
|
71
|
+
// trailing slash on directories
|
|
72
|
+
const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
|
|
70
73
|
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
71
74
|
const shouldApplyFilesystemMagicResolution =
|
|
72
75
|
reference.type === "js_import";
|
|
@@ -96,20 +99,16 @@ export const jsenvPluginFsRedirection = ({
|
|
|
96
99
|
// url has an extension, we assume it's a file request -> let 404 happen
|
|
97
100
|
return null;
|
|
98
101
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// the SPA fallback answers a request; during build there is none
|
|
102
|
+
if (specifierUsesTrailingSlash) {
|
|
103
|
+
// the trailing slash asks for a directory and there is none here
|
|
104
|
+
// -> let 404 happen (same reasoning as the extension above)
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
if (closestHtmlRootFile) {
|
|
110
|
-
return closestHtmlRootFile;
|
|
107
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
108
|
+
if (spaFallbackUrl) {
|
|
109
|
+
return spaFallbackUrl;
|
|
111
110
|
}
|
|
112
|
-
return
|
|
111
|
+
return null;
|
|
113
112
|
}
|
|
114
113
|
if (fsStat.isDirectory()) {
|
|
115
114
|
// When requesting a directory, check if we have an HTML entry file for that directory
|
|
@@ -118,6 +117,21 @@ export const jsenvPluginFsRedirection = ({
|
|
|
118
117
|
reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
|
|
119
118
|
return directoryEntryFileUrl;
|
|
120
119
|
}
|
|
120
|
+
if (!specifierUsesTrailingSlash) {
|
|
121
|
+
// the trailing slash is what tells a directory apart from a route:
|
|
122
|
+
// "/join/" is the directory, "/join" is a route owned by the SPA
|
|
123
|
+
// even when "join/" exists in the source files.
|
|
124
|
+
// Without this a source directory would shadow the route having
|
|
125
|
+
// the same name and the SPA would be unreachable in dev while
|
|
126
|
+
// being perfectly fine once built
|
|
127
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
128
|
+
if (spaFallbackUrl) {
|
|
129
|
+
reference.fsStat = readEntryStatSync(spaFallbackUrl, {
|
|
130
|
+
nullIfNotFound: true,
|
|
131
|
+
});
|
|
132
|
+
return spaFallbackUrl;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
137
|
if (!fsStat) {
|
|
@@ -180,6 +194,22 @@ const getDirectoryEntryFileUrl = (directoryUrl) => {
|
|
|
180
194
|
}
|
|
181
195
|
return null;
|
|
182
196
|
};
|
|
197
|
+
const getSpaFallbackUrl = (reference) => {
|
|
198
|
+
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
199
|
+
reference.ownerUrlInfo.context;
|
|
200
|
+
if (!requestedUrl) {
|
|
201
|
+
// the SPA fallback answers a request; during build there is none
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const closestHtmlRootFile = getClosestHtmlRootFile(
|
|
205
|
+
requestedUrl,
|
|
206
|
+
rootDirectoryUrl,
|
|
207
|
+
);
|
|
208
|
+
if (closestHtmlRootFile) {
|
|
209
|
+
return closestHtmlRootFile;
|
|
210
|
+
}
|
|
211
|
+
return String(new URL(mainFilePath, rootDirectoryUrl));
|
|
212
|
+
};
|
|
183
213
|
const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
|
|
184
214
|
let directoryUrl = new URL("./", requestedUrl);
|
|
185
215
|
while (true) {
|