repopo 0.7.0 → 0.8.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 (102) hide show
  1. package/CHANGELOG.md +187 -6
  2. package/THIRD-PARTY-LICENSES.txt +149 -3
  3. package/esm/adapters/fluidFramework.d.ts +103 -0
  4. package/esm/adapters/fluidFramework.d.ts.map +1 -0
  5. package/esm/adapters/fluidFramework.js +119 -0
  6. package/esm/adapters/fluidFramework.js.map +1 -0
  7. package/esm/baseCommand.d.ts +1 -0
  8. package/esm/baseCommand.d.ts.map +1 -1
  9. package/esm/baseCommand.js +2 -1
  10. package/esm/baseCommand.js.map +1 -1
  11. package/esm/commands/check.d.ts.map +1 -1
  12. package/esm/commands/check.js +9 -8
  13. package/esm/commands/check.js.map +1 -1
  14. package/esm/index.d.ts +1 -0
  15. package/esm/index.d.ts.map +1 -1
  16. package/esm/index.js +1 -0
  17. package/esm/index.js.map +1 -1
  18. package/esm/perf.d.ts +4 -1
  19. package/esm/perf.d.ts.map +1 -1
  20. package/esm/perf.js +3 -2
  21. package/esm/perf.js.map +1 -1
  22. package/esm/policies/LicenseFileExists.d.ts.map +1 -1
  23. package/esm/policies/LicenseFileExists.js +3 -1
  24. package/esm/policies/LicenseFileExists.js.map +1 -1
  25. package/esm/policies/NoJsFileExtensions.d.ts.map +1 -1
  26. package/esm/policies/NoJsFileExtensions.js +4 -1
  27. package/esm/policies/NoJsFileExtensions.js.map +1 -1
  28. package/esm/policies/NoLargeBinaryFiles.d.ts.map +1 -1
  29. package/esm/policies/NoLargeBinaryFiles.js +3 -1
  30. package/esm/policies/NoLargeBinaryFiles.js.map +1 -1
  31. package/esm/policies/NoPrivateWorkspaceDependencies.d.ts +37 -0
  32. package/esm/policies/NoPrivateWorkspaceDependencies.d.ts.map +1 -0
  33. package/esm/policies/NoPrivateWorkspaceDependencies.js +137 -0
  34. package/esm/policies/NoPrivateWorkspaceDependencies.js.map +1 -0
  35. package/esm/policies/PackageAllowedScopes.d.ts +88 -0
  36. package/esm/policies/PackageAllowedScopes.d.ts.map +1 -0
  37. package/esm/policies/PackageAllowedScopes.js +113 -0
  38. package/esm/policies/PackageAllowedScopes.js.map +1 -0
  39. package/esm/policies/PackageEsmType.d.ts +100 -0
  40. package/esm/policies/PackageEsmType.d.ts.map +1 -0
  41. package/esm/policies/PackageEsmType.js +257 -0
  42. package/esm/policies/PackageEsmType.js.map +1 -0
  43. package/esm/policies/PackageFolderName.d.ts +97 -0
  44. package/esm/policies/PackageFolderName.d.ts.map +1 -0
  45. package/esm/policies/PackageFolderName.js +104 -0
  46. package/esm/policies/PackageFolderName.js.map +1 -0
  47. package/esm/policies/PackageJsonProperties.d.ts.map +1 -1
  48. package/esm/policies/PackageJsonProperties.js +2 -1
  49. package/esm/policies/PackageJsonProperties.js.map +1 -1
  50. package/esm/policies/PackageJsonRepoDirectoryProperty.d.ts.map +1 -1
  51. package/esm/policies/PackageJsonRepoDirectoryProperty.js +4 -3
  52. package/esm/policies/PackageJsonRepoDirectoryProperty.js.map +1 -1
  53. package/esm/policies/PackageJsonSorted.d.ts.map +1 -1
  54. package/esm/policies/PackageJsonSorted.js +11 -3
  55. package/esm/policies/PackageJsonSorted.js.map +1 -1
  56. package/esm/policies/PackageLicense.d.ts +61 -0
  57. package/esm/policies/PackageLicense.d.ts.map +1 -0
  58. package/esm/policies/PackageLicense.js +131 -0
  59. package/esm/policies/PackageLicense.js.map +1 -0
  60. package/esm/policies/PackagePrivateField.d.ts +124 -0
  61. package/esm/policies/PackagePrivateField.d.ts.map +1 -0
  62. package/esm/policies/PackagePrivateField.js +155 -0
  63. package/esm/policies/PackagePrivateField.js.map +1 -0
  64. package/esm/policies/PackageReadme.d.ts +77 -0
  65. package/esm/policies/PackageReadme.d.ts.map +1 -0
  66. package/esm/policies/PackageReadme.js +209 -0
  67. package/esm/policies/PackageReadme.js.map +1 -0
  68. package/esm/policies/PackageScripts.d.ts +180 -2
  69. package/esm/policies/PackageScripts.d.ts.map +1 -1
  70. package/esm/policies/PackageScripts.js +349 -29
  71. package/esm/policies/PackageScripts.js.map +1 -1
  72. package/esm/policies/PackageTestScripts.d.ts +100 -0
  73. package/esm/policies/PackageTestScripts.d.ts.map +1 -0
  74. package/esm/policies/PackageTestScripts.js +149 -0
  75. package/esm/policies/PackageTestScripts.js.map +1 -0
  76. package/esm/policies/RequiredGitignorePatterns.d.ts.map +1 -1
  77. package/esm/policies/RequiredGitignorePatterns.js +10 -4
  78. package/esm/policies/RequiredGitignorePatterns.js.map +1 -1
  79. package/esm/policies.d.ts +9 -1
  80. package/esm/policies.d.ts.map +1 -1
  81. package/esm/policies.js +10 -1
  82. package/esm/policies.js.map +1 -1
  83. package/esm/policy.d.ts +5 -1
  84. package/esm/policy.d.ts.map +1 -1
  85. package/esm/policy.js.map +1 -1
  86. package/esm/policyDefiners/defineFileHeaderPolicy.d.ts.map +1 -1
  87. package/esm/policyDefiners/defineFileHeaderPolicy.js +2 -1
  88. package/esm/policyDefiners/defineFileHeaderPolicy.js.map +1 -1
  89. package/esm/policyDefiners/definePackagePolicy.d.ts.map +1 -1
  90. package/esm/policyDefiners/definePackagePolicy.js +2 -1
  91. package/esm/policyDefiners/definePackagePolicy.js.map +1 -1
  92. package/esm/tsdoc-metadata.json +1 -1
  93. package/esm/utils/indentation.d.ts +73 -0
  94. package/esm/utils/indentation.d.ts.map +1 -0
  95. package/esm/utils/indentation.js +97 -0
  96. package/esm/utils/indentation.js.map +1 -0
  97. package/esm/utils/packageName.d.ts +71 -0
  98. package/esm/utils/packageName.d.ts.map +1 -0
  99. package/esm/utils/packageName.js +79 -0
  100. package/esm/utils/packageName.js.map +1 -0
  101. package/oclif.manifest.json +1 -1
  102. package/package.json +30 -15
@@ -1,3 +1,64 @@
1
+ /**
2
+ * Configuration for validating that a script's body contains required substrings.
3
+ *
4
+ * @alpha
5
+ */
6
+ export interface ScriptMustContainRule {
7
+ /**
8
+ * The name of the script to validate.
9
+ */
10
+ script: string;
11
+ /**
12
+ * Substrings that must appear in the script body.
13
+ * All substrings must be present for the script to pass validation.
14
+ */
15
+ mustContain: string[];
16
+ }
17
+ /**
18
+ * Configuration for conditional script requirements.
19
+ *
20
+ * @alpha
21
+ */
22
+ export interface ConditionalScriptRule {
23
+ /**
24
+ * The script that triggers the requirement.
25
+ * If this script exists, then `requires` must also exist.
26
+ */
27
+ ifPresent: string;
28
+ /**
29
+ * Scripts that must be present if `ifPresent` script exists.
30
+ *
31
+ * @remarks
32
+ *
33
+ * Each entry can be:
34
+ * - A string: Script must exist, no auto-fix available
35
+ * - An object `{ scriptName: "default value" }`: Script must exist, auto-fix adds the
36
+ * default value if missing
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * requires: ["test", { clean: "rimraf dist" }]
41
+ * ```
42
+ */
43
+ requires: RequiredScriptEntry[];
44
+ }
45
+ /**
46
+ * A required script entry. Can be either:
47
+ * - A string (script name only, no default value)
48
+ * - An object with a single key-value pair (script name → default value)
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * // Script must exist, no auto-fix available
53
+ * "test"
54
+ *
55
+ * // Script must exist, auto-fix will add this value if missing
56
+ * { lint: "biome lint ." }
57
+ * ```
58
+ *
59
+ * @alpha
60
+ */
61
+ export type RequiredScriptEntry = string | Record<string, string>;
1
62
  /**
2
63
  * Policy settings for the PackageScripts repo policy.
3
64
  *
@@ -5,9 +66,43 @@
5
66
  */
6
67
  export interface PackageScriptsSettings {
7
68
  /**
8
- * List of script names that must be present in package.json scripts.
69
+ * Scripts that must be present in package.json.
70
+ *
71
+ * @remarks
72
+ *
73
+ * Each entry can be:
74
+ * - A string: Script must exist, no auto-fix available, content not validated
75
+ * - An object `{ scriptName: "default value" }`: Script must exist, auto-fix adds the
76
+ * default value if missing, but existing scripts with different content still pass
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * must: [
81
+ * "test", // Must exist, no auto-fix
82
+ * { lint: "biome lint ." }, // Must exist, auto-fix available
83
+ * { clean: "rimraf dist esm" }, // Must exist, auto-fix available
84
+ * ]
85
+ * ```
86
+ */
87
+ must?: RequiredScriptEntry[];
88
+ /**
89
+ * Scripts that must exist AND have exact content.
90
+ *
91
+ * @remarks
92
+ *
93
+ * Unlike {@link PackageScriptsSettings.must}, scripts in `exact` are validated against
94
+ * the specified value. If a script exists but has different content, it fails validation.
95
+ * Both missing and mismatched scripts are auto-fixable.
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * exact: {
100
+ * clean: "rimraf dist esm",
101
+ * format: "biome format --write .",
102
+ * }
103
+ * ```
9
104
  */
10
- must?: string[];
105
+ exact?: Record<string, string>;
11
106
  /**
12
107
  * List of mutually exclusive script groups. Each group is an array of script names
13
108
  * where at most one script from the group can be present.
@@ -16,14 +111,97 @@ export interface PackageScriptsSettings {
16
111
  * multiple scripts from the same group.
17
112
  *
18
113
  * @example
114
+ * ```typescript
19
115
  * // Allow "test:unit" OR "test:vitest" OR neither, but not both
20
116
  * mutuallyExclusive: [["test:unit", "test:vitest"]]
117
+ * ```
21
118
  */
22
119
  mutuallyExclusive?: string[][];
120
+ /**
121
+ * Conditional script requirements. If a script is present, other scripts must also exist.
122
+ *
123
+ * @remarks
124
+ *
125
+ * This is useful for enforcing that packages with a "build" script also have a "clean" script,
126
+ * or packages with "test" also have "test:coverage".
127
+ *
128
+ * The `requires` field accepts the same format as {@link PackageScriptsSettings.must}:
129
+ * - Strings: Script must exist, no auto-fix available
130
+ * - Objects `{ scriptName: "default" }`: Script must exist, auto-fix adds the default if missing
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * // If "build" exists, "clean" must also exist (with auto-fix default)
135
+ * conditionalRequired: [{ ifPresent: "build", requires: [{ clean: "rimraf dist" }] }]
136
+ *
137
+ * // Mixed: "test" required without default, "clean" with default
138
+ * conditionalRequired: [{ ifPresent: "build", requires: ["test", { clean: "rimraf dist" }] }]
139
+ * ```
140
+ */
141
+ conditionalRequired?: ConditionalScriptRule[];
142
+ /**
143
+ * Rules for validating script body content.
144
+ *
145
+ * @remarks
146
+ *
147
+ * This allows you to ensure that specific scripts contain required commands or patterns
148
+ * without requiring an exact match. For example, requiring that the "clean" script uses
149
+ * "rimraf" or that "build" includes "tsc".
150
+ *
151
+ * For exact content matching, use {@link PackageScriptsSettings.exact} instead.
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * scriptMustContain: [
156
+ * { script: "clean", mustContain: ["rimraf"] },
157
+ * { script: "build", mustContain: ["tsc"] },
158
+ * ]
159
+ * ```
160
+ */
161
+ scriptMustContain?: ScriptMustContainRule[];
23
162
  }
24
163
  /**
25
164
  * A RepoPolicy that validates package.json scripts based on configurable rules.
26
165
  *
166
+ * @remarks
167
+ *
168
+ * This policy provides comprehensive script validation including:
169
+ * - Required scripts that must be present (with optional defaults for auto-fix)
170
+ * - Exact script content enforcement
171
+ * - Mutually exclusive script groups (at most one can exist)
172
+ * - Conditional requirements (if X exists, Y must also exist)
173
+ * - Script body content validation (script must contain specific substrings)
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * import { makePolicy } from "repopo";
178
+ * import { PackageScripts } from "repopo/policies";
179
+ *
180
+ * const config: RepopoConfig = {
181
+ * policies: [
182
+ * makePolicy(PackageScripts, {
183
+ * // Scripts that must exist
184
+ * must: [
185
+ * "test", // Must exist, no auto-fix
186
+ * { build: "tsc" }, // Must exist, auto-fix available
187
+ * { lint: "biome lint ." }, // Must exist, auto-fix available
188
+ * ],
189
+ * // Scripts that must exist AND match exactly
190
+ * exact: {
191
+ * clean: "rimraf dist esm",
192
+ * format: "biome format --write .",
193
+ * },
194
+ * // If "build" exists, "clean" must also exist (with inline auto-fix default)
195
+ * conditionalRequired: [{ ifPresent: "build", requires: [{ clean: "rimraf dist" }] }],
196
+ * // Script body must contain these substrings
197
+ * scriptMustContain: [{ script: "build", mustContain: ["tsc"] }],
198
+ * // Only one of these can exist
199
+ * mutuallyExclusive: [["lint:eslint", "lint:biome"]],
200
+ * }),
201
+ * ],
202
+ * };
203
+ * ```
204
+ *
27
205
  * @alpha
28
206
  */
29
207
  export declare const PackageScripts: import("../policy.js").PolicyDefinition<PackageScriptsSettings | undefined>;
@@ -1 +1 @@
1
- {"version":3,"file":"PackageScripts.d.ts","sourceRoot":"","sources":["../../src/policies/PackageScripts.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAEhB;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;CAC/B;AAgDD;;;;GAIG;AACH,eAAO,MAAM,cAAc,6EAoCzB,CAAC"}
1
+ {"version":3,"file":"PackageScripts.d.ts","sourceRoot":"","sources":["../../src/policies/PackageScripts.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElE;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAE7B;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE/B;;;;;;;;;;;;OAYG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;IAE/B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,mBAAmB,CAAC,EAAE,qBAAqB,EAAE,CAAC;IAE9C;;;;;;;;;;;;;;;;;;OAkBG;IACH,iBAAiB,CAAC,EAAE,qBAAqB,EAAE,CAAC;CAC5C;AAseD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,cAAc,6EA8DzB,CAAC"}
@@ -1,18 +1,54 @@
1
+ import jsonfile from "jsonfile";
1
2
  import { definePackagePolicy } from "../policyDefiners/definePackagePolicy.js";
3
+ const POLICY_NAME = "PackageScripts";
2
4
  /**
3
- * Validates required scripts and returns error messages for missing scripts.
5
+ * Extracts script name and optional default value from a RequiredScriptEntry.
4
6
  */
5
- function validateRequiredScripts(must, scripts) {
6
- const missingScripts = [];
7
- for (const scriptName of must) {
8
- if (!(scripts && Object.hasOwn(scripts, scriptName))) {
9
- missingScripts.push(scriptName);
7
+ function parseRequiredEntry(entry) {
8
+ if (typeof entry === "string") {
9
+ return { name: entry };
10
+ }
11
+ const keys = Object.keys(entry);
12
+ if (keys.length !== 1) {
13
+ throw new Error(`Invalid required script entry: expected single key-value pair, got ${keys.length} keys`);
14
+ }
15
+ // biome-ignore lint/style/noNonNullAssertion: we verified length is 1
16
+ const name = keys[0];
17
+ const defaultValue = entry[name];
18
+ // Only include defaultValue if it's defined (exactOptionalPropertyTypes)
19
+ return defaultValue !== undefined ? { name, defaultValue } : { name };
20
+ }
21
+ /**
22
+ * Extract defaults from RequiredScriptEntry array.
23
+ */
24
+ function extractDefaultsFromEntries(entries, defaults) {
25
+ for (const entry of entries) {
26
+ const { name, defaultValue } = parseRequiredEntry(entry);
27
+ if (defaultValue !== undefined) {
28
+ defaults[name] = defaultValue;
10
29
  }
11
30
  }
12
- if (missingScripts.length > 0) {
13
- return [`Missing required scripts:\n\t${missingScripts.join("\n\t")}`];
31
+ }
32
+ /**
33
+ * Builds a map of script names to their default values from must, conditionalRequired, and exact configs.
34
+ */
35
+ function buildDefaultsMap(config) {
36
+ const defaults = {};
37
+ // Add defaults from must entries
38
+ if (config.must) {
39
+ extractDefaultsFromEntries(config.must, defaults);
14
40
  }
15
- return [];
41
+ // Add defaults from conditionalRequired entries
42
+ if (config.conditionalRequired) {
43
+ for (const rule of config.conditionalRequired) {
44
+ extractDefaultsFromEntries(rule.requires, defaults);
45
+ }
46
+ }
47
+ // Add defaults from exact (these override must/conditionalRequired if both specified)
48
+ if (config.exact) {
49
+ Object.assign(defaults, config.exact);
50
+ }
51
+ return defaults;
16
52
  }
17
53
  /**
18
54
  * Validates mutually exclusive script groups and returns error messages for violations.
@@ -28,35 +64,319 @@ function validateMutuallyExclusiveScripts(groups, scripts) {
28
64
  }
29
65
  return errors;
30
66
  }
67
+ /**
68
+ * Validates conditional script requirements (if X exists, Y must also exist).
69
+ */
70
+ function validateConditionalRequirements(rules, scripts) {
71
+ const errors = [];
72
+ for (const rule of rules) {
73
+ // Check if the triggering script exists
74
+ if (scripts && Object.hasOwn(scripts, rule.ifPresent)) {
75
+ // Find missing required scripts
76
+ const missingRequired = [];
77
+ for (const entry of rule.requires) {
78
+ const { name } = parseRequiredEntry(entry);
79
+ if (!Object.hasOwn(scripts, name)) {
80
+ missingRequired.push(name);
81
+ }
82
+ }
83
+ if (missingRequired.length > 0) {
84
+ errors.push(`Script "${rule.ifPresent}" is present, but required companion script(s) missing: ${missingRequired.join(", ")}`);
85
+ }
86
+ }
87
+ }
88
+ return errors;
89
+ }
90
+ /**
91
+ * Validates that script bodies contain required substrings.
92
+ */
93
+ function validateScriptContents(rules, scripts) {
94
+ const errors = [];
95
+ for (const rule of rules) {
96
+ // Only validate if the script exists
97
+ if (scripts && Object.hasOwn(scripts, rule.script)) {
98
+ const scriptBody = scripts[rule.script];
99
+ if (typeof scriptBody === "string") {
100
+ const missingSubstrings = rule.mustContain.filter((substring) => !scriptBody.includes(substring));
101
+ if (missingSubstrings.length > 0) {
102
+ errors.push(`Script "${rule.script}" must contain: ${missingSubstrings.join(", ")}`);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ return errors;
108
+ }
109
+ /**
110
+ * Validates required scripts from 'must' config.
111
+ */
112
+ function validateMustScripts(must, scripts) {
113
+ const missingScripts = [];
114
+ const missingWithDefaults = [];
115
+ for (const entry of must) {
116
+ const { name, defaultValue } = parseRequiredEntry(entry);
117
+ if (!(scripts && Object.hasOwn(scripts, name))) {
118
+ missingScripts.push(name);
119
+ if (defaultValue !== undefined) {
120
+ missingWithDefaults.push(name);
121
+ }
122
+ }
123
+ }
124
+ const errors = [];
125
+ if (missingScripts.length > 0) {
126
+ errors.push(`Missing required scripts:\n\t${missingScripts.join("\n\t")}`);
127
+ }
128
+ return { errors, missingWithDefaults };
129
+ }
130
+ /**
131
+ * Validates scripts from 'exact' config - must exist AND match exactly.
132
+ */
133
+ function validateExactScripts(exact, scripts) {
134
+ const errors = [];
135
+ const missingWithDefaults = [];
136
+ const mismatchedScripts = [];
137
+ for (const [scriptName, expectedValue] of Object.entries(exact)) {
138
+ if (scripts && Object.hasOwn(scripts, scriptName)) {
139
+ const actualValue = scripts[scriptName];
140
+ if (actualValue !== expectedValue) {
141
+ mismatchedScripts.push(scriptName);
142
+ errors.push(`Script "${scriptName}" must be "${expectedValue}", but found "${actualValue}"`);
143
+ }
144
+ }
145
+ else {
146
+ missingWithDefaults.push(scriptName);
147
+ errors.push(`Missing required script: ${scriptName}`);
148
+ }
149
+ }
150
+ return { errors, missingWithDefaults, mismatchedScripts };
151
+ }
152
+ /**
153
+ * Find missing scripts from a conditional rule's requirements.
154
+ */
155
+ function findMissingFromRule(rule, scripts) {
156
+ return rule.requires
157
+ .map((entry) => parseRequiredEntry(entry).name)
158
+ .filter((name) => !(scripts && Object.hasOwn(scripts, name)));
159
+ }
160
+ /**
161
+ * Validates conditional requirements and tracks missing scripts with defaults.
162
+ */
163
+ function validateConditionalWithDefaults(rules, scripts, defaultScripts, existingMissingWithDefaults) {
164
+ const errors = [];
165
+ const missingWithDefaults = [];
166
+ const allTracked = new Set(existingMissingWithDefaults);
167
+ for (const rule of rules) {
168
+ if (!(scripts && Object.hasOwn(scripts, rule.ifPresent))) {
169
+ continue;
170
+ }
171
+ const missingRequired = findMissingFromRule(rule, scripts);
172
+ for (const scriptName of missingRequired) {
173
+ if (scriptName in defaultScripts && !allTracked.has(scriptName)) {
174
+ missingWithDefaults.push(scriptName);
175
+ allTracked.add(scriptName);
176
+ }
177
+ }
178
+ if (missingRequired.length > 0) {
179
+ errors.push(`Script "${rule.ifPresent}" is present, but required companion script(s) missing: ${missingRequired.join(", ")}`);
180
+ }
181
+ }
182
+ return { errors, missingWithDefaults };
183
+ }
184
+ /**
185
+ * Apply fixes to scripts and return the updated scripts object.
186
+ */
187
+ function applyScriptFixes(scripts, missingScriptsWithDefaults, mismatchedScripts, defaultScripts) {
188
+ const updatedScripts = {};
189
+ // Copy existing scripts, filtering out undefined values
190
+ if (scripts) {
191
+ for (const [key, value] of Object.entries(scripts)) {
192
+ if (value !== undefined) {
193
+ updatedScripts[key] = value;
194
+ }
195
+ }
196
+ }
197
+ const fixedScripts = [];
198
+ for (const scriptName of missingScriptsWithDefaults) {
199
+ const defaultValue = defaultScripts[scriptName];
200
+ if (defaultValue !== undefined) {
201
+ updatedScripts[scriptName] = defaultValue;
202
+ fixedScripts.push(scriptName);
203
+ }
204
+ }
205
+ for (const scriptName of mismatchedScripts) {
206
+ const defaultValue = defaultScripts[scriptName];
207
+ if (defaultValue !== undefined && !fixedScripts.includes(scriptName)) {
208
+ updatedScripts[scriptName] = defaultValue;
209
+ fixedScripts.push(scriptName);
210
+ }
211
+ }
212
+ return { updatedScripts, fixedScripts };
213
+ }
214
+ /**
215
+ * Validate 'must' scripts and return any remaining missing scripts.
216
+ */
217
+ function validateRemainingMustScripts(must, scripts) {
218
+ const stillMissing = must
219
+ .map((entry) => parseRequiredEntry(entry).name)
220
+ .filter((name) => !Object.hasOwn(scripts, name));
221
+ if (stillMissing.length > 0) {
222
+ return [`Missing required scripts:\n\t${stillMissing.join("\n\t")}`];
223
+ }
224
+ return [];
225
+ }
226
+ /**
227
+ * Validate 'exact' scripts and return any errors.
228
+ */
229
+ function validateRemainingExactScripts(exact, scripts) {
230
+ const errors = [];
231
+ for (const [scriptName, expectedValue] of Object.entries(exact)) {
232
+ if (!Object.hasOwn(scripts, scriptName)) {
233
+ errors.push(`Missing required script: ${scriptName}`);
234
+ }
235
+ else if (scripts[scriptName] !== expectedValue) {
236
+ errors.push(`Script "${scriptName}" must be "${expectedValue}", but found "${scripts[scriptName]}"`);
237
+ }
238
+ }
239
+ return errors;
240
+ }
241
+ /**
242
+ * Re-validate all rules after applying fixes.
243
+ */
244
+ function revalidateAfterFix(config, updatedScripts) {
245
+ const errors = [];
246
+ if (config.mutuallyExclusive !== undefined &&
247
+ config.mutuallyExclusive.length > 0) {
248
+ errors.push(...validateMutuallyExclusiveScripts(config.mutuallyExclusive, updatedScripts));
249
+ }
250
+ if (config.scriptMustContain !== undefined &&
251
+ config.scriptMustContain.length > 0) {
252
+ errors.push(...validateScriptContents(config.scriptMustContain, updatedScripts));
253
+ }
254
+ if (config.must !== undefined && config.must.length > 0) {
255
+ errors.push(...validateRemainingMustScripts(config.must, updatedScripts));
256
+ }
257
+ if (config.exact !== undefined && Object.keys(config.exact).length > 0) {
258
+ errors.push(...validateRemainingExactScripts(config.exact, updatedScripts));
259
+ }
260
+ if (config.conditionalRequired !== undefined &&
261
+ config.conditionalRequired.length > 0) {
262
+ errors.push(...validateConditionalRequirements(config.conditionalRequired, updatedScripts));
263
+ }
264
+ return errors;
265
+ }
266
+ /**
267
+ * Run all script validations and aggregate results.
268
+ */
269
+ function runAllValidations(config, scripts, defaultScripts) {
270
+ const errors = [];
271
+ const missingWithDefaults = [];
272
+ let mismatchedScripts = [];
273
+ if (config.must !== undefined && config.must.length > 0) {
274
+ const result = validateMustScripts(config.must, scripts);
275
+ errors.push(...result.errors);
276
+ missingWithDefaults.push(...result.missingWithDefaults);
277
+ }
278
+ if (config.exact !== undefined && Object.keys(config.exact).length > 0) {
279
+ const result = validateExactScripts(config.exact, scripts);
280
+ errors.push(...result.errors);
281
+ missingWithDefaults.push(...result.missingWithDefaults);
282
+ mismatchedScripts = result.mismatchedScripts;
283
+ }
284
+ if (config.mutuallyExclusive !== undefined &&
285
+ config.mutuallyExclusive.length > 0) {
286
+ errors.push(...validateMutuallyExclusiveScripts(config.mutuallyExclusive, scripts));
287
+ }
288
+ if (config.conditionalRequired !== undefined &&
289
+ config.conditionalRequired.length > 0) {
290
+ const result = validateConditionalWithDefaults(config.conditionalRequired, scripts, defaultScripts, missingWithDefaults);
291
+ errors.push(...result.errors);
292
+ missingWithDefaults.push(...result.missingWithDefaults);
293
+ }
294
+ if (config.scriptMustContain !== undefined &&
295
+ config.scriptMustContain.length > 0) {
296
+ errors.push(...validateScriptContents(config.scriptMustContain, scripts));
297
+ }
298
+ return { errors, missingWithDefaults, mismatchedScripts };
299
+ }
31
300
  /**
32
301
  * A RepoPolicy that validates package.json scripts based on configurable rules.
33
302
  *
303
+ * @remarks
304
+ *
305
+ * This policy provides comprehensive script validation including:
306
+ * - Required scripts that must be present (with optional defaults for auto-fix)
307
+ * - Exact script content enforcement
308
+ * - Mutually exclusive script groups (at most one can exist)
309
+ * - Conditional requirements (if X exists, Y must also exist)
310
+ * - Script body content validation (script must contain specific substrings)
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * import { makePolicy } from "repopo";
315
+ * import { PackageScripts } from "repopo/policies";
316
+ *
317
+ * const config: RepopoConfig = {
318
+ * policies: [
319
+ * makePolicy(PackageScripts, {
320
+ * // Scripts that must exist
321
+ * must: [
322
+ * "test", // Must exist, no auto-fix
323
+ * { build: "tsc" }, // Must exist, auto-fix available
324
+ * { lint: "biome lint ." }, // Must exist, auto-fix available
325
+ * ],
326
+ * // Scripts that must exist AND match exactly
327
+ * exact: {
328
+ * clean: "rimraf dist esm",
329
+ * format: "biome format --write .",
330
+ * },
331
+ * // If "build" exists, "clean" must also exist (with inline auto-fix default)
332
+ * conditionalRequired: [{ ifPresent: "build", requires: [{ clean: "rimraf dist" }] }],
333
+ * // Script body must contain these substrings
334
+ * scriptMustContain: [{ script: "build", mustContain: ["tsc"] }],
335
+ * // Only one of these can exist
336
+ * mutuallyExclusive: [["lint:eslint", "lint:biome"]],
337
+ * }),
338
+ * ],
339
+ * };
340
+ * ```
341
+ *
34
342
  * @alpha
35
343
  */
36
- export const PackageScripts = definePackagePolicy("PackageScripts", async (json, { file, config }) => {
37
- const failResult = {
38
- name: PackageScripts.name,
39
- file,
40
- autoFixable: false,
41
- };
344
+ export const PackageScripts = definePackagePolicy("PackageScripts", async (json, { file, resolve, config }) => {
42
345
  if (config === undefined) {
43
346
  return true;
44
347
  }
45
- const hasScriptsField = Object.hasOwn(json, "scripts");
46
- const scripts = hasScriptsField ? json.scripts : {};
47
- const errorMessages = [];
48
- // Validate required scripts
49
- if (config.must && config.must.length > 0) {
50
- errorMessages.push(...validateRequiredScripts(config.must, scripts));
51
- }
52
- // Validate mutually exclusive script groups
53
- if (config.mutuallyExclusive && config.mutuallyExclusive.length > 0) {
54
- errorMessages.push(...validateMutuallyExclusiveScripts(config.mutuallyExclusive, scripts));
348
+ const scripts = Object.hasOwn(json, "scripts") ? json.scripts : {};
349
+ const defaultScripts = buildDefaultsMap(config);
350
+ const { errors, missingWithDefaults, mismatchedScripts } = runAllValidations(config, scripts, defaultScripts);
351
+ if (errors.length === 0) {
352
+ return true;
55
353
  }
56
- if (errorMessages.length > 0) {
57
- failResult.errorMessage = errorMessages.join("\n\n");
58
- return failResult;
354
+ const hasAutoFixableErrors = missingWithDefaults.length > 0 || mismatchedScripts.length > 0;
355
+ if (resolve && hasAutoFixableErrors) {
356
+ const { updatedScripts, fixedScripts } = applyScriptFixes(scripts, missingWithDefaults, mismatchedScripts, defaultScripts);
357
+ try {
358
+ await jsonfile.writeFile(file, { ...json, scripts: updatedScripts }, { spaces: 2 });
359
+ const postFixErrors = revalidateAfterFix(config, updatedScripts);
360
+ return {
361
+ name: POLICY_NAME,
362
+ file,
363
+ resolved: postFixErrors.length === 0,
364
+ errorMessages: postFixErrors.length > 0
365
+ ? [
366
+ `Fixed scripts: ${fixedScripts.join(", ")}. Remaining errors:\n${postFixErrors.join("\n\n")}`,
367
+ ]
368
+ : [`Fixed scripts: ${fixedScripts.join(", ")}`],
369
+ };
370
+ }
371
+ catch {
372
+ // Fall through to return regular failure
373
+ }
59
374
  }
60
- return true;
375
+ return {
376
+ name: POLICY_NAME,
377
+ file,
378
+ autoFixable: hasAutoFixableErrors,
379
+ errorMessages: errors,
380
+ };
61
381
  });
62
382
  //# sourceMappingURL=PackageScripts.js.map