@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
package/dist/build/build.js
CHANGED
|
@@ -132,6 +132,136 @@ const watchSourceFiles = (
|
|
|
132
132
|
|
|
133
133
|
const jsenvCoreDirectoryUrl = new URL("../", import.meta.url);
|
|
134
134
|
|
|
135
|
+
/*
|
|
136
|
+
* Compares what a package.json declares with what is actually inside
|
|
137
|
+
* node_modules, so the dev server can tell a dependency apart when it is
|
|
138
|
+
* missing (never installed) or outdated (installed at an other version).
|
|
139
|
+
*
|
|
140
|
+
* Only the dependencies declared by a package are looked at, never the
|
|
141
|
+
* transitive ones: a declared dependency is what pulls the rest, so it is
|
|
142
|
+
* enough to know whether an install is needed or over.
|
|
143
|
+
*
|
|
144
|
+
* Only exact declared versions ("1.2.3") are compared: a range ("^1.2.3"), a
|
|
145
|
+
* file/workspace protocol or a tag cannot be checked without resolving what npm
|
|
146
|
+
* would pick, which is way beyond what is needed here.
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
const DEPENDENCY_FIELDS = [
|
|
151
|
+
"dependencies",
|
|
152
|
+
"devDependencies",
|
|
153
|
+
"optionalDependencies",
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const packageNameFromSpecifier = (specifier) => {
|
|
157
|
+
const parts = specifier.split("/");
|
|
158
|
+
if (specifier[0] === "@") {
|
|
159
|
+
return parts.slice(0, 2).join("/");
|
|
160
|
+
}
|
|
161
|
+
return parts[0];
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
* declaringDirectoryUrl is the package directory the importer belongs to, which
|
|
166
|
+
* is not always the project one: a file inside node_modules resolves its bare
|
|
167
|
+
* specifiers against the dependencies of the package containing it.
|
|
168
|
+
*/
|
|
169
|
+
const readDependencyStatus = (
|
|
170
|
+
packageDirectory,
|
|
171
|
+
packageName,
|
|
172
|
+
declaringDirectoryUrl = packageDirectory.url,
|
|
173
|
+
) => {
|
|
174
|
+
const packageJSON = readPackageJSON(packageDirectory, declaringDirectoryUrl);
|
|
175
|
+
if (!packageJSON) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const declaredVersion = readDeclaredVersion(packageJSON, packageName);
|
|
179
|
+
if (!declaredVersion) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return createStatus(packageDirectory, {
|
|
183
|
+
packageName,
|
|
184
|
+
declaredVersion,
|
|
185
|
+
declaringDirectoryUrl,
|
|
186
|
+
declaredBy: packageJSON.name,
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const createStatus = (
|
|
191
|
+
packageDirectory,
|
|
192
|
+
{ packageName, declaredVersion, declaringDirectoryUrl, declaredBy },
|
|
193
|
+
) => {
|
|
194
|
+
const status = {
|
|
195
|
+
packageName,
|
|
196
|
+
declaredVersion,
|
|
197
|
+
declaredBy,
|
|
198
|
+
installedVersion: null,
|
|
199
|
+
state: "missing",
|
|
200
|
+
};
|
|
201
|
+
const installedDirectoryUrl = findInstalledDirectoryUrl(
|
|
202
|
+
declaringDirectoryUrl,
|
|
203
|
+
packageName,
|
|
204
|
+
);
|
|
205
|
+
if (!installedDirectoryUrl) {
|
|
206
|
+
return status;
|
|
207
|
+
}
|
|
208
|
+
const installedPackageJSON = readPackageJSON(
|
|
209
|
+
packageDirectory,
|
|
210
|
+
installedDirectoryUrl,
|
|
211
|
+
);
|
|
212
|
+
status.installedVersion = installedPackageJSON
|
|
213
|
+
? installedPackageJSON.version
|
|
214
|
+
: null;
|
|
215
|
+
status.state =
|
|
216
|
+
isExactVersion(declaredVersion) &&
|
|
217
|
+
status.installedVersion !== declaredVersion
|
|
218
|
+
? "outdated"
|
|
219
|
+
: "installed";
|
|
220
|
+
return status;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
|
|
224
|
+
let directoryUrl = declaringDirectoryUrl;
|
|
225
|
+
while (directoryUrl) {
|
|
226
|
+
const candidateUrl = `${directoryUrl}node_modules/${packageName}/`;
|
|
227
|
+
if (existsSync(new URL(`${candidateUrl}package.json`))) {
|
|
228
|
+
return candidateUrl;
|
|
229
|
+
}
|
|
230
|
+
const parentUrl = new URL("../", directoryUrl).href;
|
|
231
|
+
if (parentUrl === directoryUrl) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
directoryUrl = parentUrl;
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const readDeclaredVersion = (packageJSON, packageName) => {
|
|
240
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
241
|
+
const dependencies = packageJSON[field];
|
|
242
|
+
if (dependencies && dependencies[packageName]) {
|
|
243
|
+
return dependencies[packageName];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// an install in progress can be caught halfway, with a package.json not written yet
|
|
250
|
+
const readPackageJSON = (packageDirectory, directoryUrl) => {
|
|
251
|
+
if (!directoryUrl) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
return packageDirectory.read(directoryUrl);
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const isExactVersion = (declaredVersion) => {
|
|
262
|
+
return /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(declaredVersion);
|
|
263
|
+
};
|
|
264
|
+
|
|
135
265
|
const createResolveUrlError = ({
|
|
136
266
|
jsenvPluginsController,
|
|
137
267
|
reference,
|
|
@@ -171,6 +301,18 @@ ${reason}`,
|
|
|
171
301
|
});
|
|
172
302
|
}
|
|
173
303
|
if (error.code === "MODULE_NOT_FOUND") {
|
|
304
|
+
const notInstalledStatus = readNotInstalledStatus(reference);
|
|
305
|
+
if (notInstalledStatus) {
|
|
306
|
+
const { packageName, declaredVersion, declaredBy, isProjectDependency } =
|
|
307
|
+
notInstalledStatus;
|
|
308
|
+
return createFailedToResolveUrlError({
|
|
309
|
+
"reason": isProjectDependency
|
|
310
|
+
? `"${packageName}" is declared in package.json but not installed`
|
|
311
|
+
: `"${packageName}" is declared by "${declaredBy}" but not installed`,
|
|
312
|
+
"declared version": declaredVersion,
|
|
313
|
+
"suggestion": `run npm install, the page will reload once "${packageName}" is installed`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
174
316
|
const bareSpecifierError = createFailedToResolveUrlError({
|
|
175
317
|
reason: `"${reference.specifier}" is a bare specifier but cannot be remapped to a package`,
|
|
176
318
|
});
|
|
@@ -425,6 +567,32 @@ const getErrorTrace = (error, reference) => {
|
|
|
425
567
|
};
|
|
426
568
|
};
|
|
427
569
|
|
|
570
|
+
// a bare specifier is resolved against the dependencies of the package
|
|
571
|
+
// containing the file that imports it, which is the project one for a source
|
|
572
|
+
// file but an other one for a file inside node_modules
|
|
573
|
+
const readNotInstalledStatus = (reference) => {
|
|
574
|
+
const { ownerUrlInfo } = reference;
|
|
575
|
+
const { packageDirectory } = ownerUrlInfo.context;
|
|
576
|
+
if (!packageDirectory) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const declaringDirectoryUrl =
|
|
580
|
+
packageDirectory.find(ownerUrlInfo.url) || packageDirectory.url;
|
|
581
|
+
const packageName = packageNameFromSpecifier(reference.specifier);
|
|
582
|
+
const status = readDependencyStatus(
|
|
583
|
+
packageDirectory,
|
|
584
|
+
packageName,
|
|
585
|
+
declaringDirectoryUrl,
|
|
586
|
+
);
|
|
587
|
+
if (!status || status.state !== "missing") {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
...status,
|
|
592
|
+
isProjectDependency: declaringDirectoryUrl === packageDirectory.url,
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
|
|
428
596
|
const detailsFromFirstReference = (reference) => {
|
|
429
597
|
const referenceInProject = getFirstReferenceInProject(reference);
|
|
430
598
|
if (
|
|
@@ -8288,6 +8456,7 @@ const jsenvPluginAutoreloadClient = () => {
|
|
|
8288
8456
|
const jsenvPluginAutoreloadServer = ({
|
|
8289
8457
|
clientFileChangeEventEmitter,
|
|
8290
8458
|
clientFileDereferencedEventEmitter,
|
|
8459
|
+
reloadRequestEventEmitter,
|
|
8291
8460
|
}) => {
|
|
8292
8461
|
return {
|
|
8293
8462
|
name: "jsenv:autoreload_server",
|
|
@@ -8616,6 +8785,15 @@ const jsenvPluginAutoreloadServer = ({
|
|
|
8616
8785
|
});
|
|
8617
8786
|
},
|
|
8618
8787
|
);
|
|
8788
|
+
// something outside the url graph wants the page back from scratch,
|
|
8789
|
+
// typically a dependency that just got installed into node_modules
|
|
8790
|
+
reloadRequestEventEmitter.on(({ cause, reason }) => {
|
|
8791
|
+
serverEventInfo.sendServerEvent({
|
|
8792
|
+
cause,
|
|
8793
|
+
type: "full",
|
|
8794
|
+
typeReason: reason,
|
|
8795
|
+
});
|
|
8796
|
+
});
|
|
8619
8797
|
},
|
|
8620
8798
|
},
|
|
8621
8799
|
serverRoutes: [
|
|
@@ -8726,6 +8904,7 @@ const jsenvPluginHotSearchParam = () => {
|
|
|
8726
8904
|
const jsenvPluginAutoreload = ({
|
|
8727
8905
|
clientFileChangeEventEmitter,
|
|
8728
8906
|
clientFileDereferencedEventEmitter,
|
|
8907
|
+
reloadRequestEventEmitter,
|
|
8729
8908
|
}) => {
|
|
8730
8909
|
return [
|
|
8731
8910
|
jsenvPluginHotSearchParam(),
|
|
@@ -8733,10 +8912,75 @@ const jsenvPluginAutoreload = ({
|
|
|
8733
8912
|
jsenvPluginAutoreloadServer({
|
|
8734
8913
|
clientFileChangeEventEmitter,
|
|
8735
8914
|
clientFileDereferencedEventEmitter,
|
|
8915
|
+
reloadRequestEventEmitter,
|
|
8736
8916
|
}),
|
|
8737
8917
|
];
|
|
8738
8918
|
};
|
|
8739
8919
|
|
|
8920
|
+
/*
|
|
8921
|
+
* Tells the browser about the dependencies declared in package.json that
|
|
8922
|
+
* node_modules does not match, so the page can say it is running with something
|
|
8923
|
+
* else than what the project asks for.
|
|
8924
|
+
*
|
|
8925
|
+
* The state is sent on page load (it is known before any client connects) and
|
|
8926
|
+
* again whenever it changes, so a page opened during an install is updated
|
|
8927
|
+
* without being reloaded.
|
|
8928
|
+
*/
|
|
8929
|
+
|
|
8930
|
+
|
|
8931
|
+
const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
|
|
8932
|
+
|
|
8933
|
+
const jsenvPluginDependencyStatus = ({
|
|
8934
|
+
dependencyProblemEventEmitter,
|
|
8935
|
+
getDependencyProblems,
|
|
8936
|
+
}) => {
|
|
8937
|
+
return {
|
|
8938
|
+
name: "jsenv:dependency_status",
|
|
8939
|
+
appliesDuring: "dev",
|
|
8940
|
+
serverEvents: {
|
|
8941
|
+
dependency_status: (serverEventInfo) => {
|
|
8942
|
+
dependencyProblemEventEmitter.on((problems) => {
|
|
8943
|
+
// the state is baked into the html by the injection below, so a page
|
|
8944
|
+
// served from the graph as it is would come back with the previous
|
|
8945
|
+
// state, which is exactly what a reload triggered by an install does
|
|
8946
|
+
for (const urlInfo of serverEventInfo.kitchen.graph.urlInfoMap.values()) {
|
|
8947
|
+
if (urlInfo.type === "html" && urlInfo.content !== undefined) {
|
|
8948
|
+
urlInfo.onModified();
|
|
8949
|
+
}
|
|
8950
|
+
}
|
|
8951
|
+
serverEventInfo.sendServerEvent({ problems });
|
|
8952
|
+
});
|
|
8953
|
+
},
|
|
8954
|
+
},
|
|
8955
|
+
transformUrlContent: {
|
|
8956
|
+
html: (htmlUrlInfo) => {
|
|
8957
|
+
const htmlAst = parseHtml({
|
|
8958
|
+
html: htmlUrlInfo.content,
|
|
8959
|
+
url: htmlUrlInfo.url,
|
|
8960
|
+
});
|
|
8961
|
+
const clientReference = htmlUrlInfo.dependencies.inject({
|
|
8962
|
+
type: "script",
|
|
8963
|
+
subtype: "js_module",
|
|
8964
|
+
expectedType: "js_module",
|
|
8965
|
+
specifier: clientFileUrl,
|
|
8966
|
+
});
|
|
8967
|
+
injectJsenvScript(htmlAst, {
|
|
8968
|
+
type: "module",
|
|
8969
|
+
src: clientReference.generatedSpecifier,
|
|
8970
|
+
initCall: {
|
|
8971
|
+
callee: "initDependencyStatus",
|
|
8972
|
+
params: { problems: getDependencyProblems() },
|
|
8973
|
+
},
|
|
8974
|
+
pluginName: "jsenv:dependency_status",
|
|
8975
|
+
});
|
|
8976
|
+
return {
|
|
8977
|
+
content: stringifyHtmlAst(htmlAst),
|
|
8978
|
+
};
|
|
8979
|
+
},
|
|
8980
|
+
},
|
|
8981
|
+
};
|
|
8982
|
+
};
|
|
8983
|
+
|
|
8740
8984
|
const jsenvPluginCacheControl = ({
|
|
8741
8985
|
versionedUrls = true,
|
|
8742
8986
|
maxAge = SECONDS_IN_30_DAYS,
|
|
@@ -9158,6 +9402,16 @@ const jsenvPluginWorkspaceBundle = ({ packageDirectory }) => {
|
|
|
9158
9402
|
// root package, we don't want to bundle
|
|
9159
9403
|
return null;
|
|
9160
9404
|
}
|
|
9405
|
+
if (reference.type !== "js_import") {
|
|
9406
|
+
// Only consolidate ES imports of a workspace package into its single
|
|
9407
|
+
// bundle. Other reference kinds are their own entry points — most
|
|
9408
|
+
// importantly an HTML <script src> pointing at a package file (jsenv
|
|
9409
|
+
// injects its own client scripts that way: server events, the client
|
|
9410
|
+
// monitoring reporter, custom-elements-redefine…). Redirecting those to
|
|
9411
|
+
// the package main would run the wrong module, and for @jsenv/core the
|
|
9412
|
+
// main is node-only code (node:url) that cannot be served to a browser.
|
|
9413
|
+
return null;
|
|
9414
|
+
}
|
|
9161
9415
|
// we make sure we target the bundle version of the package
|
|
9162
9416
|
// otherwise we might execute some parts of the package code multiple times.
|
|
9163
9417
|
// so we need to redirect the potential reference to non entry point to the package main entry point
|
|
@@ -9263,6 +9517,7 @@ const getCorePlugins = ({
|
|
|
9263
9517
|
|
|
9264
9518
|
clientAutoreload,
|
|
9265
9519
|
clientAutoreloadOnServerRestart,
|
|
9520
|
+
dependencyStatus,
|
|
9266
9521
|
cacheControl,
|
|
9267
9522
|
scenarioPlaceholders = true,
|
|
9268
9523
|
ribbon = true,
|
|
@@ -9361,6 +9616,9 @@ const getCorePlugins = ({
|
|
|
9361
9616
|
...(clientAutoreload && clientAutoreload.enabled
|
|
9362
9617
|
? [jsenvPluginAutoreload(clientAutoreload)]
|
|
9363
9618
|
: []),
|
|
9619
|
+
...(dependencyStatus
|
|
9620
|
+
? [jsenvPluginDependencyStatus(dependencyStatus)]
|
|
9621
|
+
: []),
|
|
9364
9622
|
...(cacheControl ? [jsenvPluginCacheControl(cacheControl)] : []),
|
|
9365
9623
|
...(ribbon ? [jsenvPluginRibbon({ rootDirectoryUrl, ...ribbon })] : []),
|
|
9366
9624
|
...(dropToOpen ? [jsenvPluginDropToOpen()] : []),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Displays the "some dependencies are outdated" warning overlay.
|
|
3
|
+
*
|
|
4
|
+
* A missing dependency is not handled here: it makes the import fail, so the
|
|
5
|
+
* error overlay already says it, with more precision (which import, where).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
let removeOverlay = () => {};
|
|
9
|
+
|
|
10
|
+
const initDependencyStatus = ({ problems }) => {
|
|
11
|
+
render(problems);
|
|
12
|
+
// without the server events channel the page only knows the state it was
|
|
13
|
+
// served with, which is still better than nothing
|
|
14
|
+
if (window.__server_events__) {
|
|
15
|
+
window.__server_events__.listenEvents({
|
|
16
|
+
dependency_status: (event) => {
|
|
17
|
+
render(event.data.problems);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const render = (problems) => {
|
|
24
|
+
const outdatedList = problems.filter(({ state }) => state === "outdated");
|
|
25
|
+
removeOverlay();
|
|
26
|
+
removeOverlay = () => {};
|
|
27
|
+
if (outdatedList.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const supervisor = window.__supervisor__;
|
|
31
|
+
if (!supervisor || !supervisor.reportWarning) {
|
|
32
|
+
console.warn(summarize(outdatedList));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
removeOverlay = supervisor.reportWarning({
|
|
36
|
+
title:
|
|
37
|
+
outdatedList.length === 1
|
|
38
|
+
? "A dependency is outdated"
|
|
39
|
+
: `${outdatedList.length} dependencies are outdated`,
|
|
40
|
+
text: outdatedList.map(describe).join("\n\n"),
|
|
41
|
+
details: [
|
|
42
|
+
"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
|
+
"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
|
+
],
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const describe = ({ packageName, declaredVersion, installedVersion }) => {
|
|
50
|
+
return `${packageName}
|
|
51
|
+
package.json declares ${declaredVersion}
|
|
52
|
+
node_modules holds ${installedVersion || "an unknown version"}`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const summarize = (outdatedList) => {
|
|
56
|
+
return `[jsenv] outdated dependencies, run npm install: ${outdatedList
|
|
57
|
+
.map(({ packageName }) => packageName)
|
|
58
|
+
.join(", ")}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export { initDependencyStatus };
|