@jsenv/core 41.3.0 → 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 +258 -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 +467 -23
- package/package.json +6 -6
- 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
- package/src/plugins/workspace_bundle/jsenv_plugin_workspace_bundle.js +10 -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,
|
|
@@ -6798,6 +7180,16 @@ const jsenvPluginWorkspaceBundle = ({ packageDirectory }) => {
|
|
|
6798
7180
|
// root package, we don't want to bundle
|
|
6799
7181
|
return null;
|
|
6800
7182
|
}
|
|
7183
|
+
if (reference.type !== "js_import") {
|
|
7184
|
+
// Only consolidate ES imports of a workspace package into its single
|
|
7185
|
+
// bundle. Other reference kinds are their own entry points — most
|
|
7186
|
+
// importantly an HTML <script src> pointing at a package file (jsenv
|
|
7187
|
+
// injects its own client scripts that way: server events, the client
|
|
7188
|
+
// monitoring reporter, custom-elements-redefine…). Redirecting those to
|
|
7189
|
+
// the package main would run the wrong module, and for @jsenv/core the
|
|
7190
|
+
// main is node-only code (node:url) that cannot be served to a browser.
|
|
7191
|
+
return null;
|
|
7192
|
+
}
|
|
6801
7193
|
// we make sure we target the bundle version of the package
|
|
6802
7194
|
// otherwise we might execute some parts of the package code multiple times.
|
|
6803
7195
|
// so we need to redirect the potential reference to non entry point to the package main entry point
|
|
@@ -6903,6 +7295,7 @@ const getCorePlugins = ({
|
|
|
6903
7295
|
|
|
6904
7296
|
clientAutoreload,
|
|
6905
7297
|
clientAutoreloadOnServerRestart,
|
|
7298
|
+
dependencyStatus,
|
|
6906
7299
|
cacheControl,
|
|
6907
7300
|
scenarioPlaceholders = true,
|
|
6908
7301
|
ribbon = true,
|
|
@@ -7001,6 +7394,9 @@ const getCorePlugins = ({
|
|
|
7001
7394
|
...(clientAutoreload && clientAutoreload.enabled
|
|
7002
7395
|
? [jsenvPluginAutoreload(clientAutoreload)]
|
|
7003
7396
|
: []),
|
|
7397
|
+
...(dependencyStatus
|
|
7398
|
+
? [jsenvPluginDependencyStatus(dependencyStatus)]
|
|
7399
|
+
: []),
|
|
7004
7400
|
...(cacheControl ? [jsenvPluginCacheControl(cacheControl)] : []),
|
|
7005
7401
|
...(ribbon ? [jsenvPluginRibbon({ rootDirectoryUrl, ...ribbon })] : []),
|
|
7006
7402
|
...(dropToOpen ? [jsenvPluginDropToOpen()] : []),
|
|
@@ -10551,31 +10947,36 @@ const devServerPluginServeSourceFiles = ({
|
|
|
10551
10947
|
}
|
|
10552
10948
|
const urlInfo = reference.urlInfo;
|
|
10553
10949
|
const ifNoneMatch = request.headers["if-none-match"];
|
|
10554
|
-
const
|
|
10555
|
-
|
|
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
|
+
};
|
|
10556
10966
|
|
|
10557
10967
|
try {
|
|
10558
|
-
|
|
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) {
|
|
10559
10972
|
const [clientOriginalContentEtag, clientContentEtag] =
|
|
10560
10973
|
ifNoneMatch.split("_");
|
|
10561
10974
|
if (
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
urlInfoTargetedByCache.isValid()
|
|
10975
|
+
urlInfo.originalContentEtag === clientOriginalContentEtag &&
|
|
10976
|
+
urlInfo.contentEtag === clientContentEtag &&
|
|
10977
|
+
urlInfo.isValid()
|
|
10566
10978
|
) {
|
|
10567
|
-
|
|
10568
|
-
"cache-control": `private,max-age=0,must-revalidate`,
|
|
10569
|
-
};
|
|
10570
|
-
Object.keys(urlInfo.headers).forEach((key) => {
|
|
10571
|
-
if (key !== "content-length") {
|
|
10572
|
-
headers[key] = urlInfo.headers[key];
|
|
10573
|
-
}
|
|
10574
|
-
});
|
|
10575
|
-
return {
|
|
10576
|
-
status: 304,
|
|
10577
|
-
headers,
|
|
10578
|
-
};
|
|
10979
|
+
return respondWithNotModified();
|
|
10579
10980
|
}
|
|
10580
10981
|
}
|
|
10581
10982
|
await urlInfo.cook({ request, reference });
|
|
@@ -10583,6 +10984,19 @@ const devServerPluginServeSourceFiles = ({
|
|
|
10583
10984
|
if (response) {
|
|
10584
10985
|
return response;
|
|
10585
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
|
+
}
|
|
10586
11000
|
response = {
|
|
10587
11001
|
url: reference.url,
|
|
10588
11002
|
status: 200,
|
|
@@ -10599,7 +11013,7 @@ const devServerPluginServeSourceFiles = ({
|
|
|
10599
11013
|
: {
|
|
10600
11014
|
"cache-control": `private,max-age=0,must-revalidate`,
|
|
10601
11015
|
// it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
|
|
10602
|
-
|
|
11016
|
+
eTag,
|
|
10603
11017
|
}),
|
|
10604
11018
|
...urlInfo.headers,
|
|
10605
11019
|
"content-type": urlInfo.contentType,
|
|
@@ -10678,7 +11092,10 @@ const devServerPluginServeSourceFiles = ({
|
|
|
10678
11092
|
statusText: originalError.reason,
|
|
10679
11093
|
};
|
|
10680
11094
|
}
|
|
10681
|
-
|
|
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") {
|
|
10682
11099
|
return {
|
|
10683
11100
|
url: reference.url,
|
|
10684
11101
|
status: 404,
|
|
@@ -10873,13 +11290,36 @@ const startDevServer = async ({
|
|
|
10873
11290
|
const packageDirectory = createPackageDirectory({ sourceDirectoryUrl });
|
|
10874
11291
|
const clientFileChangeEventEmitter = createEventEmitter();
|
|
10875
11292
|
const clientFileDereferencedEventEmitter = createEventEmitter();
|
|
11293
|
+
const reloadRequestEventEmitter = createEventEmitter();
|
|
10876
11294
|
clientAutoreload = {
|
|
10877
11295
|
enabled: true,
|
|
10878
11296
|
clientServerEventsConfig: {},
|
|
10879
11297
|
clientFileChangeEventEmitter,
|
|
10880
11298
|
clientFileDereferencedEventEmitter,
|
|
11299
|
+
reloadRequestEventEmitter,
|
|
10881
11300
|
...clientAutoreload,
|
|
10882
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);
|
|
10883
11323
|
|
|
10884
11324
|
const devServerJsenvPluginStore = await createJsenvPluginStore([
|
|
10885
11325
|
jsenvPluginServerEvents({ clientAutoreload }),
|
|
@@ -10914,6 +11354,10 @@ const startDevServer = async ({
|
|
|
10914
11354
|
|
|
10915
11355
|
clientAutoreload,
|
|
10916
11356
|
clientAutoreloadOnServerRestart,
|
|
11357
|
+
dependencyStatus: {
|
|
11358
|
+
dependencyProblemEventEmitter,
|
|
11359
|
+
getDependencyProblems: dependencyWatcher.getProblems,
|
|
11360
|
+
},
|
|
10917
11361
|
cacheControl,
|
|
10918
11362
|
ribbon,
|
|
10919
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": {
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@jsenv/plugin-minification": "1.7.5",
|
|
79
79
|
"@jsenv/plugin-supervisor": "1.8.7",
|
|
80
80
|
"@jsenv/plugin-transpilation": "1.5.78",
|
|
81
|
-
"@jsenv/server": "17.4.
|
|
81
|
+
"@jsenv/server": "17.4.1",
|
|
82
82
|
"@jsenv/sourcemap": "1.4.2",
|
|
83
83
|
"react-table": "7.8.0"
|
|
84
84
|
},
|
|
@@ -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
|
}
|