@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.
@@ -246,31 +246,36 @@ export const devServerPluginServeSourceFiles = ({
246
246
  }
247
247
  const urlInfo = reference.urlInfo;
248
248
  const ifNoneMatch = request.headers["if-none-match"];
249
- const urlInfoTargetedByCache =
250
- urlInfo.findParentIfInline() || urlInfo;
249
+ const inlineParentUrlInfo = urlInfo.findParentIfInline();
250
+ const urlInfoTargetedByCache = inlineParentUrlInfo || urlInfo;
251
+ const respondWithNotModified = () => {
252
+ const headers = {
253
+ "cache-control": `private,max-age=0,must-revalidate`,
254
+ };
255
+ Object.keys(urlInfo.headers).forEach((key) => {
256
+ if (key !== "content-length") {
257
+ headers[key] = urlInfo.headers[key];
258
+ }
259
+ });
260
+ return {
261
+ status: 304,
262
+ headers,
263
+ };
264
+ };
251
265
 
252
266
  try {
253
- if (!urlInfo.error && ifNoneMatch) {
267
+ // an inline url info is cooked again every time the file containing it
268
+ // is cooked, so its content is only known after cooking; its etag is
269
+ // compared below, once cooked
270
+ if (!urlInfo.error && ifNoneMatch && !inlineParentUrlInfo) {
254
271
  const [clientOriginalContentEtag, clientContentEtag] =
255
272
  ifNoneMatch.split("_");
256
273
  if (
257
- urlInfoTargetedByCache.originalContentEtag ===
258
- clientOriginalContentEtag &&
259
- urlInfoTargetedByCache.contentEtag === clientContentEtag &&
260
- urlInfoTargetedByCache.isValid()
274
+ urlInfo.originalContentEtag === clientOriginalContentEtag &&
275
+ urlInfo.contentEtag === clientContentEtag &&
276
+ urlInfo.isValid()
261
277
  ) {
262
- const headers = {
263
- "cache-control": `private,max-age=0,must-revalidate`,
264
- };
265
- Object.keys(urlInfo.headers).forEach((key) => {
266
- if (key !== "content-length") {
267
- headers[key] = urlInfo.headers[key];
268
- }
269
- });
270
- return {
271
- status: 304,
272
- headers,
273
- };
278
+ return respondWithNotModified();
274
279
  }
275
280
  }
276
281
  await urlInfo.cook({ request, reference });
@@ -278,6 +283,19 @@ export const devServerPluginServeSourceFiles = ({
278
283
  if (response) {
279
284
  return response;
280
285
  }
286
+ // the original content of an inline url info is the one of the file
287
+ // containing it, but its cooked content is its own: it can change while
288
+ // the containing file stays identical (an import resolving to a new
289
+ // version of a package for instance)
290
+ const eTag = `${urlInfoTargetedByCache.originalContentEtag}_${urlInfo.contentEtag}`;
291
+ if (
292
+ !urlInfo.error &&
293
+ ifNoneMatch === eTag &&
294
+ inlineParentUrlInfo &&
295
+ !cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
296
+ ) {
297
+ return respondWithNotModified();
298
+ }
281
299
  response = {
282
300
  url: reference.url,
283
301
  status: 200,
@@ -294,7 +312,7 @@ export const devServerPluginServeSourceFiles = ({
294
312
  : {
295
313
  "cache-control": `private,max-age=0,must-revalidate`,
296
314
  // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
297
- "eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
315
+ eTag,
298
316
  }),
299
317
  ...urlInfo.headers,
300
318
  "content-type": urlInfo.contentType,
@@ -373,7 +391,10 @@ export const devServerPluginServeSourceFiles = ({
373
391
  statusText: originalError.reason,
374
392
  };
375
393
  }
376
- if (code === "NOT_FOUND") {
394
+ // MODULE_NOT_FOUND: a specifier could not be resolved to a file,
395
+ // so something is missing on the filesystem; 500 is for the errors
396
+ // the server does not see coming
397
+ if (code === "NOT_FOUND" || code === "MODULE_NOT_FOUND") {
377
398
  return {
378
399
  url: reference.url,
379
400
  status: 404,
@@ -13,6 +13,7 @@ import { existsSync } from "node:fs";
13
13
 
14
14
  import { defaultRuntimeCompat } from "../build/build_params.js";
15
15
  import { createEventEmitter } from "../helpers/event_emitter.js";
16
+ import { watchDependencies } from "../helpers/watch_dependencies.js";
16
17
  import { jsenvCoreDirectoryUrl } from "../jsenv_core_directory_url.js";
17
18
  import { createPackageDirectory } from "../kitchen/package_directory.js";
18
19
  import { createJsenvPluginStore } from "../plugins/jsenv_plugins_controller.js";
@@ -187,13 +188,36 @@ export const startDevServer = async ({
187
188
  const packageDirectory = createPackageDirectory({ sourceDirectoryUrl });
188
189
  const clientFileChangeEventEmitter = createEventEmitter();
189
190
  const clientFileDereferencedEventEmitter = createEventEmitter();
191
+ const reloadRequestEventEmitter = createEventEmitter();
190
192
  clientAutoreload = {
191
193
  enabled: true,
192
194
  clientServerEventsConfig: {},
193
195
  clientFileChangeEventEmitter,
194
196
  clientFileDereferencedEventEmitter,
197
+ reloadRequestEventEmitter,
195
198
  ...clientAutoreload,
196
199
  };
200
+ const dependencyProblemEventEmitter = createEventEmitter();
201
+ const dependencyWatcher = watchDependencies(packageDirectory, {
202
+ onChange: (problems) => {
203
+ dependencyProblemEventEmitter.emit(problems);
204
+ },
205
+ onProblem: ({ packageName, declaredVersion, installedVersion, state }) => {
206
+ logger.warn(
207
+ state === "missing"
208
+ ? `"${packageName}@${declaredVersion}" is declared in package.json but not installed, run npm install`
209
+ : `"${packageName}" is installed in ${installedVersion} but package.json declares ${declaredVersion}, run npm install`,
210
+ );
211
+ },
212
+ onInstalled: ({ packageName, declaredVersion }) => {
213
+ logger.info(`"${packageName}@${declaredVersion}" is now installed`);
214
+ reloadRequestEventEmitter.emit({
215
+ cause: `${packageName}@${declaredVersion} installed`,
216
+ reason: `a dependency became available in node_modules`,
217
+ });
218
+ },
219
+ });
220
+ serverStopCallbackSet.add(dependencyWatcher.stop);
197
221
 
198
222
  const devServerJsenvPluginStore = await createJsenvPluginStore([
199
223
  jsenvPluginServerEvents({ clientAutoreload }),
@@ -228,6 +252,10 @@ export const startDevServer = async ({
228
252
 
229
253
  clientAutoreload,
230
254
  clientAutoreloadOnServerRestart,
255
+ dependencyStatus: {
256
+ dependencyProblemEventEmitter,
257
+ getDependencyProblems: dependencyWatcher.getProblems,
258
+ },
231
259
  cacheControl,
232
260
  ribbon,
233
261
  dropToOpen,
@@ -0,0 +1,110 @@
1
+ /*
2
+ * Detects when "npm install" makes a missing or outdated dependency match what
3
+ * the project package.json declares, so the browser can be reloaded at that
4
+ * moment.
5
+ *
6
+ * node_modules is deliberately not watched: it is far too big, and an install
7
+ * rewrites, dedupes and moves package directories around, so a watcher placed
8
+ * on one of them is unreliable. Instead the few packages known to be missing or
9
+ * outdated are polled, which costs a couple of readFileSync and stops as soon as
10
+ * they are all installed. The project package.json is watched though: it is a
11
+ * single file, and editing it is what puts a dependency out of date in the first
12
+ * place.
13
+ */
14
+
15
+ import { registerFileLifecycle } from "@jsenv/filesystem";
16
+
17
+ import { readDependencyStatuses } from "../kitchen/package_dependencies.js";
18
+
19
+ const POLL_INTERVAL = 500;
20
+
21
+ export const watchDependencies = (
22
+ packageDirectory,
23
+ { onProblem, onInstalled, onChange, pollInterval = POLL_INTERVAL },
24
+ ) => {
25
+ let problemMap = new Map();
26
+ const watcher = {
27
+ getProblems: () => Array.from(problemMap.values()),
28
+ stop: () => {},
29
+ };
30
+ if (!packageDirectory.url) {
31
+ return watcher;
32
+ }
33
+ let timer = null;
34
+
35
+ const check = () => {
36
+ const nextProblemMap = new Map();
37
+ for (const status of readDependencyStatuses(packageDirectory)) {
38
+ if (status.state === "missing" || status.state === "outdated") {
39
+ nextProblemMap.set(status.packageName, status);
40
+ }
41
+ }
42
+ for (const [packageName, status] of nextProblemMap) {
43
+ const previousStatus = problemMap.get(packageName);
44
+ if (
45
+ !previousStatus ||
46
+ previousStatus.state !== status.state ||
47
+ previousStatus.declaredVersion !== status.declaredVersion
48
+ ) {
49
+ onProblem(status);
50
+ }
51
+ }
52
+ for (const [packageName, previousStatus] of problemMap) {
53
+ if (!nextProblemMap.has(packageName)) {
54
+ onInstalled(previousStatus);
55
+ }
56
+ }
57
+ const changed =
58
+ nextProblemMap.size !== problemMap.size ||
59
+ Array.from(nextProblemMap.keys()).some((packageName) => {
60
+ const previousStatus = problemMap.get(packageName);
61
+ const status = nextProblemMap.get(packageName);
62
+ return (
63
+ !previousStatus ||
64
+ previousStatus.state !== status.state ||
65
+ previousStatus.declaredVersion !== status.declaredVersion ||
66
+ previousStatus.installedVersion !== status.installedVersion
67
+ );
68
+ });
69
+ problemMap = nextProblemMap;
70
+ if (changed) {
71
+ onChange(watcher.getProblems());
72
+ }
73
+ if (problemMap.size === 0) {
74
+ stopPolling();
75
+ } else {
76
+ startPolling();
77
+ }
78
+ };
79
+
80
+ const startPolling = () => {
81
+ if (timer) {
82
+ return;
83
+ }
84
+ timer = setInterval(check, pollInterval);
85
+ timer.unref();
86
+ };
87
+ const stopPolling = () => {
88
+ if (!timer) {
89
+ return;
90
+ }
91
+ clearInterval(timer);
92
+ timer = null;
93
+ };
94
+
95
+ const unwatchPackageJson = registerFileLifecycle(
96
+ new URL("package.json", packageDirectory.url),
97
+ {
98
+ added: check,
99
+ updated: check,
100
+ keepProcessAlive: false,
101
+ },
102
+ );
103
+ check();
104
+
105
+ watcher.stop = () => {
106
+ stopPolling();
107
+ unwatchPackageJson();
108
+ };
109
+ return watcher;
110
+ };
@@ -2,6 +2,11 @@ import { createDetailedMessage, generateContentFrame } from "@jsenv/humanize";
2
2
  import { stringifyUrlSite } from "@jsenv/urls";
3
3
  import { pathToFileURL } from "node:url";
4
4
 
5
+ import {
6
+ packageNameFromSpecifier,
7
+ readDependencyStatus,
8
+ } from "./package_dependencies.js";
9
+
5
10
  export const createResolveUrlError = ({
6
11
  jsenvPluginsController,
7
12
  reference,
@@ -41,6 +46,18 @@ ${reason}`,
41
46
  });
42
47
  }
43
48
  if (error.code === "MODULE_NOT_FOUND") {
49
+ const notInstalledStatus = readNotInstalledStatus(reference);
50
+ if (notInstalledStatus) {
51
+ const { packageName, declaredVersion, declaredBy, isProjectDependency } =
52
+ notInstalledStatus;
53
+ return createFailedToResolveUrlError({
54
+ "reason": isProjectDependency
55
+ ? `"${packageName}" is declared in package.json but not installed`
56
+ : `"${packageName}" is declared by "${declaredBy}" but not installed`,
57
+ "declared version": declaredVersion,
58
+ "suggestion": `run npm install, the page will reload once "${packageName}" is installed`,
59
+ });
60
+ }
44
61
  const bareSpecifierError = createFailedToResolveUrlError({
45
62
  reason: `"${reference.specifier}" is a bare specifier but cannot be remapped to a package`,
46
63
  });
@@ -295,6 +312,32 @@ const getErrorTrace = (error, reference) => {
295
312
  };
296
313
  };
297
314
 
315
+ // a bare specifier is resolved against the dependencies of the package
316
+ // containing the file that imports it, which is the project one for a source
317
+ // file but an other one for a file inside node_modules
318
+ const readNotInstalledStatus = (reference) => {
319
+ const { ownerUrlInfo } = reference;
320
+ const { packageDirectory } = ownerUrlInfo.context;
321
+ if (!packageDirectory) {
322
+ return null;
323
+ }
324
+ const declaringDirectoryUrl =
325
+ packageDirectory.find(ownerUrlInfo.url) || packageDirectory.url;
326
+ const packageName = packageNameFromSpecifier(reference.specifier);
327
+ const status = readDependencyStatus(
328
+ packageDirectory,
329
+ packageName,
330
+ declaringDirectoryUrl,
331
+ );
332
+ if (!status || status.state !== "missing") {
333
+ return null;
334
+ }
335
+ return {
336
+ ...status,
337
+ isProjectDependency: declaringDirectoryUrl === packageDirectory.url,
338
+ };
339
+ };
340
+
298
341
  const detailsFromFirstReference = (reference) => {
299
342
  const referenceInProject = getFirstReferenceInProject(reference);
300
343
  if (
@@ -0,0 +1,160 @@
1
+ /*
2
+ * Compares what a package.json declares with what is actually inside
3
+ * node_modules, so the dev server can tell a dependency apart when it is
4
+ * missing (never installed) or outdated (installed at an other version).
5
+ *
6
+ * Only the dependencies declared by a package are looked at, never the
7
+ * transitive ones: a declared dependency is what pulls the rest, so it is
8
+ * enough to know whether an install is needed or over.
9
+ *
10
+ * Only exact declared versions ("1.2.3") are compared: a range ("^1.2.3"), a
11
+ * file/workspace protocol or a tag cannot be checked without resolving what npm
12
+ * would pick, which is way beyond what is needed here.
13
+ */
14
+
15
+ import { existsSync } from "node:fs";
16
+
17
+ const DEPENDENCY_FIELDS = [
18
+ "dependencies",
19
+ "devDependencies",
20
+ "optionalDependencies",
21
+ ];
22
+
23
+ export const packageNameFromSpecifier = (specifier) => {
24
+ const parts = specifier.split("/");
25
+ if (specifier[0] === "@") {
26
+ return parts.slice(0, 2).join("/");
27
+ }
28
+ return parts[0];
29
+ };
30
+
31
+ /*
32
+ * declaringDirectoryUrl is the package directory the importer belongs to, which
33
+ * is not always the project one: a file inside node_modules resolves its bare
34
+ * specifiers against the dependencies of the package containing it.
35
+ */
36
+ export const readDependencyStatus = (
37
+ packageDirectory,
38
+ packageName,
39
+ declaringDirectoryUrl = packageDirectory.url,
40
+ ) => {
41
+ const packageJSON = readPackageJSON(packageDirectory, declaringDirectoryUrl);
42
+ if (!packageJSON) {
43
+ return null;
44
+ }
45
+ const declaredVersion = readDeclaredVersion(packageJSON, packageName);
46
+ if (!declaredVersion) {
47
+ return null;
48
+ }
49
+ return createStatus(packageDirectory, {
50
+ packageName,
51
+ declaredVersion,
52
+ declaringDirectoryUrl,
53
+ declaredBy: packageJSON.name,
54
+ });
55
+ };
56
+
57
+ export const readDependencyStatuses = (packageDirectory) => {
58
+ const packageJSON = readPackageJSON(packageDirectory, packageDirectory.url);
59
+ if (!packageJSON) {
60
+ return [];
61
+ }
62
+ const statuses = [];
63
+ const packageNameSet = new Set();
64
+ for (const field of DEPENDENCY_FIELDS) {
65
+ const dependencies = packageJSON[field];
66
+ if (!dependencies) {
67
+ continue;
68
+ }
69
+ for (const packageName of Object.keys(dependencies)) {
70
+ if (packageNameSet.has(packageName)) {
71
+ continue;
72
+ }
73
+ packageNameSet.add(packageName);
74
+ statuses.push(
75
+ createStatus(packageDirectory, {
76
+ packageName,
77
+ declaredVersion: dependencies[packageName],
78
+ declaringDirectoryUrl: packageDirectory.url,
79
+ declaredBy: packageJSON.name,
80
+ }),
81
+ );
82
+ }
83
+ }
84
+ return statuses;
85
+ };
86
+
87
+ const createStatus = (
88
+ packageDirectory,
89
+ { packageName, declaredVersion, declaringDirectoryUrl, declaredBy },
90
+ ) => {
91
+ const status = {
92
+ packageName,
93
+ declaredVersion,
94
+ declaredBy,
95
+ installedVersion: null,
96
+ state: "missing",
97
+ };
98
+ const installedDirectoryUrl = findInstalledDirectoryUrl(
99
+ declaringDirectoryUrl,
100
+ packageName,
101
+ );
102
+ if (!installedDirectoryUrl) {
103
+ return status;
104
+ }
105
+ const installedPackageJSON = readPackageJSON(
106
+ packageDirectory,
107
+ installedDirectoryUrl,
108
+ );
109
+ status.installedVersion = installedPackageJSON
110
+ ? installedPackageJSON.version
111
+ : null;
112
+ status.state =
113
+ isExactVersion(declaredVersion) &&
114
+ status.installedVersion !== declaredVersion
115
+ ? "outdated"
116
+ : "installed";
117
+ return status;
118
+ };
119
+
120
+ const findInstalledDirectoryUrl = (declaringDirectoryUrl, packageName) => {
121
+ let directoryUrl = declaringDirectoryUrl;
122
+ while (directoryUrl) {
123
+ const candidateUrl = `${directoryUrl}node_modules/${packageName}/`;
124
+ if (existsSync(new URL(`${candidateUrl}package.json`))) {
125
+ return candidateUrl;
126
+ }
127
+ const parentUrl = new URL("../", directoryUrl).href;
128
+ if (parentUrl === directoryUrl) {
129
+ return null;
130
+ }
131
+ directoryUrl = parentUrl;
132
+ }
133
+ return null;
134
+ };
135
+
136
+ const readDeclaredVersion = (packageJSON, packageName) => {
137
+ for (const field of DEPENDENCY_FIELDS) {
138
+ const dependencies = packageJSON[field];
139
+ if (dependencies && dependencies[packageName]) {
140
+ return dependencies[packageName];
141
+ }
142
+ }
143
+ return null;
144
+ };
145
+
146
+ // an install in progress can be caught halfway, with a package.json not written yet
147
+ const readPackageJSON = (packageDirectory, directoryUrl) => {
148
+ if (!directoryUrl) {
149
+ return null;
150
+ }
151
+ try {
152
+ return packageDirectory.read(directoryUrl);
153
+ } catch {
154
+ return null;
155
+ }
156
+ };
157
+
158
+ const isExactVersion = (declaredVersion) => {
159
+ return /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(declaredVersion);
160
+ };
@@ -5,6 +5,7 @@ import { jsenvPluginHotSearchParam } from "./jsenv_plugin_hot_search_param.js";
5
5
  export const jsenvPluginAutoreload = ({
6
6
  clientFileChangeEventEmitter,
7
7
  clientFileDereferencedEventEmitter,
8
+ reloadRequestEventEmitter,
8
9
  }) => {
9
10
  return [
10
11
  jsenvPluginHotSearchParam(),
@@ -12,6 +13,7 @@ export const jsenvPluginAutoreload = ({
12
13
  jsenvPluginAutoreloadServer({
13
14
  clientFileChangeEventEmitter,
14
15
  clientFileDereferencedEventEmitter,
16
+ reloadRequestEventEmitter,
15
17
  }),
16
18
  ];
17
19
  };
@@ -3,6 +3,7 @@ import { urlIsOrIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
3
3
  export const jsenvPluginAutoreloadServer = ({
4
4
  clientFileChangeEventEmitter,
5
5
  clientFileDereferencedEventEmitter,
6
+ reloadRequestEventEmitter,
6
7
  }) => {
7
8
  return {
8
9
  name: "jsenv:autoreload_server",
@@ -331,6 +332,15 @@ export const jsenvPluginAutoreloadServer = ({
331
332
  });
332
333
  },
333
334
  );
335
+ // something outside the url graph wants the page back from scratch,
336
+ // typically a dependency that just got installed into node_modules
337
+ reloadRequestEventEmitter.on(({ cause, reason }) => {
338
+ serverEventInfo.sendServerEvent({
339
+ cause,
340
+ type: "full",
341
+ typeReason: reason,
342
+ });
343
+ });
334
344
  },
335
345
  },
336
346
  serverRoutes: [
@@ -0,0 +1,59 @@
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
+ export 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
+ };
@@ -0,0 +1,64 @@
1
+ /*
2
+ * Tells the browser about the dependencies declared in package.json that
3
+ * node_modules does not match, so the page can say it is running with something
4
+ * else than what the project asks for.
5
+ *
6
+ * The state is sent on page load (it is known before any client connects) and
7
+ * again whenever it changes, so a page opened during an install is updated
8
+ * without being reloaded.
9
+ */
10
+
11
+ import { injectJsenvScript, parseHtml, stringifyHtmlAst } from "@jsenv/ast";
12
+
13
+ const clientFileUrl = import.meta.resolve("./client/dependency_status.js");
14
+
15
+ export const jsenvPluginDependencyStatus = ({
16
+ dependencyProblemEventEmitter,
17
+ getDependencyProblems,
18
+ }) => {
19
+ return {
20
+ name: "jsenv:dependency_status",
21
+ appliesDuring: "dev",
22
+ serverEvents: {
23
+ dependency_status: (serverEventInfo) => {
24
+ dependencyProblemEventEmitter.on((problems) => {
25
+ // the state is baked into the html by the injection below, so a page
26
+ // served from the graph as it is would come back with the previous
27
+ // state, which is exactly what a reload triggered by an install does
28
+ for (const urlInfo of serverEventInfo.kitchen.graph.urlInfoMap.values()) {
29
+ if (urlInfo.type === "html" && urlInfo.content !== undefined) {
30
+ urlInfo.onModified();
31
+ }
32
+ }
33
+ serverEventInfo.sendServerEvent({ problems });
34
+ });
35
+ },
36
+ },
37
+ transformUrlContent: {
38
+ html: (htmlUrlInfo) => {
39
+ const htmlAst = parseHtml({
40
+ html: htmlUrlInfo.content,
41
+ url: htmlUrlInfo.url,
42
+ });
43
+ const clientReference = htmlUrlInfo.dependencies.inject({
44
+ type: "script",
45
+ subtype: "js_module",
46
+ expectedType: "js_module",
47
+ specifier: clientFileUrl,
48
+ });
49
+ injectJsenvScript(htmlAst, {
50
+ type: "module",
51
+ src: clientReference.generatedSpecifier,
52
+ initCall: {
53
+ callee: "initDependencyStatus",
54
+ params: { problems: getDependencyProblems() },
55
+ },
56
+ pluginName: "jsenv:dependency_status",
57
+ });
58
+ return {
59
+ content: stringifyHtmlAst(htmlAst),
60
+ };
61
+ },
62
+ },
63
+ };
64
+ };
@@ -20,6 +20,7 @@ import { jsenvPluginImportMetaCss } from "./import_meta_css/jsenv_plugin_import_
20
20
  // autoreload
21
21
  import { jsenvPluginImportMetaHot } from "./import_meta_hot/jsenv_plugin_import_meta_hot.js";
22
22
  import { jsenvPluginAutoreload } from "./autoreload/jsenv_plugin_autoreload.js";
23
+ import { jsenvPluginDependencyStatus } from "./dependency_status/jsenv_plugin_dependency_status.js";
23
24
  import { jsenvPluginCacheControl } from "./cache_control/jsenv_plugin_cache_control.js";
24
25
  import { jsenvPluginCustomElementsRedefine } from "./custom_elements_redefine/jsenv_plugin_custom_elements_redefine.js";
25
26
  // other
@@ -55,6 +56,7 @@ export const getCorePlugins = ({
55
56
 
56
57
  clientAutoreload,
57
58
  clientAutoreloadOnServerRestart,
59
+ dependencyStatus,
58
60
  cacheControl,
59
61
  scenarioPlaceholders = true,
60
62
  ribbon = true,
@@ -153,6 +155,9 @@ export const getCorePlugins = ({
153
155
  ...(clientAutoreload && clientAutoreload.enabled
154
156
  ? [jsenvPluginAutoreload(clientAutoreload)]
155
157
  : []),
158
+ ...(dependencyStatus
159
+ ? [jsenvPluginDependencyStatus(dependencyStatus)]
160
+ : []),
156
161
  ...(cacheControl ? [jsenvPluginCacheControl(cacheControl)] : []),
157
162
  ...(ribbon ? [jsenvPluginRibbon({ rootDirectoryUrl, ...ribbon })] : []),
158
163
  ...(dropToOpen ? [jsenvPluginDropToOpen()] : []),