@maka/meteor-sdk 0.2.7 → 0.2.9

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 (35) hide show
  1. package/README.md +26 -0
  2. package/isopackets/combined/isopacket-buildinfo.json +1 -1
  3. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/.npm-shrinkwrap.json +3 -3
  4. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/.package-lock.json +4 -4
  5. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/full-chromium-versions.js +2 -1
  6. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/full-chromium-versions.json +1 -1
  7. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/full-versions.js +1 -0
  8. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/full-versions.json +1 -1
  9. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/package.json +1 -1
  10. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/versions.js +1 -0
  11. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/versions.json +1 -1
  12. package/isopackets/combined/npm/node_modules/meteor/boilerplate-generator/node_modules/.package-lock.json +1 -1
  13. package/isopackets/combined/npm/node_modules/meteor/constraint-solver/node_modules/.package-lock.json +1 -1
  14. package/isopackets/combined/npm/node_modules/meteor/ddp-client/node_modules/.package-lock.json +1 -1
  15. package/isopackets/combined/npm/node_modules/meteor/ecmascript/node_modules/.package-lock.json +1 -1
  16. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime/node_modules/.package-lock.json +1 -1
  17. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime-client/node_modules/.package-lock.json +1 -1
  18. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime-server/node_modules/.package-lock.json +1 -1
  19. package/isopackets/combined/npm/node_modules/meteor/fetch/node_modules/.package-lock.json +1 -1
  20. package/isopackets/combined/npm/node_modules/meteor/inter-process-messaging/node_modules/.package-lock.json +1 -1
  21. package/isopackets/combined/npm/node_modules/meteor/logging/node_modules/.package-lock.json +1 -1
  22. package/isopackets/combined/npm/node_modules/meteor/logic-solver/node_modules/.package-lock.json +1 -1
  23. package/isopackets/combined/npm/node_modules/meteor/meteor/node_modules/.package-lock.json +1 -1
  24. package/isopackets/combined/npm/node_modules/meteor/modules/node_modules/.package-lock.json +1 -1
  25. package/isopackets/combined/npm/node_modules/meteor/modules-runtime/node_modules/.package-lock.json +1 -1
  26. package/isopackets/combined/npm/node_modules/meteor/npm-mongo/node_modules/.package-lock.json +1 -1
  27. package/isopackets/combined/npm/node_modules/meteor/package-version-parser/node_modules/.package-lock.json +1 -1
  28. package/isopackets/combined/npm/node_modules/meteor/promise/node_modules/.package-lock.json +1 -1
  29. package/isopackets/combined/npm/node_modules/meteor/react-fast-refresh/node_modules/.package-lock.json +1 -1
  30. package/isopackets/combined/npm/node_modules/meteor/socket-stream-client/node_modules/.package-lock.json +1 -1
  31. package/package.json +1 -1
  32. package/src/bootstrap.js +26 -0
  33. package/src/bundle.js +5 -0
  34. package/src/project.js +40 -0
  35. package/tools/isobuild/meteor-npm.js +412 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-d6edvk3v9nl",
2
+ "name": "package-new-mt7pq41l8d9",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-8e8v01oivjc",
2
+ "name": "package-new-i4l2akdfgip",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-3efvozmolln",
2
+ "name": "package-new-4dhnqh88rfe",
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.7",
3
+ "version": "0.2.9",
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": {
package/src/bootstrap.js CHANGED
@@ -120,6 +120,31 @@ function ensureMainModuleShim() {
120
120
  };
121
121
  }
122
122
 
123
+ // Kick off the persistent npm-deps cache prewarm (see tools/isobuild/
124
+ // meteor-npm.js): restore every bundled package's node_modules from
125
+ // ~/.meteor-npm-cache in parallel, so the first build after a fresh SDK
126
+ // install doesn't pay a serial `npm install` per package. Deliberately
127
+ // NOT awaited -- each restore holds the same cross-process lock
128
+ // updateDependencies() takes, so a build racing ahead of the prewarm
129
+ // simply waits on (or wins) the per-package lock; firing it before
130
+ // ensureRuntimeReady() lets even the isopacket builds overlap with it.
131
+ let npmDepsCachePrewarm = null;
132
+ function startNpmDepsCachePrewarm() {
133
+ if (npmDepsCachePrewarm) {
134
+ return;
135
+ }
136
+ try {
137
+ const meteorNpm = require("../tools/isobuild/meteor-npm.js");
138
+ const path = require("path");
139
+ npmDepsCachePrewarm = Promise.resolve(
140
+ meteorNpm.prewarmNpmDepsCache(path.resolve(__dirname, "..", "packages"))
141
+ ).catch(() => {});
142
+ } catch (e) {
143
+ // Best-effort: the build's own per-package install path still works.
144
+ npmDepsCachePrewarm = Promise.resolve();
145
+ }
146
+ }
147
+
123
148
  // Idempotent: safe to call from every public SDK entry point. The actual
124
149
  // Babel require() hook installed by install-babel.js is itself naturally
125
150
  // idempotent too (Node's own require() cache means requiring the same
@@ -152,6 +177,7 @@ async function bootstrap(options) {
152
177
  require("../tools/tool-env/install-babel.js");
153
178
  ensureAsyncLocalStorage();
154
179
  consoleAdapter.patch();
180
+ startNpmDepsCachePrewarm();
155
181
  await ensureRuntimeReady(options && options.offline);
156
182
  bootstrapped = true;
157
183
  }
package/src/bundle.js CHANGED
@@ -45,6 +45,11 @@ async function bundle(project, options) {
45
45
  throwIfHasMessages(result.errors);
46
46
  }
47
47
 
48
+ // Background npm-deps cache writes (see meteor-npm.js) ran concurrently
49
+ // with the build; wait for them here so a build-and-exit process still
50
+ // gets its cache populated. Usually a no-op by this point.
51
+ await require("../tools/isobuild/meteor-npm.js").flushNpmDepsCacheWrites();
52
+
48
53
  return {
49
54
  outputPath: options.outputPath,
50
55
  starManifest: result.starManifest,
package/src/project.js CHANGED
@@ -65,6 +65,19 @@ async function loadProject(projectDir) {
65
65
  projectDir: standardProjectDir,
66
66
  });
67
67
 
68
+ // Second-stage npm-deps prewarm (bootstrap.js fired the restore-only
69
+ // first stage): now that an app is known, its .meteor/versions names
70
+ // the exact core-package set the build will need, so cache MISSES can
71
+ // be npm-installed too -- in parallel (small pool; concurrent npm
72
+ // processes contend on npm's shared content cache) and BEFORE the
73
+ // build's serial per-package walk, which also keeps those installs'
74
+ // .npm churn from re-firing the app runner's file watcher mid-build.
75
+ // Un-awaited: every prewarm operation holds the same cross-process
76
+ // per-package lock updateDependencies() takes, so the build below
77
+ // simply waits on (or wins) each package's lock. Best-effort by
78
+ // design -- on any failure the build installs serially as before.
79
+ startAppNpmDepsPrewarm(standardProjectDir);
80
+
68
81
  await runCaptured("preparing project", () =>
69
82
  projectContext.prepareProjectForBuild()
70
83
  );
@@ -72,4 +85,31 @@ async function loadProject(projectDir) {
72
85
  return new Project(projectContext);
73
86
  }
74
87
 
88
+ function startAppNpmDepsPrewarm(standardProjectDir) {
89
+ try {
90
+ const files = require("../tools/fs/files");
91
+ const versionsFile = files.pathJoin(
92
+ standardProjectDir, ".meteor", "versions");
93
+ // name@version lines; author:package names never match the bundled
94
+ // core-package directory names, so no special-casing needed. A
95
+ // missing versions file (brand-new app) just skips this stage.
96
+ const names = files.readFile(versionsFile, "utf8")
97
+ .split("\n")
98
+ .map((line) => line.trim().split("@")[0])
99
+ .filter(Boolean);
100
+ if (!names.length) {
101
+ return;
102
+ }
103
+ const meteorNpm = require("../tools/isobuild/meteor-npm.js");
104
+ Promise.resolve(
105
+ meteorNpm.prewarmNpmDepsCache(
106
+ path.resolve(__dirname, "..", "packages"),
107
+ { onlyPackages: names, installOnMiss: true }
108
+ )
109
+ ).catch(() => {});
110
+ } catch (e) {
111
+ // Best-effort: the build's own per-package install path still works.
112
+ }
113
+ }
114
+
75
115
  module.exports = { loadProject, Project };
@@ -676,6 +676,392 @@ var makeNewPackageNpmDir = function (newPackageNpmDir) {
676
676
  ''/*git diff complains without trailing newline*/].join('\n'));
677
677
  };
678
678
 
679
+ // --- Persistent npm-deps cache ---------------------------------------
680
+ //
681
+ // A package's .npm/package/node_modules is fully determined by three
682
+ // things: the npm-shrinkwrap.json content, the Node compatibility
683
+ // version, and the platform/arch. Yet every fresh install of the tool
684
+ // (e.g. `npm i -g` of a new SDK version) starts with empty node_modules
685
+ // for every bundled package and pays a serial `npm install` per package
686
+ // on the first build. Since shrinkwraps rarely change between releases,
687
+ // keep a per-user cache of completed node_modules trees keyed by a hash
688
+ // of those three inputs, and restore by directory copy instead of
689
+ // spawning npm.
690
+ //
691
+ // Everything here is best-effort: any failure falls through to the
692
+ // normal npm install path, and a stale/corrupt restore is caught by the
693
+ // existing shrinkwrap-vs-installed subtree comparison (which then
694
+ // triggers a real reinstall over it).
695
+ //
696
+ // METEOR_PACKAGE_NPM_CACHE: unset -> enabled at ~/.meteor-npm-cache;
697
+ // "0"/"false"/"no"/"off" -> disabled; any other value -> cache root.
698
+
699
+ function npmDepsCacheDisabled() {
700
+ const v = process.env.METEOR_PACKAGE_NPM_CACHE;
701
+ return typeof v === "string" && /^\s*(0|false|no|off)\s*$/i.test(v);
702
+ }
703
+
704
+ function npmDepsCacheRoot() {
705
+ const v = process.env.METEOR_PACKAGE_NPM_CACHE;
706
+ if (v && ! npmDepsCacheDisabled()) {
707
+ return files.convertToStandardPath(v);
708
+ }
709
+ return files.pathJoin(
710
+ files.convertToStandardPath(require("os").homedir()),
711
+ ".meteor-npm-cache"
712
+ );
713
+ }
714
+
715
+ // JSON.stringify with deterministically sorted object keys, so the hash
716
+ // is insensitive to key order, whitespace, and line endings of the
717
+ // on-disk shrinkwrap file.
718
+ function stableStringify(value) {
719
+ if (Array.isArray(value)) {
720
+ return "[" + value.map(stableStringify).join(",") + "]";
721
+ }
722
+ if (value && typeof value === "object") {
723
+ return "{" + Object.keys(value).sort().map(
724
+ key => JSON.stringify(key) + ":" + stableStringify(value[key])
725
+ ).join(",") + "}";
726
+ }
727
+ return JSON.stringify(value);
728
+ }
729
+
730
+ // Hash of the package's current npm-shrinkwrap.json plus everything
731
+ // else that determines node_modules content. Returns null if the
732
+ // shrinkwrap is missing or unparsable.
733
+ function npmDepsCacheKey(packageNpmDir) {
734
+ let shrinkwrap;
735
+ try {
736
+ shrinkwrap = JSON.parse(files.readFile(
737
+ files.pathJoin(packageNpmDir, "npm-shrinkwrap.json"), "utf8"));
738
+ } catch (e) {
739
+ return null;
740
+ }
741
+ return require("crypto").createHash("sha256").update(stableStringify({
742
+ shrinkwrap,
743
+ node: currentNodeCompatibilityVersion(),
744
+ platform: process.platform,
745
+ arch: process.arch,
746
+ })).digest("hex").slice(0, 32);
747
+ }
748
+
749
+ function npmDepsCacheEntryDir(packageName, key) {
750
+ // Meteor package names may contain characters that are not valid in
751
+ // paths everywhere (e.g. ":" on Windows); the hash disambiguates, so
752
+ // a lossy sanitization is fine.
753
+ const safeName = packageName.replace(/[^A-Za-z0-9._-]/g, "_");
754
+ return files.pathJoin(npmDepsCacheRoot(), safeName, key);
755
+ }
756
+
757
+ // Copy a cached node_modules into packageNpmDir (via temp dir + rename,
758
+ // so a crash never leaves a half-restored node_modules in place).
759
+ // Returns true on success. Callers must hold the npm package lock for
760
+ // packageNpmDir.
761
+ async function restoreNodeModulesFromNpmDepsCache(packageName, packageNpmDir) {
762
+ if (npmDepsCacheDisabled()) {
763
+ return false;
764
+ }
765
+ const key = npmDepsCacheKey(packageNpmDir);
766
+ if (! key) {
767
+ return false;
768
+ }
769
+ const cachedNodeModules = files.pathJoin(
770
+ npmDepsCacheEntryDir(packageName, key), "node_modules");
771
+ if (! files.exists(
772
+ files.pathJoin(cachedNodeModules, ".package-lock.json"))) {
773
+ return false;
774
+ }
775
+
776
+ // Sweep temp dirs left behind by a crashed earlier restore (safe: we
777
+ // hold the package lock).
778
+ try {
779
+ files.readdir(packageNpmDir).forEach(entry => {
780
+ if (entry.startsWith(".node_modules-from-cache-")) {
781
+ files.rm_recursive(files.pathJoin(packageNpmDir, entry));
782
+ }
783
+ });
784
+ } catch (e) {}
785
+
786
+ const nodeModulesDir = files.pathJoin(packageNpmDir, "node_modules");
787
+ const tmpDir = files.pathJoin(
788
+ packageNpmDir, ".node_modules-from-cache-" + utils.randomToken());
789
+ tmpDirs.push(tmpDir);
790
+ try {
791
+ await files.cp_r(cachedNodeModules, tmpDir, { preserveSymlinks: true });
792
+ await files.renameDirAlmostAtomically(tmpDir, nodeModulesDir);
793
+ dirtyNodeModulesDirectory(nodeModulesDir);
794
+ return true;
795
+ } catch (e) {
796
+ try {
797
+ if (files.exists(tmpDir)) {
798
+ await files.rm_recursive(tmpDir);
799
+ }
800
+ } catch (cleanupError) {}
801
+ return false;
802
+ } finally {
803
+ tmpDirs = _.without(tmpDirs, tmpDir);
804
+ }
805
+ }
806
+
807
+ // Cache writes run in the background (fire-and-forget with an atomic
808
+ // temp-dir + rename publish) so they never sit on the build's critical
809
+ // path. flushNpmDepsCacheWrites() lets one-shot processes (e.g. a
810
+ // build-and-exit embedder) wait for them before exiting; if a process
811
+ // dies first, the orphaned .tmp-* dir is swept by a later prune.
812
+ const pendingNpmDepsCacheWrites = [];
813
+
814
+ meteorNpm.flushNpmDepsCacheWrites = function () {
815
+ return Promise.allSettled(pendingNpmDepsCacheWrites);
816
+ };
817
+
818
+ function scheduleNpmDepsCachePopulate(packageName, packageNpmDir, key) {
819
+ if (npmDepsCacheDisabled()) {
820
+ return;
821
+ }
822
+ pendingNpmDepsCacheWrites.push(
823
+ populateNpmDepsCache(packageName, packageNpmDir, key).catch(e => {})
824
+ );
825
+ }
826
+
827
+ async function populateNpmDepsCache(packageName, packageNpmDir, key) {
828
+ key = key || npmDepsCacheKey(packageNpmDir);
829
+ if (! key) {
830
+ return;
831
+ }
832
+ const entryDir = npmDepsCacheEntryDir(packageName, key);
833
+ if (files.exists(entryDir)) {
834
+ return;
835
+ }
836
+ const packageCacheDir = files.pathDirname(entryDir);
837
+ files.mkdir_p(packageCacheDir);
838
+ const tmpDir = files.pathJoin(
839
+ packageCacheDir, ".tmp-" + utils.randomToken());
840
+ try {
841
+ await files.cp_r(
842
+ files.pathJoin(packageNpmDir, "node_modules"),
843
+ files.pathJoin(tmpDir, "node_modules"),
844
+ { preserveSymlinks: true }
845
+ );
846
+ files.rename(tmpDir, entryDir);
847
+ } catch (e) {
848
+ // Concurrent populate may have won the rename race, or the copy
849
+ // failed midway; either way discard our temp dir and move on.
850
+ try {
851
+ if (files.exists(tmpDir)) {
852
+ await files.rm_recursive(tmpDir);
853
+ }
854
+ } catch (cleanupError) {}
855
+ return;
856
+ }
857
+ pruneNpmDepsCache(packageCacheDir, entryDir);
858
+ }
859
+
860
+ // Keep the 3 newest entries per package (different keys = different
861
+ // shrinkwrap/node/platform combos) and sweep stale temp dirs, so the
862
+ // cache stays bounded without any global bookkeeping.
863
+ function pruneNpmDepsCache(packageCacheDir, keepDir) {
864
+ try {
865
+ const keepBase = files.pathBasename(keepDir);
866
+ const entries = [];
867
+ files.readdir(packageCacheDir).forEach(entry => {
868
+ const fullPath = files.pathJoin(packageCacheDir, entry);
869
+ if (entry.startsWith(".tmp-")) {
870
+ // Only sweep temp dirs that are clearly abandoned; a concurrent
871
+ // process may still be filling a fresh one.
872
+ try {
873
+ const ageMs = Date.now() - files.stat(fullPath).mtime.getTime();
874
+ if (ageMs > 60 * 60 * 1000) {
875
+ files.rm_recursive(fullPath);
876
+ }
877
+ } catch (e) {}
878
+ return;
879
+ }
880
+ if (entry === keepBase) {
881
+ return;
882
+ }
883
+ try {
884
+ entries.push({ fullPath, mtimeMs: files.stat(fullPath).mtime.getTime() });
885
+ } catch (e) {}
886
+ });
887
+ entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
888
+ entries.slice(2).forEach(entry => {
889
+ try {
890
+ files.rm_recursive(entry.fullPath);
891
+ } catch (e) {}
892
+ });
893
+ } catch (e) {}
894
+ }
895
+
896
+ // Install a package's npm deps purely from its shipped
897
+ // npm-shrinkwrap.json -- no evaluated Npm.depends needed, which is what
898
+ // makes prewarm-time (pre-package.js) installs possible. Mirrors
899
+ // updateExistingNpmDirectory's preserved-shrinkwrap branch exactly:
900
+ // mapped shrinkwrap + minimal package.json into a temp dir, `npm
901
+ // install`, then completeNpmDirectory (which also seeds the cache).
902
+ // The top-level dependency map is derived from the shrinkwrap itself
903
+ // (exact versions; resolved URLs for non-registry deps) -- a superset
904
+ // of the real Npm.depends, which npm accepts since every entry is
905
+ // pinned to what the lock installs. If the shipped shrinkwrap turns
906
+ // out stale relative to the package's actual Npm.depends, the build's
907
+ // own subtree verification catches it and reinstalls normally.
908
+ // Callers must hold the npm package lock for packageNpmDir.
909
+ async function preinstallNpmDepsFromShrinkwrap(packageName, packageNpmDir) {
910
+ const shrinkwrappedDependenciesTree =
911
+ getShrinkwrappedDependenciesTree(packageNpmDir);
912
+ const npmDependencies = treeToDependencies(
913
+ minimizeDependencyTree(shrinkwrappedDependenciesTree));
914
+ if (_.isEmpty(npmDependencies)) {
915
+ return false;
916
+ }
917
+ const cacheKey = npmDepsCacheKey(packageNpmDir);
918
+
919
+ const newPackageNpmDir =
920
+ convertColonsInPath(packageNpmDir) + '-prewarm-' + utils.randomToken();
921
+ tmpDirs.push(newPackageNpmDir);
922
+ try {
923
+ makeNewPackageNpmDir(newPackageNpmDir);
924
+
925
+ const mappedDependencies = Object.entries(
926
+ shrinkwrappedDependenciesTree.dependencies
927
+ ).reduce((acc, [name, info]) => {
928
+ return { ...acc, [`node_modules/${name}`]: info };
929
+ }, {});
930
+
931
+ files.writeFile(
932
+ files.pathJoin(newPackageNpmDir, 'npm-shrinkwrap.json'),
933
+ JSON.stringify({
934
+ ...shrinkwrappedDependenciesTree,
935
+ dependencies: mappedDependencies,
936
+ }, null, 2)
937
+ );
938
+ files.writeFile(
939
+ files.pathJoin(newPackageNpmDir, 'package.json'),
940
+ JSON.stringify({ dependencies: npmDependencies }, null, 2)
941
+ );
942
+
943
+ logUpdateDependencies(packageName, npmDependencies);
944
+ await installFromShrinkwrap(newPackageNpmDir);
945
+
946
+ files.unlink(files.pathJoin(newPackageNpmDir, 'npm-shrinkwrap.json'));
947
+ files.unlink(files.pathJoin(newPackageNpmDir, 'package.json'));
948
+
949
+ await completeNpmDirectory(
950
+ packageName, newPackageNpmDir, packageNpmDir, npmDependencies,
951
+ cacheKey);
952
+ return true;
953
+ } finally {
954
+ if (files.exists(newPackageNpmDir)) {
955
+ await files.rm_recursive_deferred(newPackageNpmDir);
956
+ }
957
+ tmpDirs = _.without(tmpDirs, newPackageNpmDir);
958
+ }
959
+ }
960
+
961
+ // Restore every restorable package under packagesDir from the cache, in
962
+ // parallel, before the (serial) per-package build pipeline reaches
963
+ // them. Purely filesystem-driven: a candidate is any package dir with a
964
+ // .npm/package/npm-shrinkwrap.json but no installed node_modules, so no
965
+ // package.js evaluation is needed. Safe to run concurrently with a
966
+ // build -- each restore holds the same cross-process lock
967
+ // updateDependencies() takes, and re-checks its precondition under it.
968
+ //
969
+ // Options:
970
+ // - onlyPackages: array of package names to consider (REQUIRED for
971
+ // installOnMiss -- without a scope, cold-cache installs would cover
972
+ // every bundled package instead of just the app's).
973
+ // - installOnMiss: after the restore phase, npm-install the remaining
974
+ // misses from their shipped shrinkwraps, in parallel.
975
+ // - concurrency (restore pool, default 8), installConcurrency (npm
976
+ // pool, default 3 -- kept small because concurrent npm processes
977
+ // contend on npm's shared content cache).
978
+ //
979
+ // Returns {restored, installed}.
980
+ meteorNpm.prewarmNpmDepsCache = async function (packagesDir, options) {
981
+ options = options || {};
982
+ const none = { restored: 0, installed: 0 };
983
+ if (npmDepsCacheDisabled()) {
984
+ return none;
985
+ }
986
+ packagesDir = files.convertToStandardPath(packagesDir);
987
+
988
+ const only = options.onlyPackages &&
989
+ new Set(options.onlyPackages);
990
+ const candidates = [];
991
+ try {
992
+ files.readdir(packagesDir).forEach(name => {
993
+ if (only && ! only.has(name)) {
994
+ return;
995
+ }
996
+ const packageNpmDir =
997
+ files.pathJoin(packagesDir, name, ".npm", "package");
998
+ if (! files.exists(
999
+ files.pathJoin(packageNpmDir, "npm-shrinkwrap.json"))) {
1000
+ return;
1001
+ }
1002
+ if (files.exists(files.pathJoin(
1003
+ packageNpmDir, "node_modules", ".package-lock.json"))) {
1004
+ return;
1005
+ }
1006
+ candidates.push({ name, packageNpmDir });
1007
+ });
1008
+ } catch (e) {
1009
+ return none;
1010
+ }
1011
+ if (! candidates.length) {
1012
+ return none;
1013
+ }
1014
+
1015
+ function installedAlready(candidate) {
1016
+ return files.exists(files.pathJoin(
1017
+ candidate.packageNpmDir, "node_modules", ".package-lock.json"));
1018
+ }
1019
+
1020
+ async function runPool(queue, size, fn) {
1021
+ let nextIndex = 0;
1022
+ let completed = 0;
1023
+ async function worker() {
1024
+ while (nextIndex < queue.length) {
1025
+ const candidate = queue[nextIndex++];
1026
+ try {
1027
+ // Re-check under the lock: a concurrent build (or the other
1028
+ // prewarm phase) may have handled this package already.
1029
+ const ok = await withNpmPackageLock(
1030
+ candidate.packageNpmDir,
1031
+ async () => ! installedAlready(candidate) && fn(candidate)
1032
+ );
1033
+ if (ok) {
1034
+ completed++;
1035
+ }
1036
+ } catch (e) {
1037
+ // Best-effort; the build's own updateDependencies() will
1038
+ // handle this package the normal way.
1039
+ if (options.debugThrow) {
1040
+ console.error("prewarm error for " + candidate.name + ":",
1041
+ (e && e.stack) || e);
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ await Promise.all(
1047
+ _.times(Math.min(size, queue.length || 1), worker));
1048
+ return completed;
1049
+ }
1050
+
1051
+ const restored = await runPool(
1052
+ candidates, options.concurrency || 8,
1053
+ c => restoreNodeModulesFromNpmDepsCache(c.name, c.packageNpmDir));
1054
+
1055
+ let installed = 0;
1056
+ if (options.installOnMiss && only) {
1057
+ const misses = candidates.filter(c => ! installedAlready(c));
1058
+ installed = await runPool(
1059
+ misses, options.installConcurrency || 3,
1060
+ c => preinstallNpmDepsFromShrinkwrap(c.name, c.packageNpmDir));
1061
+ }
1062
+ return { restored, installed };
1063
+ };
1064
+
679
1065
  var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
680
1066
  packageNpmDir, npmDependencies,
681
1067
  quiet) {
@@ -713,6 +1099,14 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
713
1099
  }
714
1100
  }
715
1101
 
1102
+ // No installed tree yet (fresh tool install, node-version wipe above,
1103
+ // or first build of a cloned package)? Try the persistent cache
1104
+ // before paying for an npm install.
1105
+ if (! _.isEmpty(npmDependencies) &&
1106
+ ! files.exists(files.pathJoin(nodeModulesDir, ".package-lock.json"))) {
1107
+ await restoreNodeModulesFromNpmDepsCache(packageName, packageNpmDir);
1108
+ }
1109
+
716
1110
  // Make sure node_modules is present (fix for #1761). Prevents npm install
717
1111
  // from installing to an existing node_modules dir higher up in the
718
1112
  // filesystem. node_modules may be absent due to a change in Node version or
@@ -759,6 +1153,16 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
759
1153
 
760
1154
  let preservedShrinkwrap;
761
1155
 
1156
+ // Key the eventual cache entry by the shrinkwrap that PRODUCED the
1157
+ // install, captured before completeNpmDirectory() regenerates the
1158
+ // file. When installing from the existing (shipped) shrinkwrap, that
1159
+ // is the same content the next fresh install of this tool version
1160
+ // will probe with -- the regenerated file can differ (e.g.
1161
+ // platform-specific optional deps), which would make every probe
1162
+ // miss. When NOT installing from the shipped shrinkwrap, leave this
1163
+ // null so the populate step keys off the regenerated file instead.
1164
+ let npmDepsCachePopulateKey = null;
1165
+
762
1166
  if (_.isEmpty(npmDependencies)) {
763
1167
  // If there are no npmDependencies, make sure nothing is installed.
764
1168
  preservedShrinkwrap = { dependencies: {} };
@@ -767,6 +1171,7 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
767
1171
  // If the top-level npm dependencies are already encompassed by the
768
1172
  // npm-shrinkwrap.json file, then reuse that file.
769
1173
  preservedShrinkwrap = shrinkwrappedDependenciesTree;
1174
+ npmDepsCachePopulateKey = npmDepsCacheKey(packageNpmDir);
770
1175
 
771
1176
  } else {
772
1177
  // Otherwise install npmTree.dependencies as if we were creating a new
@@ -837,7 +1242,7 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
837
1242
  }
838
1243
 
839
1244
  await completeNpmDirectory(packageName, newPackageNpmDir, packageNpmDir,
840
- npmDependencies);
1245
+ npmDependencies, npmDepsCachePopulateKey);
841
1246
  };
842
1247
 
843
1248
  // Git-URL dependencies could never satisfy the strict string comparison
@@ -969,6 +1374,7 @@ async function completeNpmDirectory(
969
1374
  newPackageNpmDir,
970
1375
  packageNpmDir,
971
1376
  npmDependencies,
1377
+ npmDepsCachePopulateKey,
972
1378
  ) {
973
1379
  // Create a shrinkwrap file.
974
1380
  shrinkwrap(newPackageNpmDir);
@@ -984,6 +1390,11 @@ async function completeNpmDirectory(
984
1390
  await files.renameDirAlmostAtomically(newPackageNpmDir, packageNpmDir);
985
1391
 
986
1392
  dirtyNodeModulesDirectory(files.pathJoin(packageNpmDir, "node_modules"));
1393
+
1394
+ if (! _.isEmpty(npmDependencies)) {
1395
+ scheduleNpmDepsCachePopulate(
1396
+ packageName, packageNpmDir, npmDepsCachePopulateKey);
1397
+ }
987
1398
  }
988
1399
 
989
1400
  var createReadme = function (newPackageNpmDir) {