-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Expand file tree
/
Copy pathcreate-workspace.ts
More file actions
323 lines (288 loc) · 9.96 KB
/
create-workspace.ts
File metadata and controls
323 lines (288 loc) · 9.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
import { existsSync, unlinkSync } from 'node:fs';
import { join } from 'path';
import { createEmptyWorkspace } from './create-empty-workspace';
import { createPreset } from './create-preset';
import { createSandbox } from './create-sandbox';
import { CreateWorkspaceOptions } from './create-workspace-options';
import { setupCI } from './utils/ci/setup-ci';
import { mapErrorToBodyLines } from './utils/error-utils';
import {
initializeGitRepo,
pushToGitHub,
VcsPushStatus,
} from './utils/git/git';
import {
connectToNxCloudForTemplate,
createNxCloudOnboardingUrl,
getNxCloudInfo,
getSkippedNxCloudInfo,
readNxCloudToken,
} from './utils/nx/nx-cloud';
import { output } from './utils/output';
import { getPackageNameFromThirdPartyPreset } from './utils/preset/get-third-party-preset';
import { Preset } from './utils/preset/preset';
import { cloneTemplate } from './utils/template/clone-template';
import {
addConnectUrlToReadme,
amendOrCommitReadme,
} from './utils/template/update-readme';
import { execAndWait } from './utils/child-process-utils';
import {
generatePackageManagerFiles,
getPackageManagerCommand,
} from './utils/package-manager';
import { isAiAgent, logProgress } from './utils/ai/ai-output';
// State for SIGINT handler - only set after workspace is fully installed
let workspaceDirectory: string | undefined;
let cloudConnectUrl: string | undefined;
export function getInterruptedWorkspaceState(): {
directory: string | undefined;
connectUrl: string | undefined;
} {
return { directory: workspaceDirectory, connectUrl: cloudConnectUrl };
}
export async function createWorkspace<T extends CreateWorkspaceOptions>(
preset: string | undefined,
options: T,
rawArgs?: T
) {
const {
packageManager,
name,
nxCloud,
skipGit = false,
defaultBase = 'main',
commit,
cliName,
useGitHub,
skipGitHubPush = false,
verbose = false,
} = options;
if (cliName) {
output.setCliName(cliName ?? 'NX');
}
let directory: string;
if (options.template) {
if (!options.template.startsWith('nrwl/'))
throw new Error(
`Invalid template. Only templates from the 'nrwl' GitHub org are supported.`
);
const templateUrl = `https://github.com/${options.template}`;
const workingDir = process.cwd().replace(/\\/g, '/');
directory = join(workingDir, name);
const aiMode = isAiAgent();
// Use spinner for human mode, progress logs for AI mode
let workspaceSetupSpinner: any;
if (aiMode) {
logProgress('cloning', `Cloning template ${options.template}...`);
} else {
const ora = require('ora');
workspaceSetupSpinner = ora(`Creating workspace from template`).start();
}
try {
await cloneTemplate(templateUrl, name);
// Remove npm lockfile from template since we'll generate the correct one
const npmLockPath = join(directory, 'package-lock.json');
if (existsSync(npmLockPath)) {
unlinkSync(npmLockPath);
}
// Generate package manager specific files (e.g., .yarnrc.yml for Yarn Berry)
generatePackageManagerFiles(directory, packageManager);
// Install dependencies with the user's package manager
if (aiMode) {
logProgress(
'installing',
`Installing dependencies with ${packageManager}...`
);
}
const pmc = getPackageManagerCommand(packageManager);
if (pmc.preInstall) {
await execAndWait(pmc.preInstall, directory);
}
await execAndWait(pmc.install, directory);
// Mark workspace as ready for SIGINT handler
workspaceDirectory = directory;
if (aiMode) {
logProgress(
'configuring',
`Successfully created the workspace: ${directory}`
);
} else {
workspaceSetupSpinner.succeed(
`Successfully created the workspace: ${directory}`
);
}
} catch (e) {
if (!aiMode) {
workspaceSetupSpinner.fail();
}
throw e;
}
// Connect to Nx Cloud for template flow
// For variant 1 (NXC-3628): Skip connection, use GitHub flow for URL generation
if (nxCloud !== 'skip' && !options.skipCloudConnect) {
await connectToNxCloudForTemplate(
directory,
'create-nx-workspace',
useGitHub
);
}
} else {
// Preset flow - existing behavior
if (!preset) {
throw new Error(
'Preset is required when not using a template. Please provide --preset or --template.'
);
}
const tmpDir = await createSandbox(packageManager);
const workspaceGlobs = getWorkspaceGlobsFromPreset(preset);
// nx new requires a preset currently. We should probably make it optional.
directory = await createEmptyWorkspace<T>(tmpDir, name, packageManager, {
...options,
preset,
workspaceGlobs,
// We want new workspaces to have a short URL to finish Cloud onboarding, but not have nxCloudId set up since it will be handled as part of the onboarding flow.
// This is skipping nxCloudId for the "custom" flow.
nxCloud: 'skip',
});
// Mark workspace as ready for SIGINT handler
workspaceDirectory = directory;
// If the preset is a third-party preset, we need to call createPreset to install it
// For first-party presets, it will be created by createEmptyWorkspace instead.
// In createEmptyWorkspace, it will call `nx new` -> `@nx/workspace newGenerator` -> `@nx/workspace generatePreset`.
const thirdPartyPackageName = getPackageNameFromThirdPartyPreset(preset);
if (thirdPartyPackageName) {
await createPreset(
thirdPartyPackageName,
options,
packageManager,
directory
);
}
}
const isTemplate = !!options.template;
// Generate CI for preset flow (not template)
// When nxCloud === 'yes' (from simplified prompt), use GitHub as the CI provider
if (nxCloud !== 'skip' && !isTemplate) {
const ciProvider = nxCloud === 'yes' ? 'github' : nxCloud;
await setupCI(directory, ciProvider, packageManager);
}
let pushedToVcs = VcsPushStatus.SkippedGit;
if (!skipGit) {
const aiMode = isAiAgent();
if (aiMode) {
logProgress('initializing', 'Initializing git repository...');
}
try {
await initializeGitRepo(directory, { defaultBase, commit });
// Push to GitHub if commit was made, GitHub push is not skipped, and:
// - CI provider is GitHub (preset flow with CLI arg), OR
// - Nx Cloud enabled via simplified prompt (nxCloud === 'yes')
if (
commit &&
!skipGitHubPush &&
(nxCloud === 'github' || nxCloud === 'yes')
) {
pushedToVcs = await pushToGitHub(directory, {
skipGitHubPush,
name,
defaultBase,
verbose,
});
}
} catch (e) {
if (e instanceof Error) {
if (!aiMode) {
output.error({
title: 'Could not initialize git repository',
bodyLines: mapErrorToBodyLines(e),
});
}
// In AI mode, error will be handled by the caller
} else {
console.error(e);
}
}
}
// Create onboarding URL AFTER git operations so getVcsRemoteInfo() can detect the repo
let connectUrl: string | undefined;
let nxCloudInfo: string | undefined;
if (nxCloud !== 'skip') {
const aiModeForCloud = isAiAgent();
if (aiModeForCloud) {
logProgress('configuring', 'Configuring Nx Cloud...');
}
// For variant 1 (skipCloudConnect=true): Skip readNxCloudToken() entirely
// - We didn't call connectToNxCloudForTemplate(), so no token exists
// - The spinner message "Checking Nx Cloud setup" would be misleading
// - createNxCloudOnboardingUrl() uses GitHub flow which sends accessToken: null
//
// For variant 0: Read the token as before (cloud was connected)
const token = options.skipCloudConnect
? undefined
: readNxCloudToken(directory);
connectUrl = await createNxCloudOnboardingUrl(
nxCloud,
token,
directory,
useGitHub
);
// Store for SIGINT handler
cloudConnectUrl = connectUrl;
// Update README with connect URL (strips markers, adds connect section)
// Then commit the change - amend if not pushed, new commit if already pushed
if (isTemplate) {
const readmeUpdated = addConnectUrlToReadme(directory, connectUrl);
if (readmeUpdated && !skipGit && commit) {
const alreadyPushed = pushedToVcs === VcsPushStatus.PushedToVcs;
await amendOrCommitReadme(directory, alreadyPushed);
}
}
nxCloudInfo = await getNxCloudInfo(
connectUrl,
pushedToVcs,
options.completionMessageKey,
name
);
} else if (isTemplate && nxCloud === 'skip') {
// Strip marker comments from README even when cloud is skipped
// so users don't see raw <!-- BEGIN/END: nx-cloud --> markers
const readmeUpdated = addConnectUrlToReadme(directory, undefined);
if (readmeUpdated && !skipGit && commit) {
const alreadyPushed = pushedToVcs === VcsPushStatus.PushedToVcs;
await amendOrCommitReadme(directory, alreadyPushed);
}
// Show nx connect message when user skips cloud in template flow
nxCloudInfo = getSkippedNxCloudInfo();
}
return {
nxCloudInfo,
directory,
pushedToVcs,
connectUrl,
};
}
export function extractConnectUrl(text: string): string | null {
const urlPattern = /(https:\/\/[^\s]+\/connect\/[^\s]+)/g;
const match = text.match(urlPattern);
return match ? match[0] : null;
}
function getWorkspaceGlobsFromPreset(preset: string): string[] {
// Should match how apps are created in `packages/workspace/src/generators/preset/preset.ts`.
switch (preset) {
case Preset.AngularMonorepo:
case Preset.Expo:
case Preset.Express:
case Preset.Nest:
case Preset.NextJs:
case Preset.NodeMonorepo:
case Preset.Nuxt:
case Preset.ReactNative:
case Preset.ReactMonorepo:
case Preset.VueMonorepo:
case Preset.WebComponents:
return ['apps/*'];
default:
return ['packages/*'];
}
}