repopo 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +187 -6
  2. package/THIRD-PARTY-LICENSES.txt +149 -3
  3. package/esm/adapters/fluidFramework.d.ts +103 -0
  4. package/esm/adapters/fluidFramework.d.ts.map +1 -0
  5. package/esm/adapters/fluidFramework.js +119 -0
  6. package/esm/adapters/fluidFramework.js.map +1 -0
  7. package/esm/baseCommand.d.ts +1 -0
  8. package/esm/baseCommand.d.ts.map +1 -1
  9. package/esm/baseCommand.js +2 -1
  10. package/esm/baseCommand.js.map +1 -1
  11. package/esm/commands/check.d.ts.map +1 -1
  12. package/esm/commands/check.js +9 -8
  13. package/esm/commands/check.js.map +1 -1
  14. package/esm/index.d.ts +1 -0
  15. package/esm/index.d.ts.map +1 -1
  16. package/esm/index.js +1 -0
  17. package/esm/index.js.map +1 -1
  18. package/esm/perf.d.ts +4 -1
  19. package/esm/perf.d.ts.map +1 -1
  20. package/esm/perf.js +3 -2
  21. package/esm/perf.js.map +1 -1
  22. package/esm/policies/LicenseFileExists.d.ts.map +1 -1
  23. package/esm/policies/LicenseFileExists.js +3 -1
  24. package/esm/policies/LicenseFileExists.js.map +1 -1
  25. package/esm/policies/NoJsFileExtensions.d.ts.map +1 -1
  26. package/esm/policies/NoJsFileExtensions.js +4 -1
  27. package/esm/policies/NoJsFileExtensions.js.map +1 -1
  28. package/esm/policies/NoLargeBinaryFiles.d.ts.map +1 -1
  29. package/esm/policies/NoLargeBinaryFiles.js +3 -1
  30. package/esm/policies/NoLargeBinaryFiles.js.map +1 -1
  31. package/esm/policies/NoPrivateWorkspaceDependencies.d.ts +37 -0
  32. package/esm/policies/NoPrivateWorkspaceDependencies.d.ts.map +1 -0
  33. package/esm/policies/NoPrivateWorkspaceDependencies.js +137 -0
  34. package/esm/policies/NoPrivateWorkspaceDependencies.js.map +1 -0
  35. package/esm/policies/PackageAllowedScopes.d.ts +88 -0
  36. package/esm/policies/PackageAllowedScopes.d.ts.map +1 -0
  37. package/esm/policies/PackageAllowedScopes.js +113 -0
  38. package/esm/policies/PackageAllowedScopes.js.map +1 -0
  39. package/esm/policies/PackageEsmType.d.ts +100 -0
  40. package/esm/policies/PackageEsmType.d.ts.map +1 -0
  41. package/esm/policies/PackageEsmType.js +257 -0
  42. package/esm/policies/PackageEsmType.js.map +1 -0
  43. package/esm/policies/PackageFolderName.d.ts +97 -0
  44. package/esm/policies/PackageFolderName.d.ts.map +1 -0
  45. package/esm/policies/PackageFolderName.js +104 -0
  46. package/esm/policies/PackageFolderName.js.map +1 -0
  47. package/esm/policies/PackageJsonProperties.d.ts.map +1 -1
  48. package/esm/policies/PackageJsonProperties.js +2 -1
  49. package/esm/policies/PackageJsonProperties.js.map +1 -1
  50. package/esm/policies/PackageJsonRepoDirectoryProperty.d.ts.map +1 -1
  51. package/esm/policies/PackageJsonRepoDirectoryProperty.js +4 -3
  52. package/esm/policies/PackageJsonRepoDirectoryProperty.js.map +1 -1
  53. package/esm/policies/PackageJsonSorted.d.ts.map +1 -1
  54. package/esm/policies/PackageJsonSorted.js +11 -3
  55. package/esm/policies/PackageJsonSorted.js.map +1 -1
  56. package/esm/policies/PackageLicense.d.ts +61 -0
  57. package/esm/policies/PackageLicense.d.ts.map +1 -0
  58. package/esm/policies/PackageLicense.js +131 -0
  59. package/esm/policies/PackageLicense.js.map +1 -0
  60. package/esm/policies/PackagePrivateField.d.ts +124 -0
  61. package/esm/policies/PackagePrivateField.d.ts.map +1 -0
  62. package/esm/policies/PackagePrivateField.js +155 -0
  63. package/esm/policies/PackagePrivateField.js.map +1 -0
  64. package/esm/policies/PackageReadme.d.ts +77 -0
  65. package/esm/policies/PackageReadme.d.ts.map +1 -0
  66. package/esm/policies/PackageReadme.js +209 -0
  67. package/esm/policies/PackageReadme.js.map +1 -0
  68. package/esm/policies/PackageScripts.d.ts +180 -2
  69. package/esm/policies/PackageScripts.d.ts.map +1 -1
  70. package/esm/policies/PackageScripts.js +349 -29
  71. package/esm/policies/PackageScripts.js.map +1 -1
  72. package/esm/policies/PackageTestScripts.d.ts +100 -0
  73. package/esm/policies/PackageTestScripts.d.ts.map +1 -0
  74. package/esm/policies/PackageTestScripts.js +149 -0
  75. package/esm/policies/PackageTestScripts.js.map +1 -0
  76. package/esm/policies/RequiredGitignorePatterns.d.ts.map +1 -1
  77. package/esm/policies/RequiredGitignorePatterns.js +10 -4
  78. package/esm/policies/RequiredGitignorePatterns.js.map +1 -1
  79. package/esm/policies.d.ts +9 -1
  80. package/esm/policies.d.ts.map +1 -1
  81. package/esm/policies.js +10 -1
  82. package/esm/policies.js.map +1 -1
  83. package/esm/policy.d.ts +5 -1
  84. package/esm/policy.d.ts.map +1 -1
  85. package/esm/policy.js.map +1 -1
  86. package/esm/policyDefiners/defineFileHeaderPolicy.d.ts.map +1 -1
  87. package/esm/policyDefiners/defineFileHeaderPolicy.js +2 -1
  88. package/esm/policyDefiners/defineFileHeaderPolicy.js.map +1 -1
  89. package/esm/policyDefiners/definePackagePolicy.d.ts.map +1 -1
  90. package/esm/policyDefiners/definePackagePolicy.js +2 -1
  91. package/esm/policyDefiners/definePackagePolicy.js.map +1 -1
  92. package/esm/tsdoc-metadata.json +1 -1
  93. package/esm/utils/indentation.d.ts +73 -0
  94. package/esm/utils/indentation.d.ts.map +1 -0
  95. package/esm/utils/indentation.js +97 -0
  96. package/esm/utils/indentation.js.map +1 -0
  97. package/esm/utils/packageName.d.ts +71 -0
  98. package/esm/utils/packageName.d.ts.map +1 -0
  99. package/esm/utils/packageName.js +79 -0
  100. package/esm/utils/packageName.js.map +1 -0
  101. package/oclif.manifest.json +1 -1
  102. package/package.json +30 -15
@@ -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":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAGhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,QAAQ,CAAC;AA6F1C;;GAEG;AACH,SAAS,qBAAqB,CAC7B,OAA+B;IAE/B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC9E,MAAM,MAAM,GACX,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAEjE,qDAAqD;IACrD,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;QACtB,OAAO;YACN,IAAI,EAAE,QAAQ;YACd,YAAY,EAAE,IAAI;YAClB,MAAM,EACL,6EAA6E;SAC9E,CAAC;IACH,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO;YACN,IAAI,EAAE,QAAQ;YACd,YAAY,EAAE,KAAK;YACnB,MAAM,EAAE,4DAA4D;SACpE,CAAC;IACH,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO;YACN,IAAI,EAAE,UAAU;YAChB,YAAY,EAAE,KAAK;YACnB,MAAM,EACL,kEAAkE;SACnE,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,IAAa;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,OAAO;YACN,IAAI,EAAE,QAAQ;YACd,YAAY,EAAE,KAAK;YACnB,MAAM,EAAE,+CAA+C;SACvD,CAAC;IACH,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,OAAO;YACN,IAAI,EAAE,UAAU;YAChB,YAAY,EAAE,KAAK;YACnB,MAAM,EAAE,oDAAoD;SAC5D,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,IAAiB;IAC/C,qCAAqC;IACrC,MAAM,aAAa,GAAG,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1D,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,aAAa,CAAC;IACtB,CAAC;IAED,mBAAmB;IACnB,MAAM,UAAU,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,UAAU,CAAC;IACnB,CAAC;IAED,uCAAuC;IACvC,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO;YACN,IAAI,EAAE,QAAQ;YACd,YAAY,EAAE,KAAK;YACnB,MAAM,EAAE,yCAAyC;SACjD,CAAC;IACH,CAAC;IAED,OAAO;QACN,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,KAAK;QACnB,MAAM,EAAE,mEAAmE;KAC3E,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,UAAU,CAClB,WAAmB,EACnB,eAAqC;IAErC,IAAI,eAAe,KAAK,SAAS,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnE,OAAO,KAAK,CAAC;IACd,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;QACvC,iBAAiB;QACjB,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,EAAE,CAAC;YACtE,OAAO,IAAI,CAAC;QACb,CAAC;QACD,cAAc;QACd,IAAI,WAAW,KAAK,OAAO,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IAED,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB,CACzB,WAA+B,EAC/B,MAA4B;IAE5B,+BAA+B;IAC/B,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,wBAAwB;IACxB,IAAI,WAAW,KAAK,MAAM,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,mBAAmB;IACnB,OAAO,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;AACxD,CAAC;AAED;;GAEG;AACH,SAAS,qBAAqB,CAC7B,IAAiB,EACjB,MAA4B;IAK5B,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvC,OAAO;YACN,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,eAAe,EAAE,kBAAkB,MAAM,CAAC,YAAY,EAAE;SACxD,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,iBAAiB,KAAK,IAAI,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC9C,OAAO;YACN,YAAY,EAAE,SAAS,CAAC,IAAI;YAC5B,eAAe,EAAE,SAAS,CAAC,MAAM;SACjC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,CAAC;AAChE,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB,CACzB,WAAmB,EACnB,WAA+B,EAC/B,YAAoB;IAEpB,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,YAAY,WAAW,qDAAqD,YAAY,GAAG,CAAC;IACpG,CAAC;IACD,OAAO,YAAY,WAAW,kBAAkB,WAAW,2BAA2B,YAAY,GAAG,CAAC;AACvG,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,QAAQ,CACtB,IAAiB,EACjB,IAAY,EACZ,YAAmC,EACnC,UAAyB;IAEzB,MAAM,SAAS,GAAoB;QAClC,GAAG,UAAU;QACb,QAAQ,EAAE,KAAK;KACf,CAAC;IAEF,IAAI,CAAC;QACJ,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAChD,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACR,SAAS,CAAC,QAAQ,GAAG,KAAK,CAAC;QAC3B,SAAS,CAAC,aAAa,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,SAAS,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,CAG/C,gBAAgB,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;IAC7D,yCAAyC;IACzC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC;IAE9B,qCAAqC;IACrC,IAAI,iBAAiB,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC;IACb,CAAC;IAED,8BAA8B;IAC9B,MAAM,EAAE,YAAY,EAAE,eAAe,EAAE,GAAG,qBAAqB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAE9E,4CAA4C;IAC5C,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAChC,IAAI,MAAM,CAAC,kBAAkB,KAAK,MAAM,EAAE,CAAC;YAC1C,OAAO;gBACN,IAAI,EAAE,gBAAgB;gBACtB,IAAI;gBACJ,WAAW,EAAE,KAAK;gBAClB,aAAa,EAAE;oBACd,YAAY,WAAW,MAAM,eAAe,IAAI,8BAA8B,6DAA6D;iBAC3I;aACD,CAAC;QACH,CAAC;QACD,yCAAyC;QACzC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,yCAAyC;IACzC,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAChC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,uBAAuB;IACvB,MAAM,UAAU,GAAkB;QACjC,IAAI,EAAE,cAAc,CAAC,IAAI;QACzB,IAAI;QACJ,WAAW,EAAE,IAAI;QACjB,8EAA8E;QAC9E,aAAa,EAAE;YACd,iBAAiB,CAAC,WAAqB,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;SACjE;KACD,CAAC;IAEF,IAAI,OAAO,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,UAAU,CAAC;AACnB,CAAC,CAAC,CAAC","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"]}