@jsenv/core 41.5.13 → 41.5.15

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.
@@ -2131,6 +2131,10 @@ const createUrlInfo = (url, context) => {
2131
2131
  modifiedTimestamp: 0,
2132
2132
  descendantModifiedTimestamp: 0,
2133
2133
  dereferencedTimestamp: 0,
2134
+ // when a client last fetched this url outside a hot request; in other
2135
+ // words the last time this url entered a fresh page (see
2136
+ // jsenv_plugin_hot_search_param)
2137
+ servedWithoutHotTimestamp: 0,
2134
2138
  originalContentEtag: null,
2135
2139
  contentEtag: null,
2136
2140
  isValid: () => false,
@@ -7631,7 +7635,12 @@ const getDirectoryContentItems = ({
7631
7635
  fileUrls.push(fileUrlObject);
7632
7636
  }
7633
7637
  }
7634
- fileUrls.sort(compareFileUrls);
7638
+ // Not the default numeric order: what is drawn here is a directory as a human
7639
+ // reads it, so a leading number is part of the name ("10_a" right after
7640
+ // "1_a") the way the filesystem and the editor next to this browser show it.
7641
+ fileUrls.sort((leftUrl, rightUrl) =>
7642
+ compareFileUrls(leftUrl, rightUrl, { numeric: false }),
7643
+ );
7635
7644
 
7636
7645
  const items = [];
7637
7646
  for (const fileUrl of fileUrls) {
@@ -9440,6 +9449,7 @@ const jsenvPluginHotSearchParam = () => {
9440
9449
  modifiedTimestamp,
9441
9450
  descendantModifiedTimestamp,
9442
9451
  dereferencedTimestamp,
9452
+ servedWithoutHotTimestamp,
9443
9453
  } = referencedUrlInfo;
9444
9454
  if (
9445
9455
  !modifiedTimestamp &&
@@ -9463,6 +9473,18 @@ const jsenvPluginHotSearchParam = () => {
9463
9473
  descendantModifiedTimestamp,
9464
9474
  dereferencedTimestamp,
9465
9475
  );
9476
+ // These timestamps say "this url changed at some point", not "the client
9477
+ // is running an outdated version of it": they are never cleared, so a
9478
+ // file modified once keeps them for the rest of the dev server's life.
9479
+ // A client that fetched this url after that modification (a page load:
9480
+ // see rememberServedWithoutHot in the dev server) already runs the
9481
+ // latest content. Sending it "?hot" then makes the browser evaluate a
9482
+ // second, identical copy of a module it already has — a second module
9483
+ // scope for a file that never changed, which breaks everything a module
9484
+ // holds once (registries, contexts, singletons).
9485
+ if (latestTimestamp <= servedWithoutHotTimestamp) {
9486
+ return null;
9487
+ }
9466
9488
  return {
9467
9489
  hot: latestTimestamp,
9468
9490
  };
@@ -2620,7 +2620,25 @@ const assertAndNormalizeFileUrl = (
2620
2620
  return value;
2621
2621
  };
2622
2622
 
2623
- const comparePathnames = (leftPathame, rightPathname) => {
2623
+ /*
2624
+ * Which of two pathnames comes first: directories before what is under them,
2625
+ * deeper before shallower, then the names themselves.
2626
+ *
2627
+ * How a leading number reads is the caller's call, because the two readings are
2628
+ * both right somewhere:
2629
+ * - numeric (the default), where the number is a quantity: "9_x" then "10_x" —
2630
+ * what a generated list wants, so a story numbered by hand stays in the order
2631
+ * it happens;
2632
+ * - { numeric: false }, where the number is part of the name: "10_x" right
2633
+ * after "1_x" — the order the filesystem itself gives, and the one every file
2634
+ * explorer above it shows, so a human reading a directory finds it in the
2635
+ * order their editor already shows.
2636
+ */
2637
+ const comparePathnames = (
2638
+ leftPathame,
2639
+ rightPathname,
2640
+ { numeric = true } = {},
2641
+ ) => {
2624
2642
  const leftPartArray = leftPathame.split("/");
2625
2643
  const rightPartArray = rightPathname.split("/");
2626
2644
 
@@ -2656,7 +2674,7 @@ const comparePathnames = (leftPathame, rightPathname) => {
2656
2674
  i++;
2657
2675
  // local comparison comes first
2658
2676
  const comparison = leftPart.localeCompare(rightPart, undefined, {
2659
- numeric: true,
2677
+ numeric,
2660
2678
  sensitivity: "base",
2661
2679
  });
2662
2680
  if (comparison !== 0) {
@@ -2673,8 +2691,8 @@ const comparePathnames = (leftPathame, rightPathname) => {
2673
2691
  return 0;
2674
2692
  };
2675
2693
 
2676
- const compareFileUrls = (a, b) => {
2677
- return comparePathnames(new URL(a).pathname, new URL(b).pathname);
2694
+ const compareFileUrls = (a, b, options) => {
2695
+ return comparePathnames(new URL(a).pathname, new URL(b).pathname, options);
2678
2696
  };
2679
2697
 
2680
2698
  const isWindows$3 = process.platform === "win32";
@@ -1921,7 +1921,25 @@ const assertAndNormalizeFileUrl = (
1921
1921
  return value;
1922
1922
  };
1923
1923
 
1924
- const comparePathnames = (leftPathame, rightPathname) => {
1924
+ /*
1925
+ * Which of two pathnames comes first: directories before what is under them,
1926
+ * deeper before shallower, then the names themselves.
1927
+ *
1928
+ * How a leading number reads is the caller's call, because the two readings are
1929
+ * both right somewhere:
1930
+ * - numeric (the default), where the number is a quantity: "9_x" then "10_x" —
1931
+ * what a generated list wants, so a story numbered by hand stays in the order
1932
+ * it happens;
1933
+ * - { numeric: false }, where the number is part of the name: "10_x" right
1934
+ * after "1_x" — the order the filesystem itself gives, and the one every file
1935
+ * explorer above it shows, so a human reading a directory finds it in the
1936
+ * order their editor already shows.
1937
+ */
1938
+ const comparePathnames = (
1939
+ leftPathame,
1940
+ rightPathname,
1941
+ { numeric = true } = {},
1942
+ ) => {
1925
1943
  const leftPartArray = leftPathame.split("/");
1926
1944
  const rightPartArray = rightPathname.split("/");
1927
1945
 
@@ -1957,7 +1975,7 @@ const comparePathnames = (leftPathame, rightPathname) => {
1957
1975
  i++;
1958
1976
  // local comparison comes first
1959
1977
  const comparison = leftPart.localeCompare(rightPart, undefined, {
1960
- numeric: true,
1978
+ numeric,
1961
1979
  sensitivity: "base",
1962
1980
  });
1963
1981
  if (comparison !== 0) {
@@ -1974,8 +1992,8 @@ const comparePathnames = (leftPathame, rightPathname) => {
1974
1992
  return 0;
1975
1993
  };
1976
1994
 
1977
- const compareFileUrls = (a, b) => {
1978
- return comparePathnames(new URL(a).pathname, new URL(b).pathname);
1995
+ const compareFileUrls = (a, b, options) => {
1996
+ return comparePathnames(new URL(a).pathname, new URL(b).pathname, options);
1979
1997
  };
1980
1998
 
1981
1999
  const isWindows$3 = process.platform === "win32";
@@ -4363,7 +4363,12 @@ const getDirectoryContentItems = ({
4363
4363
  fileUrls.push(fileUrlObject);
4364
4364
  }
4365
4365
  }
4366
- fileUrls.sort(compareFileUrls);
4366
+ // Not the default numeric order: what is drawn here is a directory as a human
4367
+ // reads it, so a leading number is part of the name ("10_a" right after
4368
+ // "1_a") the way the filesystem and the editor next to this browser show it.
4369
+ fileUrls.sort((leftUrl, rightUrl) =>
4370
+ compareFileUrls(leftUrl, rightUrl, { numeric: false }),
4371
+ );
4367
4372
 
4368
4373
  const items = [];
4369
4374
  for (const fileUrl of fileUrls) {
@@ -7165,6 +7170,7 @@ const jsenvPluginHotSearchParam = () => {
7165
7170
  modifiedTimestamp,
7166
7171
  descendantModifiedTimestamp,
7167
7172
  dereferencedTimestamp,
7173
+ servedWithoutHotTimestamp,
7168
7174
  } = referencedUrlInfo;
7169
7175
  if (
7170
7176
  !modifiedTimestamp &&
@@ -7188,6 +7194,18 @@ const jsenvPluginHotSearchParam = () => {
7188
7194
  descendantModifiedTimestamp,
7189
7195
  dereferencedTimestamp,
7190
7196
  );
7197
+ // These timestamps say "this url changed at some point", not "the client
7198
+ // is running an outdated version of it": they are never cleared, so a
7199
+ // file modified once keeps them for the rest of the dev server's life.
7200
+ // A client that fetched this url after that modification (a page load:
7201
+ // see rememberServedWithoutHot in the dev server) already runs the
7202
+ // latest content. Sending it "?hot" then makes the browser evaluate a
7203
+ // second, identical copy of a module it already has — a second module
7204
+ // scope for a file that never changed, which breaks everything a module
7205
+ // holds once (registries, contexts, singletons).
7206
+ if (latestTimestamp <= servedWithoutHotTimestamp) {
7207
+ return null;
7208
+ }
7191
7209
  return {
7192
7210
  hot: latestTimestamp,
7193
7211
  };
@@ -9670,6 +9688,10 @@ const createUrlInfo = (url, context) => {
9670
9688
  modifiedTimestamp: 0,
9671
9689
  descendantModifiedTimestamp: 0,
9672
9690
  dereferencedTimestamp: 0,
9691
+ // when a client last fetched this url outside a hot request; in other
9692
+ // words the last time this url entered a fresh page (see
9693
+ // jsenv_plugin_hot_search_param)
9694
+ servedWithoutHotTimestamp: 0,
9673
9695
  originalContentEtag: null,
9674
9696
  contentEtag: null,
9675
9697
  isValid: () => false,
@@ -11917,7 +11939,20 @@ const devServerPluginServeSourceFiles = ({
11917
11939
  );
11918
11940
  return response;
11919
11941
  };
11942
+ // What the client holds for this url is what we last sent it: a
11943
+ // request without "?hot" is a page loading this url into an empty
11944
+ // module registry, so from here on the client runs this exact
11945
+ // content. Remembering when that happened is what allows
11946
+ // jsenv_plugin_hot_search_param to tell a modification the client
11947
+ // has already received from one it must re-execute to see.
11948
+ const rememberServedWithoutHot = () => {
11949
+ if (request.searchParams.has("hot")) {
11950
+ return;
11951
+ }
11952
+ urlInfo.servedWithoutHotTimestamp = Date.now();
11953
+ };
11920
11954
  const respondWithNotModified = () => {
11955
+ rememberServedWithoutHot();
11921
11956
  const headers = {
11922
11957
  "cache-control": `private,max-age=0,must-revalidate`,
11923
11958
  };
@@ -12002,6 +12037,9 @@ const devServerPluginServeSourceFiles = ({
12002
12037
  ) {
12003
12038
  return respondWithNotModified();
12004
12039
  }
12040
+ if (urlInfo.status === 200) {
12041
+ rememberServedWithoutHot();
12042
+ }
12005
12043
  response = {
12006
12044
  url: reference.url,
12007
12045
  // a plugin can cook a complete response body for an url that is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.5.13",
3
+ "version": "41.5.15",
4
4
  "type": "module",
5
5
  "description": "Tool to develop, test and build js projects",
6
6
  "repository": {
@@ -75,7 +75,7 @@
75
75
  "dependencies": {
76
76
  "@jsenv/ast": "6.10.3",
77
77
  "@jsenv/js-module-fallback": "1.6.7",
78
- "@jsenv/plugin-bundling": "2.10.26",
78
+ "@jsenv/plugin-bundling": "2.10.27",
79
79
  "@jsenv/plugin-minification": "1.7.20",
80
80
  "@jsenv/plugin-supervisor": "1.8.22",
81
81
  "@jsenv/plugin-transpilation": "1.7.7",
@@ -387,7 +387,20 @@ export const devServerPluginServeSourceFiles = ({
387
387
  );
388
388
  return response;
389
389
  };
390
+ // What the client holds for this url is what we last sent it: a
391
+ // request without "?hot" is a page loading this url into an empty
392
+ // module registry, so from here on the client runs this exact
393
+ // content. Remembering when that happened is what allows
394
+ // jsenv_plugin_hot_search_param to tell a modification the client
395
+ // has already received from one it must re-execute to see.
396
+ const rememberServedWithoutHot = () => {
397
+ if (request.searchParams.has("hot")) {
398
+ return;
399
+ }
400
+ urlInfo.servedWithoutHotTimestamp = Date.now();
401
+ };
390
402
  const respondWithNotModified = () => {
403
+ rememberServedWithoutHot();
391
404
  const headers = {
392
405
  "cache-control": `private,max-age=0,must-revalidate`,
393
406
  };
@@ -472,6 +485,9 @@ export const devServerPluginServeSourceFiles = ({
472
485
  ) {
473
486
  return respondWithNotModified();
474
487
  }
488
+ if (urlInfo.status === 200) {
489
+ rememberServedWithoutHot();
490
+ }
475
491
  response = {
476
492
  url: reference.url,
477
493
  // a plugin can cook a complete response body for an url that is
@@ -183,6 +183,10 @@ const createUrlInfo = (url, context) => {
183
183
  modifiedTimestamp: 0,
184
184
  descendantModifiedTimestamp: 0,
185
185
  dereferencedTimestamp: 0,
186
+ // when a client last fetched this url outside a hot request; in other
187
+ // words the last time this url entered a fresh page (see
188
+ // jsenv_plugin_hot_search_param)
189
+ servedWithoutHotTimestamp: 0,
186
190
  originalContentEtag: null,
187
191
  contentEtag: null,
188
192
  isValid: () => false,
@@ -48,6 +48,7 @@ export const jsenvPluginHotSearchParam = () => {
48
48
  modifiedTimestamp,
49
49
  descendantModifiedTimestamp,
50
50
  dereferencedTimestamp,
51
+ servedWithoutHotTimestamp,
51
52
  } = referencedUrlInfo;
52
53
  if (
53
54
  !modifiedTimestamp &&
@@ -71,6 +72,18 @@ export const jsenvPluginHotSearchParam = () => {
71
72
  descendantModifiedTimestamp,
72
73
  dereferencedTimestamp,
73
74
  );
75
+ // These timestamps say "this url changed at some point", not "the client
76
+ // is running an outdated version of it": they are never cleared, so a
77
+ // file modified once keeps them for the rest of the dev server's life.
78
+ // A client that fetched this url after that modification (a page load:
79
+ // see rememberServedWithoutHot in the dev server) already runs the
80
+ // latest content. Sending it "?hot" then makes the browser evaluate a
81
+ // second, identical copy of a module it already has — a second module
82
+ // scope for a file that never changed, which breaks everything a module
83
+ // holds once (registries, contexts, singletons).
84
+ if (latestTimestamp <= servedWithoutHotTimestamp) {
85
+ return null;
86
+ }
74
87
  return {
75
88
  hot: latestTimestamp,
76
89
  };
@@ -456,7 +456,12 @@ const getDirectoryContentItems = ({
456
456
  fileUrls.push(fileUrlObject);
457
457
  }
458
458
  }
459
- fileUrls.sort(compareFileUrls);
459
+ // Not the default numeric order: what is drawn here is a directory as a human
460
+ // reads it, so a leading number is part of the name ("10_a" right after
461
+ // "1_a") the way the filesystem and the editor next to this browser show it.
462
+ fileUrls.sort((leftUrl, rightUrl) =>
463
+ compareFileUrls(leftUrl, rightUrl, { numeric: false }),
464
+ );
460
465
 
461
466
  const items = [];
462
467
  for (const fileUrl of fileUrls) {