@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
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) {
|
|
@@ -4119,7 +4136,9 @@ const jsenvPluginDirectoryReferenceEffect = (
|
|
|
4119
4136
|
} else if (reference.specifierPathname.endsWith("./")) ; else {
|
|
4120
4137
|
const directoryRelativeUrl = urlToRelativeUrl(
|
|
4121
4138
|
reference.url,
|
|
4122
|
-
reference
|
|
4139
|
+
// the root url info has no originalUrl; it owns the reference
|
|
4140
|
+
// created for an incoming request ("http_request")
|
|
4141
|
+
reference.ownerUrlInfo.originalUrl || reference.ownerUrlInfo.url,
|
|
4123
4142
|
);
|
|
4124
4143
|
reference.filenameHint = directoryRelativeUrl;
|
|
4125
4144
|
}
|
|
@@ -7242,6 +7261,9 @@ const jsenvPluginFsRedirection = ({
|
|
|
7242
7261
|
const { search, hash } = urlObject;
|
|
7243
7262
|
urlObject.search = "";
|
|
7244
7263
|
urlObject.hash = "";
|
|
7264
|
+
// must be read before applyFsStatEffectsOnUrlObject which forces the
|
|
7265
|
+
// trailing slash on directories
|
|
7266
|
+
const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
|
|
7245
7267
|
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
7246
7268
|
const shouldApplyFilesystemMagicResolution =
|
|
7247
7269
|
reference.type === "js_import";
|
|
@@ -7271,20 +7293,16 @@ const jsenvPluginFsRedirection = ({
|
|
|
7271
7293
|
// url has an extension, we assume it's a file request -> let 404 happen
|
|
7272
7294
|
return null;
|
|
7273
7295
|
}
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
// the SPA fallback answers a request; during build there is none
|
|
7296
|
+
if (specifierUsesTrailingSlash) {
|
|
7297
|
+
// the trailing slash asks for a directory and there is none here
|
|
7298
|
+
// -> let 404 happen (same reasoning as the extension above)
|
|
7278
7299
|
return null;
|
|
7279
7300
|
}
|
|
7280
|
-
const
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
);
|
|
7284
|
-
if (closestHtmlRootFile) {
|
|
7285
|
-
return closestHtmlRootFile;
|
|
7301
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
7302
|
+
if (spaFallbackUrl) {
|
|
7303
|
+
return spaFallbackUrl;
|
|
7286
7304
|
}
|
|
7287
|
-
return
|
|
7305
|
+
return null;
|
|
7288
7306
|
}
|
|
7289
7307
|
if (fsStat.isDirectory()) {
|
|
7290
7308
|
// When requesting a directory, check if we have an HTML entry file for that directory
|
|
@@ -7293,6 +7311,21 @@ const jsenvPluginFsRedirection = ({
|
|
|
7293
7311
|
reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
|
|
7294
7312
|
return directoryEntryFileUrl;
|
|
7295
7313
|
}
|
|
7314
|
+
if (!specifierUsesTrailingSlash) {
|
|
7315
|
+
// the trailing slash is what tells a directory apart from a route:
|
|
7316
|
+
// "/join/" is the directory, "/join" is a route owned by the SPA
|
|
7317
|
+
// even when "join/" exists in the source files.
|
|
7318
|
+
// Without this a source directory would shadow the route having
|
|
7319
|
+
// the same name and the SPA would be unreachable in dev while
|
|
7320
|
+
// being perfectly fine once built
|
|
7321
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
7322
|
+
if (spaFallbackUrl) {
|
|
7323
|
+
reference.fsStat = readEntryStatSync(spaFallbackUrl, {
|
|
7324
|
+
nullIfNotFound: true,
|
|
7325
|
+
});
|
|
7326
|
+
return spaFallbackUrl;
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7296
7329
|
}
|
|
7297
7330
|
}
|
|
7298
7331
|
if (!fsStat) {
|
|
@@ -7355,6 +7388,22 @@ const getDirectoryEntryFileUrl = (directoryUrl) => {
|
|
|
7355
7388
|
}
|
|
7356
7389
|
return null;
|
|
7357
7390
|
};
|
|
7391
|
+
const getSpaFallbackUrl = (reference) => {
|
|
7392
|
+
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
7393
|
+
reference.ownerUrlInfo.context;
|
|
7394
|
+
if (!requestedUrl) {
|
|
7395
|
+
// the SPA fallback answers a request; during build there is none
|
|
7396
|
+
return null;
|
|
7397
|
+
}
|
|
7398
|
+
const closestHtmlRootFile = getClosestHtmlRootFile(
|
|
7399
|
+
requestedUrl,
|
|
7400
|
+
rootDirectoryUrl,
|
|
7401
|
+
);
|
|
7402
|
+
if (closestHtmlRootFile) {
|
|
7403
|
+
return closestHtmlRootFile;
|
|
7404
|
+
}
|
|
7405
|
+
return String(new URL(mainFilePath, rootDirectoryUrl));
|
|
7406
|
+
};
|
|
7358
7407
|
const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
|
|
7359
7408
|
let directoryUrl = new URL("./", requestedUrl);
|
|
7360
7409
|
while (true) {
|
|
@@ -9266,6 +9315,7 @@ const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
|
|
|
9266
9315
|
const jsenvPluginDependencyStatus = ({
|
|
9267
9316
|
dependencyProblemEventEmitter,
|
|
9268
9317
|
getDependencyProblems,
|
|
9318
|
+
getDependencyWatchInfo = () => ({}),
|
|
9269
9319
|
}) => {
|
|
9270
9320
|
return {
|
|
9271
9321
|
name: "jsenv:dependency_status",
|
|
@@ -9302,7 +9352,10 @@ const jsenvPluginDependencyStatus = ({
|
|
|
9302
9352
|
src: clientReference.generatedSpecifier,
|
|
9303
9353
|
initCall: {
|
|
9304
9354
|
callee: "initDependencyStatus",
|
|
9305
|
-
params: {
|
|
9355
|
+
params: {
|
|
9356
|
+
problems: getDependencyProblems(),
|
|
9357
|
+
watchInfo: getDependencyWatchInfo(),
|
|
9358
|
+
},
|
|
9306
9359
|
},
|
|
9307
9360
|
pluginName: "jsenv:dependency_status",
|
|
9308
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) {
|
|
@@ -4075,6 +4097,9 @@ const jsenvPluginFsRedirection = ({
|
|
|
4075
4097
|
const { search, hash } = urlObject;
|
|
4076
4098
|
urlObject.search = "";
|
|
4077
4099
|
urlObject.hash = "";
|
|
4100
|
+
// must be read before applyFsStatEffectsOnUrlObject which forces the
|
|
4101
|
+
// trailing slash on directories
|
|
4102
|
+
const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
|
|
4078
4103
|
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
4079
4104
|
const shouldApplyFilesystemMagicResolution =
|
|
4080
4105
|
reference.type === "js_import";
|
|
@@ -4104,20 +4129,16 @@ const jsenvPluginFsRedirection = ({
|
|
|
4104
4129
|
// url has an extension, we assume it's a file request -> let 404 happen
|
|
4105
4130
|
return null;
|
|
4106
4131
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
// the SPA fallback answers a request; during build there is none
|
|
4132
|
+
if (specifierUsesTrailingSlash) {
|
|
4133
|
+
// the trailing slash asks for a directory and there is none here
|
|
4134
|
+
// -> let 404 happen (same reasoning as the extension above)
|
|
4111
4135
|
return null;
|
|
4112
4136
|
}
|
|
4113
|
-
const
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
);
|
|
4117
|
-
if (closestHtmlRootFile) {
|
|
4118
|
-
return closestHtmlRootFile;
|
|
4137
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
4138
|
+
if (spaFallbackUrl) {
|
|
4139
|
+
return spaFallbackUrl;
|
|
4119
4140
|
}
|
|
4120
|
-
return
|
|
4141
|
+
return null;
|
|
4121
4142
|
}
|
|
4122
4143
|
if (fsStat.isDirectory()) {
|
|
4123
4144
|
// When requesting a directory, check if we have an HTML entry file for that directory
|
|
@@ -4126,6 +4147,21 @@ const jsenvPluginFsRedirection = ({
|
|
|
4126
4147
|
reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
|
|
4127
4148
|
return directoryEntryFileUrl;
|
|
4128
4149
|
}
|
|
4150
|
+
if (!specifierUsesTrailingSlash) {
|
|
4151
|
+
// the trailing slash is what tells a directory apart from a route:
|
|
4152
|
+
// "/join/" is the directory, "/join" is a route owned by the SPA
|
|
4153
|
+
// even when "join/" exists in the source files.
|
|
4154
|
+
// Without this a source directory would shadow the route having
|
|
4155
|
+
// the same name and the SPA would be unreachable in dev while
|
|
4156
|
+
// being perfectly fine once built
|
|
4157
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
4158
|
+
if (spaFallbackUrl) {
|
|
4159
|
+
reference.fsStat = readEntryStatSync(spaFallbackUrl, {
|
|
4160
|
+
nullIfNotFound: true,
|
|
4161
|
+
});
|
|
4162
|
+
return spaFallbackUrl;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4129
4165
|
}
|
|
4130
4166
|
}
|
|
4131
4167
|
if (!fsStat) {
|
|
@@ -4188,6 +4224,22 @@ const getDirectoryEntryFileUrl = (directoryUrl) => {
|
|
|
4188
4224
|
}
|
|
4189
4225
|
return null;
|
|
4190
4226
|
};
|
|
4227
|
+
const getSpaFallbackUrl = (reference) => {
|
|
4228
|
+
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
4229
|
+
reference.ownerUrlInfo.context;
|
|
4230
|
+
if (!requestedUrl) {
|
|
4231
|
+
// the SPA fallback answers a request; during build there is none
|
|
4232
|
+
return null;
|
|
4233
|
+
}
|
|
4234
|
+
const closestHtmlRootFile = getClosestHtmlRootFile(
|
|
4235
|
+
requestedUrl,
|
|
4236
|
+
rootDirectoryUrl,
|
|
4237
|
+
);
|
|
4238
|
+
if (closestHtmlRootFile) {
|
|
4239
|
+
return closestHtmlRootFile;
|
|
4240
|
+
}
|
|
4241
|
+
return String(new URL(mainFilePath, rootDirectoryUrl));
|
|
4242
|
+
};
|
|
4191
4243
|
const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
|
|
4192
4244
|
let directoryUrl = new URL("./", requestedUrl);
|
|
4193
4245
|
while (true) {
|
|
@@ -4995,7 +5047,9 @@ const jsenvPluginDirectoryReferenceEffect = (
|
|
|
4995
5047
|
} else if (reference.specifierPathname.endsWith("./")) ; else {
|
|
4996
5048
|
const directoryRelativeUrl = urlToRelativeUrl(
|
|
4997
5049
|
reference.url,
|
|
4998
|
-
reference
|
|
5050
|
+
// the root url info has no originalUrl; it owns the reference
|
|
5051
|
+
// created for an incoming request ("http_request")
|
|
5052
|
+
reference.ownerUrlInfo.originalUrl || reference.ownerUrlInfo.url,
|
|
4999
5053
|
);
|
|
5000
5054
|
reference.filenameHint = directoryRelativeUrl;
|
|
5001
5055
|
}
|
|
@@ -7090,6 +7144,7 @@ const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
|
|
|
7090
7144
|
const jsenvPluginDependencyStatus = ({
|
|
7091
7145
|
dependencyProblemEventEmitter,
|
|
7092
7146
|
getDependencyProblems,
|
|
7147
|
+
getDependencyWatchInfo = () => ({}),
|
|
7093
7148
|
}) => {
|
|
7094
7149
|
return {
|
|
7095
7150
|
name: "jsenv:dependency_status",
|
|
@@ -7126,7 +7181,10 @@ const jsenvPluginDependencyStatus = ({
|
|
|
7126
7181
|
src: clientReference.generatedSpecifier,
|
|
7127
7182
|
initCall: {
|
|
7128
7183
|
callee: "initDependencyStatus",
|
|
7129
|
-
params: {
|
|
7184
|
+
params: {
|
|
7185
|
+
problems: getDependencyProblems(),
|
|
7186
|
+
watchInfo: getDependencyWatchInfo(),
|
|
7187
|
+
},
|
|
7130
7188
|
},
|
|
7131
7189
|
pluginName: "jsenv:dependency_status",
|
|
7132
7190
|
});
|
|
@@ -11421,6 +11479,14 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11421
11479
|
!inlineParentUrlInfo &&
|
|
11422
11480
|
!urlInfo.response &&
|
|
11423
11481
|
urlInfo.content !== undefined &&
|
|
11482
|
+
// content can be defined while a cook is still in flight (a file
|
|
11483
|
+
// watcher invalidation re-cooking in the background, for
|
|
11484
|
+
// instance): at that point it holds the raw fetched content,
|
|
11485
|
+
// transformations not applied yet. Serving that would send an
|
|
11486
|
+
// html without any of the injected scripts. Only finalized
|
|
11487
|
+
// content is a complete response; anything else must go through
|
|
11488
|
+
// cook() below, which joins the pending cook (see debounceCook).
|
|
11489
|
+
urlInfo.contentFinalized &&
|
|
11424
11490
|
!cacheIsDisabledInResponseHeader(urlInfo) &&
|
|
11425
11491
|
// a "?hot" request exists to bypass every cache, this one
|
|
11426
11492
|
// included: it must be cooked, because cooking is what rewrites
|
|
@@ -11465,7 +11531,10 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11465
11531
|
}
|
|
11466
11532
|
response = {
|
|
11467
11533
|
url: reference.url,
|
|
11468
|
-
|
|
11534
|
+
// a plugin can cook a complete response body for an url that is
|
|
11535
|
+
// not a 200: the directory listing does this to answer a request
|
|
11536
|
+
// for a file that does not exist with the explorer page
|
|
11537
|
+
status: urlInfo.status,
|
|
11469
11538
|
headers: {
|
|
11470
11539
|
// when we send eTag to the client the next request to the server
|
|
11471
11540
|
// will send etag in request headers.
|
|
@@ -11839,6 +11908,7 @@ const startDevServer = async ({
|
|
|
11839
11908
|
dependencyStatus: {
|
|
11840
11909
|
dependencyProblemEventEmitter,
|
|
11841
11910
|
getDependencyProblems: dependencyWatcher.getProblems,
|
|
11911
|
+
getDependencyWatchInfo: dependencyWatcher.getWatchInfo,
|
|
11842
11912
|
},
|
|
11843
11913
|
cacheControl,
|
|
11844
11914
|
ribbon,
|
package/package.json
CHANGED
|
@@ -317,6 +317,14 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
317
317
|
!inlineParentUrlInfo &&
|
|
318
318
|
!urlInfo.response &&
|
|
319
319
|
urlInfo.content !== undefined &&
|
|
320
|
+
// content can be defined while a cook is still in flight (a file
|
|
321
|
+
// watcher invalidation re-cooking in the background, for
|
|
322
|
+
// instance): at that point it holds the raw fetched content,
|
|
323
|
+
// transformations not applied yet. Serving that would send an
|
|
324
|
+
// html without any of the injected scripts. Only finalized
|
|
325
|
+
// content is a complete response; anything else must go through
|
|
326
|
+
// cook() below, which joins the pending cook (see debounceCook).
|
|
327
|
+
urlInfo.contentFinalized &&
|
|
320
328
|
!cacheIsDisabledInResponseHeader(urlInfo) &&
|
|
321
329
|
// a "?hot" request exists to bypass every cache, this one
|
|
322
330
|
// included: it must be cooked, because cooking is what rewrites
|
|
@@ -361,7 +369,10 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
361
369
|
}
|
|
362
370
|
response = {
|
|
363
371
|
url: reference.url,
|
|
364
|
-
|
|
372
|
+
// a plugin can cook a complete response body for an url that is
|
|
373
|
+
// not a 200: the directory listing does this to answer a request
|
|
374
|
+
// for a file that does not exist with the explorer page
|
|
375
|
+
status: urlInfo.status,
|
|
365
376
|
headers: {
|
|
366
377
|
// when we send eTag to the client the next request to the server
|
|
367
378
|
// will send etag in request headers.
|
|
@@ -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) {
|