@fynpo/base 1.1.22 → 2.0.0

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.
@@ -1,21 +1,28 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FynpoDepGraph = exports.PackageRef = void 0;
4
- exports.getDepSection = getDepSection;
5
- exports.makeDepStep = makeDepStep;
6
- exports.pkgId = pkgId;
7
- exports.pkgInfoId = pkgInfoId;
8
- const tslib_1 = require("tslib");
9
- const assert_1 = tslib_1.__importDefault(require("assert"));
10
- const path_1 = tslib_1.__importDefault(require("path"));
11
- const fs_1 = require("fs");
12
- const filter_scan_dir_1 = require("filter-scan-dir");
13
- const minimatch_1 = tslib_1.__importDefault(require("minimatch"));
14
- const lodash_1 = tslib_1.__importDefault(require("lodash"));
15
- const semver_1 = tslib_1.__importDefault(require("semver"));
16
- const minimatch_group_1 = require("./minimatch-group");
17
- const util_1 = require("./util");
18
- const is_path_inside_1 = tslib_1.__importDefault(require("is-path-inside"));
1
+ import assert from "assert";
2
+ import Path from "path";
3
+ import { promises as Fs } from "fs";
4
+ import { filterScanDir } from "filter-scan-dir";
5
+ import { Minimatch } from "minimatch";
6
+ import _ from "lodash";
7
+ import Semver from "semver";
8
+ import { groupMM } from "./minimatch-group.js";
9
+ import { posixify } from "./util.js";
10
+ import { resolvePackagesConfig, scanPatterns, includeFilter } from "./packages-config.js";
11
+ import { makeGitignoreMatcher } from "./gitignore.js";
12
+ /**
13
+ * Check that `childPath` is inside `parentPath`.
14
+ *
15
+ * Local replacement for the `is-path-inside` package, mirroring its exact
16
+ * semantics: based on `path.relative`, so on win32 it inherits node's
17
+ * case-insensitive drive/path comparison. A path is not inside itself.
18
+ */
19
+ function isPathInside(childPath, parentPath) {
20
+ const relation = Path.relative(parentPath, childPath);
21
+ return Boolean(relation &&
22
+ relation !== ".." &&
23
+ !relation.startsWith(`..${Path.sep}`) &&
24
+ relation !== Path.resolve(childPath));
25
+ }
19
26
  const DepSectionMap = {
20
27
  dependencies: "dep",
21
28
  devDependencies: "dev",
@@ -32,7 +39,7 @@ const DepSectionMap = {
32
39
  * @param fullName - full name from package.json dep section
33
40
  * @returns dep section
34
41
  */
35
- function getDepSection(fullName) {
42
+ export function getDepSection(fullName) {
36
43
  return DepSectionMap[fullName] || "dep";
37
44
  }
38
45
  /**
@@ -43,7 +50,7 @@ function getDepSection(fullName) {
43
50
  * @param sec section it's specified as a dep package
44
51
  * @returns step string
45
52
  */
46
- function makeDepStep(name, version, sec) {
53
+ export function makeDepStep(name, version, sec) {
47
54
  return `${pkgId(name, version)}(${sec})`;
48
55
  }
49
56
  /**
@@ -51,7 +58,7 @@ function makeDepStep(name, version, sec) {
51
58
  *
52
59
  * Supports RegExp or minimatch (for path)
53
60
  */
54
- class PackageRef {
61
+ export class PackageRef {
55
62
  constructor(ref) {
56
63
  this.parseRef(ref);
57
64
  }
@@ -96,7 +103,7 @@ class PackageRef {
96
103
  }
97
104
  }
98
105
  else if (this.type === "path") {
99
- this.mm = new minimatch_1.default.Minimatch(this.value);
106
+ this.mm = new Minimatch(this.value);
100
107
  }
101
108
  }
102
109
  match(pkgInfo) {
@@ -113,7 +120,6 @@ class PackageRef {
113
120
  }
114
121
  }
115
122
  }
116
- exports.PackageRef = PackageRef;
117
123
  /**
118
124
  * Create a package ID by joining name and version into a single string with `@`
119
125
  *
@@ -121,7 +127,7 @@ exports.PackageRef = PackageRef;
121
127
  * @param version package version
122
128
  * @returns
123
129
  */
124
- function pkgId(name, version) {
130
+ export function pkgId(name, version) {
125
131
  /* istanbul ignore next */
126
132
  const ver = version ? `@${version}` : "";
127
133
  return `${name}${ver}`;
@@ -137,7 +143,7 @@ function pkgId(name, version) {
137
143
  * @returns package info
138
144
  */
139
145
  function resolvePackage(semver, packages, fallbackToFirst = true) {
140
- const found = packages.find((pkg) => semver_1.default.satisfies(pkg.version, semver));
146
+ const found = packages.find((pkg) => Semver.satisfies(pkg.version, semver));
141
147
  if (found) {
142
148
  return found;
143
149
  }
@@ -152,13 +158,13 @@ function resolvePackage(semver, packages, fallbackToFirst = true) {
152
158
  * @param info package basic info
153
159
  * @returns
154
160
  */
155
- function pkgInfoId(info) {
161
+ export function pkgInfoId(info) {
156
162
  return pkgId(info.name, info.version);
157
163
  }
158
164
  /**
159
165
  * fynpo dep graph manager
160
166
  */
161
- class FynpoDepGraph {
167
+ export class FynpoDepGraph {
162
168
  constructor(options = {}) {
163
169
  this._options = { cwd: process.cwd(), ...options };
164
170
  this.depMapByPath = Object.create(null);
@@ -175,7 +181,7 @@ class FynpoDepGraph {
175
181
  * @param indirects - set true to resolve indirect deps also
176
182
  */
177
183
  async resolve(indirects = false) {
178
- if (lodash_1.default.isEmpty(this.packages.byName)) {
184
+ if (_.isEmpty(this.packages.byName)) {
179
185
  await this.readPackages();
180
186
  }
181
187
  this.resolveDirectDeps();
@@ -301,22 +307,36 @@ class FynpoDepGraph {
301
307
  async readPackages() {
302
308
  const { patterns, cwd } = this._options;
303
309
  let groups;
304
- // no patterns => setup to search for all package.json
305
- const autoSearch = lodash_1.default.isEmpty(patterns);
310
+ //
311
+ // Discovery is driven by the `packages` config, resolved the same way readFynpoPackages
312
+ // resolves it so the two paths cannot drift again (FPO-17). Explicit `patterns` passed to
313
+ // the constructor still win, for callers that already decided.
314
+ //
315
+ const pkgConfig = resolvePackagesConfig(this._options.packages);
316
+ const resolved = _.isEmpty(patterns) ? scanPatterns(pkgConfig) : patterns;
317
+ // `include` filters what the scan found - it does not replace the scan (FPO-17)
318
+ const includeMms = (_.isEmpty(patterns) ? includeFilter(pkgConfig) : []).map((p) => new Minimatch(p));
319
+ const isIncluded = (path) => includeMms.length === 0 || includeMms.some((m) => m.match(posixify(path)));
320
+ const autoSearch = resolved === null;
321
+ this.autoSearched = autoSearch;
306
322
  const autoSearchFound = [];
323
+ const gitignore = makeGitignoreMatcher(cwd);
324
+ const excludeMms = pkgConfig.exclude.map((p) => new Minimatch(p));
325
+ const isExcluded = (path) => Boolean(path) && excludeMms.some((m) => m.match(path.split(Path.sep).join("/")));
326
+ const skipForAutoSearch = (path) => autoSearch && pkgConfig.autoSearch.respectGitignore && gitignore.ignores(path);
307
327
  if (autoSearch) {
308
328
  groups = { ".": null };
309
329
  }
310
330
  else {
311
- const mms = patterns.map((p) => new minimatch_1.default.Minimatch(p));
312
- groups = (0, minimatch_group_1.groupMM)(mms, {});
331
+ const mms = resolved.map((p) => new Minimatch(p));
332
+ groups = groupMM(mms, {});
313
333
  }
314
334
  const foundInAutoSearch = (path) => {
315
335
  if (!autoSearch) {
316
336
  return false;
317
337
  }
318
338
  for (const e of autoSearchFound) {
319
- if ((0, is_path_inside_1.default)(path, e)) {
339
+ if (isPathInside(path, e)) {
320
340
  return true;
321
341
  }
322
342
  }
@@ -324,12 +344,15 @@ class FynpoDepGraph {
324
344
  };
325
345
  const files = [];
326
346
  for (const prefix in groups) {
327
- const scanned = await (0, filter_scan_dir_1.filterScanDir)({
347
+ const scanned = await filterScanDir({
328
348
  cwd,
329
349
  prefix,
330
350
  concurrency: 500,
331
351
  filter: (file, path, extras) => {
332
352
  if (path && path !== "." && file === "package.json") {
353
+ if (isExcluded(path) || skipForAutoSearch(path)) {
354
+ return false;
355
+ }
333
356
  if (extras.files.includes("fynpo.json") || foundInAutoSearch(path)) {
334
357
  //
335
358
  // We ignore dir with package.json if:
@@ -346,7 +369,7 @@ class FynpoDepGraph {
346
369
  },
347
370
  filterDir: (dir, path, extras) => {
348
371
  if (dir !== "node_modules") {
349
- if (foundInAutoSearch(path)) {
372
+ if (foundInAutoSearch(path) || isExcluded(path) || skipForAutoSearch(path)) {
350
373
  return false;
351
374
  }
352
375
  return prefix === "." && groups[prefix] === null
@@ -358,7 +381,9 @@ class FynpoDepGraph {
358
381
  });
359
382
  files.push(scanned);
360
383
  }
361
- const allFiles = [].concat(...files).sort();
384
+ const allFiles = [].concat(...files)
385
+ .filter((f) => isIncluded(Path.dirname(f)))
386
+ .sort();
362
387
  // Read each package.json and generate PackageInfo for it
363
388
  for (const pkgFile of allFiles) {
364
389
  await this.addPackageByFile(pkgFile);
@@ -372,9 +397,9 @@ class FynpoDepGraph {
372
397
  * @param pkgFile - path to the package.json file
373
398
  */
374
399
  async addPackageByFile(pkgFile) {
375
- const pkgStr = await fs_1.promises.readFile(path_1.default.join(this._options.cwd, pkgFile), "utf-8");
400
+ const pkgStr = await Fs.readFile(Path.join(this._options.cwd, pkgFile), "utf-8");
376
401
  const pkgJson = JSON.parse(pkgStr);
377
- this.addPackage(pkgJson, path_1.default.dirname(pkgFile), pkgStr);
402
+ this.addPackage(pkgJson, Path.dirname(pkgFile), pkgStr);
378
403
  }
379
404
  /**
380
405
  * Add a package to the graph using the data from its package.json
@@ -388,17 +413,17 @@ class FynpoDepGraph {
388
413
  if (pkgJson.fynpo === false) {
389
414
  return;
390
415
  }
391
- (0, assert_1.default)(pkgJson.name, `package at ${pkgPath} doesn't have name`);
416
+ assert(pkgJson.name, `package at ${pkgPath} doesn't have name`);
392
417
  // check for npm scope
393
418
  const pkgDir = pkgJson.name[0] === "@" && (pkgPath.endsWith(`/${pkgJson.name}`) || pkgPath === pkgJson.name)
394
419
  ? pkgJson.name
395
- : path_1.default.basename(pkgPath);
420
+ : Path.basename(pkgPath);
396
421
  const pkgInfo = {
397
422
  name: pkgJson.name,
398
423
  version: pkgJson.version,
399
424
  path: pkgPath,
400
425
  pkgDir,
401
- ...lodash_1.default.pick(pkgJson, [
426
+ ..._.pick(pkgJson, [
402
427
  "private",
403
428
  "dependencies",
404
429
  "devDependencies",
@@ -440,7 +465,7 @@ class FynpoDepGraph {
440
465
  });
441
466
  // sort package with multiple versions from latest to oldest
442
467
  if (byName[name].length > 1) {
443
- byName[name].sort((a, b) => semver_1.default.compare(b.version, a.version));
468
+ byName[name].sort((a, b) => Semver.compare(b.version, a.version));
444
469
  }
445
470
  }
446
471
  }
@@ -458,6 +483,38 @@ class FynpoDepGraph {
458
483
  }
459
484
  return undefined;
460
485
  }
486
+ /**
487
+ * Check if a dependency should NOT be resolved as a local package.
488
+ *
489
+ * Checks both:
490
+ * 1. Monorepo-wide `noFynLocal` option
491
+ * 2. Per-package `fyn.dependencies[name]` setting (false or "no-fyn-local")
492
+ *
493
+ * @param pkgInfo - the package that has the dependency
494
+ * @param depName - name of the dependency to check
495
+ * @returns true if the dependency should NOT be resolved locally
496
+ */
497
+ checkNoFynLocal(pkgInfo, depName) {
498
+ var _a, _b;
499
+ // Check monorepo-wide noFynLocal list
500
+ if ((_a = this._options.noFynLocal) === null || _a === void 0 ? void 0 : _a.includes(depName)) {
501
+ return true;
502
+ }
503
+ // Check per-package fyn.dependencies setting
504
+ const fynConfig = (_b = pkgInfo.pkgJson) === null || _b === void 0 ? void 0 : _b.fyn;
505
+ if (fynConfig) {
506
+ for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
507
+ const deps = fynConfig[section];
508
+ if (deps) {
509
+ const val = deps[depName];
510
+ if (val === false || (typeof val === "string" && val.includes("no-fyn-local"))) {
511
+ return true;
512
+ }
513
+ }
514
+ }
515
+ }
516
+ return false;
517
+ }
461
518
  /**
462
519
  * Figure out all the packages' direct dependencies on other local packages
463
520
  *
@@ -467,12 +524,16 @@ class FynpoDepGraph {
467
524
  const depMapByPath = this.depMapByPath;
468
525
  const doResolve = (depData, deps, section) => {
469
526
  const { pkgInfo } = depData;
470
- lodash_1.default.each(deps, (semver, name) => {
527
+ _.each(deps, (semver, name) => {
471
528
  // dep is not a local package in the monorepo, nothing to do
472
529
  /* istanbul ignore if */
473
530
  if (!byName[name]) {
474
531
  return;
475
532
  }
533
+ // check if this dep should not be resolved as local package
534
+ if (this.checkNoFynLocal(pkgInfo, name)) {
535
+ return;
536
+ }
476
537
  const semId = pkgId(name, semver);
477
538
  const resolveId = this.resolvedCache[semId];
478
539
  const depPkg = (resolveId && byId[resolveId]) || resolvePackage(semver, byName[name]);
@@ -550,7 +611,7 @@ class FynpoDepGraph {
550
611
  }
551
612
  return true;
552
613
  }
553
- return !lodash_1.default.isEmpty(dataDep.pathOfCirculars);
614
+ return !_.isEmpty(dataDep.pathOfCirculars);
554
615
  }
555
616
  /**
556
617
  * Add depdencies from a list of dep relations
@@ -604,7 +665,7 @@ class FynpoDepGraph {
604
665
  * @param dir - directory of package to get
605
666
  */
606
667
  getPackageAtDir(dir) {
607
- const path = (0, util_1.posixify)(path_1.default.isAbsolute(dir) ? path_1.default.relative(this._options.cwd, dir) : dir);
668
+ const path = posixify(Path.isAbsolute(dir) ? Path.relative(this._options.cwd, dir) : dir);
608
669
  return this.packages.byPath[path];
609
670
  }
610
671
  /**
@@ -613,7 +674,7 @@ class FynpoDepGraph {
613
674
  * @returns
614
675
  */
615
676
  getPackageByName(name) {
616
- return lodash_1.default.first(this.packages.byName[name]);
677
+ return _.first(this.packages.byName[name]);
617
678
  }
618
679
  /**
619
680
  * Figure out all the packages' indirect dependencies on other local packages
@@ -627,7 +688,7 @@ class FynpoDepGraph {
627
688
  const doResolve = (dataPkg, localDeps, steps, section) => {
628
689
  const { pkgInfo } = dataPkg;
629
690
  // go through all found deps
630
- lodash_1.default.each(localDeps, (depRef) => {
691
+ _.each(localDeps, (depRef) => {
631
692
  const sec = section || depRef.depSection;
632
693
  const depInfo = byPath[depRef.path];
633
694
  const dataDep = depMapByPath[depRef.path];
@@ -653,10 +714,8 @@ class FynpoDepGraph {
653
714
  doResolve(depData, depData.localDepsByPath, [pkgInfoId(depData.pkgInfo)]);
654
715
  }
655
716
  if (change > 0) {
656
- (0, assert_1.default)(nestedLevel < 50, "FynpoDepGraph.resolveIndirectDeps nested too deep - there may be circular deps that's not detected");
717
+ assert(nestedLevel < 50, "FynpoDepGraph.resolveIndirectDeps nested too deep - there may be circular deps that's not detected");
657
718
  this.resolveIndirectDeps(nestedLevel + 1);
658
719
  }
659
720
  }
660
721
  }
661
- exports.FynpoDepGraph = FynpoDepGraph;
662
- //# sourceMappingURL=fynpo-dep-graph.js.map
@@ -0,0 +1,17 @@
1
+ /** matches paths against a repo's gitignore rules */
2
+ export type GitignoreMatcher = {
3
+ /** true if the given repo-relative path is ignored by git */
4
+ ignores(relPath: string): boolean;
5
+ /** true if any rule was actually loaded - false means every check is a no-op */
6
+ hasRules: boolean;
7
+ };
8
+ /**
9
+ * Build a gitignore matcher for a repo root.
10
+ *
11
+ * Never throws for a missing or unreadable ignore file - a repo with no rules simply matches
12
+ * nothing.
13
+ *
14
+ * @param cwd - repo root to read ignore rules from
15
+ * @returns a matcher; {@link GitignoreMatcher.hasRules} is false when nothing was loaded
16
+ */
17
+ export declare function makeGitignoreMatcher(cwd: string): GitignoreMatcher;
@@ -0,0 +1,67 @@
1
+ import Fs from "fs";
2
+ import Path from "path";
3
+ import ignore from "ignore";
4
+ //
5
+ // Gitignore matching, used for two separate things (FPO-17):
6
+ //
7
+ // 1. `packages.autoSearch.respectGitignore` - opt-in, default off. When on, auto-search skips
8
+ // gitignored paths.
9
+ // 2. The publish veto - ALWAYS on. A gitignored package is never in publish jurisdiction, even
10
+ // when respectGitignore is off and even if `publishInclude` names it. A gitignored package
11
+ // is typically a nested clone of some other repo; publishing it from here is never right.
12
+ //
13
+ // Because of (2) the matcher is built regardless of the flag. In a directory with no gitignore
14
+ // rules - or no git at all - nothing matches and both behaviors are no-ops.
15
+ //
16
+ // Scope: the repo root `.gitignore` and `.git/info/exclude`. Nested `.gitignore` files deeper
17
+ // in the tree are NOT consulted, nor is the user's global excludesfile. That covers the case
18
+ // this exists for (a top-level ignored directory holding cloned repos) without shelling out to
19
+ // git, at the cost of missing rules declared further down.
20
+ //
21
+ const IGNORE_SOURCES = [".gitignore", Path.join(".git", "info", "exclude")];
22
+ const NO_RULES = {
23
+ ignores: () => false,
24
+ hasRules: false,
25
+ };
26
+ const readIfPresent = (file) => {
27
+ try {
28
+ return Fs.readFileSync(file, "utf8");
29
+ }
30
+ catch (err) {
31
+ if ((err === null || err === void 0 ? void 0 : err.code) !== "ENOENT" && (err === null || err === void 0 ? void 0 : err.code) !== "ENOTDIR") {
32
+ throw err;
33
+ }
34
+ return undefined;
35
+ }
36
+ };
37
+ /**
38
+ * Build a gitignore matcher for a repo root.
39
+ *
40
+ * Never throws for a missing or unreadable ignore file - a repo with no rules simply matches
41
+ * nothing.
42
+ *
43
+ * @param cwd - repo root to read ignore rules from
44
+ * @returns a matcher; {@link GitignoreMatcher.hasRules} is false when nothing was loaded
45
+ */
46
+ export function makeGitignoreMatcher(cwd) {
47
+ const sources = IGNORE_SOURCES.map((f) => readIfPresent(Path.join(cwd, f))).filter(Boolean);
48
+ if (sources.length === 0) {
49
+ return NO_RULES;
50
+ }
51
+ const ig = ignore().add(sources.join("\n"));
52
+ return {
53
+ hasRules: true,
54
+ ignores(relPath) {
55
+ // `ignore` rejects absolute paths and anything outside the root, and refuses "."
56
+ if (!relPath || relPath === "." || Path.isAbsolute(relPath)) {
57
+ return false;
58
+ }
59
+ // it wants posix separators relative to the root
60
+ const normalized = relPath.split(Path.sep).join("/").replace(/^\.\//, "");
61
+ if (!normalized || normalized.startsWith("..")) {
62
+ return false;
63
+ }
64
+ return ig.ignores(normalized);
65
+ },
66
+ };
67
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- export * from "./fynpo-dep-graph";
2
- export * from "./fynpo-config";
3
- export * from "./util";
4
- export * as caching from "./caching";
1
+ export * from "./fynpo-dep-graph.js";
2
+ export * from "./fynpo-config.js";
3
+ export * from "./packages-config.js";
4
+ export * from "./gitignore.js";
5
+ export * from "./util.js";
6
+ export * as caching from "./caching.js";
5
7
  /**
6
8
  * Information about a package within the mono-repo
7
9
  */
@@ -57,12 +59,19 @@ export type PackageInfo = {
57
59
  /**
58
60
  * Read the packages of a fynpo mono-repo
59
61
  *
60
- * @param patterns - array of minimatch patterns. default: `["packages/*"]`
62
+ * Honors the same `packages` config as {@link FynpoDepGraph}, so both discovery paths agree.
63
+ * Passing `patterns` directly still works and takes precedence, for callers that already know
64
+ * what they want.
65
+ *
66
+ * @param patterns - explicit minimatch patterns. Overrides whatever `packages` config says.
67
+ * @param cwd - repo root
68
+ * @param packages - raw `packages` config, resolved via {@link resolvePackagesConfig}
61
69
  * @returns - packages from the fynpo mono-repo
62
70
  */
63
- export declare function readFynpoPackages({ patterns, cwd, }?: {
71
+ export declare function readFynpoPackages({ patterns, cwd, packages, }?: {
64
72
  patterns?: string[];
65
73
  cwd?: string;
74
+ packages?: unknown;
66
75
  }): Promise<Record<string, PackageInfo>>;
67
76
  /**
68
77
  * calculate dep graphs for packages under the mono-repo
@@ -73,7 +82,9 @@ export declare function readFynpoPackages({ patterns, cwd, }?: {
73
82
  */
74
83
  export declare function makePkgDeps(packages: any, opts: any): {
75
84
  packages: any;
76
- depMap: any;
85
+ depMap: {
86
+ [x: string]: Pick<any, string>;
87
+ };
77
88
  circulars: any[];
78
89
  warnings: any[];
79
90
  only: any;