fyn 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +161 -0
  2. package/dist/fyn.js +1163 -158
  3. package/package.json +1 -1
package/dist/fyn.js CHANGED
@@ -143,6 +143,7 @@ const myPkg = __webpack_require__("./cli/mypkg.ts");
143
143
  const { cleanErrorStack } = __webpack_require__("./node_modules/.f/_/@jchip/error/1.0.3/@jchip/error/dist/index.js");
144
144
  const { setupNodeGypEnv } = __webpack_require__("./lib/util/setup-node-gyp.ts");
145
145
  const hardLinkDir = __webpack_require__("./lib/util/hard-link-dir.ts");
146
+ const { syncLocalExports, localExportsScanIgnores } = __webpack_require__("./lib/local-exports.ts");
146
147
  const xsh = __webpack_require__("./node_modules/.f/_/xsh/0.4.6/xsh/lib/index.js");
147
148
  function checkNewVersion(npmConfig) {
148
149
  checkPkgNewVersionEngine({
@@ -417,13 +418,21 @@ class FynCli {
417
418
  }
418
419
  async syncLocalLinks() {
419
420
  await this.fyn._initializePkg();
420
- const { localPkgLinks } = this.fyn._installConfig;
421
+ const { localPkgLinks, localExports } = this.fyn._installConfig;
422
+ let refreshed = false;
421
423
  if (!_.isEmpty(localPkgLinks)) {
422
424
  for (const vdir in localPkgLinks) {
423
425
  const tgtDir = Path.join(this.fyn._cwd, vdir);
424
426
  const srcDir = Path.join(this.fyn._cwd, localPkgLinks[vdir].srcDir);
425
427
  await hardLinkDir.link(srcDir, tgtDir, { sourceMaps: localPkgLinks[vdir].sourceMaps });
426
428
  }
429
+ refreshed = true;
430
+ }
431
+ if (localExports) {
432
+ await syncLocalExports({ cwd: this.fyn._cwd, manifest: localExports });
433
+ refreshed = true;
434
+ }
435
+ if (refreshed) {
427
436
  logger.info(`refreshed linked files for local packages`);
428
437
  } else {
429
438
  logger.info(`There are no local packages`);
@@ -444,7 +453,9 @@ class FynCli {
444
453
  return Promise.try(() => this.fyn._initializePkg()).then(async () => {
445
454
  checkNewVersion(this.fyn._options);
446
455
  if (!this.fyn._changeProdMode && !this.fyn._options.forceInstall && this.fyn._installConfig.time) {
447
- const stats = await scanFileStats(this.fyn.cwd);
456
+ const stats = await scanFileStats(this.fyn.cwd, {
457
+ moreIgnores: localExportsScanIgnores(this.fyn._pkg)
458
+ });
448
459
  const { latestMtimeMs } = stats;
449
460
  logger.debug(
450
461
  "time check from install config - last install time",
@@ -616,16 +627,13 @@ class FynCli {
616
627
  }
617
628
  script = script || argv.args?.script;
618
629
  if (argv.opts?.list || !script) {
619
- try {
620
- await this.fyn.loadPkg();
621
- if (!argv.opts?.list) {
622
- console.log(`Lifecycle scripts included in ${this.fyn._pkg.name}:
630
+ await this.fyn.loadPkg();
631
+ if (!argv.opts?.list) {
632
+ console.log(`Lifecycle scripts included in ${this.fyn._pkg.name}:
623
633
  `);
624
- }
625
- console.log(Object.keys(_.get(this.fyn._pkg, "scripts", {})).join("\n"));
626
- } finally {
627
- fyntil.exit(0);
628
634
  }
635
+ console.log(Object.keys(_.get(this.fyn._pkg, "scripts", {})).join("\n"));
636
+ fyntil.exit(0);
629
637
  }
630
638
  await this.fyn.loadPkg();
631
639
  if (!_.get(this.fyn._pkg, ["scripts", script])) {
@@ -786,8 +794,15 @@ const pickEnvOptions = () => {
786
794
  cfg[m.optKey] = ev === m.checkValue;
787
795
  logger.info(`setting option ${m.optKey} to ${cfg[m.optKey]} by env ${envKey} value ${ev}`);
788
796
  }
797
+ return cfg;
789
798
  }, {});
790
799
  };
800
+ const getRunExitCode = (err) => err.code !== void 0 ? err.code : err.errno || 1;
801
+ const setLockfile = (config, lockfile) => {
802
+ const previous = config.opts.lockfile;
803
+ config.opts.lockfile = lockfile;
804
+ return previous;
805
+ };
791
806
  const pickOptions = async (cmd, checkFynpo = true) => {
792
807
  const meta = cmd.jsonMeta;
793
808
  const rootOpts = cmd.rootCmd?.opts || {};
@@ -830,6 +845,11 @@ const pickOptions = async (cmd, checkFynpo = true) => {
830
845
  if (allOpts.progress) logger.setItemType(allOpts.progress);
831
846
  return { opts: allOpts, rcData, _cliSource: meta.source, _fynpo: fynpo };
832
847
  };
848
+ const makeFynGlobal = async (cmd, extra = {}) => {
849
+ const { opts } = await pickOptions(cmd, false);
850
+ const tag = cmd.getParent()?.opts?.tag;
851
+ return new FynGlobal({ globalDir: cmd.opts?.dir, tag, fynOpts: opts, ...extra });
852
+ };
833
853
  const options = {
834
854
  fynlocal: {
835
855
  args: "<flag boolean>",
@@ -930,6 +950,10 @@ const options = {
930
950
  args: "<flag boolean>",
931
951
  desc: "Ignore host in tarball URL from meta dist."
932
952
  },
953
+ "enforce-registry-deps": {
954
+ args: "<flag boolean>",
955
+ desc: "Require transitive deps to be from a registry (default on). --no-enforce-registry-deps to disable."
956
+ },
933
957
  "show-deprecated": {
934
958
  alias: "s",
935
959
  args: "<flag boolean>",
@@ -1030,13 +1054,12 @@ const commands = {
1030
1054
  Object.assign(meta.opts, cmd.rootCmd.opts);
1031
1055
  }
1032
1056
  const config = await pickOptions(cmd);
1033
- const lockFile = config.lockfile;
1034
- config.lockfile = false;
1057
+ const lockFile = setLockfile(config, false);
1035
1058
  const cli = new FynCli(config);
1036
1059
  const opts = Object.assign({}, meta.opts, meta.args);
1037
1060
  return cli.add(opts).then((added) => {
1038
1061
  if (!added || !meta.opts.install) return;
1039
- config.lockfile = lockFile;
1062
+ setLockfile(config, lockFile);
1040
1063
  config.noStartupInfo = true;
1041
1064
  logger.info("installing...");
1042
1065
  fynTil.resetFynpo();
@@ -1080,14 +1103,13 @@ const commands = {
1080
1103
  Object.assign(meta.opts, cmd.rootCmd.opts);
1081
1104
  }
1082
1105
  const options2 = await pickOptions(cmd);
1083
- const lockFile = options2.lockfile;
1084
- options2.lockfile = false;
1106
+ const lockFile = setLockfile(options2, false);
1085
1107
  const cli = new FynCli(options2);
1086
1108
  const opts = Object.assign({}, meta.opts, meta.args);
1087
1109
  const removed = await cli.remove(opts);
1088
1110
  if (removed) {
1089
1111
  if (!meta.opts.install) return;
1090
- options2.lockfile = lockFile;
1112
+ setLockfile(options2, lockFile);
1091
1113
  options2.noStartupInfo = true;
1092
1114
  fynTil.resetFynpo();
1093
1115
  logger.info("installing...");
@@ -1121,7 +1143,7 @@ const commands = {
1121
1143
  const options2 = await pickOptions(cmd, !meta.opts.list);
1122
1144
  return await new FynCli(options2).run(meta, void 0, cmd, parsed);
1123
1145
  } catch (err) {
1124
- const exitCode = err.errno !== void 0 ? err.errno : err.code || 1;
1146
+ const exitCode = getRunExitCode(err);
1125
1147
  if (err.event && err.script) {
1126
1148
  logger.error(chalk.red(`Script '${err.event}' failed${err.pkgid ? ` for ${err.pkgid}` : ""}`));
1127
1149
  if (err.path) {
@@ -1202,9 +1224,7 @@ const commands = {
1202
1224
  "new-tag": { args: "<flag boolean>", desc: "Install as new tag even if same version exists" }
1203
1225
  },
1204
1226
  async exec(cmd) {
1205
- setLogLevel(cmd.opts?.logLevel || cmd.rootCmd?.opts?.logLevel);
1206
- const tag = cmd.getParent()?.opts?.tag;
1207
- const fynGlobal = new FynGlobal({ globalDir: cmd.opts?.dir, yes: cmd.opts?.yes, tag });
1227
+ const fynGlobal = await makeFynGlobal(cmd, { yes: cmd.opts?.yes });
1208
1228
  const packages = cmd.args?.packages || [];
1209
1229
  if (packages.length === 0) {
1210
1230
  logger.error("No packages specified");
@@ -1229,11 +1249,9 @@ const commands = {
1229
1249
  "yes": { alias: "y", desc: "Auto-confirm all prompts" }
1230
1250
  },
1231
1251
  async exec(cmd) {
1232
- setLogLevel(cmd.opts?.logLevel || cmd.rootCmd?.opts?.logLevel);
1233
- const tag = cmd.getParent()?.opts?.tag;
1234
- const fynGlobal = new FynGlobal({ globalDir: cmd.opts?.dir, yes: cmd.opts?.yes, tag });
1252
+ const fynGlobal = await makeFynGlobal(cmd, { yes: cmd.opts?.yes });
1235
1253
  const packageSpec = cmd.args?.package;
1236
- if (!packageSpec && !tag) {
1254
+ if (!packageSpec && !fynGlobal.tag) {
1237
1255
  logger.error("No package specified");
1238
1256
  logger.info("Usage: fyn global remove <name>[@<version>]");
1239
1257
  logger.info(" Or: fyn global --tag=<tag> remove");
@@ -1252,11 +1270,9 @@ const commands = {
1252
1270
  "dir": { args: "<dir string>", desc: "Directory for global packages (default: ~/.fyn/global)" }
1253
1271
  },
1254
1272
  async exec(cmd) {
1255
- setLogLevel(cmd.opts?.logLevel || cmd.rootCmd?.opts?.logLevel);
1256
- const tag = cmd.getParent()?.opts?.tag;
1257
- const fynGlobal = new FynGlobal({ globalDir: cmd.opts?.dir, tag });
1273
+ const fynGlobal = await makeFynGlobal(cmd);
1258
1274
  const packageSpec = cmd.args?.package;
1259
- if (!packageSpec && !tag) {
1275
+ if (!packageSpec && !fynGlobal.tag) {
1260
1276
  logger.error("No package specified");
1261
1277
  logger.info("Usage: fyn global link <name>@<version>");
1262
1278
  logger.info(" Or: fyn global --tag=<tag> link");
@@ -1276,9 +1292,7 @@ const commands = {
1276
1292
  "dir": { args: "<dir string>", desc: "Directory for global packages (default: ~/.fyn/global)" }
1277
1293
  },
1278
1294
  async exec(cmd) {
1279
- setLogLevel(cmd.opts?.logLevel || cmd.rootCmd?.opts?.logLevel);
1280
- const tag = cmd.getParent()?.opts?.tag;
1281
- const fynGlobal = new FynGlobal({ globalDir: cmd.opts?.dir, tag });
1295
+ const fynGlobal = await makeFynGlobal(cmd);
1282
1296
  await fynGlobal.listGlobalPackages(cmd.args?.name);
1283
1297
  }
1284
1298
  },
@@ -1289,9 +1303,7 @@ const commands = {
1289
1303
  "dir": { args: "<dir string>", desc: "Directory for global packages (default: ~/.fyn/global)" }
1290
1304
  },
1291
1305
  async exec(cmd) {
1292
- setLogLevel(cmd.opts?.logLevel || cmd.rootCmd?.opts?.logLevel);
1293
- const tag = cmd.getParent()?.opts?.tag;
1294
- const fynGlobal = new FynGlobal({ globalDir: cmd.opts?.dir, tag });
1306
+ const fynGlobal = await makeFynGlobal(cmd);
1295
1307
  const packageSpec = cmd.args?.package;
1296
1308
  const updated = await fynGlobal.updateGlobalPackage(packageSpec);
1297
1309
  if (!updated) {
@@ -1306,9 +1318,7 @@ const commands = {
1306
1318
  "dir": { args: "<dir string>", desc: "Directory for global packages (default: ~/.fyn/global)" }
1307
1319
  },
1308
1320
  async exec(cmd) {
1309
- setLogLevel(cmd.opts?.logLevel || cmd.rootCmd?.opts?.logLevel);
1310
- const tag = cmd.getParent()?.opts?.tag;
1311
- const fynGlobal = new FynGlobal({ globalDir: cmd.opts?.dir, tag });
1321
+ const fynGlobal = await makeFynGlobal(cmd);
1312
1322
  await fynGlobal.useNodeVersion(cmd.args?.version);
1313
1323
  }
1314
1324
  },
@@ -1319,8 +1329,7 @@ const commands = {
1319
1329
  "dir": { args: "<dir string>", desc: "Directory for global packages (default: ~/.fyn/global)" }
1320
1330
  },
1321
1331
  async exec(cmd) {
1322
- setLogLevel(cmd.opts?.logLevel || cmd.rootCmd?.opts?.logLevel);
1323
- const fynGlobal = new FynGlobal({ globalDir: cmd.opts?.dir });
1332
+ const fynGlobal = await makeFynGlobal(cmd);
1324
1333
  const packageName = cmd.args?.package;
1325
1334
  const removed = await fynGlobal.cleanupPackage(packageName);
1326
1335
  if (removed === 0) {
@@ -1336,9 +1345,7 @@ const commands = {
1336
1345
  "dir": { args: "<dir string>", desc: "Directory for global packages (default: ~/.fyn/global)" }
1337
1346
  },
1338
1347
  async exec(cmd) {
1339
- setLogLevel(cmd.opts?.logLevel || cmd.rootCmd?.opts?.logLevel);
1340
- const tag = cmd.getParent()?.opts?.tag;
1341
- const fynGlobal = new FynGlobal({ globalDir: cmd.opts?.dir, tag });
1348
+ const fynGlobal = await makeFynGlobal(cmd);
1342
1349
  fynGlobal.showPathSetup();
1343
1350
  }
1344
1351
  }
@@ -1420,6 +1427,9 @@ module.exports = {
1420
1427
  run,
1421
1428
  fun,
1422
1429
  nodeGyp,
1430
+ getRunExitCode,
1431
+ pickEnvOptions,
1432
+ setLockfile,
1423
1433
  hardLinkDir
1424
1434
  };
1425
1435
 
@@ -1686,6 +1696,7 @@ class ShowStat {
1686
1696
  return this._show(pkgIds);
1687
1697
  }).catch((err) => {
1688
1698
  logger.error(err);
1699
+ throw err;
1689
1700
  }).finally(() => {
1690
1701
  logger.removeItem(FETCH_META);
1691
1702
  });
@@ -1977,10 +1988,26 @@ module.exports.__TEST__ = {
1977
1988
  /***/ "./lib/cacache-util.ts":
1978
1989
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
1979
1990
 
1991
+ "use strict";
1992
+
1980
1993
  const cacache = __webpack_require__("./node_modules/.f/_/cacache/20.0.1/cacache/lib/index.js");
1981
1994
  const fs = (__webpack_require__("fs").promises);
1982
1995
  const path = __webpack_require__("path");
1983
1996
  const crypto = __webpack_require__("crypto");
1997
+ function hashKey(key) {
1998
+ return crypto.createHash("sha256").update(key).digest("hex");
1999
+ }
2000
+ function getBucketPath(cache, key) {
2001
+ const hashed = hashKey(key);
2002
+ const indexV = (__webpack_require__("./node_modules/.f/_/cacache/20.0.1/cacache/package.json")["cache-version"].index);
2003
+ return path.join(
2004
+ cache,
2005
+ `index-v${indexV}`,
2006
+ hashed.slice(0, 2),
2007
+ hashed.slice(2, 4),
2008
+ hashed.slice(4)
2009
+ );
2010
+ }
1984
2011
  async function refreshCacheEntry(cache, key) {
1985
2012
  const bucket = getBucketPath(cache, key);
1986
2013
  const time = /* @__PURE__ */ new Date();
@@ -2007,20 +2034,6 @@ async function getCacheInfoWithRefreshTime(cache, key) {
2007
2034
  throw err;
2008
2035
  }
2009
2036
  }
2010
- function getBucketPath(cache, key) {
2011
- const hashed = hashKey(key);
2012
- const indexV = (__webpack_require__("./node_modules/.f/_/cacache/20.0.1/cacache/package.json")["cache-version"].index);
2013
- return path.join(
2014
- cache,
2015
- `index-v${indexV}`,
2016
- hashed.slice(0, 2),
2017
- hashed.slice(2, 4),
2018
- hashed.slice(4)
2019
- );
2020
- }
2021
- function hashKey(key) {
2022
- return crypto.createHash("sha256").update(key).digest("hex");
2023
- }
2024
2037
  module.exports = {
2025
2038
  refreshCacheEntry,
2026
2039
  getCacheInfoWithRefreshTime,
@@ -2105,9 +2118,12 @@ class DepData {
2105
2118
  return this.getPkgsData(item.optFailed)[item.name];
2106
2119
  }
2107
2120
  getPkgById(id) {
2108
- const splits = id.split("@");
2109
- const x = this.getPkgsData()[splits[0]];
2110
- return splits[1] ? x[splits[1]] : x;
2121
+ const lastAt = id.lastIndexOf("@");
2122
+ const sep = lastAt > 0 ? lastAt : -1;
2123
+ const name = sep > 0 ? id.slice(0, sep) : id;
2124
+ const version = sep > 0 ? id.slice(sep + 1) : void 0;
2125
+ const x = this.getPkgsData()[name];
2126
+ return version ? x[version] : x;
2111
2127
  }
2112
2128
  eachVersion(cb) {
2113
2129
  const pkgs = this.pkgs;
@@ -2740,6 +2756,7 @@ const unlock = util.promisify(lockfile.unlock);
2740
2756
  class FynGlobal {
2741
2757
  /**
2742
2758
  * Create a FynGlobal instance
2759
+ *
2743
2760
  * @param {Object} options - Configuration options
2744
2761
  * @param {string} [options.globalDir] - Root directory for global packages (default: ~/.fyn/global)
2745
2762
  * @param {string} [options.registry] - NPM registry URL (default: https://registry.npmjs.org)
@@ -2764,21 +2781,24 @@ class FynGlobal {
2764
2781
  }
2765
2782
  /**
2766
2783
  * Create a Fyn instance for global package installation
2784
+ *
2767
2785
  * @param {string} cwd - Working directory for the install
2768
2786
  * @param {boolean} fynlocal - Whether this is a local package
2769
2787
  * @returns {Fyn} Configured Fyn instance
2770
2788
  */
2771
2789
  _createFyn(cwd, fynlocal) {
2772
2790
  process.env.FYN_CENTRAL_DIR = Path.join(this.globalRoot, "_central-storage");
2791
+ const fynOpts = this.options.fynOpts || {};
2773
2792
  return new Fyn({
2774
2793
  opts: {
2794
+ ...fynOpts,
2775
2795
  cwd,
2776
2796
  targetDir: "node_modules",
2777
2797
  centralStore: true,
2778
2798
  lockfile: true,
2779
2799
  fynlocal,
2780
2800
  sourceMaps: false,
2781
- registry: this.options.registry || "https://registry.npmjs.org",
2801
+ registry: this.options.registry || fynOpts.registry || "https://registry.npmjs.org",
2782
2802
  layout: "normal",
2783
2803
  flattenTop: true
2784
2804
  },
@@ -2801,6 +2821,7 @@ class FynGlobal {
2801
2821
  }
2802
2822
  /**
2803
2823
  * Read the installed.json registry
2824
+ *
2804
2825
  * @returns {Object} Registry object with packages info
2805
2826
  */
2806
2827
  async readInstalledJson() {
@@ -2820,6 +2841,7 @@ class FynGlobal {
2820
2841
  }
2821
2842
  /**
2822
2843
  * Get all versions of a package from the registry
2844
+ *
2823
2845
  * @param {string} packageName
2824
2846
  * @returns {Array} Array of version info objects
2825
2847
  */
@@ -2829,6 +2851,7 @@ class FynGlobal {
2829
2851
  }
2830
2852
  /**
2831
2853
  * Get the linked version of a package
2854
+ *
2832
2855
  * @param {string} packageName
2833
2856
  * @returns {Object|null} Version info or null
2834
2857
  */
@@ -2838,6 +2861,7 @@ class FynGlobal {
2838
2861
  }
2839
2862
  /**
2840
2863
  * Find package info by tag (e.g., g1, g2)
2864
+ *
2841
2865
  * @param {string} tag - The tag to find
2842
2866
  * @returns {Object|null} Object with packageName and versionInfo, or null if not found
2843
2867
  */
@@ -2854,6 +2878,7 @@ class FynGlobal {
2854
2878
  }
2855
2879
  /**
2856
2880
  * Validate that the tag option points to an existing installation
2881
+ *
2857
2882
  * @returns {Object} Object with packageName and versionInfo
2858
2883
  * @throws {Error} If tag is set but doesn't exist
2859
2884
  */
@@ -2884,14 +2909,19 @@ class FynGlobal {
2884
2909
  }
2885
2910
  /**
2886
2911
  * Remove a version from the registry
2912
+ *
2913
+ * @param {string} packageName - package name
2914
+ * @param {string} dir - the tag dir (e.g. "g1"); the unique key of an entry.
2915
+ * The same version can exist under multiple tag dirs (--new-tag), so entries
2916
+ * must be identified by dir, not by version string.
2887
2917
  */
2888
- async removeFromRegistry(packageName, version) {
2918
+ async removeFromRegistry(packageName, dir) {
2889
2919
  const registry = await this.readInstalledJson();
2890
2920
  if (!registry.packages[packageName]) {
2891
2921
  return;
2892
2922
  }
2893
2923
  const versions = registry.packages[packageName].versions;
2894
- const idx = versions.findIndex((v) => v.version === version);
2924
+ const idx = versions.findIndex((v) => v.dir === dir);
2895
2925
  if (idx >= 0) {
2896
2926
  versions.splice(idx, 1);
2897
2927
  if (versions.length === 0) {
@@ -3125,6 +3155,7 @@ class FynGlobal {
3125
3155
  }
3126
3156
  /**
3127
3157
  * Link bin executables to global bin directory
3158
+ *
3128
3159
  * @param {string} gId - Package directory ID (e.g., "g1")
3129
3160
  * @param {Object} bins - Map of binName -> binPath
3130
3161
  * @param {boolean} force - If true, overwrite existing bins
@@ -3158,6 +3189,7 @@ class FynGlobal {
3158
3189
  * - Latest version wins (becomes linked)
3159
3190
  * - Prompts if newer version available when installing older
3160
3191
  * - Prompts to remove old versions after installing new
3192
+ *
3161
3193
  * @param {string} packageSpec - Package spec to install
3162
3194
  * @param {Object} [options] - Install options
3163
3195
  * @param {boolean} [options.newTag] - Install as new tag even if same version exists
@@ -3209,7 +3241,7 @@ class FynGlobal {
3209
3241
  if (!existingExact.linked) {
3210
3242
  const linkIt = await this.promptYesNo(`Link this version to make it active?`);
3211
3243
  if (linkIt) {
3212
- await this.linkPackageVersion(packageName, existingExact.version);
3244
+ await this.linkPackageVersion(`${packageName}@${existingExact.version}`);
3213
3245
  }
3214
3246
  }
3215
3247
  return false;
@@ -3256,7 +3288,7 @@ class FynGlobal {
3256
3288
  if (shouldLink) {
3257
3289
  for (const existing of existingVersions) {
3258
3290
  if (existing.linked) {
3259
- await this.unlinkBinsForVersion(packageName, existing.version);
3291
+ await this.unlinkBinsForVersion(packageName, existing.dir);
3260
3292
  existing.linked = false;
3261
3293
  await this.addToRegistry(packageName, existing);
3262
3294
  }
@@ -3291,7 +3323,7 @@ class FynGlobal {
3291
3323
  if (removeOld) {
3292
3324
  for (const existing of existingVersions) {
3293
3325
  if (existing.version !== installedVersion) {
3294
- await this.removeVersion(packageName, existing.version);
3326
+ await this.removeVersion(packageName, existing.dir);
3295
3327
  logger.info(`Removed ${packageName}@${existing.version}`);
3296
3328
  }
3297
3329
  }
@@ -3315,9 +3347,9 @@ class FynGlobal {
3315
3347
  /**
3316
3348
  * Unlink bins for a specific version
3317
3349
  */
3318
- async unlinkBinsForVersion(packageName, version) {
3350
+ async unlinkBinsForVersion(packageName, dir) {
3319
3351
  const versions = await this.getPackageVersions(packageName);
3320
- const versionInfo = versions.find((v) => v.version === version);
3352
+ const versionInfo = versions.find((v) => v.dir === dir);
3321
3353
  if (!versionInfo) return;
3322
3354
  const binLinker = this._getBinLinker();
3323
3355
  const bins = this._getVersionBinTargets(versionInfo);
@@ -3331,24 +3363,30 @@ class FynGlobal {
3331
3363
  }
3332
3364
  }
3333
3365
  /**
3334
- * Remove a specific version of a package
3366
+ * Remove a specific installed entry of a package, identified by its tag dir
3367
+ * (the unique key — the same version may exist under several tag dirs).
3368
+ *
3369
+ * @param {string} packageName - package name
3370
+ * @param {string} dir - the entry's tag dir (e.g. "g1")
3371
+ * @returns {boolean} true if an entry was removed
3335
3372
  */
3336
- async removeVersion(packageName, version) {
3373
+ async removeVersion(packageName, dir) {
3337
3374
  const versions = await this.getPackageVersions(packageName);
3338
- const versionInfo = versions.find((v) => v.version === version);
3375
+ const versionInfo = versions.find((v) => v.dir === dir);
3339
3376
  if (!versionInfo) {
3340
3377
  return false;
3341
3378
  }
3342
3379
  if (versionInfo.linked) {
3343
- await this.unlinkBinsForVersion(packageName, version);
3380
+ await this.unlinkBinsForVersion(packageName, dir);
3344
3381
  }
3345
3382
  const pkgDir = Path.join(this.packagesDir, versionInfo.dir);
3346
3383
  await Fs.$.rimraf(pkgDir);
3347
- await this.removeFromRegistry(packageName, version);
3384
+ await this.removeFromRegistry(packageName, dir);
3348
3385
  return true;
3349
3386
  }
3350
3387
  /**
3351
3388
  * Remove globally installed package version(s)
3389
+ *
3352
3390
  * @param {string} packageSpec - Package name or name@semver pattern
3353
3391
  * - name: remove all versions (warns if multiple, won't remove linked unless only one)
3354
3392
  * - name@version: remove exact version
@@ -3359,11 +3397,12 @@ class FynGlobal {
3359
3397
  if (this.tag) {
3360
3398
  const found = await this.validateTag();
3361
3399
  logger.info(`Removing ${found.packageName}@${found.versionInfo.version} (${this.tag})`);
3362
- await this.removeVersion(found.packageName, found.versionInfo.version);
3400
+ await this.removeVersion(found.packageName, found.versionInfo.dir);
3363
3401
  logger.info(`Removed ${found.packageName}@${found.versionInfo.version}`);
3364
3402
  return true;
3365
3403
  }
3366
- let packageName, versionSpec;
3404
+ let packageName;
3405
+ let versionSpec;
3367
3406
  if (packageSpec.startsWith("@")) {
3368
3407
  const lastAt = packageSpec.lastIndexOf("@");
3369
3408
  if (lastAt > 0 && lastAt !== packageSpec.indexOf("@")) {
@@ -3418,13 +3457,14 @@ class FynGlobal {
3418
3457
  }
3419
3458
  }
3420
3459
  for (const versionInfo of toRemove) {
3421
- await this.removeVersion(packageName, versionInfo.version);
3460
+ await this.removeVersion(packageName, versionInfo.dir);
3422
3461
  logger.info(`Removed ${packageName}@${versionInfo.version}`);
3423
3462
  }
3424
3463
  return true;
3425
3464
  }
3426
3465
  /**
3427
3466
  * List globally installed packages
3467
+ *
3428
3468
  * @param {string} [filterName] - Optional package name to filter by
3429
3469
  */
3430
3470
  async listGlobalPackages(filterName) {
@@ -3482,10 +3522,13 @@ ${packageName}:`);
3482
3522
  }
3483
3523
  /**
3484
3524
  * Link (activate) a specific version of a package
3525
+ *
3485
3526
  * @param {string} packageSpec - Package name@version to link, or ignored if --tag is specified
3486
3527
  */
3487
3528
  async linkPackageVersion(packageSpec) {
3488
- let packageName, version, targetVersion;
3529
+ let packageName;
3530
+ let version;
3531
+ let targetVersion;
3489
3532
  if (this.tag) {
3490
3533
  const found = await this.validateTag();
3491
3534
  packageName = found.packageName;
@@ -3539,7 +3582,7 @@ Use: fyn global link ${packageName}@<version>`);
3539
3582
  const versions = await this.getPackageVersions(packageName);
3540
3583
  const currentLinked = versions.find((v) => v.linked);
3541
3584
  if (currentLinked) {
3542
- await this.unlinkBinsForVersion(packageName, currentLinked.version);
3585
+ await this.unlinkBinsForVersion(packageName, currentLinked.dir);
3543
3586
  }
3544
3587
  const pkgDir = Path.join(this.packagesDir, targetVersion.dir);
3545
3588
  const bins = await this.discoverBins(pkgDir, packageName);
@@ -3553,6 +3596,7 @@ Use: fyn global link ${packageName}@<version>`);
3553
3596
  }
3554
3597
  /**
3555
3598
  * Find a package by its local path
3599
+ *
3556
3600
  * @param {string} localPath - The local path to search for
3557
3601
  * @returns {Object|null} Object with packageName and versions, or null
3558
3602
  */
@@ -3572,6 +3616,7 @@ Use: fyn global link ${packageName}@<version>`);
3572
3616
  }
3573
3617
  /**
3574
3618
  * Update a globally installed package (linked version or specific tag)
3619
+ *
3575
3620
  * @param {string} packageSpec - Package name or local path
3576
3621
  */
3577
3622
  async updateGlobalPackage(packageSpec) {
@@ -3697,6 +3742,7 @@ Use: fyn global link ${packageName}@<version>`);
3697
3742
  }
3698
3743
  /**
3699
3744
  * Cleanup non-linked versions of a package
3745
+ *
3700
3746
  * @param {string} [packageName] - Package name to cleanup, or all packages if not specified
3701
3747
  * @returns {number} Number of versions removed
3702
3748
  */
@@ -3726,7 +3772,7 @@ Use: fyn global link ${packageName}@<version>`);
3726
3772
  continue;
3727
3773
  }
3728
3774
  for (const v of nonLinked) {
3729
- await this.removeVersion(pkgName, v.version);
3775
+ await this.removeVersion(pkgName, v.dir);
3730
3776
  logger.info(`Removed ${pkgName}@${v.version} (${v.dir})`);
3731
3777
  totalRemoved++;
3732
3778
  }
@@ -3782,6 +3828,7 @@ const fynTil = __webpack_require__("./lib/util/fyntil.ts");
3782
3828
  const FynCentral = __webpack_require__("./lib/fyn-central.ts");
3783
3829
  const xaa = __webpack_require__("./lib/util/xaa.ts");
3784
3830
  const { checkPkgNeedInstall } = __webpack_require__("./lib/util/check-pkg-need-install.ts");
3831
+ const { localExportsNeedInstall } = __webpack_require__("./lib/local-exports.ts");
3785
3832
  const lockfile = __webpack_require__("./node_modules/.f/_/lockfile/1.0.4/lockfile/lockfile.js");
3786
3833
  const createLock = util.promisify(lockfile.lock);
3787
3834
  const unlock = util.promisify(lockfile.unlock);
@@ -3907,7 +3954,13 @@ class Fyn {
3907
3954
  } else {
3908
3955
  centralDir = this._fynpo.config.centralDir;
3909
3956
  if (!centralDir) {
3910
- centralDir = Path.join(this._fynpo.dir, ".fynpo", "_store");
3957
+ const storeBaseDir = await fynTil.resolveGitMainWorktreeDir(this._fynpo.dir);
3958
+ if (storeBaseDir !== this._fynpo.dir) {
3959
+ logger.info(
3960
+ `fynpo monorepo is a git worktree; sharing central store from main worktree ${storeBaseDir}`
3961
+ );
3962
+ }
3963
+ centralDir = Path.join(storeBaseDir, ".fynpo", "_store");
3911
3964
  }
3912
3965
  logger.info(`Enabling central store by fynpo monorepo using dir ${centralDir}`);
3913
3966
  }
@@ -3922,6 +3975,7 @@ class Fyn {
3922
3975
  }
3923
3976
  /**
3924
3977
  * Check user production mode option against saved install config in node_modules
3978
+ *
3925
3979
  * @remarks - this._installConfig must've been initialized
3926
3980
  * @returns nothing
3927
3981
  */
@@ -3979,13 +4033,13 @@ class Fyn {
3979
4033
  const fynInstallConfig = JSON.parse(await Fs.readFile(filename));
3980
4034
  logger.debug("loaded fynInstallConfig", fynInstallConfig);
3981
4035
  const { layout } = fynInstallConfig;
3982
- if (layout && layout !== this._layout) {
4036
+ if (layout && layout !== this._options.layout) {
3983
4037
  if (this._cliSource.layout !== "default") {
3984
4038
  logger.warn(
3985
- `Forcing layout to ${layout} from ${this._layout} because your existing node_modules uses that. To change it, please remove node_modules first.`
4039
+ `Forcing layout to ${layout} from ${this._options.layout} because your existing node_modules uses that. To change it, please remove node_modules first.`
3986
4040
  );
3987
4041
  }
3988
- this._layout = layout;
4042
+ this._options.layout = layout;
3989
4043
  }
3990
4044
  const recordedShort = fynInstallConfig.shortPkgDir === void 0 ? true : fynInstallConfig.shortPkgDir;
3991
4045
  if (recordedShort !== this._shortPkgDir) {
@@ -4106,6 +4160,7 @@ class Fyn {
4106
4160
  }
4107
4161
  /**
4108
4162
  * Get the version of a direct dependency from package.json
4163
+ *
4109
4164
  * @param {string} pkgName - Package name to look up
4110
4165
  * @returns {string|null} The version or null if not found
4111
4166
  */
@@ -4180,6 +4235,12 @@ class Fyn {
4180
4235
  return true;
4181
4236
  }
4182
4237
  }
4238
+ if (await localExportsNeedInstall({
4239
+ cwd: this._cwd,
4240
+ manifest: this._installConfig.localExports
4241
+ })) {
4242
+ return true;
4243
+ }
4183
4244
  return false;
4184
4245
  }
4185
4246
  setLocalDeps(localsByDepth) {
@@ -4194,6 +4255,9 @@ class Fyn {
4194
4255
  setLocalPkgLinks(localLinks) {
4195
4256
  this._installConfig.localPkgLinks = localLinks;
4196
4257
  }
4258
+ setLocalExports(manifest) {
4259
+ this._installConfig.localExports = manifest;
4260
+ }
4197
4261
  // save the config to outputDir
4198
4262
  async saveInstallConfig() {
4199
4263
  const outputDir = this.getOutputDir();
@@ -4210,7 +4274,7 @@ class Fyn {
4210
4274
  time: Date.now() + 5,
4211
4275
  centralDir,
4212
4276
  production: this.production,
4213
- layout: this._layout,
4277
+ layout: this._options.layout,
4214
4278
  shortPkgDir: this._shortPkgDir
4215
4279
  // not a good idea to save --run-npm options to install config because
4216
4280
  // future fyn install will automatically run them and would be unexpected.
@@ -4247,7 +4311,7 @@ class Fyn {
4247
4311
  let pkgFile;
4248
4312
  if (options.pkgFile === "package.json") {
4249
4313
  let foundDir = null;
4250
- const paths = pathUpEach(this._cwd, (path) => {
4314
+ pathUpEach(this._cwd, (path) => {
4251
4315
  const testPath = Path.join(path, "package.json");
4252
4316
  if (Fs.existsSync(testPath)) {
4253
4317
  foundDir = path;
@@ -4331,6 +4395,46 @@ class Fyn {
4331
4395
  get showDeprecated() {
4332
4396
  return this._options.showDeprecated && "show-deprecated";
4333
4397
  }
4398
+ // package.json `fyn.allowScripts` whitelist - maps `name@spec` or
4399
+ // `name@version` to the lifecycle scripts allowed for packages that did not
4400
+ // come from a configured registry (github/git/url tarball deps).
4401
+ get allowScripts() {
4402
+ if (this._allowScripts === void 0 && this._pkg) {
4403
+ this._allowScripts = _.get(this._pkg, ["fyn", "allowScripts"]) || {};
4404
+ }
4405
+ return this._allowScripts || {};
4406
+ }
4407
+ // package.json `fyn.allowTopLevelScripts` - opt-in (default off) to trust the
4408
+ // lifecycle scripts of non-registry packages (github/git/url tarball) that are
4409
+ // declared directly in the top-level package.json, without per-package
4410
+ // `fyn.allowScripts` entries. `true`/`"*"` allows all lifecycle scripts; an
4411
+ // array allows only those script names. Transitive deps stay blocked.
4412
+ get allowTopLevelScripts() {
4413
+ if (this._allowTopLevelScripts === void 0 && this._pkg) {
4414
+ this._allowTopLevelScripts = _.get(this._pkg, ["fyn", "allowTopLevelScripts"]) || false;
4415
+ }
4416
+ return this._allowTopLevelScripts || false;
4417
+ }
4418
+ // Security policy: transitive (non-top-level) dependencies must resolve from
4419
+ // a published registry. git/github/url-tarball sources and unparseable semver
4420
+ // are rejected (hard error). Local (file:/link:/symlink, incl. fynpo
4421
+ // siblings) and `npm:` aliases are accepted. Only the top-level package.json
4422
+ // may declare non-registry deps.
4423
+ //
4424
+ // Default ON. Disable explicitly with the CLI flag
4425
+ // `--no-enforce-registry-deps` or package.json `fyn.enforceRegistryDeps:false`.
4426
+ // Precedence: CLI option (when given) > package.json fyn flag > default (on).
4427
+ get enforceRegistryDeps() {
4428
+ const cliOpt = this._options.enforceRegistryDeps;
4429
+ if (cliOpt !== void 0) {
4430
+ return Boolean(cliOpt);
4431
+ }
4432
+ if (this._enforceRegistryDeps === void 0 && this._pkg) {
4433
+ const pkgOpt = _.get(this._pkg, ["fyn", "enforceRegistryDeps"]);
4434
+ this._enforceRegistryDeps = pkgOpt === void 0 ? true : Boolean(pkgOpt);
4435
+ }
4436
+ return this._enforceRegistryDeps === void 0 ? true : this._enforceRegistryDeps;
4437
+ }
4334
4438
  get refreshOptionals() {
4335
4439
  return this._options.refreshOptionals;
4336
4440
  }
@@ -4522,6 +4626,7 @@ class Fyn {
4522
4626
  }
4523
4627
  /**
4524
4628
  * Scan FV_DIR for modules saved in the ${name}/${version} format
4629
+ *
4525
4630
  * @returns {*} pkgs under fv dir with their versions
4526
4631
  */
4527
4632
  async loadFvVersions() {
@@ -4559,7 +4664,7 @@ class Fyn {
4559
4664
  async createPkgOutDir(dir, keep) {
4560
4665
  try {
4561
4666
  const r = await Fs.$.mkdirp(dir);
4562
- if (r === null && !keep && dir !== this.getOutputDir()) {
4667
+ if (r === void 0 && !keep && dir !== this.getOutputDir()) {
4563
4668
  await this.clearPkgOutDir(dir);
4564
4669
  }
4565
4670
  } catch (err) {
@@ -4823,6 +4928,373 @@ class LifecycleScripts {
4823
4928
  module.exports = LifecycleScripts;
4824
4929
 
4825
4930
 
4931
+ /***/ }),
4932
+
4933
+ /***/ "./lib/local-exports.ts":
4934
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4935
+
4936
+ "use strict";
4937
+
4938
+ const Path = __webpack_require__("path");
4939
+ const Fs = __webpack_require__("./lib/util/file-ops.ts");
4940
+ const fynTil = __webpack_require__("./lib/util/fyntil.ts");
4941
+ const { getUrlType } = __webpack_require__("./lib/util/lifecycle-script-policy.ts");
4942
+ const DEFAULT_ROOT_DIR = "_fyn";
4943
+ const MANIFEST_FILE = ".fyn-local-exports.json";
4944
+ const MANIFEST_VERSION = 1;
4945
+ const SAFE_NAME = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
4946
+ const posixify = (value) => value.split(Path.sep).join("/");
4947
+ const rootOfEntry = (entry) => entry.root || DEFAULT_ROOT_DIR;
4948
+ const emptyManifest = () => ({ version: MANIFEST_VERSION, exports: {} });
4949
+ const packagePathParts = (name) => {
4950
+ const parts = typeof name === "string" ? name.split("/") : [];
4951
+ const scoped = parts.length === 2 && parts[0][0] === "@";
4952
+ const safe = scoped ? SAFE_NAME.test(parts[0].slice(1)) && SAFE_NAME.test(parts[1]) : parts.length === 1 && SAFE_NAME.test(parts[0]);
4953
+ if (!safe) {
4954
+ throw new Error(`Invalid package name for fyn.localExports: ${name}`);
4955
+ }
4956
+ return parts;
4957
+ };
4958
+ const checkExportName = (packageName, exportName) => {
4959
+ if (!SAFE_NAME.test(exportName) || exportName === "." || exportName === "..") {
4960
+ throw new Error(
4961
+ `Invalid fyn.localExports export name ${JSON.stringify(exportName)} in ${packageName}`
4962
+ );
4963
+ }
4964
+ };
4965
+ const normalizeRootDir = (value, ctx) => {
4966
+ if (typeof value !== "string" || !value.trim()) {
4967
+ throw new Error(`${ctx} must be a non-empty relative directory`);
4968
+ }
4969
+ if (Path.isAbsolute(value) || Path.posix.isAbsolute(value) || Path.win32.isAbsolute(value)) {
4970
+ throw new Error(`${ctx} must not be absolute: ${value}`);
4971
+ }
4972
+ const parts = value.split(/[\\/]+/).filter((part) => part && part !== ".");
4973
+ if (parts.length === 0 || parts.includes("..") || parts.includes("node_modules") || parts.includes(".git")) {
4974
+ throw new Error(`Unsafe ${ctx}: ${value}`);
4975
+ }
4976
+ return parts.join("/");
4977
+ };
4978
+ const nestsWithin = (parent, child) => {
4979
+ const relative = Path.posix.relative(parent, child);
4980
+ return relative !== "" && !relative.startsWith("..");
4981
+ };
4982
+ const assertNoNestedRoots = (roots) => {
4983
+ const uniq = [...new Set(roots)];
4984
+ for (const a of uniq) {
4985
+ for (const b of uniq) {
4986
+ if (a !== b && nestsWithin(a, b)) {
4987
+ throw new Error(`fyn local export directories must not nest: ${a} contains ${b}`);
4988
+ }
4989
+ }
4990
+ }
4991
+ };
4992
+ const resolveLocalExportsConfig = (pkg) => {
4993
+ const fyn = pkg && pkg.fyn || {};
4994
+ const defaultDir = fyn.localExportsDir === void 0 ? DEFAULT_ROOT_DIR : normalizeRootDir(fyn.localExportsDir, "fyn.localExportsDir");
4995
+ const byPackage = {};
4996
+ const dirs = fyn.localExportsDirs;
4997
+ if (dirs !== void 0 && dirs !== null) {
4998
+ if (typeof dirs !== "object" || Array.isArray(dirs)) {
4999
+ throw new Error("fyn.localExportsDirs must be an object of package name to directory");
5000
+ }
5001
+ for (const name of Object.keys(dirs)) {
5002
+ packagePathParts(name);
5003
+ byPackage[name] = normalizeRootDir(dirs[name], `fyn.localExportsDirs[${JSON.stringify(name)}]`);
5004
+ }
5005
+ }
5006
+ assertNoNestedRoots([defaultDir, ...Object.values(byPackage)]);
5007
+ return { defaultDir, byPackage };
5008
+ };
5009
+ const rootDirForPackage = (config, name) => config.byPackage && config.byPackage[name] || config.defaultDir;
5010
+ const localExportsScanIgnores = (pkg) => {
5011
+ let config;
5012
+ try {
5013
+ config = resolveLocalExportsConfig(pkg);
5014
+ } catch (err) {
5015
+ return [];
5016
+ }
5017
+ const roots = [config.defaultDir, ...Object.values(config.byPackage)];
5018
+ return [...new Set(roots)].map((root) => `**/${root}`);
5019
+ };
5020
+ const groupByRoot = (exportsMap) => {
5021
+ const byRoot = /* @__PURE__ */ new Map();
5022
+ for (const target of Object.keys(exportsMap)) {
5023
+ const root = rootOfEntry(exportsMap[target]);
5024
+ if (!byRoot.has(root)) {
5025
+ byRoot.set(root, {});
5026
+ }
5027
+ byRoot.get(root)[target] = exportsMap[target];
5028
+ }
5029
+ return byRoot;
5030
+ };
5031
+ const normalizeManifest = (manifest) => {
5032
+ if (!manifest) {
5033
+ return emptyManifest();
5034
+ }
5035
+ if (manifest.version !== MANIFEST_VERSION || !manifest.exports || Array.isArray(manifest.exports) || typeof manifest.exports !== "object") {
5036
+ throw new Error("Invalid fyn local exports manifest");
5037
+ }
5038
+ for (const target of Object.keys(manifest.exports)) {
5039
+ const entry = manifest.exports[target];
5040
+ if (!entry || typeof entry !== "object" || typeof entry.source !== "string" || !entry.source) {
5041
+ throw new Error("Invalid fyn local exports manifest entry");
5042
+ }
5043
+ const root = normalizeRootDir(rootOfEntry(entry), "fyn local exports manifest root");
5044
+ const expectedTarget = posixify(
5045
+ Path.join(root, ...packagePathParts(entry.package), entry.export)
5046
+ );
5047
+ checkExportName(entry.package, entry.export);
5048
+ if (target !== expectedTarget || entry.target !== expectedTarget) {
5049
+ throw new Error(`Invalid fyn local exports manifest target: ${target}`);
5050
+ }
5051
+ }
5052
+ return manifest;
5053
+ };
5054
+ const checkSourcePath = (packageName, sourcePath) => {
5055
+ if (typeof sourcePath !== "string" || !sourcePath.trim()) {
5056
+ throw new Error(`fyn.localExports source for ${packageName} must be a relative directory`);
5057
+ }
5058
+ if (Path.isAbsolute(sourcePath) || Path.posix.isAbsolute(sourcePath) || Path.win32.isAbsolute(sourcePath)) {
5059
+ throw new Error(`fyn.localExports source for ${packageName} must not be absolute`);
5060
+ }
5061
+ const parts = sourcePath.split(/[\\/]+/);
5062
+ if (parts.includes("..") || parts.includes("node_modules") || parts.includes(".git")) {
5063
+ throw new Error(`Unsafe fyn.localExports source ${sourcePath} in ${packageName}`);
5064
+ }
5065
+ };
5066
+ const isInside = (root, child) => {
5067
+ const relative = Path.relative(root, child);
5068
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${Path.sep}`);
5069
+ };
5070
+ const readOwnedManifest = async (cwd, root) => {
5071
+ const marker = Path.join(cwd, root, MANIFEST_FILE);
5072
+ try {
5073
+ const value = JSON.parse(await Fs.readFile(marker, "utf8"));
5074
+ return normalizeManifest(value);
5075
+ } catch (err) {
5076
+ if (err.code === "ENOENT" || err.code === "ENOTDIR") {
5077
+ return null;
5078
+ }
5079
+ throw new Error(`Invalid ${root} ownership manifest: ${err.message}`);
5080
+ }
5081
+ };
5082
+ const sourcePathFor = (cwd, entry) => Path.resolve(cwd, entry.source);
5083
+ const linkMatches = async (cwd, entry) => {
5084
+ try {
5085
+ const target = await Fs.realpath(Path.resolve(cwd, entry.target));
5086
+ const source = await Fs.realpath(sourcePathFor(cwd, entry));
5087
+ return target === source;
5088
+ } catch (err) {
5089
+ return false;
5090
+ }
5091
+ };
5092
+ async function makeLocalExportsManifest({ cwd, depInfos, config: exportsConfig }) {
5093
+ const resolved = exportsConfig || { defaultDir: DEFAULT_ROOT_DIR, byPackage: {} };
5094
+ const entries = {};
5095
+ const consumerRoot = await Fs.realpath(cwd);
5096
+ for (const depInfo of depInfos) {
5097
+ if (depInfo.local !== "hard" || getUrlType(depInfo) || depInfo.optFailed || depInfo._removed) {
5098
+ continue;
5099
+ }
5100
+ const config = depInfo.json && depInfo.json.fyn && depInfo.json.fyn.localExports;
5101
+ if (config === void 0 || config === false) {
5102
+ continue;
5103
+ }
5104
+ if (!config || Array.isArray(config) || typeof config !== "object") {
5105
+ throw new Error(`fyn.localExports for ${depInfo.name} must be an object or false`);
5106
+ }
5107
+ const packageParts = packagePathParts(depInfo.name);
5108
+ const packageRoot = await Fs.realpath(depInfo.dir);
5109
+ for (const exportName of Object.keys(config).sort()) {
5110
+ const configuredSource = config[exportName];
5111
+ if (configuredSource === false) {
5112
+ continue;
5113
+ }
5114
+ checkExportName(depInfo.name, exportName);
5115
+ checkSourcePath(depInfo.name, configuredSource);
5116
+ const unresolvedSource = Path.resolve(packageRoot, configuredSource);
5117
+ let source;
5118
+ let sourceStat;
5119
+ try {
5120
+ source = await Fs.realpath(unresolvedSource);
5121
+ sourceStat = await Fs.stat(source);
5122
+ } catch (err) {
5123
+ throw new Error(
5124
+ `fyn.localExports source ${configuredSource} in ${depInfo.name} does not exist`
5125
+ );
5126
+ }
5127
+ if (!isInside(packageRoot, source)) {
5128
+ throw new Error(
5129
+ `fyn.localExports source ${configuredSource} in ${depInfo.name} escapes the package`
5130
+ );
5131
+ }
5132
+ if (!sourceStat.isDirectory()) {
5133
+ throw new Error(
5134
+ `fyn.localExports source ${configuredSource} in ${depInfo.name} is not a directory`
5135
+ );
5136
+ }
5137
+ const root = rootDirForPackage(resolved, depInfo.name);
5138
+ const target = posixify(Path.join(root, ...packageParts, exportName));
5139
+ const relativeSource = posixify(Path.relative(consumerRoot, source));
5140
+ const prior = entries[target];
5141
+ if (prior && (prior.source !== relativeSource || prior.version !== depInfo.version)) {
5142
+ throw new Error(
5143
+ `Local export destination collision for ${depInfo.name}@${depInfo.version}: ${target}`
5144
+ );
5145
+ }
5146
+ const targetPath = Path.resolve(consumerRoot, target);
5147
+ entries[target] = {
5148
+ package: depInfo.name,
5149
+ version: depInfo.version,
5150
+ export: exportName,
5151
+ source: relativeSource,
5152
+ target,
5153
+ root,
5154
+ linkTarget: fynTil.isWin32 ? source : posixify(Path.relative(Path.dirname(targetPath), source))
5155
+ };
5156
+ }
5157
+ }
5158
+ const sortedEntries = {};
5159
+ for (const target of Object.keys(entries).sort()) {
5160
+ sortedEntries[target] = entries[target];
5161
+ }
5162
+ return { version: MANIFEST_VERSION, exports: sortedEntries };
5163
+ }
5164
+ async function rootNeedsInstall(cwd, root, exportsForRoot) {
5165
+ const rootManifest = { version: MANIFEST_VERSION, exports: exportsForRoot };
5166
+ let realized;
5167
+ try {
5168
+ realized = await readOwnedManifest(cwd, root);
5169
+ } catch (err) {
5170
+ return true;
5171
+ }
5172
+ if (!realized || JSON.stringify(realized) !== JSON.stringify(rootManifest)) {
5173
+ return true;
5174
+ }
5175
+ for (const target of Object.keys(exportsForRoot)) {
5176
+ if (!await linkMatches(cwd, exportsForRoot[target])) {
5177
+ return true;
5178
+ }
5179
+ }
5180
+ return false;
5181
+ }
5182
+ async function localExportsNeedInstall({ cwd, manifest }) {
5183
+ const desired = normalizeManifest(manifest);
5184
+ const byRoot = groupByRoot(desired.exports);
5185
+ if (byRoot.size === 0) {
5186
+ try {
5187
+ return Boolean(await readOwnedManifest(cwd, DEFAULT_ROOT_DIR));
5188
+ } catch (err) {
5189
+ return false;
5190
+ }
5191
+ }
5192
+ for (const [root, exportsForRoot] of byRoot) {
5193
+ if (await rootNeedsInstall(cwd, root, exportsForRoot)) {
5194
+ return true;
5195
+ }
5196
+ }
5197
+ return false;
5198
+ }
5199
+ async function reconcileOneRoot(cwd, root, exportsForRoot) {
5200
+ const targets = Object.keys(exportsForRoot);
5201
+ const rootPath = Path.join(cwd, root);
5202
+ let owned;
5203
+ try {
5204
+ owned = await readOwnedManifest(cwd, root);
5205
+ } catch (err) {
5206
+ if (targets.length === 0) {
5207
+ return;
5208
+ }
5209
+ throw err;
5210
+ }
5211
+ if (targets.length === 0) {
5212
+ if (owned) {
5213
+ await Fs.$.rimraf(rootPath);
5214
+ }
5215
+ return;
5216
+ }
5217
+ if (await Fs.exists(rootPath) && !owned) {
5218
+ throw new Error(`Refusing to modify ${rootPath} without a fyn ownership manifest`);
5219
+ }
5220
+ if (owned && !await rootNeedsInstall(cwd, root, exportsForRoot)) {
5221
+ return;
5222
+ }
5223
+ const desiredRootManifest = { version: MANIFEST_VERSION, exports: exportsForRoot };
5224
+ const suffix = `${process.pid}-${Date.now()}`;
5225
+ const staging = `${rootPath}.fyn-tmp-${suffix}`;
5226
+ const backup = `${rootPath}.fyn-old-${suffix}`;
5227
+ let movedExisting = false;
5228
+ try {
5229
+ await Fs.$.rimraf(staging);
5230
+ await Fs.$.mkdirp(staging);
5231
+ for (const target of targets) {
5232
+ const entry = exportsForRoot[target];
5233
+ const stagedTarget = Path.join(staging, Path.relative(root, target));
5234
+ const source = sourcePathFor(cwd, entry);
5235
+ const stat = await Fs.stat(source);
5236
+ if (!stat.isDirectory()) {
5237
+ throw new Error(`Local export source is not a directory: ${source}`);
5238
+ }
5239
+ await Fs.$.mkdirp(Path.dirname(stagedTarget));
5240
+ await fynTil.symlinkDir(stagedTarget, source, !fynTil.isWin32);
5241
+ }
5242
+ await Fs.writeFile(
5243
+ Path.join(staging, MANIFEST_FILE),
5244
+ `${JSON.stringify(desiredRootManifest, null, 2)}
5245
+ `
5246
+ );
5247
+ if (owned) {
5248
+ await Fs.$.rimraf(backup);
5249
+ await Fs.rename(rootPath, backup);
5250
+ movedExisting = true;
5251
+ }
5252
+ await Fs.$.mkdirp(Path.dirname(rootPath));
5253
+ await Fs.rename(staging, rootPath);
5254
+ if (movedExisting) {
5255
+ movedExisting = false;
5256
+ await Fs.$.rimraf(backup);
5257
+ }
5258
+ } catch (err) {
5259
+ await Fs.$.rimraf(staging);
5260
+ if (movedExisting) {
5261
+ await Fs.$.rimraf(rootPath);
5262
+ await Fs.rename(backup, rootPath);
5263
+ }
5264
+ throw err;
5265
+ }
5266
+ }
5267
+ async function reconcileLocalExports({ cwd, manifest, previous }) {
5268
+ const desired = normalizeManifest(manifest);
5269
+ const byRoot = groupByRoot(desired.exports);
5270
+ const allRoots = /* @__PURE__ */ new Set([DEFAULT_ROOT_DIR, ...byRoot.keys()]);
5271
+ if (previous) {
5272
+ for (const root of groupByRoot(normalizeManifest(previous).exports).keys()) {
5273
+ allRoots.add(root);
5274
+ }
5275
+ }
5276
+ for (const root of [...allRoots].sort()) {
5277
+ if (!byRoot.has(root)) {
5278
+ await reconcileOneRoot(cwd, root, {});
5279
+ }
5280
+ }
5281
+ for (const root of [...byRoot.keys()].sort()) {
5282
+ await reconcileOneRoot(cwd, root, byRoot.get(root));
5283
+ }
5284
+ }
5285
+ async function syncLocalExports(options) {
5286
+ return reconcileLocalExports(options);
5287
+ }
5288
+ module.exports = {
5289
+ makeLocalExportsManifest,
5290
+ reconcileLocalExports,
5291
+ syncLocalExports,
5292
+ localExportsNeedInstall,
5293
+ resolveLocalExportsConfig,
5294
+ localExportsScanIgnores
5295
+ };
5296
+
5297
+
4826
5298
  /***/ }),
4827
5299
 
4828
5300
  /***/ "./lib/local-pkg-builder.ts":
@@ -4891,7 +5363,12 @@ class LocalPkgBuilder {
4891
5363
  }, {});
4892
5364
  logger.debug("local pkgs for build all paths", allPaths, "uniq paths", uniqPaths);
4893
5365
  for (const path of uniqPaths) {
4894
- await this.addItem(byPathLookup[path]);
5366
+ try {
5367
+ await this.addItem(byPathLookup[path]);
5368
+ } catch (error) {
5369
+ this._startError = error;
5370
+ break;
5371
+ }
4895
5372
  }
4896
5373
  logger.debug("resolving build local _started promise");
4897
5374
  this._started.resolve();
@@ -4930,6 +5407,9 @@ class LocalPkgBuilder {
4930
5407
  logger.debug("waiting for local build item start, fullPath:", fullPath);
4931
5408
  await this._started.promise;
4932
5409
  }
5410
+ if (this._startError) {
5411
+ return { error: this._startError };
5412
+ }
4933
5413
  const x = this._waitItems[fullPath];
4934
5414
  if (x && x.promise) {
4935
5415
  logger.debug("waiting for build local item", fullPath, x);
@@ -5386,6 +5866,7 @@ module.exports = PkgBinLinker;
5386
5866
  "use strict";
5387
5867
 
5388
5868
  const Fs = __webpack_require__("./lib/util/file-ops.ts");
5869
+ const Path = __webpack_require__("path");
5389
5870
  const PkgBinLinkerBase = __webpack_require__("./lib/pkg-bin-linker-base.ts");
5390
5871
  const CYGWIN_LINK = `#!/bin/sh
5391
5872
  basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
@@ -5441,6 +5922,31 @@ class PkgBinLinkerWin32 extends PkgBinLinkerBase {
5441
5922
  await this._unlinkFile(symlink);
5442
5923
  await this._unlinkFile(symlink + ".cmd");
5443
5924
  }
5925
+ // Extract the {{TARGET}} path baked into a generated .cmd wrapper (the path
5926
+ // after `%~dp0\`, ignoring the node.exe reference).
5927
+ async _readBinLinkTarget(symlink) {
5928
+ const content = (await Fs.readFile(symlink + ".cmd")).toString();
5929
+ const matches = [...content.matchAll(/%~dp0[\\/]+([^"\r\n]+)/g)].map((m) => m[1]);
5930
+ return matches.find((m) => m !== "node.exe");
5931
+ }
5932
+ //
5933
+ // Platform specific: the "bin" is a pair of regular script files (cygwin +
5934
+ // .cmd), not a symlink, so the base _cleanLink's Fs.access on the wrapper
5935
+ // always succeeds and never cleans a stale bin. Instead, read the wrapper and
5936
+ // remove it only if the target it points to no longer exists.
5937
+ //
5938
+ async _cleanLink(sym) {
5939
+ const symlink = Path.join(this._binDir, sym);
5940
+ try {
5941
+ const target = await this._readBinLinkTarget(symlink);
5942
+ if (target && await Fs.exists(Path.join(this._binDir, target))) {
5943
+ return false;
5944
+ }
5945
+ } catch (e) {
5946
+ }
5947
+ await this._rmBinLink(symlink);
5948
+ return true;
5949
+ }
5444
5950
  async _readBinLinks() {
5445
5951
  return (await Fs.readdir(this._binDir)).filter((x) => !x.endsWith(".cmd"));
5446
5952
  }
@@ -5636,6 +6142,7 @@ const Fs = __webpack_require__("./lib/util/file-ops.ts");
5636
6142
  const _ = __webpack_require__("./node_modules/.f/_/lodash/4.17.21/lodash/lodash.min.js");
5637
6143
  const chalk = __webpack_require__("./node_modules/.f/_/chalk/4.1.2/chalk/source/index.js");
5638
6144
  const simpleSemverCompare = (__webpack_require__("./lib/util/semver.ts").simpleCompare);
6145
+ const Semver = __webpack_require__("./node_modules/.f/_/semver/7.8.0/semver/index.js");
5639
6146
  const Yaml = __webpack_require__("./node_modules/.f/_/yamljs/0.3.0/yamljs/lib/Yaml.js");
5640
6147
  const sortObjKeys = __webpack_require__("./lib/util/sort-obj-keys.ts");
5641
6148
  const {
@@ -5830,7 +6337,10 @@ class PkgDepLocker {
5830
6337
  const versions = {};
5831
6338
  _.each(sorted, (version) => {
5832
6339
  const vpkg = locked[version];
5833
- if (!_.isEmpty(vpkg) && vpkg._valid !== false) {
6340
+ const isLocalVpkg = vpkg && vpkg.$ === "local";
6341
+ const badVersionKey = !isLocalVpkg && !Semver.valid(version);
6342
+ const badDeps = vpkg && vpkg.dependencies && !this._depsResolvable(vpkg.dependencies);
6343
+ if (!_.isEmpty(vpkg) && vpkg._valid !== false && !badVersionKey && !badDeps) {
5834
6344
  if (vpkg.$ === "local") {
5835
6345
  vpkg.local = true;
5836
6346
  vpkg.dist = {
@@ -5853,6 +6363,15 @@ class PkgDepLocker {
5853
6363
  }
5854
6364
  versions[version] = vpkg;
5855
6365
  } else {
6366
+ if (badVersionKey) {
6367
+ logger.error(
6368
+ `lockfile entry for ${item.name} has invalid version key "${version}" - ignoring and re-resolving from registry`
6369
+ );
6370
+ } else if (badDeps) {
6371
+ logger.error(
6372
+ `lockfile entry ${item.name}@${version} has dependencies not satisfiable within the lock (corrupt lock) - ignoring and re-resolving from registry`
6373
+ );
6374
+ }
5856
6375
  valid = false;
5857
6376
  }
5858
6377
  });
@@ -5874,6 +6393,37 @@ class PkgDepLocker {
5874
6393
  }
5875
6394
  return valid && locked;
5876
6395
  }
6396
+ //
6397
+ // Check that every recorded dependency pin can be resolved within the lock data.
6398
+ // A healthy fyn lock is self-contained: every locked package's dependency specs are
6399
+ // satisfied by some version that also has a lock entry. A pin that points at a version
6400
+ // absent from the lock signals corruption (e.g. a version block's deps got swapped
6401
+ // during a rebase/merge), so the entry must be dropped and re-resolved.
6402
+ //
6403
+ _depsResolvable(deps) {
6404
+ for (const depName in deps) {
6405
+ if (!this._isDepResolvable(depName, deps[depName])) {
6406
+ return false;
6407
+ }
6408
+ }
6409
+ return true;
6410
+ }
6411
+ _isDepResolvable(depName, spec) {
6412
+ const depLock = this._lockData[depName];
6413
+ if (!depLock) return true;
6414
+ let versions;
6415
+ let rangeMap;
6416
+ if (depLock.versions) {
6417
+ versions = Object.keys(depLock.versions);
6418
+ rangeMap = depLock[LOCK_RSEMVERS] || {};
6419
+ } else {
6420
+ versions = Object.keys(depLock).filter((k) => !k.startsWith("_"));
6421
+ rangeMap = depLock._ || {};
6422
+ }
6423
+ if (Object.prototype.hasOwnProperty.call(rangeMap, spec)) return true;
6424
+ if (!Semver.validRange(spec)) return true;
6425
+ return versions.some((v) => Semver.valid(v) && Semver.satisfies(v, spec));
6426
+ }
5877
6427
  /**
5878
6428
  * Set the package.json's dependencies items and check if they changed from
5879
6429
  * lock data.
@@ -5881,6 +6431,7 @@ class PkgDepLocker {
5881
6431
  * - dependencies
5882
6432
  * - optionalDependencies
5883
6433
  * - devDependencies
6434
+ *
5884
6435
  * @param {*} pkgDepItems - dep items generated by makePkgDepItems in pkg-dep-resolver.js
5885
6436
  *
5886
6437
  * @returns {*} none
@@ -6044,6 +6595,14 @@ class PkgDepLocker {
6044
6595
  if (!Path.isAbsolute(filename)) filename = Path.resolve(filename);
6045
6596
  const data = (await Fs.readFile(filename)).toString();
6046
6597
  this._shaSum = this.shasum(data);
6598
+ if (/^(<<<<<<<|=======|>>>>>>>)/m.test(data)) {
6599
+ logger.error(
6600
+ `lockfile ${filename} has git conflict markers - ignoring it and re-resolving from registry`
6601
+ );
6602
+ this._shaSum = Date.now();
6603
+ this._lockData = {};
6604
+ return false;
6605
+ }
6047
6606
  this._lockData = Yaml.parse(data);
6048
6607
  const basedir = Path.dirname(filename);
6049
6608
  this._fullLocalPath(basedir);
@@ -6102,6 +6661,7 @@ const logFormat = __webpack_require__("./lib/util/log-format.ts");
6102
6661
  const { LONG_WAIT_META } = __webpack_require__("./lib/log-items.ts");
6103
6662
  const { checkPkgOsCpu, relativePath, unSlashNpmScope } = __webpack_require__("./lib/util/fyntil.ts");
6104
6663
  const { getDepSection, makeDepStep } = __webpack_require__("./node_modules/.f/_/@fynpo/base/1.1.22-fynlocal_h/@fynpo/base/dist/index.js");
6664
+ const { violatesRegistryPolicy } = __webpack_require__("./lib/util/registry-dep-policy.ts");
6105
6665
  const xaa = __webpack_require__("./lib/util/xaa.ts");
6106
6666
  const { AggregateError } = __webpack_require__("./node_modules/.f/_/@jchip/error/1.0.3/@jchip/error/dist/index.js");
6107
6667
  const Promise = __webpack_require__("./node_modules/.f/_/aveazul/1.1.0/aveazul/cjs-entry.cjs");
@@ -6368,6 +6928,31 @@ class PkgDepResolver {
6368
6928
  }
6369
6929
  return semver;
6370
6930
  }
6931
+ /**
6932
+ * Enforce the `fyn.enforceRegistryDeps` policy on a transitive dep item.
6933
+ * Throws (aborting the install) when the item is a non-registry source or has
6934
+ * an unparseable semver.
6935
+ *
6936
+ * @param {object} item - the (transitive) dep item being added
6937
+ * @param {object} parent - the dep item that requires `item`
6938
+ * @returns {void}
6939
+ */
6940
+ _enforceRegistryDep(item, parent) {
6941
+ const violation = violatesRegistryPolicy(item);
6942
+ if (!violation) {
6943
+ return;
6944
+ }
6945
+ const depId = `${item.name}@${item.semver}`;
6946
+ let reason = `has an invalid/unparseable version "${violation.semver}"`;
6947
+ if (violation.kind === "url") {
6948
+ reason = `is from a non-registry source (${violation.urlType})`;
6949
+ } else if (violation.kind === "local") {
6950
+ reason = `is a local dependency declared below a non-registry source (${violation.urlType})`;
6951
+ }
6952
+ throw new Error(
6953
+ `fyn.enforceRegistryDeps: transitive dependency "${depId}" required by "${parent.id}" ${reason}. Only the top-level package.json may use git/github/URL dependencies - transitive dependencies must come from a registry.`
6954
+ );
6955
+ }
6371
6956
  /**
6372
6957
  * create the dep relation items for a package
6373
6958
  *
@@ -6403,6 +6988,9 @@ class PkgDepResolver {
6403
6988
  deepResolve
6404
6989
  };
6405
6990
  const newItem = new DepItem(opt, depItem);
6991
+ if (this._fyn.enforceRegistryDeps && depItem.depth >= 1) {
6992
+ this._enforceRegistryDep(newItem, depItem);
6993
+ }
6406
6994
  if (noPrefetch !== true) this.prefetchMeta(newItem);
6407
6995
  items.push(newItem);
6408
6996
  }
@@ -6574,6 +7162,15 @@ class PkgDepResolver {
6574
7162
  }
6575
7163
  }
6576
7164
  const metaJson = meta.versions[resolved];
7165
+ const localFromMeta = meta.local || metaJson.local;
7166
+ if (localFromMeta) {
7167
+ if (!item.localType) {
7168
+ item.localType = localFromMeta;
7169
+ }
7170
+ if (this._fyn.enforceRegistryDeps && item.parent.depth >= 1) {
7171
+ this._enforceRegistryDep(item, item.parent);
7172
+ }
7173
+ }
6577
7174
  const platformCheck = () => {
6578
7175
  const sysCheck2 = checkPkgOsCpu(metaJson);
6579
7176
  if (sysCheck2 !== true) {
@@ -6630,11 +7227,7 @@ class PkgDepResolver {
6630
7227
  pkgV.hasI = 1;
6631
7228
  }
6632
7229
  }
6633
- const localFromMeta = meta.local || metaJson.local;
6634
7230
  if (localFromMeta) {
6635
- if (!item.localType) {
6636
- item.localType = localFromMeta;
6637
- }
6638
7231
  pkgV.local = item.localType;
6639
7232
  item.fullPath = pkgV.dir = pkgV.dist.fullPath;
6640
7233
  pkgV.str = meta.jsonStr;
@@ -7108,9 +7701,9 @@ ${item.depPath.join(" > ")}`
7108
7701
  throw new Error(failMetaMsg(item.name));
7109
7702
  }
7110
7703
  const updated = this._fyn.depLocker.update(item, meta);
7111
- const r2 = this._resolveWithMeta({ item, meta: updated, force: false, noLocal: true });
7112
- if (r2) {
7113
- return r2;
7704
+ const resolved = this._resolveWithMeta({ item, meta: updated, force: false, noLocal: true });
7705
+ if (resolved) {
7706
+ return resolved;
7114
7707
  }
7115
7708
  logger.debug(
7116
7709
  `cached meta for ${item.name} has no version satisfying ${item.semver}; refetching from registry`
@@ -7128,7 +7721,7 @@ ${item.depPath.join(" > ")}`
7128
7721
  });
7129
7722
  });
7130
7723
  }).catch((err) => {
7131
- if (item.dsrc !== "opt") {
7724
+ if (!item.dsrc || !item.dsrc.includes("opt")) {
7132
7725
  if (err.message.includes("Unable to retrieve meta")) {
7133
7726
  throw err;
7134
7727
  } else {
@@ -7186,7 +7779,13 @@ class PkgDistExtractor {
7186
7779
  });
7187
7780
  this._fyn = options.fyn;
7188
7781
  this._promiseQ.on("done", (x) => this.done(x));
7189
- this._promiseQ.on("failItem", (x) => logger.error("dist extractor failed item", x.error));
7782
+ this._promiseQ.on("failItem", (x) => {
7783
+ logger.error("dist extractor failed item", x.error);
7784
+ const listener = _.get(x, "item.listener");
7785
+ if (listener) {
7786
+ setTimeout(() => listener.emit("fail", x.error), 0);
7787
+ }
7788
+ });
7190
7789
  }
7191
7790
  addPkgDist(data) {
7192
7791
  this._promiseQ.addItem(data);
@@ -7244,6 +7843,10 @@ class PkgDistExtractor {
7244
7843
  } else {
7245
7844
  const json = await this._fyn.ensureProperPkgDir(pkg, fullOutDir);
7246
7845
  if (json) {
7846
+ if (data.listener) {
7847
+ const listener = data.listener;
7848
+ setTimeout(() => listener.emit("done", json), 0);
7849
+ }
7247
7850
  return json;
7248
7851
  }
7249
7852
  await this._fyn.createPkgOutDir(fullOutDir);
@@ -7451,6 +8054,7 @@ class PkgDistFetcher {
7451
8054
  }
7452
8055
  /**
7453
8056
  * Check if pkg already has a copy extracted to node_modules
8057
+ *
7454
8058
  * @param {*} pkg - package info
7455
8059
  * @returns {*} pkg in FV_DIR and its package.json
7456
8060
  */
@@ -7533,8 +8137,14 @@ const logger = __webpack_require__("./lib/logger.ts");
7533
8137
  const logFormat = __webpack_require__("./lib/util/log-format.ts");
7534
8138
  const fynTil = __webpack_require__("./lib/util/fyntil.ts");
7535
8139
  const hardLinkDir = __webpack_require__("./lib/util/hard-link-dir.ts");
8140
+ const {
8141
+ makeLocalExportsManifest,
8142
+ reconcileLocalExports,
8143
+ resolveLocalExportsConfig
8144
+ } = __webpack_require__("./lib/local-exports.ts");
7536
8145
  const { INSTALL_PACKAGE } = __webpack_require__("./lib/log-items.ts");
7537
8146
  const { runNpmScript } = __webpack_require__("./lib/util/run-npm-script.ts");
8147
+ const { evaluateScriptPolicy, isScriptAllowed } = __webpack_require__("./lib/util/lifecycle-script-policy.ts");
7538
8148
  const xaa = __webpack_require__("./lib/util/xaa.ts");
7539
8149
  const { AggregateError } = __webpack_require__("./node_modules/.f/_/@jchip/error/1.0.3/@jchip/error/dist/index.js");
7540
8150
  const { RESOLVE_ORDER, RSEMVERS, LOCK_RSEMVERS, SEMVER } = __webpack_require__("./lib/symbols.ts");
@@ -7616,7 +8226,7 @@ class PkgInstaller {
7616
8226
  } catch (err) {
7617
8227
  if (err.code === "EPERM") {
7618
8228
  const st = await Fs.stat(pkgJsonFp);
7619
- await Fs.chmod(pkgJsonFp, st.mode + 384);
8229
+ await Fs.chmod(pkgJsonFp, st.mode | 384);
7620
8230
  await Fs.writeFile(pkgJsonFp, `${outputStr}
7621
8231
  `);
7622
8232
  }
@@ -7688,7 +8298,7 @@ class PkgInstaller {
7688
8298
  if (depInfo._removing) return;
7689
8299
  depInfo._removing = true;
7690
8300
  const optReqs = depInfo.requests.map((req) => {
7691
- return req.reverse().find((r) => r.startsWith("opt"));
8301
+ return req.slice().reverse().find((r) => r.startsWith("opt"));
7692
8302
  });
7693
8303
  const failedId = `${depInfo.name}@${depInfo.version}`;
7694
8304
  for (const r of optReqs) {
@@ -7811,12 +8421,27 @@ class PkgInstaller {
7811
8421
  if (this._fyn.showDeprecated && _.isEmpty(warned)) {
7812
8422
  logger.info(chalk.green("HOORAY!!! None of your dependencies are marked deprecated."));
7813
8423
  }
7814
- }).then(() => this._saveLockData()).then(() => {
8424
+ }).then(() => this._installLocalExports()).then(() => this._saveLockData()).then(() => {
7815
8425
  logger.info(`${chalk.green("done install")} ${logFormat.time(Date.now() - start)}`);
7816
8426
  }).finally(() => {
7817
8427
  logger.removeItem(INSTALL_PACKAGE);
7818
8428
  });
7819
8429
  }
8430
+ async _installLocalExports() {
8431
+ const pkgsData = this._data.getPkgsData();
8432
+ const config = resolveLocalExportsConfig(this._fyn._pkg);
8433
+ const manifest = await makeLocalExportsManifest({
8434
+ cwd: this._fyn._cwd,
8435
+ config,
8436
+ depInfos: this.toLink.filter((depInfo) => {
8437
+ const versions = pkgsData[depInfo.name];
8438
+ return versions && versions[depInfo.version] === depInfo;
8439
+ })
8440
+ });
8441
+ const previous = this._fyn._installConfig.localExports;
8442
+ await reconcileLocalExports({ cwd: this._fyn._cwd, manifest, previous });
8443
+ this._fyn.setLocalExports(manifest);
8444
+ }
7820
8445
  async _buildLocalPkg(depInfo) {
7821
8446
  if (this._fyn._options.buildLocal && this._fyn._localPkgBuilder) {
7822
8447
  const itemRes = await this._fyn._localPkgBuilder.waitForItem(depInfo.dir);
@@ -7848,25 +8473,60 @@ class PkgInstaller {
7848
8473
  }
7849
8474
  json._fyn = {};
7850
8475
  const scripts = json.scripts || {};
8476
+ const scriptPolicy = evaluateScriptPolicy(depInfo, this._fyn.allowScripts, {
8477
+ allowTopLevel: this._fyn.allowTopLevelScripts
8478
+ });
8479
+ const blockedScripts = [];
8480
+ const isAllowed = (scriptName) => {
8481
+ if (isScriptAllowed(scriptPolicy, scriptName)) {
8482
+ return true;
8483
+ }
8484
+ blockedScripts.push(scriptName);
8485
+ return false;
8486
+ };
7851
8487
  const hasPI = json.hasPI || Boolean(scripts.preinstall);
7852
8488
  const piExed = Boolean(depInfo.preinstall);
7853
8489
  if (!piExed && hasPI) {
7854
8490
  if (depInfo.preInstalled) {
7855
8491
  json._fyn.preinstall = true;
7856
- } else {
8492
+ } else if (isAllowed("preinstall")) {
7857
8493
  logger.debug("adding preinstall step for", depInfo.dir);
7858
8494
  this.preInstall.push(depInfo);
7859
8495
  }
7860
8496
  }
7861
8497
  this.toLink.push(depInfo);
7862
8498
  const install = ["install", "postinstall"].filter((x) => {
7863
- return Boolean(scripts[x]) && !json._fyn[x];
8499
+ return Boolean(scripts[x]) && !json._fyn[x] && isAllowed(x);
7864
8500
  });
7865
8501
  if (install.length > 0) {
7866
8502
  logger.debug("adding install step for", depInfo.dir, install);
7867
8503
  depInfo.install = install;
7868
8504
  this.postInstall.push(depInfo);
7869
8505
  }
8506
+ if (blockedScripts.length > 0) {
8507
+ this._warnBlockedScripts(depInfo, scriptPolicy, blockedScripts);
8508
+ }
8509
+ }
8510
+ _warnBlockedScripts(depInfo, policy, blocked) {
8511
+ const id = logFormat.pkgId(depInfo);
8512
+ logger.warn(
8513
+ `${chalk.black.bgYellow("WARN")} ${chalk.magenta("scripts blocked")} ${id}`,
8514
+ chalk.yellow(
8515
+ `skipped lifecycle [${blocked.join(", ")}] for non-registry (${policy.urlType}) package - not in fyn.allowScripts`
8516
+ )
8517
+ );
8518
+ logger.verbose(
8519
+ chalk.blue(" To allow, add to package.json:"),
8520
+ chalk.cyan(
8521
+ `"fyn": { "allowScripts": { "${policy.key}": [${blocked.map((s) => `"${s}"`).join(", ")}] } }`
8522
+ )
8523
+ );
8524
+ if (policy.topLevel) {
8525
+ logger.verbose(
8526
+ chalk.blue(" Or trust all direct deps' scripts with:"),
8527
+ chalk.cyan(`"fyn": { "allowTopLevelScripts": true }`)
8528
+ );
8529
+ }
7870
8530
  }
7871
8531
  _cleanBin() {
7872
8532
  logger.updateItem(INSTALL_PACKAGE, "cleaning node_modules/.bin");
@@ -8068,6 +8728,8 @@ const PkgDepLinker = __webpack_require__("./lib/pkg-dep-linker.ts");
8068
8728
  const semverUtil = __webpack_require__("./lib/util/semver.ts");
8069
8729
  const { readPkgJson } = __webpack_require__("./lib/util/fyntil.ts");
8070
8730
  const { OPTIONAL_RESOLVER } = __webpack_require__("./lib/log-items.ts");
8731
+ const { DEP_ITEM, SEMVER } = __webpack_require__("./lib/symbols.ts");
8732
+ const { evaluateScriptPolicy, isScriptAllowed } = __webpack_require__("./lib/util/lifecycle-script-policy.ts");
8071
8733
  xsh.Promise = Promise;
8072
8734
  class PkgOptResolver {
8073
8735
  constructor(options) {
@@ -8130,6 +8792,32 @@ class PkgOptResolver {
8130
8792
  // - 0: add item back to resolve
8131
8793
  // - not: add item to queue for logging at end
8132
8794
  //
8795
+ /**
8796
+ * Evaluate the lifecycle-script policy for an optional dep's preinstall.
8797
+ *
8798
+ * Non-registry (github/git/url) optional deps do not run preinstall unless
8799
+ * whitelisted via `fyn.allowScripts` / `fyn.allowTopLevelScripts` - the same
8800
+ * deny-by-default policy the regular installer enforces (FPM-41). Registry
8801
+ * and local deps are trusted and always allowed.
8802
+ *
8803
+ * @param {object} item the optional dep's DepItem
8804
+ * @param {string} name package name
8805
+ * @param {string} version resolved version
8806
+ * @returns {{allowed:boolean, policy:object}} the policy decision
8807
+ */
8808
+ checkPreinstallPolicy(item, name, version) {
8809
+ const optDepInfo = {
8810
+ [DEP_ITEM]: item,
8811
+ [SEMVER]: item.semver,
8812
+ name,
8813
+ version,
8814
+ top: !_.get(item, ["parent", "depth"])
8815
+ };
8816
+ const policy = evaluateScriptPolicy(optDepInfo, this._fyn.allowScripts, {
8817
+ allowTopLevel: this._fyn.allowTopLevelScripts
8818
+ });
8819
+ return { allowed: isScriptAllowed(policy, "preinstall"), policy };
8820
+ }
8133
8821
  /* eslint-disable max-statements */
8134
8822
  optCheck(data) {
8135
8823
  const name = data.item.name;
@@ -8248,6 +8936,17 @@ class PkgOptResolver {
8248
8936
  );
8249
8937
  return { passed: true };
8250
8938
  } else if (_.get(res, "pkg.scripts.preinstall")) {
8939
+ const { allowed, policy } = this.checkPreinstallPolicy(data.item, name, version);
8940
+ if (!allowed) {
8941
+ logger.warn(
8942
+ `${chalk.black.bgYellow("WARN")} ${chalk.magenta("scripts blocked")} ${displayId}`,
8943
+ chalk.yellow(
8944
+ `skipped optional preinstall for non-registry (${policy.urlType}) package - not in fyn.allowScripts`
8945
+ )
8946
+ );
8947
+ logPass("preinstall blocked by script policy - keeping optional package");
8948
+ return { passed: true };
8949
+ }
8251
8950
  data.runningScript = true;
8252
8951
  logger.updateItem(OPTIONAL_RESOLVER, `running preinstall for ${displayId}`);
8253
8952
  const ls = new LifecycleScripts({
@@ -8346,12 +9045,19 @@ const { PackageRef } = __webpack_require__("./node_modules/.f/_/@fynpo/base/1.1.
8346
9045
  const Arborist = __webpack_require__("./node_modules/.f/_/@npmcli/arborist/9.1.6/@npmcli/arborist/lib/index.js");
8347
9046
  const WATCH_TIME = 5e3;
8348
9047
  const META_CACHE_STALE_TIME = 24 * 60 * 60 * 1e3;
9048
+ const SAFE_GIT_TOKEN = /^[A-Za-z0-9._/~^-]+$/;
9049
+ function isSafeGitToken(s) {
9050
+ return typeof s === "string" && s.length > 0 && s.length < 256 && SAFE_GIT_TOKEN.test(s) && !s.startsWith("-");
9051
+ }
9052
+ function isPinnedGitCommit(semver) {
9053
+ if (!semver) return false;
9054
+ const committish = semver.includes("#") ? semver.split("#")[1] : semver;
9055
+ return /^[a-f0-9]{40}$/.test(committish);
9056
+ }
8349
9057
  async function checkGitRepoHasNewCommits(gitUrl, ref, cachedCommitHash) {
8350
9058
  if (!cachedCommitHash) return true;
8351
9059
  try {
8352
- const { execSync } = __webpack_require__("child_process");
8353
- const Path2 = __webpack_require__("path");
8354
- const fs2 = __webpack_require__("fs");
9060
+ const { execFileSync } = __webpack_require__("child_process");
8355
9061
  let actualGitUrl = gitUrl;
8356
9062
  let isLocalRepo = false;
8357
9063
  let localRepoPath = null;
@@ -8363,10 +9069,15 @@ async function checkGitRepoHasNewCommits(gitUrl, ref, cachedCommitHash) {
8363
9069
  }
8364
9070
  } else if (!gitUrl.includes("://")) {
8365
9071
  isLocalRepo = true;
8366
- localRepoPath = Path2.resolve(gitUrl);
9072
+ localRepoPath = Path.resolve(gitUrl);
8367
9073
  }
8368
9074
  if (isLocalRepo && localRepoPath) {
8369
- const output = execSync(`git rev-parse ${ref || "HEAD"}`, {
9075
+ const safeRef = ref || "HEAD";
9076
+ if (!isSafeGitToken(safeRef)) {
9077
+ logger.debug(`refusing potentially unsafe git ref: ${safeRef}`);
9078
+ return null;
9079
+ }
9080
+ const output = execFileSync("git", ["rev-parse", safeRef], {
8370
9081
  cwd: localRepoPath,
8371
9082
  stdio: "pipe",
8372
9083
  encoding: "utf8",
@@ -8389,7 +9100,12 @@ async function checkGitRepoHasNewCommits(gitUrl, ref, cachedCommitHash) {
8389
9100
  } else {
8390
9101
  actualGitUrl = gitUrl;
8391
9102
  }
8392
- const output = execSync(`git ls-remote ${actualGitUrl} ${ref || "HEAD"}`, {
9103
+ const safeRef = ref || "HEAD";
9104
+ if (!isSafeGitToken(safeRef) || actualGitUrl.startsWith("-")) {
9105
+ logger.debug(`refusing potentially unsafe git url/ref: ${actualGitUrl}#${safeRef}`);
9106
+ return null;
9107
+ }
9108
+ const output = execFileSync("git", ["ls-remote", actualGitUrl, safeRef], {
8393
9109
  stdio: "pipe",
8394
9110
  encoding: "utf8",
8395
9111
  timeout: 1e4
@@ -8626,16 +9342,17 @@ class PkgSrcManager {
8626
9342
  logger.debug(`pacote.packument ${qItem.packumentUrl}`);
8627
9343
  const promise2 = pacote.packument(
8628
9344
  pkgName,
9345
+ // pacote 21 / npm-registry-fetch 19 read camelCase options; the old
9346
+ // kebab-case names were silently ignored. preferOnline forces a server
9347
+ // revalidation (cache mode "no-cache") for this refresh path.
8629
9348
  this.getPacoteOpts({
8630
- "full-metadata": true,
8631
- "fetch-retries": 3,
8632
- "cache-policy": "ignore",
8633
- "cache-key": qItem.cacheKey,
9349
+ fullMetadata: true,
9350
+ fetchRetries: 3,
9351
+ preferOnline: true,
8634
9352
  memoize: false
8635
9353
  })
8636
9354
  );
8637
9355
  return promise2.then((x) => {
8638
- this._metaStat.inTx--;
8639
9356
  if (!x) {
8640
9357
  const msg = `pacote returned null/undefined for packument of ${pkgName}`;
8641
9358
  logger.error(chalk.yellow(msg));
@@ -8656,6 +9373,7 @@ class PkgSrcManager {
8656
9373
  this.updateFetchMetaStatus(false);
8657
9374
  const promise = qItem.item.urlType ? this.fetchUrlSemverMeta(qItem.item) : pacoteRequest();
8658
9375
  return promise.then((x) => {
9376
+ this._metaStat.inTx--;
8659
9377
  const time = Date.now() - startTime;
8660
9378
  if (time > 20 * 1e3) {
8661
9379
  logger.info(
@@ -8670,10 +9388,11 @@ class PkgSrcManager {
8670
9388
  qItem.defer.reject(new AggregateError([new Error(msg)], msg));
8671
9389
  return;
8672
9390
  }
8673
- refreshCacheEntry(this._fyn._fynCacheDir, qItem.cacheKey).catch(() => {
9391
+ refreshCacheEntry(this._cacheDir, qItem.cacheKey).catch(() => {
8674
9392
  });
8675
9393
  qItem.defer.resolve(x);
8676
9394
  }).catch((err) => {
9395
+ this._metaStat.inTx--;
8677
9396
  qItem.defer.reject(err);
8678
9397
  });
8679
9398
  }
@@ -8715,19 +9434,7 @@ class PkgSrcManager {
8715
9434
  } else {
8716
9435
  dirPacker = this._getPacoteDirPacker();
8717
9436
  }
8718
- let pacoteOpts = { dirPacker };
8719
- if (item.urlType.startsWith("git") && item.semver && !item.semver.match(/^[a-f0-9]{40}$/)) {
8720
- const potentialCacheKeys = [
8721
- // Try common cache key patterns based on semver
8722
- `fyn-tarball-for-git+https://github.com/${item.semver.replace(/^github:/, "").split("#")[0]}.git#`,
8723
- `fyn-tarball-for-git+ssh://git@github.com/${item.semver.replace(/^github:/, "").split("#")[0]}.git#`
8724
- ];
8725
- for (const baseKey of potentialCacheKeys) {
8726
- try {
8727
- } catch (e) {
8728
- }
8729
- }
8730
- }
9437
+ const pacoteOpts = { dirPacker };
8731
9438
  return pacote.manifest(`${item.name}@${item.semver}`, this.getPacoteOpts(pacoteOpts)).then((manifest) => {
8732
9439
  manifest = Object.assign({}, manifest);
8733
9440
  return {
@@ -8751,9 +9458,10 @@ class PkgSrcManager {
8751
9458
  let integrity;
8752
9459
  let shouldRefresh = false;
8753
9460
  if (tgzCacheInfo) {
8754
- const cachedResolved = tgzCacheInfo.metadata?._resolved || (tgzCacheInfo.metadata?.dist?.tarball?.match(/MARK_URL_SPEC(.+)/)?.[1] ? JSON.parse(tgzCacheInfo.metadata.dist.tarball.match(/MARK_URL_SPEC(.+)/)[1])._resolved : null);
9461
+ const cachedTarball = tgzCacheInfo.metadata?.dist?.tarball;
9462
+ const cachedResolved = tgzCacheInfo.metadata?._resolved || (cachedTarball?.startsWith(MARK_URL_SPEC) ? JSON.parse(cachedTarball.slice(MARK_URL_SPEC.length))._resolved : null);
8755
9463
  const cachedCommitHash = cachedResolved?.match(/#([a-f0-9]{40})$/)?.[1];
8756
- if (!shouldRefresh && cachedCommitHash && item.semver && !item.semver.match(/^[a-f0-9]{40}$/)) {
9464
+ if (!shouldRefresh && cachedCommitHash && item.semver && !isPinnedGitCommit(item.semver)) {
8757
9465
  let gitUrl = item.semver;
8758
9466
  let ref = "HEAD";
8759
9467
  if (gitUrl.includes("#")) {
@@ -8776,17 +9484,15 @@ class PkgSrcManager {
8776
9484
  logger.debug(
8777
9485
  `git cache for '${item.name}' has new commits (cached: ${cachedCommitHash.substring(0, 8)}, checking ${ref}), forcing refresh`
8778
9486
  );
8779
- } else {
8780
- if (tgzCacheInfo.refreshTime) {
8781
- const stale = Date.now() - tgzCacheInfo.refreshTime;
8782
- const staleByTime = stale >= META_CACHE_STALE_TIME;
8783
- if (staleByTime) {
8784
- shouldRefresh = true;
8785
- const reason = hasNewCommits === null ? "ls-remote failed" : "no new commits";
8786
- logger.debug(
8787
- `git cache for '${item.name}' (${reason}), cache is stale by time (${(stale / 1e3 / 60 / 60).toFixed(1)}h old), forcing refresh`
8788
- );
8789
- }
9487
+ } else if (tgzCacheInfo.refreshTime) {
9488
+ const stale = Date.now() - tgzCacheInfo.refreshTime;
9489
+ const staleByTime = stale >= META_CACHE_STALE_TIME;
9490
+ if (staleByTime) {
9491
+ shouldRefresh = true;
9492
+ const reason = hasNewCommits === null ? "ls-remote failed" : "no new commits";
9493
+ logger.debug(
9494
+ `git cache for '${item.name}' (${reason}), cache is stale by time (${(stale / 1e3 / 60 / 60).toFixed(1)}h old), forcing refresh`
9495
+ );
8790
9496
  }
8791
9497
  }
8792
9498
  } else if (!shouldRefresh && tgzCacheInfo.refreshTime) {
@@ -8860,6 +9566,7 @@ class PkgSrcManager {
8860
9566
  const cacheKey = `make-fetch-happen:request-cache:${packumentUrl}`;
8861
9567
  const legacyCacheKey = `make-fetch-happen:request-cache:full:${packumentUrl}`;
8862
9568
  const cacheKeys = [cacheKey, legacyCacheKey];
9569
+ let cacheMemoized = false;
8863
9570
  const loadCachedPackument = async (key, memoize = true) => {
8864
9571
  try {
8865
9572
  const cached = await cacache.get(this._cacheDir, key, { memoize });
@@ -8882,6 +9589,33 @@ class PkgSrcManager {
8882
9589
  }
8883
9590
  return _.maxBy(cachedEntries, (cached) => cached.refreshTime || 0) || cachedEntries[0];
8884
9591
  };
9592
+ const loadPreparedUrlMeta = async () => {
9593
+ const info = await getCacheInfoWithRefreshTime(
9594
+ this._cacheDir,
9595
+ `fyn-tarball-for-${item.semver}`
9596
+ );
9597
+ const cachedManifest = info && info.metadata;
9598
+ if (!(cachedManifest && cachedManifest.version)) {
9599
+ return void 0;
9600
+ }
9601
+ const tarball = JSON.stringify(
9602
+ Object.assign(
9603
+ _.pick(item, ["urlType", "semver"]),
9604
+ _.pick(cachedManifest, ["_resolved", "_id"])
9605
+ )
9606
+ );
9607
+ const manifest = Object.assign({}, cachedManifest, {
9608
+ dist: {
9609
+ integrity: info.integrity,
9610
+ tarball: `${MARK_URL_SPEC}${tarball}`
9611
+ }
9612
+ });
9613
+ return {
9614
+ name: item.name,
9615
+ versions: { [manifest.version]: manifest },
9616
+ urlVersions: { [item.semver]: manifest }
9617
+ };
9618
+ };
8885
9619
  const readMemoizedPackument = async () => {
8886
9620
  const memoized = await loadBestCachedPackument(false);
8887
9621
  if (!(memoized && memoized.data)) {
@@ -8892,7 +9626,11 @@ class PkgSrcManager {
8892
9626
  this._metaStat.wait--;
8893
9627
  return JSON.parse(memoized.data.toString());
8894
9628
  };
9629
+ let fetchAttempted = false;
9630
+ let foundCache;
9631
+ let foundPackument;
8895
9632
  const queueMetaFetchRequest = (cached) => {
9633
+ fetchAttempted = true;
8896
9634
  const offline = this._fyn.remoteMetaDisabled;
8897
9635
  if (cached && this._fyn.forceCache) {
8898
9636
  this._metaStat.wait--;
@@ -8917,18 +9655,24 @@ class PkgSrcManager {
8917
9655
  return netQItem.defer.promise;
8918
9656
  };
8919
9657
  this._metaStat.wait++;
8920
- let foundCache;
8921
- let cacheMemoized = false;
8922
9658
  const metaMemoizeUrl = this._fyn._options.metaMemoize;
8923
- const promise = (item.urlType || forceRefresh ? (
8924
- // when the semver is a url then the meta is not from npm registry and
8925
- // we can't use the cache for registry.
8926
- // when forceRefresh, skip the local cacache + meta-mem path so we go
8927
- // straight to a fresh registry fetch via queueMetaFetchRequest.
8928
- Promise.resolve()
8929
- ) : loadBestCachedPackument(true)).then((cached) => {
8930
- const packument = cached && cached.data && JSON.parse(cached.data);
9659
+ let cacheLookup;
9660
+ if (item.urlType) {
9661
+ cacheLookup = this._fyn.remoteMetaDisabled ? loadPreparedUrlMeta().then((preparedUrlMeta) => ({ preparedUrlMeta })) : Promise.resolve();
9662
+ } else if (forceRefresh) {
9663
+ cacheLookup = Promise.resolve();
9664
+ } else {
9665
+ cacheLookup = loadBestCachedPackument(true);
9666
+ }
9667
+ const promise = cacheLookup.then((cached) => {
9668
+ if (cached && cached.preparedUrlMeta) {
9669
+ cacheMemoized = true;
9670
+ this._metaStat.wait--;
9671
+ return cached.preparedUrlMeta;
9672
+ }
8931
9673
  foundCache = cached;
9674
+ const packument = cached && cached.data && JSON.parse(cached.data);
9675
+ foundPackument = packument;
8932
9676
  if (cached && cached.refreshTime) {
8933
9677
  const stale = Date.now() - cached.refreshTime;
8934
9678
  const since = (stale / 1e3).toFixed(2);
@@ -8971,10 +9715,19 @@ class PkgSrcManager {
8971
9715
  return queueMetaFetchRequest(packument);
8972
9716
  }
8973
9717
  }).catch((err) => {
9718
+ if (fetchAttempted) {
9719
+ if (foundPackument) {
9720
+ cacheMemoized = true;
9721
+ logger.warn(
9722
+ `failed to refresh packument for '${pkgName}', using stale cached metadata: ${err.message}`
9723
+ );
9724
+ return foundPackument;
9725
+ }
9726
+ throw err;
9727
+ }
8974
9728
  if (foundCache) {
8975
9729
  const data = foundCache.data && foundCache.data.toString();
8976
9730
  logger.debug(`fail to process packument cache - ${err.message}; data ${data}`);
8977
- throw err;
8978
9731
  }
8979
9732
  return queueMetaFetchRequest();
8980
9733
  }).then((meta) => {
@@ -9038,7 +9791,7 @@ class PkgSrcManager {
9038
9791
  return passthrough2;
9039
9792
  }
9040
9793
  const opts = this.getPacoteOpts({
9041
- fullMeta: true,
9794
+ fullMetadata: true,
9042
9795
  integrity,
9043
9796
  resolved: tarballUrl
9044
9797
  });
@@ -9155,6 +9908,7 @@ class PkgSrcManager {
9155
9908
  }
9156
9909
  module.exports = PkgSrcManager;
9157
9910
  module.exports.META_CACHE_STALE_TIME = META_CACHE_STALE_TIME;
9911
+ module.exports.isPinnedGitCommit = isPinnedGitCommit;
9158
9912
 
9159
9913
 
9160
9914
  /***/ }),
@@ -9267,7 +10021,8 @@ async function checkPkgNeedInstall(dir, checkCtime = 0) {
9267
10021
  hasScript
9268
10022
  };
9269
10023
  } catch (error) {
9270
- return { install: false, error };
10024
+ logger.warn(`unable to determine if local package at ${dir} needs install: ${error.message}`);
10025
+ throw error;
9271
10026
  }
9272
10027
  }
9273
10028
  exports.checkPkgNeedInstall = checkPkgNeedInstall;
@@ -9402,6 +10157,67 @@ const fyntil = {
9402
10157
  return fyntil.fynpoConfig = {};
9403
10158
  }
9404
10159
  },
10160
+ /**
10161
+ * Detect if `dir` lives inside a git linked worktree and, if so, resolve the
10162
+ * equivalent directory in the repo's main worktree.
10163
+ *
10164
+ * fynpo uses `<monorepo>/.fynpo/_store` as the central package store. When the
10165
+ * monorepo is checked out as a git linked worktree, each worktree would
10166
+ * otherwise get its own `.fynpo` store - wasteful and slow. Pointing them at
10167
+ * the main worktree's store lets all worktrees share one central store.
10168
+ *
10169
+ * @param dir directory to resolve (typically the fynpo monorepo top dir)
10170
+ * @returns the equivalent directory in the main worktree, or `dir` unchanged
10171
+ * when it's not in a git repo or already in the main worktree.
10172
+ */
10173
+ async resolveGitMainWorktreeDir(dir) {
10174
+ const startDir = Path.resolve(dir);
10175
+ let treeTop = startDir;
10176
+ let gitPath;
10177
+ let gitStat;
10178
+ for (; ; ) {
10179
+ try {
10180
+ const p = Path.join(treeTop, ".git");
10181
+ gitStat = await Fs.stat(p);
10182
+ gitPath = p;
10183
+ break;
10184
+ } catch (err) {
10185
+ if (err.code !== "ENOENT") {
10186
+ throw err;
10187
+ }
10188
+ }
10189
+ const parent = Path.dirname(treeTop);
10190
+ if (parent === treeTop) {
10191
+ break;
10192
+ }
10193
+ treeTop = parent;
10194
+ }
10195
+ if (!gitPath || gitStat.isDirectory()) {
10196
+ return dir;
10197
+ }
10198
+ try {
10199
+ const gitFile = await Fs.readFile(gitPath, "utf8");
10200
+ const m = gitFile.match(/^gitdir:\s*(.+)$/m);
10201
+ if (!m) {
10202
+ return dir;
10203
+ }
10204
+ let worktreeGitDir = m[1].trim();
10205
+ if (!Path.isAbsolute(worktreeGitDir)) {
10206
+ worktreeGitDir = Path.resolve(treeTop, worktreeGitDir);
10207
+ }
10208
+ const commondir = (await Fs.readFile(Path.join(worktreeGitDir, "commondir"), "utf8")).trim();
10209
+ const commonGitDir = Path.isAbsolute(commondir) ? commondir : Path.resolve(worktreeGitDir, commondir);
10210
+ if (Path.basename(commonGitDir) !== ".git") {
10211
+ return dir;
10212
+ }
10213
+ const mainTreeTop = Path.dirname(commonGitDir);
10214
+ const rel = Path.relative(treeTop, startDir);
10215
+ return Path.join(mainTreeTop, rel);
10216
+ } catch (err) {
10217
+ logger.debug(`resolveGitMainWorktreeDir failed for ${dir}: ${err.message}`);
10218
+ return dir;
10219
+ }
10220
+ },
9405
10221
  removeAuthInfo(rcObj) {
9406
10222
  const rmObj = {};
9407
10223
  for (const key in rcObj) {
@@ -9413,7 +10229,10 @@ const fyntil = {
9413
10229
  return rmObj;
9414
10230
  },
9415
10231
  exit(err) {
9416
- process.exit(err ? 1 : 0);
10232
+ if (typeof err === "number") {
10233
+ return process.exit(err);
10234
+ }
10235
+ return process.exit(err ? 1 : 0);
9417
10236
  },
9418
10237
  async readJson(file, defaultData) {
9419
10238
  try {
@@ -9807,7 +10626,7 @@ async function linkPackTree({ tree, src, dest, sym1, sourceMaps }) {
9807
10626
  const files = tree[SYM_FILES];
9808
10627
  const destFiles = await prepDestDir(dest);
9809
10628
  for (const file of files) {
9810
- if (!ci.isCI && file.match(/.+\..+\.map$/)) {
10629
+ if (!ci.isCI && file.match(/\.(js|mjs)\.map$/)) {
9811
10630
  continue;
9812
10631
  }
9813
10632
  destFiles[file] = true;
@@ -9868,6 +10687,133 @@ module.exports = {
9868
10687
  module.exports = __webpack_require__("./node_modules/.f/_/item-queue/1.1.2/item-queue/lib/inflight.js");
9869
10688
 
9870
10689
 
10690
+ /***/ }),
10691
+
10692
+ /***/ "./lib/util/lifecycle-script-policy.ts":
10693
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
10694
+
10695
+ "use strict";
10696
+
10697
+ const semverUtil = __webpack_require__("./lib/util/semver.ts");
10698
+ const { DEP_ITEM, SEMVER } = __webpack_require__("./lib/symbols.ts");
10699
+ const TRUSTED_URL_TYPES = /* @__PURE__ */ new Set(["npm"]);
10700
+ function getSourceUrlType(depItem) {
10701
+ let item = depItem;
10702
+ while (item) {
10703
+ const analyzed = item.semver ? semverUtil.analyze(item.semver) : {};
10704
+ const urlType = item.urlType || analyzed.urlType;
10705
+ if (urlType) {
10706
+ return urlType;
10707
+ }
10708
+ if (!(item.localType || analyzed.localType)) {
10709
+ return void 0;
10710
+ }
10711
+ item = item.parent;
10712
+ }
10713
+ return void 0;
10714
+ }
10715
+ function getUrlType(depInfo) {
10716
+ const depItem = depInfo[DEP_ITEM];
10717
+ if (depItem) {
10718
+ return getSourceUrlType(depItem);
10719
+ }
10720
+ const spec = depInfo[SEMVER];
10721
+ if (spec) {
10722
+ return semverUtil.analyze(spec).urlType;
10723
+ }
10724
+ return void 0;
10725
+ }
10726
+ function isTrustedScriptSource(depInfo) {
10727
+ const urlType = getUrlType(depInfo);
10728
+ return !urlType || TRUSTED_URL_TYPES.has(urlType);
10729
+ }
10730
+ function makeAllowKeys(depInfo) {
10731
+ const depItem = depInfo[DEP_ITEM];
10732
+ const spec = depItem && depItem.semver || depInfo[SEMVER];
10733
+ const keys = [];
10734
+ if (spec) {
10735
+ keys.push(`${depInfo.name}@${spec}`);
10736
+ }
10737
+ if (depInfo.version && depInfo.version !== spec) {
10738
+ keys.push(`${depInfo.name}@${depInfo.version}`);
10739
+ }
10740
+ return keys;
10741
+ }
10742
+ function normalizeAllowEntry(value, acc) {
10743
+ if (value === true || value === "*") {
10744
+ acc.allowAll = true;
10745
+ return acc;
10746
+ }
10747
+ let list = [];
10748
+ if (Array.isArray(value)) {
10749
+ list = value;
10750
+ } else if (value !== void 0) {
10751
+ list = [value];
10752
+ }
10753
+ for (const s of list) {
10754
+ if (s === true || s === "*") {
10755
+ acc.allowAll = true;
10756
+ } else if (typeof s === "string") {
10757
+ acc.scripts.add(s.toLowerCase());
10758
+ }
10759
+ }
10760
+ return acc;
10761
+ }
10762
+ function foldAllowScripts(keys, allowScripts, acc) {
10763
+ let matchedKey;
10764
+ for (const key of keys) {
10765
+ const value = allowScripts && allowScripts[key];
10766
+ if (value !== void 0) {
10767
+ if (!matchedKey) matchedKey = key;
10768
+ normalizeAllowEntry(value, acc);
10769
+ }
10770
+ }
10771
+ return matchedKey;
10772
+ }
10773
+ function isTopLevelDep(depInfo) {
10774
+ return Boolean(depInfo && depInfo.top);
10775
+ }
10776
+ function evaluateScriptPolicy(depInfo, allowScripts, options = {}) {
10777
+ const urlType = getUrlType(depInfo);
10778
+ const keys = makeAllowKeys(depInfo);
10779
+ const topLevel = isTopLevelDep(depInfo);
10780
+ if (!urlType || TRUSTED_URL_TYPES.has(urlType)) {
10781
+ return { trusted: true, urlType, allowAll: true, allowed: /* @__PURE__ */ new Set(), key: keys[0], topLevel };
10782
+ }
10783
+ const acc = { allowAll: false, scripts: /* @__PURE__ */ new Set() };
10784
+ const matchedKey = foldAllowScripts(keys, allowScripts, acc);
10785
+ const { allowTopLevel } = options;
10786
+ if (topLevel && allowTopLevel !== void 0 && allowTopLevel !== false) {
10787
+ normalizeAllowEntry(allowTopLevel, acc);
10788
+ }
10789
+ return {
10790
+ trusted: false,
10791
+ urlType,
10792
+ allowAll: acc.allowAll,
10793
+ allowed: acc.scripts,
10794
+ // key to suggest when warning - prefer a matched key, else the spec form
10795
+ key: matchedKey || keys[0],
10796
+ topLevel
10797
+ };
10798
+ }
10799
+ function isScriptAllowed(policy, scriptName) {
10800
+ if (policy.trusted || policy.allowAll) {
10801
+ return true;
10802
+ }
10803
+ return policy.allowed.has(String(scriptName).toLowerCase());
10804
+ }
10805
+ module.exports = {
10806
+ TRUSTED_URL_TYPES,
10807
+ getSourceUrlType,
10808
+ getUrlType,
10809
+ isTrustedScriptSource,
10810
+ isTopLevelDep,
10811
+ makeAllowKeys,
10812
+ evaluateScriptPolicy,
10813
+ isScriptAllowed
10814
+ };
10815
+
10816
+
9871
10817
  /***/ }),
9872
10818
 
9873
10819
  /***/ "./lib/util/log-format.ts":
@@ -9888,6 +10834,7 @@ module.exports = {
9888
10834
  * - the part before package name blue
9889
10835
  * - the package name part magenta
9890
10836
  * - remaining as is
10837
+ *
9891
10838
  * @param {*} name - package name
9892
10839
  * @param {*} path - path to highlight
9893
10840
  * @returns
@@ -9987,7 +10934,7 @@ function makeNpmEnv(data, opts, prefix, env) {
9987
10934
  if (minimalConfigKeys.includes(normalizedKey) || minimalConfigKeys.includes(key)) {
9988
10935
  const envKey = `npm_config_${normalizedKey}`;
9989
10936
  const val = opts.config[key];
9990
- if (val != null && typeof val !== "function") {
10937
+ if (val !== null && val !== void 0 && typeof val !== "function") {
9991
10938
  env[envKey] = String(val);
9992
10939
  }
9993
10940
  }
@@ -10054,6 +11001,61 @@ ItemQueue.Promise = __webpack_require__("./lib/util/aveazul.ts");
10054
11001
  module.exports = ItemQueue;
10055
11002
 
10056
11003
 
11004
+ /***/ }),
11005
+
11006
+ /***/ "./lib/util/registry-dep-policy.ts":
11007
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
11008
+
11009
+ "use strict";
11010
+
11011
+ const Semver = __webpack_require__("./node_modules/.f/_/semver/7.8.0/semver/index.js");
11012
+ const { TRUSTED_URL_TYPES, getSourceUrlType } = __webpack_require__("./lib/util/lifecycle-script-policy.ts");
11013
+ function isNonRegistryUrlType(urlType) {
11014
+ return Boolean(urlType) && !TRUSTED_URL_TYPES.has(urlType);
11015
+ }
11016
+ function isValidRegistrySpec(spec) {
11017
+ if (spec === void 0 || spec === null) {
11018
+ return false;
11019
+ }
11020
+ const s = String(spec).trim();
11021
+ if (s === "" || s === "*" || s === "x") {
11022
+ return true;
11023
+ }
11024
+ if (Semver.validRange(s) !== null) {
11025
+ return true;
11026
+ }
11027
+ if (Semver.coerce(s)) {
11028
+ return true;
11029
+ }
11030
+ return /^[a-zA-Z][\w.-]*$/.test(s);
11031
+ }
11032
+ function violatesRegistryPolicy(dep) {
11033
+ const urlType = dep.urlType;
11034
+ if (urlType) {
11035
+ if (TRUSTED_URL_TYPES.has(urlType)) {
11036
+ return null;
11037
+ }
11038
+ return { kind: "url", urlType };
11039
+ }
11040
+ if (dep.localType) {
11041
+ const sourceUrlType = getSourceUrlType(dep);
11042
+ if (isNonRegistryUrlType(sourceUrlType)) {
11043
+ return { kind: "local", urlType: sourceUrlType };
11044
+ }
11045
+ return null;
11046
+ }
11047
+ if (!isValidRegistrySpec(dep.semver)) {
11048
+ return { kind: "semver", semver: dep.semver };
11049
+ }
11050
+ return null;
11051
+ }
11052
+ module.exports = {
11053
+ isNonRegistryUrlType,
11054
+ isValidRegistrySpec,
11055
+ violatesRegistryPolicy
11056
+ };
11057
+
11058
+
10057
11059
  /***/ }),
10058
11060
 
10059
11061
  /***/ "./lib/util/run-npm-script.ts":
@@ -10160,11 +11162,14 @@ function simpleCompare(a, b) {
10160
11162
  }
10161
11163
  if (partsA[1]) {
10162
11164
  if (partsB[1]) {
11165
+ if (Semver.valid(a) && Semver.valid(b)) {
11166
+ return Semver.rcompare(a, b);
11167
+ }
10163
11168
  return partsA[1] > partsB[1] ? -1 : 1;
10164
11169
  }
10165
- return -1;
10166
- } else if (partsB[1]) {
10167
11170
  return 1;
11171
+ } else if (partsB[1]) {
11172
+ return -1;
10168
11173
  } else {
10169
11174
  return 0;
10170
11175
  }
@@ -10448,7 +11453,7 @@ async function _scanFileStats(dir, ignores, baseDir = "") {
10448
11453
  }
10449
11454
  function scanFileStats(dir, options = {}) {
10450
11455
  const ignores = [
10451
- `**/?(node_modules|.vscode|.DS_Store|coverage|.nyc_output|.fynpo|.git|.github|.gitignore)`,
11456
+ `**/?(node_modules|_fyn|.vscode|.DS_Store|coverage|.nyc_output|.fynpo|.git|.github|.gitignore)`,
10452
11457
  "**/*.?(log|md)"
10453
11458
  ].concat(options.ignores || `**/?(docs|docusaurus|packages|tmp|.etmp|samples|dist)`).concat(options.moreIgnores).filter((x) => x);
10454
11459
  return _scanFileStats(dir, ignores, "");