@jsenv/core 41.5.20 → 41.5.22

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.
@@ -236,17 +236,29 @@ const isExactVersion = (declaredVersion) => {
236
236
  };
237
237
 
238
238
  /*
239
- * Detects when "npm install" makes a missing or outdated dependency match what
240
- * the project package.json declares, so the browser can be reloaded at that
241
- * moment.
239
+ * Detects the moment "npm install" changes what the running pages get from
240
+ * node_modules, so the browser can be reloaded at that moment:
241
+ *
242
+ * - a declared dependency stops being missing or outdated: what package.json
243
+ * asks for is there;
244
+ * - a package the pages were served changes version: the "?v=" baked into the
245
+ * urls they evaluated names a version node_modules no longer holds. Left
246
+ * alone, the next hot reload of a file importing that package resolves it to
247
+ * the new version and the page evaluates a second copy of the package next
248
+ * to the first, two module scopes for something meant to exist once.
242
249
  *
243
250
  * node_modules is deliberately not watched: it is far too big, and an install
244
251
  * rewrites, dedupes and moves package directories around, so a watcher placed
245
- * on one of them is unreliable. Instead the few packages known to be missing or
246
- * outdated are polled, which costs a couple of readFileSync and stops as soon as
247
- * they are all installed. The project package.json is watched though: it is a
248
- * single file, and editing it is what puts a dependency out of date in the first
249
- * place.
252
+ * on one of them is unreliable. Instead a few package.json files are polled,
253
+ * at the cost of a stat each: the packages known to be missing or outdated,
254
+ * and the packages the pages were served, whose package.json the url graph
255
+ * keeps (that is where "?v=" comes from). The project package.json is watched
256
+ * though: it is a single file, and editing it is what puts a dependency out of
257
+ * date in the first place.
258
+ *
259
+ * Both detections run in the same tick so that an install fixing an outdated
260
+ * package the page runs is reported once: two reports would be two reloads,
261
+ * the second one landing on the page that just came back.
250
262
  */
251
263
 
252
264
 
@@ -254,7 +266,13 @@ const POLL_INTERVAL = 500;
254
266
 
255
267
  const watchDependencies = (
256
268
  packageDirectory,
257
- { onProblem, onInstalled, onChange, pollInterval = POLL_INTERVAL },
269
+ {
270
+ getKitchens = () => [],
271
+ onProblem,
272
+ onInstalled,
273
+ onChange,
274
+ pollInterval = POLL_INTERVAL,
275
+ },
258
276
  ) => {
259
277
  let problemMap = new Map();
260
278
  // every path given to the browser is relative to the package directory, the
@@ -269,11 +287,49 @@ const watchDependencies = (
269
287
  if (!packageDirectory.url) {
270
288
  return watcher;
271
289
  }
272
- let timer = null;
273
290
 
274
- const check = () => {
291
+ const checkServedVersions = (installMap) => {
292
+ for (const kitchen of getKitchens()) {
293
+ for (const urlInfo of kitchen.graph.urlInfoMap.values()) {
294
+ const served = readServedVersion(urlInfo);
295
+ if (!served) {
296
+ continue;
297
+ }
298
+ const installedPackageJson = readInstalledPackageJson(
299
+ packageDirectory,
300
+ served.directoryUrl,
301
+ );
302
+ if (!installedPackageJson) {
303
+ // the package is being rewritten, what it becomes is known once it is back
304
+ continue;
305
+ }
306
+ const installedVersion = installedPackageJson.version;
307
+ if (installedVersion === served.version) {
308
+ continue;
309
+ }
310
+ // the graph learns the file moved, as it would from a watcher: the
311
+ // files importing this package are cooked again with the new "?v="
312
+ // when the page comes back
313
+ urlInfo.onModified();
314
+ if (installMap.has(served.packageName)) {
315
+ continue;
316
+ }
317
+ installMap.set(served.packageName, {
318
+ packageName: served.packageName,
319
+ installedVersion,
320
+ servedVersion: served.version,
321
+ declaredVersion: null,
322
+ severity: "warning",
323
+ });
324
+ }
325
+ }
326
+ };
327
+
328
+ const checkDeclared = (installMap) => {
329
+ const statusMap = new Map();
275
330
  const nextProblemMap = new Map();
276
331
  for (const status of readDependencyStatuses(packageDirectory)) {
332
+ statusMap.set(status.packageName, status);
277
333
  if (status.state === "missing" || status.state === "outdated") {
278
334
  nextProblemMap.set(status.packageName, status);
279
335
  }
@@ -289,9 +345,21 @@ const watchDependencies = (
289
345
  }
290
346
  }
291
347
  for (const [packageName, previousStatus] of problemMap) {
292
- if (!nextProblemMap.has(packageName)) {
293
- onInstalled(previousStatus);
348
+ if (nextProblemMap.has(packageName)) {
349
+ continue;
294
350
  }
351
+ const install = installMap.get(packageName);
352
+ if (install) {
353
+ install.declaredVersion = previousStatus.declaredVersion;
354
+ continue;
355
+ }
356
+ installMap.set(packageName, {
357
+ packageName,
358
+ installedVersion: statusMap.get(packageName).installedVersion,
359
+ servedVersion: null,
360
+ declaredVersion: previousStatus.declaredVersion,
361
+ severity: previousStatus.severity,
362
+ });
295
363
  }
296
364
  const changed =
297
365
  nextProblemMap.size !== problemMap.size ||
@@ -309,45 +377,96 @@ const watchDependencies = (
309
377
  if (changed) {
310
378
  onChange(watcher.getProblems());
311
379
  }
312
- if (problemMap.size === 0) {
313
- stopPolling();
314
- } else {
315
- startPolling();
316
- }
317
380
  };
318
381
 
319
- const startPolling = () => {
320
- if (timer) {
321
- return;
382
+ // The declared dependencies are compared with node_modules only when
383
+ // something can have changed for them: the project package.json was edited,
384
+ // or a problem is known and an install may be fixing it. Comparing them at
385
+ // every tick would catch installs halfway and report a package as missing
386
+ // while it is being rewritten.
387
+ const check = ({ declared }) => {
388
+ const installMap = new Map();
389
+ checkServedVersions(installMap);
390
+ if (declared || problemMap.size > 0) {
391
+ checkDeclared(installMap);
322
392
  }
323
- timer = setInterval(check, pollInterval);
324
- timer.unref();
325
- };
326
- const stopPolling = () => {
327
- if (!timer) {
328
- return;
393
+ for (const install of installMap.values()) {
394
+ onInstalled(install);
329
395
  }
330
- clearInterval(timer);
331
- timer = null;
332
396
  };
333
397
 
334
398
  const unwatchPackageJson = registerFileLifecycle(
335
399
  new URL("package.json", packageDirectory.url),
336
400
  {
337
- added: check,
338
- updated: check,
401
+ added: () => check({ declared: true }),
402
+ updated: () => check({ declared: true }),
339
403
  keepProcessAlive: false,
340
404
  },
341
405
  );
342
- check();
406
+ check({ declared: true });
407
+ const timer = setInterval(() => {
408
+ check({ declared: false });
409
+ }, pollInterval);
410
+ timer.unref();
343
411
 
344
412
  watcher.stop = () => {
345
- stopPolling();
413
+ clearInterval(timer);
346
414
  unwatchPackageJson();
347
415
  };
348
416
  return watcher;
349
417
  };
350
418
 
419
+ // The url graph keeps the content of every package.json it resolved a bare
420
+ // specifier against, and that content holds the version the page received.
421
+ // It is parsed once per content: the same string is seen at every tick.
422
+ const servedVersionCache = new WeakMap();
423
+ const readServedVersion = (urlInfo) => {
424
+ const { url, content } = urlInfo;
425
+ if (content === undefined) {
426
+ return null;
427
+ }
428
+ if (!url.startsWith("file:") || !url.endsWith("/package.json")) {
429
+ return null;
430
+ }
431
+ const nodeModulesIndex = url.lastIndexOf("/node_modules/");
432
+ if (nodeModulesIndex === -1) {
433
+ return null;
434
+ }
435
+ // dereferenced url infos stay in the graph; nothing imports this package anymore
436
+ if (urlInfo.referenceFromOthersSet.size === 0) {
437
+ return null;
438
+ }
439
+ const fromCache = servedVersionCache.get(urlInfo);
440
+ if (fromCache && fromCache.content === content) {
441
+ return fromCache;
442
+ }
443
+ let packageJson;
444
+ try {
445
+ packageJson = JSON.parse(content);
446
+ } catch {
447
+ return null;
448
+ }
449
+ const served = {
450
+ content,
451
+ directoryUrl: new URL("./", url).href,
452
+ packageName: packageNameFromSpecifier(
453
+ url.slice(nodeModulesIndex + "/node_modules/".length),
454
+ ),
455
+ version: packageJson.version,
456
+ };
457
+ servedVersionCache.set(urlInfo, served);
458
+ return served;
459
+ };
460
+
461
+ // an install in progress can be caught halfway, with a package.json not written yet
462
+ const readInstalledPackageJson = (packageDirectory, directoryUrl) => {
463
+ try {
464
+ return packageDirectory.read(directoryUrl);
465
+ } catch {
466
+ return null;
467
+ }
468
+ };
469
+
351
470
  const jsenvCoreDirectoryUrl = new URL("../", import.meta.url);
352
471
 
353
472
  const createPackageDirectory = ({
@@ -5652,6 +5771,140 @@ const asInheritedInjections = (injections) => {
5652
5771
  return inheritedInjections;
5653
5772
  };
5654
5773
 
5774
+ /*
5775
+ * Text patches applied to files as they are served and built, keyed by file:
5776
+ *
5777
+ * patches: {
5778
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
5779
+ * }
5780
+ *
5781
+ * A key is a url pattern relative to the root directory ("./main.js",
5782
+ * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
5783
+ * walking up from the root directory into node_modules, the way node does,
5784
+ * so the key holds wherever the package manager hoists the package.
5785
+ *
5786
+ * Every `from` must occur exactly once in the file, otherwise the file fails
5787
+ * to cook and says which patch did not apply: a dependency update that moved
5788
+ * the patched code must be looked at, never silently unpatched.
5789
+ */
5790
+
5791
+
5792
+ const jsenvPluginPatches = (rawPatches) => {
5793
+ if (!rawPatches || Object.keys(rawPatches).length === 0) {
5794
+ return [];
5795
+ }
5796
+ let findPatches;
5797
+ const patchesPlugin = {
5798
+ name: "jsenv:patches",
5799
+ appliesDuring: "*",
5800
+ init: (context) => {
5801
+ const { rootDirectoryUrl } = context;
5802
+ const patchesByPattern = {};
5803
+ for (const key of Object.keys(rawPatches)) {
5804
+ const patches = rawPatches[key];
5805
+ assertPatches(patches, key);
5806
+ patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
5807
+ }
5808
+ const associations = URL_META.resolveAssociations(
5809
+ { patches: patchesByPattern },
5810
+ rootDirectoryUrl,
5811
+ );
5812
+ findPatches = (url) => {
5813
+ const { patches } = URL_META.applyAssociations({
5814
+ url: asUrlWithoutSearch(url),
5815
+ associations,
5816
+ });
5817
+ return patches;
5818
+ };
5819
+ },
5820
+ transformUrlContent: (urlInfo) => {
5821
+ const patches = findPatches(urlInfo.url);
5822
+ if (!patches) {
5823
+ return null;
5824
+ }
5825
+ const { content } = urlInfo;
5826
+ const magicSource = createMagicSource(content);
5827
+ for (const { from, to } of patches) {
5828
+ const start = content.indexOf(from);
5829
+ const occurrenceCount =
5830
+ start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
5831
+ if (occurrenceCount !== 1) {
5832
+ const fileRelativeUrl = urlToRelativeUrl(
5833
+ urlInfo.url,
5834
+ urlInfo.context.rootDirectoryUrl,
5835
+ );
5836
+ throw new Error(
5837
+ `patch cannot apply on "${fileRelativeUrl}": ${JSON.stringify(from)} found ${occurrenceCount === 0 ? "nowhere" : "more than once"} in the file. The file may have changed since the patch was written.`,
5838
+ );
5839
+ }
5840
+ magicSource.replace({
5841
+ start,
5842
+ end: start + from.length,
5843
+ replacement: to,
5844
+ });
5845
+ }
5846
+ return magicSource.toContentAndSourcemap();
5847
+ },
5848
+ };
5849
+ return [patchesPlugin];
5850
+ };
5851
+
5852
+ const assertPatches = (patches, key) => {
5853
+ if (!Array.isArray(patches)) {
5854
+ throw new TypeError(
5855
+ `patches["${key}"] must be an array of { from, to }, got ${patches}`,
5856
+ );
5857
+ }
5858
+ for (const patch of patches) {
5859
+ if (
5860
+ !patch ||
5861
+ typeof patch.from !== "string" ||
5862
+ patch.from === "" ||
5863
+ typeof patch.to !== "string"
5864
+ ) {
5865
+ throw new TypeError(
5866
+ `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
5867
+ );
5868
+ }
5869
+ }
5870
+ };
5871
+
5872
+ // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
5873
+ // else names a path inside a package, looked up in node_modules
5874
+ const resolvePatchKey = (key, rootDirectoryUrl) => {
5875
+ if (
5876
+ key.startsWith("./") ||
5877
+ key.startsWith("../") ||
5878
+ key.startsWith("/") ||
5879
+ key.startsWith("file:") ||
5880
+ key.startsWith("*")
5881
+ ) {
5882
+ return key;
5883
+ }
5884
+ const segments = key.split("/");
5885
+ const packageName = key.startsWith("@")
5886
+ ? `${segments[0]}/${segments[1]}`
5887
+ : segments[0];
5888
+ const pathInsidePackage = key.slice(packageName.length);
5889
+ let directoryUrl = new URL(rootDirectoryUrl);
5890
+ while (true) {
5891
+ const packageDirectoryUrl = new URL(
5892
+ `./node_modules/${packageName}/`,
5893
+ directoryUrl,
5894
+ );
5895
+ if (existsSync(packageDirectoryUrl)) {
5896
+ return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
5897
+ }
5898
+ const parentDirectoryUrl = new URL("../", directoryUrl);
5899
+ if (parentDirectoryUrl.href === directoryUrl.href) {
5900
+ throw new Error(
5901
+ `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
5902
+ );
5903
+ }
5904
+ directoryUrl = parentDirectoryUrl;
5905
+ }
5906
+ };
5907
+
5655
5908
  const jsenvPluginInliningAsDataUrl = () => {
5656
5909
  return {
5657
5910
  name: "jsenv:inlining_as_data_url",
@@ -8101,6 +8354,7 @@ const getCorePlugins = ({
8101
8354
  directoryListing = true,
8102
8355
  directoryReferenceEffect,
8103
8356
  supervisor,
8357
+ patches,
8104
8358
  injections,
8105
8359
  transpilation = true,
8106
8360
  inlining = true,
@@ -8142,6 +8396,9 @@ const getCorePlugins = ({
8142
8396
  ...(packageBundle
8143
8397
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
8144
8398
  : []),
8399
+ // before everything else: what the other plugins read must be the
8400
+ // patched file
8401
+ ...jsenvPluginPatches(patches),
8145
8402
  // before reference analysis: an url written by an injection must hold its
8146
8403
  // final value when references are analyzed
8147
8404
  jsenvPluginInjections(injections),
@@ -12278,6 +12535,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
12278
12535
  * @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
12279
12536
  * @param {boolean} [params.modulepreload=false] - Send `Link: <url>; rel=modulepreload` response headers listing the static import graph of a page (as far as the graph knows it). Off by default, to enable once the server runs on http/2 or http/3: over http/1.1 the preloads take the 6 connections per origin ahead of the render-blocking scripts, and pages get slower (the why, with measures, in jsenv_plugin_modulepreload.js).
12280
12537
  * @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
12538
+ * @param {object} [params.patches] - Text patches applied to files as they are served, as `{ file: [{ from, to }] }`. A key is a url pattern relative to sourceDirectoryUrl (`"./main.js"`) or a path inside a package (`"preact/dist/preact.mjs"`, found in node_modules the way node does). Each `from` must occur exactly once in the file, otherwise the file fails to load and says which patch did not apply.
12281
12539
  * @param {object} [params.injections] - Values to inject into files, as `{ urlPattern: getInjections }`. Keys are url patterns relative to sourceDirectoryUrl (`"./index.html"`, `"**\/*.js"`), values are functions receiving `urlInfo` and returning (or resolving to) an object of placeholders to replace, named `__LIKE_THIS__` by convention. In JS the value is injected as a JS literal (a string brings its own quotes), everywhere else as-is so it can be concatenated: `href="__BACKEND_URL__/users/me"`. An html url pattern also covers what is inlined in that html, so `<script>window.backendUrl = __BACKEND_URL__;</script>` shares the value with every js file of the page. See `INJECTIONS.optional` and `INJECTIONS.global`.
12282
12540
  * @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
12283
12541
  * @param {string} [params.sourcemaps="inline"] - Sourcemap mode.
@@ -12339,6 +12597,7 @@ const startDevServer = async ({
12339
12597
  magicExtensions,
12340
12598
  magicDirectoryIndex,
12341
12599
  directoryListing,
12600
+ patches,
12342
12601
  injections,
12343
12602
  transpilation,
12344
12603
  cacheControl = true,
@@ -12444,6 +12703,7 @@ const startDevServer = async ({
12444
12703
  };
12445
12704
  const dependencyProblemEventEmitter = createEventEmitter();
12446
12705
  const dependencyWatcher = watchDependencies(packageDirectory, {
12706
+ getKitchens: () => kitchenCache.values(),
12447
12707
  onChange: (problems) => {
12448
12708
  dependencyProblemEventEmitter.emit(problems);
12449
12709
  },
@@ -12465,11 +12725,30 @@ const startDevServer = async ({
12465
12725
  logger.info(message);
12466
12726
  }
12467
12727
  },
12468
- onInstalled: ({ packageName, declaredVersion, severity }) => {
12469
- logger.info(`"${packageName}@${declaredVersion}" is now installed`);
12728
+ onInstalled: ({
12729
+ packageName,
12730
+ installedVersion,
12731
+ servedVersion,
12732
+ declaredVersion,
12733
+ severity,
12734
+ }) => {
12735
+ if (servedVersion) {
12736
+ logger.info(
12737
+ `"${packageName}@${installedVersion}" is now installed, the page runs ${servedVersion}`,
12738
+ );
12739
+ } else {
12740
+ logger.info(`"${packageName}@${declaredVersion}" is now installed`);
12741
+ }
12470
12742
  if (severity !== "warning") {
12471
12743
  return;
12472
12744
  }
12745
+ if (servedVersion) {
12746
+ reloadRequestEventEmitter.emit({
12747
+ cause: `${packageName} ${servedVersion} -> ${installedVersion} installed`,
12748
+ reason: `a dependency the page runs changed version in node_modules`,
12749
+ });
12750
+ return;
12751
+ }
12473
12752
  reloadRequestEventEmitter.emit({
12474
12753
  cause: `${packageName}@${declaredVersion} installed`,
12475
12754
  reason: `a dependency became available in node_modules`,
@@ -12506,6 +12785,7 @@ const startDevServer = async ({
12506
12785
  magicDirectoryIndex,
12507
12786
  directoryListing,
12508
12787
  supervisor,
12788
+ patches,
12509
12789
  injections,
12510
12790
  transpilation,
12511
12791
  spa,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.5.20",
3
+ "version": "41.5.22",
4
4
  "type": "module",
5
5
  "description": "Tool to develop, test and build js projects",
6
6
  "repository": {
@@ -79,7 +79,7 @@
79
79
  "@jsenv/plugin-minification": "1.7.22",
80
80
  "@jsenv/plugin-supervisor": "1.8.24",
81
81
  "@jsenv/plugin-transpilation": "1.7.9",
82
- "@jsenv/server": "17.6.4",
82
+ "@jsenv/server": "17.6.5",
83
83
  "@jsenv/sourcemap": "1.4.9",
84
84
  "react-table": "7.8.0"
85
85
  },
@@ -149,6 +149,17 @@ import { jsenvPluginMappings } from "./jsenv_plugin_mappings.js";
149
149
  * }
150
150
  *
151
151
  * See startDevServer "ribbon" param for the full list of options.
152
+ * @param {object} [entryPoint.patches]
153
+ * Text patches applied to files as they are built, as { file: [{ from, to }] }.
154
+ * A key is a url pattern relative to sourceDirectoryUrl ("./main.js") or a path
155
+ * inside a package ("preact/dist/preact.mjs", found in node_modules the way node does):
156
+ *
157
+ * patches: {
158
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
159
+ * }
160
+ *
161
+ * Each `from` must occur exactly once in the file, otherwise the build fails
162
+ * and says which patch did not apply.
152
163
  * @param {object} [entryPoint.injections]
153
164
  * Values to inject into files, as { urlPattern: getInjections }.
154
165
  * Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
@@ -1016,6 +1027,7 @@ const entryPointDefaultParams = {
1016
1027
  directoryReferenceEffect: undefined,
1017
1028
  scenarioPlaceholders: undefined,
1018
1029
  ribbon: false,
1030
+ patches: undefined,
1019
1031
  injections: undefined,
1020
1032
  transpilation: {},
1021
1033
  preserveComments: undefined,
@@ -1072,6 +1084,7 @@ const prepareEntryPointBuild = async (
1072
1084
  directoryReferenceEffect,
1073
1085
  scenarioPlaceholders,
1074
1086
  ribbon,
1087
+ patches,
1075
1088
  injections,
1076
1089
  transpilation,
1077
1090
  preserveComments,
@@ -1240,6 +1253,7 @@ const prepareEntryPointBuild = async (
1240
1253
  magicExtensions,
1241
1254
  magicDirectoryIndex,
1242
1255
  directoryReferenceEffect,
1256
+ patches,
1243
1257
  injections,
1244
1258
  transpilation: {
1245
1259
  babelHelpersAsImport: !explicitJsModuleConversion,
@@ -60,6 +60,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
60
60
  * @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
61
61
  * @param {boolean} [params.modulepreload=false] - Send `Link: <url>; rel=modulepreload` response headers listing the static import graph of a page (as far as the graph knows it). Off by default, to enable once the server runs on http/2 or http/3: over http/1.1 the preloads take the 6 connections per origin ahead of the render-blocking scripts, and pages get slower (the why, with measures, in jsenv_plugin_modulepreload.js).
62
62
  * @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
63
+ * @param {object} [params.patches] - Text patches applied to files as they are served, as `{ file: [{ from, to }] }`. A key is a url pattern relative to sourceDirectoryUrl (`"./main.js"`) or a path inside a package (`"preact/dist/preact.mjs"`, found in node_modules the way node does). Each `from` must occur exactly once in the file, otherwise the file fails to load and says which patch did not apply.
63
64
  * @param {object} [params.injections] - Values to inject into files, as `{ urlPattern: getInjections }`. Keys are url patterns relative to sourceDirectoryUrl (`"./index.html"`, `"**\/*.js"`), values are functions receiving `urlInfo` and returning (or resolving to) an object of placeholders to replace, named `__LIKE_THIS__` by convention. In JS the value is injected as a JS literal (a string brings its own quotes), everywhere else as-is so it can be concatenated: `href="__BACKEND_URL__/users/me"`. An html url pattern also covers what is inlined in that html, so `<script>window.backendUrl = __BACKEND_URL__;</script>` shares the value with every js file of the page. See `INJECTIONS.optional` and `INJECTIONS.global`.
64
65
  * @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
65
66
  * @param {string} [params.sourcemaps="inline"] - Sourcemap mode.
@@ -121,6 +122,7 @@ export const startDevServer = async ({
121
122
  magicExtensions,
122
123
  magicDirectoryIndex,
123
124
  directoryListing,
125
+ patches,
124
126
  injections,
125
127
  transpilation,
126
128
  cacheControl = true,
@@ -227,6 +229,7 @@ export const startDevServer = async ({
227
229
  };
228
230
  const dependencyProblemEventEmitter = createEventEmitter();
229
231
  const dependencyWatcher = watchDependencies(packageDirectory, {
232
+ getKitchens: () => kitchenCache.values(),
230
233
  onChange: (problems) => {
231
234
  dependencyProblemEventEmitter.emit(problems);
232
235
  },
@@ -248,11 +251,30 @@ export const startDevServer = async ({
248
251
  logger.info(message);
249
252
  }
250
253
  },
251
- onInstalled: ({ packageName, declaredVersion, severity }) => {
252
- logger.info(`"${packageName}@${declaredVersion}" is now installed`);
254
+ onInstalled: ({
255
+ packageName,
256
+ installedVersion,
257
+ servedVersion,
258
+ declaredVersion,
259
+ severity,
260
+ }) => {
261
+ if (servedVersion) {
262
+ logger.info(
263
+ `"${packageName}@${installedVersion}" is now installed, the page runs ${servedVersion}`,
264
+ );
265
+ } else {
266
+ logger.info(`"${packageName}@${declaredVersion}" is now installed`);
267
+ }
253
268
  if (severity !== "warning") {
254
269
  return;
255
270
  }
271
+ if (servedVersion) {
272
+ reloadRequestEventEmitter.emit({
273
+ cause: `${packageName} ${servedVersion} -> ${installedVersion} installed`,
274
+ reason: `a dependency the page runs changed version in node_modules`,
275
+ });
276
+ return;
277
+ }
256
278
  reloadRequestEventEmitter.emit({
257
279
  cause: `${packageName}@${declaredVersion} installed`,
258
280
  reason: `a dependency became available in node_modules`,
@@ -289,6 +311,7 @@ export const startDevServer = async ({
289
311
  magicDirectoryIndex,
290
312
  directoryListing,
291
313
  supervisor,
314
+ patches,
292
315
  injections,
293
316
  transpilation,
294
317
  spa,