fyn 3.1.10 → 3.2.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.
- package/bin/fun.mjs +2 -1
- package/bin/fyn.mjs +2 -1
- package/dist/fyn.mjs +951 -47
- package/package.json +4 -5
package/bin/fun.mjs
CHANGED
package/bin/fyn.mjs
CHANGED
package/dist/fyn.mjs
CHANGED
|
@@ -26180,7 +26180,7 @@ function simpleCompare(a, b) {
|
|
|
26180
26180
|
function isVersionNewer(a, b) {
|
|
26181
26181
|
return simpleCompare(a, b) < 0;
|
|
26182
26182
|
}
|
|
26183
|
-
function clean(v) {
|
|
26183
|
+
function clean$1(v) {
|
|
26184
26184
|
const f = v.split("-");
|
|
26185
26185
|
f[0] = f[0].split("+")[0].split(".").map((x) => x && parseInt(x, 10) || 0).join(".");
|
|
26186
26186
|
return f.join("-");
|
|
@@ -26943,6 +26943,11 @@ const PACKAGE_FYN_JSON = "package-fyn.json";
|
|
|
26943
26943
|
*/
|
|
26944
26944
|
const FYN_INSTALL_CONFIG_FILE = ".fyn.json";
|
|
26945
26945
|
const FYN_LOCK_FILE = "fyn-lock.yaml";
|
|
26946
|
+
/**
|
|
26947
|
+
* Copy of the lock data saved under FV_DIR. It always matches what is installed in
|
|
26948
|
+
* node_modules. Install uses it when FYN_LOCK_FILE is missing.
|
|
26949
|
+
*/
|
|
26950
|
+
const FYN_INSTALLED_LOCK_FILE = "lock.yaml";
|
|
26946
26951
|
|
|
26947
26952
|
//#endregion
|
|
26948
26953
|
//#region node_modules/filter-scan-dir/dist/util.js
|
|
@@ -30697,7 +30702,7 @@ var FynpoConfigManager = class {
|
|
|
30697
30702
|
* Pipe streams together and resolve when the pipeline fully completes.
|
|
30698
30703
|
* Replaces mississippi.pipe with node:stream/promises pipeline.
|
|
30699
30704
|
*/
|
|
30700
|
-
const missPipe$3 = (
|
|
30705
|
+
const missPipe$3 = (source, destination) => pipeline(source, destination);
|
|
30701
30706
|
const DIR_SYMLINK_TYPE = isWin32 ? "junction" : "dir";
|
|
30702
30707
|
/**
|
|
30703
30708
|
* Check if a value satisfies a list of rules.
|
|
@@ -30779,7 +30784,7 @@ const fyntil = {
|
|
|
30779
30784
|
if (parent === treeTop) break;
|
|
30780
30785
|
treeTop = parent;
|
|
30781
30786
|
}
|
|
30782
|
-
if (!gitPath || gitStat
|
|
30787
|
+
if (!gitPath || gitStat?.isDirectory()) return dir;
|
|
30783
30788
|
try {
|
|
30784
30789
|
const m = (await file_ops_default.readFile(gitPath, "utf8")).match(/^gitdir:\s*(.+)$/m);
|
|
30785
30790
|
if (!m) return dir;
|
|
@@ -30870,7 +30875,7 @@ const fyntil = {
|
|
|
30870
30875
|
existTarget = false;
|
|
30871
30876
|
try {
|
|
30872
30877
|
await file_ops_default.unlink(linkName);
|
|
30873
|
-
} catch
|
|
30878
|
+
} catch {
|
|
30874
30879
|
await file_ops_default.$.rimraf(linkName);
|
|
30875
30880
|
}
|
|
30876
30881
|
} else logger$1.debug("local link existTarget", existTarget, "match new target", actualTarget);
|
|
@@ -32117,7 +32122,6 @@ var PkgDepResolver = class {
|
|
|
32117
32122
|
};
|
|
32118
32123
|
if (item.specifier) newPkgV.specifier = item.specifier;
|
|
32119
32124
|
pkgV = kpkg.versions[resolved] = newPkgV;
|
|
32120
|
-
if (meta[LOCK_RSEMVERS]) pkgV.fromLock = true;
|
|
32121
32125
|
const scripts = metaJson.scripts || {};
|
|
32122
32126
|
if (metaJson.hasPI || scripts.preinstall || scripts.preInstall) pkgV.hasPI = 1;
|
|
32123
32127
|
if (metaJson.hasI || scripts.install || scripts.postinstall || scripts.postInstall) pkgV.hasI = 1;
|
|
@@ -94231,7 +94235,8 @@ var PkgSrcManager = class {
|
|
|
94231
94235
|
email: this._options.email,
|
|
94232
94236
|
alwaysAuth: this._options["always-auth"],
|
|
94233
94237
|
username: this._options.username,
|
|
94234
|
-
password: this._options.password
|
|
94238
|
+
password: this._options.password,
|
|
94239
|
+
maxSockets: this._fyn.concurrency
|
|
94235
94240
|
}, authTokens, registryData);
|
|
94236
94241
|
this._pacoteOpts.Arborist = import_lib$5.default;
|
|
94237
94242
|
this._regData = registryData;
|
|
@@ -96473,7 +96478,12 @@ var PkgDepLocker = class {
|
|
|
96473
96478
|
}
|
|
96474
96479
|
const badDepsVersion = Object.entries(locked.versions).find(([, vpkg]) => vpkg.dependencies && !this._depsResolvable(item, vpkg.dependencies))?.[0];
|
|
96475
96480
|
if (badDepsVersion) {
|
|
96476
|
-
|
|
96481
|
+
const entry = `${item.name}@${badDepsVersion}`;
|
|
96482
|
+
const msgKey = `staleLock:${entry}`;
|
|
96483
|
+
if (!this._fyn._shownMissingFiles.has(msgKey)) {
|
|
96484
|
+
this._fyn._shownMissingFiles.add(msgKey);
|
|
96485
|
+
this._fyn._staleLockEntries.push(entry);
|
|
96486
|
+
}
|
|
96477
96487
|
return false;
|
|
96478
96488
|
}
|
|
96479
96489
|
return valid && locked;
|
|
@@ -96633,12 +96643,7 @@ var PkgDepLocker = class {
|
|
|
96633
96643
|
let resolvedFilename = filename;
|
|
96634
96644
|
if (!Path.isAbsolute(resolvedFilename)) resolvedFilename = Path.resolve(resolvedFilename);
|
|
96635
96645
|
if (!this._lockOnly) {
|
|
96636
|
-
|
|
96637
|
-
const basedir = Path.dirname(resolvedFilename);
|
|
96638
|
-
this._lockData.$fyn = this._config;
|
|
96639
|
-
const sortData = sortObjKeys(this._lockData);
|
|
96640
|
-
this._relativeLocalPath(basedir, sortData);
|
|
96641
|
-
const data = import_Yaml.default.stringify(sortData, 4, 1);
|
|
96646
|
+
const data = this._stringify(Path.dirname(resolvedFilename));
|
|
96642
96647
|
if (this.shasum(data) !== this._shaSum) {
|
|
96643
96648
|
logger$1.info("saving lock file", resolvedFilename);
|
|
96644
96649
|
file_ops_default.writeFileSync(resolvedFilename, data);
|
|
@@ -96646,6 +96651,33 @@ var PkgDepLocker = class {
|
|
|
96646
96651
|
}
|
|
96647
96652
|
}
|
|
96648
96653
|
/**
|
|
96654
|
+
* Save the lock copy that tracks what's installed in node_modules. Unlike save,
|
|
96655
|
+
* it always writes, even in lock-only mode, so it matches every install.
|
|
96656
|
+
*/
|
|
96657
|
+
saveInstalled(filename) {
|
|
96658
|
+
if (!this._enable) return;
|
|
96659
|
+
file_ops_default.writeFileSync(filename, this._stringify(Path.dirname(filename)));
|
|
96660
|
+
}
|
|
96661
|
+
/**
|
|
96662
|
+
* Read the lock copy the last install saved in node_modules. The outside lockfile
|
|
96663
|
+
* is missing, so reset the checksum to make save write it again.
|
|
96664
|
+
*/
|
|
96665
|
+
async readInstalled(filename) {
|
|
96666
|
+
const found = await this.read(filename);
|
|
96667
|
+
this._shaSum = Date.now();
|
|
96668
|
+
return found;
|
|
96669
|
+
}
|
|
96670
|
+
/**
|
|
96671
|
+
* Serialize lock data, with local package paths relative to basedir
|
|
96672
|
+
*/
|
|
96673
|
+
_stringify(basedir) {
|
|
96674
|
+
assert(this._isFynFormat, "can't save lock data that's no longer in fyn format");
|
|
96675
|
+
this._lockData.$fyn = this._config;
|
|
96676
|
+
const sortData = sortObjKeys(this._lockData);
|
|
96677
|
+
this._relativeLocalPath(basedir, sortData);
|
|
96678
|
+
return import_Yaml.default.stringify(sortData, 4, 1);
|
|
96679
|
+
}
|
|
96680
|
+
/**
|
|
96649
96681
|
* Read lock file
|
|
96650
96682
|
*/
|
|
96651
96683
|
async read(filename) {
|
|
@@ -96722,11 +96754,6 @@ var DepData = class {
|
|
|
96722
96754
|
this.pkgs = {};
|
|
96723
96755
|
Object.keys(pkgs).sort().forEach((x) => this.pkgs[x] = pkgs[x]);
|
|
96724
96756
|
}
|
|
96725
|
-
cleanLinked() {
|
|
96726
|
-
this.eachVersion((pkg) => {
|
|
96727
|
-
pkg.linked = 0;
|
|
96728
|
-
});
|
|
96729
|
-
}
|
|
96730
96757
|
getPkgsData(bad) {
|
|
96731
96758
|
return bad ? this.badPkgs : this.pkgs;
|
|
96732
96759
|
}
|
|
@@ -96742,15 +96769,6 @@ var DepData = class {
|
|
|
96742
96769
|
if (!kpkg) return void 0;
|
|
96743
96770
|
return version ? kpkg.versions[version] : kpkg;
|
|
96744
96771
|
}
|
|
96745
|
-
eachVersion(cb) {
|
|
96746
|
-
const pkgs = this.pkgs;
|
|
96747
|
-
Object.keys(pkgs).forEach((name) => {
|
|
96748
|
-
const kpkg = pkgs[name];
|
|
96749
|
-
Object.entries(kpkg.versions).forEach(([version, pkg]) => {
|
|
96750
|
-
cb(pkg, version, kpkg);
|
|
96751
|
-
});
|
|
96752
|
-
});
|
|
96753
|
-
}
|
|
96754
96772
|
};
|
|
96755
96773
|
|
|
96756
96774
|
//#endregion
|
|
@@ -100413,14 +100431,6 @@ var PkgDepLinker = class {
|
|
|
100413
100431
|
async loadLocalPackageAppFynLink() {
|
|
100414
100432
|
throw new Error("symlink local package is deprecated, only hard linking.");
|
|
100415
100433
|
}
|
|
100416
|
-
async loadPkgDepData(depInfo) {
|
|
100417
|
-
const installedDir = this._fyn.getInstalledPkgDir(depInfo.name, depInfo.version, depInfo);
|
|
100418
|
-
if (!depInfo.json) {
|
|
100419
|
-
const fname = Path.join(installedDir, "package.json");
|
|
100420
|
-
depInfo.json = JSON.parse(await file_ops_default.readFile(fname));
|
|
100421
|
-
}
|
|
100422
|
-
if (depInfo.local === "sym") throw new Error("sym linking local package deprecated. only hard linking.");
|
|
100423
|
-
}
|
|
100424
100434
|
};
|
|
100425
100435
|
|
|
100426
100436
|
//#endregion
|
|
@@ -101351,6 +101361,7 @@ var Fyn = class Fyn {
|
|
|
101351
101361
|
_shownMissingFiles;
|
|
101352
101362
|
_localCopiesInfo;
|
|
101353
101363
|
_skipBuildLocalPaths;
|
|
101364
|
+
_staleLockEntries;
|
|
101354
101365
|
_options;
|
|
101355
101366
|
_cwd;
|
|
101356
101367
|
_initCwd;
|
|
@@ -101399,6 +101410,7 @@ var Fyn = class Fyn {
|
|
|
101399
101410
|
this._shownMissingFiles = /* @__PURE__ */ new Set();
|
|
101400
101411
|
this._localCopiesInfo = [];
|
|
101401
101412
|
this._skipBuildLocalPaths = [];
|
|
101413
|
+
this._staleLockEntries = [];
|
|
101402
101414
|
const options = this._options = fynConfig(opts);
|
|
101403
101415
|
this._cwd = options.cwd || process.cwd();
|
|
101404
101416
|
this._initCwd = options.initCwd || this._cwd;
|
|
@@ -101446,7 +101458,11 @@ var Fyn = class Fyn {
|
|
|
101446
101458
|
if (this._depLocker) return null;
|
|
101447
101459
|
this._npmLockData = null;
|
|
101448
101460
|
this._depLocker = new PkgDepLocker(Boolean(this.lockOnly), Boolean(this._options.lockfile), this);
|
|
101449
|
-
|
|
101461
|
+
let foundLock = await this._depLocker.read(Path.join(this._cwd, FYN_LOCK_FILE));
|
|
101462
|
+
if (!foundLock && !this.lockOnly && this._options.lockfile) {
|
|
101463
|
+
foundLock = await this._depLocker.readInstalled(this.getInstalledLockFile());
|
|
101464
|
+
if (foundLock) logger$1.info(`${FYN_LOCK_FILE} not found - using lock data from last install. Remove ${this._options.targetDir} to resolve everything again.`);
|
|
101465
|
+
}
|
|
101450
101466
|
this.updateConfigInLockfile("layout", this._options.layout);
|
|
101451
101467
|
this.updateConfigInLockfile("flattenTop", this._options.flattenTop);
|
|
101452
101468
|
this.checkLayoutOption();
|
|
@@ -101802,6 +101818,9 @@ var Fyn = class Fyn {
|
|
|
101802
101818
|
getInstallConfigFile() {
|
|
101803
101819
|
return Path.join(this.getFvDir(FYN_INSTALL_CONFIG_FILE));
|
|
101804
101820
|
}
|
|
101821
|
+
getInstalledLockFile() {
|
|
101822
|
+
return this.getFvDir(FYN_INSTALLED_LOCK_FILE);
|
|
101823
|
+
}
|
|
101805
101824
|
setLocalPkgLinks(localLinks) {
|
|
101806
101825
|
this._installConfig.localPkgLinks = localLinks;
|
|
101807
101826
|
}
|
|
@@ -102186,6 +102205,7 @@ var Fyn = class Fyn {
|
|
|
102186
102205
|
const lines = this._localCopiesInfo.map(({ pkgName, depSec, names }) => ` ${pkgName} ${depSec}: ${names}`);
|
|
102187
102206
|
logger$1.info(`Following package and their dependencies using local copies from monorepo:\n${lines.join("\n")}`);
|
|
102188
102207
|
}
|
|
102208
|
+
if (this._staleLockEntries.length > 0) logger$1.verbose(`lockfile stale for following entries - refreshing:\n ${this._staleLockEntries.join(", ")}`);
|
|
102189
102209
|
if (this._skipBuildLocalPaths.length > 0) {
|
|
102190
102210
|
const lines = this._skipBuildLocalPaths.map((x) => ` ${x}`);
|
|
102191
102211
|
logger$1.info(`installing fynpo top level modules - skip build local for packages at:\n${lines.join("\n")}`);
|
|
@@ -102349,7 +102369,7 @@ var Fyn = class Fyn {
|
|
|
102349
102369
|
const pkgId = `${pkg.name}@${pkg.version}`;
|
|
102350
102370
|
const id = `${json.name}@${json.version}`;
|
|
102351
102371
|
if (json.version !== pkg.version) {
|
|
102352
|
-
const cleanVersion = import_semver.default.valid(json.version) || clean(json.version);
|
|
102372
|
+
const cleanVersion = import_semver.default.valid(json.version) || clean$1(json.version);
|
|
102353
102373
|
if (cleanVersion !== json.version) {
|
|
102354
102374
|
assert(import_semver.default.valid(cleanVersion), `Pkg ${id} version is not valid semver and fyn was unable to fix it.`);
|
|
102355
102375
|
json._origVersion = json.version;
|
|
@@ -103451,6 +103471,7 @@ var PkgInstaller = class {
|
|
|
103451
103471
|
async install() {
|
|
103452
103472
|
this._stepTime = Date.now();
|
|
103453
103473
|
this.timeCheck("beginning");
|
|
103474
|
+
await file_ops_default.unlink(this._fyn.getInstalledLockFile()).catch(() => void 0);
|
|
103454
103475
|
const outputDir = this._fyn.getOutputDir();
|
|
103455
103476
|
this._binLinker = new PkgBinLinker({
|
|
103456
103477
|
outputDir,
|
|
@@ -103462,7 +103483,6 @@ var PkgInstaller = class {
|
|
|
103462
103483
|
this.blockedScripts = [];
|
|
103463
103484
|
this.pendingScripts = [];
|
|
103464
103485
|
this._blockedDeps = [];
|
|
103465
|
-
this._data.cleanLinked();
|
|
103466
103486
|
this._fyn._depResolver.resolvePkgPeerDep(this._fyn._pkg, "your app", this._data);
|
|
103467
103487
|
const pkgsData = this._data.getPkgsData();
|
|
103468
103488
|
this.timeCheck("queueing packages");
|
|
@@ -104030,11 +104050,10 @@ var PkgInstaller = class {
|
|
|
104030
104050
|
}
|
|
104031
104051
|
}
|
|
104032
104052
|
_saveLockData() {
|
|
104033
|
-
|
|
104034
|
-
|
|
104035
|
-
|
|
104036
|
-
|
|
104037
|
-
}
|
|
104053
|
+
const locker = this._fyn._depLocker || new PkgDepLocker(false, true, this._fyn);
|
|
104054
|
+
locker.generate(this._fyn._data);
|
|
104055
|
+
if (!this._fyn.lockOnly) locker.save(Path.join(this._fyn.cwd, FYN_LOCK_FILE));
|
|
104056
|
+
locker.saveInstalled(this._fyn.getInstalledLockFile());
|
|
104038
104057
|
}
|
|
104039
104058
|
};
|
|
104040
104059
|
|
|
@@ -104759,7 +104778,7 @@ var AuditReport = class {
|
|
|
104759
104778
|
Object.keys(kpkg.versions).forEach((version) => {
|
|
104760
104779
|
const pkgInfo = kpkg.versions[version];
|
|
104761
104780
|
if (this.isOmitted(pkgInfo)) return;
|
|
104762
|
-
if (pkgInfo.local
|
|
104781
|
+
if (pkgInfo.local) return;
|
|
104763
104782
|
if (!payload[name]) payload[name] = [];
|
|
104764
104783
|
if (!payload[name].includes(version)) payload[name].push(version);
|
|
104765
104784
|
});
|
|
@@ -105594,6 +105613,851 @@ const __filename$1 = fileURLToPath$1(import.meta.url);
|
|
|
105594
105613
|
const __dirname$1 = Path.dirname(__filename$1);
|
|
105595
105614
|
const myPkg = JSON.parse(fs.readFileSync(Path.join(__dirname$1, "../package.json"), "utf-8"));
|
|
105596
105615
|
|
|
105616
|
+
//#endregion
|
|
105617
|
+
//#region lib/prod-prune/licenses.ts
|
|
105618
|
+
/**
|
|
105619
|
+
* Recognisable licenses, most specific first, matched against the text with its
|
|
105620
|
+
* case and whitespace normalised. Each phrase is both the fingerprint of the
|
|
105621
|
+
* license and the point its terms begin, so it must be the opening words of the
|
|
105622
|
+
* license proper and cannot be wording another license shares.
|
|
105623
|
+
*/
|
|
105624
|
+
const SIGNATURES = [
|
|
105625
|
+
{
|
|
105626
|
+
id: "Apache-2.0",
|
|
105627
|
+
name: "Apache License, Version 2.0",
|
|
105628
|
+
phrase: "apache license version 2.0 january 2004"
|
|
105629
|
+
},
|
|
105630
|
+
{
|
|
105631
|
+
id: "MPL-2.0",
|
|
105632
|
+
name: "Mozilla Public License, Version 2.0",
|
|
105633
|
+
phrase: "mozilla public license version 2.0"
|
|
105634
|
+
},
|
|
105635
|
+
{
|
|
105636
|
+
id: "CC0-1.0",
|
|
105637
|
+
name: "Creative Commons Zero, Version 1.0 Universal",
|
|
105638
|
+
phrase: "cc0 1.0 universal"
|
|
105639
|
+
},
|
|
105640
|
+
{
|
|
105641
|
+
id: "BlueOak-1.0.0",
|
|
105642
|
+
name: "Blue Oak Model License, Version 1.0.0",
|
|
105643
|
+
phrase: "blue oak model license version 1.0.0"
|
|
105644
|
+
},
|
|
105645
|
+
{
|
|
105646
|
+
id: "Unlicense",
|
|
105647
|
+
name: "The Unlicense",
|
|
105648
|
+
phrase: "this is free and unencumbered software released into the public domain"
|
|
105649
|
+
},
|
|
105650
|
+
{
|
|
105651
|
+
id: "Python-2.0",
|
|
105652
|
+
name: "Python Software Foundation License, Version 2",
|
|
105653
|
+
phrase: "python software foundation license version 2"
|
|
105654
|
+
},
|
|
105655
|
+
{
|
|
105656
|
+
id: "ISC",
|
|
105657
|
+
name: "ISC License",
|
|
105658
|
+
phrase: "permission to use copy modify and/or distribute this software for any purpose with or without fee is hereby granted provided that the above copyright notice"
|
|
105659
|
+
},
|
|
105660
|
+
{
|
|
105661
|
+
id: "0BSD",
|
|
105662
|
+
name: "BSD Zero Clause License",
|
|
105663
|
+
phrase: "permission to use copy modify and/or distribute this software for any purpose with or without fee is hereby granted"
|
|
105664
|
+
},
|
|
105665
|
+
{
|
|
105666
|
+
id: "MIT",
|
|
105667
|
+
name: "MIT License",
|
|
105668
|
+
phrase: "permission is hereby granted free of charge to any person obtaining a copy"
|
|
105669
|
+
},
|
|
105670
|
+
{
|
|
105671
|
+
id: "BSD-2-Clause",
|
|
105672
|
+
name: "BSD 2-Clause License",
|
|
105673
|
+
phrase: "redistribution and use in source and binary forms",
|
|
105674
|
+
refine: (flat) => flat.includes("neither the name") ? {
|
|
105675
|
+
id: "BSD-3-Clause",
|
|
105676
|
+
name: "BSD 3-Clause License"
|
|
105677
|
+
} : null
|
|
105678
|
+
}
|
|
105679
|
+
];
|
|
105680
|
+
/** Words a line may consist of and still be only a title, not attribution. */
|
|
105681
|
+
const TITLE_WORDS = new Set("the mit isc bsd apache mozilla blue oak model unlicense python software foundation public license licence clause zero universal version cc0 0 1 2 3 1.0.0 2.0 v2 creative commons".split(" "));
|
|
105682
|
+
/**
|
|
105683
|
+
* Reduce a text to what it actually says, so wording can be compared without
|
|
105684
|
+
* layout getting in the way.
|
|
105685
|
+
*
|
|
105686
|
+
* Every run of anything that is not a letter, digit, `/` or `.` collapses to a
|
|
105687
|
+
* single space, which takes care of line wrapping and of the packages that
|
|
105688
|
+
* indent every line of their license. Trailing punctuation then goes too:
|
|
105689
|
+
* DefinitelyTyped ends its MIT with "SOFTWARE" where everyone else writes
|
|
105690
|
+
* "SOFTWARE.", and a full stop is not a difference in terms.
|
|
105691
|
+
*
|
|
105692
|
+
* `.` and `/` survive mid-string because the signature phrases need them —
|
|
105693
|
+
* "version 2.0 january 2004", "cc0 1.0 universal", "and/or distribute".
|
|
105694
|
+
*/
|
|
105695
|
+
const normalise = (text) => text.toLowerCase().replace(/[^a-z0-9/.]+/g, " ").trim().replace(/[./]+$/, "");
|
|
105696
|
+
/**
|
|
105697
|
+
* @param text license file contents
|
|
105698
|
+
*/
|
|
105699
|
+
function detectLicense(text) {
|
|
105700
|
+
const flat = normalise(text);
|
|
105701
|
+
for (const signature of SIGNATURES) {
|
|
105702
|
+
if (!flat.includes(signature.phrase)) continue;
|
|
105703
|
+
const refined = signature.refine?.(flat);
|
|
105704
|
+
return {
|
|
105705
|
+
id: refined?.id ?? signature.id,
|
|
105706
|
+
name: refined?.name ?? signature.name,
|
|
105707
|
+
phrase: signature.phrase
|
|
105708
|
+
};
|
|
105709
|
+
}
|
|
105710
|
+
return {
|
|
105711
|
+
id: "",
|
|
105712
|
+
name: "",
|
|
105713
|
+
phrase: ""
|
|
105714
|
+
};
|
|
105715
|
+
}
|
|
105716
|
+
const isTitleOnly = (line) => {
|
|
105717
|
+
const words = normalise(line).split(" ").filter(Boolean);
|
|
105718
|
+
return words.length > 0 && words.every((word) => TITLE_WORDS.has(word));
|
|
105719
|
+
};
|
|
105720
|
+
/**
|
|
105721
|
+
* Split a license file into the package's own attribution and the license terms.
|
|
105722
|
+
*
|
|
105723
|
+
* @param text
|
|
105724
|
+
* @param phrase opening words of the terms, normalised; `""` when the license
|
|
105725
|
+
* was not recognised, in which case the whole text is the terms
|
|
105726
|
+
*/
|
|
105727
|
+
function splitAtTerms(text, phrase) {
|
|
105728
|
+
const lines = text.split("\n");
|
|
105729
|
+
let start = 0;
|
|
105730
|
+
if (phrase) {
|
|
105731
|
+
start = lines.length;
|
|
105732
|
+
for (let i = 0; i < lines.length; i++) if (normalise(lines.slice(i).join("\n")).startsWith(phrase)) {
|
|
105733
|
+
start = i;
|
|
105734
|
+
break;
|
|
105735
|
+
}
|
|
105736
|
+
if (start === lines.length) start = 0;
|
|
105737
|
+
}
|
|
105738
|
+
return {
|
|
105739
|
+
attribution: lines.slice(0, start).map((line) => line.trim().replace(/\s+/g, " ")).filter((line) => line && !isTitleOnly(line)),
|
|
105740
|
+
terms: lines.slice(start).join("\n").trim()
|
|
105741
|
+
};
|
|
105742
|
+
}
|
|
105743
|
+
/**
|
|
105744
|
+
* Merge collected notices into the contents of a single file.
|
|
105745
|
+
*
|
|
105746
|
+
* Everything is done from what the caller already read; nothing is re-read from
|
|
105747
|
+
* disk. Output is fully sorted, so identical input always produces identical
|
|
105748
|
+
* bytes and the image layer stays reproducible.
|
|
105749
|
+
*
|
|
105750
|
+
* @param entries
|
|
105751
|
+
*/
|
|
105752
|
+
function combineLicenses(entries) {
|
|
105753
|
+
const groups = /* @__PURE__ */ new Map();
|
|
105754
|
+
for (const entry of entries) {
|
|
105755
|
+
const text = entry.text.trim();
|
|
105756
|
+
if (!text) continue;
|
|
105757
|
+
const license = /^notice/i.test(entry.file) ? {
|
|
105758
|
+
id: "NOTICE",
|
|
105759
|
+
name: "Attribution notice",
|
|
105760
|
+
phrase: ""
|
|
105761
|
+
} : detectLicense(text);
|
|
105762
|
+
const { attribution, terms } = splitAtTerms(text, license.phrase);
|
|
105763
|
+
const key = `${license.id}\u0000${normalise(terms)}`;
|
|
105764
|
+
if (!groups.has(key)) groups.set(key, {
|
|
105765
|
+
license,
|
|
105766
|
+
terms,
|
|
105767
|
+
packages: /* @__PURE__ */ new Map()
|
|
105768
|
+
});
|
|
105769
|
+
const group = groups.get(key);
|
|
105770
|
+
const notices = group.packages.get(entry.package) ?? /* @__PURE__ */ new Set();
|
|
105771
|
+
for (const line of attribution) notices.add(line);
|
|
105772
|
+
group.packages.set(entry.package, notices);
|
|
105773
|
+
}
|
|
105774
|
+
const collected = [...groups.values()].map((group) => ({
|
|
105775
|
+
...group,
|
|
105776
|
+
packages: [...group.packages].map(([name, notices]) => ({
|
|
105777
|
+
name,
|
|
105778
|
+
notices: [...notices]
|
|
105779
|
+
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
105780
|
+
})).sort((a, b) => b.packages.length - a.packages.length || a.license.id.localeCompare(b.license.id));
|
|
105781
|
+
const packages = /* @__PURE__ */ new Set();
|
|
105782
|
+
for (const group of collected) for (const entry of group.packages) packages.add(entry.name);
|
|
105783
|
+
const out = [
|
|
105784
|
+
"Third-party license and copyright notices",
|
|
105785
|
+
"=========================================",
|
|
105786
|
+
"",
|
|
105787
|
+
`${packages.size} packages installed under this directory ship a license or notice file.`,
|
|
105788
|
+
`They use ${collected.length} distinct ${collected.length === 1 ? "set of terms" : "sets of terms"}, reproduced once each below, with the`,
|
|
105789
|
+
"packages that use them, and their copyright notices, listed above each.",
|
|
105790
|
+
""
|
|
105791
|
+
];
|
|
105792
|
+
for (const group of collected) {
|
|
105793
|
+
out.push(RULE, group.license.id ? `${group.license.name} (${group.license.id})` : "Unrecognised license", "-".repeat(RULE.length), "");
|
|
105794
|
+
for (const entry of group.packages) {
|
|
105795
|
+
out.push(entry.name);
|
|
105796
|
+
for (const notice of entry.notices) out.push(` ${notice}`);
|
|
105797
|
+
}
|
|
105798
|
+
out.push("", RULE, "", group.terms, "", "");
|
|
105799
|
+
}
|
|
105800
|
+
return {
|
|
105801
|
+
content: out.join("\n"),
|
|
105802
|
+
licenses: collected.length,
|
|
105803
|
+
packages: packages.size
|
|
105804
|
+
};
|
|
105805
|
+
}
|
|
105806
|
+
const RULE = "=".repeat(78);
|
|
105807
|
+
|
|
105808
|
+
//#endregion
|
|
105809
|
+
//#region lib/prod-prune/rules.ts
|
|
105810
|
+
/**
|
|
105811
|
+
* What counts as non-runtime material in an installed `node_modules` tree.
|
|
105812
|
+
*
|
|
105813
|
+
* ESM, TypeScript. See index.ts for this module's own reasoning.
|
|
105814
|
+
*
|
|
105815
|
+
* Every rule is keyed on what a file *is*, never on which package ships it. A
|
|
105816
|
+
* rule that has to name a package is only correct for the dependency tree as it
|
|
105817
|
+
* looked on the day it was written, and it rots silently — the failure mode
|
|
105818
|
+
* being a runtime crash in a deployed image that no build-time check catches.
|
|
105819
|
+
*
|
|
105820
|
+
* License and notice files are never removed by these rules — they are gathered
|
|
105821
|
+
* into a single combined file first (see `isLicenseFile`). MIT, BSD and Apache
|
|
105822
|
+
* all require the copyright notice to travel with redistributed code, and
|
|
105823
|
+
* shipping a container image is redistribution.
|
|
105824
|
+
*
|
|
105825
|
+
* Build formats are deliberately not touched either. A package that publishes
|
|
105826
|
+
* both CJS and ESM has both trees reachable, and `require-in-the-middle` (which
|
|
105827
|
+
* @sentry/node and @opentelemetry monkey-patch through) needs the CJS one
|
|
105828
|
+
* present. Deleting a format risks a runtime failure no build-time check sees.
|
|
105829
|
+
* The one exception is `isBundlerOnlyEsnext`, which node provably cannot reach.
|
|
105830
|
+
*/
|
|
105831
|
+
/**
|
|
105832
|
+
* Removed whole, wherever they appear in the tree, except as a package's own
|
|
105833
|
+
* directory (see `isPackageRoot`).
|
|
105834
|
+
*/
|
|
105835
|
+
const REMOVE_DIRS = /* @__PURE__ */ new Set([
|
|
105836
|
+
"test",
|
|
105837
|
+
"tests",
|
|
105838
|
+
"__tests__",
|
|
105839
|
+
"spec",
|
|
105840
|
+
"__mocks__",
|
|
105841
|
+
"docs",
|
|
105842
|
+
"doc",
|
|
105843
|
+
"example",
|
|
105844
|
+
"examples",
|
|
105845
|
+
"benchmark",
|
|
105846
|
+
"benchmarks",
|
|
105847
|
+
"coverage",
|
|
105848
|
+
".nyc_output",
|
|
105849
|
+
".github",
|
|
105850
|
+
".circleci",
|
|
105851
|
+
".husky",
|
|
105852
|
+
".idea",
|
|
105853
|
+
".vscode"
|
|
105854
|
+
]);
|
|
105855
|
+
/**
|
|
105856
|
+
* Removed by extension, compared lower-case. `.ts` covers `.d.ts`, since the
|
|
105857
|
+
* extension is taken from the last dot; both are compile-time only. `.mts` and
|
|
105858
|
+
* `.cts` are the same in the module and commonjs flavours — node loads `.mjs`
|
|
105859
|
+
* and `.cjs`, never their TypeScript spellings, so nothing there is reachable
|
|
105860
|
+
* either.
|
|
105861
|
+
*/
|
|
105862
|
+
const REMOVE_EXTS = /* @__PURE__ */ new Set([
|
|
105863
|
+
".ts",
|
|
105864
|
+
".mts",
|
|
105865
|
+
".cts",
|
|
105866
|
+
".map",
|
|
105867
|
+
".tsbuildinfo",
|
|
105868
|
+
".md",
|
|
105869
|
+
".markdown",
|
|
105870
|
+
".log",
|
|
105871
|
+
".tmp",
|
|
105872
|
+
".temp",
|
|
105873
|
+
".bak",
|
|
105874
|
+
".orig",
|
|
105875
|
+
".rej",
|
|
105876
|
+
".swp",
|
|
105877
|
+
".swo"
|
|
105878
|
+
]);
|
|
105879
|
+
/** Removed by exact name, case-sensitive. */
|
|
105880
|
+
const REMOVE_NAMES = /* @__PURE__ */ new Set([
|
|
105881
|
+
".editorconfig",
|
|
105882
|
+
".npmignore",
|
|
105883
|
+
".gitignore",
|
|
105884
|
+
".gitattributes",
|
|
105885
|
+
".jshintrc",
|
|
105886
|
+
".taprc",
|
|
105887
|
+
".c8rc",
|
|
105888
|
+
".nojekyll",
|
|
105889
|
+
".runkit_example.js",
|
|
105890
|
+
"Makefile",
|
|
105891
|
+
".DS_Store",
|
|
105892
|
+
"Thumbs.db"
|
|
105893
|
+
]);
|
|
105894
|
+
/**
|
|
105895
|
+
* YAML removed by exact name, compared lower-case. YAML is not removed by
|
|
105896
|
+
* extension: a package may read its own `.yaml` config, schema or spec at
|
|
105897
|
+
* runtime. These names are CI and tooling config that no package loads.
|
|
105898
|
+
*/
|
|
105899
|
+
const REMOVE_YAML_NAMES = /* @__PURE__ */ new Set([
|
|
105900
|
+
".travis.yml",
|
|
105901
|
+
"appveyor.yml",
|
|
105902
|
+
".appveyor.yml",
|
|
105903
|
+
"azure-pipelines.yml",
|
|
105904
|
+
"bitbucket-pipelines.yml",
|
|
105905
|
+
".drone.yml",
|
|
105906
|
+
".gitlab-ci.yml",
|
|
105907
|
+
".codeclimate.yml",
|
|
105908
|
+
"codecov.yml",
|
|
105909
|
+
".codecov.yml",
|
|
105910
|
+
".readthedocs.yml",
|
|
105911
|
+
".readthedocs.yaml",
|
|
105912
|
+
".pre-commit-config.yaml",
|
|
105913
|
+
".yarnrc.yml",
|
|
105914
|
+
"pnpm-lock.yaml",
|
|
105915
|
+
"pnpm-workspace.yaml",
|
|
105916
|
+
".mocharc.yml",
|
|
105917
|
+
".mocharc.yaml",
|
|
105918
|
+
".nycrc.yml",
|
|
105919
|
+
".nycrc.yaml",
|
|
105920
|
+
".lintstagedrc.yml",
|
|
105921
|
+
".lintstagedrc.yaml",
|
|
105922
|
+
".stylelintrc.yml",
|
|
105923
|
+
".stylelintrc.yaml",
|
|
105924
|
+
".markdownlint.yml",
|
|
105925
|
+
".markdownlint.yaml",
|
|
105926
|
+
".markdownlint-cli2.yaml",
|
|
105927
|
+
".borp.yml",
|
|
105928
|
+
".borp.yaml",
|
|
105929
|
+
".airtap.yml"
|
|
105930
|
+
]);
|
|
105931
|
+
/**
|
|
105932
|
+
* Tooling config removed when the file name starts with one of these, compared
|
|
105933
|
+
* lower-case. Any extension, since a dotfile with these names is never loaded
|
|
105934
|
+
* by a package.
|
|
105935
|
+
*
|
|
105936
|
+
* `.eslint` and `.prettier` rather than `.eslintrc` and `.prettierrc`, so the
|
|
105937
|
+
* matching `.eslintignore` and `.prettierignore` go with them.
|
|
105938
|
+
*/
|
|
105939
|
+
const REMOVE_CONFIG_PREFIXES = [
|
|
105940
|
+
".eslint",
|
|
105941
|
+
".prettier",
|
|
105942
|
+
".babelrc"
|
|
105943
|
+
];
|
|
105944
|
+
/**
|
|
105945
|
+
* Project documents removed when the file name starts with one of these,
|
|
105946
|
+
* compared lower-case, and only with a document extension. The names are
|
|
105947
|
+
* ordinary words, so a code file can start with one: the `history` package
|
|
105948
|
+
* loads `umd/history.production.min.js`.
|
|
105949
|
+
*/
|
|
105950
|
+
const REMOVE_DOC_PREFIXES = [
|
|
105951
|
+
"changelog",
|
|
105952
|
+
"history",
|
|
105953
|
+
"authors",
|
|
105954
|
+
"contributing",
|
|
105955
|
+
"code_of_conduct"
|
|
105956
|
+
];
|
|
105957
|
+
const DOC_EXTS = /* @__PURE__ */ new Set([
|
|
105958
|
+
"",
|
|
105959
|
+
".md",
|
|
105960
|
+
".markdown",
|
|
105961
|
+
".txt",
|
|
105962
|
+
".rst"
|
|
105963
|
+
]);
|
|
105964
|
+
const TSCONFIG_RE = /^tsconfig.*\.json$/;
|
|
105965
|
+
/**
|
|
105966
|
+
* Names that carry a copyright notice. The extension list keeps this from
|
|
105967
|
+
* matching source files such as `license.js`.
|
|
105968
|
+
*
|
|
105969
|
+
* This check runs *before* `isNonRuntimeFile`, which would otherwise delete the
|
|
105970
|
+
* nine `LICENSE.md` files in a BFF install under its `.md` rule.
|
|
105971
|
+
*
|
|
105972
|
+
* @param file file name, no directory part
|
|
105973
|
+
* @param ext extension including the dot, `""` for a dotfile
|
|
105974
|
+
*/
|
|
105975
|
+
function isLicenseFile(file, ext) {
|
|
105976
|
+
if (!LICENSE_EXTS.has(ext.toLowerCase())) return false;
|
|
105977
|
+
return LICENSE_RE.test(file);
|
|
105978
|
+
}
|
|
105979
|
+
const LICENSE_EXTS = /* @__PURE__ */ new Set([
|
|
105980
|
+
"",
|
|
105981
|
+
".txt",
|
|
105982
|
+
".md",
|
|
105983
|
+
".markdown"
|
|
105984
|
+
]);
|
|
105985
|
+
const LICENSE_RE = /^(licen[cs]e|notice|copying)/i;
|
|
105986
|
+
/**
|
|
105987
|
+
* @param file file name, no directory part
|
|
105988
|
+
* @param ext extension including the dot, `""` for a dotfile
|
|
105989
|
+
* @returns true when node never loads this file
|
|
105990
|
+
*/
|
|
105991
|
+
function isNonRuntimeFile(file, ext) {
|
|
105992
|
+
const lowerExt = ext.toLowerCase();
|
|
105993
|
+
if (REMOVE_EXTS.has(lowerExt)) return true;
|
|
105994
|
+
if (REMOVE_NAMES.has(file)) return true;
|
|
105995
|
+
const lower = file.toLowerCase();
|
|
105996
|
+
if (TSCONFIG_RE.test(lower) || REMOVE_YAML_NAMES.has(lower)) return true;
|
|
105997
|
+
if (REMOVE_CONFIG_PREFIXES.some((prefix) => lower.startsWith(prefix))) return true;
|
|
105998
|
+
return DOC_EXTS.has(lowerExt) && REMOVE_DOC_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
105999
|
+
}
|
|
106000
|
+
/**
|
|
106001
|
+
* Is this fyn's installed-lock copy? fyn writes it at `.f/lock.yaml` to record
|
|
106002
|
+
* what it installed. A pruned tree no longer matches a fyn install, so the copy
|
|
106003
|
+
* goes, and fyn cannot trust it later. `.f/.fyn.json` stays: `fyn prod-prune`
|
|
106004
|
+
* reads its `production` flag.
|
|
106005
|
+
*
|
|
106006
|
+
* @param root the node_modules being cleaned
|
|
106007
|
+
* @param fullPath
|
|
106008
|
+
*/
|
|
106009
|
+
function isFynInstalledLock(root, fullPath) {
|
|
106010
|
+
return Path.relative(root, fullPath) === Path.join(".f", "lock.yaml");
|
|
106011
|
+
}
|
|
106012
|
+
/**
|
|
106013
|
+
* Is this directory a package's own folder rather than one inside it?
|
|
106014
|
+
* `REMOVE_DIRS` names are real package names too (`benchmark` is one), and
|
|
106015
|
+
* `esnext` sitting beside the `esm` package would pass `isBundlerOnlyEsnext`.
|
|
106016
|
+
* A name match alone would delete a whole dependency.
|
|
106017
|
+
*
|
|
106018
|
+
* A package folder sits directly under a `node_modules` (or the tree root),
|
|
106019
|
+
* under an `@scope` there, or in fyn's store at `.f/_/<name>` or
|
|
106020
|
+
* `.f/_/@scope/<name>`.
|
|
106021
|
+
*
|
|
106022
|
+
* @param root the node_modules being cleaned
|
|
106023
|
+
* @param dir full path of the directory
|
|
106024
|
+
*/
|
|
106025
|
+
function isPackageRoot(root, dir) {
|
|
106026
|
+
const parts = Path.relative(root, dir).split(Path.sep);
|
|
106027
|
+
const n = parts.length;
|
|
106028
|
+
const container = n >= 2 && parts[n - 2].startsWith("@") ? parts.slice(0, n - 2) : parts.slice(0, n - 1);
|
|
106029
|
+
const c = container.length;
|
|
106030
|
+
return c === 0 || container[c - 1] === "node_modules" || c === 2 && container[0] === ".f" && container[1] === "_";
|
|
106031
|
+
}
|
|
106032
|
+
/**
|
|
106033
|
+
* Media and archives that survive the prune and are worth a human's attention.
|
|
106034
|
+
*
|
|
106035
|
+
* These are *reported, never removed*. An image or a zip in a server's runtime
|
|
106036
|
+
* tree is odd — it is usually a test fixture, a README screenshot or vendored
|
|
106037
|
+
* source — but a package is free to read its own asset at runtime, and nothing
|
|
106038
|
+
* about the file says which case this is. Deleting on a guess would break a
|
|
106039
|
+
* dependency in a way no build-time check catches, so the tool surfaces them
|
|
106040
|
+
* and leaves the judgement to whoever reads the build log.
|
|
106041
|
+
*
|
|
106042
|
+
* Native and WebAssembly binaries (`.node`, `.wasm`, `.so`, `.dylib`) are
|
|
106043
|
+
* deliberately absent: those genuinely are loaded at runtime.
|
|
106044
|
+
*
|
|
106045
|
+
* @param ext extension including the dot
|
|
106046
|
+
*/
|
|
106047
|
+
function isReviewableAsset(ext) {
|
|
106048
|
+
return REVIEW_EXTS.has(ext.toLowerCase());
|
|
106049
|
+
}
|
|
106050
|
+
const REVIEW_EXTS = /* @__PURE__ */ new Set([
|
|
106051
|
+
".png",
|
|
106052
|
+
".jpg",
|
|
106053
|
+
".jpeg",
|
|
106054
|
+
".gif",
|
|
106055
|
+
".bmp",
|
|
106056
|
+
".ico",
|
|
106057
|
+
".webp",
|
|
106058
|
+
".avif",
|
|
106059
|
+
".svg",
|
|
106060
|
+
".psd",
|
|
106061
|
+
".mp4",
|
|
106062
|
+
".webm",
|
|
106063
|
+
".mov",
|
|
106064
|
+
".mp3",
|
|
106065
|
+
".wav",
|
|
106066
|
+
".ogg",
|
|
106067
|
+
".ttf",
|
|
106068
|
+
".otf",
|
|
106069
|
+
".woff",
|
|
106070
|
+
".woff2",
|
|
106071
|
+
".eot",
|
|
106072
|
+
".zip",
|
|
106073
|
+
".tar",
|
|
106074
|
+
".tgz",
|
|
106075
|
+
".gz",
|
|
106076
|
+
".bz2",
|
|
106077
|
+
".xz",
|
|
106078
|
+
".7z",
|
|
106079
|
+
".rar",
|
|
106080
|
+
".br",
|
|
106081
|
+
".pdf",
|
|
106082
|
+
".drawio",
|
|
106083
|
+
".sketch"
|
|
106084
|
+
]);
|
|
106085
|
+
/**
|
|
106086
|
+
* Is this `esnext/` directory a build only a bundler will ever read?
|
|
106087
|
+
*
|
|
106088
|
+
* `esnext` is not a condition node implements, so node can enter such a
|
|
106089
|
+
* directory only if the package's own manifest points at it. Two questions
|
|
106090
|
+
* decide it, and both must agree:
|
|
106091
|
+
*
|
|
106092
|
+
* 1. **Can node reach it?** Every path a resolver can land on comes from the
|
|
106093
|
+
* manifest — `main`, `bin`, or an `exports` entry under a condition node
|
|
106094
|
+
* implements (`import`, `require`, `node`, `default`). If any of them lands
|
|
106095
|
+
* inside the directory, it stays, whatever else suggests otherwise. This is
|
|
106096
|
+
* what keeps the rule from going stale: a future package that does point
|
|
106097
|
+
* node at its `esnext/` is left alone instead of being broken at runtime.
|
|
106098
|
+
*
|
|
106099
|
+
* 2. **Is something else claiming it?** Absence of evidence is not enough, so
|
|
106100
|
+
* the directory also has to be claimed by a bundler: named under a condition
|
|
106101
|
+
* node does not implement (`esnext`, `module`, `browser`, `webpack`, …),
|
|
106102
|
+
* pointed at by the legacy `module` field, or sitting beside an `esm/`
|
|
106103
|
+
* sibling — which shows the package publishes per-syntax-level variants for
|
|
106104
|
+
* a bundler to choose between. Anything unclaimed is left alone.
|
|
106105
|
+
*
|
|
106106
|
+
* The one case this cannot see is a reachable file reaching sideways with a
|
|
106107
|
+
* relative `../esnext/x.js` import. Packages publish parallel compiled outputs
|
|
106108
|
+
* precisely so that they do not cross-import, so the assumption holds in
|
|
106109
|
+
* practice, but it is an assumption rather than a proof.
|
|
106110
|
+
*
|
|
106111
|
+
* @param dir full path of the candidate `esnext` directory
|
|
106112
|
+
* @param siblings names of the entries beside it
|
|
106113
|
+
* @param manifestTargets resolves a package directory to its manifest paths,
|
|
106114
|
+
* memoised by the caller
|
|
106115
|
+
*/
|
|
106116
|
+
function isBundlerOnlyEsnext(dir, siblings, manifestTargets) {
|
|
106117
|
+
if (Path.basename(dir) !== "esnext") return false;
|
|
106118
|
+
const pkgDir = findPackageDir(dir);
|
|
106119
|
+
if (!pkgDir) return false;
|
|
106120
|
+
const inside = `${Path.relative(pkgDir, dir).split(Path.sep).join("/")}/`;
|
|
106121
|
+
const pointsInside = (target) => target.replace(/^\.\//, "").startsWith(inside);
|
|
106122
|
+
const targets = manifestTargets(pkgDir);
|
|
106123
|
+
if (targets.node.some(pointsInside)) return false;
|
|
106124
|
+
return targets.bundler.some(pointsInside) || siblings.includes("esm");
|
|
106125
|
+
}
|
|
106126
|
+
/** Nearest ancestor directory holding a package.json, or `""`. */
|
|
106127
|
+
function findPackageDir(dir) {
|
|
106128
|
+
let cur = Path.dirname(dir);
|
|
106129
|
+
for (let i = 0; i < 8 && cur && cur !== Path.dirname(cur); i++) {
|
|
106130
|
+
if (fs.existsSync(Path.join(cur, "package.json"))) return cur;
|
|
106131
|
+
cur = Path.dirname(cur);
|
|
106132
|
+
}
|
|
106133
|
+
return "";
|
|
106134
|
+
}
|
|
106135
|
+
/** Conditions node resolves. `module` and `esnext` are bundler conventions. */
|
|
106136
|
+
const NODE_CONDITIONS = /* @__PURE__ */ new Set([
|
|
106137
|
+
"import",
|
|
106138
|
+
"require",
|
|
106139
|
+
"node",
|
|
106140
|
+
"node-addons",
|
|
106141
|
+
"default"
|
|
106142
|
+
]);
|
|
106143
|
+
/**
|
|
106144
|
+
* Split a manifest's entry points into the ones node can resolve and the ones
|
|
106145
|
+
* only a bundler will.
|
|
106146
|
+
*
|
|
106147
|
+
* @param pkgDir directory holding the package.json
|
|
106148
|
+
*/
|
|
106149
|
+
function readManifestTargets(pkgDir) {
|
|
106150
|
+
const targets = {
|
|
106151
|
+
node: [],
|
|
106152
|
+
bundler: []
|
|
106153
|
+
};
|
|
106154
|
+
let manifest;
|
|
106155
|
+
try {
|
|
106156
|
+
manifest = JSON.parse(fs.readFileSync(Path.join(pkgDir, "package.json"), "utf8"));
|
|
106157
|
+
} catch {
|
|
106158
|
+
return targets;
|
|
106159
|
+
}
|
|
106160
|
+
if (typeof manifest.main === "string") targets.node.push(manifest.main);
|
|
106161
|
+
if (typeof manifest.bin === "string") targets.node.push(manifest.bin);
|
|
106162
|
+
else if (manifest.bin && typeof manifest.bin === "object") for (const key of Object.keys(manifest.bin)) targets.node.push(manifest.bin[key]);
|
|
106163
|
+
if (typeof manifest.module === "string") targets.bundler.push(manifest.module);
|
|
106164
|
+
collectExportTargets(manifest.exports, targets, false);
|
|
106165
|
+
return targets;
|
|
106166
|
+
}
|
|
106167
|
+
function collectExportTargets(node, targets, bundlerOnly) {
|
|
106168
|
+
if (typeof node === "string") {
|
|
106169
|
+
targets[bundlerOnly ? "bundler" : "node"].push(node);
|
|
106170
|
+
return;
|
|
106171
|
+
}
|
|
106172
|
+
if (!node || typeof node !== "object") return;
|
|
106173
|
+
for (const key of Object.keys(node)) if (key.startsWith(".")) collectExportTargets(node[key], targets, bundlerOnly);
|
|
106174
|
+
else if (key !== "types") collectExportTargets(node[key], targets, bundlerOnly || !NODE_CONDITIONS.has(key));
|
|
106175
|
+
}
|
|
106176
|
+
|
|
106177
|
+
//#endregion
|
|
106178
|
+
//#region lib/prod-prune/index.ts
|
|
106179
|
+
/**
|
|
106180
|
+
* Remove non-runtime files from a production `node_modules` tree, so a
|
|
106181
|
+
* container image carries only what node actually loads.
|
|
106182
|
+
*
|
|
106183
|
+
* ESM, TypeScript, bundled like the rest of fyn (see tsconfig.json for why the
|
|
106184
|
+
* lib target sits at ES2022). This backs the `fyn prod-prune` command rather
|
|
106185
|
+
* than running as its own standalone tool.
|
|
106186
|
+
*
|
|
106187
|
+
* None of what this deletes is reachable code, all of it is readable by anyone
|
|
106188
|
+
* who pulls the image, and every file of it is inventory a scanner has to
|
|
106189
|
+
* enumerate. Source maps reconstruct a dependency's original source; test and
|
|
106190
|
+
* fixture directories are a well-known home for hardcoded credentials and
|
|
106191
|
+
* sample tokens.
|
|
106192
|
+
*
|
|
106193
|
+
* fyn prod-prune [dir] [--max-kb <n>] [--max-files <n>]
|
|
106194
|
+
*
|
|
106195
|
+
* Nothing about a file's size says whether it may be deleted — the largest file
|
|
106196
|
+
* in a BFF install is loaded at runtime — so size is only ever reported, never
|
|
106197
|
+
* acted on. Every run lists the files over 100kB, so the build log says where
|
|
106198
|
+
* the weight sits. The budgets are the regression gate on top of that: they
|
|
106199
|
+
* make new material entering the image someone's decision at review time
|
|
106200
|
+
* rather than a surprise at deploy time, and exceeding either names the largest
|
|
106201
|
+
* packages and exits non-zero.
|
|
106202
|
+
*/
|
|
106203
|
+
const LARGE_FILE_BYTES = 102400;
|
|
106204
|
+
const LARGE_LIST_LIMIT = 20;
|
|
106205
|
+
const REVIEW_LIST_LIMIT = 20;
|
|
106206
|
+
const COMBINED_LICENSE_FILE = "THIRD-PARTY-LICENSES.txt";
|
|
106207
|
+
/**
|
|
106208
|
+
* What the package owning this tree declares about its own pruning, under a
|
|
106209
|
+
* `prodPruning` key in the package.json beside `node_modules`.
|
|
106210
|
+
*
|
|
106211
|
+
* "prodPruning": {
|
|
106212
|
+
* "maxKb": 15600, // fail the build above this many kB
|
|
106213
|
+
* "maxFiles": 2850, // fail the build above this many files
|
|
106214
|
+
* "keepPaths": ["some-pkg/vendor", "some-pkg/table.bin"]
|
|
106215
|
+
* }
|
|
106216
|
+
*
|
|
106217
|
+
* Budgets live here rather than in a Dockerfile so that a dependency change and
|
|
106218
|
+
* the budget it moves land in the same file, in the same commit, under the same
|
|
106219
|
+
* review. `keepPaths` is the escape hatch that lets the rules stay generic: an
|
|
106220
|
+
* app that needs one specific file or directory spared says so here, instead of
|
|
106221
|
+
* a package name leaking into a rule where it would go stale.
|
|
106222
|
+
*
|
|
106223
|
+
* @param root the node_modules being cleaned
|
|
106224
|
+
*/
|
|
106225
|
+
function readConfig(root) {
|
|
106226
|
+
let declared = {};
|
|
106227
|
+
try {
|
|
106228
|
+
const manifest = Path.join(Path.dirname(root), "package.json");
|
|
106229
|
+
declared = JSON.parse(fs.readFileSync(manifest, "utf8")).prodPruning ?? {};
|
|
106230
|
+
} catch {}
|
|
106231
|
+
return {
|
|
106232
|
+
maxKb: Number(declared.maxKb) || 0,
|
|
106233
|
+
maxFiles: Number(declared.maxFiles) || 0,
|
|
106234
|
+
keepPaths: Array.isArray(declared.keepPaths) ? declared.keepPaths : []
|
|
106235
|
+
};
|
|
106236
|
+
}
|
|
106237
|
+
/**
|
|
106238
|
+
* Walk the tree once, sorting every entry into what goes, what stays, and what
|
|
106239
|
+
* carries a copyright notice.
|
|
106240
|
+
*
|
|
106241
|
+
* @param root
|
|
106242
|
+
* @param keepPaths relative to `root`, each either a file or a directory;
|
|
106243
|
+
* naming a directory spares everything under it
|
|
106244
|
+
*/
|
|
106245
|
+
async function scan(root, keepPaths) {
|
|
106246
|
+
const keepList = keepPaths.map((path) => path.split("/").join(Path.sep));
|
|
106247
|
+
const isKeepPath = (fullPath) => {
|
|
106248
|
+
const rel = Path.relative(root, fullPath);
|
|
106249
|
+
return keepList.some((path) => rel === path || rel.startsWith(`${path}${Path.sep}`));
|
|
106250
|
+
};
|
|
106251
|
+
const removeDirs = [];
|
|
106252
|
+
const kept = [];
|
|
106253
|
+
const licenses = [];
|
|
106254
|
+
let keptBytes = 0;
|
|
106255
|
+
const targetCache = /* @__PURE__ */ new Map();
|
|
106256
|
+
const manifestTargets = (pkgDir) => {
|
|
106257
|
+
if (!targetCache.has(pkgDir)) targetCache.set(pkgDir, readManifestTargets(pkgDir));
|
|
106258
|
+
return targetCache.get(pkgDir);
|
|
106259
|
+
};
|
|
106260
|
+
return {
|
|
106261
|
+
removeDirs,
|
|
106262
|
+
removeFiles: (await filterScanDir({
|
|
106263
|
+
cwd: root,
|
|
106264
|
+
prependCwd: true,
|
|
106265
|
+
grouping: true,
|
|
106266
|
+
rethrowError: true,
|
|
106267
|
+
filterDir: (file, path, extras) => {
|
|
106268
|
+
if (isKeepPath(extras.fullFile)) return true;
|
|
106269
|
+
if (!isPackageRoot(root, extras.fullFile) && (REMOVE_DIRS.has(file) || isBundlerOnlyEsnext(extras.fullFile, extras.files, manifestTargets))) {
|
|
106270
|
+
removeDirs.push(extras.fullFile);
|
|
106271
|
+
return false;
|
|
106272
|
+
}
|
|
106273
|
+
return true;
|
|
106274
|
+
},
|
|
106275
|
+
filter: (file, path, extras) => {
|
|
106276
|
+
if (isKeepPath(extras.fullFile)) {
|
|
106277
|
+
kept.push({
|
|
106278
|
+
path: extras.fullFile,
|
|
106279
|
+
size: extras.stat.size,
|
|
106280
|
+
review: false
|
|
106281
|
+
});
|
|
106282
|
+
keptBytes += extras.stat.size;
|
|
106283
|
+
return false;
|
|
106284
|
+
}
|
|
106285
|
+
if (isLicenseFile(file, extras.ext)) {
|
|
106286
|
+
const owner = packageOf(root, extras.fullFile);
|
|
106287
|
+
licenses.push({
|
|
106288
|
+
path: extras.fullFile,
|
|
106289
|
+
package: owner.version ? `${owner.name}@${owner.version}` : owner.name,
|
|
106290
|
+
file,
|
|
106291
|
+
text: fs.readFileSync(extras.fullFile, "utf8")
|
|
106292
|
+
});
|
|
106293
|
+
return false;
|
|
106294
|
+
}
|
|
106295
|
+
if (isNonRuntimeFile(file, extras.ext) || isFynInstalledLock(root, extras.fullFile)) return "remove";
|
|
106296
|
+
kept.push({
|
|
106297
|
+
path: extras.fullFile,
|
|
106298
|
+
size: extras.stat.size,
|
|
106299
|
+
review: isReviewableAsset(extras.ext)
|
|
106300
|
+
});
|
|
106301
|
+
keptBytes += extras.stat.size;
|
|
106302
|
+
return false;
|
|
106303
|
+
}
|
|
106304
|
+
})).remove || [],
|
|
106305
|
+
licenses,
|
|
106306
|
+
kept,
|
|
106307
|
+
keptBytes
|
|
106308
|
+
};
|
|
106309
|
+
}
|
|
106310
|
+
/**
|
|
106311
|
+
* Delete the directories the deletions left empty. Deepest first, so a parent
|
|
106312
|
+
* emptied by its own children going away is collected in the same pass.
|
|
106313
|
+
*/
|
|
106314
|
+
async function removeEmptyDirs(root) {
|
|
106315
|
+
const dirs = await filterScanDir({
|
|
106316
|
+
cwd: root,
|
|
106317
|
+
prependCwd: true,
|
|
106318
|
+
includeDir: true,
|
|
106319
|
+
rethrowError: true,
|
|
106320
|
+
filter: () => false
|
|
106321
|
+
});
|
|
106322
|
+
const depth = (dir) => dir.split("/").length;
|
|
106323
|
+
dirs.sort((a, b) => depth(b) - depth(a));
|
|
106324
|
+
let removed = 0;
|
|
106325
|
+
for (const dir of dirs) try {
|
|
106326
|
+
fs.rmdirSync(dir);
|
|
106327
|
+
removed++;
|
|
106328
|
+
} catch {}
|
|
106329
|
+
return removed;
|
|
106330
|
+
}
|
|
106331
|
+
const packageCache = /* @__PURE__ */ new Map();
|
|
106332
|
+
/**
|
|
106333
|
+
* Which package ships this file, and at which version.
|
|
106334
|
+
*
|
|
106335
|
+
* The directory is found from the *last* `node_modules` in the path, not the
|
|
106336
|
+
* first, so a nested copy is attributed to the package itself rather than to
|
|
106337
|
+
* the store holding it — fyn keeps duplicate versions under
|
|
106338
|
+
* `.f/_/<name>/<version>/node_modules/`, and reporting those as a package
|
|
106339
|
+
* called `.f` tells you nothing. One path segment after that, or two when the
|
|
106340
|
+
* first starts with `@`.
|
|
106341
|
+
*
|
|
106342
|
+
* The name then comes from that directory's own package.json rather than from
|
|
106343
|
+
* the directory name, which is the only authoritative answer — and the reason
|
|
106344
|
+
* to compute the directory this way instead of walking up looking for the
|
|
106345
|
+
* nearest manifest, since packages routinely drop a `dist/cjs/package.json`
|
|
106346
|
+
* carrying nothing but `{"type": "commonjs"}`.
|
|
106347
|
+
*/
|
|
106348
|
+
function packageOf(root, fullPath) {
|
|
106349
|
+
const parts = Path.relative(root, fullPath).split(Path.sep);
|
|
106350
|
+
const start = parts.lastIndexOf("node_modules") + 1;
|
|
106351
|
+
if (start >= parts.length - 1) return {
|
|
106352
|
+
name: parts.at(-1),
|
|
106353
|
+
version: ""
|
|
106354
|
+
};
|
|
106355
|
+
const span = parts[start].startsWith("@") ? 2 : 1;
|
|
106356
|
+
const dir = Path.join(root, ...parts.slice(0, start + span));
|
|
106357
|
+
if (!packageCache.has(dir)) packageCache.set(dir, readPackageIdentity(dir, parts.slice(start, start + span).join("/")));
|
|
106358
|
+
return packageCache.get(dir);
|
|
106359
|
+
}
|
|
106360
|
+
function readPackageIdentity(dir, fallbackName) {
|
|
106361
|
+
try {
|
|
106362
|
+
const manifest = JSON.parse(fs.readFileSync(Path.join(dir, "package.json"), "utf8"));
|
|
106363
|
+
return {
|
|
106364
|
+
name: manifest.name ?? fallbackName,
|
|
106365
|
+
version: manifest.version ?? ""
|
|
106366
|
+
};
|
|
106367
|
+
} catch {
|
|
106368
|
+
return {
|
|
106369
|
+
name: fallbackName,
|
|
106370
|
+
version: ""
|
|
106371
|
+
};
|
|
106372
|
+
}
|
|
106373
|
+
}
|
|
106374
|
+
/** Total size of the surviving files, grouped by the package that ships them. */
|
|
106375
|
+
function largestPackages(root, files, limit) {
|
|
106376
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
106377
|
+
for (const file of files) {
|
|
106378
|
+
const name = packageOf(root, file.path).name;
|
|
106379
|
+
const group = byPackage.get(name);
|
|
106380
|
+
if (group) group.push(file);
|
|
106381
|
+
else byPackage.set(name, [file]);
|
|
106382
|
+
}
|
|
106383
|
+
return [...byPackage.entries()].map(([name, group]) => [name, group.reduce((total, file) => total + file.size, 0)]).sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
106384
|
+
}
|
|
106385
|
+
function kb(bytes) {
|
|
106386
|
+
return Math.round(bytes / 1024);
|
|
106387
|
+
}
|
|
106388
|
+
/** The limits that are set, like `15600kB / 2850 files`, or `none`. */
|
|
106389
|
+
function describeBudget(maxKb, maxFiles) {
|
|
106390
|
+
const limits = [maxKb > 0 && `${maxKb}kB`, maxFiles > 0 && `${maxFiles} file${maxFiles === 1 ? "" : "s"}`].filter(Boolean);
|
|
106391
|
+
return limits.length > 0 ? limits.join(" / ") : "none";
|
|
106392
|
+
}
|
|
106393
|
+
/**
|
|
106394
|
+
* @returns the state of the tree afterwards. Returned rather than turned
|
|
106395
|
+
* straight into an exit code so the caller decides what a budget overrun
|
|
106396
|
+
* means — the CLI fails the build with it; a test just reads it.
|
|
106397
|
+
*/
|
|
106398
|
+
async function clean(dir, cliMaxKb, cliMaxFiles) {
|
|
106399
|
+
const root = Path.resolve(dir);
|
|
106400
|
+
if (!fs.existsSync(root)) throw new Error(`no such directory: ${root}`);
|
|
106401
|
+
const config = readConfig(root);
|
|
106402
|
+
const maxKb = cliMaxKb || config.maxKb;
|
|
106403
|
+
const maxFiles = cliMaxFiles || config.maxFiles;
|
|
106404
|
+
if (config.keepPaths.length > 0) logger$1.info(`keeping ${config.keepPaths.length} path${config.keepPaths.length === 1 ? "" : "s"} named in prodPruning: ` + config.keepPaths.join(", "));
|
|
106405
|
+
const found = await scan(root, config.keepPaths);
|
|
106406
|
+
let combined = null;
|
|
106407
|
+
if (found.licenses.length > 0) {
|
|
106408
|
+
const merged = combineLicenses(found.licenses);
|
|
106409
|
+
const path = Path.join(root, COMBINED_LICENSE_FILE);
|
|
106410
|
+
fs.writeFileSync(path, merged.content);
|
|
106411
|
+
combined = {
|
|
106412
|
+
...merged,
|
|
106413
|
+
path,
|
|
106414
|
+
size: Buffer.byteLength(merged.content)
|
|
106415
|
+
};
|
|
106416
|
+
}
|
|
106417
|
+
for (const path of found.removeDirs) fs.rmSync(path, {
|
|
106418
|
+
recursive: true,
|
|
106419
|
+
force: true
|
|
106420
|
+
});
|
|
106421
|
+
for (const path of found.removeFiles.concat(found.licenses.map((l) => l.path))) fs.rmSync(path, { force: true });
|
|
106422
|
+
const emptied = await removeEmptyDirs(root);
|
|
106423
|
+
const kept = combined ? found.kept.concat([{
|
|
106424
|
+
path: combined.path,
|
|
106425
|
+
size: combined.size
|
|
106426
|
+
}]) : found.kept;
|
|
106427
|
+
const keptBytes = found.keptBytes + (combined ? combined.size : 0);
|
|
106428
|
+
logger$1.info(`removed ${found.removeFiles.length} files, ${found.removeDirs.length} directories and ${emptied} more left empty`);
|
|
106429
|
+
if (combined) logger$1.info(`merged ${found.licenses.length} license files from ${combined.packages} packages into ${combined.licenses} distinct ${combined.licenses === 1 ? "license" : "licenses"} in ${COMBINED_LICENSE_FILE}`);
|
|
106430
|
+
logger$1.info(`${kept.length} files, ${kb(keptBytes)}kB remain (budget ${describeBudget(maxKb, maxFiles)})`);
|
|
106431
|
+
const review = found.kept.filter((file) => file.review).sort((a, b) => b.size - a.size);
|
|
106432
|
+
if (review.length > 0) {
|
|
106433
|
+
logger$1.info(`${review.length} media or archive ${review.length === 1 ? "file survives" : "files survive"} the prune. A server's runtime tree rarely needs one, but only a human can say whether a package reads its own asset, so these are reported and left alone:`);
|
|
106434
|
+
for (const file of review.slice(0, REVIEW_LIST_LIMIT)) logger$1.info(` ${String(kb(file.size)).padStart(7)}kB ${Path.relative(root, file.path)}`);
|
|
106435
|
+
if (review.length > REVIEW_LIST_LIMIT) logger$1.info(` ... and ${review.length - REVIEW_LIST_LIMIT} more`);
|
|
106436
|
+
}
|
|
106437
|
+
const large = kept.filter((file) => file.size > LARGE_FILE_BYTES).sort((a, b) => b.size - a.size);
|
|
106438
|
+
if (large.length > 0) {
|
|
106439
|
+
const total = large.reduce((sum, file) => sum + file.size, 0);
|
|
106440
|
+
logger$1.info(`${large.length} files over ${kb(LARGE_FILE_BYTES)}kB hold ${kb(total)}kB of the remaining ${kb(keptBytes)}kB:`);
|
|
106441
|
+
for (const file of large.slice(0, LARGE_LIST_LIMIT)) logger$1.info(` ${String(kb(file.size)).padStart(7)}kB ${Path.relative(root, file.path)}`);
|
|
106442
|
+
if (large.length > LARGE_LIST_LIMIT) logger$1.info(` ... and ${large.length - LARGE_LIST_LIMIT} more`);
|
|
106443
|
+
}
|
|
106444
|
+
const result = {
|
|
106445
|
+
files: kept.length,
|
|
106446
|
+
kb: kb(keptBytes),
|
|
106447
|
+
withinBudget: true
|
|
106448
|
+
};
|
|
106449
|
+
const overKb = maxKb > 0 && result.kb > maxKb;
|
|
106450
|
+
const overFiles = maxFiles > 0 && result.files > maxFiles;
|
|
106451
|
+
if (!overKb && !overFiles) return result;
|
|
106452
|
+
logger$1.error(`FATAL: ${root} holds ${result.files} files and ${result.kb}kB, over the ${describeBudget(maxKb, maxFiles)} budget.`);
|
|
106453
|
+
logger$1.error("Largest packages:");
|
|
106454
|
+
for (const [name, size] of largestPackages(root, kept, 20)) logger$1.error(` ${String(kb(size)).padStart(7)}kB ${name}`);
|
|
106455
|
+
return {
|
|
106456
|
+
...result,
|
|
106457
|
+
withinBudget: false
|
|
106458
|
+
};
|
|
106459
|
+
}
|
|
106460
|
+
|
|
105597
106461
|
//#endregion
|
|
105598
106462
|
//#region cli/fyn-cli.ts
|
|
105599
106463
|
function checkNewVersion(npmConfig) {
|
|
@@ -106076,6 +106940,23 @@ var FynCli = class {
|
|
|
106076
106940
|
return this._opts.saveLogs && this.saveLogs(this._opts.saveLogs);
|
|
106077
106941
|
});
|
|
106078
106942
|
}
|
|
106943
|
+
/**
|
|
106944
|
+
* Remove non-runtime files from a production node_modules. Only runs on demand,
|
|
106945
|
+
* and refuses a tree that did not come from `fyn install --production` unless forced.
|
|
106946
|
+
*/
|
|
106947
|
+
async prodPrune(argv) {
|
|
106948
|
+
const dir = argv.args.dir ? Path.resolve(this.fyn.cwd, argv.args.dir) : this.fyn.getOutputDir();
|
|
106949
|
+
try {
|
|
106950
|
+
if (!argv.opts.force) {
|
|
106951
|
+
const configFile = Path.join(dir, ".f", FYN_INSTALL_CONFIG_FILE);
|
|
106952
|
+
if ((await file_ops_default.readFile(configFile, "utf8").then((data) => JSON.parse(data)).catch(() => ({}))).production !== true) throw new Error(`${dir} is not from a production install (fyn install --production). Use --force to prune it anyway.`);
|
|
106953
|
+
}
|
|
106954
|
+
if (!(await clean(dir, argv.opts.maxKb || 0, argv.opts.maxFiles || 0)).withinBudget) fyntil.exit(1);
|
|
106955
|
+
} catch (err) {
|
|
106956
|
+
logger$1.error(err.message);
|
|
106957
|
+
fyntil.exit(1);
|
|
106958
|
+
}
|
|
106959
|
+
}
|
|
106079
106960
|
async runScript(pkg, script, env, scriptArgs = []) {
|
|
106080
106961
|
const config = (x) => this.fyn.allrc[x];
|
|
106081
106962
|
const options = {
|
|
@@ -114234,11 +115115,10 @@ var FynGlobal = class {
|
|
|
114234
115115
|
pkgJson.dependencies[targetPackageName] = targetVersion.semver || "latest";
|
|
114235
115116
|
await file_ops_default.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");
|
|
114236
115117
|
}
|
|
114237
|
-
const
|
|
114238
|
-
try {
|
|
115118
|
+
const fyn = this._createFyn(pkgDir, targetVersion.local);
|
|
115119
|
+
for (const lockPath of [Path.join(pkgDir, "fyn-lock.yaml"), fyn.getInstalledLockFile()]) try {
|
|
114239
115120
|
await file_ops_default.unlink(lockPath);
|
|
114240
115121
|
} catch (err) {}
|
|
114241
|
-
const fyn = this._createFyn(pkgDir, targetVersion.local);
|
|
114242
115122
|
await fyn.resolveDependencies();
|
|
114243
115123
|
await fyn.fetchPackages();
|
|
114244
115124
|
await new PkgInstaller({ fyn }).install();
|
|
@@ -115902,6 +116782,30 @@ const commands = {
|
|
|
115902
116782
|
}
|
|
115903
116783
|
}
|
|
115904
116784
|
},
|
|
116785
|
+
"prod-prune": {
|
|
116786
|
+
desc: "Remove non-runtime files from a production node_modules",
|
|
116787
|
+
usage: "$0 $1 [dir] [--max-kb <n>] [--max-files <n>] [--force]",
|
|
116788
|
+
args: "[dir string]",
|
|
116789
|
+
exec: async (cmd) => {
|
|
116790
|
+
return new FynCli(await pickOptions(cmd)).prodPrune(cmd.jsonMeta);
|
|
116791
|
+
},
|
|
116792
|
+
options: {
|
|
116793
|
+
"max-kb": {
|
|
116794
|
+
args: "<kb number>",
|
|
116795
|
+
desc: "Fail when the pruned tree exceeds this many kB",
|
|
116796
|
+
argDefault: "0"
|
|
116797
|
+
},
|
|
116798
|
+
"max-files": {
|
|
116799
|
+
args: "<count number>",
|
|
116800
|
+
desc: "Fail when the pruned tree exceeds this many files",
|
|
116801
|
+
argDefault: "0"
|
|
116802
|
+
},
|
|
116803
|
+
force: {
|
|
116804
|
+
args: "<flag boolean>",
|
|
116805
|
+
desc: "Prune even if the tree is not from a production install"
|
|
116806
|
+
}
|
|
116807
|
+
}
|
|
116808
|
+
},
|
|
115905
116809
|
"sync-local": {
|
|
115906
116810
|
desc: "Refresh locally linked package files",
|
|
115907
116811
|
alias: "sl",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fyn",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.2.0",
|
|
5
5
|
"description": "fyn is a node.js package manager, and supports fynpo -- a zero setup monorepo tool",
|
|
6
6
|
"main": "./bin/index.mjs",
|
|
7
7
|
"homepage": "https://github.com/jchip/fynjs/tree/main/packages/fyn",
|
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"author": "Joel Chen",
|
|
37
37
|
"publishConfig": {
|
|
38
|
-
"access": "public"
|
|
39
|
-
"registry": "https://registry.npmjs.com/"
|
|
38
|
+
"access": "public"
|
|
40
39
|
},
|
|
41
40
|
"files": [
|
|
42
41
|
"bin",
|
|
@@ -82,7 +81,7 @@
|
|
|
82
81
|
"Walmart GTP.js Team and Contributors"
|
|
83
82
|
],
|
|
84
83
|
"devDependencies": {
|
|
85
|
-
"@fynjs/run": "^1.1.
|
|
84
|
+
"@fynjs/run": "^1.1.8",
|
|
86
85
|
"@fynjs/ts-resolve": "^1.0.3",
|
|
87
86
|
"@types/node": "^26.4.1",
|
|
88
87
|
"@vitest/coverage-v8": "^5.0.0",
|
|
@@ -92,7 +91,7 @@
|
|
|
92
91
|
"patch-package": "^8.0.0",
|
|
93
92
|
"prettier": "^3.5.3",
|
|
94
93
|
"rolldown": "^1.2.6",
|
|
95
|
-
"run-verify": "^2.1.
|
|
94
|
+
"run-verify": "^2.1.8",
|
|
96
95
|
"typescript": "^7.0.2",
|
|
97
96
|
"vite": "^8.2.2",
|
|
98
97
|
"vitest": "^5.0.0",
|