@maka/meteor-sdk 0.2.6 → 0.2.8
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 +20 -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/tools/isobuild/meteor-npm.js +383 -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.8",
|
|
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,
|
|
@@ -676,6 +676,295 @@ 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
|
+
// Restore every restorable package under packagesDir from the cache, in
|
|
897
|
+
// parallel, before the (serial) per-package build pipeline reaches
|
|
898
|
+
// them. Purely filesystem-driven: a candidate is any package dir with a
|
|
899
|
+
// .npm/package/npm-shrinkwrap.json but no installed node_modules, so no
|
|
900
|
+
// package.js evaluation is needed. Safe to run concurrently with a
|
|
901
|
+
// build -- each restore holds the same cross-process lock
|
|
902
|
+
// updateDependencies() takes, and re-checks its precondition under it.
|
|
903
|
+
// Returns the number of packages restored.
|
|
904
|
+
meteorNpm.prewarmNpmDepsCache = async function (packagesDir, options) {
|
|
905
|
+
options = options || {};
|
|
906
|
+
if (npmDepsCacheDisabled()) {
|
|
907
|
+
return 0;
|
|
908
|
+
}
|
|
909
|
+
packagesDir = files.convertToStandardPath(packagesDir);
|
|
910
|
+
|
|
911
|
+
const candidates = [];
|
|
912
|
+
try {
|
|
913
|
+
files.readdir(packagesDir).forEach(name => {
|
|
914
|
+
const packageNpmDir =
|
|
915
|
+
files.pathJoin(packagesDir, name, ".npm", "package");
|
|
916
|
+
if (! files.exists(
|
|
917
|
+
files.pathJoin(packageNpmDir, "npm-shrinkwrap.json"))) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (files.exists(files.pathJoin(
|
|
921
|
+
packageNpmDir, "node_modules", ".package-lock.json"))) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
candidates.push({ name, packageNpmDir });
|
|
925
|
+
});
|
|
926
|
+
} catch (e) {
|
|
927
|
+
return 0;
|
|
928
|
+
}
|
|
929
|
+
if (! candidates.length) {
|
|
930
|
+
return 0;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
let restored = 0;
|
|
934
|
+
let nextIndex = 0;
|
|
935
|
+
async function worker() {
|
|
936
|
+
while (nextIndex < candidates.length) {
|
|
937
|
+
const candidate = candidates[nextIndex++];
|
|
938
|
+
try {
|
|
939
|
+
const ok = await withNpmPackageLock(
|
|
940
|
+
candidate.packageNpmDir,
|
|
941
|
+
async () => {
|
|
942
|
+
// Re-check under the lock: a concurrent build may have
|
|
943
|
+
// installed (or restored) this package already.
|
|
944
|
+
if (files.exists(files.pathJoin(
|
|
945
|
+
candidate.packageNpmDir,
|
|
946
|
+
"node_modules", ".package-lock.json"))) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
return restoreNodeModulesFromNpmDepsCache(
|
|
950
|
+
candidate.name, candidate.packageNpmDir);
|
|
951
|
+
}
|
|
952
|
+
);
|
|
953
|
+
if (ok) {
|
|
954
|
+
restored++;
|
|
955
|
+
}
|
|
956
|
+
} catch (e) {
|
|
957
|
+
// Best-effort; the build's own updateDependencies() will handle
|
|
958
|
+
// this package the normal way.
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const concurrency = Math.min(
|
|
963
|
+
options.concurrency || 8, candidates.length);
|
|
964
|
+
await Promise.all(_.times(concurrency, worker));
|
|
965
|
+
return restored;
|
|
966
|
+
};
|
|
967
|
+
|
|
679
968
|
var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
|
|
680
969
|
packageNpmDir, npmDependencies,
|
|
681
970
|
quiet) {
|
|
@@ -713,6 +1002,14 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
|
|
|
713
1002
|
}
|
|
714
1003
|
}
|
|
715
1004
|
|
|
1005
|
+
// No installed tree yet (fresh tool install, node-version wipe above,
|
|
1006
|
+
// or first build of a cloned package)? Try the persistent cache
|
|
1007
|
+
// before paying for an npm install.
|
|
1008
|
+
if (! _.isEmpty(npmDependencies) &&
|
|
1009
|
+
! files.exists(files.pathJoin(nodeModulesDir, ".package-lock.json"))) {
|
|
1010
|
+
await restoreNodeModulesFromNpmDepsCache(packageName, packageNpmDir);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
716
1013
|
// Make sure node_modules is present (fix for #1761). Prevents npm install
|
|
717
1014
|
// from installing to an existing node_modules dir higher up in the
|
|
718
1015
|
// filesystem. node_modules may be absent due to a change in Node version or
|
|
@@ -731,6 +1028,8 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
|
|
|
731
1028
|
npmTree.dependencies[name] = { version };
|
|
732
1029
|
});
|
|
733
1030
|
|
|
1031
|
+
reconcileGitDependencies(npmTree, installedDependenciesTree);
|
|
1032
|
+
|
|
734
1033
|
let minInstalledTree;
|
|
735
1034
|
try {
|
|
736
1035
|
minInstalledTree = minimizeDependencyTree(installedDependenciesTree);
|
|
@@ -757,6 +1056,16 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
|
|
|
757
1056
|
|
|
758
1057
|
let preservedShrinkwrap;
|
|
759
1058
|
|
|
1059
|
+
// Key the eventual cache entry by the shrinkwrap that PRODUCED the
|
|
1060
|
+
// install, captured before completeNpmDirectory() regenerates the
|
|
1061
|
+
// file. When installing from the existing (shipped) shrinkwrap, that
|
|
1062
|
+
// is the same content the next fresh install of this tool version
|
|
1063
|
+
// will probe with -- the regenerated file can differ (e.g.
|
|
1064
|
+
// platform-specific optional deps), which would make every probe
|
|
1065
|
+
// miss. When NOT installing from the shipped shrinkwrap, leave this
|
|
1066
|
+
// null so the populate step keys off the regenerated file instead.
|
|
1067
|
+
let npmDepsCachePopulateKey = null;
|
|
1068
|
+
|
|
760
1069
|
if (_.isEmpty(npmDependencies)) {
|
|
761
1070
|
// If there are no npmDependencies, make sure nothing is installed.
|
|
762
1071
|
preservedShrinkwrap = { dependencies: {} };
|
|
@@ -765,6 +1074,7 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
|
|
|
765
1074
|
// If the top-level npm dependencies are already encompassed by the
|
|
766
1075
|
// npm-shrinkwrap.json file, then reuse that file.
|
|
767
1076
|
preservedShrinkwrap = shrinkwrappedDependenciesTree;
|
|
1077
|
+
npmDepsCachePopulateKey = npmDepsCacheKey(packageNpmDir);
|
|
768
1078
|
|
|
769
1079
|
} else {
|
|
770
1080
|
// Otherwise install npmTree.dependencies as if we were creating a new
|
|
@@ -835,9 +1145,75 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
|
|
|
835
1145
|
}
|
|
836
1146
|
|
|
837
1147
|
await completeNpmDirectory(packageName, newPackageNpmDir, packageNpmDir,
|
|
838
|
-
npmDependencies);
|
|
1148
|
+
npmDependencies, npmDepsCachePopulateKey);
|
|
839
1149
|
};
|
|
840
1150
|
|
|
1151
|
+
// Git-URL dependencies could never satisfy the strict string comparison
|
|
1152
|
+
// in updateExistingNpmDirectory: Npm.depends declares the spec the
|
|
1153
|
+
// author wrote (e.g. "git+https://github.com/org/repo#v1.2.3") while
|
|
1154
|
+
// npm's lockfile records what it resolved (e.g.
|
|
1155
|
+
// "git+ssh://git@github.com/org/repo.git#<commit-sha>"). The result was
|
|
1156
|
+
// a FULL reinstall of the package's npm deps -- git clone included --
|
|
1157
|
+
// on every single build, and during `run` the .npm/package churn could
|
|
1158
|
+
// re-fire the file watcher, restarting the app in a loop ("ddp-server:
|
|
1159
|
+
// updating npm dependencies ..." flapping forever).
|
|
1160
|
+
//
|
|
1161
|
+
// Reconcile before comparing: a declared git dep is satisfied by an
|
|
1162
|
+
// installed copy from the same repository when the declared ref is
|
|
1163
|
+
// consistent with what's installed -- a commit-sha ref must prefix the
|
|
1164
|
+
// resolved sha, a "v1.2.3"/"1.2.3" tag must equal the installed
|
|
1165
|
+
// package's own version, and a symbolic ref (branch) is accepted as-is
|
|
1166
|
+
// (it can move upstream, but reinstalling every build is strictly
|
|
1167
|
+
// worse; pin by tag or sha for reproducibility). On a match the
|
|
1168
|
+
// declared version is rewritten to the installed resolved URL so the
|
|
1169
|
+
// strict subtree comparison passes; on a mismatch (e.g. the tag was
|
|
1170
|
+
// bumped) nothing is rewritten and the normal update runs.
|
|
1171
|
+
function parseGitUrl(url) {
|
|
1172
|
+
if (typeof url !== "string") {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
const m = url.match(
|
|
1176
|
+
/^git\+(?:https?|ssh|git|file):\/\/(?:[^@/]+@)?([^#?]+?)(?:\.git)?(?:#(.*))?$/i
|
|
1177
|
+
);
|
|
1178
|
+
if (! m) {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
return { repo: m[1].toLowerCase(), ref: m[2] || null };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function reconcileGitDependencies(npmTree, installedTree) {
|
|
1185
|
+
_.each(npmTree.dependencies, (dep, name) => {
|
|
1186
|
+
const declared = parseGitUrl(dep.version);
|
|
1187
|
+
if (! declared) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const installed = installedTree.dependencies &&
|
|
1191
|
+
installedTree.dependencies[name];
|
|
1192
|
+
const resolved = installed && parseGitUrl(installed.resolved);
|
|
1193
|
+
if (! resolved || resolved.repo !== declared.repo) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const ref = declared.ref;
|
|
1197
|
+
let refSatisfied;
|
|
1198
|
+
if (! ref) {
|
|
1199
|
+
refSatisfied = true;
|
|
1200
|
+
} else if (/^[0-9a-f]{7,40}$/i.test(ref)) {
|
|
1201
|
+
refSatisfied = !! resolved.ref &&
|
|
1202
|
+
resolved.ref.toLowerCase().startsWith(ref.toLowerCase());
|
|
1203
|
+
} else if (/^v?\d+(\.\d+)*([-+.].*)?$/.test(ref)) {
|
|
1204
|
+
refSatisfied = !! installed.version &&
|
|
1205
|
+
ref.replace(/^v/, "") === installed.version;
|
|
1206
|
+
} else {
|
|
1207
|
+
refSatisfied = true;
|
|
1208
|
+
}
|
|
1209
|
+
if (refSatisfied) {
|
|
1210
|
+
dep.version = minimizeDependencyTree({
|
|
1211
|
+
dependencies: { [name]: installed },
|
|
1212
|
+
}).dependencies[name].version;
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
841
1217
|
function isSubtreeOf(subsetTree, supersetTree, predicate) {
|
|
842
1218
|
if (subsetTree === supersetTree) {
|
|
843
1219
|
return true;
|
|
@@ -901,6 +1277,7 @@ async function completeNpmDirectory(
|
|
|
901
1277
|
newPackageNpmDir,
|
|
902
1278
|
packageNpmDir,
|
|
903
1279
|
npmDependencies,
|
|
1280
|
+
npmDepsCachePopulateKey,
|
|
904
1281
|
) {
|
|
905
1282
|
// Create a shrinkwrap file.
|
|
906
1283
|
shrinkwrap(newPackageNpmDir);
|
|
@@ -916,6 +1293,11 @@ async function completeNpmDirectory(
|
|
|
916
1293
|
await files.renameDirAlmostAtomically(newPackageNpmDir, packageNpmDir);
|
|
917
1294
|
|
|
918
1295
|
dirtyNodeModulesDirectory(files.pathJoin(packageNpmDir, "node_modules"));
|
|
1296
|
+
|
|
1297
|
+
if (! _.isEmpty(npmDependencies)) {
|
|
1298
|
+
scheduleNpmDepsCachePopulate(
|
|
1299
|
+
packageName, packageNpmDir, npmDepsCachePopulateKey);
|
|
1300
|
+
}
|
|
919
1301
|
}
|
|
920
1302
|
|
|
921
1303
|
var createReadme = function (newPackageNpmDir) {
|