@maka/meteor-sdk 0.2.7 → 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.
Files changed (34) hide show
  1. package/README.md +20 -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/tools/isobuild/meteor-npm.js +315 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-d6edvk3v9nl",
2
+ "name": "package-new-19ro17vxabi",
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-1mbjo8u1bdw",
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-1bskl16cqu8",
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.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
@@ -759,6 +1056,16 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
759
1056
 
760
1057
  let preservedShrinkwrap;
761
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
+
762
1069
  if (_.isEmpty(npmDependencies)) {
763
1070
  // If there are no npmDependencies, make sure nothing is installed.
764
1071
  preservedShrinkwrap = { dependencies: {} };
@@ -767,6 +1074,7 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
767
1074
  // If the top-level npm dependencies are already encompassed by the
768
1075
  // npm-shrinkwrap.json file, then reuse that file.
769
1076
  preservedShrinkwrap = shrinkwrappedDependenciesTree;
1077
+ npmDepsCachePopulateKey = npmDepsCacheKey(packageNpmDir);
770
1078
 
771
1079
  } else {
772
1080
  // Otherwise install npmTree.dependencies as if we were creating a new
@@ -837,7 +1145,7 @@ var updateExistingNpmDirectory = async function (packageName, newPackageNpmDir,
837
1145
  }
838
1146
 
839
1147
  await completeNpmDirectory(packageName, newPackageNpmDir, packageNpmDir,
840
- npmDependencies);
1148
+ npmDependencies, npmDepsCachePopulateKey);
841
1149
  };
842
1150
 
843
1151
  // Git-URL dependencies could never satisfy the strict string comparison
@@ -969,6 +1277,7 @@ async function completeNpmDirectory(
969
1277
  newPackageNpmDir,
970
1278
  packageNpmDir,
971
1279
  npmDependencies,
1280
+ npmDepsCachePopulateKey,
972
1281
  ) {
973
1282
  // Create a shrinkwrap file.
974
1283
  shrinkwrap(newPackageNpmDir);
@@ -984,6 +1293,11 @@ async function completeNpmDirectory(
984
1293
  await files.renameDirAlmostAtomically(newPackageNpmDir, packageNpmDir);
985
1294
 
986
1295
  dirtyNodeModulesDirectory(files.pathJoin(packageNpmDir, "node_modules"));
1296
+
1297
+ if (! _.isEmpty(npmDependencies)) {
1298
+ scheduleNpmDepsCachePopulate(
1299
+ packageName, packageNpmDir, npmDepsCachePopulateKey);
1300
+ }
987
1301
  }
988
1302
 
989
1303
  var createReadme = function (newPackageNpmDir) {