@maka/meteor-sdk 0.2.8 → 0.2.10

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 (33) hide show
  1. package/README.md +11 -5
  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/baseline-browser-mapping/dist/index.cjs +1 -1
  6. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/baseline-browser-mapping/dist/index.js +1 -1
  7. package/isopackets/combined/npm/node_modules/meteor/babel-compiler/node_modules/baseline-browser-mapping/package.json +1 -1
  8. package/isopackets/combined/npm/node_modules/meteor/boilerplate-generator/node_modules/.package-lock.json +1 -1
  9. package/isopackets/combined/npm/node_modules/meteor/constraint-solver/node_modules/.package-lock.json +1 -1
  10. package/isopackets/combined/npm/node_modules/meteor/ddp-client/node_modules/.package-lock.json +1 -1
  11. package/isopackets/combined/npm/node_modules/meteor/ecmascript/node_modules/.package-lock.json +1 -1
  12. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime/node_modules/.package-lock.json +1 -1
  13. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime-client/node_modules/.package-lock.json +1 -1
  14. package/isopackets/combined/npm/node_modules/meteor/ecmascript-runtime-server/node_modules/.package-lock.json +1 -1
  15. package/isopackets/combined/npm/node_modules/meteor/fetch/node_modules/.package-lock.json +1 -1
  16. package/isopackets/combined/npm/node_modules/meteor/inter-process-messaging/node_modules/.package-lock.json +1 -1
  17. package/isopackets/combined/npm/node_modules/meteor/logging/node_modules/.package-lock.json +1 -1
  18. package/isopackets/combined/npm/node_modules/meteor/logic-solver/node_modules/.package-lock.json +1 -1
  19. package/isopackets/combined/npm/node_modules/meteor/meteor/node_modules/.package-lock.json +1 -1
  20. package/isopackets/combined/npm/node_modules/meteor/modules/node_modules/.package-lock.json +1 -1
  21. package/isopackets/combined/npm/node_modules/meteor/modules-runtime/node_modules/.package-lock.json +1 -1
  22. package/isopackets/combined/npm/node_modules/meteor/npm-mongo/node_modules/.package-lock.json +1 -1
  23. package/isopackets/combined/npm/node_modules/meteor/package-version-parser/node_modules/.package-lock.json +1 -1
  24. package/isopackets/combined/npm/node_modules/meteor/promise/node_modules/.package-lock.json +1 -1
  25. package/isopackets/combined/npm/node_modules/meteor/react-fast-refresh/node_modules/.package-lock.json +1 -1
  26. package/isopackets/combined/npm/node_modules/meteor/socket-stream-client/node_modules/.package-lock.json +1 -1
  27. package/package.json +1 -1
  28. package/src/project.js +40 -0
  29. package/tools/fs/optimistic.ts +106 -1
  30. package/tools/fs/safe-watcher.ts +40 -0
  31. package/tools/fs/watch.ts +39 -0
  32. package/tools/isobuild/meteor-npm.js +171 -39
  33. package/isopackets/combined/npm/node_modules/meteor/fetch/node_modules/node-fetch/.meteor-portable-2.json +0 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-6gguidiwzx",
2
+ "name": "package-new-17en99ibbmv",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-ovtntj9ibd",
2
+ "name": "package-new-1cp3re039w9",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-t8jk4dq32z",
2
+ "name": "package-new-1wkg0541fwq",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1t8h3r5p5mc",
2
+ "name": "package-new-b5a9l27ykfp",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-19ro17vxabi",
2
+ "name": "package-new-1f74r589ogwk",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1mbjo8u1bdw",
2
+ "name": "package-new-1v9u2tnhfn",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "package-new-1bskl16cqu8",
2
+ "name": "package-new-lsdjl8inhw",
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.8",
3
+ "version": "0.2.10",
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/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 };
@@ -85,9 +85,40 @@ function makeOptimistic<
85
85
  // subscribing to file changes.
86
86
  const optimisticFindAppDir = wrap(findAppDir);
87
87
 
88
+ // The running tool's own installation root (tools/fs/optimistic.ts ->
89
+ // two levels up). When the tool is installed as an npm package (e.g.
90
+ // @maka/meteor-sdk under a global node_modules), every file it owns
91
+ // lives beneath one or more node_modules segments, which made the
92
+ // nested-node_modules rule below refuse to watch ANY tool file. With no
93
+ // watcher, an optimistic result cached while a package's .npm directory
94
+ // was being atomically swapped (renameDirAlmostAtomically's brief gap
95
+ // during npm dependency updates) could never be invalidated -- observed
96
+ // as optimisticHashOrNull permanently returning null for an existing
97
+ // npm-shrinkwrap.json, convincing every isopack up-to-date check and
98
+ // the app runner's watchset that a build input was deleted: an eternal
99
+ // rebuild/restart loop ("flapping") after a fresh install. A git
100
+ // checkout has no node_modules prefix and always watched these files;
101
+ // ignoring node_modules segments inside the tool's own prefix restores
102
+ // exactly those semantics.
103
+ const toolRootParts = pathDirname(pathDirname(__dirname))
104
+ .split("\\").join(pathSep)
105
+ .split(pathSep);
106
+
107
+ function toolRootPrefixLength(parts: string[]): number {
108
+ if (parts.length <= toolRootParts.length) {
109
+ return 0;
110
+ }
111
+ for (let i = 0; i < toolRootParts.length; i++) {
112
+ if (parts[i] !== toolRootParts[i]) {
113
+ return 0;
114
+ }
115
+ }
116
+ return toolRootParts.length;
117
+ }
118
+
88
119
  const shouldWatch = wrap(Profile("shouldWatch", (path: string) => {
89
120
  const parts = path.split(pathSep);
90
- const nmi = parts.indexOf("node_modules");
121
+ const nmi = parts.indexOf("node_modules", toolRootPrefixLength(parts));
91
122
 
92
123
  if (nmi < 0) {
93
124
  // Watch everything not in a node_modules directory.
@@ -204,6 +235,36 @@ function dependOnParentDirectory(path: string) {
204
235
  }
205
236
  }
206
237
 
238
+ // For files belonging to the running tool itself, never let a negative
239
+ // (missing-file) result stick: these paths only ever go missing during
240
+ // meteor-npm.js's atomic .npm directory swaps, and every event- or
241
+ // dirty-based invalidation can be raced by a computation that STARTED
242
+ // before the swap and commits its cached null after them (the
243
+ // dirtyReplacedDirectory call included). A null that survives such a
244
+ // race convinced every rebuild that npm-shrinkwrap.json was deleted --
245
+ // the eternal restart loop. Self-dirtying on the next tick makes the
246
+ // null last at most one turn of the event loop; the recompute then sees
247
+ // the real post-swap file. Genuinely missing tool files are rare enough
248
+ // that losing negative-caching for them costs nothing measurable.
249
+ function scheduleEphemeralNull(
250
+ wrapper: { dirty: (...args: any[]) => any },
251
+ args: IArguments | any[],
252
+ ) {
253
+ const path = args[0];
254
+ if (typeof path !== "string") {
255
+ return;
256
+ }
257
+ if (toolRootPrefixLength(path.split(pathSep)) === 0) {
258
+ return;
259
+ }
260
+ const argArray = Array.prototype.slice.call(args);
261
+ setImmediate(() => {
262
+ try {
263
+ wrapper.dirty(...argArray);
264
+ } catch (e) {}
265
+ });
266
+ }
267
+
207
268
  // Called by any optimistic function that receives a */node_modules/* path
208
269
  // as its first argument, so that we can later bulk-invalidate the results
209
270
  // of those calls if the contents of the node_modules directory change.
@@ -255,6 +316,7 @@ export const optimisticStatOrNull = makeCheapPathFunction(
255
316
  const result = statOrNull(path);
256
317
  if (result === null) {
257
318
  dependOnParentDirectory(path);
319
+ scheduleEphemeralNull(optimisticStatOrNull, [path]);
258
320
  }
259
321
  return result;
260
322
  },
@@ -268,6 +330,8 @@ export const optimisticLStatOrNull = makeCheapPathFunction(
268
330
  } catch (e: any) {
269
331
  if (e.code !== "ENOENT") throw e;
270
332
  dependOnParentDirectory(path);
333
+ scheduleEphemeralNull(optimisticLStatOrNull, [path]);
334
+ scheduleEphemeralNull(optimisticLStat, [path]);
271
335
  return null;
272
336
  }
273
337
  },
@@ -290,6 +354,10 @@ export const optimisticHashOrNull = makeOptimistic("hashOrNull", (
290
354
  }
291
355
 
292
356
  dependOnParentDirectory(path);
357
+ scheduleEphemeralNull(
358
+ optimisticHashOrNull, options === undefined ? [path] : [path, options]);
359
+ scheduleEphemeralNull(
360
+ optimisticReadFile, options === undefined ? [path] : [path, options]);
293
361
 
294
362
  return null;
295
363
  });
@@ -312,6 +380,12 @@ makeOptimistic("readJsonOrNull", (
312
380
  } catch (e: any) {
313
381
  if (e.code === "ENOENT") {
314
382
  dependOnParentDirectory(path);
383
+ scheduleEphemeralNull(
384
+ optimisticReadJsonOrNull,
385
+ options === undefined ? [path] : [path, options]);
386
+ scheduleEphemeralNull(
387
+ optimisticReadFile,
388
+ options === undefined ? [path] : [path, options]);
315
389
  return null;
316
390
  }
317
391
  throw e;
@@ -338,6 +412,37 @@ makeOptimistic("readJsonOrNull", (
338
412
  }
339
413
  });
340
414
 
415
+ // Deterministically forget cached results for a directory that was
416
+ // just replaced wholesale (meteor-npm.js's atomic .npm swap via
417
+ // renameDirAlmostAtomically). Watcher-event-driven invalidation cannot
418
+ // be relied on for such swaps: the events arrive only for the
419
+ // directory's own path (see safe-watcher's fireEntriesUnder), can race
420
+ // the read that cached the stale result, and in npm-installed tool
421
+ // trees per-file watches historically never existed at all (see
422
+ // toolRootPrefixLength above). A stale null cached here convinced
423
+ // every rebuild that npm-shrinkwrap.json was deleted -- an eternal
424
+ // rebuild/restart loop after fresh installs. An explicit dirty at the
425
+ // mutation site is the only race-free invalidation. Dirties the
426
+ // directory, its parent, and its current immediate children across
427
+ // every per-path optimistic function.
428
+ export function dirtyReplacedDirectory(dir: string) {
429
+ const paths = [dir, pathDirname(dir)];
430
+ try {
431
+ readdir(dir).forEach(name => paths.push(pathJoin(dir, name)));
432
+ } catch (e) {}
433
+ paths.forEach(path => {
434
+ optimisticStatOrNull.dirty(path);
435
+ optimisticLStat.dirty(path);
436
+ optimisticLStatOrNull.dirty(path);
437
+ optimisticReadFile.dirty(path);
438
+ optimisticReaddir.dirty(path);
439
+ optimisticHashOrNull.dirty(path);
440
+ optimisticReadJsonOrNull.dirty(path);
441
+ dependOnDirectory.dirty(path);
442
+ dependOnPath.dirty(path);
443
+ });
444
+ }
445
+
341
446
  export const optimisticReadMeteorIgnore = wrap((dir: string) => {
342
447
  const meteorIgnorePath = pathJoin(dir, ".meteorignore");
343
448
  const meteorIgnoreStat = optimisticStatOrNull(meteorIgnorePath);
@@ -93,6 +93,25 @@ function deleteEntry(path: string): void {
93
93
  entries.delete(path);
94
94
  }
95
95
 
96
+ // A rename or removal of a DIRECTORY reaches us as a single event for
97
+ // the directory's own path; entries registered for files INSIDE it
98
+ // (e.g. the optimistic layer's per-file watches under a package's .npm
99
+ // directory, which meteor-npm.js swaps wholesale via
100
+ // renameDirAlmostAtomically) would otherwise never hear about it --
101
+ // findNearestEntry only walks UP. Observed failure: an
102
+ // optimisticHashOrNull result cached as null during the swap was never
103
+ // invalidated, convincing every rebuild a build input was deleted (an
104
+ // eternal rebuild/restart loop after fresh installs). Fire every
105
+ // registered descendant of the changed path too.
106
+ function fireEntriesUnder(changedPath: string, evtType: string): void {
107
+ const prefix = pathResolve(changedPath) + "/";
108
+ for (const [entryPath, entry] of entries) {
109
+ if (entry && entryPath.startsWith(prefix)) {
110
+ entry._fire(evtType);
111
+ }
112
+ }
113
+ }
114
+
96
115
  function findNearestEntry(startPath: string): Entry | null {
97
116
  let currentPath = pathResolve(startPath);
98
117
 
@@ -353,6 +372,27 @@ async function ensureWatchRoot(dirPath: string): Promise<void> {
353
372
  for (const event of events) {
354
373
  const changedPath = toPosixPath(event.path);
355
374
  const entry = findNearestEntry(changedPath);
375
+ // Deletes/creates can be directory renames whose contents
376
+ // never get their own events -- notify entries BELOW the
377
+ // path as well (see fireEntriesUnder).
378
+ if (event.type !== "update") {
379
+ fireEntriesUnder(
380
+ changedPath, event.type === "delete" ? "delete" : "change");
381
+ }
382
+ if (process.env.METEOR_WATCH_DEBUG) {
383
+ // Re-derive the matched registration so flap debugging can
384
+ // see WHICH registered path a deep event was attributed to.
385
+ let cursor = pathResolve(changedPath);
386
+ let matched = null;
387
+ while (true) {
388
+ if (getEntry(cursor)) { matched = cursor; break; }
389
+ const parent = pathDirname(cursor);
390
+ if (parent === cursor) break;
391
+ cursor = parent;
392
+ }
393
+ console.log("WATCH-FIRE", event.type, changedPath,
394
+ "->", matched || "(no entry)");
395
+ }
356
396
  if (!entry) continue;
357
397
  // In Meteor's safe-watcher API, both create/update trigger "change" events.
358
398
  const evtType = event.type === "delete" ? "delete" : "change";
package/tools/fs/watch.ts CHANGED
@@ -123,6 +123,14 @@ export class WatchSet {
123
123
  return;
124
124
  }
125
125
  // Nope, inconsistent.
126
+ if (process.env.METEOR_WATCH_DEBUG) {
127
+ // The file changed DURING the build (recorded twice with
128
+ // different hashes). Every Watcher built on this set now fires
129
+ // immediately -- flap debugging needs the culprit's name.
130
+ console.log("WATCHSET-CONFLICT", filePath,
131
+ "recorded", this.files[filePath], "now", hash, "at",
132
+ new Error("watchset-conflict").stack);
133
+ }
126
134
  this.alwaysFire = true;
127
135
  return;
128
136
  }
@@ -448,6 +456,10 @@ export class Watcher {
448
456
 
449
457
  // Were we given an inconsistent WatchSet? Fire now and be done with it.
450
458
  if (this.watchSet.alwaysFire) {
459
+ if (process.env.METEOR_WATCH_DEBUG) {
460
+ console.log("WATCHER-ALWAYSFIRE: inconsistent watchSet -- " +
461
+ "restarting immediately (see WATCHSET-CONFLICT above)");
462
+ }
451
463
  this.fire();
452
464
  return;
453
465
  }
@@ -481,12 +493,18 @@ export class Watcher {
481
493
  return false;
482
494
  }
483
495
  // Nope, not what we expected.
496
+ if (process.env.METEOR_WATCH_DEBUG) {
497
+ console.log("WATCH-CHANGED-FILE (deleted/now-dir)", absPath);
498
+ }
484
499
  this.fire();
485
500
  return true;
486
501
  }
487
502
 
488
503
  // File exists! Is that what we expected?
489
504
  if (oldHash === null) {
505
+ if (process.env.METEOR_WATCH_DEBUG) {
506
+ console.log("WATCH-CHANGED-FILE (appeared)", absPath);
507
+ }
490
508
  this.fire();
491
509
  return true;
492
510
  }
@@ -496,6 +514,9 @@ export class Watcher {
496
514
  return false;
497
515
  }
498
516
 
517
+ if (process.env.METEOR_WATCH_DEBUG) {
518
+ console.log("WATCH-CHANGED-FILE", absPath, oldHash, "->", newHash);
519
+ }
499
520
  this.fire();
500
521
  return true;
501
522
  }
@@ -513,6 +534,11 @@ export class Watcher {
513
534
 
514
535
  // If the directory has changed (including being deleted or created).
515
536
  if (! _.isEqual(info.contents, newContents)) {
537
+ if (process.env.METEOR_WATCH_DEBUG) {
538
+ console.log("WATCH-CHANGED-DIR", info.absPath,
539
+ "expected", JSON.stringify(info.contents),
540
+ "found", JSON.stringify(newContents));
541
+ }
516
542
  this.fire();
517
543
  return true;
518
544
  }
@@ -678,6 +704,11 @@ export class Watcher {
678
704
  let stat = files.statOrNull(absPath);
679
705
  const mustNotExist = this.mustNotExist(absPath);
680
706
  const mustBeAFile = this.mustBeAFile(absPath);
707
+ if (process.env.METEOR_WATCH_DEBUG) {
708
+ // Tag the path before any of the fires below so the adjacent
709
+ // WATCH-FIRE-STACK line can be attributed to a file.
710
+ (this as any)._debugLastStatPath = absPath;
711
+ }
681
712
 
682
713
  if (stat && lastStat === undefined) {
683
714
  // We have not checked for this file before, so our expectations are
@@ -787,6 +818,14 @@ export class Watcher {
787
818
 
788
819
  private fire() {
789
820
  if (this.stopped) return;
821
+ if (process.env.METEOR_WATCH_DEBUG) {
822
+ // The chokepoint every restart-triggering change funnels through;
823
+ // the stack identifies WHICH check fired when the per-site logs
824
+ // (WATCH-CHANGED-*, WATCHSET-CONFLICT) didn't catch it.
825
+ console.log("WATCH-FIRE-STACK",
826
+ "lastStatPath=" + ((this as any)._debugLastStatPath || "(none)"),
827
+ new Error("watcher-fired").stack);
828
+ }
790
829
  this.stop();
791
830
  this.onChange();
792
831
  }