@maka/meteor-sdk 0.2.2 → 0.2.4

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.
Files changed (26) hide show
  1. package/README.md +9 -0
  2. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/.package-lock.json +1 -1
  3. package/isopackets/combined/npm/node_modules/meteor/boilerplate-generator/node_modules/.package-lock.json +1 -1
  4. package/isopackets/combined/npm/node_modules/meteor/constraint-solver/node_modules/.package-lock.json +1 -1
  5. package/isopackets/combined/npm/node_modules/meteor/ddp-client/node_modules/.package-lock.json +1 -1
  6. package/isopackets/combined/npm/node_modules/meteor/ecmascript/node_modules/.package-lock.json +1 -1
  7. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime/node_modules/.package-lock.json +1 -1
  8. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime-client/node_modules/.package-lock.json +1 -1
  9. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime-server/node_modules/.package-lock.json +1 -1
  10. package/isopackets/combined/npm/node_modules/meteor/fetch/node_modules/.package-lock.json +1 -1
  11. package/isopackets/combined/npm/node_modules/meteor/inter-process-messaging/node_modules/.package-lock.json +1 -1
  12. package/isopackets/combined/npm/node_modules/meteor/logging/node_modules/.package-lock.json +1 -1
  13. package/isopackets/combined/npm/node_modules/meteor/logic-solver/node_modules/.package-lock.json +1 -1
  14. package/isopackets/combined/npm/node_modules/meteor/meteor/node_modules/.package-lock.json +1 -1
  15. package/isopackets/combined/npm/node_modules/meteor/modules/node_modules/.package-lock.json +1 -1
  16. package/isopackets/combined/npm/node_modules/meteor/modules-runtime/node_modules/.package-lock.json +1 -1
  17. package/isopackets/combined/npm/node_modules/meteor/npm-mongo/node_modules/.package-lock.json +1 -1
  18. package/isopackets/combined/npm/node_modules/meteor/package-version-parser/node_modules/.package-lock.json +1 -1
  19. package/isopackets/combined/npm/node_modules/meteor/promise/node_modules/.package-lock.json +1 -1
  20. package/isopackets/combined/npm/node_modules/meteor/react-fast-refresh/node_modules/.package-lock.json +1 -1
  21. package/isopackets/combined/npm/node_modules/meteor/socket-stream-client/node_modules/.package-lock.json +1 -1
  22. package/package.json +3 -6
  23. package/src/release.js +37 -1
  24. package/tools/fs/dev-bundle-links.js +1 -1
  25. package/tools/fs/files.ts +20 -7
  26. package/tools/utils/http-helpers.js +283 -285
package/README.md CHANGED
@@ -42,6 +42,15 @@ the target app's own `.meteor/release` (e.g. `"METEOR@3.5.1"`) -- it pins
42
42
  which Meteor release's package versions and build behavior this process
43
43
  uses.
44
44
 
45
+ Passing a specific release requires that release to be present in the
46
+ local package catalog (the Meteor warehouse) -- on a machine that has
47
+ never downloaded it, `init` rejects with a `MeteorSdkError` naming the
48
+ missing release. **Embedders that only need `connectDdp()` (or other
49
+ non-build APIs) should call `init({})` with no release**: that uses
50
+ checkout semantics (the tool copy this package itself ships), needs no
51
+ warehouse or catalog access, and works on a machine whose only Meteor
52
+ footprint is this npm package.
53
+
45
54
  ### `loadProject(appDir)`
46
55
 
47
56
  Resolves an app directory into a `Project`, ready to bundle or run.
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-10g0ajfucol",
2
+ "name": "package-new-1snkx7vjcuf",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1slwkyce3sz",
2
+ "name": "package-new-4u9fx8mbxny",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-fmtb13edl7",
2
+ "name": "package-new-sm91z7t3z4o",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-17ww1zgjylj",
2
+ "name": "package-new-gsvm5rcc2iq",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-g6t4wfio7ve",
2
+ "name": "package-new-3gg4doz5i88",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-aqph2241bof",
2
+ "name": "package-new-1nb1rwir7xu",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-xcpgqd2fln9",
2
+ "name": "package-new-1jo8olhxkjm",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1cqofk7h0vn",
2
+ "name": "package-new-2ybexym7dno",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1g6gs3yfiwp",
2
+ "name": "package-new-i9b1vaovh2j",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-nt8samf9uwk",
2
+ "name": "package-new-dymdsziwayf",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-ksjvqwow5eq",
2
+ "name": "package-new-ffz4udz1rzl",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-usn32d5f7m",
2
+ "name": "package-new-1gj1i1e8erq",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1wbkujoz6iei",
2
+ "name": "package-new-1hynmr3gjvhf",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-s9fhxhunuf",
2
+ "name": "package-new-1ffagznc9qh",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1v67hnpaktth",
2
+ "name": "package-new-e37v0zigxr6",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-3xnmqbs5th5",
2
+ "name": "package-new-wfebwy06fwm",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-xjm9993m8hk",
2
+ "name": "package-new-dbv0mrj9kh",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-d5x03oy37qo",
2
+ "name": "package-new-4rd6cn8q4z7",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-15icl6ez9mo",
2
+ "name": "package-new-m44yzrtc8cq",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1wtb47fkaa7",
2
+ "name": "package-new-c4g2z219k47",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/meteor-sdk",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Programmatic Node API over the Meteor build tool (bundle/run/create/etc.) -- for driving Meteor's build system from plain Node code instead of shelling out to a meteor CLI (there isn't one anymore; see README.md). \"dependencies\" is generated by scripts/write-tool-package-json.js from scripts/dev-bundle-tool-package.js at the monorepo root -- do not hand-edit that field, edit that file and re-run the script instead.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -44,10 +44,8 @@
44
44
  "source-map-v01": "npm:source-map@0.1.32",
45
45
  "@types/semver": "7.5.8",
46
46
  "semver": "7.6.3",
47
- "request": "2.88.2",
48
- "uuid": "8.3.2",
47
+ "undici": "8.10.0",
49
48
  "graceful-fs": "4.2.11",
50
- "tar": "6.1.11",
51
49
  "tar-fs": "2.1.1",
52
50
  "source-map": "0.7.4",
53
51
  "chalk": "4.1.2",
@@ -56,8 +54,7 @@
56
54
  "is-reachable": "3.1.0",
57
55
  "wordwrap": "1.0.0",
58
56
  "moment": "2.30.1",
59
- "rimraf": "2.6.2",
60
- "glob": "11.0.1",
57
+ "glob": "13.0.6",
61
58
  "ignore": "7.0.5",
62
59
  "commonmark": "0.15.0",
63
60
  "escope": "4.0.0",
package/src/release.js CHANGED
@@ -36,7 +36,43 @@ async function init(options) {
36
36
  // release cached (or be reachable); see release.load's own doc comment
37
37
  // in meteor-tool for the exact fetch/cache behavior.
38
38
  const releaseName = options.release == null ? null : options.release;
39
- const releaseObject = await release.load(releaseName);
39
+ let releaseObject;
40
+ try {
41
+ releaseObject = await release.load(releaseName);
42
+ } catch (e) {
43
+ // release.NoSuchReleaseError is an EMPTY constructor -- not even an
44
+ // Error, no .message -- so embedders logging err.message printed
45
+ // "undefined" and the real cause (release not in the local package
46
+ // catalog, e.g. a machine with no populated Meteor warehouse) was
47
+ // invisible. Cost a real debugging evening in the field; wrap it
48
+ // (and any other message-less throw from release.load) in a proper
49
+ // MeteorSdkError that names the release and where the catalog lives.
50
+ let warehouse = "~/.meteor";
51
+ try {
52
+ warehouse = require("../tools/packaging/tropohouse.js").default.root;
53
+ } catch (_) {
54
+ // Best effort -- the default hint above is close enough.
55
+ }
56
+ if (e instanceof release.NoSuchReleaseError) {
57
+ throw new MeteorSdkError(
58
+ "init(): release " + JSON.stringify(releaseName) + " is not in " +
59
+ "the local package catalog (warehouse: " + warehouse + "). " +
60
+ "On a machine that has never downloaded that Meteor release " +
61
+ "this is expected -- either install/download the release " +
62
+ "first, or, for embedders that only need connectDdp() and " +
63
+ "other non-build APIs, call init({}) with no release (checkout " +
64
+ "semantics, no catalog needed)."
65
+ );
66
+ }
67
+ if (e instanceof Error) {
68
+ throw e;
69
+ }
70
+ throw new MeteorSdkError(
71
+ "init(): release.load(" + JSON.stringify(releaseName) + ") failed " +
72
+ "with a message-less throw (" + Object.prototype.toString.call(e) +
73
+ "); warehouse: " + warehouse
74
+ );
75
+ }
40
76
  // forced is always true: init({release}) is the embedder explicitly
41
77
  // choosing the release for this process -- the moral equivalent of the
42
78
  // CLI's --release flag, which is exactly what release.forced means.
@@ -22,7 +22,7 @@ exports.makeLink = function (target, linkPath) {
22
22
  fs.renameSync(tempPath, linkPath);
23
23
  } catch (e) {
24
24
  // If renaming fails, try unlinking first.
25
- require("rimraf").sync(linkPath);
25
+ require("fs").rmSync(linkPath, { recursive: true, force: true });
26
26
  fs.renameSync(tempPath, linkPath);
27
27
  }
28
28
  };
package/tools/fs/files.ts CHANGED
@@ -30,7 +30,6 @@ import { realpathSync } from './fsFixPath';
30
30
 
31
31
  const _ = require('underscore');
32
32
 
33
- const rimraf = require('rimraf');
34
33
  const sourcemap = require('source-map');
35
34
  const sourceMapRetrieverStack = require('../tool-env/source-map-retriever-stack.js');
36
35
 
@@ -322,7 +321,16 @@ export function getCurrentNodeBinDir() {
322
321
  // Return the top-level directory for this meteor install or checkout
323
322
  export function getCurrentToolsDir() {
324
323
  if (!process.env.SANDBOX && process.env.METEOR_WAREHOUSE_DIR) {
325
- return pathDirname(realpathSync(pathJoin(process.env.METEOR_WAREHOUSE_DIR, 'meteor')));
324
+ // Only follow the warehouse's `meteor` link when it actually exists:
325
+ // an empty/fresh METEOR_WAREHOUSE_DIR (observed: SDK embedders
326
+ // pointing the env var at a scratch dir) made realpathSync throw
327
+ // ENOENT uncaught through getToolBundleRoot() and everything above
328
+ // it. Falling through to the __dirname-based answer keeps the
329
+ // running tool copy resolvable regardless of warehouse state.
330
+ const warehouseMeteor = pathJoin(process.env.METEOR_WAREHOUSE_DIR, 'meteor');
331
+ if (exists(warehouseMeteor)) {
332
+ return pathDirname(realpathSync(warehouseMeteor));
333
+ }
326
334
  }
327
335
 
328
336
  return pathDirname(pathDirname(convertToStandardPath(__dirname)));
@@ -457,16 +465,21 @@ export function realpathOrNull(path: string) {
457
465
 
458
466
  export function rm_recursive_async(path: string) {
459
467
  return new Promise<void>((resolve, reject) => {
460
- rimraf(convertToOSPath(path), (err: Error) => err
461
- ? reject(err)
462
- : resolve());
468
+ // fs.rm with force+maxRetries replaces the old rimraf dependency
469
+ // (Node's built-in grew the same EPERM/EBUSY retry behavior it
470
+ // existed for).
471
+ fs.rm(
472
+ convertToOSPath(path),
473
+ { recursive: true, force: true, maxRetries: 3 },
474
+ (err) => err ? reject(err) : resolve()
475
+ );
463
476
  });
464
477
  }
465
478
 
466
479
  // Like rm -r.
467
480
  export const rm_recursive = Profile("files.rm_recursive", async (path: string) => {
468
481
  try {
469
- rimraf.sync(convertToOSPath(path));
482
+ fs.rmSync(convertToOSPath(path), { recursive: true, force: true });
470
483
  } catch (e: any) {
471
484
  if ((e.code === "ENOTEMPTY" ||
472
485
  e.code === "EPERM")) {
@@ -1734,7 +1747,7 @@ export const rename = isWindowsLikeFilesystem() ? function (from: string, to: st
1734
1747
  // Despite previous failures, the top-level destination directory
1735
1748
  // may have been successfully created, so we must remove it to
1736
1749
  // avoid moving the source file *into* the destination directory.
1737
- rimraf.sync(osTo);
1750
+ fs.rmSync(osTo, { recursive: true, force: true });
1738
1751
  wrappedRename(from, to);
1739
1752
  resolve();
1740
1753
  } catch (err: any) {
@@ -1,6 +1,15 @@
1
1
  ///
2
2
  /// utility functions for dealing with urls and http
3
3
  ///
4
+ /// Implemented on undici's fetch (the engine behind Node's own global
5
+ /// fetch) rather than the long-deprecated `request` package -- request
6
+ /// dragged har-validator and uuid@3 (both deprecation-flagged on every
7
+ /// consumer install) and has been unmaintained since 2020. The public
8
+ /// surface here (request/getUrl/getUrlWithResuming and their option
9
+ /// shapes) is preserved for the nine in-tree callers; see the audit in
10
+ /// the git history of this rewrite for exactly which options each
11
+ /// caller uses.
12
+ ///
4
13
 
5
14
  var os = require('os');
6
15
  var util = require('util');
@@ -38,6 +47,13 @@ class ConcatStream extends Writable {
38
47
  return this.chunks[0];
39
48
  }
40
49
 
50
+ // A resumed download whose server ignored our Range header restarts
51
+ // from byte 0 -- drop what we had so we don't concatenate duplicates.
52
+ reset() {
53
+ this.chunks = [];
54
+ this.size = 0;
55
+ }
56
+
41
57
  end(force = false) {
42
58
  // Override the Writable#end method to ignore any .end() calls for
43
59
  // this stream, since we likely want to resume the download later.
@@ -47,51 +63,6 @@ class ConcatStream extends Writable {
47
63
  }
48
64
  }
49
65
 
50
- // Helper that tracks bytes written to a writable
51
- var WritableWithProgress = function (writable, listener) {
52
- var self = this;
53
- self._inner = writable;
54
- self._listener = listener;
55
- };
56
-
57
- Object.assign(WritableWithProgress.prototype, {
58
- write: function (chunk, encoding, callback) {
59
- var self = this;
60
- self._listener(chunk.length, false);
61
- return self._inner.write(chunk, encoding);
62
- },
63
-
64
- end: function (chunk, encoding, callback) {
65
- var self = this;
66
- self._listener(chunk ? chunk.length : 0, true);
67
- return self._inner.end(chunk, encoding);
68
- },
69
-
70
- _progress: function (n, done) {
71
- var self = this;
72
-
73
- var state = self._state;
74
- state.current += n;
75
- if (done) {
76
- state.current.done = true;
77
- }
78
- self.progress.reportProgress(state);
79
- },
80
-
81
- on: function (name, callback) {
82
- return this._inner.on(name, callback);
83
- },
84
-
85
- once: function () {
86
- return this._inner.once(...arguments);
87
- },
88
-
89
- emit: function () {
90
- return this._inner.emit(...arguments);
91
- }
92
- });
93
-
94
-
95
66
  // Compose a User-Agent header.
96
67
  var getUserAgent = function () {
97
68
  var version;
@@ -111,45 +82,70 @@ var getUserAgent = function () {
111
82
  os.platform(), os.type(), os.release(), os.arch());
112
83
  };
113
84
 
85
+ // Builds the undici dispatcher for this request: env-var proxy support
86
+ // (same env semantics the request package had, and the same explicit
87
+ // detection packages/socket-stream-client uses) and CAFILE support.
88
+ function buildDispatcher(url, undici) {
89
+ var proxy = process.env.HTTP_PROXY || process.env.http_proxy || null;
90
+ if (/^https/i.test(url)) {
91
+ proxy = process.env.HTTPS_PROXY || process.env.https_proxy || proxy;
92
+ }
93
+
94
+ var connect;
95
+ if (process.env.CAFILE) {
96
+ connect = { ca: files.readFile(process.env.CAFILE) };
97
+ }
98
+
99
+ if (proxy) {
100
+ return new undici.ProxyAgent(
101
+ connect ? { uri: proxy, connect } : { uri: proxy }
102
+ );
103
+ }
104
+ if (connect) {
105
+ return new undici.Agent({ connect });
106
+ }
107
+ return undefined;
108
+ }
109
+
110
+ // Wraps a Node Readable request body in an async generator that reports
111
+ // upload progress per chunk.
112
+ async function* streamWithProgress(bodyStream, onChunk) {
113
+ for await (const chunk of bodyStream) {
114
+ onChunk(chunk.length);
115
+ yield chunk;
116
+ }
117
+ }
114
118
 
115
119
  var httpHelpers = exports;
116
120
  Object.assign(exports, {
117
121
  getUserAgent: getUserAgent,
118
122
 
119
- // A wrapper around request with the following improvements:
123
+ // A fetch-based replacement for the old request-package wrapper, with
124
+ // the same contract:
120
125
  //
121
- // - It will respect proxy environment variables if present
122
- // (HTTP_PROXY or HTTPS_PROXY as appropriate).
126
+ // - Respects HTTP_PROXY/HTTPS_PROXY, sets a Meteor User-Agent, honors
127
+ // $CAFILE for a custom CA.
123
128
  //
124
- // - It will set a reasonable User-Agent header.
129
+ // - Never follows redirects (fetch redirect: "manual"): redirects
130
+ // don't segregate cookies/headers by origin, and callers like the
131
+ // OAuth flow in auth.js depend on seeing the 302 themselves.
125
132
  //
126
- // - If you omit the callback it will run synchronously. The return
127
- // value will be an object with keys 'response' and 'body' (with
128
- // the same meaning as the arguments to request's normal
129
- // callback), or it will throw.
133
+ // - Options: url (or a bare url string instead of the object), method,
134
+ // headers, qs (object appended to the url as a query string),
135
+ // json: true (parse the response body as JSON), encoding
136
+ // (null => body is a Buffer; anything else => utf8 string, the
137
+ // default), bodyStream (+ bodyStreamLength) for a streamed request
138
+ // body, outputStream to stream the response body into (body is not
139
+ // buffered then), progress (a progress tracker; incompatible with
140
+ // callback), timeout (idle ms between response chunks; default 60s,
141
+ // disable by passing a non-positive/non-number), useSessionHeader /
142
+ // useAuthHeader for the Meteor services session file.
130
143
  //
131
- // - If Set-Cookie headers are present on the response, *and* you
132
- // are using a callback, it will parse the cookies and include
133
- // them as a setCookie attribute on the object passed to the
134
- // callback. setCookie is a simple map from cookie name to cookie
135
- // value. If you want expiration time and attributes you'll have
136
- // to parse it yourself. If there are multiple Set-Cookie headers
137
- // for the same cookie it is unspecified which one you'll get.
138
- //
139
- // - You can provide a 'bodyStream' option which is a stream that
140
- // will be used for the body of the request.
141
- //
142
- // - For authenticated Meteor Software services, you can set the
143
- // 'useSessionHeader' and/or 'useAuthHeader' options (to true) to
144
- // send X-Meteor-Session/X-Meteor-Auth headers using values from
145
- // the session file.
146
- //
147
- // - forceSSL is always set to true. Always. And followRedirect is
148
- // set to false since it doesn't understand origins (see comment
149
- // in implementation).
150
- //
151
- // - An optional options.onRequest callback may be provided if the
152
- // caller desires access to the request object.
144
+ // - Without a callback, returns a Promise of { response, body,
145
+ // setCookie }. response is a plain object: { statusCode, headers
146
+ // (lowercased plain object), request: { href } } -- the subset of
147
+ // the old request response our callers read. With a callback, calls
148
+ // callback(err, response, body) instead.
153
149
  //
154
150
  // NB: With useSessionHeader and useAuthHeader, this function will
155
151
  // read *and possibly write to* the session file, so if you are
@@ -163,75 +159,41 @@ Object.assign(exports, {
163
159
  options = _.clone(urlOrOptions);
164
160
  }
165
161
 
166
- var outputStream;
167
- if (_.has(options, 'outputStream')) {
168
- outputStream = options.outputStream;
169
- delete options.outputStream;
170
- }
171
-
172
- var bodyStream;
173
- if (_.has(options, 'bodyStream')) {
174
- bodyStream = options.bodyStream;
175
- delete options.bodyStream;
176
- }
162
+ var outputStream = options.outputStream;
163
+ var bodyStream = options.bodyStream;
177
164
 
178
165
  // Body stream length for progress
179
166
  var bodyStreamLength = 0;
180
167
  if (_.has(options, 'bodyStreamLength')) {
181
168
  bodyStreamLength = options.bodyStreamLength;
182
- delete options.bodyStreamLength;
183
- } else {
169
+ } else if (bodyStream) {
184
170
  // Guess the body stream length as 1MB
185
171
  // Hopefully if it's much bigger the caller will set it
186
172
  // If it is much small, we will pleasantly surprise the user!
187
- if (bodyStream) {
188
- bodyStreamLength = 1024 * 1024;
189
- }
173
+ bodyStreamLength = 1024 * 1024;
190
174
  }
191
175
 
192
176
  // Response length for progress
193
177
  var responseLength = 128 * 1024;
194
178
  if (_.has(options, 'responseLength')) {
195
179
  responseLength = options.responseLength;
196
- delete options.responseLength;
197
180
  }
198
181
 
199
- var progress = null;
200
- if (_.has(options, 'progress')) {
201
- progress = options.progress;
202
- delete options.progress;
203
-
204
- if (callback) {
205
- throw new Error("Not safe to use progress with callback");
206
- }
182
+ var progress = options.progress || null;
183
+ if (progress && callback) {
184
+ throw new Error("Not safe to use progress with callback");
207
185
  }
208
186
 
209
- options.headers = Object.assign({
187
+ var headers = Object.assign({
210
188
  'User-Agent': getUserAgent()
211
189
  }, options.headers || {});
212
190
 
213
- // This should never, ever be false, or else why are you using SSL?
214
- options.forceSSL = true;
215
- if (process.env.CAFILE) {
216
- options.ca = files.readFile(process.env.CAFILE);
217
- }
218
-
219
- // followRedirect is very dangerous because request does not
220
- // appear to segregate cookies by origin, so any cookies (and
221
- // apparently headers as well, eg X-Meteor-Auth) sent on the
222
- // original request could get forwarded to an unexpected domain in
223
- // a redirect. This is almost certainly not something you ever
224
- // want.
225
- options.followRedirect = false;
226
-
227
191
  var useSessionHeader = options.useSessionHeader;
228
- delete options.useSessionHeader;
229
192
  var useAuthHeader = options.useAuthHeader;
230
- delete options.useAuthHeader;
231
193
  if (useSessionHeader || useAuthHeader) {
232
194
  var sessionHeader = auth.getSessionId(config.getAccountsDomain());
233
195
  if (sessionHeader) {
234
- options.headers['X-Meteor-Session'] = sessionHeader;
196
+ headers['X-Meteor-Session'] = sessionHeader;
235
197
  }
236
198
  if (callback) {
237
199
  throw new Error("session header can't be used with callback");
@@ -240,170 +202,197 @@ Object.assign(exports, {
240
202
  if (useAuthHeader) {
241
203
  var authHeader = auth.getSessionToken(config.getAccountsDomain());
242
204
  if (authHeader) {
243
- options.headers['X-Meteor-Auth'] = authHeader;
205
+ headers['X-Meteor-Auth'] = authHeader;
244
206
  }
245
207
  }
246
208
 
247
- var promise;
248
- if (! callback) {
249
- promise = new Promise(function (resolve, reject) {
250
- callback = function (err, response, body) {
251
- if (err) {
252
- reject(err);
253
- return;
254
- }
255
-
256
- var setCookie = {};
257
- _.each(response.headers["set-cookie"] || [], function (h) {
258
- var match = h.match(/^([^=\s]+)=([^;\s]+)/);
259
- if (match) {
260
- setCookie[match[1]] = match[2];
261
- }
262
- });
263
-
264
- if (useSessionHeader && _.has(response.headers, "x-meteor-session")) {
265
- auth.setSessionId(config.getAccountsDomain(),
266
- response.headers['x-meteor-session']);
267
- }
209
+ // 60 seconds of idle time between response chunks; disable by
210
+ // passing anything other than a positive number, e.g. {timeout: null}.
211
+ var idleTimeoutMs = 60 * 1000 * timeoutScaleFactor;
212
+ if (_.has(options, "timeout")) {
213
+ idleTimeoutMs =
214
+ (typeof options.timeout === "number" && options.timeout > 0)
215
+ ? options.timeout
216
+ : null;
217
+ }
268
218
 
269
- resolve({
270
- response: response,
271
- body: body,
272
- setCookie: setCookie
273
- });
274
- };
219
+ var url = options.url;
220
+ if (options.qs && Object.keys(options.qs).length > 0) {
221
+ var search = new URLSearchParams();
222
+ Object.entries(options.qs).forEach(function ([k, v]) {
223
+ if (v !== undefined && v !== null) {
224
+ search.append(k, String(v));
225
+ }
275
226
  });
227
+ var qsString = search.toString();
228
+ if (qsString) {
229
+ url += (url.includes('?') ? '&' : '?') + qsString;
230
+ }
276
231
  }
277
232
 
278
- // try to get proxy from environment.
279
- // similar code is in packages/ddp/stream_client_nodejs.js
280
- var proxy = process.env.HTTP_PROXY || process.env.http_proxy || null;
281
- // if we're going to an https url, try the https_proxy env variable first.
282
- if (/^https/i.test(options.url)) {
283
- proxy = process.env.HTTPS_PROXY || process.env.https_proxy || proxy;
284
- }
285
- if (proxy && !options.proxy) {
286
- options.proxy = proxy;
233
+ var totalProgress = {
234
+ current: 0,
235
+ end: bodyStreamLength + responseLength,
236
+ done: false,
237
+ };
238
+ function reportProgress() {
239
+ if (progress && !totalProgress.done) {
240
+ progress.reportProgress(totalProgress);
241
+ }
287
242
  }
288
243
 
289
- if (! _.has(options, "timeout")) {
290
- // 60 seconds for timeout between initial response headers and data,
291
- // and between chunks of data while reading the rest of the response.
292
- options.timeout = 60 * 1000 * timeoutScaleFactor;
293
- } else if (! (typeof options.timeout === "number" &&
294
- options.timeout > 0)) {
295
- // The timeout can be disabled by passing anything other than a
296
- // positive number, e.g. { timeout: null }.
297
- delete options.timeout;
298
- }
244
+ Console.debug("Doing HTTP request: ", options.method || 'GET', url);
299
245
 
300
- let onRequest;
301
- if (_.has(options, "onRequest")) {
302
- onRequest = options.onRequest;
303
- delete options.onRequest;
304
- }
246
+ // undici, not Node's global fetch: we need dispatcher control
247
+ // (proxy/CA) and the same fetch implementation regardless of what
248
+ // the embedding process did to globalThis.fetch.
249
+ var undici = require('undici');
305
250
 
306
- // request is the most heavy-weight of the tool's npm dependencies; don't
307
- // require it until we definitely need it.
308
- Console.debug("Doing HTTP request: ", options.method || 'GET', options.url);
309
- var request = require('request');
310
- var req = request(options, function (error, response, body) {
311
- if (! error &&
312
- response &&
313
- (typeof body === "string" ||
314
- Buffer.isBuffer(body))) {
315
- const contentLength = Number(response.headers["content-length"]);
316
- const actualLength = Buffer.byteLength(body);
317
-
318
- if (contentLength > 0 &&
319
- actualLength < contentLength) {
320
- error = new Error(
321
- "Expected " + contentLength + " bytes in request body " +
322
- "but received only " + actualLength);
251
+ var promise = (async () => {
252
+ var controller = new AbortController();
253
+ var idleTimer = null;
254
+ function armIdleTimer() {
255
+ if (idleTimeoutMs == null) {
256
+ return;
257
+ }
258
+ if (idleTimer) {
259
+ clearTimeout(idleTimer);
260
+ }
261
+ idleTimer = setTimeout(function () {
262
+ controller.abort(new Error(
263
+ "HTTP request idle-timed out after " + idleTimeoutMs + "ms: " + url
264
+ ));
265
+ }, idleTimeoutMs);
266
+ // Don't hold the event loop open just for the timeout.
267
+ if (idleTimer.unref) {
268
+ idleTimer.unref();
323
269
  }
324
270
  }
325
271
 
326
- return callback.call(this, error, response, body);
327
- });
328
-
329
- if (_.isFunction(onRequest)) {
330
- onRequest(req);
331
- }
332
-
333
- var totalProgress = { current: 0, end: bodyStreamLength + responseLength, done: false };
334
-
335
- if (bodyStream) {
336
- var dest = req;
337
- if (progress) {
338
- dest = new WritableWithProgress(dest, function (n, done) {
339
- if (!totalProgress.done) {
340
- totalProgress.current += n;
341
- progress.reportProgress(totalProgress);
342
- }
272
+ var fetchBody;
273
+ if (bodyStream) {
274
+ var uploaded = 0;
275
+ fetchBody = streamWithProgress(bodyStream, function (n) {
276
+ uploaded += n;
277
+ totalProgress.current = Math.min(uploaded, bodyStreamLength);
278
+ reportProgress();
343
279
  });
280
+ } else if (_.has(options, 'body')) {
281
+ fetchBody = options.body;
344
282
  }
345
- bodyStream.pipe(dest);
346
- }
347
283
 
348
- if (outputStream) {
349
- req.pipe(outputStream);
350
- }
351
-
352
- if (progress) {
353
- httpHelpers._addProgressEvents(req);
354
- req.on('progress', function (state) {
355
- if (!totalProgress.done) {
356
- totalProgress.current = bodyStreamLength + state.current;
357
- totalProgress.end = bodyStreamLength + state.end;
358
- totalProgress.done = state.done;
284
+ var res;
285
+ try {
286
+ armIdleTimer();
287
+ res = await undici.fetch(url, {
288
+ method: options.method || 'GET',
289
+ headers: headers,
290
+ body: fetchBody,
291
+ // See the redirect comment in this function's doc block.
292
+ redirect: 'manual',
293
+ signal: controller.signal,
294
+ dispatcher: buildDispatcher(url, undici),
295
+ ...(bodyStream ? { duplex: 'half' } : null),
296
+ });
359
297
 
360
- progress.reportProgress(totalProgress);
298
+ // Response shape: the subset of the old request-package response
299
+ // object our callers actually read.
300
+ var headerObj = {};
301
+ res.headers.forEach(function (value, key) {
302
+ headerObj[key.toLowerCase()] = value;
303
+ });
304
+ var setCookieList = typeof res.headers.getSetCookie === 'function'
305
+ ? res.headers.getSetCookie()
306
+ : [];
307
+ if (setCookieList.length) {
308
+ headerObj['set-cookie'] = setCookieList;
361
309
  }
362
- });
363
- }
310
+ var response = {
311
+ statusCode: res.status,
312
+ headers: headerObj,
313
+ request: { href: url },
314
+ };
364
315
 
365
- if (promise) {
366
- return promise.finally(() => progress && progress.reportProgressDone());
367
- } else {
368
- return req;
369
- }
370
- },
316
+ // Stream the response body: to outputStream when given (body
317
+ // stays unbuffered), else into memory.
318
+ var collected = outputStream ? null : [];
319
+ var received = 0;
320
+ if (res.body) {
321
+ for await (const chunk of res.body) {
322
+ armIdleTimer();
323
+ var buf = Buffer.from(chunk);
324
+ received += buf.length;
325
+ if (outputStream) {
326
+ outputStream.write(buf);
327
+ } else {
328
+ collected.push(buf);
329
+ }
330
+ totalProgress.current = bodyStreamLength + received;
331
+ var contentLength = Number(headerObj['content-length']);
332
+ totalProgress.end = bodyStreamLength +
333
+ (contentLength > 0 ? contentLength : Math.max(received, responseLength));
334
+ reportProgress();
335
+ }
336
+ }
337
+ totalProgress.done = true;
338
+
339
+ var body;
340
+ if (!outputStream) {
341
+ var bodyBuffer = Buffer.concat(collected);
342
+ var expected = Number(headerObj['content-length']);
343
+ if (expected > 0 && bodyBuffer.length < expected) {
344
+ throw new Error(
345
+ "Expected " + expected + " bytes in request body " +
346
+ "but received only " + bodyBuffer.length);
347
+ }
348
+ body = options.encoding === null
349
+ ? bodyBuffer
350
+ : bodyBuffer.toString('utf8');
351
+ if (options.json) {
352
+ body = body.length ? JSON.parse(body) : undefined;
353
+ }
354
+ }
371
355
 
372
- // Adds progress callbacks to a request
373
- // Based on request-progress
374
- _addProgressEvents: function (request) {
375
- var state = {};
356
+ var setCookie = {};
357
+ setCookieList.forEach(function (h) {
358
+ var match = h.match(/^([^=\s]+)=([^;\s]+)/);
359
+ if (match) {
360
+ setCookie[match[1]] = match[2];
361
+ }
362
+ });
376
363
 
377
- var emitProgress = function () {
378
- request.emit('progress', state);
379
- };
364
+ if (useSessionHeader && _.has(headerObj, "x-meteor-session")) {
365
+ auth.setSessionId(config.getAccountsDomain(),
366
+ headerObj['x-meteor-session']);
367
+ }
380
368
 
381
- request
382
- .on('response', function (response) {
383
- state.end = undefined;
384
- state.done = false;
385
- state.current = 0;
386
- var contentLength = response.headers['content-length'];
387
- if (contentLength) {
388
- state.end = Number(contentLength);
369
+ return { response: response, body: body, setCookie: setCookie };
370
+ } finally {
371
+ if (idleTimer) {
372
+ clearTimeout(idleTimer);
389
373
  }
390
- emitProgress();
391
- })
392
- .on('data', function (data) {
393
- state.current += data.length;
394
- emitProgress();
395
- })
396
- .on('end', function (data) {
397
- state.done = true;
398
- emitProgress();
399
- });
374
+ if (progress) {
375
+ progress.reportProgressDone();
376
+ }
377
+ }
378
+ })();
379
+
380
+ if (callback) {
381
+ promise.then(
382
+ function (result) {
383
+ callback(null, result.response, result.body);
384
+ },
385
+ function (err) {
386
+ callback(err);
387
+ }
388
+ );
389
+ return undefined;
390
+ }
391
+ return promise;
400
392
  },
401
393
 
402
- // A synchronous wrapper around request(...) that returns the response "body"
403
- // or throws.
404
- //
405
- // (This has gone through a few refactors and it might be possible
406
- // to fully roll it into httpHelpers.request() at this point.)
394
+ // Returns the response body or throws; network-level failures become
395
+ // files.OfflineError.
407
396
  getUrl: async function (urlOrOptions) {
408
397
  try {
409
398
  var result = await httpHelpers.request(urlOrOptions);
@@ -426,10 +415,13 @@ Object.assign(exports, {
426
415
 
427
416
  // More or less as above, except with support for multiple attempts per
428
417
  // request and resuming on retries. This means if the connection is bad,
429
- // we can sometimes complete a request, even if each individual attempt fails.
430
- // We only use this for package downloads. In theory we could use it for
431
- // all requests but that seems like overkill and it isn't well tested in
432
- // other scenarioes.
418
+ // we can sometimes complete a request, even if each individual attempt
419
+ // fails. We only use this for package downloads.
420
+ //
421
+ // (Historical note: the request-package version of this had retry
422
+ // logic that could never fire -- its try/catch wrapped a Promise
423
+ // construction, so async rejections bypassed it entirely. This
424
+ // version actually retries.)
433
425
  async getUrlWithResuming(urlOrOptions) {
434
426
  const options = _.isObject(urlOrOptions) ? _.clone(urlOrOptions) : {
435
427
  url: urlOrOptions,
@@ -446,50 +438,56 @@ Object.assign(exports, {
446
438
  const masterProgress = options.progress;
447
439
  const outputStream = new ConcatStream();
448
440
 
449
- function attempt(triesRemaining = maxAttempts, startAt = 0) {
441
+ let triesRemaining = maxAttempts;
442
+ let result;
443
+ for (;;) {
444
+ const startAt = outputStream.size;
445
+ const attemptOptions = Object.assign({}, options, { outputStream });
450
446
  if (startAt > 0) {
451
- options.headers = {
447
+ attemptOptions.headers = {
452
448
  ...options.headers,
453
- Range: `bytes=${startAt}-`
449
+ Range: `bytes=${startAt}-`,
454
450
  };
455
451
  }
456
-
457
- if (masterProgress &&
458
- masterProgress.addChildTask) {
459
- options.progress = masterProgress.addChildTask({
460
- title: masterProgress.title
452
+ if (masterProgress && masterProgress.addChildTask) {
453
+ attemptOptions.progress = masterProgress.addChildTask({
454
+ title: masterProgress.title,
461
455
  });
462
456
  }
463
457
 
464
458
  try {
465
- return Promise.resolve(httpHelpers.request({
466
- outputStream,
467
- ...options,
468
- }));
469
-
459
+ result = await httpHelpers.request(attemptOptions);
460
+ // A server that ignored our Range restarted from byte 0 -- the
461
+ // stream got the full body appended after the partial prefix.
462
+ // Detect and keep only the fresh copy.
463
+ if (startAt > 0 && result.response.statusCode === 200) {
464
+ const full = outputStream.getBuffer().slice(startAt);
465
+ outputStream.reset();
466
+ outputStream.write(full);
467
+ }
468
+ break;
470
469
  } catch (e) {
471
470
  const size = outputStream.size;
472
- const useTry = size === startAt;
473
- const change = size - startAt;
471
+ const madeProgress = size > startAt;
474
472
 
475
- if (!useTry || triesRemaining > 0) {
476
- if (useTry) {
477
- Console.debug(`Request failed, ${triesRemaining - 1} attempts left`);
473
+ if (madeProgress || triesRemaining > 1) {
474
+ if (!madeProgress) {
475
+ triesRemaining -= 1;
476
+ Console.debug(`Request failed, ${triesRemaining} attempts left`);
478
477
  } else {
479
- Console.debug(`Request failed after ${change} bytes, retrying`);
478
+ Console.debug(
479
+ `Request failed after ${size - startAt} bytes, retrying`);
480
480
  }
481
-
482
- return new Promise(
483
- resolve => setTimeout(resolve, retryDelaySecs * 1000)
484
- ).then(() => attempt(triesRemaining - (useTry ? 1 : 0), size));
481
+ await new Promise(
482
+ resolve => setTimeout(resolve, retryDelaySecs * 1000));
483
+ continue;
485
484
  }
486
485
 
487
486
  Console.debug(`Request failed ${maxAttempts} times: failing`);
488
- return Promise.reject(new files.OfflineError(e));
487
+ throw new files.OfflineError(e);
489
488
  }
490
489
  }
491
490
 
492
- const result = await attempt();
493
491
  const response = result.response;
494
492
  if (response.statusCode >= 400 && response.statusCode < 600) {
495
493
  const href = response.request.href;