@smoothbricks/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +338 -88
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +136 -22
- package/dist/devenv/index.d.ts +10 -0
- package/dist/devenv/index.d.ts.map +1 -0
- package/dist/devenv/index.js +27 -0
- package/dist/lib/cli-package.d.ts +4 -0
- package/dist/lib/cli-package.d.ts.map +1 -0
- package/dist/lib/cli-package.js +15 -0
- 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/run.d.ts +5 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +13 -0
- package/dist/lib/workspace.d.ts +20 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +87 -7
- package/dist/monorepo/commit-msg.d.ts +8 -1
- package/dist/monorepo/commit-msg.d.ts.map +1 -1
- package/dist/monorepo/commit-msg.js +85 -7
- package/dist/monorepo/index.d.ts +24 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +136 -15
- 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 +28 -11
- package/dist/monorepo/nx-sync.d.ts +2 -1
- package/dist/monorepo/nx-sync.d.ts.map +1 -1
- package/dist/monorepo/nx-sync.js +8 -2
- package/dist/monorepo/package-hygiene.d.ts +6 -2
- package/dist/monorepo/package-hygiene.d.ts.map +1 -1
- package/dist/monorepo/package-hygiene.js +12 -4
- package/dist/monorepo/package-policy.d.ts +25 -2
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +1289 -27
- 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 +3 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +69 -31
- package/dist/monorepo/packs/index.d.ts +20 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +240 -18
- package/dist/monorepo/publish-workflow.d.ts +82 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -0
- package/dist/monorepo/publish-workflow.js +336 -0
- package/dist/monorepo/tool-validation.d.ts +22 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -0
- package/dist/monorepo/tool-validation.js +399 -0
- package/dist/nx/index.d.ts +24 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +94 -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/bootstrap-npm-packages.d.ts +19 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
- package/dist/release/bootstrap-npm-packages.js +58 -0
- package/dist/release/candidates.d.ts +11 -0
- package/dist/release/candidates.d.ts.map +1 -0
- package/dist/release/candidates.js +181 -0
- package/dist/release/core.d.ts +51 -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 +39 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +763 -130
- package/dist/release/npm-auth.d.ts +16 -0
- package/dist/release/npm-auth.d.ts.map +1 -0
- package/dist/release/npm-auth.js +39 -0
- package/dist/release/orchestration.d.ts +49 -0
- package/dist/release/orchestration.d.ts.map +1 -0
- package/dist/release/orchestration.js +113 -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/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/raw/tooling/git-hooks/pre-commit.sh +2 -7
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +3 -0
- package/package.json +37 -4
- package/src/cli.ts +193 -40
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/devenv.test.ts +28 -0
- package/src/lib/devenv.ts +89 -0
- package/src/lib/run.ts +19 -0
- package/src/lib/workspace.ts +117 -7
- package/src/monorepo/__tests__/nx-version-actions.test.ts +75 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +322 -0
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +181 -15
- package/src/monorepo/lockfile.ts +52 -7
- package/src/monorepo/managed-files.ts +43 -13
- package/src/monorepo/nx-sync.ts +9 -2
- package/src/monorepo/package-hygiene.test.ts +34 -0
- package/src/monorepo/package-hygiene.ts +26 -4
- package/src/monorepo/package-policy.test.ts +902 -0
- package/src/monorepo/package-policy.ts +1527 -25
- package/src/monorepo/packed-manifest.ts +67 -0
- package/src/monorepo/packed-package.ts +90 -31
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +314 -19
- package/src/monorepo/publish-workflow.ts +422 -0
- package/src/monorepo/setup-test-tracing.test.ts +120 -0
- package/src/monorepo/tool-validation.test.ts +261 -0
- package/src/monorepo/tool-validation.ts +462 -0
- package/src/nx/index.test.ts +64 -0
- package/src/nx/index.ts +119 -0
- package/src/nx-version-actions.cts +36 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +217 -0
- package/src/release/__tests__/core-properties.test.ts +149 -0
- package/src/release/__tests__/core-scenarios.test.ts +187 -0
- package/src/release/__tests__/core.test.ts +73 -0
- package/src/release/__tests__/fixture-repo.test.ts +314 -0
- package/src/release/__tests__/github-release.test.ts +149 -0
- package/src/release/__tests__/helpers/fixture-repo.ts +114 -0
- package/src/release/__tests__/npm-auth.test.ts +120 -0
- package/src/release/__tests__/orchestration.test.ts +246 -0
- package/src/release/__tests__/publish-plan.test.ts +67 -0
- package/src/release/__tests__/retag-unpublished.test.ts +160 -0
- package/src/release/__tests__/trust-publisher.test.ts +133 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +171 -0
- package/src/release/github-release.ts +134 -0
- package/src/release/index.ts +974 -138
- package/src/release/npm-auth.ts +93 -0
- package/src/release/orchestration.ts +196 -0
- package/src/release/publish-plan.ts +37 -0
- package/src/release/retag-unpublished.ts +122 -0
- package/managed/raw/.git-format-staged.yml +0 -47
- package/managed/templates/github/workflows/publish.yml +0 -136
|
@@ -1,24 +1,102 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { getOrCreateRecord, hasOwn, hasOwnString, isRecord, readJsonObject, recordProperty, requiredJsonObject, setMissingStringProperty, setStringProperty, stringProperty, writeJsonObject, } from '../lib/json.js';
|
|
4
|
+
import { getWorkspacePackages, listPackageJsonRecords, listPublicPackages, packageRepositoryInfo, repositoryInfo, sameRepositoryAfterNormalization, workspaceDependencyFields, } from '../lib/workspace.js';
|
|
5
|
+
export const SMOO_NX_VERSION_ACTIONS = '@smoothbricks/cli/nx-version-actions';
|
|
6
|
+
export const SMOO_NX_RELEASE_TAG_PATTERN = '{projectName}@{version}';
|
|
7
|
+
const extraCommitScopes = ['release'];
|
|
8
|
+
const rootScriptPolicy = {
|
|
9
|
+
lint: 'nx run-many -t lint',
|
|
10
|
+
'lint:fix': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --unstaged',
|
|
11
|
+
'format:staged': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml',
|
|
12
|
+
'format:changed': 'git-format-staged --config tooling/git-hooks/git-format-staged.yml --also-unstaged',
|
|
13
|
+
};
|
|
14
|
+
const nxJsTypescriptPlugin = '@nx/js/typescript';
|
|
15
|
+
const smoothBricksNxPlugin = '@smoothbricks/nx-plugin';
|
|
16
|
+
const expectedSharedGlobalsNamedInput = ['{workspaceRoot}/.github/workflows/ci.yml'];
|
|
17
|
+
const defaultProductionNamedInput = [
|
|
18
|
+
'{projectRoot}/src/**/*',
|
|
19
|
+
'{projectRoot}/package.json',
|
|
20
|
+
'!{projectRoot}/**/__tests__/**',
|
|
21
|
+
'!{projectRoot}/**/*.test.*',
|
|
22
|
+
'!{projectRoot}/**/*.spec.*',
|
|
23
|
+
];
|
|
24
|
+
const impreciseProductionInputs = new Set(['default', '{projectRoot}/**/*', '{projectRoot}/**']);
|
|
25
|
+
export function applyFixableMonorepoDefaults(root) {
|
|
26
|
+
applyRootScriptDefaults(root);
|
|
27
|
+
applyNxPluginDefaults(root);
|
|
28
|
+
applyNxProjectNameDefaults(root);
|
|
29
|
+
}
|
|
30
|
+
export function applyRootScriptDefaults(root) {
|
|
31
|
+
const rootPackagePath = join(root, 'package.json');
|
|
32
|
+
const rootPackage = readJsonObject(rootPackagePath);
|
|
33
|
+
if (!rootPackage) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const scripts = getOrCreateRecord(rootPackage, 'scripts');
|
|
37
|
+
let changed = false;
|
|
38
|
+
for (const [name, command] of Object.entries(rootScriptPolicy)) {
|
|
39
|
+
changed = setStringProperty(scripts, name, command) || changed;
|
|
40
|
+
}
|
|
41
|
+
const nx = getOrCreateRecord(rootPackage, 'nx');
|
|
42
|
+
if (!Array.isArray(nx.includedScripts) || nx.includedScripts.length !== 0) {
|
|
43
|
+
nx.includedScripts = [];
|
|
44
|
+
changed = true;
|
|
45
|
+
}
|
|
46
|
+
changed = sortRecordInPlace(scripts) || changed;
|
|
47
|
+
if (changed) {
|
|
48
|
+
writeJsonObject(rootPackagePath, rootPackage);
|
|
49
|
+
console.log('updated package.json root smoo scripts');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log('unchanged package.json root smoo scripts');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function applyNxPluginDefaults(root) {
|
|
56
|
+
const nxJsonPath = join(root, 'nx.json');
|
|
57
|
+
const nxJson = readJsonObject(nxJsonPath);
|
|
58
|
+
if (!nxJson) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let changed = removeColonTargetDefaults(nxJson);
|
|
62
|
+
changed = applyBuildTargetDefault(nxJson) || changed;
|
|
63
|
+
changed = applyNamedInputDefaults(nxJson) || changed;
|
|
64
|
+
const currentPlugins = Array.isArray(nxJson.plugins) ? nxJson.plugins : [];
|
|
65
|
+
const nextPlugins = upsertNxPlugin(upsertNxPlugin(currentPlugins, expectedNxJsTypescriptPlugin()), smoothBricksNxPlugin);
|
|
66
|
+
if (JSON.stringify(currentPlugins) !== JSON.stringify(nextPlugins)) {
|
|
67
|
+
nxJson.plugins = nextPlugins;
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
if (changed) {
|
|
71
|
+
writeJsonObject(nxJsonPath, nxJson);
|
|
72
|
+
console.log('updated nx.json smoo plugin config');
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log('unchanged nx.json smoo plugin config');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
4
78
|
export function applyPublicPackageDefaults(root) {
|
|
5
79
|
const rootPackage = requiredJsonObject(join(root, 'package.json'));
|
|
6
80
|
const rootLicense = stringProperty(rootPackage, 'license');
|
|
7
81
|
const rootRepository = repositoryInfo(rootPackage);
|
|
8
|
-
if (!rootLicense) {
|
|
9
|
-
throw new Error('Root package.json must define license before public package defaults can be applied.');
|
|
10
|
-
}
|
|
11
|
-
if (!rootRepository) {
|
|
12
|
-
throw new Error('Root package.json must define repository.url before public package defaults can be applied.');
|
|
13
|
-
}
|
|
14
82
|
for (const pkg of listPublicPackages(root)) {
|
|
15
83
|
let changed = false;
|
|
16
|
-
|
|
84
|
+
const existingRepository = packageRepositoryInfo(pkg);
|
|
85
|
+
if (existingRepository &&
|
|
86
|
+
rootRepository &&
|
|
87
|
+
existingRepository.url === rootRepository.url &&
|
|
88
|
+
rootLicense &&
|
|
89
|
+
rootLicense !== 'UNLICENSED') {
|
|
90
|
+
changed = setMissingStringProperty(pkg.json, 'license', rootLicense) || changed;
|
|
91
|
+
}
|
|
17
92
|
const publishConfig = getOrCreateRecord(pkg.json, 'publishConfig');
|
|
18
93
|
changed = setStringProperty(publishConfig, 'access', 'public') || changed;
|
|
19
94
|
const repository = getOrCreateRecord(pkg.json, 'repository');
|
|
20
|
-
changed =
|
|
21
|
-
|
|
95
|
+
changed =
|
|
96
|
+
setStringProperty(repository, 'type', existingRepository?.type ?? rootRepository?.type ?? 'git') || changed;
|
|
97
|
+
if (existingRepository && !stringProperty(repository, 'url')) {
|
|
98
|
+
changed = setStringProperty(repository, 'url', existingRepository.url) || changed;
|
|
99
|
+
}
|
|
22
100
|
changed = setStringProperty(repository, 'directory', pkg.path.replaceAll('\\', '/')) || changed;
|
|
23
101
|
changed = normalizeExportConditionOrder(pkg.json.exports) || changed;
|
|
24
102
|
if (hasDevelopmentSourceExport(pkg.json.exports)) {
|
|
@@ -33,19 +111,108 @@ export function applyPublicPackageDefaults(root) {
|
|
|
33
111
|
}
|
|
34
112
|
}
|
|
35
113
|
}
|
|
36
|
-
export function applyWorkspaceDependencyDefaults(root) {
|
|
114
|
+
export function applyWorkspaceDependencyDefaults(root, options = {}) {
|
|
37
115
|
const workspaceNames = new Set(getWorkspacePackages(root).map((pkg) => pkg.name));
|
|
38
116
|
for (const pkg of listPackageJsonRecords(root)) {
|
|
39
|
-
|
|
117
|
+
let changed = fixWorkspaceDependencyRanges(pkg.json, workspaceNames);
|
|
118
|
+
const projectName = packageNxProjectName(pkg.json);
|
|
119
|
+
const resolvedProject = projectName ? options.resolvedTargetsByProject?.get(projectName) : undefined;
|
|
120
|
+
const resolvedTargets = resolvedProjectTargetNames(resolvedProject);
|
|
121
|
+
changed = migratePackageColonTargets(pkg.json, resolvedTargets) || changed;
|
|
122
|
+
changed = rewriteColonTargetDependenciesInPackage(pkg.json, resolvedTargets) || changed;
|
|
123
|
+
changed = removePackageColonTargets(pkg.json) || changed;
|
|
124
|
+
changed = removeRedundantNoopBuildTarget(pkg.json, resolvedProject) || changed;
|
|
125
|
+
changed = applyPackageScriptPolicy(pkg.json, pkg.path, workspaceNames, { resolvedTargets }) || changed;
|
|
40
126
|
if (changed) {
|
|
41
127
|
writeJsonObject(pkg.packageJsonPath, pkg.json);
|
|
42
|
-
console.log(`updated ${pkg.path}/package.json workspace dependency
|
|
128
|
+
console.log(`updated ${pkg.path}/package.json workspace dependency policy`);
|
|
43
129
|
}
|
|
44
130
|
else {
|
|
45
|
-
console.log(`unchanged ${pkg.path}/package.json workspace dependency
|
|
131
|
+
console.log(`unchanged ${pkg.path}/package.json workspace dependency policy`);
|
|
46
132
|
}
|
|
133
|
+
applyBunTestTsconfigDefaults(root, pkg.path, pkg.json, workspaceNames);
|
|
134
|
+
applyTsconfigTestReferenceDefaults(root, pkg.path);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function applyNxReleaseDefaults(root) {
|
|
138
|
+
const nxJsonPath = join(root, 'nx.json');
|
|
139
|
+
const nxJson = requiredJsonObject(nxJsonPath);
|
|
140
|
+
let changed = false;
|
|
141
|
+
const release = getOrCreateRecord(nxJson, 'release');
|
|
142
|
+
changed = setStringProperty(release, 'projectsRelationship', 'independent') || changed;
|
|
143
|
+
const version = getOrCreateRecord(release, 'version');
|
|
144
|
+
changed = setStringProperty(version, 'specifierSource', 'conventional-commits') || changed;
|
|
145
|
+
changed = setStringProperty(version, 'currentVersionResolver', 'git-tag') || changed;
|
|
146
|
+
changed = setStringProperty(version, 'fallbackCurrentVersionResolver', 'disk') || changed;
|
|
147
|
+
changed = setStringProperty(version, 'versionActions', SMOO_NX_VERSION_ACTIONS) || changed;
|
|
148
|
+
if (delete version.preVersionCommand) {
|
|
149
|
+
changed = true;
|
|
150
|
+
}
|
|
151
|
+
const releaseTag = getOrCreateRecord(release, 'releaseTag');
|
|
152
|
+
changed = setStringProperty(releaseTag, 'pattern', SMOO_NX_RELEASE_TAG_PATTERN) || changed;
|
|
153
|
+
const changelog = getOrCreateRecord(release, 'changelog');
|
|
154
|
+
changed = setBooleanProperty(changelog, 'workspaceChangelog', false) || changed;
|
|
155
|
+
const projectChangelogs = getOrCreateRecord(changelog, 'projectChangelogs');
|
|
156
|
+
changed = setBooleanProperty(projectChangelogs, 'createRelease', false) || changed;
|
|
157
|
+
changed = setBooleanProperty(projectChangelogs, 'file', false) || changed;
|
|
158
|
+
const renderOptions = getOrCreateRecord(projectChangelogs, 'renderOptions');
|
|
159
|
+
if (typeof renderOptions.authors !== 'boolean') {
|
|
160
|
+
renderOptions.authors = true;
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
if (typeof renderOptions.applyUsernameToAuthors !== 'boolean') {
|
|
164
|
+
renderOptions.applyUsernameToAuthors = true;
|
|
165
|
+
changed = true;
|
|
166
|
+
}
|
|
167
|
+
if (changed) {
|
|
168
|
+
writeJsonObject(nxJsonPath, nxJson);
|
|
169
|
+
console.log('updated nx.json release config');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log('unchanged nx.json release config');
|
|
47
173
|
}
|
|
48
174
|
}
|
|
175
|
+
export function applyNxProjectNameDefaults(root) {
|
|
176
|
+
const rootPackage = requiredJsonObject(join(root, 'package.json'));
|
|
177
|
+
const rootName = stringProperty(rootPackage, 'name');
|
|
178
|
+
if (!rootName) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
for (const pkg of getWorkspacePackages(root)) {
|
|
182
|
+
const suggestedName = suggestNxProjectName(rootName, pkg.name);
|
|
183
|
+
if (!suggestedName) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const changed = applyPackageNxConfig(pkg.json, { projectName: suggestedName }).changed;
|
|
187
|
+
if (changed) {
|
|
188
|
+
writeJsonObject(pkg.packageJsonPath, pkg.json);
|
|
189
|
+
console.log(`updated ${pkg.path}/package.json nx.name`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(`unchanged ${pkg.path}/package.json nx.name`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function listValidCommitScopes(root) {
|
|
197
|
+
return new Set([...listNxProjectNames(root), ...extraCommitScopes]);
|
|
198
|
+
}
|
|
199
|
+
export function listNxProjectNames(root) {
|
|
200
|
+
const rootPackage = readJsonObject(join(root, 'package.json'));
|
|
201
|
+
const rootName = rootPackage ? stringProperty(rootPackage, 'name') : null;
|
|
202
|
+
const names = [];
|
|
203
|
+
for (const pkg of getWorkspacePackages(root)) {
|
|
204
|
+
const nx = recordProperty(pkg.json, 'nx');
|
|
205
|
+
const configuredName = nx ? stringProperty(nx, 'name') : null;
|
|
206
|
+
const suggestedName = rootName ? suggestNxProjectName(rootName, pkg.name) : null;
|
|
207
|
+
if (configuredName) {
|
|
208
|
+
names.push(configuredName);
|
|
209
|
+
}
|
|
210
|
+
else if (suggestedName) {
|
|
211
|
+
names.push(suggestedName);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return names;
|
|
215
|
+
}
|
|
49
216
|
export function validateRootPackagePolicy(root) {
|
|
50
217
|
const rootPackage = readJsonObject(join(root, 'package.json'));
|
|
51
218
|
if (!rootPackage) {
|
|
@@ -65,6 +232,8 @@ export function validateRootPackagePolicy(root) {
|
|
|
65
232
|
console.error('package.json must define repository.url');
|
|
66
233
|
failures++;
|
|
67
234
|
}
|
|
235
|
+
failures += validateRootScripts(rootPackage);
|
|
236
|
+
failures += validateRootNxScriptInference(rootPackage);
|
|
68
237
|
const packageManager = stringProperty(rootPackage, 'packageManager');
|
|
69
238
|
if (!packageManager?.startsWith('bun@')) {
|
|
70
239
|
console.error('package.json packageManager must use bun@<version>');
|
|
@@ -91,11 +260,16 @@ export function validateNxReleaseConfig(root) {
|
|
|
91
260
|
}
|
|
92
261
|
const release = recordProperty(nxJson, 'release');
|
|
93
262
|
const version = release ? recordProperty(release, 'version') : null;
|
|
263
|
+
const releaseTag = release ? recordProperty(release, 'releaseTag') : null;
|
|
264
|
+
const changelog = release ? recordProperty(release, 'changelog') : null;
|
|
265
|
+
const projectChangelogs = changelog ? recordProperty(changelog, 'projectChangelogs') : null;
|
|
266
|
+
const renderOptions = projectChangelogs ? recordProperty(projectChangelogs, 'renderOptions') : null;
|
|
94
267
|
let failures = 0;
|
|
95
268
|
if (!release) {
|
|
96
269
|
console.error('nx.json release config is missing');
|
|
97
270
|
failures++;
|
|
98
271
|
}
|
|
272
|
+
failures += validateNxPluginConfig(nxJson);
|
|
99
273
|
if (release && stringProperty(release, 'projectsRelationship') !== 'independent') {
|
|
100
274
|
console.error('nx.json release.projectsRelationship must be independent');
|
|
101
275
|
failures++;
|
|
@@ -118,12 +292,72 @@ export function validateNxReleaseConfig(root) {
|
|
|
118
292
|
console.error('nx.json release.version.fallbackCurrentVersionResolver must be disk');
|
|
119
293
|
failures++;
|
|
120
294
|
}
|
|
121
|
-
if (version &&
|
|
122
|
-
console.error(
|
|
295
|
+
if (version && stringProperty(version, 'versionActions') !== SMOO_NX_VERSION_ACTIONS) {
|
|
296
|
+
console.error(`nx.json release.version.versionActions must be ${SMOO_NX_VERSION_ACTIONS}`);
|
|
297
|
+
failures++;
|
|
298
|
+
}
|
|
299
|
+
if (version && stringProperty(version, 'preVersionCommand')) {
|
|
300
|
+
console.error('nx.json release.version.preVersionCommand must not be defined; smoo builds npm-missing packages before publish');
|
|
301
|
+
failures++;
|
|
302
|
+
}
|
|
303
|
+
if (!releaseTag) {
|
|
304
|
+
console.error('nx.json release.releaseTag config is missing');
|
|
305
|
+
failures++;
|
|
306
|
+
}
|
|
307
|
+
if (releaseTag && stringProperty(releaseTag, 'pattern') !== SMOO_NX_RELEASE_TAG_PATTERN) {
|
|
308
|
+
console.error(`nx.json release.releaseTag.pattern must be ${SMOO_NX_RELEASE_TAG_PATTERN}`);
|
|
309
|
+
failures++;
|
|
310
|
+
}
|
|
311
|
+
if (!changelog) {
|
|
312
|
+
console.error('nx.json release.changelog config is missing');
|
|
313
|
+
failures++;
|
|
314
|
+
}
|
|
315
|
+
if (changelog && changelog.workspaceChangelog !== false) {
|
|
316
|
+
console.error('nx.json release.changelog.workspaceChangelog must be false');
|
|
317
|
+
failures++;
|
|
318
|
+
}
|
|
319
|
+
if (!projectChangelogs) {
|
|
320
|
+
console.error('nx.json release.changelog.projectChangelogs config is missing');
|
|
321
|
+
failures++;
|
|
322
|
+
}
|
|
323
|
+
if (projectChangelogs && projectChangelogs.createRelease !== false) {
|
|
324
|
+
console.error('nx.json release.changelog.projectChangelogs.createRelease must be false');
|
|
325
|
+
failures++;
|
|
326
|
+
}
|
|
327
|
+
if (projectChangelogs && projectChangelogs.file !== false) {
|
|
328
|
+
console.error('nx.json release.changelog.projectChangelogs.file must be false');
|
|
329
|
+
failures++;
|
|
330
|
+
}
|
|
331
|
+
if (!renderOptions) {
|
|
332
|
+
console.error('nx.json release.changelog.projectChangelogs.renderOptions config is missing');
|
|
123
333
|
failures++;
|
|
124
334
|
}
|
|
125
335
|
return failures;
|
|
126
336
|
}
|
|
337
|
+
export function validateNxProjectNames(root) {
|
|
338
|
+
const rootPackage = readJsonObject(join(root, 'package.json'));
|
|
339
|
+
const rootName = rootPackage ? stringProperty(rootPackage, 'name') : null;
|
|
340
|
+
if (!rootName) {
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
let failures = 0;
|
|
344
|
+
for (const pkg of getWorkspacePackages(root)) {
|
|
345
|
+
const suggestedName = suggestNxProjectName(rootName, pkg.name);
|
|
346
|
+
if (!suggestedName) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const nx = recordProperty(pkg.json, 'nx');
|
|
350
|
+
const configuredName = nx ? stringProperty(nx, 'name') : null;
|
|
351
|
+
if (configuredName !== suggestedName) {
|
|
352
|
+
console.error(`${pkg.path}: package.json nx.name must be "${suggestedName}" so fix(${suggestedName}): maps to this project`);
|
|
353
|
+
failures++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (failures === 0) {
|
|
357
|
+
console.log('Nx project names are valid.');
|
|
358
|
+
}
|
|
359
|
+
return failures;
|
|
360
|
+
}
|
|
127
361
|
export function validatePublicTags(root) {
|
|
128
362
|
let failures = 0;
|
|
129
363
|
for (const pkg of getWorkspacePackages(root)) {
|
|
@@ -132,10 +366,6 @@ export function validatePublicTags(root) {
|
|
|
132
366
|
console.error(`${pkg.path}: private package must not have nx tag npm:public`);
|
|
133
367
|
failures++;
|
|
134
368
|
}
|
|
135
|
-
if (!pkg.private && !hasPublicTag) {
|
|
136
|
-
console.error(`${pkg.path}: public package must have nx tag npm:public`);
|
|
137
|
-
failures++;
|
|
138
|
-
}
|
|
139
369
|
}
|
|
140
370
|
if (failures > 0) {
|
|
141
371
|
return failures;
|
|
@@ -148,6 +378,10 @@ export function validatePublicPackageMetadata(root) {
|
|
|
148
378
|
const rootRepository = rootPackage ? repositoryInfo(rootPackage) : null;
|
|
149
379
|
let failures = 0;
|
|
150
380
|
for (const pkg of listPublicPackages(root)) {
|
|
381
|
+
if (pkg.private) {
|
|
382
|
+
console.error(`${pkg.path}: npm:public package must not be private`);
|
|
383
|
+
failures++;
|
|
384
|
+
}
|
|
151
385
|
if (!stringProperty(pkg.json, 'license')) {
|
|
152
386
|
console.error(`${pkg.path}: public package must define license`);
|
|
153
387
|
failures++;
|
|
@@ -158,9 +392,21 @@ export function validatePublicPackageMetadata(root) {
|
|
|
158
392
|
failures++;
|
|
159
393
|
}
|
|
160
394
|
const repository = recordProperty(pkg.json, 'repository');
|
|
161
|
-
const packageRepository =
|
|
162
|
-
if (!
|
|
163
|
-
console.error(`${pkg.path}: public package
|
|
395
|
+
const packageRepository = packageRepositoryInfo(pkg);
|
|
396
|
+
if (!packageRepository) {
|
|
397
|
+
console.error(`${pkg.path}: public package must define repository.url`);
|
|
398
|
+
failures++;
|
|
399
|
+
}
|
|
400
|
+
if (rootRepository &&
|
|
401
|
+
packageRepository &&
|
|
402
|
+
packageRepository.url !== rootRepository.url &&
|
|
403
|
+
sameRepositoryAfterNormalization(packageRepository.url, rootRepository.url)) {
|
|
404
|
+
console.error(`${pkg.path}: repository.url refers to the root repository but is not an exact match. ` +
|
|
405
|
+
`Use ${rootRepository.url}`);
|
|
406
|
+
failures++;
|
|
407
|
+
}
|
|
408
|
+
if (!repository || !stringProperty(repository, 'type')) {
|
|
409
|
+
console.error(`${pkg.path}: public package must define repository.type`);
|
|
164
410
|
failures++;
|
|
165
411
|
}
|
|
166
412
|
if (!repository || stringProperty(repository, 'directory') !== pkg.path.replaceAll('\\', '/')) {
|
|
@@ -182,10 +428,17 @@ export function validatePublicPackageMetadata(root) {
|
|
|
182
428
|
}
|
|
183
429
|
return failures;
|
|
184
430
|
}
|
|
185
|
-
export function validateWorkspaceDependencies(root) {
|
|
431
|
+
export function validateWorkspaceDependencies(root, options = {}) {
|
|
186
432
|
const workspaceNames = new Set(getWorkspacePackages(root).map((pkg) => pkg.name));
|
|
187
433
|
let failures = 0;
|
|
188
434
|
for (const pkg of listPackageJsonRecords(root)) {
|
|
435
|
+
const projectName = packageNxProjectName(pkg.json);
|
|
436
|
+
const resolvedTargets = resolvedProjectTargetNames(projectName ? options.resolvedTargetsByProject?.get(projectName) : undefined);
|
|
437
|
+
failures += validateExplicitNxTargets(pkg.json, pkg.path, resolvedTargets);
|
|
438
|
+
failures += validateBunTestTsconfigPresence(root, pkg.path, pkg.json);
|
|
439
|
+
failures += validateTsconfigTestPolicy(root, pkg.path);
|
|
440
|
+
failures += validateTsconfigTestReferencePolicy(root, pkg.path);
|
|
441
|
+
failures += validateBuildZigPolicy(root, pkg.path);
|
|
189
442
|
for (const field of workspaceDependencyFields) {
|
|
190
443
|
const dependencies = recordProperty(pkg.json, field);
|
|
191
444
|
if (!dependencies) {
|
|
@@ -198,9 +451,148 @@ export function validateWorkspaceDependencies(root) {
|
|
|
198
451
|
}
|
|
199
452
|
}
|
|
200
453
|
}
|
|
454
|
+
failures += validatePackageScriptPolicy(pkg.json, pkg.path, workspaceNames, { resolvedTargets });
|
|
201
455
|
}
|
|
202
456
|
if (failures === 0) {
|
|
203
|
-
console.log('Workspace dependency
|
|
457
|
+
console.log('Workspace dependency policy is valid.');
|
|
458
|
+
}
|
|
459
|
+
return failures;
|
|
460
|
+
}
|
|
461
|
+
export function applyPackageScriptPolicy(pkg, _packagePath, workspaceNames, options = {}) {
|
|
462
|
+
if (!hasWorkspaceDependency(pkg, workspaceNames)) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
const scripts = recordProperty(pkg, 'scripts');
|
|
466
|
+
if (!scripts) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const projectName = packageNxProjectName(pkg);
|
|
470
|
+
if (!projectName) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const nxConfig = applyPackageNxConfig(pkg, { projectName, targets: true });
|
|
474
|
+
const targets = nxConfig.targets;
|
|
475
|
+
let changed = nxConfig.changed;
|
|
476
|
+
for (const [scriptName, rawCommand] of Object.entries(scripts)) {
|
|
477
|
+
if (typeof rawCommand !== 'string') {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const rewrite = classifyScriptRewrite(scriptName, rawCommand);
|
|
481
|
+
if (!rewrite) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const targetName = rewrite.targetName;
|
|
485
|
+
const alias = nxRunAlias(projectName, targetName, rewrite.continuous);
|
|
486
|
+
const existingTarget = recordProperty(targets, targetName);
|
|
487
|
+
if (!existingTarget &&
|
|
488
|
+
targetName !== scriptName &&
|
|
489
|
+
targetExistsInResolvedProject(targetName, options.resolvedTargets)) {
|
|
490
|
+
if (scripts[scriptName] !== alias) {
|
|
491
|
+
scripts[scriptName] = alias;
|
|
492
|
+
changed = true;
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const existingOptions = existingTarget ? recordProperty(existingTarget, 'options') : null;
|
|
497
|
+
const existingCommand = existingOptions ? stringProperty(existingOptions, 'command') : null;
|
|
498
|
+
const command = isScriptRunnerCommand(existingCommand, scriptName)
|
|
499
|
+
? rewrite.command
|
|
500
|
+
: (existingCommand ?? rewrite.command);
|
|
501
|
+
const target = existingTarget ?? {};
|
|
502
|
+
changed = setStringProperty(target, 'executor', 'nx:run-commands') || changed;
|
|
503
|
+
changed = setStringArrayProperty(target, 'dependsOn', expectedTargetDependencies(targetName)) || changed;
|
|
504
|
+
if (rewrite.continuous && target.continuous !== true) {
|
|
505
|
+
target.continuous = true;
|
|
506
|
+
changed = true;
|
|
507
|
+
}
|
|
508
|
+
const targetOptions = getOrCreateRecord(target, 'options');
|
|
509
|
+
changed = setStringProperty(targetOptions, 'command', command) || changed;
|
|
510
|
+
changed = setStringProperty(targetOptions, 'cwd', '{projectRoot}') || changed;
|
|
511
|
+
for (const [name, value] of Object.entries(rewrite.env)) {
|
|
512
|
+
const env = getOrCreateRecord(targetOptions, 'env');
|
|
513
|
+
changed = setStringProperty(env, name, value) || changed;
|
|
514
|
+
}
|
|
515
|
+
if (targets[targetName] !== target) {
|
|
516
|
+
targets[targetName] = target;
|
|
517
|
+
changed = true;
|
|
518
|
+
}
|
|
519
|
+
if (scripts[scriptName] !== alias) {
|
|
520
|
+
scripts[scriptName] = alias;
|
|
521
|
+
changed = true;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return changed;
|
|
525
|
+
}
|
|
526
|
+
export function validatePackageScriptPolicy(pkg, packagePath, workspaceNames, options = {}) {
|
|
527
|
+
if (!hasWorkspaceDependency(pkg, workspaceNames)) {
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
const scripts = recordProperty(pkg, 'scripts');
|
|
531
|
+
if (!scripts) {
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
const nx = recordProperty(pkg, 'nx');
|
|
535
|
+
const projectName = nx ? stringProperty(nx, 'name') : stringProperty(pkg, 'name');
|
|
536
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
537
|
+
let failures = 0;
|
|
538
|
+
for (const [scriptName, rawCommand] of Object.entries(scripts)) {
|
|
539
|
+
if (typeof rawCommand !== 'string') {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const alias = parseNxRunAlias(rawCommand);
|
|
543
|
+
const rewrite = alias
|
|
544
|
+
? { targetName: alias.targetName, continuous: isContinuousTarget(alias.targetName, '') }
|
|
545
|
+
: classifyScriptRewrite(scriptName, rawCommand);
|
|
546
|
+
if (!rewrite || (alias && projectName && alias.projectName !== projectName)) {
|
|
547
|
+
if (alias && projectName && alias.projectName !== projectName) {
|
|
548
|
+
console.error(`${packagePath}: scripts.${scriptName} must delegate to project ${projectName}`);
|
|
549
|
+
failures++;
|
|
550
|
+
}
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (!projectName) {
|
|
554
|
+
console.error(`${packagePath}: package scripts that use workspace dependencies require package.json nx.name`);
|
|
555
|
+
failures++;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const expectedAlias = nxRunAlias(projectName, rewrite.targetName, rewrite.continuous);
|
|
559
|
+
if (rawCommand !== expectedAlias) {
|
|
560
|
+
console.error(`${packagePath}: scripts.${scriptName} must delegate to ${expectedAlias}`);
|
|
561
|
+
failures++;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const target = targets ? recordProperty(targets, rewrite.targetName) : null;
|
|
565
|
+
if (!target &&
|
|
566
|
+
rewrite.targetName !== scriptName &&
|
|
567
|
+
targetExistsInResolvedProject(rewrite.targetName, options.resolvedTargets)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (rewrite.targetName.includes(':')) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const targetOptions = target ? recordProperty(target, 'options') : null;
|
|
574
|
+
const command = targetOptions ? stringProperty(targetOptions, 'command') : null;
|
|
575
|
+
if (!target || stringProperty(target, 'executor') !== 'nx:run-commands' || !targetOptions || !command) {
|
|
576
|
+
console.error(`${packagePath}: nx.targets.${rewrite.targetName} must use nx:run-commands with options.command`);
|
|
577
|
+
failures++;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (stringProperty(targetOptions, 'cwd') !== '{projectRoot}') {
|
|
581
|
+
console.error(`${packagePath}: nx.targets.${rewrite.targetName}.options.cwd must be {projectRoot}`);
|
|
582
|
+
failures++;
|
|
583
|
+
}
|
|
584
|
+
if (!targetDependsOn(target, expectedTargetDependencies(rewrite.targetName))) {
|
|
585
|
+
console.error(`${packagePath}: nx.targets.${rewrite.targetName}.dependsOn must include ${expectedTargetDependencies(rewrite.targetName).join(', ')}`);
|
|
586
|
+
failures++;
|
|
587
|
+
}
|
|
588
|
+
if (rewrite.continuous && target.continuous !== true) {
|
|
589
|
+
console.error(`${packagePath}: nx.targets.${rewrite.targetName}.continuous must be true`);
|
|
590
|
+
failures++;
|
|
591
|
+
}
|
|
592
|
+
if (isScriptRunnerCommand(command, scriptName)) {
|
|
593
|
+
console.error(`${packagePath}: nx.targets.${rewrite.targetName}.options.command must not call scripts.${scriptName}`);
|
|
594
|
+
failures++;
|
|
595
|
+
}
|
|
204
596
|
}
|
|
205
597
|
return failures;
|
|
206
598
|
}
|
|
@@ -220,6 +612,876 @@ function fixWorkspaceDependencyRanges(pkg, workspaceNames) {
|
|
|
220
612
|
}
|
|
221
613
|
return changed;
|
|
222
614
|
}
|
|
615
|
+
function validateRootScripts(rootPackage) {
|
|
616
|
+
const scripts = recordProperty(rootPackage, 'scripts');
|
|
617
|
+
let failures = 0;
|
|
618
|
+
for (const [name, command] of Object.entries(rootScriptPolicy)) {
|
|
619
|
+
if (scripts?.[name] !== command) {
|
|
620
|
+
console.error(`package.json scripts.${name} must be ${command}`);
|
|
621
|
+
failures++;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (scripts && !recordKeysAreSorted(scripts)) {
|
|
625
|
+
console.error('package.json scripts must be sorted alphabetically so root command policy stays stable across fixes.');
|
|
626
|
+
failures++;
|
|
627
|
+
}
|
|
628
|
+
return failures;
|
|
629
|
+
}
|
|
630
|
+
function validateRootNxScriptInference(rootPackage) {
|
|
631
|
+
const nx = recordProperty(rootPackage, 'nx');
|
|
632
|
+
if (nx && Array.isArray(nx.includedScripts) && nx.includedScripts.length === 0) {
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
635
|
+
console.error('package.json nx.includedScripts must be [] so root scripts do not become recursive Nx targets.');
|
|
636
|
+
return 1;
|
|
637
|
+
}
|
|
638
|
+
function validateNxPluginConfig(nxJson) {
|
|
639
|
+
let failures = 0;
|
|
640
|
+
const targetDefaults = recordProperty(nxJson, 'targetDefaults');
|
|
641
|
+
if (targetDefaults) {
|
|
642
|
+
for (const targetName of Object.keys(targetDefaults)) {
|
|
643
|
+
if (targetName.includes(':')) {
|
|
644
|
+
console.error(`nx.json targetDefaults.${targetName} must not use colon target names. ` +
|
|
645
|
+
'Nx CLI syntax already uses project:target:configuration, so smoo Nx target names must be unambiguous tool-output names.');
|
|
646
|
+
failures++;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
failures += validateBuildTargetDefault(nxJson);
|
|
651
|
+
failures += validateNamedInputDefaults(nxJson);
|
|
652
|
+
const plugins = Array.isArray(nxJson.plugins) ? nxJson.plugins : [];
|
|
653
|
+
const nxJsPlugin = plugins.find(isNxJsTypescriptPlugin);
|
|
654
|
+
if (!nxJsPlugin) {
|
|
655
|
+
console.error(`nx.json plugins must configure ${nxJsTypescriptPlugin}. ` +
|
|
656
|
+
'Official Nx owns TypeScript library inference; smoo configures it so tsconfig.lib.json produces tsc-js and leaves build available as an aggregate target.');
|
|
657
|
+
failures++;
|
|
658
|
+
}
|
|
659
|
+
else if (nxJsBuildTargetName(nxJsPlugin) !== 'tsc-js') {
|
|
660
|
+
console.error(`nx.json ${nxJsTypescriptPlugin} build.targetName must be tsc-js. ` +
|
|
661
|
+
'TypeScript library output is a concrete tool-output target; build is reserved for aggregate targets that depend on concrete build work.');
|
|
662
|
+
failures++;
|
|
663
|
+
}
|
|
664
|
+
if (!plugins.includes(smoothBricksNxPlugin) && !plugins.some(isSmoothBricksNxPluginRecord)) {
|
|
665
|
+
console.error(`nx.json plugins must include ${smoothBricksNxPlugin}. ` +
|
|
666
|
+
'Smoo relies on this plugin to infer convention targets that official Nx does not provide, including typecheck-tests, non-TypeScript build-tool targets, and aggregate build/lint targets.');
|
|
667
|
+
failures++;
|
|
668
|
+
}
|
|
669
|
+
return failures;
|
|
670
|
+
}
|
|
671
|
+
function validateExplicitNxTargets(pkg, packagePath, resolvedTargets) {
|
|
672
|
+
const nx = recordProperty(pkg, 'nx');
|
|
673
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
674
|
+
if (!targets) {
|
|
675
|
+
return 0;
|
|
676
|
+
}
|
|
677
|
+
let failures = 0;
|
|
678
|
+
for (const [targetName, rawTarget] of Object.entries(targets)) {
|
|
679
|
+
if (targetName.includes(':')) {
|
|
680
|
+
console.error(`${packagePath}: package.json nx.targets.${targetName} must not use colon target names. ` +
|
|
681
|
+
'Nx CLI syntax already uses project:target:configuration; use a concrete tool-output target name and keep colon names only as package-script aliases.');
|
|
682
|
+
failures++;
|
|
683
|
+
}
|
|
684
|
+
if (!isRecord(rawTarget)) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
failures += validateTargetDependencies(rawTarget, `${packagePath}: nx.targets.${targetName}`, resolvedTargets);
|
|
688
|
+
}
|
|
689
|
+
return failures;
|
|
690
|
+
}
|
|
691
|
+
function validateTargetDependencies(target, label, resolvedTargets) {
|
|
692
|
+
if (!Array.isArray(target.dependsOn)) {
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
let failures = 0;
|
|
696
|
+
for (const dependency of target.dependsOn) {
|
|
697
|
+
if (typeof dependency !== 'string') {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (dependency.includes(':')) {
|
|
701
|
+
console.error(`${label}.dependsOn must not include colon target dependency ${dependency}`);
|
|
702
|
+
failures++;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (label.endsWith('nx.targets.build') &&
|
|
706
|
+
!dependency.startsWith('^') &&
|
|
707
|
+
!targetExistsInResolvedProject(dependency, resolvedTargets)) {
|
|
708
|
+
console.error(`${label}.dependsOn references missing target ${dependency}`);
|
|
709
|
+
failures++;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return failures;
|
|
713
|
+
}
|
|
714
|
+
function validateTsconfigTestPolicy(root, packagePath) {
|
|
715
|
+
const path = join(root, packagePath, 'tsconfig.test.json');
|
|
716
|
+
const tsconfig = readJsonObject(path);
|
|
717
|
+
if (!tsconfig) {
|
|
718
|
+
return 0;
|
|
719
|
+
}
|
|
720
|
+
const compilerOptions = recordProperty(tsconfig, 'compilerOptions');
|
|
721
|
+
let failures = 0;
|
|
722
|
+
if (!compilerOptions || compilerOptions.noEmit !== true) {
|
|
723
|
+
console.error(`${packagePath}/tsconfig.test.json compilerOptions.noEmit must be true`);
|
|
724
|
+
failures++;
|
|
725
|
+
}
|
|
726
|
+
if (compilerOptions?.composite === true) {
|
|
727
|
+
console.error(`${packagePath}/tsconfig.test.json must not set compilerOptions.composite = true. ` +
|
|
728
|
+
'Bun test typechecking is a no-emit validation pass, not a TypeScript build-mode project.');
|
|
729
|
+
failures++;
|
|
730
|
+
}
|
|
731
|
+
if (compilerOptions?.declaration === true) {
|
|
732
|
+
console.error(`${packagePath}/tsconfig.test.json must not set compilerOptions.declaration = true`);
|
|
733
|
+
failures++;
|
|
734
|
+
}
|
|
735
|
+
if (compilerOptions?.declarationMap === true) {
|
|
736
|
+
console.error(`${packagePath}/tsconfig.test.json must not set compilerOptions.declarationMap = true`);
|
|
737
|
+
failures++;
|
|
738
|
+
}
|
|
739
|
+
if (compilerOptions?.outDir === 'dist-test') {
|
|
740
|
+
console.error(`${packagePath}/tsconfig.test.json must not emit to dist-test`);
|
|
741
|
+
failures++;
|
|
742
|
+
}
|
|
743
|
+
if (typeof compilerOptions?.tsBuildInfoFile === 'string' && compilerOptions.tsBuildInfoFile.includes('dist-test')) {
|
|
744
|
+
console.error(`${packagePath}/tsconfig.test.json must not write tsbuildinfo under dist-test`);
|
|
745
|
+
failures++;
|
|
746
|
+
}
|
|
747
|
+
return failures;
|
|
748
|
+
}
|
|
749
|
+
function validateTsconfigTestReferencePolicy(root, packagePath) {
|
|
750
|
+
const testTsconfigPath = join(root, packagePath, 'tsconfig.test.json');
|
|
751
|
+
if (!existsSync(testTsconfigPath)) {
|
|
752
|
+
return 0;
|
|
753
|
+
}
|
|
754
|
+
const projectTsconfig = readJsonObject(join(root, packagePath, 'tsconfig.json'));
|
|
755
|
+
if (!projectTsconfigHasTestReference(projectTsconfig)) {
|
|
756
|
+
return 0;
|
|
757
|
+
}
|
|
758
|
+
console.error(`${packagePath}/tsconfig.json must not reference ./tsconfig.test.json. ` +
|
|
759
|
+
'Test typechecking is run by the inferred typecheck-tests target with tsc --noEmit, not TypeScript build mode.');
|
|
760
|
+
return 1;
|
|
761
|
+
}
|
|
762
|
+
function validateBunTestTsconfigPresence(root, packagePath, pkg) {
|
|
763
|
+
if (!usesBunTest(pkg)) {
|
|
764
|
+
return 0;
|
|
765
|
+
}
|
|
766
|
+
const path = join(root, packagePath, 'tsconfig.test.json');
|
|
767
|
+
if (existsSync(path)) {
|
|
768
|
+
return 0;
|
|
769
|
+
}
|
|
770
|
+
console.error(`${packagePath}: bun test requires tsconfig.test.json because Bun executes tests without typechecking. ` +
|
|
771
|
+
'Run smoo monorepo validate --fix to create the no-emit test typecheck config.');
|
|
772
|
+
return 1;
|
|
773
|
+
}
|
|
774
|
+
function applyBunTestTsconfigDefaults(root, packagePath, pkg, workspaceNames) {
|
|
775
|
+
if (!usesBunTest(pkg)) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const tsconfigTestPath = join(root, packagePath, 'tsconfig.test.json');
|
|
779
|
+
const existing = readJsonObject(tsconfigTestPath);
|
|
780
|
+
const tsconfigTest = existing ?? {};
|
|
781
|
+
let changed = existing === null;
|
|
782
|
+
changed = applyTsconfigTestDefaults(root, packagePath, pkg, tsconfigTest, workspaceNames) || changed;
|
|
783
|
+
if (changed) {
|
|
784
|
+
writeJsonObject(tsconfigTestPath, tsconfigTest);
|
|
785
|
+
console.log(`updated ${packagePath}/tsconfig.test.json bun test typecheck config`);
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
console.log(`unchanged ${packagePath}/tsconfig.test.json bun test typecheck config`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function applyTsconfigTestReferenceDefaults(root, packagePath) {
|
|
792
|
+
const projectTsconfigPath = join(root, packagePath, 'tsconfig.json');
|
|
793
|
+
const projectTsconfig = readJsonObject(projectTsconfigPath);
|
|
794
|
+
if (!projectTsconfig || !projectTsconfigHasTestReference(projectTsconfig)) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const references = Array.isArray(projectTsconfig.references) ? projectTsconfig.references : [];
|
|
798
|
+
projectTsconfig.references = references.filter((entry) => !isRecord(entry) || entry.path !== './tsconfig.test.json');
|
|
799
|
+
writeJsonObject(projectTsconfigPath, projectTsconfig);
|
|
800
|
+
console.log(`updated ${packagePath}/tsconfig.json removed test project reference`);
|
|
801
|
+
}
|
|
802
|
+
function applyTsconfigTestDefaults(root, packagePath, pkg, tsconfigTest, workspaceNames) {
|
|
803
|
+
let changed = setMissingStringProperty(tsconfigTest, 'extends', defaultTsconfigTestExtends(root, packagePath));
|
|
804
|
+
const compilerOptions = getOrCreateRecord(tsconfigTest, 'compilerOptions');
|
|
805
|
+
changed = copyLibCompilerOptions(root, packagePath, compilerOptions) || changed;
|
|
806
|
+
changed = setBooleanProperty(compilerOptions, 'composite', false) || changed;
|
|
807
|
+
changed = setBooleanProperty(compilerOptions, 'declaration', false) || changed;
|
|
808
|
+
changed = setBooleanProperty(compilerOptions, 'declarationMap', false) || changed;
|
|
809
|
+
changed = setBooleanProperty(compilerOptions, 'emitDeclarationOnly', false) || changed;
|
|
810
|
+
changed = setBooleanProperty(compilerOptions, 'noEmit', true) || changed;
|
|
811
|
+
changed = mergeStringListProperty(compilerOptions, 'types', ['bun']) || changed;
|
|
812
|
+
if (delete compilerOptions.outDir) {
|
|
813
|
+
changed = true;
|
|
814
|
+
}
|
|
815
|
+
if (delete compilerOptions.tsBuildInfoFile) {
|
|
816
|
+
changed = true;
|
|
817
|
+
}
|
|
818
|
+
changed =
|
|
819
|
+
mergeStringListProperty(tsconfigTest, 'include', [
|
|
820
|
+
'src/**/*.test.ts',
|
|
821
|
+
'src/**/*.spec.ts',
|
|
822
|
+
'src/**/__tests__/**/*.ts',
|
|
823
|
+
'src/**/__tests__/**/*.tsx',
|
|
824
|
+
'src/test-suite-tracer.ts',
|
|
825
|
+
]) || changed;
|
|
826
|
+
for (const referencePath of collectTsconfigTestReferencePaths(root, packagePath, pkg, workspaceNames)) {
|
|
827
|
+
changed = addTsconfigReference(tsconfigTest, referencePath) || changed;
|
|
828
|
+
}
|
|
829
|
+
return changed;
|
|
830
|
+
}
|
|
831
|
+
function defaultTsconfigTestExtends(root, packagePath) {
|
|
832
|
+
const libTsconfig = readJsonObject(join(root, packagePath, 'tsconfig.lib.json'));
|
|
833
|
+
return stringProperty(libTsconfig ?? {}, 'extends') ?? '../../tsconfig.base.json';
|
|
834
|
+
}
|
|
835
|
+
function copyLibCompilerOptions(root, packagePath, target) {
|
|
836
|
+
const libTsconfig = readJsonObject(join(root, packagePath, 'tsconfig.lib.json'));
|
|
837
|
+
const libCompilerOptions = libTsconfig ? recordProperty(libTsconfig, 'compilerOptions') : null;
|
|
838
|
+
if (!libCompilerOptions) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
let changed = false;
|
|
842
|
+
for (const key of ['baseUrl', 'module', 'moduleResolution', 'jsx', 'lib']) {
|
|
843
|
+
if (hasOwn(libCompilerOptions, key) && target[key] !== libCompilerOptions[key]) {
|
|
844
|
+
target[key] = libCompilerOptions[key];
|
|
845
|
+
changed = true;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return changed;
|
|
849
|
+
}
|
|
850
|
+
function collectTsconfigTestReferencePaths(root, packagePath, pkg, workspaceNames) {
|
|
851
|
+
const paths = existsSync(join(root, packagePath, 'tsconfig.lib.json')) ? ['./tsconfig.lib.json'] : [];
|
|
852
|
+
const packagesByName = new Map(getWorkspacePackages(root).map((workspacePkg) => [workspacePkg.name, workspacePkg]));
|
|
853
|
+
for (const field of workspaceDependencyFields) {
|
|
854
|
+
const dependencies = recordProperty(pkg, field);
|
|
855
|
+
if (!dependencies) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
for (const dependencyName of Object.keys(dependencies)) {
|
|
859
|
+
if (!workspaceNames.has(dependencyName)) {
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
const dependencyPackage = packagesByName.get(dependencyName);
|
|
863
|
+
if (!dependencyPackage) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const dependencyTsconfig = join(root, dependencyPackage.path, 'tsconfig.lib.json');
|
|
867
|
+
if (!existsSync(dependencyTsconfig)) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const refPath = relative(join(root, packagePath), dependencyTsconfig).replaceAll('\\', '/');
|
|
871
|
+
if (!paths.includes(refPath)) {
|
|
872
|
+
paths.push(refPath);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return paths;
|
|
877
|
+
}
|
|
878
|
+
function usesBunTest(pkg) {
|
|
879
|
+
const scripts = recordProperty(pkg, 'scripts');
|
|
880
|
+
if (scripts && Object.values(scripts).some((command) => typeof command === 'string' && isBunTestCommand(command))) {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
const nx = recordProperty(pkg, 'nx');
|
|
884
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
885
|
+
if (!targets) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
for (const target of Object.values(targets)) {
|
|
889
|
+
if (!isRecord(target)) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const options = recordProperty(target, 'options');
|
|
893
|
+
const command = options ? stringProperty(options, 'command') : null;
|
|
894
|
+
if (command && isBunTestCommand(command)) {
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
function isBunTestCommand(command) {
|
|
901
|
+
return /^bun\s+test(?:\s|$)/.test(parseEnvPrefixedCommand(command).command.trim());
|
|
902
|
+
}
|
|
903
|
+
function mergeStringListProperty(record, key, values) {
|
|
904
|
+
const rawCurrent = record[key];
|
|
905
|
+
const current = Array.isArray(rawCurrent)
|
|
906
|
+
? rawCurrent.filter((entry) => typeof entry === 'string')
|
|
907
|
+
: [];
|
|
908
|
+
const next = [...current];
|
|
909
|
+
for (const value of values) {
|
|
910
|
+
if (!next.includes(value)) {
|
|
911
|
+
next.push(value);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (Array.isArray(rawCurrent) &&
|
|
915
|
+
next.length === rawCurrent.length &&
|
|
916
|
+
next.every((entry, index) => entry === rawCurrent[index])) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
record[key] = next;
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
function addTsconfigReference(tsconfig, path) {
|
|
923
|
+
const current = Array.isArray(tsconfig.references)
|
|
924
|
+
? tsconfig.references.filter((entry) => isRecord(entry))
|
|
925
|
+
: [];
|
|
926
|
+
if (current.some((entry) => entry.path === path)) {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
tsconfig.references = [...current, { path }];
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
function projectTsconfigHasTestReference(projectTsconfig) {
|
|
933
|
+
return Boolean(projectTsconfig &&
|
|
934
|
+
Array.isArray(projectTsconfig.references) &&
|
|
935
|
+
projectTsconfig.references.some((entry) => isRecord(entry) && entry.path === './tsconfig.test.json'));
|
|
936
|
+
}
|
|
937
|
+
function validateBuildZigPolicy(root, packagePath) {
|
|
938
|
+
const path = join(root, packagePath, 'build.zig');
|
|
939
|
+
if (!existsSync(path)) {
|
|
940
|
+
return 0;
|
|
941
|
+
}
|
|
942
|
+
if (/\bb\.step\s*\(/.test(readFileSync(path, 'utf8'))) {
|
|
943
|
+
return 0;
|
|
944
|
+
}
|
|
945
|
+
console.error(`${packagePath}/build.zig must define at least one b.step(...) target`);
|
|
946
|
+
return 1;
|
|
947
|
+
}
|
|
948
|
+
function applyBuildTargetDefault(nxJson) {
|
|
949
|
+
const targetDefaults = getOrCreateRecord(nxJson, 'targetDefaults');
|
|
950
|
+
const build = getOrCreateRecord(targetDefaults, 'build');
|
|
951
|
+
let changed = setBooleanProperty(build, 'cache', true);
|
|
952
|
+
changed = setStringArrayProperty(build, 'outputs', ['{projectRoot}/dist']) || changed;
|
|
953
|
+
return changed;
|
|
954
|
+
}
|
|
955
|
+
function validateBuildTargetDefault(nxJson) {
|
|
956
|
+
const targetDefaults = recordProperty(nxJson, 'targetDefaults');
|
|
957
|
+
const build = targetDefaults ? recordProperty(targetDefaults, 'build') : null;
|
|
958
|
+
let failures = 0;
|
|
959
|
+
if (!build || build.cache !== true) {
|
|
960
|
+
console.error('nx.json targetDefaults.build.cache must be true');
|
|
961
|
+
failures++;
|
|
962
|
+
}
|
|
963
|
+
const outputs = build?.outputs;
|
|
964
|
+
if (!Array.isArray(outputs) || outputs.length !== 1 || outputs[0] !== '{projectRoot}/dist') {
|
|
965
|
+
console.error('nx.json targetDefaults.build.outputs must be ["{projectRoot}/dist"]');
|
|
966
|
+
failures++;
|
|
967
|
+
}
|
|
968
|
+
return failures;
|
|
969
|
+
}
|
|
970
|
+
function applyNamedInputDefaults(nxJson) {
|
|
971
|
+
const namedInputs = getOrCreateRecord(nxJson, 'namedInputs');
|
|
972
|
+
let changed = false;
|
|
973
|
+
if (!Array.isArray(namedInputs.default)) {
|
|
974
|
+
namedInputs.default = ['{projectRoot}/**/*', 'sharedGlobals'];
|
|
975
|
+
changed = true;
|
|
976
|
+
}
|
|
977
|
+
changed = setStringArrayProperty(namedInputs, 'sharedGlobals', expectedSharedGlobalsNamedInput) || changed;
|
|
978
|
+
const production = namedInputs.production;
|
|
979
|
+
if (!Array.isArray(production) || !isPreciseProductionNamedInput(production)) {
|
|
980
|
+
namedInputs.production = defaultProductionNamedInput;
|
|
981
|
+
changed = true;
|
|
982
|
+
}
|
|
983
|
+
return changed;
|
|
984
|
+
}
|
|
985
|
+
function validateNamedInputDefaults(nxJson) {
|
|
986
|
+
const namedInputs = recordProperty(nxJson, 'namedInputs');
|
|
987
|
+
const production = namedInputs?.production;
|
|
988
|
+
let failures = 0;
|
|
989
|
+
if (!namedInputs) {
|
|
990
|
+
console.error('nx.json namedInputs must be configured so production builds have precise cache inputs.');
|
|
991
|
+
return 1;
|
|
992
|
+
}
|
|
993
|
+
if (!Array.isArray(namedInputs.default)) {
|
|
994
|
+
console.error('nx.json namedInputs.default must be an array; smoo allows it to remain broad for non-production tasks.');
|
|
995
|
+
failures++;
|
|
996
|
+
}
|
|
997
|
+
if (!stringArrayEquals(namedInputs.sharedGlobals, expectedSharedGlobalsNamedInput)) {
|
|
998
|
+
console.error('nx.json namedInputs.sharedGlobals must include only {workspaceRoot}/.github/workflows/ci.yml');
|
|
999
|
+
failures++;
|
|
1000
|
+
}
|
|
1001
|
+
if (!Array.isArray(production)) {
|
|
1002
|
+
console.error('nx.json namedInputs.production must be an array of precise production inputs.');
|
|
1003
|
+
return failures + 1;
|
|
1004
|
+
}
|
|
1005
|
+
if (!isPreciseProductionNamedInput(production)) {
|
|
1006
|
+
console.error('nx.json namedInputs.production must enumerate precise production inputs. Do not include default or broad {projectRoot}/** globs; use language/tool-specific paths such as {projectRoot}/src/**/*, {projectRoot}/Cargo.toml, or {projectRoot}/pyproject.toml.');
|
|
1007
|
+
failures++;
|
|
1008
|
+
}
|
|
1009
|
+
return failures;
|
|
1010
|
+
}
|
|
1011
|
+
function isPreciseProductionNamedInput(production) {
|
|
1012
|
+
let hasPositiveProjectInput = false;
|
|
1013
|
+
for (const input of production) {
|
|
1014
|
+
if (typeof input !== 'string') {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
const normalized = input.startsWith('!') ? input.slice(1) : input;
|
|
1018
|
+
if (impreciseProductionInputs.has(input) || impreciseProductionInputs.has(normalized)) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
if (!input.startsWith('!') && normalized.startsWith('{projectRoot}/')) {
|
|
1022
|
+
hasPositiveProjectInput = true;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return hasPositiveProjectInput;
|
|
1026
|
+
}
|
|
1027
|
+
function removeColonTargetDefaults(nxJson) {
|
|
1028
|
+
const targetDefaults = recordProperty(nxJson, 'targetDefaults');
|
|
1029
|
+
if (!targetDefaults) {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
let changed = false;
|
|
1033
|
+
for (const targetName of Object.keys(targetDefaults)) {
|
|
1034
|
+
if (targetName.includes(':')) {
|
|
1035
|
+
delete targetDefaults[targetName];
|
|
1036
|
+
changed = true;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return changed;
|
|
1040
|
+
}
|
|
1041
|
+
function rewriteColonTargetDependenciesInPackage(pkg, resolvedTargets) {
|
|
1042
|
+
const nx = recordProperty(pkg, 'nx');
|
|
1043
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
1044
|
+
if (!targets) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
const projectName = packageNxProjectName(pkg);
|
|
1048
|
+
const scriptTargetAliases = scriptTargetAliasesForProject(pkg, projectName);
|
|
1049
|
+
let changed = false;
|
|
1050
|
+
for (const target of Object.values(targets)) {
|
|
1051
|
+
if (!isRecord(target) || !Array.isArray(target.dependsOn)) {
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
target.dependsOn = target.dependsOn.map((dependency) => {
|
|
1055
|
+
if (typeof dependency !== 'string' || !dependency.includes(':')) {
|
|
1056
|
+
return dependency;
|
|
1057
|
+
}
|
|
1058
|
+
const next = scriptTargetAliases.get(dependency) ?? replacementTargetName(dependency, null, resolvedTargets);
|
|
1059
|
+
if (!next) {
|
|
1060
|
+
return dependency;
|
|
1061
|
+
}
|
|
1062
|
+
changed = true;
|
|
1063
|
+
return next;
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
return changed;
|
|
1067
|
+
}
|
|
1068
|
+
function scriptTargetAliasesForProject(pkg, projectName) {
|
|
1069
|
+
if (!projectName) {
|
|
1070
|
+
return new Map();
|
|
1071
|
+
}
|
|
1072
|
+
const scripts = recordProperty(pkg, 'scripts');
|
|
1073
|
+
if (!scripts) {
|
|
1074
|
+
return new Map();
|
|
1075
|
+
}
|
|
1076
|
+
const aliases = new Map();
|
|
1077
|
+
for (const [scriptName, rawCommand] of Object.entries(scripts)) {
|
|
1078
|
+
if (typeof rawCommand !== 'string' || !scriptName.includes(':')) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
const alias = parseNxRunAlias(rawCommand);
|
|
1082
|
+
if (alias?.projectName === projectName && !alias.targetName.includes(':')) {
|
|
1083
|
+
aliases.set(scriptName, alias.targetName);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return aliases;
|
|
1087
|
+
}
|
|
1088
|
+
function removePackageColonTargets(pkg) {
|
|
1089
|
+
const nx = recordProperty(pkg, 'nx');
|
|
1090
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
1091
|
+
if (!targets) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
let changed = false;
|
|
1095
|
+
for (const targetName of Object.keys(targets)) {
|
|
1096
|
+
if (targetName.includes(':')) {
|
|
1097
|
+
delete targets[targetName];
|
|
1098
|
+
changed = true;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return changed;
|
|
1102
|
+
}
|
|
1103
|
+
function resolvedProjectTargetNames(resolvedProject) {
|
|
1104
|
+
if (!resolvedProject) {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
return isResolvedProjectTargets(resolvedProject) ? resolvedProject.targets : resolvedProject;
|
|
1108
|
+
}
|
|
1109
|
+
function resolvedProjectBuildDependsOn(resolvedProject) {
|
|
1110
|
+
if (!isResolvedProjectTargets(resolvedProject)) {
|
|
1111
|
+
return undefined;
|
|
1112
|
+
}
|
|
1113
|
+
return resolvedProject.buildDependsOn;
|
|
1114
|
+
}
|
|
1115
|
+
function isResolvedProjectTargets(value) {
|
|
1116
|
+
return isRecord(value) && value.targets instanceof Set;
|
|
1117
|
+
}
|
|
1118
|
+
function removeRedundantNoopBuildTarget(pkg, resolvedProject) {
|
|
1119
|
+
const resolvedTargets = resolvedProjectTargetNames(resolvedProject);
|
|
1120
|
+
const resolvedBuildDependsOn = resolvedProjectBuildDependsOn(resolvedProject);
|
|
1121
|
+
if (!resolvedTargets?.has('build') || !resolvedBuildDependsOn) {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
const nx = recordProperty(pkg, 'nx');
|
|
1125
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
1126
|
+
const build = targets ? recordProperty(targets, 'build') : null;
|
|
1127
|
+
if (!targets ||
|
|
1128
|
+
!build ||
|
|
1129
|
+
!isNoopTarget(build) ||
|
|
1130
|
+
!targetDependenciesMatchResolvedBuild(build, resolvedBuildDependsOn)) {
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
delete targets.build;
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
function isNoopTarget(target) {
|
|
1137
|
+
const executor = stringProperty(target, 'executor');
|
|
1138
|
+
if (executor !== null && executor !== 'nx:noop') {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
const options = recordProperty(target, 'options');
|
|
1142
|
+
return !options || stringProperty(options, 'command') === null;
|
|
1143
|
+
}
|
|
1144
|
+
function targetDependenciesMatchResolvedBuild(target, resolvedBuildDependsOn) {
|
|
1145
|
+
if (!Array.isArray(target.dependsOn)) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
const expected = new Set(resolvedBuildDependsOn);
|
|
1149
|
+
if (target.dependsOn.length !== expected.size) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
return target.dependsOn.every((dependency) => {
|
|
1153
|
+
if (typeof dependency !== 'string') {
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
return expected.has(dependency);
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
function migratePackageColonTargets(pkg, resolvedTargets) {
|
|
1160
|
+
const nx = recordProperty(pkg, 'nx');
|
|
1161
|
+
const targets = nx ? recordProperty(nx, 'targets') : null;
|
|
1162
|
+
if (!targets) {
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
const projectName = packageNxProjectName(pkg);
|
|
1166
|
+
const scripts = recordProperty(pkg, 'scripts');
|
|
1167
|
+
let changed = false;
|
|
1168
|
+
const renamedTargets = new Map();
|
|
1169
|
+
for (const [targetName, rawTarget] of Object.entries(targets)) {
|
|
1170
|
+
if (!targetName.includes(':') || !isRecord(rawTarget)) {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const nextTargetName = replacementTargetName(targetName, targetCommand(rawTarget), resolvedTargets);
|
|
1174
|
+
if (!nextTargetName || nextTargetName.includes(':')) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (!targetExistsInResolvedProject(nextTargetName, resolvedTargets)) {
|
|
1178
|
+
targets[nextTargetName] = rawTarget;
|
|
1179
|
+
}
|
|
1180
|
+
delete targets[targetName];
|
|
1181
|
+
renamedTargets.set(targetName, nextTargetName);
|
|
1182
|
+
changed = true;
|
|
1183
|
+
}
|
|
1184
|
+
if (renamedTargets.size === 0) {
|
|
1185
|
+
return changed;
|
|
1186
|
+
}
|
|
1187
|
+
for (const target of Object.values(targets)) {
|
|
1188
|
+
if (isRecord(target)) {
|
|
1189
|
+
changed = rewriteTargetDependencies(target, renamedTargets) || changed;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (scripts && projectName) {
|
|
1193
|
+
for (const [scriptName, rawCommand] of Object.entries(scripts)) {
|
|
1194
|
+
if (typeof rawCommand !== 'string') {
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
const alias = parseNxRunAlias(rawCommand);
|
|
1198
|
+
if (!alias || alias.projectName !== projectName) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const nextTargetName = renamedTargets.get(alias.targetName);
|
|
1202
|
+
if (!nextTargetName) {
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
scripts[scriptName] = nxRunAlias(projectName, nextTargetName, isContinuousTarget(nextTargetName, ''));
|
|
1206
|
+
changed = true;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return changed;
|
|
1210
|
+
}
|
|
1211
|
+
function rewriteTargetDependencies(target, renamedTargets) {
|
|
1212
|
+
if (!Array.isArray(target.dependsOn)) {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
let changed = false;
|
|
1216
|
+
target.dependsOn = target.dependsOn.map((dependency) => {
|
|
1217
|
+
if (typeof dependency !== 'string') {
|
|
1218
|
+
return dependency;
|
|
1219
|
+
}
|
|
1220
|
+
const next = renamedTargets.get(dependency);
|
|
1221
|
+
if (!next) {
|
|
1222
|
+
return dependency;
|
|
1223
|
+
}
|
|
1224
|
+
changed = true;
|
|
1225
|
+
return next;
|
|
1226
|
+
});
|
|
1227
|
+
return changed;
|
|
1228
|
+
}
|
|
1229
|
+
function targetCommand(target) {
|
|
1230
|
+
const options = recordProperty(target, 'options');
|
|
1231
|
+
return options ? stringProperty(options, 'command') : null;
|
|
1232
|
+
}
|
|
1233
|
+
function replacementTargetName(targetName, command, resolvedTargets) {
|
|
1234
|
+
if (command) {
|
|
1235
|
+
const commandTargetName = targetNameForCommand(command);
|
|
1236
|
+
if (commandTargetName) {
|
|
1237
|
+
return commandTargetName;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const suffix = targetName.slice(targetName.lastIndexOf(':') + 1);
|
|
1241
|
+
if (suffix && targetExistsInResolvedProject(suffix, resolvedTargets)) {
|
|
1242
|
+
return suffix;
|
|
1243
|
+
}
|
|
1244
|
+
const dashed = targetName.replaceAll(':', '-');
|
|
1245
|
+
if (targetExistsInResolvedProject(dashed, resolvedTargets)) {
|
|
1246
|
+
return dashed;
|
|
1247
|
+
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
function expectedNxJsTypescriptPlugin() {
|
|
1251
|
+
return {
|
|
1252
|
+
plugin: nxJsTypescriptPlugin,
|
|
1253
|
+
options: {
|
|
1254
|
+
typecheck: { targetName: 'typecheck' },
|
|
1255
|
+
build: {
|
|
1256
|
+
targetName: 'tsc-js',
|
|
1257
|
+
configName: 'tsconfig.lib.json',
|
|
1258
|
+
buildDepsName: 'build-deps',
|
|
1259
|
+
watchDepsName: 'watch-deps',
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function upsertNxPlugin(plugins, plugin) {
|
|
1265
|
+
const pluginName = typeof plugin === 'string' ? plugin : stringProperty(plugin, 'plugin');
|
|
1266
|
+
const next = plugins.filter((entry) => nxPluginName(entry) !== pluginName);
|
|
1267
|
+
next.push(plugin);
|
|
1268
|
+
return next;
|
|
1269
|
+
}
|
|
1270
|
+
function nxPluginName(value) {
|
|
1271
|
+
if (typeof value === 'string') {
|
|
1272
|
+
return value;
|
|
1273
|
+
}
|
|
1274
|
+
return isRecord(value) ? stringProperty(value, 'plugin') : null;
|
|
1275
|
+
}
|
|
1276
|
+
function isNxJsTypescriptPlugin(value) {
|
|
1277
|
+
return isRecord(value) && stringProperty(value, 'plugin') === nxJsTypescriptPlugin;
|
|
1278
|
+
}
|
|
1279
|
+
function isSmoothBricksNxPluginRecord(value) {
|
|
1280
|
+
return isRecord(value) && stringProperty(value, 'plugin') === smoothBricksNxPlugin;
|
|
1281
|
+
}
|
|
1282
|
+
function nxJsBuildTargetName(plugin) {
|
|
1283
|
+
const options = recordProperty(plugin, 'options');
|
|
1284
|
+
const build = options ? recordProperty(options, 'build') : null;
|
|
1285
|
+
return build ? stringProperty(build, 'targetName') : null;
|
|
1286
|
+
}
|
|
1287
|
+
function applyPackageNxConfig(pkg, options) {
|
|
1288
|
+
const nx = getOrCreateRecord(pkg, 'nx');
|
|
1289
|
+
const changed = setStringProperty(nx, 'name', options.projectName);
|
|
1290
|
+
const targets = options.targets === true ? getOrCreateRecord(nx, 'targets') : null;
|
|
1291
|
+
return { nx, targets, changed };
|
|
1292
|
+
}
|
|
1293
|
+
function packageNxProjectName(pkg) {
|
|
1294
|
+
const nx = recordProperty(pkg, 'nx');
|
|
1295
|
+
return (nx ? stringProperty(nx, 'name') : null) ?? stringProperty(pkg, 'name');
|
|
1296
|
+
}
|
|
1297
|
+
function classifyScriptRewrite(scriptName, command) {
|
|
1298
|
+
if (isNxRunAlias(command) || isBlockedScriptCommand(scriptName, command)) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
const parsed = parseEnvPrefixedCommand(command);
|
|
1302
|
+
const targetName = rewriteTargetName(scriptName, parsed.command);
|
|
1303
|
+
if (!targetName || !isSafeNxScriptCommand(parsed.command)) {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
targetName,
|
|
1308
|
+
continuous: isContinuousTarget(targetName, parsed.command),
|
|
1309
|
+
command: parsed.command,
|
|
1310
|
+
env: parsed.env,
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function rewriteTargetName(scriptName, command) {
|
|
1314
|
+
return targetNameForCommand(command) ?? (scriptName.includes(':') ? null : scriptName);
|
|
1315
|
+
}
|
|
1316
|
+
function targetNameForCommand(command) {
|
|
1317
|
+
const trimmed = command.trim();
|
|
1318
|
+
if (/^tsc\s+--build\s+tsconfig\.lib\.json(?:\s|$)/.test(trimmed)) {
|
|
1319
|
+
return 'tsc-js';
|
|
1320
|
+
}
|
|
1321
|
+
const zigStep = /^zig\s+build\s+([A-Za-z0-9_-]+)(?:\s|$)/.exec(trimmed)?.[1];
|
|
1322
|
+
if (zigStep) {
|
|
1323
|
+
return `zig-${zigStep}`;
|
|
1324
|
+
}
|
|
1325
|
+
if (/^wrangler\s+build(?:\s|$)/.test(trimmed)) {
|
|
1326
|
+
return 'build';
|
|
1327
|
+
}
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
function nxRunAlias(projectName, targetName, continuous) {
|
|
1331
|
+
const flags = continuous ? ' --tui=false --outputStyle=stream' : '';
|
|
1332
|
+
return `nx run ${projectName}:${targetName}${flags}`;
|
|
1333
|
+
}
|
|
1334
|
+
function expectedTargetDependencies(targetName) {
|
|
1335
|
+
return targetName === 'preview' ? ['build'] : ['^build'];
|
|
1336
|
+
}
|
|
1337
|
+
function targetExistsInResolvedProject(targetName, resolvedTargets) {
|
|
1338
|
+
return resolvedTargets?.has(targetName) === true;
|
|
1339
|
+
}
|
|
1340
|
+
function setStringArrayProperty(record, key, value) {
|
|
1341
|
+
const current = record[key];
|
|
1342
|
+
if (stringArrayEquals(current, value)) {
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
record[key] = value;
|
|
1346
|
+
return true;
|
|
1347
|
+
}
|
|
1348
|
+
function stringArrayEquals(value, expected) {
|
|
1349
|
+
return (Array.isArray(value) && value.length === expected.length && value.every((entry, index) => entry === expected[index]));
|
|
1350
|
+
}
|
|
1351
|
+
function targetDependsOn(target, expected) {
|
|
1352
|
+
const dependsOn = target.dependsOn;
|
|
1353
|
+
return Array.isArray(dependsOn) && expected.every((entry) => dependsOn.includes(entry));
|
|
1354
|
+
}
|
|
1355
|
+
function hasWorkspaceDependency(pkg, workspaceNames) {
|
|
1356
|
+
for (const field of workspaceDependencyFields) {
|
|
1357
|
+
const dependencies = recordProperty(pkg, field);
|
|
1358
|
+
if (!dependencies) {
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
for (const name of Object.keys(dependencies)) {
|
|
1362
|
+
if (workspaceNames.has(name)) {
|
|
1363
|
+
return true;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
function isSafeNxScriptCommand(command) {
|
|
1370
|
+
const trimmed = command.trim();
|
|
1371
|
+
return (/^bun\s+test(?:\s|$)/.test(trimmed) ||
|
|
1372
|
+
/^tsc-bun-test(?:\s|$)/.test(trimmed) ||
|
|
1373
|
+
/^vitest(?:\s|$)/.test(trimmed) ||
|
|
1374
|
+
/^tsc\s+(?:--build|--noEmit)(?:\s|$)/.test(trimmed) ||
|
|
1375
|
+
/^tsdown(?:\s|$)/.test(trimmed) ||
|
|
1376
|
+
/^vite\s+(?:build|dev|preview)(?:\s|$)/.test(trimmed) ||
|
|
1377
|
+
/^astro\s+(?:build|dev|preview|check)(?:\s|$)/.test(trimmed) ||
|
|
1378
|
+
/^zig\s+build(?:\s|$)/.test(trimmed) ||
|
|
1379
|
+
/^bun\s+[./\w-]*build[\w.-]*\.ts(?:\s|$)/.test(trimmed) ||
|
|
1380
|
+
/(?:^|\s)(?:bench|benchmark)(?:\s|$)/.test(trimmed) ||
|
|
1381
|
+
/^wrangler\s+build(?:\s|$)/.test(trimmed));
|
|
1382
|
+
}
|
|
1383
|
+
function isBlockedScriptCommand(scriptName, command) {
|
|
1384
|
+
if (/^(?:deploy|db|release|sync|subtree|publish|pack)(?::|$)/.test(scriptName)) {
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1387
|
+
const trimmed = parseEnvPrefixedCommand(command).command;
|
|
1388
|
+
return /^(?:deploy|db|release|sync|subtree|publish|pack)(?:\s|$)/.test(trimmed) || trimmed === 'astro';
|
|
1389
|
+
}
|
|
1390
|
+
function parseEnvPrefixedCommand(command) {
|
|
1391
|
+
const env = {};
|
|
1392
|
+
let rest = command.trimStart();
|
|
1393
|
+
while (true) {
|
|
1394
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)=/.exec(rest);
|
|
1395
|
+
if (!match?.[1]) {
|
|
1396
|
+
return { command: rest.trim(), env };
|
|
1397
|
+
}
|
|
1398
|
+
let index = match[0].length;
|
|
1399
|
+
let value = '';
|
|
1400
|
+
const quote = rest[index];
|
|
1401
|
+
if (quote === '"' || quote === "'") {
|
|
1402
|
+
index += 1;
|
|
1403
|
+
const end = rest.indexOf(quote, index);
|
|
1404
|
+
if (end === -1) {
|
|
1405
|
+
return { command: command.trim(), env: {} };
|
|
1406
|
+
}
|
|
1407
|
+
value = rest.slice(index, end);
|
|
1408
|
+
index = end + 1;
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
const end = rest.slice(index).search(/\s/);
|
|
1412
|
+
const valueEnd = end === -1 ? rest.length : index + end;
|
|
1413
|
+
value = rest.slice(index, valueEnd);
|
|
1414
|
+
index = valueEnd;
|
|
1415
|
+
}
|
|
1416
|
+
if (index < rest.length && !/\s/.test(rest[index] ?? '')) {
|
|
1417
|
+
return { command: command.trim(), env: {} };
|
|
1418
|
+
}
|
|
1419
|
+
env[match[1]] = value;
|
|
1420
|
+
rest = rest.slice(index).trimStart();
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function isContinuousTarget(targetName, command) {
|
|
1424
|
+
return (/(?:^|:|-)(?:dev|serve|preview|watch)(?:$|:|-)/.test(targetName) ||
|
|
1425
|
+
/(?:^|\s)(?:dev|serve|preview|--watch|-w)(?:\s|$)/.test(command));
|
|
1426
|
+
}
|
|
1427
|
+
function isNxRunAlias(command) {
|
|
1428
|
+
return /^nx\s+run\s+\S+:\S+/.test(command.trim());
|
|
1429
|
+
}
|
|
1430
|
+
function parseNxRunAlias(command) {
|
|
1431
|
+
const match = /^nx\s+run\s+([^:\s]+):([^\s]+)(?:\s|$)/.exec(command.trim());
|
|
1432
|
+
if (!match?.[1] || !match[2]) {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
return { projectName: match[1], targetName: match[2] };
|
|
1436
|
+
}
|
|
1437
|
+
function recordKeysAreSorted(record) {
|
|
1438
|
+
const keys = Object.keys(record);
|
|
1439
|
+
return keys.every((key, index) => index === 0 || keys[index - 1] <= key);
|
|
1440
|
+
}
|
|
1441
|
+
function sortRecordInPlace(record) {
|
|
1442
|
+
if (recordKeysAreSorted(record)) {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
const entries = Object.entries(record).sort(([a], [b]) => a.localeCompare(b));
|
|
1446
|
+
for (const key of Object.keys(record)) {
|
|
1447
|
+
delete record[key];
|
|
1448
|
+
}
|
|
1449
|
+
for (const [key, value] of entries) {
|
|
1450
|
+
record[key] = value;
|
|
1451
|
+
}
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
function isScriptRunnerCommand(command, scriptName) {
|
|
1455
|
+
if (!command) {
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
const escaped = escapeRegex(scriptName);
|
|
1459
|
+
return new RegExp(`^(?:bun|npm)\\s+run\\s+${escaped}(?:\\s|$)`).test(command.trim());
|
|
1460
|
+
}
|
|
1461
|
+
function escapeRegex(value) {
|
|
1462
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1463
|
+
}
|
|
1464
|
+
function suggestNxProjectName(rootPackageName, packageName) {
|
|
1465
|
+
const rootScope = npmScope(rootPackageName);
|
|
1466
|
+
if (!rootScope || npmScope(packageName) !== rootScope) {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
return unscopedPackageName(packageName);
|
|
1470
|
+
}
|
|
1471
|
+
function npmScope(packageName) {
|
|
1472
|
+
const match = /^(@[^/]+)\//.exec(packageName);
|
|
1473
|
+
return match?.[1] ?? null;
|
|
1474
|
+
}
|
|
1475
|
+
function unscopedPackageName(packageName) {
|
|
1476
|
+
return packageName.startsWith('@') ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
|
|
1477
|
+
}
|
|
1478
|
+
function setBooleanProperty(record, key, value) {
|
|
1479
|
+
if (record[key] === value) {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
record[key] = value;
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
223
1485
|
function normalizeExportConditionOrder(value) {
|
|
224
1486
|
if (!isRecord(value)) {
|
|
225
1487
|
return false;
|