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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the PackagePrivateField policy.
|
|
3
|
+
*
|
|
4
|
+
* This policy enforces that packages are correctly marked as private or public based on
|
|
5
|
+
* their scope or name. This is essential for monorepos where some packages should be
|
|
6
|
+
* published to npm while others should remain internal.
|
|
7
|
+
*
|
|
8
|
+
* All pattern matching uses glob patterns via picomatch.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const config: PackagePrivateFieldConfig = {
|
|
13
|
+
* // Packages in myorg scope must be published (private: false or absent)
|
|
14
|
+
* mustPublish: ["@myorg/*"],
|
|
15
|
+
* // Packages in internal scope must be private
|
|
16
|
+
* mustBePrivate: ["@internal/*"],
|
|
17
|
+
* // Packages in experimental scope can choose
|
|
18
|
+
* mayPublish: ["@experimental/*"],
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @alpha
|
|
23
|
+
*/
|
|
24
|
+
export interface PackagePrivateFieldConfig {
|
|
25
|
+
/**
|
|
26
|
+
* A list of glob patterns for packages that must be published.
|
|
27
|
+
*
|
|
28
|
+
* Packages matching these patterns must NOT have `private: true` in their package.json.
|
|
29
|
+
* If they do, the policy will fail and can auto-fix by removing the private field.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
*
|
|
33
|
+
* Uses glob patterns via picomatch. Use "@scope/*" to match all packages in a scope,
|
|
34
|
+
* or use exact package names.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* mustPublish: ["@fluidframework/*", "@fluid-tools/*", "tinylicious"]
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
mustPublish?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* A list of glob patterns for packages that must be private.
|
|
44
|
+
*
|
|
45
|
+
* Packages matching these patterns must have `private: true` in their package.json.
|
|
46
|
+
* If they don't, the policy will fail and can auto-fix by adding `private: true`.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
*
|
|
50
|
+
* Uses glob patterns via picomatch (see mustPublish for pattern examples).
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* mustBePrivate: ["@fluid-internal/*", "@fluid-private/*"]
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
mustBePrivate?: string[];
|
|
58
|
+
/**
|
|
59
|
+
* A list of glob patterns for packages that may optionally be published.
|
|
60
|
+
*
|
|
61
|
+
* Packages matching these patterns can choose whether to be private or public.
|
|
62
|
+
* The policy will not enforce either state for these packages.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
*
|
|
66
|
+
* Uses glob patterns via picomatch (see mustPublish for pattern examples).
|
|
67
|
+
* Packages not matching any list default to requiring `private: true`.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* mayPublish: ["@fluid-experimental/*"]
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
mayPublish?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Default behavior for packages that don't match any of the above lists.
|
|
77
|
+
*
|
|
78
|
+
* - `"private"` (default): Unmatched packages must have `private: true`
|
|
79
|
+
* - `"public"`: Unmatched packages must NOT have `private: true`
|
|
80
|
+
* - `"ignore"`: No enforcement for unmatched packages
|
|
81
|
+
*
|
|
82
|
+
* @defaultValue "private"
|
|
83
|
+
*/
|
|
84
|
+
unmatchedPackages?: "private" | "public" | "ignore";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A policy that enforces the `private` field in package.json based on package scope or name.
|
|
88
|
+
*
|
|
89
|
+
* This policy is essential for monorepos where publishing control is important:
|
|
90
|
+
*
|
|
91
|
+
* - Prevent accidental publishing of internal packages
|
|
92
|
+
* - Ensure public packages don't have `private: true` blocking publishing
|
|
93
|
+
* - Allow experimental packages to choose their own publishing status
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
*
|
|
97
|
+
* The policy evaluates packages in the following order:
|
|
98
|
+
*
|
|
99
|
+
* 1. If the package matches `mustPublish`, it must NOT have `private: true`
|
|
100
|
+
* 2. If the package matches `mustBePrivate`, it MUST have `private: true`
|
|
101
|
+
* 3. If the package matches `mayPublish`, no enforcement is applied
|
|
102
|
+
* 4. Otherwise, the `unmatchedPackages` setting determines behavior (defaults to requiring private)
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* import { makePolicy } from "repopo";
|
|
107
|
+
* import { PackagePrivateField } from "repopo/policies";
|
|
108
|
+
*
|
|
109
|
+
* const config: RepopoConfig = {
|
|
110
|
+
* policies: [
|
|
111
|
+
* makePolicy(PackagePrivateField, {
|
|
112
|
+
* mustPublish: ["@myorg/*"],
|
|
113
|
+
* mustBePrivate: ["@internal/*"],
|
|
114
|
+
* mayPublish: ["@experimental/*"],
|
|
115
|
+
* unmatchedPackages: "private",
|
|
116
|
+
* }),
|
|
117
|
+
* ],
|
|
118
|
+
* };
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @alpha
|
|
122
|
+
*/
|
|
123
|
+
export declare const PackagePrivateField: import("../policy.js").PolicyDefinition<PackagePrivateFieldConfig | undefined>;
|
|
124
|
+
//# sourceMappingURL=PackagePrivateField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PackagePrivateField.d.ts","sourceRoot":"","sources":["../../src/policies/PackagePrivateField.ts"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,yBAAyB;IACzC;;;;;;;;;;;;;;;OAeG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB;;;;;;;;;;;;;;OAcG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;;;;;;;;;;;;;;OAeG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACpD;AA+DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,mBAAmB,gFA0E9B,CAAC"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import jsonfile from "jsonfile";
|
|
2
|
+
import picomatch from "picomatch";
|
|
3
|
+
import { definePackagePolicy } from "../policyDefiners/definePackagePolicy.js";
|
|
4
|
+
const { writeFile: writeJson } = jsonfile;
|
|
5
|
+
/**
|
|
6
|
+
* Sentinel value indicating that the package's private status should not be enforced.
|
|
7
|
+
* This is more explicit than using `undefined` to represent "ignore".
|
|
8
|
+
*/
|
|
9
|
+
const IGNORE_PRIVATE_STATE = "ignore";
|
|
10
|
+
/**
|
|
11
|
+
* Check if a package name matches any pattern in the list.
|
|
12
|
+
*
|
|
13
|
+
* Supports glob patterns via picomatch. Use patterns like:
|
|
14
|
+
* - "@myorg/\*" - matches all packages in the myorg scope
|
|
15
|
+
* - "my-package" - exact match
|
|
16
|
+
* - "my-*" - matches my-foo, my-bar, etc.
|
|
17
|
+
*
|
|
18
|
+
* @param packageName - The package name to check (e.g., `@myorg/foo`)
|
|
19
|
+
* @param patterns - List of glob patterns or exact package names
|
|
20
|
+
* @returns `true` if the package matches any pattern
|
|
21
|
+
*/
|
|
22
|
+
function matchesAnyPattern(packageName, patterns) {
|
|
23
|
+
if (patterns === undefined || patterns.length === 0) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return picomatch.isMatch(packageName, patterns);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Determine what the private field should be for a package.
|
|
30
|
+
*
|
|
31
|
+
* @returns `true` if package must be private, `false` if must be public, `"ignore"` if either is ok
|
|
32
|
+
*/
|
|
33
|
+
function getRequiredPrivateState(packageName, config) {
|
|
34
|
+
// Check mustPublish first - these packages must NOT be private
|
|
35
|
+
if (matchesAnyPattern(packageName, config.mustPublish)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Check mustBePrivate - these packages MUST be private
|
|
39
|
+
if (matchesAnyPattern(packageName, config.mustBePrivate)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// Check mayPublish - these packages can be either
|
|
43
|
+
if (matchesAnyPattern(packageName, config.mayPublish)) {
|
|
44
|
+
return IGNORE_PRIVATE_STATE;
|
|
45
|
+
}
|
|
46
|
+
// Default behavior for unmatched packages
|
|
47
|
+
const unmatchedBehavior = config.unmatchedPackages ?? "private";
|
|
48
|
+
switch (unmatchedBehavior) {
|
|
49
|
+
case "private":
|
|
50
|
+
return true;
|
|
51
|
+
case "public":
|
|
52
|
+
return false;
|
|
53
|
+
case "ignore":
|
|
54
|
+
return IGNORE_PRIVATE_STATE;
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unknown unmatchedPackages value: ${unmatchedBehavior}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A policy that enforces the `private` field in package.json based on package scope or name.
|
|
61
|
+
*
|
|
62
|
+
* This policy is essential for monorepos where publishing control is important:
|
|
63
|
+
*
|
|
64
|
+
* - Prevent accidental publishing of internal packages
|
|
65
|
+
* - Ensure public packages don't have `private: true` blocking publishing
|
|
66
|
+
* - Allow experimental packages to choose their own publishing status
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
*
|
|
70
|
+
* The policy evaluates packages in the following order:
|
|
71
|
+
*
|
|
72
|
+
* 1. If the package matches `mustPublish`, it must NOT have `private: true`
|
|
73
|
+
* 2. If the package matches `mustBePrivate`, it MUST have `private: true`
|
|
74
|
+
* 3. If the package matches `mayPublish`, no enforcement is applied
|
|
75
|
+
* 4. Otherwise, the `unmatchedPackages` setting determines behavior (defaults to requiring private)
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* import { makePolicy } from "repopo";
|
|
80
|
+
* import { PackagePrivateField } from "repopo/policies";
|
|
81
|
+
*
|
|
82
|
+
* const config: RepopoConfig = {
|
|
83
|
+
* policies: [
|
|
84
|
+
* makePolicy(PackagePrivateField, {
|
|
85
|
+
* mustPublish: ["@myorg/*"],
|
|
86
|
+
* mustBePrivate: ["@internal/*"],
|
|
87
|
+
* mayPublish: ["@experimental/*"],
|
|
88
|
+
* unmatchedPackages: "private",
|
|
89
|
+
* }),
|
|
90
|
+
* ],
|
|
91
|
+
* };
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @alpha
|
|
95
|
+
*/
|
|
96
|
+
export const PackagePrivateField = definePackagePolicy("PackagePrivateField", async (json, { file, config, resolve }) => {
|
|
97
|
+
// If no config provided, skip validation
|
|
98
|
+
if (config === undefined) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
const packageName = json.name;
|
|
102
|
+
// Skip packages without a name (shouldn't happen in practice)
|
|
103
|
+
if (packageName === undefined) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
// Skip the root package (typically named "root" in monorepos)
|
|
107
|
+
if (packageName === "root") {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
const requiredState = getRequiredPrivateState(packageName, config);
|
|
111
|
+
// If "ignore", the package can be either private or public
|
|
112
|
+
if (requiredState === IGNORE_PRIVATE_STATE) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const currentlyPrivate = json.private === true;
|
|
116
|
+
// Check if the current state matches the required state
|
|
117
|
+
if (requiredState === currentlyPrivate) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
// State mismatch - build error message
|
|
121
|
+
const errorMessage = requiredState === true
|
|
122
|
+
? `Package "${packageName}" must be marked private. Add "private": true to package.json.`
|
|
123
|
+
: `Package "${packageName}" must not be marked private. Remove "private": true from package.json to allow publishing.`;
|
|
124
|
+
const failResult = {
|
|
125
|
+
name: PackagePrivateField.name,
|
|
126
|
+
file,
|
|
127
|
+
autoFixable: true,
|
|
128
|
+
errorMessages: [errorMessage],
|
|
129
|
+
};
|
|
130
|
+
if (resolve) {
|
|
131
|
+
const fixResult = {
|
|
132
|
+
...failResult,
|
|
133
|
+
resolved: false,
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
if (requiredState === true) {
|
|
137
|
+
// Add private: true
|
|
138
|
+
json.private = true;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Remove private field
|
|
142
|
+
delete json.private;
|
|
143
|
+
}
|
|
144
|
+
await writeJson(file, json, { spaces: "\t" });
|
|
145
|
+
fixResult.resolved = true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
fixResult.resolved = false;
|
|
149
|
+
fixResult.errorMessages = [`Failed to update ${file}`];
|
|
150
|
+
}
|
|
151
|
+
return fixResult;
|
|
152
|
+
}
|
|
153
|
+
return failResult;
|
|
154
|
+
});
|
|
155
|
+
//# sourceMappingURL=PackagePrivateField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PackagePrivateField.js","sourceRoot":"","sources":["../../src/policies/PackagePrivateField.ts"],"names":[],"mappings":"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","sourcesContent":["import jsonfile from \"jsonfile\";\nimport picomatch from \"picomatch\";\nimport type { PackageJson } from \"type-fest\";\nimport type { PolicyFailure, PolicyFixResult } from \"../policy.js\";\nimport { definePackagePolicy } from \"../policyDefiners/definePackagePolicy.js\";\n\nconst { writeFile: writeJson } = jsonfile;\n\n/**\n * Sentinel value indicating that the package's private status should not be enforced.\n * This is more explicit than using `undefined` to represent \"ignore\".\n */\nconst IGNORE_PRIVATE_STATE = \"ignore\" as const;\n\n/**\n * The required private state for a package.\n * - `true`: Package must have `private: true`\n * - `false`: Package must NOT have `private: true`\n * - `\"ignore\"`: No enforcement - package can be either private or public\n */\ntype RequiredPrivateState = boolean | typeof IGNORE_PRIVATE_STATE;\n\n/**\n * Configuration for the PackagePrivateField policy.\n *\n * This policy enforces that packages are correctly marked as private or public based on\n * their scope or name. This is essential for monorepos where some packages should be\n * published to npm while others should remain internal.\n *\n * All pattern matching uses glob patterns via picomatch.\n *\n * @example\n * ```typescript\n * const config: PackagePrivateFieldConfig = {\n * // Packages in myorg scope must be published (private: false or absent)\n * mustPublish: [\"@myorg/*\"],\n * // Packages in internal scope must be private\n * mustBePrivate: [\"@internal/*\"],\n * // Packages in experimental scope can choose\n * mayPublish: [\"@experimental/*\"],\n * };\n * ```\n *\n * @alpha\n */\nexport interface PackagePrivateFieldConfig {\n\t/**\n\t * A list of glob patterns for packages that must be published.\n\t *\n\t * Packages matching these patterns must NOT have `private: true` in their package.json.\n\t * If they do, the policy will fail and can auto-fix by removing the private field.\n\t *\n\t * @remarks\n\t *\n\t * Uses glob patterns via picomatch. Use \"@scope/*\" to match all packages in a scope,\n\t * or use exact package names.\n\t *\n\t * @example\n\t * ```typescript\n\t * mustPublish: [\"@fluidframework/*\", \"@fluid-tools/*\", \"tinylicious\"]\n\t * ```\n\t */\n\tmustPublish?: string[];\n\n\t/**\n\t * A list of glob patterns for packages that must be private.\n\t *\n\t * Packages matching these patterns must have `private: true` in their package.json.\n\t * If they don't, the policy will fail and can auto-fix by adding `private: true`.\n\t *\n\t * @remarks\n\t *\n\t * Uses glob patterns via picomatch (see mustPublish for pattern examples).\n\t *\n\t * @example\n\t * ```typescript\n\t * mustBePrivate: [\"@fluid-internal/*\", \"@fluid-private/*\"]\n\t * ```\n\t */\n\tmustBePrivate?: string[];\n\n\t/**\n\t * A list of glob patterns for packages that may optionally be published.\n\t *\n\t * Packages matching these patterns can choose whether to be private or public.\n\t * The policy will not enforce either state for these packages.\n\t *\n\t * @remarks\n\t *\n\t * Uses glob patterns via picomatch (see mustPublish for pattern examples).\n\t * Packages not matching any list default to requiring `private: true`.\n\t *\n\t * @example\n\t * ```typescript\n\t * mayPublish: [\"@fluid-experimental/*\"]\n\t * ```\n\t */\n\tmayPublish?: string[];\n\n\t/**\n\t * Default behavior for packages that don't match any of the above lists.\n\t *\n\t * - `\"private\"` (default): Unmatched packages must have `private: true`\n\t * - `\"public\"`: Unmatched packages must NOT have `private: true`\n\t * - `\"ignore\"`: No enforcement for unmatched packages\n\t *\n\t * @defaultValue \"private\"\n\t */\n\tunmatchedPackages?: \"private\" | \"public\" | \"ignore\";\n}\n\n/**\n * Check if a package name matches any pattern in the list.\n *\n * Supports glob patterns via picomatch. Use patterns like:\n * - \"@myorg/\\*\" - matches all packages in the myorg scope\n * - \"my-package\" - exact match\n * - \"my-*\" - matches my-foo, my-bar, etc.\n *\n * @param packageName - The package name to check (e.g., `@myorg/foo`)\n * @param patterns - List of glob patterns or exact package names\n * @returns `true` if the package matches any pattern\n */\nfunction matchesAnyPattern(\n\tpackageName: string,\n\tpatterns: string[] | undefined,\n): boolean {\n\tif (patterns === undefined || patterns.length === 0) {\n\t\treturn false;\n\t}\n\n\treturn picomatch.isMatch(packageName, patterns);\n}\n\n/**\n * Determine what the private field should be for a package.\n *\n * @returns `true` if package must be private, `false` if must be public, `\"ignore\"` if either is ok\n */\nfunction getRequiredPrivateState(\n\tpackageName: string,\n\tconfig: PackagePrivateFieldConfig,\n): RequiredPrivateState {\n\t// Check mustPublish first - these packages must NOT be private\n\tif (matchesAnyPattern(packageName, config.mustPublish)) {\n\t\treturn false;\n\t}\n\n\t// Check mustBePrivate - these packages MUST be private\n\tif (matchesAnyPattern(packageName, config.mustBePrivate)) {\n\t\treturn true;\n\t}\n\n\t// Check mayPublish - these packages can be either\n\tif (matchesAnyPattern(packageName, config.mayPublish)) {\n\t\treturn IGNORE_PRIVATE_STATE;\n\t}\n\n\t// Default behavior for unmatched packages\n\tconst unmatchedBehavior = config.unmatchedPackages ?? \"private\";\n\tswitch (unmatchedBehavior) {\n\t\tcase \"private\":\n\t\t\treturn true;\n\t\tcase \"public\":\n\t\t\treturn false;\n\t\tcase \"ignore\":\n\t\t\treturn IGNORE_PRIVATE_STATE;\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown unmatchedPackages value: ${unmatchedBehavior}`);\n\t}\n}\n\n/**\n * A policy that enforces the `private` field in package.json based on package scope or name.\n *\n * This policy is essential for monorepos where publishing control is important:\n *\n * - Prevent accidental publishing of internal packages\n * - Ensure public packages don't have `private: true` blocking publishing\n * - Allow experimental packages to choose their own publishing status\n *\n * @remarks\n *\n * The policy evaluates packages in the following order:\n *\n * 1. If the package matches `mustPublish`, it must NOT have `private: true`\n * 2. If the package matches `mustBePrivate`, it MUST have `private: true`\n * 3. If the package matches `mayPublish`, no enforcement is applied\n * 4. Otherwise, the `unmatchedPackages` setting determines behavior (defaults to requiring private)\n *\n * @example\n * ```typescript\n * import { makePolicy } from \"repopo\";\n * import { PackagePrivateField } from \"repopo/policies\";\n *\n * const config: RepopoConfig = {\n * policies: [\n * makePolicy(PackagePrivateField, {\n * mustPublish: [\"@myorg/*\"],\n * mustBePrivate: [\"@internal/*\"],\n * mayPublish: [\"@experimental/*\"],\n * unmatchedPackages: \"private\",\n * }),\n * ],\n * };\n * ```\n *\n * @alpha\n */\nexport const PackagePrivateField = definePackagePolicy<\n\tPackageJson,\n\tPackagePrivateFieldConfig | undefined\n>(\"PackagePrivateField\", async (json, { file, config, resolve }) => {\n\t// If no config provided, skip validation\n\tif (config === undefined) {\n\t\treturn true;\n\t}\n\n\tconst packageName = json.name;\n\n\t// Skip packages without a name (shouldn't happen in practice)\n\tif (packageName === undefined) {\n\t\treturn true;\n\t}\n\n\t// Skip the root package (typically named \"root\" in monorepos)\n\tif (packageName === \"root\") {\n\t\treturn true;\n\t}\n\n\tconst requiredState = getRequiredPrivateState(packageName, config);\n\n\t// If \"ignore\", the package can be either private or public\n\tif (requiredState === IGNORE_PRIVATE_STATE) {\n\t\treturn true;\n\t}\n\n\tconst currentlyPrivate = json.private === true;\n\n\t// Check if the current state matches the required state\n\tif (requiredState === currentlyPrivate) {\n\t\treturn true;\n\t}\n\n\t// State mismatch - build error message\n\tconst errorMessage =\n\t\trequiredState === true\n\t\t\t? `Package \"${packageName}\" must be marked private. Add \"private\": true to package.json.`\n\t\t\t: `Package \"${packageName}\" must not be marked private. Remove \"private\": true from package.json to allow publishing.`;\n\n\tconst failResult: PolicyFailure = {\n\t\tname: PackagePrivateField.name,\n\t\tfile,\n\t\tautoFixable: true,\n\t\terrorMessages: [errorMessage],\n\t};\n\n\tif (resolve) {\n\t\tconst fixResult: PolicyFixResult = {\n\t\t\t...failResult,\n\t\t\tresolved: false,\n\t\t};\n\n\t\ttry {\n\t\t\tif (requiredState === true) {\n\t\t\t\t// Add private: true\n\t\t\t\tjson.private = true;\n\t\t\t} else {\n\t\t\t\t// Remove private field\n\t\t\t\tdelete json.private;\n\t\t\t}\n\n\t\t\tawait writeJson(file, json, { spaces: \"\\t\" });\n\t\t\tfixResult.resolved = true;\n\t\t} catch {\n\t\t\tfixResult.resolved = false;\n\t\t\tfixResult.errorMessages = [`Failed to update ${file}`];\n\t\t}\n\n\t\treturn fixResult;\n\t}\n\n\treturn failResult;\n});\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy settings for the PackageReadme repo policy.
|
|
3
|
+
*
|
|
4
|
+
* @alpha
|
|
5
|
+
*/
|
|
6
|
+
export interface PackageReadmeSettings {
|
|
7
|
+
/**
|
|
8
|
+
* Whether to skip validation for private packages.
|
|
9
|
+
*
|
|
10
|
+
* @defaultValue true
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
*
|
|
14
|
+
* Private packages are typically not published and may not need a README file.
|
|
15
|
+
* Set to `false` to require README files for all packages.
|
|
16
|
+
*/
|
|
17
|
+
skipPrivate?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether to require the README title to match the package name.
|
|
20
|
+
*
|
|
21
|
+
* @defaultValue true
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
*
|
|
25
|
+
* When enabled, the first line of the README must be `# package-name`.
|
|
26
|
+
* This ensures consistency and helps users identify the package.
|
|
27
|
+
*/
|
|
28
|
+
requireMatchingTitle?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* A string that must appear in the README content.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
*
|
|
34
|
+
* Useful for ensuring all READMEs include required content like trademark
|
|
35
|
+
* notices, disclaimers, or standard documentation sections.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* requiredContent: "## Trademark"
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
requiredContent?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A repo policy that ensures each package has a README.md file with proper content.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
*
|
|
49
|
+
* This policy validates that packages have documentation and can enforce:
|
|
50
|
+
* 1. README.md file exists
|
|
51
|
+
* 2. README title matches the package name
|
|
52
|
+
* 3. README contains required content (e.g., trademark notices)
|
|
53
|
+
*
|
|
54
|
+
* The policy supports auto-fixing by creating a basic README with the package name as title.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { makePolicy } from "repopo";
|
|
59
|
+
* import { PackageReadme } from "repopo/policies";
|
|
60
|
+
*
|
|
61
|
+
* const config: RepopoConfig = {
|
|
62
|
+
* policies: [
|
|
63
|
+
* // Basic README validation
|
|
64
|
+
* makePolicy(PackageReadme),
|
|
65
|
+
*
|
|
66
|
+
* // Require trademark notice in all READMEs
|
|
67
|
+
* makePolicy(PackageReadme, {
|
|
68
|
+
* requiredContent: "## Trademark",
|
|
69
|
+
* }),
|
|
70
|
+
* ],
|
|
71
|
+
* };
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @alpha
|
|
75
|
+
*/
|
|
76
|
+
export declare const PackageReadme: import("../policy.js").PolicyDefinition<PackageReadmeSettings | undefined>;
|
|
77
|
+
//# sourceMappingURL=PackageReadme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PackageReadme.d.ts","sourceRoot":"","sources":["../../src/policies/PackageReadme.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;;;;;OASG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAkMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,aAAa,4EAuCxB,CAAC"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "pathe";
|
|
4
|
+
import { definePackagePolicy } from "../policyDefiners/definePackagePolicy.js";
|
|
5
|
+
// Pre-compiled regex for performance
|
|
6
|
+
const LINE_SPLIT_REGEX = /\r?\n/;
|
|
7
|
+
const HEADING_REGEX = /^#\s+(.+)$/;
|
|
8
|
+
/**
|
|
9
|
+
* Extract the title from a README file (first markdown H1 heading).
|
|
10
|
+
*
|
|
11
|
+
* @param content - The README content
|
|
12
|
+
* @returns The title text (without the `# ` prefix) or `undefined` if not found
|
|
13
|
+
*/
|
|
14
|
+
function extractReadmeTitle(content) {
|
|
15
|
+
const lines = content.split(LINE_SPLIT_REGEX);
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
const match = HEADING_REGEX.exec(line);
|
|
18
|
+
if (match?.[1]) {
|
|
19
|
+
return match[1].trim();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a basic README content with package name as title.
|
|
26
|
+
*/
|
|
27
|
+
function createReadmeContent(packageName, requiredContent) {
|
|
28
|
+
let content = `# ${packageName}\n`;
|
|
29
|
+
if (requiredContent) {
|
|
30
|
+
content += `\n${requiredContent}\n`;
|
|
31
|
+
}
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Handle case when README is missing.
|
|
36
|
+
*/
|
|
37
|
+
async function handleMissingReadme(readmePath, packageName, file, resolve, requiredContent) {
|
|
38
|
+
if (resolve) {
|
|
39
|
+
try {
|
|
40
|
+
await writeFile(readmePath, createReadmeContent(packageName, requiredContent));
|
|
41
|
+
return {
|
|
42
|
+
name: "PackageReadme",
|
|
43
|
+
file,
|
|
44
|
+
resolved: true,
|
|
45
|
+
autoFixable: true,
|
|
46
|
+
errorMessages: [`Created README.md for package "${packageName}"`],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {
|
|
51
|
+
name: "PackageReadme",
|
|
52
|
+
file,
|
|
53
|
+
resolved: false,
|
|
54
|
+
autoFixable: true,
|
|
55
|
+
errorMessages: [
|
|
56
|
+
`Failed to create README.md for package "${packageName}"`,
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
name: "PackageReadme",
|
|
63
|
+
file,
|
|
64
|
+
autoFixable: true,
|
|
65
|
+
errorMessages: [`README.md missing for package "${packageName}"`],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate README title against package name.
|
|
70
|
+
*
|
|
71
|
+
* @returns `true` if valid, or an error message string if invalid
|
|
72
|
+
*/
|
|
73
|
+
function validateTitle(readmeContent, packageName) {
|
|
74
|
+
const title = extractReadmeTitle(readmeContent);
|
|
75
|
+
if (title !== packageName) {
|
|
76
|
+
return `README title "${title ?? "(none)"}" doesn't match package name "${packageName}"`;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Handle required content validation and auto-fix.
|
|
82
|
+
*/
|
|
83
|
+
async function handleRequiredContent(readmePath, readmeContent, requiredContent, resolve) {
|
|
84
|
+
if (readmeContent.includes(requiredContent)) {
|
|
85
|
+
return { fixed: false };
|
|
86
|
+
}
|
|
87
|
+
if (resolve) {
|
|
88
|
+
try {
|
|
89
|
+
const newContent = readmeContent.endsWith("\n")
|
|
90
|
+
? `${readmeContent}${requiredContent}\n`
|
|
91
|
+
: `${readmeContent}\n${requiredContent}\n`;
|
|
92
|
+
await writeFile(readmePath, newContent);
|
|
93
|
+
return {
|
|
94
|
+
error: `README missing required content: "${requiredContent}"`,
|
|
95
|
+
fixed: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Fall through to report error
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
error: `README missing required content: "${requiredContent}"`,
|
|
104
|
+
fixed: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validate existing README content.
|
|
109
|
+
*/
|
|
110
|
+
async function validateExistingReadme(ctx) {
|
|
111
|
+
const errors = [];
|
|
112
|
+
let wasFixed = false;
|
|
113
|
+
// Check title matches package name
|
|
114
|
+
if (ctx.requireMatchingTitle) {
|
|
115
|
+
const titleResult = validateTitle(ctx.readmeContent, ctx.packageName);
|
|
116
|
+
if (titleResult !== true) {
|
|
117
|
+
errors.push(titleResult);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Check required content
|
|
121
|
+
if (ctx.requiredContent) {
|
|
122
|
+
const contentResult = await handleRequiredContent(ctx.readmePath, ctx.readmeContent, ctx.requiredContent, ctx.resolve);
|
|
123
|
+
if (contentResult.error) {
|
|
124
|
+
errors.push(contentResult.error);
|
|
125
|
+
}
|
|
126
|
+
if (contentResult.fixed) {
|
|
127
|
+
wasFixed = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (errors.length === 0) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (wasFixed) {
|
|
134
|
+
return {
|
|
135
|
+
name: "PackageReadme",
|
|
136
|
+
file: ctx.file,
|
|
137
|
+
resolved: true,
|
|
138
|
+
autoFixable: true,
|
|
139
|
+
errorMessages: [errors.join("; ")],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
name: "PackageReadme",
|
|
144
|
+
file: ctx.file,
|
|
145
|
+
autoFixable: ctx.requiredContent !== undefined &&
|
|
146
|
+
!ctx.readmeContent.includes(ctx.requiredContent),
|
|
147
|
+
errorMessages: [errors.join("; ")],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A repo policy that ensures each package has a README.md file with proper content.
|
|
152
|
+
*
|
|
153
|
+
* @remarks
|
|
154
|
+
*
|
|
155
|
+
* This policy validates that packages have documentation and can enforce:
|
|
156
|
+
* 1. README.md file exists
|
|
157
|
+
* 2. README title matches the package name
|
|
158
|
+
* 3. README contains required content (e.g., trademark notices)
|
|
159
|
+
*
|
|
160
|
+
* The policy supports auto-fixing by creating a basic README with the package name as title.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* import { makePolicy } from "repopo";
|
|
165
|
+
* import { PackageReadme } from "repopo/policies";
|
|
166
|
+
*
|
|
167
|
+
* const config: RepopoConfig = {
|
|
168
|
+
* policies: [
|
|
169
|
+
* // Basic README validation
|
|
170
|
+
* makePolicy(PackageReadme),
|
|
171
|
+
*
|
|
172
|
+
* // Require trademark notice in all READMEs
|
|
173
|
+
* makePolicy(PackageReadme, {
|
|
174
|
+
* requiredContent: "## Trademark",
|
|
175
|
+
* }),
|
|
176
|
+
* ],
|
|
177
|
+
* };
|
|
178
|
+
* ```
|
|
179
|
+
*
|
|
180
|
+
* @alpha
|
|
181
|
+
*/
|
|
182
|
+
export const PackageReadme = definePackagePolicy("PackageReadme", async (json, { file, resolve, config }) => {
|
|
183
|
+
const skipPrivate = config?.skipPrivate ?? true;
|
|
184
|
+
const requireMatchingTitle = config?.requireMatchingTitle ?? true;
|
|
185
|
+
const requiredContent = config?.requiredContent;
|
|
186
|
+
// Skip private packages if configured to do so
|
|
187
|
+
if (skipPrivate && json.private === true) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
const packageName = json.name ?? "unknown";
|
|
191
|
+
const packageDir = path.dirname(file);
|
|
192
|
+
const readmePath = path.join(packageDir, "README.md");
|
|
193
|
+
// Check if README exists
|
|
194
|
+
if (!existsSync(readmePath)) {
|
|
195
|
+
return await handleMissingReadme(readmePath, packageName, file, resolve, requiredContent);
|
|
196
|
+
}
|
|
197
|
+
// README exists, validate content
|
|
198
|
+
const readmeContent = await readFile(readmePath, "utf-8");
|
|
199
|
+
return await validateExistingReadme({
|
|
200
|
+
readmePath,
|
|
201
|
+
readmeContent,
|
|
202
|
+
packageName,
|
|
203
|
+
file,
|
|
204
|
+
resolve,
|
|
205
|
+
requireMatchingTitle,
|
|
206
|
+
requiredContent,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
//# sourceMappingURL=PackageReadme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PackageReadme.js","sourceRoot":"","sources":["../../src/policies/PackageReadme.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport path from \"pathe\";\nimport type { PackageJson } from \"type-fest\";\nimport type { PolicyFailure, PolicyFixResult } from \"../policy.js\";\nimport { definePackagePolicy } from \"../policyDefiners/definePackagePolicy.js\";\n\n// Pre-compiled regex for performance\nconst LINE_SPLIT_REGEX = /\\r?\\n/;\nconst HEADING_REGEX = /^#\\s+(.+)$/;\n\n/**\n * Policy settings for the PackageReadme repo policy.\n *\n * @alpha\n */\nexport interface PackageReadmeSettings {\n\t/**\n\t * Whether to skip validation for private packages.\n\t *\n\t * @defaultValue true\n\t *\n\t * @remarks\n\t *\n\t * Private packages are typically not published and may not need a README file.\n\t * Set to `false` to require README files for all packages.\n\t */\n\tskipPrivate?: boolean;\n\n\t/**\n\t * Whether to require the README title to match the package name.\n\t *\n\t * @defaultValue true\n\t *\n\t * @remarks\n\t *\n\t * When enabled, the first line of the README must be `# package-name`.\n\t * This ensures consistency and helps users identify the package.\n\t */\n\trequireMatchingTitle?: boolean;\n\n\t/**\n\t * A string that must appear in the README content.\n\t *\n\t * @remarks\n\t *\n\t * Useful for ensuring all READMEs include required content like trademark\n\t * notices, disclaimers, or standard documentation sections.\n\t *\n\t * @example\n\t * ```typescript\n\t * requiredContent: \"## Trademark\"\n\t * ```\n\t */\n\trequiredContent?: string;\n}\n\n/**\n * Extract the title from a README file (first markdown H1 heading).\n *\n * @param content - The README content\n * @returns The title text (without the `# ` prefix) or `undefined` if not found\n */\nfunction extractReadmeTitle(content: string): string | undefined {\n\tconst lines = content.split(LINE_SPLIT_REGEX);\n\tfor (const line of lines) {\n\t\tconst match = HEADING_REGEX.exec(line);\n\t\tif (match?.[1]) {\n\t\t\treturn match[1].trim();\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Create a basic README content with package name as title.\n */\nfunction createReadmeContent(\n\tpackageName: string,\n\trequiredContent?: string,\n): string {\n\tlet content = `# ${packageName}\\n`;\n\tif (requiredContent) {\n\t\tcontent += `\\n${requiredContent}\\n`;\n\t}\n\treturn content;\n}\n\n/**\n * Handle case when README is missing.\n */\nasync function handleMissingReadme(\n\treadmePath: string,\n\tpackageName: string,\n\tfile: string,\n\tresolve: boolean,\n\trequiredContent?: string,\n): Promise<PolicyFailure | PolicyFixResult> {\n\tif (resolve) {\n\t\ttry {\n\t\t\tawait writeFile(\n\t\t\t\treadmePath,\n\t\t\t\tcreateReadmeContent(packageName, requiredContent),\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tname: \"PackageReadme\",\n\t\t\t\tfile,\n\t\t\t\tresolved: true,\n\t\t\t\tautoFixable: true,\n\t\t\t\terrorMessages: [`Created README.md for package \"${packageName}\"`],\n\t\t\t};\n\t\t} catch {\n\t\t\treturn {\n\t\t\t\tname: \"PackageReadme\",\n\t\t\t\tfile,\n\t\t\t\tresolved: false,\n\t\t\t\tautoFixable: true,\n\t\t\t\terrorMessages: [\n\t\t\t\t\t`Failed to create README.md for package \"${packageName}\"`,\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\t}\n\n\treturn {\n\t\tname: \"PackageReadme\",\n\t\tfile,\n\t\tautoFixable: true,\n\t\terrorMessages: [`README.md missing for package \"${packageName}\"`],\n\t};\n}\n\n/**\n * Validate README title against package name.\n *\n * @returns `true` if valid, or an error message string if invalid\n */\nfunction validateTitle(\n\treadmeContent: string,\n\tpackageName: string,\n): true | string {\n\tconst title = extractReadmeTitle(readmeContent);\n\tif (title !== packageName) {\n\t\treturn `README title \"${title ?? \"(none)\"}\" doesn't match package name \"${packageName}\"`;\n\t}\n\treturn true;\n}\n\n/**\n * Handle required content validation and auto-fix.\n */\nasync function handleRequiredContent(\n\treadmePath: string,\n\treadmeContent: string,\n\trequiredContent: string,\n\tresolve: boolean,\n): Promise<{ error?: string; fixed: boolean }> {\n\tif (readmeContent.includes(requiredContent)) {\n\t\treturn { fixed: false };\n\t}\n\n\tif (resolve) {\n\t\ttry {\n\t\t\tconst newContent = readmeContent.endsWith(\"\\n\")\n\t\t\t\t? `${readmeContent}${requiredContent}\\n`\n\t\t\t\t: `${readmeContent}\\n${requiredContent}\\n`;\n\t\t\tawait writeFile(readmePath, newContent);\n\t\t\treturn {\n\t\t\t\terror: `README missing required content: \"${requiredContent}\"`,\n\t\t\t\tfixed: true,\n\t\t\t};\n\t\t} catch {\n\t\t\t// Fall through to report error\n\t\t}\n\t}\n\n\treturn {\n\t\terror: `README missing required content: \"${requiredContent}\"`,\n\t\tfixed: false,\n\t};\n}\n\ninterface ValidationContext {\n\treadmePath: string;\n\treadmeContent: string;\n\tpackageName: string;\n\tfile: string;\n\tresolve: boolean;\n\trequireMatchingTitle: boolean;\n\trequiredContent: string | undefined;\n}\n\n/**\n * Validate existing README content.\n */\nasync function validateExistingReadme(\n\tctx: ValidationContext,\n): Promise<true | PolicyFailure | PolicyFixResult> {\n\tconst errors: string[] = [];\n\tlet wasFixed = false;\n\n\t// Check title matches package name\n\tif (ctx.requireMatchingTitle) {\n\t\tconst titleResult = validateTitle(ctx.readmeContent, ctx.packageName);\n\t\tif (titleResult !== true) {\n\t\t\terrors.push(titleResult);\n\t\t}\n\t}\n\n\t// Check required content\n\tif (ctx.requiredContent) {\n\t\tconst contentResult = await handleRequiredContent(\n\t\t\tctx.readmePath,\n\t\t\tctx.readmeContent,\n\t\t\tctx.requiredContent,\n\t\t\tctx.resolve,\n\t\t);\n\t\tif (contentResult.error) {\n\t\t\terrors.push(contentResult.error);\n\t\t}\n\t\tif (contentResult.fixed) {\n\t\t\twasFixed = true;\n\t\t}\n\t}\n\n\tif (errors.length === 0) {\n\t\treturn true;\n\t}\n\n\tif (wasFixed) {\n\t\treturn {\n\t\t\tname: \"PackageReadme\",\n\t\t\tfile: ctx.file,\n\t\t\tresolved: true,\n\t\t\tautoFixable: true,\n\t\t\terrorMessages: [errors.join(\"; \")],\n\t\t};\n\t}\n\n\treturn {\n\t\tname: \"PackageReadme\",\n\t\tfile: ctx.file,\n\t\tautoFixable:\n\t\t\tctx.requiredContent !== undefined &&\n\t\t\t!ctx.readmeContent.includes(ctx.requiredContent),\n\t\terrorMessages: [errors.join(\"; \")],\n\t};\n}\n\n/**\n * A repo policy that ensures each package has a README.md file with proper content.\n *\n * @remarks\n *\n * This policy validates that packages have documentation and can enforce:\n * 1. README.md file exists\n * 2. README title matches the package name\n * 3. README contains required content (e.g., trademark notices)\n *\n * The policy supports auto-fixing by creating a basic README with the package name as title.\n *\n * @example\n * ```typescript\n * import { makePolicy } from \"repopo\";\n * import { PackageReadme } from \"repopo/policies\";\n *\n * const config: RepopoConfig = {\n * policies: [\n * // Basic README validation\n * makePolicy(PackageReadme),\n *\n * // Require trademark notice in all READMEs\n * makePolicy(PackageReadme, {\n * requiredContent: \"## Trademark\",\n * }),\n * ],\n * };\n * ```\n *\n * @alpha\n */\nexport const PackageReadme = definePackagePolicy<\n\tPackageJson,\n\tPackageReadmeSettings | undefined\n>(\"PackageReadme\", async (json, { file, resolve, config }) => {\n\tconst skipPrivate = config?.skipPrivate ?? true;\n\tconst requireMatchingTitle = config?.requireMatchingTitle ?? true;\n\tconst requiredContent = config?.requiredContent;\n\n\t// Skip private packages if configured to do so\n\tif (skipPrivate && json.private === true) {\n\t\treturn true;\n\t}\n\n\tconst packageName = json.name ?? \"unknown\";\n\tconst packageDir = path.dirname(file);\n\tconst readmePath = path.join(packageDir, \"README.md\");\n\n\t// Check if README exists\n\tif (!existsSync(readmePath)) {\n\t\treturn await handleMissingReadme(\n\t\t\treadmePath,\n\t\t\tpackageName,\n\t\t\tfile,\n\t\t\tresolve,\n\t\t\trequiredContent,\n\t\t);\n\t}\n\n\t// README exists, validate content\n\tconst readmeContent = await readFile(readmePath, \"utf-8\");\n\treturn await validateExistingReadme({\n\t\treadmePath,\n\t\treadmeContent,\n\t\tpackageName,\n\t\tfile,\n\t\tresolve,\n\t\trequireMatchingTitle,\n\t\trequiredContent,\n\t});\n});\n"]}
|