syncpack 12.3.3 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +1 -2
  2. package/dist/bin-fix-mismatches/fix-mismatches.d.ts +2 -2
  3. package/dist/bin-fix-mismatches/fix-mismatches.js +13 -8
  4. package/dist/bin-fix-mismatches/index.js +1 -32
  5. package/dist/bin-format/format.d.ts +2 -2
  6. package/dist/bin-format/format.js +10 -7
  7. package/dist/bin-format/index.js +1 -26
  8. package/dist/bin-lint/index.js +1 -22
  9. package/dist/bin-lint/lint.d.ts +1 -1
  10. package/dist/bin-lint/lint.js +23 -23
  11. package/dist/bin-lint-semver-ranges/index.js +1 -36
  12. package/dist/bin-lint-semver-ranges/lint-semver-ranges.d.ts +2 -2
  13. package/dist/bin-lint-semver-ranges/lint-semver-ranges.js +5 -4
  14. package/dist/bin-list/index.js +1 -31
  15. package/dist/bin-list/list.d.ts +1 -1
  16. package/dist/bin-list/list.js +9 -5
  17. package/dist/bin-list-mismatches/index.js +1 -31
  18. package/dist/bin-list-mismatches/list-mismatches.d.ts +2 -2
  19. package/dist/bin-list-mismatches/list-mismatches.js +15 -9
  20. package/dist/bin-prompt/index.js +1 -33
  21. package/dist/bin-prompt/prompt.d.ts +2 -2
  22. package/dist/bin-prompt/prompt.js +13 -11
  23. package/dist/bin-set-semver-ranges/index.js +1 -38
  24. package/dist/bin-set-semver-ranges/set-semver-ranges.d.ts +2 -2
  25. package/dist/bin-set-semver-ranges/set-semver-ranges.js +8 -5
  26. package/dist/bin-update/effects.d.ts +1 -1
  27. package/dist/bin-update/effects.js +46 -31
  28. package/dist/bin-update/index.js +1 -31
  29. package/dist/bin-update/update.d.ts +1 -1
  30. package/dist/bin-update/update.js +4 -4
  31. package/dist/bin.js +3 -3
  32. package/dist/config/get-custom-types.js +20 -13
  33. package/dist/config/get-enabled-types.js +27 -12
  34. package/dist/config/get-sort-exports.js +2 -1
  35. package/dist/config/types.d.ts +2 -0
  36. package/dist/error-handlers/default-error-handlers.js +2 -2
  37. package/dist/get-context/index.d.ts +2 -2
  38. package/dist/get-context/index.js +1 -1
  39. package/dist/get-instances/index.js +6 -6
  40. package/dist/get-instances/instance.js +6 -2
  41. package/dist/get-package-json-files/get-patterns/get-lerna-patterns.js +4 -2
  42. package/dist/get-package-json-files/get-patterns/get-pnpm-patterns.d.ts +1 -1
  43. package/dist/get-package-json-files/get-patterns/get-pnpm-patterns.js +2 -2
  44. package/dist/get-package-json-files/get-patterns/get-yarn-patterns.js +2 -2
  45. package/dist/get-package-json-files/get-patterns/index.js +3 -3
  46. package/dist/get-package-json-files/index.js +1 -1
  47. package/dist/get-package-json-files/package-json-file.d.ts +7 -0
  48. package/dist/get-package-json-files/package-json-file.js +18 -3
  49. package/dist/guards/can-add-to-group.js +19 -13
  50. package/dist/guards/is-semver.js +3 -1
  51. package/dist/io/ask-for-choice.js +3 -3
  52. package/dist/io/ask-for-input.js +2 -2
  53. package/dist/io/exit-if-invalid.js +1 -1
  54. package/dist/io/glob-sync.js +1 -1
  55. package/dist/io/index.d.ts +1 -2
  56. package/dist/io/index.js +1 -1
  57. package/dist/io/read-config-file.js +5 -3
  58. package/dist/io/read-file-sync.js +1 -1
  59. package/dist/io/read-json-file-sync.d.ts +2 -1
  60. package/dist/io/read-json-file-sync.js +9 -5
  61. package/dist/io/read-yaml-file-sync.js +1 -1
  62. package/dist/io/{to-json.d.ts → to-formatted-json.d.ts} +1 -1
  63. package/dist/io/{to-json.js → to-formatted-json.js} +11 -8
  64. package/dist/io/write-file-sync.js +2 -2
  65. package/dist/io/write-if-changed.js +3 -4
  66. package/dist/lib/format-repository-url.js +8 -5
  67. package/dist/lib/get-group-header.js +3 -1
  68. package/dist/lib/get.js +20 -12
  69. package/dist/lib/ring-buffer.js +1 -1
  70. package/dist/lib/set-semver-range.js +8 -4
  71. package/dist/lib/with-logger.js +6 -6
  72. package/dist/option.js +4 -1
  73. package/dist/schema.json +3 -0
  74. package/dist/semver-group/create-semver-groups.js +7 -3
  75. package/dist/semver-group/disabled.js +1 -1
  76. package/dist/semver-group/filtered-out.js +1 -1
  77. package/dist/semver-group/ignored.js +1 -1
  78. package/dist/semver-group/with-range.js +3 -3
  79. package/dist/specifier/alias.js +3 -2
  80. package/dist/specifier/delete.d.ts +2 -2
  81. package/dist/specifier/exact.js +1 -1
  82. package/dist/specifier/hosted-git.js +5 -3
  83. package/dist/specifier/index.js +27 -15
  84. package/dist/specifier/latest.js +1 -1
  85. package/dist/specifier/lib/parse-specifier.js +3 -1
  86. package/dist/specifier/range.js +1 -1
  87. package/dist/specifier/workspace-protocol.js +2 -1
  88. package/dist/strategy/lib/get-non-empty-string-prop.js +1 -1
  89. package/dist/strategy/name-and-version-props.js +7 -7
  90. package/dist/strategy/named-version-string.js +11 -8
  91. package/dist/strategy/unnamed-version-string.js +6 -6
  92. package/dist/strategy/versions-by-name.js +4 -2
  93. package/dist/version-group/banned.js +1 -1
  94. package/dist/version-group/create-version-groups.js +11 -7
  95. package/dist/version-group/filtered-out.js +1 -1
  96. package/dist/version-group/ignored.js +1 -1
  97. package/dist/version-group/lib/get-preferred-version.js +22 -13
  98. package/dist/version-group/lib/get-range-score.js +3 -1
  99. package/dist/version-group/pinned.js +2 -2
  100. package/dist/version-group/same-range.js +6 -6
  101. package/dist/version-group/snapped-to.js +7 -5
  102. package/dist/version-group/standard.js +19 -16
  103. package/package.json +41 -33
@@ -16,9 +16,9 @@ export function getEnabledTypes({ cli, rcFile, }) {
16
16
  return pipe(
17
17
  // Look for dependency types defined using the old `{ prod: true }` syntax
18
18
  // deprecated in syncpack@9.0.0
19
- Effect.succeed(INTERNAL_TYPES.filter((key) => isBoolean(rcFile[key]))),
19
+ Effect.succeed(INTERNAL_TYPES.filter(key => isBoolean(rcFile[key]))),
20
20
  // Short-circuit and quit if deprecated config is used
21
- Effect.flatMap((deprecatedTypeProps) => deprecatedTypeProps.length > 0
21
+ Effect.flatMap(deprecatedTypeProps => deprecatedTypeProps.length > 0
22
22
  ? Effect.fail(new DeprecatedTypesError({ types: deprecatedTypeProps }))
23
23
  : Effect.void), Effect.flatMap(() => pipe(Effect.Do,
24
24
  // Get index of every available strategy, keyed by their names as
@@ -30,13 +30,28 @@ export function getEnabledTypes({ cli, rcFile, }) {
30
30
  // Combine them with the default/internal dependency types
31
31
  Effect.map((customTypes) => Object.fromEntries([
32
32
  ['dev', new VersionsByNameStrategy('dev', 'devDependencies')],
33
- ['local', new NameAndVersionPropsStrategy('local', 'version', 'name')],
34
- ['overrides', new VersionsByNameStrategy('overrides', 'overrides')],
35
- ['peer', new VersionsByNameStrategy('peer', 'peerDependencies')],
36
- ['pnpmOverrides', new VersionsByNameStrategy('pnpmOverrides', 'pnpm.overrides')],
33
+ [
34
+ 'local',
35
+ new NameAndVersionPropsStrategy('local', 'version', 'name'),
36
+ ],
37
+ [
38
+ 'overrides',
39
+ new VersionsByNameStrategy('overrides', 'overrides'),
40
+ ],
41
+ [
42
+ 'peer',
43
+ new VersionsByNameStrategy('peer', 'peerDependencies'),
44
+ ],
45
+ [
46
+ 'pnpmOverrides',
47
+ new VersionsByNameStrategy('pnpmOverrides', 'pnpm.overrides'),
48
+ ],
37
49
  ['prod', new VersionsByNameStrategy('prod', 'dependencies')],
38
- ['resolutions', new VersionsByNameStrategy('resolutions', 'resolutions')],
39
- ...customTypes.map((type) => [type.name, type]),
50
+ [
51
+ 'resolutions',
52
+ new VersionsByNameStrategy('resolutions', 'resolutions'),
53
+ ],
54
+ ...customTypes.map(type => [type.name, type]),
40
55
  ])))),
41
56
  // The names of every available strategy
42
57
  Effect.bind('allStrategyNames', ({ allStrategiesByName }) => Effect.succeed(Object.keys(allStrategiesByName))),
@@ -61,7 +76,7 @@ export function getEnabledTypes({ cli, rcFile, }) {
61
76
  strategyNamesByStatus.provided.join('') === '**') {
62
77
  return Effect.succeed(allStrategyNames.map(getStrategyByName));
63
78
  }
64
- strategyNamesByStatus.provided.forEach((name) => {
79
+ strategyNamesByStatus.provided.forEach(name => {
65
80
  if (name.startsWith('!')) {
66
81
  strategyNamesByStatus.negative.push(name.replace('!', ''));
67
82
  }
@@ -70,14 +85,14 @@ export function getEnabledTypes({ cli, rcFile, }) {
70
85
  }
71
86
  });
72
87
  if (isNonEmptyArray(strategyNamesByStatus.negative)) {
73
- allStrategyNames.forEach((name) => {
88
+ allStrategyNames.forEach(name => {
74
89
  if (!strategyNamesByStatus.negative.includes(name)) {
75
90
  strategyNamesByStatus.enabled.push(name);
76
91
  }
77
92
  });
78
93
  }
79
94
  if (isNonEmptyArray(strategyNamesByStatus.positive)) {
80
- strategyNamesByStatus.positive.forEach((name) => {
95
+ strategyNamesByStatus.positive.forEach(name => {
81
96
  if (!strategyNamesByStatus.enabled.includes(name)) {
82
97
  strategyNamesByStatus.enabled.push(name);
83
98
  }
@@ -90,5 +105,5 @@ export function getEnabledTypes({ cli, rcFile, }) {
90
105
  function getStrategyByName(type) {
91
106
  return allStrategiesByName[type];
92
107
  }
93
- }), Effect.tap((enabledTypes) => Effect.logDebug(`enabled dependency types determined to be: ${JSON.stringify(enabledTypes)}`)));
108
+ }), Effect.tap(enabledTypes => Effect.logDebug(`enabled dependency types determined to be: ${JSON.stringify(enabledTypes)}`)));
94
109
  }
@@ -2,7 +2,8 @@ import { isArrayOfStrings } from 'tightrope/guard/is-array-of-strings.js';
2
2
  import { isEmptyArray } from 'tightrope/guard/is-empty-array.js';
3
3
  import { DEFAULT_CONFIG } from '../constants.js';
4
4
  export function getSortExports({ rcFile }) {
5
- return isArrayOfStrings(rcFile.sortExports) || isEmptyArray(rcFile.sortExports)
5
+ return isArrayOfStrings(rcFile.sortExports) ||
6
+ isEmptyArray(rcFile.sortExports)
6
7
  ? rcFile.sortExports
7
8
  : DEFAULT_CONFIG.sortExports;
8
9
  }
@@ -92,6 +92,8 @@ export interface CliConfig {
92
92
  readonly types: string;
93
93
  }
94
94
  export interface RcConfig {
95
+ /** @see https://jamiemason.github.io/syncpack/integrations/json-schema */
96
+ $schema?: string;
95
97
  /** @see https://jamiemason.github.io/syncpack/config/custom-types */
96
98
  customTypes: Record<string, CustomTypeConfig.Any>;
97
99
  /** @see https://jamiemason.github.io/syncpack/config/dependency-types */
@@ -1,6 +1,6 @@
1
+ import { EOL } from 'node:os';
1
2
  import chalk from 'chalk';
2
3
  import { Effect } from 'effect';
3
- import { EOL } from 'os';
4
4
  export const defaultErrorHandlers = {
5
5
  // getContext
6
6
  GlobError(err) {
@@ -13,7 +13,7 @@ export const defaultErrorHandlers = {
13
13
  return Effect.logError([
14
14
  chalk.red('An error was found when parsing a JSON file'),
15
15
  chalk.red(' File:', err.filePath),
16
- chalk.red(' Error:', err.error),
16
+ chalk.red(' Error:', err.errors),
17
17
  ].join(EOL));
18
18
  },
19
19
  NoSourcesFoundError(err) {
@@ -1,6 +1,6 @@
1
1
  import { Effect } from 'effect';
2
2
  import type { O } from 'ts-toolbelt';
3
- import { type CliConfig, type RcConfig } from '../config/types.js';
3
+ import type { CliConfig, RcConfig } from '../config/types.js';
4
4
  import type { ErrorHandlers } from '../error-handlers/default-error-handlers.js';
5
5
  import type { PackageJsonFile } from '../get-package-json-files/package-json-file.js';
6
6
  import type { Io } from '../io/index.js';
@@ -19,5 +19,5 @@ interface Input {
19
19
  cli: Partial<CliConfig>;
20
20
  errorHandlers: ErrorHandlers;
21
21
  }
22
- export declare function getContext({ io, cli, errorHandlers }: Input): Effect.Effect<Ctx>;
22
+ export declare function getContext({ io, cli, errorHandlers, }: Input): Effect.Effect<Ctx>;
23
23
  export {};
@@ -2,7 +2,7 @@ import { Effect, flow, pipe } from 'effect';
2
2
  import { getPackageJsonFiles } from '../get-package-json-files/index.js';
3
3
  import { readConfigFile } from '../io/read-config-file.js';
4
4
  import { keyBy } from './lib/key-by.js';
5
- export function getContext({ io, cli, errorHandlers }) {
5
+ export function getContext({ io, cli, errorHandlers, }) {
6
6
  const exitOnError = Effect.flatMap(() => Effect.failSync(() => io.process.exit(1)));
7
7
  return pipe(Effect.Do, Effect.bind('rcFile', () => readConfigFile(io, cli.configPath)), Effect.bind('packageJsonFiles', ({ rcFile }) => getPackageJsonFiles(io, { cli, rcFile })), Effect.map(({ rcFile, packageJsonFiles }) => ({
8
8
  config: { cli, rcFile },
@@ -6,22 +6,22 @@ import { createSemverGroups } from '../semver-group/create-semver-groups.js';
6
6
  import { createVersionGroups } from '../version-group/create-version-groups.js';
7
7
  export function getInstances(ctx, io, errorHandlers) {
8
8
  const exitOnError = Effect.flatMap(() => Effect.failSync(() => io.process.exit(1)));
9
- return pipe(Effect.Do, Effect.bind('enabledTypes', () => getEnabledTypes(ctx.config)), Effect.bind('semverGroups', () => createSemverGroups(ctx)), Effect.bind('versionGroups', () => createVersionGroups(ctx)), Effect.bind('instances', (acc) => pipe(ctx.packageJsonFiles, Effect.forEach((file) => file.getInstances(acc.enabledTypes)), Effect.map((instancesByFile) => instancesByFile.flat()))), Effect.tap(({ instances, semverGroups, versionGroups }) => Effect.sync(() => {
9
+ return pipe(Effect.Do, Effect.bind('enabledTypes', () => getEnabledTypes(ctx.config)), Effect.bind('semverGroups', () => createSemverGroups(ctx)), Effect.bind('versionGroups', () => createVersionGroups(ctx)), Effect.bind('instances', acc => pipe(ctx.packageJsonFiles, Effect.forEach(file => file.getInstances(acc.enabledTypes)), Effect.map(instancesByFile => instancesByFile.flat()))), Effect.tap(({ instances, semverGroups, versionGroups }) => Effect.sync(() => {
10
10
  for (const instance of instances) {
11
11
  // assign each instance to its semver group, first match wins
12
- assignToSemverGroup: for (const group of semverGroups) {
12
+ for (const group of semverGroups) {
13
13
  if (canAddToGroup(ctx.packageJsonFilesByName, group, instance)) {
14
14
  instance.semverGroup = group;
15
15
  group.instances.push(instance);
16
- break assignToSemverGroup;
16
+ break;
17
17
  }
18
18
  }
19
19
  // assign each instance to its version group, first match wins
20
- assignToVersionGroup: for (const group of versionGroups) {
20
+ for (const group of versionGroups) {
21
21
  if (canAddToGroup(ctx.packageJsonFilesByName, group, instance)) {
22
22
  instance.versionGroup = group;
23
23
  group.instances.push(instance);
24
- break assignToVersionGroup;
24
+ break;
25
25
  }
26
26
  }
27
27
  }
@@ -37,6 +37,6 @@ export function getInstances(ctx, io, errorHandlers) {
37
37
  VersionGroupConfigError: flow(errorHandlers.VersionGroupConfigError, exitOnError),
38
38
  }));
39
39
  function getSortedAndFiltered(groups) {
40
- return groups.filter((group) => group.instances.sort(sortByName).length > 0);
40
+ return groups.filter(group => group.instances.sort(sortByName).length > 0);
41
41
  }
42
42
  }
@@ -16,7 +16,8 @@ export class Instance {
16
16
  versionGroup;
17
17
  constructor(name, rawSpecifier, packageJsonFile, strategy) {
18
18
  this.name = name;
19
- this.pkgName = packageJsonFile.jsonFile.contents.name || 'PACKAGE_JSON_HAS_NO_NAME';
19
+ this.pkgName =
20
+ packageJsonFile.jsonFile.contents.name || 'PACKAGE_JSON_HAS_NO_NAME';
20
21
  this.packageJsonFile = packageJsonFile;
21
22
  this.strategy = strategy;
22
23
  this.semverGroup = null;
@@ -26,6 +27,9 @@ export class Instance {
26
27
  /** Mutate the package.json file in memory with the latest version specifier */
27
28
  write(rawSpecifier) {
28
29
  this.rawSpecifier = Specifier.create(this, rawSpecifier);
29
- return this.strategy.write(this.packageJsonFile, [this.name, this.rawSpecifier.raw]);
30
+ return this.strategy.write(this.packageJsonFile, [
31
+ this.name,
32
+ this.rawSpecifier.raw,
33
+ ]);
30
34
  }
31
35
  }
@@ -1,9 +1,11 @@
1
+ import { join } from 'node:path';
1
2
  import { Effect, Option as O, pipe } from 'effect';
2
- import { join } from 'path';
3
3
  import { isArrayOfStrings } from 'tightrope/guard/is-array-of-strings.js';
4
4
  import { readJsonFileSync } from '../../io/read-json-file-sync.js';
5
5
  export function getLernaPatterns(io) {
6
- return pipe(readJsonFileSync(io, join(io.process.cwd(), 'lerna.json')), Effect.map((file) => isArrayOfStrings(file.contents.packages) ? O.some(file.contents.packages) : O.none()), Effect.catchTags({
6
+ return pipe(readJsonFileSync(io, join(io.process.cwd(), 'lerna.json')), Effect.map(file => isArrayOfStrings(file.contents.packages)
7
+ ? O.some(file.contents.packages)
8
+ : O.none()), Effect.catchTags({
7
9
  ReadFileError: () => Effect.succeed(O.none()),
8
10
  JsonParseError: () => Effect.succeed(O.none()),
9
11
  }));
@@ -1,3 +1,3 @@
1
1
  import { Effect, Option as O } from 'effect';
2
- import { type Io } from '../../io/index.js';
2
+ import type { Io } from '../../io/index.js';
3
3
  export declare function getPnpmPatterns(io: Io): Effect.Effect<O.Option<string[]>>;
@@ -1,5 +1,5 @@
1
+ import { join } from 'node:path';
1
2
  import { Effect, Option as O, pipe } from 'effect';
2
- import { join } from 'path';
3
3
  import { isArrayOfStrings } from 'tightrope/guard/is-array-of-strings.js';
4
4
  import { readYamlFileSync } from '../../io/read-yaml-file-sync.js';
5
5
  export function getPnpmPatterns(io) {
@@ -8,7 +8,7 @@ export function getPnpmPatterns(io) {
8
8
  // - "packages/**"
9
9
  // - "components/**"
10
10
  // - "!**/test/**"
11
- readYamlFileSync(io, join(io.process.cwd(), 'pnpm-workspace.yaml')), Effect.map((file) => (isArrayOfStrings(file?.packages) ? O.some(file.packages) : O.none())), Effect.catchTags({
11
+ readYamlFileSync(io, join(io.process.cwd(), 'pnpm-workspace.yaml')), Effect.map(file => isArrayOfStrings(file?.packages) ? O.some(file.packages) : O.none()), Effect.catchTags({
12
12
  ReadYamlFileError: () => Effect.succeed(O.none()),
13
13
  }));
14
14
  }
@@ -1,10 +1,10 @@
1
+ import { join } from 'node:path';
1
2
  import { Effect, Option as O, pipe } from 'effect';
2
- import { join } from 'path';
3
3
  import { isArrayOfStrings } from 'tightrope/guard/is-array-of-strings.js';
4
4
  import { isNonEmptyObject } from 'tightrope/guard/is-non-empty-object.js';
5
5
  import { readJsonFileSync } from '../../io/read-json-file-sync.js';
6
6
  export function getYarnPatterns(io) {
7
- return pipe(readJsonFileSync(io, join(io.process.cwd(), 'package.json')), Effect.map((file) => isNonEmptyObject(file.contents.workspaces) &&
7
+ return pipe(readJsonFileSync(io, join(io.process.cwd(), 'package.json')), Effect.map(file => isNonEmptyObject(file.contents.workspaces) &&
8
8
  isArrayOfStrings(file.contents.workspaces.packages)
9
9
  ? O.some(file.contents.workspaces.packages)
10
10
  : isArrayOfStrings(file.contents.workspaces)
@@ -10,14 +10,14 @@ import { getYarnPatterns } from './get-yarn-patterns.js';
10
10
  * this monorepo.
11
11
  */
12
12
  export function getPatterns(io, config) {
13
- return pipe(getCliPatterns(), Effect.flatMap((opt) => (O.isSome(opt) ? Effect.succeed(opt) : getWorkspacePatterns())), Effect.map(O.map(limitToPackageJson)), Effect.map(O.getOrElse(() => [...DEFAULT_CONFIG.source])));
13
+ return pipe(getCliPatterns(), Effect.flatMap(opt => O.isSome(opt) ? Effect.succeed(opt) : getWorkspacePatterns()), Effect.map(O.map(limitToPackageJson)), Effect.map(O.getOrElse(() => [...DEFAULT_CONFIG.source])));
14
14
  function getCliPatterns() {
15
15
  return pipe(O.some(getSource(config)), O.filter(isArrayOfStrings), Effect.succeed);
16
16
  }
17
17
  function getWorkspacePatterns() {
18
- return pipe(getYarnPatterns(io), Effect.flatMap((opt) => (O.isSome(opt) ? Effect.succeed(opt) : getPnpmPatterns(io))), Effect.flatMap((opt) => (O.isSome(opt) ? Effect.succeed(opt) : getLernaPatterns(io))), Effect.map(O.map((patterns) => ['package.json', ...patterns])));
18
+ return pipe(getYarnPatterns(io), Effect.flatMap(opt => O.isSome(opt) ? Effect.succeed(opt) : getPnpmPatterns(io)), Effect.flatMap(opt => O.isSome(opt) ? Effect.succeed(opt) : getLernaPatterns(io)), Effect.map(O.map(patterns => ['package.json', ...patterns])));
19
19
  }
20
20
  function limitToPackageJson(patterns) {
21
- return patterns.map((pattern) => pattern.includes('package.json') ? pattern : `${pattern}/package.json`);
21
+ return patterns.map(pattern => pattern.includes('package.json') ? pattern : `${pattern}/package.json`);
22
22
  }
23
23
  }
@@ -4,5 +4,5 @@ import { getFilePaths } from './get-file-paths.js';
4
4
  import { PackageJsonFile } from './package-json-file.js';
5
5
  /** Create an API for every package.json file needed. */
6
6
  export function getPackageJsonFiles(io, config) {
7
- return pipe(getFilePaths(io, config), Effect.flatMap((filePaths) => Effect.all(filePaths.map((filePath) => readJsonFileSync(io, filePath)))), Effect.map((files) => files.map((file) => new PackageJsonFile(file, config))), Effect.tap((files) => Effect.logDebug(`${files.length} package.json files found`)));
7
+ return pipe(getFilePaths(io, config), Effect.flatMap(filePaths => Effect.all(filePaths.map(filePath => readJsonFileSync(io, filePath)))), Effect.map(files => files.map(file => new PackageJsonFile(file, config))), Effect.tap(files => Effect.logDebug(`${files.length} package.json files found`)));
8
8
  }
@@ -42,6 +42,13 @@ export declare class PackageJsonFile {
42
42
  jsonFile: JsonFile<PackageJson>;
43
43
  /** the .name property from the package.json file */
44
44
  name: string | undefined;
45
+ /** the next package.json file contents after modification, with formatting preserved */
46
+ nextJson: string;
45
47
  constructor(jsonFile: JsonFile<PackageJson>, config: Ctx['config']);
46
48
  getInstances(enabledTypes: Strategy.Any[]): Effect.Effect<Instance[]>;
49
+ /**
50
+ * Apply an edit to the raw JSON string which will be written to disk. This string preserves the
51
+ * original formatting of the file.
52
+ */
53
+ applyEdit(fullPath: string[], value: string | undefined): void;
47
54
  }
@@ -1,4 +1,5 @@
1
1
  import { Effect, pipe } from 'effect';
2
+ import { applyEdits, modify } from 'jsonc-parser';
2
3
  import { Instance } from '../get-instances/instance.js';
3
4
  export class PackageJsonFile {
4
5
  /** resolved configuration */
@@ -9,21 +10,35 @@ export class PackageJsonFile {
9
10
  jsonFile;
10
11
  /** the .name property from the package.json file */
11
12
  name;
13
+ /** the next package.json file contents after modification, with formatting preserved */
14
+ nextJson;
12
15
  constructor(jsonFile, config) {
13
16
  this._instances = null;
14
17
  this.config = config;
15
18
  this.jsonFile = jsonFile;
16
19
  this.name = jsonFile.contents.name;
20
+ this.nextJson = jsonFile.json;
17
21
  }
18
22
  getInstances(enabledTypes) {
19
23
  if (!this._instances) {
20
- return pipe(Effect.all(enabledTypes.map((strategy) => pipe(strategy.read(this), Effect.map((entries) => entries.map(([name, rawSpecifier]) => new Instance(name, rawSpecifier, this, strategy)))))), Effect.map((array) => array.flat()), Effect.tapBoth({
21
- onSuccess: (instances) => Effect.logDebug(`found ${instances.length} instances in <${this.jsonFile.shortPath}>`),
24
+ return pipe(Effect.all(enabledTypes.map(strategy => pipe(strategy.read(this), Effect.map(entries => entries.map(([name, rawSpecifier]) => new Instance(name, rawSpecifier, this, strategy)))))), Effect.map(array => array.flat()), Effect.tapBoth({
25
+ onSuccess: instances => Effect.logDebug(`found ${instances.length} instances in <${this.jsonFile.shortPath}>`),
22
26
  onFailure: () => Effect.logError(`failed to get instances from <${this.jsonFile.shortPath}>`),
23
- }), Effect.catchAll(() => Effect.succeed([])), Effect.tap((instances) => Effect.sync(() => {
27
+ }), Effect.catchAll(() => Effect.succeed([])), Effect.tap(instances => Effect.sync(() => {
24
28
  this._instances = instances;
25
29
  })));
26
30
  }
27
31
  return Effect.succeed(this._instances);
28
32
  }
33
+ /**
34
+ * Apply an edit to the raw JSON string which will be written to disk. This string preserves the
35
+ * original formatting of the file.
36
+ */
37
+ applyEdit(fullPath, value) {
38
+ const edits = modify(this.nextJson, fullPath.map(parseNumericStrings), value, {});
39
+ this.nextJson = applyEdits(this.nextJson, edits);
40
+ }
41
+ }
42
+ function parseNumericStrings(key) {
43
+ return /[^0-9]/.test(key) ? key : Number(key);
29
44
  }
@@ -10,19 +10,19 @@ export function canAddToGroup(packageJsonFilesByName, group, instance) {
10
10
  }
11
11
  function matchesDependencies(packageJsonFilesByName, group, dependencies, instance) {
12
12
  // matches if not defined
13
- if (!isNonEmptyArray(dependencies))
13
+ if (!isNonEmptyArray(dependencies)) {
14
14
  return true;
15
- return dependencies.some((pattern) => (pattern === '$LOCAL' &&
15
+ }
16
+ return dependencies.some(pattern => (pattern === '$LOCAL' &&
16
17
  instance.name in packageJsonFilesByName &&
17
- ((group.groupType === 'versionGroup' && instance.versionGroup === null) ||
18
- (group.groupType === 'semverGroup' && instance.semverGroup === null))) ||
18
+ ((group.groupType === 'versionGroup' &&
19
+ instance.versionGroup === null) ||
20
+ (group.groupType === 'semverGroup' &&
21
+ instance.semverGroup === null))) ||
19
22
  minimatch(instance.name, pattern));
20
23
  }
21
24
  function matchesPackages(packages, instance) {
22
- // matches if not defined
23
- if (!isNonEmptyArray(packages))
24
- return true;
25
- return packages.some((pattern) => minimatch(instance.pkgName, pattern));
25
+ return matchesKnownList(packages, instance.pkgName);
26
26
  }
27
27
  function matchesDependencyTypes(dependencyTypes, instance) {
28
28
  return matchesKnownList(dependencyTypes, instance.strategy.name);
@@ -32,13 +32,15 @@ function matchesSpecifierTypes(specifierTypes, instance) {
32
32
  }
33
33
  function matchesKnownList(values, value) {
34
34
  // matches if not defined
35
- if (!isNonEmptyArray(values))
35
+ if (!isNonEmptyArray(values)) {
36
36
  return true;
37
- if (values.join('') === '**')
37
+ }
38
+ if (values.join('') === '**') {
38
39
  return true;
40
+ }
39
41
  const negative = [];
40
42
  const positive = [];
41
- values.forEach((name) => {
43
+ values.forEach(name => {
42
44
  if (name.startsWith('!')) {
43
45
  negative.push(name.replace('!', ''));
44
46
  }
@@ -46,7 +48,11 @@ function matchesKnownList(values, value) {
46
48
  positive.push(name);
47
49
  }
48
50
  });
49
- if (isNonEmptyArray(negative) && !negative.includes(value))
51
+ if (isNonEmptyArray(negative) && !someMinimatch(value, negative)) {
50
52
  return true;
51
- return isNonEmptyArray(positive) && positive.includes(value);
53
+ }
54
+ return isNonEmptyArray(positive) && someMinimatch(value, positive);
55
+ }
56
+ function someMinimatch(value, patterns) {
57
+ return patterns.some(pattern => minimatch(value, pattern));
52
58
  }
@@ -9,5 +9,7 @@ export function isSemver(version) {
9
9
  const minor = new RegExp(`^${range}${ints}${dot}${intsOrX}$`);
10
10
  const patch = new RegExp(`^${range}${ints}${dot}${intsOrX}${dot}${intsOrX}$`);
11
11
  return (isString(version) &&
12
- (version.search(major) !== -1 || version.search(minor) !== -1 || version.search(patch) !== -1));
12
+ (version.search(major) !== -1 ||
13
+ version.search(minor) !== -1 ||
14
+ version.search(patch) !== -1));
13
15
  }
@@ -3,7 +3,7 @@ import { IoTag } from './index.js';
3
3
  class AskForChoiceError extends Data.TaggedClass('AskForChoiceError') {
4
4
  }
5
5
  export function askForChoice(opts) {
6
- return pipe(IoTag, Effect.flatMap((io) => Effect.tryPromise({
6
+ return pipe(IoTag, Effect.flatMap(io => Effect.tryPromise({
7
7
  try: () => io.enquirer
8
8
  .prompt({
9
9
  type: 'select',
@@ -11,7 +11,7 @@ export function askForChoice(opts) {
11
11
  message: opts.message,
12
12
  choices: opts.choices,
13
13
  })
14
- .then((res) => res.choice),
15
- catch: (err) => new AskForChoiceError({ error: String(err) }),
14
+ .then(res => res.choice),
15
+ catch: err => new AskForChoiceError({ error: String(err) }),
16
16
  })));
17
17
  }
@@ -3,12 +3,12 @@ import { IoTag } from './index.js';
3
3
  class AskForInputError extends Data.TaggedClass('AskForInputError') {
4
4
  }
5
5
  export function askForInput(opts) {
6
- return pipe(IoTag, Effect.flatMap((io) => Effect.tryPromise({
6
+ return pipe(IoTag, Effect.flatMap(io => Effect.tryPromise({
7
7
  try: () => io.enquirer.prompt({
8
8
  name: 'version',
9
9
  type: 'input',
10
10
  message: opts.message,
11
11
  }),
12
- catch: (err) => new AskForInputError({ error: String(err) }),
12
+ catch: err => new AskForInputError({ error: String(err) }),
13
13
  })));
14
14
  }
@@ -1,7 +1,7 @@
1
1
  import { Effect, pipe } from 'effect';
2
2
  import { IoTag } from './index.js';
3
3
  export function exitIfInvalid(ctx) {
4
- return pipe(IoTag, Effect.tap((io) => Effect.sync(() => {
4
+ return pipe(IoTag, Effect.tap(io => Effect.sync(() => {
5
5
  if (ctx.isInvalid) {
6
6
  io.process.exit(1);
7
7
  }
@@ -9,6 +9,6 @@ export function globSync(io, patterns) {
9
9
  fs: io.fs,
10
10
  ignore: ['**/node_modules/**'],
11
11
  }),
12
- catch: (err) => new GlobError({ error: String(err) }),
12
+ catch: err => new GlobError({ error: String(err) }),
13
13
  });
14
14
  }
@@ -1,8 +1,7 @@
1
- /// <reference types="node" resolution-mode="require"/>
1
+ import fs from 'node:fs';
2
2
  import { cosmiconfig } from 'cosmiconfig';
3
3
  import { Context } from 'effect';
4
4
  import enquirer from 'enquirer';
5
- import fs from 'fs';
6
5
  import { globbySync } from 'globby';
7
6
  import { sync as readYamlFileSync } from 'read-yaml-file';
8
7
  export interface Io {
package/dist/io/index.js CHANGED
@@ -1,7 +1,7 @@
1
+ import fs from 'node:fs';
1
2
  import { cosmiconfig } from 'cosmiconfig';
2
3
  import { Context } from 'effect';
3
4
  import enquirer from 'enquirer';
4
- import fs from 'fs';
5
5
  import { globbySync } from 'globby';
6
6
  import { sync as readYamlFileSync } from 'read-yaml-file';
7
7
  export const IoTag = Context.GenericTag('@services/IoTag');
@@ -1,10 +1,12 @@
1
+ import { join } from 'node:path';
1
2
  import { Effect, Option, pipe } from 'effect';
2
- import { join } from 'path';
3
3
  import { isNonEmptyObject } from 'tightrope/guard/is-non-empty-object.js';
4
4
  import { readJsonFileSync } from './read-json-file-sync.js';
5
5
  const getOptionOfNonEmptyObject = Option.liftPredicate((isNonEmptyObject));
6
6
  export function readConfigFile(io, configPath) {
7
- return pipe(Effect.try(() => io.cosmiconfig.cosmiconfig('syncpack')), Effect.flatMap((client) => Effect.tryPromise(() => (configPath ? client.load(configPath) : client.search()))), Effect.flatMap((result) => result !== null ? getValueFromCosmiconfig(result) : findConfigInPackageJson(io)), Effect.tap((config) => Effect.logDebug(`config file found: ${JSON.stringify(config)}`)), Effect.tapError(() => Effect.logDebug('no config file found, will use defaults')), Effect.catchAll(() => Effect.succeed({})));
7
+ return pipe(Effect.try(() => io.cosmiconfig.cosmiconfig('syncpack')), Effect.flatMap(client => Effect.tryPromise(() => configPath ? client.load(configPath) : client.search())), Effect.flatMap(result => result !== null
8
+ ? getValueFromCosmiconfig(result)
9
+ : findConfigInPackageJson(io)), Effect.tap(config => Effect.logDebug(`config file found: ${JSON.stringify(config)}`)), Effect.tapError(() => Effect.logDebug('no config file found, will use defaults')), Effect.catchAll(() => Effect.succeed({})));
8
10
  }
9
11
  /**
10
12
  * Look for a .config.syncpack property in the root package.json.
@@ -18,5 +20,5 @@ function findConfigInPackageJson(io) {
18
20
  }
19
21
  /** Extract the value from a successful search by cosmiconfig */
20
22
  function getValueFromCosmiconfig(result) {
21
- return pipe(Effect.succeed(result), Effect.tap((result) => Effect.logDebug(`cosmiconfig found ${result.filepath}`)), Effect.flatMap((result) => getOptionOfNonEmptyObject(result.config)));
23
+ return pipe(Effect.succeed(result), Effect.tap(result => Effect.logDebug(`cosmiconfig found ${result.filepath}`)), Effect.flatMap(result => getOptionOfNonEmptyObject(result.config)));
22
24
  }
@@ -4,6 +4,6 @@ export class ReadFileError extends Data.TaggedClass('ReadFileError') {
4
4
  export function readFileSync(io, filePath) {
5
5
  return Effect.try({
6
6
  try: () => io.fs.readFileSync(filePath, { encoding: 'utf8' }),
7
- catch: (err) => new ReadFileError({ filePath, error: String(err) }),
7
+ catch: err => new ReadFileError({ filePath, error: String(err) }),
8
8
  });
9
9
  }
@@ -1,11 +1,12 @@
1
1
  import { Effect } from 'effect';
2
+ import { type ParseError } from 'jsonc-parser';
2
3
  import type { Io } from './index.js';
3
4
  import type { ReadFileError } from './read-file-sync.js';
4
5
  declare const JsonParseError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => Readonly<A> & {
5
6
  readonly _tag: "JsonParseError";
6
7
  };
7
8
  export declare class JsonParseError extends JsonParseError_base<{
8
- readonly error: unknown;
9
+ readonly errors: ParseError[];
9
10
  readonly filePath: string;
10
11
  readonly json: string;
11
12
  }> {
@@ -1,15 +1,19 @@
1
+ import { dirname, relative } from 'node:path';
1
2
  import { Data, Effect, pipe } from 'effect';
2
- import { dirname, relative } from 'path';
3
+ import { parse } from 'jsonc-parser';
3
4
  import { readFileSync } from './read-file-sync.js';
4
5
  export class JsonParseError extends Data.TaggedClass('JsonParseError') {
5
6
  }
6
7
  export class JsonFile extends Data.TaggedClass('JsonFile') {
7
8
  }
8
9
  export function readJsonFileSync(io, filePath) {
9
- return pipe(Effect.Do, Effect.bind('json', () => readFileSync(io, filePath)), Effect.bind('contents', ({ json }) => Effect.try({
10
- try: () => JSON.parse(json),
11
- catch: (error) => new JsonParseError({ error, filePath, json }),
12
- })), Effect.map(({ contents, json }) => new JsonFile({
10
+ return pipe(Effect.Do, Effect.bind('json', () => readFileSync(io, filePath)), Effect.bind('contents', ({ json }) => {
11
+ const errors = [];
12
+ const data = parse(json, errors);
13
+ return errors.length === 0
14
+ ? Effect.succeed(data)
15
+ : Effect.fail(new JsonParseError({ errors, filePath, json }));
16
+ }), Effect.map(({ contents, json }) => new JsonFile({
13
17
  contents,
14
18
  dirPath: dirname(filePath),
15
19
  filePath,
@@ -4,6 +4,6 @@ class ReadYamlFileError extends Data.TaggedClass('ReadYamlFileError') {
4
4
  export function readYamlFileSync(io, filePath) {
5
5
  return Effect.try({
6
6
  try: () => io.readYamlFile.sync(filePath),
7
- catch: (err) => new ReadYamlFileError({ filePath, error: String(err) }),
7
+ catch: err => new ReadYamlFileError({ filePath, error: String(err) }),
8
8
  });
9
9
  }
@@ -5,5 +5,5 @@ export declare const newlines: {
5
5
  detect(source: string): Ending;
6
6
  fix(source: string, lineEnding: Ending): string;
7
7
  };
8
- export declare function toJson(ctx: Ctx, file: PackageJsonFile): string;
8
+ export declare function toFormattedJson(ctx: Ctx, file: PackageJsonFile): string;
9
9
  export {};
@@ -1,4 +1,4 @@
1
- import { EOL } from 'os';
1
+ import { EOL } from 'node:os';
2
2
  import { getIndent } from '../config/get-indent.js';
3
3
  const CR = '\r';
4
4
  const CRLF = '\r\n';
@@ -8,22 +8,25 @@ export const newlines = {
8
8
  const cr = source.split(CR).length;
9
9
  const lf = source.split(LF).length;
10
10
  const crlf = source.split(CRLF).length;
11
- if (cr + lf === 0)
11
+ if (cr + lf === 0) {
12
12
  return EOL;
13
- if (crlf === cr && crlf === lf)
13
+ }
14
+ if (crlf === cr && crlf === lf) {
14
15
  return CRLF;
15
- if (cr > lf)
16
+ }
17
+ if (cr > lf) {
16
18
  return CR;
19
+ }
17
20
  return LF;
18
21
  },
19
22
  fix(source, lineEnding) {
20
23
  return source.replace(/\r\n|\n|\r/g, lineEnding);
21
24
  },
22
25
  };
23
- export function toJson(ctx, file) {
26
+ export function toFormattedJson(ctx, file) {
24
27
  const contents = file.jsonFile.contents;
25
28
  const indent = getIndent(ctx.config);
26
- const EOL = newlines.detect(file.jsonFile.json);
27
- const source = `${JSON.stringify(contents, null, indent)}${EOL}`;
28
- return newlines.fix(source, EOL);
29
+ const eol = newlines.detect(file.jsonFile.json);
30
+ const source = `${JSON.stringify(contents, null, indent)}${eol}`;
31
+ return newlines.fix(source, eol);
29
32
  }