@jsenv/core 38.2.10 → 38.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "38.2.10",
3
+ "version": "38.3.0",
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
  }
@@ -135,7 +135,7 @@ export const createFileService = ({
135
135
  associations: watchAssociations,
136
136
  });
137
137
  urlInfoCreated.isWatched = watch;
138
- // wehn an url depends on many others, we check all these (like package.json)
138
+ // when an url depends on many others, we check all these (like package.json)
139
139
  urlInfoCreated.isValid = () => {
140
140
  if (!urlInfoCreated.url.startsWith("file:")) {
141
141
  return false;
@@ -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(
@@ -386,12 +386,12 @@ const createReference = ({
386
386
  ownerUrlInfo.context.finalizeReference(reference);
387
387
  };
388
388
 
389
- // "formatReferencedUrl" can be async BUT this is an exception
389
+ // "formatReference" can be async BUT this is an exception
390
390
  // for most cases it will be sync. We want to favor the sync signature to keep things simpler
391
391
  // The only case where it needs to be async is when
392
392
  // the specifier is a `data:*` url
393
393
  // in this case we'll wait for the promise returned by
394
- // "formatReferencedUrl"
394
+ // "formatReference"
395
395
  reference.readGeneratedSpecifier = () => {
396
396
  if (reference.generatedSpecifier.then) {
397
397
  return reference.generatedSpecifier.then((value) => {
@@ -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,
@@ -254,6 +255,9 @@ const createUrlInfo = (url, context) => {
254
255
  continue;
255
256
  }
256
257
  if (ref.gotInlined()) {
258
+ if (ref.ownerUrlInfo.isUsed()) {
259
+ return true;
260
+ }
257
261
  // the url info was inlined, an other reference is required
258
262
  // to consider the non-inlined urlInfo as used
259
263
  continue;
@@ -353,7 +357,7 @@ const createUrlInfo = (url, context) => {
353
357
  };
354
358
  urlInfo.onModified = ({ modifiedTimestamp = Date.now() } = {}) => {
355
359
  const visitedSet = new Set();
356
- const iterate = (urlInfo) => {
360
+ const considerModified = (urlInfo) => {
357
361
  if (visitedSet.has(urlInfo)) {
358
362
  return;
359
363
  }
@@ -363,14 +367,21 @@ const createUrlInfo = (url, context) => {
363
367
  for (const referenceToOther of urlInfo.referenceToOthersSet) {
364
368
  const referencedUrlInfo = referenceToOther.urlInfo;
365
369
  if (referencedUrlInfo.isInline) {
366
- iterate(referencedUrlInfo);
370
+ considerModified(referencedUrlInfo);
371
+ }
372
+ }
373
+ for (const referenceFromOther of urlInfo.referenceFromOthersSet) {
374
+ if (referenceFromOther.gotInlined()) {
375
+ const urlInfoReferencingThisOne = referenceFromOther.ownerUrlInfo;
376
+ considerModified(urlInfoReferencingThisOne);
367
377
  }
368
378
  }
369
379
  for (const searchParamVariant of urlInfo.searchParamVariantSet) {
370
- iterate(searchParamVariant);
380
+ considerModified(searchParamVariant);
371
381
  }
372
382
  };
373
- iterate(urlInfo);
383
+ considerModified(urlInfo);
384
+ visitedSet.clear();
374
385
  };
375
386
  urlInfo.onDereferenced = (lastReferenceFromOther) => {
376
387
  urlInfo.dereferencedTimestamp = Date.now();
@@ -18,89 +18,157 @@ 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
  }
57
- const { hotAcceptDependencies = [] } =
58
- urlInfoReferencingThisOne.data;
59
- if (hotAcceptDependencies.includes(urlInfo.url)) {
60
- instructions.push({
61
- type: urlInfoReferencingThisOne.type,
62
- boundary: formatUrlForClient(urlInfoReferencingThisOne.url),
63
- acceptedBy: formatUrlForClient(urlInfo.url),
64
- });
65
- continue;
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;
66
124
  }
67
- if (seen.includes(urlInfoReferencingThisOne.url)) {
68
- return {
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
+ let 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;
139
+ const { hotDecline, hotAcceptDependencies = [] } =
140
+ urlInfoReferencingThisOne.data;
141
+ if (hotDecline) {
142
+ propagationResult = {
69
143
  declined: true,
70
- reason: "circular dependency",
144
+ reason: `file declines hot reload`,
71
145
  declinedBy: formatUrlForClient(urlInfoReferencingThisOne.url),
72
146
  };
147
+ return;
73
148
  }
74
- const dependentPropagationResult = iterate(
75
- urlInfoReferencingThisOne,
76
- [...seen, urlInfoReferencingThisOne.url],
77
- );
78
- if (dependentPropagationResult.accepted) {
79
- instructions.push(...dependentPropagationResult.instructions);
149
+ if (hotAcceptDependencies.includes(urlInfo.url)) {
80
150
  continue;
81
151
  }
82
- if (
83
- // declined explicitely by an other file, it must decline the whole update
84
- dependentPropagationResult.declinedBy
85
- ) {
86
- return dependentPropagationResult;
152
+ if (seen.has(urlInfoReferencingThisOne)) {
153
+ continue;
87
154
  }
88
- // declined by absence of boundary, we can keep searching
89
- }
90
- if (instructions.length === 0) {
91
- return {
92
- declined: true,
93
- reason: `there is no file accepting hot reload while propagating update`,
94
- };
155
+ seen.add(urlInfoReferencingThisOne);
156
+ // see https://github.com/vitejs/vite/blob/ab5bb40942c7023046fa6f6d0b49cabc105b6073/packages/vite/src/node/server/moduleGraph.ts#L205C5-L207C6
157
+ if (boundaries.has(urlInfoReferencingThisOne)) {
158
+ return;
159
+ }
160
+ urlInfoReferencingThisOne.descendantModifiedTimestamp =
161
+ Date.now();
162
+ invalidateImporters(urlInfoReferencingThisOne);
95
163
  }
96
- return {
97
- accepted: true,
98
- reason: `${instructions.length} dependent file(s) accepts hot reload`,
99
- instructions,
100
- };
101
164
  };
102
- const seen = [];
103
- return iterate(firstUrlInfo, seen);
165
+ invalidateImporters(firstUrlInfo);
166
+ boundaries.clear();
167
+ seen.clear();
168
+ return {
169
+ ...propagationResult,
170
+ instructions,
171
+ };
104
172
  };
105
173
 
106
174
  // We are delaying the moment we tell client how to reload because:
@@ -134,7 +202,7 @@ export const jsenvPluginAutoreloadServer = ({
134
202
  if (!changedUrlInfo.isUsed()) {
135
203
  continue;
136
204
  }
137
- const hotUpdate = propagateUpdate(changedUrlInfo);
205
+ const hotUpdate = update(changedUrlInfo);
138
206
  const relativeUrl = formatUrlForClient(changedUrlInfo.url);
139
207
  if (hotUpdate.declined) {
140
208
  reloadMessage = {
@@ -171,7 +239,7 @@ export const jsenvPluginAutoreloadServer = ({
171
239
  if (!ownerUrlInfo.isUsed()) {
172
240
  continue;
173
241
  }
174
- const ownerHotUpdate = propagateUpdate(ownerUrlInfo);
242
+ const ownerHotUpdate = update(ownerUrlInfo);
175
243
  const cause = `${formatUrlForClient(
176
244
  prunedUrlInfo.url,
177
245
  )} 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
  };
@@ -2,7 +2,6 @@ import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
2
2
  import { jsenvPluginTranspilation } from "@jsenv/plugin-transpilation";
3
3
 
4
4
  import { jsenvPluginReferenceAnalysis } from "./reference_analysis/jsenv_plugin_reference_analysis.js";
5
- import { jsenvPluginImportmap } from "./importmap/jsenv_plugin_importmap.js";
6
5
  import { jsenvPluginNodeEsmResolution } from "./resolution_node_esm/jsenv_plugin_node_esm_resolution.js";
7
6
  import { jsenvPluginWebResolution } from "./resolution_web/jsenv_plugin_web_resolution.js";
8
7
  import { jsenvPluginVersionSearchParam } from "./version_search_param/jsenv_plugin_version_search_param.js";
@@ -55,7 +54,6 @@ export const getCorePlugins = ({
55
54
  jsenvPluginReferenceAnalysis(referenceAnalysis),
56
55
  ...(injections ? [jsenvPluginInjections(injections)] : []),
57
56
  jsenvPluginTranspilation(transpilation),
58
- jsenvPluginImportmap(),
59
57
  ...(inlining ? [jsenvPluginInlining()] : []),
60
58
  ...(supervisor ? [jsenvPluginSupervisor(supervisor)] : []), // after inline as it needs inline script to be cooked
61
59
 
@@ -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) {