@jsenv/core 41.3.1 → 41.4.0
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 +248 -0
- package/dist/js/dependency_status.js +61 -0
- package/dist/start_dev_server/jsenv_core_packages.js +262 -10
- package/dist/start_dev_server/start_dev_server.js +457 -23
- package/package.json +5 -5
- package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +42 -21
- package/src/dev/start_dev_server.js +28 -0
- package/src/helpers/watch_dependencies.js +110 -0
- package/src/kitchen/errors.js +43 -0
- package/src/kitchen/package_dependencies.js +160 -0
- package/src/plugins/autoreload/jsenv_plugin_autoreload.js +2 -0
- package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +10 -0
- package/src/plugins/dependency_status/client/dependency_status.js +59 -0
- package/src/plugins/dependency_status/jsenv_plugin_dependency_status.js +64 -0
- package/src/plugins/plugins.js +5 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebSocketResponse, pickContentType, ServerEvents, serverPluginErrorHandler, composeTwoResponses, fetchDirectory, serverPluginCORS, jsenvAccessControlAllowedHeaders, startServer } from "@jsenv/server";
|
|
2
|
-
import { readFileSync, readdirSync,
|
|
3
|
-
import { lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, urlToRelativeUrl, errorToHTML, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, URL_META, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, 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";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
3
|
+
import { registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, urlToRelativeUrl, errorToHTML, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, URL_META, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, 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";
|
|
@@ -50,6 +50,274 @@ const createEventEmitter = () => {
|
|
|
50
50
|
return { on, off, emit };
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
/*
|
|
54
|
+
* Compares what a package.json declares with what is actually inside
|
|
55
|
+
* node_modules, so the dev server can tell a dependency apart when it is
|
|
56
|
+
* missing (never installed) or outdated (installed at an other version).
|
|
57
|
+
*
|
|
58
|
+
* Only the dependencies declared by a package are looked at, never the
|
|
59
|
+
* transitive ones: a declared dependency is what pulls the rest, so it is
|
|
60
|
+
* enough to know whether an install is needed or over.
|
|
61
|
+
*
|
|
62
|
+
* Only exact declared versions ("1.2.3") are compared: a range ("^1.2.3"), a
|
|
63
|
+
* file/workspace protocol or a tag cannot be checked without resolving what npm
|
|
64
|
+
* would pick, which is way beyond what is needed here.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
const DEPENDENCY_FIELDS = [
|
|
69
|
+
"dependencies",
|
|
70
|
+
"devDependencies",
|
|
71
|
+
"optionalDependencies",
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const packageNameFromSpecifier = (specifier) => {
|
|
75
|
+
const parts = specifier.split("/");
|
|
76
|
+
if (specifier[0] === "@") {
|
|
77
|
+
return parts.slice(0, 2).join("/");
|
|
78
|
+
}
|
|
79
|
+
return parts[0];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* declaringDirectoryUrl is the package directory the importer belongs to, which
|
|
84
|
+
* is not always the project one: a file inside node_modules resolves its bare
|
|
85
|
+
* specifiers against the dependencies of the package containing it.
|
|
86
|
+
*/
|
|
87
|
+
const readDependencyStatus = (
|
|
88
|
+
packageDirectory,
|
|
89
|
+
packageName,
|
|
90
|
+
declaringDirectoryUrl = packageDirectory.url,
|
|
91
|
+
) => {
|
|
92
|
+
const packageJSON = readPackageJSON(packageDirectory, declaringDirectoryUrl);
|
|
93
|
+
if (!packageJSON) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const declaredVersion = readDeclaredVersion(packageJSON, packageName);
|
|
97
|
+
if (!declaredVersion) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return createStatus(packageDirectory, {
|
|
101
|
+
packageName,
|
|
102
|
+
declaredVersion,
|
|
103
|
+
declaringDirectoryUrl,
|
|
104
|
+
declaredBy: packageJSON.name,
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const readDependencyStatuses = (packageDirectory) => {
|
|
109
|
+
const packageJSON = readPackageJSON(packageDirectory, packageDirectory.url);
|
|
110
|
+
if (!packageJSON) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const statuses = [];
|
|
114
|
+
const packageNameSet = new Set();
|
|
115
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
116
|
+
const dependencies = packageJSON[field];
|
|
117
|
+
if (!dependencies) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
for (const packageName of Object.keys(dependencies)) {
|
|
121
|
+
if (packageNameSet.has(packageName)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
packageNameSet.add(packageName);
|
|
125
|
+
statuses.push(
|
|
126
|
+
createStatus(packageDirectory, {
|
|
127
|
+
packageName,
|
|
128
|
+
declaredVersion: dependencies[packageName],
|
|
129
|
+
declaringDirectoryUrl: packageDirectory.url,
|
|
130
|
+
declaredBy: packageJSON.name,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return statuses;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const createStatus = (
|
|
139
|
+
packageDirectory,
|
|
140
|
+
{ packageName, declaredVersion, declaringDirectoryUrl, declaredBy },
|
|
141
|
+
) => {
|
|
142
|
+
const status = {
|
|
143
|
+
packageName,
|
|
144
|
+
declaredVersion,
|
|
145
|
+
declaredBy,
|
|
146
|
+
installedVersion: null,
|
|
147
|
+
state: "missing",
|
|
148
|
+
};
|
|
149
|
+
const installedDirectoryUrl = findInstalledDirectoryUrl(
|
|
150
|
+
declaringDirectoryUrl,
|
|
151
|
+
packageName,
|
|
152
|
+
);
|
|
153
|
+
if (!installedDirectoryUrl) {
|
|
154
|
+
return status;
|
|
155
|
+
}
|
|
156
|
+
const installedPackageJSON = readPackageJSON(
|
|
157
|
+
packageDirectory,
|
|
158
|
+
installedDirectoryUrl,
|
|
159
|
+
);
|
|
160
|
+
status.installedVersion = installedPackageJSON
|
|
161
|
+
? installedPackageJSON.version
|
|
162
|
+
: null;
|
|
163
|
+
status.state =
|
|
164
|
+
isExactVersion(declaredVersion) &&
|
|
165
|
+
status.installedVersion !== declaredVersion
|
|
166
|
+
? "outdated"
|
|
167
|
+
: "installed";
|
|
168
|
+
return status;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
|
|
172
|
+
let directoryUrl = declaringDirectoryUrl;
|
|
173
|
+
while (directoryUrl) {
|
|
174
|
+
const candidateUrl = `${directoryUrl}node_modules/${packageName}/`;
|
|
175
|
+
if (existsSync(new URL(`${candidateUrl}package.json`))) {
|
|
176
|
+
return candidateUrl;
|
|
177
|
+
}
|
|
178
|
+
const parentUrl = new URL("../", directoryUrl).href;
|
|
179
|
+
if (parentUrl === directoryUrl) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
directoryUrl = parentUrl;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const readDeclaredVersion = (packageJSON, packageName) => {
|
|
188
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
189
|
+
const dependencies = packageJSON[field];
|
|
190
|
+
if (dependencies && dependencies[packageName]) {
|
|
191
|
+
return dependencies[packageName];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// an install in progress can be caught halfway, with a package.json not written yet
|
|
198
|
+
const readPackageJSON = (packageDirectory, directoryUrl) => {
|
|
199
|
+
if (!directoryUrl) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return packageDirectory.read(directoryUrl);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const isExactVersion = (declaredVersion) => {
|
|
210
|
+
return /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(declaredVersion);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/*
|
|
214
|
+
* Detects when "npm install" makes a missing or outdated dependency match what
|
|
215
|
+
* the project package.json declares, so the browser can be reloaded at that
|
|
216
|
+
* moment.
|
|
217
|
+
*
|
|
218
|
+
* node_modules is deliberately not watched: it is far too big, and an install
|
|
219
|
+
* rewrites, dedupes and moves package directories around, so a watcher placed
|
|
220
|
+
* on one of them is unreliable. Instead the few packages known to be missing or
|
|
221
|
+
* outdated are polled, which costs a couple of readFileSync and stops as soon as
|
|
222
|
+
* they are all installed. The project package.json is watched though: it is a
|
|
223
|
+
* single file, and editing it is what puts a dependency out of date in the first
|
|
224
|
+
* place.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
const POLL_INTERVAL = 500;
|
|
229
|
+
|
|
230
|
+
const watchDependencies = (
|
|
231
|
+
packageDirectory,
|
|
232
|
+
{ onProblem, onInstalled, onChange, pollInterval = POLL_INTERVAL },
|
|
233
|
+
) => {
|
|
234
|
+
let problemMap = new Map();
|
|
235
|
+
const watcher = {
|
|
236
|
+
getProblems: () => Array.from(problemMap.values()),
|
|
237
|
+
stop: () => {},
|
|
238
|
+
};
|
|
239
|
+
if (!packageDirectory.url) {
|
|
240
|
+
return watcher;
|
|
241
|
+
}
|
|
242
|
+
let timer = null;
|
|
243
|
+
|
|
244
|
+
const check = () => {
|
|
245
|
+
const nextProblemMap = new Map();
|
|
246
|
+
for (const status of readDependencyStatuses(packageDirectory)) {
|
|
247
|
+
if (status.state === "missing" || status.state === "outdated") {
|
|
248
|
+
nextProblemMap.set(status.packageName, status);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const [packageName, status] of nextProblemMap) {
|
|
252
|
+
const previousStatus = problemMap.get(packageName);
|
|
253
|
+
if (
|
|
254
|
+
!previousStatus ||
|
|
255
|
+
previousStatus.state !== status.state ||
|
|
256
|
+
previousStatus.declaredVersion !== status.declaredVersion
|
|
257
|
+
) {
|
|
258
|
+
onProblem(status);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const [packageName, previousStatus] of problemMap) {
|
|
262
|
+
if (!nextProblemMap.has(packageName)) {
|
|
263
|
+
onInstalled(previousStatus);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const changed =
|
|
267
|
+
nextProblemMap.size !== problemMap.size ||
|
|
268
|
+
Array.from(nextProblemMap.keys()).some((packageName) => {
|
|
269
|
+
const previousStatus = problemMap.get(packageName);
|
|
270
|
+
const status = nextProblemMap.get(packageName);
|
|
271
|
+
return (
|
|
272
|
+
!previousStatus ||
|
|
273
|
+
previousStatus.state !== status.state ||
|
|
274
|
+
previousStatus.declaredVersion !== status.declaredVersion ||
|
|
275
|
+
previousStatus.installedVersion !== status.installedVersion
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
problemMap = nextProblemMap;
|
|
279
|
+
if (changed) {
|
|
280
|
+
onChange(watcher.getProblems());
|
|
281
|
+
}
|
|
282
|
+
if (problemMap.size === 0) {
|
|
283
|
+
stopPolling();
|
|
284
|
+
} else {
|
|
285
|
+
startPolling();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const startPolling = () => {
|
|
290
|
+
if (timer) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
timer = setInterval(check, pollInterval);
|
|
294
|
+
timer.unref();
|
|
295
|
+
};
|
|
296
|
+
const stopPolling = () => {
|
|
297
|
+
if (!timer) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
clearInterval(timer);
|
|
301
|
+
timer = null;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const unwatchPackageJson = registerFileLifecycle(
|
|
305
|
+
new URL("package.json", packageDirectory.url),
|
|
306
|
+
{
|
|
307
|
+
added: check,
|
|
308
|
+
updated: check,
|
|
309
|
+
keepProcessAlive: false,
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
check();
|
|
313
|
+
|
|
314
|
+
watcher.stop = () => {
|
|
315
|
+
stopPolling();
|
|
316
|
+
unwatchPackageJson();
|
|
317
|
+
};
|
|
318
|
+
return watcher;
|
|
319
|
+
};
|
|
320
|
+
|
|
53
321
|
const jsenvCoreDirectoryUrl = new URL("../", import.meta.url);
|
|
54
322
|
|
|
55
323
|
const createPackageDirectory = ({
|
|
@@ -4050,6 +4318,18 @@ ${reason}`,
|
|
|
4050
4318
|
});
|
|
4051
4319
|
}
|
|
4052
4320
|
if (error.code === "MODULE_NOT_FOUND") {
|
|
4321
|
+
const notInstalledStatus = readNotInstalledStatus(reference);
|
|
4322
|
+
if (notInstalledStatus) {
|
|
4323
|
+
const { packageName, declaredVersion, declaredBy, isProjectDependency } =
|
|
4324
|
+
notInstalledStatus;
|
|
4325
|
+
return createFailedToResolveUrlError({
|
|
4326
|
+
"reason": isProjectDependency
|
|
4327
|
+
? `"${packageName}" is declared in package.json but not installed`
|
|
4328
|
+
: `"${packageName}" is declared by "${declaredBy}" but not installed`,
|
|
4329
|
+
"declared version": declaredVersion,
|
|
4330
|
+
"suggestion": `run npm install, the page will reload once "${packageName}" is installed`,
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4053
4333
|
const bareSpecifierError = createFailedToResolveUrlError({
|
|
4054
4334
|
reason: `"${reference.specifier}" is a bare specifier but cannot be remapped to a package`,
|
|
4055
4335
|
});
|
|
@@ -4304,6 +4584,32 @@ const getErrorTrace = (error, reference) => {
|
|
|
4304
4584
|
};
|
|
4305
4585
|
};
|
|
4306
4586
|
|
|
4587
|
+
// a bare specifier is resolved against the dependencies of the package
|
|
4588
|
+
// containing the file that imports it, which is the project one for a source
|
|
4589
|
+
// file but an other one for a file inside node_modules
|
|
4590
|
+
const readNotInstalledStatus = (reference) => {
|
|
4591
|
+
const { ownerUrlInfo } = reference;
|
|
4592
|
+
const { packageDirectory } = ownerUrlInfo.context;
|
|
4593
|
+
if (!packageDirectory) {
|
|
4594
|
+
return null;
|
|
4595
|
+
}
|
|
4596
|
+
const declaringDirectoryUrl =
|
|
4597
|
+
packageDirectory.find(ownerUrlInfo.url) || packageDirectory.url;
|
|
4598
|
+
const packageName = packageNameFromSpecifier(reference.specifier);
|
|
4599
|
+
const status = readDependencyStatus(
|
|
4600
|
+
packageDirectory,
|
|
4601
|
+
packageName,
|
|
4602
|
+
declaringDirectoryUrl,
|
|
4603
|
+
);
|
|
4604
|
+
if (!status || status.state !== "missing") {
|
|
4605
|
+
return null;
|
|
4606
|
+
}
|
|
4607
|
+
return {
|
|
4608
|
+
...status,
|
|
4609
|
+
isProjectDependency: declaringDirectoryUrl === packageDirectory.url,
|
|
4610
|
+
};
|
|
4611
|
+
};
|
|
4612
|
+
|
|
4307
4613
|
const detailsFromFirstReference = (reference) => {
|
|
4308
4614
|
const referenceInProject = getFirstReferenceInProject(reference);
|
|
4309
4615
|
if (
|
|
@@ -5928,6 +6234,7 @@ const jsenvPluginAutoreloadClient = () => {
|
|
|
5928
6234
|
const jsenvPluginAutoreloadServer = ({
|
|
5929
6235
|
clientFileChangeEventEmitter,
|
|
5930
6236
|
clientFileDereferencedEventEmitter,
|
|
6237
|
+
reloadRequestEventEmitter,
|
|
5931
6238
|
}) => {
|
|
5932
6239
|
return {
|
|
5933
6240
|
name: "jsenv:autoreload_server",
|
|
@@ -6256,6 +6563,15 @@ const jsenvPluginAutoreloadServer = ({
|
|
|
6256
6563
|
});
|
|
6257
6564
|
},
|
|
6258
6565
|
);
|
|
6566
|
+
// something outside the url graph wants the page back from scratch,
|
|
6567
|
+
// typically a dependency that just got installed into node_modules
|
|
6568
|
+
reloadRequestEventEmitter.on(({ cause, reason }) => {
|
|
6569
|
+
serverEventInfo.sendServerEvent({
|
|
6570
|
+
cause,
|
|
6571
|
+
type: "full",
|
|
6572
|
+
typeReason: reason,
|
|
6573
|
+
});
|
|
6574
|
+
});
|
|
6259
6575
|
},
|
|
6260
6576
|
},
|
|
6261
6577
|
serverRoutes: [
|
|
@@ -6366,6 +6682,7 @@ const jsenvPluginHotSearchParam = () => {
|
|
|
6366
6682
|
const jsenvPluginAutoreload = ({
|
|
6367
6683
|
clientFileChangeEventEmitter,
|
|
6368
6684
|
clientFileDereferencedEventEmitter,
|
|
6685
|
+
reloadRequestEventEmitter,
|
|
6369
6686
|
}) => {
|
|
6370
6687
|
return [
|
|
6371
6688
|
jsenvPluginHotSearchParam(),
|
|
@@ -6373,10 +6690,75 @@ const jsenvPluginAutoreload = ({
|
|
|
6373
6690
|
jsenvPluginAutoreloadServer({
|
|
6374
6691
|
clientFileChangeEventEmitter,
|
|
6375
6692
|
clientFileDereferencedEventEmitter,
|
|
6693
|
+
reloadRequestEventEmitter,
|
|
6376
6694
|
}),
|
|
6377
6695
|
];
|
|
6378
6696
|
};
|
|
6379
6697
|
|
|
6698
|
+
/*
|
|
6699
|
+
* Tells the browser about the dependencies declared in package.json that
|
|
6700
|
+
* node_modules does not match, so the page can say it is running with something
|
|
6701
|
+
* else than what the project asks for.
|
|
6702
|
+
*
|
|
6703
|
+
* The state is sent on page load (it is known before any client connects) and
|
|
6704
|
+
* again whenever it changes, so a page opened during an install is updated
|
|
6705
|
+
* without being reloaded.
|
|
6706
|
+
*/
|
|
6707
|
+
|
|
6708
|
+
|
|
6709
|
+
const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
|
|
6710
|
+
|
|
6711
|
+
const jsenvPluginDependencyStatus = ({
|
|
6712
|
+
dependencyProblemEventEmitter,
|
|
6713
|
+
getDependencyProblems,
|
|
6714
|
+
}) => {
|
|
6715
|
+
return {
|
|
6716
|
+
name: "jsenv:dependency_status",
|
|
6717
|
+
appliesDuring: "dev",
|
|
6718
|
+
serverEvents: {
|
|
6719
|
+
dependency_status: (serverEventInfo) => {
|
|
6720
|
+
dependencyProblemEventEmitter.on((problems) => {
|
|
6721
|
+
// the state is baked into the html by the injection below, so a page
|
|
6722
|
+
// served from the graph as it is would come back with the previous
|
|
6723
|
+
// state, which is exactly what a reload triggered by an install does
|
|
6724
|
+
for (const urlInfo of serverEventInfo.kitchen.graph.urlInfoMap.values()) {
|
|
6725
|
+
if (urlInfo.type === "html" && urlInfo.content !== undefined) {
|
|
6726
|
+
urlInfo.onModified();
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
serverEventInfo.sendServerEvent({ problems });
|
|
6730
|
+
});
|
|
6731
|
+
},
|
|
6732
|
+
},
|
|
6733
|
+
transformUrlContent: {
|
|
6734
|
+
html: (htmlUrlInfo) => {
|
|
6735
|
+
const htmlAst = parseHtml({
|
|
6736
|
+
html: htmlUrlInfo.content,
|
|
6737
|
+
url: htmlUrlInfo.url,
|
|
6738
|
+
});
|
|
6739
|
+
const clientReference = htmlUrlInfo.dependencies.inject({
|
|
6740
|
+
type: "script",
|
|
6741
|
+
subtype: "js_module",
|
|
6742
|
+
expectedType: "js_module",
|
|
6743
|
+
specifier: clientFileUrl,
|
|
6744
|
+
});
|
|
6745
|
+
injectJsenvScript(htmlAst, {
|
|
6746
|
+
type: "module",
|
|
6747
|
+
src: clientReference.generatedSpecifier,
|
|
6748
|
+
initCall: {
|
|
6749
|
+
callee: "initDependencyStatus",
|
|
6750
|
+
params: { problems: getDependencyProblems() },
|
|
6751
|
+
},
|
|
6752
|
+
pluginName: "jsenv:dependency_status",
|
|
6753
|
+
});
|
|
6754
|
+
return {
|
|
6755
|
+
content: stringifyHtmlAst(htmlAst),
|
|
6756
|
+
};
|
|
6757
|
+
},
|
|
6758
|
+
},
|
|
6759
|
+
};
|
|
6760
|
+
};
|
|
6761
|
+
|
|
6380
6762
|
const jsenvPluginCacheControl = ({
|
|
6381
6763
|
versionedUrls = true,
|
|
6382
6764
|
maxAge = SECONDS_IN_30_DAYS,
|
|
@@ -6913,6 +7295,7 @@ const getCorePlugins = ({
|
|
|
6913
7295
|
|
|
6914
7296
|
clientAutoreload,
|
|
6915
7297
|
clientAutoreloadOnServerRestart,
|
|
7298
|
+
dependencyStatus,
|
|
6916
7299
|
cacheControl,
|
|
6917
7300
|
scenarioPlaceholders = true,
|
|
6918
7301
|
ribbon = true,
|
|
@@ -7011,6 +7394,9 @@ const getCorePlugins = ({
|
|
|
7011
7394
|
...(clientAutoreload && clientAutoreload.enabled
|
|
7012
7395
|
? [jsenvPluginAutoreload(clientAutoreload)]
|
|
7013
7396
|
: []),
|
|
7397
|
+
...(dependencyStatus
|
|
7398
|
+
? [jsenvPluginDependencyStatus(dependencyStatus)]
|
|
7399
|
+
: []),
|
|
7014
7400
|
...(cacheControl ? [jsenvPluginCacheControl(cacheControl)] : []),
|
|
7015
7401
|
...(ribbon ? [jsenvPluginRibbon({ rootDirectoryUrl, ...ribbon })] : []),
|
|
7016
7402
|
...(dropToOpen ? [jsenvPluginDropToOpen()] : []),
|
|
@@ -10561,31 +10947,36 @@ const devServerPluginServeSourceFiles = ({
|
|
|
10561
10947
|
}
|
|
10562
10948
|
const urlInfo = reference.urlInfo;
|
|
10563
10949
|
const ifNoneMatch = request.headers["if-none-match"];
|
|
10564
|
-
const
|
|
10565
|
-
|
|
10950
|
+
const inlineParentUrlInfo = urlInfo.findParentIfInline();
|
|
10951
|
+
const urlInfoTargetedByCache = inlineParentUrlInfo || urlInfo;
|
|
10952
|
+
const respondWithNotModified = () => {
|
|
10953
|
+
const headers = {
|
|
10954
|
+
"cache-control": `private,max-age=0,must-revalidate`,
|
|
10955
|
+
};
|
|
10956
|
+
Object.keys(urlInfo.headers).forEach((key) => {
|
|
10957
|
+
if (key !== "content-length") {
|
|
10958
|
+
headers[key] = urlInfo.headers[key];
|
|
10959
|
+
}
|
|
10960
|
+
});
|
|
10961
|
+
return {
|
|
10962
|
+
status: 304,
|
|
10963
|
+
headers,
|
|
10964
|
+
};
|
|
10965
|
+
};
|
|
10566
10966
|
|
|
10567
10967
|
try {
|
|
10568
|
-
|
|
10968
|
+
// an inline url info is cooked again every time the file containing it
|
|
10969
|
+
// is cooked, so its content is only known after cooking; its etag is
|
|
10970
|
+
// compared below, once cooked
|
|
10971
|
+
if (!urlInfo.error && ifNoneMatch && !inlineParentUrlInfo) {
|
|
10569
10972
|
const [clientOriginalContentEtag, clientContentEtag] =
|
|
10570
10973
|
ifNoneMatch.split("_");
|
|
10571
10974
|
if (
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
urlInfoTargetedByCache.isValid()
|
|
10975
|
+
urlInfo.originalContentEtag === clientOriginalContentEtag &&
|
|
10976
|
+
urlInfo.contentEtag === clientContentEtag &&
|
|
10977
|
+
urlInfo.isValid()
|
|
10576
10978
|
) {
|
|
10577
|
-
|
|
10578
|
-
"cache-control": `private,max-age=0,must-revalidate`,
|
|
10579
|
-
};
|
|
10580
|
-
Object.keys(urlInfo.headers).forEach((key) => {
|
|
10581
|
-
if (key !== "content-length") {
|
|
10582
|
-
headers[key] = urlInfo.headers[key];
|
|
10583
|
-
}
|
|
10584
|
-
});
|
|
10585
|
-
return {
|
|
10586
|
-
status: 304,
|
|
10587
|
-
headers,
|
|
10588
|
-
};
|
|
10979
|
+
return respondWithNotModified();
|
|
10589
10980
|
}
|
|
10590
10981
|
}
|
|
10591
10982
|
await urlInfo.cook({ request, reference });
|
|
@@ -10593,6 +10984,19 @@ const devServerPluginServeSourceFiles = ({
|
|
|
10593
10984
|
if (response) {
|
|
10594
10985
|
return response;
|
|
10595
10986
|
}
|
|
10987
|
+
// the original content of an inline url info is the one of the file
|
|
10988
|
+
// containing it, but its cooked content is its own: it can change while
|
|
10989
|
+
// the containing file stays identical (an import resolving to a new
|
|
10990
|
+
// version of a package for instance)
|
|
10991
|
+
const eTag = `${urlInfoTargetedByCache.originalContentEtag}_${urlInfo.contentEtag}`;
|
|
10992
|
+
if (
|
|
10993
|
+
!urlInfo.error &&
|
|
10994
|
+
ifNoneMatch === eTag &&
|
|
10995
|
+
inlineParentUrlInfo &&
|
|
10996
|
+
!cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
|
|
10997
|
+
) {
|
|
10998
|
+
return respondWithNotModified();
|
|
10999
|
+
}
|
|
10596
11000
|
response = {
|
|
10597
11001
|
url: reference.url,
|
|
10598
11002
|
status: 200,
|
|
@@ -10609,7 +11013,7 @@ const devServerPluginServeSourceFiles = ({
|
|
|
10609
11013
|
: {
|
|
10610
11014
|
"cache-control": `private,max-age=0,must-revalidate`,
|
|
10611
11015
|
// it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
|
|
10612
|
-
|
|
11016
|
+
eTag,
|
|
10613
11017
|
}),
|
|
10614
11018
|
...urlInfo.headers,
|
|
10615
11019
|
"content-type": urlInfo.contentType,
|
|
@@ -10688,7 +11092,10 @@ const devServerPluginServeSourceFiles = ({
|
|
|
10688
11092
|
statusText: originalError.reason,
|
|
10689
11093
|
};
|
|
10690
11094
|
}
|
|
10691
|
-
|
|
11095
|
+
// MODULE_NOT_FOUND: a specifier could not be resolved to a file,
|
|
11096
|
+
// so something is missing on the filesystem; 500 is for the errors
|
|
11097
|
+
// the server does not see coming
|
|
11098
|
+
if (code === "NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
10692
11099
|
return {
|
|
10693
11100
|
url: reference.url,
|
|
10694
11101
|
status: 404,
|
|
@@ -10883,13 +11290,36 @@ const startDevServer = async ({
|
|
|
10883
11290
|
const packageDirectory = createPackageDirectory({ sourceDirectoryUrl });
|
|
10884
11291
|
const clientFileChangeEventEmitter = createEventEmitter();
|
|
10885
11292
|
const clientFileDereferencedEventEmitter = createEventEmitter();
|
|
11293
|
+
const reloadRequestEventEmitter = createEventEmitter();
|
|
10886
11294
|
clientAutoreload = {
|
|
10887
11295
|
enabled: true,
|
|
10888
11296
|
clientServerEventsConfig: {},
|
|
10889
11297
|
clientFileChangeEventEmitter,
|
|
10890
11298
|
clientFileDereferencedEventEmitter,
|
|
11299
|
+
reloadRequestEventEmitter,
|
|
10891
11300
|
...clientAutoreload,
|
|
10892
11301
|
};
|
|
11302
|
+
const dependencyProblemEventEmitter = createEventEmitter();
|
|
11303
|
+
const dependencyWatcher = watchDependencies(packageDirectory, {
|
|
11304
|
+
onChange: (problems) => {
|
|
11305
|
+
dependencyProblemEventEmitter.emit(problems);
|
|
11306
|
+
},
|
|
11307
|
+
onProblem: ({ packageName, declaredVersion, installedVersion, state }) => {
|
|
11308
|
+
logger.warn(
|
|
11309
|
+
state === "missing"
|
|
11310
|
+
? `"${packageName}@${declaredVersion}" is declared in package.json but not installed, run npm install`
|
|
11311
|
+
: `"${packageName}" is installed in ${installedVersion} but package.json declares ${declaredVersion}, run npm install`,
|
|
11312
|
+
);
|
|
11313
|
+
},
|
|
11314
|
+
onInstalled: ({ packageName, declaredVersion }) => {
|
|
11315
|
+
logger.info(`"${packageName}@${declaredVersion}" is now installed`);
|
|
11316
|
+
reloadRequestEventEmitter.emit({
|
|
11317
|
+
cause: `${packageName}@${declaredVersion} installed`,
|
|
11318
|
+
reason: `a dependency became available in node_modules`,
|
|
11319
|
+
});
|
|
11320
|
+
},
|
|
11321
|
+
});
|
|
11322
|
+
serverStopCallbackSet.add(dependencyWatcher.stop);
|
|
10893
11323
|
|
|
10894
11324
|
const devServerJsenvPluginStore = await createJsenvPluginStore([
|
|
10895
11325
|
jsenvPluginServerEvents({ clientAutoreload }),
|
|
@@ -10924,6 +11354,10 @@ const startDevServer = async ({
|
|
|
10924
11354
|
|
|
10925
11355
|
clientAutoreload,
|
|
10926
11356
|
clientAutoreloadOnServerRestart,
|
|
11357
|
+
dependencyStatus: {
|
|
11358
|
+
dependencyProblemEventEmitter,
|
|
11359
|
+
getDependencyProblems: dependencyWatcher.getProblems,
|
|
11360
|
+
},
|
|
10927
11361
|
cacheControl,
|
|
10928
11362
|
ribbon,
|
|
10929
11363
|
dropToOpen,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "41.
|
|
3
|
+
"version": "41.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tool to develop, test and build js projects",
|
|
6
6
|
"repository": {
|
|
@@ -144,14 +144,14 @@
|
|
|
144
144
|
"./dist/client/inline_content/inline_content.js",
|
|
145
145
|
"./dist/client/directory_listing/jsenv_core_node_modules.js"
|
|
146
146
|
],
|
|
147
|
-
"volta": {
|
|
148
|
-
"node": "26.5.0",
|
|
149
|
-
"npm": "11.17.0"
|
|
150
|
-
},
|
|
151
147
|
"allowScripts": {
|
|
152
148
|
"unrs-resolver": true,
|
|
153
149
|
"@playwright/browser-chromium": true,
|
|
154
150
|
"@playwright/browser-firefox": true,
|
|
155
151
|
"@playwright/browser-webkit": true
|
|
152
|
+
},
|
|
153
|
+
"volta": {
|
|
154
|
+
"node": "26.5.0",
|
|
155
|
+
"npm": "11.17.0"
|
|
156
156
|
}
|
|
157
157
|
}
|