@jsenv/core 38.2.10 → 38.2.11

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/README.md CHANGED
@@ -40,6 +40,7 @@ It will prompt to choose one of the available demo:
40
40
  ```console
41
41
  ? Select a demo: › - Use arrow-keys. Return to submit.
42
42
  ❯ web
43
+ web-components
43
44
  web-react
44
45
  web-preact
45
46
  node-package
@@ -7355,7 +7355,11 @@ const serveDirectory = (
7355
7355
  const directoryContentArray = readdirSync(new URL(url));
7356
7356
  const responseProducers = {
7357
7357
  "application/json": () => {
7358
- const directoryContentJson = JSON.stringify(directoryContentArray);
7358
+ const directoryContentJson = JSON.stringify(
7359
+ directoryContentArray,
7360
+ null,
7361
+ " ",
7362
+ );
7359
7363
  return {
7360
7364
  status: 200,
7361
7365
  headers: {
@@ -9343,6 +9347,7 @@ const babelPluginCompatMap = {
9343
9347
  samsung: "9",
9344
9348
  electron: "3",
9345
9349
  },
9350
+ "proposal-decorators": {},
9346
9351
  "transform-parameters": {
9347
9352
  chrome: "49",
9348
9353
  opera: "36",
@@ -9739,6 +9744,14 @@ const getBaseBabelPluginStructure = ({
9739
9744
  babelPluginStructure["proposal-unicode-property-regex"] =
9740
9745
  requireBabelPlugin("@babel/plugin-proposal-unicode-property-regex");
9741
9746
  }
9747
+ // if (isBabelPluginNeeded("proposal-decorators") && content.includes("@")) {
9748
+ // babelPluginStructure["proposal-decorators"] = [
9749
+ // requireBabelPlugin("@babel/plugin-proposal-decorators"),
9750
+ // {
9751
+ // version: "2023-05",
9752
+ // },
9753
+ // ];
9754
+ // }
9742
9755
  if (isBabelPluginNeeded("transform-async-to-promises")) {
9743
9756
  babelPluginStructure["transform-async-to-promises"] = [
9744
9757
  requireBabelPlugin("babel-plugin-transform-async-to-promises"),
@@ -12311,6 +12324,7 @@ const createUrlInfo = (url, context) => {
12311
12324
  context,
12312
12325
  error: null,
12313
12326
  modifiedTimestamp: 0,
12327
+ descendantModifiedTimestamp: 0,
12314
12328
  dereferencedTimestamp: 0,
12315
12329
  originalContentEtag: null,
12316
12330
  contentEtag: null,
@@ -14378,7 +14392,7 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
14378
14392
  // the HTML in itself it still valid
14379
14393
  // keep the syntax error and continue with the HTML
14380
14394
  const errorInfo =
14381
- e.code === "PARSE_ERROR"
14395
+ e.code === "PARSE_ERROR" && e.cause
14382
14396
  ? `${e.cause.reasonCode}\n${e.traceMessage}`
14383
14397
  : e.stack;
14384
14398
  logger.error(
@@ -14885,6 +14899,9 @@ const jsenvPluginDirectoryReferenceAnalysis = () => {
14885
14899
  urlInfo.url,
14886
14900
  urlInfo.context.rootDirectoryUrl,
14887
14901
  );
14902
+ if (urlInfo.contentType !== "application/json") {
14903
+ return null;
14904
+ }
14888
14905
  const entryNames = JSON.parse(urlInfo.content);
14889
14906
  const newEntryNames = [];
14890
14907
  for (const entryName of entryNames) {
@@ -18050,6 +18067,7 @@ const jsenvPluginProtocolFile = ({
18050
18067
  reference.leadsToADirectory = stat && stat.isDirectory();
18051
18068
  if (reference.leadsToADirectory) {
18052
18069
  const directoryAllowed =
18070
+ reference.type === "http_request" ||
18053
18071
  reference.type === "filesystem" ||
18054
18072
  (typeof directoryReferenceAllowed === "function" &&
18055
18073
  directoryReferenceAllowed(reference)) ||
@@ -18115,7 +18133,6 @@ const jsenvPluginProtocolFile = ({
18115
18133
  }
18116
18134
  const urlObject = new URL(urlInfo.url);
18117
18135
  if (urlInfo.firstReference.leadsToADirectory) {
18118
- const directoryEntries = readdirSync(urlObject);
18119
18136
  if (!urlInfo.filenameHint) {
18120
18137
  if (urlInfo.firstReference.type === "filesystem") {
18121
18138
  urlInfo.filenameHint = `${
@@ -18125,10 +18142,17 @@ const jsenvPluginProtocolFile = ({
18125
18142
  urlInfo.filenameHint = `${urlToFilename$1(urlInfo.url)}/`;
18126
18143
  }
18127
18144
  }
18145
+ const { headers, body } = serveDirectory(urlObject.href, {
18146
+ headers: urlInfo.context.request
18147
+ ? urlInfo.context.request.headers
18148
+ : {},
18149
+ rootDirectoryUrl: urlInfo.context.rootDirectoryUrl,
18150
+ });
18128
18151
  return {
18129
18152
  type: "directory",
18130
- contentType: "application/json",
18131
- content: JSON.stringify(directoryEntries, null, " "),
18153
+ contentType: headers["content-type"],
18154
+ contentLength: headers["content-length"],
18155
+ content: body,
18132
18156
  };
18133
18157
  }
18134
18158
  if (
@@ -19254,8 +19278,16 @@ const jsenvPluginHotSearchParam = () => {
19254
19278
  // At this stage the parent is using ?hot and we are going to decide if
19255
19279
  // we propagate the search param to child.
19256
19280
  const referencedUrlInfo = reference.urlInfo;
19257
- const { modifiedTimestamp, dereferencedTimestamp } = referencedUrlInfo;
19258
- if (!modifiedTimestamp && !dereferencedTimestamp) {
19281
+ const {
19282
+ modifiedTimestamp,
19283
+ descendantModifiedTimestamp,
19284
+ dereferencedTimestamp,
19285
+ } = referencedUrlInfo;
19286
+ if (
19287
+ !modifiedTimestamp &&
19288
+ !descendantModifiedTimestamp &&
19289
+ !dereferencedTimestamp
19290
+ ) {
19259
19291
  return null;
19260
19292
  }
19261
19293
  // The goal is to send an url that will bypass client (the browser) cache
@@ -19268,10 +19300,11 @@ const jsenvPluginHotSearchParam = () => {
19268
19300
  // We use the latest timestamp to ensure it's fresh
19269
19301
  // The dereferencedTimestamp is needed because when a js module is re-referenced
19270
19302
  // browser must re-execute it, even if the code is not modified
19271
- const latestTimestamp =
19272
- dereferencedTimestamp && modifiedTimestamp
19273
- ? Math.max(dereferencedTimestamp, modifiedTimestamp)
19274
- : dereferencedTimestamp || modifiedTimestamp;
19303
+ const latestTimestamp = Math.max(
19304
+ modifiedTimestamp,
19305
+ descendantModifiedTimestamp,
19306
+ dereferencedTimestamp,
19307
+ );
19275
19308
  return {
19276
19309
  hot: latestTimestamp,
19277
19310
  };
@@ -19333,89 +19366,149 @@ const jsenvPluginAutoreloadServer = ({
19333
19366
  }
19334
19367
  return url;
19335
19368
  };
19336
- const propagateUpdate = (firstUrlInfo) => {
19337
- const iterate = (urlInfo, seen) => {
19338
- if (urlInfo.data.hotAcceptSelf) {
19339
- return {
19340
- accepted: true,
19341
- reason:
19342
- urlInfo === firstUrlInfo
19343
- ? `file accepts hot reload`
19344
- : `a dependent file accepts hot reload`,
19345
- instructions: [
19346
- {
19347
- type: urlInfo.type,
19348
- boundary: formatUrlForClient(urlInfo.url),
19369
+ const update = (firstUrlInfo) => {
19370
+ const boundaries = new Set();
19371
+ const instructions = [];
19372
+ const propagateUpdate = (firstUrlInfo) => {
19373
+ const iterate = (urlInfo, chain) => {
19374
+ if (urlInfo.data.hotAcceptSelf) {
19375
+ boundaries.add(urlInfo);
19376
+ instructions.push({
19377
+ type: urlInfo.type,
19378
+ boundary: formatUrlForClient(urlInfo.url),
19379
+ acceptedBy: formatUrlForClient(urlInfo.url),
19380
+ });
19381
+ return {
19382
+ accepted: true,
19383
+ reason:
19384
+ urlInfo === firstUrlInfo
19385
+ ? `file accepts hot reload`
19386
+ : `a dependent file accepts hot reload`,
19387
+ };
19388
+ }
19389
+ if (urlInfo.data.hotDecline) {
19390
+ return {
19391
+ declined: true,
19392
+ reason: `file declines hot reload`,
19393
+ declinedBy: formatUrlForClient(urlInfo.url),
19394
+ };
19395
+ }
19396
+ let instructionCountBefore = instructions.length;
19397
+ for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
19398
+ if (
19399
+ referenceFromOther.isImplicit &&
19400
+ referenceFromOther.isWeak
19401
+ ) {
19402
+ if (!referenceFromOther.original) {
19403
+ continue;
19404
+ }
19405
+ if (referenceFromOther.original.isWeak) {
19406
+ continue;
19407
+ }
19408
+ }
19409
+ const urlInfoReferencingThisOne =
19410
+ referenceFromOther.ownerUrlInfo;
19411
+ if (urlInfoReferencingThisOne.data.hotDecline) {
19412
+ return {
19413
+ declined: true,
19414
+ reason: `a dependent file declines hot reload`,
19415
+ declinedBy: formatUrlForClient(
19416
+ urlInfoReferencingThisOne.url,
19417
+ ),
19418
+ };
19419
+ }
19420
+ const { hotAcceptDependencies = [] } =
19421
+ urlInfoReferencingThisOne.data;
19422
+ if (hotAcceptDependencies.includes(urlInfo.url)) {
19423
+ boundaries.add(urlInfoReferencingThisOne);
19424
+ instructions.push({
19425
+ type: urlInfoReferencingThisOne.type,
19426
+ boundary: formatUrlForClient(urlInfoReferencingThisOne.url),
19349
19427
  acceptedBy: formatUrlForClient(urlInfo.url),
19350
- },
19351
- ],
19352
- };
19353
- }
19354
- const instructions = [];
19355
- for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
19356
- if (referenceFromOther.isImplicit && referenceFromOther.isWeak) {
19357
- if (!referenceFromOther.original) {
19428
+ });
19358
19429
  continue;
19359
19430
  }
19360
- if (referenceFromOther.original.isWeak) {
19431
+ if (chain.includes(urlInfoReferencingThisOne.url)) {
19432
+ return {
19433
+ declined: true,
19434
+ reason: "dead end",
19435
+ declinedBy: formatUrlForClient(
19436
+ urlInfoReferencingThisOne.url,
19437
+ ),
19438
+ };
19439
+ }
19440
+ const dependentPropagationResult = iterateMemoized(
19441
+ urlInfoReferencingThisOne,
19442
+ [...chain, urlInfoReferencingThisOne.url],
19443
+ );
19444
+ if (dependentPropagationResult.accepted) {
19361
19445
  continue;
19362
19446
  }
19447
+ if (
19448
+ // declined explicitely by an other file, it must decline the whole update
19449
+ dependentPropagationResult.declinedBy
19450
+ ) {
19451
+ return dependentPropagationResult;
19452
+ }
19453
+ // declined by absence of boundary, we can keep searching
19363
19454
  }
19364
- const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
19365
- if (urlInfoReferencingThisOne.data.hotDecline) {
19455
+ if (instructionCountBefore === instructions.length) {
19366
19456
  return {
19367
19457
  declined: true,
19368
- reason: `a dependent file declines hot reload`,
19369
- declinedBy: urlInfoReferencingThisOne.url,
19458
+ reason: `there is no file accepting hot reload while propagating update`,
19370
19459
  };
19371
19460
  }
19461
+ return {
19462
+ accepted: true,
19463
+ reason: `${instructions.length} dependent file(s) accepts hot reload`,
19464
+ };
19465
+ };
19466
+
19467
+ const map = new Map();
19468
+ const iterateMemoized = (urlInfo, chain) => {
19469
+ const resultFromCache = map.get(urlInfo.url);
19470
+ if (resultFromCache) {
19471
+ return resultFromCache;
19472
+ }
19473
+ const result = iterate(urlInfo, chain);
19474
+ map.set(urlInfo.url, result);
19475
+ return result;
19476
+ };
19477
+ map.clear();
19478
+ return iterateMemoized(firstUrlInfo, []);
19479
+ };
19480
+
19481
+ const propagationResult = propagateUpdate(firstUrlInfo);
19482
+ const seen = new Set();
19483
+ const invalidateImporters = (urlInfo) => {
19484
+ // to indicate this urlInfo should be modified
19485
+ for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
19486
+ const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
19372
19487
  const { hotAcceptDependencies = [] } =
19373
19488
  urlInfoReferencingThisOne.data;
19374
19489
  if (hotAcceptDependencies.includes(urlInfo.url)) {
19375
- instructions.push({
19376
- type: urlInfoReferencingThisOne.type,
19377
- boundary: formatUrlForClient(urlInfoReferencingThisOne.url),
19378
- acceptedBy: formatUrlForClient(urlInfo.url),
19379
- });
19380
19490
  continue;
19381
19491
  }
19382
- if (seen.includes(urlInfoReferencingThisOne.url)) {
19383
- return {
19384
- declined: true,
19385
- reason: "circular dependency",
19386
- declinedBy: formatUrlForClient(urlInfoReferencingThisOne.url),
19387
- };
19388
- }
19389
- const dependentPropagationResult = iterate(
19390
- urlInfoReferencingThisOne,
19391
- [...seen, urlInfoReferencingThisOne.url],
19392
- );
19393
- if (dependentPropagationResult.accepted) {
19394
- instructions.push(...dependentPropagationResult.instructions);
19492
+ if (seen.has(urlInfoReferencingThisOne)) {
19395
19493
  continue;
19396
19494
  }
19397
- if (
19398
- // declined explicitely by an other file, it must decline the whole update
19399
- dependentPropagationResult.declinedBy
19400
- ) {
19401
- return dependentPropagationResult;
19495
+ seen.add(urlInfoReferencingThisOne);
19496
+ // see https://github.com/vitejs/vite/blob/ab5bb40942c7023046fa6f6d0b49cabc105b6073/packages/vite/src/node/server/moduleGraph.ts#L205C5-L207C6
19497
+ if (boundaries.has(urlInfoReferencingThisOne)) {
19498
+ return;
19402
19499
  }
19403
- // declined by absence of boundary, we can keep searching
19404
- }
19405
- if (instructions.length === 0) {
19406
- return {
19407
- declined: true,
19408
- reason: `there is no file accepting hot reload while propagating update`,
19409
- };
19500
+ urlInfoReferencingThisOne.descendantModifiedTimestamp =
19501
+ Date.now();
19502
+ invalidateImporters(urlInfoReferencingThisOne);
19410
19503
  }
19411
- return {
19412
- accepted: true,
19413
- reason: `${instructions.length} dependent file(s) accepts hot reload`,
19414
- instructions,
19415
- };
19416
19504
  };
19417
- const seen = [];
19418
- return iterate(firstUrlInfo, seen);
19505
+ invalidateImporters(firstUrlInfo);
19506
+ boundaries.clear();
19507
+ seen.clear();
19508
+ return {
19509
+ ...propagationResult,
19510
+ instructions,
19511
+ };
19419
19512
  };
19420
19513
 
19421
19514
  // We are delaying the moment we tell client how to reload because:
@@ -19449,7 +19542,7 @@ const jsenvPluginAutoreloadServer = ({
19449
19542
  if (!changedUrlInfo.isUsed()) {
19450
19543
  continue;
19451
19544
  }
19452
- const hotUpdate = propagateUpdate(changedUrlInfo);
19545
+ const hotUpdate = update(changedUrlInfo);
19453
19546
  const relativeUrl = formatUrlForClient(changedUrlInfo.url);
19454
19547
  if (hotUpdate.declined) {
19455
19548
  reloadMessage = {
@@ -19486,7 +19579,7 @@ const jsenvPluginAutoreloadServer = ({
19486
19579
  if (!ownerUrlInfo.isUsed()) {
19487
19580
  continue;
19488
19581
  }
19489
- const ownerHotUpdate = propagateUpdate(ownerUrlInfo);
19582
+ const ownerHotUpdate = update(ownerUrlInfo);
19490
19583
  const cause = `${formatUrlForClient(
19491
19584
  prunedUrlInfo.url,
19492
19585
  )} is no longer referenced`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "38.2.10",
3
+ "version": "38.2.11",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -61,20 +61,20 @@
61
61
  "dependencies": {
62
62
  "@financial-times/polyfill-useragent-normaliser": "1.10.2",
63
63
  "@jsenv/abort": "4.2.4",
64
- "@jsenv/ast": "5.1.4",
64
+ "@jsenv/ast": "5.2.0",
65
65
  "@jsenv/filesystem": "4.3.2",
66
66
  "@jsenv/importmap": "1.2.1",
67
67
  "@jsenv/integrity": "0.0.1",
68
+ "@jsenv/js-module-fallback": "1.3.6",
68
69
  "@jsenv/log": "3.4.1",
69
70
  "@jsenv/node-esm-resolution": "1.0.1",
70
- "@jsenv/js-module-fallback": "1.3.5",
71
- "@jsenv/runtime-compat": "1.2.0",
72
- "@jsenv/server": "15.1.3",
73
- "@jsenv/sourcemap": "1.2.3",
74
71
  "@jsenv/plugin-bundling": "2.5.7",
75
72
  "@jsenv/plugin-minification": "1.5.3",
76
- "@jsenv/plugin-transpilation": "1.3.4",
77
- "@jsenv/plugin-supervisor": "1.3.5",
73
+ "@jsenv/plugin-supervisor": "1.3.6",
74
+ "@jsenv/plugin-transpilation": "1.3.5",
75
+ "@jsenv/runtime-compat": "1.2.0",
76
+ "@jsenv/server": "15.1.4",
77
+ "@jsenv/sourcemap": "1.2.3",
78
78
  "@jsenv/url-meta": "8.1.0",
79
79
  "@jsenv/urls": "2.2.1",
80
80
  "@jsenv/utils": "2.0.1"
@@ -82,22 +82,22 @@
82
82
  "devDependencies": {
83
83
  "@babel/eslint-parser": "7.22.15",
84
84
  "@babel/plugin-syntax-import-assertions": "7.22.5",
85
- "babel-plugin-transform-async-to-promises": "0.8.18",
86
85
  "@jsenv/assert": "./packages/independent/assert/",
87
86
  "@jsenv/core": "./",
88
87
  "@jsenv/eslint-config": "./packages/independent/eslint-config/",
89
- "@jsenv/file-size-impact": "14.1.2",
88
+ "@jsenv/file-size-impact": "14.1.3",
90
89
  "@jsenv/https-local": "3.0.7",
91
90
  "@jsenv/package-workspace": "0.5.3",
92
- "@jsenv/performance-impact": "4.1.2",
91
+ "@jsenv/performance-impact": "4.1.3",
93
92
  "@jsenv/plugin-as-js-classic": "./packages/related/plugin-as-js-classic/",
94
93
  "@jsenv/test": "./packages/related/test/",
95
- "eslint": "8.51.0",
94
+ "babel-plugin-transform-async-to-promises": "0.8.18",
95
+ "eslint": "8.52.0",
96
96
  "eslint-plugin-html": "7.1.0",
97
- "eslint-plugin-import": "2.28.1",
97
+ "eslint-plugin-import": "2.29.0",
98
98
  "eslint-plugin-react": "7.33.2",
99
99
  "open": "9.1.0",
100
- "playwright": "1.38.1",
100
+ "playwright": "1.39.0",
101
101
  "prettier": "3.0.3"
102
102
  }
103
103
  }
@@ -488,7 +488,7 @@ ${ANSI.color(normalizedReturnValue, ANSI.YELLOW)}
488
488
  // the HTML in itself it still valid
489
489
  // keep the syntax error and continue with the HTML
490
490
  const errorInfo =
491
- e.code === "PARSE_ERROR"
491
+ e.code === "PARSE_ERROR" && e.cause
492
492
  ? `${e.cause.reasonCode}\n${e.traceMessage}`
493
493
  : e.stack;
494
494
  logger.error(
@@ -181,6 +181,7 @@ const createUrlInfo = (url, context) => {
181
181
  context,
182
182
  error: null,
183
183
  modifiedTimestamp: 0,
184
+ descendantModifiedTimestamp: 0,
184
185
  dereferencedTimestamp: 0,
185
186
  originalContentEtag: null,
186
187
  contentEtag: null,
@@ -18,89 +18,149 @@ export const jsenvPluginAutoreloadServer = ({
18
18
  }
19
19
  return url;
20
20
  };
21
- const propagateUpdate = (firstUrlInfo) => {
22
- const iterate = (urlInfo, seen) => {
23
- if (urlInfo.data.hotAcceptSelf) {
24
- return {
25
- accepted: true,
26
- reason:
27
- urlInfo === firstUrlInfo
28
- ? `file accepts hot reload`
29
- : `a dependent file accepts hot reload`,
30
- instructions: [
31
- {
32
- type: urlInfo.type,
33
- boundary: formatUrlForClient(urlInfo.url),
21
+ const update = (firstUrlInfo) => {
22
+ const boundaries = new Set();
23
+ const instructions = [];
24
+ const propagateUpdate = (firstUrlInfo) => {
25
+ const iterate = (urlInfo, chain) => {
26
+ if (urlInfo.data.hotAcceptSelf) {
27
+ boundaries.add(urlInfo);
28
+ instructions.push({
29
+ type: urlInfo.type,
30
+ boundary: formatUrlForClient(urlInfo.url),
31
+ acceptedBy: formatUrlForClient(urlInfo.url),
32
+ });
33
+ return {
34
+ accepted: true,
35
+ reason:
36
+ urlInfo === firstUrlInfo
37
+ ? `file accepts hot reload`
38
+ : `a dependent file accepts hot reload`,
39
+ };
40
+ }
41
+ if (urlInfo.data.hotDecline) {
42
+ return {
43
+ declined: true,
44
+ reason: `file declines hot reload`,
45
+ declinedBy: formatUrlForClient(urlInfo.url),
46
+ };
47
+ }
48
+ let instructionCountBefore = instructions.length;
49
+ for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
50
+ if (
51
+ referenceFromOther.isImplicit &&
52
+ referenceFromOther.isWeak
53
+ ) {
54
+ if (!referenceFromOther.original) {
55
+ continue;
56
+ }
57
+ if (referenceFromOther.original.isWeak) {
58
+ continue;
59
+ }
60
+ }
61
+ const urlInfoReferencingThisOne =
62
+ referenceFromOther.ownerUrlInfo;
63
+ if (urlInfoReferencingThisOne.data.hotDecline) {
64
+ return {
65
+ declined: true,
66
+ reason: `a dependent file declines hot reload`,
67
+ declinedBy: formatUrlForClient(
68
+ urlInfoReferencingThisOne.url,
69
+ ),
70
+ };
71
+ }
72
+ const { hotAcceptDependencies = [] } =
73
+ urlInfoReferencingThisOne.data;
74
+ if (hotAcceptDependencies.includes(urlInfo.url)) {
75
+ boundaries.add(urlInfoReferencingThisOne);
76
+ instructions.push({
77
+ type: urlInfoReferencingThisOne.type,
78
+ boundary: formatUrlForClient(urlInfoReferencingThisOne.url),
34
79
  acceptedBy: formatUrlForClient(urlInfo.url),
35
- },
36
- ],
37
- };
38
- }
39
- const instructions = [];
40
- for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
41
- if (referenceFromOther.isImplicit && referenceFromOther.isWeak) {
42
- if (!referenceFromOther.original) {
80
+ });
43
81
  continue;
44
82
  }
45
- if (referenceFromOther.original.isWeak) {
83
+ if (chain.includes(urlInfoReferencingThisOne.url)) {
84
+ return {
85
+ declined: true,
86
+ reason: "dead end",
87
+ declinedBy: formatUrlForClient(
88
+ urlInfoReferencingThisOne.url,
89
+ ),
90
+ };
91
+ }
92
+ const dependentPropagationResult = iterateMemoized(
93
+ urlInfoReferencingThisOne,
94
+ [...chain, urlInfoReferencingThisOne.url],
95
+ );
96
+ if (dependentPropagationResult.accepted) {
46
97
  continue;
47
98
  }
99
+ if (
100
+ // declined explicitely by an other file, it must decline the whole update
101
+ dependentPropagationResult.declinedBy
102
+ ) {
103
+ return dependentPropagationResult;
104
+ }
105
+ // declined by absence of boundary, we can keep searching
48
106
  }
49
- const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
50
- if (urlInfoReferencingThisOne.data.hotDecline) {
107
+ if (instructionCountBefore === instructions.length) {
51
108
  return {
52
109
  declined: true,
53
- reason: `a dependent file declines hot reload`,
54
- declinedBy: urlInfoReferencingThisOne.url,
110
+ reason: `there is no file accepting hot reload while propagating update`,
55
111
  };
56
112
  }
113
+ return {
114
+ accepted: true,
115
+ reason: `${instructions.length} dependent file(s) accepts hot reload`,
116
+ };
117
+ };
118
+
119
+ const map = new Map();
120
+ const iterateMemoized = (urlInfo, chain) => {
121
+ const resultFromCache = map.get(urlInfo.url);
122
+ if (resultFromCache) {
123
+ return resultFromCache;
124
+ }
125
+ const result = iterate(urlInfo, chain);
126
+ map.set(urlInfo.url, result);
127
+ return result;
128
+ };
129
+ map.clear();
130
+ return iterateMemoized(firstUrlInfo, []);
131
+ };
132
+
133
+ const propagationResult = propagateUpdate(firstUrlInfo);
134
+ const seen = new Set();
135
+ const invalidateImporters = (urlInfo) => {
136
+ // to indicate this urlInfo should be modified
137
+ for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
138
+ const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
57
139
  const { hotAcceptDependencies = [] } =
58
140
  urlInfoReferencingThisOne.data;
59
141
  if (hotAcceptDependencies.includes(urlInfo.url)) {
60
- instructions.push({
61
- type: urlInfoReferencingThisOne.type,
62
- boundary: formatUrlForClient(urlInfoReferencingThisOne.url),
63
- acceptedBy: formatUrlForClient(urlInfo.url),
64
- });
65
142
  continue;
66
143
  }
67
- if (seen.includes(urlInfoReferencingThisOne.url)) {
68
- return {
69
- declined: true,
70
- reason: "circular dependency",
71
- declinedBy: formatUrlForClient(urlInfoReferencingThisOne.url),
72
- };
73
- }
74
- const dependentPropagationResult = iterate(
75
- urlInfoReferencingThisOne,
76
- [...seen, urlInfoReferencingThisOne.url],
77
- );
78
- if (dependentPropagationResult.accepted) {
79
- instructions.push(...dependentPropagationResult.instructions);
144
+ if (seen.has(urlInfoReferencingThisOne)) {
80
145
  continue;
81
146
  }
82
- if (
83
- // declined explicitely by an other file, it must decline the whole update
84
- dependentPropagationResult.declinedBy
85
- ) {
86
- return dependentPropagationResult;
147
+ seen.add(urlInfoReferencingThisOne);
148
+ // see https://github.com/vitejs/vite/blob/ab5bb40942c7023046fa6f6d0b49cabc105b6073/packages/vite/src/node/server/moduleGraph.ts#L205C5-L207C6
149
+ if (boundaries.has(urlInfoReferencingThisOne)) {
150
+ return;
87
151
  }
88
- // declined by absence of boundary, we can keep searching
152
+ urlInfoReferencingThisOne.descendantModifiedTimestamp =
153
+ Date.now();
154
+ invalidateImporters(urlInfoReferencingThisOne);
89
155
  }
90
- if (instructions.length === 0) {
91
- return {
92
- declined: true,
93
- reason: `there is no file accepting hot reload while propagating update`,
94
- };
95
- }
96
- return {
97
- accepted: true,
98
- reason: `${instructions.length} dependent file(s) accepts hot reload`,
99
- instructions,
100
- };
101
156
  };
102
- const seen = [];
103
- return iterate(firstUrlInfo, seen);
157
+ invalidateImporters(firstUrlInfo);
158
+ boundaries.clear();
159
+ seen.clear();
160
+ return {
161
+ ...propagationResult,
162
+ instructions,
163
+ };
104
164
  };
105
165
 
106
166
  // We are delaying the moment we tell client how to reload because:
@@ -134,7 +194,7 @@ export const jsenvPluginAutoreloadServer = ({
134
194
  if (!changedUrlInfo.isUsed()) {
135
195
  continue;
136
196
  }
137
- const hotUpdate = propagateUpdate(changedUrlInfo);
197
+ const hotUpdate = update(changedUrlInfo);
138
198
  const relativeUrl = formatUrlForClient(changedUrlInfo.url);
139
199
  if (hotUpdate.declined) {
140
200
  reloadMessage = {
@@ -171,7 +231,7 @@ export const jsenvPluginAutoreloadServer = ({
171
231
  if (!ownerUrlInfo.isUsed()) {
172
232
  continue;
173
233
  }
174
- const ownerHotUpdate = propagateUpdate(ownerUrlInfo);
234
+ const ownerHotUpdate = update(ownerUrlInfo);
175
235
  const cause = `${formatUrlForClient(
176
236
  prunedUrlInfo.url,
177
237
  )} is no longer referenced`;
@@ -44,8 +44,16 @@ export const jsenvPluginHotSearchParam = () => {
44
44
  // At this stage the parent is using ?hot and we are going to decide if
45
45
  // we propagate the search param to child.
46
46
  const referencedUrlInfo = reference.urlInfo;
47
- const { modifiedTimestamp, dereferencedTimestamp } = referencedUrlInfo;
48
- if (!modifiedTimestamp && !dereferencedTimestamp) {
47
+ const {
48
+ modifiedTimestamp,
49
+ descendantModifiedTimestamp,
50
+ dereferencedTimestamp,
51
+ } = referencedUrlInfo;
52
+ if (
53
+ !modifiedTimestamp &&
54
+ !descendantModifiedTimestamp &&
55
+ !dereferencedTimestamp
56
+ ) {
49
57
  return null;
50
58
  }
51
59
  // The goal is to send an url that will bypass client (the browser) cache
@@ -58,10 +66,11 @@ export const jsenvPluginHotSearchParam = () => {
58
66
  // We use the latest timestamp to ensure it's fresh
59
67
  // The dereferencedTimestamp is needed because when a js module is re-referenced
60
68
  // browser must re-execute it, even if the code is not modified
61
- const latestTimestamp =
62
- dereferencedTimestamp && modifiedTimestamp
63
- ? Math.max(dereferencedTimestamp, modifiedTimestamp)
64
- : dereferencedTimestamp || modifiedTimestamp;
69
+ const latestTimestamp = Math.max(
70
+ modifiedTimestamp,
71
+ descendantModifiedTimestamp,
72
+ dereferencedTimestamp,
73
+ );
65
74
  return {
66
75
  hot: latestTimestamp,
67
76
  };
@@ -1,4 +1,5 @@
1
- import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
1
+ import { readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { serveDirectory } from "@jsenv/server";
2
3
  import { pathToFileURL } from "node:url";
3
4
  import {
4
5
  urlIsInsideOf,
@@ -98,6 +99,7 @@ export const jsenvPluginProtocolFile = ({
98
99
  reference.leadsToADirectory = stat && stat.isDirectory();
99
100
  if (reference.leadsToADirectory) {
100
101
  const directoryAllowed =
102
+ reference.type === "http_request" ||
101
103
  reference.type === "filesystem" ||
102
104
  (typeof directoryReferenceAllowed === "function" &&
103
105
  directoryReferenceAllowed(reference)) ||
@@ -163,7 +165,6 @@ export const jsenvPluginProtocolFile = ({
163
165
  }
164
166
  const urlObject = new URL(urlInfo.url);
165
167
  if (urlInfo.firstReference.leadsToADirectory) {
166
- const directoryEntries = readdirSync(urlObject);
167
168
  if (!urlInfo.filenameHint) {
168
169
  if (urlInfo.firstReference.type === "filesystem") {
169
170
  urlInfo.filenameHint = `${
@@ -173,10 +174,17 @@ export const jsenvPluginProtocolFile = ({
173
174
  urlInfo.filenameHint = `${urlToFilename(urlInfo.url)}/`;
174
175
  }
175
176
  }
177
+ const { headers, body } = serveDirectory(urlObject.href, {
178
+ headers: urlInfo.context.request
179
+ ? urlInfo.context.request.headers
180
+ : {},
181
+ rootDirectoryUrl: urlInfo.context.rootDirectoryUrl,
182
+ });
176
183
  return {
177
184
  type: "directory",
178
- contentType: "application/json",
179
- content: JSON.stringify(directoryEntries, null, " "),
185
+ contentType: headers["content-type"],
186
+ contentLength: headers["content-length"],
187
+ content: body,
180
188
  };
181
189
  }
182
190
  if (
@@ -11,6 +11,9 @@ export const jsenvPluginDirectoryReferenceAnalysis = () => {
11
11
  urlInfo.url,
12
12
  urlInfo.context.rootDirectoryUrl,
13
13
  );
14
+ if (urlInfo.contentType !== "application/json") {
15
+ return null;
16
+ }
14
17
  const entryNames = JSON.parse(urlInfo.content);
15
18
  const newEntryNames = [];
16
19
  for (const entryName of entryNames) {