@fynpo/base 1.1.23 → 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,23 +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;
306
321
  this.autoSearched = autoSearch;
307
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);
308
327
  if (autoSearch) {
309
328
  groups = { ".": null };
310
329
  }
311
330
  else {
312
- const mms = patterns.map((p) => new minimatch_1.default.Minimatch(p));
313
- groups = (0, minimatch_group_1.groupMM)(mms, {});
331
+ const mms = resolved.map((p) => new Minimatch(p));
332
+ groups = groupMM(mms, {});
314
333
  }
315
334
  const foundInAutoSearch = (path) => {
316
335
  if (!autoSearch) {
317
336
  return false;
318
337
  }
319
338
  for (const e of autoSearchFound) {
320
- if ((0, is_path_inside_1.default)(path, e)) {
339
+ if (isPathInside(path, e)) {
321
340
  return true;
322
341
  }
323
342
  }
@@ -325,12 +344,15 @@ class FynpoDepGraph {
325
344
  };
326
345
  const files = [];
327
346
  for (const prefix in groups) {
328
- const scanned = await (0, filter_scan_dir_1.filterScanDir)({
347
+ const scanned = await filterScanDir({
329
348
  cwd,
330
349
  prefix,
331
350
  concurrency: 500,
332
351
  filter: (file, path, extras) => {
333
352
  if (path && path !== "." && file === "package.json") {
353
+ if (isExcluded(path) || skipForAutoSearch(path)) {
354
+ return false;
355
+ }
334
356
  if (extras.files.includes("fynpo.json") || foundInAutoSearch(path)) {
335
357
  //
336
358
  // We ignore dir with package.json if:
@@ -347,7 +369,7 @@ class FynpoDepGraph {
347
369
  },
348
370
  filterDir: (dir, path, extras) => {
349
371
  if (dir !== "node_modules") {
350
- if (foundInAutoSearch(path)) {
372
+ if (foundInAutoSearch(path) || isExcluded(path) || skipForAutoSearch(path)) {
351
373
  return false;
352
374
  }
353
375
  return prefix === "." && groups[prefix] === null
@@ -359,7 +381,9 @@ class FynpoDepGraph {
359
381
  });
360
382
  files.push(scanned);
361
383
  }
362
- const allFiles = [].concat(...files).sort();
384
+ const allFiles = [].concat(...files)
385
+ .filter((f) => isIncluded(Path.dirname(f)))
386
+ .sort();
363
387
  // Read each package.json and generate PackageInfo for it
364
388
  for (const pkgFile of allFiles) {
365
389
  await this.addPackageByFile(pkgFile);
@@ -373,9 +397,9 @@ class FynpoDepGraph {
373
397
  * @param pkgFile - path to the package.json file
374
398
  */
375
399
  async addPackageByFile(pkgFile) {
376
- 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");
377
401
  const pkgJson = JSON.parse(pkgStr);
378
- this.addPackage(pkgJson, path_1.default.dirname(pkgFile), pkgStr);
402
+ this.addPackage(pkgJson, Path.dirname(pkgFile), pkgStr);
379
403
  }
380
404
  /**
381
405
  * Add a package to the graph using the data from its package.json
@@ -389,17 +413,17 @@ class FynpoDepGraph {
389
413
  if (pkgJson.fynpo === false) {
390
414
  return;
391
415
  }
392
- (0, assert_1.default)(pkgJson.name, `package at ${pkgPath} doesn't have name`);
416
+ assert(pkgJson.name, `package at ${pkgPath} doesn't have name`);
393
417
  // check for npm scope
394
418
  const pkgDir = pkgJson.name[0] === "@" && (pkgPath.endsWith(`/${pkgJson.name}`) || pkgPath === pkgJson.name)
395
419
  ? pkgJson.name
396
- : path_1.default.basename(pkgPath);
420
+ : Path.basename(pkgPath);
397
421
  const pkgInfo = {
398
422
  name: pkgJson.name,
399
423
  version: pkgJson.version,
400
424
  path: pkgPath,
401
425
  pkgDir,
402
- ...lodash_1.default.pick(pkgJson, [
426
+ ..._.pick(pkgJson, [
403
427
  "private",
404
428
  "dependencies",
405
429
  "devDependencies",
@@ -441,7 +465,7 @@ class FynpoDepGraph {
441
465
  });
442
466
  // sort package with multiple versions from latest to oldest
443
467
  if (byName[name].length > 1) {
444
- 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));
445
469
  }
446
470
  }
447
471
  }
@@ -459,6 +483,38 @@ class FynpoDepGraph {
459
483
  }
460
484
  return undefined;
461
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
+ }
462
518
  /**
463
519
  * Figure out all the packages' direct dependencies on other local packages
464
520
  *
@@ -468,12 +524,16 @@ class FynpoDepGraph {
468
524
  const depMapByPath = this.depMapByPath;
469
525
  const doResolve = (depData, deps, section) => {
470
526
  const { pkgInfo } = depData;
471
- lodash_1.default.each(deps, (semver, name) => {
527
+ _.each(deps, (semver, name) => {
472
528
  // dep is not a local package in the monorepo, nothing to do
473
529
  /* istanbul ignore if */
474
530
  if (!byName[name]) {
475
531
  return;
476
532
  }
533
+ // check if this dep should not be resolved as local package
534
+ if (this.checkNoFynLocal(pkgInfo, name)) {
535
+ return;
536
+ }
477
537
  const semId = pkgId(name, semver);
478
538
  const resolveId = this.resolvedCache[semId];
479
539
  const depPkg = (resolveId && byId[resolveId]) || resolvePackage(semver, byName[name]);
@@ -551,7 +611,7 @@ class FynpoDepGraph {
551
611
  }
552
612
  return true;
553
613
  }
554
- return !lodash_1.default.isEmpty(dataDep.pathOfCirculars);
614
+ return !_.isEmpty(dataDep.pathOfCirculars);
555
615
  }
556
616
  /**
557
617
  * Add depdencies from a list of dep relations
@@ -605,7 +665,7 @@ class FynpoDepGraph {
605
665
  * @param dir - directory of package to get
606
666
  */
607
667
  getPackageAtDir(dir) {
608
- 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);
609
669
  return this.packages.byPath[path];
610
670
  }
611
671
  /**
@@ -614,7 +674,7 @@ class FynpoDepGraph {
614
674
  * @returns
615
675
  */
616
676
  getPackageByName(name) {
617
- return lodash_1.default.first(this.packages.byName[name]);
677
+ return _.first(this.packages.byName[name]);
618
678
  }
619
679
  /**
620
680
  * Figure out all the packages' indirect dependencies on other local packages
@@ -628,7 +688,7 @@ class FynpoDepGraph {
628
688
  const doResolve = (dataPkg, localDeps, steps, section) => {
629
689
  const { pkgInfo } = dataPkg;
630
690
  // go through all found deps
631
- lodash_1.default.each(localDeps, (depRef) => {
691
+ _.each(localDeps, (depRef) => {
632
692
  const sec = section || depRef.depSection;
633
693
  const depInfo = byPath[depRef.path];
634
694
  const dataDep = depMapByPath[depRef.path];
@@ -654,10 +714,8 @@ class FynpoDepGraph {
654
714
  doResolve(depData, depData.localDepsByPath, [pkgInfoId(depData.pkgInfo)]);
655
715
  }
656
716
  if (change > 0) {
657
- (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");
658
718
  this.resolveIndirectDeps(nestedLevel + 1);
659
719
  }
660
720
  }
661
721
  }
662
- exports.FynpoDepGraph = FynpoDepGraph;
663
- //# 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;