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.
- package/CHANGELOG.md +187 -6
- package/THIRD-PARTY-LICENSES.txt +149 -3
- package/esm/adapters/fluidFramework.d.ts +103 -0
- package/esm/adapters/fluidFramework.d.ts.map +1 -0
- package/esm/adapters/fluidFramework.js +119 -0
- package/esm/adapters/fluidFramework.js.map +1 -0
- package/esm/baseCommand.d.ts +1 -0
- package/esm/baseCommand.d.ts.map +1 -1
- package/esm/baseCommand.js +2 -1
- package/esm/baseCommand.js.map +1 -1
- package/esm/commands/check.d.ts.map +1 -1
- package/esm/commands/check.js +9 -8
- package/esm/commands/check.js.map +1 -1
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/esm/perf.d.ts +4 -1
- package/esm/perf.d.ts.map +1 -1
- package/esm/perf.js +3 -2
- package/esm/perf.js.map +1 -1
- package/esm/policies/LicenseFileExists.d.ts.map +1 -1
- package/esm/policies/LicenseFileExists.js +3 -1
- package/esm/policies/LicenseFileExists.js.map +1 -1
- package/esm/policies/NoJsFileExtensions.d.ts.map +1 -1
- package/esm/policies/NoJsFileExtensions.js +4 -1
- package/esm/policies/NoJsFileExtensions.js.map +1 -1
- package/esm/policies/NoLargeBinaryFiles.d.ts.map +1 -1
- package/esm/policies/NoLargeBinaryFiles.js +3 -1
- package/esm/policies/NoLargeBinaryFiles.js.map +1 -1
- package/esm/policies/NoPrivateWorkspaceDependencies.d.ts +37 -0
- package/esm/policies/NoPrivateWorkspaceDependencies.d.ts.map +1 -0
- package/esm/policies/NoPrivateWorkspaceDependencies.js +137 -0
- package/esm/policies/NoPrivateWorkspaceDependencies.js.map +1 -0
- package/esm/policies/PackageAllowedScopes.d.ts +88 -0
- package/esm/policies/PackageAllowedScopes.d.ts.map +1 -0
- package/esm/policies/PackageAllowedScopes.js +113 -0
- package/esm/policies/PackageAllowedScopes.js.map +1 -0
- package/esm/policies/PackageEsmType.d.ts +100 -0
- package/esm/policies/PackageEsmType.d.ts.map +1 -0
- package/esm/policies/PackageEsmType.js +257 -0
- package/esm/policies/PackageEsmType.js.map +1 -0
- package/esm/policies/PackageFolderName.d.ts +97 -0
- package/esm/policies/PackageFolderName.d.ts.map +1 -0
- package/esm/policies/PackageFolderName.js +104 -0
- package/esm/policies/PackageFolderName.js.map +1 -0
- package/esm/policies/PackageJsonProperties.d.ts.map +1 -1
- package/esm/policies/PackageJsonProperties.js +2 -1
- package/esm/policies/PackageJsonProperties.js.map +1 -1
- package/esm/policies/PackageJsonRepoDirectoryProperty.d.ts.map +1 -1
- package/esm/policies/PackageJsonRepoDirectoryProperty.js +4 -3
- package/esm/policies/PackageJsonRepoDirectoryProperty.js.map +1 -1
- package/esm/policies/PackageJsonSorted.d.ts.map +1 -1
- package/esm/policies/PackageJsonSorted.js +11 -3
- package/esm/policies/PackageJsonSorted.js.map +1 -1
- package/esm/policies/PackageLicense.d.ts +61 -0
- package/esm/policies/PackageLicense.d.ts.map +1 -0
- package/esm/policies/PackageLicense.js +131 -0
- package/esm/policies/PackageLicense.js.map +1 -0
- package/esm/policies/PackagePrivateField.d.ts +124 -0
- package/esm/policies/PackagePrivateField.d.ts.map +1 -0
- package/esm/policies/PackagePrivateField.js +155 -0
- package/esm/policies/PackagePrivateField.js.map +1 -0
- package/esm/policies/PackageReadme.d.ts +77 -0
- package/esm/policies/PackageReadme.d.ts.map +1 -0
- package/esm/policies/PackageReadme.js +209 -0
- package/esm/policies/PackageReadme.js.map +1 -0
- package/esm/policies/PackageScripts.d.ts +180 -2
- package/esm/policies/PackageScripts.d.ts.map +1 -1
- package/esm/policies/PackageScripts.js +349 -29
- package/esm/policies/PackageScripts.js.map +1 -1
- package/esm/policies/PackageTestScripts.d.ts +100 -0
- package/esm/policies/PackageTestScripts.d.ts.map +1 -0
- package/esm/policies/PackageTestScripts.js +149 -0
- package/esm/policies/PackageTestScripts.js.map +1 -0
- package/esm/policies/RequiredGitignorePatterns.d.ts.map +1 -1
- package/esm/policies/RequiredGitignorePatterns.js +10 -4
- package/esm/policies/RequiredGitignorePatterns.js.map +1 -1
- package/esm/policies.d.ts +9 -1
- package/esm/policies.d.ts.map +1 -1
- package/esm/policies.js +10 -1
- package/esm/policies.js.map +1 -1
- package/esm/policy.d.ts +5 -1
- package/esm/policy.d.ts.map +1 -1
- package/esm/policy.js.map +1 -1
- package/esm/policyDefiners/defineFileHeaderPolicy.d.ts.map +1 -1
- package/esm/policyDefiners/defineFileHeaderPolicy.js +2 -1
- package/esm/policyDefiners/defineFileHeaderPolicy.js.map +1 -1
- package/esm/policyDefiners/definePackagePolicy.d.ts.map +1 -1
- package/esm/policyDefiners/definePackagePolicy.js +2 -1
- package/esm/policyDefiners/definePackagePolicy.js.map +1 -1
- package/esm/tsdoc-metadata.json +1 -1
- package/esm/utils/indentation.d.ts +73 -0
- package/esm/utils/indentation.d.ts.map +1 -0
- package/esm/utils/indentation.js +97 -0
- package/esm/utils/indentation.js.map +1 -0
- package/esm/utils/packageName.d.ts +71 -0
- package/esm/utils/packageName.d.ts.map +1 -0
- package/esm/utils/packageName.js +79 -0
- package/esm/utils/packageName.js.map +1 -0
- package/oclif.manifest.json +1 -1
- 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
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
-
*
|
|
5
|
+
* Extracts script name and optional default value from a RequiredScriptEntry.
|
|
4
6
|
*/
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
375
|
+
return {
|
|
376
|
+
name: POLICY_NAME,
|
|
377
|
+
file,
|
|
378
|
+
autoFixable: hasAutoFixableErrors,
|
|
379
|
+
errorMessages: errors,
|
|
380
|
+
};
|
|
61
381
|
});
|
|
62
382
|
//# sourceMappingURL=PackageScripts.js.map
|