@smoothbricks/cli 0.1.1 → 0.3.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 (131) hide show
  1. package/README.md +161 -32
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +110 -10
  4. package/dist/devenv/index.d.ts +10 -0
  5. package/dist/devenv/index.d.ts.map +1 -0
  6. package/dist/devenv/index.js +27 -0
  7. package/dist/generate/index.d.ts +20 -0
  8. package/dist/generate/index.d.ts.map +1 -0
  9. package/dist/generate/index.js +47 -0
  10. package/dist/lib/cli-package.d.ts +4 -0
  11. package/dist/lib/cli-package.d.ts.map +1 -0
  12. package/dist/lib/cli-package.js +15 -0
  13. package/dist/lib/run.d.ts +6 -0
  14. package/dist/lib/run.d.ts.map +1 -1
  15. package/dist/lib/run.js +32 -0
  16. package/dist/lib/workspace.d.ts +25 -1
  17. package/dist/lib/workspace.d.ts.map +1 -1
  18. package/dist/lib/workspace.js +79 -17
  19. package/dist/monorepo/commit-msg.d.ts +8 -1
  20. package/dist/monorepo/commit-msg.d.ts.map +1 -1
  21. package/dist/monorepo/commit-msg.js +85 -7
  22. package/dist/monorepo/index.d.ts +19 -2
  23. package/dist/monorepo/index.d.ts.map +1 -1
  24. package/dist/monorepo/index.js +104 -16
  25. package/dist/monorepo/lockfile.d.ts.map +1 -1
  26. package/dist/monorepo/lockfile.js +9 -0
  27. package/dist/monorepo/managed-files.js +1 -1
  28. package/dist/monorepo/nx-sync.d.ts +2 -1
  29. package/dist/monorepo/nx-sync.d.ts.map +1 -1
  30. package/dist/monorepo/nx-sync.js +8 -2
  31. package/dist/monorepo/package-hygiene.d.ts +6 -2
  32. package/dist/monorepo/package-hygiene.d.ts.map +1 -1
  33. package/dist/monorepo/package-hygiene.js +12 -4
  34. package/dist/monorepo/package-policy.d.ts +13 -4
  35. package/dist/monorepo/package-policy.d.ts.map +1 -1
  36. package/dist/monorepo/package-policy.js +208 -113
  37. package/dist/monorepo/packed-package.d.ts +3 -0
  38. package/dist/monorepo/packed-package.d.ts.map +1 -1
  39. package/dist/monorepo/packed-package.js +68 -36
  40. package/dist/monorepo/packs/index.d.ts +20 -1
  41. package/dist/monorepo/packs/index.d.ts.map +1 -1
  42. package/dist/monorepo/packs/index.js +238 -19
  43. package/dist/monorepo/publish-workflow.d.ts +0 -4
  44. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  45. package/dist/monorepo/publish-workflow.js +7 -15
  46. package/dist/monorepo/tool-validation.d.ts +22 -0
  47. package/dist/monorepo/tool-validation.d.ts.map +1 -0
  48. package/dist/monorepo/tool-validation.js +399 -0
  49. package/dist/nx/index.d.ts +25 -0
  50. package/dist/nx/index.d.ts.map +1 -0
  51. package/dist/nx/index.js +112 -0
  52. package/dist/release/bootstrap-npm-packages.d.ts +19 -0
  53. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
  54. package/dist/release/bootstrap-npm-packages.js +58 -0
  55. package/dist/release/candidates.d.ts +11 -0
  56. package/dist/release/candidates.d.ts.map +1 -0
  57. package/dist/release/candidates.js +181 -0
  58. package/dist/release/core.d.ts +4 -1
  59. package/dist/release/core.d.ts.map +1 -1
  60. package/dist/release/core.js +76 -9
  61. package/dist/release/github-release.d.ts +1 -0
  62. package/dist/release/github-release.d.ts.map +1 -1
  63. package/dist/release/github-release.js +3 -3
  64. package/dist/release/index.d.ts +32 -1
  65. package/dist/release/index.d.ts.map +1 -1
  66. package/dist/release/index.js +488 -64
  67. package/dist/release/npm-auth.d.ts +0 -1
  68. package/dist/release/npm-auth.d.ts.map +1 -1
  69. package/dist/release/npm-auth.js +4 -16
  70. package/dist/release/orchestration.d.ts +11 -2
  71. package/dist/release/orchestration.d.ts.map +1 -1
  72. package/dist/release/orchestration.js +41 -3
  73. package/dist/release/publish-plan.js +1 -1
  74. package/managed/raw/git-format-staged.yml +54 -0
  75. package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
  76. package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
  77. package/managed/templates/github/workflows/ci.yml +4 -2
  78. package/package.json +15 -8
  79. package/src/cli.ts +153 -22
  80. package/src/devenv/index.test.ts +49 -0
  81. package/src/devenv/index.ts +38 -0
  82. package/src/generate/index.ts +92 -0
  83. package/src/lib/cli-package.ts +18 -0
  84. package/src/lib/run.ts +44 -0
  85. package/src/lib/workspace.ts +112 -17
  86. package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
  87. package/src/monorepo/commit-msg.test.ts +45 -0
  88. package/src/monorepo/commit-msg.ts +107 -7
  89. package/src/monorepo/index.ts +142 -16
  90. package/src/monorepo/lockfile.ts +9 -0
  91. package/src/monorepo/managed-files.ts +1 -1
  92. package/src/monorepo/nx-sync.ts +9 -2
  93. package/src/monorepo/package-hygiene.test.ts +34 -0
  94. package/src/monorepo/package-hygiene.ts +26 -4
  95. package/src/monorepo/package-policy.test.ts +1130 -0
  96. package/src/monorepo/package-policy.ts +237 -114
  97. package/src/monorepo/packed-package.ts +89 -37
  98. package/src/monorepo/packs/index.test.ts +189 -0
  99. package/src/monorepo/packs/index.ts +311 -20
  100. package/src/monorepo/publish-workflow.ts +10 -19
  101. package/src/monorepo/setup-test-tracing.test.ts +120 -0
  102. package/src/monorepo/tool-validation.test.ts +261 -0
  103. package/src/monorepo/tool-validation.ts +462 -0
  104. package/src/nx/index.test.ts +73 -0
  105. package/src/nx/index.ts +139 -0
  106. package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
  107. package/src/release/__tests__/candidates.test.ts +262 -0
  108. package/src/release/__tests__/core-properties.test.ts +6 -6
  109. package/src/release/__tests__/core-scenarios.test.ts +27 -20
  110. package/src/release/__tests__/core.test.ts +52 -5
  111. package/src/release/__tests__/fixture-repo.test.ts +90 -31
  112. package/src/release/__tests__/github-release.test.ts +22 -24
  113. package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
  114. package/src/release/__tests__/npm-auth.test.ts +12 -21
  115. package/src/release/__tests__/orchestration.test.ts +142 -12
  116. package/src/release/__tests__/publish-plan.test.ts +9 -3
  117. package/src/release/__tests__/retag-unpublished.test.ts +10 -10
  118. package/src/release/__tests__/trust-publisher.test.ts +165 -0
  119. package/src/release/bootstrap-npm-packages.ts +88 -0
  120. package/src/release/candidates.ts +225 -0
  121. package/src/release/core.ts +96 -10
  122. package/src/release/github-release.ts +3 -3
  123. package/src/release/index.ts +658 -74
  124. package/src/release/npm-auth.ts +4 -33
  125. package/src/release/orchestration.ts +60 -5
  126. package/src/release/publish-plan.ts +1 -1
  127. package/dist/nx-version-actions.cjs +0 -25
  128. package/dist/nx-version-actions.d.cts +0 -6
  129. package/dist/nx-version-actions.d.cts.map +0 -1
  130. package/managed/raw/.git-format-staged.yml +0 -47
  131. package/src/nx-version-actions.cts +0 -36
@@ -1,4 +1,25 @@
1
1
  import { join } from 'node:path';
2
+ import {
3
+ applyWorkspaceBoundedTestTargetPolicy,
4
+ checkWorkspaceBoundedTestTargetPolicy,
5
+ } from '@smoothbricks/nx-plugin/bounded-test-policy';
6
+ import {
7
+ applyPackageTargetPolicy,
8
+ checkPackageTargetPolicy,
9
+ type PackageTargetPolicyOptions,
10
+ type ResolvedProjectTargets,
11
+ } from '@smoothbricks/nx-plugin/package-target-policy';
12
+ import {
13
+ applyReleaseConfigPolicy,
14
+ checkReleaseConfigPolicy,
15
+ SMOO_NX_RELEASE_TAG_PATTERN,
16
+ SMOO_NX_VERSION_ACTIONS,
17
+ } from '@smoothbricks/nx-plugin/release-config-policy';
18
+ import { applyTypecheckTestPolicy, checkTypecheckTestPolicy } from '@smoothbricks/nx-plugin/typecheck-test-policy';
19
+ import {
20
+ applyWorkspaceConfigPolicy,
21
+ checkWorkspaceConfigPolicy,
22
+ } from '@smoothbricks/nx-plugin/workspace-config-policy';
2
23
  import {
3
24
  getOrCreateRecord,
4
25
  hasOwnString,
@@ -12,6 +33,7 @@ import {
12
33
  writeJsonObject,
13
34
  } from '../lib/json.js';
14
35
  import {
36
+ getWorkspacePackageManifests,
15
37
  getWorkspacePackages,
16
38
  listPackageJsonRecords,
17
39
  listPublicPackages,
@@ -21,8 +43,55 @@ import {
21
43
  workspaceDependencyFields,
22
44
  } from '../lib/workspace.js';
23
45
 
24
- export const SMOO_NX_VERSION_ACTIONS = '@smoothbricks/cli/nx-version-actions';
25
- export const SMOO_NX_RELEASE_TAG_PATTERN = '{projectName}@{version}';
46
+ export type { PackageTargetPolicyOptions as WorkspaceDependencyDefaultOptions, ResolvedProjectTargets };
47
+ export { SMOO_NX_RELEASE_TAG_PATTERN, SMOO_NX_VERSION_ACTIONS };
48
+
49
+ const extraCommitScopes = ['release'];
50
+ const rootScriptPolicy: Record<string, string> = {
51
+ lint: 'nx run-many -t lint',
52
+ 'lint:fix': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --unstaged',
53
+ 'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
54
+ 'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
55
+ };
56
+
57
+ export function applyFixableMonorepoDefaults(root: string): void {
58
+ applyRootScriptDefaults(root);
59
+ applyNxPluginDefaults(root);
60
+ applyNxProjectNameDefaults(root);
61
+ }
62
+
63
+ export function applyRootScriptDefaults(root: string): void {
64
+ const rootPackagePath = join(root, 'package.json');
65
+ const rootPackage = readJsonObject(rootPackagePath);
66
+ if (!rootPackage) {
67
+ return;
68
+ }
69
+ const scripts = getOrCreateRecord(rootPackage, 'scripts');
70
+ let changed = false;
71
+ for (const [name, command] of Object.entries(rootScriptPolicy)) {
72
+ changed = setStringProperty(scripts, name, command) || changed;
73
+ }
74
+ const nx = getOrCreateRecord(rootPackage, 'nx');
75
+ if (!Array.isArray(nx.includedScripts) || nx.includedScripts.length !== 0) {
76
+ nx.includedScripts = [];
77
+ changed = true;
78
+ }
79
+ changed = sortRecordInPlace(scripts) || changed;
80
+ if (changed) {
81
+ writeJsonObject(rootPackagePath, rootPackage);
82
+ console.log('updated package.json root smoo scripts');
83
+ } else {
84
+ console.log('unchanged package.json root smoo scripts');
85
+ }
86
+ }
87
+
88
+ export function applyNxPluginDefaults(root: string): void {
89
+ if (applyWorkspaceConfigPolicy(root)) {
90
+ console.log('updated nx.json smoo plugin config');
91
+ } else {
92
+ console.log('unchanged nx.json smoo plugin config');
93
+ }
94
+ }
26
95
 
27
96
  export function applyPublicPackageDefaults(root: string): void {
28
97
  const rootPackage = requiredJsonObject(join(root, 'package.json'));
@@ -65,58 +134,83 @@ export function applyPublicPackageDefaults(root: string): void {
65
134
  }
66
135
  }
67
136
 
68
- export function applyWorkspaceDependencyDefaults(root: string): void {
137
+ export function applyWorkspaceDependencyDefaults(root: string, options: PackageTargetPolicyOptions = {}): void {
69
138
  const workspaceNames = new Set(getWorkspacePackages(root).map((pkg) => pkg.name));
70
139
  for (const pkg of listPackageJsonRecords(root)) {
71
140
  const changed = fixWorkspaceDependencyRanges(pkg.json, workspaceNames);
72
141
  if (changed) {
73
142
  writeJsonObject(pkg.packageJsonPath, pkg.json);
74
143
  console.log(`updated ${pkg.path}/package.json workspace dependency ranges`);
75
- } else {
76
- console.log(`unchanged ${pkg.path}/package.json workspace dependency ranges`);
77
144
  }
78
145
  }
79
- }
80
-
81
- export function applyNxReleaseDefaults(root: string): void {
82
- const nxJsonPath = join(root, 'nx.json');
83
- const nxJson = requiredJsonObject(nxJsonPath);
84
- let changed = false;
85
- const release = getOrCreateRecord(nxJson, 'release');
86
- changed = setStringProperty(release, 'projectsRelationship', 'independent') || changed;
87
- const version = getOrCreateRecord(release, 'version');
88
- changed = setStringProperty(version, 'specifierSource', 'conventional-commits') || changed;
89
- changed = setStringProperty(version, 'currentVersionResolver', 'git-tag') || changed;
90
- changed = setStringProperty(version, 'fallbackCurrentVersionResolver', 'disk') || changed;
91
- changed = setStringProperty(version, 'versionActions', SMOO_NX_VERSION_ACTIONS) || changed;
92
- if (delete version.preVersionCommand) {
93
- changed = true;
146
+ if (applyPackageTargetPolicy(root, options)) {
147
+ console.log('updated package Nx target policy');
148
+ } else {
149
+ console.log('unchanged package Nx target policy');
94
150
  }
95
- const releaseTag = getOrCreateRecord(release, 'releaseTag');
96
- changed = setStringProperty(releaseTag, 'pattern', SMOO_NX_RELEASE_TAG_PATTERN) || changed;
97
- const changelog = getOrCreateRecord(release, 'changelog');
98
- changed = setBooleanProperty(changelog, 'workspaceChangelog', false) || changed;
99
- const projectChangelogs = getOrCreateRecord(changelog, 'projectChangelogs');
100
- changed = setBooleanProperty(projectChangelogs, 'createRelease', false) || changed;
101
- changed = setBooleanProperty(projectChangelogs, 'file', false) || changed;
102
- const renderOptions = getOrCreateRecord(projectChangelogs, 'renderOptions');
103
- if (typeof renderOptions.authors !== 'boolean') {
104
- renderOptions.authors = true;
105
- changed = true;
151
+ if (applyTypecheckTestPolicy(root)) {
152
+ console.log('updated tsconfig.test.json policy');
153
+ } else {
154
+ console.log('unchanged tsconfig.test.json policy');
106
155
  }
107
- if (typeof renderOptions.applyUsernameToAuthors !== 'boolean') {
108
- renderOptions.applyUsernameToAuthors = true;
109
- changed = true;
156
+ if (applyWorkspaceBoundedTestTargetPolicy(root)) {
157
+ console.log('updated package test targets bounded execution policy');
158
+ } else {
159
+ console.log('unchanged package test targets bounded execution policy');
110
160
  }
161
+ }
111
162
 
112
- if (changed) {
113
- writeJsonObject(nxJsonPath, nxJson);
163
+ export function applyNxReleaseDefaults(root: string): void {
164
+ if (applyReleaseConfigPolicy(root)) {
114
165
  console.log('updated nx.json release config');
115
166
  } else {
116
167
  console.log('unchanged nx.json release config');
117
168
  }
118
169
  }
119
170
 
171
+ export function applyNxProjectNameDefaults(root: string): void {
172
+ const rootPackage = requiredJsonObject(join(root, 'package.json'));
173
+ const rootName = stringProperty(rootPackage, 'name');
174
+ if (!rootName) {
175
+ return;
176
+ }
177
+ for (const pkg of getWorkspacePackageManifests(root)) {
178
+ const suggestedName = suggestNxProjectName(rootName, pkg.name);
179
+ if (!suggestedName) {
180
+ continue;
181
+ }
182
+ const nx = getOrCreateRecord(pkg.json, 'nx');
183
+ const changed = setStringProperty(nx, 'name', suggestedName);
184
+ if (changed) {
185
+ writeJsonObject(pkg.packageJsonPath, pkg.json);
186
+ console.log(`updated ${pkg.path}/package.json nx.name`);
187
+ } else {
188
+ console.log(`unchanged ${pkg.path}/package.json nx.name`);
189
+ }
190
+ }
191
+ }
192
+
193
+ export function listValidCommitScopes(root: string): ReadonlySet<string> {
194
+ return new Set([...listNxProjectNames(root), ...extraCommitScopes]);
195
+ }
196
+
197
+ export function listNxProjectNames(root: string): string[] {
198
+ const rootPackage = readJsonObject(join(root, 'package.json'));
199
+ const rootName = rootPackage ? stringProperty(rootPackage, 'name') : null;
200
+ const names: string[] = [];
201
+ for (const pkg of getWorkspacePackageManifests(root)) {
202
+ const nx = recordProperty(pkg.json, 'nx');
203
+ const configuredName = nx ? stringProperty(nx, 'name') : null;
204
+ const suggestedName = rootName ? suggestNxProjectName(rootName, pkg.name) : null;
205
+ if (configuredName) {
206
+ names.push(configuredName);
207
+ } else if (suggestedName) {
208
+ names.push(suggestedName);
209
+ }
210
+ }
211
+ return names;
212
+ }
213
+
120
214
  export function validateRootPackagePolicy(root: string): number {
121
215
  const rootPackage = readJsonObject(join(root, 'package.json'));
122
216
  if (!rootPackage) {
@@ -136,6 +230,8 @@ export function validateRootPackagePolicy(root: string): number {
136
230
  console.error('package.json must define repository.url');
137
231
  failures++;
138
232
  }
233
+ failures += validateRootScripts(rootPackage);
234
+ failures += validateRootNxScriptInference(rootPackage);
139
235
  const packageManager = stringProperty(rootPackage, 'packageManager');
140
236
  if (!packageManager?.startsWith('bun@')) {
141
237
  console.error('package.json packageManager must use bun@<version>');
@@ -156,85 +252,41 @@ export function validateRootPackagePolicy(root: string): number {
156
252
  }
157
253
 
158
254
  export function validateNxReleaseConfig(root: string): number {
159
- const nxJson = readJsonObject(join(root, 'nx.json'));
160
- if (!nxJson) {
161
- console.error('nx.json not found or invalid');
162
- return 1;
163
- }
164
- const release = recordProperty(nxJson, 'release');
165
- const version = release ? recordProperty(release, 'version') : null;
166
- const releaseTag = release ? recordProperty(release, 'releaseTag') : null;
167
- const changelog = release ? recordProperty(release, 'changelog') : null;
168
- const projectChangelogs = changelog ? recordProperty(changelog, 'projectChangelogs') : null;
169
- const renderOptions = projectChangelogs ? recordProperty(projectChangelogs, 'renderOptions') : null;
170
255
  let failures = 0;
171
- if (!release) {
172
- console.error('nx.json release config is missing');
173
- failures++;
174
- }
175
- if (release && stringProperty(release, 'projectsRelationship') !== 'independent') {
176
- console.error('nx.json release.projectsRelationship must be independent');
177
- failures++;
178
- }
179
- if (!version) {
180
- console.error('nx.json release.version config is missing');
181
- failures++;
182
- }
183
- if (version && stringProperty(version, 'specifierSource') !== 'conventional-commits') {
184
- console.error('nx.json release.version.specifierSource must be conventional-commits');
185
- failures++;
186
- }
187
- // Nx requires git-tag as the primary resolver when deriving versions from
188
- // conventional commits. Disk is only a fallback for initial untagged packages.
189
- if (version && stringProperty(version, 'currentVersionResolver') !== 'git-tag') {
190
- console.error('nx.json release.version.currentVersionResolver must be git-tag');
256
+ for (const issue of checkWorkspaceConfigPolicy(root)) {
257
+ console.error(issue.message);
191
258
  failures++;
192
259
  }
193
- if (version && stringProperty(version, 'fallbackCurrentVersionResolver') !== 'disk') {
194
- console.error('nx.json release.version.fallbackCurrentVersionResolver must be disk');
260
+ for (const issue of checkReleaseConfigPolicy(root)) {
261
+ console.error(issue.message);
195
262
  failures++;
196
263
  }
197
- if (version && stringProperty(version, 'versionActions') !== SMOO_NX_VERSION_ACTIONS) {
198
- console.error(`nx.json release.version.versionActions must be ${SMOO_NX_VERSION_ACTIONS}`);
199
- failures++;
200
- }
201
- if (version && stringProperty(version, 'preVersionCommand')) {
202
- console.error(
203
- 'nx.json release.version.preVersionCommand must not be defined; smoo builds npm-missing packages before publish',
204
- );
205
- failures++;
206
- }
207
- if (!releaseTag) {
208
- console.error('nx.json release.releaseTag config is missing');
209
- failures++;
210
- }
211
- if (releaseTag && stringProperty(releaseTag, 'pattern') !== SMOO_NX_RELEASE_TAG_PATTERN) {
212
- console.error(`nx.json release.releaseTag.pattern must be ${SMOO_NX_RELEASE_TAG_PATTERN}`);
213
- failures++;
214
- }
215
- if (!changelog) {
216
- console.error('nx.json release.changelog config is missing');
217
- failures++;
218
- }
219
- if (changelog && changelog.workspaceChangelog !== false) {
220
- console.error('nx.json release.changelog.workspaceChangelog must be false');
221
- failures++;
222
- }
223
- if (!projectChangelogs) {
224
- console.error('nx.json release.changelog.projectChangelogs config is missing');
225
- failures++;
226
- }
227
- if (projectChangelogs && projectChangelogs.createRelease !== false) {
228
- console.error('nx.json release.changelog.projectChangelogs.createRelease must be false');
229
- failures++;
264
+ return failures;
265
+ }
266
+
267
+ export function validateNxProjectNames(root: string): number {
268
+ const rootPackage = readJsonObject(join(root, 'package.json'));
269
+ const rootName = rootPackage ? stringProperty(rootPackage, 'name') : null;
270
+ if (!rootName) {
271
+ return 0;
230
272
  }
231
- if (projectChangelogs && projectChangelogs.file !== false) {
232
- console.error('nx.json release.changelog.projectChangelogs.file must be false');
233
- failures++;
273
+ let failures = 0;
274
+ for (const pkg of getWorkspacePackageManifests(root)) {
275
+ const suggestedName = suggestNxProjectName(rootName, pkg.name);
276
+ if (!suggestedName) {
277
+ continue;
278
+ }
279
+ const nx = recordProperty(pkg.json, 'nx');
280
+ const configuredName = nx ? stringProperty(nx, 'name') : null;
281
+ if (configuredName !== suggestedName) {
282
+ console.error(
283
+ `${pkg.path}: package.json nx.name must be "${suggestedName}" so fix(${suggestedName}): maps to this project`,
284
+ );
285
+ failures++;
286
+ }
234
287
  }
235
- if (!renderOptions) {
236
- console.error('nx.json release.changelog.projectChangelogs.renderOptions config is missing');
237
- failures++;
288
+ if (failures === 0) {
289
+ console.log('Nx project names are valid.');
238
290
  }
239
291
  return failures;
240
292
  }
@@ -315,9 +367,9 @@ export function validatePublicPackageMetadata(root: string): number {
315
367
  return failures;
316
368
  }
317
369
 
318
- export function validateWorkspaceDependencies(root: string): number {
319
- const workspaceNames = new Set(getWorkspacePackages(root).map((pkg) => pkg.name));
370
+ export function validateWorkspaceDependencies(root: string, options: PackageTargetPolicyOptions = {}): number {
320
371
  let failures = 0;
372
+ const workspaceNames = new Set(getWorkspacePackages(root).map((pkg) => pkg.name));
321
373
  for (const pkg of listPackageJsonRecords(root)) {
322
374
  for (const field of workspaceDependencyFields) {
323
375
  const dependencies = recordProperty(pkg.json, field);
@@ -332,12 +384,28 @@ export function validateWorkspaceDependencies(root: string): number {
332
384
  }
333
385
  }
334
386
  }
387
+ for (const issue of checkPackageTargetPolicy(root, options)) {
388
+ console.error(`${issue.path}: ${issue.message}`);
389
+ failures++;
390
+ }
391
+ for (const issue of checkTypecheckTestPolicy(root)) {
392
+ console.error(`${issue.path}: ${issue.message}`);
393
+ failures++;
394
+ }
395
+ for (const issue of checkWorkspaceBoundedTestTargetPolicy(root)) {
396
+ console.error(`${issue.path}: ${issue.message}`);
397
+ failures++;
398
+ }
335
399
  if (failures === 0) {
336
- console.log('Workspace dependency ranges are valid.');
400
+ console.log('Workspace dependency policy is valid.');
337
401
  }
338
402
  return failures;
339
403
  }
340
404
 
405
+ // ---------------------------------------------------------------------------
406
+ // Helpers kept in CLI (not Nx-specific)
407
+ // ---------------------------------------------------------------------------
408
+
341
409
  function fixWorkspaceDependencyRanges(pkg: Record<string, unknown>, workspaceNames: Set<string>): boolean {
342
410
  let changed = false;
343
411
  for (const field of workspaceDependencyFields) {
@@ -355,11 +423,66 @@ function fixWorkspaceDependencyRanges(pkg: Record<string, unknown>, workspaceNam
355
423
  return changed;
356
424
  }
357
425
 
358
- function setBooleanProperty(record: Record<string, unknown>, key: string, value: boolean): boolean {
359
- if (record[key] === value) {
426
+ function validateRootScripts(rootPackage: Record<string, unknown>): number {
427
+ const scripts = recordProperty(rootPackage, 'scripts');
428
+ let failures = 0;
429
+ for (const [name, command] of Object.entries(rootScriptPolicy)) {
430
+ if (scripts?.[name] !== command) {
431
+ console.error(`package.json scripts.${name} must be ${command}`);
432
+ failures++;
433
+ }
434
+ }
435
+ if (scripts && !recordKeysAreSorted(scripts)) {
436
+ console.error(
437
+ 'package.json scripts must be sorted alphabetically so root command policy stays stable across fixes.',
438
+ );
439
+ failures++;
440
+ }
441
+ return failures;
442
+ }
443
+
444
+ function validateRootNxScriptInference(rootPackage: Record<string, unknown>): number {
445
+ const nx = recordProperty(rootPackage, 'nx');
446
+ if (nx && Array.isArray(nx.includedScripts) && nx.includedScripts.length === 0) {
447
+ return 0;
448
+ }
449
+ console.error('package.json nx.includedScripts must be [] so root scripts do not become recursive Nx targets.');
450
+ return 1;
451
+ }
452
+
453
+ function suggestNxProjectName(rootPackageName: string, packageName: string): string | null {
454
+ const rootScope = npmScope(rootPackageName);
455
+ if (!rootScope || npmScope(packageName) !== rootScope) {
456
+ return null;
457
+ }
458
+ return unscopedPackageName(packageName);
459
+ }
460
+
461
+ function npmScope(packageName: string): string | null {
462
+ const match = /^(@[^/]+)\//.exec(packageName);
463
+ return match?.[1] ?? null;
464
+ }
465
+
466
+ function unscopedPackageName(packageName: string): string {
467
+ return packageName.startsWith('@') ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
468
+ }
469
+
470
+ function recordKeysAreSorted(record: Record<string, unknown>): boolean {
471
+ const keys = Object.keys(record);
472
+ return keys.every((key, index) => index === 0 || keys[index - 1] <= key);
473
+ }
474
+
475
+ function sortRecordInPlace(record: Record<string, unknown>): boolean {
476
+ if (recordKeysAreSorted(record)) {
360
477
  return false;
361
478
  }
362
- record[key] = value;
479
+ const entries = Object.entries(record).sort(([a], [b]) => a.localeCompare(b));
480
+ for (const key of Object.keys(record)) {
481
+ delete record[key];
482
+ }
483
+ for (const [key, value] of entries) {
484
+ record[key] = value;
485
+ }
363
486
  return true;
364
487
  }
365
488
 
@@ -3,67 +3,119 @@ import { join } from 'node:path';
3
3
  import { publint } from 'publint';
4
4
  import { formatMessage } from 'publint/utils';
5
5
  import { isRecord } from '../lib/json.js';
6
- import { runStatus } from '../lib/run.js';
6
+ import { runResult, runStatus } from '../lib/run.js';
7
7
  import type { PackageInfo } from '../lib/workspace.js';
8
8
  import { listPublicPackages } from '../lib/workspace.js';
9
9
  import { readPackedPackageJson, validatePackedWorkspaceDependencies } from './packed-manifest.js';
10
10
 
11
11
  export async function validatePackedPublicPackages(root: string): Promise<number> {
12
+ return (
13
+ (await validatePackedPublicPackagePublint(root)) +
14
+ (await validatePackedPublicPackageManifest(root)) +
15
+ (await validatePackedPublicPackageTypes(root))
16
+ );
17
+ }
18
+
19
+ export async function validatePackedPublicPackagePublint(root: string): Promise<number> {
12
20
  let failures = 0;
13
21
  for (const pkg of listPublicPackages(root)) {
14
- failures += await validatePackedPublicPackage(root, pkg);
22
+ failures += await validatePackedPublicPackageTool(root, pkg, validatePublint);
15
23
  }
16
24
  return failures;
17
25
  }
18
26
 
19
- async function validatePackedPublicPackage(root: string, pkg: PackageInfo): Promise<number> {
27
+ export async function validatePackedPublicPackageManifest(root: string): Promise<number> {
20
28
  let failures = 0;
21
- const packed = await packPackage(root, pkg);
22
- try {
23
- const lint = await publint({ pack: { tarball: packed.arrayBuffer }, level: 'warning', strict: true });
24
- for (const message of lint.messages) {
25
- console.error(`${pkg.path}: publint ${message.type} ${message.code}: ${formatMessage(message, lint.pkg)}`);
26
- failures++;
27
- }
29
+ for (const pkg of listPublicPackages(root)) {
30
+ failures += await validatePackedPublicPackageTool(root, pkg, validatePackedManifest);
31
+ }
32
+ return failures;
33
+ }
28
34
 
29
- const packedPackage = await readPackedPackageJson(root, packed.path, pkg.name);
30
- for (const message of validatePackedWorkspaceDependencies(root, pkg, packedPackage)) {
31
- console.error(message);
32
- failures++;
33
- }
35
+ export async function validatePackedPublicPackageTypes(root: string): Promise<number> {
36
+ let failures = 0;
37
+ for (const pkg of listPublicPackages(root)) {
38
+ failures += await validatePackedPublicPackageTool(root, pkg, validateAttw);
39
+ }
40
+ return failures;
41
+ }
34
42
 
35
- const attwArgs = [
36
- packed.path,
37
- '--format',
38
- 'ascii',
39
- '--no-color',
40
- '--profile',
41
- 'node16',
42
- '--ignore-rules',
43
- 'cjs-resolves-to-esm',
44
- ...attwExcludedEntrypointArgs(pkg),
45
- ];
46
- const attwStatus = await runStatus('attw', ['--quiet', ...attwArgs], root, true);
47
- if (attwStatus !== 0) {
48
- await runStatus('attw', attwArgs, root);
49
- console.error(`${pkg.path}: are-the-types-wrong validation failed`);
50
- failures++;
51
- } else {
52
- console.log(`${pkg.path}: are-the-types-wrong valid.`);
53
- }
43
+ async function validatePackedPublicPackageTool(
44
+ root: string,
45
+ pkg: PackageInfo,
46
+ validate: (root: string, pkg: PackageInfo, packed: { path: string; arrayBuffer: ArrayBuffer }) => Promise<number>,
47
+ ): Promise<number> {
48
+ const packed = await packPackage(root, pkg);
49
+ try {
50
+ return await validate(root, pkg, packed);
54
51
  } catch (error) {
55
52
  const message = error instanceof Error ? error.message : String(error);
56
53
  console.error(`${pkg.path}: packed package validation failed: ${message}`);
57
- failures++;
54
+ return 1;
58
55
  } finally {
59
56
  unlinkSync(packed.path);
60
57
  }
61
- if (failures === 0) {
62
- console.log(`${pkg.path}: packed package is valid.`);
58
+ }
59
+
60
+ async function validatePublint(
61
+ _root: string,
62
+ pkg: PackageInfo,
63
+ packed: { path: string; arrayBuffer: ArrayBuffer },
64
+ ): Promise<number> {
65
+ let failures = 0;
66
+ const lint = await publint({ pack: { tarball: packed.arrayBuffer }, level: 'warning', strict: true });
67
+ for (const message of lint.messages) {
68
+ console.error(`${pkg.path}: publint ${message.type} ${message.code}: ${formatMessage(message, lint.pkg)}`);
69
+ failures++;
70
+ }
71
+ return failures;
72
+ }
73
+
74
+ async function validatePackedManifest(
75
+ root: string,
76
+ pkg: PackageInfo,
77
+ packed: { path: string; arrayBuffer: ArrayBuffer },
78
+ ): Promise<number> {
79
+ let failures = 0;
80
+ const packedPackage = await readPackedPackageJson(root, packed.path, pkg.name);
81
+ for (const message of validatePackedWorkspaceDependencies(root, pkg, packedPackage)) {
82
+ console.error(message);
83
+ failures++;
63
84
  }
64
85
  return failures;
65
86
  }
66
87
 
88
+ async function validateAttw(root: string, pkg: PackageInfo, packed: { path: string }): Promise<number> {
89
+ const attwArgs = [
90
+ packed.path,
91
+ '--format',
92
+ 'ascii',
93
+ '--no-color',
94
+ '--profile',
95
+ 'node16',
96
+ '--ignore-rules',
97
+ 'cjs-resolves-to-esm',
98
+ ...attwExcludedEntrypointArgs(pkg),
99
+ ];
100
+ const attw = await runResult('attw', attwArgs, root);
101
+ if (attw.exitCode === 0) {
102
+ return 0;
103
+ }
104
+ printAttwOutput(attw.stdout);
105
+ printAttwOutput(attw.stderr);
106
+ console.error(`${pkg.path}: are-the-types-wrong validation failed`);
107
+ return 1;
108
+ }
109
+
110
+ function printAttwOutput(output: string): void {
111
+ for (const line of output.split('\n')) {
112
+ if (!line || line.includes('(ignored)')) {
113
+ continue;
114
+ }
115
+ console.error(line);
116
+ }
117
+ }
118
+
67
119
  function attwExcludedEntrypointArgs(pkg: PackageInfo): string[] {
68
120
  const excluded = wasmExportEntrypoints(pkg.json.exports);
69
121
  return excluded.length === 0 ? [] : ['--exclude-entrypoints', ...excluded];