@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.
- package/README.md +208 -83
- package/dist/cli.js +50 -20
- package/dist/lib/devenv.d.ts +6 -0
- package/dist/lib/devenv.d.ts.map +1 -0
- package/dist/lib/devenv.js +82 -0
- package/dist/lib/workspace.d.ts +5 -0
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +62 -5
- package/dist/monorepo/index.d.ts +6 -1
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +38 -5
- package/dist/monorepo/lockfile.d.ts +5 -1
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +45 -5
- package/dist/monorepo/managed-files.d.ts +1 -1
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +27 -10
- package/dist/monorepo/package-hygiene.js +1 -1
- package/dist/monorepo/package-policy.d.ts +3 -0
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +122 -19
- package/dist/monorepo/packed-manifest.d.ts +4 -0
- package/dist/monorepo/packed-manifest.d.ts.map +1 -0
- package/dist/monorepo/packed-manifest.js +51 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +6 -0
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +4 -1
- package/dist/monorepo/publish-workflow.d.ts +86 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -0
- package/dist/monorepo/publish-workflow.js +343 -0
- package/dist/nx-version-actions.cjs +25 -0
- package/dist/nx-version-actions.d.cts +6 -0
- package/dist/nx-version-actions.d.cts.map +1 -0
- package/dist/release/core.d.ts +50 -0
- package/dist/release/core.d.ts.map +1 -0
- package/dist/release/core.js +97 -0
- package/dist/release/github-release.d.ts +46 -0
- package/dist/release/github-release.d.ts.map +1 -0
- package/dist/release/github-release.js +97 -0
- package/dist/release/index.d.ts +14 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +544 -109
- package/dist/release/npm-auth.d.ts +17 -0
- package/dist/release/npm-auth.d.ts.map +1 -0
- package/dist/release/npm-auth.js +51 -0
- package/dist/release/orchestration.d.ts +48 -0
- package/dist/release/orchestration.d.ts.map +1 -0
- package/dist/release/orchestration.js +105 -0
- package/dist/release/publish-plan.d.ts +17 -0
- package/dist/release/publish-plan.d.ts.map +1 -0
- package/dist/release/publish-plan.js +15 -0
- package/dist/release/retag-unpublished.d.ts +34 -0
- package/dist/release/retag-unpublished.d.ts.map +1 -0
- package/dist/release/retag-unpublished.js +77 -0
- package/managed/raw/tooling/git-hooks/pre-commit.sh +2 -7
- package/managed/templates/github/workflows/ci.yml +2 -0
- package/package.json +24 -3
- package/src/cli.ts +64 -25
- package/src/lib/devenv.test.ts +28 -0
- package/src/lib/devenv.ts +89 -0
- package/src/lib/workspace.ts +76 -5
- package/src/monorepo/__tests__/nx-version-actions.test.ts +75 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +315 -0
- package/src/monorepo/index.ts +45 -5
- package/src/monorepo/lockfile.ts +52 -7
- package/src/monorepo/managed-files.ts +42 -12
- package/src/monorepo/package-hygiene.ts +1 -1
- package/src/monorepo/package-policy.ts +134 -18
- package/src/monorepo/packed-manifest.ts +67 -0
- package/src/monorepo/packed-package.ts +7 -0
- package/src/monorepo/packs/index.ts +4 -0
- package/src/monorepo/publish-workflow.ts +430 -0
- package/src/nx-version-actions.cts +36 -0
- package/src/release/__tests__/core-properties.test.ts +149 -0
- package/src/release/__tests__/core-scenarios.test.ts +177 -0
- package/src/release/__tests__/core.test.ts +73 -0
- package/src/release/__tests__/fixture-repo.test.ts +305 -0
- package/src/release/__tests__/github-release.test.ts +144 -0
- package/src/release/__tests__/helpers/fixture-repo.ts +113 -0
- package/src/release/__tests__/npm-auth.test.ts +129 -0
- package/src/release/__tests__/orchestration.test.ts +202 -0
- package/src/release/__tests__/publish-plan.test.ts +61 -0
- package/src/release/__tests__/retag-unpublished.test.ts +160 -0
- package/src/release/core.ts +170 -0
- package/src/release/github-release.ts +134 -0
- package/src/release/index.ts +662 -116
- package/src/release/npm-auth.ts +122 -0
- package/src/release/orchestration.ts +186 -0
- package/src/release/publish-plan.ts +37 -0
- package/src/release/retag-unpublished.ts +122 -0
- package/managed/templates/github/workflows/publish.yml +0 -136
package/src/monorepo/lockfile.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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: '
|
|
51
|
-
source: '
|
|
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
|
-
|
|
93
|
+
const context = getManagedFileContext(root);
|
|
94
|
+
return managedFiles.map((file) => applyManagedFile(root, file, mode, context));
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
function applyManagedFile(
|
|
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(
|
|
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(
|
|
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(
|
|
146
|
+
return renderTemplate(context, content);
|
|
123
147
|
}
|
|
124
148
|
|
|
125
|
-
function
|
|
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
|
|
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
|
-
|
|
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 =
|
|
41
|
-
|
|
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 &&
|
|
144
|
-
console.error(
|
|
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 =
|
|
186
|
-
if (!
|
|
187
|
-
console.error(`${pkg.path}: public package
|
|
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
|
},
|