@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.
- package/README.md +26 -0
- package/isopackets/combined/isopacket-buildinfo.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/.npm-shrinkwrap.json +3 -3
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/.package-lock.json +4 -4
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/full-chromium-versions.js +2 -1
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/full-chromium-versions.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/full-versions.js +1 -0
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/full-versions.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/package.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/versions.js +1 -0
- package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/electron-to-chromium/versions.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/boilerplate-generator/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/constraint-solver/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/ddp-client/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/ecmascript/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime-client/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime-server/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/fetch/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/inter-process-messaging/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/logging/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/logic-solver/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/meteor/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/modules/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/modules-runtime/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/npm-mongo/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/package-version-parser/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/promise/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/react-fast-refresh/node_modules/.package-lock.json +1 -1
- package/isopackets/combined/npm/node_modules/meteor/socket-stream-client/node_modules/.package-lock.json +1 -1
- package/package.json +1 -1
- package/src/bootstrap.js +26 -0
- package/src/bundle.js +5 -0
- package/src/project.js +40 -0
- package/tools/isobuild/meteor-npm.js +412 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/meteor-sdk",
|
|
3
|
-
"version": "0.2.
|
|
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) {
|