@jsenv/core 41.4.6 → 41.4.8

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.
@@ -6551,7 +6551,13 @@ const jsenvPluginWebResolution = () => {
6551
6551
  if (ownerUrlInfo.originalUrl?.startsWith("http")) {
6552
6552
  return new URL(resource, ownerUrlInfo.originalUrl);
6553
6553
  }
6554
- const url = new URL(resource.slice(1), ownerUrlInfo.entryUrlInfo.url);
6554
+ // "/x" is the web meaning of a root-relative url: the root of what is
6555
+ // served, which is the source directory — not the directory the entry
6556
+ // point happens to sit in.
6557
+ const url = new URL(
6558
+ resource.slice(1),
6559
+ ownerUrlInfo.context.rootDirectoryUrl,
6560
+ );
6555
6561
  return url;
6556
6562
  }
6557
6563
  // baseUrl happens second argument to new URL() is different from
@@ -10364,6 +10370,7 @@ const createBuildSpecifierManager = ({
10364
10370
  buildDirectoryUrl,
10365
10371
  assetsDirectory,
10366
10372
  buildUrlsGenerator,
10373
+ entryKey,
10367
10374
  base,
10368
10375
  length = 8,
10369
10376
 
@@ -10402,6 +10409,7 @@ const createBuildSpecifierManager = ({
10402
10409
  urlInfo,
10403
10410
  ownerUrlInfo: reference.ownerUrlInfo,
10404
10411
  assetsDirectory,
10412
+ entryKey,
10405
10413
  });
10406
10414
  }
10407
10415
 
@@ -11459,9 +11467,14 @@ const createBuildSpecifierManager = ({
11459
11467
  let contentKey;
11460
11468
  // if to guard for html where versioned build specifier is not generated
11461
11469
  if (buildSpecifierVersioned) {
11462
- const buildUrlVersioned = asBuildUrlVersioned({
11463
- buildSpecifierVersioned,
11464
- buildDirectoryUrl,
11470
+ // The version goes on the build url, not on the specifier: a
11471
+ // specifier is written for one importer to read (base: "./" makes
11472
+ // it relative to that importer, base: "https://cdn…" makes it
11473
+ // absolute elsewhere) and says nothing about where the file lands.
11474
+ const buildUrlVersioned = injectVersionIntoBuildSpecifier({
11475
+ buildSpecifier: buildUrl,
11476
+ version: versionMap.get(urlInfo),
11477
+ versioningMethod,
11465
11478
  });
11466
11479
  const buildRelativeUrlVersioned = urlToRelativeUrl(
11467
11480
  buildUrlVersioned,
@@ -11705,23 +11718,6 @@ const injectVersionIntoBuildSpecifier = ({
11705
11718
  );
11706
11719
  };
11707
11720
 
11708
- const asBuildUrlVersioned = ({
11709
- buildSpecifierVersioned,
11710
- buildDirectoryUrl,
11711
- }) => {
11712
- if (buildSpecifierVersioned[0] === "/") {
11713
- return new URL(buildSpecifierVersioned.slice(1), buildDirectoryUrl).href;
11714
- }
11715
- const buildUrl = new URL(buildSpecifierVersioned, buildDirectoryUrl).href;
11716
- if (buildUrl.startsWith(buildDirectoryUrl)) {
11717
- return buildUrl;
11718
- }
11719
- // it's likely "base" parameter was set to an url origin like "https://cdn.example.com"
11720
- // let's move url to build directory
11721
- const { pathname, search, hash } = new URL(buildSpecifierVersioned);
11722
- return `${buildDirectoryUrl}${pathname}${search}${hash}`;
11723
- };
11724
-
11725
11721
  const targetsAFileThatDoesNotExist = (url) => {
11726
11722
  if (!url.startsWith("file:")) {
11727
11723
  return false;
@@ -11759,12 +11755,41 @@ const createBuildUrlsGenerator = ({
11759
11755
  };
11760
11756
 
11761
11757
  const nameSetPerDirectoryMap = new Map();
11762
- const generate = (url, { urlInfo, ownerUrlInfo, assetsDirectory }) => {
11763
- const buildUrlFromMap = buildUrlMap.get(url);
11758
+ const reserveName = (directoryPath, urlName) => {
11759
+ let nameSet = nameSetPerDirectoryMap.get(directoryPath);
11760
+ if (!nameSet) {
11761
+ nameSet = new Set();
11762
+ nameSetPerDirectoryMap.set(directoryPath, nameSet);
11763
+ }
11764
+ let [basename, extension] = splitFileExtension(urlName);
11765
+ extension = extensionMappings[extension] || extension;
11766
+ let nameCandidate = `${basename}${extension}`; // reconstruct name in case extension was normalized
11767
+ let integer = 1;
11768
+ while (nameSet.has(nameCandidate)) {
11769
+ integer++;
11770
+ nameCandidate = `${basename}${integer}${extension}`;
11771
+ }
11772
+ nameSet.add(nameCandidate);
11773
+ return nameCandidate;
11774
+ };
11775
+ const generate = (
11776
+ url,
11777
+ { urlInfo, ownerUrlInfo, assetsDirectory, entryKey },
11778
+ ) => {
11779
+ // A url already inside the build directory names a chunk a bundler just
11780
+ // produced, and each entry point is bundled on its own: two of them
11781
+ // routinely produce a chunk of the same name holding different code (the
11782
+ // shared dependencies of THAT entry point, exported under names decided by
11783
+ // THAT bundle). They are told apart by the entry point they come from —
11784
+ // sharing the file would leave one entry importing exports the file on disk
11785
+ // does not have.
11786
+ const insideBuildDirectory = urlIsOrIsInsideOf(url, buildDirectoryUrl);
11787
+ const key = insideBuildDirectory ? `${entryKey} ${url}` : url;
11788
+ const buildUrlFromMap = buildUrlMap.get(key);
11764
11789
  if (buildUrlFromMap) {
11765
11790
  return buildUrlFromMap;
11766
11791
  }
11767
- if (urlIsOrIsInsideOf(url, buildDirectoryUrl)) {
11792
+ if (insideBuildDirectory) {
11768
11793
  if (ownerUrlInfo.searchParams.has("dynamic_import_id")) {
11769
11794
  const ownerDirectoryPath = determineDirectoryPath({
11770
11795
  sourceDirectoryUrl,
@@ -11776,11 +11801,18 @@ const createBuildUrlsGenerator = ({
11776
11801
  buildUrl = injectQueryParams(buildUrl, {
11777
11802
  dynamic_import_id: undefined,
11778
11803
  });
11779
- associateBuildUrl(url, buildUrl);
11804
+ associateBuildUrl(key, buildUrl);
11780
11805
  return buildUrl;
11781
11806
  }
11782
- associateBuildUrl(url, url);
11783
- return url;
11807
+ const urlObject = new URL(url);
11808
+ const chunkDirectoryPath = urlToRelativeUrl(
11809
+ new URL("./", urlObject),
11810
+ buildDirectoryUrl,
11811
+ );
11812
+ const chunkName = reserveName(chunkDirectoryPath, urlToFilename(url));
11813
+ const buildUrl = `${buildDirectoryUrl}${chunkDirectoryPath}${chunkName}${urlObject.search}${urlObject.hash}`;
11814
+ associateBuildUrl(key, buildUrl);
11815
+ return buildUrl;
11784
11816
  }
11785
11817
  if (urlInfo.type === "entry_build") {
11786
11818
  const buildUrl = new URL(urlInfo.filenameHint, buildDirectoryUrl).href;
@@ -11812,25 +11844,10 @@ const createBuildUrlsGenerator = ({
11812
11844
  urlInfo,
11813
11845
  ownerUrlInfo,
11814
11846
  });
11815
- let nameSet = nameSetPerDirectoryMap.get(directoryPath);
11816
- if (!nameSet) {
11817
- nameSet = new Set();
11818
- nameSetPerDirectoryMap.set(directoryPath, nameSet);
11819
- }
11820
11847
  const urlObject = new URL(url);
11821
11848
  injectQueryParams(urlObject, { dynamic_import_id: undefined });
11822
11849
  let { search, hash } = urlObject;
11823
- let urlName = getUrlName(url, urlInfo);
11824
- let [basename, extension] = splitFileExtension(urlName);
11825
- extension = extensionMappings[extension] || extension;
11826
- let nameCandidate = `${basename}${extension}`; // reconstruct name in case extension was normalized
11827
- let integer = 1;
11828
- while (nameSet.has(nameCandidate)) {
11829
- integer++;
11830
- nameCandidate = `${basename}${integer}${extension}`;
11831
- }
11832
- const name = nameCandidate;
11833
- nameSet.add(name);
11850
+ const name = reserveName(directoryPath, getUrlName(url, urlInfo));
11834
11851
  const buildUrl = `${buildDirectoryUrl}${directoryPath}${name}${search}${hash}`;
11835
11852
  associateBuildUrl(url, buildUrl);
11836
11853
  return buildUrl;
@@ -13217,6 +13234,7 @@ const prepareEntryPointBuild = async (
13217
13234
  base,
13218
13235
  assetsDirectory,
13219
13236
  buildUrlsGenerator,
13237
+ entryKey: sourceRelativeUrl,
13220
13238
 
13221
13239
  versioning,
13222
13240
  versioningMethod,
@@ -768,7 +768,9 @@ const parseUserAgentHeader = (userAgent) => {
768
768
  * - "clients_list" the whole registry (the dashboard renders it)
769
769
  * - "client_log" a single log line (a monitor appends it)
770
770
  * - "client_activity" a single qualified activity (a monitor appends it)
771
- * - "client_here" a client just appeared/resumed (every page can toast it)
771
+ * - "client_here" a networked client just appeared/resumed (every page
772
+ * can toast it); never sent for the main client, which
773
+ * is the person reading the toast
772
774
  * - "client_command" pilot one client (navigate/reload its tab) from the
773
775
  * dashboard; the matching reporter runs it
774
776
  * Server events are broadcast, so consumers filter what they care about.
@@ -783,7 +785,7 @@ const parseUserAgentHeader = (userAgent) => {
783
785
  *
784
786
  * The monitoring script is injected into EVERY cooked page, including our own
785
787
  * dashboard and monitor pages — so opening one of those counts as a connected
786
- * client, and any open page gets toasted when another client appears.
788
+ * client, and any open page gets toasted when another networked client appears.
787
789
  *
788
790
  * Pages:
789
791
  * - /.internal/clients → dashboard listing every client seen
@@ -1114,12 +1116,25 @@ const jsenvPluginClientMonitoring = () => {
1114
1116
  pruneTabs(client);
1115
1117
  pruneClients();
1116
1118
 
1117
- if (firstEver) {
1118
- sendClientHere({ reason: "new", client: serializeClient(client) });
1119
- sendClientsList();
1120
- } else if (!wasOnline) {
1121
- // A report after a long quiet spell means the client was picked back up.
1122
- sendClientHere({ reason: "resumed", client: serializeClient(client) });
1119
+ if (firstEver || !wasOnline) {
1120
+ // The toast invites the reader to monitor the client that just appeared,
1121
+ // so it is only worth showing for a client there is something to watch:
1122
+ // one reaching the server over the network. The machine running the dev
1123
+ // server is the person reading the toast it browses over localhost, its
1124
+ // devtools are one keystroke away and nothing is captured for it, so
1125
+ // announcing it means interrupting someone about themselves. It happens
1126
+ // more than one would think: the client id lives in localStorage, so the
1127
+ // same browser gets a fresh id per origin (localhost vs 127.0.0.1 vs the
1128
+ // LAN ip, another port) and in private windows, and each of those looks
1129
+ // like a brand new client to the server.
1130
+ if (!isLocalIp(client.ip)) {
1131
+ sendClientHere({
1132
+ // A report after a long quiet spell means the client was picked back
1133
+ // up rather than newly seen.
1134
+ reason: firstEver ? "new" : "resumed",
1135
+ client: serializeClient(client),
1136
+ });
1137
+ }
1123
1138
  sendClientsList();
1124
1139
  }
1125
1140
 
@@ -3228,7 +3243,13 @@ const jsenvPluginWebResolution = () => {
3228
3243
  if (ownerUrlInfo.originalUrl?.startsWith("http")) {
3229
3244
  return new URL(resource, ownerUrlInfo.originalUrl);
3230
3245
  }
3231
- const url = new URL(resource.slice(1), ownerUrlInfo.entryUrlInfo.url);
3246
+ // "/x" is the web meaning of a root-relative url: the root of what is
3247
+ // served, which is the source directory — not the directory the entry
3248
+ // point happens to sit in.
3249
+ const url = new URL(
3250
+ resource.slice(1),
3251
+ ownerUrlInfo.context.rootDirectoryUrl,
3252
+ );
3232
3253
  return url;
3233
3254
  }
3234
3255
  // baseUrl happens second argument to new URL() is different from
@@ -11154,7 +11175,16 @@ const devServerPluginServeSourceFiles = ({
11154
11175
  }
11155
11176
  }
11156
11177
  });
11157
- const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
11178
+ // A client we cannot identify (curl, fetch, a healthcheck, a proxy dropping
11179
+ // the user agent) is assumed to be one of the runtimes the project targets.
11180
+ // Treating it as a runtime supporting nothing would serve it the js module
11181
+ // fallback, breaking tooling perfectly capable of ES modules; an actual
11182
+ // ancient browser hiding its identity fails loudly instead, which is the
11183
+ // cheaper of the two mistakes.
11184
+ const clientRuntimeCompat =
11185
+ runtimeName === "unknown"
11186
+ ? runtimeCompat
11187
+ : { [runtimeName]: runtimeVersion };
11158
11188
 
11159
11189
  kitchen = createKitchen({
11160
11190
  name: runtimeId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.4.6",
3
+ "version": "41.4.8",
4
4
  "type": "module",
5
5
  "description": "Tool to develop, test and build js projects",
6
6
  "repository": {
@@ -1249,6 +1249,7 @@ const prepareEntryPointBuild = async (
1249
1249
  base,
1250
1250
  assetsDirectory,
1251
1251
  buildUrlsGenerator,
1252
+ entryKey: sourceRelativeUrl,
1252
1253
 
1253
1254
  versioning,
1254
1255
  versioningMethod,
@@ -39,6 +39,7 @@ export const createBuildSpecifierManager = ({
39
39
  buildDirectoryUrl,
40
40
  assetsDirectory,
41
41
  buildUrlsGenerator,
42
+ entryKey,
42
43
  base,
43
44
  length = 8,
44
45
 
@@ -77,6 +78,7 @@ export const createBuildSpecifierManager = ({
77
78
  urlInfo,
78
79
  ownerUrlInfo: reference.ownerUrlInfo,
79
80
  assetsDirectory,
81
+ entryKey,
80
82
  });
81
83
  }
82
84
 
@@ -1134,9 +1136,14 @@ export const createBuildSpecifierManager = ({
1134
1136
  let contentKey;
1135
1137
  // if to guard for html where versioned build specifier is not generated
1136
1138
  if (buildSpecifierVersioned) {
1137
- const buildUrlVersioned = asBuildUrlVersioned({
1138
- buildSpecifierVersioned,
1139
- buildDirectoryUrl,
1139
+ // The version goes on the build url, not on the specifier: a
1140
+ // specifier is written for one importer to read (base: "./" makes
1141
+ // it relative to that importer, base: "https://cdn…" makes it
1142
+ // absolute elsewhere) and says nothing about where the file lands.
1143
+ const buildUrlVersioned = injectVersionIntoBuildSpecifier({
1144
+ buildSpecifier: buildUrl,
1145
+ version: versionMap.get(urlInfo),
1146
+ versioningMethod,
1140
1147
  });
1141
1148
  const buildRelativeUrlVersioned = urlToRelativeUrl(
1142
1149
  buildUrlVersioned,
@@ -1380,23 +1387,6 @@ const injectVersionIntoBuildSpecifier = ({
1380
1387
  );
1381
1388
  };
1382
1389
 
1383
- const asBuildUrlVersioned = ({
1384
- buildSpecifierVersioned,
1385
- buildDirectoryUrl,
1386
- }) => {
1387
- if (buildSpecifierVersioned[0] === "/") {
1388
- return new URL(buildSpecifierVersioned.slice(1), buildDirectoryUrl).href;
1389
- }
1390
- const buildUrl = new URL(buildSpecifierVersioned, buildDirectoryUrl).href;
1391
- if (buildUrl.startsWith(buildDirectoryUrl)) {
1392
- return buildUrl;
1393
- }
1394
- // it's likely "base" parameter was set to an url origin like "https://cdn.example.com"
1395
- // let's move url to build directory
1396
- const { pathname, search, hash } = new URL(buildSpecifierVersioned);
1397
- return `${buildDirectoryUrl}${pathname}${search}${hash}`;
1398
- };
1399
-
1400
1390
  const targetsAFileThatDoesNotExist = (url) => {
1401
1391
  if (!url.startsWith("file:")) {
1402
1392
  return false;
@@ -31,12 +31,41 @@ export const createBuildUrlsGenerator = ({
31
31
  };
32
32
 
33
33
  const nameSetPerDirectoryMap = new Map();
34
- const generate = (url, { urlInfo, ownerUrlInfo, assetsDirectory }) => {
35
- const buildUrlFromMap = buildUrlMap.get(url);
34
+ const reserveName = (directoryPath, urlName) => {
35
+ let nameSet = nameSetPerDirectoryMap.get(directoryPath);
36
+ if (!nameSet) {
37
+ nameSet = new Set();
38
+ nameSetPerDirectoryMap.set(directoryPath, nameSet);
39
+ }
40
+ let [basename, extension] = splitFileExtension(urlName);
41
+ extension = extensionMappings[extension] || extension;
42
+ let nameCandidate = `${basename}${extension}`; // reconstruct name in case extension was normalized
43
+ let integer = 1;
44
+ while (nameSet.has(nameCandidate)) {
45
+ integer++;
46
+ nameCandidate = `${basename}${integer}${extension}`;
47
+ }
48
+ nameSet.add(nameCandidate);
49
+ return nameCandidate;
50
+ };
51
+ const generate = (
52
+ url,
53
+ { urlInfo, ownerUrlInfo, assetsDirectory, entryKey },
54
+ ) => {
55
+ // A url already inside the build directory names a chunk a bundler just
56
+ // produced, and each entry point is bundled on its own: two of them
57
+ // routinely produce a chunk of the same name holding different code (the
58
+ // shared dependencies of THAT entry point, exported under names decided by
59
+ // THAT bundle). They are told apart by the entry point they come from —
60
+ // sharing the file would leave one entry importing exports the file on disk
61
+ // does not have.
62
+ const insideBuildDirectory = urlIsOrIsInsideOf(url, buildDirectoryUrl);
63
+ const key = insideBuildDirectory ? `${entryKey} ${url}` : url;
64
+ const buildUrlFromMap = buildUrlMap.get(key);
36
65
  if (buildUrlFromMap) {
37
66
  return buildUrlFromMap;
38
67
  }
39
- if (urlIsOrIsInsideOf(url, buildDirectoryUrl)) {
68
+ if (insideBuildDirectory) {
40
69
  if (ownerUrlInfo.searchParams.has("dynamic_import_id")) {
41
70
  const ownerDirectoryPath = determineDirectoryPath({
42
71
  sourceDirectoryUrl,
@@ -48,11 +77,18 @@ export const createBuildUrlsGenerator = ({
48
77
  buildUrl = injectQueryParams(buildUrl, {
49
78
  dynamic_import_id: undefined,
50
79
  });
51
- associateBuildUrl(url, buildUrl);
80
+ associateBuildUrl(key, buildUrl);
52
81
  return buildUrl;
53
82
  }
54
- associateBuildUrl(url, url);
55
- return url;
83
+ const urlObject = new URL(url);
84
+ const chunkDirectoryPath = urlToRelativeUrl(
85
+ new URL("./", urlObject),
86
+ buildDirectoryUrl,
87
+ );
88
+ const chunkName = reserveName(chunkDirectoryPath, urlToFilename(url));
89
+ const buildUrl = `${buildDirectoryUrl}${chunkDirectoryPath}${chunkName}${urlObject.search}${urlObject.hash}`;
90
+ associateBuildUrl(key, buildUrl);
91
+ return buildUrl;
56
92
  }
57
93
  if (urlInfo.type === "entry_build") {
58
94
  const buildUrl = new URL(urlInfo.filenameHint, buildDirectoryUrl).href;
@@ -84,25 +120,10 @@ export const createBuildUrlsGenerator = ({
84
120
  urlInfo,
85
121
  ownerUrlInfo,
86
122
  });
87
- let nameSet = nameSetPerDirectoryMap.get(directoryPath);
88
- if (!nameSet) {
89
- nameSet = new Set();
90
- nameSetPerDirectoryMap.set(directoryPath, nameSet);
91
- }
92
123
  const urlObject = new URL(url);
93
124
  injectQueryParams(urlObject, { dynamic_import_id: undefined });
94
125
  let { search, hash } = urlObject;
95
- let urlName = getUrlName(url, urlInfo);
96
- let [basename, extension] = splitFileExtension(urlName);
97
- extension = extensionMappings[extension] || extension;
98
- let nameCandidate = `${basename}${extension}`; // reconstruct name in case extension was normalized
99
- let integer = 1;
100
- while (nameSet.has(nameCandidate)) {
101
- integer++;
102
- nameCandidate = `${basename}${integer}${extension}`;
103
- }
104
- const name = nameCandidate;
105
- nameSet.add(name);
126
+ const name = reserveName(directoryPath, getUrlName(url, urlInfo));
106
127
  const buildUrl = `${buildDirectoryUrl}${directoryPath}${name}${search}${hash}`;
107
128
  associateBuildUrl(url, buildUrl);
108
129
  return buildUrl;
@@ -71,7 +71,16 @@ export const devServerPluginServeSourceFiles = ({
71
71
  }
72
72
  }
73
73
  });
74
- const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
74
+ // A client we cannot identify (curl, fetch, a healthcheck, a proxy dropping
75
+ // the user agent) is assumed to be one of the runtimes the project targets.
76
+ // Treating it as a runtime supporting nothing would serve it the js module
77
+ // fallback, breaking tooling perfectly capable of ES modules; an actual
78
+ // ancient browser hiding its identity fails loudly instead, which is the
79
+ // cheaper of the two mistakes.
80
+ const clientRuntimeCompat =
81
+ runtimeName === "unknown"
82
+ ? runtimeCompat
83
+ : { [runtimeName]: runtimeVersion };
75
84
 
76
85
  kitchen = createKitchen({
77
86
  name: runtimeId,
@@ -22,7 +22,9 @@
22
22
  * - "clients_list" the whole registry (the dashboard renders it)
23
23
  * - "client_log" a single log line (a monitor appends it)
24
24
  * - "client_activity" a single qualified activity (a monitor appends it)
25
- * - "client_here" a client just appeared/resumed (every page can toast it)
25
+ * - "client_here" a networked client just appeared/resumed (every page
26
+ * can toast it); never sent for the main client, which
27
+ * is the person reading the toast
26
28
  * - "client_command" pilot one client (navigate/reload its tab) from the
27
29
  * dashboard; the matching reporter runs it
28
30
  * Server events are broadcast, so consumers filter what they care about.
@@ -37,7 +39,7 @@
37
39
  *
38
40
  * The monitoring script is injected into EVERY cooked page, including our own
39
41
  * dashboard and monitor pages — so opening one of those counts as a connected
40
- * client, and any open page gets toasted when another client appears.
42
+ * client, and any open page gets toasted when another networked client appears.
41
43
  *
42
44
  * Pages:
43
45
  * - /.internal/clients → dashboard listing every client seen
@@ -370,12 +372,25 @@ export const jsenvPluginClientMonitoring = () => {
370
372
  pruneTabs(client);
371
373
  pruneClients();
372
374
 
373
- if (firstEver) {
374
- sendClientHere({ reason: "new", client: serializeClient(client) });
375
- sendClientsList();
376
- } else if (!wasOnline) {
377
- // A report after a long quiet spell means the client was picked back up.
378
- sendClientHere({ reason: "resumed", client: serializeClient(client) });
375
+ if (firstEver || !wasOnline) {
376
+ // The toast invites the reader to monitor the client that just appeared,
377
+ // so it is only worth showing for a client there is something to watch:
378
+ // one reaching the server over the network. The machine running the dev
379
+ // server is the person reading the toast it browses over localhost, its
380
+ // devtools are one keystroke away and nothing is captured for it, so
381
+ // announcing it means interrupting someone about themselves. It happens
382
+ // more than one would think: the client id lives in localStorage, so the
383
+ // same browser gets a fresh id per origin (localhost vs 127.0.0.1 vs the
384
+ // LAN ip, another port) and in private windows, and each of those looks
385
+ // like a brand new client to the server.
386
+ if (!isLocalIp(client.ip)) {
387
+ sendClientHere({
388
+ // A report after a long quiet spell means the client was picked back
389
+ // up rather than newly seen.
390
+ reason: firstEver ? "new" : "resumed",
391
+ client: serializeClient(client),
392
+ });
393
+ }
379
394
  sendClientsList();
380
395
  }
381
396
 
@@ -9,7 +9,13 @@ export const jsenvPluginWebResolution = () => {
9
9
  if (ownerUrlInfo.originalUrl?.startsWith("http")) {
10
10
  return new URL(resource, ownerUrlInfo.originalUrl);
11
11
  }
12
- const url = new URL(resource.slice(1), ownerUrlInfo.entryUrlInfo.url);
12
+ // "/x" is the web meaning of a root-relative url: the root of what is
13
+ // served, which is the source directory — not the directory the entry
14
+ // point happens to sit in.
15
+ const url = new URL(
16
+ resource.slice(1),
17
+ ownerUrlInfo.context.rootDirectoryUrl,
18
+ );
13
19
  return url;
14
20
  }
15
21
  // baseUrl happens second argument to new URL() is different from