fyn 2.1.3 → 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 +1042 -156
  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);
@@ -3928,6 +3975,7 @@ class Fyn {
3928
3975
  }
3929
3976
  /**
3930
3977
  * Check user production mode option against saved install config in node_modules
3978
+ *
3931
3979
  * @remarks - this._installConfig must've been initialized
3932
3980
  * @returns nothing
3933
3981
  */
@@ -3985,13 +4033,13 @@ class Fyn {
3985
4033
  const fynInstallConfig = JSON.parse(await Fs.readFile(filename));
3986
4034
  logger.debug("loaded fynInstallConfig", fynInstallConfig);
3987
4035
  const { layout } = fynInstallConfig;
3988
- if (layout && layout !== this._layout) {
4036
+ if (layout && layout !== this._options.layout) {
3989
4037
  if (this._cliSource.layout !== "default") {
3990
4038
  logger.warn(
3991
- `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.`
3992
4040
  );
3993
4041
  }
3994
- this._layout = layout;
4042
+ this._options.layout = layout;
3995
4043
  }
3996
4044
  const recordedShort = fynInstallConfig.shortPkgDir === void 0 ? true : fynInstallConfig.shortPkgDir;
3997
4045
  if (recordedShort !== this._shortPkgDir) {
@@ -4112,6 +4160,7 @@ class Fyn {
4112
4160
  }
4113
4161
  /**
4114
4162
  * Get the version of a direct dependency from package.json
4163
+ *
4115
4164
  * @param {string} pkgName - Package name to look up
4116
4165
  * @returns {string|null} The version or null if not found
4117
4166
  */
@@ -4186,6 +4235,12 @@ class Fyn {
4186
4235
  return true;
4187
4236
  }
4188
4237
  }
4238
+ if (await localExportsNeedInstall({
4239
+ cwd: this._cwd,
4240
+ manifest: this._installConfig.localExports
4241
+ })) {
4242
+ return true;
4243
+ }
4189
4244
  return false;
4190
4245
  }
4191
4246
  setLocalDeps(localsByDepth) {
@@ -4200,6 +4255,9 @@ class Fyn {
4200
4255
  setLocalPkgLinks(localLinks) {
4201
4256
  this._installConfig.localPkgLinks = localLinks;
4202
4257
  }
4258
+ setLocalExports(manifest) {
4259
+ this._installConfig.localExports = manifest;
4260
+ }
4203
4261
  // save the config to outputDir
4204
4262
  async saveInstallConfig() {
4205
4263
  const outputDir = this.getOutputDir();
@@ -4216,7 +4274,7 @@ class Fyn {
4216
4274
  time: Date.now() + 5,
4217
4275
  centralDir,
4218
4276
  production: this.production,
4219
- layout: this._layout,
4277
+ layout: this._options.layout,
4220
4278
  shortPkgDir: this._shortPkgDir
4221
4279
  // not a good idea to save --run-npm options to install config because
4222
4280
  // future fyn install will automatically run them and would be unexpected.
@@ -4253,7 +4311,7 @@ class Fyn {
4253
4311
  let pkgFile;
4254
4312
  if (options.pkgFile === "package.json") {
4255
4313
  let foundDir = null;
4256
- const paths = pathUpEach(this._cwd, (path) => {
4314
+ pathUpEach(this._cwd, (path) => {
4257
4315
  const testPath = Path.join(path, "package.json");
4258
4316
  if (Fs.existsSync(testPath)) {
4259
4317
  foundDir = path;
@@ -4337,6 +4395,46 @@ class Fyn {
4337
4395
  get showDeprecated() {
4338
4396
  return this._options.showDeprecated && "show-deprecated";
4339
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
+ }
4340
4438
  get refreshOptionals() {
4341
4439
  return this._options.refreshOptionals;
4342
4440
  }
@@ -4528,6 +4626,7 @@ class Fyn {
4528
4626
  }
4529
4627
  /**
4530
4628
  * Scan FV_DIR for modules saved in the ${name}/${version} format
4629
+ *
4531
4630
  * @returns {*} pkgs under fv dir with their versions
4532
4631
  */
4533
4632
  async loadFvVersions() {
@@ -4565,7 +4664,7 @@ class Fyn {
4565
4664
  async createPkgOutDir(dir, keep) {
4566
4665
  try {
4567
4666
  const r = await Fs.$.mkdirp(dir);
4568
- if (r === null && !keep && dir !== this.getOutputDir()) {
4667
+ if (r === void 0 && !keep && dir !== this.getOutputDir()) {
4569
4668
  await this.clearPkgOutDir(dir);
4570
4669
  }
4571
4670
  } catch (err) {
@@ -4829,6 +4928,373 @@ class LifecycleScripts {
4829
4928
  module.exports = LifecycleScripts;
4830
4929
 
4831
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
+
4832
5298
  /***/ }),
4833
5299
 
4834
5300
  /***/ "./lib/local-pkg-builder.ts":
@@ -4897,7 +5363,12 @@ class LocalPkgBuilder {
4897
5363
  }, {});
4898
5364
  logger.debug("local pkgs for build all paths", allPaths, "uniq paths", uniqPaths);
4899
5365
  for (const path of uniqPaths) {
4900
- await this.addItem(byPathLookup[path]);
5366
+ try {
5367
+ await this.addItem(byPathLookup[path]);
5368
+ } catch (error) {
5369
+ this._startError = error;
5370
+ break;
5371
+ }
4901
5372
  }
4902
5373
  logger.debug("resolving build local _started promise");
4903
5374
  this._started.resolve();
@@ -4936,6 +5407,9 @@ class LocalPkgBuilder {
4936
5407
  logger.debug("waiting for local build item start, fullPath:", fullPath);
4937
5408
  await this._started.promise;
4938
5409
  }
5410
+ if (this._startError) {
5411
+ return { error: this._startError };
5412
+ }
4939
5413
  const x = this._waitItems[fullPath];
4940
5414
  if (x && x.promise) {
4941
5415
  logger.debug("waiting for build local item", fullPath, x);
@@ -5392,6 +5866,7 @@ module.exports = PkgBinLinker;
5392
5866
  "use strict";
5393
5867
 
5394
5868
  const Fs = __webpack_require__("./lib/util/file-ops.ts");
5869
+ const Path = __webpack_require__("path");
5395
5870
  const PkgBinLinkerBase = __webpack_require__("./lib/pkg-bin-linker-base.ts");
5396
5871
  const CYGWIN_LINK = `#!/bin/sh
5397
5872
  basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
@@ -5447,6 +5922,31 @@ class PkgBinLinkerWin32 extends PkgBinLinkerBase {
5447
5922
  await this._unlinkFile(symlink);
5448
5923
  await this._unlinkFile(symlink + ".cmd");
5449
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
+ }
5450
5950
  async _readBinLinks() {
5451
5951
  return (await Fs.readdir(this._binDir)).filter((x) => !x.endsWith(".cmd"));
5452
5952
  }
@@ -5931,6 +6431,7 @@ class PkgDepLocker {
5931
6431
  * - dependencies
5932
6432
  * - optionalDependencies
5933
6433
  * - devDependencies
6434
+ *
5934
6435
  * @param {*} pkgDepItems - dep items generated by makePkgDepItems in pkg-dep-resolver.js
5935
6436
  *
5936
6437
  * @returns {*} none
@@ -6160,6 +6661,7 @@ const logFormat = __webpack_require__("./lib/util/log-format.ts");
6160
6661
  const { LONG_WAIT_META } = __webpack_require__("./lib/log-items.ts");
6161
6662
  const { checkPkgOsCpu, relativePath, unSlashNpmScope } = __webpack_require__("./lib/util/fyntil.ts");
6162
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");
6163
6665
  const xaa = __webpack_require__("./lib/util/xaa.ts");
6164
6666
  const { AggregateError } = __webpack_require__("./node_modules/.f/_/@jchip/error/1.0.3/@jchip/error/dist/index.js");
6165
6667
  const Promise = __webpack_require__("./node_modules/.f/_/aveazul/1.1.0/aveazul/cjs-entry.cjs");
@@ -6426,6 +6928,31 @@ class PkgDepResolver {
6426
6928
  }
6427
6929
  return semver;
6428
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
+ }
6429
6956
  /**
6430
6957
  * create the dep relation items for a package
6431
6958
  *
@@ -6461,6 +6988,9 @@ class PkgDepResolver {
6461
6988
  deepResolve
6462
6989
  };
6463
6990
  const newItem = new DepItem(opt, depItem);
6991
+ if (this._fyn.enforceRegistryDeps && depItem.depth >= 1) {
6992
+ this._enforceRegistryDep(newItem, depItem);
6993
+ }
6464
6994
  if (noPrefetch !== true) this.prefetchMeta(newItem);
6465
6995
  items.push(newItem);
6466
6996
  }
@@ -6632,6 +7162,15 @@ class PkgDepResolver {
6632
7162
  }
6633
7163
  }
6634
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
+ }
6635
7174
  const platformCheck = () => {
6636
7175
  const sysCheck2 = checkPkgOsCpu(metaJson);
6637
7176
  if (sysCheck2 !== true) {
@@ -6688,11 +7227,7 @@ class PkgDepResolver {
6688
7227
  pkgV.hasI = 1;
6689
7228
  }
6690
7229
  }
6691
- const localFromMeta = meta.local || metaJson.local;
6692
7230
  if (localFromMeta) {
6693
- if (!item.localType) {
6694
- item.localType = localFromMeta;
6695
- }
6696
7231
  pkgV.local = item.localType;
6697
7232
  item.fullPath = pkgV.dir = pkgV.dist.fullPath;
6698
7233
  pkgV.str = meta.jsonStr;
@@ -7166,9 +7701,9 @@ ${item.depPath.join(" > ")}`
7166
7701
  throw new Error(failMetaMsg(item.name));
7167
7702
  }
7168
7703
  const updated = this._fyn.depLocker.update(item, meta);
7169
- const r2 = this._resolveWithMeta({ item, meta: updated, force: false, noLocal: true });
7170
- if (r2) {
7171
- return r2;
7704
+ const resolved = this._resolveWithMeta({ item, meta: updated, force: false, noLocal: true });
7705
+ if (resolved) {
7706
+ return resolved;
7172
7707
  }
7173
7708
  logger.debug(
7174
7709
  `cached meta for ${item.name} has no version satisfying ${item.semver}; refetching from registry`
@@ -7186,7 +7721,7 @@ ${item.depPath.join(" > ")}`
7186
7721
  });
7187
7722
  });
7188
7723
  }).catch((err) => {
7189
- if (item.dsrc !== "opt") {
7724
+ if (!item.dsrc || !item.dsrc.includes("opt")) {
7190
7725
  if (err.message.includes("Unable to retrieve meta")) {
7191
7726
  throw err;
7192
7727
  } else {
@@ -7244,7 +7779,13 @@ class PkgDistExtractor {
7244
7779
  });
7245
7780
  this._fyn = options.fyn;
7246
7781
  this._promiseQ.on("done", (x) => this.done(x));
7247
- 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
+ });
7248
7789
  }
7249
7790
  addPkgDist(data) {
7250
7791
  this._promiseQ.addItem(data);
@@ -7302,6 +7843,10 @@ class PkgDistExtractor {
7302
7843
  } else {
7303
7844
  const json = await this._fyn.ensureProperPkgDir(pkg, fullOutDir);
7304
7845
  if (json) {
7846
+ if (data.listener) {
7847
+ const listener = data.listener;
7848
+ setTimeout(() => listener.emit("done", json), 0);
7849
+ }
7305
7850
  return json;
7306
7851
  }
7307
7852
  await this._fyn.createPkgOutDir(fullOutDir);
@@ -7509,6 +8054,7 @@ class PkgDistFetcher {
7509
8054
  }
7510
8055
  /**
7511
8056
  * Check if pkg already has a copy extracted to node_modules
8057
+ *
7512
8058
  * @param {*} pkg - package info
7513
8059
  * @returns {*} pkg in FV_DIR and its package.json
7514
8060
  */
@@ -7591,8 +8137,14 @@ const logger = __webpack_require__("./lib/logger.ts");
7591
8137
  const logFormat = __webpack_require__("./lib/util/log-format.ts");
7592
8138
  const fynTil = __webpack_require__("./lib/util/fyntil.ts");
7593
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");
7594
8145
  const { INSTALL_PACKAGE } = __webpack_require__("./lib/log-items.ts");
7595
8146
  const { runNpmScript } = __webpack_require__("./lib/util/run-npm-script.ts");
8147
+ const { evaluateScriptPolicy, isScriptAllowed } = __webpack_require__("./lib/util/lifecycle-script-policy.ts");
7596
8148
  const xaa = __webpack_require__("./lib/util/xaa.ts");
7597
8149
  const { AggregateError } = __webpack_require__("./node_modules/.f/_/@jchip/error/1.0.3/@jchip/error/dist/index.js");
7598
8150
  const { RESOLVE_ORDER, RSEMVERS, LOCK_RSEMVERS, SEMVER } = __webpack_require__("./lib/symbols.ts");
@@ -7674,7 +8226,7 @@ class PkgInstaller {
7674
8226
  } catch (err) {
7675
8227
  if (err.code === "EPERM") {
7676
8228
  const st = await Fs.stat(pkgJsonFp);
7677
- await Fs.chmod(pkgJsonFp, st.mode + 384);
8229
+ await Fs.chmod(pkgJsonFp, st.mode | 384);
7678
8230
  await Fs.writeFile(pkgJsonFp, `${outputStr}
7679
8231
  `);
7680
8232
  }
@@ -7746,7 +8298,7 @@ class PkgInstaller {
7746
8298
  if (depInfo._removing) return;
7747
8299
  depInfo._removing = true;
7748
8300
  const optReqs = depInfo.requests.map((req) => {
7749
- return req.reverse().find((r) => r.startsWith("opt"));
8301
+ return req.slice().reverse().find((r) => r.startsWith("opt"));
7750
8302
  });
7751
8303
  const failedId = `${depInfo.name}@${depInfo.version}`;
7752
8304
  for (const r of optReqs) {
@@ -7869,12 +8421,27 @@ class PkgInstaller {
7869
8421
  if (this._fyn.showDeprecated && _.isEmpty(warned)) {
7870
8422
  logger.info(chalk.green("HOORAY!!! None of your dependencies are marked deprecated."));
7871
8423
  }
7872
- }).then(() => this._saveLockData()).then(() => {
8424
+ }).then(() => this._installLocalExports()).then(() => this._saveLockData()).then(() => {
7873
8425
  logger.info(`${chalk.green("done install")} ${logFormat.time(Date.now() - start)}`);
7874
8426
  }).finally(() => {
7875
8427
  logger.removeItem(INSTALL_PACKAGE);
7876
8428
  });
7877
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
+ }
7878
8445
  async _buildLocalPkg(depInfo) {
7879
8446
  if (this._fyn._options.buildLocal && this._fyn._localPkgBuilder) {
7880
8447
  const itemRes = await this._fyn._localPkgBuilder.waitForItem(depInfo.dir);
@@ -7906,25 +8473,60 @@ class PkgInstaller {
7906
8473
  }
7907
8474
  json._fyn = {};
7908
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
+ };
7909
8487
  const hasPI = json.hasPI || Boolean(scripts.preinstall);
7910
8488
  const piExed = Boolean(depInfo.preinstall);
7911
8489
  if (!piExed && hasPI) {
7912
8490
  if (depInfo.preInstalled) {
7913
8491
  json._fyn.preinstall = true;
7914
- } else {
8492
+ } else if (isAllowed("preinstall")) {
7915
8493
  logger.debug("adding preinstall step for", depInfo.dir);
7916
8494
  this.preInstall.push(depInfo);
7917
8495
  }
7918
8496
  }
7919
8497
  this.toLink.push(depInfo);
7920
8498
  const install = ["install", "postinstall"].filter((x) => {
7921
- return Boolean(scripts[x]) && !json._fyn[x];
8499
+ return Boolean(scripts[x]) && !json._fyn[x] && isAllowed(x);
7922
8500
  });
7923
8501
  if (install.length > 0) {
7924
8502
  logger.debug("adding install step for", depInfo.dir, install);
7925
8503
  depInfo.install = install;
7926
8504
  this.postInstall.push(depInfo);
7927
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
+ }
7928
8530
  }
7929
8531
  _cleanBin() {
7930
8532
  logger.updateItem(INSTALL_PACKAGE, "cleaning node_modules/.bin");
@@ -8126,6 +8728,8 @@ const PkgDepLinker = __webpack_require__("./lib/pkg-dep-linker.ts");
8126
8728
  const semverUtil = __webpack_require__("./lib/util/semver.ts");
8127
8729
  const { readPkgJson } = __webpack_require__("./lib/util/fyntil.ts");
8128
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");
8129
8733
  xsh.Promise = Promise;
8130
8734
  class PkgOptResolver {
8131
8735
  constructor(options) {
@@ -8188,6 +8792,32 @@ class PkgOptResolver {
8188
8792
  // - 0: add item back to resolve
8189
8793
  // - not: add item to queue for logging at end
8190
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
+ }
8191
8821
  /* eslint-disable max-statements */
8192
8822
  optCheck(data) {
8193
8823
  const name = data.item.name;
@@ -8306,6 +8936,17 @@ class PkgOptResolver {
8306
8936
  );
8307
8937
  return { passed: true };
8308
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
+ }
8309
8950
  data.runningScript = true;
8310
8951
  logger.updateItem(OPTIONAL_RESOLVER, `running preinstall for ${displayId}`);
8311
8952
  const ls = new LifecycleScripts({
@@ -8404,12 +9045,19 @@ const { PackageRef } = __webpack_require__("./node_modules/.f/_/@fynpo/base/1.1.
8404
9045
  const Arborist = __webpack_require__("./node_modules/.f/_/@npmcli/arborist/9.1.6/@npmcli/arborist/lib/index.js");
8405
9046
  const WATCH_TIME = 5e3;
8406
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
+ }
8407
9057
  async function checkGitRepoHasNewCommits(gitUrl, ref, cachedCommitHash) {
8408
9058
  if (!cachedCommitHash) return true;
8409
9059
  try {
8410
- const { execSync } = __webpack_require__("child_process");
8411
- const Path2 = __webpack_require__("path");
8412
- const fs2 = __webpack_require__("fs");
9060
+ const { execFileSync } = __webpack_require__("child_process");
8413
9061
  let actualGitUrl = gitUrl;
8414
9062
  let isLocalRepo = false;
8415
9063
  let localRepoPath = null;
@@ -8421,10 +9069,15 @@ async function checkGitRepoHasNewCommits(gitUrl, ref, cachedCommitHash) {
8421
9069
  }
8422
9070
  } else if (!gitUrl.includes("://")) {
8423
9071
  isLocalRepo = true;
8424
- localRepoPath = Path2.resolve(gitUrl);
9072
+ localRepoPath = Path.resolve(gitUrl);
8425
9073
  }
8426
9074
  if (isLocalRepo && localRepoPath) {
8427
- 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], {
8428
9081
  cwd: localRepoPath,
8429
9082
  stdio: "pipe",
8430
9083
  encoding: "utf8",
@@ -8447,7 +9100,12 @@ async function checkGitRepoHasNewCommits(gitUrl, ref, cachedCommitHash) {
8447
9100
  } else {
8448
9101
  actualGitUrl = gitUrl;
8449
9102
  }
8450
- 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], {
8451
9109
  stdio: "pipe",
8452
9110
  encoding: "utf8",
8453
9111
  timeout: 1e4
@@ -8684,16 +9342,17 @@ class PkgSrcManager {
8684
9342
  logger.debug(`pacote.packument ${qItem.packumentUrl}`);
8685
9343
  const promise2 = pacote.packument(
8686
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.
8687
9348
  this.getPacoteOpts({
8688
- "full-metadata": true,
8689
- "fetch-retries": 3,
8690
- "cache-policy": "ignore",
8691
- "cache-key": qItem.cacheKey,
9349
+ fullMetadata: true,
9350
+ fetchRetries: 3,
9351
+ preferOnline: true,
8692
9352
  memoize: false
8693
9353
  })
8694
9354
  );
8695
9355
  return promise2.then((x) => {
8696
- this._metaStat.inTx--;
8697
9356
  if (!x) {
8698
9357
  const msg = `pacote returned null/undefined for packument of ${pkgName}`;
8699
9358
  logger.error(chalk.yellow(msg));
@@ -8714,6 +9373,7 @@ class PkgSrcManager {
8714
9373
  this.updateFetchMetaStatus(false);
8715
9374
  const promise = qItem.item.urlType ? this.fetchUrlSemverMeta(qItem.item) : pacoteRequest();
8716
9375
  return promise.then((x) => {
9376
+ this._metaStat.inTx--;
8717
9377
  const time = Date.now() - startTime;
8718
9378
  if (time > 20 * 1e3) {
8719
9379
  logger.info(
@@ -8728,10 +9388,11 @@ class PkgSrcManager {
8728
9388
  qItem.defer.reject(new AggregateError([new Error(msg)], msg));
8729
9389
  return;
8730
9390
  }
8731
- refreshCacheEntry(this._fyn._fynCacheDir, qItem.cacheKey).catch(() => {
9391
+ refreshCacheEntry(this._cacheDir, qItem.cacheKey).catch(() => {
8732
9392
  });
8733
9393
  qItem.defer.resolve(x);
8734
9394
  }).catch((err) => {
9395
+ this._metaStat.inTx--;
8735
9396
  qItem.defer.reject(err);
8736
9397
  });
8737
9398
  }
@@ -8773,19 +9434,7 @@ class PkgSrcManager {
8773
9434
  } else {
8774
9435
  dirPacker = this._getPacoteDirPacker();
8775
9436
  }
8776
- let pacoteOpts = { dirPacker };
8777
- if (item.urlType.startsWith("git") && item.semver && !item.semver.match(/^[a-f0-9]{40}$/)) {
8778
- const potentialCacheKeys = [
8779
- // Try common cache key patterns based on semver
8780
- `fyn-tarball-for-git+https://github.com/${item.semver.replace(/^github:/, "").split("#")[0]}.git#`,
8781
- `fyn-tarball-for-git+ssh://git@github.com/${item.semver.replace(/^github:/, "").split("#")[0]}.git#`
8782
- ];
8783
- for (const baseKey of potentialCacheKeys) {
8784
- try {
8785
- } catch (e) {
8786
- }
8787
- }
8788
- }
9437
+ const pacoteOpts = { dirPacker };
8789
9438
  return pacote.manifest(`${item.name}@${item.semver}`, this.getPacoteOpts(pacoteOpts)).then((manifest) => {
8790
9439
  manifest = Object.assign({}, manifest);
8791
9440
  return {
@@ -8809,9 +9458,10 @@ class PkgSrcManager {
8809
9458
  let integrity;
8810
9459
  let shouldRefresh = false;
8811
9460
  if (tgzCacheInfo) {
8812
- 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);
8813
9463
  const cachedCommitHash = cachedResolved?.match(/#([a-f0-9]{40})$/)?.[1];
8814
- if (!shouldRefresh && cachedCommitHash && item.semver && !item.semver.match(/^[a-f0-9]{40}$/)) {
9464
+ if (!shouldRefresh && cachedCommitHash && item.semver && !isPinnedGitCommit(item.semver)) {
8815
9465
  let gitUrl = item.semver;
8816
9466
  let ref = "HEAD";
8817
9467
  if (gitUrl.includes("#")) {
@@ -8834,17 +9484,15 @@ class PkgSrcManager {
8834
9484
  logger.debug(
8835
9485
  `git cache for '${item.name}' has new commits (cached: ${cachedCommitHash.substring(0, 8)}, checking ${ref}), forcing refresh`
8836
9486
  );
8837
- } else {
8838
- if (tgzCacheInfo.refreshTime) {
8839
- const stale = Date.now() - tgzCacheInfo.refreshTime;
8840
- const staleByTime = stale >= META_CACHE_STALE_TIME;
8841
- if (staleByTime) {
8842
- shouldRefresh = true;
8843
- const reason = hasNewCommits === null ? "ls-remote failed" : "no new commits";
8844
- logger.debug(
8845
- `git cache for '${item.name}' (${reason}), cache is stale by time (${(stale / 1e3 / 60 / 60).toFixed(1)}h old), forcing refresh`
8846
- );
8847
- }
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
+ );
8848
9496
  }
8849
9497
  }
8850
9498
  } else if (!shouldRefresh && tgzCacheInfo.refreshTime) {
@@ -8918,6 +9566,7 @@ class PkgSrcManager {
8918
9566
  const cacheKey = `make-fetch-happen:request-cache:${packumentUrl}`;
8919
9567
  const legacyCacheKey = `make-fetch-happen:request-cache:full:${packumentUrl}`;
8920
9568
  const cacheKeys = [cacheKey, legacyCacheKey];
9569
+ let cacheMemoized = false;
8921
9570
  const loadCachedPackument = async (key, memoize = true) => {
8922
9571
  try {
8923
9572
  const cached = await cacache.get(this._cacheDir, key, { memoize });
@@ -8940,6 +9589,33 @@ class PkgSrcManager {
8940
9589
  }
8941
9590
  return _.maxBy(cachedEntries, (cached) => cached.refreshTime || 0) || cachedEntries[0];
8942
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
+ };
8943
9619
  const readMemoizedPackument = async () => {
8944
9620
  const memoized = await loadBestCachedPackument(false);
8945
9621
  if (!(memoized && memoized.data)) {
@@ -8950,7 +9626,11 @@ class PkgSrcManager {
8950
9626
  this._metaStat.wait--;
8951
9627
  return JSON.parse(memoized.data.toString());
8952
9628
  };
9629
+ let fetchAttempted = false;
9630
+ let foundCache;
9631
+ let foundPackument;
8953
9632
  const queueMetaFetchRequest = (cached) => {
9633
+ fetchAttempted = true;
8954
9634
  const offline = this._fyn.remoteMetaDisabled;
8955
9635
  if (cached && this._fyn.forceCache) {
8956
9636
  this._metaStat.wait--;
@@ -8975,18 +9655,24 @@ class PkgSrcManager {
8975
9655
  return netQItem.defer.promise;
8976
9656
  };
8977
9657
  this._metaStat.wait++;
8978
- let foundCache;
8979
- let cacheMemoized = false;
8980
9658
  const metaMemoizeUrl = this._fyn._options.metaMemoize;
8981
- const promise = (item.urlType || forceRefresh ? (
8982
- // when the semver is a url then the meta is not from npm registry and
8983
- // we can't use the cache for registry.
8984
- // when forceRefresh, skip the local cacache + meta-mem path so we go
8985
- // straight to a fresh registry fetch via queueMetaFetchRequest.
8986
- Promise.resolve()
8987
- ) : loadBestCachedPackument(true)).then((cached) => {
8988
- 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
+ }
8989
9673
  foundCache = cached;
9674
+ const packument = cached && cached.data && JSON.parse(cached.data);
9675
+ foundPackument = packument;
8990
9676
  if (cached && cached.refreshTime) {
8991
9677
  const stale = Date.now() - cached.refreshTime;
8992
9678
  const since = (stale / 1e3).toFixed(2);
@@ -9029,10 +9715,19 @@ class PkgSrcManager {
9029
9715
  return queueMetaFetchRequest(packument);
9030
9716
  }
9031
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
+ }
9032
9728
  if (foundCache) {
9033
9729
  const data = foundCache.data && foundCache.data.toString();
9034
9730
  logger.debug(`fail to process packument cache - ${err.message}; data ${data}`);
9035
- throw err;
9036
9731
  }
9037
9732
  return queueMetaFetchRequest();
9038
9733
  }).then((meta) => {
@@ -9096,7 +9791,7 @@ class PkgSrcManager {
9096
9791
  return passthrough2;
9097
9792
  }
9098
9793
  const opts = this.getPacoteOpts({
9099
- fullMeta: true,
9794
+ fullMetadata: true,
9100
9795
  integrity,
9101
9796
  resolved: tarballUrl
9102
9797
  });
@@ -9213,6 +9908,7 @@ class PkgSrcManager {
9213
9908
  }
9214
9909
  module.exports = PkgSrcManager;
9215
9910
  module.exports.META_CACHE_STALE_TIME = META_CACHE_STALE_TIME;
9911
+ module.exports.isPinnedGitCommit = isPinnedGitCommit;
9216
9912
 
9217
9913
 
9218
9914
  /***/ }),
@@ -9325,7 +10021,8 @@ async function checkPkgNeedInstall(dir, checkCtime = 0) {
9325
10021
  hasScript
9326
10022
  };
9327
10023
  } catch (error) {
9328
- return { install: false, error };
10024
+ logger.warn(`unable to determine if local package at ${dir} needs install: ${error.message}`);
10025
+ throw error;
9329
10026
  }
9330
10027
  }
9331
10028
  exports.checkPkgNeedInstall = checkPkgNeedInstall;
@@ -9532,7 +10229,10 @@ const fyntil = {
9532
10229
  return rmObj;
9533
10230
  },
9534
10231
  exit(err) {
9535
- process.exit(err ? 1 : 0);
10232
+ if (typeof err === "number") {
10233
+ return process.exit(err);
10234
+ }
10235
+ return process.exit(err ? 1 : 0);
9536
10236
  },
9537
10237
  async readJson(file, defaultData) {
9538
10238
  try {
@@ -9926,7 +10626,7 @@ async function linkPackTree({ tree, src, dest, sym1, sourceMaps }) {
9926
10626
  const files = tree[SYM_FILES];
9927
10627
  const destFiles = await prepDestDir(dest);
9928
10628
  for (const file of files) {
9929
- if (!ci.isCI && file.match(/.+\..+\.map$/)) {
10629
+ if (!ci.isCI && file.match(/\.(js|mjs)\.map$/)) {
9930
10630
  continue;
9931
10631
  }
9932
10632
  destFiles[file] = true;
@@ -9987,6 +10687,133 @@ module.exports = {
9987
10687
  module.exports = __webpack_require__("./node_modules/.f/_/item-queue/1.1.2/item-queue/lib/inflight.js");
9988
10688
 
9989
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
+
9990
10817
  /***/ }),
9991
10818
 
9992
10819
  /***/ "./lib/util/log-format.ts":
@@ -10007,6 +10834,7 @@ module.exports = {
10007
10834
  * - the part before package name blue
10008
10835
  * - the package name part magenta
10009
10836
  * - remaining as is
10837
+ *
10010
10838
  * @param {*} name - package name
10011
10839
  * @param {*} path - path to highlight
10012
10840
  * @returns
@@ -10106,7 +10934,7 @@ function makeNpmEnv(data, opts, prefix, env) {
10106
10934
  if (minimalConfigKeys.includes(normalizedKey) || minimalConfigKeys.includes(key)) {
10107
10935
  const envKey = `npm_config_${normalizedKey}`;
10108
10936
  const val = opts.config[key];
10109
- if (val != null && typeof val !== "function") {
10937
+ if (val !== null && val !== void 0 && typeof val !== "function") {
10110
10938
  env[envKey] = String(val);
10111
10939
  }
10112
10940
  }
@@ -10173,6 +11001,61 @@ ItemQueue.Promise = __webpack_require__("./lib/util/aveazul.ts");
10173
11001
  module.exports = ItemQueue;
10174
11002
 
10175
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
+
10176
11059
  /***/ }),
10177
11060
 
10178
11061
  /***/ "./lib/util/run-npm-script.ts":
@@ -10279,11 +11162,14 @@ function simpleCompare(a, b) {
10279
11162
  }
10280
11163
  if (partsA[1]) {
10281
11164
  if (partsB[1]) {
11165
+ if (Semver.valid(a) && Semver.valid(b)) {
11166
+ return Semver.rcompare(a, b);
11167
+ }
10282
11168
  return partsA[1] > partsB[1] ? -1 : 1;
10283
11169
  }
10284
- return -1;
10285
- } else if (partsB[1]) {
10286
11170
  return 1;
11171
+ } else if (partsB[1]) {
11172
+ return -1;
10287
11173
  } else {
10288
11174
  return 0;
10289
11175
  }
@@ -10567,7 +11453,7 @@ async function _scanFileStats(dir, ignores, baseDir = "") {
10567
11453
  }
10568
11454
  function scanFileStats(dir, options = {}) {
10569
11455
  const ignores = [
10570
- `**/?(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)`,
10571
11457
  "**/*.?(log|md)"
10572
11458
  ].concat(options.ignores || `**/?(docs|docusaurus|packages|tmp|.etmp|samples|dist)`).concat(options.moreIgnores).filter((x) => x);
10573
11459
  return _scanFileStats(dir, ignores, "");