repopo 0.8.0 → 0.9.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 +46 -0
- package/THIRD-PARTY-LICENSES.txt +4 -3
- package/esm/adapters/fluidFramework.js +8 -3
- package/esm/adapters/fluidFramework.js.map +1 -1
- 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 +69 -44
- package/esm/commands/check.js.map +1 -1
- package/esm/commands/list.d.ts.map +1 -1
- package/esm/commands/list.js +3 -1
- package/esm/commands/list.js.map +1 -1
- package/esm/generators.d.ts +30 -2
- package/esm/generators.d.ts.map +1 -1
- package/esm/generators.js +31 -1
- package/esm/generators.js.map +1 -1
- package/esm/index.d.ts +13 -3
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +12 -2
- package/esm/index.js.map +1 -1
- package/esm/makePolicy.d.ts +31 -2
- package/esm/makePolicy.d.ts.map +1 -1
- package/esm/makePolicy.js +27 -13
- package/esm/makePolicy.js.map +1 -1
- package/esm/perf.d.ts +3 -2
- package/esm/perf.d.ts.map +1 -1
- package/esm/perf.js +2 -2
- package/esm/perf.js.map +1 -1
- package/esm/policies/HtmlFileHeaders.d.ts.map +1 -1
- package/esm/policies/HtmlFileHeaders.js +11 -7
- package/esm/policies/HtmlFileHeaders.js.map +1 -1
- package/esm/policies/JsTsFileHeaders.d.ts.map +1 -1
- package/esm/policies/JsTsFileHeaders.js +11 -7
- package/esm/policies/JsTsFileHeaders.js.map +1 -1
- package/esm/policies/LicenseFileExists.d.ts.map +1 -1
- package/esm/policies/LicenseFileExists.js +22 -18
- package/esm/policies/LicenseFileExists.js.map +1 -1
- package/esm/policies/NoJsFileExtensions.d.ts.map +1 -1
- package/esm/policies/NoJsFileExtensions.js +16 -12
- package/esm/policies/NoJsFileExtensions.js.map +1 -1
- package/esm/policies/NoLargeBinaryFiles.d.ts.map +1 -1
- package/esm/policies/NoLargeBinaryFiles.js +35 -31
- package/esm/policies/NoLargeBinaryFiles.js.map +1 -1
- package/esm/policies/NoPrivateWorkspaceDependencies.d.ts.map +1 -1
- package/esm/policies/NoPrivateWorkspaceDependencies.js +53 -45
- package/esm/policies/NoPrivateWorkspaceDependencies.js.map +1 -1
- package/esm/policies/PackageAllowedScopes.d.ts.map +1 -1
- package/esm/policies/PackageAllowedScopes.js +45 -40
- package/esm/policies/PackageAllowedScopes.js.map +1 -1
- package/esm/policies/PackageEsmType.d.ts.map +1 -1
- package/esm/policies/PackageEsmType.js +48 -44
- package/esm/policies/PackageEsmType.js.map +1 -1
- package/esm/policies/PackageFolderName.d.ts.map +1 -1
- package/esm/policies/PackageFolderName.js +38 -34
- package/esm/policies/PackageFolderName.js.map +1 -1
- package/esm/policies/PackageJsonProperties.d.ts.map +1 -1
- package/esm/policies/PackageJsonProperties.js +36 -31
- package/esm/policies/PackageJsonProperties.js.map +1 -1
- package/esm/policies/PackageJsonRepoDirectoryProperty.d.ts.map +1 -1
- package/esm/policies/PackageJsonRepoDirectoryProperty.js +41 -38
- package/esm/policies/PackageJsonRepoDirectoryProperty.js.map +1 -1
- package/esm/policies/PackageJsonSorted.d.ts.map +1 -1
- package/esm/policies/PackageJsonSorted.js +40 -36
- package/esm/policies/PackageJsonSorted.js.map +1 -1
- package/esm/policies/PackageLicense.d.ts.map +1 -1
- package/esm/policies/PackageLicense.js +58 -54
- package/esm/policies/PackageLicense.js.map +1 -1
- package/esm/policies/PackagePrivateField.d.ts.map +1 -1
- package/esm/policies/PackagePrivateField.js +58 -54
- package/esm/policies/PackagePrivateField.js.map +1 -1
- package/esm/policies/PackageReadme.d.ts.map +1 -1
- package/esm/policies/PackageReadme.js +30 -26
- package/esm/policies/PackageReadme.js.map +1 -1
- package/esm/policies/PackageScripts.d.ts.map +1 -1
- package/esm/policies/PackageScripts.js +39 -35
- package/esm/policies/PackageScripts.js.map +1 -1
- package/esm/policies/PackageTestScripts.d.ts.map +1 -1
- package/esm/policies/PackageTestScripts.js +46 -40
- package/esm/policies/PackageTestScripts.js.map +1 -1
- package/esm/policies/RequiredGitignorePatterns.d.ts.map +1 -1
- package/esm/policies/RequiredGitignorePatterns.js +20 -15
- package/esm/policies/RequiredGitignorePatterns.js.map +1 -1
- package/esm/policy.d.ts +32 -7
- package/esm/policy.d.ts.map +1 -1
- package/esm/policy.js +18 -9
- package/esm/policy.js.map +1 -1
- package/esm/policyDefiners/defineFileHeaderPolicy.d.ts +43 -1
- package/esm/policyDefiners/defineFileHeaderPolicy.d.ts.map +1 -1
- package/esm/policyDefiners/defineFileHeaderPolicy.js +16 -4
- package/esm/policyDefiners/defineFileHeaderPolicy.js.map +1 -1
- package/esm/policyDefiners/definePackagePolicy.d.ts +43 -2
- package/esm/policyDefiners/definePackagePolicy.d.ts.map +1 -1
- package/esm/policyDefiners/definePackagePolicy.js +40 -5
- package/esm/policyDefiners/definePackagePolicy.js.map +1 -1
- package/esm/tsdoc-metadata.json +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +6 -4
|
@@ -105,45 +105,51 @@ function shouldSkipValidation(json, config) {
|
|
|
105
105
|
*
|
|
106
106
|
* @alpha
|
|
107
107
|
*/
|
|
108
|
-
export const PackageTestScripts = definePackagePolicy(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
108
|
+
export const PackageTestScripts = definePackagePolicy({
|
|
109
|
+
name: "PackageTestScripts",
|
|
110
|
+
description: "Ensures packages have test scripts when test directories or test framework dependencies exist.",
|
|
111
|
+
handler: async (json, { file, config }) => {
|
|
112
|
+
// If no config provided, skip validation
|
|
113
|
+
if (config === undefined) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// Skip validation based on package properties and config
|
|
117
|
+
if (shouldSkipValidation(json, config)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
// Get config values
|
|
121
|
+
const testDirectories = config.testDirectories ?? [];
|
|
122
|
+
const testDependencies = config.testDependencies ?? [];
|
|
123
|
+
const requiredScripts = config.requiredScripts ?? ["test"];
|
|
124
|
+
const packageDir = dirname(file);
|
|
125
|
+
const devDeps = json.devDependencies;
|
|
126
|
+
// Check if tests are expected based on what's configured
|
|
127
|
+
const hasTestDir = testDirectories.length > 0 &&
|
|
128
|
+
hasTestDirectory(packageDir, testDirectories);
|
|
129
|
+
const hasTestDep = testDependencies.length > 0 &&
|
|
130
|
+
hasTestDependency(devDeps, testDependencies);
|
|
131
|
+
// If no tests expected based on configured checks, pass
|
|
132
|
+
if (!(hasTestDir || hasTestDep)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
// Tests are expected - check for required scripts
|
|
136
|
+
const missingScripts = findMissingScripts(json.scripts, requiredScripts);
|
|
137
|
+
if (missingScripts.length === 0) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
// Build error message
|
|
141
|
+
const reason = hasTestDir
|
|
142
|
+
? "test directory exists"
|
|
143
|
+
: `test dependencies found (${getPresentTestDependencies(devDeps, testDependencies).join(", ")})`;
|
|
144
|
+
const failResult = {
|
|
145
|
+
name: PackageTestScripts.name,
|
|
146
|
+
file,
|
|
147
|
+
autoFixable: false,
|
|
148
|
+
errorMessages: [
|
|
149
|
+
`Package "${json.name}" has ${reason} but is missing test scripts: ${missingScripts.join(", ")}`,
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
return failResult;
|
|
153
|
+
},
|
|
148
154
|
});
|
|
149
155
|
//# sourceMappingURL=PackageTestScripts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PackageTestScripts.js","sourceRoot":"","sources":["../../src/policies/PackageTestScripts.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { dirname, join } from \"pathe\";\nimport type { PackageJson } from \"type-fest\";\nimport type { PolicyFailure } from \"../policy.js\";\nimport { definePackagePolicy } from \"../policyDefiners/definePackagePolicy.js\";\n\n/**\n * Configuration for the PackageTestScripts policy.\n *\n * This policy ensures that packages have appropriate test scripts when they\n * contain test files or have test-related dependencies.\n *\n * The policy automatically determines what to check based on what's configured:\n * - If `testDirectories` is set, checks for test directories\n * - If `testDependencies` is set, checks for test dependencies\n * - If both are set, a package triggers validation if either condition is met\n *\n * @example\n * ```typescript\n * const config: PackageTestScriptsConfig = {\n * // Check for these test directories\n * testDirectories: [\"test\", \"tests\", \"__tests__\"],\n * // Require test script if these devDependencies exist\n * testDependencies: [\"vitest\", \"jest\", \"mocha\"],\n * // Required test script names\n * requiredScripts: [\"test\"],\n * };\n * ```\n *\n * @alpha\n */\nexport interface PackageTestScriptsConfig {\n\t/**\n\t * Directory names that indicate test files are present.\n\t *\n\t * If any of these directories exist relative to the package.json,\n\t * the policy will require test scripts to be present.\n\t *\n\t * If not set or empty, directory checking is skipped.\n\t *\n\t * @example\n\t * ```typescript\n\t * testDirectories: [\"test\", \"tests\", \"__tests__\"]\n\t * ```\n\t */\n\ttestDirectories?: string[];\n\n\t/**\n\t * Test framework dependencies that indicate tests are expected.\n\t *\n\t * If any of these appear in devDependencies, the policy will require\n\t * test scripts to be present.\n\t *\n\t * If not set or empty, dependency checking is skipped.\n\t *\n\t * @example\n\t * ```typescript\n\t * testDependencies: [\"vitest\", \"jest\", \"mocha\", \"@jest/globals\"]\n\t * ```\n\t */\n\ttestDependencies?: string[];\n\n\t/**\n\t * Script names that must exist when tests are detected.\n\t *\n\t * @defaultValue [\"test\"]\n\t */\n\trequiredScripts?: string[];\n\n\t/**\n\t * Package names or scopes to exclude from validation.\n\t */\n\texcludePackages?: string[];\n}\n\n/**\n * Check if a package should be excluded from validation.\n */\nfunction isExcluded(\n\tpackageName: string,\n\texcludePackages: string[] | undefined,\n): boolean {\n\tif (excludePackages === undefined || excludePackages.length === 0) {\n\t\treturn false;\n\t}\n\n\tfor (const pattern of excludePackages) {\n\t\tif (pattern.startsWith(\"@\") && packageName.startsWith(`${pattern}/`)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (packageName === pattern) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\n/**\n * Check if any test directories exist.\n */\nfunction hasTestDirectory(\n\tpackageDir: string,\n\ttestDirectories: string[],\n): boolean {\n\tfor (const testDir of testDirectories) {\n\t\tconst testPath = join(packageDir, testDir);\n\t\tif (existsSync(testPath)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Check if any test dependencies are present.\n */\nfunction hasTestDependency(\n\tdevDependencies: Record<string, string> | undefined,\n\ttestDependencies: string[],\n): boolean {\n\tif (devDependencies === undefined) {\n\t\treturn false;\n\t}\n\n\treturn testDependencies.some((dep) => devDependencies[dep] !== undefined);\n}\n\n/**\n * Find missing scripts from the required list.\n */\nfunction findMissingScripts(\n\tscripts: PackageJson[\"scripts\"],\n\trequiredScripts: string[],\n): string[] {\n\treturn requiredScripts.filter(\n\t\t(scriptName) => scripts?.[scriptName] === undefined,\n\t);\n}\n\n/**\n * Get the list of test dependencies that are present in devDependencies.\n */\nfunction getPresentTestDependencies(\n\tdevDependencies: Record<string, string> | undefined,\n\ttestDependencies: string[],\n): string[] {\n\tif (devDependencies === undefined) {\n\t\treturn [];\n\t}\n\treturn testDependencies.filter((d) => devDependencies[d] !== undefined);\n}\n\n/**\n * Check if a package should be validated based on config and package properties.\n */\nfunction shouldSkipValidation(\n\tjson: PackageJson,\n\tconfig: PackageTestScriptsConfig,\n): boolean {\n\tconst packageName = json.name;\n\n\t// Skip packages without a name or root package\n\tif (packageName === undefined || packageName === \"root\") {\n\t\treturn true;\n\t}\n\n\t// Check exclusions\n\tif (isExcluded(packageName, config.excludePackages)) {\n\t\treturn true;\n\t}\n\n\t// Skip if neither directories nor dependencies are configured\n\tconst testDirectories = config.testDirectories ?? [];\n\tconst testDependencies = config.testDependencies ?? [];\n\n\treturn testDirectories.length === 0 && testDependencies.length === 0;\n}\n\n/**\n * A policy that ensures packages have test scripts when test files exist.\n *\n * This policy helps maintain testing discipline by requiring test scripts\n * for packages that have test directories or test framework dependencies.\n *\n * @remarks\n *\n * The policy detects that tests are expected based on:\n *\n * 1. **Test directories**: If `test/`, `tests/`, or `__tests__/` exists\n * 2. **Test dependencies**: If vitest, jest, mocha, etc. are in devDependencies\n *\n * When tests are detected, the policy requires specified test scripts to exist.\n *\n * @example\n * ```typescript\n * import { makePolicy } from \"repopo\";\n * import { PackageTestScripts } from \"repopo/policies\";\n *\n * const config: RepopoConfig = {\n * policies: [\n * makePolicy(PackageTestScripts, {\n * testDirectories: [\"test\", \"tests\"],\n * testDependencies: [\"vitest\", \"jest\"],\n * requiredScripts: [\"test\"],\n * }),\n * ],\n * };\n * ```\n *\n * @alpha\n */\nexport const PackageTestScripts = definePackagePolicy<\n\tPackageJson,\n\tPackageTestScriptsConfig | undefined\n>(\"PackageTestScripts\", async (json, { file, config }) => {\n\t// If no config provided, skip validation\n\tif (config === undefined) {\n\t\treturn true;\n\t}\n\n\t// Skip validation based on package properties and config\n\tif (shouldSkipValidation(json, config)) {\n\t\treturn true;\n\t}\n\n\t// Get config values\n\tconst testDirectories = config.testDirectories ?? [];\n\tconst testDependencies = config.testDependencies ?? [];\n\tconst requiredScripts = config.requiredScripts ?? [\"test\"];\n\tconst packageDir = dirname(file);\n\tconst devDeps = json.devDependencies as Record<string, string> | undefined;\n\n\t// Check if tests are expected based on what's configured\n\tconst hasTestDir =\n\t\ttestDirectories.length > 0 && hasTestDirectory(packageDir, testDirectories);\n\tconst hasTestDep =\n\t\ttestDependencies.length > 0 && hasTestDependency(devDeps, testDependencies);\n\n\t// If no tests expected based on configured checks, pass\n\tif (!(hasTestDir || hasTestDep)) {\n\t\treturn true;\n\t}\n\n\t// Tests are expected - check for required scripts\n\tconst missingScripts = findMissingScripts(json.scripts, requiredScripts);\n\n\tif (missingScripts.length === 0) {\n\t\treturn true;\n\t}\n\n\t// Build error message\n\tconst reason = hasTestDir\n\t\t? \"test directory exists\"\n\t\t: `test dependencies found (${getPresentTestDependencies(devDeps, testDependencies).join(\", \")})`;\n\n\tconst failResult: PolicyFailure = {\n\t\tname: PackageTestScripts.name,\n\t\tfile,\n\t\tautoFixable: false,\n\t\terrorMessages: [\n\t\t\t`Package \"${json.name}\" has ${reason} but is missing test scripts: ${missingScripts.join(\", \")}`,\n\t\t],\n\t};\n\treturn failResult;\n});\n"]}
|
|
1
|
+
{"version":3,"file":"PackageTestScripts.js","sourceRoot":"","sources":["../../src/policies/PackageTestScripts.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { dirname, join } from \"pathe\";\nimport type { PackageJson } from \"type-fest\";\nimport type { PolicyFailure } from \"../policy.js\";\nimport { definePackagePolicy } from \"../policyDefiners/definePackagePolicy.js\";\n\n/**\n * Configuration for the PackageTestScripts policy.\n *\n * This policy ensures that packages have appropriate test scripts when they\n * contain test files or have test-related dependencies.\n *\n * The policy automatically determines what to check based on what's configured:\n * - If `testDirectories` is set, checks for test directories\n * - If `testDependencies` is set, checks for test dependencies\n * - If both are set, a package triggers validation if either condition is met\n *\n * @example\n * ```typescript\n * const config: PackageTestScriptsConfig = {\n * // Check for these test directories\n * testDirectories: [\"test\", \"tests\", \"__tests__\"],\n * // Require test script if these devDependencies exist\n * testDependencies: [\"vitest\", \"jest\", \"mocha\"],\n * // Required test script names\n * requiredScripts: [\"test\"],\n * };\n * ```\n *\n * @alpha\n */\nexport interface PackageTestScriptsConfig {\n\t/**\n\t * Directory names that indicate test files are present.\n\t *\n\t * If any of these directories exist relative to the package.json,\n\t * the policy will require test scripts to be present.\n\t *\n\t * If not set or empty, directory checking is skipped.\n\t *\n\t * @example\n\t * ```typescript\n\t * testDirectories: [\"test\", \"tests\", \"__tests__\"]\n\t * ```\n\t */\n\ttestDirectories?: string[];\n\n\t/**\n\t * Test framework dependencies that indicate tests are expected.\n\t *\n\t * If any of these appear in devDependencies, the policy will require\n\t * test scripts to be present.\n\t *\n\t * If not set or empty, dependency checking is skipped.\n\t *\n\t * @example\n\t * ```typescript\n\t * testDependencies: [\"vitest\", \"jest\", \"mocha\", \"@jest/globals\"]\n\t * ```\n\t */\n\ttestDependencies?: string[];\n\n\t/**\n\t * Script names that must exist when tests are detected.\n\t *\n\t * @defaultValue [\"test\"]\n\t */\n\trequiredScripts?: string[];\n\n\t/**\n\t * Package names or scopes to exclude from validation.\n\t */\n\texcludePackages?: string[];\n}\n\n/**\n * Check if a package should be excluded from validation.\n */\nfunction isExcluded(\n\tpackageName: string,\n\texcludePackages: string[] | undefined,\n): boolean {\n\tif (excludePackages === undefined || excludePackages.length === 0) {\n\t\treturn false;\n\t}\n\n\tfor (const pattern of excludePackages) {\n\t\tif (pattern.startsWith(\"@\") && packageName.startsWith(`${pattern}/`)) {\n\t\t\treturn true;\n\t\t}\n\t\tif (packageName === pattern) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\n/**\n * Check if any test directories exist.\n */\nfunction hasTestDirectory(\n\tpackageDir: string,\n\ttestDirectories: string[],\n): boolean {\n\tfor (const testDir of testDirectories) {\n\t\tconst testPath = join(packageDir, testDir);\n\t\tif (existsSync(testPath)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Check if any test dependencies are present.\n */\nfunction hasTestDependency(\n\tdevDependencies: Record<string, string> | undefined,\n\ttestDependencies: string[],\n): boolean {\n\tif (devDependencies === undefined) {\n\t\treturn false;\n\t}\n\n\treturn testDependencies.some((dep) => devDependencies[dep] !== undefined);\n}\n\n/**\n * Find missing scripts from the required list.\n */\nfunction findMissingScripts(\n\tscripts: PackageJson[\"scripts\"],\n\trequiredScripts: string[],\n): string[] {\n\treturn requiredScripts.filter(\n\t\t(scriptName) => scripts?.[scriptName] === undefined,\n\t);\n}\n\n/**\n * Get the list of test dependencies that are present in devDependencies.\n */\nfunction getPresentTestDependencies(\n\tdevDependencies: Record<string, string> | undefined,\n\ttestDependencies: string[],\n): string[] {\n\tif (devDependencies === undefined) {\n\t\treturn [];\n\t}\n\treturn testDependencies.filter((d) => devDependencies[d] !== undefined);\n}\n\n/**\n * Check if a package should be validated based on config and package properties.\n */\nfunction shouldSkipValidation(\n\tjson: PackageJson,\n\tconfig: PackageTestScriptsConfig,\n): boolean {\n\tconst packageName = json.name;\n\n\t// Skip packages without a name or root package\n\tif (packageName === undefined || packageName === \"root\") {\n\t\treturn true;\n\t}\n\n\t// Check exclusions\n\tif (isExcluded(packageName, config.excludePackages)) {\n\t\treturn true;\n\t}\n\n\t// Skip if neither directories nor dependencies are configured\n\tconst testDirectories = config.testDirectories ?? [];\n\tconst testDependencies = config.testDependencies ?? [];\n\n\treturn testDirectories.length === 0 && testDependencies.length === 0;\n}\n\n/**\n * A policy that ensures packages have test scripts when test files exist.\n *\n * This policy helps maintain testing discipline by requiring test scripts\n * for packages that have test directories or test framework dependencies.\n *\n * @remarks\n *\n * The policy detects that tests are expected based on:\n *\n * 1. **Test directories**: If `test/`, `tests/`, or `__tests__/` exists\n * 2. **Test dependencies**: If vitest, jest, mocha, etc. are in devDependencies\n *\n * When tests are detected, the policy requires specified test scripts to exist.\n *\n * @example\n * ```typescript\n * import { makePolicy } from \"repopo\";\n * import { PackageTestScripts } from \"repopo/policies\";\n *\n * const config: RepopoConfig = {\n * policies: [\n * makePolicy(PackageTestScripts, {\n * testDirectories: [\"test\", \"tests\"],\n * testDependencies: [\"vitest\", \"jest\"],\n * requiredScripts: [\"test\"],\n * }),\n * ],\n * };\n * ```\n *\n * @alpha\n */\nexport const PackageTestScripts = definePackagePolicy<\n\tPackageJson,\n\tPackageTestScriptsConfig | undefined\n>({\n\tname: \"PackageTestScripts\",\n\tdescription:\n\t\t\"Ensures packages have test scripts when test directories or test framework dependencies exist.\",\n\thandler: async (json, { file, config }) => {\n\t\t// If no config provided, skip validation\n\t\tif (config === undefined) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Skip validation based on package properties and config\n\t\tif (shouldSkipValidation(json, config)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Get config values\n\t\tconst testDirectories = config.testDirectories ?? [];\n\t\tconst testDependencies = config.testDependencies ?? [];\n\t\tconst requiredScripts = config.requiredScripts ?? [\"test\"];\n\t\tconst packageDir = dirname(file);\n\t\tconst devDeps = json.devDependencies as Record<string, string> | undefined;\n\n\t\t// Check if tests are expected based on what's configured\n\t\tconst hasTestDir =\n\t\t\ttestDirectories.length > 0 &&\n\t\t\thasTestDirectory(packageDir, testDirectories);\n\t\tconst hasTestDep =\n\t\t\ttestDependencies.length > 0 &&\n\t\t\thasTestDependency(devDeps, testDependencies);\n\n\t\t// If no tests expected based on configured checks, pass\n\t\tif (!(hasTestDir || hasTestDep)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Tests are expected - check for required scripts\n\t\tconst missingScripts = findMissingScripts(json.scripts, requiredScripts);\n\n\t\tif (missingScripts.length === 0) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// Build error message\n\t\tconst reason = hasTestDir\n\t\t\t? \"test directory exists\"\n\t\t\t: `test dependencies found (${getPresentTestDependencies(devDeps, testDependencies).join(\", \")})`;\n\n\t\tconst failResult: PolicyFailure = {\n\t\t\tname: PackageTestScripts.name,\n\t\t\tfile,\n\t\t\tautoFixable: false,\n\t\t\terrorMessages: [\n\t\t\t\t`Package \"${json.name}\" has ${reason} but is missing test scripts: ${missingScripts.join(\", \")}`,\n\t\t\t],\n\t\t};\n\t\treturn failResult;\n\t},\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequiredGitignorePatterns.d.ts","sourceRoot":"","sources":["../../src/policies/RequiredGitignorePatterns.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,gBAAgB,EAGhB,MAAM,cAAc,CAAC;AAEtB;;;;GAIG;AACH,MAAM,WAAW,iCAAiC;IACjD;;;OAGG;IACH,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE;AAyMD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,gBAAgB,CAAC,iCAAiC,
|
|
1
|
+
{"version":3,"file":"RequiredGitignorePatterns.d.ts","sourceRoot":"","sources":["../../src/policies/RequiredGitignorePatterns.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,gBAAgB,EAGhB,MAAM,cAAc,CAAC;AAEtB;;;;GAIG;AACH,MAAM,WAAW,iCAAiC;IACjD;;;OAGG;IACH,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE;AAyMD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,gBAAgB,CAAC,iCAAiC,CAgBvF,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "pathe";
|
|
3
3
|
import { makePolicyDefinition } from "../makePolicy.js";
|
|
4
4
|
/**
|
|
@@ -90,8 +90,8 @@ function createGitignoreContent(patterns) {
|
|
|
90
90
|
/**
|
|
91
91
|
* Handle the case when .gitignore exists.
|
|
92
92
|
*/
|
|
93
|
-
function handleExistingGitignore(filePath, patterns, file, resolve) {
|
|
94
|
-
const gitignoreContent =
|
|
93
|
+
async function handleExistingGitignore(filePath, patterns, file, resolve) {
|
|
94
|
+
const gitignoreContent = await readFile(filePath, "utf-8");
|
|
95
95
|
const lines = gitignoreContent.split("\n");
|
|
96
96
|
const missingPatterns = findMissingPatterns(patterns, lines);
|
|
97
97
|
if (missingPatterns.length === 0) {
|
|
@@ -112,7 +112,7 @@ function handleExistingGitignore(filePath, patterns, file, resolve) {
|
|
|
112
112
|
};
|
|
113
113
|
try {
|
|
114
114
|
const newContent = addPatternsToContent(gitignoreContent, missingPatterns);
|
|
115
|
-
|
|
115
|
+
await writeFile(filePath, newContent);
|
|
116
116
|
fixResult.resolved = true;
|
|
117
117
|
}
|
|
118
118
|
catch (error) {
|
|
@@ -127,7 +127,7 @@ function handleExistingGitignore(filePath, patterns, file, resolve) {
|
|
|
127
127
|
/**
|
|
128
128
|
* Handle the case when .gitignore doesn't exist.
|
|
129
129
|
*/
|
|
130
|
-
function handleMissingGitignore(filePath, patterns, file, resolve) {
|
|
130
|
+
async function handleMissingGitignore(filePath, patterns, file, resolve) {
|
|
131
131
|
const result = {
|
|
132
132
|
name: RequiredGitignorePatterns.name,
|
|
133
133
|
file,
|
|
@@ -141,7 +141,7 @@ function handleMissingGitignore(filePath, patterns, file, resolve) {
|
|
|
141
141
|
};
|
|
142
142
|
try {
|
|
143
143
|
const content = createGitignoreContent(patterns);
|
|
144
|
-
|
|
144
|
+
await writeFile(filePath, content);
|
|
145
145
|
fixResult.resolved = true;
|
|
146
146
|
}
|
|
147
147
|
catch (writeError) {
|
|
@@ -159,14 +159,19 @@ function handleMissingGitignore(filePath, patterns, file, resolve) {
|
|
|
159
159
|
*
|
|
160
160
|
* @alpha
|
|
161
161
|
*/
|
|
162
|
-
export const RequiredGitignorePatterns = makePolicyDefinition(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
export const RequiredGitignorePatterns = makePolicyDefinition({
|
|
163
|
+
name: "RequiredGitignorePatterns",
|
|
164
|
+
description: "Ensures .gitignore contains required patterns to prevent committing sensitive files, dependencies, and build artifacts.",
|
|
165
|
+
match: /^\.gitignore$/,
|
|
166
|
+
handler: async ({ file, root, resolve, config }) => {
|
|
167
|
+
const patterns = config?.patterns ?? DEFAULT_PATTERNS;
|
|
168
|
+
const filePath = path.join(root, file);
|
|
169
|
+
try {
|
|
170
|
+
return await handleExistingGitignore(filePath, patterns, file, resolve);
|
|
171
|
+
}
|
|
172
|
+
catch (_error) {
|
|
173
|
+
return await handleMissingGitignore(filePath, patterns, file, resolve);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
171
176
|
});
|
|
172
177
|
//# sourceMappingURL=RequiredGitignorePatterns.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequiredGitignorePatterns.js","sourceRoot":"","sources":["../../src/policies/RequiredGitignorePatterns.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync, writeFileSync } from \"node:fs\";\nimport path from \"pathe\";\nimport { makePolicyDefinition } from \"../makePolicy.js\";\nimport type {\n\tPolicyDefinition,\n\tPolicyFailure,\n\tPolicyFixResult,\n} from \"../policy.js\";\n\n/**\n * Policy settings for the RequiredGitignorePatterns repo policy.\n *\n * @alpha\n */\nexport interface RequiredGitignorePatternsSettings {\n\t/**\n\t * Array of patterns that must be present in .gitignore.\n\t * Each pattern can be a string or an object with pattern and comment.\n\t */\n\tpatterns: Array<string | { pattern: string; comment?: string }>;\n}\n\n/**\n * Default patterns that should typically be in .gitignore files.\n */\nconst DEFAULT_PATTERNS = [\n\t{ pattern: \"node_modules/\", comment: \"Dependencies\" },\n\t{ pattern: \".env\", comment: \"Environment files\" },\n\t{ pattern: \".env.*\", comment: \"Environment files\" },\n\t{ pattern: \"!.env.example\", comment: \"Allow example env file\" },\n\t{ pattern: \"dist/\", comment: \"Build output\" },\n\t{ pattern: \"build/\", comment: \"Build output\" },\n\t{ pattern: \".DS_Store\", comment: \"macOS system files\" },\n\t{ pattern: \"Thumbs.db\", comment: \"Windows system files\" },\n];\n\n/**\n * Check if a pattern exists in gitignore content.\n * Handles patterns with inline comments (e.g., \"node_modules/ # comment\").\n */\nfunction patternExists(lines: string[], pattern: string): boolean {\n\treturn lines.some((line) => {\n\t\tconst trimmed = line.trim();\n\t\t// Remove inline comments (everything after # that's not escaped)\n\t\tconst withoutComment = trimmed.split(\"#\")[0]?.trim() ?? \"\";\n\t\treturn (\n\t\t\twithoutComment === pattern ||\n\t\t\ttrimmed === pattern ||\n\t\t\ttrimmed.startsWith(`${pattern} `) ||\n\t\t\ttrimmed.startsWith(`${pattern}\\t`)\n\t\t);\n\t});\n}\n\n/**\n * Find missing patterns in gitignore content.\n */\nfunction findMissingPatterns(\n\tpatterns: Array<string | { pattern: string; comment?: string }>,\n\tlines: string[],\n): Array<string | { pattern: string; comment?: string }> {\n\tconst missingPatterns: Array<string | { pattern: string; comment?: string }> =\n\t\t[];\n\n\tfor (const patternConfig of patterns) {\n\t\tconst pattern =\n\t\t\ttypeof patternConfig === \"string\" ? patternConfig : patternConfig.pattern;\n\n\t\tif (!patternExists(lines, pattern)) {\n\t\t\tmissingPatterns.push(patternConfig);\n\t\t}\n\t}\n\n\treturn missingPatterns;\n}\n\n/**\n * Add patterns to gitignore content.\n */\nfunction addPatternsToContent(\n\tcontent: string,\n\tmissingPatterns: Array<string | { pattern: string; comment?: string }>,\n): string {\n\tlet newContent = content;\n\n\t// Ensure file ends with newline\n\tif (!newContent.endsWith(\"\\n\")) {\n\t\tnewContent += \"\\n\";\n\t}\n\n\t// Add a section header if we're adding patterns\n\tif (missingPatterns.length > 0) {\n\t\tnewContent += \"\\n# Added by repopo RequiredGitignorePatterns policy\\n\";\n\t}\n\n\t// Add each missing pattern\n\tfor (const patternConfig of missingPatterns) {\n\t\tif (typeof patternConfig === \"string\") {\n\t\t\tnewContent += `${patternConfig}\\n`;\n\t\t} else {\n\t\t\tif (patternConfig.comment) {\n\t\t\t\tnewContent += `# ${patternConfig.comment}\\n`;\n\t\t\t}\n\t\t\tnewContent += `${patternConfig.pattern}\\n`;\n\t\t}\n\t}\n\n\treturn newContent;\n}\n\n/**\n * Create gitignore content from scratch.\n */\nfunction createGitignoreContent(\n\tpatterns: Array<string | { pattern: string; comment?: string }>,\n): string {\n\tlet content = \"# Generated by repopo RequiredGitignorePatterns policy\\n\\n\";\n\n\tfor (const patternConfig of patterns) {\n\t\tif (typeof patternConfig === \"string\") {\n\t\t\tcontent += `${patternConfig}\\n`;\n\t\t} else {\n\t\t\tif (patternConfig.comment) {\n\t\t\t\tcontent += `# ${patternConfig.comment}\\n`;\n\t\t\t}\n\t\t\tcontent += `${patternConfig.pattern}\\n`;\n\t\t}\n\t}\n\n\treturn content;\n}\n\n/**\n * Handle the case when .gitignore exists.\n */\nfunction handleExistingGitignore(\n\tfilePath: string,\n\tpatterns: Array<string | { pattern: string; comment?: string }>,\n\tfile: string,\n\tresolve: boolean,\n) {\n\tconst gitignoreContent = readFileSync(filePath, \"utf-8\");\n\tconst lines = gitignoreContent.split(\"\\n\");\n\tconst missingPatterns = findMissingPatterns(patterns, lines);\n\n\tif (missingPatterns.length === 0) {\n\t\treturn true;\n\t}\n\n\tconst result: PolicyFailure = {\n\t\tname: RequiredGitignorePatterns.name,\n\t\tfile,\n\t\tautoFixable: true,\n\t\terrorMessages: [\n\t\t\t`Missing required .gitignore patterns: ${missingPatterns.map((p) => (typeof p === \"string\" ? p : p.pattern)).join(\", \")}`,\n\t\t],\n\t};\n\n\tif (resolve) {\n\t\tconst fixResult: PolicyFixResult = {\n\t\t\t...result,\n\t\t\tresolved: false,\n\t\t};\n\n\t\ttry {\n\t\t\tconst newContent = addPatternsToContent(\n\t\t\t\tgitignoreContent,\n\t\t\t\tmissingPatterns,\n\t\t\t);\n\t\t\twriteFileSync(filePath, newContent);\n\t\t\tfixResult.resolved = true;\n\t\t} catch (error: unknown) {\n\t\t\tfixResult.errorMessages = [\n\t\t\t\t`Failed to update .gitignore: ${(error as Error).message}`,\n\t\t\t];\n\t\t}\n\n\t\treturn fixResult;\n\t}\n\n\treturn result;\n}\n\n/**\n * Handle the case when .gitignore doesn't exist.\n */\nfunction handleMissingGitignore(\n\tfilePath: string,\n\tpatterns: Array<string | { pattern: string; comment?: string }>,\n\tfile: string,\n\tresolve: boolean,\n) {\n\tconst result: PolicyFailure = {\n\t\tname: RequiredGitignorePatterns.name,\n\t\tfile,\n\t\tautoFixable: true,\n\t\terrorMessages: [\"No .gitignore file found in repository root\"],\n\t};\n\n\tif (resolve) {\n\t\tconst fixResult: PolicyFixResult = {\n\t\t\t...result,\n\t\t\tresolved: false,\n\t\t};\n\n\t\ttry {\n\t\t\tconst content = createGitignoreContent(patterns);\n\t\t\twriteFileSync(filePath, content);\n\t\t\tfixResult.resolved = true;\n\t\t} catch (writeError: unknown) {\n\t\t\tfixResult.errorMessages = [\n\t\t\t\t`Failed to create .gitignore: ${(writeError as Error).message}`,\n\t\t\t];\n\t\t}\n\n\t\treturn fixResult;\n\t}\n\n\treturn result;\n}\n\n/**\n * A repo policy that ensures .gitignore contains required patterns to prevent\n * committing sensitive files, dependencies, and build artifacts.\n *\n * @alpha\n */\nexport const RequiredGitignorePatterns: PolicyDefinition<RequiredGitignorePatternsSettings> =\n\tmakePolicyDefinition(\n\t\t\"RequiredGitignorePatterns\",\n\t\t/^\\.gitignore$/,\n\t\tasync ({ file, root, resolve, config }) => {\n\t\t\tconst patterns = config?.patterns ?? DEFAULT_PATTERNS;\n\t\t\tconst filePath = path.join(root, file);\n\n\t\t\ttry {\n\t\t\t\treturn handleExistingGitignore(filePath, patterns, file, resolve);\n\t\t\t} catch (_error: unknown) {\n\t\t\t\treturn handleMissingGitignore(filePath, patterns, file, resolve);\n\t\t\t}\n\t\t},\n\t);\n"]}
|
|
1
|
+
{"version":3,"file":"RequiredGitignorePatterns.js","sourceRoot":"","sources":["../../src/policies/RequiredGitignorePatterns.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile, writeFile } from \"node:fs/promises\";\nimport path from \"pathe\";\nimport { makePolicyDefinition } from \"../makePolicy.js\";\nimport type {\n\tPolicyDefinition,\n\tPolicyFailure,\n\tPolicyFixResult,\n} from \"../policy.js\";\n\n/**\n * Policy settings for the RequiredGitignorePatterns repo policy.\n *\n * @alpha\n */\nexport interface RequiredGitignorePatternsSettings {\n\t/**\n\t * Array of patterns that must be present in .gitignore.\n\t * Each pattern can be a string or an object with pattern and comment.\n\t */\n\tpatterns: Array<string | { pattern: string; comment?: string }>;\n}\n\n/**\n * Default patterns that should typically be in .gitignore files.\n */\nconst DEFAULT_PATTERNS = [\n\t{ pattern: \"node_modules/\", comment: \"Dependencies\" },\n\t{ pattern: \".env\", comment: \"Environment files\" },\n\t{ pattern: \".env.*\", comment: \"Environment files\" },\n\t{ pattern: \"!.env.example\", comment: \"Allow example env file\" },\n\t{ pattern: \"dist/\", comment: \"Build output\" },\n\t{ pattern: \"build/\", comment: \"Build output\" },\n\t{ pattern: \".DS_Store\", comment: \"macOS system files\" },\n\t{ pattern: \"Thumbs.db\", comment: \"Windows system files\" },\n];\n\n/**\n * Check if a pattern exists in gitignore content.\n * Handles patterns with inline comments (e.g., \"node_modules/ # comment\").\n */\nfunction patternExists(lines: string[], pattern: string): boolean {\n\treturn lines.some((line) => {\n\t\tconst trimmed = line.trim();\n\t\t// Remove inline comments (everything after # that's not escaped)\n\t\tconst withoutComment = trimmed.split(\"#\")[0]?.trim() ?? \"\";\n\t\treturn (\n\t\t\twithoutComment === pattern ||\n\t\t\ttrimmed === pattern ||\n\t\t\ttrimmed.startsWith(`${pattern} `) ||\n\t\t\ttrimmed.startsWith(`${pattern}\\t`)\n\t\t);\n\t});\n}\n\n/**\n * Find missing patterns in gitignore content.\n */\nfunction findMissingPatterns(\n\tpatterns: Array<string | { pattern: string; comment?: string }>,\n\tlines: string[],\n): Array<string | { pattern: string; comment?: string }> {\n\tconst missingPatterns: Array<string | { pattern: string; comment?: string }> =\n\t\t[];\n\n\tfor (const patternConfig of patterns) {\n\t\tconst pattern =\n\t\t\ttypeof patternConfig === \"string\" ? patternConfig : patternConfig.pattern;\n\n\t\tif (!patternExists(lines, pattern)) {\n\t\t\tmissingPatterns.push(patternConfig);\n\t\t}\n\t}\n\n\treturn missingPatterns;\n}\n\n/**\n * Add patterns to gitignore content.\n */\nfunction addPatternsToContent(\n\tcontent: string,\n\tmissingPatterns: Array<string | { pattern: string; comment?: string }>,\n): string {\n\tlet newContent = content;\n\n\t// Ensure file ends with newline\n\tif (!newContent.endsWith(\"\\n\")) {\n\t\tnewContent += \"\\n\";\n\t}\n\n\t// Add a section header if we're adding patterns\n\tif (missingPatterns.length > 0) {\n\t\tnewContent += \"\\n# Added by repopo RequiredGitignorePatterns policy\\n\";\n\t}\n\n\t// Add each missing pattern\n\tfor (const patternConfig of missingPatterns) {\n\t\tif (typeof patternConfig === \"string\") {\n\t\t\tnewContent += `${patternConfig}\\n`;\n\t\t} else {\n\t\t\tif (patternConfig.comment) {\n\t\t\t\tnewContent += `# ${patternConfig.comment}\\n`;\n\t\t\t}\n\t\t\tnewContent += `${patternConfig.pattern}\\n`;\n\t\t}\n\t}\n\n\treturn newContent;\n}\n\n/**\n * Create gitignore content from scratch.\n */\nfunction createGitignoreContent(\n\tpatterns: Array<string | { pattern: string; comment?: string }>,\n): string {\n\tlet content = \"# Generated by repopo RequiredGitignorePatterns policy\\n\\n\";\n\n\tfor (const patternConfig of patterns) {\n\t\tif (typeof patternConfig === \"string\") {\n\t\t\tcontent += `${patternConfig}\\n`;\n\t\t} else {\n\t\t\tif (patternConfig.comment) {\n\t\t\t\tcontent += `# ${patternConfig.comment}\\n`;\n\t\t\t}\n\t\t\tcontent += `${patternConfig.pattern}\\n`;\n\t\t}\n\t}\n\n\treturn content;\n}\n\n/**\n * Handle the case when .gitignore exists.\n */\nasync function handleExistingGitignore(\n\tfilePath: string,\n\tpatterns: Array<string | { pattern: string; comment?: string }>,\n\tfile: string,\n\tresolve: boolean,\n): Promise<true | PolicyFailure | PolicyFixResult> {\n\tconst gitignoreContent = await readFile(filePath, \"utf-8\");\n\tconst lines = gitignoreContent.split(\"\\n\");\n\tconst missingPatterns = findMissingPatterns(patterns, lines);\n\n\tif (missingPatterns.length === 0) {\n\t\treturn true;\n\t}\n\n\tconst result: PolicyFailure = {\n\t\tname: RequiredGitignorePatterns.name,\n\t\tfile,\n\t\tautoFixable: true,\n\t\terrorMessages: [\n\t\t\t`Missing required .gitignore patterns: ${missingPatterns.map((p) => (typeof p === \"string\" ? p : p.pattern)).join(\", \")}`,\n\t\t],\n\t};\n\n\tif (resolve) {\n\t\tconst fixResult: PolicyFixResult = {\n\t\t\t...result,\n\t\t\tresolved: false,\n\t\t};\n\n\t\ttry {\n\t\t\tconst newContent = addPatternsToContent(\n\t\t\t\tgitignoreContent,\n\t\t\t\tmissingPatterns,\n\t\t\t);\n\t\t\tawait writeFile(filePath, newContent);\n\t\t\tfixResult.resolved = true;\n\t\t} catch (error: unknown) {\n\t\t\tfixResult.errorMessages = [\n\t\t\t\t`Failed to update .gitignore: ${(error as Error).message}`,\n\t\t\t];\n\t\t}\n\n\t\treturn fixResult;\n\t}\n\n\treturn result;\n}\n\n/**\n * Handle the case when .gitignore doesn't exist.\n */\nasync function handleMissingGitignore(\n\tfilePath: string,\n\tpatterns: Array<string | { pattern: string; comment?: string }>,\n\tfile: string,\n\tresolve: boolean,\n): Promise<PolicyFailure | PolicyFixResult> {\n\tconst result: PolicyFailure = {\n\t\tname: RequiredGitignorePatterns.name,\n\t\tfile,\n\t\tautoFixable: true,\n\t\terrorMessages: [\"No .gitignore file found in repository root\"],\n\t};\n\n\tif (resolve) {\n\t\tconst fixResult: PolicyFixResult = {\n\t\t\t...result,\n\t\t\tresolved: false,\n\t\t};\n\n\t\ttry {\n\t\t\tconst content = createGitignoreContent(patterns);\n\t\t\tawait writeFile(filePath, content);\n\t\t\tfixResult.resolved = true;\n\t\t} catch (writeError: unknown) {\n\t\t\tfixResult.errorMessages = [\n\t\t\t\t`Failed to create .gitignore: ${(writeError as Error).message}`,\n\t\t\t];\n\t\t}\n\n\t\treturn fixResult;\n\t}\n\n\treturn result;\n}\n\n/**\n * A repo policy that ensures .gitignore contains required patterns to prevent\n * committing sensitive files, dependencies, and build artifacts.\n *\n * @alpha\n */\nexport const RequiredGitignorePatterns: PolicyDefinition<RequiredGitignorePatternsSettings> =\n\tmakePolicyDefinition({\n\t\tname: \"RequiredGitignorePatterns\",\n\t\tdescription:\n\t\t\t\"Ensures .gitignore contains required patterns to prevent committing sensitive files, dependencies, and build artifacts.\",\n\t\tmatch: /^\\.gitignore$/,\n\t\thandler: async ({ file, root, resolve, config }) => {\n\t\t\tconst patterns = config?.patterns ?? DEFAULT_PATTERNS;\n\t\t\tconst filePath = path.join(root, file);\n\n\t\t\ttry {\n\t\t\t\treturn await handleExistingGitignore(filePath, patterns, file, resolve);\n\t\t\t} catch (_error: unknown) {\n\t\t\t\treturn await handleMissingGitignore(filePath, patterns, file, resolve);\n\t\t\t}\n\t\t},\n\t});\n"]}
|
package/esm/policy.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Operation } from "effection";
|
|
2
|
+
import type { RequireExactlyOne } from "type-fest";
|
|
1
3
|
/**
|
|
2
4
|
* A type representing a policy name.
|
|
3
5
|
*
|
|
@@ -30,17 +32,31 @@ export interface PolicyFunctionArguments<C> {
|
|
|
30
32
|
config?: C | undefined;
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
|
-
* A policy handler
|
|
35
|
+
* A policy handler function that is called to check policy against a file.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Policy handlers can be implemented in two ways:
|
|
39
|
+
* - As an async function returning a Promise
|
|
40
|
+
* - As an Effection generator function returning an Operation
|
|
41
|
+
*
|
|
42
|
+
* Both styles are supported to allow gradual migration and flexibility.
|
|
34
43
|
*
|
|
35
44
|
* @alpha
|
|
36
45
|
*/
|
|
37
|
-
export type PolicyHandler<C = unknown | undefined> = (args: PolicyFunctionArguments<C>) => Promise<PolicyHandlerResult
|
|
46
|
+
export type PolicyHandler<C = unknown | undefined> = ((args: PolicyFunctionArguments<C>) => Promise<PolicyHandlerResult>) | ((args: PolicyFunctionArguments<C>) => Operation<PolicyHandlerResult>);
|
|
38
47
|
/**
|
|
39
48
|
* A standalone function that can be called to resolve a policy failure.
|
|
40
49
|
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* Resolvers can be implemented in two ways:
|
|
52
|
+
* - As an async function returning a Promise
|
|
53
|
+
* - As an Effection generator function returning an Operation
|
|
54
|
+
*
|
|
55
|
+
* Both styles are supported to allow gradual migration and flexibility.
|
|
56
|
+
*
|
|
41
57
|
* @alpha
|
|
42
58
|
*/
|
|
43
|
-
export type PolicyStandaloneResolver<C = undefined> = (args: Omit<PolicyFunctionArguments<C>, "resolve">) => Promise<PolicyFixResult>;
|
|
59
|
+
export type PolicyStandaloneResolver<C = undefined> = (args: Omit<PolicyFunctionArguments<C>, "resolve">) => Promise<PolicyFixResult> | Operation<PolicyFixResult>;
|
|
44
60
|
/**
|
|
45
61
|
* A RepoPolicyDefinition checks and applies policies to files in the repository.
|
|
46
62
|
*
|
|
@@ -62,7 +78,7 @@ export interface PolicyDefinition<C = undefined> {
|
|
|
62
78
|
/**
|
|
63
79
|
* A more detailed description of the policy and its intended function.
|
|
64
80
|
*/
|
|
65
|
-
description
|
|
81
|
+
description: string;
|
|
66
82
|
/**
|
|
67
83
|
* A regular expression that is used to match files in the repo.
|
|
68
84
|
*/
|
|
@@ -108,7 +124,7 @@ export interface PolicyInstanceSettings<C> {
|
|
|
108
124
|
/**
|
|
109
125
|
* @alpha
|
|
110
126
|
*/
|
|
111
|
-
export type PolicyInstance<C = undefined> = PolicyDefinition<C> & PolicyInstanceSettings<C>;
|
|
127
|
+
export type PolicyInstance<C = undefined> = RequireExactlyOne<PolicyDefinition<C> & PolicyInstanceSettings<C>, "handler">;
|
|
112
128
|
/**
|
|
113
129
|
* A policy failure.
|
|
114
130
|
*
|
|
@@ -158,13 +174,22 @@ export declare function isPolicyFixResult(toCheck: any): toCheck is PolicyFixRes
|
|
|
158
174
|
* @alpha
|
|
159
175
|
*/
|
|
160
176
|
export declare const DefaultPolicies: PolicyDefinition<any>[];
|
|
177
|
+
/**
|
|
178
|
+
* Abstract base class for creating policies with object-based construction.
|
|
179
|
+
*
|
|
180
|
+
* @remarks
|
|
181
|
+
* This class accepts a {@link PolicyDefinition} object in its constructor,
|
|
182
|
+
* making it easier to add new optional properties in the future without breaking changes.
|
|
183
|
+
*
|
|
184
|
+
* @alpha
|
|
185
|
+
*/
|
|
161
186
|
export declare abstract class Policy<C> implements PolicyDefinition<C> {
|
|
162
187
|
readonly name: string;
|
|
188
|
+
readonly description: string;
|
|
163
189
|
readonly match: RegExp;
|
|
164
190
|
readonly handler: PolicyHandler<C>;
|
|
165
|
-
readonly description?: string | undefined;
|
|
166
191
|
readonly defaultConfig?: C | undefined;
|
|
167
192
|
readonly resolver?: PolicyStandaloneResolver<C> | undefined;
|
|
168
|
-
constructor(
|
|
193
|
+
constructor(definition: PolicyDefinition<C>);
|
|
169
194
|
}
|
|
170
195
|
//# sourceMappingURL=policy.d.ts.map
|
package/esm/policy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
|
package/esm/policy.js
CHANGED
|
@@ -4,7 +4,7 @@ import { PackageJsonSorted } from "./policies/PackageJsonSorted.js";
|
|
|
4
4
|
import { PackageScripts } from "./policies/PackageScripts.js";
|
|
5
5
|
// biome-ignore lint/suspicious/noExplicitAny: type guard
|
|
6
6
|
export function isPolicyFixResult(toCheck) {
|
|
7
|
-
if (typeof toCheck !== "object") {
|
|
7
|
+
if (typeof toCheck !== "object" || toCheck === null) {
|
|
8
8
|
return false;
|
|
9
9
|
}
|
|
10
10
|
return "resolved" in toCheck;
|
|
@@ -21,20 +21,29 @@ export const DefaultPolicies = [
|
|
|
21
21
|
PackageJsonSorted,
|
|
22
22
|
PackageScripts,
|
|
23
23
|
];
|
|
24
|
+
/**
|
|
25
|
+
* Abstract base class for creating policies with object-based construction.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* This class accepts a {@link PolicyDefinition} object in its constructor,
|
|
29
|
+
* making it easier to add new optional properties in the future without breaking changes.
|
|
30
|
+
*
|
|
31
|
+
* @alpha
|
|
32
|
+
*/
|
|
24
33
|
export class Policy {
|
|
25
34
|
name;
|
|
35
|
+
description;
|
|
26
36
|
match;
|
|
27
37
|
handler;
|
|
28
|
-
description;
|
|
29
38
|
defaultConfig;
|
|
30
39
|
resolver;
|
|
31
|
-
constructor(
|
|
32
|
-
this.name = name;
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
this.defaultConfig = defaultConfig;
|
|
37
|
-
this.resolver = resolver;
|
|
40
|
+
constructor(definition) {
|
|
41
|
+
this.name = definition.name;
|
|
42
|
+
this.description = definition.description;
|
|
43
|
+
this.match = definition.match;
|
|
44
|
+
this.handler = definition.handler;
|
|
45
|
+
this.defaultConfig = definition.defaultConfig;
|
|
46
|
+
this.resolver = definition.resolver;
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
//# sourceMappingURL=policy.js.map
|
package/esm/policy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,EAAE,gCAAgC,EAAE,MAAM,gDAAgD,CAAC;AAClG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAyM9D,yDAAyD;AACzD,MAAM,UAAU,iBAAiB,CAAC,OAAY;IAC7C,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrD,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,UAAU,IAAI,OAAO,CAAC;AAC9B,CAAC;AAED;;;;GAIG;AAEH,oDAAoD;AACpD,MAAM,CAAC,MAAM,eAAe,GAA4B;IACvD,kBAAkB;IAClB,gCAAgC;IAChC,iBAAiB;IACjB,cAAc;CACL,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,OAAgB,MAAM;IACX,IAAI,CAAS;IACb,WAAW,CAAS;IACpB,KAAK,CAAS;IACd,OAAO,CAAmB;IAC1B,aAAa,CAAiB;IAC9B,QAAQ,CAA2C;IAEnE,YAAmB,UAA+B;QACjD,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC;QAClC,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,aAAa,CAAC;QAC9C,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;IACrC,CAAC;CACD","sourcesContent":["import type { Operation } from \"effection\";\nimport type { RequireExactlyOne } from \"type-fest\";\nimport { NoJsFileExtensions } from \"./policies/NoJsFileExtensions.js\";\nimport { PackageJsonRepoDirectoryProperty } from \"./policies/PackageJsonRepoDirectoryProperty.js\";\nimport { PackageJsonSorted } from \"./policies/PackageJsonSorted.js\";\nimport { PackageScripts } from \"./policies/PackageScripts.js\";\n\n/**\n * A type representing a policy name.\n *\n * @alpha\n */\nexport type PolicyName = string;\n\n/**\n * Arguments passed to policy functions.\n *\n * @alpha\n */\n\nexport interface PolicyFunctionArguments<C> {\n\t/**\n\t * Path to the file, relative to the repo root.\n\t */\n\tfile: string;\n\n\t/**\n\t * Absolute path to the root of the repo.\n\t */\n\troot: string;\n\n\t/**\n\t * If true, the handler should resolve any violations automatically if possible.\n\t */\n\tresolve: boolean;\n\n\t/**\n\t * @remarks\n\t *\n\t * Note that the handler function receives the config as an argument.\n\t */\n\tconfig?: C | undefined;\n}\n\n/**\n * A policy handler function that is called to check policy against a file.\n *\n * @remarks\n * Policy handlers can be implemented in two ways:\n * - As an async function returning a Promise\n * - As an Effection generator function returning an Operation\n *\n * Both styles are supported to allow gradual migration and flexibility.\n *\n * @alpha\n */\nexport type PolicyHandler<C = unknown | undefined> =\n\t| ((args: PolicyFunctionArguments<C>) => Promise<PolicyHandlerResult>)\n\t| ((args: PolicyFunctionArguments<C>) => Operation<PolicyHandlerResult>);\n\n/**\n * A standalone function that can be called to resolve a policy failure.\n *\n * @remarks\n * Resolvers can be implemented in two ways:\n * - As an async function returning a Promise\n * - As an Effection generator function returning an Operation\n *\n * Both styles are supported to allow gradual migration and flexibility.\n *\n * @alpha\n */\nexport type PolicyStandaloneResolver<C = undefined> = (\n\targs: Omit<PolicyFunctionArguments<C>, \"resolve\">,\n) => Promise<PolicyFixResult> | Operation<PolicyFixResult>;\n\n/**\n * A RepoPolicyDefinition checks and applies policies to files in the repository.\n *\n * Each policy has a name and a match regex for matching which files it should apply to. Every file in th repo is\n * enumerated and if it matches the regex for a policy, that policy is applied.\n *\n * Each policy includes a handler function that checks a file against the policy and can optionally resolve any problems\n * (automated resolutions depend on the policy implementation).\n *\n * @typeParam C - type of configuration object used by the policy\n *\n * @alpha\n */\nexport interface PolicyDefinition<C = undefined> {\n\t/**\n\t * The name of the policy; displayed in UI and used in settings.\n\t */\n\tname: PolicyName;\n\n\t/**\n\t * A more detailed description of the policy and its intended function.\n\t */\n\tdescription: string;\n\n\t/**\n\t * A regular expression that is used to match files in the repo.\n\t */\n\tmatch: RegExp;\n\n\t/**\n\t * A handler function that checks if a file is compliant with the policy.\n\t *\n\t * @param file - Repo-relative path to the file to check.\n\t * @param root - Absolute path to the root of the repo.\n\t * @param resolve - If true, automated policy fixes will be applied. Not all policies support automated fixes.\n\t * @returns True if the file passed the policy; otherwise a PolicyFailure object will be returned.\n\t */\n\n\thandler: PolicyHandler<C>;\n\n\t/**\n\t * A resolver function that can be used to automatically address the policy violation.\n\t *\n\t * @param file - Repo-relative path to the file to check.\n\t * @param root - Absolute path to the root of the repo.\n\t * @returns true if the file passed the policy; otherwise a PolicyFailure object will be returned.\n\t */\n\tresolver?: PolicyStandaloneResolver<C> | undefined;\n\n\t/**\n\t * A default config that will be used if none is provided.\n\t */\n\tdefaultConfig?: C | undefined;\n}\n\n/**\n * @alpha\n */\nexport interface PolicyInstanceSettings<C> {\n\t/**\n\t * An array of strings/regular expressions. File paths that match any of these expressions will be completely excluded\n\t * from policy.\n\t *\n\t * Paths will be matched relative to the root of the repo.\n\t */\n\texcludeFiles?: (string | RegExp)[];\n\n\t/**\n\t * The config that is applied to the policy instance.\n\t */\n\tconfig?: C | undefined;\n}\n\n/**\n * @alpha\n */\nexport type PolicyInstance<C = undefined> = RequireExactlyOne<\n\tPolicyDefinition<C> & PolicyInstanceSettings<C>,\n\t\"handler\"\n>;\n\n/**\n * A policy failure.\n *\n * @alpha\n */\nexport interface PolicyFailure {\n\t/**\n\t * Name of the policy that failed.\n\t */\n\tname: PolicyName;\n\n\t/**\n\t * Path to the file that failed the policy.\n\t */\n\tfile: string;\n\n\t/**\n\t * Set to `true` if the policy can be fixed automatically.\n\t */\n\tautoFixable?: boolean | undefined;\n\n\t/**\n\t * An optional error message accompanying the failure.\n\t */\n\terrorMessages: string[];\n\n\t/**\n\t * An optional string that tells the user how to fix the failure(s).\n\t */\n\tmanualFix?: string;\n}\n\n/**\n * The result of an automatic fix for a failing policy.\n *\n * @alpha\n */\nexport interface PolicyFixResult extends PolicyFailure {\n\t/**\n\t * Set to true if the failure was resolved by the automated fixer.\n\t */\n\tresolved: boolean;\n}\n\n/**\n * @alpha\n */\nexport type PolicyHandlerResult = true | PolicyFailure | PolicyFixResult;\n\n// biome-ignore lint/suspicious/noExplicitAny: type guard\nexport function isPolicyFixResult(toCheck: any): toCheck is PolicyFixResult {\n\tif (typeof toCheck !== \"object\" || toCheck === null) {\n\t\treturn false;\n\t}\n\treturn \"resolved\" in toCheck;\n}\n\n/**\n * Default policies included with repopo.\n *\n * @alpha\n */\n\n// biome-ignore lint/suspicious/noExplicitAny: FIXME\nexport const DefaultPolicies: PolicyDefinition<any>[] = [\n\tNoJsFileExtensions,\n\tPackageJsonRepoDirectoryProperty,\n\tPackageJsonSorted,\n\tPackageScripts,\n] as const;\n\n/**\n * Abstract base class for creating policies with object-based construction.\n *\n * @remarks\n * This class accepts a {@link PolicyDefinition} object in its constructor,\n * making it easier to add new optional properties in the future without breaking changes.\n *\n * @alpha\n */\nexport abstract class Policy<C> implements PolicyDefinition<C> {\n\tpublic readonly name: string;\n\tpublic readonly description: string;\n\tpublic readonly match: RegExp;\n\tpublic readonly handler: PolicyHandler<C>;\n\tpublic readonly defaultConfig?: C | undefined;\n\tpublic readonly resolver?: PolicyStandaloneResolver<C> | undefined;\n\n\tpublic constructor(definition: PolicyDefinition<C>) {\n\t\tthis.name = definition.name;\n\t\tthis.description = definition.description;\n\t\tthis.match = definition.match;\n\t\tthis.handler = definition.handler;\n\t\tthis.defaultConfig = definition.defaultConfig;\n\t\tthis.resolver = definition.resolver;\n\t}\n}\n"]}
|
|
@@ -13,9 +13,16 @@ export interface FileHeaderPolicyConfig {
|
|
|
13
13
|
autoGenText?: string;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
+
* Configuration for generating file headers with specific formatting rules.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* This interface extends FileHeaderPolicyConfig and adds formatting-specific
|
|
20
|
+
* properties to control how headers are inserted into different file types.
|
|
21
|
+
*
|
|
16
22
|
* @alpha
|
|
17
23
|
*/
|
|
18
24
|
export interface FileHeaderGeneratorConfig extends Partial<FileHeaderPolicyConfig> {
|
|
25
|
+
/** Regular expression that matches files this header generator applies to */
|
|
19
26
|
match: RegExp;
|
|
20
27
|
/**
|
|
21
28
|
* Regex matching header prefix (e.g. `/*!\r?\n`)
|
|
@@ -33,13 +40,48 @@ export interface FileHeaderGeneratorConfig extends Partial<FileHeaderPolicyConfi
|
|
|
33
40
|
* Regex matching the header postfix.
|
|
34
41
|
*/
|
|
35
42
|
headerEnd?: RegExp;
|
|
43
|
+
/**
|
|
44
|
+
* Function that generates the properly formatted header content for insertion into files.
|
|
45
|
+
*
|
|
46
|
+
* @param content - The original file content
|
|
47
|
+
* @param config - The file header policy configuration
|
|
48
|
+
* @returns The formatted header string to insert
|
|
49
|
+
*/
|
|
36
50
|
replacer: (content: string, config: FileHeaderPolicyConfig) => string;
|
|
37
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Input arguments for defining a file header policy.
|
|
54
|
+
*
|
|
55
|
+
* @alpha
|
|
56
|
+
*/
|
|
57
|
+
export interface DefineFileHeaderPolicyArgs {
|
|
58
|
+
/**
|
|
59
|
+
* The name of the policy.
|
|
60
|
+
*/
|
|
61
|
+
name: string;
|
|
62
|
+
/**
|
|
63
|
+
* A description of the policy's purpose.
|
|
64
|
+
*/
|
|
65
|
+
description: string;
|
|
66
|
+
/**
|
|
67
|
+
* Configuration for how headers are detected and inserted.
|
|
68
|
+
*/
|
|
69
|
+
config: FileHeaderGeneratorConfig;
|
|
70
|
+
}
|
|
38
71
|
/**
|
|
39
72
|
* Given a {@link FileHeaderPolicyConfig}, produces a function that detects correct file headers
|
|
40
73
|
* and returns an error string if the header is missing or incorrect.
|
|
41
74
|
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const MyHeaderPolicy = defineFileHeaderPolicy({
|
|
78
|
+
* name: "MyHeaderPolicy",
|
|
79
|
+
* description: "Ensures files have required headers",
|
|
80
|
+
* config: { match: /\.ts$/, lineStart: /\/\/ /, lineEnd: /\r?\n/, replacer: ... },
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
42
84
|
* @alpha
|
|
43
85
|
*/
|
|
44
|
-
export declare function defineFileHeaderPolicy(
|
|
86
|
+
export declare function defineFileHeaderPolicy(args: DefineFileHeaderPolicyArgs): PolicyDefinition<FileHeaderPolicyConfig>;
|
|
45
87
|
//# sourceMappingURL=defineFileHeaderPolicy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineFileHeaderPolicy.d.ts","sourceRoot":"","sources":["../../src/policyDefiners/defineFileHeaderPolicy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"defineFileHeaderPolicy.d.ts","sourceRoot":"","sources":["../../src/policyDefiners/defineFileHeaderPolicy.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,gBAAgB,EAGhB,MAAM,cAAc,CAAC;AAItB;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACtC;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,yBAChB,SAAQ,OAAO,CAAC,sBAAsB,CAAC;IACvC,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;OAMG;IACH,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,KAAK,MAAM,CAAC;CACtE;AAED;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IAC1C;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,MAAM,EAAE,yBAAyB,CAAC;CAClC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACrC,IAAI,EAAE,0BAA0B,GAC9B,gBAAgB,CAAC,sBAAsB,CAAC,CAoE1C"}
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { EOL as newline } from "node:os";
|
|
3
|
+
import { call } from "effection";
|
|
3
4
|
import { extname } from "pathe";
|
|
4
5
|
const trailingSpaces = /\s*\\r\?\\n/;
|
|
5
6
|
/**
|
|
6
7
|
* Given a {@link FileHeaderPolicyConfig}, produces a function that detects correct file headers
|
|
7
8
|
* and returns an error string if the header is missing or incorrect.
|
|
8
9
|
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const MyHeaderPolicy = defineFileHeaderPolicy({
|
|
13
|
+
* name: "MyHeaderPolicy",
|
|
14
|
+
* description: "Ensures files have required headers",
|
|
15
|
+
* config: { match: /\.ts$/, lineStart: /\/\/ /, lineEnd: /\r?\n/, replacer: ... },
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
9
19
|
* @alpha
|
|
10
20
|
*/
|
|
11
|
-
export function defineFileHeaderPolicy(
|
|
21
|
+
export function defineFileHeaderPolicy(args) {
|
|
22
|
+
const { name, description, config } = args;
|
|
12
23
|
const pre = config.headerStart?.source ?? "";
|
|
13
24
|
const start = config.lineStart.source;
|
|
14
25
|
const end = config.lineEnd.source;
|
|
@@ -25,8 +36,9 @@ export function defineFileHeaderPolicy(name, config) {
|
|
|
25
36
|
const regex = new RegExp(`^${pre}${toRegex(config.headerText ?? "")}(${toRegex(config.autoGenText ?? "")})?${post}`);
|
|
26
37
|
return {
|
|
27
38
|
name,
|
|
39
|
+
description,
|
|
28
40
|
match: config.match,
|
|
29
|
-
handler:
|
|
41
|
+
handler: function* ({ file, resolve, config: policyConfig }) {
|
|
30
42
|
if (policyConfig === undefined) {
|
|
31
43
|
return true;
|
|
32
44
|
}
|
|
@@ -38,7 +50,7 @@ export function defineFileHeaderPolicy(name, config) {
|
|
|
38
50
|
};
|
|
39
51
|
// TODO: Consider reading only the first 512B or so since headers are typically
|
|
40
52
|
// at the beginning of the file.
|
|
41
|
-
const content =
|
|
53
|
+
const content = yield* call(() => readFile(file, { encoding: "utf8" }));
|
|
42
54
|
const failed = !regex.test(content);
|
|
43
55
|
if (failed) {
|
|
44
56
|
failResult.errorMessages.push(`${extname(file)} file missing header`);
|
|
@@ -46,7 +58,7 @@ export function defineFileHeaderPolicy(name, config) {
|
|
|
46
58
|
if (failed) {
|
|
47
59
|
if (resolve) {
|
|
48
60
|
const newContent = config.replacer(content, policyConfig);
|
|
49
|
-
|
|
61
|
+
yield* call(() => writeFile(file, newContent));
|
|
50
62
|
const fixResult = {
|
|
51
63
|
...failResult,
|
|
52
64
|
resolved: true,
|