@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.
@@ -8184,6 +8184,140 @@ const asInheritedInjections = (injections) => {
8184
8184
  return inheritedInjections;
8185
8185
  };
8186
8186
 
8187
+ /*
8188
+ * Text patches applied to files as they are served and built, keyed by file:
8189
+ *
8190
+ * patches: {
8191
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
8192
+ * }
8193
+ *
8194
+ * A key is a url pattern relative to the root directory ("./main.js",
8195
+ * "**\/*.css"), or a path inside a package ("preact/dist/preact.mjs") found by
8196
+ * walking up from the root directory into node_modules, the way node does,
8197
+ * so the key holds wherever the package manager hoists the package.
8198
+ *
8199
+ * Every `from` must occur exactly once in the file, otherwise the file fails
8200
+ * to cook and says which patch did not apply: a dependency update that moved
8201
+ * the patched code must be looked at, never silently unpatched.
8202
+ */
8203
+
8204
+
8205
+ const jsenvPluginPatches = (rawPatches) => {
8206
+ if (!rawPatches || Object.keys(rawPatches).length === 0) {
8207
+ return [];
8208
+ }
8209
+ let findPatches;
8210
+ const patchesPlugin = {
8211
+ name: "jsenv:patches",
8212
+ appliesDuring: "*",
8213
+ init: (context) => {
8214
+ const { rootDirectoryUrl } = context;
8215
+ const patchesByPattern = {};
8216
+ for (const key of Object.keys(rawPatches)) {
8217
+ const patches = rawPatches[key];
8218
+ assertPatches(patches, key);
8219
+ patchesByPattern[resolvePatchKey(key, rootDirectoryUrl)] = patches;
8220
+ }
8221
+ const associations = URL_META.resolveAssociations(
8222
+ { patches: patchesByPattern },
8223
+ rootDirectoryUrl,
8224
+ );
8225
+ findPatches = (url) => {
8226
+ const { patches } = URL_META.applyAssociations({
8227
+ url: asUrlWithoutSearch(url),
8228
+ associations,
8229
+ });
8230
+ return patches;
8231
+ };
8232
+ },
8233
+ transformUrlContent: (urlInfo) => {
8234
+ const patches = findPatches(urlInfo.url);
8235
+ if (!patches) {
8236
+ return null;
8237
+ }
8238
+ const { content } = urlInfo;
8239
+ const magicSource = createMagicSource(content);
8240
+ for (const { from, to } of patches) {
8241
+ const start = content.indexOf(from);
8242
+ const occurrenceCount =
8243
+ start === -1 ? 0 : content.indexOf(from, start + 1) === -1 ? 1 : 2;
8244
+ if (occurrenceCount !== 1) {
8245
+ const fileRelativeUrl = urlToRelativeUrl(
8246
+ urlInfo.url,
8247
+ urlInfo.context.rootDirectoryUrl,
8248
+ );
8249
+ throw new Error(
8250
+ `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.`,
8251
+ );
8252
+ }
8253
+ magicSource.replace({
8254
+ start,
8255
+ end: start + from.length,
8256
+ replacement: to,
8257
+ });
8258
+ }
8259
+ return magicSource.toContentAndSourcemap();
8260
+ },
8261
+ };
8262
+ return [patchesPlugin];
8263
+ };
8264
+
8265
+ const assertPatches = (patches, key) => {
8266
+ if (!Array.isArray(patches)) {
8267
+ throw new TypeError(
8268
+ `patches["${key}"] must be an array of { from, to }, got ${patches}`,
8269
+ );
8270
+ }
8271
+ for (const patch of patches) {
8272
+ if (
8273
+ !patch ||
8274
+ typeof patch.from !== "string" ||
8275
+ patch.from === "" ||
8276
+ typeof patch.to !== "string"
8277
+ ) {
8278
+ throw new TypeError(
8279
+ `patches["${key}"] entries must be { from: string, to: string } with a non-empty "from"`,
8280
+ );
8281
+ }
8282
+ }
8283
+ };
8284
+
8285
+ // "./x", "../x", "/x", "file:///x" and "**/x" are url patterns; anything
8286
+ // else names a path inside a package, looked up in node_modules
8287
+ const resolvePatchKey = (key, rootDirectoryUrl) => {
8288
+ if (
8289
+ key.startsWith("./") ||
8290
+ key.startsWith("../") ||
8291
+ key.startsWith("/") ||
8292
+ key.startsWith("file:") ||
8293
+ key.startsWith("*")
8294
+ ) {
8295
+ return key;
8296
+ }
8297
+ const segments = key.split("/");
8298
+ const packageName = key.startsWith("@")
8299
+ ? `${segments[0]}/${segments[1]}`
8300
+ : segments[0];
8301
+ const pathInsidePackage = key.slice(packageName.length);
8302
+ let directoryUrl = new URL(rootDirectoryUrl);
8303
+ while (true) {
8304
+ const packageDirectoryUrl = new URL(
8305
+ `./node_modules/${packageName}/`,
8306
+ directoryUrl,
8307
+ );
8308
+ if (existsSync(packageDirectoryUrl)) {
8309
+ return String(new URL(`.${pathInsidePackage}`, packageDirectoryUrl));
8310
+ }
8311
+ const parentDirectoryUrl = new URL("../", directoryUrl);
8312
+ if (parentDirectoryUrl.href === directoryUrl.href) {
8313
+ throw new Error(
8314
+ `patches["${key}"]: package "${packageName}" not found in any node_modules above ${rootDirectoryUrl}`,
8315
+ );
8316
+ }
8317
+ directoryUrl = parentDirectoryUrl;
8318
+ }
8319
+ };
8320
+
8187
8321
  /*
8188
8322
  * Some code uses globals specific to Node.js in code meant to run in browsers...
8189
8323
  * This plugin will replace some node globals to things compatible with web:
@@ -10320,6 +10454,7 @@ const getCorePlugins = ({
10320
10454
  directoryListing = true,
10321
10455
  directoryReferenceEffect,
10322
10456
  supervisor,
10457
+ patches,
10323
10458
  injections,
10324
10459
  transpilation = true,
10325
10460
  inlining = true,
@@ -10361,6 +10496,9 @@ const getCorePlugins = ({
10361
10496
  ...(packageBundle
10362
10497
  ? [jsenvPluginWorkspaceBundle({ packageDirectory })]
10363
10498
  : []),
10499
+ // before everything else: what the other plugins read must be the
10500
+ // patched file
10501
+ ...jsenvPluginPatches(patches),
10364
10502
  // before reference analysis: an url written by an injection must hold its
10365
10503
  // final value when references are analyzed
10366
10504
  jsenvPluginInjections(injections),
@@ -12645,6 +12783,17 @@ const jsenvPluginMappings = (mappings) => {
12645
12783
  * }
12646
12784
  *
12647
12785
  * See startDevServer "ribbon" param for the full list of options.
12786
+ * @param {object} [entryPoint.patches]
12787
+ * Text patches applied to files as they are built, as { file: [{ from, to }] }.
12788
+ * A key is a url pattern relative to sourceDirectoryUrl ("./main.js") or a path
12789
+ * inside a package ("preact/dist/preact.mjs", found in node_modules the way node does):
12790
+ *
12791
+ * patches: {
12792
+ * "preact/dist/preact.mjs": [{ from: "a&&b", to: "a&&b&&c" }],
12793
+ * }
12794
+ *
12795
+ * Each `from` must occur exactly once in the file, otherwise the build fails
12796
+ * and says which patch did not apply.
12648
12797
  * @param {object} [entryPoint.injections]
12649
12798
  * Values to inject into files, as { urlPattern: getInjections }.
12650
12799
  * Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
@@ -13510,6 +13659,7 @@ const entryPointDefaultParams = {
13510
13659
  directoryReferenceEffect: undefined,
13511
13660
  scenarioPlaceholders: undefined,
13512
13661
  ribbon: false,
13662
+ patches: undefined,
13513
13663
  injections: undefined,
13514
13664
  transpilation: {},
13515
13665
  preserveComments: undefined,
@@ -13566,6 +13716,7 @@ const prepareEntryPointBuild = async (
13566
13716
  directoryReferenceEffect,
13567
13717
  scenarioPlaceholders,
13568
13718
  ribbon,
13719
+ patches,
13569
13720
  injections,
13570
13721
  transpilation,
13571
13722
  preserveComments,
@@ -13734,6 +13885,7 @@ const prepareEntryPointBuild = async (
13734
13885
  magicExtensions,
13735
13886
  magicDirectoryIndex,
13736
13887
  directoryReferenceEffect,
13888
+ patches,
13737
13889
  injections,
13738
13890
  transpilation: {
13739
13891
  babelHelpersAsImport: !explicitJsModuleConversion,
@@ -4,7 +4,7 @@ let createEventsManager;
4
4
  const callbacksMap = new Map();
5
5
  let cleanup;
6
6
  const addCallbacks = (namedCallbacks) => {
7
- let callbacksMapSize = callbacksMap.size;
7
+ const sizeBeforeAdd = callbacksMap.size;
8
8
  Object.keys(namedCallbacks).forEach((eventName) => {
9
9
  const callback = namedCallbacks[eventName];
10
10
  const existingCallbacks = callbacksMap.get(eventName);
@@ -17,7 +17,10 @@ let createEventsManager;
17
17
  }
18
18
  callbacks.push(callback);
19
19
  });
20
- if (effect && callbacksMapSize === 0 && callbacksMapSize.size > 0) {
20
+ // the effect owns whatever the callbacks need to exist (the connection),
21
+ // so it runs on the edge into "someone is listening" and its cleanup runs
22
+ // on the edge back out of it
23
+ if (sizeBeforeAdd === 0 && callbacksMap.size > 0) {
21
24
  cleanup = effect();
22
25
  }
23
26
 
@@ -25,7 +28,7 @@ let createEventsManager;
25
28
  return () => {
26
29
  if (removed) return;
27
30
  removed = true;
28
- callbacksMapSize = callbacksMap.size;
31
+ const sizeBeforeRemove = callbacksMap.size;
29
32
  Object.keys(namedCallbacks).forEach((eventName) => {
30
33
  const callback = namedCallbacks[eventName];
31
34
  const callbacks = callbacksMap.get(eventName);
@@ -40,12 +43,7 @@ let createEventsManager;
40
43
  }
41
44
  });
42
45
  namedCallbacks = null; // allow garbage collect
43
- if (
44
- cleanup &&
45
- typeof cleanup === "function" &&
46
- callbacksMapSize > 0 &&
47
- callbacksMapSize.size === 0
48
- ) {
46
+ if (cleanup && sizeBeforeRemove > 0 && callbacksMap.size === 0) {
49
47
  cleanup();
50
48
  cleanup = null;
51
49
  }
@@ -85,9 +83,14 @@ let createConnectionManager;
85
83
  CLOSED: "closed",
86
84
  };
87
85
 
86
+ // The peer is a dev server being restarted, not a flaky network: there is no
87
+ // attempt count or time budget past which giving up would be correct. Attempts
88
+ // space out to retryAfterMax and never stop, and the moments where the server
89
+ // is most likely to be back already (tab visible, window focused, network
90
+ // back) skip the wait instead of leaving a detached page on screen.
88
91
  createConnectionManager = (
89
92
  attemptConnection,
90
- { logs, retry, retryAfter, retryMaxAttempt, retryAllocatedMs },
93
+ { logs, retry, retryAfter, retryAfterMax },
91
94
  ) => {
92
95
  const readyState = {
93
96
  value: READY_STATES.CLOSED,
@@ -102,6 +105,7 @@ let createConnectionManager;
102
105
  };
103
106
 
104
107
  let _disconnect = () => {};
108
+ let _retryNow = () => {};
105
109
  const connect = () => {
106
110
  if (
107
111
  readyState.value === READY_STATES.CONNECTING ||
@@ -110,11 +114,11 @@ let createConnectionManager;
110
114
  return;
111
115
  }
112
116
 
113
- let retryCount = 0;
114
- let msSpent = 0;
117
+ let retryDelay = retryAfter;
118
+ let retryTimeout = null;
115
119
  const attempt = () => {
120
+ retryTimeout = null;
116
121
  readyState.goTo(READY_STATES.CONNECTING);
117
- let timeout;
118
122
  const cancelAttempt = attemptConnection({
119
123
  onClosed: () => {
120
124
  if (!retry) {
@@ -124,24 +128,6 @@ let createConnectionManager;
124
128
  }
125
129
  return;
126
130
  }
127
- if (retryCount > retryMaxAttempt) {
128
- readyState.goTo(READY_STATES.CLOSED);
129
- if (logs) {
130
- console.info(
131
- `[jsenv] could not connect to server after ${retryMaxAttempt} attempt`,
132
- );
133
- }
134
- return;
135
- }
136
- if (retryAllocatedMs && msSpent > retryAllocatedMs) {
137
- readyState.goTo(READY_STATES.CLOSED);
138
- if (logs) {
139
- console.info(
140
- `[jsenv] could not connect to server in less than ${retryAllocatedMs}ms`,
141
- );
142
- }
143
- return;
144
- }
145
131
  // if closed while open -> connection lost
146
132
  // otherwise it's the attempt to connect for the first time
147
133
  // or to reconnect
@@ -152,22 +138,31 @@ let createConnectionManager;
152
138
  );
153
139
  }
154
140
  }
155
- retryCount++;
156
- timeout = setTimeout(() => {
157
- msSpent += retryAfter;
158
- attempt();
159
- }, retryAfter);
141
+ retryTimeout = setTimeout(attempt, retryDelay);
142
+ const nextRetryDelay = retryDelay * 2;
143
+ retryDelay =
144
+ nextRetryDelay > retryAfterMax ? retryAfterMax : nextRetryDelay;
160
145
  },
161
146
  onOpen: () => {
147
+ retryDelay = retryAfter;
162
148
  readyState.goTo(READY_STATES.OPEN);
163
149
  },
164
150
  });
165
151
  _disconnect = () => {
166
152
  cancelAttempt();
167
- clearTimeout(timeout);
153
+ clearTimeout(retryTimeout);
154
+ retryTimeout = null;
168
155
  readyState.goTo(READY_STATES.CLOSED);
169
156
  };
170
157
  };
158
+ _retryNow = () => {
159
+ if (retryTimeout === null) {
160
+ return;
161
+ }
162
+ clearTimeout(retryTimeout);
163
+ retryDelay = retryAfter;
164
+ attempt();
165
+ };
171
166
  attempt();
172
167
  };
173
168
 
@@ -194,6 +189,9 @@ let createConnectionManager;
194
189
  _disconnect();
195
190
  }
196
191
  });
192
+ const removeServerMightBeBackListeners = listenServerMightBeBack(() => {
193
+ _retryNow();
194
+ });
197
195
 
198
196
  return {
199
197
  readyState,
@@ -201,11 +199,31 @@ let createConnectionManager;
201
199
  disconnect,
202
200
  destroy: () => {
203
201
  removePageUnloadListener();
202
+ removeServerMightBeBackListeners();
204
203
  disconnect();
205
204
  },
206
205
  };
207
206
  };
208
207
 
208
+ const listenServerMightBeBack = (callback) => {
209
+ const removeOnlineListener = listenEvent(window, "online", callback);
210
+ const removeFocusListener = listenEvent(window, "focus", callback);
211
+ const removeVisibilityChangeListener = listenEvent(
212
+ document,
213
+ "visibilitychange",
214
+ () => {
215
+ if (document.visibilityState === "visible") {
216
+ callback();
217
+ }
218
+ },
219
+ );
220
+ return () => {
221
+ removeOnlineListener();
222
+ removeFocusListener();
223
+ removeVisibilityChangeListener();
224
+ };
225
+ };
226
+
209
227
  // const listenPageMightFreeze = (callback) => {
210
228
  // const removePageHideListener = listenEvent(window, "pagehide", (pageHideEvent) => {
211
229
  // if (pageHideEvent.persisted === true) {
@@ -271,8 +289,7 @@ let createWebSocketConnection;
271
289
  useEventsToManageConnection = true,
272
290
  retry = false,
273
291
  retryAfter = 1000,
274
- retryMaxAttempt = Infinity,
275
- retryAllocatedMs = Infinity,
292
+ retryAfterMax = 5000,
276
293
  } = {},
277
294
  ) => {
278
295
  const connectionManager = createConnectionManager(
@@ -315,7 +332,7 @@ let createWebSocketConnection;
315
332
  }
316
333
  };
317
334
  },
318
- { logs, retry, retryAfter, retryMaxAttempt, retryAllocatedMs },
335
+ { logs, retry, retryAfter, retryAfterMax },
319
336
  );
320
337
  const eventsManager = createEventsManager({
321
338
  effect: () => {
@@ -439,7 +456,6 @@ const serverEventsInterface = {
439
456
  const websocketConnection = createWebSocketConnection(websocketUrl, {
440
457
  logs,
441
458
  retry: true,
442
- retryAllocatedMs: 10_000,
443
459
  });
444
460
 
445
461
  const { readyState, connect, disconnect, listenEvents } =