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 @@
|
|
|
1
|
+
{"version":3,"file":"NoPrivateWorkspaceDependencies.js","sourceRoot":"","sources":["../../src/policies/NoPrivateWorkspaceDependencies.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { getWorkspaceGlobs } from \"resolve-workspace-root\";\nimport { globSync } from \"tinyglobby\";\nimport type { PackageJson } from \"type-fest\";\nimport type { PolicyFailure } from \"../policy.js\";\nimport { definePackagePolicy } from \"../policyDefiners/definePackagePolicy.js\";\n\n/**\n * Configuration for the NoPrivateWorkspaceDependencies policy.\n */\nexport interface NoPrivateWorkspaceDependenciesSettings {\n\t/**\n\t * Whether to check devDependencies in addition to dependencies.\n\t * @default false\n\t */\n\tcheckDevDependencies?: boolean;\n}\n\n/**\n * Cache for workspace package lookups to avoid repeated filesystem operations.\n */\nconst workspacePackageCache = new Map<\n\tstring,\n\tMap<string, PackageJson | undefined>\n>();\n\n/**\n * Builds a map of package name to package.json for all packages in the workspace.\n * Uses the workspace configuration (pnpm-workspace.yaml or package.json workspaces) to find packages.\n */\nfunction buildWorkspacePackageMap(\n\troot: string,\n): Map<string, PackageJson | undefined> {\n\t// Check cache first\n\tconst cached = workspacePackageCache.get(root);\n\tif (cached !== undefined) {\n\t\treturn cached;\n\t}\n\n\tconst packageMap = new Map<string, PackageJson | undefined>();\n\n\t// Get workspace globs from pnpm-workspace.yaml or package.json\n\tconst workspaceGlobs = getWorkspaceGlobs(root);\n\tif (workspaceGlobs === null) {\n\t\t// Not a workspace - return empty map\n\t\tworkspacePackageCache.set(root, packageMap);\n\t\treturn packageMap;\n\t}\n\n\t// Find all package.json files matching the workspace globs\n\tconst packageGlobs = workspaceGlobs.map((g) => `${g}/package.json`);\n\tconst packageJsonPaths = globSync(packageGlobs, {\n\t\tcwd: root,\n\t\tignore: [\"**/node_modules/**\"],\n\t\tonlyFiles: true,\n\t\tabsolute: true,\n\t});\n\n\t// Load each package.json and map by name\n\tfor (const pkgPath of packageJsonPaths) {\n\t\ttry {\n\t\t\tconst content = readFileSync(pkgPath, \"utf-8\");\n\t\t\tconst pkg = JSON.parse(content) as PackageJson;\n\t\t\tif (pkg.name) {\n\t\t\t\tpackageMap.set(pkg.name, pkg);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore parse errors\n\t\t}\n\t}\n\n\tworkspacePackageCache.set(root, packageMap);\n\treturn packageMap;\n}\n\n/**\n * Looks up a workspace package by name using the workspace configuration.\n */\nfunction findWorkspacePackage(\n\tdepName: string,\n\troot: string,\n): PackageJson | undefined {\n\tconst packageMap = buildWorkspacePackageMap(root);\n\treturn packageMap.get(depName);\n}\n\n/**\n * Checks if a dependency version uses the workspace protocol.\n */\nfunction isWorkspaceDependency(version: string | undefined): boolean {\n\treturn version?.startsWith(\"workspace:\") ?? false;\n}\n\n/**\n * Policy that prevents publishable packages from depending on private workspace packages.\n *\n * A package is considered \"publishable\" if it does not have `\"private\": true` in its package.json.\n * Such packages cannot have `workspace:` dependencies on packages that are marked as private,\n * because those dependencies won't be resolvable when the package is published to npm.\n *\n * This policy uses the workspace configuration (pnpm-workspace.yaml or package.json workspaces field)\n * to locate workspace packages, rather than hardcoding directory conventions.\n *\n * @example\n * ```typescript\n * // In repopo.config.ts\n * import { NoPrivateWorkspaceDependencies } from \"repopo/policies\";\n *\n * makePolicy(NoPrivateWorkspaceDependencies, {\n * checkDevDependencies: false, // Only check production dependencies (default)\n * });\n *\n * // Or check both dependencies and devDependencies\n * makePolicy(NoPrivateWorkspaceDependencies, {\n * checkDevDependencies: true,\n * });\n * ```\n */\nexport const NoPrivateWorkspaceDependencies = definePackagePolicy<\n\tPackageJson,\n\tNoPrivateWorkspaceDependenciesSettings | undefined\n>(\"NoPrivateWorkspaceDependencies\", async (json, { file, root, config }) => {\n\t// Private packages can depend on anything - they won't be published\n\tif (json.private === true) {\n\t\treturn true;\n\t}\n\n\tconst checkDevDeps = config?.checkDevDependencies ?? false;\n\n\t// Collect dependencies to check\n\tconst depsToCheck: Record<string, string | undefined> = {\n\t\t...(json.dependencies ?? {}),\n\t};\n\n\tif (checkDevDeps) {\n\t\tObject.assign(depsToCheck, json.devDependencies ?? {});\n\t}\n\n\tconst privateWorkspaceDeps: string[] = [];\n\tconst unresolvableDeps: string[] = [];\n\n\tfor (const [depName, version] of Object.entries(depsToCheck)) {\n\t\tif (!isWorkspaceDependency(version)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst depPackageJson = findWorkspacePackage(depName, root);\n\n\t\tif (depPackageJson === undefined) {\n\t\t\t// Could not find the package - might be a configuration issue\n\t\t\t// We'll warn but not fail, as this could be a valid setup we don't understand\n\t\t\tunresolvableDeps.push(depName);\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (depPackageJson.private === true) {\n\t\t\tprivateWorkspaceDeps.push(depName);\n\t\t}\n\t}\n\n\tconst errorMessages: string[] = [];\n\n\tif (privateWorkspaceDeps.length > 0) {\n\t\terrorMessages.push(\n\t\t\t`Publishable package has workspace dependencies on private packages that won't be available on npm:\\n\\t${privateWorkspaceDeps.join(\"\\n\\t\")}`,\n\t\t);\n\t}\n\n\tif (unresolvableDeps.length > 0) {\n\t\terrorMessages.push(\n\t\t\t`Could not resolve workspace dependencies (verify they exist):\\n\\t${unresolvableDeps.join(\"\\n\\t\")}`,\n\t\t);\n\t}\n\n\tif (errorMessages.length > 0) {\n\t\tconst failResult: PolicyFailure = {\n\t\t\tname: NoPrivateWorkspaceDependencies.name,\n\t\t\tfile,\n\t\t\tautoFixable: false,\n\t\t\terrorMessages,\n\t\t};\n\t\treturn failResult;\n\t}\n\n\treturn true;\n});\n"]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the PackageAllowedScopes policy.
|
|
3
|
+
*
|
|
4
|
+
* This policy ensures packages in a monorepo use only approved scopes or package names.
|
|
5
|
+
* This prevents accidental introduction of new scopes and ensures naming consistency.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const config: PackageAllowedScopesConfig = {
|
|
10
|
+
* // Only packages in these scopes are allowed
|
|
11
|
+
* allowedScopes: ["@myorg", "@internal", "@experimental"],
|
|
12
|
+
* // These specific unscoped packages are also allowed
|
|
13
|
+
* unscopedPackages: ["my-legacy-tool", "special-package"],
|
|
14
|
+
* };
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @alpha
|
|
18
|
+
*/
|
|
19
|
+
export interface PackageAllowedScopesConfig {
|
|
20
|
+
/**
|
|
21
|
+
* A list of npm scopes that are allowed for packages in this repository.
|
|
22
|
+
*
|
|
23
|
+
* Scopes should start with `@` (e.g., `@myorg`). A package like `@myorg/foo`
|
|
24
|
+
* would match the scope `@myorg`.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
*
|
|
28
|
+
* If a package's scope is not in this list and the package is not in
|
|
29
|
+
* `unscopedPackages`, the policy will fail.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* allowedScopes: ["@myorg", "@myorg-tools", "@myorg-internal"]
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
allowedScopes?: string[];
|
|
37
|
+
/**
|
|
38
|
+
* A list of unscoped package names that are explicitly allowed.
|
|
39
|
+
*
|
|
40
|
+
* Use this for legacy packages or special cases where an unscoped name is needed.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
*
|
|
44
|
+
* Unscoped packages not in this list will cause the policy to fail.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* unscopedPackages: ["my-legacy-tool", "special-package"]
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
unscopedPackages?: string[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A policy that ensures packages use only allowed scopes and package names.
|
|
55
|
+
*
|
|
56
|
+
* This policy prevents accidental introduction of new npm scopes or unscoped packages
|
|
57
|
+
* in a monorepo. It helps maintain naming consistency and prevents typos or mistakes
|
|
58
|
+
* when creating new packages.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
*
|
|
62
|
+
* The policy checks packages in the following order:
|
|
63
|
+
*
|
|
64
|
+
* 1. If the package is scoped (starts with `@`), check if its scope is in `allowedScopes`
|
|
65
|
+
* 2. If the package is unscoped, check if it's in `unscopedPackages`
|
|
66
|
+
* 3. If neither check passes, the policy fails
|
|
67
|
+
*
|
|
68
|
+
* The "root" package (common in monorepos) is automatically skipped.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { makePolicy } from "repopo";
|
|
73
|
+
* import { PackageAllowedScopes } from "repopo/policies";
|
|
74
|
+
*
|
|
75
|
+
* const config: RepopoConfig = {
|
|
76
|
+
* policies: [
|
|
77
|
+
* makePolicy(PackageAllowedScopes, {
|
|
78
|
+
* allowedScopes: ["@myorg", "@internal"],
|
|
79
|
+
* unscopedPackages: ["legacy-package"],
|
|
80
|
+
* }),
|
|
81
|
+
* ],
|
|
82
|
+
* };
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @alpha
|
|
86
|
+
*/
|
|
87
|
+
export declare const PackageAllowedScopes: import("../policy.js").PolicyDefinition<PackageAllowedScopesConfig | undefined>;
|
|
88
|
+
//# sourceMappingURL=PackageAllowedScopes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PackageAllowedScopes.d.ts","sourceRoot":"","sources":["../../src/policies/PackageAllowedScopes.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,0BAA0B;IAC1C;;;;;;;;;;;;;;;OAeG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;;;;;;;;;;;;OAaG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAiDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,oBAAoB,iFAiD/B,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { definePackagePolicy } from "../policyDefiners/definePackagePolicy.js";
|
|
2
|
+
import { getPackageScope, isScoped } from "../utils/packageName.js";
|
|
3
|
+
/**
|
|
4
|
+
* Check if a package's scope is in the allowed list.
|
|
5
|
+
*
|
|
6
|
+
* @param packageName - The package name to check
|
|
7
|
+
* @param allowedScopes - List of allowed scopes
|
|
8
|
+
* @returns `true` if the package's scope is allowed
|
|
9
|
+
*/
|
|
10
|
+
function isScopeAllowed(packageName, allowedScopes) {
|
|
11
|
+
if (allowedScopes === undefined || allowedScopes.length === 0) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const scope = getPackageScope(packageName);
|
|
15
|
+
if (scope === undefined) {
|
|
16
|
+
// Package is unscoped, so scope check doesn't apply
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return allowedScopes.includes(scope);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if an unscoped package name is in the allowed list.
|
|
23
|
+
*
|
|
24
|
+
* @param packageName - The package name to check
|
|
25
|
+
* @param unscopedPackages - List of allowed unscoped package names
|
|
26
|
+
* @returns `true` if the unscoped package is allowed
|
|
27
|
+
*/
|
|
28
|
+
function isUnscopedPackageAllowed(packageName, unscopedPackages) {
|
|
29
|
+
if (unscopedPackages === undefined || unscopedPackages.length === 0) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// Only applies to unscoped packages
|
|
33
|
+
if (isScoped(packageName)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return unscopedPackages.includes(packageName);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A policy that ensures packages use only allowed scopes and package names.
|
|
40
|
+
*
|
|
41
|
+
* This policy prevents accidental introduction of new npm scopes or unscoped packages
|
|
42
|
+
* in a monorepo. It helps maintain naming consistency and prevents typos or mistakes
|
|
43
|
+
* when creating new packages.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
*
|
|
47
|
+
* The policy checks packages in the following order:
|
|
48
|
+
*
|
|
49
|
+
* 1. If the package is scoped (starts with `@`), check if its scope is in `allowedScopes`
|
|
50
|
+
* 2. If the package is unscoped, check if it's in `unscopedPackages`
|
|
51
|
+
* 3. If neither check passes, the policy fails
|
|
52
|
+
*
|
|
53
|
+
* The "root" package (common in monorepos) is automatically skipped.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { makePolicy } from "repopo";
|
|
58
|
+
* import { PackageAllowedScopes } from "repopo/policies";
|
|
59
|
+
*
|
|
60
|
+
* const config: RepopoConfig = {
|
|
61
|
+
* policies: [
|
|
62
|
+
* makePolicy(PackageAllowedScopes, {
|
|
63
|
+
* allowedScopes: ["@myorg", "@internal"],
|
|
64
|
+
* unscopedPackages: ["legacy-package"],
|
|
65
|
+
* }),
|
|
66
|
+
* ],
|
|
67
|
+
* };
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @alpha
|
|
71
|
+
*/
|
|
72
|
+
export const PackageAllowedScopes = definePackagePolicy("PackageAllowedScopes", async (json, { file, config }) => {
|
|
73
|
+
// If no config provided, skip validation
|
|
74
|
+
if (config === undefined) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const packageName = json.name;
|
|
78
|
+
// Skip packages without a name
|
|
79
|
+
if (packageName === undefined) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
// Skip the root package (common in monorepos)
|
|
83
|
+
if (packageName === "root") {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
// Check if the package uses an allowed scope
|
|
87
|
+
if (isScopeAllowed(packageName, config.allowedScopes)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Check if the package is an allowed unscoped package
|
|
91
|
+
if (isUnscopedPackageAllowed(packageName, config.unscopedPackages)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
// Package doesn't match any allowed pattern - build error message
|
|
95
|
+
const scope = getPackageScope(packageName);
|
|
96
|
+
let errorMessage;
|
|
97
|
+
if (scope !== undefined) {
|
|
98
|
+
// Package has an unexpected scope
|
|
99
|
+
errorMessage = `Package "${packageName}" uses scope "${scope}" which is not in the allowed scopes list. Allowed scopes: ${config.allowedScopes?.join(", ") ?? "(none)"}`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Package is unscoped but not in the allowed list
|
|
103
|
+
errorMessage = `Package "${packageName}" is an unscoped package that is not in the allowed unscoped packages list. Allowed unscoped packages: ${config.unscopedPackages?.join(", ") ?? "(none)"}`;
|
|
104
|
+
}
|
|
105
|
+
const failResult = {
|
|
106
|
+
name: PackageAllowedScopes.name,
|
|
107
|
+
file,
|
|
108
|
+
autoFixable: false, // Can't auto-fix package names
|
|
109
|
+
errorMessages: [errorMessage],
|
|
110
|
+
};
|
|
111
|
+
return failResult;
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=PackageAllowedScopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PackageAllowedScopes.js","sourceRoot":"","sources":["../../src/policies/PackageAllowedScopes.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAwDpE;;;;;;GAMG;AACH,SAAS,cAAc,CACtB,WAAmB,EACnB,aAAmC;IAEnC,IAAI,aAAa,KAAK,SAAS,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/D,OAAO,KAAK,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC3C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACzB,oDAAoD;QACpD,OAAO,KAAK,CAAC;IACd,CAAC;IAED,OAAO,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;GAMG;AACH,SAAS,wBAAwB,CAChC,WAAmB,EACnB,gBAAsC;IAEtC,IAAI,gBAAgB,KAAK,SAAS,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrE,OAAO,KAAK,CAAC;IACd,CAAC;IAED,oCAAoC;IACpC,IAAI,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACd,CAAC;IAED,OAAO,gBAAgB,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAGrD,sBAAsB,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;IAC1D,yCAAyC;IACzC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC;IAE9B,+BAA+B;IAC/B,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,8CAA8C;IAC9C,IAAI,WAAW,KAAK,MAAM,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,6CAA6C;IAC7C,IAAI,cAAc,CAAC,WAAW,EAAE,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC;IACb,CAAC;IAED,sDAAsD;IACtD,IAAI,wBAAwB,CAAC,WAAW,EAAE,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACpE,OAAO,IAAI,CAAC;IACb,CAAC;IAED,kEAAkE;IAClE,MAAM,KAAK,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC3C,IAAI,YAAoB,CAAC;IACzB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACzB,kCAAkC;QAClC,YAAY,GAAG,YAAY,WAAW,iBAAiB,KAAK,8DAA8D,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;IAC1K,CAAC;SAAM,CAAC;QACP,kDAAkD;QAClD,YAAY,GAAG,YAAY,WAAW,0GAA0G,MAAM,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;IACnM,CAAC;IAED,MAAM,UAAU,GAAkB;QACjC,IAAI,EAAE,oBAAoB,CAAC,IAAI;QAC/B,IAAI;QACJ,WAAW,EAAE,KAAK,EAAE,+BAA+B;QACnD,aAAa,EAAE,CAAC,YAAY,CAAC;KAC7B,CAAC;IACF,OAAO,UAAU,CAAC;AACnB,CAAC,CAAC,CAAC","sourcesContent":["import type { PackageJson } from \"type-fest\";\nimport type { PolicyFailure } from \"../policy.js\";\nimport { definePackagePolicy } from \"../policyDefiners/definePackagePolicy.js\";\nimport { getPackageScope, isScoped } from \"../utils/packageName.js\";\n\n/**\n * Configuration for the PackageAllowedScopes policy.\n *\n * This policy ensures packages in a monorepo use only approved scopes or package names.\n * This prevents accidental introduction of new scopes and ensures naming consistency.\n *\n * @example\n * ```typescript\n * const config: PackageAllowedScopesConfig = {\n * // Only packages in these scopes are allowed\n * allowedScopes: [\"@myorg\", \"@internal\", \"@experimental\"],\n * // These specific unscoped packages are also allowed\n * unscopedPackages: [\"my-legacy-tool\", \"special-package\"],\n * };\n * ```\n *\n * @alpha\n */\nexport interface PackageAllowedScopesConfig {\n\t/**\n\t * A list of npm scopes that are allowed for packages in this repository.\n\t *\n\t * Scopes should start with `@` (e.g., `@myorg`). A package like `@myorg/foo`\n\t * would match the scope `@myorg`.\n\t *\n\t * @remarks\n\t *\n\t * If a package's scope is not in this list and the package is not in\n\t * `unscopedPackages`, the policy will fail.\n\t *\n\t * @example\n\t * ```typescript\n\t * allowedScopes: [\"@myorg\", \"@myorg-tools\", \"@myorg-internal\"]\n\t * ```\n\t */\n\tallowedScopes?: string[];\n\n\t/**\n\t * A list of unscoped package names that are explicitly allowed.\n\t *\n\t * Use this for legacy packages or special cases where an unscoped name is needed.\n\t *\n\t * @remarks\n\t *\n\t * Unscoped packages not in this list will cause the policy to fail.\n\t *\n\t * @example\n\t * ```typescript\n\t * unscopedPackages: [\"my-legacy-tool\", \"special-package\"]\n\t * ```\n\t */\n\tunscopedPackages?: string[];\n}\n\n/**\n * Check if a package's scope is in the allowed list.\n *\n * @param packageName - The package name to check\n * @param allowedScopes - List of allowed scopes\n * @returns `true` if the package's scope is allowed\n */\nfunction isScopeAllowed(\n\tpackageName: string,\n\tallowedScopes: string[] | undefined,\n): boolean {\n\tif (allowedScopes === undefined || allowedScopes.length === 0) {\n\t\treturn false;\n\t}\n\n\tconst scope = getPackageScope(packageName);\n\tif (scope === undefined) {\n\t\t// Package is unscoped, so scope check doesn't apply\n\t\treturn false;\n\t}\n\n\treturn allowedScopes.includes(scope);\n}\n\n/**\n * Check if an unscoped package name is in the allowed list.\n *\n * @param packageName - The package name to check\n * @param unscopedPackages - List of allowed unscoped package names\n * @returns `true` if the unscoped package is allowed\n */\nfunction isUnscopedPackageAllowed(\n\tpackageName: string,\n\tunscopedPackages: string[] | undefined,\n): boolean {\n\tif (unscopedPackages === undefined || unscopedPackages.length === 0) {\n\t\treturn false;\n\t}\n\n\t// Only applies to unscoped packages\n\tif (isScoped(packageName)) {\n\t\treturn false;\n\t}\n\n\treturn unscopedPackages.includes(packageName);\n}\n\n/**\n * A policy that ensures packages use only allowed scopes and package names.\n *\n * This policy prevents accidental introduction of new npm scopes or unscoped packages\n * in a monorepo. It helps maintain naming consistency and prevents typos or mistakes\n * when creating new packages.\n *\n * @remarks\n *\n * The policy checks packages in the following order:\n *\n * 1. If the package is scoped (starts with `@`), check if its scope is in `allowedScopes`\n * 2. If the package is unscoped, check if it's in `unscopedPackages`\n * 3. If neither check passes, the policy fails\n *\n * The \"root\" package (common in monorepos) is automatically skipped.\n *\n * @example\n * ```typescript\n * import { makePolicy } from \"repopo\";\n * import { PackageAllowedScopes } from \"repopo/policies\";\n *\n * const config: RepopoConfig = {\n * policies: [\n * makePolicy(PackageAllowedScopes, {\n * allowedScopes: [\"@myorg\", \"@internal\"],\n * unscopedPackages: [\"legacy-package\"],\n * }),\n * ],\n * };\n * ```\n *\n * @alpha\n */\nexport const PackageAllowedScopes = definePackagePolicy<\n\tPackageJson,\n\tPackageAllowedScopesConfig | undefined\n>(\"PackageAllowedScopes\", async (json, { file, config }) => {\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\n\tif (packageName === undefined) {\n\t\treturn true;\n\t}\n\n\t// Skip the root package (common in monorepos)\n\tif (packageName === \"root\") {\n\t\treturn true;\n\t}\n\n\t// Check if the package uses an allowed scope\n\tif (isScopeAllowed(packageName, config.allowedScopes)) {\n\t\treturn true;\n\t}\n\n\t// Check if the package is an allowed unscoped package\n\tif (isUnscopedPackageAllowed(packageName, config.unscopedPackages)) {\n\t\treturn true;\n\t}\n\n\t// Package doesn't match any allowed pattern - build error message\n\tconst scope = getPackageScope(packageName);\n\tlet errorMessage: string;\n\tif (scope !== undefined) {\n\t\t// Package has an unexpected scope\n\t\terrorMessage = `Package \"${packageName}\" uses scope \"${scope}\" which is not in the allowed scopes list. Allowed scopes: ${config.allowedScopes?.join(\", \") ?? \"(none)\"}`;\n\t} else {\n\t\t// Package is unscoped but not in the allowed list\n\t\terrorMessage = `Package \"${packageName}\" is an unscoped package that is not in the allowed unscoped packages list. Allowed unscoped packages: ${config.unscopedPackages?.join(\", \") ?? \"(none)\"}`;\n\t}\n\n\tconst failResult: PolicyFailure = {\n\t\tname: PackageAllowedScopes.name,\n\t\tfile,\n\t\tautoFixable: false, // Can't auto-fix package names\n\t\terrorMessages: [errorMessage],\n\t};\n\treturn failResult;\n});\n"]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the PackageEsmType policy.
|
|
3
|
+
*
|
|
4
|
+
* This policy ensures the `type` field in package.json is correctly set
|
|
5
|
+
* to indicate whether the package is an ESM or CommonJS module.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const config: PackageEsmTypeConfig = {
|
|
10
|
+
* // Require ESM type field for all packages
|
|
11
|
+
* requiredType: "module",
|
|
12
|
+
* // Or detect based on exports/main field
|
|
13
|
+
* detectFromExports: true,
|
|
14
|
+
* };
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @alpha
|
|
18
|
+
*/
|
|
19
|
+
export interface PackageEsmTypeConfig {
|
|
20
|
+
/**
|
|
21
|
+
* The required `type` field value for all packages.
|
|
22
|
+
*
|
|
23
|
+
* - `"module"` - All packages must be ESM (use import/export)
|
|
24
|
+
* - `"commonjs"` - All packages must be CommonJS (use require)
|
|
25
|
+
* - `undefined` - Use detection logic based on exports/main
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
*
|
|
29
|
+
* If set, this overrides automatic detection. Use `detectFromExports`
|
|
30
|
+
* for more flexible per-package type detection.
|
|
31
|
+
*/
|
|
32
|
+
requiredType?: "module" | "commonjs";
|
|
33
|
+
/**
|
|
34
|
+
* Detect the expected `type` field from the exports or main field.
|
|
35
|
+
*
|
|
36
|
+
* When enabled, the policy analyzes the package's entry points:
|
|
37
|
+
* - If exports use `.mjs` or have `import` conditions → `"module"`
|
|
38
|
+
* - If exports use `.cjs` or have `require` conditions → `"commonjs"`
|
|
39
|
+
* - If main field uses `.mjs` → `"module"`
|
|
40
|
+
* - If main field uses `.cjs` → `"commonjs"`
|
|
41
|
+
*
|
|
42
|
+
* @defaultValue false
|
|
43
|
+
*/
|
|
44
|
+
detectFromExports?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Package names or scopes to exclude from validation.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* excludePackages: ["@myorg/legacy-package", "old-cjs-lib"]
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
excludePackages?: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Behavior when module type cannot be detected from exports/main.
|
|
56
|
+
*
|
|
57
|
+
* - `"skip"` (default) - Skip validation for packages where type cannot be detected
|
|
58
|
+
* - `"warn"` - Pass validation but include a warning in the output
|
|
59
|
+
* - `"fail"` - Fail validation when type cannot be detected
|
|
60
|
+
*
|
|
61
|
+
* This only applies when `detectFromExports` is true and no `requiredType` is set.
|
|
62
|
+
*
|
|
63
|
+
* @defaultValue "skip"
|
|
64
|
+
*/
|
|
65
|
+
onDetectionFailure?: "skip" | "warn" | "fail";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A policy that ensures the `type` field in package.json is correctly set.
|
|
69
|
+
*
|
|
70
|
+
* This policy helps maintain consistency in how packages declare their module format,
|
|
71
|
+
* which is essential for proper module resolution in Node.js and bundlers.
|
|
72
|
+
*
|
|
73
|
+
* @remarks
|
|
74
|
+
*
|
|
75
|
+
* The policy can operate in two modes:
|
|
76
|
+
*
|
|
77
|
+
* 1. **Required Type**: Set `requiredType` to enforce a specific type for all packages
|
|
78
|
+
* 2. **Detection**: Set `detectFromExports: true` to detect the expected type from
|
|
79
|
+
* the package's exports or main field
|
|
80
|
+
*
|
|
81
|
+
* When `detectFromExports` is enabled, the policy looks for:
|
|
82
|
+
* - `.mjs` extensions or `"import"` conditions → expects `"module"`
|
|
83
|
+
* - `.cjs` extensions or `"require"` conditions → expects `"commonjs"`
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import { makePolicy } from "repopo";
|
|
88
|
+
* import { PackageEsmType } from "repopo/policies";
|
|
89
|
+
*
|
|
90
|
+
* // Require all packages to be ESM
|
|
91
|
+
* makePolicy(PackageEsmType, { requiredType: "module" })
|
|
92
|
+
*
|
|
93
|
+
* // Or detect based on exports
|
|
94
|
+
* makePolicy(PackageEsmType, { detectFromExports: true })
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @alpha
|
|
98
|
+
*/
|
|
99
|
+
export declare const PackageEsmType: import("../policy.js").PolicyDefinition<PackageEsmTypeConfig | undefined>;
|
|
100
|
+
//# sourceMappingURL=PackageEsmType.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PackageEsmType.d.ts","sourceRoot":"","sources":["../../src/policies/PackageEsmType.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,oBAAoB;IACpC;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAErC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAE3B;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;CAC9C;AAyPD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,cAAc,2EAwDzB,CAAC"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import jsonfile from "jsonfile";
|
|
2
|
+
import { definePackagePolicy } from "../policyDefiners/definePackagePolicy.js";
|
|
3
|
+
import { detectIndentation } from "../utils/indentation.js";
|
|
4
|
+
const { writeFile: writeJson } = jsonfile;
|
|
5
|
+
/**
|
|
6
|
+
* Detect module type from the exports field.
|
|
7
|
+
*/
|
|
8
|
+
function detectTypeFromExports(exports) {
|
|
9
|
+
if (exports === undefined) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const exportsStr = JSON.stringify(exports);
|
|
13
|
+
const hasEsm = exportsStr.includes(".mjs") || exportsStr.includes('"import"');
|
|
14
|
+
const hasCjs = exportsStr.includes(".cjs") || exportsStr.includes('"require"');
|
|
15
|
+
// Dual format packages have both ESM and CJS exports
|
|
16
|
+
if (hasEsm && hasCjs) {
|
|
17
|
+
return {
|
|
18
|
+
type: "module",
|
|
19
|
+
isDualFormat: true,
|
|
20
|
+
reason: "Package has both ESM and CommonJS exports (dual format). Defaulting to ESM.",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (hasEsm) {
|
|
24
|
+
return {
|
|
25
|
+
type: "module",
|
|
26
|
+
isDualFormat: false,
|
|
27
|
+
reason: "Detected ESM from exports field (.mjs or import condition)",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (hasCjs) {
|
|
31
|
+
return {
|
|
32
|
+
type: "commonjs",
|
|
33
|
+
isDualFormat: false,
|
|
34
|
+
reason: "Detected CommonJS from exports field (.cjs or require condition)",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Detect module type from the main field.
|
|
41
|
+
*/
|
|
42
|
+
function detectTypeFromMain(main) {
|
|
43
|
+
if (typeof main !== "string") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (main.endsWith(".mjs")) {
|
|
47
|
+
return {
|
|
48
|
+
type: "module",
|
|
49
|
+
isDualFormat: false,
|
|
50
|
+
reason: "Detected ESM from main field (.mjs extension)",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (main.endsWith(".cjs")) {
|
|
54
|
+
return {
|
|
55
|
+
type: "commonjs",
|
|
56
|
+
isDualFormat: false,
|
|
57
|
+
reason: "Detected CommonJS from main field (.cjs extension)",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Detect the expected type field from the package's exports or main field.
|
|
64
|
+
*
|
|
65
|
+
* @param json - The package.json content
|
|
66
|
+
* @returns Detection result with type, dual-format indicator, and reason
|
|
67
|
+
*/
|
|
68
|
+
function detectTypeFromPackage(json) {
|
|
69
|
+
// Check exports field for type hints
|
|
70
|
+
const exportsResult = detectTypeFromExports(json.exports);
|
|
71
|
+
if (exportsResult !== undefined) {
|
|
72
|
+
return exportsResult;
|
|
73
|
+
}
|
|
74
|
+
// Check main field
|
|
75
|
+
const mainResult = detectTypeFromMain(json.main);
|
|
76
|
+
if (mainResult !== undefined) {
|
|
77
|
+
return mainResult;
|
|
78
|
+
}
|
|
79
|
+
// Check module field (ESM entry point)
|
|
80
|
+
if (json.module !== undefined) {
|
|
81
|
+
return {
|
|
82
|
+
type: "module",
|
|
83
|
+
isDualFormat: false,
|
|
84
|
+
reason: "Detected ESM from module field presence",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
type: undefined,
|
|
89
|
+
isDualFormat: false,
|
|
90
|
+
reason: "Could not detect module type from exports, main, or module fields",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a package matches an exclusion pattern.
|
|
95
|
+
*
|
|
96
|
+
* @param packageName - The package name
|
|
97
|
+
* @param excludePackages - List of patterns to exclude
|
|
98
|
+
* @returns true if the package should be excluded
|
|
99
|
+
*/
|
|
100
|
+
function isExcluded(packageName, excludePackages) {
|
|
101
|
+
if (excludePackages === undefined || excludePackages.length === 0) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
for (const pattern of excludePackages) {
|
|
105
|
+
// Scope matching
|
|
106
|
+
if (pattern.startsWith("@") && packageName.startsWith(`${pattern}/`)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
// Exact match
|
|
110
|
+
if (packageName === pattern) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if the package should be skipped from validation.
|
|
118
|
+
*/
|
|
119
|
+
function shouldSkipPackage(packageName, config) {
|
|
120
|
+
// Skip packages without a name
|
|
121
|
+
if (packageName === undefined) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
// Skip the root package
|
|
125
|
+
if (packageName === "root") {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
// Check exclusions
|
|
129
|
+
return isExcluded(packageName, config.excludePackages);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Determine the expected type and reason from the config and package.
|
|
133
|
+
*/
|
|
134
|
+
function determineExpectedType(json, config) {
|
|
135
|
+
if (config.requiredType !== undefined) {
|
|
136
|
+
return {
|
|
137
|
+
expectedType: config.requiredType,
|
|
138
|
+
detectionReason: `Required type: ${config.requiredType}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (config.detectFromExports === true) {
|
|
142
|
+
const detection = detectTypeFromPackage(json);
|
|
143
|
+
return {
|
|
144
|
+
expectedType: detection.type,
|
|
145
|
+
detectionReason: detection.reason,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { expectedType: undefined, detectionReason: undefined };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build the error message for a type mismatch.
|
|
152
|
+
*/
|
|
153
|
+
function buildErrorMessage(packageName, currentType, expectedType) {
|
|
154
|
+
if (currentType === undefined) {
|
|
155
|
+
return `Package "${packageName}" is missing the "type" field. Expected: "type": "${expectedType}"`;
|
|
156
|
+
}
|
|
157
|
+
return `Package "${packageName}" has "type": "${currentType}" but expected "type": "${expectedType}"`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Apply the auto-fix for the type field.
|
|
161
|
+
*/
|
|
162
|
+
async function applyFix(json, file, expectedType, failResult) {
|
|
163
|
+
const fixResult = {
|
|
164
|
+
...failResult,
|
|
165
|
+
resolved: false,
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
json.type = expectedType;
|
|
169
|
+
const indent = await detectIndentation(file);
|
|
170
|
+
await writeJson(file, json, { spaces: indent });
|
|
171
|
+
fixResult.resolved = true;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
fixResult.resolved = false;
|
|
175
|
+
fixResult.errorMessages = [`Failed to update ${file}`];
|
|
176
|
+
}
|
|
177
|
+
return fixResult;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A policy that ensures the `type` field in package.json is correctly set.
|
|
181
|
+
*
|
|
182
|
+
* This policy helps maintain consistency in how packages declare their module format,
|
|
183
|
+
* which is essential for proper module resolution in Node.js and bundlers.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
*
|
|
187
|
+
* The policy can operate in two modes:
|
|
188
|
+
*
|
|
189
|
+
* 1. **Required Type**: Set `requiredType` to enforce a specific type for all packages
|
|
190
|
+
* 2. **Detection**: Set `detectFromExports: true` to detect the expected type from
|
|
191
|
+
* the package's exports or main field
|
|
192
|
+
*
|
|
193
|
+
* When `detectFromExports` is enabled, the policy looks for:
|
|
194
|
+
* - `.mjs` extensions or `"import"` conditions → expects `"module"`
|
|
195
|
+
* - `.cjs` extensions or `"require"` conditions → expects `"commonjs"`
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* import { makePolicy } from "repopo";
|
|
200
|
+
* import { PackageEsmType } from "repopo/policies";
|
|
201
|
+
*
|
|
202
|
+
* // Require all packages to be ESM
|
|
203
|
+
* makePolicy(PackageEsmType, { requiredType: "module" })
|
|
204
|
+
*
|
|
205
|
+
* // Or detect based on exports
|
|
206
|
+
* makePolicy(PackageEsmType, { detectFromExports: true })
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @alpha
|
|
210
|
+
*/
|
|
211
|
+
export const PackageEsmType = definePackagePolicy("PackageEsmType", async (json, { file, config, resolve }) => {
|
|
212
|
+
// If no config provided, skip validation
|
|
213
|
+
if (config === undefined) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
const packageName = json.name;
|
|
217
|
+
// Check if package should be skipped
|
|
218
|
+
if (shouldSkipPackage(packageName, config)) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
// Determine the expected type
|
|
222
|
+
const { expectedType, detectionReason } = determineExpectedType(json, config);
|
|
223
|
+
// Handle case where type cannot be detected
|
|
224
|
+
if (expectedType === undefined) {
|
|
225
|
+
if (config.onDetectionFailure === "fail") {
|
|
226
|
+
return {
|
|
227
|
+
name: "PackageEsmType",
|
|
228
|
+
file,
|
|
229
|
+
autoFixable: false,
|
|
230
|
+
errorMessages: [
|
|
231
|
+
`Package "${packageName}": ${detectionReason ?? "Could not detect module type"}. Set "type" field explicitly or use "requiredType" config.`,
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// "skip" and "warn" both pass validation
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
// Check if current type matches expected
|
|
239
|
+
if (json.type === expectedType) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
// Build failure result
|
|
243
|
+
const failResult = {
|
|
244
|
+
name: PackageEsmType.name,
|
|
245
|
+
file,
|
|
246
|
+
autoFixable: true,
|
|
247
|
+
// packageName is guaranteed to be defined here due to shouldSkipPackage check
|
|
248
|
+
errorMessages: [
|
|
249
|
+
buildErrorMessage(packageName, json.type, expectedType),
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
if (resolve) {
|
|
253
|
+
return applyFix(json, file, expectedType, failResult);
|
|
254
|
+
}
|
|
255
|
+
return failResult;
|
|
256
|
+
});
|
|
257
|
+
//# sourceMappingURL=PackageEsmType.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PackageEsmType.js","sourceRoot":"","sources":["../../src/policies/PackageEsmType.ts"],"names":[],"mappings":"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","sourcesContent":["import jsonfile from \"jsonfile\";\nimport type { PackageJson } from \"type-fest\";\nimport type { PolicyFailure, PolicyFixResult } from \"../policy.js\";\nimport { definePackagePolicy } from \"../policyDefiners/definePackagePolicy.js\";\nimport { detectIndentation } from \"../utils/indentation.js\";\n\nconst { writeFile: writeJson } = jsonfile;\n\n/**\n * Configuration for the PackageEsmType policy.\n *\n * This policy ensures the `type` field in package.json is correctly set\n * to indicate whether the package is an ESM or CommonJS module.\n *\n * @example\n * ```typescript\n * const config: PackageEsmTypeConfig = {\n * // Require ESM type field for all packages\n * requiredType: \"module\",\n * // Or detect based on exports/main field\n * detectFromExports: true,\n * };\n * ```\n *\n * @alpha\n */\nexport interface PackageEsmTypeConfig {\n\t/**\n\t * The required `type` field value for all packages.\n\t *\n\t * - `\"module\"` - All packages must be ESM (use import/export)\n\t * - `\"commonjs\"` - All packages must be CommonJS (use require)\n\t * - `undefined` - Use detection logic based on exports/main\n\t *\n\t * @remarks\n\t *\n\t * If set, this overrides automatic detection. Use `detectFromExports`\n\t * for more flexible per-package type detection.\n\t */\n\trequiredType?: \"module\" | \"commonjs\";\n\n\t/**\n\t * Detect the expected `type` field from the exports or main field.\n\t *\n\t * When enabled, the policy analyzes the package's entry points:\n\t * - If exports use `.mjs` or have `import` conditions → `\"module\"`\n\t * - If exports use `.cjs` or have `require` conditions → `\"commonjs\"`\n\t * - If main field uses `.mjs` → `\"module\"`\n\t * - If main field uses `.cjs` → `\"commonjs\"`\n\t *\n\t * @defaultValue false\n\t */\n\tdetectFromExports?: boolean;\n\n\t/**\n\t * Package names or scopes to exclude from validation.\n\t *\n\t * @example\n\t * ```typescript\n\t * excludePackages: [\"@myorg/legacy-package\", \"old-cjs-lib\"]\n\t * ```\n\t */\n\texcludePackages?: string[];\n\n\t/**\n\t * Behavior when module type cannot be detected from exports/main.\n\t *\n\t * - `\"skip\"` (default) - Skip validation for packages where type cannot be detected\n\t * - `\"warn\"` - Pass validation but include a warning in the output\n\t * - `\"fail\"` - Fail validation when type cannot be detected\n\t *\n\t * This only applies when `detectFromExports` is true and no `requiredType` is set.\n\t *\n\t * @defaultValue \"skip\"\n\t */\n\tonDetectionFailure?: \"skip\" | \"warn\" | \"fail\";\n}\n\n/**\n * Result of detecting the module type from a package.\n */\ninterface DetectionResult {\n\t/**\n\t * The detected type, if any.\n\t */\n\ttype: \"module\" | \"commonjs\" | undefined;\n\n\t/**\n\t * Whether the package appears to be dual-format (both ESM and CommonJS).\n\t * When true, the `type` field should typically match the package's default format.\n\t */\n\tisDualFormat: boolean;\n\n\t/**\n\t * Human-readable reason for the detection result.\n\t */\n\treason: string;\n}\n\n/**\n * Detect module type from the exports field.\n */\nfunction detectTypeFromExports(\n\texports: PackageJson[\"exports\"],\n): DetectionResult | undefined {\n\tif (exports === undefined) {\n\t\treturn undefined;\n\t}\n\n\tconst exportsStr = JSON.stringify(exports);\n\tconst hasEsm = exportsStr.includes(\".mjs\") || exportsStr.includes('\"import\"');\n\tconst hasCjs =\n\t\texportsStr.includes(\".cjs\") || exportsStr.includes('\"require\"');\n\n\t// Dual format packages have both ESM and CJS exports\n\tif (hasEsm && hasCjs) {\n\t\treturn {\n\t\t\ttype: \"module\",\n\t\t\tisDualFormat: true,\n\t\t\treason:\n\t\t\t\t\"Package has both ESM and CommonJS exports (dual format). Defaulting to ESM.\",\n\t\t};\n\t}\n\n\tif (hasEsm) {\n\t\treturn {\n\t\t\ttype: \"module\",\n\t\t\tisDualFormat: false,\n\t\t\treason: \"Detected ESM from exports field (.mjs or import condition)\",\n\t\t};\n\t}\n\n\tif (hasCjs) {\n\t\treturn {\n\t\t\ttype: \"commonjs\",\n\t\t\tisDualFormat: false,\n\t\t\treason:\n\t\t\t\t\"Detected CommonJS from exports field (.cjs or require condition)\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Detect module type from the main field.\n */\nfunction detectTypeFromMain(main: unknown): DetectionResult | undefined {\n\tif (typeof main !== \"string\") {\n\t\treturn undefined;\n\t}\n\n\tif (main.endsWith(\".mjs\")) {\n\t\treturn {\n\t\t\ttype: \"module\",\n\t\t\tisDualFormat: false,\n\t\t\treason: \"Detected ESM from main field (.mjs extension)\",\n\t\t};\n\t}\n\n\tif (main.endsWith(\".cjs\")) {\n\t\treturn {\n\t\t\ttype: \"commonjs\",\n\t\t\tisDualFormat: false,\n\t\t\treason: \"Detected CommonJS from main field (.cjs extension)\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Detect the expected type field from the package's exports or main field.\n *\n * @param json - The package.json content\n * @returns Detection result with type, dual-format indicator, and reason\n */\nfunction detectTypeFromPackage(json: PackageJson): DetectionResult {\n\t// Check exports field for type hints\n\tconst exportsResult = detectTypeFromExports(json.exports);\n\tif (exportsResult !== undefined) {\n\t\treturn exportsResult;\n\t}\n\n\t// Check main field\n\tconst mainResult = detectTypeFromMain(json.main);\n\tif (mainResult !== undefined) {\n\t\treturn mainResult;\n\t}\n\n\t// Check module field (ESM entry point)\n\tif (json.module !== undefined) {\n\t\treturn {\n\t\t\ttype: \"module\",\n\t\t\tisDualFormat: false,\n\t\t\treason: \"Detected ESM from module field presence\",\n\t\t};\n\t}\n\n\treturn {\n\t\ttype: undefined,\n\t\tisDualFormat: false,\n\t\treason: \"Could not detect module type from exports, main, or module fields\",\n\t};\n}\n\n/**\n * Check if a package matches an exclusion pattern.\n *\n * @param packageName - The package name\n * @param excludePackages - List of patterns to exclude\n * @returns true if the package should be excluded\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\t// Scope matching\n\t\tif (pattern.startsWith(\"@\") && packageName.startsWith(`${pattern}/`)) {\n\t\t\treturn true;\n\t\t}\n\t\t// Exact match\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 the package should be skipped from validation.\n */\nfunction shouldSkipPackage(\n\tpackageName: string | undefined,\n\tconfig: PackageEsmTypeConfig,\n): boolean {\n\t// Skip packages without a name\n\tif (packageName === undefined) {\n\t\treturn true;\n\t}\n\n\t// Skip the root package\n\tif (packageName === \"root\") {\n\t\treturn true;\n\t}\n\n\t// Check exclusions\n\treturn isExcluded(packageName, config.excludePackages);\n}\n\n/**\n * Determine the expected type and reason from the config and package.\n */\nfunction determineExpectedType(\n\tjson: PackageJson,\n\tconfig: PackageEsmTypeConfig,\n): {\n\texpectedType: \"module\" | \"commonjs\" | undefined;\n\tdetectionReason: string | undefined;\n} {\n\tif (config.requiredType !== undefined) {\n\t\treturn {\n\t\t\texpectedType: config.requiredType,\n\t\t\tdetectionReason: `Required type: ${config.requiredType}`,\n\t\t};\n\t}\n\n\tif (config.detectFromExports === true) {\n\t\tconst detection = detectTypeFromPackage(json);\n\t\treturn {\n\t\t\texpectedType: detection.type,\n\t\t\tdetectionReason: detection.reason,\n\t\t};\n\t}\n\n\treturn { expectedType: undefined, detectionReason: undefined };\n}\n\n/**\n * Build the error message for a type mismatch.\n */\nfunction buildErrorMessage(\n\tpackageName: string,\n\tcurrentType: string | undefined,\n\texpectedType: string,\n): string {\n\tif (currentType === undefined) {\n\t\treturn `Package \"${packageName}\" is missing the \"type\" field. Expected: \"type\": \"${expectedType}\"`;\n\t}\n\treturn `Package \"${packageName}\" has \"type\": \"${currentType}\" but expected \"type\": \"${expectedType}\"`;\n}\n\n/**\n * Apply the auto-fix for the type field.\n */\nasync function applyFix(\n\tjson: PackageJson,\n\tfile: string,\n\texpectedType: \"module\" | \"commonjs\",\n\tfailResult: PolicyFailure,\n): Promise<PolicyFixResult> {\n\tconst fixResult: PolicyFixResult = {\n\t\t...failResult,\n\t\tresolved: false,\n\t};\n\n\ttry {\n\t\tjson.type = expectedType;\n\t\tconst indent = await detectIndentation(file);\n\t\tawait writeJson(file, json, { spaces: indent });\n\t\tfixResult.resolved = true;\n\t} catch {\n\t\tfixResult.resolved = false;\n\t\tfixResult.errorMessages = [`Failed to update ${file}`];\n\t}\n\n\treturn fixResult;\n}\n\n/**\n * A policy that ensures the `type` field in package.json is correctly set.\n *\n * This policy helps maintain consistency in how packages declare their module format,\n * which is essential for proper module resolution in Node.js and bundlers.\n *\n * @remarks\n *\n * The policy can operate in two modes:\n *\n * 1. **Required Type**: Set `requiredType` to enforce a specific type for all packages\n * 2. **Detection**: Set `detectFromExports: true` to detect the expected type from\n * the package's exports or main field\n *\n * When `detectFromExports` is enabled, the policy looks for:\n * - `.mjs` extensions or `\"import\"` conditions → expects `\"module\"`\n * - `.cjs` extensions or `\"require\"` conditions → expects `\"commonjs\"`\n *\n * @example\n * ```typescript\n * import { makePolicy } from \"repopo\";\n * import { PackageEsmType } from \"repopo/policies\";\n *\n * // Require all packages to be ESM\n * makePolicy(PackageEsmType, { requiredType: \"module\" })\n *\n * // Or detect based on exports\n * makePolicy(PackageEsmType, { detectFromExports: true })\n * ```\n *\n * @alpha\n */\nexport const PackageEsmType = definePackagePolicy<\n\tPackageJson,\n\tPackageEsmTypeConfig | undefined\n>(\"PackageEsmType\", 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// Check if package should be skipped\n\tif (shouldSkipPackage(packageName, config)) {\n\t\treturn true;\n\t}\n\n\t// Determine the expected type\n\tconst { expectedType, detectionReason } = determineExpectedType(json, config);\n\n\t// Handle case where type cannot be detected\n\tif (expectedType === undefined) {\n\t\tif (config.onDetectionFailure === \"fail\") {\n\t\t\treturn {\n\t\t\t\tname: \"PackageEsmType\",\n\t\t\t\tfile,\n\t\t\t\tautoFixable: false,\n\t\t\t\terrorMessages: [\n\t\t\t\t\t`Package \"${packageName}\": ${detectionReason ?? \"Could not detect module type\"}. Set \"type\" field explicitly or use \"requiredType\" config.`,\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\t\t// \"skip\" and \"warn\" both pass validation\n\t\treturn true;\n\t}\n\n\t// Check if current type matches expected\n\tif (json.type === expectedType) {\n\t\treturn true;\n\t}\n\n\t// Build failure result\n\tconst failResult: PolicyFailure = {\n\t\tname: PackageEsmType.name,\n\t\tfile,\n\t\tautoFixable: true,\n\t\t// packageName is guaranteed to be defined here due to shouldSkipPackage check\n\t\terrorMessages: [\n\t\t\tbuildErrorMessage(packageName as string, json.type, expectedType),\n\t\t],\n\t};\n\n\tif (resolve) {\n\t\treturn applyFix(json, file, expectedType, failResult);\n\t}\n\n\treturn failResult;\n});\n"]}
|