@smoothbricks/cli 0.1.0 → 0.1.1

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 (92) hide show
  1. package/README.md +208 -83
  2. package/dist/cli.js +50 -20
  3. package/dist/lib/devenv.d.ts +6 -0
  4. package/dist/lib/devenv.d.ts.map +1 -0
  5. package/dist/lib/devenv.js +82 -0
  6. package/dist/lib/workspace.d.ts +5 -0
  7. package/dist/lib/workspace.d.ts.map +1 -1
  8. package/dist/lib/workspace.js +62 -5
  9. package/dist/monorepo/index.d.ts +6 -1
  10. package/dist/monorepo/index.d.ts.map +1 -1
  11. package/dist/monorepo/index.js +38 -5
  12. package/dist/monorepo/lockfile.d.ts +5 -1
  13. package/dist/monorepo/lockfile.d.ts.map +1 -1
  14. package/dist/monorepo/lockfile.js +45 -5
  15. package/dist/monorepo/managed-files.d.ts +1 -1
  16. package/dist/monorepo/managed-files.d.ts.map +1 -1
  17. package/dist/monorepo/managed-files.js +27 -10
  18. package/dist/monorepo/package-hygiene.js +1 -1
  19. package/dist/monorepo/package-policy.d.ts +3 -0
  20. package/dist/monorepo/package-policy.d.ts.map +1 -1
  21. package/dist/monorepo/package-policy.js +122 -19
  22. package/dist/monorepo/packed-manifest.d.ts +4 -0
  23. package/dist/monorepo/packed-manifest.d.ts.map +1 -0
  24. package/dist/monorepo/packed-manifest.js +51 -0
  25. package/dist/monorepo/packed-package.d.ts.map +1 -1
  26. package/dist/monorepo/packed-package.js +6 -0
  27. package/dist/monorepo/packs/index.d.ts.map +1 -1
  28. package/dist/monorepo/packs/index.js +4 -1
  29. package/dist/monorepo/publish-workflow.d.ts +86 -0
  30. package/dist/monorepo/publish-workflow.d.ts.map +1 -0
  31. package/dist/monorepo/publish-workflow.js +343 -0
  32. package/dist/nx-version-actions.cjs +25 -0
  33. package/dist/nx-version-actions.d.cts +6 -0
  34. package/dist/nx-version-actions.d.cts.map +1 -0
  35. package/dist/release/core.d.ts +50 -0
  36. package/dist/release/core.d.ts.map +1 -0
  37. package/dist/release/core.js +97 -0
  38. package/dist/release/github-release.d.ts +46 -0
  39. package/dist/release/github-release.d.ts.map +1 -0
  40. package/dist/release/github-release.js +97 -0
  41. package/dist/release/index.d.ts +14 -5
  42. package/dist/release/index.d.ts.map +1 -1
  43. package/dist/release/index.js +544 -109
  44. package/dist/release/npm-auth.d.ts +17 -0
  45. package/dist/release/npm-auth.d.ts.map +1 -0
  46. package/dist/release/npm-auth.js +51 -0
  47. package/dist/release/orchestration.d.ts +48 -0
  48. package/dist/release/orchestration.d.ts.map +1 -0
  49. package/dist/release/orchestration.js +105 -0
  50. package/dist/release/publish-plan.d.ts +17 -0
  51. package/dist/release/publish-plan.d.ts.map +1 -0
  52. package/dist/release/publish-plan.js +15 -0
  53. package/dist/release/retag-unpublished.d.ts +34 -0
  54. package/dist/release/retag-unpublished.d.ts.map +1 -0
  55. package/dist/release/retag-unpublished.js +77 -0
  56. package/managed/raw/tooling/git-hooks/pre-commit.sh +2 -7
  57. package/managed/templates/github/workflows/ci.yml +2 -0
  58. package/package.json +24 -3
  59. package/src/cli.ts +64 -25
  60. package/src/lib/devenv.test.ts +28 -0
  61. package/src/lib/devenv.ts +89 -0
  62. package/src/lib/workspace.ts +76 -5
  63. package/src/monorepo/__tests__/nx-version-actions.test.ts +75 -0
  64. package/src/monorepo/__tests__/publish-workflow.test.ts +315 -0
  65. package/src/monorepo/index.ts +45 -5
  66. package/src/monorepo/lockfile.ts +52 -7
  67. package/src/monorepo/managed-files.ts +42 -12
  68. package/src/monorepo/package-hygiene.ts +1 -1
  69. package/src/monorepo/package-policy.ts +134 -18
  70. package/src/monorepo/packed-manifest.ts +67 -0
  71. package/src/monorepo/packed-package.ts +7 -0
  72. package/src/monorepo/packs/index.ts +4 -0
  73. package/src/monorepo/publish-workflow.ts +430 -0
  74. package/src/nx-version-actions.cts +36 -0
  75. package/src/release/__tests__/core-properties.test.ts +149 -0
  76. package/src/release/__tests__/core-scenarios.test.ts +177 -0
  77. package/src/release/__tests__/core.test.ts +73 -0
  78. package/src/release/__tests__/fixture-repo.test.ts +305 -0
  79. package/src/release/__tests__/github-release.test.ts +144 -0
  80. package/src/release/__tests__/helpers/fixture-repo.ts +113 -0
  81. package/src/release/__tests__/npm-auth.test.ts +129 -0
  82. package/src/release/__tests__/orchestration.test.ts +202 -0
  83. package/src/release/__tests__/publish-plan.test.ts +61 -0
  84. package/src/release/__tests__/retag-unpublished.test.ts +160 -0
  85. package/src/release/core.ts +170 -0
  86. package/src/release/github-release.ts +134 -0
  87. package/src/release/index.ts +662 -116
  88. package/src/release/npm-auth.ts +122 -0
  89. package/src/release/orchestration.ts +186 -0
  90. package/src/release/publish-plan.ts +37 -0
  91. package/src/release/retag-unpublished.ts +122 -0
  92. package/managed/templates/github/workflows/publish.yml +0 -136
@@ -2,7 +2,12 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
4
4
 
5
- export function syncBunLockfileVersions(root: string): void {
5
+ export interface SyncBunLockfileVersionsOptions {
6
+ log?: boolean;
7
+ }
8
+
9
+ export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVersionsOptions = {}): number {
10
+ const log = options.log ?? true;
6
11
  const lockfilePath = join(root, 'bun.lock');
7
12
  if (!existsSync(lockfilePath)) {
8
13
  throw new Error('bun.lock not found');
@@ -16,22 +21,62 @@ export function syncBunLockfileVersions(root: string): void {
16
21
  const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
17
22
  const match = lockfile.match(pattern);
18
23
  if (!match) {
19
- console.log(`skip: ${relativePath} (not found in lockfile)`);
24
+ if (log) {
25
+ console.log(`skip: ${relativePath} (not found in lockfile)`);
26
+ }
20
27
  continue;
21
28
  }
22
29
  const lockVersion = match[2];
23
30
  if (lockVersion === pkg.version) {
24
- console.log(`ok: ${relativePath} = ${pkg.version}`);
31
+ if (log) {
32
+ console.log(`ok: ${relativePath} = ${pkg.version}`);
33
+ }
25
34
  continue;
26
35
  }
27
36
  lockfile = lockfile.replace(pattern, `$1${pkg.version}$3`);
28
- console.log(`fix: ${relativePath}: ${lockVersion} -> ${pkg.version}`);
37
+ if (log) {
38
+ console.log(`fix: ${relativePath}: ${lockVersion} -> ${pkg.version}`);
39
+ }
29
40
  updated++;
30
41
  }
31
42
  if (updated > 0) {
32
43
  writeFileSync(lockfilePath, lockfile);
33
44
  }
34
- console.log(
35
- updated > 0 ? `Updated ${updated} workspace version(s) in bun.lock` : 'All workspace versions already in sync.',
36
- );
45
+ if (log) {
46
+ console.log(
47
+ updated > 0 ? `Updated ${updated} workspace version(s) in bun.lock` : 'All workspace versions already in sync.',
48
+ );
49
+ }
50
+ return updated;
51
+ }
52
+
53
+ export function validateBunLockfileVersions(root: string): number {
54
+ const lockfilePath = join(root, 'bun.lock');
55
+ if (!existsSync(lockfilePath)) {
56
+ console.error('bun.lock not found');
57
+ return 1;
58
+ }
59
+ const packages = getWorkspacePackages(root);
60
+ const lockfile = readFileSync(lockfilePath, 'utf8');
61
+ let failures = 0;
62
+ for (const pkg of packages) {
63
+ const relativePath = pkg.path.replaceAll('\\', '/');
64
+ const escaped = escapeRegex(relativePath);
65
+ const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
66
+ const match = lockfile.match(pattern);
67
+ if (!match) {
68
+ console.error(`bun.lock missing workspace entry for ${relativePath}`);
69
+ failures++;
70
+ continue;
71
+ }
72
+ const lockVersion = match[2];
73
+ if (lockVersion !== pkg.version) {
74
+ console.error(`${relativePath}: bun.lock workspace version must be ${pkg.version}, got ${lockVersion}`);
75
+ failures++;
76
+ }
77
+ }
78
+ if (failures === 0) {
79
+ console.log('bun.lock workspace versions are valid.');
80
+ }
81
+ return failures;
37
82
  }
@@ -1,20 +1,28 @@
1
1
  import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { readPackageJson } from '../lib/workspace.js';
4
+ import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
5
+ import { renderPublishWorkflowYaml } from './publish-workflow.js';
5
6
 
6
- type ManagedKind = 'raw' | 'template';
7
+ type ManagedKind = 'raw' | 'template' | 'generated';
7
8
 
8
9
  interface ManagedFile {
9
10
  kind: ManagedKind;
10
11
  source: string;
11
12
  target: string;
12
13
  executable?: boolean;
14
+ releasePackagesOnly?: boolean;
13
15
  }
14
16
 
15
17
  export interface FileResult {
16
18
  target: string;
17
- action: 'created' | 'updated' | 'unchanged' | 'skipped-symlink' | 'drifted' | 'ok-symlink';
19
+ action: 'created' | 'updated' | 'unchanged' | 'skipped' | 'skipped-symlink' | 'drifted' | 'ok-symlink';
20
+ }
21
+
22
+ interface ManagedFileContext {
23
+ hasReleasePackages: boolean;
24
+ nodeModulesCacheKey: string;
25
+ repoName: string;
18
26
  }
19
27
 
20
28
  const managedFiles: ManagedFile[] = [
@@ -47,9 +55,10 @@ const managedFiles: ManagedFile[] = [
47
55
  target: '.github/workflows/ci.yml',
48
56
  },
49
57
  {
50
- kind: 'template',
51
- source: 'github/workflows/publish.yml',
58
+ kind: 'generated',
59
+ source: 'publish-workflow',
52
60
  target: '.github/workflows/publish.yml',
61
+ releasePackagesOnly: true,
53
62
  },
54
63
  {
55
64
  kind: 'template',
@@ -81,12 +90,21 @@ const managedFiles: ManagedFile[] = [
81
90
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
82
91
 
83
92
  export function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): FileResult[] {
84
- return managedFiles.map((file) => applyManagedFile(root, file, mode));
93
+ const context = getManagedFileContext(root);
94
+ return managedFiles.map((file) => applyManagedFile(root, file, mode, context));
85
95
  }
86
96
 
87
- function applyManagedFile(root: string, file: ManagedFile, mode: 'update' | 'check' | 'diff'): FileResult {
97
+ function applyManagedFile(
98
+ root: string,
99
+ file: ManagedFile,
100
+ mode: 'update' | 'check' | 'diff',
101
+ context: ManagedFileContext,
102
+ ): FileResult {
103
+ if (file.releasePackagesOnly === true && !context.hasReleasePackages) {
104
+ return { target: file.target, action: 'skipped' };
105
+ }
88
106
  const target = resolve(root, file.target);
89
- const content = getManagedContent(root, file);
107
+ const content = getManagedContent(file, context);
90
108
  if (existsSync(target)) {
91
109
  const info = lstatSync(target);
92
110
  if (info.isSymbolicLink()) {
@@ -112,23 +130,35 @@ function applyManagedFile(root: string, file: ManagedFile, mode: 'update' | 'che
112
130
  return { target: file.target, action: 'created' };
113
131
  }
114
132
 
115
- function getManagedContent(root: string, file: ManagedFile): string {
133
+ function getManagedContent(file: ManagedFile, context: ManagedFileContext): string {
134
+ if (file.kind === 'generated') {
135
+ if (file.source === 'publish-workflow') {
136
+ return renderPublishWorkflowYaml({ repoName: context.repoName });
137
+ }
138
+ throw new Error(`Unknown generated managed file source ${file.source}`);
139
+ }
116
140
  const sourceRoot = file.kind === 'raw' ? 'managed/raw' : 'managed/templates';
117
141
  const sourcePath = join(packageRoot, sourceRoot, file.source);
118
142
  const content = readFileSync(sourcePath, 'utf8');
119
143
  if (file.kind === 'raw') {
120
144
  return content;
121
145
  }
122
- return renderTemplate(root, content);
146
+ return renderTemplate(context, content);
123
147
  }
124
148
 
125
- function renderTemplate(root: string, template: string): string {
149
+ function getManagedFileContext(root: string): ManagedFileContext {
126
150
  const packageJson = readPackageJson(join(root, 'package.json'));
127
151
  const repoName = packageJson?.name ?? 'monorepo';
128
152
  const nodeModulesCacheKey = existsSync(join(root, 'bun.lock'))
129
153
  ? `$${"{{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}"}`
130
154
  : `$${"{{ hashFiles('bun.lockb', 'package.json', 'packages/*/package.json') }}"}`;
131
- return template.replaceAll('{{REPO_NAME}}', repoName).replaceAll('{{NODE_MODULES_CACHE_KEY}}', nodeModulesCacheKey);
155
+ return { hasReleasePackages: listReleasePackages(root, packageJson).length > 0, nodeModulesCacheKey, repoName };
156
+ }
157
+
158
+ function renderTemplate(context: ManagedFileContext, template: string): string {
159
+ return template
160
+ .replaceAll('{{REPO_NAME}}', context.repoName)
161
+ .replaceAll('{{NODE_MODULES_CACHE_KEY}}', context.nodeModulesCacheKey);
132
162
  }
133
163
 
134
164
  function writeManagedFile(path: string, content: string, executable: boolean): void {
@@ -5,7 +5,7 @@ export async function fixPackageHygiene(root: string): Promise<void> {
5
5
  }
6
6
 
7
7
  export async function validatePackageHygiene(root: string): Promise<number> {
8
- const status = await runStatus('sherif', [], root);
8
+ const status = await runStatus('sherif', ['--fail-on-warnings'], root);
9
9
  if (status !== 0) {
10
10
  console.error('sherif package hygiene validation failed');
11
11
  return 1;
@@ -15,30 +15,41 @@ import {
15
15
  getWorkspacePackages,
16
16
  listPackageJsonRecords,
17
17
  listPublicPackages,
18
+ packageRepositoryInfo,
18
19
  repositoryInfo,
20
+ sameRepositoryAfterNormalization,
19
21
  workspaceDependencyFields,
20
22
  } from '../lib/workspace.js';
21
23
 
24
+ export const SMOO_NX_VERSION_ACTIONS = '@smoothbricks/cli/nx-version-actions';
25
+ export const SMOO_NX_RELEASE_TAG_PATTERN = '{projectName}@{version}';
26
+
22
27
  export function applyPublicPackageDefaults(root: string): void {
23
28
  const rootPackage = requiredJsonObject(join(root, 'package.json'));
24
29
  const rootLicense = stringProperty(rootPackage, 'license');
25
30
  const rootRepository = repositoryInfo(rootPackage);
26
- if (!rootLicense) {
27
- throw new Error('Root package.json must define license before public package defaults can be applied.');
28
- }
29
- if (!rootRepository) {
30
- throw new Error('Root package.json must define repository.url before public package defaults can be applied.');
31
- }
32
31
 
33
32
  for (const pkg of listPublicPackages(root)) {
34
33
  let changed = false;
35
- changed = setMissingStringProperty(pkg.json, 'license', rootLicense) || changed;
34
+ const existingRepository = packageRepositoryInfo(pkg);
35
+ if (
36
+ existingRepository &&
37
+ rootRepository &&
38
+ existingRepository.url === rootRepository.url &&
39
+ rootLicense &&
40
+ rootLicense !== 'UNLICENSED'
41
+ ) {
42
+ changed = setMissingStringProperty(pkg.json, 'license', rootLicense) || changed;
43
+ }
36
44
  const publishConfig = getOrCreateRecord(pkg.json, 'publishConfig');
37
45
  changed = setStringProperty(publishConfig, 'access', 'public') || changed;
38
46
 
39
47
  const repository = getOrCreateRecord(pkg.json, 'repository');
40
- changed = setStringProperty(repository, 'type', rootRepository.type) || changed;
41
- changed = setStringProperty(repository, 'url', rootRepository.url) || changed;
48
+ changed =
49
+ setStringProperty(repository, 'type', existingRepository?.type ?? rootRepository?.type ?? 'git') || changed;
50
+ if (existingRepository && !stringProperty(repository, 'url')) {
51
+ changed = setStringProperty(repository, 'url', existingRepository.url) || changed;
52
+ }
42
53
  changed = setStringProperty(repository, 'directory', pkg.path.replaceAll('\\', '/')) || changed;
43
54
  changed = normalizeExportConditionOrder(pkg.json.exports) || changed;
44
55
  if (hasDevelopmentSourceExport(pkg.json.exports)) {
@@ -67,6 +78,45 @@ export function applyWorkspaceDependencyDefaults(root: string): void {
67
78
  }
68
79
  }
69
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;
94
+ }
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;
106
+ }
107
+ if (typeof renderOptions.applyUsernameToAuthors !== 'boolean') {
108
+ renderOptions.applyUsernameToAuthors = true;
109
+ changed = true;
110
+ }
111
+
112
+ if (changed) {
113
+ writeJsonObject(nxJsonPath, nxJson);
114
+ console.log('updated nx.json release config');
115
+ } else {
116
+ console.log('unchanged nx.json release config');
117
+ }
118
+ }
119
+
70
120
  export function validateRootPackagePolicy(root: string): number {
71
121
  const rootPackage = readJsonObject(join(root, 'package.json'));
72
122
  if (!rootPackage) {
@@ -113,6 +163,10 @@ export function validateNxReleaseConfig(root: string): number {
113
163
  }
114
164
  const release = recordProperty(nxJson, 'release');
115
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;
116
170
  let failures = 0;
117
171
  if (!release) {
118
172
  console.error('nx.json release config is missing');
@@ -140,8 +194,46 @@ export function validateNxReleaseConfig(root: string): number {
140
194
  console.error('nx.json release.version.fallbackCurrentVersionResolver must be disk');
141
195
  failures++;
142
196
  }
143
- if (version && !stringProperty(version, 'preVersionCommand')) {
144
- console.error('nx.json release.version.preVersionCommand must be defined');
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++;
230
+ }
231
+ if (projectChangelogs && projectChangelogs.file !== false) {
232
+ console.error('nx.json release.changelog.projectChangelogs.file must be false');
233
+ failures++;
234
+ }
235
+ if (!renderOptions) {
236
+ console.error('nx.json release.changelog.projectChangelogs.renderOptions config is missing');
145
237
  failures++;
146
238
  }
147
239
  return failures;
@@ -155,10 +247,6 @@ export function validatePublicTags(root: string): number {
155
247
  console.error(`${pkg.path}: private package must not have nx tag npm:public`);
156
248
  failures++;
157
249
  }
158
- if (!pkg.private && !hasPublicTag) {
159
- console.error(`${pkg.path}: public package must have nx tag npm:public`);
160
- failures++;
161
- }
162
250
  }
163
251
  if (failures > 0) {
164
252
  return failures;
@@ -172,6 +260,10 @@ export function validatePublicPackageMetadata(root: string): number {
172
260
  const rootRepository = rootPackage ? repositoryInfo(rootPackage) : null;
173
261
  let failures = 0;
174
262
  for (const pkg of listPublicPackages(root)) {
263
+ if (pkg.private) {
264
+ console.error(`${pkg.path}: npm:public package must not be private`);
265
+ failures++;
266
+ }
175
267
  if (!stringProperty(pkg.json, 'license')) {
176
268
  console.error(`${pkg.path}: public package must define license`);
177
269
  failures++;
@@ -182,9 +274,25 @@ export function validatePublicPackageMetadata(root: string): number {
182
274
  failures++;
183
275
  }
184
276
  const repository = recordProperty(pkg.json, 'repository');
185
- const packageRepository = repository ? repositoryInfo(pkg.json) : null;
186
- if (!rootRepository || !packageRepository || packageRepository.url !== rootRepository.url) {
187
- console.error(`${pkg.path}: public package repository.url must match root package.json repository.url`);
277
+ const packageRepository = packageRepositoryInfo(pkg);
278
+ if (!packageRepository) {
279
+ console.error(`${pkg.path}: public package must define repository.url`);
280
+ failures++;
281
+ }
282
+ if (
283
+ rootRepository &&
284
+ packageRepository &&
285
+ packageRepository.url !== rootRepository.url &&
286
+ sameRepositoryAfterNormalization(packageRepository.url, rootRepository.url)
287
+ ) {
288
+ console.error(
289
+ `${pkg.path}: repository.url refers to the root repository but is not an exact match. ` +
290
+ `Use ${rootRepository.url}`,
291
+ );
292
+ failures++;
293
+ }
294
+ if (!repository || !stringProperty(repository, 'type')) {
295
+ console.error(`${pkg.path}: public package must define repository.type`);
188
296
  failures++;
189
297
  }
190
298
  if (!repository || stringProperty(repository, 'directory') !== pkg.path.replaceAll('\\', '/')) {
@@ -247,6 +355,14 @@ function fixWorkspaceDependencyRanges(pkg: Record<string, unknown>, workspaceNam
247
355
  return changed;
248
356
  }
249
357
 
358
+ function setBooleanProperty(record: Record<string, unknown>, key: string, value: boolean): boolean {
359
+ if (record[key] === value) {
360
+ return false;
361
+ }
362
+ record[key] = value;
363
+ return true;
364
+ }
365
+
250
366
  function normalizeExportConditionOrder(value: unknown): boolean {
251
367
  if (!isRecord(value)) {
252
368
  return false;
@@ -0,0 +1,67 @@
1
+ import { $ } from 'bun';
2
+ import { isRecord, recordProperty } from '../lib/json.js';
3
+ import { decode } from '../lib/run.js';
4
+ import type { PackageInfo } from '../lib/workspace.js';
5
+ import { getWorkspacePackages, workspaceDependencyFields } from '../lib/workspace.js';
6
+
7
+ export async function readPackedPackageJson(
8
+ root: string,
9
+ tarball: string,
10
+ packageName: string,
11
+ ): Promise<Record<string, unknown>> {
12
+ const result = await $`tar -xOf ${tarball} package/package.json`.cwd(root).quiet().nothrow();
13
+ if (result.exitCode !== 0) {
14
+ throw new Error(`${packageName}: unable to inspect packed package.json.`);
15
+ }
16
+ const parsed = JSON.parse(decode(result.stdout));
17
+ if (!isRecord(parsed)) {
18
+ throw new Error(`${packageName}: packed package.json is not an object.`);
19
+ }
20
+ return parsed;
21
+ }
22
+
23
+ export function validatePackedWorkspaceDependencies(
24
+ root: string,
25
+ sourcePackage: PackageInfo,
26
+ packedPackage: Record<string, unknown>,
27
+ ): string[] {
28
+ const workspaceVersions = new Map(getWorkspacePackages(root).map((pkg) => [pkg.name, pkg.version]));
29
+ const failures: string[] = [];
30
+ for (const field of workspaceDependencyFields) {
31
+ const sourceDependencies = recordProperty(sourcePackage.json, field);
32
+ const packedDependencies = recordProperty(packedPackage, field);
33
+ if (!sourceDependencies && !packedDependencies) {
34
+ continue;
35
+ }
36
+
37
+ for (const [name, range] of Object.entries(packedDependencies ?? {})) {
38
+ if (typeof range === 'string' && range.startsWith('workspace:')) {
39
+ failures.push(`${sourcePackage.path}: packed ${field}.${name} must not contain ${range}`);
40
+ }
41
+ }
42
+
43
+ if (!sourceDependencies) {
44
+ continue;
45
+ }
46
+ for (const [name, sourceRange] of Object.entries(sourceDependencies)) {
47
+ const workspaceVersion = workspaceVersions.get(name);
48
+ if (!workspaceVersion) {
49
+ continue;
50
+ }
51
+ const packedRange = packedDependencies?.[name];
52
+ if (sourceRange !== 'workspace:*') {
53
+ failures.push(`${sourcePackage.path}: source ${field}.${name} must use workspace:*`);
54
+ }
55
+ if (packedRange !== workspaceVersion) {
56
+ failures.push(
57
+ `${sourcePackage.path}: packed ${field}.${name} must be ${workspaceVersion}, got ${formatRange(packedRange)}`,
58
+ );
59
+ }
60
+ }
61
+ }
62
+ return failures;
63
+ }
64
+
65
+ function formatRange(value: unknown): string {
66
+ return typeof value === 'string' ? value : '<missing>';
67
+ }
@@ -6,6 +6,7 @@ import { isRecord } from '../lib/json.js';
6
6
  import { runStatus } from '../lib/run.js';
7
7
  import type { PackageInfo } from '../lib/workspace.js';
8
8
  import { listPublicPackages } from '../lib/workspace.js';
9
+ import { readPackedPackageJson, validatePackedWorkspaceDependencies } from './packed-manifest.js';
9
10
 
10
11
  export async function validatePackedPublicPackages(root: string): Promise<number> {
11
12
  let failures = 0;
@@ -25,6 +26,12 @@ async function validatePackedPublicPackage(root: string, pkg: PackageInfo): Prom
25
26
  failures++;
26
27
  }
27
28
 
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
+ }
34
+
28
35
  const attwArgs = [
29
36
  packed.path,
30
37
  '--format',
@@ -1,9 +1,11 @@
1
1
  import { chmodSync, existsSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { validateBunLockfileVersions } from '../lockfile.js';
3
4
  import { validateManagedFiles } from '../managed-files.js';
4
5
  import { validateNxSync } from '../nx-sync.js';
5
6
  import { fixPackageHygiene, validatePackageHygiene } from '../package-hygiene.js';
6
7
  import {
8
+ applyNxReleaseDefaults,
7
9
  applyPublicPackageDefaults,
8
10
  applyWorkspaceDependencyDefaults,
9
11
  validateNxReleaseConfig,
@@ -35,6 +37,7 @@ const packs: MonorepoPack[] = [
35
37
  name: 'core',
36
38
  async init(ctx) {
37
39
  ensureLocalSmooShim(ctx.root);
40
+ applyNxReleaseDefaults(ctx.root);
38
41
  if (ctx.syncRuntime) {
39
42
  await syncRootRuntimeVersions(ctx.root);
40
43
  } else {
@@ -46,6 +49,7 @@ const packs: MonorepoPack[] = [
46
49
  validateManagedFiles(ctx.root) +
47
50
  validateRootPackagePolicy(ctx.root) +
48
51
  validateNxReleaseConfig(ctx.root) +
52
+ validateBunLockfileVersions(ctx.root) +
49
53
  (await validateNxSync(ctx.root))
50
54
  );
51
55
  },